canli-validation-mcp 0.10.1 → 0.12.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/README.md CHANGED
@@ -71,9 +71,19 @@ repository for the full design.
71
71
 
72
72
  ## Tools
73
73
 
74
+ Unreleased source adds local journal binding to `validate_paper_evidence`: run
75
+ `CANLI_LOCAL=1 node mcp/src/server.mjs` from the repository, and send exactly one of `record`
76
+ or `record_file`, plus `journal_file` to check source hashes, signatures and recomputed claims.
77
+ `record_file` accepts a standard record, a `{record,signature}` envelope, or the full execution
78
+ export bundle; companion metrics/observations are checked for full bundles. Signed records
79
+ need the detached signature. `bindings.all_match` must be true for source validation to pass.
80
+ Without a journal, the result explicitly reports structure-only conformance. Files and
81
+ signatures are refused in hosted/remote mode and never uploaded. Local results have no Canli
82
+ receipt. The current npm/hosted release retains the existing record-only API.
83
+
74
84
  | Tool | Calls | Key required |
75
85
  |---|---|---|
76
- | `get_key` | `POST /api/v1/keys` | no |
86
+ | `get_key` | `POST /api/v1/keys` (listed only when no key is configured) | no |
77
87
  | `validate_deflated_sharpe` | `POST /api/v1/validate/deflated-sharpe` | yes |
78
88
  | `validate_overfitting` | `POST /api/v1/validate/overfitting` | yes |
79
89
  | `validate_paper_evidence` | `POST /api/v1/validate/paper-evidence` | yes |
@@ -83,9 +93,8 @@ repository for the full design.
83
93
  | `validate_haircut_sharpe` | `POST /api/v1/validate/haircut-sharpe` | yes |
84
94
  | `validate_luck_trials` | `POST /api/v1/validate/luck-trials` | yes |
85
95
  | `validate_reality_check` | `POST /api/v1/validate/reality-check` (a `matrix_file` is read on your machine and sent as numbers) | yes |
86
- | `audit_backtest` | the deflated Sharpe, track record and, with `variants`, overfitting routes, one validation each | yes |
87
- | `verify_receipt` | `GET /api/v1/receipts/{id}` when given an id; the checks run locally | no |
88
- | `get_receipt` | `GET /api/v1/receipts/{id}` | no |
96
+ | `audit_backtest` | the deflated Sharpe and track record routes, then the headline: with `variants`, the overfitting and data-snooping routes; without, luck trials. One validation each | yes |
97
+ | `verify_receipt` | `GET /api/v1/receipts/{id}` when given an id (`include_receipt` returns the stored receipt); the checks run locally | no |
89
98
  | `service_status` | `GET /api/v1/validate/status` | no |
90
99
  | `company_financial_history` | `GET /company-data/{cik}.json` (a ticker resolves through `GET /api/v1/company-tickers.json`) | no |
91
100
 
@@ -127,14 +136,72 @@ original SEC response and the record's own boundary sentence: these are accounti
127
136
  reported to the SEC, not market prices, returns or a recommendation. Companies and concepts
128
137
  outside the current release return an error with the available concepts listed.
129
138
 
