@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.
@@ -781,7 +781,7 @@ __export(runtimeDashboard_exports, {
781
781
  });
782
782
  module.exports = __toCommonJS(runtimeDashboard_exports);
783
783
  var import_time4 = require("@tradejs/core/time");
784
- var import_logger14 = require("@tradejs/infra/logger");
784
+ var import_logger15 = require("@tradejs/infra/logger");
785
785
  var import_runtimeDeploymentEvents = require("@tradejs/infra/runtimeDeploymentEvents");
786
786
  var import_tradingAccounts3 = require("@tradejs/infra/tradingAccounts");
787
787
  var import_redis4 = require("@tradejs/infra/redis");
@@ -2342,13 +2342,210 @@ var assessSignalWithJev = async ({
2342
2342
  // src/strategyRuntime.ts
2343
2343
  var import_constants7 = require("@tradejs/core/constants");
2344
2344
  var import_strategies6 = require("@tradejs/core/strategies");
2345
- var import_logger13 = require("@tradejs/infra/logger");
2345
+ var import_logger14 = require("@tradejs/infra/logger");
2346
2346
 
2347
2347
  // src/strategyHelpers/runtime.ts
2348
- var import_logger11 = require("@tradejs/infra/logger");
2348
+ var import_logger12 = require("@tradejs/infra/logger");
2349
2349
  var import_constants5 = require("@tradejs/core/constants");
2350
2350
  var import_ml2 = require("@tradejs/infra/ml");
2351
2351
 
2352
+ // src/strategy/entrySubmissionValidation.ts
2353
+ var import_logger5 = require("@tradejs/infra/logger");
2354
+
2355
+ // src/strategy/entryExecutionValidation.ts
2356
+ var EntrySubmissionValidationError = class extends Error {
2357
+ constructor() {
2358
+ super(...arguments);
2359
+ this.name = "EntrySubmissionValidationError";
2360
+ }
2361
+ };
2362
+ var resolveEntryExecutionLimits = (config, direction) => {
2363
+ const side = config[direction];
2364
+ const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
2365
+ const maximum = config.MAX_LOSS_VALUE;
2366
+ if (minimum == null && maximum == null) return void 0;
2367
+ const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
2368
+ const maxLossValue = maximum == null ? void 0 : Number(maximum);
2369
+ const feeRate = Number(config.RISK_FEE_RATE ?? 0);
2370
+ const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
2371
+ const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
2372
+ const slippageBps = slippage + impact;
2373
+ 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)
2374
+ throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
2375
+ return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
2376
+ };
2377
+ var validateEntryExecution = ({
2378
+ direction,
2379
+ price,
2380
+ qty,
2381
+ stopLossPrice,
2382
+ takeProfitPrice,
2383
+ limits
2384
+ }) => {
2385
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
2386
+ throw new EntrySubmissionValidationError(
2387
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2388
+ );
2389
+ const side = direction === "LONG" ? 1 : -1;
2390
+ if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
2391
+ throw new EntrySubmissionValidationError(
2392
+ "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
2393
+ );
2394
+ const slippage = limits.slippageBps / 1e4;
2395
+ const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
2396
+ const entryCost = cost(price, side);
2397
+ const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
2398
+ const rewardPerUnit = Math.max(
2399
+ 0,
2400
+ Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
2401
+ );
2402
+ const netRiskRatio = rewardPerUnit / lossPerUnit;
2403
+ const plannedLossValue = qty * lossPerUnit;
2404
+ if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
2405
+ throw new EntrySubmissionValidationError(
2406
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2407
+ );
2408
+ if (limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio)
2409
+ throw new EntrySubmissionValidationError(
2410
+ "ENTRY_RR_BELOW_MIN_AT_SUBMISSION"
2411
+ );
2412
+ const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
2413
+ return { netRiskRatio, plannedLossValue, riskExceeded };
2414
+ };
2415
+
2416
+ // src/strategy/entrySubmissionValidation.ts
2417
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
2418
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
2419
+ var getOrderArrivalSnapshot = async ({
2420
+ connector,
2421
+ symbol,
2422
+ requireTimestamp = false
2423
+ }) => {
2424
+ if (typeof connector.getTopOfBookTicker !== "function") {
2425
+ return {
2426
+ arrivalSnapshotTime: Date.now(),
2427
+ arrivalSource: "unavailable",
2428
+ bid: null,
2429
+ ask: null,
2430
+ arrivalMid: null,
2431
+ spreadBps: null
2432
+ };
2433
+ }
2434
+ try {
2435
+ const ticker = await connector.getTopOfBookTicker(symbol);
2436
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
2437
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
2438
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
2439
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
2440
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
2441
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
2442
+ return {
2443
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
2444
+ arrivalSource: "top_of_book",
2445
+ bid,
2446
+ ask,
2447
+ arrivalMid,
2448
+ spreadBps
2449
+ };
2450
+ } catch (error) {
2451
+ import_logger5.logger.warn(
2452
+ "runtime order arrival snapshot failed: %s %s",
2453
+ symbol,
2454
+ error?.message || String(error)
2455
+ );
2456
+ return {
2457
+ arrivalSnapshotTime: Date.now(),
2458
+ arrivalSource: "top_of_book_error",
2459
+ bid: null,
2460
+ ask: null,
2461
+ arrivalMid: null,
2462
+ spreadBps: null
2463
+ };
2464
+ }
2465
+ };
2466
+ var validateEntryProtectionAtArrival = ({
2467
+ direction,
2468
+ signalPrice,
2469
+ bid,
2470
+ ask,
2471
+ arrivalMid,
2472
+ takeProfits,
2473
+ stopLossPrice
2474
+ }) => {
2475
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
2476
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
2477
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
2478
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
2479
+ }
2480
+ for (const takeProfit of takeProfits) {
2481
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
2482
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
2483
+ }
2484
+ }
2485
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
2486
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
2487
+ }
2488
+ };
2489
+ var createEntrySubmissionValidation = ({
2490
+ connector,
2491
+ symbol,
2492
+ direction,
2493
+ takeProfits,
2494
+ stopLossPrice,
2495
+ limits,
2496
+ onValidated
2497
+ }) => {
2498
+ if (!limits) return void 0;
2499
+ if (!connector.capabilities?.orderSubmissionValidation)
2500
+ throw new EntrySubmissionValidationError(
2501
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
2502
+ );
2503
+ return async ({ qty, executionPrice }) => {
2504
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
2505
+ const snapshot2 = simulated && executionPrice != null ? {
2506
+ arrivalSnapshotTime: Date.now(),
2507
+ arrivalSource: "simulation",
2508
+ bid: executionPrice,
2509
+ ask: executionPrice,
2510
+ arrivalMid: executionPrice,
2511
+ spreadBps: 0
2512
+ } : await getOrderArrivalSnapshot({
2513
+ connector,
2514
+ symbol,
2515
+ requireTimestamp: true
2516
+ });
2517
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
2518
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
2519
+ throw new EntrySubmissionValidationError(
2520
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
2521
+ );
2522
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
2523
+ throw new EntrySubmissionValidationError(
2524
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
2525
+ );
2526
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
2527
+ validateEntryProtectionAtArrival({
2528
+ direction,
2529
+ signalPrice: price,
2530
+ bid: snapshot2.bid,
2531
+ ask: snapshot2.ask,
2532
+ arrivalMid: snapshot2.arrivalMid,
2533
+ takeProfits,
2534
+ stopLossPrice
2535
+ });
2536
+ const prices = takeProfits.map((tp) => tp.price);
2537
+ const economics2 = validateEntryExecution({
2538
+ direction,
2539
+ price,
2540
+ qty,
2541
+ stopLossPrice,
2542
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
2543
+ limits
2544
+ });
2545
+ onValidated?.(snapshot2, economics2);
2546
+ };
2547
+ };
2548
+
2352
2549
  // src/ai.ts
