@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.
@@ -1073,13 +1073,213 @@ var assessSignalWithJev = async ({
1073
1073
  // src/strategyRuntime.ts
1074
1074
  var import_constants5 = require("@tradejs/core/constants");
1075
1075
  var import_strategies6 = require("@tradejs/core/strategies");
1076
- var import_logger10 = require("@tradejs/infra/logger");
1076
+ var import_logger11 = require("@tradejs/infra/logger");
1077
1077
 
1078
1078
  // src/strategyHelpers/runtime.ts
1079
- var import_logger8 = require("@tradejs/infra/logger");
1079
+ var import_logger9 = require("@tradejs/infra/logger");
1080
1080
  var import_constants3 = require("@tradejs/core/constants");
1081
1081
  var import_ml2 = require("@tradejs/infra/ml");
1082
1082
 
1083
+ // src/strategy/entrySubmissionValidation.ts
1084
+ var import_logger = require("@tradejs/infra/logger");
1085
+
1086
+ // src/strategy/entryExecutionValidation.ts
1087
+ var EntrySubmissionValidationError = class extends Error {
1088
+ constructor() {
1089
+ super(...arguments);
1090
+ this.name = "EntrySubmissionValidationError";
1091
+ }
1092
+ };
1093
+ var resolveEntryExecutionLimits = (config, direction) => {
1094
+ const side = config[direction];
1095
+ const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
1096
+ const maximum = config.MAX_LOSS_VALUE;
1097
+ if (minimum == null && maximum == null) return void 0;
1098
+ const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
1099
+ const maxLossValue = maximum == null ? void 0 : Number(maximum);
1100
+ const feeRate = Number(config.RISK_FEE_RATE ?? 0);
1101
+ const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
1102
+ const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
1103
+ const slippageBps = slippage + impact;
1104
+ 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)
1105
+ throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
1106
+ return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
1107
+ };
1108
+ var validateEntryExecution = ({
1109
+ direction,
1110
+ price,
1111
+ qty,
1112
+ stopLossPrice,
1113
+ takeProfitPrice,
1114
+ limits
1115
+ }) => {
1116
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
1117
+ throw new EntrySubmissionValidationError(
1118
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1119
+ );
1120
+ const side = direction === "LONG" ? 1 : -1;
1121
+ if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
1122
+ throw new EntrySubmissionValidationError(
1123
+ "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
1124
+ );
1125
+ const slippage = limits.slippageBps / 1e4;
1126
+ const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
1127
+ const entryCost = cost(price, side);
1128
+ const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
1129
+ const rewardPerUnit = Math.max(
1130
+ 0,
1131
+ Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
1132
+ );
1133
+ const netRiskRatio = rewardPerUnit / lossPerUnit;
1134
+ const plannedLossValue = qty * lossPerUnit;
1135
+ if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
1136
+ throw new EntrySubmissionValidationError(
1137
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1138
+ );
1139
+ if (limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio)
1140
+ throw new EntrySubmissionValidationError(
1141
+ "ENTRY_RR_BELOW_MIN_AT_SUBMISSION"
1142
+ );
1143
+ if (limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9))
1144
+ throw new EntrySubmissionValidationError(
1145
+ "ENTRY_RISK_EXCEEDS_MAX_LOSS_AT_SUBMISSION"
1146
+ );
1147
+ return { netRiskRatio, plannedLossValue };
1148
+ };
1149
+
1150
+ // src/strategy/entrySubmissionValidation.ts
1151
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
1152
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
1153
+ var getOrderArrivalSnapshot = async ({
1154
+ connector,
1155
+ symbol,
1156
+ requireTimestamp = false
1157
+ }) => {
1158
+ if (typeof connector.getTopOfBookTicker !== "function") {
1159
+ return {
1160
+ arrivalSnapshotTime: Date.now(),
1161
+ arrivalSource: "unavailable",
1162
+ bid: null,
1163
+ ask: null,
1164
+ arrivalMid: null,
1165
+ spreadBps: null
1166
+ };
1167
+ }
1168
+ try {
1169
+ const ticker = await connector.getTopOfBookTicker(symbol);
1170
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
1171
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
1172
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
1173
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
1174
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
1175
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
1176
+ return {
1177
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
1178
+ arrivalSource: "top_of_book",
1179
+ bid,
1180
+ ask,
1181
+ arrivalMid,
1182
+ spreadBps
1183
+ };
1184
+ } catch (error) {
1185
+ import_logger.logger.warn(
1186
+ "runtime order arrival snapshot failed: %s %s",
1187
+ symbol,
1188
+ error?.message || String(error)
1189
+ );
1190
+ return {
1191
+ arrivalSnapshotTime: Date.now(),
1192
+ arrivalSource: "top_of_book_error",
1193
+ bid: null,
1194
+ ask: null,
1195
+ arrivalMid: null,
1196
+ spreadBps: null
1197
+ };
1198
+ }
1199
+ };
1200
+ var validateEntryProtectionAtArrival = ({
1201
+ direction,
1202
+ signalPrice,
1203
+ bid,
1204
+ ask,
1205
+ arrivalMid,
1206
+ takeProfits,
1207
+ stopLossPrice
1208
+ }) => {
1209
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
1210
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
1211
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
1212
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
1213
+ }
1214
+ for (const takeProfit of takeProfits) {
1215
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
1216
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
1217
+ }
1218
+ }
1219
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
1220
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
1221
+ }
1222
+ };
1223
+ var createEntrySubmissionValidation = ({
1224
+ connector,
1225
+ symbol,
1226
+ direction,
1227
+ takeProfits,
1228
+ stopLossPrice,
1229
+ limits,
1230
+ onValidated
1231
+ }) => {
1232
+ if (!limits) return void 0;
1233
+ if (!connector.capabilities?.orderSubmissionValidation)
1234
+ throw new EntrySubmissionValidationError(
1235
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
1236
+ );
1237
+ return async ({ qty, executionPrice }) => {
1238
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
1239
+ const snapshot2 = simulated && executionPrice != null ? {
1240
+ arrivalSnapshotTime: Date.now(),
1241
+ arrivalSource: "simulation",
1242
+ bid: executionPrice,
1243
+ ask: executionPrice,
1244
+ arrivalMid: executionPrice,
1245
+ spreadBps: 0
1246
+ } : await getOrderArrivalSnapshot({
1247
+ connector,
1248
+ symbol,
1249
+ requireTimestamp: true
1250
+ });
1251
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
1252
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
1253
+ throw new EntrySubmissionValidationError(
1254
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
1255
+ );
1256
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
1257
+ throw new EntrySubmissionValidationError(
1258
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
1259
+ );
1260
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
1261
+ validateEntryProtectionAtArrival({
1262
+ direction,
1263
+ signalPrice: price,
1264
+ bid: snapshot2.bid,
1265
+ ask: snapshot2.ask,
1266
+ arrivalMid: snapshot2.arrivalMid,
1267
+ takeProfits,
1268
+ stopLossPrice
1269
+ });
1270
+ const prices = takeProfits.map((tp) => tp.price);
1271
+ const economics2 = validateEntryExecution({
1272
+ direction,
1273
+ price,
1274
+ qty,
1275
+ stopLossPrice,
1276
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
1277
+ limits
1278
+ });
1279
+ onValidated?.(snapshot2, economics2);
1280
+ };
1281
+ };
1282
+
1083
1283
  // src/ai.ts
