@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,23 @@
1
+ import { ExposurePerExpiry } from '../types';
2
+ import { CharmIntegralConfig, CharmIntegral } from './types';
3
+ /**
4
+ * Compute the charm integral from now until expiration.
5
+ *
6
+ * The charm integral represents the cumulative expected delta change
7
+ * from time decay alone — i.e., what happens to dealer hedging
8
+ * regardless of price movement. This is the "unconditional" pressure.
9
+ *
10
+ * The dollar CEX values already incorporate Black-Scholes time decay
11
+ * acceleration (charm ∝ 1/√T near expiry), so no additional time
12
+ * weighting is needed. Larger CEX values near expiry are the math
13
+ * doing its job, not something we need to amplify.
14
+ *
15
+ * When open interest changes intraday (detected via live OI tracking),
16
+ * this function should be recomputed with updated exposures to reflect
17
+ * the new charm landscape.
18
+ *
19
+ * @param exposures - Current per-strike exposure data (with live OI if available)
20
+ * @param config - Optional time step configuration
21
+ * @returns Charm integral analysis with cumulative curve and per-strike breakdown
22
+ */
23
+ export declare function computeCharmIntegral(exposures: ExposurePerExpiry, config?: CharmIntegralConfig): CharmIntegral;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeCharmIntegral = computeCharmIntegral;
4
+ /**
5
+ * Compute the charm integral from now until expiration.
6
+ *
7
+ * The charm integral represents the cumulative expected delta change
8
+ * from time decay alone — i.e., what happens to dealer hedging
9
+ * regardless of price movement. This is the "unconditional" pressure.
10
+ *
11
+ * The dollar CEX values already incorporate Black-Scholes time decay
12
+ * acceleration (charm ∝ 1/√T near expiry), so no additional time
13
+ * weighting is needed. Larger CEX values near expiry are the math
14
+ * doing its job, not something we need to amplify.
15
+ *
16
+ * When open interest changes intraday (detected via live OI tracking),
17
+ * this function should be recomputed with updated exposures to reflect
18
+ * the new charm landscape.
19
+ *
20
+ * @param exposures - Current per-strike exposure data (with live OI if available)
21
+ * @param config - Optional time step configuration
22
+ * @returns Charm integral analysis with cumulative curve and per-strike breakdown
23
+ */
24
+ function computeCharmIntegral(exposures, config = {}) {
25
+ const { timeStepMinutes = 15 } = config;
26
+ const spot = exposures.spotPrice;
27
+ const expiration = exposures.expiration;
28
+ const now = Date.now();
29
+ const msRemaining = expiration - now;
30
+ const minutesRemaining = Math.max(0, msRemaining / 60000);
31
+ // Per-strike charm breakdown
32
+ const totalAbsCharm = exposures.strikeExposures.reduce((sum, s) => sum + Math.abs(s.charmExposure), 0);
33
+ const strikeContributions = exposures.strikeExposures
34
+ .filter(s => s.charmExposure !== 0)
35
+ .map(s => ({
36
+ strike: s.strikePrice,
37
+ charmExposure: s.charmExposure,
38
+ fractionOfTotal: totalAbsCharm > 0
39
+ ? Math.abs(s.charmExposure) / totalAbsCharm
40
+ : 0,
41
+ }))
42
+ .sort((a, b) => Math.abs(b.charmExposure) - Math.abs(a.charmExposure));
43
+ // Build time-bucketed charm curve
44
+ // The total CEX is already a per-day rate. For sub-day intervals,
45
+ // we scale by the fraction of the day each bucket represents.
46
+ const totalCEX = exposures.totalCharmExposure;
47
+ const buckets = [];
48
+ let cumulativeCEX = 0;
49
+ if (minutesRemaining <= 0) {
50
+ // Already expired
51
+ return {
52
+ spot,
53
+ expiration,
54
+ computedAt: now,
55
+ minutesRemaining: 0,
56
+ totalCharmToClose: 0,
57
+ direction: 'neutral',
58
+ buckets: [],
59
+ strikeContributions,
60
+ };
61
+ }
62
+ // Walk from now backward toward expiry in timeStepMinutes increments.
63
+ // At each step, the charm exposure intensifies because the remaining
64
+ // options are closer to expiry.
65
+ //
66
+ // Since CEX is already the dollar exposure rate that incorporates the
67
+ // current time to expiry, and charm accelerates as 1/√T, we model
68
+ // the instantaneous charm at t minutes remaining as:
69
+ //
70
+ // CEX(t) ≈ totalCEX * √(minutesRemaining / t)
71
+ //
72
+ // This scaling comes from charm ∝ 1/√T: if current charm is computed
73
+ // at T minutes out, then at t < T minutes it will be larger by √(T/t).
74
+ //
75
+ // The integral of CEX(t) dt from t=minutesRemaining down to t=0
76
+ // gives the total expected delta change, but note the integral of
77
+ // 1/√t diverges — in practice, charm exposure is bounded because
78
+ // deep ITM/OTM options have vanishing charm. We cap at t=1 minute.
79
+ for (let t = minutesRemaining; t >= Math.max(1, timeStepMinutes); t -= timeStepMinutes) {
80
+ // Charm at this time: scale by √(minutesRemaining / t)
81
+ const timeScaling = Math.sqrt(minutesRemaining / t);
82
+ const instantCEX = totalCEX * timeScaling;
83
+ // Each bucket represents timeStepMinutes of elapsed time.
84
+ // Convert to fraction of a trading day for the integral.
85
+ // 6.5 hours = 390 minutes in a standard session.
86
+ const bucketFractionOfDay = timeStepMinutes / 390;
87
+ const bucketContribution = instantCEX * bucketFractionOfDay;
88
+ cumulativeCEX += bucketContribution;
89
+ buckets.push({
90
+ minutesRemaining: t,
91
+ instantaneousCEX: instantCEX,
92
+ cumulativeCEX,
93
+ });
94
+ }
95
+ // Determine direction
96
+ let direction = 'neutral';
97
+ if (cumulativeCEX > 0) {
98
+ direction = 'buying';
99
+ }
100
+ else if (cumulativeCEX < 0) {
101
+ direction = 'selling';
102
+ }
103
+ return {
104
+ spot,
105
+ expiration,
106
+ computedAt: now,
107
+ minutesRemaining,
108
+ totalCharmToClose: cumulativeCEX,
109
+ direction,
110
+ buckets,
111
+ strikeContributions,
112
+ };
113
+ }
@@ -0,0 +1,27 @@
1
+ import { ExposurePerExpiry, IVSurface } from '../types';
2
+ import { HedgeImpulseConfig, HedgeImpulseCurve } from './types';
3
+ /**
4
+ * Compute the hedge impulse curve across a price grid.
5
+ *
6
+ * The hedge impulse H(S) at each price level S combines gamma and vanna
7
+ * exposures via the empirical spot-vol coupling relationship:
8
+ *
9
+ * H(S) = GEX_smoothed(S) - (k / S) * VEX_smoothed(S)
10
+ *
11
+ * where:
12
+ * - GEX_smoothed(S) is the kernel-smoothed gamma exposure at S
13
+ * - VEX_smoothed(S) is the kernel-smoothed vanna exposure at S
14
+ * - k is the spot-vol coupling coefficient derived from the IV surface
15
+ *
16
+ * Positive H(S) means a spot move toward S triggers dealer buying that
17
+ * dampens the move (mean-reversion / "wall" behavior).
18
+ *
19
+ * Negative H(S) means a spot move toward S triggers dealer selling that
20
+ * amplifies the move (trend acceleration / "vacuum" behavior).
21
+ *
22
+ * @param exposures - Per-strike gamma, vanna, charm exposures
23
+ * @param ivSurface - The IV surface for regime derivation
24
+ * @param config - Optional configuration for grid and kernel parameters
25
+ * @returns Complete hedge impulse curve analysis
26
+ */
27
+ export declare function computeHedgeImpulseCurve(exposures: ExposurePerExpiry, ivSurface: IVSurface, config?: HedgeImpulseConfig): HedgeImpulseCurve;
@@ -0,0 +1,315 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeHedgeImpulseCurve = computeHedgeImpulseCurve;
4
+ const regime_1 = require("./regime");
5
+ /**
6
+ * Detect the modal (most common) strike spacing from an array of strikes.
7
+ * Handles irregular spacing by finding the most frequent gap.
8
+ */
9
+ function detectStrikeSpacing(strikes) {
10
+ if (strikes.length < 2)
11
+ return 1;
12
+ const gaps = [];
13
+ for (let i = 1; i < strikes.length; i++) {
14
+ const gap = Math.abs(strikes[i] - strikes[i - 1]);
15
+ if (gap > 0)
16
+ gaps.push(gap);
17
+ }
18
+ if (gaps.length === 0)
19
+ return 1;
20
+ // Find modal gap (most common spacing)
21
+ const gapCounts = new Map();
22
+ for (const gap of gaps) {
23
+ // Round to avoid floating point issues
24
+ const rounded = Math.round(gap * 100) / 100;
25
+ gapCounts.set(rounded, (gapCounts.get(rounded) || 0) + 1);
26
+ }
27
+ let modalGap = gaps[0];
28
+ let maxCount = 0;
29
+ for (const [gap, count] of gapCounts) {
30
+ if (count > maxCount) {
31
+ maxCount = count;
32
+ modalGap = gap;
33
+ }
34
+ }
35
+ return modalGap;
36
+ }
37
+ /**
38
+ * Derive the spot-vol coupling coefficient k from the IV surface.
39
+ *
40
+ * From stochastic vol models, the skew encodes the spot-vol correlation:
41
+ * skew ≈ rho_SV * volOfVol / atmIV
42
+ *
43
+ * The spot-vol coupling for the dealer hedge equation is:
44
+ * dSigma ≈ -k * (dS/S)
45
+ *
46
+ * So k = -rho_SV * volOfVol * sqrt(252) = -impliedSpotVolCorr * atmIV * sqrt(252)
47
+ *
48
+ * For equity indices this typically lands in the range 4-12.
49
+ */
50
+ function deriveSpotVolCoupling(regimeParams) {
51
+ const { impliedSpotVolCorr, atmIV } = regimeParams;
52
+ // k = -correlation * atmIV * annualization
53
+ // The negative sign is because negative correlation (spot down = vol up)
54
+ // should produce a positive k (so that -k/S * Vanna has the right sign)
55
+ const k = -impliedSpotVolCorr * atmIV * Math.sqrt(252);
56
+ // Clamp to reasonable range (2 to 20)
57
+ return Math.max(2, Math.min(20, k));
58
+ }
59
+ /**
60
+ * Apply Gaussian kernel smoothing to map strike-space exposures
61
+ * into price-space at a given evaluation point.
62
+ *
63
+ * weight(K, S) = exp(-((K - S) / lambda)^2)
64
+ *
65
+ * @param strikes - Strike prices where exposures are defined
66
+ * @param values - Exposure values at each strike
67
+ * @param evalPrice - Price level to evaluate at
68
+ * @param lambda - Kernel width in price units
69
+ * @returns Smoothed exposure value at evalPrice
70
+ */
71
+ function kernelSmooth(strikes, values, evalPrice, lambda) {
72
+ let weightedSum = 0;
73
+ let weightSum = 0;
74
+ for (let i = 0; i < strikes.length; i++) {
75
+ const dist = (strikes[i] - evalPrice) / lambda;
76
+ const weight = Math.exp(-(dist * dist));
77
+ weightedSum += values[i] * weight;
78
+ weightSum += weight;
79
+ }
80
+ return weightSum > 0 ? weightedSum / weightSum : 0;
81
+ }
82
+ /**
83
+ * Compute the hedge impulse curve across a price grid.
84
+ *
85
+ * The hedge impulse H(S) at each price level S combines gamma and vanna
86
+ * exposures via the empirical spot-vol coupling relationship:
87
+ *
88
+ * H(S) = GEX_smoothed(S) - (k / S) * VEX_smoothed(S)
89
+ *
90
+ * where:
91
+ * - GEX_smoothed(S) is the kernel-smoothed gamma exposure at S
92
+ * - VEX_smoothed(S) is the kernel-smoothed vanna exposure at S
93
+ * - k is the spot-vol coupling coefficient derived from the IV surface
94
+ *
95
+ * Positive H(S) means a spot move toward S triggers dealer buying that
96
+ * dampens the move (mean-reversion / "wall" behavior).
97
+ *
98
+ * Negative H(S) means a spot move toward S triggers dealer selling that
99
+ * amplifies the move (trend acceleration / "vacuum" behavior).
100
+ *
101
+ * @param exposures - Per-strike gamma, vanna, charm exposures
102
+ * @param ivSurface - The IV surface for regime derivation
103
+ * @param config - Optional configuration for grid and kernel parameters
104
+ * @returns Complete hedge impulse curve analysis
105
+ */
106
+ function computeHedgeImpulseCurve(exposures, ivSurface, config = {}) {
107
+ const { rangePercent = 3, stepPercent = 0.05, kernelWidthStrikes = 2, } = config;
108
+ const spot = exposures.spotPrice;
109
+ // Derive regime params and spot-vol coupling from IV surface
110
+ const regimeParams = (0, regime_1.deriveRegimeParams)(ivSurface, spot);
111
+ const k = deriveSpotVolCoupling(regimeParams);
112
+ // Extract strike-space data
113
+ const strikes = exposures.strikeExposures.map(s => s.strikePrice);
114
+ const gexValues = exposures.strikeExposures.map(s => s.gammaExposure);
115
+ const vexValues = exposures.strikeExposures.map(s => s.vannaExposure);
116
+ // Detect strike spacing and compute kernel width in price units
117
+ const strikeSpacing = detectStrikeSpacing(strikes);
118
+ const lambda = kernelWidthStrikes * strikeSpacing;
119
+ // Build price grid
120
+ const gridMin = spot * (1 - rangePercent / 100);
121
+ const gridMax = spot * (1 + rangePercent / 100);
122
+ const gridStep = spot * (stepPercent / 100);
123
+ const curve = [];
124
+ for (let price = gridMin; price <= gridMax; price += gridStep) {
125
+ const gamma = kernelSmooth(strikes, gexValues, price, lambda);
126
+ const vanna = kernelSmooth(strikes, vexValues, price, lambda);
127
+ // H(S) = Gamma(S) - (k / S) * Vanna(S)
128
+ const impulse = gamma - (k / price) * vanna;
129
+ curve.push({ price, gamma, vanna, impulse });
130
+ }
131
+ // Compute impulse at current spot
132
+ const impulseAtSpot = interpolateImpulseAtPrice(curve, spot);
133
+ // Compute slope at current spot (dH/dS via central difference)
134
+ const slopeAtSpot = computeSlopeAtPrice(curve, spot);
135
+ // Find zero crossings
136
+ const zeroCrossings = findZeroCrossings(curve);
137
+ // Find local extrema
138
+ const extrema = findExtrema(curve);
139
+ // Compute directional asymmetry
140
+ const asymmetry = computeAsymmetry(curve, spot);
141
+ // Classify regime
142
+ const regime = classifyRegime(impulseAtSpot, slopeAtSpot, asymmetry, curve, spot);
143
+ // Find nearest attractors (positive impulse basins)
144
+ const basinsAbove = extrema.filter(e => e.type === 'basin' && e.price > spot);
145
+ const basinsBelow = extrema.filter(e => e.type === 'basin' && e.price < spot);
146
+ const nearestAttractorAbove = basinsAbove.length > 0
147
+ ? basinsAbove.reduce((a, b) => a.price < b.price ? a : b).price
148
+ : null;
149
+ const nearestAttractorBelow = basinsBelow.length > 0
150
+ ? basinsBelow.reduce((a, b) => a.price > b.price ? a : b).price
151
+ : null;
152
+ return {
153
+ spot,
154
+ expiration: exposures.expiration,
155
+ computedAt: Date.now(),
156
+ spotVolCoupling: k,
157
+ kernelWidth: lambda,
158
+ strikeSpacing,
159
+ curve,
160
+ impulseAtSpot,
161
+ slopeAtSpot,
162
+ zeroCrossings,
163
+ extrema,
164
+ asymmetry,
165
+ regime,
166
+ nearestAttractorAbove,
167
+ nearestAttractorBelow,
168
+ };
169
+ }
170
+ /**
171
+ * Interpolate impulse value at an arbitrary price within the curve
172
+ */
173
+ function interpolateImpulseAtPrice(curve, price) {
174
+ if (curve.length === 0)
175
+ return 0;
176
+ if (price <= curve[0].price)
177
+ return curve[0].impulse;
178
+ if (price >= curve[curve.length - 1].price)
179
+ return curve[curve.length - 1].impulse;
180
+ for (let i = 0; i < curve.length - 1; i++) {
181
+ if (curve[i].price <= price && curve[i + 1].price >= price) {
182
+ const t = (price - curve[i].price) / (curve[i + 1].price - curve[i].price);
183
+ return curve[i].impulse + t * (curve[i + 1].impulse - curve[i].impulse);
184
+ }
185
+ }
186
+ return 0;
187
+ }
188
+ /**
189
+ * Compute slope of the impulse curve at a given price via central difference
190
+ */
191
+ function computeSlopeAtPrice(curve, price) {
192
+ if (curve.length < 3)
193
+ return 0;
194
+ // Find bracketing points
195
+ const step = curve[1].price - curve[0].price;
196
+ const above = interpolateImpulseAtPrice(curve, price + step);
197
+ const below = interpolateImpulseAtPrice(curve, price - step);
198
+ return (above - below) / (2 * step);
199
+ }
200
+ /**
201
+ * Find all zero crossings of the impulse curve
202
+ */
203
+ function findZeroCrossings(curve) {
204
+ const crossings = [];
205
+ for (let i = 0; i < curve.length - 1; i++) {
206
+ const a = curve[i].impulse;
207
+ const b = curve[i + 1].impulse;
208
+ if (a * b < 0) {
209
+ // Linear interpolation for crossing price
210
+ const t = Math.abs(a) / (Math.abs(a) + Math.abs(b));
211
+ const crossPrice = curve[i].price + t * (curve[i + 1].price - curve[i].price);
212
+ crossings.push({
213
+ price: crossPrice,
214
+ direction: b > a ? 'rising' : 'falling',
215
+ });
216
+ }
217
+ }
218
+ return crossings;
219
+ }
220
+ /**
221
+ * Find local extrema (basins and peaks) of the impulse curve
222
+ */
223
+ function findExtrema(curve) {
224
+ const extrema = [];
225
+ for (let i = 1; i < curve.length - 1; i++) {
226
+ const prev = curve[i - 1].impulse;
227
+ const curr = curve[i].impulse;
228
+ const next = curve[i + 1].impulse;
229
+ // Local maximum
230
+ if (curr > prev && curr > next && curr > 0) {
231
+ extrema.push({
232
+ price: curve[i].price,
233
+ impulse: curr,
234
+ type: 'basin', // positive max = attractor
235
+ });
236
+ }
237
+ // Local minimum
238
+ if (curr < prev && curr < next && curr < 0) {
239
+ extrema.push({
240
+ price: curve[i].price,
241
+ impulse: curr,
242
+ type: 'peak', // negative min = accelerator
243
+ });
244
+ }
245
+ }
246
+ return extrema;
247
+ }
248
+ /**
249
+ * Compute directional asymmetry by integrating impulse above and below spot.
250
+ *
251
+ * The integration uses the trapezoidal rule over the default range of ±0.5% of spot.
252
+ * The side with more negative impulse is the path of least resistance.
253
+ */
254
+ function computeAsymmetry(curve, spot, integrationRangePercent = 0.5) {
255
+ const rangePrice = spot * (integrationRangePercent / 100);
256
+ // Integrate from spot to spot + range (upside)
257
+ let upsideIntegral = 0;
258
+ let downsideIntegral = 0;
259
+ const step = curve.length > 1 ? curve[1].price - curve[0].price : 1;
260
+ for (const point of curve) {
261
+ if (point.price > spot && point.price <= spot + rangePrice) {
262
+ upsideIntegral += point.impulse * step;
263
+ }
264
+ if (point.price < spot && point.price >= spot - rangePrice) {
265
+ downsideIntegral += point.impulse * step;
266
+ }
267
+ }
268
+ // More negative integral = more acceleration = path of least resistance
269
+ let bias = 'neutral';
270
+ const threshold = Math.max(Math.abs(upsideIntegral), Math.abs(downsideIntegral)) * 0.1;
271
+ if (upsideIntegral < downsideIntegral - threshold) {
272
+ bias = 'up'; // Upside has more negative impulse = price gets pulled up
273
+ }
274
+ else if (downsideIntegral < upsideIntegral - threshold) {
275
+ bias = 'down'; // Downside has more negative impulse = price gets pulled down
276
+ }
277
+ const denominator = Math.abs(downsideIntegral) || 1e-10;
278
+ const asymmetryRatio = Math.abs(upsideIntegral) / denominator;
279
+ return {
280
+ upside: upsideIntegral,
281
+ downside: downsideIntegral,
282
+ integrationRangePercent,
283
+ bias,
284
+ asymmetryRatio,
285
+ };
286
+ }
287
+ /**
288
+ * Classify the current regime based on the impulse curve characteristics
289
+ */
290
+ function classifyRegime(impulseAtSpot, slopeAtSpot, asymmetry, curve, spot) {
291
+ // Compute a threshold based on the curve's overall scale
292
+ const impulseValues = curve.map(p => Math.abs(p.impulse));
293
+ const meanAbsImpulse = impulseValues.reduce((a, b) => a + b, 0) / impulseValues.length;
294
+ if (meanAbsImpulse === 0)
295
+ return 'neutral';
296
+ const normalizedAtSpot = impulseAtSpot / meanAbsImpulse;
297
+ // Strong positive impulse at spot = pinned
298
+ if (normalizedAtSpot > 0.5) {
299
+ return 'pinned';
300
+ }
301
+ // Negative impulse at spot = expansion potential
302
+ if (normalizedAtSpot < -0.3) {
303
+ if (asymmetry.bias === 'up')
304
+ return 'squeeze-up';
305
+ if (asymmetry.bias === 'down')
306
+ return 'squeeze-down';
307
+ return 'expansion';
308
+ }
309
+ // Weak impulse at spot but strong asymmetry
310
+ if (asymmetry.bias === 'up' && asymmetry.asymmetryRatio > 1.5)
311
+ return 'squeeze-up';
312
+ if (asymmetry.bias === 'down' && asymmetry.asymmetryRatio > 1.5)
313
+ return 'squeeze-down';
314
+ return 'neutral';
315
+ }
@@ -0,0 +1,33 @@
1
+ import { ExposurePerExpiry, IVSurface } from '../types';
2
+ import { HedgeImpulseConfig, CharmIntegralConfig, HedgeFlowAnalysis } from './types';
3
+ export type { MarketRegime, RegimeParams, HedgeImpulseConfig, HedgeImpulsePoint, HedgeImpulseCurve, ZeroCrossing, ImpulseExtremum, DirectionalAsymmetry, ImpulseRegime, CharmIntegralConfig, CharmBucket, CharmIntegral, HedgeFlowAnalysis, } from './types';
4
+ export { deriveRegimeParams, interpolateIVAtStrike } from './regime';
5
+ export { computeHedgeImpulseCurve } from './curve';
6
+ export { computeCharmIntegral } from './charm';
7
+ /**
8
+ * Compute a complete hedge flow analysis for a single expiration.
9
+ *
10
+ * This combines the hedge impulse curve (conditional: what happens if
11
+ * price moves) with the charm integral (unconditional: what happens
12
+ * from time passage alone).
13
+ *
14
+ * The two analyses are intentionally kept separate because they answer
15
+ * orthogonal questions:
16
+ *
17
+ * - Impulse curve: "If spot moves to price S, do dealers amplify or
18
+ * dampen the move?" (Left panel)
19
+ * - Charm integral: "If spot does nothing, where does time decay
20
+ * push things?" (Right panel)
21
+ *
22
+ * Both update in real-time as:
23
+ * - Spot price changes → impulse curve re-evaluates k and kernel positions
24
+ * - Option quotes change → IV surface updates → regime params change
25
+ * - Open interest changes → exposure recalculation → both panels update
26
+ *
27
+ * @param exposures - Per-strike gamma, vanna, charm exposures for one expiration
28
+ * @param ivSurface - IV surface for the same expiration (used for regime derivation)
29
+ * @param impulseConfig - Optional config for the impulse curve grid and kernel
30
+ * @param charmConfig - Optional config for the charm integral time stepping
31
+ * @returns Combined hedge flow analysis
32
+ */
33
+ export declare function analyzeHedgeFlow(exposures: ExposurePerExpiry, ivSurface: IVSurface, impulseConfig?: HedgeImpulseConfig, charmConfig?: CharmIntegralConfig): HedgeFlowAnalysis;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeCharmIntegral = exports.computeHedgeImpulseCurve = exports.interpolateIVAtStrike = exports.deriveRegimeParams = void 0;
4
+ exports.analyzeHedgeFlow = analyzeHedgeFlow;
5
+ const regime_1 = require("./regime");
6
+ const curve_1 = require("./curve");
7
+ const charm_1 = require("./charm");
8
+ // Re-export regime functions
9
+ var regime_2 = require("./regime");
10
+ Object.defineProperty(exports, "deriveRegimeParams", { enumerable: true, get: function () { return regime_2.deriveRegimeParams; } });
11
+ Object.defineProperty(exports, "interpolateIVAtStrike", { enumerable: true, get: function () { return regime_2.interpolateIVAtStrike; } });
12
+ // Re-export computation functions
13
+ var curve_2 = require("./curve");
14
+ Object.defineProperty(exports, "computeHedgeImpulseCurve", { enumerable: true, get: function () { return curve_2.computeHedgeImpulseCurve; } });
15
+ var charm_2 = require("./charm");
16
+ Object.defineProperty(exports, "computeCharmIntegral", { enumerable: true, get: function () { return charm_2.computeCharmIntegral; } });
17
+ /**
18
+ * Compute a complete hedge flow analysis for a single expiration.
19
+ *
20
+ * This combines the hedge impulse curve (conditional: what happens if
21
+ * price moves) with the charm integral (unconditional: what happens
22
+ * from time passage alone).
23
+ *
24
+ * The two analyses are intentionally kept separate because they answer
25
+ * orthogonal questions:
26
+ *
27
+ * - Impulse curve: "If spot moves to price S, do dealers amplify or
28
+ * dampen the move?" (Left panel)
29
+ * - Charm integral: "If spot does nothing, where does time decay
30
+ * push things?" (Right panel)
31
+ *
32
+ * Both update in real-time as:
33
+ * - Spot price changes → impulse curve re-evaluates k and kernel positions
34
+ * - Option quotes change → IV surface updates → regime params change
35
+ * - Open interest changes → exposure recalculation → both panels update
36
+ *
37
+ * @param exposures - Per-strike gamma, vanna, charm exposures for one expiration
38
+ * @param ivSurface - IV surface for the same expiration (used for regime derivation)
39
+ * @param impulseConfig - Optional config for the impulse curve grid and kernel
40
+ * @param charmConfig - Optional config for the charm integral time stepping
41
+ * @returns Combined hedge flow analysis
42
+ */
43
+ function analyzeHedgeFlow(exposures, ivSurface, impulseConfig = {}, charmConfig = {}) {
44
+ const regimeParams = (0, regime_1.deriveRegimeParams)(ivSurface, exposures.spotPrice);
45
+ const impulseCurve = (0, curve_1.computeHedgeImpulseCurve)(exposures, ivSurface, impulseConfig);
46
+ const charmIntegral = (0, charm_1.computeCharmIntegral)(exposures, charmConfig);
47
+ return {
48
+ impulseCurve,
49
+ charmIntegral,
50
+ regimeParams,
51
+ };
52
+ }
@@ -0,0 +1,7 @@
1
+ import { IVSurface } from '../types';
2
+ import { RegimeParams } from './types';
3
+ /**
4
+ * Derive regime parameters from the IV surface
5
+ */
6
+ export declare function deriveRegimeParams(ivSurface: IVSurface, spot: number): RegimeParams;
7
+ export declare function interpolateIVAtStrike(strikes: number[], ivs: number[], targetStrike: number): number;
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deriveRegimeParams = deriveRegimeParams;
4
+ exports.interpolateIVAtStrike = interpolateIVAtStrike;
5
+ /**
6
+ * Derive regime parameters from the IV surface
7
+ */
8
+ function deriveRegimeParams(ivSurface, spot) {
9
+ const { strikes, smoothedIVs } = ivSurface;
10
+ const atmIV = interpolateIVAtStrike(strikes, smoothedIVs, spot) / 100;
11
+ const skew = calculateSkewAtSpot(strikes, smoothedIVs, spot);
12
+ const impliedSpotVolCorr = skewToCorrelation(skew);
13
+ const curvature = calculateCurvatureAtSpot(strikes, smoothedIVs, spot);
14
+ const impliedVolOfVol = curvatureToVolOfVol(curvature, atmIV);
15
+ const regime = ivToRegime(atmIV);
16
+ const expectedDailySpotMove = atmIV / Math.sqrt(252);
17
+ const expectedDailyVolMove = impliedVolOfVol / Math.sqrt(252);
18
+ return {
19
+ atmIV,
20
+ impliedSpotVolCorr,
21
+ impliedVolOfVol,
22
+ regime,
23
+ expectedDailySpotMove,
24
+ expectedDailyVolMove,
25
+ };
26
+ }
27
+ function skewToCorrelation(skew) {
28
+ const SKEW_TO_CORR_SCALE = 0.15;
29
+ return Math.max(-0.95, Math.min(0.5, skew * SKEW_TO_CORR_SCALE));
30
+ }
31
+ function curvatureToVolOfVol(curvature, atmIV) {
32
+ const VOL_OF_VOL_SCALE = 2.0;
33
+ return Math.sqrt(Math.abs(curvature)) * VOL_OF_VOL_SCALE * atmIV;
34
+ }
35
+ function ivToRegime(atmIV) {
36
+ if (atmIV < 0.15)
37
+ return 'calm';
38
+ if (atmIV < 0.20)
39
+ return 'normal';
40
+ if (atmIV < 0.35)
41
+ return 'stressed';
42
+ return 'crisis';
43
+ }
44
+ function interpolateIVAtStrike(strikes, ivs, targetStrike) {
45
+ if (strikes.length === 0 || ivs.length === 0)
46
+ return 20;
47
+ if (strikes.length === 1)
48
+ return ivs[0];
49
+ let lower = 0;
50
+ let upper = strikes.length - 1;
51
+ for (let i = 0; i < strikes.length - 1; i++) {
52
+ if (strikes[i] <= targetStrike && strikes[i + 1] >= targetStrike) {
53
+ lower = i;
54
+ upper = i + 1;
55
+ break;
56
+ }
57
+ }
58
+ if (targetStrike <= strikes[0])
59
+ return ivs[0];
60
+ if (targetStrike >= strikes[strikes.length - 1])
61
+ return ivs[ivs.length - 1];
62
+ const t = (targetStrike - strikes[lower]) / (strikes[upper] - strikes[lower]);
63
+ return ivs[lower] + t * (ivs[upper] - ivs[lower]);
64
+ }
65
+ function calculateSkewAtSpot(strikes, ivs, spot) {
66
+ if (strikes.length < 2)
67
+ return 0;
68
+ let lowerIdx = 0;
69
+ let upperIdx = strikes.length - 1;
70
+ for (let i = 0; i < strikes.length - 1; i++) {
71
+ if (strikes[i] <= spot && strikes[i + 1] >= spot) {
72
+ lowerIdx = i;
73
+ upperIdx = i + 1;
74
+ break;
75
+ }
76
+ }
77
+ const dIV = ivs[upperIdx] - ivs[lowerIdx];
78
+ const dK = strikes[upperIdx] - strikes[lowerIdx];
79
+ return dK > 0 ? (dIV / dK) * spot : 0;
80
+ }
81
+ function calculateCurvatureAtSpot(strikes, ivs, spot) {
82
+ if (strikes.length < 3)
83
+ return 0;
84
+ let centerIdx = 0;
85
+ for (let i = 0; i < strikes.length; i++) {
86
+ if (Math.abs(strikes[i] - spot) < Math.abs(strikes[centerIdx] - spot)) {
87
+ centerIdx = i;
88
+ }
89
+ }
90
+ if (centerIdx === 0 || centerIdx === strikes.length - 1)
91
+ return 0;
92
+ const h = (strikes[centerIdx + 1] - strikes[centerIdx - 1]) / 2;
93
+ if (h <= 0)
94
+ return 0;
95
+ const ivMinus = ivs[centerIdx - 1];
96
+ const iv = ivs[centerIdx];
97
+ const ivPlus = ivs[centerIdx + 1];
98
+ return ((ivPlus - 2 * iv + ivMinus) / (h * h)) * spot * spot;
99
+ }