@tradejs/node 3.1.27 → 3.1.28-beta.251

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ai.js CHANGED
@@ -652,6 +652,7 @@ var createStrategyRegistryState = () => ({
652
652
  strategyManifestsMap: /* @__PURE__ */ new Map(),
653
653
  strategyEntriesMap: /* @__PURE__ */ new Map(),
654
654
  strategySourcesMap: /* @__PURE__ */ new Map(),
655
+ strategyModuleResolutions: /* @__PURE__ */ new Map(),
655
656
  pluginsLoadPromise: null
656
657
  });
657
658
  var registryStateByProjectRoot = sharedStrategyRegistry.registryStateByProjectRoot;
package/dist/ai.mjs CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  runAiPrompt,
15
15
  runAiPromptLocal,
16
16
  trimSeriesDeep
17
- } from "./chunk-IKRSNRP6.mjs";
17
+ } from "./chunk-UITD6LG6.mjs";
18
18
  import "./chunk-LZDXRXIU.mjs";
19
19
  import "./chunk-Y6FXYEAI.mjs";
20
20
  export {
@@ -1,5 +1,5 @@
1
1
  export * from '@tradejs/core/backtest';
2
- import { Test, TestingBox, TestingBoxResult, TestConnectorCreator, Connector, StrategyConfig, InstrumentDescriptor, ExecutionCostModel, FundingRatePoint } from '@tradejs/types';
2
+ import { Test, TestingBox, TestingBoxResult, TestConnectorCreator, Connector, StrategyConfig, BacktestExecutionCosts, InstrumentDescriptor, ExecutionCostModel, FundingRatePoint } from '@tradejs/types';
3
3
 
4
4
  type TestingGroupResult = {
5
5
  test: Test;
@@ -22,6 +22,8 @@ declare const resolveExecutionCosts: (params: {
22
22
  connector: Connector;
23
23
  symbol: string;
24
24
  config: StrategyConfig;
25
+ executionCosts?: BacktestExecutionCosts;
26
+ cacheOnly?: boolean;
25
27
  startTime: number;
26
28
  endTime: number;
27
29
  instrument?: InstrumentDescriptor;
@@ -1,5 +1,5 @@
1
1
  export * from '@tradejs/core/backtest';
2
- import { Test, TestingBox, TestingBoxResult, TestConnectorCreator, Connector, StrategyConfig, InstrumentDescriptor, ExecutionCostModel, FundingRatePoint } from '@tradejs/types';
2
+ import { Test, TestingBox, TestingBoxResult, TestConnectorCreator, Connector, StrategyConfig, BacktestExecutionCosts, InstrumentDescriptor, ExecutionCostModel, FundingRatePoint } from '@tradejs/types';
3
3
 
4
4
  type TestingGroupResult = {
5
5
  test: Test;
@@ -22,6 +22,8 @@ declare const resolveExecutionCosts: (params: {
22
22
  connector: Connector;
23
23
  symbol: string;
24
24
  config: StrategyConfig;
25
+ executionCosts?: BacktestExecutionCosts;
26
+ cacheOnly?: boolean;
25
27
  startTime: number;
26
28
  endTime: number;
27
29
  instrument?: InstrumentDescriptor;
package/dist/backtest.js CHANGED
@@ -936,6 +936,7 @@ var createStrategyRegistryState = () => ({
936
936
  strategyManifestsMap: /* @__PURE__ */ new Map(),
937
937
  strategyEntriesMap: /* @__PURE__ */ new Map(),
938
938
  strategySourcesMap: /* @__PURE__ */ new Map(),
939
+ strategyModuleResolutions: /* @__PURE__ */ new Map(),
939
940
  pluginsLoadPromise: null
940
941
  });
941
942
  var registryStateByProjectRoot = sharedStrategyRegistry.registryStateByProjectRoot;
@@ -997,7 +998,7 @@ var validateStrategyEntries = (moduleName, entries, state) => {
997
998
  const strategyName = entry?.manifest?.name;
998
999
  if (typeof strategyName !== "string" || !strategyName.trim()) {
999
1000
  issues.push(`${entryPath}: manifest.name is required`);
1000
- } else if (names.has(strategyName) || state.strategyEntriesMap.has(strategyName)) {
1001
+ } else if (names.has(strategyName) || state?.strategyEntriesMap.has(strategyName)) {
1001
1002
  issues.push(`${entryPath}: duplicate strategy ${strategyName}`);
1002
1003
  } else {
1003
1004
  names.add(strategyName);
@@ -1059,6 +1060,72 @@ var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()
1059
1060
  moduleName
1060
1061
  );
1061
1062
  };
1063
+ var resolveStrategyModuleEntry = async ({
1064
+ strategyName,
1065
+ moduleName,
1066
+ cwd
1067
+ }) => {
1068
+ const { projectRoot, state } = getStrategyRegistryState(cwd);
1069
+ const source = moduleName.trim();
1070
+ const resolvedModuleName = resolvePluginModuleSpecifier(source, projectRoot);
1071
+ const cacheKey = `${resolvedModuleName}\0${strategyName}`;
1072
+ const cached = state.strategyModuleResolutions.get(cacheKey);
1073
+ if (cached) return cached;
1074
+ const resolution = (async () => {
1075
+ const moduleExport = await importStrategyPluginModule(
1076
+ resolvedModuleName,
1077
+ projectRoot
1078
+ );
1079
+ const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
1080
+ if (!pluginDefinition) {
1081
+ throw new Error(`${source}: export { strategyEntries } is missing`);
1082
+ }
1083
+ const issues = validateStrategyEntries(
1084
+ source,
1085
+ pluginDefinition.strategyEntries
1086
+ );
1087
+ if (issues.length > 0) {
1088
+ throw new Error(
1089
+ ["Invalid TradeJS strategy module:", ...issues].join("\n")
1090
+ );
1091
+ }
1092
+ const entry = pluginDefinition.strategyEntries.find(
1093
+ (candidate) => candidate.manifest.name === strategyName
1094
+ );
1095
+ if (!entry) {
1096
+ throw new Error(`${source}: strategy ${strategyName} is missing`);
1097
+ }
1098
+ const factory = sharedStrategyRegistry.strategyRuntimeFactory;
1099
+ if (!factory) {
1100
+ throw new Error("Strategy runtime factory is not configured");
1101
+ }
1102
+ const manifestsByName = new Map(
1103
+ pluginDefinition.strategyEntries.map(
1104
+ (candidate) => [candidate.manifest.name, candidate.manifest]
1105
+ )
1106
+ );
1107
+ return {
1108
+ entry,
1109
+ creator: factory({
1110
+ strategyName,
1111
+ defaults: entry.defaults,
1112
+ createCore: entry.createCore,
1113
+ manifest: entry.manifest,
1114
+ detectorKey: entry.detectorKey,
1115
+ detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
1116
+ resolveRegisteredManifest: (name) => manifestsByName.get(name) ?? state.strategyManifestsMap.get(name)
1117
+ }),
1118
+ source
1119
+ };
1120
+ })();
1121
+ state.strategyModuleResolutions.set(cacheKey, resolution);
1122
+ try {
1123
+ return await resolution;
1124
+ } catch (error) {
1125
+ state.strategyModuleResolutions.delete(cacheKey);
1126
+ throw error;
1127
+ }
1128
+ };
1062
1129
  var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
1063
1130
  const { projectRoot, state } = getStrategyRegistryState(cwd);
1064
1131
  if (!state.pluginsLoadPromise) {
@@ -1136,7 +1203,11 @@ var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
1136
1203
  }
1137
1204
  await state.pluginsLoadPromise;
1138
1205
  };
1139
- var getStrategyCreator = async (name, cwd = getTradejsProjectCwd()) => {
1206
+ var getStrategyCreator = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
1207
+ if (moduleName?.trim()) {
1208
+ await ensureStrategyPluginsLoaded(cwd);
1209
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).creator;
1210
+ }
1140
1211
  await ensureStrategyPluginsLoaded(cwd);
1141
1212
  const { state } = getStrategyRegistryState(cwd);
1142
1213
  return state.strategyCreators.get(name);
@@ -2355,9 +2426,40 @@ var DEFAULT_MAX_AGE_BY_INTERVAL = {
2355
2426
  "15m": 30 * 6e4,
2356
2427
  "1h": 2 * 60 * 6e4
2357
2428
  };
2429
+ var MARKET_CONTEXT_PRELOAD_CHUNK_MS = 7 * 24 * 60 * 6e4;
2358
2430
  var binanceMarketContextUnavailable = false;
2359
2431
  var referenceRowsCache = /* @__PURE__ */ new Map();
2360
2432
  var breadthCache = /* @__PURE__ */ new Map();
2433
+ var BREADTH_COMMON_NUMERIC_FIELDS = [
2434
+ "symbolsCount",
2435
+ "advancers",
2436
+ "decliners",
2437
+ "unchanged",
2438
+ "advanceDeclineRatio",
2439
+ "pctAboveMa20",
2440
+ "pctAboveMa50",
2441
+ "equalWeightedReturn",
2442
+ "volumeWeightedReturn",
2443
+ "dispersion"
2444
+ ];
2445
+ var BREADTH_REGIME_NUMERIC_FIELDS = [
2446
+ "btcReturn1h",
2447
+ "btcReturn4h",
2448
+ "btcReturn24h",
2449
+ "altBasketReturn1h",
2450
+ "altBasketReturn4h",
2451
+ "altBasketReturn24h",
2452
+ "btcVsAltReturn1h",
2453
+ "btcVsAltReturn4h",
2454
+ "btcVsAltReturn24h",
2455
+ "btcTurnoverShare1h",
2456
+ "btcTurnoverShare24h",
2457
+ "btcTurnoverShareChange24h",
2458
+ "altVolToBtcVol24h",
2459
+ "altDispersion24h"
2460
+ ];
2461
+ var preloadedTradeFlowBySymbol = null;
2462
+ var preloadedBreadthByUniverse = null;
2361
2463
  var parseEnabledFlag = (value, env) => {
2362
2464
  const normalized = String(value ?? "").trim().toLowerCase();
2363
2465
  if (!normalized)
@@ -2393,6 +2495,100 @@ var hasBaseContext = (signal) => Boolean(
2393
2495
  signal.additionalIndicators?.baseContext && typeof signal.additionalIndicators.baseContext === "object" && !Array.isArray(signal.additionalIndicators.baseContext)
2394
2496
  );
2395
2497
  var isBinanceMarketContextEnabled = (env) => parseEnabledFlag(process.env.BINANCE_MARKET_CONTEXT_ENABLED, env);
2498
+ var latestIndexAtOrBefore = (timestamps, timestamp) => {
2499
+ if (!timestamps?.length) return -1;
2500
+ let low = 0;
2501
+ let high = timestamps.length - 1;
2502
+ let match = -1;
2503
+ while (low <= high) {
2504
+ const middle = Math.floor((low + high) / 2);
2505
+ const rowTimestamp = timestamps[middle];
2506
+ if (rowTimestamp == null) break;
2507
+ if (rowTimestamp <= timestamp) {
2508
+ match = middle;
2509
+ low = middle + 1;
2510
+ } else {
2511
+ high = middle - 1;
2512
+ }
2513
+ }
2514
+ return match;
2515
+ };
2516
+ var latestChunkAtOrBefore = (chunks, timestamp) => {
2517
+ if (!chunks?.length) return null;
2518
+ let low = 0;
2519
+ let high = chunks.length - 1;
2520
+ let match = null;
2521
+ while (low <= high) {
2522
+ const middle = Math.floor((low + high) / 2);
2523
+ const chunk = chunks[middle];
2524
+ const firstTimestamp = chunk?.timestamps[0];
2525
+ if (!chunk || firstTimestamp == null) break;
2526
+ if (firstTimestamp <= timestamp) {
2527
+ match = chunk;
2528
+ low = middle + 1;
2529
+ } else {
2530
+ high = middle - 1;
2531
+ }
2532
+ }
2533
+ return match;
2534
+ };
2535
+ var unpackedNumber = (value) => value == null || Number.isNaN(value) ? null : value;
2536
+ var unpackTradeFlowRow = (chunks, timestamp) => {
2537
+ const rows = latestChunkAtOrBefore(chunks, timestamp);
2538
+ const index = latestIndexAtOrBefore(rows?.timestamps, timestamp);
2539
+ if (!rows || index < 0) return null;
2540
+ return {
2541
+ symbol: rows.symbol,
2542
+ interval: rows.interval,
2543
+ ts: new Date(rows.timestamps[index]),
2544
+ trades: unpackedNumber(rows.columns.trades[index]) ?? 0,
2545
+ buyBaseVolume: unpackedNumber(rows.columns.buyBaseVolume[index]),
2546
+ sellBaseVolume: unpackedNumber(rows.columns.sellBaseVolume[index]),
2547
+ buyQuoteVolume: unpackedNumber(rows.columns.buyQuoteVolume[index]),
2548
+ sellQuoteVolume: unpackedNumber(rows.columns.sellQuoteVolume[index]),
2549
+ netBaseDelta: unpackedNumber(rows.columns.netBaseDelta[index]),
2550
+ netQuoteDelta: unpackedNumber(rows.columns.netQuoteDelta[index]),
2551
+ buyPressurePct: unpackedNumber(rows.columns.buyPressurePct[index])
2552
+ };
2553
+ };
2554
+ var unpackBreadthRow = (chunks, timestamp) => {
2555
+ const rows = latestChunkAtOrBefore(chunks, timestamp);
2556
+ const index = latestIndexAtOrBefore(rows?.timestamps, timestamp);
2557
+ if (!rows || index < 0) return null;
2558
+ const commonNumeric = Object.fromEntries(
2559
+ BREADTH_COMMON_NUMERIC_FIELDS.map((field) => [
2560
+ field,
2561
+ unpackedNumber(rows.commonColumns[field][index])
2562
+ ])
2563
+ );
2564
+ const regimeNumeric = Object.fromEntries(
2565
+ BREADTH_REGIME_NUMERIC_FIELDS.map((field) => [
2566
+ field,
2567
+ unpackedNumber(rows.regimeColumns?.[field][index])
2568
+ ])
2569
+ );
2570
+ return {
2571
+ universe: rows.universe,
2572
+ interval: rows.interval,
2573
+ ts: new Date(rows.timestamps[index]),
2574
+ ...commonNumeric,
2575
+ ...regimeNumeric,
2576
+ symbolsCount: commonNumeric.symbolsCount ?? 0,
2577
+ advancers: commonNumeric.advancers ?? 0,
2578
+ decliners: commonNumeric.decliners ?? 0,
2579
+ unchanged: commonNumeric.unchanged ?? 0,
2580
+ btcAltRegime: rows.regimes?.[index] ?? null
2581
+ };
2582
+ };
2583
+ var toAsOfRow = (row, timestamp, maxAgeMs) => {
2584
+ if (!row) return null;
2585
+ const ageMs = timestamp - row.ts.getTime();
2586
+ return {
2587
+ ...row,
2588
+ ageMs: Number.isFinite(ageMs) ? ageMs : null,
2589
+ stale: !Number.isFinite(ageMs) || ageMs < 0 || ageMs > maxAgeMs
2590
+ };
2591
+ };
2396
2592
  var toTradeFlowContext = (row, interval) => row ? {
2397
2593
  source: "binance_agg_trades",
2398
2594
  interval,
@@ -2415,6 +2611,24 @@ var getCachedReferenceRows = ({
2415
2611
  maxAgeMs,
2416
2612
  abortSignal
2417
2613
  }) => {
2614
+ if (preloadedTradeFlowBySymbol) {
2615
+ return Promise.resolve(
2616
+ referenceSymbols.map((symbol) => ({
2617
+ symbol,
2618
+ tradeFlow: toTradeFlowContext(
2619
+ toAsOfRow(
2620
+ unpackTradeFlowRow(
2621
+ preloadedTradeFlowBySymbol?.get(symbol),
2622
+ timestamp
2623
+ ),
2624
+ timestamp,
2625
+ maxAgeMs
2626
+ ),
2627
+ interval
2628
+ )
2629
+ }))
2630
+ );
2631
+ }
2418
2632
  const key = `${referenceSymbols.join(",")}:${interval}:${timestamp}:${maxAgeMs}`;
2419
2633
  const cached = referenceRowsCache.get(key);
2420
2634
  if (cached) return cached;
@@ -2444,6 +2658,18 @@ var getCachedBreadth = ({
2444
2658
  maxAgeMs,
2445
2659
  abortSignal
2446
2660
  }) => {
2661
+ if (preloadedBreadthByUniverse) {
2662
+ return Promise.resolve(
2663
+ toAsOfRow(
2664
+ unpackBreadthRow(
2665
+ preloadedBreadthByUniverse.get(breadthUniverse),
2666
+ timestamp
2667
+ ),
2668
+ timestamp,
2669
+ maxAgeMs
2670
+ )
2671
+ );
2672
+ }
2447
2673
  const key = `${breadthUniverse}:${interval}:${timestamp}:${maxAgeMs}`;
2448
2674
  const cached = breadthCache.get(key);
2449
2675
  if (cached) return cached;
@@ -2650,12 +2876,20 @@ var SOURCE_EXCHANGE_LIQUIDITY = "coinmarketcap_exchange_liquidity";
2650
2876
  var SOURCE_FEAR_GREED = "coinmarketcap_fear_greed";
2651
2877
  var SOURCE_INDEX = "coinmarketcap_index";
2652
2878
  var DAY_MS = 864e5;
2879
+ var MAX_TIMESTAMP_CACHE_ENTRIES = 512;
2653
2880
  var coinMarketCapContextUnavailable = false;
2654
2881
  var globalContextCache = /* @__PURE__ */ new Map();
2655
2882
  var referenceContextCache = /* @__PURE__ */ new Map();
2656
2883
  var exchangeLiquidityContextCache = /* @__PURE__ */ new Map();
2657
2884
  var fearGreedContextCache = /* @__PURE__ */ new Map();
2658
2885
  var indexContextCache = /* @__PURE__ */ new Map();
2886
+ var setBoundedCache = (cache, key, value) => {
2887
+ if (cache.size >= MAX_TIMESTAMP_CACHE_ENTRIES) {
2888
+ const oldestKey = cache.keys().next().value;
2889
+ if (oldestKey != null) cache.delete(oldestKey);
2890
+ }
2891
+ cache.set(key, value);
2892
+ };
2659
2893
  var parseEnabledFlag2 = (value, env) => {
2660
2894
  const normalized = String(value ?? "").trim().toLowerCase();
2661
2895
  if (!normalized) {
@@ -2762,7 +2996,7 @@ var getCachedGlobalContext = ({
2762
2996
  maxAgeMs,
2763
2997
  ...abortSignal ? { signal: abortSignal } : {}
2764
2998
  });
2765
- globalContextCache.set(key, promise);
2999
+ setBoundedCache(globalContextCache, key, promise);
2766
3000
  void promise.catch(() => globalContextCache.delete(key));
2767
3001
  return promise;
2768
3002
  };
@@ -2782,7 +3016,7 @@ var getCachedReferenceContexts = ({
2782
3016
  maxAgeMs,
2783
3017
  ...abortSignal ? { signal: abortSignal } : {}
2784
3018
  });
2785
- referenceContextCache.set(key, promise);
3019
+ setBoundedCache(referenceContextCache, key, promise);
2786
3020
  void promise.catch(() => referenceContextCache.delete(key));
2787
3021
  return promise;
2788
3022
  };
@@ -2801,7 +3035,7 @@ var getCachedExchangeLiquidityContext = ({
2801
3035
  maxAgeMs,
2802
3036
  ...abortSignal ? { signal: abortSignal } : {}
2803
3037
  });
2804
- exchangeLiquidityContextCache.set(key, promise);
3038
+ setBoundedCache(exchangeLiquidityContextCache, key, promise);
2805
3039
  void promise.catch(() => exchangeLiquidityContextCache.delete(key));
2806
3040
  return promise;
2807
3041
  };
@@ -2820,7 +3054,7 @@ var getCachedFearGreedContext = ({
2820
3054
  maxAgeMs,
2821
3055
  ...abortSignal ? { signal: abortSignal } : {}
2822
3056
  });
2823
- fearGreedContextCache.set(key, promise);
3057
+ setBoundedCache(fearGreedContextCache, key, promise);
2824
3058
  void promise.catch(() => fearGreedContextCache.delete(key));
2825
3059
  return promise;
2826
3060
  };
@@ -2840,7 +3074,7 @@ var getCachedIndexContexts = ({
2840
3074
  maxAgeMs,
2841
3075
  ...abortSignal ? { signal: abortSignal } : {}
2842
3076
  });
2843
- indexContextCache.set(key, promise);
3077
+ setBoundedCache(indexContextCache, key, promise);
2844
3078
  void promise.catch(() => indexContextCache.delete(key));
2845
3079
  return promise;
2846
3080
  };
@@ -3650,7 +3884,7 @@ var signalIntervalToMarketInterval2 = (value) => {
3650
3884
  var hasBaseContext3 = (signal) => Boolean(
3651
3885
  signal.additionalIndicators?.baseContext && typeof signal.additionalIndicators.baseContext === "object" && !Array.isArray(signal.additionalIndicators.baseContext)
3652
3886
  );
3653
- var setBoundedCache = (key, value) => {
3887
+ var setBoundedCache2 = (key, value) => {
3654
3888
  if (contextCache.size >= MAX_CACHE_ENTRIES) {
3655
3889
  const oldestKey = contextCache.keys().next().value;
3656
3890
  if (oldestKey != null) contextCache.delete(oldestKey);
@@ -3897,7 +4131,7 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
3897
4131
  whaleRegistryFingerprint: whales.fingerprint,
3898
4132
  ...params.abortSignal ? { signal: params.abortSignal } : {}
3899
4133
  });
3900
- setBoundedCache(cacheKey, pending);
4134
+ setBoundedCache2(cacheKey, pending);
3901
4135
  }
3902
4136
  row = await pending;
3903
4137
  }
@@ -5032,20 +5266,23 @@ var buildHookCtx = ({
5032
5266
  strategyConfig,
5033
5267
  env,
5034
5268
  isConfigFromBacktest
5035
- }) => ({
5036
- connector,
5037
- strategyName,
5038
- userName,
5039
- symbol,
5040
- ...universe ? { universe } : {},
5041
- ...assetClass ? { assetClass } : {},
5042
- ...accountId ? { accountId } : {},
5043
- ...deploymentId ? { deploymentId } : {},
5044
- ...policyProfileId ? { policyProfileId } : {},
5045
- strategyConfig,
5046
- env,
5047
- isConfigFromBacktest
5048
- });
5269
+ }) => {
5270
+ const context = {
5271
+ connector,
5272
+ strategyName,
5273
+ userName,
5274
+ symbol
5275
+ };
5276
+ if (universe) context.universe = universe;
5277
+ if (assetClass) context.assetClass = assetClass;
5278
+ if (accountId) context.accountId = accountId;
5279
+ if (deploymentId) context.deploymentId = deploymentId;
5280
+ if (policyProfileId) context.policyProfileId = policyProfileId;
5281
+ context.strategyConfig = strategyConfig;
5282
+ context.env = env;
5283
+ context.isConfigFromBacktest = isConfigFromBacktest;
5284
+ return context;
5285
+ };
5049
5286
  var shouldRecordRuntimeJournal = ({
5050
5287
  env,
5051
5288
  config
@@ -6989,6 +7226,7 @@ var import_ml3 = require("@tradejs/infra/ml");
6989
7226
 
6990
7227
  // src/executionCosts.ts
6991
7228
  var import_constants7 = require("@tradejs/core/constants");
7229
+ var import_backtest = require("@tradejs/core/backtest");
6992
7230
  var finiteOr = (value, fallback) => {
6993
7231
  const parsed = Number(value);
6994
7232
  return Number.isFinite(parsed) ? parsed : fallback;
@@ -7025,20 +7263,27 @@ var loadFundingRates = (connector, symbol, startTime, endTime) => {
7025
7263
  };
7026
7264
  var resolveExecutionCosts = async (params) => {
7027
7265
  const { connector, symbol, config, startTime, endTime, instrument } = params;
7028
- const cacheOnly = config.EXECUTION_COSTS_CACHE_ONLY === true;
7029
- const hasConfiguredFees = Number.isFinite(Number(config.MAKER_FEE_RATE)) && Number.isFinite(Number(config.TAKER_FEE_RATE));
7266
+ (0, import_backtest.assertStrategyExecutionIsolation)(config);
7267
+ const costs = params.executionCosts == null ? void 0 : (0, import_backtest.parseBacktestExecutionCosts)(params.executionCosts);
7268
+ const cacheOnly = params.cacheOnly === true;
7269
+ const hasConfiguredFees = costs != null;
7030
7270
  const exchangeFees = !cacheOnly && !hasConfiguredFees && connector.getTradingFeeRate ? await loadTradingFee(connector, symbol).catch(() => null) : null;
7031
- const makerRate = hasConfiguredFees ? Number(config.MAKER_FEE_RATE) : exchangeFees?.makerRate ?? import_constants7.FEE_PERCENT;
7032
- const takerRate = hasConfiguredFees ? Number(config.TAKER_FEE_RATE) : exchangeFees?.takerRate ?? import_constants7.FEE_PERCENT;
7033
- const fundingEnabled = config.FUNDING_ENABLED !== false && !cacheOnly && typeof connector.getFundingRateHistory === "function";
7271
+ const makerRate = hasConfiguredFees ? costs.fees.makerRate : exchangeFees?.makerRate ?? import_constants7.FEE_PERCENT;
7272
+ const takerRate = hasConfiguredFees ? costs.fees.takerRate : exchangeFees?.takerRate ?? import_constants7.FEE_PERCENT;
7273
+ const fundingEnabled = costs?.funding.enabled !== false && !cacheOnly && typeof connector.getFundingRateHistory === "function";
7034
7274
  const fundingRates = fundingEnabled ? await loadFundingRates(connector, symbol, startTime, endTime) : [];
7035
7275
  const requestedLeverage = Math.max(1, finiteOr(config.LEVERAGE, 10));
7036
7276
  const venueMaxLeverage = Number(instrument?.venueMetadata?.maxLeverage);
7037
7277
  const maxAllowed = Number.isFinite(venueMaxLeverage) ? venueMaxLeverage : null;
7038
7278
  const effectiveLeverage = maxAllowed == null ? requestedLeverage : Math.min(requestedLeverage, maxAllowed);
7039
7279
  const feeSource = hasConfiguredFees ? "config" : exchangeFees?.source ?? "fallback";
7040
- const fundingSource = !fundingEnabled ? cacheOnly ? "fallback" : "disabled" : fundingRates.length ? "historical" : "unavailable";
7041
- const usesFallback = feeSource === "fallback" || fundingEnabled && fundingSource === "unavailable" || config.SLIPPAGE_BASE_BPS == null && config.SLIPPAGE_SPREAD_MULTIPLIER == null && config.SLIPPAGE_MARKET_IMPACT_BPS == null;
7280
+ const fundingSource = !fundingEnabled ? costs?.funding.enabled === false ? "disabled" : cacheOnly ? "fallback" : "disabled" : fundingRates.length ? "historical" : "unavailable";
7281
+ const usesFallback = feeSource === "fallback" || fundingEnabled && fundingSource === "unavailable" || !costs;
7282
+ if (costs?.funding.enabled && (!fundingEnabled || fundingSource !== "historical")) {
7283
+ throw new Error(
7284
+ "Requested funding history is unavailable; explicitly disable funding or provide historical data"
7285
+ );
7286
+ }
7042
7287
  return {
7043
7288
  model: {
7044
7289
  fees: { makerRate, takerRate, source: feeSource },
@@ -7050,20 +7295,20 @@ var resolveExecutionCosts = async (params) => {
7050
7295
  toTimestamp: fundingRates.at(-1)?.timestamp ?? null
7051
7296
  },
7052
7297
  slippage: {
7053
- baseBps: finiteOr(config.SLIPPAGE_BASE_BPS, import_constants7.BACKTEST_BASE_SLIPPAGE_BPS),
7298
+ baseBps: costs?.slippage.baseBps ?? import_constants7.BACKTEST_BASE_SLIPPAGE_BPS,
7054
7299
  spreadMultiplier: finiteOr(
7055
- config.SLIPPAGE_SPREAD_MULTIPLIER,
7300
+ costs?.slippage.spreadMultiplier,
7056
7301
  import_constants7.BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER
7057
7302
  ),
7058
7303
  marketImpactBps: finiteOr(
7059
- config.SLIPPAGE_MARKET_IMPACT_BPS,
7304
+ costs?.slippage.marketImpactBps,
7060
7305
  import_constants7.BACKTEST_MARKET_IMPACT_BPS
7061
7306
  ),
7062
7307
  delayRiskMultiplier: finiteOr(
7063
- config.SLIPPAGE_DELAY_RISK_MULTIPLIER,
7308
+ costs?.slippage.delayRiskMultiplier,
7064
7309
  import_constants7.BACKTEST_DELAY_RISK_MULTIPLIER
7065
7310
  ),
7066
- source: config.SLIPPAGE_BASE_BPS != null || config.SLIPPAGE_SPREAD_MULTIPLIER != null || config.SLIPPAGE_MARKET_IMPACT_BPS != null ? "config" : "fallback"
7311
+ source: costs != null ? "config" : "fallback"
7067
7312
  },
7068
7313
  leverage: {
7069
7314
  requested: requestedLeverage,
@@ -7080,7 +7325,7 @@ var resolveExecutionCosts = async (params) => {
7080
7325
  // src/testConnector.ts
7081
7326
  var import_node_crypto4 = require("crypto");
7082
7327
  var import_constants8 = require("@tradejs/core/constants");
7083
- var import_backtest = require("@tradejs/core/backtest");
7328
+ var import_backtest2 = require("@tradejs/core/backtest");
7084
7329
  var import_trade2 = require("@tradejs/core/trade");
7085
7330
  var import_math = require("@tradejs/core/math");
7086
7331
  var PRICE_PRECISION = 8;
@@ -7379,7 +7624,7 @@ var createTestConnector = (connector, context) => {
7379
7624
  allocatedQty += legExitQty;
7380
7625
  const legResult = currentEntryLegResults[leg.index];
7381
7626
  const legGrossProfit = currentPosition.direction === "LONG" ? (executionPrice - legResult.entryPrice) * legExitQty : (legResult.entryPrice - executionPrice) * legExitQty;
7382
- const legFee = executionPrice * legExitQty * takerFeeRate;
7627
+ const legFee = fee * (legExitQty / qty);
7383
7628
  currentEntryLegResults[leg.index] = appendExitToTradeResult({
7384
7629
  tradeResult: legResult,
7385
7630
  direction: currentPosition.direction,
@@ -7557,7 +7802,7 @@ var createTestConnector = (connector, context) => {
7557
7802
  kline: async (options) => connector.kline(options),
7558
7803
  getResult: async () => {
7559
7804
  const orderLogId = (0, import_node_crypto4.randomUUID)().slice(-12);
7560
- const fullStat = fastMode ? (0, import_backtest.calculateStatsFull)(positionLog) : null;
7805
+ const fullStat = fastMode ? (0, import_backtest2.calculateStatsFull)(positionLog) : null;
7561
7806
  return {
7562
7807
  stat: fullStat ? {
7563
7808
  ...fullStat,
@@ -8063,7 +8308,8 @@ var createTestConnector = (connector, context) => {
8063
8308
  const { fee, profit } = getNetProfit({
8064
8309
  grossProfit,
8065
8310
  price: executionPrice,
8066
- qty: currentPosition.qty
8311
+ qty: currentPosition.qty,
8312
+ feeRate: order.isLimit ? makerFeeRate : takerFeeRate
8067
8313
  });
8068
8314
  recordExitResult({
8069
8315
  timestamp: order.timestamp,
@@ -8208,6 +8454,8 @@ var createBacktestSession = async ({
8208
8454
  connector,
8209
8455
  symbol: test.symbol,
8210
8456
  config: test.strategyConfig,
8457
+ executionCosts: test.executionCosts,
8458
+ cacheOnly: test.executionCostsCacheOnly,
8211
8459
  startTime: start,
8212
8460
  endTime: test.options.end,
8213
8461
  instrument