@alphafox/cli 0.3.13 → 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.
- package/dist/engine-backtest/activity.d.ts +60 -0
- package/dist/engine-backtest/activity.js +215 -0
- package/dist/engine-backtest/dca-first-order-amount-compat.d.ts +7 -0
- package/dist/engine-backtest/dca-first-order-amount-compat.js +71 -0
- package/dist/engine-backtest/load-config.d.ts +6 -2
- package/dist/engine-backtest/load-config.js +23 -0
- package/dist/engine-backtest/persist.d.ts +3 -0
- package/dist/engine-backtest/persist.js +22 -1
- package/dist/engine-backtest/prepared-tape.d.ts +10 -0
- package/dist/engine-backtest/prepared-tape.js +136 -0
- package/dist/engine-backtest/result-attribution.d.ts +35 -0
- package/dist/engine-backtest/result-attribution.js +156 -0
- package/dist/engine-backtest/run-command.js +21 -13
- package/dist/engine-backtest/sweep-command.d.ts +2 -2
- package/dist/engine-backtest/sweep-command.js +70 -51
- package/dist/engine-backtest/types.d.ts +18 -2
- package/dist/skills-manifest.json +40 -40
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/account/SKILL.md +1 -1
- package/skills/admin/SKILL.md +1 -1
- package/skills/alphafox/SKILL.md +2 -2
- package/skills/alphafox-shared/SKILL.md +1 -1
- package/skills/auth/SKILL.md +1 -1
- package/skills/cache/SKILL.md +1 -1
- package/skills/engine-backtest/SKILL.md +3 -1
- package/skills/exchange/SKILL.md +1 -1
- package/skills/market/SKILL.md +1 -1
- package/skills/notification/SKILL.md +1 -1
- package/skills/strategy/SKILL.md +17 -11
- 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.
|
|
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
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
32
|
-
|
|
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.
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
675
|
-
|
|
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 ?? "
|
|
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
|
-
|
|
241
|
-
|
|
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;
|