@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.
- package/README.md +4 -0
- package/dist/ai.d.mts +26 -4
- package/dist/ai.d.ts +26 -4
- package/dist/ai.js +162 -165
- package/dist/ai.mjs +4 -1
- package/dist/backtest.js +515 -188
- package/dist/backtest.mjs +6 -4
- package/dist/chunk-3TWULKHV.mjs +595 -0
- package/dist/chunk-FB5NUEOQ.mjs +295 -0
- package/dist/chunk-LAJ7NA3Q.mjs +377 -0
- package/dist/{chunk-VRXU4E4O.mjs → chunk-SEQJ6V6B.mjs} +165 -446
- package/dist/{chunk-OGAWBO3Z.mjs → chunk-W26Y6IRP.mjs} +22 -3
- package/dist/chunk-XN7BC7XK.mjs +295 -0
- package/dist/cli.js +152 -161
- package/dist/cli.mjs +4 -2
- package/dist/connectors.d.mts +5 -2
- package/dist/connectors.d.ts +5 -2
- package/dist/connectors.js +329 -8
- package/dist/connectors.mjs +5 -1
- package/dist/registry-DHTLjQcr.d.mts +17 -0
- package/dist/registry-DHTLjQcr.d.ts +17 -0
- package/dist/registry.d.mts +2 -15
- package/dist/registry.d.ts +2 -15
- package/dist/registry.js +176 -161
- package/dist/registry.mjs +5 -2
- package/dist/runtimeDashboard.d.mts +12 -0
- package/dist/runtimeDashboard.d.ts +12 -0
- package/dist/runtimeDashboard.js +8502 -0
- package/dist/runtimeDashboard.mjs +1000 -0
- package/dist/runtimeStrategies.d.mts +38 -0
- package/dist/runtimeStrategies.d.ts +38 -0
- package/dist/runtimeStrategies.js +798 -0
- package/dist/runtimeStrategies.mjs +264 -0
- package/dist/runtimeTrades.d.mts +31 -0
- package/dist/runtimeTrades.d.ts +31 -0
- package/dist/runtimeTrades.js +321 -0
- package/dist/runtimeTrades.mjs +11 -0
- package/dist/strategies.d.mts +2 -2
- package/dist/strategies.d.ts +2 -2
- package/dist/strategies.js +194 -165
- package/dist/strategies.mjs +24 -379
- package/package.json +27 -6
- 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
|
+
};
|