@fullstackcraftllc/floe 0.0.2 → 0.0.4

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.
@@ -1 +1,278 @@
1
1
  "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.estimateImpliedProbabilityDistribution = estimateImpliedProbabilityDistribution;
4
+ exports.estimateImpliedProbabilityDistributions = estimateImpliedProbabilityDistributions;
5
+ exports.getProbabilityInRange = getProbabilityInRange;
6
+ exports.getCumulativeProbability = getCumulativeProbability;
7
+ exports.getQuantile = getQuantile;
8
+ /**
9
+ * Estimate an implied probability density function (PDF) for a single expiry
10
+ * using Breeden-Litzenberger style numerical differentiation of call prices.
11
+ *
12
+ * This method computes the second derivative of call option prices with respect
13
+ * to strike price, which under risk-neutral pricing gives the probability density
14
+ * of the underlying ending at each strike.
15
+ *
16
+ * @param symbol - Underlying ticker symbol
17
+ * @param underlyingPrice - Current spot/mark price of the underlying
18
+ * @param callOptions - Array of call options for a single expiry (must have bid > 0 and ask > 0)
19
+ * @returns ImpliedProbabilityDistribution with strike-level probabilities and summary statistics
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * const result = estimateImpliedProbabilityDistribution(
24
+ * 'QQQ',
25
+ * 500.00,
26
+ * callOptionsForExpiry
27
+ * );
28
+ *
29
+ * if (result.success) {
30
+ * console.log('Mode:', result.distribution.mostLikelyPrice);
31
+ * console.log('Expected move:', result.distribution.expectedMove);
32
+ * }
33
+ * ```
34
+ */
35
+ function estimateImpliedProbabilityDistribution(symbol, underlyingPrice, callOptions) {
36
+ // Sort call options by strike price ascending
37
+ const sortedOptions = [...callOptions].sort((a, b) => a.strike - b.strike);
38
+ const n = sortedOptions.length;
39
+ if (n < 3) {
40
+ return { success: false, error: 'Not enough data points (need at least 3 call options)' };
41
+ }
42
+ // Get expiration from first option (assuming all same expiry)
43
+ const expiryDate = sortedOptions[0].expirationTimestamp;
44
+ // Estimate second derivative numerically (central difference)
45
+ // f(K) ≈ d²C/dK² where C is the call price
46
+ const strikeProbabilities = new Array(n);
47
+ // Initialize edge cases with zero probability
48
+ strikeProbabilities[0] = { strike: sortedOptions[0].strike, probability: 0 };
49
+ strikeProbabilities[n - 1] = { strike: sortedOptions[n - 1].strike, probability: 0 };
50
+ for (let i = 1; i < n - 1; i++) {
51
+ const kPrev = sortedOptions[i - 1].strike;
52
+ const kCurr = sortedOptions[i].strike;
53
+ const kNext = sortedOptions[i + 1].strike;
54
+ // Use mid prices for stability
55
+ const midPrev = (sortedOptions[i - 1].bid + sortedOptions[i - 1].ask) / 2;
56
+ const midCurr = (sortedOptions[i].bid + sortedOptions[i].ask) / 2;
57
+ const midNext = (sortedOptions[i + 1].bid + sortedOptions[i + 1].ask) / 2;
58
+ const cPrev = midPrev;
59
+ const cNext = midNext;
60
+ // Protect against division by zero if strikes are too close
61
+ const strikeDiff = kNext - kPrev;
62
+ if (Math.abs(strikeDiff) < 1e-9) {
63
+ strikeProbabilities[i] = { strike: kCurr, probability: 0 };
64
+ continue;
65
+ }
66
+ // Second derivative: d²C/dK² ≈ (C(K+) - 2*C(K) + C(K-)) / (ΔK)²
67
+ const d2 = (cNext - 2 * midCurr + cPrev) / Math.pow(strikeDiff, 2);
68
+ // f(K) = e^{rT} * d²C/dK², ignoring discount for simplicity
69
+ // Ensure non-negative probability
70
+ strikeProbabilities[i] = { strike: kCurr, probability: Math.max(d2, 0) };
71
+ }
72
+ // Normalize densities to sum to 1
73
+ let sum = 0;
74
+ for (const sp of strikeProbabilities) {
75
+ sum += sp.probability;
76
+ }
77
+ // Protect against division by zero if all probabilities are 0
78
+ if (sum < 1e-9) {
79
+ return { success: false, error: `Insufficient probability mass to normalize (sum=${sum})` };
80
+ }
81
+ for (let i = 0; i < strikeProbabilities.length; i++) {
82
+ strikeProbabilities[i].probability /= sum;
83
+ }
84
+ // Compute summary statistics
85
+ // Most likely price (mode)
86
+ let mostLikelyPrice = strikeProbabilities[0].strike;
87
+ let maxProb = 0;
88
+ for (const sp of strikeProbabilities) {
89
+ if (sp.probability > maxProb) {
90
+ maxProb = sp.probability;
91
+ mostLikelyPrice = sp.strike;
92
+ }
93
+ }
94
+ // Compute cumulative distribution for median
95
+ let cumulative = 0;
96
+ let medianPrice = strikeProbabilities[Math.floor(strikeProbabilities.length / 2)].strike;
97
+ for (const sp of strikeProbabilities) {
98
+ cumulative += sp.probability;
99
+ if (cumulative >= 0.5) {
100
+ medianPrice = sp.strike;
101
+ break;
102
+ }
103
+ }
104
+ // Expected value (mean)
105
+ let mean = 0;
106
+ for (const sp of strikeProbabilities) {
107
+ mean += sp.strike * sp.probability;
108
+ }
109
+ // Variance and expected move (standard deviation)
110
+ let variance = 0;
111
+ for (const sp of strikeProbabilities) {
112
+ const diff = sp.strike - mean;
113
+ variance += diff * diff * sp.probability;
114
+ }
115
+ const expectedMove = Math.sqrt(variance);
116
+ // Tail skew: rightTail / leftTail relative to mean
117
+ let leftTail = 0;
118
+ let rightTail = 0;
119
+ for (const sp of strikeProbabilities) {
120
+ if (sp.strike < mean) {
121
+ leftTail += sp.probability;
122
+ }
123
+ else {
124
+ rightTail += sp.probability;
125
+ }
126
+ }
127
+ const tailSkew = rightTail / Math.max(leftTail, 1e-9);
128
+ // Cumulative probabilities above and below spot price
129
+ let cumulativeBelowSpot = 0;
130
+ let cumulativeAboveSpot = 0;
131
+ for (const sp of strikeProbabilities) {
132
+ if (sp.strike < underlyingPrice) {
133
+ cumulativeBelowSpot += sp.probability;
134
+ }
135
+ else if (sp.strike > underlyingPrice) {
136
+ cumulativeAboveSpot += sp.probability;
137
+ }
138
+ }
139
+ return {
140
+ success: true,
141
+ distribution: {
142
+ symbol,
143
+ expiryDate,
144
+ calculationTimestamp: Date.now(),
145
+ underlyingPrice,
146
+ strikeProbabilities,
147
+ mostLikelyPrice,
148
+ medianPrice,
149
+ expectedValue: mean,
150
+ expectedMove,
151
+ tailSkew,
152
+ cumulativeProbabilityAboveSpot: cumulativeAboveSpot,
153
+ cumulativeProbabilityBelowSpot: cumulativeBelowSpot,
154
+ },
155
+ };
156
+ }
157
+ /**
158
+ * Estimate implied probability distributions for all expirations in an option chain
159
+ *
160
+ * @param symbol - Underlying ticker symbol
161
+ * @param underlyingPrice - Current spot/mark price of the underlying
162
+ * @param options - Array of all options (calls and puts, all expirations)
163
+ * @returns Array of ImpliedProbabilityDistribution for each expiration
164
+ *
165
+ * @example
166
+ * ```typescript
167
+ * const distributions = estimateImpliedProbabilityDistributions(
168
+ * 'QQQ',
169
+ * 500.00,
170
+ * chain.options
171
+ * );
172
+ *
173
+ * for (const dist of distributions) {
174
+ * console.log(`Expiry: ${new Date(dist.expiryDate).toISOString()}`);
175
+ * console.log(`Mode: ${dist.mostLikelyPrice}`);
176
+ * }
177
+ * ```
178
+ */
179
+ function estimateImpliedProbabilityDistributions(symbol, underlyingPrice, options) {
180
+ // Get unique expirations
181
+ const expirationsSet = new Set();
182
+ for (const option of options) {
183
+ expirationsSet.add(option.expirationTimestamp);
184
+ }
185
+ const expirations = Array.from(expirationsSet).sort((a, b) => a - b);
186
+ const distributions = [];
187
+ for (const expiry of expirations) {
188
+ // Filter to call options at this expiry with valid bid/ask
189
+ const callOptionsAtExpiry = options.filter((opt) => opt.expirationTimestamp === expiry &&
190
+ opt.optionType === 'call' &&
191
+ opt.bid > 0 &&
192
+ opt.ask > 0);
193
+ const result = estimateImpliedProbabilityDistribution(symbol, underlyingPrice, callOptionsAtExpiry);
194
+ if (result.success) {
195
+ distributions.push(result.distribution);
196
+ }
197
+ // Silently skip expirations that don't have enough data
198
+ }
199
+ return distributions;
200
+ }
201
+ /**
202
+ * Get the probability of the underlying finishing between two price levels
203
+ *
204
+ * @param distribution - Implied probability distribution
205
+ * @param lowerBound - Lower price bound
206
+ * @param upperBound - Upper price bound
207
+ * @returns Probability of finishing between the bounds
208
+ *
209
+ * @example
210
+ * ```typescript
211
+ * // Probability of QQQ finishing between 490 and 510
212
+ * const prob = getProbabilityInRange(distribution, 490, 510);
213
+ * console.log(`${(prob * 100).toFixed(1)}% chance of finishing in range`);
214
+ * ```
215
+ */
216
+ function getProbabilityInRange(distribution, lowerBound, upperBound) {
217
+ let probability = 0;
218
+ for (const sp of distribution.strikeProbabilities) {
219
+ if (sp.strike >= lowerBound && sp.strike <= upperBound) {
220
+ probability += sp.probability;
221
+ }
222
+ }
223
+ return probability;
224
+ }
225
+ /**
226
+ * Get the cumulative probability up to a given price level
227
+ *
228
+ * @param distribution - Implied probability distribution
229
+ * @param price - Price level
230
+ * @returns Cumulative probability of finishing at or below the price
231
+ *
232
+ * @example
233
+ * ```typescript
234
+ * // Probability of QQQ finishing at or below 495
235
+ * const prob = getCumulativeProbability(distribution, 495);
236
+ * console.log(`${(prob * 100).toFixed(1)}% chance of finishing <= 495`);
237
+ * ```
238
+ */
239
+ function getCumulativeProbability(distribution, price) {
240
+ let probability = 0;
241
+ for (const sp of distribution.strikeProbabilities) {
242
+ if (sp.strike <= price) {
243
+ probability += sp.probability;
244
+ }
245
+ }
246
+ return probability;
247
+ }
248
+ /**
249
+ * Get the quantile (inverse CDF) for a given probability
250
+ *
251
+ * @param distribution - Implied probability distribution
252
+ * @param probability - Probability value between 0 and 1
253
+ * @returns Strike price at the given probability quantile
254
+ *
255
+ * @example
256
+ * ```typescript
257
+ * // Find the 5th and 95th percentile strikes
258
+ * const p5 = getQuantile(distribution, 0.05);
259
+ * const p95 = getQuantile(distribution, 0.95);
260
+ * console.log(`90% confidence interval: [${p5}, ${p95}]`);
261
+ * ```
262
+ */
263
+ function getQuantile(distribution, probability) {
264
+ if (probability <= 0) {
265
+ return distribution.strikeProbabilities[0]?.strike ?? 0;
266
+ }
267
+ if (probability >= 1) {
268
+ return distribution.strikeProbabilities[distribution.strikeProbabilities.length - 1]?.strike ?? 0;
269
+ }
270
+ let cumulative = 0;
271
+ for (const sp of distribution.strikeProbabilities) {
272
+ cumulative += sp.probability;
273
+ if (cumulative >= probability) {
274
+ return sp.strike;
275
+ }
276
+ }
277
+ return distribution.strikeProbabilities[distribution.strikeProbabilities.length - 1]?.strike ?? 0;
278
+ }
package/dist/index.d.ts CHANGED
@@ -12,6 +12,9 @@ export { calculateGammaVannaCharmExposures, calculateSharesNeededToCover, } from
12
12
  export { cumulativeNormalDistribution, normalPDF, } from './utils/statistics';
