@tradejs/node 3.1.29-beta.287 → 3.1.29-beta.289

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 CHANGED
@@ -799,7 +799,7 @@ var import_indicators3 = require("@tradejs/core/indicators");
799
799
  var import_constants9 = require("@tradejs/core/constants");
800
800
  var import_strategies7 = require("@tradejs/core/strategies");
801
801
  var import_time3 = require("@tradejs/core/time");
802
- var import_logger14 = require("@tradejs/infra/logger");
802
+ var import_logger15 = require("@tradejs/infra/logger");
803
803
 
804
804
  // src/strategyRuntime.ts
805
805
  var import_jev6 = require("@tradejs/core/jev");
@@ -1054,13 +1054,213 @@ var assessSignalWithJev = async ({
1054
1054
  // src/strategyRuntime.ts
1055
1055
  var import_constants5 = require("@tradejs/core/constants");
1056
1056
  var import_strategies6 = require("@tradejs/core/strategies");
1057
- var import_logger10 = require("@tradejs/infra/logger");
1057
+ var import_logger11 = require("@tradejs/infra/logger");
1058
1058
 
1059
1059
  // src/strategyHelpers/runtime.ts
1060
- var import_logger8 = require("@tradejs/infra/logger");
1060
+ var import_logger9 = require("@tradejs/infra/logger");
1061
1061
  var import_constants3 = require("@tradejs/core/constants");
1062
1062
  var import_ml2 = require("@tradejs/infra/ml");
1063
1063
 
1064
+ // src/strategy/entrySubmissionValidation.ts
1065
+ var import_logger = require("@tradejs/infra/logger");
1066
+
1067
+ // src/strategy/entryExecutionValidation.ts
1068
+ var EntrySubmissionValidationError = class extends Error {
1069
+ constructor() {
1070
+ super(...arguments);
1071
+ this.name = "EntrySubmissionValidationError";
1072
+ }
1073
+ };
1074
+ var resolveEntryExecutionLimits = (config, direction) => {
1075
+ const side = config[direction];
1076
+ const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
1077
+ const maximum = config.MAX_LOSS_VALUE;
1078
+ if (minimum == null && maximum == null) return void 0;
1079
+ const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
1080
+ const maxLossValue = maximum == null ? void 0 : Number(maximum);
1081
+ const feeRate = Number(config.RISK_FEE_RATE ?? 0);
1082
+ const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
1083
+ const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
1084
+ const slippageBps = slippage + impact;
1085
+ if (minimumNetRiskRatio != null && (!Number.isFinite(minimumNetRiskRatio) || minimumNetRiskRatio < 0) || maxLossValue != null && (!Number.isFinite(maxLossValue) || maxLossValue <= 0) || !Number.isFinite(feeRate) || feeRate < 0 || feeRate >= 1 || !Number.isFinite(slippage) || slippage < 0 || !Number.isFinite(impact) || impact < 0 || !Number.isFinite(slippageBps) || slippageBps < 0 || slippageBps >= 1e4)
1086
+ throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
1087
+ return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
1088
+ };
1089
+ var validateEntryExecution = ({
1090
+ direction,
1091
+ price,
1092
+ qty,
1093
+ stopLossPrice,
1094
+ takeProfitPrice,
1095
+ limits
1096
+ }) => {
1097
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
1098
+ throw new EntrySubmissionValidationError(
1099
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1100
+ );
1101
+ const side = direction === "LONG" ? 1 : -1;
1102
+ if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
1103
+ throw new EntrySubmissionValidationError(
1104
+ "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
1105
+ );
1106
+ const slippage = limits.slippageBps / 1e4;
1107
+ const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
1108
+ const entryCost = cost(price, side);
1109
+ const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
1110
+ const rewardPerUnit = Math.max(
1111
+ 0,
1112
+ Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
1113
+ );
1114
+ const netRiskRatio = rewardPerUnit / lossPerUnit;
1115
+ const plannedLossValue = qty * lossPerUnit;
1116
+ if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
1117
+ throw new EntrySubmissionValidationError(
1118
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1119
+ );
1120
+ if (limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio)
1121
+ throw new EntrySubmissionValidationError(
1122
+ "ENTRY_RR_BELOW_MIN_AT_SUBMISSION"
1123
+ );
1124
+ if (limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9))
1125
+ throw new EntrySubmissionValidationError(
1126
+ "ENTRY_RISK_EXCEEDS_MAX_LOSS_AT_SUBMISSION"
1127
+ );
1128
+ return { netRiskRatio, plannedLossValue };
1129
+ };
1130
+
1131
+ // src/strategy/entrySubmissionValidation.ts
1132
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
1133
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
1134
+ var getOrderArrivalSnapshot = async ({
1135
+ connector,
1136
+ symbol,
1137
+ requireTimestamp = false
1138
+ }) => {
1139
+ if (typeof connector.getTopOfBookTicker !== "function") {
1140
+ return {
1141
+ arrivalSnapshotTime: Date.now(),
1142
+ arrivalSource: "unavailable",
1143
+ bid: null,
1144
+ ask: null,
1145
+ arrivalMid: null,
1146
+ spreadBps: null
1147
+ };
1148
+ }
1149
+ try {
1150
+ const ticker = await connector.getTopOfBookTicker(symbol);
1151
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
1152
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
1153
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
1154
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
1155
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
1156
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
1157
+ return {
1158
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
1159
+ arrivalSource: "top_of_book",
1160
+ bid,
1161
+ ask,
1162
+ arrivalMid,
1163
+ spreadBps
1164
+ };
1165
+ } catch (error) {
1166
+ import_logger.logger.warn(
1167
+ "runtime order arrival snapshot failed: %s %s",
1168
+ symbol,
1169
+ error?.message || String(error)
1170
+ );
1171
+ return {
1172
+ arrivalSnapshotTime: Date.now(),
1173
+ arrivalSource: "top_of_book_error",
1174
+ bid: null,
1175
+ ask: null,
1176
+ arrivalMid: null,
1177
+ spreadBps: null
1178
+ };
1179
+ }
1180
+ };
1181
+ var validateEntryProtectionAtArrival = ({
1182
+ direction,
1183
+ signalPrice,
1184
+ bid,
1185
+ ask,
1186
+ arrivalMid,
1187
+ takeProfits,
1188
+ stopLossPrice
1189
+ }) => {
1190
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
1191
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
1192
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
1193
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
1194
+ }
1195
+ for (const takeProfit of takeProfits) {
1196
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
1197
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
1198
+ }
1199
+ }
1200
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
1201
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
1202
+ }
1203
+ };
1204
+ var createEntrySubmissionValidation = ({
1205
+ connector,
1206
+ symbol,
1207
+ direction,
1208
+ takeProfits,
1209
+ stopLossPrice,
1210
+ limits,
1211
+ onValidated
1212
+ }) => {
1213
+ if (!limits) return void 0;
1214
+ if (!connector.capabilities?.orderSubmissionValidation)
1215
+ throw new EntrySubmissionValidationError(
1216
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
1217
+ );
1218
+ return async ({ qty, executionPrice }) => {
1219
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
1220
+ const snapshot2 = simulated && executionPrice != null ? {
1221
+ arrivalSnapshotTime: Date.now(),
1222
+ arrivalSource: "simulation",
1223
+ bid: executionPrice,
1224
+ ask: executionPrice,
1225
+ arrivalMid: executionPrice,
1226
+ spreadBps: 0
1227
+ } : await getOrderArrivalSnapshot({
1228
+ connector,
1229
+ symbol,
1230
+ requireTimestamp: true
1231
+ });
1232
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
1233
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
1234
+ throw new EntrySubmissionValidationError(
1235
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
1236
+ );
1237
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
1238
+ throw new EntrySubmissionValidationError(
1239
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
1240
+ );
1241
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
1242
+ validateEntryProtectionAtArrival({
1243
+ direction,
1244
+ signalPrice: price,
1245
+ bid: snapshot2.bid,
1246
+ ask: snapshot2.ask,
1247
+ arrivalMid: snapshot2.arrivalMid,
1248
+ takeProfits,
1249
+ stopLossPrice
1250
+ });
1251
+ const prices = takeProfits.map((tp) => tp.price);
1252
+ const economics2 = validateEntryExecution({
1253
+ direction,
1254
+ price,
1255
+ qty,
1256
+ stopLossPrice,
1257
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
1258
+ limits
1259
+ });
1260
+ onValidated?.(snapshot2, economics2);
1261
+ };
1262
+ };
1263
+
1064
1264
  // src/ai.ts
