canli-validation-mcp 0.5.0 → 0.7.0

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/src/schemas.mjs CHANGED
@@ -8,6 +8,53 @@
8
8
  // validator semantics these shapes mirror.
9
9
  import { z } from "zod";
10
10
 
11
+ // One description per input field, shared by every shape that uses the field, so a client reads
12
+ // the same meaning, unit and example wherever the field appears. Agents choose arguments from
13
+ // these; a bare "number" leaves units (fraction or percent, kurtosis or excess kurtosis) to guess.
14
+ export const FIELD_DESCRIPTIONS = Object.freeze({
15
+ observed_sharpe_annualized: "Annualized Sharpe as observed.",
16
+ observations: "Number of return observations.",
17
+ periods_per_year: "Observations per year: 252 daily, 365 daily crypto, 52 weekly, 12 monthly.",
18
+ skew: "Skewness of returns; 0 if Normal.",
19
+ non_excess_kurtosis: "Kurtosis, not excess kurtosis; 3 if Normal.",
20
+ effective_independent_trials: "Independent variants tried before choosing this one.",
21
+ cross_trial_sharpe_sd_annualized: "Standard deviation of the annualized Sharpe across those trials.",
22
+ returns: "Periodic returns as fractions (0.01 is 1%), oldest first; replaces the Sharpe, observations, skew and kurtosis fields.",
23
+ matrix: "Returns as fractions: one row per period, one column per variant.",
24
+ n_splits: "Even number of blocks, at least 2; default 16.",
25
+ max_combinations: "Most splits evaluated, up to 2000 (default).",
26
+ seed: "Sampling seed; default 42.",
27
+ record: "A canli.paper-evidence.v0 record.",
28
+ sleeve_sharpe: "Annualized Sharpe of one sleeve.",
29
+ average_pairwise_correlation: "Average correlation between sleeves, -1 to 1.",
30
+ sleeves: "Sleeve count, to get that book's Sharpe.",
31
+ target: "Target book Sharpe, to get the sleeves it needs.",
32
+ benchmark_sharpe_annualized: "Annualized Sharpe to beat; default 0.",
33
+ confidence: "Between 0 and 1; default 0.95.",
34
+ record_observations: "Record length so far, to get its probabilistic Sharpe.",
35
+ label: "Name for the key.",
36
+ receipt_id: "Receipt id from a validation result.",
37
+ receipt_object: "A receipt as get_receipt returns it (its data), to verify without fetching it.",
38
+ cik: "SEC CIK; send cik or ticker.",
39
+ ticker: "Ticker such as AAPL; send ticker or cik.",
40
+ concept: "us-gaap concept such as Assets; omit to list them.",
41
+ limit: "Most observations, newest first; default 40.",
42
+ target_sharpe_annualized: "In-sample annualized Sharpe you would take as a discovery; default 1.",
43
+ backtest_years: "Length of the backtest in years, to get the most independent trials it allows.",
44
+ bt_trials: "Independent trials (backtests, parameter sets, ideas) tried; gives the minimum backtest length.",
45
+ hc_observations: "Number of return observations behind the Sharpe ratio.",
46
+ hc_tests: "Total tests run, this one included; gives the Bonferroni and independent-test haircuts.",
47
+ autocorrelation: "First-order autocorrelation of the returns, -1 to 1; default 0. Corrects the annualized Sharpe as Lo (2002).",
48
+ other_sharpes: "Annualized Sharpe ratios of the other tests, over the same observations; adds the Holm and BHY haircuts.",
49
+ lt_trials: "Independent trials tried; adds the chance that the best of them reached this Sharpe by luck.",
50
+ skew: "Skewness of the returns; below -0.5 the reading warns that the counts are too generous.",
51
+ variants: "Optional returns of every variant tried, this one included, as fractions: one row per period, one column per variant. Adds the overfitting check.",
52
+ returns_file: "Path to a CSV or JSON file of the returns on the machine running this server, instead of returns. Not available on the hosted endpoint.",
53
+ returns_column: "Header name or 1-based position of the returns column when returns_file has several numeric columns.",
54
+ variants_file: "Path to a CSV or JSON file of every variant's returns (one numeric column per variant), instead of variants.",
55
+ });
56
+ const d = FIELD_DESCRIPTIONS;
57
+
11
58
  // ---------------------------------------------------------------------------------------------