2353
2550
  var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
2354
2551
  var import_redis2 = require("@tradejs/infra/redis");
@@ -3881,7 +4078,7 @@ var import_node_crypto = require("crypto");
3881
4078
  var import_constants3 = require("@tradejs/core/constants");
3882
4079
  var import_time3 = require("@tradejs/core/time");
3883
4080
  var import_trade = require("@tradejs/core/trade");
3884
- var import_logger5 = require("@tradejs/infra/logger");
4081
+ var import_logger6 = require("@tradejs/infra/logger");
3885
4082
  var import_redis3 = require("@tradejs/infra/redis");
3886
4083
  var now = () => Date.now();
3887
4084
  var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
@@ -3940,7 +4137,7 @@ var recordRuntimeTradeOpen = async (params) => {
3940
4137
  )
3941
4138
  ]);
3942
4139
  } catch (error) {
3943
- import_logger5.logger.error(
4140
+ import_logger6.logger.error(
3944
4141
  "runtime trade open journal failed: %s %s",
3945
4142
  record.symbol,
3946
4143
  error?.message || String(error)
@@ -4008,7 +4205,7 @@ var recordRuntimeTradeIncrease = async (params) => {
4008
4205
  )
4009
4206
  ]);
4010
4207
  } catch (error) {
4011
- import_logger5.logger.error(
4208
+ import_logger6.logger.error(
4012
4209
  "runtime trade increase journal failed: %s %s",
4013
4210
  symbol,
4014
4211
  error?.message || String(error)
@@ -4115,7 +4312,7 @@ var markRuntimeTradeClosed = async (params) => {
4115
4312
  )
4116
4313
  ]);
4117
4314
  } catch (error) {
4118
- import_logger5.logger.error(
4315
+ import_logger6.logger.error(
4119
4316
  "runtime trade close journal failed: %s %s",
4120
4317
  symbol,
4121
4318
  error?.message || String(error)
@@ -4125,11 +4322,11 @@ var markRuntimeTradeClosed = async (params) => {
4125
4322
  };
4126
4323
 
4127
4324
  // src/strategyHelpers/marketContextStages.ts
4128
- var import_logger10 = require("@tradejs/infra/logger");
4325
+ var import_logger11 = require("@tradejs/infra/logger");
4129
4326
 
4130
4327
  // src/strategyHelpers/binanceMarketContext.ts
4131
4328
  var import_marketContext = require("@tradejs/infra/timescale/marketContext");
4132
- var import_logger6 = require("@tradejs/infra/logger");
4329
+ var import_logger7 = require("@tradejs/infra/logger");
4133
4330
  var import_strategies = require("@tradejs/core/strategies");
4134
4331
 
4135
4332
  // src/binanceBreadthUniverses.ts
@@ -4493,7 +4690,7 @@ var parseEnabledFlag = (value, env) => {
4493
4690
  if (normalized === "live") return env !== "BACKTEST";
4494
4691
  return false;
4495
4692
  };
4496
- var toFiniteNumberOrNull = (value) => {
4693
+ var toFiniteNumberOrNull2 = (value) => {
4497
4694
  const numeric = typeof value === "number" ? value : Number(value);
4498
4695
  return Number.isFinite(numeric) ? numeric : null;
4499
4696
  };
@@ -4618,14 +4815,14 @@ var toTradeFlowContext = (row, interval) => row ? {
4618
4815
  asOfTs: row.ts.getTime(),
4619
4816
  ageMs: row.ageMs,
4620
4817
  stale: row.stale,
4621
- trades: toFiniteNumberOrNull(row.trades),
4622
- buyPressurePct: toFiniteNumberOrNull(row.buyPressurePct),
4623
- buyBaseVolume: toFiniteNumberOrNull(row.buyBaseVolume),
4624
- sellBaseVolume: toFiniteNumberOrNull(row.sellBaseVolume),
4625
- buyQuoteVolume: toFiniteNumberOrNull(row.buyQuoteVolume),
4626
- sellQuoteVolume: toFiniteNumberOrNull(row.sellQuoteVolume),
4627
- netBaseDelta: toFiniteNumberOrNull(row.netBaseDelta),
4628
- netQuoteDelta: toFiniteNumberOrNull(row.netQuoteDelta)
4818
+ trades: toFiniteNumberOrNull2(row.trades),
4819
+ buyPressurePct: toFiniteNumberOrNull2(row.buyPressurePct),
4820
+ buyBaseVolume: toFiniteNumberOrNull2(row.buyBaseVolume),
4821
+ sellBaseVolume: toFiniteNumberOrNull2(row.sellBaseVolume),
4822
+ buyQuoteVolume: toFiniteNumberOrNull2(row.buyQuoteVolume),
4823
+ sellQuoteVolume: toFiniteNumberOrNull2(row.sellQuoteVolume),
4824
+ netBaseDelta: toFiniteNumberOrNull2(row.netBaseDelta),
4825
+ netQuoteDelta: toFiniteNumberOrNull2(row.netQuoteDelta)
4629
4826
  } : null;
4630
4827
  var getCachedReferenceRows = ({
4631
4828
  referenceSymbols,
@@ -4714,16 +4911,16 @@ var toMarketBreadthContext = (breadth, interval) => ({
4714
4911
  asOfTs: breadth.ts.getTime(),
4715
4912
  ageMs: breadth.ageMs,
4716
4913
  stale: breadth.stale,
4717
- symbolsCount: toFiniteNumberOrNull(breadth.symbolsCount),
4718
- advancers: toFiniteNumberOrNull(breadth.advancers),
4719
- decliners: toFiniteNumberOrNull(breadth.decliners),
4720
- unchanged: toFiniteNumberOrNull(breadth.unchanged),
4721
- advanceDeclineRatio: toFiniteNumberOrNull(breadth.advanceDeclineRatio),
4722
- pctAboveMa20: toFiniteNumberOrNull(breadth.pctAboveMa20),
4723
- pctAboveMa50: toFiniteNumberOrNull(breadth.pctAboveMa50),
4724
- equalWeightedReturn: toFiniteNumberOrNull(breadth.equalWeightedReturn),
4725
- volumeWeightedReturn: toFiniteNumberOrNull(breadth.volumeWeightedReturn),
4726
- dispersion: toFiniteNumberOrNull(breadth.dispersion)
4914
+ symbolsCount: toFiniteNumberOrNull2(breadth.symbolsCount),
4915
+ advancers: toFiniteNumberOrNull2(breadth.advancers),
4916
+ decliners: toFiniteNumberOrNull2(breadth.decliners),
4917
+ unchanged: toFiniteNumberOrNull2(breadth.unchanged),
4918
+ advanceDeclineRatio: toFiniteNumberOrNull2(breadth.advanceDeclineRatio),
4919
+ pctAboveMa20: toFiniteNumberOrNull2(breadth.pctAboveMa20),
4920
+ pctAboveMa50: toFiniteNumberOrNull2(breadth.pctAboveMa50),
4921
+ equalWeightedReturn: toFiniteNumberOrNull2(breadth.equalWeightedReturn),
4922
+ volumeWeightedReturn: toFiniteNumberOrNull2(breadth.volumeWeightedReturn),
4923
+ dispersion: toFiniteNumberOrNull2(breadth.dispersion)
4727
4924
  });
4728
4925
  var enrichSignalWithBinanceMarketContext = async (params) => {
4729
4926
  const {
@@ -4824,46 +5021,46 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
4824
5021
  asOfTs: primaryBreadth.ts.getTime(),
4825
5022
  ageMs: primaryBreadth.ageMs,
4826
5023
  stale: primaryBreadth.stale,
4827
- btcReturn1h: toFiniteNumberOrNull(
5024
+ btcReturn1h: toFiniteNumberOrNull2(
4828
5025
  primaryBreadth.btcReturn1h
4829
5026
  ),
4830
- btcReturn4h: toFiniteNumberOrNull(
5027
+ btcReturn4h: toFiniteNumberOrNull2(
4831
5028
  primaryBreadth.btcReturn4h
4832
5029
  ),
4833
- btcReturn24h: toFiniteNumberOrNull(
5030
+ btcReturn24h: toFiniteNumberOrNull2(
4834
5031
  primaryBreadth.btcReturn24h
4835
5032
  ),
4836
- altBasketReturn1h: toFiniteNumberOrNull(
5033
+ altBasketReturn1h: toFiniteNumberOrNull2(
4837
5034
  primaryBreadth.altBasketReturn1h
4838
5035
  ),
4839
- altBasketReturn4h: toFiniteNumberOrNull(
5036
+ altBasketReturn4h: toFiniteNumberOrNull2(
4840
5037
  primaryBreadth.altBasketReturn4h
4841
5038
  ),
4842
- altBasketReturn24h: toFiniteNumberOrNull(
5039
+ altBasketReturn24h: toFiniteNumberOrNull2(
4843
5040
  primaryBreadth.altBasketReturn24h
4844
5041
  ),
4845
- btcVsAltReturn1h: toFiniteNumberOrNull(
5042
+ btcVsAltReturn1h: toFiniteNumberOrNull2(
4846
5043
  primaryBreadth.btcVsAltReturn1h
4847
5044
  ),
4848
- btcVsAltReturn4h: toFiniteNumberOrNull(
5045
+ btcVsAltReturn4h: toFiniteNumberOrNull2(
4849
5046
  primaryBreadth.btcVsAltReturn4h
4850
5047
  ),
4851
- btcVsAltReturn24h: toFiniteNumberOrNull(
5048
+ btcVsAltReturn24h: toFiniteNumberOrNull2(
4852
5049
  primaryBreadth.btcVsAltReturn24h
4853
5050
  ),
4854
- btcTurnoverShare1h: toFiniteNumberOrNull(
5051
+ btcTurnoverShare1h: toFiniteNumberOrNull2(
4855
5052
  primaryBreadth.btcTurnoverShare1h
4856
5053
  ),
4857
- btcTurnoverShare24h: toFiniteNumberOrNull(
5054
+ btcTurnoverShare24h: toFiniteNumberOrNull2(
4858
5055
  primaryBreadth.btcTurnoverShare24h
4859
5056
  ),
4860
- btcTurnoverShareChange24h: toFiniteNumberOrNull(
5057
+ btcTurnoverShareChange24h: toFiniteNumberOrNull2(
4861
5058
  primaryBreadth.btcTurnoverShareChange24h
4862
5059
  ),
4863
- altVolToBtcVol24h: toFiniteNumberOrNull(
5060
+ altVolToBtcVol24h: toFiniteNumberOrNull2(
4864
5061
  primaryBreadth.altVolToBtcVol24h
4865
5062
  ),
4866
- altDispersion24h: toFiniteNumberOrNull(
5063
+ altDispersion24h: toFiniteNumberOrNull2(
4867
5064
  primaryBreadth.altDispersion24h
4868
5065
  ),
4869
5066
  regime: primaryBreadth.btcAltRegime ?? "unknown"
@@ -4880,7 +5077,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
4880
5077
  throw error;
4881
5078
  }
4882
5079
  binanceMarketContextUnavailable = true;
4883
- import_logger6.logger.warn(
5080
+ import_logger7.logger.warn(
4884
5081
  "Binance market context disabled after Timescale read failure: %s",
4885
5082
  String(error)
4886
5083
  );
@@ -4890,7 +5087,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
4890
5087
 
4891
5088
  // src/strategyHelpers/coinMarketCapContext.ts
4892
5089
  var import_strategies2 = require("@tradejs/core/strategies");
4893
- var import_logger7 = require("@tradejs/infra/logger");
5090
+ var import_logger8 = require("@tradejs/infra/logger");
4894
5091
  var import_marketContext2 = require("@tradejs/infra/timescale/marketContext");
4895
5092
  var DEFAULT_MAX_AGE_MS = 48 * 60 * 6e4;
4896
5093
  var SOURCE_GLOBAL_DAILY = "coinmarketcap_global";
@@ -4928,7 +5125,7 @@ var asInt = (value, fallback) => {
4928
5125
  const parsed = Number.parseInt(String(value ?? ""), 10);
4929
5126
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
4930
5127
  };
4931
- var toFiniteNumberOrNull2 = (value) => {
5128
+ var toFiniteNumberOrNull3 = (value) => {
4932
5129
  const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
4933
5130
  return Number.isFinite(numeric) ? numeric : null;
4934
5131
  };
@@ -5162,12 +5359,12 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
5162
5359
  const ethRow = references.get("ETHUSDT") ?? null;
5163
5360
  const previousBtcRow = previousReferences.get("BTCUSDT") ?? null;
5164
5361
  const previousEthRow = previousReferences.get("ETHUSDT") ?? null;
5165
- const btcMarketCapUsd = toFiniteNumberOrNull2(btcRow?.marketCapUsd);
5166
- const ethMarketCapUsd = toFiniteNumberOrNull2(ethRow?.marketCapUsd);
5167
- const previousBtcMarketCapUsd = toFiniteNumberOrNull2(
5362
+ const btcMarketCapUsd = toFiniteNumberOrNull3(btcRow?.marketCapUsd);
5363
+ const ethMarketCapUsd = toFiniteNumberOrNull3(ethRow?.marketCapUsd);
5364
+ const previousBtcMarketCapUsd = toFiniteNumberOrNull3(
5168
5365
  previousBtcRow?.marketCapUsd
5169
5366
  );
5170
- const previousEthMarketCapUsd = toFiniteNumberOrNull2(
5367
+ const previousEthMarketCapUsd = toFiniteNumberOrNull3(
5171
5368
  previousEthRow?.marketCapUsd
5172
5369
  );
5173
5370
  const ethBtcMarketCapRatio = safeDivide(ethMarketCapUsd, btcMarketCapUsd);
@@ -5176,16 +5373,16 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
5176
5373
  previousBtcMarketCapUsd
5177
5374
  );
5178
5375
  const ethBtcMarketCapRatioChange24hPct = ethBtcMarketCapRatio != null && previousEthBtcMarketCapRatio != null && previousEthBtcMarketCapRatio > 0 ? (ethBtcMarketCapRatio - previousEthBtcMarketCapRatio) / previousEthBtcMarketCapRatio : null;
5179
- const btcVolumeUsd = toFiniteNumberOrNull2(btcRow?.volumeUsd);
5180
- const ethVolumeUsd = toFiniteNumberOrNull2(ethRow?.volumeUsd);
5376
+ const btcVolumeUsd = toFiniteNumberOrNull3(btcRow?.volumeUsd);
5377
+ const ethVolumeUsd = toFiniteNumberOrNull3(ethRow?.volumeUsd);
5181
5378
  const referenceStale = btcRow?.stale === true || ethRow?.stale === true || !btcRow || !ethRow;
5182
- const btcDominanceChange24hPct = toFiniteNumberOrNull2(
5379
+ const btcDominanceChange24hPct = toFiniteNumberOrNull3(
5183
5380
  globalRow?.btcDominanceChange24hPct
5184
5381
  );
5185
- const altMarketCapChange24hPct = toFiniteNumberOrNull2(
5382
+ const altMarketCapChange24hPct = toFiniteNumberOrNull3(
5186
5383
  globalRow?.altMarketCapChange24hPct
5187
5384
  );
5188
- const altVolumeChange24hPct = toFiniteNumberOrNull2(
5385
+ const altVolumeChange24hPct = toFiniteNumberOrNull3(
5189
5386
  globalRow?.altVolumeChange24hPct
5190
5387
  );
5191
5388
  const altLiquidityRegime = globalRow ? toAltLiquidityRegime({
@@ -5196,19 +5393,19 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
5196
5393
  }) : "unknown";
5197
5394
  const exchangeLiquidityRegime = exchangeLiquidityRow ? toExchangeLiquidityRegime({
5198
5395
  stale: exchangeLiquidityRow.stale,
5199
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
5396
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
5200
5397
  exchangeLiquidityRow.totalVolumeChange24hPct
5201
5398
  ),
5202
5399
  fallback: exchangeLiquidityRow.liquidityRegime ?? "unknown"
5203
5400
  }) : "unknown";
5204
5401
  const cmc100Row = indexRows.get("cmc100") ?? null;
5205
5402
  const cmc20Row = indexRows.get("cmc20") ?? null;
5206
- const cmc100Value = toFiniteNumberOrNull2(cmc100Row?.value);
5207
- const cmc20Value = toFiniteNumberOrNull2(cmc20Row?.value);
5208
- const cmc100Change24hPct = toFiniteNumberOrNull2(
5403
+ const cmc100Value = toFiniteNumberOrNull3(cmc100Row?.value);
5404
+ const cmc20Value = toFiniteNumberOrNull3(cmc20Row?.value);
5405
+ const cmc100Change24hPct = toFiniteNumberOrNull3(
5209
5406
  cmc100Row?.valueChange24hPct
5210
5407
  );
5211
- const cmc20Change24hPct = toFiniteNumberOrNull2(cmc20Row?.valueChange24hPct);
5408
+ const cmc20Change24hPct = toFiniteNumberOrNull3(cmc20Row?.valueChange24hPct);
5212
5409
  const cmc20ToCmc100Ratio = safeDivide(cmc20Value, cmc100Value);
5213
5410
  const cmc20ToCmc100RatioChange24hPct = cmc20Change24hPct != null && cmc100Change24hPct != null ? (1 + cmc20Change24hPct) / (1 + cmc100Change24hPct) - 1 : null;
5214
5411
  const indexStale = cmc100Row?.stale === true || cmc20Row?.stale === true || !cmc100Row || !cmc20Row;
@@ -5232,41 +5429,41 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
5232
5429
  asOfTs: globalRow.ts.getTime(),
5233
5430
  ageMs: globalRow.ageMs,
5234
5431
  stale: globalRow.stale,
5235
- totalMarketCapUsd: toFiniteNumberOrNull2(
5432
+ totalMarketCapUsd: toFiniteNumberOrNull3(
5236
5433
  globalRow.totalMarketCapUsd
5237
5434
  ),
5238
- totalVolumeUsd: toFiniteNumberOrNull2(
5435
+ totalVolumeUsd: toFiniteNumberOrNull3(
5239
5436
  globalRow.totalVolumeUsd
5240
5437
  ),
5241
- totalVolumeReportedUsd: toFiniteNumberOrNull2(
5438
+ totalVolumeReportedUsd: toFiniteNumberOrNull3(
5242
5439
  globalRow.totalVolumeReportedUsd
5243
5440
  ),
5244
- altMarketCapUsd: toFiniteNumberOrNull2(
5441
+ altMarketCapUsd: toFiniteNumberOrNull3(
5245
5442
  globalRow.altMarketCapUsd
5246
5443
  ),
5247
- altVolumeUsd: toFiniteNumberOrNull2(globalRow.altVolumeUsd),
5248
- altVolumeReportedUsd: toFiniteNumberOrNull2(
5444
+ altVolumeUsd: toFiniteNumberOrNull3(globalRow.altVolumeUsd),
5445
+ altVolumeReportedUsd: toFiniteNumberOrNull3(
5249
5446
  globalRow.altVolumeReportedUsd
5250
5447
  ),
5251
- btcDominancePct: toFiniteNumberOrNull2(
5448
+ btcDominancePct: toFiniteNumberOrNull3(
5252
5449
  globalRow.btcDominancePct
5253
5450
  ),
5254
- ethDominancePct: toFiniteNumberOrNull2(
5451
+ ethDominancePct: toFiniteNumberOrNull3(
5255
5452
  globalRow.ethDominancePct
5256
5453
  ),
5257
5454
  btcDominanceChange24hPct,
5258
- ethDominanceChange24hPct: toFiniteNumberOrNull2(
5455
+ ethDominanceChange24hPct: toFiniteNumberOrNull3(
5259
5456
  globalRow.ethDominanceChange24hPct
5260
5457
  ),
5261
5458
  altMarketCapChange24hPct,
5262
5459
  altVolumeChange24hPct,
5263
- activeCryptocurrencies: toFiniteNumberOrNull2(
5460
+ activeCryptocurrencies: toFiniteNumberOrNull3(
5264
5461
  globalRow.activeCryptocurrencies
5265
5462
  ),
5266
- activeExchanges: toFiniteNumberOrNull2(
5463
+ activeExchanges: toFiniteNumberOrNull3(
5267
5464
  globalRow.activeExchanges
5268
5465
  ),
5269
- activeMarketPairs: toFiniteNumberOrNull2(
5466
+ activeMarketPairs: toFiniteNumberOrNull3(
5270
5467
  globalRow.activeMarketPairs
5271
5468
  ),
5272
5469
  altLiquidityRegime
@@ -5311,22 +5508,22 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
5311
5508
  asOfTs: exchangeLiquidityRow.ts.getTime(),
5312
5509
  ageMs: exchangeLiquidityRow.ageMs,
5313
5510
  stale: exchangeLiquidityRow.stale,
5314
- exchangesCount: toFiniteNumberOrNull2(
5511
+ exchangesCount: toFiniteNumberOrNull3(
5315
5512
  exchangeLiquidityRow.exchangesCount
5316
5513
  ),
5317
- totalVolumeUsd: toFiniteNumberOrNull2(
5514
+ totalVolumeUsd: toFiniteNumberOrNull3(
5318
5515
  exchangeLiquidityRow.totalVolumeUsd
5319
5516
  ),
5320
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
5517
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
5321
5518
  exchangeLiquidityRow.totalVolumeChange24hPct
5322
5519
  ),
5323
- binanceVolumeUsd: toFiniteNumberOrNull2(
5520
+ binanceVolumeUsd: toFiniteNumberOrNull3(
5324
5521
  exchangeLiquidityRow.binanceVolumeUsd
5325
5522
  ),
5326
- binanceVolumeShare: toFiniteNumberOrNull2(
5523
+ binanceVolumeShare: toFiniteNumberOrNull3(
5327
5524
  exchangeLiquidityRow.binanceVolumeShare
5328
5525
  ),
5329
- topExchangeVolumeShare: toFiniteNumberOrNull2(
5526
+ topExchangeVolumeShare: toFiniteNumberOrNull3(
5330
5527
  exchangeLiquidityRow.topExchangeVolumeShare
5331
5528
  ),
5332
5529
  liquidityRegime: exchangeLiquidityRegime
@@ -5339,11 +5536,11 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
5339
5536
  asOfTs: fearGreedRow.ts.getTime(),
5340
5537
  ageMs: fearGreedRow.ageMs,
5341
5538
  stale: fearGreedRow.stale,
5342
- value: toFiniteNumberOrNull2(fearGreedRow.value),
5343
- valueChange24h: toFiniteNumberOrNull2(
5539
+ value: toFiniteNumberOrNull3(fearGreedRow.value),
5540
+ valueChange24h: toFiniteNumberOrNull3(
5344
5541
  fearGreedRow.valueChange24h
5345
5542
  ),
5346
- valueChange7d: toFiniteNumberOrNull2(
5543
+ valueChange7d: toFiniteNumberOrNull3(
5347
5544
  fearGreedRow.valueChange7d
5348
5545
  ),
5349
5546
  classification: fearGreedRow.classification ?? "Unknown",
@@ -5363,13 +5560,13 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
5363
5560
  cmc100Value,
5364
5561
  cmc100Change24hPct,
5365
5562
  cmc100TopConstituentSymbol: cmc100Row?.topConstituentSymbol ?? null,
5366
- cmc100TopConstituentWeightPct: toFiniteNumberOrNull2(
5563
+ cmc100TopConstituentWeightPct: toFiniteNumberOrNull3(
5367
5564
  cmc100Row?.topConstituentWeightPct
5368
5565
  ),
5369
5566
  cmc20Value,
5370
5567
  cmc20Change24hPct,
5371
5568
  cmc20TopConstituentSymbol: cmc20Row?.topConstituentSymbol ?? null,
5372
- cmc20TopConstituentWeightPct: toFiniteNumberOrNull2(
5569
+ cmc20TopConstituentWeightPct: toFiniteNumberOrNull3(
5373
5570
  cmc20Row?.topConstituentWeightPct
5374
5571
  ),
5375
5572
  cmc20ToCmc100Ratio,
@@ -5387,7 +5584,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
5387
5584
  throw error;
5388
5585
  }
5389
5586
  coinMarketCapContextUnavailable = true;
5390
- import_logger7.logger.warn(
5587
+ import_logger8.logger.warn(
5391
5588
  "CoinMarketCap context disabled after Timescale read failure: %s",
5392
5589
  String(error)
5393
5590
  );
@@ -5401,7 +5598,7 @@ var import_data3 = require("@tradejs/core/data");
5401
5598
  var import_strategies3 = require("@tradejs/core/strategies");
5402
5599
  var import_constants4 = require("@tradejs/core/constants");
5403
5600
  var import_derivatives = require("@tradejs/infra/timescale/derivatives");
5404
- var import_logger8 = require("@tradejs/infra/logger");
5601
+ var import_logger9 = require("@tradejs/infra/logger");
5405
5602
  var STORED_INTERVALS = ["15m", "1h"];
5406
5603
  var CONTEXT_INTERVALS = ["15m", "1h"];
5407
5604
  var DEFAULT_LOOKBACK_HOURS = 48;
@@ -5456,7 +5653,7 @@ var resolvePrimaryReferenceSymbol2 = () => PRIMARY_DERIVATIVES_REFERENCE_SYMBOL;
5456
5653
  var resolveSecondaryReferenceSymbol = () => SECONDARY_DERIVATIVES_REFERENCE_SYMBOL;
5457
5654
  var getPrimaryIntervalContext = (context) => context?.intervals["15m"] ?? context?.intervals["1h"] ?? null;
5458
5655
  var hasDerivativesSymbolData = (context) => Object.keys(context.intervals).length > 0 && !context.summary.riskFlags.includes("missing_derivatives");
5459
- var toFiniteNumberOrNull3 = (value) => {
5656
+ var toFiniteNumberOrNull4 = (value) => {
5460
5657
  if (typeof value === "number" && Number.isFinite(value)) return value;
5461
5658
  if (typeof value === "string" && value.trim()) {
5462
5659
  const parsed = Number(value);
@@ -5470,8 +5667,8 @@ var roundNullable = (value, digits = 4) => {
5470
5667
  return Math.round(value * multiplier) / multiplier;
5471
5668
  };
5472
5669
  var deltaNullable = (targetValue, referenceValue) => {
5473
- const target = toFiniteNumberOrNull3(targetValue);
5474
- const reference = toFiniteNumberOrNull3(referenceValue);
5670
+ const target = toFiniteNumberOrNull4(targetValue);
5671
+ const reference = toFiniteNumberOrNull4(referenceValue);
5475
5672
  return target == null || reference == null ? null : roundNullable(target - reference);
5476
5673
  };
5477
5674
  var buildTargetDerivedContext = (params) => {
@@ -5633,7 +5830,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
5633
5830
  throw error;
5634
5831
  }
5635
5832
  derivativesContextUnavailable = true;
5636
- import_logger8.logger.warn(
5833
+ import_logger9.logger.warn(
5637
5834
  "Derivatives context disabled after Timescale read failure: %s",
5638
5835
  String(error)
5639
5836
  );
@@ -5645,7 +5842,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
5645
5842
  var import_data4 = require("@tradejs/core/data");
5646
5843
  var import_strategies4 = require("@tradejs/core/strategies");
5647
5844
  var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWhales");
5648
- var import_logger9 = require("@tradejs/infra/logger");
5845
+ var import_logger10 = require("@tradejs/infra/logger");
5649
5846
 
5650
5847
  // src/hyperliquidWhaleUniverse.ts
5651
5848
  var import_node_crypto3 = require("crypto");
@@ -6165,7 +6362,7 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
6165
6362
  throw error;
6166
6363
  }
6167
6364
  hyperliquidWhaleContextUnavailable = true;
6168
- import_logger9.logger.warn(
6365
+ import_logger10.logger.warn(
6169
6366
  "Hyperliquid whale context disabled after Timescale read failure: %s",
6170
6367
  String(error)
6171
6368
  );
@@ -6248,7 +6445,7 @@ var runMarketContextStage = async ({
6248
6445
  elapsedMs: Date.now() - startedAt
6249
6446
  };
6250
6447
  if (status === "timed_out") {
6251
- import_logger10.logger.warn(
6448
+ import_logger11.logger.warn(
6252
6449
  "Market context stage timed out: %s after %sms",
6253
6450
  stage,
6254
6451
  result.elapsedMs
@@ -6438,79 +6635,10 @@ var enrichSignalWithAi = async ({
6438
6635
  signal.aiAnalysis = analysis;
6439
6636
  return resolveAiQuality(analysis, direction);
6440
6637
  } catch (err) {
6441
- import_logger11.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
6638
+ import_logger12.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
6442
6639
  }
6443
6640
  return void 0;
6444
6641
  };
6445
- var toFiniteNumberOrNull4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
6446
- var getOrderArrivalSnapshot = async ({
6447
- connector,
6448
- symbol
6449
- }) => {
6450
- if (typeof connector.getTopOfBookTicker !== "function") {
6451
- return {
6452
- arrivalSnapshotTime: Date.now(),
6453
- arrivalSource: "unavailable",
6454
- bid: null,
6455
- ask: null,
6456
- arrivalMid: null,
6457
- spreadBps: null
6458
- };
6459
- }
6460
- try {
6461
- const ticker = await connector.getTopOfBookTicker(symbol);
6462
- const arrivalSnapshotTime = toFiniteNumberOrNull4(ticker?.timestamp);
6463
- const bid = toFiniteNumberOrNull4(ticker?.bidPrice);
6464
- const ask = toFiniteNumberOrNull4(ticker?.askPrice);
6465
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
6466
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
6467
- return {
6468
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
6469
- arrivalSource: "top_of_book",
6470
- bid,
6471
- ask,
6472
- arrivalMid,
6473
- spreadBps
6474
- };
6475
- } catch (error) {
6476
- import_logger11.logger.warn(
6477
- "runtime order arrival snapshot failed: %s %s",
6478
- symbol,
6479
- error?.message || String(error)
6480
- );
6481
- return {
6482
- arrivalSnapshotTime: Date.now(),
6483
- arrivalSource: "top_of_book_error",
6484
- bid: null,
6485
- ask: null,
6486
- arrivalMid: null,
6487
- spreadBps: null
6488
- };
6489
- }
6490
- };
6491
- var validateEntryProtectionAtArrival = ({
6492
- direction,
6493
- signalPrice,
6494
- bid,
6495
- ask,
6496
- arrivalMid,
6497
- takeProfits,
6498
- stopLossPrice
6499
- }) => {
6500
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
6501
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
6502
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
6503
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
6504
- }
6505
- for (const takeProfit of takeProfits) {
6506
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
6507
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
6508
- }
6509
- }
6510
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
6511
- throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
6512
- }
6513
- };
6514
6642
  var resolveRuntimeTelemetryQuality = ({
6515
6643
  signalClosePrice,
6516
6644
  arrivalMid,
@@ -6587,7 +6715,8 @@ var executeEntryOrder = async ({
6587
6715
  signal,
6588
6716
  beforePlaceOrder,
6589
6717
  recordRuntimeTrade = true,
6590
- leverage
6718
+ leverage,
6719
+ executionLimits
6591
6720
  }) => {
6592
6721
  await beforePlaceOrder?.();
6593
6722
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
@@ -6598,23 +6727,57 @@ var executeEntryOrder = async ({
6598
6727
  signal.orderQty = qty;
6599
6728
  signal.orderValue = qty * currentPrice;
6600
6729
  signal.orderFailureReason = void 0;
6601
- const arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
6602
- try {
6603
- validateEntryProtectionAtArrival({
6604
- direction,
6605
- signalPrice: currentPrice,
6606
- bid: arrivalSnapshot.bid,
6607
- ask: arrivalSnapshot.ask,
6608
- arrivalMid: arrivalSnapshot.arrivalMid,
6609
- takeProfits,
6610
- stopLossPrice
6611
- });
6612
- } catch (error) {
6613
- signal.orderStatus = "failed";
6614
- signal.orderFailureReason = error.message;
6615
- throw error;
6730
+ let arrivalSnapshot = {
6731
+ arrivalSnapshotTime: Date.now(),
6732
+ arrivalSource: "unavailable",
6733
+ bid: null,
6734
+ ask: null,
6735
+ arrivalMid: null,
6736
+ spreadBps: null
6737
+ };
6738
+ let orderSubmitTime = Date.now();
6739
+ const beforeSubmit = createEntrySubmissionValidation({
6740
+ connector,
6741
+ symbol,
6742
+ direction,
6743
+ takeProfits,
6744
+ stopLossPrice,
6745
+ limits: executionLimits,
6746
+ onValidated: (snapshot2, economics2) => {
6747
+ arrivalSnapshot = snapshot2;
6748
+ orderSubmitTime = Date.now();
6749
+ if (executionLimits?.maxLossValue != null) {
6750
+ signal.riskDecision = {
6751
+ stage: "order_plan",
6752
+ status: economics2.riskExceeded ? "warning" : "approved",
6753
+ reason: economics2.riskExceeded ? "ENTRY_RISK_EXCEEDS_MAX_LOSS_AT_SUBMISSION" : "PLANNED_LOSS_WITHIN_MAX_LOSS_VALUE",
6754
+ plannedLossValue: economics2.plannedLossValue,
6755
+ maxLossValue: executionLimits.maxLossValue,
6756
+ limitUtilization: economics2.plannedLossValue / executionLimits.maxLossValue,
6757
+ enforced: false
6758
+ };
6759
+ }
6760
+ }
6761
+ });
6762
+ if (!beforeSubmit) {
6763
+ arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
6764
+ try {
6765
+ validateEntryProtectionAtArrival({
6766
+ direction,
6767
+ signalPrice: currentPrice,
6768
+ bid: arrivalSnapshot.bid,
6769
+ ask: arrivalSnapshot.ask,
6770
+ arrivalMid: arrivalSnapshot.arrivalMid,
6771
+ takeProfits,
6772
+ stopLossPrice
6773
+ });
6774
+ } catch (error) {
6775
+ signal.orderStatus = "failed";
6776
+ signal.orderFailureReason = error.message;
6777
+ throw error;
6778
+ }
6779
+ orderSubmitTime = Date.now();
6616
6780
  }
6617
- const orderSubmitTime = Date.now();
6618
6781
  const orderPlaced = await connector.placeOrder({
6619
6782
  symbol,
6620
6783
  qty,
@@ -6625,7 +6788,8 @@ var executeEntryOrder = async ({
6625
6788
  direction,
6626
6789
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
6627
6790
  orderId,
6628
- signal
6791
+ signal,
6792
+ ...beforeSubmit ? { beforeSubmit } : {}
6629
6793
  });
6630
6794
  const orderAckTime = Date.now();
6631
6795
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -7454,7 +7618,7 @@ var canUseSharedReplayState = ({
7454
7618
  }) => (env === "BACKTEST" || env === "PARITY") && Boolean(sharedReplayKey);
7455
7619
 
7456
7620
  // src/strategy/runtimeExecution.ts
7457
- var import_logger12 = require("@tradejs/infra/logger");
7621
+ var import_logger13 = require("@tradejs/infra/logger");
7458
7622
  var import_types = require("@tradejs/types");
7459
7623
  var buildExitOrderSignal = ({
7460
7624
  strategyName,
@@ -7506,7 +7670,7 @@ var handleExitDecision = async ({
7506
7670
  deploymentId: connector.deploymentId
7507
7671
  });
7508
7672
  if (!activeTrade) {
7509
- import_logger12.logger.warn(
7673
+ import_logger13.logger.warn(
7510
7674
  "[%s] blocked closePosition for untracked runtime position: %s",
7511
7675
  strategyName ?? "unknown",
7512
7676
  symbol
@@ -7514,7 +7678,7 @@ var handleExitDecision = async ({
7514
7678
  return "CLOSE_BLOCKED_BY_UNTRACKED_POSITION";
7515
7679
  }
7516
7680
  if (!strategyName || activeTrade.strategy !== strategyName) {
7517
- import_logger12.logger.warn(
7681
+ import_logger13.logger.warn(
7518
7682
  "[%s] blocked closePosition for foreign runtime position: %s ownedBy=%s",
7519
7683
  strategyName ?? "unknown",
7520
7684
  symbol,
@@ -7567,7 +7731,7 @@ var handleExitDecision = async ({
7567
7731
  exitType: closedTrade?.exitType ?? "exit"
7568
7732
  });
7569
7733
  } catch (notificationError) {
7570
- import_logger12.logger.error(
7734
+ import_logger13.logger.error(
7571
7735
  "runtime close notification error: %s %s",
7572
7736
  symbol,
7573
7737
  notificationError
@@ -7581,7 +7745,7 @@ var handleExitDecision = async ({
7581
7745
  decision,
7582
7746
  market
7583
7747
  });
7584
- import_logger12.logger.error("close order error: %s %s", symbol, err);
7748
+ import_logger13.logger.error("close order error: %s %s", symbol, err);
7585
7749
  return "ORDER_ERROR";
7586
7750
  }
7587
7751
  return decision.code;
@@ -7608,7 +7772,7 @@ var handleProtectDecision = async ({
7608
7772
  decision,
7609
7773
  market
7610
7774
  });
7611
- import_logger12.logger.error("protect position error: %s %s", symbol, err);
7775
+ import_logger13.logger.error("protect position error: %s %s", symbol, err);
7612
7776
  return "ORDER_ERROR";
7613
7777
  }
7614
7778
  return decision.code;
@@ -7674,7 +7838,11 @@ var executeEntryDecision = async ({
7674
7838
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
7675
7839
  signal,
7676
7840
  beforePlaceOrder,
7677
- recordRuntimeTrade: recordRuntimeJournal
7841
+ recordRuntimeTrade: recordRuntimeJournal,
7842
+ executionLimits: resolveEntryExecutionLimits(
7843
+ hookCtx.strategyConfig,
7844
+ decision.entryContext.direction
7845
+ )
7678
7846
  });
7679
7847
  await invokeStageHooks(
7680
7848
  "afterPlaceOrder",
@@ -7696,19 +7864,32 @@ var executeEntryDecision = async ({
7696
7864
  return signal;
7697
7865
  }
7698
7866
  await beforePlaceOrder();
7699
- const arrivalSnapshot = await getOrderArrivalSnapshot({
7867
+ const beforeSubmit = createEntrySubmissionValidation({
7700
7868
  connector,
7701
- symbol
7702
- });
7703
- validateEntryProtectionAtArrival({
7869
+ symbol,
7704
7870
  direction: decision.entryContext.direction,
7705
- signalPrice: decision.entryContext.prices.currentPrice,
7706
- bid: arrivalSnapshot.bid,
7707
- ask: arrivalSnapshot.ask,
7708
- arrivalMid: arrivalSnapshot.arrivalMid,
7709
7871
  takeProfits: decision.orderPlan.takeProfits,
7710
- stopLossPrice: decision.orderPlan.stopLossPrice
7872
+ stopLossPrice: decision.orderPlan.stopLossPrice,
7873
+ limits: resolveEntryExecutionLimits(
7874
+ hookCtx.strategyConfig,
7875
+ decision.entryContext.direction
7876
+ )
7711
7877
  });
7878
+ if (!beforeSubmit) {
7879
+ const arrivalSnapshot = await getOrderArrivalSnapshot({
7880
+ connector,
7881
+ symbol
7882
+ });
7883
+ validateEntryProtectionAtArrival({
7884
+ direction: decision.entryContext.direction,
7885
+ signalPrice: decision.entryContext.prices.currentPrice,
7886
+ bid: arrivalSnapshot.bid,
7887
+ ask: arrivalSnapshot.ask,
7888
+ arrivalMid: arrivalSnapshot.arrivalMid,
7889
+ takeProfits: decision.orderPlan.takeProfits,
7890
+ stopLossPrice: decision.orderPlan.stopLossPrice
7891
+ });
7892
+ }
7712
7893
  const orderPlaced = await connector.placeOrder({
7713
7894
  symbol,
7714
7895
  qty: decision.orderPlan.qty,
@@ -7716,6 +7897,7 @@ var executeEntryDecision = async ({
7716
7897
  timestamp: decision.entryContext.timestamp,
7717
7898
  direction: decision.entryContext.direction,
7718
7899
  positionIntent: decision.orderPlan.positionIntent,
7900
+ ...beforeSubmit ? { beforeSubmit } : {},
7719
7901
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {}
7720
7902
  });
7721
7903
  if (!orderPlaced) {
@@ -7757,6 +7939,23 @@ var executeEntryDecision = async ({
7757
7939
  { decision, entry, market }
7758
7940
  );
7759
7941
  } catch (err) {
7942
+ if (err instanceof EntrySubmissionValidationError) {
7943
+ if (signal) {
7944
+ signal.orderStatus = "skipped";
7945
+ signal.orderSkipReason = err.message;
7946
+ signal.allocatorDecision = {
7947
+ stage: "runtime_admission",
7948
+ status: "rejected",
7949
+ reason: err.message
7950
+ };
7951
+ }
7952
+ import_logger13.logger.warn(
7953
+ "entry rejected before submission: %s %s",
7954
+ symbol,
7955
+ err.message
7956
+ );
7957
+ return signal ?? err.message;
7958
+ }
7760
7959
  if (signal) {
7761
7960
  signal.orderStatus = "failed";
7762
7961
  if (typeof signal.orderFailureReason !== "string" || !signal.orderFailureReason.trim()) {
@@ -7771,9 +7970,9 @@ var executeEntryDecision = async ({
7771
7970
  market
7772
7971
  });
7773
7972
  if (err?.message === import_types.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY) {
7774
- import_logger12.logger.warn("order warning: %s %s", symbol, err);
7973
+ import_logger13.logger.warn("order warning: %s %s", symbol, err);
7775
7974
  } else {
7776
- import_logger12.logger.error("order error: %s %s", symbol, err);
7975
+ import_logger13.logger.error("order error: %s %s", symbol, err);
7777
7976
  }
7778
7977
  return signal ?? "ORDER_ERROR";
7779
7978
  }
@@ -7881,6 +8080,9 @@ var createStrategyRuntime = ({
7881
8080
  });
7882
8081
  const strategyPolicyProfile = getPolicyProfile();
7883
8082
  const indicatorPeriods = (0, import_strategies6.buildDefaultIndicatorPeriods)(config);
8083
+ const strategyConfig = { ...config };
8084
+ delete strategyConfig.RECORD_RUNTIME_TRADES;
8085
+ delete strategyConfig.SIMULATE_ORDERS;
7884
8086
  const hookBase = {
7885
8087
  connector,
7886
8088
  strategyName,
@@ -7891,7 +8093,7 @@ var createStrategyRuntime = ({
7891
8093
  accountId,
7892
8094
  deploymentId,
7893
8095
  policyProfileId: strategyPolicyProfile?.id ?? requestedPolicyProfileId,
7894
- strategyConfig: config,
8096
+ strategyConfig,
7895
8097
  env,
7896
8098
  isConfigFromBacktest
7897
8099
  };
@@ -7938,7 +8140,7 @@ var createStrategyRuntime = ({
7938
8140
  try {
7939
8141
  await projectHook(errorParams);
7940
8142
  } catch (hookError) {
7941
- import_logger13.logger.error(
8143
+ import_logger14.logger.error(
7942
8144
  "project hook onRuntimeError failed: %s %s",
7943
8145
  strategyName,
7944
8146
  hookError
@@ -7952,21 +8154,21 @@ var createStrategyRuntime = ({
7952
8154
  try {
7953
8155
  await onRuntimeError(errorParams);
7954
8156
  } catch (hookError) {
7955
- import_logger13.logger.error(
8157
+ import_logger14.logger.error(
7956
8158
  "runtime hook onRuntimeError failed: %s %s",
7957
8159
  strategyName,
7958
8160
  hookError
7959
8161
  );
7960
8162
  }
7961
8163
  };
7962
- const invokeHook = async (stage, hook, params, errorContext = {}) => {
8164
+ const invokeHook = async (stage, hook, params, errorContext = {}, failureResult) => {
7963
8165
  if (!hook) {
7964
8166
  return void 0;
7965
8167
  }
7966
8168
  try {
7967
8169
  return await hook(params);
7968
8170
  } catch (error) {
7969
- import_logger13.logger.error(
8171
+ import_logger14.logger.error(
7970
8172
  'strategy hook "%s" failed for %s: %s',
7971
8173
  stage,
7972
8174
  strategyName,
@@ -7979,17 +8181,18 @@ var createStrategyRuntime = ({
7979
8181
  entry: errorContext.entry,
7980
8182
  market: errorContext.market
7981
8183
  });
7982
- return void 0;
8184
+ return failureResult;
7983
8185
  }
7984
8186
  };
7985
- const invokeProjectHooks = async (stage, params, errorContext = {}) => {
8187
+ const invokeProjectHooks = async (stage, params, errorContext = {}, failureResult) => {
7986
8188
  const results = [];
7987
8189
  for (const hook of getProjectHookList(stage)) {
7988
8190
  const result = await invokeHook(
7989
8191
  stage,
7990
8192
  hook,
7991
8193
  params,
7992
- errorContext
8194
+ errorContext,
8195
+ failureResult
7993
8196
  );
7994
8197
  if (result !== void 0) {
7995
8198
  results.push(result);
@@ -8004,10 +8207,12 @@ var createStrategyRuntime = ({
8004
8207
  return invokeHook(stage, hook, params, errorContext);
8005
8208
  };
8006
8209
  const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
8210
+ const failureResult = stage === "beforeEntryGate" ? { allow: false, reason: "ERROR" } : void 0;
8007
8211
  const projectResults = await invokeProjectHooks(
8008
8212
  stage,
8009
8213
  params,
8010
- errorContext
8214
+ errorContext,
8215
+ failureResult
8011
8216
  );
8012
8217
  const projectBlock = projectResults.find(
8013
8218
  (result) => result?.allow === false
@@ -8019,7 +8224,8 @@ var createStrategyRuntime = ({
8019
8224
  stage,
8020
8225
  hook,
8021
8226
  params,
8022
- errorContext
8227
+ errorContext,
8228
+ failureResult
8023
8229
  );
8024
8230
  };
8025
8231
  const applyProjectAfterCoreDecisionHooks = async ({
@@ -8165,11 +8371,8 @@ var createStrategyRuntime = ({
8165
8371
  return enrichedBaseContext;
8166
8372
  }
8167
8373
  });
8168
- const coreConfig = { ...config };
8169
- delete coreConfig.RECORD_RUNTIME_TRADES;
8170
- delete coreConfig.SIMULATE_ORDERS;
8171
8374
  const core = await createCore({
8172
- config: coreConfig,
8375
+ config: strategyConfig,
8173
8376
  data,
8174
8377
  strategyApi,
8175
8378
  indicatorsState
@@ -9520,7 +9723,7 @@ var loadExchangeRange = async ({
9520
9723
  } catch (error) {
9521
9724
  const message = error?.message || String(error);
9522
9725
  errors?.push(`${label}: ${message}`);
9523
- import_logger14.logger.warn("strategies runtime: %s failed: %s", label, message);
9726
+ import_logger15.logger.warn("strategies runtime: %s failed: %s", label, message);
9524
9727
  return [];
9525
9728
  }
9526
9729
  };
@@ -9560,7 +9763,7 @@ var loadClosedPnlRows = async ({
9560
9763
  } catch (error) {
9561
9764
  const message = error?.message || String(error);
9562
9765
  errors?.push(`getClosedPnl: ${message}`);
9563
- import_logger14.logger.warn("strategies runtime: getClosedPnl failed: %s", message);
9766
+ import_logger15.logger.warn("strategies runtime: getClosedPnl failed: %s", message);
9564
9767
  return [];
9565
9768
  }
9566
9769
  };
@@ -9593,7 +9796,7 @@ var loadExchangeEntryRows = async ({
9593
9796
  } catch (error) {
9594
9797
  const message = error?.message || String(error);
9595
9798
  errors?.push(`getEntryExecutions: ${message}`);
9596
- import_logger14.logger.warn("strategies runtime: getEntryExecutions failed: %s", message);
9799
+ import_logger15.logger.warn("strategies runtime: getEntryExecutions failed: %s", message);
9597
9800
  return [];
9598
9801
  }
9599
9802
  };
@@ -9609,7 +9812,7 @@ var loadOpenPositions = async (connector, errors) => {
9609
9812
  } catch (error) {
9610
9813
  const message = error?.message || String(error);
9611
9814
  errors?.push(`getOpenPositionPnl: ${message}`);
9612
- import_logger14.logger.warn("strategies runtime: getOpenPositionPnl failed: %s", message);
9815
+ import_logger15.logger.warn("strategies runtime: getOpenPositionPnl failed: %s", message);
9613
9816
  return { positions: [], reliable: false };
9614
9817
  }
9615
9818
  };