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

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?.({
@@ -2647,526 +2708,240 @@ var askAI = async (signal, options = {}) => {
2647
2708
  humanPrompt: buildAiHumanPrompt(signal, payload)
2648
2709
  },
2649
2710
  {
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
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
+ let retry = 0;
2857
+ let attemptId = entry.id;
2858
+ let needsReservation = false;
2859
+ try {
2860
+ const value = await send(
2861
+ async (qty) => {
2862
+ if (!Number.isFinite(qty) || qty <= 0)
2863
+ throw new Error("Invalid submitted portfolio quantity");
2864
+ if (needsReservation) {
2865
+ attemptId = `${entry.id.slice(0, 30)}r${++retry}`;
2866
+ const admission = await transact(
2867
+ now2,
2868
+ (s, o) => {
2869
+ const r = (0, import_portfolio.reservePortfolioEntry)(policy, strategy, s, o, {
2870
+ ...entry,
2871
+ id: attemptId,
2872
+ qty,
2873
+ submitted: true,
2874
+ oppositeHandled: false
2875
+ });
2876
+ return { state: r.state, result: r.reason };
2877
+ },
2878
+ true
2879
+ );
2880
+ if (admission.result) throw new Error(admission.result);
2881
+ needsReservation = false;
2882
+ submitted = true;
2883
+ return attemptId;
2884
+ }
2885
+ const update = await updateReservation(now2, attemptId, true, qty);
2886
+ if (!update.result)
2887
+ throw new Error("PORTFOLIO_SUBMISSION_NO_LONGER_ADMITTED");
2888
+ submitted = true;
2889
+ return attemptId;
2890
+ },
2891
+ async (retrying = false) => {
2892
+ await transact(
2893
+ now2,
2894
+ (s) => ({
2895
+ state: {
2896
+ ...s,
2897
+ reservations: s.reservations.filter(
2898
+ (r) => r.id !== attemptId
2899
+ ),
2900
+ completedSignals: {
2901
+ ...Object.fromEntries(
2902
+ Object.entries(s.completedSignals).filter(
2903
+ ([id]) => !retrying || id !== entry.signalId
2904
+ )
2905
+ ),
2906
+ ...retrying ? {} : { [entry.signalId]: now2 }
2907
+ }
2908
+ },
2909
+ result: void 0
2910
+ }),
2911
+ true
2912
+ );
2913
+ submitted = false;
2914
+ needsReservation = retrying;
2915
+ }
2916
+ );
2917
+ if (!submitted) await updateReservation(now2, attemptId, false);
2918
+ else await tick(now2, true);
2919
+ return { value };
2920
+ } catch (error) {
2921
+ if (!submitted) await updateReservation(now2, attemptId, false);
2922
+ throw error;
2923
+ }
3148
2924
  }
3149
2925
  };
3150
- const nextContext = payload.context ? {
3151
- ...payload.context,
3152
- strategyConfig: normalizeStrategyConfig(
3153
- payload.context.strategyConfig,
3154
- strategyName,
3155
- profileId
3156
- )
3157
- } : void 0;
2926
+ }
2927
+ function createMemoryPortfolioStore() {
2928
+ let state = null;
3158
2929
  return {
3159
- signal: nextSignal,
3160
- context: nextContext
2930
+ read: async () => state === null ? null : structuredClone(state),
2931
+ compare: async (expected, next) => {
2932
+ if ((state?.revision ?? -1) !== (expected?.revision ?? -1)) return false;
2933
+ state = structuredClone(next);
2934
+ return true;
2935
+ }
3161
2936
  };
3162
- };
2937
+ }
3163
2938
 
3164
2939
  // src/runtimeJournal.ts
3165
2940
  var import_node_crypto = require("crypto");
3166
2941
  var import_constants = require("@tradejs/core/constants");
3167
2942
  var import_time = require("@tradejs/core/time");
3168
2943
  var import_trade = require("@tradejs/core/trade");
3169
- var import_logger3 = require("@tradejs/infra/logger");
2944
+ var import_logger2 = require("@tradejs/infra/logger");
3170
2945
  var import_redis2 = require("@tradejs/infra/redis");
3171
2946
  var now = () => Date.now();
3172
2947
  var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
@@ -3225,188 +3000,992 @@ var recordRuntimeTradeOpen = async (params) => {
3225
3000
  )
3226
3001
  ]);
