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

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.
@@ -843,869 +843,881 @@ __reExport(strategies_exports, require("@tradejs/core/strategies"), module.expor
843
843
  var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
844
844
  var import_redis = require("@tradejs/infra/redis");
845
845
 
846
- // src/aiShared.ts
847
- var MAX_AI_SERIES_POINTS = 5;
848
- var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
849
- "tradejs.indicators.compactSnapshot"
850
- );
851
- var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
852
- var trimSeriesDeep = (value) => {
853
- if (Array.isArray(value)) {
854
- const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
855
- const isMatrix = trimmed.every((item) => Array.isArray(item));
856
- if (isMatrix) {
857
- return trimmed;
858
- }
859
- return trimmed.map(
860
- (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
861
- );
846
+ // src/strategyAdapters/ai.ts
847
+ var import_indicators2 = require("@tradejs/core/indicators");
848
+
849
+ // src/tradejsConfig.ts
850
+ var import_fs = __toESM(require("fs"));
851
+ var import_path = __toESM(require("path"));
852
+ var import_url = require("url");
853
+ var import_config = require("@tradejs/core/config");
854
+ var import_logger = require("@tradejs/infra/logger");
855
+ var CONFIG_FILE_NAMES = [
856
+ "tradejs.config.ts",
857
+ "tradejs.config.mts",
858
+ "tradejs.config.js",
859
+ "tradejs.config.mjs",
860
+ "tradejs.config.cjs"
861
+ ];
862
+ var TS_MODULE_RE = /\.(cts|mts|ts)$/i;
863
+ var cachedByCwd = /* @__PURE__ */ new Map();
864
+ var announcedConfigFile = /* @__PURE__ */ new Set();
865
+ var tsNodeRegistered = false;
866
+ var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
867
+ var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
868
+ var getTradejsProjectCwd = (cwd) => {
869
+ const explicit = String(cwd ?? "").trim();
870
+ if (explicit) {
871
+ return import_path.default.resolve(explicit);
862
872
  }
863
- if (value && typeof value === "object") {
864
- return Object.fromEntries(
865
- Object.entries(value).map(([key, nested]) => [
866
- key,
867
- trimSeriesDeep(nested)
868
- ])
869
- );
873
+ const fromEnv = String(process.env.PROJECT_CWD || "").trim();
874
+ if (fromEnv) {
875
+ return import_path.default.resolve(fromEnv);
870
876
  }
871
- return value;
877
+ return process.cwd();
872
878
  };
873
- var buildCompactAiIndicatorsSnapshot = (value) => {
874
- const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
875
- if (typeof compactSnapshot === "function") {
876
- return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
879
+ var normalizeConfig = (rawConfig) => {
880
+ if (!rawConfig || typeof rawConfig !== "object") {
881
+ return {};
877
882
  }
878
- return trimSeriesDeep(value);
883
+ const config = rawConfig;
884
+ const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
885
+ const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
886
+ const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
887
+ const hooks = (0, import_config.normalizeTradejsConfigHooks)(
888
+ config.hooks
889
+ );
890
+ const runtime = config.runtime && typeof config.runtime === "object" && !Array.isArray(config.runtime) ? config.runtime : void 0;
891
+ return {
892
+ strategies: strategies2,
893
+ indicators,
894
+ connectors,
895
+ ...hooks ? { hooks } : {},
896
+ ...runtime ? { runtime } : {}
897
+ };
879
898
  };
880
-
881
- // src/aiMarketContext.ts
882
- var toRecord = (value) => {
883
- if (!value || typeof value !== "object" || Array.isArray(value)) {
884
- return null;
899
+ var getNodeCreateRequire = () => {
900
+ const builtinModule = process.getBuiltinModule?.("module");
901
+ if (typeof builtinModule?.createRequire === "function") {
902
+ return builtinModule.createRequire;
885
903
  }
886
- return value;
904
+ throw new TypeError("module.createRequire is not available");
887
905
  };
888
- var toFiniteNumber = (value) => {
889
- const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
890
- return Number.isFinite(numeric) ? numeric : null;
906
+ var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
907
+ var ensureTsNodeRegistered = async () => {
908
+ if (tsNodeRegistered) {
909
+ return;
910
+ }
911
+ const tsNodeModule = await import("ts-node");
912
+ const tsNode = tsNodeModule.default ?? tsNodeModule;
913
+ tsNode.register?.({
914
+ transpileOnly: true,
915
+ compilerOptions: {
916
+ module: "Node16",
917
+ moduleResolution: "node16"
918
+ }
919
+ });
920
+ tsNodeRegistered = true;
891
921
  };
892
- var roundTo = (value, decimals) => {
893
- const factor = 10 ** decimals;
894
- return Math.round(value * factor) / factor;
922
+ var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
923
+ const projectRoot = getTradejsProjectCwd(cwd);
924
+ if (tsconfigPathsRegisteredByCwd.has(projectRoot)) {
925
+ return;
926
+ }
927
+ const tsconfigPathsModule = await import("tsconfig-paths");
928
+ const loadConfig = tsconfigPathsModule.loadConfig;
929
+ const register = tsconfigPathsModule.register;
930
+ if (typeof loadConfig !== "function" || typeof register !== "function") {
931
+ return;
932
+ }
933
+ const loadedConfig = loadConfig(projectRoot);
934
+ if (loadedConfig.resultType !== "success") {
935
+ return;
936
+ }
937
+ register({
938
+ baseUrl: loadedConfig.absoluteBaseUrl,
939
+ paths: loadedConfig.paths,
940
+ addMatchAll: false
941
+ });
942
+ tsconfigPathsRegisteredByCwd.add(projectRoot);
895
943
  };
896
- var buildMissingSpreadContext = () => ({
897
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
898
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
899
- available: false,
900
- value: null,
901
- zScore: null,
902
- bps: null,
903
- absBps: null,
904
- bias: null,
905
- severity: null
906
- });
907
- var buildSpreadContextFromSignal = (signal) => {
908
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
909
- const relative = toRecord(baseContext?.relative);
910
- const execution = toRecord(relative?.execution);
911
- const spread = toFiniteNumber(execution?.venueSpread);
912
- const zScore = toFiniteNumber(execution?.venueSpreadZScore);
913
- if (spread == null) {
914
- return buildMissingSpreadContext();
944
+ var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
945
+ const projectRoot = getTradejsProjectCwd(cwd);
946
+ const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
947
+ if (cachedMatcher) {
948
+ const resolved2 = cachedMatcher(moduleName);
949
+ return resolved2 || null;
915
950
  }
916
- const value = roundTo(spread, 8);
917
- const bps = roundTo(value * 1e4, 2);
918
- const absBps = Math.abs(bps);
919
- const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
920
- const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
921
- return {
922
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
923
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
924
- available: true,
925
- value,
926
- zScore,
927
- bps,
928
- absBps,
929
- bias,
930
- severity
931
- };
951
+ const tsconfigPathsModule = await import("tsconfig-paths");
952
+ const loadConfig = tsconfigPathsModule.loadConfig;
953
+ const createMatchPath = tsconfigPathsModule.createMatchPath;
954
+ if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
955
+ return null;
956
+ }
957
+ const loadedConfig = loadConfig(projectRoot);
958
+ if (loadedConfig.resultType !== "success") {
959
+ return null;
960
+ }
961
+ const matchPath = createMatchPath(
962
+ loadedConfig.absoluteBaseUrl,
963
+ loadedConfig.paths
964
+ );
965
+ const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
966
+ ".ts",
967
+ ".tsx",
968
+ ".mts",
969
+ ".cts",
970
+ ".js",
971
+ ".jsx",
972
+ ".mjs",
973
+ ".cjs",
974
+ ".json"
975
+ ]) || "";
976
+ tsconfigPathMatchersByCwd.set(projectRoot, matcher);
977
+ const resolved = matcher(moduleName);
978
+ return resolved || null;
932
979
  };
933
- var buildTrueDeltaContextFromSignal = (signal) => {
934
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
935
- const participation = toRecord(baseContext?.participation);
936
- const delta = toRecord(participation?.delta);
937
- const source = String(delta?.source ?? "");
938
- const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
939
- if (!delta || !isTrueDeltaSource) {
940
- return {
941
- source: source || null,
942
- available: false,
943
- buyPressurePct: null,
944
- buyVolume: null,
945
- sellVolume: null,
946
- netDelta: null,
947
- deltaPct: null,
948
- signedVolumeZScore: null
949
- };
980
+ var toImportSpecifier = (moduleName) => {
981
+ if (moduleName.startsWith("file://")) {
982
+ return moduleName;
950
983
  }
951
- return {
952
- source,
953
- available: true,
954
- buyPressurePct: toFiniteNumber(delta.buyPressurePct),
955
- buyVolume: toFiniteNumber(delta.buyVolume),
956
- sellVolume: toFiniteNumber(delta.sellVolume),
957
- netDelta: toFiniteNumber(delta.netDelta),
958
- deltaPct: toFiniteNumber(delta.deltaPct),
959
- signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
960
- };
984
+ if (import_path.default.isAbsolute(moduleName)) {
985
+ return (0, import_url.pathToFileURL)(moduleName).href;
986
+ }
987
+ return moduleName;
961
988
  };
962
- var buildTradeFlowContextFromSignal = (signal) => {
963
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
964
- const participation = toRecord(baseContext?.participation);
965
- const tradeFlow = toRecord(participation?.tradeFlow);
966
- if (!tradeFlow) {
967
- return {
968
- source: null,
969
- available: false,
970
- interval: null,
971
- stale: null,
972
- trades: null,
973
- buyPressurePct: null,
974
- netBaseDelta: null,
975
- netQuoteDelta: null
976
- };
989
+ var isTsModulePath = (moduleName) => TS_MODULE_RE.test(moduleName.split("?")[0]);
990
+ var isRelativeModulePath = (moduleName) => moduleName.startsWith("./") || moduleName.startsWith("../");
991
+ var isBareModuleSpecifier = (moduleName) => {
992
+ const normalized = String(moduleName ?? "").trim();
993
+ if (!normalized) {
994
+ return false;
977
995
  }
978
- return {
979
- source: String(tradeFlow.source ?? ""),
980
- available: true,
981
- interval: String(tradeFlow.interval ?? ""),
982
- stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
983
- trades: toFiniteNumber(tradeFlow.trades),
984
- buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
985
- netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
986
- netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
987
- };
996
+ if (normalized.startsWith("file://") || import_path.default.isAbsolute(normalized) || isRelativeModulePath(normalized)) {
997
+ return false;
998
+ }
999
+ return true;
988
1000
  };
989
- var buildMarketBreadthContextFromSignal = (signal) => {
990
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
991
- const relative = toRecord(baseContext?.relative);
992
- const breadth = toRecord(relative?.marketBreadth);
993
- if (!breadth) {
994
- return {
995
- source: null,
996
- available: false,
997
- universe: null,
998
- interval: null,
999
- stale: null,
1000
- symbolsCount: null,
1001
- advanceDeclineRatio: null,
1002
- pctAboveMa20: null,
1003
- pctAboveMa50: null,
1004
- equalWeightedReturn: null,
1005
- volumeWeightedReturn: null,
1006
- dispersion: null
1007
- };
1001
+ var importConfigFile = async (configFilePath) => {
1002
+ const ext = import_path.default.extname(configFilePath).toLowerCase();
1003
+ const configFileUrl = `${toImportSpecifier(configFilePath)}?t=${Date.now()}`;
1004
+ if (ext === ".ts" || ext === ".mts") {
1005
+ const requireFn = getRequireFn(import_path.default.dirname(configFilePath));
1006
+ await ensureTsNodeRegistered();
1007
+ await ensureTsconfigPathsRegistered(import_path.default.dirname(configFilePath));
1008
+ return requireFn(configFilePath);
1008
1009
  }
1009
- return {
1010
- source: String(breadth.source ?? ""),
1011
- available: true,
1012
- universe: String(breadth.universe ?? ""),
1013
- interval: String(breadth.interval ?? ""),
1014
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
1015
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
1016
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
1017
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
1018
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
1019
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
1020
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
1021
- dispersion: toFiniteNumber(breadth.dispersion)
1022
- };
1010
+ return import(
1011
+ /* webpackIgnore: true */
1012
+ configFileUrl
1013
+ );
1023
1014
  };
1024
- var buildMarketBreadthsContextFromSignal = (signal) => {
1025
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1026
- const relative = toRecord(baseContext?.relative);
1027
- const breadths = toRecord(relative?.marketBreadths);
1028
- const build = (key) => {
1029
- const breadth = toRecord(breadths?.[key]);
1030
- if (!breadth) {
1031
- return {
1032
- source: null,
1033
- available: false,
1034
- universe: null,
1035
- interval: null,
1036
- stale: null,
1037
- symbolsCount: null,
1038
- advanceDeclineRatio: null,
1039
- pctAboveMa20: null,
1040
- pctAboveMa50: null,
1041
- equalWeightedReturn: null,
1042
- volumeWeightedReturn: null,
1043
- dispersion: null
1044
- };
1015
+ var importTradejsModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1016
+ const normalized = String(moduleName ?? "").trim();
1017
+ if (!normalized) {
1018
+ return {};
1019
+ }
1020
+ let modulePath = normalized;
1021
+ if (normalized.startsWith("file://")) {
1022
+ try {
1023
+ modulePath = (0, import_url.fileURLToPath)(normalized);
1024
+ } catch {
1025
+ modulePath = normalized;
1045
1026
  }
1046
- return {
1047
- source: String(breadth.source ?? ""),
1048
- available: true,
1049
- universe: String(breadth.universe ?? ""),
1050
- interval: String(breadth.interval ?? ""),
1051
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
1052
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
1053
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
1054
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
1055
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
1056
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
1057
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
1058
- dispersion: toFiniteNumber(breadth.dispersion)
1059
- };
1060
- };
1061
- return {
1062
- top5: build("top5"),
1063
- top10: build("top10"),
1064
- top30: build("top30"),
1065
- top50: build("top50"),
1066
- top100: build("top100")
1067
- };
1068
- };
1069
- var buildTargetVsBtcContextFromSignal = (signal) => {
1070
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1071
- const relative = toRecord(baseContext?.relative);
1072
- const targetVsBtc = toRecord(relative?.targetVsBtc);
1073
- if (!targetVsBtc) {
1074
- return {
1075
- source: null,
1076
- available: false,
1077
- ratioReturn1h: null,
1078
- ratioReturn4h: null,
1079
- ratioReturn24h: null,
1080
- alphaVsBtc1h: null,
1081
- alphaVsBtc4h: null,
1082
- alphaVsBtc24h: null,
1083
- betaToBtc20: null,
1084
- correlationToBtc20: null,
1085
- ratioTrend: null
1086
- };
1087
1027
  }
1088
- return {
1089
- source: String(targetVsBtc.source ?? ""),
1090
- available: true,
1091
- ratioReturn1h: toFiniteNumber(targetVsBtc.ratioReturn1h),
1092
- ratioReturn4h: toFiniteNumber(targetVsBtc.ratioReturn4h),
1093
- ratioReturn24h: toFiniteNumber(targetVsBtc.ratioReturn24h),
1094
- alphaVsBtc1h: toFiniteNumber(targetVsBtc.alphaVsBtc1h),
1095
- alphaVsBtc4h: toFiniteNumber(targetVsBtc.alphaVsBtc4h),
1096
- alphaVsBtc24h: toFiniteNumber(targetVsBtc.alphaVsBtc24h),
1097
- betaToBtc20: toFiniteNumber(targetVsBtc.betaToBtc20),
1098
- correlationToBtc20: toFiniteNumber(targetVsBtc.correlationToBtc20),
1099
- ratioTrend: typeof targetVsBtc.ratioTrend === "string" ? targetVsBtc.ratioTrend : null
1100
- };
1028
+ const requireFn = getRequireFn(
1029
+ import_path.default.isAbsolute(modulePath) ? import_path.default.dirname(modulePath) : cwd
1030
+ );
1031
+ if (isTsModulePath(modulePath)) {
1032
+ await ensureTsNodeRegistered();
1033
+ await ensureTsconfigPathsRegistered(cwd);
1034
+ return requireFn(modulePath);
1035
+ }
1036
+ if (isBareModuleSpecifier(normalized)) {
1037
+ await ensureTsconfigPathsRegistered(cwd);
1038
+ try {
1039
+ return requireFn(normalized);
1040
+ } catch (error) {
1041
+ const resolvedByTsconfig = await resolveTsconfigPathModule(
1042
+ normalized,
1043
+ cwd
1044
+ );
1045
+ if (resolvedByTsconfig && resolvedByTsconfig !== normalized) {
1046
+ return requireFn(resolvedByTsconfig);
1047
+ }
1048
+ throw error;
1049
+ }
1050
+ }
1051
+ try {
1052
+ return await import(
1053
+ /* webpackIgnore: true */
1054
+ toImportSpecifier(normalized)
1055
+ );
1056
+ } catch (error) {
1057
+ if (isTsModulePath(modulePath)) {
1058
+ await ensureTsNodeRegistered();
1059
+ await ensureTsconfigPathsRegistered(cwd);
1060
+ return requireFn(modulePath);
1061
+ }
1062
+ throw error;
1063
+ }
1101
1064
  };
1102
- var buildBtcAltRegimeContextFromSignal = (signal) => {
1103
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1104
- const relative = toRecord(baseContext?.relative);
1105
- const btcAltRegime = toRecord(relative?.btcAltRegime);
1106
- if (!btcAltRegime) {
1107
- return {
1108
- source: null,
1109
- available: false,
1110
- universe: null,
1111
- interval: null,
1112
- stale: null,
1113
- regime: null,
1114
- btcReturn24h: null,
1115
- altBasketReturn24h: null,
1116
- btcVsAltReturn24h: null,
1117
- btcTurnoverShare24h: null,
1118
- btcTurnoverShareChange24h: null,
1119
- altVolToBtcVol24h: null,
1120
- altDispersion24h: null
1121
- };
1065
+ var resolveExportedConfig = (moduleExports) => {
1066
+ const candidate = moduleExports && typeof moduleExports === "object" && "default" in moduleExports ? moduleExports.default : moduleExports;
1067
+ return normalizeConfig(candidate);
1068
+ };
1069
+ var findConfigFilePath = (cwd) => {
1070
+ let currentDir = import_path.default.resolve(cwd);
1071
+ while (true) {
1072
+ for (const fileName of CONFIG_FILE_NAMES) {
1073
+ const fullPath = import_path.default.join(currentDir, fileName);
1074
+ if (import_fs.default.existsSync(fullPath) && import_fs.default.statSync(fullPath).isFile()) {
1075
+ return fullPath;
1076
+ }
1077
+ }
1078
+ const parentDir = import_path.default.dirname(currentDir);
1079
+ if (parentDir === currentDir) {
1080
+ return null;
1081
+ }
1082
+ currentDir = parentDir;
1122
1083
  }
1123
- return {
1124
- source: String(btcAltRegime.source ?? ""),
1125
- available: true,
1126
- universe: String(btcAltRegime.universe ?? ""),
1127
- interval: String(btcAltRegime.interval ?? ""),
1128
- stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
1129
- regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
1130
- btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
1131
- altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
1132
- btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
1133
- btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
1134
- btcTurnoverShareChange24h: toFiniteNumber(
1135
- btcAltRegime.btcTurnoverShareChange24h
1136
- ),
1137
- altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
1138
- altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
1139
- };
1140
1084
  };
1141
- var buildCmcGlobalContextFromSignal = (signal) => {
1142
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1143
- const relative = toRecord(baseContext?.relative);
1144
- const cmcGlobal = toRecord(relative?.cmcGlobal);
1145
- if (!cmcGlobal) {
1146
- return {
1147
- source: null,
1148
- available: false,
1149
- interval: null,
1150
- asOfTs: null,
1151
- stale: null,
1152
- totalMarketCapUsd: null,
1153
- totalVolumeUsd: null,
1154
- totalVolumeReportedUsd: null,
1155
- altMarketCapUsd: null,
1156
- altVolumeUsd: null,
1157
- altVolumeReportedUsd: null,
1158
- btcDominancePct: null,
1159
- ethDominancePct: null,
1160
- btcDominanceChange24hPct: null,
1161
- ethDominanceChange24hPct: null,
1162
- altMarketCapChange24hPct: null,
1163
- altVolumeChange24hPct: null,
1164
- activeCryptocurrencies: null,
1165
- activeExchanges: null,
1166
- activeMarketPairs: null,
1167
- altLiquidityRegime: null
1168
- };
1085
+ var resolvePluginModuleSpecifier = (moduleName, cwd = getTradejsProjectCwd()) => {
1086
+ const normalized = String(moduleName ?? "").trim();
1087
+ if (!normalized) {
1088
+ return "";
1169
1089
  }
1170
- return {
1171
- source: String(cmcGlobal.source ?? ""),
1172
- available: true,
1173
- interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
1174
- asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
1175
- stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
1176
- totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
1177
- totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
1178
- totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
1179
- altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
1180
- altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
1181
- altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
1182
- btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
1183
- ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
1184
- btcDominanceChange24hPct: toFiniteNumber(
1185
- cmcGlobal.btcDominanceChange24hPct
1186
- ),
1187
- ethDominanceChange24hPct: toFiniteNumber(
1188
- cmcGlobal.ethDominanceChange24hPct
1189
- ),
1190
- altMarketCapChange24hPct: toFiniteNumber(
1191
- cmcGlobal.altMarketCapChange24hPct
1192
- ),
1193
- altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
1194
- activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
1195
- activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
1196
- activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
1197
- altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
1198
- };
1090
+ if (normalized.startsWith("file://")) {
1091
+ try {
1092
+ return (0, import_url.fileURLToPath)(normalized);
1093
+ } catch {
1094
+ return normalized;
1095
+ }
1096
+ }
1097
+ if (import_path.default.isAbsolute(normalized)) {
1098
+ return normalized;
1099
+ }
1100
+ if (isRelativeModulePath(normalized)) {
1101
+ return import_path.default.resolve(cwd, normalized);
1102
+ }
1103
+ return normalized;
1199
1104
  };
1200
- var buildCmcReferenceAssetsContextFromSignal = (signal) => {
1105
+ var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
1106
+ const cached = cachedByCwd.get(cwd);
1107
+ if (cached) {
1108
+ return cached;
1109
+ }
1110
+ const configFilePath = findConfigFilePath(cwd);
1111
+ if (!configFilePath) {
1112
+ cachedByCwd.set(cwd, {});
1113
+ return {};
1114
+ }
1115
+ try {
1116
+ const moduleExports = await importConfigFile(configFilePath);
1117
+ const config = resolveExportedConfig(moduleExports);
1118
+ cachedByCwd.set(cwd, config);
1119
+ if (!announcedConfigFile.has(configFilePath)) {
1120
+ announcedConfigFile.add(configFilePath);
1121
+ import_logger.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
1122
+ }
1123
+ return config;
1124
+ } catch (error) {
1125
+ import_logger.logger.log(
1126
+ "warn",
1127
+ "Failed to load TradeJS config from %s: %s",
1128
+ configFilePath,
1129
+ String(error)
1130
+ );
1131
+ cachedByCwd.set(cwd, {});
1132
+ return {};
1133
+ }
1134
+ };
1135
+
1136
+ // src/strategyAdapters/ai.ts
1137
+ var import_aiFeatures = require("@tradejs/core/aiFeatures");
1138
+
1139
+ // src/aiShared.ts
1140
+ var MAX_AI_SERIES_POINTS = 5;
1141
+ var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
1142
+ "tradejs.indicators.compactSnapshot"
1143
+ );
1144
+ var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
1145
+ var trimSeriesDeep = (value) => {
1146
+ if (Array.isArray(value)) {
1147
+ const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
1148
+ const isMatrix = trimmed.every((item) => Array.isArray(item));
1149
+ if (isMatrix) {
1150
+ return trimmed;
1151
+ }
1152
+ return trimmed.map(
1153
+ (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
1154
+ );
1155
+ }
1156
+ if (value && typeof value === "object") {
1157
+ return Object.fromEntries(
1158
+ Object.entries(value).map(([key, nested]) => [
1159
+ key,
1160
+ trimSeriesDeep(nested)
1161
+ ])
1162
+ );
1163
+ }
1164
+ return value;
1165
+ };
1166
+ var buildCompactAiIndicatorsSnapshot = (value) => {
1167
+ const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
1168
+ if (typeof compactSnapshot === "function") {
1169
+ return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
1170
+ }
1171
+ return trimSeriesDeep(value);
1172
+ };
1173
+
1174
+ // src/aiMarketContext.ts
1175
+ var toRecord = (value) => {
1176
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1177
+ return null;
1178
+ }
1179
+ return value;
1180
+ };
1181
+ var toFiniteNumber = (value) => {
1182
+ const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
1183
+ return Number.isFinite(numeric) ? numeric : null;
1184
+ };
1185
+ var roundTo = (value, decimals) => {
1186
+ const factor = 10 ** decimals;
1187
+ return Math.round(value * factor) / factor;
1188
+ };
1189
+ var buildMissingSpreadContext = () => ({
1190
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1191
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1192
+ available: false,
1193
+ value: null,
1194
+ zScore: null,
1195
+ bps: null,
1196
+ absBps: null,
1197
+ bias: null,
1198
+ severity: null
1199
+ });
1200
+ var buildSpreadContextFromSignal = (signal) => {
1201
1201
  const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1202
1202
  const relative = toRecord(baseContext?.relative);
1203
- const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
1204
- if (!cmcReferenceAssets) {
1205
- return {
1206
- source: null,
1207
- available: false,
1208
- interval: null,
1209
- asOfTs: null,
1210
- stale: null,
1211
- btcMarketCapUsd: null,
1212
- ethMarketCapUsd: null,
1213
- btcVolumeUsd: null,
1214
- ethVolumeUsd: null,
1215
- btcVolumeToMarketCap: null,
1216
- ethVolumeToMarketCap: null,
1217
- ethBtcMarketCapRatio: null,
1218
- ethBtcMarketCapRatioChange24hPct: null,
1219
- ethVsBtcVolumeRatio: null,
1220
- referenceLiquidityRegime: null
1221
- };
1203
+ const execution = toRecord(relative?.execution);
1204
+ const spread = toFiniteNumber(execution?.venueSpread);
1205
+ const zScore = toFiniteNumber(execution?.venueSpreadZScore);
1206
+ if (spread == null) {
1207
+ return buildMissingSpreadContext();
1222
1208
  }
1209
+ const value = roundTo(spread, 8);
1210
+ const bps = roundTo(value * 1e4, 2);
1211
+ const absBps = Math.abs(bps);
1212
+ const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
1213
+ const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
1223
1214
  return {
1224
- source: String(cmcReferenceAssets.source ?? ""),
1215
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1216
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1225
1217
  available: true,
1226
- interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
1227
- asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
1228
- stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
1229
- btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
1230
- ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
1231
- btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
1232
- ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
1233
- btcVolumeToMarketCap: toFiniteNumber(
1234
- cmcReferenceAssets.btcVolumeToMarketCap
1235
- ),
1236
- ethVolumeToMarketCap: toFiniteNumber(
1237
- cmcReferenceAssets.ethVolumeToMarketCap
1238
- ),
1239
- ethBtcMarketCapRatio: toFiniteNumber(
1240
- cmcReferenceAssets.ethBtcMarketCapRatio
1241
- ),
1242
- ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
1243
- cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
1244
- ),
1245
- ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
1246
- referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
1218
+ value,
1219
+ zScore,
1220
+ bps,
1221
+ absBps,
1222
+ bias,
1223
+ severity
1247
1224
  };
1248
1225
  };
1249
- var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
1226
+ var buildTrueDeltaContextFromSignal = (signal) => {
1250
1227
  const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1251
- const relative = toRecord(baseContext?.relative);
1252
- const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
1253
- if (!cmcExchangeLiquidity) {
1228
+ const participation = toRecord(baseContext?.participation);
1229
+ const delta = toRecord(participation?.delta);
1230
+ const source = String(delta?.source ?? "");
1231
+ const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
1232
+ if (!delta || !isTrueDeltaSource) {
1254
1233
  return {
1255
- source: null,
1234
+ source: source || null,
1256
1235
  available: false,
1257
- interval: null,
1258
- asOfTs: null,
1259
- stale: null,
1260
- exchangesCount: null,
1261
- totalVolumeUsd: null,
1262
- totalVolumeChange24hPct: null,
1263
- binanceVolumeUsd: null,
1264
- binanceVolumeShare: null,
1265
- topExchangeVolumeShare: null,
1266
- liquidityRegime: null
1236
+ buyPressurePct: null,
1237
+ buyVolume: null,
1238
+ sellVolume: null,
1239
+ netDelta: null,
1240
+ deltaPct: null,
1241
+ signedVolumeZScore: null
1267
1242
  };
1268
1243
  }
1269
1244
  return {
1270
- source: String(cmcExchangeLiquidity.source ?? ""),
1245
+ source,
1271
1246
  available: true,
1272
- interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
1273
- asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
1274
- stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
1275
- exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
1276
- totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
1277
- totalVolumeChange24hPct: toFiniteNumber(
1278
- cmcExchangeLiquidity.totalVolumeChange24hPct
1279
- ),
1280
- binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
1281
- binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
1282
- topExchangeVolumeShare: toFiniteNumber(
1283
- cmcExchangeLiquidity.topExchangeVolumeShare
1284
- ),
1285
- liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
1247
+ buyPressurePct: toFiniteNumber(delta.buyPressurePct),
1248
+ buyVolume: toFiniteNumber(delta.buyVolume),
1249
+ sellVolume: toFiniteNumber(delta.sellVolume),
1250
+ netDelta: toFiniteNumber(delta.netDelta),
1251
+ deltaPct: toFiniteNumber(delta.deltaPct),
1252
+ signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
1286
1253
  };
1287
1254
  };
1288
- var buildCmcFearGreedContextFromSignal = (signal) => {
1255
+ var buildTradeFlowContextFromSignal = (signal) => {
1289
1256
  const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1290
- const relative = toRecord(baseContext?.relative);
1291
- const cmcFearGreed = toRecord(relative?.cmcFearGreed);
1292
- if (!cmcFearGreed) {
1257
+ const participation = toRecord(baseContext?.participation);
1258
+ const tradeFlow = toRecord(participation?.tradeFlow);
1259
+ if (!tradeFlow) {
1293
1260
  return {
1294
1261
  source: null,
1295
1262
  available: false,
1296
1263
  interval: null,
1297
- asOfTs: null,
1298
1264
  stale: null,
1299
- value: null,
1300
- valueChange24h: null,
1301
- valueChange7d: null,
1302
- classification: null,
1303
- sentimentRegime: null
1265
+ trades: null,
1266
+ buyPressurePct: null,
1267
+ netBaseDelta: null,
1268
+ netQuoteDelta: null
1304
1269
  };
1305
1270
  }
1306
1271
  return {
1307
- source: String(cmcFearGreed.source ?? ""),
1272
+ source: String(tradeFlow.source ?? ""),
1308
1273
  available: true,
1309
- interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
1310
- asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
1311
- stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
1312
- value: toFiniteNumber(cmcFearGreed.value),
1313
- valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
1314
- valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
1315
- classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
1316
- sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
1274
+ interval: String(tradeFlow.interval ?? ""),
1275
+ stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
1276
+ trades: toFiniteNumber(tradeFlow.trades),
1277
+ buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
1278
+ netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
1279
+ netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
1317
1280
  };
1318
1281
  };
1319
- var buildCmcIndexesContextFromSignal = (signal) => {
1282
+ var buildMarketBreadthContextFromSignal = (signal) => {
1320
1283
  const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1321
1284
  const relative = toRecord(baseContext?.relative);
1322
- const cmcIndexes = toRecord(relative?.cmcIndexes);
1323
- if (!cmcIndexes) {
1285
+ const breadth = toRecord(relative?.marketBreadth);
1286
+ if (!breadth) {
1324
1287
  return {
1325
1288
  source: null,
1326
1289
  available: false,
1290
+ universe: null,
1327
1291
  interval: null,
1328
- asOfTs: null,
1329
1292
  stale: null,
1330
- cmc100Value: null,
1331
- cmc100Change24hPct: null,
1332
- cmc100TopConstituentSymbol: null,
1333
- cmc100TopConstituentWeightPct: null,
1334
- cmc20Value: null,
1335
- cmc20Change24hPct: null,
1336
- cmc20TopConstituentSymbol: null,
1337
- cmc20TopConstituentWeightPct: null,
1338
- cmc20ToCmc100Ratio: null,
1339
- cmc20ToCmc100RatioChange24hPct: null,
1340
- indexRegime: null
1293
+ symbolsCount: null,
1294
+ advanceDeclineRatio: null,
1295
+ pctAboveMa20: null,
1296
+ pctAboveMa50: null,
1297
+ equalWeightedReturn: null,
1298
+ volumeWeightedReturn: null,
1299
+ dispersion: null
1341
1300
  };
1342
1301
  }
1343
1302
  return {
1344
- source: String(cmcIndexes.source ?? ""),
1303
+ source: String(breadth.source ?? ""),
1345
1304
  available: true,
1346
- interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
1347
- asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
1348
- stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
1349
- cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
1350
- cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
1351
- cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
1352
- cmc100TopConstituentWeightPct: toFiniteNumber(
1353
- cmcIndexes.cmc100TopConstituentWeightPct
1354
- ),
1355
- cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
1356
- cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
1357
- cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
1358
- cmc20TopConstituentWeightPct: toFiniteNumber(
1359
- cmcIndexes.cmc20TopConstituentWeightPct
1360
- ),
1361
- cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
1362
- cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
1363
- cmcIndexes.cmc20ToCmc100RatioChange24hPct
1364
- ),
1365
- indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
1305
+ universe: String(breadth.universe ?? ""),
1306
+ interval: String(breadth.interval ?? ""),
1307
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
1308
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
1309
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
1310
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
1311
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
1312
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
1313
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
1314
+ dispersion: toFiniteNumber(breadth.dispersion)
1366
1315
  };
1367
1316
  };
1368
- var buildReferenceTradeFlowContextFromSignal = (signal) => {
1317
+ var buildMarketBreadthsContextFromSignal = (signal) => {
1369
1318
  const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1370
1319
  const relative = toRecord(baseContext?.relative);
1371
- const refs = toRecord(relative?.referenceTradeFlow);
1372
- const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
1373
- const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
1374
- const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
1375
- if (!refs) {
1320
+ const breadths = toRecord(relative?.marketBreadths);
1321
+ const build = (key) => {
1322
+ const breadth = toRecord(breadths?.[key]);
1323
+ if (!breadth) {
1324
+ return {
1325
+ source: null,
1326
+ available: false,
1327
+ universe: null,
1328
+ interval: null,
1329
+ stale: null,
1330
+ symbolsCount: null,
1331
+ advanceDeclineRatio: null,
1332
+ pctAboveMa20: null,
1333
+ pctAboveMa50: null,
1334
+ equalWeightedReturn: null,
1335
+ volumeWeightedReturn: null,
1336
+ dispersion: null
1337
+ };
1338
+ }
1376
1339
  return {
1377
- source: null,
1378
- available: false,
1379
- primaryReferenceSymbol: null,
1380
- referenceSymbols: [],
1381
- primaryTradeFlowBuyPressurePct: null,
1382
- primaryTradeFlowStale: null
1340
+ source: String(breadth.source ?? ""),
1341
+ available: true,
1342
+ universe: String(breadth.universe ?? ""),
1343
+ interval: String(breadth.interval ?? ""),
1344
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
1345
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
1346
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
1347
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
1348
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
1349
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
1350
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
1351
+ dispersion: toFiniteNumber(breadth.dispersion)
1383
1352
  };
1384
- }
1353
+ };
1385
1354
  return {
1386
- source: String(refs.source ?? ""),
1387
- available: true,
1388
- primaryReferenceSymbol,
1389
- referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
1390
- primaryTradeFlowBuyPressurePct: toFiniteNumber(
1391
- primaryTradeFlow?.buyPressurePct
1392
- ),
1393
- primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
1355
+ top5: build("top5"),
1356
+ top10: build("top10"),
1357
+ top30: build("top30"),
1358
+ top50: build("top50"),
1359
+ top100: build("top100")
1394
1360
  };
1395
1361
  };
1396
- var buildAiMarketContext = (signal) => ({
1397
- execution: {
1398
- binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
1399
- },
1400
- participation: {
1401
- trueDelta: buildTrueDeltaContextFromSignal(signal),
1402
- tradeFlow: buildTradeFlowContextFromSignal(signal)
1403
- },
1404
- relative: {
1405
- marketBreadth: buildMarketBreadthContextFromSignal(signal),
1406
- marketBreadths: buildMarketBreadthsContextFromSignal(signal),
1407
- targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
1408
- btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
1409
- cmcGlobal: buildCmcGlobalContextFromSignal(signal),
1410
- cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
1411
- cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
1412
- cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
1413
- cmcIndexes: buildCmcIndexesContextFromSignal(signal),
1414
- referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
1415
- }
1416
- });
1417
-
1418
- // src/strategy/manifests.ts
1419
- var import_indicators = require("@tradejs/core/indicators");
1420
-
1421
- // src/tradejsConfig.ts
1422
- var import_fs = __toESM(require("fs"));
1423
- var import_path = __toESM(require("path"));
1424
- var import_url = require("url");
1425
- var import_config = require("@tradejs/core/config");
1426
- var import_logger = require("@tradejs/infra/logger");
1427
- var CONFIG_FILE_NAMES = [
1428
- "tradejs.config.ts",
1429
- "tradejs.config.mts",
1430
- "tradejs.config.js",
1431
- "tradejs.config.mjs",
1432
- "tradejs.config.cjs"
1433
- ];
1434
- var TS_MODULE_RE = /\.(cts|mts|ts)$/i;
1435
- var cachedByCwd = /* @__PURE__ */ new Map();
1436
- var announcedConfigFile = /* @__PURE__ */ new Set();
1437
- var tsNodeRegistered = false;
1438
- var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
1439
- var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
1440
- var getTradejsProjectCwd = (cwd) => {
1441
- const explicit = String(cwd ?? "").trim();
1442
- if (explicit) {
1443
- return import_path.default.resolve(explicit);
1444
- }
1445
- const fromEnv = String(process.env.PROJECT_CWD || "").trim();
1446
- if (fromEnv) {
1447
- return import_path.default.resolve(fromEnv);
1448
- }
1449
- return process.cwd();
1450
- };
1451
- var normalizeConfig = (rawConfig) => {
1452
- if (!rawConfig || typeof rawConfig !== "object") {
1453
- return {};
1454
- }
1455
- const config = rawConfig;
1456
- const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
1457
- const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
1458
- const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
1459
- const hooks = (0, import_config.normalizeTradejsConfigHooks)(
1460
- config.hooks
1461
- );
1462
- const runtime = config.runtime && typeof config.runtime === "object" && !Array.isArray(config.runtime) ? config.runtime : void 0;
1362
+ var buildTargetVsReference = (signal, asset) => {
1363
+ const base = toRecord(signal.additionalIndicators?.baseContext);
1364
+ const relative = toRecord(base?.relative);
1365
+ const target = toRecord(relative?.[`targetVs${asset}`]);
1463
1366
  return {
1464
- strategies: strategies2,
1465
- indicators,
1466
- connectors,
1467
- ...hooks ? { hooks } : {},
1468
- ...runtime ? { runtime } : {}
1367
+ source: target ? String(target.source ?? "") : null,
1368
+ available: Boolean(target),
1369
+ ratioReturn1h: toFiniteNumber(target?.ratioReturn1h),
1370
+ ratioReturn4h: toFiniteNumber(target?.ratioReturn4h),
1371
+ ratioReturn24h: toFiniteNumber(target?.ratioReturn24h),
1372
+ alpha1h: toFiniteNumber(target?.[`alphaVs${asset}1h`]),
1373
+ alpha4h: toFiniteNumber(target?.[`alphaVs${asset}4h`]),
1374
+ alpha24h: toFiniteNumber(target?.[`alphaVs${asset}24h`]),
1375
+ beta: toFiniteNumber(target?.[`betaTo${asset}20`]),
1376
+ correlation: toFiniteNumber(target?.[`correlationTo${asset}20`]),
1377
+ ratioTrend: typeof target?.ratioTrend === "string" ? target.ratioTrend : null
1469
1378
  };
1470
1379
  };
1471
- var getNodeCreateRequire = () => {
1472
- const builtinModule = process.getBuiltinModule?.("module");
1473
- if (typeof builtinModule?.createRequire === "function") {
1474
- return builtinModule.createRequire;
1475
- }
1476
- throw new TypeError("module.createRequire is not available");
1380
+ var buildTargetVsBtcContextFromSignal = (signal) => {
1381
+ const { alpha1h, alpha4h, alpha24h, beta, correlation, ...shared } = buildTargetVsReference(signal, "Btc");
1382
+ return {
1383
+ ...shared,
1384
+ alphaVsBtc1h: alpha1h,
1385
+ alphaVsBtc4h: alpha4h,
1386
+ alphaVsBtc24h: alpha24h,
1387
+ betaToBtc20: beta,
1388
+ correlationToBtc20: correlation
1389
+ };
1477
1390
  };
1478
- var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
1479
- var ensureTsNodeRegistered = async () => {
1480
- if (tsNodeRegistered) {
1481
- return;
1482
- }
1483
- const tsNodeModule = await import("ts-node");
1484
- const tsNode = tsNodeModule.default ?? tsNodeModule;
1485
- tsNode.register?.({
1486
- transpileOnly: true,
1487
- compilerOptions: {
1488
- module: "Node16",
1489
- moduleResolution: "node16"
1490
- }
1491
- });
1492
- tsNodeRegistered = true;
1391
+ var buildTargetVsEthContextFromSignal = (signal) => {
1392
+ const { alpha1h, alpha4h, alpha24h, beta, correlation, ...shared } = buildTargetVsReference(signal, "Eth");
1393
+ return {
1394
+ ...shared,
1395
+ alphaVsEth1h: alpha1h,
1396
+ alphaVsEth4h: alpha4h,
1397
+ alphaVsEth24h: alpha24h,
1398
+ betaToEth20: beta,
1399
+ correlationToEth20: correlation
1400
+ };
1493
1401
  };
1494
- var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
1495
- const projectRoot = getTradejsProjectCwd(cwd);
1496
- if (tsconfigPathsRegisteredByCwd.has(projectRoot)) {
1497
- return;
1402
+ var buildBtcAltRegimeContextFromSignal = (signal) => {
1403
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1404
+ const relative = toRecord(baseContext?.relative);
1405
+ const btcAltRegime = toRecord(relative?.btcAltRegime);
1406
+ if (!btcAltRegime) {
1407
+ return {
1408
+ source: null,
1409
+ available: false,
1410
+ universe: null,
1411
+ interval: null,
1412
+ stale: null,
1413
+ regime: null,
1414
+ btcReturn24h: null,
1415
+ altBasketReturn24h: null,
1416
+ btcVsAltReturn24h: null,
1417
+ btcTurnoverShare24h: null,
1418
+ btcTurnoverShareChange24h: null,
1419
+ altVolToBtcVol24h: null,
1420
+ altDispersion24h: null
1421
+ };
1498
1422
  }
1499
- const tsconfigPathsModule = await import("tsconfig-paths");
1500
- const loadConfig = tsconfigPathsModule.loadConfig;
1501
- const register = tsconfigPathsModule.register;
1502
- if (typeof loadConfig !== "function" || typeof register !== "function") {
1503
- return;
1504
- }
1505
- const loadedConfig = loadConfig(projectRoot);
1506
- if (loadedConfig.resultType !== "success") {
1507
- return;
1508
- }
1509
- register({
1510
- baseUrl: loadedConfig.absoluteBaseUrl,
1511
- paths: loadedConfig.paths,
1512
- addMatchAll: false
1513
- });
1514
- tsconfigPathsRegisteredByCwd.add(projectRoot);
1423
+ return {
1424
+ source: String(btcAltRegime.source ?? ""),
1425
+ available: true,
1426
+ universe: String(btcAltRegime.universe ?? ""),
1427
+ interval: String(btcAltRegime.interval ?? ""),
1428
+ stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
1429
+ regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
1430
+ btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
1431
+ altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
1432
+ btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
1433
+ btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
1434
+ btcTurnoverShareChange24h: toFiniteNumber(
1435
+ btcAltRegime.btcTurnoverShareChange24h
1436
+ ),
1437
+ altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
1438
+ altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
1439
+ };
1515
1440
  };
1516
- var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1517
- const projectRoot = getTradejsProjectCwd(cwd);
1518
- const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
1519
- if (cachedMatcher) {
1520
- const resolved2 = cachedMatcher(moduleName);
1521
- return resolved2 || null;
1522
- }
1523
- const tsconfigPathsModule = await import("tsconfig-paths");
1524
- const loadConfig = tsconfigPathsModule.loadConfig;
1525
- const createMatchPath = tsconfigPathsModule.createMatchPath;
1526
- if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
1527
- return null;
1528
- }
1529
- const loadedConfig = loadConfig(projectRoot);
1530
- if (loadedConfig.resultType !== "success") {
1531
- return null;
1441
+ var buildCmcGlobalContextFromSignal = (signal) => {
1442
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1443
+ const relative = toRecord(baseContext?.relative);
1444
+ const cmcGlobal = toRecord(relative?.cmcGlobal);
1445
+ if (!cmcGlobal) {
1446
+ return {
1447
+ source: null,
1448
+ available: false,
1449
+ interval: null,
1450
+ asOfTs: null,
1451
+ stale: null,
1452
+ totalMarketCapUsd: null,
1453
+ totalVolumeUsd: null,
1454
+ totalVolumeReportedUsd: null,
1455
+ altMarketCapUsd: null,
1456
+ altVolumeUsd: null,
1457
+ altVolumeReportedUsd: null,
1458
+ btcDominancePct: null,
1459
+ ethDominancePct: null,
1460
+ btcDominanceChange24hPct: null,
1461
+ ethDominanceChange24hPct: null,
1462
+ altMarketCapChange24hPct: null,
1463
+ altVolumeChange24hPct: null,
1464
+ activeCryptocurrencies: null,
1465
+ activeExchanges: null,
1466
+ activeMarketPairs: null,
1467
+ altLiquidityRegime: null
1468
+ };
1532
1469
  }
1533
- const matchPath = createMatchPath(
1534
- loadedConfig.absoluteBaseUrl,
1535
- loadedConfig.paths
1536
- );
1537
- const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
1538
- ".ts",
1539
- ".tsx",
1540
- ".mts",
1541
- ".cts",
1542
- ".js",
1543
- ".jsx",
1544
- ".mjs",
1545
- ".cjs",
1546
- ".json"
1547
- ]) || "";
1548
- tsconfigPathMatchersByCwd.set(projectRoot, matcher);
1549
- const resolved = matcher(moduleName);
1550
- return resolved || null;
1470
+ return {
1471
+ source: String(cmcGlobal.source ?? ""),
1472
+ available: true,
1473
+ interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
1474
+ asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
1475
+ stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
1476
+ totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
1477
+ totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
1478
+ totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
1479
+ altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
1480
+ altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
1481
+ altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
1482
+ btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
1483
+ ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
1484
+ btcDominanceChange24hPct: toFiniteNumber(
1485
+ cmcGlobal.btcDominanceChange24hPct
1486
+ ),
1487
+ ethDominanceChange24hPct: toFiniteNumber(
1488
+ cmcGlobal.ethDominanceChange24hPct
1489
+ ),
1490
+ altMarketCapChange24hPct: toFiniteNumber(
1491
+ cmcGlobal.altMarketCapChange24hPct
1492
+ ),
1493
+ altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
1494
+ activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
1495
+ activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
1496
+ activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
1497
+ altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
1498
+ };
1551
1499
  };
1552
- var toImportSpecifier = (moduleName) => {
1553
- if (moduleName.startsWith("file://")) {
1554
- return moduleName;
1555
- }
1556
- if (import_path.default.isAbsolute(moduleName)) {
1557
- return (0, import_url.pathToFileURL)(moduleName).href;
1500
+ var buildCmcReferenceAssetsContextFromSignal = (signal) => {
1501
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1502
+ const relative = toRecord(baseContext?.relative);
1503
+ const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
1504
+ if (!cmcReferenceAssets) {
1505
+ return {
1506
+ source: null,
1507
+ available: false,
1508
+ interval: null,
1509
+ asOfTs: null,
1510
+ stale: null,
1511
+ btcMarketCapUsd: null,
1512
+ ethMarketCapUsd: null,
1513
+ btcVolumeUsd: null,
1514
+ ethVolumeUsd: null,
1515
+ btcVolumeToMarketCap: null,
1516
+ ethVolumeToMarketCap: null,
1517
+ ethBtcMarketCapRatio: null,
1518
+ ethBtcMarketCapRatioChange24hPct: null,
1519
+ ethVsBtcVolumeRatio: null,
1520
+ referenceLiquidityRegime: null
1521
+ };
1558
1522
  }
1559
- return moduleName;
1523
+ return {
1524
+ source: String(cmcReferenceAssets.source ?? ""),
1525
+ available: true,
1526
+ interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
1527
+ asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
1528
+ stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
1529
+ btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
1530
+ ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
1531
+ btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
1532
+ ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
1533
+ btcVolumeToMarketCap: toFiniteNumber(
1534
+ cmcReferenceAssets.btcVolumeToMarketCap
1535
+ ),
1536
+ ethVolumeToMarketCap: toFiniteNumber(
1537
+ cmcReferenceAssets.ethVolumeToMarketCap
1538
+ ),
1539
+ ethBtcMarketCapRatio: toFiniteNumber(
1540
+ cmcReferenceAssets.ethBtcMarketCapRatio
1541
+ ),
1542
+ ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
1543
+ cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
1544
+ ),
1545
+ ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
1546
+ referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
1547
+ };
1560
1548
  };
1561
- var isTsModulePath = (moduleName) => TS_MODULE_RE.test(moduleName.split("?")[0]);
1562
- var isRelativeModulePath = (moduleName) => moduleName.startsWith("./") || moduleName.startsWith("../");
1563
- var isBareModuleSpecifier = (moduleName) => {
1564
- const normalized = String(moduleName ?? "").trim();
1565
- if (!normalized) {
1566
- return false;
1567
- }
1568
- if (normalized.startsWith("file://") || import_path.default.isAbsolute(normalized) || isRelativeModulePath(normalized)) {
1569
- return false;
1549
+ var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
1550
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1551
+ const relative = toRecord(baseContext?.relative);
1552
+ const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
1553
+ if (!cmcExchangeLiquidity) {
1554
+ return {
1555
+ source: null,
1556
+ available: false,
1557
+ interval: null,
1558
+ asOfTs: null,
1559
+ stale: null,
1560
+ exchangesCount: null,
1561
+ totalVolumeUsd: null,
1562
+ totalVolumeChange24hPct: null,
1563
+ binanceVolumeUsd: null,
1564
+ binanceVolumeShare: null,
1565
+ topExchangeVolumeShare: null,
1566
+ liquidityRegime: null
1567
+ };
1570
1568
  }
1571
- return true;
1569
+ return {
1570
+ source: String(cmcExchangeLiquidity.source ?? ""),
1571
+ available: true,
1572
+ interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
1573
+ asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
1574
+ stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
1575
+ exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
1576
+ totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
1577
+ totalVolumeChange24hPct: toFiniteNumber(
1578
+ cmcExchangeLiquidity.totalVolumeChange24hPct
1579
+ ),
1580
+ binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
1581
+ binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
1582
+ topExchangeVolumeShare: toFiniteNumber(
1583
+ cmcExchangeLiquidity.topExchangeVolumeShare
1584
+ ),
1585
+ liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
1586
+ };
1572
1587
  };
1573
- var importConfigFile = async (configFilePath) => {
1574
- const ext = import_path.default.extname(configFilePath).toLowerCase();
1575
- const configFileUrl = `${toImportSpecifier(configFilePath)}?t=${Date.now()}`;
1576
- if (ext === ".ts" || ext === ".mts") {
1577
- const requireFn = getRequireFn(import_path.default.dirname(configFilePath));
1578
- await ensureTsNodeRegistered();
1579
- await ensureTsconfigPathsRegistered(import_path.default.dirname(configFilePath));
1580
- return requireFn(configFilePath);
1588
+ var buildCmcFearGreedContextFromSignal = (signal) => {
1589
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1590
+ const relative = toRecord(baseContext?.relative);
1591
+ const cmcFearGreed = toRecord(relative?.cmcFearGreed);
1592
+ if (!cmcFearGreed) {
1593
+ return {
1594
+ source: null,
1595
+ available: false,
1596
+ interval: null,
1597
+ asOfTs: null,
1598
+ stale: null,
1599
+ value: null,
1600
+ valueChange24h: null,
1601
+ valueChange7d: null,
1602
+ classification: null,
1603
+ sentimentRegime: null
1604
+ };
1581
1605
  }
1582
- return import(
1583
- /* webpackIgnore: true */
1584
- configFileUrl
1585
- );
1586
- };
1587
- var importTradejsModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1588
- const normalized = String(moduleName ?? "").trim();
1589
- if (!normalized) {
1590
- return {};
1591
- }
1592
- let modulePath = normalized;
1593
- if (normalized.startsWith("file://")) {
1594
- try {
1595
- modulePath = (0, import_url.fileURLToPath)(normalized);
1596
- } catch {
1597
- modulePath = normalized;
1598
- }
1599
- }
1600
- const requireFn = getRequireFn(
1601
- import_path.default.isAbsolute(modulePath) ? import_path.default.dirname(modulePath) : cwd
1602
- );
1603
- if (isTsModulePath(modulePath)) {
1604
- await ensureTsNodeRegistered();
1605
- await ensureTsconfigPathsRegistered(cwd);
1606
- return requireFn(modulePath);
1607
- }
1608
- if (isBareModuleSpecifier(normalized)) {
1609
- await ensureTsconfigPathsRegistered(cwd);
1610
- try {
1611
- return requireFn(normalized);
1612
- } catch (error) {
1613
- const resolvedByTsconfig = await resolveTsconfigPathModule(
1614
- normalized,
1615
- cwd
1616
- );
1617
- if (resolvedByTsconfig && resolvedByTsconfig !== normalized) {
1618
- return requireFn(resolvedByTsconfig);
1619
- }
1620
- throw error;
1621
- }
1622
- }
1623
- try {
1624
- return await import(
1625
- /* webpackIgnore: true */
1626
- toImportSpecifier(normalized)
1627
- );
1628
- } catch (error) {
1629
- if (isTsModulePath(modulePath)) {
1630
- await ensureTsNodeRegistered();
1631
- await ensureTsconfigPathsRegistered(cwd);
1632
- return requireFn(modulePath);
1633
- }
1634
- throw error;
1635
- }
1636
- };
1637
- var resolveExportedConfig = (moduleExports) => {
1638
- const candidate = moduleExports && typeof moduleExports === "object" && "default" in moduleExports ? moduleExports.default : moduleExports;
1639
- return normalizeConfig(candidate);
1606
+ return {
1607
+ source: String(cmcFearGreed.source ?? ""),
1608
+ available: true,
1609
+ interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
1610
+ asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
1611
+ stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
1612
+ value: toFiniteNumber(cmcFearGreed.value),
1613
+ valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
1614
+ valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
1615
+ classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
1616
+ sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
1617
+ };
1640
1618
  };
1641
- var findConfigFilePath = (cwd) => {
1642
- let currentDir = import_path.default.resolve(cwd);
1643
- while (true) {
1644
- for (const fileName of CONFIG_FILE_NAMES) {
1645
- const fullPath = import_path.default.join(currentDir, fileName);
1646
- if (import_fs.default.existsSync(fullPath) && import_fs.default.statSync(fullPath).isFile()) {
1647
- return fullPath;
1648
- }
1649
- }
1650
- const parentDir = import_path.default.dirname(currentDir);
1651
- if (parentDir === currentDir) {
1652
- return null;
1653
- }
1654
- currentDir = parentDir;
1619
+ var buildCmcIndexesContextFromSignal = (signal) => {
1620
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1621
+ const relative = toRecord(baseContext?.relative);
1622
+ const cmcIndexes = toRecord(relative?.cmcIndexes);
1623
+ if (!cmcIndexes) {
1624
+ return {
1625
+ source: null,
1626
+ available: false,
1627
+ interval: null,
1628
+ asOfTs: null,
1629
+ stale: null,
1630
+ cmc100Value: null,
1631
+ cmc100Change24hPct: null,
1632
+ cmc100TopConstituentSymbol: null,
1633
+ cmc100TopConstituentWeightPct: null,
1634
+ cmc20Value: null,
1635
+ cmc20Change24hPct: null,
1636
+ cmc20TopConstituentSymbol: null,
1637
+ cmc20TopConstituentWeightPct: null,
1638
+ cmc20ToCmc100Ratio: null,
1639
+ cmc20ToCmc100RatioChange24hPct: null,
1640
+ indexRegime: null
1641
+ };
1655
1642
  }
1643
+ return {
1644
+ source: String(cmcIndexes.source ?? ""),
1645
+ available: true,
1646
+ interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
1647
+ asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
1648
+ stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
1649
+ cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
1650
+ cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
1651
+ cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
1652
+ cmc100TopConstituentWeightPct: toFiniteNumber(
1653
+ cmcIndexes.cmc100TopConstituentWeightPct
1654
+ ),
1655
+ cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
1656
+ cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
1657
+ cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
1658
+ cmc20TopConstituentWeightPct: toFiniteNumber(
1659
+ cmcIndexes.cmc20TopConstituentWeightPct
1660
+ ),
1661
+ cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
1662
+ cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
1663
+ cmcIndexes.cmc20ToCmc100RatioChange24hPct
1664
+ ),
1665
+ indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
1666
+ };
1656
1667
  };
1657
- var resolvePluginModuleSpecifier = (moduleName, cwd = getTradejsProjectCwd()) => {
1658
- const normalized = String(moduleName ?? "").trim();
1659
- if (!normalized) {
1660
- return "";
1661
- }
1662
- if (normalized.startsWith("file://")) {
1663
- try {
1664
- return (0, import_url.fileURLToPath)(normalized);
1665
- } catch {
1666
- return normalized;
1667
- }
1668
- }
1669
- if (import_path.default.isAbsolute(normalized)) {
1670
- return normalized;
1671
- }
1672
- if (isRelativeModulePath(normalized)) {
1673
- return import_path.default.resolve(cwd, normalized);
1668
+ var buildReferenceTradeFlowContextFromSignal = (signal) => {
1669
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1670
+ const relative = toRecord(baseContext?.relative);
1671
+ const refs = toRecord(relative?.referenceTradeFlow);
1672
+ const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
1673
+ const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
1674
+ const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
1675
+ if (!refs) {
1676
+ return {
1677
+ source: null,
1678
+ available: false,
1679
+ primaryReferenceSymbol: null,
1680
+ referenceSymbols: [],
1681
+ primaryTradeFlowBuyPressurePct: null,
1682
+ primaryTradeFlowStale: null
1683
+ };
1674
1684
  }
1675
- return normalized;
1685
+ return {
1686
+ source: String(refs.source ?? ""),
1687
+ available: true,
1688
+ primaryReferenceSymbol,
1689
+ referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
1690
+ primaryTradeFlowBuyPressurePct: toFiniteNumber(
1691
+ primaryTradeFlow?.buyPressurePct
1692
+ ),
1693
+ primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
1694
+ };
1676
1695
  };
1677
- var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
1678
- const cached = cachedByCwd.get(cwd);
1679
- if (cached) {
1680
- return cached;
1681
- }
1682
- const configFilePath = findConfigFilePath(cwd);
1683
- if (!configFilePath) {
1684
- cachedByCwd.set(cwd, {});
1685
- return {};
1686
- }
1687
- try {
1688
- const moduleExports = await importConfigFile(configFilePath);
1689
- const config = resolveExportedConfig(moduleExports);
1690
- cachedByCwd.set(cwd, config);
1691
- if (!announcedConfigFile.has(configFilePath)) {
1692
- announcedConfigFile.add(configFilePath);
1693
- import_logger.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
1694
- }
1695
- return config;
1696
- } catch (error) {
1697
- import_logger.logger.log(
1698
- "warn",
1699
- "Failed to load TradeJS config from %s: %s",
1700
- configFilePath,
1701
- String(error)
1702
- );
1703
- cachedByCwd.set(cwd, {});
1704
- return {};
1696
+ var buildAiMarketContext = (signal) => ({
1697
+ execution: {
1698
+ binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
1699
+ },
1700
+ participation: {
1701
+ trueDelta: buildTrueDeltaContextFromSignal(signal),
1702
+ tradeFlow: buildTradeFlowContextFromSignal(signal)
1703
+ },
1704
+ relative: {
1705
+ marketBreadth: buildMarketBreadthContextFromSignal(signal),
1706
+ marketBreadths: buildMarketBreadthsContextFromSignal(signal),
1707
+ targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
1708
+ targetVsEth: buildTargetVsEthContextFromSignal(signal),
1709
+ btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
1710
+ cmcGlobal: buildCmcGlobalContextFromSignal(signal),
1711
+ cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
1712
+ cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
1713
+ cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
1714
+ cmcIndexes: buildCmcIndexesContextFromSignal(signal),
1715
+ referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
1705
1716
  }
1706
- };
1717
+ });
1707
1718
 
1708
1719
  // src/strategy/manifests.ts
1720
+ var import_indicators = require("@tradejs/core/indicators");
1709
1721
  var SHARED_STRATEGY_REGISTRY_KEY = "__tradejsNodeSharedStrategyRegistryV1__";
1710
1722
  var sharedRegistryScope = globalThis;
1711
1723
  var sharedStrategyRegistry = sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] ?? (sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] = {
@@ -2169,12 +2181,61 @@ var buildBaseAiPayload = (signal) => {
2169
2181
  };
2170
2182
  };
2171
2183
  var defaultAiAdapter = {};
2184
+ var tickerProfileFeatureIds = new Set(
2185
+ import_aiFeatures.TICKER_PROFILE_AI_FEATURES.map(({ id }) => id)
2186
+ );
2172
2187
  var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
2173
2188
  var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
2174
2189
  var buildAiPayloadByStrategy = (signal) => {
2175
2190
  const basePayload = buildBaseAiPayload(signal);
2176
2191
  const adapter = getSignalAiAdapter(signal);
2177
- return adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
2192
+ const payload = adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
2193
+ const custom = getStrategyManifest(signal.strategy)?.aiFeatures ?? [];
2194
+ const frozen = signal.aiFeatureReplay?.snapshot;
2195
+ if (frozen) (0, import_aiFeatures.validateAiFeatureSnapshot)(frozen);
2196
+ const tickerProfile = signal.additionalIndicators?.tickerProfile ?? signal.additionalIndicators?.baseContext?.tickerProfile;
2197
+ const hasProfile = Boolean(
2198
+ tickerProfile || frozen?.definitions.some(({ id }) => tickerProfileFeatureIds.has(id))
2199
+ );
2200
+ const indicatorFeatures = (0, import_indicators2.getRegisteredIndicatorEntries)(
2201
+ getTradejsProjectCwd()
2202
+ ).flatMap((entry) => entry.indicator.enabled ? entry.aiFeatures ?? [] : []);
2203
+ const hasPortfolio = Boolean(
2204
+ signal.additionalIndicators?.portfolioContext || frozen?.definitions.some(
2205
+ ({ id }) => import_aiFeatures.PORTFOLIO_AI_FEATURES.some((d) => d.id === id)
2206
+ )
2207
+ );
2208
+ const definitions = [
2209
+ ...hasPortfolio ? import_aiFeatures.PORTFOLIO_AI_FEATURES : [],
2210
+ ...hasProfile ? import_aiFeatures.TICKER_PROFILE_AI_FEATURES : [],
2211
+ ...indicatorFeatures,
2212
+ ...custom
2213
+ ];
2214
+ if (!definitions.length && !signal.aiFeatureReplay?.snapshot) return payload;
2215
+ (0, import_aiFeatures.validateAiFeatureDefinitions)(definitions);
2216
+ if (signal.aiFeatureReplay && !frozen) {
2217
+ throw new Error(
2218
+ "Dataset has no frozen AI features; regenerate the decision-time export"
2219
+ );
2220
+ }
2221
+ if (frozen && JSON.stringify(frozen.definitions) !== JSON.stringify(definitions)) {
2222
+ throw new Error(
2223
+ "AI feature definition changed; regenerate the decision-time export"
2224
+ );
2225
+ }
2226
+ return {
2227
+ ...payload,
2228
+ featureSnapshot: frozen ?? (0, import_aiFeatures.projectAiFeatures)(definitions, {
2229
+ signal: tickerProfile ? {
2230
+ ...signal,
2231
+ additionalIndicators: {
2232
+ ...signal.additionalIndicators,
2233
+ tickerProfile
2234
+ }
2235
+ } : signal,
2236
+ payload
2237
+ })
2238
+ };
2178
2239
  };
2179
2240
  var buildAiSystemPromptAddonByStrategy = (signal) => getSignalAiAdapter(signal).buildSystemPromptAddon?.({ signal }) ?? "";
2180
2241
  var buildAiHumanPromptAddonByStrategy = (signal, payload) => getSignalAiAdapter(signal).buildHumanPromptAddon?.({
@@ -2638,535 +2699,216 @@ var runAiPromptLocal = async (signal, options = {}) => {
2638
2699
  payload
2639
2700
  );
2640
2701
  };
2641
- var askAI = async (signal, options = {}) => {
2642
- const { symbol } = signal;
2643
- const payload = buildAiPayload(signal);
2644
- const content = await runAiPrompt(
2645
- {
2646
- systemPrompt: buildAiSystemPrompt(signal),
2647
- humanPrompt: buildAiHumanPrompt(signal, payload)
2648
- },
2649
- {
2650
- ...options,
2651
- signal,
2652
- payload
2653
- }
2654
- );
2655
- await (0, import_redis.setData)(import_redis.redisKeys.analysis(symbol, signal.signalId), content);
2656
- return content;
2657
- };
2658
-
2659
- // src/strategyRuntime.ts
2660
- var import_jev6 = require("@tradejs/core/jev");
2661
-
2662
- // src/jev.ts
2663
- var import_promises = __toESM(require("fs/promises"));
2664
- var import_node_path = __toESM(require("path"));
2665
- var import_jev4 = require("@tradejs/core/jev");
2666
- var import_jev5 = require("@tradejs/infra/jev");
2667
- var import_userSettings2 = require("@tradejs/infra/userSettings");
2668
- init_jevInput();
2669
- init_jevInput();
2670
- init_jevTraining();
2671
- var resolveJevProvider = async (userName) => {
2672
- const settings = await (0, import_userSettings2.getUserSettings)(userName);
2673
- const provider = {
2674
- endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
2675
- model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
2676
- };
2677
- (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
2678
- if (!settings.JEV_API_KEY)
2679
- throw new Error("Configure the Jev API key in Account settings");
2680
- return provider;
2681
- };
2682
- var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
2683
- var pending = /* @__PURE__ */ new Map();
2684
- var pendingResponses = /* @__PURE__ */ new Map();
2685
- var evaluateJevInput = async ({
2686
- input,
2687
- config,
2688
- userName,
2689
- projectRoot
2690
- }) => {
2691
- config = (0, import_jev4.parseJevConfig)(config);
2692
- const inputHash = (0, import_jev5.jevHash)(input);
2693
- const questions = questionsForJevInput(input);
2694
- const questionsHash = (0, import_jev5.jevHash)(questions);
2695
- const base = {
2696
- questionSet: import_jev4.JEV_QUESTION_SET,
2697
- schema: "tradejs-signal-assessment/v5",
2698
- source: config.source,
2699
- mode: config.mode,
2700
- inputHash,
2701
- questionsHash,
2702
- geometryStatus: input.geometryStatus,
2703
- levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
2704
- };
2705
- if (!input.questions.length)
2706
- return {
2707
- assessment: {
2708
- ...base,
2709
- status: "unavailable",
2710
- model: "",
2711
- scores: emptyScores(),
2712
- confidence: emptyScores()
2713
- }
2714
- };
2715
- if (config.source === "local") {
2716
- const contents = await import_promises.default.readFile(
2717
- import_node_path.default.resolve(projectRoot, config.modelFile),
2718
- "utf8"
2719
- );
2720
- if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
2721
- throw new Error("Local Jev gate checksum mismatch");
2722
- const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
2723
- const model = validateJevGateModel2(JSON.parse(contents));
2724
- if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
2725
- throw new Error("Local Jev gate strategy or question version mismatch");
2726
- const scores = Object.fromEntries(
2727
- import_jev4.JEV_DIMENSIONS.map((key) => [
2728
- key,
2729
- input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
2730
- ])
2731
- );
2732
- return {
2733
- assessment: {
2734
- ...base,
2735
- status: "available",
2736
- model: config.modelSha256,
2737
- scores,
2738
- confidence: emptyScores()
2739
- }
2740
- };
2741
- }
2742
- const provider = config.provider ?? await resolveJevProvider(userName);
2743
- const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
2744
- const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
2745
- const file = import_node_path.default.join(dir, "records", `${id}.json`);
2746
- const getRecord = async () => {
2747
- const existing = await (0, import_jev5.readJevArtifact)(file);
2748
- if (existing) {
2749
- if (existing.schema !== "tradejs-jev-record/v4" || existing.id !== id || existing.inputHash !== inputHash || existing.questionsHash !== questionsHash || (0, import_jev5.jevHash)(existing.input) !== inputHash || (0, import_jev5.jevHash)(existing.provider) !== (0, import_jev5.jevHash)(provider))
2750
- throw new Error("Jev recording identity mismatch");
2751
- (0, import_jev5.validateJevResponse)(existing.response, input.questions);
2752
- for (const key of import_jev4.JEV_DIMENSIONS) {
2753
- const expected = input.questions.includes(key) ? existing.response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null;
2754
- if (existing.scores[key] !== expected)
2755
- throw new Error("Jev recording scores do not match the response");
2756
- }
2757
- return existing;
2758
- }
2759
- if (config.source === "recorded")
2760
- throw new Error(
2761
- `Missing Jev recording ${id} for ${input.strategy} ${input.symbol} at ${new Date(input.timestamp).toISOString()}. Recorded mode requires the same signal-time facts and provider; a shorter backtest can change warm-up indicators or prior trade state.`
2762
- );
2763
- const settings = await (0, import_userSettings2.getUserSettings)(userName);
2764
- if (!settings.JEV_API_KEY)
2765
- throw new Error("Configure the Jev API key in Account settings");
2766
- const apiKey = settings.JEV_API_KEY;
2767
- const configuredProvider = await resolveJevProvider(userName);
2768
- if (configuredProvider.endpoint !== provider.endpoint)
2769
- throw new Error(
2770
- "Jev account endpoint changed; update the frozen provider configuration"
2771
- );
2772
- const started = Date.now();
2773
- const state = stateForJevInput(input);
2774
- const responseId = (0, import_jev5.jevHash)({
2775
- state,
2776
- questionsHash,
2777
- provider,
2778
- user: (0, import_jev5.jevHash)(userName)
2779
- });
2780
- const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
2781
- let responsePromise = pendingResponses.get(responseFile);
2782
- if (!responsePromise) {
2783
- responsePromise = (async () => {
2784
- const saved = await (0, import_jev5.readJevArtifact)(responseFile);
2785
- if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
2786
- const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
2787
- await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
2788
- return fresh;
2789
- })();
2790
- pendingResponses.set(responseFile, responsePromise);
2791
- }
2792
- let response;
2793
- try {
2794
- response = await responsePromise;
2795
- } finally {
2796
- pendingResponses.delete(responseFile);
2797
- }
2798
- const scores = Object.fromEntries(
2799
- import_jev4.JEV_DIMENSIONS.map((key) => [
2800
- key,
2801
- input.questions.includes(key) ? response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null
2802
- ])
2803
- );
2804
- const value = {
2805
- schema: "tradejs-jev-record/v4",
2806
- id,
2807
- inputHash,
2808
- questionsHash,
2809
- provider,
2810
- input,
2811
- response,
2812
- scores,
2813
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2814
- elapsedMs: Date.now() - started
2815
- };
2816
- await (0, import_jev5.writeJevArtifact)(file, value);
2817
- return await (0, import_jev5.readJevArtifact)(file);
2818
- };
2819
- const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
2820
- let promise = pending.get(pendingKey);
2821
- if (!promise) {
2822
- promise = getRecord();
2823
- pending.set(pendingKey, promise);
2824
- }
2825
- let record;
2826
- try {
2827
- record = await promise;
2828
- } finally {
2829
- pending.delete(pendingKey);
2830
- }
2831
- return {
2832
- record,
2833
- assessment: {
2834
- ...base,
2835
- status: "available",
2836
- recordId: id,
2837
- model: record.response.model,
2838
- scores: record.scores,
2839
- confidence: Object.fromEntries(
2840
- import_jev4.JEV_DIMENSIONS.map((key) => [
2841
- key,
2842
- record.response.answers[key]?.confidence ?? null
2843
- ])
2844
- )
2845
- }
2846
- };
2847
- };
2848
- var assessSignalWithJev = async ({
2849
- signal,
2850
- config,
2851
- userName,
2852
- projectRoot,
2853
- strict,
2854
- scope = "runtime"
2855
- }) => {
2856
- let assessment;
2857
- try {
2858
- const input = buildJevInput(signal);
2859
- ({ assessment } = await evaluateJevInput({
2860
- input,
2861
- config,
2862
- userName,
2863
- projectRoot
2864
- }));
2865
- } catch (error) {
2866
- if (strict) throw error;
2867
- assessment = {
2868
- questionSet: import_jev4.JEV_QUESTION_SET,
2869
- schema: "tradejs-signal-assessment/v5",
2870
- source: config.source,
2871
- mode: config.mode,
2872
- status: "unavailable",
2873
- inputHash: "",
2874
- questionsHash: "",
2875
- geometryStatus: "absent",
2876
- levelsValid: null,
2877
- model: config.provider?.model ?? config.modelSha256 ?? "",
2878
- scores: emptyScores(),
2879
- confidence: emptyScores()
2880
- };
2881
- }
2882
- signal.assessment = assessment;
2883
- signal.additionalIndicators = {
2884
- ...signal.additionalIndicators,
2885
- jev: (0, import_jev4.jevAssessmentFeatures)(assessment)
2886
- };
2887
- const identity = {
2888
- signalId: signal.signalId,
2889
- strategy: signal.strategy,
2890
- symbol: signal.symbol,
2891
- timestamp: signal.timestamp,
2892
- scope,
2893
- user: (0, import_jev5.jevHash)(userName),
2894
- config,
2895
- assessment
2896
- };
2897
- await (0, import_jev5.writeJevArtifact)(
2898
- import_node_path.default.resolve(
2899
- projectRoot,
2900
- config.recordsDir ?? "data/ai/jev",
2901
- "decisions",
2902
- `${(0, import_jev5.jevHash)(identity)}.json`
2903
- ),
2904
- identity
2905
- );
2906
- return assessment;
2907
- };
2908
-
2909
- // src/strategyRuntime.ts
2910
- var import_constants5 = require("@tradejs/core/constants");
2911
- var import_strategies6 = require("@tradejs/core/strategies");
2912
- var import_logger11 = require("@tradejs/infra/logger");
2913
-
2914
- // src/strategyHelpers/runtime.ts
2915
- var import_logger9 = require("@tradejs/infra/logger");
2916
- var import_constants3 = require("@tradejs/core/constants");
2917
- var import_ml2 = require("@tradejs/infra/ml");
2918
-
2919
- // src/strategy/entrySubmissionValidation.ts
2920
- var import_logger2 = require("@tradejs/infra/logger");
2921
-
2922
- // src/strategy/entryExecutionValidation.ts
2923
- var EntrySubmissionValidationError = class extends Error {
2924
- constructor() {
2925
- super(...arguments);
2926
- this.name = "EntrySubmissionValidationError";
2927
- }
2928
- };
2929
- var resolveEntryExecutionLimits = (config, direction) => {
2930
- const side = config[direction];
2931
- const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
2932
- const maximum = config.MAX_LOSS_VALUE;
2933
- if (minimum == null && maximum == null) return void 0;
2934
- const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
2935
- const maxLossValue = maximum == null ? void 0 : Number(maximum);
2936
- const feeRate = Number(config.RISK_FEE_RATE ?? 0);
2937
- const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
2938
- const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
2939
- const slippageBps = slippage + impact;
2940
- if (minimumNetRiskRatio != null && (!Number.isFinite(minimumNetRiskRatio) || minimumNetRiskRatio < 0) || maxLossValue != null && (!Number.isFinite(maxLossValue) || maxLossValue <= 0) || !Number.isFinite(feeRate) || feeRate < 0 || feeRate >= 1 || !Number.isFinite(slippage) || slippage < 0 || !Number.isFinite(impact) || impact < 0 || !Number.isFinite(slippageBps) || slippageBps < 0 || slippageBps >= 1e4)
2941
- throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
2942
- return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
2943
- };
2944
- var validateEntryExecution = ({
2945
- direction,
2946
- price,
2947
- qty,
2948
- stopLossPrice,
2949
- takeProfitPrice,
2950
- limits
2951
- }) => {
2952
- if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
2953
- throw new EntrySubmissionValidationError(
2954
- "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2955
- );
2956
- const side = direction === "LONG" ? 1 : -1;
2957
- if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
2958
- throw new EntrySubmissionValidationError(
2959
- "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
2960
- );
2961
- const slippage = limits.slippageBps / 1e4;
2962
- const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
2963
- const entryCost = cost(price, side);
2964
- const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
2965
- const rewardPerUnit = Math.max(
2966
- 0,
2967
- Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
2968
- );
2969
- const netRiskRatio = rewardPerUnit / lossPerUnit;
2970
- const plannedLossValue = qty * lossPerUnit;
2971
- if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
2972
- throw new EntrySubmissionValidationError(
2973
- "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2974
- );
2975
- if (limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio)
2976
- throw new EntrySubmissionValidationError(
2977
- "ENTRY_RR_BELOW_MIN_AT_SUBMISSION"
2978
- );
2979
- const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
2980
- return { netRiskRatio, plannedLossValue, riskExceeded };
2981
- };
2982
-
2983
- // src/strategy/entrySubmissionValidation.ts
2984
- var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
2985
- var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
2986
- var getOrderArrivalSnapshot = async ({
2987
- connector,
2988
- symbol,
2989
- requireTimestamp = false
2990
- }) => {
2991
- if (typeof connector.getTopOfBookTicker !== "function") {
2992
- return {
2993
- arrivalSnapshotTime: Date.now(),
2994
- arrivalSource: "unavailable",
2995
- bid: null,
2996
- ask: null,
2997
- arrivalMid: null,
2998
- spreadBps: null
2999
- };
3000
- }
3001
- try {
3002
- const ticker = await connector.getTopOfBookTicker(symbol);
3003
- const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
3004
- const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
3005
- const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
3006
- const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
3007
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
3008
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
3009
- return {
3010
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
3011
- arrivalSource: "top_of_book",
3012
- bid,
3013
- ask,
3014
- arrivalMid,
3015
- spreadBps
3016
- };
3017
- } catch (error) {
3018
- import_logger2.logger.warn(
3019
- "runtime order arrival snapshot failed: %s %s",
3020
- symbol,
3021
- error?.message || String(error)
3022
- );
3023
- return {
3024
- arrivalSnapshotTime: Date.now(),
3025
- arrivalSource: "top_of_book_error",
3026
- bid: null,
3027
- ask: null,
3028
- arrivalMid: null,
3029
- spreadBps: null
3030
- };
3031
- }
3032
- };
3033
- var validateEntryProtectionAtArrival = ({
3034
- direction,
3035
- signalPrice,
3036
- bid,
3037
- ask,
3038
- arrivalMid,
3039
- takeProfits,
3040
- stopLossPrice
3041
- }) => {
3042
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
3043
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
3044
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
3045
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
3046
- }
3047
- for (const takeProfit of takeProfits) {
3048
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
3049
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
3050
- }
3051
- }
3052
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
3053
- throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
3054
- }
3055
- };
3056
- var createEntrySubmissionValidation = ({
3057
- connector,
3058
- symbol,
3059
- direction,
3060
- takeProfits,
3061
- stopLossPrice,
3062
- limits,
3063
- onValidated
3064
- }) => {
3065
- if (!limits) return void 0;
3066
- if (!connector.capabilities?.orderSubmissionValidation)
3067
- throw new EntrySubmissionValidationError(
3068
- "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
3069
- );
3070
- return async ({ qty, executionPrice }) => {
3071
- const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
3072
- const snapshot2 = simulated && executionPrice != null ? {
3073
- arrivalSnapshotTime: Date.now(),
3074
- arrivalSource: "simulation",
3075
- bid: executionPrice,
3076
- ask: executionPrice,
3077
- arrivalMid: executionPrice,
3078
- spreadBps: 0
3079
- } : await getOrderArrivalSnapshot({
3080
- connector,
3081
- symbol,
3082
- requireTimestamp: true
3083
- });
3084
- const age = Date.now() - snapshot2.arrivalSnapshotTime;
3085
- if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
3086
- throw new EntrySubmissionValidationError(
3087
- "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
3088
- );
3089
- if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
3090
- throw new EntrySubmissionValidationError(
3091
- "ENTRY_QUOTE_STALE_AT_SUBMISSION"
3092
- );
3093
- const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
3094
- validateEntryProtectionAtArrival({
3095
- direction,
3096
- signalPrice: price,
3097
- bid: snapshot2.bid,
3098
- ask: snapshot2.ask,
3099
- arrivalMid: snapshot2.arrivalMid,
3100
- takeProfits,
3101
- stopLossPrice
3102
- });
3103
- const prices = takeProfits.map((tp) => tp.price);
3104
- const economics2 = validateEntryExecution({
3105
- direction,
3106
- price,
3107
- qty,
3108
- stopLossPrice,
3109
- takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
3110
- limits
3111
- });
3112
- onValidated?.(snapshot2, economics2);
3113
- };
3114
- };
3115
-
3116
- // src/strategyAdapters/ml.ts
3117
- var defaultMlAdapter = {
3118
- normalizeStrategyConfig: (strategyConfig) => strategyConfig
3119
- };
3120
- var getStrategyMlAdapter = (strategy, profileId) => {
3121
- const strategyAdapter = getStrategyProfileMlAdapter(
3122
- getStrategyManifest(strategy),
3123
- profileId
2702
+ var askAI = async (signal, options = {}) => {
2703
+ const { symbol } = signal;
2704
+ const payload = buildAiPayload(signal);
2705
+ const content = await runAiPrompt(
2706
+ {
2707
+ systemPrompt: buildAiSystemPrompt(signal),
2708
+ humanPrompt: buildAiHumanPrompt(signal, payload)
2709
+ },
2710
+ {
2711
+ ...options,
2712
+ signal,
2713
+ payload
2714
+ }
3124
2715
  );
3125
- if (!strategyAdapter) return defaultMlAdapter;
3126
- return {
3127
- ...defaultMlAdapter,
3128
- ...strategyAdapter
3129
- };
2716
+ await (0, import_redis.setData)(import_redis.redisKeys.analysis(symbol, signal.signalId), content);
2717
+ return content;
3130
2718
  };
3131
2719
 
3132
- // src/mlPayload.ts
3133
- var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
3134
- return getStrategyMlAdapter(
3135
- strategyName,
3136
- profileId
3137
- ).normalizeStrategyConfig?.(strategyConfig);
3138
- };
3139
- var buildMlPayload = (payload) => {
3140
- const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
3141
- const profileId = payload.signal?.policyProfileId;
3142
- const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
3143
- const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
3144
- const nextSignal = {
3145
- ...normalizedSignal,
3146
- indicators: {
3147
- ...normalizedSignal?.indicators ?? {}
2720
+ // src/strategy/portfolioRuntime.ts
2721
+ var import_node_crypto2 = require("crypto");
2722
+ var import_portfolio2 = require("@tradejs/core/portfolio");
2723
+ var import_redis3 = require("@tradejs/infra/redis");
2724
+
2725
+ // src/portfolio.ts
2726
+ var import_portfolio = require("@tradejs/core/portfolio");
2727
+ function createPortfolioCoordinator({
2728
+ policy,
2729
+ strategy,
2730
+ store,
2731
+ execution
2732
+ }) {
2733
+ const fingerprint3 = (0, import_portfolio.portfolioFingerprint)(policy, strategy);
2734
+ let cache;
2735
+ async function transact(now2, change, force = false) {
2736
+ for (let attempt = 0; attempt < 16; attempt++) {
2737
+ const previous = await store.read();
2738
+ if (previous && previous.fingerprint !== fingerprint3)
2739
+ throw new Error(
2740
+ "Portfolio policy changed under existing state; reconcile the persisted scope before changing configuration"
2741
+ );
2742
+ const base = previous ?? (0, import_portfolio.createPortfolioState)(fingerprint3);
2743
+ const bucket = Math.floor(now2 / policy.observationIntervalMs);
2744
+ const observation = !force && cache?.revision === base.revision && cache.bucket === bucket ? cache.observation : await execution.observe(now2, base.reservations);
2745
+ const reconciled = (0, import_portfolio.reconcilePortfolio)(
2746
+ policy,
2747
+ strategy,
2748
+ base,
2749
+ observation
2750
+ );
2751
+ for (const intent of base.closing) {
2752
+ if (!observation.positions.some(
2753
+ (p) => p.symbol === intent.position.symbol
2754
+ ))
2755
+ await execution.confirmClosed?.(intent, observation.asOf);
2756
+ }
2757
+ const changed = change(reconciled, observation);
2758
+ const next = { ...changed.state, revision: base.revision + 1 };
2759
+ if (await store.compare(previous, next)) {
2760
+ cache = { revision: next.revision, bucket, observation };
2761
+ return { state: next, observation, result: changed.result };
2762
+ }
2763
+ cache = void 0;
2764
+ }
2765
+ throw new Error("Portfolio state contention; entry was not submitted");
2766
+ }
2767
+ async function tick(now2, force = false) {
2768
+ const claimed = await transact(
2769
+ now2,
2770
+ (state, observation) => {
2771
+ const commands = (0, import_portfolio.portfolioCloseCommands)(policy, state, observation);
2772
+ return {
2773
+ state: {
2774
+ ...state,
2775
+ closing: state.closing.map(
2776
+ (c) => commands.some((x) => x.intent.positionId === c.positionId) ? { ...c, lastAttemptAt: observation.asOf } : c
2777
+ )
2778
+ },
2779
+ result: commands
2780
+ };
2781
+ },
2782
+ force
2783
+ );
2784
+ for (const command of claimed.result)
2785
+ await execution.close(
2786
+ command.position,
2787
+ command.intent,
2788
+ claimed.observation.asOf
2789
+ );
2790
+ if (claimed.result.length) {
2791
+ cache = void 0;
2792
+ const refreshed = await transact(
2793
+ now2,
2794
+ (state) => ({ state, result: void 0 }),
2795
+ true
2796
+ );
2797
+ return (0, import_portfolio.buildPortfolioContext)(
2798
+ policy,
2799
+ strategy,
2800
+ refreshed.state,
2801
+ refreshed.observation
2802
+ );
2803
+ }
2804
+ return (0, import_portfolio.buildPortfolioContext)(
2805
+ policy,
2806
+ strategy,
2807
+ claimed.state,
2808
+ claimed.observation
2809
+ );
2810
+ }
2811
+ async function updateReservation(now2, id, submitted, qty) {
2812
+ return transact(
2813
+ now2,
2814
+ (s, o) => {
2815
+ const exists = s.reservations.some((r) => r.id === id);
2816
+ const blocked = submitted && (!exists || s.closing.length > 0 || o.asOf <= s.blockedThrough);
2817
+ return {
2818
+ state: {
2819
+ ...s,
2820
+ reservations: s.reservations.flatMap(
2821
+ (r) => r.id !== id ? [r] : submitted && !blocked ? [{ ...r, submitted: true, qty: qty ?? r.qty }] : []
2822
+ )
2823
+ },
2824
+ result: !blocked
2825
+ };
2826
+ },
2827
+ true
2828
+ );
2829
+ }
2830
+ return {
2831
+ async context(now2) {
2832
+ const state = await store.read() ?? (0, import_portfolio.createPortfolioState)(fingerprint3);
2833
+ if (state.fingerprint !== fingerprint3)
2834
+ throw new Error("Portfolio policy changed under existing state");
2835
+ const observation = await execution.observe(now2, state.reservations);
2836
+ (0, import_portfolio.validatePortfolioObservation)(observation);
2837
+ return (0, import_portfolio.buildPortfolioContext)(policy, strategy, state, observation);
2838
+ },
2839
+ tick,
2840
+ async submit(now2, entry, send) {
2841
+ await tick(now2);
2842
+ const admitted = await transact(
2843
+ now2,
2844
+ (s, o) => {
2845
+ const r = (0, import_portfolio.reservePortfolioEntry)(policy, strategy, s, o, {
2846
+ ...entry,
2847
+ submitted: false,
2848
+ oppositeHandled: false
2849
+ });
2850
+ return { state: r.state, result: r.reason };
2851
+ },
2852
+ true
2853
+ );
2854
+ if (admitted.result) return { reason: admitted.result };
2855
+ let submitted = false;
2856
+ try {
2857
+ const value = await send(
2858
+ async (qty) => {
2859
+ if (!Number.isFinite(qty) || qty <= 0)
2860
+ throw new Error("Invalid submitted portfolio quantity");
2861
+ submitted = true;
2862
+ const update = await updateReservation(now2, entry.id, true, qty);
2863
+ if (!update.result)
2864
+ throw new Error("PORTFOLIO_SUBMISSION_NO_LONGER_ADMITTED");
2865
+ },
2866
+ async () => {
2867
+ await transact(
2868
+ now2,
2869
+ (s) => ({
2870
+ state: {
2871
+ ...s,
2872
+ reservations: s.reservations.filter((r) => r.id !== entry.id),
2873
+ completedSignals: {
2874
+ ...s.completedSignals,
2875
+ [entry.signalId]: now2
2876
+ }
2877
+ },
2878
+ result: void 0
2879
+ }),
2880
+ true
2881
+ );
2882
+ }
2883
+ );
2884
+ if (!submitted) await updateReservation(now2, entry.id, false);
2885
+ else await tick(now2, true);
2886
+ return { value };
2887
+ } catch (error) {
2888
+ if (!submitted) await updateReservation(now2, entry.id, false);
2889
+ throw error;
2890
+ }
3148
2891
  }
3149
2892
  };
3150
- const nextContext = payload.context ? {
3151
- ...payload.context,
3152
- strategyConfig: normalizeStrategyConfig(
3153
- payload.context.strategyConfig,
3154
- strategyName,
3155
- profileId
3156
- )
3157
- } : void 0;
2893
+ }
2894
+ function createMemoryPortfolioStore() {
2895
+ let state = null;
3158
2896
  return {
3159
- signal: nextSignal,
3160
- context: nextContext
2897
+ read: async () => state === null ? null : structuredClone(state),
2898
+ compare: async (expected, next) => {
2899
+ if ((state?.revision ?? -1) !== (expected?.revision ?? -1)) return false;
2900
+ state = structuredClone(next);
2901
+ return true;
2902
+ }
3161
2903
  };
3162
- };
2904
+ }
3163
2905
 
3164
2906
  // src/runtimeJournal.ts
3165
2907
  var import_node_crypto = require("crypto");
3166
2908
  var import_constants = require("@tradejs/core/constants");
3167
2909
  var import_time = require("@tradejs/core/time");
3168
2910
  var import_trade = require("@tradejs/core/trade");
3169
- var import_logger3 = require("@tradejs/infra/logger");
2911
+ var import_logger2 = require("@tradejs/infra/logger");
3170
2912
  var import_redis2 = require("@tradejs/infra/redis");
3171
2913
  var now = () => Date.now();
3172
2914
  var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
@@ -3218,195 +2960,988 @@ var recordRuntimeTradeOpen = async (params) => {
3218
2960
  record,
3219
2961
  { expire: 0 }
3220
2962
  ),
3221
- (0, import_redis2.setData)(
3222
- import_redis2.redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
3223
- { orderId: record.orderId },
3224
- { expire: 0 }
2963
+ (0, import_redis2.setData)(
2964
+ import_redis2.redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
2965
+ { orderId: record.orderId },
2966
+ { expire: 0 }
2967
+ )
2968
+ ]);
2969
+ } catch (error) {
2970
+ import_logger2.logger.error(
2971
+ "runtime trade open journal failed: %s %s",
2972
+ record.symbol,
2973
+ error?.message || String(error)
2974
+ );
2975
+ }
2976
+ return record;
2977
+ };
2978
+ var recordRuntimeTradeIncrease = async (params) => {
2979
+ const {
2980
+ userName,
2981
+ strategy,
2982
+ symbol,
2983
+ direction,
2984
+ resultingQty,
2985
+ resultingEntryPrice,
2986
+ addedQty,
2987
+ addedEntryPrice,
2988
+ entryTimestamp,
2989
+ fee,
2990
+ accountId,
2991
+ deploymentId
2992
+ } = params;
2993
+ if (!userName) {
2994
+ return null;
2995
+ }
2996
+ const existing = await getActiveRuntimeTrade({
2997
+ userName,
2998
+ symbol,
2999
+ accountId,
3000
+ deploymentId
3001
+ });
3002
+ if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
3003
+ return null;
3004
+ }
3005
+ const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
3006
+ const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
3007
+ const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
3008
+ const next = {
3009
+ ...existing,
3010
+ ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
3011
+ qty: resultingQty,
3012
+ entryPrice: resultingEntryPrice,
3013
+ entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
3014
+ lastEntryPrice: addedEntryPrice,
3015
+ lastEntryQty: addedQty,
3016
+ lastEntryTimestamp: entryTimestamp,
3017
+ currentPrice: resultingEntryPrice,
3018
+ currentPnl: 0,
3019
+ fee: openFee,
3020
+ openFee,
3021
+ totalFee,
3022
+ lastSyncedAt: now()
3023
+ };
3024
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3025
+ try {
3026
+ await Promise.all([
3027
+ (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, existing.orderId), next, {
3028
+ expire: 0
3029
+ }),
3030
+ (0, import_redis2.setHashJsonField)(
3031
+ import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3032
+ existing.orderId,
3033
+ next,
3034
+ { expire: 0 }
3035
+ )
3036
+ ]);
3037
+ } catch (error) {
3038
+ import_logger2.logger.error(
3039
+ "runtime trade increase journal failed: %s %s",
3040
+ symbol,
3041
+ error?.message || String(error)
3042
+ );
3043
+ }
3044
+ return next;
3045
+ };
3046
+ var getActiveRuntimeTrade = async (params) => {
3047
+ const { userName, symbol, accountId, deploymentId } = params;
3048
+ if (!userName) {
3049
+ return null;
3050
+ }
3051
+ const activeRef = await (0, import_redis2.getData)(
3052
+ import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
3053
+ null
3054
+ );
3055
+ const orderId = String(activeRef?.orderId || "").trim();
3056
+ if (!orderId) {
3057
+ return null;
3058
+ }
3059
+ const existing = await (0, import_redis2.getData)(
3060
+ import_redis2.redisKeys.runtimeTrade(userName, orderId),
3061
+ null
3062
+ );
3063
+ if (!existing || existing.status !== "active") {
3064
+ await (0, import_redis2.delKey)(
3065
+ import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
3066
+ );
3067
+ return null;
3068
+ }
3069
+ return existing;
3070
+ };
3071
+ var markRuntimeTradeClosed = async (params) => {
3072
+ const {
3073
+ userName,
3074
+ expectedOrderId,
3075
+ symbol,
3076
+ strategy,
3077
+ exitPrice,
3078
+ exitTimestamp,
3079
+ closedPnl,
3080
+ exitType,
3081
+ accountId,
3082
+ deploymentId
3083
+ } = params;
3084
+ if (!userName) {
3085
+ return null;
3086
+ }
3087
+ const existing = expectedOrderId ? await (0, import_redis2.getDataStrict)(
3088
+ import_redis2.redisKeys.runtimeTrade(userName, expectedOrderId)
3089
+ ) : await getActiveRuntimeTrade({
3090
+ userName,
3091
+ symbol,
3092
+ accountId,
3093
+ deploymentId
3094
+ });
3095
+ if (!existing || expectedOrderId && existing.orderId !== expectedOrderId) {
3096
+ return null;
3097
+ }
3098
+ const orderId = existing.orderId;
3099
+ if (strategy && existing.strategy !== strategy) {
3100
+ return null;
3101
+ }
3102
+ const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
3103
+ const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
3104
+ direction: existing.direction,
3105
+ entryPrice: existing.entryPrice,
3106
+ exitPrice: resolvedExitPrice,
3107
+ qty: existing.qty
3108
+ }) : existing.closedPnl ?? existing.currentPnl ?? null;
3109
+ const next = expectedOrderId && existing.status === "closed" ? existing : {
3110
+ ...existing,
3111
+ status: "closed",
3112
+ currentPrice: resolvedExitPrice,
3113
+ currentPnl: resolvedClosedPnl,
3114
+ closedPnl: resolvedClosedPnl,
3115
+ exitPrice: resolvedExitPrice,
3116
+ exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
3117
+ exitType: exitType ?? existing.exitType ?? null,
3118
+ lastSyncedAt: now()
3119
+ };
3120
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3121
+ const closeDayKey = (0, import_time.getRuntimeStorageDayKey)(next.exitTimestamp);
3122
+ try {
3123
+ await Promise.all([
3124
+ (expectedOrderId ? import_redis2.setDataStrict : import_redis2.setData)(
3125
+ import_redis2.redisKeys.runtimeTrade(userName, orderId),
3126
+ next,
3127
+ {
3128
+ expire: import_constants.TTL_1M
3129
+ }
3130
+ ),
3131
+ (expectedOrderId ? import_redis2.setHashJsonFieldStrict : import_redis2.setHashJsonField)(
3132
+ import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3133
+ orderId,
3134
+ next,
3135
+ { expire: import_constants.TTL_1M }
3136
+ ),
3137
+ (expectedOrderId ? import_redis2.setHashJsonFieldStrict : import_redis2.setHashJsonField)(
3138
+ import_redis2.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
3139
+ orderId,
3140
+ next,
3141
+ { expire: import_constants.TTL_1M }
3142
+ ),
3143
+ expectedOrderId ? (0, import_redis2.deleteRuntimeTradeReference)(
3144
+ import_redis2.redisKeys.runtimeActiveTrade(
3145
+ userName,
3146
+ symbol,
3147
+ deploymentId ?? accountId
3148
+ ),
3149
+ expectedOrderId
3150
+ ) : (0, import_redis2.delKey)(
3151
+ import_redis2.redisKeys.runtimeActiveTrade(
3152
+ userName,
3153
+ symbol,
3154
+ deploymentId ?? accountId
3155
+ )
3225
3156
  )
3226
3157
  ]);
3227
3158
  } catch (error) {
3228
- import_logger3.logger.error(
3229
- "runtime trade open journal failed: %s %s",
3230
- record.symbol,
3159
+ if (expectedOrderId) throw error;
3160
+ import_logger2.logger.error(
3161
+ "runtime trade close journal failed: %s %s",
3162
+ symbol,
3231
3163
  error?.message || String(error)
3232
3164
  );
3233
3165
  }
3234
- return record;
3166
+ return next;
3235
3167
  };
3236
- var recordRuntimeTradeIncrease = async (params) => {
3237
- const {
3168
+
3169
+ // src/strategy/portfolioRuntime.ts
3170
+ var coordinators = /* @__PURE__ */ new WeakMap();
3171
+ async function observeRuntimePortfolio(connector, binding, asOf, reservations, observedNow = Date.now) {
3172
+ if (!connector.getOpenPositionPnl || !connector.getEntryExecutions)
3173
+ throw new Error(
3174
+ "Portfolio policies require position marks and entry executions"
3175
+ );
3176
+ const orders = await Promise.all(
3177
+ reservations.map(async (r) => {
3178
+ if (r.submitted && connector.getEntryOrderObservation) {
3179
+ const fact2 = await connector.getEntryOrderObservation({
3180
+ symbol: r.symbol,
3181
+ orderId: r.id
3182
+ });
3183
+ if (fact2) return fact2;
3184
+ }
3185
+ const executions = r.submitted ? await connector.getEntryExecutions({
3186
+ symbol: r.symbol,
3187
+ startTime: r.timestamp,
3188
+ endTime: asOf
3189
+ }) : [];
3190
+ const filledQty = executions.filter(
3191
+ (e) => (e.orderLinkId === r.id || e.orderId === r.id) && e.direction === r.direction
3192
+ ).reduce((sum, e) => sum + e.qty, 0);
3193
+ return {
3194
+ id: r.id,
3195
+ status: filledQty >= r.qty ? "filled" : filledQty > 0 ? "partial" : "pending",
3196
+ filledQty,
3197
+ positionId: r.id
3198
+ };
3199
+ })
3200
+ );
3201
+ const snapshots2 = await connector.getOpenPositionPnl();
3202
+ const positions = await Promise.all(
3203
+ snapshots2.map(async (p) => {
3204
+ const reserved = reservations.find(
3205
+ (r) => r.symbol === p.symbol && r.direction === p.direction && orders.some(
3206
+ (o) => o.id === r.id && o.filledQty > 0 && p.qty <= o.filledQty * (1 + 1e-8)
3207
+ )
3208
+ );
3209
+ const trade = await getActiveRuntimeTrade({
3210
+ ...binding,
3211
+ symbol: p.symbol
3212
+ });
3213
+ const owner = trade && trade.direction === p.direction && p.qty <= trade.qty * (1 + 1e-8) && Math.abs(p.price - (trade.actualEntryPrice ?? trade.entryPrice)) <= p.price * 1e-8 ? trade : null;
3214
+ return {
3215
+ id: reserved?.id ?? owner?.orderId ?? `unknown:${p.symbol}:${p.direction}`,
3216
+ strategy: reserved?.strategy ?? owner?.strategy ?? null,
3217
+ symbol: p.symbol,
3218
+ direction: p.direction,
3219
+ qty: p.qty,
3220
+ entryPrice: p.price,
3221
+ markPrice: p.currentPrice,
3222
+ markedAt: p.observedAt ?? null,
3223
+ stopLossPrice: p.slPrice ?? p.stopLossPrice ?? null
3224
+ };
3225
+ })
3226
+ );
3227
+ return { asOf: Math.max(asOf, observedNow()), positions, orders };
3228
+ }
3229
+ function persistentStore(key) {
3230
+ const raw = /* @__PURE__ */ new WeakMap();
3231
+ return {
3232
+ async read() {
3233
+ const value = await import_redis3.portfolioStateStorage.read(key);
3234
+ if (value === null) return null;
3235
+ const state = JSON.parse(value);
3236
+ (0, import_portfolio2.assertPortfolioState)(state);
3237
+ raw.set(state, value);
3238
+ return state;
3239
+ },
3240
+ async compare(previous, next) {
3241
+ const expected = previous === null ? null : raw.get(previous);
3242
+ if (expected === void 0)
3243
+ throw new Error("Portfolio CAS requires a storage snapshot");
3244
+ return import_redis3.portfolioStateStorage.compare(key, expected, JSON.stringify(next));
3245
+ }
3246
+ };
3247
+ }
3248
+ function getPortfolioRuntime({
3249
+ value,
3250
+ connectorName,
3251
+ strategy,
3252
+ connector,
3253
+ env,
3254
+ userName,
3255
+ accountId,
3256
+ deploymentId
3257
+ }) {
3258
+ const policy = (0, import_portfolio2.parsePortfolioPolicy)(value);
3259
+ if (!policy) return void 0;
3260
+ const replay = (env === "BACKTEST" || env === "PARITY") && Reflect.get(connector, "__tradejsReplayConnector") === true;
3261
+ if ((env === "BACKTEST" || env === "PARITY") && (!replay || !connector.getPortfolioObservation))
3262
+ throw new Error(
3263
+ "Portfolio policies require shared portfolio replay; isolated symbol backtests are unsupported"
3264
+ );
3265
+ if (!replay && (!userName || !accountId || !deploymentId || !connector.getOpenPositionPnl || !connector.getEntryExecutions))
3266
+ throw new Error(
3267
+ "Portfolio runtime requires exact user/account/deployment binding, marks and executions"
3268
+ );
3269
+ const binding = {
3238
3270
  userName,
3239
- strategy,
3240
- symbol,
3241
- direction,
3242
- resultingQty,
3243
- resultingEntryPrice,
3244
- addedQty,
3245
- addedEntryPrice,
3246
- entryTimestamp,
3247
- fee,
3248
3271
  accountId,
3249
3272
  deploymentId
3250
- } = params;
3251
- if (!userName) {
3252
- return null;
3273
+ };
3274
+ const scope = policy.scope === "strategy" ? strategy : policy.scope === "account" ? "account" : [...policy.scope.strategies].sort();
3275
+ const key = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify([binding, connectorName, connector.universe, scope])).digest("hex");
3276
+ const fingerprint3 = (0, import_portfolio2.portfolioFingerprint)(policy, strategy);
3277
+ const cacheKey = key;
3278
+ let registry = coordinators.get(connector);
3279
+ if (!registry) {
3280
+ registry = /* @__PURE__ */ new Map();
3281
+ coordinators.set(connector, registry);
3282
+ }
3283
+ const existing = registry.get(cacheKey);
3284
+ if (existing) {
3285
+ if (existing.fingerprint !== fingerprint3)
3286
+ throw new Error("Conflicting portfolio configuration for the same scope");
3287
+ return existing.coordinator;
3288
+ }
3289
+ const coordinator = createPortfolioCoordinator({
3290
+ policy,
3291
+ strategy,
3292
+ store: replay ? createMemoryPortfolioStore() : persistentStore(key),
3293
+ execution: {
3294
+ async confirmClosed(intent, asOf) {
3295
+ if (replay) return;
3296
+ const position = intent.position;
3297
+ await markRuntimeTradeClosed({
3298
+ ...binding,
3299
+ strategy: position.strategy,
3300
+ expectedOrderId: position.id,
3301
+ symbol: position.symbol,
3302
+ exitPrice: position.markPrice,
3303
+ exitTimestamp: asOf,
3304
+ exitType: "exit"
3305
+ });
3306
+ },
3307
+ observe: (asOf, reservations) => replay ? connector.getPortfolioObservation(asOf, reservations) : observeRuntimePortfolio(connector, binding, asOf, reservations),
3308
+ async close(position, intent, asOf) {
3309
+ if (!replay) {
3310
+ const trade = await getActiveRuntimeTrade({
3311
+ ...binding,
3312
+ symbol: position.symbol
3313
+ });
3314
+ if (!trade || trade.orderId !== position.id)
3315
+ throw new Error("Portfolio close ownership changed");
3316
+ }
3317
+ await connector.closePosition({
3318
+ symbol: position.symbol,
3319
+ direction: position.direction,
3320
+ price: position.markPrice,
3321
+ timestamp: asOf,
3322
+ signal: {
3323
+ signalId: `${position.id}:${intent.reason}`,
3324
+ strategy: position.strategy,
3325
+ symbol: position.symbol,
3326
+ direction: position.direction,
3327
+ interval: "1",
3328
+ timestamp: asOf,
3329
+ prices: {
3330
+ riskRatio: 0,
3331
+ currentPrice: position.markPrice,
3332
+ takeProfitPrice: position.markPrice,
3333
+ stopLossPrice: position.markPrice
3334
+ },
3335
+ figures: {},
3336
+ indicators: {},
3337
+ additionalIndicators: { portfolioExitReason: intent.reason }
3338
+ }
3339
+ });
3340
+ }
3341
+ }
3342
+ });
3343
+ registry.set(cacheKey, { fingerprint: fingerprint3, coordinator });
3344
+ return coordinator;
3345
+ }
3346
+
3347
+ // src/strategyHelpers/tickerProfiles.ts
3348
+ var import_tickerProfiles = require("@tradejs/infra/tickerProfiles");
3349
+ var import_tickerProfiles2 = require("@tradejs/core/tickerProfiles");
3350
+ var snapshots = /* @__PURE__ */ new Map();
3351
+ var resolveTickerProfileSource = (strategy) => {
3352
+ const declared = getStrategyManifest(strategy)?.contextRequirements?.tickerProfiles;
3353
+ if (declared) return declared;
3354
+ const provider = process.env.TICKER_PROFILES_PROVIDER?.trim();
3355
+ const universeId = process.env.TICKER_PROFILES_UNIVERSE?.trim();
3356
+ if (!provider && !universeId) return void 0;
3357
+ if (!provider || !universeId)
3358
+ throw new Error(
3359
+ "Set both TICKER_PROFILES_PROVIDER and TICKER_PROFILES_UNIVERSE"
3360
+ );
3361
+ return { provider, universeId };
3362
+ };
3363
+ var loadTickerProfileContext = async ({
3364
+ projectRoot,
3365
+ provider,
3366
+ universeId,
3367
+ symbol,
3368
+ timestamp
3369
+ }) => {
3370
+ const asOf = (0, import_tickerProfiles2.tickerProfileMonth)(timestamp), key = JSON.stringify([projectRoot, provider, universeId, asOf]);
3371
+ let cached = snapshots.get(key);
3372
+ if (!cached || cached.expires < Date.now()) {
3373
+ const value = (0, import_tickerProfiles.readTickerProfiles)(projectRoot, provider, universeId, asOf).then((envelope) => {
3374
+ if (!envelope) return null;
3375
+ const s = envelope.snapshot;
3376
+ if (!(0, import_tickerProfiles2.isTickerProfileSnapshot)(s) || s.asOf !== asOf || s.provider !== provider || s.universeId !== universeId)
3377
+ throw new Error("Ticker profile identity/schema mismatch");
3378
+ const entry = snapshots.get(key);
3379
+ if (entry) entry.expires = Infinity;
3380
+ return new Map(
3381
+ s.profiles.map((p) => [
3382
+ p.symbol,
3383
+ Object.freeze({
3384
+ ...p,
3385
+ definition: s.definition,
3386
+ provider,
3387
+ universeId,
3388
+ snapshotId: envelope.snapshotId
3389
+ })
3390
+ ])
3391
+ );
3392
+ }).catch((error) => {
3393
+ snapshots.delete(key);
3394
+ throw error;
3395
+ });
3396
+ cached = { expires: Date.now() + 6e4, value };
3397
+ snapshots.set(key, cached);
3398
+ if (snapshots.size > 24) {
3399
+ const oldest = snapshots.keys().next().value;
3400
+ if (oldest) snapshots.delete(oldest);
3401
+ }
3402
+ }
3403
+ return (await cached.value)?.get(symbol);
3404
+ };
3405
+ var withTickerProfile = (baseContext, profile) => Object.defineProperty(
3406
+ Object.create(
3407
+ Object.getPrototypeOf(baseContext),
3408
+ Object.getOwnPropertyDescriptors(baseContext)
3409
+ ),
3410
+ "tickerProfile",
3411
+ { enumerable: true, configurable: true, value: profile }
3412
+ );
3413
+ var enrichSignalWithTickerProfile = async (signal) => {
3414
+ const source = resolveTickerProfileSource(signal.strategy);
3415
+ if (!source || signal.universe === "tradfi" || !signal.symbol.endsWith("USDT"))
3416
+ return;
3417
+ const profile = await loadTickerProfileContext({
3418
+ ...source,
3419
+ projectRoot: getTradejsProjectCwd(),
3420
+ symbol: signal.symbol,
3421
+ timestamp: signal.timestamp
3422
+ });
3423
+ if (profile) {
3424
+ signal.additionalIndicators = {
3425
+ ...signal.additionalIndicators,
3426
+ tickerProfile: profile
3427
+ };
3428
+ }
3429
+ };
3430
+
3431
+ // src/strategyRuntime.ts
3432
+ var import_jev6 = require("@tradejs/core/jev");
3433
+
3434
+ // src/jev.ts
3435
+ var import_promises = __toESM(require("fs/promises"));
3436
+ var import_node_path = __toESM(require("path"));
3437
+ var import_jev4 = require("@tradejs/core/jev");
3438
+ var import_jev5 = require("@tradejs/infra/jev");
3439
+ var import_userSettings2 = require("@tradejs/infra/userSettings");
3440
+ init_jevInput();
3441
+ init_jevInput();
3442
+ init_jevTraining();
3443
+ var resolveJevProvider = async (userName) => {
3444
+ const settings = await (0, import_userSettings2.getUserSettings)(userName);
3445
+ const provider = {
3446
+ endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
3447
+ model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
3448
+ };
3449
+ (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
3450
+ if (!settings.JEV_API_KEY)
3451
+ throw new Error("Configure the Jev API key in Account settings");
3452
+ return provider;
3453
+ };
3454
+ var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
3455
+ var pending = /* @__PURE__ */ new Map();
3456
+ var pendingResponses = /* @__PURE__ */ new Map();
3457
+ var evaluateJevInput = async ({
3458
+ input,
3459
+ config,
3460
+ userName,
3461
+ projectRoot
3462
+ }) => {
3463
+ config = (0, import_jev4.parseJevConfig)(config);
3464
+ const inputHash = (0, import_jev5.jevHash)(input);
3465
+ const questions = questionsForJevInput(input);
3466
+ const questionsHash = (0, import_jev5.jevHash)(questions);
3467
+ const base = {
3468
+ questionSet: import_jev4.JEV_QUESTION_SET,
3469
+ schema: "tradejs-signal-assessment/v5",
3470
+ source: config.source,
3471
+ mode: config.mode,
3472
+ inputHash,
3473
+ questionsHash,
3474
+ geometryStatus: input.geometryStatus,
3475
+ levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
3476
+ };
3477
+ if (!input.questions.length)
3478
+ return {
3479
+ assessment: {
3480
+ ...base,
3481
+ status: "unavailable",
3482
+ model: "",
3483
+ scores: emptyScores(),
3484
+ confidence: emptyScores()
3485
+ }
3486
+ };
3487
+ if (config.source === "local") {
3488
+ const contents = await import_promises.default.readFile(
3489
+ import_node_path.default.resolve(projectRoot, config.modelFile),
3490
+ "utf8"
3491
+ );
3492
+ if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
3493
+ throw new Error("Local Jev gate checksum mismatch");
3494
+ const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
3495
+ const model = validateJevGateModel2(JSON.parse(contents));
3496
+ if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
3497
+ throw new Error("Local Jev gate strategy or question version mismatch");
3498
+ const scores = Object.fromEntries(
3499
+ import_jev4.JEV_DIMENSIONS.map((key) => [
3500
+ key,
3501
+ input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
3502
+ ])
3503
+ );
3504
+ return {
3505
+ assessment: {
3506
+ ...base,
3507
+ status: "available",
3508
+ model: config.modelSha256,
3509
+ scores,
3510
+ confidence: emptyScores()
3511
+ }
3512
+ };
3513
+ }
3514
+ const provider = config.provider ?? await resolveJevProvider(userName);
3515
+ const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
3516
+ const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
3517
+ const file = import_node_path.default.join(dir, "records", `${id}.json`);
3518
+ const getRecord = async () => {
3519
+ const existing = await (0, import_jev5.readJevArtifact)(file);
3520
+ if (existing) {
3521
+ if (existing.schema !== "tradejs-jev-record/v4" || existing.id !== id || existing.inputHash !== inputHash || existing.questionsHash !== questionsHash || (0, import_jev5.jevHash)(existing.input) !== inputHash || (0, import_jev5.jevHash)(existing.provider) !== (0, import_jev5.jevHash)(provider))
3522
+ throw new Error("Jev recording identity mismatch");
3523
+ (0, import_jev5.validateJevResponse)(existing.response, input.questions);
3524
+ for (const key of import_jev4.JEV_DIMENSIONS) {
3525
+ const expected = input.questions.includes(key) ? existing.response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null;
3526
+ if (existing.scores[key] !== expected)
3527
+ throw new Error("Jev recording scores do not match the response");
3528
+ }
3529
+ return existing;
3530
+ }
3531
+ if (config.source === "recorded")
3532
+ throw new Error(
3533
+ `Missing Jev recording ${id} for ${input.strategy} ${input.symbol} at ${new Date(input.timestamp).toISOString()}. Recorded mode requires the same signal-time facts and provider; a shorter backtest can change warm-up indicators or prior trade state.`
3534
+ );
3535
+ const settings = await (0, import_userSettings2.getUserSettings)(userName);
3536
+ if (!settings.JEV_API_KEY)
3537
+ throw new Error("Configure the Jev API key in Account settings");
3538
+ const apiKey = settings.JEV_API_KEY;
3539
+ const configuredProvider = await resolveJevProvider(userName);
3540
+ if (configuredProvider.endpoint !== provider.endpoint)
3541
+ throw new Error(
3542
+ "Jev account endpoint changed; update the frozen provider configuration"
3543
+ );
3544
+ const started = Date.now();
3545
+ const state = stateForJevInput(input);
3546
+ const responseId = (0, import_jev5.jevHash)({
3547
+ state,
3548
+ questionsHash,
3549
+ provider,
3550
+ user: (0, import_jev5.jevHash)(userName)
3551
+ });
3552
+ const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
3553
+ let responsePromise = pendingResponses.get(responseFile);
3554
+ if (!responsePromise) {
3555
+ responsePromise = (async () => {
3556
+ const saved = await (0, import_jev5.readJevArtifact)(responseFile);
3557
+ if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
3558
+ const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
3559
+ await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
3560
+ return fresh;
3561
+ })();
3562
+ pendingResponses.set(responseFile, responsePromise);
3563
+ }
3564
+ let response;
3565
+ try {
3566
+ response = await responsePromise;
3567
+ } finally {
3568
+ pendingResponses.delete(responseFile);
3569
+ }
3570
+ const scores = Object.fromEntries(
3571
+ import_jev4.JEV_DIMENSIONS.map((key) => [
3572
+ key,
3573
+ input.questions.includes(key) ? response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null
3574
+ ])
3575
+ );
3576
+ const value = {
3577
+ schema: "tradejs-jev-record/v4",
3578
+ id,
3579
+ inputHash,
3580
+ questionsHash,
3581
+ provider,
3582
+ input,
3583
+ response,
3584
+ scores,
3585
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3586
+ elapsedMs: Date.now() - started
3587
+ };
3588
+ await (0, import_jev5.writeJevArtifact)(file, value);
3589
+ return await (0, import_jev5.readJevArtifact)(file);
3590
+ };
3591
+ const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
3592
+ let promise = pending.get(pendingKey);
3593
+ if (!promise) {
3594
+ promise = getRecord();
3595
+ pending.set(pendingKey, promise);
3253
3596
  }
3254
- const existing = await getActiveRuntimeTrade({
3255
- userName,
3256
- symbol,
3257
- accountId,
3258
- deploymentId
3259
- });
3260
- if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
3261
- return null;
3597
+ let record;
3598
+ try {
3599
+ record = await promise;
3600
+ } finally {
3601
+ pending.delete(pendingKey);
3262
3602
  }
3263
- const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
3264
- const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
3265
- const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
3266
- const next = {
3267
- ...existing,
3268
- ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
3269
- qty: resultingQty,
3270
- entryPrice: resultingEntryPrice,
3271
- entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
3272
- lastEntryPrice: addedEntryPrice,
3273
- lastEntryQty: addedQty,
3274
- lastEntryTimestamp: entryTimestamp,
3275
- currentPrice: resultingEntryPrice,
3276
- currentPnl: 0,
3277
- fee: openFee,
3278
- openFee,
3279
- totalFee,
3280
- lastSyncedAt: now()
3603
+ return {
3604
+ record,
3605
+ assessment: {
3606
+ ...base,
3607
+ status: "available",
3608
+ recordId: id,
3609
+ model: record.response.model,
3610
+ scores: record.scores,
3611
+ confidence: Object.fromEntries(
3612
+ import_jev4.JEV_DIMENSIONS.map((key) => [
3613
+ key,
3614
+ record.response.answers[key]?.confidence ?? null
3615
+ ])
3616
+ )
3617
+ }
3281
3618
  };
3282
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3619
+ };
3620
+ var assessSignalWithJev = async ({
3621
+ signal,
3622
+ config,
3623
+ userName,
3624
+ projectRoot,
3625
+ strict,
3626
+ scope = "runtime"
3627
+ }) => {
3628
+ let assessment;
3283
3629
  try {
3284
- await Promise.all([
3285
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, existing.orderId), next, {
3286
- expire: 0
3287
- }),
3288
- (0, import_redis2.setHashJsonField)(
3289
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3290
- existing.orderId,
3291
- next,
3292
- { expire: 0 }
3293
- )
3294
- ]);
3630
+ const input = buildJevInput(signal);
3631
+ ({ assessment } = await evaluateJevInput({
3632
+ input,
3633
+ config,
3634
+ userName,
3635
+ projectRoot
3636
+ }));
3295
3637
  } catch (error) {
3296
- import_logger3.logger.error(
3297
- "runtime trade increase journal failed: %s %s",
3298
- symbol,
3299
- error?.message || String(error)
3300
- );
3301
- }
3302
- return next;
3303
- };
3304
- var getActiveRuntimeTrade = async (params) => {
3305
- const { userName, symbol, accountId, deploymentId } = params;
3306
- if (!userName) {
3307
- return null;
3308
- }
3309
- const activeRef = await (0, import_redis2.getData)(
3310
- import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
3311
- null
3312
- );
3313
- const orderId = String(activeRef?.orderId || "").trim();
3314
- if (!orderId) {
3315
- return null;
3638
+ if (strict) throw error;
3639
+ assessment = {
3640
+ questionSet: import_jev4.JEV_QUESTION_SET,
3641
+ schema: "tradejs-signal-assessment/v5",
3642
+ source: config.source,
3643
+ mode: config.mode,
3644
+ status: "unavailable",
3645
+ inputHash: "",
3646
+ questionsHash: "",
3647
+ geometryStatus: "absent",
3648
+ levelsValid: null,
3649
+ model: config.provider?.model ?? config.modelSha256 ?? "",
3650
+ scores: emptyScores(),
3651
+ confidence: emptyScores()
3652
+ };
3316
3653
  }
3317
- const existing = await (0, import_redis2.getData)(
3318
- import_redis2.redisKeys.runtimeTrade(userName, orderId),
3319
- null
3654
+ signal.assessment = assessment;
3655
+ signal.additionalIndicators = {
3656
+ ...signal.additionalIndicators,
3657
+ jev: (0, import_jev4.jevAssessmentFeatures)(assessment)
3658
+ };
3659
+ const identity = {
3660
+ signalId: signal.signalId,
3661
+ strategy: signal.strategy,
3662
+ symbol: signal.symbol,
3663
+ timestamp: signal.timestamp,
3664
+ scope,
3665
+ user: (0, import_jev5.jevHash)(userName),
3666
+ config,
3667
+ assessment
3668
+ };
3669
+ await (0, import_jev5.writeJevArtifact)(
3670
+ import_node_path.default.resolve(
3671
+ projectRoot,
3672
+ config.recordsDir ?? "data/ai/jev",
3673
+ "decisions",
3674
+ `${(0, import_jev5.jevHash)(identity)}.json`
3675
+ ),
3676
+ identity
3320
3677
  );
3321
- if (!existing || existing.status !== "active") {
3322
- await (0, import_redis2.delKey)(
3323
- import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
3324
- );
3325
- return null;
3326
- }
3327
- return existing;
3678
+ return assessment;
3328
3679
  };
3329
- var markRuntimeTradeClosed = async (params) => {
3330
- const {
3331
- userName,
3332
- symbol,
3333
- strategy,
3334
- exitPrice,
3335
- exitTimestamp,
3336
- closedPnl,
3337
- exitType,
3338
- accountId,
3339
- deploymentId
3340
- } = params;
3341
- if (!userName) {
3342
- return null;
3343
- }
3344
- const existing = await getActiveRuntimeTrade({
3345
- userName,
3346
- symbol,
3347
- accountId,
3348
- deploymentId
3349
- });
3350
- if (!existing) {
3351
- return null;
3352
- }
3353
- const orderId = existing.orderId;
3354
- if (strategy && existing.strategy !== strategy) {
3355
- return null;
3680
+
3681
+ // src/strategyRuntime.ts
3682
+ var import_constants5 = require("@tradejs/core/constants");
3683
+ var import_strategies6 = require("@tradejs/core/strategies");
3684
+ var import_logger11 = require("@tradejs/infra/logger");
3685
+
3686
+ // src/strategyHelpers/runtime.ts
3687
+ var import_logger9 = require("@tradejs/infra/logger");
3688
+ var import_constants3 = require("@tradejs/core/constants");
3689
+ var import_ml2 = require("@tradejs/infra/ml");
3690
+
3691
+ // src/strategy/entrySubmissionValidation.ts
3692
+ var import_logger3 = require("@tradejs/infra/logger");
3693
+
3694
+ // src/strategy/entryExecutionValidation.ts
3695
+ var EntrySubmissionValidationError = class extends Error {
3696
+ constructor() {
3697
+ super(...arguments);
3698
+ this.name = "EntrySubmissionValidationError";
3356
3699
  }
3357
- const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
3358
- const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
3359
- direction: existing.direction,
3360
- entryPrice: existing.entryPrice,
3361
- exitPrice: resolvedExitPrice,
3362
- qty: existing.qty
3363
- }) : existing.closedPnl ?? existing.currentPnl ?? null;
3364
- const next = {
3365
- ...existing,
3366
- status: "closed",
3367
- currentPrice: resolvedExitPrice,
3368
- currentPnl: resolvedClosedPnl,
3369
- closedPnl: resolvedClosedPnl,
3370
- exitPrice: resolvedExitPrice,
3371
- exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
3372
- exitType: exitType ?? existing.exitType ?? null,
3373
- lastSyncedAt: now()
3700
+ };
3701
+ var resolveEntryExecutionLimits = (config, direction) => {
3702
+ const side = config[direction];
3703
+ const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
3704
+ const maximum = config.MAX_LOSS_VALUE;
3705
+ if (minimum == null && maximum == null) return void 0;
3706
+ const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
3707
+ const maxLossValue = maximum == null ? void 0 : Number(maximum);
3708
+ const feeRate = Number(config.RISK_FEE_RATE ?? 0);
3709
+ const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
3710
+ const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
3711
+ const slippageBps = slippage + impact;
3712
+ if (minimumNetRiskRatio != null && (!Number.isFinite(minimumNetRiskRatio) || minimumNetRiskRatio < 0) || maxLossValue != null && (!Number.isFinite(maxLossValue) || maxLossValue <= 0) || !Number.isFinite(feeRate) || feeRate < 0 || feeRate >= 1 || !Number.isFinite(slippage) || slippage < 0 || !Number.isFinite(impact) || impact < 0 || !Number.isFinite(slippageBps) || slippageBps < 0 || slippageBps >= 1e4)
3713
+ throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
3714
+ return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
3715
+ };
3716
+ var validateEntryExecution = ({
3717
+ direction,
3718
+ price,
3719
+ qty,
3720
+ stopLossPrice,
3721
+ takeProfitPrice,
3722
+ limits
3723
+ }) => {
3724
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
3725
+ throw new EntrySubmissionValidationError(
3726
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
3727
+ );
3728
+ const side = direction === "LONG" ? 1 : -1;
3729
+ if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
3730
+ throw new EntrySubmissionValidationError(
3731
+ "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
3732
+ );
3733
+ const slippage = limits.slippageBps / 1e4;
3734
+ const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
3735
+ const entryCost = cost(price, side);
3736
+ const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
3737
+ const rewardPerUnit = Math.max(
3738
+ 0,
3739
+ Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
3740
+ );
3741
+ const netRiskRatio = rewardPerUnit / lossPerUnit;
3742
+ const plannedLossValue = qty * lossPerUnit;
3743
+ if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
3744
+ throw new EntrySubmissionValidationError(
3745
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
3746
+ );
3747
+ const belowMinimumNetRiskRatio = limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio;
3748
+ const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
3749
+ return {
3750
+ netRiskRatio,
3751
+ plannedLossValue,
3752
+ riskExceeded,
3753
+ belowMinimumNetRiskRatio
3374
3754
  };
3375
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3376
- const closeDayKey = (0, import_time.getRuntimeStorageDayKey)(next.exitTimestamp);
3755
+ };
3756
+
3757
+ // src/strategy/entrySubmissionValidation.ts
3758
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
3759
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
3760
+ var getOrderArrivalSnapshot = async ({
3761
+ connector,
3762
+ symbol,
3763
+ requireTimestamp = false
3764
+ }) => {
3765
+ if (typeof connector.getTopOfBookTicker !== "function") {
3766
+ return {
3767
+ arrivalSnapshotTime: Date.now(),
3768
+ arrivalSource: "unavailable",
3769
+ bid: null,
3770
+ ask: null,
3771
+ arrivalMid: null,
3772
+ spreadBps: null
3773
+ };
3774
+ }
3377
3775
  try {
3378
- await Promise.all([
3379
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, orderId), next, {
3380
- expire: import_constants.TTL_1M
3381
- }),
3382
- (0, import_redis2.setHashJsonField)(
3383
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3384
- orderId,
3385
- next,
3386
- { expire: import_constants.TTL_1M }
3387
- ),
3388
- (0, import_redis2.setHashJsonField)(
3389
- import_redis2.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
3390
- orderId,
3391
- next,
3392
- { expire: import_constants.TTL_1M }
3393
- ),
3394
- (0, import_redis2.delKey)(
3395
- import_redis2.redisKeys.runtimeActiveTrade(
3396
- userName,
3397
- symbol,
3398
- deploymentId ?? accountId
3399
- )
3400
- )
3401
- ]);
3776
+ const ticker = await connector.getTopOfBookTicker(symbol);
3777
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
3778
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
3779
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
3780
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
3781
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
3782
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
3783
+ return {
3784
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
3785
+ arrivalSource: "top_of_book",
3786
+ bid,
3787
+ ask,
3788
+ arrivalMid,
3789
+ spreadBps
3790
+ };
3402
3791
  } catch (error) {
3403
- import_logger3.logger.error(
3404
- "runtime trade close journal failed: %s %s",
3792
+ import_logger3.logger.warn(
3793
+ "runtime order arrival snapshot failed: %s %s",
3405
3794
  symbol,
3406
3795
  error?.message || String(error)
3407
3796
  );
3797
+ return {
3798
+ arrivalSnapshotTime: Date.now(),
3799
+ arrivalSource: "top_of_book_error",
3800
+ bid: null,
3801
+ ask: null,
3802
+ arrivalMid: null,
3803
+ spreadBps: null
3804
+ };
3408
3805
  }
3409
- return next;
3806
+ };
3807
+ var validateEntryProtectionAtArrival = ({
3808
+ direction,
3809
+ signalPrice,
3810
+ bid,
3811
+ ask,
3812
+ arrivalMid,
3813
+ takeProfits,
3814
+ stopLossPrice
3815
+ }) => {
3816
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
3817
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
3818
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
3819
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
3820
+ }
3821
+ for (const takeProfit of takeProfits) {
3822
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
3823
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
3824
+ }
3825
+ }
3826
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
3827
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
3828
+ }
3829
+ };
3830
+ var createEntrySubmissionValidation = ({
3831
+ connector,
3832
+ symbol,
3833
+ direction,
3834
+ takeProfits,
3835
+ stopLossPrice,
3836
+ limits,
3837
+ onValidated
3838
+ }) => {
3839
+ if (!limits) return void 0;
3840
+ if (!connector.capabilities?.orderSubmissionValidation)
3841
+ throw new EntrySubmissionValidationError(
3842
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
3843
+ );
3844
+ return async ({ qty, executionPrice }) => {
3845
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
3846
+ const snapshot2 = simulated && executionPrice != null ? {
3847
+ arrivalSnapshotTime: Date.now(),
3848
+ arrivalSource: "simulation",
3849
+ bid: executionPrice,
3850
+ ask: executionPrice,
3851
+ arrivalMid: executionPrice,
3852
+ spreadBps: 0
3853
+ } : await getOrderArrivalSnapshot({
3854
+ connector,
3855
+ symbol,
3856
+ requireTimestamp: true
3857
+ });
3858
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
3859
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
3860
+ throw new EntrySubmissionValidationError(
3861
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
3862
+ );
3863
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
3864
+ throw new EntrySubmissionValidationError(
3865
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
3866
+ );
3867
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
3868
+ validateEntryProtectionAtArrival({
3869
+ direction,
3870
+ signalPrice: price,
3871
+ bid: snapshot2.bid,
3872
+ ask: snapshot2.ask,
3873
+ arrivalMid: snapshot2.arrivalMid,
3874
+ takeProfits,
3875
+ stopLossPrice
3876
+ });
3877
+ const prices = takeProfits.map((tp) => tp.price);
3878
+ const economics2 = validateEntryExecution({
3879
+ direction,
3880
+ price,
3881
+ qty,
3882
+ stopLossPrice,
3883
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
3884
+ limits
3885
+ });
3886
+ if (economics2.belowMinimumNetRiskRatio) {
3887
+ import_logger3.logger.warn(
3888
+ "entry submission warning: %s %s ENTRY_RR_BELOW_MIN_AT_SUBMISSION (RR %s, threshold %s)",
3889
+ symbol,
3890
+ direction,
3891
+ economics2.netRiskRatio,
3892
+ limits.minimumNetRiskRatio
3893
+ );
3894
+ }
3895
+ onValidated?.(snapshot2, economics2);
3896
+ };
3897
+ };
3898
+
3899
+ // src/strategyAdapters/ml.ts
3900
+ var defaultMlAdapter = {
3901
+ normalizeStrategyConfig: (strategyConfig) => strategyConfig
3902
+ };
3903
+ var getStrategyMlAdapter = (strategy, profileId) => {
3904
+ const strategyAdapter = getStrategyProfileMlAdapter(
3905
+ getStrategyManifest(strategy),
3906
+ profileId
3907
+ );
3908
+ if (!strategyAdapter) return defaultMlAdapter;
3909
+ return {
3910
+ ...defaultMlAdapter,
3911
+ ...strategyAdapter
3912
+ };
3913
+ };
3914
+
3915
+ // src/mlPayload.ts
3916
+ var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
3917
+ return getStrategyMlAdapter(
3918
+ strategyName,
3919
+ profileId
3920
+ ).normalizeStrategyConfig?.(strategyConfig);
3921
+ };
3922
+ var buildMlPayload = (payload) => {
3923
+ const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
3924
+ const profileId = payload.signal?.policyProfileId;
3925
+ const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
3926
+ const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
3927
+ const nextSignal = {
3928
+ ...normalizedSignal,
3929
+ indicators: {
3930
+ ...normalizedSignal?.indicators ?? {}
3931
+ }
3932
+ };
3933
+ const nextContext = payload.context ? {
3934
+ ...payload.context,
3935
+ strategyConfig: normalizeStrategyConfig(
3936
+ payload.context.strategyConfig,
3937
+ strategyName,
3938
+ profileId
3939
+ )
3940
+ } : void 0;
3941
+ return {
3942
+ signal: nextSignal,
3943
+ context: nextContext
3944
+ };
3410
3945
  };
3411
3946
 
3412
3947
  // src/strategyHelpers/marketContextStages.ts
@@ -3418,7 +3953,7 @@ var import_logger4 = require("@tradejs/infra/logger");
3418
3953
  var import_strategies = require("@tradejs/core/strategies");
3419
3954
 
3420
3955
  // src/binanceBreadthUniverses.ts
3421
- var import_node_crypto2 = require("crypto");
3956
+ var import_node_crypto3 = require("crypto");
3422
3957
 
3423
3958
  // src/config/binanceBreadthUniverses.json
3424
3959
  var binanceBreadthUniverses_default = {
@@ -3657,7 +4192,7 @@ var BINANCE_BREADTH_UNIVERSE_KEYS = [
3657
4192
  "top50",
3658
4193
  "top100"
3659
4194
  ];
3660
- var fingerprint = (value, length) => (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
4195
+ var fingerprint = (value, length) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
3661
4196
  var normalizeSymbols = (symbols) => symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean);
3662
4197
  var buildBinanceBreadthUniverseSnapshot = ({
3663
4198
  rankedSymbols,
@@ -4833,7 +5368,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4833
5368
  };
4834
5369
 
4835
5370
  // src/strategyHelpers/derivativesContext.ts
4836
- var import_indicators2 = require("@tradejs/core/indicators");
5371
+ var import_indicators3 = require("@tradejs/core/indicators");
4837
5372
  var import_data2 = require("@tradejs/core/data");
4838
5373
  var import_strategies3 = require("@tradejs/core/strategies");
4839
5374
  var import_constants2 = require("@tradejs/core/constants");
@@ -4867,7 +5402,7 @@ var parseLookbackMs = () => {
4867
5402
  };
4868
5403
  var withHourlyFallbackRows = (rowsByInterval) => ({
4869
5404
  "15m": rowsByInterval["15m"] ?? [],
4870
- "1h": (0, import_indicators2.buildCoinalyzeHourlyRowsWithFallback)({
5405
+ "1h": (0, import_indicators3.buildCoinalyzeHourlyRowsWithFallback)({
4871
5406
  rows15m: rowsByInterval["15m"],
4872
5407
  fallbackRows1h: rowsByInterval["1h"]
4873
5408
  })
@@ -4995,7 +5530,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4995
5530
  const targetSymbol = normalizeSymbol(signal.symbol);
4996
5531
  const lookbackMs = parseLookbackMs();
4997
5532
  const decisionTimeMs = signal.timestamp + (0, import_data2.intervalToMs)(signal.interval);
4998
- const derivativesEndMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
5533
+ const derivativesEndMs = (0, import_indicators3.getLastClosedDerivativesBarStartMs)(
4999
5534
  decisionTimeMs,
5000
5535
  "15m"
5001
5536
  );
@@ -5010,7 +5545,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
5010
5545
  });
5011
5546
  return [
5012
5547
  symbol,
5013
- (0, import_indicators2.buildDerivativesContext)({
5548
+ (0, import_indicators3.buildDerivativesContext)({
5014
5549
  symbol,
5015
5550
  direction: signal.direction,
5016
5551
  timestamp: derivativesEndMs,
@@ -5037,7 +5572,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
5037
5572
  lookbackMs,
5038
5573
  ...params.abortSignal ? { signal: params.abortSignal } : {}
5039
5574
  });
5040
- const context = (0, import_indicators2.buildDerivativesContext)({
5575
+ const context = (0, import_indicators3.buildDerivativesContext)({
5041
5576
  symbol: targetSymbol,
5042
5577
  direction: signal.direction,
5043
5578
  timestamp: derivativesEndMs,
@@ -5085,7 +5620,7 @@ var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWha
5085
5620
  var import_logger7 = require("@tradejs/infra/logger");
5086
5621
 
5087
5622
  // src/hyperliquidWhaleUniverse.ts
5088
- var import_node_crypto3 = require("crypto");
5623
+ var import_node_crypto4 = require("crypto");
5089
5624
 
5090
5625
  // src/config/hyperliquidPerpUniverse.json
5091
5626
  var hyperliquidPerpUniverse_default = {
@@ -5257,7 +5792,7 @@ var hyperliquidWhales_default = {
5257
5792
  };
5258
5793
 
5259
5794
  // src/hyperliquidWhaleUniverse.ts
5260
- var fingerprint2 = (value) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
5795
+ var fingerprint2 = (value) => (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
5261
5796
  var validateUnique = (values, label) => {
5262
5797
  if (new Set(values).size !== values.length) {
5263
5798
  throw new Error(`Hyperliquid ${label} snapshot contains duplicates`);
@@ -5748,6 +6283,7 @@ var enrichSignalWithMarketContextStages = async ({
5748
6283
  })
5749
6284
  });
5750
6285
  }
6286
+ await enrichSignalWithTickerProfile(signal);
5751
6287
  const results = [];
5752
6288
  for (const stage of stages) {
5753
6289
  if (abortSignal?.aborted) break;
@@ -5795,12 +6331,12 @@ var findReplayAiAnalysis = ({
5795
6331
  direction,
5796
6332
  ai
5797
6333
  }) => {
5798
- const snapshots = ai?.replayAnalyses;
5799
- if (!Array.isArray(snapshots) || !snapshots.length) {
6334
+ const snapshots2 = ai?.replayAnalyses;
6335
+ if (!Array.isArray(snapshots2) || !snapshots2.length) {
5800
6336
  return void 0;
5801
6337
  }
5802
6338
  let best = null;
5803
- for (const snapshot2 of snapshots) {
6339
+ for (const snapshot2 of snapshots2) {
5804
6340
  if (snapshot2.symbol !== signal.symbol || snapshot2.direction !== direction || snapshot2.strategy && snapshot2.strategy !== signal.strategy) {
5805
6341
  continue;
5806
6342
  }
@@ -5973,7 +6509,9 @@ var executeEntryOrder = async ({
5973
6509
  beforePlaceOrder,
5974
6510
  recordRuntimeTrade = true,
5975
6511
  leverage,
5976
- executionLimits
6512
+ executionLimits,
6513
+ markPortfolioSubmitted,
6514
+ rejectPortfolioSubmission
5977
6515
  }) => {
5978
6516
  await beforePlaceOrder?.();
5979
6517
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
@@ -6046,7 +6584,13 @@ var executeEntryOrder = async ({
6046
6584
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
6047
6585
  orderId,
6048
6586
  signal,
6049
- ...beforeSubmit ? { beforeSubmit } : {}
6587
+ ...rejectPortfolioSubmission ? { onRejected: rejectPortfolioSubmission } : {},
6588
+ ...beforeSubmit || markPortfolioSubmitted ? {
6589
+ beforeSubmit: async (context) => {
6590
+ await beforeSubmit?.(context);
6591
+ await markPortfolioSubmitted?.(context.qty);
6592
+ }
6593
+ } : {}
6050
6594
  });
6051
6595
  const orderAckTime = Date.now();
6052
6596
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -7034,7 +7578,7 @@ var handleProtectDecision = async ({
7034
7578
  }
7035
7579
  return decision.code;
7036
7580
  };
7037
- var executeEntryDecision = async ({
7581
+ var executeAdmittedEntryDecision = async ({
7038
7582
  connector,
7039
7583
  symbol,
7040
7584
  decision,
@@ -7048,7 +7592,9 @@ var executeEntryDecision = async ({
7048
7592
  ai,
7049
7593
  recordRuntimeJournal,
7050
7594
  invokeStageHooks,
7051
- notifyRuntimeError
7595
+ notifyRuntimeError,
7596
+ markPortfolioSubmitted,
7597
+ rejectPortfolioSubmission
7052
7598
  }) => {
7053
7599
  const signal = decision.signal;
7054
7600
  const beforePlaceOrder = async () => {
@@ -7095,6 +7641,8 @@ var executeEntryDecision = async ({
7095
7641
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
7096
7642
  signal,
7097
7643
  beforePlaceOrder,
7644
+ markPortfolioSubmitted,
7645
+ rejectPortfolioSubmission,
7098
7646
  recordRuntimeTrade: recordRuntimeJournal,
7099
7647
  executionLimits: resolveEntryExecutionLimits(
7100
7648
  hookCtx.strategyConfig,
@@ -7235,6 +7783,37 @@ var executeEntryDecision = async ({
7235
7783
  }
7236
7784
  return signal ?? decision.code;
7237
7785
  };
7786
+ async function executeEntryDecision(params) {
7787
+ if (!params.portfolio) return executeAdmittedEntryDecision(params);
7788
+ const { decision, portfolio } = params;
7789
+ const signal = decision.signal;
7790
+ if (!signal || decision.orderPlan.positionIntent === "increase")
7791
+ return "PORTFOLIO_REQUIRES_NEW_SIGNAL_POSITION";
7792
+ signal.orderId ??= createRuntimeOrderId(signal.strategy);
7793
+ const result = await portfolio.submit(
7794
+ params.portfolioTime ?? decision.entryContext.timestamp,
7795
+ {
7796
+ id: signal.orderId,
7797
+ signalId: signal.signalId,
7798
+ strategy: signal.strategy,
7799
+ symbol: params.symbol,
7800
+ direction: decision.entryContext.direction,
7801
+ qty: decision.orderPlan.qty,
7802
+ timestamp: decision.entryContext.timestamp
7803
+ },
7804
+ (markPortfolioSubmitted, rejectPortfolioSubmission) => executeAdmittedEntryDecision({
7805
+ ...params,
7806
+ markPortfolioSubmitted,
7807
+ rejectPortfolioSubmission
7808
+ })
7809
+ );
7810
+ if (result.reason) {
7811
+ signal.orderStatus = "failed";
7812
+ signal.orderFailureReason = result.reason;
7813
+ return signal;
7814
+ }
7815
+ return result.value;
7816
+ }
7238
7817
 
7239
7818
  // src/strategyRuntime.ts
7240
7819
  var cloneWithPropertyDescriptors = (value) => Object.create(
@@ -7262,6 +7841,7 @@ var createStrategyRuntime = ({
7262
7841
  };
7263
7842
  const creator = async ({
7264
7843
  userName,
7844
+ connectorName,
7265
7845
  config: baseConfig,
7266
7846
  symbol,
7267
7847
  universe: requestedUniverse,
@@ -7300,6 +7880,17 @@ var createStrategyRuntime = ({
7300
7880
  const projectConfig = await loadTradejsConfig(projectRoot);
7301
7881
  const projectHooks = projectConfig.hooks;
7302
7882
  const env = String(config.ENV ?? "BACKTEST");
7883
+ const portfolio = getPortfolioRuntime({
7884
+ connectorName,
7885
+ value: baseConfig?.PORTFOLIO_POLICY ?? config.PORTFOLIO_POLICY,
7886
+ strategy: strategyName,
7887
+ connector,
7888
+ env,
7889
+ userName,
7890
+ accountId,
7891
+ deploymentId
7892
+ });
7893
+ const portfolioContext = portfolio ? (now2) => isRuntimeOrderExecutionEnabled({ config, env, connector }) ? portfolio.tick(now2) : portfolio.context(now2) : void 0;
7303
7894
  const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
7304
7895
  const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
7305
7896
  if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
@@ -7598,6 +8189,20 @@ var createStrategyRuntime = ({
7598
8189
  interval: decisionInterval
7599
8190
  }) => {
7600
8191
  if (!baseContext) return void 0;
8192
+ const profileSource = strategyManifest?.contextRequirements?.tickerProfiles;
8193
+ if (profileSource) {
8194
+ const profile = await loadTickerProfileContext({
8195
+ ...profileSource,
8196
+ projectRoot,
8197
+ symbol: decisionSymbol,
8198
+ timestamp: candle.timestamp
8199
+ });
8200
+ if (!profile)
8201
+ throw new Error(
8202
+ "Required ticker profile is unavailable at decision time"
8203
+ );
8204
+ baseContext = withTickerProfile(baseContext, profile);
8205
+ }
7601
8206
  if (!coreContextRequirements.has("hyperliquidWhales")) {
7602
8207
  return baseContext;
7603
8208
  }
@@ -7765,6 +8370,8 @@ var createStrategyRuntime = ({
7765
8370
  runtime: pending2.runtime
7766
8371
  });
7767
8372
  return executeEntryDecision({
8373
+ portfolio,
8374
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
7768
8375
  connector,
7769
8376
  symbol,
7770
8377
  decision: pending2.decision,
@@ -7782,6 +8389,10 @@ var createStrategyRuntime = ({
7782
8389
  });
7783
8390
  };
7784
8391
  const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
8392
+ if (portfolioContext)
8393
+ await portfolioContext(
8394
+ env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now()
8395
+ );
7785
8396
  appendCurrentMarketData(candle, btcCandle, options.ethCandle);
7786
8397
  const ethCandle = resolveEthCandle(candle, options.ethCandle);
7787
8398
  indicatorsState.setCurrentBar(candle, btcCandle, ethCandle);
@@ -7952,6 +8563,13 @@ var createStrategyRuntime = ({
7952
8563
  });
7953
8564
  const signal = decision.signal;
7954
8565
  if (signal) {
8566
+ if (portfolio)
8567
+ signal.additionalIndicators = {
8568
+ ...signal.additionalIndicators,
8569
+ portfolioContext: await portfolioContext(
8570
+ env === "BACKTEST" || env === "PARITY" ? decision.entryContext.timestamp : Date.now()
8571
+ )
8572
+ };
7955
8573
  if (runtimeLineage) signal.runtimeLineage = runtimeLineage;
7956
8574
  if (universe) signal.universe = universe;
7957
8575
  if (assetClass) signal.assetClass = assetClass;
@@ -8191,6 +8809,8 @@ var createStrategyRuntime = ({
8191
8809
  });
8192
8810
  }
8193
8811
  return executeEntryDecision({
8812
+ portfolio,
8813
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
8194
8814
  connector,
8195
8815
  symbol,
8196
8816
  decision,