@almanak/mcp-server 0.2.5 → 0.2.7
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 +68 -9
- package/dist/testing/__tests__/runner.test.js.map +1 -1
- package/dist/testing/runner.d.ts +18 -3
- package/dist/testing/runner.js +49 -2
- package/dist/testing/runner.js.map +1 -1
- package/dist/tools.d.ts +19 -0
- package/dist/tools.js +385 -31
- 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: {
|
|
@@ -1539,7 +1607,11 @@ class PlatformToolHandler {
|
|
|
1539
1607
|
'steps[3].summary. SDK v3 workspaces (config.yaml) execute (1) almanak check --json ' +
|
|
1540
1608
|
'(its document is returned on steps[0].summary) and (2) pytest tests/; the on-chain ' +
|
|
1541
1609
|
'steps are skipped because a v3 fork run needs provider credentials this container ' +
|
|
1542
|
-
'does not hold.'
|
|
1610
|
+
'does not hold. The v3 check step passes only when the SDK reports ready: true. An ' +
|
|
1611
|
+
'offline check here reports valid: true with ready: false, so the step is not passed, ' +
|
|
1612
|
+
'all_passed is false and steps[0].not_ready_reason lists the not_checked stages: ' +
|
|
1613
|
+
'production readiness, including the production gas policy (strategy.gas), is not ' +
|
|
1614
|
+
'verified in this container.',
|
|
1543
1615
|
inputSchema: {
|
|
1544
1616
|
type: 'object',
|
|
1545
1617
|
properties: {
|
|
@@ -1611,7 +1683,7 @@ class PlatformToolHandler {
|
|
|
1611
1683
|
* turn into a widget (the error card auto-resolves the gate immediately).
|
|
1612
1684
|
*/
|
|
1613
1685
|
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;
|
|
1686
|
+
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
1687
|
const strategyLinkId = String((_a = args.strategy_link_id) !== null && _a !== void 0 ? _a : '').trim();
|
|
1616
1688
|
const startTime = String((_b = args.start_time) !== null && _b !== void 0 ? _b : '').trim();
|
|
1617
1689
|
// required[] only checks presence — an empty/whitespace id would call
|
|
@@ -1659,6 +1731,28 @@ class PlatformToolHandler {
|
|
|
1659
1731
|
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
1732
|
}
|
|
1661
1733
|
const strategy = strategyRes.data;
|
|
1734
|
+
// The stored framework_version is authoritative for which runner family serves the strategy.
|
|
1735
|
+
const sessionIsV3 = this.options.sdkGeneration === 'v3';
|
|
1736
|
+
if (strategy.framework_version === 'V3') {
|
|
1737
|
+
if (!sessionIsV3) {
|
|
1738
|
+
// The v2 schema requires the window inputs a v3 run refuses; retrying with other inputs cannot succeed here.
|
|
1739
|
+
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 ' +
|
|
1740
|
+
'the SDK v2 window inputs that SDK v3 backtests refuse. Do not retry with other inputs: start the run from the ' +
|
|
1741
|
+
"strategy's Backtest page on the platform, or from an SDK v3 chat session attached to this strategy.", { errorCode: 'SDK_GENERATION_MISMATCH' });
|
|
1742
|
+
}
|
|
1743
|
+
return await this.runV3Backtest(args, strategyLinkId, strategy);
|
|
1744
|
+
}
|
|
1745
|
+
if (sessionIsV3) {
|
|
1746
|
+
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 ` +
|
|
1747
|
+
'runs the SDK v3 lane. The stored generation decides which backtest runs, and the two disagree, so nothing was ' +
|
|
1748
|
+
'submitted. Do not add SDK v2 window inputs; tell the user the strategy link is not recognised as SDK v3 ' +
|
|
1749
|
+
'(its repository must resolve almanak 3.x and carry strategy.py and config.yaml at the root).', { errorCode: 'SDK_GENERATION_MISMATCH' });
|
|
1750
|
+
}
|
|
1751
|
+
const v3Inputs = backtest_v3_1.V3_ONLY_BACKTEST_ARGS.filter((key) => args[key] !== undefined);
|
|
1752
|
+
if (v3Inputs.length > 0) {
|
|
1753
|
+
return fail(400, `${v3Inputs.join(', ')} ${v3Inputs.length > 1 ? 'are SDK v3 inputs' : 'is an SDK v3 input'}, and this strategy is ` +
|
|
1754
|
+
'stored as SDK v2: its backtest takes start_time and end_time (YYYY-MM-DD).', { errorCode: 'SDK_GENERATION_MISMATCH' });
|
|
1755
|
+
}
|
|
1662
1756
|
const strategyName = (typeof strategy.display_name === 'string' && strategy.display_name) ||
|
|
1663
1757
|
(typeof strategy.github_repo_name === 'string' && strategy.github_repo_name) ||
|
|
1664
1758
|
null;
|
|
@@ -1684,19 +1778,19 @@ class PlatformToolHandler {
|
|
|
1684
1778
|
const refType = explicitSha ? 'commit' : 'branch';
|
|
1685
1779
|
const refConfigRes = await this.client.getStrategyRefConfig(strategyLinkId, ref, refType);
|
|
1686
1780
|
if ((refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.valid) === false || !(refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.data)) {
|
|
1687
|
-
const baseError = (
|
|
1781
|
+
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
1782
|
const recoveryGuidance = explicitSha
|
|
1689
1783
|
? ` Omit commit_sha only if the user explicitly approves running the latest commit on ${defaultBranch}; ` +
|
|
1690
1784
|
'do not retry with different code automatically.'
|
|
1691
1785
|
: '';
|
|
1692
|
-
return fail((
|
|
1693
|
-
errorCode: (
|
|
1786
|
+
return fail((_l = refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.statusCode) !== null && _l !== void 0 ? _l : 400, `${baseError}${recoveryGuidance}`, {
|
|
1787
|
+
errorCode: (_m = refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.errorCode) !== null && _m !== void 0 ? _m : 'INTERNAL_ERROR',
|
|
1694
1788
|
retryable: (refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.retryable) === true,
|
|
1695
1789
|
...recoveryContext,
|
|
1696
1790
|
});
|
|
1697
1791
|
}
|
|
1698
1792
|
const refConfig = refConfigRes.data;
|
|
1699
|
-
const commitSha = (
|
|
1793
|
+
const commitSha = (_o = refConfig.github_commit_sha) !== null && _o !== void 0 ? _o : '';
|
|
1700
1794
|
if (!FULL_COMMIT_SHA_RE.test(commitSha)) {
|
|
1701
1795
|
return fail(400, `Could not resolve "${ref}" to a full commit SHA — has the strategy been pushed to its repository?`, {
|
|
1702
1796
|
errorCode: 'COMMIT_UNRESOLVED',
|
|
@@ -1707,17 +1801,22 @@ class PlatformToolHandler {
|
|
|
1707
1801
|
// /ref-config — pin it to the resolved commit so it matches the config.
|
|
1708
1802
|
const refMetaRes = await this.client.getStrategyRefMetadata(strategyLinkId, commitSha);
|
|
1709
1803
|
if ((refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.valid) === false || !(refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.data)) {
|
|
1710
|
-
return fail((
|
|
1804
|
+
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
1805
|
}
|
|
1712
|
-
const sdkVersion = (
|
|
1806
|
+
const sdkVersion = (_s = refMetaRes.data.almanak_sdk_version) !== null && _s !== void 0 ? _s : null;
|
|
1713
1807
|
if (!sdkVersion) {
|
|
1714
1808
|
return fail(400, 'Could not detect the Almanak SDK version from pyproject.toml at the selected commit.');
|
|
1715
1809
|
}
|
|
1810
|
+
if (((_u = (_t = parseSdkVersion(sdkVersion)) === null || _t === void 0 ? void 0 : _t.major) !== null && _u !== void 0 ? _u : 0) >= 3) {
|
|
1811
|
+
// The backend refuses a v3 SDK against a V2 record; say why instead of submitting the v2 shape.
|
|
1812
|
+
return fail(400, `Commit ${commitSha.slice(0, 7)} resolves almanak ${sdkVersion} (SDK v3), but the strategy link is stored as SDK v2. ` +
|
|
1813
|
+
'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 });
|
|
1814
|
+
}
|
|
1716
1815
|
if (!isSdkVersionBacktestable(sdkVersion)) {
|
|
1717
1816
|
return fail(400, `Backtesting requires Almanak SDK >= ${MIN_BACKTEST_SDK_VERSION}; the repo pins ${sdkVersion}. ` +
|
|
1718
1817
|
'Upgrade the almanak dependency in pyproject.toml, push, and re-run.');
|
|
1719
1818
|
}
|
|
1720
|
-
const strategyConfig = (
|
|
1819
|
+
const strategyConfig = (_v = args.strategy_config) !== null && _v !== void 0 ? _v : refConfig.effective_config_json;
|
|
1721
1820
|
const submit = await this.client.submitBacktest({
|
|
1722
1821
|
github_strategy_link_id: strategyLinkId,
|
|
1723
1822
|
commit_sha: commitSha,
|
|
@@ -1726,7 +1825,7 @@ class PlatformToolHandler {
|
|
|
1726
1825
|
backtest_config: backtestConfig,
|
|
1727
1826
|
});
|
|
1728
1827
|
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((
|
|
1828
|
+
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
1829
|
commit_sha: commitSha,
|
|
1731
1830
|
sdk_version: sdkVersion,
|
|
1732
1831
|
...(typeof (submit === null || submit === void 0 ? void 0 : submit.errorCode) === 'string' ? { errorCode: submit.errorCode } : {}),
|
|
@@ -1738,8 +1837,8 @@ class PlatformToolHandler {
|
|
|
1738
1837
|
}
|
|
1739
1838
|
return {
|
|
1740
1839
|
backtest_id: submit.id,
|
|
1741
|
-
status: (
|
|
1742
|
-
created_at: (
|
|
1840
|
+
status: (_z = submit.status) !== null && _z !== void 0 ? _z : 'PENDING',
|
|
1841
|
+
created_at: (_0 = submit.created_at) !== null && _0 !== void 0 ? _0 : null,
|
|
1743
1842
|
strategy_link_id: strategyLinkId,
|
|
1744
1843
|
strategy_name: strategyName,
|
|
1745
1844
|
commit_sha: commitSha,
|
|
@@ -1748,6 +1847,175 @@ class PlatformToolHandler {
|
|
|
1748
1847
|
show_widget: true,
|
|
1749
1848
|
};
|
|
1750
1849
|
}
|
|
1850
|
+
catch (err) {
|
|
1851
|
+
return fail(502, `Backtest submission failed: ${(_1 = err === null || err === void 0 ? void 0 : err.message) !== null && _1 !== void 0 ? _1 : String(err)}`);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Submit an SDK v3 run: resolve the pushed commit, pick the committed backtest YAML the same way the strategy's
|
|
1856
|
+
* Backtest page does, and send `backtest_config_path` (never the v2 `backtest_config`). Like runBacktest it never
|
|
1857
|
+
* throws and every path returns a widget-renderable shape.
|
|
1858
|
+
*/
|
|
1859
|
+
async runV3Backtest(args, strategyLinkId, strategy) {
|
|
1860
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
|
|
1861
|
+
const strategyName = (typeof strategy.display_name === 'string' && strategy.display_name) ||
|
|
1862
|
+
(typeof strategy.github_repo_name === 'string' && strategy.github_repo_name) ||
|
|
1863
|
+
null;
|
|
1864
|
+
const repoOwner = typeof strategy.github_repo_owner === 'string' ? strategy.github_repo_owner : '';
|
|
1865
|
+
const repoName = typeof strategy.github_repo_name === 'string' ? strategy.github_repo_name : '';
|
|
1866
|
+
const linkedRepo = repoOwner && repoName ? `${repoOwner}/${repoName}` : null;
|
|
1867
|
+
const defaultBranch = (typeof strategy.github_default_branch === 'string' && strategy.github_default_branch.trim()) || 'main';
|
|
1868
|
+
const context = { strategy_link_id: strategyLinkId, strategy_name: strategyName, sdk_generation: 'v3' };
|
|
1869
|
+
const fail = (statusCode, error, extra) => ({ statusCode, error, ...context, ...extra });
|
|
1870
|
+
const v2Inputs = backtest_v3_1.V2_ONLY_BACKTEST_ARGS.filter((key) => args[key] !== undefined);
|
|
1871
|
+
if (v2Inputs.length > 0) {
|
|
1872
|
+
return fail(400, `${v2Inputs.join(', ')} ${v2Inputs.length > 1 ? 'are SDK v2 inputs' : 'is an SDK v2 input'}. An SDK v3 backtest takes its ` +
|
|
1873
|
+
'window, inventory, cadence and costs from the committed backtest YAML: set them in that file, commit and push it, ' +
|
|
1874
|
+
'and call again with backtest_config_path and without the v2 inputs.', { errorCode: 'V3_BACKTEST_INPUT_INVALID' });
|
|
1875
|
+
}
|
|
1876
|
+
let requestedPath = null;
|
|
1877
|
+
if (args.backtest_config_path !== undefined) {
|
|
1878
|
+
const normalized = (0, backtest_v3_1.normalizeBacktestConfigPath)(args.backtest_config_path);
|
|
1879
|
+
if ('error' in normalized) {
|
|
1880
|
+
return fail(400, normalized.error, { errorCode: 'V3_BACKTEST_INPUT_INVALID' });
|
|
1881
|
+
}
|
|
1882
|
+
requestedPath = normalized.path;
|
|
1883
|
+
context.backtest_config_path = requestedPath;
|
|
1884
|
+
}
|
|
1885
|
+
const explicitSha = typeof args.commit_sha === 'string' ? args.commit_sha.trim() : '';
|
|
1886
|
+
const branch = typeof args.branch === 'string' ? args.branch.trim() : '';
|
|
1887
|
+
if (explicitSha && branch) {
|
|
1888
|
+
return fail(400, 'Pass commit_sha or branch, not both: commit_sha already names the exact code to run.', {
|
|
1889
|
+
errorCode: 'V3_BACKTEST_INPUT_INVALID',
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
if (explicitSha && !COMMIT_SHA_PREFIX_RE.test(explicitSha)) {
|
|
1893
|
+
return fail(400, `commit_sha must contain 7 to 40 hexadecimal characters, got "${explicitSha}".`, {
|
|
1894
|
+
errorCode: 'INVALID_COMMIT_FORMAT',
|
|
1895
|
+
requestedCommit: explicitSha,
|
|
1896
|
+
linkedRepo,
|
|
1897
|
+
defaultBranch,
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
if (args.branch !== undefined && !(0, backtest_v3_1.isValidBranchName)(branch)) {
|
|
1901
|
+
return fail(400, `branch must be a git branch name, got "${branch}".`, { errorCode: 'V3_BACKTEST_INPUT_INVALID' });
|
|
1902
|
+
}
|
|
1903
|
+
const ref = explicitSha ? explicitSha.toLowerCase() : branch || defaultBranch;
|
|
1904
|
+
const refType = explicitSha ? 'commit' : 'branch';
|
|
1905
|
+
const recoveryContext = { requestedCommit: explicitSha || null, linkedRepo, defaultBranch };
|
|
1906
|
+
try {
|
|
1907
|
+
const refConfigRes = await this.client.getStrategyRefConfig(strategyLinkId, ref, refType);
|
|
1908
|
+
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 : '');
|
|
1909
|
+
if (!FULL_COMMIT_SHA_RE.test(commitSha)) {
|
|
1910
|
+
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.`;
|
|
1911
|
+
// Push advice fits only a ref the repository does not have; access and availability errors keep their own text.
|
|
1912
|
+
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';
|
|
1913
|
+
const guidance = errorCode !== 'COMMIT_UNRESOLVED'
|
|
1914
|
+
? ''
|
|
1915
|
+
: explicitSha
|
|
1916
|
+
? ` Only pushed commits can be backtested: make sure the commit is pushed to ${linkedRepo !== null && linkedRepo !== void 0 ? linkedRepo : 'the linked repository'} ` +
|
|
1917
|
+
'(git push, then git rev-parse HEAD) and call again with that SHA. Do not switch to a different commit without the user.'
|
|
1918
|
+
: ` Push the branch to ${linkedRepo !== null && linkedRepo !== void 0 ? linkedRepo : 'the linked repository'} first.`;
|
|
1919
|
+
return fail((_f = refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.statusCode) !== null && _f !== void 0 ? _f : 400, `${baseError}${guidance}`, {
|
|
1920
|
+
errorCode,
|
|
1921
|
+
retryable: (refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.retryable) === true,
|
|
1922
|
+
...recoveryContext,
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
context.commit_sha = commitSha;
|
|
1926
|
+
const refMetaRes = await this.client.getStrategyRefMetadata(strategyLinkId, commitSha);
|
|
1927
|
+
if ((refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.valid) === false || !(refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.data)) {
|
|
1928
|
+
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');
|
|
1929
|
+
}
|
|
1930
|
+
const meta = refMetaRes.data;
|
|
1931
|
+
const sdkVersion = typeof meta.almanak_sdk_version === 'string' ? meta.almanak_sdk_version : '';
|
|
1932
|
+
if (!sdkVersion) {
|
|
1933
|
+
return fail(400, 'Could not detect the almanak SDK version (uv.lock or pyproject.toml) at the selected commit.', {
|
|
1934
|
+
errorCode: 'V3_BACKTEST_INPUT_INVALID',
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
context.sdk_version = sdkVersion;
|
|
1938
|
+
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)) {
|
|
1939
|
+
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 ` +
|
|
1940
|
+
`${sdkVersion}. Update the almanak pin (uv.lock / pyproject.toml), commit, push and re-run.`, { errorCode: 'V3_BACKTEST_INPUT_INVALID' });
|
|
1941
|
+
}
|
|
1942
|
+
const committed = {};
|
|
1943
|
+
const configs = meta.backtest_configs && typeof meta.backtest_configs === 'object' ? meta.backtest_configs : {};
|
|
1944
|
+
for (const [name, text] of Object.entries(configs)) {
|
|
1945
|
+
if (typeof text === 'string')
|
|
1946
|
+
committed[name] = text;
|
|
1947
|
+
}
|
|
1948
|
+
const discovered = Array.isArray(meta.backtest_config_paths)
|
|
1949
|
+
? meta.backtest_config_paths.filter((name) => typeof name === 'string')
|
|
1950
|
+
: Object.keys(committed);
|
|
1951
|
+
const shortSha = commitSha.slice(0, 7);
|
|
1952
|
+
let backtestPath = requestedPath;
|
|
1953
|
+
if (!backtestPath) {
|
|
1954
|
+
if (discovered.length === 1) {
|
|
1955
|
+
backtestPath = discovered[0];
|
|
1956
|
+
}
|
|
1957
|
+
else if (discovered.length === 0) {
|
|
1958
|
+
return fail(404, `Commit ${shortSha} has no backtest.yaml or backtest-economic.yaml at the repository root. Write the backtest YAML ` +
|
|
1959
|
+
'(schema: almanak support --schema backtest), commit and push it, then call again with backtest_config_path.', { errorCode: 'BACKTEST_CONFIG_NOT_FOUND' });
|
|
1960
|
+
}
|
|
1961
|
+
else {
|
|
1962
|
+
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 });
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
context.backtest_config_path = backtestPath;
|
|
1966
|
+
let backtestYaml = (_m = committed[backtestPath]) !== null && _m !== void 0 ? _m : null;
|
|
1967
|
+
if (backtestYaml === null) {
|
|
1968
|
+
const fileRes = await this.client.getStrategyFileContent(strategyLinkId, backtestPath, commitSha);
|
|
1969
|
+
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;
|
|
1970
|
+
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 : '') : '';
|
|
1971
|
+
if (typeof (file === null || file === void 0 ? void 0 : file.content) === 'string') {
|
|
1972
|
+
backtestYaml = file.content;
|
|
1973
|
+
}
|
|
1974
|
+
else if (file) {
|
|
1975
|
+
// The preview refuses some present files (build/dist/venv paths, directories, oversized files) with a reason.
|
|
1976
|
+
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 ` +
|
|
1977
|
+
'YAML a small text file outside build output directories, commit and push it, then call again.', { errorCode: 'BACKTEST_CONFIG_UNREADABLE' });
|
|
1978
|
+
}
|
|
1979
|
+
else if (lookupError !== 'File not found') {
|
|
1980
|
+
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'}`, {
|
|
1981
|
+
errorCode: 'BACKTEST_CONFIG_UNREADABLE',
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
else {
|
|
1985
|
+
return fail(404, `${backtestPath} is not in commit ${shortSha}. Only committed, pushed files run: commit and push it, then call ` +
|
|
1986
|
+
'again with the pushed commit_sha.' +
|
|
1987
|
+
(discovered.length > 0 ? ` Committed backtest files at that commit: ${discovered.join(', ')}.` : ''), { errorCode: 'BACKTEST_CONFIG_NOT_FOUND', ...(discovered.length > 0 ? { backtest_config_paths: discovered } : {}) });
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
const configYaml = typeof meta.config_yaml === 'string' ? meta.config_yaml : '';
|
|
1991
|
+
if ((0, backtest_v3_1.backtestMode)(backtestYaml) === 'economic' && (0, backtest_v3_1.declaresBandAllocation)(configYaml)) {
|
|
1992
|
+
return fail(400, `${backtestPath} selects the economic model, and config.yaml at ${shortSha} declares a banded liquidity allocation ` +
|
|
1993
|
+
`(band). The SDK refuses that combination ("${backtest_v3_1.SDK_BAND_ECONOMIC_REFUSAL}"), so the run was not submitted. ` +
|
|
1994
|
+
'Tell the user; backtesting this strategy needs a fixed range allocation instead of a band.', { errorCode: 'V3_BACKTEST_BAND_UNSUPPORTED' });
|
|
1995
|
+
}
|
|
1996
|
+
const submit = await this.client.submitBacktest({
|
|
1997
|
+
github_strategy_link_id: strategyLinkId,
|
|
1998
|
+
commit_sha: commitSha,
|
|
1999
|
+
sdk_version: sdkVersion,
|
|
2000
|
+
sdk_generation: 'v3',
|
|
2001
|
+
backtest_config_path: backtestPath,
|
|
2002
|
+
});
|
|
2003
|
+
if ((submit === null || submit === void 0 ? void 0 : submit.valid) === false || typeof (submit === null || submit === void 0 ? void 0 : submit.id) !== 'string') {
|
|
2004
|
+
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', {
|
|
2005
|
+
...(typeof (submit === null || submit === void 0 ? void 0 : submit.errorCode) === 'string' ? { errorCode: submit.errorCode } : {}),
|
|
2006
|
+
...(typeof (submit === null || submit === void 0 ? void 0 : submit.activeBacktests) === 'number' ? { activeBacktests: submit.activeBacktests } : {}),
|
|
2007
|
+
...(typeof (submit === null || submit === void 0 ? void 0 : submit.maxActiveBacktests) === 'number' ? { maxActiveBacktests: submit.maxActiveBacktests } : {}),
|
|
2008
|
+
...((submit === null || submit === void 0 ? void 0 : submit.errorCode) === 'BACKTEST_CONCURRENCY_LIMIT' ? { retryable: false } : {}),
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
return {
|
|
2012
|
+
backtest_id: submit.id,
|
|
2013
|
+
status: (_w = submit.status) !== null && _w !== void 0 ? _w : 'PENDING',
|
|
2014
|
+
created_at: (_x = submit.created_at) !== null && _x !== void 0 ? _x : null,
|
|
2015
|
+
...context,
|
|
2016
|
+
show_widget: true,
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
1751
2019
|
catch (err) {
|
|
1752
2020
|
return fail(502, `Backtest submission failed: ${(_y = err === null || err === void 0 ? void 0 : err.message) !== null && _y !== void 0 ? _y : String(err)}`);
|
|
1753
2021
|
}
|
|
@@ -1761,7 +2029,7 @@ class PlatformToolHandler {
|
|
|
1761
2029
|
* path returned — equity curves and per-trade detail are read on demand.
|
|
1762
2030
|
*/
|
|
1763
2031
|
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
|
|
2032
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
|
|
1765
2033
|
const backtestId = typeof args.backtest_id === 'string' ? args.backtest_id.trim() : '';
|
|
1766
2034
|
if (!backtestId) {
|
|
1767
2035
|
return {
|
|
@@ -1783,6 +2051,9 @@ class PlatformToolHandler {
|
|
|
1783
2051
|
backtest_id: backtestId,
|
|
1784
2052
|
};
|
|
1785
2053
|
}
|
|
2054
|
+
if ((0, backtest_v3_1.isV3BacktestRun)(run)) {
|
|
2055
|
+
return this.getV3BacktestResults(backtestId, run);
|
|
2056
|
+
}
|
|
1786
2057
|
const summaryVerdict = summarizeRunValidity((_d = run.result_summary) === null || _d === void 0 ? void 0 : _d.run_validity);
|
|
1787
2058
|
const base = {
|
|
1788
2059
|
backtest_id: (_e = run.id) !== null && _e !== void 0 ? _e : backtestId,
|
|
@@ -1853,34 +2124,117 @@ class PlatformToolHandler {
|
|
|
1853
2124
|
'environment/data plane, not strategy logic.'
|
|
1854
2125
|
: 'This run predates decision telemetry (older SDK), so per-tick hold reasons are unavailable — attribute ' +
|
|
1855
2126
|
'zero-trade results by reproducing the decision path rather than assuming a strategy bug.');
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2127
|
+
const written = await this.writeBacktestResultArtifact(backtestId, doc);
|
|
2128
|
+
if ('refused' in written) {
|
|
2129
|
+
return { error: written.refused, statusCode: 500 };
|
|
2130
|
+
}
|
|
2131
|
+
if ('error' in written) {
|
|
2132
|
+
return { ...inline, result_write_error: written.error, hint: telemetryHint };
|
|
2133
|
+
}
|
|
2134
|
+
const fpath = written.path;
|
|
2135
|
+
return {
|
|
2136
|
+
...inline,
|
|
2137
|
+
result_path: fpath,
|
|
2138
|
+
hint: 'Full result.json written to result_path — Grep/Read it for equity_curve, trades, price_series, and ' +
|
|
2139
|
+
'data_manifest entries (content is not returned inline). ' +
|
|
2140
|
+
telemetryHint,
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Persist a backtest result document to `.logs/` for Grep/Read and return its path. Compact on purpose: the artifact
|
|
2145
|
+
* can be MBs and is a Grep/jq target. The id is a UUID in normal usage, but anything other than letters, digits and
|
|
2146
|
+
* dashes is stripped before it reaches a filename (the same hardening as fetchAgentLogsToFile), and a document above
|
|
2147
|
+
* the fetch cap is not written.
|
|
2148
|
+
*/
|
|
2149
|
+
async writeBacktestResultArtifact(backtestId, doc) {
|
|
2150
|
+
var _a;
|
|
1859
2151
|
const safeId = backtestId.replace(/[^a-zA-Z0-9-]/g, '').slice(0, 8);
|
|
1860
2152
|
const logsDir = path.resolve(process.cwd(), '.logs');
|
|
1861
2153
|
const fpath = path.join(logsDir, `backtest_${safeId}_result_${Date.now()}.json`);
|
|
1862
2154
|
if (!fpath.startsWith(logsDir + path.sep)) {
|
|
1863
|
-
return {
|
|
2155
|
+
return { refused: 'refused to write outside .logs/' };
|
|
1864
2156
|
}
|
|
1865
2157
|
try {
|
|
2158
|
+
const text = JSON.stringify(doc);
|
|
2159
|
+
const bytes = Buffer.byteLength(text, 'utf8');
|
|
2160
|
+
if (bytes > client_1.BACKTEST_RESULT_MAX_BYTES) {
|
|
2161
|
+
return { error: `result.json is ${bytes} bytes, above the ${client_1.BACKTEST_RESULT_MAX_BYTES}-byte cap; it was not written to .logs/` };
|
|
2162
|
+
}
|
|
1866
2163
|
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');
|
|
2164
|
+
await node_fs_1.promises.writeFile(fpath, text, 'utf8');
|
|
1870
2165
|
}
|
|
1871
2166
|
catch (err) {
|
|
2167
|
+
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)}` };
|
|
2168
|
+
}
|
|
2169
|
+
return { path: fpath };
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* backtests_results for an SDK v3 run. The runner's summary carries run facts (or a failure block with stage, code
|
|
2173
|
+
* and message) instead of v2 metrics, and the result document embeds the SDK report under `report`; the v2 trade,
|
|
2174
|
+
* decision-telemetry and manifest fields do not exist, so they are not reported as empty.
|
|
2175
|
+
*/
|
|
2176
|
+
async getV3BacktestResults(backtestId, run) {
|
|
2177
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
2178
|
+
const failure = (0, backtest_v3_1.v3BacktestFailure)(run);
|
|
2179
|
+
const base = {
|
|
2180
|
+
backtest_id: (_a = run.id) !== null && _a !== void 0 ? _a : backtestId,
|
|
2181
|
+
status: run.status,
|
|
2182
|
+
sdk_generation: 'v3',
|
|
2183
|
+
created_at: (_b = run.created_at) !== null && _b !== void 0 ? _b : null,
|
|
2184
|
+
completed_at: (_c = run.completed_at) !== null && _c !== void 0 ? _c : null,
|
|
2185
|
+
strategy_link_id: (_d = run.github_strategy_link_id) !== null && _d !== void 0 ? _d : null,
|
|
2186
|
+
commit_sha: (_e = run.commit_sha) !== null && _e !== void 0 ? _e : null,
|
|
2187
|
+
sdk_version: (_f = run.sdk_version) !== null && _f !== void 0 ? _f : null,
|
|
2188
|
+
...(0, backtest_v3_1.v3BacktestSource)(run),
|
|
2189
|
+
result_summary: (_g = run.result_summary) !== null && _g !== void 0 ? _g : null,
|
|
2190
|
+
error_message: (_h = run.error_message) !== null && _h !== void 0 ? _h : null,
|
|
2191
|
+
};
|
|
2192
|
+
if (run.status !== 'COMPLETED') {
|
|
2193
|
+
if (run.status === 'FAILED') {
|
|
2194
|
+
return { ...base, failure, hint: (0, backtest_v3_1.v3FailureHint)(failure, typeof run.error_message === 'string' ? run.error_message : null) };
|
|
2195
|
+
}
|
|
2196
|
+
if (run.status === 'CANCELLED') {
|
|
2197
|
+
return { ...base, hint: 'The run was cancelled by the user; no results exist and none will. Do not re-run unless asked.' };
|
|
2198
|
+
}
|
|
1872
2199
|
return {
|
|
1873
|
-
...
|
|
1874
|
-
|
|
1875
|
-
hint:
|
|
2200
|
+
...base,
|
|
2201
|
+
progress: (0, backtest_v3_1.backtestProgress)(run),
|
|
2202
|
+
hint: 'The run has not finished. progress (when present) is the runner snapshot: phase, completed_ticks of total_ticks ' +
|
|
2203
|
+
'and simulation_elapsed_ms. Call again later; do not re-submit.',
|
|
1876
2204
|
};
|
|
1877
2205
|
}
|
|
2206
|
+
let doc;
|
|
2207
|
+
try {
|
|
2208
|
+
doc = await this.client.getBacktestResult(backtestId);
|
|
2209
|
+
}
|
|
2210
|
+
catch (err) {
|
|
2211
|
+
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)}` };
|
|
2212
|
+
}
|
|
2213
|
+
if (!doc || doc.valid === false) {
|
|
2214
|
+
return { ...base, result_fetch_error: (_k = doc === null || doc === void 0 ? void 0 : doc.error) !== null && _k !== void 0 ? _k : 'Backtest result not found' };
|
|
2215
|
+
}
|
|
2216
|
+
const inline = {
|
|
2217
|
+
...base,
|
|
2218
|
+
report_summary: (0, backtest_v3_1.summarizeV3Report)(doc),
|
|
2219
|
+
artifacts: doc.artifacts && typeof doc.artifacts === 'object' ? Object.keys(doc.artifacts) : [],
|
|
2220
|
+
};
|
|
2221
|
+
const reportHint = 'report_summary compacts the SDK report: metrics and net_quote_return are in the quote currency, and absent fields ' +
|
|
2222
|
+
'mean the run did not measure them (for example no price bindings), never zero. incomplete_equity_points counts marks ' +
|
|
2223
|
+
'where a held asset had no valid price.';
|
|
2224
|
+
const written = await this.writeBacktestResultArtifact(backtestId, doc);
|
|
2225
|
+
if ('refused' in written) {
|
|
2226
|
+
return { error: written.refused, statusCode: 500 };
|
|
2227
|
+
}
|
|
2228
|
+
if ('error' in written) {
|
|
2229
|
+
return { ...inline, result_write_error: written.error, hint: reportHint };
|
|
2230
|
+
}
|
|
2231
|
+
const fpath = written.path;
|
|
1878
2232
|
return {
|
|
1879
2233
|
...inline,
|
|
1880
2234
|
result_path: fpath,
|
|
1881
|
-
hint: 'Full result.json written to result_path
|
|
1882
|
-
'
|
|
1883
|
-
|
|
2235
|
+
hint: 'Full result.json written to result_path; the SDK report is under `report` (equity_curve, price_series, positions, ' +
|
|
2236
|
+
'wallets, assumptions). Grep/Read it for detail. ' +
|
|
2237
|
+
reportHint,
|
|
1884
2238
|
};
|
|
1885
2239
|
}
|
|
1886
2240
|
/**
|