@tradejs/node 3.0.1 → 3.1.1

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.
Files changed (43) hide show
  1. package/README.md +4 -0
  2. package/dist/ai.d.mts +26 -4
  3. package/dist/ai.d.ts +26 -4
  4. package/dist/ai.js +162 -165
  5. package/dist/ai.mjs +4 -1
  6. package/dist/backtest.js +515 -188
  7. package/dist/backtest.mjs +6 -4
  8. package/dist/chunk-3TWULKHV.mjs +595 -0
  9. package/dist/chunk-FB5NUEOQ.mjs +295 -0
  10. package/dist/chunk-LAJ7NA3Q.mjs +377 -0
  11. package/dist/{chunk-VRXU4E4O.mjs → chunk-SEQJ6V6B.mjs} +165 -446
  12. package/dist/{chunk-OGAWBO3Z.mjs → chunk-W26Y6IRP.mjs} +22 -3
  13. package/dist/chunk-XN7BC7XK.mjs +295 -0
  14. package/dist/cli.js +152 -161
  15. package/dist/cli.mjs +4 -2
  16. package/dist/connectors.d.mts +5 -2
  17. package/dist/connectors.d.ts +5 -2
  18. package/dist/connectors.js +329 -8
  19. package/dist/connectors.mjs +5 -1
  20. package/dist/registry-DHTLjQcr.d.mts +17 -0
  21. package/dist/registry-DHTLjQcr.d.ts +17 -0
  22. package/dist/registry.d.mts +2 -15
  23. package/dist/registry.d.ts +2 -15
  24. package/dist/registry.js +176 -161
  25. package/dist/registry.mjs +5 -2
  26. package/dist/runtimeDashboard.d.mts +12 -0
  27. package/dist/runtimeDashboard.d.ts +12 -0
  28. package/dist/runtimeDashboard.js +8502 -0
  29. package/dist/runtimeDashboard.mjs +1000 -0
  30. package/dist/runtimeStrategies.d.mts +38 -0
  31. package/dist/runtimeStrategies.d.ts +38 -0
  32. package/dist/runtimeStrategies.js +798 -0
  33. package/dist/runtimeStrategies.mjs +264 -0
  34. package/dist/runtimeTrades.d.mts +31 -0
  35. package/dist/runtimeTrades.d.ts +31 -0
  36. package/dist/runtimeTrades.js +321 -0
  37. package/dist/runtimeTrades.mjs +11 -0
  38. package/dist/strategies.d.mts +2 -2
  39. package/dist/strategies.d.ts +2 -2
  40. package/dist/strategies.js +194 -165
  41. package/dist/strategies.mjs +24 -379
  42. package/package.json +27 -6
  43. package/dist/chunk-V3YMKE4I.mjs +0 -271
