@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.
@@ -16,6 +16,203 @@ 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
+ const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
80
+ return { netRiskRatio, plannedLossValue, riskExceeded };
81
+ };
82
+
83
+ // src/strategy/entrySubmissionValidation.ts
84
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
85
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
86
+ var getOrderArrivalSnapshot = async ({
87
+ connector,
88
+ symbol,
89
+ requireTimestamp = false
90
+ }) => {
91
+ if (typeof connector.getTopOfBookTicker !== "function") {
92
+ return {
93
+ arrivalSnapshotTime: Date.now(),
94
+ arrivalSource: "unavailable",
95
+ bid: null,
96
+ ask: null,
97
+ arrivalMid: null,
98
+ spreadBps: null
99
+ };
100
+ }
101
+ try {
102
+ const ticker = await connector.getTopOfBookTicker(symbol);
103
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
104
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
105
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
106
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
107
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
108
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
109
+ return {
110
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
111
+ arrivalSource: "top_of_book",
112
+ bid,
113
+ ask,
114
+ arrivalMid,
115
+ spreadBps
116
+ };
117
+ } catch (error) {
118
+ logger.warn(
119
+ "runtime order arrival snapshot failed: %s %s",
120
+ symbol,
121
+ error?.message || String(error)
122
+ );
123
+ return {
124
+ arrivalSnapshotTime: Date.now(),
125
+ arrivalSource: "top_of_book_error",
126
+ bid: null,
127
+ ask: null,
128
+ arrivalMid: null,
129
+ spreadBps: null
130
+ };
131
+ }
132
+ };
133
+ var validateEntryProtectionAtArrival = ({
134
+ direction,
135
+ signalPrice,
136
+ bid,
137
+ ask,
138
+ arrivalMid,
139
+ takeProfits,
140
+ stopLossPrice
141
+ }) => {
142
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
143
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
144
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
145
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
146
+ }
147
+ for (const takeProfit of takeProfits) {
148
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
149
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
150
+ }
151
+ }
152
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
153
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
154
+ }
155
+ };
156
+ var createEntrySubmissionValidation = ({
157
+ connector,
158
+ symbol,
159
+ direction,
160
+ takeProfits,
161
+ stopLossPrice,
162
+ limits,
163
+ onValidated
164
+ }) => {
165
+ if (!limits) return void 0;
166
+ if (!connector.capabilities?.orderSubmissionValidation)
167
+ throw new EntrySubmissionValidationError(
168
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
169
+ );
170
+ return async ({ qty, executionPrice }) => {
171
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
172
+ const snapshot2 = simulated && executionPrice != null ? {
173
+ arrivalSnapshotTime: Date.now(),
174
+ arrivalSource: "simulation",
175
+ bid: executionPrice,
176
+ ask: executionPrice,
177
+ arrivalMid: executionPrice,
178
+ spreadBps: 0
179
+ } : await getOrderArrivalSnapshot({
180
+ connector,
181
+ symbol,
182
+ requireTimestamp: true
183
+ });
184
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
185
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
186
+ throw new EntrySubmissionValidationError(
187
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
188
+ );
189
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
190
+ throw new EntrySubmissionValidationError(
191
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
192
+ );
193
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
194
+ validateEntryProtectionAtArrival({
195
+ direction,
196
+ signalPrice: price,
197
+ bid: snapshot2.bid,
198
+ ask: snapshot2.ask,
199
+ arrivalMid: snapshot2.arrivalMid,
200
+ takeProfits,
201
+ stopLossPrice
202
+ });
203
+ const prices = takeProfits.map((tp) => tp.price);
204
+ const economics = validateEntryExecution({
205
+ direction,
206
+ price,
207
+ qty,
208
+ stopLossPrice,
209
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
210
+ limits
211
+ });
212
+ onValidated?.(snapshot2, economics);
213
+ };
214
+ };
215
+
19
216
  // src/binanceBreadthUniverses.ts
20
217
  import { createHash } from "crypto";
21
218
 
@@ -328,7 +525,7 @@ import {
328
525
  getLatestMarketBreadth,
329
526
  getLatestMarketTradeFlow
330
527
  } from "@tradejs/infra/timescale/marketContext";
331
- import { logger } from "@tradejs/infra/logger";
528
+ import { logger as logger2 } from "@tradejs/infra/logger";
332
529
  import { refreshSignalBaseContextGateFeatures } from "@tradejs/core/strategies";
333
530
 
334
531
  // src/strategyHelpers/marketContextErrors.ts
@@ -399,7 +596,7 @@ var parseEnabledFlag = (value, env) => {
399
596
  if (normalized === "live") return env !== "BACKTEST";
400
597
  return false;
401
598
  };
402
- var toFiniteNumberOrNull = (value) => {
599
+ var toFiniteNumberOrNull2 = (value) => {
403
600
  const numeric = typeof value === "number" ? value : Number(value);
404
601
  return Number.isFinite(numeric) ? numeric : null;
405
602
  };
@@ -664,14 +861,14 @@ var toTradeFlowContext = (row, interval) => row ? {
664
861
  asOfTs: row.ts.getTime(),
665
862
  ageMs: row.ageMs,
666
863
  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)
864
+ trades: toFiniteNumberOrNull2(row.trades),
865
+ buyPressurePct: toFiniteNumberOrNull2(row.buyPressurePct),
866
+ buyBaseVolume: toFiniteNumberOrNull2(row.buyBaseVolume),
867
+ sellBaseVolume: toFiniteNumberOrNull2(row.sellBaseVolume),
868
+ buyQuoteVolume: toFiniteNumberOrNull2(row.buyQuoteVolume),
869
+ sellQuoteVolume: toFiniteNumberOrNull2(row.sellQuoteVolume),
870
+ netBaseDelta: toFiniteNumberOrNull2(row.netBaseDelta),
871
+ netQuoteDelta: toFiniteNumberOrNull2(row.netQuoteDelta)
675
872
  } : null;
