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