Skip to main content

hypercall/observability/metrics_collector/
trading.rs

1use super::*;
2
3impl MetricsCollector {
4    // ===== Trading Metrics =====
5
6    pub(super) async fn collect_trading_metrics(&self) {
7        // Get all portfolios from cache
8        let portfolios = self.portfolio_cache.get_all_portfolios().await;
9
10        let mut total_positions: i64 = 0;
11        let mut accounts_with_positions: i64 = 0;
12        let mut total_realized_pnl: f64 = 0.0;
13        let mut total_unrealized_pnl: f64 = 0.0;
14        let mut total_notional_usd: f64 = 0.0;
15        let mut conversion_failures: i64 = 0;
16
17        // Open interest by underlying
18        let mut open_interest_by_underlying: HashMap<String, f64> = HashMap::new();
19
20        for portfolio in portfolios.values() {
21            let positions = &portfolio.positions;
22            if !positions.is_empty() {
23                accounts_with_positions += 1;
24            }
25
26            for position in positions.values() {
27                total_positions += 1;
28
29                let Some(amount) = position.amount.to_f64() else {
30                    warn!(
31                        symbol = %position.symbol,
32                        amount = %position.amount,
33                        "Failed to convert position amount for trading metrics"
34                    );
35                    conversion_failures += 1;
36                    continue;
37                };
38                let Some(entry_price) = position.entry_price.to_f64() else {
39                    warn!(
40                        symbol = %position.symbol,
41                        entry_price = %position.entry_price,
42                        "Failed to convert position entry price for trading metrics"
43                    );
44                    conversion_failures += 1;
45                    continue;
46                };
47                let Some(realized) = position.realized_pnl.to_f64() else {
48                    warn!(
49                        symbol = %position.symbol,
50                        realized_pnl = %position.realized_pnl,
51                        "Failed to convert realized PnL for trading metrics"
52                    );
53                    conversion_failures += 1;
54                    continue;
55                };
56                let Some(unrealized) = position.unrealized_pnl.to_f64() else {
57                    warn!(
58                        symbol = %position.symbol,
59                        unrealized_pnl = %position.unrealized_pnl,
60                        "Failed to convert unrealized PnL for trading metrics"
61                    );
62                    conversion_failures += 1;
63                    continue;
64                };
65
66                total_realized_pnl += realized;
67                total_unrealized_pnl += unrealized;
68                total_notional_usd += (amount * entry_price).abs();
69
70                // Extract underlying from symbol (e.g., "BTC-20260115-100000-C" -> "BTC")
71                let underlying = position
72                    .symbol
73                    .split('-')
74                    .next()
75                    .unwrap_or("UNKNOWN")
76                    .to_string();
77
78                *open_interest_by_underlying.entry(underlying).or_insert(0.0) += amount.abs();
79            }
80        }
81
82        // Export trading metrics
83        // Note: Using "ht_positions" not "ht_positions_total" as _total suffix is reserved for counters
84        gauge!("ht_positions").set(total_positions as f64);
85        gauge!("ht_active_accounts").set(accounts_with_positions as f64);
86        gauge!("ht_realized_pnl_usd").set(total_realized_pnl);
87        gauge!("ht_unrealized_pnl_usd").set(total_unrealized_pnl);
88        gauge!("ht_position_notional_usd").set(total_notional_usd);
89        gauge!("ht_trading_metrics_conversion_failures").set(conversion_failures as f64);
90        gauge!("ht_trading_metrics_complete").set(if conversion_failures == 0 { 1.0 } else { 0.0 });
91
92        // Open interest per underlying
93        for (underlying, oi) in open_interest_by_underlying {
94            gauge!("ht_open_interest", "underlying" => underlying).set(oi);
95        }
96
97        debug!(
98            "Trading metrics: {} positions, {} accounts, ${:.0} notional, {} conversion failures",
99            total_positions, accounts_with_positions, total_notional_usd, conversion_failures
100        );
101    }
102
103    // ===== Market Stats Metrics (from MarketStatsCache) =====
104
105    /// Collect aggregate metrics from MarketStatsCache.
106    /// This provides accurate 24h rolling volume (event-sourced from fills).
107    pub(super) async fn collect_market_stats_metrics(&self) {
108        let Some(ref market_stats) = self.market_stats_cache else {
109            return;
110        };
111
112        // Get all per-symbol stats
113        let all_stats = market_stats.get_all_stats().await;
114
115        let mut total_volume_24h: f64 = 0.0;
116        let mut total_open_interest: f64 = 0.0;
117        let mut volume_by_underlying: HashMap<String, f64> = HashMap::new();
118
119        for (symbol, (volume, oi)) in &all_stats {
120            // Aggregate totals
121            if let Some(v) = volume.to_f64() {
122                total_volume_24h += v;
123
124                // Extract underlying from symbol (e.g., "BTC-20251231-100000-C" -> "BTC")
125                let underlying = symbol.split('-').next().unwrap_or("UNKNOWN").to_string();
126                *volume_by_underlying.entry(underlying).or_insert(0.0) += v;
127            }
128            if let Some(o) = oi.to_f64() {
129                total_open_interest += o;
130            }
131        }
132
133        // Export aggregate metrics
134        gauge!("ht_volume_24h_total_usd").set(total_volume_24h);
135        gauge!("ht_open_interest_total").set(total_open_interest);
136
137        // Per-underlying volume for breakdown
138        for (underlying, vol) in volume_by_underlying {
139            gauge!("ht_volume_24h_usd", "underlying" => underlying).set(vol);
140        }
141
142        debug!(
143            "Market stats: ${:.0} 24h volume, {:.2} total OI ({} symbols)",
144            total_volume_24h,
145            total_open_interest,
146            all_stats.len()
147        );
148    }
149}