@tradejs/node 2.0.18 → 2.0.20

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.
package/dist/ai.js CHANGED
@@ -48,7 +48,7 @@ __export(ai_exports, {
48
48
  });
49
49
  module.exports = __toCommonJS(ai_exports);
50
50
  var import_aiLanguages = require("@tradejs/infra/aiLanguages");
51
- var import_redis = require("@tradejs/infra/redis");
51
+ var import_redis2 = require("@tradejs/infra/redis");
52
52
  var import_userSettings = require("@tradejs/infra/userSettings");
53
53
 
54
54
  // src/aiShared.ts
@@ -624,8 +624,131 @@ var buildAiMarketContext = (signal) => ({
624
624
  });
625
625
 
626
626
  // src/strategy/manifests.ts
627
- var import_indicators = require("@tradejs/core/indicators");
628
- var import_logger2 = require("@tradejs/infra/logger");
627
+ var import_indicators2 = require("@tradejs/core/indicators");
628
+ var import_logger11 = require("@tradejs/infra/logger");
629
+
630
+ // src/strategyRuntime.ts
631
+ var import_constants5 = require("@tradejs/core/constants");
632
+ var import_strategies6 = require("@tradejs/core/strategies");
633
+ var import_logger10 = require("@tradejs/infra/logger");
634
+
635
+ // src/strategyHelpers/runtime.ts
636
+ var import_logger8 = require("@tradejs/infra/logger");
637
+ var import_constants3 = require("@tradejs/core/constants");
638
+ var import_ml2 = require("@tradejs/infra/ml");
639
+
640
+ // src/strategy/policyProfiles.ts
641
+ var profileMatches = (profile, universe, assetClass) => {
642
+ const { appliesTo } = profile;
643
+ if (!appliesTo) return true;
644
+ if (appliesTo.universes?.length && (!universe || !appliesTo.universes.includes(universe))) {
645
+ return false;
646
+ }
647
+ if (appliesTo.assetClasses?.length && (!assetClass || !appliesTo.assetClasses.includes(assetClass))) {
648
+ return false;
649
+ }
650
+ return true;
651
+ };
652
+ var resolveStrategyPolicyProfile = (manifest, params) => {
653
+ const profiles = manifest?.policyProfiles ?? [];
654
+ if (!profiles.length) {
655
+ const inferredId = params.profileId ?? (params.universe === "tradfi" ? "tradfi" : void 0);
656
+ if (!inferredId) return void 0;
657
+ if (inferredId !== "crypto" && inferredId !== "tradfi") {
658
+ throw new Error(
659
+ `Unknown policy profile "${inferredId}" for strategy "${manifest?.name}"`
660
+ );
661
+ }
662
+ if (params.universe && inferredId !== params.universe) {
663
+ throw new Error(
664
+ `Policy profile "${inferredId}" is not compatible with ${params.universe}`
665
+ );
666
+ }
667
+ return {
668
+ id: inferredId,
669
+ appliesTo: { universes: [inferredId] },
670
+ marketDataRequirements: inferredId === "crypto" ? ["crypto.btcReference"] : [],
671
+ ...manifest?.mlAdapter ? {
672
+ entryRuntimeDefaults: {
673
+ ml: {
674
+ modelKey: inferredId === "crypto" ? manifest.name : `${manifest.name}:tradfi`
675
+ }
676
+ }
677
+ } : {}
678
+ };
679
+ }
680
+ if (params.profileId) {
681
+ const profile = profiles.find(({ id }) => id === params.profileId);
682
+ if (!profile) {
683
+ throw new Error(
684
+ `Unknown policy profile "${params.profileId}" for strategy "${manifest?.name}"`
685
+ );
686
+ }
687
+ if (!profileMatches(profile, params.universe, params.assetClass)) {
688
+ throw new Error(
689
+ `Policy profile "${params.profileId}" is not compatible with ${params.universe ?? "unknown"}:${params.assetClass ?? "unknown"}`
690
+ );
691
+ }
692
+ return profile;
693
+ }
694
+ const matching = profiles.filter(
695
+ (profile) => profileMatches(profile, params.universe, params.assetClass)
696
+ );
697
+ const defaultProfile = matching.find(
698
+ ({ id }) => id === manifest?.defaultPolicyProfileId
699
+ );
700
+ return defaultProfile ?? matching[0];
701
+ };
702
+ var getStrategyProfileAiAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.aiAdapter ?? manifest?.aiAdapter;
703
+ var getStrategyProfileMlAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.mlAdapter ?? manifest?.mlAdapter;
704
+
705
+ // src/strategyAdapters/ml.ts
706
+ var defaultMlAdapter = {
707
+ normalizeStrategyConfig: (strategyConfig) => strategyConfig
708
+ };
709
+ var getStrategyMlAdapter = (strategy, profileId) => {
710
+ const strategyAdapter = getStrategyProfileMlAdapter(
711
+ getStrategyManifest(strategy),
712
+ profileId
713
+ );
714
+ if (!strategyAdapter) return defaultMlAdapter;
715
+ return {
716
+ ...defaultMlAdapter,
717
+ ...strategyAdapter
718
+ };
719
+ };
720
+
721
+ // src/mlPayload.ts
722
+ var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
723
+ return getStrategyMlAdapter(
724
+ strategyName,
725
+ profileId
726
+ ).normalizeStrategyConfig?.(strategyConfig);
727
+ };
728
+ var buildMlPayload = (payload) => {
729
+ const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
730
+ const profileId = payload.signal?.policyProfileId;
731
+ const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
732
+ const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
733
+ const nextSignal = {
734
+ ...normalizedSignal,
735
+ indicators: {
736
+ ...normalizedSignal?.indicators ?? {}
737
+ }
738
+ };
739
+ const nextContext = payload.context ? {
740
+ ...payload.context,
741
+ strategyConfig: normalizeStrategyConfig(
742
+ payload.context.strategyConfig,
743
+ strategyName,
744
+ profileId
745
+ )
746
+ } : void 0;
747
+ return {
748
+ signal: nextSignal,
749
+ context: nextContext
750
+ };
751
+ };
629
752
 
630
753
  // src/tradejsConfig.ts
631
754
  var import_fs = __toESM(require("fs"));
@@ -912,191 +1035,4663 @@ var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
912
1035
  }
913
1036
  };
914
1037
 
