@tradejs/node 3.1.29-beta.287 → 3.1.29-beta.289

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -1066,13 +1066,213 @@ var assessSignalWithJev = async ({
1066
1066
  // src/strategyRuntime.ts
1067
1067
  var import_constants5 = require("@tradejs/core/constants");
1068
1068
  var import_strategies6 = require("@tradejs/core/strategies");
1069
- var import_logger10 = require("@tradejs/infra/logger");
1069
+ var import_logger11 = require("@tradejs/infra/logger");
1070
1070
 
1071
1071
  // src/strategyHelpers/runtime.ts
1072
- var import_logger8 = require("@tradejs/infra/logger");
1072
+ var import_logger9 = require("@tradejs/infra/logger");
1073
1073
  var import_constants3 = require("@tradejs/core/constants");
1074
1074
  var import_ml2 = require("@tradejs/infra/ml");
1075
1075
 
1076
+ // src/strategy/entrySubmissionValidation.ts
1077
+ var import_logger = require("@tradejs/infra/logger");
1078
+
1079
+ // src/strategy/entryExecutionValidation.ts
1080
+ var EntrySubmissionValidationError = class extends Error {
1081
+ constructor() {
1082
+ super(...arguments);
1083
+ this.name = "EntrySubmissionValidationError";
1084
+ }
1085
+ };
1086
+ var resolveEntryExecutionLimits = (config, direction) => {
1087
+ const side = config[direction];
1088
+ const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
1089
+ const maximum = config.MAX_LOSS_VALUE;
1090
+ if (minimum == null && maximum == null) return void 0;
1091
+ const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
1092
+ const maxLossValue = maximum == null ? void 0 : Number(maximum);
1093
+ const feeRate = Number(config.RISK_FEE_RATE ?? 0);
1094
+ const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
1095
+ const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
1096
+ const slippageBps = slippage + impact;
1097
+ 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)
1098
+ throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
1099
+ return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
1100
+ };
1101
+ var validateEntryExecution = ({
1102
+ direction,
1103
+ price,
1104
+ qty,
1105
+ stopLossPrice,
1106
+ takeProfitPrice,
1107
+ limits
1108
+ }) => {
1109
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
1110
+ throw new EntrySubmissionValidationError(
1111
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1112
+ );
1113
+ const side = direction === "LONG" ? 1 : -1;
1114
+ if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
1115
+ throw new EntrySubmissionValidationError(
1116
+ "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
1117
+ );
1118
+ const slippage = limits.slippageBps / 1e4;
1119
+ const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
1120
+ const entryCost = cost(price, side);
1121
+ const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
1122
+ const rewardPerUnit = Math.max(
1123
+ 0,
1124
+ Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
1125
+ );
1126
+ const netRiskRatio = rewardPerUnit / lossPerUnit;
1127
+ const plannedLossValue = qty * lossPerUnit;
1128
+ if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
1129
+ throw new EntrySubmissionValidationError(
1130
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1131
+ );
1132
+ if (limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio)
1133
+ throw new EntrySubmissionValidationError(
1134
+ "ENTRY_RR_BELOW_MIN_AT_SUBMISSION"
1135
+ );
1136
+ if (limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9))
1137
+ throw new EntrySubmissionValidationError(
1138
+ "ENTRY_RISK_EXCEEDS_MAX_LOSS_AT_SUBMISSION"
1139
+ );
1140
+ return { netRiskRatio, plannedLossValue };
1141
+ };
1142
+
1143
+ // src/strategy/entrySubmissionValidation.ts
1144
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
1145
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
1146
+ var getOrderArrivalSnapshot = async ({
1147
+ connector,
1148
+ symbol,
1149
+ requireTimestamp = false
1150
+ }) => {
1151
+ if (typeof connector.getTopOfBookTicker !== "function") {
1152
+ return {
1153
+ arrivalSnapshotTime: Date.now(),
1154
+ arrivalSource: "unavailable",
1155
+ bid: null,
1156
+ ask: null,
1157
+ arrivalMid: null,
1158
+ spreadBps: null
1159
+ };
1160
+ }
1161
+ try {
1162
+ const ticker = await connector.getTopOfBookTicker(symbol);
1163
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
1164
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
1165
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
1166
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
1167
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
1168
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
1169
+ return {
1170
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
1171
+ arrivalSource: "top_of_book",
1172
+ bid,
1173
+ ask,
1174
+ arrivalMid,
1175
+ spreadBps
1176
+ };
1177
+ } catch (error) {
1178
+ import_logger.logger.warn(
1179
+ "runtime order arrival snapshot failed: %s %s",
1180
+ symbol,
1181
+ error?.message || String(error)
1182
+ );
1183
+ return {
1184
+ arrivalSnapshotTime: Date.now(),
1185
+ arrivalSource: "top_of_book_error",
1186
+ bid: null,
1187
+ ask: null,
1188
+ arrivalMid: null,
1189
+ spreadBps: null
1190
+ };
1191
+ }
1192
+ };
1193
+ var validateEntryProtectionAtArrival = ({
1194
+ direction,
1195
+ signalPrice,
1196
+ bid,
1197
+ ask,
1198
+ arrivalMid,
1199
+ takeProfits,
1200
+ stopLossPrice
1201
+ }) => {
1202
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
1203
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
1204
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
1205
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
1206
+ }
1207
+ for (const takeProfit of takeProfits) {
1208
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
1209
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
1210
+ }
1211
+ }
1212
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
1213
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
1214
+ }
1215
+ };
1216
+ var createEntrySubmissionValidation = ({
1217
+ connector,
1218
+ symbol,
1219
+ direction,
1220
+ takeProfits,
1221
+ stopLossPrice,
1222
+ limits,
1223
+ onValidated
1224
+ }) => {
1225
+ if (!limits) return void 0;
1226
+ if (!connector.capabilities?.orderSubmissionValidation)
1227
+ throw new EntrySubmissionValidationError(
1228
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
1229
+ );
1230
+ return async ({ qty, executionPrice }) => {
1231
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
1232
+ const snapshot2 = simulated && executionPrice != null ? {
1233
+ arrivalSnapshotTime: Date.now(),
1234
+ arrivalSource: "simulation",
1235
+ bid: executionPrice,
1236
+ ask: executionPrice,
1237
+ arrivalMid: executionPrice,
1238
+ spreadBps: 0
1239
+ } : await getOrderArrivalSnapshot({
1240
+ connector,
1241
+ symbol,
1242
+ requireTimestamp: true
1243
+ });
1244
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
1245
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
1246
+ throw new EntrySubmissionValidationError(
1247
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
1248
+ );
1249
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
1250
+ throw new EntrySubmissionValidationError(
1251
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
1252
+ );
1253
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
1254
+ validateEntryProtectionAtArrival({
1255
+ direction,
1256
+ signalPrice: price,
1257
+ bid: snapshot2.bid,
1258
+ ask: snapshot2.ask,
1259
+ arrivalMid: snapshot2.arrivalMid,
1260
+ takeProfits,
1261
+ stopLossPrice
1262
+ });
1263
+ const prices = takeProfits.map((tp) => tp.price);
1264
+ const economics2 = validateEntryExecution({
1265
+ direction,
1266
+ price,
1267
+ qty,
1268
+ stopLossPrice,
1269
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
1270
+ limits
1271
+ });
1272
+ onValidated?.(snapshot2, economics2);
1273
+ };
1274
+ };
1275
+
1076
1276
  // src/ai.ts
1077
1277
  var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
