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 +106 -26
- package/package.json +5 -2
- package/src/journal-evidence.mjs +35 -0
- package/src/lab-schemas.mjs +122 -0
- package/src/lab.mjs +308 -0
- package/src/local/js/audit-core.js +329 -0
- package/src/local/js/backtest-core.js +484 -0
- package/src/local/js/feasibility-core.js +206 -0
- package/src/local/js/journal-files.js +34 -0
- package/src/local/js/null-zoo-v1-sizes.js +40 -0
- package/src/local/js/series-summary-core.js +267 -0
- package/src/local/js/stress-core.js +195 -0
- package/src/local/js/trade-journal-core.js +210 -0
- package/src/local/js/trade-journal-export-core.js +265 -0
- package/src/local/js/validate/overfitting.js +3 -0
- package/src/local/scripts/canonical-json.mjs +10 -6
- package/src/schemas.mjs +18 -10
- package/src/series-file.mjs +18 -0
- package/src/server.mjs +194 -33
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
|
|
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,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
151
|
-
overfitting, then the track record needed, reported with what each number does not establish)
|
|
152
|
-
`track_record_needed
|
|
153
|
-
|
|
154
|
-
|
|
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 `
|
|
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
|
-
`
|
|
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 `
|
|
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
|
|
342
|
-
|
|
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` | `
|
|
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
|
-
|
|
|
358
|
-
|
|
|
359
|
-
| `
|
|
360
|
-
| `
|
|
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.
|
|
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.");
|