3227
3002
  } catch (error) {
3228
- import_logger3.logger.error(
3229
- "runtime trade open journal failed: %s %s",
3230
- record.symbol,
3003
+ import_logger2.logger.error(
3004
+ "runtime trade open journal failed: %s %s",
3005
+ record.symbol,
3006
+ error?.message || String(error)
3007
+ );
3008
+ }
3009
+ return record;
3010
+ };
3011
+ var recordRuntimeTradeIncrease = async (params) => {
3012
+ const {
3013
+ userName,
3014
+ strategy,
3015
+ symbol,
3016
+ direction,
3017
+ resultingQty,
3018
+ resultingEntryPrice,
3019
+ addedQty,
3020
+ addedEntryPrice,
3021
+ entryTimestamp,
3022
+ fee,
3023
+ accountId,
3024
+ deploymentId
3025
+ } = params;
3026
+ if (!userName) {
3027
+ return null;
3028
+ }
3029
+ const existing = await getActiveRuntimeTrade({
3030
+ userName,
3031
+ symbol,
3032
+ accountId,
3033
+ deploymentId
3034
+ });
3035
+ if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
3036
+ return null;
3037
+ }
3038
+ const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
3039
+ const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
3040
+ const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
3041
+ const next = {
3042
+ ...existing,
3043
+ ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
3044
+ qty: resultingQty,
3045
+ entryPrice: resultingEntryPrice,
3046
+ entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
3047
+ lastEntryPrice: addedEntryPrice,
3048
+ lastEntryQty: addedQty,
3049
+ lastEntryTimestamp: entryTimestamp,
3050
+ currentPrice: resultingEntryPrice,
3051
+ currentPnl: 0,
3052
+ fee: openFee,
3053
+ openFee,
3054
+ totalFee,
3055
+ lastSyncedAt: now()
3056
+ };
3057
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3058
+ try {
3059
+ await Promise.all([
3060
+ (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, existing.orderId), next, {
3061
+ expire: 0
3062
+ }),
3063
+ (0, import_redis2.setHashJsonField)(
3064
+ import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3065
+ existing.orderId,
3066
+ next,
3067
+ { expire: 0 }
3068
+ )
3069
+ ]);
3070
+ } catch (error) {
3071
+ import_logger2.logger.error(
3072
+ "runtime trade increase journal failed: %s %s",
3073
+ symbol,
3074
+ error?.message || String(error)
3075
+ );
3076
+ }
3077
+ return next;
3078
+ };
3079
+ var getActiveRuntimeTrade = async (params) => {
3080
+ const { userName, symbol, accountId, deploymentId } = params;
3081
+ if (!userName) {
3082
+ return null;
3083
+ }
3084
+ const activeRef = await (0, import_redis2.getData)(
3085
+ import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
3086
+ null
3087
+ );
3088
+ const orderId = String(activeRef?.orderId || "").trim();
3089
+ if (!orderId) {
3090
+ return null;
3091
+ }
3092
+ const existing = await (0, import_redis2.getData)(
3093
+ import_redis2.redisKeys.runtimeTrade(userName, orderId),
3094
+ null
3095
+ );
3096
+ if (!existing || existing.status !== "active") {
3097
+ await (0, import_redis2.delKey)(
3098
+ import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
3099
+ );
3100
+ return null;
3101
+ }
3102
+ return existing;
3103
+ };
3104
+ var markRuntimeTradeClosed = async (params) => {
3105
+ const {
3106
+ userName,
3107
+ expectedOrderId,
3108
+ symbol,
3109
+ strategy,
3110
+ exitPrice,
3111
+ exitTimestamp,
3112
+ closedPnl,
3113
+ exitType,
3114
+ accountId,
3115
+ deploymentId
3116
+ } = params;
3117
+ if (!userName) {
3118
+ return null;
3119
+ }
3120
+ const existing = expectedOrderId ? await (0, import_redis2.getDataStrict)(
3121
+ import_redis2.redisKeys.runtimeTrade(userName, expectedOrderId)
3122
+ ) : await getActiveRuntimeTrade({
3123
+ userName,
3124
+ symbol,
3125
+ accountId,
3126
+ deploymentId
3127
+ });
3128
+ if (!existing || expectedOrderId && existing.orderId !== expectedOrderId) {
3129
+ return null;
3130
+ }
3131
+ const orderId = existing.orderId;
3132
+ if (strategy && existing.strategy !== strategy) {
3133
+ return null;
3134
+ }
3135
+ const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
3136
+ const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
3137
+ direction: existing.direction,
3138
+ entryPrice: existing.entryPrice,
3139
+ exitPrice: resolvedExitPrice,
3140
+ qty: existing.qty
3141
+ }) : existing.closedPnl ?? existing.currentPnl ?? null;
3142
+ const next = expectedOrderId && existing.status === "closed" ? existing : {
3143
+ ...existing,
3144
+ status: "closed",
3145
+ currentPrice: resolvedExitPrice,
3146
+ currentPnl: resolvedClosedPnl,
3147
+ closedPnl: resolvedClosedPnl,
3148
+ exitPrice: resolvedExitPrice,
3149
+ exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
3150
+ exitType: exitType ?? existing.exitType ?? null,
3151
+ lastSyncedAt: now()
3152
+ };
3153
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3154
+ const closeDayKey = (0, import_time.getRuntimeStorageDayKey)(next.exitTimestamp);
3155
+ try {
3156
+ await Promise.all([
3157
+ (expectedOrderId ? import_redis2.setDataStrict : import_redis2.setData)(
3158
+ import_redis2.redisKeys.runtimeTrade(userName, orderId),
3159
+ next,
3160
+ {
3161
+ expire: import_constants.TTL_1M
3162
+ }
3163
+ ),
3164
+ (expectedOrderId ? import_redis2.setHashJsonFieldStrict : import_redis2.setHashJsonField)(
3165
+ import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3166
+ orderId,
3167
+ next,
3168
+ { expire: import_constants.TTL_1M }
3169
+ ),
3170
+ (expectedOrderId ? import_redis2.setHashJsonFieldStrict : import_redis2.setHashJsonField)(
3171
+ import_redis2.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
3172
+ orderId,
3173
+ next,
3174
+ { expire: import_constants.TTL_1M }
3175
+ ),
3176
+ expectedOrderId ? (0, import_redis2.deleteRuntimeTradeReference)(
3177
+ import_redis2.redisKeys.runtimeActiveTrade(
3178
+ userName,
3179
+ symbol,
3180
+ deploymentId ?? accountId
3181
+ ),
3182
+ expectedOrderId
3183
+ ) : (0, import_redis2.delKey)(
3184
+ import_redis2.redisKeys.runtimeActiveTrade(
3185
+ userName,
3186
+ symbol,
3187
+ deploymentId ?? accountId
3188
+ )
3189
+ )
3190
+ ]);
3191
+ } catch (error) {
3192
+ if (expectedOrderId) throw error;
3193
+ import_logger2.logger.error(
3194
+ "runtime trade close journal failed: %s %s",
3195
+ symbol,
3231
3196
  error?.message || String(error)
3232
3197
  );
3233
3198
  }
3234
- return record;
3199
+ return next;
3235
3200
  };
