tradeblocks-mcp 3.0.2 → 3.0.3
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 +82 -68
- package/dist/{chunk-LDKTV7GW.js → chunk-27S67XW3.js} +24 -52
- package/dist/chunk-27S67XW3.js.map +1 -0
- package/dist/{chunk-FGZH632F.js → chunk-2E63THNI.js} +13 -11
- package/dist/chunk-2E63THNI.js.map +1 -0
- package/dist/{chunk-BKQ4PM4Y.js → chunk-6S37CXUA.js} +11 -13
- package/dist/chunk-6S37CXUA.js.map +1 -0
- package/dist/{chunk-PRAYH3RT.js → chunk-AP7IHUUR.js} +29 -12
- package/dist/chunk-AP7IHUUR.js.map +1 -0
- package/dist/{chunk-QTTR7AAW.js → chunk-C6LL746C.js} +67 -84
- package/dist/{chunk-QTTR7AAW.js.map → chunk-C6LL746C.js.map} +1 -1
- package/dist/{chunk-W2PP3LEH.js → chunk-SEUZYQGQ.js} +17 -8
- package/dist/chunk-SEUZYQGQ.js.map +1 -0
- package/dist/{chunk-4BLCXNQ6.js → chunk-UBUC5A66.js} +105 -114
- package/dist/chunk-UBUC5A66.js.map +1 -0
- package/dist/{chunk-XXYOUIZY.js → chunk-VDU25Z6X.js} +63 -80
- package/dist/chunk-VDU25Z6X.js.map +1 -0
- package/dist/daily-log-processor-BY3ISY6K.js +9 -0
- package/dist/{daily-logs-store-D3QC2XPV.js → daily-logs-store-3C3OTGPK.js} +2 -2
- package/dist/iv-solver-worker.js +9 -1
- package/dist/market-provider-H3ARIVZ4.js +16 -0
- package/dist/{proto-3HZTCWK4.js → proto-V23GIZG2.js} +2 -2
- package/dist/{sync-PO4IPCYV.js → sync-EVLKB3ZJ.js} +7 -7
- package/dist/test-exports.js +358 -254
- package/dist/test-exports.js.map +1 -1
- package/dist/trade-processor-EYA3I3XB.js +9 -0
- package/dist/{trades-store-LOL3FQSF.js → trades-store-R3NL25T2.js} +2 -2
- package/manifest.json +3 -11
- package/package.json +1 -1
- package/server/{chunk-NZO6PT64.js → chunk-4P7D7YZP.js} +102 -113
- package/server/chunk-4P7D7YZP.js.map +1 -0
- package/server/{chunk-72GKJE2U.js → chunk-BOPHW5M6.js} +521 -235
- package/server/chunk-BOPHW5M6.js.map +1 -0
- package/server/{chunk-WA5AAPCH.js → chunk-GH2552SE.js} +17 -8
- package/server/chunk-GH2552SE.js.map +1 -0
- package/server/{chunk-5EBXHT6C.js → chunk-OBYKFW2B.js} +11 -13
- package/server/chunk-OBYKFW2B.js.map +1 -0
- package/server/{chunk-FBNDMCT5.js → chunk-T66KH2XH.js} +13 -11
- package/server/chunk-T66KH2XH.js.map +1 -0
- package/server/{chunk-NRFXAJF7.js → chunk-W5E7FHC4.js} +24 -52
- package/server/chunk-W5E7FHC4.js.map +1 -0
- package/server/{chunk-CSDVJPBB.js → chunk-YUCOAJ4Z.js} +29 -12
- package/server/chunk-YUCOAJ4Z.js.map +1 -0
- package/server/{chunk-PNKG7RY7.js → chunk-ZBJCF4ZG.js} +63 -80
- package/server/chunk-ZBJCF4ZG.js.map +1 -0
- package/server/{config-6IZXEFEX.js → config-DK7KOMNL.js} +1 -1
- package/server/config-DK7KOMNL.js.map +1 -0
- package/server/daily-log-processor-ENEUT22A.js +10 -0
- package/server/{daily-logs-store-NU7M4L5C.js → daily-logs-store-YFNXTB7J.js} +2 -2
- package/server/http-server.js +2 -5
- package/server/index.js +1771 -1538
- package/server/index.js.map +1 -1
- package/server/iv-solver-worker.js +9 -1
- package/server/market-provider-B437HKLW.js +17 -0
- package/server/{proto-DWRZJO4E.js → proto-YZ4TLX2N.js} +2 -2
- package/server/{sync-QFI5L7S7.js → sync-V25UQJA3.js} +7 -7
- package/server/trade-processor-L3PIQ5TG.js +10 -0
- package/server/{trades-store-VY26MWER.js → trades-store-RTTEYMXH.js} +2 -2
- package/src/auth/clients-store.ts +4 -4
- package/src/auth/code-store.ts +1 -1
- package/src/auth/config.ts +11 -12
- package/src/auth/login-page.ts +8 -10
- package/src/auth/provider.ts +35 -30
- package/src/auth/token.ts +17 -15
- package/src/db/backtest-schemas.ts +12 -4
- package/src/db/connection.ts +107 -40
- package/src/db/index.ts +37 -5
- package/src/db/json-adapters.ts +22 -31
- package/src/db/json-migration.ts +11 -15
- package/src/db/json-store.ts +5 -3
- package/src/db/market-datasets.ts +48 -15
- package/src/db/market-schemas.ts +11 -5
- package/src/db/market-views.ts +119 -32
- package/src/db/parquet-writer.ts +7 -7
- package/src/db/profile-schemas.ts +39 -22
- package/src/db/schemas.ts +2 -2
- package/src/http-server.ts +7 -12
- package/src/index.ts +8 -9
- package/src/market/ingestor/index.ts +1 -4
- package/src/market/ingestor/market-ingestor.ts +140 -67
- package/src/market/ingestor/types.ts +1 -3
- package/src/market/stores/chain-sql.ts +2 -8
- package/src/market/stores/chain-store.ts +1 -4
- package/src/market/stores/duckdb-chain-store.ts +3 -14
- package/src/market/stores/duckdb-enriched-store.ts +3 -13
- package/src/market/stores/duckdb-quote-store.ts +10 -27
- package/src/market/stores/duckdb-spot-store.ts +4 -20
- package/src/market/stores/enriched-sql.ts +3 -9
- package/src/market/stores/enriched-store.ts +2 -2
- package/src/market/stores/parquet-chain-store.ts +5 -22
- package/src/market/stores/parquet-enriched-store.ts +4 -16
- package/src/market/stores/parquet-oi-daily-store.ts +3 -14
- package/src/market/stores/parquet-quote-store.ts +9 -29
- package/src/market/stores/parquet-spot-store.ts +14 -33
- package/src/market/stores/quote-store.ts +2 -10
- package/src/market/stores/spot-sql.ts +3 -15
- package/src/market/stores/spot-store.ts +1 -1
- package/src/market/tickers/defaults.json +6 -6
- package/src/market/tickers/loader.ts +2 -7
- package/src/market/tickers/registry.ts +5 -15
- package/src/market/tickers/schemas.ts +2 -6
- package/src/models/strategy-profile.ts +39 -39
- package/src/sync/block-sync.ts +44 -65
- package/src/sync/index.ts +1 -4
- package/src/sync/metadata.ts +14 -20
- package/src/test-exports.ts +127 -123
- package/src/tools/analysis.ts +180 -159
- package/src/tools/batch-exit-analysis.ts +100 -59
- package/src/tools/blocks/analysis.ts +116 -101
- package/src/tools/blocks/comparison.ts +281 -364
- package/src/tools/blocks/core.ts +208 -253
- package/src/tools/blocks/health.ts +571 -563
- package/src/tools/blocks/similarity.ts +316 -283
- package/src/tools/edge-decay.ts +124 -197
- package/src/tools/exit-analysis.ts +130 -77
- package/src/tools/greeks-attribution.ts +84 -35
- package/src/tools/guides.ts +4 -6
- package/src/tools/imports.ts +9 -12
- package/src/tools/market-data.ts +1067 -757
- package/src/tools/market-enrichment.ts +3 -3
- package/src/tools/market-fetch.ts +148 -67
- package/src/tools/market-imports.ts +12 -12
- package/src/tools/middleware/sync-middleware.ts +5 -6
- package/src/tools/performance.ts +185 -302
- package/src/tools/profile-analysis.ts +52 -66
- package/src/tools/profiles.ts +106 -69
- package/src/tools/regime-advisor.ts +20 -45
- package/src/tools/replay.ts +81 -77
- package/src/tools/reports/discrepancies.ts +298 -328
- package/src/tools/reports/fields.ts +7 -25
- package/src/tools/reports/helpers.ts +18 -49
- package/src/tools/reports/predictive.ts +27 -70
- package/src/tools/reports/slippage-trends.ts +315 -345
- package/src/tools/reports/slippage.ts +1 -4
- package/src/tools/reports/strategy-matches.ts +399 -441
- package/src/tools/schema.ts +43 -40
- package/src/tools/shared/filters.ts +3 -9
- package/src/tools/snapshot.ts +9 -30
- package/src/tools/sql.ts +15 -14
- package/src/tools/tickers.ts +1 -4
- package/src/utils/batch-exit-analysis.ts +31 -42
- package/src/utils/black-scholes.ts +39 -29
- package/src/utils/block-loader.ts +68 -82
- package/src/utils/calibration-probe.ts +1 -2
- package/src/utils/chain-loader.ts +3 -3
- package/src/utils/csv-discovery.ts +16 -22
- package/src/utils/data-quality.ts +24 -36
- package/src/utils/exit-triggers.ts +91 -96
- package/src/utils/field-timing.ts +94 -79
- package/src/utils/filter-predicates.ts +13 -9
- package/src/utils/flatfile-importer.ts +94 -64
- package/src/utils/greeks-decomposition.ts +152 -100
- package/src/utils/iv-solver-pool.ts +55 -25
- package/src/utils/iv-solver-worker.ts +5 -5
- package/src/utils/market-enricher.ts +528 -497
- package/src/utils/market-importer.ts +31 -12
- package/src/utils/market-provider.ts +21 -23
- package/src/utils/massive-tier.ts +5 -7
- package/src/utils/migrate-option-data-helpers.ts +2 -8
- package/src/utils/option-quote-greeks.ts +25 -31
- package/src/utils/option-time.ts +4 -8
- package/src/utils/output-formatter.ts +1 -4
- package/src/utils/provider-capabilities.ts +1 -4
- package/src/utils/providers/massive.ts +59 -93
- package/src/utils/providers/thetadata/backfill.ts +14 -23
- package/src/utils/providers/thetadata/client.ts +12 -8
- package/src/utils/providers/thetadata/decode.ts +2 -20
- package/src/utils/providers/thetadata/endpoints.ts +32 -40
- package/src/utils/providers/thetadata/join.ts +11 -10
- package/src/utils/providers/thetadata/proto.ts +12 -10
- package/src/utils/providers/thetadata/quote-mid-greeks.ts +5 -5
- package/src/utils/providers/thetadata.ts +11 -10
- package/src/utils/quote-enricher.ts +4 -4
- package/src/utils/quote-parquet-projection.ts +3 -11
- package/src/utils/sample-date-selector.ts +3 -5
- package/src/utils/schema-metadata.ts +102 -70
- package/src/utils/ticker.ts +5 -9
- package/src/utils/trade-replay.ts +77 -68
- package/dist/chunk-4BLCXNQ6.js.map +0 -1
- package/dist/chunk-BKQ4PM4Y.js.map +0 -1
- package/dist/chunk-FGZH632F.js.map +0 -1
- package/dist/chunk-LDKTV7GW.js.map +0 -1
- package/dist/chunk-PRAYH3RT.js.map +0 -1
- package/dist/chunk-W2PP3LEH.js.map +0 -1
- package/dist/chunk-XXYOUIZY.js.map +0 -1
- package/dist/daily-log-processor-6MWJ23JK.js +0 -9
- package/dist/market-provider-VDRJUEF2.js +0 -16
- package/dist/trade-processor-NHU4VWRX.js +0 -9
- package/server/chunk-5EBXHT6C.js.map +0 -1
- package/server/chunk-72GKJE2U.js.map +0 -1
- package/server/chunk-CSDVJPBB.js.map +0 -1
- package/server/chunk-FBNDMCT5.js.map +0 -1
- package/server/chunk-NRFXAJF7.js.map +0 -1
- package/server/chunk-NZO6PT64.js.map +0 -1
- package/server/chunk-PNKG7RY7.js.map +0 -1
- package/server/chunk-WA5AAPCH.js.map +0 -1
- package/server/config-6IZXEFEX.js.map +0 -1
- package/server/daily-log-processor-Y3PVSVBM.js +0 -10
- package/server/market-provider-VOYYVYWT.js +0 -17
- package/server/trade-processor-JWVS37KM.js +0 -10
- /package/dist/{daily-log-processor-6MWJ23JK.js.map → daily-log-processor-BY3ISY6K.js.map} +0 -0
- /package/dist/{daily-logs-store-D3QC2XPV.js.map → daily-logs-store-3C3OTGPK.js.map} +0 -0
- /package/dist/{market-provider-VDRJUEF2.js.map → market-provider-H3ARIVZ4.js.map} +0 -0
- /package/dist/{proto-3HZTCWK4.js.map → proto-V23GIZG2.js.map} +0 -0
- /package/dist/{sync-PO4IPCYV.js.map → sync-EVLKB3ZJ.js.map} +0 -0
- /package/dist/{trade-processor-NHU4VWRX.js.map → trade-processor-EYA3I3XB.js.map} +0 -0
- /package/dist/{trades-store-LOL3FQSF.js.map → trades-store-R3NL25T2.js.map} +0 -0
- /package/server/{daily-log-processor-Y3PVSVBM.js.map → daily-log-processor-ENEUT22A.js.map} +0 -0
- /package/server/{daily-logs-store-NU7M4L5C.js.map → daily-logs-store-YFNXTB7J.js.map} +0 -0
- /package/server/{market-provider-VOYYVYWT.js.map → market-provider-B437HKLW.js.map} +0 -0
- /package/server/{proto-DWRZJO4E.js.map → proto-YZ4TLX2N.js.map} +0 -0
- /package/server/{sync-QFI5L7S7.js.map → sync-V25UQJA3.js.map} +0 -0
- /package/server/{trade-processor-JWVS37KM.js.map → trade-processor-L3PIQ5TG.js.map} +0 -0
- /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
|
|
19
|
-
import { bsPrice, bachelierPrice, BACHELIER_DTE_THRESHOLD } from
|
|
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 =
|
|
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;
|
|
30
|
-
pctOfTotal: number;
|
|
31
|
-
steps: number[];
|
|
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;
|
|
36
|
-
legIndices: number[];
|
|
37
|
-
totalVegaPnl: number;
|
|
38
|
-
avgIvChange: number;
|
|
39
|
-
steps: number[];
|
|
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[];
|
|
44
|
-
legGroupVega?: LegGroupVega[];
|
|
45
|
-
totalPnlChange: number;
|
|
46
|
-
totalAttributed: number;
|
|
47
|
-
totalResidual: number;
|
|
48
|
-
stepCount: number;
|
|
49
|
-
summary: string;
|
|
50
|
-
warning?: string | null;
|
|
51
|
-
method:
|
|
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:
|
|
62
|
-
expiryDate: string;
|
|
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>;
|
|
69
|
-
legGroups?: LegGroupDef[];
|
|
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;
|
|
74
|
-
dividendYield?: number;
|
|
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(
|
|
97
|
-
const [h2, m2] = time2.split(
|
|
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 +
|
|
106
|
-
const d2 = new Date(date2 +
|
|
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(
|
|
112
|
-
const [h2, m2] = time2.split(
|
|
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 =
|
|
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 =
|
|
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:
|
|
202
|
-
{ factor:
|
|
203
|
-
{ factor:
|
|
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(
|
|
214
|
-
|
|
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,
|
|
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:
|
|
225
|
-
|
|
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(
|
|
238
|
-
const timePart = timestamp.split(
|
|
239
|
-
const [eyy, emm, edd] = expiryDate.split(
|
|
240
|
-
const [byy, bmm, bdd] = dateStr.split(
|
|
241
|
-
const [hh, min] = timePart.split(
|
|
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:
|
|
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 ===
|
|
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 {
|
|
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:
|
|
288
|
-
{ factor:
|
|
289
|
-
{ factor:
|
|
290
|
-
{ factor:
|
|
291
|
-
{ factor:
|
|
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
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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:
|
|
327
|
+
summary: "No P&L path to decompose (0 steps)",
|
|
307
328
|
warning: null,
|
|
308
|
-
method:
|
|
329
|
+
method: "full_reval",
|
|
309
330
|
};
|
|
310
331
|
}
|
|
311
332
|
|
|
312
333
|
const stepCount = pnlPath.length - 1;
|
|
313
|
-
const canFullReval =
|
|
314
|
-
|
|
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 =
|
|
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 (
|
|
360
|
-
|
|
361
|
-
|
|
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 (
|
|
386
|
-
|
|
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 =
|
|
391
|
-
|
|
392
|
-
|
|
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:
|
|
443
|
-
{ factor:
|
|
444
|
-
{ factor:
|
|
445
|
-
{ factor:
|
|
446
|
-
{ factor:
|
|
447
|
-
{ factor:
|
|
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 =
|
|
504
|
+
const totalAttributed =
|
|
505
|
+
sumSteps(deltaSteps) +
|
|
506
|
+
sumSteps(thetaSteps) +
|
|
507
|
+
sumSteps(vegaSteps) +
|
|
508
|
+
sumSteps(charmSteps) +
|
|
509
|
+
sumSteps(vannaSteps);
|
|
460
510
|
|
|
461
|
-
const residualPct =
|
|
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 ?
|
|
554
|
+
const methodLabel = canFullReval ? "full_reval" : "model";
|
|
506
555
|
const summaryParts = factors
|
|
507
|
-
.filter(f => f.factor !==
|
|
508
|
-
.map(f => `${f.factor} ${f.totalPnl.toFixed(2)} (${f.pctOfTotal.toFixed(0)}%)`);
|
|
509
|
-
const residualFactor = factors.find(f => f.factor ===
|
|
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(
|
|
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 =
|
|
516
|
-
|
|
517
|
-
|
|
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 ?
|
|
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(
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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;
|