canli-validation-mcp 0.4.0 → 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
@@ -127,14 +127,27 @@ export const getReceiptInput = z
127
127
  export const emptyInput = z.object({}).strict();
128
128
 
129
129
  // company_financial_history reads the public company reference, not the validation API.
130
- 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
131
133
  .object({
132
- 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(),
133
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(),
134
137
  limit: z.number().int().min(1).max(200).optional(),
135
138
  })
136
139
  .strict();
137
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
+
138
151
  // The company reference carries its own boundary sentence in every record (claim_boundary),
139
152
  // written by scripts/lib/company-reference.mjs. A test pins this copy to that source verbatim.
140
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.";
@@ -163,7 +176,7 @@ export const REGISTRY_DESCRIPTION_MAX = 100;
163
176
  // above) what the tool's result cannot be used to claim, so an agent sees this before it ever
164
177
  // calls the tool, not only inside the returned envelope.
165
178
  export const TOOL_DESCRIPTIONS = Object.freeze({
166
- 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}`,
167
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}`,
168
181
  validate_overfitting: `Probability of backtest overfitting by CSCV over the returns of every variant tried. ${LIMITS_SENTENCES.notAdmission}`,
169
182
  validate_paper_evidence: `Conformance of a performance record against the canli.paper-evidence.v0 standard. ${LIMITS_SENTENCES.scope}`,
@@ -171,5 +184,5 @@ export const TOOL_DESCRIPTIONS = Object.freeze({
171
184
  validate_breadth: `Book Sharpe ceiling from per-sleeve quality and average pairwise correlation, and the sleeves a target needs. ${LIMITS_SENTENCES.scope}`,
172
185
  get_receipt: `Fetch a stored verdict by its content-hash id (GET /api/v1/receipts/{id}), immutable and cacheable. ${LIMITS_SENTENCES.unsigned}`,
173
186
  service_status: `Service, store and quota constants for the validation API (GET /api/v1/validate/status); no key required. ${LIMITS_SENTENCES.scope}`,
174
- 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}`,
175
188
  });