@tradejs/node 3.1.3 → 3.1.5

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.
@@ -8,9 +8,7 @@ import "./chunk-Y6FXYEAI.mjs";
8
8
  // src/runtimeStrategies.ts
9
9
  import { readFile } from "fs/promises";
10
10
  import path from "path";
11
- import { getData, redisKeys } from "@tradejs/infra/redis";
12
11
  import { getRuntimeStrategyRelease } from "@tradejs/infra/runtimeStrategyReleases";
13
- import { loadRuntimeStrategyConfigs } from "@tradejs/infra/runtimeStrategyConfigs";
14
12
  import { resolveTradingAccount } from "@tradejs/infra/tradingAccounts";
15
13
  var readPackageManifest = async (projectRoot) => {
16
14
  const candidates = [
@@ -45,6 +43,23 @@ var resolveInstalledPackageVersion = async (projectRoot, packageName, manifest)
45
43
  return null;
46
44
  }
47
45
  };
46
+ var resolveStrategyPackageName = async ({
47
+ pluginSource,
48
+ projectRoot
49
+ }) => {
50
+ if (!pluginSource) return null;
51
+ if (!pluginSource.startsWith(".") && !path.isAbsolute(pluginSource)) {
52
+ return pluginSource;
53
+ }
54
+ try {
55
+ const packageJson = JSON.parse(
56
+ await readFile(path.join(projectRoot, "package.json"), "utf8")
57
+ );
58
+ return typeof packageJson.name === "string" && packageJson.name.trim() ? packageJson.name : null;
59
+ } catch {
60
+ return null;
61
+ }
62
+ };
48
63
  var validateReleaseRuntimeCompatibility = async ({
49
64
  release,
50
65
  projectRoot,
@@ -55,13 +70,17 @@ var validateReleaseRuntimeCompatibility = async ({
55
70
  release.strategyPackage,
56
71
  packageManifest
57
72
  );
58
- if (release.strategyPackageVersion && installedStrategyVersion && release.strategyPackageVersion !== installedStrategyVersion) {
73
+ if (release.strategyPackageVersion !== installedStrategyVersion) {
59
74
  throw new Error(
60
75
  `${release.strategyName} v${release.releaseVersion} requires ${release.strategyPackage}@${release.strategyPackageVersion}, image has ${installedStrategyVersion}`
61
76
  );
62
77
  }
63
- const installedRuntimeVersion = packageManifest.packages?.["@tradejs/node"];
64
- if (release.runtimePackageVersion && installedRuntimeVersion && release.runtimePackageVersion !== installedRuntimeVersion) {
78
+ const installedRuntimeVersion = await resolveInstalledPackageVersion(
79
+ projectRoot,
80
+ "@tradejs/node",
81
+ packageManifest
82
+ );
83
+ if (release.runtimePackageVersion !== installedRuntimeVersion) {
65
84
  throw new Error(
66
85
  `${release.strategyName} v${release.releaseVersion} requires @tradejs/node@${release.runtimePackageVersion}, image has ${installedRuntimeVersion}`
67
86
  );
@@ -70,24 +89,20 @@ var validateReleaseRuntimeCompatibility = async ({
70
89
  var resolveAccountId = async ({
71
90
  userName,
72
91
  deployment,
73
- connectorName,
74
- universe,
75
- legacyAccountId
92
+ universe
76
93
  }) => {
77
- const requestedAccountId = deployment?.accountId ?? legacyAccountId;
78
94
  const account = await resolveTradingAccount({
79
95
  userName,
80
- accountId: requestedAccountId,
81
- provider: deployment?.provider ?? connectorName,
96
+ accountId: deployment.accountId,
97
+ provider: deployment.provider,
82
98
  universe
83
99
  });
84
- return account?.id ?? requestedAccountId;
100
+ return account?.id ?? deployment.accountId;
85
101
  };
86
102
  var loadVersionedRuntimeStrategies = async ({
87
103
  userName,
88
104
  projectRoot,
89
- deployment,
90
- connectorName
105
+ deployment
91
106
  }) => {
92
107
  const packageManifest = await readPackageManifest(projectRoot);
93
108
  return Promise.all(
@@ -97,9 +112,14 @@ var loadVersionedRuntimeStrategies = async ({
97
112
  `Deployment ${deployment.id} strategy ${reference.strategyName} has no releaseVersion`
98
113
  );
99
114
  }
100
- if (reference.config && Object.keys(reference.config).length) {
115
+ if (Object.keys(reference).some(
116
+ (key) => !["strategyName", "releaseVersion", "controlState"].includes(key)
117
+ )) {
118
+ throw new Error(`Deployment ${deployment.id} has invalid fields`);
119
+ }
120
+ if (!reference.controlState) {
101
121
  throw new Error(
102
- `Deployment ${deployment.id} must not embed config for ${reference.strategyName}`
122
+ `Deployment ${deployment.id} strategy ${reference.strategyName} has no controlState`
103
123
  );
104
124
  }
105
125
  const release = await getRuntimeStrategyRelease(
@@ -129,13 +149,12 @@ var loadVersionedRuntimeStrategies = async ({
129
149
  const accountId = await resolveAccountId({
130
150
  userName,
131
151
  deployment,
132
- connectorName,
133
152
  universe
134
153
  });
135
154
  return {
136
155
  strategyName: reference.strategyName,
137
156
  releaseVersion: release.releaseVersion,
138
- controlState: reference.controlState ?? "active",
157
+ controlState: reference.controlState,
139
158
  interval,
140
159
  universe,
141
160
  accountId,
@@ -144,90 +163,24 @@ var loadVersionedRuntimeStrategies = async ({
144
163
  runtimePackageVersion: release.runtimePackageVersion,
145
164
  strategyCreator,
146
165
  sourceStrategyConfig: release.config,
147
- strategyConfig: release.config,
148
- // Symbol result configs are mutable legacy overlays and are not read by v2.
149
- strategyResults: {}
166
+ strategyConfig: release.config
150
167
  };
151
168
  })
152
169
  );
153
170
  };
154
- var loadLegacyRuntimeStrategies = async ({
155
- userName,
156
- projectRoot,
157
- deployment,
158
- connectorName
159
- }) => {
160
- const deploymentStrategies = new Map(
161
- (deployment?.strategies ?? []).map((strategy) => [
162
- strategy.strategyName,
163
- strategy
164
- ])
165
- );
166
- const candidates = await Promise.all(
167
- (await loadRuntimeStrategyConfigs(userName)).map(async (record) => {
168
- const binding = deploymentStrategies.get(record.strategyName);
169
- if (binding?.enabled === false || record.strategyConfig.ENABLE === false) {
170
- return null;
171
- }
172
- const universe = record.strategyConfig.UNIVERSE === "tradfi" ? "tradfi" : "crypto";
173
- const interval = String(
174
- record.strategyConfig.INTERVAL ?? "15"
175
- );
176
- const accountId = await resolveAccountId({
177
- userName,
178
- deployment,
179
- connectorName,
180
- universe,
181
- legacyAccountId: typeof record.strategyConfig.ACCOUNT_ID === "string" ? record.strategyConfig.ACCOUNT_ID : void 0
182
- });
183
- const [strategyCreator, strategyResults] = await Promise.all([
184
- getStrategyCreator(record.strategyName, projectRoot),
185
- getData(redisKeys.strategyResults(userName, record.strategyName), {})
186
- ]);
187
- if (!strategyCreator) return null;
188
- return {
189
- strategyName: record.strategyName,
190
- configId: record.configId,
191
- controlState: "active",
192
- interval,
193
- universe,
194
- accountId,
195
- strategyCreator,
196
- sourceStrategyConfig: record.strategyConfig,
197
- strategyConfig: record.strategyConfig,
198
- strategyResults: strategyResults ?? {}
199
- };
200
- })
201
- );
202
- return candidates.filter(Boolean);
203
- };
204
171
  var loadResolvedRuntimeStrategies = async ({
205
172
  userName,
206
173
  projectRoot,
207
174
  deployment,
208
- connectorName = "bybit",
209
175
  universe,
210
176
  accountId,
211
177
  interval
212
178
  }) => {
213
- const hasVersionedReferences = Boolean(
214
- deployment?.strategies.some((strategy) => strategy.releaseVersion != null)
215
- );
216
- if (hasVersionedReferences && deployment?.strategies.some((strategy) => strategy.releaseVersion == null)) {
217
- throw new Error(
218
- `Deployment ${deployment.id} mixes legacy configs and versioned releases`
219
- );
220
- }
221
- const strategies = hasVersionedReferences ? await loadVersionedRuntimeStrategies({
179
+ if (!deployment) throw new Error("Runtime deployment is required");
180
+ const strategies = await loadVersionedRuntimeStrategies({
222
181
  userName,
223
182
  projectRoot,
224
- deployment,
225
- connectorName
226
- }) : await loadLegacyRuntimeStrategies({
227
- userName,
228
- projectRoot,
229
- deployment,
230
- connectorName
183
+ deployment
231
184
  });
232
185
  const filtered = strategies.filter(
233
186
  (candidate) => (!universe || candidate.universe === universe) && (!interval || String(candidate.interval) === String(interval)) && (!accountId || candidate.accountId === accountId)
@@ -247,7 +200,11 @@ var getRuntimeStrategyPackageMetadata = async ({
247
200
  projectRoot
248
201
  }) => {
249
202
  const packageManifest = await readPackageManifest(projectRoot);
250
- const strategyPackage = await getStrategyPluginSource(strategyName, projectRoot) ?? null;
203
+ const pluginSource = await getStrategyPluginSource(strategyName, projectRoot) ?? null;
204
+ const strategyPackage = await resolveStrategyPackageName({
205
+ pluginSource,
206
+ projectRoot
207
+ });
251
208
  return {
252
209
  strategyPackage,
253
210
  strategyPackageVersion: await resolveInstalledPackageVersion(
@@ -24,7 +24,7 @@ interface ResolveStrategyConfigParams<TConfig extends StrategyConfig> {
24
24
  runtimeConfigId?: string;
25
25
  runtimeConfigSnapshot?: RuntimeStrategyConfigSnapshot;
26
26
  }
27
- declare const resolveStrategyConfig: <TConfig extends StrategyConfig>({ strategyName, userName, symbol, baseConfig, defaults, runtimeConfigId, runtimeConfigSnapshot, }: ResolveStrategyConfigParams<TConfig>) => Promise<{
27
+ declare const resolveStrategyConfig: <TConfig extends StrategyConfig>({ strategyName, baseConfig, defaults, runtimeConfigSnapshot, }: ResolveStrategyConfigParams<TConfig>) => Promise<{
28
28
  config: TConfig;
29
29
  isConfigFromBacktest: boolean;
30
30
  }>;
@@ -24,7 +24,7 @@ interface ResolveStrategyConfigParams<TConfig extends StrategyConfig> {
24
24
  runtimeConfigId?: string;
25
25
  runtimeConfigSnapshot?: RuntimeStrategyConfigSnapshot;
26
26
  }
27
- declare const resolveStrategyConfig: <TConfig extends StrategyConfig>({ strategyName, userName, symbol, baseConfig, defaults, runtimeConfigId, runtimeConfigSnapshot, }: ResolveStrategyConfigParams<TConfig>) => Promise<{
27
+ declare const resolveStrategyConfig: <TConfig extends StrategyConfig>({ strategyName, baseConfig, defaults, runtimeConfigSnapshot, }: ResolveStrategyConfigParams<TConfig>) => Promise<{
28
28
  config: TConfig;
29
29
  isConfigFromBacktest: boolean;
30
30
  }>;
@@ -4592,14 +4592,10 @@ var updatePositionProtection = async ({
4592
4592
 
4593
4593
  // src/strategyHelpers/config.ts
4594
4594
  var import_lodash = __toESM(require("lodash"));
4595
- var import_runtimeStrategyConfigs = require("@tradejs/infra/runtimeStrategyConfigs");
4596
4595
  var resolveStrategyConfig = async ({
4597
4596
  strategyName,
4598
- userName,
4599
- symbol,
4600
4597
  baseConfig,
4601
4598
  defaults,
4602
- runtimeConfigId,
4603
4599
  runtimeConfigSnapshot
4604
4600
  }) => {
4605
4601
  const mergeIfNotEmpty = (target, patch) => patch && !import_lodash.default.isEmpty(patch) ? {
@@ -4612,22 +4608,13 @@ var resolveStrategyConfig = async ({
4612
4608
  };
4613
4609
  let isConfigFromBacktest = false;
4614
4610
  if (config.ENV !== "BACKTEST") {
4615
- const userConfig = runtimeConfigSnapshot ? runtimeConfigSnapshot.userConfig : await (0, import_runtimeStrategyConfigs.getRuntimeStrategyConfig)(
4616
- userName,
4617
- strategyName,
4618
- runtimeConfigId
4619
- ) ?? {};
4620
- config = mergeIfNotEmpty(config, userConfig);
4621
- if (!runtimeConfigId || runtimeConfigId === "config") {
4622
- const symbolResultConfig = runtimeConfigSnapshot ? runtimeConfigSnapshot.symbolResultConfig : await (0, import_runtimeStrategyConfigs.getRuntimeStrategyResultConfig)(userName, strategyName, symbol);
4623
- if (symbolResultConfig && !import_lodash.default.isEmpty(symbolResultConfig)) {
4624
- config = mergeIfNotEmpty(
4625
- config,
4626
- symbolResultConfig
4627
- );
4628
- isConfigFromBacktest = true;
4629
- }
4611
+ if (!runtimeConfigSnapshot) {
4612
+ throw new Error(
4613
+ `Runtime strategy release snapshot is required for ${strategyName}`
4614
+ );
4630
4615
  }
4616
+ const userConfig = runtimeConfigSnapshot.userConfig;
4617
+ config = mergeIfNotEmpty(config, userConfig);
4631
4618
  }
4632
4619
  return { config, isConfigFromBacktest };
4633
4620
  };
@@ -5462,7 +5449,6 @@ var createStrategyRuntime = ({
5462
5449
  };
5463
5450
  const creator = async ({
5464
5451
  userName,
5465
- connectorName,
5466
5452
  config: baseConfig,
5467
5453
  symbol,
5468
5454
  universe: requestedUniverse,
@@ -1,10 +1,3 @@
1
- import {
2
- closeOppositePositionsBeforeOpen,
3
- createCloseAllOnGlobalProfitBeforeSignalsHook,
4
- createCloseOppositeBeforePlaceOrderHook,
5
- createMoveStopToBreakEvenAfterCoreDecisionHook,
6
- createMoveStopToBreakEvenOnBarHook
7
- } from "./chunk-LAJ7NA3Q.mjs";
8
1
  import {
9
2
  BINANCE_BREADTH_UNIVERSE_KEYS,
10
3
  buildBinanceBreadthUniverseSnapshot,
@@ -29,7 +22,7 @@ import {
29
22
  resolveHyperliquidPerpFromSignalSymbol,
30
23
  resolveStrategyConfig,
31
24
  validateEntryProtectionAtArrival
32
- } from "./chunk-W26Y6IRP.mjs";
25
+ } from "./chunk-MCDICN3I.mjs";
33
26
  import {
34
27
  DEFAULT_AI_MODEL,
35
28
  MAX_AI_SERIES_POINTS,
@@ -64,6 +57,376 @@ import {
64
57
  } from "./chunk-XN7BC7XK.mjs";
65
58
  import "./chunk-WS5DYEVZ.mjs";
66
59
  import "./chunk-Y6FXYEAI.mjs";
60
+
61
+ // src/strategies.ts
62
+ export * from "@tradejs/core/strategies";
63
+
64
+ // src/strategyHooks/closeOppositePositionsBeforeOpen.ts
65
+ import _ from "lodash";
66
+ import { logger } from "@tradejs/infra/logger";
67
+ var closeOppositePositionsBeforeOpen = async ({
68
+ connector,
69
+ entryContext
70
+ }) => {
71
+ const {
72
+ symbol: currentSymbol,
73
+ direction: currentDirection,
74
+ timestamp,
75
+ prices,
76
+ strategy: strategyName
77
+ } = entryContext;
78
+ const price = prices.currentPrice;
79
+ try {
80
+ logger.log(
81
+ "info",
82
+ "[%s] checking open positions before open: %s %s",
83
+ strategyName,
84
+ currentSymbol,
85
+ currentDirection
86
+ );
87
+ const positions = await connector.getPositions();
88
+ const openPositions = (positions || []).filter(
89
+ (item) => item && Number(item.qty) > 0
90
+ );
91
+ logger.log(
92
+ "info",
93
+ "[%s] open positions found: %s",
94
+ strategyName,
95
+ openPositions.length
96
+ );
97
+ const oppositePositions = openPositions.filter(
98
+ (item) => item.symbol !== currentSymbol && item.direction !== currentDirection
99
+ );
100
+ if (_.isEmpty(oppositePositions)) {
101
+ logger.log(
102
+ "info",
103
+ "[%s] no opposite positions to close before open: %s",
104
+ strategyName,
105
+ currentSymbol
106
+ );
107
+ return;
108
+ }
109
+ for (const position of oppositePositions) {
110
+ logger.log(
111
+ "info",
112
+ "[%s] closing opposite position: %s %s qty=%s",
113
+ strategyName,
114
+ position.symbol,
115
+ position.direction,
116
+ position.qty
117
+ );
118
+ try {
119
+ await connector.closePosition({
120
+ symbol: position.symbol,
121
+ price,
122
+ timestamp,
123
+ direction: position.direction
124
+ });
125
+ logger.log(
126
+ "info",
127
+ "[%s] opposite position closed: %s",
128
+ strategyName,
129
+ position.symbol
130
+ );
131
+ } catch (err) {
132
+ logger.log(
133
+ "error",
134
+ "[%s] failed to close opposite position: %s %s",
135
+ strategyName,
136
+ position.symbol,
137
+ err
138
+ );
139
+ }
140
+ }
141
+ } catch (err) {
142
+ logger.log(
143
+ "error",
144
+ "[%s] failed to load open positions before open: %s %s",
145
+ strategyName,
146
+ currentSymbol,
147
+ err
148
+ );
149
+ }
150
+ };
151
+ var createCloseOppositeBeforePlaceOrderHook = ({
152
+ isEnabled
153
+ }) => {
154
+ return async ({ ctx, entry }) => {
155
+ if (ctx.env === "BACKTEST") {
156
+ return;
157
+ }
158
+ if (!isEnabled(ctx.strategyConfig)) {
159
+ return;
160
+ }
161
+ await closeOppositePositionsBeforeOpen({
162
+ connector: ctx.connector,
163
+ entryContext: entry.context
164
+ });
165
+ };
166
+ };
167
+
168
+ // src/strategyHooks/shared.ts
169
+ var DEFAULT_BREAK_EVEN_TRIGGER_RISK_MULTIPLIER = 0.5;
170
+ var DEFAULT_BREAK_EVEN_STOP_PROFIT_MULTIPLIER = 0;
171
+ var DEFAULT_GLOBAL_UNREALIZED_PNL_TRIGGER_RISK_MULTIPLIER = 4;
172
+ var GLOBAL_UNREALIZED_PNL_CLOSE_ALL_CODE = "GLOBAL_UNREALIZED_PNL_TARGET_REACHED_CLOSE_ALL";
173
+ var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
174
+ var isOpenPosition = (position) => Boolean(
175
+ position && isFiniteNumber(position.price) && isFiniteNumber(position.qty) && position.qty > 0 && (position.direction === "LONG" || position.direction === "SHORT")
176
+ );
177
+ var isOpenPositionPnlSnapshot = (position) => Boolean(
178
+ isOpenPosition(position) && isFiniteNumber(position?.currentPrice) && isFiniteNumber(position?.unrealizedPnl)
179
+ );
180
+ var getStrategyMaxLossValue = (strategyConfig) => {
181
+ const maxLossValue = Number(strategyConfig?.MAX_LOSS_VALUE ?? Number.NaN);
182
+ return Number.isFinite(maxLossValue) && maxLossValue > 0 ? maxLossValue : null;
183
+ };
184
+ var getPositionStopLossPrice = (position) => {
185
+ if (!position || typeof position !== "object") {
186
+ return null;
187
+ }
188
+ const slPrice = Number(
189
+ position.slPrice ?? Number.NaN
190
+ );
191
+ if (Number.isFinite(slPrice)) {
192
+ return slPrice;
193
+ }
194
+ const signalStopLossPrice = Number(
195
+ position.signal?.prices?.stopLossPrice ?? Number.NaN
196
+ );
197
+ return Number.isFinite(signalStopLossPrice) ? signalStopLossPrice : null;
198
+ };
199
+ var getPositionTakeProfitPrice = (position) => {
200
+ if (!position || typeof position !== "object") {
201
+ return null;
202
+ }
203
+ const directTakeProfitPrice = Number(
204
+ position.tpPrice ?? position.takeProfitPrice ?? Number.NaN
205
+ );
206
+ if (Number.isFinite(directTakeProfitPrice)) {
207
+ return directTakeProfitPrice;
208
+ }
209
+ const signalTakeProfitPrice = Number(
210
+ position.signal?.prices?.takeProfitPrice ?? Number.NaN
211
+ );
212
+ return Number.isFinite(signalTakeProfitPrice) ? signalTakeProfitPrice : null;
213
+ };
214
+ var getBreakEvenStopPrice = ({
215
+ direction,
216
+ entryPrice,
217
+ takeProfitPrice,
218
+ stopProfitMultiplier
219
+ }) => {
220
+ if (!Number.isFinite(entryPrice)) {
221
+ return null;
222
+ }
223
+ const normalizedStopProfitMultiplier = Number.isFinite(stopProfitMultiplier) ? Math.min(Math.max(stopProfitMultiplier, 0), 1) : DEFAULT_BREAK_EVEN_STOP_PROFIT_MULTIPLIER;
224
+ if (takeProfitPrice == null || !Number.isFinite(takeProfitPrice) || direction === "LONG" && takeProfitPrice <= entryPrice || direction === "SHORT" && takeProfitPrice >= entryPrice) {
225
+ return entryPrice;
226
+ }
227
+ const distanceToTakeProfit = takeProfitPrice - entryPrice;
228
+ return entryPrice + distanceToTakeProfit * normalizedStopProfitMultiplier;
229
+ };
230
+ var getFavorableMovePct = ({
231
+ direction,
232
+ entryPrice,
233
+ currentPrice
234
+ }) => {
235
+ if (!Number.isFinite(entryPrice) || !Number.isFinite(currentPrice) || entryPrice <= 0) {
236
+ return null;
237
+ }
238
+ return direction === "LONG" ? (currentPrice - entryPrice) / entryPrice * 100 : (entryPrice - currentPrice) / entryPrice * 100;
239
+ };
240
+ var getPositionRiskPct = ({
241
+ direction,
242
+ entryPrice,
243
+ stopLossPrice
244
+ }) => {
245
+ if (stopLossPrice == null || !Number.isFinite(entryPrice) || !Number.isFinite(stopLossPrice) || entryPrice <= 0) {
246
+ return null;
247
+ }
248
+ return direction === "LONG" ? (entryPrice - stopLossPrice) / entryPrice * 100 : (stopLossPrice - entryPrice) / entryPrice * 100;
249
+ };
250
+ var isBreakEvenStopAlreadyApplied = ({
251
+ direction,
252
+ entryPrice,
253
+ stopLossPrice
254
+ }) => {
255
+ if (stopLossPrice == null || !Number.isFinite(entryPrice) || !Number.isFinite(stopLossPrice)) {
256
+ return false;
257
+ }
258
+ return direction === "LONG" ? stopLossPrice >= entryPrice : stopLossPrice <= entryPrice;
259
+ };
260
+ var getConfiguredDirectionRiskPct = ({
261
+ strategyConfig,
262
+ direction
263
+ }) => {
264
+ if (!strategyConfig || typeof strategyConfig !== "object") {
265
+ return null;
266
+ }
267
+ const directSideConfig = strategyConfig[direction];
268
+ const directSideRiskPct = Number(directSideConfig?.SL ?? Number.NaN);
269
+ if (Number.isFinite(directSideRiskPct)) {
270
+ return directSideRiskPct;
271
+ }
272
+ for (const candidate of Object.values(strategyConfig)) {
273
+ if (!candidate || typeof candidate !== "object") {
274
+ continue;
275
+ }
276
+ const candidateDirection = candidate.direction;
277
+ const candidateRiskPct = Number(
278
+ candidate.SL ?? Number.NaN
279
+ );
280
+ if (candidateDirection === direction && Number.isFinite(candidateRiskPct)) {
281
+ return candidateRiskPct;
282
+ }
283
+ }
284
+ return null;
285
+ };
286
+ var toStrategyCodePrefix = (strategyName) => strategyName === "TrendLine" ? "TRENDLINE" : strategyName.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase();
287
+
288
+ // src/strategyHooks/moveStopToBreakEvenAfterCoreDecision.ts
289
+ var createMoveStopToBreakEvenOnBarHook = ({
290
+ isEnabled = () => true,
291
+ triggerRiskMultiplier = DEFAULT_BREAK_EVEN_TRIGGER_RISK_MULTIPLIER,
292
+ stopProfitMultiplier = DEFAULT_BREAK_EVEN_STOP_PROFIT_MULTIPLIER
293
+ } = {}) => {
294
+ return async ({ ctx, market }) => {
295
+ if (!isEnabled(ctx.strategyConfig)) {
296
+ return;
297
+ }
298
+ const currentPosition = await ctx.connector.getPosition(ctx.symbol);
299
+ if (!isOpenPosition(currentPosition)) {
300
+ return;
301
+ }
302
+ const currentPrice = Number(market.candle.close ?? Number.NaN);
303
+ if (!Number.isFinite(currentPrice)) {
304
+ return;
305
+ }
306
+ const currentStopLossPrice = getPositionStopLossPrice(currentPosition);
307
+ if (isBreakEvenStopAlreadyApplied({
308
+ direction: currentPosition.direction,
309
+ entryPrice: currentPosition.price,
310
+ stopLossPrice: currentStopLossPrice
311
+ })) {
312
+ return;
313
+ }
314
+ const favorableMovePct = getFavorableMovePct({
315
+ direction: currentPosition.direction,
316
+ entryPrice: currentPosition.price,
317
+ currentPrice
318
+ });
319
+ const currentPositionRiskPct = getPositionRiskPct({
320
+ direction: currentPosition.direction,
321
+ entryPrice: currentPosition.price,
322
+ stopLossPrice: currentStopLossPrice
323
+ });
324
+ const configuredRiskPct = getConfiguredDirectionRiskPct({
325
+ strategyConfig: ctx.strategyConfig,
326
+ direction: currentPosition.direction
327
+ });
328
+ const triggerRiskPct = currentPositionRiskPct ?? configuredRiskPct;
329
+ if (favorableMovePct == null || triggerRiskPct == null || favorableMovePct < triggerRiskPct * triggerRiskMultiplier) {
330
+ return;
331
+ }
332
+ const stopLossPrice = getBreakEvenStopPrice({
333
+ direction: currentPosition.direction,
334
+ entryPrice: currentPosition.price,
335
+ takeProfitPrice: getPositionTakeProfitPrice(currentPosition),
336
+ stopProfitMultiplier
337
+ });
338
+ if (stopLossPrice == null) {
339
+ return;
340
+ }
341
+ return {
342
+ kind: "protect",
343
+ code: `${toStrategyCodePrefix(ctx.strategyName)}_MOVE_STOP_TO_BREAK_EVEN`,
344
+ protectPlan: {
345
+ direction: currentPosition.direction,
346
+ stopLossPrice
347
+ }
348
+ };
349
+ };
350
+ };
351
+ var createMoveStopToBreakEvenAfterCoreDecisionHook = createMoveStopToBreakEvenOnBarHook;
352
+
353
+ // src/signalsHooks/closeAllPositionsOnGlobalProfitBeforeSignals.ts
354
+ import { logger as logger2 } from "@tradejs/infra/logger";
355
+ var createCloseAllOnGlobalProfitBeforeSignalsHook = ({
356
+ getStrategyDefaultConfig = () => void 0,
357
+ profitRiskMultiplier = DEFAULT_GLOBAL_UNREALIZED_PNL_TRIGGER_RISK_MULTIPLIER
358
+ } = {}) => {
359
+ return async ({ connector, runtimeStrategies }) => {
360
+ if (typeof connector.getOpenPositionPnl !== "function") {
361
+ return;
362
+ }
363
+ const openPositions = (await connector.getOpenPositionPnl()).filter(
364
+ isOpenPositionPnlSnapshot
365
+ );
366
+ if (!openPositions.length) {
367
+ return;
368
+ }
369
+ const totalUnrealizedPnl = openPositions.reduce(
370
+ (sum, position) => sum + position.unrealizedPnl,
371
+ 0
372
+ );
373
+ if (!Number.isFinite(totalUnrealizedPnl) || totalUnrealizedPnl <= 0) {
374
+ return;
375
+ }
376
+ const maxLossValues = runtimeStrategies.flatMap(
377
+ ({ strategyName, strategyConfig }) => {
378
+ const maxLossValue = getStrategyMaxLossValue({
379
+ ...getStrategyDefaultConfig(strategyName) ?? {},
380
+ ...strategyConfig ?? {}
381
+ });
382
+ return maxLossValue == null ? [] : [maxLossValue];
383
+ }
384
+ );
385
+ if (!maxLossValues.length) {
386
+ return;
387
+ }
388
+ const averageMaxLossValue = maxLossValues.reduce((sum, value) => sum + value, 0) / maxLossValues.length;
389
+ const unrealizedPnlThreshold = averageMaxLossValue * profitRiskMultiplier;
390
+ if (!Number.isFinite(unrealizedPnlThreshold) || unrealizedPnlThreshold <= 0 || totalUnrealizedPnl < unrealizedPnlThreshold) {
391
+ return;
392
+ }
393
+ logger2.info(
394
+ "closing all positions before signals by global unrealized pnl threshold: totalPnl=%s threshold=%s positions=%s",
395
+ totalUnrealizedPnl,
396
+ unrealizedPnlThreshold,
397
+ openPositions.length
398
+ );
399
+ const closeTimestamp = Date.now();
400
+ const closeResults = await Promise.allSettled(
401
+ openPositions.map(
402
+ (position) => connector.closePosition({
403
+ symbol: position.symbol,
404
+ direction: position.direction,
405
+ price: position.currentPrice,
406
+ timestamp: closeTimestamp
407
+ })
408
+ )
409
+ );
410
+ const failedClosures = closeResults.flatMap((result, index) => {
411
+ if (result.status === "fulfilled" && result.value === true) {
412
+ return [];
413
+ }
414
+ return [
415
+ `${openPositions[index]?.symbol}:${openPositions[index]?.direction ?? "UNKNOWN"}`
416
+ ];
417
+ });
418
+ if (failedClosures.length) {
419
+ logger2.warn(
420
+ "close-all before signals hook could not confirm closures for %s",
421
+ failedClosures.join(", ")
422
+ );
423
+ }
424
+ return {
425
+ abort: true,
426
+ reason: GLOBAL_UNREALIZED_PNL_CLOSE_ALL_CODE
427
+ };
428
+ };
429
+ };
67
430
  export {
68
431
  BINANCE_BREADTH_UNIVERSE_KEYS,
69
432
  DEFAULT_AI_MODEL,