@tradejs/node 1.0.0

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.
@@ -0,0 +1,847 @@
1
+ import {
2
+ buildMlPayload
3
+ } from "./chunk-PXJJPAQT.mjs";
4
+ import {
5
+ require_lodash
6
+ } from "./chunk-GKDBAF3A.mjs";
7
+ import {
8
+ createLoadPineScript
9
+ } from "./chunk-CVTV6S2V.mjs";
10
+ import "./chunk-5YNMSWL3.mjs";
11
+ import {
12
+ MAX_AI_SERIES_POINTS,
13
+ askAI,
14
+ buildAiHumanPrompt,
15
+ buildAiPayload,
16
+ buildAiSystemPrompt,
17
+ trimSeriesDeep
18
+ } from "./chunk-ZIMX3JX2.mjs";
19
+ import {
20
+ ensureIndicatorPluginsLoaded,
21
+ ensureStrategyPluginsLoaded,
22
+ getAvailableStrategyNames,
23
+ getRegisteredManifests,
24
+ getRegisteredStrategies,
25
+ getStrategyCreator,
26
+ getStrategyManifest,
27
+ isKnownStrategy,
28
+ registerStrategyEntries,
29
+ resetStrategyRegistryCache,
30
+ strategies
31
+ } from "./chunk-MHCXPD2B.mjs";
32
+ import "./chunk-DE7ADBIR.mjs";
33
+ import {
34
+ __toESM
35
+ } from "./chunk-6DZX6EAA.mjs";
36
+
37
+ // src/closeOppositePositionsBeforeOpen.ts
38
+ var import_lodash = __toESM(require_lodash());
39
+ import { logger } from "@tradejs/infra/logger";
40
+ var closeOppositePositionsBeforeOpen = async ({
41
+ connector,
42
+ entryContext
43
+ }) => {
44
+ const {
45
+ symbol: currentSymbol,
46
+ direction: currentDirection,
47
+ timestamp,
48
+ prices,
49
+ strategy: strategyName
50
+ } = entryContext;
51
+ const price = prices.currentPrice;
52
+ try {
53
+ logger.log(
54
+ "info",
55
+ "[%s] checking open positions before open: %s %s",
56
+ strategyName,
57
+ currentSymbol,
58
+ currentDirection
59
+ );
60
+ const positions = await connector.getPositions();
61
+ const openPositions = (positions || []).filter(
62
+ (item) => item && Number(item.qty) > 0
63
+ );
64
+ logger.log(
65
+ "info",
66
+ "[%s] open positions found: %s",
67
+ strategyName,
68
+ openPositions.length
69
+ );
70
+ const oppositePositions = openPositions.filter(
71
+ (item) => item.symbol !== currentSymbol && item.direction !== currentDirection
72
+ );
73
+ if (import_lodash.default.isEmpty(oppositePositions)) {
74
+ logger.log(
75
+ "info",
76
+ "[%s] no opposite positions to close before open: %s",
77
+ strategyName,
78
+ currentSymbol
79
+ );
80
+ return;
81
+ }
82
+ for (const position of oppositePositions) {
83
+ logger.log(
84
+ "info",
85
+ "[%s] closing opposite position: %s %s qty=%s",
86
+ strategyName,
87
+ position.symbol,
88
+ position.direction,
89
+ position.qty
90
+ );
91
+ try {
92
+ await connector.closePosition({
93
+ symbol: position.symbol,
94
+ price,
95
+ timestamp,
96
+ direction: position.direction
97
+ });
98
+ logger.log(
99
+ "info",
100
+ "[%s] opposite position closed: %s",
101
+ strategyName,
102
+ position.symbol
103
+ );
104
+ } catch (err) {
105
+ logger.log(
106
+ "error",
107
+ "[%s] failed to close opposite position: %s %s",
108
+ strategyName,
109
+ position.symbol,
110
+ err
111
+ );
112
+ }
113
+ }
114
+ } catch (err) {
115
+ logger.log(
116
+ "error",
117
+ "[%s] failed to load open positions before open: %s %s",
118
+ strategyName,
119
+ currentSymbol,
120
+ err
121
+ );
122
+ }
123
+ };
124
+
125
+ // src/strategies.ts
126
+ export * from "@tradejs/core/strategies";
127
+
128
+ // src/strategyRuntime.ts
129
+ import path from "path";
130
+ import { SIGNALS_PRELOAD_DAYS } from "@tradejs/core/constants";
131
+ import {
132
+ buildDefaultIndicatorPeriods,
133
+ createStrategyAPI,
134
+ createStrategyIndicatorsState
135
+ } from "@tradejs/core/strategies";
136
+ import { getTimestamp } from "@tradejs/core/time";
137
+ import { logger as logger3 } from "@tradejs/infra/logger";
138
+
139
+ // src/strategyHelpers/runtime.ts
140
+ import { logger as logger2 } from "@tradejs/infra/logger";
141
+ import {
142
+ buildMlFeatures,
143
+ buildMlTrainingRow,
144
+ fetchMlThreshold,
145
+ trimMlTrainingRowWindows
146
+ } from "@tradejs/infra/ml";
147
+ var formatAiError = (err) => {
148
+ const error = err;
149
+ const safeJson = (value) => {
150
+ try {
151
+ return JSON.stringify(value);
152
+ } catch {
153
+ return String(value);
154
+ }
155
+ };
156
+ const details = {
157
+ message: String(error?.message ?? "unknown"),
158
+ status: error?.status ?? null,
159
+ code: error?.code ?? null,
160
+ type: error?.type ?? null,
161
+ providerError: error?.error ?? null
162
+ };
163
+ return safeJson(details);
164
+ };
165
+ var enrichSignalWithMl = async ({
166
+ signal,
167
+ env,
168
+ ml
169
+ }) => {
170
+ if (env !== "BACKTEST" && ml && ml.enabled !== false && ml.strategyConfig && typeof ml.mlThreshold === "number") {
171
+ const strategy = signal.strategy;
172
+ const fullRow = buildMlTrainingRow(
173
+ buildMlPayload({
174
+ signal,
175
+ context: {
176
+ strategyConfig: ml.strategyConfig,
177
+ strategyName: strategy,
178
+ symbol: signal.symbol
179
+ }
180
+ }),
181
+ null
182
+ );
183
+ const row = trimMlTrainingRowWindows(fullRow, 5);
184
+ const features = buildMlFeatures(row);
185
+ const mlResult = await fetchMlThreshold({
186
+ strategy,
187
+ features,
188
+ threshold: ml.mlThreshold
189
+ });
190
+ if (mlResult) {
191
+ signal.ml = mlResult;
192
+ }
193
+ }
194
+ };
195
+ var enrichSignalWithAi = async ({
196
+ signal,
197
+ symbol,
198
+ direction,
199
+ env,
200
+ ai
201
+ }) => {
202
+ if (env === "BACKTEST" || ai?.enabled === false) {
203
+ return void 0;
204
+ }
205
+ try {
206
+ const { askAI: askAI2 } = await import("./ai-NNJ3RLLL.mjs");
207
+ const analysis = await askAI2(signal);
208
+ if (typeof analysis?.quality === "number") {
209
+ const normalizedQuality = Math.round(analysis.quality);
210
+ const aiApprovedCurrentTrade = analysis?.direction === direction;
211
+ return aiApprovedCurrentTrade ? normalizedQuality : 0;
212
+ }
213
+ } catch (err) {
214
+ logger2.error("AI analysis error: %s %s", symbol, formatAiError(err));
215
+ }
216
+ return void 0;
217
+ };
218
+ var enrichSignalWithMlAi = async ({
219
+ signal,
220
+ symbol,
221
+ direction,
222
+ env,
223
+ ml,
224
+ ai
225
+ }) => {
226
+ await enrichSignalWithMl({ signal, env, ml });
227
+ return enrichSignalWithAi({ signal, symbol, direction, env, ai });
228
+ };
229
+ var executeEntryOrder = async ({
230
+ connector,
231
+ symbol,
232
+ direction,
233
+ qty,
234
+ currentPrice,
235
+ timestamp,
236
+ takeProfits,
237
+ stopLossPrice,
238
+ signal,
239
+ beforePlaceOrder
240
+ }) => {
241
+ await beforePlaceOrder?.();
242
+ const orderPlaced = await connector.placeOrder(
243
+ {
244
+ symbol,
245
+ qty,
246
+ price: currentPrice,
247
+ isLimit: false,
248
+ timestamp,
249
+ direction,
250
+ signal
251
+ },
252
+ takeProfits,
253
+ stopLossPrice
254
+ );
255
+ signal.orderStatus = orderPlaced ? "completed" : "failed";
256
+ signal.orderSkipReason = void 0;
257
+ const currentPosition = await connector.getPosition(symbol);
258
+ if (currentPosition?.price) {
259
+ signal.prices.currentPrice = currentPosition.price;
260
+ return currentPosition.price;
261
+ }
262
+ return currentPrice;
263
+ };
264
+
265
+ // src/strategyHelpers/config.ts
266
+ var import_lodash2 = __toESM(require_lodash());
267
+ import { getData, redisKeys } from "@tradejs/infra/redis";
268
+ var resolveStrategyConfig = async ({
269
+ strategyName,
270
+ userName,
271
+ symbol,
272
+ baseConfig,
273
+ defaults
274
+ }) => {
275
+ const mergeIfNotEmpty = (target, patch) => patch && !import_lodash2.default.isEmpty(patch) ? {
276
+ ...target,
277
+ ...patch
278
+ } : target;
279
+ let config = {
280
+ ...defaults,
281
+ ...baseConfig
282
+ };
283
+ let isConfigFromBacktest = false;
284
+ if (config.ENV !== "BACKTEST") {
285
+ const userConfig = await getData(
286
+ redisKeys.strategyConfig(userName, strategyName),
287
+ {}
288
+ );
289
+ config = mergeIfNotEmpty(config, userConfig);
290
+ const results = await getData(
291
+ redisKeys.strategyResults(userName, strategyName),
292
+ {}
293
+ );
294
+ const backtestResult = results?.[symbol];
295
+ if (backtestResult && !import_lodash2.default.isEmpty(backtestResult.config)) {
296
+ config = mergeIfNotEmpty(
297
+ config,
298
+ backtestResult.config
299
+ );
300
+ isConfigFromBacktest = true;
301
+ }
302
+ }
303
+ return { config, isConfigFromBacktest };
304
+ };
305
+
306
+ // src/strategyRuntime.ts
307
+ var resolveEntryRuntimePolicy = ({
308
+ decision,
309
+ config,
310
+ manifest
311
+ }) => {
312
+ const manifestDefaults = manifest?.entryRuntimeDefaults;
313
+ const adapterMl = manifest?.mlAdapter?.mapEntryRuntimeFromConfig?.(config);
314
+ const adapterAi = manifest?.aiAdapter?.mapEntryRuntimeFromConfig?.(config);
315
+ const ml = manifestDefaults?.ml || adapterMl || decision.runtime?.ml ? {
316
+ ...manifestDefaults?.ml,
317
+ ...adapterMl,
318
+ ...decision.runtime?.ml
319
+ } : void 0;
320
+ const ai = manifestDefaults?.ai || adapterAi || decision.runtime?.ai ? {
321
+ ...manifestDefaults?.ai,
322
+ ...adapterAi,
323
+ ...decision.runtime?.ai
324
+ } : void 0;
325
+ return {
326
+ ...manifestDefaults,
327
+ ...decision.runtime,
328
+ ml,
329
+ ai
330
+ };
331
+ };
332
+ var shouldExecuteEntryDecision = ({
333
+ makeOrdersEnabled,
334
+ env,
335
+ signal,
336
+ quality,
337
+ minAiQuality
338
+ }) => makeOrdersEnabled && (!signal || env === "BACKTEST" || quality == null || quality >= minAiQuality);
339
+ var getEntrySkipReason = ({
340
+ makeOrdersEnabled,
341
+ env,
342
+ quality,
343
+ minAiQuality
344
+ }) => {
345
+ if (!makeOrdersEnabled) {
346
+ return "MAKE_ORDERS_DISABLED";
347
+ }
348
+ if (env !== "BACKTEST" && quality != null && Number.isFinite(quality) && quality < minAiQuality) {
349
+ return `AI_QUALITY_BELOW_MIN (${quality} < ${minAiQuality})`;
350
+ }
351
+ return "ENTRY_POLICY_BLOCKED";
352
+ };
353
+ var handleExitDecision = async ({
354
+ connector,
355
+ symbol,
356
+ decision,
357
+ onRuntimeError
358
+ }) => {
359
+ try {
360
+ await connector.closePosition({
361
+ symbol,
362
+ price: decision.closePlan.price,
363
+ timestamp: decision.closePlan.timestamp,
364
+ direction: decision.closePlan.direction
365
+ });
366
+ } catch (err) {
367
+ await onRuntimeError?.({
368
+ stage: "closePosition",
369
+ error: err,
370
+ decision
371
+ });
372
+ logger3.error("close order error: %s %s", symbol, err);
373
+ return "ORDER_ERROR";
374
+ }
375
+ return decision.code;
376
+ };
377
+ var executeEntryDecision = async ({
378
+ connector,
379
+ symbol,
380
+ decision,
381
+ runtime,
382
+ manifest,
383
+ hookBase,
384
+ invokeHook,
385
+ notifyRuntimeError
386
+ }) => {
387
+ const signal = decision.signal;
388
+ const beforePlaceOrder = async () => {
389
+ await invokeHook(
390
+ "beforePlaceOrder",
391
+ manifest?.hooks?.beforePlaceOrder,
392
+ {
393
+ ...hookBase,
394
+ entryContext: decision.entryContext,
395
+ runtime,
396
+ decision,
397
+ signal
398
+ },
399
+ { decision, signal }
400
+ );
401
+ try {
402
+ await runtime.beforePlaceOrder?.();
403
+ } catch (error) {
404
+ await notifyRuntimeError({
405
+ stage: "runtime.beforePlaceOrder",
406
+ error,
407
+ decision,
408
+ signal
409
+ });
410
+ throw error;
411
+ }
412
+ };
413
+ try {
414
+ if (signal) {
415
+ await executeEntryOrder({
416
+ connector,
417
+ symbol,
418
+ direction: decision.entryContext.direction,
419
+ qty: decision.orderPlan.qty,
420
+ currentPrice: decision.entryContext.prices.currentPrice,
421
+ timestamp: decision.entryContext.timestamp,
422
+ takeProfits: decision.orderPlan.takeProfits,
423
+ stopLossPrice: decision.orderPlan.stopLossPrice,
424
+ signal,
425
+ beforePlaceOrder
426
+ });
427
+ await invokeHook(
428
+ "afterPlaceOrder",
429
+ manifest?.hooks?.afterPlaceOrder,
430
+ {
431
+ ...hookBase,
432
+ decision,
433
+ runtime,
434
+ signal,
435
+ orderResult: signal
436
+ },
437
+ { decision, signal }
438
+ );
439
+ return signal;
440
+ }
441
+ await beforePlaceOrder();
442
+ await connector.placeOrder(
443
+ {
444
+ symbol,
445
+ qty: decision.orderPlan.qty,
446
+ price: decision.entryContext.prices.currentPrice,
447
+ timestamp: decision.entryContext.timestamp,
448
+ direction: decision.entryContext.direction
449
+ },
450
+ decision.orderPlan.takeProfits,
451
+ decision.orderPlan.stopLossPrice
452
+ );
453
+ await invokeHook(
454
+ "afterPlaceOrder",
455
+ manifest?.hooks?.afterPlaceOrder,
456
+ {
457
+ ...hookBase,
458
+ decision,
459
+ runtime,
460
+ signal,
461
+ orderResult: decision.code
462
+ },
463
+ { decision, signal }
464
+ );
465
+ } catch (err) {
466
+ if (signal) {
467
+ signal.orderStatus = "failed";
468
+ }
469
+ await notifyRuntimeError({
470
+ stage: "placeOrder",
471
+ error: err,
472
+ decision,
473
+ signal
474
+ });
475
+ logger3.error("order error: %s %s", symbol, err);
476
+ return signal ?? "ORDER_ERROR";
477
+ }
478
+ return signal ?? decision.code;
479
+ };
480
+ var createStrategyRuntime = ({
481
+ strategyName,
482
+ defaults,
483
+ createCore,
484
+ manifest: staticManifest,
485
+ strategyDirectory
486
+ }) => {
487
+ const resolveManifest = (name) => {
488
+ if (!name) {
489
+ return void 0;
490
+ }
491
+ if (staticManifest?.name === name) {
492
+ return staticManifest;
493
+ }
494
+ return getStrategyManifest(name);
495
+ };
496
+ const loadPineScript = createLoadPineScript(
497
+ strategyDirectory ? path.resolve(strategyDirectory) : path.resolve(
498
+ process.cwd(),
499
+ "packages",
500
+ "strategies",
501
+ "src",
502
+ strategyName
503
+ )
504
+ );
505
+ return async ({
506
+ userName,
507
+ config: baseConfig,
508
+ symbol,
509
+ data,
510
+ btcData,
511
+ btcBinanceData,
512
+ btcCoinbaseData,
513
+ connector
514
+ }) => {
515
+ const { config, isConfigFromBacktest } = await resolveStrategyConfig({
516
+ strategyName,
517
+ userName,
518
+ symbol,
519
+ baseConfig,
520
+ defaults
521
+ });
522
+ const env = String(config.ENV ?? "BACKTEST");
523
+ const strategyManifest = resolveManifest(strategyName);
524
+ const hookBase = {
525
+ connector,
526
+ strategyName,
527
+ userName,
528
+ symbol,
529
+ config,
530
+ env,
531
+ isConfigFromBacktest
532
+ };
533
+ const notifyRuntimeError = async ({
534
+ stage,
535
+ error,
536
+ decision,
537
+ signal
538
+ }) => {
539
+ const errorStrategyName = decision?.kind === "entry" ? decision.entryContext.strategy : strategyName;
540
+ const errorManifest = resolveManifest(errorStrategyName) ?? strategyManifest;
541
+ const errorHookBase = {
542
+ ...hookBase,
543
+ strategyName: errorStrategyName
544
+ };
545
+ const onRuntimeError = errorManifest?.hooks?.onRuntimeError;
546
+ if (!onRuntimeError) {
547
+ return;
548
+ }
549
+ try {
550
+ await onRuntimeError({
551
+ ...errorHookBase,
552
+ stage,
553
+ error,
554
+ decision,
555
+ signal
556
+ });
557
+ } catch (hookError) {
558
+ logger3.error(
559
+ "runtime hook onRuntimeError failed: %s %s",
560
+ strategyName,
561
+ hookError
562
+ );
563
+ }
564
+ };
565
+ const invokeHook = async (stage, hook, params, errorContext = {}) => {
566
+ if (!hook) {
567
+ return void 0;
568
+ }
569
+ try {
570
+ return await hook(params);
571
+ } catch (error) {
572
+ logger3.error(
573
+ 'strategy hook "%s" failed for %s: %s',
574
+ stage,
575
+ strategyName,
576
+ error
577
+ );
578
+ await notifyRuntimeError({
579
+ stage,
580
+ error,
581
+ decision: errorContext.decision,
582
+ signal: errorContext.signal
583
+ });
584
+ return void 0;
585
+ }
586
+ };
587
+ const indicatorsState = createStrategyIndicatorsState({
588
+ env,
589
+ data,
590
+ btcData,
591
+ btcBinanceData,
592
+ btcCoinbaseData,
593
+ periods: buildDefaultIndicatorPeriods(config)
594
+ });
595
+ const strategyApi = createStrategyAPI({
596
+ strategy: strategyName,
597
+ symbol,
598
+ interval: config.INTERVAL ?? "15",
599
+ env,
600
+ connector,
601
+ cachedData: data,
602
+ indicatorsState,
603
+ preloadStart: getTimestamp(SIGNALS_PRELOAD_DAYS),
604
+ backtestPriceMode: config.BACKTEST_PRICE_MODE,
605
+ isConfigFromBacktest
606
+ });
607
+ const core = await createCore({
608
+ userName,
609
+ symbol,
610
+ config,
611
+ isConfigFromBacktest,
612
+ connector,
613
+ data,
614
+ btcData,
615
+ loadPineScript,
616
+ strategyApi,
617
+ indicatorsState
618
+ });
619
+ await invokeHook("onInit", strategyManifest?.hooks?.onInit, {
620
+ ...hookBase,
621
+ data,
622
+ btcData
623
+ });
624
+ return async (candle, btcCandle) => {
625
+ data.push(candle);
626
+ btcData.push(btcCandle);
627
+ indicatorsState.setCurrentBar(candle, btcCandle);
628
+ const decision = await core(candle, btcCandle);
629
+ const decisionStrategyName = decision.kind === "entry" ? decision.entryContext.strategy : strategyName;
630
+ const decisionManifest = resolveManifest(decisionStrategyName) ?? strategyManifest;
631
+ const decisionHookBase = {
632
+ ...hookBase,
633
+ strategyName: decisionStrategyName
634
+ };
635
+ await invokeHook(
636
+ "afterCoreDecision",
637
+ decisionManifest?.hooks?.afterCoreDecision,
638
+ {
639
+ ...decisionHookBase,
640
+ decision,
641
+ candle,
642
+ btcCandle
643
+ },
644
+ { decision }
645
+ );
646
+ if (decision.kind === "skip") {
647
+ await invokeHook("onSkip", decisionManifest?.hooks?.onSkip, {
648
+ ...decisionHookBase,
649
+ decision,
650
+ candle,
651
+ btcCandle
652
+ });
653
+ return decision.code;
654
+ }
655
+ const makeOrdersEnabled = typeof config.MAKE_ORDERS === "boolean" ? config.MAKE_ORDERS : true;
656
+ if (decision.kind === "exit") {
657
+ if (!makeOrdersEnabled) {
658
+ return decision.code;
659
+ }
660
+ const closeGate = await invokeHook(
661
+ "beforeClosePosition",
662
+ decisionManifest?.hooks?.beforeClosePosition,
663
+ {
664
+ ...decisionHookBase,
665
+ decision
666
+ },
667
+ { decision }
668
+ );
669
+ if (closeGate?.allow === false) {
670
+ return closeGate.reason ? `CLOSE_BLOCKED_BY_HOOK:${closeGate.reason}` : "CLOSE_BLOCKED_BY_HOOK";
671
+ }
672
+ return handleExitDecision({
673
+ connector,
674
+ symbol,
675
+ decision,
676
+ onRuntimeError: async ({ stage, error, decision: exitDecision }) => {
677
+ await notifyRuntimeError({
678
+ stage,
679
+ error,
680
+ decision: exitDecision
681
+ });
682
+ }
683
+ });
684
+ }
685
+ const runtime = resolveEntryRuntimePolicy({
686
+ decision,
687
+ config,
688
+ manifest: decisionManifest
689
+ });
690
+ const signal = decision.signal;
691
+ if (signal) {
692
+ try {
693
+ await enrichSignalWithMl({
694
+ signal,
695
+ env,
696
+ ml: runtime.ml
697
+ });
698
+ } catch (error) {
699
+ await notifyRuntimeError({
700
+ stage: "enrichSignalWithMl",
701
+ error,
702
+ decision,
703
+ signal
704
+ });
705
+ throw error;
706
+ }
707
+ await invokeHook(
708
+ "afterEnrichMl",
709
+ decisionManifest?.hooks?.afterEnrichMl,
710
+ {
711
+ ...decisionHookBase,
712
+ decision,
713
+ runtime,
714
+ signal
715
+ },
716
+ { decision, signal }
717
+ );
718
+ }
719
+ let quality;
720
+ if (signal) {
721
+ try {
722
+ quality = await enrichSignalWithAi({
723
+ signal,
724
+ symbol,
725
+ direction: signal.direction,
726
+ env,
727
+ ai: runtime.ai
728
+ });
729
+ } catch (error) {
730
+ await notifyRuntimeError({
731
+ stage: "enrichSignalWithAi",
732
+ error,
733
+ decision,
734
+ signal
735
+ });
736
+ throw error;
737
+ }
738
+ await invokeHook(
739
+ "afterEnrichAi",
740
+ decisionManifest?.hooks?.afterEnrichAi,
741
+ {
742
+ ...decisionHookBase,
743
+ decision,
744
+ runtime,
745
+ signal,
746
+ quality
747
+ },
748
+ { decision, signal }
749
+ );
750
+ }
751
+ const minAiQuality = runtime.ai?.minQuality ?? 4;
752
+ const shouldMakeOrder = shouldExecuteEntryDecision({
753
+ makeOrdersEnabled,
754
+ env,
755
+ signal,
756
+ quality,
757
+ minAiQuality
758
+ });
759
+ if (!shouldMakeOrder) {
760
+ if (signal) {
761
+ signal.orderStatus = "skipped";
762
+ signal.orderSkipReason = getEntrySkipReason({
763
+ makeOrdersEnabled,
764
+ env,
765
+ quality,
766
+ minAiQuality
767
+ });
768
+ }
769
+ return signal ?? decision.code;
770
+ }
771
+ const entryGate = await invokeHook(
772
+ "beforeEntryGate",
773
+ decisionManifest?.hooks?.beforeEntryGate,
774
+ {
775
+ ...decisionHookBase,
776
+ decision,
777
+ runtime,
778
+ signal,
779
+ quality,
780
+ makeOrdersEnabled,
781
+ minAiQuality
782
+ },
783
+ { decision, signal }
784
+ );
785
+ if (entryGate?.allow === false) {
786
+ const skipReason = entryGate.reason ? `HOOK_BEFORE_ENTRY_GATE:${entryGate.reason}` : "HOOK_BEFORE_ENTRY_GATE";
787
+ if (signal) {
788
+ signal.orderStatus = "skipped";
789
+ signal.orderSkipReason = skipReason;
790
+ }
791
+ return signal ?? skipReason;
792
+ }
793
+ return executeEntryDecision({
794
+ connector,
795
+ symbol,
796
+ decision,
797
+ runtime,
798
+ manifest: decisionManifest,
799
+ hookBase: decisionHookBase,
800
+ invokeHook,
801
+ notifyRuntimeError
802
+ });
803
+ };
804
+ };
805
+ };
806
+
807
+ // src/strategies.ts
808
+ var createCloseOppositeBeforePlaceOrderHook = ({
809
+ isEnabled
810
+ }) => {
811
+ return async ({ connector, entryContext, config }) => {
812
+ if (!isEnabled(config)) {
813
+ return;
814
+ }
815
+ await closeOppositePositionsBeforeOpen({
816
+ connector,
817
+ entryContext
818
+ });
819
+ };
820
+ };
821
+ export {
822
+ MAX_AI_SERIES_POINTS,
823
+ askAI,
824
+ buildAiHumanPrompt,
825
+ buildAiPayload,
826
+ buildAiSystemPrompt,
827
+ closeOppositePositionsBeforeOpen,
828
+ createCloseOppositeBeforePlaceOrderHook,
829
+ createStrategyRuntime,
830
+ enrichSignalWithAi,
831
+ enrichSignalWithMl,
832
+ enrichSignalWithMlAi,
833
+ ensureIndicatorPluginsLoaded,
834
+ ensureStrategyPluginsLoaded,
835
+ executeEntryOrder,
836
+ getAvailableStrategyNames,
837
+ getRegisteredManifests,
838
+ getRegisteredStrategies,
839
+ getStrategyCreator,
840
+ getStrategyManifest,
841
+ isKnownStrategy,
842
+ registerStrategyEntries,
843
+ resetStrategyRegistryCache,
844
+ resolveStrategyConfig,
845
+ strategies,
846
+ trimSeriesDeep
847
+ };