1065
1265
  var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
1066
1266
  var import_redis = require("@tradejs/infra/redis");
@@ -1645,7 +1845,7 @@ var import_fs = __toESM(require("fs"));
1645
1845
  var import_path = __toESM(require("path"));
1646
1846
  var import_url = require("url");
1647
1847
  var import_config = require("@tradejs/core/config");
1648
- var import_logger = require("@tradejs/infra/logger");
1848
+ var import_logger2 = require("@tradejs/infra/logger");
1649
1849
  var CONFIG_FILE_NAMES = [
1650
1850
  "tradejs.config.ts",
1651
1851
  "tradejs.config.mts",
@@ -1912,11 +2112,11 @@ var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
1912
2112
  cachedByCwd.set(cwd, config);
1913
2113
  if (!announcedConfigFile.has(configFilePath)) {
1914
2114
  announcedConfigFile.add(configFilePath);
1915
- import_logger.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
2115
+ import_logger2.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
1916
2116
  }
1917
2117
  return config;
1918
2118
  } catch (error) {
1919
- import_logger.logger.log(
2119
+ import_logger2.logger.log(
1920
2120
  "warn",
1921
2121
  "Failed to load TradeJS config from %s: %s",
1922
2122
  configFilePath,
@@ -2864,7 +3064,7 @@ var import_node_crypto = require("crypto");
2864
3064
  var import_constants = require("@tradejs/core/constants");
2865
3065
  var import_time = require("@tradejs/core/time");
2866
3066
  var import_trade = require("@tradejs/core/trade");
2867
- var import_logger2 = require("@tradejs/infra/logger");
3067
+ var import_logger3 = require("@tradejs/infra/logger");
2868
3068
  var import_redis2 = require("@tradejs/infra/redis");
2869
3069
  var now = () => Date.now();
2870
3070
  var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
@@ -2923,7 +3123,7 @@ var recordRuntimeTradeOpen = async (params) => {
2923
3123
  )
2924
3124
  ]);
2925
3125
  } catch (error) {
2926
- import_logger2.logger.error(
3126
+ import_logger3.logger.error(
2927
3127
  "runtime trade open journal failed: %s %s",
2928
3128
  record.symbol,
2929
3129
  error?.message || String(error)
@@ -2991,7 +3191,7 @@ var recordRuntimeTradeIncrease = async (params) => {
2991
3191
  )
2992
3192
  ]);
2993
3193
  } catch (error) {
2994
- import_logger2.logger.error(
3194
+ import_logger3.logger.error(
2995
3195
  "runtime trade increase journal failed: %s %s",
2996
3196
  symbol,
2997
3197
  error?.message || String(error)
@@ -3098,7 +3298,7 @@ var markRuntimeTradeClosed = async (params) => {
3098
3298
  )
3099
3299
  ]);
3100
3300
  } catch (error) {
3101
- import_logger2.logger.error(
3301
+ import_logger3.logger.error(
3102
3302
  "runtime trade close journal failed: %s %s",
3103
3303
  symbol,
3104
3304
  error?.message || String(error)
@@ -3108,11 +3308,11 @@ var markRuntimeTradeClosed = async (params) => {
3108
3308
  };
3109
3309
 
3110
3310
  // src/strategyHelpers/marketContextStages.ts
3111
- var import_logger7 = require("@tradejs/infra/logger");
3311
+ var import_logger8 = require("@tradejs/infra/logger");
3112
3312
 
3113
3313
  // src/strategyHelpers/binanceMarketContext.ts
3114
3314
  var import_marketContext = require("@tradejs/infra/timescale/marketContext");
3115
- var import_logger3 = require("@tradejs/infra/logger");
3315
+ var import_logger4 = require("@tradejs/infra/logger");
3116
3316
  var import_strategies = require("@tradejs/core/strategies");
3117
3317
 
3118
3318
  // src/binanceBreadthUniverses.ts
@@ -3476,7 +3676,7 @@ var parseEnabledFlag = (value, env) => {
3476
3676
  if (normalized === "live") return env !== "BACKTEST";
3477
3677
  return false;
3478
3678
  };
3479
- var toFiniteNumberOrNull = (value) => {
3679
+ var toFiniteNumberOrNull2 = (value) => {
3480
3680
  const numeric = typeof value === "number" ? value : Number(value);
3481
3681
  return Number.isFinite(numeric) ? numeric : null;
3482
3682
  };
@@ -3601,14 +3801,14 @@ var toTradeFlowContext = (row, interval) => row ? {
3601
3801
  asOfTs: row.ts.getTime(),
3602
3802
  ageMs: row.ageMs,
3603
3803
  stale: row.stale,
3604
- trades: toFiniteNumberOrNull(row.trades),
3605
- buyPressurePct: toFiniteNumberOrNull(row.buyPressurePct),
3606
- buyBaseVolume: toFiniteNumberOrNull(row.buyBaseVolume),
3607
- sellBaseVolume: toFiniteNumberOrNull(row.sellBaseVolume),
3608
- buyQuoteVolume: toFiniteNumberOrNull(row.buyQuoteVolume),
3609
- sellQuoteVolume: toFiniteNumberOrNull(row.sellQuoteVolume),
3610
- netBaseDelta: toFiniteNumberOrNull(row.netBaseDelta),
3611
- netQuoteDelta: toFiniteNumberOrNull(row.netQuoteDelta)
3804
+ trades: toFiniteNumberOrNull2(row.trades),
3805
+ buyPressurePct: toFiniteNumberOrNull2(row.buyPressurePct),
3806
+ buyBaseVolume: toFiniteNumberOrNull2(row.buyBaseVolume),
3807
+ sellBaseVolume: toFiniteNumberOrNull2(row.sellBaseVolume),
3808
+ buyQuoteVolume: toFiniteNumberOrNull2(row.buyQuoteVolume),
3809
+ sellQuoteVolume: toFiniteNumberOrNull2(row.sellQuoteVolume),
3810
+ netBaseDelta: toFiniteNumberOrNull2(row.netBaseDelta),
3811
+ netQuoteDelta: toFiniteNumberOrNull2(row.netQuoteDelta)
3612
3812
  } : null;
3613
3813
  var getCachedReferenceRows = ({
3614
3814
  referenceSymbols,
@@ -3697,16 +3897,16 @@ var toMarketBreadthContext = (breadth, interval) => ({
3697
3897
  asOfTs: breadth.ts.getTime(),
3698
3898
  ageMs: breadth.ageMs,
3699
3899
  stale: breadth.stale,
3700
- symbolsCount: toFiniteNumberOrNull(breadth.symbolsCount),
3701
- advancers: toFiniteNumberOrNull(breadth.advancers),
3702
- decliners: toFiniteNumberOrNull(breadth.decliners),
3703
- unchanged: toFiniteNumberOrNull(breadth.unchanged),
3704
- advanceDeclineRatio: toFiniteNumberOrNull(breadth.advanceDeclineRatio),
3705
- pctAboveMa20: toFiniteNumberOrNull(breadth.pctAboveMa20),
3706
- pctAboveMa50: toFiniteNumberOrNull(breadth.pctAboveMa50),
3707
- equalWeightedReturn: toFiniteNumberOrNull(breadth.equalWeightedReturn),
3708
- volumeWeightedReturn: toFiniteNumberOrNull(breadth.volumeWeightedReturn),
3709
- dispersion: toFiniteNumberOrNull(breadth.dispersion)
3900
+ symbolsCount: toFiniteNumberOrNull2(breadth.symbolsCount),
3901
+ advancers: toFiniteNumberOrNull2(breadth.advancers),
3902
+ decliners: toFiniteNumberOrNull2(breadth.decliners),
3903
+ unchanged: toFiniteNumberOrNull2(breadth.unchanged),
3904
+ advanceDeclineRatio: toFiniteNumberOrNull2(breadth.advanceDeclineRatio),
3905
+ pctAboveMa20: toFiniteNumberOrNull2(breadth.pctAboveMa20),
3906
+ pctAboveMa50: toFiniteNumberOrNull2(breadth.pctAboveMa50),
3907
+ equalWeightedReturn: toFiniteNumberOrNull2(breadth.equalWeightedReturn),
3908
+ volumeWeightedReturn: toFiniteNumberOrNull2(breadth.volumeWeightedReturn),
3909
+ dispersion: toFiniteNumberOrNull2(breadth.dispersion)
3710
3910
  });
3711
3911
  var enrichSignalWithBinanceMarketContext = async (params) => {
3712
3912
  const {
@@ -3807,46 +4007,46 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3807
4007
  asOfTs: primaryBreadth.ts.getTime(),
3808
4008
  ageMs: primaryBreadth.ageMs,
3809
4009
  stale: primaryBreadth.stale,
3810
- btcReturn1h: toFiniteNumberOrNull(
4010
+ btcReturn1h: toFiniteNumberOrNull2(
3811
4011
  primaryBreadth.btcReturn1h
3812
4012
  ),
3813
- btcReturn4h: toFiniteNumberOrNull(
4013
+ btcReturn4h: toFiniteNumberOrNull2(
3814
4014
  primaryBreadth.btcReturn4h
3815
4015
  ),
3816
- btcReturn24h: toFiniteNumberOrNull(
4016
+ btcReturn24h: toFiniteNumberOrNull2(
3817
4017
  primaryBreadth.btcReturn24h
3818
4018
  ),
3819
- altBasketReturn1h: toFiniteNumberOrNull(
4019
+ altBasketReturn1h: toFiniteNumberOrNull2(
3820
4020
  primaryBreadth.altBasketReturn1h
3821
4021
  ),
3822
- altBasketReturn4h: toFiniteNumberOrNull(
4022
+ altBasketReturn4h: toFiniteNumberOrNull2(
3823
4023
  primaryBreadth.altBasketReturn4h
3824
4024
  ),
3825
- altBasketReturn24h: toFiniteNumberOrNull(
4025
+ altBasketReturn24h: toFiniteNumberOrNull2(
3826
4026
  primaryBreadth.altBasketReturn24h
3827
4027
  ),
3828
- btcVsAltReturn1h: toFiniteNumberOrNull(
4028
+ btcVsAltReturn1h: toFiniteNumberOrNull2(
3829
4029
  primaryBreadth.btcVsAltReturn1h
3830
4030
  ),
3831
- btcVsAltReturn4h: toFiniteNumberOrNull(
4031
+ btcVsAltReturn4h: toFiniteNumberOrNull2(
3832
4032
  primaryBreadth.btcVsAltReturn4h
3833
4033
  ),
3834
- btcVsAltReturn24h: toFiniteNumberOrNull(
4034
+ btcVsAltReturn24h: toFiniteNumberOrNull2(
3835
4035
  primaryBreadth.btcVsAltReturn24h
3836
4036
  ),
3837
- btcTurnoverShare1h: toFiniteNumberOrNull(
4037
+ btcTurnoverShare1h: toFiniteNumberOrNull2(
3838
4038
  primaryBreadth.btcTurnoverShare1h
3839
4039
  ),
3840
- btcTurnoverShare24h: toFiniteNumberOrNull(
4040
+ btcTurnoverShare24h: toFiniteNumberOrNull2(
3841
4041
  primaryBreadth.btcTurnoverShare24h
3842
4042
  ),
3843
- btcTurnoverShareChange24h: toFiniteNumberOrNull(
4043
+ btcTurnoverShareChange24h: toFiniteNumberOrNull2(
3844
4044
  primaryBreadth.btcTurnoverShareChange24h
3845
4045
  ),
3846
- altVolToBtcVol24h: toFiniteNumberOrNull(
4046
+ altVolToBtcVol24h: toFiniteNumberOrNull2(
3847
4047
  primaryBreadth.altVolToBtcVol24h
3848
4048
  ),
3849
- altDispersion24h: toFiniteNumberOrNull(
4049
+ altDispersion24h: toFiniteNumberOrNull2(
3850
4050
  primaryBreadth.altDispersion24h
3851
4051
  ),
3852
4052
  regime: primaryBreadth.btcAltRegime ?? "unknown"
@@ -3863,7 +4063,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3863
4063
  throw error;
3864
4064
  }
3865
4065
  binanceMarketContextUnavailable = true;
3866
- import_logger3.logger.warn(
4066
+ import_logger4.logger.warn(
3867
4067
  "Binance market context disabled after Timescale read failure: %s",
3868
4068
  String(error)
3869
4069
  );
@@ -3873,7 +4073,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3873
4073
 
3874
4074
  // src/strategyHelpers/coinMarketCapContext.ts
3875
4075
  var import_strategies2 = require("@tradejs/core/strategies");
3876
- var import_logger4 = require("@tradejs/infra/logger");
4076
+ var import_logger5 = require("@tradejs/infra/logger");
3877
4077
  var import_marketContext2 = require("@tradejs/infra/timescale/marketContext");
3878
4078
  var DEFAULT_MAX_AGE_MS = 48 * 60 * 6e4;
3879
4079
  var SOURCE_GLOBAL_DAILY = "coinmarketcap_global";
@@ -3911,7 +4111,7 @@ var asInt = (value, fallback) => {
3911
4111
  const parsed = Number.parseInt(String(value ?? ""), 10);
3912
4112
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
3913
4113
  };
3914
- var toFiniteNumberOrNull2 = (value) => {
4114
+ var toFiniteNumberOrNull3 = (value) => {
3915
4115
  const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
3916
4116
  return Number.isFinite(numeric) ? numeric : null;
3917
4117
  };
@@ -4145,12 +4345,12 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4145
4345
  const ethRow = references.get("ETHUSDT") ?? null;
4146
4346
  const previousBtcRow = previousReferences.get("BTCUSDT") ?? null;
4147
4347
  const previousEthRow = previousReferences.get("ETHUSDT") ?? null;
4148
- const btcMarketCapUsd = toFiniteNumberOrNull2(btcRow?.marketCapUsd);
4149
- const ethMarketCapUsd = toFiniteNumberOrNull2(ethRow?.marketCapUsd);
4150
- const previousBtcMarketCapUsd = toFiniteNumberOrNull2(
4348
+ const btcMarketCapUsd = toFiniteNumberOrNull3(btcRow?.marketCapUsd);
4349
+ const ethMarketCapUsd = toFiniteNumberOrNull3(ethRow?.marketCapUsd);
4350
+ const previousBtcMarketCapUsd = toFiniteNumberOrNull3(
4151
4351
  previousBtcRow?.marketCapUsd
4152
4352
  );
4153
- const previousEthMarketCapUsd = toFiniteNumberOrNull2(
4353
+ const previousEthMarketCapUsd = toFiniteNumberOrNull3(
4154
4354
  previousEthRow?.marketCapUsd
4155
4355
  );
4156
4356
  const ethBtcMarketCapRatio = safeDivide(ethMarketCapUsd, btcMarketCapUsd);
@@ -4159,16 +4359,16 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4159
4359
  previousBtcMarketCapUsd
4160
4360
  );
4161
4361
  const ethBtcMarketCapRatioChange24hPct = ethBtcMarketCapRatio != null && previousEthBtcMarketCapRatio != null && previousEthBtcMarketCapRatio > 0 ? (ethBtcMarketCapRatio - previousEthBtcMarketCapRatio) / previousEthBtcMarketCapRatio : null;
4162
- const btcVolumeUsd = toFiniteNumberOrNull2(btcRow?.volumeUsd);
4163
- const ethVolumeUsd = toFiniteNumberOrNull2(ethRow?.volumeUsd);
4362
+ const btcVolumeUsd = toFiniteNumberOrNull3(btcRow?.volumeUsd);
4363
+ const ethVolumeUsd = toFiniteNumberOrNull3(ethRow?.volumeUsd);
4164
4364
  const referenceStale = btcRow?.stale === true || ethRow?.stale === true || !btcRow || !ethRow;
4165
- const btcDominanceChange24hPct = toFiniteNumberOrNull2(
4365
+ const btcDominanceChange24hPct = toFiniteNumberOrNull3(
4166
4366
  globalRow?.btcDominanceChange24hPct
4167
4367
  );
4168
- const altMarketCapChange24hPct = toFiniteNumberOrNull2(
4368
+ const altMarketCapChange24hPct = toFiniteNumberOrNull3(
4169
4369
  globalRow?.altMarketCapChange24hPct
4170
4370
  );
4171
- const altVolumeChange24hPct = toFiniteNumberOrNull2(
4371
+ const altVolumeChange24hPct = toFiniteNumberOrNull3(
4172
4372
  globalRow?.altVolumeChange24hPct
4173
4373
  );
4174
4374
  const altLiquidityRegime = globalRow ? toAltLiquidityRegime({
@@ -4179,19 +4379,19 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4179
4379
  }) : "unknown";
4180
4380
  const exchangeLiquidityRegime = exchangeLiquidityRow ? toExchangeLiquidityRegime({
4181
4381
  stale: exchangeLiquidityRow.stale,
4182
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
4382
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
4183
4383
  exchangeLiquidityRow.totalVolumeChange24hPct
4184
4384
  ),
4185
4385
  fallback: exchangeLiquidityRow.liquidityRegime ?? "unknown"
4186
4386
  }) : "unknown";
4187
4387
  const cmc100Row = indexRows.get("cmc100") ?? null;
4188
4388
  const cmc20Row = indexRows.get("cmc20") ?? null;
4189
- const cmc100Value = toFiniteNumberOrNull2(cmc100Row?.value);
4190
- const cmc20Value = toFiniteNumberOrNull2(cmc20Row?.value);
4191
- const cmc100Change24hPct = toFiniteNumberOrNull2(
4389
+ const cmc100Value = toFiniteNumberOrNull3(cmc100Row?.value);
4390
+ const cmc20Value = toFiniteNumberOrNull3(cmc20Row?.value);
4391
+ const cmc100Change24hPct = toFiniteNumberOrNull3(
4192
4392
  cmc100Row?.valueChange24hPct
4193
4393
  );
4194
- const cmc20Change24hPct = toFiniteNumberOrNull2(cmc20Row?.valueChange24hPct);
4394
+ const cmc20Change24hPct = toFiniteNumberOrNull3(cmc20Row?.valueChange24hPct);
4195
4395
  const cmc20ToCmc100Ratio = safeDivide(cmc20Value, cmc100Value);
4196
4396
  const cmc20ToCmc100RatioChange24hPct = cmc20Change24hPct != null && cmc100Change24hPct != null ? (1 + cmc20Change24hPct) / (1 + cmc100Change24hPct) - 1 : null;
4197
4397
  const indexStale = cmc100Row?.stale === true || cmc20Row?.stale === true || !cmc100Row || !cmc20Row;
@@ -4215,41 +4415,41 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4215
4415
  asOfTs: globalRow.ts.getTime(),
4216
4416
  ageMs: globalRow.ageMs,
4217
4417
  stale: globalRow.stale,
4218
- totalMarketCapUsd: toFiniteNumberOrNull2(
4418
+ totalMarketCapUsd: toFiniteNumberOrNull3(
4219
4419
  globalRow.totalMarketCapUsd
4220
4420
  ),
4221
- totalVolumeUsd: toFiniteNumberOrNull2(
4421
+ totalVolumeUsd: toFiniteNumberOrNull3(
4222
4422
  globalRow.totalVolumeUsd
4223
4423
  ),
4224
- totalVolumeReportedUsd: toFiniteNumberOrNull2(
4424
+ totalVolumeReportedUsd: toFiniteNumberOrNull3(
4225
4425
  globalRow.totalVolumeReportedUsd
4226
4426
  ),
4227
- altMarketCapUsd: toFiniteNumberOrNull2(
4427
+ altMarketCapUsd: toFiniteNumberOrNull3(
4228
4428
  globalRow.altMarketCapUsd
4229
4429
  ),
4230
- altVolumeUsd: toFiniteNumberOrNull2(globalRow.altVolumeUsd),
4231
- altVolumeReportedUsd: toFiniteNumberOrNull2(
4430
+ altVolumeUsd: toFiniteNumberOrNull3(globalRow.altVolumeUsd),
4431
+ altVolumeReportedUsd: toFiniteNumberOrNull3(
4232
4432
  globalRow.altVolumeReportedUsd
4233
4433
  ),
4234
- btcDominancePct: toFiniteNumberOrNull2(
4434
+ btcDominancePct: toFiniteNumberOrNull3(
4235
4435
  globalRow.btcDominancePct
4236
4436
  ),
4237
- ethDominancePct: toFiniteNumberOrNull2(
4437
+ ethDominancePct: toFiniteNumberOrNull3(
4238
4438
  globalRow.ethDominancePct
4239
4439
  ),
4240
4440
  btcDominanceChange24hPct,
4241
- ethDominanceChange24hPct: toFiniteNumberOrNull2(
4441
+ ethDominanceChange24hPct: toFiniteNumberOrNull3(
4242
4442
  globalRow.ethDominanceChange24hPct
4243
4443
  ),
4244
4444
  altMarketCapChange24hPct,
4245
4445
  altVolumeChange24hPct,
4246
- activeCryptocurrencies: toFiniteNumberOrNull2(
4446
+ activeCryptocurrencies: toFiniteNumberOrNull3(
4247
4447
  globalRow.activeCryptocurrencies
4248
4448
  ),
4249
- activeExchanges: toFiniteNumberOrNull2(
4449
+ activeExchanges: toFiniteNumberOrNull3(
4250
4450
  globalRow.activeExchanges
4251
4451
  ),
4252
- activeMarketPairs: toFiniteNumberOrNull2(
4452
+ activeMarketPairs: toFiniteNumberOrNull3(
4253
4453
  globalRow.activeMarketPairs
4254
4454
  ),
4255
4455
  altLiquidityRegime
@@ -4294,22 +4494,22 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4294
4494
  asOfTs: exchangeLiquidityRow.ts.getTime(),
4295
4495
  ageMs: exchangeLiquidityRow.ageMs,
4296
4496
  stale: exchangeLiquidityRow.stale,
4297
- exchangesCount: toFiniteNumberOrNull2(
4497
+ exchangesCount: toFiniteNumberOrNull3(
4298
4498
  exchangeLiquidityRow.exchangesCount
4299
4499
  ),
4300
- totalVolumeUsd: toFiniteNumberOrNull2(
4500
+ totalVolumeUsd: toFiniteNumberOrNull3(
4301
4501
  exchangeLiquidityRow.totalVolumeUsd
4302
4502
  ),
4303
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
4503
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
4304
4504
  exchangeLiquidityRow.totalVolumeChange24hPct
4305
4505
  ),
4306
- binanceVolumeUsd: toFiniteNumberOrNull2(
4506
+ binanceVolumeUsd: toFiniteNumberOrNull3(
4307
4507
  exchangeLiquidityRow.binanceVolumeUsd
4308
4508
  ),
4309
- binanceVolumeShare: toFiniteNumberOrNull2(
4509
+ binanceVolumeShare: toFiniteNumberOrNull3(
4310
4510
  exchangeLiquidityRow.binanceVolumeShare
4311
4511
  ),
4312
- topExchangeVolumeShare: toFiniteNumberOrNull2(
4512
+ topExchangeVolumeShare: toFiniteNumberOrNull3(
4313
4513
  exchangeLiquidityRow.topExchangeVolumeShare
4314
4514
  ),
4315
4515
  liquidityRegime: exchangeLiquidityRegime
@@ -4322,11 +4522,11 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4322
4522
  asOfTs: fearGreedRow.ts.getTime(),
4323
4523
  ageMs: fearGreedRow.ageMs,
4324
4524
  stale: fearGreedRow.stale,
4325
- value: toFiniteNumberOrNull2(fearGreedRow.value),
4326
- valueChange24h: toFiniteNumberOrNull2(
4525
+ value: toFiniteNumberOrNull3(fearGreedRow.value),
4526
+ valueChange24h: toFiniteNumberOrNull3(
4327
4527
  fearGreedRow.valueChange24h
4328
4528
  ),
4329
- valueChange7d: toFiniteNumberOrNull2(
4529
+ valueChange7d: toFiniteNumberOrNull3(
4330
4530
  fearGreedRow.valueChange7d
4331
4531
  ),
4332
4532
  classification: fearGreedRow.classification ?? "Unknown",
@@ -4346,13 +4546,13 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4346
4546
  cmc100Value,
4347
4547
  cmc100Change24hPct,
4348
4548
  cmc100TopConstituentSymbol: cmc100Row?.topConstituentSymbol ?? null,
4349
- cmc100TopConstituentWeightPct: toFiniteNumberOrNull2(
4549
+ cmc100TopConstituentWeightPct: toFiniteNumberOrNull3(
4350
4550
  cmc100Row?.topConstituentWeightPct
4351
4551
  ),
4352
4552
  cmc20Value,
4353
4553
  cmc20Change24hPct,
4354
4554
  cmc20TopConstituentSymbol: cmc20Row?.topConstituentSymbol ?? null,
4355
- cmc20TopConstituentWeightPct: toFiniteNumberOrNull2(
4555
+ cmc20TopConstituentWeightPct: toFiniteNumberOrNull3(
4356
4556
  cmc20Row?.topConstituentWeightPct
4357
4557
  ),
4358
4558
  cmc20ToCmc100Ratio,
@@ -4370,7 +4570,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4370
4570
  throw error;
4371
4571
  }
4372
4572
  coinMarketCapContextUnavailable = true;
4373
- import_logger4.logger.warn(
4573
+ import_logger5.logger.warn(
4374
4574
  "CoinMarketCap context disabled after Timescale read failure: %s",
4375
4575
  String(error)
4376
4576
  );
@@ -4384,7 +4584,7 @@ var import_data2 = require("@tradejs/core/data");
4384
4584
  var import_strategies3 = require("@tradejs/core/strategies");
4385
4585
  var import_constants2 = require("@tradejs/core/constants");
4386
4586
  var import_derivatives = require("@tradejs/infra/timescale/derivatives");
4387
- var import_logger5 = require("@tradejs/infra/logger");
4587
+ var import_logger6 = require("@tradejs/infra/logger");
4388
4588
  var STORED_INTERVALS = ["15m", "1h"];
4389
4589
  var CONTEXT_INTERVALS = ["15m", "1h"];
4390
4590
  var DEFAULT_LOOKBACK_HOURS = 48;
@@ -4439,7 +4639,7 @@ var resolvePrimaryReferenceSymbol2 = () => PRIMARY_DERIVATIVES_REFERENCE_SYMBOL;
4439
4639
  var resolveSecondaryReferenceSymbol = () => SECONDARY_DERIVATIVES_REFERENCE_SYMBOL;
4440
4640
  var getPrimaryIntervalContext = (context) => context?.intervals["15m"] ?? context?.intervals["1h"] ?? null;
4441
4641
  var hasDerivativesSymbolData = (context) => Object.keys(context.intervals).length > 0 && !context.summary.riskFlags.includes("missing_derivatives");
4442
- var toFiniteNumberOrNull3 = (value) => {
4642
+ var toFiniteNumberOrNull4 = (value) => {
4443
4643
  if (typeof value === "number" && Number.isFinite(value)) return value;
4444
4644
  if (typeof value === "string" && value.trim()) {
4445
4645
  const parsed = Number(value);
@@ -4453,8 +4653,8 @@ var roundNullable = (value, digits = 4) => {
4453
4653
  return Math.round(value * multiplier) / multiplier;
4454
4654
  };
4455
4655
  var deltaNullable = (targetValue, referenceValue) => {
4456
- const target = toFiniteNumberOrNull3(targetValue);
4457
- const reference = toFiniteNumberOrNull3(referenceValue);
4656
+ const target = toFiniteNumberOrNull4(targetValue);
4657
+ const reference = toFiniteNumberOrNull4(referenceValue);
4458
4658
  return target == null || reference == null ? null : roundNullable(target - reference);
4459
4659
  };
4460
4660
  var buildTargetDerivedContext = (params) => {
@@ -4616,7 +4816,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4616
4816
  throw error;
4617
4817
  }
4618
4818
  derivativesContextUnavailable = true;
4619
- import_logger5.logger.warn(
4819
+ import_logger6.logger.warn(
4620
4820
  "Derivatives context disabled after Timescale read failure: %s",
4621
4821
  String(error)
4622
4822
  );
@@ -4628,7 +4828,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4628
4828
  var import_data3 = require("@tradejs/core/data");
4629
4829
  var import_strategies4 = require("@tradejs/core/strategies");
4630
4830
  var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWhales");
4631
- var import_logger6 = require("@tradejs/infra/logger");
4831
+ var import_logger7 = require("@tradejs/infra/logger");
4632
4832
 
4633
4833
  // src/hyperliquidWhaleUniverse.ts
4634
4834
  var import_node_crypto3 = require("crypto");
@@ -5148,7 +5348,7 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
5148
5348
  throw error;
5149
5349
  }
5150
5350
  hyperliquidWhaleContextUnavailable = true;
5151
- import_logger6.logger.warn(
5351
+ import_logger7.logger.warn(
5152
5352
  "Hyperliquid whale context disabled after Timescale read failure: %s",
5153
5353
  String(error)
5154
5354
  );
@@ -5231,7 +5431,7 @@ var runMarketContextStage = async ({
5231
5431
  elapsedMs: Date.now() - startedAt
5232
5432
  };
5233
5433
  if (status === "timed_out") {
5234
- import_logger7.logger.warn(
5434
+ import_logger8.logger.warn(
5235
5435
  "Market context stage timed out: %s after %sms",
5236
5436
  stage,
5237
5437
  result.elapsedMs
@@ -5421,79 +5621,10 @@ var enrichSignalWithAi = async ({
5421
5621
  signal.aiAnalysis = analysis;
5422
5622
  return resolveAiQuality(analysis, direction);
5423
5623
  } catch (err) {
5424
- import_logger8.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
5624
+ import_logger9.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
5425
5625
  }
5426
5626
  return void 0;
5427
5627
  };
5428
- var toFiniteNumberOrNull4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
5429
- var getOrderArrivalSnapshot = async ({
5430
- connector,
5431
- symbol
5432
- }) => {
5433
- if (typeof connector.getTopOfBookTicker !== "function") {
5434
- return {
5435
- arrivalSnapshotTime: Date.now(),
5436
- arrivalSource: "unavailable",
5437
- bid: null,
5438
- ask: null,
5439
- arrivalMid: null,
5440
- spreadBps: null
5441
- };
5442
- }
5443
- try {
5444
- const ticker = await connector.getTopOfBookTicker(symbol);
5445
- const arrivalSnapshotTime = toFiniteNumberOrNull4(ticker?.timestamp);
5446
- const bid = toFiniteNumberOrNull4(ticker?.bidPrice);
5447
- const ask = toFiniteNumberOrNull4(ticker?.askPrice);
5448
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
5449
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
5450
- return {
5451
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
5452
- arrivalSource: "top_of_book",
5453
- bid,
5454
- ask,
5455
- arrivalMid,
5456
- spreadBps
5457
- };
5458
- } catch (error) {
5459
- import_logger8.logger.warn(
5460
- "runtime order arrival snapshot failed: %s %s",
5461
- symbol,
5462
- error?.message || String(error)
5463
- );
5464
- return {
5465
- arrivalSnapshotTime: Date.now(),
5466
- arrivalSource: "top_of_book_error",
5467
- bid: null,
5468
- ask: null,
5469
- arrivalMid: null,
5470
- spreadBps: null
5471
- };
5472
- }
5473
- };
5474
- var validateEntryProtectionAtArrival = ({
5475
- direction,
5476
- signalPrice,
5477
- bid,
5478
- ask,
5479
- arrivalMid,
5480
- takeProfits,
5481
- stopLossPrice
5482
- }) => {
5483
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
5484
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
5485
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
5486
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
5487
- }
5488
- for (const takeProfit of takeProfits) {
5489
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
5490
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
5491
- }
5492
- }
5493
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
5494
- throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
5495
- }
5496
- };
5497
5628
  var resolveRuntimeTelemetryQuality = ({
5498
5629
  signalClosePrice,
5499
5630
  arrivalMid,
@@ -5570,7 +5701,8 @@ var executeEntryOrder = async ({
5570
5701
  signal,
5571
5702
  beforePlaceOrder,
5572
5703
  recordRuntimeTrade = true,
5573
- leverage
5704
+ leverage,
5705
+ executionLimits
5574
5706
  }) => {
5575
5707
  await beforePlaceOrder?.();
5576
5708
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
@@ -5581,23 +5713,57 @@ var executeEntryOrder = async ({
5581
5713
  signal.orderQty = qty;
5582
5714
  signal.orderValue = qty * currentPrice;
5583
5715
  signal.orderFailureReason = void 0;
5584
- const arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
5585
- try {
5586
- validateEntryProtectionAtArrival({
5587
- direction,
5588
- signalPrice: currentPrice,
5589
- bid: arrivalSnapshot.bid,
5590
- ask: arrivalSnapshot.ask,
5591
- arrivalMid: arrivalSnapshot.arrivalMid,
5592
- takeProfits,
5593
- stopLossPrice
5594
- });
5595
- } catch (error) {
5596
- signal.orderStatus = "failed";
5597
- signal.orderFailureReason = error.message;
5598
- throw error;
5716
+ let arrivalSnapshot = {
5717
+ arrivalSnapshotTime: Date.now(),
5718
+ arrivalSource: "unavailable",
5719
+ bid: null,
5720
+ ask: null,
5721
+ arrivalMid: null,
5722
+ spreadBps: null
5723
+ };
5724
+ let orderSubmitTime = Date.now();
5725
+ const beforeSubmit = createEntrySubmissionValidation({
5726
+ connector,
5727
+ symbol,
5728
+ direction,
5729
+ takeProfits,
5730
+ stopLossPrice,
5731
+ limits: executionLimits,
5732
+ onValidated: (snapshot2, economics2) => {
5733
+ arrivalSnapshot = snapshot2;
5734
+ orderSubmitTime = Date.now();
5735
+ if (executionLimits?.maxLossValue != null) {
5736
+ signal.riskDecision = {
5737
+ stage: "order_plan",
5738
+ status: "approved",
5739
+ reason: "PLANNED_LOSS_WITHIN_MAX_LOSS_VALUE",
5740
+ plannedLossValue: economics2.plannedLossValue,
5741
+ maxLossValue: executionLimits.maxLossValue,
5742
+ limitUtilization: economics2.plannedLossValue / executionLimits.maxLossValue,
5743
+ enforced: true
5744
+ };
5745
+ }
5746
+ }
5747
+ });
5748
+ if (!beforeSubmit) {
5749
+ arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
5750
+ try {
5751
+ validateEntryProtectionAtArrival({
5752
+ direction,
5753
+ signalPrice: currentPrice,
5754
+ bid: arrivalSnapshot.bid,
5755
+ ask: arrivalSnapshot.ask,
5756
+ arrivalMid: arrivalSnapshot.arrivalMid,
5757
+ takeProfits,
5758
+ stopLossPrice
5759
+ });
5760
+ } catch (error) {
5761
+ signal.orderStatus = "failed";
5762
+ signal.orderFailureReason = error.message;
5763
+ throw error;
5764
+ }
5765
+ orderSubmitTime = Date.now();
5599
5766
  }
5600
- const orderSubmitTime = Date.now();
5601
5767
  const orderPlaced = await connector.placeOrder({
5602
5768
  symbol,
5603
5769
  qty,
@@ -5608,7 +5774,8 @@ var executeEntryOrder = async ({
5608
5774
  direction,
5609
5775
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
5610
5776
  orderId,
5611
- signal
5777
+ signal,
5778
+ ...beforeSubmit ? { beforeSubmit } : {}
5612
5779
  });
5613
5780
  const orderAckTime = Date.now();
5614
5781
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -6440,7 +6607,7 @@ var canUseSharedReplayState = ({
6440
6607
  }) => (env === "BACKTEST" || env === "PARITY") && Boolean(sharedReplayKey);
6441
6608
 
6442
6609
  // src/strategy/runtimeExecution.ts
6443
- var import_logger9 = require("@tradejs/infra/logger");
6610
+ var import_logger10 = require("@tradejs/infra/logger");
6444
6611
  var import_types = require("@tradejs/types");
6445
6612
  var buildExitOrderSignal = ({
6446
6613
  strategyName,
@@ -6492,7 +6659,7 @@ var handleExitDecision = async ({
6492
6659
  deploymentId: connector.deploymentId
6493
6660
  });
6494
6661
  if (!activeTrade) {
6495
- import_logger9.logger.warn(
6662
+ import_logger10.logger.warn(
6496
6663
  "[%s] blocked closePosition for untracked runtime position: %s",
6497
6664
  strategyName ?? "unknown",
6498
6665
  symbol
@@ -6500,7 +6667,7 @@ var handleExitDecision = async ({
6500
6667
  return "CLOSE_BLOCKED_BY_UNTRACKED_POSITION";
6501
6668
  }
6502
6669
  if (!strategyName || activeTrade.strategy !== strategyName) {
6503
- import_logger9.logger.warn(
6670
+ import_logger10.logger.warn(
6504
6671
  "[%s] blocked closePosition for foreign runtime position: %s ownedBy=%s",
6505
6672
  strategyName ?? "unknown",
6506
6673
  symbol,
@@ -6553,7 +6720,7 @@ var handleExitDecision = async ({
6553
6720
  exitType: closedTrade?.exitType ?? "exit"
6554
6721
  });
6555
6722
  } catch (notificationError) {
6556
- import_logger9.logger.error(
6723
+ import_logger10.logger.error(
6557
6724
  "runtime close notification error: %s %s",
6558
6725
  symbol,
6559
6726
  notificationError
@@ -6567,7 +6734,7 @@ var handleExitDecision = async ({
6567
6734
  decision,
6568
6735
  market
6569
6736
  });
6570
- import_logger9.logger.error("close order error: %s %s", symbol, err);
6737
+ import_logger10.logger.error("close order error: %s %s", symbol, err);
6571
6738
  return "ORDER_ERROR";
6572
6739
  }
6573
6740
  return decision.code;
@@ -6594,7 +6761,7 @@ var handleProtectDecision = async ({
6594
6761
  decision,
6595
6762
  market
6596
6763
  });
6597
- import_logger9.logger.error("protect position error: %s %s", symbol, err);
6764
+ import_logger10.logger.error("protect position error: %s %s", symbol, err);
6598
6765
  return "ORDER_ERROR";
6599
6766
  }
6600
6767
  return decision.code;
@@ -6660,7 +6827,11 @@ var executeEntryDecision = async ({
6660
6827
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
6661
6828
  signal,
6662
6829
  beforePlaceOrder,
6663
- recordRuntimeTrade: recordRuntimeJournal
6830
+ recordRuntimeTrade: recordRuntimeJournal,
6831
+ executionLimits: resolveEntryExecutionLimits(
6832
+ hookCtx.strategyConfig,
6833
+ decision.entryContext.direction
6834
+ )
6664
6835
  });
6665
6836
  await invokeStageHooks(
6666
6837
  "afterPlaceOrder",
@@ -6682,19 +6853,32 @@ var executeEntryDecision = async ({
6682
6853
  return signal;
6683
6854
  }
6684
6855
  await beforePlaceOrder();
6685
- const arrivalSnapshot = await getOrderArrivalSnapshot({
6856
+ const beforeSubmit = createEntrySubmissionValidation({
6686
6857
  connector,
6687
- symbol
6688
- });
6689
- validateEntryProtectionAtArrival({
6858
+ symbol,
6690
6859
  direction: decision.entryContext.direction,
6691
- signalPrice: decision.entryContext.prices.currentPrice,
6692
- bid: arrivalSnapshot.bid,
6693
- ask: arrivalSnapshot.ask,
6694
- arrivalMid: arrivalSnapshot.arrivalMid,
6695
6860
  takeProfits: decision.orderPlan.takeProfits,
6696
- stopLossPrice: decision.orderPlan.stopLossPrice
6861
+ stopLossPrice: decision.orderPlan.stopLossPrice,
6862
+ limits: resolveEntryExecutionLimits(
6863
+ hookCtx.strategyConfig,
6864
+ decision.entryContext.direction
6865
+ )
6697
6866
  });
6867
+ if (!beforeSubmit) {
6868
+ const arrivalSnapshot = await getOrderArrivalSnapshot({
6869
+ connector,
6870
+ symbol
6871
+ });
6872
+ validateEntryProtectionAtArrival({
6873
+ direction: decision.entryContext.direction,
6874
+ signalPrice: decision.entryContext.prices.currentPrice,
6875
+ bid: arrivalSnapshot.bid,
6876
+ ask: arrivalSnapshot.ask,
6877
+ arrivalMid: arrivalSnapshot.arrivalMid,
6878
+ takeProfits: decision.orderPlan.takeProfits,
6879
+ stopLossPrice: decision.orderPlan.stopLossPrice
6880
+ });
6881
+ }
6698
6882
  const orderPlaced = await connector.placeOrder({
6699
6883
  symbol,
6700
6884
  qty: decision.orderPlan.qty,
@@ -6702,6 +6886,7 @@ var executeEntryDecision = async ({
6702
6886
  timestamp: decision.entryContext.timestamp,
6703
6887
  direction: decision.entryContext.direction,
6704
6888
  positionIntent: decision.orderPlan.positionIntent,
6889
+ ...beforeSubmit ? { beforeSubmit } : {},
6705
6890
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {}
6706
6891
  });
6707
6892
  if (!orderPlaced) {
@@ -6743,6 +6928,23 @@ var executeEntryDecision = async ({
6743
6928
  { decision, entry, market }
6744
6929
  );
6745
6930
  } catch (err) {
6931
+ if (err instanceof EntrySubmissionValidationError) {
6932
+ if (signal) {
6933
+ signal.orderStatus = "skipped";
6934
+ signal.orderSkipReason = err.message;
6935
+ signal.allocatorDecision = {
6936
+ stage: "runtime_admission",
6937
+ status: "rejected",
6938
+ reason: err.message
6939
+ };
6940
+ }
6941
+ import_logger10.logger.warn(
6942
+ "entry rejected before submission: %s %s",
6943
+ symbol,
6944
+ err.message
6945
+ );
6946
+ return signal ?? err.message;
6947
+ }
6746
6948
  if (signal) {
6747
6949
  signal.orderStatus = "failed";
6748
6950
  if (typeof signal.orderFailureReason !== "string" || !signal.orderFailureReason.trim()) {
@@ -6757,9 +6959,9 @@ var executeEntryDecision = async ({
6757
6959
  market
6758
6960
  });
6759
6961
  if (err?.message === import_types.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY) {
6760
- import_logger9.logger.warn("order warning: %s %s", symbol, err);
6962
+ import_logger10.logger.warn("order warning: %s %s", symbol, err);
6761
6963
  } else {
6762
- import_logger9.logger.error("order error: %s %s", symbol, err);
6964
+ import_logger10.logger.error("order error: %s %s", symbol, err);
6763
6965
  }
6764
6966
  return signal ?? "ORDER_ERROR";
6765
6967
  }
@@ -6924,7 +7126,7 @@ var createStrategyRuntime = ({
6924
7126
  try {
6925
7127
  await projectHook(errorParams);
6926
7128
  } catch (hookError) {
6927
- import_logger10.logger.error(
7129
+ import_logger11.logger.error(
6928
7130
  "project hook onRuntimeError failed: %s %s",
6929
7131
  strategyName,
6930
7132
  hookError
@@ -6938,7 +7140,7 @@ var createStrategyRuntime = ({
6938
7140
  try {
6939
7141
  await onRuntimeError(errorParams);
6940
7142
  } catch (hookError) {
6941
- import_logger10.logger.error(
7143
+ import_logger11.logger.error(
6942
7144
  "runtime hook onRuntimeError failed: %s %s",
6943
7145
  strategyName,
6944
7146
  hookError
@@ -6952,7 +7154,7 @@ var createStrategyRuntime = ({
6952
7154
  try {
6953
7155
  return await hook(params);
6954
7156
  } catch (error) {
6955
- import_logger10.logger.error(
7157
+ import_logger11.logger.error(
6956
7158
  'strategy hook "%s" failed for %s: %s',
6957
7159
  stage,
6958
7160
  strategyName,
@@ -7151,8 +7353,11 @@ var createStrategyRuntime = ({
7151
7353
  return enrichedBaseContext;
7152
7354
  }
7153
7355
  });
7356
+ const coreConfig = { ...config };
7357
+ delete coreConfig.RECORD_RUNTIME_TRADES;
7358
+ delete coreConfig.SIMULATE_ORDERS;
7154
7359
  const core = await createCore({
7155
- config,
7360
+ config: coreConfig,
7156
7361
  data,
7157
7362
  strategyApi,
7158
7363
  indicatorsState
@@ -7771,10 +7976,10 @@ var createStrategyRuntime = ({
7771
7976
  setStrategyRuntimeFactory(createStrategyRuntime);
7772
7977
 
7773
7978
  // src/connectorsRegistry.ts
7774
- var import_logger13 = require("@tradejs/infra/logger");
7979
+ var import_logger14 = require("@tradejs/infra/logger");
7775
7980
 
7776
7981
  // src/connectorRuntime.ts
7777
- var import_logger12 = require("@tradejs/infra/logger");
7982
+ var import_logger13 = require("@tradejs/infra/logger");
7778
7983
  var import_tradingAccounts = require("@tradejs/infra/tradingAccounts");
7779
7984
 
7780
7985
  // src/timescaleCachedKline.ts
@@ -7782,7 +7987,7 @@ var import_constants6 = require("@tradejs/core/constants");
7782
7987
  var import_data5 = require("@tradejs/core/data");
7783
7988
  var import_time2 = require("@tradejs/core/time");
7784
7989
  var import_async = require("@tradejs/core/async");
7785
- var import_logger11 = require("@tradejs/infra/logger");
7990
+ var import_logger12 = require("@tradejs/infra/logger");
7786
7991
  var import_candles = require("@tradejs/infra/timescale/candles");
7787
7992
  var DEFAULT_LIMIT = 1e3;
7788
7993
  var DEFAULT_CACHE_FALLBACK_WINDOW = 1e3;
@@ -7892,7 +8097,7 @@ var createTimescaleCachedKline = ({
7892
8097
  }) => {
7893
8098
  const intMinutes = intervalToMinutes(interval);
7894
8099
  if (!intMinutes) {
7895
- import_logger11.logger.log("error", "refreshTail: invalid interval %s", interval);
8100
+ import_logger12.logger.log("error", "refreshTail: invalid interval %s", interval);
7896
8101
  return;
7897
8102
  }
7898
8103
  const intervalMs = intervalMsOf(intMinutes);
@@ -7923,7 +8128,7 @@ var createTimescaleCachedKline = ({
7923
8128
  }) => {
7924
8129
  const intMinutes = intervalToMinutes(interval);
7925
8130
  if (!intMinutes) {
7926
- import_logger11.logger.log("error", "kline: invalid interval %s", interval);
8131
+ import_logger12.logger.log("error", "kline: invalid interval %s", interval);
7927
8132
  return [];
7928
8133
  }
7929
8134
  if (defaultStart !== void 0 && defaultEnd !== void 0 && defaultEnd <= defaultStart) {
@@ -8032,7 +8237,7 @@ var createTimescaleCachedKline = ({
8032
8237
  if (warmOnly) {
8033
8238
  if (isTimescaleFallbackMode) {
8034
8239
  isTimescaleFallbackMode = false;
8035
- import_logger11.logger.log("info", "TimescaleDB connection restored for kline cache");
8240
+ import_logger12.logger.log("info", "TimescaleDB connection restored for kline cache");
8036
8241
  }
8037
8242
  return [];
8038
8243
  }
@@ -8050,13 +8255,13 @@ var createTimescaleCachedKline = ({
8050
8255
  );
8051
8256
  if (isTimescaleFallbackMode) {
8052
8257
  isTimescaleFallbackMode = false;
8053
- import_logger11.logger.log("info", "TimescaleDB connection restored for kline cache");
8258
+ import_logger12.logger.log("info", "TimescaleDB connection restored for kline cache");
8054
8259
  }
8055
8260
  return dbData;
8056
8261
  } catch (error) {
8057
8262
  if (!isTimescaleFallbackMode) {
8058
8263
  isTimescaleFallbackMode = true;
8059
- import_logger11.logger.log(
8264
+ import_logger12.logger.log(
8060
8265
  "warn",
8061
8266
  "TimescaleDB unavailable for %s %s: %s. Falling back to exchange API.",
8062
8267
  symbol,
@@ -8080,7 +8285,7 @@ var createTimescaleCachedKline = ({
8080
8285
 
8081
8286
  // src/connectorRuntime.ts
8082
8287
  var connectorRuntime = {
8083
- logger: import_logger12.logger,
8288
+ logger: import_logger13.logger,
8084
8289
  resolveTradingAccount: import_tradingAccounts.resolveTradingAccount,
8085
8290
  createCachedKline: createTimescaleCachedKline
8086
8291
  };
@@ -8141,7 +8346,7 @@ var normalizeProviders = (providers, connectorName) => {
8141
8346
  var registerProvider = (provider, connectorName, source, providerToConnectorName) => {
8142
8347
  const existing = providerToConnectorName.get(provider);
8143
8348
  if (existing && existing !== connectorName) {
8144
- import_logger13.logger.warn(
8349
+ import_logger14.logger.warn(
8145
8350
  'Skip duplicate connector provider "%s" from %s: already mapped to %s',
8146
8351
  provider,
8147
8352
  source,
@@ -8154,11 +8359,11 @@ var registerProvider = (provider, connectorName, source, providerToConnectorName
8154
8359
  var registerEntry = (entry, source, state) => {
8155
8360
  const connectorName = String(entry?.name ?? "").trim();
8156
8361
  if (!connectorName) {
8157
- import_logger13.logger.warn("Skip connector entry without name from %s", source);
8362
+ import_logger14.logger.warn("Skip connector entry without name from %s", source);
8158
8363
  return;
8159
8364
  }
8160
8365
  if (typeof entry.creator !== "function") {
8161
- import_logger13.logger.warn(
8366
+ import_logger14.logger.warn(
8162
8367
  'Skip connector entry "%s" from %s: creator must be a function',
8163
8368
  connectorName,
8164
8369
  source
@@ -8170,7 +8375,7 @@ var registerEntry = (entry, source, state) => {
8170
8375
  state.connectorCreators
8171
8376
  );
8172
8377
  if (existingByName) {
8173
- import_logger13.logger.warn(
8378
+ import_logger14.logger.warn(
8174
8379
  'Skip duplicate connector "%s" from %s: already registered as %s',
8175
8380
  connectorName,
8176
8381
  source,
@@ -8245,7 +8450,7 @@ var ensureConnectorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
8245
8450
  );
8246
8451
  const pluginDefinition = extractConnectorPluginDefinition(moduleExport);
8247
8452
  if (!pluginDefinition) {
8248
- import_logger13.logger.warn(
8453
+ import_logger14.logger.warn(
8249
8454
  'Skip connector plugin "%s": export { connectorEntries } is missing',
8250
8455
  moduleName
8251
8456
  );
@@ -8253,7 +8458,7 @@ var ensureConnectorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
8253
8458
  }
8254
8459
  registerEntries2(pluginDefinition.connectorEntries, moduleName, state);
8255
8460
  } catch (error) {
8256
- import_logger13.logger.warn(
8461
+ import_logger14.logger.warn(
8257
8462
  'Failed to load connector plugin "%s": %s',
8258
8463
  moduleName,
8259
8464
  String(error)
@@ -8950,7 +9155,10 @@ var createTestConnector = (connector, context) => {
8950
9155
  });
8951
9156
  return {
8952
9157
  __tradejsTestConnector: true,
8953
- capabilities: connector.capabilities,
9158
+ capabilities: {
9159
+ ...connector.capabilities,
9160
+ orderSubmissionValidation: true
9161
+ },
8954
9162
  universe: connector.universe,
8955
9163
  accountId: connector.accountId,
8956
9164
  deploymentId: connector.deploymentId,
@@ -9232,6 +9440,7 @@ var createTestConnector = (connector, context) => {
9232
9440
  }
9233
9441
  },
9234
9442
  placeOrder: async (order) => {
9443
+ const { beforeSubmit, ...submittedOrder } = order;
9235
9444
  const isPositionIncrease = Boolean(
9236
9445
  currentPosition && order.positionIntent === "increase" && currentPosition.direction === order.direction
9237
9446
  );
@@ -9268,6 +9477,7 @@ var createTestConnector = (connector, context) => {
9268
9477
  signal: order.signal
9269
9478
  });
9270
9479
  const previousPosition = currentPosition;
9480
+ await beforeSubmit?.({ qty: orderQty, executionPrice: entryPrice });
9271
9481
  const previousQty = previousPosition?.qty ?? 0;
9272
9482
  const resultingQty = previousQty + orderQty;
9273
9483
  const resultingEntryPrice = previousPosition ? getWeightedAverage(
@@ -9280,7 +9490,7 @@ var createTestConnector = (connector, context) => {
9280
9490
  ...previousPosition,
9281
9491
  qty: resultingQty,
9282
9492
  price: resultingEntryPrice
9283
- } : { ...order, qty: orderQty, price: entryPrice, amount };
9493
+ } : { ...submittedOrder, qty: orderQty, price: entryPrice, amount };
9284
9494
  originalQty = resultingQty;
9285
9495
  if (isPositionIncrease) {
9286
9496
  const { fee: fee2, profit: profit2 } = getNetProfit({
@@ -9380,7 +9590,7 @@ var createTestConnector = (connector, context) => {
9380
9590
  };
9381
9591
  }
9382
9592
  logOrder({
9383
- ...order,
9593
+ ...submittedOrder,
9384
9594
  qty: orderQty,
9385
9595
  price: entryPrice,
9386
9596
  profit: profit2,
@@ -9421,7 +9631,7 @@ var createTestConnector = (connector, context) => {
9421
9631
  currentEntryLegResults = [openTradeResult];
9422
9632
  currentTradeResult = currentSignalId ? { ...openTradeResult, signalId: currentSignalId } : null;
9423
9633
  logOrder({
9424
- ...order,
9634
+ ...submittedOrder,
9425
9635
  qty: orderQty,
9426
9636
  price: entryPrice,
9427
9637
  profit,
@@ -10177,7 +10387,7 @@ var prepareTestingData = async (params) => {
10177
10387
  throw new Error(`Unknown connector: ${connectorName}`);
10178
10388
  }
10179
10389
  if (!binanceConnector || !coinbaseConnector) {
10180
- import_logger14.logger.warn(
10390
+ import_logger15.logger.warn(
10181
10391
  "Binance/Coinbase connectors are unavailable. Reusing %s for BTC references.",
10182
10392
  connectorName
10183
10393
  );