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
package/src/utils/ticker.ts
CHANGED
|
@@ -37,7 +37,7 @@ export function normalizeTicker(value: string | null | undefined): string | null
|
|
|
37
37
|
* Resolve ticker from arbitrary object fields (case/alias tolerant).
|
|
38
38
|
*/
|
|
39
39
|
export function resolveTickerFromFields(
|
|
40
|
-
fields: Record<string, unknown> | null | undefined
|
|
40
|
+
fields: Record<string, unknown> | null | undefined,
|
|
41
41
|
): string | null {
|
|
42
42
|
if (!fields) return null;
|
|
43
43
|
for (const field of TICKER_FIELD_CANDIDATES) {
|
|
@@ -55,11 +55,10 @@ export function resolveTickerFromFields(
|
|
|
55
55
|
*/
|
|
56
56
|
export function resolveTradeTicker(
|
|
57
57
|
trade: Pick<Trade, "customFields">,
|
|
58
|
-
fallback: string = DEFAULT_MARKET_TICKER
|
|
58
|
+
fallback: string = DEFAULT_MARKET_TICKER,
|
|
59
59
|
): string {
|
|
60
60
|
return (
|
|
61
|
-
resolveTickerFromFields(trade.customFields as Record<string, unknown> | undefined) ??
|
|
62
|
-
fallback
|
|
61
|
+
resolveTickerFromFields(trade.customFields as Record<string, unknown> | undefined) ?? fallback
|
|
63
62
|
);
|
|
64
63
|
}
|
|
65
64
|
|
|
@@ -68,12 +67,9 @@ export function resolveTradeTicker(
|
|
|
68
67
|
*/
|
|
69
68
|
export function resolveTickerFromCsvRow(
|
|
70
69
|
row: Record<string, string>,
|
|
71
|
-
fallback: string = DEFAULT_MARKET_TICKER
|
|
70
|
+
fallback: string = DEFAULT_MARKET_TICKER,
|
|
72
71
|
): string {
|
|
73
|
-
return (
|
|
74
|
-
resolveTickerFromFields(row as Record<string, unknown>) ??
|
|
75
|
-
fallback
|
|
76
|
-
);
|
|
72
|
+
return resolveTickerFromFields(row as Record<string, unknown>) ?? fallback;
|
|
77
73
|
}
|
|
78
74
|
|
|
79
75
|
/**
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* All functions are pure — no fetch, no DuckDB.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { BarRow } from
|
|
11
|
-
import { computeLegGreeks, type GreeksResult } from
|
|
10
|
+
import type { BarRow } from "./market-provider.ts";
|
|
11
|
+
import { computeLegGreeks, type GreeksResult } from "./black-scholes.ts";
|
|
12
12
|
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
// Types
|
|
@@ -16,25 +16,25 @@ import { computeLegGreeks, type GreeksResult } from './black-scholes.ts';
|
|
|
16
16
|
|
|
17
17
|
/** A parsed leg from a tradelog "legs" string (before OCC ticker resolution). */
|
|
18
18
|
export interface ParsedLeg {
|
|
19
|
-
root: string;
|
|
20
|
-
strike: number;
|
|
21
|
-
type:
|
|
22
|
-
quantity: number;
|
|
19
|
+
root: string; // "SPY", "SPX", "SPXW"
|
|
20
|
+
strike: number; // Numeric strike price
|
|
21
|
+
type: "C" | "P"; // Call or Put
|
|
22
|
+
quantity: number; // +1 or -1 (direction derived from position in spread)
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/** A fully resolved leg ready for replay (after OCC ticker construction). */
|
|
26
26
|
export interface ReplayLeg {
|
|
27
|
-
occTicker: string;
|
|
28
|
-
quantity: number;
|
|
29
|
-
entryPrice: number;
|
|
30
|
-
multiplier: number;
|
|
27
|
+
occTicker: string; // Full OCC ticker for Massive API fetch
|
|
28
|
+
quantity: number; // Positive = long, negative = short
|
|
29
|
+
entryPrice: number; // Per-contract entry price
|
|
30
|
+
multiplier: number; // 100 for standard equity/index options
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/** A single point on the strategy P&L path. */
|
|
34
34
|
export interface PnlPoint {
|
|
35
|
-
timestamp: string;
|
|
36
|
-
strategyPnl: number;
|
|
37
|
-
legPrices: number[];
|
|
35
|
+
timestamp: string; // "YYYY-MM-DD HH:MM" ET
|
|
36
|
+
strategyPnl: number; // Combined P&L across all legs at this minute
|
|
37
|
+
legPrices: number[]; // Mark price for each leg at this minute (bid/ask mid or HL2 fallback)
|
|
38
38
|
underlyingPrice?: number; // Underlying price used for greeks / decomposition at this timestamp
|
|
39
39
|
// Per-leg greeks (Phase 69) — array parallel to legPrices
|
|
40
40
|
legGreeks?: GreeksResult[];
|
|
@@ -55,11 +55,11 @@ export interface PnlPoint {
|
|
|
55
55
|
|
|
56
56
|
/** Configuration for greeks computation in P&L path. */
|
|
57
57
|
export interface GreeksConfig {
|
|
58
|
-
underlyingPrices: Map<string, number>;
|
|
59
|
-
legs: Array<{ strike: number; type:
|
|
60
|
-
riskFreeRate: number;
|
|
61
|
-
dividendYield: number;
|
|
62
|
-
ivpByDate?: Map<string, number>;
|
|
58
|
+
underlyingPrices: Map<string, number>; // timestamp -> underlying price
|
|
59
|
+
legs: Array<{ strike: number; type: "C" | "P"; expiryDate: string }>; // per-leg BS inputs
|
|
60
|
+
riskFreeRate: number; // e.g. 0.045
|
|
61
|
+
dividendYield: number; // e.g. 0.015 for SPX, 0 otherwise
|
|
62
|
+
ivpByDate?: Map<string, number>; // date -> IVP value
|
|
63
63
|
/** Sorted intraday timestamps from underlyingPrices for nearest-timestamp binary search. */
|
|
64
64
|
sortedTimestamps?: string[];
|
|
65
65
|
}
|
|
@@ -67,14 +67,14 @@ export interface GreeksConfig {
|
|
|
67
67
|
/** Complete replay result with P&L path, MFE/MAE, and metadata. */
|
|
68
68
|
export interface ReplayResult {
|
|
69
69
|
pnlPath: PnlPoint[];
|
|
70
|
-
mfe: number;
|
|
71
|
-
mae: number;
|
|
72
|
-
mfeTimestamp: string;
|
|
73
|
-
maeTimestamp: string;
|
|
74
|
-
totalPnl: number;
|
|
75
|
-
totalBars?: number;
|
|
76
|
-
legs: ReplayLeg[];
|
|
77
|
-
greeksWarning?: string | null;
|
|
70
|
+
mfe: number; // Max of strategyPnl series
|
|
71
|
+
mae: number; // Min of strategyPnl series
|
|
72
|
+
mfeTimestamp: string; // When MFE occurred
|
|
73
|
+
maeTimestamp: string; // When MAE occurred
|
|
74
|
+
totalPnl: number; // Final P&L at last bar
|
|
75
|
+
totalBars?: number; // Total minute bars before format filtering
|
|
76
|
+
legs: ReplayLeg[]; // The legs that were replayed
|
|
77
|
+
greeksWarning?: string | null; // D-12: warning when >50% of leg-timestamps have null greeks
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// ---------------------------------------------------------------------------
|
|
@@ -91,7 +91,7 @@ export interface ReplayResult {
|
|
|
91
91
|
* Guards against broken exchange quotes (crossed bid>ask; blown ask>10×bid
|
|
92
92
|
* with mid>$1) by falling back to HL2.
|
|
93
93
|
*/
|
|
94
|
-
export function markPrice(bar: Pick<BarRow,
|
|
94
|
+
export function markPrice(bar: Pick<BarRow, "high" | "low" | "bid" | "ask">): number {
|
|
95
95
|
const { bid, ask } = bar;
|
|
96
96
|
const hl2 = (bar.high + bar.low) / 2;
|
|
97
97
|
if (bid != null && ask != null && (bid > 0 || ask > 0)) {
|
|
@@ -127,17 +127,24 @@ export function findNearestTimestamp(
|
|
|
127
127
|
const targetMin = timestampToMinutes(target);
|
|
128
128
|
if (targetMin === null) return undefined;
|
|
129
129
|
|
|
130
|
-
let lo = 0,
|
|
130
|
+
let lo = 0,
|
|
131
|
+
hi = sortedTimestamps.length - 1;
|
|
131
132
|
let bestIdx = 0;
|
|
132
133
|
let bestDiff = Infinity;
|
|
133
134
|
|
|
134
135
|
while (lo <= hi) {
|
|
135
136
|
const mid = (lo + hi) >>> 1;
|
|
136
137
|
const midMin = timestampToMinutes(sortedTimestamps[mid]);
|
|
137
|
-
if (midMin === null) {
|
|
138
|
+
if (midMin === null) {
|
|
139
|
+
lo = mid + 1;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
138
142
|
|
|
139
143
|
const diff = Math.abs(midMin - targetMin);
|
|
140
|
-
if (diff < bestDiff) {
|
|
144
|
+
if (diff < bestDiff) {
|
|
145
|
+
bestDiff = diff;
|
|
146
|
+
bestIdx = mid;
|
|
147
|
+
}
|
|
141
148
|
if (midMin < targetMin) lo = mid + 1;
|
|
142
149
|
else if (midMin > targetMin) hi = mid - 1;
|
|
143
150
|
else break; // exact match
|
|
@@ -148,9 +155,9 @@ export function findNearestTimestamp(
|
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
function timestampToMinutes(ts: string): number | null {
|
|
151
|
-
const timePart = ts.split(
|
|
158
|
+
const timePart = ts.split(" ")[1];
|
|
152
159
|
if (!timePart) return null;
|
|
153
|
-
const [h, m] = timePart.split(
|
|
160
|
+
const [h, m] = timePart.split(":").map(Number);
|
|
154
161
|
if (isNaN(h) || isNaN(m)) return null;
|
|
155
162
|
return h * 60 + m;
|
|
156
163
|
}
|
|
@@ -171,13 +178,14 @@ const VERBOSE_LEG_RE = /^([A-Z]+)\s+\w+\s+(\d+(?:\.\d+)?)\s+(Call|Put)$/i;
|
|
|
171
178
|
// Option Omega format: "{contracts} {Mon} {day} {strike} {P|C} {STO|BTO} {price}"
|
|
172
179
|
// Example: "397 Mar 12 6610 P STO 35.85"
|
|
173
180
|
// Captures: (1)contracts (2)month (3)day (4)strike (5)C|P (6)STO|BTO (7)price
|
|
174
|
-
const OO_LEG_RE =
|
|
181
|
+
const OO_LEG_RE =
|
|
182
|
+
/^(\d+)\s+(\w+)\s+(\d+)\s+(\d+(?:\.\d+)?)\s+(C|P)\s+(STO|BTO|STC|BTC)\s+(\d+(?:\.\d+)?)$/i;
|
|
175
183
|
|
|
176
184
|
/** Extended parsed leg with entry price from OO format. */
|
|
177
185
|
export interface ParsedLegOO extends ParsedLeg {
|
|
178
|
-
entryPrice?: number;
|
|
179
|
-
contracts?: number;
|
|
180
|
-
expiryHint?: string;
|
|
186
|
+
entryPrice?: number; // Fill price from OO leg (e.g., 35.85)
|
|
187
|
+
contracts?: number; // Contract count from OO leg
|
|
188
|
+
expiryHint?: string; // "Mon DD" from OO format (e.g., "Mar 12") for multi-expiry strategies
|
|
181
189
|
}
|
|
182
190
|
|
|
183
191
|
/**
|
|
@@ -196,46 +204,46 @@ export interface ParsedLegOO extends ParsedLeg {
|
|
|
196
204
|
* @throws Error if legs string is empty or cannot be parsed
|
|
197
205
|
*/
|
|
198
206
|
export function parseLegsString(legsStr: string): ParsedLegOO[] {
|
|
199
|
-
if (!legsStr || legsStr.trim() ===
|
|
207
|
+
if (!legsStr || legsStr.trim() === "") {
|
|
200
208
|
throw new Error('Cannot parse legs "" — use hypothetical mode with explicit strikes');
|
|
201
209
|
}
|
|
202
210
|
|
|
203
211
|
// Detect Option Omega pipe-delimited format
|
|
204
|
-
if (legsStr.includes(
|
|
212
|
+
if (legsStr.includes("|")) {
|
|
205
213
|
return parseOOLegs(legsStr);
|
|
206
214
|
}
|
|
207
215
|
|
|
208
|
-
const parts = legsStr.includes(
|
|
216
|
+
const parts = legsStr.includes("/") ? legsStr.split("/") : [legsStr];
|
|
209
217
|
const legs: ParsedLegOO[] = [];
|
|
210
|
-
let inheritedRoot =
|
|
218
|
+
let inheritedRoot = "";
|
|
211
219
|
|
|
212
220
|
for (let i = 0; i < parts.length; i++) {
|
|
213
221
|
const raw = parts[i].trim();
|
|
214
222
|
let root: string;
|
|
215
223
|
let strike: number;
|
|
216
|
-
let type:
|
|
224
|
+
let type: "C" | "P";
|
|
217
225
|
|
|
218
226
|
const compactMatch = raw.match(COMPACT_LEG_RE);
|
|
219
227
|
if (compactMatch) {
|
|
220
228
|
root = compactMatch[1].toUpperCase();
|
|
221
229
|
strike = parseFloat(compactMatch[2]);
|
|
222
|
-
type = compactMatch[3].toUpperCase() as
|
|
230
|
+
type = compactMatch[3].toUpperCase() as "C" | "P";
|
|
223
231
|
} else {
|
|
224
232
|
// Try compact without root (e.g., "465C" in "SPY 470C/465C")
|
|
225
233
|
const noRootMatch = raw.match(COMPACT_NO_ROOT_RE);
|
|
226
234
|
if (noRootMatch && inheritedRoot) {
|
|
227
235
|
root = inheritedRoot;
|
|
228
236
|
strike = parseFloat(noRootMatch[1]);
|
|
229
|
-
type = noRootMatch[2].toUpperCase() as
|
|
237
|
+
type = noRootMatch[2].toUpperCase() as "C" | "P";
|
|
230
238
|
} else {
|
|
231
239
|
const verboseMatch = raw.match(VERBOSE_LEG_RE);
|
|
232
240
|
if (verboseMatch) {
|
|
233
241
|
root = verboseMatch[1].toUpperCase();
|
|
234
242
|
strike = parseFloat(verboseMatch[2]);
|
|
235
|
-
type = verboseMatch[3].toLowerCase() ===
|
|
243
|
+
type = verboseMatch[3].toLowerCase() === "call" ? "C" : "P";
|
|
236
244
|
} else {
|
|
237
245
|
throw new Error(
|
|
238
|
-
`Cannot parse legs "${legsStr}" — use hypothetical mode with explicit strikes
|
|
246
|
+
`Cannot parse legs "${legsStr}" — use hypothetical mode with explicit strikes`,
|
|
239
247
|
);
|
|
240
248
|
}
|
|
241
249
|
}
|
|
@@ -245,7 +253,7 @@ export function parseLegsString(legsStr: string): ParsedLegOO[] {
|
|
|
245
253
|
if (i === 0) inheritedRoot = root;
|
|
246
254
|
|
|
247
255
|
// First leg is bought (+1), subsequent alternate -1, +1, -1...
|
|
248
|
-
const quantity = i === 0 ? 1 :
|
|
256
|
+
const quantity = i === 0 ? 1 : i % 2 === 0 ? 1 : -1;
|
|
249
257
|
|
|
250
258
|
legs.push({ root, strike, type, quantity });
|
|
251
259
|
}
|
|
@@ -264,7 +272,7 @@ export function parseLegsString(legsStr: string): ParsedLegOO[] {
|
|
|
264
272
|
* - Open+close fills: same strike, same date, opposite direction (close dropped)
|
|
265
273
|
*/
|
|
266
274
|
function parseOOLegs(legsStr: string): ParsedLegOO[] {
|
|
267
|
-
const segments = legsStr.split(
|
|
275
|
+
const segments = legsStr.split("|").map((s) => s.trim());
|
|
268
276
|
const legs: ParsedLegOO[] = [];
|
|
269
277
|
const seen = new Set<string>();
|
|
270
278
|
|
|
@@ -272,7 +280,7 @@ function parseOOLegs(legsStr: string): ParsedLegOO[] {
|
|
|
272
280
|
const match = seg.match(OO_LEG_RE);
|
|
273
281
|
if (!match) {
|
|
274
282
|
throw new Error(
|
|
275
|
-
`Cannot parse OO leg segment "${seg}" — use hypothetical mode with explicit strikes
|
|
283
|
+
`Cannot parse OO leg segment "${seg}" — use hypothetical mode with explicit strikes`,
|
|
276
284
|
);
|
|
277
285
|
}
|
|
278
286
|
|
|
@@ -280,7 +288,7 @@ function parseOOLegs(legsStr: string): ParsedLegOO[] {
|
|
|
280
288
|
const month = match[2];
|
|
281
289
|
const day = match[3];
|
|
282
290
|
const strike = parseFloat(match[4]);
|
|
283
|
-
const type = match[5].toUpperCase() as
|
|
291
|
+
const type = match[5].toUpperCase() as "C" | "P";
|
|
284
292
|
const direction = match[6].toUpperCase();
|
|
285
293
|
const price = parseFloat(match[7]);
|
|
286
294
|
|
|
@@ -291,10 +299,10 @@ function parseOOLegs(legsStr: string): ParsedLegOO[] {
|
|
|
291
299
|
seen.add(key);
|
|
292
300
|
|
|
293
301
|
legs.push({
|
|
294
|
-
root:
|
|
302
|
+
root: "", // OO format doesn't include root — caller provides via trade's ticker field
|
|
295
303
|
strike,
|
|
296
304
|
type,
|
|
297
|
-
quantity: direction ===
|
|
305
|
+
quantity: direction === "BTO" ? 1 : -1,
|
|
298
306
|
entryPrice: price,
|
|
299
307
|
contracts,
|
|
300
308
|
expiryHint: `${month} ${day}`,
|
|
@@ -318,16 +326,16 @@ function parseOOLegs(legsStr: string): ParsedLegOO[] {
|
|
|
318
326
|
export function buildOccTicker(
|
|
319
327
|
root: string,
|
|
320
328
|
expiry: string,
|
|
321
|
-
type:
|
|
329
|
+
type: "C" | "P",
|
|
322
330
|
strike: number,
|
|
323
331
|
): string {
|
|
324
332
|
// Extract YYMMDD from "YYYY-MM-DD"
|
|
325
|
-
const [yyyy, mm, dd] = expiry.split(
|
|
333
|
+
const [yyyy, mm, dd] = expiry.split("-");
|
|
326
334
|
const yy = yyyy.slice(2);
|
|
327
335
|
|
|
328
336
|
// Strike * 1000 padded to 8 digits
|
|
329
337
|
const strikeInt = Math.round(strike * 1000);
|
|
330
|
-
const strikePadded = String(strikeInt).padStart(8,
|
|
338
|
+
const strikePadded = String(strikeInt).padStart(8, "0");
|
|
331
339
|
|
|
332
340
|
return `${root}${yy}${mm}${dd}${type}${strikePadded}`;
|
|
333
341
|
}
|
|
@@ -361,7 +369,7 @@ export function computeStrategyPnlPath(
|
|
|
361
369
|
const legMaps: Map<string, BarRow>[] = barsByLeg.map((bars) => {
|
|
362
370
|
const map = new Map<string, BarRow>();
|
|
363
371
|
for (const bar of bars) {
|
|
364
|
-
const ts = `${bar.date} ${bar.time ??
|
|
372
|
+
const ts = `${bar.date} ${bar.time ?? ""}`.trim();
|
|
365
373
|
map.set(ts, bar);
|
|
366
374
|
}
|
|
367
375
|
return map;
|
|
@@ -371,7 +379,7 @@ export function computeStrategyPnlPath(
|
|
|
371
379
|
const allTimestamps = new Set<string>();
|
|
372
380
|
for (const bars of barsByLeg) {
|
|
373
381
|
for (const bar of bars) {
|
|
374
|
-
allTimestamps.add(`${bar.date} ${bar.time ??
|
|
382
|
+
allTimestamps.add(`${bar.date} ${bar.time ?? ""}`.trim());
|
|
375
383
|
}
|
|
376
384
|
}
|
|
377
385
|
const sortedTimestamps = [...allTimestamps].sort();
|
|
@@ -380,7 +388,6 @@ export function computeStrategyPnlPath(
|
|
|
380
388
|
const path: PnlPoint[] = [];
|
|
381
389
|
const lastBar: (BarRow | undefined)[] = new Array(legs.length).fill(undefined);
|
|
382
390
|
|
|
383
|
-
|
|
384
391
|
for (const ts of sortedTimestamps) {
|
|
385
392
|
let complete = true;
|
|
386
393
|
const legPrices: number[] = [];
|
|
@@ -413,14 +420,17 @@ export function computeStrategyPnlPath(
|
|
|
413
420
|
if (nearest) underlyingPrice = greeksConfig.underlyingPrices.get(nearest);
|
|
414
421
|
}
|
|
415
422
|
if (underlyingPrice === undefined) {
|
|
416
|
-
const dateOnly = ts.split(
|
|
423
|
+
const dateOnly = ts.split(" ")[0];
|
|
417
424
|
underlyingPrice = greeksConfig.underlyingPrices.get(dateOnly);
|
|
418
425
|
}
|
|
419
426
|
|
|
420
427
|
if (underlyingPrice !== undefined) {
|
|
421
428
|
point.underlyingPrice = underlyingPrice;
|
|
422
429
|
const legGreeksArr: GreeksResult[] = [];
|
|
423
|
-
let netDelta = 0,
|
|
430
|
+
let netDelta = 0,
|
|
431
|
+
netGamma = 0,
|
|
432
|
+
netTheta = 0,
|
|
433
|
+
netVega = 0;
|
|
424
434
|
let allNull = true;
|
|
425
435
|
|
|
426
436
|
for (let j = 0; j < legs.length; j++) {
|
|
@@ -431,11 +441,11 @@ export function computeStrategyPnlPath(
|
|
|
431
441
|
}
|
|
432
442
|
|
|
433
443
|
// Compute fractional DTE from bar timestamp to leg expiry
|
|
434
|
-
const dateStr = ts.split(
|
|
435
|
-
const timePart = ts.split(
|
|
436
|
-
const [eyy, emm, edd] = legCfg.expiryDate.split(
|
|
437
|
-
const [byy, bmm, bdd] = dateStr.split(
|
|
438
|
-
const [hh, min] = timePart.split(
|
|
444
|
+
const dateStr = ts.split(" ")[0];
|
|
445
|
+
const timePart = ts.split(" ")[1] ?? "09:30";
|
|
446
|
+
const [eyy, emm, edd] = legCfg.expiryDate.split("-").map(Number);
|
|
447
|
+
const [byy, bmm, bdd] = dateStr.split("-").map(Number);
|
|
448
|
+
const [hh, min] = timePart.split(":").map(Number);
|
|
439
449
|
|
|
440
450
|
const expiryMs = new Date(eyy, emm - 1, edd).getTime() + 16 * 60 * 60 * 1000; // 4:00 PM ET
|
|
441
451
|
const barMs = new Date(byy, bmm - 1, bdd).getTime() + (hh * 60 + min) * 60 * 1000;
|
|
@@ -459,7 +469,7 @@ export function computeStrategyPnlPath(
|
|
|
459
469
|
|
|
460
470
|
if (g.delta !== null) {
|
|
461
471
|
allNull = false;
|
|
462
|
-
const weight = legs[j].quantity * legs[j].multiplier / 100;
|
|
472
|
+
const weight = (legs[j].quantity * legs[j].multiplier) / 100;
|
|
463
473
|
netDelta += g.delta * weight;
|
|
464
474
|
netGamma += g.gamma! * weight;
|
|
465
475
|
netTheta += g.theta! * weight;
|
|
@@ -473,9 +483,8 @@ export function computeStrategyPnlPath(
|
|
|
473
483
|
point.netTheta = allNull ? null : netTheta;
|
|
474
484
|
point.netVega = allNull ? null : netVega;
|
|
475
485
|
|
|
476
|
-
|
|
477
486
|
// IVP lookup by date
|
|
478
|
-
const ivpDate = ts.split(
|
|
487
|
+
const ivpDate = ts.split(" ")[0];
|
|
479
488
|
point.ivp = greeksConfig.ivpByDate?.get(ivpDate) ?? null;
|
|
480
489
|
}
|
|
481
490
|
}
|
|
@@ -505,7 +514,7 @@ export function computeReplayMfeMae(pnlPath: PnlPoint[]): {
|
|
|
505
514
|
maeTimestamp: string;
|
|
506
515
|
} {
|
|
507
516
|
if (pnlPath.length === 0) {
|
|
508
|
-
return { mfe: 0, mae: 0, mfeTimestamp:
|
|
517
|
+
return { mfe: 0, mae: 0, mfeTimestamp: "", maeTimestamp: "" };
|
|
509
518
|
}
|
|
510
519
|
|
|
511
520
|
let mfe = pnlPath[0].strategyPnl;
|