use clap::{Args, Subcommand};
use tracing::instrument;
use volumeleaders_client::{
AlertConfigsRequest, DeleteAlertConfigRequest, SaveAlertConfigFields, SaveAlertConfigRequest,
};
use crate::cli::AlertArgs;
use crate::common::auth::{handle_api_error, make_client};
use crate::common::types::OutputFormat;
use crate::output::{finish_output, print_json, print_records};
const DEFAULT_CONFIGS_FIELDS: [&str; 9] = [
"AlertConfigKey",
"Name",
"Tickers",
"TradeConditions",
"ClosingTradeConditions",
"DarkPool",
"Sweep",
"OffsettingPrint",
"PhantomPrint",
];
#[derive(Debug, Subcommand)]
pub enum AlertCommand {
Configs(ConfigsArgs),
Create(CreateArgs),
Edit(EditArgs),
Delete(DeleteArgs),
}
#[derive(Debug, Args)]
pub struct ConfigsArgs {
#[arg(long, value_enum, default_value = "json")]
pub format: OutputFormat,
#[arg(long, conflicts_with = "all_fields")]
pub fields: Option<String>,
#[arg(long)]
pub all_fields: bool,
}
#[derive(Debug, Args)]
pub struct CreateArgs {
#[arg(long)]
pub name: String,
#[arg(long)]
pub ticker_group: Option<String>,
#[arg(long, default_value = "")]
pub tickers: String,
#[arg(long, default_value = "0")]
pub trade_rank_lte: i64,
#[arg(long, default_value = "0")]
pub trade_vcd_gte: i64,
#[arg(long, default_value = "0")]
pub trade_mult_gte: i64,
#[arg(long, default_value = "0")]
pub trade_volume_gte: i64,
#[arg(long, default_value = "0")]
pub trade_dollars_gte: i64,
#[arg(long, default_value = "0")]
pub trade_conditions: String,
#[arg(long, default_value = "false")]
pub dark_pool: bool,
#[arg(long, default_value = "false")]
pub sweep: bool,
#[arg(long, default_value = "0")]
pub closing_trade_rank_lte: i64,
#[arg(long, default_value = "0")]
pub closing_trade_vcd_gte: i64,
#[arg(long, default_value = "0")]
pub closing_trade_mult_gte: i64,
#[arg(long, default_value = "0")]
pub closing_trade_volume_gte: i64,
#[arg(long, default_value = "0")]
pub closing_trade_dollars_gte: i64,
#[arg(long, default_value = "0")]
pub closing_trade_conditions: String,
#[arg(long, default_value = "0")]
pub cluster_rank_lte: i64,
#[arg(long, default_value = "0")]
pub cluster_vcd_gte: i64,
#[arg(long, default_value = "0")]
pub cluster_mult_gte: i64,
#[arg(long, default_value = "0")]
pub cluster_volume_gte: i64,
#[arg(long, default_value = "0")]
pub cluster_dollars_gte: i64,
#[arg(long, default_value = "0")]
pub total_rank_lte: i64,
#[arg(long, default_value = "0")]
pub total_volume_gte: i64,
#[arg(long, default_value = "0")]
pub total_dollars_gte: i64,
#[arg(long, default_value = "0")]
pub ah_rank_lte: i64,
#[arg(long, default_value = "0")]
pub ah_volume_gte: i64,
#[arg(long, default_value = "0")]
pub ah_dollars_gte: i64,
#[arg(long, default_value = "false")]
pub offsetting_print: bool,
#[arg(long, default_value = "false")]
pub phantom_print: bool,
}
#[derive(Debug, Args)]
pub struct EditArgs {
#[arg(long)]
pub key: i64,
#[arg(long)]
pub name: Option<String>,
#[arg(long)]
pub ticker_group: Option<String>,
#[arg(long, default_value = "")]
pub tickers: String,
#[arg(long, default_value = "0")]
pub trade_rank_lte: i64,
#[arg(long, default_value = "0")]
pub trade_vcd_gte: i64,
#[arg(long, default_value = "0")]
pub trade_mult_gte: i64,
#[arg(long, default_value = "0")]
pub trade_volume_gte: i64,
#[arg(long, default_value = "0")]
pub trade_dollars_gte: i64,
#[arg(long, default_value = "0")]
pub trade_conditions: String,
#[arg(long, default_value = "false")]
pub dark_pool: bool,
#[arg(long, default_value = "false")]
pub sweep: bool,
#[arg(long, default_value = "0")]
pub closing_trade_rank_lte: i64,
#[arg(long, default_value = "0")]
pub closing_trade_vcd_gte: i64,
#[arg(long, default_value = "0")]
pub closing_trade_mult_gte: i64,
#[arg(long, default_value = "0")]
pub closing_trade_volume_gte: i64,
#[arg(long, default_value = "0")]
pub closing_trade_dollars_gte: i64,
#[arg(long, default_value = "0")]
pub closing_trade_conditions: String,
#[arg(long, default_value = "0")]
pub cluster_rank_lte: i64,
#[arg(long, default_value = "0")]
pub cluster_vcd_gte: i64,
#[arg(long, default_value = "0")]
pub cluster_mult_gte: i64,
#[arg(long, default_value = "0")]
pub cluster_volume_gte: i64,
#[arg(long, default_value = "0")]
pub cluster_dollars_gte: i64,
#[arg(long, default_value = "0")]
pub total_rank_lte: i64,
#[arg(long, default_value = "0")]
pub total_volume_gte: i64,
#[arg(long, default_value = "0")]
pub total_dollars_gte: i64,
#[arg(long, default_value = "0")]
pub ah_rank_lte: i64,
#[arg(long, default_value = "0")]
pub ah_volume_gte: i64,
#[arg(long, default_value = "0")]
pub ah_dollars_gte: i64,
#[arg(long, default_value = "false")]
pub offsetting_print: bool,
#[arg(long, default_value = "false")]
pub phantom_print: bool,
}
#[derive(Debug, Args)]
pub struct DeleteArgs {
#[arg(long)]
pub key: i64,
}
#[instrument(skip_all)]
pub async fn handle(args: &AlertArgs, pretty: bool) -> i32 {
match &args.command {
AlertCommand::Configs(a) => execute_configs(a, pretty).await,
AlertCommand::Create(a) => execute_create(a, pretty).await,
AlertCommand::Edit(a) => execute_edit(a, pretty).await,
AlertCommand::Delete(a) => execute_delete(a, pretty).await,
}
}
#[instrument(skip_all)]
async fn execute_configs(args: &ConfigsArgs, pretty: bool) -> i32 {
let client = match make_client().await {
Ok(c) => c,
Err(code) => return code,
};
let request = AlertConfigsRequest::new();
let configs = match client.get_alert_configs_limit(&request, usize::MAX).await {
Ok(c) => c,
Err(err) => return handle_api_error(err),
};
finish_output(print_records(
&configs,
args.format,
pretty,
&DEFAULT_CONFIGS_FIELDS,
args.fields.as_deref(),
args.all_fields,
))
}
#[instrument(skip_all)]
async fn execute_create(args: &CreateArgs, pretty: bool) -> i32 {
let client = match make_client().await {
Ok(c) => c,
Err(code) => return code,
};
let request = build_create_request(args);
if let Err(err) = client.save_alert_config(request).await {
return handle_api_error(err);
}
let result = serde_json::json!({"success": true, "action": "created", "key": 0});
finish_output(print_json(&result, pretty))
}
#[instrument(skip_all)]
async fn execute_edit(args: &EditArgs, pretty: bool) -> i32 {
let client = match make_client().await {
Ok(c) => c,
Err(code) => return code,
};
let request = build_edit_request(args);
if let Err(err) = client.save_alert_config(request).await {
return handle_api_error(err);
}
let result = serde_json::json!({"success": true, "action": "updated", "key": args.key});
finish_output(print_json(&result, pretty))
}
#[instrument(skip_all)]
async fn execute_delete(args: &DeleteArgs, pretty: bool) -> i32 {
let client = match make_client().await {
Ok(c) => c,
Err(code) => return code,
};
let request = DeleteAlertConfigRequest {
alert_config_key: args.key,
};
if let Err(err) = client.delete_alert_config(&request).await {
return handle_api_error(err);
}
let result = serde_json::json!({"success": true, "action": "deleted", "key": args.key});
finish_output(print_json(&result, pretty))
}
fn build_create_request(args: &CreateArgs) -> SaveAlertConfigRequest {
let ticker_group = resolve_ticker_group(args.ticker_group.as_deref(), &args.tickers);
SaveAlertConfigRequest::from_config(SaveAlertConfigFields {
alert_config_key: 0,
name: args.name.clone(),
ticker_group,
tickers: args.tickers.clone(),
trade_rank_lte: args.trade_rank_lte,
trade_vcd_gte: args.trade_vcd_gte,
trade_mult_gte: args.trade_mult_gte,
trade_volume_gte: args.trade_volume_gte,
trade_dollars_gte: args.trade_dollars_gte,
trade_conditions: args.trade_conditions.clone(),
dark_pool: args.dark_pool,
sweep: args.sweep,
closing_trade_rank_lte: args.closing_trade_rank_lte,
closing_trade_vcd_gte: args.closing_trade_vcd_gte,
closing_trade_mult_gte: args.closing_trade_mult_gte,
closing_trade_volume_gte: args.closing_trade_volume_gte,
closing_trade_dollars_gte: args.closing_trade_dollars_gte,
closing_trade_conditions: args.closing_trade_conditions.clone(),
cluster_rank_lte: args.cluster_rank_lte,
cluster_vcd_gte: args.cluster_vcd_gte,
cluster_mult_gte: args.cluster_mult_gte,
cluster_volume_gte: args.cluster_volume_gte,
cluster_dollars_gte: args.cluster_dollars_gte,
total_rank_lte: args.total_rank_lte,
total_volume_gte: args.total_volume_gte,
total_dollars_gte: args.total_dollars_gte,
ah_rank_lte: args.ah_rank_lte,
ah_volume_gte: args.ah_volume_gte,
ah_dollars_gte: args.ah_dollars_gte,
offsetting_print: args.offsetting_print,
phantom_print: args.phantom_print,
})
}
fn build_edit_request(args: &EditArgs) -> SaveAlertConfigRequest {
let ticker_group = resolve_ticker_group(args.ticker_group.as_deref(), &args.tickers);
let name = args.name.clone().unwrap_or_default();
SaveAlertConfigRequest::from_config(SaveAlertConfigFields {
alert_config_key: args.key,
name,
ticker_group,
tickers: args.tickers.clone(),
trade_rank_lte: args.trade_rank_lte,
trade_vcd_gte: args.trade_vcd_gte,
trade_mult_gte: args.trade_mult_gte,
trade_volume_gte: args.trade_volume_gte,
trade_dollars_gte: args.trade_dollars_gte,
trade_conditions: args.trade_conditions.clone(),
dark_pool: args.dark_pool,
sweep: args.sweep,
closing_trade_rank_lte: args.closing_trade_rank_lte,
closing_trade_vcd_gte: args.closing_trade_vcd_gte,
closing_trade_mult_gte: args.closing_trade_mult_gte,
closing_trade_volume_gte: args.closing_trade_volume_gte,
closing_trade_dollars_gte: args.closing_trade_dollars_gte,
closing_trade_conditions: args.closing_trade_conditions.clone(),
cluster_rank_lte: args.cluster_rank_lte,
cluster_vcd_gte: args.cluster_vcd_gte,
cluster_mult_gte: args.cluster_mult_gte,
cluster_volume_gte: args.cluster_volume_gte,
cluster_dollars_gte: args.cluster_dollars_gte,
total_rank_lte: args.total_rank_lte,
total_volume_gte: args.total_volume_gte,
total_dollars_gte: args.total_dollars_gte,
ah_rank_lte: args.ah_rank_lte,
ah_volume_gte: args.ah_volume_gte,
ah_dollars_gte: args.ah_dollars_gte,
offsetting_print: args.offsetting_print,
phantom_print: args.phantom_print,
})
}
fn resolve_ticker_group(explicit: Option<&str>, tickers: &str) -> String {
match explicit {
Some(group) => group.to_string(),
None if !tickers.is_empty() => "SelectedTickers".to_string(),
None => "AllTickers".to_string(),
}
}
#[cfg(test)]
mod tests {
use clap::{CommandFactory, Parser};
use crate::cli::Cli;
use super::*;
#[test]
fn cli_alert_command_has_four_subcommands() {
let command = Cli::command();
let alert = command.find_subcommand("alert").expect("alert command");
let names: Vec<_> = alert
.get_subcommands()
.map(|cmd| cmd.get_name().to_string())
.collect();
assert_eq!(names, vec!["configs", "create", "edit", "delete"]);
}
#[test]
fn edit_requires_key_flag() {
let result = Cli::try_parse_from(["volumeleaders-agent", "alert", "edit"]);
assert!(result.is_err(), "edit without --key should fail");
let result = Cli::try_parse_from(["volumeleaders-agent", "alert", "edit", "--key", "42"]);
assert!(result.is_ok(), "edit with --key should succeed");
}
#[test]
fn build_create_request_auto_selects_ticker_group() {
let args = CreateArgs {
name: "Test Alert".to_string(),
ticker_group: None,
tickers: "AAPL,MSFT".to_string(),
trade_rank_lte: 0,
trade_vcd_gte: 0,
trade_mult_gte: 0,
trade_volume_gte: 0,
trade_dollars_gte: 0,
trade_conditions: "0".to_string(),
dark_pool: true,
sweep: false,
closing_trade_rank_lte: 0,
closing_trade_vcd_gte: 0,
closing_trade_mult_gte: 0,
closing_trade_volume_gte: 0,
closing_trade_dollars_gte: 0,
closing_trade_conditions: "0".to_string(),
cluster_rank_lte: 0,
cluster_vcd_gte: 0,
cluster_mult_gte: 0,
cluster_volume_gte: 0,
cluster_dollars_gte: 0,
total_rank_lte: 0,
total_volume_gte: 0,
total_dollars_gte: 0,
ah_rank_lte: 0,
ah_volume_gte: 0,
ah_dollars_gte: 0,
offsetting_print: true,
phantom_print: false,
};
let request = build_create_request(&args);
let fields = request.fields();
assert_eq!(fields[0], ("AlertConfigKey".into(), "0".into()));
assert_eq!(fields[1], ("Name".into(), "Test Alert".into()));
assert_eq!(fields[2], ("TickerGroup".into(), "SelectedTickers".into()));
assert_eq!(fields[3], ("Tickers".into(), "AAPL,MSFT".into()));
let dark_pool_entries: Vec<_> = fields.iter().filter(|(k, _)| k == "DarkPool").collect();
assert_eq!(dark_pool_entries.len(), 2);
assert_eq!(dark_pool_entries[0].1, "true");
assert_eq!(dark_pool_entries[1].1, "false");
let sweep_entries: Vec<_> = fields.iter().filter(|(k, _)| k == "Sweep").collect();
assert_eq!(sweep_entries.len(), 1);
assert_eq!(sweep_entries[0].1, "false");
let op_entries: Vec<_> = fields
.iter()
.filter(|(k, _)| k == "OffsettingPrint")
.collect();
assert_eq!(op_entries.len(), 2);
assert_eq!(op_entries[0].1, "true");
assert_eq!(op_entries[1].1, "false");
let pp_entries: Vec<_> = fields.iter().filter(|(k, _)| k == "PhantomPrint").collect();
assert_eq!(pp_entries.len(), 1);
assert_eq!(pp_entries[0].1, "false");
}
}