139
+ ## The lab: backtest, summarize, stress, check feasibility
140
+
141
+ Four tools compute in the server process itself, on your machine or on the hosted endpoint, with
142
+ nothing sent to the API and no receipt stored. Each result names the SHA-256 of exactly the numbers
143
+ it was computed from, and states what it does not establish.
144
+
145
+ - **`backtest_strategy`** runs a rule over every parameter set in a grid, on your prices:
146
+ `sma_cross` (fast, slow), `momentum` (lookback, skip), `mean_reversion` (window, entry_z),
147
+ `breakout` (lookback) or `buy_and_hold`, long-only or with `allow_short`, after a cost per unit
148
+ of turnover. The position held over each period is decided from closes up to the one before it,
149
+ so no rule can see the future (a test changes later prices and checks that no earlier position
150
+ moves). It then validates the best variant with the number of variants **this call ran**, not a
151
+ number someone declared: the deflated Sharpe ratio across the grid's measured Sharpe dispersion,
152
+ the CSCV probability of backtest overfitting on every variant's returns, the probabilistic Sharpe
153
+ and the minimum track record, next to buy and hold over the same window. At most 200 variants and
154
+ 20,000 prices per call. No code is ever run: a strategy is a family plus numbers.
155
+ - **`summarize_series`** reads a long price or return series and states it in about a hundred words,
156
+ plus a field for every figure: growth, volatility, Sharpe and drift t-statistic, the worst
157
+ drawdown with its dates and recovery, trend, the volatility regime (terciles of its own 21-period
158
+ volatility), tails, jumps (beyond six robust standard deviations), autocorrelation, stale data and,
159
+ with a benchmark, correlation and beta. A five-year daily series is about 1,260 numbers; the
160
+ summary is a paragraph.
161
+ - **`stress_test`** resamples a strategy's returns with a stationary block bootstrap (seeded, so a run
162
+ reproduces exactly) and reports how often the drawdown limit breaks and the Sharpe turns negative,
163
+ then applies four named scenarios with their rules stated: a crash at the equity high (default the
164
+ larger of 20% and three times the worst period), volatility multiplied, the worst stretch lived
165
+ twice, and positions stuck for several periods after the worst one. The fragility share is the part
166
+ of resampled histories that break the drawdown limit or lose money on a risk-adjusted basis.
167
+ Generative models are not used: trained on one series they memorize it or invent dynamics nobody
168
+ can check.
169
+ - **`check_feasibility`** checks a plan against a real broker before it trades: the broker's published
170
+ order-rate limit against the rebalance's burst of orders (Alpaca 200 a minute, Interactive Brokers
171
+ about 50 a second), the minimum order (Alpaca 1 USD notional), US day-trading rules as they stand
172
+ in 2026 (FINRA retired the pattern day trader rule and its 25,000 USD minimum on 4 June 2026; firms
173
+ may phase in its replacement until 20 October 2027), T+1 settlement in cash accounts, each order's
174
+ share of daily volume, square-root market impact (coefficient Y, default 1) with every copy of the
175
+ same strategy counted, and the capital at which costs and impact eat the expected gross return.
176
+ Every broker fact carries the date it was checked and its source.
177
+
178
+ On your own machine, `prices_file`, `series_file` and `returns_file` take a CSV or JSON path instead
179
+ of the numbers. Only numbers are read, plus an ISO date column when the file has one, so results can
180
+ name the day a drawdown began.
181
+
130
182
  ## Auditing a backtest in one call
131
183
 
132
- `audit_backtest` takes one strategy's return series, the number of variants tried and their Sharpe
133
- dispersion, and optionally every variant's returns. It runs `validate_deflated_sharpe` on the
134
- series, then `validate_track_record` on the Sharpe, skew and kurtosis that check derived, then,
135
- with `variants`, `validate_overfitting`. Each check is exactly what its own tool returns, with its
136
- own receipt; the boundary sentences they share are stated once. A check that refuses (a Sharpe
137
- that cannot beat the benchmark has no minimum track record) is reported as that check's error.
184
+ `audit_backtest` takes one strategy's return series and, ideally, every variant's returns
185
+ (`variants` or `variants_file`, the dropped ones too); without them, the number of variants tried
186
+ and their Sharpe spread. The result opens with:
187
+
188
+ - `headline`: one test chosen because its false-positive rate was measured before anyone looked at
189
+ your numbers. Null Zoo v1 (`scripts/research/null-zoo/v1/` in the repository) drew hundreds of
190
+ thousands of simulated searches from nine return shapes with known ground truth; the bar and the
191
+ rule that picks the test were committed before the run. With variants the headline comes from
192
+ the data-snooping tests (`validate_reality_check`, with its receipt); without, from luck trials
193
+ with Lo's autocorrelation correction. Its sentence quotes the measured false-positive rate for
194
+ returns shaped like yours, and the worst across the nine shapes.
195
+ - `fix_next`: at most five next steps, each `{id, why, next}` with a stable id.
196
+ - `computed`: Lo's adjusted Sharpe, a stationary-bootstrap 95% interval for the Sharpe, the
197
+ haircut Sharpe, the minimum backtest length and, with variants, how many independent trials
198
+ they count as (Li and Ji) and the selected variant's out-of-sample decay.
199
+
200
+ Then the checks: `validate_deflated_sharpe` on the series (an estimate), `validate_track_record`
201
+ on the Sharpe, skew and kurtosis that check derived and, with variants, `validate_overfitting`.
202
+ Each is exactly what its own tool returns, with its own receipt; the deflated Sharpe runs with the
203
+ larger of the declared and counted trials and Sharpe spread, so a search cannot be made to look
204
+ smaller than it was. A check that refuses or cannot be reached is reported as that check's error.
138
205
  The audit adds no grade of its own. Through the API it uses one validation per check.