12
59
  // validate_deflated_sharpe: the API accepts exactly one of two input modes, never a mix.
13
60
  // Input A is the seven fields of deflated_sharpe_calculator_contract.json.
@@ -16,22 +63,22 @@ import { z } from "zod";
16
63
 
17
64
  export const deflatedSharpeContractInputs = z
18
65
  .object({
19
- observed_sharpe_annualized: z.number().min(-10).max(10),
20
- observations: z.number().int().min(2).max(1000000),
21
- periods_per_year: z.number().min(1).max(10000),
22
- skew: z.number().min(-20).max(20),
23
- non_excess_kurtosis: z.number().min(1).max(100),
24
- effective_independent_trials: z.number().int().min(2).max(10000000),
25
- cross_trial_sharpe_sd_annualized: z.number().min(0).max(10),
66
+ observed_sharpe_annualized: z.number().min(-10).max(10).describe(d.observed_sharpe_annualized),
67
+ observations: z.number().int().min(2).max(1000000).describe(d.observations),
68
+ periods_per_year: z.number().min(1).max(10000).describe(d.periods_per_year),
69
+ skew: z.number().min(-20).max(20).describe(d.skew),
70
+ non_excess_kurtosis: z.number().min(1).max(100).describe(d.non_excess_kurtosis),
71
+ effective_independent_trials: z.number().int().min(2).max(10000000).describe(d.effective_independent_trials),
72
+ cross_trial_sharpe_sd_annualized: z.number().min(0).max(10).describe(d.cross_trial_sharpe_sd_annualized),
26
73
  })
27
74
  .strict();
28
75
 
29
76
  export const deflatedSharpeReturnSeriesInputs = z
30
77
  .object({
31
- returns: z.array(z.number()).min(2).max(20000),
32
- periods_per_year: z.number().min(1).max(10000),
33
- effective_independent_trials: z.number().int().min(2).max(10000000),
34
- cross_trial_sharpe_sd_annualized: z.number().min(0).max(10),
78
+ returns: z.array(z.number()).min(2).max(20000).describe(d.returns),
79
+ periods_per_year: z.number().min(1).max(10000).describe(d.periods_per_year),
80
+ effective_independent_trials: z.number().int().min(2).max(10000000).describe(d.effective_independent_trials),
81
+ cross_trial_sharpe_sd_annualized: z.number().min(0).max(10).describe(d.cross_trial_sharpe_sd_annualized),
35
82
  })
36
83
  .strict();
37
84
 
