canli-validation-mcp 0.3.1 → 0.5.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.
@@ -0,0 +1,429 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://canlicapital.com/standards/paper-evidence/v0/schema.json",
4
+ "title": "canli.paper-evidence.v0",
5
+ "description": "A record of paper-traded or simulated strategy performance that states what it does not know. The schema exists because the failure mode of a performance claim is not a wrong number, it is a number whose basis, cost assumptions, search history and missing evidence are unstated. Every required field below is one of those.",
6
+ "type": "object",
7
+ "required": [
8
+ "schema",
9
+ "generated_at",
10
+ "capital",
11
+ "identity",
12
+ "period",
13
+ "returns",
14
+ "costs",
15
+ "selection",
16
+ "risk",
17
+ "corrections",
18
+ "provenance",
19
+ "claim_maturity"
20
+ ],
21
+ "additionalProperties": false,
22
+ "properties": {
23
+ "schema": {
24
+ "const": "canli.paper-evidence.v0"
25
+ },
26
+ "generated_at": {
27
+ "type": "string",
28
+ "format": "date-time"
29
+ },
30
+ "capital": {
31
+ "type": "object",
32
+ "description": "What kind of money produced this record. The single most misreadable fact about any track record, so it is required and enumerated rather than described in prose.",
33
+ "required": [
34
+ "kind",
35
+ "execution"
36
+ ],
37
+ "additionalProperties": false,
38
+ "properties": {
39
+ "kind": {
40
+ "enum": [
41
+ "PAPER",
42
+ "FUNDED",
43
+ "SIMULATED",
44
+ "MIXED"
45
+ ],
46
+ "description": "PAPER: orders placed at a broker against a paper account. SIMULATED: no broker involved. FUNDED: real capital at risk. MIXED: any combination, which must be decomposed in `notes`."
47
+ },
48
+ "execution": {
49
+ "enum": [
50
+ "BROKER_PAPER_FILLS",
51
+ "LOCAL_SIMULATED_FILLS",
52
+ "BROKER_LIVE_FILLS",
53
+ "MIXED"
54
+ ],
55
+ "description": "Where the fills came from. A broker paper fill and a locally modelled fill are different evidence and may not be merged silently."
56
+ },
57
+ "venue": {
58
+ "type": "string"
59
+ },
60
+ "notes": {
61
+ "type": "string"
62
+ }
63
+ }
64
+ },
65
+ "identity": {
66
+ "type": "object",
67
+ "description": "What this record is about, and whether that thing was frozen before the returns were seen.",
68
+ "required": [
69
+ "name",
70
+ "kind",
71
+ "preregistered"
72
+ ],
73
+ "additionalProperties": false,
74
+ "properties": {
75
+ "name": {
76
+ "type": "string",
77
+ "minLength": 1
78
+ },
79
+ "kind": {
80
+ "enum": [
81
+ "SLEEVE",
82
+ "BOOK",
83
+ "CANDIDATE"
84
+ ]
85
+ },
86
+ "preregistered": {
87
+ "type": "boolean",
88
+ "description": "Whether the strategy identity was frozen before its out-of-sample returns were opened. False is a legitimate answer and is far more useful than an absent field."
89
+ },
90
+ "preregistration_ref": {
91
+ "type": "string"
92
+ },
93
+ "constituents": {
94
+ "type": "array",
95
+ "items": {
96
+ "type": "string"
97
+ }
98
+ }
99
+ }
100
+ },
101
+ "period": {
102
+ "type": "object",
103
+ "required": [
104
+ "first_observation",
105
+ "last_observation",
106
+ "observation_count",
107
+ "frequency"
108
+ ],
109
+ "additionalProperties": false,
110
+ "properties": {
111
+ "first_observation": {
112
+ "type": "string",
113
+ "format": "date"
114
+ },
115
+ "last_observation": {
116
+ "type": "string",
117
+ "format": "date"
118
+ },
119
+ "observation_count": {
120
+ "type": "integer",
121
+ "minimum": 0
122
+ },
123
+ "frequency": {
124
+ "enum": [
125
+ "DAILY",
126
+ "WEEKLY",
127
+ "MONTHLY",
128
+ "HOURLY",
129
+ "IRREGULAR"
130
+ ]
131
+ },
132
+ "calendar": {
133
+ "type": "string"
134
+ }
135
+ }
136
+ },
137
+ "returns": {
138
+ "type": "object",
139
+ "description": "Return figures, always net-of-what stated. A return series whose cost treatment is unstated is not a measurement.",
140
+ "required": [
141
+ "basis",
142
+ "cumulative",
143
+ "series_available"
144
+ ],
145
+ "additionalProperties": false,
146
+ "properties": {
147
+ "basis": {
148
+ "enum": [
149
+ "GROSS",
150
+ "NET_OF_MODELLED_COSTS",
151
+ "NET_OF_REALISED_COSTS"
152
+ ],
153
+ "description": "NET_OF_REALISED_COSTS requires broker-observed costs, not a model."
154
+ },
155
+ "cumulative": {
156
+ "type": "number"
157
+ },
158
+ "annualised": {
159
+ "type": [
160
+ "number",
161
+ "null"
162
+ ]
163
+ },
164
+ "sharpe_annualised": {
165
+ "type": [
166
+ "number",
167
+ "null"
168
+ ]
169
+ },
170
+ "sharpe_reportable": {
171
+ "type": "boolean",
172
+ "description": "Whether the sample supports reporting a Sharpe at all. A short record must set this false and leave sharpe_annualised null rather than publishing a figure the sample cannot carry."
173
+ },
174
+ "series_available": {
175
+ "type": "boolean"
176
+ },
177
+ "series_url": {
178
+ "type": "string"
179
+ }
180
+ }
181
+ },
182
+ "costs": {
183
+ "type": "object",
184
+ "description": "What was charged and what was assumed. Absent cost modelling is itself a disclosure.",
185
+ "required": [
186
+ "modelled"
187
+ ],
188
+ "additionalProperties": false,
189
+ "properties": {
190
+ "modelled": {
191
+ "type": "boolean"
192
+ },
193
+ "components": {
194
+ "type": "array",
195
+ "items": {
196
+ "enum": [
197
+ "SPREAD",
198
+ "FEES",
199
+ "IMPACT",
200
+ "LATENCY",
201
+ "FINANCING",
202
+ "BORROW",
203
+ "FUNDING"
204
+ ]
205
+ }
206
+ },
207
+ "turnover_annualised": {
208
+ "type": [
209
+ "number",
210
+ "null"
211
+ ]
212
+ },
213
+ "notes": {
214
+ "type": "string"
215
+ },
216
+ "not_modelled": {
217
+ "type": "array",
218
+ "items": {
219
+ "type": "string"
220
+ },
221
+ "description": "Cost categories the publisher does NOT charge. Optional in v0 and a candidate to become required in the next version, because a cost model described only by its inclusions is structurally misleading: a reader cannot distinguish a cost judged immaterial from one nobody considered, since both appear as silence. Unmodelled costs almost always flatter, so this list has a known direction."
222
+ },
223
+ "coverage_url": {
224
+ "type": "string"
225
+ }
226
+ }
227
+ },
228
+ "selection": {
229
+ "type": "object",
230
+ "description": "How much searching produced this result. Without it a Sharpe ratio is uninterpretable, which is the single most common defect in published performance.",
231
+ "required": [
232
+ "trials_counted",
233
+ "trial_count",
234
+ "deflation_applied"
235
+ ],
236
+ "additionalProperties": false,
237
+ "properties": {
238
+ "trials_counted": {
239
+ "type": "boolean",
240
+ "description": "Whether the publisher actually maintains a trial ledger. False is honest; an unstated trial count is not."
241
+ },
242
+ "trial_count": {
243
+ "type": [
244
+ "integer",
245
+ "null"
246
+ ],
247
+ "minimum": 0
248
+ },
249
+ "trial_unit": {
250
+ "type": "string"
251
+ },
252
+ "deflation_applied": {
253
+ "type": "boolean"
254
+ },
255
+ "deflated_sharpe_ratio": {
256
+ "type": [
257
+ "number",
258
+ "null"
259
+ ],
260
+ "minimum": 0,
261
+ "maximum": 1
262
+ },
263
+ "deflation_method": {
264
+ "type": "string"
265
+ }
266
+ }
267
+ },
268
+ "risk": {
269
+ "type": "object",
270
+ "required": [
271
+ "max_drawdown_realised",
272
+ "drawdown_basis"
273
+ ],
274
+ "additionalProperties": false,
275
+ "properties": {
276
+ "max_drawdown_realised": {
277
+ "type": [
278
+ "number",
279
+ "null"
280
+ ]
281
+ },
282
+ "drawdown_basis": {
283
+ "enum": [
284
+ "OBSERVED",
285
+ "MODEL_ESTIMATED",
286
+ "NOT_ESTABLISHED"
287
+ ]
288
+ },
289
+ "max_drawdown_model_expected": {
290
+ "type": [
291
+ "number",
292
+ "null"
293
+ ]
294
+ },
295
+ "exposure_notes": {
296
+ "type": "string"
297
+ }
298
+ }
299
+ },
300
+ "corrections": {
301
+ "type": "object",
302
+ "description": "Whether anything in this record has been withdrawn. A record with no correction history and no statement that it has none is silent, not clean.",
303
+ "required": [
304
+ "count",
305
+ "withdrawn_figures"
306
+ ],
307
+ "additionalProperties": false,
308
+ "properties": {
309
+ "count": {
310
+ "type": "integer",
311
+ "minimum": 0
312
+ },
313
+ "withdrawn_figures": {
314
+ "type": "array",
315
+ "items": {
316
+ "type": "object",
317
+ "required": [
318
+ "figure",
319
+ "withdrawn_on",
320
+ "reason"
321
+ ],
322
+ "additionalProperties": false,
323
+ "properties": {
324
+ "figure": {
325
+ "type": "string"
326
+ },
327
+ "withdrawn_on": {
328
+ "type": "string",
329
+ "format": "date"
330
+ },
331
+ "reason": {
332
+ "type": "string"
333
+ },
334
+ "superseded_by": {
335
+ "type": "string"
336
+ }
337
+ }
338
+ }
339
+ },
340
+ "log_url": {
341
+ "type": "string"
342
+ }
343
+ }
344
+ },
345
+ "provenance": {
346
+ "type": "object",
347
+ "description": "What a reader can check without trusting the publisher.",
348
+ "required": [
349
+ "source_bindings",
350
+ "independently_verifiable"
351
+ ],
352
+ "additionalProperties": false,
353
+ "properties": {
354
+ "source_bindings": {
355
+ "type": "array",
356
+ "minItems": 1,
357
+ "items": {
358
+ "type": "object",
359
+ "required": [
360
+ "path",
361
+ "sha256"
362
+ ],
363
+ "additionalProperties": false,
364
+ "properties": {
365
+ "path": {
366
+ "type": "string"
367
+ },
368
+ "sha256": {
369
+ "type": "string",
370
+ "pattern": "^(sha256:)?[0-9a-f]{64}$"
371
+ },
372
+ "url": {
373
+ "type": "string"
374
+ }
375
+ }
376
+ }
377
+ },
378
+ "independently_verifiable": {
379
+ "type": "boolean"
380
+ },
381
+ "verification_url": {
382
+ "type": "string"
383
+ },
384
+ "signed": {
385
+ "type": "boolean"
386
+ },
387
+ "signature_scheme": {
388
+ "type": "string"
389
+ }
390
+ }
391
+ },
392
+ "claim_maturity": {
393
+ "type": "object",
394
+ "description": "What this record does NOT establish. Required, because the omission of this section is what makes an otherwise accurate record misleading.",
395
+ "required": [
396
+ "establishes",
397
+ "does_not_establish"
398
+ ],
399
+ "additionalProperties": false,
400
+ "properties": {
401
+ "establishes": {
402
+ "type": "array",
403
+ "items": {
404
+ "type": "string"
405
+ }
406
+ },
407
+ "does_not_establish": {
408
+ "type": "array",
409
+ "minItems": 1,
410
+ "items": {
411
+ "type": "string"
412
+ },
413
+ "description": "At least one entry. Every record fails to establish something, and a publisher who cannot name one has not looked."
414
+ },
415
+ "external_review_count": {
416
+ "type": "integer",
417
+ "minimum": 0
418
+ },
419
+ "independent_replication_count": {
420
+ "type": "integer",
421
+ "minimum": 0
422
+ }
423
+ }
424
+ },
425
+ "notes": {
426
+ "type": "string"
427
+ }
428
+ }
429
+ }
package/src/local.mjs ADDED
@@ -0,0 +1,39 @@
1
+ // Private local mode: the validators computed on this machine from src/local, a byte-for-byte
2
+ // mirror of the API's own computation (see scripts/sync-local.mjs). Nothing is sent to
3
+ // canlicapital.com and no receipt is stored, so the result names no receipt id.
4
+ import { LIMITS_TEXT } from "./local/api/_lib/limits.js";
5
+ import { compute as breadth } from "./local/js/validate/breadth.js";
6
+ import { compute as deflatedSharpe } from "./local/js/validate/deflated-sharpe.js";
7
+ import { compute as overfitting } from "./local/js/validate/overfitting.js";
8
+ import { compute as paperEvidence } from "./local/js/validate/paper-evidence.js";
9
+ import { compute as trackRecord } from "./local/js/validate/track-record.js";
10
+
11
+ export const LOCAL_VALIDATORS = Object.freeze({
12
+ validate_deflated_sharpe: { endpoint: "validate/deflated-sharpe", compute: deflatedSharpe },
13
+ validate_overfitting: { endpoint: "validate/overfitting", compute: overfitting },
14
+ validate_paper_evidence: { endpoint: "validate/paper-evidence", compute: paperEvidence },
15
+ validate_breadth: { endpoint: "validate/breadth", compute: breadth },
16
+ validate_track_record: { endpoint: "validate/track-record", compute: trackRecord },
17
+ });
18
+
19
+ const NOTE = "Computed on this machine in local mode. Nothing was sent to canlicapital.com and no receipt was stored.";
20
+
21
+ export function computeLocally(tool, body, now = () => new Date()) {
22
+ const validator = LOCAL_VALIDATORS[tool];
23
+ if (!validator) throw new Error(`${tool} has no local computation`);
24
+ const base = {
25
+ schema: "canli.local.v1",
26
+ endpoint: validator.endpoint,
27
+ computed: "locally",
28
+ generated_at: now().toISOString().replace(/\.\d{3}Z$/, "Z"),
29
+ note: NOTE,
30
+ limits: LIMITS_TEXT,
31
+ receipt: null,
32
+ };
33
+ try {
34
+ return { envelope: { ...base, data: validator.compute(body), error: null }, failed: false };
35
+ } catch (error) {
36
+ const message = error instanceof Error ? error.message : String(error);
37
+ return { envelope: { ...base, data: null, error: { code: "invalid_input", message } }, failed: true };
38
+ }
39
+ }
package/src/schemas.mjs CHANGED
@@ -92,6 +92,22 @@ export const breadthInput = z
92
92
  })