3236
- var recordRuntimeTradeIncrease = async (params) => {
3237
- const {
3201
+
3202
+ // src/strategy/portfolioRuntime.ts
3203
+ var coordinators = /* @__PURE__ */ new WeakMap();
3204
+ async function observeRuntimePortfolio(connector, binding, asOf, reservations, observedNow = Date.now) {
3205
+ if (!connector.getOpenPositionPnl || !connector.getEntryExecutions)
3206
+ throw new Error(
3207
+ "Portfolio policies require position marks and entry executions"
3208
+ );
3209
+ const orders = await Promise.all(
3210
+ reservations.map(async (r) => {
3211
+ if (r.submitted && connector.getEntryOrderObservation) {
3212
+ const fact2 = await connector.getEntryOrderObservation({
3213
+ symbol: r.symbol,
3214
+ orderId: r.id
3215
+ });
3216
+ if (fact2) return fact2;
3217
+ }
3218
+ const executions = r.submitted ? await connector.getEntryExecutions({
3219
+ symbol: r.symbol,
3220
+ startTime: r.timestamp,
3221
+ endTime: asOf
3222
+ }) : [];
3223
+ const filledQty = executions.filter(
3224
+ (e) => (e.orderLinkId === r.id || e.orderId === r.id) && e.direction === r.direction
3225
+ ).reduce((sum, e) => sum + e.qty, 0);
3226
+ return {
3227
+ id: r.id,
3228
+ status: filledQty >= r.qty ? "filled" : filledQty > 0 ? "partial" : "pending",
3229
+ filledQty,
3230
+ positionId: r.id
3231
+ };
3232
+ })
3233
+ );
3234
+ const snapshots2 = await connector.getOpenPositionPnl();
3235
+ const positions = await Promise.all(
3236
+ snapshots2.map(async (p) => {
3237
+ const reserved = reservations.find(
3238
+ (r) => r.symbol === p.symbol && r.direction === p.direction && orders.some(
3239
+ (o) => o.id === r.id && o.filledQty > 0 && p.qty <= o.filledQty * (1 + 1e-8)
3240
+ )
3241
+ );
3242
+ const trade = await getActiveRuntimeTrade({
3243
+ ...binding,
3244
+ symbol: p.symbol
3245
+ });
3246
+ 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;
3247
+ return {
3248
+ id: reserved?.id ?? owner?.orderId ?? `unknown:${p.symbol}:${p.direction}`,
3249
+ strategy: reserved?.strategy ?? owner?.strategy ?? null,
3250
+ symbol: p.symbol,
3251
+ direction: p.direction,
3252
+ qty: p.qty,
3253
+ entryPrice: p.price,
3254
+ markPrice: p.currentPrice,
3255
+ markedAt: p.observedAt ?? null,
3256
+ stopLossPrice: p.slPrice ?? p.stopLossPrice ?? null
3257
+ };
3258
+ })
3259
+ );
3260
+ return { asOf: Math.max(asOf, observedNow()), positions, orders };
3261
+ }
3262
+ function persistentStore(key) {
3263
+ const raw = /* @__PURE__ */ new WeakMap();
3264
+ return {
3265
+ async read() {
3266
+ const value = await import_redis3.portfolioStateStorage.read(key);
3267
+ if (value === null) return null;
3268
+ const state = JSON.parse(value);
3269
+ (0, import_portfolio2.assertPortfolioState)(state);
3270
+ raw.set(state, value);
3271
+ return state;
3272
+ },
3273
+ async compare(previous, next) {
3274
+ const expected = previous === null ? null : raw.get(previous);
3275
+ if (expected === void 0)
3276
+ throw new Error("Portfolio CAS requires a storage snapshot");
3277
+ return import_redis3.portfolioStateStorage.compare(key, expected, JSON.stringify(next));
3278
+ }
3279
+ };
3280
+ }
3281
+ function getPortfolioRuntime({
3282
+ value,
3283
+ connectorName,
3284
+ strategy,
3285
+ connector,
3286
+ env,
3287
+ userName,
3288
+ accountId,
3289
+ deploymentId,
3290
+ symbol,
3291
+ canClose
3292
+ }) {
3293
+ const policy = (0, import_portfolio2.parsePortfolioPolicy)(value);
3294
+ if (!policy) return void 0;
3295
+ const replay = (env === "BACKTEST" || env === "PARITY") && Reflect.get(connector, "__tradejsReplayConnector") === true;
3296
+ if ((env === "BACKTEST" || env === "PARITY") && (!replay || !connector.getPortfolioObservation))
3297
+ throw new Error(
3298
+ "Portfolio policies require shared portfolio replay; isolated symbol backtests are unsupported"
3299
+ );
3300
+ if (!replay && (!userName || !accountId || !deploymentId || !connector.getOpenPositionPnl || !connector.getEntryExecutions))
3301
+ throw new Error(
3302
+ "Portfolio runtime requires exact user/account/deployment binding, marks and executions"
3303
+ );
3304
+ const binding = {
3238
3305
  userName,
3239
- strategy,
3240
- symbol,
3241
- direction,
3242
- resultingQty,
3243
- resultingEntryPrice,
3244
- addedQty,
3245
- addedEntryPrice,
3246
- entryTimestamp,
3247
- fee,
3248
3306
  accountId,
3249
3307
  deploymentId
3250
- } = params;
3251
- if (!userName) {
3252
- return null;
3308
+ };
3309
+ const scope = policy.scope === "strategy" ? strategy : policy.scope === "account" ? "account" : [...policy.scope.strategies].sort();
3310
+ const key = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify([binding, connectorName, connector.universe, scope])).digest("hex");
3311
+ const fingerprint3 = (0, import_portfolio2.portfolioFingerprint)(policy, strategy);
3312
+ const cacheKey = key;
3313
+ let registry = coordinators.get(connector);
3314
+ if (!registry) {
3315
+ registry = /* @__PURE__ */ new Map();
3316
+ coordinators.set(connector, registry);
3317
+ }
3318
+ const existing = registry.get(cacheKey);
3319
+ if (existing) {
3320
+ if (existing.fingerprint !== fingerprint3)
3321
+ throw new Error("Conflicting portfolio configuration for the same scope");
3322
+ if (symbol && canClose)
3323
+ existing.permissions.set(JSON.stringify([strategy, symbol]), canClose);
3324
+ return existing.coordinator;
3325
+ }
3326
+ const permissions = /* @__PURE__ */ new Map();
3327
+ if (symbol && canClose)
3328
+ permissions.set(JSON.stringify([strategy, symbol]), canClose);
3329
+ const coordinator = createPortfolioCoordinator({
3330
+ policy,
3331
+ strategy,
3332
+ store: replay ? createMemoryPortfolioStore() : persistentStore(key),
3333
+ execution: {
3334
+ async confirmClosed(intent, asOf) {
3335
+ if (replay) return;
3336
+ const position = intent.position;
3337
+ await markRuntimeTradeClosed({
3338
+ ...binding,
3339
+ strategy: position.strategy,
3340
+ expectedOrderId: position.id,
3341
+ symbol: position.symbol,
3342
+ exitPrice: position.markPrice,
3343
+ exitTimestamp: asOf,
3344
+ exitType: "exit"
3345
+ });
3346
+ },
3347
+ observe: (asOf, reservations) => replay ? connector.getPortfolioObservation(asOf, reservations) : observeRuntimePortfolio(connector, binding, asOf, reservations),
3348
+ async close(position, intent, asOf) {
3349
+ const permission = permissions.get(
3350
+ JSON.stringify([position.strategy, position.symbol])
3351
+ );
3352
+ if (!permission || !await permission(position, intent, asOf)) return;
3353
+ if (!replay) {
3354
+ const trade = await getActiveRuntimeTrade({
3355
+ ...binding,
3356
+ symbol: position.symbol
3357
+ });
3358
+ if (!trade || trade.orderId !== position.id)
3359
+ throw new Error("Portfolio close ownership changed");
3360
+ }
3361
+ await connector.closePosition({
3362
+ symbol: position.symbol,
3363
+ direction: position.direction,
3364
+ price: position.markPrice,
3365
+ timestamp: asOf,
3366
+ signal: {
3367
+ signalId: `${position.id}:${intent.reason}`,
3368
+ strategy: position.strategy,
3369
+ symbol: position.symbol,
3370
+ direction: position.direction,
3371
+ interval: "1",
3372
+ timestamp: asOf,
3373
+ prices: {
3374
+ riskRatio: 0,
3375
+ currentPrice: position.markPrice,
3376
+ takeProfitPrice: position.markPrice,
3377
+ stopLossPrice: position.markPrice
3378
+ },
3379
+ figures: {},
3380
+ indicators: {},
3381
+ additionalIndicators: { portfolioExitReason: intent.reason }
3382
+ }
3383
+ });
3384
+ }
3385
+ }
3386
+ });
3387
+ registry.set(cacheKey, { fingerprint: fingerprint3, coordinator, permissions });
3388
+ return coordinator;
3389
+ }
3390
+
3391
+ // src/strategyHelpers/tickerProfiles.ts
3392
+ var import_tickerProfiles = require("@tradejs/infra/tickerProfiles");
3393
+ var import_tickerProfiles2 = require("@tradejs/core/tickerProfiles");
3394
+ var snapshots = /* @__PURE__ */ new Map();
3395
+ var resolveTickerProfileSource = (strategy) => {
3396
+ const declared = getStrategyManifest(strategy)?.contextRequirements?.tickerProfiles;
3397
+ if (declared) return declared;
3398
+ const provider = process.env.TICKER_PROFILES_PROVIDER?.trim();
3399
+ const universeId = process.env.TICKER_PROFILES_UNIVERSE?.trim();
3400
+ if (!provider && !universeId) return void 0;
3401
+ if (!provider || !universeId)
3402
+ throw new Error(
3403
+ "Set both TICKER_PROFILES_PROVIDER and TICKER_PROFILES_UNIVERSE"
3404
+ );
3405
+ return { provider, universeId };
3406
+ };
3407
+ var loadTickerProfileContext = async ({
3408
+ projectRoot,
3409
+ provider,
3410
+ universeId,
3411
+ symbol,
3412
+ timestamp
3413
+ }) => {
3414
+ const asOf = (0, import_tickerProfiles2.tickerProfileMonth)(timestamp), key = JSON.stringify([projectRoot, provider, universeId, asOf]);
3415
+ let cached = snapshots.get(key);
3416
+ if (!cached || cached.expires < Date.now()) {
3417
+ const value = (0, import_tickerProfiles.readTickerProfiles)(projectRoot, provider, universeId, asOf).then((envelope) => {
3418
+ if (!envelope) return null;
3419
+ const s = envelope.snapshot;
3420
+ if (!(0, import_tickerProfiles2.isTickerProfileSnapshot)(s) || s.asOf !== asOf || s.provider !== provider || s.universeId !== universeId)
3421
+ throw new Error("Ticker profile identity/schema mismatch");
3422
+ const entry = snapshots.get(key);
3423
+ if (entry) entry.expires = Infinity;
3424
+ return new Map(
3425
+ s.profiles.map((p) => [
3426
+ p.symbol,
3427
+ Object.freeze({
3428
+ ...p,
3429
+ definition: s.definition,
3430
+ provider,
3431
+ universeId,
3432
+ snapshotId: envelope.snapshotId
3433
+ })
3434
+ ])
3435
+ );
3436
+ }).catch((error) => {
3437
+ snapshots.delete(key);
3438
+ throw error;
3439
+ });
3440
+ cached = { expires: Date.now() + 6e4, value };
3441
+ snapshots.set(key, cached);
3442
+ if (snapshots.size > 24) {
3443
+ const oldest = snapshots.keys().next().value;
3444
+ if (oldest) snapshots.delete(oldest);
3445
+ }
3446
+ }
3447
+ return (await cached.value)?.get(symbol);
3448
+ };
3449
+ var withTickerProfile = (baseContext, profile) => Object.defineProperty(
3450
+ Object.create(
3451
+ Object.getPrototypeOf(baseContext),
3452
+ Object.getOwnPropertyDescriptors(baseContext)
3453
+ ),
3454
+ "tickerProfile",
3455
+ { enumerable: true, configurable: true, value: profile }
3456
+ );
3457
+ var enrichSignalWithTickerProfile = async (signal) => {
3458
+ const source = resolveTickerProfileSource(signal.strategy);
3459
+ if (!source || signal.universe === "tradfi" || !signal.symbol.endsWith("USDT"))
3460
+ return;
3461
+ const profile = await loadTickerProfileContext({
3462
+ ...source,
3463
+ projectRoot: getTradejsProjectCwd(),
3464
+ symbol: signal.symbol,
3465
+ timestamp: signal.timestamp
3466
+ });
3467
+ if (profile) {
3468
+ signal.additionalIndicators = {
3469
+ ...signal.additionalIndicators,
3470
+ tickerProfile: profile
3471
+ };
3472
+ }
3473
+ };
3474
+
3475
+ // src/strategyRuntime.ts
3476
+ var import_jev6 = require("@tradejs/core/jev");
3477
+
3478
+ // src/jev.ts
3479
+ var import_promises = __toESM(require("fs/promises"));
3480
+ var import_node_path = __toESM(require("path"));
3481
+ var import_jev4 = require("@tradejs/core/jev");
3482
+ var import_jev5 = require("@tradejs/infra/jev");
3483
+ var import_userSettings2 = require("@tradejs/infra/userSettings");
3484
+ init_jevInput();
3485
+ init_jevInput();
3486
+ init_jevTraining();
3487
+ var resolveJevProvider = async (userName) => {
3488
+ const settings = await (0, import_userSettings2.getUserSettings)(userName);
3489
+ const provider = {
3490
+ endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
3491
+ model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
3492
+ };
3493
+ (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
3494
+ if (!settings.JEV_API_KEY)
3495
+ throw new Error("Configure the Jev API key in Account settings");
3496
+ return provider;
3497
+ };
3498
+ var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
3499
+ var pending = /* @__PURE__ */ new Map();
3500
+ var pendingResponses = /* @__PURE__ */ new Map();
3501
+ var evaluateJevInput = async ({
3502
+ input,
3503
+ config,
3504
+ userName,
3505
+ projectRoot
3506
+ }) => {
3507
+ config = (0, import_jev4.parseJevConfig)(config);
3508
+ const inputHash = (0, import_jev5.jevHash)(input);
3509
+ const questions = questionsForJevInput(input);
3510
+ const questionsHash = (0, import_jev5.jevHash)(questions);
3511
+ const base = {
3512
+ questionSet: import_jev4.JEV_QUESTION_SET,
3513
+ schema: "tradejs-signal-assessment/v5",
3514
+ source: config.source,
3515
+ mode: config.mode,
3516
+ inputHash,
3517
+ questionsHash,
3518
+ geometryStatus: input.geometryStatus,
3519
+ levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
3520
+ };
3521
+ if (!input.questions.length)
3522
+ return {
3523
+ assessment: {
3524
+ ...base,
3525
+ status: "unavailable",
3526
+ model: "",
3527
+ scores: emptyScores(),
3528
+ confidence: emptyScores()
3529
+ }
3530
+ };
3531
+ if (config.source === "local") {
3532
+ const contents = await import_promises.default.readFile(
3533
+ import_node_path.default.resolve(projectRoot, config.modelFile),
3534
+ "utf8"
3535
+ );
3536
+ if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
3537
+ throw new Error("Local Jev gate checksum mismatch");
3538
+ const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
3539
+ const model = validateJevGateModel2(JSON.parse(contents));
3540
+ if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
3541
+ throw new Error("Local Jev gate strategy or question version mismatch");
3542
+ const scores = Object.fromEntries(
3543
+ import_jev4.JEV_DIMENSIONS.map((key) => [
3544
+ key,
3545
+ input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
3546
+ ])
3547
+ );
3548
+ return {
3549
+ assessment: {
3550
+ ...base,
3551
+ status: "available",
3552
+ model: config.modelSha256,
3553
+ scores,
3554
+ confidence: emptyScores()
3555
+ }
3556
+ };
3557
+ }
3558
+ const provider = config.provider ?? await resolveJevProvider(userName);
3559
+ const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
3560
+ const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
3561
+ const file = import_node_path.default.join(dir, "records", `${id}.json`);
3562
+ const getRecord = async () => {
3563
+ const existing = await (0, import_jev5.readJevArtifact)(file);
3564
+ if (existing) {
3565
+ 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))
3566
+ throw new Error("Jev recording identity mismatch");
3567
+ (0, import_jev5.validateJevResponse)(existing.response, input.questions);
3568
+ for (const key of import_jev4.JEV_DIMENSIONS) {
3569
+ const expected = input.questions.includes(key) ? existing.response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null;
3570
+ if (existing.scores[key] !== expected)
3571
+ throw new Error("Jev recording scores do not match the response");
3572
+ }
3573
+ return existing;
3574
+ }
3575
+ if (config.source === "recorded")
3576
+ throw new Error(
3577
+ `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.`
3578
+ );
3579
+ const settings = await (0, import_userSettings2.getUserSettings)(userName);
3580
+ if (!settings.JEV_API_KEY)
3581
+ throw new Error("Configure the Jev API key in Account settings");
3582
+ const apiKey = settings.JEV_API_KEY;
3583
+ const configuredProvider = await resolveJevProvider(userName);
3584
+ if (configuredProvider.endpoint !== provider.endpoint)
3585
+ throw new Error(
3586
+ "Jev account endpoint changed; update the frozen provider configuration"
3587
+ );
3588
+ const started = Date.now();
3589
+ const state = stateForJevInput(input);
3590
+ const responseId = (0, import_jev5.jevHash)({
3591
+ state,
3592
+ questionsHash,
3593
+ provider,
3594
+ user: (0, import_jev5.jevHash)(userName)
3595
+ });
3596
+ const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
3597
+ let responsePromise = pendingResponses.get(responseFile);
3598
+ if (!responsePromise) {
3599
+ responsePromise = (async () => {
3600
+ const saved = await (0, import_jev5.readJevArtifact)(responseFile);
3601
+ if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
3602
+ const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
3603
+ await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
3604
+ return fresh;
3605
+ })();
3606
+ pendingResponses.set(responseFile, responsePromise);
3607
+ }
3608
+ let response;
3609
+ try {
3610
+ response = await responsePromise;
3611
+ } finally {
3612
+ pendingResponses.delete(responseFile);
3613
+ }
3614
+ const scores = Object.fromEntries(
3615
+ import_jev4.JEV_DIMENSIONS.map((key) => [
3616
+ key,
3617
+ input.questions.includes(key) ? response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null
3618
+ ])
3619
+ );
3620
+ const value = {
3621
+ schema: "tradejs-jev-record/v4",
3622
+ id,
3623
+ inputHash,
3624
+ questionsHash,
3625
+ provider,
3626
+ input,
3627
+ response,
3628
+ scores,
3629
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3630
+ elapsedMs: Date.now() - started
3631
+ };
3632
+ await (0, import_jev5.writeJevArtifact)(file, value);
3633
+ return await (0, import_jev5.readJevArtifact)(file);
3634
+ };
3635
+ const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
3636
+ let promise = pending.get(pendingKey);
3637
+ if (!promise) {
3638
+ promise = getRecord();
3639
+ pending.set(pendingKey, promise);
3253
3640
  }
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;
3641
+ let record;
3642
+ try {
3643
+ record = await promise;
3644
+ } finally {
3645
+ pending.delete(pendingKey);
3262
3646
  }
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()
3647
+ return {
3648
+ record,
3649
+ assessment: {
3650
+ ...base,
3651
+ status: "available",
3652
+ recordId: id,
3653
+ model: record.response.model,
3654
+ scores: record.scores,
3655
+ confidence: Object.fromEntries(
3656
+ import_jev4.JEV_DIMENSIONS.map((key) => [
3657
+ key,
3658
+ record.response.answers[key]?.confidence ?? null
3659
+ ])
3660
+ )
3661
+ }
3281
3662
  };
3282
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3663
+ };
3664
+ var assessSignalWithJev = async ({
3665
+ signal,
3666
+ config,
3667
+ userName,
3668
+ projectRoot,
3669
+ strict,
3670
+ scope = "runtime"
3671
+ }) => {
3672
+ let assessment;
3283
3673
  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
- ]);
3674
+ const input = buildJevInput(signal);
3675
+ ({ assessment } = await evaluateJevInput({
3676
+ input,
3677
+ config,
3678
+ userName,
3679
+ projectRoot
3680
+ }));
3295
3681
  } 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;
