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