tranc-cli 0.1.1

Tranc CLI — trade indicator queries from the command line.
//! `tranc indicator` subcommand — single indicator query.
//!
//! Examples:
//!   tranc indicator rsi BTC-USD --tf 5m --period 14
//!   tranc indicator macd ETH-USD --tf 1h
//!   tranc indicator bb SOL-USD --tf 15m --pretty
//!   tranc indicator donchian BTC-USD --tf 1h --period 20
//!
//! Supported indicators (matches the v1 set from README §5):
//!   sma, ema, wma, rsi, macd, bb, atr, adx, stoch, vwap, obv,
//!   ichimoku, supertrend, pivot, fib, volume_profile,
//!   donchian, keltner, cci, psar

use anyhow::Result;
use clap::Parser;

use crate::client::{build_client, get_json};
use crate::config::canonical_base_url;
use crate::output::print_json;

/// `tranc indicator` — fetch a single indicator value.
#[derive(Debug, Parser)]
pub struct IndicatorCmd {
    /// Indicator name (rsi, macd, bb, ema, sma, atr, adx, stoch, vwap, obv,
    /// ichimoku, supertrend, pivot, fib, volume_profile, donchian, keltner,
    /// cci, psar).
    pub name: String,

    /// Symbol (e.g. BTC-USD, ETH-USD, EUR-USD).
    pub symbol: String,

    /// Timeframe (1m, 5m, 15m, 1h, 4h, 1d). Default: 5m.
    #[arg(long, default_value = "5m")]
    pub tf: String,

    /// Primary period parameter (period / length / window). Default: indicator-specific.
    #[arg(long)]
    pub period: Option<u32>,

    /// Response format: verbose (default) or compact (strips hint_for_llm / context).
    #[arg(long, default_value = "verbose")]
    pub format: String,

    /// Exchange override (binance, coinbase, kraken, bybit, okx, oanda).
    #[arg(long)]
    pub exchange: Option<String>,
}

impl IndicatorCmd {
    pub async fn run(self, api_url: &str, pretty: bool) -> Result<()> {
        let base = canonical_base_url(api_url);
        let (client, token) = build_client()?;

        let mut params: Vec<(&str, String)> = vec![
            ("symbol", self.symbol.to_uppercase()),
            ("tf", self.tf.clone()),
            ("format", self.format.clone()),
        ];

        if let Some(p) = self.period {
            params.push(("period", p.to_string()));
        }

        if let Some(ex) = &self.exchange {
            params.push(("exchange", ex.clone()));
        }

        let url = format!("{base}/v1/indicator/{}", self.name.to_lowercase());
        let rb = client.get(&url).bearer_auth(&token).query(&params);
        let json = get_json(rb).await?;
        print_json(&json, pretty)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use clap::Parser;

    #[test]
    fn parse_rsi_minimal() {
        let cmd = IndicatorCmd::try_parse_from(["indicator", "rsi", "BTC-USD"]).unwrap();
        assert_eq!(cmd.name, "rsi");
        assert_eq!(cmd.symbol, "BTC-USD");
        assert_eq!(cmd.tf, "5m");
        assert!(cmd.period.is_none());
    }

    #[test]
    fn parse_rsi_with_period() {
        let cmd = IndicatorCmd::try_parse_from([
            "indicator",
            "rsi",
            "ETH-USD",
            "--tf",
            "1h",
            "--period",
            "21",
        ])
        .unwrap();
        assert_eq!(cmd.tf, "1h");
        assert_eq!(cmd.period, Some(21));
    }

    #[test]
    fn parse_macd_with_exchange() {
        let cmd = IndicatorCmd::try_parse_from([
            "indicator",
            "macd",
            "BTC-USD",
            "--exchange",
            "coinbase",
        ])
        .unwrap();
        assert_eq!(cmd.name, "macd");
        assert_eq!(cmd.exchange.as_deref(), Some("coinbase"));
    }

    #[test]
    fn parse_compact_format() {
        let cmd =
            IndicatorCmd::try_parse_from(["indicator", "rsi", "BTC-USD", "--format", "compact"])
                .unwrap();
        assert_eq!(cmd.format, "compact");
    }

    /// All supported v1 indicator names can be parsed.
    #[test]
    fn all_indicator_names_parse() {
        let names = [
            "sma",
            "ema",
            "rsi",
            "macd",
            "bb",
            "atr",
            "adx",
            "stoch",
            "vwap",
            "obv",
            "ichimoku",
            "supertrend",
            "pivot",
            "fib",
            "volume_profile",
            "donchian",
            "keltner",
            "cci",
            "psar",
        ];
        for name in names {
            let cmd = IndicatorCmd::try_parse_from(["indicator", name, "BTC-USD"])
                .unwrap_or_else(|e| panic!("failed to parse '{name}': {e}"));
            assert_eq!(cmd.name, name);
        }
    }
}