@alphafox/cli 0.3.2 → 0.3.4
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/README.md +4 -3
- package/dist/auth/loopback-callback.js +6 -6
- package/dist/catalog/allowlist.d.ts +1 -1
- package/dist/catalog/allowlist.js +1 -1
- package/dist/catalog/generated/registry.json +338 -49
- package/dist/catalog/generated/schemas.json +1880 -779
- package/dist/catalog/omit.d.ts +6 -0
- package/dist/catalog/omit.js +11 -0
- package/dist/catalog/operations.d.ts +1 -1
- package/dist/catalog/operations.js +5 -2
- package/dist/commands/run.js +10 -3
- package/dist/engine-backtest/load-config.d.ts +4 -0
- package/dist/engine-backtest/load-config.js +44 -0
- package/dist/engine-backtest/parse-args.d.ts +3 -1
- package/dist/engine-backtest/parse-args.js +87 -3
- package/dist/engine-backtest/parse-axes.d.ts +3 -0
- package/dist/engine-backtest/parse-axes.js +167 -0
- package/dist/engine-backtest/persist.d.ts +60 -1
- package/dist/engine-backtest/persist.js +196 -1
- package/dist/engine-backtest/return-curve.d.ts +27 -0
- package/dist/engine-backtest/return-curve.js +80 -0
- package/dist/engine-backtest/run-command.d.ts +1 -4
- package/dist/engine-backtest/run-command.js +61 -45
- package/dist/engine-backtest/sweep-command.d.ts +13 -0
- package/dist/engine-backtest/sweep-command.js +749 -0
- package/dist/engine-backtest/sweep-kernel/caps.d.ts +26 -0
- package/dist/engine-backtest/sweep-kernel/caps.js +48 -0
- package/dist/engine-backtest/sweep-kernel/index.d.ts +13 -0
- package/dist/engine-backtest/sweep-kernel/index.js +34 -0
- package/dist/engine-backtest/sweep-kernel/plan.d.ts +22 -0
- package/dist/engine-backtest/sweep-kernel/plan.js +320 -0
- package/dist/engine-backtest/sweep-kernel/results.d.ts +72 -0
- package/dist/engine-backtest/sweep-kernel/results.js +150 -0
- package/dist/engine-backtest/sweep-kernel/session.d.ts +55 -0
- package/dist/engine-backtest/sweep-kernel/session.js +56 -0
- package/dist/engine-backtest/sweep-kernel/types.d.ts +36 -0
- package/dist/engine-backtest/sweep-kernel/types.js +2 -0
- package/dist/engine-backtest/types.d.ts +130 -0
- package/dist/install/exec.js +5 -1
- package/dist/install/wizard.js +26 -29
- package/dist/resolve-symbols/catalog.d.ts +3 -0
- package/dist/resolve-symbols/catalog.js +50 -0
- package/dist/resolve-symbols/errors.d.ts +18 -0
- package/dist/resolve-symbols/errors.js +26 -0
- package/dist/resolve-symbols/exchanges.d.ts +8 -0
- package/dist/resolve-symbols/exchanges.js +53 -0
- package/dist/resolve-symbols/match.d.ts +6 -0
- package/dist/resolve-symbols/match.js +250 -0
- package/dist/resolve-symbols/parse-args.d.ts +5 -0
- package/dist/resolve-symbols/parse-args.js +106 -0
- package/dist/resolve-symbols/run-command.d.ts +21 -0
- package/dist/resolve-symbols/run-command.js +149 -0
- package/dist/resolve-symbols/types.d.ts +37 -0
- package/dist/resolve-symbols/types.js +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/alphafox-cli-installation-guide.md +2 -2
- package/package.json +1 -1
- package/skills/account/SKILL.md +1 -1
- package/skills/admin/SKILL.md +1 -1
- package/skills/alphafox/SKILL.md +38 -0
- package/skills/alphafox-shared/SKILL.md +5 -1
- package/skills/auth/SKILL.md +1 -1
- package/skills/engine-backtest/SKILL.md +48 -6
- package/skills/exchange/SKILL.md +1 -1
- package/skills/market/SKILL.md +27 -4
- package/skills/notification/SKILL.md +1 -1
- package/skills/strategy/SKILL.md +7 -20
- package/skills/trading/SKILL.md +3 -1
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SNAPSHOT_SCHEMA_VERSION = exports.DEFAULT_EXECUTION_MODEL = void 0;
|
|
3
|
+
exports.ENGINE_BACKTEST_SWEEP_ERROR_MAX_UTF8_BYTES = exports.ENGINE_BACKTEST_SWEEP_MAX_UTF8_BYTES = exports.SNAPSHOT_SCHEMA_VERSION = exports.DEFAULT_EXECUTION_MODEL = void 0;
|
|
4
4
|
exports.exclusiveTapeEndToRangeEnd = exclusiveTapeEndToRangeEnd;
|
|
5
5
|
exports.uniqueSeriesField = uniqueSeriesField;
|
|
6
6
|
exports.buildCreateRunRequest = buildCreateRunRequest;
|
|
7
7
|
exports.experimentPageUrl = experimentPageUrl;
|
|
8
|
+
exports.experimentSweepPageUrl = experimentSweepPageUrl;
|
|
9
|
+
exports.shouldPersistSweepCompletion = shouldPersistSweepCompletion;
|
|
10
|
+
exports.mintClientSweepId = mintClientSweepId;
|
|
11
|
+
exports.sweepsPath = sweepsPath;
|
|
12
|
+
exports.buildSweepBaseSnapshot = buildSweepBaseSnapshot;
|
|
13
|
+
exports.buildCreateSweepRequest = buildCreateSweepRequest;
|
|
14
|
+
exports.assertSweepCreateRequestSize = assertSweepCreateRequestSize;
|
|
8
15
|
const errors_1 = require("./errors");
|
|
16
|
+
const return_curve_1 = require("./return-curve");
|
|
9
17
|
/** Aligned with `@alphafoxai/backtest-runner` DEFAULT_EXECUTION_MODEL. */
|
|
10
18
|
exports.DEFAULT_EXECUTION_MODEL = {
|
|
11
19
|
pricePath: "ohlc_path_4",
|
|
@@ -69,12 +77,17 @@ function buildCreateRunRequest(input) {
|
|
|
69
77
|
positionSideDual: scenario.exchange.positionSideDual,
|
|
70
78
|
engineVersion,
|
|
71
79
|
};
|
|
80
|
+
const returnCurve = (0, return_curve_1.compressEngineBacktestReturnCurve)({
|
|
81
|
+
initialEquity: snapshot.initialEquity,
|
|
82
|
+
equityCurve: input.equityCurve,
|
|
83
|
+
});
|
|
72
84
|
return {
|
|
73
85
|
clientRunId: input.clientRunId,
|
|
74
86
|
snapshot,
|
|
75
87
|
metrics: input.metrics,
|
|
76
88
|
engineVersion,
|
|
77
89
|
configSchemaVersion: snapshot.configSchemaVersion,
|
|
90
|
+
...(returnCurve ? { returnCurve } : {}),
|
|
78
91
|
};
|
|
79
92
|
}
|
|
80
93
|
/**
|
|
@@ -92,3 +105,185 @@ function experimentPageUrl(profile, experimentId) {
|
|
|
92
105
|
}
|
|
93
106
|
return `https://alphafox.app${path}`;
|
|
94
107
|
}
|
|
108
|
+
/** Web experiment workspace opens the Sweep tab with `?tab=sweep`. */
|
|
109
|
+
function experimentSweepPageUrl(profile, experimentId) {
|
|
110
|
+
return `${experimentPageUrl(profile, experimentId)}?tab=sweep`;
|
|
111
|
+
}
|
|
112
|
+
exports.ENGINE_BACKTEST_SWEEP_MAX_UTF8_BYTES = 4 * 1024 * 1024;
|
|
113
|
+
exports.ENGINE_BACKTEST_SWEEP_ERROR_MAX_UTF8_BYTES = 4 * 1024;
|
|
114
|
+
const CLIENT_SWEEP_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
115
|
+
function shouldPersistSweepCompletion(input) {
|
|
116
|
+
return input.completed && !input.cancelled;
|
|
117
|
+
}
|
|
118
|
+
function mintClientSweepId(randomUuid = () => globalThis.crypto.randomUUID()) {
|
|
119
|
+
const allocated = randomUuid();
|
|
120
|
+
if (!CLIENT_SWEEP_ID_RE.test(allocated)) {
|
|
121
|
+
throw new errors_1.EngineBacktestError({
|
|
122
|
+
type: "runtime",
|
|
123
|
+
subtype: "invalid_client_sweep_id",
|
|
124
|
+
message: "clientSweepId must be a UUID.",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return allocated;
|
|
128
|
+
}
|
|
129
|
+
function sweepsPath(experimentId) {
|
|
130
|
+
return `/api/v1/engine-backtest/experiments/${encodeURIComponent(experimentId)}/sweeps`;
|
|
131
|
+
}
|
|
132
|
+
function buildSweepBaseSnapshot(input) {
|
|
133
|
+
const symbols = [...new Set(input.symbols.map((item) => item.trim()).filter(Boolean))];
|
|
134
|
+
if (symbols.length === 0) {
|
|
135
|
+
throw new errors_1.EngineBacktestError({
|
|
136
|
+
type: "runtime",
|
|
137
|
+
subtype: "persist_no_symbols",
|
|
138
|
+
message: "Completed sweep has no symbols to persist.",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const timeframes = [
|
|
142
|
+
...new Set(input.timeframes.map((item) => item.trim()).filter(Boolean)),
|
|
143
|
+
];
|
|
144
|
+
return {
|
|
145
|
+
snapshotSchemaVersion: exports.SNAPSHOT_SCHEMA_VERSION,
|
|
146
|
+
strategyDefinitionId: input.definitionId,
|
|
147
|
+
configSchemaVersion: input.configSchemaVersion,
|
|
148
|
+
config: input.config,
|
|
149
|
+
exchangeId: input.exchangeId.trim() || "binance",
|
|
150
|
+
rangeStart: input.rangeStart,
|
|
151
|
+
rangeEnd: input.rangeEnd,
|
|
152
|
+
initialEquity: input.initialEquity,
|
|
153
|
+
subscriptionTier: input.subscriptionTier,
|
|
154
|
+
dataQualityMode: input.dataQualityMode,
|
|
155
|
+
symbols,
|
|
156
|
+
timeframes: timeframes.length > 0 ? timeframes : ["1m"],
|
|
157
|
+
baseTimeframe: input.baseTimeframe.trim() || "1m",
|
|
158
|
+
executionModel: input.executionModel,
|
|
159
|
+
positionSideDual: input.positionSideDual,
|
|
160
|
+
engineVersion: input.engineVersion.trim() || "node-wasm",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Build POST .../sweeps body from a completed local search.
|
|
165
|
+
* Field set matches web `buildCreateSweepRequestFromCompleted`.
|
|
166
|
+
*/
|
|
167
|
+
function buildCreateSweepRequest(input) {
|
|
168
|
+
const clientSweepId = mintClientSweepId(() => input.clientSweepId);
|
|
169
|
+
const axes = input.axes.map((axis, axisIndex) => ({
|
|
170
|
+
path: [...axis.path],
|
|
171
|
+
current: axis.current,
|
|
172
|
+
values: uniqueFiniteNumbers(input.points.map((point) => point.coordinate.values[axisIndex]), axis.current),
|
|
173
|
+
}));
|
|
174
|
+
const pointSummaries = input.points.map((point) => toPointSummary(point));
|
|
175
|
+
const body = {
|
|
176
|
+
clientSweepId,
|
|
177
|
+
baseInputSnapshot: input.snapshot,
|
|
178
|
+
axes,
|
|
179
|
+
searchMetadata: {
|
|
180
|
+
mode: input.mode,
|
|
181
|
+
searchMode: input.searchMode,
|
|
182
|
+
concurrency: input.concurrency,
|
|
183
|
+
requestedCombinationCount: input.requestedCombinationCount,
|
|
184
|
+
sampled: input.sampled,
|
|
185
|
+
},
|
|
186
|
+
aggregateSummary: {
|
|
187
|
+
successfulCount: input.successfulCount,
|
|
188
|
+
failedCount: input.failedCount,
|
|
189
|
+
liquidatedCount: input.liquidatedCount,
|
|
190
|
+
elapsedMs: Math.max(0, Math.round(input.elapsedMs)),
|
|
191
|
+
best: input.best
|
|
192
|
+
? {
|
|
193
|
+
coordinate: { values: [...input.best.coordinate.values] },
|
|
194
|
+
returnPct: input.best.returnPct,
|
|
195
|
+
}
|
|
196
|
+
: null,
|
|
197
|
+
},
|
|
198
|
+
pointSummaries,
|
|
199
|
+
engineVersion: input.engineVersion.trim() || input.snapshot.engineVersion,
|
|
200
|
+
configSchemaVersion: input.snapshot.configSchemaVersion,
|
|
201
|
+
};
|
|
202
|
+
assertSweepCreateRequestSize(body);
|
|
203
|
+
return body;
|
|
204
|
+
}
|
|
205
|
+
function toPointSummary(row) {
|
|
206
|
+
const coordinate = { values: [...row.coordinate.values] };
|
|
207
|
+
if (row.status === "ok" && row.metrics) {
|
|
208
|
+
return {
|
|
209
|
+
coordinate,
|
|
210
|
+
status: "ok",
|
|
211
|
+
metrics: {
|
|
212
|
+
returnPct: row.metrics.returnPct,
|
|
213
|
+
maxDrawdownPct: row.metrics.maxDrawdownPct,
|
|
214
|
+
sharpeRatio: row.metrics.sharpeRatio,
|
|
215
|
+
winRatePct: row.metrics.winRatePct,
|
|
216
|
+
liquidationCount: row.metrics.liquidationCount,
|
|
217
|
+
netPnl: row.metrics.netPnl,
|
|
218
|
+
finalEquity: row.metrics.finalEquity,
|
|
219
|
+
tradeCount: row.metrics.tradeCount,
|
|
220
|
+
...(typeof row.metrics.maxLeverage === "number" &&
|
|
221
|
+
Number.isFinite(row.metrics.maxLeverage)
|
|
222
|
+
? { maxLeverage: row.metrics.maxLeverage }
|
|
223
|
+
: {}),
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
coordinate,
|
|
229
|
+
status: "failed",
|
|
230
|
+
error: truncateUtf8(row.error?.trim() || "Point failed", exports.ENGINE_BACKTEST_SWEEP_ERROR_MAX_UTF8_BYTES),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function uniqueFiniteNumbers(values, fallback) {
|
|
234
|
+
const unique = new Set();
|
|
235
|
+
for (const value of values) {
|
|
236
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
237
|
+
unique.add(value);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (unique.size === 0) {
|
|
241
|
+
unique.add(fallback);
|
|
242
|
+
}
|
|
243
|
+
return [...unique].sort((left, right) => left - right);
|
|
244
|
+
}
|
|
245
|
+
function truncateUtf8(value, maxBytes) {
|
|
246
|
+
const bytes = new TextEncoder().encode(value);
|
|
247
|
+
if (bytes.length <= maxBytes) {
|
|
248
|
+
return value;
|
|
249
|
+
}
|
|
250
|
+
return new TextDecoder("utf-8", { fatal: false })
|
|
251
|
+
.decode(bytes.slice(0, maxBytes))
|
|
252
|
+
.replace(/\uFFFD$/u, "");
|
|
253
|
+
}
|
|
254
|
+
function utf8ByteLength(value) {
|
|
255
|
+
return new TextEncoder().encode(value).length;
|
|
256
|
+
}
|
|
257
|
+
function assertSweepCreateRequestSize(body) {
|
|
258
|
+
let serialized;
|
|
259
|
+
try {
|
|
260
|
+
serialized = JSON.stringify(body);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
throw new errors_1.EngineBacktestError({
|
|
264
|
+
type: "usage",
|
|
265
|
+
subtype: "sweep_invalid",
|
|
266
|
+
message: "Sweep create request is not JSON-serializable.",
|
|
267
|
+
status: 400,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (utf8ByteLength(serialized) > exports.ENGINE_BACKTEST_SWEEP_MAX_UTF8_BYTES) {
|
|
271
|
+
throw new errors_1.EngineBacktestError({
|
|
272
|
+
type: "usage",
|
|
273
|
+
subtype: "sweep_too_large",
|
|
274
|
+
message: `Sweep create request exceeds ${exports.ENGINE_BACKTEST_SWEEP_MAX_UTF8_BYTES} UTF-8 bytes.`,
|
|
275
|
+
status: 400,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
for (const [index, point] of body.pointSummaries.entries()) {
|
|
279
|
+
if (point.error !== undefined &&
|
|
280
|
+
utf8ByteLength(point.error) > exports.ENGINE_BACKTEST_SWEEP_ERROR_MAX_UTF8_BYTES) {
|
|
281
|
+
throw new errors_1.EngineBacktestError({
|
|
282
|
+
type: "usage",
|
|
283
|
+
subtype: "sweep_too_large",
|
|
284
|
+
message: `Sweep point error text at index ${index} exceeds ${exports.ENGINE_BACKTEST_SWEEP_ERROR_MAX_UTF8_BYTES} UTF-8 bytes.`,
|
|
285
|
+
status: 400,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact Engine Backtest Run return-curve contract.
|
|
3
|
+
* Must stay aligned with alphafox-web `domains/engine-backtest-return-curve.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Stored shape: [[unix_ms, cumulative_return]]
|
|
6
|
+
* - timestamp: engine equity point `t`, Unix milliseconds
|
|
7
|
+
* - cumulative_return: (equity / initialEquity) - 1 (decimal ratio, not percent),
|
|
8
|
+
* rounded to 10 decimal places
|
|
9
|
+
* - even-sample when longer than 4000: index = round(i * (n-1) / (limit-1))
|
|
10
|
+
*/
|
|
11
|
+
export declare const ENGINE_BACKTEST_RETURN_CURVE_MAX_POINTS = 4000;
|
|
12
|
+
/** Must match alphafox-web `ENGINE_BACKTEST_RETURN_CURVE_RETURN_DECIMALS`. */
|
|
13
|
+
export declare const ENGINE_BACKTEST_RETURN_CURVE_RETURN_DECIMALS = 10;
|
|
14
|
+
export declare function roundCumulativeReturn(value: number): number;
|
|
15
|
+
export type EngineBacktestReturnCurvePoint = readonly [number, number];
|
|
16
|
+
export type EngineBacktestReturnCurve = readonly EngineBacktestReturnCurvePoint[];
|
|
17
|
+
export interface EngineBacktestEquitySample {
|
|
18
|
+
readonly t: number;
|
|
19
|
+
readonly equity: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function downsampleEvenlySpacedPoints<T>(points: readonly T[], limit: number): T[];
|
|
22
|
+
export declare function readEquitySamples(value: unknown): EngineBacktestEquitySample[];
|
|
23
|
+
export declare function compressEngineBacktestReturnCurve(input: {
|
|
24
|
+
readonly initialEquity: number;
|
|
25
|
+
readonly equityCurve: unknown;
|
|
26
|
+
readonly maxPoints?: number;
|
|
27
|
+
}): EngineBacktestReturnCurve | undefined;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Compact Engine Backtest Run return-curve contract.
|
|
4
|
+
* Must stay aligned with alphafox-web `domains/engine-backtest-return-curve.ts`.
|
|
5
|
+
*
|
|
6
|
+
* Stored shape: [[unix_ms, cumulative_return]]
|
|
7
|
+
* - timestamp: engine equity point `t`, Unix milliseconds
|
|
8
|
+
* - cumulative_return: (equity / initialEquity) - 1 (decimal ratio, not percent),
|
|
9
|
+
* rounded to 10 decimal places
|
|
10
|
+
* - even-sample when longer than 4000: index = round(i * (n-1) / (limit-1))
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.ENGINE_BACKTEST_RETURN_CURVE_RETURN_DECIMALS = exports.ENGINE_BACKTEST_RETURN_CURVE_MAX_POINTS = void 0;
|
|
14
|
+
exports.roundCumulativeReturn = roundCumulativeReturn;
|
|
15
|
+
exports.downsampleEvenlySpacedPoints = downsampleEvenlySpacedPoints;
|
|
16
|
+
exports.readEquitySamples = readEquitySamples;
|
|
17
|
+
exports.compressEngineBacktestReturnCurve = compressEngineBacktestReturnCurve;
|
|
18
|
+
exports.ENGINE_BACKTEST_RETURN_CURVE_MAX_POINTS = 4_000;
|
|
19
|
+
/** Must match alphafox-web `ENGINE_BACKTEST_RETURN_CURVE_RETURN_DECIMALS`. */
|
|
20
|
+
exports.ENGINE_BACKTEST_RETURN_CURVE_RETURN_DECIMALS = 10;
|
|
21
|
+
function roundCumulativeReturn(value) {
|
|
22
|
+
if (value === 0) {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
const factor = 10 ** exports.ENGINE_BACKTEST_RETURN_CURVE_RETURN_DECIMALS;
|
|
26
|
+
return Math.round(value * factor) / factor;
|
|
27
|
+
}
|
|
28
|
+
function downsampleEvenlySpacedPoints(points, limit) {
|
|
29
|
+
if (points.length <= limit) {
|
|
30
|
+
return [...points];
|
|
31
|
+
}
|
|
32
|
+
if (limit <= 1) {
|
|
33
|
+
return points[0] === undefined ? [] : [points[0]];
|
|
34
|
+
}
|
|
35
|
+
const lastIndex = points.length - 1;
|
|
36
|
+
const sampled = [];
|
|
37
|
+
let previousIndex = -1;
|
|
38
|
+
for (let slot = 0; slot < limit; slot += 1) {
|
|
39
|
+
const index = Math.round((slot * lastIndex) / (limit - 1));
|
|
40
|
+
if (index === previousIndex) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
sampled.push(points[index]);
|
|
44
|
+
previousIndex = index;
|
|
45
|
+
}
|
|
46
|
+
return sampled;
|
|
47
|
+
}
|
|
48
|
+
function readEquitySamples(value) {
|
|
49
|
+
if (!Array.isArray(value)) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const samples = [];
|
|
53
|
+
for (const item of value) {
|
|
54
|
+
if (!item || typeof item !== "object") {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const record = item;
|
|
58
|
+
const t = Number(record.t);
|
|
59
|
+
const equity = Number(record.equity);
|
|
60
|
+
if (!Number.isFinite(t) || !Number.isFinite(equity)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
samples.push({ t, equity });
|
|
64
|
+
}
|
|
65
|
+
return samples;
|
|
66
|
+
}
|
|
67
|
+
function compressEngineBacktestReturnCurve(input) {
|
|
68
|
+
if (!Number.isFinite(input.initialEquity) || input.initialEquity <= 0) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const samples = readEquitySamples(input.equityCurve);
|
|
72
|
+
if (samples.length === 0) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const points = samples.map((sample) => [
|
|
76
|
+
sample.t,
|
|
77
|
+
roundCumulativeReturn(sample.equity / input.initialEquity - 1),
|
|
78
|
+
]);
|
|
79
|
+
return downsampleEvenlySpacedPoints(points, input.maxPoints ?? exports.ENGINE_BACKTEST_RETURN_CURVE_MAX_POINTS);
|
|
80
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { ApiRequestOptions, ApiResponse } from "../http/client";
|
|
|
2
2
|
import { loadTokens } from "../keychain/store";
|
|
3
3
|
import { type ResolvePackageHooks } from "./resolve-packages";
|
|
4
4
|
import type { BacktestRunnerModule, BacktestWasmModule, EngineBacktestRunArgs, EngineBacktestRunSuccess } from "./types";
|
|
5
|
+
export { loadConfigValue } from "./load-config";
|
|
5
6
|
export interface EngineBacktestCliFlags {
|
|
6
7
|
readonly profile?: string;
|
|
7
8
|
readonly format: "json" | "jsonl" | "text";
|
|
@@ -25,10 +26,6 @@ export interface EngineBacktestRunDeps {
|
|
|
25
26
|
readonly writeLine?: (value: unknown) => void;
|
|
26
27
|
readonly resolveHooks?: ResolvePackageHooks;
|
|
27
28
|
}
|
|
28
|
-
export declare function loadConfigValue(raw: string, options?: {
|
|
29
|
-
readonly cwd?: string;
|
|
30
|
-
readonly readFile?: (path: string) => string;
|
|
31
|
-
}): unknown;
|
|
32
29
|
export declare function executeEngineBacktestRun(args: EngineBacktestRunArgs, flags: EngineBacktestCliFlags, env?: NodeJS.ProcessEnv, deps?: EngineBacktestRunDeps): Promise<EngineBacktestRunSuccess>;
|
|
33
30
|
export declare function engineBacktestHelpData(): {
|
|
34
31
|
readonly name: string;
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.loadConfigValue =
|
|
3
|
+
exports.loadConfigValue = void 0;
|
|
4
4
|
exports.executeEngineBacktestRun = executeEngineBacktestRun;
|
|
5
5
|
exports.engineBacktestHelpData = engineBacktestHelpData;
|
|
6
6
|
exports.cmdEngineBacktest = cmdEngineBacktest;
|
|
7
7
|
const node_crypto_1 = require("node:crypto");
|
|
8
|
-
const node_fs_1 = require("node:fs");
|
|
9
|
-
const node_path_1 = require("node:path");
|
|
10
8
|
const profiles_1 = require("../config/profiles");
|
|
11
9
|
const envelope_1 = require("../envelope");
|
|
12
10
|
const client_1 = require("../http/client");
|
|
13
11
|
const store_1 = require("../keychain/store");
|
|
14
12
|
const errors_1 = require("./errors");
|
|
13
|
+
const load_config_1 = require("./load-config");
|
|
15
14
|
const parse_args_1 = require("./parse-args");
|
|
15
|
+
const sweep_command_1 = require("./sweep-command");
|
|
16
16
|
const persist_1 = require("./persist");
|
|
17
17
|
const replay_timeframe_1 = require("./replay-timeframe");
|
|
18
18
|
const resolve_packages_1 = require("./resolve-packages");
|
|
19
|
+
var load_config_2 = require("./load-config");
|
|
20
|
+
Object.defineProperty(exports, "loadConfigValue", { enumerable: true, get: function () { return load_config_2.loadConfigValue; } });
|
|
19
21
|
const CREATE_EXPERIMENT_PATH = "/api/v1/engine-backtest/experiments";
|
|
20
22
|
const SUBSCRIPTION_PATH = "/api/v1/subscriptions/me";
|
|
21
23
|
const SUBSCRIPTION_TIERS = new Set([
|
|
@@ -26,44 +28,6 @@ const SUBSCRIPTION_TIERS = new Set([
|
|
|
26
28
|
function runsPath(experimentId) {
|
|
27
29
|
return `/api/v1/engine-backtest/experiments/${encodeURIComponent(experimentId)}/runs`;
|
|
28
30
|
}
|
|
29
|
-
function loadConfigValue(raw, options = {}) {
|
|
30
|
-
const cwd = options.cwd ?? process.cwd();
|
|
31
|
-
const read = options.readFile ?? ((p) => (0, node_fs_1.readFileSync)(p, "utf8"));
|
|
32
|
-
if (raw.startsWith("@")) {
|
|
33
|
-
const rel = raw.slice(1);
|
|
34
|
-
if (!rel) {
|
|
35
|
-
throw new errors_1.EngineBacktestError({
|
|
36
|
-
type: "usage",
|
|
37
|
-
subtype: "missing_config",
|
|
38
|
-
message: "--config @path is empty",
|
|
39
|
-
status: 400,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
const abs = (0, node_path_1.isAbsolute)(rel) ? rel : (0, node_path_1.resolve)(cwd, rel);
|
|
43
|
-
try {
|
|
44
|
-
return JSON.parse(read(abs));
|
|
45
|
-
}
|
|
46
|
-
catch (err) {
|
|
47
|
-
throw new errors_1.EngineBacktestError({
|
|
48
|
-
type: "usage",
|
|
49
|
-
subtype: "invalid_config",
|
|
50
|
-
message: `Cannot read --config file ${abs}: ${err instanceof Error ? err.message : String(err)}`,
|
|
51
|
-
status: 400,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
try {
|
|
56
|
-
return JSON.parse(raw);
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
throw new errors_1.EngineBacktestError({
|
|
60
|
-
type: "usage",
|
|
61
|
-
subtype: "invalid_config",
|
|
62
|
-
message: "--config must be JSON or @path-to.json",
|
|
63
|
-
status: 400,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
31
|
function extractErrorMessage(json, fallback) {
|
|
68
32
|
if (json && typeof json === "object") {
|
|
69
33
|
const o = json;
|
|
@@ -227,7 +191,7 @@ async function executeEngineBacktestRun(args, flags, env = process.env, deps = {
|
|
|
227
191
|
if (needsApi) {
|
|
228
192
|
requireAuth(profile, env, tokensFn);
|
|
229
193
|
}
|
|
230
|
-
const config = loadConfigValue(args.configRaw, {
|
|
194
|
+
const config = (0, load_config_1.loadConfigValue)(args.configRaw, {
|
|
231
195
|
cwd: deps.cwd,
|
|
232
196
|
readFile: deps.readFile,
|
|
233
197
|
});
|
|
@@ -433,6 +397,7 @@ async function executeEngineBacktestRun(args, flags, env = process.env, deps = {
|
|
|
433
397
|
exchangeId: exchange.id,
|
|
434
398
|
dataQualityMode: args.dataQualityMode,
|
|
435
399
|
engineVersion,
|
|
400
|
+
equityCurve: result.equityCurve,
|
|
436
401
|
});
|
|
437
402
|
const res = await postJson(api, profile, env, runsPath(experimentId), body, mintId);
|
|
438
403
|
persistedRunId = extractEntityId(res.json);
|
|
@@ -464,10 +429,13 @@ function engineBacktestHelpData() {
|
|
|
464
429
|
name: "engine-backtest",
|
|
465
430
|
usage: [
|
|
466
431
|
...parse_args_1.ENGINE_BACKTEST_RUN_USAGE,
|
|
432
|
+
...parse_args_1.ENGINE_BACKTEST_SWEEP_USAGE,
|
|
467
433
|
"Catalog CRUD (underscore domain): alphafox engine_backtest experiments list|create|...",
|
|
468
434
|
],
|
|
469
435
|
notes: [
|
|
470
|
-
"Local WASM run is hyphenated engine-backtest so it does not steal typed catalog engine_backtest.*",
|
|
436
|
+
"Local WASM run/sweep is hyphenated engine-backtest so it does not steal typed catalog engine_backtest.*",
|
|
437
|
+
"Sweep persist POSTs engine_backtest.experiments.byId.sweeps.create once after local completion; --no-persist writes nothing",
|
|
438
|
+
"sweeps.create is write (not high-risk-write); sweeps.byId.delete is high-risk-write and needs --yes",
|
|
471
439
|
"runs.create is write (not high-risk-write); --yes is not required",
|
|
472
440
|
"Do not pass --token; use alphafox auth login",
|
|
473
441
|
"--replay-timeframe defaults to 1m (min 1m). Indicator series still download their native plan timeframes.",
|
|
@@ -486,14 +454,17 @@ async function cmdEngineBacktest(args, flags, env = process.env, deps = {}) {
|
|
|
486
454
|
});
|
|
487
455
|
return 0;
|
|
488
456
|
}
|
|
489
|
-
if (sub !== "run") {
|
|
457
|
+
if (sub !== "run" && sub !== "sweep") {
|
|
490
458
|
(0, envelope_1.writeError)({
|
|
491
459
|
type: "usage",
|
|
492
460
|
subtype: "unknown_subcommand",
|
|
493
461
|
message: `Unknown engine-backtest subcommand "${sub}"`,
|
|
494
|
-
hint: "Local WASM: alphafox engine-backtest run. Catalog CRUD: alphafox engine_backtest experiments list",
|
|
462
|
+
hint: "Local WASM: alphafox engine-backtest run|sweep. Catalog CRUD: alphafox engine_backtest experiments list",
|
|
495
463
|
});
|
|
496
464
|
}
|
|
465
|
+
if (sub === "sweep") {
|
|
466
|
+
return await invokeSweep(rest, flags, env, deps);
|
|
467
|
+
}
|
|
497
468
|
let parsed;
|
|
498
469
|
try {
|
|
499
470
|
parsed = (0, parse_args_1.parseEngineBacktestRunArgs)(rest);
|
|
@@ -538,3 +509,48 @@ async function cmdEngineBacktest(args, flags, env = process.env, deps = {}) {
|
|
|
538
509
|
throw err;
|
|
539
510
|
}
|
|
540
511
|
}
|
|
512
|
+
async function invokeSweep(rest, flags, env, deps) {
|
|
513
|
+
let parsed;
|
|
514
|
+
try {
|
|
515
|
+
parsed = (0, parse_args_1.parseEngineBacktestSweepArgs)(rest);
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
if ((0, errors_1.isEngineBacktestError)(err)) {
|
|
519
|
+
(0, envelope_1.writeError)({
|
|
520
|
+
type: err.type,
|
|
521
|
+
subtype: err.subtype,
|
|
522
|
+
message: err.message,
|
|
523
|
+
hint: err.hint,
|
|
524
|
+
status: err.status,
|
|
525
|
+
details: err.details,
|
|
526
|
+
}, { exitCode: err.status === 401 || err.status === 403 ? 77 : undefined });
|
|
527
|
+
}
|
|
528
|
+
throw err;
|
|
529
|
+
}
|
|
530
|
+
if (parsed.help) {
|
|
531
|
+
(0, envelope_1.writeSuccess)(engineBacktestHelpData(), {
|
|
532
|
+
format: flags.format,
|
|
533
|
+
jq: flags.jq,
|
|
534
|
+
});
|
|
535
|
+
return 0;
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
const result = await (0, sweep_command_1.executeEngineBacktestSweep)(parsed, flags, env, deps);
|
|
539
|
+
(0, envelope_1.writeSuccess)(result, { format: flags.format, jq: flags.jq });
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
if ((0, errors_1.isEngineBacktestError)(err)) {
|
|
544
|
+
(0, envelope_1.writeError)({
|
|
545
|
+
type: err.type,
|
|
546
|
+
subtype: err.subtype,
|
|
547
|
+
message: err.message,
|
|
548
|
+
hint: err.hint,
|
|
549
|
+
status: err.status,
|
|
550
|
+
code: err.code,
|
|
551
|
+
details: err.details,
|
|
552
|
+
}, { exitCode: err.status === 401 || err.status === 403 ? 77 : undefined });
|
|
553
|
+
}
|
|
554
|
+
throw err;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EngineBacktestCliFlags, EngineBacktestRunDeps } from "./run-command";
|
|
2
|
+
import type { EngineBacktestSweepArgs, EngineBacktestSweepSuccess } from "./types";
|
|
3
|
+
/** WASM `runBacktestBatch` hard limit. Keep in sync with Engine. */
|
|
4
|
+
export declare const MAX_ENGINE_BACKTEST_BATCH_VARIANTS = 256;
|
|
5
|
+
/** Sweep chunk size: below the 256 cap so JSONL can refresh mid-search. */
|
|
6
|
+
export declare const SWEEP_WASM_BATCH_VARIANTS = 32;
|
|
7
|
+
export interface EngineBacktestSweepDeps extends EngineBacktestRunDeps {
|
|
8
|
+
readonly maxVariantsPerBatch?: number;
|
|
9
|
+
readonly now?: () => number;
|
|
10
|
+
}
|
|
11
|
+
export declare function cloneTapeBuffers(sourceBuffers: Readonly<Record<string, ArrayBuffer>>): Record<string, ArrayBuffer>;
|
|
12
|
+
export declare function splitBatchChunk<T>(chunk: readonly T[], maxVariants?: number): T[][];
|
|
13
|
+
export declare function executeEngineBacktestSweep(args: EngineBacktestSweepArgs, flags: EngineBacktestCliFlags, env?: NodeJS.ProcessEnv, deps?: EngineBacktestSweepDeps): Promise<EngineBacktestSweepSuccess>;
|