Skip to main content

sandbox_quant/ui/
app_state_v2.rs

1use std::collections::HashMap;
2
3use super::AppState;
4
5#[derive(Debug, Clone, Default)]
6pub struct PortfolioSummary {
7    pub total_equity_usdt: Option<f64>,
8    pub total_realized_pnl_usdt: f64,
9    pub total_unrealized_pnl_usdt: f64,
10    pub ws_connected: bool,
11}
12
13#[derive(Debug, Clone, Default)]
14pub struct AssetEntry {
15    pub symbol: String,
16    pub last_price: Option<f64>,
17    pub position_qty: f64,
18    pub realized_pnl_usdt: f64,
19    pub unrealized_pnl_usdt: f64,
20}
21
22#[derive(Debug, Clone, Default)]
23pub struct StrategyEntry {
24    pub strategy_id: String,
25    pub trade_count: u32,
26    pub win_count: u32,
27    pub lose_count: u32,
28    pub realized_pnl_usdt: f64,
29}
30
31#[derive(Debug, Clone, Default)]
32pub struct MatrixCell {
33    pub symbol: String,
34    pub strategy_id: String,
35    pub trade_count: u32,
36    pub realized_pnl_usdt: f64,
37}
38
39#[derive(Debug, Clone, Default)]
40pub struct FocusState {
41    pub symbol: Option<String>,
42    pub strategy_id: Option<String>,
43}
44
45#[derive(Debug, Clone, Default)]
46pub struct AppStateV2 {
47    pub portfolio: PortfolioSummary,
48    pub assets: Vec<AssetEntry>,
49    pub strategies: Vec<StrategyEntry>,
50    pub matrix: Vec<MatrixCell>,
51    pub focus: FocusState,
52}
53
54impl AppStateV2 {
55    pub fn new() -> Self {
56        Self::default()
57    }
58
59    pub fn from_legacy(state: &AppState) -> Self {
60        let mut strategy_rows = Vec::new();
61        let mut matrix_rows = Vec::new();
62        for (strategy_id, stats) in &state.strategy_stats {
63            strategy_rows.push(StrategyEntry {
64                strategy_id: strategy_id.clone(),
65                trade_count: stats.trade_count,
66                win_count: stats.win_count,
67                lose_count: stats.lose_count,
68                realized_pnl_usdt: stats.realized_pnl,
69            });
70            matrix_rows.push(MatrixCell {
71                symbol: state.symbol.clone(),
72                strategy_id: strategy_id.clone(),
73                trade_count: stats.trade_count,
74                realized_pnl_usdt: stats.realized_pnl,
75            });
76        }
77        strategy_rows.sort_by(|a, b| a.strategy_id.cmp(&b.strategy_id));
78        matrix_rows.sort_by(|a, b| {
79            a.symbol
80                .cmp(&b.symbol)
81                .then_with(|| a.strategy_id.cmp(&b.strategy_id))
82        });
83        let mut asset_symbols: Vec<String> = state
84            .symbol_items
85            .iter()
86            .cloned()
87            .chain(state.strategy_item_symbols.iter().cloned())
88            .chain(state.balances.keys().cloned())
89            .filter(|s| !s.trim().is_empty())
90            .collect();
91        asset_symbols.sort();
92        asset_symbols.dedup();
93        if asset_symbols.is_empty() {
94            asset_symbols.push(state.symbol.clone());
95        }
96        let assets = asset_symbols
97            .into_iter()
98            .map(|symbol| {
99                if symbol == state.symbol {
100                    AssetEntry {
101                        symbol,
102                        last_price: state.last_price(),
103                        position_qty: state.position.qty,
104                        realized_pnl_usdt: state.history_realized_pnl,
105                        unrealized_pnl_usdt: state.position.unrealized_pnl,
106                    }
107                } else {
108                    let inferred_qty = state
109                        .balances
110                        .get(&symbol)
111                        .copied()
112                        .or_else(|| {
113                            let (base, _) = split_symbol_assets(&symbol);
114                            if base.is_empty() {
115                                None
116                            } else {
117                                state.balances.get(&base).copied()
118                            }
119                        })
120                        .unwrap_or(0.0);
121                    AssetEntry {
122                        symbol,
123                        last_price: None,
124                        position_qty: inferred_qty,
125                        realized_pnl_usdt: 0.0,
126                        unrealized_pnl_usdt: 0.0,
127                    }
128                }
129            })
130            .collect();
131
132        Self {
133            portfolio: PortfolioSummary {
134                total_equity_usdt: state.current_equity_usdt,
135                total_realized_pnl_usdt: state.history_realized_pnl,
136                total_unrealized_pnl_usdt: state.position.unrealized_pnl,
137                ws_connected: state.ws_connected,
138            },
139            assets,
140            strategies: strategy_rows,
141            matrix: matrix_rows,
142            focus: FocusState {
143                symbol: Some(state.symbol.clone()),
144                strategy_id: Some(state.strategy_label.clone()),
145            },
146        }
147    }
148
149    pub fn strategy_lookup(&self) -> HashMap<String, StrategyEntry> {
150        self.strategies
151            .iter()
152            .cloned()
153            .map(|s| (s.strategy_id.clone(), s))
154            .collect()
155    }
156}
157
158fn split_symbol_assets(symbol: &str) -> (String, String) {
159    const QUOTE_SUFFIXES: [&str; 10] = [
160        "USDT", "USDC", "FDUSD", "BUSD", "TUSD", "TRY", "EUR", "BTC", "ETH", "BNB",
161    ];
162    for q in QUOTE_SUFFIXES {
163        if let Some(base) = symbol.strip_suffix(q) {
164            if !base.is_empty() {
165                return (base.to_string(), q.to_string());
166            }
167        }
168    }
169    (String::new(), String::new())
170}