@tradejs/node 3.1.29-beta.288 → 3.1.29-beta.290

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,210 @@ 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
+ const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
1119
+ return { netRiskRatio, plannedLossValue, riskExceeded };
1120
+ };
1121
+
1122
+ // src/strategy/entrySubmissionValidation.ts
1123
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
1124
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
1125
+ var getOrderArrivalSnapshot = async ({
1126
+ connector,
1127
+ symbol,
1128
+ requireTimestamp = false
1129
+ }) => {
1130
+ if (typeof connector.getTopOfBookTicker !== "function") {
1131
+ return {
1132
+ arrivalSnapshotTime: Date.now(),
1133
+ arrivalSource: "unavailable",
1134
+ bid: null,
1135
+ ask: null,
1136
+ arrivalMid: null,
1137
+ spreadBps: null
1138
+ };
1139
+ }
1140
+ try {
1141
+ const ticker = await connector.getTopOfBookTicker(symbol);
1142
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
1143
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
1144
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
1145
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
1146
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
1147
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
1148
+ return {
1149
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
1150
+ arrivalSource: "top_of_book",
1151
+ bid,
1152
+ ask,
1153
+ arrivalMid,
1154
+ spreadBps
1155
+ };
1156
+ } catch (error) {
1157
+ import_logger.logger.warn(
1158
+ "runtime order arrival snapshot failed: %s %s",
1159
+ symbol,
1160
+ error?.message || String(error)
1161
+ );
1162
+ return {
1163
+ arrivalSnapshotTime: Date.now(),
1164
+ arrivalSource: "top_of_book_error",
1165
+ bid: null,
1166
+ ask: null,
1167
+ arrivalMid: null,
1168
+ spreadBps: null
1169
+ };
1170
+ }
1171
+ };
1172
+ var validateEntryProtectionAtArrival = ({
1173
+ direction,
1174
+ signalPrice,
1175
+ bid,
1176
+ ask,
1177
+ arrivalMid,
1178
+ takeProfits,
1179
+ stopLossPrice
1180
+ }) => {
1181
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
1182
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
1183
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
1184
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
1185
+ }
1186
+ for (const takeProfit of takeProfits) {
1187
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
1188
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
1189
+ }
1190
+ }
1191
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
1192
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
1193
+ }
1194
+ };
1195
+ var createEntrySubmissionValidation = ({
1196
+ connector,
1197
+ symbol,
1198
+ direction,
1199
+ takeProfits,
1200
+ stopLossPrice,
1201
+ limits,
1202
+ onValidated
1203
+ }) => {
1204
+ if (!limits) return void 0;
1205
+ if (!connector.capabilities?.orderSubmissionValidation)
1206
+ throw new EntrySubmissionValidationError(
1207
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
1208
+ );
1209
+ return async ({ qty, executionPrice }) => {
1210
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
1211
+ const snapshot2 = simulated && executionPrice != null ? {
1212
+ arrivalSnapshotTime: Date.now(),
1213
+ arrivalSource: "simulation",
1214
+ bid: executionPrice,
1215
+ ask: executionPrice,
1216
+ arrivalMid: executionPrice,
1217
+ spreadBps: 0
1218
+ } : await getOrderArrivalSnapshot({
1219
+ connector,
1220
+ symbol,
1221
+ requireTimestamp: true
1222
+ });
1223
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
1224
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
1225
+ throw new EntrySubmissionValidationError(
1226
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
1227
+ );
1228
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
1229
+ throw new EntrySubmissionValidationError(
1230
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
1231
+ );
1232
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
1233
+ validateEntryProtectionAtArrival({
1234
+ direction,
1235
+ signalPrice: price,
1236
+ bid: snapshot2.bid,
1237
+ ask: snapshot2.ask,
1238
+ arrivalMid: snapshot2.arrivalMid,
1239
+ takeProfits,
1240
+ stopLossPrice
1241
+ });
1242
+ const prices = takeProfits.map((tp) => tp.price);
1243
+ const economics2 = validateEntryExecution({
1244
+ direction,
1245
+ price,
1246
+ qty,
1247
+ stopLossPrice,
1248
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
1249
+ limits
1250
+ });
1251
+ onValidated?.(snapshot2, economics2);
1252
+ };
1253
+ };
1254
+
1058
1255
  // src/ai.ts
1059
1256
  var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
1060
1257
  var import_redis = require("@tradejs/infra/redis");
@@ -1639,7 +1836,7 @@ var import_fs = __toESM(require("fs"));
1639
1836
  var import_path = __toESM(require("path"));
1640
1837
  var import_url = require("url");
1641
1838
  var import_config = require("@tradejs/core/config");
