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 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 UiProjection {
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 UiProjection {
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                let pnl = state.asset_pnl_by_symbol.get(&symbol);
100                let inferred_qty = state
101                    .balances
102                    .get(&symbol)
103                    .copied()
104                    .or_else(|| {
105                        let (base, _) = split_symbol_assets(&symbol);
106                        if base.is_empty() {
107                            None
108                        } else {
109                            state.balances.get(&base).copied()
110                        }
111                    })
112                    .unwrap_or(0.0);
113                AssetEntry {
114                    symbol: symbol.clone(),
115                    last_price: if symbol == state.symbol {
116                        state.last_price()
117                    } else {
118                        None
119                    },
120                    position_qty: pnl.map(|p| p.position_qty).unwrap_or_else(|| {
121                        if symbol == state.symbol {
122                            state.position.qty
123                        } else {
124                            inferred_qty
125                        }
126                    }),
127                    realized_pnl_usdt: pnl.map(|p| p.realized_pnl_usdt).unwrap_or_else(|| {
128                        if symbol == state.symbol {
129                            state.history_realized_pnl
130                        } else {
131                            0.0
132                        }
133                    }),
134                    unrealized_pnl_usdt: pnl.map(|p| p.unrealized_pnl_usdt).unwrap_or_else(|| {
135                        if symbol == state.symbol {
136                            state.position.unrealized_pnl
137                        } else {
138                            0.0
139                        }
140                    }),
141                }
142            })
143            .collect();
144
145        Self {
146            portfolio: PortfolioSummary {
147                total_equity_usdt: state.current_equity_usdt,
148                total_realized_pnl_usdt: state.history_realized_pnl,
149                total_unrealized_pnl_usdt: state.position.unrealized_pnl,
150                ws_connected: state.ws_connected,
151            },
152            assets,
153            strategies: strategy_rows,
154            matrix: matrix_rows,
155            focus: FocusState {
156                symbol: Some(state.symbol.clone()),
157                strategy_id: Some(state.strategy_label.clone()),
158            },
159        }
160    }
161
162    pub fn strategy_lookup(&self) -> HashMap<String, StrategyEntry> {
163        self.strategies
164            .iter()
165            .cloned()
166            .map(|s| (s.strategy_id.clone(), s))
167            .collect()
168    }
169}
170
171fn split_symbol_assets(symbol: &str) -> (String, String) {
172    const QUOTE_SUFFIXES: [&str; 10] = [
173        "USDT", "USDC", "FDUSD", "BUSD", "TUSD", "TRY", "EUR", "BTC", "ETH", "BNB",
174    ];
175    for q in QUOTE_SUFFIXES {
176        if let Some(base) = symbol.strip_suffix(q) {
177            if !base.is_empty() {
178                return (base.to_string(), q.to_string());
179            }
180        }
181    }
182    (String::new(), String::new())
183}