13
13
  export { buildOCCSymbol, parseOCCSymbol, generateStrikesAroundSpot, generateOCCSymbolsForStrikes, generateOCCSymbolsAroundSpot, } from './utils/occ';
14
14
  export type { OCCSymbolParams, ParsedOCCSymbol, StrikeGenerationParams, } from './utils/occ';
15
+ export { estimateImpliedProbabilityDistribution, estimateImpliedProbabilityDistributions, getProbabilityInRange, getCumulativeProbability, getQuantile, } from './impliedpdf';
16
+ export type { StrikeProbability, ImpliedProbabilityDistribution, ImpliedPDFResult, } from './impliedpdf';
15
17
  export { FloeClient, Broker } from './client/FloeClient';
16
18
  export { TradierClient } from './client/brokers/TradierClient';
19
+ export type { AggressorSide, IntradayTrade } from './client/brokers/TradierClient';
17
20
  export { genericAdapter, schwabAdapter, ibkrAdapter, tdaAdapter, brokerAdapters, getAdapter, createOptionChain, } from './adapters';
package/dist/index.js CHANGED
@@ -20,7 +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.createOptionChain = exports.getAdapter = exports.brokerAdapters = exports.tdaAdapter = exports.ibkrAdapter = exports.schwabAdapter = exports.genericAdapter = exports.TradierClient = exports.Broker = exports.FloeClient = 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;
23
+ exports.createOptionChain = exports.getAdapter = exports.brokerAdapters = exports.tdaAdapter = exports.ibkrAdapter = exports.schwabAdapter = exports.genericAdapter = exports.TradierClient = exports.Broker = exports.FloeClient = 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
24
  // Core types
