tradeblocks-mcp 3.0.2 → 3.3.1

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.
Files changed (218) hide show
  1. package/README.md +85 -80
  2. package/dist/{chunk-LDKTV7GW.js → chunk-27S67XW3.js} +24 -52
  3. package/dist/chunk-27S67XW3.js.map +1 -0
  4. package/dist/{chunk-FGZH632F.js → chunk-2E63THNI.js} +13 -11
  5. package/dist/chunk-2E63THNI.js.map +1 -0
  6. package/dist/{chunk-QTTR7AAW.js → chunk-2I3S2ZLT.js} +325 -84
  7. package/dist/chunk-2I3S2ZLT.js.map +1 -0
  8. package/dist/{chunk-4BLCXNQ6.js → chunk-4HZLVUOZ.js} +179 -183
  9. package/dist/chunk-4HZLVUOZ.js.map +1 -0
  10. package/dist/{chunk-XXYOUIZY.js → chunk-NWNSKALN.js} +63 -80
  11. package/dist/chunk-NWNSKALN.js.map +1 -0
  12. package/dist/{chunk-PRAYH3RT.js → chunk-WNTYA7ER.js} +29 -12
  13. package/dist/chunk-WNTYA7ER.js.map +1 -0
  14. package/dist/{chunk-BKQ4PM4Y.js → chunk-YZA4RS76.js} +24 -18
  15. package/dist/chunk-YZA4RS76.js.map +1 -0
  16. package/dist/{chunk-W2PP3LEH.js → chunk-ZTJXC3LS.js} +17 -8
  17. package/dist/chunk-ZTJXC3LS.js.map +1 -0
  18. package/dist/daily-log-processor-MZW2XBZY.js +9 -0
  19. package/dist/{daily-logs-store-D3QC2XPV.js → daily-logs-store-3C3OTGPK.js} +2 -2
  20. package/dist/iv-solver-worker.js +9 -1
  21. package/dist/market-provider-6U33TQUT.js +16 -0
  22. package/dist/{proto-3HZTCWK4.js → proto-V23GIZG2.js} +2 -2
  23. package/dist/{sync-PO4IPCYV.js → sync-3UBC37VW.js} +7 -7
  24. package/dist/test-exports.js +876 -483
  25. package/dist/test-exports.js.map +1 -1
  26. package/dist/trade-processor-DKDCWGKD.js +9 -0
  27. package/dist/{trades-store-LOL3FQSF.js → trades-store-R3NL25T2.js} +2 -2
  28. package/package.json +2 -6
  29. package/server/{chunk-CSDVJPBB.js → chunk-536NSFED.js} +29 -12
  30. package/server/chunk-536NSFED.js.map +1 -0
  31. package/server/{chunk-WA5AAPCH.js → chunk-A3UKD64A.js} +17 -8
  32. package/server/chunk-A3UKD64A.js.map +1 -0
  33. package/server/{chunk-PNKG7RY7.js → chunk-OMUDNJBJ.js} +63 -80
  34. package/server/chunk-OMUDNJBJ.js.map +1 -0
  35. package/server/{chunk-72GKJE2U.js → chunk-RJDJHZ5Y.js} +934 -335
  36. package/server/chunk-RJDJHZ5Y.js.map +1 -0
  37. package/server/{chunk-NZO6PT64.js → chunk-RM4B2VKS.js} +167 -177
  38. package/server/chunk-RM4B2VKS.js.map +1 -0
  39. package/server/{chunk-5EBXHT6C.js → chunk-SY6GDNVI.js} +24 -18
  40. package/server/chunk-SY6GDNVI.js.map +1 -0
  41. package/server/{chunk-FBNDMCT5.js → chunk-T66KH2XH.js} +13 -11
  42. package/server/chunk-T66KH2XH.js.map +1 -0
  43. package/server/{chunk-NRFXAJF7.js → chunk-W5E7FHC4.js} +24 -52
  44. package/server/chunk-W5E7FHC4.js.map +1 -0
  45. package/server/{config-6IZXEFEX.js → config-DK7KOMNL.js} +1 -1
  46. package/server/config-DK7KOMNL.js.map +1 -0
  47. package/server/daily-log-processor-B2VPU2FQ.js +10 -0
  48. package/server/{daily-logs-store-NU7M4L5C.js → daily-logs-store-YFNXTB7J.js} +2 -2
  49. package/server/http-server.js +2 -5
  50. package/server/index.js +2497 -2029
  51. package/server/index.js.map +1 -1
  52. package/server/iv-solver-worker.js +9 -1
  53. package/server/market-provider-L56RFHAC.js +17 -0
  54. package/server/{proto-DWRZJO4E.js → proto-YZ4TLX2N.js} +2 -2
  55. package/server/{sync-QFI5L7S7.js → sync-CENKDN53.js} +7 -7
  56. package/server/trade-processor-3PI4LHZI.js +10 -0
  57. package/server/{trades-store-VY26MWER.js → trades-store-RTTEYMXH.js} +2 -2
  58. package/src/auth/clients-store.ts +4 -4
  59. package/src/auth/code-store.ts +1 -1
  60. package/src/auth/config.ts +11 -12
  61. package/src/auth/login-page.ts +8 -10
  62. package/src/auth/provider.ts +35 -30
  63. package/src/auth/token.ts +17 -15
  64. package/src/db/backtest-schemas.ts +38 -8
  65. package/src/db/connection.ts +107 -40
  66. package/src/db/data-root.ts +22 -0
  67. package/src/db/index.ts +37 -5
  68. package/src/db/json-adapters.ts +22 -31
  69. package/src/db/json-migration.ts +11 -15
  70. package/src/db/json-store.ts +5 -3
  71. package/src/db/market-datasets.ts +48 -15
  72. package/src/db/market-schemas.ts +11 -5
  73. package/src/db/market-views.ts +119 -32
  74. package/src/db/parquet-writer.ts +7 -7
  75. package/src/db/profile-schemas.ts +39 -22
  76. package/src/db/schemas.ts +2 -2
  77. package/src/http-server.ts +7 -12
  78. package/src/index.ts +8 -9
  79. package/src/market/ingestor/index.ts +1 -4
  80. package/src/market/ingestor/market-ingestor.ts +140 -67
  81. package/src/market/ingestor/types.ts +1 -3
  82. package/src/market/stores/chain-sql.ts +2 -8
  83. package/src/market/stores/chain-store.ts +1 -4
  84. package/src/market/stores/duckdb-chain-store.ts +3 -14
  85. package/src/market/stores/duckdb-enriched-store.ts +3 -13
  86. package/src/market/stores/duckdb-quote-store.ts +10 -27
  87. package/src/market/stores/duckdb-spot-store.ts +4 -20
  88. package/src/market/stores/enriched-sql.ts +3 -9
  89. package/src/market/stores/enriched-store.ts +2 -2
  90. package/src/market/stores/parquet-chain-store.ts +5 -22
  91. package/src/market/stores/parquet-enriched-store.ts +4 -16
  92. package/src/market/stores/parquet-oi-daily-store.ts +3 -14
  93. package/src/market/stores/parquet-quote-store.ts +9 -29
  94. package/src/market/stores/parquet-spot-store.ts +14 -33
  95. package/src/market/stores/quote-store.ts +2 -10
  96. package/src/market/stores/spot-sql.ts +3 -15
  97. package/src/market/stores/spot-store.ts +1 -1
  98. package/src/market/tickers/defaults.json +6 -6
  99. package/src/market/tickers/loader.ts +2 -7
  100. package/src/market/tickers/registry.ts +5 -15
  101. package/src/market/tickers/schemas.ts +2 -6
  102. package/src/models/strategy-profile.ts +39 -39
  103. package/src/sync/block-sync.ts +44 -65
  104. package/src/sync/index.ts +1 -4
  105. package/src/sync/metadata.ts +14 -20
  106. package/src/test-exports.ts +139 -123
  107. package/src/tools/analysis.ts +180 -159
  108. package/src/tools/batch-exit-analysis.ts +100 -59
  109. package/src/tools/blocks/analysis.ts +116 -101
  110. package/src/tools/blocks/comparison.ts +281 -364
  111. package/src/tools/blocks/core.ts +208 -253
  112. package/src/tools/blocks/health.ts +571 -563
  113. package/src/tools/blocks/index.ts +2 -0
  114. package/src/tools/blocks/paired-comparison.ts +425 -0
  115. package/src/tools/blocks/similarity.ts +316 -283
  116. package/src/tools/edge-decay.ts +124 -197
  117. package/src/tools/exit-analysis.ts +130 -77
  118. package/src/tools/greeks-attribution.ts +84 -35
  119. package/src/tools/guides.ts +4 -6
  120. package/src/tools/imports.ts +11 -14
  121. package/src/tools/market-data.ts +1067 -757
  122. package/src/tools/market-enrichment.ts +3 -3
  123. package/src/tools/market-fetch.ts +148 -67
  124. package/src/tools/market-imports.ts +12 -12
  125. package/src/tools/middleware/sync-middleware.ts +5 -6
  126. package/src/tools/performance.ts +185 -302
  127. package/src/tools/profile-analysis.ts +52 -66
  128. package/src/tools/profiles.ts +106 -69
  129. package/src/tools/regime-advisor.ts +20 -45
  130. package/src/tools/replay.ts +81 -77
  131. package/src/tools/reports/discrepancies.ts +298 -328
  132. package/src/tools/reports/fields.ts +7 -25
  133. package/src/tools/reports/helpers.ts +18 -49
  134. package/src/tools/reports/predictive.ts +27 -70
  135. package/src/tools/reports/slippage-trends.ts +315 -345
  136. package/src/tools/reports/slippage.ts +1 -4
  137. package/src/tools/reports/strategy-matches.ts +399 -441
  138. package/src/tools/schema.ts +43 -40
  139. package/src/tools/shared/filters.ts +3 -9
  140. package/src/tools/snapshot.ts +9 -30
  141. package/src/tools/sql.ts +15 -14
  142. package/src/tools/tickers.ts +1 -4
  143. package/src/utils/batch-exit-analysis.ts +31 -42
  144. package/src/utils/black-scholes.ts +39 -29
  145. package/src/utils/block-loader.ts +69 -83
  146. package/src/utils/calibration-probe.ts +3 -4
  147. package/src/utils/chain-loader.ts +3 -3
  148. package/src/utils/csv-discovery.ts +16 -22
  149. package/src/utils/data-quality.ts +24 -36
  150. package/src/utils/exit-triggers.ts +91 -96
  151. package/src/utils/field-timing.ts +94 -79
  152. package/src/utils/filter-predicates.ts +13 -9
  153. package/src/utils/flatfile-importer.ts +94 -64
  154. package/src/utils/greeks-decomposition.ts +152 -100
  155. package/src/utils/iv-solver-pool.ts +55 -25
  156. package/src/utils/iv-solver-worker.ts +5 -5
  157. package/src/utils/market-enricher.ts +528 -497
  158. package/src/utils/market-importer.ts +31 -12
  159. package/src/utils/market-provider.ts +21 -23
  160. package/src/utils/massive-tier.ts +5 -7
  161. package/src/utils/migrate-option-data-helpers.ts +2 -8
  162. package/src/utils/option-quote-greeks.ts +25 -31
  163. package/src/utils/option-time.ts +4 -8
  164. package/src/utils/output-formatter.ts +1 -4
  165. package/src/utils/provider-capabilities.ts +1 -4
  166. package/src/utils/providers/massive.ts +59 -93
  167. package/src/utils/providers/thetadata/backfill.ts +14 -23
  168. package/src/utils/providers/thetadata/client.ts +12 -8
  169. package/src/utils/providers/thetadata/decode.ts +2 -20
  170. package/src/utils/providers/thetadata/endpoints.ts +32 -40
  171. package/src/utils/providers/thetadata/join.ts +11 -10
  172. package/src/utils/providers/thetadata/proto.ts +12 -10
  173. package/src/utils/providers/thetadata/quote-mid-greeks.ts +5 -5
  174. package/src/utils/providers/thetadata.ts +11 -10
  175. package/src/utils/quote-enricher.ts +4 -4
  176. package/src/utils/quote-parquet-projection.ts +3 -11
  177. package/src/utils/sample-date-selector.ts +3 -5
  178. package/src/utils/schema-metadata.ts +102 -70
  179. package/src/utils/ticker.ts +5 -9
  180. package/src/utils/trade-replay.ts +77 -68
  181. package/dist/chunk-4BLCXNQ6.js.map +0 -1
  182. package/dist/chunk-BKQ4PM4Y.js.map +0 -1
  183. package/dist/chunk-FGZH632F.js.map +0 -1
  184. package/dist/chunk-LDKTV7GW.js.map +0 -1
  185. package/dist/chunk-PRAYH3RT.js.map +0 -1
  186. package/dist/chunk-QTTR7AAW.js.map +0 -1
  187. package/dist/chunk-W2PP3LEH.js.map +0 -1
  188. package/dist/chunk-XXYOUIZY.js.map +0 -1
  189. package/dist/daily-log-processor-6MWJ23JK.js +0 -9
  190. package/dist/market-provider-VDRJUEF2.js +0 -16
  191. package/dist/trade-processor-NHU4VWRX.js +0 -9
  192. package/manifest.json +0 -141
  193. package/server/chunk-5EBXHT6C.js.map +0 -1
  194. package/server/chunk-72GKJE2U.js.map +0 -1
  195. package/server/chunk-CSDVJPBB.js.map +0 -1
  196. package/server/chunk-FBNDMCT5.js.map +0 -1
  197. package/server/chunk-NRFXAJF7.js.map +0 -1
  198. package/server/chunk-NZO6PT64.js.map +0 -1
  199. package/server/chunk-PNKG7RY7.js.map +0 -1
  200. package/server/chunk-WA5AAPCH.js.map +0 -1
  201. package/server/config-6IZXEFEX.js.map +0 -1
  202. package/server/daily-log-processor-Y3PVSVBM.js +0 -10
  203. package/server/market-provider-VOYYVYWT.js +0 -17
  204. package/server/trade-processor-JWVS37KM.js +0 -10
  205. /package/dist/{daily-log-processor-6MWJ23JK.js.map → daily-log-processor-MZW2XBZY.js.map} +0 -0
  206. /package/dist/{daily-logs-store-D3QC2XPV.js.map → daily-logs-store-3C3OTGPK.js.map} +0 -0
  207. /package/dist/{market-provider-VDRJUEF2.js.map → market-provider-6U33TQUT.js.map} +0 -0
  208. /package/dist/{proto-3HZTCWK4.js.map → proto-V23GIZG2.js.map} +0 -0
  209. /package/dist/{sync-PO4IPCYV.js.map → sync-3UBC37VW.js.map} +0 -0
  210. /package/dist/{trade-processor-NHU4VWRX.js.map → trade-processor-DKDCWGKD.js.map} +0 -0
  211. /package/dist/{trades-store-LOL3FQSF.js.map → trades-store-R3NL25T2.js.map} +0 -0
  212. /package/server/{daily-log-processor-Y3PVSVBM.js.map → daily-log-processor-B2VPU2FQ.js.map} +0 -0
  213. /package/server/{daily-logs-store-NU7M4L5C.js.map → daily-logs-store-YFNXTB7J.js.map} +0 -0
  214. /package/server/{market-provider-VOYYVYWT.js.map → market-provider-L56RFHAC.js.map} +0 -0
  215. /package/server/{proto-DWRZJO4E.js.map → proto-YZ4TLX2N.js.map} +0 -0
  216. /package/server/{sync-QFI5L7S7.js.map → sync-CENKDN53.js.map} +0 -0
  217. /package/server/{trade-processor-JWVS37KM.js.map → trade-processor-3PI4LHZI.js.map} +0 -0
  218. /package/server/{trades-store-VY26MWER.js.map → trades-store-RTTEYMXH.js.map} +0 -0
