Skip to main content

hypercall_api/handlers/
greeks.rs

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/// Query parameters for getting Greeks
17#[derive(Debug, Deserialize, IntoParams)]
18pub struct GetGreeksQuery {
19    /// Instrument symbol (e.g., "BTC-20250131-100000-C")
20    #[param(example = "BTC-20250131-100000-C")]
21    pub symbol: String,
22}
23
24/// Greeks response for an instrument
25#[derive(Debug, Serialize, ToSchema)]
26pub struct GreeksResponse {
27    /// Instrument symbol
28    pub symbol: String,
29    /// Delta - rate of change of option price with respect to underlying
30    pub delta: f64,
31    /// Gamma - rate of change of delta with respect to underlying
32    pub gamma: f64,
33    /// Theta - rate of decay of option value over time
34    pub theta: f64,
35    /// Vega - sensitivity to volatility changes
36    pub vega: f64,
37    /// Rho - sensitivity to interest rate changes
38    pub rho: f64,
39    /// Implied volatility
40    pub implied_vol: f64,
41    /// Theoretical Black-Scholes price used for greeks.
42    pub theoretical_price: f64,
43    /// Live market mid when a quote is available.
44    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/// Query parameters for portfolio Greeks.
62#[derive(Debug, Deserialize, IntoParams)]
63pub struct PortfolioGreeksQuery {
64    /// Wallet address to calculate portfolio Greeks for.
65    #[param(example = "0x1234567890abcdef1234567890abcdef12345678")]
66    pub wallet: WalletAddress,
67    /// Optional JSON array of simulated orders.
68    /// Example: [{"symbol":"BTC-20260131-100000-C","side":"Buy","size":"1.0"}]
69    #[param(value_type = Option<String>)]
70    pub simulated_orders: Option<String>,
71}
72
73/// Get Greeks for an instrument
74#[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(&params.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/// Get per-position and aggregate portfolio Greeks with optional simulated orders.
114#[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(&params.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}