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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,6 +16,206 @@ import {
16
16
  assessSignalWithJev
17
17
  } from "./chunk-YR55Q355.mjs";
18
18
 
19
+ // src/strategy/entrySubmissionValidation.ts
20
+ import { logger } from "@tradejs/infra/logger";
21
+
22
+ // src/strategy/entryExecutionValidation.ts
23
+ var EntrySubmissionValidationError = class extends Error {
24
+ constructor() {
25
+ super(...arguments);
26
+ this.name = "EntrySubmissionValidationError";
27
+ }
28
+ };
29
+ var resolveEntryExecutionLimits = (config, direction) => {
30
+ const side = config[direction];
31
+ const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
32
+ const maximum = config.MAX_LOSS_VALUE;
33
+ if (minimum == null && maximum == null) return void 0;
34
+ const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
35
+ const maxLossValue = maximum == null ? void 0 : Number(maximum);
36
+ const feeRate = Number(config.RISK_FEE_RATE ?? 0);
37
+ const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
38
+ const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
39
+ const slippageBps = slippage + impact;
40
+ 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)
41
+ throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
42
+ return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
43
+ };
44
+ var validateEntryExecution = ({
45
+ direction,
46
+ price,
47
+ qty,
48
+ stopLossPrice,
49
+ takeProfitPrice,
50
+ limits
51
+ }) => {
52
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
53
+ throw new EntrySubmissionValidationError(
54
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
55
+ );
56
+ const side = direction === "LONG" ? 1 : -1;
57
+ if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
58
+ throw new EntrySubmissionValidationError(
59
+ "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
60
+ );
61
+ const slippage = limits.slippageBps / 1e4;
62
+ const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
63
+ const entryCost = cost(price, side);
64
+ const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
65
+ const rewardPerUnit = Math.max(
66
+ 0,
67
+ Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
68
+ );
69
+ const netRiskRatio = rewardPerUnit / lossPerUnit;
70
+ const plannedLossValue = qty * lossPerUnit;
71
+ if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
72
+ throw new EntrySubmissionValidationError(
73
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
74
+ );
75
+ if (limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio)
76
+ throw new EntrySubmissionValidationError(
77
+ "ENTRY_RR_BELOW_MIN_AT_SUBMISSION"
78
+ );
79
+ if (limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9))
80
+ throw new EntrySubmissionValidationError(
81
+ "ENTRY_RISK_EXCEEDS_MAX_LOSS_AT_SUBMISSION"
82
+ );
83
+ return { netRiskRatio, plannedLossValue };
84
+ };
85
+
86
+ // src/strategy/entrySubmissionValidation.ts
87
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
88
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
89
+ var getOrderArrivalSnapshot = async ({
90
+ connector,
91
+ symbol,
92
+ requireTimestamp = false
93
+ }) => {
94
+ if (typeof connector.getTopOfBookTicker !== "function") {
95
+ return {
96
+ arrivalSnapshotTime: Date.now(),
97
+ arrivalSource: "unavailable",
98
+ bid: null,
99
+ ask: null,
100
+ arrivalMid: null,
101
+ spreadBps: null
102
+ };
103
+ }
104
+ try {
105
+ const ticker = await connector.getTopOfBookTicker(symbol);
106
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
107
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
108
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
109
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
110
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
111
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
112
+ return {
113
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
114
+ arrivalSource: "top_of_book",
115
+ bid,
116
+ ask,
117
+ arrivalMid,
118
+ spreadBps
119
+ };
120
+ } catch (error) {
121
+ logger.warn(
122
+ "runtime order arrival snapshot failed: %s %s",
123
+ symbol,
124
+ error?.message || String(error)
125
+ );
126
+ return {
127
+ arrivalSnapshotTime: Date.now(),
128
+ arrivalSource: "top_of_book_error",
129
+ bid: null,
130
+ ask: null,
131
+ arrivalMid: null,
132
+ spreadBps: null
133
+ };
134
+ }
135
+ };
136
+ var validateEntryProtectionAtArrival = ({
137
+ direction,
138
+ signalPrice,
139
+ bid,
140
+ ask,
141
+ arrivalMid,
142
+ takeProfits,
143
+ stopLossPrice
144
+ }) => {
145
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
146
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
147
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
148
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
149
+ }
150
+ for (const takeProfit of takeProfits) {
151
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
152
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
153
+ }
154
+ }
155
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
156
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
157
+ }
158
+ };
159
+ var createEntrySubmissionValidation = ({
160
+ connector,
161
+ symbol,
162
+ direction,
163
+ takeProfits,
164
+ stopLossPrice,
165
+ limits,
166
+ onValidated
167
+ }) => {
168
+ if (!limits) return void 0;
169
+ if (!connector.capabilities?.orderSubmissionValidation)
170
+ throw new EntrySubmissionValidationError(
171
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
172
+ );
173
+ return async ({ qty, executionPrice }) => {
174
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
175
+ const snapshot2 = simulated && executionPrice != null ? {
176
+ arrivalSnapshotTime: Date.now(),
177
+ arrivalSource: "simulation",
178
+ bid: executionPrice,
179
+ ask: executionPrice,
180
+ arrivalMid: executionPrice,
181
+ spreadBps: 0
182
+ } : await getOrderArrivalSnapshot({
183
+ connector,
184
+ symbol,
185
+ requireTimestamp: true
186
+ });
187
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
188
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
189
+ throw new EntrySubmissionValidationError(
190
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
191
+ );
192
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
193
+ throw new EntrySubmissionValidationError(
194
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
195
+ );
196
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
197
+ validateEntryProtectionAtArrival({
198
+ direction,
199
+ signalPrice: price,
200
+ bid: snapshot2.bid,
201
+ ask: snapshot2.ask,
202
+ arrivalMid: snapshot2.arrivalMid,
203
+ takeProfits,
204
+ stopLossPrice
205
+ });
206
+ const prices = takeProfits.map((tp) => tp.price);
207
+ const economics = validateEntryExecution({
208
+ direction,
209
+ price,
210
+ qty,
211
+ stopLossPrice,
212
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
213
+ limits
214
+ });
215
+ onValidated?.(snapshot2, economics);
216
+ };
217
+ };
218
+
19
219
  // src/binanceBreadthUniverses.ts
20
220
  import { createHash } from "crypto";
21
221
 
@@ -328,7 +528,7 @@ import {
328
528
  getLatestMarketBreadth,
329
529
  getLatestMarketTradeFlow
330
530
  } from "@tradejs/infra/timescale/marketContext";
