@tradejs/node 3.1.27 → 3.1.28-beta.252

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.mjs CHANGED
@@ -5,11 +5,11 @@ import {
5
5
  import {
6
6
  buildMlPayload,
7
7
  enrichSignalWithMarketContextStages
8
- } from "./chunk-XHNJ757K.mjs";
8
+ } from "./chunk-VQSGH7YA.mjs";
9
9
  import {
10
10
  buildAiPayload,
11
11
  getStrategyCreator
12
- } from "./chunk-IKRSNRP6.mjs";
12
+ } from "./chunk-UITD6LG6.mjs";
13
13
  import {
14
14
  getTradejsProjectCwd
15
15
  } from "./chunk-LZDXRXIU.mjs";
@@ -159,6 +159,10 @@ import {
159
159
  BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER,
160
160
  FEE_PERCENT
161
161
  } from "@tradejs/core/constants";
162
+ import {
163
+ assertStrategyExecutionIsolation,
164
+ parseBacktestExecutionCosts
165
+ } from "@tradejs/core/backtest";
162
166
  var finiteOr = (value, fallback) => {
163
167
  const parsed = Number(value);
164
168
  return Number.isFinite(parsed) ? parsed : fallback;
@@ -195,20 +199,27 @@ var loadFundingRates = (connector, symbol, startTime, endTime) => {
195
199
  };
196
200
  var resolveExecutionCosts = async (params) => {
197
201
  const { connector, symbol, config, startTime, endTime, instrument } = params;
198
- const cacheOnly = config.EXECUTION_COSTS_CACHE_ONLY === true;
199
- const hasConfiguredFees = Number.isFinite(Number(config.MAKER_FEE_RATE)) && Number.isFinite(Number(config.TAKER_FEE_RATE));
202
+ assertStrategyExecutionIsolation(config);
203
+ const costs = params.executionCosts == null ? void 0 : parseBacktestExecutionCosts(params.executionCosts);
204
+ const cacheOnly = params.cacheOnly === true;
205
+ const hasConfiguredFees = costs != null;
200
206
  const exchangeFees = !cacheOnly && !hasConfiguredFees && connector.getTradingFeeRate ? await loadTradingFee(connector, symbol).catch(() => null) : null;
201
- const makerRate = hasConfiguredFees ? Number(config.MAKER_FEE_RATE) : exchangeFees?.makerRate ?? FEE_PERCENT;
202
- const takerRate = hasConfiguredFees ? Number(config.TAKER_FEE_RATE) : exchangeFees?.takerRate ?? FEE_PERCENT;
203
- const fundingEnabled = config.FUNDING_ENABLED !== false && !cacheOnly && typeof connector.getFundingRateHistory === "function";
207
+ const makerRate = hasConfiguredFees ? costs.fees.makerRate : exchangeFees?.makerRate ?? FEE_PERCENT;
208
+ const takerRate = hasConfiguredFees ? costs.fees.takerRate : exchangeFees?.takerRate ?? FEE_PERCENT;
209
+ const fundingEnabled = costs?.funding.enabled !== false && !cacheOnly && typeof connector.getFundingRateHistory === "function";
204
210
  const fundingRates = fundingEnabled ? await loadFundingRates(connector, symbol, startTime, endTime) : [];
205
211
  const requestedLeverage = Math.max(1, finiteOr(config.LEVERAGE, 10));
206
212
  const venueMaxLeverage = Number(instrument?.venueMetadata?.maxLeverage);
207
213
  const maxAllowed = Number.isFinite(venueMaxLeverage) ? venueMaxLeverage : null;
208
214
  const effectiveLeverage = maxAllowed == null ? requestedLeverage : Math.min(requestedLeverage, maxAllowed);
209
215
  const feeSource = hasConfiguredFees ? "config" : exchangeFees?.source ?? "fallback";
210
- const fundingSource = !fundingEnabled ? cacheOnly ? "fallback" : "disabled" : fundingRates.length ? "historical" : "unavailable";
211
- const usesFallback = feeSource === "fallback" || fundingEnabled && fundingSource === "unavailable" || config.SLIPPAGE_BASE_BPS == null && config.SLIPPAGE_SPREAD_MULTIPLIER == null && config.SLIPPAGE_MARKET_IMPACT_BPS == null;
216
+ const fundingSource = !fundingEnabled ? costs?.funding.enabled === false ? "disabled" : cacheOnly ? "fallback" : "disabled" : fundingRates.length ? "historical" : "unavailable";
217
+ const usesFallback = feeSource === "fallback" || fundingEnabled && fundingSource === "unavailable" || !costs;
218
+ if (costs?.funding.enabled && (!fundingEnabled || fundingSource !== "historical")) {
219
+ throw new Error(
220
+ "Requested funding history is unavailable; explicitly disable funding or provide historical data"
221
+ );
222
+ }
212
223
  return {
213
224
  model: {
214
225
  fees: { makerRate, takerRate, source: feeSource },
@@ -220,20 +231,20 @@ var resolveExecutionCosts = async (params) => {
220
231
  toTimestamp: fundingRates.at(-1)?.timestamp ?? null
221
232
  },
222
233
  slippage: {
223
- baseBps: finiteOr(config.SLIPPAGE_BASE_BPS, BACKTEST_BASE_SLIPPAGE_BPS),
234
+ baseBps: costs?.slippage.baseBps ?? BACKTEST_BASE_SLIPPAGE_BPS,
224
235
  spreadMultiplier: finiteOr(
225
- config.SLIPPAGE_SPREAD_MULTIPLIER,
236
+ costs?.slippage.spreadMultiplier,
226
237
  BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER
227
238
  ),
228
239
  marketImpactBps: finiteOr(
229
- config.SLIPPAGE_MARKET_IMPACT_BPS,
240
+ costs?.slippage.marketImpactBps,
230
241
  BACKTEST_MARKET_IMPACT_BPS
231
242
  ),
232
243
  delayRiskMultiplier: finiteOr(
233
- config.SLIPPAGE_DELAY_RISK_MULTIPLIER,
244
+ costs?.slippage.delayRiskMultiplier,
234
245
  BACKTEST_DELAY_RISK_MULTIPLIER
235
246
  ),
236
- source: config.SLIPPAGE_BASE_BPS != null || config.SLIPPAGE_SPREAD_MULTIPLIER != null || config.SLIPPAGE_MARKET_IMPACT_BPS != null ? "config" : "fallback"
247
+ source: costs != null ? "config" : "fallback"
237
248
  },
238
249
  leverage: {
239
250
  requested: requestedLeverage,
@@ -555,7 +566,7 @@ var createTestConnector = (connector, context) => {
555
566
  allocatedQty += legExitQty;
556
567
  const legResult = currentEntryLegResults[leg.index];
557
568
  const legGrossProfit = currentPosition.direction === "LONG" ? (executionPrice - legResult.entryPrice) * legExitQty : (legResult.entryPrice - executionPrice) * legExitQty;
558
- const legFee = executionPrice * legExitQty * takerFeeRate;
569
+ const legFee = fee * (legExitQty / qty);
559
570
  currentEntryLegResults[leg.index] = appendExitToTradeResult({
560
571
  tradeResult: legResult,
561
572
  direction: currentPosition.direction,
@@ -1239,7 +1250,8 @@ var createTestConnector = (connector, context) => {
1239
1250
  const { fee, profit } = getNetProfit({
1240
1251
  grossProfit,
1241
1252
  price: executionPrice,
1242
- qty: currentPosition.qty
1253
+ qty: currentPosition.qty,
1254
+ feeRate: order.isLimit ? makerFeeRate : takerFeeRate
1243
1255
  });
1244
1256
  recordExitResult({
1245
1257
  timestamp: order.timestamp,
@@ -1384,6 +1396,8 @@ var createBacktestSession = async ({
1384
1396
  connector,
1385
1397
  symbol: test.symbol,
1386
1398
  config: test.strategyConfig,
1399
+ executionCosts: test.executionCosts,
1400
+ cacheOnly: test.executionCostsCacheOnly,
1387
1401
  startTime: start,
1388
1402
  endTime: test.options.end,
1389
1403
  instrument
@@ -2,7 +2,7 @@ import {
2
2
  getStrategyCreator,
3
3
  getStrategyEntry,
4
4
  getStrategyPluginSource
5
- } from "./chunk-IKRSNRP6.mjs";
5
+ } from "./chunk-UITD6LG6.mjs";
6
6
  import {
7
7
  loadTradejsConfig
8
8
  } from "./chunk-LZDXRXIU.mjs";
@@ -40,7 +40,13 @@ var DEPLOYMENT_KEYS = /* @__PURE__ */ new Set([
40
40
  "assetClasses",
41
41
  "tickers"
42
42
  ]);
43
- var STRATEGY_KEYS = /* @__PURE__ */ new Set(["generation", "enabled", "selection", "config"]);
43
+ var STRATEGY_KEYS = /* @__PURE__ */ new Set([
44
+ "module",
45
+ "generation",
46
+ "enabled",
47
+ "selection",
48
+ "config"
49
+ ]);
44
50
  var SELECTION_KEYS = /* @__PURE__ */ new Set(["tickers"]);
45
51
  var FORBIDDEN_CONFIG_KEYS = /* @__PURE__ */ new Set([
46
52
  "ACCOUNT_ID",
@@ -112,6 +118,39 @@ var readRuntimePackageManifest = async (projectRoot) => {
112
118
  }
113
119
  return value;
114
120
  };
121
+ var parseNpmAliasReference = (value) => {
122
+ if (typeof value !== "string" || !value.startsWith("npm:")) return null;
123
+ const target = value.slice("npm:".length);
124
+ const versionSeparator = target.lastIndexOf("@");
125
+ if (versionSeparator <= 0 || versionSeparator === target.length - 1) {
126
+ return null;
127
+ }
128
+ return {
129
+ packageName: target.slice(0, versionSeparator),
130
+ version: target.slice(versionSeparator + 1)
131
+ };
132
+ };
133
+ var verifyInstalledPackageName = async ({
134
+ projectRoot,
135
+ packageName,
136
+ installedName,
137
+ installedVersion,
138
+ projectPackage
139
+ }) => {
140
+ if (installedName === void 0 || installedName === packageName) return;
141
+ if (projectPackage || typeof installedName !== "string") {
142
+ throw new Error("name is invalid");
143
+ }
144
+ const projectManifest = JSON.parse(
145
+ await readFile(path.join(projectRoot, "package.json"), "utf8")
146
+ );
147
+ const alias = parseNpmAliasReference(
148
+ projectManifest.dependencies?.[packageName]
149
+ );
150
+ if (alias?.packageName !== installedName || alias.version !== installedVersion) {
151
+ throw new Error("name or alias target is invalid");
152
+ }
153
+ };
115
154
  var readInstalledPackageMetadata = async ({
116
155
  projectRoot,
117
156
  packageName,
@@ -125,9 +164,16 @@ var readInstalledPackageMetadata = async ({
125
164
  );
126
165
  try {
127
166
  const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
128
- if (packageJson.name !== void 0 && packageJson.name !== packageName || typeof packageJson.version !== "string" || !packageJson.version.trim()) {
129
- throw new Error("name or version is invalid");
167
+ if (typeof packageJson.version !== "string" || !packageJson.version.trim()) {
168
+ throw new Error("version is invalid");
130
169
  }
170
+ await verifyInstalledPackageName({
171
+ projectRoot,
172
+ packageName,
173
+ installedName: packageJson.name,
174
+ installedVersion: packageJson.version,
175
+ projectPackage
176
+ });
131
177
  const dependencyNames = [
132
178
  ...isRecord(packageJson.dependencies) ? Object.keys(packageJson.dependencies) : [],
133
179
  ...isRecord(packageJson.peerDependencies) ? Object.keys(packageJson.peerDependencies) : []
@@ -222,7 +268,7 @@ var verifyDeploymentDeclaration = (deploymentId, value) => {
222
268
  for (const [strategyName, strategyValue] of Object.entries(
223
269
  value.strategies
224
270
  )) {
225
- if (!strategyName.trim() || !isRecord(strategyValue) || Object.keys(strategyValue).some((key) => !STRATEGY_KEYS.has(key)) || strategyValue.generation !== void 0 && (typeof strategyValue.generation !== "string" || !strategyValue.generation.trim()) || typeof strategyValue.enabled !== "boolean" || !verifyStrategySelection(strategyValue.selection) || !isRecord(strategyValue.config) || Object.keys(strategyValue.config).some(
271
+ if (!strategyName.trim() || !isRecord(strategyValue) || Object.keys(strategyValue).some((key) => !STRATEGY_KEYS.has(key)) || strategyValue.generation !== void 0 && (typeof strategyValue.generation !== "string" || !strategyValue.generation.trim()) || strategyValue.module !== void 0 && (typeof strategyValue.module !== "string" || !strategyValue.module.trim()) || typeof strategyValue.enabled !== "boolean" || !verifyStrategySelection(strategyValue.selection) || !isRecord(strategyValue.config) || Object.keys(strategyValue.config).some(
226
272
  (key) => FORBIDDEN_CONFIG_KEYS.has(key)
227
273
  ) || !INTERVALS.has(String(strategyValue.config.INTERVAL)) || !["crypto", "tradfi"].includes(String(strategyValue.config.UNIVERSE))) {
228
274
  throw new Error(
@@ -256,9 +302,9 @@ var resolveStrategyComposition = async ({
256
302
  packageManifest
257
303
  }) => {
258
304
  const [strategyEntry, strategyCreator, pluginSource] = await Promise.all([
259
- getStrategyEntry(strategyName, projectRoot),
260
- getStrategyCreator(strategyName, projectRoot),
261
- getStrategyPluginSource(strategyName, projectRoot)
305
+ getStrategyEntry(strategyName, projectRoot, declaration.module),
306
+ getStrategyCreator(strategyName, projectRoot, declaration.module),
307
+ getStrategyPluginSource(strategyName, projectRoot, declaration.module)
262
308
  ]);
263
309
  if (!strategyEntry || !strategyCreator) {
264
310
  throw new Error(`Unknown strategy: ${strategyName}`);
@@ -308,6 +354,7 @@ var resolveStrategyComposition = async ({
308
354
  });
309
355
  return {
310
356
  strategyName,
357
+ strategyModule: pluginSource ?? strategyPackage.name,
311
358
  strategyRevision: computeStrategyRevision({
312
359
  strategyName,
313
360
  strategyPackage: strategyPackage.name,
@@ -496,11 +543,12 @@ var loadResolvedRuntimeStrategies = async ({
496
543
  };
497
544
  var getRuntimeStrategyPackageMetadata = async ({
498
545
  strategyName,
499
- projectRoot
546
+ projectRoot,
547
+ strategyModule
500
548
  }) => {
501
549
  const [packageManifest, pluginSource] = await Promise.all([
502
550
  readRuntimePackageManifest(projectRoot),
503
- getStrategyPluginSource(strategyName, projectRoot)
551
+ getStrategyPluginSource(strategyName, projectRoot, strategyModule)
504
552
  ]);
505
553
  const strategyPackage = await resolveStrategyPackage({
506
554
  pluginSource: pluginSource ?? null,
@@ -164,7 +164,8 @@ var buildExchangeFallbackRuntimeTrades = ({
164
164
  openPositions,
165
165
  strategyNames,
166
166
  existingTrades,
167
- endTime
167
+ endTime,
168
+ scope
168
169
  }) => {
169
170
  if (!entryRows.length && !closedPnlRows.length) return [];
170
171
  const strategyNameByOrderId = new Map(
@@ -219,6 +220,7 @@ var buildExchangeFallbackRuntimeTrades = ({
219
220
  const entryPrice = typeof entry.entryPrice === "number" && Number.isFinite(entry.entryPrice) ? entry.entryPrice : typeof closed?.entryPrice === "number" && Number.isFinite(closed.entryPrice) ? closed.entryPrice : null;
220
221
  if (entryPrice == null) return null;
221
222
  return {
223
+ ...scope,
222
224
  orderId: runtimeOrderId,
223
225
  strategy,
224
226
  symbol: entry.symbol,
@@ -261,6 +263,7 @@ var buildExchangeFallbackRuntimeTrades = ({
261
263
  return null;
262
264
  }
263
265
  return {
266
+ ...scope,
264
267
  orderId: runtimeOrderId,
265
268
  strategy,
266
269
  symbol: row.symbol,
@@ -599,6 +599,7 @@ var createStrategyRegistryState = () => ({
599
599
  strategyManifestsMap: /* @__PURE__ */ new Map(),
600
600
  strategyEntriesMap: /* @__PURE__ */ new Map(),
601
601
  strategySourcesMap: /* @__PURE__ */ new Map(),
602
+ strategyModuleResolutions: /* @__PURE__ */ new Map(),
602
603
  pluginsLoadPromise: null
603
604
  });
604
605
  var registryStateByProjectRoot = sharedStrategyRegistry.registryStateByProjectRoot;
@@ -660,7 +661,7 @@ var validateStrategyEntries = (moduleName, entries, state) => {
660
661
  const strategyName = entry?.manifest?.name;
661
662
  if (typeof strategyName !== "string" || !strategyName.trim()) {
662
663
  issues.push(`${entryPath}: manifest.name is required`);
663
- } else if (names.has(strategyName) || state.strategyEntriesMap.has(strategyName)) {
664
+ } else if (names.has(strategyName) || state?.strategyEntriesMap.has(strategyName)) {
664
665
  issues.push(`${entryPath}: duplicate strategy ${strategyName}`);
665
666
  } else {
666
667
  names.add(strategyName);
@@ -722,6 +723,72 @@ var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()
722
723
  moduleName
723
724
  );
724
725
  };
726
+ var resolveStrategyModuleEntry = async ({
727
+ strategyName,
728
+ moduleName,
729
+ cwd
730
+ }) => {
731
+ const { projectRoot, state } = getStrategyRegistryState(cwd);
732
+ const source = moduleName.trim();
733
+ const resolvedModuleName = resolvePluginModuleSpecifier(source, projectRoot);
734
+ const cacheKey = `${resolvedModuleName}\0${strategyName}`;
735
+ const cached = state.strategyModuleResolutions.get(cacheKey);
736
+ if (cached) return cached;
737
+ const resolution = (async () => {
738
+ const moduleExport = await importStrategyPluginModule(
739
+ resolvedModuleName,
740
+ projectRoot
741
+ );
742
+ const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
743
+ if (!pluginDefinition) {
744
+ throw new Error(`${source}: export { strategyEntries } is missing`);
745
+ }
746
+ const issues = validateStrategyEntries(
747
+ source,
748
+ pluginDefinition.strategyEntries
749
+ );
750
+ if (issues.length > 0) {
751
+ throw new Error(
752
+ ["Invalid TradeJS strategy module:", ...issues].join("\n")
753
+ );
754
+ }
755
+ const entry = pluginDefinition.strategyEntries.find(
756
+ (candidate) => candidate.manifest.name === strategyName
757
+ );
758
+ if (!entry) {
759
+ throw new Error(`${source}: strategy ${strategyName} is missing`);
760
+ }
761
+ const factory = sharedStrategyRegistry.strategyRuntimeFactory;
762
+ if (!factory) {
763
+ throw new Error("Strategy runtime factory is not configured");
764
+ }
765
+ const manifestsByName = new Map(
766
+ pluginDefinition.strategyEntries.map(
767
+ (candidate) => [candidate.manifest.name, candidate.manifest]
768
+ )
769
+ );
770
+ return {
771
+ entry,
772
+ creator: factory({
773
+ strategyName,
774
+ defaults: entry.defaults,
775
+ createCore: entry.createCore,
776
+ manifest: entry.manifest,
777
+ detectorKey: entry.detectorKey,
778
+ detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
779
+ resolveRegisteredManifest: (name) => manifestsByName.get(name) ?? state.strategyManifestsMap.get(name)
780
+ }),
781
+ source
782
+ };
783
+ })();
784
+ state.strategyModuleResolutions.set(cacheKey, resolution);
785
+ try {
786
+ return await resolution;
787
+ } catch (error) {
788
+ state.strategyModuleResolutions.delete(cacheKey);
789
+ throw error;
790
+ }
791
+ };
725
792
  var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
726
793
  const { projectRoot, state } = getStrategyRegistryState(cwd);
727
794
  if (!state.pluginsLoadPromise) {
@@ -800,7 +867,11 @@ var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
800
867
  await state.pluginsLoadPromise;
801
868
  };
802
869
  var ensureIndicatorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => ensureStrategyPluginsLoaded(cwd);
803
- var getStrategyCreator = async (name, cwd = getTradejsProjectCwd()) => {
870
+ var getStrategyCreator = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
871
+ if (moduleName?.trim()) {
872
+ await ensureStrategyPluginsLoaded(cwd);
873
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).creator;
874
+ }
804
875
  await ensureStrategyPluginsLoaded(cwd);
805
876
  const { state } = getStrategyRegistryState(cwd);
806
877
  return state.strategyCreators.get(name);
@@ -810,12 +881,20 @@ var getStrategyDefaults = async (name, cwd = getTradejsProjectCwd()) => {
810
881
  const { state } = getStrategyRegistryState(cwd);
811
882
  return state.strategyEntriesMap.get(name)?.defaults;
812
883
  };
813
- var getStrategyEntry = async (name, cwd = getTradejsProjectCwd()) => {
884
+ var getStrategyEntry = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
885
+ if (moduleName?.trim()) {
886
+ await ensureStrategyPluginsLoaded(cwd);
887
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).entry;
888
+ }
814
889
  await ensureStrategyPluginsLoaded(cwd);
815
890
  const { state } = getStrategyRegistryState(cwd);
816
891
  return state.strategyEntriesMap.get(name);
817
892
  };
818
- var getStrategyPluginSource = async (name, cwd = getTradejsProjectCwd()) => {
893
+ var getStrategyPluginSource = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
894
+ if (moduleName?.trim()) {
895
+ await ensureStrategyPluginsLoaded(cwd);
896
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).source;
897
+ }
819
898
  await ensureStrategyPluginsLoaded(cwd);
820
899
  const { state } = getStrategyRegistryState(cwd);
821
900
  return state.strategySourcesMap.get(name);