@tradejs/node 1.0.6 → 1.0.9

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,9 +1,10 @@
1
1
  import {
2
- buildMlPayload
3
- } from "./chunk-PQH5PAFC.mjs";
2
+ buildMlPayload,
3
+ enrichSignalWithDerivativesContext
4
+ } from "./chunk-JRRG3YQG.mjs";
4
5
  import {
5
6
  require_lodash
6
- } from "./chunk-GKDBAF3A.mjs";
7
+ } from "./chunk-KZDHZ56N.mjs";
7
8
  import {
8
9
  DEFAULT_AI_MODEL,
9
10
  MAX_AI_SERIES_POINTS,
@@ -19,7 +20,7 @@ import {
19
20
  runAiPrompt,
20
21
  runAiPromptLocal,
21
22
  trimSeriesDeep
22
- } from "./chunk-72FKXJ2I.mjs";
23
+ } from "./chunk-2JKX3DM7.mjs";
23
24
  import {
24
25
  createPineScriptLoader
25
26
  } from "./chunk-H6LIYHU4.mjs";
@@ -36,102 +37,15 @@ import {
36
37
  registerStrategyEntries,
37
38
  resetStrategyRegistryCache,
38
39
  strategies
39
- } from "./chunk-EOZSJKUM.mjs";
40
+ } from "./chunk-WGOYR6AB.mjs";
40
41
  import {
41
- getTradejsProjectCwd
42
- } from "./chunk-CGJ2UU6H.mjs";
42
+ getTradejsProjectCwd,
43
+ loadTradejsConfig
44
+ } from "./chunk-JU77QVJ3.mjs";
43
45
  import {
44
46
  __toESM
45
47
  } from "./chunk-6DZX6EAA.mjs";
46
48
 
47
- // src/closeOppositePositionsBeforeOpen.ts
48
- var import_lodash = __toESM(require_lodash());
49
- import { logger } from "@tradejs/infra/logger";
50
- var closeOppositePositionsBeforeOpen = async ({
51
- connector,
52
- entryContext
53
- }) => {
54
- const {
55
- symbol: currentSymbol,
56
- direction: currentDirection,
57
- timestamp,
58
- prices,
59
- strategy: strategyName
60
- } = entryContext;
61
- const price = prices.currentPrice;
62
- try {
63
- logger.log(
64
- "info",
65
- "[%s] checking open positions before open: %s %s",
66
- strategyName,
67
- currentSymbol,
68
- currentDirection
69
- );
70
- const positions = await connector.getPositions();
71
- const openPositions = (positions || []).filter(
72
- (item) => item && Number(item.qty) > 0
73
- );
74
- logger.log(
75
- "info",
76
- "[%s] open positions found: %s",
77
- strategyName,
78
- openPositions.length
79
- );
80
- const oppositePositions = openPositions.filter(
81
- (item) => item.symbol !== currentSymbol && item.direction !== currentDirection
82
- );
83
- if (import_lodash.default.isEmpty(oppositePositions)) {
84
- logger.log(
85
- "info",
86
- "[%s] no opposite positions to close before open: %s",
87
- strategyName,
88
- currentSymbol
89
- );
90
- return;
91
- }
92
- for (const position of oppositePositions) {
93
- logger.log(
94
- "info",
95
- "[%s] closing opposite position: %s %s qty=%s",
96
- strategyName,
97
- position.symbol,
98
- position.direction,
99
- position.qty
100
- );
101
- try {
102
- await connector.closePosition({
103
- symbol: position.symbol,
104
- price,
105
- timestamp,
106
- direction: position.direction
107
- });
108
- logger.log(
109
- "info",
110
- "[%s] opposite position closed: %s",
111
- strategyName,
112
- position.symbol
113
- );
114
- } catch (err) {
115
- logger.log(
116
- "error",
117
- "[%s] failed to close opposite position: %s %s",
118
- strategyName,
119
- position.symbol,
120
- err
121
- );
122
- }
123
- }
124
- } catch (err) {
125
- logger.log(
126
- "error",
127
- "[%s] failed to load open positions before open: %s %s",
128
- strategyName,
129
- currentSymbol,
130
- err
131
- );
132
- }
133
- };
134
-
135
49
  // src/strategies.ts
136
50
  export * from "@tradejs/core/strategies";
137
51
 
@@ -148,12 +62,125 @@ import { logger as logger3 } from "@tradejs/infra/logger";
148
62
 
149
63
  // src/strategyHelpers/runtime.ts
150
64
  import { logger as logger2 } from "@tradejs/infra/logger";
65
+ import { redisKeys as redisKeys2, setData as setData2 } from "@tradejs/infra/redis";
151
66
  import {
152
67
  buildMlFeatures,
153
68
  buildMlTrainingRow,
154
69
  fetchMlThreshold,
155
70
  trimMlTrainingRowWindows
156
71
  } from "@tradejs/infra/ml";
