canli-backtest-mcp 0.1.0 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canli-backtest-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "MCP server for backtests that refuse lookahead: point-in-time factors from SEC filings, cross-sectional backtests of any signal on your own prices, costs on actual trades, and every variant counted in a trial ledger.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -21,7 +21,7 @@
21
21
  "dependencies": {
22
22
  "@modelcontextprotocol/server": "2.3.1",
23
23
  "canli-fundamentals-mcp": "^0.5.0",
24
- "canli-validation-mcp": "file:../mcp",
24
+ "canli-validation-mcp": "^0.14.0",
25
25
  "zod": "4.6.5"
26
26
  },
27
27
  "devDependencies": {
@@ -37,5 +37,19 @@
37
37
  "bugs": {
38
38
  "url": "https://github.com/arhancanli/canlicapital/issues"
39
39
  },
40
- "keywords": ["mcp", "model-context-protocol", "backtesting", "quant", "quantitative-finance", "point-in-time", "sec", "xbrl", "lookahead-bias", "deflated-sharpe", "overfitting", "factor-investing", "ai-agents"]
40
+ "keywords": [
41
+ "mcp",
42
+ "model-context-protocol",
43
+ "backtesting",
44
+ "quant",
45
+ "quantitative-finance",
46
+ "point-in-time",
47
+ "sec",
48
+ "xbrl",
49
+ "lookahead-bias",
50
+ "deflated-sharpe",
51
+ "overfitting",
52
+ "factor-investing",
53
+ "ai-agents"
54
+ ]
41
55
  }
package/src/pit.mjs CHANGED
@@ -65,7 +65,8 @@ export function factorAsOf(factor, series, asOf) {
65
65
 
66
66
  export function factorNeeds(factor) {
67
67
  if (factor.startsWith("level:")) return [factor.slice(6)];
68
- const f = FACTORS[factor];
68
+ // Own entries only: "constructor" is an unknown factor, not Object's constructor.
69
+ const f = Object.hasOwn(FACTORS, factor) ? FACTORS[factor] : undefined;
69
70
  if (!f) throw new RangeError(`unknown factor ${JSON.stringify(factor)}; choose from ${Object.keys(FACTORS).join(", ")} or level:<measure>`);
70
71
  return f.needs;
71
72
  }
package/src/server.mjs CHANGED
@@ -57,15 +57,17 @@ export function monthStarts(start, end) {
57
57
  return out;
58
58
  }
59
59
 
60
+ // A path the file system can take: no NUL characters (Node throws a TypeError on them).
61
+ const filePath = z.string().min(1).refine((s) => !s.includes("\0"), "a file path cannot contain a NUL character");
60
62
  export const pitInput = z.object({
61
63
  factor: z.string().describe(`One of ${Object.keys(FACTORS).join(", ")}, or level:<measure> (revenue, net_income, assets, ...).`),
62
64
  tickers: z.array(z.string().min(1).max(20)).min(1).max(1000).describe("Tickers (or CIKs) to compute it for."),
63
65
  start: date, end: date,
64
- out_file: z.string().optional().describe("Where to write the signal CSV; default a new temporary file."),
66
+ out_file: filePath.optional().describe("Where to write the signal CSV; default a new temporary file."),
65
67
  });
66
68
  export const backtestInput = z.object({
67
- prices_file: z.string().describe("CSV: a date column (YYYY-MM-DD) and one column of closing prices per ticker."),
68
- signal_file: z.string().describe("CSV in the same shape; each row dated when its values became known (pit_factor writes this)."),
69
+ prices_file: filePath.describe("CSV: a date column (YYYY-MM-DD) and one column of closing prices per ticker."),
70
+ signal_file: filePath.describe("CSV in the same shape; each row dated when its values became known (pit_factor writes this)."),
69
71
  rebalance: z.union([z.enum(["weekly", "monthly", "quarterly"]), z.number().int().min(1)]).optional().describe("Default monthly; or every N trading days."),
70
72
  lag_days: z.number().int().min(1).optional().describe("Trading days between a signal becoming known and trading on it. Default 1, the minimum."),
71
73
  side: z.enum(["long_short", "long_only"]).optional().describe("Default long_short: long the top quantile, short the bottom."),
@@ -98,7 +100,7 @@ export async function toolPitFactor(session, args) {
98
100
  writePanel(file, panel);
99
101
  const covered = panel.coverage.map((c) => c.companies_with_value);
100
102
  return text({
101
- factor, meaning: FACTORS[factor]?.meaning ?? `annual ${factor.slice(6)} as filed`, file,
103
+ factor, meaning: (Object.hasOwn(FACTORS, factor) ? FACTORS[factor].meaning : null) ?? `annual ${factor.slice(6)} as filed`, file,
102
104
  dates: dates.length, tickers: tickers.length, first_date: dates[0], last_date: dates.at(-1),
103
105
  coverage: { min: Math.min(...covered), median: covered.slice().sort((a, b) => a - b)[Math.floor(covered.length / 2)], max: Math.max(...covered) },
104
106
  matched: panel.matched, ...(Object.keys(panel.missing).length ? { missing: panel.missing } : {}),