Skip to main content

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