@tradejs/node 1.0.5 → 1.0.8

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,21 +1,30 @@
1
1
  import {
2
- buildMlPayload
3
- } from "./chunk-RBE4PZER.mjs";
2
+ buildMlPayload,
3
+ enrichSignalWithDerivativesContext
4
+ } from "./chunk-JRRG3YQG.mjs";
4
5
  import {
5
6
  require_lodash
6
- } from "./chunk-GKDBAF3A.mjs";
7
- import {
8
- createLoadPineScript
9
- } from "./chunk-7ICOZAKA.mjs";
10
- import "./chunk-5YNMSWL3.mjs";
7
+ } from "./chunk-KZDHZ56N.mjs";
11
8
  import {
9
+ DEFAULT_AI_MODEL,
12
10
  MAX_AI_SERIES_POINTS,
13
11
  askAI,
14
12
  buildAiHumanPrompt,
15
13
  buildAiPayload,
14
+ buildAiPrompts,
16
15
  buildAiSystemPrompt,
16
+ ensureAiStrategyPluginsLoaded,
17
+ getDeterministicAiGateContext,
18
+ getOpenRouterModelKwargs,
19
+ resetAiRuntimeCache,
20
+ runAiPrompt,
21
+ runAiPromptLocal,
17
22
  trimSeriesDeep
18
- } from "./chunk-LMAKIC3C.mjs";
23
+ } from "./chunk-2JKX3DM7.mjs";
24
+ import {
25
+ createPineScriptLoader
26
+ } from "./chunk-H6LIYHU4.mjs";
27
+ import "./chunk-5YNMSWL3.mjs";
19
28
  import {
20
29
  ensureIndicatorPluginsLoaded,
21
30
  ensureStrategyPluginsLoaded,
@@ -28,102 +37,15 @@ import {
28
37
  registerStrategyEntries,
29
38
  resetStrategyRegistryCache,
30
39
  strategies
31
- } from "./chunk-ZY6ULOWK.mjs";
40
+ } from "./chunk-WGOYR6AB.mjs";
32
41
  import {
33
- getTradejsProjectCwd
34
- } from "./chunk-P2ZUWONT.mjs";
42
+ getTradejsProjectCwd,
43
+ loadTradejsConfig
44
+ } from "./chunk-JU77QVJ3.mjs";
35
45
  import {
36
46
  __toESM
37
47
  } from "./chunk-6DZX6EAA.mjs";
38
48
 
39
- // src/closeOppositePositionsBeforeOpen.ts
40
- var import_lodash = __toESM(require_lodash());
41
- import { logger } from "@tradejs/infra/logger";
42
- var closeOppositePositionsBeforeOpen = async ({
43
- connector,
44
- entryContext
45
- }) => {
46
- const {
47
- symbol: currentSymbol,
48
- direction: currentDirection,
49
- timestamp,
50
- prices,
51
- strategy: strategyName
52
- } = entryContext;
53
- const price = prices.currentPrice;
54
- try {
55
- logger.log(
56
- "info",
57
- "[%s] checking open positions before open: %s %s",
58
- strategyName,
59
- currentSymbol,
60
- currentDirection
61
- );
62
- const positions = await connector.getPositions();
63
- const openPositions = (positions || []).filter(
64
- (item) => item && Number(item.qty) > 0
65
- );
66
- logger.log(
67
- "info",
68
- "[%s] open positions found: %s",
69
- strategyName,
70
- openPositions.length
71
- );
72
- const oppositePositions = openPositions.filter(
73
- (item) => item.symbol !== currentSymbol && item.direction !== currentDirection
74
- );
75
- if (import_lodash.default.isEmpty(oppositePositions)) {
76
- logger.log(
77
- "info",
78
- "[%s] no opposite positions to close before open: %s",
79
- strategyName,
80
- currentSymbol
81
- );
82
- return;
83
- }
84
- for (const position of oppositePositions) {
85
- logger.log(
86
- "info",
87
- "[%s] closing opposite position: %s %s qty=%s",
88
- strategyName,
89
- position.symbol,
90
- position.direction,
91
- position.qty
92
- );
93
- try {
94
- await connector.closePosition({
95
- symbol: position.symbol,
96
- price,
97
- timestamp,
98
- direction: position.direction
99
- });
100
- logger.log(
101
- "info",
102
- "[%s] opposite position closed: %s",
103
- strategyName,
104
- position.symbol
105
- );
106
- } catch (err) {
107
- logger.log(
108
- "error",
109
- "[%s] failed to close opposite position: %s %s",
110
- strategyName,
111
- position.symbol,
112
- err
113
- );
114
- }
115
- }
116
- } catch (err) {
117
- logger.log(
118
- "error",
119
- "[%s] failed to load open positions before open: %s %s",
120
- strategyName,
121
- currentSymbol,
122
- err
123
- );
124
- }
125
- };
126
-
127
49
  // src/strategies.ts