93
93
  .strict();
94
94
 
95
+ // ---------------------------------------------------------------------------------------------
96
+ // validate_track_record
97
+ // ---------------------------------------------------------------------------------------------
98
+
99
+ export const trackRecordInput = z
100
+ .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(),
108
+ })
109
+ .strict();
110
+
95
111
  // ---------------------------------------------------------------------------------------------
96
112
  // get_key / get_receipt
97
113
  // ---------------------------------------------------------------------------------------------
@@ -111,14 +127,27 @@ export const getReceiptInput = z
111
127
  export const emptyInput = z.object({}).strict();
112
128
 
113
129
  // company_financial_history reads the public company reference, not the validation API.
114
- export const companyHistoryInput = z
130
+ // Advertised to clients as a flat object (a refinement has no useful JSON Schema); the handler
131
+ // enforces the exactly-one-of rule with companyHistoryInput.
132
+ export const companyHistoryToolShape = z
115
133
  .object({
116
- cik: z.string().regex(/^\d{1,10}$/, "A CIK is 1 to 10 digits"),
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(),
117
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(),
118
137
  limit: z.number().int().min(1).max(200).optional(),
119
138
  })
120
139
  .strict();
121
140
 
141
+ export const companyHistoryInput = z
142
+ .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(),
147
+ })
148
+ .strict()
149
+ .refine((v) => (v.cik === undefined) !== (v.ticker === undefined), "Send exactly one of cik or ticker");
150
+
122
151
  // The company reference carries its own boundary sentence in every record (claim_boundary),
