@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.
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  buildMlPayload,
3
+ enrichSignalWithBinanceMarketContext,
4
+ enrichSignalWithCoinMarketCapContext,
3
5
  enrichSignalWithDerivativesContext
4
- } from "./chunk-JRRG3YQG.mjs";
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-2JKX3DM7.mjs";
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-WGOYR6AB.mjs";
44
+ } from "./chunk-QVSMINLG.mjs";
41
45
  import {
42
46
  getTradejsProjectCwd,
43
47
  loadTradejsConfig
44
- } from "./chunk-JU77QVJ3.mjs";
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 { SIGNALS_PRELOAD_DAYS } from "@tradejs/core/constants";
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 { redisKeys as redisKeys2, setData as setData2 } from "@tradejs/infra/redis";
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 { delKey, getData, redisKeys, setData } from "@tradejs/infra/redis";
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 toOrderId = () => `tjs-${randomUUID().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
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 = () => toOrderId();
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 markRuntimeTradeClosed = async (params) => {
126
- const { userName, symbol, strategy, exitPrice, exitTimestamp, closedPnl } = params;
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(redisKeys.runtimeActiveTrade(userName, symbol));
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
- delKey(redisKeys.runtimeActiveTrade(userName, symbol))
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 minQuality = ai.minQuality ?? 4;
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
- try {
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 { askAI: askAI2 } = await import("./ai.mjs");
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: currentPrice,
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
- const currentPosition = await connector.getPosition(symbol);
435
- const entryPrice = currentPosition?.price && Number.isFinite(currentPosition.price) ? currentPosition.price : currentPrice;
436
- signal.prices.currentPrice = entryPrice;
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 redisKeys3 } from "@tradejs/infra/redis";
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
- redisKeys3.strategyConfig(userName, strategyName),
652
+ runtimeConfigId ? redisKeys2.strategyConfig(userName, strategyName, runtimeConfigId) : redisKeys2.strategyConfig(userName, strategyName),
496
653
  {}
497
654
  );
498
655
  config = mergeIfNotEmpty(config, userConfig);
499
- const results = await getData2(
500
- redisKeys3.strategyResults(userName, strategyName),
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
- isConfigFromBacktest = true;
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 manifestDefaults = manifest?.entryRuntimeDefaults;
522
- const adapterMl = manifest?.mlAdapter?.mapEntryRuntimeFromConfig?.(config);
523
- const adapterAi = manifest?.aiAdapter?.mapEntryRuntimeFromConfig?.(config);
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
- return async ({
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
- connector
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) => buildHookCtx({
1047
- ...hookBase,
1048
- strategyName: name
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: buildDefaultIndicatorPeriods(config),
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
- preloadStart: getTimestamp(SIGNALS_PRELOAD_DAYS),
1256
- backtestPriceMode: config.BACKTEST_PRICE_MODE,
1257
- isConfigFromBacktest
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
- return async (candle, btcCandle) => {
1279
- data.push(candle);
1280
- btcData.push(btcCandle);
1281
- indicatorsState.setCurrentBar(candle, btcCandle);
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: currentPosition.price
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,