canli-quant-mcp 0.1.2 → 0.2.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/CHANGELOG.md +11 -0
- package/README.md +2 -0
- package/package.json +2 -2
- package/src/tools/strategies.mjs +33 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- Backtests can return their path: `curve: true` adds the equity curve (from 1) against buy-and-hold,
|
|
6
|
+
the drawdown from the running peak and the exposure at each close (at most 1,000 rows, evenly
|
|
7
|
+
thinned, the last kept), and every weight change as a trade with its price (the last 1,000).
|
|
8
|
+
`dates` labels both. The default answer is unchanged, and strategy_sweep never builds curves.
|
|
9
|
+
|
|
10
|
+
## 0.1.3
|
|
11
|
+
|
|
12
|
+
- zod 4.6.7 (a high advisory in earlier zod: memory exhaustion on oversized constrained collections). No code change.
|
|
13
|
+
|
|
3
14
|
## Unreleased
|
|
4
15
|
|
|
5
16
|
## 0.1.2
|
package/README.md
CHANGED
|
@@ -429,6 +429,8 @@ randomness is the sleeve bootstrap, drawn from a stated seed.
|
|
|
429
429
|
|
|
430
430
|
<details><summary><b>strategies</b>: Strategy backtests (20)</summary>
|
|
431
431
|
|
|
432
|
+
Every recipe and `backtest_weights` take `curve: true` (and optional `dates`) to add the equity curve against buy-and-hold, the drawdown, the exposure and every trade, for charts.
|
|
433
|
+
|
|
432
434
|
| tool | what it does |
|
|
433
435
|
|---|---|
|
|
434
436
|
| `backtest_ma_crossover` | Backtest a fast/slow moving-average crossover on your prices with costs and no lookahead: CAGR, Sharpe, drawdown, turnover, cost drag and buy-and-hold for comparison. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canli-quant-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "235 quant finance tools and a library of 399 strategy sleeves as one MCP server: performance and risk, options and exotics, fixed income, portfolio construction, econometrics (regime switching, Johansen, CUSUM, HAR), indicators, execution, crypto and FX, valuation, backtests with multiple-testing corrections (deflated Sharpe, SPA, StepM, PBO), data annotation and ML labels (triple barrier, purged CV) and data checks, each checked against QuantLib, statsmodels, arch, TA-Lib or numpy. Keyless and local with no network, disk writes or logging; three tools in context by default.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@modelcontextprotocol/server": "2.3.1",
|
|
25
|
-
"zod": "4.6.
|
|
25
|
+
"zod": "4.6.7"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@modelcontextprotocol/client": "2.3.1"
|
package/src/tools/strategies.mjs
CHANGED
|
@@ -14,7 +14,11 @@ import { minVariance } from "./portfolio.mjs";
|
|
|
14
14
|
const pricesArg = z.array(z.number().positive()).min(30).max(MAX_SERIES).describe("Prices oldest first (daily closes by default).");
|
|
15
15
|
const matrixArg = z.array(z.array(z.number().positive()).min(2).max(200)).min(30).max(MAX_SERIES).describe("Prices, one row per period, one column per asset.");
|
|
16
16
|
const costArg = z.number().min(0).max(1000).optional().describe("Cost in basis points per unit of turnover (one-way); default 5.");
|
|
17
|
-
const
|
|
17
|
+
const curveArgs = {
|
|
18
|
+
curve: z.boolean().optional().describe("Also return the equity curve against buy-and-hold, the drawdown, the exposure and the trade log, for charts; default false."),
|
|
19
|
+
dates: z.array(z.string().min(1).max(40)).min(30).max(MAX_SERIES).optional().describe("Dates of the price rows (same length), to label the curve and trades."),
|
|
20
|
+
};
|
|
21
|
+
const common = { ...curveArgs, cost_bps: costArg, periods_per_year: ppyArg, risk_free: z.number().gt(-1).lt(1).optional().describe("Annual rate earned on cash; default 0."), long_only: z.boolean().optional().describe("Long-only (true) or long-short (false); default true.") };
|
|
18
22
|
const int = (d, lo = 1, hi = 5000) => z.number().int().min(lo).max(hi).optional().describe(`Default ${d}.`);
|
|
19
23
|
const fractionArg = z.number().gt(0).max(1).optional().describe("Fraction of the universe to hold, e.g. 0.25 for the top quarter; overrides top.");
|
|
20
24
|
// Number of assets a fraction of the universe selects: rounded, at least one.
|
|
@@ -42,11 +46,12 @@ export function runEngine(P, targets, { cost_bps = 5, periods_per_year: ppy = 25
|
|
|
42
46
|
let w = new Array(N).fill(0);
|
|
43
47
|
const net = [], gross = [], turnovers = [], exposures = [];
|
|
44
48
|
let trades = 0;
|
|
49
|
+
const log = [];
|
|
45
50
|
for (let t = 0; t < T - 1; t++) {
|
|
46
51
|
const tgt = targets[t];
|
|
47
52
|
let to = 0;
|
|
48
53
|
if (tgt) {
|
|
49
|
-
for (let i = 0; i < N; i++) { const d = Math.abs(tgt[i] - w[i]); to += d; if (d > 1e-12) trades++; }
|
|
54
|
+
for (let i = 0; i < N; i++) { const d = Math.abs(tgt[i] - w[i]); to += d; if (d > 1e-12) { trades++; log.push([t, i, w[i], tgt[i]]); } }
|
|
50
55
|
w = [...tgt];
|
|
51
56
|
}
|
|
52
57
|
const r = P[t + 1].map((p, i) => p / P[t][i] - 1);
|
|
@@ -57,7 +62,7 @@ export function runEngine(P, targets, { cost_bps = 5, periods_per_year: ppy = 25
|
|
|
57
62
|
const growth = 1 + g;
|
|
58
63
|
w = w.map((x, i) => (growth > 0 ? x * (1 + r[i]) / growth : 0));
|
|
59
64
|
}
|
|
60
|
-
return { net, gross, turnovers, exposures, trades, final_weights: w };
|
|
65
|
+
return { net, gross, turnovers, exposures, trades, log, final_weights: w };
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
export function statsOf(rets, ppy) {
|
|
@@ -81,10 +86,34 @@ function report(P, targets, a, rule, params) {
|
|
|
81
86
|
buy_and_hold: { cagr: b.cagr, sharpe: b.sharpe, max_drawdown: b.max_drawdown, note: N > 1 ? "Equal weights at the start, never rebalanced." : undefined },
|
|
82
87
|
returns_tail: eng.net.slice(-5),
|
|
83
88
|
caution: "One backtest of one parameter set. If you tried others, count them and use strategy_sweep or deflated_sharpe_ratio.",
|
|
89
|
+
...(a.curve ? curveOf(P, eng, bench, a.dates) : {}),
|
|
84
90
|
_net: eng.net,
|
|
85
91
|
};
|
|
86
92
|
}
|
|
87
93
|
|
|
94
|
+
// The curve for charts: equity (from 1) of the strategy and of buy-and-hold at each close, the
|
|
95
|
+
// strategy's drawdown from its running peak, and the gross exposure held into the next period, at
|
|
96
|
+
// most CURVE_POINTS rows (evenly spaced, the last always kept); and every weight change as a trade
|
|
97
|
+
// (the last TRADES_KEPT if there are more), at the close it was decided.
|
|
98
|
+
export const CURVE_POINTS = 1000, TRADES_KEPT = 1000;
|
|
99
|
+
export function curveOf(P, eng, bench, dates) {
|
|
100
|
+
const T = P.length, N = P[0].length;
|
|
101
|
+
if (dates && dates.length !== T) throw new Error(`dates has ${dates.length} entries and prices ${T}.`);
|
|
102
|
+
const at = (t) => (dates ? dates[t] : t);
|
|
103
|
+
let e = 1, b = 1, peak = 1;
|
|
104
|
+
const all = [[at(0), 1, 1, 0, eng.exposures[0] ?? 0]];
|
|
105
|
+
for (let t = 1; t < T; t++) {
|
|
106
|
+
e *= 1 + eng.net[t - 1]; b *= 1 + bench.net[t - 1]; peak = Math.max(peak, e);
|
|
107
|
+
all.push([at(t), e, b, e / peak - 1, eng.exposures[t] ?? eng.exposures.at(-1) ?? 0]);
|
|
108
|
+
}
|
|
109
|
+
const step = Math.max(1, Math.ceil(T / CURVE_POINTS)), rows = all.filter((_, t) => t % step === 0 || t === T - 1);
|
|
110
|
+
const trades = eng.log.slice(-TRADES_KEPT).map(([t, i, from, to]) => [at(t), ...(N > 1 ? [i] : []), P[t][i], from, to, to > from ? "buy" : "sell"]);
|
|
111
|
+
return {
|
|
112
|
+
curve: { columns: [dates ? "date" : "period", "strategy", "buy_and_hold", "drawdown", "exposure"], rows, ...(step > 1 ? { every: step } : {}) },
|
|
113
|
+
trades: { columns: [dates ? "date" : "period", ...(N > 1 ? ["asset"] : []), "price", "weight_before", "weight_after", "side"], rows: trades, total: eng.log.length },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
88
117
|
const strip = (r) => { const { _net, ...rest } = r; return rest; };
|
|
89
118
|
const single = (p) => p.map((x) => [x]);
|
|
90
119
|
|
|
@@ -334,7 +363,7 @@ function sweep(recipe, base, grid, ppy) {
|
|
|
334
363
|
if (combos.length > 400) throw new Error(`The grid has ${combos.length} combinations; keep it to 400 or fewer.`);
|
|
335
364
|
const rows = [];
|
|
336
365
|
for (const c of combos) {
|
|
337
|
-
try { const b = RECIPES[recipe].build({ ...base, ...c }); const r = report(b.P, b.targets, { ...base, ...c }, b.rule, b.params); rows.push({ params: c, sharpe: r.sharpe, cagr: r.cagr, max_drawdown: r.max_drawdown, annual_turnover: r.annual_turnover, net: r._net }); }
|
|
366
|
+
try { const b = RECIPES[recipe].build({ ...base, ...c }); const r = report(b.P, b.targets, { ...base, ...c, curve: false }, b.rule, b.params); rows.push({ params: c, sharpe: r.sharpe, cagr: r.cagr, max_drawdown: r.max_drawdown, annual_turnover: r.annual_turnover, net: r._net }); }
|
|
338
367
|
catch (e) { rows.push({ params: c, error: e.message }); }
|
|
339
368
|
}
|
|
340
369
|
const ok = rows.filter((r) => r.sharpe !== null && r.sharpe !== undefined);
|