@fullstackcraftllc/floe 0.0.14 → 0.0.16
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.
- package/README.md +1 -1
- package/dist/client/FloeClient.d.ts +5 -1
- package/dist/client/FloeClient.js +54 -0
- package/dist/client/brokers/IBKRClient.d.ts +324 -0
- package/dist/client/brokers/IBKRClient.js +797 -0
- package/dist/exposure/index.d.ts +15 -10
- package/dist/exposure/index.js +164 -106
- package/dist/hedgeflow/charm.d.ts +23 -0
- package/dist/hedgeflow/charm.js +113 -0
- package/dist/hedgeflow/curve.d.ts +27 -0
- package/dist/hedgeflow/curve.js +315 -0
- package/dist/hedgeflow/index.d.ts +33 -0
- package/dist/hedgeflow/index.js +52 -0
- package/dist/hedgeflow/regime.d.ts +7 -0
- package/dist/hedgeflow/regime.js +99 -0
- package/dist/hedgeflow/types.d.ts +185 -0
- package/dist/hedgeflow/types.js +2 -0
- package/dist/impliedpdf/adjusted.d.ts +5 -2
- package/dist/impliedpdf/adjusted.js +5 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +19 -19
- package/dist/iv/index.d.ts +52 -0
- package/dist/iv/index.js +287 -0
- package/dist/iv/types.d.ts +40 -0
- package/dist/iv/types.js +2 -0
- package/dist/rv/index.d.ts +26 -0
- package/dist/rv/index.js +81 -0
- package/dist/rv/types.d.ts +32 -0
- package/dist/rv/types.js +2 -0
- package/dist/types/index.d.ts +66 -0
- package/package.json +1 -1
package/dist/exposure/index.d.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
import { OptionChain,
|
|
1
|
+
import { OptionChain, ExposureVariantsPerExpiry, ExposureCalculationOptions, IVSurface } from '../types';
|
|
2
2
|
/**
|
|
3
|
-
* Calculate
|
|
3
|
+
* Calculate canonical, state-weighted, and flow-delta exposure variants.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* canonical:
|
|
6
|
+
* - GEX: dollars per 1% underlying move
|
|
7
|
+
* - VEX: dollars per 1 vol-point move
|
|
8
|
+
* - CEX: dollars per 1 day of time decay
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* stateWeighted:
|
|
11
|
+
* - Gamma: same as canonical (spot is already the state variable)
|
|
12
|
+
* - Vanna: canonical vanna weighted by strike IV level
|
|
13
|
+
* - Charm: canonical charm weighted by days-to-expiration
|
|
14
|
+
*
|
|
15
|
+
* flowDelta:
|
|
16
|
+
* - Canonical exposure formulas using OI deltas:
|
|
17
|
+
* (liveOpenInterest - openInterest)
|
|
13
18
|
*/
|
|
14
|
-
export declare function calculateGammaVannaCharmExposures(chain: OptionChain, ivSurfaces: IVSurface[]):
|
|
19
|
+
export declare function calculateGammaVannaCharmExposures(chain: OptionChain, ivSurfaces: IVSurface[], options?: ExposureCalculationOptions): ExposureVariantsPerExpiry[];
|
|
15
20
|
/**
|
|
16
21
|
* Calculate shares needed to cover net exposure
|
|
17
22
|
*
|
package/dist/exposure/index.js
CHANGED
|
@@ -6,70 +6,62 @@ const types_1 = require("../types");
|
|
|
6
6
|
const blackscholes_1 = require("../blackscholes");
|
|
7
7
|
const volatility_1 = require("../volatility");
|
|
8
8
|
/**
|
|
9
|
-
* Calculate
|
|
9
|
+
* Calculate canonical, state-weighted, and flow-delta exposure variants.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* canonical:
|
|
12
|
+
* - GEX: dollars per 1% underlying move
|
|
13
|
+
* - VEX: dollars per 1 vol-point move
|
|
14
|
+
* - CEX: dollars per 1 day of time decay
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* stateWeighted:
|
|
17
|
+
* - Gamma: same as canonical (spot is already the state variable)
|
|
18
|
+
* - Vanna: canonical vanna weighted by strike IV level
|
|
19
|
+
* - Charm: canonical charm weighted by days-to-expiration
|
|
20
|
+
*
|
|
21
|
+
* flowDelta:
|
|
22
|
+
* - Canonical exposure formulas using OI deltas:
|
|
23
|
+
* (liveOpenInterest - openInterest)
|
|
19
24
|
*/
|
|
20
|
-
function calculateGammaVannaCharmExposures(chain, ivSurfaces) {
|
|
21
|
-
const { spot, riskFreeRate, dividendYield, options } = chain;
|
|
25
|
+
function calculateGammaVannaCharmExposures(chain, ivSurfaces, options = {}) {
|
|
26
|
+
const { spot, riskFreeRate, dividendYield, options: chainOptions } = chain;
|
|
27
|
+
const asOfTimestamp = options.asOfTimestamp ?? Date.now();
|
|
22
28
|
const exposureRows = [];
|
|
23
|
-
// Get unique expirations from options
|
|
24
29
|
const expirationsSet = new Set();
|
|
25
|
-
for (const option of
|
|
30
|
+
for (const option of chainOptions) {
|
|
26
31
|
expirationsSet.add(option.expirationTimestamp);
|
|
27
32
|
}
|
|
28
33
|
const expirations = Array.from(expirationsSet).sort((a, b) => a - b);
|
|
29
|
-
|
|
34
|
+
const putOptionsByKey = new Map();
|
|
35
|
+
for (const option of chainOptions) {
|
|
36
|
+
if (option.optionType === 'put') {
|
|
37
|
+
putOptionsByKey.set(getOptionKey(option.expirationTimestamp, option.strike), option);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
30
40
|
for (const expiration of expirations) {
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
if (expiration < asOfTimestamp) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const timeToExpirationInYears = (expiration - asOfTimestamp) / types_1.MILLISECONDS_PER_YEAR;
|
|
45
|
+
if (timeToExpirationInYears <= 0) {
|
|
33
46
|
continue;
|
|
34
47
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
let strikeOfMaxGamma = 0.0;
|
|
40
|
-
let strikeOfMinGamma = 0.0;
|
|
41
|
-
let strikeOfMaxVanna = 0.0;
|
|
42
|
-
let strikeOfMinVanna = 0.0;
|
|
43
|
-
let strikeOfMaxCharm = 0.0;
|
|
44
|
-
let strikeOfMinCharm = 0.0;
|
|
45
|
-
let strikeOfMaxNet = 0.0;
|
|
46
|
-
let strikeOfMinNet = 0.0;
|
|
47
|
-
const strikeExposures = [];
|
|
48
|
-
// Process all call options first
|
|
49
|
-
for (const callOption of options) {
|
|
50
|
-
// Check if this option is at the expiration we are looking at
|
|
51
|
-
if (callOption.expirationTimestamp !== expiration || callOption.optionType === 'put') {
|
|
48
|
+
const timeToExpirationInDays = Math.max(timeToExpirationInYears * types_1.DAYS_PER_YEAR, 0);
|
|
49
|
+
const strikeExposureVariants = [];
|
|
50
|
+
for (const callOption of chainOptions) {
|
|
51
|
+
if (callOption.expirationTimestamp !== expiration || callOption.optionType !== 'call') {
|
|
52
52
|
continue;
|
|
53
53
|
}
|
|
54
|
-
|
|
55
|
-
const putOption = options.find((opt) => opt.expirationTimestamp === expiration &&
|
|
56
|
-
opt.optionType === 'put' &&
|
|
57
|
-
opt.strike === callOption.strike);
|
|
54
|
+
const putOption = putOptionsByKey.get(getOptionKey(expiration, callOption.strike));
|
|
58
55
|
if (!putOption) {
|
|
59
|
-
continue;
|
|
56
|
+
continue;
|
|
60
57
|
}
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
const putIVAtStrike = (0, volatility_1.getIVForStrike)(ivSurfaces, expiration, 'put', putOption.strike);
|
|
64
|
-
// Get time to expiration in years
|
|
65
|
-
const timeToExpirationInYears = (0, blackscholes_1.getTimeToExpirationInYears)(expiration);
|
|
66
|
-
// Calculate Greeks for both call and put
|
|
67
|
-
// Rates are already decimals in OptionChain, IV from surface is percentage
|
|
58
|
+
const callIVAtStrike = resolveIVPercent((0, volatility_1.getIVForStrike)(ivSurfaces, expiration, 'call', callOption.strike), callOption.impliedVolatility);
|
|
59
|
+
const putIVAtStrike = resolveIVPercent((0, volatility_1.getIVForStrike)(ivSurfaces, expiration, 'put', putOption.strike), putOption.impliedVolatility);
|
|
68
60
|
const callGreeks = (0, blackscholes_1.calculateGreeks)({
|
|
69
61
|
spot,
|
|
70
62
|
strike: callOption.strike,
|
|
71
63
|
timeToExpiry: timeToExpirationInYears,
|
|
72
|
-
volatility: callIVAtStrike / 100.0,
|
|
64
|
+
volatility: callIVAtStrike / 100.0,
|
|
73
65
|
riskFreeRate,
|
|
74
66
|
dividendYield,
|
|
75
67
|
optionType: 'call',
|
|
@@ -83,74 +75,33 @@ function calculateGammaVannaCharmExposures(chain, ivSurfaces) {
|
|
|
83
75
|
dividendYield,
|
|
84
76
|
optionType: 'put',
|
|
85
77
|
});
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const vannaExposureForStrike = -callOption.openInterest * callGreeks.vanna * (spot * 100.0) * callIVAtStrike * 0.01 +
|
|
95
|
-
putOption.openInterest * putGreeks.vanna * (spot * 100.0) * putIVAtStrike * 0.01;
|
|
96
|
-
// Charm: second order with respect to price and time
|
|
97
|
-
// Already normalized per day in calculateGreeks
|
|
98
|
-
const charmExposureForStrike = -callOption.openInterest * callGreeks.charm * (spot * 100.0) * types_1.DAYS_PER_YEAR * timeToExpirationInYears +
|
|
99
|
-
putOption.openInterest * putGreeks.charm * (spot * 100.0) * types_1.DAYS_PER_YEAR * timeToExpirationInYears;
|
|
100
|
-
// NaN checks
|
|
101
|
-
const gammaExposure = isNaN(gammaExposureForStrike) ? 0.0 : gammaExposureForStrike;
|
|
102
|
-
const vannaExposure = isNaN(vannaExposureForStrike) ? 0.0 : vannaExposureForStrike;
|
|
103
|
-
const charmExposure = isNaN(charmExposureForStrike) ? 0.0 : charmExposureForStrike;
|
|
104
|
-
// Add to totals
|
|
105
|
-
totalGammaExposure += gammaExposure;
|
|
106
|
-
totalVannaExposure += vannaExposure;
|
|
107
|
-
totalCharmExposure += charmExposure;
|
|
108
|
-
// Add to strike exposures
|
|
109
|
-
strikeExposures.push({
|
|
78
|
+
const callOpenInterest = sanitizeFinite(callOption.openInterest);
|
|
79
|
+
const putOpenInterest = sanitizeFinite(putOption.openInterest);
|
|
80
|
+
const canonical = calculateCanonicalVector(spot, callOpenInterest, putOpenInterest, callGreeks.gamma, putGreeks.gamma, callGreeks.vanna, putGreeks.vanna, callGreeks.charm, putGreeks.charm);
|
|
81
|
+
const stateWeighted = calculateStateWeightedVector(spot, callOpenInterest, putOpenInterest, callGreeks.vanna, putGreeks.vanna, callGreeks.charm, putGreeks.charm, callIVAtStrike, putIVAtStrike, timeToExpirationInDays, canonical.gammaExposure);
|
|
82
|
+
const callFlowDelta = resolveFlowDeltaOpenInterest(callOption.openInterest, callOption.liveOpenInterest);
|
|
83
|
+
const putFlowDelta = resolveFlowDeltaOpenInterest(putOption.openInterest, putOption.liveOpenInterest);
|
|
84
|
+
const flowDelta = calculateCanonicalVector(spot, callFlowDelta, putFlowDelta, callGreeks.gamma, putGreeks.gamma, callGreeks.vanna, putGreeks.vanna, callGreeks.charm, putGreeks.charm);
|
|
85
|
+
strikeExposureVariants.push({
|
|
110
86
|
strikePrice: callOption.strike,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
netExposure: gammaExposure + vannaExposure + charmExposure,
|
|
87
|
+
canonical,
|
|
88
|
+
stateWeighted,
|
|
89
|
+
flowDelta,
|
|
115
90
|
});
|
|
116
91
|
}
|
|
117
|
-
if (
|
|
118
|
-
continue;
|
|
92
|
+
if (strikeExposureVariants.length === 0) {
|
|
93
|
+
continue;
|
|
119
94
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
strikeOfMinGamma = strikeExposures[strikeExposures.length - 1].strikePrice;
|
|
124
|
-
// Sort by vanna exposure and find extremes
|
|
125
|
-
strikeExposures.sort((a, b) => b.vannaExposure - a.vannaExposure);
|
|
126
|
-
strikeOfMaxVanna = strikeExposures[0].strikePrice;
|
|
127
|
-
strikeOfMinVanna = strikeExposures[strikeExposures.length - 1].strikePrice;
|
|
128
|
-
// Sort by charm exposure and find extremes
|
|
129
|
-
strikeExposures.sort((a, b) => b.charmExposure - a.charmExposure);
|
|
130
|
-
strikeOfMaxCharm = strikeExposures[0].strikePrice;
|
|
131
|
-
strikeOfMinCharm = strikeExposures[strikeExposures.length - 1].strikePrice;
|
|
132
|
-
// Sort by net exposure and find extremes
|
|
133
|
-
strikeExposures.sort((a, b) => b.netExposure - a.netExposure);
|
|
134
|
-
strikeOfMaxNet = strikeExposures[0].strikePrice;
|
|
135
|
-
strikeOfMinNet = strikeExposures[strikeExposures.length - 1].strikePrice;
|
|
136
|
-
const totalNetExposure = totalGammaExposure + totalVannaExposure + totalCharmExposure;
|
|
137
|
-
// Add exposure row
|
|
95
|
+
const canonical = buildModeBreakdown(strikeExposureVariants, 'canonical');
|
|
96
|
+
const stateWeighted = buildModeBreakdown(strikeExposureVariants, 'stateWeighted');
|
|
97
|
+
const flowDelta = buildModeBreakdown(strikeExposureVariants, 'flowDelta');
|
|
138
98
|
exposureRows.push({
|
|
139
99
|
spotPrice: spot,
|
|
140
100
|
expiration,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
strikeOfMaxGamma,
|
|
146
|
-
strikeOfMinGamma,
|
|
147
|
-
strikeOfMaxVanna,
|
|
148
|
-
strikeOfMinVanna,
|
|
149
|
-
strikeOfMaxCharm,
|
|
150
|
-
strikeOfMinCharm,
|
|
151
|
-
strikeOfMaxNet,
|
|
152
|
-
strikeOfMinNet,
|
|
153
|
-
strikeExposures
|
|
101
|
+
canonical,
|
|
102
|
+
stateWeighted,
|
|
103
|
+
flowDelta,
|
|
104
|
+
strikeExposureVariants,
|
|
154
105
|
});
|
|
155
106
|
}
|
|
156
107
|
return exposureRows;
|
|
@@ -210,3 +161,110 @@ function calculateSharesNeededToCover(sharesOutstanding, totalNetExposure, under
|
|
|
210
161
|
resultingSpotToCover: resultingPrice,
|
|
211
162
|
};
|
|
212
163
|
}
|
|
164
|
+
function getOptionKey(expiration, strike) {
|
|
165
|
+
return `${expiration}:${strike}`;
|
|
166
|
+
}
|
|
167
|
+
function calculateCanonicalVector(spot, callPosition, putPosition, callGamma, putGamma, callVanna, putVanna, callCharm, putCharm) {
|
|
168
|
+
const gammaExposure = -callPosition * callGamma * (spot * 100.0) * spot * 0.01 +
|
|
169
|
+
putPosition * putGamma * (spot * 100.0) * spot * 0.01;
|
|
170
|
+
const vannaExposure = -callPosition * callVanna * (spot * 100.0) * 0.01 +
|
|
171
|
+
putPosition * putVanna * (spot * 100.0) * 0.01;
|
|
172
|
+
const charmExposure = -callPosition * callCharm * (spot * 100.0) +
|
|
173
|
+
putPosition * putCharm * (spot * 100.0);
|
|
174
|
+
return sanitizeVector({
|
|
175
|
+
gammaExposure,
|
|
176
|
+
vannaExposure,
|
|
177
|
+
charmExposure,
|
|
178
|
+
netExposure: gammaExposure + vannaExposure + charmExposure,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function calculateStateWeightedVector(spot, callPosition, putPosition, callVanna, putVanna, callCharm, putCharm, callIVPercent, putIVPercent, timeToExpirationInDays, canonicalGammaExposure) {
|
|
182
|
+
const callIVLevel = Math.max(callIVPercent * 0.01, 0);
|
|
183
|
+
const putIVLevel = Math.max(putIVPercent * 0.01, 0);
|
|
184
|
+
// Gamma already uses instantaneous price scaling in canonical GEX.
|
|
185
|
+
const gammaExposure = canonicalGammaExposure;
|
|
186
|
+
const vannaExposure = -callPosition * callVanna * (spot * 100.0) * 0.01 * callIVLevel +
|
|
187
|
+
putPosition * putVanna * (spot * 100.0) * 0.01 * putIVLevel;
|
|
188
|
+
const canonicalCharmComponent = -callPosition * callCharm * (spot * 100.0) +
|
|
189
|
+
putPosition * putCharm * (spot * 100.0);
|
|
190
|
+
const charmExposure = canonicalCharmComponent * Math.max(timeToExpirationInDays, 0);
|
|
191
|
+
return sanitizeVector({
|
|
192
|
+
gammaExposure,
|
|
193
|
+
vannaExposure,
|
|
194
|
+
charmExposure,
|
|
195
|
+
netExposure: gammaExposure + vannaExposure + charmExposure,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function resolveFlowDeltaOpenInterest(openInterest, liveOpenInterest) {
|
|
199
|
+
if (typeof liveOpenInterest !== 'number' || !isFinite(liveOpenInterest)) {
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
return sanitizeFinite(liveOpenInterest - openInterest);
|
|
203
|
+
}
|
|
204
|
+
function resolveIVPercent(ivFromSurface, optionImpliedVolatilityDecimal) {
|
|
205
|
+
if (isFinite(ivFromSurface) && ivFromSurface > 0) {
|
|
206
|
+
return ivFromSurface;
|
|
207
|
+
}
|
|
208
|
+
const fallback = optionImpliedVolatilityDecimal * 100.0;
|
|
209
|
+
if (isFinite(fallback) && fallback > 0) {
|
|
210
|
+
return fallback;
|
|
211
|
+
}
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
function buildModeBreakdown(strikeExposureVariants, mode) {
|
|
215
|
+
const strikeExposures = strikeExposureVariants.map((strike) => ({
|
|
216
|
+
strikePrice: strike.strikePrice,
|
|
217
|
+
...strike[mode],
|
|
218
|
+
}));
|
|
219
|
+
if (strikeExposures.length === 0) {
|
|
220
|
+
return {
|
|
221
|
+
totalGammaExposure: 0,
|
|
222
|
+
totalVannaExposure: 0,
|
|
223
|
+
totalCharmExposure: 0,
|
|
224
|
+
totalNetExposure: 0,
|
|
225
|
+
strikeOfMaxGamma: 0,
|
|
226
|
+
strikeOfMinGamma: 0,
|
|
227
|
+
strikeOfMaxVanna: 0,
|
|
228
|
+
strikeOfMinVanna: 0,
|
|
229
|
+
strikeOfMaxCharm: 0,
|
|
230
|
+
strikeOfMinCharm: 0,
|
|
231
|
+
strikeOfMaxNet: 0,
|
|
232
|
+
strikeOfMinNet: 0,
|
|
233
|
+
strikeExposures: [],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const totalGammaExposure = strikeExposures.reduce((sum, s) => sum + s.gammaExposure, 0);
|
|
237
|
+
const totalVannaExposure = strikeExposures.reduce((sum, s) => sum + s.vannaExposure, 0);
|
|
238
|
+
const totalCharmExposure = strikeExposures.reduce((sum, s) => sum + s.charmExposure, 0);
|
|
239
|
+
const totalNetExposure = totalGammaExposure + totalVannaExposure + totalCharmExposure;
|
|
240
|
+
const byGamma = [...strikeExposures].sort((a, b) => b.gammaExposure - a.gammaExposure);
|
|
241
|
+
const byVanna = [...strikeExposures].sort((a, b) => b.vannaExposure - a.vannaExposure);
|
|
242
|
+
const byCharm = [...strikeExposures].sort((a, b) => b.charmExposure - a.charmExposure);
|
|
243
|
+
const byNet = [...strikeExposures].sort((a, b) => b.netExposure - a.netExposure);
|
|
244
|
+
return {
|
|
245
|
+
totalGammaExposure: sanitizeFinite(totalGammaExposure),
|
|
246
|
+
totalVannaExposure: sanitizeFinite(totalVannaExposure),
|
|
247
|
+
totalCharmExposure: sanitizeFinite(totalCharmExposure),
|
|
248
|
+
totalNetExposure: sanitizeFinite(totalNetExposure),
|
|
249
|
+
strikeOfMaxGamma: byGamma[0].strikePrice,
|
|
250
|
+
strikeOfMinGamma: byGamma[byGamma.length - 1].strikePrice,
|
|
251
|
+
strikeOfMaxVanna: byVanna[0].strikePrice,
|
|
252
|
+
strikeOfMinVanna: byVanna[byVanna.length - 1].strikePrice,
|
|
253
|
+
strikeOfMaxCharm: byCharm[0].strikePrice,
|
|
254
|
+
strikeOfMinCharm: byCharm[byCharm.length - 1].strikePrice,
|
|
255
|
+
strikeOfMaxNet: byNet[0].strikePrice,
|
|
256
|
+
strikeOfMinNet: byNet[byNet.length - 1].strikePrice,
|
|
257
|
+
strikeExposures: byNet,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function sanitizeVector(vector) {
|
|
261
|
+
return {
|
|
262
|
+
gammaExposure: sanitizeFinite(vector.gammaExposure),
|
|
263
|
+
vannaExposure: sanitizeFinite(vector.vannaExposure),
|
|
264
|
+
charmExposure: sanitizeFinite(vector.charmExposure),
|
|
265
|
+
netExposure: sanitizeFinite(vector.netExposure),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function sanitizeFinite(value) {
|
|
269
|
+
return isFinite(value) && !isNaN(value) ? value : 0;
|
|
270
|
+
}
|
|
@@ -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;
|