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