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