915
- // src/strategy/manifests.ts
916
- var createStrategyRegistryState = () => ({
917
- strategyCreators: /* @__PURE__ */ new Map(),
918
- strategyManifestsMap: /* @__PURE__ */ new Map(),
919
- pluginsLoadPromise: null
920
- });
921
- var registryStateByProjectRoot = /* @__PURE__ */ new Map();
922
- var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
923
- const projectRoot = getTradejsProjectCwd(cwd);
924
- let state = registryStateByProjectRoot.get(projectRoot);
925
- if (!state) {
926
- state = createStrategyRegistryState();
927
- registryStateByProjectRoot.set(projectRoot, state);
1038
+ // src/runtimeJournal.ts
1039
+ var import_node_crypto = require("crypto");
1040
+ var import_constants = require("@tradejs/core/constants");
1041
+ var import_time = require("@tradejs/core/time");
1042
+ var import_trade = require("@tradejs/core/trade");
1043
+ var import_logger2 = require("@tradejs/infra/logger");
1044
+ var import_redis = require("@tradejs/infra/redis");
1045
+ var now = () => Date.now();
1046
+ var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
1047
+ var calculateClosedPnl = ({
1048
+ direction,
1049
+ entryPrice,
1050
+ exitPrice,
1051
+ qty
1052
+ }) => {
1053
+ const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
1054
+ return Number.isFinite(pnl) ? pnl : null;
1055
+ };
1056
+ var createRuntimeOrderId = (strategy) => {
1057
+ const prefix = (0, import_trade.createRuntimeOrderLinkPrefix)(strategy);
1058
+ if (prefix === "tjs-") {
1059
+ return `tjs-${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
928
1060
  }
929
- return {
930
- projectRoot,
931
- state
932
- };
1061
+ return `${prefix}${toRandomOrderSuffix()}`;
933
1062
  };
934
- var toUniqueModules = (modules = []) => [
935
- ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
936
- ];
937
- var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
938
- const config = await loadTradejsConfig(cwd);
939
- return {
940
- strategyModules: toUniqueModules(config.strategies),
941
- indicatorModules: toUniqueModules(config.indicators)
1063
+ var recordRuntimeTradeOpen = async (params) => {
1064
+ const { userName } = params;
1065
+ if (!userName) {
1066
+ return null;
1067
+ }
1068
+ const record = {
1069
+ ...params,
1070
+ entryCount: 1,
1071
+ lastEntryPrice: params.entryPrice,
1072
+ lastEntryQty: params.qty,
1073
+ lastEntryTimestamp: params.entryTimestamp,
1074
+ status: "active",
1075
+ currentPrice: params.entryPrice,
1076
+ currentPnl: 0,
1077
+ closedPnl: null,
1078
+ exitPrice: null,
1079
+ exitTimestamp: null,
1080
+ lastSyncedAt: now()
942
1081
  };
1082
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(record.entryTimestamp);
1083
+ const runtimeScopeId = record.deploymentId ?? record.accountId;
1084
+ try {
1085
+ await Promise.all([
1086
+ (0, import_redis.setData)(import_redis.redisKeys.runtimeTrade(userName, record.orderId), record, {
1087
+ expire: 0
1088
+ }),
1089
+ (0, import_redis.setHashJsonField)(
1090
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1091
+ record.orderId,
1092
+ record,
1093
+ { expire: 0 }
1094
+ ),
1095
+ (0, import_redis.setData)(
1096
+ import_redis.redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
1097
+ { orderId: record.orderId },
1098
+ { expire: 0 }
1099
+ )
1100
+ ]);
1101
+ } catch (error) {
1102
+ import_logger2.logger.error(
1103
+ "runtime trade open journal failed: %s %s",
1104
+ record.symbol,
1105
+ error?.message || String(error)
1106
+ );
1107
+ }
1108
+ return record;
943
1109
  };
944
- var extractModuleEntries = (moduleExport, key) => {
945
- if (!moduleExport || typeof moduleExport !== "object") {
1110
+ var recordRuntimeTradeIncrease = async (params) => {
1111
+ const {
1112
+ userName,
1113
+ strategy,
1114
+ symbol,
1115
+ direction,
1116
+ resultingQty,
1117
+ resultingEntryPrice,
1118
+ addedQty,
1119
+ addedEntryPrice,
1120
+ entryTimestamp,
1121
+ fee,
1122
+ accountId,
1123
+ deploymentId
1124
+ } = params;
1125
+ if (!userName) {
946
1126
  return null;
947
1127
  }
948
- const candidate = moduleExport;
949
- if (Array.isArray(candidate[key])) {
950
- return candidate[key];
1128
+ const existing = await getActiveRuntimeTrade({
1129
+ userName,
1130
+ symbol,
1131
+ accountId,
1132
+ deploymentId
1133
+ });
1134
+ if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
1135
+ return null;
951
1136
  }
952
- const defaultExport = candidate.default;
953
- if (defaultExport && Array.isArray(defaultExport[key])) {
954
- return defaultExport[key];
1137
+ const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
1138
+ const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
1139
+ const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
1140
+ const next = {
1141
+ ...existing,
1142
+ qty: resultingQty,
1143
+ entryPrice: resultingEntryPrice,
1144
+ entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
1145
+ lastEntryPrice: addedEntryPrice,
1146
+ lastEntryQty: addedQty,
1147
+ lastEntryTimestamp: entryTimestamp,
1148
+ currentPrice: resultingEntryPrice,
1149
+ currentPnl: 0,
1150
+ fee: openFee,
1151
+ openFee,
1152
+ totalFee,
1153
+ lastSyncedAt: now()
1154
+ };
1155
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
1156
+ try {
1157
+ await Promise.all([
1158
+ (0, import_redis.setData)(import_redis.redisKeys.runtimeTrade(userName, existing.orderId), next, {
1159
+ expire: 0
1160
+ }),
1161
+ (0, import_redis.setHashJsonField)(
1162
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1163
+ existing.orderId,
1164
+ next,
1165
+ { expire: 0 }
1166
+ )
1167
+ ]);
1168
+ } catch (error) {
1169
+ import_logger2.logger.error(
1170
+ "runtime trade increase journal failed: %s %s",
1171
+ symbol,
1172
+ error?.message || String(error)
1173
+ );
955
1174
  }
956
- return null;
1175
+ return next;
957
1176
  };
958
- var extractStrategyPluginDefinition = (moduleExport) => {
959
- const strategyEntries = extractModuleEntries(
960
- moduleExport,
961
- "strategyEntries"
1177
+ var getActiveRuntimeTrade = async (params) => {
1178
+ const { userName, symbol, accountId, deploymentId } = params;
1179
+ if (!userName) {
1180
+ return null;
1181
+ }
1182
+ const activeRef = await (0, import_redis.getData)(
1183
+ import_redis.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
1184
+ null
962
1185
  );
963
- return strategyEntries ? { strategyEntries } : null;
964
- };
965
- var extractIndicatorPluginDefinition = (moduleExport) => {
966
- const indicatorEntries = extractModuleEntries(
967
- moduleExport,
968
- "indicatorEntries"
1186
+ const orderId = String(activeRef?.orderId || "").trim();
1187
+ if (!orderId) {
1188
+ return null;
1189
+ }
1190
+ const existing = await (0, import_redis.getData)(
1191
+ import_redis.redisKeys.runtimeTrade(userName, orderId),
1192
+ null
969
1193
  );
970
- return indicatorEntries ? { indicatorEntries } : null;
971
- };
972
- var registerEntries = (entries, source, state) => {
973
- for (const entry of entries) {
974
- const strategyName = entry.manifest?.name;
975
- if (!strategyName) {
976
- import_logger2.logger.warn("Skip strategy entry without name from %s", source);
977
- continue;
978
- }
979
- if (state.strategyCreators.has(strategyName)) {
980
- import_logger2.logger.warn(
981
- 'Skip duplicate strategy "%s" from %s: already registered',
982
- strategyName,
983
- source
984
- );
985
- continue;
986
- }
987
- state.strategyCreators.set(strategyName, entry.creator);
988
- state.strategyManifestsMap.set(strategyName, entry.manifest);
1194
+ if (!existing || existing.status !== "active") {
1195
+ await (0, import_redis.delKey)(
1196
+ import_redis.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
1197
+ );
1198
+ return null;
989
1199
  }
1200
+ return existing;
990
1201
  };
991
- var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
992
- if (typeof importTradejsModule === "function") {
993
- return importTradejsModule(moduleName, cwd);
1202
+ var markRuntimeTradeClosed = async (params) => {
1203
+ const {
1204
+ userName,
1205
+ symbol,
1206
+ strategy,
1207
+ exitPrice,
1208
+ exitTimestamp,
1209
+ closedPnl,
1210
+ exitType,
1211
+ accountId,
1212
+ deploymentId
1213
+ } = params;
1214
+ if (!userName) {
1215
+ return null;
994
1216
  }
995
- return import(
996
- /* webpackIgnore: true */
997
- moduleName
998
- );
999
- };
1000
- var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
1001
- const { projectRoot, state } = getStrategyRegistryState(cwd);
1002
- if (!state.pluginsLoadPromise) {
1003
- (0, import_indicators.resetIndicatorRegistryCache)(projectRoot);
1004
- state.pluginsLoadPromise = (async () => {
1005
- const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
1006
- const strategySet = new Set(strategyModules);
1007
- const indicatorSet = new Set(indicatorModules);
1008
- const pluginModuleNames = [
1009
- .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
1010
- ];
1011
- if (!pluginModuleNames.length) {
1012
- return;
1013
- }
1014
- for (const moduleName of pluginModuleNames) {
1015
- try {
1016
- const resolvedModuleName = resolvePluginModuleSpecifier(
1017
- moduleName,
1018
- projectRoot
1019
- );
1020
- const moduleExport = await importStrategyPluginModule(
1021
- resolvedModuleName,
1022
- projectRoot
1023
- );
1024
- if (strategySet.has(moduleName)) {
1025
- const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
1026
- if (!pluginDefinition) {
1027
- import_logger2.logger.warn(
1028
- 'Skip strategy plugin "%s": export { strategyEntries } is missing',
1029
- moduleName
1030
- );
1031
- } else {
1032
- registerEntries(
1033
- pluginDefinition.strategyEntries,
1034
- moduleName,
1035
- state
1036
- );
1037
- }
1038
- }
1039
- if (indicatorSet.has(moduleName)) {
1040
- const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
1041
- if (!indicatorPluginDefinition) {
1042
- import_logger2.logger.warn(
1043
- 'Skip indicator plugin "%s": export { indicatorEntries } is missing',
1044
- moduleName
1045
- );
1046
- } else {
1047
- (0, import_indicators.registerIndicatorEntries)(
1048
- indicatorPluginDefinition.indicatorEntries,
1049
- moduleName,
1050
- projectRoot
1051
- );
1052
- }
1053
- }
1054
- if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
1055
- import_logger2.logger.warn(
1056
- 'Skip plugin "%s": no strategy/indicator sections requested in config',
1057
- moduleName
1058
- );
1059
- }
1060
- } catch (error) {
1061
- import_logger2.logger.warn(
1062
- 'Failed to load plugin "%s": %s',
1063
- moduleName,
1064
- String(error)
1065
- );
1066
- }
1067
- }
1068
- })();
1217
+ const existing = await getActiveRuntimeTrade({
1218
+ userName,
1219
+ symbol,
1220
+ accountId,
1221
+ deploymentId
1222
+ });
1223
+ if (!existing) {
1224
+ return null;
1069
1225
  }
1070
- await state.pluginsLoadPromise;
1071
- };
1072
- var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
1073
- if (!name) {
1074
- return void 0;
1226
+ const orderId = existing.orderId;
1227
+ if (strategy && existing.strategy !== strategy) {
1228
+ return null;
1075
1229
  }
1076
- const { state } = getStrategyRegistryState(cwd);
1077
- return state.strategyManifestsMap.get(name);
1230
+ const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
1231
+ const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
1232
+ direction: existing.direction,
1233
+ entryPrice: existing.entryPrice,
1234
+ exitPrice: resolvedExitPrice,
1235
+ qty: existing.qty
1236
+ }) : existing.closedPnl ?? existing.currentPnl ?? null;
1237
+ const next = {
1238
+ ...existing,
1239
+ status: "closed",
1240
+ currentPrice: resolvedExitPrice,
1241
+ currentPnl: resolvedClosedPnl,
1242
+ closedPnl: resolvedClosedPnl,
1243
+ exitPrice: resolvedExitPrice,
1244
+ exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
1245
+ exitType: exitType ?? existing.exitType ?? null,
1246
+ lastSyncedAt: now()
1247
+ };
1248
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
1249
+ const closeDayKey = (0, import_time.getRuntimeStorageDayKey)(next.exitTimestamp);
1250
+ try {
1251
+ await Promise.all([
1252
+ (0, import_redis.setData)(import_redis.redisKeys.runtimeTrade(userName, orderId), next, {
1253
+ expire: import_constants.TTL_1M
1254
+ }),
1255
+ (0, import_redis.setHashJsonField)(
1256
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1257
+ orderId,
1258
+ next,
1259
+ { expire: import_constants.TTL_1M }
1260
+ ),
1261
+ (0, import_redis.setHashJsonField)(
1262
+ import_redis.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
1263
+ orderId,
1264
+ next,
1265
+ { expire: import_constants.TTL_1M }
1266
+ ),
1267
+ (0, import_redis.delKey)(
1268
+ import_redis.redisKeys.runtimeActiveTrade(
1269
+ userName,
1270
+ symbol,
1271
+ deploymentId ?? accountId
1272
+ )
1273
+ )
1274
+ ]);
1275
+ } catch (error) {
1276
+ import_logger2.logger.error(
1277
+ "runtime trade close journal failed: %s %s",
1278
+ symbol,
1279
+ error?.message || String(error)
1280
+ );
1281
+ }
1282
+ return next;
1078
1283
  };
1079
- var strategies = new Proxy(
1080
- {},
1081
- {
1082
- get: (_target, property) => {
1083
- if (typeof property !== "string") {
1084
- return void 0;
1085
- }
1086
- return getStrategyRegistryState().state.strategyCreators.get(property);
1284
+
1285
+ // src/strategyHelpers/marketContextStages.ts
1286
+ var import_logger7 = require("@tradejs/infra/logger");
1287
+
1288
+ // src/strategyHelpers/binanceMarketContext.ts
1289
+ var import_marketContext = require("@tradejs/infra/timescale/marketContext");
1290
+ var import_logger3 = require("@tradejs/infra/logger");
1291
+ var import_strategies = require("@tradejs/core/strategies");
1292
+
1293
+ // src/binanceBreadthUniverses.ts
1294
+ var import_node_crypto2 = require("crypto");
1295
+
1296
+ // src/config/binanceBreadthUniverses.json
1297
+ var binanceBreadthUniverses_default = {
1298
+ schemaVersion: 1,
1299
+ updatedAt: "2026-07-24T15:28:27.234Z",
1300
+ source: "binance_spot_usdt_turnover24h",
1301
+ fingerprint: "43df006e5510bb99",
1302
+ universes: {
1303
+ top5: {
1304
+ size: 5,
1305
+ fingerprint: "ba5b73456b18",
1306
+ symbols: ["ETHUSDT", "USD1USDT", "SOLUSDT", "OPNUSDT", "VANAUSDT"]
1087
1307
  },
1088
- ownKeys: () => {
1089
- return [...getStrategyRegistryState().state.strategyCreators.keys()];
1308
+ top10: {
1309
+ size: 10,
1310
+ fingerprint: "370eb5af9778",
1311
+ symbols: [
1312
+ "ETHUSDT",
1313
+ "USD1USDT",
1314
+ "SOLUSDT",
1315
+ "OPNUSDT",
1316
+ "VANAUSDT",
1317
+ "DEXEUSDT",
1318
+ "BANKUSDT",
1319
+ "ZECUSDT",
1320
+ "XRPUSDT",
1321
+ "RLUSDUSDT"
1322
+ ]
1090
1323
  },
1091
- getOwnPropertyDescriptor: () => ({
1092
- enumerable: true,
1093
- configurable: true
1094
- })
1324
+ top30: {
1325
+ size: 30,
1326
+ fingerprint: "685a7e7f5fce",
1327
+ symbols: [
1328
+ "ETHUSDT",
1329
+ "USD1USDT",
1330
+ "SOLUSDT",
1331
+ "OPNUSDT",
1332
+ "VANAUSDT",
1333
+ "DEXEUSDT",
1334
+ "BANKUSDT",
1335
+ "ZECUSDT",
1336
+ "XRPUSDT",
1337
+ "RLUSDUSDT",
1338
+ "BNBUSDT",
1339
+ "DOGEUSDT",
1340
+ "REUSDT",
1341
+ "RIFUSDT",
1342
+ "ZAMAUSDT",
1343
+ "TRXUSDT",
1344
+ "EURUSDT",
1345
+ "AEROUSDT",
1346
+ "UUSDT",
1347
+ "SUIUSDT",
1348
+ "XAUTUSDT",
1349
+ "NEARUSDT",
1350
+ "LTCUSDT",
1351
+ "SPCXBUSDT",
1352
+ "LAUSDT",
1353
+ "AVAXUSDT",
1354
+ "WLDUSDT",
1355
+ "ONDOUSDT",
1356
+ "XLMUSDT",
1357
+ "UNIUSDT"
1358
+ ]
1359
+ },
1360
+ top50: {
1361
+ size: 50,
1362
+ fingerprint: "cb9725e24141",
1363
+ symbols: [
1364
+ "ETHUSDT",
1365
+ "USD1USDT",
1366
+ "SOLUSDT",
1367
+ "OPNUSDT",
1368
+ "VANAUSDT",
1369
+ "DEXEUSDT",
1370
+ "BANKUSDT",
1371
+ "ZECUSDT",
1372
+ "XRPUSDT",
1373
+ "RLUSDUSDT",
1374
+ "BNBUSDT",
1375
+ "DOGEUSDT",
1376
+ "REUSDT",
1377
+ "RIFUSDT",
1378
+ "ZAMAUSDT",
1379
+ "TRXUSDT",
1380
+ "EURUSDT",
1381
+ "AEROUSDT",
1382
+ "UUSDT",
1383
+ "SUIUSDT",
1384
+ "XAUTUSDT",
1385
+ "NEARUSDT",
1386
+ "LTCUSDT",
1387
+ "SPCXBUSDT",
1388
+ "LAUSDT",
1389
+ "AVAXUSDT",
1390
+ "WLDUSDT",
1391
+ "ONDOUSDT",
1392
+ "XLMUSDT",
1393
+ "UNIUSDT",
1394
+ "ADAUSDT",
1395
+ "SNDKBUSDT",
1396
+ "KAITOUSDT",
1397
+ "UTKUSDT",
1398
+ "PEPEUSDT",
1399
+ "WLFIUSDT",
1400
+ "PAXGUSDT",
1401
+ "ENAUSDT",
1402
+ "TAOUSDT",
1403
+ "AAVEUSDT",
1404
+ "CRCLBUSDT",
1405
+ "LINKUSDT",
1406
+ "TONUSDT",
1407
+ "ALLOUSDT",
1408
+ "PUMPUSDT",
1409
+ "INJUSDT",
1410
+ "HOMEUSDT",
1411
+ "MUBUSDT",
1412
+ "TSLABUSDT",
1413
+ "GRAMUSDT"
1414
+ ]
1415
+ },
1416
+ top100: {
1417
+ size: 100,
1418
+ fingerprint: "f397883a81cf",
1419
+ symbols: [
1420
+ "ETHUSDT",
1421
+ "USD1USDT",
1422
+ "SOLUSDT",
1423
+ "OPNUSDT",
1424
+ "VANAUSDT",
1425
+ "DEXEUSDT",
1426
+ "BANKUSDT",
1427
+ "ZECUSDT",
1428
+ "XRPUSDT",
1429
+ "RLUSDUSDT",
1430
+ "BNBUSDT",
1431
+ "DOGEUSDT",
1432
+ "REUSDT",
1433
+ "RIFUSDT",
1434
+ "ZAMAUSDT",
1435
+ "TRXUSDT",
1436
+ "EURUSDT",
1437
+ "AEROUSDT",
1438
+ "UUSDT",
1439
+ "SUIUSDT",
1440
+ "XAUTUSDT",
1441
+ "NEARUSDT",
1442
+ "LTCUSDT",
1443
+ "SPCXBUSDT",
1444
+ "LAUSDT",
1445
+ "AVAXUSDT",
1446
+ "WLDUSDT",
1447
+ "ONDOUSDT",
1448
+ "XLMUSDT",
1449
+ "UNIUSDT",
1450
+ "ADAUSDT",
1451
+ "SNDKBUSDT",
1452
+ "KAITOUSDT",
1453
+ "UTKUSDT",
1454
+ "PEPEUSDT",
1455
+ "WLFIUSDT",
1456
+ "PAXGUSDT",
1457
+ "ENAUSDT",
1458
+ "TAOUSDT",
1459
+ "AAVEUSDT",
1460
+ "CRCLBUSDT",
1461
+ "LINKUSDT",
1462
+ "TONUSDT",
1463
+ "ALLOUSDT",
1464
+ "PUMPUSDT",
1465
+ "INJUSDT",
1466
+ "HOMEUSDT",
1467
+ "MUBUSDT",
1468
+ "TSLABUSDT",
1469
+ "GRAMUSDT",
1470
+ "\u5E01\u5B89\u4EBA\u751FUSDT",
1471
+ "SYNUSDT",
1472
+ "TRUMPUSDT",
1473
+ "PROMUSDT",
1474
+ "XPLUSDT",
1475
+ "HBARUSDT",
1476
+ "ERAUSDT",
1477
+ "BCHUSDT",
1478
+ "SOXLBUSDT",
1479
+ "DOTUSDT",
1480
+ "EPICUSDT",
1481
+ "KITEUSDT",
1482
+ "NFPUSDT",
1483
+ "KGSTUSDT",
1484
+ "FILUSDT",
1485
+ "FETUSDT",
1486
+ "KORUBUSDT",
1487
+ "ARBUSDT",
1488
+ "STXUSDT",
1489
+ "HEIUSDT",
1490
+ "BARDUSDT",
1491
+ "DASHUSDT",
1492
+ "ASTERUSDT",
1493
+ "VANRYUSDT",
1494
+ "INTCBUSDT",
1495
+ "ETCUSDT",
1496
+ "EIGENUSDT",
1497
+ "PENGUUSDT",
1498
+ "LRCUSDT",
1499
+ "VIRTUALUSDT",
1500
+ "POLUSDT",
1501
+ "ICPUSDT",
1502
+ "ZBTUSDT",
1503
+ "SKHYBUSDT",
1504
+ "SHIBUSDT",
1505
+ "LDOUSDT",
1506
+ "APTUSDT",
1507
+ "JTOUSDT",
1508
+ "NOMUSDT",
1509
+ "ZROUSDT",
1510
+ "OPUSDT",
1511
+ "BONKUSDT",
1512
+ "SKLUSDT",
1513
+ "HEMIUSDT",
1514
+ "TNSRUSDT",
1515
+ "INTWBUSDT",
1516
+ "BIOUSDT",
1517
+ "TIAUSDT",
1518
+ "ORDIUSDT",
1519
+ "XUSDUSDT"
1520
+ ]
1521
+ }
1095
1522
  }
1096
- );
1523
+ };
1097
1524
 
1098
- // src/strategy/policyProfiles.ts
1099
- var getStrategyProfileAiAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.aiAdapter ?? manifest?.aiAdapter;
1525
+ // src/binanceBreadthUniverses.ts
1526
+ var BINANCE_BREADTH_UNIVERSE_KEYS = [
1527
+ "top5",
1528
+ "top10",
1529
+ "top30",
1530
+ "top50",
1531
+ "top100"
1532
+ ];
1533
+ var fingerprint = (value, length) => (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
1534
+ var normalizeSymbols = (symbols) => symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean);
1535
+ var buildBinanceBreadthUniverseSnapshot = ({
1536
+ rankedSymbols,
1537
+ updatedAt = (/* @__PURE__ */ new Date()).toISOString()
1538
+ }) => {
1539
+ const uniqueSymbols = [...new Set(normalizeSymbols(rankedSymbols))];
1540
+ if (uniqueSymbols.length < 100) {
1541
+ throw new Error(
1542
+ `Binance breadth snapshot requires at least 100 symbols, got ${uniqueSymbols.length}`
1543
+ );
1544
+ }
1545
+ const universes = Object.fromEntries(
1546
+ BINANCE_BREADTH_UNIVERSE_KEYS.map((key) => {
1547
+ const size = Number(key.slice(3));
1548
+ const symbols = uniqueSymbols.slice(0, size);
1549
+ return [
1550
+ key,
1551
+ {
1552
+ size,
1553
+ fingerprint: fingerprint(symbols, 12),
1554
+ symbols
1555
+ }
1556
+ ];
1557
+ })
1558
+ );
1559
+ return {
1560
+ schemaVersion: 1,
1561
+ updatedAt,
1562
+ source: "binance_spot_usdt_turnover24h",
1563
+ fingerprint: fingerprint(universes, 16),
1564
+ universes
1565
+ };
1566
+ };
1567
+ var validateSnapshot = (value) => {
1568
+ const rebuilt = buildBinanceBreadthUniverseSnapshot({
1569
+ rankedSymbols: value.universes.top100.symbols,
1570
+ updatedAt: value.updatedAt
1571
+ });
1572
+ if (value.schemaVersion !== 1 || value.source !== rebuilt.source || value.fingerprint !== rebuilt.fingerprint) {
1573
+ throw new Error("Invalid Binance breadth universe snapshot fingerprint");
1574
+ }
1575
+ for (const key of BINANCE_BREADTH_UNIVERSE_KEYS) {
1576
+ const actual = value.universes[key];
1577
+ const expected = rebuilt.universes[key];
1578
+ if (actual.size !== expected.size || actual.fingerprint !== expected.fingerprint || JSON.stringify(actual.symbols) !== JSON.stringify(expected.symbols)) {
1579
+ throw new Error(`Invalid Binance breadth universe snapshot: ${key}`);
1580
+ }
1581
+ }
1582
+ return value;
1583
+ };
1584
+ var snapshot = validateSnapshot(binanceBreadthUniverses_default);
1585
+ var getBinanceBreadthUniverses = () => BINANCE_BREADTH_UNIVERSE_KEYS.map((key) => {
1586
+ const definition = snapshot.universes[key];
1587
+ return {
1588
+ key,
1589
+ size: definition.size,
1590
+ fingerprint: definition.fingerprint,
1591
+ universe: `binance_${key}_usdt_${definition.fingerprint}`,
1592
+ symbols: [...definition.symbols]
1593
+ };
1594
+ });
1595
+
1596
+ // src/strategyHelpers/marketContextErrors.ts
1597
+ var MARKET_CONTEXT_CANCELLATION_ERROR_NAMES = /* @__PURE__ */ new Set([
1598
+ "AbortError",
1599
+ "TimescaleQueryTimeoutError"
1600
+ ]);
1601
+ var isMarketContextCancellationError = (error, abortSignal) => abortSignal?.aborted === true || error instanceof Error && MARKET_CONTEXT_CANCELLATION_ERROR_NAMES.has(error.name);
1602
+
1603
+ // src/strategyHelpers/binanceMarketContext.ts
1604
+ var DEFAULT_MAX_AGE_BY_INTERVAL = {
1605
+ "1m": 3 * 6e4,
1606
+ "5m": 10 * 6e4,
1607
+ "15m": 30 * 6e4,
1608
+ "1h": 2 * 60 * 6e4
1609
+ };
1610
+ var binanceMarketContextUnavailable = false;
1611
+ var referenceRowsCache = /* @__PURE__ */ new Map();
1612
+ var breadthCache = /* @__PURE__ */ new Map();
1613
+ var parseEnabledFlag = (value, env) => {
1614
+ const normalized = String(value ?? "").trim().toLowerCase();
1615
+ if (!normalized)
1616
+ return env === "BACKTEST" || env === "CRON" || env === "PARITY";
1617
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
1618
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
1619
+ if (normalized === "backtest") return env === "BACKTEST";
1620
+ if (normalized === "live") return env !== "BACKTEST";
1621
+ return false;
1622
+ };
1623
+ var toFiniteNumberOrNull = (value) => {
1624
+ const numeric = typeof value === "number" ? value : Number(value);
1625
+ return Number.isFinite(numeric) ? numeric : null;
1626
+ };
1627
+ var signalIntervalToMarketInterval = (value) => {
1628
+ const normalized = String(value).trim().toLowerCase();
1629
+ if (normalized === "1" || normalized === "1m") return "1m";
1630
+ if (normalized === "5" || normalized === "5m") return "5m";
1631
+ if (normalized === "60" || normalized === "1h") return "1h";
1632
+ return "15m";
1633
+ };
1634
+ var resolveMarketInterval = (signal, override) => override ?? signalIntervalToMarketInterval(signal.interval);
1635
+ var getReferenceSymbols = () => {
1636
+ const symbols = (process.env.BINANCE_MARKET_CONTEXT_REFERENCE_SYMBOLS || "BTCUSDT,ETHUSDT").split(",").map((item) => item.trim().toUpperCase()).filter(Boolean);
1637
+ return symbols.length ? [...new Set(symbols)] : ["BTCUSDT", "ETHUSDT"];
1638
+ };
1639
+ var resolvePrimaryReferenceSymbol = (signalSymbol) => {
1640
+ const symbol = signalSymbol.trim().toUpperCase();
1641
+ const referenceSymbols = getReferenceSymbols();
1642
+ return referenceSymbols.includes(symbol) ? symbol : referenceSymbols[0];
1643
+ };
1644
+ var hasBaseContext = (signal) => Boolean(
1645
+ signal.additionalIndicators?.baseContext && typeof signal.additionalIndicators.baseContext === "object" && !Array.isArray(signal.additionalIndicators.baseContext)
1646
+ );
1647
+ var isBinanceMarketContextEnabled = (env) => parseEnabledFlag(process.env.BINANCE_MARKET_CONTEXT_ENABLED, env);
1648
+ var toTradeFlowContext = (row, interval) => row ? {
1649
+ source: "binance_agg_trades",
1650
+ interval,
1651
+ asOfTs: row.ts.getTime(),
1652
+ ageMs: row.ageMs,
1653
+ stale: row.stale,
1654
+ trades: toFiniteNumberOrNull(row.trades),
1655
+ buyPressurePct: toFiniteNumberOrNull(row.buyPressurePct),
1656
+ buyBaseVolume: toFiniteNumberOrNull(row.buyBaseVolume),
1657
+ sellBaseVolume: toFiniteNumberOrNull(row.sellBaseVolume),
1658
+ buyQuoteVolume: toFiniteNumberOrNull(row.buyQuoteVolume),
1659
+ sellQuoteVolume: toFiniteNumberOrNull(row.sellQuoteVolume),
1660
+ netBaseDelta: toFiniteNumberOrNull(row.netBaseDelta),
1661
+ netQuoteDelta: toFiniteNumberOrNull(row.netQuoteDelta)
1662
+ } : null;
1663
+ var getCachedReferenceRows = ({
1664
+ referenceSymbols,
1665
+ interval,
1666
+ timestamp,
1667
+ maxAgeMs,
1668
+ abortSignal
1669
+ }) => {
1670
+ const key = `${referenceSymbols.join(",")}:${interval}:${timestamp}:${maxAgeMs}`;
1671
+ const cached = referenceRowsCache.get(key);
1672
+ if (cached) return cached;
1673
+ const promise = Promise.all(
1674
+ referenceSymbols.map(async (symbol) => {
1675
+ const tradeFlow = await (0, import_marketContext.getLatestMarketTradeFlow)({
1676
+ symbol,
1677
+ interval,
1678
+ atMs: timestamp,
1679
+ maxAgeMs,
1680
+ ...abortSignal ? { signal: abortSignal } : {}
1681
+ });
1682
+ return {
1683
+ symbol,
1684
+ tradeFlow: toTradeFlowContext(tradeFlow, interval)
1685
+ };
1686
+ })
1687
+ );
1688
+ referenceRowsCache.set(key, promise);
1689
+ void promise.catch(() => referenceRowsCache.delete(key));
1690
+ return promise;
1691
+ };
1692
+ var getCachedBreadth = ({
1693
+ breadthUniverse,
1694
+ interval,
1695
+ timestamp,
1696
+ maxAgeMs,
1697
+ abortSignal
1698
+ }) => {
1699
+ const key = `${breadthUniverse}:${interval}:${timestamp}:${maxAgeMs}`;
1700
+ const cached = breadthCache.get(key);
1701
+ if (cached) return cached;
1702
+ const promise = (0, import_marketContext.getLatestMarketBreadth)({
1703
+ universe: breadthUniverse,
1704
+ interval,
1705
+ atMs: timestamp,
1706
+ maxAgeMs,
1707
+ ...abortSignal ? { signal: abortSignal } : {}
1708
+ });
1709
+ breadthCache.set(key, promise);
1710
+ void promise.catch(() => breadthCache.delete(key));
1711
+ return promise;
1712
+ };
1713
+ var toMarketBreadthContext = (breadth, interval) => ({
1714
+ source: "binance_klines",
1715
+ universe: breadth.universe,
1716
+ interval,
1717
+ asOfTs: breadth.ts.getTime(),
1718
+ ageMs: breadth.ageMs,
1719
+ stale: breadth.stale,
1720
+ symbolsCount: toFiniteNumberOrNull(breadth.symbolsCount),
1721
+ advancers: toFiniteNumberOrNull(breadth.advancers),
1722
+ decliners: toFiniteNumberOrNull(breadth.decliners),
1723
+ unchanged: toFiniteNumberOrNull(breadth.unchanged),
1724
+ advanceDeclineRatio: toFiniteNumberOrNull(breadth.advanceDeclineRatio),
1725
+ pctAboveMa20: toFiniteNumberOrNull(breadth.pctAboveMa20),
1726
+ pctAboveMa50: toFiniteNumberOrNull(breadth.pctAboveMa50),
1727
+ equalWeightedReturn: toFiniteNumberOrNull(breadth.equalWeightedReturn),
1728
+ volumeWeightedReturn: toFiniteNumberOrNull(breadth.volumeWeightedReturn),
1729
+ dispersion: toFiniteNumberOrNull(breadth.dispersion)
1730
+ });
1731
+ var enrichSignalWithBinanceMarketContext = async (params) => {
1732
+ const {
1733
+ signal,
1734
+ env,
1735
+ enabled = isBinanceMarketContextEnabled(env),
1736
+ interval = resolveMarketInterval(signal, params.interval),
1737
+ maxAgeMs = DEFAULT_MAX_AGE_BY_INTERVAL[interval]
1738
+ } = params;
1739
+ if (signal.universe === "tradfi" || !enabled || binanceMarketContextUnavailable || !hasBaseContext(signal)) {
1740
+ return false;
1741
+ }
1742
+ try {
1743
+ const referenceSymbols = getReferenceSymbols();
1744
+ const primaryReferenceSymbol = resolvePrimaryReferenceSymbol(signal.symbol);
1745
+ const breadthUniverses = params.breadthUniverse ? [
1746
+ {
1747
+ key: "top30",
1748
+ universe: params.breadthUniverse
1749
+ }
1750
+ ] : getBinanceBreadthUniverses();
1751
+ const [referenceRows, breadthRows] = await Promise.all([
1752
+ getCachedReferenceRows({
1753
+ referenceSymbols,
1754
+ interval,
1755
+ timestamp: signal.timestamp,
1756
+ maxAgeMs,
1757
+ abortSignal: params.abortSignal
1758
+ }),
1759
+ Promise.all(
1760
+ breadthUniverses.map(async ({ key, universe }) => ({
1761
+ key,
1762
+ breadth: await getCachedBreadth({
1763
+ breadthUniverse: universe,
1764
+ interval,
1765
+ timestamp: signal.timestamp,
1766
+ maxAgeMs,
1767
+ abortSignal: params.abortSignal
1768
+ })
1769
+ }))
1770
+ )
1771
+ ]);
1772
+ const availableBreadths = breadthRows.filter(
1773
+ (row) => row.breadth != null
1774
+ );
1775
+ const marketBreadths = Object.fromEntries(
1776
+ availableBreadths.map(({ key, breadth }) => [
1777
+ key,
1778
+ toMarketBreadthContext(breadth, interval)
1779
+ ])
1780
+ );
1781
+ const primaryBreadth = availableBreadths.find(
1782
+ ({ key }) => key === "top30"
1783
+ )?.breadth;
1784
+ const tradeFlowBySymbol = Object.fromEntries(
1785
+ referenceRows.filter((row) => row.tradeFlow).map((row) => [row.symbol, row.tradeFlow])
1786
+ );
1787
+ const targetReferenceSymbol = signal.symbol.trim().toUpperCase();
1788
+ const targetTradeFlow = tradeFlowBySymbol[targetReferenceSymbol];
1789
+ if (!Object.keys(tradeFlowBySymbol).length && !availableBreadths.length) {
1790
+ return false;
1791
+ }
1792
+ const baseContext = signal.additionalIndicators.baseContext;
1793
+ signal.additionalIndicators = {
1794
+ ...signal.additionalIndicators,
1795
+ baseContext: {
1796
+ ...baseContext,
1797
+ participation: {
1798
+ ...baseContext.participation,
1799
+ ...targetTradeFlow ? {
1800
+ tradeFlow: targetTradeFlow
1801
+ } : {}
1802
+ },
1803
+ relative: {
1804
+ ...baseContext.relative,
1805
+ execution: {
1806
+ ...baseContext.relative.execution
1807
+ },
1808
+ ...Object.keys(tradeFlowBySymbol).length ? {
1809
+ referenceTradeFlow: {
1810
+ source: "binance_reference_market",
1811
+ primaryReferenceSymbol,
1812
+ referenceSymbols,
1813
+ tradeFlowBySymbol
1814
+ }
1815
+ } : {},
1816
+ ...availableBreadths.length ? {
1817
+ marketBreadths,
1818
+ ...primaryBreadth ? {
1819
+ marketBreadth: toMarketBreadthContext(
1820
+ primaryBreadth,
1821
+ interval
1822
+ ),
1823
+ btcAltRegime: {
1824
+ source: "binance_klines",
1825
+ universe: primaryBreadth.universe,
1826
+ interval,
1827
+ asOfTs: primaryBreadth.ts.getTime(),
1828
+ ageMs: primaryBreadth.ageMs,
1829
+ stale: primaryBreadth.stale,
1830
+ btcReturn1h: toFiniteNumberOrNull(
1831
+ primaryBreadth.btcReturn1h
1832
+ ),
1833
+ btcReturn4h: toFiniteNumberOrNull(
1834
+ primaryBreadth.btcReturn4h
1835
+ ),
1836
+ btcReturn24h: toFiniteNumberOrNull(
1837
+ primaryBreadth.btcReturn24h
1838
+ ),
1839
+ altBasketReturn1h: toFiniteNumberOrNull(
1840
+ primaryBreadth.altBasketReturn1h
1841
+ ),
1842
+ altBasketReturn4h: toFiniteNumberOrNull(
1843
+ primaryBreadth.altBasketReturn4h
1844
+ ),
1845
+ altBasketReturn24h: toFiniteNumberOrNull(
1846
+ primaryBreadth.altBasketReturn24h
1847
+ ),
1848
+ btcVsAltReturn1h: toFiniteNumberOrNull(
1849
+ primaryBreadth.btcVsAltReturn1h
1850
+ ),
1851
+ btcVsAltReturn4h: toFiniteNumberOrNull(
1852
+ primaryBreadth.btcVsAltReturn4h
1853
+ ),
1854
+ btcVsAltReturn24h: toFiniteNumberOrNull(
1855
+ primaryBreadth.btcVsAltReturn24h
1856
+ ),
1857
+ btcTurnoverShare1h: toFiniteNumberOrNull(
1858
+ primaryBreadth.btcTurnoverShare1h
1859
+ ),
1860
+ btcTurnoverShare24h: toFiniteNumberOrNull(
1861
+ primaryBreadth.btcTurnoverShare24h
1862
+ ),
1863
+ btcTurnoverShareChange24h: toFiniteNumberOrNull(
1864
+ primaryBreadth.btcTurnoverShareChange24h
1865
+ ),
1866
+ altVolToBtcVol24h: toFiniteNumberOrNull(
1867
+ primaryBreadth.altVolToBtcVol24h
1868
+ ),
1869
+ altDispersion24h: toFiniteNumberOrNull(
1870
+ primaryBreadth.altDispersion24h
1871
+ ),
1872
+ regime: primaryBreadth.btcAltRegime ?? "unknown"
1873
+ }
1874
+ } : {}
1875
+ } : {}
1876
+ }
1877
+ }
1878
+ };
1879
+ (0, import_strategies.refreshSignalBaseContextGateFeatures)(signal);
1880
+ return true;
1881
+ } catch (error) {
1882
+ if (isMarketContextCancellationError(error, params.abortSignal)) {
1883
+ throw error;
1884
+ }
1885
+ binanceMarketContextUnavailable = true;
1886
+ import_logger3.logger.warn(
1887
+ "Binance market context disabled after Timescale read failure: %s",
1888
+ String(error)
1889
+ );
1890
+ return false;
1891
+ }
1892
+ };
1893
+
1894
+ // src/strategyHelpers/coinMarketCapContext.ts
1895
+ var import_strategies2 = require("@tradejs/core/strategies");
1896
+ var import_logger4 = require("@tradejs/infra/logger");
1897
+ var import_marketContext2 = require("@tradejs/infra/timescale/marketContext");
1898
+ var DEFAULT_MAX_AGE_MS = 48 * 60 * 6e4;
1899
+ var SOURCE_GLOBAL_DAILY = "coinmarketcap_global";
1900
+ var SOURCE_REFERENCE = "coinmarketcap_reference_asset";
1901
+ var SOURCE_EXCHANGE_LIQUIDITY = "coinmarketcap_exchange_liquidity";
1902
+ var SOURCE_FEAR_GREED = "coinmarketcap_fear_greed";
1903
+ var SOURCE_INDEX = "coinmarketcap_index";
1904
+ var DAY_MS = 864e5;
1905
+ var coinMarketCapContextUnavailable = false;
1906
+ var globalContextCache = /* @__PURE__ */ new Map();
1907
+ var referenceContextCache = /* @__PURE__ */ new Map();
1908
+ var exchangeLiquidityContextCache = /* @__PURE__ */ new Map();
1909
+ var fearGreedContextCache = /* @__PURE__ */ new Map();
1910
+ var indexContextCache = /* @__PURE__ */ new Map();
1911
+ var parseEnabledFlag2 = (value, env) => {
1912
+ const normalized = String(value ?? "").trim().toLowerCase();
1913
+ if (!normalized) {
1914
+ return env === "BACKTEST" || env === "PARITY" || env === "CRON";
1915
+ }
1916
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
1917
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
1918
+ if (normalized === "backtest") return env === "BACKTEST";
1919
+ if (normalized === "live") return env !== "BACKTEST";
1920
+ return false;
1921
+ };
1922
+ var asInt = (value, fallback) => {
1923
+ const parsed = Number.parseInt(String(value ?? ""), 10);
1924
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
1925
+ };
1926
+ var toFiniteNumberOrNull2 = (value) => {
1927
+ const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
1928
+ return Number.isFinite(numeric) ? numeric : null;
1929
+ };
1930
+ var safeDivide = (numerator, denominator) => numerator != null && denominator != null && denominator > 0 ? numerator / denominator : null;
1931
+ var hasBaseContext2 = (signal) => Boolean(
1932
+ signal.additionalIndicators?.baseContext && typeof signal.additionalIndicators.baseContext === "object" && !Array.isArray(signal.additionalIndicators.baseContext)
1933
+ );
1934
+ var resolveMaxAgeMs = () => asInt(process.env.COINMARKETCAP_CONTEXT_MAX_AGE_MS, DEFAULT_MAX_AGE_MS);
1935
+ var toAltLiquidityRegime = ({
1936
+ stale,
1937
+ btcDominanceChange24hPct,
1938
+ altMarketCapChange24hPct,
1939
+ altVolumeChange24hPct
1940
+ }) => {
1941
+ if (stale) return "unknown";
1942
+ if (altMarketCapChange24hPct != null && altMarketCapChange24hPct <= -0.03 || altVolumeChange24hPct != null && altVolumeChange24hPct <= -0.15) {
1943
+ return "risk_off";
1944
+ }
1945
+ if (btcDominanceChange24hPct != null && btcDominanceChange24hPct >= 0.25) {
1946
+ return "btc_favored";
1947
+ }
1948
+ if (btcDominanceChange24hPct != null && btcDominanceChange24hPct <= -0.25 && (altMarketCapChange24hPct == null || altMarketCapChange24hPct >= 0)) {
1949
+ return "alt_friendly";
1950
+ }
1951
+ return "neutral";
1952
+ };
1953
+ var toReferenceLiquidityRegime = ({
1954
+ stale,
1955
+ ethBtcMarketCapRatioChange24hPct,
1956
+ ethVsBtcVolumeRatio
1957
+ }) => {
1958
+ if (stale) return "unknown";
1959
+ if (ethVsBtcVolumeRatio != null && ethVsBtcVolumeRatio < 0.15) return "thin";
1960
+ if (ethBtcMarketCapRatioChange24hPct != null && ethBtcMarketCapRatioChange24hPct >= 0.01) {
1961
+ return "eth_led";
1962
+ }
1963
+ if (ethBtcMarketCapRatioChange24hPct != null && ethBtcMarketCapRatioChange24hPct <= -0.01) {
1964
+ return "btc_led";
1965
+ }
1966
+ return "balanced";
1967
+ };
1968
+ var toExchangeLiquidityRegime = ({
1969
+ stale,
1970
+ totalVolumeChange24hPct,
1971
+ fallback
1972
+ }) => {
1973
+ if (stale) return "unknown";
1974
+ if (totalVolumeChange24hPct != null && totalVolumeChange24hPct >= 0.15) {
1975
+ return "expanding";
1976
+ }
1977
+ if (totalVolumeChange24hPct != null && totalVolumeChange24hPct <= -0.15) {
1978
+ return "contracting";
1979
+ }
1980
+ return fallback;
1981
+ };
1982
+ var toIndexRegime = ({
1983
+ stale,
1984
+ cmc100Change24hPct,
1985
+ cmc20Change24hPct,
1986
+ cmc20ToCmc100RatioChange24hPct
1987
+ }) => {
1988
+ if (stale) return "unknown";
1989
+ if (cmc100Change24hPct == null && cmc20Change24hPct == null) {
1990
+ return "unknown";
1991
+ }
1992
+ if ((cmc100Change24hPct ?? 0) <= -0.02 && (cmc20Change24hPct ?? 0) <= -0.02) {
1993
+ return "risk_off";
1994
+ }
1995
+ if ((cmc20ToCmc100RatioChange24hPct ?? 0) >= 5e-3) {
1996
+ return "top20_led";
1997
+ }
1998
+ if ((cmc20ToCmc100RatioChange24hPct ?? 0) <= -5e-3) {
1999
+ return "large_cap_led";
2000
+ }
2001
+ return "balanced";
2002
+ };
2003
+ var getCachedGlobalContext = ({
2004
+ timestamp,
2005
+ maxAgeMs,
2006
+ abortSignal
2007
+ }) => {
2008
+ const key = `${SOURCE_GLOBAL_DAILY}:${timestamp}:${maxAgeMs}`;
2009
+ const cached = globalContextCache.get(key);
2010
+ if (cached) return cached;
2011
+ const promise = (0, import_marketContext2.getLatestMarketGlobalContext)({
2012
+ source: SOURCE_GLOBAL_DAILY,
2013
+ atMs: timestamp,
2014
+ maxAgeMs,
2015
+ ...abortSignal ? { signal: abortSignal } : {}
2016
+ });
2017
+ globalContextCache.set(key, promise);
2018
+ void promise.catch(() => globalContextCache.delete(key));
2019
+ return promise;
2020
+ };
2021
+ var getCachedReferenceContexts = ({
2022
+ timestamp,
2023
+ maxAgeMs,
2024
+ abortSignal
2025
+ }) => {
2026
+ const key = `${SOURCE_REFERENCE}:1d:${timestamp}:${maxAgeMs}`;
2027
+ const cached = referenceContextCache.get(key);
2028
+ if (cached) return cached;
2029
+ const promise = (0, import_marketContext2.getLatestMarketReferenceAssetContexts)({
2030
+ source: SOURCE_REFERENCE,
2031
+ symbols: ["BTCUSDT", "ETHUSDT"],
2032
+ interval: "1d",
2033
+ atMs: timestamp,
2034
+ maxAgeMs,
2035
+ ...abortSignal ? { signal: abortSignal } : {}
2036
+ });
2037
+ referenceContextCache.set(key, promise);
2038
+ void promise.catch(() => referenceContextCache.delete(key));
2039
+ return promise;
2040
+ };
2041
+ var getCachedExchangeLiquidityContext = ({
2042
+ timestamp,
2043
+ maxAgeMs,
2044
+ abortSignal
2045
+ }) => {
2046
+ const key = `${SOURCE_EXCHANGE_LIQUIDITY}:1d:${timestamp}:${maxAgeMs}`;
2047
+ const cached = exchangeLiquidityContextCache.get(key);
2048
+ if (cached) return cached;
2049
+ const promise = (0, import_marketContext2.getLatestMarketCmcExchangeLiquidityContext)({
2050
+ source: SOURCE_EXCHANGE_LIQUIDITY,
2051
+ interval: "1d",
2052
+ atMs: timestamp,
2053
+ maxAgeMs,
2054
+ ...abortSignal ? { signal: abortSignal } : {}
2055
+ });
2056
+ exchangeLiquidityContextCache.set(key, promise);
2057
+ void promise.catch(() => exchangeLiquidityContextCache.delete(key));
2058
+ return promise;
2059
+ };
2060
+ var getCachedFearGreedContext = ({
2061
+ timestamp,
2062
+ maxAgeMs,
2063
+ abortSignal
2064
+ }) => {
2065
+ const key = `${SOURCE_FEAR_GREED}:1d:${timestamp}:${maxAgeMs}`;
2066
+ const cached = fearGreedContextCache.get(key);
2067
+ if (cached) return cached;
2068
+ const promise = (0, import_marketContext2.getLatestMarketCmcFearGreedContext)({
2069
+ source: SOURCE_FEAR_GREED,
2070
+ interval: "1d",
2071
+ atMs: timestamp,
2072
+ maxAgeMs,
2073
+ ...abortSignal ? { signal: abortSignal } : {}
2074
+ });
2075
+ fearGreedContextCache.set(key, promise);
2076
+ void promise.catch(() => fearGreedContextCache.delete(key));
2077
+ return promise;
2078
+ };
2079
+ var getCachedIndexContexts = ({
2080
+ timestamp,
2081
+ maxAgeMs,
2082
+ abortSignal
2083
+ }) => {
2084
+ const key = `${SOURCE_INDEX}:1d:${timestamp}:${maxAgeMs}`;
2085
+ const cached = indexContextCache.get(key);
2086
+ if (cached) return cached;
2087
+ const promise = (0, import_marketContext2.getLatestMarketCmcIndexContexts)({
2088
+ source: SOURCE_INDEX,
2089
+ indexSlugs: ["cmc100", "cmc20"],
2090
+ interval: "1d",
2091
+ atMs: timestamp,
2092
+ maxAgeMs,
2093
+ ...abortSignal ? { signal: abortSignal } : {}
2094
+ });
2095
+ indexContextCache.set(key, promise);
2096
+ void promise.catch(() => indexContextCache.delete(key));
2097
+ return promise;
2098
+ };
2099
+ var isCoinMarketCapContextEnabled = (env) => parseEnabledFlag2(process.env.COINMARKETCAP_CONTEXT_ENABLED, env);
2100
+ var enrichSignalWithCoinMarketCapContext = async (params) => {
2101
+ const {
2102
+ signal,
2103
+ env,
2104
+ enabled = isCoinMarketCapContextEnabled(env),
2105
+ maxAgeMs = resolveMaxAgeMs()
2106
+ } = params;
2107
+ if (signal.universe === "tradfi" || !enabled || coinMarketCapContextUnavailable || !hasBaseContext2(signal)) {
2108
+ return false;
2109
+ }
2110
+ try {
2111
+ const [
2112
+ globalDailyRow,
2113
+ dailyReferences,
2114
+ previousDailyReferences,
2115
+ exchangeLiquidityRow,
2116
+ fearGreedRow,
2117
+ indexRows
2118
+ ] = await Promise.all([
2119
+ getCachedGlobalContext({
2120
+ timestamp: signal.timestamp,
2121
+ maxAgeMs,
2122
+ abortSignal: params.abortSignal
2123
+ }),
2124
+ getCachedReferenceContexts({
2125
+ timestamp: signal.timestamp,
2126
+ maxAgeMs,
2127
+ abortSignal: params.abortSignal
2128
+ }),
2129
+ getCachedReferenceContexts({
2130
+ timestamp: signal.timestamp - DAY_MS,
2131
+ maxAgeMs: maxAgeMs + DAY_MS,
2132
+ abortSignal: params.abortSignal
2133
+ }),
2134
+ getCachedExchangeLiquidityContext({
2135
+ timestamp: signal.timestamp,
2136
+ maxAgeMs,
2137
+ abortSignal: params.abortSignal
2138
+ }),
2139
+ getCachedFearGreedContext({
2140
+ timestamp: signal.timestamp,
2141
+ maxAgeMs,
2142
+ abortSignal: params.abortSignal
2143
+ }),
2144
+ getCachedIndexContexts({
2145
+ timestamp: signal.timestamp,
2146
+ maxAgeMs,
2147
+ abortSignal: params.abortSignal
2148
+ })
2149
+ ]);
2150
+ const globalRow = globalDailyRow;
2151
+ const references = dailyReferences;
2152
+ const previousReferences = previousDailyReferences;
2153
+ if (!globalRow && !references.size && !exchangeLiquidityRow && !fearGreedRow && !indexRows.size) {
2154
+ return false;
2155
+ }
2156
+ const btcRow = references.get("BTCUSDT") ?? null;
2157
+ const ethRow = references.get("ETHUSDT") ?? null;
2158
+ const previousBtcRow = previousReferences.get("BTCUSDT") ?? null;
2159
+ const previousEthRow = previousReferences.get("ETHUSDT") ?? null;
2160
+ const btcMarketCapUsd = toFiniteNumberOrNull2(btcRow?.marketCapUsd);
2161
+ const ethMarketCapUsd = toFiniteNumberOrNull2(ethRow?.marketCapUsd);
2162
+ const previousBtcMarketCapUsd = toFiniteNumberOrNull2(
2163
+ previousBtcRow?.marketCapUsd
2164
+ );
2165
+ const previousEthMarketCapUsd = toFiniteNumberOrNull2(
2166
+ previousEthRow?.marketCapUsd
2167
+ );
2168
+ const ethBtcMarketCapRatio = safeDivide(ethMarketCapUsd, btcMarketCapUsd);
2169
+ const previousEthBtcMarketCapRatio = safeDivide(
2170
+ previousEthMarketCapUsd,
2171
+ previousBtcMarketCapUsd
2172
+ );
2173
+ const ethBtcMarketCapRatioChange24hPct = ethBtcMarketCapRatio != null && previousEthBtcMarketCapRatio != null && previousEthBtcMarketCapRatio > 0 ? (ethBtcMarketCapRatio - previousEthBtcMarketCapRatio) / previousEthBtcMarketCapRatio : null;
2174
+ const btcVolumeUsd = toFiniteNumberOrNull2(btcRow?.volumeUsd);
2175
+ const ethVolumeUsd = toFiniteNumberOrNull2(ethRow?.volumeUsd);
2176
+ const referenceStale = btcRow?.stale === true || ethRow?.stale === true || !btcRow || !ethRow;
2177
+ const btcDominanceChange24hPct = toFiniteNumberOrNull2(
2178
+ globalRow?.btcDominanceChange24hPct
2179
+ );
2180
+ const altMarketCapChange24hPct = toFiniteNumberOrNull2(
2181
+ globalRow?.altMarketCapChange24hPct
2182
+ );
2183
+ const altVolumeChange24hPct = toFiniteNumberOrNull2(
2184
+ globalRow?.altVolumeChange24hPct
2185
+ );
2186
+ const altLiquidityRegime = globalRow ? toAltLiquidityRegime({
2187
+ stale: globalRow.stale,
2188
+ btcDominanceChange24hPct,
2189
+ altMarketCapChange24hPct,
2190
+ altVolumeChange24hPct
2191
+ }) : "unknown";
2192
+ const exchangeLiquidityRegime = exchangeLiquidityRow ? toExchangeLiquidityRegime({
2193
+ stale: exchangeLiquidityRow.stale,
2194
+ totalVolumeChange24hPct: toFiniteNumberOrNull2(
2195
+ exchangeLiquidityRow.totalVolumeChange24hPct
2196
+ ),
2197
+ fallback: exchangeLiquidityRow.liquidityRegime ?? "unknown"
2198
+ }) : "unknown";
2199
+ const cmc100Row = indexRows.get("cmc100") ?? null;
2200
+ const cmc20Row = indexRows.get("cmc20") ?? null;
2201
+ const cmc100Value = toFiniteNumberOrNull2(cmc100Row?.value);
2202
+ const cmc20Value = toFiniteNumberOrNull2(cmc20Row?.value);
2203
+ const cmc100Change24hPct = toFiniteNumberOrNull2(
2204
+ cmc100Row?.valueChange24hPct
2205
+ );
2206
+ const cmc20Change24hPct = toFiniteNumberOrNull2(cmc20Row?.valueChange24hPct);
2207
+ const cmc20ToCmc100Ratio = safeDivide(cmc20Value, cmc100Value);
2208
+ const cmc20ToCmc100RatioChange24hPct = cmc20Change24hPct != null && cmc100Change24hPct != null ? (1 + cmc20Change24hPct) / (1 + cmc100Change24hPct) - 1 : null;
2209
+ const indexStale = cmc100Row?.stale === true || cmc20Row?.stale === true || !cmc100Row || !cmc20Row;
2210
+ const indexRegime = toIndexRegime({
2211
+ stale: indexStale,
2212
+ cmc100Change24hPct,
2213
+ cmc20Change24hPct,
2214
+ cmc20ToCmc100RatioChange24hPct
2215
+ });
2216
+ const baseContext = signal.additionalIndicators.baseContext;
2217
+ signal.additionalIndicators = {
2218
+ ...signal.additionalIndicators,
2219
+ baseContext: {
2220
+ ...baseContext,
2221
+ relative: {
2222
+ ...baseContext.relative,
2223
+ ...globalRow ? {
2224
+ cmcGlobal: {
2225
+ source: globalRow.source,
2226
+ interval: "1d",
2227
+ asOfTs: globalRow.ts.getTime(),
2228
+ ageMs: globalRow.ageMs,
2229
+ stale: globalRow.stale,
2230
+ totalMarketCapUsd: toFiniteNumberOrNull2(
2231
+ globalRow.totalMarketCapUsd
2232
+ ),
2233
+ totalVolumeUsd: toFiniteNumberOrNull2(
2234
+ globalRow.totalVolumeUsd
2235
+ ),
2236
+ totalVolumeReportedUsd: toFiniteNumberOrNull2(
2237
+ globalRow.totalVolumeReportedUsd
2238
+ ),
2239
+ altMarketCapUsd: toFiniteNumberOrNull2(
2240
+ globalRow.altMarketCapUsd
2241
+ ),
2242
+ altVolumeUsd: toFiniteNumberOrNull2(globalRow.altVolumeUsd),
2243
+ altVolumeReportedUsd: toFiniteNumberOrNull2(
2244
+ globalRow.altVolumeReportedUsd
2245
+ ),
2246
+ btcDominancePct: toFiniteNumberOrNull2(
2247
+ globalRow.btcDominancePct
2248
+ ),
2249
+ ethDominancePct: toFiniteNumberOrNull2(
2250
+ globalRow.ethDominancePct
2251
+ ),
2252
+ btcDominanceChange24hPct,
2253
+ ethDominanceChange24hPct: toFiniteNumberOrNull2(
2254
+ globalRow.ethDominanceChange24hPct
2255
+ ),
2256
+ altMarketCapChange24hPct,
2257
+ altVolumeChange24hPct,
2258
+ activeCryptocurrencies: toFiniteNumberOrNull2(
2259
+ globalRow.activeCryptocurrencies
2260
+ ),
2261
+ activeExchanges: toFiniteNumberOrNull2(
2262
+ globalRow.activeExchanges
2263
+ ),
2264
+ activeMarketPairs: toFiniteNumberOrNull2(
2265
+ globalRow.activeMarketPairs
2266
+ ),
2267
+ altLiquidityRegime
2268
+ }
2269
+ } : {},
2270
+ ...btcRow || ethRow ? {
2271
+ cmcReferenceAssets: {
2272
+ source: SOURCE_REFERENCE,
2273
+ interval: "1d",
2274
+ asOfTs: Math.max(
2275
+ btcRow?.ts.getTime() ?? 0,
2276
+ ethRow?.ts.getTime() ?? 0
2277
+ ),
2278
+ ageMs: btcRow?.ageMs != null && ethRow?.ageMs != null ? Math.max(btcRow.ageMs, ethRow.ageMs) : btcRow?.ageMs ?? ethRow?.ageMs ?? null,
2279
+ stale: referenceStale,
2280
+ btcMarketCapUsd,
2281
+ ethMarketCapUsd,
2282
+ btcVolumeUsd,
2283
+ ethVolumeUsd,
2284
+ btcVolumeToMarketCap: safeDivide(
2285
+ btcVolumeUsd,
2286
+ btcMarketCapUsd
2287
+ ),
2288
+ ethVolumeToMarketCap: safeDivide(
2289
+ ethVolumeUsd,
2290
+ ethMarketCapUsd
2291
+ ),
2292
+ ethBtcMarketCapRatio,
2293
+ ethBtcMarketCapRatioChange24hPct,
2294
+ ethVsBtcVolumeRatio: safeDivide(ethVolumeUsd, btcVolumeUsd),
2295
+ referenceLiquidityRegime: toReferenceLiquidityRegime({
2296
+ stale: referenceStale,
2297
+ ethBtcMarketCapRatioChange24hPct,
2298
+ ethVsBtcVolumeRatio: safeDivide(ethVolumeUsd, btcVolumeUsd)
2299
+ })
2300
+ }
2301
+ } : {},
2302
+ ...exchangeLiquidityRow ? {
2303
+ cmcExchangeLiquidity: {
2304
+ source: SOURCE_EXCHANGE_LIQUIDITY,
2305
+ interval: exchangeLiquidityRow.interval,
2306
+ asOfTs: exchangeLiquidityRow.ts.getTime(),
2307
+ ageMs: exchangeLiquidityRow.ageMs,
2308
+ stale: exchangeLiquidityRow.stale,
2309
+ exchangesCount: toFiniteNumberOrNull2(
2310
+ exchangeLiquidityRow.exchangesCount
2311
+ ),
2312
+ totalVolumeUsd: toFiniteNumberOrNull2(
2313
+ exchangeLiquidityRow.totalVolumeUsd
2314
+ ),
2315
+ totalVolumeChange24hPct: toFiniteNumberOrNull2(
2316
+ exchangeLiquidityRow.totalVolumeChange24hPct
2317
+ ),
2318
+ binanceVolumeUsd: toFiniteNumberOrNull2(
2319
+ exchangeLiquidityRow.binanceVolumeUsd
2320
+ ),
2321
+ binanceVolumeShare: toFiniteNumberOrNull2(
2322
+ exchangeLiquidityRow.binanceVolumeShare
2323
+ ),
2324
+ topExchangeVolumeShare: toFiniteNumberOrNull2(
2325
+ exchangeLiquidityRow.topExchangeVolumeShare
2326
+ ),
2327
+ liquidityRegime: exchangeLiquidityRegime
2328
+ }
2329
+ } : {},
2330
+ ...fearGreedRow ? {
2331
+ cmcFearGreed: {
2332
+ source: SOURCE_FEAR_GREED,
2333
+ interval: "1d",
2334
+ asOfTs: fearGreedRow.ts.getTime(),
2335
+ ageMs: fearGreedRow.ageMs,
2336
+ stale: fearGreedRow.stale,
2337
+ value: toFiniteNumberOrNull2(fearGreedRow.value),
2338
+ valueChange24h: toFiniteNumberOrNull2(
2339
+ fearGreedRow.valueChange24h
2340
+ ),
2341
+ valueChange7d: toFiniteNumberOrNull2(
2342
+ fearGreedRow.valueChange7d
2343
+ ),
2344
+ classification: fearGreedRow.classification ?? "Unknown",
2345
+ sentimentRegime: fearGreedRow.sentimentRegime ?? "unknown"
2346
+ }
2347
+ } : {},
2348
+ ...cmc100Row || cmc20Row ? {
2349
+ cmcIndexes: {
2350
+ source: SOURCE_INDEX,
2351
+ interval: "1d",
2352
+ asOfTs: Math.max(
2353
+ cmc100Row?.ts.getTime() ?? 0,
2354
+ cmc20Row?.ts.getTime() ?? 0
2355
+ ),
2356
+ ageMs: cmc100Row?.ageMs != null && cmc20Row?.ageMs != null ? Math.max(cmc100Row.ageMs, cmc20Row.ageMs) : cmc100Row?.ageMs ?? cmc20Row?.ageMs ?? null,
2357
+ stale: indexStale,
2358
+ cmc100Value,
2359
+ cmc100Change24hPct,
2360
+ cmc100TopConstituentSymbol: cmc100Row?.topConstituentSymbol ?? null,
2361
+ cmc100TopConstituentWeightPct: toFiniteNumberOrNull2(
2362
+ cmc100Row?.topConstituentWeightPct
2363
+ ),
2364
+ cmc20Value,
2365
+ cmc20Change24hPct,
2366
+ cmc20TopConstituentSymbol: cmc20Row?.topConstituentSymbol ?? null,
2367
+ cmc20TopConstituentWeightPct: toFiniteNumberOrNull2(
2368
+ cmc20Row?.topConstituentWeightPct
2369
+ ),
2370
+ cmc20ToCmc100Ratio,
2371
+ cmc20ToCmc100RatioChange24hPct,
2372
+ indexRegime
2373
+ }
2374
+ } : {}
2375
+ }
2376
+ }
2377
+ };
2378
+ (0, import_strategies2.refreshSignalBaseContextGateFeatures)(signal);
2379
+ return true;
2380
+ } catch (error) {
2381
+ if (isMarketContextCancellationError(error, params.abortSignal)) {
2382
+ throw error;
2383
+ }
2384
+ coinMarketCapContextUnavailable = true;
2385
+ import_logger4.logger.warn(
2386
+ "CoinMarketCap context disabled after Timescale read failure: %s",
2387
+ String(error)
2388
+ );
2389
+ return false;
2390
+ }
2391
+ };
2392
+
2393
+ // src/strategyHelpers/derivativesContext.ts
2394
+ var import_indicators = require("@tradejs/core/indicators");
2395
+ var import_data = require("@tradejs/core/data");
2396
+ var import_strategies3 = require("@tradejs/core/strategies");
2397
+ var import_constants2 = require("@tradejs/core/constants");
2398
+ var import_derivatives = require("@tradejs/infra/timescale/derivatives");
2399
+ var import_logger5 = require("@tradejs/infra/logger");
2400
+ var STORED_INTERVALS = ["15m", "1h"];
2401
+ var CONTEXT_INTERVALS = ["15m", "1h"];
2402
+ var DEFAULT_LOOKBACK_HOURS = 48;
2403
+ var PRIMARY_DERIVATIVES_REFERENCE_SYMBOL = import_constants2.DERIVATIVES_CONTEXT_BASE_REFERENCE_SYMBOLS[0];
2404
+ var SECONDARY_DERIVATIVES_REFERENCE_SYMBOL = import_constants2.DERIVATIVES_CONTEXT_BASE_REFERENCE_SYMBOLS[1];
2405
+ var derivativesContextUnavailable = false;
2406
+ var parseEnabledFlag3 = (value, env) => {
2407
+ const normalized = String(value ?? "").trim().toLowerCase();
2408
+ if (!normalized) return true;
2409
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
2410
+ if (normalized === "backtest") return env === "BACKTEST";
2411
+ if (normalized === "live") return env !== "BACKTEST";
2412
+ return false;
2413
+ };
2414
+ var parseBooleanFlag = (value, fallback = false) => {
2415
+ const normalized = String(value ?? "").trim().toLowerCase();
2416
+ if (!normalized) return fallback;
2417
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
2418
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
2419
+ return fallback;
2420
+ };
2421
+ var parseLookbackMs = () => {
2422
+ const hours = Number(process.env.DERIVATIVES_CONTEXT_LOOKBACK_HOURS);
2423
+ const normalizedHours = Number.isFinite(hours) && hours > 0 ? hours : DEFAULT_LOOKBACK_HOURS;
2424
+ return normalizedHours * 60 * 60 * 1e3;
2425
+ };
2426
+ var withHourlyFallbackRows = (rowsByInterval) => ({
2427
+ "15m": rowsByInterval["15m"] ?? [],
2428
+ "1h": (0, import_indicators.buildCoinalyzeHourlyRowsWithFallback)({
2429
+ rows15m: rowsByInterval["15m"],
2430
+ fallbackRows1h: rowsByInterval["1h"]
2431
+ })
2432
+ });
2433
+ var getDerivativesContextReferenceSymbols = () => [
2434
+ ...(0, import_constants2.resolveDerivativesContextReferenceSymbols)(
2435
+ process.env.DERIVATIVES_CONTEXT_EXTRA_REFERENCE_SYMBOLS
2436
+ )
2437
+ ];
2438
+ var normalizeSymbol = (symbol) => String(symbol || "").trim().toUpperCase();
2439
+ var getSignalPriceChangePct1h = (signal) => {
2440
+ const baseContext = signal.additionalIndicators?.baseContext;
2441
+ if (!baseContext || typeof baseContext !== "object" || Array.isArray(baseContext)) {
2442
+ return null;
2443
+ }
2444
+ const raw = typeof baseContext.raw === "object" && baseContext.raw && !Array.isArray(baseContext.raw) ? baseContext.raw : null;
2445
+ const price = raw && typeof raw.price === "object" && raw.price && !Array.isArray(raw.price) ? raw.price : null;
2446
+ const value = price?.price1hPct;
2447
+ const numeric = typeof value === "number" ? value : Number(value);
2448
+ return Number.isFinite(numeric) ? numeric : null;
2449
+ };
2450
+ var resolvePrimaryReferenceSymbol2 = () => PRIMARY_DERIVATIVES_REFERENCE_SYMBOL;
2451
+ var resolveSecondaryReferenceSymbol = () => SECONDARY_DERIVATIVES_REFERENCE_SYMBOL;
2452
+ var getPrimaryIntervalContext = (context) => context?.intervals["15m"] ?? context?.intervals["1h"] ?? null;
2453
+ var hasDerivativesSymbolData = (context) => Object.keys(context.intervals).length > 0 && !context.summary.riskFlags.includes("missing_derivatives");
2454
+ var toFiniteNumberOrNull3 = (value) => {
2455
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2456
+ if (typeof value === "string" && value.trim()) {
2457
+ const parsed = Number(value);
2458
+ return Number.isFinite(parsed) ? parsed : null;
2459
+ }
2460
+ return null;
2461
+ };
2462
+ var roundNullable = (value, digits = 4) => {
2463
+ if (value == null || !Number.isFinite(value)) return null;
2464
+ const multiplier = 10 ** digits;
2465
+ return Math.round(value * multiplier) / multiplier;
2466
+ };
2467
+ var deltaNullable = (targetValue, referenceValue) => {
2468
+ const target = toFiniteNumberOrNull3(targetValue);
2469
+ const reference = toFiniteNumberOrNull3(referenceValue);
2470
+ return target == null || reference == null ? null : roundNullable(target - reference);
2471
+ };
2472
+ var buildTargetDerivedContext = (params) => {
2473
+ const { targetContext, primaryReferenceContext } = params;
2474
+ const targetPrimary = getPrimaryIntervalContext(targetContext);
2475
+ const referencePrimary = getPrimaryIntervalContext(primaryReferenceContext);
2476
+ const targetDirectionAligned = targetContext.summary.directionAligned;
2477
+ const referenceDirectionAligned = primaryReferenceContext?.summary.directionAligned ?? null;
2478
+ return {
2479
+ available: hasDerivativesSymbolData(targetContext),
2480
+ stale: targetContext.summary.riskFlags.includes("stale_derivatives") || targetPrimary?.stale === true ? true : targetPrimary == null ? null : false,
2481
+ sourceSymbol: targetContext.symbol,
2482
+ referenceSymbol: primaryReferenceContext?.symbol ?? null,
2483
+ directionAligned: targetDirectionAligned,
2484
+ referenceDirectionAligned,
2485
+ pressure: targetContext.summary.pressure ?? null,
2486
+ referencePressure: primaryReferenceContext?.summary.pressure ?? null,
2487
+ riskFlags: targetContext.summary.riskFlags,
2488
+ oiChangePct1h: targetPrimary?.oiChangePct1h ?? null,
2489
+ oiAcceleration: targetContext.summary.oiAcceleration ?? null,
2490
+ fundingRate: targetPrimary?.fundingRate ?? null,
2491
+ fundingZScore: targetPrimary?.fundingZScore ?? null,
2492
+ fundingChange1h: targetContext.summary.fundingChange1h ?? null,
2493
+ liqSpikeRatio: targetPrimary?.liqSpikeRatio ?? null,
2494
+ liqImbalance: targetPrimary?.liqImbalance ?? null,
2495
+ targetVsPrimaryOiChangePct1hDelta: deltaNullable(
2496
+ targetPrimary?.oiChangePct1h,
2497
+ referencePrimary?.oiChangePct1h
2498
+ ),
2499
+ targetVsPrimaryFundingZScoreDelta: deltaNullable(
2500
+ targetPrimary?.fundingZScore,
2501
+ referencePrimary?.fundingZScore
2502
+ ),
2503
+ targetReferenceConflict: targetDirectionAligned == null || referenceDirectionAligned == null ? null : targetDirectionAligned !== referenceDirectionAligned
2504
+ };
2505
+ };
2506
+ var buildReferenceDerivativesContext = (params) => {
2507
+ const {
2508
+ targetSymbol,
2509
+ primaryReferenceSymbol,
2510
+ secondaryReferenceSymbol,
2511
+ referenceSymbols,
2512
+ referenceContexts,
2513
+ targetContext
2514
+ } = params;
2515
+ const primaryContext = referenceContexts[primaryReferenceSymbol] ?? referenceContexts[referenceSymbols[0]];
2516
+ if (!primaryContext) {
2517
+ throw new Error("No derivatives reference contexts built");
2518
+ }
2519
+ const referenceSymbolsMetadata = [
2520
+ .../* @__PURE__ */ new Set([
2521
+ primaryReferenceSymbol,
2522
+ secondaryReferenceSymbol,
2523
+ ...referenceSymbols,
2524
+ ...Object.keys(referenceContexts)
2525
+ ])
2526
+ ];
2527
+ const targetDerived = targetContext && hasDerivativesSymbolData(targetContext) ? buildTargetDerivedContext({
2528
+ targetContext,
2529
+ primaryReferenceContext: primaryContext
2530
+ }) : void 0;
2531
+ return {
2532
+ ...primaryContext,
2533
+ targetSymbol,
2534
+ primaryReferenceSymbol: primaryContext.symbol,
2535
+ secondaryReferenceSymbol: referenceContexts[secondaryReferenceSymbol]?.symbol ?? secondaryReferenceSymbol,
2536
+ referenceSymbols: referenceSymbolsMetadata,
2537
+ referenceContexts,
2538
+ ...targetContext && targetDerived ? {
2539
+ targetContext,
2540
+ targetDerived
2541
+ } : {}
2542
+ };
2543
+ };
2544
+ var isDerivativesContextEnabled = (env) => parseEnabledFlag3(process.env.DERIVATIVES_CONTEXT_ENABLED, env);
2545
+ var isDerivativesTargetContextEnabled = () => parseBooleanFlag(process.env.DERIVATIVES_CONTEXT_TARGET_ENABLED, false);
2546
+ var enrichSignalWithDerivativesContext = async (params) => {
2547
+ const { signal, env, enabled = isDerivativesContextEnabled(env) } = params;
2548
+ if (signal.universe === "tradfi" || !enabled || derivativesContextUnavailable) {
2549
+ return false;
2550
+ }
2551
+ try {
2552
+ const referenceSymbols = getDerivativesContextReferenceSymbols();
2553
+ const targetSymbol = normalizeSymbol(signal.symbol);
2554
+ const lookbackMs = parseLookbackMs();
2555
+ const decisionTimeMs = signal.timestamp + (0, import_data.intervalToMs)(signal.interval);
2556
+ const derivativesEndMs = (0, import_indicators.getLastClosedDerivativesBarStartMs)(
2557
+ decisionTimeMs,
2558
+ "15m"
2559
+ );
2560
+ const contexts = await Promise.all(
2561
+ referenceSymbols.map(async (symbol) => {
2562
+ const rowsByInterval = await (0, import_derivatives.getDerivativesWindow)({
2563
+ symbol,
2564
+ intervals: STORED_INTERVALS,
2565
+ endMs: derivativesEndMs,
2566
+ lookbackMs,
2567
+ ...params.abortSignal ? { signal: params.abortSignal } : {}
2568
+ });
2569
+ return [
2570
+ symbol,
2571
+ (0, import_indicators.buildDerivativesContext)({
2572
+ symbol,
2573
+ direction: signal.direction,
2574
+ timestamp: derivativesEndMs,
2575
+ rowsByInterval: withHourlyFallbackRows(rowsByInterval),
2576
+ priceChangePct1h: getSignalPriceChangePct1h(signal),
2577
+ intervals: CONTEXT_INTERVALS
2578
+ })
2579
+ ];
2580
+ })
2581
+ );
2582
+ const referenceContexts = Object.fromEntries(contexts);
2583
+ const primaryReferenceSymbol = resolvePrimaryReferenceSymbol2();
2584
+ const secondaryReferenceSymbol = resolveSecondaryReferenceSymbol();
2585
+ const targetContextEnabled = isDerivativesTargetContextEnabled();
2586
+ const referenceTargetContext = targetContextEnabled && targetSymbol !== primaryReferenceSymbol ? referenceContexts[targetSymbol] : void 0;
2587
+ const shouldFetchTargetContext = targetContextEnabled && targetSymbol.length > 0 && !referenceSymbols.some(
2588
+ (referenceSymbol) => referenceSymbol === targetSymbol
2589
+ );
2590
+ const fetchedTargetContext = shouldFetchTargetContext ? await (async () => {
2591
+ const rowsByInterval = await (0, import_derivatives.getDerivativesWindow)({
2592
+ symbol: targetSymbol,
2593
+ intervals: STORED_INTERVALS,
2594
+ endMs: derivativesEndMs,
2595
+ lookbackMs,
2596
+ ...params.abortSignal ? { signal: params.abortSignal } : {}
2597
+ });
2598
+ const context = (0, import_indicators.buildDerivativesContext)({
2599
+ symbol: targetSymbol,
2600
+ direction: signal.direction,
2601
+ timestamp: derivativesEndMs,
2602
+ rowsByInterval: withHourlyFallbackRows(rowsByInterval),
2603
+ priceChangePct1h: getSignalPriceChangePct1h(signal),
2604
+ intervals: CONTEXT_INTERVALS
2605
+ });
2606
+ return hasDerivativesSymbolData(context) ? context : void 0;
2607
+ })() : void 0;
2608
+ const targetContext = referenceTargetContext && hasDerivativesSymbolData(referenceTargetContext) ? referenceTargetContext : fetchedTargetContext;
2609
+ const derivativesContext = buildReferenceDerivativesContext({
2610
+ targetSymbol: targetSymbol || signal.symbol,
2611
+ primaryReferenceSymbol,
2612
+ secondaryReferenceSymbol,
2613
+ referenceSymbols,
2614
+ referenceContexts,
2615
+ targetContext
2616
+ });
2617
+ signal.additionalIndicators = {
2618
+ ...signal.additionalIndicators ?? {},
2619
+ baseContext: signal.additionalIndicators?.baseContext && typeof signal.additionalIndicators.baseContext === "object" && !Array.isArray(signal.additionalIndicators.baseContext) ? {
2620
+ ...signal.additionalIndicators.baseContext,
2621
+ derivatives: derivativesContext
2622
+ } : signal.additionalIndicators?.baseContext
2623
+ };
2624
+ (0, import_strategies3.refreshSignalBaseContextGateFeatures)(signal);
2625
+ return true;
2626
+ } catch (error) {
2627
+ if (isMarketContextCancellationError(error, params.abortSignal)) {
2628
+ throw error;
2629
+ }
2630
+ derivativesContextUnavailable = true;
2631
+ import_logger5.logger.warn(
2632
+ "Derivatives context disabled after Timescale read failure: %s",
2633
+ String(error)
2634
+ );
2635
+ return false;
2636
+ }
2637
+ };
2638
+
2639
+ // src/strategyHelpers/hyperliquidWhaleContext.ts
2640
+ var import_data2 = require("@tradejs/core/data");
2641
+ var import_strategies4 = require("@tradejs/core/strategies");
2642
+ var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWhales");
2643
+ var import_logger6 = require("@tradejs/infra/logger");
2644
+
2645
+ // src/hyperliquidWhaleUniverse.ts
2646
+ var import_node_crypto3 = require("crypto");
2647
+
2648
+ // src/config/hyperliquidPerpUniverse.json
2649
+ var hyperliquidPerpUniverse_default = {
2650
+ schemaVersion: 1,
2651
+ updatedAt: "2026-08-04T11:46:43.476Z",
2652
+ source: "hyperliquid_main_perp_day_notional_volume",
2653
+ fingerprint: "58f7e024b19158dc",
2654
+ size: 30,
2655
+ symbols: [
2656
+ "BTC",
2657
+ "ETH",
2658
+ "HYPE",
2659
+ "SOL",
2660
+ "ZEC",
2661
+ "PUMP",
2662
+ "XRP",
2663
+ "CASHCAT",
2664
+ "WLD",
2665
+ "GRAM",
2666
+ "UNI",
2667
+ "ADA",
2668
+ "LIT",
2669
+ "ENA",
2670
+ "NEAR",
2671
+ "kPEPE",
2672
+ "FARTCOIN",
2673
+ "KAITO",
2674
+ "AAVE",
2675
+ "SUI",
2676
+ "ONDO",
2677
+ "AVAX",
2678
+ "DOGE",
2679
+ "VVV",
2680
+ "BNB",
2681
+ "ALGO",
2682
+ "XMR",
2683
+ "MEGA",
2684
+ "LINK",
2685
+ "TAO"
2686
+ ]
2687
+ };
2688
+
2689
+ // src/config/hyperliquidWhales.json
2690
+ var hyperliquidWhales_default = {
2691
+ schemaVersion: 2,
2692
+ updatedAt: "2026-08-04T15:10:21.316Z",
2693
+ source: "hyperliquid_structural_fills_snapshot",
2694
+ selection: {
2695
+ calibrationFrom: "2026-07-28T00:00:00.000Z",
2696
+ calibrationTo: "2026-08-04T00:00:00.000Z",
2697
+ effectiveFrom: "2026-08-04T00:00:00.000Z",
2698
+ candidateLimit: 400,
2699
+ minimumAccountValueUsd: 5e5,
2700
+ minimumActiveDays: 1,
2701
+ maximumRawFillsPerDay: 20,
2702
+ maximumDirectionalExecutionsPerDay: 10,
2703
+ minimumMedianNotionalUsd: 0,
2704
+ minimumMedianInterExecutionMinutes: 0,
2705
+ minimumTop30NotionalShare: 1e-3,
2706
+ minimumTurnoverToEquity: 0.05,
2707
+ maximumTurnoverToEquity: 5,
2708
+ score: "log1p(accountValue)*log1p(medianExecutionNotional)*log1p(max(1,medianInterExecutionMinutes))*sqrt(activeDays)*top30Share/sqrt(1+rawFillsPerDay)",
2709
+ forbiddenSelectionMetrics: ["pnl", "roi", "winRate", "closedPnl"]
2710
+ },
2711
+ fingerprint: "77a6e9d41222efdb",
2712
+ size: 100,
2713
+ addresses: [
2714
+ "0xacb66d1996501bcc82a0e7819c9e041599f5efbd",
2715
+ "0x90dbb196cacf24bd212b377a0dd62eaad5e8151b",
2716
+ "0xde5b8c8e713f7c29ec72c22a326d61e5ac76e402",
2717
+ "0x22edc38749b59c31c9319eae7fc62143ac2fcc6c",
2718
+ "0x4bc9a771cd2c96cb2a4501f2c049402b4e7e11b3",
2719
+ "0x9a0825ca6c4c577a1202a8fae3b8f044c1d5711d",
2720
+ "0xb6ca5d1ed0ca4bf8444c9dee2d068a9f4c5e2e92",
2721
+ "0xc3610d02548cdb1a30ede87c5dc0d2ffe5b9b99f",
2722
+ "0x48c6aaa647ad5197e85797f8eb95d7b9ce1ec5b6",
2723
+ "0x8c1ab015da7e5319649afca4a551924f89eff883",
2724
+ "0x64bddab5f13a99576fca6747f0aa205422c5f708",
2725
+ "0x98785f2a43fe32f972476b86ed5a5cfaf5240f00",
2726
+ "0x9836e286fbf4f71998827d4bd6ea3b470c26457e",
2727
+ "0x8f8d2d2565bfb10608a7ce64b48e2aea7875a344",
2728
+ "0xe6df4ed7a6ccb8426cbc87bdcdd3cc6d0e6c0690",
2729
+ "0xab04d040dec929a204298d44701476968b238b69",
2730
+ "0x870f718a84843abc3e22412a9b4dc2924466d448",
2731
+ "0x12c206d3e2f0d0059420c54d33fe604ebbecc230",
2732
+ "0x7d9e91a8a47eb553cbfd323e7633c0c213327124",
2733
+ "0x2057d4f2b6ea957b63371980775b0a5505a438f0",
2734
+ "0xead5b7d86c681c036c59cd00a0390541061c69f2",
2735
+ "0x3005fade4c0df5e1cd187d7062da359416f0eb8e",
2736
+ "0xe83b5e3644b50e802a25b34b38b91a061a0d3ca8",
2737
+ "0x525232cb6ed5030d2b052b36bfc0baec96986ee3",
2738
+ "0xa251520154ca342f0b1d702bf5a56f78c982405b",
2739
+ "0xa9184bb57fd75c7c63e4c074d6319eceac6be53b",
2740
+ "0xa20fb0c9e04063eec5be286e9269028d966646fa",
2741
+ "0x169ba96d963e77a87dbe58922ecdcdd0f0cf3315",
2742
+ "0x115849ce84370f25cadcf0d348510d73837e1aa5",
2743
+ "0x37b81ab9e3ab04b9e3738e4891621205aaa31fd5",
2744
+ "0x90f57d90a7dd3114312d4c31803ad5732baf5935",
2745
+ "0xad227f63d34e7251c1d0ab65e64eeea07aee4e44",
2746
+ "0xd38e2b0b3c85ae138532f9fe1a1d5f5455866b82",
2747
+ "0xf5f4c3aa62a0f903a8529905031b1536d1cf16f8",
2748
+ "0x75632aaf83120eae9b0618f8f50773ce347de2ad",
2749
+ "0x3927af597c09774f91ef84bf765ded4109e9f8ea",
2750
+ "0xf1863bd6d60a8f4b5f583ec54009af6a044877bf",
2751
+ "0xe7b34f28c54f85533896f8827edc92edac28b17f",
2752
+ "0x1cda20f9f6a23c209d54940af3ef63b96c9b2c31",
2753
+ "0x9e55b0e43b7f7f3909af841d326b2dadfda9392e",
2754
+ "0x610b8dd6b392d010c2b6ad8d121dfc09524cb613",
2755
+ "0x1d639a688b78f3d7e57166e59d9a8886b1bfb5a5",
2756
+ "0x34d3a17095ef7def39a489d63c7c916e673c9ca2",
2757
+ "0x5d0bde4017530ed43370e332a5d30194941f73c6",
2758
+ "0xaa7afc250041e2c56a06d3b7a5295bd01929e11b",
2759
+ "0x325edd95bb016c36027ce3b9f7595af7094a9564",
2760
+ "0x47f5a6397b79deaaca5c456503e7c1a49357b942",
2761
+ "0xf5e7ffe523d0491709958490b4877e1d9de6c6f7",
2762
+ "0x64702807a1f0f9888ea22b858099d7f481227b55",
2763
+ "0xd08a4557671ca465e7bba41110a93148c90f9931",
2764
+ "0x0423b4b2acb4f219ae4a270abd5dde2360e455bb",
2765
+ "0x7b29f8c0ea709b96b72214b3de5d85c3d9570794",
2766
+ "0xeb8ebad162a9ce4cb7e2325c92b960b9df63365c",
2767
+ "0x939f95036d2e7b6d7419ec072bf9d967352204d2",
2768
+ "0x2490aa2b48022e857d44e738911787c8e2296aed",
2769
+ "0x7622f628135378f1b8d1876952de9bab80245f1c",
2770
+ "0x9d677028e0e592d48fc8a8ddc910693301a4a450",
2771
+ "0x85e410e4c535cb1709dbd36468f132037a5b2087",
2772
+ "0xd6f52e2ab58a678eced4a4d1693757b0677631c8",
2773
+ "0x24d8969e2bd6aabb6c4dfe042ef8b2cd8085217e",
2774
+ "0xda51323fe9800c8365646ad5c7ade0dd17fdc167",
2775
+ "0x53f8f390fd4f70941c5d160a964f6893c8dbceff",
2776
+ "0x72dae3560e7f4c59a305e2e739d6bd6777f738b5",
2777
+ "0x36f5e448c9cf0032551e58919cf4eabefcd742c1",
2778
+ "0x5b43b7162694de766c164202df0f0c716e4292ab",
2779
+ "0xae551d73161bac3315c5ade0e2d499a44ebe2236",
2780
+ "0x4eca8d62e07b3f3e78a8ae8725d4666da324c5a8",
2781
+ "0x40a632026febd007b0e4431a29170507f7aa3896",
2782
+ "0xd453e91f692eb5437cf345e09545827576e3225d",
2783
+ "0xbe2c69eb3d7b8ee35cbe3a28293472be31179d75",
2784
+ "0xfdf891f2b214a4c9374d26595ec6d4080262e381",
2785
+ "0xfd97600ac44b3c4e20ac1a5f23e3b18d10fa5912",
2786
+ "0x98e073b579fd483eac8f10d5bd0b32c8c3bbd7e0",
2787
+ "0xb56f19e917546a60d658110a611cf62a6877b517",
2788
+ "0xbd334a55db91617545e8d1b09941e036ad8e4200",
2789
+ "0x9f1920d0cbb63ed03376a1e09fd2851d601234c8",
2790
+ "0xe9cef9fd872a03cedf9d3e6e90309782b3724ea6",
2791
+ "0xa43682a1f129e4faf0152175cc03c44e4957e8cf",
2792
+ "0x6fbb33de5f39c690d2decb95b9ab46ed0c83e3bc",
2793
+ "0x81d4163289cf61a0eaaac4862cb88ef0f4215c29",
2794
+ "0xd248d2f09bfbe04e67fc7fea08828d6ad6d95b6d",
2795
+ "0x01ffcc7285866e5cee14c8e807cb2e8d5f61079f",
2796
+ "0xc69ae428f6049e78d445f053d2c1df879c59b34c",
2797
+ "0xaab4dfe6d735c4ac46217216fe883a39fbfe8284",
2798
+ "0xe912cac1a6641004a8803687ee7699227fdb0550",
2799
+ "0x5fdbf83c3d95d525704f61401db68cbec1b2a775",
2800
+ "0xebd7c4b3677a4480fb5f69ac2f51c51d66a19990",
2801
+ "0xe4d1fa7e6141f9131345117bdb8b5701bca67662",
2802
+ "0x685feceec46dd4e5c9b5b726f5d7550fd0eda526",
2803
+ "0x3ea8b09da72972e953541500721803a187526da6",
2804
+ "0xdbe0780a5c5a0e3b345101bbc01ac2218196d6d5",
2805
+ "0x18cd4597e06b7fe0a8cd33dda499121b3a145a8b",
2806
+ "0xb8243756956e722bff5c763971b562ac551bfbd2",
2807
+ "0xe0bf8c96795d2df0b61b3b17e7cfd7d3ba13d4be",
2808
+ "0x218a65e21eddeece7a9df38c6bbdd89f692b7da2",
2809
+ "0x53b63a30a688beb53b5dc7bd731c661d678c555c",
2810
+ "0xfce053a5e461683454bf37ad66d20344c0e3f4c0",
2811
+ "0x3e516cf3c9d4f29fae6c1324c2414dc872fc9c09",
2812
+ "0xe5f1486dc84706d2509885fe6bbc46d21ba81286",
2813
+ "0x38042d713af713f0275b00640a83654db91ea543"
2814
+ ]
2815
+ };
2816
+
2817
+ // src/hyperliquidWhaleUniverse.ts
2818
+ var fingerprint2 = (value) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
2819
+ var validateUnique = (values, label) => {
2820
+ if (new Set(values).size !== values.length) {
2821
+ throw new Error(`Hyperliquid ${label} snapshot contains duplicates`);
2822
+ }
2823
+ };
2824
+ var validatePerpSnapshot = (value) => {
2825
+ const symbols = value.symbols.map((symbol) => symbol.trim()).filter(Boolean);
2826
+ validateUnique(symbols, "perp universe");
2827
+ if (value.schemaVersion !== 1 || value.source !== "hyperliquid_main_perp_day_notional_volume" || value.size !== 30 || symbols.length !== 30 || value.fingerprint !== fingerprint2(symbols)) {
2828
+ throw new Error("Invalid Hyperliquid perp universe snapshot");
2829
+ }
2830
+ return value;
2831
+ };
2832
+ var validateWhaleSnapshot = (value) => {
2833
+ const addresses = value.addresses.map((address) => address.toLowerCase());
2834
+ validateUnique(addresses, "whale registry");
2835
+ if (value.schemaVersion !== 2 || value.source !== "hyperliquid_structural_fills_snapshot" || value.size !== 100 || addresses.length !== 100 || addresses.some((address) => !/^0x[0-9a-f]{40}$/.test(address)) || value.fingerprint !== fingerprint2(addresses)) {
2836
+ throw new Error("Invalid Hyperliquid whale registry snapshot");
2837
+ }
2838
+ return value;
2839
+ };
2840
+ var perpSnapshot = validatePerpSnapshot(hyperliquidPerpUniverse_default);
2841
+ var whaleSnapshot = validateWhaleSnapshot(hyperliquidWhales_default);
2842
+ var perpSymbolSet = new Set(perpSnapshot.symbols);
2843
+ var whaleAddressSet = new Set(whaleSnapshot.addresses);
2844
+ var getHyperliquidPerpUniverseSnapshot = () => perpSnapshot;
2845
+ var getHyperliquidWhaleRegistrySnapshot = () => whaleSnapshot;
2846
+ var SIGNAL_TO_HYPERLIQUID_ALIASES = {
2847
+ PEPE: "kPEPE",
2848
+ "1000PEPE": "kPEPE",
2849
+ PUMPFUN: "PUMP",
2850
+ SHIB: "kSHIB",
2851
+ BONK: "kBONK"
2852
+ };
2853
+ var resolveHyperliquidPerpFromSignalSymbol = (symbol) => {
2854
+ const normalized = symbol.trim().toUpperCase().replace(/(?:USDT|USDC)$/, "");
2855
+ const candidate = SIGNAL_TO_HYPERLIQUID_ALIASES[normalized] ?? normalized;
2856
+ return perpSymbolSet.has(candidate) ? candidate : null;
2857
+ };
2858
+
2859
+ // src/strategyHelpers/hyperliquidWhaleContext.ts
2860
+ var MAX_CACHE_ENTRIES = 2048;
2861
+ var MAX_COVERAGE_SERIES_CACHE_ENTRIES = 20;
2862
+ var MAX_FLOW_SERIES_CACHE_ENTRIES = 64;
2863
+ var SERIES_CHUNK_MS = 90 * 24 * 60 * 6e4;
2864
+ var MAX_SERIES_LOOKBACK_MS = 60 * 6e4;
2865
+ var DEFAULT_MIN_COVERAGE_PCT = 0.8;
2866
+ var INTERVAL_MS = {
2867
+ "1m": 6e4,
2868
+ "5m": 5 * 6e4,
2869
+ "15m": 15 * 6e4,
2870
+ "1h": 60 * 6e4
2871
+ };
2872
+ var DEFAULT_MAX_AGE_BY_INTERVAL2 = {
2873
+ "1m": 2 * 6e4,
2874
+ "5m": 10 * 6e4,
2875
+ "15m": 30 * 6e4,
2876
+ "1h": 2 * 60 * 6e4
2877
+ };
2878
+ var hyperliquidWhaleContextUnavailable = false;
2879
+ var contextCache = /* @__PURE__ */ new Map();
2880
+ var coverageSeriesCache = /* @__PURE__ */ new Map();
2881
+ var flowSeriesCache = /* @__PURE__ */ new Map();
2882
+ var parseEnabledFlag4 = (value, env) => {
2883
+ const normalized = String(value ?? "").trim().toLowerCase();
2884
+ if (!normalized) return env !== "TEST";
2885
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
2886
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
2887
+ if (normalized === "backtest") return env === "BACKTEST";
2888
+ if (normalized === "live") return env !== "BACKTEST";
2889
+ return false;
2890
+ };
2891
+ var getMinimumCoveragePct = () => {
2892
+ const parsed = Number(process.env.HYPERLIQUID_WHALE_MIN_COVERAGE_PCT);
2893
+ return Number.isFinite(parsed) ? Math.max(0, Math.min(1, parsed)) : DEFAULT_MIN_COVERAGE_PCT;
2894
+ };
2895
+ var signalIntervalToMarketInterval2 = (value) => {
2896
+ const normalized = String(value).trim().toLowerCase();
2897
+ if (normalized === "1" || normalized === "1m") return "1m";
2898
+ if (normalized === "5" || normalized === "5m") return "5m";
2899
+ if (normalized === "60" || normalized === "1h") return "1h";
2900
+ return "15m";
2901
+ };
2902
+ var hasBaseContext3 = (signal) => Boolean(
2903
+ signal.additionalIndicators?.baseContext && typeof signal.additionalIndicators.baseContext === "object" && !Array.isArray(signal.additionalIndicators.baseContext)
2904
+ );
2905
+ var setBoundedCache = (key, value) => {
2906
+ if (contextCache.size >= MAX_CACHE_ENTRIES) {
2907
+ const oldestKey = contextCache.keys().next().value;
2908
+ if (oldestKey != null) contextCache.delete(oldestKey);
2909
+ }
2910
+ contextCache.set(key, value);
2911
+ void value.catch(() => contextCache.delete(key));
2912
+ };
2913
+ var setBoundedSeriesCache = (cache, maxEntries, key, value) => {
2914
+ if (cache.size >= maxEntries) {
2915
+ const oldestKey = cache.keys().next().value;
2916
+ if (oldestKey != null) cache.delete(oldestKey);
2917
+ }
2918
+ cache.set(key, value);
2919
+ void value.catch(() => cache.delete(key));
2920
+ };
2921
+ var uniqueAddressCount = (rows, field) => {
2922
+ const addresses = /* @__PURE__ */ new Set();
2923
+ for (const row of rows) {
2924
+ for (const address of row[field]) addresses.add(address);
2925
+ }
2926
+ return addresses.size;
2927
+ };
2928
+ var sumFlowField = (rows, field) => rows.reduce((sum, row) => sum + row[field], 0);
2929
+ var sliceSeriesWindow = (rows, fromMs, toMs) => {
2930
+ const lowerBound = (timestamp) => {
2931
+ let low = 0;
2932
+ let high = rows.length;
2933
+ while (low < high) {
2934
+ const middle = low + Math.floor((high - low) / 2);
2935
+ if (rows[middle].ts.getTime() < timestamp) low = middle + 1;
2936
+ else high = middle;
2937
+ }
2938
+ return low;
2939
+ };
2940
+ return rows.slice(lowerBound(fromMs), lowerBound(toMs));
2941
+ };
2942
+ var aggregateHyperliquidWhaleFlowSeries = (params) => {
2943
+ const intervalMs = INTERVAL_MS[params.interval];
2944
+ const windowStartMs = params.decisionTimeMs - intervalMs;
2945
+ const coverageRows = sliceSeriesWindow(
2946
+ params.coverageRows,
2947
+ windowStartMs,
2948
+ params.decisionTimeMs
2949
+ );
2950
+ const expectedBuckets = Math.ceil(intervalMs / 6e4);
2951
+ if (coverageRows.length !== expectedBuckets || coverageRows.some((row) => row.coveredWhales <= 0)) {
2952
+ return null;
2953
+ }
2954
+ const flowRows = sliceSeriesWindow(
2955
+ params.flowRows,
2956
+ windowStartMs,
2957
+ params.decisionTimeMs
2958
+ );
2959
+ const asOfTs = new Date(
2960
+ Math.max(...coverageRows.map((row) => row.ts.getTime()))
2961
+ );
2962
+ const coveredWhales = Math.min(
2963
+ ...coverageRows.map((row) => row.coveredWhales)
2964
+ );
2965
+ const expectedWhales = Math.max(
2966
+ ...coverageRows.map((row) => row.expectedWhales)
2967
+ );
2968
+ const coveragePct = Math.min(...coverageRows.map((row) => row.coveragePct));
2969
+ const trades = sumFlowField(flowRows, "trades");
2970
+ const whaleSides = sumFlowField(flowRows, "whaleSides");
2971
+ const buyNotionalUsd = sumFlowField(flowRows, "buyNotionalUsd");
2972
+ const sellNotionalUsd = sumFlowField(flowRows, "sellNotionalUsd");
2973
+ const positionAwareWhaleSides = sumFlowField(
2974
+ flowRows,
2975
+ "positionAwareWhaleSides"
2976
+ );
2977
+ const longEntryNotionalUsd = sumFlowField(flowRows, "longEntryNotionalUsd");
2978
+ const shortEntryNotionalUsd = sumFlowField(flowRows, "shortEntryNotionalUsd");
2979
+ const longExitNotionalUsd = sumFlowField(flowRows, "longExitNotionalUsd");
2980
+ const shortExitNotionalUsd = sumFlowField(flowRows, "shortExitNotionalUsd");
2981
+ const totalNotionalUsd = buyNotionalUsd + sellNotionalUsd;
2982
+ const totalEntryNotionalUsd = longEntryNotionalUsd + shortEntryNotionalUsd;
2983
+ const ageMs = params.decisionTimeMs - (asOfTs.getTime() + 6e4);
2984
+ return {
2985
+ symbol: params.symbol,
2986
+ interval: params.interval,
2987
+ asOfTs,
2988
+ windowEndTs: new Date(params.decisionTimeMs),
2989
+ trades,
2990
+ whaleSides,
2991
+ uniqueWhales: uniqueAddressCount(flowRows, "whaleAddresses"),
2992
+ coveredWhales,
2993
+ expectedWhales,
2994
+ coveragePct,
2995
+ buyNotionalUsd,
2996
+ sellNotionalUsd,
2997
+ netNotionalUsd: buyNotionalUsd - sellNotionalUsd,
2998
+ buySharePct: totalNotionalUsd > 0 ? buyNotionalUsd / totalNotionalUsd : null,
2999
+ positionAwareWhaleSides,
3000
+ positionAwarePct: whaleSides > 0 ? positionAwareWhaleSides / whaleSides : 0,
3001
+ longEntryWhales: uniqueAddressCount(flowRows, "longEntryWhaleAddresses"),
3002
+ shortEntryWhales: uniqueAddressCount(flowRows, "shortEntryWhaleAddresses"),
3003
+ longExitWhales: uniqueAddressCount(flowRows, "longExitWhaleAddresses"),
3004
+ shortExitWhales: uniqueAddressCount(flowRows, "shortExitWhaleAddresses"),
3005
+ longEntryNotionalUsd,
3006
+ shortEntryNotionalUsd,
3007
+ longExitNotionalUsd,
3008
+ shortExitNotionalUsd,
3009
+ entryNetNotionalUsd: longEntryNotionalUsd - shortEntryNotionalUsd,
3010
+ entryLongSharePct: totalEntryNotionalUsd > 0 ? longEntryNotionalUsd / totalEntryNotionalUsd : null,
3011
+ universeFingerprint: params.universeFingerprint,
3012
+ whaleRegistryFingerprint: params.whaleRegistryFingerprint,
3013
+ source: positionAwareWhaleSides > 0 ? "hyperliquid_user_fills" : null,
3014
+ ageMs,
3015
+ stale: ageMs < 0 || (params.maxAgeMs != null && Number.isFinite(params.maxAgeMs) ? ageMs > params.maxAgeMs : false)
3016
+ };
3017
+ };
3018
+ var loadHyperliquidWhaleFlowAggregateFromSeries = async (params) => {
3019
+ const chunkStartMs = Math.floor(params.decisionTimeMs / SERIES_CHUNK_MS) * SERIES_CHUNK_MS;
3020
+ const fromMs = chunkStartMs - MAX_SERIES_LOOKBACK_MS;
3021
+ const toMs = chunkStartMs + SERIES_CHUNK_MS;
3022
+ const sharedKey = [
3023
+ fromMs,
3024
+ toMs,
3025
+ params.universeFingerprint,
3026
+ params.whaleRegistryFingerprint
3027
+ ].join(":");
3028
+ let pendingCoverage = coverageSeriesCache.get(sharedKey);
3029
+ if (!pendingCoverage) {
3030
+ pendingCoverage = (0, import_hyperliquidWhales2.getHyperliquidWhaleCoverageSeriesRows)({
3031
+ fromMs,
3032
+ toMs,
3033
+ universeFingerprint: params.universeFingerprint,
3034
+ whaleRegistryFingerprint: params.whaleRegistryFingerprint,
3035
+ ...params.abortSignal ? { signal: params.abortSignal } : {}
3036
+ });
3037
+ setBoundedSeriesCache(
3038
+ coverageSeriesCache,
3039
+ MAX_COVERAGE_SERIES_CACHE_ENTRIES,
3040
+ sharedKey,
3041
+ pendingCoverage
3042
+ );
3043
+ }
3044
+ const flowKey = `${params.symbol}:${sharedKey}`;
3045
+ let pendingFlow = flowSeriesCache.get(flowKey);
3046
+ if (!pendingFlow) {
3047
+ pendingFlow = (0, import_hyperliquidWhales2.getHyperliquidWhaleFlowSeriesRows)({
3048
+ symbol: params.symbol,
3049
+ fromMs,
3050
+ toMs,
3051
+ universeFingerprint: params.universeFingerprint,
3052
+ whaleRegistryFingerprint: params.whaleRegistryFingerprint,
3053
+ ...params.abortSignal ? { signal: params.abortSignal } : {}
3054
+ });
3055
+ setBoundedSeriesCache(
3056
+ flowSeriesCache,
3057
+ MAX_FLOW_SERIES_CACHE_ENTRIES,
3058
+ flowKey,
3059
+ pendingFlow
3060
+ );
3061
+ }
3062
+ const [coverageRows, flowRows] = await Promise.all([
3063
+ pendingCoverage,
3064
+ pendingFlow
3065
+ ]);
3066
+ return aggregateHyperliquidWhaleFlowSeries({
3067
+ ...params,
3068
+ coverageRows,
3069
+ flowRows
3070
+ });
3071
+ };
3072
+ var toBaseHyperliquidWhaleFlowContext = (row, minimumCoveragePct) => ({
3073
+ source: row.positionAwareWhaleSides > 0 ? "hyperliquid_user_fills" : "hyperliquid_trades",
3074
+ interval: row.interval,
3075
+ asOfTs: row.asOfTs.getTime(),
3076
+ windowEndTs: row.windowEndTs.getTime(),
3077
+ ageMs: row.ageMs,
3078
+ stale: row.stale,
3079
+ symbol: row.symbol,
3080
+ trades: row.trades,
3081
+ whaleSides: row.whaleSides,
3082
+ uniqueWhales: row.uniqueWhales,
3083
+ coveredWhales: row.coveredWhales,
3084
+ expectedWhales: row.expectedWhales,
3085
+ coveragePct: row.coveragePct,
3086
+ coverageSufficient: row.coveragePct >= minimumCoveragePct,
3087
+ buyNotionalUsd: row.buyNotionalUsd,
3088
+ sellNotionalUsd: row.sellNotionalUsd,
3089
+ netNotionalUsd: row.netNotionalUsd,
3090
+ buySharePct: row.buySharePct,
3091
+ positionAwareWhaleSides: row.positionAwareWhaleSides,
3092
+ positionAwarePct: row.positionAwarePct,
3093
+ longEntryWhales: row.longEntryWhales,
3094
+ shortEntryWhales: row.shortEntryWhales,
3095
+ longExitWhales: row.longExitWhales,
3096
+ shortExitWhales: row.shortExitWhales,
3097
+ longEntryNotionalUsd: row.longEntryNotionalUsd,
3098
+ shortEntryNotionalUsd: row.shortEntryNotionalUsd,
3099
+ longExitNotionalUsd: row.longExitNotionalUsd,
3100
+ shortExitNotionalUsd: row.shortExitNotionalUsd,
3101
+ entryNetNotionalUsd: row.entryNetNotionalUsd,
3102
+ entryLongSharePct: row.entryLongSharePct,
3103
+ universeFingerprint: row.universeFingerprint,
3104
+ whaleRegistryFingerprint: row.whaleRegistryFingerprint
3105
+ });
3106
+ var isHyperliquidWhaleContextEnabled = (env) => parseEnabledFlag4(process.env.HYPERLIQUID_WHALE_CONTEXT_ENABLED, env);
3107
+ var loadHyperliquidWhaleFlowContext = async (params) => {
3108
+ if (!(params.enabled ?? isHyperliquidWhaleContextEnabled(params.env)) || hyperliquidWhaleContextUnavailable || params.interval == null) {
3109
+ return null;
3110
+ }
3111
+ const symbol = resolveHyperliquidPerpFromSignalSymbol(params.symbol);
3112
+ if (!symbol) return null;
3113
+ const interval = params.marketInterval ?? signalIntervalToMarketInterval2(params.interval);
3114
+ const decisionTimeMs = params.timestamp + (0, import_data2.intervalToMs)(params.interval);
3115
+ const maxAgeMs = params.maxAgeMs ?? DEFAULT_MAX_AGE_BY_INTERVAL2[interval];
3116
+ const universe = getHyperliquidPerpUniverseSnapshot();
3117
+ const whales = getHyperliquidWhaleRegistrySnapshot();
3118
+ const minimumCoveragePct = getMinimumCoveragePct();
3119
+ const cacheKey = [
3120
+ symbol,
3121
+ interval,
3122
+ decisionTimeMs,
3123
+ maxAgeMs,
3124
+ universe.fingerprint,
3125
+ whales.fingerprint,
3126
+ minimumCoveragePct
3127
+ ].join(":");
3128
+ try {
3129
+ let row;
3130
+ if (params.useSeriesCache) {
3131
+ row = await loadHyperliquidWhaleFlowAggregateFromSeries({
3132
+ symbol,
3133
+ interval,
3134
+ decisionTimeMs,
3135
+ maxAgeMs,
3136
+ universeFingerprint: universe.fingerprint,
3137
+ whaleRegistryFingerprint: whales.fingerprint,
3138
+ abortSignal: params.abortSignal
3139
+ });
3140
+ } else {
3141
+ let pending = contextCache.get(cacheKey);
3142
+ if (!pending) {
3143
+ pending = (0, import_hyperliquidWhales2.getHyperliquidWhaleFlowAggregate)({
3144
+ symbol,
3145
+ interval,
3146
+ decisionTimeMs,
3147
+ maxAgeMs,
3148
+ universeFingerprint: universe.fingerprint,
3149
+ whaleRegistryFingerprint: whales.fingerprint,
3150
+ ...params.abortSignal ? { signal: params.abortSignal } : {}
3151
+ });
3152
+ setBoundedCache(cacheKey, pending);
3153
+ }
3154
+ row = await pending;
3155
+ }
3156
+ if (!row) return null;
3157
+ return toBaseHyperliquidWhaleFlowContext(row, minimumCoveragePct);
3158
+ } catch (error) {
3159
+ if (isMarketContextCancellationError(error, params.abortSignal)) {
3160
+ throw error;
3161
+ }
3162
+ hyperliquidWhaleContextUnavailable = true;
3163
+ import_logger6.logger.warn(
3164
+ "Hyperliquid whale context disabled after Timescale read failure: %s",
3165
+ String(error)
3166
+ );
3167
+ return null;
3168
+ }
3169
+ };
3170
+ var enrichSignalWithHyperliquidWhaleContext = async (params) => {
3171
+ const { signal, env } = params;
3172
+ if (signal.universe === "tradfi" || signal.interval == null || !hasBaseContext3(signal)) {
3173
+ return false;
3174
+ }
3175
+ const hyperliquidWhales = await loadHyperliquidWhaleFlowContext({
3176
+ symbol: signal.symbol,
3177
+ interval: signal.interval,
3178
+ timestamp: signal.timestamp,
3179
+ env,
3180
+ enabled: params.enabled,
3181
+ marketInterval: params.interval,
3182
+ maxAgeMs: params.maxAgeMs,
3183
+ abortSignal: params.abortSignal
3184
+ });
3185
+ if (!hyperliquidWhales) return false;
3186
+ const baseContext = signal.additionalIndicators.baseContext;
3187
+ signal.additionalIndicators = {
3188
+ ...signal.additionalIndicators,
3189
+ baseContext: {
3190
+ ...baseContext,
3191
+ participation: {
3192
+ ...baseContext.participation,
3193
+ hyperliquidWhales
3194
+ }
3195
+ }
3196
+ };
3197
+ (0, import_strategies4.refreshSignalBaseContextGateFeatures)(signal);
3198
+ return true;
3199
+ };
3200
+
3201
+ // src/strategyHelpers/marketContextStages.ts
3202
+ var STAGE_ENV_KEYS = {
3203
+ binance: "BINANCE_MARKET_CONTEXT_STAGE_TIMEOUT_MS",
3204
+ coinmarketcap: "COINMARKETCAP_CONTEXT_STAGE_TIMEOUT_MS",
3205
+ derivatives: "DERIVATIVES_CONTEXT_STAGE_TIMEOUT_MS",
3206
+ hyperliquidWhales: "HYPERLIQUID_WHALE_CONTEXT_STAGE_TIMEOUT_MS"
3207
+ };
3208
+ var parsePositiveInt = (value) => {
3209
+ const parsed = Number.parseInt(String(value ?? ""), 10);
3210
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
3211
+ };
3212
+ var resolveMarketContextStageTimeoutMs = (stage) => parsePositiveInt(process.env[STAGE_ENV_KEYS[stage]]) ?? parsePositiveInt(process.env.MARKET_CONTEXT_STAGE_TIMEOUT_MS) ?? 35e3;
3213
+ var runMarketContextStage = async ({
3214
+ stage,
3215
+ parentSignal,
3216
+ operation,
3217
+ onStart,
3218
+ onComplete
3219
+ }) => {
3220
+ const controller = new AbortController();
3221
+ const timeoutMs = resolveMarketContextStageTimeoutMs(stage);
3222
+ const startedAt = Date.now();
3223
+ const onParentAbort = () => controller.abort(parentSignal?.reason);
3224
+ parentSignal?.addEventListener("abort", onParentAbort, { once: true });
3225
+ if (parentSignal?.aborted) onParentAbort();
3226
+ onStart?.(stage);
3227
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3228
+ timer.unref?.();
3229
+ try {
3230
+ let available = false;
3231
+ try {
3232
+ available = await operation(controller.signal);
3233
+ } catch (error) {
3234
+ if (isMarketContextCancellationError(error)) {
3235
+ controller.abort(error);
3236
+ }
3237
+ if (!controller.signal.aborted) throw error;
3238
+ }
3239
+ const status = controller.signal.aborted ? "timed_out" : available ? "available" : "absent";
3240
+ const result = {
3241
+ stage,
3242
+ status,
3243
+ elapsedMs: Date.now() - startedAt
3244
+ };
3245
+ if (status === "timed_out") {
3246
+ import_logger7.logger.warn(
3247
+ "Market context stage timed out: %s after %sms",
3248
+ stage,
3249
+ result.elapsedMs
3250
+ );
3251
+ }
3252
+ onComplete?.(result);
3253
+ return result;
3254
+ } finally {
3255
+ clearTimeout(timer);
3256
+ parentSignal?.removeEventListener("abort", onParentAbort);
3257
+ }
3258
+ };
3259
+ var enrichSignalWithMarketContextStages = async ({
3260
+ signal,
3261
+ env,
3262
+ coinMarketCapEnabled,
3263
+ includeHyperliquidWhales = true,
3264
+ abortSignal,
3265
+ onStageStart,
3266
+ onStageComplete
3267
+ }) => {
3268
+ const stages = [
3269
+ {
3270
+ stage: "binance",
3271
+ operation: (stageSignal) => enrichSignalWithBinanceMarketContext({
3272
+ signal,
3273
+ env,
3274
+ abortSignal: stageSignal
3275
+ })
3276
+ },
3277
+ {
3278
+ stage: "coinmarketcap",
3279
+ operation: (stageSignal) => enrichSignalWithCoinMarketCapContext({
3280
+ signal,
3281
+ env,
3282
+ enabled: coinMarketCapEnabled,
3283
+ abortSignal: stageSignal
3284
+ })
3285
+ },
3286
+ {
3287
+ stage: "derivatives",
3288
+ operation: (stageSignal) => enrichSignalWithDerivativesContext({
3289
+ signal,
3290
+ env,
3291
+ abortSignal: stageSignal
3292
+ })
3293
+ }
3294
+ ];
3295
+ if (includeHyperliquidWhales) {
3296
+ stages.push({
3297
+ stage: "hyperliquidWhales",
3298
+ operation: (stageSignal) => enrichSignalWithHyperliquidWhaleContext({
3299
+ signal,
3300
+ env,
3301
+ abortSignal: stageSignal
3302
+ })
3303
+ });
3304
+ }
3305
+ const results = [];
3306
+ for (const stage of stages) {
3307
+ if (abortSignal?.aborted) break;
3308
+ results.push(
3309
+ await runMarketContextStage({
3310
+ ...stage,
3311
+ parentSignal: abortSignal,
3312
+ onStart: onStageStart,
3313
+ onComplete: onStageComplete
3314
+ })
3315
+ );
3316
+ }
3317
+ return results;
3318
+ };
3319
+
3320
+ // src/strategyHelpers/runtime.ts
3321
+ var formatAiError = (err) => {
3322
+ const error = err;
3323
+ const safeJson = (value) => {
3324
+ try {
3325
+ return JSON.stringify(value);
3326
+ } catch {
3327
+ return String(value);
3328
+ }
3329
+ };
3330
+ const details = {
3331
+ message: String(error?.message ?? "unknown"),
3332
+ status: error?.status ?? null,
3333
+ code: error?.code ?? null,
3334
+ type: error?.type ?? null,
3335
+ providerError: error?.error ?? null
3336
+ };
3337
+ return safeJson(details);
3338
+ };
3339
+ var resolveAiQuality = (analysis, direction) => {
3340
+ if (typeof analysis?.quality !== "number") {
3341
+ return void 0;
3342
+ }
3343
+ const normalizedQuality = Math.round(analysis.quality);
3344
+ const aiApprovedCurrentTrade = analysis.direction === direction;
3345
+ return aiApprovedCurrentTrade ? normalizedQuality : 0;
3346
+ };
3347
+ var findReplayAiAnalysis = ({
3348
+ signal,
3349
+ direction,
3350
+ ai
3351
+ }) => {
3352
+ const snapshots = ai?.replayAnalyses;
3353
+ if (!Array.isArray(snapshots) || !snapshots.length) {
3354
+ return void 0;
3355
+ }
3356
+ let best = null;
3357
+ for (const snapshot2 of snapshots) {
3358
+ if (snapshot2.symbol !== signal.symbol || snapshot2.direction !== direction || snapshot2.strategy && snapshot2.strategy !== signal.strategy) {
3359
+ continue;
3360
+ }
3361
+ const toleranceMs = Math.max(0, Number(snapshot2.toleranceMs ?? 0));
3362
+ const diff = Math.abs(snapshot2.timestamp - signal.timestamp);
3363
+ if (diff > toleranceMs || best && diff >= best.diff) {
3364
+ continue;
3365
+ }
3366
+ best = {
3367
+ diff,
3368
+ analysis: snapshot2.analysis
3369
+ };
3370
+ }
3371
+ return best?.analysis;
3372
+ };
3373
+ var enrichSignalWithMl = async ({
3374
+ signal,
3375
+ env,
3376
+ ml
3377
+ }) => {
3378
+ if (env !== "BACKTEST" && ml && ml.enabled !== false && ml.strategyConfig && typeof ml.mlThreshold === "number") {
3379
+ const strategy = signal.strategy;
3380
+ const fullRow = (0, import_ml2.buildMlTrainingRow)(
3381
+ buildMlPayload({
3382
+ signal,
3383
+ context: {
3384
+ strategyConfig: ml.strategyConfig,
3385
+ strategyName: strategy,
3386
+ symbol: signal.symbol
3387
+ }
3388
+ }),
3389
+ null
3390
+ );
3391
+ const row = (0, import_ml2.trimMlTrainingRowWindows)(fullRow, 5);
3392
+ const features = (0, import_ml2.buildMlFeatures)(row);
3393
+ const mlResult = await (0, import_ml2.fetchMlThreshold)({
3394
+ strategy: ml.modelKey ?? strategy,
3395
+ features,
3396
+ threshold: ml.mlThreshold,
3397
+ projectRoot: getTradejsProjectCwd()
3398
+ });
3399
+ if (mlResult) {
3400
+ signal.ml = mlResult;
3401
+ }
3402
+ }
3403
+ };
3404
+ var enrichSignalWithAi = async ({
3405
+ signal,
3406
+ symbol,
3407
+ userName,
3408
+ direction,
3409
+ env,
3410
+ ai
3411
+ }) => {
3412
+ if (ai?.enabled === false) {
3413
+ return void 0;
3414
+ }
3415
+ if (env === "PARITY") {
3416
+ const replayAnalysis = findReplayAiAnalysis({ signal, direction, ai });
3417
+ if (replayAnalysis) {
3418
+ signal.aiAnalysis = replayAnalysis;
3419
+ return resolveAiQuality(replayAnalysis, direction);
3420
+ }
3421
+ }
3422
+ if (env === "BACKTEST") {
3423
+ return void 0;
3424
+ }
3425
+ if (ai?.mode === "gate") {
3426
+ const gateAnalysis = await runAiPromptLocal(signal);
3427
+ const gateQuality = resolveAiQuality(gateAnalysis, direction);
3428
+ signal.aiAnalysis = gateAnalysis;
3429
+ return gateQuality;
3430
+ }
3431
+ try {
3432
+ const analysis = await askAI(signal, { userName });
3433
+ signal.aiAnalysis = analysis;
3434
+ return resolveAiQuality(analysis, direction);
3435
+ } catch (err) {
3436
+ import_logger8.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
3437
+ }
3438
+ return void 0;
3439
+ };
3440
+ var toFiniteNumberOrNull4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
3441
+ var getOrderArrivalSnapshot = async ({
3442
+ connector,
3443
+ symbol
3444
+ }) => {
3445
+ if (typeof connector.getTopOfBookTicker !== "function") {
3446
+ return {
3447
+ arrivalSnapshotTime: Date.now(),
3448
+ arrivalSource: "unavailable",
3449
+ bid: null,
3450
+ ask: null,
3451
+ arrivalMid: null,
3452
+ spreadBps: null
3453
+ };
3454
+ }
3455
+ try {
3456
+ const ticker = await connector.getTopOfBookTicker(symbol);
3457
+ const arrivalSnapshotTime = toFiniteNumberOrNull4(ticker?.timestamp);
3458
+ const bid = toFiniteNumberOrNull4(ticker?.bidPrice);
3459
+ const ask = toFiniteNumberOrNull4(ticker?.askPrice);
3460
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
3461
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
3462
+ return {
3463
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
3464
+ arrivalSource: "top_of_book",
3465
+ bid,
3466
+ ask,
3467
+ arrivalMid,
3468
+ spreadBps
3469
+ };
3470
+ } catch (error) {
3471
+ import_logger8.logger.warn(
3472
+ "runtime order arrival snapshot failed: %s %s",
3473
+ symbol,
3474
+ error?.message || String(error)
3475
+ );
3476
+ return {
3477
+ arrivalSnapshotTime: Date.now(),
3478
+ arrivalSource: "top_of_book_error",
3479
+ bid: null,
3480
+ ask: null,
3481
+ arrivalMid: null,
3482
+ spreadBps: null
3483
+ };
3484
+ }
3485
+ };
3486
+ var validateEntryProtectionAtArrival = ({
3487
+ direction,
3488
+ signalPrice,
3489
+ bid,
3490
+ ask,
3491
+ arrivalMid,
3492
+ takeProfits,
3493
+ stopLossPrice
3494
+ }) => {
3495
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
3496
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
3497
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
3498
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
3499
+ }
3500
+ for (const takeProfit of takeProfits) {
3501
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
3502
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
3503
+ }
3504
+ }
3505
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
3506
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
3507
+ }
3508
+ };
3509
+ var resolveRuntimeTelemetryQuality = ({
3510
+ signalClosePrice,
3511
+ arrivalMid,
3512
+ orderSubmitTime,
3513
+ orderAckTime,
3514
+ fillAvgPrice,
3515
+ fillTime
3516
+ }) => {
3517
+ if (signalClosePrice != null && arrivalMid != null && orderSubmitTime != null && orderAckTime != null && fillAvgPrice != null && fillTime != null) {
3518
+ return "full";
3519
+ }
3520
+ if (fillAvgPrice != null && (arrivalMid != null || orderSubmitTime != null)) {
3521
+ return "partial";
3522
+ }
3523
+ if (fillAvgPrice != null) {
3524
+ return "price_only";
3525
+ }
3526
+ return "none";
3527
+ };
3528
+ var applyProtectiveOrders = async ({
3529
+ connector,
3530
+ symbol,
3531
+ direction,
3532
+ qty,
3533
+ takeProfits,
3534
+ stopLossPrice
3535
+ }) => {
3536
+ if (Array.isArray(takeProfits) && takeProfits.length > 0) {
3537
+ const tpOk = await connector.setTakeProfits({
3538
+ symbol,
3539
+ direction,
3540
+ qty,
3541
+ takeProfits
3542
+ });
3543
+ if (!tpOk) {
3544
+ throw new Error("SET_TAKE_PROFITS_FAILED");
3545
+ }
3546
+ }
3547
+ if (typeof stopLossPrice === "number" && Number.isFinite(stopLossPrice)) {
3548
+ const slOk = await connector.setStopLoss({
3549
+ symbol,
3550
+ direction,
3551
+ stopLossPrice
3552
+ });
3553
+ if (!slOk) {
3554
+ throw new Error("SET_STOP_LOSS_FAILED");
3555
+ }
3556
+ }
3557
+ };
3558
+ var executeEntryOrder = async ({
3559
+ connector,
3560
+ userName,
3561
+ symbol,
3562
+ direction,
3563
+ qty,
3564
+ currentPrice,
3565
+ timestamp,
3566
+ takeProfits,
3567
+ stopLossPrice,
3568
+ positionIntent = "open",
3569
+ signal,
3570
+ beforePlaceOrder,
3571
+ recordRuntimeTrade = true,
3572
+ leverage
3573
+ }) => {
3574
+ await beforePlaceOrder?.();
3575
+ const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
3576
+ const orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
3577
+ const signalTimestamp = signal.timestamp;
3578
+ const signalClosePrice = currentPrice;
3579
+ signal.orderId = orderId;
3580
+ signal.orderQty = qty;
3581
+ signal.orderValue = qty * currentPrice;
3582
+ signal.orderFailureReason = void 0;
3583
+ const arrivalSnapshot = await getOrderArrivalSnapshot({ connector, symbol });
3584
+ try {
3585
+ validateEntryProtectionAtArrival({
3586
+ direction,
3587
+ signalPrice: currentPrice,
3588
+ bid: arrivalSnapshot.bid,
3589
+ ask: arrivalSnapshot.ask,
3590
+ arrivalMid: arrivalSnapshot.arrivalMid,
3591
+ takeProfits,
3592
+ stopLossPrice
3593
+ });
3594
+ } catch (error) {
3595
+ signal.orderStatus = "failed";
3596
+ signal.orderFailureReason = error.message;
3597
+ throw error;
3598
+ }
3599
+ const orderSubmitTime = Date.now();
3600
+ const orderPlaced = await connector.placeOrder({
3601
+ symbol,
3602
+ qty,
3603
+ price: currentPrice,
3604
+ isLimit: false,
3605
+ positionIntent,
3606
+ timestamp,
3607
+ direction,
3608
+ ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
3609
+ orderId,
3610
+ signal
3611
+ });
3612
+ const orderAckTime = Date.now();
3613
+ const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
3614
+ const currentPosition = await connector.getPosition(symbol);
3615
+ const fillTime = Date.now();
3616
+ const isPositionIncrease = positionIntent === "increase" && previousPosition?.direction === direction && Number.isFinite(previousPosition.qty) && previousPosition.qty > 0;
3617
+ const hasRefreshedIncreasedPosition = Boolean(
3618
+ isPositionIncrease && previousPosition && currentPosition?.qty && currentPosition.qty > previousPosition.qty
3619
+ );
3620
+ const hasUsableCurrentPosition = Boolean(
3621
+ currentPosition && Number.isFinite(currentPosition.price) && Number.isFinite(currentPosition.qty) && (!isPositionIncrease || hasRefreshedIncreasedPosition)
3622
+ );
3623
+ const resultingEntryPrice = hasUsableCurrentPosition && currentPosition ? currentPosition.price : isPositionIncrease && previousPosition ? (previousPosition.price * previousPosition.qty + currentPrice * placedQty) / (previousPosition.qty + placedQty) : currentPrice;
3624
+ const resultingQty = hasUsableCurrentPosition && currentPosition ? currentPosition.qty : isPositionIncrease && previousPosition ? previousPosition.qty + placedQty : placedQty;
3625
+ const filledQty = isPositionIncrease && previousPosition ? hasRefreshedIncreasedPosition && currentPosition ? currentPosition.qty - previousPosition.qty : placedQty : resultingQty;
3626
+ const fillPrice = isPositionIncrease && previousPosition && hasRefreshedIncreasedPosition && currentPosition ? (resultingEntryPrice * currentPosition.qty - previousPosition.price * previousPosition.qty) / (currentPosition.qty - previousPosition.qty) : isPositionIncrease ? currentPrice : resultingEntryPrice;
3627
+ const fillSource = hasUsableCurrentPosition ? "exchange_position" : orderPlaced ? "requested_price" : "unknown";
3628
+ const estimatedOpenFee = fillPrice * filledQty * import_constants3.FEE_PERCENT;
3629
+ signal.prices.currentPrice = fillPrice;
3630
+ signal.orderQty = filledQty;
3631
+ signal.orderValue = filledQty * fillPrice;
3632
+ if (orderPlaced) {
3633
+ try {
3634
+ await applyProtectiveOrders({
3635
+ connector,
3636
+ symbol,
3637
+ direction,
3638
+ qty: resultingQty,
3639
+ takeProfits,
3640
+ stopLossPrice
3641
+ });
3642
+ } catch (error) {
3643
+ await connector.closePosition({
3644
+ symbol,
3645
+ price: resultingEntryPrice,
3646
+ timestamp,
3647
+ direction,
3648
+ signal
3649
+ });
3650
+ throw error;
3651
+ }
3652
+ }
3653
+ signal.orderStatus = orderPlaced ? "completed" : "failed";
3654
+ signal.orderSkipReason = void 0;
3655
+ if (orderPlaced) {
3656
+ signal.orderFailureReason = void 0;
3657
+ }
3658
+ if (orderPlaced && recordRuntimeTrade && !isPositionIncrease) {
3659
+ await recordRuntimeTradeOpen({
3660
+ userName,
3661
+ orderId,
3662
+ signalId: signal.signalId,
3663
+ strategy: signal.strategy,
3664
+ symbol,
3665
+ interval: signal.interval,
3666
+ direction,
3667
+ qty: resultingQty,
3668
+ entryPrice: resultingEntryPrice,
3669
+ signalTimestamp,
3670
+ signalClosePrice,
3671
+ arrivalSnapshotTime: arrivalSnapshot.arrivalSnapshotTime,
3672
+ arrivalSource: arrivalSnapshot.arrivalSource,
3673
+ arrivalMid: arrivalSnapshot.arrivalMid,
3674
+ bid: arrivalSnapshot.bid,
3675
+ ask: arrivalSnapshot.ask,
3676
+ spreadBps: arrivalSnapshot.spreadBps,
3677
+ orderSubmitTime,
3678
+ orderAckTime,
3679
+ fillAvgPrice: fillPrice,
3680
+ fillSource,
3681
+ fillTime,
3682
+ telemetryQuality: resolveRuntimeTelemetryQuality({
3683
+ signalClosePrice,
3684
+ arrivalMid: arrivalSnapshot.arrivalMid,
3685
+ orderSubmitTime,
3686
+ orderAckTime,
3687
+ fillAvgPrice: fillPrice,
3688
+ fillTime
3689
+ }),
3690
+ fee: estimatedOpenFee,
3691
+ openFee: estimatedOpenFee,
3692
+ totalFee: estimatedOpenFee,
3693
+ entryTimestamp: timestamp,
3694
+ universe: signal.universe,
3695
+ assetClass: signal.assetClass,
3696
+ accountId: signal.accountId,
3697
+ deploymentId: signal.deploymentId,
3698
+ policyProfileId: signal.policyProfileId,
3699
+ runtimeConfigId: signal.runtimeConfigId,
3700
+ runtimeLineage: signal.runtimeLineage,
3701
+ ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {}
3702
+ });
3703
+ }
3704
+ if (orderPlaced && recordRuntimeTrade && isPositionIncrease) {
3705
+ await recordRuntimeTradeIncrease({
3706
+ userName,
3707
+ strategy: signal.strategy,
3708
+ symbol,
3709
+ direction,
3710
+ resultingQty,
3711
+ resultingEntryPrice,
3712
+ addedQty: filledQty,
3713
+ addedEntryPrice: fillPrice,
3714
+ entryTimestamp: timestamp,
3715
+ fee: estimatedOpenFee,
3716
+ accountId: signal.accountId,
3717
+ deploymentId: signal.deploymentId
3718
+ });
3719
+ }
3720
+ if (hasUsableCurrentPosition && currentPosition?.price) {
3721
+ return currentPosition.price;
3722
+ }
3723
+ return resultingEntryPrice;
3724
+ };
3725
+ var updatePositionProtection = async ({
3726
+ connector,
3727
+ symbol,
3728
+ direction,
3729
+ qty,
3730
+ takeProfits,
3731
+ stopLossPrice
3732
+ }) => {
3733
+ await applyProtectiveOrders({
3734
+ connector,
3735
+ symbol,
3736
+ direction,
3737
+ qty,
3738
+ takeProfits: takeProfits ?? [],
3739
+ stopLossPrice: stopLossPrice ?? null
3740
+ });
3741
+ };
3742
+
3743
+ // src/strategyHelpers/config.ts
3744
+ var import_lodash = __toESM(require("lodash"));
3745
+ var import_runtimeStrategyConfigs = require("@tradejs/infra/runtimeStrategyConfigs");
3746
+ var resolveStrategyConfig = async ({
3747
+ strategyName,
3748
+ userName,
3749
+ symbol,
3750
+ baseConfig,
3751
+ defaults,
3752
+ runtimeConfigId,
3753
+ runtimeConfigSnapshot
3754
+ }) => {
3755
+ const mergeIfNotEmpty = (target, patch) => patch && !import_lodash.default.isEmpty(patch) ? {
3756
+ ...target,
3757
+ ...patch
3758
+ } : target;
3759
+ let config = {
3760
+ ...defaults,
3761
+ ...baseConfig
3762
+ };
3763
+ let isConfigFromBacktest = false;
3764
+ if (config.ENV !== "BACKTEST") {
3765
+ const userConfig = runtimeConfigSnapshot ? runtimeConfigSnapshot.userConfig : await (0, import_runtimeStrategyConfigs.getRuntimeStrategyConfig)(
3766
+ userName,
3767
+ strategyName,
3768
+ runtimeConfigId
3769
+ ) ?? {};
3770
+ config = mergeIfNotEmpty(config, userConfig);
3771
+ if (!runtimeConfigId || runtimeConfigId === "config") {
3772
+ const symbolResultConfig = runtimeConfigSnapshot ? runtimeConfigSnapshot.symbolResultConfig : await (0, import_runtimeStrategyConfigs.getRuntimeStrategyResultConfig)(userName, strategyName, symbol);
3773
+ if (symbolResultConfig && !import_lodash.default.isEmpty(symbolResultConfig)) {
3774
+ config = mergeIfNotEmpty(
3775
+ config,
3776
+ symbolResultConfig
3777
+ );
3778
+ isConfigFromBacktest = true;
3779
+ }
3780
+ }
3781
+ }
3782
+ return { config, isConfigFromBacktest };
3783
+ };
3784
+
3785
+ // src/strategy/runtimeBacktestDelay.ts
3786
+ var import_constants4 = require("@tradejs/core/constants");
3787
+ var import_data3 = require("@tradejs/core/data");
3788
+ var import_strategies5 = require("@tradejs/core/strategies");
3789
+ var resolveBacktestEntryDelayBars = (value) => {
3790
+ if (value == null || value === "") {
3791
+ return 1;
3792
+ }
3793
+ const parsed = parseInt(String(value), 10);
3794
+ return Number.isFinite(parsed) ? Math.max(0, parsed) : 1;
3795
+ };
3796
+ var resolveBacktestExecutionIntervalForPrimary = (interval) => {
3797
+ const normalized = String(interval ?? "15");
3798
+ if (normalized === "15") {
3799
+ return import_constants4.BACKTEST_EXECUTION_INTERVAL;
3800
+ }
3801
+ if (normalized === "60") {
3802
+ return "15";
3803
+ }
3804
+ return null;
3805
+ };
3806
+ var resolveBacktestExecutionDelayMs = (value, fallbackDelayMs) => {
3807
+ if (value == null || value === "") {
3808
+ return fallbackDelayMs;
3809
+ }
3810
+ const parsed = Number(value);
3811
+ return Number.isFinite(parsed) ? Math.max(0, Math.trunc(parsed)) : fallbackDelayMs;
3812
+ };
3813
+ var safeIntervalToMs = (interval) => {
3814
+ try {
3815
+ return (0, import_data3.intervalToMs)(interval);
3816
+ } catch {
3817
+ return null;
3818
+ }
3819
+ };
3820
+ var buildCandleByTimestamp = (candles) => new Map(
3821
+ (candles ?? []).filter((candle) => typeof candle?.timestamp === "number").map((candle) => [candle.timestamp, candle])
3822
+ );
3823
+ var buildBacktestExecutionOnlyCandle = (candle, executionPrice) => ({
3824
+ ...candle,
3825
+ open: executionPrice,
3826
+ high: executionPrice,
3827
+ low: executionPrice,
3828
+ close: executionPrice,
3829
+ volume: 0,
3830
+ turnover: 0
3831
+ });
3832
+ var resolveInvalidDelayedEntryReason = ({
3833
+ decision,
3834
+ executionPrice,
3835
+ takeProfitPrice,
3836
+ stopLossPrice,
3837
+ riskRatio
3838
+ }) => {
3839
+ if (!Number.isFinite(executionPrice) || !Number.isFinite(takeProfitPrice) || !Number.isFinite(stopLossPrice)) {
3840
+ return "BACKTEST_DELAYED_ENTRY_INVALID_PRICE";
3841
+ }
3842
+ if (decision.entryContext.direction === "LONG") {
3843
+ if (executionPrice <= stopLossPrice) {
3844
+ return "BACKTEST_DELAYED_ENTRY_BEYOND_STOP";
3845
+ }
3846
+ if (executionPrice >= takeProfitPrice) {
3847
+ return "BACKTEST_DELAYED_ENTRY_BEYOND_TAKE_PROFIT";
3848
+ }
3849
+ return !Number.isFinite(riskRatio) || riskRatio <= 0 ? "BACKTEST_DELAYED_ENTRY_INVALID_PRICE" : null;
3850
+ }
3851
+ if (executionPrice >= stopLossPrice) {
3852
+ return "BACKTEST_DELAYED_ENTRY_BEYOND_STOP";
3853
+ }
3854
+ if (executionPrice <= takeProfitPrice) {
3855
+ return "BACKTEST_DELAYED_ENTRY_BEYOND_TAKE_PROFIT";
3856
+ }
3857
+ return !Number.isFinite(riskRatio) || riskRatio <= 0 ? "BACKTEST_DELAYED_ENTRY_INVALID_PRICE" : null;
3858
+ };
3859
+ var applyBacktestDelayedEntryExecution = ({
3860
+ decision,
3861
+ execution,
3862
+ backtestPriceMode,
3863
+ delayBars
3864
+ }) => {
3865
+ const { candle, btcCandle } = execution;
3866
+ const signalTimestamp = decision.signal?.timestamp ?? decision.entryContext.timestamp;
3867
+ const signalPrice = decision.signal?.prices.currentPrice ?? decision.entryContext.prices.currentPrice;
3868
+ const skipReason = execution.skipReason ?? (!candle || !btcCandle ? "BACKTEST_LOWER_EXECUTION_CANDLE_MISSING" : void 0);
3869
+ if (skipReason || !candle || !btcCandle) {
3870
+ if (decision.signal) {
3871
+ decision.signal.additionalIndicators = {
3872
+ ...decision.signal.additionalIndicators ?? {},
3873
+ backtestExecution: {
3874
+ entryDelayBars: delayBars,
3875
+ priceMode: backtestPriceMode ?? "open",
3876
+ signalTimestamp,
3877
+ signalPrice,
3878
+ executionSource: execution.source,
3879
+ ...execution.executionInterval ? { executionInterval: execution.executionInterval } : {},
3880
+ ...execution.executionDelayMs != null ? { executionDelayMs: execution.executionDelayMs } : {},
3881
+ ...execution.primaryExecutionTimestamp != null ? { primaryExecutionTimestamp: execution.primaryExecutionTimestamp } : {},
3882
+ ...execution.requestedExecutionTimestamp != null ? {
3883
+ requestedExecutionTimestamp: execution.requestedExecutionTimestamp
3884
+ } : {},
3885
+ skipReason
3886
+ }
3887
+ };
3888
+ decision.signal.orderStatus = "skipped";
3889
+ decision.signal.orderSkipReason = skipReason;
3890
+ }
3891
+ return {
3892
+ skipReason,
3893
+ executionCandle: null,
3894
+ btcExecutionCandle: null
3895
+ };
3896
+ }
3897
+ const executionPrice = (0, import_strategies5.resolveBacktestExecutionPrice)(
3898
+ candle,
3899
+ backtestPriceMode ?? "open"
3900
+ );
3901
+ const executionTimestamp = candle.timestamp;
3902
+ const takeProfitPrice = decision.entryContext.prices.takeProfitPrice;
3903
+ const stopLossPrice = decision.orderPlan.stopLossPrice;
3904
+ const riskRatio = (0, import_strategies5.calculateRiskRatio)({
3905
+ direction: decision.entryContext.direction,
3906
+ currentPrice: executionPrice,
3907
+ takeProfitPrice,
3908
+ stopLossPrice
3909
+ });
3910
+ const invalidSkipReason = resolveInvalidDelayedEntryReason({
3911
+ decision,
3912
+ executionPrice,
3913
+ takeProfitPrice,
3914
+ stopLossPrice,
3915
+ riskRatio
3916
+ });
3917
+ decision.entryContext = {
3918
+ ...decision.entryContext,
3919
+ timestamp: executionTimestamp,
3920
+ prices: {
3921
+ ...decision.entryContext.prices,
3922
+ currentPrice: executionPrice,
3923
+ stopLossPrice,
3924
+ riskRatio
3925
+ }
3926
+ };
3927
+ const executionResult = {
3928
+ skipReason: invalidSkipReason,
3929
+ executionCandle: buildBacktestExecutionOnlyCandle(candle, executionPrice),
3930
+ btcExecutionCandle: buildBacktestExecutionOnlyCandle(
3931
+ btcCandle,
3932
+ (0, import_strategies5.resolveBacktestExecutionPrice)(btcCandle, backtestPriceMode ?? "open")
3933
+ )
3934
+ };
3935
+ if (!decision.signal) {
3936
+ return executionResult;
3937
+ }
3938
+ decision.signal.prices = {
3939
+ ...decision.signal.prices,
3940
+ currentPrice: executionPrice,
3941
+ stopLossPrice,
3942
+ riskRatio
3943
+ };
3944
+ decision.signal.additionalIndicators = {
3945
+ ...decision.signal.additionalIndicators ?? {},
3946
+ backtestExecution: {
3947
+ entryDelayBars: delayBars,
3948
+ priceMode: backtestPriceMode ?? "open",
3949
+ signalTimestamp,
3950
+ signalPrice,
3951
+ executionTimestamp,
3952
+ executionPrice,
3953
+ executionSource: execution.source,
3954
+ ...execution.executionInterval ? { executionInterval: execution.executionInterval } : {},
3955
+ ...execution.executionDelayMs != null ? { executionDelayMs: execution.executionDelayMs } : {},
3956
+ ...execution.primaryExecutionTimestamp != null ? { primaryExecutionTimestamp: execution.primaryExecutionTimestamp } : {},
3957
+ ...execution.requestedExecutionTimestamp != null ? { requestedExecutionTimestamp: execution.requestedExecutionTimestamp } : {},
3958
+ ...invalidSkipReason ? { skipReason: invalidSkipReason } : {}
3959
+ }
3960
+ };
3961
+ if (invalidSkipReason) {
3962
+ decision.signal.orderStatus = "skipped";
3963
+ decision.signal.orderSkipReason = invalidSkipReason;
3964
+ }
3965
+ return executionResult;
3966
+ };
3967
+
3968
+ // src/strategy/runtimeEntryPolicy.ts
3969
+ var resolveEntryRuntimePolicy = ({
3970
+ decision,
3971
+ config,
3972
+ manifest,
3973
+ policyProfile
3974
+ }) => {
3975
+ const baseDefaults = manifest?.entryRuntimeDefaults;
3976
+ const profileDefaults = policyProfile?.entryRuntimeDefaults;
3977
+ const manifestDefaults = baseDefaults || profileDefaults ? {
3978
+ ...baseDefaults,
3979
+ ...profileDefaults,
3980
+ ...baseDefaults?.ml || profileDefaults?.ml ? { ml: { ...baseDefaults?.ml, ...profileDefaults?.ml } } : {},
3981
+ ...baseDefaults?.ai || profileDefaults?.ai ? { ai: { ...baseDefaults?.ai, ...profileDefaults?.ai } } : {}
3982
+ } : void 0;
3983
+ const adapterMl = (policyProfile?.mlAdapter ?? manifest?.mlAdapter)?.mapEntryRuntimeFromConfig?.(config);
3984
+ const adapterAi = (policyProfile?.aiAdapter ?? manifest?.aiAdapter)?.mapEntryRuntimeFromConfig?.(config);
3985
+ const ml = manifestDefaults?.ml || adapterMl || decision.runtime?.ml ? {
3986
+ ...manifestDefaults?.ml,
3987
+ ...adapterMl,
3988
+ ...decision.runtime?.ml
3989
+ } : void 0;
3990
+ const ai = manifestDefaults?.ai || adapterAi || decision.runtime?.ai ? {
3991
+ ...manifestDefaults?.ai,
3992
+ ...adapterAi,
3993
+ ...decision.runtime?.ai
3994
+ } : void 0;
3995
+ return {
3996
+ ...manifestDefaults,
3997
+ ...decision.runtime,
3998
+ ml,
3999
+ ai
4000
+ };
4001
+ };
4002
+ var formatGateNumber = (value) => {
4003
+ if (!Number.isFinite(value)) {
4004
+ return String(value);
4005
+ }
4006
+ const normalized = Number(value.toFixed(6));
4007
+ return Number.isInteger(normalized) ? String(normalized) : String(normalized);
4008
+ };
4009
+ var isMlRuntimeGateEnabled = (params) => {
4010
+ const { env, ml } = params;
4011
+ return env !== "BACKTEST" && ml?.config != null && ml.config.enabled !== false;
4012
+ };
4013
+ var isMlResultUnavailable = (params) => {
4014
+ const { env, ml } = params;
4015
+ return isMlRuntimeGateEnabled({ env, ml }) && ml?.result == null;
4016
+ };
4017
+ var shouldExecuteEntryDecision = ({
4018
+ makeOrdersEnabled,
4019
+ env,
4020
+ signal,
4021
+ ml,
4022
+ aiEnabled,
4023
+ quality,
4024
+ minAiQuality
4025
+ }) => {
4026
+ if (!makeOrdersEnabled) {
4027
+ return false;
4028
+ }
4029
+ if (!signal || env === "BACKTEST") {
4030
+ return true;
4031
+ }
4032
+ if (isMlResultUnavailable({ env, ml })) {
4033
+ return false;
4034
+ }
4035
+ if (isMlRuntimeGateEnabled({ env, ml }) && ml?.result?.passed === false) {
4036
+ return false;
4037
+ }
4038
+ if (!aiEnabled) {
4039
+ return true;
4040
+ }
4041
+ return Number.isFinite(quality) && quality >= minAiQuality;
4042
+ };
4043
+ var getEntrySkipReason = ({
4044
+ makeOrdersEnabled,
4045
+ env,
4046
+ ml,
4047
+ aiEnabled,
4048
+ quality,
4049
+ minAiQuality
4050
+ }) => {
4051
+ if (!makeOrdersEnabled) {
4052
+ return "MAKE_ORDERS_DISABLED";
4053
+ }
4054
+ if (isMlResultUnavailable({ env, ml })) {
4055
+ return "ML_RESULT_UNAVAILABLE";
4056
+ }
4057
+ if (isMlRuntimeGateEnabled({ env, ml }) && ml?.result?.passed === false) {
4058
+ const probability = formatGateNumber(ml.result.probability);
4059
+ const threshold = formatGateNumber(ml.result.threshold);
4060
+ return `ML_THRESHOLD_NOT_MET (${probability} < ${threshold})`;
4061
+ }
4062
+ if (env !== "BACKTEST" && aiEnabled && quality == null) {
4063
+ return "AI_QUALITY_UNAVAILABLE";
4064
+ }
4065
+ if (env !== "BACKTEST" && aiEnabled && quality != null && Number.isFinite(quality) && quality < minAiQuality) {
4066
+ return `AI_QUALITY_BELOW_MIN (${quality} < ${minAiQuality})`;
4067
+ }
4068
+ return "ENTRY_POLICY_BLOCKED";
4069
+ };
4070
+ var buildHookEntry = ({
4071
+ decision,
4072
+ runtime
4073
+ }) => ({
4074
+ context: decision.entryContext,
4075
+ orderPlan: decision.orderPlan,
4076
+ signal: decision.signal,
4077
+ runtime: {
4078
+ raw: decision.runtime,
4079
+ resolved: runtime
4080
+ }
4081
+ });
4082
+ var buildHookPolicy = ({
4083
+ quality,
4084
+ makeOrdersEnabled,
4085
+ minAiQuality
4086
+ }) => ({
4087
+ aiQuality: quality,
4088
+ makeOrdersEnabled,
4089
+ minAiQuality
4090
+ });
4091
+ var buildMlHookContext = ({
4092
+ signal,
4093
+ env,
4094
+ ml
4095
+ }) => {
4096
+ if (env === "BACKTEST") {
4097
+ return {
4098
+ config: ml,
4099
+ attempted: false,
4100
+ applied: false,
4101
+ skippedReason: "BACKTEST"
4102
+ };
4103
+ }
4104
+ if (!ml) {
4105
+ return {
4106
+ attempted: false,
4107
+ applied: false,
4108
+ skippedReason: "NO_RUNTIME"
4109
+ };
4110
+ }
4111
+ if (ml.enabled === false) {
4112
+ return {
4113
+ config: ml,
4114
+ attempted: false,
4115
+ applied: false,
4116
+ skippedReason: "DISABLED"
4117
+ };
4118
+ }
4119
+ if (!ml.strategyConfig) {
4120
+ return {
4121
+ config: ml,
4122
+ attempted: false,
4123
+ applied: false,
4124
+ skippedReason: "NO_STRATEGY_CONFIG"
4125
+ };
4126
+ }
4127
+ if (typeof ml.mlThreshold !== "number") {
4128
+ return {
4129
+ config: ml,
4130
+ attempted: false,
4131
+ applied: false,
4132
+ skippedReason: "NO_THRESHOLD"
4133
+ };
4134
+ }
4135
+ if (signal.ml) {
4136
+ return {
4137
+ config: ml,
4138
+ attempted: true,
4139
+ applied: true,
4140
+ result: signal.ml
4141
+ };
4142
+ }
4143
+ return {
4144
+ config: ml,
4145
+ attempted: true,
4146
+ applied: false,
4147
+ skippedReason: "NO_RESULT"
4148
+ };
4149
+ };
4150
+ var buildAiHookContext = ({
4151
+ env,
4152
+ ai,
4153
+ quality
4154
+ }) => {
4155
+ if (env === "BACKTEST") {
4156
+ return {
4157
+ config: ai,
4158
+ attempted: false,
4159
+ applied: false,
4160
+ skippedReason: "BACKTEST"
4161
+ };
4162
+ }
4163
+ if (!ai) {
4164
+ return {
4165
+ attempted: false,
4166
+ applied: false,
4167
+ skippedReason: "NO_RUNTIME"
4168
+ };
4169
+ }
4170
+ if (ai.enabled === false) {
4171
+ return {
4172
+ config: ai,
4173
+ attempted: false,
4174
+ applied: false,
4175
+ skippedReason: "DISABLED"
4176
+ };
4177
+ }
4178
+ if (typeof quality === "number") {
4179
+ return {
4180
+ config: ai,
4181
+ attempted: true,
4182
+ applied: true,
4183
+ quality
4184
+ };
4185
+ }
4186
+ return {
4187
+ config: ai,
4188
+ attempted: true,
4189
+ applied: false,
4190
+ skippedReason: "NO_QUALITY"
4191
+ };
4192
+ };
4193
+
4194
+ // src/strategy/runtimeHooks.ts
4195
+ var normalizeConfigHookList = (value) => {
4196
+ if (Array.isArray(value)) {
4197
+ return value;
4198
+ }
4199
+ return value ? [value] : [];
4200
+ };
4201
+ var isStrategyDecision = (value) => {
4202
+ if (!value || typeof value !== "object") {
4203
+ return false;
4204
+ }
4205
+ const kind = value.kind;
4206
+ return kind === "skip" || kind === "entry" || kind === "exit" || kind === "protect";
4207
+ };
4208
+ var CONFIG_HOOK_STAGES = [
4209
+ "onInit",
4210
+ "onBar",
4211
+ "afterCoreDecision",
4212
+ "afterBarDecision",
4213
+ "onSkip",
4214
+ "beforeClosePosition",
4215
+ "afterEnrichMl",
4216
+ "afterEnrichAi",
4217
+ "beforeEntryGate",
4218
+ "beforePlaceOrder",
4219
+ "afterPlaceOrder"
4220
+ ];
4221
+ var isConfigHookStage = (stage) => CONFIG_HOOK_STAGES.includes(
4222
+ stage
4223
+ );
4224
+ var buildHookCtx = ({
4225
+ connector,
4226
+ strategyName,
4227
+ userName,
4228
+ symbol,
4229
+ universe,
4230
+ assetClass,
4231
+ accountId,
4232
+ deploymentId,
4233
+ policyProfileId,
4234
+ strategyConfig,
4235
+ env,
4236
+ isConfigFromBacktest
4237
+ }) => ({
4238
+ connector,
4239
+ strategyName,
4240
+ userName,
4241
+ symbol,
4242
+ ...universe ? { universe } : {},
4243
+ ...assetClass ? { assetClass } : {},
4244
+ ...accountId ? { accountId } : {},
4245
+ ...deploymentId ? { deploymentId } : {},
4246
+ ...policyProfileId ? { policyProfileId } : {},
4247
+ strategyConfig,
4248
+ env,
4249
+ isConfigFromBacktest
4250
+ });
4251
+ var shouldRecordRuntimeJournal = ({
4252
+ env,
4253
+ config
4254
+ }) => env !== "BACKTEST" && env !== "PARITY" && config.RECORD_RUNTIME_TRADES !== false;
4255
+ var isTestConnector = (connector) => Boolean(
4256
+ connector.__tradejsTestConnector
4257
+ );
4258
+ var canUseSharedReplayState = ({
4259
+ env,
4260
+ sharedReplayKey
4261
+ }) => (env === "BACKTEST" || env === "PARITY") && Boolean(sharedReplayKey);
4262
+
4263
+ // src/strategy/runtimeExecution.ts
4264
+ var import_logger9 = require("@tradejs/infra/logger");
4265
+ var import_types = require("@tradejs/types");
4266
+ var buildExitOrderSignal = ({
4267
+ strategyName,
4268
+ symbol,
4269
+ decision
4270
+ }) => {
4271
+ if (!strategyName) {
4272
+ return void 0;
4273
+ }
4274
+ return {
4275
+ signalId: `${strategyName}:${symbol}:exit:${decision.closePlan.timestamp}`,
4276
+ strategy: strategyName,
4277
+ symbol,
4278
+ interval: "15",
4279
+ direction: decision.closePlan.direction,
4280
+ timestamp: decision.closePlan.timestamp,
4281
+ figures: {},
4282
+ indicators: {},
4283
+ prices: {
4284
+ currentPrice: decision.closePlan.price,
4285
+ takeProfitPrice: decision.closePlan.price,
4286
+ stopLossPrice: decision.closePlan.price,
4287
+ riskRatio: 0
4288
+ },
4289
+ additionalIndicators: {
4290
+ exit: {
4291
+ code: decision.code
4292
+ }
4293
+ }
4294
+ };
4295
+ };
4296
+ var handleExitDecision = async ({
4297
+ connector,
4298
+ userName,
4299
+ strategyName,
4300
+ symbol,
4301
+ decision,
4302
+ market,
4303
+ onRuntimeClose,
4304
+ onRuntimeError
4305
+ }) => {
4306
+ try {
4307
+ let activeTradeForClose = null;
4308
+ if (userName) {
4309
+ const activeTrade = await getActiveRuntimeTrade({
4310
+ userName,
4311
+ symbol,
4312
+ accountId: connector.accountId,
4313
+ deploymentId: connector.deploymentId
4314
+ });
4315
+ if (!activeTrade) {
4316
+ import_logger9.logger.warn(
4317
+ "[%s] blocked closePosition for untracked runtime position: %s",
4318
+ strategyName ?? "unknown",
4319
+ symbol
4320
+ );
4321
+ return "CLOSE_BLOCKED_BY_UNTRACKED_POSITION";
4322
+ }
4323
+ if (!strategyName || activeTrade.strategy !== strategyName) {
4324
+ import_logger9.logger.warn(
4325
+ "[%s] blocked closePosition for foreign runtime position: %s ownedBy=%s",
4326
+ strategyName ?? "unknown",
4327
+ symbol,
4328
+ activeTrade.strategy
4329
+ );
4330
+ return "CLOSE_BLOCKED_BY_FOREIGN_STRATEGY_POSITION";
4331
+ }
4332
+ activeTradeForClose = activeTrade;
4333
+ }
4334
+ await connector.closePosition({
4335
+ symbol,
4336
+ price: decision.closePlan.price,
4337
+ timestamp: decision.closePlan.timestamp,
4338
+ direction: decision.closePlan.direction,
4339
+ signal: buildExitOrderSignal({
4340
+ strategyName,
4341
+ symbol,
4342
+ decision
4343
+ })
4344
+ });
4345
+ const closedTrade = await markRuntimeTradeClosed({
4346
+ userName,
4347
+ strategy: strategyName,
4348
+ symbol,
4349
+ exitPrice: decision.closePlan.price,
4350
+ exitTimestamp: decision.closePlan.timestamp,
4351
+ exitType: "exit",
4352
+ accountId: connector.accountId,
4353
+ deploymentId: connector.deploymentId
4354
+ });
4355
+ const trade = closedTrade ?? activeTradeForClose;
4356
+ if (trade && strategyName) {
4357
+ try {
4358
+ onRuntimeClose?.({
4359
+ userName,
4360
+ strategy: strategyName,
4361
+ openedByStrategy: trade.strategy,
4362
+ symbol,
4363
+ direction: trade.direction,
4364
+ code: decision.code,
4365
+ orderId: trade.orderId,
4366
+ signalId: trade.signalId,
4367
+ qty: trade.qty,
4368
+ entryPrice: trade.entryPrice,
4369
+ entryTimestamp: trade.entryTimestamp,
4370
+ exitPrice: closedTrade?.exitPrice ?? decision.closePlan.price,
4371
+ exitTimestamp: closedTrade?.exitTimestamp ?? decision.closePlan.timestamp,
4372
+ closedPnl: closedTrade?.closedPnl ?? trade.closedPnl ?? null,
4373
+ exitType: closedTrade?.exitType ?? "exit"
4374
+ });
4375
+ } catch (notificationError) {
4376
+ import_logger9.logger.error(
4377
+ "runtime close notification error: %s %s",
4378
+ symbol,
4379
+ notificationError
4380
+ );
4381
+ }
4382
+ }
4383
+ } catch (err) {
4384
+ await onRuntimeError?.({
4385
+ stage: "closePosition",
4386
+ error: err,
4387
+ decision,
4388
+ market
4389
+ });
4390
+ import_logger9.logger.error("close order error: %s %s", symbol, err);
4391
+ return "ORDER_ERROR";
4392
+ }
4393
+ return decision.code;
4394
+ };
4395
+ var handleProtectDecision = async ({
4396
+ connector,
4397
+ symbol,
4398
+ decision,
4399
+ market,
4400
+ onRuntimeError
4401
+ }) => {
4402
+ try {
4403
+ await updatePositionProtection({
4404
+ connector,
4405
+ symbol,
4406
+ direction: decision.protectPlan.direction,
4407
+ takeProfits: decision.protectPlan.takeProfits ?? [],
4408
+ stopLossPrice: decision.protectPlan.stopLossPrice ?? null
4409
+ });
4410
+ } catch (err) {
4411
+ await onRuntimeError?.({
4412
+ stage: "protectPosition",
4413
+ error: err,
4414
+ decision,
4415
+ market
4416
+ });
4417
+ import_logger9.logger.error("protect position error: %s %s", symbol, err);
4418
+ return "ORDER_ERROR";
4419
+ }
4420
+ return decision.code;
4421
+ };
4422
+ var executeEntryDecision = async ({
4423
+ connector,
4424
+ symbol,
4425
+ decision,
4426
+ runtime,
4427
+ manifest,
4428
+ hookCtx,
4429
+ market,
4430
+ entry,
4431
+ policy,
4432
+ ml,
4433
+ ai,
4434
+ recordRuntimeJournal,
4435
+ invokeStageHooks,
4436
+ notifyRuntimeError
4437
+ }) => {
4438
+ const signal = decision.signal;
4439
+ const beforePlaceOrder = async () => {
4440
+ await invokeStageHooks(
4441
+ "beforePlaceOrder",
4442
+ manifest?.hooks?.beforePlaceOrder,
4443
+ {
4444
+ ctx: hookCtx,
4445
+ market,
4446
+ decision,
4447
+ entry,
4448
+ policy,
4449
+ ml,
4450
+ ai
4451
+ },
4452
+ { decision, entry, market }
4453
+ );
4454
+ try {
4455
+ await runtime.beforePlaceOrder?.();
4456
+ } catch (error) {
4457
+ await notifyRuntimeError({
4458
+ stage: "runtime.beforePlaceOrder",
4459
+ error,
4460
+ decision,
4461
+ entry,
4462
+ market
4463
+ });
4464
+ throw error;
4465
+ }
4466
+ };
4467
+ try {
4468
+ if (signal) {
4469
+ await executeEntryOrder({
4470
+ connector,
4471
+ userName: hookCtx.userName,
4472
+ symbol,
4473
+ direction: decision.entryContext.direction,
4474
+ qty: decision.orderPlan.qty,
4475
+ currentPrice: decision.entryContext.prices.currentPrice,
4476
+ timestamp: decision.entryContext.timestamp,
4477
+ takeProfits: decision.orderPlan.takeProfits,
4478
+ stopLossPrice: decision.orderPlan.stopLossPrice,
4479
+ positionIntent: decision.orderPlan.positionIntent,
4480
+ ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
4481
+ signal,
4482
+ beforePlaceOrder,
4483
+ recordRuntimeTrade: recordRuntimeJournal
4484
+ });
4485
+ await invokeStageHooks(
4486
+ "afterPlaceOrder",
4487
+ manifest?.hooks?.afterPlaceOrder,
4488
+ {
4489
+ ctx: hookCtx,
4490
+ market,
4491
+ decision,
4492
+ entry,
4493
+ policy,
4494
+ ml,
4495
+ ai,
4496
+ order: {
4497
+ result: signal
4498
+ }
4499
+ },
4500
+ { decision, entry, market }
4501
+ );
4502
+ return signal;
4503
+ }
4504
+ await beforePlaceOrder();
4505
+ const arrivalSnapshot = await getOrderArrivalSnapshot({
4506
+ connector,
4507
+ symbol
4508
+ });
4509
+ validateEntryProtectionAtArrival({
4510
+ direction: decision.entryContext.direction,
4511
+ signalPrice: decision.entryContext.prices.currentPrice,
4512
+ bid: arrivalSnapshot.bid,
4513
+ ask: arrivalSnapshot.ask,
4514
+ arrivalMid: arrivalSnapshot.arrivalMid,
4515
+ takeProfits: decision.orderPlan.takeProfits,
4516
+ stopLossPrice: decision.orderPlan.stopLossPrice
4517
+ });
4518
+ const orderPlaced = await connector.placeOrder({
4519
+ symbol,
4520
+ qty: decision.orderPlan.qty,
4521
+ price: decision.entryContext.prices.currentPrice,
4522
+ timestamp: decision.entryContext.timestamp,
4523
+ direction: decision.entryContext.direction,
4524
+ positionIntent: decision.orderPlan.positionIntent,
4525
+ ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {}
4526
+ });
4527
+ if (!orderPlaced) {
4528
+ throw new Error("PLACE_ORDER_FAILED");
4529
+ }
4530
+ try {
4531
+ await updatePositionProtection({
4532
+ connector,
4533
+ symbol,
4534
+ direction: decision.entryContext.direction,
4535
+ qty: decision.orderPlan.qty,
4536
+ takeProfits: decision.orderPlan.takeProfits,
4537
+ stopLossPrice: decision.orderPlan.stopLossPrice
4538
+ });
4539
+ } catch (error) {
4540
+ await connector.closePosition({
4541
+ symbol,
4542
+ price: decision.entryContext.prices.currentPrice,
4543
+ timestamp: decision.entryContext.timestamp,
4544
+ direction: decision.entryContext.direction
4545
+ });
4546
+ throw error;
4547
+ }
4548
+ await invokeStageHooks(
4549
+ "afterPlaceOrder",
4550
+ manifest?.hooks?.afterPlaceOrder,
4551
+ {
4552
+ ctx: hookCtx,
4553
+ market,
4554
+ decision,
4555
+ entry,
4556
+ policy,
4557
+ ml,
4558
+ ai,
4559
+ order: {
4560
+ result: decision.code
4561
+ }
4562
+ },
4563
+ { decision, entry, market }
4564
+ );
4565
+ } catch (err) {
4566
+ if (signal) {
4567
+ signal.orderStatus = "failed";
4568
+ if (typeof signal.orderFailureReason !== "string" || !signal.orderFailureReason.trim()) {
4569
+ signal.orderFailureReason = typeof err?.message === "string" && err.message.trim() ? err.message.trim() : void 0;
4570
+ }
4571
+ }
4572
+ await notifyRuntimeError({
4573
+ stage: "placeOrder",
4574
+ error: err,
4575
+ decision,
4576
+ entry,
4577
+ market
4578
+ });
4579
+ if (err?.message === import_types.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY) {
4580
+ import_logger9.logger.warn("order warning: %s %s", symbol, err);
4581
+ } else {
4582
+ import_logger9.logger.error("order error: %s %s", symbol, err);
4583
+ }
4584
+ return signal ?? "ORDER_ERROR";
4585
+ }
4586
+ return signal ?? decision.code;
4587
+ };
4588
+
4589
+ // src/strategyRuntime.ts
4590
+ var cloneWithPropertyDescriptors = (value) => Object.create(
4591
+ Object.getPrototypeOf(value),
4592
+ Object.getOwnPropertyDescriptors(value)
4593
+ );
4594
+ var createStrategyRuntime = ({
4595
+ strategyName,
4596
+ defaults,
4597
+ createCore,
4598
+ manifest: staticManifest,
4599
+ detectorKey,
4600
+ detectorNoSignalSkipReason,
4601
+ resolveRegisteredManifest
4602
+ }) => {
4603
+ const projectRoot = getTradejsProjectCwd();
4604
+ const resolveManifest = (name) => {
4605
+ if (!name) {
4606
+ return void 0;
4607
+ }
4608
+ if (staticManifest?.name === name) {
4609
+ return staticManifest;
4610
+ }
4611
+ return resolveRegisteredManifest?.(name);
4612
+ };
4613
+ const creator = async ({
4614
+ userName,
4615
+ connectorName,
4616
+ config: baseConfig,
4617
+ symbol,
4618
+ universe: requestedUniverse,
4619
+ assetClass,
4620
+ accountId: requestedAccountId,
4621
+ deploymentId: requestedDeploymentId,
4622
+ policyProfileId,
4623
+ runtimeConfigId,
4624
+ runtimeConfigSnapshot,
4625
+ data,
4626
+ btcData,
4627
+ ethData = [],
4628
+ btcBinanceData,
4629
+ btcCoinbaseData,
4630
+ backtestExecutionMarketData,
4631
+ connector,
4632
+ sharedIndicatorsReplayKey,
4633
+ sharedStrategyStateKey,
4634
+ onRuntimeClose
4635
+ }) => {
4636
+ const { config, isConfigFromBacktest } = await resolveStrategyConfig({
4637
+ strategyName,
4638
+ userName,
4639
+ symbol,
4640
+ baseConfig,
4641
+ defaults,
4642
+ runtimeConfigId,
4643
+ runtimeConfigSnapshot
4644
+ });
4645
+ const universe = requestedUniverse ?? connector.universe;
4646
+ const accountId = requestedAccountId ?? connector.accountId;
4647
+ const deploymentId = requestedDeploymentId ?? connector.deploymentId;
4648
+ const projectConfig = await loadTradejsConfig(projectRoot);
4649
+ const projectHooks = projectConfig.hooks;
4650
+ const env = String(config.ENV ?? "BACKTEST");
4651
+ const backtestPriceMode = config.BACKTEST_PRICE_MODE ?? "open";
4652
+ const backtestEntryDelayBars = env === "BACKTEST" ? resolveBacktestEntryDelayBars(config.BACKTEST_ENTRY_DELAY_BARS) : 0;
4653
+ const resolvedBacktestExecutionInterval = config.BACKTEST_EXECUTION_INTERVAL ?? backtestExecutionMarketData?.interval ?? resolveBacktestExecutionIntervalForPrimary(config.INTERVAL ?? "15");
4654
+ const backtestExecutionInterval = resolvedBacktestExecutionInterval == null ? null : String(resolvedBacktestExecutionInterval);
4655
+ const backtestExecutionIntervalLabel = backtestExecutionInterval == null ? void 0 : String(backtestExecutionInterval);
4656
+ const primaryIntervalMs = safeIntervalToMs(config.INTERVAL ?? "15");
4657
+ const backtestExecutionIntervalMs = safeIntervalToMs(
4658
+ backtestExecutionInterval
4659
+ );
4660
+ const backtestExecutionDelayMs = resolveBacktestExecutionDelayMs(
4661
+ config.BACKTEST_EXECUTION_DELAY_MS,
4662
+ backtestExecutionIntervalMs ?? import_constants5.BACKTEST_EXECUTION_DELAY_MS
4663
+ );
4664
+ const backtestExecutionCandleByTimestamp = backtestExecutionMarketData?.dataByTimestamp ?? buildCandleByTimestamp(backtestExecutionMarketData?.data);
4665
+ const backtestExecutionBtcCandleByTimestamp = backtestExecutionMarketData?.btcDataByTimestamp ?? buildCandleByTimestamp(backtestExecutionMarketData?.btcData);
4666
+ const canUseLowerBacktestExecution = env === "BACKTEST" && backtestEntryDelayBars > 0 && backtestExecutionIntervalMs != null && primaryIntervalMs != null && backtestExecutionIntervalMs < primaryIntervalMs;
4667
+ const recordRuntimeJournal = shouldRecordRuntimeJournal({ env, config });
4668
+ const strategyManifest = resolveManifest(strategyName);
4669
+ const requestedPolicyProfileId = policyProfileId ?? (typeof config.POLICY_PROFILE_ID === "string" ? config.POLICY_PROFILE_ID : void 0);
4670
+ const getPolicyProfile = (name = strategyName) => resolveStrategyPolicyProfile(resolveManifest(name), {
4671
+ profileId: requestedPolicyProfileId,
4672
+ universe,
4673
+ assetClass
4674
+ });
4675
+ const strategyPolicyProfile = getPolicyProfile();
4676
+ const indicatorPeriods = (0, import_strategies6.buildDefaultIndicatorPeriods)(config);
4677
+ const hookBase = {
4678
+ connector,
4679
+ strategyName,
4680
+ userName,
4681
+ symbol,
4682
+ universe,
4683
+ assetClass,
4684
+ accountId,
4685
+ deploymentId,
4686
+ policyProfileId: strategyPolicyProfile?.id ?? requestedPolicyProfileId,
4687
+ strategyConfig: config,
4688
+ env,
4689
+ isConfigFromBacktest
4690
+ };
4691
+ const getHookCtx = (name = strategyName) => {
4692
+ const profile = getPolicyProfile(name);
4693
+ return buildHookCtx({
4694
+ ...hookBase,
4695
+ strategyName: name,
4696
+ policyProfileId: profile?.id ?? requestedPolicyProfileId
4697
+ });
4698
+ };
4699
+ const getProjectHookList = (stage) => normalizeConfigHookList(projectHooks?.[stage]);
4700
+ const indicatorReplayKey = JSON.stringify({
4701
+ periods: indicatorPeriods,
4702
+ universe
4703
+ });
4704
+ const sharedReplayEnabled = canUseSharedReplayState({
4705
+ env,
4706
+ sharedReplayKey: sharedIndicatorsReplayKey
4707
+ });
4708
+ const indicatorSharedReplayKey = sharedReplayEnabled && sharedIndicatorsReplayKey ? `${sharedIndicatorsReplayKey}:indicators:${indicatorReplayKey}` : void 0;
4709
+ const strategyStateBaseKey = env === "CRON" && sharedStrategyStateKey ? sharedStrategyStateKey : sharedReplayEnabled ? sharedIndicatorsReplayKey : void 0;
4710
+ const strategySharedReplayKey = strategyStateBaseKey ? `${strategyStateBaseKey}:strategy:${strategyName}` : void 0;
4711
+ const notifyRuntimeError = async ({
4712
+ stage,
4713
+ error,
4714
+ decision,
4715
+ entry,
4716
+ market
4717
+ }) => {
4718
+ const errorStrategyName = decision?.kind === "entry" ? decision.entryContext.strategy : strategyName;
4719
+ const errorManifest = resolveManifest(errorStrategyName) ?? strategyManifest;
4720
+ const errorParams = {
4721
+ ctx: getHookCtx(errorStrategyName),
4722
+ market,
4723
+ decision,
4724
+ entry,
4725
+ error: {
4726
+ stage,
4727
+ cause: error
4728
+ }
4729
+ };
4730
+ for (const projectHook of getProjectHookList("onRuntimeError")) {
4731
+ try {
4732
+ await projectHook(errorParams);
4733
+ } catch (hookError) {
4734
+ import_logger10.logger.error(
4735
+ "project hook onRuntimeError failed: %s %s",
4736
+ strategyName,
4737
+ hookError
4738
+ );
4739
+ }
4740
+ }
4741
+ const onRuntimeError = errorManifest?.hooks?.onRuntimeError;
4742
+ if (!onRuntimeError) {
4743
+ return;
4744
+ }
4745
+ try {
4746
+ await onRuntimeError(errorParams);
4747
+ } catch (hookError) {
4748
+ import_logger10.logger.error(
4749
+ "runtime hook onRuntimeError failed: %s %s",
4750
+ strategyName,
4751
+ hookError
4752
+ );
4753
+ }
4754
+ };
4755
+ const invokeHook = async (stage, hook, params, errorContext = {}) => {
4756
+ if (!hook) {
4757
+ return void 0;
4758
+ }
4759
+ try {
4760
+ return await hook(params);
4761
+ } catch (error) {
4762
+ import_logger10.logger.error(
4763
+ 'strategy hook "%s" failed for %s: %s',
4764
+ stage,
4765
+ strategyName,
4766
+ error
4767
+ );
4768
+ await notifyRuntimeError({
4769
+ stage,
4770
+ error,
4771
+ decision: errorContext.decision,
4772
+ entry: errorContext.entry,
4773
+ market: errorContext.market
4774
+ });
4775
+ return void 0;
4776
+ }
4777
+ };
4778
+ const invokeProjectHooks = async (stage, params, errorContext = {}) => {
4779
+ const results = [];
4780
+ for (const hook of getProjectHookList(stage)) {
4781
+ const result = await invokeHook(
4782
+ stage,
4783
+ hook,
4784
+ params,
4785
+ errorContext
4786
+ );
4787
+ if (result !== void 0) {
4788
+ results.push(result);
4789
+ }
4790
+ }
4791
+ return results;
4792
+ };
4793
+ const invokeStageHooks = async (stage, hook, params, errorContext = {}) => {
4794
+ if (isConfigHookStage(stage)) {
4795
+ await invokeProjectHooks(stage, params, errorContext);
4796
+ }
4797
+ return invokeHook(stage, hook, params, errorContext);
4798
+ };
4799
+ const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
4800
+ const projectResults = await invokeProjectHooks(
4801
+ stage,
4802
+ params,
4803
+ errorContext
4804
+ );
4805
+ const projectBlock = projectResults.find(
4806
+ (result) => result?.allow === false
4807
+ );
4808
+ if (projectBlock?.allow === false) {
4809
+ return projectBlock;
4810
+ }
4811
+ return invokeHook(
4812
+ stage,
4813
+ hook,
4814
+ params,
4815
+ errorContext
4816
+ );
4817
+ };
4818
+ const applyProjectAfterCoreDecisionHooks = async ({
4819
+ hookCtx,
4820
+ market,
4821
+ decision
4822
+ }) => {
4823
+ let nextDecision = decision;
4824
+ for (const hook of getProjectHookList(
4825
+ "afterCoreDecision"
4826
+ )) {
4827
+ const result = await invokeHook(
4828
+ "afterCoreDecision",
4829
+ hook,
4830
+ {
4831
+ ctx: hookCtx,
4832
+ market,
4833
+ decision: nextDecision
4834
+ },
4835
+ {
4836
+ decision: nextDecision,
4837
+ market
4838
+ }
4839
+ );
4840
+ if (isStrategyDecision(result)) {
4841
+ nextDecision = result;
4842
+ }
4843
+ }
4844
+ return nextDecision;
4845
+ };
4846
+ const applyProjectAfterBarDecisionHooks = async ({
4847
+ hookCtx,
4848
+ market,
4849
+ decision
4850
+ }) => {
4851
+ let nextDecision = decision;
4852
+ for (const hook of getProjectHookList(
4853
+ "afterBarDecision"
4854
+ )) {
4855
+ const result = await invokeHook(
4856
+ "afterBarDecision",
4857
+ hook,
4858
+ {
4859
+ ctx: hookCtx,
4860
+ market,
4861
+ decision: nextDecision
4862
+ },
4863
+ {
4864
+ decision: nextDecision,
4865
+ market
4866
+ }
4867
+ );
4868
+ if (isStrategyDecision(result)) {
4869
+ nextDecision = result;
4870
+ }
4871
+ }
4872
+ return nextDecision;
4873
+ };
4874
+ const applyProjectOnBarHooks = async ({
4875
+ hookCtx,
4876
+ market
4877
+ }) => {
4878
+ for (const hook of getProjectHookList(
4879
+ "onBar"
4880
+ )) {
4881
+ const result = await invokeHook(
4882
+ "onBar",
4883
+ hook,
4884
+ {
4885
+ ctx: hookCtx,
4886
+ market
4887
+ },
4888
+ {
4889
+ market
4890
+ }
4891
+ );
4892
+ if (isStrategyDecision(result)) {
4893
+ return result;
4894
+ }
4895
+ }
4896
+ return void 0;
4897
+ };
4898
+ const indicatorsState = (0, import_strategies6.createStrategyIndicatorsState)({
4899
+ env,
4900
+ data,
4901
+ btcData,
4902
+ ethData,
4903
+ btcBinanceData,
4904
+ btcCoinbaseData,
4905
+ periods: indicatorPeriods,
4906
+ pluginRegistryScope: projectRoot,
4907
+ sharedReplayKey: indicatorSharedReplayKey,
4908
+ useBtcReference: universe === "crypto"
4909
+ });
4910
+ const coreContextRequirements = new Set(
4911
+ strategyManifest?.contextRequirements?.core ?? []
4912
+ );
4913
+ const strategyApi = (0, import_strategies6.createStrategyAPI)({
4914
+ strategy: strategyName,
4915
+ symbol,
4916
+ interval: config.INTERVAL ?? "15",
4917
+ env,
4918
+ connector,
4919
+ cachedData: data,
4920
+ indicatorsState,
4921
+ isConfigFromBacktest,
4922
+ sharedReplayKey: strategySharedReplayKey,
4923
+ getSharedReplayState: import_strategies6.getSharedStrategyReplayState,
4924
+ loadDecisionBaseContext: async ({
4925
+ baseContext,
4926
+ candle,
4927
+ symbol: decisionSymbol,
4928
+ interval: decisionInterval
4929
+ }) => {
4930
+ if (!baseContext) return void 0;
4931
+ if (!coreContextRequirements.has("hyperliquidWhales")) {
4932
+ return baseContext;
4933
+ }
4934
+ const hyperliquidWhales = await loadHyperliquidWhaleFlowContext({
4935
+ symbol: decisionSymbol,
4936
+ interval: decisionInterval,
4937
+ timestamp: candle.timestamp,
4938
+ env,
4939
+ useSeriesCache: env === "BACKTEST" || env === "PARITY"
4940
+ });
4941
+ if (!hyperliquidWhales) return baseContext;
4942
+ const participation = cloneWithPropertyDescriptors(
4943
+ baseContext.participation
4944
+ );
4945
+ Object.defineProperty(participation, "hyperliquidWhales", {
4946
+ configurable: true,
4947
+ enumerable: true,
4948
+ value: hyperliquidWhales,
4949
+ writable: true
4950
+ });
4951
+ const enrichedBaseContext = cloneWithPropertyDescriptors(baseContext);
4952
+ Object.defineProperty(enrichedBaseContext, "participation", {
4953
+ configurable: true,
4954
+ enumerable: true,
4955
+ value: participation,
4956
+ writable: true
4957
+ });
4958
+ return enrichedBaseContext;
4959
+ }
4960
+ });
4961
+ const core = await createCore({
4962
+ config,
4963
+ data,
4964
+ strategyApi,
4965
+ indicatorsState
4966
+ });
4967
+ await invokeStageHooks("onInit", strategyManifest?.hooks?.onInit, {
4968
+ ctx: getHookCtx(),
4969
+ market: {
4970
+ data,
4971
+ btcData
4972
+ }
4973
+ });
4974
+ const appendCurrentMarketData = (candle, btcCandle, ethCandle) => {
4975
+ if (data[data.length - 1]?.timestamp !== candle.timestamp) {
4976
+ data.push(candle);
4977
+ }
4978
+ if (universe === "crypto" && btcData[btcData.length - 1]?.timestamp !== btcCandle.timestamp) {
4979
+ btcData.push(btcCandle);
4980
+ }
4981
+ if (universe === "crypto" && ethCandle && ethData[ethData.length - 1]?.timestamp !== ethCandle.timestamp) {
4982
+ ethData.push(ethCandle);
4983
+ }
4984
+ };
4985
+ const resolveEthCandle = (candle, ethCandle) => {
4986
+ if (ethCandle?.timestamp === candle.timestamp) {
4987
+ return ethCandle;
4988
+ }
4989
+ const alignedEthCandle = ethData[data.length - 1];
4990
+ if (alignedEthCandle?.timestamp === candle.timestamp) {
4991
+ return alignedEthCandle;
4992
+ }
4993
+ const latestEthCandle = ethData[ethData.length - 1];
4994
+ if (latestEthCandle?.timestamp === candle.timestamp) {
4995
+ return latestEthCandle;
4996
+ }
4997
+ return void 0;
4998
+ };
4999
+ const resolveBacktestExecutionCandle = (candle, btcCandle) => {
5000
+ if (!import_constants5.BACKTEST_LOWER_TIMEFRAME_EXECUTION_ENABLED) {
5001
+ return {
5002
+ candle,
5003
+ btcCandle,
5004
+ source: "primary_timeframe",
5005
+ requestedExecutionTimestamp: candle.timestamp,
5006
+ executionInterval: String(config.INTERVAL ?? "15"),
5007
+ executionDelayMs: 0,
5008
+ primaryExecutionTimestamp: candle.timestamp
5009
+ };
5010
+ }
5011
+ const requestedExecutionTimestamp = candle.timestamp + backtestExecutionDelayMs;
5012
+ const primaryExecutionTimestamp = candle.timestamp;
5013
+ if (!canUseLowerBacktestExecution || primaryIntervalMs == null) {
5014
+ return {
5015
+ source: "lower_timeframe",
5016
+ requestedExecutionTimestamp,
5017
+ executionInterval: backtestExecutionIntervalLabel,
5018
+ executionDelayMs: backtestExecutionDelayMs,
5019
+ primaryExecutionTimestamp,
5020
+ skipReason: "BACKTEST_LOWER_EXECUTION_UNAVAILABLE"
5021
+ };
5022
+ }
5023
+ if (requestedExecutionTimestamp >= candle.timestamp + primaryIntervalMs) {
5024
+ return {
5025
+ source: "lower_timeframe",
5026
+ requestedExecutionTimestamp,
5027
+ executionInterval: backtestExecutionIntervalLabel,
5028
+ executionDelayMs: backtestExecutionDelayMs,
5029
+ primaryExecutionTimestamp,
5030
+ skipReason: "BACKTEST_LOWER_EXECUTION_DELAY_OUT_OF_BAR"
5031
+ };
5032
+ }
5033
+ const lowerCandle = backtestExecutionCandleByTimestamp.get(
5034
+ requestedExecutionTimestamp
5035
+ );
5036
+ const lowerBtcCandle = backtestExecutionBtcCandleByTimestamp.get(
5037
+ requestedExecutionTimestamp
5038
+ );
5039
+ if (lowerCandle && lowerBtcCandle) {
5040
+ return {
5041
+ candle: lowerCandle,
5042
+ btcCandle: lowerBtcCandle,
5043
+ source: "lower_timeframe",
5044
+ requestedExecutionTimestamp,
5045
+ executionInterval: backtestExecutionIntervalLabel,
5046
+ executionDelayMs: backtestExecutionDelayMs,
5047
+ primaryExecutionTimestamp
5048
+ };
5049
+ }
5050
+ return {
5051
+ source: "lower_timeframe",
5052
+ requestedExecutionTimestamp,
5053
+ executionInterval: backtestExecutionIntervalLabel,
5054
+ executionDelayMs: backtestExecutionDelayMs,
5055
+ primaryExecutionTimestamp,
5056
+ skipReason: !lowerCandle ? "BACKTEST_LOWER_EXECUTION_CANDLE_MISSING" : "BACKTEST_LOWER_EXECUTION_BTC_CANDLE_MISSING"
5057
+ };
5058
+ };
5059
+ let pendingBacktestEntry = null;
5060
+ const flushPendingBacktestEntry = async (candle, btcCandle, ethCandle) => {
5061
+ if (!pendingBacktestEntry) {
5062
+ return void 0;
5063
+ }
5064
+ appendCurrentMarketData(candle, btcCandle, ethCandle);
5065
+ const resolvedEthCandle = resolveEthCandle(candle, ethCandle);
5066
+ indicatorsState.setCurrentBar(candle, btcCandle, resolvedEthCandle);
5067
+ pendingBacktestEntry.delayBarsRemaining -= 1;
5068
+ if (pendingBacktestEntry.delayBarsRemaining > 0) {
5069
+ return `BACKTEST_ENTRY_DELAY_PENDING:${pendingBacktestEntry.delayBarsRemaining}`;
5070
+ }
5071
+ const pending = pendingBacktestEntry;
5072
+ pendingBacktestEntry = null;
5073
+ const executionCandleResolution = resolveBacktestExecutionCandle(
5074
+ candle,
5075
+ btcCandle
5076
+ );
5077
+ const execution = applyBacktestDelayedEntryExecution({
5078
+ decision: pending.decision,
5079
+ execution: executionCandleResolution,
5080
+ backtestPriceMode: executionCandleResolution.source === "primary_timeframe" ? "open" : backtestPriceMode,
5081
+ delayBars: pending.delayBars
5082
+ });
5083
+ if (execution.skipReason) {
5084
+ return pending.decision.signal ?? execution.skipReason;
5085
+ }
5086
+ if (!execution.executionCandle || !execution.btcExecutionCandle) {
5087
+ return pending.decision.signal ?? "BACKTEST_LOWER_EXECUTION_CANDLE_MISSING";
5088
+ }
5089
+ const market = {
5090
+ candle: execution.executionCandle,
5091
+ btcCandle: execution.btcExecutionCandle
5092
+ };
5093
+ const entry = buildHookEntry({
5094
+ decision: pending.decision,
5095
+ runtime: pending.runtime
5096
+ });
5097
+ return executeEntryDecision({
5098
+ connector,
5099
+ symbol,
5100
+ decision: pending.decision,
5101
+ runtime: pending.runtime,
5102
+ manifest: pending.manifest,
5103
+ hookCtx: pending.hookCtx,
5104
+ market,
5105
+ entry,
5106
+ policy: pending.policy,
5107
+ ml: pending.ml,
5108
+ ai: pending.ai,
5109
+ recordRuntimeJournal,
5110
+ invokeStageHooks,
5111
+ notifyRuntimeError
5112
+ });
5113
+ };
5114
+ const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
5115
+ appendCurrentMarketData(candle, btcCandle, options.ethCandle);
5116
+ const ethCandle = resolveEthCandle(candle, options.ethCandle);
5117
+ indicatorsState.setCurrentBar(candle, btcCandle, ethCandle);
5118
+ const delayedEntrySignal = await flushPendingBacktestEntry(
5119
+ candle,
5120
+ btcCandle,
5121
+ ethCandle
5122
+ );
5123
+ if (delayedEntrySignal) {
5124
+ return delayedEntrySignal;
5125
+ }
5126
+ const market = {
5127
+ candle,
5128
+ btcCandle
5129
+ };
5130
+ const onBarHookCtx = getHookCtx();
5131
+ const projectOnBarDecision = await applyProjectOnBarHooks({
5132
+ hookCtx: onBarHookCtx,
5133
+ market
5134
+ });
5135
+ let decision;
5136
+ let shouldInvokeAfterCoreDecisionHook = false;
5137
+ if (projectOnBarDecision) {
5138
+ decision = projectOnBarDecision;
5139
+ } else {
5140
+ const manifestOnBarDecision = await invokeHook(
5141
+ "onBar",
5142
+ strategyManifest?.hooks?.onBar,
5143
+ {
5144
+ ctx: onBarHookCtx,
5145
+ market
5146
+ },
5147
+ { market }
5148
+ );
5149
+ if (isStrategyDecision(manifestOnBarDecision)) {
5150
+ decision = manifestOnBarDecision;
5151
+ } else {
5152
+ decision = options.coreDecisionOverride ?? await core(candle, btcCandle);
5153
+ shouldInvokeAfterCoreDecisionHook = true;
5154
+ }
5155
+ }
5156
+ if (shouldInvokeAfterCoreDecisionHook) {
5157
+ const initialDecisionStrategyName = decision.kind === "entry" ? decision.entryContext.strategy : strategyName;
5158
+ decision = await applyProjectAfterCoreDecisionHooks({
5159
+ hookCtx: getHookCtx(initialDecisionStrategyName),
5160
+ market,
5161
+ decision
5162
+ });
5163
+ }
5164
+ const initialAfterBarDecisionStrategyName = decision.kind === "entry" ? decision.entryContext.strategy : strategyName;
5165
+ decision = await applyProjectAfterBarDecisionHooks({
5166
+ hookCtx: getHookCtx(initialAfterBarDecisionStrategyName),
5167
+ market,
5168
+ decision
5169
+ });
5170
+ const decisionStrategyName = decision.kind === "entry" ? decision.entryContext.strategy : strategyName;
5171
+ const decisionManifest = resolveManifest(decisionStrategyName) ?? strategyManifest;
5172
+ const decisionHookCtx = getHookCtx(decisionStrategyName);
5173
+ if (shouldInvokeAfterCoreDecisionHook) {
5174
+ await invokeHook(
5175
+ "afterCoreDecision",
5176
+ decisionManifest?.hooks?.afterCoreDecision,
5177
+ {
5178
+ ctx: decisionHookCtx,
5179
+ market,
5180
+ decision
5181
+ },
5182
+ { decision, market }
5183
+ );
5184
+ }
5185
+ await invokeHook(
5186
+ "afterBarDecision",
5187
+ decisionManifest?.hooks?.afterBarDecision,
5188
+ {
5189
+ ctx: decisionHookCtx,
5190
+ market,
5191
+ decision
5192
+ },
5193
+ { decision, market }
5194
+ );
5195
+ if (decision.kind === "skip") {
5196
+ await invokeStageHooks(
5197
+ "onSkip",
5198
+ decisionManifest?.hooks?.onSkip,
5199
+ {
5200
+ ctx: decisionHookCtx,
5201
+ market,
5202
+ decision
5203
+ },
5204
+ { decision, market }
5205
+ );
5206
+ return decision.code;
5207
+ }
5208
+ const rawMakeOrdersEnabled = typeof config.MAKE_ORDERS === "boolean" ? config.MAKE_ORDERS : true;
5209
+ const makeOrdersEnabled = rawMakeOrdersEnabled && (env !== "PARITY" || isTestConnector(connector));
5210
+ if (decision.kind === "exit") {
5211
+ if (!makeOrdersEnabled) {
5212
+ return decision.code;
5213
+ }
5214
+ const closeGate = await invokeGateHooks(
5215
+ "beforeClosePosition",
5216
+ decisionManifest?.hooks?.beforeClosePosition,
5217
+ {
5218
+ ctx: decisionHookCtx,
5219
+ market,
5220
+ decision
5221
+ },
5222
+ { decision, market }
5223
+ );
5224
+ if (closeGate?.allow === false) {
5225
+ return closeGate.reason ? `CLOSE_BLOCKED_BY_HOOK:${closeGate.reason}` : "CLOSE_BLOCKED_BY_HOOK";
5226
+ }
5227
+ return handleExitDecision({
5228
+ connector,
5229
+ userName: recordRuntimeJournal ? userName : void 0,
5230
+ strategyName,
5231
+ symbol,
5232
+ decision,
5233
+ market,
5234
+ onRuntimeClose,
5235
+ onRuntimeError: async ({
5236
+ stage,
5237
+ error,
5238
+ decision: exitDecision,
5239
+ market: errorMarket
5240
+ }) => {
5241
+ await notifyRuntimeError({
5242
+ stage,
5243
+ error,
5244
+ decision: exitDecision,
5245
+ market: errorMarket
5246
+ });
5247
+ }
5248
+ });
5249
+ }
5250
+ if (decision.kind === "protect") {
5251
+ if (!makeOrdersEnabled) {
5252
+ return decision.code;
5253
+ }
5254
+ return handleProtectDecision({
5255
+ connector,
5256
+ symbol,
5257
+ decision,
5258
+ market,
5259
+ onRuntimeError: async ({
5260
+ stage,
5261
+ error,
5262
+ decision: protectDecision,
5263
+ market: errorMarket
5264
+ }) => {
5265
+ await notifyRuntimeError({
5266
+ stage,
5267
+ error,
5268
+ decision: protectDecision,
5269
+ market: errorMarket
5270
+ });
5271
+ }
5272
+ });
5273
+ }
5274
+ const runtime = resolveEntryRuntimePolicy({
5275
+ decision,
5276
+ config,
5277
+ manifest: decisionManifest,
5278
+ policyProfile: getPolicyProfile(decisionStrategyName)
5279
+ });
5280
+ const signal = decision.signal;
5281
+ if (signal) {
5282
+ if (universe) signal.universe = universe;
5283
+ if (assetClass) signal.assetClass = assetClass;
5284
+ if (accountId) signal.accountId = accountId;
5285
+ if (deploymentId) signal.deploymentId = deploymentId;
5286
+ if (runtimeConfigId) {
5287
+ signal.runtimeConfigId = runtimeConfigId;
5288
+ if (runtimeConfigId !== "config") {
5289
+ signal.signalId = `${signal.signalId}:${runtimeConfigId}`;
5290
+ }
5291
+ }
5292
+ if (decisionHookCtx.policyProfileId) {
5293
+ signal.policyProfileId = decisionHookCtx.policyProfileId;
5294
+ }
5295
+ }
5296
+ const entry = buildHookEntry({
5297
+ decision,
5298
+ runtime
5299
+ });
5300
+ let ml;
5301
+ if (signal) {
5302
+ try {
5303
+ await enrichSignalWithMl({
5304
+ signal,
5305
+ env,
5306
+ ml: runtime.ml
5307
+ });
5308
+ } catch (error) {
5309
+ await notifyRuntimeError({
5310
+ stage: "enrichSignalWithMl",
5311
+ error,
5312
+ decision,
5313
+ entry,
5314
+ market
5315
+ });
5316
+ throw error;
5317
+ }
5318
+ ml = buildMlHookContext({
5319
+ signal,
5320
+ env,
5321
+ ml: runtime.ml
5322
+ });
5323
+ await invokeStageHooks(
5324
+ "afterEnrichMl",
5325
+ decisionManifest?.hooks?.afterEnrichMl,
5326
+ {
5327
+ ctx: decisionHookCtx,
5328
+ market,
5329
+ decision,
5330
+ entry,
5331
+ ml
5332
+ },
5333
+ { decision, entry, market }
5334
+ );
5335
+ }
5336
+ let quality;
5337
+ let ai;
5338
+ if (signal) {
5339
+ try {
5340
+ await enrichSignalWithMarketContextStages({
5341
+ signal,
5342
+ env,
5343
+ includeHyperliquidWhales: false
5344
+ });
5345
+ quality = await enrichSignalWithAi({
5346
+ signal,
5347
+ userName,
5348
+ symbol,
5349
+ direction: signal.direction,
5350
+ env,
5351
+ ai: runtime.ai
5352
+ });
5353
+ } catch (error) {
5354
+ await notifyRuntimeError({
5355
+ stage: "enrichSignalWithAi",
5356
+ error,
5357
+ decision,
5358
+ entry,
5359
+ market
5360
+ });
5361
+ throw error;
5362
+ }
5363
+ ai = buildAiHookContext({
5364
+ env,
5365
+ ai: runtime.ai,
5366
+ quality
5367
+ });
5368
+ await invokeStageHooks(
5369
+ "afterEnrichAi",
5370
+ decisionManifest?.hooks?.afterEnrichAi,
5371
+ {
5372
+ ctx: decisionHookCtx,
5373
+ market,
5374
+ decision,
5375
+ entry,
5376
+ ml: ml ?? buildMlHookContext({ signal, env, ml: runtime.ml }),
5377
+ ai
5378
+ },
5379
+ { decision, entry, market }
5380
+ );
5381
+ }
5382
+ const minAiQuality = runtime.ai?.minQuality ?? 4;
5383
+ const aiEnabled = runtime.ai?.enabled !== false && runtime.ai != null;
5384
+ const policy = buildHookPolicy({
5385
+ quality,
5386
+ makeOrdersEnabled,
5387
+ minAiQuality
5388
+ });
5389
+ const shouldMakeOrder = shouldExecuteEntryDecision({
5390
+ makeOrdersEnabled,
5391
+ env,
5392
+ signal,
5393
+ ml,
5394
+ aiEnabled,
5395
+ quality,
5396
+ minAiQuality
5397
+ });
5398
+ if (!shouldMakeOrder) {
5399
+ if (signal) {
5400
+ signal.orderStatus = "skipped";
5401
+ signal.orderSkipReason = getEntrySkipReason({
5402
+ makeOrdersEnabled,
5403
+ env,
5404
+ ml,
5405
+ aiEnabled,
5406
+ quality,
5407
+ minAiQuality
5408
+ });
5409
+ }
5410
+ return signal ?? decision.code;
5411
+ }
5412
+ const entryGate = await invokeGateHooks(
5413
+ "beforeEntryGate",
5414
+ decisionManifest?.hooks?.beforeEntryGate,
5415
+ {
5416
+ ctx: decisionHookCtx,
5417
+ market,
5418
+ decision,
5419
+ entry,
5420
+ policy,
5421
+ ml,
5422
+ ai
5423
+ },
5424
+ { decision, entry, market }
5425
+ );
5426
+ if (entryGate?.allow === false) {
5427
+ const skipReason = entryGate.reason ? `HOOK_BEFORE_ENTRY_GATE:${entryGate.reason}` : "HOOK_BEFORE_ENTRY_GATE";
5428
+ if (signal) {
5429
+ signal.orderStatus = "skipped";
5430
+ signal.orderSkipReason = skipReason;
5431
+ }
5432
+ return signal ?? skipReason;
5433
+ }
5434
+ if (backtestEntryDelayBars > 0) {
5435
+ pendingBacktestEntry = {
5436
+ delayBars: backtestEntryDelayBars,
5437
+ delayBarsRemaining: backtestEntryDelayBars,
5438
+ decision,
5439
+ runtime,
5440
+ manifest: decisionManifest,
5441
+ hookCtx: decisionHookCtx,
5442
+ policy,
5443
+ ml,
5444
+ ai
5445
+ };
5446
+ return `BACKTEST_ENTRY_DELAY_QUEUED:${backtestEntryDelayBars}`;
5447
+ }
5448
+ return executeEntryDecision({
5449
+ connector,
5450
+ symbol,
5451
+ decision,
5452
+ runtime,
5453
+ manifest: decisionManifest,
5454
+ hookCtx: decisionHookCtx,
5455
+ market,
5456
+ entry,
5457
+ policy,
5458
+ ml,
5459
+ ai,
5460
+ recordRuntimeJournal,
5461
+ invokeStageHooks,
5462
+ notifyRuntimeError
5463
+ });
5464
+ };
5465
+ const strategy = (async (candle, btcCandle, ethCandle) => runWithDecisionOverride(candle, btcCandle, { ethCandle }));
5466
+ strategy.__tradejsUpdateReferenceData = (params) => indicatorsState.updateReferenceData?.(params);
5467
+ strategy.__tradejsFlushBacktestDelayedEntry = flushPendingBacktestEntry;
5468
+ const resolvedDetectorKey = detectorKey?.(config);
5469
+ if (resolvedDetectorKey && detectorNoSignalSkipReason) {
5470
+ const canFastAdvanceDetectorNoSignal = env === "BACKTEST" && getProjectHookList("onBar").length === 0 && getProjectHookList("afterCoreDecision").length === 0 && getProjectHookList("afterBarDecision").length === 0 && getProjectHookList("onSkip").length === 0 && !strategyManifest?.hooks?.onBar && !strategyManifest?.hooks?.afterCoreDecision && !strategyManifest?.hooks?.afterBarDecision && !strategyManifest?.hooks?.onSkip;
5471
+ strategy.detectorFanoutKey = [strategyName, resolvedDetectorKey].join(
5472
+ ":"
5473
+ );
5474
+ strategy.detectorNoSignalSkipReason = detectorNoSignalSkipReason;
5475
+ strategy.canFastAdvanceDetectorNoSignal = canFastAdvanceDetectorNoSignal;
5476
+ if (canFastAdvanceDetectorNoSignal) {
5477
+ strategy.advanceDetectorNoSignal = (candle, btcCandle, code) => {
5478
+ appendCurrentMarketData(candle, btcCandle);
5479
+ indicatorsState.setCurrentBar(
5480
+ candle,
5481
+ btcCandle,
5482
+ resolveEthCandle(candle)
5483
+ );
5484
+ return Promise.resolve(code);
5485
+ };
5486
+ }
5487
+ strategy.skipDetectorNoSignal = (candle, btcCandle, code) => runWithDecisionOverride(candle, btcCandle, {
5488
+ coreDecisionOverride: strategyApi.skip(code)
5489
+ });
5490
+ }
5491
+ return strategy;
5492
+ };
5493
+ if (detectorKey) {
5494
+ creator.detectorKey = detectorKey;
5495
+ }
5496
+ if (detectorNoSignalSkipReason) {
5497
+ creator.detectorNoSignalSkipReason = detectorNoSignalSkipReason;
5498
+ }
5499
+ return creator;
5500
+ };
5501
+
5502
+ // src/strategy/manifests.ts
5503
+ var createStrategyRegistryState = () => ({
5504
+ strategyCreators: /* @__PURE__ */ new Map(),
5505
+ strategyManifestsMap: /* @__PURE__ */ new Map(),
5506
+ pluginsLoadPromise: null
5507
+ });
5508
+ var registryStateByProjectRoot = /* @__PURE__ */ new Map();
5509
+ var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
5510
+ const projectRoot = getTradejsProjectCwd(cwd);
5511
+ let state = registryStateByProjectRoot.get(projectRoot);
5512
+ if (!state) {
5513
+ state = createStrategyRegistryState();
5514
+ registryStateByProjectRoot.set(projectRoot, state);
5515
+ }
5516
+ return {
5517
+ projectRoot,
5518
+ state
5519
+ };
5520
+ };
5521
+ var toUniqueModules = (modules = []) => [
5522
+ ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
5523
+ ];
5524
+ var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
5525
+ const config = await loadTradejsConfig(cwd);
5526
+ return {
5527
+ strategyModules: toUniqueModules(config.strategies),
5528
+ indicatorModules: toUniqueModules(config.indicators)
5529
+ };
5530
+ };
5531
+ var extractModuleEntries = (moduleExport, key) => {
5532
+ if (!moduleExport || typeof moduleExport !== "object") {
5533
+ return null;
5534
+ }
5535
+ const candidate = moduleExport;
5536
+ if (Array.isArray(candidate[key])) {
5537
+ return candidate[key];
5538
+ }
5539
+ const defaultExport = candidate.default;
5540
+ if (defaultExport && Array.isArray(defaultExport[key])) {
5541
+ return defaultExport[key];
5542
+ }
5543
+ return null;
5544
+ };
5545
+ var extractStrategyPluginDefinition = (moduleExport) => {
5546
+ const strategyEntries = extractModuleEntries(
5547
+ moduleExport,
5548
+ "strategyEntries"
5549
+ );
5550
+ return strategyEntries ? { strategyEntries } : null;
5551
+ };
5552
+ var extractIndicatorPluginDefinition = (moduleExport) => {
5553
+ const indicatorEntries = extractModuleEntries(
5554
+ moduleExport,
5555
+ "indicatorEntries"
5556
+ );
5557
+ return indicatorEntries ? { indicatorEntries } : null;
5558
+ };
5559
+ var registerEntries = (entries, source, state) => {
5560
+ for (const entry of entries) {
5561
+ const strategyName = entry.manifest?.name;
5562
+ if (!strategyName) {
5563
+ import_logger11.logger.warn("Skip strategy entry without name from %s", source);
5564
+ continue;
5565
+ }
5566
+ if (state.strategyCreators.has(strategyName)) {
5567
+ import_logger11.logger.warn(
5568
+ 'Skip duplicate strategy "%s" from %s: already registered',
5569
+ strategyName,
5570
+ source
5571
+ );
5572
+ continue;
5573
+ }
5574
+ state.strategyManifestsMap.set(strategyName, entry.manifest);
5575
+ state.strategyCreators.set(
5576
+ strategyName,
5577
+ createStrategyRuntime({
5578
+ strategyName,
5579
+ defaults: entry.defaults,
5580
+ createCore: entry.createCore,
5581
+ manifest: entry.manifest,
5582
+ detectorKey: entry.detectorKey,
5583
+ detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
5584
+ resolveRegisteredManifest: (name) => state.strategyManifestsMap.get(name)
5585
+ })
5586
+ );
5587
+ }
5588
+ };
5589
+ var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
5590
+ if (typeof importTradejsModule === "function") {
5591
+ return importTradejsModule(moduleName, cwd);
5592
+ }
5593
+ return import(
5594
+ /* webpackIgnore: true */
5595
+ moduleName
5596
+ );
5597
+ };
5598
+ var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
5599
+ const { projectRoot, state } = getStrategyRegistryState(cwd);
5600
+ if (!state.pluginsLoadPromise) {
5601
+ (0, import_indicators2.resetIndicatorRegistryCache)(projectRoot);
5602
+ state.pluginsLoadPromise = (async () => {
5603
+ const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
5604
+ const strategySet = new Set(strategyModules);
5605
+ const indicatorSet = new Set(indicatorModules);
5606
+ const pluginModuleNames = [
5607
+ .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
5608
+ ];
5609
+ if (!pluginModuleNames.length) {
5610
+ return;
5611
+ }
5612
+ for (const moduleName of pluginModuleNames) {
5613
+ try {
5614
+ const resolvedModuleName = resolvePluginModuleSpecifier(
5615
+ moduleName,
5616
+ projectRoot
5617
+ );
5618
+ const moduleExport = await importStrategyPluginModule(
5619
+ resolvedModuleName,
5620
+ projectRoot
5621
+ );
5622
+ if (strategySet.has(moduleName)) {
5623
+ const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
5624
+ if (!pluginDefinition) {
5625
+ import_logger11.logger.warn(
5626
+ 'Skip strategy plugin "%s": export { strategyEntries } is missing',
5627
+ moduleName
5628
+ );
5629
+ } else {
5630
+ registerEntries(
5631
+ pluginDefinition.strategyEntries,
5632
+ moduleName,
5633
+ state
5634
+ );
5635
+ }
5636
+ }
5637
+ if (indicatorSet.has(moduleName)) {
5638
+ const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
5639
+ if (!indicatorPluginDefinition) {
5640
+ import_logger11.logger.warn(
5641
+ 'Skip indicator plugin "%s": export { indicatorEntries } is missing',
5642
+ moduleName
5643
+ );
5644
+ } else {
5645
+ (0, import_indicators2.registerIndicatorEntries)(
5646
+ indicatorPluginDefinition.indicatorEntries,
5647
+ moduleName,
5648
+ projectRoot
5649
+ );
5650
+ }
5651
+ }
5652
+ if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
5653
+ import_logger11.logger.warn(
5654
+ 'Skip plugin "%s": no strategy/indicator sections requested in config',
5655
+ moduleName
5656
+ );
5657
+ }
5658
+ } catch (error) {
5659
+ import_logger11.logger.warn(
5660
+ 'Failed to load plugin "%s": %s',
5661
+ moduleName,
5662
+ String(error)
5663
+ );
5664
+ }
5665
+ }
5666
+ })();
5667
+ }
5668
+ await state.pluginsLoadPromise;
5669
+ };
5670
+ var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
5671
+ if (!name) {
5672
+ return void 0;
5673
+ }
5674
+ const { state } = getStrategyRegistryState(cwd);
5675
+ return state.strategyManifestsMap.get(name);
5676
+ };
5677
+ var strategies = new Proxy(
5678
+ {},
5679
+ {
5680
+ get: (_target, property) => {
5681
+ if (typeof property !== "string") {
5682
+ return void 0;
5683
+ }
5684
+ return getStrategyRegistryState().state.strategyCreators.get(property);
5685
+ },
5686
+ ownKeys: () => {
5687
+ return [...getStrategyRegistryState().state.strategyCreators.keys()];
5688
+ },
5689
+ getOwnPropertyDescriptor: () => ({
5690
+ enumerable: true,
5691
+ configurable: true
5692
+ })
5693
+ }
5694
+ );
1100
5695
 
1101
5696
  // src/strategyAdapters/ai.ts
1102
5697
  var toRecord2 = (value) => {
@@ -1614,7 +6209,7 @@ var askAI = async (signal, options = {}) => {
1614
6209
  payload
1615
6210
  }
1616
6211
  );
1617
- await (0, import_redis.setData)(import_redis.redisKeys.analysis(symbol, signal.signalId), content);
6212
+ await (0, import_redis2.setData)(import_redis2.redisKeys.analysis(symbol, signal.signalId), content);
1618
6213
  return content;
1619
6214
  };
1620
6215
  // Annotate the CommonJS export names for ESM import in node: