canli-backtest-mcp 0.3.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.
- package/README.md +17 -0
- package/docs/AGENT_EXCHANGE.md +45 -0
- package/package.json +2 -1
- package/src/arena.mjs +150 -0
- package/src/consensus.mjs +89 -0
- package/src/cpcv.mjs +164 -0
- package/src/diversifiers.mjs +69 -0
- package/src/engine.mjs +94 -6
- package/src/exchange.mjs +439 -0
- package/src/league.mjs +243 -0
- package/src/proof.mjs +200 -0
- package/src/server.mjs +100 -7
package/src/server.mjs
CHANGED
|
@@ -22,14 +22,21 @@ import { ledgerDir, recordTrial } from "canli-validation-mcp/src/ledger.mjs";
|
|
|
22
22
|
import { agreementInput, annotationAgreement, annotationGold, annotationPacket, goldInput, packetInput } from "./annotate.mjs";
|
|
23
23
|
import { BREADTH_LIMITS_TEXT, effectiveBets, runBreadth, signalCatalog } from "./breadth.mjs";
|
|
24
24
|
import { buildPanel, panelInput } from "./panel.mjs";
|
|
25
|
-
import { ENGINE_LIMITS_TEXT, describe, runBacktest } from "./engine.mjs";
|
|
25
|
+
import { CRISES, crisisReplay, ENGINE_LIMITS_TEXT, describe, runBacktest } from "./engine.mjs";
|
|
26
|
+
import { cpcv, cpcvInput, CPCV_LIMITS_TEXT } from "./cpcv.mjs";
|
|
27
|
+
import { arenaNew, arenaNewInput, arenaOrders, arenaOrdersInput, arenaStep, arenaStepInput } from "./arena.mjs";
|
|
28
|
+
import { exchangeJoin, exchangeJoinInput, exchangeNew, exchangeNewInput, exchangeOrders, exchangeOrdersInput, exchangeReport, exchangeReportInput, exchangeRun, exchangeRunInput, exchangeStep, exchangeStepInput, exchangeView, exchangeViewInput } from "./exchange.mjs";
|
|
29
|
+
import { agentConsensus, consensusInput } from "./consensus.mjs";
|
|
30
|
+
import { diversifiersInput, findDiversifiers } from "./diversifiers.mjs";
|
|
31
|
+
import { leagueCommit, leagueCommitInput, leagueNew, leagueNewInput, leagueScore, leagueScoreInput } from "./league.mjs";
|
|
32
|
+
import { proofFile, proofFileInput, proofFilings, proofFilingsInput, proofFind, proofFindInput, proofVerifyInput, verifyLedger } from "./proof.mjs";
|
|
26
33
|
import { readPanel, writePanel } from "./panel-io.mjs";
|
|
27
34
|
import { breadthSweep, sweepInput } from "./sweep.mjs";
|
|
28
35
|
import { FACTORS, PIT_LIMITS_TEXT, buildFactorPanel } from "./pit.mjs";
|
|
29
36
|
|
|
30
37
|
export const SERVER_NAME = "canli-backtest-mcp";
|
|
31
38
|
export const SERVER_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
32
|
-
export const SERVER_INSTRUCTIONS = "Backtests that refuse lookahead. Build a point-in-time factor from SEC filings with pit_factor (or bring your own signal CSV, each row dated when it was known), then run backtest_signal against your own prices CSV. Pass the same ledger name to every backtest_signal call in one search: the reply judges the best variant against all of them. For breadth, price_panel builds a free price panel (an index fund's holdings, prices, dollar volume and industries), breadth_book builds a sleeve for every signal in every industry from one price panel and trades them as one netted account; effective_bets counts the independent bets in any set of return series. For datasets, annotation_packet, annotation_agreement and annotation_gold turn figures into a gold set people have checked against the filings.";
|
|
39
|
+
export const SERVER_INSTRUCTIONS = "Backtests that refuse lookahead. Build a point-in-time factor from SEC filings with pit_factor (or bring your own signal CSV, each row dated when it was known), then run backtest_signal against your own prices CSV. Pass the same ledger name to every backtest_signal call in one search: the reply judges the best variant against all of them. For breadth, price_panel builds a free price panel (an index fund's holdings, prices, dollar volume and industries), breadth_book builds a sleeve for every signal in every industry from one price panel and trades them as one netted account; effective_bets counts the independent bets in any set of return series. For a search, cpcv scores the chosen variant on held-out blocks across many paths, and crisis_replay holds any returns against named crises. proof_filings and proof_file keep a signed, append-only record; league_new, league_commit and league_score run a paper league scored after the fact; arena_new, arena_orders and arena_step are a paper market for agents, with made-up assets and money. For datasets, annotation_packet, annotation_agreement and annotation_gold turn figures into a gold set people have checked against the filings.";
|
|
33
40
|
export const SERVER_INFO = Object.freeze({
|
|
34
41
|
name: SERVER_NAME,
|
|
35
42
|
version: SERVER_VERSION,
|
|
@@ -80,10 +87,20 @@ export const backtestInput = z.object({
|
|
|
80
87
|
cost_bps: z.number().min(0).optional().describe("Cost per unit traded, in basis points. Default 10."),
|
|
81
88
|
min_names: z.number().int().min(2).optional().describe("Skip a rebalance with fewer ranked names. Default 5."),
|
|
82
89
|
periods_per_year: z.number().positive().optional().describe("Default 252 (daily prices)."),
|
|
90
|
+
capital_usd: z.number().positive().optional().describe("Size to trade at, in dollars. Needs dollar_volume_file. Orders fill only up to max_participation of recent volume and pay square-root impact, and the reply adds a capacity curve."),
|
|
91
|
+
dollar_volume_file: filePath.optional().describe("CSV like prices_file with daily dollar volume. price_panel writes one from adjusted closes, which understates older volume by the dividends paid since."),
|
|
92
|
+
max_participation: z.number().gt(0).max(1).optional().describe("Largest share of a name's average daily dollar volume one order can take. Default 0.1."),
|
|
93
|
+
impact_coefficient: z.number().min(0).max(5).optional().describe("Y in impact = Y x daily volatility x sqrt(participation). Default 1."),
|
|
94
|
+
crisis_drawdown_limit: z.number().gt(0).max(1).optional().describe("A drawdown beyond this inside one crisis counts as a rupture. Default 0.5."),
|
|
83
95
|
ledger: z.string().regex(/^[a-z0-9][a-z0-9-]{2,63}$/).optional().describe("Record this run in a search ledger (created if new) and judge it against every run recorded there."),
|
|
84
96
|
label: z.string().min(1).max(200).optional().describe("The variant's name in the ledger. Default: the settings."),
|
|
85
97
|
});
|
|
86
98
|
|
|
99
|
+
export const crisisInput = z.object({
|
|
100
|
+
returns_file: filePath.describe("CSV: a date column (YYYY-MM-DD) and one column of daily returns per strategy, at most 50."),
|
|
101
|
+
drawdown_limit: z.number().gt(0).max(1).optional().describe("A drawdown beyond this inside one crisis counts as a rupture. Default 0.5."),
|
|
102
|
+
}).strict();
|
|
103
|
+
|
|
87
104
|
export const breadthInput = z.object({
|
|
88
105
|
prices_file: filePath.describe("CSV: a date column and one column of closing (total-return) prices per ticker."),
|
|
89
106
|
groups_file: filePath.optional().describe("CSV with columns ticker,group (an industry, say): one sleeve per signal per group. Default: one group, the whole panel."),
|
|
@@ -114,7 +131,28 @@ export const TOOL_DESCRIPTIONS = Object.freeze({
|
|
|
114
131
|
effective_bets: "How many independent bets a set of return series holds: the participation ratio of their weekly correlations (n for unrelated series, 1 for copies), and with families, the families' bets and the ideas they cluster into.",
|
|
115
132
|
list_factors: "The point-in-time factors pit_factor builds from SEC filings, and what each measures.",
|
|
116
133
|
pit_factor: `Build a factor from SEC filings for a list of tickers, monthly from start to end, using only what had been filed by each date (restatements invisible until filed). Writes a signal CSV for backtest_signal. ${PIT_LIMITS_TEXT[0]}`,
|
|
117
|
-
|
|
134
|
+
proof_filings: "Add what companies filed to a signed, append-only ledger: each figure with its filing, and a restatement line whenever a later filing changes one. Re-running adds only what is new. It shows the lines were not altered afterwards, not that a figure is right.",
|
|
135
|
+
proof_file: "Record a file's hash in a signed, append-only ledger, a backtest result or a returns file, so anyone can later check this exact file existed by then. Nothing else about the file is claimed.",
|
|
136
|
+
proof_verify: "Check a proof ledger from the file alone: every hash, order and signature, and any heads published elsewhere. Says which line first fails.",
|
|
137
|
+
proof_find: "Look up a file, company or concept in a proof ledger. No record means only that this ledger holds none, not that anything is wrong.",
|
|
138
|
+
league_new: "Start a paper league: a schedule of rounds and rules with a hash to publish. No prizes, no stakes and nobody's money.",
|
|
139
|
+
league_commit: "Commit a league entry before its round closes: only the hash of its CSV of ticker,score goes in the ledger, and the file is shown after the round. Only the latest commit counts, so revealing whichever of several did best is refused.",
|
|
140
|
+
league_score: "Score a paper league from its files and prices, repeatable by anyone: each entrant's rank correlation with what followed, set against the best luck gives across that many entrants, how different each is from the rest, and a paper book of the top entrants chosen on earlier rounds only.",
|
|
141
|
+
exchange_new: "Start a paper exchange where different AI agents and scripted traders (market makers, momentum, value, deleveragers, stop-losses, levered longs) trade in limit order books, with margin rules and forced selling, plus shocks at set steps. Made-up assets and money, reproducible from a seed.",
|
|
142
|
+
exchange_join: "Join a paper exchange as an agent. Returns the token its other calls need, so no agent can act as another.",
|
|
143
|
+
exchange_view: "What an agent in a paper exchange can see: the books, recent trades and its own account. No other agent's positions.",
|
|
144
|
+
exchange_orders: "Send an agent's limit orders, market orders and cancels for the coming step of a paper exchange. An empty list passes.",
|
|
145
|
+
exchange_step: "Move a paper exchange on. Waits for every joined agent to act, so nobody moves time on the others.",
|
|
146
|
+
exchange_report: "What happened in a paper exchange: each asset's worst fall and recovery, spreads and depth, who gained and lost, forced selling and the longest chain of it, and a hash anyone can check by replaying.",
|
|
147
|
+
exchange_run: "Run a whole paper exchange of scripted traders in one call and report it: for comparing set-ups, such as how much leverage turns a shock into a cascade. A simulation, not a forecast.",
|
|
148
|
+
agent_consensus: "Combine several AI agents' probabilities, and count how many independent views they really hold, from how their errors move together: five models that share their mistakes are not five votes. Weights each by its record on settled questions.",
|
|
149
|
+
find_diversifiers: "Rank candidate strategies by what they add to a book: independent bets added first, then how they behave on the book's worst days, with return shown last and never used to rank. Correlation first.",
|
|
150
|
+
arena_new: "Start a paper market for AI agents to trade in: made-up assets and made-up money, with scripted traders, reproducible from a seed. Nothing here represents or reaches a real security, token or currency.",
|
|
151
|
+
arena_orders: "Send paper orders for an agent in an arena, for the coming auction. No short selling and no borrowing.",
|
|
152
|
+
arena_step: "Clear a paper arena's next call auctions at one price per asset, and show prices and standings. Paper only.",
|
|
153
|
+
crisis_replay: `Hold any strategy's daily returns against named market crises (${Object.values(CRISES).map((c) => c.what.replace(/^the /, "")).slice(0, 4).join(", ")} and others): its return, drawdown and worst day in each one its dates fully cover, and the crises it was not there for, listed as untested and never as passed.`,
|
|
154
|
+
cpcv: `Combinatorial purged cross-validation of a search: choose the best trial on training days, score it on held-out blocks, with purge and embargo at the edges, for every choice of blocks, stitched into whole paths. Returns the spread of out-of-sample Sharpes against the in-sample one. Splits can run as shards on other machines and merge back. ${CPCV_LIMITS_TEXT[1]}`,
|
|
155
|
+
backtest_signal: `Backtest a signal against your prices: rank, long and short quantiles, rebalance, trading only on what was known lag_days earlier, costs on actual trades. Every reply holds the result against the crises its dates cover and lists the ones it does not. With capital_usd and dollar_volume_file, orders fill only as far as the market traded, pay square-root impact, and the reply shows the Sharpe at each size the market can take. Pass ledger to judge the best of a search against all of it. ${ENGINE_LIMITS_TEXT[1]}`,
|
|
118
156
|
});
|
|
119
157
|
|
|
120
158
|
export async function toolPitFactor(session, args) {
|
|
@@ -141,23 +179,57 @@ export async function toolPitFactor(session, args) {
|
|
|
141
179
|
});
|
|
142
180
|
}
|
|
143
181
|
|
|
182
|
+
const CAPACITY_LADDER = Object.freeze([1e5, 1e6, 1e7, 1e8, 1e9]);
|
|
183
|
+
|
|
144
184
|
export async function toolBacktestSignal(session, args) {
|
|
145
185
|
const a = backtestInput.parse(args);
|
|
146
186
|
const prices = readPanel(a.prices_file, "prices_file");
|
|
147
187
|
const signal = readPanel(a.signal_file, "signal_file");
|
|
148
188
|
const tickers = prices.tickers.filter((t) => signal.tickers.includes(t));
|
|
149
189
|
const cfg = { rebalance: a.rebalance ?? "monthly", lagDays: a.lag_days ?? 1, side: a.side ?? "long_short", quantile: a.quantile ?? 0.2, costBps: a.cost_bps ?? 10, minNames: a.min_names ?? 5, periodsPerYear: a.periods_per_year ?? 252 };
|
|
150
|
-
|
|
190
|
+
if (a.capital_usd && !a.dollar_volume_file) throw new RangeError("capital_usd needs dollar_volume_file: without volume nothing says how much the market can take");
|
|
191
|
+
if (a.dollar_volume_file && !a.capital_usd) throw new RangeError("dollar_volume_file needs capital_usd: volume only matters at a size");
|
|
192
|
+
const volumes = a.dollar_volume_file ? readPanel(a.dollar_volume_file, "dollar_volume_file") : null;
|
|
193
|
+
if (volumes && volumes.dates.join() !== prices.dates.join()) throw new RangeError("dollar_volume_file must have the same dates as prices_file");
|
|
194
|
+
const size = volumes ? { dollarVolumes: volumes.values, maxParticipation: a.max_participation ?? 0.1, impactCoefficient: a.impact_coefficient ?? 1 } : {};
|
|
195
|
+
const run = (capital) => runBacktest({ dates: prices.dates, tickers, prices: prices.values, signalDates: signal.dates, signals: signal.values, ...cfg, ...size, ...(capital ? { capital } : {}) });
|
|
196
|
+
const result = run(a.capital_usd);
|
|
151
197
|
const stats = describe(result);
|
|
152
|
-
const
|
|
198
|
+
const crises = crisisReplay({ dates: result.dates, returns: result.returns, ...(a.crisis_drawdown_limit ? { limit: a.crisis_drawdown_limit } : {}) });
|
|
199
|
+
const capacity = a.capital_usd ? CAPACITY_LADDER.map((capital) => {
|
|
200
|
+
const r = run(capital), d = describe(r);
|
|
201
|
+
// A book that cannot fill holds stale positions, and its Sharpe can even rise: not a figure to choose a size by.
|
|
202
|
+
const tradable = r.liquidity.verdict === "tradable";
|
|
203
|
+
return { capital_usd: capital, sharpe_annualized: tradable ? d.sharpe_annualized : null, annualized_return: tradable ? d.annualized_return : null, fill_ratio: r.liquidity.fill_ratio, impact_paid: r.liquidity.impact_paid, verdict: r.liquidity.verdict };
|
|
204
|
+
}) : null;
|
|
205
|
+
const settings = { rebalance: cfg.rebalance, lag_days: cfg.lagDays, side: cfg.side, quantile: cfg.quantile, cost_bps: cfg.costBps, min_names: cfg.minNames, ...(a.capital_usd ? { capital_usd: a.capital_usd, max_participation: size.maxParticipation, impact_coefficient: size.impactCoefficient } : {}) };
|
|
153
206
|
let search = null;
|
|
154
207
|
if (a.ledger) {
|
|
155
|
-
const label = a.label ?? `${a.signal_file.split("/").pop()} ${cfg.side} q${cfg.quantile} ${cfg.rebalance} lag${cfg.lagDays} ${cfg.costBps}bps`;
|
|
208
|
+
const label = a.label ?? `${a.signal_file.split("/").pop()} ${cfg.side} q${cfg.quantile} ${cfg.rebalance} lag${cfg.lagDays} ${cfg.costBps}bps${a.capital_usd ? ` $${a.capital_usd}` : ""}`;
|
|
156
209
|
const r = recordTrial(session.ledgerDir, { ledger: a.ledger, label, returns: result.returns, periods_per_year: cfg.periodsPerYear, params: { ...settings, signal_file: a.signal_file } });
|
|
157
210
|
search = { ledger: r.ledger, trial: r.trial, trials: r.trials, best: r.best, deflated: r.deflated, overfitting: r.overfitting, plain_reading: r.plain_reading };
|
|
158
211
|
}
|
|
159
212
|
const plain = `${stats.first_date} to ${stats.last_date}, ${tickers.length} tickers: annualized Sharpe ${stats.sharpe_annualized?.toFixed(2)}, return ${(100 * (stats.annualized_return ?? 0)).toFixed(1)}% a year, worst drawdown ${(100 * stats.max_drawdown).toFixed(1)}%, turnover ${stats.turnover_per_year?.toFixed(1)}x a year after ${cfg.costBps} bps costs. Every trade used signals at least ${stats.causality.min_days_signal_to_trade} calendar days old.`;
|
|
160
|
-
|
|
213
|
+
const fails = [
|
|
214
|
+
...(result.liquidity?.verdict === "too_big" ? [`Too big to trade: at $${a.capital_usd.toLocaleString("en-US")} only ${(100 * result.liquidity.fill_ratio).toFixed(0)}% of the wanted trading could fill; the figures below are for the smaller book that did.`] : []),
|
|
215
|
+
...crises.tested.filter((c) => c.ruptured).map((c) => `Ruptured in ${c.what}: a ${(100 * c.drawdown).toFixed(0)}% drawdown against a ${(100 * crises.drawdown_limit).toFixed(0)}% limit.`),
|
|
216
|
+
];
|
|
217
|
+
const coverage = `Crises: ${crises.tested.length} covered by these dates, ${crises.untested.length} untested${crises.untested.length ? ` (not having been there is not surviving them)` : ""}.`;
|
|
218
|
+
return text({ settings, tickers: tickers.length, fails, ...stats, crises, ...(result.liquidity ? { liquidity: result.liquidity, capacity } : {}), plain_reading: [...fails, plain, coverage].join(" "), ...(search ? { search } : {}), limits: ENGINE_LIMITS_TEXT });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function toolCrisisReplay(args) {
|
|
222
|
+
const a = crisisInput.parse(args);
|
|
223
|
+
const p = readPanel(a.returns_file, "returns_file");
|
|
224
|
+
if (p.tickers.length > 50) throw new RangeError("returns_file has more than 50 columns; crisis_replay takes at most 50");
|
|
225
|
+
const series = p.tickers.map((name) => {
|
|
226
|
+
// A series counts as there from its first value to its last; blank days in between earn nothing.
|
|
227
|
+
const v = p.values[name], first = v.findIndex((x) => x !== null), last = v.findLastIndex((x) => x !== null);
|
|
228
|
+
if (first < 0) return { name, verdict: "untested", tested: [], untested: Object.entries(CRISES).map(([id, c]) => ({ id, what: c.what, from: c.from, to: c.to })) };
|
|
229
|
+
return { name, ...crisisReplay({ dates: p.dates.slice(first, last + 1), returns: v.slice(first, last + 1).map((x) => x ?? 0), ...(a.drawdown_limit ? { limit: a.drawdown_limit } : {}) }) };
|
|
230
|
+
});
|
|
231
|
+
const ruptured = series.filter((s) => s.verdict === "ruptured");
|
|
232
|
+
return text({ series, plain_reading: `${series.length} ${series.length === 1 ? "strategy" : "strategies"} held against ${Object.keys(CRISES).length} crises: ${ruptured.length} ruptured${ruptured.length ? ` (${ruptured.slice(0, 5).map((s) => s.name).join(", ")})` : ""}. A crisis outside a strategy's dates is untested, not passed; surviving past crises says nothing about the next one.` });
|
|
161
233
|
}
|
|
162
234
|
|
|
163
235
|
function readGroups(path) {
|
|
@@ -224,6 +296,27 @@ export function registerTools(server, session) {
|
|
|
224
296
|
tool("list_factors", "Point-in-time factors", z.object({}).strict(), READ_ONLY, async () => text({ factors: Object.fromEntries(Object.entries(FACTORS).map(([k, v]) => [k, v.meaning])), plus: "level:<measure> for any annual measure (revenue, net_income, assets, equity, operating_cash_flow, ...)", limits: PIT_LIMITS_TEXT }));
|
|
225
297
|
tool("pit_factor", "Build a point-in-time factor", pitInput, { ...WRITES_LOCAL, openWorldHint: true }, (args) => toolPitFactor(session, args));
|
|
226
298
|
tool("backtest_signal", "Backtest a signal without lookahead", backtestInput, WRITES_LOCAL, (args) => toolBacktestSignal(session, args));
|
|
299
|
+
tool("proof_filings", "Add company filings to a proof ledger", proofFilingsInput, { ...WRITES_LOCAL, openWorldHint: true }, async (args) => text(await proofFilings(session.markets ??= marketsSession(), args)));
|
|
300
|
+
tool("proof_file", "Record a file in a proof ledger", proofFileInput, WRITES_LOCAL, async (args) => text(proofFile(args)));
|
|
301
|
+
tool("proof_verify", "Verify a proof ledger", proofVerifyInput, READ_ONLY, async (args) => { const a = proofVerifyInput.parse(args); return text(verifyLedger(a.ledger_file, a.anchors ?? [])); });
|
|
302
|
+
tool("proof_find", "Look up a proof ledger", proofFindInput, READ_ONLY, async (args) => text(proofFind(args)));
|
|
303
|
+
tool("league_new", "Start a paper league", leagueNewInput, WRITES_LOCAL, async (args) => text(leagueNew(args)));
|
|
304
|
+
tool("league_commit", "Commit a league entry", leagueCommitInput, WRITES_LOCAL, async (args) => text(leagueCommit(args)));
|
|
305
|
+
tool("league_score", "Score a paper league", leagueScoreInput, READ_ONLY, async (args) => text(leagueScore(args)));
|
|
306
|
+
tool("exchange_new", "Start a paper exchange", exchangeNewInput, WRITES_LOCAL, async (args) => text(exchangeNew(args)));
|
|
307
|
+
tool("exchange_join", "Join a paper exchange", exchangeJoinInput, WRITES_LOCAL, async (args) => text(exchangeJoin(args)));
|
|
308
|
+
tool("exchange_view", "See a paper exchange", exchangeViewInput, READ_ONLY, async (args) => text(exchangeView(args)));
|
|
309
|
+
tool("exchange_orders", "Send orders to a paper exchange", exchangeOrdersInput, WRITES_LOCAL, async (args) => text(exchangeOrders(args)));
|
|
310
|
+
tool("exchange_step", "Move a paper exchange on", exchangeStepInput, WRITES_LOCAL, async (args) => text(exchangeStep(args)));
|
|
311
|
+
tool("exchange_report", "Report a paper exchange", exchangeReportInput, READ_ONLY, async (args) => text(exchangeReport(args)));
|
|
312
|
+
tool("exchange_run", "Run a whole paper exchange", exchangeRunInput, READ_ONLY, async (args) => text(exchangeRun(args)));
|
|
313
|
+
tool("agent_consensus", "Combine agents' forecasts and count independent views", consensusInput, READ_ONLY, async (args) => text(agentConsensus(args)));
|
|
314
|
+
tool("find_diversifiers", "Find what diversifies a book", diversifiersInput, READ_ONLY, async (args) => text(findDiversifiers(args)));
|
|
315
|
+
tool("arena_new", "Start a paper arena", arenaNewInput, WRITES_LOCAL, async (args) => text(arenaNew(args)));
|
|
316
|
+
tool("arena_orders", "Send paper orders to an arena", arenaOrdersInput, WRITES_LOCAL, async (args) => text(arenaOrders(args)));
|
|
317
|
+
tool("arena_step", "Clear a paper arena", arenaStepInput, WRITES_LOCAL, async (args) => text(arenaStep(args)));
|
|
318
|
+
tool("crisis_replay", "Replay strategies through named crises", crisisInput, READ_ONLY, (args) => toolCrisisReplay(args));
|
|
319
|
+
tool("cpcv", "Combinatorial purged cross-validation", cpcvInput, WRITES_LOCAL, async (args) => text(cpcv(args)));
|
|
227
320
|
tool("price_panel", "Build a price panel", panelInput, { ...WRITES_LOCAL, openWorldHint: true }, async (args) => text(await buildPanel(session.markets ??= marketsSession(), args)));
|
|
228
321
|
tool("breadth_signals", "Breadth lab signals", z.object({}).strict(), READ_ONLY, async () => text({ signals: signalCatalog(), next: "breadth_book with your prices_file (and groups_file, dollar_volume_file) builds a sleeve per signal per group.", limits: BREADTH_LIMITS_TEXT }));
|
|
229
322
|
tool("breadth_book", "Breadth book of sleeves", breadthInput, WRITES_LOCAL, (args) => toolBreadthBook(session, args));
|