139
206
 
140
207
  When the server runs on your machine, `returns_file` and `variants_file` take the path of the
@@ -147,11 +214,18 @@ took three audit questions from 4 of 9 to 8 of 9 answered correctly on gpt-5.4-m
147
214
 
148
215
  ## Prompts, resources and structured results
149
216
 
150
- Clients that show MCP prompts offer two guided workflows: `validate_backtest` (deflated Sharpe, then
151
- overfitting, then the track record needed, reported with what each number does not establish) and
152
- `track_record_needed`. Two resources can be read: `canli://limits`, the boundary sentences every
153
- result carries, and `canli://sources`, the papers behind each validator and how each is checked
154
- against them. Every tool result carries its envelope both as text and as `structuredContent`.
217
+ Clients that show MCP prompts offer six guided workflows: `validate_backtest` (deflated Sharpe, then
218
+ overfitting, then the track record needed, reported with what each number does not establish),
219
+ `track_record_needed`, `backtest_and_validate`, `stress_my_strategy`, `production_check` and
220
+ `summarize_market_series`. Resources: `canli://limits`, the boundary sentences every result carries;
221
+ `canli://sources`, the papers behind each validator and how each is checked against them;
222
+ `canli://strategy-spec`, the JSON Schema of every strategy family and its parameters; and
223
+ `canli://openapi`, the API's OpenAPI 3.1 document. Two resource templates are for writing code
224
+ against the tools: `canli://schemas/{tool}` returns a tool's exact input and output JSON Schema, and
225
+ `canli://examples/{language}/{tool}` a working call in `python`, `javascript` or `curl` (both
226
+ variables complete). A test compiles every generated Python and JavaScript example and checks that
227
+ every example's arguments are valid input. Every tool result carries its envelope both as text and
228
+ as `structuredContent`.
155
229
 
156
230
  ## Compact context (0.3.0)
157
231
 
@@ -183,7 +257,7 @@ an array of objects.
183
257
  | `CANLI_API_BASE` | `https://canlicapital.com` | Where the API lives. Point it at a preview deployment for testing. |
184
258
  | `CANLI_KEY` | unset | A key already issued from `POST /api/v1/keys`. When set, `get_key` sends no request and reports the key is already configured; every other tool sends it as `Authorization: Bearer <key>`. |
185
259
  | `CANLI_FULL_ENVELOPE` | unset | `1` or `true` returns each validation's full API envelope instead of the compact result (below). |
186
- | `CANLI_TOOLSETS` | all | Which tools to list: a comma-separated choice of `validate`, `receipts`, `company` and `status`, or `all`. An unknown name is refused at startup. See "Toolsets" below. |
260
+ | `CANLI_TOOLSETS` | all | Which tools to list: a comma-separated choice of `validate`, `receipts`, `company`, `status` and `lab`, or `all`. An unknown name is refused at startup. See "Toolsets" below. |
187
261
  | `CANLI_LOCAL` | unset | `1` or `true` runs the eight validators on this machine (private local mode, below): no key, no network, no receipt. |
188
262
 
189
263
  If `CANLI_KEY` is not set and local mode is off, call `get_key` once per session before the validators. The key it
