@tradejs/node 2.0.17 → 2.0.19
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/ai.js +4763 -168
- package/dist/ai.mjs +2 -3
- package/dist/backtest.js +4018 -1372
- package/dist/backtest.mjs +537 -872
- package/dist/chunk-4AVFJMQL.mjs +6054 -0
- package/dist/chunk-Y6FXYEAI.mjs +10 -0
- package/dist/cli.js +5537 -6403
- package/dist/cli.mjs +5 -11
- package/dist/connectors.mjs +1 -1
- package/dist/constants.mjs +1 -1
- package/dist/pine.mjs +122 -13
- package/dist/registry.js +5732 -48
- package/dist/registry.mjs +2 -2
- package/dist/strategies.d.mts +2 -2
- package/dist/strategies.d.ts +2 -2
- package/dist/strategies.js +2296 -7843
- package/dist/strategies.mjs +46 -2547
- package/package.json +6 -4
- package/dist/chunk-3BP3ETAD.mjs +0 -2134
- package/dist/chunk-GPR56UYQ.mjs +0 -5518
- package/dist/chunk-IQKMII6L.mjs +0 -137
- package/dist/chunk-QVSMINLG.mjs +0 -246
- package/dist/chunk-UV2HVMKZ.mjs +0 -41
- package/dist/chunk-WK5EUCX5.mjs +0 -1191
package/dist/strategies.mjs
CHANGED
|
@@ -1,27 +1,6 @@
|
|
|
1
|
+
import "./chunk-5YNMSWL3.mjs";
|
|
1
2
|
import {
|
|
2
3
|
BINANCE_BREADTH_UNIVERSE_KEYS,
|
|
3
|
-
buildBinanceBreadthUniverseSnapshot,
|
|
4
|
-
buildMlPayload,
|
|
5
|
-
enrichSignalWithBinanceMarketContext,
|
|
6
|
-
enrichSignalWithCoinMarketCapContext,
|
|
7
|
-
enrichSignalWithHyperliquidWhaleContext,
|
|
8
|
-
enrichSignalWithMarketContextStages,
|
|
9
|
-
getBinanceBreadthUniverseSnapshot,
|
|
10
|
-
getBinanceBreadthUniverses,
|
|
11
|
-
getHyperliquidPerpSymbols,
|
|
12
|
-
getHyperliquidPerpUniverseSnapshot,
|
|
13
|
-
getHyperliquidWhaleAddresses,
|
|
14
|
-
getHyperliquidWhaleRegistrySnapshot,
|
|
15
|
-
getPrimaryBinanceBreadthUniverse,
|
|
16
|
-
isTrackedHyperliquidPerp,
|
|
17
|
-
isTrackedHyperliquidWhale,
|
|
18
|
-
loadHyperliquidWhaleFlowContext,
|
|
19
|
-
resolveHyperliquidPerpFromSignalSymbol
|
|
20
|
-
} from "./chunk-3BP3ETAD.mjs";
|
|
21
|
-
import {
|
|
22
|
-
require_lodash
|
|
23
|
-
} from "./chunk-GPR56UYQ.mjs";
|
|
24
|
-
import {
|
|
25
4
|
DEFAULT_AI_MODEL,
|
|
26
5
|
MAX_AI_SERIES_POINTS,
|
|
27
6
|
askAI,
|
|
@@ -29,2537 +8,57 @@ import {
|
|
|
29
8
|
buildAiPayload,
|
|
30
9
|
buildAiPrompts,
|
|
31
10
|
buildAiSystemPrompt,
|
|
11
|
+
buildBinanceBreadthUniverseSnapshot,
|
|
32
12
|
buildCompactAiIndicatorsSnapshot,
|
|
13
|
+
createStrategyRuntime,
|
|
14
|
+
enrichSignalWithAi,
|
|
15
|
+
enrichSignalWithBinanceMarketContext,
|
|
16
|
+
enrichSignalWithCoinMarketCapContext,
|
|
17
|
+
enrichSignalWithHyperliquidWhaleContext,
|
|
18
|
+
enrichSignalWithMl,
|
|
19
|
+
enrichSignalWithMlAi,
|
|
33
20
|
ensureAiStrategyPluginsLoaded,
|
|
34
|
-
getDeterministicAiGateContext,
|
|
35
|
-
getOpenRouterModelKwargs,
|
|
36
|
-
resetAiRuntimeCache,
|
|
37
|
-
resolveStrategyPolicyProfile,
|
|
38
|
-
runAiPrompt,
|
|
39
|
-
runAiPromptLocal,
|
|
40
|
-
trimSeriesDeep
|
|
41
|
-
} from "./chunk-WK5EUCX5.mjs";
|
|
42
|
-
import {
|
|
43
|
-
createPineScriptLoader
|
|
44
|
-
} from "./chunk-IQKMII6L.mjs";
|
|
45
|
-
import "./chunk-5YNMSWL3.mjs";
|
|
46
|
-
import {
|
|
47
21
|
ensureIndicatorPluginsLoaded,
|
|
48
22
|
ensureStrategyPluginsLoaded,
|
|
23
|
+
executeEntryOrder,
|
|
49
24
|
getAvailableStrategyNames,
|
|
25
|
+
getBinanceBreadthUniverseSnapshot,
|
|
26
|
+
getBinanceBreadthUniverses,
|
|
27
|
+
getDeterministicAiGateContext,
|
|
28
|
+
getHyperliquidPerpSymbols,
|
|
29
|
+
getHyperliquidPerpUniverseSnapshot,
|
|
30
|
+
getHyperliquidWhaleAddresses,
|
|
31
|
+
getHyperliquidWhaleRegistrySnapshot,
|
|
32
|
+
getOpenRouterModelKwargs,
|
|
33
|
+
getOrderArrivalSnapshot,
|
|
34
|
+
getPrimaryBinanceBreadthUniverse,
|
|
50
35
|
getRegisteredManifests,
|
|
51
36
|
getRegisteredStrategies,
|
|
52
37
|
getStrategyCreator,
|
|
53
38
|
getStrategyManifest,
|
|
54
39
|
isKnownStrategy,
|
|
40
|
+
isTrackedHyperliquidPerp,
|
|
41
|
+
isTrackedHyperliquidWhale,
|
|
55
42
|
registerStrategyEntries,
|
|
43
|
+
resetAiRuntimeCache,
|
|
56
44
|
resetStrategyRegistryCache,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
45
|
+
resolveHyperliquidPerpFromSignalSymbol,
|
|
46
|
+
resolveStrategyConfig,
|
|
47
|
+
runAiPrompt,
|
|
48
|
+
runAiPromptLocal,
|
|
49
|
+
strategies,
|
|
50
|
+
trimSeriesDeep,
|
|
51
|
+
validateEntryProtectionAtArrival
|
|
52
|
+
} from "./chunk-4AVFJMQL.mjs";
|
|
53
|
+
import "./chunk-WS5DYEVZ.mjs";
|
|
54
|
+
import "./chunk-Y6FXYEAI.mjs";
|
|
66
55
|
|
|
67
56
|
// src/strategies.ts
|
|
68
57
|
export * from "@tradejs/core/strategies";
|
|
69
58
|
|
|
70
|
-
// src/strategyRuntime.ts
|
|
71
|
-
import path from "path";
|
|
72
|
-
import {
|
|
73
|
-
BACKTEST_EXECUTION_DELAY_MS,
|
|
74
|
-
BACKTEST_EXECUTION_INTERVAL,
|
|
75
|
-
BACKTEST_LOWER_TIMEFRAME_EXECUTION_ENABLED
|
|
76
|
-
} from "@tradejs/core/constants";
|
|
77
|
-
import { intervalToMs } from "@tradejs/core/data";
|
|
78
|
-
import {
|
|
79
|
-
buildDefaultIndicatorPeriods,
|
|
80
|
-
calculateRiskRatio,
|
|
81
|
-
createStrategyAPI,
|
|
82
|
-
createStrategyIndicatorsState,
|
|
83
|
-
getSharedStrategyReplayState,
|
|
84
|
-
resolveBacktestExecutionPrice
|
|
85
|
-
} from "@tradejs/core/strategies";
|
|
86
|
-
import { logger as logger3 } from "@tradejs/infra/logger";
|
|
87
|
-
|
|
88
|
-
// src/strategyHelpers/runtime.ts
|
|
89
|
-
import { logger as logger2 } from "@tradejs/infra/logger";
|
|
90
|
-
import { FEE_PERCENT } from "@tradejs/core/constants";
|
|
91
|
-
import {
|
|
92
|
-
buildMlFeatures,
|
|
93
|
-
buildMlTrainingRow,
|
|
94
|
-
fetchMlThreshold,
|
|
95
|
-
trimMlTrainingRowWindows
|
|
96
|
-
} from "@tradejs/infra/ml";
|
|
97
|
-
|
|
98
|
-
// src/runtimeJournal.ts
|
|
99
|
-
import { randomUUID } from "crypto";
|
|
100
|
-
import { TTL_1M } from "@tradejs/core/constants";
|
|
101
|
-
import { getRuntimeStorageDayKey } from "@tradejs/core/time";
|
|
102
|
-
import { createRuntimeOrderLinkPrefix } from "@tradejs/core/trade";
|
|
103
|
-
import { logger } from "@tradejs/infra/logger";
|
|
104
|
-
import {
|
|
105
|
-
delKey,
|
|
106
|
-
getData,
|
|
107
|
-
redisKeys,
|
|
108
|
-
setData,
|
|
109
|
-
setHashJsonField
|
|
110
|
-
} from "@tradejs/infra/redis";
|
|
111
|
-
var now = () => Date.now();
|
|
112
|
-
var toRandomOrderSuffix = () => randomUUID().replace(/-/g, "").slice(0, 12).toLowerCase();
|
|
113
|
-
var calculateClosedPnl = ({
|
|
114
|
-
direction,
|
|
115
|
-
entryPrice,
|
|
116
|
-
exitPrice,
|
|
117
|
-
qty
|
|
118
|
-
}) => {
|
|
119
|
-
const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
|
|
120
|
-
return Number.isFinite(pnl) ? pnl : null;
|
|
121
|
-
};
|
|
122
|
-
var createRuntimeOrderId = (strategy) => {
|
|
123
|
-
const prefix = createRuntimeOrderLinkPrefix(strategy);
|
|
124
|
-
if (prefix === "tjs-") {
|
|
125
|
-
return `tjs-${randomUUID().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
|
|
126
|
-
}
|
|
127
|
-
return `${prefix}${toRandomOrderSuffix()}`;
|
|
128
|
-
};
|
|
129
|
-
var recordRuntimeTradeOpen = async (params) => {
|
|
130
|
-
const { userName } = params;
|
|
131
|
-
if (!userName) {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
const record = {
|
|
135
|
-
...params,
|
|
136
|
-
entryCount: 1,
|
|
137
|
-
lastEntryPrice: params.entryPrice,
|
|
138
|
-
lastEntryQty: params.qty,
|
|
139
|
-
lastEntryTimestamp: params.entryTimestamp,
|
|
140
|
-
status: "active",
|
|
141
|
-
currentPrice: params.entryPrice,
|
|
142
|
-
currentPnl: 0,
|
|
143
|
-
closedPnl: null,
|
|
144
|
-
exitPrice: null,
|
|
145
|
-
exitTimestamp: null,
|
|
146
|
-
lastSyncedAt: now()
|
|
147
|
-
};
|
|
148
|
-
const dayKey = getRuntimeStorageDayKey(record.entryTimestamp);
|
|
149
|
-
const runtimeScopeId = record.deploymentId ?? record.accountId;
|
|
150
|
-
try {
|
|
151
|
-
await Promise.all([
|
|
152
|
-
setData(redisKeys.runtimeTrade(userName, record.orderId), record, {
|
|
153
|
-
expire: 0
|
|
154
|
-
}),
|
|
155
|
-
setHashJsonField(
|
|
156
|
-
redisKeys.runtimeTradeBucket(userName, dayKey),
|
|
157
|
-
record.orderId,
|
|
158
|
-
record,
|
|
159
|
-
{ expire: 0 }
|
|
160
|
-
),
|
|
161
|
-
setData(
|
|
162
|
-
redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
|
|
163
|
-
{ orderId: record.orderId },
|
|
164
|
-
{ expire: 0 }
|
|
165
|
-
)
|
|
166
|
-
]);
|
|
167
|
-
} catch (error) {
|
|
168
|
-
logger.error(
|
|
169
|
-
"runtime trade open journal failed: %s %s",
|
|
170
|
-
record.symbol,
|
|
171
|
-
error?.message || String(error)
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
return record;
|
|
175
|
-
};
|
|
176
|
-
var recordRuntimeTradeIncrease = async (params) => {
|
|
177
|
-
const {
|
|
178
|
-
userName,
|
|
179
|
-
strategy,
|
|
180
|
-
symbol,
|
|
181
|
-
direction,
|
|
182
|
-
resultingQty,
|
|
183
|
-
resultingEntryPrice,
|
|
184
|
-
addedQty,
|
|
185
|
-
addedEntryPrice,
|
|
186
|
-
entryTimestamp,
|
|
187
|
-
fee,
|
|
188
|
-
accountId,
|
|
189
|
-
deploymentId
|
|
190
|
-
} = params;
|
|
191
|
-
if (!userName) {
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
const existing = await getActiveRuntimeTrade({
|
|
195
|
-
userName,
|
|
196
|
-
symbol,
|
|
197
|
-
accountId,
|
|
198
|
-
deploymentId
|
|
199
|
-
});
|
|
200
|
-
if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
|
|
201
|
-
return null;
|
|
202
|
-
}
|
|
203
|
-
const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
|
|
204
|
-
const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
|
|
205
|
-
const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
|
|
206
|
-
const next = {
|
|
207
|
-
...existing,
|
|
208
|
-
qty: resultingQty,
|
|
209
|
-
entryPrice: resultingEntryPrice,
|
|
210
|
-
entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
|
|
211
|
-
lastEntryPrice: addedEntryPrice,
|
|
212
|
-
lastEntryQty: addedQty,
|
|
213
|
-
lastEntryTimestamp: entryTimestamp,
|
|
214
|
-
currentPrice: resultingEntryPrice,
|
|
215
|
-
currentPnl: 0,
|
|
216
|
-
fee: openFee,
|
|
217
|
-
openFee,
|
|
218
|
-
totalFee,
|
|
219
|
-
lastSyncedAt: now()
|
|
220
|
-
};
|
|
221
|
-
const dayKey = getRuntimeStorageDayKey(existing.entryTimestamp);
|
|
222
|
-
try {
|
|
223
|
-
await Promise.all([
|
|
224
|
-
setData(redisKeys.runtimeTrade(userName, existing.orderId), next, {
|
|
225
|
-
expire: 0
|
|
226
|
-
}),
|
|
227
|
-
setHashJsonField(
|
|
228
|
-
redisKeys.runtimeTradeBucket(userName, dayKey),
|
|
229
|
-
existing.orderId,
|
|
230
|
-
next,
|
|
231
|
-
{ expire: 0 }
|
|
232
|
-
)
|
|
233
|
-
]);
|
|
234
|
-
} catch (error) {
|
|
235
|
-
logger.error(
|
|
236
|
-
"runtime trade increase journal failed: %s %s",
|
|
237
|
-
symbol,
|
|
238
|
-
error?.message || String(error)
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
return next;
|
|
242
|
-
};
|
|
243
|
-
var getActiveRuntimeTrade = async (params) => {
|
|
244
|
-
const { userName, symbol, accountId, deploymentId } = params;
|
|
245
|
-
if (!userName) {
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
|
-
const activeRef = await getData(
|
|
249
|
-
redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
|
|
250
|
-
null
|
|
251
|
-
);
|
|
252
|
-
const orderId = String(activeRef?.orderId || "").trim();
|
|
253
|
-
if (!orderId) {
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
const existing = await getData(
|
|
257
|
-
redisKeys.runtimeTrade(userName, orderId),
|
|
258
|
-
null
|
|
259
|
-
);
|
|
260
|
-
if (!existing || existing.status !== "active") {
|
|
261
|
-
await delKey(
|
|
262
|
-
redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
|
|
263
|
-
);
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
return existing;
|
|
267
|
-
};
|
|
268
|
-
var markRuntimeTradeClosed = async (params) => {
|
|
269
|
-
const {
|
|
270
|
-
userName,
|
|
271
|
-
symbol,
|
|
272
|
-
strategy,
|
|
273
|
-
exitPrice,
|
|
274
|
-
exitTimestamp,
|
|
275
|
-
closedPnl,
|
|
276
|
-
exitType,
|
|
277
|
-
accountId,
|
|
278
|
-
deploymentId
|
|
279
|
-
} = params;
|
|
280
|
-
if (!userName) {
|
|
281
|
-
return null;
|
|
282
|
-
}
|
|
283
|
-
const existing = await getActiveRuntimeTrade({
|
|
284
|
-
userName,
|
|
285
|
-
symbol,
|
|
286
|
-
accountId,
|
|
287
|
-
deploymentId
|
|
288
|
-
});
|
|
289
|
-
if (!existing) {
|
|
290
|
-
return null;
|
|
291
|
-
}
|
|
292
|
-
const orderId = existing.orderId;
|
|
293
|
-
if (strategy && existing.strategy !== strategy) {
|
|
294
|
-
return null;
|
|
295
|
-
}
|
|
296
|
-
const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
|
|
297
|
-
const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
|
|
298
|
-
direction: existing.direction,
|
|
299
|
-
entryPrice: existing.entryPrice,
|
|
300
|
-
exitPrice: resolvedExitPrice,
|
|
301
|
-
qty: existing.qty
|
|
302
|
-
}) : existing.closedPnl ?? existing.currentPnl ?? null;
|
|
303
|
-
const next = {
|
|
304
|
-
...existing,
|
|
305
|
-
status: "closed",
|
|
306
|
-
currentPrice: resolvedExitPrice,
|
|
307
|
-
currentPnl: resolvedClosedPnl,
|
|
308
|
-
closedPnl: resolvedClosedPnl,
|
|
309
|
-
exitPrice: resolvedExitPrice,
|
|
310
|
-
exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
|
|
311
|
-
exitType: exitType ?? existing.exitType ?? null,
|
|
312
|
-
lastSyncedAt: now()
|
|
313
|
-
};
|
|
314
|
-
const dayKey = getRuntimeStorageDayKey(existing.entryTimestamp);
|
|
315
|
-
const closeDayKey = getRuntimeStorageDayKey(next.exitTimestamp);
|
|
316
|
-
try {
|
|
317
|
-
await Promise.all([
|
|
318
|
-
setData(redisKeys.runtimeTrade(userName, orderId), next, {
|
|
319
|
-
expire: TTL_1M
|
|
320
|
-
}),
|
|
321
|
-
setHashJsonField(
|
|
322
|
-
redisKeys.runtimeTradeBucket(userName, dayKey),
|
|
323
|
-
orderId,
|
|
324
|
-
next,
|
|
325
|
-
{ expire: TTL_1M }
|
|
326
|
-
),
|
|
327
|
-
setHashJsonField(
|
|
328
|
-
redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
|
|
329
|
-
orderId,
|
|
330
|
-
next,
|
|
331
|
-
{ expire: TTL_1M }
|
|
332
|
-
),
|
|
333
|
-
delKey(
|
|
334
|
-
redisKeys.runtimeActiveTrade(
|
|
335
|
-
userName,
|
|
336
|
-
symbol,
|
|
337
|
-
deploymentId ?? accountId
|
|
338
|
-
)
|
|
339
|
-
)
|
|
340
|
-
]);
|
|
341
|
-
} catch (error) {
|
|
342
|
-
logger.error(
|
|
343
|
-
"runtime trade close journal failed: %s %s",
|
|
344
|
-
symbol,
|
|
345
|
-
error?.message || String(error)
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
return next;
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
// src/strategyHelpers/runtime.ts
|
|
352
|
-
var formatAiError = (err) => {
|
|
353
|
-
const error = err;
|
|
354
|
-
const safeJson = (value) => {
|
|
355
|
-
try {
|
|
356
|
-
return JSON.stringify(value);
|
|
357
|
-
} catch {
|
|
358
|
-
return String(value);
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
const details = {
|
|
362
|
-
message: String(error?.message ?? "unknown"),
|
|
363
|
-
status: error?.status ?? null,
|
|
364
|
-
code: error?.code ?? null,
|
|
365
|
-
type: error?.type ?? null,
|
|
366
|
-
providerError: error?.error ?? null
|
|
367
|
-
};
|
|
368
|
-
return safeJson(details);
|
|
369
|
-
};
|
|
370
|
-
var resolveAiQuality = (analysis, direction) => {
|
|
371
|
-
if (typeof analysis?.quality !== "number") {
|
|
372
|
-
return void 0;
|
|
373
|
-
}
|
|
374
|
-
const normalizedQuality = Math.round(analysis.quality);
|
|
375
|
-
const aiApprovedCurrentTrade = analysis.direction === direction;
|
|
376
|
-
return aiApprovedCurrentTrade ? normalizedQuality : 0;
|
|
377
|
-
};
|
|
378
|
-
var findReplayAiAnalysis = ({
|
|
379
|
-
signal,
|
|
380
|
-
direction,
|
|
381
|
-
ai
|
|
382
|
-
}) => {
|
|
383
|
-
const snapshots = ai?.replayAnalyses;
|
|
384
|
-
if (!Array.isArray(snapshots) || !snapshots.length) {
|
|
385
|
-
return void 0;
|
|
386
|
-
}
|
|
387
|
-
let best = null;
|
|
388
|
-
for (const snapshot of snapshots) {
|
|
389
|
-
if (snapshot.symbol !== signal.symbol || snapshot.direction !== direction || snapshot.strategy && snapshot.strategy !== signal.strategy) {
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
const toleranceMs = Math.max(0, Number(snapshot.toleranceMs ?? 0));
|
|
393
|
-
const diff = Math.abs(snapshot.timestamp - signal.timestamp);
|
|
394
|
-
if (diff > toleranceMs || best && diff >= best.diff) {
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
best = {
|
|
398
|
-
diff,
|
|
399
|
-
analysis: snapshot.analysis
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
return best?.analysis;
|
|
403
|
-
};
|
|
404
|
-
var enrichSignalWithMl = async ({
|
|
405
|
-
signal,
|
|
406
|
-
env,
|
|
407
|
-
ml
|
|
408
|
-
}) => {
|
|
409
|
-
if (env !== "BACKTEST" && ml && ml.enabled !== false && ml.strategyConfig && typeof ml.mlThreshold === "number") {
|
|
410
|
-
const strategy = signal.strategy;
|
|
411
|
-
const fullRow = buildMlTrainingRow(
|
|
412
|
-
buildMlPayload({
|
|
413
|
-
signal,
|
|
414
|
-
context: {
|
|
415
|
-
strategyConfig: ml.strategyConfig,
|
|
416
|
-
strategyName: strategy,
|
|
417
|
-
symbol: signal.symbol
|
|
418
|
-
}
|
|
419
|
-
}),
|
|
420
|
-
null
|
|
421
|
-
);
|
|
422
|
-
const row = trimMlTrainingRowWindows(fullRow, 5);
|
|
423
|
-
const features = buildMlFeatures(row);
|
|
424
|
-
const mlResult = await fetchMlThreshold({
|
|
425
|
-
strategy: ml.modelKey ?? strategy,
|
|
426
|
-
features,
|
|
427
|
-
threshold: ml.mlThreshold,
|
|
428
|
-
projectRoot: getTradejsProjectCwd()
|
|
429
|
-
});
|
|
430
|
-
if (mlResult) {
|
|
431
|
-
signal.ml = mlResult;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
};
|
|
435
|
-
var enrichSignalWithAi = async ({
|
|
436
|
-
signal,
|
|
437
|
-
symbol,
|
|
438
|
-
userName,
|
|
439
|
-
direction,
|
|
440
|
-
env,
|
|
441
|
-
ai
|
|
442
|
-
}) => {
|
|
443
|
-
if (ai?.enabled === false) {
|
|
444
|
-
return void 0;
|
|
445
|
-
}
|
|
446
|
-
if (env === "PARITY") {
|
|
447
|
-
const replayAnalysis = findReplayAiAnalysis({ signal, direction, ai });
|
|
448
|
-
if (replayAnalysis) {
|
|
449
|
-
signal.aiAnalysis = replayAnalysis;
|
|
450
|
-
return resolveAiQuality(replayAnalysis, direction);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
if (env === "BACKTEST") {
|
|
454
|
-
return void 0;
|
|
455
|
-
}
|
|
456
|
-
if (ai?.mode === "gate") {
|
|
457
|
-
const gateAnalysis = await runAiPromptLocal(signal);
|
|
458
|
-
const gateQuality = resolveAiQuality(gateAnalysis, direction);
|
|
459
|
-
signal.aiAnalysis = gateAnalysis;
|
|
460
|
-
return gateQuality;
|
|
461
|
-
}
|
|
462
|
-
try {
|
|
463
|
-
const analysis = await askAI(signal, { userName });
|
|
464
|
-
signal.aiAnalysis = analysis;
|
|
465
|
-
return resolveAiQuality(analysis, direction);
|
|
466
|
-
} catch (err) {
|
|
467
|
-
logger2.error("AI analysis error: %s %s", symbol, formatAiError(err));
|
|
468
|
-
}
|
|
469
|
-
return void 0;
|
|
470
|
-
};
|
|
471
|
-
var enrichSignalWithMlAi = async ({
|
|
472
|
-
signal,
|
|
473
|
-
userName,
|
|
474
|
-
symbol,
|
|
475
|
-
direction,
|
|
476
|
-
env,
|
|
477
|
-
ml,
|
|
478
|
-
ai
|
|
479
|
-
}) => {
|
|
480
|
-
await enrichSignalWithMarketContextStages({ signal, env });
|
|
481
|
-
await enrichSignalWithMl({ signal, env, ml });
|
|
482
|
-
return enrichSignalWithAi({ signal, userName, symbol, direction, env, ai });
|
|
483
|
-
};
|
|
484
|
-
var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
485
|
-
var getOrderArrivalSnapshot = async ({
|
|
486
|
-
connector,
|
|
487
|
-
symbol
|
|
488
|
-
}) => {
|
|
489
|
-
if (typeof connector.getTopOfBookTicker !== "function") {
|
|
490
|
-
return {
|
|
491
|
-
arrivalSnapshotTime: Date.now(),
|
|
492
|
-
arrivalSource: "unavailable",
|
|
493
|
-
bid: null,
|
|
494
|
-
ask: null,
|
|
495
|
-
arrivalMid: null,
|
|
496
|
-
spreadBps: null
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
try {
|
|
500
|
-
const ticker = await connector.getTopOfBookTicker(symbol);
|
|
501
|
-
const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
|
|
502
|
-
const bid = toFiniteNumberOrNull(ticker?.bidPrice);
|
|
503
|
-
const ask = toFiniteNumberOrNull(ticker?.askPrice);
|
|
504
|
-
const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
|
|
505
|
-
const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
|
|
506
|
-
return {
|
|
507
|
-
arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
|
|
508
|
-
arrivalSource: "top_of_book",
|
|
509
|
-
bid,
|
|
510
|
-
ask,
|
|
511
|
-
arrivalMid,
|
|
512
|
-
spreadBps
|
|
513
|
-
};
|
|
514
|
-
} catch (error) {
|
|
515
|
-
logger2.warn(
|
|
516
|
-
"runtime order arrival snapshot failed: %s %s",
|
|
517
|
-
symbol,
|
|
518
|
-
error?.message || String(error)
|
|
519
|
-
);
|
|
520
|
-
return {
|
|
521
|
-
arrivalSnapshotTime: Date.now(),
|
|
522
|
-
arrivalSource: "top_of_book_error",
|
|
523
|
-
bid: null,
|
|
524
|
-
ask: null,
|
|
525
|
-
arrivalMid: null,
|
|
526
|
-
spreadBps: null
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
};
|
|
530
|
-
var validateEntryProtectionAtArrival = ({
|
|
531
|
-
direction,
|
|
532
|
-
signalPrice,
|
|
533
|
-
bid,
|
|
534
|
-
ask,
|
|
535
|
-
arrivalMid,
|
|
536
|
-
takeProfits,
|
|
537
|
-
stopLossPrice
|
|
538
|
-
}) => {
|
|
539
|
-
const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
|
|
540
|
-
const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
|
|
541
|
-
if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
|
|
542
|
-
throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
|
|
543
|
-
}
|
|
544
|
-
for (const takeProfit of takeProfits) {
|
|
545
|
-
if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
|
|
546
|
-
throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
|
|
550
|
-
throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
|
|
551
|
-
}
|
|
552
|
-
};
|
|
553
|
-
var resolveRuntimeTelemetryQuality = ({
|
|
554
|
-
signalClosePrice,
|
|
555
|
-
arrivalMid,
|
|
556
|
-
orderSubmitTime,
|
|
557
|
-
orderAckTime,
|
|
558
|
-
fillAvgPrice,
|
|
559
|
-
fillTime
|
|
560
|
-
}) => {
|
|
561
|
-
if (signalClosePrice != null && arrivalMid != null && orderSubmitTime != null && orderAckTime != null && fillAvgPrice != null && fillTime != null) {
|
|
562
|
-
return "full";
|
|
563
|
-
}
|
|
564
|
-
if (fillAvgPrice != null && (arrivalMid != null || orderSubmitTime != null)) {
|
|
565
|
-
return "partial";
|
|
566
|
-
}
|
|
567
|
-
if (fillAvgPrice != null) {
|
|
568
|
-
return "price_only";
|
|
569
|
-
}
|
|
570
|
-
return "none";
|
|
571
|
-
};
|
|
572
|
-
var applyProtectiveOrders = async ({
|
|
573
|
-
connector,
|
|
574
|
-
symbol,
|
|
575
|
-
direction,
|
|
576
|
-
qty,
|
|
577
|
-
takeProfits,
|
|
578
|
-
stopLossPrice
|
|
579
|
-
}) => {
|
|
580
|
-
if (Array.isArray(takeProfits) && takeProfits.length > 0) {
|
|
581
|
-
const tpOk = await connector.setTakeProfits({
|
|
582
|
-
symbol,
|
|
583
|
-
direction,
|
|
584
|
-
qty,
|
|
585
|
-
takeProfits
|
|
586
|
-
});
|
|
587
|
-
if (!tpOk) {
|
|
588
|
-
throw new Error("SET_TAKE_PROFITS_FAILED");
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
if (typeof stopLossPrice === "number" && Number.isFinite(stopLossPrice)) {
|
|
592
|
-
const slOk = await connector.setStopLoss({
|
|
593
|
-
symbol,
|
|
594
|
-
direction,
|
|
595
|
-
stopLossPrice
|
|
596
|
-
});
|
|
597
|
-
if (!slOk) {
|
|
598
|
-
throw new Error("SET_STOP_LOSS_FAILED");
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
};
|
|
602
|
-
var executeEntryOrder = async ({
|
|
603
|
-
connector,
|
|
604
|
-
userName,
|
|
605
|
-
symbol,
|
|
606
|
-
direction,
|
|
607
|
-
qty,
|
|
608
|
-
currentPrice,
|
|
609
|
-
timestamp,
|
|
610
|
-
takeProfits,
|
|
611
|
-
stopLossPrice,
|
|
612
|
-
positionIntent = "open",
|
|
613
|
-
signal,
|
|
614
|
-
beforePlaceOrder,
|
|
615
|
-
recordRuntimeTrade = true,
|
|
616
|
-
leverage
|
|
617
|
-
}) => {
|
|
618
|
-
await beforePlaceOrder?.();
|
|
619
|
-
const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
|
|
620
|
-
const orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
|
|
621
|
-
const signalTimestamp = signal.timestamp;
|
|
622
|
-
const signalClosePrice = currentPrice;
|
|
623
|
-
signal.orderId = orderId;
|
|
624
|
-
signal.orderQty = qty;
|
|
625
|
-
signal.orderValue = qty * currentPrice;
|
|
626
|
-
signal.orderFailureReason = void 0;
|
|
627
|
-
const arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
|
|
628
|
-
try {
|
|
629
|
-
validateEntryProtectionAtArrival({
|
|
630
|
-
direction,
|
|
631
|
-
signalPrice: currentPrice,
|
|
632
|
-
bid: arrivalSnapshot.bid,
|
|
633
|
-
ask: arrivalSnapshot.ask,
|
|
634
|
-
arrivalMid: arrivalSnapshot.arrivalMid,
|
|
635
|
-
takeProfits,
|
|
636
|
-
stopLossPrice
|
|
637
|
-
});
|
|
638
|
-
} catch (error) {
|
|
639
|
-
signal.orderStatus = "failed";
|
|
640
|
-
signal.orderFailureReason = error.message;
|
|
641
|
-
throw error;
|
|
642
|
-
}
|
|
643
|
-
const orderSubmitTime = Date.now();
|
|
644
|
-
const orderPlaced = await connector.placeOrder({
|
|
645
|
-
symbol,
|
|
646
|
-
qty,
|
|
647
|
-
price: currentPrice,
|
|
648
|
-
isLimit: false,
|
|
649
|
-
positionIntent,
|
|
650
|
-
timestamp,
|
|
651
|
-
direction,
|
|
652
|
-
...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
|
|
653
|
-
orderId,
|
|
654
|
-
signal
|
|
655
|
-
});
|
|
656
|
-
const orderAckTime = Date.now();
|
|
657
|
-
const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
|
|
658
|
-
const currentPosition = await connector.getPosition(symbol);
|
|
659
|
-
const fillTime = Date.now();
|
|
660
|
-
const isPositionIncrease = positionIntent === "increase" && previousPosition?.direction === direction && Number.isFinite(previousPosition.qty) && previousPosition.qty > 0;
|
|
661
|
-
const hasRefreshedIncreasedPosition = Boolean(
|
|
662
|
-
isPositionIncrease && previousPosition && currentPosition?.qty && currentPosition.qty > previousPosition.qty
|
|
663
|
-
);
|
|
664
|
-
const hasUsableCurrentPosition = Boolean(
|
|
665
|
-
currentPosition && Number.isFinite(currentPosition.price) && Number.isFinite(currentPosition.qty) && (!isPositionIncrease || hasRefreshedIncreasedPosition)
|
|
666
|
-
);
|
|
667
|
-
const resultingEntryPrice = hasUsableCurrentPosition && currentPosition ? currentPosition.price : isPositionIncrease && previousPosition ? (previousPosition.price * previousPosition.qty + currentPrice * placedQty) / (previousPosition.qty + placedQty) : currentPrice;
|
|
668
|
-
const resultingQty = hasUsableCurrentPosition && currentPosition ? currentPosition.qty : isPositionIncrease && previousPosition ? previousPosition.qty + placedQty : placedQty;
|
|
669
|
-
const filledQty = isPositionIncrease && previousPosition ? hasRefreshedIncreasedPosition && currentPosition ? currentPosition.qty - previousPosition.qty : placedQty : resultingQty;
|
|
670
|
-
const fillPrice = isPositionIncrease && previousPosition && hasRefreshedIncreasedPosition && currentPosition ? (resultingEntryPrice * currentPosition.qty - previousPosition.price * previousPosition.qty) / (currentPosition.qty - previousPosition.qty) : isPositionIncrease ? currentPrice : resultingEntryPrice;
|
|
671
|
-
const fillSource = hasUsableCurrentPosition ? "exchange_position" : orderPlaced ? "requested_price" : "unknown";
|
|
672
|
-
const estimatedOpenFee = fillPrice * filledQty * FEE_PERCENT;
|
|
673
|
-
signal.prices.currentPrice = fillPrice;
|
|
674
|
-
signal.orderQty = filledQty;
|
|
675
|
-
signal.orderValue = filledQty * fillPrice;
|
|
676
|
-
if (orderPlaced) {
|
|
677
|
-
try {
|
|
678
|
-
await applyProtectiveOrders({
|
|
679
|
-
connector,
|
|
680
|
-
symbol,
|
|
681
|
-
direction,
|
|
682
|
-
qty: resultingQty,
|
|
683
|
-
takeProfits,
|
|
684
|
-
stopLossPrice
|
|
685
|
-
});
|
|
686
|
-
} catch (error) {
|
|
687
|
-
await connector.closePosition({
|
|
688
|
-
symbol,
|
|
689
|
-
price: resultingEntryPrice,
|
|
690
|
-
timestamp,
|
|
691
|
-
direction,
|
|
692
|
-
signal
|
|
693
|
-
});
|
|
694
|
-
throw error;
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
signal.orderStatus = orderPlaced ? "completed" : "failed";
|
|
698
|
-
signal.orderSkipReason = void 0;
|
|
699
|
-
if (orderPlaced) {
|
|
700
|
-
signal.orderFailureReason = void 0;
|
|
701
|
-
}
|
|
702
|
-
if (orderPlaced && recordRuntimeTrade && !isPositionIncrease) {
|
|
703
|
-
await recordRuntimeTradeOpen({
|
|
704
|
-
userName,
|
|
705
|
-
orderId,
|
|
706
|
-
signalId: signal.signalId,
|
|
707
|
-
strategy: signal.strategy,
|
|
708
|
-
symbol,
|
|
709
|
-
interval: signal.interval,
|
|
710
|
-
direction,
|
|
711
|
-
qty: resultingQty,
|
|
712
|
-
entryPrice: resultingEntryPrice,
|
|
713
|
-
signalTimestamp,
|
|
714
|
-
signalClosePrice,
|
|
715
|
-
arrivalSnapshotTime: arrivalSnapshot.arrivalSnapshotTime,
|
|
716
|
-
arrivalSource: arrivalSnapshot.arrivalSource,
|
|
717
|
-
arrivalMid: arrivalSnapshot.arrivalMid,
|
|
718
|
-
bid: arrivalSnapshot.bid,
|
|
719
|
-
ask: arrivalSnapshot.ask,
|
|
720
|
-
spreadBps: arrivalSnapshot.spreadBps,
|
|
721
|
-
orderSubmitTime,
|
|
722
|
-
orderAckTime,
|
|
723
|
-
fillAvgPrice: fillPrice,
|
|
724
|
-
fillSource,
|
|
725
|
-
fillTime,
|
|
726
|
-
telemetryQuality: resolveRuntimeTelemetryQuality({
|
|
727
|
-
signalClosePrice,
|
|
728
|
-
arrivalMid: arrivalSnapshot.arrivalMid,
|
|
729
|
-
orderSubmitTime,
|
|
730
|
-
orderAckTime,
|
|
731
|
-
fillAvgPrice: fillPrice,
|
|
732
|
-
fillTime
|
|
733
|
-
}),
|
|
734
|
-
fee: estimatedOpenFee,
|
|
735
|
-
openFee: estimatedOpenFee,
|
|
736
|
-
totalFee: estimatedOpenFee,
|
|
737
|
-
entryTimestamp: timestamp,
|
|
738
|
-
universe: signal.universe,
|
|
739
|
-
assetClass: signal.assetClass,
|
|
740
|
-
accountId: signal.accountId,
|
|
741
|
-
deploymentId: signal.deploymentId,
|
|
742
|
-
policyProfileId: signal.policyProfileId,
|
|
743
|
-
runtimeConfigId: signal.runtimeConfigId,
|
|
744
|
-
runtimeLineage: signal.runtimeLineage,
|
|
745
|
-
...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {}
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
if (orderPlaced && recordRuntimeTrade && isPositionIncrease) {
|
|
749
|
-
await recordRuntimeTradeIncrease({
|
|
750
|
-
userName,
|
|
751
|
-
strategy: signal.strategy,
|
|
752
|
-
symbol,
|
|
753
|
-
direction,
|
|
754
|
-
resultingQty,
|
|
755
|
-
resultingEntryPrice,
|
|
756
|
-
addedQty: filledQty,
|
|
757
|
-
addedEntryPrice: fillPrice,
|
|
758
|
-
entryTimestamp: timestamp,
|
|
759
|
-
fee: estimatedOpenFee,
|
|
760
|
-
accountId: signal.accountId,
|
|
761
|
-
deploymentId: signal.deploymentId
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
if (hasUsableCurrentPosition && currentPosition?.price) {
|
|
765
|
-
return currentPosition.price;
|
|
766
|
-
}
|
|
767
|
-
return resultingEntryPrice;
|
|
768
|
-
};
|
|
769
|
-
var updatePositionProtection = async ({
|
|
770
|
-
connector,
|
|
771
|
-
symbol,
|
|
772
|
-
direction,
|
|
773
|
-
qty,
|
|
774
|
-
takeProfits,
|
|
775
|
-
stopLossPrice
|
|
776
|
-
}) => {
|
|
777
|
-
await applyProtectiveOrders({
|
|
778
|
-
connector,
|
|
779
|
-
symbol,
|
|
780
|
-
direction,
|
|
781
|
-
qty,
|
|
782
|
-
takeProfits: takeProfits ?? [],
|
|
783
|
-
stopLossPrice: stopLossPrice ?? null
|
|
784
|
-
});
|
|
785
|
-
};
|
|
786
|
-
|
|
787
|
-
// src/strategyHelpers/config.ts
|
|
788
|
-
var import_lodash = __toESM(require_lodash());
|
|
789
|
-
import { getData as getData2, redisKeys as redisKeys2 } from "@tradejs/infra/redis";
|
|
790
|
-
var resolveStrategyConfig = async ({
|
|
791
|
-
strategyName,
|
|
792
|
-
userName,
|
|
793
|
-
symbol,
|
|
794
|
-
baseConfig,
|
|
795
|
-
defaults,
|
|
796
|
-
runtimeConfigId,
|
|
797
|
-
runtimeConfigSnapshot
|
|
798
|
-
}) => {
|
|
799
|
-
const mergeIfNotEmpty = (target, patch) => patch && !import_lodash.default.isEmpty(patch) ? {
|
|
800
|
-
...target,
|
|
801
|
-
...patch
|
|
802
|
-
} : target;
|
|
803
|
-
let config = {
|
|
804
|
-
...defaults,
|
|
805
|
-
...baseConfig
|
|
806
|
-
};
|
|
807
|
-
let isConfigFromBacktest = false;
|
|
808
|
-
if (config.ENV !== "BACKTEST") {
|
|
809
|
-
const userConfig = runtimeConfigSnapshot ? runtimeConfigSnapshot.userConfig : await getData2(
|
|
810
|
-
runtimeConfigId ? redisKeys2.strategyConfig(
|
|
811
|
-
userName,
|
|
812
|
-
strategyName,
|
|
813
|
-
runtimeConfigId
|
|
814
|
-
) : redisKeys2.strategyConfig(userName, strategyName),
|
|
815
|
-
{}
|
|
816
|
-
);
|
|
817
|
-
config = mergeIfNotEmpty(config, userConfig);
|
|
818
|
-
if (!runtimeConfigId || runtimeConfigId === "config") {
|
|
819
|
-
const symbolResultConfig = runtimeConfigSnapshot ? runtimeConfigSnapshot.symbolResultConfig : (await getData2(
|
|
820
|
-
redisKeys2.strategyResults(userName, strategyName),
|
|
821
|
-
{}
|
|
822
|
-
))?.[symbol]?.config;
|
|
823
|
-
if (symbolResultConfig && !import_lodash.default.isEmpty(symbolResultConfig)) {
|
|
824
|
-
config = mergeIfNotEmpty(
|
|
825
|
-
config,
|
|
826
|
-
symbolResultConfig
|
|
827
|
-
);
|
|
828
|
-
isConfigFromBacktest = true;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
return { config, isConfigFromBacktest };
|
|
833
|
-
};
|
|
834
|
-
|
|
835
|
-
// src/strategyRuntime.ts
|
|
836
|
-
import {
|
|
837
|
-
BACKTEST_WARNING_CODES
|
|
838
|
-
} from "@tradejs/types";
|
|
839
|
-
var cloneWithPropertyDescriptors = (value) => Object.create(
|
|
840
|
-
Object.getPrototypeOf(value),
|
|
841
|
-
Object.getOwnPropertyDescriptors(value)
|
|
842
|
-
);
|
|
843
|
-
var buildExitOrderSignal = ({
|
|
844
|
-
strategyName,
|
|
845
|
-
symbol,
|
|
846
|
-
decision
|
|
847
|
-
}) => {
|
|
848
|
-
if (!strategyName) {
|
|
849
|
-
return void 0;
|
|
850
|
-
}
|
|
851
|
-
return {
|
|
852
|
-
signalId: `${strategyName}:${symbol}:exit:${decision.closePlan.timestamp}`,
|
|
853
|
-
strategy: strategyName,
|
|
854
|
-
symbol,
|
|
855
|
-
interval: "15",
|
|
856
|
-
direction: decision.closePlan.direction,
|
|
857
|
-
timestamp: decision.closePlan.timestamp,
|
|
858
|
-
figures: {},
|
|
859
|
-
indicators: {},
|
|
860
|
-
prices: {
|
|
861
|
-
currentPrice: decision.closePlan.price,
|
|
862
|
-
takeProfitPrice: decision.closePlan.price,
|
|
863
|
-
stopLossPrice: decision.closePlan.price,
|
|
864
|
-
riskRatio: 0
|
|
865
|
-
},
|
|
866
|
-
additionalIndicators: {
|
|
867
|
-
exit: {
|
|
868
|
-
code: decision.code
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
};
|
|
872
|
-
};
|
|
873
|
-
var resolveEntryRuntimePolicy = ({
|
|
874
|
-
decision,
|
|
875
|
-
config,
|
|
876
|
-
manifest,
|
|
877
|
-
policyProfile
|
|
878
|
-
}) => {
|
|
879
|
-
const baseDefaults = manifest?.entryRuntimeDefaults;
|
|
880
|
-
const profileDefaults = policyProfile?.entryRuntimeDefaults;
|
|
881
|
-
const manifestDefaults = baseDefaults || profileDefaults ? {
|
|
882
|
-
...baseDefaults,
|
|
883
|
-
...profileDefaults,
|
|
884
|
-
...baseDefaults?.ml || profileDefaults?.ml ? { ml: { ...baseDefaults?.ml, ...profileDefaults?.ml } } : {},
|
|
885
|
-
...baseDefaults?.ai || profileDefaults?.ai ? { ai: { ...baseDefaults?.ai, ...profileDefaults?.ai } } : {}
|
|
886
|
-
} : void 0;
|
|
887
|
-
const adapterMl = (policyProfile?.mlAdapter ?? manifest?.mlAdapter)?.mapEntryRuntimeFromConfig?.(config);
|
|
888
|
-
const adapterAi = (policyProfile?.aiAdapter ?? manifest?.aiAdapter)?.mapEntryRuntimeFromConfig?.(config);
|
|
889
|
-
const ml = manifestDefaults?.ml || adapterMl || decision.runtime?.ml ? {
|
|
890
|
-
...manifestDefaults?.ml,
|
|
891
|
-
...adapterMl,
|
|
892
|
-
...decision.runtime?.ml
|
|
893
|
-
} : void 0;
|
|
894
|
-
const ai = manifestDefaults?.ai || adapterAi || decision.runtime?.ai ? {
|
|
895
|
-
...manifestDefaults?.ai,
|
|
896
|
-
...adapterAi,
|
|
897
|
-
...decision.runtime?.ai
|
|
898
|
-
} : void 0;
|
|
899
|
-
return {
|
|
900
|
-
...manifestDefaults,
|
|
901
|
-
...decision.runtime,
|
|
902
|
-
ml,
|
|
903
|
-
ai
|
|
904
|
-
};
|
|
905
|
-
};
|
|
906
|
-
var formatGateNumber = (value) => {
|
|
907
|
-
if (!Number.isFinite(value)) {
|
|
908
|
-
return String(value);
|
|
909
|
-
}
|
|
910
|
-
const normalized = Number(value.toFixed(6));
|
|
911
|
-
return Number.isInteger(normalized) ? String(normalized) : String(normalized);
|
|
912
|
-
};
|
|
913
|
-
var isMlRuntimeGateEnabled = (params) => {
|
|
914
|
-
const { env, ml } = params;
|
|
915
|
-
return env !== "BACKTEST" && ml?.config != null && ml.config.enabled !== false;
|
|
916
|
-
};
|
|
917
|
-
var isMlResultUnavailable = (params) => {
|
|
918
|
-
const { env, ml } = params;
|
|
919
|
-
return isMlRuntimeGateEnabled({ env, ml }) && ml?.result == null;
|
|
920
|
-
};
|
|
921
|
-
var shouldExecuteEntryDecision = ({
|
|
922
|
-
makeOrdersEnabled,
|
|
923
|
-
env,
|
|
924
|
-
signal,
|
|
925
|
-
ml,
|
|
926
|
-
aiEnabled,
|
|
927
|
-
quality,
|
|
928
|
-
minAiQuality
|
|
929
|
-
}) => {
|
|
930
|
-
if (!makeOrdersEnabled) {
|
|
931
|
-
return false;
|
|
932
|
-
}
|
|
933
|
-
if (!signal || env === "BACKTEST") {
|
|
934
|
-
return true;
|
|
935
|
-
}
|
|
936
|
-
if (isMlResultUnavailable({ env, ml })) {
|
|
937
|
-
return false;
|
|
938
|
-
}
|
|
939
|
-
if (isMlRuntimeGateEnabled({ env, ml }) && ml?.result?.passed === false) {
|
|
940
|
-
return false;
|
|
941
|
-
}
|
|
942
|
-
if (!aiEnabled) {
|
|
943
|
-
return true;
|
|
944
|
-
}
|
|
945
|
-
return Number.isFinite(quality) && quality >= minAiQuality;
|
|
946
|
-
};
|
|
947
|
-
var getEntrySkipReason = ({
|
|
948
|
-
makeOrdersEnabled,
|
|
949
|
-
env,
|
|
950
|
-
ml,
|
|
951
|
-
aiEnabled,
|
|
952
|
-
quality,
|
|
953
|
-
minAiQuality
|
|
954
|
-
}) => {
|
|
955
|
-
if (!makeOrdersEnabled) {
|
|
956
|
-
return "MAKE_ORDERS_DISABLED";
|
|
957
|
-
}
|
|
958
|
-
if (isMlResultUnavailable({ env, ml })) {
|
|
959
|
-
return "ML_RESULT_UNAVAILABLE";
|
|
960
|
-
}
|
|
961
|
-
if (isMlRuntimeGateEnabled({ env, ml }) && ml?.result?.passed === false) {
|
|
962
|
-
const probability = formatGateNumber(ml.result.probability);
|
|
963
|
-
const threshold = formatGateNumber(ml.result.threshold);
|
|
964
|
-
return `ML_THRESHOLD_NOT_MET (${probability} < ${threshold})`;
|
|
965
|
-
}
|
|
966
|
-
if (env !== "BACKTEST" && aiEnabled && quality == null) {
|
|
967
|
-
return "AI_QUALITY_UNAVAILABLE";
|
|
968
|
-
}
|
|
969
|
-
if (env !== "BACKTEST" && aiEnabled && quality != null && Number.isFinite(quality) && quality < minAiQuality) {
|
|
970
|
-
return `AI_QUALITY_BELOW_MIN (${quality} < ${minAiQuality})`;
|
|
971
|
-
}
|
|
972
|
-
return "ENTRY_POLICY_BLOCKED";
|
|
973
|
-
};
|
|
974
|
-
var resolveBacktestEntryDelayBars = (value) => {
|
|
975
|
-
if (value == null || value === "") {
|
|
976
|
-
return 1;
|
|
977
|
-
}
|
|
978
|
-
const parsed = parseInt(String(value), 10);
|
|
979
|
-
return Number.isFinite(parsed) ? Math.max(0, parsed) : 1;
|
|
980
|
-
};
|
|
981
|
-
var resolveBacktestExecutionIntervalForPrimary = (interval) => {
|
|
982
|
-
const normalized = String(interval ?? "15");
|
|
983
|
-
if (normalized === "15") {
|
|
984
|
-
return BACKTEST_EXECUTION_INTERVAL;
|
|
985
|
-
}
|
|
986
|
-
if (normalized === "60") {
|
|
987
|
-
return "15";
|
|
988
|
-
}
|
|
989
|
-
return null;
|
|
990
|
-
};
|
|
991
|
-
var resolveBacktestExecutionDelayMs = (value, fallbackDelayMs) => {
|
|
992
|
-
if (value == null || value === "") {
|
|
993
|
-
return fallbackDelayMs;
|
|
994
|
-
}
|
|
995
|
-
const parsed = Number(value);
|
|
996
|
-
return Number.isFinite(parsed) ? Math.max(0, Math.trunc(parsed)) : fallbackDelayMs;
|
|
997
|
-
};
|
|
998
|
-
var safeIntervalToMs = (interval) => {
|
|
999
|
-
try {
|
|
1000
|
-
return intervalToMs(interval);
|
|
1001
|
-
} catch {
|
|
1002
|
-
return null;
|
|
1003
|
-
}
|
|
1004
|
-
};
|
|
1005
|
-
var buildCandleByTimestamp = (candles) => new Map(
|
|
1006
|
-
(candles ?? []).filter((candle) => typeof candle?.timestamp === "number").map((candle) => [candle.timestamp, candle])
|
|
1007
|
-
);
|
|
1008
|
-
var buildBacktestExecutionOnlyCandle = (candle, executionPrice) => ({
|
|
1009
|
-
...candle,
|
|
1010
|
-
open: executionPrice,
|
|
1011
|
-
high: executionPrice,
|
|
1012
|
-
low: executionPrice,
|
|
1013
|
-
close: executionPrice,
|
|
1014
|
-
volume: 0,
|
|
1015
|
-
turnover: 0
|
|
1016
|
-
});
|
|
1017
|
-
var resolveInvalidDelayedEntryReason = ({
|
|
1018
|
-
decision,
|
|
1019
|
-
executionPrice,
|
|
1020
|
-
takeProfitPrice,
|
|
1021
|
-
stopLossPrice,
|
|
1022
|
-
riskRatio
|
|
1023
|
-
}) => {
|
|
1024
|
-
if (!Number.isFinite(executionPrice) || !Number.isFinite(takeProfitPrice) || !Number.isFinite(stopLossPrice)) {
|
|
1025
|
-
return "BACKTEST_DELAYED_ENTRY_INVALID_PRICE";
|
|
1026
|
-
}
|
|
1027
|
-
if (decision.entryContext.direction === "LONG") {
|
|
1028
|
-
if (executionPrice <= stopLossPrice) {
|
|
1029
|
-
return "BACKTEST_DELAYED_ENTRY_BEYOND_STOP";
|
|
1030
|
-
}
|
|
1031
|
-
if (executionPrice >= takeProfitPrice) {
|
|
1032
|
-
return "BACKTEST_DELAYED_ENTRY_BEYOND_TAKE_PROFIT";
|
|
1033
|
-
}
|
|
1034
|
-
return !Number.isFinite(riskRatio) || riskRatio <= 0 ? "BACKTEST_DELAYED_ENTRY_INVALID_PRICE" : null;
|
|
1035
|
-
}
|
|
1036
|
-
if (executionPrice >= stopLossPrice) {
|
|
1037
|
-
return "BACKTEST_DELAYED_ENTRY_BEYOND_STOP";
|
|
1038
|
-
}
|
|
1039
|
-
if (executionPrice <= takeProfitPrice) {
|
|
1040
|
-
return "BACKTEST_DELAYED_ENTRY_BEYOND_TAKE_PROFIT";
|
|
1041
|
-
}
|
|
1042
|
-
return !Number.isFinite(riskRatio) || riskRatio <= 0 ? "BACKTEST_DELAYED_ENTRY_INVALID_PRICE" : null;
|
|
1043
|
-
};
|
|
1044
|
-
var applyBacktestDelayedEntryExecution = ({
|
|
1045
|
-
decision,
|
|
1046
|
-
execution,
|
|
1047
|
-
backtestPriceMode,
|
|
1048
|
-
delayBars
|
|
1049
|
-
}) => {
|
|
1050
|
-
const { candle, btcCandle } = execution;
|
|
1051
|
-
const signalTimestamp = decision.signal?.timestamp ?? decision.entryContext.timestamp;
|
|
1052
|
-
const signalPrice = decision.signal?.prices.currentPrice ?? decision.entryContext.prices.currentPrice;
|
|
1053
|
-
const skipReason = execution.skipReason ?? (!candle || !btcCandle ? "BACKTEST_LOWER_EXECUTION_CANDLE_MISSING" : void 0);
|
|
1054
|
-
if (skipReason || !candle || !btcCandle) {
|
|
1055
|
-
if (decision.signal) {
|
|
1056
|
-
decision.signal.additionalIndicators = {
|
|
1057
|
-
...decision.signal.additionalIndicators ?? {},
|
|
1058
|
-
backtestExecution: {
|
|
1059
|
-
entryDelayBars: delayBars,
|
|
1060
|
-
priceMode: backtestPriceMode ?? "open",
|
|
1061
|
-
signalTimestamp,
|
|
1062
|
-
signalPrice,
|
|
1063
|
-
executionSource: execution.source,
|
|
1064
|
-
...execution.executionInterval ? { executionInterval: execution.executionInterval } : {},
|
|
1065
|
-
...execution.executionDelayMs != null ? { executionDelayMs: execution.executionDelayMs } : {},
|
|
1066
|
-
...execution.primaryExecutionTimestamp != null ? { primaryExecutionTimestamp: execution.primaryExecutionTimestamp } : {},
|
|
1067
|
-
...execution.requestedExecutionTimestamp != null ? {
|
|
1068
|
-
requestedExecutionTimestamp: execution.requestedExecutionTimestamp
|
|
1069
|
-
} : {},
|
|
1070
|
-
skipReason
|
|
1071
|
-
}
|
|
1072
|
-
};
|
|
1073
|
-
decision.signal.orderStatus = "skipped";
|
|
1074
|
-
decision.signal.orderSkipReason = skipReason;
|
|
1075
|
-
}
|
|
1076
|
-
return {
|
|
1077
|
-
skipReason,
|
|
1078
|
-
executionCandle: null,
|
|
1079
|
-
btcExecutionCandle: null
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
const executionPrice = resolveBacktestExecutionPrice(
|
|
1083
|
-
candle,
|
|
1084
|
-
backtestPriceMode ?? "open"
|
|
1085
|
-
);
|
|
1086
|
-
const executionTimestamp = candle.timestamp;
|
|
1087
|
-
const takeProfitPrice = decision.entryContext.prices.takeProfitPrice;
|
|
1088
|
-
const stopLossPrice = decision.orderPlan.stopLossPrice;
|
|
1089
|
-
const riskRatio = calculateRiskRatio({
|
|
1090
|
-
direction: decision.entryContext.direction,
|
|
1091
|
-
currentPrice: executionPrice,
|
|
1092
|
-
takeProfitPrice,
|
|
1093
|
-
stopLossPrice
|
|
1094
|
-
});
|
|
1095
|
-
const invalidSkipReason = resolveInvalidDelayedEntryReason({
|
|
1096
|
-
decision,
|
|
1097
|
-
executionPrice,
|
|
1098
|
-
takeProfitPrice,
|
|
1099
|
-
stopLossPrice,
|
|
1100
|
-
riskRatio
|
|
1101
|
-
});
|
|
1102
|
-
decision.entryContext = {
|
|
1103
|
-
...decision.entryContext,
|
|
1104
|
-
timestamp: executionTimestamp,
|
|
1105
|
-
prices: {
|
|
1106
|
-
...decision.entryContext.prices,
|
|
1107
|
-
currentPrice: executionPrice,
|
|
1108
|
-
stopLossPrice,
|
|
1109
|
-
riskRatio
|
|
1110
|
-
}
|
|
1111
|
-
};
|
|
1112
|
-
const executionResult = {
|
|
1113
|
-
skipReason: invalidSkipReason,
|
|
1114
|
-
executionCandle: buildBacktestExecutionOnlyCandle(candle, executionPrice),
|
|
1115
|
-
btcExecutionCandle: buildBacktestExecutionOnlyCandle(
|
|
1116
|
-
btcCandle,
|
|
1117
|
-
resolveBacktestExecutionPrice(btcCandle, backtestPriceMode ?? "open")
|
|
1118
|
-
)
|
|
1119
|
-
};
|
|
1120
|
-
if (!decision.signal) {
|
|
1121
|
-
return executionResult;
|
|
1122
|
-
}
|
|
1123
|
-
decision.signal.prices = {
|
|
1124
|
-
...decision.signal.prices,
|
|
1125
|
-
currentPrice: executionPrice,
|
|
1126
|
-
stopLossPrice,
|
|
1127
|
-
riskRatio
|
|
1128
|
-
};
|
|
1129
|
-
decision.signal.additionalIndicators = {
|
|
1130
|
-
...decision.signal.additionalIndicators ?? {},
|
|
1131
|
-
backtestExecution: {
|
|
1132
|
-
entryDelayBars: delayBars,
|
|
1133
|
-
priceMode: backtestPriceMode ?? "open",
|
|
1134
|
-
signalTimestamp,
|
|
1135
|
-
signalPrice,
|
|
1136
|
-
executionTimestamp,
|
|
1137
|
-
executionPrice,
|
|
1138
|
-
executionSource: execution.source,
|
|
1139
|
-
...execution.executionInterval ? { executionInterval: execution.executionInterval } : {},
|
|
1140
|
-
...execution.executionDelayMs != null ? { executionDelayMs: execution.executionDelayMs } : {},
|
|
1141
|
-
...execution.primaryExecutionTimestamp != null ? { primaryExecutionTimestamp: execution.primaryExecutionTimestamp } : {},
|
|
1142
|
-
...execution.requestedExecutionTimestamp != null ? { requestedExecutionTimestamp: execution.requestedExecutionTimestamp } : {},
|
|
1143
|
-
...invalidSkipReason ? { skipReason: invalidSkipReason } : {}
|
|
1144
|
-
}
|
|
1145
|
-
};
|
|
1146
|
-
if (invalidSkipReason) {
|
|
1147
|
-
decision.signal.orderStatus = "skipped";
|
|
1148
|
-
decision.signal.orderSkipReason = invalidSkipReason;
|
|
1149
|
-
}
|
|
1150
|
-
return executionResult;
|
|
1151
|
-
};
|
|
1152
|
-
var normalizeConfigHookList = (value) => {
|
|
1153
|
-
if (Array.isArray(value)) {
|
|
1154
|
-
return value;
|
|
1155
|
-
}
|
|
1156
|
-
return value ? [value] : [];
|
|
1157
|
-
};
|
|
1158
|
-
var isStrategyDecision = (value) => {
|
|
1159
|
-
if (!value || typeof value !== "object") {
|
|
1160
|
-
return false;
|
|
1161
|
-
}
|
|
1162
|
-
const kind = value.kind;
|
|
1163
|
-
return kind === "skip" || kind === "entry" || kind === "exit" || kind === "protect";
|
|
1164
|
-
};
|
|
1165
|
-
var CONFIG_HOOK_STAGES = [
|
|
1166
|
-
"onInit",
|
|
1167
|
-
"onBar",
|
|
1168
|
-
"afterCoreDecision",
|
|
1169
|
-
"afterBarDecision",
|
|
1170
|
-
"onSkip",
|
|
1171
|
-
"beforeClosePosition",
|
|
1172
|
-
"afterEnrichMl",
|
|
1173
|
-
"afterEnrichAi",
|
|
1174
|
-
"beforeEntryGate",
|
|
1175
|
-
"beforePlaceOrder",
|
|
1176
|
-
"afterPlaceOrder"
|
|
1177
|
-
];
|
|
1178
|
-
var isConfigHookStage = (stage) => CONFIG_HOOK_STAGES.includes(
|
|
1179
|
-
stage
|
|
1180
|
-
);
|
|
1181
|
-
var buildHookCtx = ({
|
|
1182
|
-
connector,
|
|
1183
|
-
strategyName,
|
|
1184
|
-
userName,
|
|
1185
|
-
symbol,
|
|
1186
|
-
universe,
|
|
1187
|
-
assetClass,
|
|
1188
|
-
accountId,
|
|
1189
|
-
deploymentId,
|
|
1190
|
-
policyProfileId,
|
|
1191
|
-
strategyConfig,
|
|
1192
|
-
env,
|
|
1193
|
-
isConfigFromBacktest
|
|
1194
|
-
}) => ({
|
|
1195
|
-
connector,
|
|
1196
|
-
strategyName,
|
|
1197
|
-
userName,
|
|
1198
|
-
symbol,
|
|
1199
|
-
...universe ? { universe } : {},
|
|
1200
|
-
...assetClass ? { assetClass } : {},
|
|
1201
|
-
...accountId ? { accountId } : {},
|
|
1202
|
-
...deploymentId ? { deploymentId } : {},
|
|
1203
|
-
...policyProfileId ? { policyProfileId } : {},
|
|
1204
|
-
strategyConfig,
|
|
1205
|
-
env,
|
|
1206
|
-
isConfigFromBacktest
|
|
1207
|
-
});
|
|
1208
|
-
var buildHookEntry = ({
|
|
1209
|
-
decision,
|
|
1210
|
-
runtime
|
|
1211
|
-
}) => ({
|
|
1212
|
-
context: decision.entryContext,
|
|
1213
|
-
orderPlan: decision.orderPlan,
|
|
1214
|
-
signal: decision.signal,
|
|
1215
|
-
runtime: {
|
|
1216
|
-
raw: decision.runtime,
|
|
1217
|
-
resolved: runtime
|
|
1218
|
-
}
|
|
1219
|
-
});
|
|
1220
|
-
var buildHookPolicy = ({
|
|
1221
|
-
quality,
|
|
1222
|
-
makeOrdersEnabled,
|
|
1223
|
-
minAiQuality
|
|
1224
|
-
}) => ({
|
|
1225
|
-
aiQuality: quality,
|
|
1226
|
-
makeOrdersEnabled,
|
|
1227
|
-
minAiQuality
|
|
1228
|
-
});
|
|
1229
|
-
var shouldRecordRuntimeJournal = ({
|
|
1230
|
-
env,
|
|
1231
|
-
config
|
|
1232
|
-
}) => env !== "BACKTEST" && env !== "PARITY" && config.RECORD_RUNTIME_TRADES !== false;
|
|
1233
|
-
var isTestConnector = (connector) => Boolean(
|
|
1234
|
-
connector.__tradejsTestConnector
|
|
1235
|
-
);
|
|
1236
|
-
var canUseSharedReplayState = ({
|
|
1237
|
-
env,
|
|
1238
|
-
sharedReplayKey
|
|
1239
|
-
}) => (env === "BACKTEST" || env === "PARITY") && Boolean(sharedReplayKey);
|
|
1240
|
-
var buildMlHookContext = ({
|
|
1241
|
-
signal,
|
|
1242
|
-
env,
|
|
1243
|
-
ml
|
|
1244
|
-
}) => {
|
|
1245
|
-
if (env === "BACKTEST") {
|
|
1246
|
-
return {
|
|
1247
|
-
config: ml,
|
|
1248
|
-
attempted: false,
|
|
1249
|
-
applied: false,
|
|
1250
|
-
skippedReason: "BACKTEST"
|
|
1251
|
-
};
|
|
1252
|
-
}
|
|
1253
|
-
if (!ml) {
|
|
1254
|
-
return {
|
|
1255
|
-
attempted: false,
|
|
1256
|
-
applied: false,
|
|
1257
|
-
skippedReason: "NO_RUNTIME"
|
|
1258
|
-
};
|
|
1259
|
-
}
|
|
1260
|
-
if (ml.enabled === false) {
|
|
1261
|
-
return {
|
|
1262
|
-
config: ml,
|
|
1263
|
-
attempted: false,
|
|
1264
|
-
applied: false,
|
|
1265
|
-
skippedReason: "DISABLED"
|
|
1266
|
-
};
|
|
1267
|
-
}
|
|
1268
|
-
if (!ml.strategyConfig) {
|
|
1269
|
-
return {
|
|
1270
|
-
config: ml,
|
|
1271
|
-
attempted: false,
|
|
1272
|
-
applied: false,
|
|
1273
|
-
skippedReason: "NO_STRATEGY_CONFIG"
|
|
1274
|
-
};
|
|
1275
|
-
}
|
|
1276
|
-
if (typeof ml.mlThreshold !== "number") {
|
|
1277
|
-
return {
|
|
1278
|
-
config: ml,
|
|
1279
|
-
attempted: false,
|
|
1280
|
-
applied: false,
|
|
1281
|
-
skippedReason: "NO_THRESHOLD"
|
|
1282
|
-
};
|
|
1283
|
-
}
|
|
1284
|
-
if (signal.ml) {
|
|
1285
|
-
return {
|
|
1286
|
-
config: ml,
|
|
1287
|
-
attempted: true,
|
|
1288
|
-
applied: true,
|
|
1289
|
-
result: signal.ml
|
|
1290
|
-
};
|
|
1291
|
-
}
|
|
1292
|
-
return {
|
|
1293
|
-
config: ml,
|
|
1294
|
-
attempted: true,
|
|
1295
|
-
applied: false,
|
|
1296
|
-
skippedReason: "NO_RESULT"
|
|
1297
|
-
};
|
|
1298
|
-
};
|
|
1299
|
-
var buildAiHookContext = ({
|
|
1300
|
-
env,
|
|
1301
|
-
ai,
|
|
1302
|
-
quality
|
|
1303
|
-
}) => {
|
|
1304
|
-
if (env === "BACKTEST") {
|
|
1305
|
-
return {
|
|
1306
|
-
config: ai,
|
|
1307
|
-
attempted: false,
|
|
1308
|
-
applied: false,
|
|
1309
|
-
skippedReason: "BACKTEST"
|
|
1310
|
-
};
|
|
1311
|
-
}
|
|
1312
|
-
if (!ai) {
|
|
1313
|
-
return {
|
|
1314
|
-
attempted: false,
|
|
1315
|
-
applied: false,
|
|
1316
|
-
skippedReason: "NO_RUNTIME"
|
|
1317
|
-
};
|
|
1318
|
-
}
|
|
1319
|
-
if (ai.enabled === false) {
|
|
1320
|
-
return {
|
|
1321
|
-
config: ai,
|
|
1322
|
-
attempted: false,
|
|
1323
|
-
applied: false,
|
|
1324
|
-
skippedReason: "DISABLED"
|
|
1325
|
-
};
|
|
1326
|
-
}
|
|
1327
|
-
if (typeof quality === "number") {
|
|
1328
|
-
return {
|
|
1329
|
-
config: ai,
|
|
1330
|
-
attempted: true,
|
|
1331
|
-
applied: true,
|
|
1332
|
-
quality
|
|
1333
|
-
};
|
|
1334
|
-
}
|
|
1335
|
-
return {
|
|
1336
|
-
config: ai,
|
|
1337
|
-
attempted: true,
|
|
1338
|
-
applied: false,
|
|
1339
|
-
skippedReason: "NO_QUALITY"
|
|
1340
|
-
};
|
|
1341
|
-
};
|
|
1342
|
-
var handleExitDecision = async ({
|
|
1343
|
-
connector,
|
|
1344
|
-
userName,
|
|
1345
|
-
strategyName,
|
|
1346
|
-
symbol,
|
|
1347
|
-
decision,
|
|
1348
|
-
market,
|
|
1349
|
-
onRuntimeClose,
|
|
1350
|
-
onRuntimeError
|
|
1351
|
-
}) => {
|
|
1352
|
-
try {
|
|
1353
|
-
let activeTradeForClose = null;
|
|
1354
|
-
if (userName) {
|
|
1355
|
-
const activeTrade = await getActiveRuntimeTrade({
|
|
1356
|
-
userName,
|
|
1357
|
-
symbol,
|
|
1358
|
-
accountId: connector.accountId,
|
|
1359
|
-
deploymentId: connector.deploymentId
|
|
1360
|
-
});
|
|
1361
|
-
if (!activeTrade) {
|
|
1362
|
-
logger3.warn(
|
|
1363
|
-
"[%s] blocked closePosition for untracked runtime position: %s",
|
|
1364
|
-
strategyName ?? "unknown",
|
|
1365
|
-
symbol
|
|
1366
|
-
);
|
|
1367
|
-
return "CLOSE_BLOCKED_BY_UNTRACKED_POSITION";
|
|
1368
|
-
}
|
|
1369
|
-
if (!strategyName || activeTrade.strategy !== strategyName) {
|
|
1370
|
-
logger3.warn(
|
|
1371
|
-
"[%s] blocked closePosition for foreign runtime position: %s ownedBy=%s",
|
|
1372
|
-
strategyName ?? "unknown",
|
|
1373
|
-
symbol,
|
|
1374
|
-
activeTrade.strategy
|
|
1375
|
-
);
|
|
1376
|
-
return "CLOSE_BLOCKED_BY_FOREIGN_STRATEGY_POSITION";
|
|
1377
|
-
}
|
|
1378
|
-
activeTradeForClose = activeTrade;
|
|
1379
|
-
}
|
|
1380
|
-
await connector.closePosition({
|
|
1381
|
-
symbol,
|
|
1382
|
-
price: decision.closePlan.price,
|
|
1383
|
-
timestamp: decision.closePlan.timestamp,
|
|
1384
|
-
direction: decision.closePlan.direction,
|
|
1385
|
-
signal: buildExitOrderSignal({
|
|
1386
|
-
strategyName,
|
|
1387
|
-
symbol,
|
|
1388
|
-
decision
|
|
1389
|
-
})
|
|
1390
|
-
});
|
|
1391
|
-
const closedTrade = await markRuntimeTradeClosed({
|
|
1392
|
-
userName,
|
|
1393
|
-
strategy: strategyName,
|
|
1394
|
-
symbol,
|
|
1395
|
-
exitPrice: decision.closePlan.price,
|
|
1396
|
-
exitTimestamp: decision.closePlan.timestamp,
|
|
1397
|
-
exitType: "exit",
|
|
1398
|
-
accountId: connector.accountId,
|
|
1399
|
-
deploymentId: connector.deploymentId
|
|
1400
|
-
});
|
|
1401
|
-
const trade = closedTrade ?? activeTradeForClose;
|
|
1402
|
-
if (trade && strategyName) {
|
|
1403
|
-
try {
|
|
1404
|
-
onRuntimeClose?.({
|
|
1405
|
-
userName,
|
|
1406
|
-
strategy: strategyName,
|
|
1407
|
-
openedByStrategy: trade.strategy,
|
|
1408
|
-
symbol,
|
|
1409
|
-
direction: trade.direction,
|
|
1410
|
-
code: decision.code,
|
|
1411
|
-
orderId: trade.orderId,
|
|
1412
|
-
signalId: trade.signalId,
|
|
1413
|
-
qty: trade.qty,
|
|
1414
|
-
entryPrice: trade.entryPrice,
|
|
1415
|
-
entryTimestamp: trade.entryTimestamp,
|
|
1416
|
-
exitPrice: closedTrade?.exitPrice ?? decision.closePlan.price,
|
|
1417
|
-
exitTimestamp: closedTrade?.exitTimestamp ?? decision.closePlan.timestamp,
|
|
1418
|
-
closedPnl: closedTrade?.closedPnl ?? trade.closedPnl ?? null,
|
|
1419
|
-
exitType: closedTrade?.exitType ?? "exit"
|
|
1420
|
-
});
|
|
1421
|
-
} catch (notificationError) {
|
|
1422
|
-
logger3.error(
|
|
1423
|
-
"runtime close notification error: %s %s",
|
|
1424
|
-
symbol,
|
|
1425
|
-
notificationError
|
|
1426
|
-
);
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
} catch (err) {
|
|
1430
|
-
await onRuntimeError?.({
|
|
1431
|
-
stage: "closePosition",
|
|
1432
|
-
error: err,
|
|
1433
|
-
decision,
|
|
1434
|
-
market
|
|
1435
|
-
});
|
|
1436
|
-
logger3.error("close order error: %s %s", symbol, err);
|
|
1437
|
-
return "ORDER_ERROR";
|
|
1438
|
-
}
|
|
1439
|
-
return decision.code;
|
|
1440
|
-
};
|
|
1441
|
-
var handleProtectDecision = async ({
|
|
1442
|
-
connector,
|
|
1443
|
-
symbol,
|
|
1444
|
-
decision,
|
|
1445
|
-
market,
|
|
1446
|
-
onRuntimeError
|
|
1447
|
-
}) => {
|
|
1448
|
-
try {
|
|
1449
|
-
await updatePositionProtection({
|
|
1450
|
-
connector,
|
|
1451
|
-
symbol,
|
|
1452
|
-
direction: decision.protectPlan.direction,
|
|
1453
|
-
takeProfits: decision.protectPlan.takeProfits ?? [],
|
|
1454
|
-
stopLossPrice: decision.protectPlan.stopLossPrice ?? null
|
|
1455
|
-
});
|
|
1456
|
-
} catch (err) {
|
|
1457
|
-
await onRuntimeError?.({
|
|
1458
|
-
stage: "protectPosition",
|
|
1459
|
-
error: err,
|
|
1460
|
-
decision,
|
|
1461
|
-
market
|
|
1462
|
-
});
|
|
1463
|
-
logger3.error("protect position error: %s %s", symbol, err);
|
|
1464
|
-
return "ORDER_ERROR";
|
|
1465
|
-
}
|
|
1466
|
-
return decision.code;
|
|
1467
|
-
};
|
|
1468
|
-
var executeEntryDecision = async ({
|
|
1469
|
-
connector,
|
|
1470
|
-
symbol,
|
|
1471
|
-
decision,
|
|
1472
|
-
runtime,
|
|
1473
|
-
manifest,
|
|
1474
|
-
hookCtx,
|
|
1475
|
-
market,
|
|
1476
|
-
entry,
|
|
1477
|
-
policy,
|
|
1478
|
-
ml,
|
|
1479
|
-
ai,
|
|
1480
|
-
recordRuntimeJournal,
|
|
1481
|
-
invokeStageHooks,
|
|
1482
|
-
notifyRuntimeError
|
|
1483
|
-
}) => {
|
|
1484
|
-
const signal = decision.signal;
|
|
1485
|
-
const beforePlaceOrder = async () => {
|
|
1486
|
-
await invokeStageHooks(
|
|
1487
|
-
"beforePlaceOrder",
|
|
1488
|
-
manifest?.hooks?.beforePlaceOrder,
|
|
1489
|
-
{
|
|
1490
|
-
ctx: hookCtx,
|
|
1491
|
-
market,
|
|
1492
|
-
decision,
|
|
1493
|
-
entry,
|
|
1494
|
-
policy,
|
|
1495
|
-
ml,
|
|
1496
|
-
ai
|
|
1497
|
-
},
|
|
1498
|
-
{ decision, entry, market }
|
|
1499
|
-
);
|
|
1500
|
-
try {
|
|
1501
|
-
await runtime.beforePlaceOrder?.();
|
|
1502
|
-
} catch (error) {
|
|
1503
|
-
await notifyRuntimeError({
|
|
1504
|
-
stage: "runtime.beforePlaceOrder",
|
|
1505
|
-
error,
|
|
1506
|
-
decision,
|
|
1507
|
-
entry,
|
|
1508
|
-
market
|
|
1509
|
-
});
|
|
1510
|
-
throw error;
|
|
1511
|
-
}
|
|
1512
|
-
};
|
|
1513
|
-
try {
|
|
1514
|
-
if (signal) {
|
|
1515
|
-
await executeEntryOrder({
|
|
1516
|
-
connector,
|
|
1517
|
-
userName: hookCtx.userName,
|
|
1518
|
-
symbol,
|
|
1519
|
-
direction: decision.entryContext.direction,
|
|
1520
|
-
qty: decision.orderPlan.qty,
|
|
1521
|
-
currentPrice: decision.entryContext.prices.currentPrice,
|
|
1522
|
-
timestamp: decision.entryContext.timestamp,
|
|
1523
|
-
takeProfits: decision.orderPlan.takeProfits,
|
|
1524
|
-
stopLossPrice: decision.orderPlan.stopLossPrice,
|
|
1525
|
-
positionIntent: decision.orderPlan.positionIntent,
|
|
1526
|
-
...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
|
|
1527
|
-
signal,
|
|
1528
|
-
beforePlaceOrder,
|
|
1529
|
-
recordRuntimeTrade: recordRuntimeJournal
|
|
1530
|
-
});
|
|
1531
|
-
await invokeStageHooks(
|
|
1532
|
-
"afterPlaceOrder",
|
|
1533
|
-
manifest?.hooks?.afterPlaceOrder,
|
|
1534
|
-
{
|
|
1535
|
-
ctx: hookCtx,
|
|
1536
|
-
market,
|
|
1537
|
-
decision,
|
|
1538
|
-
entry,
|
|
1539
|
-
policy,
|
|
1540
|
-
ml,
|
|
1541
|
-
ai,
|
|
1542
|
-
order: {
|
|
1543
|
-
result: signal
|
|
1544
|
-
}
|
|
1545
|
-
},
|
|
1546
|
-
{ decision, entry, market }
|
|
1547
|
-
);
|
|
1548
|
-
return signal;
|
|
1549
|
-
}
|
|
1550
|
-
await beforePlaceOrder();
|
|
1551
|
-
const arrivalSnapshot = await getOrderArrivalSnapshot({
|
|
1552
|
-
connector,
|
|
1553
|
-
symbol
|
|
1554
|
-
});
|
|
1555
|
-
validateEntryProtectionAtArrival({
|
|
1556
|
-
direction: decision.entryContext.direction,
|
|
1557
|
-
signalPrice: decision.entryContext.prices.currentPrice,
|
|
1558
|
-
bid: arrivalSnapshot.bid,
|
|
1559
|
-
ask: arrivalSnapshot.ask,
|
|
1560
|
-
arrivalMid: arrivalSnapshot.arrivalMid,
|
|
1561
|
-
takeProfits: decision.orderPlan.takeProfits,
|
|
1562
|
-
stopLossPrice: decision.orderPlan.stopLossPrice
|
|
1563
|
-
});
|
|
1564
|
-
const orderPlaced = await connector.placeOrder({
|
|
1565
|
-
symbol,
|
|
1566
|
-
qty: decision.orderPlan.qty,
|
|
1567
|
-
price: decision.entryContext.prices.currentPrice,
|
|
1568
|
-
timestamp: decision.entryContext.timestamp,
|
|
1569
|
-
direction: decision.entryContext.direction,
|
|
1570
|
-
positionIntent: decision.orderPlan.positionIntent,
|
|
1571
|
-
...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {}
|
|
1572
|
-
});
|
|
1573
|
-
if (!orderPlaced) {
|
|
1574
|
-
throw new Error("PLACE_ORDER_FAILED");
|
|
1575
|
-
}
|
|
1576
|
-
try {
|
|
1577
|
-
await updatePositionProtection({
|
|
1578
|
-
connector,
|
|
1579
|
-
symbol,
|
|
1580
|
-
direction: decision.entryContext.direction,
|
|
1581
|
-
qty: decision.orderPlan.qty,
|
|
1582
|
-
takeProfits: decision.orderPlan.takeProfits,
|
|
1583
|
-
stopLossPrice: decision.orderPlan.stopLossPrice
|
|
1584
|
-
});
|
|
1585
|
-
} catch (error) {
|
|
1586
|
-
await connector.closePosition({
|
|
1587
|
-
symbol,
|
|
1588
|
-
price: decision.entryContext.prices.currentPrice,
|
|
1589
|
-
timestamp: decision.entryContext.timestamp,
|
|
1590
|
-
direction: decision.entryContext.direction
|
|
1591
|
-
});
|
|
1592
|
-
throw error;
|
|
1593
|
-
}
|
|
1594
|
-
await invokeStageHooks(
|
|
1595
|
-
"afterPlaceOrder",
|
|
1596
|
-
manifest?.hooks?.afterPlaceOrder,
|
|
1597
|
-
{
|
|
1598
|
-
ctx: hookCtx,
|
|
1599
|
-
market,
|
|
1600
|
-
decision,
|
|
1601
|
-
entry,
|
|
1602
|
-
policy,
|
|
1603
|
-
ml,
|
|
1604
|
-
ai,
|
|
1605
|
-
order: {
|
|
1606
|
-
result: decision.code
|
|
1607
|
-
}
|
|
1608
|
-
},
|
|
1609
|
-
{ decision, entry, market }
|
|
1610
|
-
);
|
|
1611
|
-
} catch (err) {
|
|
1612
|
-
if (signal) {
|
|
1613
|
-
signal.orderStatus = "failed";
|
|
1614
|
-
if (typeof signal.orderFailureReason !== "string" || !signal.orderFailureReason.trim()) {
|
|
1615
|
-
signal.orderFailureReason = typeof err?.message === "string" && err.message.trim() ? err.message.trim() : void 0;
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
await notifyRuntimeError({
|
|
1619
|
-
stage: "placeOrder",
|
|
1620
|
-
error: err,
|
|
1621
|
-
decision,
|
|
1622
|
-
entry,
|
|
1623
|
-
market
|
|
1624
|
-
});
|
|
1625
|
-
if (err?.message === BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY) {
|
|
1626
|
-
logger3.warn("order warning: %s %s", symbol, err);
|
|
1627
|
-
} else {
|
|
1628
|
-
logger3.error("order error: %s %s", symbol, err);
|
|
1629
|
-
}
|
|
1630
|
-
return signal ?? "ORDER_ERROR";
|
|
1631
|
-
}
|
|
1632
|
-
return signal ?? decision.code;
|
|
1633
|
-
};
|
|
1634
|
-
var createStrategyRuntime = ({
|
|
1635
|
-
strategyName,
|
|
1636
|
-
defaults,
|
|
1637
|
-
createCore,
|
|
1638
|
-
manifest: staticManifest,
|
|
1639
|
-
strategyDirectory,
|
|
1640
|
-
detectorKey,
|
|
1641
|
-
detectorNoSignalSkipReason
|
|
1642
|
-
}) => {
|
|
1643
|
-
const projectRoot = getTradejsProjectCwd();
|
|
1644
|
-
const resolveManifest = (name) => {
|
|
1645
|
-
if (!name) {
|
|
1646
|
-
return void 0;
|
|
1647
|
-
}
|
|
1648
|
-
if (staticManifest?.name === name) {
|
|
1649
|
-
return staticManifest;
|
|
1650
|
-
}
|
|
1651
|
-
return getStrategyManifest(name, projectRoot);
|
|
1652
|
-
};
|
|
1653
|
-
const loadPineScriptFile = createPineScriptLoader(
|
|
1654
|
-
strategyDirectory ? path.resolve(strategyDirectory) : path.resolve(
|
|
1655
|
-
projectRoot,
|
|
1656
|
-
"packages",
|
|
1657
|
-
"strategies",
|
|
1658
|
-
"src",
|
|
1659
|
-
strategyName
|
|
1660
|
-
)
|
|
1661
|
-
);
|
|
1662
|
-
const creator = async ({
|
|
1663
|
-
userName,
|
|
1664
|
-
connectorName,
|
|
1665
|
-
config: baseConfig,
|
|
1666
|
-
symbol,
|
|
1667
|
-
universe: requestedUniverse,
|
|
1668
|
-
assetClass,
|
|
1669
|
-
accountId: requestedAccountId,
|
|
1670
|
-
deploymentId: requestedDeploymentId,
|
|
1671
|
-
policyProfileId,
|
|
1672
|
-
runtimeConfigId,
|
|
1673
|
-
runtimeConfigSnapshot,
|
|
1674
|
-
data,
|
|
1675
|
-
btcData,
|
|
1676
|
-
ethData = [],
|
|
1677
|
-
btcBinanceData,
|
|
1678
|
-
btcCoinbaseData,
|
|
1679
|
-
backtestExecutionMarketData,
|
|
1680
|
-
connector,
|
|
1681
|
-
sharedIndicatorsReplayKey,
|
|
1682
|
-
sharedStrategyStateKey,
|
|
1683
|
-
onRuntimeClose
|
|
1684
|
-
}) => {
|
|
1685
|
-
const { config, isConfigFromBacktest } = await resolveStrategyConfig({
|
|
1686
|
-
strategyName,
|
|
1687
|
-
userName,
|
|
1688
|
-
symbol,
|
|
1689
|
-
baseConfig,
|
|
1690
|
-
defaults,
|
|
1691
|
-
runtimeConfigId,
|
|
1692
|
-
runtimeConfigSnapshot
|
|
1693
|
-
});
|
|
1694
|
-
const universe = requestedUniverse ?? connector.universe;
|
|
1695
|
-
const accountId = requestedAccountId ?? connector.accountId;
|
|
1696
|
-
const deploymentId = requestedDeploymentId ?? connector.deploymentId;
|
|
1697
|
-
const projectConfig = await loadTradejsConfig(projectRoot);
|
|
1698
|
-
const projectHooks = projectConfig.hooks;
|
|
1699
|
-
const env = String(config.ENV ?? "BACKTEST");
|
|
1700
|
-
const backtestPriceMode = config.BACKTEST_PRICE_MODE ?? "open";
|
|
1701
|
-
const backtestEntryDelayBars = env === "BACKTEST" ? resolveBacktestEntryDelayBars(config.BACKTEST_ENTRY_DELAY_BARS) : 0;
|
|
1702
|
-
const resolvedBacktestExecutionInterval = config.BACKTEST_EXECUTION_INTERVAL ?? backtestExecutionMarketData?.interval ?? resolveBacktestExecutionIntervalForPrimary(config.INTERVAL ?? "15");
|
|
1703
|
-
const backtestExecutionInterval = resolvedBacktestExecutionInterval == null ? null : String(resolvedBacktestExecutionInterval);
|
|
1704
|
-
const backtestExecutionIntervalLabel = backtestExecutionInterval == null ? void 0 : String(backtestExecutionInterval);
|
|
1705
|
-
const primaryIntervalMs = safeIntervalToMs(config.INTERVAL ?? "15");
|
|
1706
|
-
const backtestExecutionIntervalMs = safeIntervalToMs(
|
|
1707
|
-
backtestExecutionInterval
|
|
1708
|
-
);
|
|
1709
|
-
const backtestExecutionDelayMs = resolveBacktestExecutionDelayMs(
|
|
1710
|
-
config.BACKTEST_EXECUTION_DELAY_MS,
|
|
1711
|
-
backtestExecutionIntervalMs ?? BACKTEST_EXECUTION_DELAY_MS
|
|
1712
|
-
);
|
|
1713
|
-
const backtestExecutionCandleByTimestamp = backtestExecutionMarketData?.dataByTimestamp ?? buildCandleByTimestamp(backtestExecutionMarketData?.data);
|
|
1714
|
-
const backtestExecutionBtcCandleByTimestamp = backtestExecutionMarketData?.btcDataByTimestamp ?? buildCandleByTimestamp(backtestExecutionMarketData?.btcData);
|
|
1715
|
-
const canUseLowerBacktestExecution = env === "BACKTEST" && backtestEntryDelayBars > 0 && backtestExecutionIntervalMs != null && primaryIntervalMs != null && backtestExecutionIntervalMs < primaryIntervalMs;
|
|
1716
|
-
const recordRuntimeJournal = shouldRecordRuntimeJournal({ env, config });
|
|
1717
|
-
const strategyManifest = resolveManifest(strategyName);
|
|
1718
|
-
const requestedPolicyProfileId = policyProfileId ?? (typeof config.POLICY_PROFILE_ID === "string" ? config.POLICY_PROFILE_ID : void 0);
|
|
1719
|
-
const getPolicyProfile = (name = strategyName) => resolveStrategyPolicyProfile(resolveManifest(name), {
|
|
1720
|
-
profileId: requestedPolicyProfileId,
|
|
1721
|
-
universe,
|
|
1722
|
-
assetClass
|
|
1723
|
-
});
|
|
1724
|
-
const strategyPolicyProfile = getPolicyProfile();
|
|
1725
|
-
const indicatorPeriods = buildDefaultIndicatorPeriods(config);
|
|
1726
|
-
const hookBase = {
|
|
1727
|
-
connector,
|
|
1728
|
-
strategyName,
|
|
1729
|
-
userName,
|
|
1730
|
-
symbol,
|
|
1731
|
-
universe,
|
|
1732
|
-
assetClass,
|
|
1733
|
-
accountId,
|
|
1734
|
-
deploymentId,
|
|
1735
|
-
policyProfileId: strategyPolicyProfile?.id ?? requestedPolicyProfileId,
|
|
1736
|
-
strategyConfig: config,
|
|
1737
|
-
env,
|
|
1738
|
-
isConfigFromBacktest
|
|
1739
|
-
};
|
|
1740
|
-
const getHookCtx = (name = strategyName) => {
|
|
1741
|
-
const profile = getPolicyProfile(name);
|
|
1742
|
-
return buildHookCtx({
|
|
1743
|
-
...hookBase,
|
|
1744
|
-
strategyName: name,
|
|
1745
|
-
policyProfileId: profile?.id ?? requestedPolicyProfileId
|
|
1746
|
-
});
|
|
1747
|
-
};
|
|
1748
|
-
const getProjectHookList = (stage) => normalizeConfigHookList(projectHooks?.[stage]);
|
|
1749
|
-
const indicatorReplayKey = JSON.stringify({
|
|
1750
|
-
periods: indicatorPeriods,
|
|
1751
|
-
universe
|
|
1752
|
-
});
|
|
1753
|
-
const sharedReplayEnabled = canUseSharedReplayState({
|
|
1754
|
-
env,
|
|
1755
|
-
sharedReplayKey: sharedIndicatorsReplayKey
|
|
1756
|
-
});
|
|
1757
|
-
const indicatorSharedReplayKey = sharedReplayEnabled && sharedIndicatorsReplayKey ? `${sharedIndicatorsReplayKey}:indicators:${indicatorReplayKey}` : void 0;
|
|
1758
|
-
const strategyStateBaseKey = env === "CRON" && sharedStrategyStateKey ? sharedStrategyStateKey : sharedReplayEnabled ? sharedIndicatorsReplayKey : void 0;
|
|
1759
|
-
const strategySharedReplayKey = strategyStateBaseKey ? `${strategyStateBaseKey}:strategy:${strategyName}` : void 0;
|
|
1760
|
-
const notifyRuntimeError = async ({
|
|
1761
|
-
stage,
|
|
1762
|
-
error,
|
|
1763
|
-
decision,
|
|
1764
|
-
entry,
|
|
1765
|
-
market
|
|
1766
|
-
}) => {
|
|
1767
|
-
const errorStrategyName = decision?.kind === "entry" ? decision.entryContext.strategy : strategyName;
|
|
1768
|
-
const errorManifest = resolveManifest(errorStrategyName) ?? strategyManifest;
|
|
1769
|
-
const errorParams = {
|
|
1770
|
-
ctx: getHookCtx(errorStrategyName),
|
|
1771
|
-
market,
|
|
1772
|
-
decision,
|
|
1773
|
-
entry,
|
|
1774
|
-
error: {
|
|
1775
|
-
stage,
|
|
1776
|
-
cause: error
|
|
1777
|
-
}
|
|
1778
|
-
};
|
|
1779
|
-
for (const projectHook of getProjectHookList("onRuntimeError")) {
|
|
1780
|
-
try {
|
|
1781
|
-
await projectHook(errorParams);
|
|
1782
|
-
} catch (hookError) {
|
|
1783
|
-
logger3.error(
|
|
1784
|
-
"project hook onRuntimeError failed: %s %s",
|
|
1785
|
-
strategyName,
|
|
1786
|
-
hookError
|
|
1787
|
-
);
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
const onRuntimeError = errorManifest?.hooks?.onRuntimeError;
|
|
1791
|
-
if (!onRuntimeError) {
|
|
1792
|
-
return;
|
|
1793
|
-
}
|
|
1794
|
-
try {
|
|
1795
|
-
await onRuntimeError(errorParams);
|
|
1796
|
-
} catch (hookError) {
|
|
1797
|
-
logger3.error(
|
|
1798
|
-
"runtime hook onRuntimeError failed: %s %s",
|
|
1799
|
-
strategyName,
|
|
1800
|
-
hookError
|
|
1801
|
-
);
|
|
1802
|
-
}
|
|
1803
|
-
};
|
|
1804
|
-
const invokeHook = async (stage, hook, params, errorContext = {}) => {
|
|
1805
|
-
if (!hook) {
|
|
1806
|
-
return void 0;
|
|
1807
|
-
}
|
|
1808
|
-
try {
|
|
1809
|
-
return await hook(params);
|
|
1810
|
-
} catch (error) {
|
|
1811
|
-
logger3.error(
|
|
1812
|
-
'strategy hook "%s" failed for %s: %s',
|
|
1813
|
-
stage,
|
|
1814
|
-
strategyName,
|
|
1815
|
-
error
|
|
1816
|
-
);
|
|
1817
|
-
await notifyRuntimeError({
|
|
1818
|
-
stage,
|
|
1819
|
-
error,
|
|
1820
|
-
decision: errorContext.decision,
|
|
1821
|
-
entry: errorContext.entry,
|
|
1822
|
-
market: errorContext.market
|
|
1823
|
-
});
|
|
1824
|
-
return void 0;
|
|
1825
|
-
}
|
|
1826
|
-
};
|
|
1827
|
-
const invokeProjectHooks = async (stage, params, errorContext = {}) => {
|
|
1828
|
-
const results = [];
|
|
1829
|
-
for (const hook of getProjectHookList(stage)) {
|
|
1830
|
-
const result = await invokeHook(
|
|
1831
|
-
stage,
|
|
1832
|
-
hook,
|
|
1833
|
-
params,
|
|
1834
|
-
errorContext
|
|
1835
|
-
);
|
|
1836
|
-
if (result !== void 0) {
|
|
1837
|
-
results.push(result);
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
return results;
|
|
1841
|
-
};
|
|
1842
|
-
const invokeStageHooks = async (stage, hook, params, errorContext = {}) => {
|
|
1843
|
-
if (isConfigHookStage(stage)) {
|
|
1844
|
-
await invokeProjectHooks(stage, params, errorContext);
|
|
1845
|
-
}
|
|
1846
|
-
return invokeHook(stage, hook, params, errorContext);
|
|
1847
|
-
};
|
|
1848
|
-
const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
|
|
1849
|
-
const projectResults = await invokeProjectHooks(
|
|
1850
|
-
stage,
|
|
1851
|
-
params,
|
|
1852
|
-
errorContext
|
|
1853
|
-
);
|
|
1854
|
-
const projectBlock = projectResults.find(
|
|
1855
|
-
(result) => result?.allow === false
|
|
1856
|
-
);
|
|
1857
|
-
if (projectBlock?.allow === false) {
|
|
1858
|
-
return projectBlock;
|
|
1859
|
-
}
|
|
1860
|
-
return invokeHook(
|
|
1861
|
-
stage,
|
|
1862
|
-
hook,
|
|
1863
|
-
params,
|
|
1864
|
-
errorContext
|
|
1865
|
-
);
|
|
1866
|
-
};
|
|
1867
|
-
const applyProjectAfterCoreDecisionHooks = async ({
|
|
1868
|
-
hookCtx,
|
|
1869
|
-
market,
|
|
1870
|
-
decision
|
|
1871
|
-
}) => {
|
|
1872
|
-
let nextDecision = decision;
|
|
1873
|
-
for (const hook of getProjectHookList(
|
|
1874
|
-
"afterCoreDecision"
|
|
1875
|
-
)) {
|
|
1876
|
-
const result = await invokeHook(
|
|
1877
|
-
"afterCoreDecision",
|
|
1878
|
-
hook,
|
|
1879
|
-
{
|
|
1880
|
-
ctx: hookCtx,
|
|
1881
|
-
market,
|
|
1882
|
-
decision: nextDecision
|
|
1883
|
-
},
|
|
1884
|
-
{
|
|
1885
|
-
decision: nextDecision,
|
|
1886
|
-
market
|
|
1887
|
-
}
|
|
1888
|
-
);
|
|
1889
|
-
if (isStrategyDecision(result)) {
|
|
1890
|
-
nextDecision = result;
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
return nextDecision;
|
|
1894
|
-
};
|
|
1895
|
-
const applyProjectAfterBarDecisionHooks = async ({
|
|
1896
|
-
hookCtx,
|
|
1897
|
-
market,
|
|
1898
|
-
decision
|
|
1899
|
-
}) => {
|
|
1900
|
-
let nextDecision = decision;
|
|
1901
|
-
for (const hook of getProjectHookList(
|
|
1902
|
-
"afterBarDecision"
|
|
1903
|
-
)) {
|
|
1904
|
-
const result = await invokeHook(
|
|
1905
|
-
"afterBarDecision",
|
|
1906
|
-
hook,
|
|
1907
|
-
{
|
|
1908
|
-
ctx: hookCtx,
|
|
1909
|
-
market,
|
|
1910
|
-
decision: nextDecision
|
|
1911
|
-
},
|
|
1912
|
-
{
|
|
1913
|
-
decision: nextDecision,
|
|
1914
|
-
market
|
|
1915
|
-
}
|
|
1916
|
-
);
|
|
1917
|
-
if (isStrategyDecision(result)) {
|
|
1918
|
-
nextDecision = result;
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
return nextDecision;
|
|
1922
|
-
};
|
|
1923
|
-
const applyProjectOnBarHooks = async ({
|
|
1924
|
-
hookCtx,
|
|
1925
|
-
market
|
|
1926
|
-
}) => {
|
|
1927
|
-
for (const hook of getProjectHookList(
|
|
1928
|
-
"onBar"
|
|
1929
|
-
)) {
|
|
1930
|
-
const result = await invokeHook(
|
|
1931
|
-
"onBar",
|
|
1932
|
-
hook,
|
|
1933
|
-
{
|
|
1934
|
-
ctx: hookCtx,
|
|
1935
|
-
market
|
|
1936
|
-
},
|
|
1937
|
-
{
|
|
1938
|
-
market
|
|
1939
|
-
}
|
|
1940
|
-
);
|
|
1941
|
-
if (isStrategyDecision(result)) {
|
|
1942
|
-
return result;
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
return void 0;
|
|
1946
|
-
};
|
|
1947
|
-
const indicatorsState = createStrategyIndicatorsState({
|
|
1948
|
-
env,
|
|
1949
|
-
data,
|
|
1950
|
-
btcData,
|
|
1951
|
-
ethData,
|
|
1952
|
-
btcBinanceData,
|
|
1953
|
-
btcCoinbaseData,
|
|
1954
|
-
periods: indicatorPeriods,
|
|
1955
|
-
pluginRegistryScope: projectRoot,
|
|
1956
|
-
sharedReplayKey: indicatorSharedReplayKey,
|
|
1957
|
-
useBtcReference: universe === "crypto"
|
|
1958
|
-
});
|
|
1959
|
-
const coreContextRequirements = new Set(
|
|
1960
|
-
strategyManifest?.contextRequirements?.core ?? []
|
|
1961
|
-
);
|
|
1962
|
-
const strategyApi = createStrategyAPI({
|
|
1963
|
-
strategy: strategyName,
|
|
1964
|
-
symbol,
|
|
1965
|
-
interval: config.INTERVAL ?? "15",
|
|
1966
|
-
env,
|
|
1967
|
-
connector,
|
|
1968
|
-
cachedData: data,
|
|
1969
|
-
indicatorsState,
|
|
1970
|
-
isConfigFromBacktest,
|
|
1971
|
-
sharedReplayKey: strategySharedReplayKey,
|
|
1972
|
-
getSharedReplayState: getSharedStrategyReplayState,
|
|
1973
|
-
loadDecisionBaseContext: async ({
|
|
1974
|
-
baseContext,
|
|
1975
|
-
candle,
|
|
1976
|
-
symbol: decisionSymbol,
|
|
1977
|
-
interval: decisionInterval
|
|
1978
|
-
}) => {
|
|
1979
|
-
if (!baseContext) return void 0;
|
|
1980
|
-
if (!coreContextRequirements.has("hyperliquidWhales")) {
|
|
1981
|
-
return baseContext;
|
|
1982
|
-
}
|
|
1983
|
-
const hyperliquidWhales = await loadHyperliquidWhaleFlowContext({
|
|
1984
|
-
symbol: decisionSymbol,
|
|
1985
|
-
interval: decisionInterval,
|
|
1986
|
-
timestamp: candle.timestamp,
|
|
1987
|
-
env,
|
|
1988
|
-
useSeriesCache: env === "BACKTEST" || env === "PARITY"
|
|
1989
|
-
});
|
|
1990
|
-
if (!hyperliquidWhales) return baseContext;
|
|
1991
|
-
const participation = cloneWithPropertyDescriptors(
|
|
1992
|
-
baseContext.participation
|
|
1993
|
-
);
|
|
1994
|
-
Object.defineProperty(participation, "hyperliquidWhales", {
|
|
1995
|
-
configurable: true,
|
|
1996
|
-
enumerable: true,
|
|
1997
|
-
value: hyperliquidWhales,
|
|
1998
|
-
writable: true
|
|
1999
|
-
});
|
|
2000
|
-
const enrichedBaseContext = cloneWithPropertyDescriptors(baseContext);
|
|
2001
|
-
Object.defineProperty(enrichedBaseContext, "participation", {
|
|
2002
|
-
configurable: true,
|
|
2003
|
-
enumerable: true,
|
|
2004
|
-
value: participation,
|
|
2005
|
-
writable: true
|
|
2006
|
-
});
|
|
2007
|
-
return enrichedBaseContext;
|
|
2008
|
-
}
|
|
2009
|
-
});
|
|
2010
|
-
const core = await createCore({
|
|
2011
|
-
userName,
|
|
2012
|
-
symbol,
|
|
2013
|
-
config,
|
|
2014
|
-
isConfigFromBacktest,
|
|
2015
|
-
connector,
|
|
2016
|
-
data,
|
|
2017
|
-
btcData,
|
|
2018
|
-
ethData,
|
|
2019
|
-
loadPineScriptFile,
|
|
2020
|
-
strategyApi,
|
|
2021
|
-
indicatorsState,
|
|
2022
|
-
sharedReplayKey: strategySharedReplayKey,
|
|
2023
|
-
getSharedReplayState: getSharedStrategyReplayState
|
|
2024
|
-
});
|
|
2025
|
-
await invokeStageHooks("onInit", strategyManifest?.hooks?.onInit, {
|
|
2026
|
-
ctx: getHookCtx(),
|
|
2027
|
-
market: {
|
|
2028
|
-
data,
|
|
2029
|
-
btcData
|
|
2030
|
-
}
|
|
2031
|
-
});
|
|
2032
|
-
const appendCurrentMarketData = (candle, btcCandle, ethCandle) => {
|
|
2033
|
-
if (data[data.length - 1]?.timestamp !== candle.timestamp) {
|
|
2034
|
-
data.push(candle);
|
|
2035
|
-
}
|
|
2036
|
-
if (universe === "crypto" && btcData[btcData.length - 1]?.timestamp !== btcCandle.timestamp) {
|
|
2037
|
-
btcData.push(btcCandle);
|
|
2038
|
-
}
|
|
2039
|
-
if (universe === "crypto" && ethCandle && ethData[ethData.length - 1]?.timestamp !== ethCandle.timestamp) {
|
|
2040
|
-
ethData.push(ethCandle);
|
|
2041
|
-
}
|
|
2042
|
-
};
|
|
2043
|
-
const resolveEthCandle = (candle, ethCandle) => {
|
|
2044
|
-
if (ethCandle?.timestamp === candle.timestamp) {
|
|
2045
|
-
return ethCandle;
|
|
2046
|
-
}
|
|
2047
|
-
const alignedEthCandle = ethData[data.length - 1];
|
|
2048
|
-
if (alignedEthCandle?.timestamp === candle.timestamp) {
|
|
2049
|
-
return alignedEthCandle;
|
|
2050
|
-
}
|
|
2051
|
-
const latestEthCandle = ethData[ethData.length - 1];
|
|
2052
|
-
if (latestEthCandle?.timestamp === candle.timestamp) {
|
|
2053
|
-
return latestEthCandle;
|
|
2054
|
-
}
|
|
2055
|
-
return void 0;
|
|
2056
|
-
};
|
|
2057
|
-
const resolveBacktestExecutionCandle = (candle, btcCandle) => {
|
|
2058
|
-
if (!BACKTEST_LOWER_TIMEFRAME_EXECUTION_ENABLED) {
|
|
2059
|
-
return {
|
|
2060
|
-
candle,
|
|
2061
|
-
btcCandle,
|
|
2062
|
-
source: "primary_timeframe",
|
|
2063
|
-
requestedExecutionTimestamp: candle.timestamp,
|
|
2064
|
-
executionInterval: String(config.INTERVAL ?? "15"),
|
|
2065
|
-
executionDelayMs: 0,
|
|
2066
|
-
primaryExecutionTimestamp: candle.timestamp
|
|
2067
|
-
};
|
|
2068
|
-
}
|
|
2069
|
-
const requestedExecutionTimestamp = candle.timestamp + backtestExecutionDelayMs;
|
|
2070
|
-
const primaryExecutionTimestamp = candle.timestamp;
|
|
2071
|
-
if (!canUseLowerBacktestExecution || primaryIntervalMs == null) {
|
|
2072
|
-
return {
|
|
2073
|
-
source: "lower_timeframe",
|
|
2074
|
-
requestedExecutionTimestamp,
|
|
2075
|
-
executionInterval: backtestExecutionIntervalLabel,
|
|
2076
|
-
executionDelayMs: backtestExecutionDelayMs,
|
|
2077
|
-
primaryExecutionTimestamp,
|
|
2078
|
-
skipReason: "BACKTEST_LOWER_EXECUTION_UNAVAILABLE"
|
|
2079
|
-
};
|
|
2080
|
-
}
|
|
2081
|
-
if (requestedExecutionTimestamp >= candle.timestamp + primaryIntervalMs) {
|
|
2082
|
-
return {
|
|
2083
|
-
source: "lower_timeframe",
|
|
2084
|
-
requestedExecutionTimestamp,
|
|
2085
|
-
executionInterval: backtestExecutionIntervalLabel,
|
|
2086
|
-
executionDelayMs: backtestExecutionDelayMs,
|
|
2087
|
-
primaryExecutionTimestamp,
|
|
2088
|
-
skipReason: "BACKTEST_LOWER_EXECUTION_DELAY_OUT_OF_BAR"
|
|
2089
|
-
};
|
|
2090
|
-
}
|
|
2091
|
-
const lowerCandle = backtestExecutionCandleByTimestamp.get(
|
|
2092
|
-
requestedExecutionTimestamp
|
|
2093
|
-
);
|
|
2094
|
-
const lowerBtcCandle = backtestExecutionBtcCandleByTimestamp.get(
|
|
2095
|
-
requestedExecutionTimestamp
|
|
2096
|
-
);
|
|
2097
|
-
if (lowerCandle && lowerBtcCandle) {
|
|
2098
|
-
return {
|
|
2099
|
-
candle: lowerCandle,
|
|
2100
|
-
btcCandle: lowerBtcCandle,
|
|
2101
|
-
source: "lower_timeframe",
|
|
2102
|
-
requestedExecutionTimestamp,
|
|
2103
|
-
executionInterval: backtestExecutionIntervalLabel,
|
|
2104
|
-
executionDelayMs: backtestExecutionDelayMs,
|
|
2105
|
-
primaryExecutionTimestamp
|
|
2106
|
-
};
|
|
2107
|
-
}
|
|
2108
|
-
return {
|
|
2109
|
-
source: "lower_timeframe",
|
|
2110
|
-
requestedExecutionTimestamp,
|
|
2111
|
-
executionInterval: backtestExecutionIntervalLabel,
|
|
2112
|
-
executionDelayMs: backtestExecutionDelayMs,
|
|
2113
|
-
primaryExecutionTimestamp,
|
|
2114
|
-
skipReason: !lowerCandle ? "BACKTEST_LOWER_EXECUTION_CANDLE_MISSING" : "BACKTEST_LOWER_EXECUTION_BTC_CANDLE_MISSING"
|
|
2115
|
-
};
|
|
2116
|
-
};
|
|
2117
|
-
let pendingBacktestEntry = null;
|
|
2118
|
-
const flushPendingBacktestEntry = async (candle, btcCandle, ethCandle) => {
|
|
2119
|
-
if (!pendingBacktestEntry) {
|
|
2120
|
-
return void 0;
|
|
2121
|
-
}
|
|
2122
|
-
appendCurrentMarketData(candle, btcCandle, ethCandle);
|
|
2123
|
-
const resolvedEthCandle = resolveEthCandle(candle, ethCandle);
|
|
2124
|
-
indicatorsState.setCurrentBar(candle, btcCandle, resolvedEthCandle);
|
|
2125
|
-
pendingBacktestEntry.delayBarsRemaining -= 1;
|
|
2126
|
-
if (pendingBacktestEntry.delayBarsRemaining > 0) {
|
|
2127
|
-
return `BACKTEST_ENTRY_DELAY_PENDING:${pendingBacktestEntry.delayBarsRemaining}`;
|
|
2128
|
-
}
|
|
2129
|
-
const pending = pendingBacktestEntry;
|
|
2130
|
-
pendingBacktestEntry = null;
|
|
2131
|
-
const executionCandleResolution = resolveBacktestExecutionCandle(
|
|
2132
|
-
candle,
|
|
2133
|
-
btcCandle
|
|
2134
|
-
);
|
|
2135
|
-
const execution = applyBacktestDelayedEntryExecution({
|
|
2136
|
-
decision: pending.decision,
|
|
2137
|
-
execution: executionCandleResolution,
|
|
2138
|
-
backtestPriceMode: executionCandleResolution.source === "primary_timeframe" ? "open" : backtestPriceMode,
|
|
2139
|
-
delayBars: pending.delayBars
|
|
2140
|
-
});
|
|
2141
|
-
if (execution.skipReason) {
|
|
2142
|
-
return pending.decision.signal ?? execution.skipReason;
|
|
2143
|
-
}
|
|
2144
|
-
if (!execution.executionCandle || !execution.btcExecutionCandle) {
|
|
2145
|
-
return pending.decision.signal ?? "BACKTEST_LOWER_EXECUTION_CANDLE_MISSING";
|
|
2146
|
-
}
|
|
2147
|
-
const market = {
|
|
2148
|
-
candle: execution.executionCandle,
|
|
2149
|
-
btcCandle: execution.btcExecutionCandle
|
|
2150
|
-
};
|
|
2151
|
-
const entry = buildHookEntry({
|
|
2152
|
-
decision: pending.decision,
|
|
2153
|
-
runtime: pending.runtime
|
|
2154
|
-
});
|
|
2155
|
-
return executeEntryDecision({
|
|
2156
|
-
connector,
|
|
2157
|
-
symbol,
|
|
2158
|
-
decision: pending.decision,
|
|
2159
|
-
runtime: pending.runtime,
|
|
2160
|
-
manifest: pending.manifest,
|
|
2161
|
-
hookCtx: pending.hookCtx,
|
|
2162
|
-
market,
|
|
2163
|
-
entry,
|
|
2164
|
-
policy: pending.policy,
|
|
2165
|
-
ml: pending.ml,
|
|
2166
|
-
ai: pending.ai,
|
|
2167
|
-
recordRuntimeJournal,
|
|
2168
|
-
invokeStageHooks,
|
|
2169
|
-
notifyRuntimeError
|
|
2170
|
-
});
|
|
2171
|
-
};
|
|
2172
|
-
const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
|
|
2173
|
-
appendCurrentMarketData(candle, btcCandle, options.ethCandle);
|
|
2174
|
-
const ethCandle = resolveEthCandle(candle, options.ethCandle);
|
|
2175
|
-
indicatorsState.setCurrentBar(candle, btcCandle, ethCandle);
|
|
2176
|
-
const delayedEntrySignal = await flushPendingBacktestEntry(
|
|
2177
|
-
candle,
|
|
2178
|
-
btcCandle,
|
|
2179
|
-
ethCandle
|
|
2180
|
-
);
|
|
2181
|
-
if (delayedEntrySignal) {
|
|
2182
|
-
return delayedEntrySignal;
|
|
2183
|
-
}
|
|
2184
|
-
const market = {
|
|
2185
|
-
candle,
|
|
2186
|
-
btcCandle
|
|
2187
|
-
};
|
|
2188
|
-
const onBarHookCtx = getHookCtx();
|
|
2189
|
-
const projectOnBarDecision = await applyProjectOnBarHooks({
|
|
2190
|
-
hookCtx: onBarHookCtx,
|
|
2191
|
-
market
|
|
2192
|
-
});
|
|
2193
|
-
let decision;
|
|
2194
|
-
let shouldInvokeAfterCoreDecisionHook = false;
|
|
2195
|
-
if (projectOnBarDecision) {
|
|
2196
|
-
decision = projectOnBarDecision;
|
|
2197
|
-
} else {
|
|
2198
|
-
const manifestOnBarDecision = await invokeHook(
|
|
2199
|
-
"onBar",
|
|
2200
|
-
strategyManifest?.hooks?.onBar,
|
|
2201
|
-
{
|
|
2202
|
-
ctx: onBarHookCtx,
|
|
2203
|
-
market
|
|
2204
|
-
},
|
|
2205
|
-
{ market }
|
|
2206
|
-
);
|
|
2207
|
-
if (isStrategyDecision(manifestOnBarDecision)) {
|
|
2208
|
-
decision = manifestOnBarDecision;
|
|
2209
|
-
} else {
|
|
2210
|
-
decision = options.coreDecisionOverride ?? await core(candle, btcCandle);
|
|
2211
|
-
shouldInvokeAfterCoreDecisionHook = true;
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
|
-
if (shouldInvokeAfterCoreDecisionHook) {
|
|
2215
|
-
const initialDecisionStrategyName = decision.kind === "entry" ? decision.entryContext.strategy : strategyName;
|
|
2216
|
-
decision = await applyProjectAfterCoreDecisionHooks({
|
|
2217
|
-
hookCtx: getHookCtx(initialDecisionStrategyName),
|
|
2218
|
-
market,
|
|
2219
|
-
decision
|
|
2220
|
-
});
|
|
2221
|
-
}
|
|
2222
|
-
const initialAfterBarDecisionStrategyName = decision.kind === "entry" ? decision.entryContext.strategy : strategyName;
|
|
2223
|
-
decision = await applyProjectAfterBarDecisionHooks({
|
|
2224
|
-
hookCtx: getHookCtx(initialAfterBarDecisionStrategyName),
|
|
2225
|
-
market,
|
|
2226
|
-
decision
|
|
2227
|
-
});
|
|
2228
|
-
const decisionStrategyName = decision.kind === "entry" ? decision.entryContext.strategy : strategyName;
|
|
2229
|
-
const decisionManifest = resolveManifest(decisionStrategyName) ?? strategyManifest;
|
|
2230
|
-
const decisionHookCtx = getHookCtx(decisionStrategyName);
|
|
2231
|
-
if (shouldInvokeAfterCoreDecisionHook) {
|
|
2232
|
-
await invokeHook(
|
|
2233
|
-
"afterCoreDecision",
|
|
2234
|
-
decisionManifest?.hooks?.afterCoreDecision,
|
|
2235
|
-
{
|
|
2236
|
-
ctx: decisionHookCtx,
|
|
2237
|
-
market,
|
|
2238
|
-
decision
|
|
2239
|
-
},
|
|
2240
|
-
{ decision, market }
|
|
2241
|
-
);
|
|
2242
|
-
}
|
|
2243
|
-
await invokeHook(
|
|
2244
|
-
"afterBarDecision",
|
|
2245
|
-
decisionManifest?.hooks?.afterBarDecision,
|
|
2246
|
-
{
|
|
2247
|
-
ctx: decisionHookCtx,
|
|
2248
|
-
market,
|
|
2249
|
-
decision
|
|
2250
|
-
},
|
|
2251
|
-
{ decision, market }
|
|
2252
|
-
);
|
|
2253
|
-
if (decision.kind === "skip") {
|
|
2254
|
-
await invokeStageHooks(
|
|
2255
|
-
"onSkip",
|
|
2256
|
-
decisionManifest?.hooks?.onSkip,
|
|
2257
|
-
{
|
|
2258
|
-
ctx: decisionHookCtx,
|
|
2259
|
-
market,
|
|
2260
|
-
decision
|
|
2261
|
-
},
|
|
2262
|
-
{ decision, market }
|
|
2263
|
-
);
|
|
2264
|
-
return decision.code;
|
|
2265
|
-
}
|
|
2266
|
-
const rawMakeOrdersEnabled = typeof config.MAKE_ORDERS === "boolean" ? config.MAKE_ORDERS : true;
|
|
2267
|
-
const makeOrdersEnabled = rawMakeOrdersEnabled && (env !== "PARITY" || isTestConnector(connector));
|
|
2268
|
-
if (decision.kind === "exit") {
|
|
2269
|
-
if (!makeOrdersEnabled) {
|
|
2270
|
-
return decision.code;
|
|
2271
|
-
}
|
|
2272
|
-
const closeGate = await invokeGateHooks(
|
|
2273
|
-
"beforeClosePosition",
|
|
2274
|
-
decisionManifest?.hooks?.beforeClosePosition,
|
|
2275
|
-
{
|
|
2276
|
-
ctx: decisionHookCtx,
|
|
2277
|
-
market,
|
|
2278
|
-
decision
|
|
2279
|
-
},
|
|
2280
|
-
{ decision, market }
|
|
2281
|
-
);
|
|
2282
|
-
if (closeGate?.allow === false) {
|
|
2283
|
-
return closeGate.reason ? `CLOSE_BLOCKED_BY_HOOK:${closeGate.reason}` : "CLOSE_BLOCKED_BY_HOOK";
|
|
2284
|
-
}
|
|
2285
|
-
return handleExitDecision({
|
|
2286
|
-
connector,
|
|
2287
|
-
userName: recordRuntimeJournal ? userName : void 0,
|
|
2288
|
-
strategyName,
|
|
2289
|
-
symbol,
|
|
2290
|
-
decision,
|
|
2291
|
-
market,
|
|
2292
|
-
onRuntimeClose,
|
|
2293
|
-
onRuntimeError: async ({
|
|
2294
|
-
stage,
|
|
2295
|
-
error,
|
|
2296
|
-
decision: exitDecision,
|
|
2297
|
-
market: errorMarket
|
|
2298
|
-
}) => {
|
|
2299
|
-
await notifyRuntimeError({
|
|
2300
|
-
stage,
|
|
2301
|
-
error,
|
|
2302
|
-
decision: exitDecision,
|
|
2303
|
-
market: errorMarket
|
|
2304
|
-
});
|
|
2305
|
-
}
|
|
2306
|
-
});
|
|
2307
|
-
}
|
|
2308
|
-
if (decision.kind === "protect") {
|
|
2309
|
-
if (!makeOrdersEnabled) {
|
|
2310
|
-
return decision.code;
|
|
2311
|
-
}
|
|
2312
|
-
return handleProtectDecision({
|
|
2313
|
-
connector,
|
|
2314
|
-
symbol,
|
|
2315
|
-
decision,
|
|
2316
|
-
market,
|
|
2317
|
-
onRuntimeError: async ({
|
|
2318
|
-
stage,
|
|
2319
|
-
error,
|
|
2320
|
-
decision: protectDecision,
|
|
2321
|
-
market: errorMarket
|
|
2322
|
-
}) => {
|
|
2323
|
-
await notifyRuntimeError({
|
|
2324
|
-
stage,
|
|
2325
|
-
error,
|
|
2326
|
-
decision: protectDecision,
|
|
2327
|
-
market: errorMarket
|
|
2328
|
-
});
|
|
2329
|
-
}
|
|
2330
|
-
});
|
|
2331
|
-
}
|
|
2332
|
-
const runtime = resolveEntryRuntimePolicy({
|
|
2333
|
-
decision,
|
|
2334
|
-
config,
|
|
2335
|
-
manifest: decisionManifest,
|
|
2336
|
-
policyProfile: getPolicyProfile(decisionStrategyName)
|
|
2337
|
-
});
|
|
2338
|
-
const signal = decision.signal;
|
|
2339
|
-
if (signal) {
|
|
2340
|
-
if (universe) signal.universe = universe;
|
|
2341
|
-
if (assetClass) signal.assetClass = assetClass;
|
|
2342
|
-
if (accountId) signal.accountId = accountId;
|
|
2343
|
-
if (deploymentId) signal.deploymentId = deploymentId;
|
|
2344
|
-
if (runtimeConfigId) {
|
|
2345
|
-
signal.runtimeConfigId = runtimeConfigId;
|
|
2346
|
-
if (runtimeConfigId !== "config") {
|
|
2347
|
-
signal.signalId = `${signal.signalId}:${runtimeConfigId}`;
|
|
2348
|
-
}
|
|
2349
|
-
}
|
|
2350
|
-
if (decisionHookCtx.policyProfileId) {
|
|
2351
|
-
signal.policyProfileId = decisionHookCtx.policyProfileId;
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
const entry = buildHookEntry({
|
|
2355
|
-
decision,
|
|
2356
|
-
runtime
|
|
2357
|
-
});
|
|
2358
|
-
let ml;
|
|
2359
|
-
if (signal) {
|
|
2360
|
-
try {
|
|
2361
|
-
await enrichSignalWithMl({
|
|
2362
|
-
signal,
|
|
2363
|
-
env,
|
|
2364
|
-
ml: runtime.ml
|
|
2365
|
-
});
|
|
2366
|
-
} catch (error) {
|
|
2367
|
-
await notifyRuntimeError({
|
|
2368
|
-
stage: "enrichSignalWithMl",
|
|
2369
|
-
error,
|
|
2370
|
-
decision,
|
|
2371
|
-
entry,
|
|
2372
|
-
market
|
|
2373
|
-
});
|
|
2374
|
-
throw error;
|
|
2375
|
-
}
|
|
2376
|
-
ml = buildMlHookContext({
|
|
2377
|
-
signal,
|
|
2378
|
-
env,
|
|
2379
|
-
ml: runtime.ml
|
|
2380
|
-
});
|
|
2381
|
-
await invokeStageHooks(
|
|
2382
|
-
"afterEnrichMl",
|
|
2383
|
-
decisionManifest?.hooks?.afterEnrichMl,
|
|
2384
|
-
{
|
|
2385
|
-
ctx: decisionHookCtx,
|
|
2386
|
-
market,
|
|
2387
|
-
decision,
|
|
2388
|
-
entry,
|
|
2389
|
-
ml
|
|
2390
|
-
},
|
|
2391
|
-
{ decision, entry, market }
|
|
2392
|
-
);
|
|
2393
|
-
}
|
|
2394
|
-
let quality;
|
|
2395
|
-
let ai;
|
|
2396
|
-
if (signal) {
|
|
2397
|
-
try {
|
|
2398
|
-
await enrichSignalWithMarketContextStages({
|
|
2399
|
-
signal,
|
|
2400
|
-
env,
|
|
2401
|
-
includeHyperliquidWhales: false
|
|
2402
|
-
});
|
|
2403
|
-
quality = await enrichSignalWithAi({
|
|
2404
|
-
signal,
|
|
2405
|
-
userName,
|
|
2406
|
-
symbol,
|
|
2407
|
-
direction: signal.direction,
|
|
2408
|
-
env,
|
|
2409
|
-
ai: runtime.ai
|
|
2410
|
-
});
|
|
2411
|
-
} catch (error) {
|
|
2412
|
-
await notifyRuntimeError({
|
|
2413
|
-
stage: "enrichSignalWithAi",
|
|
2414
|
-
error,
|
|
2415
|
-
decision,
|
|
2416
|
-
entry,
|
|
2417
|
-
market
|
|
2418
|
-
});
|
|
2419
|
-
throw error;
|
|
2420
|
-
}
|
|
2421
|
-
ai = buildAiHookContext({
|
|
2422
|
-
env,
|
|
2423
|
-
ai: runtime.ai,
|
|
2424
|
-
quality
|
|
2425
|
-
});
|
|
2426
|
-
await invokeStageHooks(
|
|
2427
|
-
"afterEnrichAi",
|
|
2428
|
-
decisionManifest?.hooks?.afterEnrichAi,
|
|
2429
|
-
{
|
|
2430
|
-
ctx: decisionHookCtx,
|
|
2431
|
-
market,
|
|
2432
|
-
decision,
|
|
2433
|
-
entry,
|
|
2434
|
-
ml: ml ?? buildMlHookContext({ signal, env, ml: runtime.ml }),
|
|
2435
|
-
ai
|
|
2436
|
-
},
|
|
2437
|
-
{ decision, entry, market }
|
|
2438
|
-
);
|
|
2439
|
-
}
|
|
2440
|
-
const minAiQuality = runtime.ai?.minQuality ?? 4;
|
|
2441
|
-
const aiEnabled = runtime.ai?.enabled !== false && runtime.ai != null;
|
|
2442
|
-
const policy = buildHookPolicy({
|
|
2443
|
-
quality,
|
|
2444
|
-
makeOrdersEnabled,
|
|
2445
|
-
minAiQuality
|
|
2446
|
-
});
|
|
2447
|
-
const shouldMakeOrder = shouldExecuteEntryDecision({
|
|
2448
|
-
makeOrdersEnabled,
|
|
2449
|
-
env,
|
|
2450
|
-
signal,
|
|
2451
|
-
ml,
|
|
2452
|
-
aiEnabled,
|
|
2453
|
-
quality,
|
|
2454
|
-
minAiQuality
|
|
2455
|
-
});
|
|
2456
|
-
if (!shouldMakeOrder) {
|
|
2457
|
-
if (signal) {
|
|
2458
|
-
signal.orderStatus = "skipped";
|
|
2459
|
-
signal.orderSkipReason = getEntrySkipReason({
|
|
2460
|
-
makeOrdersEnabled,
|
|
2461
|
-
env,
|
|
2462
|
-
ml,
|
|
2463
|
-
aiEnabled,
|
|
2464
|
-
quality,
|
|
2465
|
-
minAiQuality
|
|
2466
|
-
});
|
|
2467
|
-
}
|
|
2468
|
-
return signal ?? decision.code;
|
|
2469
|
-
}
|
|
2470
|
-
const entryGate = await invokeGateHooks(
|
|
2471
|
-
"beforeEntryGate",
|
|
2472
|
-
decisionManifest?.hooks?.beforeEntryGate,
|
|
2473
|
-
{
|
|
2474
|
-
ctx: decisionHookCtx,
|
|
2475
|
-
market,
|
|
2476
|
-
decision,
|
|
2477
|
-
entry,
|
|
2478
|
-
policy,
|
|
2479
|
-
ml,
|
|
2480
|
-
ai
|
|
2481
|
-
},
|
|
2482
|
-
{ decision, entry, market }
|
|
2483
|
-
);
|
|
2484
|
-
if (entryGate?.allow === false) {
|
|
2485
|
-
const skipReason = entryGate.reason ? `HOOK_BEFORE_ENTRY_GATE:${entryGate.reason}` : "HOOK_BEFORE_ENTRY_GATE";
|
|
2486
|
-
if (signal) {
|
|
2487
|
-
signal.orderStatus = "skipped";
|
|
2488
|
-
signal.orderSkipReason = skipReason;
|
|
2489
|
-
}
|
|
2490
|
-
return signal ?? skipReason;
|
|
2491
|
-
}
|
|
2492
|
-
if (backtestEntryDelayBars > 0) {
|
|
2493
|
-
pendingBacktestEntry = {
|
|
2494
|
-
delayBars: backtestEntryDelayBars,
|
|
2495
|
-
delayBarsRemaining: backtestEntryDelayBars,
|
|
2496
|
-
decision,
|
|
2497
|
-
runtime,
|
|
2498
|
-
manifest: decisionManifest,
|
|
2499
|
-
hookCtx: decisionHookCtx,
|
|
2500
|
-
policy,
|
|
2501
|
-
ml,
|
|
2502
|
-
ai
|
|
2503
|
-
};
|
|
2504
|
-
return `BACKTEST_ENTRY_DELAY_QUEUED:${backtestEntryDelayBars}`;
|
|
2505
|
-
}
|
|
2506
|
-
return executeEntryDecision({
|
|
2507
|
-
connector,
|
|
2508
|
-
symbol,
|
|
2509
|
-
decision,
|
|
2510
|
-
runtime,
|
|
2511
|
-
manifest: decisionManifest,
|
|
2512
|
-
hookCtx: decisionHookCtx,
|
|
2513
|
-
market,
|
|
2514
|
-
entry,
|
|
2515
|
-
policy,
|
|
2516
|
-
ml,
|
|
2517
|
-
ai,
|
|
2518
|
-
recordRuntimeJournal,
|
|
2519
|
-
invokeStageHooks,
|
|
2520
|
-
notifyRuntimeError
|
|
2521
|
-
});
|
|
2522
|
-
};
|
|
2523
|
-
const strategy = (async (candle, btcCandle, ethCandle) => runWithDecisionOverride(candle, btcCandle, { ethCandle }));
|
|
2524
|
-
strategy.__tradejsUpdateReferenceData = (params) => indicatorsState.updateReferenceData?.(params);
|
|
2525
|
-
strategy.__tradejsFlushBacktestDelayedEntry = flushPendingBacktestEntry;
|
|
2526
|
-
const resolvedDetectorKey = detectorKey?.(config);
|
|
2527
|
-
if (resolvedDetectorKey && detectorNoSignalSkipReason) {
|
|
2528
|
-
const canFastAdvanceDetectorNoSignal = env === "BACKTEST" && getProjectHookList("onBar").length === 0 && getProjectHookList("afterCoreDecision").length === 0 && getProjectHookList("afterBarDecision").length === 0 && getProjectHookList("onSkip").length === 0 && !strategyManifest?.hooks?.onBar && !strategyManifest?.hooks?.afterCoreDecision && !strategyManifest?.hooks?.afterBarDecision && !strategyManifest?.hooks?.onSkip;
|
|
2529
|
-
strategy.detectorFanoutKey = [strategyName, resolvedDetectorKey].join(
|
|
2530
|
-
":"
|
|
2531
|
-
);
|
|
2532
|
-
strategy.detectorNoSignalSkipReason = detectorNoSignalSkipReason;
|
|
2533
|
-
strategy.canFastAdvanceDetectorNoSignal = canFastAdvanceDetectorNoSignal;
|
|
2534
|
-
if (canFastAdvanceDetectorNoSignal) {
|
|
2535
|
-
strategy.advanceDetectorNoSignal = (candle, btcCandle, code) => {
|
|
2536
|
-
appendCurrentMarketData(candle, btcCandle);
|
|
2537
|
-
indicatorsState.setCurrentBar(
|
|
2538
|
-
candle,
|
|
2539
|
-
btcCandle,
|
|
2540
|
-
resolveEthCandle(candle)
|
|
2541
|
-
);
|
|
2542
|
-
return Promise.resolve(code);
|
|
2543
|
-
};
|
|
2544
|
-
}
|
|
2545
|
-
strategy.skipDetectorNoSignal = (candle, btcCandle, code) => runWithDecisionOverride(candle, btcCandle, {
|
|
2546
|
-
coreDecisionOverride: strategyApi.skip(code)
|
|
2547
|
-
});
|
|
2548
|
-
}
|
|
2549
|
-
return strategy;
|
|
2550
|
-
};
|
|
2551
|
-
if (detectorKey) {
|
|
2552
|
-
creator.detectorKey = detectorKey;
|
|
2553
|
-
}
|
|
2554
|
-
if (detectorNoSignalSkipReason) {
|
|
2555
|
-
creator.detectorNoSignalSkipReason = detectorNoSignalSkipReason;
|
|
2556
|
-
}
|
|
2557
|
-
return creator;
|
|
2558
|
-
};
|
|
2559
|
-
|
|
2560
59
|
// src/strategyHooks/closeOppositePositionsBeforeOpen.ts
|
|
2561
|
-
|
|
2562
|
-
import { logger
|
|
60
|
+
import _ from "lodash";
|
|
61
|
+
import { logger } from "@tradejs/infra/logger";
|
|
2563
62
|
var closeOppositePositionsBeforeOpen = async ({
|
|
2564
63
|
connector,
|
|
2565
64
|
entryContext
|
|
@@ -2573,7 +72,7 @@ var closeOppositePositionsBeforeOpen = async ({
|
|
|
2573
72
|
} = entryContext;
|
|
2574
73
|
const price = prices.currentPrice;
|
|
2575
74
|
try {
|
|
2576
|
-
|
|
75
|
+
logger.log(
|
|
2577
76
|
"info",
|
|
2578
77
|
"[%s] checking open positions before open: %s %s",
|
|
2579
78
|
strategyName,
|
|
@@ -2584,7 +83,7 @@ var closeOppositePositionsBeforeOpen = async ({
|
|
|
2584
83
|
const openPositions = (positions || []).filter(
|
|
2585
84
|
(item) => item && Number(item.qty) > 0
|
|
2586
85
|
);
|
|
2587
|
-
|
|
86
|
+
logger.log(
|
|
2588
87
|
"info",
|
|
2589
88
|
"[%s] open positions found: %s",
|
|
2590
89
|
strategyName,
|
|
@@ -2593,8 +92,8 @@ var closeOppositePositionsBeforeOpen = async ({
|
|
|
2593
92
|
const oppositePositions = openPositions.filter(
|
|
2594
93
|
(item) => item.symbol !== currentSymbol && item.direction !== currentDirection
|
|
2595
94
|
);
|
|
2596
|
-
if (
|
|
2597
|
-
|
|
95
|
+
if (_.isEmpty(oppositePositions)) {
|
|
96
|
+
logger.log(
|
|
2598
97
|
"info",
|
|
2599
98
|
"[%s] no opposite positions to close before open: %s",
|
|
2600
99
|
strategyName,
|
|
@@ -2603,7 +102,7 @@ var closeOppositePositionsBeforeOpen = async ({
|
|
|
2603
102
|
return;
|
|
2604
103
|
}
|
|
2605
104
|
for (const position of oppositePositions) {
|
|
2606
|
-
|
|
105
|
+
logger.log(
|
|
2607
106
|
"info",
|
|
2608
107
|
"[%s] closing opposite position: %s %s qty=%s",
|
|
2609
108
|
strategyName,
|
|
@@ -2618,14 +117,14 @@ var closeOppositePositionsBeforeOpen = async ({
|
|
|
2618
117
|
timestamp,
|
|
2619
118
|
direction: position.direction
|
|
2620
119
|
});
|
|
2621
|
-
|
|
120
|
+
logger.log(
|
|
2622
121
|
"info",
|
|
2623
122
|
"[%s] opposite position closed: %s",
|
|
2624
123
|
strategyName,
|
|
2625
124
|
position.symbol
|
|
2626
125
|
);
|
|
2627
126
|
} catch (err) {
|
|
2628
|
-
|
|
127
|
+
logger.log(
|
|
2629
128
|
"error",
|
|
2630
129
|
"[%s] failed to close opposite position: %s %s",
|
|
2631
130
|
strategyName,
|
|
@@ -2635,7 +134,7 @@ var closeOppositePositionsBeforeOpen = async ({
|
|
|
2635
134
|
}
|
|
2636
135
|
}
|
|
2637
136
|
} catch (err) {
|
|
2638
|
-
|
|
137
|
+
logger.log(
|
|
2639
138
|
"error",
|
|
2640
139
|
"[%s] failed to load open positions before open: %s %s",
|
|
2641
140
|
strategyName,
|
|
@@ -2847,7 +346,7 @@ var createMoveStopToBreakEvenOnBarHook = ({
|
|
|
2847
346
|
var createMoveStopToBreakEvenAfterCoreDecisionHook = createMoveStopToBreakEvenOnBarHook;
|
|
2848
347
|
|
|
2849
348
|
// src/signalsHooks/closeAllPositionsOnGlobalProfitBeforeSignals.ts
|
|
2850
|
-
import { logger as
|
|
349
|
+
import { logger as logger2 } from "@tradejs/infra/logger";
|
|
2851
350
|
var createCloseAllOnGlobalProfitBeforeSignalsHook = ({
|
|
2852
351
|
getStrategyDefaultConfig = () => void 0,
|
|
2853
352
|
profitRiskMultiplier = DEFAULT_GLOBAL_UNREALIZED_PNL_TRIGGER_RISK_MULTIPLIER
|
|
@@ -2886,7 +385,7 @@ var createCloseAllOnGlobalProfitBeforeSignalsHook = ({
|
|
|
2886
385
|
if (!Number.isFinite(unrealizedPnlThreshold) || unrealizedPnlThreshold <= 0 || totalUnrealizedPnl < unrealizedPnlThreshold) {
|
|
2887
386
|
return;
|
|
2888
387
|
}
|
|
2889
|
-
|
|
388
|
+
logger2.info(
|
|
2890
389
|
"closing all positions before signals by global unrealized pnl threshold: totalPnl=%s threshold=%s positions=%s",
|
|
2891
390
|
totalUnrealizedPnl,
|
|
2892
391
|
unrealizedPnlThreshold,
|
|
@@ -2912,7 +411,7 @@ var createCloseAllOnGlobalProfitBeforeSignalsHook = ({
|
|
|
2912
411
|
];
|
|
2913
412
|
});
|
|
2914
413
|
if (failedClosures.length) {
|
|
2915
|
-
|
|
414
|
+
logger2.warn(
|
|
2916
415
|
"close-all before signals hook could not confirm closures for %s",
|
|
2917
416
|
failedClosures.join(", ")
|
|
2918
417
|
);
|