@tradejs/node 3.1.29-beta.288 → 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/registry.js CHANGED
@@ -1045,13 +1045,213 @@ var assessSignalWithJev = async ({
1045
1045
  // src/strategyRuntime.ts
1046
1046
  var import_constants5 = require("@tradejs/core/constants");
1047
1047
  var import_strategies6 = require("@tradejs/core/strategies");
1048
- var import_logger10 = require("@tradejs/infra/logger");
1048
+ var import_logger11 = require("@tradejs/infra/logger");
1049
1049
 
1050
1050
  // src/strategyHelpers/runtime.ts
1051
- var import_logger8 = require("@tradejs/infra/logger");
1051
+ var import_logger9 = require("@tradejs/infra/logger");
1052
1052
  var import_constants3 = require("@tradejs/core/constants");
1053
1053
  var import_ml2 = require("@tradejs/infra/ml");
1054
1054
 
1055
+ // src/strategy/entrySubmissionValidation.ts
1056
+ var import_logger = require("@tradejs/infra/logger");
1057
+
1058
+ // src/strategy/entryExecutionValidation.ts
1059
+ var EntrySubmissionValidationError = class extends Error {
1060
+ constructor() {
1061
+ super(...arguments);
1062
+ this.name = "EntrySubmissionValidationError";
1063
+ }
1064
+ };
1065
+ var resolveEntryExecutionLimits = (config, direction) => {
1066
+ const side = config[direction];
1067
+ const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
1068
+ const maximum = config.MAX_LOSS_VALUE;
1069
+ if (minimum == null && maximum == null) return void 0;
1070
+ const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
1071
+ const maxLossValue = maximum == null ? void 0 : Number(maximum);
1072
+ const feeRate = Number(config.RISK_FEE_RATE ?? 0);
1073
+ const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
1074
+ const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
1075
+ const slippageBps = slippage + impact;
1076
+ 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)
1077
+ throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
1078
+ return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
1079
+ };
1080
+ var validateEntryExecution = ({
1081
+ direction,
1082
+ price,
1083
+ qty,
1084
+ stopLossPrice,
1085
+ takeProfitPrice,
1086
+ limits
1087
+ }) => {
1088
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
1089
+ throw new EntrySubmissionValidationError(
1090
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1091
+ );
1092
+ const side = direction === "LONG" ? 1 : -1;
1093
+ if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
1094
+ throw new EntrySubmissionValidationError(
1095
+ "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
1096
+ );
1097
+ const slippage = limits.slippageBps / 1e4;
1098
+ const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
1099
+ const entryCost = cost(price, side);
1100
+ const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
1101
+ const rewardPerUnit = Math.max(
1102
+ 0,
1103
+ Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
1104
+ );
1105
+ const netRiskRatio = rewardPerUnit / lossPerUnit;
1106
+ const plannedLossValue = qty * lossPerUnit;
1107
+ if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
1108
+ throw new EntrySubmissionValidationError(
1109
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1110
+ );
1111
+ if (limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio)
1112
+ throw new EntrySubmissionValidationError(
1113
+ "ENTRY_RR_BELOW_MIN_AT_SUBMISSION"
1114
+ );
1115
+ if (limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9))
1116
+ throw new EntrySubmissionValidationError(
1117
+ "ENTRY_RISK_EXCEEDS_MAX_LOSS_AT_SUBMISSION"
1118
+ );
1119
+ return { netRiskRatio, plannedLossValue };
1120
+ };
1121
+
1122
+ // src/strategy/entrySubmissionValidation.ts
1123
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
1124
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
1125
+ var getOrderArrivalSnapshot = async ({
1126
+ connector,
1127
+ symbol,
1128
+ requireTimestamp = false
1129
+ }) => {
1130
+ if (typeof connector.getTopOfBookTicker !== "function") {
1131
+ return {
1132
+ arrivalSnapshotTime: Date.now(),
1133
+ arrivalSource: "unavailable",
1134
+ bid: null,
1135
+ ask: null,
1136
+ arrivalMid: null,
1137
+ spreadBps: null
1138
+ };
1139
+ }
1140
+ try {
1141
+ const ticker = await connector.getTopOfBookTicker(symbol);
1142
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
1143
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
1144
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
1145
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
1146
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
1147
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
1148
+ return {
1149
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
1150
+ arrivalSource: "top_of_book",
1151
+ bid,
1152
+ ask,
1153
+ arrivalMid,
1154
+ spreadBps
1155
+ };
1156
+ } catch (error) {
1157
+ import_logger.logger.warn(
1158
+ "runtime order arrival snapshot failed: %s %s",
1159
+ symbol,
1160
+ error?.message || String(error)
1161
+ );
1162
+ return {
1163
+ arrivalSnapshotTime: Date.now(),
1164
+ arrivalSource: "top_of_book_error",
1165
+ bid: null,
1166
+ ask: null,
1167
+ arrivalMid: null,
1168
+ spreadBps: null
1169
+ };
1170
+ }
1171
+ };
1172
+ var validateEntryProtectionAtArrival = ({
1173
+ direction,
1174
+ signalPrice,
1175
+ bid,
1176
+ ask,
1177
+ arrivalMid,
1178
+ takeProfits,
1179
+ stopLossPrice
1180
+ }) => {
1181
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
1182
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
1183
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
1184
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
1185
+ }
1186
+ for (const takeProfit of takeProfits) {
1187
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
1188
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
1189
+ }
1190
+ }
1191
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
1192
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
1193
+ }
1194
+ };
1195
+ var createEntrySubmissionValidation = ({
1196
+ connector,
1197
+ symbol,
1198
+ direction,
1199
+ takeProfits,
1200
+ stopLossPrice,
1201
+ limits,
1202
+ onValidated
1203
+ }) => {
1204
+ if (!limits) return void 0;
1205
+ if (!connector.capabilities?.orderSubmissionValidation)
1206
+ throw new EntrySubmissionValidationError(
1207
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
1208
+ );
1209
+ return async ({ qty, executionPrice }) => {
1210
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
1211
+ const snapshot2 = simulated && executionPrice != null ? {
1212
+ arrivalSnapshotTime: Date.now(),
1213
+ arrivalSource: "simulation",
1214
+ bid: executionPrice,
1215
+ ask: executionPrice,
1216
+ arrivalMid: executionPrice,
1217
+ spreadBps: 0
1218
+ } : await getOrderArrivalSnapshot({
1219
+ connector,
1220
+ symbol,
1221
+ requireTimestamp: true
1222
+ });
1223
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
1224
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
1225
+ throw new EntrySubmissionValidationError(
1226
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
1227
+ );
1228
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
1229
+ throw new EntrySubmissionValidationError(
1230
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
1231
+ );
1232
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
1233
+ validateEntryProtectionAtArrival({
1234
+ direction,
1235
+ signalPrice: price,
1236
+ bid: snapshot2.bid,
1237
+ ask: snapshot2.ask,
1238
+ arrivalMid: snapshot2.arrivalMid,
1239
+ takeProfits,
1240
+ stopLossPrice
1241
+ });
1242
+ const prices = takeProfits.map((tp) => tp.price);
1243
+ const economics2 = validateEntryExecution({
1244
+ direction,
1245
+ price,
1246
+ qty,
1247
+ stopLossPrice,
1248
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
1249
+ limits
1250
+ });
1251
+ onValidated?.(snapshot2, economics2);
1252
+ };
1253
+ };
1254
+
1055
1255
  // src/ai.ts
