canli-backtest-mcp 0.1.1 → 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 +16 -2
- package/src/pit.mjs +2 -1
- package/src/server.mjs +6 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canli-backtest-mcp",
|
|
3
|
-
"version": "0.1.
|
|
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",
|
|
@@ -37,5 +37,19 @@
|
|
|
37
37
|
"bugs": {
|
|
38
38
|
"url": "https://github.com/arhancanli/canlicapital/issues"
|
|
39
39
|
},
|
|
40
|
-
"keywords": [
|
|
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
|
-
|
|
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:
|
|
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:
|
|
68
|
-
signal_file:
|
|
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]
|
|
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 } : {}),
|