@@ -15,40 +15,48 @@
15
15
  * Pure logic module — no I/O, no DuckDB, no fetch.
16
16
  */
17
17
 
18
- import type { PnlPoint, ReplayLeg } from './trade-replay.ts';
19
- import { bsPrice, bachelierPrice, BACHELIER_DTE_THRESHOLD } from './black-scholes.ts';
18
+ import type { PnlPoint, ReplayLeg } from "./trade-replay.ts";
19
+ import { bsPrice, bachelierPrice, BACHELIER_DTE_THRESHOLD } from "./black-scholes.ts";
20
20
 
21
21
  // ---------------------------------------------------------------------------
22
22
  // Types
23
23
  // ---------------------------------------------------------------------------
24
24
 
25
- export type FactorName = 'delta' | 'gamma' | 'theta' | 'vega' | 'charm' | 'vanna' | 'residual' | 'time_and_vol';
25
+ export type FactorName =
26
+ | "delta"
27
+ | "gamma"
28
+ | "theta"
29
+ | "vega"
30
+ | "charm"
31
+ | "vanna"
32
+ | "residual"
33
+ | "time_and_vol";
26
34
 
27
35
  export interface FactorContribution {
28
36
  factor: FactorName;
29
- totalPnl: number; // Sum of step contributions
30
- pctOfTotal: number; // % of total abs P&L move
31
- steps: number[]; // Per-step contribution values
37
+ totalPnl: number; // Sum of step contributions
38
+ pctOfTotal: number; // % of total abs P&L move
39
+ steps: number[]; // Per-step contribution values
32
40
  }
