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