123
152
  // written by scripts/lib/company-reference.mjs. A test pins this copy to that source verbatim.
124
153
  export const COMPANY_REFERENCE_BOUNDARY = "Public company accounting reference, not market prices, returns, an investment recommendation, or ALPHAC performance. Validate a separately constructed return series with the validation API; accounting values are not returns.";
@@ -147,12 +176,13 @@ export const REGISTRY_DESCRIPTION_MAX = 100;
147
176
  // above) what the tool's result cannot be used to claim, so an agent sees this before it ever
148
177
  // calls the tool, not only inside the returned envelope.
149
178
  export const TOOL_DESCRIPTIONS = Object.freeze({
150
- get_key: `Issue a free canlicapital.com validation key (POST /api/v1/keys) and hold it in memory for this session; skipped when CANLI_KEY is already set. ${LIMITS_SENTENCES.quotas}`,
179
+ 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}`,
151
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}`,
152
181
  validate_overfitting: `Probability of backtest overfitting by CSCV over the returns of every variant tried. ${LIMITS_SENTENCES.notAdmission}`,
153
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}`,
154
184
  validate_breadth: `Book Sharpe ceiling from per-sleeve quality and average pairwise correlation, and the sleeves a target needs. ${LIMITS_SENTENCES.scope}`,
155
185
  get_receipt: `Fetch a stored verdict by its content-hash id (GET /api/v1/receipts/{id}), immutable and cacheable. ${LIMITS_SENTENCES.unsigned}`,
156
186
  service_status: `Service, store and quota constants for the validation API (GET /api/v1/validate/status); no key required. ${LIMITS_SENTENCES.scope}`,
157
- company_financial_history: `SEC-reported financial history for one company from the canlicapital.com company reference (GET /company-data/{cik}.json). 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}`,
187
+ 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}`,
158
188
  });