@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,60 @@
|
|
|
1
|
+
import type { EngineBacktestAttribution } from "./result-attribution";
|
|
2
|
+
export declare const ENGINE_BACKTEST_ACTIVITY_SCHEMA_VERSION: 1;
|
|
3
|
+
export declare const ENGINE_BACKTEST_ACTIVITY_MAX_UTF8_BYTES: number;
|
|
4
|
+
export declare const ENGINE_BACKTEST_ACTIVITY_MAX_FILLED_ORDERS = 2000;
|
|
5
|
+
export declare function utf8ByteLength(value: string): number;
|
|
6
|
+
export interface EngineBacktestOrder {
|
|
7
|
+
readonly clientOrderId: string;
|
|
8
|
+
readonly orderId: string;
|
|
9
|
+
readonly symbol: string;
|
|
10
|
+
readonly side: string;
|
|
11
|
+
readonly positionSide?: string;
|
|
12
|
+
readonly type: string;
|
|
13
|
+
readonly status: string;
|
|
14
|
+
readonly reduceOnly: boolean;
|
|
15
|
+
readonly price: number;
|
|
16
|
+
readonly contractAmount: number;
|
|
17
|
+
readonly filledQuantity: number;
|
|
18
|
+
readonly fee: number;
|
|
19
|
+
readonly timestamp: string;
|
|
20
|
+
readonly message?: string;
|
|
21
|
+
readonly executionReason?: "liquidation";
|
|
22
|
+
}
|
|
23
|
+
export interface EngineBacktestPosition {
|
|
24
|
+
readonly symbol: string;
|
|
25
|
+
readonly side: string;
|
|
26
|
+
readonly contracts: number;
|
|
27
|
+
readonly entryPrice: number;
|
|
28
|
+
readonly markPrice: number;
|
|
29
|
+
readonly unrealizedPnl: number;
|
|
30
|
+
readonly leverage: number;
|
|
31
|
+
}
|
|
32
|
+
export interface EngineBacktestAccountAdjustment {
|
|
33
|
+
readonly type: string;
|
|
34
|
+
readonly amount: number;
|
|
35
|
+
readonly timestamp: string;
|
|
36
|
+
}
|
|
37
|
+
export interface EngineBacktestActivity {
|
|
38
|
+
readonly schemaVersion: 1;
|
|
39
|
+
readonly attribution: EngineBacktestAttribution;
|
|
40
|
+
readonly openPositions: readonly EngineBacktestPosition[];
|
|
41
|
+
readonly accountAdjustments: readonly EngineBacktestAccountAdjustment[];
|
|
42
|
+
readonly filledOrders: readonly EngineBacktestOrder[];
|
|
43
|
+
readonly filledOrderTotal: number;
|
|
44
|
+
readonly truncated: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare function readActivityOrders(value: unknown): EngineBacktestOrder[];
|
|
47
|
+
export declare function readActivityPositions(value: unknown): EngineBacktestPosition[];
|
|
48
|
+
export declare function readActivityAdjustments(value: unknown): EngineBacktestAccountAdjustment[];
|
|
49
|
+
export declare function selectFilledOrderTail(orders: readonly EngineBacktestOrder[], limit?: number): {
|
|
50
|
+
readonly filledOrders: EngineBacktestOrder[];
|
|
51
|
+
readonly filledOrderTotal: number;
|
|
52
|
+
readonly truncated: boolean;
|
|
53
|
+
};
|
|
54
|
+
export declare function buildEngineBacktestActivityFromCompleted(input: {
|
|
55
|
+
readonly attribution: EngineBacktestAttribution;
|
|
56
|
+
readonly orders: readonly EngineBacktestOrder[];
|
|
57
|
+
readonly openPositions: readonly EngineBacktestPosition[];
|
|
58
|
+
readonly accountAdjustments?: readonly EngineBacktestAccountAdjustment[];
|
|
59
|
+
readonly maxFilledOrders?: number;
|
|
60
|
+
}): EngineBacktestActivity;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ENGINE_BACKTEST_ACTIVITY_MAX_FILLED_ORDERS = exports.ENGINE_BACKTEST_ACTIVITY_MAX_UTF8_BYTES = exports.ENGINE_BACKTEST_ACTIVITY_SCHEMA_VERSION = void 0;
|
|
4
|
+
exports.utf8ByteLength = utf8ByteLength;
|
|
5
|
+
exports.readActivityOrders = readActivityOrders;
|
|
6
|
+
exports.readActivityPositions = readActivityPositions;
|
|
7
|
+
exports.readActivityAdjustments = readActivityAdjustments;
|
|
8
|
+
exports.selectFilledOrderTail = selectFilledOrderTail;
|
|
9
|
+
exports.buildEngineBacktestActivityFromCompleted = buildEngineBacktestActivityFromCompleted;
|
|
10
|
+
const errors_1 = require("./errors");
|
|
11
|
+
exports.ENGINE_BACKTEST_ACTIVITY_SCHEMA_VERSION = 1;
|
|
12
|
+
exports.ENGINE_BACKTEST_ACTIVITY_MAX_UTF8_BYTES = 1024 * 1024;
|
|
13
|
+
exports.ENGINE_BACKTEST_ACTIVITY_MAX_FILLED_ORDERS = 2_000;
|
|
14
|
+
function utf8ByteLength(value) {
|
|
15
|
+
return new TextEncoder().encode(value).length;
|
|
16
|
+
}
|
|
17
|
+
function readActivityOrders(value) {
|
|
18
|
+
if (value === undefined || value === null)
|
|
19
|
+
return [];
|
|
20
|
+
if (!Array.isArray(value)) {
|
|
21
|
+
throw invalidActivityField("orders", "must be an array");
|
|
22
|
+
}
|
|
23
|
+
return value.map((item, index) => readOrder(item, index));
|
|
24
|
+
}
|
|
25
|
+
function readActivityPositions(value) {
|
|
26
|
+
if (value === undefined || value === null)
|
|
27
|
+
return [];
|
|
28
|
+
if (!Array.isArray(value)) {
|
|
29
|
+
throw invalidActivityField("openPositions", "must be an array");
|
|
30
|
+
}
|
|
31
|
+
return value.map((item, index) => readPosition(item, index));
|
|
32
|
+
}
|
|
33
|
+
function readActivityAdjustments(value) {
|
|
34
|
+
if (value === undefined || value === null)
|
|
35
|
+
return [];
|
|
36
|
+
if (!Array.isArray(value)) {
|
|
37
|
+
throw invalidActivityField("accountAdjustments", "must be an array");
|
|
38
|
+
}
|
|
39
|
+
return value.map((item, index) => readAdjustment(item, index));
|
|
40
|
+
}
|
|
41
|
+
function selectFilledOrderTail(orders, limit = exports.ENGINE_BACKTEST_ACTIVITY_MAX_FILLED_ORDERS) {
|
|
42
|
+
const filled = orders
|
|
43
|
+
.map((order, index) => ({ order, index }))
|
|
44
|
+
.filter((entry) => entry.order.filledQuantity > 0)
|
|
45
|
+
.sort((left, right) => {
|
|
46
|
+
const byTimestamp = Date.parse(left.order.timestamp) - Date.parse(right.order.timestamp);
|
|
47
|
+
if (byTimestamp !== 0)
|
|
48
|
+
return byTimestamp;
|
|
49
|
+
return left.index - right.index;
|
|
50
|
+
})
|
|
51
|
+
.map((entry) => entry.order);
|
|
52
|
+
const filledOrderTotal = filled.length;
|
|
53
|
+
const truncated = filledOrderTotal > limit;
|
|
54
|
+
return {
|
|
55
|
+
filledOrders: truncated ? filled.slice(-limit) : [...filled],
|
|
56
|
+
filledOrderTotal,
|
|
57
|
+
truncated,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function buildEngineBacktestActivityFromCompleted(input) {
|
|
61
|
+
const countCap = input.maxFilledOrders ?? exports.ENGINE_BACKTEST_ACTIVITY_MAX_FILLED_ORDERS;
|
|
62
|
+
const tail = selectFilledOrderTail(input.orders, countCap);
|
|
63
|
+
return fitActivityToUtf8Budget({
|
|
64
|
+
schemaVersion: exports.ENGINE_BACKTEST_ACTIVITY_SCHEMA_VERSION,
|
|
65
|
+
attribution: {
|
|
66
|
+
...input.attribution,
|
|
67
|
+
symbols: [...input.attribution.symbols],
|
|
68
|
+
},
|
|
69
|
+
openPositions: [...input.openPositions],
|
|
70
|
+
accountAdjustments: [...(input.accountAdjustments ?? [])],
|
|
71
|
+
filledOrders: tail.filledOrders.map(toPersistedFilledOrder),
|
|
72
|
+
filledOrderTotal: tail.filledOrderTotal,
|
|
73
|
+
truncated: tail.truncated,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function fitActivityToUtf8Budget(activity) {
|
|
77
|
+
if (activityUtf8ByteLength(activity) <= exports.ENGINE_BACKTEST_ACTIVITY_MAX_UTF8_BYTES) {
|
|
78
|
+
return activity;
|
|
79
|
+
}
|
|
80
|
+
let low = 0;
|
|
81
|
+
let high = activity.filledOrders.length;
|
|
82
|
+
let fitted = withFilledOrderTail(activity, 0);
|
|
83
|
+
while (low <= high) {
|
|
84
|
+
const mid = Math.floor((low + high) / 2);
|
|
85
|
+
const candidate = withFilledOrderTail(activity, mid);
|
|
86
|
+
if (activityUtf8ByteLength(candidate) <= exports.ENGINE_BACKTEST_ACTIVITY_MAX_UTF8_BYTES) {
|
|
87
|
+
fitted = candidate;
|
|
88
|
+
low = mid + 1;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
high = mid - 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (activityUtf8ByteLength(fitted) > exports.ENGINE_BACKTEST_ACTIVITY_MAX_UTF8_BYTES) {
|
|
95
|
+
throw new errors_1.EngineBacktestError({
|
|
96
|
+
type: "runtime",
|
|
97
|
+
subtype: "activity_too_large",
|
|
98
|
+
message: `Activity exceeds ${exports.ENGINE_BACKTEST_ACTIVITY_MAX_UTF8_BYTES} UTF-8 bytes.`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return fitted;
|
|
102
|
+
}
|
|
103
|
+
function withFilledOrderTail(activity, keepCount) {
|
|
104
|
+
const filledOrders = keepCount <= 0 ? [] : activity.filledOrders.slice(-keepCount);
|
|
105
|
+
return {
|
|
106
|
+
...activity,
|
|
107
|
+
filledOrders,
|
|
108
|
+
truncated: activity.filledOrderTotal > filledOrders.length,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function activityUtf8ByteLength(activity) {
|
|
112
|
+
return utf8ByteLength(JSON.stringify(activity));
|
|
113
|
+
}
|
|
114
|
+
function toPersistedFilledOrder(order) {
|
|
115
|
+
return {
|
|
116
|
+
clientOrderId: order.clientOrderId,
|
|
117
|
+
orderId: order.orderId,
|
|
118
|
+
symbol: order.symbol,
|
|
119
|
+
side: order.side,
|
|
120
|
+
...(order.positionSide ? { positionSide: order.positionSide } : {}),
|
|
121
|
+
type: order.type,
|
|
122
|
+
status: order.status,
|
|
123
|
+
reduceOnly: order.reduceOnly,
|
|
124
|
+
price: order.price,
|
|
125
|
+
contractAmount: order.contractAmount,
|
|
126
|
+
filledQuantity: order.filledQuantity,
|
|
127
|
+
fee: order.fee,
|
|
128
|
+
timestamp: order.timestamp,
|
|
129
|
+
...(order.message?.trim() ? { message: order.message } : {}),
|
|
130
|
+
...(order.executionReason === "liquidation"
|
|
131
|
+
? { executionReason: "liquidation" }
|
|
132
|
+
: {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function readOrder(value, index) {
|
|
136
|
+
const record = asRecord(value, `orders[${index}]`);
|
|
137
|
+
const executionReason = optionalString(record.executionReason);
|
|
138
|
+
return {
|
|
139
|
+
clientOrderId: requiredString(record.clientOrderId, `orders[${index}].clientOrderId`),
|
|
140
|
+
orderId: requiredString(record.orderId, `orders[${index}].orderId`),
|
|
141
|
+
symbol: requiredString(record.symbol, `orders[${index}].symbol`),
|
|
142
|
+
side: requiredString(record.side, `orders[${index}].side`),
|
|
143
|
+
...(optionalString(record.positionSide)
|
|
144
|
+
? { positionSide: optionalString(record.positionSide) }
|
|
145
|
+
: {}),
|
|
146
|
+
type: requiredString(record.type, `orders[${index}].type`),
|
|
147
|
+
status: requiredString(record.status, `orders[${index}].status`),
|
|
148
|
+
reduceOnly: requiredBoolean(record.reduceOnly, `orders[${index}].reduceOnly`),
|
|
149
|
+
price: requiredFinite(record.price, `orders[${index}].price`),
|
|
150
|
+
contractAmount: requiredFinite(record.contractAmount, `orders[${index}].contractAmount`),
|
|
151
|
+
filledQuantity: requiredFinite(record.filledQuantity, `orders[${index}].filledQuantity`),
|
|
152
|
+
fee: requiredFinite(record.fee, `orders[${index}].fee`),
|
|
153
|
+
timestamp: requiredString(record.timestamp, `orders[${index}].timestamp`),
|
|
154
|
+
...(optionalString(record.message)
|
|
155
|
+
? { message: optionalString(record.message) }
|
|
156
|
+
: {}),
|
|
157
|
+
...(executionReason === "liquidation"
|
|
158
|
+
? { executionReason: "liquidation" }
|
|
159
|
+
: {}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function readPosition(value, index) {
|
|
163
|
+
const record = asRecord(value, `openPositions[${index}]`);
|
|
164
|
+
return {
|
|
165
|
+
symbol: requiredString(record.symbol, `openPositions[${index}].symbol`),
|
|
166
|
+
side: requiredString(record.side, `openPositions[${index}].side`),
|
|
167
|
+
contracts: requiredFinite(record.contracts, `openPositions[${index}].contracts`),
|
|
168
|
+
entryPrice: requiredFinite(record.entryPrice, `openPositions[${index}].entryPrice`),
|
|
169
|
+
markPrice: requiredFinite(record.markPrice, `openPositions[${index}].markPrice`),
|
|
170
|
+
unrealizedPnl: requiredFinite(record.unrealizedPnl, `openPositions[${index}].unrealizedPnl`),
|
|
171
|
+
leverage: requiredFinite(record.leverage, `openPositions[${index}].leverage`),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function readAdjustment(value, index) {
|
|
175
|
+
const record = asRecord(value, `accountAdjustments[${index}]`);
|
|
176
|
+
return {
|
|
177
|
+
type: requiredString(record.type, `accountAdjustments[${index}].type`),
|
|
178
|
+
amount: requiredFinite(record.amount, `accountAdjustments[${index}].amount`),
|
|
179
|
+
timestamp: requiredString(record.timestamp, `accountAdjustments[${index}].timestamp`),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function asRecord(value, path) {
|
|
183
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
184
|
+
throw invalidActivityField(path, "must be an object");
|
|
185
|
+
}
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
function requiredString(value, path) {
|
|
189
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
190
|
+
throw invalidActivityField(path, "must be a non-empty string");
|
|
191
|
+
}
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
function optionalString(value) {
|
|
195
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
196
|
+
}
|
|
197
|
+
function requiredBoolean(value, path) {
|
|
198
|
+
if (typeof value !== "boolean") {
|
|
199
|
+
throw invalidActivityField(path, "must be a boolean");
|
|
200
|
+
}
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
function requiredFinite(value, path) {
|
|
204
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
205
|
+
throw invalidActivityField(path, "must be a finite number");
|
|
206
|
+
}
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
function invalidActivityField(path, reason) {
|
|
210
|
+
return new errors_1.EngineBacktestError({
|
|
211
|
+
type: "runtime",
|
|
212
|
+
subtype: "persist_invalid_activity",
|
|
213
|
+
message: `Cannot persist activity ${path}: ${reason}`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WASM runtimes older than the firstOrderAmount rename still size DCA
|
|
3
|
+
* entries from initialMarginPercent / fixedInitialNotional and default to
|
|
4
|
+
* 2% when those keys are missing. Mirror the active mode onto the legacy
|
|
5
|
+
* keys so both runtimes honor the user's first-order size.
|
|
6
|
+
*/
|
|
7
|
+
export declare function projectDcaFirstOrderAmountForLegacyRuntime(config: unknown): unknown;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.projectDcaFirstOrderAmountForLegacyRuntime = projectDcaFirstOrderAmountForLegacyRuntime;
|
|
4
|
+
const DECISION_LOGIC_KEYS = [
|
|
5
|
+
"decisionLogic",
|
|
6
|
+
"longDecisionLogic",
|
|
7
|
+
"shortDecisionLogic",
|
|
8
|
+
];
|
|
9
|
+
const FIRST_ORDER_AMOUNT_TYPE_FIXED = "fixedAmount";
|
|
10
|
+
/**
|
|
11
|
+
* WASM runtimes older than the firstOrderAmount rename still size DCA
|
|
12
|
+
* entries from initialMarginPercent / fixedInitialNotional and default to
|
|
13
|
+
* 2% when those keys are missing. Mirror the active mode onto the legacy
|
|
14
|
+
* keys so both runtimes honor the user's first-order size.
|
|
15
|
+
*/
|
|
16
|
+
function projectDcaFirstOrderAmountForLegacyRuntime(config) {
|
|
17
|
+
const record = asRecord(config);
|
|
18
|
+
if (!record) {
|
|
19
|
+
return config;
|
|
20
|
+
}
|
|
21
|
+
const strategy = asRecord(record.strategy);
|
|
22
|
+
if (!strategy) {
|
|
23
|
+
return config;
|
|
24
|
+
}
|
|
25
|
+
let changed = false;
|
|
26
|
+
const nextStrategy = { ...strategy };
|
|
27
|
+
for (const key of DECISION_LOGIC_KEYS) {
|
|
28
|
+
const logic = asRecord(strategy[key]);
|
|
29
|
+
const params = logic ? asRecord(logic.params) : null;
|
|
30
|
+
if (!logic || !params) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const projected = projectDecisionLogicParams(params);
|
|
34
|
+
if (projected === params) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
nextStrategy[key] = { ...logic, params: projected };
|
|
38
|
+
changed = true;
|
|
39
|
+
}
|
|
40
|
+
if (!changed) {
|
|
41
|
+
return config;
|
|
42
|
+
}
|
|
43
|
+
return { ...record, strategy: nextStrategy };
|
|
44
|
+
}
|
|
45
|
+
function projectDecisionLogicParams(params) {
|
|
46
|
+
const amount = toFiniteNumber(params.firstOrderAmount);
|
|
47
|
+
if (amount === undefined || amount <= 0) {
|
|
48
|
+
return params;
|
|
49
|
+
}
|
|
50
|
+
const isFixed = params.firstOrderAmountType === FIRST_ORDER_AMOUNT_TYPE_FIXED;
|
|
51
|
+
const next = { ...params };
|
|
52
|
+
if (isFixed) {
|
|
53
|
+
next.fixedInitialNotional = amount;
|
|
54
|
+
delete next.initialMarginPercent;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
next.initialMarginPercent = amount;
|
|
58
|
+
delete next.fixedInitialNotional;
|
|
59
|
+
}
|
|
60
|
+
return next;
|
|
61
|
+
}
|
|
62
|
+
function asRecord(value) {
|
|
63
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
64
|
+
? value
|
|
65
|
+
: null;
|
|
66
|
+
}
|
|
67
|
+
function toFiniteNumber(value) {
|
|
68
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
69
|
+
? value
|
|
70
|
+
: undefined;
|
|
71
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
type LoadConfigOptions = {
|
|
2
2
|
readonly cwd?: string;
|
|
3
3
|
readonly readFile?: (path: string) => string;
|
|
4
|
-
}
|
|
4
|
+
};
|
|
5
|
+
export declare function loadConfigValue(raw: string, options?: LoadConfigOptions): unknown;
|
|
6
|
+
export declare function loadEngineBacktestConfig(raw: string, options?: LoadConfigOptions): unknown;
|
|
7
|
+
export declare function assertEngineBacktestConfig(value: unknown): void;
|
|
8
|
+
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.loadConfigValue = loadConfigValue;
|
|
4
|
+
exports.loadEngineBacktestConfig = loadEngineBacktestConfig;
|
|
5
|
+
exports.assertEngineBacktestConfig = assertEngineBacktestConfig;
|
|
4
6
|
const node_fs_1 = require("node:fs");
|
|
5
7
|
const node_path_1 = require("node:path");
|
|
6
8
|
const errors_1 = require("./errors");
|
|
@@ -42,3 +44,24 @@ function loadConfigValue(raw, options = {}) {
|
|
|
42
44
|
});
|
|
43
45
|
}
|
|
44
46
|
}
|
|
47
|
+
function loadEngineBacktestConfig(raw, options = {}) {
|
|
48
|
+
const value = loadConfigValue(raw, options);
|
|
49
|
+
assertEngineBacktestConfig(value);
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function assertEngineBacktestConfig(value) {
|
|
53
|
+
if (!isPlainObject(value) || !isPlainObject(value.config)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
throw new errors_1.EngineBacktestError({
|
|
57
|
+
type: "usage",
|
|
58
|
+
subtype: "validate_config_envelope",
|
|
59
|
+
message: "--config looks like a validate_config HTTP body; engine-backtest needs the inner trader config",
|
|
60
|
+
hint: "Pass { common, strategy }. Extract the `config` field; do not send { configSchemaVersion, config }.",
|
|
61
|
+
status: 400,
|
|
62
|
+
details: { keys: Object.keys(value) },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function isPlainObject(value) {
|
|
66
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
67
|
+
}
|
|
@@ -25,6 +25,9 @@ export declare function buildCreateRunRequest(input: {
|
|
|
25
25
|
readonly engineVersion: string;
|
|
26
26
|
readonly equityCurve?: unknown;
|
|
27
27
|
readonly coverageIssues?: readonly TapeCoverageIssue[];
|
|
28
|
+
readonly orders?: unknown;
|
|
29
|
+
readonly openPositions?: unknown;
|
|
30
|
+
readonly accountAdjustments?: unknown;
|
|
28
31
|
}): CreateRunRequestBody;
|
|
29
32
|
/**
|
|
30
33
|
* Experiment detail URL. Locale prefix (`/zh`, `/en`) is optional — web
|
|
@@ -12,9 +12,11 @@ exports.sweepsPath = sweepsPath;
|
|
|
12
12
|
exports.buildSweepBaseSnapshot = buildSweepBaseSnapshot;
|
|
13
13
|
exports.buildCreateSweepRequest = buildCreateSweepRequest;
|
|
14
14
|
exports.assertSweepCreateRequestSize = assertSweepCreateRequestSize;
|
|
15
|
+
const activity_1 = require("./activity");
|
|
16
|
+
const coverage_notice_1 = require("./coverage-notice");
|
|
15
17
|
const errors_1 = require("./errors");
|
|
18
|
+
const result_attribution_1 = require("./result-attribution");
|
|
16
19
|
const return_curve_1 = require("./return-curve");
|
|
17
|
-
const coverage_notice_1 = require("./coverage-notice");
|
|
18
20
|
/** Aligned with `@alphafoxai/backtest-runner` DEFAULT_EXECUTION_MODEL. */
|
|
19
21
|
exports.DEFAULT_EXECUTION_MODEL = {
|
|
20
22
|
pricePath: "ohlc_path_4",
|
|
@@ -84,6 +86,24 @@ function buildCreateRunRequest(input) {
|
|
|
84
86
|
initialEquity: snapshot.initialEquity,
|
|
85
87
|
equityCurve: input.equityCurve,
|
|
86
88
|
});
|
|
89
|
+
const orders = (0, activity_1.readActivityOrders)(input.orders);
|
|
90
|
+
const openPositions = (0, activity_1.readActivityPositions)(input.openPositions);
|
|
91
|
+
const accountAdjustments = (0, activity_1.readActivityAdjustments)(input.accountAdjustments);
|
|
92
|
+
const activity = (0, activity_1.buildEngineBacktestActivityFromCompleted)({
|
|
93
|
+
attribution: (0, result_attribution_1.attributeEngineBacktestResult)({
|
|
94
|
+
result: {
|
|
95
|
+
metrics: input.metrics,
|
|
96
|
+
orders,
|
|
97
|
+
openPositions,
|
|
98
|
+
accountAdjustments,
|
|
99
|
+
},
|
|
100
|
+
markets: scenario.tape.markets,
|
|
101
|
+
symbols,
|
|
102
|
+
}),
|
|
103
|
+
orders,
|
|
104
|
+
openPositions,
|
|
105
|
+
accountAdjustments,
|
|
106
|
+
});
|
|
87
107
|
return {
|
|
88
108
|
clientRunId: input.clientRunId,
|
|
89
109
|
snapshot,
|
|
@@ -91,6 +111,7 @@ function buildCreateRunRequest(input) {
|
|
|
91
111
|
engineVersion,
|
|
92
112
|
configSchemaVersion: snapshot.configSchemaVersion,
|
|
93
113
|
...(returnCurve ? { returnCurve } : {}),
|
|
114
|
+
activity,
|
|
94
115
|
};
|
|
95
116
|
}
|
|
96
117
|
/**
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BacktestClientLike, EngineBacktestResult, EngineBacktestTapeInput, EnginePreparedTape } from "./types";
|
|
2
|
+
/** Matches Web `DEFAULT_TAPE_SERIES_CONCURRENCY`. */
|
|
3
|
+
export declare const ENGINE_BACKTEST_TAPE_SERIES_CONCURRENCY = 8;
|
|
4
|
+
export declare function cloneTapeBuffers(sourceBuffers: Readonly<Record<string, ArrayBuffer>>): Record<string, ArrayBuffer>;
|
|
5
|
+
export declare function requirePreparedTapeClient(client: BacktestClientLike): void;
|
|
6
|
+
export declare function prepareWorkerTape(client: BacktestClientLike, tape: EngineBacktestTapeInput, buffers: Readonly<Record<string, ArrayBuffer>>): Promise<EnginePreparedTape>;
|
|
7
|
+
export declare function releaseWorkerTape(client: BacktestClientLike, handle: string | undefined): Promise<void>;
|
|
8
|
+
export declare function requireCompletedPreparedRun(result: EngineBacktestResult): EngineBacktestResult;
|
|
9
|
+
export declare function throwIfPreparedTapeErrors(errors: EngineBacktestResult["errors"] | undefined, details?: unknown): void;
|
|
10
|
+
export declare function mapPreparedTapeError(error: unknown): never;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ENGINE_BACKTEST_TAPE_SERIES_CONCURRENCY = void 0;
|
|
4
|
+
exports.cloneTapeBuffers = cloneTapeBuffers;
|
|
5
|
+
exports.requirePreparedTapeClient = requirePreparedTapeClient;
|
|
6
|
+
exports.prepareWorkerTape = prepareWorkerTape;
|
|
7
|
+
exports.releaseWorkerTape = releaseWorkerTape;
|
|
8
|
+
exports.requireCompletedPreparedRun = requireCompletedPreparedRun;
|
|
9
|
+
exports.throwIfPreparedTapeErrors = throwIfPreparedTapeErrors;
|
|
10
|
+
exports.mapPreparedTapeError = mapPreparedTapeError;
|
|
11
|
+
const errors_1 = require("./errors");
|
|
12
|
+
/** Matches Web `DEFAULT_TAPE_SERIES_CONCURRENCY`. */
|
|
13
|
+
exports.ENGINE_BACKTEST_TAPE_SERIES_CONCURRENCY = 8;
|
|
14
|
+
const PREPARED_TAPE_METHODS = [
|
|
15
|
+
"prepareTape",
|
|
16
|
+
"runPreparedBacktest",
|
|
17
|
+
"runPreparedBacktestBatch",
|
|
18
|
+
"releaseTape",
|
|
19
|
+
];
|
|
20
|
+
const PREPARED_TAPE_ERROR_CODES = new Set([
|
|
21
|
+
"prepared_tape_not_found",
|
|
22
|
+
"prepared_tape_mismatch",
|
|
23
|
+
]);
|
|
24
|
+
function cloneTapeBuffers(sourceBuffers) {
|
|
25
|
+
return Object.fromEntries(Object.entries(sourceBuffers).map(([key, buffer]) => [key, buffer.slice(0)]));
|
|
26
|
+
}
|
|
27
|
+
function requirePreparedTapeClient(client) {
|
|
28
|
+
for (const name of PREPARED_TAPE_METHODS) {
|
|
29
|
+
if (typeof client[name] !== "function") {
|
|
30
|
+
throw new errors_1.EngineBacktestError({
|
|
31
|
+
type: "runtime",
|
|
32
|
+
subtype: "prepared_tape_unavailable",
|
|
33
|
+
message: `Backtest runtime is missing ${name}; one-shot runBacktest is not used.`,
|
|
34
|
+
hint: "Use an Engine WASM host that exports prepareTape, runPreparedBacktest, runPreparedBacktestBatch, and releaseTape.",
|
|
35
|
+
details: { missing: name },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function prepareWorkerTape(client, tape, buffers) {
|
|
41
|
+
requirePreparedTapeClient(client);
|
|
42
|
+
const prepared = await client.prepareTape(tape, cloneTapeBuffers(buffers));
|
|
43
|
+
const failure = preparedTapeFailure(prepared);
|
|
44
|
+
if (failure) {
|
|
45
|
+
throw preparedTapeError(failure.code, failure.message, prepared);
|
|
46
|
+
}
|
|
47
|
+
if (!prepared ||
|
|
48
|
+
typeof prepared.handle !== "string" ||
|
|
49
|
+
prepared.handle.trim() === "") {
|
|
50
|
+
throw new errors_1.EngineBacktestError({
|
|
51
|
+
type: "runtime",
|
|
52
|
+
subtype: "prepared_tape_unavailable",
|
|
53
|
+
message: "prepareTape did not return a worker-local handle.",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return prepared;
|
|
57
|
+
}
|
|
58
|
+
async function releaseWorkerTape(client, handle) {
|
|
59
|
+
if (!handle)
|
|
60
|
+
return;
|
|
61
|
+
try {
|
|
62
|
+
await client.releaseTape(handle);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (preparedTapeCodeFromUnknown(error) === "prepared_tape_not_found") {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
throw mapPreparedTapeError(error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function requireCompletedPreparedRun(result) {
|
|
72
|
+
throwIfPreparedTapeErrors(result.errors, result);
|
|
73
|
+
if (result.status === "failed") {
|
|
74
|
+
throw new errors_1.EngineBacktestError({
|
|
75
|
+
type: "runtime",
|
|
76
|
+
subtype: "backtest_failed",
|
|
77
|
+
message: "runPreparedBacktest returned status=failed",
|
|
78
|
+
details: { errors: result.errors, runId: result.runId },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
function throwIfPreparedTapeErrors(errors, details) {
|
|
84
|
+
const failure = preparedTapeFailure({ errors });
|
|
85
|
+
if (!failure)
|
|
86
|
+
return;
|
|
87
|
+
throw preparedTapeError(failure.code, failure.message, details);
|
|
88
|
+
}
|
|
89
|
+
function mapPreparedTapeError(error) {
|
|
90
|
+
if (error instanceof errors_1.EngineBacktestError)
|
|
91
|
+
throw error;
|
|
92
|
+
const code = preparedTapeCodeFromUnknown(error);
|
|
93
|
+
if (code) {
|
|
94
|
+
throw preparedTapeError(code, error instanceof Error ? error.message : String(error), error);
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
function preparedTapeFailure(value) {
|
|
99
|
+
if (!value || typeof value !== "object")
|
|
100
|
+
return undefined;
|
|
101
|
+
const record = value;
|
|
102
|
+
const code = preparedTapeCodeFromErrors(record.errors);
|
|
103
|
+
if (!code)
|
|
104
|
+
return undefined;
|
|
105
|
+
return {
|
|
106
|
+
code,
|
|
107
|
+
message: record.errors?.[0]?.message?.trim() || code,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function preparedTapeCodeFromErrors(errors) {
|
|
111
|
+
const code = errors?.[0]?.code;
|
|
112
|
+
return typeof code === "string" && PREPARED_TAPE_ERROR_CODES.has(code)
|
|
113
|
+
? code
|
|
114
|
+
: undefined;
|
|
115
|
+
}
|
|
116
|
+
function preparedTapeCodeFromUnknown(error) {
|
|
117
|
+
if (error instanceof errors_1.EngineBacktestError && error.subtype) {
|
|
118
|
+
return PREPARED_TAPE_ERROR_CODES.has(error.subtype)
|
|
119
|
+
? error.subtype
|
|
120
|
+
: undefined;
|
|
121
|
+
}
|
|
122
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
+
for (const code of PREPARED_TAPE_ERROR_CODES) {
|
|
124
|
+
if (message.includes(code))
|
|
125
|
+
return code;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
function preparedTapeError(code, message, details) {
|
|
130
|
+
return new errors_1.EngineBacktestError({
|
|
131
|
+
type: "runtime",
|
|
132
|
+
subtype: code,
|
|
133
|
+
message,
|
|
134
|
+
details,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { EngineBacktestAccountAdjustment, EngineBacktestOrder, EngineBacktestPosition } from "./activity";
|
|
2
|
+
import type { EngineBacktestMetrics } from "./types";
|
|
3
|
+
export interface EngineBacktestSymbolAttribution {
|
|
4
|
+
readonly symbol: string;
|
|
5
|
+
readonly realizedPnl: number;
|
|
6
|
+
readonly liquidationRealizedPnl: number;
|
|
7
|
+
readonly unrealizedPnl: number;
|
|
8
|
+
readonly feesPaid: number;
|
|
9
|
+
readonly netPnl: number;
|
|
10
|
+
readonly orderCount: number;
|
|
11
|
+
readonly filledOrderCount: number;
|
|
12
|
+
readonly canceledOrderCount: number;
|
|
13
|
+
readonly tradeCount: number;
|
|
14
|
+
readonly winningTrades: number;
|
|
15
|
+
readonly losingTrades: number;
|
|
16
|
+
readonly winRatePct: number;
|
|
17
|
+
}
|
|
18
|
+
export interface EngineBacktestAttribution {
|
|
19
|
+
readonly symbols: readonly EngineBacktestSymbolAttribution[];
|
|
20
|
+
readonly attributedNetPnl: number;
|
|
21
|
+
readonly accountAdjustmentPnl: number;
|
|
22
|
+
readonly residualPnl: number;
|
|
23
|
+
readonly reconciliationTolerance: number;
|
|
24
|
+
readonly reconciled: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function attributeEngineBacktestResult(input: {
|
|
27
|
+
readonly result: {
|
|
28
|
+
readonly metrics: Pick<EngineBacktestMetrics, "netPnl">;
|
|
29
|
+
readonly orders: readonly EngineBacktestOrder[];
|
|
30
|
+
readonly openPositions: readonly EngineBacktestPosition[];
|
|
31
|
+
readonly accountAdjustments?: readonly EngineBacktestAccountAdjustment[];
|
|
32
|
+
};
|
|
33
|
+
readonly markets: Readonly<Record<string, unknown>>;
|
|
34
|
+
readonly symbols?: readonly string[];
|
|
35
|
+
}): EngineBacktestAttribution;
|