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.
- package/README.md +85 -80
- 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-QTTR7AAW.js → chunk-2I3S2ZLT.js} +325 -84
- package/dist/chunk-2I3S2ZLT.js.map +1 -0
- package/dist/{chunk-4BLCXNQ6.js → chunk-4HZLVUOZ.js} +179 -183
- package/dist/chunk-4HZLVUOZ.js.map +1 -0
- package/dist/{chunk-XXYOUIZY.js → chunk-NWNSKALN.js} +63 -80
- package/dist/chunk-NWNSKALN.js.map +1 -0
- package/dist/{chunk-PRAYH3RT.js → chunk-WNTYA7ER.js} +29 -12
- package/dist/chunk-WNTYA7ER.js.map +1 -0
- package/dist/{chunk-BKQ4PM4Y.js → chunk-YZA4RS76.js} +24 -18
- package/dist/chunk-YZA4RS76.js.map +1 -0
- package/dist/{chunk-W2PP3LEH.js → chunk-ZTJXC3LS.js} +17 -8
- package/dist/chunk-ZTJXC3LS.js.map +1 -0
- package/dist/daily-log-processor-MZW2XBZY.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-6U33TQUT.js +16 -0
- package/dist/{proto-3HZTCWK4.js → proto-V23GIZG2.js} +2 -2
- package/dist/{sync-PO4IPCYV.js → sync-3UBC37VW.js} +7 -7
- package/dist/test-exports.js +876 -483
- package/dist/test-exports.js.map +1 -1
- package/dist/trade-processor-DKDCWGKD.js +9 -0
- package/dist/{trades-store-LOL3FQSF.js → trades-store-R3NL25T2.js} +2 -2
- package/package.json +2 -6
- package/server/{chunk-CSDVJPBB.js → chunk-536NSFED.js} +29 -12
- package/server/chunk-536NSFED.js.map +1 -0
- package/server/{chunk-WA5AAPCH.js → chunk-A3UKD64A.js} +17 -8
- package/server/chunk-A3UKD64A.js.map +1 -0
- package/server/{chunk-PNKG7RY7.js → chunk-OMUDNJBJ.js} +63 -80
- package/server/chunk-OMUDNJBJ.js.map +1 -0
- package/server/{chunk-72GKJE2U.js → chunk-RJDJHZ5Y.js} +934 -335
- package/server/chunk-RJDJHZ5Y.js.map +1 -0
- package/server/{chunk-NZO6PT64.js → chunk-RM4B2VKS.js} +167 -177
- package/server/chunk-RM4B2VKS.js.map +1 -0
- package/server/{chunk-5EBXHT6C.js → chunk-SY6GDNVI.js} +24 -18
- package/server/chunk-SY6GDNVI.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/{config-6IZXEFEX.js → config-DK7KOMNL.js} +1 -1
- package/server/config-DK7KOMNL.js.map +1 -0
- package/server/daily-log-processor-B2VPU2FQ.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 +2497 -2029
- package/server/index.js.map +1 -1
- package/server/iv-solver-worker.js +9 -1
- package/server/market-provider-L56RFHAC.js +17 -0
- package/server/{proto-DWRZJO4E.js → proto-YZ4TLX2N.js} +2 -2
- package/server/{sync-QFI5L7S7.js → sync-CENKDN53.js} +7 -7
- package/server/trade-processor-3PI4LHZI.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 +38 -8
- package/src/db/connection.ts +107 -40
- package/src/db/data-root.ts +22 -0
- 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 +139 -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/index.ts +2 -0
- package/src/tools/blocks/paired-comparison.ts +425 -0
- 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 +11 -14
- 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 +69 -83
- package/src/utils/calibration-probe.ts +3 -4
- 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-QTTR7AAW.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/manifest.json +0 -141
- 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-MZW2XBZY.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-6U33TQUT.js.map} +0 -0
- /package/dist/{proto-3HZTCWK4.js.map → proto-V23GIZG2.js.map} +0 -0
- /package/dist/{sync-PO4IPCYV.js.map → sync-3UBC37VW.js.map} +0 -0
- /package/dist/{trade-processor-NHU4VWRX.js.map → trade-processor-DKDCWGKD.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-B2VPU2FQ.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-L56RFHAC.js.map} +0 -0
- /package/server/{proto-DWRZJO4E.js.map → proto-YZ4TLX2N.js.map} +0 -0
- /package/server/{sync-QFI5L7S7.js.map → sync-CENKDN53.js.map} +0 -0
- /package/server/{trade-processor-JWVS37KM.js.map → trade-processor-3PI4LHZI.js.map} +0 -0
- /package/server/{trades-store-VY26MWER.js.map → trades-store-RTTEYMXH.js.map} +0 -0
|
@@ -90,7 +90,7 @@ export async function detectCsvType(filePath: string): Promise<CsvType> {
|
|
|
90
90
|
const hasPl = plColumnAliases.some((alias) => headers.includes(alias));
|
|
91
91
|
// Match trade columns - require header to contain the full column pattern
|
|
92
92
|
const matchedTradeColumns = tradeOptionalColumns.filter((col) =>
|
|
93
|
-
headers.some((h) => h.includes(col))
|
|
93
|
+
headers.some((h) => h.includes(col)),
|
|
94
94
|
);
|
|
95
95
|
|
|
96
96
|
if (hasPl && matchedTradeColumns.length >= 2) {
|
|
@@ -99,7 +99,7 @@ export async function detectCsvType(filePath: string): Promise<CsvType> {
|
|
|
99
99
|
// but also have "Initial Premium" (or aliases) which tradelogs use "Premium" instead.
|
|
100
100
|
const reportingOnlyColumns = ["initial premium", "initial credit", "initial premium ($)"];
|
|
101
101
|
const hasReportingOnly = reportingOnlyColumns.some((col) =>
|
|
102
|
-
headers.some((h) => h.includes(col))
|
|
102
|
+
headers.some((h) => h.includes(col)),
|
|
103
103
|
);
|
|
104
104
|
if (hasReportingOnly) {
|
|
105
105
|
return "reportinglog";
|
|
@@ -110,7 +110,7 @@ export async function detectCsvType(filePath: string): Promise<CsvType> {
|
|
|
110
110
|
// Daily log detection:
|
|
111
111
|
// Required: "Date" (but not "Date Opened"/"Date Closed"), and value column
|
|
112
112
|
const hasSimpleDate = headers.some(
|
|
113
|
-
(h) => h === "date" || (h.includes("date") && !h.includes("opened") && !h.includes("closed"))
|
|
113
|
+
(h) => h === "date" || (h.includes("date") && !h.includes("opened") && !h.includes("closed")),
|
|
114
114
|
);
|
|
115
115
|
const valueColumnAliases = [
|
|
116
116
|
"portfolio value",
|
|
@@ -120,7 +120,7 @@ export async function detectCsvType(filePath: string): Promise<CsvType> {
|
|
|
120
120
|
"netliquidity",
|
|
121
121
|
];
|
|
122
122
|
const hasValue = valueColumnAliases.some((alias) =>
|
|
123
|
-
headers.some((h) => h.includes(alias) || alias.includes(h))
|
|
123
|
+
headers.some((h) => h.includes(alias) || alias.includes(h)),
|
|
124
124
|
);
|
|
125
125
|
|
|
126
126
|
// Dailylog: has date + value columns but lacks trade-specific columns
|
|
@@ -130,26 +130,21 @@ export async function detectCsvType(filePath: string): Promise<CsvType> {
|
|
|
130
130
|
|
|
131
131
|
// TAT (Trade Automation Toolbox) detection:
|
|
132
132
|
// Has "TradeID" AND "ProfitLoss" AND "BuyingPower"
|
|
133
|
-
const tatSignature = [
|
|
133
|
+
const tatSignature = ["tradeid", "profitloss", "buyingpower"];
|
|
134
134
|
const isTat = tatSignature.every((sig) => headers.includes(sig));
|
|
135
135
|
if (isTat) {
|
|
136
|
-
return
|
|
136
|
+
return "reportinglog";
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// Reporting log detection:
|
|
140
140
|
// Has "Actual P/L" or columns from REPORTING_TRADE_COLUMN_ALIASES
|
|
141
|
-
const reportingAliases = Object.keys(REPORTING_TRADE_COLUMN_ALIASES).map(
|
|
142
|
-
|
|
143
|
-
);
|
|
144
|
-
const hasReportingColumns = reportingAliases.some((alias) =>
|
|
145
|
-
headers.includes(alias)
|
|
146
|
-
);
|
|
147
|
-
const hasActualPl = headers.some(
|
|
148
|
-
(h) => h.includes("actual") && h.includes("p")
|
|
141
|
+
const reportingAliases = Object.keys(REPORTING_TRADE_COLUMN_ALIASES).map((k) =>
|
|
142
|
+
k.toLowerCase(),
|
|
149
143
|
);
|
|
144
|
+
const hasReportingColumns = reportingAliases.some((alias) => headers.includes(alias));
|
|
145
|
+
const hasActualPl = headers.some((h) => h.includes("actual") && h.includes("p"));
|
|
150
146
|
const hasReportedStyle =
|
|
151
|
-
headers.includes("trade id") ||
|
|
152
|
-
headers.some((h) => h.includes("reported"));
|
|
147
|
+
headers.includes("trade id") || headers.some((h) => h.includes("reported"));
|
|
153
148
|
|
|
154
149
|
if (hasActualPl || hasReportingColumns || hasReportedStyle) {
|
|
155
150
|
// Double-check it's not a regular tradelog
|
|
@@ -174,7 +169,7 @@ export async function detectCsvType(filePath: string): Promise<CsvType> {
|
|
|
174
169
|
* Returns mapping of detected CSV types to filenames.
|
|
175
170
|
*/
|
|
176
171
|
export async function discoverCsvFiles(
|
|
177
|
-
folderPath: string
|
|
172
|
+
folderPath: string,
|
|
178
173
|
): Promise<{ mappings: CsvMappings; unrecognized: string[] }> {
|
|
179
174
|
const mappings: CsvMappings = {};
|
|
180
175
|
const unrecognized: string[] = [];
|
|
@@ -252,11 +247,10 @@ export async function discoverCsvFiles(
|
|
|
252
247
|
/**
|
|
253
248
|
* Log warning when folder has CSVs but none match expected patterns
|
|
254
249
|
*/
|
|
255
|
-
export function logCsvDiscoveryWarning(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
)
|
|
259
|
-
console.error(`Warning: Folder '${folderName}' has CSV files but none match expected trade log format.`);
|
|
250
|
+
export function logCsvDiscoveryWarning(folderName: string, csvFiles: string[]): void {
|
|
251
|
+
console.error(
|
|
252
|
+
`Warning: Folder '${folderName}' has CSV files but none match expected trade log format.`,
|
|
253
|
+
);
|
|
260
254
|
console.error(` Found: ${csvFiles.join(", ")}`);
|
|
261
255
|
console.error(` Expected columns: P/L, Date Opened, Date Closed, Symbol, Strategy`);
|
|
262
256
|
}
|
|
@@ -52,7 +52,7 @@ export interface DataQuality {
|
|
|
52
52
|
/** Calendar dates where no data was found at all. */
|
|
53
53
|
missingDataDates: string[];
|
|
54
54
|
/** Overall confidence level based on data density and coverage. */
|
|
55
|
-
confidenceLevel:
|
|
55
|
+
confidenceLevel: "high" | "medium" | "low";
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
@@ -106,27 +106,23 @@ export interface CoverageResult {
|
|
|
106
106
|
* medium — everything else
|
|
107
107
|
*/
|
|
108
108
|
export function scoreDataQuality(input: DataQualityInput): DataQuality {
|
|
109
|
-
const avgBarsPerDay =
|
|
110
|
-
input.tradingDays > 0
|
|
111
|
-
? Math.round(input.totalBars / input.tradingDays)
|
|
112
|
-
: 0;
|
|
109
|
+
const avgBarsPerDay = input.tradingDays > 0 ? Math.round(input.totalBars / input.tradingDays) : 0;
|
|
113
110
|
|
|
114
111
|
// Total days = tradingDays (days with data) + daysWithNoData
|
|
115
112
|
const totalDaysInRange = input.tradingDays + input.daysWithNoData;
|
|
116
|
-
const missingPct =
|
|
117
|
-
totalDaysInRange > 0 ? input.daysWithNoData / totalDaysInRange : 1;
|
|
113
|
+
const missingPct = totalDaysInRange > 0 ? input.daysWithNoData / totalDaysInRange : 1;
|
|
118
114
|
|
|
119
|
-
let confidenceLevel:
|
|
115
|
+
let confidenceLevel: "high" | "medium" | "low";
|
|
120
116
|
|
|
121
117
|
if (input.tradingDays === 0 || avgBarsPerDay < 50) {
|
|
122
|
-
confidenceLevel =
|
|
123
|
-
} else if (missingPct > 0.
|
|
118
|
+
confidenceLevel = "low";
|
|
119
|
+
} else if (missingPct > 0.1) {
|
|
124
120
|
// More than 10% of calendar trading days missing → cap at low
|
|
125
|
-
confidenceLevel =
|
|
121
|
+
confidenceLevel = "low";
|
|
126
122
|
} else if (avgBarsPerDay >= 200 && missingPct <= 0.05) {
|
|
127
|
-
confidenceLevel =
|
|
123
|
+
confidenceLevel = "high";
|
|
128
124
|
} else {
|
|
129
|
-
confidenceLevel =
|
|
125
|
+
confidenceLevel = "medium";
|
|
130
126
|
}
|
|
131
127
|
|
|
132
128
|
return {
|
|
@@ -169,12 +165,11 @@ export async function queryCoverage(
|
|
|
169
165
|
const quoteCov = await stores.quote.getCoverage(underlying, fromDate, toDate);
|
|
170
166
|
|
|
171
167
|
// Build covered-date set.
|
|
172
|
-
const dates = enumerateCoveredDates(spotCov.earliest, spotCov.latest)
|
|
173
|
-
.concat(enumerateCoveredDates(quoteCov.earliest, quoteCov.latest));
|
|
174
|
-
const uniqueDates = [...new Set(dates)].sort();
|
|
175
|
-
const quoteDateSet = new Set(
|
|
168
|
+
const dates = enumerateCoveredDates(spotCov.earliest, spotCov.latest).concat(
|
|
176
169
|
enumerateCoveredDates(quoteCov.earliest, quoteCov.latest),
|
|
177
170
|
);
|
|
171
|
+
const uniqueDates = [...new Set(dates)].sort();
|
|
172
|
+
const quoteDateSet = new Set(enumerateCoveredDates(quoteCov.earliest, quoteCov.latest));
|
|
178
173
|
|
|
179
174
|
if (uniqueDates.length === 0) {
|
|
180
175
|
return {
|
|
@@ -230,10 +225,10 @@ function enumerateCoveredDates(from: string | null, to: string | null): string[]
|
|
|
230
225
|
// ---------------------------------------------------------------------------
|
|
231
226
|
|
|
232
227
|
/** Classify bar count into density label. */
|
|
233
|
-
function densityLabel(barCount: number):
|
|
234
|
-
if (barCount >= 200) return
|
|
235
|
-
if (barCount > 0) return
|
|
236
|
-
return
|
|
228
|
+
function densityLabel(barCount: number): "dense" | "sparse" | "none" {
|
|
229
|
+
if (barCount >= 200) return "dense";
|
|
230
|
+
if (barCount > 0) return "sparse";
|
|
231
|
+
return "none";
|
|
237
232
|
}
|
|
238
233
|
|
|
239
234
|
/**
|
|
@@ -247,10 +242,7 @@ function densityLabel(barCount: number): 'dense' | 'sparse' | 'none' {
|
|
|
247
242
|
* 2025-04-01 to 2025-06-30: no data
|
|
248
243
|
* ```
|
|
249
244
|
*/
|
|
250
|
-
export function formatCoverageReport(
|
|
251
|
-
tickerPattern: string,
|
|
252
|
-
coverage: CoverageResult,
|
|
253
|
-
): string {
|
|
245
|
+
export function formatCoverageReport(tickerPattern: string, coverage: CoverageResult): string {
|
|
254
246
|
if (coverage.dateBreakdown.length === 0) {
|
|
255
247
|
return `No data found for ${tickerPattern}`;
|
|
256
248
|
}
|
|
@@ -264,7 +256,7 @@ export function formatCoverageReport(
|
|
|
264
256
|
interface Group {
|
|
265
257
|
fromDate: string;
|
|
266
258
|
toDate: string;
|
|
267
|
-
density:
|
|
259
|
+
density: "dense" | "sparse" | "none";
|
|
268
260
|
barCounts: number[];
|
|
269
261
|
hasQuotes: boolean;
|
|
270
262
|
}
|
|
@@ -295,27 +287,23 @@ export function formatCoverageReport(
|
|
|
295
287
|
for (const group of groups) {
|
|
296
288
|
const avgBars =
|
|
297
289
|
group.barCounts.length > 0
|
|
298
|
-
? Math.round(
|
|
299
|
-
group.barCounts.reduce((s, v) => s + v, 0) / group.barCounts.length,
|
|
300
|
-
)
|
|
290
|
+
? Math.round(group.barCounts.reduce((s, v) => s + v, 0) / group.barCounts.length)
|
|
301
291
|
: 0;
|
|
302
292
|
|
|
303
293
|
const rangeStr =
|
|
304
|
-
group.fromDate === group.toDate
|
|
305
|
-
? group.fromDate
|
|
306
|
-
: `${group.fromDate} to ${group.toDate}`;
|
|
294
|
+
group.fromDate === group.toDate ? group.fromDate : `${group.fromDate} to ${group.toDate}`;
|
|
307
295
|
|
|
308
296
|
let detail: string;
|
|
309
|
-
if (group.density ===
|
|
297
|
+
if (group.density === "dense") {
|
|
310
298
|
detail = `dense (avg ${avgBars} bars/day, quotes available)`;
|
|
311
|
-
} else if (group.density ===
|
|
299
|
+
} else if (group.density === "sparse") {
|
|
312
300
|
detail = `sparse (avg ${avgBars} bars/day, trade bars only)`;
|
|
313
301
|
} else {
|
|
314
|
-
detail =
|
|
302
|
+
detail = "no data";
|
|
315
303
|
}
|
|
316
304
|
|
|
317
305
|
lines.push(` ${rangeStr}: ${detail}`);
|
|
318
306
|
}
|
|
319
307
|
|
|
320
|
-
return lines.join(
|
|
308
|
+
return lines.join("\n");
|
|
321
309
|
}
|
|
@@ -7,28 +7,28 @@
|
|
|
7
7
|
* Provides the computational heart of the `analyze_exit_triggers` tool.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { PnlPoint, ReplayLeg } from
|
|
10
|
+
import type { PnlPoint, ReplayLeg } from "./trade-replay.ts";
|
|
11
11
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
// Types
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
|
|
16
16
|
export type TriggerType =
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
17
|
+
| "profitTarget"
|
|
18
|
+
| "stopLoss"
|
|
19
|
+
| "trailingStop"
|
|
20
|
+
| "profitAction"
|
|
21
|
+
| "dteExit"
|
|
22
|
+
| "ditExit"
|
|
23
|
+
| "clockTimeExit"
|
|
24
|
+
| "underlyingPriceMove"
|
|
25
|
+
| "positionDelta"
|
|
26
|
+
| "perLegDelta"
|
|
27
|
+
| "vixMove"
|
|
28
|
+
| "vix9dMove"
|
|
29
|
+
| "vix9dVixRatio"
|
|
30
|
+
| "slRatioThreshold"
|
|
31
|
+
| "slRatioMove";
|
|
32
32
|
|
|
33
33
|
export interface PartialClose {
|
|
34
34
|
index: number;
|
|
@@ -40,49 +40,49 @@ export interface PartialClose {
|
|
|
40
40
|
export interface ExitTriggerConfig {
|
|
41
41
|
type: TriggerType;
|
|
42
42
|
threshold: number;
|
|
43
|
-
unit?:
|
|
43
|
+
unit?: "percent" | "dollar"; // D-07: default 'dollar', backwards compatible
|
|
44
44
|
steps?: Array<{ armAt: number; stopAt: number; closeAllocationPct?: number }>;
|
|
45
45
|
// Context-specific optional fields:
|
|
46
|
-
expiry?: string;
|
|
47
|
-
openDate?: string;
|
|
48
|
-
clockTime?: string;
|
|
49
|
-
trailAmount?: number;
|
|
46
|
+
expiry?: string; // YYYY-MM-DD for dteExit
|
|
47
|
+
openDate?: string; // YYYY-MM-DD for ditExit
|
|
48
|
+
clockTime?: string; // "HH:MM" for clockTimeExit (threshold ignored)
|
|
49
|
+
trailAmount?: number; // Dollar trail for trailingStop
|
|
50
50
|
// Directional delta fields (per-leg directional exits):
|
|
51
|
-
legIndex?: number;
|
|
52
|
-
exitAbove?: number;
|
|
53
|
-
exitBelow?: number;
|
|
51
|
+
legIndex?: number; // 0-based leg index for perLegDelta — targets specific leg
|
|
52
|
+
exitAbove?: number; // Fire when value > exitAbove (directional, no abs)
|
|
53
|
+
exitBelow?: number; // Fire when value < exitBelow (directional, no abs)
|
|
54
54
|
// Data maps for triggers needing external prices:
|
|
55
|
-
underlyingPrices?: Map<string, number>;
|
|
56
|
-
vixPrices?: Map<string, number>;
|
|
57
|
-
vix9dPrices?: Map<string, number>;
|
|
55
|
+
underlyingPrices?: Map<string, number>; // timestamp -> price
|
|
56
|
+
vixPrices?: Map<string, number>; // timestamp -> VIX price
|
|
57
|
+
vix9dPrices?: Map<string, number>; // timestamp -> VIX9D price
|
|
58
58
|
// S/L ratio inputs:
|
|
59
|
-
spreadWidth?: number;
|
|
60
|
-
contracts?: number;
|
|
61
|
-
multiplier?: number;
|
|
62
|
-
// profitTarget confirmation: N synchronized-quote bars at-or-above threshold required before firing (default
|
|
59
|
+
spreadWidth?: number; // Width of spread in dollars
|
|
60
|
+
contracts?: number; // Number of contracts
|
|
61
|
+
multiplier?: number; // Default 100
|
|
62
|
+
// profitTarget confirmation: N synchronized-quote bars at-or-above threshold required before firing (default 2 = require two synchronized-quote confirmations before firing; set to 1 to fire on the first cross)
|
|
63
63
|
requiredHits?: number;
|
|
64
64
|
// Internal: set by handler when unit='percent' to compute dollar threshold
|
|
65
|
-
entryCost?: number;
|
|
66
|
-
entrySLRatio?: number;
|
|
65
|
+
entryCost?: number; // D-11: cost/credit of entry (negative = credit received)
|
|
66
|
+
entrySLRatio?: number; // Runtime-hydrated opening short/long ratio for slRatioMove
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
export interface TriggerFireEvent {
|
|
70
70
|
type: TriggerType;
|
|
71
|
-
firedAt: string;
|
|
72
|
-
pnlAtFire: number;
|
|
73
|
-
index: number;
|
|
74
|
-
detail?: string;
|
|
71
|
+
firedAt: string; // Timestamp when trigger fired
|
|
72
|
+
pnlAtFire: number; // Strategy P&L when trigger fired
|
|
73
|
+
index: number; // Index into pnlPath
|
|
74
|
+
detail?: string; // Human-readable description
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export interface ExitTriggerResult {
|
|
78
|
-
triggers: TriggerFireEvent[];
|
|
78
|
+
triggers: TriggerFireEvent[]; // All triggers that fired (sorted by fire time)
|
|
79
79
|
firstToFire: TriggerFireEvent | null; // Earliest trigger
|
|
80
80
|
actualExit?: {
|
|
81
81
|
timestamp: string;
|
|
82
82
|
pnl: number;
|
|
83
|
-
pnlDifference: number;
|
|
83
|
+
pnlDifference: number; // firstToFire.pnl - actualExit.pnl
|
|
84
84
|
};
|
|
85
|
-
partialCloses?: PartialClose[];
|
|
85
|
+
partialCloses?: PartialClose[]; // Partial position closes from profitAction steps
|
|
86
86
|
summary: string;
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -104,7 +104,7 @@ export interface LegGroupResult {
|
|
|
104
104
|
|
|
105
105
|
/** Parse "YYYY-MM-DD" to a Date at local midnight. */
|
|
106
106
|
function parseDate(dateStr: string): Date {
|
|
107
|
-
const [y, m, d] = dateStr.split(
|
|
107
|
+
const [y, m, d] = dateStr.split("-").map(Number);
|
|
108
108
|
return new Date(y, m - 1, d);
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -181,13 +181,11 @@ export function evaluateProfitAction(
|
|
|
181
181
|
if (pnlPath.length === 0 || !trigger.steps?.length) {
|
|
182
182
|
return { fireEvent: null, partialCloses };
|
|
183
183
|
}
|
|
184
|
-
if (trigger.unit ===
|
|
184
|
+
if (trigger.unit === "percent" && trigger.entryCost == null) {
|
|
185
185
|
return { fireEvent: null, partialCloses };
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
const scale = trigger.unit ===
|
|
189
|
-
? Math.abs(trigger.entryCost!)
|
|
190
|
-
: 1;
|
|
188
|
+
const scale = trigger.unit === "percent" ? Math.abs(trigger.entryCost!) : 1;
|
|
191
189
|
|
|
192
190
|
const normalizedSteps = [...trigger.steps]
|
|
193
191
|
.sort((a, b) => a.armAt - b.armAt)
|
|
@@ -218,7 +216,7 @@ export function evaluateProfitAction(
|
|
|
218
216
|
index: i,
|
|
219
217
|
pnlAtFire: pnl * remainingAllocation * step.closeAllocationPct,
|
|
220
218
|
allocation: closeAmt,
|
|
221
|
-
trigger:
|
|
219
|
+
trigger: "profitAction",
|
|
222
220
|
});
|
|
223
221
|
remainingAllocation -= closeAmt;
|
|
224
222
|
}
|
|
@@ -237,13 +235,14 @@ export function evaluateProfitAction(
|
|
|
237
235
|
// Simplifies to: pnl <= activeFloor (when remainingAllocation > 0)
|
|
238
236
|
if (activeFloor > -Infinity && remainingAllocation > 0 && pnl <= activeFloor) {
|
|
239
237
|
const effectivePnl = pnl * remainingAllocation;
|
|
240
|
-
const detail =
|
|
241
|
-
|
|
242
|
-
|
|
238
|
+
const detail =
|
|
239
|
+
trigger.unit === "percent"
|
|
240
|
+
? `Profit action: stop adjusted to ${((activeFloor / scale) * 100).toFixed(0)}% ($${activeFloor.toFixed(2)}) at max P&L $${runningMaxPnl.toFixed(2)}, hit at $${pnl.toFixed(2)} (remaining ${(remainingAllocation * 100).toFixed(0)}%)`
|
|
241
|
+
: `Profit action: stop adjusted to $${activeFloor.toFixed(2)} at max P&L $${runningMaxPnl.toFixed(2)}, hit at $${pnl.toFixed(2)} (remaining ${(remainingAllocation * 100).toFixed(0)}%)`;
|
|
243
242
|
|
|
244
243
|
return {
|
|
245
244
|
fireEvent: {
|
|
246
|
-
type:
|
|
245
|
+
type: "profitAction",
|
|
247
246
|
firedAt: point.timestamp,
|
|
248
247
|
pnlAtFire: effectivePnl,
|
|
249
248
|
index: i,
|
|
@@ -293,44 +292,44 @@ export function evaluateTrigger(
|
|
|
293
292
|
let detail: string | undefined;
|
|
294
293
|
|
|
295
294
|
switch (type) {
|
|
296
|
-
case
|
|
295
|
+
case "profitTarget": {
|
|
297
296
|
// unit='percent' requires entryCost; if missing, cannot compute — no fire
|
|
298
|
-
if (trigger.unit ===
|
|
299
|
-
const requiredHits = trigger.requiredHits ??
|
|
300
|
-
const dollarThresholdPT =
|
|
301
|
-
? threshold * Math.abs(trigger.entryCost!)
|
|
302
|
-
: threshold;
|
|
297
|
+
if (trigger.unit === "percent" && trigger.entryCost == null) break;
|
|
298
|
+
const requiredHits = trigger.requiredHits ?? 2;
|
|
299
|
+
const dollarThresholdPT =
|
|
300
|
+
trigger.unit === "percent" ? threshold * Math.abs(trigger.entryCost!) : threshold;
|
|
303
301
|
if (pnl >= dollarThresholdPT) {
|
|
304
302
|
if (point.allLegsSync !== false) profitTargetHits++;
|
|
305
303
|
if (profitTargetHits < requiredHits) break;
|
|
306
304
|
fired = true;
|
|
307
|
-
detail =
|
|
308
|
-
|
|
309
|
-
|
|
305
|
+
detail =
|
|
306
|
+
trigger.unit === "percent"
|
|
307
|
+
? `P&L $${pnl.toFixed(2)} >= ${(threshold * 100).toFixed(0)}% of $${Math.abs(trigger.entryCost!).toFixed(2)} ($${dollarThresholdPT.toFixed(2)})`
|
|
308
|
+
: `P&L $${pnl.toFixed(2)} >= target $${dollarThresholdPT.toFixed(2)}`;
|
|
310
309
|
} else if (point.allLegsSync !== false) {
|
|
311
310
|
profitTargetHits = 0;
|
|
312
311
|
}
|
|
313
312
|
break;
|
|
314
313
|
}
|
|
315
314
|
|
|
316
|
-
case
|
|
315
|
+
case "stopLoss": {
|
|
317
316
|
// Normalize negative threshold — users may pass -2 meaning "stop at $2 loss"
|
|
318
317
|
const absThreshold = Math.abs(threshold);
|
|
319
318
|
// unit='percent' requires entryCost; if missing, cannot compute — no fire
|
|
320
|
-
if (trigger.unit ===
|
|
321
|
-
const dollarThresholdSL =
|
|
322
|
-
? absThreshold * Math.abs(trigger.entryCost!)
|
|
323
|
-
: absThreshold;
|
|
319
|
+
if (trigger.unit === "percent" && trigger.entryCost == null) break;
|
|
320
|
+
const dollarThresholdSL =
|
|
321
|
+
trigger.unit === "percent" ? absThreshold * Math.abs(trigger.entryCost!) : absThreshold;
|
|
324
322
|
if (pnl <= -dollarThresholdSL) {
|
|
325
323
|
fired = true;
|
|
326
|
-
detail =
|
|
327
|
-
|
|
328
|
-
|
|
324
|
+
detail =
|
|
325
|
+
trigger.unit === "percent"
|
|
326
|
+
? `P&L $${pnl.toFixed(2)} <= -${(absThreshold * 100).toFixed(0)}% of $${Math.abs(trigger.entryCost!).toFixed(2)} (-$${dollarThresholdSL.toFixed(2)})`
|
|
327
|
+
: `P&L $${pnl.toFixed(2)} <= stop -$${dollarThresholdSL.toFixed(2)}`;
|
|
329
328
|
}
|
|
330
329
|
break;
|
|
331
330
|
}
|
|
332
331
|
|
|
333
|
-
case
|
|
332
|
+
case "trailingStop": {
|
|
334
333
|
const trailAmt = trigger.trailAmount ?? threshold;
|
|
335
334
|
const dropdown = runningMaxPnl - pnl;
|
|
336
335
|
if (dropdown >= trailAmt && runningMaxPnl > -Infinity) {
|
|
@@ -340,7 +339,7 @@ export function evaluateTrigger(
|
|
|
340
339
|
break;
|
|
341
340
|
}
|
|
342
341
|
|
|
343
|
-
case
|
|
342
|
+
case "profitAction": {
|
|
344
343
|
// Delegate to evaluateProfitAction for the full path evaluation
|
|
345
344
|
// (evaluateTrigger is called point-by-point in the loop, but profitAction
|
|
346
345
|
// needs full-path context for partial close tracking, so we handle it
|
|
@@ -349,7 +348,7 @@ export function evaluateTrigger(
|
|
|
349
348
|
return paResult.fireEvent;
|
|
350
349
|
}
|
|
351
350
|
|
|
352
|
-
case
|
|
351
|
+
case "dteExit": {
|
|
353
352
|
if (!trigger.expiry) break;
|
|
354
353
|
const pointDate = parseDate(extractDate(point.timestamp));
|
|
355
354
|
const expiryDate = parseDate(trigger.expiry);
|
|
@@ -362,7 +361,7 @@ export function evaluateTrigger(
|
|
|
362
361
|
break;
|
|
363
362
|
}
|
|
364
363
|
|
|
365
|
-
case
|
|
364
|
+
case "ditExit": {
|
|
366
365
|
if (!trigger.openDate) break;
|
|
367
366
|
const pointDate = parseDate(extractDate(point.timestamp));
|
|
368
367
|
const openDate = parseDate(trigger.openDate);
|
|
@@ -374,8 +373,8 @@ export function evaluateTrigger(
|
|
|
374
373
|
break;
|
|
375
374
|
}
|
|
376
375
|
|
|
377
|
-
case
|
|
378
|
-
const clockTime = trigger.clockTime ??
|
|
376
|
+
case "clockTimeExit": {
|
|
377
|
+
const clockTime = trigger.clockTime ?? "15:00";
|
|
379
378
|
const pointTime = extractTime(point.timestamp);
|
|
380
379
|
if (pointTime >= clockTime) {
|
|
381
380
|
fired = true;
|
|
@@ -384,7 +383,7 @@ export function evaluateTrigger(
|
|
|
384
383
|
break;
|
|
385
384
|
}
|
|
386
385
|
|
|
387
|
-
case
|
|
386
|
+
case "underlyingPriceMove": {
|
|
388
387
|
if (!trigger.underlyingPrices) break;
|
|
389
388
|
const price = trigger.underlyingPrices.get(point.timestamp);
|
|
390
389
|
if (price == null) break;
|
|
@@ -400,7 +399,7 @@ export function evaluateTrigger(
|
|
|
400
399
|
break;
|
|
401
400
|
}
|
|
402
401
|
|
|
403
|
-
case
|
|
402
|
+
case "positionDelta": {
|
|
404
403
|
const netDelta = point.netDelta ?? 0;
|
|
405
404
|
if (trigger.exitAbove != null) {
|
|
406
405
|
if (netDelta > trigger.exitAbove) {
|
|
@@ -419,7 +418,7 @@ export function evaluateTrigger(
|
|
|
419
418
|
break;
|
|
420
419
|
}
|
|
421
420
|
|
|
422
|
-
case
|
|
421
|
+
case "perLegDelta": {
|
|
423
422
|
if (!point.legGreeks) break;
|
|
424
423
|
if (trigger.legIndex != null) {
|
|
425
424
|
// Target a specific leg
|
|
@@ -458,7 +457,7 @@ export function evaluateTrigger(
|
|
|
458
457
|
break;
|
|
459
458
|
}
|
|
460
459
|
|
|
461
|
-
case
|
|
460
|
+
case "vixMove": {
|
|
462
461
|
if (!trigger.vixPrices) break;
|
|
463
462
|
const vix = trigger.vixPrices.get(point.timestamp);
|
|
464
463
|
if (vix == null) break;
|
|
@@ -474,7 +473,7 @@ export function evaluateTrigger(
|
|
|
474
473
|
break;
|
|
475
474
|
}
|
|
476
475
|
|
|
477
|
-
case
|
|
476
|
+
case "vix9dMove": {
|
|
478
477
|
if (!trigger.vix9dPrices) break;
|
|
479
478
|
const vix9d = trigger.vix9dPrices.get(point.timestamp);
|
|
480
479
|
if (vix9d == null) break;
|
|
@@ -490,7 +489,7 @@ export function evaluateTrigger(
|
|
|
490
489
|
break;
|
|
491
490
|
}
|
|
492
491
|
|
|
493
|
-
case
|
|
492
|
+
case "vix9dVixRatio": {
|
|
494
493
|
if (!trigger.vixPrices || !trigger.vix9dPrices) break;
|
|
495
494
|
const vix = trigger.vixPrices.get(point.timestamp);
|
|
496
495
|
const vix9d = trigger.vix9dPrices.get(point.timestamp);
|
|
@@ -506,7 +505,7 @@ export function evaluateTrigger(
|
|
|
506
505
|
break;
|
|
507
506
|
}
|
|
508
507
|
|
|
509
|
-
case
|
|
508
|
+
case "slRatioThreshold": {
|
|
510
509
|
const sw = trigger.spreadWidth ?? 0;
|
|
511
510
|
const ct = trigger.contracts ?? 1;
|
|
512
511
|
const mp = trigger.multiplier ?? 100;
|
|
@@ -519,13 +518,13 @@ export function evaluateTrigger(
|
|
|
519
518
|
break;
|
|
520
519
|
}
|
|
521
520
|
|
|
522
|
-
case
|
|
521
|
+
case "slRatioMove": {
|
|
523
522
|
const sw = trigger.spreadWidth ?? 0;
|
|
524
523
|
const ct = trigger.contracts ?? 1;
|
|
525
524
|
const mp = trigger.multiplier ?? 100;
|
|
526
525
|
if (sw === 0) break;
|
|
527
526
|
const slRatio = computeSLRatio(point, legs, sw, ct, mp);
|
|
528
|
-
if (initialSLRatio === null && typeof trigger.entrySLRatio ===
|
|
527
|
+
if (initialSLRatio === null && typeof trigger.entrySLRatio === "number") {
|
|
529
528
|
initialSLRatio = trigger.entrySLRatio;
|
|
530
529
|
}
|
|
531
530
|
if (initialSLRatio === null) {
|
|
@@ -563,11 +562,7 @@ export function evaluateTrigger(
|
|
|
563
562
|
* Compute per-group P&L at each timestamp from leg prices.
|
|
564
563
|
* groupPnl[t] = sum over legIndices of (legPrices[i] - entryPrice[i]) * quantity[i] * multiplier[i]
|
|
565
564
|
*/
|
|
566
|
-
function computeGroupPnl(
|
|
567
|
-
pnlPath: PnlPoint[],
|
|
568
|
-
legs: ReplayLeg[],
|
|
569
|
-
legIndices: number[],
|
|
570
|
-
): number[] {
|
|
565
|
+
function computeGroupPnl(pnlPath: PnlPoint[], legs: ReplayLeg[], legIndices: number[]): number[] {
|
|
571
566
|
return pnlPath.map((point) => {
|
|
572
567
|
let groupPnl = 0;
|
|
573
568
|
for (const idx of legIndices) {
|
|
@@ -605,7 +600,7 @@ export function analyzeExitTriggers(config: {
|
|
|
605
600
|
const fireEvents: TriggerFireEvent[] = [];
|
|
606
601
|
let allPartialCloses: PartialClose[] = [];
|
|
607
602
|
for (const trigger of triggers) {
|
|
608
|
-
if (trigger.type ===
|
|
603
|
+
if (trigger.type === "profitAction") {
|
|
609
604
|
// Use the partial-close-aware helper for profitAction
|
|
610
605
|
const paResult = evaluateProfitAction(trigger, pnlPath, legs);
|
|
611
606
|
if (paResult.fireEvent) {
|
|
@@ -628,7 +623,7 @@ export function analyzeExitTriggers(config: {
|
|
|
628
623
|
const firstToFire = fireEvents.length > 0 ? fireEvents[0] : null;
|
|
629
624
|
|
|
630
625
|
// Actual exit comparison
|
|
631
|
-
let actualExit: ExitTriggerResult[
|
|
626
|
+
let actualExit: ExitTriggerResult["actualExit"];
|
|
632
627
|
if (actualExitTimestamp && firstToFire) {
|
|
633
628
|
// Find closest point to actualExitTimestamp
|
|
634
629
|
let closestIdx = 0;
|
|
@@ -662,12 +657,14 @@ export function analyzeExitTriggers(config: {
|
|
|
662
657
|
if (!firstToFire) {
|
|
663
658
|
summary = `No triggers fired across ${pnlPath.length} data points.`;
|
|
664
659
|
} else if (actualExit) {
|
|
665
|
-
const betterWorse = actualExit.pnlDifference > 0 ?
|
|
666
|
-
summary =
|
|
660
|
+
const betterWorse = actualExit.pnlDifference > 0 ? "better" : "worse";
|
|
661
|
+
summary =
|
|
662
|
+
`${firstToFire.type} fired at ${firstToFire.firedAt} (P&L $${firstToFire.pnlAtFire.toFixed(2)}). ` +
|
|
667
663
|
`Actual exit at ${actualExit.timestamp} (P&L $${actualExit.pnl.toFixed(2)}). ` +
|
|
668
664
|
`Trigger was $${Math.abs(actualExit.pnlDifference).toFixed(2)} ${betterWorse}.`;
|
|
669
665
|
} else {
|
|
670
|
-
summary =
|
|
666
|
+
summary =
|
|
667
|
+
`${firstToFire.type} fired first at ${firstToFire.firedAt} (P&L $${firstToFire.pnlAtFire.toFixed(2)}). ` +
|
|
671
668
|
`${fireEvents.length} trigger(s) fired total.`;
|
|
672
669
|
}
|
|
673
670
|
|
|
@@ -691,9 +688,7 @@ export function analyzeExitTriggers(config: {
|
|
|
691
688
|
strategyPnl: groupPnlArr[idx],
|
|
692
689
|
// Filter legPrices/legGreeks to only this group's legs
|
|
693
690
|
legPrices: group.legIndices.map((li) => point.legPrices[li] ?? 0),
|
|
694
|
-
legGreeks: point.legGreeks
|
|
695
|
-
? group.legIndices.map((li) => point.legGreeks![li])
|
|
696
|
-
: undefined,
|
|
691
|
+
legGreeks: point.legGreeks ? group.legIndices.map((li) => point.legGreeks![li]) : undefined,
|
|
697
692
|
}));
|
|
698
693
|
|
|
699
694
|
// Build group legs subset
|
|
@@ -710,7 +705,7 @@ export function analyzeExitTriggers(config: {
|
|
|
710
705
|
const groupFirstToFire = groupFireEvents.length > 0 ? groupFireEvents[0] : null;
|
|
711
706
|
|
|
712
707
|
// Actual exit for group
|
|
713
|
-
let groupActualExit: ExitTriggerResult[
|
|
708
|
+
let groupActualExit: ExitTriggerResult["actualExit"];
|
|
714
709
|
if (actualExitTimestamp && groupFirstToFire) {
|
|
715
710
|
let closestIdx = pnlPath.length - 1;
|
|
716
711
|
for (let i = 0; i < pnlPath.length; i++) {
|