@tradejs/node 3.1.2 → 3.1.4
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 +24 -88
- package/dist/runtimeStrategies.mjs +24 -88
- 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 = [
|
|
@@ -55,13 +53,17 @@ var validateReleaseRuntimeCompatibility = async ({
|
|
|
55
53
|
release.strategyPackage,
|
|
56
54
|
packageManifest
|
|
57
55
|
);
|
|
58
|
-
if (release.strategyPackageVersion
|
|
56
|
+
if (release.strategyPackageVersion !== installedStrategyVersion) {
|
|
59
57
|
throw new Error(
|
|
60
58
|
`${release.strategyName} v${release.releaseVersion} requires ${release.strategyPackage}@${release.strategyPackageVersion}, image has ${installedStrategyVersion}`
|
|
61
59
|
);
|
|
62
60
|
}
|
|
63
|
-
const installedRuntimeVersion =
|
|
64
|
-
|
|
61
|
+
const installedRuntimeVersion = await resolveInstalledPackageVersion(
|
|
62
|
+
projectRoot,
|
|
63
|
+
"@tradejs/node",
|
|
64
|
+
packageManifest
|
|
65
|
+
);
|
|
66
|
+
if (release.runtimePackageVersion !== installedRuntimeVersion) {
|
|
65
67
|
throw new Error(
|
|
66
68
|
`${release.strategyName} v${release.releaseVersion} requires @tradejs/node@${release.runtimePackageVersion}, image has ${installedRuntimeVersion}`
|
|
67
69
|
);
|
|
@@ -70,24 +72,20 @@ var validateReleaseRuntimeCompatibility = async ({
|
|
|
70
72
|
var resolveAccountId = async ({
|
|
71
73
|
userName,
|
|
72
74
|
deployment,
|
|
73
|
-
|
|
74
|
-
universe,
|
|
75
|
-
legacyAccountId
|
|
75
|
+
universe
|
|
76
76
|
}) => {
|
|
77
|
-
const requestedAccountId = deployment?.accountId ?? legacyAccountId;
|
|
78
77
|
const account = await resolveTradingAccount({
|
|
79
78
|
userName,
|
|
80
|
-
accountId:
|
|
81
|
-
provider: deployment
|
|
79
|
+
accountId: deployment.accountId,
|
|
80
|
+
provider: deployment.provider,
|
|
82
81
|
universe
|
|
83
82
|
});
|
|
84
|
-
return account?.id ??
|
|
83
|
+
return account?.id ?? deployment.accountId;
|
|
85
84
|
};
|
|
86
85
|
var loadVersionedRuntimeStrategies = async ({
|
|
87
86
|
userName,
|
|
88
87
|
projectRoot,
|
|
89
|
-
deployment
|
|
90
|
-
connectorName
|
|
88
|
+
deployment
|
|
91
89
|
}) => {
|
|
92
90
|
const packageManifest = await readPackageManifest(projectRoot);
|
|
93
91
|
return Promise.all(
|
|
@@ -97,9 +95,14 @@ var loadVersionedRuntimeStrategies = async ({
|
|
|
97
95
|
`Deployment ${deployment.id} strategy ${reference.strategyName} has no releaseVersion`
|
|
98
96
|
);
|
|
99
97
|
}
|
|
100
|
-
if (
|
|
98
|
+
if (Object.keys(reference).some(
|
|
99
|
+
(key) => !["strategyName", "releaseVersion", "controlState"].includes(key)
|
|
100
|
+
)) {
|
|
101
|
+
throw new Error(`Deployment ${deployment.id} has invalid fields`);
|
|
102
|
+
}
|
|
103
|
+
if (!reference.controlState) {
|
|
101
104
|
throw new Error(
|
|
102
|
-
`Deployment ${deployment.id}
|
|
105
|
+
`Deployment ${deployment.id} strategy ${reference.strategyName} has no controlState`
|
|
103
106
|
);
|
|
104
107
|
}
|
|
105
108
|
const release = await getRuntimeStrategyRelease(
|
|
@@ -129,13 +132,12 @@ var loadVersionedRuntimeStrategies = async ({
|
|
|
129
132
|
const accountId = await resolveAccountId({
|
|
130
133
|
userName,
|
|
131
134
|
deployment,
|
|
132
|
-
connectorName,
|
|
133
135
|
universe
|
|
134
136
|
});
|
|
135
137
|
return {
|
|
136
138
|
strategyName: reference.strategyName,
|
|
137
139
|
releaseVersion: release.releaseVersion,
|
|
138
|
-
controlState: reference.controlState
|
|
140
|
+
controlState: reference.controlState,
|
|
139
141
|
interval,
|
|
140
142
|
universe,
|
|
141
143
|
accountId,
|
|
@@ -144,90 +146,24 @@ var loadVersionedRuntimeStrategies = async ({
|
|
|
144
146
|
runtimePackageVersion: release.runtimePackageVersion,
|
|
145
147
|
strategyCreator,
|
|
146
148
|
sourceStrategyConfig: release.config,
|
|
147
|
-
strategyConfig: release.config
|
|
148
|
-
// Symbol result configs are mutable legacy overlays and are not read by v2.
|
|
149
|
-
strategyResults: {}
|
|
149
|
+
strategyConfig: release.config
|
|
150
150
|
};
|
|
151
151
|
})
|
|
152
152
|
);
|
|
153
153
|
};
|
|
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
154
|
var loadResolvedRuntimeStrategies = async ({
|
|
205
155
|
userName,
|
|
206
156
|
projectRoot,
|
|
207
157
|
deployment,
|
|
208
|
-
connectorName = "bybit",
|
|
209
158
|
universe,
|
|
210
159
|
accountId,
|
|
211
160
|
interval
|
|
212
161
|
}) => {
|
|
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({
|
|
222
|
-
userName,
|
|
223
|
-
projectRoot,
|
|
224
|
-
deployment,
|
|
225
|
-
connectorName
|
|
226
|
-
}) : await loadLegacyRuntimeStrategies({
|
|
162
|
+
if (!deployment) throw new Error("Runtime deployment is required");
|
|
163
|
+
const strategies = await loadVersionedRuntimeStrategies({
|
|
227
164
|
userName,
|
|
228
165
|
projectRoot,
|
|
229
|
-
deployment
|
|
230
|
-
connectorName
|
|
166
|
+
deployment
|
|
231
167
|
});
|
|
232
168
|
const filtered = strategies.filter(
|
|
233
169
|
(candidate) => (!universe || candidate.universe === universe) && (!interval || String(candidate.interval) === String(interval)) && (!accountId || candidate.accountId === accountId)
|
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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tradejs/node",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.4",
|
|
4
4
|
"description": "Node-only runtime for the TradeJS TypeScript framework: strategies, backtests, Pine strategy loading, and plugin registries.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tradejs",
|
|
@@ -85,9 +85,9 @@
|
|
|
85
85
|
"dependencies": {
|
|
86
86
|
"@langchain/core": "^1.2.3",
|
|
87
87
|
"@langchain/openai": "^1.5.5",
|
|
88
|
-
"@tradejs/core": "^3.1.
|
|
89
|
-
"@tradejs/infra": "^3.1.
|
|
90
|
-
"@tradejs/types": "^3.1.
|
|
88
|
+
"@tradejs/core": "^3.1.4",
|
|
89
|
+
"@tradejs/infra": "^3.1.4",
|
|
90
|
+
"@tradejs/types": "^3.1.4",
|
|
91
91
|
"chalk": "4.1.2",
|
|
92
92
|
"ioredis": "5.11.1",
|
|
93
93
|
"lodash": "^4.18.1",
|