@@ -0,0 +1,295 @@
1
+ // src/runtimeTrades.ts
2
+ import { resolveStrategyNameByOrderLinkId } from "@tradejs/core/runtimeTrades";
3
+
4
+ // src/runtimeTradeReconciliation.ts
5
+ var toNonEmptyString = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
6
+ var removeFromExactMaps = (exactByOrderLinkId, exactByOrderId, row) => {
7
+ for (const [key, value] of exactByOrderLinkId) {
8
+ if (value === row) exactByOrderLinkId.delete(key);
9
+ }
10
+ for (const [key, value] of exactByOrderId) {
11
+ if (value === row) exactByOrderId.delete(key);
12
+ }
13
+ };
14
+ var removeFromSymbolBuckets = (buckets, row) => {
15
+ const rows = buckets.get(row.symbol);
16
+ const index = rows?.findIndex((candidate) => candidate === row) ?? -1;
17
+ if (index >= 0) rows?.splice(index, 1);
18
+ };
19
+ var takeExactClosedPnlMatch = ({
20
+ exactByOrderLinkId,
21
+ exactByOrderId,
22
+ symbolBuckets,
23
+ orderLinkId,
24
+ orderId
25
+ }) => {
26
+ const keys = [
27
+ [exactByOrderLinkId, orderLinkId],
28
+ [exactByOrderId, orderId]
29
+ ];
30
+ for (const [bucket, key] of keys) {
31
+ const normalizedKey = toNonEmptyString(key);
32
+ if (!normalizedKey) continue;
33
+ const match = bucket.get(normalizedKey);
34
+ if (!match) continue;
35
+ removeFromExactMaps(exactByOrderLinkId, exactByOrderId, match);
36
+ removeFromSymbolBuckets(symbolBuckets, match);
37
+ return match;
38
+ }
39
+ return null;
40
+ };
41
+ var takeClosedPnlMatch = ({
42
+ exactByOrderLinkId,
43
+ exactByOrderId = /* @__PURE__ */ new Map(),
44
+ symbolBuckets,
45
+ trade
46
+ }) => {
47
+ const exactMatch = takeExactClosedPnlMatch({
48
+ exactByOrderLinkId,
49
+ exactByOrderId,
50
+ symbolBuckets,
51
+ orderLinkId: trade.orderId,
52
+ orderId: trade.orderId
53
+ });
54
+ if (exactMatch) return exactMatch;
55
+ const rows = symbolBuckets.get(trade.symbol);
56
+ if (!rows?.length) return null;
57
+ const minimumClosedAt = trade.entryTimestamp - 5 * 6e4;
58
+ const matchIndex = rows.reduce((bestIndex, row2, index) => {
59
+ if (!Number.isFinite(row2.closedAt) || row2.closedAt < minimumClosedAt || row2.direction && row2.direction !== trade.direction) {
60
+ return bestIndex;
61
+ }
62
+ if (bestIndex < 0) return index;
63
+ return row2.closedAt < rows[bestIndex].closedAt ? index : bestIndex;
64
+ }, -1);
65
+ if (matchIndex < 0) return null;
66
+ const [row] = rows.splice(matchIndex, 1);
67
+ if (row) removeFromExactMaps(exactByOrderLinkId, exactByOrderId, row);
68
+ return row ?? null;
69
+ };
70
+
71
+ // src/runtimeTrades.ts
72
+ var toNonEmptyString2 = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
73
+ var roundValue = (value, digits = 2) => {
74
+ if (!Number.isFinite(value)) return 0;
75
+ const factor = 10 ** digits;
76
+ return Math.round(value * factor) / factor;
77
+ };
78
+ var removeExactMatches = (exactByOrderLinkId, exactByOrderId, row) => {
79
+ if (row.orderLinkId) exactByOrderLinkId.delete(row.orderLinkId);
80
+ if (row.orderId) exactByOrderId.delete(row.orderId);
81
+ };
82
+ var takeClosedPnlMatchForEntry = ({
83
+ exactByOrderLinkId,
84
+ exactByOrderId,
85
+ symbolBuckets,
86
+ entry
87
+ }) => {
88
+ const exactMatch = takeExactClosedPnlMatch({
89
+ exactByOrderLinkId,
90
+ exactByOrderId,
91
+ symbolBuckets,
92
+ orderLinkId: entry.orderLinkId,
93
+ orderId: entry.orderId
94
+ });
95
+ if (exactMatch) return exactMatch;
96
+ const rows = symbolBuckets.get(entry.symbol);
97
+ if (!rows?.length) return null;
98
+ const minimumClosedAt = entry.entryTimestamp - 5 * 6e4;
99
+ const matchIndex = rows.reduce((bestIndex, row, index) => {
100
+ if (!Number.isFinite(row.closedAt) || row.closedAt < minimumClosedAt || row.direction && row.direction !== entry.direction) {
101
+ return bestIndex;
102
+ }
103
+ if (bestIndex < 0) return index;
104
+ return row.closedAt < rows[bestIndex].closedAt ? index : bestIndex;
105
+ }, -1);
106
+ if (matchIndex < 0) return null;
107
+ const [match] = rows.splice(matchIndex, 1);
108
+ if (match) removeExactMatches(exactByOrderLinkId, exactByOrderId, match);
109
+ return match ?? null;
110
+ };
111
+ var aggregateExchangeEntriesByOrder = (entryRows) => {
112
+ const grouped = /* @__PURE__ */ new Map();
113
+ entryRows.forEach((entry, index) => {
114
+ const orderLinkId = toNonEmptyString2(entry.orderLinkId);
115
+ const orderId = toNonEmptyString2(entry.orderId);
116
+ const key = orderLinkId || orderId || `${entry.symbol}:${entry.direction}:${entry.entryTimestamp}:${index}`;
117
+ const existing = grouped.get(key);
118
+ const hasPrice = Number.isFinite(entry.qty) && typeof entry.entryPrice === "number" && Number.isFinite(entry.entryPrice);
119
+ if (!existing) {
120
+ grouped.set(key, {
121
+ ...entry,
122
+ qty: Number.isFinite(entry.qty) ? entry.qty : 0,
123
+ pricingQty: hasPrice ? entry.qty : 0,
124
+ pricingNotional: hasPrice ? entry.qty * (entry.entryPrice ?? 0) : 0
125
+ });
126
+ return;
127
+ }
128
+ existing.qty += Number.isFinite(entry.qty) ? entry.qty : 0;
129
+ existing.entryTimestamp = Math.min(
130
+ existing.entryTimestamp,
131
+ entry.entryTimestamp
132
+ );
133
+ if (hasPrice) {
134
+ existing.pricingQty += entry.qty;
135
+ existing.pricingNotional += entry.qty * (entry.entryPrice ?? 0);
136
+ }
137
+ });
138
+ return [...grouped.values()].map(({ pricingQty, pricingNotional, ...entry }) => ({
139
+ ...entry,
140
+ qty: roundValue(entry.qty, 8),
141
+ entryPrice: pricingQty > 0 ? roundValue(pricingNotional / pricingQty, 8) : null
142
+ })).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
143
+ };
144
+ var resolveStrategy = ({
145
+ orderLinkId,
146
+ orderId,
147
+ strategyNameByOrderId,
148
+ strategyNames
149
+ }) => (orderLinkId ? strategyNameByOrderId.get(orderLinkId) : null) ?? (orderId ? strategyNameByOrderId.get(orderId) : null) ?? resolveStrategyNameByOrderLinkId({ orderLinkId, strategyNames });
150
+ var buildRiskLevels = (position) => {
151
+ const takeProfitPrice = position?.takeProfitPrice;
152
+ const stopLossPrice = position?.stopLossPrice;
153
+ if ((typeof takeProfitPrice !== "number" || !Number.isFinite(takeProfitPrice)) && (typeof stopLossPrice !== "number" || !Number.isFinite(stopLossPrice))) {
154
+ return null;
155
+ }
156
+ return {
157
+ ...typeof takeProfitPrice === "number" && Number.isFinite(takeProfitPrice) ? { takeProfitPrice } : {},
158
+ ...typeof stopLossPrice === "number" && Number.isFinite(stopLossPrice) ? { stopLossPrice } : {}
159
+ };
160
+ };
161
+ var buildExchangeFallbackRuntimeTrades = ({
162
+ entryRows,
163
+ closedPnlRows,
164
+ openPositions,
165
+ strategyNames,
166
+ existingTrades,
167
+ endTime
168
+ }) => {
169
+ if (!entryRows.length && !closedPnlRows.length) return [];
170
+ const strategyNameByOrderId = new Map(
171
+ existingTrades.filter(
172
+ (trade) => Boolean(trade.orderId?.trim() && trade.strategy?.trim())
173
+ ).map((trade) => [trade.orderId, trade.strategy])
174
+ );
175
+ const strategyNamesPool = [
176
+ .../* @__PURE__ */ new Set([
177
+ ...strategyNames,
178
+ ...existingTrades.map(({ strategy }) => strategy)
179
+ ])
180
+ ];
181
+ const openPositionBySymbol = new Map(
182
+ openPositions.map((position) => [position.symbol, position])
183
+ );
184
+ const existingOrderIds = new Set(
185
+ existingTrades.map(({ orderId }) => toNonEmptyString2(orderId)).filter((value) => value != null)
186
+ );
187
+ const exactByOrderLinkId = new Map(
188
+ closedPnlRows.filter((row) => Boolean(row.orderLinkId)).map((row) => [row.orderLinkId, row])
189
+ );
190
+ const exactByOrderId = new Map(
191
+ closedPnlRows.filter((row) => Boolean(row.orderId)).map((row) => [row.orderId, row])
192
+ );
193
+ const symbolBuckets = /* @__PURE__ */ new Map();
194
+ for (const row of closedPnlRows) {
195
+ const bucket = symbolBuckets.get(row.symbol) ?? [];
196
+ bucket.push(row);
197
+ symbolBuckets.set(row.symbol, bucket);
198
+ }
199
+ const fallbackTrades = aggregateExchangeEntriesByOrder(entryRows).map((entry) => {
200
+ const orderLinkId = toNonEmptyString2(entry.orderLinkId);
201
+ const orderId = toNonEmptyString2(entry.orderId);
202
+ const runtimeOrderId = orderLinkId ?? orderId;
203
+ if (!runtimeOrderId || existingOrderIds.has(runtimeOrderId)) return null;
204
+ const strategy = resolveStrategy({
205
+ orderLinkId,
206
+ orderId,
207
+ strategyNameByOrderId,
208
+ strategyNames: strategyNamesPool
209
+ });
210
+ if (!strategy) return null;
211
+ const closed = takeClosedPnlMatchForEntry({
212
+ exactByOrderLinkId,
213
+ exactByOrderId,
214
+ symbolBuckets,
215
+ entry
216
+ });
217
+ const position = openPositionBySymbol.get(entry.symbol);
218
+ const isActive = !closed && position?.direction === entry.direction && Number.isFinite(position.currentPrice) && Number.isFinite(position.unrealizedPnl);
219
+ const entryPrice = typeof entry.entryPrice === "number" && Number.isFinite(entry.entryPrice) ? entry.entryPrice : typeof closed?.entryPrice === "number" && Number.isFinite(closed.entryPrice) ? closed.entryPrice : null;
220
+ if (entryPrice == null) return null;
221
+ return {
222
+ orderId: runtimeOrderId,
223
+ strategy,
224
+ symbol: entry.symbol,
225
+ direction: entry.direction,
226
+ qty: entry.qty,
227
+ entryPrice,
228
+ actualEntryPrice: closed?.entryPrice ?? entry.entryPrice ?? null,
229
+ entryTimestamp: entry.entryTimestamp,
230
+ status: isActive ? "active" : "closed",
231
+ currentPrice: isActive ? position?.currentPrice ?? null : closed?.exitPrice ?? null,
232
+ currentPnl: isActive ? position?.unrealizedPnl ?? null : closed?.closedPnl ?? null,
233
+ closedPnl: isActive ? null : closed?.closedPnl ?? null,
234
+ exitPrice: isActive ? null : closed?.exitPrice ?? null,
235
+ actualExitPrice: isActive ? null : closed?.exitPrice ?? null,
236
+ exitTimestamp: isActive ? null : closed?.closedAt ?? null,
237
+ aiAnalysis: isActive ? buildRiskLevels(position) : null,
238
+ openFee: closed?.openFee ?? entry.openFee ?? null,
239
+ closeFee: closed?.closeFee ?? entry.closeFee ?? null,
240
+ fundingFee: closed?.fundingFee ?? entry.fundingFee ?? null,
241
+ totalFee: closed?.totalFee ?? entry.totalFee ?? null,
242
+ lastSyncedAt: endTime
243
+ };
244
+ }).filter((trade) => trade != null);
245
+ const usedOrderIds = /* @__PURE__ */ new Set([
246
+ ...existingOrderIds,
247
+ ...fallbackTrades.map(({ orderId }) => orderId)
248
+ ]);
249
+ const remainingClosedTrades = [...symbolBuckets.values()].flat().map((row) => {
250
+ const orderLinkId = toNonEmptyString2(row.orderLinkId);
251
+ const orderId = toNonEmptyString2(row.orderId);
252
+ const runtimeOrderId = orderLinkId ?? orderId;
253
+ if (!runtimeOrderId || usedOrderIds.has(runtimeOrderId)) return null;
254
+ const strategy = resolveStrategy({
255
+ orderLinkId,
256
+ orderId,
257
+ strategyNameByOrderId,
258
+ strategyNames: strategyNamesPool
259
+ });
260
+ if (!strategy || row.entryPrice == null || !Number.isFinite(row.entryPrice) || !row.direction) {
261
+ return null;
262
+ }
263
+ return {
264
+ orderId: runtimeOrderId,
265
+ strategy,
266
+ symbol: row.symbol,
267
+ direction: row.direction,
268
+ qty: row.qty,
269
+ entryPrice: row.entryPrice,
270
+ actualEntryPrice: row.entryPrice,
271
+ entryTimestamp: typeof row.entryTimestamp === "number" && Number.isFinite(row.entryTimestamp) ? row.entryTimestamp : row.closedAt,
272
+ status: "closed",
273
+ currentPrice: row.exitPrice,
274
+ currentPnl: row.closedPnl,
275
+ closedPnl: row.closedPnl,
276
+ exitPrice: row.exitPrice,
277
+ actualExitPrice: row.exitPrice,
278
+ exitTimestamp: row.closedAt,
279
+ openFee: row.openFee ?? null,
280
+ closeFee: row.closeFee ?? null,
281
+ fundingFee: row.fundingFee ?? null,
282
+ totalFee: row.totalFee ?? null,
283
+ lastSyncedAt: endTime
284
+ };
285
+ }).filter((trade) => trade != null);
286
+ return [...fallbackTrades, ...remainingClosedTrades].sort(
287
+ (left, right) => left.entryTimestamp - right.entryTimestamp
288
+ );
289
+ };
290
+
291
+ export {
292
+ takeExactClosedPnlMatch,
293
+ takeClosedPnlMatch,
294
+ buildExchangeFallbackRuntimeTrades
295
+ };
@@ -0,0 +1,377 @@
1
+ // src/strategies.ts
2
+ export * from "@tradejs/core/strategies";
3
+
4
+ // src/strategyHooks/closeOppositePositionsBeforeOpen.ts
5
+ import _ from "lodash";
6
+ import { logger } from "@tradejs/infra/logger";
7
+ var closeOppositePositionsBeforeOpen = async ({
8
+ connector,
9
+ entryContext
10
+ }) => {
11
+ const {
12
+ symbol: currentSymbol,
13
+ direction: currentDirection,
14
+ timestamp,
15
+ prices,
16
+ strategy: strategyName
17
+ } = entryContext;
18
+ const price = prices.currentPrice;
19
+ try {
20
+ logger.log(
21
+ "info",
22
+ "[%s] checking open positions before open: %s %s",
23
+ strategyName,
24
+ currentSymbol,
25
+ currentDirection
26
+ );
27
+ const positions = await connector.getPositions();
28
+ const openPositions = (positions || []).filter(
29
+ (item) => item && Number(item.qty) > 0
30
+ );
31
+ logger.log(
32
+ "info",
33
+ "[%s] open positions found: %s",
34
+ strategyName,
35
+ openPositions.length
36
+ );
37
+ const oppositePositions = openPositions.filter(
38
+ (item) => item.symbol !== currentSymbol && item.direction !== currentDirection
39
+ );
40
+ if (_.isEmpty(oppositePositions)) {
41
+ logger.log(
42
+ "info",
43
+ "[%s] no opposite positions to close before open: %s",
44
+ strategyName,
45
+ currentSymbol
46
+ );
47
+ return;
48
+ }
49
+ for (const position of oppositePositions) {
50
+ logger.log(
51
+ "info",
52
+ "[%s] closing opposite position: %s %s qty=%s",
53
+ strategyName,
54
+ position.symbol,
55
+ position.direction,
56
+ position.qty
57
+ );
58
+ try {
59
+ await connector.closePosition({
60
+ symbol: position.symbol,
61
+ price,
62
+ timestamp,
63
+ direction: position.direction
64
+ });
65
+ logger.log(
66
+ "info",
67
+ "[%s] opposite position closed: %s",
68
+ strategyName,
69
+ position.symbol
70
+ );
71
+ } catch (err) {
72
+ logger.log(
73
+ "error",
74
+ "[%s] failed to close opposite position: %s %s",
75
+ strategyName,
76
+ position.symbol,
77
+ err
78
+ );
79
+ }
80
+ }
81
+ } catch (err) {
82
+ logger.log(
83
+ "error",
84
+ "[%s] failed to load open positions before open: %s %s",
85
+ strategyName,
86
+ currentSymbol,
87
+ err
88
+ );
89
+ }
90
+ };
91
+ var createCloseOppositeBeforePlaceOrderHook = ({
92
+ isEnabled
93
+ }) => {
94
+ return async ({ ctx, entry }) => {
95
+ if (ctx.env === "BACKTEST") {
96
+ return;
97
+ }
98
+ if (!isEnabled(ctx.strategyConfig)) {
99
+ return;
100
+ }
101
+ await closeOppositePositionsBeforeOpen({
102
+ connector: ctx.connector,
103
+ entryContext: entry.context
104
+ });
105
+ };
106
+ };
107
+
108
+ // src/strategyHooks/shared.ts
109
+ var DEFAULT_BREAK_EVEN_TRIGGER_RISK_MULTIPLIER = 0.5;
110
+ var DEFAULT_BREAK_EVEN_STOP_PROFIT_MULTIPLIER = 0;
111
+ var DEFAULT_GLOBAL_UNREALIZED_PNL_TRIGGER_RISK_MULTIPLIER = 4;
112
+ var GLOBAL_UNREALIZED_PNL_CLOSE_ALL_CODE = "GLOBAL_UNREALIZED_PNL_TARGET_REACHED_CLOSE_ALL";
113
+ var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
114
+ var isOpenPosition = (position) => Boolean(
115
+ position && isFiniteNumber(position.price) && isFiniteNumber(position.qty) && position.qty > 0 && (position.direction === "LONG" || position.direction === "SHORT")
116
+ );
117
+ var isOpenPositionPnlSnapshot = (position) => Boolean(
118
+ isOpenPosition(position) && isFiniteNumber(position?.currentPrice) && isFiniteNumber(position?.unrealizedPnl)
119
+ );
120
+ var getStrategyMaxLossValue = (strategyConfig) => {
121
+ const maxLossValue = Number(strategyConfig?.MAX_LOSS_VALUE ?? Number.NaN);
122
+ return Number.isFinite(maxLossValue) && maxLossValue > 0 ? maxLossValue : null;
123
+ };
124
+ var getPositionStopLossPrice = (position) => {
125
+ if (!position || typeof position !== "object") {
126
+ return null;
127
+ }
128
+ const slPrice = Number(
129
+ position.slPrice ?? Number.NaN
130
+ );
131
+ if (Number.isFinite(slPrice)) {
132
+ return slPrice;
133
+ }
134
+ const signalStopLossPrice = Number(
135
+ position.signal?.prices?.stopLossPrice ?? Number.NaN
136
+ );
137
+ return Number.isFinite(signalStopLossPrice) ? signalStopLossPrice : null;
138
+ };
139
+ var getPositionTakeProfitPrice = (position) => {
140
+ if (!position || typeof position !== "object") {
141
+ return null;
142
+ }
143
+ const directTakeProfitPrice = Number(
144
+ position.tpPrice ?? position.takeProfitPrice ?? Number.NaN
145
+ );
146
+ if (Number.isFinite(directTakeProfitPrice)) {
147
+ return directTakeProfitPrice;
148
+ }
149
+ const signalTakeProfitPrice = Number(
150
+ position.signal?.prices?.takeProfitPrice ?? Number.NaN
151
+ );
152
+ return Number.isFinite(signalTakeProfitPrice) ? signalTakeProfitPrice : null;
153
+ };
154
+ var getBreakEvenStopPrice = ({
155
+ direction,
156
+ entryPrice,
157
+ takeProfitPrice,
158
+ stopProfitMultiplier
159
+ }) => {
160
+ if (!Number.isFinite(entryPrice)) {
161
+ return null;
162
+ }
163
+ const normalizedStopProfitMultiplier = Number.isFinite(stopProfitMultiplier) ? Math.min(Math.max(stopProfitMultiplier, 0), 1) : DEFAULT_BREAK_EVEN_STOP_PROFIT_MULTIPLIER;
164
+ if (takeProfitPrice == null || !Number.isFinite(takeProfitPrice) || direction === "LONG" && takeProfitPrice <= entryPrice || direction === "SHORT" && takeProfitPrice >= entryPrice) {
165
+ return entryPrice;
166
+ }
167
+ const distanceToTakeProfit = takeProfitPrice - entryPrice;
168
+ return entryPrice + distanceToTakeProfit * normalizedStopProfitMultiplier;
169
+ };
170
+ var getFavorableMovePct = ({
171
+ direction,
172
+ entryPrice,
173
+ currentPrice
174
+ }) => {
175
+ if (!Number.isFinite(entryPrice) || !Number.isFinite(currentPrice) || entryPrice <= 0) {
176
+ return null;
177
+ }
178
+ return direction === "LONG" ? (currentPrice - entryPrice) / entryPrice * 100 : (entryPrice - currentPrice) / entryPrice * 100;
179
+ };
180
+ var getPositionRiskPct = ({
181
+ direction,
182
+ entryPrice,
183
+ stopLossPrice
184
+ }) => {
185
+ if (stopLossPrice == null || !Number.isFinite(entryPrice) || !Number.isFinite(stopLossPrice) || entryPrice <= 0) {
186
+ return null;
187
+ }
188
+ return direction === "LONG" ? (entryPrice - stopLossPrice) / entryPrice * 100 : (stopLossPrice - entryPrice) / entryPrice * 100;
189
+ };
190
+ var isBreakEvenStopAlreadyApplied = ({
191
+ direction,
192
+ entryPrice,
193
+ stopLossPrice
194
+ }) => {
195
+ if (stopLossPrice == null || !Number.isFinite(entryPrice) || !Number.isFinite(stopLossPrice)) {
196
+ return false;
197
+ }
198
+ return direction === "LONG" ? stopLossPrice >= entryPrice : stopLossPrice <= entryPrice;
199
+ };
200
+ var getConfiguredDirectionRiskPct = ({
201
+ strategyConfig,
202
+ direction
203
+ }) => {
204
+ if (!strategyConfig || typeof strategyConfig !== "object") {
205
+ return null;
206
+ }
207
+ const directSideConfig = strategyConfig[direction];
208
+ const directSideRiskPct = Number(directSideConfig?.SL ?? Number.NaN);
209
+ if (Number.isFinite(directSideRiskPct)) {
210
+ return directSideRiskPct;
211
+ }
212
+ for (const candidate of Object.values(strategyConfig)) {
213
+ if (!candidate || typeof candidate !== "object") {
214
+ continue;
215
+ }
216
+ const candidateDirection = candidate.direction;
217
+ const candidateRiskPct = Number(
218
+ candidate.SL ?? Number.NaN
219
+ );
220
+ if (candidateDirection === direction && Number.isFinite(candidateRiskPct)) {
221
+ return candidateRiskPct;
222
+ }
223
+ }
224
+ return null;
225
+ };
226
+ var toStrategyCodePrefix = (strategyName) => strategyName === "TrendLine" ? "TRENDLINE" : strategyName.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase();
227
+
228
+ // src/strategyHooks/moveStopToBreakEvenAfterCoreDecision.ts
229
+ var createMoveStopToBreakEvenOnBarHook = ({
230
+ isEnabled = () => true,
231
+ triggerRiskMultiplier = DEFAULT_BREAK_EVEN_TRIGGER_RISK_MULTIPLIER,
232
+ stopProfitMultiplier = DEFAULT_BREAK_EVEN_STOP_PROFIT_MULTIPLIER
233
+ } = {}) => {
234
+ return async ({ ctx, market }) => {
235
+ if (!isEnabled(ctx.strategyConfig)) {
236
+ return;
237
+ }
238
+ const currentPosition = await ctx.connector.getPosition(ctx.symbol);
239
+ if (!isOpenPosition(currentPosition)) {
240
+ return;
241
+ }
242
+ const currentPrice = Number(market.candle.close ?? Number.NaN);
243
+ if (!Number.isFinite(currentPrice)) {
244
+ return;
245
+ }
246
+ const currentStopLossPrice = getPositionStopLossPrice(currentPosition);
247
+ if (isBreakEvenStopAlreadyApplied({
248
+ direction: currentPosition.direction,
249
+ entryPrice: currentPosition.price,
250
+ stopLossPrice: currentStopLossPrice
251
+ })) {
252
+ return;
253
+ }
254
+ const favorableMovePct = getFavorableMovePct({
255
+ direction: currentPosition.direction,
256
+ entryPrice: currentPosition.price,
257
+ currentPrice
258
+ });
259
+ const currentPositionRiskPct = getPositionRiskPct({
260
+ direction: currentPosition.direction,
261
+ entryPrice: currentPosition.price,
262
+ stopLossPrice: currentStopLossPrice
263
+ });
264
+ const configuredRiskPct = getConfiguredDirectionRiskPct({
265
+ strategyConfig: ctx.strategyConfig,
266
+ direction: currentPosition.direction
267
+ });
268
+ const triggerRiskPct = currentPositionRiskPct ?? configuredRiskPct;
269
+ if (favorableMovePct == null || triggerRiskPct == null || favorableMovePct < triggerRiskPct * triggerRiskMultiplier) {
270
+ return;
271
+ }
272
+ const stopLossPrice = getBreakEvenStopPrice({
273
+ direction: currentPosition.direction,
274
+ entryPrice: currentPosition.price,
275
+ takeProfitPrice: getPositionTakeProfitPrice(currentPosition),
276
+ stopProfitMultiplier
277
+ });
278
+ if (stopLossPrice == null) {
279
+ return;
280
+ }
281
+ return {
282
+ kind: "protect",
283
+ code: `${toStrategyCodePrefix(ctx.strategyName)}_MOVE_STOP_TO_BREAK_EVEN`,
284
+ protectPlan: {
285
+ direction: currentPosition.direction,
286
+ stopLossPrice
287
+ }
288
+ };
289
+ };
290
+ };
291
+ var createMoveStopToBreakEvenAfterCoreDecisionHook = createMoveStopToBreakEvenOnBarHook;
292
+
293
+ // src/signalsHooks/closeAllPositionsOnGlobalProfitBeforeSignals.ts
294
+ import { logger as logger2 } from "@tradejs/infra/logger";
295
+ var createCloseAllOnGlobalProfitBeforeSignalsHook = ({
296
+ getStrategyDefaultConfig = () => void 0,
297
+ profitRiskMultiplier = DEFAULT_GLOBAL_UNREALIZED_PNL_TRIGGER_RISK_MULTIPLIER
298
+ } = {}) => {
299
+ return async ({ connector, runtimeStrategies }) => {
300
+ if (typeof connector.getOpenPositionPnl !== "function") {
301
+ return;
302
+ }
303
+ const openPositions = (await connector.getOpenPositionPnl()).filter(
304
+ isOpenPositionPnlSnapshot
305
+ );
306
+ if (!openPositions.length) {
307
+ return;
308
+ }
309
+ const totalUnrealizedPnl = openPositions.reduce(
310
+ (sum, position) => sum + position.unrealizedPnl,
311
+ 0
312
+ );
313
+ if (!Number.isFinite(totalUnrealizedPnl) || totalUnrealizedPnl <= 0) {
314
+ return;
315
+ }
316
+ const maxLossValues = runtimeStrategies.flatMap(
317
+ ({ strategyName, strategyConfig }) => {
318
+ const maxLossValue = getStrategyMaxLossValue({
319
+ ...getStrategyDefaultConfig(strategyName) ?? {},
320
+ ...strategyConfig ?? {}
321
+ });
322
+ return maxLossValue == null ? [] : [maxLossValue];
323
+ }
324
+ );
325
+ if (!maxLossValues.length) {
326
+ return;
327
+ }
328
+ const averageMaxLossValue = maxLossValues.reduce((sum, value) => sum + value, 0) / maxLossValues.length;
329
+ const unrealizedPnlThreshold = averageMaxLossValue * profitRiskMultiplier;
330
+ if (!Number.isFinite(unrealizedPnlThreshold) || unrealizedPnlThreshold <= 0 || totalUnrealizedPnl < unrealizedPnlThreshold) {
331
+ return;
332
+ }
333
+ logger2.info(
334
+ "closing all positions before signals by global unrealized pnl threshold: totalPnl=%s threshold=%s positions=%s",
335
+ totalUnrealizedPnl,
336
+ unrealizedPnlThreshold,
337
+ openPositions.length
338
+ );
339
+ const closeTimestamp = Date.now();
340
+ const closeResults = await Promise.allSettled(
341
+ openPositions.map(
342
+ (position) => connector.closePosition({
343
+ symbol: position.symbol,
344
+ direction: position.direction,
345
+ price: position.currentPrice,
346
+ timestamp: closeTimestamp
347
+ })
348
+ )
349
+ );
350
+ const failedClosures = closeResults.flatMap((result, index) => {
351
+ if (result.status === "fulfilled" && result.value === true) {
352
+ return [];
353
+ }
354
+ return [
355
+ `${openPositions[index]?.symbol}:${openPositions[index]?.direction ?? "UNKNOWN"}`
356
+ ];
357
+ });
358
+ if (failedClosures.length) {
359
+ logger2.warn(
360
+ "close-all before signals hook could not confirm closures for %s",
361
+ failedClosures.join(", ")
362
+ );
363
+ }
364
+ return {
365
+ abort: true,
366
+ reason: GLOBAL_UNREALIZED_PNL_CLOSE_ALL_CODE
367
+ };
368
+ };
369
+ };
370
+
371
+ export {
372
+ closeOppositePositionsBeforeOpen,
373
+ createCloseOppositeBeforePlaceOrderHook,
374
+ createMoveStopToBreakEvenOnBarHook,
375
+ createMoveStopToBreakEvenAfterCoreDecisionHook,
376
+ createCloseAllOnGlobalProfitBeforeSignalsHook
377
+ };