@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.
@@ -1048,13 +1048,213 @@ var assessSignalWithJev = async ({
1048
1048
  // src/strategyRuntime.ts
1049
1049
  var import_constants5 = require("@tradejs/core/constants");
1050
1050
  var import_strategies6 = require("@tradejs/core/strategies");
1051
- var import_logger10 = require("@tradejs/infra/logger");
1051
+ var import_logger11 = require("@tradejs/infra/logger");
1052
1052
 
1053
1053
  // src/strategyHelpers/runtime.ts
1054
- var import_logger8 = require("@tradejs/infra/logger");
1054
+ var import_logger9 = require("@tradejs/infra/logger");
1055
1055
  var import_constants3 = require("@tradejs/core/constants");
1056
1056
  var import_ml2 = require("@tradejs/infra/ml");
1057
1057
 
1058
+ // src/strategy/entrySubmissionValidation.ts
1059
+ var import_logger = require("@tradejs/infra/logger");
1060
+
1061
+ // src/strategy/entryExecutionValidation.ts
1062
+ var EntrySubmissionValidationError = class extends Error {
1063
+ constructor() {
1064
+ super(...arguments);
1065
+ this.name = "EntrySubmissionValidationError";
1066
+ }
1067
+ };
1068
+ var resolveEntryExecutionLimits = (config, direction) => {
1069
+ const side = config[direction];
1070
+ const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
1071
+ const maximum = config.MAX_LOSS_VALUE;
1072
+ if (minimum == null && maximum == null) return void 0;
1073
+ const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
1074
+ const maxLossValue = maximum == null ? void 0 : Number(maximum);
1075
+ const feeRate = Number(config.RISK_FEE_RATE ?? 0);
1076
+ const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
1077
+ const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
1078
+ const slippageBps = slippage + impact;
1079
+ 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)
1080
+ throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
1081
+ return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
1082
+ };
1083
+ var validateEntryExecution = ({
1084
+ direction,
1085
+ price,
1086
+ qty,
1087
+ stopLossPrice,
1088
+ takeProfitPrice,
1089
+ limits
1090
+ }) => {
1091
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
1092
+ throw new EntrySubmissionValidationError(
1093
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1094
+ );
1095
+ const side = direction === "LONG" ? 1 : -1;
1096
+ if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
1097
+ throw new EntrySubmissionValidationError(
1098
+ "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
1099
+ );
1100
+ const slippage = limits.slippageBps / 1e4;
1101
+ const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
1102
+ const entryCost = cost(price, side);
1103
+ const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
1104
+ const rewardPerUnit = Math.max(
1105
+ 0,
1106
+ Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
1107
+ );
1108
+ const netRiskRatio = rewardPerUnit / lossPerUnit;
1109
+ const plannedLossValue = qty * lossPerUnit;
1110
+ if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
1111
+ throw new EntrySubmissionValidationError(
1112
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1113
+ );
1114
+ if (limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio)
1115
+ throw new EntrySubmissionValidationError(
1116
+ "ENTRY_RR_BELOW_MIN_AT_SUBMISSION"
1117
+ );
1118
+ if (limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9))
1119
+ throw new EntrySubmissionValidationError(
1120
+ "ENTRY_RISK_EXCEEDS_MAX_LOSS_AT_SUBMISSION"
1121
+ );
1122
+ return { netRiskRatio, plannedLossValue };
1123
+ };
1124
+
1125
+ // src/strategy/entrySubmissionValidation.ts
1126
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
1127
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
1128
+ var getOrderArrivalSnapshot = async ({
1129
+ connector,
1130
+ symbol,
1131
+ requireTimestamp = false
1132
+ }) => {
1133
+ if (typeof connector.getTopOfBookTicker !== "function") {
1134
+ return {
1135
+ arrivalSnapshotTime: Date.now(),
1136
+ arrivalSource: "unavailable",
1137
+ bid: null,
1138
+ ask: null,
1139
+ arrivalMid: null,
1140
+ spreadBps: null
1141
+ };
1142
+ }
1143
+ try {
1144
+ const ticker = await connector.getTopOfBookTicker(symbol);
1145
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
1146
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
1147
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
1148
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
1149
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
1150
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
1151
+ return {
1152
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
1153
+ arrivalSource: "top_of_book",
1154
+ bid,
1155
+ ask,
1156
+ arrivalMid,
1157
+ spreadBps
1158
+ };
1159
+ } catch (error) {
1160
+ import_logger.logger.warn(
1161
+ "runtime order arrival snapshot failed: %s %s",
1162
+ symbol,
1163
+ error?.message || String(error)
1164
+ );
1165
+ return {
1166
+ arrivalSnapshotTime: Date.now(),
1167
+ arrivalSource: "top_of_book_error",
1168
+ bid: null,
1169
+ ask: null,
1170
+ arrivalMid: null,
1171
+ spreadBps: null
1172
+ };
1173
+ }
1174
+ };
1175
+ var validateEntryProtectionAtArrival = ({
1176
+ direction,
1177
+ signalPrice,
1178
+ bid,
1179
+ ask,
1180
+ arrivalMid,
1181
+ takeProfits,
1182
+ stopLossPrice
1183
+ }) => {
1184
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
1185
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
1186
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
1187
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
1188
+ }
1189
+ for (const takeProfit of takeProfits) {
1190
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
1191
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
1192
+ }
1193
+ }
1194
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
1195
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
1196
+ }
1197
+ };
1198
+ var createEntrySubmissionValidation = ({
1199
+ connector,
1200
+ symbol,
1201
+ direction,
1202
+ takeProfits,
1203
+ stopLossPrice,
1204
+ limits,
1205
+ onValidated
1206
+ }) => {
1207
+ if (!limits) return void 0;
1208
+ if (!connector.capabilities?.orderSubmissionValidation)
1209
+ throw new EntrySubmissionValidationError(
1210
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
1211
+ );
1212
+ return async ({ qty, executionPrice }) => {
1213
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
1214
+ const snapshot2 = simulated && executionPrice != null ? {
1215
+ arrivalSnapshotTime: Date.now(),
1216
+ arrivalSource: "simulation",
1217
+ bid: executionPrice,
1218
+ ask: executionPrice,
1219
+ arrivalMid: executionPrice,
1220
+ spreadBps: 0
1221
+ } : await getOrderArrivalSnapshot({
1222
+ connector,
1223
+ symbol,
1224
+ requireTimestamp: true
1225
+ });
1226
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
1227
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
1228
+ throw new EntrySubmissionValidationError(
1229
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
1230
+ );
1231
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
1232
+ throw new EntrySubmissionValidationError(
1233
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
1234
+ );
1235
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
1236
+ validateEntryProtectionAtArrival({
1237
+ direction,
1238
+ signalPrice: price,
1239
+ bid: snapshot2.bid,
1240
+ ask: snapshot2.ask,
1241
+ arrivalMid: snapshot2.arrivalMid,
1242
+ takeProfits,
1243
+ stopLossPrice
1244
+ });
1245
+ const prices = takeProfits.map((tp) => tp.price);
1246
+ const economics2 = validateEntryExecution({
1247
+ direction,
1248
+ price,
1249
+ qty,
1250
+ stopLossPrice,
1251
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
1252
+ limits
1253
+ });
1254
+ onValidated?.(snapshot2, economics2);
1255
+ };
1256
+ };
1257
+
1058
1258
  // src/ai.ts
