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 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}