3682
+ if (strict) throw error;
3683
+ assessment = {
3684
+ questionSet: import_jev4.JEV_QUESTION_SET,
3685
+ schema: "tradejs-signal-assessment/v5",
3686
+ source: config.source,
3687
+ mode: config.mode,
3688
+ status: "unavailable",
3689
+ inputHash: "",
3690
+ questionsHash: "",
3691
+ geometryStatus: "absent",
3692
+ levelsValid: null,
3693
+ model: config.provider?.model ?? config.modelSha256 ?? "",
3694
+ scores: emptyScores(),
3695
+ confidence: emptyScores()
3696
+ };
3316
3697
  }
3317
- const existing = await (0, import_redis2.getData)(
3318
- import_redis2.redisKeys.runtimeTrade(userName, orderId),
3319
- null
3698
+ signal.assessment = assessment;
3699
+ signal.additionalIndicators = {
3700
+ ...signal.additionalIndicators,
3701
+ jev: (0, import_jev4.jevAssessmentFeatures)(assessment)
3702
+ };
3703
+ const identity = {
3704
+ signalId: signal.signalId,
3705
+ strategy: signal.strategy,
3706
+ symbol: signal.symbol,
3707
+ timestamp: signal.timestamp,
3708
+ scope,
3709
+ user: (0, import_jev5.jevHash)(userName),
3710
+ config,
3711
+ assessment
3712
+ };
3713
+ await (0, import_jev5.writeJevArtifact)(
3714
+ import_node_path.default.resolve(
3715
+ projectRoot,
3716
+ config.recordsDir ?? "data/ai/jev",
3717
+ "decisions",
3718
+ `${(0, import_jev5.jevHash)(identity)}.json`
3719
+ ),
3720
+ identity
3320
3721
  );
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;
3722
+ return assessment;
3328
3723
  };
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;
3724
+
3725
+ // src/strategyRuntime.ts
3726
+ var import_constants5 = require("@tradejs/core/constants");
3727
+ var import_strategies6 = require("@tradejs/core/strategies");
3728
+ var import_logger11 = require("@tradejs/infra/logger");
3729
+
3730
+ // src/strategyHelpers/runtime.ts
3731
+ var import_logger9 = require("@tradejs/infra/logger");
3732
+ var import_constants3 = require("@tradejs/core/constants");
3733
+ var import_ml2 = require("@tradejs/infra/ml");
3734
+
3735
+ // src/strategy/entrySubmissionValidation.ts
3736
+ var import_logger3 = require("@tradejs/infra/logger");
3737
+
3738
+ // src/strategy/entryExecutionValidation.ts
3739
+ var EntrySubmissionValidationError = class extends Error {
3740
+ constructor() {
3741
+ super(...arguments);
3742
+ this.name = "EntrySubmissionValidationError";
3356
3743
  }
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()
3744
+ };
3745
+ var resolveEntryExecutionLimits = (config, direction) => {
3746
+ const side = config[direction];
3747
+ const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
3748
+ const maximum = config.MAX_LOSS_VALUE;
3749
+ if (minimum == null && maximum == null) return void 0;
3750
+ const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
3751
+ const maxLossValue = maximum == null ? void 0 : Number(maximum);
3752
+ const feeRate = Number(config.RISK_FEE_RATE ?? 0);
3753
+ const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
3754
+ const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
3755
+ const slippageBps = slippage + impact;
3756
+ 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)
3757
+ throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
3758
+ return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
3759
+ };
3760
+ var validateEntryExecution = ({
3761
+ direction,
3762
+ price,
3763
+ qty,
3764
+ stopLossPrice,
3765
+ takeProfitPrice,
3766
+ limits
3767
+ }) => {
3768
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
3769
+ throw new EntrySubmissionValidationError(
3770
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
3771
+ );
3772
+ const side = direction === "LONG" ? 1 : -1;
3773
+ if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
3774
+ throw new EntrySubmissionValidationError(
3775
+ "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
3776
+ );
3777
+ const slippage = limits.slippageBps / 1e4;
3778
+ const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
3779
+ const entryCost = cost(price, side);
3780
+ const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
3781
+ const rewardPerUnit = Math.max(
3782
+ 0,
3783
+ Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
3784
+ );
3785
+ const netRiskRatio = rewardPerUnit / lossPerUnit;
3786
+ const plannedLossValue = qty * lossPerUnit;
3787
+ if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
3788
+ throw new EntrySubmissionValidationError(
3789
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
3790
+ );
3791
+ const belowMinimumNetRiskRatio = limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio;
3792
+ const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
3793
+ return {
3794
+ netRiskRatio,
3795
+ plannedLossValue,
3796
+ riskExceeded,
3797
+ belowMinimumNetRiskRatio
3374
3798
  };
3375
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3376
- const closeDayKey = (0, import_time.getRuntimeStorageDayKey)(next.exitTimestamp);
3799
+ };
3800
+
3801
+ // src/strategy/entrySubmissionValidation.ts
3802
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
3803
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
3804
+ var getOrderArrivalSnapshot = async ({
3805
+ connector,
3806
+ symbol,
3807
+ requireTimestamp = false
3808
+ }) => {
3809
+ if (typeof connector.getTopOfBookTicker !== "function") {
3810
+ return {
3811
+ arrivalSnapshotTime: Date.now(),
3812
+ arrivalSource: "unavailable",
3813
+ bid: null,
3814
+ ask: null,
3815
+ arrivalMid: null,
3816
+ spreadBps: null
3817
+ };
3818
+ }
3377
3819
  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
- ]);
3820
+ const ticker = await connector.getTopOfBookTicker(symbol);
3821
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
3822
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
3823
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
3824
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
3825
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
3826
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
3827
+ return {
3828
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
3829
+ arrivalSource: "top_of_book",
3830
+ bid,
3831
+ ask,
3832
+ arrivalMid,
3833
+ spreadBps
3834
+ };
3402
3835
  } catch (error) {
3403
- import_logger3.logger.error(
3404
- "runtime trade close journal failed: %s %s",
3836
+ import_logger3.logger.warn(
3837
+ "runtime order arrival snapshot failed: %s %s",
3405
3838
  symbol,
3406
3839
  error?.message || String(error)
3407
3840
  );
3841
+ return {
3842
+ arrivalSnapshotTime: Date.now(),
3843
+ arrivalSource: "top_of_book_error",
3844
+ bid: null,
3845
+ ask: null,
3846
+ arrivalMid: null,
3847
+ spreadBps: null
3848
+ };
3408
3849
  }
3409
- return next;
3850
+ };
3851
+ var validateEntryProtectionAtArrival = ({
3852
+ direction,
3853
+ signalPrice,
3854
+ bid,
3855
+ ask,
3856
+ arrivalMid,
3857
+ takeProfits,
3858
+ stopLossPrice
3859
+ }) => {
3860
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
3861
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
3862
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
3863
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
3864
+ }
3865
+ for (const takeProfit of takeProfits) {
3866
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
3867
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
3868
+ }
3869
+ }
3870
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
3871
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
3872
+ }
3873
+ };
3874
+ var createEntrySubmissionValidation = ({
3875
+ connector,
3876
+ symbol,
3877
+ direction,
3878
+ takeProfits,
3879
+ stopLossPrice,
3880
+ limits,
3881
+ onValidated
3882
+ }) => {
3883
+ if (!limits) return void 0;
3884
+ if (!connector.capabilities?.orderSubmissionValidation)
3885
+ throw new EntrySubmissionValidationError(
3886
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
3887
+ );
3888
+ return async ({ qty, executionPrice }) => {
3889
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
3890
+ const snapshot2 = simulated && executionPrice != null ? {
3891
+ arrivalSnapshotTime: Date.now(),
3892
+ arrivalSource: "simulation",
3893
+ bid: executionPrice,
3894
+ ask: executionPrice,
3895
+ arrivalMid: executionPrice,
3896
+ spreadBps: 0
3897
+ } : await getOrderArrivalSnapshot({
3898
+ connector,
3899
+ symbol,
3900
+ requireTimestamp: true
3901
+ });
3902
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
3903
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
3904
+ throw new EntrySubmissionValidationError(
3905
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
3906
+ );
3907
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
3908
+ throw new EntrySubmissionValidationError(
3909
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
3910
+ );
3911
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
3912
+ validateEntryProtectionAtArrival({
3913
+ direction,
3914
+ signalPrice: price,
3915
+ bid: snapshot2.bid,
3916
+ ask: snapshot2.ask,
3917
+ arrivalMid: snapshot2.arrivalMid,
3918
+ takeProfits,
3919
+ stopLossPrice
3920
+ });
3921
+ const prices = takeProfits.map((tp) => tp.price);
3922
+ const economics2 = validateEntryExecution({
3923
+ direction,
3924
+ price,
3925
+ qty,
3926
+ stopLossPrice,
3927
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
3928
+ limits
3929
+ });
3930
+ if (economics2.belowMinimumNetRiskRatio) {
3931
+ import_logger3.logger.warn(
3932
+ "entry submission warning: %s %s ENTRY_RR_BELOW_MIN_AT_SUBMISSION (RR %s, threshold %s)",
3933
+ symbol,
3934
+ direction,
3935
+ economics2.netRiskRatio,
3936
+ limits.minimumNetRiskRatio
3937
+ );
3938
+ }
3939
+ onValidated?.(snapshot2, economics2);
3940
+ };
3941
+ };
3942
+
3943
+ // src/strategyAdapters/ml.ts
3944
+ var defaultMlAdapter = {
3945
+ normalizeStrategyConfig: (strategyConfig) => strategyConfig
3946
+ };
3947
+ var getStrategyMlAdapter = (strategy, profileId) => {
3948
+ const strategyAdapter = getStrategyProfileMlAdapter(
3949
+ getStrategyManifest(strategy),
3950
+ profileId
3951
+ );
3952
+ if (!strategyAdapter) return defaultMlAdapter;
3953
+ return {
3954
+ ...defaultMlAdapter,
3955
+ ...strategyAdapter
3956
+ };
3957
+ };
3958
+
3959
+ // src/mlPayload.ts
3960
+ var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
3961
+ return getStrategyMlAdapter(
3962
+ strategyName,
3963
+ profileId
3964
+ ).normalizeStrategyConfig?.(strategyConfig);
3965
+ };
3966
+ var buildMlPayload = (payload) => {
3967
+ const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
3968
+ const profileId = payload.signal?.policyProfileId;
3969
+ const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
3970
+ const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
3971
+ const nextSignal = {
3972
+ ...normalizedSignal,
3973
+ indicators: {
3974
+ ...normalizedSignal?.indicators ?? {}
3975
+ }
3976
+ };
3977
+ const nextContext = payload.context ? {
3978
+ ...payload.context,
3979
+ strategyConfig: normalizeStrategyConfig(
3980
+ payload.context.strategyConfig,
3981
+ strategyName,
3982
+ profileId
3983
+ )
3984
+ } : void 0;
3985
+ return {
3986
+ signal: nextSignal,
3987
+ context: nextContext
3988
+ };
3410
3989
  };
