@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/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(MIN_BACKTEST_SDK_VERSION);
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: ['pct_of_balance', 'usd', 'bps', 'raw'],
1007
- description: 'pct_of_balance | usd | bps | raw — drives display + funding scaling.',
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
- this.register('strategies', {
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
- }, (args) => this.runBacktest(args));
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 = (_j = (_h = refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.message) !== null && _h !== void 0 ? _h : refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.error) !== null && _j !== void 0 ? _j : `Could not resolve ref "${ref}" for the linked strategy repository.`;
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((_k = refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.statusCode) !== null && _k !== void 0 ? _k : 400, `${baseError}${recoveryGuidance}`, {
1684
- errorCode: (_l = refConfigRes === null || refConfigRes === void 0 ? void 0 : refConfigRes.errorCode) !== null && _l !== void 0 ? _l : 'INTERNAL_ERROR',
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 = (_m = refConfig.github_commit_sha) !== null && _m !== void 0 ? _m : '';
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((_o = refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.statusCode) !== null && _o !== void 0 ? _o : 400, (_q = (_p = refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.message) !== null && _p !== void 0 ? _p : refMetaRes === null || refMetaRes === void 0 ? void 0 : refMetaRes.error) !== null && _q !== void 0 ? _q : 'Could not load strategy ref metadata');
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 = (_r = refMetaRes.data.almanak_sdk_version) !== null && _r !== void 0 ? _r : null;
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 = (_s = args.strategy_config) !== null && _s !== void 0 ? _s : refConfig.effective_config_json;
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((_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', {
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: (_w = submit.status) !== null && _w !== void 0 ? _w : 'PENDING',
1733
- created_at: (_x = submit.created_at) !== null && _x !== void 0 ? _x : null,
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, _w;
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
- // Same path-hardening rules as fetchAgentLogsToFile: the id is a UUID in
1848
- // normal usage, but strip anything that isn't hex/dash before using it in
1849
- // a filename.
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 { error: 'refused to write outside .logs/', statusCode: 500 };
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
- // Compact on purpose: the artifact can be MBs and this is a Grep/jq
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
- ...inline,
1865
- result_write_error: `Could not persist result.json to .logs/: ${(_w = err === null || err === void 0 ? void 0 : err.message) !== null && _w !== void 0 ? _w : String(err)}`,
1866
- hint: telemetryHint,
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 — Grep/Read it for equity_curve, trades, price_series, and ' +
1873
- 'data_manifest entries (content is not returned inline). ' +
1874
- telemetryHint,
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
  /**