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
|
@@ -10,6 +10,7 @@ import { registerComparisonBlockTools } from "./comparison.ts";
|
|
|
10
10
|
import { registerAnalysisBlockTools } from "./analysis.ts";
|
|
11
11
|
import { registerSimilarityBlockTools } from "./similarity.ts";
|
|
12
12
|
import { registerHealthBlockTools } from "./health.ts";
|
|
13
|
+
import { registerPairedComparisonTool } from "./paired-comparison.ts";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Register all block-related MCP tools
|
|
@@ -20,4 +21,5 @@ export function registerBlockTools(server: McpServer, baseDir: string): void {
|
|
|
20
21
|
registerAnalysisBlockTools(server, baseDir);
|
|
21
22
|
registerSimilarityBlockTools(server, baseDir);
|
|
22
23
|
registerHealthBlockTools(server, baseDir);
|
|
24
|
+
registerPairedComparisonTool(server, baseDir);
|
|
23
25
|
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paired Bootstrap Comparison Tool
|
|
3
|
+
*
|
|
4
|
+
* Honest confidence intervals for "is strategy A actually different from strategy
|
|
5
|
+
* B (or from zero)". Wraps the paired day-block bootstrap primitive from
|
|
6
|
+
* @tradeblocks/lib with a declarative attribution of each trade's P&L onto a
|
|
7
|
+
* daily grid derived from the block's own trade data.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
|
+
import { loadBlock } from "../../utils/block-loader.ts";
|
|
13
|
+
import { createToolOutput, formatCurrency } from "../../utils/output-formatter.ts";
|
|
14
|
+
import {
|
|
15
|
+
pairedBlockBootstrap,
|
|
16
|
+
holdingPeriodBlockDays,
|
|
17
|
+
type DaySeries,
|
|
18
|
+
type PairedBlockBootstrapResult,
|
|
19
|
+
} from "@tradeblocks/lib";
|
|
20
|
+
import type { Trade } from "@tradeblocks/lib";
|
|
21
|
+
import { filterByStrategy, filterByDateRange } from "../shared/filters.ts";
|
|
22
|
+
import { withSyncedBlock } from "../middleware/sync-middleware.ts";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* How each trade's P&L reaches the daily grid. Reused in the tool description
|
|
26
|
+
* and echoed in the structured output so the methodology is never implicit.
|
|
27
|
+
*/
|
|
28
|
+
const ATTRIBUTION_NOTE =
|
|
29
|
+
"A trade's P&L is attributed evenly across the trading days it was open " +
|
|
30
|
+
"(dateOpened through dateClosed). Trading days are derived from the block's own " +
|
|
31
|
+
"trade data (the union of every trade's open and close dates) -- no market " +
|
|
32
|
+
"calendar is assumed, so weekends or holidays appear only if a trade spans them.";
|
|
33
|
+
|
|
34
|
+
// Frozen, mathematically-neutral defaults (documented on the schema). Block
|
|
35
|
+
// length is NEVER defaulted to a constant -- it is derived from the submitted
|
|
36
|
+
// block's own holding-period distribution unless the caller overrides it.
|
|
37
|
+
const DEFAULT_CI_LEVEL = 0.95;
|
|
38
|
+
const DEFAULT_RESAMPLES = 2000;
|
|
39
|
+
const DEFAULT_SEED = 42;
|
|
40
|
+
const SENSITIVITY_MULTIPLIERS = [0.5, 2];
|
|
41
|
+
|
|
42
|
+
export const pairedComparisonInputSchema = z.object({
|
|
43
|
+
blockId: z.string().describe("Block folder name to analyze"),
|
|
44
|
+
strategyA: z.string().describe("Strategy name for arm A (case-insensitive)"),
|
|
45
|
+
strategyB: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe(
|
|
49
|
+
"Strategy name for arm B (case-insensitive). If omitted, arm A is compared against a " +
|
|
50
|
+
"constant 0 -- i.e. is the edge distinguishable from nothing.",
|
|
51
|
+
),
|
|
52
|
+
statistic: z
|
|
53
|
+
.enum(["mean_daily_pnl", "median_daily_pnl"])
|
|
54
|
+
.default("mean_daily_pnl")
|
|
55
|
+
.describe("Which daily-P&L functional to compare. Default mean_daily_pnl."),
|
|
56
|
+
dateRange: z
|
|
57
|
+
.object({
|
|
58
|
+
start: z.string().describe("Start date (YYYY-MM-DD)"),
|
|
59
|
+
end: z.string().describe("End date (YYYY-MM-DD)"),
|
|
60
|
+
})
|
|
61
|
+
.optional()
|
|
62
|
+
.describe("Optional date range filter applied to both arms (by trade open date)."),
|
|
63
|
+
ciLevel: z
|
|
64
|
+
.number()
|
|
65
|
+
.default(DEFAULT_CI_LEVEL)
|
|
66
|
+
.describe("Confidence level in (0, 1). Default 0.95 (deterministic)."),
|
|
67
|
+
resamples: z
|
|
68
|
+
.number()
|
|
69
|
+
.int()
|
|
70
|
+
.default(DEFAULT_RESAMPLES)
|
|
71
|
+
.describe("Number of bootstrap resamples. Default 2000 (deterministic)."),
|
|
72
|
+
seed: z
|
|
73
|
+
.number()
|
|
74
|
+
.int()
|
|
75
|
+
.default(DEFAULT_SEED)
|
|
76
|
+
.describe("PRNG seed. Same inputs + seed -> identical result. Default 42 (deterministic)."),
|
|
77
|
+
blockDays: z
|
|
78
|
+
.number()
|
|
79
|
+
.int()
|
|
80
|
+
.min(1)
|
|
81
|
+
.optional()
|
|
82
|
+
.describe(
|
|
83
|
+
"Override the block length. By default it is derived from this block's own " +
|
|
84
|
+
"holding-period distribution (95th percentile of days-open).",
|
|
85
|
+
),
|
|
86
|
+
effectiveNFloorBlocks: z
|
|
87
|
+
.number()
|
|
88
|
+
.min(0)
|
|
89
|
+
.optional()
|
|
90
|
+
.describe(
|
|
91
|
+
"Optional: refuse with notComparable when the shared overlap yields fewer distinct " +
|
|
92
|
+
"blocks than this floor. When omitted, only structural degeneracy refuses (underpowered).",
|
|
93
|
+
),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
type PairedComparisonInput = z.infer<typeof pairedComparisonInputSchema>;
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Daily attribution
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Calendar date (YYYY-MM-DD) for a trade timestamp.
|
|
104
|
+
*
|
|
105
|
+
* Calendar dates are derived in UTC from the stored trade timestamps; the grid,
|
|
106
|
+
* the lib tests, and the displayed overlap windows all share this convention,
|
|
107
|
+
* so the result is independent of the host machine's timezone.
|
|
108
|
+
*/
|
|
109
|
+
function toCalendarDateStr(date: Date): string {
|
|
110
|
+
const y = date.getUTCFullYear();
|
|
111
|
+
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
112
|
+
const d = String(date.getUTCDate()).padStart(2, "0");
|
|
113
|
+
return `${y}-${m}-${d}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The block's trading-day grid: the sorted, unique union of every trade's open
|
|
118
|
+
* and close calendar dates. Days strictly between an open and a close that are
|
|
119
|
+
* not themselves any trade's open/close are unknown to be trading days and are
|
|
120
|
+
* deliberately not invented.
|
|
121
|
+
*/
|
|
122
|
+
export function buildBlockTradingDayIndex(trades: Trade[]): string[] {
|
|
123
|
+
const days = new Set<string>();
|
|
124
|
+
for (const t of trades) {
|
|
125
|
+
days.add(toCalendarDateStr(t.dateOpened));
|
|
126
|
+
days.add(toCalendarDateStr(t.dateClosed ?? t.dateOpened));
|
|
127
|
+
}
|
|
128
|
+
return Array.from(days).sort();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Inclusive index range [lo, hi] of grid days within [openStr, closeStr]. */
|
|
132
|
+
function gridSpan(grid: string[], openStr: string, closeStr: string): [number, number] {
|
|
133
|
+
let lo = grid.findIndex((d) => d >= openStr);
|
|
134
|
+
if (lo === -1) lo = grid.length;
|
|
135
|
+
let hi = -1;
|
|
136
|
+
for (let i = grid.length - 1; i >= 0; i--) {
|
|
137
|
+
if (grid[i] <= closeStr) {
|
|
138
|
+
hi = i;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return [lo, hi];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Number of grid trading days each trade was open. This is the holding period
|
|
147
|
+
* used both for even P&L attribution and for block-length derivation.
|
|
148
|
+
*/
|
|
149
|
+
export function armHoldingPeriods(armTrades: Trade[], grid: string[]): number[] {
|
|
150
|
+
return armTrades.map((t) => {
|
|
151
|
+
const openStr = toCalendarDateStr(t.dateOpened);
|
|
152
|
+
const closeStr = toCalendarDateStr(t.dateClosed ?? t.dateOpened);
|
|
153
|
+
const [lo, hi] = gridSpan(grid, openStr, closeStr);
|
|
154
|
+
return Math.max(1, hi - lo + 1);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build one arm's DaySeries on the shared trading-day grid. Each trade's P&L is
|
|
160
|
+
* spread evenly across the grid days it was open; a grid day the arm did not
|
|
161
|
+
* hold a position is not-observed (mask false), while a held day with a net-zero
|
|
162
|
+
* attributed P&L is a genuine observation (mask true).
|
|
163
|
+
*/
|
|
164
|
+
export function buildArmDaySeries(armTrades: Trade[], grid: string[]): DaySeries {
|
|
165
|
+
const values = new Array<number>(grid.length).fill(0);
|
|
166
|
+
const observedMask = new Array<boolean>(grid.length).fill(false);
|
|
167
|
+
|
|
168
|
+
for (const t of armTrades) {
|
|
169
|
+
const openStr = toCalendarDateStr(t.dateOpened);
|
|
170
|
+
const closeStr = toCalendarDateStr(t.dateClosed ?? t.dateOpened);
|
|
171
|
+
const [lo, hi] = gridSpan(grid, openStr, closeStr);
|
|
172
|
+
if (hi < lo) continue;
|
|
173
|
+
const span = hi - lo + 1;
|
|
174
|
+
const perDay = t.pl / span;
|
|
175
|
+
for (let i = lo; i <= hi; i++) {
|
|
176
|
+
values[i] += perDay;
|
|
177
|
+
observedMask[i] = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { index: grid, values, observedMask };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function countObserved(series: DaySeries): number {
|
|
185
|
+
return series.observedMask.reduce((n, o) => n + (o ? 1 : 0), 0);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Statistics
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
function meanOf(values: number[]): number {
|
|
193
|
+
return values.reduce((s, v) => s + v, 0) / values.length;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function medianOf(values: number[]): number {
|
|
197
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
198
|
+
const mid = Math.floor(sorted.length / 2);
|
|
199
|
+
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const STATISTIC_FNS: Record<PairedComparisonInput["statistic"], (d: number[]) => number> = {
|
|
203
|
+
mean_daily_pnl: meanOf,
|
|
204
|
+
median_daily_pnl: medianOf,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const STATISTIC_LABELS: Record<PairedComparisonInput["statistic"], string> = {
|
|
208
|
+
mean_daily_pnl: "mean daily P&L",
|
|
209
|
+
median_daily_pnl: "median daily P&L",
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Core computation (DB-free, directly testable)
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
export interface PairedComparisonParams {
|
|
217
|
+
armATrades: Trade[];
|
|
218
|
+
/** null -> compare arm A against a constant 0. */
|
|
219
|
+
armBTrades: Trade[] | null;
|
|
220
|
+
/** All block trades -- defines the shared trading-day grid. */
|
|
221
|
+
blockTrades: Trade[];
|
|
222
|
+
statistic: PairedComparisonInput["statistic"];
|
|
223
|
+
ciLevel: number;
|
|
224
|
+
resamples: number;
|
|
225
|
+
seed: number;
|
|
226
|
+
blockDays?: number;
|
|
227
|
+
/** Optional power floor -> notComparable when effectiveN falls below it. */
|
|
228
|
+
effectiveNFloorBlocks?: number;
|
|
229
|
+
strategyA: string;
|
|
230
|
+
strategyB?: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface PairedComparisonReport {
|
|
234
|
+
summary: string;
|
|
235
|
+
data: Record<string, unknown>;
|
|
236
|
+
result: PairedBlockBootstrapResult;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Run the paired day-block bootstrap for a strategy comparison and format an
|
|
241
|
+
* honest, plain-English report. Throws a clear error when a strategy filter
|
|
242
|
+
* matched zero trades.
|
|
243
|
+
*/
|
|
244
|
+
export function runPairedBootstrapComparison(
|
|
245
|
+
params: PairedComparisonParams,
|
|
246
|
+
): PairedComparisonReport {
|
|
247
|
+
const {
|
|
248
|
+
armATrades,
|
|
249
|
+
armBTrades,
|
|
250
|
+
blockTrades,
|
|
251
|
+
statistic,
|
|
252
|
+
ciLevel,
|
|
253
|
+
resamples,
|
|
254
|
+
seed,
|
|
255
|
+
blockDays: blockDaysOverride,
|
|
256
|
+
effectiveNFloorBlocks,
|
|
257
|
+
strategyA,
|
|
258
|
+
strategyB,
|
|
259
|
+
} = params;
|
|
260
|
+
|
|
261
|
+
if (armATrades.length === 0) {
|
|
262
|
+
throw new Error(`No trades found for strategy "${strategyA}".`);
|
|
263
|
+
}
|
|
264
|
+
if (armBTrades !== null && armBTrades.length === 0) {
|
|
265
|
+
throw new Error(`No trades found for strategy "${strategyB}".`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const grid = buildBlockTradingDayIndex(blockTrades);
|
|
269
|
+
const armA = buildArmDaySeries(armATrades, grid);
|
|
270
|
+
const armB = armBTrades !== null ? buildArmDaySeries(armBTrades, grid) : null;
|
|
271
|
+
|
|
272
|
+
// Block length: derived from the LONGER arm's 95th-percentile holding period
|
|
273
|
+
// (the conservative choice), unless the caller overrides it.
|
|
274
|
+
const hpA = holdingPeriodBlockDays(armHoldingPeriods(armATrades, grid));
|
|
275
|
+
const hpB =
|
|
276
|
+
armBTrades !== null ? holdingPeriodBlockDays(armHoldingPeriods(armBTrades, grid)) : hpA;
|
|
277
|
+
const derivedBlockDays = Math.max(hpA, hpB);
|
|
278
|
+
const derivedFromArm = hpB > hpA ? "B" : "A";
|
|
279
|
+
const blockDays = blockDaysOverride ?? derivedBlockDays;
|
|
280
|
+
|
|
281
|
+
const blockDaysNote =
|
|
282
|
+
blockDaysOverride !== undefined
|
|
283
|
+
? `block length = ${blockDays} trading day(s), caller-supplied override`
|
|
284
|
+
: armB !== null
|
|
285
|
+
? `block length = ${blockDays} trading day(s), derived from the 95th-percentile ` +
|
|
286
|
+
`holding period of arm ${derivedFromArm} (the longer-held arm -- the conservative choice)`
|
|
287
|
+
: `block length = ${blockDays} trading day(s), derived from the 95th-percentile ` +
|
|
288
|
+
`holding period of arm A`;
|
|
289
|
+
|
|
290
|
+
const result = pairedBlockBootstrap({
|
|
291
|
+
armA,
|
|
292
|
+
armB: armB ?? { constant: 0 },
|
|
293
|
+
statistic: STATISTIC_FNS[statistic],
|
|
294
|
+
holdingRule: { blockDays, sensitivity: SENSITIVITY_MULTIPLIERS },
|
|
295
|
+
ciLevel,
|
|
296
|
+
resamples,
|
|
297
|
+
seed,
|
|
298
|
+
effectiveNFloorBlocks,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const statLabel = STATISTIC_LABELS[statistic];
|
|
302
|
+
const comparisonLabel = armB !== null ? `${strategyA} minus ${strategyB}` : `${strategyA} vs 0`;
|
|
303
|
+
|
|
304
|
+
// Plain-English lead line -- refusal is surfaced first, never buried.
|
|
305
|
+
let summary: string;
|
|
306
|
+
let refusalMessage: string | null = null;
|
|
307
|
+
if (result.status === "notComparable") {
|
|
308
|
+
refusalMessage =
|
|
309
|
+
`Not comparable: effective sample (${result.effectiveN.toFixed(2)} blocks) is below the ` +
|
|
310
|
+
`required power floor. A confidence interval is withheld.`;
|
|
311
|
+
summary = `Paired comparison (${comparisonLabel}): ${refusalMessage}`;
|
|
312
|
+
} else if (result.status === "underpowered") {
|
|
313
|
+
refusalMessage =
|
|
314
|
+
`Underpowered: the shared overlap is too short to resample at a block length of ` +
|
|
315
|
+
`${blockDays} trading day(s) (fewer than two drawable blocks). A confidence interval is withheld.`;
|
|
316
|
+
summary = `Paired comparison (${comparisonLabel}): ${refusalMessage}`;
|
|
317
|
+
} else {
|
|
318
|
+
const point = result.point ?? 0;
|
|
319
|
+
const low = result.ci.low ?? 0;
|
|
320
|
+
const high = result.ci.high ?? 0;
|
|
321
|
+
const includesZero = low <= 0 && high >= 0;
|
|
322
|
+
const zeroClause = includesZero
|
|
323
|
+
? "includes zero (not distinguishable)"
|
|
324
|
+
: "excludes zero (distinguishable)";
|
|
325
|
+
const subject =
|
|
326
|
+
armB !== null ? `the difference in ${statLabel} (${comparisonLabel})` : `${statLabel}`;
|
|
327
|
+
summary =
|
|
328
|
+
`Paired comparison: ${subject} is ${formatCurrency(point)}, ` +
|
|
329
|
+
`${(ciLevel * 100).toFixed(0)}% CI [${formatCurrency(low)}, ${formatCurrency(high)}] -- ${zeroClause}.`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const data: Record<string, unknown> = {
|
|
333
|
+
comparison: {
|
|
334
|
+
strategyA,
|
|
335
|
+
strategyB: strategyB ?? null,
|
|
336
|
+
mode: armB !== null ? "two-arm" : "single-arm-vs-zero",
|
|
337
|
+
description: comparisonLabel,
|
|
338
|
+
},
|
|
339
|
+
statistic,
|
|
340
|
+
point: result.point,
|
|
341
|
+
ci: result.ci,
|
|
342
|
+
status: result.status,
|
|
343
|
+
refusal: refusalMessage,
|
|
344
|
+
effectiveN: result.effectiveN,
|
|
345
|
+
blockDays: result.blockDays,
|
|
346
|
+
blockDaysDerivation: blockDaysNote,
|
|
347
|
+
overlapWindow: result.overlapWindow,
|
|
348
|
+
observedDays: {
|
|
349
|
+
armA: countObserved(armA),
|
|
350
|
+
armB: armB !== null ? countObserved(armB) : null,
|
|
351
|
+
},
|
|
352
|
+
tradeCounts: {
|
|
353
|
+
armA: armATrades.length,
|
|
354
|
+
armB: armBTrades !== null ? armBTrades.length : null,
|
|
355
|
+
},
|
|
356
|
+
sensitivity: result.sensitivity,
|
|
357
|
+
resamples,
|
|
358
|
+
seed: result.seed,
|
|
359
|
+
ciLevel,
|
|
360
|
+
attributionMethodology: ATTRIBUTION_NOTE,
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
return { summary, data, result };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
// Tool registration
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Register the paired_bootstrap_comparison MCP tool.
|
|
372
|
+
*/
|
|
373
|
+
export function registerPairedComparisonTool(server: McpServer, baseDir: string): void {
|
|
374
|
+
server.registerTool(
|
|
375
|
+
"paired_bootstrap_comparison",
|
|
376
|
+
{
|
|
377
|
+
description:
|
|
378
|
+
"Honest confidence intervals for 'is strategy A actually different from strategy B " +
|
|
379
|
+
"(or from zero)' -- day-block resampling that accounts for multi-day positions, dormant " +
|
|
380
|
+
"periods, and paired comparison on shared days. " +
|
|
381
|
+
ATTRIBUTION_NOTE,
|
|
382
|
+
inputSchema: pairedComparisonInputSchema,
|
|
383
|
+
},
|
|
384
|
+
withSyncedBlock(baseDir, async (input: PairedComparisonInput) => {
|
|
385
|
+
try {
|
|
386
|
+
const { blockId, strategyA, strategyB, statistic, dateRange } = input;
|
|
387
|
+
const block = await loadBlock(baseDir, blockId);
|
|
388
|
+
|
|
389
|
+
const inRange = (trades: Trade[]): Trade[] =>
|
|
390
|
+
dateRange ? filterByDateRange(trades, dateRange.start, dateRange.end) : trades;
|
|
391
|
+
|
|
392
|
+
const blockTrades = inRange(block.trades);
|
|
393
|
+
const armATrades = inRange(filterByStrategy(block.trades, strategyA));
|
|
394
|
+
const armBTrades =
|
|
395
|
+
strategyB !== undefined ? inRange(filterByStrategy(block.trades, strategyB)) : null;
|
|
396
|
+
|
|
397
|
+
const { summary, data } = runPairedBootstrapComparison({
|
|
398
|
+
armATrades,
|
|
399
|
+
armBTrades,
|
|
400
|
+
blockTrades,
|
|
401
|
+
statistic,
|
|
402
|
+
ciLevel: input.ciLevel,
|
|
403
|
+
resamples: input.resamples,
|
|
404
|
+
seed: input.seed,
|
|
405
|
+
blockDays: input.blockDays,
|
|
406
|
+
effectiveNFloorBlocks: input.effectiveNFloorBlocks,
|
|
407
|
+
strategyA,
|
|
408
|
+
strategyB,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
return createToolOutput(summary, { blockId, ...data });
|
|
412
|
+
} catch (error) {
|
|
413
|
+
return {
|
|
414
|
+
content: [
|
|
415
|
+
{
|
|
416
|
+
type: "text" as const,
|
|
417
|
+
text: `Error running paired bootstrap comparison: ${(error as Error).message}`,
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
isError: true as const,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}),
|
|
424
|
+
);
|
|
425
|
+
}
|