@@ -44,14 +91,14 @@ export const deflatedSharpeInput = z.union([deflatedSharpeContractInputs, deflat
44
91
  // per-field type checking) while the strict oneOf rule above is enforced in the tool handler.
45
92
  export const deflatedSharpeToolShape = z
46
93
  .object({
47
- observed_sharpe_annualized: z.number().min(-10).max(10).optional(),
48
- observations: z.number().int().min(2).max(1000000).optional(),
49
- periods_per_year: z.number().min(1).max(10000).optional(),
50
- skew: z.number().min(-20).max(20).optional(),
51
- non_excess_kurtosis: z.number().min(1).max(100).optional(),
52
- effective_independent_trials: z.number().int().min(2).max(10000000).optional(),
53
- cross_trial_sharpe_sd_annualized: z.number().min(0).max(10).optional(),
54
- returns: z.array(z.number()).min(2).max(20000).optional(),
94
+ observed_sharpe_annualized: z.number().min(-10).max(10).optional().describe(d.observed_sharpe_annualized),
95
+ observations: z.number().int().min(2).max(1000000).optional().describe(d.observations),
96
+ periods_per_year: z.number().min(1).max(10000).optional().describe(d.periods_per_year),
97
+ skew: z.number().min(-20).max(20).optional().describe(d.skew),
98
+ non_excess_kurtosis: z.number().min(1).max(100).optional().describe(d.non_excess_kurtosis),
99
+ effective_independent_trials: z.number().int().min(2).max(10000000).optional().describe(d.effective_independent_trials),
100
+ cross_trial_sharpe_sd_annualized: z.number().min(0).max(10).optional().describe(d.cross_trial_sharpe_sd_annualized),
101
+ returns: z.array(z.number()).min(2).max(20000).optional().describe(d.returns),
55
102
  })
56
103
  .strict();
57
104
 
@@ -61,10 +108,10 @@ export const deflatedSharpeToolShape = z
61
108
 
62
109
  export const overfittingInput = z
63
110
  .object({
64
- matrix: z.array(z.array(z.number())).min(2).max(20000),
65
- n_splits: z.number().int().positive().optional(),
66
- max_combinations: z.number().int().positive().max(2000).optional(),
67
- seed: z.number().optional(),
111
+ matrix: z.array(z.array(z.number())).min(2).max(20000).describe(d.matrix),
112
+ n_splits: z.number().int().positive().optional().describe(d.n_splits),
113
+ max_combinations: z.number().int().positive().max(2000).optional().describe(d.max_combinations),
114
+ seed: z.number().optional().describe(d.seed),
68
115
  })
69
116
  .strict();
70
117
 
@@ -75,7 +122,7 @@ export const overfittingInput = z
75
122
 
76
123
  export const paperEvidenceInput = z
77
124
  .object({
78
- record: z.record(z.string(), z.unknown()),
125
+ record: z.record(z.string(), z.unknown()).describe(d.record),
79
126
  })
80
127
  .strict();
81
128
 
@@ -85,10 +132,10 @@ export const paperEvidenceInput = z
85
132
 
86
133
  export const breadthInput = z
87
134
  .object({
88
- sleeve_sharpe: z.number().positive(),
89
- average_pairwise_correlation: z.number().min(-1).max(1),
90
- sleeves: z.number().int().min(1).max(500).optional(),
91
- target: z.number().positive().optional(),
135
+ sleeve_sharpe: z.number().positive().describe(d.sleeve_sharpe),
136
+ average_pairwise_correlation: z.number().min(-1).max(1).describe(d.average_pairwise_correlation),
137
+ sleeves: z.number().int().min(1).max(500).optional().describe(d.sleeves),
138
+ target: z.number().positive().optional().describe(d.target),
92
139
  })
93
140
  .strict();
94
141
 
@@ -98,52 +145,131 @@ export const breadthInput = z
98
145
 
99
146
  export const trackRecordInput = z
100
147
  .object({
101
- observed_sharpe_annualized: z.number().min(-10).max(10),
102
- periods_per_year: z.number().min(1).max(10000),
103
- skew: z.number().min(-20).max(20),
104
- non_excess_kurtosis: z.number().min(1).max(100),
105
- benchmark_sharpe_annualized: z.number().min(-10).max(10).optional(),
106
- confidence: z.number().gt(0).lt(1).optional(),
107
- observations: z.number().int().min(2).max(1000000).optional(),
148
+ observed_sharpe_annualized: z.number().min(-10).max(10).describe(d.observed_sharpe_annualized),
149
+ periods_per_year: z.number().min(1).max(10000).describe(d.periods_per_year),
150
+ skew: z.number().min(-20).max(20).describe(d.skew),
151
+ non_excess_kurtosis: z.number().min(1).max(100).describe(d.non_excess_kurtosis),
152
+ benchmark_sharpe_annualized: z.number().min(-10).max(10).optional().describe(d.benchmark_sharpe_annualized),
153
+ confidence: z.number().gt(0).lt(1).optional().describe(d.confidence),
154
+ observations: z.number().int().min(2).max(1000000).optional().describe(d.record_observations),
155
+ })
156
+ .strict();
157
+
158
+ // ---------------------------------------------------------------------------------------------
159
+ // validate_backtest_length
160
+ // ---------------------------------------------------------------------------------------------
161
+
162
+ export const backtestLengthInput = z
163
+ .object({
164
+ effective_independent_trials: z.number().int().min(2).max(1000000000).optional().describe(d.bt_trials),
165
+ backtest_years: z.number().gt(0).max(1000).optional().describe(d.backtest_years),
166
+ target_sharpe_annualized: z.number().gt(0).max(10).optional().describe(d.target_sharpe_annualized),
108
167
  })
109
168
  .strict();
110
169
 
170
+ // ---------------------------------------------------------------------------------------------
171
+ // validate_haircut_sharpe
172
+ // ---------------------------------------------------------------------------------------------
173
+
174
+ export const haircutSharpeInput = z
175
+ .object({
176
+ observed_sharpe_annualized: z.number().gt(0).max(10).describe(d.observed_sharpe_annualized),
177
+ periods_per_year: z.number().min(1).max(10000).describe(d.periods_per_year),
178
+ observations: z.number().int().min(3).max(1000000).describe(d.hc_observations),
179
+ tests: z.number().int().min(1).max(100000000).optional().describe(d.hc_tests),
180
+ autocorrelation: z.number().gt(-1).lt(1).optional().describe(d.autocorrelation),
181
+ other_sharpe_ratios_annualized: z.array(z.number().min(-10).max(10)).min(1).max(10000).optional().describe(d.other_sharpes),
182
+ })
183
+ .strict();
184
+
185
+ // ---------------------------------------------------------------------------------------------
186
+ // validate_luck_trials
187
+ // ---------------------------------------------------------------------------------------------
188
+
189
+ export const luckTrialsInput = z
190
+ .object({
191
+ observed_sharpe_annualized: z.number().min(-20).max(20).describe(d.observed_sharpe_annualized),
192
+ periods_per_year: z.number().min(1).max(10000).describe(d.periods_per_year),
193
+ observations: z.number().int().min(3).max(1000000).describe(d.hc_observations),
194
+ effective_independent_trials: z.number().int().min(1).max(1000000000).optional().describe(d.lt_trials),
195
+ skew: z.number().min(-100).max(100).optional().describe(d.skew),
196
+ autocorrelation: z.number().gt(-1).lt(1).optional().describe(d.autocorrelation),
197
+ })
198
+ .strict();
199
+
200
+ // ---------------------------------------------------------------------------------------------
201
+ // audit_backtest: the deflated Sharpe, track record and (with variants) overfitting checks on one
202
+ // return series in one call. Each check runs through its own validator, unchanged.
203
+ // ---------------------------------------------------------------------------------------------
204
+
205
+ export const auditBacktestToolShape = z
206
+ .object({
207
+ returns: z.array(z.number()).min(2).max(20000).optional().describe(d.returns),
208
+ returns_file: z.string().min(1).max(4096).optional().describe(d.returns_file),
209
+ returns_column: z.union([z.string().min(1).max(200), z.number().int().min(1)]).optional().describe(d.returns_column),
210
+ periods_per_year: z.number().min(1).max(10000).describe(d.periods_per_year),
211
+ effective_independent_trials: z.number().int().min(2).max(10000000).describe(d.effective_independent_trials),
212
+ cross_trial_sharpe_sd_annualized: z.number().min(0).max(10).describe(d.cross_trial_sharpe_sd_annualized),
213
+ benchmark_sharpe_annualized: z.number().min(-10).max(10).optional().describe(d.benchmark_sharpe_annualized),
214
+ confidence: z.number().gt(0).lt(1).optional().describe(d.confidence),
215
+ variants: z.array(z.array(z.number())).min(2).max(20000).optional().describe(d.variants),
216
+ variants_file: z.string().min(1).max(4096).optional().describe(d.variants_file),
217
+ n_splits: z.number().int().positive().optional().describe(d.n_splits),
218
+ })
219
+ .strict();
220
+
221
+ // The handler's rule on top of the advertised shape: the returns come from exactly one place, the
222
+ // variants from at most one, and a column is named only for a file.
223
+ export const auditBacktestInput = auditBacktestToolShape
224
+ .refine((v) => (v.returns === undefined) !== (v.returns_file === undefined), "Send exactly one of returns or returns_file")
225
+ .refine((v) => v.variants === undefined || v.variants_file === undefined, "Send variants or variants_file, not both")
226
+ .refine((v) => v.returns_column === undefined || v.returns_file !== undefined, "returns_column applies only to returns_file");
227
+
111
228
  // ---------------------------------------------------------------------------------------------
112
229
  // get_key / get_receipt
113
230
  // ---------------------------------------------------------------------------------------------
114
231
 
115
232
  export const getKeyInput = z
116
233
  .object({
117
- label: z.string().max(64).optional(),
234
+ label: z.string().max(64).optional().describe(d.label),
118
235
  })
119
236
  .strict();
120
237
 
121
238
  export const getReceiptInput = z
122
239
  .object({
123
- id: z.string().regex(/^[0-9a-f]{24}$/, "A receipt id is 24 hex characters"),
240
+ id: z.string().regex(/^[0-9a-f]{24}$/, "A receipt id is 24 hex characters").describe(d.receipt_id),
124
241
  })
125
242
  .strict();
126
243
 
127
244
  export const emptyInput = z.object({}).strict();
128
245
 
246
+ // verify_receipt: a receipt id to fetch, or a receipt already fetched (get_receipt's data), to check
247
+ // offline. Advertised flat; the handler enforces exactly one.
248
+ export const verifyReceiptToolShape = z
249
+ .object({
250
+ id: z.string().regex(/^[0-9a-f]{24}$/, "A receipt id is 24 hex characters").optional().describe(d.receipt_id),
251
+ receipt: z.record(z.string(), z.unknown()).optional().describe(d.receipt_object),
252
+ })
253
+ .strict();
254
+
129
255
  // company_financial_history reads the public company reference, not the validation API.
130
256
  // Advertised to clients as a flat object (a refinement has no useful JSON Schema); the handler
131
257
  // enforces the exactly-one-of rule with companyHistoryInput.
132
258
  export const companyHistoryToolShape = z
133
259
  .object({
134
- cik: z.string().regex(/^\d{1,10}$/, "A CIK is 1 to 10 digits").optional(),
135
- ticker: z.string().regex(/^[A-Za-z0-9.\-]{1,10}$/, "A ticker is 1 to 10 letters, digits, dots or hyphens").optional(),
136
- concept: z.string().regex(/^[A-Za-z][A-Za-z0-9]{0,99}$/, "A concept is a us-gaap tag such as Revenues or Assets").optional(),
137
- limit: z.number().int().min(1).max(200).optional(),
260
+ cik: z.string().regex(/^\d{1,10}$/, "A CIK is 1 to 10 digits").optional().describe(d.cik),
261
+ ticker: z.string().regex(/^[A-Za-z0-9.\-]{1,10}$/, "A ticker is 1 to 10 letters, digits, dots or hyphens").optional().describe(d.ticker),
262
+ concept: z.string().regex(/^[A-Za-z][A-Za-z0-9]{0,99}$/, "A concept is a us-gaap tag such as Revenues or Assets").optional().describe(d.concept),
263
+ limit: z.number().int().min(1).max(200).optional().describe(d.limit),
138
264
  })
139
265
  .strict();
140
266
 
141
267
  export const companyHistoryInput = z
142
268
  .object({
143
- cik: z.string().regex(/^\d{1,10}$/, "A CIK is 1 to 10 digits").optional(),
144
- ticker: z.string().regex(/^[A-Za-z0-9.\-]{1,10}$/, "A ticker is 1 to 10 letters, digits, dots or hyphens").optional(),
145
- concept: z.string().regex(/^[A-Za-z][A-Za-z0-9]{0,99}$/, "A concept is a us-gaap tag such as Revenues or Assets").optional(),
146
- limit: z.number().int().min(1).max(200).optional(),
269
+ cik: z.string().regex(/^\d{1,10}$/, "A CIK is 1 to 10 digits").optional().describe(d.cik),
270
+ ticker: z.string().regex(/^[A-Za-z0-9.\-]{1,10}$/, "A ticker is 1 to 10 letters, digits, dots or hyphens").optional().describe(d.ticker),
271
+ concept: z.string().regex(/^[A-Za-z][A-Za-z0-9]{0,99}$/, "A concept is a us-gaap tag such as Revenues or Assets").optional().describe(d.concept),
272
+ limit: z.number().int().min(1).max(200).optional().describe(d.limit),
147
273
  })
148
274
  .strict()
149
275
  .refine((v) => (v.cik === undefined) !== (v.ticker === undefined), "Send exactly one of cik or ticker");
@@ -161,7 +287,7 @@ export const COMPANY_REFERENCE_BOUNDARY = "Public company accounting reference,
161
287
  export const LIMITS_SENTENCES = Object.freeze({
162
288
  scope: "This verdict is about the series exactly as submitted. The service never saw the data source, its costs, survivorship, or any lookahead in how the series was built.",
163
289
  notAdmission: "A deflated Sharpe or overfitting probability above or below any threshold is not admission to anything and is not a forecast.",
164
- unsigned: "The receipt is content-hashed and reproducible from the open-source core it names. It is not signed.",
290
+ unsigned: "The receipt is content-hashed, reproducible from the open-source core it names, and signed with Ed25519 by a key published at https://canlicapital.com/.well-known/canli-receipt-keys.json.",
165
291
  quotas: "Quotas: 1000 validations per key per UTC day, 5 keys per client per UTC day, 1048576 bytes per validation request, 1024 bytes per key revocation request, 20000 observations per series, 200 variants per matrix.",
166
292
  });
167
293
 
@@ -177,12 +303,17 @@ export const REGISTRY_DESCRIPTION_MAX = 100;
177
303
  // calls the tool, not only inside the returned envelope.
178
304
  export const TOOL_DESCRIPTIONS = Object.freeze({
179
305
  get_key: `Issue a free canlicapital.com validation key (POST /api/v1/keys) and hold it in memory for this session. Only needed before a validation when neither CANLI_KEY nor local mode is set; the read tools (get_receipt, service_status, company_financial_history) never need a key. ${LIMITS_SENTENCES.quotas}`,
180
- validate_deflated_sharpe: `Probabilistic and deflated Sharpe from the seven contract inputs, or from a return series plus the trials and dispersion behind it, never both. ${LIMITS_SENTENCES.notAdmission}`,
181
- validate_overfitting: `Probability of backtest overfitting by CSCV over the returns of every variant tried. ${LIMITS_SENTENCES.notAdmission}`,
182
- validate_paper_evidence: `Conformance of a performance record against the canli.paper-evidence.v0 standard. ${LIMITS_SENTENCES.scope}`,
183
- validate_track_record: `Minimum track record length for an observed Sharpe to clear a benchmark Sharpe (default 0) at a confidence level (default 0.95), and, when observations is sent, the probabilistic Sharpe of that record against the benchmark. ${LIMITS_SENTENCES.notAdmission}`,
184
- validate_breadth: `Book Sharpe ceiling from per-sleeve quality and average pairwise correlation, and the sleeves a target needs. ${LIMITS_SENTENCES.scope}`,
185
- get_receipt: `Fetch a stored verdict by its content-hash id (GET /api/v1/receipts/{id}), immutable and cacheable. ${LIMITS_SENTENCES.unsigned}`,
186
- service_status: `Service, store and quota constants for the validation API (GET /api/v1/validate/status); no key required. ${LIMITS_SENTENCES.scope}`,
306
+ validate_deflated_sharpe: `Whether a Sharpe survives the number of variants tried: probabilistic and deflated Sharpe (0 to 1) and the Sharpe luck alone would reach. Send the seven statistics or a return series, not both. ${LIMITS_SENTENCES.notAdmission}`,
307
+ audit_backtest: `Audit one strategy's return series in one call: deflated Sharpe, the minimum track record length for its Sharpe to beat the benchmark, and, with every variant's returns, the probability of backtest overfitting. Point returns_file at the backtest's CSV or JSON rather than copying long series into the call. Each check is the matching validate_ tool's result with its own receipt, side by side; the audit does not grade the strategy. Uses one validation per check. ${LIMITS_SENTENCES.notAdmission}`,
308
+ validate_overfitting: `Probability (0 to 1) that picking the best of several backtested variants was overfitting, by CSCV over every variant's returns. ${LIMITS_SENTENCES.notAdmission}`,
309
+ validate_paper_evidence: `Whether a paper or simulated performance record meets canli.paper-evidence.v0, with each failure's JSON pointer. ${LIMITS_SENTENCES.scope}`,
310
+ validate_backtest_length: `Minimum backtest length, in years, before the best of N independent trials is not expected to reach a target Sharpe by luck, and with backtest_years, the most independent trials those years allow. Send effective_independent_trials, backtest_years, or both. ${LIMITS_SENTENCES.notAdmission}`,
311
+ validate_luck_trials: `How many skill-less strategies a search would have had to try for its best to reach this Sharpe by luck, and, with a trial count, the chance that it did. Calibrated by Monte Carlo. ${LIMITS_SENTENCES.notAdmission}`,
312
+ validate_haircut_sharpe: `Haircut Sharpe ratio for multiple testing (Harvey and Liu, 2015): the Sharpe a single test would have needed once the number of tests is counted, by Bonferroni and for independent tests, and with the other tests' Sharpe ratios by Holm and BHY. ${LIMITS_SENTENCES.notAdmission}`,
313
+ validate_track_record: `Minimum track record length, in observations and years, for an observed Sharpe to beat a benchmark at a confidence level, and with observations, the record's probabilistic Sharpe so far. ${LIMITS_SENTENCES.notAdmission}`,
314
+ validate_breadth: `Highest book Sharpe reachable by adding sleeves of this quality and correlation, the Sharpe at a sleeve count, and the sleeves a target needs. ${LIMITS_SENTENCES.scope}`,
315
+ verify_receipt: `Check a validation receipt's Ed25519 signature offline against the canlicapital.com public key bundled in this package, that its output hashes to its output_sha256, and that its content hashes to its id. Send an id to fetch the receipt first, or a receipt already fetched. ${LIMITS_SENTENCES.unsigned}`,
316
+ get_receipt: `Fetch a stored verdict by receipt id (GET /api/v1/receipts/{id}) to re-check an earlier result. No key. ${LIMITS_SENTENCES.unsigned}`,
317
+ service_status: `Whether the validation API is up, with quota constants (GET /api/v1/validate/status); check after a timeout before resubmitting. No key. ${LIMITS_SENTENCES.scope}`,
187
318
  company_financial_history: `SEC-reported financial history for one company from the canlicapital.com company reference (GET /company-data/{cik}.json), by cik or by ticker (resolved through GET /api/v1/company-tickers.json, companies in the release only). Without a concept it lists the available histories; with one it returns observations, newest first, each with its filing accession, form, filed date and unit, plus the SHA-256 of the original SEC response. No key required. ${COMPANY_REFERENCE_BOUNDARY}`,
188
319
  });
@@ -0,0 +1,124 @@
1
+ // mcp/src/series-file.mjs
2
+ //
3
+ // Reads a return series, or a matrix of variant returns, from a file on the machine the server runs
4
+ // on, so an agent can point a tool at its backtest output instead of copying hundreds of numbers
5
+ // into the call (the agent benchmark measured transcription errors on long pasted series).
6
+ //
7
+ // Only numbers leave this module. Error messages name rows and columns by position, never by a
8
+ // cell's content or a header's text, so a path pointed at the wrong file cannot echo that file back
9
+ // into the conversation. The hosted endpoint never reads files (see toolAuditBacktest).
10
+ import { closeSync, fstatSync, openSync, readSync } from "node:fs";
11
+ import { resolve } from "node:path";
12
+
13
+ export const MAX_SERIES_FILE_BYTES = 5 * 1024 * 1024;
14
+
15
+ // One open file descriptor for the check and the read, so the file checked is the file read: a
16
+ // path swapped between a separate stat and read could otherwise pass the size and type checks as
17
+ // one file and be read as another. The read stops one byte past the cap, whatever the file grows to.
18
+ function readText(path) {
19
+ let fd;
20
+ try {
21
+ fd = openSync(resolve(path), "r");
22
+ } catch {
23
+ throw new Error(`${path}: no such file`);
24
+ }
25
+ try {
26
+ const stat = fstatSync(fd);
27
+ if (!stat.isFile()) throw new Error(`${path}: not a regular file`);
28
+ if (stat.size > MAX_SERIES_FILE_BYTES) throw new Error(`${path}: larger than ${MAX_SERIES_FILE_BYTES} bytes`);
29
+ const buffer = Buffer.alloc(MAX_SERIES_FILE_BYTES + 1);
30
+ let length = 0;
31
+ for (;;) {
32
+ const read = readSync(fd, buffer, length, buffer.length - length, null);
33
+ if (read === 0) break;
34
+ length += read;
35
+ if (length > MAX_SERIES_FILE_BYTES) throw new Error(`${path}: larger than ${MAX_SERIES_FILE_BYTES} bytes`);
36
+ }
37
+ return buffer.toString("utf8", 0, length);
38
+ } finally {
39
+ closeSync(fd);
40
+ }
41
+ }
42
+
43
+ const isNumber = (cell) => cell.trim() !== "" && Number.isFinite(Number(cell.trim()));
44
+
45
+ // A JSON array (of numbers, or of arrays of numbers), or delimited text: comma, semicolon or tab
46
+ // separated, one row per line, with an optional header row naming the columns.
47
+ function parseTable(text, path) {
48
+ const trimmed = text.trim();
49
+ if (trimmed.startsWith("[")) {
50
+ let value;
51
+ try {
52
+ value = JSON.parse(trimmed);
53
+ } catch {
54
+ throw new Error(`${path}: starts like JSON but does not parse as JSON`);
55
+ }
56
+ if (!Array.isArray(value)) throw new Error(`${path}: JSON must be an array`);
57
+ const rows = value.map((row) => (Array.isArray(row) ? row : [row]));
58
+ rows.forEach((row, i) => row.forEach((cell, j) => {
59
+ if (typeof cell !== "number" || !Number.isFinite(cell)) throw new Error(`${path}: JSON item ${i + 1}, position ${j + 1} is not a finite number`);
60
+ }));
61
+ return { header: null, rows };
62
+ }
63
+ const lines = trimmed.split(/\r?\n/).filter((line) => line.trim() !== "");
64
+ if (!lines.length) throw new Error(`${path}: empty`);
65
+ const delimiter = [",", ";", "\t"].find((d) => lines[0].includes(d)) ?? ",";
66
+ const split = (line) => line.split(delimiter).map((cell) => cell.trim());
67
+ const first = split(lines[0]);
68
+ const header = first.some((cell) => !isNumber(cell)) ? first : null;
69
+ const body = header ? lines.slice(1) : lines;
70
+ const width = (header ?? first).length;
71
+ const rows = body.map((line, i) => {
72
+ const cells = split(line);
73
+ if (cells.length !== width) throw new Error(`${path}: row ${i + 1 + (header ? 1 : 0)} has ${cells.length} cells, expected ${width}`);
74
+ return cells;
75
+ });
76
+ return { header, rows };
77
+ }
78
+
79
+ // A row counter, such as the unnamed index pandas writes first: an empty header, or whole numbers
80
+ // that step by exactly one. No return series looks like that, so it is skipped, and the caller
81
+ // reports the skip rather than hiding it.
82
+ function isRowCounter(name, values) {
83
+ if (name === "") return true;
84
+ return values.length >= 3 && values.every((v, i) => Number.isInteger(v) && (i === 0 || v - values[i - 1] === 1));
85
+ }
86
+
87
+ // Numeric columns only: a date or label column is dropped, never parsed; a row counter is skipped.
88
+ function numericColumns({ header, rows }) {
89
+ const width = rows[0]?.length ?? 0;
90
+ const columns = [];
91
+ const skipped = [];
92
+ for (let j = 0; j < width; j += 1) {
93
+ if (rows.every((row) => typeof row[j] === "number" || isNumber(String(row[j])))) {
94
+ const values = rows.map((row) => Number(row[j]));
95
+ if (isRowCounter(header ? header[j] : null, values)) skipped.push(j + 1);
96
+ else columns.push({ index: j, name: header ? header[j] : String(j + 1), values });
97
+ }
98
+ }
99
+ return { columns, skipped };
100
+ }
101
+
102
+ // One series. With several numeric columns, `column` (a header name or a 1-based index) picks it.
103
+ // Returns the values and the positions of any row-counter columns skipped.
104
+ export function readSeriesFile(path, column) {
105
+ const table = parseTable(readText(path), path);
106
+ if (!table.rows.length) throw new Error(`${path}: no data rows`);
107
+ const { columns, skipped } = numericColumns(table);
108
+ if (!columns.length) throw new Error(`${path}: no column holds only numbers`);
109
+ if (column === undefined) {
110
+ if (columns.length > 1) throw new Error(`${path}: ${columns.length} numeric columns (positions ${columns.map((c) => c.index + 1).join(", ")}); pick one with returns_column, by header name or position`);
111
+ return { values: columns[0].values, skipped };
112
+ }
113
+ const picked = columns.find((c) => c.name === String(column)) ?? columns.find((c) => String(c.index + 1) === String(column));
114
+ if (!picked) throw new Error(`${path}: returns_column matches no numeric column; numeric columns are at positions ${columns.map((c) => c.index + 1).join(", ")}`);
115
+ return { values: picked.values, skipped };
116
+ }
117
+
118
+ // Every numeric column is one variant; rows are periods.
119
+ export function readMatrixFile(path) {
120
+ const table = parseTable(readText(path), path);
121
+ const { columns, skipped } = numericColumns(table);
122
+ if (columns.length < 2) throw new Error(`${path}: needs at least 2 numeric columns, one per variant`);
123
+ return { matrix: table.rows.map((_, i) => columns.map((c) => c.values[i])), skipped };
124
+ }