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
package/src/tools/performance.ts
CHANGED
|
@@ -7,11 +7,7 @@
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
9
|
import { loadBlock, loadReportingLog } from "../utils/block-loader.ts";
|
|
10
|
-
import {
|
|
11
|
-
createToolOutput,
|
|
12
|
-
formatPercent,
|
|
13
|
-
formatCurrency,
|
|
14
|
-
} from "../utils/output-formatter.ts";
|
|
10
|
+
import { createToolOutput, formatPercent, formatCurrency } from "../utils/output-formatter.ts";
|
|
15
11
|
import type { Trade, ReportingTrade } from "@tradeblocks/lib";
|
|
16
12
|
import {
|
|
17
13
|
normalizeToOneLot,
|
|
@@ -85,10 +81,7 @@ function computeTotalPremium(trade: Trade): number {
|
|
|
85
81
|
/**
|
|
86
82
|
* Calculate MFE/MAE metrics for a single trade
|
|
87
83
|
*/
|
|
88
|
-
function calculateTradeExcursionMetrics(
|
|
89
|
-
trade: Trade,
|
|
90
|
-
tradeNumber: number
|
|
91
|
-
): MFEMAEDataPoint | null {
|
|
84
|
+
function calculateTradeExcursionMetrics(trade: Trade, tradeNumber: number): MFEMAEDataPoint | null {
|
|
92
85
|
const totalMFE = computeTotalMaxProfit(trade);
|
|
93
86
|
const totalMAE = computeTotalMaxLoss(trade);
|
|
94
87
|
|
|
@@ -100,9 +93,7 @@ function calculateTradeExcursionMetrics(
|
|
|
100
93
|
// Determine denominator for percentage calculations
|
|
101
94
|
const totalPremium = computeTotalPremium(trade);
|
|
102
95
|
const margin =
|
|
103
|
-
typeof trade.marginReq === "number" &&
|
|
104
|
-
isFinite(trade.marginReq) &&
|
|
105
|
-
trade.marginReq !== 0
|
|
96
|
+
typeof trade.marginReq === "number" && isFinite(trade.marginReq) && trade.marginReq !== 0
|
|
106
97
|
? Math.abs(trade.marginReq)
|
|
107
98
|
: undefined;
|
|
108
99
|
|
|
@@ -175,14 +166,10 @@ function calculateMFEMAEData(trades: Trade[]): MFEMAEDataPoint[] {
|
|
|
175
166
|
*/
|
|
176
167
|
function createExcursionDistribution(
|
|
177
168
|
dataPoints: MFEMAEDataPoint[],
|
|
178
|
-
bucketSize: number = 10
|
|
169
|
+
bucketSize: number = 10,
|
|
179
170
|
): MFEMAEDistributionBucket[] {
|
|
180
|
-
const mfeValues = dataPoints
|
|
181
|
-
|
|
182
|
-
.map((d) => d.mfePercent!);
|
|
183
|
-
const maeValues = dataPoints
|
|
184
|
-
.filter((d) => d.maePercent !== undefined)
|
|
185
|
-
.map((d) => d.maePercent!);
|
|
171
|
+
const mfeValues = dataPoints.filter((d) => d.mfePercent !== undefined).map((d) => d.mfePercent!);
|
|
172
|
+
const maeValues = dataPoints.filter((d) => d.maePercent !== undefined).map((d) => d.maePercent!);
|
|
186
173
|
|
|
187
174
|
if (mfeValues.length === 0 && maeValues.length === 0) {
|
|
188
175
|
return [];
|
|
@@ -221,18 +208,14 @@ function createExcursionDistribution(
|
|
|
221
208
|
*/
|
|
222
209
|
function filterByStrategy(trades: Trade[], strategy?: string): Trade[] {
|
|
223
210
|
if (!strategy) return trades;
|
|
224
|
-
return trades.filter(
|
|
225
|
-
(t) => t.strategy.toLowerCase() === strategy.toLowerCase()
|
|
226
|
-
);
|
|
211
|
+
return trades.filter((t) => t.strategy.toLowerCase() === strategy.toLowerCase());
|
|
227
212
|
}
|
|
228
213
|
|
|
229
214
|
/**
|
|
230
215
|
* Get the ISO week number
|
|
231
216
|
*/
|
|
232
217
|
function getISOWeekNumber(date: Date): number {
|
|
233
|
-
const d = new Date(
|
|
234
|
-
Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
|
|
235
|
-
);
|
|
218
|
+
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
236
219
|
const dayNum = d.getUTCDay() || 7;
|
|
237
220
|
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
238
221
|
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
@@ -253,8 +236,7 @@ function buildEquityCurve(trades: Trade[]): Array<{
|
|
|
253
236
|
}
|
|
254
237
|
|
|
255
238
|
const sortedTrades = [...trades].sort(
|
|
256
|
-
(a, b) =>
|
|
257
|
-
new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime()
|
|
239
|
+
(a, b) => new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime(),
|
|
258
240
|
);
|
|
259
241
|
|
|
260
242
|
// Calculate initial capital from first trade
|
|
@@ -300,7 +282,7 @@ function buildEquityCurve(trades: Trade[]): Array<{
|
|
|
300
282
|
* Calculate drawdown series from equity curve
|
|
301
283
|
*/
|
|
302
284
|
function buildDrawdownSeries(
|
|
303
|
-
equityCurve: Array<{ date: string; equity: number; highWaterMark: number }
|
|
285
|
+
equityCurve: Array<{ date: string; equity: number; highWaterMark: number }>,
|
|
304
286
|
): Array<{ date: string; drawdownPct: number }> {
|
|
305
287
|
return equityCurve.map((point) => ({
|
|
306
288
|
date: point.date,
|
|
@@ -314,9 +296,7 @@ function buildDrawdownSeries(
|
|
|
314
296
|
/**
|
|
315
297
|
* Calculate monthly returns matrix
|
|
316
298
|
*/
|
|
317
|
-
function buildMonthlyReturns(
|
|
318
|
-
trades: Trade[]
|
|
319
|
-
): Record<number, Record<number, number>> {
|
|
299
|
+
function buildMonthlyReturns(trades: Trade[]): Record<number, Record<number, number>> {
|
|
320
300
|
const monthlyData: Record<string, number> = {};
|
|
321
301
|
|
|
322
302
|
trades.forEach((trade) => {
|
|
@@ -352,7 +332,7 @@ function buildMonthlyReturns(
|
|
|
352
332
|
*/
|
|
353
333
|
function buildReturnDistribution(
|
|
354
334
|
trades: Trade[],
|
|
355
|
-
bucketCount: number = 20
|
|
335
|
+
bucketCount: number = 20,
|
|
356
336
|
): Array<{ rangeStart: number; rangeEnd: number; count: number }> {
|
|
357
337
|
if (trades.length === 0) return [];
|
|
358
338
|
|
|
@@ -362,8 +342,7 @@ function buildReturnDistribution(
|
|
|
362
342
|
const range = maxReturn - minReturn || 1;
|
|
363
343
|
const bucketSize = range / bucketCount;
|
|
364
344
|
|
|
365
|
-
const buckets: Array<{ rangeStart: number; rangeEnd: number; count: number }> =
|
|
366
|
-
[];
|
|
345
|
+
const buckets: Array<{ rangeStart: number; rangeEnd: number; count: number }> = [];
|
|
367
346
|
|
|
368
347
|
for (let i = 0; i < bucketCount; i++) {
|
|
369
348
|
const rangeStart = minReturn + i * bucketSize;
|
|
@@ -383,24 +362,14 @@ function buildReturnDistribution(
|
|
|
383
362
|
/**
|
|
384
363
|
* Calculate day of week average P/L
|
|
385
364
|
*/
|
|
386
|
-
function buildDayOfWeekData(
|
|
387
|
-
trades: Trade[]
|
|
388
|
-
): Array<{
|
|
365
|
+
function buildDayOfWeekData(trades: Trade[]): Array<{
|
|
389
366
|
day: string;
|
|
390
367
|
count: number;
|
|
391
368
|
avgPl: number;
|
|
392
369
|
totalPl: number;
|
|
393
370
|
avgPlPercent: number;
|
|
394
371
|
}> {
|
|
395
|
-
const dayNames = [
|
|
396
|
-
"Monday",
|
|
397
|
-
"Tuesday",
|
|
398
|
-
"Wednesday",
|
|
399
|
-
"Thursday",
|
|
400
|
-
"Friday",
|
|
401
|
-
"Saturday",
|
|
402
|
-
"Sunday",
|
|
403
|
-
];
|
|
372
|
+
const dayNames = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
404
373
|
const dayData: Record<
|
|
405
374
|
string,
|
|
406
375
|
{ count: number; totalPl: number; totalPlPercent: number; percentCount: number }
|
|
@@ -428,13 +397,10 @@ function buildDayOfWeekData(
|
|
|
428
397
|
return dayNames.map((day) => ({
|
|
429
398
|
day,
|
|
430
399
|
count: dayData[day]?.count || 0,
|
|
431
|
-
avgPl:
|
|
432
|
-
dayData[day]?.count > 0 ? dayData[day].totalPl / dayData[day].count : 0,
|
|
400
|
+
avgPl: dayData[day]?.count > 0 ? dayData[day].totalPl / dayData[day].count : 0,
|
|
433
401
|
totalPl: dayData[day]?.totalPl || 0,
|
|
434
402
|
avgPlPercent:
|
|
435
|
-
dayData[day]?.percentCount > 0
|
|
436
|
-
? dayData[day].totalPlPercent / dayData[day].percentCount
|
|
437
|
-
: 0,
|
|
403
|
+
dayData[day]?.percentCount > 0 ? dayData[day].totalPlPercent / dayData[day].percentCount : 0,
|
|
438
404
|
}));
|
|
439
405
|
}
|
|
440
406
|
|
|
@@ -460,8 +426,7 @@ function buildStreakData(trades: Trade[]): {
|
|
|
460
426
|
} | null;
|
|
461
427
|
} {
|
|
462
428
|
const sortedTrades = [...trades].sort(
|
|
463
|
-
(a, b) =>
|
|
464
|
-
new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime()
|
|
429
|
+
(a, b) => new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime(),
|
|
465
430
|
);
|
|
466
431
|
|
|
467
432
|
const winStreaks: number[] = [];
|
|
@@ -517,13 +482,9 @@ function buildStreakData(trades: Trade[]): {
|
|
|
517
482
|
maxWinStreak: Math.max(...winStreaks, 0),
|
|
518
483
|
maxLossStreak: Math.max(...lossStreaks, 0),
|
|
519
484
|
avgWinStreak:
|
|
520
|
-
winStreaks.length > 0
|
|
521
|
-
? winStreaks.reduce((a, b) => a + b) / winStreaks.length
|
|
522
|
-
: 0,
|
|
485
|
+
winStreaks.length > 0 ? winStreaks.reduce((a, b) => a + b) / winStreaks.length : 0,
|
|
523
486
|
avgLossStreak:
|
|
524
|
-
lossStreaks.length > 0
|
|
525
|
-
? lossStreaks.reduce((a, b) => a + b) / lossStreaks.length
|
|
526
|
-
: 0,
|
|
487
|
+
lossStreaks.length > 0 ? lossStreaks.reduce((a, b) => a + b) / lossStreaks.length : 0,
|
|
527
488
|
},
|
|
528
489
|
runsTest,
|
|
529
490
|
};
|
|
@@ -558,8 +519,7 @@ function calculateRunsTest(trades: Trade[]): {
|
|
|
558
519
|
|
|
559
520
|
const n = n1 + n0;
|
|
560
521
|
const expectedRuns = (2 * n1 * n0) / n + 1;
|
|
561
|
-
const variance =
|
|
562
|
-
(2 * n1 * n0 * (2 * n1 * n0 - n)) / (n * n * (n - 1));
|
|
522
|
+
const variance = (2 * n1 * n0 * (2 * n1 * n0 - n)) / (n * n * (n - 1));
|
|
563
523
|
const stdDev = Math.sqrt(variance);
|
|
564
524
|
|
|
565
525
|
const zScore = stdDev > 0 ? (numRuns - expectedRuns) / stdDev : 0;
|
|
@@ -603,8 +563,7 @@ function normalCDF(x: number): number {
|
|
|
603
563
|
x = Math.abs(x) / Math.sqrt(2);
|
|
604
564
|
|
|
605
565
|
const t = 1.0 / (1.0 + p * x);
|
|
606
|
-
const y =
|
|
607
|
-
1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
|
|
566
|
+
const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
|
|
608
567
|
|
|
609
568
|
return 0.5 * (1.0 + sign * y);
|
|
610
569
|
}
|
|
@@ -612,9 +571,7 @@ function normalCDF(x: number): number {
|
|
|
612
571
|
/**
|
|
613
572
|
* Build trade sequence data (P&L by trade number with ROM)
|
|
614
573
|
*/
|
|
615
|
-
function buildTradeSequence(
|
|
616
|
-
trades: Trade[]
|
|
617
|
-
): Array<{
|
|
574
|
+
function buildTradeSequence(trades: Trade[]): Array<{
|
|
618
575
|
tradeNumber: number;
|
|
619
576
|
pl: number;
|
|
620
577
|
rom: number | null;
|
|
@@ -624,9 +581,7 @@ function buildTradeSequence(
|
|
|
624
581
|
}> {
|
|
625
582
|
return trades.map((trade, index) => {
|
|
626
583
|
const marginReq =
|
|
627
|
-
typeof trade.marginReq === "number" && isFinite(trade.marginReq)
|
|
628
|
-
? trade.marginReq
|
|
629
|
-
: null;
|
|
584
|
+
typeof trade.marginReq === "number" && isFinite(trade.marginReq) ? trade.marginReq : null;
|
|
630
585
|
return {
|
|
631
586
|
tradeNumber: index + 1,
|
|
632
587
|
pl: trade.pl,
|
|
@@ -642,7 +597,7 @@ function buildTradeSequence(
|
|
|
642
597
|
* Build ROM timeline (Return on Margin over time)
|
|
643
598
|
*/
|
|
644
599
|
function buildRomTimeline(
|
|
645
|
-
trades: Trade[]
|
|
600
|
+
trades: Trade[],
|
|
646
601
|
): Array<{ date: string; rom: number; tradeNumber: number }> {
|
|
647
602
|
return trades
|
|
648
603
|
.map((trade, index) => {
|
|
@@ -663,7 +618,7 @@ function buildRomTimeline(
|
|
|
663
618
|
*/
|
|
664
619
|
function buildRollingMetrics(
|
|
665
620
|
trades: Trade[],
|
|
666
|
-
windowSize: number = 30
|
|
621
|
+
windowSize: number = 30,
|
|
667
622
|
): Array<{
|
|
668
623
|
date: string;
|
|
669
624
|
tradeNumber: number;
|
|
@@ -769,9 +724,7 @@ function buildRollingMetrics(
|
|
|
769
724
|
/**
|
|
770
725
|
* Build exit reason breakdown
|
|
771
726
|
*/
|
|
772
|
-
function buildExitReasonBreakdown(
|
|
773
|
-
trades: Trade[]
|
|
774
|
-
): Array<{
|
|
727
|
+
function buildExitReasonBreakdown(trades: Trade[]): Array<{
|
|
775
728
|
reason: string;
|
|
776
729
|
count: number;
|
|
777
730
|
avgPl: number;
|
|
@@ -785,9 +738,7 @@ function buildExitReasonBreakdown(
|
|
|
785
738
|
|
|
786
739
|
trades.forEach((trade) => {
|
|
787
740
|
const reason =
|
|
788
|
-
trade.reasonForClose && trade.reasonForClose.trim()
|
|
789
|
-
? trade.reasonForClose.trim()
|
|
790
|
-
: "Unknown";
|
|
741
|
+
trade.reasonForClose && trade.reasonForClose.trim() ? trade.reasonForClose.trim() : "Unknown";
|
|
791
742
|
const current = summaryMap.get(reason) || {
|
|
792
743
|
count: 0,
|
|
793
744
|
totalPl: 0,
|
|
@@ -819,9 +770,7 @@ function buildExitReasonBreakdown(
|
|
|
819
770
|
/**
|
|
820
771
|
* Build holding periods data
|
|
821
772
|
*/
|
|
822
|
-
function buildHoldingPeriods(
|
|
823
|
-
trades: Trade[]
|
|
824
|
-
): Array<{
|
|
773
|
+
function buildHoldingPeriods(trades: Trade[]): Array<{
|
|
825
774
|
tradeNumber: number;
|
|
826
775
|
dateOpened: string;
|
|
827
776
|
dateClosed: string | null;
|
|
@@ -854,9 +803,7 @@ function buildHoldingPeriods(
|
|
|
854
803
|
/**
|
|
855
804
|
* Build premium efficiency data
|
|
856
805
|
*/
|
|
857
|
-
function buildPremiumEfficiency(
|
|
858
|
-
trades: Trade[]
|
|
859
|
-
): Array<{
|
|
806
|
+
function buildPremiumEfficiency(trades: Trade[]): Array<{
|
|
860
807
|
tradeNumber: number;
|
|
861
808
|
date: string;
|
|
862
809
|
pl: number;
|
|
@@ -866,9 +813,7 @@ function buildPremiumEfficiency(
|
|
|
866
813
|
}> {
|
|
867
814
|
return trades.map((trade, index) => {
|
|
868
815
|
const premium =
|
|
869
|
-
typeof trade.premium === "number" && isFinite(trade.premium)
|
|
870
|
-
? trade.premium
|
|
871
|
-
: null;
|
|
816
|
+
typeof trade.premium === "number" && isFinite(trade.premium) ? trade.premium : null;
|
|
872
817
|
let efficiencyPct: number | null = null;
|
|
873
818
|
if (premium !== null && premium !== 0) {
|
|
874
819
|
efficiencyPct = (trade.pl / Math.abs(premium)) * 100;
|
|
@@ -897,7 +842,7 @@ function buildPremiumEfficiency(
|
|
|
897
842
|
*/
|
|
898
843
|
function buildMarginUtilization(
|
|
899
844
|
trades: Trade[],
|
|
900
|
-
equityCurve?: Array<{ date: string; equity: number; tradeNumber: number }
|
|
845
|
+
equityCurve?: Array<{ date: string; equity: number; tradeNumber: number }>,
|
|
901
846
|
): Array<{
|
|
902
847
|
tradeNumber: number;
|
|
903
848
|
date: string;
|
|
@@ -920,9 +865,7 @@ function buildMarginUtilization(
|
|
|
920
865
|
return trades
|
|
921
866
|
.map((trade, index) => {
|
|
922
867
|
const marginReq =
|
|
923
|
-
typeof trade.marginReq === "number" && isFinite(trade.marginReq)
|
|
924
|
-
? trade.marginReq
|
|
925
|
-
: 0;
|
|
868
|
+
typeof trade.marginReq === "number" && isFinite(trade.marginReq) ? trade.marginReq : 0;
|
|
926
869
|
const numContracts =
|
|
927
870
|
typeof trade.numContracts === "number" && isFinite(trade.numContracts)
|
|
928
871
|
? trade.numContracts
|
|
@@ -934,7 +877,8 @@ function buildMarginUtilization(
|
|
|
934
877
|
if (equityCurve && equityCurve.length > 0) {
|
|
935
878
|
const tradeNumber = index + 1;
|
|
936
879
|
const equityValue = equityByTradeNumber.get(tradeNumber);
|
|
937
|
-
fundsAtClose =
|
|
880
|
+
fundsAtClose =
|
|
881
|
+
equityValue ??
|
|
938
882
|
(typeof trade.fundsAtClose === "number" && isFinite(trade.fundsAtClose)
|
|
939
883
|
? trade.fundsAtClose
|
|
940
884
|
: 0);
|
|
@@ -963,9 +907,7 @@ function buildMarginUtilization(
|
|
|
963
907
|
/**
|
|
964
908
|
* Build volatility regimes data (VIX-correlated)
|
|
965
909
|
*/
|
|
966
|
-
function buildVolatilityRegimes(
|
|
967
|
-
trades: Trade[]
|
|
968
|
-
): Array<{
|
|
910
|
+
function buildVolatilityRegimes(trades: Trade[]): Array<{
|
|
969
911
|
tradeNumber: number;
|
|
970
912
|
date: string;
|
|
971
913
|
openingVix: number | null;
|
|
@@ -992,10 +934,7 @@ function buildVolatilityRegimes(
|
|
|
992
934
|
openingVix,
|
|
993
935
|
closingVix,
|
|
994
936
|
pl: trade.pl,
|
|
995
|
-
rom:
|
|
996
|
-
trade.marginReq && trade.marginReq > 0
|
|
997
|
-
? (trade.pl / trade.marginReq) * 100
|
|
998
|
-
: null,
|
|
937
|
+
rom: trade.marginReq && trade.marginReq > 0 ? (trade.pl / trade.marginReq) * 100 : null,
|
|
999
938
|
};
|
|
1000
939
|
})
|
|
1001
940
|
.filter((item): item is NonNullable<typeof item> => item !== null);
|
|
@@ -1005,15 +944,12 @@ function buildVolatilityRegimes(
|
|
|
1005
944
|
* Build monthly returns percent (percentage-based)
|
|
1006
945
|
* Note: Uses trade-based calculation (initial capital derived from first trade)
|
|
1007
946
|
*/
|
|
1008
|
-
function buildMonthlyReturnsPercent(
|
|
1009
|
-
trades: Trade[]
|
|
1010
|
-
): Record<number, Record<number, number>> {
|
|
947
|
+
function buildMonthlyReturnsPercent(trades: Trade[]): Record<number, Record<number, number>> {
|
|
1011
948
|
if (trades.length === 0) return {};
|
|
1012
949
|
|
|
1013
950
|
// Sort trades by date
|
|
1014
951
|
const sortedTrades = [...trades].sort(
|
|
1015
|
-
(a, b) =>
|
|
1016
|
-
new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime()
|
|
952
|
+
(a, b) => new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime(),
|
|
1017
953
|
);
|
|
1018
954
|
|
|
1019
955
|
// Calculate initial capital from first trade
|
|
@@ -1098,11 +1034,7 @@ function buildMonthlyReturnsPercent(
|
|
|
1098
1034
|
/**
|
|
1099
1035
|
* Apply date range filter to trades
|
|
1100
1036
|
*/
|
|
1101
|
-
function filterByDateRange(
|
|
1102
|
-
trades: Trade[],
|
|
1103
|
-
fromDate?: string,
|
|
1104
|
-
toDate?: string
|
|
1105
|
-
): Trade[] {
|
|
1037
|
+
function filterByDateRange(trades: Trade[], fromDate?: string, toDate?: string): Trade[] {
|
|
1106
1038
|
if (!fromDate && !toDate) return trades;
|
|
1107
1039
|
|
|
1108
1040
|
return trades.filter((trade) => {
|
|
@@ -1143,7 +1075,7 @@ interface PeakExposure {
|
|
|
1143
1075
|
*/
|
|
1144
1076
|
function buildDailyExposure(
|
|
1145
1077
|
trades: Trade[],
|
|
1146
|
-
equityCurve: Array<{ date: string; equity: number }
|
|
1078
|
+
equityCurve: Array<{ date: string; equity: number }>,
|
|
1147
1079
|
): {
|
|
1148
1080
|
dailyExposure: DailyExposurePoint[];
|
|
1149
1081
|
peakDailyExposure: PeakExposure | null;
|
|
@@ -1176,10 +1108,7 @@ function buildDailyExposure(
|
|
|
1176
1108
|
/**
|
|
1177
1109
|
* Register all performance MCP tools
|
|
1178
1110
|
*/
|
|
1179
|
-
export function registerPerformanceTools(
|
|
1180
|
-
server: McpServer,
|
|
1181
|
-
baseDir: string
|
|
1182
|
-
): void {
|
|
1111
|
+
export function registerPerformanceTools(server: McpServer, baseDir: string): void {
|
|
1183
1112
|
// Tool 1: get_performance_charts
|
|
1184
1113
|
server.registerTool(
|
|
1185
1114
|
"get_performance_charts",
|
|
@@ -1188,10 +1117,7 @@ export function registerPerformanceTools(
|
|
|
1188
1117
|
"Get chart data for performance visualizations: equity curves, drawdowns, return distributions, rolling metrics, and trade patterns. Use blockId from list_blocks.",
|
|
1189
1118
|
inputSchema: z.object({
|
|
1190
1119
|
blockId: z.string().describe("Block ID from list_blocks (e.g., 'main-port')"),
|
|
1191
|
-
strategy: z
|
|
1192
|
-
.string()
|
|
1193
|
-
.optional()
|
|
1194
|
-
.describe("Filter by strategy name (case-insensitive)"),
|
|
1120
|
+
strategy: z.string().optional().describe("Filter by strategy name (case-insensitive)"),
|
|
1195
1121
|
charts: z
|
|
1196
1122
|
.array(
|
|
1197
1123
|
z.enum([
|
|
@@ -1212,22 +1138,16 @@ export function registerPerformanceTools(
|
|
|
1212
1138
|
"volatility_regimes",
|
|
1213
1139
|
"mfe_mae",
|
|
1214
1140
|
"daily_exposure",
|
|
1215
|
-
])
|
|
1141
|
+
]),
|
|
1216
1142
|
)
|
|
1217
1143
|
.default(["equity_curve", "drawdown", "monthly_returns"])
|
|
1218
1144
|
.describe(
|
|
1219
|
-
"Which charts to include. Options: equity_curve, drawdown, monthly_returns, monthly_returns_percent, return_distribution, day_of_week, streak_data (win/loss streaks + runs test), trade_sequence (P&L by trade #), rom_timeline (Return on Margin over time), rolling_metrics (30-trade rolling sharpe/win rate), exit_reason_breakdown, holding_periods, premium_efficiency, margin_utilization, volatility_regimes (VIX-correlated), mfe_mae (Maximum Favorable/Adverse Excursion for stop loss/take profit optimization), daily_exposure (daily margin exposure with peak tracking)"
|
|
1145
|
+
"Which charts to include. Options: equity_curve, drawdown, monthly_returns, monthly_returns_percent, return_distribution, day_of_week, streak_data (win/loss streaks + runs test), trade_sequence (P&L by trade #), rom_timeline (Return on Margin over time), rolling_metrics (30-trade rolling sharpe/win rate), exit_reason_breakdown, holding_periods, premium_efficiency, margin_utilization, volatility_regimes (VIX-correlated), mfe_mae (Maximum Favorable/Adverse Excursion for stop loss/take profit optimization), daily_exposure (daily margin exposure with peak tracking)",
|
|
1220
1146
|
),
|
|
1221
1147
|
dateRange: z
|
|
1222
1148
|
.object({
|
|
1223
|
-
from: z
|
|
1224
|
-
|
|
1225
|
-
.optional()
|
|
1226
|
-
.describe("Start date YYYY-MM-DD (inclusive)"),
|
|
1227
|
-
to: z
|
|
1228
|
-
.string()
|
|
1229
|
-
.optional()
|
|
1230
|
-
.describe("End date YYYY-MM-DD (inclusive)"),
|
|
1149
|
+
from: z.string().optional().describe("Start date YYYY-MM-DD (inclusive)"),
|
|
1150
|
+
to: z.string().optional().describe("End date YYYY-MM-DD (inclusive)"),
|
|
1231
1151
|
})
|
|
1232
1152
|
.optional()
|
|
1233
1153
|
.describe("Filter trades to date range"),
|
|
@@ -1235,39 +1155,33 @@ export function registerPerformanceTools(
|
|
|
1235
1155
|
.boolean()
|
|
1236
1156
|
.default(false)
|
|
1237
1157
|
.describe(
|
|
1238
|
-
"Normalize all trades to 1 contract for fair comparison across different position sizes"
|
|
1158
|
+
"Normalize all trades to 1 contract for fair comparison across different position sizes",
|
|
1239
1159
|
),
|
|
1240
1160
|
bucketCount: z
|
|
1241
1161
|
.number()
|
|
1242
1162
|
.min(5)
|
|
1243
1163
|
.max(100)
|
|
1244
1164
|
.default(20)
|
|
1245
|
-
.describe(
|
|
1246
|
-
"Number of histogram buckets for return_distribution (default: 20)"
|
|
1247
|
-
),
|
|
1165
|
+
.describe("Number of histogram buckets for return_distribution (default: 20)"),
|
|
1248
1166
|
rollingWindowSize: z
|
|
1249
1167
|
.number()
|
|
1250
1168
|
.min(10)
|
|
1251
1169
|
.max(100)
|
|
1252
1170
|
.default(30)
|
|
1253
|
-
.describe(
|
|
1254
|
-
"Window size for rolling_metrics calculation (default: 30 trades)"
|
|
1255
|
-
),
|
|
1171
|
+
.describe("Window size for rolling_metrics calculation (default: 30 trades)"),
|
|
1256
1172
|
mfeMaeBucketSize: z
|
|
1257
1173
|
.number()
|
|
1258
1174
|
.min(1)
|
|
1259
1175
|
.max(50)
|
|
1260
1176
|
.default(10)
|
|
1261
|
-
.describe(
|
|
1262
|
-
"Bucket size (in %) for MFE/MAE distribution histogram (default: 10%)"
|
|
1263
|
-
),
|
|
1177
|
+
.describe("Bucket size (in %) for MFE/MAE distribution histogram (default: 10%)"),
|
|
1264
1178
|
maxDataPoints: z
|
|
1265
1179
|
.number()
|
|
1266
1180
|
.min(50)
|
|
1267
1181
|
.max(10000)
|
|
1268
1182
|
.default(500)
|
|
1269
1183
|
.describe(
|
|
1270
|
-
"Maximum data points for per-trade chart types (volatility_regimes, mfe_mae, trade_sequence, holding_periods, premium_efficiency, margin_utilization, rom_timeline). When exceeded, data is truncated with a flag. Default: 500."
|
|
1184
|
+
"Maximum data points for per-trade chart types (volatility_regimes, mfe_mae, trade_sequence, holding_periods, premium_efficiency, margin_utilization, rom_timeline). When exceeded, data is truncated with a flag. Default: 500.",
|
|
1271
1185
|
),
|
|
1272
1186
|
}),
|
|
1273
1187
|
},
|
|
@@ -1320,14 +1234,22 @@ export function registerPerformanceTools(
|
|
|
1320
1234
|
let anyTruncated = false;
|
|
1321
1235
|
|
|
1322
1236
|
// Helper to truncate per-trade arrays when they exceed maxDataPoints
|
|
1323
|
-
function truncateArray<T>(
|
|
1237
|
+
function truncateArray<T>(
|
|
1238
|
+
arr: T[],
|
|
1239
|
+
): T[] | { data: T[]; truncated: true; totalPoints: number } {
|
|
1324
1240
|
if (arr.length <= maxDataPoints) return arr;
|
|
1325
1241
|
anyTruncated = true;
|
|
1326
|
-
return {
|
|
1242
|
+
return {
|
|
1243
|
+
data: arr.slice(0, maxDataPoints),
|
|
1244
|
+
truncated: true as const,
|
|
1245
|
+
totalPoints: arr.length,
|
|
1246
|
+
};
|
|
1327
1247
|
}
|
|
1328
1248
|
|
|
1329
1249
|
// Helper to count actual output length from possibly-truncated data
|
|
1330
|
-
function outputLength<T>(
|
|
1250
|
+
function outputLength<T>(
|
|
1251
|
+
result: T[] | { data: T[]; truncated: true; totalPoints: number },
|
|
1252
|
+
): number {
|
|
1331
1253
|
return Array.isArray(result) ? result.length : result.data.length;
|
|
1332
1254
|
}
|
|
1333
1255
|
|
|
@@ -1349,10 +1271,7 @@ export function registerPerformanceTools(
|
|
|
1349
1271
|
|
|
1350
1272
|
if (charts.includes("monthly_returns")) {
|
|
1351
1273
|
chartData.monthlyReturns = buildMonthlyReturns(trades);
|
|
1352
|
-
const mr = chartData.monthlyReturns as Record<
|
|
1353
|
-
number,
|
|
1354
|
-
Record<number, number>
|
|
1355
|
-
>;
|
|
1274
|
+
const mr = chartData.monthlyReturns as Record<number, Record<number, number>>;
|
|
1356
1275
|
for (const year of Object.keys(mr)) {
|
|
1357
1276
|
for (const month of Object.keys(mr[Number(year)])) {
|
|
1358
1277
|
if (mr[Number(year)][Number(month)] !== 0) dataPoints++;
|
|
@@ -1362,10 +1281,7 @@ export function registerPerformanceTools(
|
|
|
1362
1281
|
|
|
1363
1282
|
if (charts.includes("monthly_returns_percent")) {
|
|
1364
1283
|
chartData.monthlyReturnsPercent = buildMonthlyReturnsPercent(trades);
|
|
1365
|
-
const mrp = chartData.monthlyReturnsPercent as Record<
|
|
1366
|
-
number,
|
|
1367
|
-
Record<number, number>
|
|
1368
|
-
>;
|
|
1284
|
+
const mrp = chartData.monthlyReturnsPercent as Record<number, Record<number, number>>;
|
|
1369
1285
|
for (const year of Object.keys(mrp)) {
|
|
1370
1286
|
for (const month of Object.keys(mrp[Number(year)])) {
|
|
1371
1287
|
if (mrp[Number(year)][Number(month)] !== 0) dataPoints++;
|
|
@@ -1374,10 +1290,7 @@ export function registerPerformanceTools(
|
|
|
1374
1290
|
}
|
|
1375
1291
|
|
|
1376
1292
|
if (charts.includes("return_distribution")) {
|
|
1377
|
-
chartData.returnDistribution = buildReturnDistribution(
|
|
1378
|
-
trades,
|
|
1379
|
-
bucketCount
|
|
1380
|
-
);
|
|
1293
|
+
chartData.returnDistribution = buildReturnDistribution(trades, bucketCount);
|
|
1381
1294
|
dataPoints += (chartData.returnDistribution as unknown[]).length;
|
|
1382
1295
|
}
|
|
1383
1296
|
|
|
@@ -1410,10 +1323,7 @@ export function registerPerformanceTools(
|
|
|
1410
1323
|
}
|
|
1411
1324
|
|
|
1412
1325
|
if (charts.includes("rolling_metrics")) {
|
|
1413
|
-
chartData.rollingMetrics = buildRollingMetrics(
|
|
1414
|
-
trades,
|
|
1415
|
-
rollingWindowSize
|
|
1416
|
-
);
|
|
1326
|
+
chartData.rollingMetrics = buildRollingMetrics(trades, rollingWindowSize);
|
|
1417
1327
|
dataPoints += (chartData.rollingMetrics as unknown[]).length;
|
|
1418
1328
|
}
|
|
1419
1329
|
|
|
@@ -1466,13 +1376,11 @@ export function registerPerformanceTools(
|
|
|
1466
1376
|
|
|
1467
1377
|
const avgMfePercent =
|
|
1468
1378
|
dataWithMfe.length > 0
|
|
1469
|
-
? dataWithMfe.reduce((sum, d) => sum + (d.mfePercent || 0), 0) /
|
|
1470
|
-
dataWithMfe.length
|
|
1379
|
+
? dataWithMfe.reduce((sum, d) => sum + (d.mfePercent || 0), 0) / dataWithMfe.length
|
|
1471
1380
|
: 0;
|
|
1472
1381
|
const avgMaePercent =
|
|
1473
1382
|
dataWithMae.length > 0
|
|
1474
|
-
? dataWithMae.reduce((sum, d) => sum + (d.maePercent || 0), 0) /
|
|
1475
|
-
dataWithMae.length
|
|
1383
|
+
? dataWithMae.reduce((sum, d) => sum + (d.maePercent || 0), 0) / dataWithMae.length
|
|
1476
1384
|
: 0;
|
|
1477
1385
|
const avgProfitCapture =
|
|
1478
1386
|
mfeData.filter((d) => d.profitCapturePercent !== undefined).length > 0
|
|
@@ -1510,7 +1418,11 @@ export function registerPerformanceTools(
|
|
|
1510
1418
|
let mfeOutputCount: number;
|
|
1511
1419
|
if (simplifiedData.length > maxDataPoints) {
|
|
1512
1420
|
anyTruncated = true;
|
|
1513
|
-
mfeDataPointsOutput = {
|
|
1421
|
+
mfeDataPointsOutput = {
|
|
1422
|
+
data: simplifiedData.slice(0, maxDataPoints),
|
|
1423
|
+
truncated: true,
|
|
1424
|
+
totalPoints: simplifiedData.length,
|
|
1425
|
+
};
|
|
1514
1426
|
mfeOutputCount = maxDataPoints;
|
|
1515
1427
|
} else {
|
|
1516
1428
|
mfeDataPointsOutput = simplifiedData;
|
|
@@ -1572,7 +1484,8 @@ export function registerPerformanceTools(
|
|
|
1572
1484
|
: 0,
|
|
1573
1485
|
},
|
|
1574
1486
|
...(isStrategyFiltered && {
|
|
1575
|
-
warning:
|
|
1487
|
+
warning:
|
|
1488
|
+
"Percentage values may be misleading when filtering by strategy. " +
|
|
1576
1489
|
"Margin values are absolute (sized for the full portfolio), but the equity " +
|
|
1577
1490
|
"curve is rebuilt for the filtered subset only. Use dollar exposure values " +
|
|
1578
1491
|
"for accurate analysis when filtering.",
|
|
@@ -1585,9 +1498,7 @@ export function registerPerformanceTools(
|
|
|
1585
1498
|
const filters: string[] = [];
|
|
1586
1499
|
if (strategy) filters.push(`strategy=${strategy}`);
|
|
1587
1500
|
if (dateRange?.from || dateRange?.to) {
|
|
1588
|
-
filters.push(
|
|
1589
|
-
`date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`
|
|
1590
|
-
);
|
|
1501
|
+
filters.push(`date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`);
|
|
1591
1502
|
}
|
|
1592
1503
|
if (normalizeTo1Lot) filters.push("normalized");
|
|
1593
1504
|
|
|
@@ -1622,7 +1533,7 @@ export function registerPerformanceTools(
|
|
|
1622
1533
|
isError: true,
|
|
1623
1534
|
};
|
|
1624
1535
|
}
|
|
1625
|
-
}
|
|
1536
|
+
},
|
|
1626
1537
|
);
|
|
1627
1538
|
|
|
1628
1539
|
// Tool 2: get_period_returns
|
|
@@ -1633,10 +1544,7 @@ export function registerPerformanceTools(
|
|
|
1633
1544
|
"Get P&L breakdown by period (monthly, weekly, or daily) with gross P/L, commissions, and net P/L",
|
|
1634
1545
|
inputSchema: z.object({
|
|
1635
1546
|
blockId: z.string().describe("Block folder name"),
|
|
1636
|
-
strategy: z
|
|
1637
|
-
.string()
|
|
1638
|
-
.optional()
|
|
1639
|
-
.describe("Filter by strategy name (case-insensitive)"),
|
|
1547
|
+
strategy: z.string().optional().describe("Filter by strategy name (case-insensitive)"),
|
|
1640
1548
|
period: z
|
|
1641
1549
|
.enum(["monthly", "weekly", "daily"])
|
|
1642
1550
|
.default("monthly")
|
|
@@ -1647,23 +1555,15 @@ export function registerPerformanceTools(
|
|
|
1647
1555
|
.describe("Filter to specific year (optional, alternative to dateRange)"),
|
|
1648
1556
|
dateRange: z
|
|
1649
1557
|
.object({
|
|
1650
|
-
from: z
|
|
1651
|
-
|
|
1652
|
-
.optional()
|
|
1653
|
-
.describe("Start date YYYY-MM-DD (inclusive)"),
|
|
1654
|
-
to: z
|
|
1655
|
-
.string()
|
|
1656
|
-
.optional()
|
|
1657
|
-
.describe("End date YYYY-MM-DD (inclusive)"),
|
|
1558
|
+
from: z.string().optional().describe("Start date YYYY-MM-DD (inclusive)"),
|
|
1559
|
+
to: z.string().optional().describe("End date YYYY-MM-DD (inclusive)"),
|
|
1658
1560
|
})
|
|
1659
1561
|
.optional()
|
|
1660
1562
|
.describe("Filter trades to date range (takes precedence over year)"),
|
|
1661
1563
|
normalizeTo1Lot: z
|
|
1662
1564
|
.boolean()
|
|
1663
1565
|
.default(false)
|
|
1664
|
-
.describe(
|
|
1665
|
-
"Normalize all trades to 1 contract for fair comparison"
|
|
1666
|
-
),
|
|
1566
|
+
.describe("Normalize all trades to 1 contract for fair comparison"),
|
|
1667
1567
|
}),
|
|
1668
1568
|
},
|
|
1669
1569
|
async ({ blockId, strategy, period, year, dateRange, normalizeTo1Lot }) => {
|
|
@@ -1678,9 +1578,7 @@ export function registerPerformanceTools(
|
|
|
1678
1578
|
if (dateRange) {
|
|
1679
1579
|
trades = filterByDateRange(trades, dateRange.from, dateRange.to);
|
|
1680
1580
|
} else if (year !== undefined) {
|
|
1681
|
-
trades = trades.filter(
|
|
1682
|
-
(t) => new Date(t.dateOpened).getFullYear() === year
|
|
1683
|
-
);
|
|
1581
|
+
trades = trades.filter((t) => new Date(t.dateOpened).getFullYear() === year);
|
|
1684
1582
|
}
|
|
1685
1583
|
|
|
1686
1584
|
// Apply normalization if requested
|
|
@@ -1740,8 +1638,7 @@ export function registerPerformanceTools(
|
|
|
1740
1638
|
};
|
|
1741
1639
|
|
|
1742
1640
|
const totalCommissions =
|
|
1743
|
-
(trade.openingCommissionsFees ?? 0) +
|
|
1744
|
-
(trade.closingCommissionsFees ?? 0);
|
|
1641
|
+
(trade.openingCommissionsFees ?? 0) + (trade.closingCommissionsFees ?? 0);
|
|
1745
1642
|
|
|
1746
1643
|
existing.grossPl += trade.pl;
|
|
1747
1644
|
existing.commissions += totalCommissions;
|
|
@@ -1770,9 +1667,7 @@ export function registerPerformanceTools(
|
|
|
1770
1667
|
const filters: string[] = [];
|
|
1771
1668
|
if (strategy) filters.push(`strategy=${strategy}`);
|
|
1772
1669
|
if (dateRange?.from || dateRange?.to) {
|
|
1773
|
-
filters.push(
|
|
1774
|
-
`date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`
|
|
1775
|
-
);
|
|
1670
|
+
filters.push(`date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`);
|
|
1776
1671
|
} else if (year !== undefined) {
|
|
1777
1672
|
filters.push(`year=${year}`);
|
|
1778
1673
|
}
|
|
@@ -1807,7 +1702,7 @@ export function registerPerformanceTools(
|
|
|
1807
1702
|
isError: true,
|
|
1808
1703
|
};
|
|
1809
1704
|
}
|
|
1810
|
-
}
|
|
1705
|
+
},
|
|
1811
1706
|
);
|
|
1812
1707
|
|
|
1813
1708
|
// Tool 3: compare_backtest_to_actual
|
|
@@ -1822,24 +1717,18 @@ export function registerPerformanceTools(
|
|
|
1822
1717
|
.string()
|
|
1823
1718
|
.optional()
|
|
1824
1719
|
.describe(
|
|
1825
|
-
"Filter to specific strategy name (matches both backtest and actual by strategy)"
|
|
1720
|
+
"Filter to specific strategy name (matches both backtest and actual by strategy)",
|
|
1826
1721
|
),
|
|
1827
1722
|
scaling: z
|
|
1828
1723
|
.enum(["raw", "perContract", "toReported"])
|
|
1829
1724
|
.default("raw")
|
|
1830
1725
|
.describe(
|
|
1831
|
-
"Scaling mode: 'raw' (no scaling), 'perContract' (divide by contracts for per-lot comparison), 'toReported' (scale backtest DOWN to match actual contract count)"
|
|
1726
|
+
"Scaling mode: 'raw' (no scaling), 'perContract' (divide by contracts for per-lot comparison), 'toReported' (scale backtest DOWN to match actual contract count)",
|
|
1832
1727
|
),
|
|
1833
1728
|
dateRange: z
|
|
1834
1729
|
.object({
|
|
1835
|
-
from: z
|
|
1836
|
-
|
|
1837
|
-
.optional()
|
|
1838
|
-
.describe("Start date YYYY-MM-DD (inclusive)"),
|
|
1839
|
-
to: z
|
|
1840
|
-
.string()
|
|
1841
|
-
.optional()
|
|
1842
|
-
.describe("End date YYYY-MM-DD (inclusive)"),
|
|
1730
|
+
from: z.string().optional().describe("Start date YYYY-MM-DD (inclusive)"),
|
|
1731
|
+
to: z.string().optional().describe("End date YYYY-MM-DD (inclusive)"),
|
|
1843
1732
|
})
|
|
1844
1733
|
.optional()
|
|
1845
1734
|
.describe("Filter trades to date range"),
|
|
@@ -1847,35 +1736,41 @@ export function registerPerformanceTools(
|
|
|
1847
1736
|
.boolean()
|
|
1848
1737
|
.default(false)
|
|
1849
1738
|
.describe(
|
|
1850
|
-
"Only include trades where both backtest and actual exist on the same date (excludes unmatched rows from output and totals)"
|
|
1739
|
+
"Only include trades where both backtest and actual exist on the same date (excludes unmatched rows from output and totals)",
|
|
1851
1740
|
),
|
|
1852
1741
|
detailLevel: z
|
|
1853
1742
|
.enum(["summary", "trades"])
|
|
1854
1743
|
.default("summary")
|
|
1855
1744
|
.describe(
|
|
1856
|
-
"'summary' (default): aggregate by date+strategy. 'trades': individual trade comparison with field-by-field differences"
|
|
1745
|
+
"'summary' (default): aggregate by date+strategy. 'trades': individual trade comparison with field-by-field differences",
|
|
1857
1746
|
),
|
|
1858
1747
|
outliersOnly: z
|
|
1859
1748
|
.boolean()
|
|
1860
1749
|
.default(false)
|
|
1861
|
-
.describe(
|
|
1862
|
-
"Only return high-slippage outliers (trades exceeding z-score threshold)"
|
|
1863
|
-
),
|
|
1750
|
+
.describe("Only return high-slippage outliers (trades exceeding z-score threshold)"),
|
|
1864
1751
|
outliersThreshold: z
|
|
1865
1752
|
.number()
|
|
1866
1753
|
.default(2)
|
|
1867
|
-
.describe(
|
|
1868
|
-
"Z-score threshold for outlier detection (default: 2 = ~95% confidence)"
|
|
1869
|
-
),
|
|
1754
|
+
.describe("Z-score threshold for outlier detection (default: 2 = ~95% confidence)"),
|
|
1870
1755
|
groupBy: z
|
|
1871
1756
|
.enum(["none", "strategy", "date", "week", "month"])
|
|
1872
1757
|
.default("none")
|
|
1873
1758
|
.describe(
|
|
1874
|
-
"Group results: 'none' (flat list), 'strategy', 'date' (daily), 'week', 'month'"
|
|
1759
|
+
"Group results: 'none' (flat list), 'strategy', 'date' (daily), 'week', 'month'",
|
|
1875
1760
|
),
|
|
1876
1761
|
}),
|
|
1877
1762
|
},
|
|
1878
|
-
async ({
|
|
1763
|
+
async ({
|
|
1764
|
+
blockId,
|
|
1765
|
+
strategy,
|
|
1766
|
+
scaling,
|
|
1767
|
+
dateRange,
|
|
1768
|
+
matchedOnly,
|
|
1769
|
+
detailLevel,
|
|
1770
|
+
outliersOnly,
|
|
1771
|
+
outliersThreshold,
|
|
1772
|
+
groupBy,
|
|
1773
|
+
}) => {
|
|
1879
1774
|
try {
|
|
1880
1775
|
const block = await loadBlock(baseDir, blockId);
|
|
1881
1776
|
let backtestTrades = block.trades;
|
|
@@ -1932,12 +1827,12 @@ export function registerPerformanceTools(
|
|
|
1932
1827
|
// when no explicit dateRange is provided
|
|
1933
1828
|
let autoFilterApplied = false;
|
|
1934
1829
|
if (!dateRange) {
|
|
1935
|
-
const actualDates = actualTrades.map(t => formatDateKey(new Date(t.dateOpened)));
|
|
1830
|
+
const actualDates = actualTrades.map((t) => formatDateKey(new Date(t.dateOpened)));
|
|
1936
1831
|
if (actualDates.length > 0) {
|
|
1937
|
-
const minActualDate = actualDates.reduce((a, b) => a < b ? a : b);
|
|
1938
|
-
const maxActualDate = actualDates.reduce((a, b) => a > b ? a : b);
|
|
1832
|
+
const minActualDate = actualDates.reduce((a, b) => (a < b ? a : b));
|
|
1833
|
+
const maxActualDate = actualDates.reduce((a, b) => (a > b ? a : b));
|
|
1939
1834
|
const beforeCount = backtestTrades.length;
|
|
1940
|
-
backtestTrades = backtestTrades.filter(t => {
|
|
1835
|
+
backtestTrades = backtestTrades.filter((t) => {
|
|
1941
1836
|
const d = formatDateKey(new Date(t.dateOpened));
|
|
1942
1837
|
return d >= minActualDate && d <= maxActualDate;
|
|
1943
1838
|
});
|
|
@@ -1949,7 +1844,7 @@ export function registerPerformanceTools(
|
|
|
1949
1844
|
const getGroupKey = (
|
|
1950
1845
|
dateStr: string,
|
|
1951
1846
|
strategyName: string,
|
|
1952
|
-
groupByMode: typeof groupBy
|
|
1847
|
+
groupByMode: typeof groupBy,
|
|
1953
1848
|
): string => {
|
|
1954
1849
|
if (groupByMode === "strategy") {
|
|
1955
1850
|
return strategyName;
|
|
@@ -2053,13 +1948,18 @@ export function registerPerformanceTools(
|
|
|
2053
1948
|
const btContracts = btTrade.numContracts;
|
|
2054
1949
|
const actualContracts = actualTrade.numContracts;
|
|
2055
1950
|
const { scaledBtPl, scaledActualPl: actualPl } = calculateScaledPl(
|
|
2056
|
-
btTrade.pl,
|
|
2057
|
-
|
|
2058
|
-
|
|
1951
|
+
btTrade.pl,
|
|
1952
|
+
actualTrade.pl,
|
|
1953
|
+
btContracts,
|
|
1954
|
+
actualContracts,
|
|
1955
|
+
scaling,
|
|
2059
1956
|
);
|
|
2060
|
-
const scalingFactor =
|
|
2061
|
-
|
|
2062
|
-
|
|
1957
|
+
const scalingFactor =
|
|
1958
|
+
scaling === "toReported" && btContracts > 0 && actualContracts > 0
|
|
1959
|
+
? actualContracts / btContracts
|
|
1960
|
+
: scaling === "toReported" && btContracts === 0
|
|
1961
|
+
? 0
|
|
1962
|
+
: 1;
|
|
2063
1963
|
|
|
2064
1964
|
const slippage = actualPl - scaledBtPl;
|
|
2065
1965
|
const slippagePercent =
|
|
@@ -2269,13 +2169,16 @@ export function registerPerformanceTools(
|
|
|
2269
2169
|
processedActual.add(key);
|
|
2270
2170
|
|
|
2271
2171
|
const { scaledBtPl, scaledActualPl } = calculateScaledPl(
|
|
2272
|
-
btData.totalPl,
|
|
2273
|
-
|
|
2274
|
-
|
|
2172
|
+
btData.totalPl,
|
|
2173
|
+
actualData.totalPl,
|
|
2174
|
+
btData.contracts,
|
|
2175
|
+
actualData.contracts,
|
|
2176
|
+
scaling,
|
|
2275
2177
|
);
|
|
2276
|
-
const scalingFactor =
|
|
2277
|
-
|
|
2278
|
-
|
|
2178
|
+
const scalingFactor =
|
|
2179
|
+
scaling === "toReported" && btData.contracts > 0 && actualData.contracts > 0
|
|
2180
|
+
? actualData.contracts / btData.contracts
|
|
2181
|
+
: 1;
|
|
2279
2182
|
|
|
2280
2183
|
const slippage = scaledActualPl - scaledBtPl;
|
|
2281
2184
|
const slippagePercent =
|
|
@@ -2378,10 +2281,8 @@ export function registerPerformanceTools(
|
|
|
2378
2281
|
const meanSlippage =
|
|
2379
2282
|
slippageValues.reduce((sum, v) => sum + v, 0) / slippageValues.length;
|
|
2380
2283
|
const variance =
|
|
2381
|
-
slippageValues.reduce(
|
|
2382
|
-
|
|
2383
|
-
0
|
|
2384
|
-
) / slippageValues.length;
|
|
2284
|
+
slippageValues.reduce((sum, v) => sum + Math.pow(v - meanSlippage, 2), 0) /
|
|
2285
|
+
slippageValues.length;
|
|
2385
2286
|
const stdDevSlippage = Math.sqrt(variance);
|
|
2386
2287
|
|
|
2387
2288
|
// Guard: skip if all values are essentially the same
|
|
@@ -2389,8 +2290,7 @@ export function registerPerformanceTools(
|
|
|
2389
2290
|
// Calculate z-scores and flag outliers
|
|
2390
2291
|
for (const comparison of comparisons) {
|
|
2391
2292
|
if (comparison.matched) {
|
|
2392
|
-
const zScore =
|
|
2393
|
-
(comparison.slippage - meanSlippage) / stdDevSlippage;
|
|
2293
|
+
const zScore = (comparison.slippage - meanSlippage) / stdDevSlippage;
|
|
2394
2294
|
comparison.zScore = zScore;
|
|
2395
2295
|
|
|
2396
2296
|
if (Math.abs(zScore) >= outliersThreshold) {
|
|
@@ -2407,10 +2307,7 @@ export function registerPerformanceTools(
|
|
|
2407
2307
|
}
|
|
2408
2308
|
|
|
2409
2309
|
const outliers = comparisons.filter((c) => c.isOutlier);
|
|
2410
|
-
const outlierTotalSlippage = outliers.reduce(
|
|
2411
|
-
(sum, c) => sum + c.slippage,
|
|
2412
|
-
0
|
|
2413
|
-
);
|
|
2310
|
+
const outlierTotalSlippage = outliers.reduce((sum, c) => sum + c.slippage, 0);
|
|
2414
2311
|
|
|
2415
2312
|
outlierStats = {
|
|
2416
2313
|
meanSlippage,
|
|
@@ -2422,43 +2319,46 @@ export function registerPerformanceTools(
|
|
|
2422
2319
|
? (outliers.length / matchedComparisons.length) * 100
|
|
2423
2320
|
: 0,
|
|
2424
2321
|
outlierTotalSlippage,
|
|
2425
|
-
outlierAvgSlippage:
|
|
2426
|
-
outliers.length > 0 ? outlierTotalSlippage / outliers.length : 0,
|
|
2322
|
+
outlierAvgSlippage: outliers.length > 0 ? outlierTotalSlippage / outliers.length : 0,
|
|
2427
2323
|
};
|
|
2428
2324
|
}
|
|
2429
2325
|
}
|
|
2430
2326
|
|
|
2431
2327
|
// Build unmatched summaries before filtering
|
|
2432
2328
|
const unmatchedBacktestEntries = comparisons.filter(
|
|
2433
|
-
(c) => !c.matched && c.backtestPl !== 0
|
|
2329
|
+
(c) => !c.matched && c.backtestPl !== 0,
|
|
2434
2330
|
);
|
|
2435
|
-
const unmatchedActualEntries = comparisons.filter(
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2331
|
+
const unmatchedActualEntries = comparisons.filter((c) => !c.matched && c.actualPl !== 0);
|
|
2332
|
+
|
|
2333
|
+
const unmatchedBacktestSummary =
|
|
2334
|
+
unmatchedBacktestEntries.length > 0
|
|
2335
|
+
? {
|
|
2336
|
+
count: unmatchedBacktestEntries.length,
|
|
2337
|
+
dateRange: {
|
|
2338
|
+
from: unmatchedBacktestEntries.reduce((a, b) => (a.date < b.date ? a : b)).date,
|
|
2339
|
+
to: unmatchedBacktestEntries.reduce((a, b) => (a.date > b.date ? a : b)).date,
|
|
2340
|
+
},
|
|
2341
|
+
totalPl: unmatchedBacktestEntries.reduce((sum, c) => sum + c.backtestPl, 0),
|
|
2342
|
+
strategies: Array.from(
|
|
2343
|
+
new Set(unmatchedBacktestEntries.map((c) => c.strategy)),
|
|
2344
|
+
).sort(),
|
|
2345
|
+
}
|
|
2346
|
+
: null;
|
|
2450
2347
|
|
|
2451
|
-
const unmatchedActualSummary =
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2348
|
+
const unmatchedActualSummary =
|
|
2349
|
+
unmatchedActualEntries.length > 0
|
|
2350
|
+
? {
|
|
2351
|
+
count: unmatchedActualEntries.length,
|
|
2352
|
+
dateRange: {
|
|
2353
|
+
from: unmatchedActualEntries.reduce((a, b) => (a.date < b.date ? a : b)).date,
|
|
2354
|
+
to: unmatchedActualEntries.reduce((a, b) => (a.date > b.date ? a : b)).date,
|
|
2355
|
+
},
|
|
2356
|
+
totalPl: unmatchedActualEntries.reduce((sum, c) => sum + c.actualPl, 0),
|
|
2357
|
+
strategies: Array.from(
|
|
2358
|
+
new Set(unmatchedActualEntries.map((c) => c.strategy)),
|
|
2359
|
+
).sort(),
|
|
2360
|
+
}
|
|
2361
|
+
: null;
|
|
2462
2362
|
|
|
2463
2363
|
// Apply matchedOnly filter to the primary output set
|
|
2464
2364
|
let outputComparisons = matchedOnly
|
|
@@ -2471,9 +2371,7 @@ export function registerPerformanceTools(
|
|
|
2471
2371
|
}
|
|
2472
2372
|
|
|
2473
2373
|
// Sort by absolute slippage (worst first) to surface problem areas
|
|
2474
|
-
outputComparisons.sort(
|
|
2475
|
-
(a, b) => Math.abs(b.slippage) - Math.abs(a.slippage)
|
|
2476
|
-
);
|
|
2374
|
+
outputComparisons.sort((a, b) => Math.abs(b.slippage) - Math.abs(a.slippage));
|
|
2477
2375
|
|
|
2478
2376
|
// Apply grouping if requested
|
|
2479
2377
|
let groups: GroupedResult[] | null = null;
|
|
@@ -2489,23 +2387,15 @@ export function registerPerformanceTools(
|
|
|
2489
2387
|
groups = Array.from(groupMap.entries())
|
|
2490
2388
|
.map(([groupKey, groupComparisons]) => {
|
|
2491
2389
|
const matchedInGroup = groupComparisons.filter((c) => c.matched);
|
|
2492
|
-
const totalSlippage = groupComparisons.reduce(
|
|
2493
|
-
|
|
2494
|
-
0
|
|
2495
|
-
);
|
|
2496
|
-
const outlierCount = groupComparisons.filter(
|
|
2497
|
-
(c) => c.isOutlier
|
|
2498
|
-
).length;
|
|
2390
|
+
const totalSlippage = groupComparisons.reduce((sum, c) => sum + c.slippage, 0);
|
|
2391
|
+
const outlierCount = groupComparisons.filter((c) => c.isOutlier).length;
|
|
2499
2392
|
|
|
2500
2393
|
return {
|
|
2501
2394
|
groupKey,
|
|
2502
2395
|
count: groupComparisons.length,
|
|
2503
2396
|
matchedCount: matchedInGroup.length,
|
|
2504
2397
|
totalSlippage,
|
|
2505
|
-
avgSlippage:
|
|
2506
|
-
matchedInGroup.length > 0
|
|
2507
|
-
? totalSlippage / matchedInGroup.length
|
|
2508
|
-
: 0,
|
|
2398
|
+
avgSlippage: matchedInGroup.length > 0 ? totalSlippage / matchedInGroup.length : 0,
|
|
2509
2399
|
outlierCount,
|
|
2510
2400
|
comparisons: groupComparisons,
|
|
2511
2401
|
};
|
|
@@ -2521,40 +2411,35 @@ export function registerPerformanceTools(
|
|
|
2521
2411
|
const matchedForSummary = outputComparisons.filter((c) => c.matched);
|
|
2522
2412
|
const totalBacktestPl = comparisonsForTotals.reduce(
|
|
2523
2413
|
(sum, c) => sum + c.scaledBacktestPl,
|
|
2524
|
-
0
|
|
2414
|
+
0,
|
|
2525
2415
|
);
|
|
2526
2416
|
const totalActualPl = comparisonsForTotals.reduce(
|
|
2527
|
-
(sum, c) =>
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2417
|
+
(sum, c) =>
|
|
2418
|
+
sum +
|
|
2419
|
+
(scaling === "perContract" && c.actualContracts > 0
|
|
2420
|
+
? c.actualPl / c.actualContracts
|
|
2421
|
+
: c.actualPl),
|
|
2422
|
+
0,
|
|
2531
2423
|
);
|
|
2532
2424
|
const totalSlippage = totalActualPl - totalBacktestPl;
|
|
2533
2425
|
const avgSlippage =
|
|
2534
2426
|
matchedForSummary.length > 0
|
|
2535
|
-
? matchedForSummary.reduce((sum, c) => sum + c.slippage, 0) /
|
|
2536
|
-
matchedForSummary.length
|
|
2427
|
+
? matchedForSummary.reduce((sum, c) => sum + c.slippage, 0) / matchedForSummary.length
|
|
2537
2428
|
: 0;
|
|
2538
2429
|
const avgSlippagePercent =
|
|
2539
|
-
totalBacktestPl !== 0
|
|
2540
|
-
? (totalSlippage / Math.abs(totalBacktestPl)) * 100
|
|
2541
|
-
: null;
|
|
2430
|
+
totalBacktestPl !== 0 ? (totalSlippage / Math.abs(totalBacktestPl)) * 100 : null;
|
|
2542
2431
|
|
|
2543
2432
|
// Get unique strategies
|
|
2544
2433
|
const backtestStrategies = Array.from(
|
|
2545
|
-
new Set(backtestTrades.map((t) => t.strategy))
|
|
2546
|
-
).sort();
|
|
2547
|
-
const actualStrategies = Array.from(
|
|
2548
|
-
new Set(actualTrades.map((t) => t.strategy))
|
|
2434
|
+
new Set(backtestTrades.map((t) => t.strategy)),
|
|
2549
2435
|
).sort();
|
|
2436
|
+
const actualStrategies = Array.from(new Set(actualTrades.map((t) => t.strategy))).sort();
|
|
2550
2437
|
|
|
2551
2438
|
// Brief summary for user display
|
|
2552
2439
|
const filters: string[] = [];
|
|
2553
2440
|
if (strategy) filters.push(`strategy=${strategy}`);
|
|
2554
2441
|
if (dateRange?.from || dateRange?.to) {
|
|
2555
|
-
filters.push(
|
|
2556
|
-
`date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`
|
|
2557
|
-
);
|
|
2442
|
+
filters.push(`date=${dateRange.from ?? "start"} to ${dateRange.to ?? "end"}`);
|
|
2558
2443
|
}
|
|
2559
2444
|
if (autoFilterApplied) filters.push("auto-date-overlap");
|
|
2560
2445
|
if (matchedOnly) filters.push("matched-only");
|
|
@@ -2568,9 +2453,7 @@ export function registerPerformanceTools(
|
|
|
2568
2453
|
? `${formatPercent(avgSlippagePercent)} slippage`
|
|
2569
2454
|
: "N/A slippage";
|
|
2570
2455
|
const outlierDisplay =
|
|
2571
|
-
outlierStats !== null
|
|
2572
|
-
? ` | ${outlierStats.outlierCount} outliers`
|
|
2573
|
-
: "";
|
|
2456
|
+
outlierStats !== null ? ` | ${outlierStats.outlierCount} outliers` : "";
|
|
2574
2457
|
const summary = `Comparison: ${blockId}${filterStr} | ${scaling} scaling | ${matchedForSummary.length}/${outputComparisons.length} matched | ${slippageDisplay}${outlierDisplay}`;
|
|
2575
2458
|
|
|
2576
2459
|
// Build structured data for Claude reasoning
|
|
@@ -2627,6 +2510,6 @@ export function registerPerformanceTools(
|
|
|
2627
2510
|
isError: true,
|
|
2628
2511
|
};
|
|
2629
2512
|
}
|
|
2630
|
-
}
|
|
2513
|
+
},
|
|
2631
2514
|
);
|
|
2632
2515
|
}
|