|
| 1 | +use std::collections::BTreeMap; |
| 2 | + |
| 3 | +use primitives::{ |
| 4 | + chart::ChartDateValue, |
| 5 | + perpetual::{PerpetualBalance, PerpetualPositionsSummary}, |
| 6 | + portfolio::{PerpetualAccountSummary, PerpetualPortfolio, PerpetualPortfolioTimeframeData}, |
| 7 | +}; |
| 8 | + |
| 9 | +use crate::models::position::AssetPositions; |
| 10 | + |
| 11 | +const HIP3_PERP_ASSET_OFFSET: u32 = 100_000; |
| 12 | +const HIP3_PERP_ASSET_STRIDE: u32 = 10_000; |
| 13 | + |
| 14 | +pub fn perp_asset_index(perp_dex_index: u32, meta_index: u32) -> u32 { |
| 15 | + if perp_dex_index == 0 { |
| 16 | + meta_index |
| 17 | + } else { |
| 18 | + HIP3_PERP_ASSET_OFFSET + perp_dex_index * HIP3_PERP_ASSET_STRIDE + meta_index |
| 19 | + } |
| 20 | +} |
| 21 | + |
| 22 | +pub fn format_display_name(name: &str) -> String { |
| 23 | + match name.split_once(':') { |
| 24 | + Some((dex, symbol)) => format!("{symbol} ({dex})"), |
| 25 | + None => name.to_string(), |
| 26 | + } |
| 27 | +} |
| 28 | + |
| 29 | +pub fn map_account_summary_aggregate(positions: &[AssetPositions]) -> PerpetualAccountSummary { |
| 30 | + let account_value: f64 = positions.iter().map(|p| p.margin_summary.account_value.parse().unwrap_or(0.0)).sum(); |
| 31 | + let total_ntl_pos: f64 = positions.iter().map(|p| p.margin_summary.total_ntl_pos.parse().unwrap_or(0.0)).sum(); |
| 32 | + let total_margin_used: f64 = positions.iter().map(|p| p.margin_summary.total_margin_used.parse().unwrap_or(0.0)).sum(); |
| 33 | + let unrealized_pnl: f64 = positions |
| 34 | + .iter() |
| 35 | + .flat_map(|p| &p.asset_positions) |
| 36 | + .map(|p| p.position.unrealized_pnl.parse().unwrap_or(0.0)) |
| 37 | + .sum(); |
| 38 | + |
| 39 | + let account_leverage = if account_value > 0.0 { total_ntl_pos / account_value } else { 0.0 }; |
| 40 | + let margin_usage = if account_value > 0.0 { total_margin_used / account_value } else { 0.0 }; |
| 41 | + |
| 42 | + PerpetualAccountSummary { |
| 43 | + account_value, |
| 44 | + account_leverage, |
| 45 | + margin_usage, |
| 46 | + unrealized_pnl, |
| 47 | + } |
| 48 | +} |
| 49 | + |
| 50 | +pub fn merge_positions_summaries(summaries: Vec<PerpetualPositionsSummary>) -> PerpetualPositionsSummary { |
| 51 | + let (positions, balance) = summaries.into_iter().fold( |
| 52 | + (Vec::new(), PerpetualBalance { available: 0.0, reserved: 0.0, withdrawable: 0.0 }), |
| 53 | + |(mut acc_pos, mut acc_bal), summary| { |
| 54 | + acc_pos.extend(summary.positions); |
| 55 | + acc_bal.available += summary.balance.available; |
| 56 | + acc_bal.reserved += summary.balance.reserved; |
| 57 | + acc_bal.withdrawable += summary.balance.withdrawable; |
| 58 | + (acc_pos, acc_bal) |
| 59 | + }, |
| 60 | + ); |
| 61 | + PerpetualPositionsSummary { positions, balance } |
| 62 | +} |
| 63 | + |
| 64 | +pub fn merge_perpetual_portfolios(portfolios: Vec<PerpetualPortfolio>, account_summary: Option<PerpetualAccountSummary>) -> PerpetualPortfolio { |
| 65 | + let mut day = Vec::new(); |
| 66 | + let mut week = Vec::new(); |
| 67 | + let mut month = Vec::new(); |
| 68 | + let mut all_time = Vec::new(); |
| 69 | + |
| 70 | + for portfolio in portfolios { |
| 71 | + day.extend(portfolio.day); |
| 72 | + week.extend(portfolio.week); |
| 73 | + month.extend(portfolio.month); |
| 74 | + all_time.extend(portfolio.all_time); |
| 75 | + } |
| 76 | + |
| 77 | + PerpetualPortfolio { |
| 78 | + day: merge_portfolio_timeframes(day), |
| 79 | + week: merge_portfolio_timeframes(week), |
| 80 | + month: merge_portfolio_timeframes(month), |
| 81 | + all_time: merge_portfolio_timeframes(all_time), |
| 82 | + account_summary, |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +fn merge_portfolio_timeframes(values: Vec<PerpetualPortfolioTimeframeData>) -> Option<PerpetualPortfolioTimeframeData> { |
| 87 | + if values.is_empty() { |
| 88 | + return None; |
| 89 | + } |
| 90 | + |
| 91 | + let mut account_value_histories = Vec::new(); |
| 92 | + let mut pnl_histories = Vec::new(); |
| 93 | + let mut volume = 0.0; |
| 94 | + |
| 95 | + for value in values { |
| 96 | + account_value_histories.push(value.account_value_history); |
| 97 | + pnl_histories.push(value.pnl_history); |
| 98 | + volume += value.volume; |
| 99 | + } |
| 100 | + |
| 101 | + Some(PerpetualPortfolioTimeframeData { |
| 102 | + account_value_history: merge_chart_histories(account_value_histories), |
| 103 | + pnl_history: merge_chart_histories(pnl_histories), |
| 104 | + volume, |
| 105 | + }) |
| 106 | +} |
| 107 | + |
| 108 | +fn merge_chart_histories(values: Vec<Vec<ChartDateValue>>) -> Vec<ChartDateValue> { |
| 109 | + let mut grouped = BTreeMap::new(); |
| 110 | + for history in values { |
| 111 | + for point in history { |
| 112 | + let entry = grouped.entry(point.date).or_insert(0.0); |
| 113 | + *entry += point.value; |
| 114 | + } |
| 115 | + } |
| 116 | + |
| 117 | + grouped.into_iter().map(|(date, value)| ChartDateValue { date, value }).collect() |
| 118 | +} |
| 119 | + |
| 120 | +#[cfg(test)] |
| 121 | +mod tests { |
| 122 | + use super::*; |
| 123 | + use primitives::portfolio::PerpetualAccountSummary; |
| 124 | + |
| 125 | + #[test] |
| 126 | + fn test_perp_asset_index() { |
| 127 | + assert_eq!(perp_asset_index(0, 0), 0); |
| 128 | + assert_eq!(perp_asset_index(0, 5), 5); |
| 129 | + assert_eq!(perp_asset_index(1, 0), 110_000); |
| 130 | + assert_eq!(perp_asset_index(1, 3), 110_003); |
| 131 | + assert_eq!(perp_asset_index(2, 0), 120_000); |
| 132 | + assert_eq!(perp_asset_index(2, 7), 120_007); |
| 133 | + } |
| 134 | + |
| 135 | + #[test] |
| 136 | + fn test_format_display_name() { |
| 137 | + assert_eq!(format_display_name("xyz:GOLD"), "GOLD (xyz)"); |
| 138 | + assert_eq!(format_display_name("BTC"), "BTC"); |
| 139 | + } |
| 140 | + |
| 141 | + #[test] |
| 142 | + fn test_map_account_summary_aggregate() { |
| 143 | + let positions = vec![AssetPositions::mock(), AssetPositions::mock()]; |
| 144 | + let summary = map_account_summary_aggregate(&positions); |
| 145 | + |
| 146 | + assert_eq!(summary.account_value, 20000.0); |
| 147 | + assert_eq!(summary.account_leverage, 0.5); |
| 148 | + assert_eq!(summary.margin_usage, 0.2); |
| 149 | + assert_eq!(summary.unrealized_pnl, 0.0); |
| 150 | + } |
| 151 | + |
| 152 | + #[test] |
| 153 | + fn test_merge_chart_histories() { |
| 154 | + use chrono::{TimeZone, Utc}; |
| 155 | + |
| 156 | + let d1 = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(); |
| 157 | + let d2 = Utc.with_ymd_and_hms(2024, 1, 2, 0, 0, 0).unwrap(); |
| 158 | + let d3 = Utc.with_ymd_and_hms(2024, 1, 3, 0, 0, 0).unwrap(); |
| 159 | + |
| 160 | + let histories = vec![ |
| 161 | + vec![ChartDateValue { date: d1, value: 100.0 }, ChartDateValue { date: d2, value: 200.0 }], |
| 162 | + vec![ChartDateValue { date: d1, value: 50.0 }, ChartDateValue { date: d3, value: 300.0 }], |
| 163 | + ]; |
| 164 | + |
| 165 | + let merged = merge_chart_histories(histories); |
| 166 | + assert_eq!(merged.len(), 3); |
| 167 | + assert_eq!(merged[0].value, 150.0); |
| 168 | + assert_eq!(merged[1].value, 200.0); |
| 169 | + assert_eq!(merged[2].value, 300.0); |
| 170 | + } |
| 171 | + |
| 172 | + #[test] |
| 173 | + fn test_merge_perpetual_portfolios() { |
| 174 | + use chrono::{TimeZone, Utc}; |
| 175 | + |
| 176 | + let d1 = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(); |
| 177 | + let portfolios = vec![ |
| 178 | + PerpetualPortfolio::mock_with_day(d1, 100.0, 10.0, 500.0), |
| 179 | + PerpetualPortfolio::mock_with_day(d1, 200.0, 20.0, 300.0), |
| 180 | + ]; |
| 181 | + let summary = PerpetualAccountSummary { account_value: 1000.0, account_leverage: 2.0, margin_usage: 0.5, unrealized_pnl: 30.0 }; |
| 182 | + |
| 183 | + let merged = merge_perpetual_portfolios(portfolios, Some(summary)); |
| 184 | + |
| 185 | + let day = merged.day.unwrap(); |
| 186 | + assert_eq!(day.volume, 800.0); |
| 187 | + assert_eq!(day.account_value_history.len(), 1); |
| 188 | + assert_eq!(day.account_value_history[0].value, 300.0); |
| 189 | + assert_eq!(day.pnl_history[0].value, 30.0); |
| 190 | + assert!(merged.week.is_none()); |
| 191 | + assert_eq!(merged.account_summary.unwrap().account_value, 1000.0); |
| 192 | + } |
| 193 | + |
| 194 | + #[test] |
| 195 | + fn test_merge_portfolio_timeframes_empty() { |
| 196 | + let result = merge_portfolio_timeframes(vec![]); |
| 197 | + assert!(result.is_none()); |
| 198 | + } |
| 199 | +} |
0 commit comments