@tradejs/node 1.0.8 → 1.0.10

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