331
- import { logger } from "@tradejs/infra/logger";
531
+ import { logger as logger2 } from "@tradejs/infra/logger";
332
532
  import { refreshSignalBaseContextGateFeatures } from "@tradejs/core/strategies";
333
533
 
334
534
  // src/strategyHelpers/marketContextErrors.ts
@@ -399,7 +599,7 @@ var parseEnabledFlag = (value, env) => {
399
599
  if (normalized === "live") return env !== "BACKTEST";
400
600
  return false;
401
601
  };
402
- var toFiniteNumberOrNull = (value) => {
602
+ var toFiniteNumberOrNull2 = (value) => {
403
603
  const numeric = typeof value === "number" ? value : Number(value);
404
604
  return Number.isFinite(numeric) ? numeric : null;
405
605
  };
@@ -664,14 +864,14 @@ var toTradeFlowContext = (row, interval) => row ? {
664
864
  asOfTs: row.ts.getTime(),
665
865
  ageMs: row.ageMs,
666
866
  stale: row.stale,
667
- trades: toFiniteNumberOrNull(row.trades),
668
- buyPressurePct: toFiniteNumberOrNull(row.buyPressurePct),
669
- buyBaseVolume: toFiniteNumberOrNull(row.buyBaseVolume),
670
- sellBaseVolume: toFiniteNumberOrNull(row.sellBaseVolume),
671
- buyQuoteVolume: toFiniteNumberOrNull(row.buyQuoteVolume),
672
- sellQuoteVolume: toFiniteNumberOrNull(row.sellQuoteVolume),
673
- netBaseDelta: toFiniteNumberOrNull(row.netBaseDelta),
674
- netQuoteDelta: toFiniteNumberOrNull(row.netQuoteDelta)
867
+ trades: toFiniteNumberOrNull2(row.trades),
868
+ buyPressurePct: toFiniteNumberOrNull2(row.buyPressurePct),
869
+ buyBaseVolume: toFiniteNumberOrNull2(row.buyBaseVolume),
870
+ sellBaseVolume: toFiniteNumberOrNull2(row.sellBaseVolume),
871
+ buyQuoteVolume: toFiniteNumberOrNull2(row.buyQuoteVolume),
872
+ sellQuoteVolume: toFiniteNumberOrNull2(row.sellQuoteVolume),
873
+ netBaseDelta: toFiniteNumberOrNull2(row.netBaseDelta),
874
+ netQuoteDelta: toFiniteNumberOrNull2(row.netQuoteDelta)
675
875
  } : null;
676
876
  var getCachedReferenceRows = ({
677
877
  referenceSymbols,
@@ -760,16 +960,16 @@ var toMarketBreadthContext = (breadth, interval) => ({
760
960
  asOfTs: breadth.ts.getTime(),
761
961
  ageMs: breadth.ageMs,
762
962
  stale: breadth.stale,
763
- symbolsCount: toFiniteNumberOrNull(breadth.symbolsCount),
764
- advancers: toFiniteNumberOrNull(breadth.advancers),
765
- decliners: toFiniteNumberOrNull(breadth.decliners),
766
- unchanged: toFiniteNumberOrNull(breadth.unchanged),
767
- advanceDeclineRatio: toFiniteNumberOrNull(breadth.advanceDeclineRatio),
768
- pctAboveMa20: toFiniteNumberOrNull(breadth.pctAboveMa20),
769
- pctAboveMa50: toFiniteNumberOrNull(breadth.pctAboveMa50),
770
- equalWeightedReturn: toFiniteNumberOrNull(breadth.equalWeightedReturn),
771
- volumeWeightedReturn: toFiniteNumberOrNull(breadth.volumeWeightedReturn),
772
- dispersion: toFiniteNumberOrNull(breadth.dispersion)
963
+ symbolsCount: toFiniteNumberOrNull2(breadth.symbolsCount),
964
+ advancers: toFiniteNumberOrNull2(breadth.advancers),
965
+ decliners: toFiniteNumberOrNull2(breadth.decliners),
966
+ unchanged: toFiniteNumberOrNull2(breadth.unchanged),
967
+ advanceDeclineRatio: toFiniteNumberOrNull2(breadth.advanceDeclineRatio),
968
+ pctAboveMa20: toFiniteNumberOrNull2(breadth.pctAboveMa20),
969
+ pctAboveMa50: toFiniteNumberOrNull2(breadth.pctAboveMa50),
970
+ equalWeightedReturn: toFiniteNumberOrNull2(breadth.equalWeightedReturn),
971
+ volumeWeightedReturn: toFiniteNumberOrNull2(breadth.volumeWeightedReturn),
972
+ dispersion: toFiniteNumberOrNull2(breadth.dispersion)
773
973
  });
774
974
  var enrichSignalWithBinanceMarketContext = async (params) => {
775
975
  const {
@@ -870,46 +1070,46 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
870
1070
  asOfTs: primaryBreadth.ts.getTime(),
871
1071
  ageMs: primaryBreadth.ageMs,
872
1072
  stale: primaryBreadth.stale,
873
- btcReturn1h: toFiniteNumberOrNull(
1073
+ btcReturn1h: toFiniteNumberOrNull2(
874
1074
  primaryBreadth.btcReturn1h
875
1075
  ),
876
- btcReturn4h: toFiniteNumberOrNull(
1076
+ btcReturn4h: toFiniteNumberOrNull2(
877
1077
  primaryBreadth.btcReturn4h
878
1078
  ),
879
- btcReturn24h: toFiniteNumberOrNull(
1079
+ btcReturn24h: toFiniteNumberOrNull2(
880
1080
  primaryBreadth.btcReturn24h
881
1081
  ),
882
- altBasketReturn1h: toFiniteNumberOrNull(
1082
+ altBasketReturn1h: toFiniteNumberOrNull2(
883
1083
  primaryBreadth.altBasketReturn1h
884
1084
  ),
885
- altBasketReturn4h: toFiniteNumberOrNull(
1085
+ altBasketReturn4h: toFiniteNumberOrNull2(
886
1086
  primaryBreadth.altBasketReturn4h
887
1087
  ),
888
- altBasketReturn24h: toFiniteNumberOrNull(
1088
+ altBasketReturn24h: toFiniteNumberOrNull2(
889
1089
  primaryBreadth.altBasketReturn24h
890
1090
  ),
891
- btcVsAltReturn1h: toFiniteNumberOrNull(
1091
+ btcVsAltReturn1h: toFiniteNumberOrNull2(
892
1092
  primaryBreadth.btcVsAltReturn1h
893
1093
  ),
894
- btcVsAltReturn4h: toFiniteNumberOrNull(
1094
+ btcVsAltReturn4h: toFiniteNumberOrNull2(
895
1095
  primaryBreadth.btcVsAltReturn4h
896
1096
  ),
897
- btcVsAltReturn24h: toFiniteNumberOrNull(
1097
+ btcVsAltReturn24h: toFiniteNumberOrNull2(
898
1098
  primaryBreadth.btcVsAltReturn24h
899
1099
  ),
900
- btcTurnoverShare1h: toFiniteNumberOrNull(
1100
+ btcTurnoverShare1h: toFiniteNumberOrNull2(
901
1101
  primaryBreadth.btcTurnoverShare1h
902
1102
  ),
903
- btcTurnoverShare24h: toFiniteNumberOrNull(
1103
+ btcTurnoverShare24h: toFiniteNumberOrNull2(
904
1104
  primaryBreadth.btcTurnoverShare24h
905
1105
  ),
906
- btcTurnoverShareChange24h: toFiniteNumberOrNull(
1106
+ btcTurnoverShareChange24h: toFiniteNumberOrNull2(
907
1107
  primaryBreadth.btcTurnoverShareChange24h
908
1108
  ),
909
- altVolToBtcVol24h: toFiniteNumberOrNull(
1109
+ altVolToBtcVol24h: toFiniteNumberOrNull2(
910
1110
  primaryBreadth.altVolToBtcVol24h
911
1111
  ),
912
- altDispersion24h: toFiniteNumberOrNull(
1112
+ altDispersion24h: toFiniteNumberOrNull2(
913
1113
  primaryBreadth.altDispersion24h
914
1114
  ),
915
1115
  regime: primaryBreadth.btcAltRegime ?? "unknown"
@@ -926,7 +1126,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
926
1126
  throw error;
927
1127
  }
928
1128
  binanceMarketContextUnavailable = true;
929
- logger.warn(
1129
+ logger2.warn(
930
1130
  "Binance market context disabled after Timescale read failure: %s",
931
1131
  String(error)
932
1132
  );
@@ -936,7 +1136,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
936
1136
 
937
1137
  // src/strategyHelpers/coinMarketCapContext.ts
938
1138
  import { refreshSignalBaseContextGateFeatures as refreshSignalBaseContextGateFeatures2 } from "@tradejs/core/strategies";
939
- import { logger as logger2 } from "@tradejs/infra/logger";
1139
+ import { logger as logger3 } from "@tradejs/infra/logger";
940
1140
  import {
941
1141
  getLatestMarketCmcExchangeLiquidityContext,
942
1142
  getLatestMarketCmcFearGreedContext,
@@ -980,7 +1180,7 @@ var asInt = (value, fallback) => {
980
1180
  const parsed = Number.parseInt(String(value ?? ""), 10);
981
1181
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
982
1182
  };
983
- var toFiniteNumberOrNull2 = (value) => {
1183
+ var toFiniteNumberOrNull3 = (value) => {
984
1184
  const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
985
1185
  return Number.isFinite(numeric) ? numeric : null;
986
1186
  };
@@ -1214,12 +1414,12 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1214
1414
  const ethRow = references.get("ETHUSDT") ?? null;
1215
1415
  const previousBtcRow = previousReferences.get("BTCUSDT") ?? null;
1216
1416
  const previousEthRow = previousReferences.get("ETHUSDT") ?? null;
1217
- const btcMarketCapUsd = toFiniteNumberOrNull2(btcRow?.marketCapUsd);
1218
- const ethMarketCapUsd = toFiniteNumberOrNull2(ethRow?.marketCapUsd);
1219
- const previousBtcMarketCapUsd = toFiniteNumberOrNull2(
1417
+ const btcMarketCapUsd = toFiniteNumberOrNull3(btcRow?.marketCapUsd);
1418
+ const ethMarketCapUsd = toFiniteNumberOrNull3(ethRow?.marketCapUsd);
1419
+ const previousBtcMarketCapUsd = toFiniteNumberOrNull3(
1220
1420
  previousBtcRow?.marketCapUsd
1221
1421
  );
1222
- const previousEthMarketCapUsd = toFiniteNumberOrNull2(
1422
+ const previousEthMarketCapUsd = toFiniteNumberOrNull3(
1223
1423
  previousEthRow?.marketCapUsd
1224
1424
  );
1225
1425
  const ethBtcMarketCapRatio = safeDivide(ethMarketCapUsd, btcMarketCapUsd);
@@ -1228,16 +1428,16 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1228
1428
  previousBtcMarketCapUsd
1229
1429
  );
1230
1430
  const ethBtcMarketCapRatioChange24hPct = ethBtcMarketCapRatio != null && previousEthBtcMarketCapRatio != null && previousEthBtcMarketCapRatio > 0 ? (ethBtcMarketCapRatio - previousEthBtcMarketCapRatio) / previousEthBtcMarketCapRatio : null;
1231
- const btcVolumeUsd = toFiniteNumberOrNull2(btcRow?.volumeUsd);
1232
- const ethVolumeUsd = toFiniteNumberOrNull2(ethRow?.volumeUsd);
1431
+ const btcVolumeUsd = toFiniteNumberOrNull3(btcRow?.volumeUsd);
1432
+ const ethVolumeUsd = toFiniteNumberOrNull3(ethRow?.volumeUsd);
1233
1433
  const referenceStale = btcRow?.stale === true || ethRow?.stale === true || !btcRow || !ethRow;
1234
- const btcDominanceChange24hPct = toFiniteNumberOrNull2(
1434
+ const btcDominanceChange24hPct = toFiniteNumberOrNull3(
1235
1435
  globalRow?.btcDominanceChange24hPct
1236
1436
  );
1237
- const altMarketCapChange24hPct = toFiniteNumberOrNull2(
1437
+ const altMarketCapChange24hPct = toFiniteNumberOrNull3(
1238
1438
  globalRow?.altMarketCapChange24hPct
1239
1439
  );
1240
- const altVolumeChange24hPct = toFiniteNumberOrNull2(
1440
+ const altVolumeChange24hPct = toFiniteNumberOrNull3(
1241
1441
  globalRow?.altVolumeChange24hPct
1242
1442
  );
1243
1443
  const altLiquidityRegime = globalRow ? toAltLiquidityRegime({
@@ -1248,19 +1448,19 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1248
1448
  }) : "unknown";
1249
1449
  const exchangeLiquidityRegime = exchangeLiquidityRow ? toExchangeLiquidityRegime({
1250
1450
  stale: exchangeLiquidityRow.stale,
1251
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
1451
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
1252
1452
  exchangeLiquidityRow.totalVolumeChange24hPct
1253
1453
  ),
1254
1454
  fallback: exchangeLiquidityRow.liquidityRegime ?? "unknown"
1255
1455
  }) : "unknown";
1256
1456
  const cmc100Row = indexRows.get("cmc100") ?? null;
1257
1457
  const cmc20Row = indexRows.get("cmc20") ?? null;
1258
- const cmc100Value = toFiniteNumberOrNull2(cmc100Row?.value);
1259
- const cmc20Value = toFiniteNumberOrNull2(cmc20Row?.value);
1260
- const cmc100Change24hPct = toFiniteNumberOrNull2(
1458
+ const cmc100Value = toFiniteNumberOrNull3(cmc100Row?.value);
1459
+ const cmc20Value = toFiniteNumberOrNull3(cmc20Row?.value);
1460
+ const cmc100Change24hPct = toFiniteNumberOrNull3(
1261
1461
  cmc100Row?.valueChange24hPct
1262
1462
  );
1263
- const cmc20Change24hPct = toFiniteNumberOrNull2(cmc20Row?.valueChange24hPct);
1463
+ const cmc20Change24hPct = toFiniteNumberOrNull3(cmc20Row?.valueChange24hPct);
1264
1464
  const cmc20ToCmc100Ratio = safeDivide(cmc20Value, cmc100Value);
1265
1465
  const cmc20ToCmc100RatioChange24hPct = cmc20Change24hPct != null && cmc100Change24hPct != null ? (1 + cmc20Change24hPct) / (1 + cmc100Change24hPct) - 1 : null;
1266
1466
  const indexStale = cmc100Row?.stale === true || cmc20Row?.stale === true || !cmc100Row || !cmc20Row;
@@ -1284,41 +1484,41 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1284
1484
  asOfTs: globalRow.ts.getTime(),
1285
1485
  ageMs: globalRow.ageMs,
1286
1486
  stale: globalRow.stale,
1287
- totalMarketCapUsd: toFiniteNumberOrNull2(
1487
+ totalMarketCapUsd: toFiniteNumberOrNull3(
1288
1488
  globalRow.totalMarketCapUsd
1289
1489
  ),
1290
- totalVolumeUsd: toFiniteNumberOrNull2(
1490
+ totalVolumeUsd: toFiniteNumberOrNull3(
1291
1491
  globalRow.totalVolumeUsd
1292
1492
  ),
1293
- totalVolumeReportedUsd: toFiniteNumberOrNull2(
1493
+ totalVolumeReportedUsd: toFiniteNumberOrNull3(
1294
1494
  globalRow.totalVolumeReportedUsd
1295
1495
  ),
1296
- altMarketCapUsd: toFiniteNumberOrNull2(
1496
+ altMarketCapUsd: toFiniteNumberOrNull3(
1297
1497
  globalRow.altMarketCapUsd
1298
1498
  ),
1299
- altVolumeUsd: toFiniteNumberOrNull2(globalRow.altVolumeUsd),
1300
- altVolumeReportedUsd: toFiniteNumberOrNull2(
1499
+ altVolumeUsd: toFiniteNumberOrNull3(globalRow.altVolumeUsd),
1500
+ altVolumeReportedUsd: toFiniteNumberOrNull3(
1301
1501
  globalRow.altVolumeReportedUsd
1302
1502
  ),
1303
- btcDominancePct: toFiniteNumberOrNull2(
1503
+ btcDominancePct: toFiniteNumberOrNull3(
1304
1504
  globalRow.btcDominancePct
1305
1505
  ),
1306
- ethDominancePct: toFiniteNumberOrNull2(
1506
+ ethDominancePct: toFiniteNumberOrNull3(
1307
1507
  globalRow.ethDominancePct
1308
1508
  ),
1309
1509
  btcDominanceChange24hPct,
1310
- ethDominanceChange24hPct: toFiniteNumberOrNull2(
1510
+ ethDominanceChange24hPct: toFiniteNumberOrNull3(
1311
1511
  globalRow.ethDominanceChange24hPct
1312
1512
  ),
1313
1513
  altMarketCapChange24hPct,
1314
1514
  altVolumeChange24hPct,
1315
- activeCryptocurrencies: toFiniteNumberOrNull2(
1515
+ activeCryptocurrencies: toFiniteNumberOrNull3(
1316
1516
  globalRow.activeCryptocurrencies
1317
1517
  ),
1318
- activeExchanges: toFiniteNumberOrNull2(
1518
+ activeExchanges: toFiniteNumberOrNull3(
1319
1519
  globalRow.activeExchanges
1320
1520
  ),
1321
- activeMarketPairs: toFiniteNumberOrNull2(
1521
+ activeMarketPairs: toFiniteNumberOrNull3(
1322
1522
  globalRow.activeMarketPairs
1323
1523
  ),
1324
1524
  altLiquidityRegime
@@ -1363,22 +1563,22 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1363
1563
  asOfTs: exchangeLiquidityRow.ts.getTime(),
1364
1564
  ageMs: exchangeLiquidityRow.ageMs,
1365
1565
  stale: exchangeLiquidityRow.stale,
1366
- exchangesCount: toFiniteNumberOrNull2(
1566
+ exchangesCount: toFiniteNumberOrNull3(
1367
1567
  exchangeLiquidityRow.exchangesCount
1368
1568
  ),
1369
- totalVolumeUsd: toFiniteNumberOrNull2(
1569
+ totalVolumeUsd: toFiniteNumberOrNull3(
1370
1570
  exchangeLiquidityRow.totalVolumeUsd
1371
1571
  ),
1372
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
1572
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
1373
1573
  exchangeLiquidityRow.totalVolumeChange24hPct
1374
1574
  ),
1375
- binanceVolumeUsd: toFiniteNumberOrNull2(
1575
+ binanceVolumeUsd: toFiniteNumberOrNull3(
1376
1576
  exchangeLiquidityRow.binanceVolumeUsd
1377
1577
  ),
1378
- binanceVolumeShare: toFiniteNumberOrNull2(
1578
+ binanceVolumeShare: toFiniteNumberOrNull3(
1379
1579
  exchangeLiquidityRow.binanceVolumeShare
1380
1580
  ),
1381
- topExchangeVolumeShare: toFiniteNumberOrNull2(
1581
+ topExchangeVolumeShare: toFiniteNumberOrNull3(
1382
1582
  exchangeLiquidityRow.topExchangeVolumeShare
1383
1583
  ),
1384
1584
  liquidityRegime: exchangeLiquidityRegime
@@ -1391,11 +1591,11 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1391
1591
  asOfTs: fearGreedRow.ts.getTime(),
1392
1592
  ageMs: fearGreedRow.ageMs,
1393
1593
  stale: fearGreedRow.stale,
1394
- value: toFiniteNumberOrNull2(fearGreedRow.value),
1395
- valueChange24h: toFiniteNumberOrNull2(
1594
+ value: toFiniteNumberOrNull3(fearGreedRow.value),
1595
+ valueChange24h: toFiniteNumberOrNull3(
1396
1596
  fearGreedRow.valueChange24h
1397
1597
  ),
1398
- valueChange7d: toFiniteNumberOrNull2(
1598
+ valueChange7d: toFiniteNumberOrNull3(
1399
1599
  fearGreedRow.valueChange7d
1400
1600
  ),
1401
1601
  classification: fearGreedRow.classification ?? "Unknown",
@@ -1415,13 +1615,13 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1415
1615
  cmc100Value,
1416
1616
  cmc100Change24hPct,
1417
1617
  cmc100TopConstituentSymbol: cmc100Row?.topConstituentSymbol ?? null,
1418
- cmc100TopConstituentWeightPct: toFiniteNumberOrNull2(
1618
+ cmc100TopConstituentWeightPct: toFiniteNumberOrNull3(
1419
1619
  cmc100Row?.topConstituentWeightPct
1420
1620
  ),
1421
1621
  cmc20Value,
1422
1622
  cmc20Change24hPct,
1423
1623
  cmc20TopConstituentSymbol: cmc20Row?.topConstituentSymbol ?? null,
1424
- cmc20TopConstituentWeightPct: toFiniteNumberOrNull2(
1624
+ cmc20TopConstituentWeightPct: toFiniteNumberOrNull3(
1425
1625
  cmc20Row?.topConstituentWeightPct
1426
1626
  ),
1427
1627
  cmc20ToCmc100Ratio,
@@ -1439,7 +1639,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1439
1639
  throw error;
1440
1640
  }
1441
1641
  coinMarketCapContextUnavailable = true;
1442
- logger2.warn(
1642
+ logger3.warn(
1443
1643
  "CoinMarketCap context disabled after Timescale read failure: %s",
1444
1644
  String(error)
1445
1645
  );
@@ -1673,7 +1873,7 @@ import {
1673
1873
  getHyperliquidWhaleFlowAggregate,
1674
1874
  getHyperliquidWhaleFlowSeriesRows
1675
1875
  } from "@tradejs/infra/timescale/hyperliquidWhales";
1676
- import { logger as logger3 } from "@tradejs/infra/logger";
1876
+ import { logger as logger4 } from "@tradejs/infra/logger";
1677
1877
  var MAX_CACHE_ENTRIES = 2048;
1678
1878
  var MAX_COVERAGE_SERIES_CACHE_ENTRIES = 20;
1679
1879
  var MAX_FLOW_SERIES_CACHE_ENTRIES = 64;
@@ -1977,7 +2177,7 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
1977
2177
  throw error;
1978
2178
  }
1979
2179
  hyperliquidWhaleContextUnavailable = true;
1980
- logger3.warn(
2180
+ logger4.warn(
1981
2181
  "Hyperliquid whale context disabled after Timescale read failure: %s",
1982
2182
  String(error)
1983
2183
  );
@@ -2016,7 +2216,7 @@ var enrichSignalWithHyperliquidWhaleContext = async (params) => {
2016
2216
  };
2017
2217
 
2018
2218
  // src/strategyHelpers/runtime.ts
2019
- import { logger as logger7 } from "@tradejs/infra/logger";
2219
+ import { logger as logger8 } from "@tradejs/infra/logger";
2020
2220
  import { FEE_PERCENT } from "@tradejs/core/constants";
2021
2221
  import {
2022
2222
  buildMlFeatures,
@@ -2078,7 +2278,7 @@ import { randomUUID } from "crypto";
2078
2278
  import { TTL_1M } from "@tradejs/core/constants";
2079
2279
  import { getRuntimeStorageDayKey } from "@tradejs/core/time";
2080
2280
  import { createRuntimeOrderLinkPrefix } from "@tradejs/core/trade";
2081
- import { logger as logger4 } from "@tradejs/infra/logger";
2281
+ import { logger as logger5 } from "@tradejs/infra/logger";
2082
2282
  import {
2083
2283
  delKey,
2084
2284
  getData,
@@ -2143,7 +2343,7 @@ var recordRuntimeTradeOpen = async (params) => {
2143
2343
  )
2144
2344
  ]);
2145
2345
  } catch (error) {
2146
- logger4.error(
2346
+ logger5.error(
2147
2347
  "runtime trade open journal failed: %s %s",
2148
2348
  record.symbol,
2149
2349
  error?.message || String(error)
@@ -2211,7 +2411,7 @@ var recordRuntimeTradeIncrease = async (params) => {
2211
2411
  )
2212
2412
  ]);
2213
2413
  } catch (error) {
2214
- logger4.error(
2414
+ logger5.error(
2215
2415
  "runtime trade increase journal failed: %s %s",
2216
2416
  symbol,
2217
2417
  error?.message || String(error)
@@ -2318,7 +2518,7 @@ var markRuntimeTradeClosed = async (params) => {
2318
2518
  )
2319
2519
  ]);
2320
2520
  } catch (error) {
2321
- logger4.error(
2521
+ logger5.error(
2322
2522
  "runtime trade close journal failed: %s %s",
2323
2523
  symbol,
2324
2524
  error?.message || String(error)
@@ -2328,7 +2528,7 @@ var markRuntimeTradeClosed = async (params) => {
2328
2528
  };
2329
2529
 
2330
2530
  // src/strategyHelpers/marketContextStages.ts
2331
- import { logger as logger6 } from "@tradejs/infra/logger";
2531
+ import { logger as logger7 } from "@tradejs/infra/logger";
2332
2532
 
2333
2533
  // src/strategyHelpers/derivativesContext.ts
2334
2534
  import {
@@ -2343,7 +2543,7 @@ import {
2343
2543
  resolveDerivativesContextReferenceSymbols
2344
2544
  } from "@tradejs/core/constants";
2345
2545
  import { getDerivativesWindow } from "@tradejs/infra/timescale/derivatives";
2346
- import { logger as logger5 } from "@tradejs/infra/logger";
2546
+ import { logger as logger6 } from "@tradejs/infra/logger";
2347
2547
  var STORED_INTERVALS = ["15m", "1h"];
2348
2548
  var CONTEXT_INTERVALS = ["15m", "1h"];
2349
2549
  var DEFAULT_LOOKBACK_HOURS = 48;
@@ -2398,7 +2598,7 @@ var resolvePrimaryReferenceSymbol2 = () => PRIMARY_DERIVATIVES_REFERENCE_SYMBOL;
2398
2598
  var resolveSecondaryReferenceSymbol = () => SECONDARY_DERIVATIVES_REFERENCE_SYMBOL;
2399
2599
  var getPrimaryIntervalContext = (context) => context?.intervals["15m"] ?? context?.intervals["1h"] ?? null;
2400
2600
  var hasDerivativesSymbolData = (context) => Object.keys(context.intervals).length > 0 && !context.summary.riskFlags.includes("missing_derivatives");
2401
- var toFiniteNumberOrNull3 = (value) => {
2601
+ var toFiniteNumberOrNull4 = (value) => {
2402
2602
  if (typeof value === "number" && Number.isFinite(value)) return value;
2403
2603
  if (typeof value === "string" && value.trim()) {
2404
2604
  const parsed = Number(value);
@@ -2412,8 +2612,8 @@ var roundNullable = (value, digits = 4) => {
2412
2612
  return Math.round(value * multiplier) / multiplier;
2413
2613
  };
2414
2614
  var deltaNullable = (targetValue, referenceValue) => {
2415
- const target = toFiniteNumberOrNull3(targetValue);
2416
- const reference = toFiniteNumberOrNull3(referenceValue);
2615
+ const target = toFiniteNumberOrNull4(targetValue);
2616
+ const reference = toFiniteNumberOrNull4(referenceValue);
2417
2617
  return target == null || reference == null ? null : roundNullable(target - reference);
2418
2618
  };
2419
2619
  var buildTargetDerivedContext = (params) => {
@@ -2575,7 +2775,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
2575
2775
  throw error;
2576
2776
  }
2577
2777
  derivativesContextUnavailable = true;
2578
- logger5.warn(
2778
+ logger6.warn(
2579
2779
  "Derivatives context disabled after Timescale read failure: %s",
2580
2780
  String(error)
2581
2781
  );
@@ -2628,7 +2828,7 @@ var runMarketContextStage = async ({
2628
2828
  elapsedMs: Date.now() - startedAt
2629
2829
  };
2630
2830
  if (status === "timed_out") {
2631
- logger6.warn(
2831
+ logger7.warn(
2632
2832
  "Market context stage timed out: %s after %sms",
2633
2833
  stage,
2634
2834
  result.elapsedMs
@@ -2818,7 +3018,7 @@ var enrichSignalWithAi = async ({
2818
3018
  signal.aiAnalysis = analysis;
2819
3019
  return resolveAiQuality(analysis, direction);
2820
3020
  } catch (err) {
2821
- logger7.error("AI analysis error: %s %s", symbol, formatAiError(err));
3021
+ logger8.error("AI analysis error: %s %s", symbol, formatAiError(err));
2822
3022
  }
2823
3023
  return void 0;
2824
3024
  };
@@ -2835,75 +3035,6 @@ var enrichSignalWithMlAi = async ({
2835
3035
  await enrichSignalWithMl({ signal, env, ml });
2836
3036
  return enrichSignalWithAi({ signal, userName, symbol, direction, env, ai });
2837
3037
  };
2838
- var toFiniteNumberOrNull4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
2839
- var getOrderArrivalSnapshot = async ({
2840
- connector,
2841
- symbol
2842
- }) => {
2843
- if (typeof connector.getTopOfBookTicker !== "function") {
2844
- return {
2845
- arrivalSnapshotTime: Date.now(),
2846
- arrivalSource: "unavailable",
2847
- bid: null,
2848
- ask: null,
2849
- arrivalMid: null,
2850
- spreadBps: null
2851
- };
2852
- }
2853
- try {
2854
- const ticker = await connector.getTopOfBookTicker(symbol);
2855
- const arrivalSnapshotTime = toFiniteNumberOrNull4(ticker?.timestamp);
2856
- const bid = toFiniteNumberOrNull4(ticker?.bidPrice);
2857
- const ask = toFiniteNumberOrNull4(ticker?.askPrice);
2858
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
2859
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
2860
- return {
2861
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
2862
- arrivalSource: "top_of_book",
2863
- bid,
2864
- ask,
2865
- arrivalMid,
2866
- spreadBps
2867
- };
2868
- } catch (error) {
2869
- logger7.warn(
2870
- "runtime order arrival snapshot failed: %s %s",
2871
- symbol,
2872
- error?.message || String(error)
2873
- );
2874
- return {
2875
- arrivalSnapshotTime: Date.now(),
2876
- arrivalSource: "top_of_book_error",
2877
- bid: null,
2878
- ask: null,
2879
- arrivalMid: null,
2880
- spreadBps: null
2881
- };
2882
- }
2883
- };
2884
- var validateEntryProtectionAtArrival = ({
2885
- direction,
2886
- signalPrice,
2887
- bid,
2888
- ask,
2889
- arrivalMid,
2890
- takeProfits,
2891
- stopLossPrice
2892
- }) => {
2893
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
2894
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
2895
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
2896
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
2897
- }
2898
- for (const takeProfit of takeProfits) {
2899
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
2900
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
2901
- }
2902
- }
2903
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
2904
- throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
2905
- }
2906
- };
2907
3038
  var resolveRuntimeTelemetryQuality = ({
2908
3039
  signalClosePrice,
2909
3040
  arrivalMid,
@@ -2980,7 +3111,8 @@ var executeEntryOrder = async ({
2980
3111
  signal,
2981
3112
  beforePlaceOrder,
2982
3113
  recordRuntimeTrade = true,
2983
- leverage
3114
+ leverage,
3115
+ executionLimits
2984
3116
  }) => {
2985
3117
  await beforePlaceOrder?.();
2986
3118
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
@@ -2991,23 +3123,57 @@ var executeEntryOrder = async ({
2991
3123
  signal.orderQty = qty;
2992
3124
  signal.orderValue = qty * currentPrice;
2993
3125
  signal.orderFailureReason = void 0;
2994
- const arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
2995
- try {
2996
- validateEntryProtectionAtArrival({
2997
- direction,
2998
- signalPrice: currentPrice,
2999
- bid: arrivalSnapshot.bid,
3000
- ask: arrivalSnapshot.ask,
3001
- arrivalMid: arrivalSnapshot.arrivalMid,
3002
- takeProfits,
3003
- stopLossPrice
3004
- });
3005
- } catch (error) {
3006
- signal.orderStatus = "failed";
3007
- signal.orderFailureReason = error.message;
3008
- throw error;
3126
+ let arrivalSnapshot = {
3127
+ arrivalSnapshotTime: Date.now(),
3128
+ arrivalSource: "unavailable",
3129
+ bid: null,
3130
+ ask: null,
3131
+ arrivalMid: null,
3132
+ spreadBps: null
3133
+ };
3134
+ let orderSubmitTime = Date.now();
3135
+ const beforeSubmit = createEntrySubmissionValidation({
3136
+ connector,
3137
+ symbol,
3138
+ direction,
3139
+ takeProfits,
3140
+ stopLossPrice,
3141
+ limits: executionLimits,
3142
+ onValidated: (snapshot2, economics) => {
3143
+ arrivalSnapshot = snapshot2;
3144
+ orderSubmitTime = Date.now();
3145
+ if (executionLimits?.maxLossValue != null) {
3146
+ signal.riskDecision = {
3147
+ stage: "order_plan",
3148
+ status: "approved",
3149
+ reason: "PLANNED_LOSS_WITHIN_MAX_LOSS_VALUE",
3150
+ plannedLossValue: economics.plannedLossValue,
3151
+ maxLossValue: executionLimits.maxLossValue,
3152
+ limitUtilization: economics.plannedLossValue / executionLimits.maxLossValue,
3153
+ enforced: true
3154
+ };
3155
+ }
3156
+ }
3157
+ });
3158
+ if (!beforeSubmit) {
3159
+ arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
3160
+ try {
3161
+ validateEntryProtectionAtArrival({
3162
+ direction,
3163
+ signalPrice: currentPrice,
3164
+ bid: arrivalSnapshot.bid,
3165
+ ask: arrivalSnapshot.ask,
3166
+ arrivalMid: arrivalSnapshot.arrivalMid,
3167
+ takeProfits,
3168
+ stopLossPrice
3169
+ });
3170
+ } catch (error) {
3171
+ signal.orderStatus = "failed";
3172
+ signal.orderFailureReason = error.message;
3173
+ throw error;
3174
+ }
3175
+ orderSubmitTime = Date.now();
3009
3176
  }
3010
- const orderSubmitTime = Date.now();
3011
3177
  const orderPlaced = await connector.placeOrder({
3012
3178
  symbol,
3013
3179
  qty,
@@ -3018,7 +3184,8 @@ var executeEntryOrder = async ({
3018
3184
  direction,
3019
3185
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
3020
3186
  orderId,
3021
- signal
3187
+ signal,
3188
+ ...beforeSubmit ? { beforeSubmit } : {}
3022
3189
  });
3023
3190
  const orderAckTime = Date.now();
3024
3191
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -3305,7 +3472,7 @@ import {
3305
3472
  createStrategyIndicatorsState,
3306
3473
  getSharedStrategyReplayState
3307
3474
  } from "@tradejs/core/strategies";
3308
- import { logger as logger9 } from "@tradejs/infra/logger";
3475
+ import { logger as logger10 } from "@tradejs/infra/logger";
3309
3476
 
3310
3477
  // src/strategy/runtimeBacktestDelay.ts
3311
3478
  import { BACKTEST_EXECUTION_INTERVAL } from "@tradejs/core/constants";
@@ -3867,7 +4034,7 @@ var canUseSharedReplayState = ({
3867
4034
  }) => (env === "BACKTEST" || env === "PARITY") && Boolean(sharedReplayKey);
3868
4035
 
3869
4036
  // src/strategy/runtimeExecution.ts
3870
- import { logger as logger8 } from "@tradejs/infra/logger";
4037
+ import { logger as logger9 } from "@tradejs/infra/logger";
3871
4038
  import {
3872
4039
  BACKTEST_WARNING_CODES
3873
4040
  } from "@tradejs/types";
@@ -3921,7 +4088,7 @@ var handleExitDecision = async ({
3921
4088
  deploymentId: connector.deploymentId
3922
4089
  });
3923
4090
  if (!activeTrade) {
3924
- logger8.warn(
4091
+ logger9.warn(
3925
4092
  "[%s] blocked closePosition for untracked runtime position: %s",
3926
4093
  strategyName ?? "unknown",
3927
4094
  symbol
@@ -3929,7 +4096,7 @@ var handleExitDecision = async ({
3929
4096
  return "CLOSE_BLOCKED_BY_UNTRACKED_POSITION";
3930
4097
  }
3931
4098
  if (!strategyName || activeTrade.strategy !== strategyName) {
3932
- logger8.warn(
4099
+ logger9.warn(
3933
4100
  "[%s] blocked closePosition for foreign runtime position: %s ownedBy=%s",
3934
4101
  strategyName ?? "unknown",
3935
4102
  symbol,
@@ -3982,7 +4149,7 @@ var handleExitDecision = async ({
3982
4149
  exitType: closedTrade?.exitType ?? "exit"
3983
4150
  });
3984
4151
  } catch (notificationError) {
3985
- logger8.error(
4152
+ logger9.error(
3986
4153
  "runtime close notification error: %s %s",
3987
4154
  symbol,
3988
4155
  notificationError
@@ -3996,7 +4163,7 @@ var handleExitDecision = async ({
3996
4163
  decision,
3997
4164
  market
3998
4165
  });
3999
- logger8.error("close order error: %s %s", symbol, err);
4166
+ logger9.error("close order error: %s %s", symbol, err);
4000
4167
  return "ORDER_ERROR";
4001
4168
  }
4002
4169
  return decision.code;
@@ -4023,7 +4190,7 @@ var handleProtectDecision = async ({
4023
4190
  decision,
4024
4191
  market
4025
4192
  });
4026
- logger8.error("protect position error: %s %s", symbol, err);
4193
+ logger9.error("protect position error: %s %s", symbol, err);
4027
4194
  return "ORDER_ERROR";
4028
4195
  }
4029
4196
  return decision.code;
@@ -4089,7 +4256,11 @@ var executeEntryDecision = async ({
4089
4256
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
4090
4257
  signal,
4091
4258
  beforePlaceOrder,
4092
- recordRuntimeTrade: recordRuntimeJournal
4259
+ recordRuntimeTrade: recordRuntimeJournal,
4260
+ executionLimits: resolveEntryExecutionLimits(
4261
+ hookCtx.strategyConfig,
4262
+ decision.entryContext.direction
4263
+ )
4093
4264
  });
4094
4265
  await invokeStageHooks(
4095
4266
  "afterPlaceOrder",
@@ -4111,19 +4282,32 @@ var executeEntryDecision = async ({
4111
4282
  return signal;
4112
4283
  }
4113
4284
  await beforePlaceOrder();
4114
- const arrivalSnapshot = await getOrderArrivalSnapshot({
4285
+ const beforeSubmit = createEntrySubmissionValidation({
4115
4286
  connector,
4116
- symbol
4117
- });
4118
- validateEntryProtectionAtArrival({
4287
+ symbol,
4119
4288
  direction: decision.entryContext.direction,
4120
- signalPrice: decision.entryContext.prices.currentPrice,
4121
- bid: arrivalSnapshot.bid,
4122
- ask: arrivalSnapshot.ask,
4123
- arrivalMid: arrivalSnapshot.arrivalMid,
4124
4289
  takeProfits: decision.orderPlan.takeProfits,
4125
- stopLossPrice: decision.orderPlan.stopLossPrice
4290
+ stopLossPrice: decision.orderPlan.stopLossPrice,
4291
+ limits: resolveEntryExecutionLimits(
4292
+ hookCtx.strategyConfig,
4293
+ decision.entryContext.direction
4294
+ )
4126
4295
  });
4296
+ if (!beforeSubmit) {
4297
+ const arrivalSnapshot = await getOrderArrivalSnapshot({
4298
+ connector,
4299
+ symbol
4300
+ });
4301
+ validateEntryProtectionAtArrival({
4302
+ direction: decision.entryContext.direction,
4303
+ signalPrice: decision.entryContext.prices.currentPrice,
4304
+ bid: arrivalSnapshot.bid,
4305
+ ask: arrivalSnapshot.ask,
4306
+ arrivalMid: arrivalSnapshot.arrivalMid,
4307
+ takeProfits: decision.orderPlan.takeProfits,
4308
+ stopLossPrice: decision.orderPlan.stopLossPrice
4309
+ });
4310
+ }
4127
4311
  const orderPlaced = await connector.placeOrder({
4128
4312
  symbol,
4129
4313
  qty: decision.orderPlan.qty,
@@ -4131,6 +4315,7 @@ var executeEntryDecision = async ({
4131
4315
  timestamp: decision.entryContext.timestamp,
4132
4316
  direction: decision.entryContext.direction,
4133
4317
  positionIntent: decision.orderPlan.positionIntent,
4318
+ ...beforeSubmit ? { beforeSubmit } : {},
4134
4319
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {}
4135
4320
  });
4136
4321
  if (!orderPlaced) {
@@ -4172,6 +4357,23 @@ var executeEntryDecision = async ({
4172
4357
  { decision, entry, market }
4173
4358
  );
4174
4359
  } catch (err) {
4360
+ if (err instanceof EntrySubmissionValidationError) {
4361
+ if (signal) {
4362
+ signal.orderStatus = "skipped";
4363
+ signal.orderSkipReason = err.message;
4364
+ signal.allocatorDecision = {
4365
+ stage: "runtime_admission",
4366
+ status: "rejected",
4367
+ reason: err.message
4368
+ };
4369
+ }
4370
+ logger9.warn(
4371
+ "entry rejected before submission: %s %s",
4372
+ symbol,
4373
+ err.message
4374
+ );
4375
+ return signal ?? err.message;
4376
+ }
4175
4377
  if (signal) {
4176
4378
  signal.orderStatus = "failed";
4177
4379
  if (typeof signal.orderFailureReason !== "string" || !signal.orderFailureReason.trim()) {
@@ -4186,9 +4388,9 @@ var executeEntryDecision = async ({
4186
4388
  market
4187
4389
  });
4188
4390
  if (err?.message === BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY) {
4189
- logger8.warn("order warning: %s %s", symbol, err);
4391
+ logger9.warn("order warning: %s %s", symbol, err);
4190
4392
  } else {
4191
- logger8.error("order error: %s %s", symbol, err);
4393
+ logger9.error("order error: %s %s", symbol, err);
4192
4394
  }
4193
4395
  return signal ?? "ORDER_ERROR";
4194
4396
  }
@@ -4353,7 +4555,7 @@ var createStrategyRuntime = ({
4353
4555
  try {
4354
4556
  await projectHook(errorParams);
4355
4557
  } catch (hookError) {
4356
- logger9.error(
4558
+ logger10.error(
4357
4559
  "project hook onRuntimeError failed: %s %s",
4358
4560
  strategyName,
4359
4561
  hookError
@@ -4367,7 +4569,7 @@ var createStrategyRuntime = ({
4367
4569
  try {
4368
4570
  await onRuntimeError(errorParams);
4369
4571
  } catch (hookError) {
4370
- logger9.error(
4572
+ logger10.error(
4371
4573
  "runtime hook onRuntimeError failed: %s %s",
4372
4574
  strategyName,
4373
4575
  hookError
@@ -4381,7 +4583,7 @@ var createStrategyRuntime = ({
4381
4583
  try {
4382
4584
  return await hook(params);
4383
4585
  } catch (error) {
4384
- logger9.error(
4586
+ logger10.error(
4385
4587
  'strategy hook "%s" failed for %s: %s',
4386
4588
  stage,
4387
4589
  strategyName,
@@ -5203,6 +5405,8 @@ var createStrategyRuntime = ({
5203
5405
  setStrategyRuntimeFactory(createStrategyRuntime);
5204
5406
 
5205
5407
  export {
5408
+ getOrderArrivalSnapshot,
5409
+ validateEntryProtectionAtArrival,
5206
5410
  buildMlPayload,
5207
5411
  BINANCE_BREADTH_UNIVERSE_KEYS,
5208
5412
  buildBinanceBreadthUniverseSnapshot,
@@ -5224,8 +5428,6 @@ export {
5224
5428
  enrichSignalWithMl,
5225
5429
  enrichSignalWithAi,
5226
5430
  enrichSignalWithMlAi,
5227
- getOrderArrivalSnapshot,
5228
- validateEntryProtectionAtArrival,
5229
5431
  executeEntryOrder,
5230
5432
  resolveStrategyConfig,
5231
5433
  RESEARCH_ENTRY_POLICY_CAPABILITIES,