@alphafox/cli 0.3.12 → 0.3.14

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.
Files changed (36) hide show
  1. package/README.md +26 -0
  2. package/dist/engine-backtest/activity.d.ts +60 -0
  3. package/dist/engine-backtest/activity.js +215 -0
  4. package/dist/engine-backtest/dca-first-order-amount-compat.d.ts +7 -0
  5. package/dist/engine-backtest/dca-first-order-amount-compat.js +71 -0
  6. package/dist/engine-backtest/load-config.d.ts +6 -2
  7. package/dist/engine-backtest/load-config.js +23 -0
  8. package/dist/engine-backtest/persist.d.ts +3 -0
  9. package/dist/engine-backtest/persist.js +22 -1
  10. package/dist/engine-backtest/prepared-tape.d.ts +10 -0
  11. package/dist/engine-backtest/prepared-tape.js +136 -0
  12. package/dist/engine-backtest/result-attribution.d.ts +35 -0
  13. package/dist/engine-backtest/result-attribution.js +156 -0
  14. package/dist/engine-backtest/run-command.js +21 -13
  15. package/dist/engine-backtest/sweep-command.d.ts +2 -2
  16. package/dist/engine-backtest/sweep-command.js +70 -51
  17. package/dist/engine-backtest/types.d.ts +18 -2
  18. package/dist/skills-manifest.json +42 -42
  19. package/dist/version.d.ts +1 -1
  20. package/dist/version.js +1 -1
  21. package/docs/alphafox-cli-installation-guide.md +16 -0
  22. package/docs/release-supply-chain.md +4 -4
  23. package/package.json +2 -1
  24. package/scripts/uninstall.cjs +438 -0
  25. package/skills/account/SKILL.md +1 -1
  26. package/skills/admin/SKILL.md +1 -1
  27. package/skills/alphafox/SKILL.md +3 -3
  28. package/skills/alphafox-shared/SKILL.md +8 -1
  29. package/skills/auth/SKILL.md +1 -1
  30. package/skills/cache/SKILL.md +1 -1
  31. package/skills/engine-backtest/SKILL.md +3 -1
  32. package/skills/exchange/SKILL.md +1 -1
  33. package/skills/market/SKILL.md +1 -1
  34. package/skills/notification/SKILL.md +1 -1
  35. package/skills/strategy/SKILL.md +17 -11
  36. package/skills/trading/SKILL.md +1 -1
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.attributeEngineBacktestResult = attributeEngineBacktestResult;
4
+ function attributeEngineBacktestResult(input) {
5
+ const bySymbol = new Map();
6
+ const ensure = (symbol) => {
7
+ const existing = bySymbol.get(symbol);
8
+ if (existing)
9
+ return existing;
10
+ const created = {
11
+ symbol,
12
+ realizedPnl: 0,
13
+ liquidationRealizedPnl: 0,
14
+ unrealizedPnl: 0,
15
+ feesPaid: 0,
16
+ orderCount: 0,
17
+ filledOrderCount: 0,
18
+ canceledOrderCount: 0,
19
+ tradeCount: 0,
20
+ winningTrades: 0,
21
+ losingTrades: 0,
22
+ };
23
+ bySymbol.set(symbol, created);
24
+ return created;
25
+ };
26
+ input.symbols?.forEach(ensure);
27
+ const book = new Map();
28
+ const orders = input.result.orders
29
+ .map((order, index) => ({ order, index }))
30
+ .sort((left, right) => {
31
+ const byTimestamp = Date.parse(left.order.timestamp) - Date.parse(right.order.timestamp);
32
+ if (byTimestamp !== 0)
33
+ return byTimestamp;
34
+ return left.index - right.index;
35
+ })
36
+ .map((entry) => entry.order);
37
+ for (const order of orders) {
38
+ const attribution = ensure(order.symbol);
39
+ attribution.orderCount++;
40
+ if (isCanceled(order.status)) {
41
+ attribution.canceledOrderCount++;
42
+ continue;
43
+ }
44
+ if (!(order.filledQuantity > 0))
45
+ continue;
46
+ attribution.filledOrderCount++;
47
+ attribution.feesPaid += finiteOrZero(order.fee);
48
+ const reduction = applyFill(book, order, contractSizeFor(input.markets[order.symbol]));
49
+ if (reduction === null)
50
+ continue;
51
+ attribution.tradeCount++;
52
+ attribution.realizedPnl += reduction.realizedPnl;
53
+ if (isLiquidationOrder(order)) {
54
+ attribution.liquidationRealizedPnl += reduction.realizedPnl;
55
+ }
56
+ if (reduction.realizedPnl > 0)
57
+ attribution.winningTrades++;
58
+ else if (reduction.realizedPnl < 0)
59
+ attribution.losingTrades++;
60
+ }
61
+ for (const position of input.result.openPositions) {
62
+ ensure(position.symbol).unrealizedPnl += finiteOrZero(position.unrealizedPnl);
63
+ }
64
+ const symbols = [...bySymbol.values()].map((value) => {
65
+ const netPnl = value.realizedPnl + value.unrealizedPnl - value.feesPaid;
66
+ return {
67
+ ...value,
68
+ netPnl,
69
+ winRatePct: value.tradeCount > 0
70
+ ? (value.winningTrades / value.tradeCount) * 100
71
+ : 0,
72
+ };
73
+ });
74
+ const attributedNetPnl = symbols.reduce((total, symbol) => total + symbol.netPnl, 0);
75
+ const accountAdjustmentPnl = (input.result.accountAdjustments ?? []).reduce((total, adjustment) => total + finiteOrZero(adjustment.amount), 0);
76
+ const residualPnl = input.result.metrics.netPnl - attributedNetPnl - accountAdjustmentPnl;
77
+ const reconciliationTolerance = Math.max(0.01, Math.abs(input.result.metrics.netPnl) * 1e-6);
78
+ return {
79
+ symbols,
80
+ attributedNetPnl,
81
+ accountAdjustmentPnl,
82
+ residualPnl,
83
+ reconciliationTolerance,
84
+ reconciled: Math.abs(residualPnl) <= reconciliationTolerance,
85
+ };
86
+ }
87
+ function applyFill(book, order, contractSize) {
88
+ const side = normalizeOrderSide(order.side);
89
+ if (!side)
90
+ return null;
91
+ const positionSide = resolveAttributionPositionSide({
92
+ positionSide: order.positionSide,
93
+ reduceOnly: order.reduceOnly,
94
+ side,
95
+ });
96
+ const isReduce = order.reduceOnly ||
97
+ (positionSide === "long" && side === "sell") ||
98
+ (positionSide === "short" && side === "buy");
99
+ const key = `${order.symbol}\u0000${positionSide}`;
100
+ const quantity = order.filledQuantity * contractSize;
101
+ const position = book.get(key);
102
+ if (!isReduce) {
103
+ if (!position) {
104
+ book.set(key, { quantity, entryPrice: order.price });
105
+ return null;
106
+ }
107
+ const total = position.quantity + quantity;
108
+ if (total > 0) {
109
+ position.entryPrice =
110
+ (position.entryPrice * position.quantity + order.price * quantity) /
111
+ total;
112
+ }
113
+ position.quantity = total;
114
+ return null;
115
+ }
116
+ if (!position || position.quantity <= 0)
117
+ return null;
118
+ const closed = Math.min(quantity, position.quantity);
119
+ const realized = positionSide === "short"
120
+ ? (position.entryPrice - order.price) * closed
121
+ : (order.price - position.entryPrice) * closed;
122
+ position.quantity -= closed;
123
+ if (position.quantity <= 1e-12)
124
+ book.delete(key);
125
+ return { realizedPnl: realized };
126
+ }
127
+ function isLiquidationOrder(order) {
128
+ if (order.executionReason === "liquidation")
129
+ return true;
130
+ return order.message?.trim().toLowerCase() === "liquidation";
131
+ }
132
+ function resolveAttributionPositionSide(input) {
133
+ const explicit = input.positionSide?.trim().toLowerCase();
134
+ if (explicit === "long" || explicit === "short")
135
+ return explicit;
136
+ if (input.reduceOnly)
137
+ return input.side === "sell" ? "long" : "short";
138
+ return input.side === "buy" ? "long" : "short";
139
+ }
140
+ function normalizeOrderSide(value) {
141
+ const normalized = value.trim().toLowerCase();
142
+ return normalized === "buy" || normalized === "sell" ? normalized : null;
143
+ }
144
+ function isCanceled(status) {
145
+ const normalized = status.toLowerCase();
146
+ return normalized === "canceled" || normalized === "cancelled";
147
+ }
148
+ function contractSizeFor(market) {
149
+ if (!market || typeof market !== "object")
150
+ return 1;
151
+ const size = market.contractSize;
152
+ return typeof size === "number" && size > 0 ? size : 1;
153
+ }
154
+ function finiteOrZero(value) {
155
+ return Number.isFinite(value) ? value : 0;
156
+ }
@@ -11,11 +11,13 @@ const client_1 = require("../http/client");
11
11
  const store_1 = require("../keychain/store");