33
41
 
34
42
  export interface LegGroupVega {
35
- label: string; // e.g., "front_month", "back_month"
36
- legIndices: number[]; // Which legs are in this group
37
- totalVegaPnl: number; // Sum of vega P&L for this group
38
- avgIvChange: number; // Average IV change for this group's legs
39
- steps: number[]; // Per-step vega contribution for this group
43
+ label: string; // e.g., "front_month", "back_month"
44
+ legIndices: number[]; // Which legs are in this group
45
+ totalVegaPnl: number; // Sum of vega P&L for this group
46
+ avgIvChange: number; // Average IV change for this group's legs
47
+ steps: number[]; // Per-step vega contribution for this group
40
48
  }
41
49
 
42
50
  export interface GreeksDecompositionResult {
43
- factors: FactorContribution[]; // Sorted by abs(totalPnl) descending
44
- legGroupVega?: LegGroupVega[]; // Per-leg-group vega attribution
45
- totalPnlChange: number; // Actual P&L change from first to last point
46
- totalAttributed: number; // Sum of factor contributions (excl residual)
47
- totalResidual: number; // Total residual
48
- stepCount: number; // Number of steps (pnlPath.length - 1)
49
- summary: string; // Human-readable summary
50
- warning?: string | null; // D-13: high residual warning
51
- method: 'full_reval' | 'model' | 'numerical'; // which method produced the attribution
51
+ factors: FactorContribution[]; // Sorted by abs(totalPnl) descending
52
+ legGroupVega?: LegGroupVega[]; // Per-leg-group vega attribution
53
+ totalPnlChange: number; // Actual P&L change from first to last point
54
+ totalAttributed: number; // Sum of factor contributions (excl residual)
55
+ totalResidual: number; // Total residual
56
+ stepCount: number; // Number of steps (pnlPath.length - 1)
57
+ summary: string; // Human-readable summary
58
+ warning?: string | null; // D-13: high residual warning
59
+ method: "full_reval" | "model" | "numerical"; // which method produced the attribution
52
60
  }