1078
1278
  var import_redis = require("@tradejs/infra/redis");
@@ -1657,7 +1857,7 @@ var import_fs = __toESM(require("fs"));
1657
1857
  var import_path = __toESM(require("path"));
1658
1858
  var import_url = require("url");
1659
1859
  var import_config = require("@tradejs/core/config");
1660
- var import_logger = require("@tradejs/infra/logger");
1860
+ var import_logger2 = require("@tradejs/infra/logger");
1661
1861
  var CONFIG_FILE_NAMES = [
1662
1862
  "tradejs.config.ts",
1663
1863
  "tradejs.config.mts",
@@ -1924,11 +2124,11 @@ var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
1924
2124
  cachedByCwd.set(cwd, config);
1925
2125
  if (!announcedConfigFile.has(configFilePath)) {
1926
2126
  announcedConfigFile.add(configFilePath);
1927
- import_logger.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
2127
+ import_logger2.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
1928
2128
  }
1929
2129
  return config;
1930
2130
  } catch (error) {
1931
- import_logger.logger.log(
2131
+ import_logger2.logger.log(
1932
2132
  "warn",
1933
2133
  "Failed to load TradeJS config from %s: %s",
1934
2134
  configFilePath,
@@ -2895,7 +3095,7 @@ var import_node_crypto = require("crypto");
2895
3095
  var import_constants = require("@tradejs/core/constants");
2896
3096
  var import_time = require("@tradejs/core/time");
2897
3097
  var import_trade = require("@tradejs/core/trade");
2898
- var import_logger2 = require("@tradejs/infra/logger");
3098
+ var import_logger3 = require("@tradejs/infra/logger");
2899
3099
  var import_redis2 = require("@tradejs/infra/redis");
2900
3100
  var now = () => Date.now();
2901
3101
  var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
@@ -2954,7 +3154,7 @@ var recordRuntimeTradeOpen = async (params) => {
2954
3154
  )
2955
3155
  ]);
2956
3156
  } catch (error) {
2957
- import_logger2.logger.error(
3157
+ import_logger3.logger.error(
2958
3158
  "runtime trade open journal failed: %s %s",
2959
3159
  record.symbol,
2960
3160
  error?.message || String(error)
@@ -3022,7 +3222,7 @@ var recordRuntimeTradeIncrease = async (params) => {
3022
3222
  )
3023
3223
  ]);
3024
3224
  } catch (error) {
3025
- import_logger2.logger.error(
3225
+ import_logger3.logger.error(
3026
3226
  "runtime trade increase journal failed: %s %s",
3027
3227
  symbol,
3028
3228
  error?.message || String(error)
@@ -3129,7 +3329,7 @@ var markRuntimeTradeClosed = async (params) => {
3129
3329
  )
3130
3330
  ]);
3131
3331
  } catch (error) {
3132
- import_logger2.logger.error(
3332
+ import_logger3.logger.error(
3133
3333
  "runtime trade close journal failed: %s %s",
3134
3334
  symbol,
3135
3335
  error?.message || String(error)
@@ -3139,11 +3339,11 @@ var markRuntimeTradeClosed = async (params) => {
3139
3339
  };
3140
3340
 
3141
3341
  // src/strategyHelpers/marketContextStages.ts
3142
- var import_logger7 = require("@tradejs/infra/logger");
3342
+ var import_logger8 = require("@tradejs/infra/logger");
3143
3343
 
3144
3344
  // src/strategyHelpers/binanceMarketContext.ts
3145
3345
  var import_marketContext = require("@tradejs/infra/timescale/marketContext");
3146
- var import_logger3 = require("@tradejs/infra/logger");
3346
+ var import_logger4 = require("@tradejs/infra/logger");
3147
3347
  var import_strategies = require("@tradejs/core/strategies");
3148
3348
 
3149
3349
  // src/binanceBreadthUniverses.ts
@@ -3507,7 +3707,7 @@ var parseEnabledFlag = (value, env) => {
3507
3707
  if (normalized === "live") return env !== "BACKTEST";
3508
3708
  return false;
3509
3709
  };
3510
- var toFiniteNumberOrNull = (value) => {
3710
+ var toFiniteNumberOrNull2 = (value) => {
3511
3711
  const numeric = typeof value === "number" ? value : Number(value);
3512
3712
  return Number.isFinite(numeric) ? numeric : null;
3513
3713
  };
@@ -3632,14 +3832,14 @@ var toTradeFlowContext = (row, interval) => row ? {
3632
3832
  asOfTs: row.ts.getTime(),
3633
3833
  ageMs: row.ageMs,
3634
3834
  stale: row.stale,
3635
- trades: toFiniteNumberOrNull(row.trades),
3636
- buyPressurePct: toFiniteNumberOrNull(row.buyPressurePct),
3637
- buyBaseVolume: toFiniteNumberOrNull(row.buyBaseVolume),
3638
- sellBaseVolume: toFiniteNumberOrNull(row.sellBaseVolume),
3639
- buyQuoteVolume: toFiniteNumberOrNull(row.buyQuoteVolume),
3640
- sellQuoteVolume: toFiniteNumberOrNull(row.sellQuoteVolume),
3641
- netBaseDelta: toFiniteNumberOrNull(row.netBaseDelta),
3642
- netQuoteDelta: toFiniteNumberOrNull(row.netQuoteDelta)
3835
+ trades: toFiniteNumberOrNull2(row.trades),
3836
+ buyPressurePct: toFiniteNumberOrNull2(row.buyPressurePct),
3837
+ buyBaseVolume: toFiniteNumberOrNull2(row.buyBaseVolume),
3838
+ sellBaseVolume: toFiniteNumberOrNull2(row.sellBaseVolume),
3839
+ buyQuoteVolume: toFiniteNumberOrNull2(row.buyQuoteVolume),
3840
+ sellQuoteVolume: toFiniteNumberOrNull2(row.sellQuoteVolume),
3841
+ netBaseDelta: toFiniteNumberOrNull2(row.netBaseDelta),
3842
+ netQuoteDelta: toFiniteNumberOrNull2(row.netQuoteDelta)
3643
3843
  } : null;
3644
3844
  var getCachedReferenceRows = ({
3645
3845
  referenceSymbols,
@@ -3728,16 +3928,16 @@ var toMarketBreadthContext = (breadth, interval) => ({
3728
3928
  asOfTs: breadth.ts.getTime(),
3729
3929
  ageMs: breadth.ageMs,
3730
3930
  stale: breadth.stale,
3731
- symbolsCount: toFiniteNumberOrNull(breadth.symbolsCount),
3732
- advancers: toFiniteNumberOrNull(breadth.advancers),
3733
- decliners: toFiniteNumberOrNull(breadth.decliners),
3734
- unchanged: toFiniteNumberOrNull(breadth.unchanged),
3735
- advanceDeclineRatio: toFiniteNumberOrNull(breadth.advanceDeclineRatio),
3736
- pctAboveMa20: toFiniteNumberOrNull(breadth.pctAboveMa20),
3737
- pctAboveMa50: toFiniteNumberOrNull(breadth.pctAboveMa50),
3738
- equalWeightedReturn: toFiniteNumberOrNull(breadth.equalWeightedReturn),
3739
- volumeWeightedReturn: toFiniteNumberOrNull(breadth.volumeWeightedReturn),
3740
- dispersion: toFiniteNumberOrNull(breadth.dispersion)
3931
+ symbolsCount: toFiniteNumberOrNull2(breadth.symbolsCount),
3932
+ advancers: toFiniteNumberOrNull2(breadth.advancers),
3933
+ decliners: toFiniteNumberOrNull2(breadth.decliners),
3934
+ unchanged: toFiniteNumberOrNull2(breadth.unchanged),
3935
+ advanceDeclineRatio: toFiniteNumberOrNull2(breadth.advanceDeclineRatio),
3936
+ pctAboveMa20: toFiniteNumberOrNull2(breadth.pctAboveMa20),
3937
+ pctAboveMa50: toFiniteNumberOrNull2(breadth.pctAboveMa50),
3938
+ equalWeightedReturn: toFiniteNumberOrNull2(breadth.equalWeightedReturn),
3939
+ volumeWeightedReturn: toFiniteNumberOrNull2(breadth.volumeWeightedReturn),
3940
+ dispersion: toFiniteNumberOrNull2(breadth.dispersion)
3741
3941
  });
3742
3942
  var enrichSignalWithBinanceMarketContext = async (params) => {
3743
3943
  const {
@@ -3838,46 +4038,46 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3838
4038
  asOfTs: primaryBreadth.ts.getTime(),
3839
4039
  ageMs: primaryBreadth.ageMs,
3840
4040
  stale: primaryBreadth.stale,
3841
- btcReturn1h: toFiniteNumberOrNull(
4041
+ btcReturn1h: toFiniteNumberOrNull2(
3842
4042
  primaryBreadth.btcReturn1h
3843
4043
  ),
3844
- btcReturn4h: toFiniteNumberOrNull(
4044
+ btcReturn4h: toFiniteNumberOrNull2(
3845
4045
  primaryBreadth.btcReturn4h
3846
4046
  ),
3847
- btcReturn24h: toFiniteNumberOrNull(
4047
+ btcReturn24h: toFiniteNumberOrNull2(
3848
4048
  primaryBreadth.btcReturn24h
3849
4049
  ),
3850
- altBasketReturn1h: toFiniteNumberOrNull(
4050
+ altBasketReturn1h: toFiniteNumberOrNull2(
3851
4051
  primaryBreadth.altBasketReturn1h
3852
4052
  ),
3853
- altBasketReturn4h: toFiniteNumberOrNull(
4053
+ altBasketReturn4h: toFiniteNumberOrNull2(
3854
4054
  primaryBreadth.altBasketReturn4h
3855
4055
  ),
3856
- altBasketReturn24h: toFiniteNumberOrNull(
4056
+ altBasketReturn24h: toFiniteNumberOrNull2(
3857
4057
  primaryBreadth.altBasketReturn24h
3858
4058
  ),
3859
- btcVsAltReturn1h: toFiniteNumberOrNull(
4059
+ btcVsAltReturn1h: toFiniteNumberOrNull2(
3860
4060
  primaryBreadth.btcVsAltReturn1h
3861
4061
  ),
3862
- btcVsAltReturn4h: toFiniteNumberOrNull(
4062
+ btcVsAltReturn4h: toFiniteNumberOrNull2(
3863
4063
  primaryBreadth.btcVsAltReturn4h
3864
4064
  ),
3865
- btcVsAltReturn24h: toFiniteNumberOrNull(
4065
+ btcVsAltReturn24h: toFiniteNumberOrNull2(
3866
4066
  primaryBreadth.btcVsAltReturn24h
3867
4067
  ),
3868
- btcTurnoverShare1h: toFiniteNumberOrNull(
4068
+ btcTurnoverShare1h: toFiniteNumberOrNull2(
3869
4069
  primaryBreadth.btcTurnoverShare1h
3870
4070
  ),
3871
- btcTurnoverShare24h: toFiniteNumberOrNull(
4071
+ btcTurnoverShare24h: toFiniteNumberOrNull2(
3872
4072
  primaryBreadth.btcTurnoverShare24h
3873
4073
  ),
3874
- btcTurnoverShareChange24h: toFiniteNumberOrNull(
4074
+ btcTurnoverShareChange24h: toFiniteNumberOrNull2(
3875
4075
  primaryBreadth.btcTurnoverShareChange24h
3876
4076
  ),
3877
- altVolToBtcVol24h: toFiniteNumberOrNull(
4077
+ altVolToBtcVol24h: toFiniteNumberOrNull2(
3878
4078
  primaryBreadth.altVolToBtcVol24h
3879
4079
  ),
3880
- altDispersion24h: toFiniteNumberOrNull(
4080
+ altDispersion24h: toFiniteNumberOrNull2(
3881
4081
  primaryBreadth.altDispersion24h
3882
4082
  ),
3883
4083
  regime: primaryBreadth.btcAltRegime ?? "unknown"
@@ -3894,7 +4094,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3894
4094
  throw error;
3895
4095
  }
3896
4096
  binanceMarketContextUnavailable = true;
3897
- import_logger3.logger.warn(
4097
+ import_logger4.logger.warn(
3898
4098
  "Binance market context disabled after Timescale read failure: %s",
3899
4099
  String(error)
3900
4100
  );
@@ -3904,7 +4104,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
3904
4104
 
3905
4105
  // src/strategyHelpers/coinMarketCapContext.ts
3906
4106
  var import_strategies2 = require("@tradejs/core/strategies");
3907
- var import_logger4 = require("@tradejs/infra/logger");
4107
+ var import_logger5 = require("@tradejs/infra/logger");
3908
4108
  var import_marketContext2 = require("@tradejs/infra/timescale/marketContext");
3909
4109
  var DEFAULT_MAX_AGE_MS = 48 * 60 * 6e4;
3910
4110
  var SOURCE_GLOBAL_DAILY = "coinmarketcap_global";
@@ -3942,7 +4142,7 @@ var asInt = (value, fallback) => {
3942
4142
  const parsed = Number.parseInt(String(value ?? ""), 10);
3943
4143
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
3944
4144
  };
3945
- var toFiniteNumberOrNull2 = (value) => {
4145
+ var toFiniteNumberOrNull3 = (value) => {
3946
4146
  const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
3947
4147
  return Number.isFinite(numeric) ? numeric : null;
3948
4148
  };
@@ -4176,12 +4376,12 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4176
4376
  const ethRow = references.get("ETHUSDT") ?? null;
4177
4377
  const previousBtcRow = previousReferences.get("BTCUSDT") ?? null;
4178
4378
  const previousEthRow = previousReferences.get("ETHUSDT") ?? null;
4179
- const btcMarketCapUsd = toFiniteNumberOrNull2(btcRow?.marketCapUsd);
4180
- const ethMarketCapUsd = toFiniteNumberOrNull2(ethRow?.marketCapUsd);
4181
- const previousBtcMarketCapUsd = toFiniteNumberOrNull2(
4379
+ const btcMarketCapUsd = toFiniteNumberOrNull3(btcRow?.marketCapUsd);
4380
+ const ethMarketCapUsd = toFiniteNumberOrNull3(ethRow?.marketCapUsd);
4381
+ const previousBtcMarketCapUsd = toFiniteNumberOrNull3(
4182
4382
  previousBtcRow?.marketCapUsd
4183
4383
  );
4184
- const previousEthMarketCapUsd = toFiniteNumberOrNull2(
4384
+ const previousEthMarketCapUsd = toFiniteNumberOrNull3(
4185
4385
  previousEthRow?.marketCapUsd
4186
4386
  );
4187
4387
  const ethBtcMarketCapRatio = safeDivide(ethMarketCapUsd, btcMarketCapUsd);
@@ -4190,16 +4390,16 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4190
4390
  previousBtcMarketCapUsd
4191
4391
  );
4192
4392
  const ethBtcMarketCapRatioChange24hPct = ethBtcMarketCapRatio != null && previousEthBtcMarketCapRatio != null && previousEthBtcMarketCapRatio > 0 ? (ethBtcMarketCapRatio - previousEthBtcMarketCapRatio) / previousEthBtcMarketCapRatio : null;
4193
- const btcVolumeUsd = toFiniteNumberOrNull2(btcRow?.volumeUsd);
4194
- const ethVolumeUsd = toFiniteNumberOrNull2(ethRow?.volumeUsd);
4393
+ const btcVolumeUsd = toFiniteNumberOrNull3(btcRow?.volumeUsd);
4394
+ const ethVolumeUsd = toFiniteNumberOrNull3(ethRow?.volumeUsd);
4195
4395
  const referenceStale = btcRow?.stale === true || ethRow?.stale === true || !btcRow || !ethRow;
4196
- const btcDominanceChange24hPct = toFiniteNumberOrNull2(
4396
+ const btcDominanceChange24hPct = toFiniteNumberOrNull3(
4197
4397
  globalRow?.btcDominanceChange24hPct
4198
4398
  );
4199
- const altMarketCapChange24hPct = toFiniteNumberOrNull2(
4399
+ const altMarketCapChange24hPct = toFiniteNumberOrNull3(
4200
4400
  globalRow?.altMarketCapChange24hPct
4201
4401
  );
4202
- const altVolumeChange24hPct = toFiniteNumberOrNull2(
4402
+ const altVolumeChange24hPct = toFiniteNumberOrNull3(
4203
4403
  globalRow?.altVolumeChange24hPct
4204
4404
  );
4205
4405
  const altLiquidityRegime = globalRow ? toAltLiquidityRegime({
@@ -4210,19 +4410,19 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4210
4410
  }) : "unknown";
4211
4411
  const exchangeLiquidityRegime = exchangeLiquidityRow ? toExchangeLiquidityRegime({
4212
4412
  stale: exchangeLiquidityRow.stale,
4213
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
4413
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
4214
4414
  exchangeLiquidityRow.totalVolumeChange24hPct
4215
4415
  ),
4216
4416
  fallback: exchangeLiquidityRow.liquidityRegime ?? "unknown"
4217
4417
  }) : "unknown";
4218
4418
  const cmc100Row = indexRows.get("cmc100") ?? null;
4219
4419
  const cmc20Row = indexRows.get("cmc20") ?? null;
4220
- const cmc100Value = toFiniteNumberOrNull2(cmc100Row?.value);
4221
- const cmc20Value = toFiniteNumberOrNull2(cmc20Row?.value);
4222
- const cmc100Change24hPct = toFiniteNumberOrNull2(
4420
+ const cmc100Value = toFiniteNumberOrNull3(cmc100Row?.value);
4421
+ const cmc20Value = toFiniteNumberOrNull3(cmc20Row?.value);
4422
+ const cmc100Change24hPct = toFiniteNumberOrNull3(
4223
4423
  cmc100Row?.valueChange24hPct
4224
4424
  );
4225
- const cmc20Change24hPct = toFiniteNumberOrNull2(cmc20Row?.valueChange24hPct);
4425
+ const cmc20Change24hPct = toFiniteNumberOrNull3(cmc20Row?.valueChange24hPct);
4226
4426
  const cmc20ToCmc100Ratio = safeDivide(cmc20Value, cmc100Value);
4227
4427
  const cmc20ToCmc100RatioChange24hPct = cmc20Change24hPct != null && cmc100Change24hPct != null ? (1 + cmc20Change24hPct) / (1 + cmc100Change24hPct) - 1 : null;
4228
4428
  const indexStale = cmc100Row?.stale === true || cmc20Row?.stale === true || !cmc100Row || !cmc20Row;
@@ -4246,41 +4446,41 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4246
4446
  asOfTs: globalRow.ts.getTime(),
4247
4447
  ageMs: globalRow.ageMs,
4248
4448
  stale: globalRow.stale,
4249
- totalMarketCapUsd: toFiniteNumberOrNull2(
4449
+ totalMarketCapUsd: toFiniteNumberOrNull3(
4250
4450
  globalRow.totalMarketCapUsd
4251
4451
  ),
4252
- totalVolumeUsd: toFiniteNumberOrNull2(
4452
+ totalVolumeUsd: toFiniteNumberOrNull3(
4253
4453
  globalRow.totalVolumeUsd
4254
4454
  ),
4255
- totalVolumeReportedUsd: toFiniteNumberOrNull2(
4455
+ totalVolumeReportedUsd: toFiniteNumberOrNull3(
4256
4456
  globalRow.totalVolumeReportedUsd
4257
4457
  ),
4258
- altMarketCapUsd: toFiniteNumberOrNull2(
4458
+ altMarketCapUsd: toFiniteNumberOrNull3(
4259
4459
  globalRow.altMarketCapUsd
4260
4460
  ),
4261
- altVolumeUsd: toFiniteNumberOrNull2(globalRow.altVolumeUsd),
4262
- altVolumeReportedUsd: toFiniteNumberOrNull2(
4461
+ altVolumeUsd: toFiniteNumberOrNull3(globalRow.altVolumeUsd),
4462
+ altVolumeReportedUsd: toFiniteNumberOrNull3(
4263
4463
  globalRow.altVolumeReportedUsd
4264
4464
  ),
4265
- btcDominancePct: toFiniteNumberOrNull2(
4465
+ btcDominancePct: toFiniteNumberOrNull3(
4266
4466
  globalRow.btcDominancePct
4267
4467
  ),
4268
- ethDominancePct: toFiniteNumberOrNull2(
4468
+ ethDominancePct: toFiniteNumberOrNull3(
4269
4469
  globalRow.ethDominancePct
4270
4470
  ),
4271
4471
  btcDominanceChange24hPct,
4272
- ethDominanceChange24hPct: toFiniteNumberOrNull2(
4472
+ ethDominanceChange24hPct: toFiniteNumberOrNull3(
4273
4473
  globalRow.ethDominanceChange24hPct
4274
4474
  ),
4275
4475
  altMarketCapChange24hPct,
4276
4476
  altVolumeChange24hPct,
4277
- activeCryptocurrencies: toFiniteNumberOrNull2(
4477
+ activeCryptocurrencies: toFiniteNumberOrNull3(
4278
4478
  globalRow.activeCryptocurrencies
4279
4479
  ),
4280
- activeExchanges: toFiniteNumberOrNull2(
4480
+ activeExchanges: toFiniteNumberOrNull3(
4281
4481
  globalRow.activeExchanges
4282
4482
  ),
4283
- activeMarketPairs: toFiniteNumberOrNull2(
4483
+ activeMarketPairs: toFiniteNumberOrNull3(
4284
4484
  globalRow.activeMarketPairs
4285
4485
  ),
4286
4486
  altLiquidityRegime
@@ -4325,22 +4525,22 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4325
4525
  asOfTs: exchangeLiquidityRow.ts.getTime(),
4326
4526
  ageMs: exchangeLiquidityRow.ageMs,
4327
4527
  stale: exchangeLiquidityRow.stale,
4328
- exchangesCount: toFiniteNumberOrNull2(
4528
+ exchangesCount: toFiniteNumberOrNull3(
4329
4529
  exchangeLiquidityRow.exchangesCount
4330
4530
  ),
4331
- totalVolumeUsd: toFiniteNumberOrNull2(
4531
+ totalVolumeUsd: toFiniteNumberOrNull3(
4332
4532
  exchangeLiquidityRow.totalVolumeUsd
4333
4533
  ),
4334
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
4534
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
4335
4535
  exchangeLiquidityRow.totalVolumeChange24hPct
4336
4536
  ),
4337
- binanceVolumeUsd: toFiniteNumberOrNull2(
4537
+ binanceVolumeUsd: toFiniteNumberOrNull3(
4338
4538
  exchangeLiquidityRow.binanceVolumeUsd
4339
4539
  ),
4340
- binanceVolumeShare: toFiniteNumberOrNull2(
4540
+ binanceVolumeShare: toFiniteNumberOrNull3(
4341
4541
  exchangeLiquidityRow.binanceVolumeShare
4342
4542
  ),
4343
- topExchangeVolumeShare: toFiniteNumberOrNull2(
4543
+ topExchangeVolumeShare: toFiniteNumberOrNull3(
4344
4544
  exchangeLiquidityRow.topExchangeVolumeShare
4345
4545
  ),
4346
4546
  liquidityRegime: exchangeLiquidityRegime
@@ -4353,11 +4553,11 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4353
4553
  asOfTs: fearGreedRow.ts.getTime(),
4354
4554
  ageMs: fearGreedRow.ageMs,
4355
4555
  stale: fearGreedRow.stale,
4356
- value: toFiniteNumberOrNull2(fearGreedRow.value),
4357
- valueChange24h: toFiniteNumberOrNull2(
4556
+ value: toFiniteNumberOrNull3(fearGreedRow.value),
4557
+ valueChange24h: toFiniteNumberOrNull3(
4358
4558
  fearGreedRow.valueChange24h
4359
4559
  ),
4360
- valueChange7d: toFiniteNumberOrNull2(
4560
+ valueChange7d: toFiniteNumberOrNull3(
4361
4561
  fearGreedRow.valueChange7d
4362
4562
  ),
4363
4563
  classification: fearGreedRow.classification ?? "Unknown",
@@ -4377,13 +4577,13 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4377
4577
  cmc100Value,
4378
4578
  cmc100Change24hPct,
4379
4579
  cmc100TopConstituentSymbol: cmc100Row?.topConstituentSymbol ?? null,
4380
- cmc100TopConstituentWeightPct: toFiniteNumberOrNull2(
4580
+ cmc100TopConstituentWeightPct: toFiniteNumberOrNull3(
4381
4581
  cmc100Row?.topConstituentWeightPct
4382
4582
  ),
4383
4583
  cmc20Value,
4384
4584
  cmc20Change24hPct,
4385
4585
  cmc20TopConstituentSymbol: cmc20Row?.topConstituentSymbol ?? null,
4386
- cmc20TopConstituentWeightPct: toFiniteNumberOrNull2(
4586
+ cmc20TopConstituentWeightPct: toFiniteNumberOrNull3(
4387
4587
  cmc20Row?.topConstituentWeightPct
4388
4588
  ),
4389
4589
  cmc20ToCmc100Ratio,
@@ -4401,7 +4601,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4401
4601
  throw error;
4402
4602
  }
4403
4603
  coinMarketCapContextUnavailable = true;
4404
- import_logger4.logger.warn(
4604
+ import_logger5.logger.warn(
4405
4605
  "CoinMarketCap context disabled after Timescale read failure: %s",
4406
4606
  String(error)
4407
4607
  );
@@ -4415,7 +4615,7 @@ var import_data2 = require("@tradejs/core/data");
4415
4615
  var import_strategies3 = require("@tradejs/core/strategies");
4416
4616
  var import_constants2 = require("@tradejs/core/constants");
4417
4617
  var import_derivatives = require("@tradejs/infra/timescale/derivatives");
4418
- var import_logger5 = require("@tradejs/infra/logger");
4618
+ var import_logger6 = require("@tradejs/infra/logger");
4419
4619
  var STORED_INTERVALS = ["15m", "1h"];
4420
4620
  var CONTEXT_INTERVALS = ["15m", "1h"];
4421
4621
  var DEFAULT_LOOKBACK_HOURS = 48;
@@ -4470,7 +4670,7 @@ var resolvePrimaryReferenceSymbol2 = () => PRIMARY_DERIVATIVES_REFERENCE_SYMBOL;
4470
4670
  var resolveSecondaryReferenceSymbol = () => SECONDARY_DERIVATIVES_REFERENCE_SYMBOL;
4471
4671
  var getPrimaryIntervalContext = (context) => context?.intervals["15m"] ?? context?.intervals["1h"] ?? null;
4472
4672
  var hasDerivativesSymbolData = (context) => Object.keys(context.intervals).length > 0 && !context.summary.riskFlags.includes("missing_derivatives");
4473
- var toFiniteNumberOrNull3 = (value) => {
4673
+ var toFiniteNumberOrNull4 = (value) => {
4474
4674
  if (typeof value === "number" && Number.isFinite(value)) return value;
4475
4675
  if (typeof value === "string" && value.trim()) {
4476
4676
  const parsed = Number(value);
@@ -4484,8 +4684,8 @@ var roundNullable = (value, digits = 4) => {
4484
4684
  return Math.round(value * multiplier) / multiplier;
4485
4685
  };
4486
4686
  var deltaNullable = (targetValue, referenceValue) => {
4487
- const target = toFiniteNumberOrNull3(targetValue);
4488
- const reference = toFiniteNumberOrNull3(referenceValue);
4687
+ const target = toFiniteNumberOrNull4(targetValue);
4688
+ const reference = toFiniteNumberOrNull4(referenceValue);
4489
4689
  return target == null || reference == null ? null : roundNullable(target - reference);
4490
4690
  };
4491
4691
  var buildTargetDerivedContext = (params) => {
@@ -4647,7 +4847,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4647
4847
  throw error;
4648
4848
  }
4649
4849
  derivativesContextUnavailable = true;
4650
- import_logger5.logger.warn(
4850
+ import_logger6.logger.warn(
4651
4851
  "Derivatives context disabled after Timescale read failure: %s",
4652
4852
  String(error)
4653
4853
  );
@@ -4659,7 +4859,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4659
4859
  var import_data3 = require("@tradejs/core/data");
4660
4860
  var import_strategies4 = require("@tradejs/core/strategies");
4661
4861
  var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWhales");
4662
- var import_logger6 = require("@tradejs/infra/logger");
4862
+ var import_logger7 = require("@tradejs/infra/logger");
4663
4863
 
4664
4864
  // src/hyperliquidWhaleUniverse.ts
4665
4865
  var import_node_crypto3 = require("crypto");
@@ -5179,7 +5379,7 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
5179
5379
  throw error;
5180
5380
  }
5181
5381
  hyperliquidWhaleContextUnavailable = true;
5182
- import_logger6.logger.warn(
5382
+ import_logger7.logger.warn(
5183
5383
  "Hyperliquid whale context disabled after Timescale read failure: %s",
5184
5384
  String(error)
5185
5385
  );
@@ -5262,7 +5462,7 @@ var runMarketContextStage = async ({
5262
5462
  elapsedMs: Date.now() - startedAt
5263
5463
  };
5264
5464
  if (status === "timed_out") {
5265
- import_logger7.logger.warn(
5465
+ import_logger8.logger.warn(
5266
5466
  "Market context stage timed out: %s after %sms",
5267
5467
  stage,
5268
5468
  result.elapsedMs
@@ -5452,79 +5652,10 @@ var enrichSignalWithAi = async ({
5452
5652
  signal.aiAnalysis = analysis;
5453
5653
  return resolveAiQuality(analysis, direction);
5454
5654
  } catch (err) {
5455
- import_logger8.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
5655
+ import_logger9.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
5456
5656
  }
5457
5657
  return void 0;
5458
5658
  };
5459
- var toFiniteNumberOrNull4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
5460
- var getOrderArrivalSnapshot = async ({
5461
- connector,
5462
- symbol
5463
- }) => {
5464
- if (typeof connector.getTopOfBookTicker !== "function") {
5465
- return {
5466
- arrivalSnapshotTime: Date.now(),
5467
- arrivalSource: "unavailable",
5468
- bid: null,
5469
- ask: null,
5470
- arrivalMid: null,
5471
- spreadBps: null
5472
- };
5473
- }
5474
- try {
5475
- const ticker = await connector.getTopOfBookTicker(symbol);
5476
- const arrivalSnapshotTime = toFiniteNumberOrNull4(ticker?.timestamp);
5477
- const bid = toFiniteNumberOrNull4(ticker?.bidPrice);
5478
- const ask = toFiniteNumberOrNull4(ticker?.askPrice);
5479
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
5480
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
5481
- return {
5482
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
5483
- arrivalSource: "top_of_book",
5484
- bid,
5485
- ask,
5486
- arrivalMid,
5487
- spreadBps
5488
- };
5489
- } catch (error) {
5490
- import_logger8.logger.warn(
5491
- "runtime order arrival snapshot failed: %s %s",
5492
- symbol,
5493
- error?.message || String(error)
5494
- );
5495
- return {
5496
- arrivalSnapshotTime: Date.now(),
5497
- arrivalSource: "top_of_book_error",
5498
- bid: null,
5499
- ask: null,
5500
- arrivalMid: null,
5501
- spreadBps: null
5502
- };
5503
- }
5504
- };
5505
- var validateEntryProtectionAtArrival = ({
5506
- direction,
5507
- signalPrice,
5508
- bid,
5509
- ask,
5510
- arrivalMid,
5511
- takeProfits,
5512
- stopLossPrice
5513
- }) => {
5514
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
5515
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
5516
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
5517
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
5518
- }
5519
- for (const takeProfit of takeProfits) {
5520
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
5521
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
5522
- }
5523
- }
5524
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
5525
- throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
5526
- }
5527
- };
5528
5659
  var resolveRuntimeTelemetryQuality = ({
5529
5660
  signalClosePrice,
5530
5661
  arrivalMid,
@@ -5601,7 +5732,8 @@ var executeEntryOrder = async ({
5601
5732
  signal,
5602
5733
  beforePlaceOrder,
5603
5734
  recordRuntimeTrade = true,
5604
- leverage
5735
+ leverage,
5736
+ executionLimits
5605
5737
  }) => {
5606
5738
  await beforePlaceOrder?.();
5607
5739
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
@@ -5612,23 +5744,57 @@ var executeEntryOrder = async ({
5612
5744
  signal.orderQty = qty;
5613
5745
  signal.orderValue = qty * currentPrice;
5614
5746
  signal.orderFailureReason = void 0;
5615
- const arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
5616
- try {
5617
- validateEntryProtectionAtArrival({
5618
- direction,
5619
- signalPrice: currentPrice,
5620
- bid: arrivalSnapshot.bid,
5621
- ask: arrivalSnapshot.ask,
5622
- arrivalMid: arrivalSnapshot.arrivalMid,
5623
- takeProfits,
5624
- stopLossPrice
5625
- });
5626
- } catch (error) {
5627
- signal.orderStatus = "failed";
5628
- signal.orderFailureReason = error.message;
5629
- throw error;
5747
+ let arrivalSnapshot = {
5748
+ arrivalSnapshotTime: Date.now(),
5749
+ arrivalSource: "unavailable",
5750
+ bid: null,
5751
+ ask: null,
5752
+ arrivalMid: null,
5753
+ spreadBps: null
5754
+ };
5755
+ let orderSubmitTime = Date.now();
5756
+ const beforeSubmit = createEntrySubmissionValidation({
5757
+ connector,
5758
+ symbol,
5759
+ direction,
5760
+ takeProfits,
5761
+ stopLossPrice,
5762
+ limits: executionLimits,
5763
+ onValidated: (snapshot2, economics2) => {
5764
+ arrivalSnapshot = snapshot2;
5765
+ orderSubmitTime = Date.now();
5766
+ if (executionLimits?.maxLossValue != null) {
5767
+ signal.riskDecision = {
5768
+ stage: "order_plan",
5769
+ status: "approved",
5770
+ reason: "PLANNED_LOSS_WITHIN_MAX_LOSS_VALUE",
5771
+ plannedLossValue: economics2.plannedLossValue,
5772
+ maxLossValue: executionLimits.maxLossValue,
5773
+ limitUtilization: economics2.plannedLossValue / executionLimits.maxLossValue,
5774
+ enforced: true
5775
+ };
5776
+ }
5777
+ }
5778
+ });
5779
+ if (!beforeSubmit) {
5780
+ arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
5781
+ try {
5782
+ validateEntryProtectionAtArrival({
5783
+ direction,
5784
+ signalPrice: currentPrice,
5785
+ bid: arrivalSnapshot.bid,
5786
+ ask: arrivalSnapshot.ask,
5787
+ arrivalMid: arrivalSnapshot.arrivalMid,
5788
+ takeProfits,
5789
+ stopLossPrice
5790
+ });
5791
+ } catch (error) {
5792
+ signal.orderStatus = "failed";
5793
+ signal.orderFailureReason = error.message;
5794
+ throw error;
5795
+ }
5796
+ orderSubmitTime = Date.now();
5630
5797
  }
5631
- const orderSubmitTime = Date.now();
5632
5798
  const orderPlaced = await connector.placeOrder({
5633
5799
  symbol,
5634
5800
  qty,
@@ -5639,7 +5805,8 @@ var executeEntryOrder = async ({
5639
5805
  direction,
5640
5806
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
5641
5807
  orderId,
5642
- signal
5808
+ signal,
5809
+ ...beforeSubmit ? { beforeSubmit } : {}
5643
5810
  });
5644
5811
  const orderAckTime = Date.now();
5645
5812
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -6468,7 +6635,7 @@ var canUseSharedReplayState = ({
6468
6635
  }) => (env === "BACKTEST" || env === "PARITY") && Boolean(sharedReplayKey);
6469
6636
 
6470
6637
  // src/strategy/runtimeExecution.ts
6471
- var import_logger9 = require("@tradejs/infra/logger");
6638
+ var import_logger10 = require("@tradejs/infra/logger");
6472
6639
  var import_types = require("@tradejs/types");
6473
6640
  var buildExitOrderSignal = ({
6474
6641
  strategyName,
@@ -6520,7 +6687,7 @@ var handleExitDecision = async ({
6520
6687
  deploymentId: connector.deploymentId
6521
6688
  });
6522
6689
  if (!activeTrade) {
6523
- import_logger9.logger.warn(
6690
+ import_logger10.logger.warn(
6524
6691
  "[%s] blocked closePosition for untracked runtime position: %s",
6525
6692
  strategyName ?? "unknown",
6526
6693
  symbol
@@ -6528,7 +6695,7 @@ var handleExitDecision = async ({
6528
6695
  return "CLOSE_BLOCKED_BY_UNTRACKED_POSITION";
6529
6696
  }
6530
6697
  if (!strategyName || activeTrade.strategy !== strategyName) {
6531
- import_logger9.logger.warn(
6698
+ import_logger10.logger.warn(
6532
6699
  "[%s] blocked closePosition for foreign runtime position: %s ownedBy=%s",
6533
6700
  strategyName ?? "unknown",
6534
6701
  symbol,
@@ -6581,7 +6748,7 @@ var handleExitDecision = async ({
6581
6748
  exitType: closedTrade?.exitType ?? "exit"
6582
6749
  });
6583
6750
  } catch (notificationError) {
6584
- import_logger9.logger.error(
6751
+ import_logger10.logger.error(
6585
6752
  "runtime close notification error: %s %s",
6586
6753
  symbol,
6587
6754
  notificationError
@@ -6595,7 +6762,7 @@ var handleExitDecision = async ({
6595
6762
  decision,
6596
6763
  market
6597
6764
  });
6598
- import_logger9.logger.error("close order error: %s %s", symbol, err);
6765
+ import_logger10.logger.error("close order error: %s %s", symbol, err);
6599
6766
  return "ORDER_ERROR";
6600
6767
  }
6601
6768
  return decision.code;
@@ -6622,7 +6789,7 @@ var handleProtectDecision = async ({
6622
6789
  decision,
6623
6790
  market
6624
6791
  });
6625
- import_logger9.logger.error("protect position error: %s %s", symbol, err);
6792
+ import_logger10.logger.error("protect position error: %s %s", symbol, err);
6626
6793
  return "ORDER_ERROR";
6627
6794
  }
6628
6795
  return decision.code;
@@ -6688,7 +6855,11 @@ var executeEntryDecision = async ({
6688
6855
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
6689
6856
  signal,
6690
6857
  beforePlaceOrder,
6691
- recordRuntimeTrade: recordRuntimeJournal
6858
+ recordRuntimeTrade: recordRuntimeJournal,
6859
+ executionLimits: resolveEntryExecutionLimits(
6860
+ hookCtx.strategyConfig,
6861
+ decision.entryContext.direction
6862
+ )
6692
6863
  });
6693
6864
  await invokeStageHooks(
6694
6865
  "afterPlaceOrder",
@@ -6710,19 +6881,32 @@ var executeEntryDecision = async ({
6710
6881
  return signal;
6711
6882
  }
6712
6883
  await beforePlaceOrder();
6713
- const arrivalSnapshot = await getOrderArrivalSnapshot({
6884
+ const beforeSubmit = createEntrySubmissionValidation({
6714
6885
  connector,
6715
- symbol
6716
- });
6717
- validateEntryProtectionAtArrival({
6886
+ symbol,
6718
6887
  direction: decision.entryContext.direction,
6719
- signalPrice: decision.entryContext.prices.currentPrice,
6720
- bid: arrivalSnapshot.bid,
6721
- ask: arrivalSnapshot.ask,
6722
- arrivalMid: arrivalSnapshot.arrivalMid,
6723
6888
  takeProfits: decision.orderPlan.takeProfits,
6724
- stopLossPrice: decision.orderPlan.stopLossPrice
6889
+ stopLossPrice: decision.orderPlan.stopLossPrice,
6890
+ limits: resolveEntryExecutionLimits(
6891
+ hookCtx.strategyConfig,
6892
+ decision.entryContext.direction
6893
+ )
6725
6894
  });
6895
+ if (!beforeSubmit) {
6896
+ const arrivalSnapshot = await getOrderArrivalSnapshot({
6897
+ connector,
6898
+ symbol
6899
+ });
6900
+ validateEntryProtectionAtArrival({
6901
+ direction: decision.entryContext.direction,
6902
+ signalPrice: decision.entryContext.prices.currentPrice,
6903
+ bid: arrivalSnapshot.bid,
6904
+ ask: arrivalSnapshot.ask,
6905
+ arrivalMid: arrivalSnapshot.arrivalMid,
6906
+ takeProfits: decision.orderPlan.takeProfits,
6907
+ stopLossPrice: decision.orderPlan.stopLossPrice
6908
+ });
6909
+ }
6726
6910
  const orderPlaced = await connector.placeOrder({
6727
6911
  symbol,
6728
6912
  qty: decision.orderPlan.qty,
@@ -6730,6 +6914,7 @@ var executeEntryDecision = async ({
6730
6914
  timestamp: decision.entryContext.timestamp,
6731
6915
  direction: decision.entryContext.direction,
6732
6916
  positionIntent: decision.orderPlan.positionIntent,
6917
+ ...beforeSubmit ? { beforeSubmit } : {},
6733
6918
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {}
6734
6919
  });
6735
6920
  if (!orderPlaced) {
@@ -6771,6 +6956,23 @@ var executeEntryDecision = async ({
6771
6956
  { decision, entry, market }
6772
6957
  );
6773
6958
  } catch (err) {
6959
+ if (err instanceof EntrySubmissionValidationError) {
6960
+ if (signal) {
6961
+ signal.orderStatus = "skipped";
6962
+ signal.orderSkipReason = err.message;
6963
+ signal.allocatorDecision = {
6964
+ stage: "runtime_admission",
6965
+ status: "rejected",
6966
+ reason: err.message
6967
+ };
6968
+ }
6969
+ import_logger10.logger.warn(
6970
+ "entry rejected before submission: %s %s",
6971
+ symbol,
6972
+ err.message
6973
+ );
6974
+ return signal ?? err.message;
6975
+ }
6774
6976
  if (signal) {
6775
6977
  signal.orderStatus = "failed";
6776
6978
  if (typeof signal.orderFailureReason !== "string" || !signal.orderFailureReason.trim()) {
@@ -6785,9 +6987,9 @@ var executeEntryDecision = async ({
6785
6987
  market
6786
6988
  });
6787
6989
  if (err?.message === import_types.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY) {
6788
- import_logger9.logger.warn("order warning: %s %s", symbol, err);
6990
+ import_logger10.logger.warn("order warning: %s %s", symbol, err);
6789
6991
  } else {
6790
- import_logger9.logger.error("order error: %s %s", symbol, err);
6992
+ import_logger10.logger.error("order error: %s %s", symbol, err);
6791
6993
  }
6792
6994
  return signal ?? "ORDER_ERROR";
6793
6995
  }
@@ -6952,7 +7154,7 @@ var createStrategyRuntime = ({
6952
7154
  try {
6953
7155
  await projectHook(errorParams);
6954
7156
  } catch (hookError) {
6955
- import_logger10.logger.error(
7157
+ import_logger11.logger.error(
6956
7158
  "project hook onRuntimeError failed: %s %s",
6957
7159
  strategyName,
6958
7160
  hookError
@@ -6966,7 +7168,7 @@ var createStrategyRuntime = ({
6966
7168
  try {
6967
7169
  await onRuntimeError(errorParams);
6968
7170
  } catch (hookError) {
6969
- import_logger10.logger.error(
7171
+ import_logger11.logger.error(
6970
7172
  "runtime hook onRuntimeError failed: %s %s",
6971
7173
  strategyName,
6972
7174
  hookError
@@ -6980,7 +7182,7 @@ var createStrategyRuntime = ({
6980
7182
  try {
6981
7183
  return await hook(params);
6982
7184
  } catch (error) {
6983
- import_logger10.logger.error(
7185
+ import_logger11.logger.error(
6984
7186
  'strategy hook "%s" failed for %s: %s',
6985
7187
  stage,
6986
7188
  strategyName,
@@ -7179,8 +7381,11 @@ var createStrategyRuntime = ({
7179
7381
  return enrichedBaseContext;
7180
7382
  }
7181
7383
  });
7384
+ const coreConfig = { ...config };
7385
+ delete coreConfig.RECORD_RUNTIME_TRADES;
7386
+ delete coreConfig.SIMULATE_ORDERS;
7182
7387
  const core = await createCore({
7183
- config,
7388
+ config: coreConfig,
7184
7389
  data,
7185
7390
  strategyApi,
7186
7391
  indicatorsState
@@ -7799,10 +8004,10 @@ var createStrategyRuntime = ({
7799
8004
  setStrategyRuntimeFactory(createStrategyRuntime);
7800
8005
 
7801
8006
  // src/connectorsRegistry.ts
7802
- var import_logger13 = require("@tradejs/infra/logger");
8007
+ var import_logger14 = require("@tradejs/infra/logger");
7803
8008
 
7804
8009
  // src/connectorRuntime.ts
7805
- var import_logger12 = require("@tradejs/infra/logger");
8010
+ var import_logger13 = require("@tradejs/infra/logger");
7806
8011
  var import_tradingAccounts = require("@tradejs/infra/tradingAccounts");
7807
8012
 
7808
8013
  // src/timescaleCachedKline.ts
@@ -7810,7 +8015,7 @@ var import_constants6 = require("@tradejs/core/constants");
7810
8015
  var import_data5 = require("@tradejs/core/data");
7811
8016
  var import_time2 = require("@tradejs/core/time");
7812
8017
  var import_async = require("@tradejs/core/async");
7813
- var import_logger11 = require("@tradejs/infra/logger");
8018
+ var import_logger12 = require("@tradejs/infra/logger");
7814
8019
  var import_candles = require("@tradejs/infra/timescale/candles");
7815
8020
  var DEFAULT_LIMIT = 1e3;
7816
8021
  var DEFAULT_CACHE_FALLBACK_WINDOW = 1e3;
@@ -7920,7 +8125,7 @@ var createTimescaleCachedKline = ({
7920
8125
  }) => {
7921
8126
  const intMinutes = intervalToMinutes(interval);
7922
8127
  if (!intMinutes) {
7923
- import_logger11.logger.log("error", "refreshTail: invalid interval %s", interval);
8128
+ import_logger12.logger.log("error", "refreshTail: invalid interval %s", interval);
7924
8129
  return;
7925
8130
  }
7926
8131
  const intervalMs = intervalMsOf(intMinutes);
@@ -7951,7 +8156,7 @@ var createTimescaleCachedKline = ({
7951
8156
  }) => {
7952
8157
  const intMinutes = intervalToMinutes(interval);
7953
8158
  if (!intMinutes) {
7954
- import_logger11.logger.log("error", "kline: invalid interval %s", interval);
8159
+ import_logger12.logger.log("error", "kline: invalid interval %s", interval);
7955
8160
  return [];
7956
8161
  }
7957
8162
  if (defaultStart !== void 0 && defaultEnd !== void 0 && defaultEnd <= defaultStart) {
@@ -8060,7 +8265,7 @@ var createTimescaleCachedKline = ({
8060
8265
  if (warmOnly) {
8061
8266
  if (isTimescaleFallbackMode) {
8062
8267
  isTimescaleFallbackMode = false;
8063
- import_logger11.logger.log("info", "TimescaleDB connection restored for kline cache");
8268
+ import_logger12.logger.log("info", "TimescaleDB connection restored for kline cache");
8064
8269
  }
8065
8270
  return [];
8066
8271
  }
@@ -8078,13 +8283,13 @@ var createTimescaleCachedKline = ({
8078
8283
  );
8079
8284
  if (isTimescaleFallbackMode) {
8080
8285
  isTimescaleFallbackMode = false;
8081
- import_logger11.logger.log("info", "TimescaleDB connection restored for kline cache");
8286
+ import_logger12.logger.log("info", "TimescaleDB connection restored for kline cache");
8082
8287
  }
8083
8288
  return dbData;
8084
8289
  } catch (error) {
8085
8290
  if (!isTimescaleFallbackMode) {
8086
8291
  isTimescaleFallbackMode = true;
8087
- import_logger11.logger.log(
8292
+ import_logger12.logger.log(
8088
8293
  "warn",
8089
8294
  "TimescaleDB unavailable for %s %s: %s. Falling back to exchange API.",
8090
8295
  symbol,
@@ -8108,7 +8313,7 @@ var createTimescaleCachedKline = ({
8108
8313
 
8109
8314
  // src/connectorRuntime.ts
8110
8315
  var connectorRuntime = {
8111
- logger: import_logger12.logger,
8316
+ logger: import_logger13.logger,
8112
8317
  resolveTradingAccount: import_tradingAccounts.resolveTradingAccount,
8113
8318
  createCachedKline: createTimescaleCachedKline
8114
8319
  };
@@ -8169,7 +8374,7 @@ var normalizeProviders = (providers, connectorName) => {
8169
8374
  var registerProvider = (provider, connectorName, source, providerToConnectorName) => {
8170
8375
  const existing = providerToConnectorName.get(provider);
8171
8376
  if (existing && existing !== connectorName) {
8172
- import_logger13.logger.warn(
8377
+ import_logger14.logger.warn(
8173
8378
  'Skip duplicate connector provider "%s" from %s: already mapped to %s',
8174
8379
  provider,
8175
8380
  source,
@@ -8182,11 +8387,11 @@ var registerProvider = (provider, connectorName, source, providerToConnectorName
8182
8387
  var registerEntry = (entry, source, state) => {
8183
8388
  const connectorName = String(entry?.name ?? "").trim();
8184
8389
  if (!connectorName) {
8185
- import_logger13.logger.warn("Skip connector entry without name from %s", source);
8390
+ import_logger14.logger.warn("Skip connector entry without name from %s", source);
8186
8391
  return;
8187
8392
  }
8188
8393
  if (typeof entry.creator !== "function") {
8189
- import_logger13.logger.warn(
8394
+ import_logger14.logger.warn(
8190
8395
  'Skip connector entry "%s" from %s: creator must be a function',
8191
8396
  connectorName,
8192
8397
  source
@@ -8198,7 +8403,7 @@ var registerEntry = (entry, source, state) => {
8198
8403
  state.connectorCreators
8199
8404
  );
8200
8405
  if (existingByName) {
8201
- import_logger13.logger.warn(
8406
+ import_logger14.logger.warn(
8202
8407
  'Skip duplicate connector "%s" from %s: already registered as %s',
8203
8408
  connectorName,
8204
8409
  source,
@@ -8273,7 +8478,7 @@ var ensureConnectorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
8273
8478
  );
8274
8479
  const pluginDefinition = extractConnectorPluginDefinition(moduleExport);
8275
8480
  if (!pluginDefinition) {
8276
- import_logger13.logger.warn(
8481
+ import_logger14.logger.warn(
8277
8482
  'Skip connector plugin "%s": export { connectorEntries } is missing',
8278
8483
  moduleName
8279
8484
  );
@@ -8281,7 +8486,7 @@ var ensureConnectorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
8281
8486
  }
8282
8487
  registerEntries2(pluginDefinition.connectorEntries, moduleName, state);
8283
8488
  } catch (error) {
8284
- import_logger13.logger.warn(
8489
+ import_logger14.logger.warn(
8285
8490
  'Failed to load connector plugin "%s": %s',
8286
8491
  moduleName,
8287
8492
  String(error)