use clap::{Args, Subcommand};
use tracing::instrument;
use volumeleaders_client::{EarningsRequest, ExhaustionScoresRequest};
use crate::cli::MarketArgs;
use crate::commands::scaffold::run_client_command;
use crate::common::auth::{handle_api_error, make_client};
use crate::common::dates::resolve_date_range;
use crate::output::{finish_output, print_json, print_records};
const DEFAULT_EARNINGS_FIELDS: [&str; 6] = [
"Ticker",
"EarningsDate",
"AfterMarketClose",
"TradeCount",
"TradeClusterCount",
"TradeClusterBombCount",
];
#[derive(Debug, Subcommand)]
pub enum MarketCommand {
Earnings(EarningsArgs),
Exhaustion(ExhaustionArgs),
}
#[derive(Debug, Args)]
pub struct EarningsArgs {
#[arg(long)]
pub start_date: Option<String>,
#[arg(long)]
pub end_date: Option<String>,
#[arg(long)]
pub days: Option<u32>,
#[arg(long, conflicts_with = "all_fields")]
pub fields: Option<String>,
#[arg(long)]
pub all_fields: bool,
}
#[derive(Debug, Args)]
pub struct ExhaustionArgs {
#[arg(long)]
pub date: Option<String>,
}
#[instrument(skip_all)]
pub async fn handle(args: &MarketArgs, pretty: bool) -> i32 {
match &args.command {
MarketCommand::Earnings(args) => execute_earnings(args, pretty).await,
MarketCommand::Exhaustion(args) => execute_exhaustion(args, pretty).await,
}
}
#[instrument(skip_all)]
async fn execute_earnings(args: &EarningsArgs, pretty: bool) -> i32 {
let request = build_earnings_request(args);
let client = match make_client().await {
Ok(client) => client,
Err(code) => return code,
};
let earnings = match client.get_earnings_limit(&request, usize::MAX).await {
Ok(earnings) => earnings,
Err(err) => return handle_api_error(err),
};
finish_output(print_records(
&earnings,
pretty,
&DEFAULT_EARNINGS_FIELDS,
args.fields.as_deref(),
args.all_fields,
))
}
#[instrument(skip_all)]
async fn execute_exhaustion(args: &ExhaustionArgs, pretty: bool) -> i32 {
let request = ExhaustionScoresRequest {
date: args.date.clone().unwrap_or_default(),
};
run_client_command(
move |client| Box::pin(async move { client.get_exhaustion_scores(&request).await }),
move |scores| {
let json = serde_json::to_value(&scores).unwrap_or(serde_json::Value::Null);
print_json(&json, pretty)
},
)
.await
}
fn build_earnings_request(args: &EarningsArgs) -> EarningsRequest {
let (start, end) = resolve_date_range(
args.start_date.as_deref(),
args.end_date.as_deref(),
args.days,
);
EarningsRequest::new().with_date_range(start, end)
}
#[cfg(test)]
mod tests {
use clap::CommandFactory;
use crate::cli::Cli;
use super::{EarningsArgs, build_earnings_request};
#[test]
fn cli_market_command_has_earnings_and_exhaustion_subcommands() {
let command = Cli::command();
let market = command.find_subcommand("market").expect("market command");
let names: Vec<_> = market
.get_subcommands()
.map(|command| command.get_name().to_string())
.collect();
assert_eq!(names, vec!["earnings", "exhaustion"]);
}
#[test]
fn build_earnings_request_sets_date_filters() {
let args = EarningsArgs {
start_date: Some("2025-01-01".to_string()),
end_date: Some("2025-01-15".to_string()),
days: None,
fields: None,
all_fields: false,
};
let request = build_earnings_request(&args);
assert_eq!(
request.extra_values()[0],
("StartDate".to_string(), "2025-01-01".to_string())
);
assert_eq!(
request.extra_values()[1],
("EndDate".to_string(), "2025-01-15".to_string())
);
}
}