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

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