@almanak/mcp-server 0.2.5 → 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 +2 -3
- package/dist/testing/__tests__/runner.test.js.map +1 -1
- package/dist/testing/runner.d.ts +2 -2
- 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 +380 -30
- 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\/[^/]+\/(.+)$/;
|
|
@@ -108,9 +110,9 @@ function parseSdkVersion(value) {
|
|
|
108
110
|
phaseNumber: phaseNumber ? Number(phaseNumber) : 0,
|
|
109
111
|
};
|
|
110
112
|
}
|
|
111
|
-
function isSdkVersionBacktestable(version) {
|
|
113
|
+
function isSdkVersionBacktestable(version, minimum = MIN_BACKTEST_SDK_VERSION) {
|
|
112
114
|
const left = parseSdkVersion(version);
|
|
113
|
-
const right = parseSdkVersion(
|
|
115
|
+
const right = parseSdkVersion(minimum);
|
|
114
116
|
if (!left || !right)
|
|
115
117
|
return false;
|
|
116
118
|
for (const key of ['major', 'minor', 'patch']) {
|
|
@@ -121,6 +123,67 @@ function isSdkVersionBacktestable(version) {
|
|
|
121
123
|
return SDK_PHASE_RANK[left.phase] > SDK_PHASE_RANK[right.phase];
|
|
122
124
|
return left.phaseNumber >= right.phaseNumber;
|
|
123
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/.';
|
|
124
187
|
/** One-line human summary of a spec's `content.exit` for the injected teardown-disclosure field. */
|
|
125
188
|
function summarizeSpecExit(exit) {
|
|
126
189
|
if (exit === undefined || exit === null || (typeof exit === 'object' && Object.keys(exit).length === 0)) {
|
|
@@ -1217,7 +1280,10 @@ class PlatformToolHandler {
|
|
|
1217
1280
|
required: ['strategy_link_id'],
|
|
1218
1281
|
},
|
|
1219
1282
|
}, (args) => this.linkWorkspace(args));
|
|
1220
|
-
|
|
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 : {
|
|
1221
1287
|
name: 'backtests_run',
|
|
1222
1288
|
description: 'Run a backtest of a linked strategy against historical market data. Use this after the strategy is in its ' +
|
|
1223
1289
|
'repository (strategies_export_workspace, or git push on a linked chat) and before deploying — backtests run against a ' +
|
|
@@ -1281,7 +1347,8 @@ class PlatformToolHandler {
|
|
|
1281
1347
|
},
|
|
1282
1348
|
required: ['strategy_link_id', 'start_time', 'end_time', 'show_widget'],
|
|
1283
1349
|
},
|
|
1284
|
-
}
|
|
1350
|
+
};
|
|
1351
|
+
this.register('strategies', backtestsRunDefinition, (args) => this.runBacktest(args));
|
|
1285
1352
|
this.register('strategies', {
|
|
1286
1353
|
name: 'backtests_results',
|
|
1287
1354
|
description: "Fetch a backtest run's status and results for diagnosis — including WHY a run traded or held. Returns " +
|
|
@@ -1294,7 +1361,8 @@ class PlatformToolHandler {
|
|
|
1294
1361
|
'returning its absolute path — use Grep/Read on it for the equity curve, trades, and price series (not ' +
|
|
1295
1362
|
'returned inline). Works for any backtest of your linked strategies, including runs started from the ' +
|
|
1296
1363
|
'platform UI and runs from earlier sessions. Use it BEFORE editing strategy code in response to a bad ' +
|
|
1297
|
-
'backtest, to attribute the cause first.'
|
|
1364
|
+
'backtest, to attribute the cause first.' +
|
|
1365
|
+
(this.options.sdkGeneration === 'v3' ? BACKTESTS_RESULTS_V3_NOTE : ''),
|
|
1298
1366
|
inputSchema: {
|
|
1299
1367
|
type: 'object',
|
|
1300
1368
|
properties: {
|
|
@@ -1611,7 +1679,7 @@ class PlatformToolHandler {
|
|
|
1611
1679
|
* turn into a widget (the error card auto-resolves the gate immediately).
|
|
1612
1680
|
*/
|
|
1613
1681
|
async runBacktest(args) {
|
|
1614
|
-
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;
|
|
1615
1683
|
const strategyLinkId = String((_a = args.strategy_link_id) !== null && _a !== void 0 ? _a : '').trim();
|
|
1616
1684
|
const startTime = String((_b = args.start_time) !== null && _b !== void 0 ? _b : '').trim();
|
|
1617
1685
|
// required[] only checks presence — an empty/whitespace id would call
|
|
@@ -1659,6 +1727,28 @@ class PlatformToolHandler {
|
|
|
1659
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');
|
|
1660
1728
|
}
|
|
1661
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
|
+
}
|
|
1662
1752
|
const strategyName = (typeof strategy.display_name === 'string' && strategy.display_name) ||
|
|
1663
1753
|
(typeof strategy.github_repo_name === 'string' && strategy.github_repo_name) ||
|
|
1664
1754
|
null;
|
|
@@ -1684,19 +1774,19 @@ class PlatformToolHandler {
|
|
|
1684
1774
|
const refType = explicitSha ? 'commit' : 'branch';
|
|
1685
1775
|
const refConfigRes = await this.client.getStrategyRefConfig(strategyLinkId, ref, refType);
|
|
1686
1776
|
if ((refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.valid) === false || !(refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.data)) {
|
|
1687
|
-
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.`;
|
|
1688
1778
|
const recoveryGuidance = explicitSha
|
|
1689
1779
|
? ` Omit commit_sha only if the user explicitly approves running the latest commit on ${defaultBranch}; ` +
|
|
1690
1780
|
'do not retry with different code automatically.'
|
|
1691
1781
|
: '';
|
|
1692
|
-
return fail((
|
|
1693
|
-
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',
|
|
1694
1784
|
retryable: (refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.retryable) === true,
|
|
1695
1785
|
...recoveryContext,
|
|
1696
1786
|
});
|
|
1697
1787
|
}
|
|
1698
1788
|
const refConfig = refConfigRes.data;
|
|
1699
|
-
const commitSha = (
|
|
1789
|
+
const commitSha = (_o = refConfig.github_commit_sha) !== null && _o !== void 0 ? _o : '';
|
|
1700
1790
|
if (!FULL_COMMIT_SHA_RE.test(commitSha)) {
|
|
1701
1791
|
return fail(400, `Could not resolve "${ref}" to a full commit SHA — has the strategy been pushed to its repository?`, {
|
|
1702
1792
|
errorCode: 'COMMIT_UNRESOLVED',
|
|
@@ -1707,17 +1797,22 @@ class PlatformToolHandler {
|
|
|
1707
1797
|
// /ref-config — pin it to the resolved commit so it matches the config.
|
|
1708
1798
|
const refMetaRes = await this.client.getStrategyRefMetadata(strategyLinkId, commitSha);
|
|
1709
1799
|
if ((refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.valid) === false || !(refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.data)) {
|
|
1710
|
-
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');
|
|
1711
1801
|
}
|
|
1712
|
-
const sdkVersion = (
|
|
1802
|
+
const sdkVersion = (_s = refMetaRes.data.almanak_sdk_version) !== null && _s !== void 0 ? _s : null;
|
|
1713
1803
|
if (!sdkVersion) {
|
|
1714
1804
|
return fail(400, 'Could not detect the Almanak SDK version from pyproject.toml at the selected commit.');
|
|
1715
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
|
+
}
|
|
1716
1811
|
if (!isSdkVersionBacktestable(sdkVersion)) {
|
|
1717
1812
|
return fail(400, `Backtesting requires Almanak SDK >= ${MIN_BACKTEST_SDK_VERSION}; the repo pins ${sdkVersion}. ` +
|
|
1718
1813
|
'Upgrade the almanak dependency in pyproject.toml, push, and re-run.');
|
|
1719
1814
|
}
|
|
1720
|
-
const strategyConfig = (
|
|
1815
|
+
const strategyConfig = (_v = args.strategy_config) !== null && _v !== void 0 ? _v : refConfig.effective_config_json;
|
|
1721
1816
|
const submit = await this.client.submitBacktest({
|
|
1722
1817
|
github_strategy_link_id: strategyLinkId,
|
|
1723
1818
|
commit_sha: commitSha,
|
|
@@ -1726,7 +1821,7 @@ class PlatformToolHandler {
|
|
|
1726
1821
|
backtest_config: backtestConfig,
|
|
1727
1822
|
});
|
|
1728
1823
|
if ((submit === null || submit === void 0 ? void 0 : submit.valid) === false || typeof (submit === null || submit === void 0 ? void 0 : submit.id) !== 'string') {
|
|
1729
|
-
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', {
|
|
1730
1825
|
commit_sha: commitSha,
|
|
1731
1826
|
sdk_version: sdkVersion,
|
|
1732
1827
|
...(typeof (submit === null || submit === void 0 ? void 0 : submit.errorCode) === 'string' ? { errorCode: submit.errorCode } : {}),
|
|
@@ -1738,8 +1833,8 @@ class PlatformToolHandler {
|
|
|
1738
1833
|
}
|
|
1739
1834
|
return {
|
|
1740
1835
|
backtest_id: submit.id,
|
|
1741
|
-
status: (
|
|
1742
|
-
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,
|
|
1743
1838
|
strategy_link_id: strategyLinkId,
|
|
1744
1839
|
strategy_name: strategyName,
|
|
1745
1840
|
commit_sha: commitSha,
|
|
@@ -1748,6 +1843,175 @@ class PlatformToolHandler {
|
|
|
1748
1843
|
show_widget: true,
|
|
1749
1844
|
};
|
|
1750
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
|
+
}
|
|
1751
2015
|
catch (err) {
|
|
1752
2016
|
return fail(502, `Backtest submission failed: ${(_y = err === null || err === void 0 ? void 0 : err.message) !== null && _y !== void 0 ? _y : String(err)}`);
|
|
1753
2017
|
}
|
|
@@ -1761,7 +2025,7 @@ class PlatformToolHandler {
|
|
|
1761
2025
|
* path returned — equity curves and per-trade detail are read on demand.
|
|
1762
2026
|
*/
|
|
1763
2027
|
async getBacktestResults(args) {
|
|
1764
|
-
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;
|
|
1765
2029
|
const backtestId = typeof args.backtest_id === 'string' ? args.backtest_id.trim() : '';
|
|
1766
2030
|
if (!backtestId) {
|
|
1767
2031
|
return {
|
|
@@ -1783,6 +2047,9 @@ class PlatformToolHandler {
|
|
|
1783
2047
|
backtest_id: backtestId,
|
|
1784
2048
|
};
|
|
1785
2049
|
}
|
|
2050
|
+
if ((0, backtest_v3_1.isV3BacktestRun)(run)) {
|
|
2051
|
+
return this.getV3BacktestResults(backtestId, run);
|
|
2052
|
+
}
|
|
1786
2053
|
const summaryVerdict = summarizeRunValidity((_d = run.result_summary) === null || _d === void 0 ? void 0 : _d.run_validity);
|
|
1787
2054
|
const base = {
|
|
1788
2055
|
backtest_id: (_e = run.id) !== null && _e !== void 0 ? _e : backtestId,
|
|
@@ -1853,34 +2120,117 @@ class PlatformToolHandler {
|
|
|
1853
2120
|
'environment/data plane, not strategy logic.'
|
|
1854
2121
|
: 'This run predates decision telemetry (older SDK), so per-tick hold reasons are unavailable — attribute ' +
|
|
1855
2122
|
'zero-trade results by reproducing the decision path rather than assuming a strategy bug.');
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
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;
|
|
1859
2147
|
const safeId = backtestId.replace(/[^a-zA-Z0-9-]/g, '').slice(0, 8);
|
|
1860
2148
|
const logsDir = path.resolve(process.cwd(), '.logs');
|
|
1861
2149
|
const fpath = path.join(logsDir, `backtest_${safeId}_result_${Date.now()}.json`);
|
|
1862
2150
|
if (!fpath.startsWith(logsDir + path.sep)) {
|
|
1863
|
-
return {
|
|
2151
|
+
return { refused: 'refused to write outside .logs/' };
|
|
1864
2152
|
}
|
|
1865
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
|
+
}
|
|
1866
2159
|
await node_fs_1.promises.mkdir(logsDir, { recursive: true });
|
|
1867
|
-
|
|
1868
|
-
// target, so don't double its size (and the in-memory copy) with pretty-printing.
|
|
1869
|
-
await node_fs_1.promises.writeFile(fpath, JSON.stringify(doc), 'utf8');
|
|
2160
|
+
await node_fs_1.promises.writeFile(fpath, text, 'utf8');
|
|
1870
2161
|
}
|
|
1871
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
|
+
}
|
|
1872
2195
|
return {
|
|
1873
|
-
...
|
|
1874
|
-
|
|
1875
|
-
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.',
|
|
1876
2200
|
};
|
|
1877
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;
|
|
1878
2228
|
return {
|
|
1879
2229
|
...inline,
|
|
1880
2230
|
result_path: fpath,
|
|
1881
|
-
hint: 'Full result.json written to result_path
|
|
1882
|
-
'
|
|
1883
|
-
|
|
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,
|
|
1884
2234
|
};
|
|
1885
2235
|
}
|
|
1886
2236
|
/**
|