@@ -267,7 +341,7 @@ equals the hosted one; it names no receipt id because none was made.
267
341
  claude mcp add canli-local --env CANLI_LOCAL=1 -- npx -y canli-validation-mcp
268
342
  ```
269
343
 
270
- `get_receipt`, `service_status` and `company_financial_history` still read from canlicapital.com;
344
+ `verify_receipt` by id, `service_status` and `company_financial_history` still read from canlicapital.com;
271
345
  they send no series. In the Claude Desktop extension this is the "Private local mode" setting.
272
346
 
273
347
  ## Generic stdio client
@@ -332,33 +406,39 @@ A validation result is the answer (`data`), the sentences above except the quota
332
406
  receipt's id and URL; an error keeps its error. The rest of the API envelope (schema, endpoint,
333
407
  timestamps, claim and capital class, the human page, the source-file hashes and the quota line)
334
408
  describes the service rather than the answer, and an agent pays for every token of it on every
335
- call. It stays in the stored receipt, which `get_receipt` returns in full, and in `service_status`.
409
+ call. It stays in the stored receipt, which `verify_receipt` returns in full with `include_receipt`, and in `service_status`.
336
410
  On a breadth result this is about half the text. Set `CANLI_FULL_ENVELOPE=1` to receive every field.
337
411
 
338
412
  ## Toolsets (tokens)
339
413
 
340
414
  A client sends the model the whole tool list on every turn, and it is most of each turn's prompt:
341
- a validation result is a few hundred tokens, the list of all fifteen tools several thousand. A
342
- client that needs one kind of tool can list only that kind, with `CANLI_TOOLSETS` (stdio) or
343
- `?toolsets=` (hosted endpoint). The default is every tool.
415
+ a validation result is a few hundred tokens, the list of every tool several thousand. A client
416
+ that needs one kind of tool can list only that kind, with `CANLI_TOOLSETS` (stdio) or
417
+ `?toolsets=` (hosted endpoint). The default is every toolset except `company`, whose one tool
418
+ canli-fundamentals-mcp does better and point in time; `all` adds it. `get_key` is listed only when
419
+ no key is configured.
344
420
 
345
421
  | toolset | tools |
346
422
  |---|---|
347
423
  | `validate` | `get_key`, the eight validators, `audit_backtest` |
348
- | `receipts` | `get_receipt`, `verify_receipt` |
424
+ | `receipts` | `verify_receipt` |
349
425
  | `company` | `company_financial_history` |
350
426
  | `status` | `service_status` |
427
+ | `lab` | `backtest_strategy`, `summarize_series`, `stress_test`, `check_feasibility` |
351
428
 
352
429
  Measured with `bench/tool_list_tokens.py` (tokenizer: tiktoken `o200k_base`; other models'
353
430
  tokenizers give different absolute counts), in the shape an OpenAI-style client sends the list:
354
431
 
355
432
  | CANLI_TOOLSETS | tools | tokens per turn | of all |
356
433
  |---|---|---|---|
357
- | `all` | 15 | 4,194 | 100% |
358
- | `validate` | 11 | 3,526 | 84% |
359
- | `receipts` | 2 | 312 | 7% |
360
- | `company` | 1 | 273 | 7% |
434
+ | (default) | 17 | 5,581 | 95% |
435
+ | (default, with CANLI_KEY) | 16 | 5,445 | 93% |
436
+ | `all` | 18 | 5,852 | 100% |
437
+ | `validate` | 11 | 3,708 | 63% |
438
+ | `receipts` | 1 | 195 | 3% |
439
+ | `company` | 1 | 273 | 5% |
361
440
  | `status` | 1 | 89 | 2% |
441
+ | `lab` | 4 | 1,595 | 27% |
362
442
 
363
443
  Providers cache a tool list that is identical from turn to turn and bill the cached part at a
364
444
  fraction of the price (`test/tool-list-stable.test.mjs` keeps each list byte-stable); a smaller list
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canli-validation-mcp",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "Check whether a backtest is real: deflated Sharpe, CSCV probability of backtest overfitting, minimum track record and backtest length, haircut Sharpe and luck-equivalent trials, as an MCP server. Local mode, hosted endpoint, signed receipts.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -15,7 +15,10 @@
15
15
  "overfitting",
16
16
  "quantitative-finance",
17
17
  "quant",
18
- "trading"
18
+ "trading",
19
+ "backtest-engine",
20
+ "stress-test",
21
+ "market-impact"
19
22
  ],
20
23
  "private": false,
21
24
  "type": "module",
@@ -0,0 +1,35 @@
1
+ import { resolve } from 'node:path';
2
+ import { computeLocally } from './local.mjs';
3
+ import { MAX_JOURNAL_BYTES, readBoundedFile, readRecordFile } from './local/js/journal-files.js';
4
+ import { journalBindings } from './local/js/trade-journal-export-core.js';
5
+
6
+ export function validateLocalJournalEvidence(input) {
7
+ let record = input.record, signature = input.signature, bundle;
8
+ if (input.record_file) {
9
+ const file = readRecordFile(resolve(input.record_file));
10
+ if (file && typeof file === 'object' && Object.hasOwn(file, 'record')) {
11
+ record = file.record;
12
+ // A {record,signature} envelope is also accepted. Full exports advertise their
13
+ // financial companion fields and those must be recomputed, not ignored.
14
+ if (Object.keys(file).some(k => !['record', 'signature'].includes(k))) bundle = file;
15
+ if (signature !== undefined && file.signature !== undefined) throw new Error('send a detached signature inline or in the record file, not both');
16
+ signature ??= file.signature;
17
+ } else record = file;
18
+ if (!record || typeof record !== 'object' || Array.isArray(record) || record.schema !== 'canli.paper-evidence.v0') throw new Error('record file must contain a canli.paper-evidence.v0 record or export bundle');
19
+ }
20
+ const result = computeLocally('validate_paper_evidence', { record });
21
+ if (result.failed) return result;
22
+ const data = result.envelope.data;
23
+ const unchecked = { checked: false, all_match: null, reason: 'No source journal checked; conformance covers structure and field relationships only.' };
24
+ // Conformance first prevents malformed record members from reaching the financial verifier.
25
+ const bindings = input.journal_file && data.valid
26
+ ? journalBindings(record, readBoundedFile(resolve(input.journal_file), { maxBytes: MAX_JOURNAL_BYTES }), signature, bundle)
27
+ : input.journal_file ? { checked: false, all_match: false, reason: 'Source verification requires a conforming record.' } : unchecked;
28
+ data.bindings = bindings;
29
+ data.conformance_valid = data.valid;
30
+ if (input.journal_file) {
31
+ data.valid = data.valid && bindings.all_match === true;
32
+ data.plain_reading = data.valid ? 'The record conforms and its reconstructable claims match the supplied signed journal and selected range. Key possession is self-attestation; broker authenticity, completeness and trusted time are not established.' : 'Conformance or source-bound recomputation failed; this record has not passed journal verification.';
33
+ } else data.plain_reading += ' No source journal or record signature was checked.';
34
+ return result;
35
+ }
@@ -0,0 +1,122 @@
1
+ // mcp/src/lab-schemas.mjs
2
+ //
3
+ // Input and output schemas for the lab tools (src/lab.mjs): backtest_strategy, summarize_series,
4
+ // stress_test and check_feasibility. Like the validators' descriptions, each tool description
5
+ // carries one sentence of its own result's boundary language verbatim (test/lab.test.mjs checks
6
+ // it against the sentences the computation attaches), so an agent reads what the result cannot be
7
+ // used to claim before it ever calls the tool. Descriptions stay terse: the tool list is re-sent to
8
+ // the model on every turn.
9
+ import { z } from "zod";
10
+ import { BACKTEST_LIMITS_TEXT, FAMILIES } from "./local/js/backtest-core.js";
11
+ import { FEASIBILITY_LIMITS_TEXT } from "./local/js/feasibility-core.js";
12
+ import { SUMMARY_LIMITS_TEXT } from "./local/js/series-summary-core.js";
13
+ import { STRESS_LIMITS_TEXT } from "./local/js/stress-core.js";
14
+
15
+ export const LAB_BOUNDARY = Object.freeze({
16
+ backtest_strategy: BACKTEST_LIMITS_TEXT[3],
17
+ summarize_series: SUMMARY_LIMITS_TEXT[2],
18
+ stress_test: STRESS_LIMITS_TEXT[2],
19
+ check_feasibility: FEASIBILITY_LIMITS_TEXT[0],
20
+ });
21
+
22
+ export const LAB_TOOL_DESCRIPTIONS = Object.freeze({
23
+ backtest_strategy: `Backtest a rule on your prices over a whole parameter grid, then validate the best with the count of variants actually run: deflated Sharpe, overfitting probability, minimum track record, vs buy and hold, after costs. ${LAB_BOUNDARY.backtest_strategy}`,
24
+ summarize_series: `A long price or return series in about 100 words plus fields: growth, risk, dated drawdowns, trend, volatility regime, tails, jumps, stale data. Use instead of reading raw bars. ${LAB_BOUNDARY.summarize_series}`,
25
+ stress_test: `Stress a strategy's returns: bootstrap histories (how often the drawdown limit breaks or Sharpe turns negative) and named crash, volatility, repeat and stuck-position scenarios, with a fragility share. ${LAB_BOUNDARY.stress_test}`,
26
+ check_feasibility: `Check a trading plan before it trades: broker order-rate and minimum-order limits, 2026 US day-trading rules, square-root market impact with crowding, and the capital where costs eat the return. ${LAB_BOUNDARY.check_feasibility}`,
27
+ });
28
+
29
+ const FAMILY_NAMES = Object.keys(FAMILIES);
30
+ // Lengths, counts and magnitudes are checked by the computation itself (src/local/js/*-core.js),
31
+ // with messages that say what to send instead; the advertised schema keeps types, enums and the
32
+ // bounds that tell a model what a value means (fractions, signs), because every bound here is
33
+ // re-sent to the model on every turn.
34
+ const series = () => z.array(z.number());
35
+ const file = (what) => z.string().describe(`CSV or JSON of ${what} on this machine; local server only.`);
36
+ const column = z.union([z.string(), z.number().int()]).describe("Column name or 1-based position.");
37
+ const periods = z.number().describe("252 daily (default), 365 crypto, 52 weekly, 12 monthly.");
38
+ const dates = z.array(z.string()).describe("Optional ISO dates, one per value.");
39
+
40
+ export const backtestInput = z
41
+ .object({
42
+ family: z.enum(FAMILY_NAMES).describe("sma_cross (fast, slow), momentum (lookback, skip), mean_reversion (window, entry_z), breakout (lookback), buy_and_hold."),
43
+ grid: z.record(z.string(), z.union([z.number(), z.array(z.number())])).optional()
44
+ .describe('Values per parameter, e.g. {"fast":[10,20],"slow":[50,200]}; every valid combination runs (max 200).'),
45
+ prices: series().optional().describe("Prices, oldest first."),
46
+ prices_file: file("prices").optional(),
47
+ prices_column: column.optional(),
48
+ dates: dates.optional(),
49
+ periods_per_year: periods.optional(),
50
+ cost_bps: z.number().min(0).optional().describe("Cost per unit of turnover, bps; default 5."),
51
+ allow_short: z.boolean().optional().describe("Short instead of flat; default false."),
52
+ })
53
+ .strict();
54
+
55
+ export const summarizeInput = z
56
+ .object({
57
+ prices: series().optional().describe("Prices, oldest first; or returns, or series_file."),
58
+ returns: z.array(z.number()).optional().describe("Returns as fractions, oldest first."),
59
+ series_file: file("prices or returns").optional(),
60
+ series_column: column.optional(),
61
+ series_kind: z.enum(["prices", "returns"]).optional().describe("series_file holds; default prices."),
62
+ dates: dates.optional(),
63
+ periods_per_year: periods.optional(),
64
+ benchmark_returns: z.array(z.number()).optional().describe("Same-period benchmark returns; adds beta."),
65
+ name: z.string().optional().describe("Label for the text."),
66
+ })
67
+ .strict();
68
+
69
+ export const stressInput = z
70
+ .object({
71
+ returns: z.array(z.number()).optional().describe("Strategy returns as fractions, oldest first."),
72
+ returns_file: file("returns").optional(),
73
+ returns_column: column.optional(),
74
+ periods_per_year: periods.optional(),
75
+ paths: z.number().int().optional().describe("Default 1000."),
76
+ block: z.number().optional().describe("Mean block length; default cube root of n."),
77
+ seed: z.number().int().optional().describe("Default 42; reproduces exactly."),
78
+ drawdown_limit: z.number().gt(0).lt(1).optional().describe("Fraction; default 0.2."),
79
+ crash: z.number().gt(-1).lt(0).optional().describe("Crash size, negative; default min(-0.2, 3x worst period)."),
80
+ volatility_multiplier: z.number().optional().describe("Default 2."),
81
+ outage_periods: z.number().int().optional().describe("Stuck periods after the worst; default 5."),
82
+ })
83
+ .strict();
84
+
85
+ export const feasibilityInput = z
86
+ .object({
87
+ broker: z.enum(["alpaca", "ibkr", "other"]).optional().describe("Default other."),
88
+ asset_class: z.enum(["us_equity", "crypto", "futures", "fx", "options"]).optional(),
89
+ account: z.enum(["cash", "margin"]).optional(),
90
+ capital_usd: z.number(),
91
+ orders_per_rebalance: z.number().int(),
92
+ rebalances_per_year: z.number(),
93
+ turnover_per_year: z.number().min(0).describe("Buys plus sells a year / capital."),
94
+ adv_usd: z.number().describe("Daily dollar volume of a typical holding."),
95
+ daily_volatility: z.number().max(1).describe("Of a typical holding, e.g. 0.02."),
96
+ copies: z.number().int().optional().describe("Agents trading the same signal at once; default 1."),
97
+ expected_gross_return: z.number().min(-1).optional().describe("Annual, before costs; adds capacity."),
98
+ spread_and_fees_bps: z.number().min(0).optional().describe("Per trade; default 0."),
99
+ impact_coefficient: z.number().optional().describe("Square-root law Y; default 1."),
100
+ minutes_per_rebalance: z.number().optional().describe("Default 1."),
101
+ orders_per_minute_limit: z.number().optional().describe("If the broker's is not on file."),
102
+ })
103
+ .strict();
104
+
105
+ const loose = z.looseObject({}).optional();
106
+ const sentences = z.array(z.string()).optional();
107
+
108
+ export const backtestOutput = z
109
+ .looseObject({ variants: loose, selected: loose, buy_and_hold: loose, validation: loose, plain_reading: z.string().optional(), limits: sentences, source: loose })
110
+ .describe("selected holds the best variant's parameters and metrics; validation the deflated Sharpe, overfitting probability and minimum track record counting every variant run; plain_reading states them.");
111
+
112
+ export const summaryOutput = z
113
+ .looseObject({ text: z.string().optional(), growth: loose, risk: loose, drawdown: loose, trend: loose, volatility: z.looseObject({}).nullable().optional(), tails: loose, jumps: loose, limits: sentences, source: loose })
114
+ .describe("text is the summary to read; the other fields hold each figure by the rule named in it.");
115
+
116
+ export const stressOutput = z
117
+ .looseObject({ baseline: loose, resampled: loose, scenarios: z.array(z.unknown()).optional(), fragility: loose, plain_reading: z.string().optional(), limits: sentences, source: loose })
118
+ .describe("resampled holds bootstrap percentiles and breach frequencies; scenarios each named stress and its rule; fragility the share of histories breaking the limit or losing.");
119
+
120
+ export const feasibilityOutput = z
121
+ .looseObject({ verdict: z.string().optional(), checks: z.array(z.unknown()).optional(), to_change: z.array(z.string()).optional(), sources: loose, plain_reading: z.string().optional(), limits: sentences })
122
+ .describe("verdict summarizes the checks; each check has a status (ok, warning, blocking, unknown), its finding and numbers; to_change lists what to fix.");