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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2909,13 +2909,210 @@ 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
+ const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
2980
+ return { netRiskRatio, plannedLossValue, riskExceeded };
2981
+ };
2982
+
2983
+ // src/strategy/entrySubmissionValidation.ts
2984
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
2985
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
2986
+ var getOrderArrivalSnapshot = async ({
2987
+ connector,
2988
+ symbol,
2989
+ requireTimestamp = false
2990
+ }) => {
2991
+ if (typeof connector.getTopOfBookTicker !== "function") {
2992
+ return {
2993
+ arrivalSnapshotTime: Date.now(),
2994
+ arrivalSource: "unavailable",
2995
+ bid: null,
2996
+ ask: null,
2997
+ arrivalMid: null,
2998
+ spreadBps: null
2999
+ };
3000
+ }
3001
+ try {
3002
+ const ticker = await connector.getTopOfBookTicker(symbol);
3003
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
3004
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
3005
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
3006
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
3007
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
3008
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
3009
+ return {
3010
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
3011
+ arrivalSource: "top_of_book",
3012
+ bid,
3013
+ ask,
3014
+ arrivalMid,
3015
+ spreadBps
3016
+ };
3017
+ } catch (error) {
3018
+ import_logger2.logger.warn(
3019
+ "runtime order arrival snapshot failed: %s %s",
3020
+ symbol,
3021
+ error?.message || String(error)
3022
+ );
3023
+ return {
3024
+ arrivalSnapshotTime: Date.now(),
3025
+ arrivalSource: "top_of_book_error",
3026
+ bid: null,
3027
+ ask: null,
3028
+ arrivalMid: null,
3029
+ spreadBps: null
3030
+ };
3031
+ }
3032
+ };
3033
+ var validateEntryProtectionAtArrival = ({
3034
+ direction,
3035
+ signalPrice,
3036
+ bid,
3037
+ ask,
3038
+ arrivalMid,
3039
+ takeProfits,
3040
+ stopLossPrice
3041
+ }) => {
3042
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
3043
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
3044
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
3045
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
3046
+ }
3047
+ for (const takeProfit of takeProfits) {
3048
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
3049
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
3050
+ }
3051
+ }
3052
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
3053
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
3054
+ }
3055
+ };
3056
+ var createEntrySubmissionValidation = ({
3057
+ connector,
3058
+ symbol,
3059
+ direction,
3060
+ takeProfits,
3061
+ stopLossPrice,
3062
+ limits,
3063
+ onValidated
3064
+ }) => {
3065
+ if (!limits) return void 0;
3066
+ if (!connector.capabilities?.orderSubmissionValidation)
3067
+ throw new EntrySubmissionValidationError(
3068
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
3069
+ );
3070
+ return async ({ qty, executionPrice }) => {
3071
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
3072
+ const snapshot2 = simulated && executionPrice != null ? {
3073
+ arrivalSnapshotTime: Date.now(),
3074
+ arrivalSource: "simulation",
3075
+ bid: executionPrice,
3076
+ ask: executionPrice,
3077
+ arrivalMid: executionPrice,
3078
+ spreadBps: 0
3079
+ } : await getOrderArrivalSnapshot({
3080
+ connector,
3081
+ symbol,
3082
+ requireTimestamp: true
3083
+ });
3084
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
3085
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
3086
+ throw new EntrySubmissionValidationError(
3087
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
3088
+ );
3089
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
3090
+ throw new EntrySubmissionValidationError(
3091
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
3092
+ );
3093
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
3094
+ validateEntryProtectionAtArrival({
3095
+ direction,
3096
+ signalPrice: price,
3097
+ bid: snapshot2.bid,
3098
+ ask: snapshot2.ask,
3099
+ arrivalMid: snapshot2.arrivalMid,
3100
+ takeProfits,
3101
+ stopLossPrice
3102
+ });
3103
+ const prices = takeProfits.map((tp) => tp.price);
3104
+ const economics2 = validateEntryExecution({
3105
+ direction,
3106
+ price,
3107
+ qty,
3108
+ stopLossPrice,
3109
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
3110
+ limits
3111
+ });
3112
+ onValidated?.(snapshot2, economics2);
3113
+ };
3114
+ };
3115
+
2919
3116
  // src/strategyAdapters/ml.ts
2920
3117
  var defaultMlAdapter = {
2921
3118
  normalizeStrategyConfig: (strategyConfig) => strategyConfig
@@ -2969,7 +3166,7 @@ var import_node_crypto = require("crypto");
2969
3166
  var import_constants = require("@tradejs/core/constants");
2970
3167
  var import_time = require("@tradejs/core/time");
2971
3168
  var import_trade = require("@tradejs/core/trade");
2972
- var import_logger2 = require("@tradejs/infra/logger");
3169
+ var import_logger3 = require("@tradejs/infra/logger");
2973
3170
  var import_redis2 = require("@tradejs/infra/redis");
2974
3171
  var now = () => Date.now();
2975
3172
  var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
@@ -3028,7 +3225,7 @@ var recordRuntimeTradeOpen = async (params) => {
3028
3225
  )
3029
3226
  ]);
