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