Skip to main content

quantwave_core/options_india/
chain_analytics.rs

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