@tradejs/node 1.0.9 → 1.0.10
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.d.mts +4 -1
- package/dist/ai.d.ts +4 -1
- package/dist/ai.js +601 -93
- package/dist/ai.mjs +5 -3
- package/dist/backtest.d.mts +26 -2
- package/dist/backtest.d.ts +26 -2
- package/dist/backtest.js +3368 -485
- package/dist/backtest.mjs +1839 -269
- package/dist/chunk-37VNDZVX.mjs +1040 -0
- package/dist/chunk-IUZML4RK.mjs +1136 -0
- package/dist/{chunk-WGOYR6AB.mjs → chunk-QVSMINLG.mjs} +1 -1
- package/dist/{chunk-JMDYEKIO.mjs → chunk-V3YMKE4I.mjs} +1 -1
- package/dist/{chunk-JU77QVJ3.mjs → chunk-WS5DYEVZ.mjs} +59 -5
- package/dist/cli.d.mts +13 -3
- package/dist/cli.d.ts +13 -3
- package/dist/cli.js +1016 -234
- package/dist/cli.mjs +338 -65
- package/dist/connectors.js +59 -5
- package/dist/connectors.mjs +2 -2
- package/dist/registry.js +59 -5
- package/dist/registry.mjs +2 -2
- package/dist/strategies.d.mts +29 -8
- package/dist/strategies.d.ts +29 -8
- package/dist/strategies.js +3417 -1285
- package/dist/strategies.mjs +892 -112
- package/package.json +6 -6
- package/dist/chunk-2JKX3DM7.mjs +0 -619
- package/dist/chunk-JRRG3YQG.mjs +0 -154
package/dist/strategies.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildMlPayload,
|
|
3
|
+
enrichSignalWithBinanceMarketContext,
|
|
4
|
+
enrichSignalWithCoinMarketCapContext,
|
|
3
5
|
enrichSignalWithDerivativesContext
|
|
4
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-37VNDZVX.mjs";
|
|
5
7
|
import {
|
|
6
8
|
require_lodash
|
|
7
9
|
} from "./chunk-KZDHZ56N.mjs";
|
|
@@ -13,14 +15,16 @@ import {
|
|
|
13
15
|
buildAiPayload,
|
|
14
16
|
buildAiPrompts,
|
|
15
17
|
buildAiSystemPrompt,
|
|
18
|
+
buildCompactAiIndicatorsSnapshot,
|
|
16
19
|
ensureAiStrategyPluginsLoaded,
|
|
17
20
|
getDeterministicAiGateContext,
|
|
18
21
|
getOpenRouterModelKwargs,
|
|
19
22
|
resetAiRuntimeCache,
|
|
23
|
+
resolveStrategyPolicyProfile,
|
|
20
24
|
runAiPrompt,
|
|
21
25
|
runAiPromptLocal,
|
|
22
26
|
trimSeriesDeep
|
|
23
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-IUZML4RK.mjs";
|
|
24
28
|
import {
|
|
25
29
|
createPineScriptLoader
|
|
26
30
|
} from "./chunk-H6LIYHU4.mjs";
|
|
@@ -37,11 +41,11 @@ import {
|
|
|
37
41
|
registerStrategyEntries,
|
|
38
42
|
resetStrategyRegistryCache,
|
|
39
43
|
strategies
|
|
40
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-QVSMINLG.mjs";
|
|
41
45
|
import {
|
|
42
46
|
getTradejsProjectCwd,
|
|
43
47
|
loadTradejsConfig
|
|
44
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-WS5DYEVZ.mjs";
|
|
45
49
|
import {
|
|
46
50
|
__toESM
|
|
47
51
|
} from "./chunk-6DZX6EAA.mjs";
|
|
@@ -51,18 +55,24 @@ export * from "@tradejs/core/strategies";
|
|
|
51
55
|
|
|
52
56
|
// src/strategyRuntime.ts
|
|
53
57
|
import path from "path";
|
|
54
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
BACKTEST_EXECUTION_DELAY_MS,
|
|
60
|
+
BACKTEST_EXECUTION_INTERVAL
|
|
61
|
+
} from "@tradejs/core/constants";
|
|
62
|
+
import { intervalToMs } from "@tradejs/core/data";
|
|
55
63
|
import {
|
|
56
64
|
buildDefaultIndicatorPeriods,
|
|
65
|
+
calculateRiskRatio,
|
|
57
66
|
createStrategyAPI,
|
|
58
|
-
createStrategyIndicatorsState
|
|
67
|
+
createStrategyIndicatorsState,
|
|
68
|
+
getSharedStrategyReplayState,
|
|
69
|
+
resolveBacktestExecutionPrice
|
|
59
70
|
} from "@tradejs/core/strategies";
|
|
60
|
-
import { getTimestamp } from "@tradejs/core/time";
|
|
61
71
|
import { logger as logger3 } from "@tradejs/infra/logger";
|
|
62
72
|
|
|
63
73
|
// src/strategyHelpers/runtime.ts
|
|
64
74
|
import { logger as logger2 } from "@tradejs/infra/logger";
|
|
65
|
-
import {
|
|
75
|
+
import { FEE_PERCENT } from "@tradejs/core/constants";
|
|
66
76
|
import {
|
|
67
77
|
buildMlFeatures,
|
|
68
78
|
buildMlTrainingRow,
|
|
@@ -73,10 +83,18 @@ import {
|
|
|
73
83
|
// src/runtimeJournal.ts
|
|
74
84
|
import { randomUUID } from "crypto";
|
|
75
85
|
import { TTL_1M } from "@tradejs/core/constants";
|
|
86
|
+
import { getRuntimeStorageDayKey } from "@tradejs/core/time";
|
|
87
|
+
import { createRuntimeOrderLinkPrefix } from "@tradejs/core/trade";
|
|
76
88
|
import { logger } from "@tradejs/infra/logger";
|
|
77
|
-
import {
|
|
89
|
+
import {
|
|
90
|
+
delKey,
|
|
91
|
+
getData,
|
|
92
|
+
redisKeys,
|
|
93
|
+
setData,
|
|
94
|
+
setHashJsonField
|
|
95
|
+
} from "@tradejs/infra/redis";
|
|
78
96
|
var now = () => Date.now();
|
|
79
|
-
var
|
|
97
|
+
var toRandomOrderSuffix = () => randomUUID().replace(/-/g, "").slice(0, 12).toLowerCase();
|
|
80
98
|
var calculateClosedPnl = ({
|
|
81
99
|
direction,
|
|
82
100
|
entryPrice,
|
|
@@ -86,7 +104,13 @@ var calculateClosedPnl = ({
|
|
|
86
104
|
const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
|
|
87
105
|
return Number.isFinite(pnl) ? pnl : null;
|
|
88
106
|
};
|
|
89
|
-
var createRuntimeOrderId = () =>
|
|
107
|
+
var createRuntimeOrderId = (strategy) => {
|
|
108
|
+
const prefix = createRuntimeOrderLinkPrefix(strategy);
|
|
109
|
+
if (prefix === "tjs-") {
|
|
110
|
+
return `tjs-${randomUUID().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
|
|
111
|
+
}
|
|
112
|
+
return `${prefix}${toRandomOrderSuffix()}`;
|
|
113
|
+
};
|
|
90
114
|
var recordRuntimeTradeOpen = async (params) => {
|
|
91
115
|
const { userName } = params;
|
|
92
116
|
if (!userName) {
|
|
@@ -102,13 +126,21 @@ var recordRuntimeTradeOpen = async (params) => {
|
|
|
102
126
|
exitTimestamp: null,
|
|
103
127
|
lastSyncedAt: now()
|
|
104
128
|
};
|
|
129
|
+
const dayKey = getRuntimeStorageDayKey(record.entryTimestamp);
|
|
130
|
+
const runtimeScopeId = record.deploymentId ?? record.accountId;
|
|
105
131
|
try {
|
|
106
132
|
await Promise.all([
|
|
107
133
|
setData(redisKeys.runtimeTrade(userName, record.orderId), record, {
|
|
108
134
|
expire: 0
|
|
109
135
|
}),
|
|
136
|
+
setHashJsonField(
|
|
137
|
+
redisKeys.runtimeTradeBucket(userName, dayKey),
|
|
138
|
+
record.orderId,
|
|
139
|
+
record,
|
|
140
|
+
{ expire: 0 }
|
|
141
|
+
),
|
|
110
142
|
setData(
|
|
111
|
-
redisKeys.runtimeActiveTrade(userName, record.symbol),
|
|
143
|
+
redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
|
|
112
144
|
{ orderId: record.orderId },
|
|
113
145
|
{ expire: 0 }
|
|
114
146
|
)
|
|
@@ -122,13 +154,13 @@ var recordRuntimeTradeOpen = async (params) => {
|
|
|
122
154
|
}
|
|
123
155
|
return record;
|
|
124
156
|
};
|
|
125
|
-
var
|
|
126
|
-
const { userName, symbol,
|
|
157
|
+
var getActiveRuntimeTrade = async (params) => {
|
|
158
|
+
const { userName, symbol, accountId, deploymentId } = params;
|
|
127
159
|
if (!userName) {
|
|
128
160
|
return null;
|
|
129
161
|
}
|
|
130
162
|
const activeRef = await getData(
|
|
131
|
-
redisKeys.runtimeActiveTrade(userName, symbol),
|
|
163
|
+
redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
|
|
132
164
|
null
|
|
133
165
|
);
|
|
134
166
|
const orderId = String(activeRef?.orderId || "").trim();
|
|
@@ -140,9 +172,38 @@ var markRuntimeTradeClosed = async (params) => {
|
|
|
140
172
|
null
|
|
141
173
|
);
|
|
142
174
|
if (!existing) {
|
|
143
|
-
await delKey(
|
|
175
|
+
await delKey(
|
|
176
|
+
redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
|
|
177
|
+
);
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
return existing;
|
|
181
|
+
};
|
|
182
|
+
var markRuntimeTradeClosed = async (params) => {
|
|
183
|
+
const {
|
|
184
|
+
userName,
|
|
185
|
+
symbol,
|
|
186
|
+
strategy,
|
|
187
|
+
exitPrice,
|
|
188
|
+
exitTimestamp,
|
|
189
|
+
closedPnl,
|
|
190
|
+
exitType,
|
|
191
|
+
accountId,
|
|
192
|
+
deploymentId
|
|
193
|
+
} = params;
|
|
194
|
+
if (!userName) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const existing = await getActiveRuntimeTrade({
|
|
198
|
+
userName,
|
|
199
|
+
symbol,
|
|
200
|
+
accountId,
|
|
201
|
+
deploymentId
|
|
202
|
+
});
|
|
203
|
+
if (!existing) {
|
|
144
204
|
return null;
|
|
145
205
|
}
|
|
206
|
+
const orderId = existing.orderId;
|
|
146
207
|
if (strategy && existing.strategy !== strategy) {
|
|
147
208
|
return null;
|
|
148
209
|
}
|
|
@@ -161,14 +222,28 @@ var markRuntimeTradeClosed = async (params) => {
|
|
|
161
222
|
closedPnl: resolvedClosedPnl,
|
|
162
223
|
exitPrice: resolvedExitPrice,
|
|
163
224
|
exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
|
|
225
|
+
exitType: exitType ?? existing.exitType ?? null,
|
|
164
226
|
lastSyncedAt: now()
|
|
165
227
|
};
|
|
228
|
+
const dayKey = getRuntimeStorageDayKey(existing.entryTimestamp);
|
|
166
229
|
try {
|
|
167
230
|
await Promise.all([
|
|
168
231
|
setData(redisKeys.runtimeTrade(userName, orderId), next, {
|
|
169
232
|
expire: TTL_1M
|
|
170
233
|
}),
|
|
171
|
-
|
|
234
|
+
setHashJsonField(
|
|
235
|
+
redisKeys.runtimeTradeBucket(userName, dayKey),
|
|
236
|
+
orderId,
|
|
237
|
+
next,
|
|
238
|
+
{ expire: TTL_1M }
|
|
239
|
+
),
|
|
240
|
+
delKey(
|
|
241
|
+
redisKeys.runtimeActiveTrade(
|
|
242
|
+
userName,
|
|
243
|
+
symbol,
|
|
244
|
+
deploymentId ?? accountId
|
|
245
|
+
)
|
|
246
|
+
)
|
|
172
247
|
]);
|
|
173
248
|
} catch (error) {
|
|
174
249
|
logger.error(
|
|
@@ -207,26 +282,6 @@ var resolveAiQuality = (analysis, direction) => {
|
|
|
207
282
|
const aiApprovedCurrentTrade = analysis.direction === direction;
|
|
208
283
|
return aiApprovedCurrentTrade ? normalizedQuality : 0;
|
|
209
284
|
};
|
|
210
|
-
var resolveAiDecision = (analysis, direction, minQuality = 4) => {
|
|
211
|
-
const quality = resolveAiQuality(analysis, direction);
|
|
212
|
-
return quality != null && quality >= minQuality ? "approved" : "rejected";
|
|
213
|
-
};
|
|
214
|
-
var withGateComparison = ({
|
|
215
|
-
llmAnalysis,
|
|
216
|
-
gateAnalysis,
|
|
217
|
-
direction,
|
|
218
|
-
minQuality
|
|
219
|
-
}) => {
|
|
220
|
-
const gateDecision = resolveAiDecision(gateAnalysis, direction, minQuality);
|
|
221
|
-
const llmDecision = resolveAiDecision(llmAnalysis, direction, minQuality);
|
|
222
|
-
return {
|
|
223
|
-
...llmAnalysis,
|
|
224
|
-
gateAnalysis,
|
|
225
|
-
gateDecision,
|
|
226
|
-
llmDecision,
|
|
227
|
-
gateContradictsLlm: gateDecision !== llmDecision
|
|
228
|
-
};
|
|
229
|
-
};
|
|
230
285
|
var findReplayAiAnalysis = ({
|
|
231
286
|
signal,
|
|
232
287
|
direction,
|
|
@@ -274,7 +329,7 @@ var enrichSignalWithMl = async ({
|
|
|
274
329
|
const row = trimMlTrainingRowWindows(fullRow, 5);
|
|
275
330
|
const features = buildMlFeatures(row);
|
|
276
331
|
const mlResult = await fetchMlThreshold({
|
|
277
|
-
strategy,
|
|
332
|
+
strategy: ml.modelKey ?? strategy,
|
|
278
333
|
features,
|
|
279
334
|
threshold: ml.mlThreshold,
|
|
280
335
|
projectRoot: getTradejsProjectCwd()
|
|
@@ -306,32 +361,13 @@ var enrichSignalWithAi = async ({
|
|
|
306
361
|
return void 0;
|
|
307
362
|
}
|
|
308
363
|
if (ai?.mode === "gate") {
|
|
309
|
-
const
|
|
310
|
-
const { askAI: askAI2, runAiPromptLocal: runAiPromptLocal2 } = await import("./ai.mjs");
|
|
311
|
-
const gateAnalysis = await runAiPromptLocal2(signal);
|
|
364
|
+
const gateAnalysis = await runAiPromptLocal(signal);
|
|
312
365
|
const gateQuality = resolveAiQuality(gateAnalysis, direction);
|
|
313
|
-
|
|
314
|
-
const llmAnalysis = await askAI2(signal, { userName });
|
|
315
|
-
const comparedAnalysis = withGateComparison({
|
|
316
|
-
llmAnalysis,
|
|
317
|
-
gateAnalysis,
|
|
318
|
-
direction,
|
|
319
|
-
minQuality
|
|
320
|
-
});
|
|
321
|
-
signal.aiAnalysis = comparedAnalysis;
|
|
322
|
-
await setData2(
|
|
323
|
-
redisKeys2.analysis(signal.symbol, signal.signalId),
|
|
324
|
-
comparedAnalysis
|
|
325
|
-
);
|
|
326
|
-
} catch (err) {
|
|
327
|
-
logger2.error("AI analysis error: %s %s", symbol, formatAiError(err));
|
|
328
|
-
signal.aiAnalysis = gateAnalysis;
|
|
329
|
-
}
|
|
366
|
+
signal.aiAnalysis = gateAnalysis;
|
|
330
367
|
return gateQuality;
|
|
331
368
|
}
|
|
332
369
|
try {
|
|
333
|
-
const
|
|
334
|
-
const analysis = await askAI2(signal, { userName });
|
|
370
|
+
const analysis = await askAI(signal, { userName });
|
|
335
371
|
signal.aiAnalysis = analysis;
|
|
336
372
|
return resolveAiQuality(analysis, direction);
|
|
337
373
|
} catch (err) {
|
|
@@ -348,10 +384,77 @@ var enrichSignalWithMlAi = async ({
|
|
|
348
384
|
ml,
|
|
349
385
|
ai
|
|
350
386
|
}) => {
|
|
387
|
+
await enrichSignalWithBinanceMarketContext({ signal, env });
|
|
388
|
+
await enrichSignalWithCoinMarketCapContext({ signal, env });
|
|
351
389
|
await enrichSignalWithDerivativesContext({ signal, env });
|
|
352
390
|
await enrichSignalWithMl({ signal, env, ml });
|
|
353
391
|
return enrichSignalWithAi({ signal, userName, symbol, direction, env, ai });
|
|
354
392
|
};
|
|
393
|
+
var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
394
|
+
var getArrivalSnapshot = async ({
|
|
395
|
+
connector,
|
|
396
|
+
symbol
|
|
397
|
+
}) => {
|
|
398
|
+
if (typeof connector.getTopOfBookTicker !== "function") {
|
|
399
|
+
return {
|
|
400
|
+
arrivalSnapshotTime: Date.now(),
|
|
401
|
+
arrivalSource: "unavailable",
|
|
402
|
+
bid: null,
|
|
403
|
+
ask: null,
|
|
404
|
+
arrivalMid: null,
|
|
405
|
+
spreadBps: null
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
const ticker = await connector.getTopOfBookTicker(symbol);
|
|
410
|
+
const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
|
|
411
|
+
const bid = toFiniteNumberOrNull(ticker?.bidPrice);
|
|
412
|
+
const ask = toFiniteNumberOrNull(ticker?.askPrice);
|
|
413
|
+
const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
|
|
414
|
+
const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
|
|
415
|
+
return {
|
|
416
|
+
arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
|
|
417
|
+
arrivalSource: "top_of_book",
|
|
418
|
+
bid,
|
|
419
|
+
ask,
|
|
420
|
+
arrivalMid,
|
|
421
|
+
spreadBps
|
|
422
|
+
};
|
|
423
|
+
} catch (error) {
|
|
424
|
+
logger2.warn(
|
|
425
|
+
"runtime order arrival snapshot failed: %s %s",
|
|
426
|
+
symbol,
|
|
427
|
+
error?.message || String(error)
|
|
428
|
+
);
|
|
429
|
+
return {
|
|
430
|
+
arrivalSnapshotTime: Date.now(),
|
|
431
|
+
arrivalSource: "top_of_book_error",
|
|
432
|
+
bid: null,
|
|
433
|
+
ask: null,
|
|
434
|
+
arrivalMid: null,
|
|
435
|
+
spreadBps: null
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
var resolveRuntimeTelemetryQuality = ({
|
|
440
|
+
signalClosePrice,
|
|
441
|
+
arrivalMid,
|
|
442
|
+
orderSubmitTime,
|
|
443
|
+
orderAckTime,
|
|
444
|
+
fillAvgPrice,
|
|
445
|
+
fillTime
|
|
446
|
+
}) => {
|
|
447
|
+
if (signalClosePrice != null && arrivalMid != null && orderSubmitTime != null && orderAckTime != null && fillAvgPrice != null && fillTime != null) {
|
|
448
|
+
return "full";
|
|
449
|
+
}
|
|
450
|
+
if (fillAvgPrice != null && (arrivalMid != null || orderSubmitTime != null)) {
|
|
451
|
+
return "partial";
|
|
452
|
+
}
|
|
453
|
+
if (fillAvgPrice != null) {
|
|
454
|
+
return "price_only";
|
|
455
|
+
}
|
|
456
|
+
return "none";
|
|
457
|
+
};
|
|
355
458
|
var applyProtectiveOrders = async ({
|
|
356
459
|
connector,
|
|
357
460
|
symbol,
|
|
@@ -394,11 +497,19 @@ var executeEntryOrder = async ({
|
|
|
394
497
|
stopLossPrice,
|
|
395
498
|
signal,
|
|
396
499
|
beforePlaceOrder,
|
|
397
|
-
recordRuntimeTrade = true
|
|
500
|
+
recordRuntimeTrade = true,
|
|
501
|
+
leverage
|
|
398
502
|
}) => {
|
|
399
503
|
await beforePlaceOrder?.();
|
|
400
|
-
const orderId = signal.orderId || createRuntimeOrderId();
|
|
504
|
+
const orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
|
|
505
|
+
const signalTimestamp = signal.timestamp;
|
|
506
|
+
const signalClosePrice = currentPrice;
|
|
401
507
|
signal.orderId = orderId;
|
|
508
|
+
signal.orderQty = qty;
|
|
509
|
+
signal.orderValue = qty * currentPrice;
|
|
510
|
+
signal.orderFailureReason = void 0;
|
|
511
|
+
const arrivalSnapshot = await getArrivalSnapshot({ connector, symbol });
|
|
512
|
+
const orderSubmitTime = Date.now();
|
|
402
513
|
const orderPlaced = await connector.placeOrder({
|
|
403
514
|
symbol,
|
|
404
515
|
qty,
|
|
@@ -406,34 +517,47 @@ var executeEntryOrder = async ({
|
|
|
406
517
|
isLimit: false,
|
|
407
518
|
timestamp,
|
|
408
519
|
direction,
|
|
520
|
+
...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
|
|
409
521
|
orderId,
|
|
410
522
|
signal
|
|
411
523
|
});
|
|
524
|
+
const orderAckTime = Date.now();
|
|
525
|
+
const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
|
|
526
|
+
const currentPosition = await connector.getPosition(symbol);
|
|
527
|
+
const fillTime = Date.now();
|
|
528
|
+
const entryPrice = currentPosition?.price && Number.isFinite(currentPosition.price) ? currentPosition.price : currentPrice;
|
|
529
|
+
const fillSource = currentPosition?.price && Number.isFinite(currentPosition.price) ? "exchange_position" : orderPlaced ? "requested_price" : "unknown";
|
|
530
|
+
const entryQty = currentPosition?.qty && Number.isFinite(currentPosition.qty) ? currentPosition.qty : placedQty;
|
|
531
|
+
const estimatedOpenFee = entryPrice * entryQty * FEE_PERCENT;
|
|
532
|
+
signal.prices.currentPrice = entryPrice;
|
|
533
|
+
signal.orderQty = entryQty;
|
|
534
|
+
signal.orderValue = entryQty * entryPrice;
|
|
412
535
|
if (orderPlaced) {
|
|
413
536
|
try {
|
|
414
537
|
await applyProtectiveOrders({
|
|
415
538
|
connector,
|
|
416
539
|
symbol,
|
|
417
540
|
direction,
|
|
418
|
-
qty,
|
|
541
|
+
qty: entryQty,
|
|
419
542
|
takeProfits,
|
|
420
543
|
stopLossPrice
|
|
421
544
|
});
|
|
422
545
|
} catch (error) {
|
|
423
546
|
await connector.closePosition({
|
|
424
547
|
symbol,
|
|
425
|
-
price:
|
|
548
|
+
price: entryPrice,
|
|
426
549
|
timestamp,
|
|
427
|
-
direction
|
|
550
|
+
direction,
|
|
551
|
+
signal
|
|
428
552
|
});
|
|
429
553
|
throw error;
|
|
430
554
|
}
|
|
431
555
|
}
|
|
432
556
|
signal.orderStatus = orderPlaced ? "completed" : "failed";
|
|
433
557
|
signal.orderSkipReason = void 0;
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
558
|
+
if (orderPlaced) {
|
|
559
|
+
signal.orderFailureReason = void 0;
|
|
560
|
+
}
|
|
437
561
|
if (orderPlaced && recordRuntimeTrade) {
|
|
438
562
|
await recordRuntimeTradeOpen({
|
|
439
563
|
userName,
|
|
@@ -441,10 +565,41 @@ var executeEntryOrder = async ({
|
|
|
441
565
|
signalId: signal.signalId,
|
|
442
566
|
strategy: signal.strategy,
|
|
443
567
|
symbol,
|
|
568
|
+
interval: signal.interval,
|
|
444
569
|
direction,
|
|
445
|
-
qty,
|
|
570
|
+
qty: entryQty,
|
|
446
571
|
entryPrice,
|
|
572
|
+
signalTimestamp,
|
|
573
|
+
signalClosePrice,
|
|
574
|
+
arrivalSnapshotTime: arrivalSnapshot.arrivalSnapshotTime,
|
|
575
|
+
arrivalSource: arrivalSnapshot.arrivalSource,
|
|
576
|
+
arrivalMid: arrivalSnapshot.arrivalMid,
|
|
577
|
+
bid: arrivalSnapshot.bid,
|
|
578
|
+
ask: arrivalSnapshot.ask,
|
|
579
|
+
spreadBps: arrivalSnapshot.spreadBps,
|
|
580
|
+
orderSubmitTime,
|
|
581
|
+
orderAckTime,
|
|
582
|
+
fillAvgPrice: entryPrice,
|
|
583
|
+
fillSource,
|
|
584
|
+
fillTime,
|
|
585
|
+
telemetryQuality: resolveRuntimeTelemetryQuality({
|
|
586
|
+
signalClosePrice,
|
|
587
|
+
arrivalMid: arrivalSnapshot.arrivalMid,
|
|
588
|
+
orderSubmitTime,
|
|
589
|
+
orderAckTime,
|
|
590
|
+
fillAvgPrice: entryPrice,
|
|
591
|
+
fillTime
|
|
592
|
+
}),
|
|
593
|
+
fee: estimatedOpenFee,
|
|
594
|
+
openFee: estimatedOpenFee,
|
|
595
|
+
totalFee: estimatedOpenFee,
|
|
447
596
|
entryTimestamp: timestamp,
|
|
597
|
+
universe: signal.universe,
|
|
598
|
+
assetClass: signal.assetClass,
|
|
599
|
+
accountId: signal.accountId,
|
|
600
|
+
deploymentId: signal.deploymentId,
|
|
601
|
+
policyProfileId: signal.policyProfileId,
|
|
602
|
+
runtimeConfigId: signal.runtimeConfigId,
|
|
448
603
|
...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {}
|
|
449
604
|
});
|
|
450
605
|
}
|
|
@@ -473,13 +628,14 @@ var updatePositionProtection = async ({
|
|
|
473
628
|
|
|
474
629
|
// src/strategyHelpers/config.ts
|
|
475
630
|
var import_lodash = __toESM(require_lodash());
|
|
476
|
-
import { getData as getData2, redisKeys as
|
|
631
|
+
import { getData as getData2, redisKeys as redisKeys2 } from "@tradejs/infra/redis";
|
|
477
632
|
var resolveStrategyConfig = async ({
|
|
478
633
|
strategyName,
|
|
479
634
|
userName,
|
|
480
635
|
symbol,
|
|
481
636
|
baseConfig,
|
|
482
|
-
defaults
|
|
637
|
+
defaults,
|
|
638
|
+
runtimeConfigId
|
|
483
639
|
}) => {
|
|
484
640
|
const mergeIfNotEmpty = (target, patch) => patch && !import_lodash.default.isEmpty(patch) ? {
|
|
485
641
|
...target,
|
|
@@ -492,35 +648,75 @@ var resolveStrategyConfig = async ({
|
|
|
492
648
|
let isConfigFromBacktest = false;
|
|
493
649
|
if (config.ENV !== "BACKTEST") {
|
|
494
650
|
const userConfig = await getData2(
|
|
495
|
-
|
|
651
|
+
runtimeConfigId ? redisKeys2.strategyConfig(userName, strategyName, runtimeConfigId) : redisKeys2.strategyConfig(userName, strategyName),
|
|
496
652
|
{}
|
|
497
653
|
);
|
|
498
654
|
config = mergeIfNotEmpty(config, userConfig);
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const backtestResult = results?.[symbol];
|
|
504
|
-
if (backtestResult && !import_lodash.default.isEmpty(backtestResult.config)) {
|
|
505
|
-
config = mergeIfNotEmpty(
|
|
506
|
-
config,
|
|
507
|
-
backtestResult.config
|
|
655
|
+
if (!runtimeConfigId || runtimeConfigId === "config") {
|
|
656
|
+
const results = await getData2(
|
|
657
|
+
redisKeys2.strategyResults(userName, strategyName),
|
|
658
|
+
{}
|
|
508
659
|
);
|
|
509
|
-
|
|
660
|
+
const backtestResult = results?.[symbol];
|
|
661
|
+
if (backtestResult && !import_lodash.default.isEmpty(backtestResult.config)) {
|
|
662
|
+
config = mergeIfNotEmpty(
|
|
663
|
+
config,
|
|
664
|
+
backtestResult.config
|
|
665
|
+
);
|
|
666
|
+
isConfigFromBacktest = true;
|
|
667
|
+
}
|
|
510
668
|
}
|
|
511
669
|
}
|
|
512
670
|
return { config, isConfigFromBacktest };
|
|
513
671
|
};
|
|
514
672
|
|
|
515
673
|
// src/strategyRuntime.ts
|
|
674
|
+
var buildExitOrderSignal = ({
|
|
675
|
+
strategyName,
|
|
676
|
+
symbol,
|
|
677
|
+
decision
|
|
678
|
+
}) => {
|
|
679
|
+
if (!strategyName) {
|
|
680
|
+
return void 0;
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
signalId: `${strategyName}:${symbol}:exit:${decision.closePlan.timestamp}`,
|
|
684
|
+
strategy: strategyName,
|
|
685
|
+
symbol,
|
|
686
|
+
interval: "15",
|
|
687
|
+
direction: decision.closePlan.direction,
|
|
688
|
+
timestamp: decision.closePlan.timestamp,
|
|
689
|
+
figures: {},
|
|
690
|
+
indicators: {},
|
|
691
|
+
prices: {
|
|
692
|
+
currentPrice: decision.closePlan.price,
|
|
693
|
+
takeProfitPrice: decision.closePlan.price,
|
|
694
|
+
stopLossPrice: decision.closePlan.price,
|
|
695
|
+
riskRatio: 0
|
|
696
|
+
},
|
|
697
|
+
additionalIndicators: {
|
|
698
|
+
exit: {
|
|
699
|
+
code: decision.code
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
};
|
|
516
704
|
var resolveEntryRuntimePolicy = ({
|
|
517
705
|
decision,
|
|
518
706
|
config,
|
|
519
|
-
manifest
|
|
707
|
+
manifest,
|
|
708
|
+
policyProfile
|
|
520
709
|
}) => {
|
|
521
|
-
const
|
|
522
|
-
const
|
|
523
|
-
const
|
|
710
|
+
const baseDefaults = manifest?.entryRuntimeDefaults;
|
|
711
|
+
const profileDefaults = policyProfile?.entryRuntimeDefaults;
|
|
712
|
+
const manifestDefaults = baseDefaults || profileDefaults ? {
|
|
713
|
+
...baseDefaults,
|
|
714
|
+
...profileDefaults,
|
|
715
|
+
...baseDefaults?.ml || profileDefaults?.ml ? { ml: { ...baseDefaults?.ml, ...profileDefaults?.ml } } : {},
|
|
716
|
+
...baseDefaults?.ai || profileDefaults?.ai ? { ai: { ...baseDefaults?.ai, ...profileDefaults?.ai } } : {}
|
|
717
|
+
} : void 0;
|
|
718
|
+
const adapterMl = (policyProfile?.mlAdapter ?? manifest?.mlAdapter)?.mapEntryRuntimeFromConfig?.(config);
|
|
719
|
+
const adapterAi = (policyProfile?.aiAdapter ?? manifest?.aiAdapter)?.mapEntryRuntimeFromConfig?.(config);
|
|
524
720
|
const ml = manifestDefaults?.ml || adapterMl || decision.runtime?.ml ? {
|
|
525
721
|
...manifestDefaults?.ml,
|
|
526
722
|
...adapterMl,
|
|
@@ -606,6 +802,184 @@ var getEntrySkipReason = ({
|
|
|
606
802
|
}
|
|
607
803
|
return "ENTRY_POLICY_BLOCKED";
|
|
608
804
|
};
|
|
805
|
+
var resolveBacktestEntryDelayBars = (value) => {
|
|
806
|
+
if (value == null || value === "") {
|
|
807
|
+
return 1;
|
|
808
|
+
}
|
|
809
|
+
const parsed = parseInt(String(value), 10);
|
|
810
|
+
return Number.isFinite(parsed) ? Math.max(0, parsed) : 1;
|
|
811
|
+
};
|
|
812
|
+
var resolveBacktestExecutionIntervalForPrimary = (interval) => {
|
|
813
|
+
const normalized = String(interval ?? "15");
|
|
814
|
+
if (normalized === "15") {
|
|
815
|
+
return BACKTEST_EXECUTION_INTERVAL;
|
|
816
|
+
}
|
|
817
|
+
if (normalized === "60") {
|
|
818
|
+
return "15";
|
|
819
|
+
}
|
|
820
|
+
return null;
|
|
821
|
+
};
|
|
822
|
+
var resolveBacktestExecutionDelayMs = (value, fallbackDelayMs) => {
|
|
823
|
+
if (value == null || value === "") {
|
|
824
|
+
return fallbackDelayMs;
|
|
825
|
+
}
|
|
826
|
+
const parsed = Number(value);
|
|
827
|
+
return Number.isFinite(parsed) ? Math.max(0, Math.trunc(parsed)) : fallbackDelayMs;
|
|
828
|
+
};
|
|
829
|
+
var safeIntervalToMs = (interval) => {
|
|
830
|
+
try {
|
|
831
|
+
return intervalToMs(interval);
|
|
832
|
+
} catch {
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
var buildCandleByTimestamp = (candles) => new Map(
|
|
837
|
+
(candles ?? []).filter((candle) => typeof candle?.timestamp === "number").map((candle) => [candle.timestamp, candle])
|
|
838
|
+
);
|
|
839
|
+
var buildBacktestExecutionOnlyCandle = (candle, executionPrice) => ({
|
|
840
|
+
...candle,
|
|
841
|
+
open: executionPrice,
|
|
842
|
+
high: executionPrice,
|
|
843
|
+
low: executionPrice,
|
|
844
|
+
close: executionPrice,
|
|
845
|
+
volume: 0,
|
|
846
|
+
turnover: 0
|
|
847
|
+
});
|
|
848
|
+
var resolveInvalidDelayedEntryReason = ({
|
|
849
|
+
decision,
|
|
850
|
+
executionPrice,
|
|
851
|
+
takeProfitPrice,
|
|
852
|
+
stopLossPrice,
|
|
853
|
+
riskRatio
|
|
854
|
+
}) => {
|
|
855
|
+
if (!Number.isFinite(executionPrice) || !Number.isFinite(takeProfitPrice) || !Number.isFinite(stopLossPrice)) {
|
|
856
|
+
return "BACKTEST_DELAYED_ENTRY_INVALID_PRICE";
|
|
857
|
+
}
|
|
858
|
+
if (decision.entryContext.direction === "LONG") {
|
|
859
|
+
if (executionPrice <= stopLossPrice) {
|
|
860
|
+
return "BACKTEST_DELAYED_ENTRY_BEYOND_STOP";
|
|
861
|
+
}
|
|
862
|
+
if (executionPrice >= takeProfitPrice) {
|
|
863
|
+
return "BACKTEST_DELAYED_ENTRY_BEYOND_TAKE_PROFIT";
|
|
864
|
+
}
|
|
865
|
+
return !Number.isFinite(riskRatio) || riskRatio <= 0 ? "BACKTEST_DELAYED_ENTRY_INVALID_PRICE" : null;
|
|
866
|
+
}
|
|
867
|
+
if (executionPrice >= stopLossPrice) {
|
|
868
|
+
return "BACKTEST_DELAYED_ENTRY_BEYOND_STOP";
|
|
869
|
+
}
|
|
870
|
+
if (executionPrice <= takeProfitPrice) {
|
|
871
|
+
return "BACKTEST_DELAYED_ENTRY_BEYOND_TAKE_PROFIT";
|
|
872
|
+
}
|
|
873
|
+
return !Number.isFinite(riskRatio) || riskRatio <= 0 ? "BACKTEST_DELAYED_ENTRY_INVALID_PRICE" : null;
|
|
874
|
+
};
|
|
875
|
+
var applyBacktestDelayedEntryExecution = ({
|
|
876
|
+
decision,
|
|
877
|
+
execution,
|
|
878
|
+
backtestPriceMode,
|
|
879
|
+
delayBars
|
|
880
|
+
}) => {
|
|
881
|
+
const { candle, btcCandle } = execution;
|
|
882
|
+
const signalTimestamp = decision.signal?.timestamp ?? decision.entryContext.timestamp;
|
|
883
|
+
const signalPrice = decision.signal?.prices.currentPrice ?? decision.entryContext.prices.currentPrice;
|
|
884
|
+
const skipReason = execution.skipReason ?? (!candle || !btcCandle ? "BACKTEST_LOWER_EXECUTION_CANDLE_MISSING" : void 0);
|
|
885
|
+
if (skipReason || !candle || !btcCandle) {
|
|
886
|
+
if (decision.signal) {
|
|
887
|
+
decision.signal.additionalIndicators = {
|
|
888
|
+
...decision.signal.additionalIndicators ?? {},
|
|
889
|
+
backtestExecution: {
|
|
890
|
+
entryDelayBars: delayBars,
|
|
891
|
+
priceMode: backtestPriceMode ?? "open",
|
|
892
|
+
signalTimestamp,
|
|
893
|
+
signalPrice,
|
|
894
|
+
executionSource: execution.source,
|
|
895
|
+
...execution.executionInterval ? { executionInterval: execution.executionInterval } : {},
|
|
896
|
+
...execution.executionDelayMs != null ? { executionDelayMs: execution.executionDelayMs } : {},
|
|
897
|
+
...execution.primaryExecutionTimestamp != null ? { primaryExecutionTimestamp: execution.primaryExecutionTimestamp } : {},
|
|
898
|
+
...execution.requestedExecutionTimestamp != null ? {
|
|
899
|
+
requestedExecutionTimestamp: execution.requestedExecutionTimestamp
|
|
900
|
+
} : {},
|
|
901
|
+
skipReason
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
decision.signal.orderStatus = "skipped";
|
|
905
|
+
decision.signal.orderSkipReason = skipReason;
|
|
906
|
+
}
|
|
907
|
+
return {
|
|
908
|
+
skipReason,
|
|
909
|
+
executionCandle: null,
|
|
910
|
+
btcExecutionCandle: null
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
const executionPrice = resolveBacktestExecutionPrice(
|
|
914
|
+
candle,
|
|
915
|
+
backtestPriceMode ?? "open"
|
|
916
|
+
);
|
|
917
|
+
const executionTimestamp = candle.timestamp;
|
|
918
|
+
const takeProfitPrice = decision.entryContext.prices.takeProfitPrice;
|
|
919
|
+
const stopLossPrice = decision.orderPlan.stopLossPrice;
|
|
920
|
+
const riskRatio = calculateRiskRatio({
|
|
921
|
+
direction: decision.entryContext.direction,
|
|
922
|
+
currentPrice: executionPrice,
|
|
923
|
+
takeProfitPrice,
|
|
924
|
+
stopLossPrice
|
|
925
|
+
});
|
|
926
|
+
const invalidSkipReason = resolveInvalidDelayedEntryReason({
|
|
927
|
+
decision,
|
|
928
|
+
executionPrice,
|
|
929
|
+
takeProfitPrice,
|
|
930
|
+
stopLossPrice,
|
|
931
|
+
riskRatio
|
|
932
|
+
});
|
|
933
|
+
decision.entryContext = {
|
|
934
|
+
...decision.entryContext,
|
|
935
|
+
timestamp: executionTimestamp,
|
|
936
|
+
prices: {
|
|
937
|
+
...decision.entryContext.prices,
|
|
938
|
+
currentPrice: executionPrice,
|
|
939
|
+
stopLossPrice,
|
|
940
|
+
riskRatio
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
const executionResult = {
|
|
944
|
+
skipReason: invalidSkipReason,
|
|
945
|
+
executionCandle: buildBacktestExecutionOnlyCandle(candle, executionPrice),
|
|
946
|
+
btcExecutionCandle: buildBacktestExecutionOnlyCandle(
|
|
947
|
+
btcCandle,
|
|
948
|
+
resolveBacktestExecutionPrice(btcCandle, backtestPriceMode ?? "open")
|
|
949
|
+
)
|
|
950
|
+
};
|
|
951
|
+
if (!decision.signal) {
|
|
952
|
+
return executionResult;
|
|
953
|
+
}
|
|
954
|
+
decision.signal.prices = {
|
|
955
|
+
...decision.signal.prices,
|
|
956
|
+
currentPrice: executionPrice,
|
|
957
|
+
stopLossPrice,
|
|
958
|
+
riskRatio
|
|
959
|
+
};
|
|
960
|
+
decision.signal.additionalIndicators = {
|
|
961
|
+
...decision.signal.additionalIndicators ?? {},
|
|
962
|
+
backtestExecution: {
|
|
963
|
+
entryDelayBars: delayBars,
|
|
964
|
+
priceMode: backtestPriceMode ?? "open",
|
|
965
|
+
signalTimestamp,
|
|
966
|
+
signalPrice,
|
|
967
|
+
executionTimestamp,
|
|
968
|
+
executionPrice,
|
|
969
|
+
executionSource: execution.source,
|
|
970
|
+
...execution.executionInterval ? { executionInterval: execution.executionInterval } : {},
|
|
971
|
+
...execution.executionDelayMs != null ? { executionDelayMs: execution.executionDelayMs } : {},
|
|
972
|
+
...execution.primaryExecutionTimestamp != null ? { primaryExecutionTimestamp: execution.primaryExecutionTimestamp } : {},
|
|
973
|
+
...execution.requestedExecutionTimestamp != null ? { requestedExecutionTimestamp: execution.requestedExecutionTimestamp } : {},
|
|
974
|
+
...invalidSkipReason ? { skipReason: invalidSkipReason } : {}
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
if (invalidSkipReason) {
|
|
978
|
+
decision.signal.orderStatus = "skipped";
|
|
979
|
+
decision.signal.orderSkipReason = invalidSkipReason;
|
|
980
|
+
}
|
|
981
|
+
return executionResult;
|
|
982
|
+
};
|
|
609
983
|
var normalizeConfigHookList = (value) => {
|
|
610
984
|
if (Array.isArray(value)) {
|
|
611
985
|
return value;
|
|
@@ -640,6 +1014,11 @@ var buildHookCtx = ({
|
|
|
640
1014
|
strategyName,
|
|
641
1015
|
userName,
|
|
642
1016
|
symbol,
|
|
1017
|
+
universe,
|
|
1018
|
+
assetClass,
|
|
1019
|
+
accountId,
|
|
1020
|
+
deploymentId,
|
|
1021
|
+
policyProfileId,
|
|
643
1022
|
strategyConfig,
|
|
644
1023
|
env,
|
|
645
1024
|
isConfigFromBacktest
|
|
@@ -648,6 +1027,11 @@ var buildHookCtx = ({
|
|
|
648
1027
|
strategyName,
|
|
649
1028
|
userName,
|
|
650
1029
|
symbol,
|
|
1030
|
+
...universe ? { universe } : {},
|
|
1031
|
+
...assetClass ? { assetClass } : {},
|
|
1032
|
+
...accountId ? { accountId } : {},
|
|
1033
|
+
...deploymentId ? { deploymentId } : {},
|
|
1034
|
+
...policyProfileId ? { policyProfileId } : {},
|
|
651
1035
|
strategyConfig,
|
|
652
1036
|
env,
|
|
653
1037
|
isConfigFromBacktest
|
|
@@ -680,6 +1064,10 @@ var shouldRecordRuntimeJournal = ({
|
|
|
680
1064
|
var isTestConnector = (connector) => Boolean(
|
|
681
1065
|
connector.__tradejsTestConnector
|
|
682
1066
|
);
|
|
1067
|
+
var canUseSharedReplayState = ({
|
|
1068
|
+
env,
|
|
1069
|
+
sharedReplayKey
|
|
1070
|
+
}) => (env === "BACKTEST" || env === "PARITY") && Boolean(sharedReplayKey);
|
|
683
1071
|
var buildMlHookContext = ({
|
|
684
1072
|
signal,
|
|
685
1073
|
env,
|
|
@@ -789,22 +1177,86 @@ var handleExitDecision = async ({
|
|
|
789
1177
|
symbol,
|
|
790
1178
|
decision,
|
|
791
1179
|
market,
|
|
1180
|
+
onRuntimeClose,
|
|
792
1181
|
onRuntimeError
|
|
793
1182
|
}) => {
|
|
794
1183
|
try {
|
|
1184
|
+
let activeTradeForClose = null;
|
|
1185
|
+
if (userName) {
|
|
1186
|
+
const activeTrade = await getActiveRuntimeTrade({
|
|
1187
|
+
userName,
|
|
1188
|
+
symbol,
|
|
1189
|
+
accountId: connector.accountId,
|
|
1190
|
+
deploymentId: connector.deploymentId
|
|
1191
|
+
});
|
|
1192
|
+
if (!activeTrade) {
|
|
1193
|
+
logger3.warn(
|
|
1194
|
+
"[%s] blocked closePosition for untracked runtime position: %s",
|
|
1195
|
+
strategyName ?? "unknown",
|
|
1196
|
+
symbol
|
|
1197
|
+
);
|
|
1198
|
+
return "CLOSE_BLOCKED_BY_UNTRACKED_POSITION";
|
|
1199
|
+
}
|
|
1200
|
+
if (!strategyName || activeTrade.strategy !== strategyName) {
|
|
1201
|
+
logger3.warn(
|
|
1202
|
+
"[%s] blocked closePosition for foreign runtime position: %s ownedBy=%s",
|
|
1203
|
+
strategyName ?? "unknown",
|
|
1204
|
+
symbol,
|
|
1205
|
+
activeTrade.strategy
|
|
1206
|
+
);
|
|
1207
|
+
return "CLOSE_BLOCKED_BY_FOREIGN_STRATEGY_POSITION";
|
|
1208
|
+
}
|
|
1209
|
+
activeTradeForClose = activeTrade;
|
|
1210
|
+
}
|
|
795
1211
|
await connector.closePosition({
|
|
796
1212
|
symbol,
|
|
797
1213
|
price: decision.closePlan.price,
|
|
798
1214
|
timestamp: decision.closePlan.timestamp,
|
|
799
|
-
direction: decision.closePlan.direction
|
|
1215
|
+
direction: decision.closePlan.direction,
|
|
1216
|
+
signal: buildExitOrderSignal({
|
|
1217
|
+
strategyName,
|
|
1218
|
+
symbol,
|
|
1219
|
+
decision
|
|
1220
|
+
})
|
|
800
1221
|
});
|
|
801
|
-
await markRuntimeTradeClosed({
|
|
1222
|
+
const closedTrade = await markRuntimeTradeClosed({
|
|
802
1223
|
userName,
|
|
803
1224
|
strategy: strategyName,
|
|
804
1225
|
symbol,
|
|
805
1226
|
exitPrice: decision.closePlan.price,
|
|
806
|
-
exitTimestamp: decision.closePlan.timestamp
|
|
1227
|
+
exitTimestamp: decision.closePlan.timestamp,
|
|
1228
|
+
exitType: "exit",
|
|
1229
|
+
accountId: connector.accountId,
|
|
1230
|
+
deploymentId: connector.deploymentId
|
|
807
1231
|
});
|
|
1232
|
+
const trade = closedTrade ?? activeTradeForClose;
|
|
1233
|
+
if (trade && strategyName) {
|
|
1234
|
+
try {
|
|
1235
|
+
onRuntimeClose?.({
|
|
1236
|
+
userName,
|
|
1237
|
+
strategy: strategyName,
|
|
1238
|
+
openedByStrategy: trade.strategy,
|
|
1239
|
+
symbol,
|
|
1240
|
+
direction: trade.direction,
|
|
1241
|
+
code: decision.code,
|
|
1242
|
+
orderId: trade.orderId,
|
|
1243
|
+
signalId: trade.signalId,
|
|
1244
|
+
qty: trade.qty,
|
|
1245
|
+
entryPrice: trade.entryPrice,
|
|
1246
|
+
entryTimestamp: trade.entryTimestamp,
|
|
1247
|
+
exitPrice: closedTrade?.exitPrice ?? decision.closePlan.price,
|
|
1248
|
+
exitTimestamp: closedTrade?.exitTimestamp ?? decision.closePlan.timestamp,
|
|
1249
|
+
closedPnl: closedTrade?.closedPnl ?? trade.closedPnl ?? null,
|
|
1250
|
+
exitType: closedTrade?.exitType ?? "exit"
|
|
1251
|
+
});
|
|
1252
|
+
} catch (notificationError) {
|
|
1253
|
+
logger3.error(
|
|
1254
|
+
"runtime close notification error: %s %s",
|
|
1255
|
+
symbol,
|
|
1256
|
+
notificationError
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
808
1260
|
} catch (err) {
|
|
809
1261
|
await onRuntimeError?.({
|
|
810
1262
|
stage: "closePosition",
|
|
@@ -901,6 +1353,7 @@ var executeEntryDecision = async ({
|
|
|
901
1353
|
timestamp: decision.entryContext.timestamp,
|
|
902
1354
|
takeProfits: decision.orderPlan.takeProfits,
|
|
903
1355
|
stopLossPrice: decision.orderPlan.stopLossPrice,
|
|
1356
|
+
...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
|
|
904
1357
|
signal,
|
|
905
1358
|
beforePlaceOrder,
|
|
906
1359
|
recordRuntimeTrade: recordRuntimeJournal
|
|
@@ -930,7 +1383,8 @@ var executeEntryDecision = async ({
|
|
|
930
1383
|
qty: decision.orderPlan.qty,
|
|
931
1384
|
price: decision.entryContext.prices.currentPrice,
|
|
932
1385
|
timestamp: decision.entryContext.timestamp,
|
|
933
|
-
direction: decision.entryContext.direction
|
|
1386
|
+
direction: decision.entryContext.direction,
|
|
1387
|
+
...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {}
|
|
934
1388
|
});
|
|
935
1389
|
if (!orderPlaced) {
|
|
936
1390
|
throw new Error("PLACE_ORDER_FAILED");
|
|
@@ -973,6 +1427,9 @@ var executeEntryDecision = async ({
|
|
|
973
1427
|
} catch (err) {
|
|
974
1428
|
if (signal) {
|
|
975
1429
|
signal.orderStatus = "failed";
|
|
1430
|
+
if (typeof signal.orderFailureReason !== "string" || !signal.orderFailureReason.trim()) {
|
|
1431
|
+
signal.orderFailureReason = typeof err?.message === "string" && err.message.trim() ? err.message.trim() : void 0;
|
|
1432
|
+
}
|
|
976
1433
|
}
|
|
977
1434
|
await notifyRuntimeError({
|
|
978
1435
|
stage: "placeOrder",
|
|
@@ -991,7 +1448,9 @@ var createStrategyRuntime = ({
|
|
|
991
1448
|
defaults,
|
|
992
1449
|
createCore,
|
|
993
1450
|
manifest: staticManifest,
|
|
994
|
-
strategyDirectory
|
|
1451
|
+
strategyDirectory,
|
|
1452
|
+
detectorKey,
|
|
1453
|
+
detectorNoSignalSkipReason
|
|
995
1454
|
}) => {
|
|
996
1455
|
const projectRoot = getTradejsProjectCwd();
|
|
997
1456
|
const resolveManifest = (name) => {
|
|
@@ -1012,42 +1471,102 @@ var createStrategyRuntime = ({
|
|
|
1012
1471
|
strategyName
|
|
1013
1472
|
)
|
|
1014
1473
|
);
|
|
1015
|
-
|
|
1474
|
+
const creator = async ({
|
|
1016
1475
|
userName,
|
|
1476
|
+
connectorName,
|
|
1017
1477
|
config: baseConfig,
|
|
1018
1478
|
symbol,
|
|
1479
|
+
universe: requestedUniverse,
|
|
1480
|
+
assetClass,
|
|
1481
|
+
accountId: requestedAccountId,
|
|
1482
|
+
deploymentId: requestedDeploymentId,
|
|
1483
|
+
policyProfileId,
|
|
1484
|
+
runtimeConfigId,
|
|
1019
1485
|
data,
|
|
1020
1486
|
btcData,
|
|
1487
|
+
ethData = [],
|
|
1021
1488
|
btcBinanceData,
|
|
1022
1489
|
btcCoinbaseData,
|
|
1023
|
-
|
|
1490
|
+
backtestExecutionMarketData,
|
|
1491
|
+
connector,
|
|
1492
|
+
sharedIndicatorsReplayKey,
|
|
1493
|
+
sharedStrategyStateKey,
|
|
1494
|
+
onRuntimeClose
|
|
1024
1495
|
}) => {
|
|
1025
1496
|
const { config, isConfigFromBacktest } = await resolveStrategyConfig({
|
|
1026
1497
|
strategyName,
|
|
1027
1498
|
userName,
|
|
1028
1499
|
symbol,
|
|
1029
1500
|
baseConfig,
|
|
1030
|
-
defaults
|
|
1501
|
+
defaults,
|
|
1502
|
+
runtimeConfigId
|
|
1031
1503
|
});
|
|
1504
|
+
const universe = requestedUniverse ?? connector.universe;
|
|
1505
|
+
const accountId = requestedAccountId ?? connector.accountId;
|
|
1506
|
+
const deploymentId = requestedDeploymentId ?? connector.deploymentId;
|
|
1032
1507
|
const projectConfig = await loadTradejsConfig(projectRoot);
|
|
1033
1508
|
const projectHooks = projectConfig.hooks;
|
|
1034
1509
|
const env = String(config.ENV ?? "BACKTEST");
|
|
1510
|
+
const backtestPriceMode = config.BACKTEST_PRICE_MODE ?? "open";
|
|
1511
|
+
const backtestEntryDelayBars = env === "BACKTEST" ? resolveBacktestEntryDelayBars(config.BACKTEST_ENTRY_DELAY_BARS) : 0;
|
|
1512
|
+
const resolvedBacktestExecutionInterval = config.BACKTEST_EXECUTION_INTERVAL ?? backtestExecutionMarketData?.interval ?? resolveBacktestExecutionIntervalForPrimary(config.INTERVAL ?? "15");
|
|
1513
|
+
const backtestExecutionInterval = resolvedBacktestExecutionInterval == null ? null : String(resolvedBacktestExecutionInterval);
|
|
1514
|
+
const backtestExecutionIntervalLabel = backtestExecutionInterval == null ? void 0 : String(backtestExecutionInterval);
|
|
1515
|
+
const primaryIntervalMs = safeIntervalToMs(config.INTERVAL ?? "15");
|
|
1516
|
+
const backtestExecutionIntervalMs = safeIntervalToMs(
|
|
1517
|
+
backtestExecutionInterval
|
|
1518
|
+
);
|
|
1519
|
+
const backtestExecutionDelayMs = resolveBacktestExecutionDelayMs(
|
|
1520
|
+
config.BACKTEST_EXECUTION_DELAY_MS,
|
|
1521
|
+
backtestExecutionIntervalMs ?? BACKTEST_EXECUTION_DELAY_MS
|
|
1522
|
+
);
|
|
1523
|
+
const backtestExecutionCandleByTimestamp = backtestExecutionMarketData?.dataByTimestamp ?? buildCandleByTimestamp(backtestExecutionMarketData?.data);
|
|
1524
|
+
const backtestExecutionBtcCandleByTimestamp = backtestExecutionMarketData?.btcDataByTimestamp ?? buildCandleByTimestamp(backtestExecutionMarketData?.btcData);
|
|
1525
|
+
const canUseLowerBacktestExecution = env === "BACKTEST" && backtestEntryDelayBars > 0 && backtestExecutionIntervalMs != null && primaryIntervalMs != null && backtestExecutionIntervalMs < primaryIntervalMs;
|
|
1035
1526
|
const recordRuntimeJournal = shouldRecordRuntimeJournal({ env, config });
|
|
1036
1527
|
const strategyManifest = resolveManifest(strategyName);
|
|
1528
|
+
const requestedPolicyProfileId = policyProfileId ?? (typeof config.POLICY_PROFILE_ID === "string" ? config.POLICY_PROFILE_ID : void 0);
|
|
1529
|
+
const getPolicyProfile = (name = strategyName) => resolveStrategyPolicyProfile(resolveManifest(name), {
|
|
1530
|
+
profileId: requestedPolicyProfileId,
|
|
1531
|
+
universe,
|
|
1532
|
+
assetClass
|
|
1533
|
+
});
|
|
1534
|
+
const strategyPolicyProfile = getPolicyProfile();
|
|
1535
|
+
const indicatorPeriods = buildDefaultIndicatorPeriods(config);
|
|
1037
1536
|
const hookBase = {
|
|
1038
1537
|
connector,
|
|
1039
1538
|
strategyName,
|
|
1040
1539
|
userName,
|
|
1041
1540
|
symbol,
|
|
1541
|
+
universe,
|
|
1542
|
+
assetClass,
|
|
1543
|
+
accountId,
|
|
1544
|
+
deploymentId,
|
|
1545
|
+
policyProfileId: strategyPolicyProfile?.id ?? requestedPolicyProfileId,
|
|
1042
1546
|
strategyConfig: config,
|
|
1043
1547
|
env,
|
|
1044
1548
|
isConfigFromBacktest
|
|
1045
1549
|
};
|
|
1046
|
-
const getHookCtx = (name = strategyName) =>
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1550
|
+
const getHookCtx = (name = strategyName) => {
|
|
1551
|
+
const profile = getPolicyProfile(name);
|
|
1552
|
+
return buildHookCtx({
|
|
1553
|
+
...hookBase,
|
|
1554
|
+
strategyName: name,
|
|
1555
|
+
policyProfileId: profile?.id ?? requestedPolicyProfileId
|
|
1556
|
+
});
|
|
1557
|
+
};
|
|
1050
1558
|
const getProjectHookList = (stage) => normalizeConfigHookList(projectHooks?.[stage]);
|
|
1559
|
+
const indicatorReplayKey = JSON.stringify({
|
|
1560
|
+
periods: indicatorPeriods,
|
|
1561
|
+
universe
|
|
1562
|
+
});
|
|
1563
|
+
const sharedReplayEnabled = canUseSharedReplayState({
|
|
1564
|
+
env,
|
|
1565
|
+
sharedReplayKey: sharedIndicatorsReplayKey
|
|
1566
|
+
});
|
|
1567
|
+
const indicatorSharedReplayKey = sharedReplayEnabled && sharedIndicatorsReplayKey ? `${sharedIndicatorsReplayKey}:indicators:${indicatorReplayKey}` : void 0;
|
|
1568
|
+
const strategyStateBaseKey = env === "CRON" && sharedStrategyStateKey ? sharedStrategyStateKey : sharedReplayEnabled ? sharedIndicatorsReplayKey : void 0;
|
|
1569
|
+
const strategySharedReplayKey = strategyStateBaseKey ? `${strategyStateBaseKey}:strategy:${strategyName}` : void 0;
|
|
1051
1570
|
const notifyRuntimeError = async ({
|
|
1052
1571
|
stage,
|
|
1053
1572
|
error,
|
|
@@ -1239,10 +1758,13 @@ var createStrategyRuntime = ({
|
|
|
1239
1758
|
env,
|
|
1240
1759
|
data,
|
|
1241
1760
|
btcData,
|
|
1761
|
+
ethData,
|
|
1242
1762
|
btcBinanceData,
|
|
1243
1763
|
btcCoinbaseData,
|
|
1244
|
-
periods:
|
|
1245
|
-
pluginRegistryScope: projectRoot
|
|
1764
|
+
periods: indicatorPeriods,
|
|
1765
|
+
pluginRegistryScope: projectRoot,
|
|
1766
|
+
sharedReplayKey: indicatorSharedReplayKey,
|
|
1767
|
+
useBtcReference: universe === "crypto"
|
|
1246
1768
|
});
|
|
1247
1769
|
const strategyApi = createStrategyAPI({
|
|
1248
1770
|
strategy: strategyName,
|
|
@@ -1252,9 +1774,9 @@ var createStrategyRuntime = ({
|
|
|
1252
1774
|
connector,
|
|
1253
1775
|
cachedData: data,
|
|
1254
1776
|
indicatorsState,
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1777
|
+
isConfigFromBacktest,
|
|
1778
|
+
sharedReplayKey: strategySharedReplayKey,
|
|
1779
|
+
getSharedReplayState: getSharedStrategyReplayState
|
|
1258
1780
|
});
|
|
1259
1781
|
const core = await createCore({
|
|
1260
1782
|
userName,
|
|
@@ -1264,9 +1786,12 @@ var createStrategyRuntime = ({
|
|
|
1264
1786
|
connector,
|
|
1265
1787
|
data,
|
|
1266
1788
|
btcData,
|
|
1789
|
+
ethData,
|
|
1267
1790
|
loadPineScriptFile,
|
|
1268
1791
|
strategyApi,
|
|
1269
|
-
indicatorsState
|
|
1792
|
+
indicatorsState,
|
|
1793
|
+
sharedReplayKey: strategySharedReplayKey,
|
|
1794
|
+
getSharedReplayState: getSharedStrategyReplayState
|
|
1270
1795
|
});
|
|
1271
1796
|
await invokeStageHooks("onInit", strategyManifest?.hooks?.onInit, {
|
|
1272
1797
|
ctx: getHookCtx(),
|
|
@@ -1275,10 +1800,147 @@ var createStrategyRuntime = ({
|
|
|
1275
1800
|
btcData
|
|
1276
1801
|
}
|
|
1277
1802
|
});
|
|
1278
|
-
|
|
1279
|
-
data.
|
|
1280
|
-
|
|
1281
|
-
|
|
1803
|
+
const appendCurrentMarketData = (candle, btcCandle, ethCandle) => {
|
|
1804
|
+
if (data[data.length - 1]?.timestamp !== candle.timestamp) {
|
|
1805
|
+
data.push(candle);
|
|
1806
|
+
}
|
|
1807
|
+
if (universe === "crypto" && btcData[btcData.length - 1]?.timestamp !== btcCandle.timestamp) {
|
|
1808
|
+
btcData.push(btcCandle);
|
|
1809
|
+
}
|
|
1810
|
+
if (universe === "crypto" && ethCandle && ethData[ethData.length - 1]?.timestamp !== ethCandle.timestamp) {
|
|
1811
|
+
ethData.push(ethCandle);
|
|
1812
|
+
}
|
|
1813
|
+
};
|
|
1814
|
+
const resolveEthCandle = (candle, ethCandle) => {
|
|
1815
|
+
if (ethCandle?.timestamp === candle.timestamp) {
|
|
1816
|
+
return ethCandle;
|
|
1817
|
+
}
|
|
1818
|
+
const alignedEthCandle = ethData[data.length - 1];
|
|
1819
|
+
if (alignedEthCandle?.timestamp === candle.timestamp) {
|
|
1820
|
+
return alignedEthCandle;
|
|
1821
|
+
}
|
|
1822
|
+
const latestEthCandle = ethData[ethData.length - 1];
|
|
1823
|
+
if (latestEthCandle?.timestamp === candle.timestamp) {
|
|
1824
|
+
return latestEthCandle;
|
|
1825
|
+
}
|
|
1826
|
+
return void 0;
|
|
1827
|
+
};
|
|
1828
|
+
const resolveBacktestExecutionCandle = (candle, btcCandle) => {
|
|
1829
|
+
const requestedExecutionTimestamp = candle.timestamp + backtestExecutionDelayMs;
|
|
1830
|
+
const primaryExecutionTimestamp = candle.timestamp;
|
|
1831
|
+
if (!canUseLowerBacktestExecution || primaryIntervalMs == null) {
|
|
1832
|
+
return {
|
|
1833
|
+
source: "lower_timeframe",
|
|
1834
|
+
requestedExecutionTimestamp,
|
|
1835
|
+
executionInterval: backtestExecutionIntervalLabel,
|
|
1836
|
+
executionDelayMs: backtestExecutionDelayMs,
|
|
1837
|
+
primaryExecutionTimestamp,
|
|
1838
|
+
skipReason: "BACKTEST_LOWER_EXECUTION_UNAVAILABLE"
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
if (requestedExecutionTimestamp >= candle.timestamp + primaryIntervalMs) {
|
|
1842
|
+
return {
|
|
1843
|
+
source: "lower_timeframe",
|
|
1844
|
+
requestedExecutionTimestamp,
|
|
1845
|
+
executionInterval: backtestExecutionIntervalLabel,
|
|
1846
|
+
executionDelayMs: backtestExecutionDelayMs,
|
|
1847
|
+
primaryExecutionTimestamp,
|
|
1848
|
+
skipReason: "BACKTEST_LOWER_EXECUTION_DELAY_OUT_OF_BAR"
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
const lowerCandle = backtestExecutionCandleByTimestamp.get(
|
|
1852
|
+
requestedExecutionTimestamp
|
|
1853
|
+
);
|
|
1854
|
+
const lowerBtcCandle = backtestExecutionBtcCandleByTimestamp.get(
|
|
1855
|
+
requestedExecutionTimestamp
|
|
1856
|
+
);
|
|
1857
|
+
if (lowerCandle && lowerBtcCandle) {
|
|
1858
|
+
return {
|
|
1859
|
+
candle: lowerCandle,
|
|
1860
|
+
btcCandle: lowerBtcCandle,
|
|
1861
|
+
source: "lower_timeframe",
|
|
1862
|
+
requestedExecutionTimestamp,
|
|
1863
|
+
executionInterval: backtestExecutionIntervalLabel,
|
|
1864
|
+
executionDelayMs: backtestExecutionDelayMs,
|
|
1865
|
+
primaryExecutionTimestamp
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
return {
|
|
1869
|
+
source: "lower_timeframe",
|
|
1870
|
+
requestedExecutionTimestamp,
|
|
1871
|
+
executionInterval: backtestExecutionIntervalLabel,
|
|
1872
|
+
executionDelayMs: backtestExecutionDelayMs,
|
|
1873
|
+
primaryExecutionTimestamp,
|
|
1874
|
+
skipReason: !lowerCandle ? "BACKTEST_LOWER_EXECUTION_CANDLE_MISSING" : "BACKTEST_LOWER_EXECUTION_BTC_CANDLE_MISSING"
|
|
1875
|
+
};
|
|
1876
|
+
};
|
|
1877
|
+
let pendingBacktestEntry = null;
|
|
1878
|
+
const flushPendingBacktestEntry = async (candle, btcCandle, ethCandle) => {
|
|
1879
|
+
if (!pendingBacktestEntry) {
|
|
1880
|
+
return void 0;
|
|
1881
|
+
}
|
|
1882
|
+
appendCurrentMarketData(candle, btcCandle, ethCandle);
|
|
1883
|
+
const resolvedEthCandle = resolveEthCandle(candle, ethCandle);
|
|
1884
|
+
indicatorsState.setCurrentBar(candle, btcCandle, resolvedEthCandle);
|
|
1885
|
+
pendingBacktestEntry.delayBarsRemaining -= 1;
|
|
1886
|
+
if (pendingBacktestEntry.delayBarsRemaining > 0) {
|
|
1887
|
+
return `BACKTEST_ENTRY_DELAY_PENDING:${pendingBacktestEntry.delayBarsRemaining}`;
|
|
1888
|
+
}
|
|
1889
|
+
const pending = pendingBacktestEntry;
|
|
1890
|
+
pendingBacktestEntry = null;
|
|
1891
|
+
const executionCandleResolution = resolveBacktestExecutionCandle(
|
|
1892
|
+
candle,
|
|
1893
|
+
btcCandle
|
|
1894
|
+
);
|
|
1895
|
+
const execution = applyBacktestDelayedEntryExecution({
|
|
1896
|
+
decision: pending.decision,
|
|
1897
|
+
execution: executionCandleResolution,
|
|
1898
|
+
backtestPriceMode,
|
|
1899
|
+
delayBars: pending.delayBars
|
|
1900
|
+
});
|
|
1901
|
+
if (execution.skipReason) {
|
|
1902
|
+
return pending.decision.signal ?? execution.skipReason;
|
|
1903
|
+
}
|
|
1904
|
+
if (!execution.executionCandle || !execution.btcExecutionCandle) {
|
|
1905
|
+
return pending.decision.signal ?? "BACKTEST_LOWER_EXECUTION_CANDLE_MISSING";
|
|
1906
|
+
}
|
|
1907
|
+
const market = {
|
|
1908
|
+
candle: execution.executionCandle,
|
|
1909
|
+
btcCandle: execution.btcExecutionCandle
|
|
1910
|
+
};
|
|
1911
|
+
const entry = buildHookEntry({
|
|
1912
|
+
decision: pending.decision,
|
|
1913
|
+
runtime: pending.runtime
|
|
1914
|
+
});
|
|
1915
|
+
return executeEntryDecision({
|
|
1916
|
+
connector,
|
|
1917
|
+
symbol,
|
|
1918
|
+
decision: pending.decision,
|
|
1919
|
+
runtime: pending.runtime,
|
|
1920
|
+
manifest: pending.manifest,
|
|
1921
|
+
hookCtx: pending.hookCtx,
|
|
1922
|
+
market,
|
|
1923
|
+
entry,
|
|
1924
|
+
policy: pending.policy,
|
|
1925
|
+
ml: pending.ml,
|
|
1926
|
+
ai: pending.ai,
|
|
1927
|
+
recordRuntimeJournal,
|
|
1928
|
+
invokeStageHooks,
|
|
1929
|
+
notifyRuntimeError
|
|
1930
|
+
});
|
|
1931
|
+
};
|
|
1932
|
+
const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
|
|
1933
|
+
appendCurrentMarketData(candle, btcCandle, options.ethCandle);
|
|
1934
|
+
const ethCandle = resolveEthCandle(candle, options.ethCandle);
|
|
1935
|
+
indicatorsState.setCurrentBar(candle, btcCandle, ethCandle);
|
|
1936
|
+
const delayedEntrySignal = await flushPendingBacktestEntry(
|
|
1937
|
+
candle,
|
|
1938
|
+
btcCandle,
|
|
1939
|
+
ethCandle
|
|
1940
|
+
);
|
|
1941
|
+
if (delayedEntrySignal) {
|
|
1942
|
+
return delayedEntrySignal;
|
|
1943
|
+
}
|
|
1282
1944
|
const market = {
|
|
1283
1945
|
candle,
|
|
1284
1946
|
btcCandle
|
|
@@ -1305,7 +1967,7 @@ var createStrategyRuntime = ({
|
|
|
1305
1967
|
if (isStrategyDecision(manifestOnBarDecision)) {
|
|
1306
1968
|
decision = manifestOnBarDecision;
|
|
1307
1969
|
} else {
|
|
1308
|
-
decision = await core(candle, btcCandle);
|
|
1970
|
+
decision = options.coreDecisionOverride ?? await core(candle, btcCandle);
|
|
1309
1971
|
shouldInvokeAfterCoreDecisionHook = true;
|
|
1310
1972
|
}
|
|
1311
1973
|
}
|
|
@@ -1387,6 +2049,7 @@ var createStrategyRuntime = ({
|
|
|
1387
2049
|
symbol,
|
|
1388
2050
|
decision,
|
|
1389
2051
|
market,
|
|
2052
|
+
onRuntimeClose,
|
|
1390
2053
|
onRuntimeError: async ({
|
|
1391
2054
|
stage,
|
|
1392
2055
|
error,
|
|
@@ -1429,9 +2092,25 @@ var createStrategyRuntime = ({
|
|
|
1429
2092
|
const runtime = resolveEntryRuntimePolicy({
|
|
1430
2093
|
decision,
|
|
1431
2094
|
config,
|
|
1432
|
-
manifest: decisionManifest
|
|
2095
|
+
manifest: decisionManifest,
|
|
2096
|
+
policyProfile: getPolicyProfile(decisionStrategyName)
|
|
1433
2097
|
});
|
|
1434
2098
|
const signal = decision.signal;
|
|
2099
|
+
if (signal) {
|
|
2100
|
+
if (universe) signal.universe = universe;
|
|
2101
|
+
if (assetClass) signal.assetClass = assetClass;
|
|
2102
|
+
if (accountId) signal.accountId = accountId;
|
|
2103
|
+
if (deploymentId) signal.deploymentId = deploymentId;
|
|
2104
|
+
if (runtimeConfigId) {
|
|
2105
|
+
signal.runtimeConfigId = runtimeConfigId;
|
|
2106
|
+
if (runtimeConfigId !== "config") {
|
|
2107
|
+
signal.signalId = `${signal.signalId}:${runtimeConfigId}`;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
if (decisionHookCtx.policyProfileId) {
|
|
2111
|
+
signal.policyProfileId = decisionHookCtx.policyProfileId;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
1435
2114
|
const entry = buildHookEntry({
|
|
1436
2115
|
decision,
|
|
1437
2116
|
runtime
|
|
@@ -1476,6 +2155,14 @@ var createStrategyRuntime = ({
|
|
|
1476
2155
|
let ai;
|
|
1477
2156
|
if (signal) {
|
|
1478
2157
|
try {
|
|
2158
|
+
await enrichSignalWithBinanceMarketContext({
|
|
2159
|
+
signal,
|
|
2160
|
+
env
|
|
2161
|
+
});
|
|
2162
|
+
await enrichSignalWithCoinMarketCapContext({
|
|
2163
|
+
signal,
|
|
2164
|
+
env
|
|
2165
|
+
});
|
|
1479
2166
|
await enrichSignalWithDerivativesContext({
|
|
1480
2167
|
signal,
|
|
1481
2168
|
env
|
|
@@ -1569,6 +2256,20 @@ var createStrategyRuntime = ({
|
|
|
1569
2256
|
}
|
|
1570
2257
|
return signal ?? skipReason;
|
|
1571
2258
|
}
|
|
2259
|
+
if (backtestEntryDelayBars > 0) {
|
|
2260
|
+
pendingBacktestEntry = {
|
|
2261
|
+
delayBars: backtestEntryDelayBars,
|
|
2262
|
+
delayBarsRemaining: backtestEntryDelayBars,
|
|
2263
|
+
decision,
|
|
2264
|
+
runtime,
|
|
2265
|
+
manifest: decisionManifest,
|
|
2266
|
+
hookCtx: decisionHookCtx,
|
|
2267
|
+
policy,
|
|
2268
|
+
ml,
|
|
2269
|
+
ai
|
|
2270
|
+
};
|
|
2271
|
+
return `BACKTEST_ENTRY_DELAY_QUEUED:${backtestEntryDelayBars}`;
|
|
2272
|
+
}
|
|
1572
2273
|
return executeEntryDecision({
|
|
1573
2274
|
connector,
|
|
1574
2275
|
symbol,
|
|
@@ -1586,7 +2287,41 @@ var createStrategyRuntime = ({
|
|
|
1586
2287
|
notifyRuntimeError
|
|
1587
2288
|
});
|
|
1588
2289
|
};
|
|
2290
|
+
const strategy = (async (candle, btcCandle, ethCandle) => runWithDecisionOverride(candle, btcCandle, { ethCandle }));
|
|
2291
|
+
strategy.__tradejsUpdateReferenceData = (params) => indicatorsState.updateReferenceData?.(params);
|
|
2292
|
+
strategy.__tradejsFlushBacktestDelayedEntry = flushPendingBacktestEntry;
|
|
2293
|
+
const resolvedDetectorKey = detectorKey?.(config);
|
|
2294
|
+
if (resolvedDetectorKey && detectorNoSignalSkipReason) {
|
|
2295
|
+
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;
|
|
2296
|
+
strategy.detectorFanoutKey = [strategyName, resolvedDetectorKey].join(
|
|
2297
|
+
":"
|
|
2298
|
+
);
|
|
2299
|
+
strategy.detectorNoSignalSkipReason = detectorNoSignalSkipReason;
|
|
2300
|
+
strategy.canFastAdvanceDetectorNoSignal = canFastAdvanceDetectorNoSignal;
|
|
2301
|
+
if (canFastAdvanceDetectorNoSignal) {
|
|
2302
|
+
strategy.advanceDetectorNoSignal = (candle, btcCandle, code) => {
|
|
2303
|
+
appendCurrentMarketData(candle, btcCandle);
|
|
2304
|
+
indicatorsState.setCurrentBar(
|
|
2305
|
+
candle,
|
|
2306
|
+
btcCandle,
|
|
2307
|
+
resolveEthCandle(candle)
|
|
2308
|
+
);
|
|
2309
|
+
return Promise.resolve(code);
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
strategy.skipDetectorNoSignal = (candle, btcCandle, code) => runWithDecisionOverride(candle, btcCandle, {
|
|
2313
|
+
coreDecisionOverride: strategyApi.skip(code)
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
return strategy;
|
|
1589
2317
|
};
|
|
2318
|
+
if (detectorKey) {
|
|
2319
|
+
creator.detectorKey = detectorKey;
|
|
2320
|
+
}
|
|
2321
|
+
if (detectorNoSignalSkipReason) {
|
|
2322
|
+
creator.detectorNoSignalSkipReason = detectorNoSignalSkipReason;
|
|
2323
|
+
}
|
|
2324
|
+
return creator;
|
|
1590
2325
|
};
|
|
1591
2326
|
|
|
1592
2327
|
// src/strategyHooks/closeOppositePositionsBeforeOpen.ts
|
|
@@ -1695,6 +2430,7 @@ var createCloseOppositeBeforePlaceOrderHook = ({
|
|
|
1695
2430
|
|
|
1696
2431
|
// src/strategyHooks/shared.ts
|
|
1697
2432
|
var DEFAULT_BREAK_EVEN_TRIGGER_RISK_MULTIPLIER = 0.5;
|
|
2433
|
+
var DEFAULT_BREAK_EVEN_STOP_PROFIT_MULTIPLIER = 0;
|
|
1698
2434
|
var DEFAULT_GLOBAL_UNREALIZED_PNL_TRIGGER_RISK_MULTIPLIER = 4;
|
|
1699
2435
|
var GLOBAL_UNREALIZED_PNL_CLOSE_ALL_CODE = "GLOBAL_UNREALIZED_PNL_TARGET_REACHED_CLOSE_ALL";
|
|
1700
2436
|
var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
@@ -1723,6 +2459,37 @@ var getPositionStopLossPrice = (position) => {
|
|
|
1723
2459
|
);
|
|
1724
2460
|
return Number.isFinite(signalStopLossPrice) ? signalStopLossPrice : null;
|
|
1725
2461
|
};
|
|
2462
|
+
var getPositionTakeProfitPrice = (position) => {
|
|
2463
|
+
if (!position || typeof position !== "object") {
|
|
2464
|
+
return null;
|
|
2465
|
+
}
|
|
2466
|
+
const directTakeProfitPrice = Number(
|
|
2467
|
+
position.tpPrice ?? position.takeProfitPrice ?? Number.NaN
|
|
2468
|
+
);
|
|
2469
|
+
if (Number.isFinite(directTakeProfitPrice)) {
|
|
2470
|
+
return directTakeProfitPrice;
|
|
2471
|
+
}
|
|
2472
|
+
const signalTakeProfitPrice = Number(
|
|
2473
|
+
position.signal?.prices?.takeProfitPrice ?? Number.NaN
|
|
2474
|
+
);
|
|
2475
|
+
return Number.isFinite(signalTakeProfitPrice) ? signalTakeProfitPrice : null;
|
|
2476
|
+
};
|
|
2477
|
+
var getBreakEvenStopPrice = ({
|
|
2478
|
+
direction,
|
|
2479
|
+
entryPrice,
|
|
2480
|
+
takeProfitPrice,
|
|
2481
|
+
stopProfitMultiplier
|
|
2482
|
+
}) => {
|
|
2483
|
+
if (!Number.isFinite(entryPrice)) {
|
|
2484
|
+
return null;
|
|
2485
|
+
}
|
|
2486
|
+
const normalizedStopProfitMultiplier = Number.isFinite(stopProfitMultiplier) ? Math.min(Math.max(stopProfitMultiplier, 0), 1) : DEFAULT_BREAK_EVEN_STOP_PROFIT_MULTIPLIER;
|
|
2487
|
+
if (takeProfitPrice == null || !Number.isFinite(takeProfitPrice) || direction === "LONG" && takeProfitPrice <= entryPrice || direction === "SHORT" && takeProfitPrice >= entryPrice) {
|
|
2488
|
+
return entryPrice;
|
|
2489
|
+
}
|
|
2490
|
+
const distanceToTakeProfit = takeProfitPrice - entryPrice;
|
|
2491
|
+
return entryPrice + distanceToTakeProfit * normalizedStopProfitMultiplier;
|
|
2492
|
+
};
|
|
1726
2493
|
var getFavorableMovePct = ({
|
|
1727
2494
|
direction,
|
|
1728
2495
|
entryPrice,
|
|
@@ -1784,7 +2551,8 @@ var toStrategyCodePrefix = (strategyName) => strategyName === "TrendLine" ? "TRE
|
|
|
1784
2551
|
// src/strategyHooks/moveStopToBreakEvenAfterCoreDecision.ts
|
|
1785
2552
|
var createMoveStopToBreakEvenOnBarHook = ({
|
|
1786
2553
|
isEnabled = () => true,
|
|
1787
|
-
triggerRiskMultiplier = DEFAULT_BREAK_EVEN_TRIGGER_RISK_MULTIPLIER
|
|
2554
|
+
triggerRiskMultiplier = DEFAULT_BREAK_EVEN_TRIGGER_RISK_MULTIPLIER,
|
|
2555
|
+
stopProfitMultiplier = DEFAULT_BREAK_EVEN_STOP_PROFIT_MULTIPLIER
|
|
1788
2556
|
} = {}) => {
|
|
1789
2557
|
return async ({ ctx, market }) => {
|
|
1790
2558
|
if (!isEnabled(ctx.strategyConfig)) {
|
|
@@ -1824,12 +2592,21 @@ var createMoveStopToBreakEvenOnBarHook = ({
|
|
|
1824
2592
|
if (favorableMovePct == null || triggerRiskPct == null || favorableMovePct < triggerRiskPct * triggerRiskMultiplier) {
|
|
1825
2593
|
return;
|
|
1826
2594
|
}
|
|
2595
|
+
const stopLossPrice = getBreakEvenStopPrice({
|
|
2596
|
+
direction: currentPosition.direction,
|
|
2597
|
+
entryPrice: currentPosition.price,
|
|
2598
|
+
takeProfitPrice: getPositionTakeProfitPrice(currentPosition),
|
|
2599
|
+
stopProfitMultiplier
|
|
2600
|
+
});
|
|
2601
|
+
if (stopLossPrice == null) {
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
1827
2604
|
return {
|
|
1828
2605
|
kind: "protect",
|
|
1829
2606
|
code: `${toStrategyCodePrefix(ctx.strategyName)}_MOVE_STOP_TO_BREAK_EVEN`,
|
|
1830
2607
|
protectPlan: {
|
|
1831
2608
|
direction: currentPosition.direction,
|
|
1832
|
-
stopLossPrice
|
|
2609
|
+
stopLossPrice
|
|
1833
2610
|
}
|
|
1834
2611
|
};
|
|
1835
2612
|
};
|
|
@@ -1921,6 +2698,7 @@ export {
|
|
|
1921
2698
|
buildAiPayload,
|
|
1922
2699
|
buildAiPrompts,
|
|
1923
2700
|
buildAiSystemPrompt,
|
|
2701
|
+
buildCompactAiIndicatorsSnapshot,
|
|
1924
2702
|
closeOppositePositionsBeforeOpen,
|
|
1925
2703
|
createCloseAllOnGlobalProfitBeforeSignalsHook,
|
|
1926
2704
|
createCloseOppositeBeforePlaceOrderHook,
|
|
@@ -1928,6 +2706,8 @@ export {
|
|
|
1928
2706
|
createMoveStopToBreakEvenOnBarHook,
|
|
1929
2707
|
createStrategyRuntime,
|
|
1930
2708
|
enrichSignalWithAi,
|
|
2709
|
+
enrichSignalWithBinanceMarketContext,
|
|
2710
|
+
enrichSignalWithCoinMarketCapContext,
|
|
1931
2711
|
enrichSignalWithMl,
|
|
1932
2712
|
enrichSignalWithMlAi,
|
|
1933
2713
|
ensureAiStrategyPluginsLoaded,
|