53
61
 
54
62
  export interface LegGroupDef {
@@ -58,20 +66,20 @@ export interface LegGroupDef {
58
66
 
59
67
  export interface LegPricingInput {
60
68
  strike: number;
61
- type: 'C' | 'P';
62
- expiryDate: string; // YYYY-MM-DD
69
+ type: "C" | "P";
70
+ expiryDate: string; // YYYY-MM-DD
63
71
  }
64
72
 
65
73
  export interface GreeksDecompositionConfig {
66
74
  pnlPath: PnlPoint[];
67
75
  legs: ReplayLeg[];
68
- underlyingPrices?: Map<string, number>; // timestamp -> underlying price
69
- legGroups?: LegGroupDef[]; // Optional leg grouping for per-group vega
76
+ underlyingPrices?: Map<string, number>; // timestamp -> underlying price
77
+ legGroups?: LegGroupDef[]; // Optional leg grouping for per-group vega
70
78
  /** Per-leg pricing inputs for full revaluation. When provided, uses full reval
71
79
  * instead of Taylor expansion. Falls back to Taylor when missing. */
72
80
  legPricingInputs?: LegPricingInput[];
73
- riskFreeRate?: number; // e.g. 0.045
74
- dividendYield?: number; // e.g. 0.015 for SPX
81
+ riskFreeRate?: number; // e.g. 0.045
82
+ dividendYield?: number; // e.g. 0.015 for SPX
75
83
  }
76
84
 
77
85
  // ---------------------------------------------------------------------------
@@ -88,13 +96,13 @@ const TRADING_MINUTES_PER_DAY = 390;
88
96
  * Cross day: calendar day difference (simplified — treats each gap as 1 day)
89
97
  */
90
98
  export function computeTimeDeltaDays(ts1: string, ts2: string): number {
91
- const [date1, time1] = ts1.split(' ');
92
- const [date2, time2] = ts2.split(' ');
99
+ const [date1, time1] = ts1.split(" ");
100
+ const [date2, time2] = ts2.split(" ");
93
101
 
94
102
  if (date1 === date2) {
95
103
  // Same day: count minutes difference
96
- const [h1, m1] = time1.split(':').map(Number);
97
- const [h2, m2] = time2.split(':').map(Number);
104
+ const [h1, m1] = time1.split(":").map(Number);
105
+ const [h2, m2] = time2.split(":").map(Number);
98
106
  const mins1 = h1 * 60 + m1;
99
107
  const mins2 = h2 * 60 + m2;
100
108
  const diffMins = Math.abs(mins2 - mins1);
@@ -102,19 +110,19 @@ export function computeTimeDeltaDays(ts1: string, ts2: string): number {
102
110
  }
103
111
 
104
112
  // Cross-day: compute calendar day difference
105
- const d1 = new Date(date1 + 'T12:00:00'); // noon to avoid DST issues
106
- const d2 = new Date(date2 + 'T12:00:00');
113
+ const d1 = new Date(date1 + "T12:00:00"); // noon to avoid DST issues
114
+ const d2 = new Date(date2 + "T12:00:00");
107
115
  const diffMs = Math.abs(d2.getTime() - d1.getTime());
108
116
  const diffDays = Math.round(diffMs / (24 * 60 * 60 * 1000));
109
117
 
110
118
  // Add fractional day from time within each day
111
- const [h1, m1] = time1.split(':').map(Number);
112
- const [h2, m2] = time2.split(':').map(Number);
119
+ const [h1, m1] = time1.split(":").map(Number);
120
+ const [h2, m2] = time2.split(":").map(Number);
113
121
  // Fraction of trading day for ts2's time (from market open ~9:30)
114
- const minsIntoDay2 = (h2 * 60 + m2) - (9 * 60 + 30);
122
+ const minsIntoDay2 = h2 * 60 + m2 - (9 * 60 + 30);
115
123
  const fracDay2 = Math.max(0, minsIntoDay2) / TRADING_MINUTES_PER_DAY;
116
124
  // Fraction remaining in ts1's day
117
- const minsIntoDay1 = (h1 * 60 + m1) - (9 * 60 + 30);
125
+ const minsIntoDay1 = h1 * 60 + m1 - (9 * 60 + 30);
118
126
  const fracDayRemaining1 = Math.max(0, 1 - minsIntoDay1 / TRADING_MINUTES_PER_DAY);
119
127
 
120
128
  // Total: remaining fraction of day1 + (diffDays - 1) full days + fraction of day2
@@ -198,31 +206,34 @@ function numericalDecomposition(
198
206
  const totalTimeAndVol = sumSteps(numResidualSteps);
199
207
 
200
208
  const rawFactors = [
201
- { factor: 'delta' as FactorName, totalPnl: totalDelta, steps: numDeltaSteps },
202
- { factor: 'gamma' as FactorName, totalPnl: totalGamma, steps: numGammaSteps },
203
- { factor: 'time_and_vol' as FactorName, totalPnl: totalTimeAndVol, steps: numResidualSteps },
209
+ { factor: "delta" as FactorName, totalPnl: totalDelta, steps: numDeltaSteps },
210
+ { factor: "gamma" as FactorName, totalPnl: totalGamma, steps: numGammaSteps },
211
+ { factor: "time_and_vol" as FactorName, totalPnl: totalTimeAndVol, steps: numResidualSteps },
204
212
  ];
205
213
 
206
214
  rawFactors.sort((a, b) => Math.abs(b.totalPnl) - Math.abs(a.totalPnl));
207
215
  const totalAbsSum = rawFactors.reduce((s, f) => s + Math.abs(f.totalPnl), 0);
208
- const factors: FactorContribution[] = rawFactors.map(f => ({
216
+ const factors: FactorContribution[] = rawFactors.map((f) => ({
209
217
  ...f,
210
218
  pctOfTotal: totalAbsSum > 0 ? (Math.abs(f.totalPnl) / totalAbsSum) * 100 : 0,
211
219
  }));
212
220
 
213
- const summaryParts = factors.map(f => `${f.factor} ${f.totalPnl.toFixed(2)} (${f.pctOfTotal.toFixed(0)}%)`);
214
- const summary = `P&L of ${totalPnlChange.toFixed(2)} (numerical): ${summaryParts.join(', ')}`;
221
+ const summaryParts = factors.map(
222
+ (f) => `${f.factor} ${f.totalPnl.toFixed(2)} (${f.pctOfTotal.toFixed(0)}%)`,
223
+ );
224
+ const summary = `P&L of ${totalPnlChange.toFixed(2)} (numerical): ${summaryParts.join(", ")}`;
215
225
 
216
226
  return {
217
227
  factors,
218
- legGroupVega: undefined, // Leg-group vega not available in numerical mode
228
+ legGroupVega: undefined, // Leg-group vega not available in numerical mode
219
229
  totalPnlChange,
220
230
  totalAttributed: totalDelta + totalGamma,
221
231
  totalResidual: totalTimeAndVol,
222
232
  stepCount,
223
233
  summary,
224
- warning: 'Model-based attribution had >80% residual. Switched to numerical method (realized delta from price changes).',
225
- method: 'numerical',
234
+ warning:
235
+ "Model-based attribution had >80% residual. Switched to numerical method (realized delta from price changes).",
236
+ method: "numerical",
226
237
  };
227
238
  }
228
239
 
@@ -234,11 +245,11 @@ function numericalDecomposition(
234
245
  * Compute DTE in days from a bar timestamp to a leg's expiry (4:00 PM ET).
235
246
  */
236
247
  function computeDte(timestamp: string, expiryDate: string): number {
237
- const dateStr = timestamp.split(' ')[0];
238
- const timePart = timestamp.split(' ')[1] ?? '09:30';
239
- const [eyy, emm, edd] = expiryDate.split('-').map(Number);
240
- const [byy, bmm, bdd] = dateStr.split('-').map(Number);
241
- const [hh, min] = timePart.split(':').map(Number);
248
+ const dateStr = timestamp.split(" ")[0];
249
+ const timePart = timestamp.split(" ")[1] ?? "09:30";
250
+ const [eyy, emm, edd] = expiryDate.split("-").map(Number);
251
+ const [byy, bmm, bdd] = dateStr.split("-").map(Number);
252
+ const [hh, min] = timePart.split(":").map(Number);
242
253
 
243
254
  const expiryMs = new Date(eyy, emm - 1, edd).getTime() + 16 * 60 * 60 * 1000; // 4:00 PM ET
244
255
  const barMs = new Date(byy, bmm - 1, bdd).getTime() + (hh * 60 + min) * 60 * 1000;
@@ -250,7 +261,7 @@ function computeDte(timestamp: string, expiryDate: string): number {
250
261
  * Returns null if pricing fails (DTE <= 0 or IV missing).
251
262
  */
252
263
  function priceOption(
253
- type: 'C' | 'P',
264
+ type: "C" | "P",
254
265
  S: number,
255
266
  K: number,
256
267
  dte: number,
@@ -260,7 +271,7 @@ function priceOption(
260
271
  ): number | null {
261
272
  if (dte <= 0 || iv <= 0) return null;
262
273
  const T = dte / 365;
263
- const bsType = type === 'C' ? 'call' as const : 'put' as const;
274
+ const bsType = type === "C" ? ("call" as const) : ("put" as const);
264
275
  if (dte < BACHELIER_DTE_THRESHOLD) {
265
276
  return bachelierPrice(bsType, S, K, T, r, q, iv);
266
277
  }
@@ -279,39 +290,53 @@ function priceOption(
279
290
  * (pricing model failure for that strategy/DTE combination).
280
291
  */
281
292
  export function decomposeGreeks(config: GreeksDecompositionConfig): GreeksDecompositionResult {
282
- const { pnlPath, legs, underlyingPrices, legGroups, legPricingInputs, riskFreeRate, dividendYield } = config;
293
+ const {
294
+ pnlPath,
295
+ legs,
296
+ underlyingPrices,
297
+ legGroups,
298
+ legPricingInputs,
299
+ riskFreeRate,
300
+ dividendYield,
301
+ } = config;
283
302
 
284
303
  // Edge case: empty or single-point path
285
304
  if (pnlPath.length <= 1) {
286
305
  const emptyFactors: FactorContribution[] = [
287
- { factor: 'delta', totalPnl: 0, pctOfTotal: 0, steps: [] },
288
- { factor: 'gamma', totalPnl: 0, pctOfTotal: 0, steps: [] },
289
- { factor: 'theta', totalPnl: 0, pctOfTotal: 0, steps: [] },
290
- { factor: 'vega', totalPnl: 0, pctOfTotal: 0, steps: [] },
291
- { factor: 'residual', totalPnl: 0, pctOfTotal: 0, steps: [] },
306
+ { factor: "delta", totalPnl: 0, pctOfTotal: 0, steps: [] },
307
+ { factor: "gamma", totalPnl: 0, pctOfTotal: 0, steps: [] },
308
+ { factor: "theta", totalPnl: 0, pctOfTotal: 0, steps: [] },
309
+ { factor: "vega", totalPnl: 0, pctOfTotal: 0, steps: [] },
310
+ { factor: "residual", totalPnl: 0, pctOfTotal: 0, steps: [] },
292
311
  ];
293
312
  return {
294
313
  factors: emptyFactors,
295
- legGroupVega: legGroups ? legGroups.map(g => ({
296
- label: g.label,
297
- legIndices: g.legIndices,
298
- totalVegaPnl: 0,
299
- avgIvChange: 0,
300
- steps: [],
301
- })) : undefined,
314
+ legGroupVega: legGroups
315
+ ? legGroups.map((g) => ({
316
+ label: g.label,
317
+ legIndices: g.legIndices,
318
+ totalVegaPnl: 0,
319
+ avgIvChange: 0,
320
+ steps: [],
321
+ }))
322
+ : undefined,
302
323
  totalPnlChange: 0,
303
324
  totalAttributed: 0,
304
325
  totalResidual: 0,
305
326
  stepCount: 0,
306
- summary: 'No P&L path to decompose (0 steps)',
327
+ summary: "No P&L path to decompose (0 steps)",
307
328
  warning: null,
308
- method: 'full_reval',
329
+ method: "full_reval",
309
330
  };
310
331
  }
311
332
 
312
333
  const stepCount = pnlPath.length - 1;
313
- const canFullReval = legPricingInputs && legPricingInputs.length === legs.length
314
- && riskFreeRate !== undefined && dividendYield !== undefined && underlyingPrices;
334
+ const canFullReval =
335
+ legPricingInputs &&
336
+ legPricingInputs.length === legs.length &&
337
+ riskFreeRate !== undefined &&
338
+ dividendYield !== undefined &&
339
+ underlyingPrices;
315
340
  const r = riskFreeRate ?? 0.045;
316
341
  const q = dividendYield ?? 0.015;
317
342
 
@@ -349,17 +374,26 @@ export function decomposeGreeks(config: GreeksDecompositionConfig): GreeksDecomp
349
374
 
350
375
  for (let j = 0; j < legCount; j++) {
351
376
  const positionSize = legs[j].quantity * legs[j].multiplier;
352
- const legActualChange = ((next.legPrices?.[j] ?? 0) - (cur.legPrices?.[j] ?? 0)) * positionSize;
377
+ const legActualChange =
378
+ ((next.legPrices?.[j] ?? 0) - (cur.legPrices?.[j] ?? 0)) * positionSize;
353
379
 
354
380
  const curIv = cur.legGreeks?.[j]?.iv;
355
381
  const nextIv = next.legGreeks?.[j]?.iv;
356
382
  const lpi = legPricingInputs?.[j];
357
383
 
358
384
  // Full revaluation: reprice with one input changed at a time
359
- if (canFullReval && lpi && S1 !== undefined && S2 !== undefined
360
- && curIv !== null && curIv !== undefined && curIv > 0
361
- && nextIv !== null && nextIv !== undefined && nextIv > 0) {
362
-
385
+ if (
386
+ canFullReval &&
387
+ lpi &&
388
+ S1 !== undefined &&
389
+ S2 !== undefined &&
390
+ curIv !== null &&
391
+ curIv !== undefined &&
392
+ curIv > 0 &&
393
+ nextIv !== null &&
394
+ nextIv !== undefined &&
395
+ nextIv > 0
396
+ ) {
363
397
  const dte1 = computeDte(cur.timestamp, lpi.expiryDate);
364
398
  const dte2 = computeDte(next.timestamp, lpi.expiryDate);
365
399
 
@@ -382,14 +416,25 @@ export function decomposeGreeks(config: GreeksDecompositionConfig): GreeksDecomp
382
416
  // Vanna (spot×vol): P(S2, T1, σ2) - base - delta - vega
383
417
  const priceVanna = priceOption(lpi.type, S2, lpi.strike, dte1, r, q, nextIv);
384
418
 
385
- if (priceBase !== null && priceDelta !== null && priceTheta !== null
386
- && priceVega !== null && priceCharm !== null && priceVanna !== null) {
419
+ if (
420
+ priceBase !== null &&
421
+ priceDelta !== null &&
422
+ priceTheta !== null &&
423
+ priceVega !== null &&
424
+ priceCharm !== null &&
425
+ priceVanna !== null
426
+ ) {
387
427
  const legDeltaPnl = (priceDelta - priceBase) * positionSize;
388
428
  const legThetaPnl = (priceTheta - priceBase) * positionSize;
389
429
  const legVegaPnl = (priceVega - priceBase) * positionSize;
390
- const legCharmPnl = ((priceCharm - priceBase) - (priceDelta - priceBase) - (priceTheta - priceBase)) * positionSize;
391
- const legVannaPnl = ((priceVanna - priceBase) - (priceDelta - priceBase) - (priceVega - priceBase)) * positionSize;
392
- const legResidual = legActualChange - legDeltaPnl - legThetaPnl - legVegaPnl - legCharmPnl - legVannaPnl;
430
+ const legCharmPnl =
431
+ (priceCharm - priceBase - (priceDelta - priceBase) - (priceTheta - priceBase)) *
432
+ positionSize;
433
+ const legVannaPnl =
434
+ (priceVanna - priceBase - (priceDelta - priceBase) - (priceVega - priceBase)) *
435
+ positionSize;
436
+ const legResidual =
437
+ legActualChange - legDeltaPnl - legThetaPnl - legVegaPnl - legCharmPnl - legVannaPnl;
393
438
 
394
439
  stepDelta += legDeltaPnl;
395
440
  stepTheta += legThetaPnl;
@@ -439,28 +484,32 @@ export function decomposeGreeks(config: GreeksDecompositionConfig): GreeksDecomp
439
484
  // - vanna: spot×vol cross-effect (delta changing with vol)
440
485
  // - residual: triple cross (spot+time+vol simultaneously) + model error
441
486
  const rawFactors: Array<{ factor: FactorName; totalPnl: number; steps: number[] }> = [
442
- { factor: 'delta', totalPnl: sumSteps(deltaSteps), steps: deltaSteps },
443
- { factor: 'theta', totalPnl: sumSteps(thetaSteps), steps: thetaSteps },
444
- { factor: 'vega', totalPnl: sumSteps(vegaSteps), steps: vegaSteps },
445
- { factor: 'charm', totalPnl: sumSteps(charmSteps), steps: charmSteps },
446
- { factor: 'vanna', totalPnl: sumSteps(vannaSteps), steps: vannaSteps },
447
- { factor: 'residual', totalPnl: sumSteps(residualSteps), steps: residualSteps },
487
+ { factor: "delta", totalPnl: sumSteps(deltaSteps), steps: deltaSteps },
488
+ { factor: "theta", totalPnl: sumSteps(thetaSteps), steps: thetaSteps },
489
+ { factor: "vega", totalPnl: sumSteps(vegaSteps), steps: vegaSteps },
490
+ { factor: "charm", totalPnl: sumSteps(charmSteps), steps: charmSteps },
491
+ { factor: "vanna", totalPnl: sumSteps(vannaSteps), steps: vannaSteps },
492
+ { factor: "residual", totalPnl: sumSteps(residualSteps), steps: residualSteps },
448
493
  ];
449
494
 
450
495
  rawFactors.sort((a, b) => Math.abs(b.totalPnl) - Math.abs(a.totalPnl));
451
496
  const totalAbsSum = rawFactors.reduce((s, f) => s + Math.abs(f.totalPnl), 0);
452
- const factors: FactorContribution[] = rawFactors.map(f => ({
497
+ const factors: FactorContribution[] = rawFactors.map((f) => ({
453
498
  ...f,
454
499
  pctOfTotal: totalAbsSum > 0 ? (Math.abs(f.totalPnl) / totalAbsSum) * 100 : 0,
455
500
  }));
456
501
 
457
502
  const totalPnlChange = pnlPath[pnlPath.length - 1].strategyPnl - pnlPath[0].strategyPnl;
458
503
  const totalResidual = sumSteps(residualSteps);
459
- const totalAttributed = sumSteps(deltaSteps) + sumSteps(thetaSteps) + sumSteps(vegaSteps) + sumSteps(charmSteps) + sumSteps(vannaSteps);
504
+ const totalAttributed =
505
+ sumSteps(deltaSteps) +
506
+ sumSteps(thetaSteps) +
507
+ sumSteps(vegaSteps) +
508
+ sumSteps(charmSteps) +
509
+ sumSteps(vannaSteps);
460
510
 
461
- const residualPct = Math.abs(totalPnlChange) > 0.01
462
- ? Math.abs(totalResidual) / Math.abs(totalPnlChange)
463
- : 0;
511
+ const residualPct =
512
+ Math.abs(totalPnlChange) > 0.01 ? Math.abs(totalResidual) / Math.abs(totalPnlChange) : 0;
464
513
 
465
514
  // Numerical fallback when full reval still produces >80% residual
466
515
  // (model pricing failure — BS/Bachelier can't accurately price these options)
@@ -502,19 +551,22 @@ export function decomposeGreeks(config: GreeksDecompositionConfig): GreeksDecomp
502
551
  }
503
552
 
504
553
  // Build summary
505
- const methodLabel = canFullReval ? 'full_reval' : 'model';
554
+ const methodLabel = canFullReval ? "full_reval" : "model";
506
555
  const summaryParts = factors
507
- .filter(f => f.factor !== 'residual')
508
- .map(f => `${f.factor} ${f.totalPnl.toFixed(2)} (${f.pctOfTotal.toFixed(0)}%)`);
509
- const residualFactor = factors.find(f => f.factor === 'residual');
556
+ .filter((f) => f.factor !== "residual")
557
+ .map((f) => `${f.factor} ${f.totalPnl.toFixed(2)} (${f.pctOfTotal.toFixed(0)}%)`);
558
+ const residualFactor = factors.find((f) => f.factor === "residual");
510
559
  if (residualFactor && Math.abs(residualFactor.totalPnl) > 0.01) {
511
- summaryParts.push(`residual ${residualFactor.totalPnl.toFixed(2)} (${residualFactor.pctOfTotal.toFixed(0)}%)`);
560
+ summaryParts.push(
561
+ `residual ${residualFactor.totalPnl.toFixed(2)} (${residualFactor.pctOfTotal.toFixed(0)}%)`,
562
+ );
512
563
  }
513
- const summary = `P&L of ${totalPnlChange.toFixed(2)} (${methodLabel}): ${summaryParts.join(', ')}`;
564
+ const summary = `P&L of ${totalPnlChange.toFixed(2)} (${methodLabel}): ${summaryParts.join(", ")}`;
514
565
 
515
- const warning = residualPct > 0.5
516
- ? `Residual ${(residualPct * 100).toFixed(0)}% — attribution limited for some legs.`
517
- : null;
566
+ const warning =
567
+ residualPct > 0.5
568
+ ? `Residual ${(residualPct * 100).toFixed(0)}% — attribution limited for some legs.`
569
+ : null;
518
570
 
519
571
  return {
520
572
  factors,
@@ -525,6 +577,6 @@ export function decomposeGreeks(config: GreeksDecompositionConfig): GreeksDecomp
525
577
  stepCount,
526
578
  summary,
527
579
  warning,
528
- method: canFullReval ? 'full_reval' : 'model',
580
+ method: canFullReval ? "full_reval" : "model",
529
581
  };
530
582
  }
@@ -146,7 +146,12 @@ function jobsToColumns(jobs: IvSolveJob[]): IvSolveColumns {
146
146
  }
147
147
 
148
148
  /** Slice a column batch into a per-shard request, copying the [lo, hi) window. */
149
- function shardRequest(cols: IvSolveColumns, id: number, lo: number, hi: number): IvSolveBatchRequest {
149
+ function shardRequest(
150
+ cols: IvSolveColumns,
151
+ id: number,
152
+ lo: number,
153
+ hi: number,
154
+ ): IvSolveBatchRequest {
150
155
  return {
151
156
  id,
152
157
  count: hi - lo,
@@ -283,37 +288,52 @@ export class IvSolverPool {
283
288
  ok: new Uint8Array(cols.count),
284
289
  };
285
290
 
286
- const base = Math.floor(cols.count / shardCount);
287
- const remainder = cols.count % shardCount;
288
- const shardPromises: Promise<void>[] = [];
289
- let cursor = 0;
290
- for (let s = 0; s < shardCount; s++) {
291
- const size = base + (s < remainder ? 1 : 0);
292
- const lo = cursor;
293
- const hi = cursor + size;
294
- cursor = hi;
295
- if (size === 0) continue;
296
- const request = shardRequest(cols, this.nextRequestId++, lo, hi);
297
- shardPromises.push(
298
- this.runOnWorker(request).then((reply) => {
299
- out.delta.set(reply.delta, lo);
300
- out.gamma.set(reply.gamma, lo);
301
- out.theta.set(reply.theta, lo);
302
- out.vega.set(reply.vega, lo);
303
- out.iv.set(reply.iv, lo);
304
- out.ok.set(reply.ok, lo);
305
- }),
306
- );
307
- }
291
+ // Ref the workers for the duration of this in-flight solve so the event
292
+ // loop stays alive until every shard's result is back — the process can't
293
+ // exit mid-solve and lose results. Idle workers are unref'd (see
294
+ // ensureWorkers), so once the solve completes the pool stops pinning the
295
+ // loop and a one-shot process exits naturally.
296
+ this.refPool();
297
+ try {
298
+ const base = Math.floor(cols.count / shardCount);
299
+ const remainder = cols.count % shardCount;
300
+ const shardPromises: Promise<void>[] = [];
301
+ let cursor = 0;
302
+ for (let s = 0; s < shardCount; s++) {
303
+ const size = base + (s < remainder ? 1 : 0);
304
+ const lo = cursor;
305
+ const hi = cursor + size;
306
+ cursor = hi;
307
+ if (size === 0) continue;
308
+ const request = shardRequest(cols, this.nextRequestId++, lo, hi);
309
+ shardPromises.push(
310
+ this.runOnWorker(request).then((reply) => {
311
+ out.delta.set(reply.delta, lo);
312
+ out.gamma.set(reply.gamma, lo);
313
+ out.theta.set(reply.theta, lo);
314
+ out.vega.set(reply.vega, lo);
315
+ out.iv.set(reply.iv, lo);
316
+ out.ok.set(reply.ok, lo);
317
+ }),
318
+ );
319
+ }
308
320
 
309
- await Promise.all(shardPromises);
310
- return out;
321
+ await Promise.all(shardPromises);
322
+ return out;
323
+ } finally {
324
+ this.unrefPool();
325
+ }
311
326
  }
312
327
 
313
328
  private ensureWorkers(target: number): void {
314
329
  while (this.pool.length < target) {
315
330
  const worker = new Worker(this.workerUrl);
316
331
  worker.setMaxListeners(0);
332
+ // Idle workers must not keep the libuv event loop alive — otherwise a
333
+ // one-shot process that uses the pool and returns normally would hang
334
+ // forever. solveColumnsParallel ref's the pool for the duration of an
335
+ // in-flight solve and unref's it again when done.
336
+ worker.unref();
317
337
  // Surface a worker crash by failing loud; an unhandled worker error
318
338
  // would otherwise leave the pending shard promise hanging forever.
319
339
  worker.on("error", (err) => {
@@ -323,6 +343,16 @@ export class IvSolverPool {
323
343
  }
324
344
  }
325
345
 
346
+ /** Keep the event loop alive while a solve is in flight. */
347
+ private refPool(): void {
348
+ for (const { worker } of this.pool) worker.ref();
349
+ }
350
+
351
+ /** Release the event loop once a solve completes so idle workers don't pin it. */
352
+ private unrefPool(): void {
353
+ for (const { worker } of this.pool) worker.unref();
354
+ }
355
+
326
356
  private runOnWorker(request: IvSolveBatchRequest): Promise<IvSolveBatchReply> {
327
357
  return new Promise<IvSolveBatchReply>((resolve, reject) => {
328
358
  const slot = this.acquire();
@@ -74,11 +74,11 @@ export function solveIvBatch(req: IvSolveBatchRequest): IvSolveBatchReply {
74
74
  req.dividendYield[i],
75
75
  );
76
76
  if (
77
- isFiniteNumber(result.delta)
78
- && isFiniteNumber(result.gamma)
79
- && isFiniteNumber(result.theta)
80
- && isFiniteNumber(result.vega)
81
- && isFiniteNumber(result.iv)
77
+ isFiniteNumber(result.delta) &&
78
+ isFiniteNumber(result.gamma) &&
79
+ isFiniteNumber(result.theta) &&
80
+ isFiniteNumber(result.vega) &&
81
+ isFiniteNumber(result.iv)
82
82
  ) {
83
83
  delta[i] = result.delta;
84
84
  gamma[i] = result.gamma;