1084
1284
  var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
1085
1285
  var import_redis = require("@tradejs/infra/redis");
@@ -1664,7 +1864,7 @@ var import_fs = __toESM(require("fs"));
1664
1864
  var import_path = __toESM(require("path"));
1665
1865
  var import_url = require("url");
1666
1866
  var import_config = require("@tradejs/core/config");
1667
- var import_logger = require("@tradejs/infra/logger");
1867
+ var import_logger2 = require("@tradejs/infra/logger");
1668
1868
  var CONFIG_FILE_NAMES = [
1669
1869
  "tradejs.config.ts",
1670
1870
  "tradejs.config.mts",
@@ -1931,11 +2131,11 @@ var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
1931
2131
  cachedByCwd.set(cwd, config);
1932
2132
  if (!announcedConfigFile.has(configFilePath)) {
1933
2133
  announcedConfigFile.add(configFilePath);
1934
- import_logger.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
2134
+ import_logger2.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
1935
2135
  }
1936
2136
  return config;
1937
2137
  } catch (error) {
1938
- import_logger.logger.log(
2138
+ import_logger2.logger.log(
1939
2139
  "warn",
1940
2140
  "Failed to load TradeJS config from %s: %s",
1941
2141
  configFilePath,
@@ -2901,7 +3101,7 @@ var import_node_crypto = require("crypto");
2901
3101
  var import_constants = require("@tradejs/core/constants");
2902
3102
  var import_time = require("@tradejs/core/time");
2903
3103
  var import_trade = require("@tradejs/core/trade");
2904
- var import_logger2 = require("@tradejs/infra/logger");
3104
+ var import_logger3 = require("@tradejs/infra/logger");
2905
3105
  var import_redis2 = require("@tradejs/infra/redis");
2906
3106
  var now = () => Date.now();
2907
3107
  var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
@@ -2960,7 +3160,7 @@ var recordRuntimeTradeOpen = async (params) => {
2960
3160
  )
2961
3161
  ]);
2962
3162
  } catch (error) {
2963
- import_logger2.logger.error(
3163
+ import_logger3.logger.error(
2964
3164
  "runtime trade open journal failed: %s %s",
2965
3165
  record2.symbol,
2966
3166
  error?.message || String(error)
@@ -3028,7 +3228,7 @@ var recordRuntimeTradeIncrease = async (params) => {
3028
3228
  )
3029
3229
  ]);
3030
3230
  } catch (error) {
3031
- import_logger2.logger.error(
3231
+ import_logger3.logger.error(
3032
3232
  "runtime trade increase journal failed: %s %s",
3033
3233
  symbol,
3034
3234
  error?.message || String(error)
@@ -3135,7 +3335,7 @@ var markRuntimeTradeClosed = async (params) => {
3135
3335
  )
3136
3336
  ]);
3137
3337
  } catch (error) {
3138
- import_logger2.logger.error(
3338
+ import_logger3.logger.error(
3139
3339
  "runtime trade close journal failed: %s %s",
3140
3340
  symbol,
3141
3341
  error?.message || String(error)
@@ -3145,11 +3345,11 @@ var markRuntimeTradeClosed = async (params) => {
3145
3345
  };
3146
3346
 
3147
3347
  // src/strategyHelpers/marketContextStages.ts
3148
- var import_logger7 = require("@tradejs/infra/logger");
3348
+ var import_logger8 = require("@tradejs/infra/logger");
3149
3349
 
3150
3350
  // src/strategyHelpers/binanceMarketContext.ts
3151
3351
  var import_marketContext = require("@tradejs/infra/timescale/marketContext");
3152
- var import_logger3 = require("@tradejs/infra/logger");
3352
+ var import_logger4 = require("@tradejs/infra/logger");
3153
3353
  var import_strategies = require("@tradejs/core/strategies");
3154
3354
 
3155
3355
  // src/binanceBreadthUniverses.ts
@@ -3513,7 +3713,7 @@ var parseEnabledFlag = (value, env) => {
3513
3713
  if (normalized === "live") return env !== "BACKTEST";
3514
3714
  return false;
3515
3715
  };
3516
- var toFiniteNumberOrNull = (value) => {
3716
+ var toFiniteNumberOrNull2 = (value) => {
3517
3717
  const numeric = typeof value === "number" ? value : Number(value);
3518
3718
  return Number.isFinite(numeric) ? numeric : null;
3519
3719
  };
@@ -3638,14 +3838,14 @@ var toTradeFlowContext = (row, interval) => row ? {
3638
3838
  asOfTs: row.ts.getTime(),
3639
3839
  ageMs: row.ageMs,
3640
3840
  stale: row.stale,
3641
- trades: toFiniteNumberOrNull(row.trades),
3642
- buyPressurePct: toFiniteNumberOrNull(row.buyPressurePct),
3643
- buyBaseVolume: toFiniteNumberOrNull(row.buyBaseVolume),
3644
- sellBaseVolume: toFiniteNumberOrNull(row.sellBaseVolume),
3645
- buyQuoteVolume: toFiniteNumberOrNull(row.buyQuoteVolume),
3646
- sellQuoteVolume: toFiniteNumberOrNull(row.sellQuoteVolume),
3647
- netBaseDelta: toFiniteNumberOrNull(row.netBaseDelta),
3648
- netQuoteDelta: toFiniteNumberOrNull(row.netQuoteDelta)
3841
+ trades: toFiniteNumberOrNull2(row.trades),
3842
+ buyPressurePct: toFiniteNumberOrNull2(row.buyPressurePct),
3843
+ buyBaseVolume: toFiniteNumberOrNull2(row.buyBaseVolume),
3844
+ sellBaseVolume: toFiniteNumberOrNull2(row.sellBaseVolume),
3845
+ buyQuoteVolume: toFiniteNumberOrNull2(row.buyQuoteVolume),
3846
+ sellQuoteVolume: toFiniteNumberOrNull2(row.sellQuoteVolume),
3847
+ netBaseDelta: toFiniteNumberOrNull2(row.netBaseDelta),
3848
+ netQuoteDelta: toFiniteNumberOrNull2(row.netQuoteDelta)
3649
3849
  } : null;
3650
3850
  var getCachedReferenceRows = ({
3651
3851
  referenceSymbols,
@@ -3734,16 +3934,16 @@ var toMarketBreadthContext = (breadth, interval) => ({
3734
3934
  asOfTs: breadth.ts.getTime(),
3735
3935
  ageMs: breadth.ageMs,
3736
3936
  stale: breadth.stale,
3737
- symbolsCount: toFiniteNumberOrNull(breadth.symbolsCount),
3738
- advancers: toFiniteNumberOrNull(breadth.advancers),
3739
- decliners: toFiniteNumberOrNull(breadth.decliners),
3740
- unchanged: toFiniteNumberOrNull(breadth.unchanged),
3741
- advanceDeclineRatio: toFiniteNumberOrNull(breadth.advanceDeclineRatio),
3742
- pctAboveMa20: toFiniteNumberOrNull(breadth.pctAboveMa20),
3743
- pctAboveMa50: toFiniteNumberOrNull(breadth.pctAboveMa50),
3744
- equalWeightedReturn: toFiniteNumberOrNull(breadth.equalWeightedReturn),
3745
- volumeWeightedReturn: toFiniteNumberOrNull(breadth.volumeWeightedReturn),
3746
- dispersion: toFiniteNumberOrNull(breadth.dispersion)
3937
+ symbolsCount: toFiniteNumberOrNull2(breadth.symbolsCount),
3938
+ advancers: toFiniteNumberOrNull2(breadth.advancers),
3939
+ decliners: toFiniteNumberOrNull2(breadth.decliners),
3940
+ unchanged: toFiniteNumberOrNull2(breadth.unchanged),
3941
+ advanceDeclineRatio: toFiniteNumberOrNull2(breadth.advanceDeclineRatio),
3942
+ pctAboveMa20: toFiniteNumberOrNull2(breadth.pctAboveMa20),
3943
+ pctAboveMa50: toFiniteNumberOrNull2(breadth.pctAboveMa50),
3944
+ equalWeightedReturn: toFiniteNumberOrNull2(breadth.equalWeightedReturn),
3945
+ volumeWeightedReturn: toFiniteNumberOrNull2(breadth.volumeWeightedReturn),
3946
+ dispersion: toFiniteNumberOrNull2(breadth.dispersion)
3747
3947
  });
3748
3948
  var enrichSignalWithBinanceMarketContext = async (params) => {
3749
3949
  const {
@@ -3844,46 +4044,46 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3844
4044
  asOfTs: primaryBreadth.ts.getTime(),
3845
4045
  ageMs: primaryBreadth.ageMs,
3846
4046
  stale: primaryBreadth.stale,
3847
- btcReturn1h: toFiniteNumberOrNull(
4047
+ btcReturn1h: toFiniteNumberOrNull2(
3848
4048
  primaryBreadth.btcReturn1h
3849
4049
  ),
3850
- btcReturn4h: toFiniteNumberOrNull(
4050
+ btcReturn4h: toFiniteNumberOrNull2(
3851
4051
  primaryBreadth.btcReturn4h
3852
4052
  ),
3853
- btcReturn24h: toFiniteNumberOrNull(
4053
+ btcReturn24h: toFiniteNumberOrNull2(
3854
4054
  primaryBreadth.btcReturn24h
3855
4055
  ),
3856
- altBasketReturn1h: toFiniteNumberOrNull(
4056
+ altBasketReturn1h: toFiniteNumberOrNull2(
3857
4057
  primaryBreadth.altBasketReturn1h
3858
4058
  ),
3859
- altBasketReturn4h: toFiniteNumberOrNull(
4059
+ altBasketReturn4h: toFiniteNumberOrNull2(
3860
4060
  primaryBreadth.altBasketReturn4h
3861
4061
  ),
3862
- altBasketReturn24h: toFiniteNumberOrNull(
4062
+ altBasketReturn24h: toFiniteNumberOrNull2(
3863
4063
  primaryBreadth.altBasketReturn24h
3864
4064
  ),
3865
- btcVsAltReturn1h: toFiniteNumberOrNull(
4065
+ btcVsAltReturn1h: toFiniteNumberOrNull2(
3866
4066
  primaryBreadth.btcVsAltReturn1h
3867
4067
  ),
3868
- btcVsAltReturn4h: toFiniteNumberOrNull(
4068
+ btcVsAltReturn4h: toFiniteNumberOrNull2(
3869
4069
  primaryBreadth.btcVsAltReturn4h
3870
4070
  ),
3871
- btcVsAltReturn24h: toFiniteNumberOrNull(
4071
+ btcVsAltReturn24h: toFiniteNumberOrNull2(
3872
4072
  primaryBreadth.btcVsAltReturn24h
3873
4073
  ),
3874
- btcTurnoverShare1h: toFiniteNumberOrNull(
4074
+ btcTurnoverShare1h: toFiniteNumberOrNull2(
3875
4075
  primaryBreadth.btcTurnoverShare1h
3876
4076
  ),
3877
- btcTurnoverShare24h: toFiniteNumberOrNull(
4077
+ btcTurnoverShare24h: toFiniteNumberOrNull2(
3878
4078
  primaryBreadth.btcTurnoverShare24h
3879
4079
  ),
3880
- btcTurnoverShareChange24h: toFiniteNumberOrNull(
4080
+ btcTurnoverShareChange24h: toFiniteNumberOrNull2(
3881
4081
  primaryBreadth.btcTurnoverShareChange24h
3882
4082
  ),
3883
- altVolToBtcVol24h: toFiniteNumberOrNull(
4083
+ altVolToBtcVol24h: toFiniteNumberOrNull2(
3884
4084
  primaryBreadth.altVolToBtcVol24h
3885
4085
  ),
3886
- altDispersion24h: toFiniteNumberOrNull(
4086
+ altDispersion24h: toFiniteNumberOrNull2(
3887
4087
  primaryBreadth.altDispersion24h
3888
4088
  ),
3889
4089
  regime: primaryBreadth.btcAltRegime ?? "unknown"
@@ -3900,7 +4100,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3900
4100
  throw error;
3901
4101
  }
3902
4102
  binanceMarketContextUnavailable = true;
3903
- import_logger3.logger.warn(
4103
+ import_logger4.logger.warn(
3904
4104
  "Binance market context disabled after Timescale read failure: %s",
3905
4105
  String(error)
3906
4106
  );
@@ -3910,7 +4110,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3910
4110
 
3911
4111
  // src/strategyHelpers/coinMarketCapContext.ts
3912
4112
  var import_strategies2 = require("@tradejs/core/strategies");
3913
- var import_logger4 = require("@tradejs/infra/logger");
4113
+ var import_logger5 = require("@tradejs/infra/logger");
3914
4114
  var import_marketContext2 = require("@tradejs/infra/timescale/marketContext");
3915
4115
  var DEFAULT_MAX_AGE_MS = 48 * 60 * 6e4;
3916
4116
  var SOURCE_GLOBAL_DAILY = "coinmarketcap_global";
@@ -3948,7 +4148,7 @@ var asInt = (value, fallback) => {
3948
4148
  const parsed = Number.parseInt(String(value ?? ""), 10);
3949
4149
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
3950
4150
  };
3951
- var toFiniteNumberOrNull2 = (value) => {
4151
+ var toFiniteNumberOrNull3 = (value) => {
3952
4152
  const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
3953
4153
  return Number.isFinite(numeric) ? numeric : null;
3954
4154
  };
@@ -4182,12 +4382,12 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4182
4382
  const ethRow = references.get("ETHUSDT") ?? null;
4183
4383
  const previousBtcRow = previousReferences.get("BTCUSDT") ?? null;
4184
4384
  const previousEthRow = previousReferences.get("ETHUSDT") ?? null;
4185
- const btcMarketCapUsd = toFiniteNumberOrNull2(btcRow?.marketCapUsd);
4186
- const ethMarketCapUsd = toFiniteNumberOrNull2(ethRow?.marketCapUsd);
4187
- const previousBtcMarketCapUsd = toFiniteNumberOrNull2(
4385
+ const btcMarketCapUsd = toFiniteNumberOrNull3(btcRow?.marketCapUsd);
4386
+ const ethMarketCapUsd = toFiniteNumberOrNull3(ethRow?.marketCapUsd);
4387
+ const previousBtcMarketCapUsd = toFiniteNumberOrNull3(
4188
4388
  previousBtcRow?.marketCapUsd
4189
4389
  );
4190
- const previousEthMarketCapUsd = toFiniteNumberOrNull2(
4390
+ const previousEthMarketCapUsd = toFiniteNumberOrNull3(
4191
4391
  previousEthRow?.marketCapUsd
4192
4392
  );
4193
4393
  const ethBtcMarketCapRatio = safeDivide(ethMarketCapUsd, btcMarketCapUsd);
@@ -4196,16 +4396,16 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4196
4396
  previousBtcMarketCapUsd
4197
4397
  );
4198
4398
  const ethBtcMarketCapRatioChange24hPct = ethBtcMarketCapRatio != null && previousEthBtcMarketCapRatio != null && previousEthBtcMarketCapRatio > 0 ? (ethBtcMarketCapRatio - previousEthBtcMarketCapRatio) / previousEthBtcMarketCapRatio : null;
4199
- const btcVolumeUsd = toFiniteNumberOrNull2(btcRow?.volumeUsd);
4200
- const ethVolumeUsd = toFiniteNumberOrNull2(ethRow?.volumeUsd);
4399
+ const btcVolumeUsd = toFiniteNumberOrNull3(btcRow?.volumeUsd);
4400
+ const ethVolumeUsd = toFiniteNumberOrNull3(ethRow?.volumeUsd);
4201
4401
  const referenceStale = btcRow?.stale === true || ethRow?.stale === true || !btcRow || !ethRow;
4202
- const btcDominanceChange24hPct = toFiniteNumberOrNull2(
4402
+ const btcDominanceChange24hPct = toFiniteNumberOrNull3(
4203
4403
  globalRow?.btcDominanceChange24hPct
4204
4404
  );
4205
- const altMarketCapChange24hPct = toFiniteNumberOrNull2(
4405
+ const altMarketCapChange24hPct = toFiniteNumberOrNull3(
4206
4406
  globalRow?.altMarketCapChange24hPct
4207
4407
  );
4208
- const altVolumeChange24hPct = toFiniteNumberOrNull2(
4408
+ const altVolumeChange24hPct = toFiniteNumberOrNull3(
4209
4409
  globalRow?.altVolumeChange24hPct
4210
4410
  );
4211
4411
  const altLiquidityRegime = globalRow ? toAltLiquidityRegime({
@@ -4216,19 +4416,19 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4216
4416
  }) : "unknown";
4217
4417
  const exchangeLiquidityRegime = exchangeLiquidityRow ? toExchangeLiquidityRegime({
4218
4418
  stale: exchangeLiquidityRow.stale,
4219
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
4419
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
4220
4420
  exchangeLiquidityRow.totalVolumeChange24hPct
4221
4421
  ),
4222
4422
  fallback: exchangeLiquidityRow.liquidityRegime ?? "unknown"
4223
4423
  }) : "unknown";
4224
4424
  const cmc100Row = indexRows.get("cmc100") ?? null;
4225
4425
  const cmc20Row = indexRows.get("cmc20") ?? null;
4226
- const cmc100Value = toFiniteNumberOrNull2(cmc100Row?.value);
4227
- const cmc20Value = toFiniteNumberOrNull2(cmc20Row?.value);
4228
- const cmc100Change24hPct = toFiniteNumberOrNull2(
4426
+ const cmc100Value = toFiniteNumberOrNull3(cmc100Row?.value);
4427
+ const cmc20Value = toFiniteNumberOrNull3(cmc20Row?.value);
4428
+ const cmc100Change24hPct = toFiniteNumberOrNull3(
4229
4429
  cmc100Row?.valueChange24hPct
4230
4430
  );
4231
- const cmc20Change24hPct = toFiniteNumberOrNull2(cmc20Row?.valueChange24hPct);
4431
+ const cmc20Change24hPct = toFiniteNumberOrNull3(cmc20Row?.valueChange24hPct);
4232
4432
  const cmc20ToCmc100Ratio = safeDivide(cmc20Value, cmc100Value);
4233
4433
  const cmc20ToCmc100RatioChange24hPct = cmc20Change24hPct != null && cmc100Change24hPct != null ? (1 + cmc20Change24hPct) / (1 + cmc100Change24hPct) - 1 : null;
4234
4434
  const indexStale = cmc100Row?.stale === true || cmc20Row?.stale === true || !cmc100Row || !cmc20Row;
@@ -4252,41 +4452,41 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4252
4452
  asOfTs: globalRow.ts.getTime(),
4253
4453
  ageMs: globalRow.ageMs,
4254
4454
  stale: globalRow.stale,
4255
- totalMarketCapUsd: toFiniteNumberOrNull2(
4455
+ totalMarketCapUsd: toFiniteNumberOrNull3(
4256
4456
  globalRow.totalMarketCapUsd
4257
4457
  ),
4258
- totalVolumeUsd: toFiniteNumberOrNull2(
4458
+ totalVolumeUsd: toFiniteNumberOrNull3(
4259
4459
  globalRow.totalVolumeUsd
4260
4460
  ),
4261
- totalVolumeReportedUsd: toFiniteNumberOrNull2(
4461
+ totalVolumeReportedUsd: toFiniteNumberOrNull3(
4262
4462
  globalRow.totalVolumeReportedUsd
4263
4463
  ),
4264
- altMarketCapUsd: toFiniteNumberOrNull2(
4464
+ altMarketCapUsd: toFiniteNumberOrNull3(
4265
4465
  globalRow.altMarketCapUsd
4266
4466
  ),
4267
- altVolumeUsd: toFiniteNumberOrNull2(globalRow.altVolumeUsd),
4268
- altVolumeReportedUsd: toFiniteNumberOrNull2(
4467
+ altVolumeUsd: toFiniteNumberOrNull3(globalRow.altVolumeUsd),
4468
+ altVolumeReportedUsd: toFiniteNumberOrNull3(
4269
4469
  globalRow.altVolumeReportedUsd
4270
4470
  ),
4271
- btcDominancePct: toFiniteNumberOrNull2(
4471
+ btcDominancePct: toFiniteNumberOrNull3(
4272
4472
  globalRow.btcDominancePct
4273
4473
  ),
4274
- ethDominancePct: toFiniteNumberOrNull2(
4474
+ ethDominancePct: toFiniteNumberOrNull3(
4275
4475
  globalRow.ethDominancePct
4276
4476
  ),
4277
4477
  btcDominanceChange24hPct,
4278
- ethDominanceChange24hPct: toFiniteNumberOrNull2(
4478
+ ethDominanceChange24hPct: toFiniteNumberOrNull3(
4279
4479
  globalRow.ethDominanceChange24hPct
4280
4480
  ),
4281
4481
  altMarketCapChange24hPct,
4282
4482
  altVolumeChange24hPct,
4283
- activeCryptocurrencies: toFiniteNumberOrNull2(
4483
+ activeCryptocurrencies: toFiniteNumberOrNull3(
4284
4484
  globalRow.activeCryptocurrencies
4285
4485
  ),
4286
- activeExchanges: toFiniteNumberOrNull2(
4486
+ activeExchanges: toFiniteNumberOrNull3(
4287
4487
  globalRow.activeExchanges
4288
4488
  ),
4289
- activeMarketPairs: toFiniteNumberOrNull2(
4489
+ activeMarketPairs: toFiniteNumberOrNull3(
4290
4490
  globalRow.activeMarketPairs
4291
4491
  ),
4292
4492
  altLiquidityRegime
@@ -4331,22 +4531,22 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4331
4531
  asOfTs: exchangeLiquidityRow.ts.getTime(),
4332
4532
  ageMs: exchangeLiquidityRow.ageMs,
4333
4533
  stale: exchangeLiquidityRow.stale,
4334
- exchangesCount: toFiniteNumberOrNull2(
4534
+ exchangesCount: toFiniteNumberOrNull3(
4335
4535
  exchangeLiquidityRow.exchangesCount
4336
4536
  ),
4337
- totalVolumeUsd: toFiniteNumberOrNull2(
4537
+ totalVolumeUsd: toFiniteNumberOrNull3(
4338
4538
  exchangeLiquidityRow.totalVolumeUsd
4339
4539
  ),
4340
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
4540
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
4341
4541
  exchangeLiquidityRow.totalVolumeChange24hPct
4342
4542
  ),
4343
- binanceVolumeUsd: toFiniteNumberOrNull2(
4543
+ binanceVolumeUsd: toFiniteNumberOrNull3(
4344
4544
  exchangeLiquidityRow.binanceVolumeUsd
4345
4545
  ),
4346
- binanceVolumeShare: toFiniteNumberOrNull2(
4546
+ binanceVolumeShare: toFiniteNumberOrNull3(
4347
4547
  exchangeLiquidityRow.binanceVolumeShare
4348
4548
  ),
4349
- topExchangeVolumeShare: toFiniteNumberOrNull2(
4549
+ topExchangeVolumeShare: toFiniteNumberOrNull3(
4350
4550
  exchangeLiquidityRow.topExchangeVolumeShare
4351
4551
  ),
4352
4552
  liquidityRegime: exchangeLiquidityRegime
@@ -4359,11 +4559,11 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4359
4559
  asOfTs: fearGreedRow.ts.getTime(),
4360
4560
  ageMs: fearGreedRow.ageMs,
4361
4561
  stale: fearGreedRow.stale,
4362
- value: toFiniteNumberOrNull2(fearGreedRow.value),
4363
- valueChange24h: toFiniteNumberOrNull2(
4562
+ value: toFiniteNumberOrNull3(fearGreedRow.value),
4563
+ valueChange24h: toFiniteNumberOrNull3(
4364
4564
  fearGreedRow.valueChange24h
4365
4565
  ),
4366
- valueChange7d: toFiniteNumberOrNull2(
4566
+ valueChange7d: toFiniteNumberOrNull3(
4367
4567
  fearGreedRow.valueChange7d
4368
4568
  ),
4369
4569
  classification: fearGreedRow.classification ?? "Unknown",
@@ -4383,13 +4583,13 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4383
4583
  cmc100Value,
4384
4584
  cmc100Change24hPct,
4385
4585
  cmc100TopConstituentSymbol: cmc100Row?.topConstituentSymbol ?? null,
4386
- cmc100TopConstituentWeightPct: toFiniteNumberOrNull2(
4586
+ cmc100TopConstituentWeightPct: toFiniteNumberOrNull3(
4387
4587
  cmc100Row?.topConstituentWeightPct
4388
4588
  ),
4389
4589
  cmc20Value,
4390
4590
  cmc20Change24hPct,
4391
4591
  cmc20TopConstituentSymbol: cmc20Row?.topConstituentSymbol ?? null,
4392
- cmc20TopConstituentWeightPct: toFiniteNumberOrNull2(
4592
+ cmc20TopConstituentWeightPct: toFiniteNumberOrNull3(
4393
4593
  cmc20Row?.topConstituentWeightPct
4394
4594
  ),
4395
4595
  cmc20ToCmc100Ratio,
@@ -4407,7 +4607,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4407
4607
  throw error;
4408
4608
  }
4409
4609
  coinMarketCapContextUnavailable = true;
4410
- import_logger4.logger.warn(
4610
+ import_logger5.logger.warn(
4411
4611
  "CoinMarketCap context disabled after Timescale read failure: %s",
4412
4612
  String(error)
4413
4613
  );
@@ -4421,7 +4621,7 @@ var import_data2 = require("@tradejs/core/data");
4421
4621
  var import_strategies3 = require("@tradejs/core/strategies");
4422
4622
  var import_constants2 = require("@tradejs/core/constants");
4423
4623
  var import_derivatives = require("@tradejs/infra/timescale/derivatives");
4424
- var import_logger5 = require("@tradejs/infra/logger");
4624
+ var import_logger6 = require("@tradejs/infra/logger");
4425
4625
  var STORED_INTERVALS = ["15m", "1h"];
4426
4626
  var CONTEXT_INTERVALS = ["15m", "1h"];
4427
4627
  var DEFAULT_LOOKBACK_HOURS = 48;
@@ -4476,7 +4676,7 @@ var resolvePrimaryReferenceSymbol2 = () => PRIMARY_DERIVATIVES_REFERENCE_SYMBOL;
4476
4676
  var resolveSecondaryReferenceSymbol = () => SECONDARY_DERIVATIVES_REFERENCE_SYMBOL;
4477
4677
  var getPrimaryIntervalContext = (context) => context?.intervals["15m"] ?? context?.intervals["1h"] ?? null;
4478
4678
  var hasDerivativesSymbolData = (context) => Object.keys(context.intervals).length > 0 && !context.summary.riskFlags.includes("missing_derivatives");
4479
- var toFiniteNumberOrNull3 = (value) => {
4679
+ var toFiniteNumberOrNull4 = (value) => {
4480
4680
  if (typeof value === "number" && Number.isFinite(value)) return value;
4481
4681
  if (typeof value === "string" && value.trim()) {
4482
4682
  const parsed = Number(value);
@@ -4490,8 +4690,8 @@ var roundNullable = (value, digits = 4) => {
4490
4690
  return Math.round(value * multiplier) / multiplier;
4491
4691
  };
4492
4692
  var deltaNullable = (targetValue, referenceValue) => {
4493
- const target = toFiniteNumberOrNull3(targetValue);
4494
- const reference = toFiniteNumberOrNull3(referenceValue);
4693
+ const target = toFiniteNumberOrNull4(targetValue);
4694
+ const reference = toFiniteNumberOrNull4(referenceValue);
4495
4695
  return target == null || reference == null ? null : roundNullable(target - reference);
4496
4696
  };
4497
4697
  var buildTargetDerivedContext = (params) => {
@@ -4653,7 +4853,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4653
4853
  throw error;
4654
4854
  }
4655
4855
  derivativesContextUnavailable = true;
4656
- import_logger5.logger.warn(
4856
+ import_logger6.logger.warn(
4657
4857
  "Derivatives context disabled after Timescale read failure: %s",
4658
4858
  String(error)
4659
4859
  );
@@ -4665,7 +4865,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4665
4865
  var import_data3 = require("@tradejs/core/data");
4666
4866
  var import_strategies4 = require("@tradejs/core/strategies");
4667
4867
  var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWhales");
4668
- var import_logger6 = require("@tradejs/infra/logger");
4868
+ var import_logger7 = require("@tradejs/infra/logger");
4669
4869
 
4670
4870
  // src/hyperliquidWhaleUniverse.ts
4671
4871
  var import_node_crypto3 = require("crypto");
@@ -5185,7 +5385,7 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
5185
5385
  throw error;
5186
5386
  }
5187
5387
  hyperliquidWhaleContextUnavailable = true;
5188
- import_logger6.logger.warn(
5388
+ import_logger7.logger.warn(
5189
5389
  "Hyperliquid whale context disabled after Timescale read failure: %s",
5190
5390
  String(error)
5191
5391
  );
@@ -5268,7 +5468,7 @@ var runMarketContextStage = async ({
5268
5468
  elapsedMs: Date.now() - startedAt
5269
5469
  };
5270
5470
  if (status === "timed_out") {
5271
- import_logger7.logger.warn(
5471
+ import_logger8.logger.warn(
5272
5472
  "Market context stage timed out: %s after %sms",
5273
5473
  stage,
5274
5474
  result.elapsedMs
@@ -5458,79 +5658,10 @@ var enrichSignalWithAi = async ({
5458
5658
  signal.aiAnalysis = analysis;
5459
5659
  return resolveAiQuality(analysis, direction);
5460
5660
  } catch (err) {
5461
- import_logger8.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
5661
+ import_logger9.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
5462
5662
  }
5463
5663
  return void 0;
5464
5664
  };
5465
- var toFiniteNumberOrNull4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
5466
- var getOrderArrivalSnapshot = async ({
5467
- connector,
5468
- symbol
5469
- }) => {
5470
- if (typeof connector.getTopOfBookTicker !== "function") {
5471
- return {
5472
- arrivalSnapshotTime: Date.now(),
5473
- arrivalSource: "unavailable",
5474
- bid: null,
5475
- ask: null,
5476
- arrivalMid: null,
5477
- spreadBps: null
5478
- };
5479
- }
5480
- try {
5481
- const ticker = await connector.getTopOfBookTicker(symbol);
5482
- const arrivalSnapshotTime = toFiniteNumberOrNull4(ticker?.timestamp);
5483
- const bid = toFiniteNumberOrNull4(ticker?.bidPrice);
5484
- const ask = toFiniteNumberOrNull4(ticker?.askPrice);
5485
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
5486
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
5487
- return {
5488
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
5489
- arrivalSource: "top_of_book",
5490
- bid,
5491
- ask,
5492
- arrivalMid,
5493
- spreadBps
5494
- };
5495
- } catch (error) {
5496
- import_logger8.logger.warn(
5497
- "runtime order arrival snapshot failed: %s %s",
5498
- symbol,
5499
- error?.message || String(error)
5500
- );
5501
- return {
5502
- arrivalSnapshotTime: Date.now(),
5503
- arrivalSource: "top_of_book_error",
5504
- bid: null,
5505
- ask: null,
5506
- arrivalMid: null,
5507
- spreadBps: null
5508
- };
5509
- }
5510
- };
5511
- var validateEntryProtectionAtArrival = ({
5512
- direction,
5513
- signalPrice,
5514
- bid,
5515
- ask,
5516
- arrivalMid,
5517
- takeProfits,
5518
- stopLossPrice
5519
- }) => {
5520
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
5521
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
5522
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
5523
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
5524
- }
5525
- for (const takeProfit of takeProfits) {
5526
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
5527
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
5528
- }
5529
- }
5530
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
5531
- throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
5532
- }
5533
- };
5534
5665
  var resolveRuntimeTelemetryQuality = ({
5535
5666
  signalClosePrice,
5536
5667
  arrivalMid,
@@ -5607,7 +5738,8 @@ var executeEntryOrder = async ({
5607
5738
  signal,
5608
5739
  beforePlaceOrder,
5609
5740
  recordRuntimeTrade = true,
5610
- leverage
5741
+ leverage,
5742
+ executionLimits
5611
5743
  }) => {
5612
5744
  await beforePlaceOrder?.();
5613
5745
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
@@ -5618,23 +5750,57 @@ var executeEntryOrder = async ({
5618
5750
  signal.orderQty = qty;
5619
5751
  signal.orderValue = qty * currentPrice;
5620
5752
  signal.orderFailureReason = void 0;
5621
- const arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
5622
- try {
5623
- validateEntryProtectionAtArrival({
5624
- direction,
5625
- signalPrice: currentPrice,
5626
- bid: arrivalSnapshot.bid,
5627
- ask: arrivalSnapshot.ask,
5628
- arrivalMid: arrivalSnapshot.arrivalMid,
5629
- takeProfits,
5630
- stopLossPrice
5631
- });
5632
- } catch (error) {
5633
- signal.orderStatus = "failed";
5634
- signal.orderFailureReason = error.message;
5635
- throw error;
5753
+ let arrivalSnapshot = {
5754
+ arrivalSnapshotTime: Date.now(),
5755
+ arrivalSource: "unavailable",
5756
+ bid: null,
5757
+ ask: null,
5758
+ arrivalMid: null,
5759
+ spreadBps: null
5760
+ };
5761
+ let orderSubmitTime = Date.now();
5762
+ const beforeSubmit = createEntrySubmissionValidation({
5763
+ connector,
5764
+ symbol,
5765
+ direction,
5766
+ takeProfits,
5767
+ stopLossPrice,
5768
+ limits: executionLimits,
5769
+ onValidated: (snapshot2, economics2) => {
5770
+ arrivalSnapshot = snapshot2;
5771
+ orderSubmitTime = Date.now();
5772
+ if (executionLimits?.maxLossValue != null) {
5773
+ signal.riskDecision = {
5774
+ stage: "order_plan",
5775
+ status: "approved",
5776
+ reason: "PLANNED_LOSS_WITHIN_MAX_LOSS_VALUE",
5777
+ plannedLossValue: economics2.plannedLossValue,
5778
+ maxLossValue: executionLimits.maxLossValue,
5779
+ limitUtilization: economics2.plannedLossValue / executionLimits.maxLossValue,
5780
+ enforced: true
5781
+ };
5782
+ }
5783
+ }
5784
+ });
5785
+ if (!beforeSubmit) {
5786
+ arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
5787
+ try {
5788
+ validateEntryProtectionAtArrival({
5789
+ direction,
5790
+ signalPrice: currentPrice,
5791
+ bid: arrivalSnapshot.bid,
5792
+ ask: arrivalSnapshot.ask,
5793
+ arrivalMid: arrivalSnapshot.arrivalMid,
5794
+ takeProfits,
5795
+ stopLossPrice
5796
+ });
5797
+ } catch (error) {
5798
+ signal.orderStatus = "failed";
5799
+ signal.orderFailureReason = error.message;
5800
+ throw error;
5801
+ }
5802
+ orderSubmitTime = Date.now();
5636
5803
  }
5637
- const orderSubmitTime = Date.now();
5638
5804
  const orderPlaced = await connector.placeOrder({
5639
5805
  symbol,
5640
5806
  qty,
@@ -5645,7 +5811,8 @@ var executeEntryOrder = async ({
5645
5811
  direction,
5646
5812
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
5647
5813
  orderId,
5648
- signal
5814
+ signal,
5815
+ ...beforeSubmit ? { beforeSubmit } : {}
5649
5816
  });
5650
5817
  const orderAckTime = Date.now();
5651
5818
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -6474,7 +6641,7 @@ var canUseSharedReplayState = ({
6474
6641
  }) => (env === "BACKTEST" || env === "PARITY") && Boolean(sharedReplayKey);
6475
6642
 
6476
6643
  // src/strategy/runtimeExecution.ts
6477
- var import_logger9 = require("@tradejs/infra/logger");
6644
+ var import_logger10 = require("@tradejs/infra/logger");
6478
6645
  var import_types = require("@tradejs/types");
6479
6646
  var buildExitOrderSignal = ({
6480
6647
  strategyName,
@@ -6526,7 +6693,7 @@ var handleExitDecision = async ({
6526
6693
  deploymentId: connector.deploymentId
6527
6694
  });
6528
6695
  if (!activeTrade) {
6529
- import_logger9.logger.warn(
6696
+ import_logger10.logger.warn(
6530
6697
  "[%s] blocked closePosition for untracked runtime position: %s",
6531
6698
  strategyName ?? "unknown",
6532
6699
  symbol
@@ -6534,7 +6701,7 @@ var handleExitDecision = async ({
6534
6701
  return "CLOSE_BLOCKED_BY_UNTRACKED_POSITION";
6535
6702
  }
6536
6703
  if (!strategyName || activeTrade.strategy !== strategyName) {
6537
- import_logger9.logger.warn(
6704
+ import_logger10.logger.warn(
6538
6705
  "[%s] blocked closePosition for foreign runtime position: %s ownedBy=%s",
6539
6706
  strategyName ?? "unknown",
6540
6707
  symbol,
@@ -6587,7 +6754,7 @@ var handleExitDecision = async ({
6587
6754
  exitType: closedTrade?.exitType ?? "exit"
6588
6755
  });
6589
6756
  } catch (notificationError) {
6590
- import_logger9.logger.error(
6757
+ import_logger10.logger.error(
6591
6758
  "runtime close notification error: %s %s",
6592
6759
  symbol,
6593
6760
  notificationError
@@ -6601,7 +6768,7 @@ var handleExitDecision = async ({
6601
6768
  decision,
6602
6769
  market
6603
6770
  });
6604
- import_logger9.logger.error("close order error: %s %s", symbol, err);
6771
+ import_logger10.logger.error("close order error: %s %s", symbol, err);
6605
6772
  return "ORDER_ERROR";
6606
6773
  }
6607
6774
  return decision.code;
@@ -6628,7 +6795,7 @@ var handleProtectDecision = async ({
6628
6795
  decision,
6629
6796
  market
6630
6797
  });
6631
- import_logger9.logger.error("protect position error: %s %s", symbol, err);
6798
+ import_logger10.logger.error("protect position error: %s %s", symbol, err);
6632
6799
  return "ORDER_ERROR";
6633
6800
  }
6634
6801
  return decision.code;
@@ -6694,7 +6861,11 @@ var executeEntryDecision = async ({
6694
6861
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
6695
6862
  signal,
6696
6863
  beforePlaceOrder,
6697
- recordRuntimeTrade: recordRuntimeJournal
6864
+ recordRuntimeTrade: recordRuntimeJournal,
6865
+ executionLimits: resolveEntryExecutionLimits(
6866
+ hookCtx.strategyConfig,
6867
+ decision.entryContext.direction
6868
+ )
6698
6869
  });
6699
6870
  await invokeStageHooks(
6700
6871
  "afterPlaceOrder",
@@ -6716,19 +6887,32 @@ var executeEntryDecision = async ({
6716
6887
  return signal;
6717
6888
  }
6718
6889
  await beforePlaceOrder();
6719
- const arrivalSnapshot = await getOrderArrivalSnapshot({
6890
+ const beforeSubmit = createEntrySubmissionValidation({
6720
6891
  connector,
6721
- symbol
6722
- });
6723
- validateEntryProtectionAtArrival({
6892
+ symbol,
6724
6893
  direction: decision.entryContext.direction,
6725
- signalPrice: decision.entryContext.prices.currentPrice,
6726
- bid: arrivalSnapshot.bid,
6727
- ask: arrivalSnapshot.ask,
6728
- arrivalMid: arrivalSnapshot.arrivalMid,
6729
6894
  takeProfits: decision.orderPlan.takeProfits,
6730
- stopLossPrice: decision.orderPlan.stopLossPrice
6895
+ stopLossPrice: decision.orderPlan.stopLossPrice,
6896
+ limits: resolveEntryExecutionLimits(
6897
+ hookCtx.strategyConfig,
6898
+ decision.entryContext.direction
6899
+ )
6731
6900
  });
6901
+ if (!beforeSubmit) {
6902
+ const arrivalSnapshot = await getOrderArrivalSnapshot({
6903
+ connector,
6904
+ symbol
6905
+ });
6906
+ validateEntryProtectionAtArrival({
6907
+ direction: decision.entryContext.direction,
6908
+ signalPrice: decision.entryContext.prices.currentPrice,
6909
+ bid: arrivalSnapshot.bid,
6910
+ ask: arrivalSnapshot.ask,
6911
+ arrivalMid: arrivalSnapshot.arrivalMid,
6912
+ takeProfits: decision.orderPlan.takeProfits,
6913
+ stopLossPrice: decision.orderPlan.stopLossPrice
6914
+ });
6915
+ }
6732
6916
  const orderPlaced = await connector.placeOrder({
6733
6917
  symbol,
6734
6918
  qty: decision.orderPlan.qty,
@@ -6736,6 +6920,7 @@ var executeEntryDecision = async ({
6736
6920
  timestamp: decision.entryContext.timestamp,
6737
6921
  direction: decision.entryContext.direction,
6738
6922
  positionIntent: decision.orderPlan.positionIntent,
6923
+ ...beforeSubmit ? { beforeSubmit } : {},
6739
6924
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {}
6740
6925
  });
6741
6926
  if (!orderPlaced) {
@@ -6777,6 +6962,23 @@ var executeEntryDecision = async ({
6777
6962
  { decision, entry, market }
6778
6963
  );
6779
6964
  } catch (err) {
6965
+ if (err instanceof EntrySubmissionValidationError) {
6966
+ if (signal) {
6967
+ signal.orderStatus = "skipped";
6968
+ signal.orderSkipReason = err.message;
6969
+ signal.allocatorDecision = {
6970
+ stage: "runtime_admission",
6971
+ status: "rejected",
6972
+ reason: err.message
6973
+ };
6974
+ }
6975
+ import_logger10.logger.warn(
6976
+ "entry rejected before submission: %s %s",
6977
+ symbol,
6978
+ err.message
6979
+ );
6980
+ return signal ?? err.message;
6981
+ }
6780
6982
  if (signal) {
6781
6983
  signal.orderStatus = "failed";
6782
6984
  if (typeof signal.orderFailureReason !== "string" || !signal.orderFailureReason.trim()) {
@@ -6791,9 +6993,9 @@ var executeEntryDecision = async ({
6791
6993
  market
6792
6994
  });
6793
6995
  if (err?.message === import_types.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY) {
6794
- import_logger9.logger.warn("order warning: %s %s", symbol, err);
6996
+ import_logger10.logger.warn("order warning: %s %s", symbol, err);
6795
6997
  } else {
6796
- import_logger9.logger.error("order error: %s %s", symbol, err);
6998
+ import_logger10.logger.error("order error: %s %s", symbol, err);
6797
6999
  }
6798
7000
  return signal ?? "ORDER_ERROR";
6799
7001
  }
@@ -6958,7 +7160,7 @@ var createStrategyRuntime = ({
6958
7160
  try {
6959
7161
  await projectHook(errorParams);
6960
7162
  } catch (hookError) {
6961
- import_logger10.logger.error(
7163
+ import_logger11.logger.error(
6962
7164
  "project hook onRuntimeError failed: %s %s",
6963
7165
  strategyName,
6964
7166
  hookError
@@ -6972,7 +7174,7 @@ var createStrategyRuntime = ({
6972
7174
  try {
6973
7175
  await onRuntimeError(errorParams);
6974
7176
  } catch (hookError) {
6975
- import_logger10.logger.error(
7177
+ import_logger11.logger.error(
6976
7178
  "runtime hook onRuntimeError failed: %s %s",
6977
7179
  strategyName,
6978
7180
  hookError
@@ -6986,7 +7188,7 @@ var createStrategyRuntime = ({
6986
7188
  try {
6987
7189
  return await hook(params);
6988
7190
  } catch (error) {
6989
- import_logger10.logger.error(
7191
+ import_logger11.logger.error(
6990
7192
  'strategy hook "%s" failed for %s: %s',
6991
7193
  stage,
6992
7194
  strategyName,