sandbox_quant/ui/
ui_projection.rs1use 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}