128
50
  export * from "@tradejs/core/strategies";
129
51
 
@@ -140,12 +62,125 @@ import { logger as logger3 } from "@tradejs/infra/logger";
140
62
 
141
63
  // src/strategyHelpers/runtime.ts
142
64
  import { logger as logger2 } from "@tradejs/infra/logger";
65
+ import { redisKeys as redisKeys2, setData as setData2 } from "@tradejs/infra/redis";
143
66
  import {
144
67
  buildMlFeatures,
145
68
  buildMlTrainingRow,
146
69
  fetchMlThreshold,
147
70
  trimMlTrainingRowWindows
148
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
149
184
  var formatAiError = (err) => {
150
185
  const error = err;
151
186
  const safeJson = (value) => {
@@ -164,6 +199,60 @@ var formatAiError = (err) => {
164
199
  };
165
200
  return safeJson(details);
166
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
+ };
167
256
  var enrichSignalWithMl = async ({
168
257
  signal,
169
258
  env,
@@ -198,21 +287,53 @@ var enrichSignalWithMl = async ({
198
287
  var enrichSignalWithAi = async ({
199
288
  signal,
200
289
  symbol,
290
+ userName,
201
291
  direction,
202
292
  env,
203
293
  ai
204
294
  }) => {
205
- if (env === "BACKTEST" || ai?.enabled === false) {
295
+ if (ai?.enabled === false) {
206
296
  return void 0;
207
297
  }
208
- try {
209
- const { askAI: askAI2 } = await import("./ai-G7ATN4YL.mjs");
210
- const analysis = await askAI2(signal);
211
- if (typeof analysis?.quality === "number") {
212
- const normalizedQuality = Math.round(analysis.quality);
213
- const aiApprovedCurrentTrade = analysis?.direction === direction;
214
- return aiApprovedCurrentTrade ? normalizedQuality : 0;
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;
215
329
  }
330
+ return gateQuality;
331
+ }
332
+ try {
333
+ const { askAI: askAI2 } = await import("./ai.mjs");
334
+ const analysis = await askAI2(signal, { userName });
335
+ signal.aiAnalysis = analysis;
336
+ return resolveAiQuality(analysis, direction);
216
337
  } catch (err) {
217
338
  logger2.error("AI analysis error: %s %s", symbol, formatAiError(err));
218
339
  }
@@ -220,17 +341,50 @@ var enrichSignalWithAi = async ({
220
341
  };
221
342
  var enrichSignalWithMlAi = async ({
222
343
  signal,
344
+ userName,
223
345
  symbol,
224
346
  direction,
225
347
  env,
226
348
  ml,
227
349
  ai
228
350
  }) => {
351
+ await enrichSignalWithDerivativesContext({ signal, env });
229
352
  await enrichSignalWithMl({ signal, env, ml });
230
- return enrichSignalWithAi({ signal, symbol, direction, env, ai });
353
+ return enrichSignalWithAi({ signal, userName, symbol, direction, env, ai });
354
+ };
355
+ var applyProtectiveOrders = async ({
356
+ connector,
357
+ symbol,
358
+ direction,
359
+ qty,
360
+ takeProfits,
361
+ stopLossPrice
362
+ }) => {
363
+ if (Array.isArray(takeProfits) && takeProfits.length > 0) {
364
+ const tpOk = await connector.setTakeProfits({
365
+ symbol,
366
+ direction,
367
+ qty,
368
+ takeProfits
369
+ });
370
+ if (!tpOk) {
371
+ throw new Error("SET_TAKE_PROFITS_FAILED");
372
+ }
373
+ }
374
+ if (typeof stopLossPrice === "number" && Number.isFinite(stopLossPrice)) {
375
+ const slOk = await connector.setStopLoss({
376
+ symbol,
377
+ direction,
378
+ stopLossPrice
379
+ });
380
+ if (!slOk) {
381
+ throw new Error("SET_STOP_LOSS_FAILED");
382
+ }
383
+ }
231
384
  };
232
385
  var executeEntryOrder = async ({
233
386
  connector,
387
+ userName,
234
388
  symbol,
235
389
  direction,
236
390
  qty,
@@ -239,35 +393,87 @@ var executeEntryOrder = async ({
239
393
  takeProfits,
240
394
  stopLossPrice,
241
395
  signal,
242
- beforePlaceOrder
396
+ beforePlaceOrder,
397
+ recordRuntimeTrade = true
243
398
  }) => {
244
399
  await beforePlaceOrder?.();
245
- const orderPlaced = await connector.placeOrder(
246
- {
247
- symbol,
248
- qty,
249
- price: currentPrice,
250
- isLimit: false,
251
- timestamp,
252
- direction,
253
- signal
254
- },
255
- takeProfits,
256
- stopLossPrice
257
- );
400
+ const orderId = signal.orderId || createRuntimeOrderId();
401
+ signal.orderId = orderId;
402
+ const orderPlaced = await connector.placeOrder({
403
+ symbol,
404
+ qty,
405
+ price: currentPrice,
406
+ isLimit: false,
407
+ timestamp,
408
+ direction,
409
+ orderId,
410
+ signal
411
+ });
412
+ if (orderPlaced) {
413
+ try {
414
+ await applyProtectiveOrders({
415
+ connector,
416
+ symbol,
417
+ direction,
418
+ qty,
419
+ takeProfits,
420
+ stopLossPrice
421
+ });
422
+ } catch (error) {
423
+ await connector.closePosition({
424
+ symbol,
425
+ price: currentPrice,
426
+ timestamp,
427
+ direction
428
+ });
429
+ throw error;
430
+ }
431
+ }
258
432
  signal.orderStatus = orderPlaced ? "completed" : "failed";
259
433
  signal.orderSkipReason = void 0;
260
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
+ }
261
451
  if (currentPosition?.price) {
262
- signal.prices.currentPrice = currentPosition.price;
263
452
  return currentPosition.price;
264
453
  }
265
- return currentPrice;
454
+ return entryPrice;
455
+ };
456
+ var updatePositionProtection = async ({
457
+ connector,
458
+ symbol,
459
+ direction,
460
+ qty,
461
+ takeProfits,
462
+ stopLossPrice
463
+ }) => {
464
+ await applyProtectiveOrders({
465
+ connector,
466
+ symbol,
467
+ direction,
468
+ qty,
469
+ takeProfits: takeProfits ?? [],
470
+ stopLossPrice: stopLossPrice ?? null
471
+ });
266
472
  };
267
473
 
268
474
  // src/strategyHelpers/config.ts
269
- var import_lodash2 = __toESM(require_lodash());
270
- 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";
271
477
  var resolveStrategyConfig = async ({
272
478
  strategyName,
273
479
  userName,
@@ -275,7 +481,7 @@ var resolveStrategyConfig = async ({
275
481
  baseConfig,
276
482
  defaults
277
483
  }) => {
278
- const mergeIfNotEmpty = (target, patch) => patch && !import_lodash2.default.isEmpty(patch) ? {
484
+ const mergeIfNotEmpty = (target, patch) => patch && !import_lodash.default.isEmpty(patch) ? {
279
485
  ...target,
280
486
  ...patch
281
487
  } : target;
@@ -285,17 +491,17 @@ var resolveStrategyConfig = async ({
285
491
  };
286
492
  let isConfigFromBacktest = false;
287
493
  if (config.ENV !== "BACKTEST") {
288
- const userConfig = await getData(
289
- redisKeys.strategyConfig(userName, strategyName),
494
+ const userConfig = await getData2(
495
+ redisKeys3.strategyConfig(userName, strategyName),
290
496
  {}
291
497
  );
292
498
  config = mergeIfNotEmpty(config, userConfig);
293
- const results = await getData(
294
- redisKeys.strategyResults(userName, strategyName),
499
+ const results = await getData2(
500
+ redisKeys3.strategyResults(userName, strategyName),
295
501
  {}
296
502
  );
297
503
  const backtestResult = results?.[symbol];
298
- if (backtestResult && !import_lodash2.default.isEmpty(backtestResult.config)) {
504
+ if (backtestResult && !import_lodash.default.isEmpty(backtestResult.config)) {
299
505
  config = mergeIfNotEmpty(
300
506
  config,
301
507
  backtestResult.config
@@ -332,27 +538,103 @@ var resolveEntryRuntimePolicy = ({
332
538
  ai
333
539
  };
334
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
+ };
335
556
  var shouldExecuteEntryDecision = ({
336
557
  makeOrdersEnabled,
337
558
  env,
338
559
  signal,
560
+ ml,
561
+ aiEnabled,
339
562
  quality,
340
563
  minAiQuality
341
- }) => makeOrdersEnabled && (!signal || env === "BACKTEST" || quality == null || quality >= minAiQuality);
564
+ }) => {
565
+ if (!makeOrdersEnabled) {
566
+ return false;
567
+ }
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) {
578
+ return true;
579
+ }
580
+ return Number.isFinite(quality) && quality >= minAiQuality;
581
+ };
342
582
  var getEntrySkipReason = ({
343
583
  makeOrdersEnabled,
344
584
  env,
585
+ ml,
586
+ aiEnabled,
345
587
  quality,
346
588
  minAiQuality
347
589
  }) => {
348
590
  if (!makeOrdersEnabled) {
349
591
  return "MAKE_ORDERS_DISABLED";
350
592
  }
351
- if (env !== "BACKTEST" && quality != null && Number.isFinite(quality) && quality < minAiQuality) {
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
+ }
601
+ if (env !== "BACKTEST" && aiEnabled && quality == null) {
602
+ return "AI_QUALITY_UNAVAILABLE";
603
+ }
604
+ if (env !== "BACKTEST" && aiEnabled && quality != null && Number.isFinite(quality) && quality < minAiQuality) {
352
605
  return `AI_QUALITY_BELOW_MIN (${quality} < ${minAiQuality})`;
353
606
  }
354
607
  return "ENTRY_POLICY_BLOCKED";
355
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
+ );
356
638
  var buildHookCtx = ({
357
639
  connector,
358
640
  strategyName,
@@ -391,6 +673,13 @@ var buildHookPolicy = ({
391
673
  makeOrdersEnabled,
392
674
  minAiQuality
393
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
+ );
394
683
  var buildMlHookContext = ({
395
684
  signal,
396
685
  env,
@@ -495,6 +784,8 @@ var buildAiHookContext = ({
495
784
  };
496
785
  var handleExitDecision = async ({
497
786
  connector,
787
+ userName,
788
+ strategyName,
498
789
  symbol,
499
790
  decision,
500
791
  market,
@@ -507,6 +798,13 @@ var handleExitDecision = async ({
507
798
  timestamp: decision.closePlan.timestamp,
508
799
  direction: decision.closePlan.direction
509
800
  });
801
+ await markRuntimeTradeClosed({
802
+ userName,
803
+ strategy: strategyName,
804
+ symbol,
805
+ exitPrice: decision.closePlan.price,
806
+ exitTimestamp: decision.closePlan.timestamp
807
+ });
510
808
  } catch (err) {
511
809
  await onRuntimeError?.({
512
810
  stage: "closePosition",
@@ -519,6 +817,33 @@ var handleExitDecision = async ({
519
817
  }
520
818
  return decision.code;
521
819
  };
820
+ var handleProtectDecision = async ({
821
+ connector,
822
+ symbol,
823
+ decision,
824
+ market,
825
+ onRuntimeError
826
+ }) => {
827
+ try {
828
+ await updatePositionProtection({
829
+ connector,
830
+ symbol,
831
+ direction: decision.protectPlan.direction,
832
+ takeProfits: decision.protectPlan.takeProfits ?? [],
833
+ stopLossPrice: decision.protectPlan.stopLossPrice ?? null
834
+ });
835
+ } catch (err) {
836
+ await onRuntimeError?.({
837
+ stage: "protectPosition",
838
+ error: err,
839
+ decision,
840
+ market
841
+ });
842
+ logger3.error("protect position error: %s %s", symbol, err);
843
+ return "ORDER_ERROR";
844
+ }
845
+ return decision.code;
846
+ };
522
847
  var executeEntryDecision = async ({
523
848
  connector,
524
849
  symbol,
@@ -531,12 +856,13 @@ var executeEntryDecision = async ({
531
856
  policy,
532
857
  ml,
533
858
  ai,
534
- invokeHook,
859
+ recordRuntimeJournal,
860
+ invokeStageHooks,
535
861
  notifyRuntimeError
536
862
  }) => {
537
863
  const signal = decision.signal;
538
864
  const beforePlaceOrder = async () => {
539
- await invokeHook(
865
+ await invokeStageHooks(
540
866
  "beforePlaceOrder",
541
867
  manifest?.hooks?.beforePlaceOrder,
542
868
  {
@@ -567,6 +893,7 @@ var executeEntryDecision = async ({
567
893
  if (signal) {
568
894
  await executeEntryOrder({
569
895
  connector,
896
+ userName: hookCtx.userName,
570
897
  symbol,
571
898
  direction: decision.entryContext.direction,
572
899
  qty: decision.orderPlan.qty,
@@ -575,9 +902,10 @@ var executeEntryDecision = async ({
575
902
  takeProfits: decision.orderPlan.takeProfits,
576
903
  stopLossPrice: decision.orderPlan.stopLossPrice,
577
904
  signal,
578
- beforePlaceOrder
905
+ beforePlaceOrder,
906
+ recordRuntimeTrade: recordRuntimeJournal
579
907
  });
580
- await invokeHook(
908
+ await invokeStageHooks(
581
909
  "afterPlaceOrder",
582
910
  manifest?.hooks?.afterPlaceOrder,
583
911
  {
@@ -597,18 +925,35 @@ var executeEntryDecision = async ({
597
925
  return signal;
598
926
  }
599
927
  await beforePlaceOrder();
600
- await connector.placeOrder(
601
- {
928
+ const orderPlaced = await connector.placeOrder({
929
+ symbol,
930
+ qty: decision.orderPlan.qty,
931
+ price: decision.entryContext.prices.currentPrice,
932
+ timestamp: decision.entryContext.timestamp,
933
+ direction: decision.entryContext.direction
934
+ });
935
+ if (!orderPlaced) {
936
+ throw new Error("PLACE_ORDER_FAILED");
937
+ }
938
+ try {
939
+ await updatePositionProtection({
940
+ connector,
602
941
  symbol,
942
+ direction: decision.entryContext.direction,
603
943
  qty: decision.orderPlan.qty,
944
+ takeProfits: decision.orderPlan.takeProfits,
945
+ stopLossPrice: decision.orderPlan.stopLossPrice
946
+ });
947
+ } catch (error) {
948
+ await connector.closePosition({
949
+ symbol,
604
950
  price: decision.entryContext.prices.currentPrice,
605
951
  timestamp: decision.entryContext.timestamp,
606
952
  direction: decision.entryContext.direction
607
- },
608
- decision.orderPlan.takeProfits,
609
- decision.orderPlan.stopLossPrice
610
- );
611
- await invokeHook(
953
+ });
954
+ throw error;
955
+ }
956
+ await invokeStageHooks(
612
957
  "afterPlaceOrder",
613
958
  manifest?.hooks?.afterPlaceOrder,
614
959
  {
@@ -658,7 +1003,7 @@ var createStrategyRuntime = ({
658
1003
  }
659
1004
  return getStrategyManifest(name, projectRoot);
660
1005
  };
661
- const loadPineScript = createLoadPineScript(
1006
+ const loadPineScriptFile = createPineScriptLoader(
662
1007
  strategyDirectory ? path.resolve(strategyDirectory) : path.resolve(
663
1008
  projectRoot,
664
1009
  "packages",
@@ -684,7 +1029,10 @@ var createStrategyRuntime = ({
684
1029
  baseConfig,
685
1030
  defaults
686
1031
  });
1032
+ const projectConfig = await loadTradejsConfig(projectRoot);
1033
+ const projectHooks = projectConfig.hooks;
687
1034
  const env = String(config.ENV ?? "BACKTEST");
1035
+ const recordRuntimeJournal = shouldRecordRuntimeJournal({ env, config });
688
1036
  const strategyManifest = resolveManifest(strategyName);
689
1037
  const hookBase = {
690
1038
  connector,
@@ -699,6 +1047,7 @@ var createStrategyRuntime = ({
699
1047
  ...hookBase,
700
1048
  strategyName: name
701
1049
  });
1050
+ const getProjectHookList = (stage) => normalizeConfigHookList(projectHooks?.[stage]);
702
1051
  const notifyRuntimeError = async ({
703
1052
  stage,
704
1053
  error,
@@ -708,21 +1057,33 @@ var createStrategyRuntime = ({
708
1057
  }) => {
709
1058
  const errorStrategyName = decision?.kind === "entry" ? decision.entryContext.strategy : strategyName;
710
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
+ }
711
1081
  const onRuntimeError = errorManifest?.hooks?.onRuntimeError;
712
1082
  if (!onRuntimeError) {
713
1083
  return;
714
1084
  }
715
1085
  try {
716
- await onRuntimeError({
717
- ctx: getHookCtx(errorStrategyName),
718
- market,
719
- decision,
720
- entry,
721
- error: {
722
- stage,
723
- cause: error
724
- }
725
- });
1086
+ await onRuntimeError(errorParams);
726
1087
  } catch (hookError) {
727
1088
  logger3.error(
728
1089
  "runtime hook onRuntimeError failed: %s %s",
@@ -754,6 +1115,126 @@ var createStrategyRuntime = ({
754
1115
  return void 0;
755
1116
  }
756
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
+ };
757
1238
  const indicatorsState = createStrategyIndicatorsState({
758
1239
  env,
759
1240
  data,
@@ -783,11 +1264,11 @@ var createStrategyRuntime = ({
783
1264
  connector,
784
1265
  data,
785
1266
  btcData,
786
- loadPineScript,
1267
+ loadPineScriptFile,
787
1268
  strategyApi,
788
1269
  indicatorsState
789
1270
  });
790
- await invokeHook("onInit", strategyManifest?.hooks?.onInit, {
1271
+ await invokeStageHooks("onInit", strategyManifest?.hooks?.onInit, {
791
1272
  ctx: getHookCtx(),
792
1273
  market: {
793
1274
  data,
@@ -802,13 +1283,64 @@ var createStrategyRuntime = ({
802
1283
  candle,
803
1284
  btcCandle
804
1285
  };
805
- 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
+ });
806
1326
  const decisionStrategyName = decision.kind === "entry" ? decision.entryContext.strategy : strategyName;
807
1327
  const decisionManifest = resolveManifest(decisionStrategyName) ?? strategyManifest;
808
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
+ }
809
1341
  await invokeHook(
810
- "afterCoreDecision",
811
- decisionManifest?.hooks?.afterCoreDecision,
1342
+ "afterBarDecision",
1343
+ decisionManifest?.hooks?.afterBarDecision,
812
1344
  {
813
1345
  ctx: decisionHookCtx,
814
1346
  market,
@@ -817,7 +1349,7 @@ var createStrategyRuntime = ({
817
1349
  { decision, market }
818
1350
  );
819
1351
  if (decision.kind === "skip") {
820
- await invokeHook(
1352
+ await invokeStageHooks(
821
1353
  "onSkip",
822
1354
  decisionManifest?.hooks?.onSkip,
823
1355
  {
@@ -829,12 +1361,13 @@ var createStrategyRuntime = ({
829
1361
  );
830
1362
  return decision.code;
831
1363
  }
832
- 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));
833
1366
  if (decision.kind === "exit") {
834
1367
  if (!makeOrdersEnabled) {
835
1368
  return decision.code;
836
1369
  }
837
- const closeGate = await invokeHook(
1370
+ const closeGate = await invokeGateHooks(
838
1371
  "beforeClosePosition",
839
1372
  decisionManifest?.hooks?.beforeClosePosition,
840
1373
  {
@@ -849,6 +1382,8 @@ var createStrategyRuntime = ({
849
1382
  }
850
1383
  return handleExitDecision({
851
1384
  connector,
1385
+ userName: recordRuntimeJournal ? userName : void 0,
1386
+ strategyName,
852
1387
  symbol,
853
1388
  decision,
854
1389
  market,
@@ -867,6 +1402,30 @@ var createStrategyRuntime = ({
867
1402
  }
868
1403
  });
869
1404
  }
1405
+ if (decision.kind === "protect") {
1406
+ if (!makeOrdersEnabled) {
1407
+ return decision.code;
1408
+ }
1409
+ return handleProtectDecision({
1410
+ connector,
1411
+ symbol,
1412
+ decision,
1413
+ market,
1414
+ onRuntimeError: async ({
1415
+ stage,
1416
+ error,
1417
+ decision: protectDecision,
1418
+ market: errorMarket
1419
+ }) => {
1420
+ await notifyRuntimeError({
1421
+ stage,
1422
+ error,
1423
+ decision: protectDecision,
1424
+ market: errorMarket
1425
+ });
1426
+ }
1427
+ });
1428
+ }
870
1429
  const runtime = resolveEntryRuntimePolicy({
871
1430
  decision,
872
1431
  config,
@@ -900,7 +1459,7 @@ var createStrategyRuntime = ({
900
1459
  env,
901
1460
  ml: runtime.ml
902
1461
  });
903
- await invokeHook(
1462
+ await invokeStageHooks(
904
1463
  "afterEnrichMl",
905
1464
  decisionManifest?.hooks?.afterEnrichMl,
906
1465
  {
@@ -917,8 +1476,13 @@ var createStrategyRuntime = ({
917
1476
  let ai;
918
1477
  if (signal) {
919
1478
  try {
1479
+ await enrichSignalWithDerivativesContext({
1480
+ signal,
1481
+ env
1482
+ });
920
1483
  quality = await enrichSignalWithAi({
921
1484
  signal,
1485
+ userName,
922
1486
  symbol,
923
1487
  direction: signal.direction,
924
1488
  env,
@@ -939,7 +1503,7 @@ var createStrategyRuntime = ({
939
1503
  ai: runtime.ai,
940
1504
  quality
941
1505
  });
942
- await invokeHook(
1506
+ await invokeStageHooks(
943
1507
  "afterEnrichAi",
944
1508
  decisionManifest?.hooks?.afterEnrichAi,
945
1509
  {
@@ -954,6 +1518,7 @@ var createStrategyRuntime = ({
954
1518
  );
955
1519
  }
956
1520
  const minAiQuality = runtime.ai?.minQuality ?? 4;
1521
+ const aiEnabled = runtime.ai?.enabled !== false && runtime.ai != null;
957
1522
  const policy = buildHookPolicy({
958
1523
  quality,
959
1524
  makeOrdersEnabled,
@@ -963,6 +1528,8 @@ var createStrategyRuntime = ({
963
1528
  makeOrdersEnabled,
964
1529
  env,
965
1530
  signal,
1531
+ ml,
1532
+ aiEnabled,
966
1533
  quality,
967
1534
  minAiQuality
968
1535
  });
@@ -972,13 +1539,15 @@ var createStrategyRuntime = ({
972
1539
  signal.orderSkipReason = getEntrySkipReason({
973
1540
  makeOrdersEnabled,
974
1541
  env,
1542
+ ml,
1543
+ aiEnabled,
975
1544
  quality,
976
1545
  minAiQuality
977
1546
  });
978
1547
  }
979
1548
  return signal ?? decision.code;
980
1549
  }
981
- const entryGate = await invokeHook(
1550
+ const entryGate = await invokeGateHooks(
982
1551
  "beforeEntryGate",
983
1552
  decisionManifest?.hooks?.beforeEntryGate,
984
1553
  {
@@ -1012,18 +1581,108 @@ var createStrategyRuntime = ({
1012
1581
  policy,
1013
1582
  ml,
1014
1583
  ai,
1015
- invokeHook,
1584
+ recordRuntimeJournal,
1585
+ invokeStageHooks,
1016
1586
  notifyRuntimeError
1017
1587
  });
1018
1588
  };
1019
1589
  };
1020
1590
  };
1021
1591
 
1022
- // 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
+ };
1023
1679
  var createCloseOppositeBeforePlaceOrderHook = ({
1024
1680
  isEnabled
1025
1681
  }) => {
1026
1682
  return async ({ ctx, entry }) => {
1683
+ if (ctx.env === "BACKTEST") {
1684
+ return;
1685
+ }
1027
1686
  if (!isEnabled(ctx.strategyConfig)) {
1028
1687
  return;
1029
1688
  }
@@ -1033,30 +1692,262 @@ var createCloseOppositeBeforePlaceOrderHook = ({
1033
1692
  });
1034
1693
  };
1035
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
+ };
1036
1916
  export {
1917
+ DEFAULT_AI_MODEL,
1037
1918
  MAX_AI_SERIES_POINTS,
1038
1919
  askAI,
1039
1920
  buildAiHumanPrompt,
1040
1921
  buildAiPayload,
1922
+ buildAiPrompts,
1041
1923
  buildAiSystemPrompt,
1042
1924
  closeOppositePositionsBeforeOpen,
1925
+ createCloseAllOnGlobalProfitBeforeSignalsHook,
1043
1926
  createCloseOppositeBeforePlaceOrderHook,
1927
+ createMoveStopToBreakEvenAfterCoreDecisionHook,
1928
+ createMoveStopToBreakEvenOnBarHook,
1044
1929
  createStrategyRuntime,
1045
1930
  enrichSignalWithAi,
1046
1931
  enrichSignalWithMl,
1047
1932
  enrichSignalWithMlAi,
1933
+ ensureAiStrategyPluginsLoaded,
1048
1934
  ensureIndicatorPluginsLoaded,
1049
1935
  ensureStrategyPluginsLoaded,
1050
1936
  executeEntryOrder,
1051
1937
  getAvailableStrategyNames,
1938
+ getDeterministicAiGateContext,
1939
+ getOpenRouterModelKwargs,
1052
1940
  getRegisteredManifests,
1053
1941
  getRegisteredStrategies,
1054
1942
  getStrategyCreator,
1055
1943
  getStrategyManifest,
1056
1944
  isKnownStrategy,
1057
1945
  registerStrategyEntries,
1946
+ resetAiRuntimeCache,
1058
1947
  resetStrategyRegistryCache,
1059
1948
  resolveStrategyConfig,
1949
+ runAiPrompt,
1950
+ runAiPromptLocal,
1060
1951
  strategies,
1061
1952
  trimSeriesDeep
1062
1953
  };