12
12
  const coverage_notice_1 = require("./coverage-notice");
13
13
  const errors_1 = require("./errors");
14
+ const dca_first_order_amount_compat_1 = require("./dca-first-order-amount-compat");
14
15
  const load_config_1 = require("./load-config");
15
16
  const parse_args_1 = require("./parse-args");
16
17
  const sweep_command_1 = require("./sweep-command");
17
18
  const persist_1 = require("./persist");
18
19
  const paths_1 = require("../cache/paths");
20
+ const prepared_tape_1 = require("./prepared-tape");
19
21
  const replay_timeframe_1 = require("./replay-timeframe");
20
22
  const resolve_packages_1 = require("./resolve-packages");
21
23
  var load_config_2 = require("./load-config");
@@ -193,10 +195,10 @@ async function executeEngineBacktestRun(args, flags, env = process.env, deps = {
193
195
  if (needsApi) {
194
196
  requireAuth(profile, env, tokensFn);
195
197
  }
196
- const config = (0, load_config_1.loadConfigValue)(args.configRaw, {
198
+ const config = (0, dca_first_order_amount_compat_1.projectDcaFirstOrderAmountForLegacyRuntime)((0, load_config_1.loadEngineBacktestConfig)(args.configRaw, {
197
199
  cwd: deps.cwd,
198
200
  readFile: deps.readFile,
199
- });
201
+ }));
200
202
  let experimentId = args.experimentId;
201
203
  if (!experimentId && !createExperiment) {
202
204
  throw new errors_1.EngineBacktestError({
@@ -319,6 +321,7 @@ async function executeEngineBacktestRun(args, flags, env = process.env, deps = {
319
321
  toMs: args.range.toMs,
320
322
  dataQualityMode: args.dataQualityMode,
321
323
  cacheDir: (0, paths_1.resolveTapeCacheDir)(env),
324
+ seriesConcurrency: prepared_tape_1.ENGINE_BACKTEST_TAPE_SERIES_CONCURRENCY,
322
325
  onProgress: (progress) => {
323
326
  emitProgress(flags, writeLine, progress.stage || "tape", progress.fraction, progress.detail);
324
327
  },
@@ -350,22 +353,24 @@ async function executeEngineBacktestRun(args, flags, env = process.env, deps = {
350
353
  runId,
351
354
  definitionId: args.definitionId,
352
355
  configSchemaVersion,
353
- config: plan.effectiveConfig ?? config,
356
+ config: (0, dca_first_order_amount_compat_1.projectDcaFirstOrderAmountForLegacyRuntime)(plan.effectiveConfig ?? config),
354
357
  subscriptionTier,
355
358
  initialEquity: args.initialEquity,
356
359
  tape: tapeResult.tape,
357
360
  executionModel,
358
361
  });
359
- const result = await client.runBacktest(scenario, tapeResult.buffers, (fraction) => {
360
- emitProgress(flags, writeLine, "wasm", fraction);
361
- });
362
- if (result.status === "failed") {
363
- throw new errors_1.EngineBacktestError({
364
- type: "runtime",
365
- subtype: "backtest_failed",
366
- message: "runBacktest returned status=failed",
367
- details: { errors: result.errors, runId: result.runId },
368
- });
362
+ const prepared = await (0, prepared_tape_1.prepareWorkerTape)(client, tapeResult.tape, tapeResult.buffers);
363
+ let result;
364
+ try {
365
+ result = (0, prepared_tape_1.requireCompletedPreparedRun)(await client.runPreparedBacktest(prepared.handle, scenario, (fraction) => {
366
+ emitProgress(flags, writeLine, "wasm", fraction);
367
+ }));
368
+ }
369
+ catch (error) {
370
+ throw (0, prepared_tape_1.mapPreparedTapeError)(error);
371
+ }
372
+ finally {
373
+ await (0, prepared_tape_1.releaseWorkerTape)(client, prepared.handle);
369
374
  }
370
375
  const engineVersion = result.engineVersion?.trim() ||
371
376
  (typeof client.version === "function" ? (await client.version()).trim() : "") ||
@@ -406,6 +411,9 @@ async function executeEngineBacktestRun(args, flags, env = process.env, deps = {
406
411
  engineVersion,
407
412
  equityCurve: result.equityCurve,
408
413
  coverageIssues: tapeResult.coverageIssues,
414
+ orders: result.orders,
415
+ openPositions: result.openPositions,
416
+ accountAdjustments: result.accountAdjustments,
409
417
  });
410
418
  const res = await postJson(api, profile, env, runsPath(experimentId), body, mintId);
411
419
  persistedRunId = extractEntityId(res.json);
@@ -1,6 +1,6 @@
1
1
  import type { EngineBacktestCliFlags, EngineBacktestRunDeps } from "./run-command";
2
2
  import type { EngineBacktestSweepArgs, EngineBacktestSweepSuccess } from "./types";
3
- /** WASM `runBacktestBatch` hard limit. Keep in sync with Engine. */
3
+ /** WASM prepared-batch variant hard limit. Keep in sync with Engine. */
4
4
  export declare const MAX_ENGINE_BACKTEST_BATCH_VARIANTS = 256;
5
5
  /** Sweep chunk size: below the 256 cap so JSONL can refresh mid-search. */
6
6
  export declare const SWEEP_WASM_BATCH_VARIANTS = 32;
@@ -8,6 +8,6 @@ export interface EngineBacktestSweepDeps extends EngineBacktestRunDeps {
8
8
  readonly maxVariantsPerBatch?: number;
9
9
  readonly now?: () => number;
10
10
  }
11
- export declare function cloneTapeBuffers(sourceBuffers: Readonly<Record<string, ArrayBuffer>>): Record<string, ArrayBuffer>;
11
+ export { cloneTapeBuffers } from "./prepared-tape";
12
12
  export declare function splitBatchChunk<T>(chunk: readonly T[], maxVariants?: number): T[][];
13
13
  export declare function executeEngineBacktestSweep(args: EngineBacktestSweepArgs, flags: EngineBacktestCliFlags, env?: NodeJS.ProcessEnv, deps?: EngineBacktestSweepDeps): Promise<EngineBacktestSweepSuccess>;
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SWEEP_WASM_BATCH_VARIANTS = exports.MAX_ENGINE_BACKTEST_BATCH_VARIANTS = void 0;
4
- exports.cloneTapeBuffers = cloneTapeBuffers;
3
+ exports.cloneTapeBuffers = exports.SWEEP_WASM_BATCH_VARIANTS = exports.MAX_ENGINE_BACKTEST_BATCH_VARIANTS = void 0;
5
4
  exports.splitBatchChunk = splitBatchChunk;
6
5
  exports.executeEngineBacktestSweep = executeEngineBacktestSweep;
7
6
  const node_crypto_1 = require("node:crypto");
@@ -11,13 +10,15 @@ const paths_1 = require("../cache/paths");
11
10
  const store_1 = require("../keychain/store");
12
11
  const coverage_notice_1 = require("./coverage-notice");
13
12
  const errors_1 = require("./errors");
13
+ const dca_first_order_amount_compat_1 = require("./dca-first-order-amount-compat");
14
14
  const load_config_1 = require("./load-config");
15
15
  const parse_axes_1 = require("./parse-axes");
16
16
  const persist_1 = require("./persist");
17
+ const prepared_tape_1 = require("./prepared-tape");
17
18
  const replay_timeframe_1 = require("./replay-timeframe");
18
19
  const resolve_packages_1 = require("./resolve-packages");
19
20
  const sweep_kernel_1 = require("./sweep-kernel");
20
- /** WASM `runBacktestBatch` hard limit. Keep in sync with Engine. */
21
+ /** WASM prepared-batch variant hard limit. Keep in sync with Engine. */
21
22
  exports.MAX_ENGINE_BACKTEST_BATCH_VARIANTS = 256;
22
23
  /** Sweep chunk size: below the 256 cap so JSONL can refresh mid-search. */
23
24
  exports.SWEEP_WASM_BATCH_VARIANTS = 32;
@@ -28,9 +29,8 @@ const SUBSCRIPTION_TIERS = new Set([
28
29
  "pro",
29
30
  "pro_max",
30
31
  ]);
31
- function cloneTapeBuffers(sourceBuffers) {
32
- return Object.fromEntries(Object.entries(sourceBuffers).map(([key, buffer]) => [key, buffer.slice(0)]));
33
- }
32
+ var prepared_tape_2 = require("./prepared-tape");
33
+ Object.defineProperty(exports, "cloneTapeBuffers", { enumerable: true, get: function () { return prepared_tape_2.cloneTapeBuffers; } });
34
34
  function splitBatchChunk(chunk, maxVariants = exports.MAX_ENGINE_BACKTEST_BATCH_VARIANTS) {
35
35
  const limit = Math.max(1, Math.floor(maxVariants));
36
36
  if (chunk.length <= limit)
@@ -72,7 +72,7 @@ async function executeEngineBacktestSweep(args, flags, env = process.env, deps =
72
72
  if (persist) {
73
73
  requireAuth(profile, env, tokensFn);
74
74
  }
75
- const config = asConfigRecord((0, load_config_1.loadConfigValue)(args.configRaw, {
75
+ const config = asConfigRecord((0, load_config_1.loadEngineBacktestConfig)(args.configRaw, {
76
76
  cwd: deps.cwd,
77
77
  readFile: deps.readFile,
78
78
  }));
@@ -193,6 +193,7 @@ async function executeEngineBacktestSweep(args, flags, env = process.env, deps =
193
193
  toMs: args.range.toMs,
194
194
  dataQualityMode: args.dataQualityMode,
195
195
  cacheDir: (0, paths_1.resolveTapeCacheDir)(env),
196
+ seriesConcurrency: prepared_tape_1.ENGINE_BACKTEST_TAPE_SERIES_CONCURRENCY,
196
197
  onProgress: (progress) => {
197
198
  emitProgress(flags, writeLine, progress.stage || "tape", progress.fraction, progress.detail);
198
199
  },
@@ -584,7 +585,7 @@ async function resolveConfigSchemaVersion(client, definitionId, explicit) {
584
585
  return version;
585
586
  }
586
587
  async function planCoordinate(input) {
587
- const nextConfig = (0, sweep_kernel_1.applySweepCoordinate)(input.config, input.axes, input.coordinate);
588
+ const nextConfig = asConfigRecord((0, dca_first_order_amount_compat_1.projectDcaFirstOrderAmountForLegacyRuntime)((0, sweep_kernel_1.applySweepCoordinate)(input.config, input.axes, input.coordinate)));
588
589
  try {
589
590
  const plan = await input.client.planBacktest({
590
591
  definitionId: input.definitionId,
@@ -619,6 +620,52 @@ async function planCoordinate(input) {
619
620
  };
620
621
  }
621
622
  }
623
+ async function runPreparedWorkerBatches(args) {
624
+ for (const subChunk of splitBatchChunk(args.chunk, args.input.maxVariantsPerBatch)) {
625
+ const scenarios = subChunk.map((item) => args.input.runner.assembleScenario({
626
+ runId: args.input.mintId(),
627
+ definitionId: args.input.definitionId,
628
+ configSchemaVersion: args.input.configSchemaVersion,
629
+ config: item.item.config,
630
+ subscriptionTier: args.input.subscriptionTier,
631
+ initialEquity: args.input.initialEquity,
632
+ tape: args.input.tape,
633
+ executionModel: args.input.executionModel,
634
+ }));
635
+ const first = scenarios[0];
636
+ if (!first)
637
+ continue;
638
+ const { tape: _tape, ...baseScenario } = first;
639
+ const batch = {
640
+ version: 1,
641
+ batchId: args.input.mintId(),
642
+ baseScenario,
643
+ variants: scenarios.map((scenario) => ({
644
+ runId: scenario.runId,
645
+ config: scenario.trader.config,
646
+ })),
647
+ tape: args.input.tape,
648
+ };
649
+ const result = await args.client.runPreparedBacktestBatch(args.handle, batch);
650
+ (0, prepared_tape_1.throwIfPreparedTapeErrors)(result.errors, result);
651
+ if (result.results.length !== subChunk.length) {
652
+ throw new errors_1.EngineBacktestError({
653
+ type: "runtime",
654
+ subtype: "batch_result_count",
655
+ message: "runPreparedBacktestBatch returned an unexpected result count",
656
+ details: {
657
+ expected: subChunk.length,
658
+ actual: result.results.length,
659
+ },
660
+ });
661
+ }
662
+ subChunk.forEach((entry, index) => {
663
+ args.completed[entry.index] = readBatchPoint(entry.item.coordinate, result.results[index]);
664
+ });
665
+ const finished = args.completed.filter((point) => point !== undefined);
666
+ args.input.onProgress(finished.length, finished);
667
+ }
668
+ }
622
669
  async function executeSweepBatches(input) {
623
670
  if (input.ready.length === 0) {
624
671
  return [];
@@ -630,49 +677,21 @@ async function executeSweepBatches(input) {
630
677
  });
631
678
  await Promise.all(chunks.map(async (chunk, workerIndex) => {
632
679
  const client = workers[workerIndex];
633
- for (const subChunk of splitBatchChunk(chunk, input.maxVariantsPerBatch)) {
634
- const scenarios = subChunk.map((item) => input.runner.assembleScenario({
635
- runId: input.mintId(),
636
- definitionId: input.definitionId,
637
- configSchemaVersion: input.configSchemaVersion,
638
- config: item.item.config,
639
- subscriptionTier: input.subscriptionTier,
640
- initialEquity: input.initialEquity,
641
- tape: input.tape,
642
- executionModel: input.executionModel,
643
- }));
644
- const first = scenarios[0];
645
- if (!first)
646
- continue;
647
- const { tape: _tape, ...baseScenario } = first;
648
- const batch = {
649
- version: 1,
650
- batchId: input.mintId(),
651
- baseScenario,
652
- variants: scenarios.map((scenario) => ({
653
- runId: scenario.runId,
654
- config: scenario.trader.config,
655
- })),
656
- tape: input.tape,
657
- };
658
- const result = await client.runBacktestBatch(batch, cloneTapeBuffers(input.buffers));
659
- if (result.results.length !== subChunk.length) {
660
- throw new errors_1.EngineBacktestError({
661
- type: "runtime",
662
- subtype: "batch_result_count",
663
- message: "runBacktestBatch returned an unexpected result count",
664
- details: {
665
- expected: subChunk.length,
666
- actual: result.results.length,
667
- },
668
- });
669
- }
670
- subChunk.forEach((entry, index) => {
671
- const point = result.results[index];
672
- completed[entry.index] = readBatchPoint(entry.item.coordinate, point);
680
+ const prepared = await (0, prepared_tape_1.prepareWorkerTape)(client, input.tape, input.buffers);
681
+ try {
682
+ await runPreparedWorkerBatches({
683
+ client,
684
+ handle: prepared.handle,
685
+ chunk,
686
+ completed,
687
+ input,
673
688
  });
674
- const finished = completed.filter((point) => point !== undefined);
675
- input.onProgress(finished.length, finished);
689
+ }
690
+ catch (error) {
691
+ (0, prepared_tape_1.mapPreparedTapeError)(error);
692
+ }
693
+ finally {
694
+ await (0, prepared_tape_1.releaseWorkerTape)(client, prepared.handle);
676
695
  }
677
696
  }));
678
697
  return completed.filter((point) => point !== undefined);
@@ -688,7 +707,7 @@ function readBatchPoint(coordinate, point) {
688
707
  return {
689
708
  coordinate,
690
709
  status: "failed",
691
- error: point.errors?.[0]?.message ?? "runBacktestBatch variant failed",
710
+ error: point.errors?.[0]?.message ?? "runPreparedBacktestBatch variant failed",
692
711
  };
693
712
  }
694
713
  function partitionRoundRobin(items, workerCount) {
@@ -1,3 +1,4 @@
1
+ import type { EngineBacktestActivity } from "./activity";
1
2
  export type SubscriptionTier = "free" | "pro" | "pro_max";
2
3
  export type DataQualityMode = "strict" | "basic";
3
4
  /** New runs default to basic; `--data-quality strict` remains an explicit override. */
@@ -124,6 +125,7 @@ export interface EngineBacktestResult {
124
125
  readonly equityCurve?: unknown[];
125
126
  readonly orders?: unknown[];
126
127
  readonly openPositions?: unknown[];
128
+ readonly accountAdjustments?: unknown[];
127
129
  readonly errors?: Array<{
128
130
  code: string;
129
131
  message: string;
@@ -222,6 +224,10 @@ export interface TapeLoadResult {
222
224
  readonly coverageWarnings: readonly string[];
223
225
  readonly coverageIssues: readonly TapeCoverageIssue[];
224
226
  }
227
+ export interface EnginePreparedTape {
228
+ readonly handle: string;
229
+ readonly fingerprint: string;
230
+ }
225
231
  export interface BacktestClientLike {
226
232
  init(): Promise<string>;
227
233
  version(): Promise<string>;
@@ -237,8 +243,15 @@ export interface BacktestClientLike {
237
243
  readonly configSchemaVersion: number;
238
244
  readonly config: unknown;
239
245
  }): Promise<EngineBacktestPlan | EngineBacktestFailure>;
240
- runBacktest(scenario: EngineBacktestScenario, buffers: Record<string, ArrayBuffer>, onProgress?: (fraction: number) => void): Promise<EngineBacktestResult>;
241
- runBacktestBatch(batch: EngineBacktestBatchRequest, buffers: Record<string, ArrayBuffer>, onProgress?: (fraction: number) => void): Promise<EngineBacktestBatchResult>;
246
+ prepareTape(tape: EngineBacktestTapeInput, buffers: Record<string, ArrayBuffer>): Promise<EnginePreparedTape>;
247
+ runPreparedBacktest(handle: string, scenario: EngineBacktestScenario, onProgress?: (fraction: number) => void): Promise<EngineBacktestResult>;
248
+ runPreparedBacktestBatch(handle: string, batch: EngineBacktestBatchRequest, onProgress?: (fraction: number) => void): Promise<EngineBacktestBatchResult>;
249
+ releaseTape(handle: string): Promise<{
250
+ readonly released: true;
251
+ }>;
252
+ /** Legacy one-shot WASM API. CLI host must not call these. */
253
+ runBacktest?(scenario: EngineBacktestScenario, buffers: Record<string, ArrayBuffer>, onProgress?: (fraction: number) => void): Promise<EngineBacktestResult>;
254
+ runBacktestBatch?(batch: EngineBacktestBatchRequest, buffers: Record<string, ArrayBuffer>, onProgress?: (fraction: number) => void): Promise<EngineBacktestBatchResult>;
242
255
  terminate(): void;
243
256
  }
244
257
  export interface BacktestWasmModule {
@@ -258,6 +271,7 @@ export interface BacktestRunnerModule {
258
271
  readonly toMs: number;
259
272
  readonly dataQualityMode?: DataQualityMode;
260
273
  readonly cacheDir?: string;
274
+ readonly seriesConcurrency?: number;
261
275
  readonly onProgress?: (progress: TapeLoadProgress) => void;
262
276
  }, options?: unknown): Promise<TapeLoadResult>;
263
277
  assembleScenario(input: {
@@ -300,6 +314,8 @@ export interface CreateRunRequestBody {
300
314
  readonly configSchemaVersion: number;
301
315
  /** Compact [[unix_ms, cumulative_return]]; omitted when the local run has no series. */
302
316
  readonly returnCurve?: ReadonlyArray<readonly [number, number]>;
317
+ /** Ledger snapshot + filled-order tail. Omitted only when persist is skipped. */
318
+ readonly activity?: EngineBacktestActivity;
303
319
  }
304
320
  export interface EngineBacktestRunSuccess {
305
321
  readonly metrics: EngineBacktestMetrics;