3411
3990
 
3412
3991
  // src/strategyHelpers/marketContextStages.ts
@@ -3418,7 +3997,7 @@ var import_logger4 = require("@tradejs/infra/logger");
3418
3997
  var import_strategies = require("@tradejs/core/strategies");
3419
3998
 
3420
3999
  // src/binanceBreadthUniverses.ts
3421
- var import_node_crypto2 = require("crypto");
4000
+ var import_node_crypto3 = require("crypto");
3422
4001
 
3423
4002
  // src/config/binanceBreadthUniverses.json
3424
4003
  var binanceBreadthUniverses_default = {
@@ -3657,7 +4236,7 @@ var BINANCE_BREADTH_UNIVERSE_KEYS = [
3657
4236
  "top50",
3658
4237
  "top100"
3659
4238
  ];
3660
- var fingerprint = (value, length) => (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
4239
+ var fingerprint = (value, length) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
3661
4240
  var normalizeSymbols = (symbols) => symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean);
3662
4241
  var buildBinanceBreadthUniverseSnapshot = ({
3663
4242
  rankedSymbols,
@@ -4833,7 +5412,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4833
5412
  };
4834
5413
 
4835
5414
  // src/strategyHelpers/derivativesContext.ts
4836
- var import_indicators2 = require("@tradejs/core/indicators");
5415
+ var import_indicators3 = require("@tradejs/core/indicators");
4837
5416
  var import_data2 = require("@tradejs/core/data");
4838
5417
  var import_strategies3 = require("@tradejs/core/strategies");
4839
5418
  var import_constants2 = require("@tradejs/core/constants");
@@ -4867,7 +5446,7 @@ var parseLookbackMs = () => {
4867
5446
  };
4868
5447
  var withHourlyFallbackRows = (rowsByInterval) => ({
4869
5448
  "15m": rowsByInterval["15m"] ?? [],
4870
- "1h": (0, import_indicators2.buildCoinalyzeHourlyRowsWithFallback)({
5449
+ "1h": (0, import_indicators3.buildCoinalyzeHourlyRowsWithFallback)({
4871
5450
  rows15m: rowsByInterval["15m"],
4872
5451
  fallbackRows1h: rowsByInterval["1h"]
4873
5452
  })
@@ -4995,7 +5574,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4995
5574
  const targetSymbol = normalizeSymbol(signal.symbol);
4996
5575
  const lookbackMs = parseLookbackMs();
4997
5576
  const decisionTimeMs = signal.timestamp + (0, import_data2.intervalToMs)(signal.interval);
4998
- const derivativesEndMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
5577
+ const derivativesEndMs = (0, import_indicators3.getLastClosedDerivativesBarStartMs)(
4999
5578
  decisionTimeMs,
5000
5579
  "15m"
5001
5580
  );
@@ -5010,7 +5589,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
5010
5589
  });
5011
5590
  return [
5012
5591
  symbol,
5013
- (0, import_indicators2.buildDerivativesContext)({
5592
+ (0, import_indicators3.buildDerivativesContext)({
5014
5593
  symbol,
5015
5594
  direction: signal.direction,
5016
5595
  timestamp: derivativesEndMs,
@@ -5037,7 +5616,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
5037
5616
  lookbackMs,
5038
5617
  ...params.abortSignal ? { signal: params.abortSignal } : {}
5039
5618
  });
5040
- const context = (0, import_indicators2.buildDerivativesContext)({
5619
+ const context = (0, import_indicators3.buildDerivativesContext)({
5041
5620
  symbol: targetSymbol,
5042
5621
  direction: signal.direction,
5043
5622
  timestamp: derivativesEndMs,
@@ -5085,7 +5664,7 @@ var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWha
5085
5664
  var import_logger7 = require("@tradejs/infra/logger");
5086
5665
 
5087
5666
  // src/hyperliquidWhaleUniverse.ts
5088
- var import_node_crypto3 = require("crypto");
5667
+ var import_node_crypto4 = require("crypto");
5089
5668
 
5090
5669
  // src/config/hyperliquidPerpUniverse.json
5091
5670
  var hyperliquidPerpUniverse_default = {
@@ -5257,7 +5836,7 @@ var hyperliquidWhales_default = {
5257
5836
  };
5258
5837
 
5259
5838
  // src/hyperliquidWhaleUniverse.ts
5260
- var fingerprint2 = (value) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
5839
+ var fingerprint2 = (value) => (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
5261
5840
  var validateUnique = (values, label) => {
5262
5841
  if (new Set(values).size !== values.length) {
5263
5842
  throw new Error(`Hyperliquid ${label} snapshot contains duplicates`);
@@ -5748,6 +6327,7 @@ var enrichSignalWithMarketContextStages = async ({
5748
6327
  })
5749
6328
  });
5750
6329
  }
6330
+ await enrichSignalWithTickerProfile(signal);
5751
6331
  const results = [];
5752
6332
  for (const stage of stages) {
5753
6333
  if (abortSignal?.aborted) break;
@@ -5795,12 +6375,12 @@ var findReplayAiAnalysis = ({
5795
6375
  direction,
5796
6376
  ai
5797
6377
  }) => {
5798
- const snapshots = ai?.replayAnalyses;
5799
- if (!Array.isArray(snapshots) || !snapshots.length) {
6378
+ const snapshots2 = ai?.replayAnalyses;
6379
+ if (!Array.isArray(snapshots2) || !snapshots2.length) {
5800
6380
  return void 0;
5801
6381
  }
5802
6382
  let best = null;
5803
- for (const snapshot2 of snapshots) {
6383
+ for (const snapshot2 of snapshots2) {
5804
6384
  if (snapshot2.symbol !== signal.symbol || snapshot2.direction !== direction || snapshot2.strategy && snapshot2.strategy !== signal.strategy) {
5805
6385
  continue;
5806
6386
  }
@@ -5973,11 +6553,13 @@ var executeEntryOrder = async ({
5973
6553
  beforePlaceOrder,
5974
6554
  recordRuntimeTrade = true,
5975
6555
  leverage,
5976
- executionLimits
6556
+ executionLimits,
6557
+ markPortfolioSubmitted,
6558
+ rejectPortfolioSubmission
5977
6559
  }) => {
5978
6560
  await beforePlaceOrder?.();
5979
6561
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
5980
- const orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
6562
+ let orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
5981
6563
  const signalTimestamp = signal.timestamp;
5982
6564
  const signalClosePrice = currentPrice;
5983
6565
  signal.orderId = orderId;
@@ -6046,9 +6628,16 @@ var executeEntryOrder = async ({
6046
6628
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
6047
6629
  orderId,
6048
6630
  signal,
6049
- ...beforeSubmit ? { beforeSubmit } : {}
6631
+ ...rejectPortfolioSubmission ? { onRejected: rejectPortfolioSubmission } : {},
6632
+ ...beforeSubmit || markPortfolioSubmitted ? {
6633
+ beforeSubmit: async (context) => {
6634
+ await beforeSubmit?.(context);
6635
+ return markPortfolioSubmitted?.(context.qty);
6636
+ }
6637
+ } : {}
6050
6638
  });
6051
6639
  const orderAckTime = Date.now();
6640
+ if (markPortfolioSubmitted && signal.orderId) orderId = signal.orderId;
6052
6641
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
6053
6642
  const currentPosition = await connector.getPosition(symbol);
6054
6643
  const fillTime = Date.now();
@@ -7034,7 +7623,7 @@ var handleProtectDecision = async ({
7034
7623
  }
7035
7624
  return decision.code;
7036
7625
  };
7037
- var executeEntryDecision = async ({
7626
+ var executeAdmittedEntryDecision = async ({
7038
7627
  connector,
7039
7628
  symbol,
7040
7629
  decision,
@@ -7048,7 +7637,9 @@ var executeEntryDecision = async ({
7048
7637
  ai,
7049
7638
  recordRuntimeJournal,
7050
7639
  invokeStageHooks,
7051
- notifyRuntimeError
7640
+ notifyRuntimeError,
7641
+ markPortfolioSubmitted,
7642
+ rejectPortfolioSubmission
7052
7643
  }) => {
7053
7644
  const signal = decision.signal;
7054
7645
  const beforePlaceOrder = async () => {
@@ -7095,6 +7686,8 @@ var executeEntryDecision = async ({
7095
7686
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
7096
7687
  signal,
7097
7688
  beforePlaceOrder,
7689
+ markPortfolioSubmitted,
7690
+ rejectPortfolioSubmission,
7098
7691
  recordRuntimeTrade: recordRuntimeJournal,
7099
7692
  executionLimits: resolveEntryExecutionLimits(
7100
7693
  hookCtx.strategyConfig,
@@ -7235,6 +7828,37 @@ var executeEntryDecision = async ({
7235
7828
  }
7236
7829
  return signal ?? decision.code;
7237
7830
  };
7831
+ async function executeEntryDecision(params) {
7832
+ if (!params.portfolio) return executeAdmittedEntryDecision(params);
7833
+ const { decision, portfolio } = params;
7834
+ const signal = decision.signal;
7835
+ if (!signal || decision.orderPlan.positionIntent === "increase")
7836
+ return "PORTFOLIO_REQUIRES_NEW_SIGNAL_POSITION";
7837
+ signal.orderId ??= createRuntimeOrderId(signal.strategy);
7838
+ const result = await portfolio.submit(
7839
+ params.portfolioTime ?? decision.entryContext.timestamp,
7840
+ {
7841
+ id: signal.orderId,
7842
+ signalId: signal.signalId,
7843
+ strategy: signal.strategy,
7844
+ symbol: params.symbol,
7845
+ direction: decision.entryContext.direction,
7846
+ qty: decision.orderPlan.qty,
7847
+ timestamp: decision.entryContext.timestamp
7848
+ },
7849
+ (markPortfolioSubmitted, rejectPortfolioSubmission) => executeAdmittedEntryDecision({
7850
+ ...params,
7851
+ markPortfolioSubmitted,
7852
+ rejectPortfolioSubmission
7853
+ })
7854
+ );
7855
+ if (result.reason) {
7856
+ signal.orderStatus = "failed";
7857
+ signal.orderFailureReason = result.reason;
7858
+ return signal;
7859
+ }
7860
+ return result.value;
7861
+ }
7238
7862
 
7239
7863
  // src/strategyRuntime.ts
7240
7864
  var cloneWithPropertyDescriptors = (value) => Object.create(
@@ -7262,6 +7886,7 @@ var createStrategyRuntime = ({
7262
7886
  };
7263
7887
  const creator = async ({
7264
7888
  userName,
7889
+ connectorName,
7265
7890
  config: baseConfig,
7266
7891
  symbol,
7267
7892
  universe: requestedUniverse,
@@ -7300,6 +7925,37 @@ var createStrategyRuntime = ({
7300
7925
  const projectConfig = await loadTradejsConfig(projectRoot);
7301
7926
  const projectHooks = projectConfig.hooks;
7302
7927
  const env = String(config.ENV ?? "BACKTEST");
7928
+ let portfolioMarket;
7929
+ const portfolio = getPortfolioRuntime({
7930
+ symbol,
7931
+ canClose: async (position, intent, asOf) => {
7932
+ if (!portfolioMarket || portfolioMarket.candle.timestamp > asOf || !isRuntimeOrderExecutionEnabled({ config, env, connector }))
7933
+ return false;
7934
+ const gate = await authorizeClose(
7935
+ {
7936
+ kind: "exit",
7937
+ code: intent.reason,
7938
+ closePlan: {
7939
+ direction: position.direction,
7940
+ price: position.markPrice,
7941
+ timestamp: asOf
7942
+ }
7943
+ },
7944
+ portfolioMarket,
7945
+ position.strategy
7946
+ );
7947
+ return gate?.allow !== false;
7948
+ },
7949
+ connectorName,
7950
+ value: baseConfig?.PORTFOLIO_POLICY ?? config.PORTFOLIO_POLICY,
7951
+ strategy: strategyName,
7952
+ connector,
7953
+ env,
7954
+ userName,
7955
+ accountId,
7956
+ deploymentId
7957
+ });
7958
+ const portfolioContext = portfolio ? (now2) => isRuntimeOrderExecutionEnabled({ config, env, connector }) ? portfolio.tick(now2) : portfolio.context(now2) : void 0;
7303
7959
  const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
7304
7960
  const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
7305
7961
  if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
@@ -7464,7 +8120,10 @@ var createStrategyRuntime = ({
7464
8120
  return invokeHook(stage, hook, params, errorContext);
7465
8121
  };
7466
8122
  const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
7467
- const failureResult = stage === "beforeEntryGate" ? { allow: false, reason: "ERROR" } : void 0;
8123
+ const failureResult = {
8124
+ allow: false,
8125
+ reason: "ERROR"
8126
+ };
7468
8127
  const projectResults = await invokeProjectHooks(
7469
8128
  stage,
7470
8129
  params,
@@ -7485,6 +8144,12 @@ var createStrategyRuntime = ({
7485
8144
  failureResult
7486
8145
  );
7487
8146
  };
8147
+ const authorizeClose = (decision, market, owner = strategyName) => invokeGateHooks(
8148
+ "beforeClosePosition",
8149
+ resolveManifest(owner)?.hooks?.beforeClosePosition,
8150
+ { ctx: getHookCtx(owner), market, decision },
8151
+ { decision, market }
8152
+ );
7488
8153
  const applyProjectAfterCoreDecisionHooks = async ({
7489
8154
  hookCtx,
7490
8155
  market,
@@ -7598,6 +8263,20 @@ var createStrategyRuntime = ({
7598
8263
  interval: decisionInterval
7599
8264
  }) => {
7600
8265
  if (!baseContext) return void 0;
8266
+ const profileSource = strategyManifest?.contextRequirements?.tickerProfiles;
8267
+ if (profileSource) {
8268
+ const profile = await loadTickerProfileContext({
8269
+ ...profileSource,
8270
+ projectRoot,
8271
+ symbol: decisionSymbol,
8272
+ timestamp: candle.timestamp
8273
+ });
8274
+ if (!profile)
8275
+ throw new Error(
8276
+ "Required ticker profile is unavailable at decision time"
8277
+ );
8278
+ baseContext = withTickerProfile(baseContext, profile);
8279
+ }
7601
8280
  if (!coreContextRequirements.has("hyperliquidWhales")) {
7602
8281
  return baseContext;
7603
8282
  }
@@ -7765,6 +8444,8 @@ var createStrategyRuntime = ({
7765
8444
  runtime: pending2.runtime
7766
8445
  });
7767
8446
  return executeEntryDecision({
8447
+ portfolio,
8448
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
7768
8449
  connector,
7769
8450
  symbol,
7770
8451
  decision: pending2.decision,
@@ -7782,6 +8463,11 @@ var createStrategyRuntime = ({
7782
8463
  });
7783
8464
  };
7784
8465
  const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
8466
+ portfolioMarket = { candle, btcCandle };
8467
+ if (portfolioContext)
8468
+ await portfolioContext(
8469
+ env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now()
8470
+ );
7785
8471
  appendCurrentMarketData(candle, btcCandle, options.ethCandle);
7786
8472
  const ethCandle = resolveEthCandle(candle, options.ethCandle);
7787
8473
  indicatorsState.setCurrentBar(candle, btcCandle, ethCandle);
@@ -7884,15 +8570,10 @@ var createStrategyRuntime = ({
7884
8570
  if (!makeOrdersEnabled) {
7885
8571
  return decision.code;
7886
8572
  }
7887
- const closeGate = await invokeGateHooks(
7888
- "beforeClosePosition",
7889
- decisionManifest?.hooks?.beforeClosePosition,
7890
- {
7891
- ctx: decisionHookCtx,
7892
- market,
7893
- decision
7894
- },
7895
- { decision, market }
8573
+ const closeGate = await authorizeClose(
8574
+ decision,
8575
+ market,
8576
+ decisionHookCtx.strategyName
7896
8577
  );
7897
8578
  if (closeGate?.allow === false) {
7898
8579
  return closeGate.reason ? `CLOSE_BLOCKED_BY_HOOK:${closeGate.reason}` : "CLOSE_BLOCKED_BY_HOOK";
@@ -7952,6 +8633,13 @@ var createStrategyRuntime = ({
7952
8633
  });
7953
8634
  const signal = decision.signal;
7954
8635
  if (signal) {
8636
+ if (portfolio)
8637
+ signal.additionalIndicators = {
8638
+ ...signal.additionalIndicators,
8639
+ portfolioContext: await portfolioContext(
8640
+ env === "BACKTEST" || env === "PARITY" ? decision.entryContext.timestamp : Date.now()
8641
+ )
8642
+ };
7955
8643
  if (runtimeLineage) signal.runtimeLineage = runtimeLineage;
7956
8644
  if (universe) signal.universe = universe;
7957
8645
  if (assetClass) signal.assetClass = assetClass;
@@ -8191,6 +8879,8 @@ var createStrategyRuntime = ({
8191
8879
  });
8192
8880
  }
8193
8881
  return executeEntryDecision({
8882
+ portfolio,
8883
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
8194
8884
  connector,
8195
8885
  symbol,
8196
8886
  decision,