25
25
  __exportStar(require("./types"), exports);
26
26
  // Black-Scholes pricing and Greeks
@@ -52,6 +52,13 @@ Object.defineProperty(exports, "parseOCCSymbol", { enumerable: true, get: functi
52
52
  Object.defineProperty(exports, "generateStrikesAroundSpot", { enumerable: true, get: function () { return occ_1.generateStrikesAroundSpot; } });
53
53
  Object.defineProperty(exports, "generateOCCSymbolsForStrikes", { enumerable: true, get: function () { return occ_1.generateOCCSymbolsForStrikes; } });
54
54
  Object.defineProperty(exports, "generateOCCSymbolsAroundSpot", { enumerable: true, get: function () { return occ_1.generateOCCSymbolsAroundSpot; } });
55
+ // Implied PDF (probability density function)
56
+ var impliedpdf_1 = require("./impliedpdf");
57
+ Object.defineProperty(exports, "estimateImpliedProbabilityDistribution", { enumerable: true, get: function () { return impliedpdf_1.estimateImpliedProbabilityDistribution; } });
58
+ Object.defineProperty(exports, "estimateImpliedProbabilityDistributions", { enumerable: true, get: function () { return impliedpdf_1.estimateImpliedProbabilityDistributions; } });
59
+ Object.defineProperty(exports, "getProbabilityInRange", { enumerable: true, get: function () { return impliedpdf_1.getProbabilityInRange; } });
60
+ Object.defineProperty(exports, "getCumulativeProbability", { enumerable: true, get: function () { return impliedpdf_1.getCumulativeProbability; } });
61
+ Object.defineProperty(exports, "getQuantile", { enumerable: true, get: function () { return impliedpdf_1.getQuantile; } });
55
62
  // Client
56
63
  var FloeClient_1 = require("./client/FloeClient");
57
64
  Object.defineProperty(exports, "FloeClient", { enumerable: true, get: function () { return FloeClient_1.FloeClient; } });
@@ -48,7 +48,7 @@ export interface StrikeGenerationParams {
48
48
  /** Number of strikes below spot to include */
49
49
  strikesBelow?: number;
50
50
  /** Strike increment (e.g., 1 for $1 increments, 5 for $5) */
51
- strikeIncrement?: number;
51
+ strikeIncrementInDollars?: number;
52
52
  }
53
53
  /**
54
54
  * Builds an OCC-formatted option symbol.
package/dist/utils/occ.js CHANGED
@@ -48,7 +48,8 @@ function buildOCCSymbol(params) {
48
48
  ? symbol.toUpperCase().padEnd(6, ' ')
49
49
  : symbol.toUpperCase();
50
50
  // Format expiration date as YYMMDD
51
- const expirationDate = typeof expiration === 'string' ? new Date(expiration) : expiration;
51
+ // Use UTC methods to avoid timezone shifts when parsing ISO date strings
52
+ const expirationDate = typeof expiration === 'string' ? new Date(expiration + 'T12:00:00') : expiration;
52
53
  const year = expirationDate.getFullYear().toString().slice(-2);
53
54
  const month = (expirationDate.getMonth() + 1).toString().padStart(2, '0');
54
55
  const day = expirationDate.getDate().toString().padStart(2, '0');
@@ -100,11 +101,11 @@ function parseOCCSymbol(occSymbol) {
100
101
  if (symbol.length === 0) {
101
102
  throw new Error(`Invalid OCC symbol: no ticker found in ${occSymbol}`);
102
103
  }
103
- // Parse date
104
+ // Parse date - use noon to avoid timezone edge cases
104
105
  const year = 2000 + parseInt(dateString.slice(0, 2), 10);
105
106
  const month = parseInt(dateString.slice(2, 4), 10) - 1; // 0-indexed
106
107
  const day = parseInt(dateString.slice(4, 6), 10);
107
- const expiration = new Date(year, month, day);
108
+ const expiration = new Date(year, month, day, 12, 0, 0);
108
109
  // Validate date
109
110
  if (isNaN(expiration.getTime())) {
110
111
  throw new Error(`Invalid date in OCC symbol: ${dateString}`);
@@ -133,7 +134,7 @@ function parseOCCSymbol(occSymbol) {
133
134
  * ```
134
135
  */
135
136
  function generateStrikesAroundSpot(params) {
136
- const { spot, strikesAbove = 10, strikesBelow = 10, strikeIncrement = 1 } = params;
137
+ const { spot, strikesAbove = 10, strikesBelow = 10, strikeIncrementInDollars: strikeIncrement = 1 } = params;
137
138
  // Find the nearest strike at or below spot
138
139
  const baseStrike = Math.floor(spot / strikeIncrement) * strikeIncrement;
139
140
  const strikes = [];
@@ -20,7 +20,7 @@ import { OptionChain, IVSurface, VolatilityModel, SmoothingModel } from '../type
20
20
  */
21
21
  export declare function getIVSurfaces(volatilityModel: VolatilityModel, smoothingModel: SmoothingModel, chain: OptionChain): IVSurface[];
22
22
  /**
23
- * Get smoothed IV for a specific expiration, option type, and strike
23
+ * Helper function to get the smoothed IV for a specific expiration, option type, and strike combination
24
24
  *
25
25
  * @param ivSurfaces - Array of IV surfaces
26
26
  * @param expiration - Expiration timestamp in milliseconds
@@ -134,7 +134,7 @@ function getIVSurfaces(volatilityModel, smoothingModel, chain) {
134
134
  return ivSurfaces;
135
135
  }
136
136
  /**
137
- * Get smoothed IV for a specific expiration, option type, and strike
137
+ * Helper function to get the smoothed IV for a specific expiration, option type, and strike combination
138
138
  *
139
139
  * @param ivSurfaces - Array of IV surfaces
140
140
  * @param expiration - Expiration timestamp in milliseconds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fullstackcraftllc/floe",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Production-ready options analytics toolkit. Normalize broker data structures and calculate Black-Scholes, Greeks, and exposures with a clean, type-safe API. Built for trading platforms and fintech applications.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -41,6 +41,7 @@
41
41
  "devDependencies": {
42
42
  "@types/jest": "^29.5.0",
43
43
  "@types/node": "^20.0.0",
44
+ "dotenv": "^17.2.3",
44
45
  "jest": "^29.5.0",
45
46
  "ts-jest": "^29.1.0",
46
47
  "typescript": "^5.9.3"