1059
1259
  var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
1060
1260
  var import_redis = require("@tradejs/infra/redis");
@@ -1639,7 +1839,7 @@ var import_fs = __toESM(require("fs"));
1639
1839
  var import_path = __toESM(require("path"));
1640
1840
  var import_url = require("url");
1641
1841
  var import_config = require("@tradejs/core/config");
1642
- var import_logger = require("@tradejs/infra/logger");
1842
+ var import_logger2 = require("@tradejs/infra/logger");
1643
1843
  var CONFIG_FILE_NAMES = [
1644
1844
  "tradejs.config.ts",
1645
1845
  "tradejs.config.mts",
@@ -1906,11 +2106,11 @@ var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
1906
2106
  cachedByCwd.set(cwd, config);
1907
2107
  if (!announcedConfigFile.has(configFilePath)) {
1908
2108
  announcedConfigFile.add(configFilePath);
1909
- import_logger.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
2109
+ import_logger2.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
1910
2110
  }
1911
2111
  return config;
1912
2112
  } catch (error) {
1913
- import_logger.logger.log(
2113
+ import_logger2.logger.log(
1914
2114
  "warn",
1915
2115
  "Failed to load TradeJS config from %s: %s",
1916
2116
  configFilePath,
@@ -2876,7 +3076,7 @@ var import_node_crypto = require("crypto");
2876
3076
  var import_constants = require("@tradejs/core/constants");
2877
3077
  var import_time = require("@tradejs/core/time");
2878
3078
  var import_trade = require("@tradejs/core/trade");
2879
- var import_logger2 = require("@tradejs/infra/logger");
3079
+ var import_logger3 = require("@tradejs/infra/logger");
2880
3080
  var import_redis2 = require("@tradejs/infra/redis");
2881
3081
  var now = () => Date.now();
2882
3082
  var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
@@ -2935,7 +3135,7 @@ var recordRuntimeTradeOpen = async (params) => {
2935
3135
  )
2936
3136
  ]);
2937
3137
  } catch (error) {
2938
- import_logger2.logger.error(
3138
+ import_logger3.logger.error(
2939
3139
  "runtime trade open journal failed: %s %s",
2940
3140
  record.symbol,
2941
3141
  error?.message || String(error)
@@ -3003,7 +3203,7 @@ var recordRuntimeTradeIncrease = async (params) => {
3003
3203
  )
3004
3204
  ]);
3005
3205
  } catch (error) {
3006
- import_logger2.logger.error(
3206
+ import_logger3.logger.error(
3007
3207
  "runtime trade increase journal failed: %s %s",
3008
3208
  symbol,
3009
3209
  error?.message || String(error)
@@ -3110,7 +3310,7 @@ var markRuntimeTradeClosed = async (params) => {
3110
3310
  )
3111
3311
  ]);
3112
3312
  } catch (error) {
3113
- import_logger2.logger.error(
3313
+ import_logger3.logger.error(
3114
3314
  "runtime trade close journal failed: %s %s",
3115
3315
  symbol,
3116
3316
  error?.message || String(error)
@@ -3120,11 +3320,11 @@ var markRuntimeTradeClosed = async (params) => {
3120
3320
  };
3121
3321
 
3122
3322
  // src/strategyHelpers/marketContextStages.ts
3123
- var import_logger7 = require("@tradejs/infra/logger");
3323
+ var import_logger8 = require("@tradejs/infra/logger");
3124
3324
 
3125
3325
  // src/strategyHelpers/binanceMarketContext.ts
3126
3326
  var import_marketContext = require("@tradejs/infra/timescale/marketContext");
3127
- var import_logger3 = require("@tradejs/infra/logger");
3327
+ var import_logger4 = require("@tradejs/infra/logger");
3128
3328
  var import_strategies = require("@tradejs/core/strategies");
3129
3329
 
3130
3330
  // src/binanceBreadthUniverses.ts
@@ -3488,7 +3688,7 @@ var parseEnabledFlag = (value, env) => {
3488
3688
  if (normalized === "live") return env !== "BACKTEST";
3489
3689
  return false;
3490
3690
  };
3491
- var toFiniteNumberOrNull = (value) => {
3691
+ var toFiniteNumberOrNull2 = (value) => {
3492
3692
  const numeric = typeof value === "number" ? value : Number(value);
3493
3693
  return Number.isFinite(numeric) ? numeric : null;
3494
3694
  };
@@ -3613,14 +3813,14 @@ var toTradeFlowContext = (row, interval) => row ? {
3613
3813
  asOfTs: row.ts.getTime(),
3614
3814
  ageMs: row.ageMs,
3615
3815
  stale: row.stale,
3616
- trades: toFiniteNumberOrNull(row.trades),
3617
- buyPressurePct: toFiniteNumberOrNull(row.buyPressurePct),
3618
- buyBaseVolume: toFiniteNumberOrNull(row.buyBaseVolume),
3619
- sellBaseVolume: toFiniteNumberOrNull(row.sellBaseVolume),
3620
- buyQuoteVolume: toFiniteNumberOrNull(row.buyQuoteVolume),
3621
- sellQuoteVolume: toFiniteNumberOrNull(row.sellQuoteVolume),
3622
- netBaseDelta: toFiniteNumberOrNull(row.netBaseDelta),
3623
- netQuoteDelta: toFiniteNumberOrNull(row.netQuoteDelta)
3816
+ trades: toFiniteNumberOrNull2(row.trades),
3817
+ buyPressurePct: toFiniteNumberOrNull2(row.buyPressurePct),
3818
+ buyBaseVolume: toFiniteNumberOrNull2(row.buyBaseVolume),
3819
+ sellBaseVolume: toFiniteNumberOrNull2(row.sellBaseVolume),
3820
+ buyQuoteVolume: toFiniteNumberOrNull2(row.buyQuoteVolume),
3821
+ sellQuoteVolume: toFiniteNumberOrNull2(row.sellQuoteVolume),
3822
+ netBaseDelta: toFiniteNumberOrNull2(row.netBaseDelta),
3823
+ netQuoteDelta: toFiniteNumberOrNull2(row.netQuoteDelta)
3624
3824
  } : null;
3625
3825
  var getCachedReferenceRows = ({
3626
3826
  referenceSymbols,
@@ -3709,16 +3909,16 @@ var toMarketBreadthContext = (breadth, interval) => ({
3709
3909
  asOfTs: breadth.ts.getTime(),
3710
3910
  ageMs: breadth.ageMs,
3711
3911
  stale: breadth.stale,
3712
- symbolsCount: toFiniteNumberOrNull(breadth.symbolsCount),
3713
- advancers: toFiniteNumberOrNull(breadth.advancers),
3714
- decliners: toFiniteNumberOrNull(breadth.decliners),
3715
- unchanged: toFiniteNumberOrNull(breadth.unchanged),
3716
- advanceDeclineRatio: toFiniteNumberOrNull(breadth.advanceDeclineRatio),
3717
- pctAboveMa20: toFiniteNumberOrNull(breadth.pctAboveMa20),
3718
- pctAboveMa50: toFiniteNumberOrNull(breadth.pctAboveMa50),
3719
- equalWeightedReturn: toFiniteNumberOrNull(breadth.equalWeightedReturn),
3720
- volumeWeightedReturn: toFiniteNumberOrNull(breadth.volumeWeightedReturn),
3721
- dispersion: toFiniteNumberOrNull(breadth.dispersion)
3912
+ symbolsCount: toFiniteNumberOrNull2(breadth.symbolsCount),
3913
+ advancers: toFiniteNumberOrNull2(breadth.advancers),
3914
+ decliners: toFiniteNumberOrNull2(breadth.decliners),
3915
+ unchanged: toFiniteNumberOrNull2(breadth.unchanged),
3916
+ advanceDeclineRatio: toFiniteNumberOrNull2(breadth.advanceDeclineRatio),
3917
+ pctAboveMa20: toFiniteNumberOrNull2(breadth.pctAboveMa20),
3918
+ pctAboveMa50: toFiniteNumberOrNull2(breadth.pctAboveMa50),
3919
+ equalWeightedReturn: toFiniteNumberOrNull2(breadth.equalWeightedReturn),
3920
+ volumeWeightedReturn: toFiniteNumberOrNull2(breadth.volumeWeightedReturn),
3921
+ dispersion: toFiniteNumberOrNull2(breadth.dispersion)
3722
3922
  });
3723
3923
  var enrichSignalWithBinanceMarketContext = async (params) => {
3724
3924
  const {
@@ -3819,46 +4019,46 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3819
4019
  asOfTs: primaryBreadth.ts.getTime(),
3820
4020
  ageMs: primaryBreadth.ageMs,
3821
4021
  stale: primaryBreadth.stale,
3822
- btcReturn1h: toFiniteNumberOrNull(
4022
+ btcReturn1h: toFiniteNumberOrNull2(
3823
4023
  primaryBreadth.btcReturn1h
3824
4024
  ),
3825
- btcReturn4h: toFiniteNumberOrNull(
4025
+ btcReturn4h: toFiniteNumberOrNull2(
3826
4026
  primaryBreadth.btcReturn4h
3827
4027
  ),
3828
- btcReturn24h: toFiniteNumberOrNull(
4028
+ btcReturn24h: toFiniteNumberOrNull2(
3829
4029
  primaryBreadth.btcReturn24h
3830
4030
  ),
3831
- altBasketReturn1h: toFiniteNumberOrNull(
4031
+ altBasketReturn1h: toFiniteNumberOrNull2(
3832
4032
  primaryBreadth.altBasketReturn1h
3833
4033
  ),
3834
- altBasketReturn4h: toFiniteNumberOrNull(
4034
+ altBasketReturn4h: toFiniteNumberOrNull2(
3835
4035
  primaryBreadth.altBasketReturn4h
3836
4036
  ),
3837
- altBasketReturn24h: toFiniteNumberOrNull(
4037
+ altBasketReturn24h: toFiniteNumberOrNull2(
3838
4038
  primaryBreadth.altBasketReturn24h
3839
4039
  ),
3840
- btcVsAltReturn1h: toFiniteNumberOrNull(
4040
+ btcVsAltReturn1h: toFiniteNumberOrNull2(
3841
4041
  primaryBreadth.btcVsAltReturn1h
3842
4042
  ),
3843
- btcVsAltReturn4h: toFiniteNumberOrNull(
4043
+ btcVsAltReturn4h: toFiniteNumberOrNull2(
3844
4044
  primaryBreadth.btcVsAltReturn4h
3845
4045
  ),
3846
- btcVsAltReturn24h: toFiniteNumberOrNull(
4046
+ btcVsAltReturn24h: toFiniteNumberOrNull2(
3847
4047
  primaryBreadth.btcVsAltReturn24h
3848
4048
  ),
3849
- btcTurnoverShare1h: toFiniteNumberOrNull(
4049
+ btcTurnoverShare1h: toFiniteNumberOrNull2(
3850
4050
  primaryBreadth.btcTurnoverShare1h
3851
4051
  ),
3852
- btcTurnoverShare24h: toFiniteNumberOrNull(
4052
+ btcTurnoverShare24h: toFiniteNumberOrNull2(
3853
4053
  primaryBreadth.btcTurnoverShare24h
3854
4054
  ),
3855
- btcTurnoverShareChange24h: toFiniteNumberOrNull(
4055
+ btcTurnoverShareChange24h: toFiniteNumberOrNull2(
3856
4056
  primaryBreadth.btcTurnoverShareChange24h
3857
4057
  ),
3858
- altVolToBtcVol24h: toFiniteNumberOrNull(
4058
+ altVolToBtcVol24h: toFiniteNumberOrNull2(
3859
4059
  primaryBreadth.altVolToBtcVol24h
3860
4060
  ),
3861
- altDispersion24h: toFiniteNumberOrNull(
4061
+ altDispersion24h: toFiniteNumberOrNull2(
3862
4062
  primaryBreadth.altDispersion24h
3863
4063
  ),
3864
4064
  regime: primaryBreadth.btcAltRegime ?? "unknown"
@@ -3875,7 +4075,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3875
4075
  throw error;
3876
4076
  }
3877
4077
  binanceMarketContextUnavailable = true;
3878
- import_logger3.logger.warn(
4078
+ import_logger4.logger.warn(
3879
4079
  "Binance market context disabled after Timescale read failure: %s",
3880
4080
  String(error)
3881
4081
  );
@@ -3885,7 +4085,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3885
4085
 
3886
4086
  // src/strategyHelpers/coinMarketCapContext.ts
3887
4087
  var import_strategies2 = require("@tradejs/core/strategies");
3888
- var import_logger4 = require("@tradejs/infra/logger");
4088
+ var import_logger5 = require("@tradejs/infra/logger");
3889
4089
  var import_marketContext2 = require("@tradejs/infra/timescale/marketContext");
3890
4090
  var DEFAULT_MAX_AGE_MS = 48 * 60 * 6e4;
3891
4091
  var SOURCE_GLOBAL_DAILY = "coinmarketcap_global";
@@ -3923,7 +4123,7 @@ var asInt = (value, fallback) => {
3923
4123
  const parsed = Number.parseInt(String(value ?? ""), 10);
3924
4124
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
3925
4125
  };
3926
- var toFiniteNumberOrNull2 = (value) => {
4126
+ var toFiniteNumberOrNull3 = (value) => {
3927
4127
  const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
3928
4128
  return Number.isFinite(numeric) ? numeric : null;
3929
4129
  };
@@ -4157,12 +4357,12 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4157
4357
  const ethRow = references.get("ETHUSDT") ?? null;
4158
4358
  const previousBtcRow = previousReferences.get("BTCUSDT") ?? null;
4159
4359
  const previousEthRow = previousReferences.get("ETHUSDT") ?? null;
4160
- const btcMarketCapUsd = toFiniteNumberOrNull2(btcRow?.marketCapUsd);
4161
- const ethMarketCapUsd = toFiniteNumberOrNull2(ethRow?.marketCapUsd);
4162
- const previousBtcMarketCapUsd = toFiniteNumberOrNull2(
4360
+ const btcMarketCapUsd = toFiniteNumberOrNull3(btcRow?.marketCapUsd);
4361
+ const ethMarketCapUsd = toFiniteNumberOrNull3(ethRow?.marketCapUsd);
4362
+ const previousBtcMarketCapUsd = toFiniteNumberOrNull3(
4163
4363
  previousBtcRow?.marketCapUsd
4164
4364
  );
4165
- const previousEthMarketCapUsd = toFiniteNumberOrNull2(
4365
+ const previousEthMarketCapUsd = toFiniteNumberOrNull3(
4166
4366
  previousEthRow?.marketCapUsd
4167
4367
  );
4168
4368
  const ethBtcMarketCapRatio = safeDivide(ethMarketCapUsd, btcMarketCapUsd);
@@ -4171,16 +4371,16 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4171
4371
  previousBtcMarketCapUsd
4172
4372
  );
4173
4373
  const ethBtcMarketCapRatioChange24hPct = ethBtcMarketCapRatio != null && previousEthBtcMarketCapRatio != null && previousEthBtcMarketCapRatio > 0 ? (ethBtcMarketCapRatio - previousEthBtcMarketCapRatio) / previousEthBtcMarketCapRatio : null;
4174
- const btcVolumeUsd = toFiniteNumberOrNull2(btcRow?.volumeUsd);
4175
- const ethVolumeUsd = toFiniteNumberOrNull2(ethRow?.volumeUsd);
4374
+ const btcVolumeUsd = toFiniteNumberOrNull3(btcRow?.volumeUsd);
4375
+ const ethVolumeUsd = toFiniteNumberOrNull3(ethRow?.volumeUsd);
4176
4376
  const referenceStale = btcRow?.stale === true || ethRow?.stale === true || !btcRow || !ethRow;
4177
- const btcDominanceChange24hPct = toFiniteNumberOrNull2(
4377
+ const btcDominanceChange24hPct = toFiniteNumberOrNull3(
4178
4378
  globalRow?.btcDominanceChange24hPct
4179
4379
  );
4180
- const altMarketCapChange24hPct = toFiniteNumberOrNull2(
4380
+ const altMarketCapChange24hPct = toFiniteNumberOrNull3(
4181
4381
  globalRow?.altMarketCapChange24hPct
4182
4382
  );
4183
- const altVolumeChange24hPct = toFiniteNumberOrNull2(
4383
+ const altVolumeChange24hPct = toFiniteNumberOrNull3(
4184
4384
  globalRow?.altVolumeChange24hPct
4185
4385
  );
4186
4386
  const altLiquidityRegime = globalRow ? toAltLiquidityRegime({
@@ -4191,19 +4391,19 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4191
4391
  }) : "unknown";
4192
4392
  const exchangeLiquidityRegime = exchangeLiquidityRow ? toExchangeLiquidityRegime({
4193
4393
  stale: exchangeLiquidityRow.stale,
4194
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
4394
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
4195
4395
  exchangeLiquidityRow.totalVolumeChange24hPct
4196
4396
  ),
4197
4397
  fallback: exchangeLiquidityRow.liquidityRegime ?? "unknown"
4198
4398
  }) : "unknown";
4199
4399
  const cmc100Row = indexRows.get("cmc100") ?? null;
4200
4400
  const cmc20Row = indexRows.get("cmc20") ?? null;
4201
- const cmc100Value = toFiniteNumberOrNull2(cmc100Row?.value);
4202
- const cmc20Value = toFiniteNumberOrNull2(cmc20Row?.value);
4203
- const cmc100Change24hPct = toFiniteNumberOrNull2(
4401
+ const cmc100Value = toFiniteNumberOrNull3(cmc100Row?.value);
4402
+ const cmc20Value = toFiniteNumberOrNull3(cmc20Row?.value);
4403
+ const cmc100Change24hPct = toFiniteNumberOrNull3(
4204
4404
  cmc100Row?.valueChange24hPct
4205
4405
  );
4206
- const cmc20Change24hPct = toFiniteNumberOrNull2(cmc20Row?.valueChange24hPct);
4406
+ const cmc20Change24hPct = toFiniteNumberOrNull3(cmc20Row?.valueChange24hPct);
4207
4407
  const cmc20ToCmc100Ratio = safeDivide(cmc20Value, cmc100Value);
4208
4408
  const cmc20ToCmc100RatioChange24hPct = cmc20Change24hPct != null && cmc100Change24hPct != null ? (1 + cmc20Change24hPct) / (1 + cmc100Change24hPct) - 1 : null;
4209
4409
  const indexStale = cmc100Row?.stale === true || cmc20Row?.stale === true || !cmc100Row || !cmc20Row;
@@ -4227,41 +4427,41 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4227
4427
  asOfTs: globalRow.ts.getTime(),
4228
4428
  ageMs: globalRow.ageMs,
4229
4429
  stale: globalRow.stale,
4230
- totalMarketCapUsd: toFiniteNumberOrNull2(
4430
+ totalMarketCapUsd: toFiniteNumberOrNull3(
4231
4431
  globalRow.totalMarketCapUsd
4232
4432
  ),
4233
- totalVolumeUsd: toFiniteNumberOrNull2(
4433
+ totalVolumeUsd: toFiniteNumberOrNull3(
4234
4434
  globalRow.totalVolumeUsd
4235
4435
  ),
4236
- totalVolumeReportedUsd: toFiniteNumberOrNull2(
4436
+ totalVolumeReportedUsd: toFiniteNumberOrNull3(
4237
4437
  globalRow.totalVolumeReportedUsd
4238
4438
  ),
4239
- altMarketCapUsd: toFiniteNumberOrNull2(
4439
+ altMarketCapUsd: toFiniteNumberOrNull3(
4240
4440
  globalRow.altMarketCapUsd
4241
4441
  ),
4242
- altVolumeUsd: toFiniteNumberOrNull2(globalRow.altVolumeUsd),
4243
- altVolumeReportedUsd: toFiniteNumberOrNull2(
4442
+ altVolumeUsd: toFiniteNumberOrNull3(globalRow.altVolumeUsd),
4443
+ altVolumeReportedUsd: toFiniteNumberOrNull3(
4244
4444
  globalRow.altVolumeReportedUsd
4245
4445
  ),
4246
- btcDominancePct: toFiniteNumberOrNull2(
4446
+ btcDominancePct: toFiniteNumberOrNull3(
4247
4447
  globalRow.btcDominancePct
4248
4448
  ),
4249
- ethDominancePct: toFiniteNumberOrNull2(
4449
+ ethDominancePct: toFiniteNumberOrNull3(
4250
4450
  globalRow.ethDominancePct
4251
4451
  ),
4252
4452
  btcDominanceChange24hPct,
4253
- ethDominanceChange24hPct: toFiniteNumberOrNull2(
4453
+ ethDominanceChange24hPct: toFiniteNumberOrNull3(
4254
4454
  globalRow.ethDominanceChange24hPct
4255
4455
  ),
4256
4456
  altMarketCapChange24hPct,
4257
4457
  altVolumeChange24hPct,
4258
- activeCryptocurrencies: toFiniteNumberOrNull2(
4458
+ activeCryptocurrencies: toFiniteNumberOrNull3(
4259
4459
  globalRow.activeCryptocurrencies
4260
4460
  ),
4261
- activeExchanges: toFiniteNumberOrNull2(
4461
+ activeExchanges: toFiniteNumberOrNull3(
4262
4462
  globalRow.activeExchanges
4263
4463
  ),
4264
- activeMarketPairs: toFiniteNumberOrNull2(
4464
+ activeMarketPairs: toFiniteNumberOrNull3(
4265
4465
  globalRow.activeMarketPairs
4266
4466
  ),
4267
4467
  altLiquidityRegime
@@ -4306,22 +4506,22 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4306
4506
  asOfTs: exchangeLiquidityRow.ts.getTime(),
4307
4507
  ageMs: exchangeLiquidityRow.ageMs,
4308
4508
  stale: exchangeLiquidityRow.stale,
4309
- exchangesCount: toFiniteNumberOrNull2(
4509
+ exchangesCount: toFiniteNumberOrNull3(
4310
4510
  exchangeLiquidityRow.exchangesCount
4311
4511
  ),
4312
- totalVolumeUsd: toFiniteNumberOrNull2(
4512
+ totalVolumeUsd: toFiniteNumberOrNull3(
4313
4513
  exchangeLiquidityRow.totalVolumeUsd
4314
4514
  ),
4315
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
4515
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
4316
4516
  exchangeLiquidityRow.totalVolumeChange24hPct
4317
4517
  ),
4318
- binanceVolumeUsd: toFiniteNumberOrNull2(
4518
+ binanceVolumeUsd: toFiniteNumberOrNull3(
4319
4519
  exchangeLiquidityRow.binanceVolumeUsd
4320
4520
  ),
4321
- binanceVolumeShare: toFiniteNumberOrNull2(
4521
+ binanceVolumeShare: toFiniteNumberOrNull3(
4322
4522
  exchangeLiquidityRow.binanceVolumeShare
4323
4523
  ),
4324
- topExchangeVolumeShare: toFiniteNumberOrNull2(
4524
+ topExchangeVolumeShare: toFiniteNumberOrNull3(
4325
4525
  exchangeLiquidityRow.topExchangeVolumeShare
4326
4526
  ),
4327
4527
  liquidityRegime: exchangeLiquidityRegime
@@ -4334,11 +4534,11 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4334
4534
  asOfTs: fearGreedRow.ts.getTime(),
4335
4535
  ageMs: fearGreedRow.ageMs,
4336
4536
  stale: fearGreedRow.stale,
4337
- value: toFiniteNumberOrNull2(fearGreedRow.value),
4338
- valueChange24h: toFiniteNumberOrNull2(
4537
+ value: toFiniteNumberOrNull3(fearGreedRow.value),
4538
+ valueChange24h: toFiniteNumberOrNull3(
4339
4539
  fearGreedRow.valueChange24h
4340
4540
  ),
4341
- valueChange7d: toFiniteNumberOrNull2(
4541
+ valueChange7d: toFiniteNumberOrNull3(
4342
4542
  fearGreedRow.valueChange7d
4343
4543
  ),
4344
4544
  classification: fearGreedRow.classification ?? "Unknown",
@@ -4358,13 +4558,13 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4358
4558
  cmc100Value,
4359
4559
  cmc100Change24hPct,
4360
4560
  cmc100TopConstituentSymbol: cmc100Row?.topConstituentSymbol ?? null,
4361
- cmc100TopConstituentWeightPct: toFiniteNumberOrNull2(
4561
+ cmc100TopConstituentWeightPct: toFiniteNumberOrNull3(
4362
4562
  cmc100Row?.topConstituentWeightPct
4363
4563
  ),
4364
4564
  cmc20Value,
4365
4565
  cmc20Change24hPct,
4366
4566
  cmc20TopConstituentSymbol: cmc20Row?.topConstituentSymbol ?? null,
4367
- cmc20TopConstituentWeightPct: toFiniteNumberOrNull2(
4567
+ cmc20TopConstituentWeightPct: toFiniteNumberOrNull3(
4368
4568
  cmc20Row?.topConstituentWeightPct
4369
4569
  ),
4370
4570
  cmc20ToCmc100Ratio,
@@ -4382,7 +4582,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4382
4582
  throw error;
4383
4583
  }
4384
4584
  coinMarketCapContextUnavailable = true;
4385
- import_logger4.logger.warn(
4585
+ import_logger5.logger.warn(
4386
4586
  "CoinMarketCap context disabled after Timescale read failure: %s",
4387
4587
  String(error)
4388
4588
  );
@@ -4396,7 +4596,7 @@ var import_data2 = require("@tradejs/core/data");
4396
4596
  var import_strategies3 = require("@tradejs/core/strategies");
4397
4597
  var import_constants2 = require("@tradejs/core/constants");
4398
4598
  var import_derivatives = require("@tradejs/infra/timescale/derivatives");
4399
- var import_logger5 = require("@tradejs/infra/logger");
4599
+ var import_logger6 = require("@tradejs/infra/logger");
4400
4600
  var STORED_INTERVALS = ["15m", "1h"];
4401
4601
  var CONTEXT_INTERVALS = ["15m", "1h"];
4402
4602
  var DEFAULT_LOOKBACK_HOURS = 48;
@@ -4451,7 +4651,7 @@ var resolvePrimaryReferenceSymbol2 = () => PRIMARY_DERIVATIVES_REFERENCE_SYMBOL;
4451
4651
  var resolveSecondaryReferenceSymbol = () => SECONDARY_DERIVATIVES_REFERENCE_SYMBOL;
4452
4652
  var getPrimaryIntervalContext = (context) => context?.intervals["15m"] ?? context?.intervals["1h"] ?? null;
4453
4653
  var hasDerivativesSymbolData = (context) => Object.keys(context.intervals).length > 0 && !context.summary.riskFlags.includes("missing_derivatives");
4454
- var toFiniteNumberOrNull3 = (value) => {
4654
+ var toFiniteNumberOrNull4 = (value) => {
4455
4655
  if (typeof value === "number" && Number.isFinite(value)) return value;
4456
4656
  if (typeof value === "string" && value.trim()) {
4457
4657
  const parsed = Number(value);
@@ -4465,8 +4665,8 @@ var roundNullable = (value, digits = 4) => {
4465
4665
  return Math.round(value * multiplier) / multiplier;
4466
4666
  };
4467
4667
  var deltaNullable = (targetValue, referenceValue) => {
4468
- const target = toFiniteNumberOrNull3(targetValue);
4469
- const reference = toFiniteNumberOrNull3(referenceValue);
4668
+ const target = toFiniteNumberOrNull4(targetValue);
4669
+ const reference = toFiniteNumberOrNull4(referenceValue);
4470
4670
  return target == null || reference == null ? null : roundNullable(target - reference);
4471
4671
  };
4472
4672
  var buildTargetDerivedContext = (params) => {
@@ -4628,7 +4828,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4628
4828
  throw error;
4629
4829
  }
4630
4830
  derivativesContextUnavailable = true;
4631
- import_logger5.logger.warn(
4831
+ import_logger6.logger.warn(
4632
4832
  "Derivatives context disabled after Timescale read failure: %s",
4633
4833
  String(error)
4634
4834
  );
@@ -4640,7 +4840,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4640
4840
  var import_data3 = require("@tradejs/core/data");
4641
4841
  var import_strategies4 = require("@tradejs/core/strategies");
4642
4842
  var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWhales");
4643
- var import_logger6 = require("@tradejs/infra/logger");
4843
+ var import_logger7 = require("@tradejs/infra/logger");
4644
4844
 
4645
4845
  // src/hyperliquidWhaleUniverse.ts
4646
4846
  var import_node_crypto3 = require("crypto");
@@ -5160,7 +5360,7 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
5160
5360
  throw error;
5161
5361
  }
5162
5362
  hyperliquidWhaleContextUnavailable = true;
5163
- import_logger6.logger.warn(
5363
+ import_logger7.logger.warn(
5164
5364
  "Hyperliquid whale context disabled after Timescale read failure: %s",
5165
5365
  String(error)
5166
5366
  );
@@ -5243,7 +5443,7 @@ var runMarketContextStage = async ({
5243
5443
  elapsedMs: Date.now() - startedAt
5244
5444
  };
5245
5445
  if (status === "timed_out") {
5246
- import_logger7.logger.warn(
5446
+ import_logger8.logger.warn(
5247
5447
  "Market context stage timed out: %s after %sms",
5248
5448
  stage,
5249
5449
  result.elapsedMs
@@ -5433,79 +5633,10 @@ var enrichSignalWithAi = async ({
5433
5633
  signal.aiAnalysis = analysis;
5434
5634
  return resolveAiQuality(analysis, direction);
5435
5635
  } catch (err) {
5436
- import_logger8.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
5636
+ import_logger9.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
5437
5637
  }
5438
5638
  return void 0;
5439
5639
  };
5440
- var toFiniteNumberOrNull4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
5441
- var getOrderArrivalSnapshot = async ({
5442
- connector,
5443
- symbol
5444
- }) => {
5445
- if (typeof connector.getTopOfBookTicker !== "function") {
5446
- return {
5447
- arrivalSnapshotTime: Date.now(),
5448
- arrivalSource: "unavailable",
5449
- bid: null,
5450
- ask: null,
5451
- arrivalMid: null,
5452
- spreadBps: null
5453
- };
5454
- }
5455
- try {
5456
- const ticker = await connector.getTopOfBookTicker(symbol);
5457
- const arrivalSnapshotTime = toFiniteNumberOrNull4(ticker?.timestamp);
5458
- const bid = toFiniteNumberOrNull4(ticker?.bidPrice);
5459
- const ask = toFiniteNumberOrNull4(ticker?.askPrice);
5460
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
5461
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
5462
- return {
5463
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
5464
- arrivalSource: "top_of_book",
5465
- bid,
5466
- ask,
5467
- arrivalMid,
5468
- spreadBps
5469
- };
5470
- } catch (error) {
5471
- import_logger8.logger.warn(
5472
- "runtime order arrival snapshot failed: %s %s",
5473
- symbol,
5474
- error?.message || String(error)
5475
- );
5476
- return {
5477
- arrivalSnapshotTime: Date.now(),
5478
- arrivalSource: "top_of_book_error",
5479
- bid: null,
5480
- ask: null,
5481
- arrivalMid: null,
5482
- spreadBps: null
5483
- };
5484
- }
5485
- };
5486
- var validateEntryProtectionAtArrival = ({
5487
- direction,
5488
- signalPrice,
5489
- bid,
5490
- ask,
5491
- arrivalMid,
5492
- takeProfits,
5493
- stopLossPrice
5494
- }) => {
5495
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
5496
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
5497
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
5498
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
5499
- }
5500
- for (const takeProfit of takeProfits) {
5501
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
5502
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
5503
- }
5504
- }
5505
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
5506
- throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
5507
- }
5508
- };
5509
5640
  var resolveRuntimeTelemetryQuality = ({
5510
5641
  signalClosePrice,
5511
5642
  arrivalMid,
@@ -5582,7 +5713,8 @@ var executeEntryOrder = async ({
5582
5713
  signal,
5583
5714
  beforePlaceOrder,
5584
5715
  recordRuntimeTrade = true,
5585
- leverage
5716
+ leverage,
5717
+ executionLimits
5586
5718
  }) => {
5587
5719
  await beforePlaceOrder?.();
5588
5720
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
@@ -5593,23 +5725,57 @@ var executeEntryOrder = async ({
5593
5725
  signal.orderQty = qty;
5594
5726
  signal.orderValue = qty * currentPrice;
5595
5727
  signal.orderFailureReason = void 0;
5596
- const arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
5597
- try {
5598
- validateEntryProtectionAtArrival({
5599
- direction,
5600
- signalPrice: currentPrice,
5601
- bid: arrivalSnapshot.bid,
5602
- ask: arrivalSnapshot.ask,
5603
- arrivalMid: arrivalSnapshot.arrivalMid,
5604
- takeProfits,
5605
- stopLossPrice
5606
- });
5607
- } catch (error) {
5608
- signal.orderStatus = "failed";
5609
- signal.orderFailureReason = error.message;
5610
- throw error;
5728
+ let arrivalSnapshot = {
5729
+ arrivalSnapshotTime: Date.now(),
5730
+ arrivalSource: "unavailable",
5731
+ bid: null,
5732
+ ask: null,
5733
+ arrivalMid: null,
5734
+ spreadBps: null
5735
+ };
5736
+ let orderSubmitTime = Date.now();
5737
+ const beforeSubmit = createEntrySubmissionValidation({
5738
+ connector,
5739
+ symbol,
5740
+ direction,
5741
+ takeProfits,
5742
+ stopLossPrice,
5743
+ limits: executionLimits,
5744
+ onValidated: (snapshot2, economics2) => {
5745
+ arrivalSnapshot = snapshot2;
5746
+ orderSubmitTime = Date.now();
5747
+ if (executionLimits?.maxLossValue != null) {
5748
+ signal.riskDecision = {
5749
+ stage: "order_plan",
5750
+ status: "approved",
5751
+ reason: "PLANNED_LOSS_WITHIN_MAX_LOSS_VALUE",
5752
+ plannedLossValue: economics2.plannedLossValue,
5753
+ maxLossValue: executionLimits.maxLossValue,
5754
+ limitUtilization: economics2.plannedLossValue / executionLimits.maxLossValue,
5755
+ enforced: true
5756
+ };
5757
+ }
5758
+ }
5759
+ });
5760
+ if (!beforeSubmit) {
5761
+ arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
5762
+ try {
5763
+ validateEntryProtectionAtArrival({
5764
+ direction,
5765
+ signalPrice: currentPrice,
5766
+ bid: arrivalSnapshot.bid,
5767
+ ask: arrivalSnapshot.ask,
5768
+ arrivalMid: arrivalSnapshot.arrivalMid,
5769
+ takeProfits,
5770
+ stopLossPrice
5771
+ });
5772
+ } catch (error) {
5773
+ signal.orderStatus = "failed";
5774
+ signal.orderFailureReason = error.message;
5775
+ throw error;
5776
+ }
5777
+ orderSubmitTime = Date.now();
5611
5778
  }
5612
- const orderSubmitTime = Date.now();
5613
5779
  const orderPlaced = await connector.placeOrder({
5614
5780
  symbol,
5615
5781
  qty,
@@ -5620,7 +5786,8 @@ var executeEntryOrder = async ({
5620
5786
  direction,
5621
5787
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
5622
5788
  orderId,
5623
- signal
5789
+ signal,
5790
+ ...beforeSubmit ? { beforeSubmit } : {}
5624
5791
  });
5625
5792
  const orderAckTime = Date.now();
5626
5793
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -6449,7 +6616,7 @@ var canUseSharedReplayState = ({
6449
6616
  }) => (env === "BACKTEST" || env === "PARITY") && Boolean(sharedReplayKey);
6450
6617
 
6451
6618
  // src/strategy/runtimeExecution.ts
6452
- var import_logger9 = require("@tradejs/infra/logger");
6619
+ var import_logger10 = require("@tradejs/infra/logger");
6453
6620
  var import_types = require("@tradejs/types");
6454
6621
  var buildExitOrderSignal = ({
6455
6622
  strategyName,
@@ -6501,7 +6668,7 @@ var handleExitDecision = async ({
6501
6668
  deploymentId: connector.deploymentId
6502
6669
  });
6503
6670
  if (!activeTrade) {
6504
- import_logger9.logger.warn(
6671
+ import_logger10.logger.warn(
6505
6672
  "[%s] blocked closePosition for untracked runtime position: %s",
6506
6673
  strategyName ?? "unknown",
6507
6674
  symbol
@@ -6509,7 +6676,7 @@ var handleExitDecision = async ({
6509
6676
  return "CLOSE_BLOCKED_BY_UNTRACKED_POSITION";
6510
6677
  }
6511
6678
  if (!strategyName || activeTrade.strategy !== strategyName) {
6512
- import_logger9.logger.warn(
6679
+ import_logger10.logger.warn(
6513
6680
  "[%s] blocked closePosition for foreign runtime position: %s ownedBy=%s",
6514
6681
  strategyName ?? "unknown",
6515
6682
  symbol,
@@ -6562,7 +6729,7 @@ var handleExitDecision = async ({
6562
6729
  exitType: closedTrade?.exitType ?? "exit"
6563
6730
  });
6564
6731
  } catch (notificationError) {
6565
- import_logger9.logger.error(
6732
+ import_logger10.logger.error(
6566
6733
  "runtime close notification error: %s %s",
6567
6734
  symbol,
6568
6735
  notificationError
@@ -6576,7 +6743,7 @@ var handleExitDecision = async ({
6576
6743
  decision,
6577
6744
  market
6578
6745
  });
6579
- import_logger9.logger.error("close order error: %s %s", symbol, err);
6746
+ import_logger10.logger.error("close order error: %s %s", symbol, err);
6580
6747
  return "ORDER_ERROR";
6581
6748
  }
6582
6749
  return decision.code;
@@ -6603,7 +6770,7 @@ var handleProtectDecision = async ({
6603
6770
  decision,
6604
6771
  market
6605
6772
  });
6606
- import_logger9.logger.error("protect position error: %s %s", symbol, err);
6773
+ import_logger10.logger.error("protect position error: %s %s", symbol, err);
6607
6774
  return "ORDER_ERROR";
6608
6775
  }
6609
6776
  return decision.code;
@@ -6669,7 +6836,11 @@ var executeEntryDecision = async ({
6669
6836
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
6670
6837
  signal,
6671
6838
  beforePlaceOrder,
6672
- recordRuntimeTrade: recordRuntimeJournal
6839
+ recordRuntimeTrade: recordRuntimeJournal,
6840
+ executionLimits: resolveEntryExecutionLimits(
6841
+ hookCtx.strategyConfig,
6842
+ decision.entryContext.direction
6843
+ )
6673
6844
  });
6674
6845
  await invokeStageHooks(
6675
6846
  "afterPlaceOrder",
@@ -6691,19 +6862,32 @@ var executeEntryDecision = async ({
6691
6862
  return signal;
6692
6863
  }
6693
6864
  await beforePlaceOrder();
6694
- const arrivalSnapshot = await getOrderArrivalSnapshot({
6865
+ const beforeSubmit = createEntrySubmissionValidation({
6695
6866
  connector,
6696
- symbol
6697
- });
6698
- validateEntryProtectionAtArrival({
6867
+ symbol,
6699
6868
  direction: decision.entryContext.direction,
6700
- signalPrice: decision.entryContext.prices.currentPrice,
6701
- bid: arrivalSnapshot.bid,
6702
- ask: arrivalSnapshot.ask,
6703
- arrivalMid: arrivalSnapshot.arrivalMid,
6704
6869
  takeProfits: decision.orderPlan.takeProfits,
6705
- stopLossPrice: decision.orderPlan.stopLossPrice
6870
+ stopLossPrice: decision.orderPlan.stopLossPrice,
6871
+ limits: resolveEntryExecutionLimits(
6872
+ hookCtx.strategyConfig,
6873
+ decision.entryContext.direction
6874
+ )
6706
6875
  });
6876
+ if (!beforeSubmit) {
6877
+ const arrivalSnapshot = await getOrderArrivalSnapshot({
6878
+ connector,
6879
+ symbol
6880
+ });
6881
+ validateEntryProtectionAtArrival({
6882
+ direction: decision.entryContext.direction,
6883
+ signalPrice: decision.entryContext.prices.currentPrice,
6884
+ bid: arrivalSnapshot.bid,
6885
+ ask: arrivalSnapshot.ask,
6886
+ arrivalMid: arrivalSnapshot.arrivalMid,
6887
+ takeProfits: decision.orderPlan.takeProfits,
6888
+ stopLossPrice: decision.orderPlan.stopLossPrice
6889
+ });
6890
+ }
6707
6891
  const orderPlaced = await connector.placeOrder({
6708
6892
  symbol,
6709
6893
  qty: decision.orderPlan.qty,
@@ -6711,6 +6895,7 @@ var executeEntryDecision = async ({
6711
6895
  timestamp: decision.entryContext.timestamp,
6712
6896
  direction: decision.entryContext.direction,
6713
6897
  positionIntent: decision.orderPlan.positionIntent,
6898
+ ...beforeSubmit ? { beforeSubmit } : {},
6714
6899
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {}
6715
6900
  });
6716
6901
  if (!orderPlaced) {
@@ -6752,6 +6937,23 @@ var executeEntryDecision = async ({
6752
6937
  { decision, entry, market }
6753
6938
  );
6754
6939
  } catch (err) {
6940
+ if (err instanceof EntrySubmissionValidationError) {
6941
+ if (signal) {
6942
+ signal.orderStatus = "skipped";
6943
+ signal.orderSkipReason = err.message;
6944
+ signal.allocatorDecision = {
6945
+ stage: "runtime_admission",
6946
+ status: "rejected",
6947
+ reason: err.message
6948
+ };
6949
+ }
6950
+ import_logger10.logger.warn(
6951
+ "entry rejected before submission: %s %s",
6952
+ symbol,
6953
+ err.message
6954
+ );
6955
+ return signal ?? err.message;
6956
+ }
6755
6957
  if (signal) {
6756
6958
  signal.orderStatus = "failed";
6757
6959
  if (typeof signal.orderFailureReason !== "string" || !signal.orderFailureReason.trim()) {
@@ -6766,9 +6968,9 @@ var executeEntryDecision = async ({
6766
6968
  market
6767
6969
  });
6768
6970
  if (err?.message === import_types.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY) {
6769
- import_logger9.logger.warn("order warning: %s %s", symbol, err);
6971
+ import_logger10.logger.warn("order warning: %s %s", symbol, err);
6770
6972
  } else {
6771
- import_logger9.logger.error("order error: %s %s", symbol, err);
6973
+ import_logger10.logger.error("order error: %s %s", symbol, err);
6772
6974
  }
6773
6975
  return signal ?? "ORDER_ERROR";
6774
6976
  }
@@ -6933,7 +7135,7 @@ var createStrategyRuntime = ({
6933
7135
  try {
6934
7136
  await projectHook(errorParams);
6935
7137
  } catch (hookError) {
6936
- import_logger10.logger.error(
7138
+ import_logger11.logger.error(
6937
7139
  "project hook onRuntimeError failed: %s %s",
6938
7140
  strategyName,
6939
7141
  hookError
@@ -6947,7 +7149,7 @@ var createStrategyRuntime = ({
6947
7149
  try {
6948
7150
  await onRuntimeError(errorParams);
6949
7151
  } catch (hookError) {
6950
- import_logger10.logger.error(
7152
+ import_logger11.logger.error(
6951
7153
  "runtime hook onRuntimeError failed: %s %s",
6952
7154
  strategyName,
6953
7155
  hookError
@@ -6961,7 +7163,7 @@ var createStrategyRuntime = ({
6961
7163
  try {
6962
7164
  return await hook(params);
6963
7165
  } catch (error) {
6964
- import_logger10.logger.error(
7166
+ import_logger11.logger.error(
6965
7167
  'strategy hook "%s" failed for %s: %s',
6966
7168
  stage,
6967
7169
  strategyName,