@almanak/mcp-server 0.2.4 → 0.2.6
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/dist/backtest-v3.d.ts +61 -0
- package/dist/backtest-v3.js +215 -0
- package/dist/backtest-v3.js.map +1 -0
- package/dist/client.d.ts +14 -0
- package/dist/client.js +16 -2
- package/dist/client.js.map +1 -1
- package/dist/testing/__tests__/backtest-v3-tool.test.d.ts +1 -0
- package/dist/testing/__tests__/backtest-v3-tool.test.js +473 -0
- package/dist/testing/__tests__/backtest-v3-tool.test.js.map +1 -0
- package/dist/testing/__tests__/fixtures/pre-v3-backtest-tools.d.ts +76 -0
- package/dist/testing/__tests__/fixtures/pre-v3-backtest-tools.js +95 -0
- package/dist/testing/__tests__/fixtures/pre-v3-backtest-tools.js.map +1 -0
- package/dist/testing/__tests__/runner.test.js +1 -1
- package/dist/testing/__tests__/runner.test.js.map +1 -1
- package/dist/testing/__tests__/tools.test.js +28 -0
- package/dist/testing/__tests__/tools.test.js.map +1 -1
- package/dist/testing/runner.d.ts +2 -1
- package/dist/testing/runner.js +1 -1
- package/dist/testing/runner.js.map +1 -1
- package/dist/tools.d.ts +19 -0
- package/dist/tools.js +392 -33
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -48,8 +48,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
48
48
|
exports.PlatformToolHandler = void 0;
|
|
49
49
|
const node_fs_1 = require("node:fs");
|
|
50
50
|
const path = __importStar(require("node:path"));
|
|
51
|
+
const client_1 = require("./client");
|
|
51
52
|
const runner_1 = require("./testing/runner");
|
|
52
53
|
const workspace_git_1 = require("./workspace-git");
|
|
54
|
+
const backtest_v3_1 = require("./backtest-v3");
|
|
53
55
|
const EXPORT_COMMIT_MESSAGE = 'feat: initial strategy export from Almanak';
|
|
54
56
|
const ROOT_DIR_ALLOWED_RE = /^[a-zA-Z0-9._/-]+$/;
|
|
55
57
|
const WORKSPACE_ABSOLUTE_PATH_RE = /^\/(?:tmp\/)?workspaces\/[^/]+\/(.+)$/;
|
|
@@ -58,6 +60,9 @@ const COMMIT_SHA_PREFIX_RE = /^[0-9a-fA-F]{7,40}$/;
|
|
|
58
60
|
const FULL_COMMIT_SHA_RE = /^[0-9a-fA-F]{40}$/;
|
|
59
61
|
const PRICE_TIMEFRAMES = ['auto', '1m', '5m', '15m', '1h', '4h', '1d'];
|
|
60
62
|
const PRICE_TIMEFRAME_SET = new Set(PRICE_TIMEFRAMES);
|
|
63
|
+
// strategies_spec field units. The frontend SpecPanel (SpecFieldUnit in
|
|
64
|
+
// packages/frontend/src/stores/almanak-code-store.ts) renders each one; keep both in step.
|
|
65
|
+
const SPEC_FIELD_UNITS = ['pct', 'pct_of_balance', 'usd', 'bps', 'raw'];
|
|
61
66
|
// Decision grid options, in seconds (1m, 5m, 15m, 1h, 4h, 1d): the SDK's canonical cadences, mirrored by the
|
|
62
67
|
// platform API validation. The default is one hour.
|
|
63
68
|
const DECISION_INTERVALS_SECONDS = [60, 300, 900, 3600, 14400, 86400];
|
|
@@ -105,9 +110,9 @@ function parseSdkVersion(value) {
|
|
|
105
110
|
phaseNumber: phaseNumber ? Number(phaseNumber) : 0,
|
|
106
111
|
};
|
|
107
112
|
}
|
|
108
|
-
function isSdkVersionBacktestable(version) {
|
|
113
|
+
function isSdkVersionBacktestable(version, minimum = MIN_BACKTEST_SDK_VERSION) {
|
|
109
114
|
const left = parseSdkVersion(version);
|
|
110
|
-
const right = parseSdkVersion(
|
|
115
|
+
const right = parseSdkVersion(minimum);
|
|
111
116
|
if (!left || !right)
|
|
112
117
|
return false;
|
|
113
118
|
for (const key of ['major', 'minor', 'patch']) {
|
|
@@ -118,6 +123,67 @@ function isSdkVersionBacktestable(version) {
|
|
|
118
123
|
return SDK_PHASE_RANK[left.phase] > SDK_PHASE_RANK[right.phase];
|
|
119
124
|
return left.phaseNumber >= right.phaseNumber;
|
|
120
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* `backtests_run` as SDK v3 sessions see it. A v3 run executes a backtest YAML committed and pushed in the linked
|
|
128
|
+
* repository with that commit's config.yaml; the window, inventory, cadence and costs live in that file, so none of
|
|
129
|
+
* the v2 window inputs apply.
|
|
130
|
+
*/
|
|
131
|
+
const BACKTESTS_RUN_V3_DEFINITION = {
|
|
132
|
+
name: 'backtests_run',
|
|
133
|
+
description: 'Run a hosted backtest of a linked SDK v3 strategy (SDK v3 backtests are a staging preview). The run executes a ' +
|
|
134
|
+
'backtest YAML committed in the strategy repository together with config.yaml and strategy.py at the same commit. ' +
|
|
135
|
+
'The mode, window, inventory, execution, costs and data policy all come from that file (schema: ' +
|
|
136
|
+
'`almanak support --schema backtest`); this tool takes no window or capital inputs. Only committed, pushed files ' +
|
|
137
|
+
'run: local or unpushed edits are never used, so commit and push the backtest YAML (and any strategy change) first, ' +
|
|
138
|
+
'then call this with commit_sha set to the pushed commit (git rev-parse HEAD after the push). ' +
|
|
139
|
+
'backtest_config_path names the file; omitted, the only committed backtest.yaml or backtest-economic.yaml at the ' +
|
|
140
|
+
'repository root is used. Economic mode (`mode: economic`) refuses banded liquidity allocations: a config.yaml with ' +
|
|
141
|
+
'a `band` allocation is refused before submission (V3_BACKTEST_BAND_UNSUPPORTED). ' +
|
|
142
|
+
'The call submits the run, renders a live backtest widget in the chat and PAUSES until the run finishes; it then ' +
|
|
143
|
+
'resumes you with the outcome as _widgetResponse. A v3 summary carries no v2 metrics: call backtests_results with ' +
|
|
144
|
+
'the backtest_id for the report (return, drawdown, positions, decision interval) or the runner failure (stage, code, ' +
|
|
145
|
+
'message). Do not re-submit while a run is in progress and do not claim results before they arrive. If the user ' +
|
|
146
|
+
'chooses "Continue in background", you are resumed early with status RUNNING; read it later with backtests_results. ' +
|
|
147
|
+
'Errors carry errorCode: COMMIT_UNRESOLVED (the commit is not in the repository: push it), REPO_ACCESS or ' +
|
|
148
|
+
'GITHUB_UNAVAILABLE (repository access, not a push problem), BACKTEST_CONFIG_NOT_FOUND (file not in that commit), ' +
|
|
149
|
+
'BACKTEST_CONFIG_UNREADABLE, BACKTEST_CONFIG_AMBIGUOUS (several files, pass backtest_config_path), ' +
|
|
150
|
+
'V3_BACKTEST_INPUT_INVALID, V3_BACKTEST_BAND_UNSUPPORTED, SDK_GENERATION_MISMATCH (the strategy link is not stored as ' +
|
|
151
|
+
'SDK v3; do not add v2 inputs). Users can have at most 10 active backtests; a 429 BACKTEST_CONCURRENCY_LIMIT error ' +
|
|
152
|
+
'means do not retry until one finishes or is cancelled.',
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
strategy_link_id: {
|
|
157
|
+
type: 'string',
|
|
158
|
+
description: 'The strategy link ID (from strategies_list or the strategies_export_workspace result).',
|
|
159
|
+
},
|
|
160
|
+
backtest_config_path: {
|
|
161
|
+
type: 'string',
|
|
162
|
+
description: 'Repository-relative path of the committed backtest YAML to run, for example "backtest-economic.yaml". ' +
|
|
163
|
+
'Optional when exactly one of backtest.yaml or backtest-economic.yaml is committed at the repository root.',
|
|
164
|
+
},
|
|
165
|
+
commit_sha: {
|
|
166
|
+
type: 'string',
|
|
167
|
+
description: 'Optional 7-40 character hexadecimal commit SHA or unique prefix of a pushed commit; it is resolved to a full SHA ' +
|
|
168
|
+
'in the linked repository. Pass the commit you just pushed. Omitted, the head of branch (or the default branch) runs.',
|
|
169
|
+
},
|
|
170
|
+
branch: {
|
|
171
|
+
type: 'string',
|
|
172
|
+
description: 'Optional branch whose head runs when commit_sha is omitted. Defaults to the repository default branch.',
|
|
173
|
+
},
|
|
174
|
+
show_widget: {
|
|
175
|
+
const: true,
|
|
176
|
+
description: 'Must be true. Renders the live backtest widget and pauses this call until the run completes.',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
required: ['strategy_link_id', 'show_widget'],
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
/** Appended to the backtests_results description in SDK v3 sessions only; other sessions keep the v2 text. */
|
|
183
|
+
const BACKTESTS_RESULTS_V3_NOTE = ' For an SDK v3 run (sdk_generation "v3") it returns the committed backtest_config_path, progress while running, ' +
|
|
184
|
+
'failure (stage preflight/run/infra, code, message) for a FAILED run, and for a COMPLETED run report_summary (mode, ' +
|
|
185
|
+
'window, decision interval, evaluations, metrics, net_quote_return, positions and equity counts) with the full ' +
|
|
186
|
+
'result.json written to .logs/.';
|
|
121
187
|
/** One-line human summary of a spec's `content.exit` for the injected teardown-disclosure field. */
|
|
122
188
|
function summarizeSpecExit(exit) {
|
|
123
189
|
if (exit === undefined || exit === null || (typeof exit === 'object' && Object.keys(exit).length === 0)) {
|
|
@@ -984,7 +1050,7 @@ class PlatformToolHandler {
|
|
|
984
1050
|
});
|
|
985
1051
|
this.register('strategies', {
|
|
986
1052
|
name: 'strategies_spec',
|
|
987
|
-
description: "Show the user an editable Strategy Spec panel to review and approve the strategy's risk posture BEFORE building. Call early — after confirming protocol support, before scaffolding. Pass plainSummary (one-line plain-language money-at-risk), fields (the few user-facing posture choices, each {key,label,description,value,unit}), the full content (the SPEC.json values), optional presets (conservative/balanced/aggressive content variants), and show_widget=true. content must include an `exit` object stating what stop/teardown does (close actions, asset policy), and fields must include an `exit.*` entry so the user sees it. If user constraints conflict with a clean exit, surface the resolution here rather than deciding silently. Execution pauses; the user reviews/edits, then the reply is a fenced ```json``` block `{action, content}` — use the returned `content` as the approved build contract (the full SPEC values with the user's edits applied). Surface only genuine posture choices the strategy actually consults — keep safety internals in config.json.",
|
|
1053
|
+
description: "Show the user an editable Strategy Spec panel to review and approve the strategy's risk posture BEFORE building. Call early — after confirming protocol support, before scaffolding. Pass plainSummary (one-line plain-language money-at-risk), fields (the few user-facing posture choices, each {key,label,description,value,unit}), the full content (the SPEC.json values), optional presets (conservative/balanced/aggressive content variants), and show_widget=true. content must include an `exit` object stating what stop/teardown does (close actions, asset policy), and fields must include an `exit.*` entry so the user sees it. If user constraints conflict with a clean exit, surface the resolution here rather than deciding silently. Give each field the unit that matches its meaning (see fields[].unit): a plain percentage such as an LP range width is `pct`, never `pct_of_balance`. Execution pauses; the user reviews/edits, then the reply is a fenced ```json``` block `{action, content}` — use the returned `content` as the approved build contract (the full SPEC values with the user's edits applied). Surface only genuine posture choices the strategy actually consults — keep safety internals in config.json.",
|
|
988
1054
|
inputSchema: {
|
|
989
1055
|
type: 'object',
|
|
990
1056
|
properties: {
|
|
@@ -1003,8 +1069,14 @@ class PlatformToolHandler {
|
|
|
1003
1069
|
value: { type: ['string', 'number', 'boolean'], description: 'Current value (number | string | boolean).' },
|
|
1004
1070
|
unit: {
|
|
1005
1071
|
type: 'string',
|
|
1006
|
-
enum:
|
|
1007
|
-
description: '
|
|
1072
|
+
enum: SPEC_FIELD_UNITS,
|
|
1073
|
+
description: 'Unit badge for the value exactly as stored in content; the panel never rescales it. ' +
|
|
1074
|
+
'pct = a plain percentage in percent points (8 means 8%): LP range width, recenter/rebalance ' +
|
|
1075
|
+
'thresholds, slippage, stop-loss, drawdown, target LTV. pct_of_balance = ONLY position sizing: the ' +
|
|
1076
|
+
"share of the strategy's funded balance one trade or position uses (percent points). usd = a US " +
|
|
1077
|
+
'dollar amount. bps = basis points (50 means 0.5%). raw = anything else (counts, durations, token ' +
|
|
1078
|
+
'amounts, ticks, ratios, text); no badge. If content stores a percentage as a fraction (0.08 for ' +
|
|
1079
|
+
'8%), use raw and state the scale in the label.',
|
|
1008
1080
|
},
|
|
1009
1081
|
editable: { type: 'boolean' },
|
|
1010
1082
|
min: { type: 'number' },
|
|
@@ -1208,7 +1280,10 @@ class PlatformToolHandler {
|
|
|
1208
1280
|
required: ['strategy_link_id'],
|
|
1209
1281
|
},
|
|
1210
1282
|
}, (args) => this.linkWorkspace(args));
|
|
1211
|
-
|
|
1283
|
+
// The session generation picks the schema the model sees: v2 sessions keep the v2 window inputs unchanged, v3
|
|
1284
|
+
// sessions see the committed-backtest-file inputs. The handler dispatches on the strategy's stored
|
|
1285
|
+
// framework_version either way, which is authoritative.
|
|
1286
|
+
const backtestsRunDefinition = this.options.sdkGeneration === 'v3' ? BACKTESTS_RUN_V3_DEFINITION : {
|
|
1212
1287
|
name: 'backtests_run',
|
|
1213
1288
|
description: 'Run a backtest of a linked strategy against historical market data. Use this after the strategy is in its ' +
|
|
1214
1289
|
'repository (strategies_export_workspace, or git push on a linked chat) and before deploying — backtests run against a ' +
|
|
@@ -1272,7 +1347,8 @@ class PlatformToolHandler {
|
|
|
1272
1347
|
},
|
|
1273
1348
|
required: ['strategy_link_id', 'start_time', 'end_time', 'show_widget'],
|
|
1274
1349
|
},
|
|
1275
|
-
}
|
|
1350
|
+
};
|
|
1351
|
+
this.register('strategies', backtestsRunDefinition, (args) => this.runBacktest(args));
|
|
1276
1352
|
this.register('strategies', {
|
|
1277
1353
|
name: 'backtests_results',
|
|
1278
1354
|
description: "Fetch a backtest run's status and results for diagnosis — including WHY a run traded or held. Returns " +
|
|
@@ -1285,7 +1361,8 @@ class PlatformToolHandler {
|
|
|
1285
1361
|
'returning its absolute path — use Grep/Read on it for the equity curve, trades, and price series (not ' +
|
|
1286
1362
|
'returned inline). Works for any backtest of your linked strategies, including runs started from the ' +
|
|
1287
1363
|
'platform UI and runs from earlier sessions. Use it BEFORE editing strategy code in response to a bad ' +
|
|
1288
|
-
'backtest, to attribute the cause first.'
|
|
1364
|
+
'backtest, to attribute the cause first.' +
|
|
1365
|
+
(this.options.sdkGeneration === 'v3' ? BACKTESTS_RESULTS_V3_NOTE : ''),
|
|
1289
1366
|
inputSchema: {
|
|
1290
1367
|
type: 'object',
|
|
1291
1368
|
properties: {
|
|
@@ -1602,7 +1679,7 @@ class PlatformToolHandler {
|
|
|
1602
1679
|
* turn into a widget (the error card auto-resolves the gate immediately).
|
|
1603
1680
|
*/
|
|
1604
1681
|
async runBacktest(args) {
|
|
1605
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
|
|
1682
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1;
|
|
1606
1683
|
const strategyLinkId = String((_a = args.strategy_link_id) !== null && _a !== void 0 ? _a : '').trim();
|
|
1607
1684
|
const startTime = String((_b = args.start_time) !== null && _b !== void 0 ? _b : '').trim();
|
|
1608
1685
|
// required[] only checks presence — an empty/whitespace id would call
|
|
@@ -1650,6 +1727,28 @@ class PlatformToolHandler {
|
|
|
1650
1727
|
return fail((_e = strategyRes === null || strategyRes === void 0 ? void 0 : strategyRes.statusCode) !== null && _e !== void 0 ? _e : 404, (_g = (_f = strategyRes === null || strategyRes === void 0 ? void 0 : strategyRes.message) !== null && _f !== void 0 ? _f : strategyRes === null || strategyRes === void 0 ? void 0 : strategyRes.error) !== null && _g !== void 0 ? _g : 'Strategy not found');
|
|
1651
1728
|
}
|
|
1652
1729
|
const strategy = strategyRes.data;
|
|
1730
|
+
// The stored framework_version is authoritative for which runner family serves the strategy.
|
|
1731
|
+
const sessionIsV3 = this.options.sdkGeneration === 'v3';
|
|
1732
|
+
if (strategy.framework_version === 'V3') {
|
|
1733
|
+
if (!sessionIsV3) {
|
|
1734
|
+
// The v2 schema requires the window inputs a v3 run refuses; retrying with other inputs cannot succeed here.
|
|
1735
|
+
return fail(400, 'This strategy is stored as SDK v3, but this chat session is not on the SDK v3 lane, so its backtests_run takes ' +
|
|
1736
|
+
'the SDK v2 window inputs that SDK v3 backtests refuse. Do not retry with other inputs: start the run from the ' +
|
|
1737
|
+
"strategy's Backtest page on the platform, or from an SDK v3 chat session attached to this strategy.", { errorCode: 'SDK_GENERATION_MISMATCH' });
|
|
1738
|
+
}
|
|
1739
|
+
return await this.runV3Backtest(args, strategyLinkId, strategy);
|
|
1740
|
+
}
|
|
1741
|
+
if (sessionIsV3) {
|
|
1742
|
+
return fail(400, `The strategy link is stored as SDK ${String((_h = strategy.framework_version) !== null && _h !== void 0 ? _h : 'v2').toLowerCase()}, but this chat session ` +
|
|
1743
|
+
'runs the SDK v3 lane. The stored generation decides which backtest runs, and the two disagree, so nothing was ' +
|
|
1744
|
+
'submitted. Do not add SDK v2 window inputs; tell the user the strategy link is not recognised as SDK v3 ' +
|
|
1745
|
+
'(its repository must resolve almanak 3.x and carry strategy.py and config.yaml at the root).', { errorCode: 'SDK_GENERATION_MISMATCH' });
|
|
1746
|
+
}
|
|
1747
|
+
const v3Inputs = backtest_v3_1.V3_ONLY_BACKTEST_ARGS.filter((key) => args[key] !== undefined);
|
|
1748
|
+
if (v3Inputs.length > 0) {
|
|
1749
|
+
return fail(400, `${v3Inputs.join(', ')} ${v3Inputs.length > 1 ? 'are SDK v3 inputs' : 'is an SDK v3 input'}, and this strategy is ` +
|
|
1750
|
+
'stored as SDK v2: its backtest takes start_time and end_time (YYYY-MM-DD).', { errorCode: 'SDK_GENERATION_MISMATCH' });
|
|
1751
|
+
}
|
|
1653
1752
|
const strategyName = (typeof strategy.display_name === 'string' && strategy.display_name) ||
|
|
1654
1753
|
(typeof strategy.github_repo_name === 'string' && strategy.github_repo_name) ||
|
|
1655
1754
|
null;
|
|
@@ -1675,19 +1774,19 @@ class PlatformToolHandler {
|
|
|
1675
1774
|
const refType = explicitSha ? 'commit' : 'branch';
|
|
1676
1775
|
const refConfigRes = await this.client.getStrategyRefConfig(strategyLinkId, ref, refType);
|
|
1677
1776
|
if ((refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.valid) === false || !(refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.data)) {
|
|
1678
|
-
const baseError = (
|
|
1777
|
+
const baseError = (_k = (_j = refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.message) !== null && _j !== void 0 ? _j : refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.error) !== null && _k !== void 0 ? _k : `Could not resolve ref "${ref}" for the linked strategy repository.`;
|
|
1679
1778
|
const recoveryGuidance = explicitSha
|
|
1680
1779
|
? ` Omit commit_sha only if the user explicitly approves running the latest commit on ${defaultBranch}; ` +
|
|
1681
1780
|
'do not retry with different code automatically.'
|
|
1682
1781
|
: '';
|
|
1683
|
-
return fail((
|
|
1684
|
-
errorCode: (
|
|
1782
|
+
return fail((_l = refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.statusCode) !== null && _l !== void 0 ? _l : 400, `${baseError}${recoveryGuidance}`, {
|
|
1783
|
+
errorCode: (_m = refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.errorCode) !== null && _m !== void 0 ? _m : 'INTERNAL_ERROR',
|
|
1685
1784
|
retryable: (refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.retryable) === true,
|
|
1686
1785
|
...recoveryContext,
|
|
1687
1786
|
});
|
|
1688
1787
|
}
|
|
1689
1788
|
const refConfig = refConfigRes.data;
|
|
1690
|
-
const commitSha = (
|
|
1789
|
+
const commitSha = (_o = refConfig.github_commit_sha) !== null && _o !== void 0 ? _o : '';
|
|
1691
1790
|
if (!FULL_COMMIT_SHA_RE.test(commitSha)) {
|
|
1692
1791
|
return fail(400, `Could not resolve "${ref}" to a full commit SHA — has the strategy been pushed to its repository?`, {
|
|
1693
1792
|
errorCode: 'COMMIT_UNRESOLVED',
|
|
@@ -1698,17 +1797,22 @@ class PlatformToolHandler {
|
|
|
1698
1797
|
// /ref-config — pin it to the resolved commit so it matches the config.
|
|
1699
1798
|
const refMetaRes = await this.client.getStrategyRefMetadata(strategyLinkId, commitSha);
|
|
1700
1799
|
if ((refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.valid) === false || !(refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.data)) {
|
|
1701
|
-
return fail((
|
|
1800
|
+
return fail((_p = refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.statusCode) !== null && _p !== void 0 ? _p : 400, (_r = (_q = refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.message) !== null && _q !== void 0 ? _q : refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.error) !== null && _r !== void 0 ? _r : 'Could not load strategy ref metadata');
|
|
1702
1801
|
}
|
|
1703
|
-
const sdkVersion = (
|
|
1802
|
+
const sdkVersion = (_s = refMetaRes.data.almanak_sdk_version) !== null && _s !== void 0 ? _s : null;
|
|
1704
1803
|
if (!sdkVersion) {
|
|
1705
1804
|
return fail(400, 'Could not detect the Almanak SDK version from pyproject.toml at the selected commit.');
|
|
1706
1805
|
}
|
|
1806
|
+
if (((_u = (_t = parseSdkVersion(sdkVersion)) === null || _t === void 0 ? void 0 : _t.major) !== null && _u !== void 0 ? _u : 0) >= 3) {
|
|
1807
|
+
// The backend refuses a v3 SDK against a V2 record; say why instead of submitting the v2 shape.
|
|
1808
|
+
return fail(400, `Commit ${commitSha.slice(0, 7)} resolves almanak ${sdkVersion} (SDK v3), but the strategy link is stored as SDK v2. ` +
|
|
1809
|
+
'The generations disagree, so nothing was submitted; SDK v2 window inputs cannot backtest an SDK v3 repository.', { errorCode: 'SDK_GENERATION_MISMATCH', commit_sha: commitSha, sdk_version: sdkVersion });
|
|
1810
|
+
}
|
|
1707
1811
|
if (!isSdkVersionBacktestable(sdkVersion)) {
|
|
1708
1812
|
return fail(400, `Backtesting requires Almanak SDK >= ${MIN_BACKTEST_SDK_VERSION}; the repo pins ${sdkVersion}. ` +
|
|
1709
1813
|
'Upgrade the almanak dependency in pyproject.toml, push, and re-run.');
|
|
1710
1814
|
}
|
|
1711
|
-
const strategyConfig = (
|
|
1815
|
+
const strategyConfig = (_v = args.strategy_config) !== null && _v !== void 0 ? _v : refConfig.effective_config_json;
|
|
1712
1816
|
const submit = await this.client.submitBacktest({
|
|
1713
1817
|
github_strategy_link_id: strategyLinkId,
|
|
1714
1818
|
commit_sha: commitSha,
|
|
@@ -1717,7 +1821,7 @@ class PlatformToolHandler {
|
|
|
1717
1821
|
backtest_config: backtestConfig,
|
|
1718
1822
|
});
|
|
1719
1823
|
if ((submit === null || submit === void 0 ? void 0 : submit.valid) === false || typeof (submit === null || submit === void 0 ? void 0 : submit.id) !== 'string') {
|
|
1720
|
-
return fail((
|
|
1824
|
+
return fail((_w = submit === null || submit === void 0 ? void 0 : submit.statusCode) !== null && _w !== void 0 ? _w : 400, (_y = (_x = submit === null || submit === void 0 ? void 0 : submit.error) !== null && _x !== void 0 ? _x : submit === null || submit === void 0 ? void 0 : submit.message) !== null && _y !== void 0 ? _y : 'Backtest submission failed', {
|
|
1721
1825
|
commit_sha: commitSha,
|
|
1722
1826
|
sdk_version: sdkVersion,
|
|
1723
1827
|
...(typeof (submit === null || submit === void 0 ? void 0 : submit.errorCode) === 'string' ? { errorCode: submit.errorCode } : {}),
|
|
@@ -1729,8 +1833,8 @@ class PlatformToolHandler {
|
|
|
1729
1833
|
}
|
|
1730
1834
|
return {
|
|
1731
1835
|
backtest_id: submit.id,
|
|
1732
|
-
status: (
|
|
1733
|
-
created_at: (
|
|
1836
|
+
status: (_z = submit.status) !== null && _z !== void 0 ? _z : 'PENDING',
|
|
1837
|
+
created_at: (_0 = submit.created_at) !== null && _0 !== void 0 ? _0 : null,
|
|
1734
1838
|
strategy_link_id: strategyLinkId,
|
|
1735
1839
|
strategy_name: strategyName,
|
|
1736
1840
|
commit_sha: commitSha,
|
|
@@ -1739,6 +1843,175 @@ class PlatformToolHandler {
|
|
|
1739
1843
|
show_widget: true,
|
|
1740
1844
|
};
|
|
1741
1845
|
}
|
|
1846
|
+
catch (err) {
|
|
1847
|
+
return fail(502, `Backtest submission failed: ${(_1 = err === null || err === void 0 ? void 0 : err.message) !== null && _1 !== void 0 ? _1 : String(err)}`);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Submit an SDK v3 run: resolve the pushed commit, pick the committed backtest YAML the same way the strategy's
|
|
1852
|
+
* Backtest page does, and send `backtest_config_path` (never the v2 `backtest_config`). Like runBacktest it never
|
|
1853
|
+
* throws and every path returns a widget-renderable shape.
|
|
1854
|
+
*/
|
|
1855
|
+
async runV3Backtest(args, strategyLinkId, strategy) {
|
|
1856
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
|
|
1857
|
+
const strategyName = (typeof strategy.display_name === 'string' && strategy.display_name) ||
|
|
1858
|
+
(typeof strategy.github_repo_name === 'string' && strategy.github_repo_name) ||
|
|
1859
|
+
null;
|
|
1860
|
+
const repoOwner = typeof strategy.github_repo_owner === 'string' ? strategy.github_repo_owner : '';
|
|
1861
|
+
const repoName = typeof strategy.github_repo_name === 'string' ? strategy.github_repo_name : '';
|
|
1862
|
+
const linkedRepo = repoOwner && repoName ? `${repoOwner}/${repoName}` : null;
|
|
1863
|
+
const defaultBranch = (typeof strategy.github_default_branch === 'string' && strategy.github_default_branch.trim()) || 'main';
|
|
1864
|
+
const context = { strategy_link_id: strategyLinkId, strategy_name: strategyName, sdk_generation: 'v3' };
|
|
1865
|
+
const fail = (statusCode, error, extra) => ({ statusCode, error, ...context, ...extra });
|
|
1866
|
+
const v2Inputs = backtest_v3_1.V2_ONLY_BACKTEST_ARGS.filter((key) => args[key] !== undefined);
|
|
1867
|
+
if (v2Inputs.length > 0) {
|
|
1868
|
+
return fail(400, `${v2Inputs.join(', ')} ${v2Inputs.length > 1 ? 'are SDK v2 inputs' : 'is an SDK v2 input'}. An SDK v3 backtest takes its ` +
|
|
1869
|
+
'window, inventory, cadence and costs from the committed backtest YAML: set them in that file, commit and push it, ' +
|
|
1870
|
+
'and call again with backtest_config_path and without the v2 inputs.', { errorCode: 'V3_BACKTEST_INPUT_INVALID' });
|
|
1871
|
+
}
|
|
1872
|
+
let requestedPath = null;
|
|
1873
|
+
if (args.backtest_config_path !== undefined) {
|
|
1874
|
+
const normalized = (0, backtest_v3_1.normalizeBacktestConfigPath)(args.backtest_config_path);
|
|
1875
|
+
if ('error' in normalized) {
|
|
1876
|
+
return fail(400, normalized.error, { errorCode: 'V3_BACKTEST_INPUT_INVALID' });
|
|
1877
|
+
}
|
|
1878
|
+
requestedPath = normalized.path;
|
|
1879
|
+
context.backtest_config_path = requestedPath;
|
|
1880
|
+
}
|
|
1881
|
+
const explicitSha = typeof args.commit_sha === 'string' ? args.commit_sha.trim() : '';
|
|
1882
|
+
const branch = typeof args.branch === 'string' ? args.branch.trim() : '';
|
|
1883
|
+
if (explicitSha && branch) {
|
|
1884
|
+
return fail(400, 'Pass commit_sha or branch, not both: commit_sha already names the exact code to run.', {
|
|
1885
|
+
errorCode: 'V3_BACKTEST_INPUT_INVALID',
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
if (explicitSha && !COMMIT_SHA_PREFIX_RE.test(explicitSha)) {
|
|
1889
|
+
return fail(400, `commit_sha must contain 7 to 40 hexadecimal characters, got "${explicitSha}".`, {
|
|
1890
|
+
errorCode: 'INVALID_COMMIT_FORMAT',
|
|
1891
|
+
requestedCommit: explicitSha,
|
|
1892
|
+
linkedRepo,
|
|
1893
|
+
defaultBranch,
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
if (args.branch !== undefined && !(0, backtest_v3_1.isValidBranchName)(branch)) {
|
|
1897
|
+
return fail(400, `branch must be a git branch name, got "${branch}".`, { errorCode: 'V3_BACKTEST_INPUT_INVALID' });
|
|
1898
|
+
}
|
|
1899
|
+
const ref = explicitSha ? explicitSha.toLowerCase() : branch || defaultBranch;
|
|
1900
|
+
const refType = explicitSha ? 'commit' : 'branch';
|
|
1901
|
+
const recoveryContext = { requestedCommit: explicitSha || null, linkedRepo, defaultBranch };
|
|
1902
|
+
try {
|
|
1903
|
+
const refConfigRes = await this.client.getStrategyRefConfig(strategyLinkId, ref, refType);
|
|
1904
|
+
const commitSha = (refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.valid) === false ? '' : String((_b = (_a = refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.data) === null || _a === void 0 ? void 0 : _a.github_commit_sha) !== null && _b !== void 0 ? _b : '');
|
|
1905
|
+
if (!FULL_COMMIT_SHA_RE.test(commitSha)) {
|
|
1906
|
+
const baseError = (_d = (_c = refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.message) !== null && _c !== void 0 ? _c : refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.error) !== null && _d !== void 0 ? _d : `Could not resolve ${refType} "${ref}" in the linked strategy repository.`;
|
|
1907
|
+
// Push advice fits only a ref the repository does not have; access and availability errors keep their own text.
|
|
1908
|
+
const errorCode = (refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.valid) === false ? ((_e = refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.errorCode) !== null && _e !== void 0 ? _e : 'INTERNAL_ERROR') : 'COMMIT_UNRESOLVED';
|
|
1909
|
+
const guidance = errorCode !== 'COMMIT_UNRESOLVED'
|
|
1910
|
+
? ''
|
|
1911
|
+
: explicitSha
|
|
1912
|
+
? ` Only pushed commits can be backtested: make sure the commit is pushed to ${linkedRepo !== null && linkedRepo !== void 0 ? linkedRepo : 'the linked repository'} ` +
|
|
1913
|
+
'(git push, then git rev-parse HEAD) and call again with that SHA. Do not switch to a different commit without the user.'
|
|
1914
|
+
: ` Push the branch to ${linkedRepo !== null && linkedRepo !== void 0 ? linkedRepo : 'the linked repository'} first.`;
|
|
1915
|
+
return fail((_f = refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.statusCode) !== null && _f !== void 0 ? _f : 400, `${baseError}${guidance}`, {
|
|
1916
|
+
errorCode,
|
|
1917
|
+
retryable: (refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.retryable) === true,
|
|
1918
|
+
...recoveryContext,
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
context.commit_sha = commitSha;
|
|
1922
|
+
const refMetaRes = await this.client.getStrategyRefMetadata(strategyLinkId, commitSha);
|
|
1923
|
+
if ((refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.valid) === false || !(refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.data)) {
|
|
1924
|
+
return fail((_g = refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.statusCode) !== null && _g !== void 0 ? _g : 400, (_j = (_h = refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.message) !== null && _h !== void 0 ? _h : refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.error) !== null && _j !== void 0 ? _j : 'Could not load strategy ref metadata');
|
|
1925
|
+
}
|
|
1926
|
+
const meta = refMetaRes.data;
|
|
1927
|
+
const sdkVersion = typeof meta.almanak_sdk_version === 'string' ? meta.almanak_sdk_version : '';
|
|
1928
|
+
if (!sdkVersion) {
|
|
1929
|
+
return fail(400, 'Could not detect the almanak SDK version (uv.lock or pyproject.toml) at the selected commit.', {
|
|
1930
|
+
errorCode: 'V3_BACKTEST_INPUT_INVALID',
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
context.sdk_version = sdkVersion;
|
|
1934
|
+
if (((_l = (_k = parseSdkVersion(sdkVersion)) === null || _k === void 0 ? void 0 : _k.major) !== null && _l !== void 0 ? _l : 0) < 3 || !isSdkVersionBacktestable(sdkVersion, backtest_v3_1.MIN_BACKTEST_SDK_VERSION_V3)) {
|
|
1935
|
+
return fail(400, `Hosted SDK v3 backtests need almanak ${backtest_v3_1.MIN_BACKTEST_SDK_VERSION_V3} or newer; commit ${commitSha.slice(0, 7)} resolves ` +
|
|
1936
|
+
`${sdkVersion}. Update the almanak pin (uv.lock / pyproject.toml), commit, push and re-run.`, { errorCode: 'V3_BACKTEST_INPUT_INVALID' });
|
|
1937
|
+
}
|
|
1938
|
+
const committed = {};
|
|
1939
|
+
const configs = meta.backtest_configs && typeof meta.backtest_configs === 'object' ? meta.backtest_configs : {};
|
|
1940
|
+
for (const [name, text] of Object.entries(configs)) {
|
|
1941
|
+
if (typeof text === 'string')
|
|
1942
|
+
committed[name] = text;
|
|
1943
|
+
}
|
|
1944
|
+
const discovered = Array.isArray(meta.backtest_config_paths)
|
|
1945
|
+
? meta.backtest_config_paths.filter((name) => typeof name === 'string')
|
|
1946
|
+
: Object.keys(committed);
|
|
1947
|
+
const shortSha = commitSha.slice(0, 7);
|
|
1948
|
+
let backtestPath = requestedPath;
|
|
1949
|
+
if (!backtestPath) {
|
|
1950
|
+
if (discovered.length === 1) {
|
|
1951
|
+
backtestPath = discovered[0];
|
|
1952
|
+
}
|
|
1953
|
+
else if (discovered.length === 0) {
|
|
1954
|
+
return fail(404, `Commit ${shortSha} has no backtest.yaml or backtest-economic.yaml at the repository root. Write the backtest YAML ` +
|
|
1955
|
+
'(schema: almanak support --schema backtest), commit and push it, then call again with backtest_config_path.', { errorCode: 'BACKTEST_CONFIG_NOT_FOUND' });
|
|
1956
|
+
}
|
|
1957
|
+
else {
|
|
1958
|
+
return fail(400, `Commit ${shortSha} has several backtest files (${discovered.join(', ')}); pass backtest_config_path to choose one.`, { errorCode: 'BACKTEST_CONFIG_AMBIGUOUS', backtest_config_paths: discovered });
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
context.backtest_config_path = backtestPath;
|
|
1962
|
+
let backtestYaml = (_m = committed[backtestPath]) !== null && _m !== void 0 ? _m : null;
|
|
1963
|
+
if (backtestYaml === null) {
|
|
1964
|
+
const fileRes = await this.client.getStrategyFileContent(strategyLinkId, backtestPath, commitSha);
|
|
1965
|
+
const file = (fileRes === null || fileRes === void 0 ? void 0 : fileRes.valid) === false ? null : (_o = fileRes === null || fileRes === void 0 ? void 0 : fileRes.data) === null || _o === void 0 ? void 0 : _o.file;
|
|
1966
|
+
const lookupError = (fileRes === null || fileRes === void 0 ? void 0 : fileRes.valid) === false ? String((_q = (_p = fileRes === null || fileRes === void 0 ? void 0 : fileRes.message) !== null && _p !== void 0 ? _p : fileRes === null || fileRes === void 0 ? void 0 : fileRes.error) !== null && _q !== void 0 ? _q : '') : '';
|
|
1967
|
+
if (typeof (file === null || file === void 0 ? void 0 : file.content) === 'string') {
|
|
1968
|
+
backtestYaml = file.content;
|
|
1969
|
+
}
|
|
1970
|
+
else if (file) {
|
|
1971
|
+
// The preview refuses some present files (build/dist/venv paths, directories, oversized files) with a reason.
|
|
1972
|
+
return fail(400, `Could not read ${backtestPath} at ${shortSha}: ${(_r = file.message) !== null && _r !== void 0 ? _r : 'the file is not readable as text'}. Keep the backtest ` +
|
|
1973
|
+
'YAML a small text file outside build output directories, commit and push it, then call again.', { errorCode: 'BACKTEST_CONFIG_UNREADABLE' });
|
|
1974
|
+
}
|
|
1975
|
+
else if (lookupError !== 'File not found') {
|
|
1976
|
+
return fail((_s = fileRes === null || fileRes === void 0 ? void 0 : fileRes.statusCode) !== null && _s !== void 0 ? _s : 502, `Could not read ${backtestPath} at ${shortSha}: ${lookupError || 'lookup failed'}`, {
|
|
1977
|
+
errorCode: 'BACKTEST_CONFIG_UNREADABLE',
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
else {
|
|
1981
|
+
return fail(404, `${backtestPath} is not in commit ${shortSha}. Only committed, pushed files run: commit and push it, then call ` +
|
|
1982
|
+
'again with the pushed commit_sha.' +
|
|
1983
|
+
(discovered.length > 0 ? ` Committed backtest files at that commit: ${discovered.join(', ')}.` : ''), { errorCode: 'BACKTEST_CONFIG_NOT_FOUND', ...(discovered.length > 0 ? { backtest_config_paths: discovered } : {}) });
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
const configYaml = typeof meta.config_yaml === 'string' ? meta.config_yaml : '';
|
|
1987
|
+
if ((0, backtest_v3_1.backtestMode)(backtestYaml) === 'economic' && (0, backtest_v3_1.declaresBandAllocation)(configYaml)) {
|
|
1988
|
+
return fail(400, `${backtestPath} selects the economic model, and config.yaml at ${shortSha} declares a banded liquidity allocation ` +
|
|
1989
|
+
`(band). The SDK refuses that combination ("${backtest_v3_1.SDK_BAND_ECONOMIC_REFUSAL}"), so the run was not submitted. ` +
|
|
1990
|
+
'Tell the user; backtesting this strategy needs a fixed range allocation instead of a band.', { errorCode: 'V3_BACKTEST_BAND_UNSUPPORTED' });
|
|
1991
|
+
}
|
|
1992
|
+
const submit = await this.client.submitBacktest({
|
|
1993
|
+
github_strategy_link_id: strategyLinkId,
|
|
1994
|
+
commit_sha: commitSha,
|
|
1995
|
+
sdk_version: sdkVersion,
|
|
1996
|
+
sdk_generation: 'v3',
|
|
1997
|
+
backtest_config_path: backtestPath,
|
|
1998
|
+
});
|
|
1999
|
+
if ((submit === null || submit === void 0 ? void 0 : submit.valid) === false || typeof (submit === null || submit === void 0 ? void 0 : submit.id) !== 'string') {
|
|
2000
|
+
return fail((_t = submit === null || submit === void 0 ? void 0 : submit.statusCode) !== null && _t !== void 0 ? _t : 400, (_v = (_u = submit === null || submit === void 0 ? void 0 : submit.error) !== null && _u !== void 0 ? _u : submit === null || submit === void 0 ? void 0 : submit.message) !== null && _v !== void 0 ? _v : 'Backtest submission failed', {
|
|
2001
|
+
...(typeof (submit === null || submit === void 0 ? void 0 : submit.errorCode) === 'string' ? { errorCode: submit.errorCode } : {}),
|
|
2002
|
+
...(typeof (submit === null || submit === void 0 ? void 0 : submit.activeBacktests) === 'number' ? { activeBacktests: submit.activeBacktests } : {}),
|
|
2003
|
+
...(typeof (submit === null || submit === void 0 ? void 0 : submit.maxActiveBacktests) === 'number' ? { maxActiveBacktests: submit.maxActiveBacktests } : {}),
|
|
2004
|
+
...((submit === null || submit === void 0 ? void 0 : submit.errorCode) === 'BACKTEST_CONCURRENCY_LIMIT' ? { retryable: false } : {}),
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
return {
|
|
2008
|
+
backtest_id: submit.id,
|
|
2009
|
+
status: (_w = submit.status) !== null && _w !== void 0 ? _w : 'PENDING',
|
|
2010
|
+
created_at: (_x = submit.created_at) !== null && _x !== void 0 ? _x : null,
|
|
2011
|
+
...context,
|
|
2012
|
+
show_widget: true,
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
1742
2015
|
catch (err) {
|
|
1743
2016
|
return fail(502, `Backtest submission failed: ${(_y = err === null || err === void 0 ? void 0 : err.message) !== null && _y !== void 0 ? _y : String(err)}`);
|
|
1744
2017
|
}
|
|
@@ -1752,7 +2025,7 @@ class PlatformToolHandler {
|
|
|
1752
2025
|
* path returned — equity curves and per-trade detail are read on demand.
|
|
1753
2026
|
*/
|
|
1754
2027
|
async getBacktestResults(args) {
|
|
1755
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v
|
|
2028
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
|
|
1756
2029
|
const backtestId = typeof args.backtest_id === 'string' ? args.backtest_id.trim() : '';
|
|
1757
2030
|
if (!backtestId) {
|
|
1758
2031
|
return {
|
|
@@ -1774,6 +2047,9 @@ class PlatformToolHandler {
|
|
|
1774
2047
|
backtest_id: backtestId,
|
|
1775
2048
|
};
|
|
1776
2049
|
}
|
|
2050
|
+
if ((0, backtest_v3_1.isV3BacktestRun)(run)) {
|
|
2051
|
+
return this.getV3BacktestResults(backtestId, run);
|
|
2052
|
+
}
|
|
1777
2053
|
const summaryVerdict = summarizeRunValidity((_d = run.result_summary) === null || _d === void 0 ? void 0 : _d.run_validity);
|
|
1778
2054
|
const base = {
|
|
1779
2055
|
backtest_id: (_e = run.id) !== null && _e !== void 0 ? _e : backtestId,
|
|
@@ -1844,34 +2120,117 @@ class PlatformToolHandler {
|
|
|
1844
2120
|
'environment/data plane, not strategy logic.'
|
|
1845
2121
|
: 'This run predates decision telemetry (older SDK), so per-tick hold reasons are unavailable — attribute ' +
|
|
1846
2122
|
'zero-trade results by reproducing the decision path rather than assuming a strategy bug.');
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
2123
|
+
const written = await this.writeBacktestResultArtifact(backtestId, doc);
|
|
2124
|
+
if ('refused' in written) {
|
|
2125
|
+
return { error: written.refused, statusCode: 500 };
|
|
2126
|
+
}
|
|
2127
|
+
if ('error' in written) {
|
|
2128
|
+
return { ...inline, result_write_error: written.error, hint: telemetryHint };
|
|
2129
|
+
}
|
|
2130
|
+
const fpath = written.path;
|
|
2131
|
+
return {
|
|
2132
|
+
...inline,
|
|
2133
|
+
result_path: fpath,
|
|
2134
|
+
hint: 'Full result.json written to result_path — Grep/Read it for equity_curve, trades, price_series, and ' +
|
|
2135
|
+
'data_manifest entries (content is not returned inline). ' +
|
|
2136
|
+
telemetryHint,
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Persist a backtest result document to `.logs/` for Grep/Read and return its path. Compact on purpose: the artifact
|
|
2141
|
+
* can be MBs and is a Grep/jq target. The id is a UUID in normal usage, but anything other than letters, digits and
|
|
2142
|
+
* dashes is stripped before it reaches a filename (the same hardening as fetchAgentLogsToFile), and a document above
|
|
2143
|
+
* the fetch cap is not written.
|
|
2144
|
+
*/
|
|
2145
|
+
async writeBacktestResultArtifact(backtestId, doc) {
|
|
2146
|
+
var _a;
|
|
1850
2147
|
const safeId = backtestId.replace(/[^a-zA-Z0-9-]/g, '').slice(0, 8);
|
|
1851
2148
|
const logsDir = path.resolve(process.cwd(), '.logs');
|
|
1852
2149
|
const fpath = path.join(logsDir, `backtest_${safeId}_result_${Date.now()}.json`);
|
|
1853
2150
|
if (!fpath.startsWith(logsDir + path.sep)) {
|
|
1854
|
-
return {
|
|
2151
|
+
return { refused: 'refused to write outside .logs/' };
|
|
1855
2152
|
}
|
|
1856
2153
|
try {
|
|
2154
|
+
const text = JSON.stringify(doc);
|
|
2155
|
+
const bytes = Buffer.byteLength(text, 'utf8');
|
|
2156
|
+
if (bytes > client_1.BACKTEST_RESULT_MAX_BYTES) {
|
|
2157
|
+
return { error: `result.json is ${bytes} bytes, above the ${client_1.BACKTEST_RESULT_MAX_BYTES}-byte cap; it was not written to .logs/` };
|
|
2158
|
+
}
|
|
1857
2159
|
await node_fs_1.promises.mkdir(logsDir, { recursive: true });
|
|
1858
|
-
|
|
1859
|
-
// target, so don't double its size (and the in-memory copy) with pretty-printing.
|
|
1860
|
-
await node_fs_1.promises.writeFile(fpath, JSON.stringify(doc), 'utf8');
|
|
2160
|
+
await node_fs_1.promises.writeFile(fpath, text, 'utf8');
|
|
1861
2161
|
}
|
|
1862
2162
|
catch (err) {
|
|
2163
|
+
return { error: `Could not persist result.json to .logs/: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err)}` };
|
|
2164
|
+
}
|
|
2165
|
+
return { path: fpath };
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* backtests_results for an SDK v3 run. The runner's summary carries run facts (or a failure block with stage, code
|
|
2169
|
+
* and message) instead of v2 metrics, and the result document embeds the SDK report under `report`; the v2 trade,
|
|
2170
|
+
* decision-telemetry and manifest fields do not exist, so they are not reported as empty.
|
|
2171
|
+
*/
|
|
2172
|
+
async getV3BacktestResults(backtestId, run) {
|
|
2173
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
2174
|
+
const failure = (0, backtest_v3_1.v3BacktestFailure)(run);
|
|
2175
|
+
const base = {
|
|
2176
|
+
backtest_id: (_a = run.id) !== null && _a !== void 0 ? _a : backtestId,
|
|
2177
|
+
status: run.status,
|
|
2178
|
+
sdk_generation: 'v3',
|
|
2179
|
+
created_at: (_b = run.created_at) !== null && _b !== void 0 ? _b : null,
|
|
2180
|
+
completed_at: (_c = run.completed_at) !== null && _c !== void 0 ? _c : null,
|
|
2181
|
+
strategy_link_id: (_d = run.github_strategy_link_id) !== null && _d !== void 0 ? _d : null,
|
|
2182
|
+
commit_sha: (_e = run.commit_sha) !== null && _e !== void 0 ? _e : null,
|
|
2183
|
+
sdk_version: (_f = run.sdk_version) !== null && _f !== void 0 ? _f : null,
|
|
2184
|
+
...(0, backtest_v3_1.v3BacktestSource)(run),
|
|
2185
|
+
result_summary: (_g = run.result_summary) !== null && _g !== void 0 ? _g : null,
|
|
2186
|
+
error_message: (_h = run.error_message) !== null && _h !== void 0 ? _h : null,
|
|
2187
|
+
};
|
|
2188
|
+
if (run.status !== 'COMPLETED') {
|
|
2189
|
+
if (run.status === 'FAILED') {
|
|
2190
|
+
return { ...base, failure, hint: (0, backtest_v3_1.v3FailureHint)(failure, typeof run.error_message === 'string' ? run.error_message : null) };
|
|
2191
|
+
}
|
|
2192
|
+
if (run.status === 'CANCELLED') {
|
|
2193
|
+
return { ...base, hint: 'The run was cancelled by the user; no results exist and none will. Do not re-run unless asked.' };
|
|
2194
|
+
}
|
|
1863
2195
|
return {
|
|
1864
|
-
...
|
|
1865
|
-
|
|
1866
|
-
hint:
|
|
2196
|
+
...base,
|
|
2197
|
+
progress: (0, backtest_v3_1.backtestProgress)(run),
|
|
2198
|
+
hint: 'The run has not finished. progress (when present) is the runner snapshot: phase, completed_ticks of total_ticks ' +
|
|
2199
|
+
'and simulation_elapsed_ms. Call again later; do not re-submit.',
|
|
1867
2200
|
};
|
|
1868
2201
|
}
|
|
2202
|
+
let doc;
|
|
2203
|
+
try {
|
|
2204
|
+
doc = await this.client.getBacktestResult(backtestId);
|
|
2205
|
+
}
|
|
2206
|
+
catch (err) {
|
|
2207
|
+
return { ...base, result_fetch_error: `Result fetch failed: ${(_j = err === null || err === void 0 ? void 0 : err.message) !== null && _j !== void 0 ? _j : String(err)}` };
|
|
2208
|
+
}
|
|
2209
|
+
if (!doc || doc.valid === false) {
|
|
2210
|
+
return { ...base, result_fetch_error: (_k = doc === null || doc === void 0 ? void 0 : doc.error) !== null && _k !== void 0 ? _k : 'Backtest result not found' };
|
|
2211
|
+
}
|
|
2212
|
+
const inline = {
|
|
2213
|
+
...base,
|
|
2214
|
+
report_summary: (0, backtest_v3_1.summarizeV3Report)(doc),
|
|
2215
|
+
artifacts: doc.artifacts && typeof doc.artifacts === 'object' ? Object.keys(doc.artifacts) : [],
|
|
2216
|
+
};
|
|
2217
|
+
const reportHint = 'report_summary compacts the SDK report: metrics and net_quote_return are in the quote currency, and absent fields ' +
|
|
2218
|
+
'mean the run did not measure them (for example no price bindings), never zero. incomplete_equity_points counts marks ' +
|
|
2219
|
+
'where a held asset had no valid price.';
|
|
2220
|
+
const written = await this.writeBacktestResultArtifact(backtestId, doc);
|
|
2221
|
+
if ('refused' in written) {
|
|
2222
|
+
return { error: written.refused, statusCode: 500 };
|
|
2223
|
+
}
|
|
2224
|
+
if ('error' in written) {
|
|
2225
|
+
return { ...inline, result_write_error: written.error, hint: reportHint };
|
|
2226
|
+
}
|
|
2227
|
+
const fpath = written.path;
|
|
1869
2228
|
return {
|
|
1870
2229
|
...inline,
|
|
1871
2230
|
result_path: fpath,
|
|
1872
|
-
hint: 'Full result.json written to result_path
|
|
1873
|
-
'
|
|
1874
|
-
|
|
2231
|
+
hint: 'Full result.json written to result_path; the SDK report is under `report` (equity_curve, price_series, positions, ' +
|
|
2232
|
+
'wallets, assumptions). Grep/Read it for detail. ' +
|
|
2233
|
+
reportHint,
|
|
1875
2234
|
};
|
|
1876
2235
|
}
|
|
1877
2236
|
/**
|