3030
3227
  } catch (error) {
3031
- import_logger2.logger.error(
3228
+ import_logger3.logger.error(
3032
3229
  "runtime trade open journal failed: %s %s",
3033
3230
  record.symbol,
3034
3231
  error?.message || String(error)
@@ -3096,7 +3293,7 @@ var recordRuntimeTradeIncrease = async (params) => {
3096
3293
  )
3097
3294
  ]);
3098
3295
  } catch (error) {
3099
- import_logger2.logger.error(
3296
+ import_logger3.logger.error(
3100
3297
  "runtime trade increase journal failed: %s %s",
3101
3298
  symbol,
3102
3299
  error?.message || String(error)
@@ -3203,7 +3400,7 @@ var markRuntimeTradeClosed = async (params) => {
3203
3400
  )
3204
3401
  ]);
3205
3402
  } catch (error) {
3206
- import_logger2.logger.error(
3403
+ import_logger3.logger.error(
3207
3404
  "runtime trade close journal failed: %s %s",
3208
3405
  symbol,
3209
3406
  error?.message || String(error)
@@ -3213,11 +3410,11 @@ var markRuntimeTradeClosed = async (params) => {
3213
3410
  };
3214
3411
 
3215
3412
  // src/strategyHelpers/marketContextStages.ts
3216
- var import_logger7 = require("@tradejs/infra/logger");
3413
+ var import_logger8 = require("@tradejs/infra/logger");
3217
3414
 
3218
3415
  // src/strategyHelpers/binanceMarketContext.ts
3219
3416
  var import_marketContext = require("@tradejs/infra/timescale/marketContext");
3220
- var import_logger3 = require("@tradejs/infra/logger");
3417
+ var import_logger4 = require("@tradejs/infra/logger");
3221
3418
  var import_strategies = require("@tradejs/core/strategies");
3222
3419
 
3223
3420
  // src/binanceBreadthUniverses.ts
@@ -3593,7 +3790,7 @@ var parseEnabledFlag = (value, env) => {
3593
3790
  if (normalized === "live") return env !== "BACKTEST";
3594
3791
  return false;
3595
3792
  };
3596
- var toFiniteNumberOrNull = (value) => {
3793
+ var toFiniteNumberOrNull2 = (value) => {
3597
3794
  const numeric = typeof value === "number" ? value : Number(value);
3598
3795
  return Number.isFinite(numeric) ? numeric : null;
3599
3796
  };
@@ -3858,14 +4055,14 @@ var toTradeFlowContext = (row, interval) => row ? {
3858
4055
  asOfTs: row.ts.getTime(),
3859
4056
  ageMs: row.ageMs,
3860
4057
  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)
4058
+ trades: toFiniteNumberOrNull2(row.trades),
4059
+ buyPressurePct: toFiniteNumberOrNull2(row.buyPressurePct),
4060
+ buyBaseVolume: toFiniteNumberOrNull2(row.buyBaseVolume),
4061
+ sellBaseVolume: toFiniteNumberOrNull2(row.sellBaseVolume),
4062
+ buyQuoteVolume: toFiniteNumberOrNull2(row.buyQuoteVolume),
4063
+ sellQuoteVolume: toFiniteNumberOrNull2(row.sellQuoteVolume),
4064
+ netBaseDelta: toFiniteNumberOrNull2(row.netBaseDelta),
4065
+ netQuoteDelta: toFiniteNumberOrNull2(row.netQuoteDelta)
3869
4066
  } : null;
3870
4067
  var getCachedReferenceRows = ({
3871
4068
  referenceSymbols,
@@ -3954,16 +4151,16 @@ var toMarketBreadthContext = (breadth, interval) => ({
3954
4151
  asOfTs: breadth.ts.getTime(),
3955
4152
  ageMs: breadth.ageMs,
3956
4153
  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)
4154
+ symbolsCount: toFiniteNumberOrNull2(breadth.symbolsCount),
4155
+ advancers: toFiniteNumberOrNull2(breadth.advancers),
4156
+ decliners: toFiniteNumberOrNull2(breadth.decliners),
4157
+ unchanged: toFiniteNumberOrNull2(breadth.unchanged),
4158
+ advanceDeclineRatio: toFiniteNumberOrNull2(breadth.advanceDeclineRatio),
4159
+ pctAboveMa20: toFiniteNumberOrNull2(breadth.pctAboveMa20),
4160
+ pctAboveMa50: toFiniteNumberOrNull2(breadth.pctAboveMa50),
4161
+ equalWeightedReturn: toFiniteNumberOrNull2(breadth.equalWeightedReturn),
4162
+ volumeWeightedReturn: toFiniteNumberOrNull2(breadth.volumeWeightedReturn),
4163
+ dispersion: toFiniteNumberOrNull2(breadth.dispersion)
3967
4164
  });
3968
4165
  var enrichSignalWithBinanceMarketContext = async (params) => {
3969
4166
  const {
@@ -4064,46 +4261,46 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
4064
4261
  asOfTs: primaryBreadth.ts.getTime(),
4065
4262
  ageMs: primaryBreadth.ageMs,
4066
4263
  stale: primaryBreadth.stale,
4067
- btcReturn1h: toFiniteNumberOrNull(
4264
+ btcReturn1h: toFiniteNumberOrNull2(
4068
4265
  primaryBreadth.btcReturn1h
4069
4266
  ),
4070
- btcReturn4h: toFiniteNumberOrNull(
4267
+ btcReturn4h: toFiniteNumberOrNull2(
4071
4268
  primaryBreadth.btcReturn4h
4072
4269
  ),
4073
- btcReturn24h: toFiniteNumberOrNull(
4270
+ btcReturn24h: toFiniteNumberOrNull2(
4074
4271
  primaryBreadth.btcReturn24h
4075
4272
  ),
4076
- altBasketReturn1h: toFiniteNumberOrNull(
4273
+ altBasketReturn1h: toFiniteNumberOrNull2(
4077
4274
  primaryBreadth.altBasketReturn1h
4078
4275
  ),
4079
- altBasketReturn4h: toFiniteNumberOrNull(
4276
+ altBasketReturn4h: toFiniteNumberOrNull2(
4080
4277
  primaryBreadth.altBasketReturn4h
4081
4278
  ),
4082
- altBasketReturn24h: toFiniteNumberOrNull(
4279
+ altBasketReturn24h: toFiniteNumberOrNull2(
4083
4280
  primaryBreadth.altBasketReturn24h
4084
4281
  ),
4085
- btcVsAltReturn1h: toFiniteNumberOrNull(
4282
+ btcVsAltReturn1h: toFiniteNumberOrNull2(
4086
4283
  primaryBreadth.btcVsAltReturn1h
4087
4284
  ),
4088
- btcVsAltReturn4h: toFiniteNumberOrNull(
4285
+ btcVsAltReturn4h: toFiniteNumberOrNull2(
4089
4286
  primaryBreadth.btcVsAltReturn4h
4090
4287
  ),
4091
- btcVsAltReturn24h: toFiniteNumberOrNull(
4288
+ btcVsAltReturn24h: toFiniteNumberOrNull2(
4092
4289
  primaryBreadth.btcVsAltReturn24h
4093
4290
  ),
4094
- btcTurnoverShare1h: toFiniteNumberOrNull(
4291
+ btcTurnoverShare1h: toFiniteNumberOrNull2(
4095
4292
  primaryBreadth.btcTurnoverShare1h
4096
4293
  ),
4097
- btcTurnoverShare24h: toFiniteNumberOrNull(
4294
+ btcTurnoverShare24h: toFiniteNumberOrNull2(
4098
4295
  primaryBreadth.btcTurnoverShare24h
4099
4296
  ),
4100
- btcTurnoverShareChange24h: toFiniteNumberOrNull(
4297
+ btcTurnoverShareChange24h: toFiniteNumberOrNull2(
4101
4298
  primaryBreadth.btcTurnoverShareChange24h
4102
4299
  ),
4103
- altVolToBtcVol24h: toFiniteNumberOrNull(
4300
+ altVolToBtcVol24h: toFiniteNumberOrNull2(
4104
4301
  primaryBreadth.altVolToBtcVol24h
4105
4302
  ),
4106
- altDispersion24h: toFiniteNumberOrNull(
4303
+ altDispersion24h: toFiniteNumberOrNull2(
4107
4304
  primaryBreadth.altDispersion24h
4108
4305
  ),
4109
4306
  regime: primaryBreadth.btcAltRegime ?? "unknown"
@@ -4120,7 +4317,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
4120
4317
  throw error;
4121
4318
  }
4122
4319
  binanceMarketContextUnavailable = true;
4123
- import_logger3.logger.warn(
4320
+ import_logger4.logger.warn(
4124
4321
  "Binance market context disabled after Timescale read failure: %s",
4125
4322
  String(error)
4126
4323
  );
@@ -4130,7 +4327,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
4130
4327
 
4131
4328
  // src/strategyHelpers/coinMarketCapContext.ts
4132
4329
  var import_strategies2 = require("@tradejs/core/strategies");
4133
- var import_logger4 = require("@tradejs/infra/logger");
4330
+ var import_logger5 = require("@tradejs/infra/logger");
4134
4331
  var import_marketContext2 = require("@tradejs/infra/timescale/marketContext");
4135
4332
  var DEFAULT_MAX_AGE_MS = 48 * 60 * 6e4;
4136
4333
  var SOURCE_GLOBAL_DAILY = "coinmarketcap_global";
@@ -4168,7 +4365,7 @@ var asInt = (value, fallback) => {
4168
4365
  const parsed = Number.parseInt(String(value ?? ""), 10);
4169
4366
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
4170
4367
  };
4171
- var toFiniteNumberOrNull2 = (value) => {
4368
+ var toFiniteNumberOrNull3 = (value) => {
4172
4369
  const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
4173
4370
  return Number.isFinite(numeric) ? numeric : null;
4174
4371
  };
@@ -4402,12 +4599,12 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4402
4599
  const ethRow = references.get("ETHUSDT") ?? null;
4403
4600
  const previousBtcRow = previousReferences.get("BTCUSDT") ?? null;
4404
4601
  const previousEthRow = previousReferences.get("ETHUSDT") ?? null;
4405
- const btcMarketCapUsd = toFiniteNumberOrNull2(btcRow?.marketCapUsd);
4406
- const ethMarketCapUsd = toFiniteNumberOrNull2(ethRow?.marketCapUsd);
4407
- const previousBtcMarketCapUsd = toFiniteNumberOrNull2(
4602
+ const btcMarketCapUsd = toFiniteNumberOrNull3(btcRow?.marketCapUsd);
4603
+ const ethMarketCapUsd = toFiniteNumberOrNull3(ethRow?.marketCapUsd);
4604
+ const previousBtcMarketCapUsd = toFiniteNumberOrNull3(
4408
4605
  previousBtcRow?.marketCapUsd
4409
4606
  );
4410
- const previousEthMarketCapUsd = toFiniteNumberOrNull2(
4607
+ const previousEthMarketCapUsd = toFiniteNumberOrNull3(
4411
4608
  previousEthRow?.marketCapUsd
4412
4609
  );
4413
4610
  const ethBtcMarketCapRatio = safeDivide(ethMarketCapUsd, btcMarketCapUsd);
@@ -4416,16 +4613,16 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4416
4613
  previousBtcMarketCapUsd
4417
4614
  );
4418
4615
  const ethBtcMarketCapRatioChange24hPct = ethBtcMarketCapRatio != null && previousEthBtcMarketCapRatio != null && previousEthBtcMarketCapRatio > 0 ? (ethBtcMarketCapRatio - previousEthBtcMarketCapRatio) / previousEthBtcMarketCapRatio : null;
4419
- const btcVolumeUsd = toFiniteNumberOrNull2(btcRow?.volumeUsd);
4420
- const ethVolumeUsd = toFiniteNumberOrNull2(ethRow?.volumeUsd);
4616
+ const btcVolumeUsd = toFiniteNumberOrNull3(btcRow?.volumeUsd);
4617
+ const ethVolumeUsd = toFiniteNumberOrNull3(ethRow?.volumeUsd);
4421
4618
  const referenceStale = btcRow?.stale === true || ethRow?.stale === true || !btcRow || !ethRow;
4422
- const btcDominanceChange24hPct = toFiniteNumberOrNull2(
4619
+ const btcDominanceChange24hPct = toFiniteNumberOrNull3(
4423
4620
  globalRow?.btcDominanceChange24hPct
4424
4621
  );
4425
- const altMarketCapChange24hPct = toFiniteNumberOrNull2(
4622
+ const altMarketCapChange24hPct = toFiniteNumberOrNull3(
4426
4623
  globalRow?.altMarketCapChange24hPct
4427
4624
  );
4428
- const altVolumeChange24hPct = toFiniteNumberOrNull2(
4625
+ const altVolumeChange24hPct = toFiniteNumberOrNull3(
4429
4626
  globalRow?.altVolumeChange24hPct
4430
4627
  );
4431
4628
  const altLiquidityRegime = globalRow ? toAltLiquidityRegime({
@@ -4436,19 +4633,19 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4436
4633
  }) : "unknown";
4437
4634
  const exchangeLiquidityRegime = exchangeLiquidityRow ? toExchangeLiquidityRegime({
4438
4635
  stale: exchangeLiquidityRow.stale,
4439
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
4636
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
4440
4637
  exchangeLiquidityRow.totalVolumeChange24hPct
4441
4638
  ),
4442
4639
  fallback: exchangeLiquidityRow.liquidityRegime ?? "unknown"
4443
4640
  }) : "unknown";
4444
4641
  const cmc100Row = indexRows.get("cmc100") ?? null;
4445
4642
  const cmc20Row = indexRows.get("cmc20") ?? null;
4446
- const cmc100Value = toFiniteNumberOrNull2(cmc100Row?.value);
4447
- const cmc20Value = toFiniteNumberOrNull2(cmc20Row?.value);
4448
- const cmc100Change24hPct = toFiniteNumberOrNull2(
4643
+ const cmc100Value = toFiniteNumberOrNull3(cmc100Row?.value);
4644
+ const cmc20Value = toFiniteNumberOrNull3(cmc20Row?.value);
4645
+ const cmc100Change24hPct = toFiniteNumberOrNull3(
4449
4646
  cmc100Row?.valueChange24hPct
4450
4647
  );
4451
- const cmc20Change24hPct = toFiniteNumberOrNull2(cmc20Row?.valueChange24hPct);
4648
+ const cmc20Change24hPct = toFiniteNumberOrNull3(cmc20Row?.valueChange24hPct);
4452
4649
  const cmc20ToCmc100Ratio = safeDivide(cmc20Value, cmc100Value);
4453
4650
  const cmc20ToCmc100RatioChange24hPct = cmc20Change24hPct != null && cmc100Change24hPct != null ? (1 + cmc20Change24hPct) / (1 + cmc100Change24hPct) - 1 : null;
4454
4651
  const indexStale = cmc100Row?.stale === true || cmc20Row?.stale === true || !cmc100Row || !cmc20Row;
@@ -4472,41 +4669,41 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4472
4669
  asOfTs: globalRow.ts.getTime(),
4473
4670
  ageMs: globalRow.ageMs,
4474
4671
  stale: globalRow.stale,
4475
- totalMarketCapUsd: toFiniteNumberOrNull2(
4672
+ totalMarketCapUsd: toFiniteNumberOrNull3(
4476
4673
  globalRow.totalMarketCapUsd
4477
4674
  ),
4478
- totalVolumeUsd: toFiniteNumberOrNull2(
4675
+ totalVolumeUsd: toFiniteNumberOrNull3(
4479
4676
  globalRow.totalVolumeUsd
4480
4677
  ),
4481
- totalVolumeReportedUsd: toFiniteNumberOrNull2(
4678
+ totalVolumeReportedUsd: toFiniteNumberOrNull3(
4482
4679
  globalRow.totalVolumeReportedUsd
4483
4680
  ),
4484
- altMarketCapUsd: toFiniteNumberOrNull2(
4681
+ altMarketCapUsd: toFiniteNumberOrNull3(
4485
4682
  globalRow.altMarketCapUsd
4486
4683
  ),
4487
- altVolumeUsd: toFiniteNumberOrNull2(globalRow.altVolumeUsd),
4488
- altVolumeReportedUsd: toFiniteNumberOrNull2(
4684
+ altVolumeUsd: toFiniteNumberOrNull3(globalRow.altVolumeUsd),
4685
+ altVolumeReportedUsd: toFiniteNumberOrNull3(
4489
4686
  globalRow.altVolumeReportedUsd
4490
4687
  ),
4491
- btcDominancePct: toFiniteNumberOrNull2(
4688
+ btcDominancePct: toFiniteNumberOrNull3(
4492
4689
  globalRow.btcDominancePct
4493
4690
  ),
4494
- ethDominancePct: toFiniteNumberOrNull2(
4691
+ ethDominancePct: toFiniteNumberOrNull3(
4495
4692
  globalRow.ethDominancePct
4496
4693
  ),
4497
4694
  btcDominanceChange24hPct,
4498
- ethDominanceChange24hPct: toFiniteNumberOrNull2(
4695
+ ethDominanceChange24hPct: toFiniteNumberOrNull3(
4499
4696
  globalRow.ethDominanceChange24hPct
4500
4697
  ),
4501
4698
  altMarketCapChange24hPct,
4502
4699
  altVolumeChange24hPct,
4503
- activeCryptocurrencies: toFiniteNumberOrNull2(
4700
+ activeCryptocurrencies: toFiniteNumberOrNull3(
4504
4701
  globalRow.activeCryptocurrencies
4505
4702
  ),
4506
- activeExchanges: toFiniteNumberOrNull2(
4703
+ activeExchanges: toFiniteNumberOrNull3(
4507
4704
  globalRow.activeExchanges
4508
4705
  ),
4509
- activeMarketPairs: toFiniteNumberOrNull2(
4706
+ activeMarketPairs: toFiniteNumberOrNull3(
4510
4707
  globalRow.activeMarketPairs
4511
4708
  ),
4512
4709
  altLiquidityRegime
@@ -4551,22 +4748,22 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4551
4748
  asOfTs: exchangeLiquidityRow.ts.getTime(),
4552
4749
  ageMs: exchangeLiquidityRow.ageMs,
4553
4750
  stale: exchangeLiquidityRow.stale,
4554
- exchangesCount: toFiniteNumberOrNull2(
4751
+ exchangesCount: toFiniteNumberOrNull3(
4555
4752
  exchangeLiquidityRow.exchangesCount
4556
4753
  ),
4557
- totalVolumeUsd: toFiniteNumberOrNull2(
4754
+ totalVolumeUsd: toFiniteNumberOrNull3(
4558
4755
  exchangeLiquidityRow.totalVolumeUsd
4559
4756
  ),
4560
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
4757
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
4561
4758
  exchangeLiquidityRow.totalVolumeChange24hPct
4562
4759
  ),
4563
- binanceVolumeUsd: toFiniteNumberOrNull2(
4760
+ binanceVolumeUsd: toFiniteNumberOrNull3(
4564
4761
  exchangeLiquidityRow.binanceVolumeUsd
4565
4762
  ),
4566
- binanceVolumeShare: toFiniteNumberOrNull2(
4763
+ binanceVolumeShare: toFiniteNumberOrNull3(
4567
4764
  exchangeLiquidityRow.binanceVolumeShare
4568
4765
  ),
4569
- topExchangeVolumeShare: toFiniteNumberOrNull2(
4766
+ topExchangeVolumeShare: toFiniteNumberOrNull3(
4570
4767
  exchangeLiquidityRow.topExchangeVolumeShare
4571
4768
  ),
4572
4769
  liquidityRegime: exchangeLiquidityRegime
@@ -4579,11 +4776,11 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4579
4776
  asOfTs: fearGreedRow.ts.getTime(),
4580
4777
  ageMs: fearGreedRow.ageMs,
4581
4778
  stale: fearGreedRow.stale,
4582
- value: toFiniteNumberOrNull2(fearGreedRow.value),
4583
- valueChange24h: toFiniteNumberOrNull2(
4779
+ value: toFiniteNumberOrNull3(fearGreedRow.value),
4780
+ valueChange24h: toFiniteNumberOrNull3(
4584
4781
  fearGreedRow.valueChange24h
4585
4782
  ),
4586
- valueChange7d: toFiniteNumberOrNull2(
4783
+ valueChange7d: toFiniteNumberOrNull3(
4587
4784
  fearGreedRow.valueChange7d
4588
4785
  ),
4589
4786
  classification: fearGreedRow.classification ?? "Unknown",
@@ -4603,13 +4800,13 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4603
4800
  cmc100Value,
4604
4801
  cmc100Change24hPct,
4605
4802
  cmc100TopConstituentSymbol: cmc100Row?.topConstituentSymbol ?? null,
4606
- cmc100TopConstituentWeightPct: toFiniteNumberOrNull2(
4803
+ cmc100TopConstituentWeightPct: toFiniteNumberOrNull3(
4607
4804
  cmc100Row?.topConstituentWeightPct
4608
4805
  ),
4609
4806
  cmc20Value,
4610
4807
  cmc20Change24hPct,
4611
4808
  cmc20TopConstituentSymbol: cmc20Row?.topConstituentSymbol ?? null,
4612
- cmc20TopConstituentWeightPct: toFiniteNumberOrNull2(
4809
+ cmc20TopConstituentWeightPct: toFiniteNumberOrNull3(
4613
4810
  cmc20Row?.topConstituentWeightPct
4614
4811
  ),
4615
4812
  cmc20ToCmc100Ratio,
@@ -4627,7 +4824,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4627
4824
  throw error;
4628
4825
  }
4629
4826
  coinMarketCapContextUnavailable = true;
4630
- import_logger4.logger.warn(
4827
+ import_logger5.logger.warn(
4631
4828
  "CoinMarketCap context disabled after Timescale read failure: %s",
4632
4829
  String(error)
4633
4830
  );
@@ -4641,7 +4838,7 @@ var import_data2 = require("@tradejs/core/data");
4641
4838
  var import_strategies3 = require("@tradejs/core/strategies");
4642
4839
  var import_constants2 = require("@tradejs/core/constants");
4643
4840
  var import_derivatives = require("@tradejs/infra/timescale/derivatives");
4644
- var import_logger5 = require("@tradejs/infra/logger");
4841
+ var import_logger6 = require("@tradejs/infra/logger");
4645
4842
  var STORED_INTERVALS = ["15m", "1h"];
4646
4843
  var CONTEXT_INTERVALS = ["15m", "1h"];
4647
4844
  var DEFAULT_LOOKBACK_HOURS = 48;
@@ -4696,7 +4893,7 @@ var resolvePrimaryReferenceSymbol2 = () => PRIMARY_DERIVATIVES_REFERENCE_SYMBOL;
4696
4893
  var resolveSecondaryReferenceSymbol = () => SECONDARY_DERIVATIVES_REFERENCE_SYMBOL;
4697
4894
  var getPrimaryIntervalContext = (context) => context?.intervals["15m"] ?? context?.intervals["1h"] ?? null;
4698
4895
  var hasDerivativesSymbolData = (context) => Object.keys(context.intervals).length > 0 && !context.summary.riskFlags.includes("missing_derivatives");
4699
- var toFiniteNumberOrNull3 = (value) => {
4896
+ var toFiniteNumberOrNull4 = (value) => {
4700
4897
  if (typeof value === "number" && Number.isFinite(value)) return value;
4701
4898
  if (typeof value === "string" && value.trim()) {
4702
4899
  const parsed = Number(value);
@@ -4710,8 +4907,8 @@ var roundNullable = (value, digits = 4) => {
4710
4907
  return Math.round(value * multiplier) / multiplier;
4711
4908
  };
4712
4909
  var deltaNullable = (targetValue, referenceValue) => {
4713
- const target = toFiniteNumberOrNull3(targetValue);
4714
- const reference = toFiniteNumberOrNull3(referenceValue);
4910
+ const target = toFiniteNumberOrNull4(targetValue);
4911
+ const reference = toFiniteNumberOrNull4(referenceValue);
4715
4912
  return target == null || reference == null ? null : roundNullable(target - reference);
4716
4913
  };
4717
4914
  var buildTargetDerivedContext = (params) => {
@@ -4873,7 +5070,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4873
5070
  throw error;
4874
5071
  }
4875
5072
  derivativesContextUnavailable = true;
4876
- import_logger5.logger.warn(
5073
+ import_logger6.logger.warn(
4877
5074
  "Derivatives context disabled after Timescale read failure: %s",
4878
5075
  String(error)
4879
5076
  );
@@ -4885,7 +5082,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4885
5082
  var import_data3 = require("@tradejs/core/data");
4886
5083
  var import_strategies4 = require("@tradejs/core/strategies");
4887
5084
  var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWhales");
4888
- var import_logger6 = require("@tradejs/infra/logger");
5085
+ var import_logger7 = require("@tradejs/infra/logger");
4889
5086
 
4890
5087
  // src/hyperliquidWhaleUniverse.ts
4891
5088
  var import_node_crypto3 = require("crypto");
@@ -5409,7 +5606,7 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
5409
5606
  throw error;
5410
5607
  }
5411
5608
  hyperliquidWhaleContextUnavailable = true;
5412
- import_logger6.logger.warn(
5609
+ import_logger7.logger.warn(
5413
5610
  "Hyperliquid whale context disabled after Timescale read failure: %s",
5414
5611
  String(error)
5415
5612
  );
@@ -5492,7 +5689,7 @@ var runMarketContextStage = async ({
5492
5689
  elapsedMs: Date.now() - startedAt
5493
5690
  };
5494
5691
  if (status === "timed_out") {
5495
- import_logger7.logger.warn(
5692
+ import_logger8.logger.warn(
5496
5693
  "Market context stage timed out: %s after %sms",
5497
5694
  stage,
5498
5695
  result.elapsedMs
@@ -5682,7 +5879,7 @@ var enrichSignalWithAi = async ({
5682
5879
  signal.aiAnalysis = analysis;
5683
5880
  return resolveAiQuality(analysis, direction);
5684
5881
  } catch (err) {
5685
- import_logger8.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
5882
+ import_logger9.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
5686
5883
  }
5687
5884
  return void 0;
5688
5885
  };
@@ -5699,75 +5896,6 @@ var enrichSignalWithMlAi = async ({
5699
5896
  await enrichSignalWithMl({ signal, env, ml });
5700
5897
  return enrichSignalWithAi({ signal, userName, symbol, direction, env, ai });
5701
5898
  };
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
5899
  var resolveRuntimeTelemetryQuality = ({
5772
5900
  signalClosePrice,
5773
5901
  arrivalMid,
@@ -5844,7 +5972,8 @@ var executeEntryOrder = async ({
5844
5972
  signal,
5845
5973
  beforePlaceOrder,
5846
5974
  recordRuntimeTrade = true,
5847
- leverage
5975
+ leverage,
5976
+ executionLimits
5848
5977
  }) => {
5849
5978
  await beforePlaceOrder?.();
5850
5979
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
@@ -5855,23 +5984,57 @@ var executeEntryOrder = async ({
5855
5984
  signal.orderQty = qty;
5856
5985
  signal.orderValue = qty * currentPrice;
5857
5986
  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;
5987
+ let arrivalSnapshot = {
5988
+ arrivalSnapshotTime: Date.now(),
5989
+ arrivalSource: "unavailable",
5990
+ bid: null,
5991
+ ask: null,
5992
+ arrivalMid: null,
5993
+ spreadBps: null
5994
+ };
5995
+ let orderSubmitTime = Date.now();
5996
+ const beforeSubmit = createEntrySubmissionValidation({
5997
+ connector,
5998
+ symbol,
5999
+ direction,
6000
+ takeProfits,
6001
+ stopLossPrice,
6002
+ limits: executionLimits,
6003
+ onValidated: (snapshot2, economics2) => {
6004
+ arrivalSnapshot = snapshot2;
6005
+ orderSubmitTime = Date.now();
6006
+ if (executionLimits?.maxLossValue != null) {
6007
+ signal.riskDecision = {
6008
+ stage: "order_plan",
6009
+ status: economics2.riskExceeded ? "warning" : "approved",
6010
+ reason: economics2.riskExceeded ? "ENTRY_RISK_EXCEEDS_MAX_LOSS_AT_SUBMISSION" : "PLANNED_LOSS_WITHIN_MAX_LOSS_VALUE",
6011
+ plannedLossValue: economics2.plannedLossValue,
6012
+ maxLossValue: executionLimits.maxLossValue,
6013
+ limitUtilization: economics2.plannedLossValue / executionLimits.maxLossValue,
6014
+ enforced: false
6015
+ };
6016
+ }
6017
+ }
6018
+ });
6019
+ if (!beforeSubmit) {
6020
+ arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
6021
+ try {
6022
+ validateEntryProtectionAtArrival({
6023
+ direction,
6024
+ signalPrice: currentPrice,
6025
+ bid: arrivalSnapshot.bid,
6026
+ ask: arrivalSnapshot.ask,
6027
+ arrivalMid: arrivalSnapshot.arrivalMid,
6028
+ takeProfits,
6029
+ stopLossPrice
6030
+ });
6031
+ } catch (error) {
6032
+ signal.orderStatus = "failed";
6033
+ signal.orderFailureReason = error.message;
6034
+ throw error;
6035
+ }
6036
+ orderSubmitTime = Date.now();
5873
6037
  }
5874
- const orderSubmitTime = Date.now();
5875
6038
  const orderPlaced = await connector.placeOrder({
5876
6039
  symbol,
5877
6040
  qty,
@@ -5882,7 +6045,8 @@ var executeEntryOrder = async ({
5882
6045
  direction,
5883
6046
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
5884
6047
  orderId,
5885
- signal
6048
+ signal,
6049
+ ...beforeSubmit ? { beforeSubmit } : {}
5886
6050
  });
5887
6051
  const orderAckTime = Date.now();
5888
6052
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -6711,7 +6875,7 @@ var canUseSharedReplayState = ({
6711
6875
  }) => (env === "BACKTEST" || env === "PARITY") && Boolean(sharedReplayKey);
6712
6876
 
6713
6877
  // src/strategy/runtimeExecution.ts
6714
- var import_logger9 = require("@tradejs/infra/logger");
6878
+ var import_logger10 = require("@tradejs/infra/logger");
6715
6879
  var import_types = require("@tradejs/types");
6716
6880
  var buildExitOrderSignal = ({
6717
6881
  strategyName,
@@ -6763,7 +6927,7 @@ var handleExitDecision = async ({
6763
6927
  deploymentId: connector.deploymentId
6764
6928
  });
6765
6929
  if (!activeTrade) {
6766
- import_logger9.logger.warn(
6930
+ import_logger10.logger.warn(
6767
6931
  "[%s] blocked closePosition for untracked runtime position: %s",
6768
6932
  strategyName ?? "unknown",
6769
6933
  symbol
@@ -6771,7 +6935,7 @@ var handleExitDecision = async ({
6771
6935
  return "CLOSE_BLOCKED_BY_UNTRACKED_POSITION";
6772
6936
  }
6773
6937
  if (!strategyName || activeTrade.strategy !== strategyName) {
6774
- import_logger9.logger.warn(
6938
+ import_logger10.logger.warn(
6775
6939
  "[%s] blocked closePosition for foreign runtime position: %s ownedBy=%s",
6776
6940
  strategyName ?? "unknown",
6777
6941
  symbol,
@@ -6824,7 +6988,7 @@ var handleExitDecision = async ({
6824
6988
  exitType: closedTrade?.exitType ?? "exit"
6825
6989
  });
6826
6990
  } catch (notificationError) {
6827
- import_logger9.logger.error(
6991
+ import_logger10.logger.error(
6828
6992
  "runtime close notification error: %s %s",
6829
6993
  symbol,
6830
6994
  notificationError
@@ -6838,7 +7002,7 @@ var handleExitDecision = async ({
6838
7002
  decision,
6839
7003
  market
6840
7004
  });
6841
- import_logger9.logger.error("close order error: %s %s", symbol, err);
7005
+ import_logger10.logger.error("close order error: %s %s", symbol, err);
6842
7006
  return "ORDER_ERROR";
6843
7007
  }
6844
7008
  return decision.code;
@@ -6865,7 +7029,7 @@ var handleProtectDecision = async ({
6865
7029
  decision,
6866
7030
  market
6867
7031
  });
6868
- import_logger9.logger.error("protect position error: %s %s", symbol, err);
7032
+ import_logger10.logger.error("protect position error: %s %s", symbol, err);
6869
7033
  return "ORDER_ERROR";
6870
7034
  }
6871
7035
  return decision.code;
@@ -6931,7 +7095,11 @@ var executeEntryDecision = async ({
6931
7095
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
6932
7096
  signal,
6933
7097
  beforePlaceOrder,
6934
- recordRuntimeTrade: recordRuntimeJournal
7098
+ recordRuntimeTrade: recordRuntimeJournal,
7099
+ executionLimits: resolveEntryExecutionLimits(
7100
+ hookCtx.strategyConfig,
7101
+ decision.entryContext.direction
7102
+ )
6935
7103
  });
6936
7104
  await invokeStageHooks(
6937
7105
  "afterPlaceOrder",
@@ -6953,19 +7121,32 @@ var executeEntryDecision = async ({
6953
7121
  return signal;
6954
7122
  }
6955
7123
  await beforePlaceOrder();
6956
- const arrivalSnapshot = await getOrderArrivalSnapshot({
7124
+ const beforeSubmit = createEntrySubmissionValidation({
6957
7125
  connector,
6958
- symbol
6959
- });
6960
- validateEntryProtectionAtArrival({
7126
+ symbol,
6961
7127
  direction: decision.entryContext.direction,
6962
- signalPrice: decision.entryContext.prices.currentPrice,
6963
- bid: arrivalSnapshot.bid,
6964
- ask: arrivalSnapshot.ask,
6965
- arrivalMid: arrivalSnapshot.arrivalMid,
6966
7128
  takeProfits: decision.orderPlan.takeProfits,
6967
- stopLossPrice: decision.orderPlan.stopLossPrice
7129
+ stopLossPrice: decision.orderPlan.stopLossPrice,
7130
+ limits: resolveEntryExecutionLimits(
7131
+ hookCtx.strategyConfig,
7132
+ decision.entryContext.direction
7133
+ )
6968
7134
  });
7135
+ if (!beforeSubmit) {
7136
+ const arrivalSnapshot = await getOrderArrivalSnapshot({
7137
+ connector,
7138
+ symbol
7139
+ });
7140
+ validateEntryProtectionAtArrival({
7141
+ direction: decision.entryContext.direction,
7142
+ signalPrice: decision.entryContext.prices.currentPrice,
7143
+ bid: arrivalSnapshot.bid,
7144
+ ask: arrivalSnapshot.ask,
7145
+ arrivalMid: arrivalSnapshot.arrivalMid,
7146
+ takeProfits: decision.orderPlan.takeProfits,
7147
+ stopLossPrice: decision.orderPlan.stopLossPrice
7148
+ });
7149
+ }
6969
7150
  const orderPlaced = await connector.placeOrder({
6970
7151
  symbol,
6971
7152
  qty: decision.orderPlan.qty,
@@ -6973,6 +7154,7 @@ var executeEntryDecision = async ({
6973
7154
  timestamp: decision.entryContext.timestamp,
6974
7155
  direction: decision.entryContext.direction,
6975
7156
  positionIntent: decision.orderPlan.positionIntent,
7157
+ ...beforeSubmit ? { beforeSubmit } : {},
6976
7158
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {}
6977
7159
  });
6978
7160
  if (!orderPlaced) {
@@ -7014,6 +7196,23 @@ var executeEntryDecision = async ({
7014
7196
  { decision, entry, market }
7015
7197
  );
7016
7198
  } catch (err) {
7199
+ if (err instanceof EntrySubmissionValidationError) {
7200
+ if (signal) {
7201
+ signal.orderStatus = "skipped";
7202
+ signal.orderSkipReason = err.message;
7203
+ signal.allocatorDecision = {
7204
+ stage: "runtime_admission",
7205
+ status: "rejected",
7206
+ reason: err.message
7207
+ };
7208
+ }
7209
+ import_logger10.logger.warn(
7210
+ "entry rejected before submission: %s %s",
7211
+ symbol,
7212
+ err.message
7213
+ );
7214
+ return signal ?? err.message;
7215
+ }
7017
7216
  if (signal) {
7018
7217
  signal.orderStatus = "failed";
7019
7218
  if (typeof signal.orderFailureReason !== "string" || !signal.orderFailureReason.trim()) {
@@ -7028,9 +7227,9 @@ var executeEntryDecision = async ({
7028
7227
  market
7029
7228
  });
7030
7229
  if (err?.message === import_types.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY) {
7031
- import_logger9.logger.warn("order warning: %s %s", symbol, err);
7230
+ import_logger10.logger.warn("order warning: %s %s", symbol, err);
7032
7231
  } else {
7033
- import_logger9.logger.error("order error: %s %s", symbol, err);
7232
+ import_logger10.logger.error("order error: %s %s", symbol, err);
7034
7233
  }
7035
7234
  return signal ?? "ORDER_ERROR";
7036
7235
  }
@@ -7138,6 +7337,9 @@ var createStrategyRuntime = ({
7138
7337
  });
7139
7338
  const strategyPolicyProfile = getPolicyProfile();
7140
7339
  const indicatorPeriods = (0, import_strategies6.buildDefaultIndicatorPeriods)(config);
7340
+ const strategyConfig = { ...config };
7341
+ delete strategyConfig.RECORD_RUNTIME_TRADES;
7342
+ delete strategyConfig.SIMULATE_ORDERS;
7141
7343
  const hookBase = {
7142
7344
  connector,
7143
7345
  strategyName,
@@ -7148,7 +7350,7 @@ var createStrategyRuntime = ({
7148
7350
  accountId,
7149
7351
  deploymentId,
7150
7352
  policyProfileId: strategyPolicyProfile?.id ?? requestedPolicyProfileId,
7151
- strategyConfig: config,
7353
+ strategyConfig,
7152
7354
  env,
7153
7355
  isConfigFromBacktest
7154
7356
  };
@@ -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,21 +7411,21 @@ 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
7216
7418
  );
7217
7419
  }
7218
7420
  };
7219
- const invokeHook = async (stage, hook, params, errorContext = {}) => {
7421
+ const invokeHook = async (stage, hook, params, errorContext = {}, failureResult) => {
7220
7422
  if (!hook) {
7221
7423
  return void 0;
7222
7424
  }
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,
@@ -7236,17 +7438,18 @@ var createStrategyRuntime = ({
7236
7438
  entry: errorContext.entry,
7237
7439
  market: errorContext.market
7238
7440
  });
7239
- return void 0;
7441
+ return failureResult;
7240
7442
  }
7241
7443
  };
7242
- const invokeProjectHooks = async (stage, params, errorContext = {}) => {
7444
+ const invokeProjectHooks = async (stage, params, errorContext = {}, failureResult) => {
7243
7445
  const results = [];
7244
7446
  for (const hook of getProjectHookList(stage)) {
7245
7447
  const result = await invokeHook(
7246
7448
  stage,
7247
7449
  hook,
7248
7450
  params,
7249
- errorContext
7451
+ errorContext,
7452
+ failureResult
7250
7453
  );
7251
7454
  if (result !== void 0) {
7252
7455
  results.push(result);
@@ -7261,10 +7464,12 @@ var createStrategyRuntime = ({
7261
7464
  return invokeHook(stage, hook, params, errorContext);
7262
7465
  };
7263
7466
  const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
7467
+ const failureResult = stage === "beforeEntryGate" ? { allow: false, reason: "ERROR" } : void 0;
7264
7468
  const projectResults = await invokeProjectHooks(
7265
7469
  stage,
7266
7470
  params,
7267
- errorContext
7471
+ errorContext,
7472
+ failureResult
7268
7473
  );
7269
7474
  const projectBlock = projectResults.find(
7270
7475
  (result) => result?.allow === false
@@ -7276,7 +7481,8 @@ var createStrategyRuntime = ({
7276
7481
  stage,
7277
7482
  hook,
7278
7483
  params,
7279
- errorContext
7484
+ errorContext,
7485
+ failureResult
7280
7486
  );
7281
7487
  };
7282
7488
  const applyProjectAfterCoreDecisionHooks = async ({
@@ -7422,11 +7628,8 @@ var createStrategyRuntime = ({
7422
7628
  return enrichedBaseContext;
7423
7629
  }
7424
7630
  });
7425
- const coreConfig = { ...config };
7426
- delete coreConfig.RECORD_RUNTIME_TRADES;
7427
- delete coreConfig.SIMULATE_ORDERS;
7428
7631
  const core = await createCore({
7429
- config: coreConfig,
7632
+ config: strategyConfig,
7430
7633
  data,
7431
7634
  strategyApi,
7432
7635
  indicatorsState
@@ -8046,7 +8249,7 @@ setStrategyRuntimeFactory(createStrategyRuntime);
8046
8249
 
8047
8250
  // src/strategyHooks/closeOppositePositionsBeforeOpen.ts
8048
8251
  var import_lodash2 = __toESM(require("lodash"));
8049
- var import_logger11 = require("@tradejs/infra/logger");
8252
+ var import_logger12 = require("@tradejs/infra/logger");
8050
8253
  var closeOppositePositionsBeforeOpen = async ({
8051
8254
  connector,
8052
8255
  entryContext
@@ -8060,7 +8263,7 @@ var closeOppositePositionsBeforeOpen = async ({
8060
8263
  } = entryContext;
8061
8264
  const price = prices.currentPrice;
8062
8265
  try {
8063
- import_logger11.logger.log(
8266
+ import_logger12.logger.log(
8064
8267
  "info",
8065
8268
  "[%s] checking open positions before open: %s %s",
8066
8269
  strategyName,
@@ -8071,7 +8274,7 @@ var closeOppositePositionsBeforeOpen = async ({
8071
8274
  const openPositions = (positions || []).filter(
8072
8275
  (item) => item && Number(item.qty) > 0
8073
8276
  );
8074
- import_logger11.logger.log(
8277
+ import_logger12.logger.log(
8075
8278
  "info",
8076
8279
  "[%s] open positions found: %s",
8077
8280
  strategyName,
@@ -8081,7 +8284,7 @@ var closeOppositePositionsBeforeOpen = async ({
8081
8284
  (item) => item.symbol !== currentSymbol && item.direction !== currentDirection
8082
8285
  );
8083
8286
  if (import_lodash2.default.isEmpty(oppositePositions)) {
8084
- import_logger11.logger.log(
8287
+ import_logger12.logger.log(
8085
8288
  "info",
8086
8289
  "[%s] no opposite positions to close before open: %s",
8087
8290
  strategyName,
@@ -8090,7 +8293,7 @@ var closeOppositePositionsBeforeOpen = async ({
8090
8293
  return;
8091
8294
  }
8092
8295
  for (const position of oppositePositions) {
8093
- import_logger11.logger.log(
8296
+ import_logger12.logger.log(
8094
8297
  "info",
8095
8298
  "[%s] closing opposite position: %s %s qty=%s",
8096
8299
  strategyName,
@@ -8105,14 +8308,14 @@ var closeOppositePositionsBeforeOpen = async ({
8105
8308
  timestamp,
8106
8309
  direction: position.direction
8107
8310
  });
8108
- import_logger11.logger.log(
8311
+ import_logger12.logger.log(
8109
8312
  "info",
8110
8313
  "[%s] opposite position closed: %s",
8111
8314
  strategyName,
8112
8315
  position.symbol
8113
8316
  );
8114
8317
  } catch (err) {
8115
- import_logger11.logger.log(
8318
+ import_logger12.logger.log(
8116
8319
  "error",
8117
8320
  "[%s] failed to close opposite position: %s %s",
8118
8321
  strategyName,
@@ -8122,7 +8325,7 @@ var closeOppositePositionsBeforeOpen = async ({
8122
8325
  }
8123
8326
  }
8124
8327
  } catch (err) {
8125
- import_logger11.logger.log(
8328
+ import_logger12.logger.log(
8126
8329
  "error",
8127
8330
  "[%s] failed to load open positions before open: %s %s",
8128
8331
  strategyName,
@@ -8334,7 +8537,7 @@ var createMoveStopToBreakEvenOnBarHook = ({
8334
8537
  var createMoveStopToBreakEvenAfterCoreDecisionHook = createMoveStopToBreakEvenOnBarHook;
8335
8538
 
8336
8539
  // src/signalsHooks/closeAllPositionsOnGlobalProfitBeforeSignals.ts
8337
- var import_logger12 = require("@tradejs/infra/logger");
8540
+ var import_logger13 = require("@tradejs/infra/logger");
8338
8541
  var createCloseAllOnGlobalProfitBeforeSignalsHook = ({
8339
8542
  getStrategyDefaultConfig = () => void 0,
8340
8543
  profitRiskMultiplier = DEFAULT_GLOBAL_UNREALIZED_PNL_TRIGGER_RISK_MULTIPLIER
@@ -8373,7 +8576,7 @@ var createCloseAllOnGlobalProfitBeforeSignalsHook = ({
8373
8576
  if (!Number.isFinite(unrealizedPnlThreshold) || unrealizedPnlThreshold <= 0 || totalUnrealizedPnl < unrealizedPnlThreshold) {
8374
8577
  return;
8375
8578
  }
8376
- import_logger12.logger.info(
8579
+ import_logger13.logger.info(
8377
8580
  "closing all positions before signals by global unrealized pnl threshold: totalPnl=%s threshold=%s positions=%s",
8378
8581
  totalUnrealizedPnl,
8379
8582
  unrealizedPnlThreshold,
@@ -8399,7 +8602,7 @@ var createCloseAllOnGlobalProfitBeforeSignalsHook = ({
8399
8602
  ];
8400
8603
  });
8401
8604
  if (failedClosures.length) {
8402
- import_logger12.logger.warn(
8605
+ import_logger13.logger.warn(
8403
8606
  "close-all before signals hook could not confirm closures for %s",
8404
8607
  failedClosures.join(", ")
8405
8608
  );