@fullstackcraftllc/floe 0.0.14 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Regime classification based on IV level and surface characteristics
3
+ */
4
+ export type MarketRegime = 'calm' | 'normal' | 'stressed' | 'crisis';
5
+ /**
6
+ * Parameters derived from the IV surface itself (no external data needed)
7
+ */
8
+ export interface RegimeParams {
9
+ /** ATM implied volatility (as decimal, e.g., 0.18 for 18%) */
10
+ atmIV: number;
11
+ /** Implied spot-vol correlation derived from skew */
12
+ impliedSpotVolCorr: number;
13
+ /** Implied vol-of-vol derived from smile curvature */
14
+ impliedVolOfVol: number;
15
+ /** Regime classification */
16
+ regime: MarketRegime;
17
+ /** Expected daily spot move (as decimal) */
18
+ expectedDailySpotMove: number;
19
+ /** Expected daily vol move (as decimal) */
20
+ expectedDailyVolMove: number;
21
+ }
22
+ /**
23
+ * Configuration for the hedge impulse curve computation
24
+ */
25
+ export interface HedgeImpulseConfig {
26
+ /** Price grid range as percentage of spot (e.g. 3 for ±3%). Default: 3 */
27
+ rangePercent?: number;
28
+ /** Price grid step as percentage of spot (e.g. 0.05 for 0.05%). Default: 0.05 */
29
+ stepPercent?: number;
30
+ /**
31
+ * Gaussian kernel width in number of strike spacings.
32
+ * Auto-detected from the option chain's strike spacing.
33
+ * Default: 2 (i.e. lambda = 2 * modal strike spacing)
34
+ */
35
+ kernelWidthStrikes?: number;
36
+ }
37
+ /**
38
+ * Hedge impulse value at a single price level
39
+ *
40
+ * The hedge impulse H(S) represents the net dealer delta hedge change
41
+ * per unit spot move if price were at S. It combines gamma and vanna
42
+ * via the empirical spot-vol coupling:
43
+ *
44
+ * H(S) = Gamma(S) - (k / S) * Vanna(S)
45
+ *
46
+ * where k is derived from the IV surface skew slope.
47
+ */
48
+ export interface HedgeImpulsePoint {
49
+ /** Price level */
50
+ price: number;
51
+ /** Smoothed gamma exposure at this price level (kernel-weighted from strikes) */
52
+ gamma: number;
53
+ /** Smoothed vanna exposure at this price level (kernel-weighted from strikes) */
54
+ vanna: number;
55
+ /** Combined hedge impulse: gamma - (k/S) * vanna */
56
+ impulse: number;
57
+ }
58
+ /**
59
+ * A zero crossing of the hedge impulse curve
60
+ */
61
+ export interface ZeroCrossing {
62
+ /** Price at which the crossing occurs (interpolated) */
63
+ price: number;
64
+ /** Direction: 'rising' means impulse goes from negative to positive */
65
+ direction: 'rising' | 'falling';
66
+ }
67
+ /**
68
+ * A local extremum (basin or peak) of the hedge impulse curve
69
+ */
70
+ export interface ImpulseExtremum {
71
+ /** Price at the extremum */
72
+ price: number;
73
+ /** Impulse value at the extremum */
74
+ impulse: number;
75
+ /** 'basin' = positive local max (attractor), 'peak' = negative local min (accelerator) */
76
+ type: 'basin' | 'peak';
77
+ }
78
+ /**
79
+ * Directional asymmetry analysis around current spot
80
+ */
81
+ export interface DirectionalAsymmetry {
82
+ /** Integrated impulse from spot to spot + integrationRange */
83
+ upside: number;
84
+ /** Integrated impulse from spot to spot - integrationRange */
85
+ downside: number;
86
+ /** Integration range used (percentage of spot) */
87
+ integrationRangePercent: number;
88
+ /** Which side has more negative impulse (= path of least resistance) */
89
+ bias: 'up' | 'down' | 'neutral';
90
+ /** Ratio of |upside| to |downside| impulse. >1 means upside is larger magnitude */
91
+ asymmetryRatio: number;
92
+ }
93
+ /**
94
+ * Regime classification based on the impulse curve shape
95
+ */
96
+ export type ImpulseRegime = 'pinned' | 'expansion' | 'squeeze-up' | 'squeeze-down' | 'neutral';
97
+ /**
98
+ * Complete hedge impulse curve result
99
+ */
100
+ export interface HedgeImpulseCurve {
101
+ /** Current spot price */
102
+ spot: number;
103
+ /** Expiration timestamp */
104
+ expiration: number;
105
+ /** Timestamp when this curve was computed */
106
+ computedAt: number;
107
+ /** Spot-vol coupling coefficient derived from IV surface */
108
+ spotVolCoupling: number;
109
+ /** Kernel width used (in price units) */
110
+ kernelWidth: number;
111
+ /** Strike spacing detected from the option chain */
112
+ strikeSpacing: number;
113
+ /** The full curve of impulse values across the price grid */
114
+ curve: HedgeImpulsePoint[];
115
+ /** Impulse value at current spot (interpolated) */
116
+ impulseAtSpot: number;
117
+ /** Slope of the impulse curve at current spot (dH/dS) */
118
+ slopeAtSpot: number;
119
+ /** Zero crossings of the impulse curve */
120
+ zeroCrossings: ZeroCrossing[];
121
+ /** Local extrema (basins = attractors, peaks = accelerators) */
122
+ extrema: ImpulseExtremum[];
123
+ /** Directional asymmetry analysis */
124
+ asymmetry: DirectionalAsymmetry;
125
+ /** Regime classification */
126
+ regime: ImpulseRegime;
127
+ /** Nearest attractor (positive impulse basin) above spot, if any */
128
+ nearestAttractorAbove: number | null;
129
+ /** Nearest attractor (positive impulse basin) below spot, if any */
130
+ nearestAttractorBelow: number | null;
131
+ }
132
+ /**
133
+ * Configuration for charm integral computation
134
+ */
135
+ export interface CharmIntegralConfig {
136
+ /** Time step for the integral in minutes. Default: 15 */
137
+ timeStepMinutes?: number;
138
+ }
139
+ /**
140
+ * Charm integral at a single time bucket
141
+ */
142
+ export interface CharmBucket {
143
+ /** Minutes remaining to expiry at start of this bucket */
144
+ minutesRemaining: number;
145
+ /** Instantaneous CEX at this time */
146
+ instantaneousCEX: number;
147
+ /** Cumulative CEX from now to this time */
148
+ cumulativeCEX: number;
149
+ }
150
+ /**
151
+ * Complete charm integral result
152
+ */
153
+ export interface CharmIntegral {
154
+ /** Current spot price */
155
+ spot: number;
156
+ /** Expiration timestamp */
157
+ expiration: number;
158
+ /** Timestamp when this was computed */
159
+ computedAt: number;
160
+ /** Minutes remaining from computation time to expiry */
161
+ minutesRemaining: number;
162
+ /** Total charm integral from now to close (cumulative expected delta change) */
163
+ totalCharmToClose: number;
164
+ /** Direction: positive = net buying pressure from charm, negative = net selling */
165
+ direction: 'buying' | 'selling' | 'neutral';
166
+ /** Bucketed charm integral curve for visualization */
167
+ buckets: CharmBucket[];
168
+ /** Per-strike charm breakdown (for understanding what drives the integral) */
169
+ strikeContributions: Array<{
170
+ strike: number;
171
+ charmExposure: number;
172
+ fractionOfTotal: number;
173
+ }>;
174
+ }
175
+ /**
176
+ * Complete hedge flow analysis combining impulse curve and charm integral
177
+ */
178
+ export interface HedgeFlowAnalysis {
179
+ /** The instantaneous hedge impulse curve (left panel) */
180
+ impulseCurve: HedgeImpulseCurve;
181
+ /** The charm integral to close (right panel) */
182
+ charmIntegral: CharmIntegral;
183
+ /** Regime params derived from IV surface */
184
+ regimeParams: RegimeParams;
185
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/index.d.ts CHANGED
@@ -14,11 +14,15 @@ export { buildOCCSymbol, parseOCCSymbol, generateStrikesAroundSpot, generateOCCS
14
14
  export type { OCCSymbolParams, ParsedOCCSymbol, StrikeGenerationParams, } from './utils/occ';
15
15
  export { estimateImpliedProbabilityDistribution, estimateImpliedProbabilityDistributions, getProbabilityInRange, getCumulativeProbability, getQuantile, estimateExposureAdjustedPDF, getEdgeAtPrice, getSignificantAdjustmentLevels, DEFAULT_ADJUSTMENT_CONFIG, LOW_VOL_CONFIG, CRISIS_CONFIG, OPEX_CONFIG, } from './impliedpdf';
16
16
  export type { StrikeProbability, ImpliedProbabilityDistribution, ImpliedPDFResult, ExposureAdjustmentConfig, AdjustedPDFResult, PDFComparison, } from './impliedpdf';
17
- export { deriveRegimeParams, interpolateIVAtStrike, normalizeExposures, calculatePressureAt, buildPressureGrid, calculateIVPressures, predictIVSurfaceEvolution, runCascadeSimulation, analyzeIVPath, getIVPathOfLeastResistance, DEFAULT_IV_PATH_CONFIG, } from './pressure';
18
- export type { MarketRegime, RegimeParams, NormalizedExposure, NormalizedExposureSummary, HedgingPressure, PressureGridCell, KeyLevels, PathOfLeastResistance, PressureGrid, StrikeIVPressure, IVSurfaceEvolution, CascadeStep, CascadeSimulation, IVPathConfig, } from './pressure';
17
+ export { deriveRegimeParams, interpolateIVAtStrike, computeHedgeImpulseCurve, computeCharmIntegral, analyzeHedgeFlow, } from './hedgeflow';
18
+ export type { MarketRegime, RegimeParams, HedgeImpulseConfig, HedgeImpulsePoint, HedgeImpulseCurve, ZeroCrossing, ImpulseExtremum, DirectionalAsymmetry, ImpulseRegime, CharmIntegralConfig, CharmBucket, CharmIntegral, HedgeFlowAnalysis, } from './hedgeflow';
19
19
  export { FloeClient, Broker } from './client/FloeClient';
20
20
  export { TradierClient } from './client/brokers/TradierClient';
21
21
  export { TastyTradeClient } from './client/brokers/TastyTradeClient';
22
22
  export { TradeStationClient } from './client/brokers/TradeStationClient';
23
23
  export type { AggressorSide, IntradayTrade } from './client/brokers/TradierClient';
24
24
  export { genericAdapter, schwabAdapter, ibkrAdapter, tdaAdapter, brokerAdapters, getAdapter, createOptionChain, } from './adapters';
25
+ export { computeVarianceSwapIV, computeImpliedVolatility, } from './iv';
26
+ export type { VarianceSwapResult, ImpliedVolatilityResult, } from './iv';
27
+ export { computeRealizedVolatility, } from './rv';
28
+ export type { PriceObservation, RealizedVolatilityResult, } from './rv';
package/dist/index.js CHANGED
@@ -20,8 +20,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
20
20
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
21
21
  };
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
- exports.brokerAdapters = exports.tdaAdapter = exports.ibkrAdapter = exports.schwabAdapter = exports.genericAdapter = exports.TradeStationClient = exports.TastyTradeClient = exports.TradierClient = exports.Broker = exports.FloeClient = exports.DEFAULT_IV_PATH_CONFIG = exports.getIVPathOfLeastResistance = exports.analyzeIVPath = exports.runCascadeSimulation = exports.predictIVSurfaceEvolution = exports.calculateIVPressures = exports.buildPressureGrid = exports.calculatePressureAt = exports.normalizeExposures = exports.interpolateIVAtStrike = exports.deriveRegimeParams = exports.OPEX_CONFIG = exports.CRISIS_CONFIG = exports.LOW_VOL_CONFIG = exports.DEFAULT_ADJUSTMENT_CONFIG = exports.getSignificantAdjustmentLevels = exports.getEdgeAtPrice = exports.estimateExposureAdjustedPDF = exports.getQuantile = exports.getCumulativeProbability = exports.getProbabilityInRange = exports.estimateImpliedProbabilityDistributions = exports.estimateImpliedProbabilityDistribution = exports.generateOCCSymbolsAroundSpot = exports.generateOCCSymbolsForStrikes = exports.generateStrikesAroundSpot = exports.parseOCCSymbol = exports.buildOCCSymbol = exports.normalPDF = exports.cumulativeNormalDistribution = exports.calculateSharesNeededToCover = exports.calculateGammaVannaCharmExposures = exports.smoothTotalVarianceSmile = exports.getIVForStrike = exports.getIVSurfaces = exports.getTimeToExpirationInYears = exports.getMillisecondsToExpiration = exports.calculateImpliedVolatility = exports.calculateGreeks = exports.blackScholes = void 0;
24
- exports.createOptionChain = exports.getAdapter = void 0;
23
+ exports.computeRealizedVolatility = exports.computeImpliedVolatility = exports.computeVarianceSwapIV = exports.createOptionChain = exports.getAdapter = exports.brokerAdapters = exports.tdaAdapter = exports.ibkrAdapter = exports.schwabAdapter = exports.genericAdapter = exports.TradeStationClient = exports.TastyTradeClient = exports.TradierClient = exports.Broker = exports.FloeClient = exports.analyzeHedgeFlow = exports.computeCharmIntegral = exports.computeHedgeImpulseCurve = exports.interpolateIVAtStrike = exports.deriveRegimeParams = exports.OPEX_CONFIG = exports.CRISIS_CONFIG = exports.LOW_VOL_CONFIG = exports.DEFAULT_ADJUSTMENT_CONFIG = exports.getSignificantAdjustmentLevels = exports.getEdgeAtPrice = exports.estimateExposureAdjustedPDF = exports.getQuantile = exports.getCumulativeProbability = exports.getProbabilityInRange = exports.estimateImpliedProbabilityDistributions = exports.estimateImpliedProbabilityDistribution = exports.generateOCCSymbolsAroundSpot = exports.generateOCCSymbolsForStrikes = exports.generateStrikesAroundSpot = exports.parseOCCSymbol = exports.buildOCCSymbol = exports.normalPDF = exports.cumulativeNormalDistribution = exports.calculateSharesNeededToCover = exports.calculateGammaVannaCharmExposures = exports.smoothTotalVarianceSmile = exports.getIVForStrike = exports.getIVSurfaces = exports.getTimeToExpirationInYears = exports.getMillisecondsToExpiration = exports.calculateImpliedVolatility = exports.calculateGreeks = exports.blackScholes = void 0;
25
24
  // Core types
26
25
  __exportStar(require("./types"), exports);
27
26
  // Black-Scholes pricing and Greeks
@@ -68,23 +67,17 @@ Object.defineProperty(exports, "DEFAULT_ADJUSTMENT_CONFIG", { enumerable: true,
68
67
  Object.defineProperty(exports, "LOW_VOL_CONFIG", { enumerable: true, get: function () { return impliedpdf_1.LOW_VOL_CONFIG; } });
69
68
  Object.defineProperty(exports, "CRISIS_CONFIG", { enumerable: true, get: function () { return impliedpdf_1.CRISIS_CONFIG; } });
70
69
  Object.defineProperty(exports, "OPEX_CONFIG", { enumerable: true, get: function () { return impliedpdf_1.OPEX_CONFIG; } });
71
- // Pressure field and IV path prediction
72
- var pressure_1 = require("./pressure");
73
- // Regime derivation
74
- Object.defineProperty(exports, "deriveRegimeParams", { enumerable: true, get: function () { return pressure_1.deriveRegimeParams; } });
75
- Object.defineProperty(exports, "interpolateIVAtStrike", { enumerable: true, get: function () { return pressure_1.interpolateIVAtStrike; } });
76
- // Exposure normalization
77
- Object.defineProperty(exports, "normalizeExposures", { enumerable: true, get: function () { return pressure_1.normalizeExposures; } });
78
- // Pressure field
79
- Object.defineProperty(exports, "calculatePressureAt", { enumerable: true, get: function () { return pressure_1.calculatePressureAt; } });
80
- Object.defineProperty(exports, "buildPressureGrid", { enumerable: true, get: function () { return pressure_1.buildPressureGrid; } });
81
- // IV path prediction
82
- Object.defineProperty(exports, "calculateIVPressures", { enumerable: true, get: function () { return pressure_1.calculateIVPressures; } });
83
- Object.defineProperty(exports, "predictIVSurfaceEvolution", { enumerable: true, get: function () { return pressure_1.predictIVSurfaceEvolution; } });
84
- Object.defineProperty(exports, "runCascadeSimulation", { enumerable: true, get: function () { return pressure_1.runCascadeSimulation; } });
85
- Object.defineProperty(exports, "analyzeIVPath", { enumerable: true, get: function () { return pressure_1.analyzeIVPath; } });
86
- Object.defineProperty(exports, "getIVPathOfLeastResistance", { enumerable: true, get: function () { return pressure_1.getIVPathOfLeastResistance; } });
87
- Object.defineProperty(exports, "DEFAULT_IV_PATH_CONFIG", { enumerable: true, get: function () { return pressure_1.DEFAULT_IV_PATH_CONFIG; } });
70
+ // Hedge flow analysis (impulse curve, charm integral, regime derivation)
71
+ var hedgeflow_1 = require("./hedgeflow");
72
+ // Regime derivation from IV surface
73
+ Object.defineProperty(exports, "deriveRegimeParams", { enumerable: true, get: function () { return hedgeflow_1.deriveRegimeParams; } });
74
+ Object.defineProperty(exports, "interpolateIVAtStrike", { enumerable: true, get: function () { return hedgeflow_1.interpolateIVAtStrike; } });
75
+ // Hedge impulse curve
76
+ Object.defineProperty(exports, "computeHedgeImpulseCurve", { enumerable: true, get: function () { return hedgeflow_1.computeHedgeImpulseCurve; } });
77
+ // Charm integral
78
+ Object.defineProperty(exports, "computeCharmIntegral", { enumerable: true, get: function () { return hedgeflow_1.computeCharmIntegral; } });
79
+ // Combined analysis
80
+ Object.defineProperty(exports, "analyzeHedgeFlow", { enumerable: true, get: function () { return hedgeflow_1.analyzeHedgeFlow; } });
88
81
  // Client
89
82
  var FloeClient_1 = require("./client/FloeClient");
90
83
  Object.defineProperty(exports, "FloeClient", { enumerable: true, get: function () { return FloeClient_1.FloeClient; } });
@@ -104,3 +97,10 @@ Object.defineProperty(exports, "tdaAdapter", { enumerable: true, get: function (
104
97
  Object.defineProperty(exports, "brokerAdapters", { enumerable: true, get: function () { return adapters_1.brokerAdapters; } });
105
98
  Object.defineProperty(exports, "getAdapter", { enumerable: true, get: function () { return adapters_1.getAdapter; } });
106
99
  Object.defineProperty(exports, "createOptionChain", { enumerable: true, get: function () { return adapters_1.createOptionChain; } });
100
+ // Model-free implied volatility (variance swap / VIX methodology)
101
+ var iv_1 = require("./iv");
102
+ Object.defineProperty(exports, "computeVarianceSwapIV", { enumerable: true, get: function () { return iv_1.computeVarianceSwapIV; } });
103
+ Object.defineProperty(exports, "computeImpliedVolatility", { enumerable: true, get: function () { return iv_1.computeImpliedVolatility; } });
104
+ // Realized volatility (tick-based quadratic variation)
105
+ var rv_1 = require("./rv");
106
+ Object.defineProperty(exports, "computeRealizedVolatility", { enumerable: true, get: function () { return rv_1.computeRealizedVolatility; } });
@@ -0,0 +1,52 @@
1
+ import { NormalizedOption } from '../types';
2
+ import { VarianceSwapResult, ImpliedVolatilityResult } from './types';
3
+ export type { VarianceSwapResult, ImpliedVolatilityResult } from './types';
4
+ /**
5
+ * Compute the model-free implied variance for a single expiration
6
+ * using the CBOE variance swap methodology.
7
+ *
8
+ * This implements the formula:
9
+ *
10
+ * σ² = (2/T) × Σᵢ (ΔKᵢ/Kᵢ²) × e^(rT) × Q(Kᵢ) - (1/T) × (F/K₀ - 1)²
11
+ *
12
+ * Where:
13
+ * - K₀ is the strike where |call_mid - put_mid| is minimized
14
+ * - F = K₀ + e^(rT) × (call(K₀) - put(K₀)) is the forward price
15
+ * - Q(Kᵢ) = put mid for Kᵢ < K₀, call mid for Kᵢ > K₀,
16
+ * average of call and put mids at K₀
17
+ * - ΔKᵢ = (Kᵢ₊₁ - Kᵢ₋₁) / 2 (actual strike spacing, not hardcoded)
18
+ *
19
+ * Two consecutive zero-bid options terminate the summation in each
20
+ * direction (puts walking down, calls walking up), per CBOE rules.
21
+ *
22
+ * @param options - All options for one expiration (calls and puts)
23
+ * @param spot - Current underlying price
24
+ * @param riskFreeRate - Annual risk-free rate as decimal (e.g. 0.05)
25
+ * @returns Variance swap result with annualized IV
26
+ */
27
+ export declare function computeVarianceSwapIV(options: NormalizedOption[], spot: number, riskFreeRate: number): VarianceSwapResult;
28
+ /**
29
+ * Compute implied volatility from option prices using the CBOE
30
+ * variance swap methodology.
31
+ *
32
+ * If only nearTermOptions are provided, computes the single-expiration
33
+ * model-free implied volatility directly.
34
+ *
35
+ * If farTermOptions are also provided, performs CBOE VIX-style
36
+ * interpolation between the two terms to produce a constant-maturity
37
+ * measure at `targetDays` (defaults to the far term's DTE).
38
+ *
39
+ * The interpolation formula (in variance space):
40
+ *
41
+ * VIX = 100 × √{ [T₁σ₁² × (N₂ - N_target)/(N₂ - N₁)
42
+ * + T₂σ₂² × (N_target - N₁)/(N₂ - N₁)]
43
+ * × N_365 / N_target }
44
+ *
45
+ * @param nearTermOptions - Options for the near-term expiration
46
+ * @param spot - Current underlying price
47
+ * @param riskFreeRate - Annual risk-free rate as decimal
48
+ * @param farTermOptions - Options for the far-term expiration (optional)
49
+ * @param targetDays - Target constant maturity in days for interpolation
50
+ * (defaults to far term DTE if far term provided)
51
+ */
52
+ export declare function computeImpliedVolatility(nearTermOptions: NormalizedOption[], spot: number, riskFreeRate: number, farTermOptions?: NormalizedOption[] | null, targetDays?: number): ImpliedVolatilityResult;
@@ -0,0 +1,287 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeVarianceSwapIV = computeVarianceSwapIV;
4
+ exports.computeImpliedVolatility = computeImpliedVolatility;
5
+ const types_1 = require("../types");
6
+ const blackscholes_1 = require("../blackscholes");
7
+ // ============================================================================
8
+ // Internal helpers
9
+ // ============================================================================
10
+ /**
11
+ * Compute ΔK for each strike: half the distance between adjacent strikes.
12
+ * For endpoints, use the distance to the single neighbor.
13
+ */
14
+ function computeDeltaK(strikes) {
15
+ const deltaK = new Map();
16
+ if (strikes.length === 0)
17
+ return deltaK;
18
+ if (strikes.length === 1) {
19
+ deltaK.set(strikes[0], 1); // fallback
20
+ return deltaK;
21
+ }
22
+ for (let i = 0; i < strikes.length; i++) {
23
+ if (i === 0) {
24
+ deltaK.set(strikes[i], strikes[i + 1] - strikes[i]);
25
+ }
26
+ else if (i === strikes.length - 1) {
27
+ deltaK.set(strikes[i], strikes[i] - strikes[i - 1]);
28
+ }
29
+ else {
30
+ deltaK.set(strikes[i], (strikes[i + 1] - strikes[i - 1]) / 2);
31
+ }
32
+ }
33
+ return deltaK;
34
+ }
35
+ /**
36
+ * Group options by strike, pairing calls and puts.
37
+ */
38
+ function pairOptionsByStrike(options) {
39
+ const pairs = new Map();
40
+ for (const opt of options) {
41
+ const existing = pairs.get(opt.strike) || {};
42
+ if (opt.optionType === 'call') {
43
+ existing.call = opt;
44
+ }
45
+ else {
46
+ existing.put = opt;
47
+ }
48
+ pairs.set(opt.strike, existing);
49
+ }
50
+ return pairs;
51
+ }
52
+ /**
53
+ * Get mid price for an option. Returns 0 if no valid quote.
54
+ */
55
+ function midPrice(opt) {
56
+ if (!opt)
57
+ return 0;
58
+ if (opt.bid > 0 && opt.ask > 0)
59
+ return (opt.bid + opt.ask) / 2;
60
+ if (opt.mark > 0)
61
+ return opt.mark;
62
+ return 0;
63
+ }
64
+ /**
65
+ * Compute the model-free implied variance for a single expiration
66
+ * using the CBOE variance swap methodology.
67
+ *
68
+ * This implements the formula:
69
+ *
70
+ * σ² = (2/T) × Σᵢ (ΔKᵢ/Kᵢ²) × e^(rT) × Q(Kᵢ) - (1/T) × (F/K₀ - 1)²
71
+ *
72
+ * Where:
73
+ * - K₀ is the strike where |call_mid - put_mid| is minimized
74
+ * - F = K₀ + e^(rT) × (call(K₀) - put(K₀)) is the forward price
75
+ * - Q(Kᵢ) = put mid for Kᵢ < K₀, call mid for Kᵢ > K₀,
76
+ * average of call and put mids at K₀
77
+ * - ΔKᵢ = (Kᵢ₊₁ - Kᵢ₋₁) / 2 (actual strike spacing, not hardcoded)
78
+ *
79
+ * Two consecutive zero-bid options terminate the summation in each
80
+ * direction (puts walking down, calls walking up), per CBOE rules.
81
+ *
82
+ * @param options - All options for one expiration (calls and puts)
83
+ * @param spot - Current underlying price
84
+ * @param riskFreeRate - Annual risk-free rate as decimal (e.g. 0.05)
85
+ * @returns Variance swap result with annualized IV
86
+ */
87
+ function computeVarianceSwapIV(options, spot, riskFreeRate) {
88
+ // Pair calls and puts by strike
89
+ const pairs = pairOptionsByStrike(options);
90
+ const strikes = Array.from(pairs.keys()).sort((a, b) => a - b);
91
+ if (strikes.length === 0) {
92
+ return emptyResult(spot);
93
+ }
94
+ // Use first option's expiration (all should be same expiry)
95
+ const expiration = options[0].expirationTimestamp;
96
+ const T = (0, blackscholes_1.getTimeToExpirationInYears)(expiration);
97
+ if (T <= 0) {
98
+ return emptyResult(spot);
99
+ }
100
+ const r = riskFreeRate;
101
+ const eRT = Math.exp(r * T);
102
+ // Step 1: Find K₀ — strike where |call_mid - put_mid| is minimized
103
+ // Only consider strikes within reasonable range of spot
104
+ let k0 = strikes[0];
105
+ let minDiff = Infinity;
106
+ let callAtK0 = 0;
107
+ let putAtK0 = 0;
108
+ for (const strike of strikes) {
109
+ const pair = pairs.get(strike);
110
+ const callMid = midPrice(pair.call);
111
+ const putMid = midPrice(pair.put);
112
+ if (callMid > 0 && putMid > 0) {
113
+ const diff = Math.abs(callMid - putMid);
114
+ if (diff < minDiff) {
115
+ minDiff = diff;
116
+ k0 = strike;
117
+ callAtK0 = callMid;
118
+ putAtK0 = putMid;
119
+ }
120
+ }
121
+ }
122
+ // Step 2: Forward price via put-call parity
123
+ const F = k0 + eRT * (callAtK0 - putAtK0);
124
+ // Step 3: Compute ΔK for each strike
125
+ const deltaKMap = computeDeltaK(strikes);
126
+ // Step 4: Sum OTM contributions
127
+ // Puts below K₀ (walk down, stop after two consecutive zero bids)
128
+ let putContribution = 0;
129
+ let callContribution = 0;
130
+ let numStrikes = 0;
131
+ // Put side: strikes ≤ K₀, walking downward from K₀
132
+ const putStrikes = strikes.filter(k => k <= k0).reverse(); // descending
133
+ let consecutiveZeroBids = 0;
134
+ for (const strike of putStrikes) {
135
+ const pair = pairs.get(strike);
136
+ const dk = deltaKMap.get(strike) || 1;
137
+ let Q = 0;
138
+ if (strike === k0) {
139
+ // At K₀: average of call and put
140
+ Q = (midPrice(pair.call) + midPrice(pair.put)) / 2;
141
+ }
142
+ else {
143
+ // Below K₀: use put
144
+ if (!pair.put || pair.put.bid === 0) {
145
+ consecutiveZeroBids++;
146
+ if (consecutiveZeroBids >= 2)
147
+ break;
148
+ continue;
149
+ }
150
+ consecutiveZeroBids = 0;
151
+ Q = midPrice(pair.put);
152
+ }
153
+ if (Q > 0) {
154
+ putContribution += (dk / (strike * strike)) * eRT * Q;
155
+ numStrikes++;
156
+ }
157
+ }
158
+ // Call side: strikes ≥ K₀, walking upward from K₀
159
+ const callStrikes = strikes.filter(k => k >= k0); // ascending
160
+ consecutiveZeroBids = 0;
161
+ for (const strike of callStrikes) {
162
+ const pair = pairs.get(strike);
163
+ const dk = deltaKMap.get(strike) || 1;
164
+ let Q = 0;
165
+ if (strike === k0) {
166
+ // Already counted in put side, skip to avoid double-counting
167
+ continue;
168
+ }
169
+ else {
170
+ // Above K₀: use call
171
+ if (!pair.call || pair.call.bid === 0) {
172
+ consecutiveZeroBids++;
173
+ if (consecutiveZeroBids >= 2)
174
+ break;
175
+ continue;
176
+ }
177
+ consecutiveZeroBids = 0;
178
+ Q = midPrice(pair.call);
179
+ }
180
+ if (Q > 0) {
181
+ callContribution += (dk / (strike * strike)) * eRT * Q;
182
+ numStrikes++;
183
+ }
184
+ }
185
+ // Step 5: Compute σ²
186
+ const totalContribution = putContribution + callContribution;
187
+ const variance = (2 / T) * totalContribution - (1 / T) * Math.pow(F / k0 - 1, 2);
188
+ // Guard against negative variance (can happen with bad data)
189
+ const clampedVariance = Math.max(0, variance);
190
+ const iv = Math.sqrt(clampedVariance);
191
+ return {
192
+ impliedVolatility: iv,
193
+ annualizedVariance: clampedVariance,
194
+ forward: F,
195
+ k0,
196
+ timeToExpiry: T,
197
+ expiration,
198
+ numStrikes,
199
+ putContribution,
200
+ callContribution,
201
+ };
202
+ }
203
+ /**
204
+ * Compute implied volatility from option prices using the CBOE
205
+ * variance swap methodology.
206
+ *
207
+ * If only nearTermOptions are provided, computes the single-expiration
208
+ * model-free implied volatility directly.
209
+ *
210
+ * If farTermOptions are also provided, performs CBOE VIX-style
211
+ * interpolation between the two terms to produce a constant-maturity
212
+ * measure at `targetDays` (defaults to the far term's DTE).
213
+ *
214
+ * The interpolation formula (in variance space):
215
+ *
216
+ * VIX = 100 × √{ [T₁σ₁² × (N₂ - N_target)/(N₂ - N₁)
217
+ * + T₂σ₂² × (N_target - N₁)/(N₂ - N₁)]
218
+ * × N_365 / N_target }
219
+ *
220
+ * @param nearTermOptions - Options for the near-term expiration
221
+ * @param spot - Current underlying price
222
+ * @param riskFreeRate - Annual risk-free rate as decimal
223
+ * @param farTermOptions - Options for the far-term expiration (optional)
224
+ * @param targetDays - Target constant maturity in days for interpolation
225
+ * (defaults to far term DTE if far term provided)
226
+ */
227
+ function computeImpliedVolatility(nearTermOptions, spot, riskFreeRate, farTermOptions, targetDays) {
228
+ const nearResult = computeVarianceSwapIV(nearTermOptions, spot, riskFreeRate);
229
+ // Single-term mode
230
+ if (!farTermOptions || farTermOptions.length === 0) {
231
+ return {
232
+ impliedVolatility: nearResult.impliedVolatility,
233
+ nearTerm: nearResult,
234
+ farTerm: null,
235
+ targetDays: null,
236
+ isInterpolated: false,
237
+ };
238
+ }
239
+ // Two-term interpolation mode
240
+ const farResult = computeVarianceSwapIV(farTermOptions, spot, riskFreeRate);
241
+ const N1 = nearResult.timeToExpiry * types_1.MILLISECONDS_PER_YEAR; // near term ms
242
+ const N2 = farResult.timeToExpiry * types_1.MILLISECONDS_PER_YEAR; // far term ms
243
+ const T1 = nearResult.timeToExpiry;
244
+ const T2 = farResult.timeToExpiry;
245
+ const sigma1sq = nearResult.annualizedVariance;
246
+ const sigma2sq = farResult.annualizedVariance;
247
+ // Target in milliseconds
248
+ const N365 = types_1.MILLISECONDS_PER_YEAR;
249
+ const effectiveTargetDays = targetDays ?? Math.round(farResult.timeToExpiry * 365);
250
+ const Ntarget = effectiveTargetDays * (types_1.MILLISECONDS_PER_YEAR / 365);
251
+ // Guard: if near and far are the same expiration, just return near
252
+ if (Math.abs(N2 - N1) < 1) {
253
+ return {
254
+ impliedVolatility: nearResult.impliedVolatility,
255
+ nearTerm: nearResult,
256
+ farTerm: farResult,
257
+ targetDays: effectiveTargetDays,
258
+ isInterpolated: false,
259
+ };
260
+ }
261
+ // CBOE interpolation in variance space
262
+ const weight1 = (N2 - Ntarget) / (N2 - N1);
263
+ const weight2 = (Ntarget - N1) / (N2 - N1);
264
+ const interpolatedVariance = (T1 * sigma1sq * weight1 + T2 * sigma2sq * weight2) * (N365 / Ntarget);
265
+ const clampedVariance = Math.max(0, interpolatedVariance);
266
+ const iv = Math.sqrt(clampedVariance);
267
+ return {
268
+ impliedVolatility: iv,
269
+ nearTerm: nearResult,
270
+ farTerm: farResult,
271
+ targetDays: effectiveTargetDays,
272
+ isInterpolated: true,
273
+ };
274
+ }
275
+ function emptyResult(spot) {
276
+ return {
277
+ impliedVolatility: 0,
278
+ annualizedVariance: 0,
279
+ forward: spot,
280
+ k0: spot,
281
+ timeToExpiry: 0,
282
+ expiration: 0,
283
+ numStrikes: 0,
284
+ putContribution: 0,
285
+ callContribution: 0,
286
+ };
287
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Result of computing model-free implied variance for a single expiration
3
+ * using the CBOE variance swap methodology.
4
+ */
5
+ export interface VarianceSwapResult {
6
+ /** Annualized implied volatility as decimal (e.g. 0.20 = 20%) */
7
+ impliedVolatility: number;
8
+ /** Annualized variance σ² */
9
+ annualizedVariance: number;
10
+ /** Forward price F derived from put-call parity at K₀ */
11
+ forward: number;
12
+ /** At-the-money strike K₀ (strike where |call - put| is minimized) */
13
+ k0: number;
14
+ /** Time to expiry in years */
15
+ timeToExpiry: number;
16
+ /** Expiration timestamp (ms) */
17
+ expiration: number;
18
+ /** Number of strikes that contributed to the variance sum */
19
+ numStrikes: number;
20
+ /** Total put-side contribution to variance */
21
+ putContribution: number;
22
+ /** Total call-side contribution to variance */
23
+ callContribution: number;
24
+ }
25
+ /**
26
+ * Result of computing implied volatility, either single-term or
27
+ * interpolated between two terms (CBOE VIX-style).
28
+ */
29
+ export interface ImpliedVolatilityResult {
30
+ /** Final annualized implied volatility as decimal */
31
+ impliedVolatility: number;
32
+ /** Near-term (or only-term) variance swap result */
33
+ nearTerm: VarianceSwapResult;
34
+ /** Far-term variance swap result, if two terms provided */
35
+ farTerm: VarianceSwapResult | null;
36
+ /** Target days for interpolation (null if single-term) */
37
+ targetDays: number | null;
38
+ /** Whether the result is interpolated between two terms */
39
+ isInterpolated: boolean;
40
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });