Skip to main content

quantwave_core/options_india/
chain_analytics.rs

1use serde::{Deserialize, Serialize};
2
3/// Max Pain strike calculation.
4/// Returns the strike price where the total loss to option buyers is minimized.
5pub fn max_pain(strikes: &[f64], ce_oi: &[u64], pe_oi: &[u64], lot_size: u32) -> f64 {
6    if strikes.is_empty() {
7        return 0.0;
8    }
9
10    let mut min_pain = f64::MAX;
11    let mut max_pain_strike = strikes[0];
12
13    for &expiry_price in strikes {
14        let mut total_pain = 0.0;
15        for i in 0..strikes.len() {
16            let strike = strikes[i];
17            // CE pain: buyer loses if expiry_price > strike
18            if expiry_price > strike {
19                total_pain += (expiry_price - strike) * ce_oi[i] as f64 * lot_size as f64;
20            }
21            // PE pain: buyer loses if expiry_price < strike
22            if expiry_price < strike {
23                total_pain += (strike - expiry_price) * pe_oi[i] as f64 * lot_size as f64;
24            }
25        }
26
27        if total_pain < min_pain {
28            min_pain = total_pain;
29            max_pain_strike = expiry_price;
30        }
31    }
32
33    max_pain_strike
34}
35
36/// Per-strike Put-Call Ratio (PCR).
37/// Returns Vec of PE_OI / CE_OI for each strike.
38pub fn strike_pcr(ce_oi: &[u64], pe_oi: &[u64]) -> Vec<f64> {
39    ce_oi
40        .iter()
41        .zip(pe_oi.iter())
42        .map(
43            |(&ce, &pe)| {
44                if ce == 0 { 0.0 } else { pe as f64 / ce as f64 }
45            },
46        )
47        .collect()
48}
49
50/// Chain-level Put-Call Ratio (PCR).
51/// Returns total PE_OI / total CE_OI.
52pub fn chain_pcr(ce_oi: &[u64], pe_oi: &[u64]) -> f64 {
53    let total_ce: u64 = ce_oi.iter().sum();
54    let total_pe: u64 = pe_oi.iter().sum();
55    if total_ce == 0 {
56        0.0
57    } else {
58        total_pe as f64 / total_ce as f64
59    }
60}
61
62/// OI concentration zones (Support and Resistance).
63#[derive(Debug, Serialize, Deserialize)]
64pub struct OIZones {
65    pub resistance_strikes: Vec<f64>, // Top N strikes by CE OI
66    pub support_strikes: Vec<f64>,    // Top N strikes by PE OI
67}
68
69pub fn oi_zones(strikes: &[f64], ce_oi: &[u64], pe_oi: &[u64], n: usize) -> OIZones {
70    let mut ce_pairs: Vec<(f64, u64)> =
71        strikes.iter().cloned().zip(ce_oi.iter().cloned()).collect();
72    let mut pe_pairs: Vec<(f64, u64)> =
73        strikes.iter().cloned().zip(pe_oi.iter().cloned()).collect();
74
75    ce_pairs.sort_by(|a, b| b.1.cmp(&a.1));
76    pe_pairs.sort_by(|a, b| b.1.cmp(&a.1));
77
78    OIZones {
79        resistance_strikes: ce_pairs.iter().take(n).map(|p| p.0).collect(),
80        support_strikes: pe_pairs.iter().take(n).map(|p| p.0).collect(),
81    }
82}
83
84/// Gamma Exposure (GEX) per strike.
85/// Returns a Vec of (ce_gex, pe_gex, net_gex) per strike.
86/// CE GEX = OI * Gamma * Spot * LotSize * 0.01
87/// PE GEX = OI * Gamma * Spot * LotSize * -0.01
88pub fn gex_per_strike(
89    spot: f64,
90    strikes: &[f64],
91    ce_gamma: &[f64],
92    pe_gamma: &[f64],
93    ce_oi: &[u64],
94    pe_oi: &[u64],
95    lot_size: u32,
96) -> Vec<(f64, f64, f64)> {
97    let mut result = Vec::with_capacity(strikes.len());
98    for i in 0..strikes.len() {
99        let ce_g = ce_oi[i] as f64 * ce_gamma[i] * spot * lot_size as f64 * 0.01;
100        let pe_g = pe_oi[i] as f64 * pe_gamma[i] * spot * lot_size as f64 * -0.01;
101        result.push((ce_g, pe_g, ce_g + pe_g));
102    }
103    result
104}
105
106/// GEX Flip Strike.
107/// Returns the strike price where the cumulative Net GEX changes sign.
108pub fn gex_flip_strike(strikes: &[f64], net_gex: &[f64]) -> Option<f64> {
109    if strikes.len() < 2 {
110        return None;
111    }
112
113    for i in 0..net_gex.len() - 1 {
114        if (net_gex[i] < 0.0 && net_gex[i + 1] > 0.0) || (net_gex[i] > 0.0 && net_gex[i + 1] < 0.0)
115        {
116            // Linear interpolation for more precision if needed, but returning nearest strike for now
117            return Some(strikes[i]);
118        }
119    }
120    None
121}
122
123/// ATM Straddle analytics.
124/// Returns (atm_strike, straddle_premium, implied_move_pct).
125pub fn atm_straddle(spot: f64, strikes: &[f64], ce_ltp: &[f64], pe_ltp: &[f64]) -> (f64, f64, f64) {
126    if strikes.is_empty() {
127        return (0.0, 0.0, 0.0);
128    }
129
130    let mut closest_idx = 0;
131    let mut min_diff = f64::MAX;
132
133    for i in 0..strikes.len() {
134        let diff = (strikes[i] - spot).abs();
135        if diff < min_diff {
136            min_diff = diff;
137            closest_idx = i;
138        }
139    }
140
141    let atm_strike = strikes[closest_idx];
142    let straddle_premium = ce_ltp[closest_idx] + pe_ltp[closest_idx];
143    let implied_move_pct = (straddle_premium / spot) * 100.0;
144
145    (atm_strike, straddle_premium, implied_move_pct)
146}
147
148/// Synthetic Futures calculation per strike.
149/// Future Price = CE_LTP - PE_LTP + Strike
150pub fn synthetic_futures(strikes: &[f64], ce_ltp: &[f64], pe_ltp: &[f64]) -> Vec<f64> {
151    strikes
152        .iter()
153        .enumerate()
154        .map(|(i, &k)| ce_ltp[i] - pe_ltp[i] + k)
155        .collect()
156}