@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.
@@ -2909,13 +2909,213 @@ var assessSignalWithJev = async ({
2909
2909
  // src/strategyRuntime.ts
2910
2910
  var import_constants5 = require("@tradejs/core/constants");
2911
2911
  var import_strategies6 = require("@tradejs/core/strategies");
2912
- var import_logger10 = require("@tradejs/infra/logger");
2912
+ var import_logger11 = require("@tradejs/infra/logger");
2913
2913
 
2914
2914
  // src/strategyHelpers/runtime.ts
2915
- var import_logger8 = require("@tradejs/infra/logger");
2915
+ var import_logger9 = require("@tradejs/infra/logger");
2916
2916
  var import_constants3 = require("@tradejs/core/constants");
2917
2917
  var import_ml2 = require("@tradejs/infra/ml");
2918
2918
 
2919
+ // src/strategy/entrySubmissionValidation.ts
2920
+ var import_logger2 = require("@tradejs/infra/logger");
2921
+
2922
+ // src/strategy/entryExecutionValidation.ts
2923
+ var EntrySubmissionValidationError = class extends Error {
2924
+ constructor() {
2925
+ super(...arguments);
2926
+ this.name = "EntrySubmissionValidationError";
2927
+ }
2928
+ };
2929
+ var resolveEntryExecutionLimits = (config, direction) => {
2930
+ const side = config[direction];
2931
+ const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
2932
+ const maximum = config.MAX_LOSS_VALUE;
2933
+ if (minimum == null && maximum == null) return void 0;
2934
+ const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
2935
+ const maxLossValue = maximum == null ? void 0 : Number(maximum);
2936
+ const feeRate = Number(config.RISK_FEE_RATE ?? 0);
2937
+ const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
2938
+ const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
2939
+ const slippageBps = slippage + impact;
2940
+ 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)
2941
+ throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
2942
+ return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
2943
+ };
2944
+ var validateEntryExecution = ({
2945
+ direction,
2946
+ price,
2947
+ qty,
2948
+ stopLossPrice,
2949
+ takeProfitPrice,
2950
+ limits
2951
+ }) => {
2952
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
2953
+ throw new EntrySubmissionValidationError(
2954
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2955
+ );
2956
+ const side = direction === "LONG" ? 1 : -1;
2957
+ if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
2958
+ throw new EntrySubmissionValidationError(
2959
+ "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
2960
+ );
2961
+ const slippage = limits.slippageBps / 1e4;
2962
+ const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
2963
+ const entryCost = cost(price, side);
2964
+ const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
2965
+ const rewardPerUnit = Math.max(
2966
+ 0,
2967
+ Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
2968
+ );
2969
+ const netRiskRatio = rewardPerUnit / lossPerUnit;
2970
+ const plannedLossValue = qty * lossPerUnit;
2971
+ if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
2972
+ throw new EntrySubmissionValidationError(
2973
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2974
+ );
2975
+ if (limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio)
2976
+ throw new EntrySubmissionValidationError(
2977
+ "ENTRY_RR_BELOW_MIN_AT_SUBMISSION"
2978
+ );
2979
+ if (limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9))
2980
+ throw new EntrySubmissionValidationError(
2981
+ "ENTRY_RISK_EXCEEDS_MAX_LOSS_AT_SUBMISSION"
2982
+ );
2983
+ return { netRiskRatio, plannedLossValue };
2984
+ };
2985
+
2986
+ // src/strategy/entrySubmissionValidation.ts
2987
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
2988
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
2989
+ var getOrderArrivalSnapshot = async ({
2990
+ connector,
2991
+ symbol,
2992
+ requireTimestamp = false
2993
+ }) => {
2994
+ if (typeof connector.getTopOfBookTicker !== "function") {
2995
+ return {
2996
+ arrivalSnapshotTime: Date.now(),
2997
+ arrivalSource: "unavailable",
2998
+ bid: null,
2999
+ ask: null,
3000
+ arrivalMid: null,
3001
+ spreadBps: null
3002
+ };
3003
+ }
3004
+ try {
3005
+ const ticker = await connector.getTopOfBookTicker(symbol);
3006
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
3007
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
3008
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
3009
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
3010
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
3011
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
3012
+ return {
3013
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
3014
+ arrivalSource: "top_of_book",
3015
+ bid,
3016
+ ask,
3017
+ arrivalMid,
3018
+ spreadBps
3019
+ };
3020
+ } catch (error) {
3021
+ import_logger2.logger.warn(
3022
+ "runtime order arrival snapshot failed: %s %s",
3023
+ symbol,
3024
+ error?.message || String(error)
3025
+ );
3026
+ return {
3027
+ arrivalSnapshotTime: Date.now(),
3028
+ arrivalSource: "top_of_book_error",
3029
+ bid: null,
3030
+ ask: null,
3031
+ arrivalMid: null,
3032
+ spreadBps: null
3033
+ };
3034
+ }
3035
+ };
3036
+ var validateEntryProtectionAtArrival = ({
3037
+ direction,
3038
+ signalPrice,
3039
+ bid,
3040
+ ask,
3041
+ arrivalMid,
3042
+ takeProfits,
3043
+ stopLossPrice
3044
+ }) => {
3045
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
3046
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
3047
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
3048
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
3049
+ }
3050
+ for (const takeProfit of takeProfits) {
3051
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
3052
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
3053
+ }
3054
+ }
3055
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
3056
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
3057
+ }
3058
+ };
3059
+ var createEntrySubmissionValidation = ({
3060
+ connector,
3061
+ symbol,
3062
+ direction,
3063
+ takeProfits,
3064
+ stopLossPrice,
3065
+ limits,
3066
+ onValidated
3067
+ }) => {
3068
+ if (!limits) return void 0;
3069
+ if (!connector.capabilities?.orderSubmissionValidation)
3070
+ throw new EntrySubmissionValidationError(
3071
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
3072
+ );
3073
+ return async ({ qty, executionPrice }) => {
3074
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
3075
+ const snapshot2 = simulated && executionPrice != null ? {
3076
+ arrivalSnapshotTime: Date.now(),
3077
+ arrivalSource: "simulation",
3078
+ bid: executionPrice,
3079
+ ask: executionPrice,
3080
+ arrivalMid: executionPrice,
3081
+ spreadBps: 0
3082
+ } : await getOrderArrivalSnapshot({
3083
+ connector,
3084
+ symbol,
3085
+ requireTimestamp: true
3086
+ });
3087
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
3088
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
3089
+ throw new EntrySubmissionValidationError(
3090
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
3091
+ );
3092
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
3093
+ throw new EntrySubmissionValidationError(
3094
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
3095
+ );
3096
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
3097
+ validateEntryProtectionAtArrival({
3098
+ direction,
3099
+ signalPrice: price,
3100
+ bid: snapshot2.bid,
3101
+ ask: snapshot2.ask,
3102
+ arrivalMid: snapshot2.arrivalMid,
3103
+ takeProfits,
3104
+ stopLossPrice
3105
+ });
3106
+ const prices = takeProfits.map((tp) => tp.price);
3107
+ const economics2 = validateEntryExecution({
3108
+ direction,
3109
+ price,
3110
+ qty,
3111
+ stopLossPrice,
3112
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
3113
+ limits
3114
+ });
3115
+ onValidated?.(snapshot2, economics2);
3116
+ };
3117
+ };
3118
+
2919
3119
  // src/strategyAdapters/ml.ts