676
873
  var getCachedReferenceRows = ({
677
874
  referenceSymbols,
@@ -760,16 +957,16 @@ var toMarketBreadthContext = (breadth, interval) => ({
760
957
  asOfTs: breadth.ts.getTime(),
761
958
  ageMs: breadth.ageMs,
762
959
  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)
960
+ symbolsCount: toFiniteNumberOrNull2(breadth.symbolsCount),
961
+ advancers: toFiniteNumberOrNull2(breadth.advancers),
962
+ decliners: toFiniteNumberOrNull2(breadth.decliners),
963
+ unchanged: toFiniteNumberOrNull2(breadth.unchanged),
964
+ advanceDeclineRatio: toFiniteNumberOrNull2(breadth.advanceDeclineRatio),
965
+ pctAboveMa20: toFiniteNumberOrNull2(breadth.pctAboveMa20),
966
+ pctAboveMa50: toFiniteNumberOrNull2(breadth.pctAboveMa50),
967
+ equalWeightedReturn: toFiniteNumberOrNull2(breadth.equalWeightedReturn),
968
+ volumeWeightedReturn: toFiniteNumberOrNull2(breadth.volumeWeightedReturn),
969
+ dispersion: toFiniteNumberOrNull2(breadth.dispersion)
773
970
  });
774
971
  var enrichSignalWithBinanceMarketContext = async (params) => {
775
972
  const {
@@ -870,46 +1067,46 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
870
1067
  asOfTs: primaryBreadth.ts.getTime(),
871
1068
  ageMs: primaryBreadth.ageMs,
872
1069
  stale: primaryBreadth.stale,
873
- btcReturn1h: toFiniteNumberOrNull(
1070
+ btcReturn1h: toFiniteNumberOrNull2(
874
1071
  primaryBreadth.btcReturn1h
875
1072
  ),
876
- btcReturn4h: toFiniteNumberOrNull(
1073
+ btcReturn4h: toFiniteNumberOrNull2(
877
1074
  primaryBreadth.btcReturn4h
878
1075
  ),
879
- btcReturn24h: toFiniteNumberOrNull(
1076
+ btcReturn24h: toFiniteNumberOrNull2(
880
1077
  primaryBreadth.btcReturn24h
881
1078
  ),
882
- altBasketReturn1h: toFiniteNumberOrNull(
1079
+ altBasketReturn1h: toFiniteNumberOrNull2(
883
1080
  primaryBreadth.altBasketReturn1h
884
1081
  ),
885
- altBasketReturn4h: toFiniteNumberOrNull(
1082
+ altBasketReturn4h: toFiniteNumberOrNull2(
886
1083
  primaryBreadth.altBasketReturn4h
887
1084
  ),
888
- altBasketReturn24h: toFiniteNumberOrNull(
1085
+ altBasketReturn24h: toFiniteNumberOrNull2(
889
1086
  primaryBreadth.altBasketReturn24h
890
1087
  ),
891
- btcVsAltReturn1h: toFiniteNumberOrNull(
1088
+ btcVsAltReturn1h: toFiniteNumberOrNull2(
892
1089
  primaryBreadth.btcVsAltReturn1h
893
1090
  ),
894
- btcVsAltReturn4h: toFiniteNumberOrNull(
1091
+ btcVsAltReturn4h: toFiniteNumberOrNull2(
895
1092
  primaryBreadth.btcVsAltReturn4h
896
1093
  ),
897
- btcVsAltReturn24h: toFiniteNumberOrNull(
1094
+ btcVsAltReturn24h: toFiniteNumberOrNull2(
898
1095
  primaryBreadth.btcVsAltReturn24h
899
1096
  ),
900
- btcTurnoverShare1h: toFiniteNumberOrNull(
1097
+ btcTurnoverShare1h: toFiniteNumberOrNull2(
901
1098
  primaryBreadth.btcTurnoverShare1h
902
1099
  ),
903
- btcTurnoverShare24h: toFiniteNumberOrNull(
1100
+ btcTurnoverShare24h: toFiniteNumberOrNull2(
904
1101
  primaryBreadth.btcTurnoverShare24h
905
1102
  ),
906
- btcTurnoverShareChange24h: toFiniteNumberOrNull(
1103
+ btcTurnoverShareChange24h: toFiniteNumberOrNull2(
907
1104
  primaryBreadth.btcTurnoverShareChange24h
908
1105
  ),
909
- altVolToBtcVol24h: toFiniteNumberOrNull(
1106
+ altVolToBtcVol24h: toFiniteNumberOrNull2(
910
1107
  primaryBreadth.altVolToBtcVol24h
911
1108
  ),
912
- altDispersion24h: toFiniteNumberOrNull(
1109
+ altDispersion24h: toFiniteNumberOrNull2(
913
1110
  primaryBreadth.altDispersion24h
914
1111
  ),
915
1112
  regime: primaryBreadth.btcAltRegime ?? "unknown"
@@ -926,7 +1123,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
926
1123
  throw error;
927
1124
  }
928
1125
  binanceMarketContextUnavailable = true;
929
- logger.warn(
1126
+ logger2.warn(
930
1127
  "Binance market context disabled after Timescale read failure: %s",
931
1128
  String(error)
932
1129
  );
@@ -936,7 +1133,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
936
1133
 
937
1134
  // src/strategyHelpers/coinMarketCapContext.ts
938
1135
  import { refreshSignalBaseContextGateFeatures as refreshSignalBaseContextGateFeatures2 } from "@tradejs/core/strategies";
939
- import { logger as logger2 } from "@tradejs/infra/logger";
1136
+ import { logger as logger3 } from "@tradejs/infra/logger";
940
1137
  import {
941
1138
  getLatestMarketCmcExchangeLiquidityContext,
942
1139
  getLatestMarketCmcFearGreedContext,
@@ -980,7 +1177,7 @@ var asInt = (value, fallback) => {
980
1177
  const parsed = Number.parseInt(String(value ?? ""), 10);
981
1178
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
982
1179
  };
983
- var toFiniteNumberOrNull2 = (value) => {
1180
+ var toFiniteNumberOrNull3 = (value) => {
984
1181
  const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
985
1182
  return Number.isFinite(numeric) ? numeric : null;
986
1183
  };
@@ -1214,12 +1411,12 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1214
1411
  const ethRow = references.get("ETHUSDT") ?? null;
1215
1412
  const previousBtcRow = previousReferences.get("BTCUSDT") ?? null;
1216
1413
  const previousEthRow = previousReferences.get("ETHUSDT") ?? null;
1217
- const btcMarketCapUsd = toFiniteNumberOrNull2(btcRow?.marketCapUsd);
1218
- const ethMarketCapUsd = toFiniteNumberOrNull2(ethRow?.marketCapUsd);
1219
- const previousBtcMarketCapUsd = toFiniteNumberOrNull2(
1414
+ const btcMarketCapUsd = toFiniteNumberOrNull3(btcRow?.marketCapUsd);
1415
+ const ethMarketCapUsd = toFiniteNumberOrNull3(ethRow?.marketCapUsd);
1416
+ const previousBtcMarketCapUsd = toFiniteNumberOrNull3(
1220
1417
  previousBtcRow?.marketCapUsd
1221
1418
  );
1222
- const previousEthMarketCapUsd = toFiniteNumberOrNull2(
1419
+ const previousEthMarketCapUsd = toFiniteNumberOrNull3(
1223
1420
  previousEthRow?.marketCapUsd
1224
1421
  );
1225
1422
  const ethBtcMarketCapRatio = safeDivide(ethMarketCapUsd, btcMarketCapUsd);
@@ -1228,16 +1425,16 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1228
1425
  previousBtcMarketCapUsd
1229
1426
  );
1230
1427
  const ethBtcMarketCapRatioChange24hPct = ethBtcMarketCapRatio != null && previousEthBtcMarketCapRatio != null && previousEthBtcMarketCapRatio > 0 ? (ethBtcMarketCapRatio - previousEthBtcMarketCapRatio) / previousEthBtcMarketCapRatio : null;
1231
- const btcVolumeUsd = toFiniteNumberOrNull2(btcRow?.volumeUsd);
1232
- const ethVolumeUsd = toFiniteNumberOrNull2(ethRow?.volumeUsd);
1428
+ const btcVolumeUsd = toFiniteNumberOrNull3(btcRow?.volumeUsd);
1429
+ const ethVolumeUsd = toFiniteNumberOrNull3(ethRow?.volumeUsd);
1233
1430
  const referenceStale = btcRow?.stale === true || ethRow?.stale === true || !btcRow || !ethRow;
1234
- const btcDominanceChange24hPct = toFiniteNumberOrNull2(
1431
+ const btcDominanceChange24hPct = toFiniteNumberOrNull3(
1235
1432
  globalRow?.btcDominanceChange24hPct
1236
1433
  );
1237
- const altMarketCapChange24hPct = toFiniteNumberOrNull2(
1434
+ const altMarketCapChange24hPct = toFiniteNumberOrNull3(
1238
1435
  globalRow?.altMarketCapChange24hPct
1239
1436
  );
1240
- const altVolumeChange24hPct = toFiniteNumberOrNull2(
1437
+ const altVolumeChange24hPct = toFiniteNumberOrNull3(
1241
1438
  globalRow?.altVolumeChange24hPct
1242
1439
  );
1243
1440
  const altLiquidityRegime = globalRow ? toAltLiquidityRegime({
@@ -1248,19 +1445,19 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1248
1445
  }) : "unknown";
1249
1446
  const exchangeLiquidityRegime = exchangeLiquidityRow ? toExchangeLiquidityRegime({
1250
1447
  stale: exchangeLiquidityRow.stale,
1251
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
1448
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
1252
1449
  exchangeLiquidityRow.totalVolumeChange24hPct
1253
1450
  ),
1254
1451
  fallback: exchangeLiquidityRow.liquidityRegime ?? "unknown"
1255
1452
  }) : "unknown";
1256
1453
  const cmc100Row = indexRows.get("cmc100") ?? null;
1257
1454
  const cmc20Row = indexRows.get("cmc20") ?? null;
1258
- const cmc100Value = toFiniteNumberOrNull2(cmc100Row?.value);
1259
- const cmc20Value = toFiniteNumberOrNull2(cmc20Row?.value);
1260
- const cmc100Change24hPct = toFiniteNumberOrNull2(
1455
+ const cmc100Value = toFiniteNumberOrNull3(cmc100Row?.value);
1456
+ const cmc20Value = toFiniteNumberOrNull3(cmc20Row?.value);
1457
+ const cmc100Change24hPct = toFiniteNumberOrNull3(
1261
1458
  cmc100Row?.valueChange24hPct
1262
1459
  );
1263
- const cmc20Change24hPct = toFiniteNumberOrNull2(cmc20Row?.valueChange24hPct);
1460
+ const cmc20Change24hPct = toFiniteNumberOrNull3(cmc20Row?.valueChange24hPct);
1264
1461
  const cmc20ToCmc100Ratio = safeDivide(cmc20Value, cmc100Value);
1265
1462
  const cmc20ToCmc100RatioChange24hPct = cmc20Change24hPct != null && cmc100Change24hPct != null ? (1 + cmc20Change24hPct) / (1 + cmc100Change24hPct) - 1 : null;
1266
1463
  const indexStale = cmc100Row?.stale === true || cmc20Row?.stale === true || !cmc100Row || !cmc20Row;
@@ -1284,41 +1481,41 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1284
1481
  asOfTs: globalRow.ts.getTime(),
1285
1482
  ageMs: globalRow.ageMs,
1286
1483
  stale: globalRow.stale,
1287
- totalMarketCapUsd: toFiniteNumberOrNull2(
1484
+ totalMarketCapUsd: toFiniteNumberOrNull3(
1288
1485
  globalRow.totalMarketCapUsd
1289
1486
  ),
1290
- totalVolumeUsd: toFiniteNumberOrNull2(
1487
+ totalVolumeUsd: toFiniteNumberOrNull3(
1291
1488
  globalRow.totalVolumeUsd
1292
1489
  ),
1293
- totalVolumeReportedUsd: toFiniteNumberOrNull2(
1490
+ totalVolumeReportedUsd: toFiniteNumberOrNull3(
1294
1491
  globalRow.totalVolumeReportedUsd
1295
1492
  ),
1296
- altMarketCapUsd: toFiniteNumberOrNull2(
1493
+ altMarketCapUsd: toFiniteNumberOrNull3(
1297
1494
  globalRow.altMarketCapUsd
1298
1495
  ),
1299
- altVolumeUsd: toFiniteNumberOrNull2(globalRow.altVolumeUsd),
1300
- altVolumeReportedUsd: toFiniteNumberOrNull2(
1496
+ altVolumeUsd: toFiniteNumberOrNull3(globalRow.altVolumeUsd),
1497
+ altVolumeReportedUsd: toFiniteNumberOrNull3(
1301
1498
  globalRow.altVolumeReportedUsd
1302
1499
  ),
1303
- btcDominancePct: toFiniteNumberOrNull2(
1500
+ btcDominancePct: toFiniteNumberOrNull3(
1304
1501
  globalRow.btcDominancePct
1305
1502
  ),
1306
- ethDominancePct: toFiniteNumberOrNull2(
1503
+ ethDominancePct: toFiniteNumberOrNull3(
1307
1504
  globalRow.ethDominancePct
1308
1505
  ),
1309
1506
  btcDominanceChange24hPct,
1310
- ethDominanceChange24hPct: toFiniteNumberOrNull2(
1507
+ ethDominanceChange24hPct: toFiniteNumberOrNull3(
1311
1508
  globalRow.ethDominanceChange24hPct
1312
1509
  ),
1313
1510
  altMarketCapChange24hPct,
1314
1511
  altVolumeChange24hPct,
1315
- activeCryptocurrencies: toFiniteNumberOrNull2(
1512
+ activeCryptocurrencies: toFiniteNumberOrNull3(
1316
1513
  globalRow.activeCryptocurrencies
1317
1514
  ),
1318
- activeExchanges: toFiniteNumberOrNull2(
1515
+ activeExchanges: toFiniteNumberOrNull3(
1319
1516
  globalRow.activeExchanges
1320
1517
  ),
1321
- activeMarketPairs: toFiniteNumberOrNull2(
1518
+ activeMarketPairs: toFiniteNumberOrNull3(
1322
1519
  globalRow.activeMarketPairs
1323
1520
  ),
1324
1521
  altLiquidityRegime
@@ -1363,22 +1560,22 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1363
1560
  asOfTs: exchangeLiquidityRow.ts.getTime(),
1364
1561
  ageMs: exchangeLiquidityRow.ageMs,
1365
1562
  stale: exchangeLiquidityRow.stale,
1366
- exchangesCount: toFiniteNumberOrNull2(
1563
+ exchangesCount: toFiniteNumberOrNull3(
1367
1564
  exchangeLiquidityRow.exchangesCount
1368
1565
  ),
1369
- totalVolumeUsd: toFiniteNumberOrNull2(
1566
+ totalVolumeUsd: toFiniteNumberOrNull3(
1370
1567
  exchangeLiquidityRow.totalVolumeUsd
1371
1568
  ),
1372
- totalVolumeChange24hPct: toFiniteNumberOrNull2(
1569
+ totalVolumeChange24hPct: toFiniteNumberOrNull3(
1373
1570
  exchangeLiquidityRow.totalVolumeChange24hPct
1374
1571
  ),
1375
- binanceVolumeUsd: toFiniteNumberOrNull2(
1572
+ binanceVolumeUsd: toFiniteNumberOrNull3(
1376
1573
  exchangeLiquidityRow.binanceVolumeUsd
1377
1574
  ),
1378
- binanceVolumeShare: toFiniteNumberOrNull2(
1575
+ binanceVolumeShare: toFiniteNumberOrNull3(
1379
1576
  exchangeLiquidityRow.binanceVolumeShare
1380
1577
  ),
1381
- topExchangeVolumeShare: toFiniteNumberOrNull2(
1578
+ topExchangeVolumeShare: toFiniteNumberOrNull3(
1382
1579
  exchangeLiquidityRow.topExchangeVolumeShare
1383
1580
  ),
1384
1581
  liquidityRegime: exchangeLiquidityRegime
@@ -1391,11 +1588,11 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1391
1588
  asOfTs: fearGreedRow.ts.getTime(),
1392
1589
  ageMs: fearGreedRow.ageMs,
1393
1590
  stale: fearGreedRow.stale,
1394
- value: toFiniteNumberOrNull2(fearGreedRow.value),
1395
- valueChange24h: toFiniteNumberOrNull2(
1591
+ value: toFiniteNumberOrNull3(fearGreedRow.value),
1592
+ valueChange24h: toFiniteNumberOrNull3(
1396
1593
  fearGreedRow.valueChange24h
1397
1594
  ),
1398
- valueChange7d: toFiniteNumberOrNull2(
1595
+ valueChange7d: toFiniteNumberOrNull3(
1399
1596
  fearGreedRow.valueChange7d
1400
1597
  ),
1401
1598
  classification: fearGreedRow.classification ?? "Unknown",
@@ -1415,13 +1612,13 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1415
1612
  cmc100Value,
1416
1613
  cmc100Change24hPct,
1417
1614
  cmc100TopConstituentSymbol: cmc100Row?.topConstituentSymbol ?? null,
1418
- cmc100TopConstituentWeightPct: toFiniteNumberOrNull2(
1615
+ cmc100TopConstituentWeightPct: toFiniteNumberOrNull3(
1419
1616
  cmc100Row?.topConstituentWeightPct
1420
1617
  ),
1421
1618
  cmc20Value,
1422
1619
  cmc20Change24hPct,
1423
1620
  cmc20TopConstituentSymbol: cmc20Row?.topConstituentSymbol ?? null,
1424
- cmc20TopConstituentWeightPct: toFiniteNumberOrNull2(
1621
+ cmc20TopConstituentWeightPct: toFiniteNumberOrNull3(
1425
1622
  cmc20Row?.topConstituentWeightPct
1426
1623
  ),
1427
1624
  cmc20ToCmc100Ratio,
@@ -1439,7 +1636,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
1439
1636
  throw error;
1440
1637
  }
1441
1638
  coinMarketCapContextUnavailable = true;
1442
- logger2.warn(
1639
+ logger3.warn(
1443
1640
  "CoinMarketCap context disabled after Timescale read failure: %s",
1444
1641
  String(error)
1445
1642
  );
@@ -1673,7 +1870,7 @@ import {
1673
1870
  getHyperliquidWhaleFlowAggregate,
1674
1871
  getHyperliquidWhaleFlowSeriesRows
1675
1872
  } from "@tradejs/infra/timescale/hyperliquidWhales";
1676
- import { logger as logger3 } from "@tradejs/infra/logger";
1873
+ import { logger as logger4 } from "@tradejs/infra/logger";
1677
1874
  var MAX_CACHE_ENTRIES = 2048;
1678
1875
  var MAX_COVERAGE_SERIES_CACHE_ENTRIES = 20;
1679
1876
  var MAX_FLOW_SERIES_CACHE_ENTRIES = 64;
@@ -1977,7 +2174,7 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
1977
2174
  throw error;
1978
2175
  }
1979
2176
  hyperliquidWhaleContextUnavailable = true;
1980
- logger3.warn(
2177
+ logger4.warn(
1981
2178
  "Hyperliquid whale context disabled after Timescale read failure: %s",
1982
2179
  String(error)
1983
2180
  );
@@ -2016,7 +2213,7 @@ var enrichSignalWithHyperliquidWhaleContext = async (params) => {
2016
2213
  };
2017
2214
 
2018
2215
  // src/strategyHelpers/runtime.ts
2019
- import { logger as logger7 } from "@tradejs/infra/logger";
2216
+ import { logger as logger8 } from "@tradejs/infra/logger";
2020
2217
  import { FEE_PERCENT } from "@tradejs/core/constants";
2021
2218
  import {
2022
2219
  buildMlFeatures,
@@ -2078,7 +2275,7 @@ import { randomUUID } from "crypto";
2078
2275
  import { TTL_1M } from "@tradejs/core/constants";
2079
2276
  import { getRuntimeStorageDayKey } from "@tradejs/core/time";
2080
2277
  import { createRuntimeOrderLinkPrefix } from "@tradejs/core/trade";
2081
- import { logger as logger4 } from "@tradejs/infra/logger";
2278
+ import { logger as logger5 } from "@tradejs/infra/logger";
2082
2279
  import {
2083
2280
  delKey,
2084
2281
  getData,
@@ -2143,7 +2340,7 @@ var recordRuntimeTradeOpen = async (params) => {
2143
2340
  )
2144
2341
  ]);
2145
2342
  } catch (error) {
2146
- logger4.error(
2343
+ logger5.error(
2147
2344
  "runtime trade open journal failed: %s %s",
2148
2345
  record.symbol,
2149
2346
  error?.message || String(error)
@@ -2211,7 +2408,7 @@ var recordRuntimeTradeIncrease = async (params) => {
2211
2408
  )
2212
2409
  ]);
2213
2410
  } catch (error) {
2214
- logger4.error(
2411
+ logger5.error(
2215
2412
  "runtime trade increase journal failed: %s %s",
2216
2413
  symbol,
2217
2414
  error?.message || String(error)
@@ -2318,7 +2515,7 @@ var markRuntimeTradeClosed = async (params) => {
2318
2515
  )
2319
2516
  ]);
2320
2517
  } catch (error) {
2321
- logger4.error(
2518
+ logger5.error(
2322
2519
  "runtime trade close journal failed: %s %s",
2323
2520
  symbol,
2324
2521
  error?.message || String(error)
@@ -2328,7 +2525,7 @@ var markRuntimeTradeClosed = async (params) => {
2328
2525
  };
2329
2526
 
2330
2527
  // src/strategyHelpers/marketContextStages.ts
2331
- import { logger as logger6 } from "@tradejs/infra/logger";
2528
+ import { logger as logger7 } from "@tradejs/infra/logger";
2332
2529
 
2333
2530
  // src/strategyHelpers/derivativesContext.ts
2334
2531
  import {
@@ -2343,7 +2540,7 @@ import {
2343
2540
  resolveDerivativesContextReferenceSymbols
2344
2541
  } from "@tradejs/core/constants";
2345
2542
  import { getDerivativesWindow } from "@tradejs/infra/timescale/derivatives";
2346
- import { logger as logger5 } from "@tradejs/infra/logger";
2543
+ import { logger as logger6 } from "@tradejs/infra/logger";
2347
2544
  var STORED_INTERVALS = ["15m", "1h"];
2348
2545
  var CONTEXT_INTERVALS = ["15m", "1h"];
2349
2546
  var DEFAULT_LOOKBACK_HOURS = 48;
@@ -2398,7 +2595,7 @@ var resolvePrimaryReferenceSymbol2 = () => PRIMARY_DERIVATIVES_REFERENCE_SYMBOL;
2398
2595
  var resolveSecondaryReferenceSymbol = () => SECONDARY_DERIVATIVES_REFERENCE_SYMBOL;
2399
2596
  var getPrimaryIntervalContext = (context) => context?.intervals["15m"] ?? context?.intervals["1h"] ?? null;
2400
2597
  var hasDerivativesSymbolData = (context) => Object.keys(context.intervals).length > 0 && !context.summary.riskFlags.includes("missing_derivatives");
2401
- var toFiniteNumberOrNull3 = (value) => {
2598
+ var toFiniteNumberOrNull4 = (value) => {
2402
2599
  if (typeof value === "number" && Number.isFinite(value)) return value;
2403
2600
  if (typeof value === "string" && value.trim()) {
2404
2601
  const parsed = Number(value);
@@ -2412,8 +2609,8 @@ var roundNullable = (value, digits = 4) => {
2412
2609
  return Math.round(value * multiplier) / multiplier;
2413
2610
  };
2414
2611
  var deltaNullable = (targetValue, referenceValue) => {
2415
- const target = toFiniteNumberOrNull3(targetValue);
2416
- const reference = toFiniteNumberOrNull3(referenceValue);
2612
+ const target = toFiniteNumberOrNull4(targetValue);
2613
+ const reference = toFiniteNumberOrNull4(referenceValue);
2417
2614
  return target == null || reference == null ? null : roundNullable(target - reference);
2418
2615
  };
2419
2616
  var buildTargetDerivedContext = (params) => {
@@ -2575,7 +2772,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
2575
2772
  throw error;
2576
2773
  }
2577
2774
  derivativesContextUnavailable = true;
2578
- logger5.warn(
2775
+ logger6.warn(
2579
2776
  "Derivatives context disabled after Timescale read failure: %s",
2580
2777
  String(error)
2581
2778
  );
@@ -2628,7 +2825,7 @@ var runMarketContextStage = async ({
2628
2825
  elapsedMs: Date.now() - startedAt
2629
2826
  };
2630
2827
  if (status === "timed_out") {
2631
- logger6.warn(
2828
+ logger7.warn(
2632
2829
  "Market context stage timed out: %s after %sms",
2633
2830
  stage,
2634
2831
  result.elapsedMs
@@ -2818,7 +3015,7 @@ var enrichSignalWithAi = async ({
2818
3015
  signal.aiAnalysis = analysis;
2819
3016
  return resolveAiQuality(analysis, direction);
2820
3017
  } catch (err) {
2821
- logger7.error("AI analysis error: %s %s", symbol, formatAiError(err));
3018
+ logger8.error("AI analysis error: %s %s", symbol, formatAiError(err));
2822
3019
  }
2823
3020
  return void 0;
2824
3021
  };
@@ -2835,75 +3032,6 @@ var enrichSignalWithMlAi = async ({
2835
3032
  await enrichSignalWithMl({ signal, env, ml });
2836
3033
  return enrichSignalWithAi({ signal, userName, symbol, direction, env, ai });
2837
3034
  };
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
3035
  var resolveRuntimeTelemetryQuality = ({
2908
3036
  signalClosePrice,
2909
3037
  arrivalMid,
@@ -2980,7 +3108,8 @@ var executeEntryOrder = async ({
2980
3108
  signal,
2981
3109
  beforePlaceOrder,
2982
3110
  recordRuntimeTrade = true,
2983
- leverage
3111
+ leverage,
3112
+ executionLimits
2984
3113
  }) => {
2985
3114
  await beforePlaceOrder?.();
2986
3115
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
@@ -2991,23 +3120,57 @@ var executeEntryOrder = async ({
2991
3120
  signal.orderQty = qty;
2992
3121
  signal.orderValue = qty * currentPrice;
2993
3122
  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;
3123
+ let arrivalSnapshot = {
3124
+ arrivalSnapshotTime: Date.now(),
3125
+ arrivalSource: "unavailable",
3126
+ bid: null,
3127
+ ask: null,
3128
+ arrivalMid: null,
3129
+ spreadBps: null
3130
+ };
3131
+ let orderSubmitTime = Date.now();
3132
+ const beforeSubmit = createEntrySubmissionValidation({
3133
+ connector,
3134
+ symbol,
3135
+ direction,
3136
+ takeProfits,
3137
+ stopLossPrice,
3138
+ limits: executionLimits,
3139
+ onValidated: (snapshot2, economics) => {
3140
+ arrivalSnapshot = snapshot2;
3141
+ orderSubmitTime = Date.now();
3142
+ if (executionLimits?.maxLossValue != null) {
3143
+ signal.riskDecision = {
3144
+ stage: "order_plan",
3145
+ status: economics.riskExceeded ? "warning" : "approved",
3146
+ reason: economics.riskExceeded ? "ENTRY_RISK_EXCEEDS_MAX_LOSS_AT_SUBMISSION" : "PLANNED_LOSS_WITHIN_MAX_LOSS_VALUE",
3147
+ plannedLossValue: economics.plannedLossValue,
3148
+ maxLossValue: executionLimits.maxLossValue,
3149
+ limitUtilization: economics.plannedLossValue / executionLimits.maxLossValue,
3150
+ enforced: false
3151
+ };
3152
+ }
3153
+ }
3154
+ });
3155
+ if (!beforeSubmit) {
3156
+ arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
3157
+ try {
3158
+ validateEntryProtectionAtArrival({
3159
+ direction,
3160
+ signalPrice: currentPrice,
3161
+ bid: arrivalSnapshot.bid,
3162
+ ask: arrivalSnapshot.ask,
3163
+ arrivalMid: arrivalSnapshot.arrivalMid,
3164
+ takeProfits,
3165
+ stopLossPrice
3166
+ });
3167
+ } catch (error) {
3168
+ signal.orderStatus = "failed";
3169
+ signal.orderFailureReason = error.message;
3170
+ throw error;
3171
+ }
3172
+ orderSubmitTime = Date.now();
3009
3173
  }
3010
- const orderSubmitTime = Date.now();
3011
3174
  const orderPlaced = await connector.placeOrder({
3012
3175
  symbol,
3013
3176
  qty,
@@ -3018,7 +3181,8 @@ var executeEntryOrder = async ({
3018
3181
  direction,
3019
3182
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
3020
3183
  orderId,
3021
- signal
3184
+ signal,
3185
+ ...beforeSubmit ? { beforeSubmit } : {}
3022
3186
  });
3023
3187
  const orderAckTime = Date.now();
3024
3188
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -3305,7 +3469,7 @@ import {
3305
3469
  createStrategyIndicatorsState,
3306
3470
  getSharedStrategyReplayState
3307
3471
  } from "@tradejs/core/strategies";
3308
- import { logger as logger9 } from "@tradejs/infra/logger";
3472
+ import { logger as logger10 } from "@tradejs/infra/logger";
3309
3473
 
3310
3474
  // src/strategy/runtimeBacktestDelay.ts
3311
3475
  import { BACKTEST_EXECUTION_INTERVAL } from "@tradejs/core/constants";
@@ -3867,7 +4031,7 @@ var canUseSharedReplayState = ({
3867
4031
  }) => (env === "BACKTEST" || env === "PARITY") && Boolean(sharedReplayKey);
3868
4032
 
3869
4033
  // src/strategy/runtimeExecution.ts
3870
- import { logger as logger8 } from "@tradejs/infra/logger";
4034
+ import { logger as logger9 } from "@tradejs/infra/logger";
3871
4035
  import {
3872
4036
  BACKTEST_WARNING_CODES
3873
4037
  } from "@tradejs/types";
@@ -3921,7 +4085,7 @@ var handleExitDecision = async ({
3921
4085
  deploymentId: connector.deploymentId
3922
4086
  });
3923
4087
  if (!activeTrade) {
3924
- logger8.warn(
4088
+ logger9.warn(
3925
4089
  "[%s] blocked closePosition for untracked runtime position: %s",
3926
4090
  strategyName ?? "unknown",
3927
4091
  symbol
@@ -3929,7 +4093,7 @@ var handleExitDecision = async ({
3929
4093
  return "CLOSE_BLOCKED_BY_UNTRACKED_POSITION";
3930
4094
  }
3931
4095
  if (!strategyName || activeTrade.strategy !== strategyName) {
3932
- logger8.warn(
4096
+ logger9.warn(
3933
4097
  "[%s] blocked closePosition for foreign runtime position: %s ownedBy=%s",
3934
4098
  strategyName ?? "unknown",
3935
4099
  symbol,
@@ -3982,7 +4146,7 @@ var handleExitDecision = async ({
3982
4146
  exitType: closedTrade?.exitType ?? "exit"
3983
4147
  });
3984
4148
  } catch (notificationError) {
3985
- logger8.error(
4149
+ logger9.error(
3986
4150
  "runtime close notification error: %s %s",
3987
4151
  symbol,
3988
4152
  notificationError
@@ -3996,7 +4160,7 @@ var handleExitDecision = async ({
3996
4160
  decision,
3997
4161
  market
3998
4162
  });
3999
- logger8.error("close order error: %s %s", symbol, err);
4163
+ logger9.error("close order error: %s %s", symbol, err);
4000
4164
  return "ORDER_ERROR";
4001
4165
  }
4002
4166
  return decision.code;
@@ -4023,7 +4187,7 @@ var handleProtectDecision = async ({
4023
4187
  decision,
4024
4188
  market
4025
4189
  });
4026
- logger8.error("protect position error: %s %s", symbol, err);
4190
+ logger9.error("protect position error: %s %s", symbol, err);
4027
4191
  return "ORDER_ERROR";
4028
4192
  }
4029
4193
  return decision.code;
@@ -4089,7 +4253,11 @@ var executeEntryDecision = async ({
4089
4253
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
4090
4254
  signal,
4091
4255
  beforePlaceOrder,
4092
- recordRuntimeTrade: recordRuntimeJournal
4256
+ recordRuntimeTrade: recordRuntimeJournal,
4257
+ executionLimits: resolveEntryExecutionLimits(
4258
+ hookCtx.strategyConfig,
4259
+ decision.entryContext.direction
4260
+ )
4093
4261
  });
4094
4262
  await invokeStageHooks(
4095
4263
  "afterPlaceOrder",
@@ -4111,19 +4279,32 @@ var executeEntryDecision = async ({
4111
4279
  return signal;
4112
4280
  }
4113
4281
  await beforePlaceOrder();
4114
- const arrivalSnapshot = await getOrderArrivalSnapshot({
4282
+ const beforeSubmit = createEntrySubmissionValidation({
4115
4283
  connector,
4116
- symbol
4117
- });
4118
- validateEntryProtectionAtArrival({
4284
+ symbol,
4119
4285
  direction: decision.entryContext.direction,
4120
- signalPrice: decision.entryContext.prices.currentPrice,
4121
- bid: arrivalSnapshot.bid,
4122
- ask: arrivalSnapshot.ask,
4123
- arrivalMid: arrivalSnapshot.arrivalMid,
4124
4286
  takeProfits: decision.orderPlan.takeProfits,
4125
- stopLossPrice: decision.orderPlan.stopLossPrice
4287
+ stopLossPrice: decision.orderPlan.stopLossPrice,
4288
+ limits: resolveEntryExecutionLimits(
4289
+ hookCtx.strategyConfig,
4290
+ decision.entryContext.direction
4291
+ )
4126
4292
  });
4293
+ if (!beforeSubmit) {
4294
+ const arrivalSnapshot = await getOrderArrivalSnapshot({
4295
+ connector,
4296
+ symbol
4297
+ });
4298
+ validateEntryProtectionAtArrival({
4299
+ direction: decision.entryContext.direction,
4300
+ signalPrice: decision.entryContext.prices.currentPrice,
4301
+ bid: arrivalSnapshot.bid,
4302
+ ask: arrivalSnapshot.ask,
4303
+ arrivalMid: arrivalSnapshot.arrivalMid,
4304
+ takeProfits: decision.orderPlan.takeProfits,
4305
+ stopLossPrice: decision.orderPlan.stopLossPrice
4306
+ });
4307
+ }
4127
4308
  const orderPlaced = await connector.placeOrder({
4128
4309
  symbol,
4129
4310
  qty: decision.orderPlan.qty,
@@ -4131,6 +4312,7 @@ var executeEntryDecision = async ({
4131
4312
  timestamp: decision.entryContext.timestamp,
4132
4313
  direction: decision.entryContext.direction,
4133
4314
  positionIntent: decision.orderPlan.positionIntent,
4315
+ ...beforeSubmit ? { beforeSubmit } : {},
4134
4316
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {}
4135
4317
  });
4136
4318
  if (!orderPlaced) {
@@ -4172,6 +4354,23 @@ var executeEntryDecision = async ({
4172
4354
  { decision, entry, market }
4173
4355
  );
4174
4356
  } catch (err) {
4357
+ if (err instanceof EntrySubmissionValidationError) {
4358
+ if (signal) {
4359
+ signal.orderStatus = "skipped";
4360
+ signal.orderSkipReason = err.message;
4361
+ signal.allocatorDecision = {
4362
+ stage: "runtime_admission",
4363
+ status: "rejected",
4364
+ reason: err.message
4365
+ };
4366
+ }
4367
+ logger9.warn(
4368
+ "entry rejected before submission: %s %s",
4369
+ symbol,
4370
+ err.message
4371
+ );
4372
+ return signal ?? err.message;
4373
+ }
4175
4374
  if (signal) {
4176
4375
  signal.orderStatus = "failed";
4177
4376
  if (typeof signal.orderFailureReason !== "string" || !signal.orderFailureReason.trim()) {
@@ -4186,9 +4385,9 @@ var executeEntryDecision = async ({
4186
4385
  market
4187
4386
  });
4188
4387
  if (err?.message === BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY) {
4189
- logger8.warn("order warning: %s %s", symbol, err);
4388
+ logger9.warn("order warning: %s %s", symbol, err);
4190
4389
  } else {
4191
- logger8.error("order error: %s %s", symbol, err);
4390
+ logger9.error("order error: %s %s", symbol, err);
4192
4391
  }
4193
4392
  return signal ?? "ORDER_ERROR";
4194
4393
  }
@@ -4296,6 +4495,9 @@ var createStrategyRuntime = ({
4296
4495
  });
4297
4496
  const strategyPolicyProfile = getPolicyProfile();
4298
4497
  const indicatorPeriods = buildDefaultIndicatorPeriods(config);
4498
+ const strategyConfig = { ...config };
4499
+ delete strategyConfig.RECORD_RUNTIME_TRADES;
4500
+ delete strategyConfig.SIMULATE_ORDERS;
4299
4501
  const hookBase = {
4300
4502
  connector,
4301
4503
  strategyName,
@@ -4306,7 +4508,7 @@ var createStrategyRuntime = ({
4306
4508
  accountId,
4307
4509
  deploymentId,
4308
4510
  policyProfileId: strategyPolicyProfile?.id ?? requestedPolicyProfileId,
4309
- strategyConfig: config,
4511
+ strategyConfig,
4310
4512
  env,
4311
4513
  isConfigFromBacktest
4312
4514
  };
@@ -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,21 +4569,21 @@ 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
4374
4576
  );
4375
4577
  }
4376
4578
  };
4377
- const invokeHook = async (stage, hook, params, errorContext = {}) => {
4579
+ const invokeHook = async (stage, hook, params, errorContext = {}, failureResult) => {
4378
4580
  if (!hook) {
4379
4581
  return void 0;
4380
4582
  }
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,
@@ -4394,17 +4596,18 @@ var createStrategyRuntime = ({
4394
4596
  entry: errorContext.entry,
4395
4597
  market: errorContext.market
4396
4598
  });
4397
- return void 0;
4599
+ return failureResult;
4398
4600
  }
4399
4601
  };
4400
- const invokeProjectHooks = async (stage, params, errorContext = {}) => {
4602
+ const invokeProjectHooks = async (stage, params, errorContext = {}, failureResult) => {
4401
4603
  const results = [];
4402
4604
  for (const hook of getProjectHookList(stage)) {
4403
4605
  const result = await invokeHook(
4404
4606
  stage,
4405
4607
  hook,
4406
4608
  params,
4407
- errorContext
4609
+ errorContext,
4610
+ failureResult
4408
4611
  );
4409
4612
  if (result !== void 0) {
4410
4613
  results.push(result);
@@ -4419,10 +4622,12 @@ var createStrategyRuntime = ({
4419
4622
  return invokeHook(stage, hook, params, errorContext);
4420
4623
  };
4421
4624
  const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
4625
+ const failureResult = stage === "beforeEntryGate" ? { allow: false, reason: "ERROR" } : void 0;
4422
4626
  const projectResults = await invokeProjectHooks(
4423
4627
  stage,
4424
4628
  params,
4425
- errorContext
4629
+ errorContext,
4630
+ failureResult
4426
4631
  );
4427
4632
  const projectBlock = projectResults.find(
4428
4633
  (result) => result?.allow === false
@@ -4434,7 +4639,8 @@ var createStrategyRuntime = ({
4434
4639
  stage,
4435
4640
  hook,
4436
4641
  params,
4437
- errorContext
4642
+ errorContext,
4643
+ failureResult
4438
4644
  );
4439
4645
  };
4440
4646
  const applyProjectAfterCoreDecisionHooks = async ({
@@ -4580,11 +4786,8 @@ var createStrategyRuntime = ({
4580
4786
  return enrichedBaseContext;
4581
4787
  }
4582
4788
  });
4583
- const coreConfig = { ...config };
4584
- delete coreConfig.RECORD_RUNTIME_TRADES;
4585
- delete coreConfig.SIMULATE_ORDERS;
4586
4789
  const core = await createCore({
4587
- config: coreConfig,
4790
+ config: strategyConfig,
4588
4791
  data,
4589
4792
  strategyApi,
4590
4793
  indicatorsState
@@ -5203,6 +5406,8 @@ var createStrategyRuntime = ({
5203
5406
  setStrategyRuntimeFactory(createStrategyRuntime);
5204
5407
 
5205
5408
  export {
5409
+ getOrderArrivalSnapshot,
5410
+ validateEntryProtectionAtArrival,
5206
5411
  buildMlPayload,
5207
5412
  BINANCE_BREADTH_UNIVERSE_KEYS,
5208
5413
  buildBinanceBreadthUniverseSnapshot,
@@ -5224,8 +5429,6 @@ export {
5224
5429
  enrichSignalWithMl,
5225
5430
  enrichSignalWithAi,
5226
5431
  enrichSignalWithMlAi,
5227
- getOrderArrivalSnapshot,
5228
- validateEntryProtectionAtArrival,
5229
5432
  executeEntryOrder,
5230
5433
  resolveStrategyConfig,
5231
5434
  RESEARCH_ENTRY_POLICY_CAPABILITIES,