1056
1256
  var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
1057
1257
  var import_redis = require("@tradejs/infra/redis");
@@ -1636,7 +1836,7 @@ var import_fs = __toESM(require("fs"));
1636
1836
  var import_path = __toESM(require("path"));
1637
1837
  var import_url = require("url");
1638
1838
  var import_config = require("@tradejs/core/config");
1639
- var import_logger = require("@tradejs/infra/logger");
1839
+ var import_logger2 = require("@tradejs/infra/logger");
1640
1840
  var CONFIG_FILE_NAMES = [
1641
1841
  "tradejs.config.ts",
1642
1842
  "tradejs.config.mts",
@@ -1903,11 +2103,11 @@ var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
1903
2103
  cachedByCwd.set(cwd, config);
1904
2104
  if (!announcedConfigFile.has(configFilePath)) {
1905
2105
  announcedConfigFile.add(configFilePath);
1906
- import_logger.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
2106
+ import_logger2.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
1907
2107
  }
1908
2108
  return config;
1909
2109
  } catch (error) {
1910
- import_logger.logger.log(
2110
+ import_logger2.logger.log(
1911
2111
  "warn",
1912
2112
  "Failed to load TradeJS config from %s: %s",
1913
2113
  configFilePath,
@@ -2897,7 +3097,7 @@ var import_node_crypto = require("crypto");
2897
3097
  var import_constants = require("@tradejs/core/constants");
2898
3098
  var import_time = require("@tradejs/core/time");
2899
3099
  var import_trade = require("@tradejs/core/trade");
2900
- var import_logger2 = require("@tradejs/infra/logger");
3100
+ var import_logger3 = require("@tradejs/infra/logger");
2901
3101
  var import_redis2 = require("@tradejs/infra/redis");
2902
3102
  var now = () => Date.now();
2903
3103
  var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
@@ -2956,7 +3156,7 @@ var recordRuntimeTradeOpen = async (params) => {
2956
3156
  )
2957
3157
  ]);
2958
3158
  } catch (error) {
2959
- import_logger2.logger.error(
3159
+ import_logger3.logger.error(
2960
3160
  "runtime trade open journal failed: %s %s",
2961
3161
  record.symbol,
2962
3162
  error?.message || String(error)
@@ -3024,7 +3224,7 @@ var recordRuntimeTradeIncrease = async (params) => {
3024
3224
  )
3025
3225
  ]);
3026
3226
  } catch (error) {
3027
- import_logger2.logger.error(
3227
+ import_logger3.logger.error(
3028
3228
  "runtime trade increase journal failed: %s %s",
3029
3229
  symbol,
3030
3230
  error?.message || String(error)
@@ -3131,7 +3331,7 @@ var markRuntimeTradeClosed = async (params) => {
3131
3331
  )
3132
3332
  ]);
3133
3333
  } catch (error) {
3134
- import_logger2.logger.error(
3334
+ import_logger3.logger.error(
3135
3335
  "runtime trade close journal failed: %s %s",
3136
3336
  symbol,
3137
3337
  error?.message || String(error)
@@ -3141,11 +3341,11 @@ var markRuntimeTradeClosed = async (params) => {
3141
3341
  };
3142
3342
 
3143
3343
  // src/strategyHelpers/marketContextStages.ts
3144
- var import_logger7 = require("@tradejs/infra/logger");
3344
+ var import_logger8 = require("@tradejs/infra/logger");
3145
3345
 
3146
3346
  // src/strategyHelpers/binanceMarketContext.ts
3147
3347
  var import_marketContext = require("@tradejs/infra/timescale/marketContext");
3148
- var import_logger3 = require("@tradejs/infra/logger");
3348
+ var import_logger4 = require("@tradejs/infra/logger");
3149
3349
  var import_strategies = require("@tradejs/core/strategies");
3150
3350
 
3151
3351
  // src/binanceBreadthUniverses.ts
@@ -3509,7 +3709,7 @@ var parseEnabledFlag = (value, env) => {
3509
3709
  if (normalized === "live") return env !== "BACKTEST";
3510
3710
  return false;
3511
3711
  };
3512
- var toFiniteNumberOrNull = (value) => {
3712
+ var toFiniteNumberOrNull2 = (value) => {
3513
3713
  const numeric = typeof value === "number" ? value : Number(value);
3514
3714
  return Number.isFinite(numeric) ? numeric : null;
3515
3715
  };
@@ -3634,14 +3834,14 @@ var toTradeFlowContext = (row, interval) => row ? {
3634
3834
  asOfTs: row.ts.getTime(),
3635
3835
  ageMs: row.ageMs,
3636
3836
  stale: row.stale,
3637
- trades: toFiniteNumberOrNull(row.trades),
3638
- buyPressurePct: toFiniteNumberOrNull(row.buyPressurePct),
3639
- buyBaseVolume: toFiniteNumberOrNull(row.buyBaseVolume),
3640
- sellBaseVolume: toFiniteNumberOrNull(row.sellBaseVolume),
3641
- buyQuoteVolume: toFiniteNumberOrNull(row.buyQuoteVolume),
3642
- sellQuoteVolume: toFiniteNumberOrNull(row.sellQuoteVolume),
3643
- netBaseDelta: toFiniteNumberOrNull(row.netBaseDelta),
3644
- netQuoteDelta: toFiniteNumberOrNull(row.netQuoteDelta)
3837
+ trades: toFiniteNumberOrNull2(row.trades),
3838
+ buyPressurePct: toFiniteNumberOrNull2(row.buyPressurePct),
3839
+ buyBaseVolume: toFiniteNumberOrNull2(row.buyBaseVolume),
3840
+ sellBaseVolume: toFiniteNumberOrNull2(row.sellBaseVolume),
3841
+ buyQuoteVolume: toFiniteNumberOrNull2(row.buyQuoteVolume),
3842
+ sellQuoteVolume: toFiniteNumberOrNull2(row.sellQuoteVolume),
3843
+ netBaseDelta: toFiniteNumberOrNull2(row.netBaseDelta),
3844
+ netQuoteDelta: toFiniteNumberOrNull2(row.netQuoteDelta)
3645
3845
  } : null;
3646
3846
  var getCachedReferenceRows = ({
3647
3847
  referenceSymbols,
@@ -3730,16 +3930,16 @@ var toMarketBreadthContext = (breadth, interval) => ({
3730
3930
  asOfTs: breadth.ts.getTime(),
3731
3931
  ageMs: breadth.ageMs,
3732
3932
  stale: breadth.stale,
3733
- symbolsCount: toFiniteNumberOrNull(breadth.symbolsCount),
3734
- advancers: toFiniteNumberOrNull(breadth.advancers),
3735
- decliners: toFiniteNumberOrNull(breadth.decliners),
3736
- unchanged: toFiniteNumberOrNull(breadth.unchanged),
3737
- advanceDeclineRatio: toFiniteNumberOrNull(breadth.advanceDeclineRatio),
3738
- pctAboveMa20: toFiniteNumberOrNull(breadth.pctAboveMa20),
3739
- pctAboveMa50: toFiniteNumberOrNull(breadth.pctAboveMa50),
3740
- equalWeightedReturn: toFiniteNumberOrNull(breadth.equalWeightedReturn),
3741
- volumeWeightedReturn: toFiniteNumberOrNull(breadth.volumeWeightedReturn),
3742
- dispersion: toFiniteNumberOrNull(breadth.dispersion)
3933
+ symbolsCount: toFiniteNumberOrNull2(breadth.symbolsCount),
3934
+ advancers: toFiniteNumberOrNull2(breadth.advancers),
3935
+ decliners: toFiniteNumberOrNull2(breadth.decliners),
3936
+ unchanged: toFiniteNumberOrNull2(breadth.unchanged),
3937
+ advanceDeclineRatio: toFiniteNumberOrNull2(breadth.advanceDeclineRatio),
3938
+ pctAboveMa20: toFiniteNumberOrNull2(breadth.pctAboveMa20),
3939
+ pctAboveMa50: toFiniteNumberOrNull2(breadth.pctAboveMa50),
3940
+ equalWeightedReturn: toFiniteNumberOrNull2(breadth.equalWeightedReturn),
3941
+ volumeWeightedReturn: toFiniteNumberOrNull2(breadth.volumeWeightedReturn),
3942
+ dispersion: toFiniteNumberOrNull2(breadth.dispersion)
3743
3943
  });
3744
3944
  var enrichSignalWithBinanceMarketContext = async (params) => {
3745
3945
  const {
@@ -3840,46 +4040,46 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3840
4040
  asOfTs: primaryBreadth.ts.getTime(),
3841
4041
  ageMs: primaryBreadth.ageMs,
3842
4042
  stale: primaryBreadth.stale,
3843
- btcReturn1h: toFiniteNumberOrNull(
4043
+ btcReturn1h: toFiniteNumberOrNull2(
3844
4044
  primaryBreadth.btcReturn1h
3845
4045
  ),
3846
- btcReturn4h: toFiniteNumberOrNull(
4046
+ btcReturn4h: toFiniteNumberOrNull2(
3847
4047
  primaryBreadth.btcReturn4h
3848
4048
  ),
3849
- btcReturn24h: toFiniteNumberOrNull(
4049
+ btcReturn24h: toFiniteNumberOrNull2(
3850
4050
  primaryBreadth.btcReturn24h
3851
4051
  ),
3852
- altBasketReturn1h: toFiniteNumberOrNull(
4052
+ altBasketReturn1h: toFiniteNumberOrNull2(
3853
4053
  primaryBreadth.altBasketReturn1h
3854
4054
  ),
3855
- altBasketReturn4h: toFiniteNumberOrNull(
4055
+ altBasketReturn4h: toFiniteNumberOrNull2(
3856
4056
  primaryBreadth.altBasketReturn4h
3857
4057
  ),
3858
- altBasketReturn24h: toFiniteNumberOrNull(
4058
+ altBasketReturn24h: toFiniteNumberOrNull2(
3859
4059
  primaryBreadth.altBasketReturn24h
3860
4060
  ),
3861
- btcVsAltReturn1h: toFiniteNumberOrNull(
4061
+ btcVsAltReturn1h: toFiniteNumberOrNull2(
3862
4062
  primaryBreadth.btcVsAltReturn1h
3863
4063
  ),
3864
- btcVsAltReturn4h: toFiniteNumberOrNull(
4064
+ btcVsAltReturn4h: toFiniteNumberOrNull2(
3865
4065
  primaryBreadth.btcVsAltReturn4h
3866
4066
  ),
3867
- btcVsAltReturn24h: toFiniteNumberOrNull(
4067
+ btcVsAltReturn24h: toFiniteNumberOrNull2(
3868
4068
  primaryBreadth.btcVsAltReturn24h
3869
4069
  ),
3870
- btcTurnoverShare1h: toFiniteNumberOrNull(
4070
+ btcTurnoverShare1h: toFiniteNumberOrNull2(
3871
4071
  primaryBreadth.btcTurnoverShare1h
3872
4072
  ),
3873
- btcTurnoverShare24h: toFiniteNumberOrNull(
4073
+ btcTurnoverShare24h: toFiniteNumberOrNull2(
3874
4074
  primaryBreadth.btcTurnoverShare24h
3875
4075
  ),
3876
- btcTurnoverShareChange24h: toFiniteNumberOrNull(
4076
+ btcTurnoverShareChange24h: toFiniteNumberOrNull2(
3877
4077
  primaryBreadth.btcTurnoverShareChange24h
3878
4078
  ),
3879
- altVolToBtcVol24h: toFiniteNumberOrNull(
4079
+ altVolToBtcVol24h: toFiniteNumberOrNull2(
3880
4080
  primaryBreadth.altVolToBtcVol24h
3881
4081
  ),
3882
- altDispersion24h: toFiniteNumberOrNull(
4082
+ altDispersion24h: toFiniteNumberOrNull2(
3883
4083
  primaryBreadth.altDispersion24h
3884
4084
  ),
3885
4085
  regime: primaryBreadth.btcAltRegime ?? "unknown"
@@ -3896,7 +4096,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3896
4096
  throw error;
3897
4097
  }
3898
4098
  binanceMarketContextUnavailable = true;
3899
- import_logger3.logger.warn(
4099
+ import_logger4.logger.warn(
3900
4100
  "Binance market context disabled after Timescale read failure: %s",
3901
4101
  String(error)
3902
4102
  );
@@ -3906,7 +4106,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3906
4106
 
3907
4107
  // src/strategyHelpers/coinMarketCapContext.ts
3908
4108
  var import_strategies2 = require("@tradejs/core/strategies");
3909
- var import_logger4 = require("@tradejs/infra/logger");
4109
+ var import_logger5 = require("@tradejs/infra/logger");
3910
4110
  var import_marketContext2 = require("@tradejs/infra/timescale/marketContext");
3911
4111
  var DEFAULT_MAX_AGE_MS = 48 * 60 * 6e4;
3912
4112
  var SOURCE_GLOBAL_DAILY = "coinmarketcap_global";
@@ -3944,7 +4144,7 @@ var asInt = (value, fallback) => {
3944
4144
  const parsed = Number.parseInt(String(value ?? ""), 10);
3945
4145
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
3946
4146
  };
3947
- var toFiniteNumberOrNull2 = (value) => {
4147
+ var toFiniteNumberOrNull3 = (value) => {
3948
4148
  const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
3949
4149
  return Number.isFinite(numeric) ? numeric : null;
3950
4150
  };
@@ -4178,12 +4378,12 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4178
4378
  const ethRow = references.get("ETHUSDT") ?? null;
4179
4379
  const previousBtcRow = previousReferences.get("BTCUSDT") ?? null;
4180
4380
  const previousEthRow = previousReferences.get("ETHUSDT") ?? null;
4181
- const btcMarketCapUsd = toFiniteNumberOrNull2(btcRow?.marketCapUsd);
4182
- const ethMarketCapUsd = toFiniteNumberOrNull2(ethRow?.marketCapUsd);
4183
- const previousBtcMarketCapUsd = toFiniteNumberOrNull2(
4381
+ const btcMarketCapUsd = toFiniteNumberOrNull3(btcRow?.marketCapUsd);
4382
+ const ethMarketCapUsd = toFiniteNumberOrNull3(ethRow?.marketCapUsd);
4383
+ const previousBtcMarketCapUsd = toFiniteNumberOrNull3(
4184
4384
  previousBtcRow?.marketCapUsd
4185
4385
  );
4186
- const previousEthMarketCapUsd = toFiniteNumberOrNull2(
4386
+ const previousEthMarketCapUsd = toFiniteNumberOrNull3(
4187
4387
  previousEthRow?.marketCapUsd
4188
4388
  );
4189
4389
  const ethBtcMarketCapRatio = safeDivide(ethMarketCapUsd, btcMarketCapUsd);
@@ -4192,16 +4392,16 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4192
4392
  previousBtcMarketCapUsd
4193
4393
  );
4194
4394
  const ethBtcMarketCapRatioChange24hPct = ethBtcMarketCapRatio != null && previousEthBtcMarketCapRatio != null && previousEthBtcMarketCapRatio > 0 ? (ethBtcMarketCapRatio - previousEthBtcMarketCapRatio) / previousEthBtcMarketCapRatio : null;
4195
- const btcVolumeUsd = toFiniteNumberOrNull2(btcRow?.volumeUsd);
4196
- const ethVolumeUsd = toFiniteNumberOrNull2(ethRow?.volumeUsd);
4395
+ const btcVolumeUsd = toFiniteNumberOrNull3(btcRow?.volumeUsd);
4396
+ const ethVolumeUsd = toFiniteNumberOrNull3(ethRow?.volumeUsd);
4197
4397
  const referenceStale = btcRow?.stale === true || ethRow?.stale === true || !btcRow || !ethRow;
4198
- const btcDominanceChange24hPct = toFiniteNumberOrNull2(
4398
+ const btcDominanceChange24hPct = toFiniteNumberOrNull3(
4199
4399
  globalRow?.btcDominanceChange24hPct
4200
4400
  );
4201
- const altMarketCapChange24hPct = toFiniteNumberOrNull2(
4401
+ const altMarketCapChange24hPct = toFiniteNumberOrNull3(
4202
4402
  globalRow?.altMarketCapChange24hPct
4203
4403
  );
4204
- const altVolumeChange24hPct = toFiniteNumberOrNull2(
4404
+ const altVolumeChange24hPct = toFiniteNumberOrNull3(
4205
4405
  globalRow?.altVolumeChange24hPct
4206
4406
  );
4207
4407
  const altLiquidityRegime = globalRow ? toAltLiquidityRegime({
@@ -4212,19 +4412,19 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4212
4412
  }) : "unknown";
4213
4413
  const exchangeLiquidityRegime = exchangeLiquidityRow ? toExchangeLiquidityRegime({
4214
4414
  stale: exchangeLiquidityRow.stale,
4215
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
4415
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
4216
4416
  exchangeLiquidityRow.totalVolumeChange24hPct
4217
4417
  ),
4218
4418
  fallback: exchangeLiquidityRow.liquidityRegime ?? "unknown"
4219
4419
  }) : "unknown";
4220
4420
  const cmc100Row = indexRows.get("cmc100") ?? null;
4221
4421
  const cmc20Row = indexRows.get("cmc20") ?? null;
4222
- const cmc100Value = toFiniteNumberOrNull2(cmc100Row?.value);
4223
- const cmc20Value = toFiniteNumberOrNull2(cmc20Row?.value);
4224
- const cmc100Change24hPct = toFiniteNumberOrNull2(
4422
+ const cmc100Value = toFiniteNumberOrNull3(cmc100Row?.value);
4423
+ const cmc20Value = toFiniteNumberOrNull3(cmc20Row?.value);
4424
+ const cmc100Change24hPct = toFiniteNumberOrNull3(
4225
4425
  cmc100Row?.valueChange24hPct
4226
4426
  );
4227
- const cmc20Change24hPct = toFiniteNumberOrNull2(cmc20Row?.valueChange24hPct);
4427
+ const cmc20Change24hPct = toFiniteNumberOrNull3(cmc20Row?.valueChange24hPct);
4228
4428
  const cmc20ToCmc100Ratio = safeDivide(cmc20Value, cmc100Value);
4229
4429
  const cmc20ToCmc100RatioChange24hPct = cmc20Change24hPct != null && cmc100Change24hPct != null ? (1 + cmc20Change24hPct) / (1 + cmc100Change24hPct) - 1 : null;
4230
4430
  const indexStale = cmc100Row?.stale === true || cmc20Row?.stale === true || !cmc100Row || !cmc20Row;
@@ -4248,41 +4448,41 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4248
4448
  asOfTs: globalRow.ts.getTime(),
4249
4449
  ageMs: globalRow.ageMs,
4250
4450
  stale: globalRow.stale,
4251
- totalMarketCapUsd: toFiniteNumberOrNull2(
4451
+ totalMarketCapUsd: toFiniteNumberOrNull3(
4252
4452
  globalRow.totalMarketCapUsd
4253
4453
  ),
4254
- totalVolumeUsd: toFiniteNumberOrNull2(
4454
+ totalVolumeUsd: toFiniteNumberOrNull3(
4255
4455
  globalRow.totalVolumeUsd
4256
4456
  ),
4257
- totalVolumeReportedUsd: toFiniteNumberOrNull2(
4457
+ totalVolumeReportedUsd: toFiniteNumberOrNull3(
4258
4458
  globalRow.totalVolumeReportedUsd
4259
4459
  ),
4260
- altMarketCapUsd: toFiniteNumberOrNull2(
4460
+ altMarketCapUsd: toFiniteNumberOrNull3(
4261
4461
  globalRow.altMarketCapUsd
4262
4462
  ),
4263
- altVolumeUsd: toFiniteNumberOrNull2(globalRow.altVolumeUsd),
4264
- altVolumeReportedUsd: toFiniteNumberOrNull2(
4463
+ altVolumeUsd: toFiniteNumberOrNull3(globalRow.altVolumeUsd),
4464
+ altVolumeReportedUsd: toFiniteNumberOrNull3(
4265
4465
  globalRow.altVolumeReportedUsd
4266
4466
  ),
4267
- btcDominancePct: toFiniteNumberOrNull2(
4467
+ btcDominancePct: toFiniteNumberOrNull3(
4268
4468
  globalRow.btcDominancePct
4269
4469
  ),
4270
- ethDominancePct: toFiniteNumberOrNull2(
4470
+ ethDominancePct: toFiniteNumberOrNull3(
4271
4471
  globalRow.ethDominancePct
4272
4472
  ),
4273
4473
  btcDominanceChange24hPct,
4274
- ethDominanceChange24hPct: toFiniteNumberOrNull2(
4474
+ ethDominanceChange24hPct: toFiniteNumberOrNull3(
4275
4475
  globalRow.ethDominanceChange24hPct
4276
4476
  ),
4277
4477
  altMarketCapChange24hPct,
4278
4478
  altVolumeChange24hPct,
4279
- activeCryptocurrencies: toFiniteNumberOrNull2(
4479
+ activeCryptocurrencies: toFiniteNumberOrNull3(
4280
4480
  globalRow.activeCryptocurrencies
4281
4481
  ),
4282
- activeExchanges: toFiniteNumberOrNull2(
4482
+ activeExchanges: toFiniteNumberOrNull3(
4283
4483
  globalRow.activeExchanges
4284
4484
  ),
4285
- activeMarketPairs: toFiniteNumberOrNull2(
4485
+ activeMarketPairs: toFiniteNumberOrNull3(
4286
4486
  globalRow.activeMarketPairs
4287
4487
  ),
4288
4488
  altLiquidityRegime
@@ -4327,22 +4527,22 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4327
4527
  asOfTs: exchangeLiquidityRow.ts.getTime(),
4328
4528
  ageMs: exchangeLiquidityRow.ageMs,
4329
4529
  stale: exchangeLiquidityRow.stale,
4330
- exchangesCount: toFiniteNumberOrNull2(
4530
+ exchangesCount: toFiniteNumberOrNull3(
4331
4531
  exchangeLiquidityRow.exchangesCount
4332
4532
  ),
4333
- totalVolumeUsd: toFiniteNumberOrNull2(
4533
+ totalVolumeUsd: toFiniteNumberOrNull3(
4334
4534
  exchangeLiquidityRow.totalVolumeUsd
4335
4535
  ),
4336
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
4536
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
4337
4537
  exchangeLiquidityRow.totalVolumeChange24hPct
4338
4538
  ),
4339
- binanceVolumeUsd: toFiniteNumberOrNull2(
4539
+ binanceVolumeUsd: toFiniteNumberOrNull3(
4340
4540
  exchangeLiquidityRow.binanceVolumeUsd
4341
4541
  ),
4342
- binanceVolumeShare: toFiniteNumberOrNull2(
4542
+ binanceVolumeShare: toFiniteNumberOrNull3(
4343
4543
  exchangeLiquidityRow.binanceVolumeShare
4344
4544
  ),
4345
- topExchangeVolumeShare: toFiniteNumberOrNull2(
4545
+ topExchangeVolumeShare: toFiniteNumberOrNull3(
4346
4546
  exchangeLiquidityRow.topExchangeVolumeShare
4347
4547
  ),
4348
4548
  liquidityRegime: exchangeLiquidityRegime
@@ -4355,11 +4555,11 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4355
4555
  asOfTs: fearGreedRow.ts.getTime(),
4356
4556
  ageMs: fearGreedRow.ageMs,
4357
4557
  stale: fearGreedRow.stale,
4358
- value: toFiniteNumberOrNull2(fearGreedRow.value),
4359
- valueChange24h: toFiniteNumberOrNull2(
4558
+ value: toFiniteNumberOrNull3(fearGreedRow.value),
4559
+ valueChange24h: toFiniteNumberOrNull3(
4360
4560
  fearGreedRow.valueChange24h
4361
4561
  ),
4362
- valueChange7d: toFiniteNumberOrNull2(
4562
+ valueChange7d: toFiniteNumberOrNull3(
4363
4563
  fearGreedRow.valueChange7d
4364
4564
  ),
4365
4565
  classification: fearGreedRow.classification ?? "Unknown",
@@ -4379,13 +4579,13 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4379
4579
  cmc100Value,
4380
4580
  cmc100Change24hPct,
4381
4581
  cmc100TopConstituentSymbol: cmc100Row?.topConstituentSymbol ?? null,
4382
- cmc100TopConstituentWeightPct: toFiniteNumberOrNull2(
4582
+ cmc100TopConstituentWeightPct: toFiniteNumberOrNull3(
4383
4583
  cmc100Row?.topConstituentWeightPct
4384
4584
  ),
4385
4585
  cmc20Value,
4386
4586
  cmc20Change24hPct,
4387
4587
  cmc20TopConstituentSymbol: cmc20Row?.topConstituentSymbol ?? null,
4388
- cmc20TopConstituentWeightPct: toFiniteNumberOrNull2(
4588
+ cmc20TopConstituentWeightPct: toFiniteNumberOrNull3(
4389
4589
  cmc20Row?.topConstituentWeightPct
4390
4590
  ),
4391
4591
  cmc20ToCmc100Ratio,
@@ -4403,7 +4603,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4403
4603
  throw error;
4404
4604
  }
4405
4605
  coinMarketCapContextUnavailable = true;
4406
- import_logger4.logger.warn(
4606
+ import_logger5.logger.warn(
4407
4607
  "CoinMarketCap context disabled after Timescale read failure: %s",
4408
4608
  String(error)
4409
4609
  );
@@ -4417,7 +4617,7 @@ var import_data2 = require("@tradejs/core/data");
4417
4617
  var import_strategies3 = require("@tradejs/core/strategies");
4418
4618
  var import_constants2 = require("@tradejs/core/constants");
4419
4619
  var import_derivatives = require("@tradejs/infra/timescale/derivatives");
4420
- var import_logger5 = require("@tradejs/infra/logger");
4620
+ var import_logger6 = require("@tradejs/infra/logger");
4421
4621
  var STORED_INTERVALS = ["15m", "1h"];
4422
4622
  var CONTEXT_INTERVALS = ["15m", "1h"];
4423
4623
  var DEFAULT_LOOKBACK_HOURS = 48;
@@ -4472,7 +4672,7 @@ var resolvePrimaryReferenceSymbol2 = () => PRIMARY_DERIVATIVES_REFERENCE_SYMBOL;
4472
4672
  var resolveSecondaryReferenceSymbol = () => SECONDARY_DERIVATIVES_REFERENCE_SYMBOL;
4473
4673
  var getPrimaryIntervalContext = (context) => context?.intervals["15m"] ?? context?.intervals["1h"] ?? null;
4474
4674
  var hasDerivativesSymbolData = (context) => Object.keys(context.intervals).length > 0 && !context.summary.riskFlags.includes("missing_derivatives");
4475
- var toFiniteNumberOrNull3 = (value) => {
4675
+ var toFiniteNumberOrNull4 = (value) => {
4476
4676
  if (typeof value === "number" && Number.isFinite(value)) return value;
4477
4677
  if (typeof value === "string" && value.trim()) {
4478
4678
  const parsed = Number(value);
@@ -4486,8 +4686,8 @@ var roundNullable = (value, digits = 4) => {
4486
4686
  return Math.round(value * multiplier) / multiplier;
4487
4687
  };
4488
4688
  var deltaNullable = (targetValue, referenceValue) => {
4489
- const target = toFiniteNumberOrNull3(targetValue);
4490
- const reference = toFiniteNumberOrNull3(referenceValue);
4689
+ const target = toFiniteNumberOrNull4(targetValue);
4690
+ const reference = toFiniteNumberOrNull4(referenceValue);
4491
4691
  return target == null || reference == null ? null : roundNullable(target - reference);
4492
4692
  };
4493
4693
  var buildTargetDerivedContext = (params) => {
@@ -4649,7 +4849,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4649
4849
  throw error;
4650
4850
  }
4651
4851
  derivativesContextUnavailable = true;
4652
- import_logger5.logger.warn(
4852
+ import_logger6.logger.warn(
4653
4853
  "Derivatives context disabled after Timescale read failure: %s",
4654
4854
  String(error)
4655
4855
  );
@@ -4661,7 +4861,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4661
4861
  var import_data3 = require("@tradejs/core/data");
4662
4862
  var import_strategies4 = require("@tradejs/core/strategies");
4663
4863
  var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWhales");
4664
- var import_logger6 = require("@tradejs/infra/logger");
4864
+ var import_logger7 = require("@tradejs/infra/logger");
4665
4865
 
4666
4866
  // src/hyperliquidWhaleUniverse.ts
4667
4867
  var import_node_crypto3 = require("crypto");
@@ -5181,7 +5381,7 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
5181
5381
  throw error;
5182
5382
  }
5183
5383
  hyperliquidWhaleContextUnavailable = true;
5184
- import_logger6.logger.warn(
5384
+ import_logger7.logger.warn(
5185
5385
  "Hyperliquid whale context disabled after Timescale read failure: %s",
5186
5386
  String(error)
5187
5387
  );
@@ -5264,7 +5464,7 @@ var runMarketContextStage = async ({
5264
5464
  elapsedMs: Date.now() - startedAt
5265
5465
  };
5266
5466
  if (status === "timed_out") {
5267
- import_logger7.logger.warn(
5467
+ import_logger8.logger.warn(
5268
5468
  "Market context stage timed out: %s after %sms",
5269
5469
  stage,
5270
5470
  result.elapsedMs
@@ -5454,79 +5654,10 @@ var enrichSignalWithAi = async ({
5454
5654
  signal.aiAnalysis = analysis;
5455
5655
  return resolveAiQuality(analysis, direction);
5456
5656
  } catch (err) {
5457
- import_logger8.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
5657
+ import_logger9.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
5458
5658
  }
5459
5659
  return void 0;
5460
5660
  };
5461
- var toFiniteNumberOrNull4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
5462
- var getOrderArrivalSnapshot = async ({
5463
- connector,
5464
- symbol
5465
- }) => {
5466
- if (typeof connector.getTopOfBookTicker !== "function") {
5467
- return {
5468
- arrivalSnapshotTime: Date.now(),
5469
- arrivalSource: "unavailable",
5470
- bid: null,
5471
- ask: null,
5472
- arrivalMid: null,
5473
- spreadBps: null
5474
- };
5475
- }
5476
- try {
5477
- const ticker = await connector.getTopOfBookTicker(symbol);
5478
- const arrivalSnapshotTime = toFiniteNumberOrNull4(ticker?.timestamp);
5479
- const bid = toFiniteNumberOrNull4(ticker?.bidPrice);
5480
- const ask = toFiniteNumberOrNull4(ticker?.askPrice);
5481
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
5482
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
5483
- return {
5484
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
5485
- arrivalSource: "top_of_book",
5486
- bid,
5487
- ask,
5488
- arrivalMid,
5489
- spreadBps
5490
- };
5491
- } catch (error) {
5492
- import_logger8.logger.warn(
5493
- "runtime order arrival snapshot failed: %s %s",
5494
- symbol,
5495
- error?.message || String(error)
5496
- );
5497
- return {
5498
- arrivalSnapshotTime: Date.now(),
5499
- arrivalSource: "top_of_book_error",
5500
- bid: null,
5501
- ask: null,
5502
- arrivalMid: null,
5503
- spreadBps: null
5504
- };
5505
- }
5506
- };
5507
- var validateEntryProtectionAtArrival = ({
5508
- direction,
5509
- signalPrice,
5510
- bid,
5511
- ask,
5512
- arrivalMid,
5513
- takeProfits,
5514
- stopLossPrice
5515
- }) => {
5516
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
5517
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
5518
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
5519
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
5520
- }
5521
- for (const takeProfit of takeProfits) {
5522
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
5523
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
5524
- }
5525
- }
5526
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
5527
- throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
5528
- }
5529
- };
5530
5661
  var resolveRuntimeTelemetryQuality = ({
5531
5662
  signalClosePrice,
5532
5663
  arrivalMid,
@@ -5603,7 +5734,8 @@ var executeEntryOrder = async ({
5603
5734
  signal,
5604
5735
  beforePlaceOrder,
5605
5736
  recordRuntimeTrade = true,
5606
- leverage
5737
+ leverage,
5738
+ executionLimits
5607
5739
  }) => {
5608
5740
  await beforePlaceOrder?.();
5609
5741
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
@@ -5614,23 +5746,57 @@ var executeEntryOrder = async ({
5614
5746
  signal.orderQty = qty;
5615
5747
  signal.orderValue = qty * currentPrice;
5616
5748
  signal.orderFailureReason = void 0;
5617
- const arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
5618
- try {
5619
- validateEntryProtectionAtArrival({
5620
- direction,
5621
- signalPrice: currentPrice,
5622
- bid: arrivalSnapshot.bid,
5623
- ask: arrivalSnapshot.ask,
5624
- arrivalMid: arrivalSnapshot.arrivalMid,
5625
- takeProfits,
5626
- stopLossPrice
5627
- });
5628
- } catch (error) {
5629
- signal.orderStatus = "failed";
5630
- signal.orderFailureReason = error.message;
5631
- throw error;
5749
+ let arrivalSnapshot = {
5750
+ arrivalSnapshotTime: Date.now(),
5751
+ arrivalSource: "unavailable",
5752
+ bid: null,
5753
+ ask: null,
5754
+ arrivalMid: null,
5755
+ spreadBps: null
5756
+ };
5757
+ let orderSubmitTime = Date.now();
5758
+ const beforeSubmit = createEntrySubmissionValidation({
5759
+ connector,
5760
+ symbol,
5761
+ direction,
5762
+ takeProfits,
5763
+ stopLossPrice,
5764
+ limits: executionLimits,
5765
+ onValidated: (snapshot2, economics2) => {
5766
+ arrivalSnapshot = snapshot2;
5767
+ orderSubmitTime = Date.now();
5768
+ if (executionLimits?.maxLossValue != null) {
5769
+ signal.riskDecision = {
5770
+ stage: "order_plan",
5771
+ status: "approved",
5772
+ reason: "PLANNED_LOSS_WITHIN_MAX_LOSS_VALUE",
5773
+ plannedLossValue: economics2.plannedLossValue,
5774
+ maxLossValue: executionLimits.maxLossValue,
5775
+ limitUtilization: economics2.plannedLossValue / executionLimits.maxLossValue,
5776
+ enforced: true
5777
+ };
5778
+ }
5779
+ }
5780
+ });
5781
+ if (!beforeSubmit) {
5782
+ arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
5783
+ try {
5784
+ validateEntryProtectionAtArrival({
5785
+ direction,
5786
+ signalPrice: currentPrice,
5787
+ bid: arrivalSnapshot.bid,
5788
+ ask: arrivalSnapshot.ask,
5789
+ arrivalMid: arrivalSnapshot.arrivalMid,
5790
+ takeProfits,
5791
+ stopLossPrice
5792
+ });
5793
+ } catch (error) {
5794
+ signal.orderStatus = "failed";
5795
+ signal.orderFailureReason = error.message;
5796
+ throw error;
5797
+ }
5798
+ orderSubmitTime = Date.now();
5632
5799
  }
5633
- const orderSubmitTime = Date.now();
5634
5800
  const orderPlaced = await connector.placeOrder({
5635
5801
  symbol,
5636
5802
  qty,
@@ -5641,7 +5807,8 @@ var executeEntryOrder = async ({
5641
5807
  direction,
5642
5808
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
5643
5809
  orderId,
5644
- signal
5810
+ signal,
5811
+ ...beforeSubmit ? { beforeSubmit } : {}
5645
5812
  });
5646
5813
  const orderAckTime = Date.now();
5647
5814
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -6470,7 +6637,7 @@ var canUseSharedReplayState = ({
6470
6637
  }) => (env === "BACKTEST" || env === "PARITY") && Boolean(sharedReplayKey);
6471
6638
 
6472
6639
  // src/strategy/runtimeExecution.ts
6473
- var import_logger9 = require("@tradejs/infra/logger");
6640
+ var import_logger10 = require("@tradejs/infra/logger");
6474
6641
  var import_types = require("@tradejs/types");
6475
6642
  var buildExitOrderSignal = ({
6476
6643
  strategyName,
@@ -6522,7 +6689,7 @@ var handleExitDecision = async ({
6522
6689
  deploymentId: connector.deploymentId
6523
6690
  });
6524
6691
  if (!activeTrade) {
6525
- import_logger9.logger.warn(
6692
+ import_logger10.logger.warn(
6526
6693
  "[%s] blocked closePosition for untracked runtime position: %s",
6527
6694
  strategyName ?? "unknown",
6528
6695
  symbol
@@ -6530,7 +6697,7 @@ var handleExitDecision = async ({
6530
6697
  return "CLOSE_BLOCKED_BY_UNTRACKED_POSITION";
6531
6698
  }
6532
6699
  if (!strategyName || activeTrade.strategy !== strategyName) {
6533
- import_logger9.logger.warn(
6700
+ import_logger10.logger.warn(
6534
6701
  "[%s] blocked closePosition for foreign runtime position: %s ownedBy=%s",
6535
6702
  strategyName ?? "unknown",
6536
6703
  symbol,
@@ -6583,7 +6750,7 @@ var handleExitDecision = async ({
6583
6750
  exitType: closedTrade?.exitType ?? "exit"
6584
6751
  });
6585
6752
  } catch (notificationError) {
6586
- import_logger9.logger.error(
6753
+ import_logger10.logger.error(
6587
6754
  "runtime close notification error: %s %s",
6588
6755
  symbol,
6589
6756
  notificationError
@@ -6597,7 +6764,7 @@ var handleExitDecision = async ({
6597
6764
  decision,
6598
6765
  market
6599
6766
  });
6600
- import_logger9.logger.error("close order error: %s %s", symbol, err);
6767
+ import_logger10.logger.error("close order error: %s %s", symbol, err);
6601
6768
  return "ORDER_ERROR";
6602
6769
  }
6603
6770
  return decision.code;
@@ -6624,7 +6791,7 @@ var handleProtectDecision = async ({
6624
6791
  decision,
6625
6792
  market
6626
6793
  });
6627
- import_logger9.logger.error("protect position error: %s %s", symbol, err);
6794
+ import_logger10.logger.error("protect position error: %s %s", symbol, err);
6628
6795
  return "ORDER_ERROR";
6629
6796
  }
6630
6797
  return decision.code;
@@ -6690,7 +6857,11 @@ var executeEntryDecision = async ({
6690
6857
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
6691
6858
  signal,
6692
6859
  beforePlaceOrder,
6693
- recordRuntimeTrade: recordRuntimeJournal
6860
+ recordRuntimeTrade: recordRuntimeJournal,
6861
+ executionLimits: resolveEntryExecutionLimits(
6862
+ hookCtx.strategyConfig,
6863
+ decision.entryContext.direction
6864
+ )
6694
6865
  });
6695
6866
  await invokeStageHooks(
6696
6867
  "afterPlaceOrder",
@@ -6712,19 +6883,32 @@ var executeEntryDecision = async ({
6712
6883
  return signal;
6713
6884
  }
6714
6885
  await beforePlaceOrder();
6715
- const arrivalSnapshot = await getOrderArrivalSnapshot({
6886
+ const beforeSubmit = createEntrySubmissionValidation({
6716
6887
  connector,
6717
- symbol
6718
- });
6719
- validateEntryProtectionAtArrival({
6888
+ symbol,
6720
6889
  direction: decision.entryContext.direction,
6721
- signalPrice: decision.entryContext.prices.currentPrice,
6722
- bid: arrivalSnapshot.bid,
6723
- ask: arrivalSnapshot.ask,
6724
- arrivalMid: arrivalSnapshot.arrivalMid,
6725
6890
  takeProfits: decision.orderPlan.takeProfits,
6726
- stopLossPrice: decision.orderPlan.stopLossPrice
6891
+ stopLossPrice: decision.orderPlan.stopLossPrice,
6892
+ limits: resolveEntryExecutionLimits(
6893
+ hookCtx.strategyConfig,
6894
+ decision.entryContext.direction
6895
+ )
6727
6896
  });
6897
+ if (!beforeSubmit) {
6898
+ const arrivalSnapshot = await getOrderArrivalSnapshot({
6899
+ connector,
6900
+ symbol
6901
+ });
6902
+ validateEntryProtectionAtArrival({
6903
+ direction: decision.entryContext.direction,
6904
+ signalPrice: decision.entryContext.prices.currentPrice,
6905
+ bid: arrivalSnapshot.bid,
6906
+ ask: arrivalSnapshot.ask,
6907
+ arrivalMid: arrivalSnapshot.arrivalMid,
6908
+ takeProfits: decision.orderPlan.takeProfits,
6909
+ stopLossPrice: decision.orderPlan.stopLossPrice
6910
+ });
6911
+ }
6728
6912
  const orderPlaced = await connector.placeOrder({
6729
6913
  symbol,
6730
6914
  qty: decision.orderPlan.qty,
@@ -6732,6 +6916,7 @@ var executeEntryDecision = async ({
6732
6916
  timestamp: decision.entryContext.timestamp,
6733
6917
  direction: decision.entryContext.direction,
6734
6918
  positionIntent: decision.orderPlan.positionIntent,
6919
+ ...beforeSubmit ? { beforeSubmit } : {},
6735
6920
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {}
6736
6921
  });
6737
6922
  if (!orderPlaced) {
@@ -6773,6 +6958,23 @@ var executeEntryDecision = async ({
6773
6958
  { decision, entry, market }
6774
6959
  );
6775
6960
  } catch (err) {
6961
+ if (err instanceof EntrySubmissionValidationError) {
6962
+ if (signal) {
6963
+ signal.orderStatus = "skipped";
6964
+ signal.orderSkipReason = err.message;
6965
+ signal.allocatorDecision = {
6966
+ stage: "runtime_admission",
6967
+ status: "rejected",
6968
+ reason: err.message
6969
+ };
6970
+ }
6971
+ import_logger10.logger.warn(
6972
+ "entry rejected before submission: %s %s",
6973
+ symbol,
6974
+ err.message
6975
+ );
6976
+ return signal ?? err.message;
6977
+ }
6776
6978
  if (signal) {
6777
6979
  signal.orderStatus = "failed";
6778
6980
  if (typeof signal.orderFailureReason !== "string" || !signal.orderFailureReason.trim()) {
@@ -6787,9 +6989,9 @@ var executeEntryDecision = async ({
6787
6989
  market
6788
6990
  });
6789
6991
  if (err?.message === import_types.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY) {
6790
- import_logger9.logger.warn("order warning: %s %s", symbol, err);
6992
+ import_logger10.logger.warn("order warning: %s %s", symbol, err);
6791
6993
  } else {
6792
- import_logger9.logger.error("order error: %s %s", symbol, err);
6994
+ import_logger10.logger.error("order error: %s %s", symbol, err);
6793
6995
  }
6794
6996
  return signal ?? "ORDER_ERROR";
6795
6997
  }
@@ -6954,7 +7156,7 @@ var createStrategyRuntime = ({
6954
7156
  try {
6955
7157
  await projectHook(errorParams);
6956
7158
  } catch (hookError) {
6957
- import_logger10.logger.error(
7159
+ import_logger11.logger.error(
6958
7160
  "project hook onRuntimeError failed: %s %s",
6959
7161
  strategyName,
6960
7162
  hookError
@@ -6968,7 +7170,7 @@ var createStrategyRuntime = ({
6968
7170
  try {
6969
7171
  await onRuntimeError(errorParams);
6970
7172
  } catch (hookError) {
6971
- import_logger10.logger.error(
7173
+ import_logger11.logger.error(
6972
7174
  "runtime hook onRuntimeError failed: %s %s",
6973
7175
  strategyName,
6974
7176
  hookError
@@ -6982,7 +7184,7 @@ var createStrategyRuntime = ({
6982
7184
  try {
6983
7185
  return await hook(params);
6984
7186
  } catch (error) {
6985
- import_logger10.logger.error(
7187
+ import_logger11.logger.error(
6986
7188
  'strategy hook "%s" failed for %s: %s',
6987
7189
  stage,
6988
7190
  strategyName,