@tradejs/node 1.0.5 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai.d.mts +31 -0
- package/dist/ai.d.ts +31 -0
- package/dist/ai.js +1074 -0
- package/dist/ai.mjs +35 -0
- package/dist/backtest.js +713 -5689
- package/dist/backtest.mjs +418 -180
- package/dist/chunk-2JKX3DM7.mjs +619 -0
- package/dist/{chunk-7ICOZAKA.mjs → chunk-H6LIYHU4.mjs} +29 -11
- package/dist/{chunk-CIY64D57.mjs → chunk-JMDYEKIO.mjs} +1 -1
- package/dist/chunk-JRRG3YQG.mjs +154 -0
- package/dist/{chunk-P2ZUWONT.mjs → chunk-JU77QVJ3.mjs} +9 -2
- package/dist/{chunk-SCMBUEGK.mjs → chunk-KMJQQ53K.mjs} +1 -1
- package/dist/{chunk-GKDBAF3A.mjs → chunk-KZDHZ56N.mjs} +25 -7
- package/dist/{chunk-ZY6ULOWK.mjs → chunk-WGOYR6AB.mjs} +1 -1
- package/dist/cli.d.mts +23 -5
- package/dist/cli.d.ts +23 -5
- package/dist/cli.js +1091 -232
- package/dist/cli.mjs +342 -108
- package/dist/connectors.js +7 -2
- package/dist/connectors.mjs +2 -2
- package/dist/constants.js +1 -1
- package/dist/constants.mjs +1 -1
- package/dist/pine.d.mts +10 -6
- package/dist/pine.d.ts +10 -6
- package/dist/pine.js +40 -18
- package/dist/pine.mjs +21 -13
- package/dist/registry.js +7 -2
- package/dist/registry.mjs +2 -2
- package/dist/strategies.d.mts +28 -19
- package/dist/strategies.d.ts +28 -19
- package/dist/strategies.js +7690 -6349
- package/dist/strategies.mjs +1060 -169
- package/package.json +11 -6
- package/dist/ai-G7ATN4YL.mjs +0 -19
- package/dist/chunk-LMAKIC3C.mjs +0 -294
- package/dist/chunk-RBE4PZER.mjs +0 -49
package/dist/strategies.mjs
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import {
|
|
2
|
-
buildMlPayload
|
|
3
|
-
|
|
2
|
+
buildMlPayload,
|
|
3
|
+
enrichSignalWithDerivativesContext
|
|
4
|
+
} from "./chunk-JRRG3YQG.mjs";
|
|
4
5
|
import {
|
|
5
6
|
require_lodash
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import {
|
|
8
|
-
createLoadPineScript
|
|
9
|
-
} from "./chunk-7ICOZAKA.mjs";
|
|
10
|
-
import "./chunk-5YNMSWL3.mjs";
|
|
7
|
+
} from "./chunk-KZDHZ56N.mjs";
|
|
11
8
|
import {
|
|
9
|
+
DEFAULT_AI_MODEL,
|
|
12
10
|
MAX_AI_SERIES_POINTS,
|
|
13
11
|
askAI,
|
|
14
12
|
buildAiHumanPrompt,
|
|
15
13
|
buildAiPayload,
|
|
14
|
+
buildAiPrompts,
|
|
16
15
|
buildAiSystemPrompt,
|
|
16
|
+
ensureAiStrategyPluginsLoaded,
|
|
17
|
+
getDeterministicAiGateContext,
|
|
18
|
+
getOpenRouterModelKwargs,
|
|
19
|
+
resetAiRuntimeCache,
|
|
20
|
+
runAiPrompt,
|
|
21
|
+
runAiPromptLocal,
|
|
17
22
|
trimSeriesDeep
|
|
18
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-2JKX3DM7.mjs";
|
|
24
|
+
import {
|
|
25
|
+
createPineScriptLoader
|
|
26
|
+
} from "./chunk-H6LIYHU4.mjs";
|
|
27
|
+
import "./chunk-5YNMSWL3.mjs";
|
|
19
28
|
import {
|
|
20
29
|
ensureIndicatorPluginsLoaded,
|
|
21
30
|
ensureStrategyPluginsLoaded,
|
|
@@ -28,102 +37,15 @@ import {
|
|
|
28
37
|
registerStrategyEntries,
|
|
29
38
|
resetStrategyRegistryCache,
|
|
30
39
|
strategies
|
|
31
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-WGOYR6AB.mjs";
|
|
32
41
|
import {
|
|
33
|
-
getTradejsProjectCwd
|
|
34
|
-
|
|
42
|
+
getTradejsProjectCwd,
|
|
43
|
+
loadTradejsConfig
|
|
44
|
+
} from "./chunk-JU77QVJ3.mjs";
|
|
35
45
|
import {
|
|
36
46
|
__toESM
|
|
37
47
|
} from "./chunk-6DZX6EAA.mjs";
|
|
38
48
|
|
|
39
|
-
// src/closeOppositePositionsBeforeOpen.ts
|
|
40
|
-
var import_lodash = __toESM(require_lodash());
|
|
41
|
-
import { logger } from "@tradejs/infra/logger";
|
|
42
|
-
var closeOppositePositionsBeforeOpen = async ({
|
|
43
|
-
connector,
|
|
44
|
-
entryContext
|
|
45
|
-
}) => {
|
|
46
|
-
const {
|
|
47
|
-
symbol: currentSymbol,
|
|
48
|
-
direction: currentDirection,
|
|
49
|
-
timestamp,
|
|
50
|
-
prices,
|
|
51
|
-
strategy: strategyName
|
|
52
|
-
} = entryContext;
|
|
53
|
-
const price = prices.currentPrice;
|
|
54
|
-
try {
|
|
55
|
-
logger.log(
|
|
56
|
-
"info",
|
|
57
|
-
"[%s] checking open positions before open: %s %s",
|
|
58
|
-
strategyName,
|
|
59
|
-
currentSymbol,
|
|
60
|
-
currentDirection
|
|
61
|
-
);
|
|
62
|
-
const positions = await connector.getPositions();
|
|
63
|
-
const openPositions = (positions || []).filter(
|
|
64
|
-
(item) => item && Number(item.qty) > 0
|
|
65
|
-
);
|
|
66
|
-
logger.log(
|
|
67
|
-
"info",
|
|
68
|
-
"[%s] open positions found: %s",
|
|
69
|
-
strategyName,
|
|
70
|
-
openPositions.length
|
|
71
|
-
);
|
|
72
|
-
const oppositePositions = openPositions.filter(
|
|
73
|
-
(item) => item.symbol !== currentSymbol && item.direction !== currentDirection
|
|
74
|
-
);
|
|
75
|
-
if (import_lodash.default.isEmpty(oppositePositions)) {
|
|
76
|
-
logger.log(
|
|
77
|
-
"info",
|
|
78
|
-
"[%s] no opposite positions to close before open: %s",
|
|
79
|
-
strategyName,
|
|
80
|
-
currentSymbol
|
|
81
|
-
);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
for (const position of oppositePositions) {
|
|
85
|
-
logger.log(
|
|
86
|
-
"info",
|
|
87
|
-
"[%s] closing opposite position: %s %s qty=%s",
|
|
88
|
-
strategyName,
|
|
89
|
-
position.symbol,
|
|
90
|
-
position.direction,
|
|
91
|
-
position.qty
|
|
92
|
-
);
|
|
93
|
-
try {
|
|
94
|
-
await connector.closePosition({
|
|
95
|
-
symbol: position.symbol,
|
|
96
|
-
price,
|
|
97
|
-
timestamp,
|
|
98
|
-
direction: position.direction
|
|
99
|
-
});
|
|
100
|
-
logger.log(
|
|
101
|
-
"info",
|
|
102
|
-
"[%s] opposite position closed: %s",
|
|
103
|
-
strategyName,
|
|
104
|
-
position.symbol
|
|
105
|
-
);
|
|
106
|
-
} catch (err) {
|
|
107
|
-
logger.log(
|
|
108
|
-
"error",
|
|
109
|
-
"[%s] failed to close opposite position: %s %s",
|
|
110
|
-
strategyName,
|
|
111
|
-
position.symbol,
|
|
112
|
-
err
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
} catch (err) {
|
|
117
|
-
logger.log(
|
|
118
|
-
"error",
|
|
119
|
-
"[%s] failed to load open positions before open: %s %s",
|
|
120
|
-
strategyName,
|
|
121
|
-
currentSymbol,
|
|
122
|
-
err
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
|
|
127
49
|
// src/strategies.ts
|
|
128
50
|
export * from "@tradejs/core/strategies";
|
|
129
51
|
|
|
@@ -140,12 +62,125 @@ import { logger as logger3 } from "@tradejs/infra/logger";
|
|
|
140
62
|
|
|
141
63
|
// src/strategyHelpers/runtime.ts
|
|
142
64
|
import { logger as logger2 } from "@tradejs/infra/logger";
|
|
65
|
+
import { redisKeys as redisKeys2, setData as setData2 } from "@tradejs/infra/redis";
|
|
143
66
|
import {
|
|
144
67
|
buildMlFeatures,
|
|
145
68
|
buildMlTrainingRow,
|
|
146
69
|
fetchMlThreshold,
|
|
147
70
|
trimMlTrainingRowWindows
|
|
148
71
|
} from "@tradejs/infra/ml";
|
|
72
|
+
|
|
73
|
+
// src/runtimeJournal.ts
|
|
74
|
+
import { randomUUID } from "crypto";
|
|
75
|
+
import { TTL_1M } from "@tradejs/core/constants";
|
|
76
|
+
import { logger } from "@tradejs/infra/logger";
|
|
77
|
+
import { delKey, getData, redisKeys, setData } from "@tradejs/infra/redis";
|
|
78
|
+
var now = () => Date.now();
|
|
79
|
+
var toOrderId = () => `tjs-${randomUUID().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
|
|
80
|
+
var calculateClosedPnl = ({
|
|
81
|
+
direction,
|
|
82
|
+
entryPrice,
|
|
83
|
+
exitPrice,
|
|
84
|
+
qty
|
|
85
|
+
}) => {
|
|
86
|
+
const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
|
|
87
|
+
return Number.isFinite(pnl) ? pnl : null;
|
|
88
|
+
};
|
|
89
|
+
var createRuntimeOrderId = () => toOrderId();
|
|
90
|
+
var recordRuntimeTradeOpen = async (params) => {
|
|
91
|
+
const { userName } = params;
|
|
92
|
+
if (!userName) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const record = {
|
|
96
|
+
...params,
|
|
97
|
+
status: "active",
|
|
98
|
+
currentPrice: params.entryPrice,
|
|
99
|
+
currentPnl: 0,
|
|
100
|
+
closedPnl: null,
|
|
101
|
+
exitPrice: null,
|
|
102
|
+
exitTimestamp: null,
|
|
103
|
+
lastSyncedAt: now()
|
|
104
|
+
};
|
|
105
|
+
try {
|
|
106
|
+
await Promise.all([
|
|
107
|
+
setData(redisKeys.runtimeTrade(userName, record.orderId), record, {
|
|
108
|
+
expire: 0
|
|
109
|
+
}),
|
|
110
|
+
setData(
|
|
111
|
+
redisKeys.runtimeActiveTrade(userName, record.symbol),
|
|
112
|
+
{ orderId: record.orderId },
|
|
113
|
+
{ expire: 0 }
|
|
114
|
+
)
|
|
115
|
+
]);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
logger.error(
|
|
118
|
+
"runtime trade open journal failed: %s %s",
|
|
119
|
+
record.symbol,
|
|
120
|
+
error?.message || String(error)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return record;
|
|
124
|
+
};
|
|
125
|
+
var markRuntimeTradeClosed = async (params) => {
|
|
126
|
+
const { userName, symbol, strategy, exitPrice, exitTimestamp, closedPnl } = params;
|
|
127
|
+
if (!userName) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const activeRef = await getData(
|
|
131
|
+
redisKeys.runtimeActiveTrade(userName, symbol),
|
|
132
|
+
null
|
|
133
|
+
);
|
|
134
|
+
const orderId = String(activeRef?.orderId || "").trim();
|
|
135
|
+
if (!orderId) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const existing = await getData(
|
|
139
|
+
redisKeys.runtimeTrade(userName, orderId),
|
|
140
|
+
null
|
|
141
|
+
);
|
|
142
|
+
if (!existing) {
|
|
143
|
+
await delKey(redisKeys.runtimeActiveTrade(userName, symbol));
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
if (strategy && existing.strategy !== strategy) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
|
|
150
|
+
const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
|
|
151
|
+
direction: existing.direction,
|
|
152
|
+
entryPrice: existing.entryPrice,
|
|
153
|
+
exitPrice: resolvedExitPrice,
|
|
154
|
+
qty: existing.qty
|
|
155
|
+
}) : existing.closedPnl ?? existing.currentPnl ?? null;
|
|
156
|
+
const next = {
|
|
157
|
+
...existing,
|
|
158
|
+
status: "closed",
|
|
159
|
+
currentPrice: resolvedExitPrice,
|
|
160
|
+
currentPnl: resolvedClosedPnl,
|
|
161
|
+
closedPnl: resolvedClosedPnl,
|
|
162
|
+
exitPrice: resolvedExitPrice,
|
|
163
|
+
exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
|
|
164
|
+
lastSyncedAt: now()
|
|
165
|
+
};
|
|
166
|
+
try {
|
|
167
|
+
await Promise.all([
|
|
168
|
+
setData(redisKeys.runtimeTrade(userName, orderId), next, {
|
|
169
|
+
expire: TTL_1M
|
|
170
|
+
}),
|
|
171
|
+
delKey(redisKeys.runtimeActiveTrade(userName, symbol))
|
|
172
|
+
]);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
logger.error(
|
|
175
|
+
"runtime trade close journal failed: %s %s",
|
|
176
|
+
symbol,
|
|
177
|
+
error?.message || String(error)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return next;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// src/strategyHelpers/runtime.ts
|
|
149
184
|
var formatAiError = (err) => {
|
|
150
185
|
const error = err;
|
|
151
186
|
const safeJson = (value) => {
|
|
@@ -164,6 +199,60 @@ var formatAiError = (err) => {
|
|
|
164
199
|
};
|
|
165
200
|
return safeJson(details);
|
|
166
201
|
};
|
|
202
|
+
var resolveAiQuality = (analysis, direction) => {
|
|
203
|
+
if (typeof analysis?.quality !== "number") {
|
|
204
|
+
return void 0;
|
|
205
|
+
}
|
|
206
|
+
const normalizedQuality = Math.round(analysis.quality);
|
|
207
|
+
const aiApprovedCurrentTrade = analysis.direction === direction;
|
|
208
|
+
return aiApprovedCurrentTrade ? normalizedQuality : 0;
|
|
209
|
+
};
|
|
210
|
+
var resolveAiDecision = (analysis, direction, minQuality = 4) => {
|
|
211
|
+
const quality = resolveAiQuality(analysis, direction);
|
|
212
|
+
return quality != null && quality >= minQuality ? "approved" : "rejected";
|
|
213
|
+
};
|
|
214
|
+
var withGateComparison = ({
|
|
215
|
+
llmAnalysis,
|
|
216
|
+
gateAnalysis,
|
|
217
|
+
direction,
|
|
218
|
+
minQuality
|
|
219
|
+
}) => {
|
|
220
|
+
const gateDecision = resolveAiDecision(gateAnalysis, direction, minQuality);
|
|
221
|
+
const llmDecision = resolveAiDecision(llmAnalysis, direction, minQuality);
|
|
222
|
+
return {
|
|
223
|
+
...llmAnalysis,
|
|
224
|
+
gateAnalysis,
|
|
225
|
+
gateDecision,
|
|
226
|
+
llmDecision,
|
|
227
|
+
gateContradictsLlm: gateDecision !== llmDecision
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
var findReplayAiAnalysis = ({
|
|
231
|
+
signal,
|
|
232
|
+
direction,
|
|
233
|
+
ai
|
|
234
|
+
}) => {
|
|
235
|
+
const snapshots = ai?.replayAnalyses;
|
|
236
|
+
if (!Array.isArray(snapshots) || !snapshots.length) {
|
|
237
|
+
return void 0;
|
|
238
|
+
}
|
|
239
|
+
let best = null;
|
|
240
|
+
for (const snapshot of snapshots) {
|
|
241
|
+
if (snapshot.symbol !== signal.symbol || snapshot.direction !== direction || snapshot.strategy && snapshot.strategy !== signal.strategy) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const toleranceMs = Math.max(0, Number(snapshot.toleranceMs ?? 0));
|
|
245
|
+
const diff = Math.abs(snapshot.timestamp - signal.timestamp);
|
|
246
|
+
if (diff > toleranceMs || best && diff >= best.diff) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
best = {
|
|
250
|
+
diff,
|
|
251
|
+
analysis: snapshot.analysis
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return best?.analysis;
|
|
255
|
+
};
|
|
167
256
|
var enrichSignalWithMl = async ({
|
|
168
257
|
signal,
|
|
169
258
|
env,
|
|
@@ -198,21 +287,53 @@ var enrichSignalWithMl = async ({
|
|
|
198
287
|
var enrichSignalWithAi = async ({
|
|
199
288
|
signal,
|
|
200
289
|
symbol,
|
|
290
|
+
userName,
|
|
201
291
|
direction,
|
|
202
292
|
env,
|
|
203
293
|
ai
|
|
204
294
|
}) => {
|
|
205
|
-
if (
|
|
295
|
+
if (ai?.enabled === false) {
|
|
206
296
|
return void 0;
|
|
207
297
|
}
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
298
|
+
if (env === "PARITY") {
|
|
299
|
+
const replayAnalysis = findReplayAiAnalysis({ signal, direction, ai });
|
|
300
|
+
if (replayAnalysis) {
|
|
301
|
+
signal.aiAnalysis = replayAnalysis;
|
|
302
|
+
return resolveAiQuality(replayAnalysis, direction);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (env === "BACKTEST") {
|
|
306
|
+
return void 0;
|
|
307
|
+
}
|
|
308
|
+
if (ai?.mode === "gate") {
|
|
309
|
+
const minQuality = ai.minQuality ?? 4;
|
|
310
|
+
const { askAI: askAI2, runAiPromptLocal: runAiPromptLocal2 } = await import("./ai.mjs");
|
|
311
|
+
const gateAnalysis = await runAiPromptLocal2(signal);
|
|
312
|
+
const gateQuality = resolveAiQuality(gateAnalysis, direction);
|
|
313
|
+
try {
|
|
314
|
+
const llmAnalysis = await askAI2(signal, { userName });
|
|
315
|
+
const comparedAnalysis = withGateComparison({
|
|
316
|
+
llmAnalysis,
|
|
317
|
+
gateAnalysis,
|
|
318
|
+
direction,
|
|
319
|
+
minQuality
|
|
320
|
+
});
|
|
321
|
+
signal.aiAnalysis = comparedAnalysis;
|
|
322
|
+
await setData2(
|
|
323
|
+
redisKeys2.analysis(signal.symbol, signal.signalId),
|
|
324
|
+
comparedAnalysis
|
|
325
|
+
);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
logger2.error("AI analysis error: %s %s", symbol, formatAiError(err));
|
|
328
|
+
signal.aiAnalysis = gateAnalysis;
|
|
215
329
|
}
|
|
330
|
+
return gateQuality;
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const { askAI: askAI2 } = await import("./ai.mjs");
|
|
334
|
+
const analysis = await askAI2(signal, { userName });
|
|
335
|
+
signal.aiAnalysis = analysis;
|
|
336
|
+
return resolveAiQuality(analysis, direction);
|
|
216
337
|
} catch (err) {
|
|
217
338
|
logger2.error("AI analysis error: %s %s", symbol, formatAiError(err));
|
|
218
339
|
}
|
|
@@ -220,17 +341,50 @@ var enrichSignalWithAi = async ({
|
|
|
220
341
|
};
|
|
221
342
|
var enrichSignalWithMlAi = async ({
|
|
222
343
|
signal,
|
|
344
|
+
userName,
|
|
223
345
|
symbol,
|
|
224
346
|
direction,
|
|
225
347
|
env,
|
|
226
348
|
ml,
|
|
227
349
|
ai
|
|
228
350
|
}) => {
|
|
351
|
+
await enrichSignalWithDerivativesContext({ signal, env });
|
|
229
352
|
await enrichSignalWithMl({ signal, env, ml });
|
|
230
|
-
return enrichSignalWithAi({ signal, symbol, direction, env, ai });
|
|
353
|
+
return enrichSignalWithAi({ signal, userName, symbol, direction, env, ai });
|
|
354
|
+
};
|
|
355
|
+
var applyProtectiveOrders = async ({
|
|
356
|
+
connector,
|
|
357
|
+
symbol,
|
|
358
|
+
direction,
|
|
359
|
+
qty,
|
|
360
|
+
takeProfits,
|
|
361
|
+
stopLossPrice
|
|
362
|
+
}) => {
|
|
363
|
+
if (Array.isArray(takeProfits) && takeProfits.length > 0) {
|
|
364
|
+
const tpOk = await connector.setTakeProfits({
|
|
365
|
+
symbol,
|
|
366
|
+
direction,
|
|
367
|
+
qty,
|
|
368
|
+
takeProfits
|
|
369
|
+
});
|
|
370
|
+
if (!tpOk) {
|
|
371
|
+
throw new Error("SET_TAKE_PROFITS_FAILED");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (typeof stopLossPrice === "number" && Number.isFinite(stopLossPrice)) {
|
|
375
|
+
const slOk = await connector.setStopLoss({
|
|
376
|
+
symbol,
|
|
377
|
+
direction,
|
|
378
|
+
stopLossPrice
|
|
379
|
+
});
|
|
380
|
+
if (!slOk) {
|
|
381
|
+
throw new Error("SET_STOP_LOSS_FAILED");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
231
384
|
};
|
|
232
385
|
var executeEntryOrder = async ({
|
|
233
386
|
connector,
|
|
387
|
+
userName,
|
|
234
388
|
symbol,
|
|
235
389
|
direction,
|
|
236
390
|
qty,
|
|
@@ -239,35 +393,87 @@ var executeEntryOrder = async ({
|
|
|
239
393
|
takeProfits,
|
|
240
394
|
stopLossPrice,
|
|
241
395
|
signal,
|
|
242
|
-
beforePlaceOrder
|
|
396
|
+
beforePlaceOrder,
|
|
397
|
+
recordRuntimeTrade = true
|
|
243
398
|
}) => {
|
|
244
399
|
await beforePlaceOrder?.();
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
)
|
|
400
|
+
const orderId = signal.orderId || createRuntimeOrderId();
|
|
401
|
+
signal.orderId = orderId;
|
|
402
|
+
const orderPlaced = await connector.placeOrder({
|
|
403
|
+
symbol,
|
|
404
|
+
qty,
|
|
405
|
+
price: currentPrice,
|
|
406
|
+
isLimit: false,
|
|
407
|
+
timestamp,
|
|
408
|
+
direction,
|
|
409
|
+
orderId,
|
|
410
|
+
signal
|
|
411
|
+
});
|
|
412
|
+
if (orderPlaced) {
|
|
413
|
+
try {
|
|
414
|
+
await applyProtectiveOrders({
|
|
415
|
+
connector,
|
|
416
|
+
symbol,
|
|
417
|
+
direction,
|
|
418
|
+
qty,
|
|
419
|
+
takeProfits,
|
|
420
|
+
stopLossPrice
|
|
421
|
+
});
|
|
422
|
+
} catch (error) {
|
|
423
|
+
await connector.closePosition({
|
|
424
|
+
symbol,
|
|
425
|
+
price: currentPrice,
|
|
426
|
+
timestamp,
|
|
427
|
+
direction
|
|
428
|
+
});
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
258
432
|
signal.orderStatus = orderPlaced ? "completed" : "failed";
|
|
259
433
|
signal.orderSkipReason = void 0;
|
|
260
434
|
const currentPosition = await connector.getPosition(symbol);
|
|
435
|
+
const entryPrice = currentPosition?.price && Number.isFinite(currentPosition.price) ? currentPosition.price : currentPrice;
|
|
436
|
+
signal.prices.currentPrice = entryPrice;
|
|
437
|
+
if (orderPlaced && recordRuntimeTrade) {
|
|
438
|
+
await recordRuntimeTradeOpen({
|
|
439
|
+
userName,
|
|
440
|
+
orderId,
|
|
441
|
+
signalId: signal.signalId,
|
|
442
|
+
strategy: signal.strategy,
|
|
443
|
+
symbol,
|
|
444
|
+
direction,
|
|
445
|
+
qty,
|
|
446
|
+
entryPrice,
|
|
447
|
+
entryTimestamp: timestamp,
|
|
448
|
+
...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
261
451
|
if (currentPosition?.price) {
|
|
262
|
-
signal.prices.currentPrice = currentPosition.price;
|
|
263
452
|
return currentPosition.price;
|
|
264
453
|
}
|
|
265
|
-
return
|
|
454
|
+
return entryPrice;
|
|
455
|
+
};
|
|
456
|
+
var updatePositionProtection = async ({
|
|
457
|
+
connector,
|
|
458
|
+
symbol,
|
|
459
|
+
direction,
|
|
460
|
+
qty,
|
|
461
|
+
takeProfits,
|
|
462
|
+
stopLossPrice
|
|
463
|
+
}) => {
|
|
464
|
+
await applyProtectiveOrders({
|
|
465
|
+
connector,
|
|
466
|
+
symbol,
|
|
467
|
+
direction,
|
|
468
|
+
qty,
|
|
469
|
+
takeProfits: takeProfits ?? [],
|
|
470
|
+
stopLossPrice: stopLossPrice ?? null
|
|
471
|
+
});
|
|
266
472
|
};
|
|
267
473
|
|
|
268
474
|
// src/strategyHelpers/config.ts
|
|
269
|
-
var
|
|
270
|
-
import { getData, redisKeys } from "@tradejs/infra/redis";
|
|
475
|
+
var import_lodash = __toESM(require_lodash());
|
|
476
|
+
import { getData as getData2, redisKeys as redisKeys3 } from "@tradejs/infra/redis";
|
|
271
477
|
var resolveStrategyConfig = async ({
|
|
272
478
|
strategyName,
|
|
273
479
|
userName,
|
|
@@ -275,7 +481,7 @@ var resolveStrategyConfig = async ({
|
|
|
275
481
|
baseConfig,
|
|
276
482
|
defaults
|
|
277
483
|
}) => {
|
|
278
|
-
const mergeIfNotEmpty = (target, patch) => patch && !
|
|
484
|
+
const mergeIfNotEmpty = (target, patch) => patch && !import_lodash.default.isEmpty(patch) ? {
|
|
279
485
|
...target,
|
|
280
486
|
...patch
|
|
281
487
|
} : target;
|
|
@@ -285,17 +491,17 @@ var resolveStrategyConfig = async ({
|
|
|
285
491
|
};
|
|
286
492
|
let isConfigFromBacktest = false;
|
|
287
493
|
if (config.ENV !== "BACKTEST") {
|
|
288
|
-
const userConfig = await
|
|
289
|
-
|
|
494
|
+
const userConfig = await getData2(
|
|
495
|
+
redisKeys3.strategyConfig(userName, strategyName),
|
|
290
496
|
{}
|
|
291
497
|
);
|
|
292
498
|
config = mergeIfNotEmpty(config, userConfig);
|
|
293
|
-
const results = await
|
|
294
|
-
|
|
499
|
+
const results = await getData2(
|
|
500
|
+
redisKeys3.strategyResults(userName, strategyName),
|
|
295
501
|
{}
|
|
296
502
|
);
|
|
297
503
|
const backtestResult = results?.[symbol];
|
|
298
|
-
if (backtestResult && !
|
|
504
|
+
if (backtestResult && !import_lodash.default.isEmpty(backtestResult.config)) {
|
|
299
505
|
config = mergeIfNotEmpty(
|
|
300
506
|
config,
|
|
301
507
|
backtestResult.config
|
|
@@ -332,27 +538,103 @@ var resolveEntryRuntimePolicy = ({
|
|
|
332
538
|
ai
|
|
333
539
|
};
|
|
334
540
|
};
|
|
541
|
+
var formatGateNumber = (value) => {
|
|
542
|
+
if (!Number.isFinite(value)) {
|
|
543
|
+
return String(value);
|
|
544
|
+
}
|
|
545
|
+
const normalized = Number(value.toFixed(6));
|
|
546
|
+
return Number.isInteger(normalized) ? String(normalized) : String(normalized);
|
|
547
|
+
};
|
|
548
|
+
var isMlRuntimeGateEnabled = (params) => {
|
|
549
|
+
const { env, ml } = params;
|
|
550
|
+
return env !== "BACKTEST" && ml?.config != null && ml.config.enabled !== false;
|
|
551
|
+
};
|
|
552
|
+
var isMlResultUnavailable = (params) => {
|
|
553
|
+
const { env, ml } = params;
|
|
554
|
+
return isMlRuntimeGateEnabled({ env, ml }) && ml?.result == null;
|
|
555
|
+
};
|
|
335
556
|
var shouldExecuteEntryDecision = ({
|
|
336
557
|
makeOrdersEnabled,
|
|
337
558
|
env,
|
|
338
559
|
signal,
|
|
560
|
+
ml,
|
|
561
|
+
aiEnabled,
|
|
339
562
|
quality,
|
|
340
563
|
minAiQuality
|
|
341
|
-
}) =>
|
|
564
|
+
}) => {
|
|
565
|
+
if (!makeOrdersEnabled) {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
if (!signal || env === "BACKTEST") {
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
if (isMlResultUnavailable({ env, ml })) {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
if (isMlRuntimeGateEnabled({ env, ml }) && ml?.result?.passed === false) {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
if (!aiEnabled) {
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
return Number.isFinite(quality) && quality >= minAiQuality;
|
|
581
|
+
};
|
|
342
582
|
var getEntrySkipReason = ({
|
|
343
583
|
makeOrdersEnabled,
|
|
344
584
|
env,
|
|
585
|
+
ml,
|
|
586
|
+
aiEnabled,
|
|
345
587
|
quality,
|
|
346
588
|
minAiQuality
|
|
347
589
|
}) => {
|
|
348
590
|
if (!makeOrdersEnabled) {
|
|
349
591
|
return "MAKE_ORDERS_DISABLED";
|
|
350
592
|
}
|
|
351
|
-
if (
|
|
593
|
+
if (isMlResultUnavailable({ env, ml })) {
|
|
594
|
+
return "ML_RESULT_UNAVAILABLE";
|
|
595
|
+
}
|
|
596
|
+
if (isMlRuntimeGateEnabled({ env, ml }) && ml?.result?.passed === false) {
|
|
597
|
+
const probability = formatGateNumber(ml.result.probability);
|
|
598
|
+
const threshold = formatGateNumber(ml.result.threshold);
|
|
599
|
+
return `ML_THRESHOLD_NOT_MET (${probability} < ${threshold})`;
|
|
600
|
+
}
|
|
601
|
+
if (env !== "BACKTEST" && aiEnabled && quality == null) {
|
|
602
|
+
return "AI_QUALITY_UNAVAILABLE";
|
|
603
|
+
}
|
|
604
|
+
if (env !== "BACKTEST" && aiEnabled && quality != null && Number.isFinite(quality) && quality < minAiQuality) {
|
|
352
605
|
return `AI_QUALITY_BELOW_MIN (${quality} < ${minAiQuality})`;
|
|
353
606
|
}
|
|
354
607
|
return "ENTRY_POLICY_BLOCKED";
|
|
355
608
|
};
|
|
609
|
+
var normalizeConfigHookList = (value) => {
|
|
610
|
+
if (Array.isArray(value)) {
|
|
611
|
+
return value;
|
|
612
|
+
}
|
|
613
|
+
return value ? [value] : [];
|
|
614
|
+
};
|
|
615
|
+
var isStrategyDecision = (value) => {
|
|
616
|
+
if (!value || typeof value !== "object") {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
const kind = value.kind;
|
|
620
|
+
return kind === "skip" || kind === "entry" || kind === "exit" || kind === "protect";
|
|
621
|
+
};
|
|
622
|
+
var CONFIG_HOOK_STAGES = [
|
|
623
|
+
"onInit",
|
|
624
|
+
"onBar",
|
|
625
|
+
"afterCoreDecision",
|
|
626
|
+
"afterBarDecision",
|
|
627
|
+
"onSkip",
|
|
628
|
+
"beforeClosePosition",
|
|
629
|
+
"afterEnrichMl",
|
|
630
|
+
"afterEnrichAi",
|
|
631
|
+
"beforeEntryGate",
|
|
632
|
+
"beforePlaceOrder",
|
|
633
|
+
"afterPlaceOrder"
|
|
634
|
+
];
|
|
635
|
+
var isConfigHookStage = (stage) => CONFIG_HOOK_STAGES.includes(
|
|
636
|
+
stage
|
|
637
|
+
);
|
|
356
638
|
var buildHookCtx = ({
|
|
357
639
|
connector,
|
|
358
640
|
strategyName,
|
|
@@ -391,6 +673,13 @@ var buildHookPolicy = ({
|
|
|
391
673
|
makeOrdersEnabled,
|
|
392
674
|
minAiQuality
|
|
393
675
|
});
|
|
676
|
+
var shouldRecordRuntimeJournal = ({
|
|
677
|
+
env,
|
|
678
|
+
config
|
|
679
|
+
}) => env !== "BACKTEST" && env !== "PARITY" && config.RECORD_RUNTIME_TRADES !== false;
|
|
680
|
+
var isTestConnector = (connector) => Boolean(
|
|
681
|
+
connector.__tradejsTestConnector
|
|
682
|
+
);
|
|
394
683
|
var buildMlHookContext = ({
|
|
395
684
|
signal,
|
|
396
685
|
env,
|
|
@@ -495,6 +784,8 @@ var buildAiHookContext = ({
|
|
|
495
784
|
};
|
|
496
785
|
var handleExitDecision = async ({
|
|
497
786
|
connector,
|
|
787
|
+
userName,
|
|
788
|
+
strategyName,
|
|
498
789
|
symbol,
|
|
499
790
|
decision,
|
|
500
791
|
market,
|
|
@@ -507,6 +798,13 @@ var handleExitDecision = async ({
|
|
|
507
798
|
timestamp: decision.closePlan.timestamp,
|
|
508
799
|
direction: decision.closePlan.direction
|
|
509
800
|
});
|
|
801
|
+
await markRuntimeTradeClosed({
|
|
802
|
+
userName,
|
|
803
|
+
strategy: strategyName,
|
|
804
|
+
symbol,
|
|
805
|
+
exitPrice: decision.closePlan.price,
|
|
806
|
+
exitTimestamp: decision.closePlan.timestamp
|
|
807
|
+
});
|
|
510
808
|
} catch (err) {
|
|
511
809
|
await onRuntimeError?.({
|
|
512
810
|
stage: "closePosition",
|
|
@@ -519,6 +817,33 @@ var handleExitDecision = async ({
|
|
|
519
817
|
}
|
|
520
818
|
return decision.code;
|
|
521
819
|
};
|
|
820
|
+
var handleProtectDecision = async ({
|
|
821
|
+
connector,
|
|
822
|
+
symbol,
|
|
823
|
+
decision,
|
|
824
|
+
market,
|
|
825
|
+
onRuntimeError
|
|
826
|
+
}) => {
|
|
827
|
+
try {
|
|
828
|
+
await updatePositionProtection({
|
|
829
|
+
connector,
|
|
830
|
+
symbol,
|
|
831
|
+
direction: decision.protectPlan.direction,
|
|
832
|
+
takeProfits: decision.protectPlan.takeProfits ?? [],
|
|
833
|
+
stopLossPrice: decision.protectPlan.stopLossPrice ?? null
|
|
834
|
+
});
|
|
835
|
+
} catch (err) {
|
|
836
|
+
await onRuntimeError?.({
|
|
837
|
+
stage: "protectPosition",
|
|
838
|
+
error: err,
|
|
839
|
+
decision,
|
|
840
|
+
market
|
|
841
|
+
});
|
|
842
|
+
logger3.error("protect position error: %s %s", symbol, err);
|
|
843
|
+
return "ORDER_ERROR";
|
|
844
|
+
}
|
|
845
|
+
return decision.code;
|
|
846
|
+
};
|
|
522
847
|
var executeEntryDecision = async ({
|
|
523
848
|
connector,
|
|
524
849
|
symbol,
|
|
@@ -531,12 +856,13 @@ var executeEntryDecision = async ({
|
|
|
531
856
|
policy,
|
|
532
857
|
ml,
|
|
533
858
|
ai,
|
|
534
|
-
|
|
859
|
+
recordRuntimeJournal,
|
|
860
|
+
invokeStageHooks,
|
|
535
861
|
notifyRuntimeError
|
|
536
862
|
}) => {
|
|
537
863
|
const signal = decision.signal;
|
|
538
864
|
const beforePlaceOrder = async () => {
|
|
539
|
-
await
|
|
865
|
+
await invokeStageHooks(
|
|
540
866
|
"beforePlaceOrder",
|
|
541
867
|
manifest?.hooks?.beforePlaceOrder,
|
|
542
868
|
{
|
|
@@ -567,6 +893,7 @@ var executeEntryDecision = async ({
|
|
|
567
893
|
if (signal) {
|
|
568
894
|
await executeEntryOrder({
|
|
569
895
|
connector,
|
|
896
|
+
userName: hookCtx.userName,
|
|
570
897
|
symbol,
|
|
571
898
|
direction: decision.entryContext.direction,
|
|
572
899
|
qty: decision.orderPlan.qty,
|
|
@@ -575,9 +902,10 @@ var executeEntryDecision = async ({
|
|
|
575
902
|
takeProfits: decision.orderPlan.takeProfits,
|
|
576
903
|
stopLossPrice: decision.orderPlan.stopLossPrice,
|
|
577
904
|
signal,
|
|
578
|
-
beforePlaceOrder
|
|
905
|
+
beforePlaceOrder,
|
|
906
|
+
recordRuntimeTrade: recordRuntimeJournal
|
|
579
907
|
});
|
|
580
|
-
await
|
|
908
|
+
await invokeStageHooks(
|
|
581
909
|
"afterPlaceOrder",
|
|
582
910
|
manifest?.hooks?.afterPlaceOrder,
|
|
583
911
|
{
|
|
@@ -597,18 +925,35 @@ var executeEntryDecision = async ({
|
|
|
597
925
|
return signal;
|
|
598
926
|
}
|
|
599
927
|
await beforePlaceOrder();
|
|
600
|
-
await connector.placeOrder(
|
|
601
|
-
|
|
928
|
+
const orderPlaced = await connector.placeOrder({
|
|
929
|
+
symbol,
|
|
930
|
+
qty: decision.orderPlan.qty,
|
|
931
|
+
price: decision.entryContext.prices.currentPrice,
|
|
932
|
+
timestamp: decision.entryContext.timestamp,
|
|
933
|
+
direction: decision.entryContext.direction
|
|
934
|
+
});
|
|
935
|
+
if (!orderPlaced) {
|
|
936
|
+
throw new Error("PLACE_ORDER_FAILED");
|
|
937
|
+
}
|
|
938
|
+
try {
|
|
939
|
+
await updatePositionProtection({
|
|
940
|
+
connector,
|
|
602
941
|
symbol,
|
|
942
|
+
direction: decision.entryContext.direction,
|
|
603
943
|
qty: decision.orderPlan.qty,
|
|
944
|
+
takeProfits: decision.orderPlan.takeProfits,
|
|
945
|
+
stopLossPrice: decision.orderPlan.stopLossPrice
|
|
946
|
+
});
|
|
947
|
+
} catch (error) {
|
|
948
|
+
await connector.closePosition({
|
|
949
|
+
symbol,
|
|
604
950
|
price: decision.entryContext.prices.currentPrice,
|
|
605
951
|
timestamp: decision.entryContext.timestamp,
|
|
606
952
|
direction: decision.entryContext.direction
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
await invokeHook(
|
|
953
|
+
});
|
|
954
|
+
throw error;
|
|
955
|
+
}
|
|
956
|
+
await invokeStageHooks(
|
|
612
957
|
"afterPlaceOrder",
|
|
613
958
|
manifest?.hooks?.afterPlaceOrder,
|
|
614
959
|
{
|
|
@@ -658,7 +1003,7 @@ var createStrategyRuntime = ({
|
|
|
658
1003
|
}
|
|
659
1004
|
return getStrategyManifest(name, projectRoot);
|
|
660
1005
|
};
|
|
661
|
-
const
|
|
1006
|
+
const loadPineScriptFile = createPineScriptLoader(
|
|
662
1007
|
strategyDirectory ? path.resolve(strategyDirectory) : path.resolve(
|
|
663
1008
|
projectRoot,
|
|
664
1009
|
"packages",
|
|
@@ -684,7 +1029,10 @@ var createStrategyRuntime = ({
|
|
|
684
1029
|
baseConfig,
|
|
685
1030
|
defaults
|
|
686
1031
|
});
|
|
1032
|
+
const projectConfig = await loadTradejsConfig(projectRoot);
|
|
1033
|
+
const projectHooks = projectConfig.hooks;
|
|
687
1034
|
const env = String(config.ENV ?? "BACKTEST");
|
|
1035
|
+
const recordRuntimeJournal = shouldRecordRuntimeJournal({ env, config });
|
|
688
1036
|
const strategyManifest = resolveManifest(strategyName);
|
|
689
1037
|
const hookBase = {
|
|
690
1038
|
connector,
|
|
@@ -699,6 +1047,7 @@ var createStrategyRuntime = ({
|
|
|
699
1047
|
...hookBase,
|
|
700
1048
|
strategyName: name
|
|
701
1049
|
});
|
|
1050
|
+
const getProjectHookList = (stage) => normalizeConfigHookList(projectHooks?.[stage]);
|
|
702
1051
|
const notifyRuntimeError = async ({
|
|
703
1052
|
stage,
|
|
704
1053
|
error,
|
|
@@ -708,21 +1057,33 @@ var createStrategyRuntime = ({
|
|
|
708
1057
|
}) => {
|
|
709
1058
|
const errorStrategyName = decision?.kind === "entry" ? decision.entryContext.strategy : strategyName;
|
|
710
1059
|
const errorManifest = resolveManifest(errorStrategyName) ?? strategyManifest;
|
|
1060
|
+
const errorParams = {
|
|
1061
|
+
ctx: getHookCtx(errorStrategyName),
|
|
1062
|
+
market,
|
|
1063
|
+
decision,
|
|
1064
|
+
entry,
|
|
1065
|
+
error: {
|
|
1066
|
+
stage,
|
|
1067
|
+
cause: error
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
for (const projectHook of getProjectHookList("onRuntimeError")) {
|
|
1071
|
+
try {
|
|
1072
|
+
await projectHook(errorParams);
|
|
1073
|
+
} catch (hookError) {
|
|
1074
|
+
logger3.error(
|
|
1075
|
+
"project hook onRuntimeError failed: %s %s",
|
|
1076
|
+
strategyName,
|
|
1077
|
+
hookError
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
711
1081
|
const onRuntimeError = errorManifest?.hooks?.onRuntimeError;
|
|
712
1082
|
if (!onRuntimeError) {
|
|
713
1083
|
return;
|
|
714
1084
|
}
|
|
715
1085
|
try {
|
|
716
|
-
await onRuntimeError(
|
|
717
|
-
ctx: getHookCtx(errorStrategyName),
|
|
718
|
-
market,
|
|
719
|
-
decision,
|
|
720
|
-
entry,
|
|
721
|
-
error: {
|
|
722
|
-
stage,
|
|
723
|
-
cause: error
|
|
724
|
-
}
|
|
725
|
-
});
|
|
1086
|
+
await onRuntimeError(errorParams);
|
|
726
1087
|
} catch (hookError) {
|
|
727
1088
|
logger3.error(
|
|
728
1089
|
"runtime hook onRuntimeError failed: %s %s",
|
|
@@ -754,6 +1115,126 @@ var createStrategyRuntime = ({
|
|
|
754
1115
|
return void 0;
|
|
755
1116
|
}
|
|
756
1117
|
};
|
|
1118
|
+
const invokeProjectHooks = async (stage, params, errorContext = {}) => {
|
|
1119
|
+
const results = [];
|
|
1120
|
+
for (const hook of getProjectHookList(stage)) {
|
|
1121
|
+
const result = await invokeHook(
|
|
1122
|
+
stage,
|
|
1123
|
+
hook,
|
|
1124
|
+
params,
|
|
1125
|
+
errorContext
|
|
1126
|
+
);
|
|
1127
|
+
if (result !== void 0) {
|
|
1128
|
+
results.push(result);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return results;
|
|
1132
|
+
};
|
|
1133
|
+
const invokeStageHooks = async (stage, hook, params, errorContext = {}) => {
|
|
1134
|
+
if (isConfigHookStage(stage)) {
|
|
1135
|
+
await invokeProjectHooks(stage, params, errorContext);
|
|
1136
|
+
}
|
|
1137
|
+
return invokeHook(stage, hook, params, errorContext);
|
|
1138
|
+
};
|
|
1139
|
+
const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
|
|
1140
|
+
const projectResults = await invokeProjectHooks(
|
|
1141
|
+
stage,
|
|
1142
|
+
params,
|
|
1143
|
+
errorContext
|
|
1144
|
+
);
|
|
1145
|
+
const projectBlock = projectResults.find(
|
|
1146
|
+
(result) => result?.allow === false
|
|
1147
|
+
);
|
|
1148
|
+
if (projectBlock?.allow === false) {
|
|
1149
|
+
return projectBlock;
|
|
1150
|
+
}
|
|
1151
|
+
return invokeHook(
|
|
1152
|
+
stage,
|
|
1153
|
+
hook,
|
|
1154
|
+
params,
|
|
1155
|
+
errorContext
|
|
1156
|
+
);
|
|
1157
|
+
};
|
|
1158
|
+
const applyProjectAfterCoreDecisionHooks = async ({
|
|
1159
|
+
hookCtx,
|
|
1160
|
+
market,
|
|
1161
|
+
decision
|
|
1162
|
+
}) => {
|
|
1163
|
+
let nextDecision = decision;
|
|
1164
|
+
for (const hook of getProjectHookList(
|
|
1165
|
+
"afterCoreDecision"
|
|
1166
|
+
)) {
|
|
1167
|
+
const result = await invokeHook(
|
|
1168
|
+
"afterCoreDecision",
|
|
1169
|
+
hook,
|
|
1170
|
+
{
|
|
1171
|
+
ctx: hookCtx,
|
|
1172
|
+
market,
|
|
1173
|
+
decision: nextDecision
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
decision: nextDecision,
|
|
1177
|
+
market
|
|
1178
|
+
}
|
|
1179
|
+
);
|
|
1180
|
+
if (isStrategyDecision(result)) {
|
|
1181
|
+
nextDecision = result;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return nextDecision;
|
|
1185
|
+
};
|
|
1186
|
+
const applyProjectAfterBarDecisionHooks = async ({
|
|
1187
|
+
hookCtx,
|
|
1188
|
+
market,
|
|
1189
|
+
decision
|
|
1190
|
+
}) => {
|
|
1191
|
+
let nextDecision = decision;
|
|
1192
|
+
for (const hook of getProjectHookList(
|
|
1193
|
+
"afterBarDecision"
|
|
1194
|
+
)) {
|
|
1195
|
+
const result = await invokeHook(
|
|
1196
|
+
"afterBarDecision",
|
|
1197
|
+
hook,
|
|
1198
|
+
{
|
|
1199
|
+
ctx: hookCtx,
|
|
1200
|
+
market,
|
|
1201
|
+
decision: nextDecision
|
|
1202
|
+
},
|
|
1203
|
+
{
|
|
1204
|
+
decision: nextDecision,
|
|
1205
|
+
market
|
|
1206
|
+
}
|
|
1207
|
+
);
|
|
1208
|
+
if (isStrategyDecision(result)) {
|
|
1209
|
+
nextDecision = result;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return nextDecision;
|
|
1213
|
+
};
|
|
1214
|
+
const applyProjectOnBarHooks = async ({
|
|
1215
|
+
hookCtx,
|
|
1216
|
+
market
|
|
1217
|
+
}) => {
|
|
1218
|
+
for (const hook of getProjectHookList(
|
|
1219
|
+
"onBar"
|
|
1220
|
+
)) {
|
|
1221
|
+
const result = await invokeHook(
|
|
1222
|
+
"onBar",
|
|
1223
|
+
hook,
|
|
1224
|
+
{
|
|
1225
|
+
ctx: hookCtx,
|
|
1226
|
+
market
|
|
1227
|
+
},
|
|
1228
|
+
{
|
|
1229
|
+
market
|
|
1230
|
+
}
|
|
1231
|
+
);
|
|
1232
|
+
if (isStrategyDecision(result)) {
|
|
1233
|
+
return result;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return void 0;
|
|
1237
|
+
};
|
|
757
1238
|
const indicatorsState = createStrategyIndicatorsState({
|
|
758
1239
|
env,
|
|
759
1240
|
data,
|
|
@@ -783,11 +1264,11 @@ var createStrategyRuntime = ({
|
|
|
783
1264
|
connector,
|
|
784
1265
|
data,
|
|
785
1266
|
btcData,
|
|
786
|
-
|
|
1267
|
+
loadPineScriptFile,
|
|
787
1268
|
strategyApi,
|
|
788
1269
|
indicatorsState
|
|
789
1270
|
});
|
|
790
|
-
await
|
|
1271
|
+
await invokeStageHooks("onInit", strategyManifest?.hooks?.onInit, {
|
|
791
1272
|
ctx: getHookCtx(),
|
|
792
1273
|
market: {
|
|
793
1274
|
data,
|
|
@@ -802,13 +1283,64 @@ var createStrategyRuntime = ({
|
|
|
802
1283
|
candle,
|
|
803
1284
|
btcCandle
|
|
804
1285
|
};
|
|
805
|
-
const
|
|
1286
|
+
const onBarHookCtx = getHookCtx();
|
|
1287
|
+
const projectOnBarDecision = await applyProjectOnBarHooks({
|
|
1288
|
+
hookCtx: onBarHookCtx,
|
|
1289
|
+
market
|
|
1290
|
+
});
|
|
1291
|
+
let decision;
|
|
1292
|
+
let shouldInvokeAfterCoreDecisionHook = false;
|
|
1293
|
+
if (projectOnBarDecision) {
|
|
1294
|
+
decision = projectOnBarDecision;
|
|
1295
|
+
} else {
|
|
1296
|
+
const manifestOnBarDecision = await invokeHook(
|
|
1297
|
+
"onBar",
|
|
1298
|
+
strategyManifest?.hooks?.onBar,
|
|
1299
|
+
{
|
|
1300
|
+
ctx: onBarHookCtx,
|
|
1301
|
+
market
|
|
1302
|
+
},
|
|
1303
|
+
{ market }
|
|
1304
|
+
);
|
|
1305
|
+
if (isStrategyDecision(manifestOnBarDecision)) {
|
|
1306
|
+
decision = manifestOnBarDecision;
|
|
1307
|
+
} else {
|
|
1308
|
+
decision = await core(candle, btcCandle);
|
|
1309
|
+
shouldInvokeAfterCoreDecisionHook = true;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
if (shouldInvokeAfterCoreDecisionHook) {
|
|
1313
|
+
const initialDecisionStrategyName = decision.kind === "entry" ? decision.entryContext.strategy : strategyName;
|
|
1314
|
+
decision = await applyProjectAfterCoreDecisionHooks({
|
|
1315
|
+
hookCtx: getHookCtx(initialDecisionStrategyName),
|
|
1316
|
+
market,
|
|
1317
|
+
decision
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
const initialAfterBarDecisionStrategyName = decision.kind === "entry" ? decision.entryContext.strategy : strategyName;
|
|
1321
|
+
decision = await applyProjectAfterBarDecisionHooks({
|
|
1322
|
+
hookCtx: getHookCtx(initialAfterBarDecisionStrategyName),
|
|
1323
|
+
market,
|
|
1324
|
+
decision
|
|
1325
|
+
});
|
|
806
1326
|
const decisionStrategyName = decision.kind === "entry" ? decision.entryContext.strategy : strategyName;
|
|
807
1327
|
const decisionManifest = resolveManifest(decisionStrategyName) ?? strategyManifest;
|
|
808
1328
|
const decisionHookCtx = getHookCtx(decisionStrategyName);
|
|
1329
|
+
if (shouldInvokeAfterCoreDecisionHook) {
|
|
1330
|
+
await invokeHook(
|
|
1331
|
+
"afterCoreDecision",
|
|
1332
|
+
decisionManifest?.hooks?.afterCoreDecision,
|
|
1333
|
+
{
|
|
1334
|
+
ctx: decisionHookCtx,
|
|
1335
|
+
market,
|
|
1336
|
+
decision
|
|
1337
|
+
},
|
|
1338
|
+
{ decision, market }
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
809
1341
|
await invokeHook(
|
|
810
|
-
"
|
|
811
|
-
decisionManifest?.hooks?.
|
|
1342
|
+
"afterBarDecision",
|
|
1343
|
+
decisionManifest?.hooks?.afterBarDecision,
|
|
812
1344
|
{
|
|
813
1345
|
ctx: decisionHookCtx,
|
|
814
1346
|
market,
|
|
@@ -817,7 +1349,7 @@ var createStrategyRuntime = ({
|
|
|
817
1349
|
{ decision, market }
|
|
818
1350
|
);
|
|
819
1351
|
if (decision.kind === "skip") {
|
|
820
|
-
await
|
|
1352
|
+
await invokeStageHooks(
|
|
821
1353
|
"onSkip",
|
|
822
1354
|
decisionManifest?.hooks?.onSkip,
|
|
823
1355
|
{
|
|
@@ -829,12 +1361,13 @@ var createStrategyRuntime = ({
|
|
|
829
1361
|
);
|
|
830
1362
|
return decision.code;
|
|
831
1363
|
}
|
|
832
|
-
const
|
|
1364
|
+
const rawMakeOrdersEnabled = typeof config.MAKE_ORDERS === "boolean" ? config.MAKE_ORDERS : true;
|
|
1365
|
+
const makeOrdersEnabled = rawMakeOrdersEnabled && (env !== "PARITY" || isTestConnector(connector));
|
|
833
1366
|
if (decision.kind === "exit") {
|
|
834
1367
|
if (!makeOrdersEnabled) {
|
|
835
1368
|
return decision.code;
|
|
836
1369
|
}
|
|
837
|
-
const closeGate = await
|
|
1370
|
+
const closeGate = await invokeGateHooks(
|
|
838
1371
|
"beforeClosePosition",
|
|
839
1372
|
decisionManifest?.hooks?.beforeClosePosition,
|
|
840
1373
|
{
|
|
@@ -849,6 +1382,8 @@ var createStrategyRuntime = ({
|
|
|
849
1382
|
}
|
|
850
1383
|
return handleExitDecision({
|
|
851
1384
|
connector,
|
|
1385
|
+
userName: recordRuntimeJournal ? userName : void 0,
|
|
1386
|
+
strategyName,
|
|
852
1387
|
symbol,
|
|
853
1388
|
decision,
|
|
854
1389
|
market,
|
|
@@ -867,6 +1402,30 @@ var createStrategyRuntime = ({
|
|
|
867
1402
|
}
|
|
868
1403
|
});
|
|
869
1404
|
}
|
|
1405
|
+
if (decision.kind === "protect") {
|
|
1406
|
+
if (!makeOrdersEnabled) {
|
|
1407
|
+
return decision.code;
|
|
1408
|
+
}
|
|
1409
|
+
return handleProtectDecision({
|
|
1410
|
+
connector,
|
|
1411
|
+
symbol,
|
|
1412
|
+
decision,
|
|
1413
|
+
market,
|
|
1414
|
+
onRuntimeError: async ({
|
|
1415
|
+
stage,
|
|
1416
|
+
error,
|
|
1417
|
+
decision: protectDecision,
|
|
1418
|
+
market: errorMarket
|
|
1419
|
+
}) => {
|
|
1420
|
+
await notifyRuntimeError({
|
|
1421
|
+
stage,
|
|
1422
|
+
error,
|
|
1423
|
+
decision: protectDecision,
|
|
1424
|
+
market: errorMarket
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
870
1429
|
const runtime = resolveEntryRuntimePolicy({
|
|
871
1430
|
decision,
|
|
872
1431
|
config,
|
|
@@ -900,7 +1459,7 @@ var createStrategyRuntime = ({
|
|
|
900
1459
|
env,
|
|
901
1460
|
ml: runtime.ml
|
|
902
1461
|
});
|
|
903
|
-
await
|
|
1462
|
+
await invokeStageHooks(
|
|
904
1463
|
"afterEnrichMl",
|
|
905
1464
|
decisionManifest?.hooks?.afterEnrichMl,
|
|
906
1465
|
{
|
|
@@ -917,8 +1476,13 @@ var createStrategyRuntime = ({
|
|
|
917
1476
|
let ai;
|
|
918
1477
|
if (signal) {
|
|
919
1478
|
try {
|
|
1479
|
+
await enrichSignalWithDerivativesContext({
|
|
1480
|
+
signal,
|
|
1481
|
+
env
|
|
1482
|
+
});
|
|
920
1483
|
quality = await enrichSignalWithAi({
|
|
921
1484
|
signal,
|
|
1485
|
+
userName,
|
|
922
1486
|
symbol,
|
|
923
1487
|
direction: signal.direction,
|
|
924
1488
|
env,
|
|
@@ -939,7 +1503,7 @@ var createStrategyRuntime = ({
|
|
|
939
1503
|
ai: runtime.ai,
|
|
940
1504
|
quality
|
|
941
1505
|
});
|
|
942
|
-
await
|
|
1506
|
+
await invokeStageHooks(
|
|
943
1507
|
"afterEnrichAi",
|
|
944
1508
|
decisionManifest?.hooks?.afterEnrichAi,
|
|
945
1509
|
{
|
|
@@ -954,6 +1518,7 @@ var createStrategyRuntime = ({
|
|
|
954
1518
|
);
|
|
955
1519
|
}
|
|
956
1520
|
const minAiQuality = runtime.ai?.minQuality ?? 4;
|
|
1521
|
+
const aiEnabled = runtime.ai?.enabled !== false && runtime.ai != null;
|
|
957
1522
|
const policy = buildHookPolicy({
|
|
958
1523
|
quality,
|
|
959
1524
|
makeOrdersEnabled,
|
|
@@ -963,6 +1528,8 @@ var createStrategyRuntime = ({
|
|
|
963
1528
|
makeOrdersEnabled,
|
|
964
1529
|
env,
|
|
965
1530
|
signal,
|
|
1531
|
+
ml,
|
|
1532
|
+
aiEnabled,
|
|
966
1533
|
quality,
|
|
967
1534
|
minAiQuality
|
|
968
1535
|
});
|
|
@@ -972,13 +1539,15 @@ var createStrategyRuntime = ({
|
|
|
972
1539
|
signal.orderSkipReason = getEntrySkipReason({
|
|
973
1540
|
makeOrdersEnabled,
|
|
974
1541
|
env,
|
|
1542
|
+
ml,
|
|
1543
|
+
aiEnabled,
|
|
975
1544
|
quality,
|
|
976
1545
|
minAiQuality
|
|
977
1546
|
});
|
|
978
1547
|
}
|
|
979
1548
|
return signal ?? decision.code;
|
|
980
1549
|
}
|
|
981
|
-
const entryGate = await
|
|
1550
|
+
const entryGate = await invokeGateHooks(
|
|
982
1551
|
"beforeEntryGate",
|
|
983
1552
|
decisionManifest?.hooks?.beforeEntryGate,
|
|
984
1553
|
{
|
|
@@ -1012,18 +1581,108 @@ var createStrategyRuntime = ({
|
|
|
1012
1581
|
policy,
|
|
1013
1582
|
ml,
|
|
1014
1583
|
ai,
|
|
1015
|
-
|
|
1584
|
+
recordRuntimeJournal,
|
|
1585
|
+
invokeStageHooks,
|
|
1016
1586
|
notifyRuntimeError
|
|
1017
1587
|
});
|
|
1018
1588
|
};
|
|
1019
1589
|
};
|
|
1020
1590
|
};
|
|
1021
1591
|
|
|
1022
|
-
// src/
|
|
1592
|
+
// src/strategyHooks/closeOppositePositionsBeforeOpen.ts
|
|
1593
|
+
var import_lodash2 = __toESM(require_lodash());
|
|
1594
|
+
import { logger as logger4 } from "@tradejs/infra/logger";
|
|
1595
|
+
var closeOppositePositionsBeforeOpen = async ({
|
|
1596
|
+
connector,
|
|
1597
|
+
entryContext
|
|
1598
|
+
}) => {
|
|
1599
|
+
const {
|
|
1600
|
+
symbol: currentSymbol,
|
|
1601
|
+
direction: currentDirection,
|
|
1602
|
+
timestamp,
|
|
1603
|
+
prices,
|
|
1604
|
+
strategy: strategyName
|
|
1605
|
+
} = entryContext;
|
|
1606
|
+
const price = prices.currentPrice;
|
|
1607
|
+
try {
|
|
1608
|
+
logger4.log(
|
|
1609
|
+
"info",
|
|
1610
|
+
"[%s] checking open positions before open: %s %s",
|
|
1611
|
+
strategyName,
|
|
1612
|
+
currentSymbol,
|
|
1613
|
+
currentDirection
|
|
1614
|
+
);
|
|
1615
|
+
const positions = await connector.getPositions();
|
|
1616
|
+
const openPositions = (positions || []).filter(
|
|
1617
|
+
(item) => item && Number(item.qty) > 0
|
|
1618
|
+
);
|
|
1619
|
+
logger4.log(
|
|
1620
|
+
"info",
|
|
1621
|
+
"[%s] open positions found: %s",
|
|
1622
|
+
strategyName,
|
|
1623
|
+
openPositions.length
|
|
1624
|
+
);
|
|
1625
|
+
const oppositePositions = openPositions.filter(
|
|
1626
|
+
(item) => item.symbol !== currentSymbol && item.direction !== currentDirection
|
|
1627
|
+
);
|
|
1628
|
+
if (import_lodash2.default.isEmpty(oppositePositions)) {
|
|
1629
|
+
logger4.log(
|
|
1630
|
+
"info",
|
|
1631
|
+
"[%s] no opposite positions to close before open: %s",
|
|
1632
|
+
strategyName,
|
|
1633
|
+
currentSymbol
|
|
1634
|
+
);
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
for (const position of oppositePositions) {
|
|
1638
|
+
logger4.log(
|
|
1639
|
+
"info",
|
|
1640
|
+
"[%s] closing opposite position: %s %s qty=%s",
|
|
1641
|
+
strategyName,
|
|
1642
|
+
position.symbol,
|
|
1643
|
+
position.direction,
|
|
1644
|
+
position.qty
|
|
1645
|
+
);
|
|
1646
|
+
try {
|
|
1647
|
+
await connector.closePosition({
|
|
1648
|
+
symbol: position.symbol,
|
|
1649
|
+
price,
|
|
1650
|
+
timestamp,
|
|
1651
|
+
direction: position.direction
|
|
1652
|
+
});
|
|
1653
|
+
logger4.log(
|
|
1654
|
+
"info",
|
|
1655
|
+
"[%s] opposite position closed: %s",
|
|
1656
|
+
strategyName,
|
|
1657
|
+
position.symbol
|
|
1658
|
+
);
|
|
1659
|
+
} catch (err) {
|
|
1660
|
+
logger4.log(
|
|
1661
|
+
"error",
|
|
1662
|
+
"[%s] failed to close opposite position: %s %s",
|
|
1663
|
+
strategyName,
|
|
1664
|
+
position.symbol,
|
|
1665
|
+
err
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
} catch (err) {
|
|
1670
|
+
logger4.log(
|
|
1671
|
+
"error",
|
|
1672
|
+
"[%s] failed to load open positions before open: %s %s",
|
|
1673
|
+
strategyName,
|
|
1674
|
+
currentSymbol,
|
|
1675
|
+
err
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1023
1679
|
var createCloseOppositeBeforePlaceOrderHook = ({
|
|
1024
1680
|
isEnabled
|
|
1025
1681
|
}) => {
|
|
1026
1682
|
return async ({ ctx, entry }) => {
|
|
1683
|
+
if (ctx.env === "BACKTEST") {
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1027
1686
|
if (!isEnabled(ctx.strategyConfig)) {
|
|
1028
1687
|
return;
|
|
1029
1688
|
}
|
|
@@ -1033,30 +1692,262 @@ var createCloseOppositeBeforePlaceOrderHook = ({
|
|
|
1033
1692
|
});
|
|
1034
1693
|
};
|
|
1035
1694
|
};
|
|
1695
|
+
|
|
1696
|
+
// src/strategyHooks/shared.ts
|
|
1697
|
+
var DEFAULT_BREAK_EVEN_TRIGGER_RISK_MULTIPLIER = 0.5;
|
|
1698
|
+
var DEFAULT_GLOBAL_UNREALIZED_PNL_TRIGGER_RISK_MULTIPLIER = 4;
|
|
1699
|
+
var GLOBAL_UNREALIZED_PNL_CLOSE_ALL_CODE = "GLOBAL_UNREALIZED_PNL_TARGET_REACHED_CLOSE_ALL";
|
|
1700
|
+
var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
1701
|
+
var isOpenPosition = (position) => Boolean(
|
|
1702
|
+
position && isFiniteNumber(position.price) && isFiniteNumber(position.qty) && position.qty > 0 && (position.direction === "LONG" || position.direction === "SHORT")
|
|
1703
|
+
);
|
|
1704
|
+
var isOpenPositionPnlSnapshot = (position) => Boolean(
|
|
1705
|
+
isOpenPosition(position) && isFiniteNumber(position?.currentPrice) && isFiniteNumber(position?.unrealizedPnl)
|
|
1706
|
+
);
|
|
1707
|
+
var getStrategyMaxLossValue = (strategyConfig) => {
|
|
1708
|
+
const maxLossValue = Number(strategyConfig?.MAX_LOSS_VALUE ?? Number.NaN);
|
|
1709
|
+
return Number.isFinite(maxLossValue) && maxLossValue > 0 ? maxLossValue : null;
|
|
1710
|
+
};
|
|
1711
|
+
var getPositionStopLossPrice = (position) => {
|
|
1712
|
+
if (!position || typeof position !== "object") {
|
|
1713
|
+
return null;
|
|
1714
|
+
}
|
|
1715
|
+
const slPrice = Number(
|
|
1716
|
+
position.slPrice ?? Number.NaN
|
|
1717
|
+
);
|
|
1718
|
+
if (Number.isFinite(slPrice)) {
|
|
1719
|
+
return slPrice;
|
|
1720
|
+
}
|
|
1721
|
+
const signalStopLossPrice = Number(
|
|
1722
|
+
position.signal?.prices?.stopLossPrice ?? Number.NaN
|
|
1723
|
+
);
|
|
1724
|
+
return Number.isFinite(signalStopLossPrice) ? signalStopLossPrice : null;
|
|
1725
|
+
};
|
|
1726
|
+
var getFavorableMovePct = ({
|
|
1727
|
+
direction,
|
|
1728
|
+
entryPrice,
|
|
1729
|
+
currentPrice
|
|
1730
|
+
}) => {
|
|
1731
|
+
if (!Number.isFinite(entryPrice) || !Number.isFinite(currentPrice) || entryPrice <= 0) {
|
|
1732
|
+
return null;
|
|
1733
|
+
}
|
|
1734
|
+
return direction === "LONG" ? (currentPrice - entryPrice) / entryPrice * 100 : (entryPrice - currentPrice) / entryPrice * 100;
|
|
1735
|
+
};
|
|
1736
|
+
var getPositionRiskPct = ({
|
|
1737
|
+
direction,
|
|
1738
|
+
entryPrice,
|
|
1739
|
+
stopLossPrice
|
|
1740
|
+
}) => {
|
|
1741
|
+
if (stopLossPrice == null || !Number.isFinite(entryPrice) || !Number.isFinite(stopLossPrice) || entryPrice <= 0) {
|
|
1742
|
+
return null;
|
|
1743
|
+
}
|
|
1744
|
+
return direction === "LONG" ? (entryPrice - stopLossPrice) / entryPrice * 100 : (stopLossPrice - entryPrice) / entryPrice * 100;
|
|
1745
|
+
};
|
|
1746
|
+
var isBreakEvenStopAlreadyApplied = ({
|
|
1747
|
+
direction,
|
|
1748
|
+
entryPrice,
|
|
1749
|
+
stopLossPrice
|
|
1750
|
+
}) => {
|
|
1751
|
+
if (stopLossPrice == null || !Number.isFinite(entryPrice) || !Number.isFinite(stopLossPrice)) {
|
|
1752
|
+
return false;
|
|
1753
|
+
}
|
|
1754
|
+
return direction === "LONG" ? stopLossPrice >= entryPrice : stopLossPrice <= entryPrice;
|
|
1755
|
+
};
|
|
1756
|
+
var getConfiguredDirectionRiskPct = ({
|
|
1757
|
+
strategyConfig,
|
|
1758
|
+
direction
|
|
1759
|
+
}) => {
|
|
1760
|
+
if (!strategyConfig || typeof strategyConfig !== "object") {
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
const directSideConfig = strategyConfig[direction];
|
|
1764
|
+
const directSideRiskPct = Number(directSideConfig?.SL ?? Number.NaN);
|
|
1765
|
+
if (Number.isFinite(directSideRiskPct)) {
|
|
1766
|
+
return directSideRiskPct;
|
|
1767
|
+
}
|
|
1768
|
+
for (const candidate of Object.values(strategyConfig)) {
|
|
1769
|
+
if (!candidate || typeof candidate !== "object") {
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
const candidateDirection = candidate.direction;
|
|
1773
|
+
const candidateRiskPct = Number(
|
|
1774
|
+
candidate.SL ?? Number.NaN
|
|
1775
|
+
);
|
|
1776
|
+
if (candidateDirection === direction && Number.isFinite(candidateRiskPct)) {
|
|
1777
|
+
return candidateRiskPct;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
return null;
|
|
1781
|
+
};
|
|
1782
|
+
var toStrategyCodePrefix = (strategyName) => strategyName === "TrendLine" ? "TRENDLINE" : strategyName.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase();
|
|
1783
|
+
|
|
1784
|
+
// src/strategyHooks/moveStopToBreakEvenAfterCoreDecision.ts
|
|
1785
|
+
var createMoveStopToBreakEvenOnBarHook = ({
|
|
1786
|
+
isEnabled = () => true,
|
|
1787
|
+
triggerRiskMultiplier = DEFAULT_BREAK_EVEN_TRIGGER_RISK_MULTIPLIER
|
|
1788
|
+
} = {}) => {
|
|
1789
|
+
return async ({ ctx, market }) => {
|
|
1790
|
+
if (!isEnabled(ctx.strategyConfig)) {
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
const currentPosition = await ctx.connector.getPosition(ctx.symbol);
|
|
1794
|
+
if (!isOpenPosition(currentPosition)) {
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
const currentPrice = Number(market.candle.close ?? Number.NaN);
|
|
1798
|
+
if (!Number.isFinite(currentPrice)) {
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
const currentStopLossPrice = getPositionStopLossPrice(currentPosition);
|
|
1802
|
+
if (isBreakEvenStopAlreadyApplied({
|
|
1803
|
+
direction: currentPosition.direction,
|
|
1804
|
+
entryPrice: currentPosition.price,
|
|
1805
|
+
stopLossPrice: currentStopLossPrice
|
|
1806
|
+
})) {
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
const favorableMovePct = getFavorableMovePct({
|
|
1810
|
+
direction: currentPosition.direction,
|
|
1811
|
+
entryPrice: currentPosition.price,
|
|
1812
|
+
currentPrice
|
|
1813
|
+
});
|
|
1814
|
+
const currentPositionRiskPct = getPositionRiskPct({
|
|
1815
|
+
direction: currentPosition.direction,
|
|
1816
|
+
entryPrice: currentPosition.price,
|
|
1817
|
+
stopLossPrice: currentStopLossPrice
|
|
1818
|
+
});
|
|
1819
|
+
const configuredRiskPct = getConfiguredDirectionRiskPct({
|
|
1820
|
+
strategyConfig: ctx.strategyConfig,
|
|
1821
|
+
direction: currentPosition.direction
|
|
1822
|
+
});
|
|
1823
|
+
const triggerRiskPct = currentPositionRiskPct ?? configuredRiskPct;
|
|
1824
|
+
if (favorableMovePct == null || triggerRiskPct == null || favorableMovePct < triggerRiskPct * triggerRiskMultiplier) {
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
return {
|
|
1828
|
+
kind: "protect",
|
|
1829
|
+
code: `${toStrategyCodePrefix(ctx.strategyName)}_MOVE_STOP_TO_BREAK_EVEN`,
|
|
1830
|
+
protectPlan: {
|
|
1831
|
+
direction: currentPosition.direction,
|
|
1832
|
+
stopLossPrice: currentPosition.price
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
};
|
|
1836
|
+
};
|
|
1837
|
+
var createMoveStopToBreakEvenAfterCoreDecisionHook = createMoveStopToBreakEvenOnBarHook;
|
|
1838
|
+
|
|
1839
|
+
// src/signalsHooks/closeAllPositionsOnGlobalProfitBeforeSignals.ts
|
|
1840
|
+
import { logger as logger5 } from "@tradejs/infra/logger";
|
|
1841
|
+
var createCloseAllOnGlobalProfitBeforeSignalsHook = ({
|
|
1842
|
+
getStrategyDefaultConfig = () => void 0,
|
|
1843
|
+
profitRiskMultiplier = DEFAULT_GLOBAL_UNREALIZED_PNL_TRIGGER_RISK_MULTIPLIER
|
|
1844
|
+
} = {}) => {
|
|
1845
|
+
return async ({ connector, runtimeStrategies }) => {
|
|
1846
|
+
if (typeof connector.getOpenPositionPnl !== "function") {
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
const openPositions = (await connector.getOpenPositionPnl()).filter(
|
|
1850
|
+
isOpenPositionPnlSnapshot
|
|
1851
|
+
);
|
|
1852
|
+
if (!openPositions.length) {
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
const totalUnrealizedPnl = openPositions.reduce(
|
|
1856
|
+
(sum, position) => sum + position.unrealizedPnl,
|
|
1857
|
+
0
|
|
1858
|
+
);
|
|
1859
|
+
if (!Number.isFinite(totalUnrealizedPnl) || totalUnrealizedPnl <= 0) {
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
const maxLossValues = runtimeStrategies.flatMap(
|
|
1863
|
+
({ strategyName, strategyConfig }) => {
|
|
1864
|
+
const maxLossValue = getStrategyMaxLossValue({
|
|
1865
|
+
...getStrategyDefaultConfig(strategyName) ?? {},
|
|
1866
|
+
...strategyConfig ?? {}
|
|
1867
|
+
});
|
|
1868
|
+
return maxLossValue == null ? [] : [maxLossValue];
|
|
1869
|
+
}
|
|
1870
|
+
);
|
|
1871
|
+
if (!maxLossValues.length) {
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
const averageMaxLossValue = maxLossValues.reduce((sum, value) => sum + value, 0) / maxLossValues.length;
|
|
1875
|
+
const unrealizedPnlThreshold = averageMaxLossValue * profitRiskMultiplier;
|
|
1876
|
+
if (!Number.isFinite(unrealizedPnlThreshold) || unrealizedPnlThreshold <= 0 || totalUnrealizedPnl < unrealizedPnlThreshold) {
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
logger5.info(
|
|
1880
|
+
"closing all positions before signals by global unrealized pnl threshold: totalPnl=%s threshold=%s positions=%s",
|
|
1881
|
+
totalUnrealizedPnl,
|
|
1882
|
+
unrealizedPnlThreshold,
|
|
1883
|
+
openPositions.length
|
|
1884
|
+
);
|
|
1885
|
+
const closeTimestamp = Date.now();
|
|
1886
|
+
const closeResults = await Promise.allSettled(
|
|
1887
|
+
openPositions.map(
|
|
1888
|
+
(position) => connector.closePosition({
|
|
1889
|
+
symbol: position.symbol,
|
|
1890
|
+
direction: position.direction,
|
|
1891
|
+
price: position.currentPrice,
|
|
1892
|
+
timestamp: closeTimestamp
|
|
1893
|
+
})
|
|
1894
|
+
)
|
|
1895
|
+
);
|
|
1896
|
+
const failedClosures = closeResults.flatMap((result, index) => {
|
|
1897
|
+
if (result.status === "fulfilled" && result.value === true) {
|
|
1898
|
+
return [];
|
|
1899
|
+
}
|
|
1900
|
+
return [
|
|
1901
|
+
`${openPositions[index]?.symbol}:${openPositions[index]?.direction ?? "UNKNOWN"}`
|
|
1902
|
+
];
|
|
1903
|
+
});
|
|
1904
|
+
if (failedClosures.length) {
|
|
1905
|
+
logger5.warn(
|
|
1906
|
+
"close-all before signals hook could not confirm closures for %s",
|
|
1907
|
+
failedClosures.join(", ")
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
return {
|
|
1911
|
+
abort: true,
|
|
1912
|
+
reason: GLOBAL_UNREALIZED_PNL_CLOSE_ALL_CODE
|
|
1913
|
+
};
|
|
1914
|
+
};
|
|
1915
|
+
};
|
|
1036
1916
|
export {
|
|
1917
|
+
DEFAULT_AI_MODEL,
|
|
1037
1918
|
MAX_AI_SERIES_POINTS,
|
|
1038
1919
|
askAI,
|
|
1039
1920
|
buildAiHumanPrompt,
|
|
1040
1921
|
buildAiPayload,
|
|
1922
|
+
buildAiPrompts,
|
|
1041
1923
|
buildAiSystemPrompt,
|
|
1042
1924
|
closeOppositePositionsBeforeOpen,
|
|
1925
|
+
createCloseAllOnGlobalProfitBeforeSignalsHook,
|
|
1043
1926
|
createCloseOppositeBeforePlaceOrderHook,
|
|
1927
|
+
createMoveStopToBreakEvenAfterCoreDecisionHook,
|
|
1928
|
+
createMoveStopToBreakEvenOnBarHook,
|
|
1044
1929
|
createStrategyRuntime,
|
|
1045
1930
|
enrichSignalWithAi,
|
|
1046
1931
|
enrichSignalWithMl,
|
|
1047
1932
|
enrichSignalWithMlAi,
|
|
1933
|
+
ensureAiStrategyPluginsLoaded,
|
|
1048
1934
|
ensureIndicatorPluginsLoaded,
|
|
1049
1935
|
ensureStrategyPluginsLoaded,
|
|
1050
1936
|
executeEntryOrder,
|
|
1051
1937
|
getAvailableStrategyNames,
|
|
1938
|
+
getDeterministicAiGateContext,
|
|
1939
|
+
getOpenRouterModelKwargs,
|
|
1052
1940
|
getRegisteredManifests,
|
|
1053
1941
|
getRegisteredStrategies,
|
|
1054
1942
|
getStrategyCreator,
|
|
1055
1943
|
getStrategyManifest,
|
|
1056
1944
|
isKnownStrategy,
|
|
1057
1945
|
registerStrategyEntries,
|
|
1946
|
+
resetAiRuntimeCache,
|
|
1058
1947
|
resetStrategyRegistryCache,
|
|
1059
1948
|
resolveStrategyConfig,
|
|
1949
|
+
runAiPrompt,
|
|
1950
|
+
runAiPromptLocal,
|
|
1060
1951
|
strategies,
|
|
1061
1952
|
trimSeriesDeep
|
|
1062
1953
|
};
|