1use chrono::{DateTime, Utc};
2use rust_decimal::Decimal;
3use rust_decimal_macros::dec;
4use serde::{Deserialize, Serialize};
5use utoipa::ToSchema;
6
7use hypercall_types::{to_human_readable_decimal, WalletAddress};
8
9pub use hypercall_types::api_models::{
12 AccountBalance, DeleteMmpConfigRequest, ExchangeInfoResponse, ExtendedRiskMatrixResponse,
13 FillApiResponse, FillsResponse, HealthResponse, InstrumentRiskRowResponse, InstrumentStatus,
14 MarginModeApiResponse, MarginModeResponse, MarginSummary, MmpConfigData, MmpConfigResponse,
15 MonitoringAccountSummary, MonitoringAccountsResponse, MonitoringPositionHolder,
16 MonitoringPositionsResponse, MonitoringSymbolPosition, OptionsChainGreeksAbs,
17 OptionsChainGreeksCash, OptionsChainLeg, OptionsChainSnapshotResponse, OptionsChainStrikeRow,
18 Order, Portfolio, PortfolioGreeksAggregate, PortfolioGreeksResponse, Position,
19 PositionGreeksLeg, PositionWithMetrics, ReadinessComponentReport, ReadyResponse,
20 ResetMmpRequest, RiskGridResponse, RiskGridScenario, ScenarioDefinition, SetMmpConfigRequest,
21 SigningDomainInfo, SimulatedGreeksOrder, SpanMarginSummary, TradeApiResponse, TradesResponse,
22 TradingLimits, VersionResponse,
23};
24
25pub use hypercall_types::api_models::Instrument;
29pub use hypercall_types::api_models::MarketInfo;
30pub use hypercall_types::api_models::MarketsResponse;
31
32pub use hypercall_types::api_models::ApiResponse;
34pub use hypercall_types::responses::Pagination;
36
37#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
38pub struct OrdersResponse {
39 pub success: bool,
40 pub data: Vec<Order>,
41 pub pagination: Pagination,
42}
43
44pub use hypercall_types::api_models::{
46 DeleteUserTierRequest, SetUserTierRequest, UserTierData, UserTierResponse,
47};
48
49pub use hypercall_types::requests::SetMarginModeRequest;
51
52#[derive(Debug, Serialize, Deserialize)]
54pub struct GetMmpConfigQuery {
55 pub wallet: WalletAddress,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub currency: Option<String>,
58}
59
60#[derive(Debug, Serialize, Deserialize)]
61pub struct GetUserTierQuery {
62 pub wallet: WalletAddress,
63}
64
65#[derive(Debug, Clone, Deserialize)]
68pub struct Trade {
69 pub trade_id: i64,
70 pub symbol: String,
71 pub price: Decimal,
72 pub size: Decimal,
73 pub maker_address: WalletAddress,
74 pub taker_address: WalletAddress,
75 pub maker_fee: Decimal,
76 pub taker_fee: Decimal,
77 pub timestamp: i64,
78 pub created_at: DateTime<Utc>,
79}
80
81impl Trade {
82 pub fn to_api_response(&self) -> TradeApiResponse {
83 TradeApiResponse {
84 trade_id: self.trade_id,
85 symbol: self.symbol.clone(),
86 price: self.price,
87 size: to_human_readable_decimal(&self.symbol, self.size),
88 maker_address: self.maker_address,
89 taker_address: self.taker_address,
90 maker_fee: self.maker_fee,
91 taker_fee: self.taker_fee,
92 timestamp: self.timestamp,
93 created_at: self.created_at,
94 }
95 }
96}
97
98pub trait PositionMetrics {
99 fn calculate_metrics(&self) -> PositionWithMetrics;
100}
101
102impl PositionMetrics for Position {
103 fn calculate_metrics(&self) -> PositionWithMetrics {
104 let notional_value = self.amount * self.entry_price;
105 let liquidation_price = dec!(0);
106 let margin_ratio = if notional_value.abs() > dec!(0) {
107 self.margin_posted / notional_value.abs()
108 } else {
109 dec!(0)
110 };
111
112 PositionWithMetrics {
113 position: Position {
114 wallet_address: self.wallet_address,
115 symbol: self.symbol.clone(),
116 amount: self.amount,
117 entry_price: self.entry_price,
118 margin_posted: self.margin_posted,
119 realized_pnl: self.realized_pnl,
120 unrealized_pnl: self.unrealized_pnl,
121 updated_at: self.updated_at,
122 },
123 notional_value,
124 maintenance_margin: dec!(0),
125 liquidation_price,
126 margin_ratio,
127 }
128 }
129}
130
131#[derive(Debug, Serialize, Deserialize, ToSchema)]
132pub struct Market {
133 pub underlying: String,
134 pub expiry: i64,
135 #[schema(value_type = String)]
136 pub created_at: DateTime<Utc>,
137}
138
139pub use hypercall_types::api_models::{
141 CompetitionData, CompetitionLeaderboardResponse, CompetitionResponse, CompetitionSortByValue,
142 CompetitionSortOrderValue, CompetitionStateValue, CompetitionUpdateRequest,
143 CompetitionUpsertRequest, CompetitionWinConditionValue, CompetitionsResponse,
144 ConnectedUserRank, LeaderboardRow, ProfileCompetitionRankSummary, ProfileData,
145 ProfileMarginStats, ProfileMetricMedals, ProfilePnlStats, ProfileResponse,
146 ProfileTradesResponse, RealizedPnlResponse, RealizedPnlRow,
147};
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use rust_decimal_macros::dec;
153
154 #[test]
155 fn risk_grid_serialization_uses_combined_pm_fields() {
156 let response = RiskGridResponse {
157 equity: dec!(10000),
158 position_initial_margin: dec!(1500),
159 position_maintenance_margin: dec!(1275),
160 open_orders_initial_margin: dec!(250),
161 total_initial_margin: dec!(1750),
162 scanning_risk: dec!(1200),
163 option_floor: dec!(1500),
164 gamma_overlay: dec!(250),
165 scenarios: vec![RiskGridScenario {
166 id: "T1".to_string(),
167 spot_shock_pct: dec!(-0.25),
168 vol_shock_pct: dec!(0.70),
169 pnl_weight: dec!(0.60),
170 is_tail: true,
171 total_pnl: dec!(-1400),
172 }],
173 extended_risk_matrix: Some(ExtendedRiskMatrixResponse {
174 scenarios: vec![ScenarioDefinition {
175 id: "T1".to_string(),
176 spot_shock_pct: dec!(-0.25),
177 vol_shock_pct: dec!(0.70),
178 pnl_weight: dec!(0.60),
179 is_tail: true,
180 }],
181 instruments: vec![InstrumentRiskRowResponse {
182 symbol: "BTC-20991231-100000-C".to_string(),
183 underlying: "BTC".to_string(),
184 amount: dec!(-1),
185 base_amount: dec!(-1),
186 current_value: dec!(-500),
187 scenario_pnls: vec![dec!(-1400)],
188 }],
189 total_pnls: vec![dec!(-1400)],
190 worst_scenario_index: 0,
191 worst_scenario_pnl: dec!(-840),
192 }),
193 };
194
195 let json = serde_json::to_value(response).unwrap();
196 assert_eq!(json["scanning_risk"], "1200");
197 assert_eq!(json["option_floor"], "1500");
198 assert_eq!(json["gamma_overlay"], "250");
199 assert_eq!(json["scenarios"][0]["spot_shock_pct"], "-0.25");
200 assert_eq!(json["scenarios"][0]["vol_shock_pct"], "0.70");
201 assert_eq!(json["scenarios"][0]["pnl_weight"], "0.60");
202 assert_eq!(json["scenarios"][0]["is_tail"], true);
203 assert!(json["scenarios"][0].get("scenario_type").is_none());
204 assert!(json["scenarios"][0].get("value").is_none());
205 assert_eq!(json["extended_risk_matrix"]["scenarios"][0]["id"], "T1");
206 assert_eq!(json["extended_risk_matrix"]["worst_scenario_pnl"], "-840");
207 }
208
209 #[test]
210 fn position_calculate_metrics_zero_amount() {
211 let pos = Position {
212 wallet_address: hypercall_types::WalletAddress::default(),
213 symbol: "BTC-20260101-100000-C".to_string(),
214 amount: dec!(0),
215 entry_price: dec!(100),
216 margin_posted: dec!(0),
217 realized_pnl: dec!(0),
218 unrealized_pnl: dec!(0),
219 updated_at: chrono::Utc::now(),
220 };
221 let metrics = pos.calculate_metrics();
222 assert_eq!(metrics.notional_value, dec!(0));
223 assert_eq!(metrics.margin_ratio, dec!(0));
224 }
225
226 #[test]
227 fn position_calculate_metrics_short() {
228 let pos = Position {
229 wallet_address: hypercall_types::WalletAddress::default(),
230 symbol: "ETH-20260301-5000-P".to_string(),
231 amount: dec!(-2),
232 entry_price: dec!(150),
233 margin_posted: dec!(60),
234 realized_pnl: dec!(0),
235 unrealized_pnl: dec!(-20),
236 updated_at: chrono::Utc::now(),
237 };
238 let metrics = pos.calculate_metrics();
239 assert_eq!(metrics.notional_value, dec!(-300));
240 assert_eq!(metrics.margin_ratio, dec!(0.2)); assert_eq!(metrics.liquidation_price, dec!(0));
242 }
243
244 #[test]
245 fn trade_to_api_response_converts_size() {
246 let trade = Trade {
247 trade_id: 1,
248 symbol: "BTC-20260101-100000-C".to_string(),
249 price: dec!(2500),
250 size: dec!(100000000), maker_address: hypercall_types::WalletAddress::default(),
252 taker_address: hypercall_types::WalletAddress::default(),
253 maker_fee: dec!(2.5),
254 taker_fee: dec!(5),
255 timestamp: 1700000000000,
256 created_at: chrono::Utc::now(),
257 };
258 let api = trade.to_api_response();
259 assert_eq!(api.price, dec!(2500));
260 assert!(
261 api.size < trade.size,
262 "API size should be human-readable (smaller than raw)"
263 );
264 }
265}