72
+
73
+ // src/runtimeJournal.ts
74
+ import { randomUUID } from "crypto";
75
+ import { TTL_1M } from "@tradejs/core/constants";
76
+ import { logger } from "@tradejs/infra/logger";
77
+ import { delKey, getData, redisKeys, setData } from "@tradejs/infra/redis";
78
+ var now = () => Date.now();
79
+ var toOrderId = () => `tjs-${randomUUID().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
80
+ var calculateClosedPnl = ({
81
+ direction,
82
+ entryPrice,
83
+ exitPrice,
84
+ qty
85
+ }) => {
86
+ const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
87
+ return Number.isFinite(pnl) ? pnl : null;
88
+ };
89
+ var createRuntimeOrderId = () => toOrderId();
90
+ var recordRuntimeTradeOpen = async (params) => {
91
+ const { userName } = params;
92
+ if (!userName) {
93
+ return null;
94
+ }
95
+ const record = {
96
+ ...params,
97
+ status: "active",
98
+ currentPrice: params.entryPrice,
99
+ currentPnl: 0,
100
+ closedPnl: null,
101
+ exitPrice: null,
102
+ exitTimestamp: null,
103
+ lastSyncedAt: now()
104
+ };
105
+ try {
106
+ await Promise.all([
107
+ setData(redisKeys.runtimeTrade(userName, record.orderId), record, {
108
+ expire: 0
109
+ }),
110
+ setData(
111
+ redisKeys.runtimeActiveTrade(userName, record.symbol),
112
+ { orderId: record.orderId },
113
+ { expire: 0 }
114
+ )
115
+ ]);
116
+ } catch (error) {
117
+ logger.error(
118
+ "runtime trade open journal failed: %s %s",
119
+ record.symbol,
120
+ error?.message || String(error)
121
+ );
122
+ }
123
+ return record;
124
+ };
125
+ var markRuntimeTradeClosed = async (params) => {
126
+ const { userName, symbol, strategy, exitPrice, exitTimestamp, closedPnl } = params;
127
+ if (!userName) {
128
+ return null;
129
+ }
130
+ const activeRef = await getData(
131
+ redisKeys.runtimeActiveTrade(userName, symbol),
132
+ null
133
+ );
134
+ const orderId = String(activeRef?.orderId || "").trim();
135
+ if (!orderId) {
136
+ return null;
137
+ }
138
+ const existing = await getData(
139
+ redisKeys.runtimeTrade(userName, orderId),
140
+ null
141
+ );
142
+ if (!existing) {
143
+ await delKey(redisKeys.runtimeActiveTrade(userName, symbol));
144
+ return null;
145
+ }
146
+ if (strategy && existing.strategy !== strategy) {
147
+ return null;
148
+ }
149
+ const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
150
+ const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
151
+ direction: existing.direction,
152
+ entryPrice: existing.entryPrice,
153
+ exitPrice: resolvedExitPrice,
154
+ qty: existing.qty
155
+ }) : existing.closedPnl ?? existing.currentPnl ?? null;
156
+ const next = {
157
+ ...existing,
158
+ status: "closed",
159
+ currentPrice: resolvedExitPrice,
160
+ currentPnl: resolvedClosedPnl,
161
+ closedPnl: resolvedClosedPnl,
162
+ exitPrice: resolvedExitPrice,
163
+ exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
164
+ lastSyncedAt: now()
165
+ };
166
+ try {
167
+ await Promise.all([
168
+ setData(redisKeys.runtimeTrade(userName, orderId), next, {
169
+ expire: TTL_1M
170
+ }),
171
+ delKey(redisKeys.runtimeActiveTrade(userName, symbol))
172
+ ]);
173
+ } catch (error) {
174
+ logger.error(
175
+ "runtime trade close journal failed: %s %s",
176
+ symbol,
177
+ error?.message || String(error)
178
+ );
179
+ }
180
+ return next;
181
+ };
182
+
183
+ // src/strategyHelpers/runtime.ts
157
184
  var formatAiError = (err) => {
158
185
  const error = err;
159
186
  const safeJson = (value) => {
@@ -172,6 +199,60 @@ var formatAiError = (err) => {
172
199
  };
173
200
  return safeJson(details);
174
201
  };
202
+ var resolveAiQuality = (analysis, direction) => {
203
+ if (typeof analysis?.quality !== "number") {
204
+ return void 0;
205
+ }
206
+ const normalizedQuality = Math.round(analysis.quality);
207
+ const aiApprovedCurrentTrade = analysis.direction === direction;
208
+ return aiApprovedCurrentTrade ? normalizedQuality : 0;
209
+ };
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
+ var findReplayAiAnalysis = ({
231
+ signal,
232
+ direction,
233
+ ai
234
+ }) => {
235
+ const snapshots = ai?.replayAnalyses;
236
+ if (!Array.isArray(snapshots) || !snapshots.length) {
237
+ return void 0;
238
+ }
239
+ let best = null;
240
+ for (const snapshot of snapshots) {
241
+ if (snapshot.symbol !== signal.symbol || snapshot.direction !== direction || snapshot.strategy && snapshot.strategy !== signal.strategy) {
242
+ continue;
243
+ }
244
+ const toleranceMs = Math.max(0, Number(snapshot.toleranceMs ?? 0));
245
+ const diff = Math.abs(snapshot.timestamp - signal.timestamp);
246
+ if (diff > toleranceMs || best && diff >= best.diff) {
247
+ continue;
248
+ }
249
+ best = {
250
+ diff,
251
+ analysis: snapshot.analysis
252
+ };
253
+ }
254
+ return best?.analysis;
255
+ };
175
256
  var enrichSignalWithMl = async ({
176
257
  signal,
177
258
  env,
@@ -211,17 +292,48 @@ var enrichSignalWithAi = async ({
211
292
  env,
212
293
  ai
213
294
  }) => {
214
- if (env === "BACKTEST" || ai?.enabled === false) {
295
+ if (ai?.enabled === false) {
215
296
  return void 0;
216
297
  }
298
+ if (env === "PARITY") {
299
+ const replayAnalysis = findReplayAiAnalysis({ signal, direction, ai });
300
+ if (replayAnalysis) {
301
+ signal.aiAnalysis = replayAnalysis;
302
+ return resolveAiQuality(replayAnalysis, direction);
303
+ }
304
+ }
305
+ if (env === "BACKTEST") {
306
+ return void 0;
307
+ }
308
+ 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);
312
+ 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
+ }
330
+ return gateQuality;
331
+ }
217
332
  try {
218
333
  const { askAI: askAI2 } = await import("./ai.mjs");
219
334
  const analysis = await askAI2(signal, { userName });
220
- if (typeof analysis?.quality === "number") {
221
- const normalizedQuality = Math.round(analysis.quality);
222
- const aiApprovedCurrentTrade = analysis?.direction === direction;
223
- return aiApprovedCurrentTrade ? normalizedQuality : 0;
224
- }
335
+ signal.aiAnalysis = analysis;
336
+ return resolveAiQuality(analysis, direction);
225
337
  } catch (err) {
226
338
  logger2.error("AI analysis error: %s %s", symbol, formatAiError(err));
227
339
  }
@@ -236,6 +348,7 @@ var enrichSignalWithMlAi = async ({
236
348
  ml,
237
349
  ai
238
350
  }) => {
351
+ await enrichSignalWithDerivativesContext({ signal, env });
239
352
  await enrichSignalWithMl({ signal, env, ml });
240
353
  return enrichSignalWithAi({ signal, userName, symbol, direction, env, ai });
241
354
  };
@@ -271,6 +384,7 @@ var applyProtectiveOrders = async ({
271
384
  };
272
385
  var executeEntryOrder = async ({
273
386
  connector,
387
+ userName,
274
388
  symbol,
275
389
  direction,
276
390
  qty,
@@ -279,9 +393,12 @@ var executeEntryOrder = async ({
279
393
  takeProfits,
280
394
  stopLossPrice,
281
395
  signal,
282
- beforePlaceOrder
396
+ beforePlaceOrder,
397
+ recordRuntimeTrade = true
283
398
  }) => {
284
399
  await beforePlaceOrder?.();
400
+ const orderId = signal.orderId || createRuntimeOrderId();
401
+ signal.orderId = orderId;
285
402
  const orderPlaced = await connector.placeOrder({
286
403
  symbol,
287
404
  qty,
@@ -289,6 +406,7 @@ var executeEntryOrder = async ({
289
406
  isLimit: false,
290
407
  timestamp,
291
408
  direction,
409
+ orderId,
292
410
  signal
293
411
  });
294
412
  if (orderPlaced) {
@@ -314,11 +432,26 @@ var executeEntryOrder = async ({
314
432
  signal.orderStatus = orderPlaced ? "completed" : "failed";
315
433
  signal.orderSkipReason = void 0;
316
434
  const currentPosition = await connector.getPosition(symbol);
435
+ const entryPrice = currentPosition?.price && Number.isFinite(currentPosition.price) ? currentPosition.price : currentPrice;
436
+ signal.prices.currentPrice = entryPrice;
437
+ if (orderPlaced && recordRuntimeTrade) {
438
+ await recordRuntimeTradeOpen({
439
+ userName,
440
+ orderId,
441
+ signalId: signal.signalId,
442
+ strategy: signal.strategy,
443
+ symbol,
444
+ direction,
445
+ qty,
446
+ entryPrice,
447
+ entryTimestamp: timestamp,
448
+ ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {}
449
+ });
450
+ }
317
451
  if (currentPosition?.price) {
318
- signal.prices.currentPrice = currentPosition.price;
319
452
  return currentPosition.price;
320
453
  }
321
- return currentPrice;
454
+ return entryPrice;
322
455
  };
323
456
  var updatePositionProtection = async ({
324
457
  connector,
@@ -339,8 +472,8 @@ var updatePositionProtection = async ({
339
472
  };
340
473
 
341
474
  // src/strategyHelpers/config.ts
342
- var import_lodash2 = __toESM(require_lodash());
343
- import { getData, redisKeys } from "@tradejs/infra/redis";
475
+ var import_lodash = __toESM(require_lodash());
476
+ import { getData as getData2, redisKeys as redisKeys3 } from "@tradejs/infra/redis";
344
477
  var resolveStrategyConfig = async ({
345
478
  strategyName,
346
479
  userName,
@@ -348,7 +481,7 @@ var resolveStrategyConfig = async ({
348
481
  baseConfig,
349
482
  defaults
350
483
  }) => {
351
- const mergeIfNotEmpty = (target, patch) => patch && !import_lodash2.default.isEmpty(patch) ? {
484
+ const mergeIfNotEmpty = (target, patch) => patch && !import_lodash.default.isEmpty(patch) ? {
352
485
  ...target,
353
486
  ...patch
354
487
  } : target;
@@ -358,17 +491,17 @@ var resolveStrategyConfig = async ({
358
491
  };
359
492
  let isConfigFromBacktest = false;
360
493
  if (config.ENV !== "BACKTEST") {
361
- const userConfig = await getData(
362
- redisKeys.strategyConfig(userName, strategyName),
494
+ const userConfig = await getData2(
495
+ redisKeys3.strategyConfig(userName, strategyName),
363
496
  {}
364
497
  );
365
498
  config = mergeIfNotEmpty(config, userConfig);
366
- const results = await getData(
367
- redisKeys.strategyResults(userName, strategyName),
499
+ const results = await getData2(
500
+ redisKeys3.strategyResults(userName, strategyName),
368
501
  {}
369
502
  );
370
503
  const backtestResult = results?.[symbol];
371
- if (backtestResult && !import_lodash2.default.isEmpty(backtestResult.config)) {
504
+ if (backtestResult && !import_lodash.default.isEmpty(backtestResult.config)) {
372
505
  config = mergeIfNotEmpty(
373
506
  config,
374
507
  backtestResult.config
@@ -405,10 +538,26 @@ var resolveEntryRuntimePolicy = ({
405
538
  ai
406
539
  };
407
540
  };
541
+ var formatGateNumber = (value) => {
542
+ if (!Number.isFinite(value)) {
543
+ return String(value);
544
+ }
545
+ const normalized = Number(value.toFixed(6));
546
+ return Number.isInteger(normalized) ? String(normalized) : String(normalized);
547
+ };
548
+ var isMlRuntimeGateEnabled = (params) => {
549
+ const { env, ml } = params;
550
+ return env !== "BACKTEST" && ml?.config != null && ml.config.enabled !== false;
551
+ };
552
+ var isMlResultUnavailable = (params) => {
553
+ const { env, ml } = params;
554
+ return isMlRuntimeGateEnabled({ env, ml }) && ml?.result == null;
555
+ };
408
556
  var shouldExecuteEntryDecision = ({
409
557
  makeOrdersEnabled,
410
558
  env,
411
559
  signal,
560
+ ml,
412
561
  aiEnabled,
413
562
  quality,
414
563
  minAiQuality
@@ -416,7 +565,16 @@ var shouldExecuteEntryDecision = ({
416
565
  if (!makeOrdersEnabled) {
417
566
  return false;
418
567
  }
419
- if (!signal || env === "BACKTEST" || !aiEnabled) {
568
+ if (!signal || env === "BACKTEST") {
569
+ return true;
570
+ }
571
+ if (isMlResultUnavailable({ env, ml })) {
572
+ return false;
573
+ }
574
+ if (isMlRuntimeGateEnabled({ env, ml }) && ml?.result?.passed === false) {
575
+ return false;
576
+ }
577
+ if (!aiEnabled) {
420
578
  return true;
421
579
  }
422
580
  return Number.isFinite(quality) && quality >= minAiQuality;
@@ -424,6 +582,7 @@ var shouldExecuteEntryDecision = ({
424
582
  var getEntrySkipReason = ({
425
583
  makeOrdersEnabled,
426
584
  env,
585
+ ml,
427
586
  aiEnabled,
428
587
  quality,
429
588
  minAiQuality
@@ -431,6 +590,14 @@ var getEntrySkipReason = ({
431
590
  if (!makeOrdersEnabled) {
432
591
  return "MAKE_ORDERS_DISABLED";
433
592
  }
593
+ if (isMlResultUnavailable({ env, ml })) {
594
+ return "ML_RESULT_UNAVAILABLE";
595
+ }
596
+ if (isMlRuntimeGateEnabled({ env, ml }) && ml?.result?.passed === false) {
597
+ const probability = formatGateNumber(ml.result.probability);
598
+ const threshold = formatGateNumber(ml.result.threshold);
599
+ return `ML_THRESHOLD_NOT_MET (${probability} < ${threshold})`;
600
+ }
434
601
  if (env !== "BACKTEST" && aiEnabled && quality == null) {
435
602
  return "AI_QUALITY_UNAVAILABLE";
436
603
  }
@@ -439,6 +606,35 @@ var getEntrySkipReason = ({
439
606
  }
440
607
  return "ENTRY_POLICY_BLOCKED";
441
608
  };
609
+ var normalizeConfigHookList = (value) => {
610
+ if (Array.isArray(value)) {
611
+ return value;
612
+ }
613
+ return value ? [value] : [];
614
+ };
615
+ var isStrategyDecision = (value) => {
616
+ if (!value || typeof value !== "object") {
617
+ return false;
618
+ }
619
+ const kind = value.kind;
620
+ return kind === "skip" || kind === "entry" || kind === "exit" || kind === "protect";
621
+ };
622
+ var CONFIG_HOOK_STAGES = [
623
+ "onInit",
624
+ "onBar",
625
+ "afterCoreDecision",
626
+ "afterBarDecision",
627
+ "onSkip",
628
+ "beforeClosePosition",
629
+ "afterEnrichMl",
630
+ "afterEnrichAi",
631
+ "beforeEntryGate",
632
+ "beforePlaceOrder",
633
+ "afterPlaceOrder"
634
+ ];
635
+ var isConfigHookStage = (stage) => CONFIG_HOOK_STAGES.includes(
636
+ stage
637
+ );
442
638
  var buildHookCtx = ({
443
639
  connector,
444
640
  strategyName,
@@ -477,6 +673,13 @@ var buildHookPolicy = ({
477
673
  makeOrdersEnabled,
478
674
  minAiQuality
479
675
  });
676
+ var shouldRecordRuntimeJournal = ({
677
+ env,
678
+ config
679
+ }) => env !== "BACKTEST" && env !== "PARITY" && config.RECORD_RUNTIME_TRADES !== false;
680
+ var isTestConnector = (connector) => Boolean(
681
+ connector.__tradejsTestConnector
682
+ );
480
683
  var buildMlHookContext = ({
481
684
  signal,
482
685
  env,
@@ -581,6 +784,8 @@ var buildAiHookContext = ({
581
784
  };
582
785
  var handleExitDecision = async ({
583
786
  connector,
787
+ userName,
788
+ strategyName,
584
789
  symbol,
585
790
  decision,
586
791
  market,
@@ -593,6 +798,13 @@ var handleExitDecision = async ({
593
798
  timestamp: decision.closePlan.timestamp,
594
799
  direction: decision.closePlan.direction
595
800
  });
801
+ await markRuntimeTradeClosed({
802
+ userName,
803
+ strategy: strategyName,
804
+ symbol,
805
+ exitPrice: decision.closePlan.price,
806
+ exitTimestamp: decision.closePlan.timestamp
807
+ });
596
808
  } catch (err) {
597
809
  await onRuntimeError?.({
598
810
  stage: "closePosition",
@@ -644,12 +856,13 @@ var executeEntryDecision = async ({
644
856
  policy,
645
857
  ml,
646
858
  ai,
647
- invokeHook,
859
+ recordRuntimeJournal,
860
+ invokeStageHooks,
648
861
  notifyRuntimeError
649
862
  }) => {
650
863
  const signal = decision.signal;
651
864
  const beforePlaceOrder = async () => {
652
- await invokeHook(
865
+ await invokeStageHooks(
653
866
  "beforePlaceOrder",
654
867
  manifest?.hooks?.beforePlaceOrder,
655
868
  {
@@ -680,6 +893,7 @@ var executeEntryDecision = async ({
680
893
  if (signal) {
681
894
  await executeEntryOrder({
682
895
  connector,
896
+ userName: hookCtx.userName,
683
897
  symbol,
684
898
  direction: decision.entryContext.direction,
685
899
  qty: decision.orderPlan.qty,
@@ -688,9 +902,10 @@ var executeEntryDecision = async ({
688
902
  takeProfits: decision.orderPlan.takeProfits,
689
903
  stopLossPrice: decision.orderPlan.stopLossPrice,
690
904
  signal,
691
- beforePlaceOrder
905
+ beforePlaceOrder,
906
+ recordRuntimeTrade: recordRuntimeJournal
692
907
  });
693
- await invokeHook(
908
+ await invokeStageHooks(
694
909
  "afterPlaceOrder",
695
910
  manifest?.hooks?.afterPlaceOrder,
696
911
  {
@@ -738,7 +953,7 @@ var executeEntryDecision = async ({
738
953
  });
739
954
  throw error;
740
955
  }
741
- await invokeHook(
956
+ await invokeStageHooks(
742
957
  "afterPlaceOrder",
743
958
  manifest?.hooks?.afterPlaceOrder,
744
959
  {
@@ -814,7 +1029,10 @@ var createStrategyRuntime = ({
814
1029
  baseConfig,
815
1030
  defaults
816
1031
  });
1032
+ const projectConfig = await loadTradejsConfig(projectRoot);
1033
+ const projectHooks = projectConfig.hooks;
817
1034
  const env = String(config.ENV ?? "BACKTEST");
1035
+ const recordRuntimeJournal = shouldRecordRuntimeJournal({ env, config });
818
1036
  const strategyManifest = resolveManifest(strategyName);
819
1037
  const hookBase = {
820
1038
  connector,
@@ -829,6 +1047,7 @@ var createStrategyRuntime = ({
829
1047
  ...hookBase,
830
1048
  strategyName: name
831
1049
  });
1050
+ const getProjectHookList = (stage) => normalizeConfigHookList(projectHooks?.[stage]);
832
1051
  const notifyRuntimeError = async ({
833
1052
  stage,
834
1053
  error,
@@ -838,21 +1057,33 @@ var createStrategyRuntime = ({
838
1057
  }) => {
839
1058
  const errorStrategyName = decision?.kind === "entry" ? decision.entryContext.strategy : strategyName;
840
1059
  const errorManifest = resolveManifest(errorStrategyName) ?? strategyManifest;
1060
+ const errorParams = {
1061
+ ctx: getHookCtx(errorStrategyName),
1062
+ market,
1063
+ decision,
1064
+ entry,
1065
+ error: {
1066
+ stage,
1067
+ cause: error
1068
+ }
1069
+ };
1070
+ for (const projectHook of getProjectHookList("onRuntimeError")) {
1071
+ try {
1072
+ await projectHook(errorParams);
1073
+ } catch (hookError) {
1074
+ logger3.error(
1075
+ "project hook onRuntimeError failed: %s %s",
1076
+ strategyName,
1077
+ hookError
1078
+ );
1079
+ }
1080
+ }
841
1081
  const onRuntimeError = errorManifest?.hooks?.onRuntimeError;
842
1082
  if (!onRuntimeError) {
843
1083
  return;
844
1084
  }
845
1085
  try {
846
- await onRuntimeError({
847
- ctx: getHookCtx(errorStrategyName),
848
- market,
849
- decision,
850
- entry,
851
- error: {
852
- stage,
853
- cause: error
854
- }
855
- });
1086
+ await onRuntimeError(errorParams);
856
1087
  } catch (hookError) {
857
1088
  logger3.error(
858
1089
  "runtime hook onRuntimeError failed: %s %s",
@@ -884,6 +1115,126 @@ var createStrategyRuntime = ({
884
1115
  return void 0;
885
1116
  }
886
1117
  };
1118
+ const invokeProjectHooks = async (stage, params, errorContext = {}) => {
1119
+ const results = [];
1120
+ for (const hook of getProjectHookList(stage)) {
1121
+ const result = await invokeHook(
1122
+ stage,
1123
+ hook,
1124
+ params,
1125
+ errorContext
1126
+ );
1127
+ if (result !== void 0) {
1128
+ results.push(result);
1129
+ }
1130
+ }
1131
+ return results;
1132
+ };
1133
+ const invokeStageHooks = async (stage, hook, params, errorContext = {}) => {
1134
+ if (isConfigHookStage(stage)) {
1135
+ await invokeProjectHooks(stage, params, errorContext);
1136
+ }
1137
+ return invokeHook(stage, hook, params, errorContext);
1138
+ };
1139
+ const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
1140
+ const projectResults = await invokeProjectHooks(
1141
+ stage,
1142
+ params,
1143
+ errorContext
1144
+ );
1145
+ const projectBlock = projectResults.find(
1146
+ (result) => result?.allow === false
1147
+ );
1148
+ if (projectBlock?.allow === false) {
1149
+ return projectBlock;
1150
+ }
1151
+ return invokeHook(
1152
+ stage,
1153
+ hook,
1154
+ params,
1155
+ errorContext
1156
+ );
1157
+ };
1158
+ const applyProjectAfterCoreDecisionHooks = async ({
1159
+ hookCtx,
1160
+ market,
1161
+ decision
1162
+ }) => {
1163
+ let nextDecision = decision;
1164
+ for (const hook of getProjectHookList(
1165
+ "afterCoreDecision"
1166
+ )) {
1167
+ const result = await invokeHook(
1168
+ "afterCoreDecision",
1169
+ hook,
1170
+ {
1171
+ ctx: hookCtx,
1172
+ market,
1173
+ decision: nextDecision
1174
+ },
1175
+ {
1176
+ decision: nextDecision,
1177
+ market
1178
+ }
1179
+ );
1180
+ if (isStrategyDecision(result)) {
1181
+ nextDecision = result;
1182
+ }
1183
+ }
1184
+ return nextDecision;
1185
+ };
1186
+ const applyProjectAfterBarDecisionHooks = async ({
1187
+ hookCtx,
1188
+ market,
1189
+ decision
1190
+ }) => {
1191
+ let nextDecision = decision;
1192
+ for (const hook of getProjectHookList(
1193
+ "afterBarDecision"
1194
+ )) {
1195
+ const result = await invokeHook(
1196
+ "afterBarDecision",
1197
+ hook,
1198
+ {
1199
+ ctx: hookCtx,
1200
+ market,
1201
+ decision: nextDecision
1202
+ },
1203
+ {
1204
+ decision: nextDecision,
1205
+ market
1206
+ }
1207
+ );
1208
+ if (isStrategyDecision(result)) {
1209
+ nextDecision = result;
1210
+ }
1211
+ }
1212
+ return nextDecision;
1213
+ };
1214
+ const applyProjectOnBarHooks = async ({
1215
+ hookCtx,
1216
+ market
1217
+ }) => {
1218
+ for (const hook of getProjectHookList(
1219
+ "onBar"
1220
+ )) {
1221
+ const result = await invokeHook(
1222
+ "onBar",
1223
+ hook,
1224
+ {
1225
+ ctx: hookCtx,
1226
+ market
1227
+ },
1228
+ {
1229
+ market
1230
+ }
1231
+ );
1232
+ if (isStrategyDecision(result)) {
1233
+ return result;
1234
+ }
1235
+ }
1236
+ return void 0;
1237
+ };
887
1238
  const indicatorsState = createStrategyIndicatorsState({
888
1239
  env,
889
1240
  data,
@@ -917,7 +1268,7 @@ var createStrategyRuntime = ({
917
1268
  strategyApi,
918
1269
  indicatorsState
919
1270
  });
920
- await invokeHook("onInit", strategyManifest?.hooks?.onInit, {
1271
+ await invokeStageHooks("onInit", strategyManifest?.hooks?.onInit, {
921
1272
  ctx: getHookCtx(),
922
1273
  market: {
923
1274
  data,
@@ -932,13 +1283,64 @@ var createStrategyRuntime = ({
932
1283
  candle,
933
1284
  btcCandle
934
1285
  };
935
- const decision = await core(candle, btcCandle);
1286
+ const onBarHookCtx = getHookCtx();
1287
+ const projectOnBarDecision = await applyProjectOnBarHooks({
1288
+ hookCtx: onBarHookCtx,
1289
+ market
1290
+ });
1291
+ let decision;
1292
+ let shouldInvokeAfterCoreDecisionHook = false;
1293
+ if (projectOnBarDecision) {
1294
+ decision = projectOnBarDecision;
1295
+ } else {
1296
+ const manifestOnBarDecision = await invokeHook(
1297
+ "onBar",
1298
+ strategyManifest?.hooks?.onBar,
1299
+ {
1300
+ ctx: onBarHookCtx,
1301
+ market
1302
+ },
1303
+ { market }
1304
+ );
1305
+ if (isStrategyDecision(manifestOnBarDecision)) {
1306
+ decision = manifestOnBarDecision;
1307
+ } else {
1308
+ decision = await core(candle, btcCandle);
1309
+ shouldInvokeAfterCoreDecisionHook = true;
1310
+ }
1311
+ }
1312
+ if (shouldInvokeAfterCoreDecisionHook) {
1313
+ const initialDecisionStrategyName = decision.kind === "entry" ? decision.entryContext.strategy : strategyName;
1314
+ decision = await applyProjectAfterCoreDecisionHooks({
1315
+ hookCtx: getHookCtx(initialDecisionStrategyName),
1316
+ market,
1317
+ decision
1318
+ });
1319
+ }
1320
+ const initialAfterBarDecisionStrategyName = decision.kind === "entry" ? decision.entryContext.strategy : strategyName;
1321
+ decision = await applyProjectAfterBarDecisionHooks({
1322
+ hookCtx: getHookCtx(initialAfterBarDecisionStrategyName),
1323
+ market,
1324
+ decision
1325
+ });
936
1326
  const decisionStrategyName = decision.kind === "entry" ? decision.entryContext.strategy : strategyName;
937
1327
  const decisionManifest = resolveManifest(decisionStrategyName) ?? strategyManifest;
938
1328
  const decisionHookCtx = getHookCtx(decisionStrategyName);
1329
+ if (shouldInvokeAfterCoreDecisionHook) {
1330
+ await invokeHook(
1331
+ "afterCoreDecision",
1332
+ decisionManifest?.hooks?.afterCoreDecision,
1333
+ {
1334
+ ctx: decisionHookCtx,
1335
+ market,
1336
+ decision
1337
+ },
1338
+ { decision, market }
1339
+ );
1340
+ }
939
1341
  await invokeHook(
940
- "afterCoreDecision",
941
- decisionManifest?.hooks?.afterCoreDecision,
1342
+ "afterBarDecision",
1343
+ decisionManifest?.hooks?.afterBarDecision,
942
1344
  {
943
1345
  ctx: decisionHookCtx,
944
1346
  market,
@@ -947,7 +1349,7 @@ var createStrategyRuntime = ({
947
1349
  { decision, market }
948
1350
  );
949
1351
  if (decision.kind === "skip") {
950
- await invokeHook(
1352
+ await invokeStageHooks(
951
1353
  "onSkip",
952
1354
  decisionManifest?.hooks?.onSkip,
953
1355
  {
@@ -959,12 +1361,13 @@ var createStrategyRuntime = ({
959
1361
  );
960
1362
  return decision.code;
961
1363
  }
962
- const makeOrdersEnabled = typeof config.MAKE_ORDERS === "boolean" ? config.MAKE_ORDERS : true;
1364
+ const rawMakeOrdersEnabled = typeof config.MAKE_ORDERS === "boolean" ? config.MAKE_ORDERS : true;
1365
+ const makeOrdersEnabled = rawMakeOrdersEnabled && (env !== "PARITY" || isTestConnector(connector));
963
1366
  if (decision.kind === "exit") {
964
1367
  if (!makeOrdersEnabled) {
965
1368
  return decision.code;
966
1369
  }
967
- const closeGate = await invokeHook(
1370
+ const closeGate = await invokeGateHooks(
968
1371
  "beforeClosePosition",
969
1372
  decisionManifest?.hooks?.beforeClosePosition,
970
1373
  {
@@ -979,6 +1382,8 @@ var createStrategyRuntime = ({
979
1382
  }
980
1383
  return handleExitDecision({
981
1384
  connector,
1385
+ userName: recordRuntimeJournal ? userName : void 0,
1386
+ strategyName,
982
1387
  symbol,
983
1388
  decision,
984
1389
  market,
@@ -1054,7 +1459,7 @@ var createStrategyRuntime = ({
1054
1459
  env,
1055
1460
  ml: runtime.ml
1056
1461
  });
1057
- await invokeHook(
1462
+ await invokeStageHooks(
1058
1463
  "afterEnrichMl",
1059
1464
  decisionManifest?.hooks?.afterEnrichMl,
1060
1465
  {
@@ -1071,6 +1476,10 @@ var createStrategyRuntime = ({
1071
1476
  let ai;
1072
1477
  if (signal) {
1073
1478
  try {
1479
+ await enrichSignalWithDerivativesContext({
1480
+ signal,
1481
+ env
1482
+ });
1074
1483
  quality = await enrichSignalWithAi({
1075
1484
  signal,
1076
1485
  userName,
@@ -1094,7 +1503,7 @@ var createStrategyRuntime = ({
1094
1503
  ai: runtime.ai,
1095
1504
  quality
1096
1505
  });
1097
- await invokeHook(
1506
+ await invokeStageHooks(
1098
1507
  "afterEnrichAi",
1099
1508
  decisionManifest?.hooks?.afterEnrichAi,
1100
1509
  {
@@ -1119,6 +1528,7 @@ var createStrategyRuntime = ({
1119
1528
  makeOrdersEnabled,
1120
1529
  env,
1121
1530
  signal,
1531
+ ml,
1122
1532
  aiEnabled,
1123
1533
  quality,
1124
1534
  minAiQuality
@@ -1129,6 +1539,7 @@ var createStrategyRuntime = ({
1129
1539
  signal.orderSkipReason = getEntrySkipReason({
1130
1540
  makeOrdersEnabled,
1131
1541
  env,
1542
+ ml,
1132
1543
  aiEnabled,
1133
1544
  quality,
1134
1545
  minAiQuality
@@ -1136,7 +1547,7 @@ var createStrategyRuntime = ({
1136
1547
  }
1137
1548
  return signal ?? decision.code;
1138
1549
  }
1139
- const entryGate = await invokeHook(
1550
+ const entryGate = await invokeGateHooks(
1140
1551
  "beforeEntryGate",
1141
1552
  decisionManifest?.hooks?.beforeEntryGate,
1142
1553
  {
@@ -1170,18 +1581,108 @@ var createStrategyRuntime = ({
1170
1581
  policy,
1171
1582
  ml,
1172
1583
  ai,
1173
- invokeHook,
1584
+ recordRuntimeJournal,
1585
+ invokeStageHooks,
1174
1586
  notifyRuntimeError
1175
1587
  });
1176
1588
  };
1177
1589
  };
1178
1590
  };
1179
1591
 
1180
- // src/strategies.ts
1592
+ // src/strategyHooks/closeOppositePositionsBeforeOpen.ts
1593
+ var import_lodash2 = __toESM(require_lodash());
1594
+ import { logger as logger4 } from "@tradejs/infra/logger";
1595
+ var closeOppositePositionsBeforeOpen = async ({
1596
+ connector,
1597
+ entryContext
1598
+ }) => {
1599
+ const {
1600
+ symbol: currentSymbol,
1601
+ direction: currentDirection,
1602
+ timestamp,
1603
+ prices,
1604
+ strategy: strategyName
1605
+ } = entryContext;
1606
+ const price = prices.currentPrice;
1607
+ try {
1608
+ logger4.log(
1609
+ "info",
1610
+ "[%s] checking open positions before open: %s %s",
1611
+ strategyName,
1612
+ currentSymbol,
1613
+ currentDirection
1614
+ );
1615
+ const positions = await connector.getPositions();
1616
+ const openPositions = (positions || []).filter(
1617
+ (item) => item && Number(item.qty) > 0
1618
+ );
1619
+ logger4.log(
1620
+ "info",
1621
+ "[%s] open positions found: %s",
1622
+ strategyName,
1623
+ openPositions.length
1624
+ );
1625
+ const oppositePositions = openPositions.filter(
1626
+ (item) => item.symbol !== currentSymbol && item.direction !== currentDirection
1627
+ );
1628
+ if (import_lodash2.default.isEmpty(oppositePositions)) {
1629
+ logger4.log(
1630
+ "info",
1631
+ "[%s] no opposite positions to close before open: %s",
1632
+ strategyName,
1633
+ currentSymbol
1634
+ );
1635
+ return;
1636
+ }
1637
+ for (const position of oppositePositions) {
1638
+ logger4.log(
1639
+ "info",
1640
+ "[%s] closing opposite position: %s %s qty=%s",
1641
+ strategyName,
1642
+ position.symbol,
1643
+ position.direction,
1644
+ position.qty
1645
+ );
1646
+ try {
1647
+ await connector.closePosition({
1648
+ symbol: position.symbol,
1649
+ price,
1650
+ timestamp,
1651
+ direction: position.direction
1652
+ });
1653
+ logger4.log(
1654
+ "info",
1655
+ "[%s] opposite position closed: %s",
1656
+ strategyName,
1657
+ position.symbol
1658
+ );
1659
+ } catch (err) {
1660
+ logger4.log(
1661
+ "error",
1662
+ "[%s] failed to close opposite position: %s %s",
1663
+ strategyName,
1664
+ position.symbol,
1665
+ err
1666
+ );
1667
+ }
1668
+ }
1669
+ } catch (err) {
1670
+ logger4.log(
1671
+ "error",
1672
+ "[%s] failed to load open positions before open: %s %s",
1673
+ strategyName,
1674
+ currentSymbol,
1675
+ err
1676
+ );
1677
+ }
1678
+ };
1181
1679
  var createCloseOppositeBeforePlaceOrderHook = ({
1182
1680
  isEnabled
1183
1681
  }) => {
1184
1682
  return async ({ ctx, entry }) => {
1683
+ if (ctx.env === "BACKTEST") {
1684
+ return;
1685
+ }
1185
1686
  if (!isEnabled(ctx.strategyConfig)) {
1186
1687
  return;
1187
1688
  }
@@ -1191,6 +1692,227 @@ var createCloseOppositeBeforePlaceOrderHook = ({
1191
1692
  });
1192
1693
  };
1193
1694
  };
1695
+
1696
+ // src/strategyHooks/shared.ts
1697
+ var DEFAULT_BREAK_EVEN_TRIGGER_RISK_MULTIPLIER = 0.5;
1698
+ var DEFAULT_GLOBAL_UNREALIZED_PNL_TRIGGER_RISK_MULTIPLIER = 4;
1699
+ var GLOBAL_UNREALIZED_PNL_CLOSE_ALL_CODE = "GLOBAL_UNREALIZED_PNL_TARGET_REACHED_CLOSE_ALL";
1700
+ var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
1701
+ var isOpenPosition = (position) => Boolean(
1702
+ position && isFiniteNumber(position.price) && isFiniteNumber(position.qty) && position.qty > 0 && (position.direction === "LONG" || position.direction === "SHORT")
1703
+ );
1704
+ var isOpenPositionPnlSnapshot = (position) => Boolean(
1705
+ isOpenPosition(position) && isFiniteNumber(position?.currentPrice) && isFiniteNumber(position?.unrealizedPnl)
1706
+ );
1707
+ var getStrategyMaxLossValue = (strategyConfig) => {
1708
+ const maxLossValue = Number(strategyConfig?.MAX_LOSS_VALUE ?? Number.NaN);
1709
+ return Number.isFinite(maxLossValue) && maxLossValue > 0 ? maxLossValue : null;
1710
+ };
1711
+ var getPositionStopLossPrice = (position) => {
1712
+ if (!position || typeof position !== "object") {
1713
+ return null;
1714
+ }
1715
+ const slPrice = Number(
1716
+ position.slPrice ?? Number.NaN
1717
+ );
1718
+ if (Number.isFinite(slPrice)) {
1719
+ return slPrice;
1720
+ }
1721
+ const signalStopLossPrice = Number(
1722
+ position.signal?.prices?.stopLossPrice ?? Number.NaN
1723
+ );
1724
+ return Number.isFinite(signalStopLossPrice) ? signalStopLossPrice : null;
1725
+ };
1726
+ var getFavorableMovePct = ({
1727
+ direction,
1728
+ entryPrice,
1729
+ currentPrice
1730
+ }) => {
1731
+ if (!Number.isFinite(entryPrice) || !Number.isFinite(currentPrice) || entryPrice <= 0) {
1732
+ return null;
1733
+ }
1734
+ return direction === "LONG" ? (currentPrice - entryPrice) / entryPrice * 100 : (entryPrice - currentPrice) / entryPrice * 100;
1735
+ };
1736
+ var getPositionRiskPct = ({
1737
+ direction,
1738
+ entryPrice,
1739
+ stopLossPrice
1740
+ }) => {
1741
+ if (stopLossPrice == null || !Number.isFinite(entryPrice) || !Number.isFinite(stopLossPrice) || entryPrice <= 0) {
1742
+ return null;
1743
+ }
1744
+ return direction === "LONG" ? (entryPrice - stopLossPrice) / entryPrice * 100 : (stopLossPrice - entryPrice) / entryPrice * 100;
1745
+ };
1746
+ var isBreakEvenStopAlreadyApplied = ({
1747
+ direction,
1748
+ entryPrice,
1749
+ stopLossPrice
1750
+ }) => {
1751
+ if (stopLossPrice == null || !Number.isFinite(entryPrice) || !Number.isFinite(stopLossPrice)) {
1752
+ return false;
1753
+ }
1754
+ return direction === "LONG" ? stopLossPrice >= entryPrice : stopLossPrice <= entryPrice;
1755
+ };
1756
+ var getConfiguredDirectionRiskPct = ({
1757
+ strategyConfig,
1758
+ direction
1759
+ }) => {
1760
+ if (!strategyConfig || typeof strategyConfig !== "object") {
1761
+ return null;
1762
+ }
1763
+ const directSideConfig = strategyConfig[direction];
1764
+ const directSideRiskPct = Number(directSideConfig?.SL ?? Number.NaN);
1765
+ if (Number.isFinite(directSideRiskPct)) {
1766
+ return directSideRiskPct;
1767
+ }
1768
+ for (const candidate of Object.values(strategyConfig)) {
1769
+ if (!candidate || typeof candidate !== "object") {
1770
+ continue;
1771
+ }
1772
+ const candidateDirection = candidate.direction;
1773
+ const candidateRiskPct = Number(
1774
+ candidate.SL ?? Number.NaN
1775
+ );
1776
+ if (candidateDirection === direction && Number.isFinite(candidateRiskPct)) {
1777
+ return candidateRiskPct;
1778
+ }
1779
+ }
1780
+ return null;
1781
+ };
1782
+ var toStrategyCodePrefix = (strategyName) => strategyName === "TrendLine" ? "TRENDLINE" : strategyName.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase();
1783
+
1784
+ // src/strategyHooks/moveStopToBreakEvenAfterCoreDecision.ts
1785
+ var createMoveStopToBreakEvenOnBarHook = ({
1786
+ isEnabled = () => true,
1787
+ triggerRiskMultiplier = DEFAULT_BREAK_EVEN_TRIGGER_RISK_MULTIPLIER
1788
+ } = {}) => {
1789
+ return async ({ ctx, market }) => {
1790
+ if (!isEnabled(ctx.strategyConfig)) {
1791
+ return;
1792
+ }
1793
+ const currentPosition = await ctx.connector.getPosition(ctx.symbol);
1794
+ if (!isOpenPosition(currentPosition)) {
1795
+ return;
1796
+ }
1797
+ const currentPrice = Number(market.candle.close ?? Number.NaN);
1798
+ if (!Number.isFinite(currentPrice)) {
1799
+ return;
1800
+ }
1801
+ const currentStopLossPrice = getPositionStopLossPrice(currentPosition);
1802
+ if (isBreakEvenStopAlreadyApplied({
1803
+ direction: currentPosition.direction,
1804
+ entryPrice: currentPosition.price,
1805
+ stopLossPrice: currentStopLossPrice
1806
+ })) {
1807
+ return;
1808
+ }
1809
+ const favorableMovePct = getFavorableMovePct({
1810
+ direction: currentPosition.direction,
1811
+ entryPrice: currentPosition.price,
1812
+ currentPrice
1813
+ });
1814
+ const currentPositionRiskPct = getPositionRiskPct({
1815
+ direction: currentPosition.direction,
1816
+ entryPrice: currentPosition.price,
1817
+ stopLossPrice: currentStopLossPrice
1818
+ });
1819
+ const configuredRiskPct = getConfiguredDirectionRiskPct({
1820
+ strategyConfig: ctx.strategyConfig,
1821
+ direction: currentPosition.direction
1822
+ });
1823
+ const triggerRiskPct = currentPositionRiskPct ?? configuredRiskPct;
1824
+ if (favorableMovePct == null || triggerRiskPct == null || favorableMovePct < triggerRiskPct * triggerRiskMultiplier) {
1825
+ return;
1826
+ }
1827
+ return {
1828
+ kind: "protect",
1829
+ code: `${toStrategyCodePrefix(ctx.strategyName)}_MOVE_STOP_TO_BREAK_EVEN`,
1830
+ protectPlan: {
1831
+ direction: currentPosition.direction,
1832
+ stopLossPrice: currentPosition.price
1833
+ }
1834
+ };
1835
+ };
1836
+ };
1837
+ var createMoveStopToBreakEvenAfterCoreDecisionHook = createMoveStopToBreakEvenOnBarHook;
1838
+
1839
+ // src/signalsHooks/closeAllPositionsOnGlobalProfitBeforeSignals.ts
1840
+ import { logger as logger5 } from "@tradejs/infra/logger";
1841
+ var createCloseAllOnGlobalProfitBeforeSignalsHook = ({
1842
+ getStrategyDefaultConfig = () => void 0,
1843
+ profitRiskMultiplier = DEFAULT_GLOBAL_UNREALIZED_PNL_TRIGGER_RISK_MULTIPLIER
1844
+ } = {}) => {
1845
+ return async ({ connector, runtimeStrategies }) => {
1846
+ if (typeof connector.getOpenPositionPnl !== "function") {
1847
+ return;
1848
+ }
1849
+ const openPositions = (await connector.getOpenPositionPnl()).filter(
1850
+ isOpenPositionPnlSnapshot
1851
+ );
1852
+ if (!openPositions.length) {
1853
+ return;
1854
+ }
1855
+ const totalUnrealizedPnl = openPositions.reduce(
1856
+ (sum, position) => sum + position.unrealizedPnl,
1857
+ 0
1858
+ );
1859
+ if (!Number.isFinite(totalUnrealizedPnl) || totalUnrealizedPnl <= 0) {
1860
+ return;
1861
+ }
1862
+ const maxLossValues = runtimeStrategies.flatMap(
1863
+ ({ strategyName, strategyConfig }) => {
1864
+ const maxLossValue = getStrategyMaxLossValue({
1865
+ ...getStrategyDefaultConfig(strategyName) ?? {},
1866
+ ...strategyConfig ?? {}
1867
+ });
1868
+ return maxLossValue == null ? [] : [maxLossValue];
1869
+ }
1870
+ );
1871
+ if (!maxLossValues.length) {
1872
+ return;
1873
+ }
1874
+ const averageMaxLossValue = maxLossValues.reduce((sum, value) => sum + value, 0) / maxLossValues.length;
1875
+ const unrealizedPnlThreshold = averageMaxLossValue * profitRiskMultiplier;
1876
+ if (!Number.isFinite(unrealizedPnlThreshold) || unrealizedPnlThreshold <= 0 || totalUnrealizedPnl < unrealizedPnlThreshold) {
1877
+ return;
1878
+ }
1879
+ logger5.info(
1880
+ "closing all positions before signals by global unrealized pnl threshold: totalPnl=%s threshold=%s positions=%s",
1881
+ totalUnrealizedPnl,
1882
+ unrealizedPnlThreshold,
1883
+ openPositions.length
1884
+ );
1885
+ const closeTimestamp = Date.now();
1886
+ const closeResults = await Promise.allSettled(
1887
+ openPositions.map(
1888
+ (position) => connector.closePosition({
1889
+ symbol: position.symbol,
1890
+ direction: position.direction,
1891
+ price: position.currentPrice,
1892
+ timestamp: closeTimestamp
1893
+ })
1894
+ )
1895
+ );
1896
+ const failedClosures = closeResults.flatMap((result, index) => {
1897
+ if (result.status === "fulfilled" && result.value === true) {
1898
+ return [];
1899
+ }
1900
+ return [
1901
+ `${openPositions[index]?.symbol}:${openPositions[index]?.direction ?? "UNKNOWN"}`
1902
+ ];
1903
+ });
1904
+ if (failedClosures.length) {
1905
+ logger5.warn(
1906
+ "close-all before signals hook could not confirm closures for %s",
1907
+ failedClosures.join(", ")
1908
+ );
1909
+ }
1910
+ return {
1911
+ abort: true,
1912
+ reason: GLOBAL_UNREALIZED_PNL_CLOSE_ALL_CODE
1913
+ };
1914
+ };
1915
+ };
1194
1916
  export {
1195
1917
  DEFAULT_AI_MODEL,
1196
1918
  MAX_AI_SERIES_POINTS,
@@ -1200,7 +1922,10 @@ export {
1200
1922
  buildAiPrompts,
1201
1923
  buildAiSystemPrompt,
1202
1924
  closeOppositePositionsBeforeOpen,
1925
+ createCloseAllOnGlobalProfitBeforeSignalsHook,
1203
1926
  createCloseOppositeBeforePlaceOrderHook,
1927
+ createMoveStopToBreakEvenAfterCoreDecisionHook,
1928
+ createMoveStopToBreakEvenOnBarHook,
1204
1929
  createStrategyRuntime,
1205
1930
  enrichSignalWithAi,
1206
1931
  enrichSignalWithMl,