1642
- var import_logger = require("@tradejs/infra/logger");
1839
+ var import_logger2 = require("@tradejs/infra/logger");
1643
1840
  var CONFIG_FILE_NAMES = [
1644
1841
  "tradejs.config.ts",
1645
1842
  "tradejs.config.mts",
@@ -1906,11 +2103,11 @@ var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
1906
2103
  cachedByCwd.set(cwd, config);
1907
2104
  if (!announcedConfigFile.has(configFilePath)) {
1908
2105
  announcedConfigFile.add(configFilePath);
1909
- import_logger.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
2106
+ import_logger2.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
1910
2107
  }
1911
2108
  return config;
1912
2109
  } catch (error) {
1913
- import_logger.logger.log(
2110
+ import_logger2.logger.log(
1914
2111
  "warn",
1915
2112
  "Failed to load TradeJS config from %s: %s",
1916
2113
  configFilePath,
@@ -2876,7 +3073,7 @@ var import_node_crypto = require("crypto");
2876
3073
  var import_constants = require("@tradejs/core/constants");
2877
3074
  var import_time = require("@tradejs/core/time");
2878
3075
  var import_trade = require("@tradejs/core/trade");
2879
- var import_logger2 = require("@tradejs/infra/logger");
3076
+ var import_logger3 = require("@tradejs/infra/logger");
2880
3077
  var import_redis2 = require("@tradejs/infra/redis");
2881
3078
  var now = () => Date.now();
2882
3079
  var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
@@ -2935,7 +3132,7 @@ var recordRuntimeTradeOpen = async (params) => {
2935
3132
  )
2936
3133
  ]);
2937
3134
  } catch (error) {
2938
- import_logger2.logger.error(
3135
+ import_logger3.logger.error(
2939
3136
  "runtime trade open journal failed: %s %s",
2940
3137
  record.symbol,
2941
3138
  error?.message || String(error)
@@ -3003,7 +3200,7 @@ var recordRuntimeTradeIncrease = async (params) => {
3003
3200
  )
3004
3201
  ]);
3005
3202
  } catch (error) {
3006
- import_logger2.logger.error(
3203
+ import_logger3.logger.error(
3007
3204
  "runtime trade increase journal failed: %s %s",
3008
3205
  symbol,
3009
3206
  error?.message || String(error)
@@ -3110,7 +3307,7 @@ var markRuntimeTradeClosed = async (params) => {
3110
3307
  )
3111
3308
  ]);
3112
3309
  } catch (error) {
3113
- import_logger2.logger.error(
3310
+ import_logger3.logger.error(
3114
3311
  "runtime trade close journal failed: %s %s",
3115
3312
  symbol,
3116
3313
  error?.message || String(error)
@@ -3120,11 +3317,11 @@ var markRuntimeTradeClosed = async (params) => {
3120
3317
  };
3121
3318
 
3122
3319
  // src/strategyHelpers/marketContextStages.ts
3123
- var import_logger7 = require("@tradejs/infra/logger");
3320
+ var import_logger8 = require("@tradejs/infra/logger");
3124
3321
 
3125
3322
  // src/strategyHelpers/binanceMarketContext.ts
3126
3323
  var import_marketContext = require("@tradejs/infra/timescale/marketContext");
3127
- var import_logger3 = require("@tradejs/infra/logger");
3324
+ var import_logger4 = require("@tradejs/infra/logger");
3128
3325
  var import_strategies = require("@tradejs/core/strategies");
3129
3326
 
3130
3327
  // src/binanceBreadthUniverses.ts
@@ -3488,7 +3685,7 @@ var parseEnabledFlag = (value, env) => {
3488
3685
  if (normalized === "live") return env !== "BACKTEST";
3489
3686
  return false;
3490
3687
  };
3491
- var toFiniteNumberOrNull = (value) => {
3688
+ var toFiniteNumberOrNull2 = (value) => {
3492
3689
  const numeric = typeof value === "number" ? value : Number(value);
3493
3690
  return Number.isFinite(numeric) ? numeric : null;
3494
3691
  };
@@ -3613,14 +3810,14 @@ var toTradeFlowContext = (row, interval) => row ? {
3613
3810
  asOfTs: row.ts.getTime(),
3614
3811
  ageMs: row.ageMs,
3615
3812
  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)
3813
+ trades: toFiniteNumberOrNull2(row.trades),
3814
+ buyPressurePct: toFiniteNumberOrNull2(row.buyPressurePct),
3815
+ buyBaseVolume: toFiniteNumberOrNull2(row.buyBaseVolume),
3816
+ sellBaseVolume: toFiniteNumberOrNull2(row.sellBaseVolume),
3817
+ buyQuoteVolume: toFiniteNumberOrNull2(row.buyQuoteVolume),
3818
+ sellQuoteVolume: toFiniteNumberOrNull2(row.sellQuoteVolume),
3819
+ netBaseDelta: toFiniteNumberOrNull2(row.netBaseDelta),
3820
+ netQuoteDelta: toFiniteNumberOrNull2(row.netQuoteDelta)
3624
3821
  } : null;
3625
3822
  var getCachedReferenceRows = ({
3626
3823
  referenceSymbols,
@@ -3709,16 +3906,16 @@ var toMarketBreadthContext = (breadth, interval) => ({
3709
3906
  asOfTs: breadth.ts.getTime(),
3710
3907
  ageMs: breadth.ageMs,
3711
3908
  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)
3909
+ symbolsCount: toFiniteNumberOrNull2(breadth.symbolsCount),
3910
+ advancers: toFiniteNumberOrNull2(breadth.advancers),
3911
+ decliners: toFiniteNumberOrNull2(breadth.decliners),
3912
+ unchanged: toFiniteNumberOrNull2(breadth.unchanged),
3913
+ advanceDeclineRatio: toFiniteNumberOrNull2(breadth.advanceDeclineRatio),
3914
+ pctAboveMa20: toFiniteNumberOrNull2(breadth.pctAboveMa20),
3915
+ pctAboveMa50: toFiniteNumberOrNull2(breadth.pctAboveMa50),
3916
+ equalWeightedReturn: toFiniteNumberOrNull2(breadth.equalWeightedReturn),
3917
+ volumeWeightedReturn: toFiniteNumberOrNull2(breadth.volumeWeightedReturn),
3918
+ dispersion: toFiniteNumberOrNull2(breadth.dispersion)
3722
3919
  });
3723
3920
  var enrichSignalWithBinanceMarketContext = async (params) => {
3724
3921
  const {
@@ -3819,46 +4016,46 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3819
4016
  asOfTs: primaryBreadth.ts.getTime(),
3820
4017
  ageMs: primaryBreadth.ageMs,
3821
4018
  stale: primaryBreadth.stale,
3822
- btcReturn1h: toFiniteNumberOrNull(
4019
+ btcReturn1h: toFiniteNumberOrNull2(
3823
4020
  primaryBreadth.btcReturn1h
3824
4021
  ),
3825
- btcReturn4h: toFiniteNumberOrNull(
4022
+ btcReturn4h: toFiniteNumberOrNull2(
3826
4023
  primaryBreadth.btcReturn4h
3827
4024
  ),
3828
- btcReturn24h: toFiniteNumberOrNull(
4025
+ btcReturn24h: toFiniteNumberOrNull2(
3829
4026
  primaryBreadth.btcReturn24h
3830
4027
  ),
3831
- altBasketReturn1h: toFiniteNumberOrNull(
4028
+ altBasketReturn1h: toFiniteNumberOrNull2(
3832
4029
  primaryBreadth.altBasketReturn1h
3833
4030
  ),
3834
- altBasketReturn4h: toFiniteNumberOrNull(
4031
+ altBasketReturn4h: toFiniteNumberOrNull2(
3835
4032
  primaryBreadth.altBasketReturn4h
3836
4033
  ),
3837
- altBasketReturn24h: toFiniteNumberOrNull(
4034
+ altBasketReturn24h: toFiniteNumberOrNull2(
3838
4035
  primaryBreadth.altBasketReturn24h
3839
4036
  ),
3840
- btcVsAltReturn1h: toFiniteNumberOrNull(
4037
+ btcVsAltReturn1h: toFiniteNumberOrNull2(
3841
4038
  primaryBreadth.btcVsAltReturn1h
3842
4039
  ),
3843
- btcVsAltReturn4h: toFiniteNumberOrNull(
4040
+ btcVsAltReturn4h: toFiniteNumberOrNull2(
3844
4041
  primaryBreadth.btcVsAltReturn4h
3845
4042
  ),
3846
- btcVsAltReturn24h: toFiniteNumberOrNull(
4043
+ btcVsAltReturn24h: toFiniteNumberOrNull2(
3847
4044
  primaryBreadth.btcVsAltReturn24h
3848
4045
  ),
3849
- btcTurnoverShare1h: toFiniteNumberOrNull(
4046
+ btcTurnoverShare1h: toFiniteNumberOrNull2(
3850
4047
  primaryBreadth.btcTurnoverShare1h
3851
4048
  ),
3852
- btcTurnoverShare24h: toFiniteNumberOrNull(
4049
+ btcTurnoverShare24h: toFiniteNumberOrNull2(
3853
4050
  primaryBreadth.btcTurnoverShare24h
3854
4051
  ),
3855
- btcTurnoverShareChange24h: toFiniteNumberOrNull(
4052
+ btcTurnoverShareChange24h: toFiniteNumberOrNull2(
3856
4053
  primaryBreadth.btcTurnoverShareChange24h
3857
4054
  ),
3858
- altVolToBtcVol24h: toFiniteNumberOrNull(
4055
+ altVolToBtcVol24h: toFiniteNumberOrNull2(
3859
4056
  primaryBreadth.altVolToBtcVol24h
3860
4057
  ),
3861
- altDispersion24h: toFiniteNumberOrNull(
4058
+ altDispersion24h: toFiniteNumberOrNull2(
3862
4059
  primaryBreadth.altDispersion24h
3863
4060
  ),
3864
4061
  regime: primaryBreadth.btcAltRegime ?? "unknown"
@@ -3875,7 +4072,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3875
4072
  throw error;
3876
4073
  }
3877
4074
  binanceMarketContextUnavailable = true;
3878
- import_logger3.logger.warn(
4075
+ import_logger4.logger.warn(
3879
4076
  "Binance market context disabled after Timescale read failure: %s",
3880
4077
  String(error)
3881
4078
  );
@@ -3885,7 +4082,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3885
4082
 
3886
4083
  // src/strategyHelpers/coinMarketCapContext.ts
3887
4084
  var import_strategies2 = require("@tradejs/core/strategies");
3888
- var import_logger4 = require("@tradejs/infra/logger");
4085
+ var import_logger5 = require("@tradejs/infra/logger");
3889
4086
  var import_marketContext2 = require("@tradejs/infra/timescale/marketContext");
3890
4087
  var DEFAULT_MAX_AGE_MS = 48 * 60 * 6e4;
3891
4088
  var SOURCE_GLOBAL_DAILY = "coinmarketcap_global";
@@ -3923,7 +4120,7 @@ var asInt = (value, fallback) => {
3923
4120
  const parsed = Number.parseInt(String(value ?? ""), 10);
3924
4121
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
3925
4122
  };
3926
- var toFiniteNumberOrNull2 = (value) => {
4123
+ var toFiniteNumberOrNull3 = (value) => {
3927
4124
  const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
3928
4125
  return Number.isFinite(numeric) ? numeric : null;
3929
4126
  };
@@ -4157,12 +4354,12 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4157
4354
  const ethRow = references.get("ETHUSDT") ?? null;
4158
4355
  const previousBtcRow = previousReferences.get("BTCUSDT") ?? null;
4159
4356
  const previousEthRow = previousReferences.get("ETHUSDT") ?? null;
4160
- const btcMarketCapUsd = toFiniteNumberOrNull2(btcRow?.marketCapUsd);
4161
- const ethMarketCapUsd = toFiniteNumberOrNull2(ethRow?.marketCapUsd);
4162
- const previousBtcMarketCapUsd = toFiniteNumberOrNull2(
4357
+ const btcMarketCapUsd = toFiniteNumberOrNull3(btcRow?.marketCapUsd);
4358
+ const ethMarketCapUsd = toFiniteNumberOrNull3(ethRow?.marketCapUsd);
4359
+ const previousBtcMarketCapUsd = toFiniteNumberOrNull3(
4163
4360
  previousBtcRow?.marketCapUsd
4164
4361
  );
4165
- const previousEthMarketCapUsd = toFiniteNumberOrNull2(
4362
+ const previousEthMarketCapUsd = toFiniteNumberOrNull3(
4166
4363
  previousEthRow?.marketCapUsd
4167
4364
  );
4168
4365
  const ethBtcMarketCapRatio = safeDivide(ethMarketCapUsd, btcMarketCapUsd);
@@ -4171,16 +4368,16 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4171
4368
  previousBtcMarketCapUsd
4172
4369
  );
4173
4370
  const ethBtcMarketCapRatioChange24hPct = ethBtcMarketCapRatio != null && previousEthBtcMarketCapRatio != null && previousEthBtcMarketCapRatio > 0 ? (ethBtcMarketCapRatio - previousEthBtcMarketCapRatio) / previousEthBtcMarketCapRatio : null;
4174
- const btcVolumeUsd = toFiniteNumberOrNull2(btcRow?.volumeUsd);
4175
- const ethVolumeUsd = toFiniteNumberOrNull2(ethRow?.volumeUsd);
4371
+ const btcVolumeUsd = toFiniteNumberOrNull3(btcRow?.volumeUsd);
4372
+ const ethVolumeUsd = toFiniteNumberOrNull3(ethRow?.volumeUsd);
4176
4373
  const referenceStale = btcRow?.stale === true || ethRow?.stale === true || !btcRow || !ethRow;
4177
- const btcDominanceChange24hPct = toFiniteNumberOrNull2(
4374
+ const btcDominanceChange24hPct = toFiniteNumberOrNull3(
4178
4375
  globalRow?.btcDominanceChange24hPct
4179
4376
  );
4180
- const altMarketCapChange24hPct = toFiniteNumberOrNull2(
4377
+ const altMarketCapChange24hPct = toFiniteNumberOrNull3(
4181
4378
  globalRow?.altMarketCapChange24hPct
4182
4379
  );
4183
- const altVolumeChange24hPct = toFiniteNumberOrNull2(
4380
+ const altVolumeChange24hPct = toFiniteNumberOrNull3(
4184
4381
  globalRow?.altVolumeChange24hPct
4185
4382
  );
4186
4383
  const altLiquidityRegime = globalRow ? toAltLiquidityRegime({
@@ -4191,19 +4388,19 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4191
4388
  }) : "unknown";
4192
4389
  const exchangeLiquidityRegime = exchangeLiquidityRow ? toExchangeLiquidityRegime({
4193
4390
  stale: exchangeLiquidityRow.stale,
4194
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
4391
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
4195
4392
  exchangeLiquidityRow.totalVolumeChange24hPct
4196
4393
  ),
4197
4394
  fallback: exchangeLiquidityRow.liquidityRegime ?? "unknown"
4198
4395
  }) : "unknown";
4199
4396
  const cmc100Row = indexRows.get("cmc100") ?? null;
4200
4397
  const cmc20Row = indexRows.get("cmc20") ?? null;
4201
- const cmc100Value = toFiniteNumberOrNull2(cmc100Row?.value);
4202
- const cmc20Value = toFiniteNumberOrNull2(cmc20Row?.value);
4203
- const cmc100Change24hPct = toFiniteNumberOrNull2(
4398
+ const cmc100Value = toFiniteNumberOrNull3(cmc100Row?.value);
4399
+ const cmc20Value = toFiniteNumberOrNull3(cmc20Row?.value);
4400
+ const cmc100Change24hPct = toFiniteNumberOrNull3(
4204
4401
  cmc100Row?.valueChange24hPct
4205
4402
  );
4206
- const cmc20Change24hPct = toFiniteNumberOrNull2(cmc20Row?.valueChange24hPct);
4403
+ const cmc20Change24hPct = toFiniteNumberOrNull3(cmc20Row?.valueChange24hPct);
4207
4404
  const cmc20ToCmc100Ratio = safeDivide(cmc20Value, cmc100Value);
4208
4405
  const cmc20ToCmc100RatioChange24hPct = cmc20Change24hPct != null && cmc100Change24hPct != null ? (1 + cmc20Change24hPct) / (1 + cmc100Change24hPct) - 1 : null;
4209
4406
  const indexStale = cmc100Row?.stale === true || cmc20Row?.stale === true || !cmc100Row || !cmc20Row;
@@ -4227,41 +4424,41 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4227
4424
  asOfTs: globalRow.ts.getTime(),
4228
4425
  ageMs: globalRow.ageMs,
4229
4426
  stale: globalRow.stale,
4230
- totalMarketCapUsd: toFiniteNumberOrNull2(
4427
+ totalMarketCapUsd: toFiniteNumberOrNull3(
4231
4428
  globalRow.totalMarketCapUsd
4232
4429
  ),
4233
- totalVolumeUsd: toFiniteNumberOrNull2(
4430
+ totalVolumeUsd: toFiniteNumberOrNull3(
4234
4431
  globalRow.totalVolumeUsd
4235
4432
  ),
4236
- totalVolumeReportedUsd: toFiniteNumberOrNull2(
4433
+ totalVolumeReportedUsd: toFiniteNumberOrNull3(
4237
4434
  globalRow.totalVolumeReportedUsd
4238
4435
  ),
4239
- altMarketCapUsd: toFiniteNumberOrNull2(
4436
+ altMarketCapUsd: toFiniteNumberOrNull3(
4240
4437
  globalRow.altMarketCapUsd
4241
4438
  ),
4242
- altVolumeUsd: toFiniteNumberOrNull2(globalRow.altVolumeUsd),
4243
- altVolumeReportedUsd: toFiniteNumberOrNull2(
4439
+ altVolumeUsd: toFiniteNumberOrNull3(globalRow.altVolumeUsd),
4440
+ altVolumeReportedUsd: toFiniteNumberOrNull3(
4244
4441
  globalRow.altVolumeReportedUsd
4245
4442
  ),
4246
- btcDominancePct: toFiniteNumberOrNull2(
4443
+ btcDominancePct: toFiniteNumberOrNull3(
4247
4444
  globalRow.btcDominancePct
4248
4445
  ),
4249
- ethDominancePct: toFiniteNumberOrNull2(
4446
+ ethDominancePct: toFiniteNumberOrNull3(
4250
4447
  globalRow.ethDominancePct
4251
4448
  ),
4252
4449
  btcDominanceChange24hPct,
4253
- ethDominanceChange24hPct: toFiniteNumberOrNull2(
4450
+ ethDominanceChange24hPct: toFiniteNumberOrNull3(
4254
4451
  globalRow.ethDominanceChange24hPct
4255
4452
  ),
4256
4453
  altMarketCapChange24hPct,
4257
4454
  altVolumeChange24hPct,
4258
- activeCryptocurrencies: toFiniteNumberOrNull2(
4455
+ activeCryptocurrencies: toFiniteNumberOrNull3(
4259
4456
  globalRow.activeCryptocurrencies
4260
4457
  ),
4261
- activeExchanges: toFiniteNumberOrNull2(
4458
+ activeExchanges: toFiniteNumberOrNull3(
4262
4459
  globalRow.activeExchanges
4263
4460
  ),
4264
- activeMarketPairs: toFiniteNumberOrNull2(
4461
+ activeMarketPairs: toFiniteNumberOrNull3(
4265
4462
  globalRow.activeMarketPairs
4266
4463
  ),
4267
4464
  altLiquidityRegime
@@ -4306,22 +4503,22 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4306
4503
  asOfTs: exchangeLiquidityRow.ts.getTime(),
4307
4504
  ageMs: exchangeLiquidityRow.ageMs,
4308
4505
  stale: exchangeLiquidityRow.stale,
4309
- exchangesCount: toFiniteNumberOrNull2(
4506
+ exchangesCount: toFiniteNumberOrNull3(
4310
4507
  exchangeLiquidityRow.exchangesCount
4311
4508
  ),
4312
- totalVolumeUsd: toFiniteNumberOrNull2(
4509
+ totalVolumeUsd: toFiniteNumberOrNull3(
4313
4510
  exchangeLiquidityRow.totalVolumeUsd
4314
4511
  ),
4315
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
4512
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
4316
4513
  exchangeLiquidityRow.totalVolumeChange24hPct
4317
4514
  ),
4318
- binanceVolumeUsd: toFiniteNumberOrNull2(
4515
+ binanceVolumeUsd: toFiniteNumberOrNull3(
4319
4516
  exchangeLiquidityRow.binanceVolumeUsd
4320
4517
  ),
4321
- binanceVolumeShare: toFiniteNumberOrNull2(
4518
+ binanceVolumeShare: toFiniteNumberOrNull3(
4322
4519
  exchangeLiquidityRow.binanceVolumeShare
4323
4520
  ),
4324
- topExchangeVolumeShare: toFiniteNumberOrNull2(
4521
+ topExchangeVolumeShare: toFiniteNumberOrNull3(
4325
4522
  exchangeLiquidityRow.topExchangeVolumeShare
4326
4523
  ),
4327
4524
  liquidityRegime: exchangeLiquidityRegime
@@ -4334,11 +4531,11 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4334
4531
  asOfTs: fearGreedRow.ts.getTime(),
4335
4532
  ageMs: fearGreedRow.ageMs,
4336
4533
  stale: fearGreedRow.stale,
4337
- value: toFiniteNumberOrNull2(fearGreedRow.value),
4338
- valueChange24h: toFiniteNumberOrNull2(
4534
+ value: toFiniteNumberOrNull3(fearGreedRow.value),
4535
+ valueChange24h: toFiniteNumberOrNull3(
4339
4536
  fearGreedRow.valueChange24h
4340
4537
  ),
4341
- valueChange7d: toFiniteNumberOrNull2(
4538
+ valueChange7d: toFiniteNumberOrNull3(
4342
4539
  fearGreedRow.valueChange7d
4343
4540
  ),
4344
4541
  classification: fearGreedRow.classification ?? "Unknown",
@@ -4358,13 +4555,13 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4358
4555
  cmc100Value,
4359
4556
  cmc100Change24hPct,
4360
4557
  cmc100TopConstituentSymbol: cmc100Row?.topConstituentSymbol ?? null,
4361
- cmc100TopConstituentWeightPct: toFiniteNumberOrNull2(
4558
+ cmc100TopConstituentWeightPct: toFiniteNumberOrNull3(
4362
4559
  cmc100Row?.topConstituentWeightPct
4363
4560
  ),
4364
4561
  cmc20Value,
4365
4562
  cmc20Change24hPct,
4366
4563
  cmc20TopConstituentSymbol: cmc20Row?.topConstituentSymbol ?? null,
4367
- cmc20TopConstituentWeightPct: toFiniteNumberOrNull2(
4564
+ cmc20TopConstituentWeightPct: toFiniteNumberOrNull3(
4368
4565
  cmc20Row?.topConstituentWeightPct
4369
4566
  ),
4370
4567
  cmc20ToCmc100Ratio,
@@ -4382,7 +4579,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4382
4579
  throw error;
4383
4580
  }
4384
4581
  coinMarketCapContextUnavailable = true;
4385
- import_logger4.logger.warn(
4582
+ import_logger5.logger.warn(
4386
4583
  "CoinMarketCap context disabled after Timescale read failure: %s",
4387
4584
  String(error)
4388
4585
  );
@@ -4396,7 +4593,7 @@ var import_data2 = require("@tradejs/core/data");
4396
4593
  var import_strategies3 = require("@tradejs/core/strategies");
4397
4594
  var import_constants2 = require("@tradejs/core/constants");
4398
4595
  var import_derivatives = require("@tradejs/infra/timescale/derivatives");
4399
- var import_logger5 = require("@tradejs/infra/logger");
4596
+ var import_logger6 = require("@tradejs/infra/logger");
4400
4597
  var STORED_INTERVALS = ["15m", "1h"];
4401
4598
  var CONTEXT_INTERVALS = ["15m", "1h"];
4402
4599
  var DEFAULT_LOOKBACK_HOURS = 48;
@@ -4451,7 +4648,7 @@ var resolvePrimaryReferenceSymbol2 = () => PRIMARY_DERIVATIVES_REFERENCE_SYMBOL;
4451
4648
  var resolveSecondaryReferenceSymbol = () => SECONDARY_DERIVATIVES_REFERENCE_SYMBOL;
4452
4649
  var getPrimaryIntervalContext = (context) => context?.intervals["15m"] ?? context?.intervals["1h"] ?? null;
4453
4650
  var hasDerivativesSymbolData = (context) => Object.keys(context.intervals).length > 0 && !context.summary.riskFlags.includes("missing_derivatives");
4454
- var toFiniteNumberOrNull3 = (value) => {
4651
+ var toFiniteNumberOrNull4 = (value) => {
4455
4652
  if (typeof value === "number" && Number.isFinite(value)) return value;
4456
4653
  if (typeof value === "string" && value.trim()) {
4457
4654
  const parsed = Number(value);
@@ -4465,8 +4662,8 @@ var roundNullable = (value, digits = 4) => {
4465
4662
  return Math.round(value * multiplier) / multiplier;
4466
4663
  };
4467
4664
  var deltaNullable = (targetValue, referenceValue) => {
4468
- const target = toFiniteNumberOrNull3(targetValue);
4469
- const reference = toFiniteNumberOrNull3(referenceValue);
4665
+ const target = toFiniteNumberOrNull4(targetValue);
4666
+ const reference = toFiniteNumberOrNull4(referenceValue);
4470
4667
  return target == null || reference == null ? null : roundNullable(target - reference);
4471
4668
  };
4472
4669
  var buildTargetDerivedContext = (params) => {
@@ -4628,7 +4825,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4628
4825
  throw error;
4629
4826
  }
4630
4827
  derivativesContextUnavailable = true;
4631
- import_logger5.logger.warn(
4828
+ import_logger6.logger.warn(
4632
4829
  "Derivatives context disabled after Timescale read failure: %s",
4633
4830
  String(error)
4634
4831
  );
@@ -4640,7 +4837,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4640
4837
  var import_data3 = require("@tradejs/core/data");
4641
4838
  var import_strategies4 = require("@tradejs/core/strategies");
4642
4839
  var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWhales");
4643
- var import_logger6 = require("@tradejs/infra/logger");
4840
+ var import_logger7 = require("@tradejs/infra/logger");
4644
4841
 
4645
4842
  // src/hyperliquidWhaleUniverse.ts
4646
4843
  var import_node_crypto3 = require("crypto");
@@ -5160,7 +5357,7 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
5160
5357
  throw error;
5161
5358
  }
5162
5359
  hyperliquidWhaleContextUnavailable = true;
5163
- import_logger6.logger.warn(
5360
+ import_logger7.logger.warn(
5164
5361
  "Hyperliquid whale context disabled after Timescale read failure: %s",
5165
5362
  String(error)
5166
5363
  );
@@ -5243,7 +5440,7 @@ var runMarketContextStage = async ({
5243
5440
  elapsedMs: Date.now() - startedAt
5244
5441
  };
5245
5442
  if (status === "timed_out") {
5246
- import_logger7.logger.warn(
5443
+ import_logger8.logger.warn(
5247
5444
  "Market context stage timed out: %s after %sms",
5248
5445
  stage,
5249
5446
  result.elapsedMs
@@ -5433,79 +5630,10 @@ var enrichSignalWithAi = async ({
5433
5630
  signal.aiAnalysis = analysis;
5434
5631
  return resolveAiQuality(analysis, direction);
5435
5632
  } catch (err) {
5436
- import_logger8.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
5633
+ import_logger9.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
5437
5634
  }
5438
5635
  return void 0;
5439
5636
  };
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
5637
  var resolveRuntimeTelemetryQuality = ({
5510
5638
  signalClosePrice,
5511
5639
  arrivalMid,
@@ -5582,7 +5710,8 @@ var executeEntryOrder = async ({
5582
5710
  signal,
5583
5711
  beforePlaceOrder,
5584
5712
  recordRuntimeTrade = true,
5585
- leverage
5713
+ leverage,
5714
+ executionLimits
5586
5715
  }) => {
5587
5716
  await beforePlaceOrder?.();
5588
5717
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
@@ -5593,23 +5722,57 @@ var executeEntryOrder = async ({
5593
5722
  signal.orderQty = qty;
5594
5723
  signal.orderValue = qty * currentPrice;
5595
5724
  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;
5725
+ let arrivalSnapshot = {
5726
+ arrivalSnapshotTime: Date.now(),
5727
+ arrivalSource: "unavailable",
5728
+ bid: null,
5729
+ ask: null,
5730
+ arrivalMid: null,
5731
+ spreadBps: null
5732
+ };
5733
+ let orderSubmitTime = Date.now();
5734
+ const beforeSubmit = createEntrySubmissionValidation({
5735
+ connector,
5736
+ symbol,
5737
+ direction,
5738
+ takeProfits,
5739
+ stopLossPrice,
5740
+ limits: executionLimits,
5741
+ onValidated: (snapshot2, economics2) => {
5742
+ arrivalSnapshot = snapshot2;
5743
+ orderSubmitTime = Date.now();
5744
+ if (executionLimits?.maxLossValue != null) {
5745
+ signal.riskDecision = {
5746
+ stage: "order_plan",
5747
+ status: economics2.riskExceeded ? "warning" : "approved",
5748
+ reason: economics2.riskExceeded ? "ENTRY_RISK_EXCEEDS_MAX_LOSS_AT_SUBMISSION" : "PLANNED_LOSS_WITHIN_MAX_LOSS_VALUE",
5749
+ plannedLossValue: economics2.plannedLossValue,
5750
+ maxLossValue: executionLimits.maxLossValue,
5751
+ limitUtilization: economics2.plannedLossValue / executionLimits.maxLossValue,
5752
+ enforced: false
5753
+ };
5754
+ }
5755
+ }
5756
+ });
5757
+ if (!beforeSubmit) {
5758
+ arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
5759
+ try {
5760
+ validateEntryProtectionAtArrival({
5761
+ direction,
5762
+ signalPrice: currentPrice,
5763
+ bid: arrivalSnapshot.bid,
5764
+ ask: arrivalSnapshot.ask,
5765
+ arrivalMid: arrivalSnapshot.arrivalMid,
5766
+ takeProfits,
5767
+ stopLossPrice
5768
+ });
5769
+ } catch (error) {
5770
+ signal.orderStatus = "failed";
5771
+ signal.orderFailureReason = error.message;
5772
+ throw error;
5773
+ }
5774
+ orderSubmitTime = Date.now();
5611
5775
  }
5612
- const orderSubmitTime = Date.now();
5613
5776
  const orderPlaced = await connector.placeOrder({
5614
5777
  symbol,
5615
5778
  qty,
@@ -5620,7 +5783,8 @@ var executeEntryOrder = async ({
5620
5783
  direction,
5621
5784
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
5622
5785
  orderId,
5623
- signal
5786
+ signal,
5787
+ ...beforeSubmit ? { beforeSubmit } : {}
5624
5788
  });
5625
5789
  const orderAckTime = Date.now();
5626
5790
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -6449,7 +6613,7 @@ var canUseSharedReplayState = ({
6449
6613
  }) => (env === "BACKTEST" || env === "PARITY") && Boolean(sharedReplayKey);
6450
6614
 
6451
6615
  // src/strategy/runtimeExecution.ts
6452
- var import_logger9 = require("@tradejs/infra/logger");
6616
+ var import_logger10 = require("@tradejs/infra/logger");
6453
6617
  var import_types = require("@tradejs/types");
6454
6618
  var buildExitOrderSignal = ({
6455
6619
  strategyName,
@@ -6501,7 +6665,7 @@ var handleExitDecision = async ({
6501
6665
  deploymentId: connector.deploymentId
6502
6666
  });
6503
6667
  if (!activeTrade) {
6504
- import_logger9.logger.warn(
6668
+ import_logger10.logger.warn(
6505
6669
  "[%s] blocked closePosition for untracked runtime position: %s",
6506
6670
  strategyName ?? "unknown",
6507
6671
  symbol
@@ -6509,7 +6673,7 @@ var handleExitDecision = async ({
6509
6673
  return "CLOSE_BLOCKED_BY_UNTRACKED_POSITION";
6510
6674
  }
6511
6675
  if (!strategyName || activeTrade.strategy !== strategyName) {
6512
- import_logger9.logger.warn(
6676
+ import_logger10.logger.warn(
6513
6677
  "[%s] blocked closePosition for foreign runtime position: %s ownedBy=%s",
6514
6678
  strategyName ?? "unknown",
6515
6679
  symbol,
@@ -6562,7 +6726,7 @@ var handleExitDecision = async ({
6562
6726
  exitType: closedTrade?.exitType ?? "exit"
6563
6727
  });
6564
6728
  } catch (notificationError) {
6565
- import_logger9.logger.error(
6729
+ import_logger10.logger.error(
6566
6730
  "runtime close notification error: %s %s",
6567
6731
  symbol,
6568
6732
  notificationError
@@ -6576,7 +6740,7 @@ var handleExitDecision = async ({
6576
6740
  decision,
6577
6741
  market
6578
6742
  });
6579
- import_logger9.logger.error("close order error: %s %s", symbol, err);
6743
+ import_logger10.logger.error("close order error: %s %s", symbol, err);
6580
6744
  return "ORDER_ERROR";
6581
6745
  }
6582
6746
  return decision.code;
@@ -6603,7 +6767,7 @@ var handleProtectDecision = async ({
6603
6767
  decision,
6604
6768
  market
6605
6769
  });
6606
- import_logger9.logger.error("protect position error: %s %s", symbol, err);
6770
+ import_logger10.logger.error("protect position error: %s %s", symbol, err);
6607
6771
  return "ORDER_ERROR";
6608
6772
  }
6609
6773
  return decision.code;
@@ -6669,7 +6833,11 @@ var executeEntryDecision = async ({
6669
6833
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
6670
6834
  signal,
6671
6835
  beforePlaceOrder,
6672
- recordRuntimeTrade: recordRuntimeJournal
6836
+ recordRuntimeTrade: recordRuntimeJournal,
6837
+ executionLimits: resolveEntryExecutionLimits(
6838
+ hookCtx.strategyConfig,
6839
+ decision.entryContext.direction
6840
+ )
6673
6841
  });
6674
6842
  await invokeStageHooks(
6675
6843
  "afterPlaceOrder",
@@ -6691,19 +6859,32 @@ var executeEntryDecision = async ({
6691
6859
  return signal;
6692
6860
  }
6693
6861
  await beforePlaceOrder();
6694
- const arrivalSnapshot = await getOrderArrivalSnapshot({
6862
+ const beforeSubmit = createEntrySubmissionValidation({
6695
6863
  connector,
6696
- symbol
6697
- });
6698
- validateEntryProtectionAtArrival({
6864
+ symbol,
6699
6865
  direction: decision.entryContext.direction,
6700
- signalPrice: decision.entryContext.prices.currentPrice,
6701
- bid: arrivalSnapshot.bid,
6702
- ask: arrivalSnapshot.ask,
6703
- arrivalMid: arrivalSnapshot.arrivalMid,
6704
6866
  takeProfits: decision.orderPlan.takeProfits,
6705
- stopLossPrice: decision.orderPlan.stopLossPrice
6867
+ stopLossPrice: decision.orderPlan.stopLossPrice,
6868
+ limits: resolveEntryExecutionLimits(
6869
+ hookCtx.strategyConfig,
6870
+ decision.entryContext.direction
6871
+ )
6706
6872
  });
6873
+ if (!beforeSubmit) {
6874
+ const arrivalSnapshot = await getOrderArrivalSnapshot({
6875
+ connector,
6876
+ symbol
6877
+ });
6878
+ validateEntryProtectionAtArrival({
6879
+ direction: decision.entryContext.direction,
6880
+ signalPrice: decision.entryContext.prices.currentPrice,
6881
+ bid: arrivalSnapshot.bid,
6882
+ ask: arrivalSnapshot.ask,
6883
+ arrivalMid: arrivalSnapshot.arrivalMid,
6884
+ takeProfits: decision.orderPlan.takeProfits,
6885
+ stopLossPrice: decision.orderPlan.stopLossPrice
6886
+ });
6887
+ }
6707
6888
  const orderPlaced = await connector.placeOrder({
6708
6889
  symbol,
6709
6890
  qty: decision.orderPlan.qty,
@@ -6711,6 +6892,7 @@ var executeEntryDecision = async ({
6711
6892
  timestamp: decision.entryContext.timestamp,
6712
6893
  direction: decision.entryContext.direction,
6713
6894
  positionIntent: decision.orderPlan.positionIntent,
6895
+ ...beforeSubmit ? { beforeSubmit } : {},
6714
6896
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {}
6715
6897
  });
6716
6898
  if (!orderPlaced) {
@@ -6752,6 +6934,23 @@ var executeEntryDecision = async ({
6752
6934
  { decision, entry, market }
6753
6935
  );
6754
6936
  } catch (err) {
6937
+ if (err instanceof EntrySubmissionValidationError) {
6938
+ if (signal) {
6939
+ signal.orderStatus = "skipped";
6940
+ signal.orderSkipReason = err.message;
6941
+ signal.allocatorDecision = {
6942
+ stage: "runtime_admission",
6943
+ status: "rejected",
6944
+ reason: err.message
6945
+ };
6946
+ }
6947
+ import_logger10.logger.warn(
6948
+ "entry rejected before submission: %s %s",
6949
+ symbol,
6950
+ err.message
6951
+ );
6952
+ return signal ?? err.message;
6953
+ }
6755
6954
  if (signal) {
6756
6955
  signal.orderStatus = "failed";
6757
6956
  if (typeof signal.orderFailureReason !== "string" || !signal.orderFailureReason.trim()) {
@@ -6766,9 +6965,9 @@ var executeEntryDecision = async ({
6766
6965
  market
6767
6966
  });
6768
6967
  if (err?.message === import_types.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY) {
6769
- import_logger9.logger.warn("order warning: %s %s", symbol, err);
6968
+ import_logger10.logger.warn("order warning: %s %s", symbol, err);
6770
6969
  } else {
6771
- import_logger9.logger.error("order error: %s %s", symbol, err);
6970
+ import_logger10.logger.error("order error: %s %s", symbol, err);
6772
6971
  }
6773
6972
  return signal ?? "ORDER_ERROR";
6774
6973
  }
@@ -6876,6 +7075,9 @@ var createStrategyRuntime = ({
6876
7075
  });
6877
7076
  const strategyPolicyProfile = getPolicyProfile();
6878
7077
  const indicatorPeriods = (0, import_strategies6.buildDefaultIndicatorPeriods)(config);
7078
+ const strategyConfig = { ...config };
7079
+ delete strategyConfig.RECORD_RUNTIME_TRADES;
7080
+ delete strategyConfig.SIMULATE_ORDERS;
6879
7081
  const hookBase = {
6880
7082
  connector,
6881
7083
  strategyName,
@@ -6886,7 +7088,7 @@ var createStrategyRuntime = ({
6886
7088
  accountId,
6887
7089
  deploymentId,
6888
7090
  policyProfileId: strategyPolicyProfile?.id ?? requestedPolicyProfileId,
6889
- strategyConfig: config,
7091
+ strategyConfig,
6890
7092
  env,
6891
7093
  isConfigFromBacktest
6892
7094
  };
@@ -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,21 +7149,21 @@ 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
6954
7156
  );
6955
7157
  }
6956
7158
  };
6957
- const invokeHook = async (stage, hook, params, errorContext = {}) => {
7159
+ const invokeHook = async (stage, hook, params, errorContext = {}, failureResult) => {
6958
7160
  if (!hook) {
6959
7161
  return void 0;
6960
7162
  }
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,
@@ -6974,17 +7176,18 @@ var createStrategyRuntime = ({
6974
7176
  entry: errorContext.entry,
6975
7177
  market: errorContext.market
6976
7178
  });
6977
- return void 0;
7179
+ return failureResult;
6978
7180
  }
6979
7181
  };
6980
- const invokeProjectHooks = async (stage, params, errorContext = {}) => {
7182
+ const invokeProjectHooks = async (stage, params, errorContext = {}, failureResult) => {
6981
7183
  const results = [];
6982
7184
  for (const hook of getProjectHookList(stage)) {
6983
7185
  const result = await invokeHook(
6984
7186
  stage,
6985
7187
  hook,
6986
7188
  params,
6987
- errorContext
7189
+ errorContext,
7190
+ failureResult
6988
7191
  );
6989
7192
  if (result !== void 0) {
6990
7193
  results.push(result);
@@ -6999,10 +7202,12 @@ var createStrategyRuntime = ({
6999
7202
  return invokeHook(stage, hook, params, errorContext);
7000
7203
  };
7001
7204
  const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
7205
+ const failureResult = stage === "beforeEntryGate" ? { allow: false, reason: "ERROR" } : void 0;
7002
7206
  const projectResults = await invokeProjectHooks(
7003
7207
  stage,
7004
7208
  params,
7005
- errorContext
7209
+ errorContext,
7210
+ failureResult
7006
7211
  );
7007
7212
  const projectBlock = projectResults.find(
7008
7213
  (result) => result?.allow === false
@@ -7014,7 +7219,8 @@ var createStrategyRuntime = ({
7014
7219
  stage,
7015
7220
  hook,
7016
7221
  params,
7017
- errorContext
7222
+ errorContext,
7223
+ failureResult
7018
7224
  );
7019
7225
  };
7020
7226
  const applyProjectAfterCoreDecisionHooks = async ({
@@ -7160,11 +7366,8 @@ var createStrategyRuntime = ({
7160
7366
  return enrichedBaseContext;
7161
7367
  }
7162
7368
  });
7163
- const coreConfig = { ...config };
7164
- delete coreConfig.RECORD_RUNTIME_TRADES;
7165
- delete coreConfig.SIMULATE_ORDERS;
7166
7369
  const core = await createCore({
7167
- config: coreConfig,
7370
+ config: strategyConfig,
7168
7371
  data,
7169
7372
  strategyApi,
7170
7373
  indicatorsState