1use super::AppState;
2use crate::models::{ApiResponse, PortfolioGreeksResponse};
3use axum::{
4 extract::{Query, State},
5 http::StatusCode,
6 Json,
7};
8use hypercall_types::portfolio_greeks::{
9 build_net_position_quantities, calculate_portfolio_greeks, parse_simulated_orders,
10};
11use hypercall_types::WalletAddress;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use utoipa::{IntoParams, ToSchema};
15
16#[derive(Debug, Deserialize, IntoParams)]
18pub struct GetGreeksQuery {
19 #[param(example = "BTC-20250131-100000-C")]
21 pub symbol: String,
22}
23
24#[derive(Debug, Serialize, ToSchema)]
26pub struct GreeksResponse {
27 pub symbol: String,
29 pub delta: f64,
31 pub gamma: f64,
33 pub theta: f64,
35 pub vega: f64,
37 pub rho: f64,
39 pub implied_vol: f64,
41 pub theoretical_price: f64,
43 pub mid_price: Option<f64>,
45}
46
47#[derive(Debug, Serialize, ToSchema)]
48pub struct GreeksApiResponse {
49 pub success: bool,
50 pub data: Option<GreeksResponse>,
51 pub error: Option<String>,
52}
53
54#[derive(Debug, Serialize, ToSchema)]
55pub struct PortfolioGreeksApiResponse {
56 pub success: bool,
57 pub data: Option<PortfolioGreeksResponse>,
58 pub error: Option<String>,
59}
60
61#[derive(Debug, Deserialize, IntoParams)]
63pub struct PortfolioGreeksQuery {
64 #[param(example = "0x1234567890abcdef1234567890abcdef12345678")]
66 pub wallet: WalletAddress,
67 #[param(value_type = Option<String>)]
70 pub simulated_orders: Option<String>,
71}
72
73#[utoipa::path(
75 get,
76 path = "/greeks",
77 params(GetGreeksQuery),
78 responses(
79 (status = 200, description = "Greeks data", body = GreeksApiResponse),
80 (status = 500, description = "Internal server error")
81 ),
82 tag = "Markets"
83)]
84pub async fn get_greeks(
85 State(app_state): State<AppState>,
86 Query(params): Query<GetGreeksQuery>,
87) -> Result<Json<ApiResponse<GreeksResponse>>, StatusCode> {
88 match app_state.greeks_cache.get_greeks(¶ms.symbol).await {
89 Ok(greeks) => {
90 let response = GreeksResponse {
91 symbol: params.symbol,
92 delta: greeks.delta,
93 gamma: greeks.gamma,
94 theta: greeks.theta,
95 vega: greeks.vega,
96 rho: greeks.rho,
97 implied_vol: greeks.implied_vol,
98 theoretical_price: greeks.theoretical_price,
99 mid_price: greeks.market_mid_price,
100 };
101 Ok(Json(ApiResponse::success(response)))
102 }
103 Err(e) => {
104 tracing::error!("Failed to get greeks for {}: {}", params.symbol, e);
105 Ok(Json(ApiResponse::error(format!(
106 "Failed to get greeks: {}",
107 e
108 ))))
109 }
110 }
111}
112
113#[utoipa::path(
115 get,
116 path = "/portfolio/greeks",
117 params(PortfolioGreeksQuery),
118 responses(
119 (status = 200, description = "Portfolio Greeks data", body = PortfolioGreeksApiResponse),
120 (status = 500, description = "Internal server error")
121 ),
122 security(("wallet_query" = [])),
123 tag = "Portfolio"
124)]
125pub async fn get_portfolio_greeks(
126 State(app_state): State<AppState>,
127 Query(params): Query<PortfolioGreeksQuery>,
128) -> Result<Json<ApiResponse<PortfolioGreeksResponse>>, StatusCode> {
129 let simulated_orders = match parse_simulated_orders(params.simulated_orders.as_deref()) {
130 Ok(orders) => orders,
131 Err(e) => {
132 return Ok(Json(ApiResponse::error(e)));
133 }
134 };
135
136 let live_portfolio = match app_state
137 .portfolio_cache
138 .get_portfolio(¶ms.wallet)
139 .await
140 {
141 Ok(portfolio) => portfolio,
142 Err(e) => {
143 tracing::error!(
144 "Failed to get live portfolio for wallet {}: {}",
145 params.wallet,
146 e
147 );
148 return Ok(Json(ApiResponse::error(format!(
149 "Failed to fetch live portfolio: {}",
150 e
151 ))));
152 }
153 };
154
155 let live_positions = live_portfolio
156 .positions
157 .into_iter()
158 .map(|p| (p.position.symbol, p.position.amount));
159 let net_quantities = match build_net_position_quantities(live_positions, &simulated_orders) {
160 Ok(quantities) => quantities,
161 Err(e) => {
162 return Ok(Json(ApiResponse::error(e)));
163 }
164 };
165
166 if net_quantities.is_empty() {
167 return Ok(Json(ApiResponse::success(PortfolioGreeksResponse {
168 wallet_address: params.wallet,
169 per_leg: Vec::new(),
170 aggregate: None,
171 })));
172 }
173
174 let mut contract_greeks = HashMap::with_capacity(net_quantities.len());
175 for symbol in net_quantities.keys() {
176 let greeks = app_state
177 .greeks_cache
178 .get_greeks(symbol)
179 .await
180 .map_err(|e| {
181 tracing::error!("Failed to fetch greeks for {}: {}", symbol, e);
182 e
183 });
184 match greeks {
185 Ok(value) => {
186 contract_greeks.insert(symbol.clone(), value);
187 }
188 Err(e) => {
189 return Ok(Json(ApiResponse::error(format!(
190 "Failed to get greeks for {}: {}",
191 symbol, e
192 ))));
193 }
194 }
195 }
196
197 match calculate_portfolio_greeks(params.wallet, &net_quantities, &contract_greeks) {
198 Ok(response) => Ok(Json(ApiResponse::success(response))),
199 Err(e) => Ok(Json(ApiResponse::error(e))),
200 }
201}