2920
3120
  var defaultMlAdapter = {
2921
3121
  normalizeStrategyConfig: (strategyConfig) => strategyConfig
@@ -2969,7 +3169,7 @@ var import_node_crypto = require("crypto");
2969
3169
  var import_constants = require("@tradejs/core/constants");
2970
3170
  var import_time = require("@tradejs/core/time");
2971
3171
  var import_trade = require("@tradejs/core/trade");
2972
- var import_logger2 = require("@tradejs/infra/logger");
3172
+ var import_logger3 = require("@tradejs/infra/logger");
2973
3173
  var import_redis2 = require("@tradejs/infra/redis");
2974
3174
  var now = () => Date.now();
2975
3175
  var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
@@ -3028,7 +3228,7 @@ var recordRuntimeTradeOpen = async (params) => {
3028
3228
  )
3029
3229
  ]);
3030
3230
  } catch (error) {
3031
- import_logger2.logger.error(
3231
+ import_logger3.logger.error(
3032
3232
  "runtime trade open journal failed: %s %s",
3033
3233
  record.symbol,
3034
3234
  error?.message || String(error)
@@ -3096,7 +3296,7 @@ var recordRuntimeTradeIncrease = async (params) => {
3096
3296
  )
3097
3297
  ]);
3098
3298
  } catch (error) {
3099
- import_logger2.logger.error(
3299
+ import_logger3.logger.error(
3100
3300
  "runtime trade increase journal failed: %s %s",
3101
3301
  symbol,
3102
3302
  error?.message || String(error)
@@ -3203,7 +3403,7 @@ var markRuntimeTradeClosed = async (params) => {
3203
3403
  )
3204
3404
  ]);
3205
3405
  } catch (error) {
3206
- import_logger2.logger.error(
3406
+ import_logger3.logger.error(
3207
3407
  "runtime trade close journal failed: %s %s",
3208
3408
  symbol,
3209
3409
  error?.message || String(error)
@@ -3213,11 +3413,11 @@ var markRuntimeTradeClosed = async (params) => {
3213
3413
  };
3214
3414
 
3215
3415
  // src/strategyHelpers/marketContextStages.ts
3216
- var import_logger7 = require("@tradejs/infra/logger");
3416
+ var import_logger8 = require("@tradejs/infra/logger");
3217
3417
 
3218
3418
  // src/strategyHelpers/binanceMarketContext.ts
3219
3419
  var import_marketContext = require("@tradejs/infra/timescale/marketContext");
3220
- var import_logger3 = require("@tradejs/infra/logger");
3420
+ var import_logger4 = require("@tradejs/infra/logger");
3221
3421
  var import_strategies = require("@tradejs/core/strategies");
3222
3422
 
3223
3423
  // src/binanceBreadthUniverses.ts
@@ -3593,7 +3793,7 @@ var parseEnabledFlag = (value, env) => {
3593
3793
  if (normalized === "live") return env !== "BACKTEST";
3594
3794
  return false;
3595
3795
  };
3596
- var toFiniteNumberOrNull = (value) => {
3796
+ var toFiniteNumberOrNull2 = (value) => {
3597
3797
  const numeric = typeof value === "number" ? value : Number(value);
3598
3798
  return Number.isFinite(numeric) ? numeric : null;
3599
3799
  };
@@ -3858,14 +4058,14 @@ var toTradeFlowContext = (row, interval) => row ? {
3858
4058
  asOfTs: row.ts.getTime(),
3859
4059
  ageMs: row.ageMs,
3860
4060
  stale: row.stale,
3861
- trades: toFiniteNumberOrNull(row.trades),
3862
- buyPressurePct: toFiniteNumberOrNull(row.buyPressurePct),
3863
- buyBaseVolume: toFiniteNumberOrNull(row.buyBaseVolume),
3864
- sellBaseVolume: toFiniteNumberOrNull(row.sellBaseVolume),
3865
- buyQuoteVolume: toFiniteNumberOrNull(row.buyQuoteVolume),
3866
- sellQuoteVolume: toFiniteNumberOrNull(row.sellQuoteVolume),
3867
- netBaseDelta: toFiniteNumberOrNull(row.netBaseDelta),
3868
- netQuoteDelta: toFiniteNumberOrNull(row.netQuoteDelta)
4061
+ trades: toFiniteNumberOrNull2(row.trades),
4062
+ buyPressurePct: toFiniteNumberOrNull2(row.buyPressurePct),
4063
+ buyBaseVolume: toFiniteNumberOrNull2(row.buyBaseVolume),
4064
+ sellBaseVolume: toFiniteNumberOrNull2(row.sellBaseVolume),
4065
+ buyQuoteVolume: toFiniteNumberOrNull2(row.buyQuoteVolume),
4066
+ sellQuoteVolume: toFiniteNumberOrNull2(row.sellQuoteVolume),
4067
+ netBaseDelta: toFiniteNumberOrNull2(row.netBaseDelta),
4068
+ netQuoteDelta: toFiniteNumberOrNull2(row.netQuoteDelta)
3869
4069
  } : null;
3870
4070
  var getCachedReferenceRows = ({
3871
4071
  referenceSymbols,
@@ -3954,16 +4154,16 @@ var toMarketBreadthContext = (breadth, interval) => ({
3954
4154
  asOfTs: breadth.ts.getTime(),
3955
4155
  ageMs: breadth.ageMs,
3956
4156
  stale: breadth.stale,
3957
- symbolsCount: toFiniteNumberOrNull(breadth.symbolsCount),
3958
- advancers: toFiniteNumberOrNull(breadth.advancers),
3959
- decliners: toFiniteNumberOrNull(breadth.decliners),
3960
- unchanged: toFiniteNumberOrNull(breadth.unchanged),
3961
- advanceDeclineRatio: toFiniteNumberOrNull(breadth.advanceDeclineRatio),
3962
- pctAboveMa20: toFiniteNumberOrNull(breadth.pctAboveMa20),
3963
- pctAboveMa50: toFiniteNumberOrNull(breadth.pctAboveMa50),
3964
- equalWeightedReturn: toFiniteNumberOrNull(breadth.equalWeightedReturn),
3965
- volumeWeightedReturn: toFiniteNumberOrNull(breadth.volumeWeightedReturn),
3966
- dispersion: toFiniteNumberOrNull(breadth.dispersion)
4157
+ symbolsCount: toFiniteNumberOrNull2(breadth.symbolsCount),
4158
+ advancers: toFiniteNumberOrNull2(breadth.advancers),
4159
+ decliners: toFiniteNumberOrNull2(breadth.decliners),
4160
+ unchanged: toFiniteNumberOrNull2(breadth.unchanged),
4161
+ advanceDeclineRatio: toFiniteNumberOrNull2(breadth.advanceDeclineRatio),
4162
+ pctAboveMa20: toFiniteNumberOrNull2(breadth.pctAboveMa20),
4163
+ pctAboveMa50: toFiniteNumberOrNull2(breadth.pctAboveMa50),
4164
+ equalWeightedReturn: toFiniteNumberOrNull2(breadth.equalWeightedReturn),
4165
+ volumeWeightedReturn: toFiniteNumberOrNull2(breadth.volumeWeightedReturn),
4166
+ dispersion: toFiniteNumberOrNull2(breadth.dispersion)
3967
4167
  });
3968
4168
  var enrichSignalWithBinanceMarketContext = async (params) => {
3969
4169
  const {
@@ -4064,46 +4264,46 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
4064
4264
  asOfTs: primaryBreadth.ts.getTime(),
4065
4265
  ageMs: primaryBreadth.ageMs,
4066
4266
  stale: primaryBreadth.stale,
4067
- btcReturn1h: toFiniteNumberOrNull(
4267
+ btcReturn1h: toFiniteNumberOrNull2(
4068
4268
  primaryBreadth.btcReturn1h
4069
4269
  ),
4070
- btcReturn4h: toFiniteNumberOrNull(
4270
+ btcReturn4h: toFiniteNumberOrNull2(
4071
4271
  primaryBreadth.btcReturn4h
4072
4272
  ),
4073
- btcReturn24h: toFiniteNumberOrNull(
4273
+ btcReturn24h: toFiniteNumberOrNull2(
4074
4274
  primaryBreadth.btcReturn24h
4075
4275
  ),
4076
- altBasketReturn1h: toFiniteNumberOrNull(
4276
+ altBasketReturn1h: toFiniteNumberOrNull2(
4077
4277
  primaryBreadth.altBasketReturn1h
4078
4278
  ),
4079
- altBasketReturn4h: toFiniteNumberOrNull(
4279
+ altBasketReturn4h: toFiniteNumberOrNull2(
4080
4280
  primaryBreadth.altBasketReturn4h
4081
4281
  ),
4082
- altBasketReturn24h: toFiniteNumberOrNull(
4282
+ altBasketReturn24h: toFiniteNumberOrNull2(
4083
4283
  primaryBreadth.altBasketReturn24h
4084
4284
  ),
4085
- btcVsAltReturn1h: toFiniteNumberOrNull(
4285
+ btcVsAltReturn1h: toFiniteNumberOrNull2(
4086
4286
  primaryBreadth.btcVsAltReturn1h
4087
4287
  ),
4088
- btcVsAltReturn4h: toFiniteNumberOrNull(
4288
+ btcVsAltReturn4h: toFiniteNumberOrNull2(
4089
4289
  primaryBreadth.btcVsAltReturn4h
4090
4290
  ),
4091
- btcVsAltReturn24h: toFiniteNumberOrNull(
4291
+ btcVsAltReturn24h: toFiniteNumberOrNull2(
4092
4292
  primaryBreadth.btcVsAltReturn24h
4093
4293
  ),
4094
- btcTurnoverShare1h: toFiniteNumberOrNull(
4294
+ btcTurnoverShare1h: toFiniteNumberOrNull2(
4095
4295
  primaryBreadth.btcTurnoverShare1h
4096
4296
  ),
4097
- btcTurnoverShare24h: toFiniteNumberOrNull(
4297
+ btcTurnoverShare24h: toFiniteNumberOrNull2(
4098
4298
  primaryBreadth.btcTurnoverShare24h
4099
4299
  ),
4100
- btcTurnoverShareChange24h: toFiniteNumberOrNull(
4300
+ btcTurnoverShareChange24h: toFiniteNumberOrNull2(
4101
4301
  primaryBreadth.btcTurnoverShareChange24h
4102
4302
  ),
4103
- altVolToBtcVol24h: toFiniteNumberOrNull(
4303
+ altVolToBtcVol24h: toFiniteNumberOrNull2(
4104
4304
  primaryBreadth.altVolToBtcVol24h
4105
4305
  ),
4106
- altDispersion24h: toFiniteNumberOrNull(
4306
+ altDispersion24h: toFiniteNumberOrNull2(
4107
4307
  primaryBreadth.altDispersion24h
4108
4308
  ),
4109
4309
  regime: primaryBreadth.btcAltRegime ?? "unknown"
@@ -4120,7 +4320,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
4120
4320
  throw error;
4121
4321
  }
4122
4322
  binanceMarketContextUnavailable = true;
4123
- import_logger3.logger.warn(
4323
+ import_logger4.logger.warn(
4124
4324
  "Binance market context disabled after Timescale read failure: %s",
4125
4325
  String(error)
4126
4326
  );
@@ -4130,7 +4330,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
4130
4330
 
4131
4331
  // src/strategyHelpers/coinMarketCapContext.ts
4132
4332
  var import_strategies2 = require("@tradejs/core/strategies");
4133
- var import_logger4 = require("@tradejs/infra/logger");
4333
+ var import_logger5 = require("@tradejs/infra/logger");
4134
4334
  var import_marketContext2 = require("@tradejs/infra/timescale/marketContext");
4135
4335
  var DEFAULT_MAX_AGE_MS = 48 * 60 * 6e4;
4136
4336
  var SOURCE_GLOBAL_DAILY = "coinmarketcap_global";
@@ -4168,7 +4368,7 @@ var asInt = (value, fallback) => {
4168
4368
  const parsed = Number.parseInt(String(value ?? ""), 10);
4169
4369
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
4170
4370
  };
4171
- var toFiniteNumberOrNull2 = (value) => {
4371
+ var toFiniteNumberOrNull3 = (value) => {
4172
4372
  const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
4173
4373
  return Number.isFinite(numeric) ? numeric : null;
4174
4374
  };
@@ -4402,12 +4602,12 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4402
4602
  const ethRow = references.get("ETHUSDT") ?? null;
4403
4603
  const previousBtcRow = previousReferences.get("BTCUSDT") ?? null;
4404
4604
  const previousEthRow = previousReferences.get("ETHUSDT") ?? null;
4405
- const btcMarketCapUsd = toFiniteNumberOrNull2(btcRow?.marketCapUsd);
4406
- const ethMarketCapUsd = toFiniteNumberOrNull2(ethRow?.marketCapUsd);
4407
- const previousBtcMarketCapUsd = toFiniteNumberOrNull2(
4605
+ const btcMarketCapUsd = toFiniteNumberOrNull3(btcRow?.marketCapUsd);
4606
+ const ethMarketCapUsd = toFiniteNumberOrNull3(ethRow?.marketCapUsd);
4607
+ const previousBtcMarketCapUsd = toFiniteNumberOrNull3(
4408
4608
  previousBtcRow?.marketCapUsd
4409
4609
  );
4410
- const previousEthMarketCapUsd = toFiniteNumberOrNull2(
4610
+ const previousEthMarketCapUsd = toFiniteNumberOrNull3(
4411
4611
  previousEthRow?.marketCapUsd
4412
4612
  );
4413
4613
  const ethBtcMarketCapRatio = safeDivide(ethMarketCapUsd, btcMarketCapUsd);
@@ -4416,16 +4616,16 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4416
4616
  previousBtcMarketCapUsd
4417
4617
  );
4418
4618
  const ethBtcMarketCapRatioChange24hPct = ethBtcMarketCapRatio != null && previousEthBtcMarketCapRatio != null && previousEthBtcMarketCapRatio > 0 ? (ethBtcMarketCapRatio - previousEthBtcMarketCapRatio) / previousEthBtcMarketCapRatio : null;
4419
- const btcVolumeUsd = toFiniteNumberOrNull2(btcRow?.volumeUsd);
4420
- const ethVolumeUsd = toFiniteNumberOrNull2(ethRow?.volumeUsd);
4619
+ const btcVolumeUsd = toFiniteNumberOrNull3(btcRow?.volumeUsd);
4620
+ const ethVolumeUsd = toFiniteNumberOrNull3(ethRow?.volumeUsd);
4421
4621
  const referenceStale = btcRow?.stale === true || ethRow?.stale === true || !btcRow || !ethRow;
4422
- const btcDominanceChange24hPct = toFiniteNumberOrNull2(
4622
+ const btcDominanceChange24hPct = toFiniteNumberOrNull3(
4423
4623
  globalRow?.btcDominanceChange24hPct
4424
4624
  );
4425
- const altMarketCapChange24hPct = toFiniteNumberOrNull2(
4625
+ const altMarketCapChange24hPct = toFiniteNumberOrNull3(
4426
4626
  globalRow?.altMarketCapChange24hPct
4427
4627
  );
4428
- const altVolumeChange24hPct = toFiniteNumberOrNull2(
4628
+ const altVolumeChange24hPct = toFiniteNumberOrNull3(
4429
4629
  globalRow?.altVolumeChange24hPct
4430
4630
  );
4431
4631
  const altLiquidityRegime = globalRow ? toAltLiquidityRegime({
@@ -4436,19 +4636,19 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4436
4636
  }) : "unknown";
4437
4637
  const exchangeLiquidityRegime = exchangeLiquidityRow ? toExchangeLiquidityRegime({
4438
4638
  stale: exchangeLiquidityRow.stale,
4439
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
4639
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
4440
4640
  exchangeLiquidityRow.totalVolumeChange24hPct
4441
4641
  ),
4442
4642
  fallback: exchangeLiquidityRow.liquidityRegime ?? "unknown"
4443
4643
  }) : "unknown";
4444
4644
  const cmc100Row = indexRows.get("cmc100") ?? null;
4445
4645
  const cmc20Row = indexRows.get("cmc20") ?? null;
4446
- const cmc100Value = toFiniteNumberOrNull2(cmc100Row?.value);
4447
- const cmc20Value = toFiniteNumberOrNull2(cmc20Row?.value);
4448
- const cmc100Change24hPct = toFiniteNumberOrNull2(
4646
+ const cmc100Value = toFiniteNumberOrNull3(cmc100Row?.value);
4647
+ const cmc20Value = toFiniteNumberOrNull3(cmc20Row?.value);
4648
+ const cmc100Change24hPct = toFiniteNumberOrNull3(
4449
4649
  cmc100Row?.valueChange24hPct
4450
4650
  );
4451
- const cmc20Change24hPct = toFiniteNumberOrNull2(cmc20Row?.valueChange24hPct);
4651
+ const cmc20Change24hPct = toFiniteNumberOrNull3(cmc20Row?.valueChange24hPct);
4452
4652
  const cmc20ToCmc100Ratio = safeDivide(cmc20Value, cmc100Value);
4453
4653
  const cmc20ToCmc100RatioChange24hPct = cmc20Change24hPct != null && cmc100Change24hPct != null ? (1 + cmc20Change24hPct) / (1 + cmc100Change24hPct) - 1 : null;
4454
4654
  const indexStale = cmc100Row?.stale === true || cmc20Row?.stale === true || !cmc100Row || !cmc20Row;
@@ -4472,41 +4672,41 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4472
4672
  asOfTs: globalRow.ts.getTime(),
4473
4673
  ageMs: globalRow.ageMs,
4474
4674
  stale: globalRow.stale,
4475
- totalMarketCapUsd: toFiniteNumberOrNull2(
4675
+ totalMarketCapUsd: toFiniteNumberOrNull3(
4476
4676
  globalRow.totalMarketCapUsd
4477
4677
  ),
4478
- totalVolumeUsd: toFiniteNumberOrNull2(
4678
+ totalVolumeUsd: toFiniteNumberOrNull3(
4479
4679
  globalRow.totalVolumeUsd
4480
4680
  ),
4481
- totalVolumeReportedUsd: toFiniteNumberOrNull2(
4681
+ totalVolumeReportedUsd: toFiniteNumberOrNull3(
4482
4682
  globalRow.totalVolumeReportedUsd
4483
4683
  ),
4484
- altMarketCapUsd: toFiniteNumberOrNull2(
4684
+ altMarketCapUsd: toFiniteNumberOrNull3(
4485
4685
  globalRow.altMarketCapUsd
4486
4686
  ),
4487
- altVolumeUsd: toFiniteNumberOrNull2(globalRow.altVolumeUsd),
4488
- altVolumeReportedUsd: toFiniteNumberOrNull2(
4687
+ altVolumeUsd: toFiniteNumberOrNull3(globalRow.altVolumeUsd),
4688
+ altVolumeReportedUsd: toFiniteNumberOrNull3(
4489
4689
  globalRow.altVolumeReportedUsd
4490
4690
  ),
4491
- btcDominancePct: toFiniteNumberOrNull2(
4691
+ btcDominancePct: toFiniteNumberOrNull3(
4492
4692
  globalRow.btcDominancePct
4493
4693
  ),
4494
- ethDominancePct: toFiniteNumberOrNull2(
4694
+ ethDominancePct: toFiniteNumberOrNull3(
4495
4695
  globalRow.ethDominancePct
4496
4696
  ),
4497
4697
  btcDominanceChange24hPct,
4498
- ethDominanceChange24hPct: toFiniteNumberOrNull2(
4698
+ ethDominanceChange24hPct: toFiniteNumberOrNull3(
4499
4699
  globalRow.ethDominanceChange24hPct
4500
4700
  ),
4501
4701
  altMarketCapChange24hPct,
4502
4702
  altVolumeChange24hPct,
4503
- activeCryptocurrencies: toFiniteNumberOrNull2(
4703
+ activeCryptocurrencies: toFiniteNumberOrNull3(
4504
4704
  globalRow.activeCryptocurrencies
4505
4705
  ),
4506
- activeExchanges: toFiniteNumberOrNull2(
4706
+ activeExchanges: toFiniteNumberOrNull3(
4507
4707
  globalRow.activeExchanges
4508
4708
  ),
4509
- activeMarketPairs: toFiniteNumberOrNull2(
4709
+ activeMarketPairs: toFiniteNumberOrNull3(
4510
4710
  globalRow.activeMarketPairs
4511
4711
  ),
4512
4712
  altLiquidityRegime
@@ -4551,22 +4751,22 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4551
4751
  asOfTs: exchangeLiquidityRow.ts.getTime(),
4552
4752
  ageMs: exchangeLiquidityRow.ageMs,
4553
4753
  stale: exchangeLiquidityRow.stale,
4554
- exchangesCount: toFiniteNumberOrNull2(
4754
+ exchangesCount: toFiniteNumberOrNull3(
4555
4755
  exchangeLiquidityRow.exchangesCount
4556
4756
  ),
4557
- totalVolumeUsd: toFiniteNumberOrNull2(
4757
+ totalVolumeUsd: toFiniteNumberOrNull3(
4558
4758
  exchangeLiquidityRow.totalVolumeUsd
4559
4759
  ),
4560
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
4760
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
4561
4761
  exchangeLiquidityRow.totalVolumeChange24hPct
4562
4762
  ),
4563
- binanceVolumeUsd: toFiniteNumberOrNull2(
4763
+ binanceVolumeUsd: toFiniteNumberOrNull3(
4564
4764
  exchangeLiquidityRow.binanceVolumeUsd
4565
4765
  ),
4566
- binanceVolumeShare: toFiniteNumberOrNull2(
4766
+ binanceVolumeShare: toFiniteNumberOrNull3(
4567
4767
  exchangeLiquidityRow.binanceVolumeShare
4568
4768
  ),
4569
- topExchangeVolumeShare: toFiniteNumberOrNull2(
4769
+ topExchangeVolumeShare: toFiniteNumberOrNull3(
4570
4770
  exchangeLiquidityRow.topExchangeVolumeShare
4571
4771
  ),
4572
4772
  liquidityRegime: exchangeLiquidityRegime
@@ -4579,11 +4779,11 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4579
4779
  asOfTs: fearGreedRow.ts.getTime(),
4580
4780
  ageMs: fearGreedRow.ageMs,
4581
4781
  stale: fearGreedRow.stale,
4582
- value: toFiniteNumberOrNull2(fearGreedRow.value),
4583
- valueChange24h: toFiniteNumberOrNull2(
4782
+ value: toFiniteNumberOrNull3(fearGreedRow.value),
4783
+ valueChange24h: toFiniteNumberOrNull3(
4584
4784
  fearGreedRow.valueChange24h
4585
4785
  ),
4586
- valueChange7d: toFiniteNumberOrNull2(
4786
+ valueChange7d: toFiniteNumberOrNull3(
4587
4787
  fearGreedRow.valueChange7d
4588
4788
  ),
4589
4789
  classification: fearGreedRow.classification ?? "Unknown",
@@ -4603,13 +4803,13 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4603
4803
  cmc100Value,
4604
4804
  cmc100Change24hPct,
4605
4805
  cmc100TopConstituentSymbol: cmc100Row?.topConstituentSymbol ?? null,
4606
- cmc100TopConstituentWeightPct: toFiniteNumberOrNull2(
4806
+ cmc100TopConstituentWeightPct: toFiniteNumberOrNull3(
4607
4807
  cmc100Row?.topConstituentWeightPct
4608
4808
  ),
4609
4809
  cmc20Value,
4610
4810
  cmc20Change24hPct,
4611
4811
  cmc20TopConstituentSymbol: cmc20Row?.topConstituentSymbol ?? null,
4612
- cmc20TopConstituentWeightPct: toFiniteNumberOrNull2(
4812
+ cmc20TopConstituentWeightPct: toFiniteNumberOrNull3(
4613
4813
  cmc20Row?.topConstituentWeightPct
4614
4814
  ),
4615
4815
  cmc20ToCmc100Ratio,
@@ -4627,7 +4827,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4627
4827
  throw error;
4628
4828
  }
4629
4829
  coinMarketCapContextUnavailable = true;
4630
- import_logger4.logger.warn(
4830
+ import_logger5.logger.warn(
4631
4831
  "CoinMarketCap context disabled after Timescale read failure: %s",
4632
4832
  String(error)
4633
4833
  );
@@ -4641,7 +4841,7 @@ var import_data2 = require("@tradejs/core/data");
4641
4841
  var import_strategies3 = require("@tradejs/core/strategies");
4642
4842
  var import_constants2 = require("@tradejs/core/constants");
4643
4843
  var import_derivatives = require("@tradejs/infra/timescale/derivatives");
4644
- var import_logger5 = require("@tradejs/infra/logger");
4844
+ var import_logger6 = require("@tradejs/infra/logger");
4645
4845
  var STORED_INTERVALS = ["15m", "1h"];
4646
4846
  var CONTEXT_INTERVALS = ["15m", "1h"];
4647
4847
  var DEFAULT_LOOKBACK_HOURS = 48;
@@ -4696,7 +4896,7 @@ var resolvePrimaryReferenceSymbol2 = () => PRIMARY_DERIVATIVES_REFERENCE_SYMBOL;
4696
4896
  var resolveSecondaryReferenceSymbol = () => SECONDARY_DERIVATIVES_REFERENCE_SYMBOL;
4697
4897
  var getPrimaryIntervalContext = (context) => context?.intervals["15m"] ?? context?.intervals["1h"] ?? null;
4698
4898
  var hasDerivativesSymbolData = (context) => Object.keys(context.intervals).length > 0 && !context.summary.riskFlags.includes("missing_derivatives");
4699
- var toFiniteNumberOrNull3 = (value) => {
4899
+ var toFiniteNumberOrNull4 = (value) => {
4700
4900
  if (typeof value === "number" && Number.isFinite(value)) return value;
4701
4901
  if (typeof value === "string" && value.trim()) {
4702
4902
  const parsed = Number(value);
@@ -4710,8 +4910,8 @@ var roundNullable = (value, digits = 4) => {
4710
4910
  return Math.round(value * multiplier) / multiplier;
4711
4911
  };
4712
4912
  var deltaNullable = (targetValue, referenceValue) => {
4713
- const target = toFiniteNumberOrNull3(targetValue);
4714
- const reference = toFiniteNumberOrNull3(referenceValue);
4913
+ const target = toFiniteNumberOrNull4(targetValue);
4914
+ const reference = toFiniteNumberOrNull4(referenceValue);
4715
4915
  return target == null || reference == null ? null : roundNullable(target - reference);
4716
4916
  };
4717
4917
  var buildTargetDerivedContext = (params) => {
@@ -4873,7 +5073,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4873
5073
  throw error;
4874
5074
  }
4875
5075
  derivativesContextUnavailable = true;
4876
- import_logger5.logger.warn(
5076
+ import_logger6.logger.warn(
4877
5077
  "Derivatives context disabled after Timescale read failure: %s",
4878
5078
  String(error)
4879
5079
  );
@@ -4885,7 +5085,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4885
5085
  var import_data3 = require("@tradejs/core/data");
4886
5086
  var import_strategies4 = require("@tradejs/core/strategies");
4887
5087
  var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWhales");
4888
- var import_logger6 = require("@tradejs/infra/logger");
5088
+ var import_logger7 = require("@tradejs/infra/logger");
4889
5089
 
4890
5090
  // src/hyperliquidWhaleUniverse.ts
4891
5091
  var import_node_crypto3 = require("crypto");
@@ -5409,7 +5609,7 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
5409
5609
  throw error;
5410
5610
  }
5411
5611
  hyperliquidWhaleContextUnavailable = true;
5412
- import_logger6.logger.warn(
5612
+ import_logger7.logger.warn(
5413
5613
  "Hyperliquid whale context disabled after Timescale read failure: %s",
5414
5614
  String(error)
5415
5615
  );
@@ -5492,7 +5692,7 @@ var runMarketContextStage = async ({
5492
5692
  elapsedMs: Date.now() - startedAt
5493
5693
  };
5494
5694
  if (status === "timed_out") {
5495
- import_logger7.logger.warn(
5695
+ import_logger8.logger.warn(
5496
5696
  "Market context stage timed out: %s after %sms",
5497
5697
  stage,
5498
5698
  result.elapsedMs
@@ -5682,7 +5882,7 @@ var enrichSignalWithAi = async ({
5682
5882
  signal.aiAnalysis = analysis;
5683
5883
  return resolveAiQuality(analysis, direction);
5684
5884
  } catch (err) {
5685
- import_logger8.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
5885
+ import_logger9.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
5686
5886
  }
5687
5887
  return void 0;
5688
5888
  };
@@ -5699,75 +5899,6 @@ var enrichSignalWithMlAi = async ({
5699
5899
  await enrichSignalWithMl({ signal, env, ml });
5700
5900
  return enrichSignalWithAi({ signal, userName, symbol, direction, env, ai });
5701
5901
  };
5702
- var toFiniteNumberOrNull4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
5703
- var getOrderArrivalSnapshot = async ({
5704
- connector,
5705
- symbol
5706
- }) => {
5707
- if (typeof connector.getTopOfBookTicker !== "function") {
5708
- return {
5709
- arrivalSnapshotTime: Date.now(),
5710
- arrivalSource: "unavailable",
5711
- bid: null,
5712
- ask: null,
5713
- arrivalMid: null,
5714
- spreadBps: null
5715
- };
5716
- }
5717
- try {
5718
- const ticker = await connector.getTopOfBookTicker(symbol);
5719
- const arrivalSnapshotTime = toFiniteNumberOrNull4(ticker?.timestamp);
5720
- const bid = toFiniteNumberOrNull4(ticker?.bidPrice);
5721
- const ask = toFiniteNumberOrNull4(ticker?.askPrice);
5722
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
5723
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
5724
- return {
5725
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
5726
- arrivalSource: "top_of_book",
5727
- bid,
5728
- ask,
5729
- arrivalMid,
5730
- spreadBps
5731
- };
5732
- } catch (error) {
5733
- import_logger8.logger.warn(
5734
- "runtime order arrival snapshot failed: %s %s",
5735
- symbol,
5736
- error?.message || String(error)
5737
- );
5738
- return {
5739
- arrivalSnapshotTime: Date.now(),
5740
- arrivalSource: "top_of_book_error",
5741
- bid: null,
5742
- ask: null,
5743
- arrivalMid: null,
5744
- spreadBps: null
5745
- };
5746
- }
5747
- };
5748
- var validateEntryProtectionAtArrival = ({
5749
- direction,
5750
- signalPrice,
5751
- bid,
5752
- ask,
5753
- arrivalMid,
5754
- takeProfits,
5755
- stopLossPrice
5756
- }) => {
5757
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
5758
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
5759
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
5760
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
5761
- }
5762
- for (const takeProfit of takeProfits) {
5763
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
5764
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
5765
- }
5766
- }
5767
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
5768
- throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
5769
- }
5770
- };
5771
5902
  var resolveRuntimeTelemetryQuality = ({
5772
5903
  signalClosePrice,
5773
5904
  arrivalMid,
@@ -5844,7 +5975,8 @@ var executeEntryOrder = async ({
5844
5975
  signal,
5845
5976
  beforePlaceOrder,
5846
5977
  recordRuntimeTrade = true,
5847
- leverage
5978
+ leverage,
5979
+ executionLimits
5848
5980
  }) => {
5849
5981
  await beforePlaceOrder?.();
5850
5982
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
@@ -5855,23 +5987,57 @@ var executeEntryOrder = async ({
5855
5987
  signal.orderQty = qty;
5856
5988
  signal.orderValue = qty * currentPrice;
5857
5989
  signal.orderFailureReason = void 0;
5858
- const arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
5859
- try {
5860
- validateEntryProtectionAtArrival({
5861
- direction,
5862
- signalPrice: currentPrice,
5863
- bid: arrivalSnapshot.bid,
5864
- ask: arrivalSnapshot.ask,
5865
- arrivalMid: arrivalSnapshot.arrivalMid,
5866
- takeProfits,
5867
- stopLossPrice
5868
- });
5869
- } catch (error) {
5870
- signal.orderStatus = "failed";
5871
- signal.orderFailureReason = error.message;
5872
- throw error;
5990
+ let arrivalSnapshot = {
5991
+ arrivalSnapshotTime: Date.now(),
5992
+ arrivalSource: "unavailable",
5993
+ bid: null,
5994
+ ask: null,
5995
+ arrivalMid: null,
5996
+ spreadBps: null
5997
+ };
5998
+ let orderSubmitTime = Date.now();
5999
+ const beforeSubmit = createEntrySubmissionValidation({
6000
+ connector,
6001
+ symbol,
6002
+ direction,
6003
+ takeProfits,
6004
+ stopLossPrice,
6005
+ limits: executionLimits,
6006
+ onValidated: (snapshot2, economics2) => {
6007
+ arrivalSnapshot = snapshot2;
6008
+ orderSubmitTime = Date.now();
6009
+ if (executionLimits?.maxLossValue != null) {
6010
+ signal.riskDecision = {
6011
+ stage: "order_plan",
6012
+ status: "approved",
6013
+ reason: "PLANNED_LOSS_WITHIN_MAX_LOSS_VALUE",
6014
+ plannedLossValue: economics2.plannedLossValue,
6015
+ maxLossValue: executionLimits.maxLossValue,
6016
+ limitUtilization: economics2.plannedLossValue / executionLimits.maxLossValue,
6017
+ enforced: true
6018
+ };
6019
+ }
6020
+ }
6021
+ });
6022
+ if (!beforeSubmit) {
6023
+ arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
6024
+ try {
6025
+ validateEntryProtectionAtArrival({
6026
+ direction,
6027
+ signalPrice: currentPrice,
6028
+ bid: arrivalSnapshot.bid,
6029
+ ask: arrivalSnapshot.ask,
6030
+ arrivalMid: arrivalSnapshot.arrivalMid,
6031
+ takeProfits,
6032
+ stopLossPrice
6033
+ });
6034
+ } catch (error) {
6035
+ signal.orderStatus = "failed";
6036
+ signal.orderFailureReason = error.message;
6037
+ throw error;
6038
+ }
6039
+ orderSubmitTime = Date.now();
5873
6040
  }
5874
- const orderSubmitTime = Date.now();
5875
6041
  const orderPlaced = await connector.placeOrder({
5876
6042
  symbol,
5877
6043
  qty,
@@ -5882,7 +6048,8 @@ var executeEntryOrder = async ({
5882
6048
  direction,
5883
6049
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
5884
6050
  orderId,
5885
- signal
6051
+ signal,
6052
+ ...beforeSubmit ? { beforeSubmit } : {}
5886
6053
  });
5887
6054
  const orderAckTime = Date.now();
5888
6055
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -6711,7 +6878,7 @@ var canUseSharedReplayState = ({
6711
6878
  }) => (env === "BACKTEST" || env === "PARITY") && Boolean(sharedReplayKey);
6712
6879
 
6713
6880
  // src/strategy/runtimeExecution.ts
6714
- var import_logger9 = require("@tradejs/infra/logger");
6881
+ var import_logger10 = require("@tradejs/infra/logger");
6715
6882
  var import_types = require("@tradejs/types");
6716
6883
  var buildExitOrderSignal = ({
6717
6884
  strategyName,
@@ -6763,7 +6930,7 @@ var handleExitDecision = async ({
6763
6930
  deploymentId: connector.deploymentId
6764
6931
  });
6765
6932
  if (!activeTrade) {
6766
- import_logger9.logger.warn(
6933
+ import_logger10.logger.warn(
6767
6934
  "[%s] blocked closePosition for untracked runtime position: %s",
6768
6935
  strategyName ?? "unknown",
6769
6936
  symbol
@@ -6771,7 +6938,7 @@ var handleExitDecision = async ({
6771
6938
  return "CLOSE_BLOCKED_BY_UNTRACKED_POSITION";
6772
6939
  }
6773
6940
  if (!strategyName || activeTrade.strategy !== strategyName) {
6774
- import_logger9.logger.warn(
6941
+ import_logger10.logger.warn(
6775
6942
  "[%s] blocked closePosition for foreign runtime position: %s ownedBy=%s",
6776
6943
  strategyName ?? "unknown",
6777
6944
  symbol,
@@ -6824,7 +6991,7 @@ var handleExitDecision = async ({
6824
6991
  exitType: closedTrade?.exitType ?? "exit"
6825
6992
  });
6826
6993
  } catch (notificationError) {
6827
- import_logger9.logger.error(
6994
+ import_logger10.logger.error(
6828
6995
  "runtime close notification error: %s %s",
6829
6996
  symbol,
6830
6997
  notificationError
@@ -6838,7 +7005,7 @@ var handleExitDecision = async ({
6838
7005
  decision,
6839
7006
  market
6840
7007
  });
6841
- import_logger9.logger.error("close order error: %s %s", symbol, err);
7008
+ import_logger10.logger.error("close order error: %s %s", symbol, err);
6842
7009
  return "ORDER_ERROR";
6843
7010
  }
6844
7011
  return decision.code;
@@ -6865,7 +7032,7 @@ var handleProtectDecision = async ({
6865
7032
  decision,
6866
7033
  market
6867
7034
  });
6868
- import_logger9.logger.error("protect position error: %s %s", symbol, err);
7035
+ import_logger10.logger.error("protect position error: %s %s", symbol, err);
6869
7036
  return "ORDER_ERROR";
6870
7037
  }
6871
7038
  return decision.code;
@@ -6931,7 +7098,11 @@ var executeEntryDecision = async ({
6931
7098
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
6932
7099
  signal,
6933
7100
  beforePlaceOrder,
6934
- recordRuntimeTrade: recordRuntimeJournal
7101
+ recordRuntimeTrade: recordRuntimeJournal,
7102
+ executionLimits: resolveEntryExecutionLimits(
7103
+ hookCtx.strategyConfig,
7104
+ decision.entryContext.direction
7105
+ )
6935
7106
  });
6936
7107
  await invokeStageHooks(
6937
7108
  "afterPlaceOrder",
@@ -6953,19 +7124,32 @@ var executeEntryDecision = async ({
6953
7124
  return signal;
6954
7125
  }
6955
7126
  await beforePlaceOrder();
6956
- const arrivalSnapshot = await getOrderArrivalSnapshot({
7127
+ const beforeSubmit = createEntrySubmissionValidation({
6957
7128
  connector,
6958
- symbol
6959
- });
6960
- validateEntryProtectionAtArrival({
7129
+ symbol,
6961
7130
  direction: decision.entryContext.direction,
6962
- signalPrice: decision.entryContext.prices.currentPrice,
6963
- bid: arrivalSnapshot.bid,
6964
- ask: arrivalSnapshot.ask,
6965
- arrivalMid: arrivalSnapshot.arrivalMid,
6966
7131
  takeProfits: decision.orderPlan.takeProfits,
6967
- stopLossPrice: decision.orderPlan.stopLossPrice
7132
+ stopLossPrice: decision.orderPlan.stopLossPrice,
7133
+ limits: resolveEntryExecutionLimits(
7134
+ hookCtx.strategyConfig,
7135
+ decision.entryContext.direction
7136
+ )
6968
7137
  });
7138
+ if (!beforeSubmit) {
7139
+ const arrivalSnapshot = await getOrderArrivalSnapshot({
7140
+ connector,
7141
+ symbol
7142
+ });
7143
+ validateEntryProtectionAtArrival({
7144
+ direction: decision.entryContext.direction,
7145
+ signalPrice: decision.entryContext.prices.currentPrice,
7146
+ bid: arrivalSnapshot.bid,
7147
+ ask: arrivalSnapshot.ask,
7148
+ arrivalMid: arrivalSnapshot.arrivalMid,
7149
+ takeProfits: decision.orderPlan.takeProfits,
7150
+ stopLossPrice: decision.orderPlan.stopLossPrice
7151
+ });
7152
+ }
6969
7153
  const orderPlaced = await connector.placeOrder({
6970
7154
  symbol,
6971
7155
  qty: decision.orderPlan.qty,
@@ -6973,6 +7157,7 @@ var executeEntryDecision = async ({
6973
7157
  timestamp: decision.entryContext.timestamp,
6974
7158
  direction: decision.entryContext.direction,
6975
7159
  positionIntent: decision.orderPlan.positionIntent,
7160
+ ...beforeSubmit ? { beforeSubmit } : {},
6976
7161
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {}
6977
7162
  });
6978
7163
  if (!orderPlaced) {
@@ -7014,6 +7199,23 @@ var executeEntryDecision = async ({
7014
7199
  { decision, entry, market }
7015
7200
  );
7016
7201
  } catch (err) {
7202
+ if (err instanceof EntrySubmissionValidationError) {
7203
+ if (signal) {
7204
+ signal.orderStatus = "skipped";
7205
+ signal.orderSkipReason = err.message;
7206
+ signal.allocatorDecision = {
7207
+ stage: "runtime_admission",
7208
+ status: "rejected",
7209
+ reason: err.message
7210
+ };
7211
+ }
7212
+ import_logger10.logger.warn(
7213
+ "entry rejected before submission: %s %s",
7214
+ symbol,
7215
+ err.message
7216
+ );
7217
+ return signal ?? err.message;
7218
+ }
7017
7219
  if (signal) {
7018
7220
  signal.orderStatus = "failed";
7019
7221
  if (typeof signal.orderFailureReason !== "string" || !signal.orderFailureReason.trim()) {
@@ -7028,9 +7230,9 @@ var executeEntryDecision = async ({
7028
7230
  market
7029
7231
  });
7030
7232
  if (err?.message === import_types.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY) {
7031
- import_logger9.logger.warn("order warning: %s %s", symbol, err);
7233
+ import_logger10.logger.warn("order warning: %s %s", symbol, err);
7032
7234
  } else {
7033
- import_logger9.logger.error("order error: %s %s", symbol, err);
7235
+ import_logger10.logger.error("order error: %s %s", symbol, err);
7034
7236
  }
7035
7237
  return signal ?? "ORDER_ERROR";
7036
7238
  }
@@ -7195,7 +7397,7 @@ var createStrategyRuntime = ({
7195
7397
  try {
7196
7398
  await projectHook(errorParams);
7197
7399
  } catch (hookError) {
7198
- import_logger10.logger.error(
7400
+ import_logger11.logger.error(
7199
7401
  "project hook onRuntimeError failed: %s %s",
7200
7402
  strategyName,
7201
7403
  hookError
@@ -7209,7 +7411,7 @@ var createStrategyRuntime = ({
7209
7411
  try {
7210
7412
  await onRuntimeError(errorParams);
7211
7413
  } catch (hookError) {
7212
- import_logger10.logger.error(
7414
+ import_logger11.logger.error(
7213
7415
  "runtime hook onRuntimeError failed: %s %s",
7214
7416
  strategyName,
7215
7417
  hookError
@@ -7223,7 +7425,7 @@ var createStrategyRuntime = ({
7223
7425
  try {
7224
7426
  return await hook(params);
7225
7427
  } catch (error) {
7226
- import_logger10.logger.error(
7428
+ import_logger11.logger.error(
7227
7429
  'strategy hook "%s" failed for %s: %s',
7228
7430
  stage,
7229
7431
  strategyName,
@@ -7422,8 +7624,11 @@ var createStrategyRuntime = ({
7422
7624
  return enrichedBaseContext;
7423
7625
  }
7424
7626
  });
7627
+ const coreConfig = { ...config };
7628
+ delete coreConfig.RECORD_RUNTIME_TRADES;
7629
+ delete coreConfig.SIMULATE_ORDERS;
7425
7630
  const core = await createCore({
7426
- config,
7631
+ config: coreConfig,
7427
7632
  data,
7428
7633
  strategyApi,
7429
7634
  indicatorsState
@@ -8043,7 +8248,7 @@ setStrategyRuntimeFactory(createStrategyRuntime);
8043
8248
 
8044
8249
  // src/strategyHooks/closeOppositePositionsBeforeOpen.ts
8045
8250
  var import_lodash2 = __toESM(require("lodash"));
8046
- var import_logger11 = require("@tradejs/infra/logger");
8251
+ var import_logger12 = require("@tradejs/infra/logger");
8047
8252
  var closeOppositePositionsBeforeOpen = async ({
8048
8253
  connector,
8049
8254
  entryContext
@@ -8057,7 +8262,7 @@ var closeOppositePositionsBeforeOpen = async ({
8057
8262
  } = entryContext;
8058
8263
  const price = prices.currentPrice;
8059
8264
  try {
8060
- import_logger11.logger.log(
8265
+ import_logger12.logger.log(
8061
8266
  "info",
8062
8267
  "[%s] checking open positions before open: %s %s",
8063
8268
  strategyName,
@@ -8068,7 +8273,7 @@ var closeOppositePositionsBeforeOpen = async ({
8068
8273
  const openPositions = (positions || []).filter(
8069
8274
  (item) => item && Number(item.qty) > 0
8070
8275
  );
8071
- import_logger11.logger.log(
8276
+ import_logger12.logger.log(
8072
8277
  "info",
8073
8278
  "[%s] open positions found: %s",
8074
8279
  strategyName,
@@ -8078,7 +8283,7 @@ var closeOppositePositionsBeforeOpen = async ({
8078
8283
  (item) => item.symbol !== currentSymbol && item.direction !== currentDirection
8079
8284
  );
8080
8285
  if (import_lodash2.default.isEmpty(oppositePositions)) {
8081
- import_logger11.logger.log(
8286
+ import_logger12.logger.log(
8082
8287
  "info",
8083
8288
  "[%s] no opposite positions to close before open: %s",
8084
8289
  strategyName,
@@ -8087,7 +8292,7 @@ var closeOppositePositionsBeforeOpen = async ({
8087
8292
  return;
8088
8293
  }
8089
8294
  for (const position of oppositePositions) {
8090
- import_logger11.logger.log(
8295
+ import_logger12.logger.log(
8091
8296
  "info",
8092
8297
  "[%s] closing opposite position: %s %s qty=%s",
8093
8298
  strategyName,
@@ -8102,14 +8307,14 @@ var closeOppositePositionsBeforeOpen = async ({
8102
8307
  timestamp,
8103
8308
  direction: position.direction
8104
8309
  });
8105
- import_logger11.logger.log(
8310
+ import_logger12.logger.log(
8106
8311
  "info",
8107
8312
  "[%s] opposite position closed: %s",
8108
8313
  strategyName,
8109
8314
  position.symbol
8110
8315
  );
8111
8316
  } catch (err) {
8112
- import_logger11.logger.log(
8317
+ import_logger12.logger.log(
8113
8318
  "error",
8114
8319
  "[%s] failed to close opposite position: %s %s",
8115
8320
  strategyName,
@@ -8119,7 +8324,7 @@ var closeOppositePositionsBeforeOpen = async ({
8119
8324
  }
8120
8325
  }
8121
8326
  } catch (err) {
8122
- import_logger11.logger.log(
8327
+ import_logger12.logger.log(
8123
8328
  "error",
8124
8329
  "[%s] failed to load open positions before open: %s %s",
8125
8330
  strategyName,
@@ -8331,7 +8536,7 @@ var createMoveStopToBreakEvenOnBarHook = ({
8331
8536
  var createMoveStopToBreakEvenAfterCoreDecisionHook = createMoveStopToBreakEvenOnBarHook;
8332
8537
 
8333
8538
  // src/signalsHooks/closeAllPositionsOnGlobalProfitBeforeSignals.ts
8334
- var import_logger12 = require("@tradejs/infra/logger");
8539
+ var import_logger13 = require("@tradejs/infra/logger");
8335
8540
  var createCloseAllOnGlobalProfitBeforeSignalsHook = ({
8336
8541
  getStrategyDefaultConfig = () => void 0,
8337
8542
  profitRiskMultiplier = DEFAULT_GLOBAL_UNREALIZED_PNL_TRIGGER_RISK_MULTIPLIER
@@ -8370,7 +8575,7 @@ var createCloseAllOnGlobalProfitBeforeSignalsHook = ({
8370
8575
  if (!Number.isFinite(unrealizedPnlThreshold) || unrealizedPnlThreshold <= 0 || totalUnrealizedPnl < unrealizedPnlThreshold) {
8371
8576
  return;
8372
8577
  }
8373
- import_logger12.logger.info(
8578
+ import_logger13.logger.info(
8374
8579
  "closing all positions before signals by global unrealized pnl threshold: totalPnl=%s threshold=%s positions=%s",
8375
8580
  totalUnrealizedPnl,
8376
8581
  unrealizedPnlThreshold,
@@ -8396,7 +8601,7 @@ var createCloseAllOnGlobalProfitBeforeSignalsHook = ({
8396
8601
  ];
8397
8602
  });
8398
8603
  if (failedClosures.length) {
8399
- import_logger12.logger.warn(
8604
+ import_logger13.logger.warn(
8400
8605
  "close-all before signals hook could not confirm closures for %s",
8401
8606
  failedClosures.join(", ")
8402
8607
  );