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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -806,1668 +806,2402 @@ __export(mcp_exports, {
806
806
  requireMcpDeployment: () => requireMcpDeployment
807
807
  });
808
808
  module.exports = __toCommonJS(mcp_exports);
809
- var import_node_crypto6 = require("crypto");
810
- var import_indicators3 = require("@tradejs/core/indicators");
809
+ var import_node_crypto7 = require("crypto");
810
+ var import_indicators4 = require("@tradejs/core/indicators");
811
811
  var import_data6 = require("@tradejs/core/data");
812
- var import_redis4 = require("@tradejs/infra/redis");
812
+ var import_redis5 = require("@tradejs/infra/redis");
813
813
  var import_runtimeHeartbeats = require("@tradejs/infra/runtimeHeartbeats");
814
814
  var import_tradingAccounts3 = require("@tradejs/infra/tradingAccounts");
815
815
  var import_time3 = require("@tradejs/core/time");
816
816
 
817
- // src/strategyRuntime.ts
818
- var import_jev6 = require("@tradejs/core/jev");
817
+ // src/strategy/portfolioRuntime.ts
818
+ var import_node_crypto2 = require("crypto");
819
+ var import_portfolio2 = require("@tradejs/core/portfolio");
820
+ var import_redis2 = require("@tradejs/infra/redis");
819
821
 
820
- // src/jev.ts
821
- var import_promises = __toESM(require("fs/promises"));
822
- var import_node_path = __toESM(require("path"));
823
- var import_jev4 = require("@tradejs/core/jev");
824
- var import_jev5 = require("@tradejs/infra/jev");
825
- var import_userSettings = require("@tradejs/infra/userSettings");
826
- init_jevInput();
827
- init_jevInput();
828
- init_jevTraining();
829
- var resolveJevProvider = async (userName) => {
830
- const settings = await (0, import_userSettings.getUserSettings)(userName);
831
- const provider = {
832
- endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
833
- model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
834
- };
835
- (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
836
- if (!settings.JEV_API_KEY)
837
- throw new Error("Configure the Jev API key in Account settings");
838
- return provider;
839
- };
840
- var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
841
- var pending = /* @__PURE__ */ new Map();
842
- var pendingResponses = /* @__PURE__ */ new Map();
843
- var evaluateJevInput = async ({
844
- input,
845
- config,
846
- userName,
847
- projectRoot
848
- }) => {
849
- config = (0, import_jev4.parseJevConfig)(config);
850
- const inputHash = (0, import_jev5.jevHash)(input);
851
- const questions = questionsForJevInput(input);
852
- const questionsHash = (0, import_jev5.jevHash)(questions);
853
- const base = {
854
- questionSet: import_jev4.JEV_QUESTION_SET,
855
- schema: "tradejs-signal-assessment/v5",
856
- source: config.source,
857
- mode: config.mode,
858
- inputHash,
859
- questionsHash,
860
- geometryStatus: input.geometryStatus,
861
- levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
862
- };
863
- if (!input.questions.length)
864
- return {
865
- assessment: {
866
- ...base,
867
- status: "unavailable",
868
- model: "",
869
- scores: emptyScores(),
870
- confidence: emptyScores()
871
- }
872
- };
873
- if (config.source === "local") {
874
- const contents = await import_promises.default.readFile(
875
- import_node_path.default.resolve(projectRoot, config.modelFile),
876
- "utf8"
877
- );
878
- if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
879
- throw new Error("Local Jev gate checksum mismatch");
880
- const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
881
- const model = validateJevGateModel2(JSON.parse(contents));
882
- if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
883
- throw new Error("Local Jev gate strategy or question version mismatch");
884
- const scores = Object.fromEntries(
885
- import_jev4.JEV_DIMENSIONS.map((key) => [
886
- key,
887
- input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
888
- ])
889
- );
890
- return {
891
- assessment: {
892
- ...base,
893
- status: "available",
894
- model: config.modelSha256,
895
- scores,
896
- confidence: emptyScores()
822
+ // src/portfolio.ts
823
+ var import_portfolio = require("@tradejs/core/portfolio");
824
+ function createPortfolioCoordinator({
825
+ policy,
826
+ strategy,
827
+ store,
828
+ execution
829
+ }) {
830
+ const fingerprint3 = (0, import_portfolio.portfolioFingerprint)(policy, strategy);
831
+ let cache;
832
+ async function transact(now2, change, force = false) {
833
+ for (let attempt = 0; attempt < 16; attempt++) {
834
+ const previous = await store.read();
835
+ if (previous && previous.fingerprint !== fingerprint3)
836
+ throw new Error(
837
+ "Portfolio policy changed under existing state; reconcile the persisted scope before changing configuration"
838
+ );
839
+ const base = previous ?? (0, import_portfolio.createPortfolioState)(fingerprint3);
840
+ const bucket = Math.floor(now2 / policy.observationIntervalMs);
841
+ const observation = !force && cache?.revision === base.revision && cache.bucket === bucket ? cache.observation : await execution.observe(now2, base.reservations);
842
+ const reconciled = (0, import_portfolio.reconcilePortfolio)(
843
+ policy,
844
+ strategy,
845
+ base,
846
+ observation
847
+ );
848
+ for (const intent of base.closing) {
849
+ if (!observation.positions.some(
850
+ (p) => p.symbol === intent.position.symbol
851
+ ))
852
+ await execution.confirmClosed?.(intent, observation.asOf);
897
853
  }
898
- };
899
- }
900
- const provider = config.provider ?? await resolveJevProvider(userName);
901
- const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
902
- const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
903
- const file = import_node_path.default.join(dir, "records", `${id}.json`);
904
- const getRecord = async () => {
905
- const existing = await (0, import_jev5.readJevArtifact)(file);
906
- if (existing) {
907
- 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))
908
- throw new Error("Jev recording identity mismatch");
909
- (0, import_jev5.validateJevResponse)(existing.response, input.questions);
910
- for (const key of import_jev4.JEV_DIMENSIONS) {
911
- const expected = input.questions.includes(key) ? existing.response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null;
912
- if (existing.scores[key] !== expected)
913
- throw new Error("Jev recording scores do not match the response");
854
+ const changed = change(reconciled, observation);
855
+ const next = { ...changed.state, revision: base.revision + 1 };
856
+ if (await store.compare(previous, next)) {
857
+ cache = { revision: next.revision, bucket, observation };
858
+ return { state: next, observation, result: changed.result };
914
859
  }
915
- return existing;
860
+ cache = void 0;
916
861
  }
917
- if (config.source === "recorded")
918
- throw new Error(
919
- `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.`
862
+ throw new Error("Portfolio state contention; entry was not submitted");
863
+ }
864
+ async function tick(now2, force = false) {
865
+ const claimed = await transact(
866
+ now2,
867
+ (state, observation) => {
868
+ const commands = (0, import_portfolio.portfolioCloseCommands)(policy, state, observation);
869
+ return {
870
+ state: {
871
+ ...state,
872
+ closing: state.closing.map(
873
+ (c) => commands.some((x) => x.intent.positionId === c.positionId) ? { ...c, lastAttemptAt: observation.asOf } : c
874
+ )
875
+ },
876
+ result: commands
877
+ };
878
+ },
879
+ force
880
+ );
881
+ for (const command of claimed.result)
882
+ await execution.close(
883
+ command.position,
884
+ command.intent,
885
+ claimed.observation.asOf
920
886
  );
921
- const settings = await (0, import_userSettings.getUserSettings)(userName);
922
- if (!settings.JEV_API_KEY)
923
- throw new Error("Configure the Jev API key in Account settings");
924
- const apiKey = settings.JEV_API_KEY;
925
- const configuredProvider = await resolveJevProvider(userName);
926
- if (configuredProvider.endpoint !== provider.endpoint)
927
- throw new Error(
928
- "Jev account endpoint changed; update the frozen provider configuration"
887
+ if (claimed.result.length) {
888
+ cache = void 0;
889
+ const refreshed = await transact(
890
+ now2,
891
+ (state) => ({ state, result: void 0 }),
892
+ true
893
+ );
894
+ return (0, import_portfolio.buildPortfolioContext)(
895
+ policy,
896
+ strategy,
897
+ refreshed.state,
898
+ refreshed.observation
929
899
  );
930
- const started = Date.now();
931
- const state = stateForJevInput(input);
932
- const responseId = (0, import_jev5.jevHash)({
933
- state,
934
- questionsHash,
935
- provider,
936
- user: (0, import_jev5.jevHash)(userName)
937
- });
938
- const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
939
- let responsePromise = pendingResponses.get(responseFile);
940
- if (!responsePromise) {
941
- responsePromise = (async () => {
942
- const saved = await (0, import_jev5.readJevArtifact)(responseFile);
943
- if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
944
- const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
945
- await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
946
- return fresh;
947
- })();
948
- pendingResponses.set(responseFile, responsePromise);
949
- }
950
- let response;
951
- try {
952
- response = await responsePromise;
953
- } finally {
954
- pendingResponses.delete(responseFile);
955
900
  }
956
- const scores = Object.fromEntries(
957
- import_jev4.JEV_DIMENSIONS.map((key) => [
958
- key,
959
- input.questions.includes(key) ? response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null
960
- ])
901
+ return (0, import_portfolio.buildPortfolioContext)(
902
+ policy,
903
+ strategy,
904
+ claimed.state,
905
+ claimed.observation
961
906
  );
962
- const value = {
963
- schema: "tradejs-jev-record/v4",
964
- id,
965
- inputHash,
966
- questionsHash,
967
- provider,
968
- input,
969
- response,
970
- scores,
971
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
972
- elapsedMs: Date.now() - started
973
- };
974
- await (0, import_jev5.writeJevArtifact)(file, value);
975
- return await (0, import_jev5.readJevArtifact)(file);
976
- };
977
- const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
978
- let promise = pending.get(pendingKey);
979
- if (!promise) {
980
- promise = getRecord();
981
- pending.set(pendingKey, promise);
982
907
  }
983
- let record;
984
- try {
985
- record = await promise;
986
- } finally {
987
- pending.delete(pendingKey);
908
+ async function updateReservation(now2, id, submitted, qty) {
909
+ return transact(
910
+ now2,
911
+ (s, o) => {
912
+ const exists = s.reservations.some((r) => r.id === id);
913
+ const blocked = submitted && (!exists || s.closing.length > 0 || o.asOf <= s.blockedThrough);
914
+ return {
915
+ state: {
916
+ ...s,
917
+ reservations: s.reservations.flatMap(
918
+ (r) => r.id !== id ? [r] : submitted && !blocked ? [{ ...r, submitted: true, qty: qty ?? r.qty }] : []
919
+ )
920
+ },
921
+ result: !blocked
922
+ };
923
+ },
924
+ true
925
+ );
988
926
  }
989
927
  return {
990
- record,
991
- assessment: {
992
- ...base,
993
- status: "available",
994
- recordId: id,
995
- model: record.response.model,
996
- scores: record.scores,
997
- confidence: Object.fromEntries(
998
- import_jev4.JEV_DIMENSIONS.map((key) => [
999
- key,
1000
- record.response.answers[key]?.confidence ?? null
1001
- ])
1002
- )
928
+ async context(now2) {
929
+ const state = await store.read() ?? (0, import_portfolio.createPortfolioState)(fingerprint3);
930
+ if (state.fingerprint !== fingerprint3)
931
+ throw new Error("Portfolio policy changed under existing state");
932
+ const observation = await execution.observe(now2, state.reservations);
933
+ (0, import_portfolio.validatePortfolioObservation)(observation);
934
+ return (0, import_portfolio.buildPortfolioContext)(policy, strategy, state, observation);
935
+ },
936
+ tick,
937
+ async submit(now2, entry, send) {
938
+ await tick(now2);
939
+ const admitted = await transact(
940
+ now2,
941
+ (s, o) => {
942
+ const r = (0, import_portfolio.reservePortfolioEntry)(policy, strategy, s, o, {
943
+ ...entry,
944
+ submitted: false,
945
+ oppositeHandled: false
946
+ });
947
+ return { state: r.state, result: r.reason };
948
+ },
949
+ true
950
+ );
951
+ if (admitted.result) return { reason: admitted.result };
952
+ let submitted = false;
953
+ try {
954
+ const value = await send(
955
+ async (qty) => {
956
+ if (!Number.isFinite(qty) || qty <= 0)
957
+ throw new Error("Invalid submitted portfolio quantity");
958
+ submitted = true;
959
+ const update = await updateReservation(now2, entry.id, true, qty);
960
+ if (!update.result)
961
+ throw new Error("PORTFOLIO_SUBMISSION_NO_LONGER_ADMITTED");
962
+ },
963
+ async () => {
964
+ await transact(
965
+ now2,
966
+ (s) => ({
967
+ state: {
968
+ ...s,
969
+ reservations: s.reservations.filter((r) => r.id !== entry.id),
970
+ completedSignals: {
971
+ ...s.completedSignals,
972
+ [entry.signalId]: now2
973
+ }
974
+ },
975
+ result: void 0
976
+ }),
977
+ true
978
+ );
979
+ }
980
+ );
981
+ if (!submitted) await updateReservation(now2, entry.id, false);
982
+ else await tick(now2, true);
983
+ return { value };
984
+ } catch (error) {
985
+ if (!submitted) await updateReservation(now2, entry.id, false);
986
+ throw error;
987
+ }
1003
988
  }
1004
989
  };
1005
- };
1006
- var assessSignalWithJev = async ({
1007
- signal,
1008
- config,
1009
- userName,
1010
- projectRoot,
1011
- strict,
1012
- scope = "runtime"
1013
- }) => {
1014
- let assessment;
1015
- try {
1016
- const input = buildJevInput(signal);
1017
- ({ assessment } = await evaluateJevInput({
1018
- input,
1019
- config,
1020
- userName,
1021
- projectRoot
1022
- }));
1023
- } catch (error) {
1024
- if (strict) throw error;
1025
- assessment = {
1026
- questionSet: import_jev4.JEV_QUESTION_SET,
1027
- schema: "tradejs-signal-assessment/v5",
1028
- source: config.source,
1029
- mode: config.mode,
1030
- status: "unavailable",
1031
- inputHash: "",
1032
- questionsHash: "",
1033
- geometryStatus: "absent",
1034
- levelsValid: null,
1035
- model: config.provider?.model ?? config.modelSha256 ?? "",
1036
- scores: emptyScores(),
1037
- confidence: emptyScores()
1038
- };
1039
- }
1040
- signal.assessment = assessment;
1041
- signal.additionalIndicators = {
1042
- ...signal.additionalIndicators,
1043
- jev: (0, import_jev4.jevAssessmentFeatures)(assessment)
990
+ }
991
+ function createMemoryPortfolioStore() {
992
+ let state = null;
993
+ return {
994
+ read: async () => state === null ? null : structuredClone(state),
995
+ compare: async (expected, next) => {
996
+ if ((state?.revision ?? -1) !== (expected?.revision ?? -1)) return false;
997
+ state = structuredClone(next);
998
+ return true;
999
+ }
1044
1000
  };
1045
- const identity = {
1046
- signalId: signal.signalId,
1047
- strategy: signal.strategy,
1048
- symbol: signal.symbol,
1049
- timestamp: signal.timestamp,
1050
- scope,
1051
- user: (0, import_jev5.jevHash)(userName),
1052
- config,
1053
- assessment
1054
- };
1055
- await (0, import_jev5.writeJevArtifact)(
1056
- import_node_path.default.resolve(
1057
- projectRoot,
1058
- config.recordsDir ?? "data/ai/jev",
1059
- "decisions",
1060
- `${(0, import_jev5.jevHash)(identity)}.json`
1061
- ),
1062
- identity
1063
- );
1064
- return assessment;
1065
- };
1066
-
1067
- // src/strategyRuntime.ts
1068
- var import_constants5 = require("@tradejs/core/constants");
1069
- var import_strategies6 = require("@tradejs/core/strategies");
1070
- var import_logger11 = require("@tradejs/infra/logger");
1071
-
1072
- // src/strategyHelpers/runtime.ts
1073
- var import_logger9 = require("@tradejs/infra/logger");
1074
- var import_constants3 = require("@tradejs/core/constants");
1075
- var import_ml2 = require("@tradejs/infra/ml");
1001
+ }
1076
1002
 
1077
- // src/strategy/entrySubmissionValidation.ts
1003
+ // src/runtimeJournal.ts
1004
+ var import_node_crypto = require("crypto");
1005
+ var import_constants = require("@tradejs/core/constants");
1006
+ var import_time = require("@tradejs/core/time");
1007
+ var import_trade = require("@tradejs/core/trade");
1078
1008
  var import_logger = require("@tradejs/infra/logger");
1079
-
1080
- // src/strategy/entryExecutionValidation.ts
1081
- var EntrySubmissionValidationError = class extends Error {
1082
- constructor() {
1083
- super(...arguments);
1084
- this.name = "EntrySubmissionValidationError";
1085
- }
1086
- };
1087
- var resolveEntryExecutionLimits = (config, direction) => {
1088
- const side = config[direction];
1089
- const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
1090
- const maximum = config.MAX_LOSS_VALUE;
1091
- if (minimum == null && maximum == null) return void 0;
1092
- const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
1093
- const maxLossValue = maximum == null ? void 0 : Number(maximum);
1094
- const feeRate = Number(config.RISK_FEE_RATE ?? 0);
1095
- const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
1096
- const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
1097
- const slippageBps = slippage + impact;
1098
- 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)
1099
- throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
1100
- return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
1101
- };
1102
- var validateEntryExecution = ({
1009
+ var import_redis = require("@tradejs/infra/redis");
1010
+ var now = () => Date.now();
1011
+ var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
1012
+ var calculateClosedPnl = ({
1103
1013
  direction,
1104
- price,
1105
- qty,
1106
- stopLossPrice,
1107
- takeProfitPrice,
1108
- limits
1014
+ entryPrice,
1015
+ exitPrice,
1016
+ qty
1109
1017
  }) => {
1110
- if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
1111
- throw new EntrySubmissionValidationError(
1112
- "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1113
- );
1114
- const side = direction === "LONG" ? 1 : -1;
1115
- if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
1116
- throw new EntrySubmissionValidationError(
1117
- "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
1118
- );
1119
- const slippage = limits.slippageBps / 1e4;
1120
- const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
1121
- const entryCost = cost(price, side);
1122
- const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
1123
- const rewardPerUnit = Math.max(
1124
- 0,
1125
- Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
1126
- );
1127
- const netRiskRatio = rewardPerUnit / lossPerUnit;
1128
- const plannedLossValue = qty * lossPerUnit;
1129
- if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
1130
- throw new EntrySubmissionValidationError(
1131
- "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1132
- );
1133
- if (limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio)
1134
- throw new EntrySubmissionValidationError(
1135
- "ENTRY_RR_BELOW_MIN_AT_SUBMISSION"
1136
- );
1137
- const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
1138
- return { netRiskRatio, plannedLossValue, riskExceeded };
1018
+ const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
1019
+ return Number.isFinite(pnl) ? pnl : null;
1139
1020
  };
1140
-
1141
- // src/strategy/entrySubmissionValidation.ts
1142
- var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
1143
- var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
1144
- var getOrderArrivalSnapshot = async ({
1145
- connector,
1146
- symbol,
1147
- requireTimestamp = false
1148
- }) => {
1149
- if (typeof connector.getTopOfBookTicker !== "function") {
1150
- return {
1151
- arrivalSnapshotTime: Date.now(),
1152
- arrivalSource: "unavailable",
1153
- bid: null,
1154
- ask: null,
1155
- arrivalMid: null,
1156
- spreadBps: null
1157
- };
1021
+ var createRuntimeOrderId = (strategy) => {
1022
+ const prefix = (0, import_trade.createRuntimeOrderLinkPrefix)(strategy);
1023
+ if (prefix === "tjs-") {
1024
+ return `tjs-${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
1025
+ }
1026
+ return `${prefix}${toRandomOrderSuffix()}`;
1027
+ };
1028
+ var recordRuntimeTradeOpen = async (params) => {
1029
+ const { userName } = params;
1030
+ if (!userName) {
1031
+ return null;
1158
1032
  }
1033
+ const record = {
1034
+ ...params,
1035
+ entryCount: 1,
1036
+ lastEntryPrice: params.entryPrice,
1037
+ lastEntryQty: params.qty,
1038
+ lastEntryTimestamp: params.entryTimestamp,
1039
+ status: "active",
1040
+ currentPrice: params.entryPrice,
1041
+ currentPnl: 0,
1042
+ closedPnl: null,
1043
+ exitPrice: null,
1044
+ exitTimestamp: null,
1045
+ lastSyncedAt: now()
1046
+ };
1047
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(record.entryTimestamp);
1048
+ const runtimeScopeId = record.deploymentId ?? record.accountId;
1159
1049
  try {
1160
- const ticker = await connector.getTopOfBookTicker(symbol);
1161
- const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
1162
- const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
1163
- const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
1164
- const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
1165
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
1166
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
1167
- return {
1168
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
1169
- arrivalSource: "top_of_book",
1170
- bid,
1171
- ask,
1172
- arrivalMid,
1173
- spreadBps
1174
- };
1050
+ await Promise.all([
1051
+ (0, import_redis.setData)(import_redis.redisKeys.runtimeTrade(userName, record.orderId), record, {
1052
+ expire: 0
1053
+ }),
1054
+ (0, import_redis.setHashJsonField)(
1055
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1056
+ record.orderId,
1057
+ record,
1058
+ { expire: 0 }
1059
+ ),
1060
+ (0, import_redis.setData)(
1061
+ import_redis.redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
1062
+ { orderId: record.orderId },
1063
+ { expire: 0 }
1064
+ )
1065
+ ]);
1175
1066
  } catch (error) {
1176
- import_logger.logger.warn(
1177
- "runtime order arrival snapshot failed: %s %s",
1178
- symbol,
1067
+ import_logger.logger.error(
1068
+ "runtime trade open journal failed: %s %s",
1069
+ record.symbol,
1179
1070
  error?.message || String(error)
1180
1071
  );
1181
- return {
1182
- arrivalSnapshotTime: Date.now(),
1183
- arrivalSource: "top_of_book_error",
1184
- bid: null,
1185
- ask: null,
1186
- arrivalMid: null,
1187
- spreadBps: null
1188
- };
1189
1072
  }
1073
+ return record;
1190
1074
  };
1191
- var validateEntryProtectionAtArrival = ({
1192
- direction,
1193
- signalPrice,
1194
- bid,
1195
- ask,
1196
- arrivalMid,
1197
- takeProfits,
1198
- stopLossPrice
1199
- }) => {
1200
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
1201
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
1202
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
1203
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
1204
- }
1205
- for (const takeProfit of takeProfits) {
1206
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
1207
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
1208
- }
1075
+ var recordRuntimeTradeIncrease = async (params) => {
1076
+ const {
1077
+ userName,
1078
+ strategy,
1079
+ symbol,
1080
+ direction,
1081
+ resultingQty,
1082
+ resultingEntryPrice,
1083
+ addedQty,
1084
+ addedEntryPrice,
1085
+ entryTimestamp,
1086
+ fee,
1087
+ accountId,
1088
+ deploymentId
1089
+ } = params;
1090
+ if (!userName) {
1091
+ return null;
1209
1092
  }
1210
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
1211
- throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
1093
+ const existing = await getActiveRuntimeTrade({
1094
+ userName,
1095
+ symbol,
1096
+ accountId,
1097
+ deploymentId
1098
+ });
1099
+ if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
1100
+ return null;
1212
1101
  }
1213
- };
1214
- var createEntrySubmissionValidation = ({
1215
- connector,
1216
- symbol,
1217
- direction,
1218
- takeProfits,
1219
- stopLossPrice,
1220
- limits,
1221
- onValidated
1222
- }) => {
1223
- if (!limits) return void 0;
1224
- if (!connector.capabilities?.orderSubmissionValidation)
1225
- throw new EntrySubmissionValidationError(
1226
- "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
1227
- );
1228
- return async ({ qty, executionPrice }) => {
1229
- const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
1230
- const snapshot2 = simulated && executionPrice != null ? {
1231
- arrivalSnapshotTime: Date.now(),
1232
- arrivalSource: "simulation",
1233
- bid: executionPrice,
1234
- ask: executionPrice,
1235
- arrivalMid: executionPrice,
1236
- spreadBps: 0
1237
- } : await getOrderArrivalSnapshot({
1238
- connector,
1239
- symbol,
1240
- requireTimestamp: true
1241
- });
1242
- const age = Date.now() - snapshot2.arrivalSnapshotTime;
1243
- if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
1244
- throw new EntrySubmissionValidationError(
1245
- "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
1246
- );
1247
- if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
1248
- throw new EntrySubmissionValidationError(
1249
- "ENTRY_QUOTE_STALE_AT_SUBMISSION"
1250
- );
1251
- const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
1252
- validateEntryProtectionAtArrival({
1253
- direction,
1254
- signalPrice: price,
1255
- bid: snapshot2.bid,
1256
- ask: snapshot2.ask,
1257
- arrivalMid: snapshot2.arrivalMid,
1258
- takeProfits,
1259
- stopLossPrice
1260
- });
1261
- const prices = takeProfits.map((tp) => tp.price);
1262
- const economics2 = validateEntryExecution({
1263
- direction,
1264
- price,
1265
- qty,
1266
- stopLossPrice,
1267
- takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
1268
- limits
1269
- });
1270
- onValidated?.(snapshot2, economics2);
1102
+ const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
1103
+ const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
1104
+ const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
1105
+ const next = {
1106
+ ...existing,
1107
+ ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
1108
+ qty: resultingQty,
1109
+ entryPrice: resultingEntryPrice,
1110
+ entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
1111
+ lastEntryPrice: addedEntryPrice,
1112
+ lastEntryQty: addedQty,
1113
+ lastEntryTimestamp: entryTimestamp,
1114
+ currentPrice: resultingEntryPrice,
1115
+ currentPnl: 0,
1116
+ fee: openFee,
1117
+ openFee,
1118
+ totalFee,
1119
+ lastSyncedAt: now()
1271
1120
  };
1272
- };
1273
-
1274
- // src/ai.ts
1275
- var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
1276
- var import_redis = require("@tradejs/infra/redis");
1277
-
1278
- // src/aiShared.ts
1279
- var MAX_AI_SERIES_POINTS = 5;
1280
- var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
1281
- "tradejs.indicators.compactSnapshot"
1282
- );
1283
- var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
1284
- var trimSeriesDeep = (value) => {
1285
- if (Array.isArray(value)) {
1286
- const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
1287
- const isMatrix = trimmed.every((item) => Array.isArray(item));
1288
- if (isMatrix) {
1289
- return trimmed;
1290
- }
1291
- return trimmed.map(
1292
- (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
1293
- );
1294
- }
1295
- if (value && typeof value === "object") {
1296
- return Object.fromEntries(
1297
- Object.entries(value).map(([key, nested]) => [
1298
- key,
1299
- trimSeriesDeep(nested)
1300
- ])
1121
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
1122
+ try {
1123
+ await Promise.all([
1124
+ (0, import_redis.setData)(import_redis.redisKeys.runtimeTrade(userName, existing.orderId), next, {
1125
+ expire: 0
1126
+ }),
1127
+ (0, import_redis.setHashJsonField)(
1128
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1129
+ existing.orderId,
1130
+ next,
1131
+ { expire: 0 }
1132
+ )
1133
+ ]);
1134
+ } catch (error) {
1135
+ import_logger.logger.error(
1136
+ "runtime trade increase journal failed: %s %s",
1137
+ symbol,
1138
+ error?.message || String(error)
1301
1139
  );
1302
1140
  }
1303
- return value;
1141
+ return next;
1304
1142
  };
1305
- var buildCompactAiIndicatorsSnapshot = (value) => {
1306
- const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
1307
- if (typeof compactSnapshot === "function") {
1308
- return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
1143
+ var getActiveRuntimeTrade = async (params) => {
1144
+ const { userName, symbol, accountId, deploymentId } = params;
1145
+ if (!userName) {
1146
+ return null;
1309
1147
  }
1310
- return trimSeriesDeep(value);
1311
- };
1312
-
1313
- // src/aiMarketContext.ts
1314
- var toRecord = (value) => {
1315
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1148
+ const activeRef = await (0, import_redis.getData)(
1149
+ import_redis.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
1150
+ null
1151
+ );
1152
+ const orderId = String(activeRef?.orderId || "").trim();
1153
+ if (!orderId) {
1316
1154
  return null;
1317
1155
  }
1318
- return value;
1319
- };
1320
- var toFiniteNumber = (value) => {
1321
- const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
1322
- return Number.isFinite(numeric) ? numeric : null;
1323
- };
1324
- var roundTo = (value, decimals) => {
1325
- const factor = 10 ** decimals;
1326
- return Math.round(value * factor) / factor;
1327
- };
1328
- var buildMissingSpreadContext = () => ({
1329
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1330
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1331
- available: false,
1332
- value: null,
1333
- zScore: null,
1334
- bps: null,
1335
- absBps: null,
1336
- bias: null,
1337
- severity: null
1338
- });
1339
- var buildSpreadContextFromSignal = (signal) => {
1340
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1341
- const relative = toRecord(baseContext?.relative);
1342
- const execution = toRecord(relative?.execution);
1343
- const spread = toFiniteNumber(execution?.venueSpread);
1344
- const zScore = toFiniteNumber(execution?.venueSpreadZScore);
1345
- if (spread == null) {
1346
- return buildMissingSpreadContext();
1156
+ const existing = await (0, import_redis.getData)(
1157
+ import_redis.redisKeys.runtimeTrade(userName, orderId),
1158
+ null
1159
+ );
1160
+ if (!existing || existing.status !== "active") {
1161
+ await (0, import_redis.delKey)(
1162
+ import_redis.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
1163
+ );
1164
+ return null;
1347
1165
  }
1348
- const value = roundTo(spread, 8);
1349
- const bps = roundTo(value * 1e4, 2);
1350
- const absBps = Math.abs(bps);
1351
- const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
1352
- const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
1353
- return {
1354
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1355
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1356
- available: true,
1357
- value,
1358
- zScore,
1359
- bps,
1360
- absBps,
1361
- bias,
1362
- severity
1363
- };
1166
+ return existing;
1364
1167
  };
1365
- var buildTrueDeltaContextFromSignal = (signal) => {
1366
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1367
- const participation = toRecord(baseContext?.participation);
1368
- const delta = toRecord(participation?.delta);
1369
- const source = String(delta?.source ?? "");
1370
- const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
1371
- if (!delta || !isTrueDeltaSource) {
1372
- return {
1373
- source: source || null,
1374
- available: false,
1375
- buyPressurePct: null,
1376
- buyVolume: null,
1377
- sellVolume: null,
1378
- netDelta: null,
1379
- deltaPct: null,
1380
- signedVolumeZScore: null
1381
- };
1168
+ var markRuntimeTradeClosed = async (params) => {
1169
+ const {
1170
+ userName,
1171
+ expectedOrderId,
1172
+ symbol,
1173
+ strategy,
1174
+ exitPrice,
1175
+ exitTimestamp,
1176
+ closedPnl,
1177
+ exitType,
1178
+ accountId,
1179
+ deploymentId
1180
+ } = params;
1181
+ if (!userName) {
1182
+ return null;
1382
1183
  }
1383
- return {
1384
- source,
1385
- available: true,
1386
- buyPressurePct: toFiniteNumber(delta.buyPressurePct),
1387
- buyVolume: toFiniteNumber(delta.buyVolume),
1388
- sellVolume: toFiniteNumber(delta.sellVolume),
1389
- netDelta: toFiniteNumber(delta.netDelta),
1390
- deltaPct: toFiniteNumber(delta.deltaPct),
1391
- signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
1392
- };
1393
- };
1394
- var buildTradeFlowContextFromSignal = (signal) => {
1395
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1396
- const participation = toRecord(baseContext?.participation);
1397
- const tradeFlow = toRecord(participation?.tradeFlow);
1398
- if (!tradeFlow) {
1399
- return {
1400
- source: null,
1401
- available: false,
1402
- interval: null,
1403
- stale: null,
1404
- trades: null,
1405
- buyPressurePct: null,
1406
- netBaseDelta: null,
1407
- netQuoteDelta: null
1408
- };
1184
+ const existing = expectedOrderId ? await (0, import_redis.getDataStrict)(
1185
+ import_redis.redisKeys.runtimeTrade(userName, expectedOrderId)
1186
+ ) : await getActiveRuntimeTrade({
1187
+ userName,
1188
+ symbol,
1189
+ accountId,
1190
+ deploymentId
1191
+ });
1192
+ if (!existing || expectedOrderId && existing.orderId !== expectedOrderId) {
1193
+ return null;
1409
1194
  }
1410
- return {
1411
- source: String(tradeFlow.source ?? ""),
1412
- available: true,
1413
- interval: String(tradeFlow.interval ?? ""),
1414
- stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
1415
- trades: toFiniteNumber(tradeFlow.trades),
1416
- buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
1417
- netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
1418
- netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
1195
+ const orderId = existing.orderId;
1196
+ if (strategy && existing.strategy !== strategy) {
1197
+ return null;
1198
+ }
1199
+ const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
1200
+ const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
1201
+ direction: existing.direction,
1202
+ entryPrice: existing.entryPrice,
1203
+ exitPrice: resolvedExitPrice,
1204
+ qty: existing.qty
1205
+ }) : existing.closedPnl ?? existing.currentPnl ?? null;
1206
+ const next = expectedOrderId && existing.status === "closed" ? existing : {
1207
+ ...existing,
1208
+ status: "closed",
1209
+ currentPrice: resolvedExitPrice,
1210
+ currentPnl: resolvedClosedPnl,
1211
+ closedPnl: resolvedClosedPnl,
1212
+ exitPrice: resolvedExitPrice,
1213
+ exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
1214
+ exitType: exitType ?? existing.exitType ?? null,
1215
+ lastSyncedAt: now()
1419
1216
  };
1420
- };
1421
- var buildMarketBreadthContextFromSignal = (signal) => {
1422
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1423
- const relative = toRecord(baseContext?.relative);
1424
- const breadth = toRecord(relative?.marketBreadth);
1425
- if (!breadth) {
1426
- return {
1427
- source: null,
1428
- available: false,
1429
- universe: null,
1430
- interval: null,
1431
- stale: null,
1432
- symbolsCount: null,
1433
- advanceDeclineRatio: null,
1434
- pctAboveMa20: null,
1435
- pctAboveMa50: null,
1436
- equalWeightedReturn: null,
1437
- volumeWeightedReturn: null,
1438
- dispersion: null
1439
- };
1217
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
1218
+ const closeDayKey = (0, import_time.getRuntimeStorageDayKey)(next.exitTimestamp);
1219
+ try {
1220
+ await Promise.all([
1221
+ (expectedOrderId ? import_redis.setDataStrict : import_redis.setData)(
1222
+ import_redis.redisKeys.runtimeTrade(userName, orderId),
1223
+ next,
1224
+ {
1225
+ expire: import_constants.TTL_1M
1226
+ }
1227
+ ),
1228
+ (expectedOrderId ? import_redis.setHashJsonFieldStrict : import_redis.setHashJsonField)(
1229
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1230
+ orderId,
1231
+ next,
1232
+ { expire: import_constants.TTL_1M }
1233
+ ),
1234
+ (expectedOrderId ? import_redis.setHashJsonFieldStrict : import_redis.setHashJsonField)(
1235
+ import_redis.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
1236
+ orderId,
1237
+ next,
1238
+ { expire: import_constants.TTL_1M }
1239
+ ),
1240
+ expectedOrderId ? (0, import_redis.deleteRuntimeTradeReference)(
1241
+ import_redis.redisKeys.runtimeActiveTrade(
1242
+ userName,
1243
+ symbol,
1244
+ deploymentId ?? accountId
1245
+ ),
1246
+ expectedOrderId
1247
+ ) : (0, import_redis.delKey)(
1248
+ import_redis.redisKeys.runtimeActiveTrade(
1249
+ userName,
1250
+ symbol,
1251
+ deploymentId ?? accountId
1252
+ )
1253
+ )
1254
+ ]);
1255
+ } catch (error) {
1256
+ if (expectedOrderId) throw error;
1257
+ import_logger.logger.error(
1258
+ "runtime trade close journal failed: %s %s",
1259
+ symbol,
1260
+ error?.message || String(error)
1261
+ );
1440
1262
  }
1441
- return {
1442
- source: String(breadth.source ?? ""),
1443
- available: true,
1444
- universe: String(breadth.universe ?? ""),
1445
- interval: String(breadth.interval ?? ""),
1446
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
1447
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
1448
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
1449
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
1450
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
1451
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
1452
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
1453
- dispersion: toFiniteNumber(breadth.dispersion)
1454
- };
1263
+ return next;
1455
1264
  };
1456
- var buildMarketBreadthsContextFromSignal = (signal) => {
1457
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1458
- const relative = toRecord(baseContext?.relative);
1459
- const breadths = toRecord(relative?.marketBreadths);
1460
- const build = (key) => {
1461
- const breadth = toRecord(breadths?.[key]);
1462
- if (!breadth) {
1265
+
1266
+ // src/strategy/portfolioRuntime.ts
1267
+ var coordinators = /* @__PURE__ */ new WeakMap();
1268
+ async function observeRuntimePortfolio(connector, binding, asOf, reservations, observedNow = Date.now) {
1269
+ if (!connector.getOpenPositionPnl || !connector.getEntryExecutions)
1270
+ throw new Error(
1271
+ "Portfolio policies require position marks and entry executions"
1272
+ );
1273
+ const orders = await Promise.all(
1274
+ reservations.map(async (r) => {
1275
+ if (r.submitted && connector.getEntryOrderObservation) {
1276
+ const fact2 = await connector.getEntryOrderObservation({
1277
+ symbol: r.symbol,
1278
+ orderId: r.id
1279
+ });
1280
+ if (fact2) return fact2;
1281
+ }
1282
+ const executions = r.submitted ? await connector.getEntryExecutions({
1283
+ symbol: r.symbol,
1284
+ startTime: r.timestamp,
1285
+ endTime: asOf
1286
+ }) : [];
1287
+ const filledQty = executions.filter(
1288
+ (e) => (e.orderLinkId === r.id || e.orderId === r.id) && e.direction === r.direction
1289
+ ).reduce((sum, e) => sum + e.qty, 0);
1463
1290
  return {
1464
- source: null,
1465
- available: false,
1466
- universe: null,
1467
- interval: null,
1468
- stale: null,
1469
- symbolsCount: null,
1470
- advanceDeclineRatio: null,
1471
- pctAboveMa20: null,
1472
- pctAboveMa50: null,
1473
- equalWeightedReturn: null,
1474
- volumeWeightedReturn: null,
1475
- dispersion: null
1291
+ id: r.id,
1292
+ status: filledQty >= r.qty ? "filled" : filledQty > 0 ? "partial" : "pending",
1293
+ filledQty,
1294
+ positionId: r.id
1295
+ };
1296
+ })
1297
+ );
1298
+ const snapshots2 = await connector.getOpenPositionPnl();
1299
+ const positions = await Promise.all(
1300
+ snapshots2.map(async (p) => {
1301
+ const reserved = reservations.find(
1302
+ (r) => r.symbol === p.symbol && r.direction === p.direction && orders.some(
1303
+ (o) => o.id === r.id && o.filledQty > 0 && p.qty <= o.filledQty * (1 + 1e-8)
1304
+ )
1305
+ );
1306
+ const trade = await getActiveRuntimeTrade({
1307
+ ...binding,
1308
+ symbol: p.symbol
1309
+ });
1310
+ 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;
1311
+ return {
1312
+ id: reserved?.id ?? owner?.orderId ?? `unknown:${p.symbol}:${p.direction}`,
1313
+ strategy: reserved?.strategy ?? owner?.strategy ?? null,
1314
+ symbol: p.symbol,
1315
+ direction: p.direction,
1316
+ qty: p.qty,
1317
+ entryPrice: p.price,
1318
+ markPrice: p.currentPrice,
1319
+ markedAt: p.observedAt ?? null,
1320
+ stopLossPrice: p.slPrice ?? p.stopLossPrice ?? null
1476
1321
  };
1322
+ })
1323
+ );
1324
+ return { asOf: Math.max(asOf, observedNow()), positions, orders };
1325
+ }
1326
+ function persistentStore(key) {
1327
+ const raw = /* @__PURE__ */ new WeakMap();
1328
+ return {
1329
+ async read() {
1330
+ const value = await import_redis2.portfolioStateStorage.read(key);
1331
+ if (value === null) return null;
1332
+ const state = JSON.parse(value);
1333
+ (0, import_portfolio2.assertPortfolioState)(state);
1334
+ raw.set(state, value);
1335
+ return state;
1336
+ },
1337
+ async compare(previous, next) {
1338
+ const expected = previous === null ? null : raw.get(previous);
1339
+ if (expected === void 0)
1340
+ throw new Error("Portfolio CAS requires a storage snapshot");
1341
+ return import_redis2.portfolioStateStorage.compare(key, expected, JSON.stringify(next));
1477
1342
  }
1478
- return {
1479
- source: String(breadth.source ?? ""),
1480
- available: true,
1481
- universe: String(breadth.universe ?? ""),
1482
- interval: String(breadth.interval ?? ""),
1483
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
1484
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
1485
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
1486
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
1487
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
1488
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
1489
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
1490
- dispersion: toFiniteNumber(breadth.dispersion)
1491
- };
1492
1343
  };
1493
- return {
1494
- top5: build("top5"),
1495
- top10: build("top10"),
1496
- top30: build("top30"),
1497
- top50: build("top50"),
1498
- top100: build("top100")
1344
+ }
1345
+ function getPortfolioRuntime({
1346
+ value,
1347
+ connectorName,
1348
+ strategy,
1349
+ connector,
1350
+ env,
1351
+ userName,
1352
+ accountId,
1353
+ deploymentId
1354
+ }) {
1355
+ const policy = (0, import_portfolio2.parsePortfolioPolicy)(value);
1356
+ if (!policy) return void 0;
1357
+ const replay = (env === "BACKTEST" || env === "PARITY") && Reflect.get(connector, "__tradejsReplayConnector") === true;
1358
+ if ((env === "BACKTEST" || env === "PARITY") && (!replay || !connector.getPortfolioObservation))
1359
+ throw new Error(
1360
+ "Portfolio policies require shared portfolio replay; isolated symbol backtests are unsupported"
1361
+ );
1362
+ if (!replay && (!userName || !accountId || !deploymentId || !connector.getOpenPositionPnl || !connector.getEntryExecutions))
1363
+ throw new Error(
1364
+ "Portfolio runtime requires exact user/account/deployment binding, marks and executions"
1365
+ );
1366
+ const binding = {
1367
+ userName,
1368
+ accountId,
1369
+ deploymentId
1499
1370
  };
1371
+ const scope = policy.scope === "strategy" ? strategy : policy.scope === "account" ? "account" : [...policy.scope.strategies].sort();
1372
+ const key = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify([binding, connectorName, connector.universe, scope])).digest("hex");
1373
+ const fingerprint3 = (0, import_portfolio2.portfolioFingerprint)(policy, strategy);
1374
+ const cacheKey = key;
1375
+ let registry = coordinators.get(connector);
1376
+ if (!registry) {
1377
+ registry = /* @__PURE__ */ new Map();
1378
+ coordinators.set(connector, registry);
1379
+ }
1380
+ const existing = registry.get(cacheKey);
1381
+ if (existing) {
1382
+ if (existing.fingerprint !== fingerprint3)
1383
+ throw new Error("Conflicting portfolio configuration for the same scope");
1384
+ return existing.coordinator;
1385
+ }
1386
+ const coordinator = createPortfolioCoordinator({
1387
+ policy,
1388
+ strategy,
1389
+ store: replay ? createMemoryPortfolioStore() : persistentStore(key),
1390
+ execution: {
1391
+ async confirmClosed(intent, asOf) {
1392
+ if (replay) return;
1393
+ const position = intent.position;
1394
+ await markRuntimeTradeClosed({
1395
+ ...binding,
1396
+ strategy: position.strategy,
1397
+ expectedOrderId: position.id,
1398
+ symbol: position.symbol,
1399
+ exitPrice: position.markPrice,
1400
+ exitTimestamp: asOf,
1401
+ exitType: "exit"
1402
+ });
1403
+ },
1404
+ observe: (asOf, reservations) => replay ? connector.getPortfolioObservation(asOf, reservations) : observeRuntimePortfolio(connector, binding, asOf, reservations),
1405
+ async close(position, intent, asOf) {
1406
+ if (!replay) {
1407
+ const trade = await getActiveRuntimeTrade({
1408
+ ...binding,
1409
+ symbol: position.symbol
1410
+ });
1411
+ if (!trade || trade.orderId !== position.id)
1412
+ throw new Error("Portfolio close ownership changed");
1413
+ }
1414
+ await connector.closePosition({
1415
+ symbol: position.symbol,
1416
+ direction: position.direction,
1417
+ price: position.markPrice,
1418
+ timestamp: asOf,
1419
+ signal: {
1420
+ signalId: `${position.id}:${intent.reason}`,
1421
+ strategy: position.strategy,
1422
+ symbol: position.symbol,
1423
+ direction: position.direction,
1424
+ interval: "1",
1425
+ timestamp: asOf,
1426
+ prices: {
1427
+ riskRatio: 0,
1428
+ currentPrice: position.markPrice,
1429
+ takeProfitPrice: position.markPrice,
1430
+ stopLossPrice: position.markPrice
1431
+ },
1432
+ figures: {},
1433
+ indicators: {},
1434
+ additionalIndicators: { portfolioExitReason: intent.reason }
1435
+ }
1436
+ });
1437
+ }
1438
+ }
1439
+ });
1440
+ registry.set(cacheKey, { fingerprint: fingerprint3, coordinator });
1441
+ return coordinator;
1442
+ }
1443
+
1444
+ // src/strategyHelpers/tickerProfiles.ts
1445
+ var import_tickerProfiles = require("@tradejs/infra/tickerProfiles");
1446
+ var import_tickerProfiles2 = require("@tradejs/core/tickerProfiles");
1447
+
1448
+ // src/tradejsConfig.ts
1449
+ var import_fs = __toESM(require("fs"));
1450
+ var import_path = __toESM(require("path"));
1451
+ var import_url = require("url");
1452
+ var import_config = require("@tradejs/core/config");
1453
+ var import_logger2 = require("@tradejs/infra/logger");
1454
+ var CONFIG_FILE_NAMES = [
1455
+ "tradejs.config.ts",
1456
+ "tradejs.config.mts",
1457
+ "tradejs.config.js",
1458
+ "tradejs.config.mjs",
1459
+ "tradejs.config.cjs"
1460
+ ];
1461
+ var TS_MODULE_RE = /\.(cts|mts|ts)$/i;
1462
+ var cachedByCwd = /* @__PURE__ */ new Map();
1463
+ var announcedConfigFile = /* @__PURE__ */ new Set();
1464
+ var tsNodeRegistered = false;
1465
+ var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
1466
+ var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
1467
+ var getTradejsProjectCwd = (cwd) => {
1468
+ const explicit = String(cwd ?? "").trim();
1469
+ if (explicit) {
1470
+ return import_path.default.resolve(explicit);
1471
+ }
1472
+ const fromEnv = String(process.env.PROJECT_CWD || "").trim();
1473
+ if (fromEnv) {
1474
+ return import_path.default.resolve(fromEnv);
1475
+ }
1476
+ return process.cwd();
1500
1477
  };
1501
- var buildTargetVsBtcContextFromSignal = (signal) => {
1502
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1503
- const relative = toRecord(baseContext?.relative);
1504
- const targetVsBtc = toRecord(relative?.targetVsBtc);
1505
- if (!targetVsBtc) {
1506
- return {
1507
- source: null,
1508
- available: false,
1509
- ratioReturn1h: null,
1510
- ratioReturn4h: null,
1511
- ratioReturn24h: null,
1512
- alphaVsBtc1h: null,
1513
- alphaVsBtc4h: null,
1514
- alphaVsBtc24h: null,
1515
- betaToBtc20: null,
1516
- correlationToBtc20: null,
1517
- ratioTrend: null
1518
- };
1478
+ var normalizeConfig = (rawConfig) => {
1479
+ if (!rawConfig || typeof rawConfig !== "object") {
1480
+ return {};
1519
1481
  }
1482
+ const config = rawConfig;
1483
+ const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
1484
+ const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
1485
+ const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
1486
+ const hooks = (0, import_config.normalizeTradejsConfigHooks)(
1487
+ config.hooks
1488
+ );
1489
+ const runtime = config.runtime && typeof config.runtime === "object" && !Array.isArray(config.runtime) ? config.runtime : void 0;
1520
1490
  return {
1521
- source: String(targetVsBtc.source ?? ""),
1522
- available: true,
1523
- ratioReturn1h: toFiniteNumber(targetVsBtc.ratioReturn1h),
1524
- ratioReturn4h: toFiniteNumber(targetVsBtc.ratioReturn4h),
1525
- ratioReturn24h: toFiniteNumber(targetVsBtc.ratioReturn24h),
1526
- alphaVsBtc1h: toFiniteNumber(targetVsBtc.alphaVsBtc1h),
1527
- alphaVsBtc4h: toFiniteNumber(targetVsBtc.alphaVsBtc4h),
1528
- alphaVsBtc24h: toFiniteNumber(targetVsBtc.alphaVsBtc24h),
1529
- betaToBtc20: toFiniteNumber(targetVsBtc.betaToBtc20),
1530
- correlationToBtc20: toFiniteNumber(targetVsBtc.correlationToBtc20),
1531
- ratioTrend: typeof targetVsBtc.ratioTrend === "string" ? targetVsBtc.ratioTrend : null
1491
+ strategies: strategies2,
1492
+ indicators,
1493
+ connectors,
1494
+ ...hooks ? { hooks } : {},
1495
+ ...runtime ? { runtime } : {}
1532
1496
  };
1533
1497
  };
1534
- var buildBtcAltRegimeContextFromSignal = (signal) => {
1535
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1536
- const relative = toRecord(baseContext?.relative);
1537
- const btcAltRegime = toRecord(relative?.btcAltRegime);
1538
- if (!btcAltRegime) {
1539
- return {
1540
- source: null,
1541
- available: false,
1542
- universe: null,
1543
- interval: null,
1544
- stale: null,
1545
- regime: null,
1546
- btcReturn24h: null,
1547
- altBasketReturn24h: null,
1548
- btcVsAltReturn24h: null,
1549
- btcTurnoverShare24h: null,
1550
- btcTurnoverShareChange24h: null,
1551
- altVolToBtcVol24h: null,
1552
- altDispersion24h: null
1553
- };
1498
+ var getNodeCreateRequire = () => {
1499
+ const builtinModule = process.getBuiltinModule?.("module");
1500
+ if (typeof builtinModule?.createRequire === "function") {
1501
+ return builtinModule.createRequire;
1554
1502
  }
1555
- return {
1556
- source: String(btcAltRegime.source ?? ""),
1557
- available: true,
1558
- universe: String(btcAltRegime.universe ?? ""),
1559
- interval: String(btcAltRegime.interval ?? ""),
1560
- stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
1561
- regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
1562
- btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
1563
- altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
1564
- btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
1565
- btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
1566
- btcTurnoverShareChange24h: toFiniteNumber(
1567
- btcAltRegime.btcTurnoverShareChange24h
1568
- ),
1569
- altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
1570
- altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
1571
- };
1503
+ throw new TypeError("module.createRequire is not available");
1572
1504
  };
1573
- var buildCmcGlobalContextFromSignal = (signal) => {
1574
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1575
- const relative = toRecord(baseContext?.relative);
1576
- const cmcGlobal = toRecord(relative?.cmcGlobal);
1577
- if (!cmcGlobal) {
1578
- return {
1579
- source: null,
1580
- available: false,
1581
- interval: null,
1582
- asOfTs: null,
1583
- stale: null,
1584
- totalMarketCapUsd: null,
1585
- totalVolumeUsd: null,
1586
- totalVolumeReportedUsd: null,
1587
- altMarketCapUsd: null,
1588
- altVolumeUsd: null,
1589
- altVolumeReportedUsd: null,
1590
- btcDominancePct: null,
1591
- ethDominancePct: null,
1592
- btcDominanceChange24hPct: null,
1593
- ethDominanceChange24hPct: null,
1594
- altMarketCapChange24hPct: null,
1595
- altVolumeChange24hPct: null,
1596
- activeCryptocurrencies: null,
1597
- activeExchanges: null,
1598
- activeMarketPairs: null,
1599
- altLiquidityRegime: null
1505
+ var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
1506
+ var ensureTsNodeRegistered = async () => {
1507
+ if (tsNodeRegistered) {
1508
+ return;
1509
+ }
1510
+ const tsNodeModule = await import("ts-node");
1511
+ const tsNode = tsNodeModule.default ?? tsNodeModule;
1512
+ tsNode.register?.({
1513
+ transpileOnly: true,
1514
+ compilerOptions: {
1515
+ module: "Node16",
1516
+ moduleResolution: "node16"
1517
+ }
1518
+ });
1519
+ tsNodeRegistered = true;
1520
+ };
1521
+ var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
1522
+ const projectRoot = getTradejsProjectCwd(cwd);
1523
+ if (tsconfigPathsRegisteredByCwd.has(projectRoot)) {
1524
+ return;
1525
+ }
1526
+ const tsconfigPathsModule = await import("tsconfig-paths");
1527
+ const loadConfig = tsconfigPathsModule.loadConfig;
1528
+ const register = tsconfigPathsModule.register;
1529
+ if (typeof loadConfig !== "function" || typeof register !== "function") {
1530
+ return;
1531
+ }
1532
+ const loadedConfig = loadConfig(projectRoot);
1533
+ if (loadedConfig.resultType !== "success") {
1534
+ return;
1535
+ }
1536
+ register({
1537
+ baseUrl: loadedConfig.absoluteBaseUrl,
1538
+ paths: loadedConfig.paths,
1539
+ addMatchAll: false
1540
+ });
1541
+ tsconfigPathsRegisteredByCwd.add(projectRoot);
1542
+ };
1543
+ var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1544
+ const projectRoot = getTradejsProjectCwd(cwd);
1545
+ const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
1546
+ if (cachedMatcher) {
1547
+ const resolved2 = cachedMatcher(moduleName);
1548
+ return resolved2 || null;
1549
+ }
1550
+ const tsconfigPathsModule = await import("tsconfig-paths");
1551
+ const loadConfig = tsconfigPathsModule.loadConfig;
1552
+ const createMatchPath = tsconfigPathsModule.createMatchPath;
1553
+ if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
1554
+ return null;
1555
+ }
1556
+ const loadedConfig = loadConfig(projectRoot);
1557
+ if (loadedConfig.resultType !== "success") {
1558
+ return null;
1559
+ }
1560
+ const matchPath = createMatchPath(
1561
+ loadedConfig.absoluteBaseUrl,
1562
+ loadedConfig.paths
1563
+ );
1564
+ const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
1565
+ ".ts",
1566
+ ".tsx",
1567
+ ".mts",
1568
+ ".cts",
1569
+ ".js",
1570
+ ".jsx",
1571
+ ".mjs",
1572
+ ".cjs",
1573
+ ".json"
1574
+ ]) || "";
1575
+ tsconfigPathMatchersByCwd.set(projectRoot, matcher);
1576
+ const resolved = matcher(moduleName);
1577
+ return resolved || null;
1578
+ };
1579
+ var toImportSpecifier = (moduleName) => {
1580
+ if (moduleName.startsWith("file://")) {
1581
+ return moduleName;
1582
+ }
1583
+ if (import_path.default.isAbsolute(moduleName)) {
1584
+ return (0, import_url.pathToFileURL)(moduleName).href;
1585
+ }
1586
+ return moduleName;
1587
+ };
1588
+ var isTsModulePath = (moduleName) => TS_MODULE_RE.test(moduleName.split("?")[0]);
1589
+ var isRelativeModulePath = (moduleName) => moduleName.startsWith("./") || moduleName.startsWith("../");
1590
+ var isBareModuleSpecifier = (moduleName) => {
1591
+ const normalized = String(moduleName ?? "").trim();
1592
+ if (!normalized) {
1593
+ return false;
1594
+ }
1595
+ if (normalized.startsWith("file://") || import_path.default.isAbsolute(normalized) || isRelativeModulePath(normalized)) {
1596
+ return false;
1597
+ }
1598
+ return true;
1599
+ };
1600
+ var importConfigFile = async (configFilePath) => {
1601
+ const ext = import_path.default.extname(configFilePath).toLowerCase();
1602
+ const configFileUrl = `${toImportSpecifier(configFilePath)}?t=${Date.now()}`;
1603
+ if (ext === ".ts" || ext === ".mts") {
1604
+ const requireFn = getRequireFn(import_path.default.dirname(configFilePath));
1605
+ await ensureTsNodeRegistered();
1606
+ await ensureTsconfigPathsRegistered(import_path.default.dirname(configFilePath));
1607
+ return requireFn(configFilePath);
1608
+ }
1609
+ return import(
1610
+ /* webpackIgnore: true */
1611
+ configFileUrl
1612
+ );
1613
+ };
1614
+ var importTradejsModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1615
+ const normalized = String(moduleName ?? "").trim();
1616
+ if (!normalized) {
1617
+ return {};
1618
+ }
1619
+ let modulePath = normalized;
1620
+ if (normalized.startsWith("file://")) {
1621
+ try {
1622
+ modulePath = (0, import_url.fileURLToPath)(normalized);
1623
+ } catch {
1624
+ modulePath = normalized;
1625
+ }
1626
+ }
1627
+ const requireFn = getRequireFn(
1628
+ import_path.default.isAbsolute(modulePath) ? import_path.default.dirname(modulePath) : cwd
1629
+ );
1630
+ if (isTsModulePath(modulePath)) {
1631
+ await ensureTsNodeRegistered();
1632
+ await ensureTsconfigPathsRegistered(cwd);
1633
+ return requireFn(modulePath);
1634
+ }
1635
+ if (isBareModuleSpecifier(normalized)) {
1636
+ await ensureTsconfigPathsRegistered(cwd);
1637
+ try {
1638
+ return requireFn(normalized);
1639
+ } catch (error) {
1640
+ const resolvedByTsconfig = await resolveTsconfigPathModule(
1641
+ normalized,
1642
+ cwd
1643
+ );
1644
+ if (resolvedByTsconfig && resolvedByTsconfig !== normalized) {
1645
+ return requireFn(resolvedByTsconfig);
1646
+ }
1647
+ throw error;
1648
+ }
1649
+ }
1650
+ try {
1651
+ return await import(
1652
+ /* webpackIgnore: true */
1653
+ toImportSpecifier(normalized)
1654
+ );
1655
+ } catch (error) {
1656
+ if (isTsModulePath(modulePath)) {
1657
+ await ensureTsNodeRegistered();
1658
+ await ensureTsconfigPathsRegistered(cwd);
1659
+ return requireFn(modulePath);
1660
+ }
1661
+ throw error;
1662
+ }
1663
+ };
1664
+ var resolveExportedConfig = (moduleExports) => {
1665
+ const candidate = moduleExports && typeof moduleExports === "object" && "default" in moduleExports ? moduleExports.default : moduleExports;
1666
+ return normalizeConfig(candidate);
1667
+ };
1668
+ var findConfigFilePath = (cwd) => {
1669
+ let currentDir = import_path.default.resolve(cwd);
1670
+ while (true) {
1671
+ for (const fileName of CONFIG_FILE_NAMES) {
1672
+ const fullPath = import_path.default.join(currentDir, fileName);
1673
+ if (import_fs.default.existsSync(fullPath) && import_fs.default.statSync(fullPath).isFile()) {
1674
+ return fullPath;
1675
+ }
1676
+ }
1677
+ const parentDir = import_path.default.dirname(currentDir);
1678
+ if (parentDir === currentDir) {
1679
+ return null;
1680
+ }
1681
+ currentDir = parentDir;
1682
+ }
1683
+ };
1684
+ var resolvePluginModuleSpecifier = (moduleName, cwd = getTradejsProjectCwd()) => {
1685
+ const normalized = String(moduleName ?? "").trim();
1686
+ if (!normalized) {
1687
+ return "";
1688
+ }
1689
+ if (normalized.startsWith("file://")) {
1690
+ try {
1691
+ return (0, import_url.fileURLToPath)(normalized);
1692
+ } catch {
1693
+ return normalized;
1694
+ }
1695
+ }
1696
+ if (import_path.default.isAbsolute(normalized)) {
1697
+ return normalized;
1698
+ }
1699
+ if (isRelativeModulePath(normalized)) {
1700
+ return import_path.default.resolve(cwd, normalized);
1701
+ }
1702
+ return normalized;
1703
+ };
1704
+ var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
1705
+ const cached = cachedByCwd.get(cwd);
1706
+ if (cached) {
1707
+ return cached;
1708
+ }
1709
+ const configFilePath = findConfigFilePath(cwd);
1710
+ if (!configFilePath) {
1711
+ cachedByCwd.set(cwd, {});
1712
+ return {};
1713
+ }
1714
+ try {
1715
+ const moduleExports = await importConfigFile(configFilePath);
1716
+ const config = resolveExportedConfig(moduleExports);
1717
+ cachedByCwd.set(cwd, config);
1718
+ if (!announcedConfigFile.has(configFilePath)) {
1719
+ announcedConfigFile.add(configFilePath);
1720
+ import_logger2.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
1721
+ }
1722
+ return config;
1723
+ } catch (error) {
1724
+ import_logger2.logger.log(
1725
+ "warn",
1726
+ "Failed to load TradeJS config from %s: %s",
1727
+ configFilePath,
1728
+ String(error)
1729
+ );
1730
+ cachedByCwd.set(cwd, {});
1731
+ return {};
1732
+ }
1733
+ };
1734
+
1735
+ // src/strategy/manifests.ts
1736
+ var import_indicators = require("@tradejs/core/indicators");
1737
+ var SHARED_STRATEGY_REGISTRY_KEY = "__tradejsNodeSharedStrategyRegistryV1__";
1738
+ var sharedRegistryScope = globalThis;
1739
+ var sharedStrategyRegistry = sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] ?? (sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] = {
1740
+ registryStateByProjectRoot: /* @__PURE__ */ new Map()
1741
+ });
1742
+ var createStrategyRegistryState = () => ({
1743
+ strategyCreators: /* @__PURE__ */ new Map(),
1744
+ strategyManifestsMap: /* @__PURE__ */ new Map(),
1745
+ strategyEntriesMap: /* @__PURE__ */ new Map(),
1746
+ strategySourcesMap: /* @__PURE__ */ new Map(),
1747
+ strategyModuleResolutions: /* @__PURE__ */ new Map(),
1748
+ pluginsLoadPromise: null
1749
+ });
1750
+ var registryStateByProjectRoot = sharedStrategyRegistry.registryStateByProjectRoot;
1751
+ var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
1752
+ const projectRoot = getTradejsProjectCwd(cwd);
1753
+ let state = registryStateByProjectRoot.get(projectRoot);
1754
+ if (!state) {
1755
+ state = createStrategyRegistryState();
1756
+ registryStateByProjectRoot.set(projectRoot, state);
1757
+ }
1758
+ return {
1759
+ projectRoot,
1760
+ state
1761
+ };
1762
+ };
1763
+ var toUniqueModules = (modules = []) => [
1764
+ ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
1765
+ ];
1766
+ var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
1767
+ const config = await loadTradejsConfig(cwd);
1768
+ return {
1769
+ strategyModules: toUniqueModules(config.strategies),
1770
+ indicatorModules: toUniqueModules(config.indicators)
1771
+ };
1772
+ };
1773
+ var extractModuleEntries = (moduleExport, key) => {
1774
+ if (!moduleExport || typeof moduleExport !== "object") {
1775
+ return null;
1776
+ }
1777
+ const candidate = moduleExport;
1778
+ if (Array.isArray(candidate[key])) {
1779
+ return candidate[key];
1780
+ }
1781
+ const defaultExport = candidate.default;
1782
+ if (defaultExport && Array.isArray(defaultExport[key])) {
1783
+ return defaultExport[key];
1784
+ }
1785
+ return null;
1786
+ };
1787
+ var extractStrategyPluginDefinition = (moduleExport) => {
1788
+ const strategyEntries = extractModuleEntries(
1789
+ moduleExport,
1790
+ "strategyEntries"
1791
+ );
1792
+ return strategyEntries ? { strategyEntries } : null;
1793
+ };
1794
+ var extractIndicatorPluginDefinition = (moduleExport) => {
1795
+ const indicatorEntries = extractModuleEntries(
1796
+ moduleExport,
1797
+ "indicatorEntries"
1798
+ );
1799
+ return indicatorEntries ? { indicatorEntries } : null;
1800
+ };
1801
+ var validateStrategyEntries = (moduleName, entries, state) => {
1802
+ const issues = [];
1803
+ const names = /* @__PURE__ */ new Set();
1804
+ entries.forEach((entry, index) => {
1805
+ const entryPath = `${moduleName}.strategyEntries[${index}]`;
1806
+ const strategyName = entry?.manifest?.name;
1807
+ if (typeof strategyName !== "string" || !strategyName.trim()) {
1808
+ issues.push(`${entryPath}: manifest.name is required`);
1809
+ } else if (names.has(strategyName) || state?.strategyEntriesMap.has(strategyName)) {
1810
+ issues.push(`${entryPath}: duplicate strategy ${strategyName}`);
1811
+ } else {
1812
+ names.add(strategyName);
1813
+ }
1814
+ if (!entry || typeof entry !== "object" || !entry.defaults || typeof entry.defaults !== "object" || Array.isArray(entry.defaults)) {
1815
+ issues.push(`${entryPath}: defaults must be an object`);
1816
+ }
1817
+ if (typeof entry?.parseConfig !== "function") {
1818
+ issues.push(`${entryPath}: parseConfig is required`);
1819
+ }
1820
+ if (typeof entry?.createCore !== "function") {
1821
+ issues.push(`${entryPath}: createCore is required`);
1822
+ }
1823
+ });
1824
+ return issues;
1825
+ };
1826
+ var registerEntries = (entries, source, state) => {
1827
+ for (const entry of entries) {
1828
+ const strategyName = entry.manifest.name;
1829
+ state.strategyManifestsMap.set(strategyName, entry.manifest);
1830
+ state.strategyEntriesMap.set(strategyName, entry);
1831
+ state.strategySourcesMap.set(strategyName, source);
1832
+ materializeStrategyCreator(strategyName, state);
1833
+ }
1834
+ };
1835
+ var materializeStrategyCreator = (strategyName, state) => {
1836
+ if (state.strategyCreators.has(strategyName) || !sharedStrategyRegistry.strategyRuntimeFactory) {
1837
+ return;
1838
+ }
1839
+ const entry = state.strategyEntriesMap.get(strategyName);
1840
+ if (!entry) return;
1841
+ state.strategyCreators.set(
1842
+ strategyName,
1843
+ sharedStrategyRegistry.strategyRuntimeFactory({
1844
+ strategyName,
1845
+ defaults: entry.defaults,
1846
+ createCore: entry.createCore,
1847
+ manifest: entry.manifest,
1848
+ detectorKey: entry.detectorKey,
1849
+ detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
1850
+ resolveRegisteredManifest: (name) => state.strategyManifestsMap.get(name)
1851
+ })
1852
+ );
1853
+ };
1854
+ var setStrategyRuntimeFactory = (factory) => {
1855
+ sharedStrategyRegistry.strategyRuntimeFactory = factory;
1856
+ for (const state of registryStateByProjectRoot.values()) {
1857
+ for (const strategyName of state.strategyEntriesMap.keys()) {
1858
+ materializeStrategyCreator(strategyName, state);
1859
+ }
1860
+ }
1861
+ };
1862
+ var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1863
+ if (typeof importTradejsModule === "function") {
1864
+ return importTradejsModule(moduleName, cwd);
1865
+ }
1866
+ return import(
1867
+ /* webpackIgnore: true */
1868
+ moduleName
1869
+ );
1870
+ };
1871
+ var resolveStrategyModuleEntry = async ({
1872
+ strategyName,
1873
+ moduleName,
1874
+ cwd
1875
+ }) => {
1876
+ const { projectRoot, state } = getStrategyRegistryState(cwd);
1877
+ const source = moduleName.trim();
1878
+ const resolvedModuleName = resolvePluginModuleSpecifier(source, projectRoot);
1879
+ const cacheKey = `${resolvedModuleName}\0${strategyName}`;
1880
+ const cached = state.strategyModuleResolutions.get(cacheKey);
1881
+ if (cached) return cached;
1882
+ const resolution = (async () => {
1883
+ const moduleExport = await importStrategyPluginModule(
1884
+ resolvedModuleName,
1885
+ projectRoot
1886
+ );
1887
+ const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
1888
+ if (!pluginDefinition) {
1889
+ throw new Error(`${source}: export { strategyEntries } is missing`);
1890
+ }
1891
+ const issues = validateStrategyEntries(
1892
+ source,
1893
+ pluginDefinition.strategyEntries
1894
+ );
1895
+ if (issues.length > 0) {
1896
+ throw new Error(
1897
+ ["Invalid TradeJS strategy module:", ...issues].join("\n")
1898
+ );
1899
+ }
1900
+ const entry = pluginDefinition.strategyEntries.find(
1901
+ (candidate) => candidate.manifest.name === strategyName
1902
+ );
1903
+ if (!entry) {
1904
+ throw new Error(`${source}: strategy ${strategyName} is missing`);
1905
+ }
1906
+ const factory = sharedStrategyRegistry.strategyRuntimeFactory;
1907
+ if (!factory) {
1908
+ throw new Error("Strategy runtime factory is not configured");
1909
+ }
1910
+ const manifestsByName = new Map(
1911
+ pluginDefinition.strategyEntries.map(
1912
+ (candidate) => [candidate.manifest.name, candidate.manifest]
1913
+ )
1914
+ );
1915
+ return {
1916
+ entry,
1917
+ creator: factory({
1918
+ strategyName,
1919
+ defaults: entry.defaults,
1920
+ createCore: entry.createCore,
1921
+ manifest: entry.manifest,
1922
+ detectorKey: entry.detectorKey,
1923
+ detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
1924
+ resolveRegisteredManifest: (name) => manifestsByName.get(name) ?? state.strategyManifestsMap.get(name)
1925
+ }),
1926
+ source
1600
1927
  };
1928
+ })();
1929
+ state.strategyModuleResolutions.set(cacheKey, resolution);
1930
+ try {
1931
+ return await resolution;
1932
+ } catch (error) {
1933
+ state.strategyModuleResolutions.delete(cacheKey);
1934
+ throw error;
1935
+ }
1936
+ };
1937
+ var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
1938
+ const { projectRoot, state } = getStrategyRegistryState(cwd);
1939
+ if (!state.pluginsLoadPromise) {
1940
+ (0, import_indicators.resetIndicatorRegistryCache)(projectRoot);
1941
+ state.pluginsLoadPromise = (async () => {
1942
+ const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
1943
+ const strategySet = new Set(strategyModules);
1944
+ const indicatorSet = new Set(indicatorModules);
1945
+ const pluginModuleNames = [
1946
+ .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
1947
+ ];
1948
+ if (!pluginModuleNames.length) {
1949
+ return;
1950
+ }
1951
+ const issues = [];
1952
+ for (const moduleName of pluginModuleNames) {
1953
+ try {
1954
+ const resolvedModuleName = resolvePluginModuleSpecifier(
1955
+ moduleName,
1956
+ projectRoot
1957
+ );
1958
+ const moduleExport = await importStrategyPluginModule(
1959
+ resolvedModuleName,
1960
+ projectRoot
1961
+ );
1962
+ if (strategySet.has(moduleName)) {
1963
+ const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
1964
+ if (!pluginDefinition) {
1965
+ issues.push(
1966
+ `${moduleName}: export { strategyEntries } is missing`
1967
+ );
1968
+ } else {
1969
+ const entryIssues = validateStrategyEntries(
1970
+ moduleName,
1971
+ pluginDefinition.strategyEntries,
1972
+ state
1973
+ );
1974
+ issues.push(...entryIssues);
1975
+ if (entryIssues.length === 0) {
1976
+ registerEntries(
1977
+ pluginDefinition.strategyEntries,
1978
+ moduleName,
1979
+ state
1980
+ );
1981
+ }
1982
+ }
1983
+ }
1984
+ if (indicatorSet.has(moduleName)) {
1985
+ const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
1986
+ if (!indicatorPluginDefinition) {
1987
+ issues.push(
1988
+ `${moduleName}: export { indicatorEntries } is missing`
1989
+ );
1990
+ } else {
1991
+ (0, import_indicators.registerIndicatorEntries)(
1992
+ indicatorPluginDefinition.indicatorEntries,
1993
+ moduleName,
1994
+ projectRoot
1995
+ );
1996
+ }
1997
+ }
1998
+ if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
1999
+ issues.push(`${moduleName}: plugin is not declared in config`);
2000
+ }
2001
+ } catch (error) {
2002
+ issues.push(`${moduleName}: failed to import: ${String(error)}`);
2003
+ }
2004
+ }
2005
+ if (issues.length > 0) {
2006
+ throw new Error(
2007
+ ["Invalid TradeJS plugin catalog:", ...issues].join("\n")
2008
+ );
2009
+ }
2010
+ })();
1601
2011
  }
1602
- return {
1603
- source: String(cmcGlobal.source ?? ""),
1604
- available: true,
1605
- interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
1606
- asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
1607
- stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
1608
- totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
1609
- totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
1610
- totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
1611
- altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
1612
- altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
1613
- altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
1614
- btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
1615
- ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
1616
- btcDominanceChange24hPct: toFiniteNumber(
1617
- cmcGlobal.btcDominanceChange24hPct
1618
- ),
1619
- ethDominanceChange24hPct: toFiniteNumber(
1620
- cmcGlobal.ethDominanceChange24hPct
1621
- ),
1622
- altMarketCapChange24hPct: toFiniteNumber(
1623
- cmcGlobal.altMarketCapChange24hPct
1624
- ),
1625
- altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
1626
- activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
1627
- activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
1628
- activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
1629
- altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
1630
- };
2012
+ await state.pluginsLoadPromise;
1631
2013
  };
1632
- var buildCmcReferenceAssetsContextFromSignal = (signal) => {
1633
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1634
- const relative = toRecord(baseContext?.relative);
1635
- const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
1636
- if (!cmcReferenceAssets) {
1637
- return {
1638
- source: null,
1639
- available: false,
1640
- interval: null,
1641
- asOfTs: null,
1642
- stale: null,
1643
- btcMarketCapUsd: null,
1644
- ethMarketCapUsd: null,
1645
- btcVolumeUsd: null,
1646
- ethVolumeUsd: null,
1647
- btcVolumeToMarketCap: null,
1648
- ethVolumeToMarketCap: null,
1649
- ethBtcMarketCapRatio: null,
1650
- ethBtcMarketCapRatioChange24hPct: null,
1651
- ethVsBtcVolumeRatio: null,
1652
- referenceLiquidityRegime: null
1653
- };
2014
+ var ensureIndicatorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => ensureStrategyPluginsLoaded(cwd);
2015
+ var getStrategyCreator = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2016
+ if (moduleName?.trim()) {
2017
+ await ensureStrategyPluginsLoaded(cwd);
2018
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).creator;
1654
2019
  }
1655
- return {
1656
- source: String(cmcReferenceAssets.source ?? ""),
1657
- available: true,
1658
- interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
1659
- asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
1660
- stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
1661
- btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
1662
- ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
1663
- btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
1664
- ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
1665
- btcVolumeToMarketCap: toFiniteNumber(
1666
- cmcReferenceAssets.btcVolumeToMarketCap
1667
- ),
1668
- ethVolumeToMarketCap: toFiniteNumber(
1669
- cmcReferenceAssets.ethVolumeToMarketCap
1670
- ),
1671
- ethBtcMarketCapRatio: toFiniteNumber(
1672
- cmcReferenceAssets.ethBtcMarketCapRatio
1673
- ),
1674
- ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
1675
- cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
1676
- ),
1677
- ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
1678
- referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
1679
- };
2020
+ await ensureStrategyPluginsLoaded(cwd);
2021
+ const { state } = getStrategyRegistryState(cwd);
2022
+ return state.strategyCreators.get(name);
1680
2023
  };
1681
- var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
1682
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1683
- const relative = toRecord(baseContext?.relative);
1684
- const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
1685
- if (!cmcExchangeLiquidity) {
1686
- return {
1687
- source: null,
1688
- available: false,
1689
- interval: null,
1690
- asOfTs: null,
1691
- stale: null,
1692
- exchangesCount: null,
1693
- totalVolumeUsd: null,
1694
- totalVolumeChange24hPct: null,
1695
- binanceVolumeUsd: null,
1696
- binanceVolumeShare: null,
1697
- topExchangeVolumeShare: null,
1698
- liquidityRegime: null
1699
- };
2024
+ var getStrategyEntry = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2025
+ if (moduleName?.trim()) {
2026
+ await ensureStrategyPluginsLoaded(cwd);
2027
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).entry;
1700
2028
  }
1701
- return {
1702
- source: String(cmcExchangeLiquidity.source ?? ""),
1703
- available: true,
1704
- interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
1705
- asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
1706
- stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
1707
- exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
1708
- totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
1709
- totalVolumeChange24hPct: toFiniteNumber(
1710
- cmcExchangeLiquidity.totalVolumeChange24hPct
1711
- ),
1712
- binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
1713
- binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
1714
- topExchangeVolumeShare: toFiniteNumber(
1715
- cmcExchangeLiquidity.topExchangeVolumeShare
1716
- ),
1717
- liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
1718
- };
2029
+ await ensureStrategyPluginsLoaded(cwd);
2030
+ const { state } = getStrategyRegistryState(cwd);
2031
+ return state.strategyEntriesMap.get(name);
1719
2032
  };
1720
- var buildCmcFearGreedContextFromSignal = (signal) => {
1721
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1722
- const relative = toRecord(baseContext?.relative);
1723
- const cmcFearGreed = toRecord(relative?.cmcFearGreed);
1724
- if (!cmcFearGreed) {
1725
- return {
1726
- source: null,
1727
- available: false,
1728
- interval: null,
1729
- asOfTs: null,
1730
- stale: null,
1731
- value: null,
1732
- valueChange24h: null,
1733
- valueChange7d: null,
1734
- classification: null,
1735
- sentimentRegime: null
1736
- };
2033
+ var getStrategyPluginSource = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2034
+ if (moduleName?.trim()) {
2035
+ await ensureStrategyPluginsLoaded(cwd);
2036
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).source;
1737
2037
  }
1738
- return {
1739
- source: String(cmcFearGreed.source ?? ""),
1740
- available: true,
1741
- interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
1742
- asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
1743
- stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
1744
- value: toFiniteNumber(cmcFearGreed.value),
1745
- valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
1746
- valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
1747
- classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
1748
- sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
1749
- };
2038
+ await ensureStrategyPluginsLoaded(cwd);
2039
+ const { state } = getStrategyRegistryState(cwd);
2040
+ return state.strategySourcesMap.get(name);
1750
2041
  };
1751
- var buildCmcIndexesContextFromSignal = (signal) => {
1752
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1753
- const relative = toRecord(baseContext?.relative);
1754
- const cmcIndexes = toRecord(relative?.cmcIndexes);
1755
- if (!cmcIndexes) {
1756
- return {
1757
- source: null,
1758
- available: false,
1759
- interval: null,
1760
- asOfTs: null,
1761
- stale: null,
1762
- cmc100Value: null,
1763
- cmc100Change24hPct: null,
1764
- cmc100TopConstituentSymbol: null,
1765
- cmc100TopConstituentWeightPct: null,
1766
- cmc20Value: null,
1767
- cmc20Change24hPct: null,
1768
- cmc20TopConstituentSymbol: null,
1769
- cmc20TopConstituentWeightPct: null,
1770
- cmc20ToCmc100Ratio: null,
1771
- cmc20ToCmc100RatioChange24hPct: null,
1772
- indexRegime: null
1773
- };
2042
+ var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
2043
+ if (!name) {
2044
+ return void 0;
2045
+ }
2046
+ const { state } = getStrategyRegistryState(cwd);
2047
+ return state.strategyManifestsMap.get(name);
2048
+ };
2049
+ var strategies = new Proxy(
2050
+ {},
2051
+ {
2052
+ get: (_target, property) => {
2053
+ if (typeof property !== "string") {
2054
+ return void 0;
2055
+ }
2056
+ return getStrategyRegistryState().state.strategyCreators.get(property);
2057
+ },
2058
+ ownKeys: () => {
2059
+ return [...getStrategyRegistryState().state.strategyCreators.keys()];
2060
+ },
2061
+ getOwnPropertyDescriptor: () => ({
2062
+ enumerable: true,
2063
+ configurable: true
2064
+ })
2065
+ }
2066
+ );
2067
+
2068
+ // src/strategyHelpers/tickerProfiles.ts
2069
+ var snapshots = /* @__PURE__ */ new Map();
2070
+ var resolveTickerProfileSource = (strategy) => {
2071
+ const declared = getStrategyManifest(strategy)?.contextRequirements?.tickerProfiles;
2072
+ if (declared) return declared;
2073
+ const provider = process.env.TICKER_PROFILES_PROVIDER?.trim();
2074
+ const universeId = process.env.TICKER_PROFILES_UNIVERSE?.trim();
2075
+ if (!provider && !universeId) return void 0;
2076
+ if (!provider || !universeId)
2077
+ throw new Error(
2078
+ "Set both TICKER_PROFILES_PROVIDER and TICKER_PROFILES_UNIVERSE"
2079
+ );
2080
+ return { provider, universeId };
2081
+ };
2082
+ var loadTickerProfileContext = async ({
2083
+ projectRoot,
2084
+ provider,
2085
+ universeId,
2086
+ symbol,
2087
+ timestamp
2088
+ }) => {
2089
+ const asOf = (0, import_tickerProfiles2.tickerProfileMonth)(timestamp), key = JSON.stringify([projectRoot, provider, universeId, asOf]);
2090
+ let cached = snapshots.get(key);
2091
+ if (!cached || cached.expires < Date.now()) {
2092
+ const value = (0, import_tickerProfiles.readTickerProfiles)(projectRoot, provider, universeId, asOf).then((envelope) => {
2093
+ if (!envelope) return null;
2094
+ const s = envelope.snapshot;
2095
+ if (!(0, import_tickerProfiles2.isTickerProfileSnapshot)(s) || s.asOf !== asOf || s.provider !== provider || s.universeId !== universeId)
2096
+ throw new Error("Ticker profile identity/schema mismatch");
2097
+ const entry = snapshots.get(key);
2098
+ if (entry) entry.expires = Infinity;
2099
+ return new Map(
2100
+ s.profiles.map((p) => [
2101
+ p.symbol,
2102
+ Object.freeze({
2103
+ ...p,
2104
+ definition: s.definition,
2105
+ provider,
2106
+ universeId,
2107
+ snapshotId: envelope.snapshotId
2108
+ })
2109
+ ])
2110
+ );
2111
+ }).catch((error) => {
2112
+ snapshots.delete(key);
2113
+ throw error;
2114
+ });
2115
+ cached = { expires: Date.now() + 6e4, value };
2116
+ snapshots.set(key, cached);
2117
+ if (snapshots.size > 24) {
2118
+ const oldest = snapshots.keys().next().value;
2119
+ if (oldest) snapshots.delete(oldest);
2120
+ }
1774
2121
  }
1775
- return {
1776
- source: String(cmcIndexes.source ?? ""),
1777
- available: true,
1778
- interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
1779
- asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
1780
- stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
1781
- cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
1782
- cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
1783
- cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
1784
- cmc100TopConstituentWeightPct: toFiniteNumber(
1785
- cmcIndexes.cmc100TopConstituentWeightPct
1786
- ),
1787
- cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
1788
- cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
1789
- cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
1790
- cmc20TopConstituentWeightPct: toFiniteNumber(
1791
- cmcIndexes.cmc20TopConstituentWeightPct
1792
- ),
1793
- cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
1794
- cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
1795
- cmcIndexes.cmc20ToCmc100RatioChange24hPct
1796
- ),
1797
- indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
1798
- };
2122
+ return (await cached.value)?.get(symbol);
1799
2123
  };
1800
- var buildReferenceTradeFlowContextFromSignal = (signal) => {
1801
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1802
- const relative = toRecord(baseContext?.relative);
1803
- const refs = toRecord(relative?.referenceTradeFlow);
1804
- const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
1805
- const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
1806
- const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
1807
- if (!refs) {
1808
- return {
1809
- source: null,
1810
- available: false,
1811
- primaryReferenceSymbol: null,
1812
- referenceSymbols: [],
1813
- primaryTradeFlowBuyPressurePct: null,
1814
- primaryTradeFlowStale: null
2124
+ var withTickerProfile = (baseContext, profile) => Object.defineProperty(
2125
+ Object.create(
2126
+ Object.getPrototypeOf(baseContext),
2127
+ Object.getOwnPropertyDescriptors(baseContext)
2128
+ ),
2129
+ "tickerProfile",
2130
+ { enumerable: true, configurable: true, value: profile }
2131
+ );
2132
+ var enrichSignalWithTickerProfile = async (signal) => {
2133
+ const source = resolveTickerProfileSource(signal.strategy);
2134
+ if (!source || signal.universe === "tradfi" || !signal.symbol.endsWith("USDT"))
2135
+ return;
2136
+ const profile = await loadTickerProfileContext({
2137
+ ...source,
2138
+ projectRoot: getTradejsProjectCwd(),
2139
+ symbol: signal.symbol,
2140
+ timestamp: signal.timestamp
2141
+ });
2142
+ if (profile) {
2143
+ signal.additionalIndicators = {
2144
+ ...signal.additionalIndicators,
2145
+ tickerProfile: profile
1815
2146
  };
1816
2147
  }
1817
- return {
1818
- source: String(refs.source ?? ""),
1819
- available: true,
1820
- primaryReferenceSymbol,
1821
- referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
1822
- primaryTradeFlowBuyPressurePct: toFiniteNumber(
1823
- primaryTradeFlow?.buyPressurePct
1824
- ),
1825
- primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
1826
- };
1827
2148
  };
1828
- var buildAiMarketContext = (signal) => ({
1829
- execution: {
1830
- binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
1831
- },
1832
- participation: {
1833
- trueDelta: buildTrueDeltaContextFromSignal(signal),
1834
- tradeFlow: buildTradeFlowContextFromSignal(signal)
1835
- },
1836
- relative: {
1837
- marketBreadth: buildMarketBreadthContextFromSignal(signal),
1838
- marketBreadths: buildMarketBreadthsContextFromSignal(signal),
1839
- targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
1840
- btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
1841
- cmcGlobal: buildCmcGlobalContextFromSignal(signal),
1842
- cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
1843
- cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
1844
- cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
1845
- cmcIndexes: buildCmcIndexesContextFromSignal(signal),
1846
- referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
1847
- }
1848
- });
1849
2149
 
1850
- // src/strategy/manifests.ts
1851
- var import_indicators = require("@tradejs/core/indicators");
2150
+ // src/strategyRuntime.ts
2151
+ var import_jev6 = require("@tradejs/core/jev");
1852
2152
 
1853
- // src/tradejsConfig.ts
1854
- var import_fs = __toESM(require("fs"));
1855
- var import_path = __toESM(require("path"));
1856
- var import_url = require("url");
1857
- var import_config = require("@tradejs/core/config");
1858
- var import_logger2 = require("@tradejs/infra/logger");
1859
- var CONFIG_FILE_NAMES = [
1860
- "tradejs.config.ts",
1861
- "tradejs.config.mts",
1862
- "tradejs.config.js",
1863
- "tradejs.config.mjs",
1864
- "tradejs.config.cjs"
1865
- ];
1866
- var TS_MODULE_RE = /\.(cts|mts|ts)$/i;
1867
- var cachedByCwd = /* @__PURE__ */ new Map();
1868
- var announcedConfigFile = /* @__PURE__ */ new Set();
1869
- var tsNodeRegistered = false;
1870
- var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
1871
- var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
1872
- var getTradejsProjectCwd = (cwd) => {
1873
- const explicit = String(cwd ?? "").trim();
1874
- if (explicit) {
1875
- return import_path.default.resolve(explicit);
1876
- }
1877
- const fromEnv = String(process.env.PROJECT_CWD || "").trim();
1878
- if (fromEnv) {
1879
- return import_path.default.resolve(fromEnv);
1880
- }
1881
- return process.cwd();
2153
+ // src/jev.ts
2154
+ var import_promises = __toESM(require("fs/promises"));
2155
+ var import_node_path = __toESM(require("path"));
2156
+ var import_jev4 = require("@tradejs/core/jev");
2157
+ var import_jev5 = require("@tradejs/infra/jev");
2158
+ var import_userSettings = require("@tradejs/infra/userSettings");
2159
+ init_jevInput();
2160
+ init_jevInput();
2161
+ init_jevTraining();
2162
+ var resolveJevProvider = async (userName) => {
2163
+ const settings = await (0, import_userSettings.getUserSettings)(userName);
2164
+ const provider = {
2165
+ endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
2166
+ model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
2167
+ };
2168
+ (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
2169
+ if (!settings.JEV_API_KEY)
2170
+ throw new Error("Configure the Jev API key in Account settings");
2171
+ return provider;
1882
2172
  };
1883
- var normalizeConfig = (rawConfig) => {
1884
- if (!rawConfig || typeof rawConfig !== "object") {
1885
- return {};
2173
+ var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
2174
+ var pending = /* @__PURE__ */ new Map();
2175
+ var pendingResponses = /* @__PURE__ */ new Map();
2176
+ var evaluateJevInput = async ({
2177
+ input,
2178
+ config,
2179
+ userName,
2180
+ projectRoot
2181
+ }) => {
2182
+ config = (0, import_jev4.parseJevConfig)(config);
2183
+ const inputHash = (0, import_jev5.jevHash)(input);
2184
+ const questions = questionsForJevInput(input);
2185
+ const questionsHash = (0, import_jev5.jevHash)(questions);
2186
+ const base = {
2187
+ questionSet: import_jev4.JEV_QUESTION_SET,
2188
+ schema: "tradejs-signal-assessment/v5",
2189
+ source: config.source,
2190
+ mode: config.mode,
2191
+ inputHash,
2192
+ questionsHash,
2193
+ geometryStatus: input.geometryStatus,
2194
+ levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
2195
+ };
2196
+ if (!input.questions.length)
2197
+ return {
2198
+ assessment: {
2199
+ ...base,
2200
+ status: "unavailable",
2201
+ model: "",
2202
+ scores: emptyScores(),
2203
+ confidence: emptyScores()
2204
+ }
2205
+ };
2206
+ if (config.source === "local") {
2207
+ const contents = await import_promises.default.readFile(
2208
+ import_node_path.default.resolve(projectRoot, config.modelFile),
2209
+ "utf8"
2210
+ );
2211
+ if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
2212
+ throw new Error("Local Jev gate checksum mismatch");
2213
+ const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
2214
+ const model = validateJevGateModel2(JSON.parse(contents));
2215
+ if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
2216
+ throw new Error("Local Jev gate strategy or question version mismatch");
2217
+ const scores = Object.fromEntries(
2218
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2219
+ key,
2220
+ input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
2221
+ ])
2222
+ );
2223
+ return {
2224
+ assessment: {
2225
+ ...base,
2226
+ status: "available",
2227
+ model: config.modelSha256,
2228
+ scores,
2229
+ confidence: emptyScores()
2230
+ }
2231
+ };
1886
2232
  }
1887
- const config = rawConfig;
1888
- const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
1889
- const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
1890
- const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
1891
- const hooks = (0, import_config.normalizeTradejsConfigHooks)(
1892
- config.hooks
1893
- );
1894
- const runtime = config.runtime && typeof config.runtime === "object" && !Array.isArray(config.runtime) ? config.runtime : void 0;
1895
- return {
1896
- strategies: strategies2,
1897
- indicators,
1898
- connectors,
1899
- ...hooks ? { hooks } : {},
1900
- ...runtime ? { runtime } : {}
2233
+ const provider = config.provider ?? await resolveJevProvider(userName);
2234
+ const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
2235
+ const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
2236
+ const file = import_node_path.default.join(dir, "records", `${id}.json`);
2237
+ const getRecord = async () => {
2238
+ const existing = await (0, import_jev5.readJevArtifact)(file);
2239
+ if (existing) {
2240
+ 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))
2241
+ throw new Error("Jev recording identity mismatch");
2242
+ (0, import_jev5.validateJevResponse)(existing.response, input.questions);
2243
+ for (const key of import_jev4.JEV_DIMENSIONS) {
2244
+ const expected = input.questions.includes(key) ? existing.response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null;
2245
+ if (existing.scores[key] !== expected)
2246
+ throw new Error("Jev recording scores do not match the response");
2247
+ }
2248
+ return existing;
2249
+ }
2250
+ if (config.source === "recorded")
2251
+ throw new Error(
2252
+ `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.`
2253
+ );
2254
+ const settings = await (0, import_userSettings.getUserSettings)(userName);
2255
+ if (!settings.JEV_API_KEY)
2256
+ throw new Error("Configure the Jev API key in Account settings");
2257
+ const apiKey = settings.JEV_API_KEY;
2258
+ const configuredProvider = await resolveJevProvider(userName);
2259
+ if (configuredProvider.endpoint !== provider.endpoint)
2260
+ throw new Error(
2261
+ "Jev account endpoint changed; update the frozen provider configuration"
2262
+ );
2263
+ const started = Date.now();
2264
+ const state = stateForJevInput(input);
2265
+ const responseId = (0, import_jev5.jevHash)({
2266
+ state,
2267
+ questionsHash,
2268
+ provider,
2269
+ user: (0, import_jev5.jevHash)(userName)
2270
+ });
2271
+ const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
2272
+ let responsePromise = pendingResponses.get(responseFile);
2273
+ if (!responsePromise) {
2274
+ responsePromise = (async () => {
2275
+ const saved = await (0, import_jev5.readJevArtifact)(responseFile);
2276
+ if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
2277
+ const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
2278
+ await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
2279
+ return fresh;
2280
+ })();
2281
+ pendingResponses.set(responseFile, responsePromise);
2282
+ }
2283
+ let response;
2284
+ try {
2285
+ response = await responsePromise;
2286
+ } finally {
2287
+ pendingResponses.delete(responseFile);
2288
+ }
2289
+ const scores = Object.fromEntries(
2290
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2291
+ key,
2292
+ input.questions.includes(key) ? response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null
2293
+ ])
2294
+ );
2295
+ const value = {
2296
+ schema: "tradejs-jev-record/v4",
2297
+ id,
2298
+ inputHash,
2299
+ questionsHash,
2300
+ provider,
2301
+ input,
2302
+ response,
2303
+ scores,
2304
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2305
+ elapsedMs: Date.now() - started
2306
+ };
2307
+ await (0, import_jev5.writeJevArtifact)(file, value);
2308
+ return await (0, import_jev5.readJevArtifact)(file);
1901
2309
  };
1902
- };
1903
- var getNodeCreateRequire = () => {
1904
- const builtinModule = process.getBuiltinModule?.("module");
1905
- if (typeof builtinModule?.createRequire === "function") {
1906
- return builtinModule.createRequire;
2310
+ const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
2311
+ let promise = pending.get(pendingKey);
2312
+ if (!promise) {
2313
+ promise = getRecord();
2314
+ pending.set(pendingKey, promise);
1907
2315
  }
1908
- throw new TypeError("module.createRequire is not available");
1909
- };
1910
- var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
1911
- var ensureTsNodeRegistered = async () => {
1912
- if (tsNodeRegistered) {
1913
- return;
2316
+ let record;
2317
+ try {
2318
+ record = await promise;
2319
+ } finally {
2320
+ pending.delete(pendingKey);
1914
2321
  }
1915
- const tsNodeModule = await import("ts-node");
1916
- const tsNode = tsNodeModule.default ?? tsNodeModule;
1917
- tsNode.register?.({
1918
- transpileOnly: true,
1919
- compilerOptions: {
1920
- module: "Node16",
1921
- moduleResolution: "node16"
2322
+ return {
2323
+ record,
2324
+ assessment: {
2325
+ ...base,
2326
+ status: "available",
2327
+ recordId: id,
2328
+ model: record.response.model,
2329
+ scores: record.scores,
2330
+ confidence: Object.fromEntries(
2331
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2332
+ key,
2333
+ record.response.answers[key]?.confidence ?? null
2334
+ ])
2335
+ )
1922
2336
  }
1923
- });
1924
- tsNodeRegistered = true;
1925
- };
1926
- var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
1927
- const projectRoot = getTradejsProjectCwd(cwd);
1928
- if (tsconfigPathsRegisteredByCwd.has(projectRoot)) {
1929
- return;
1930
- }
1931
- const tsconfigPathsModule = await import("tsconfig-paths");
1932
- const loadConfig = tsconfigPathsModule.loadConfig;
1933
- const register = tsconfigPathsModule.register;
1934
- if (typeof loadConfig !== "function" || typeof register !== "function") {
1935
- return;
1936
- }
1937
- const loadedConfig = loadConfig(projectRoot);
1938
- if (loadedConfig.resultType !== "success") {
1939
- return;
1940
- }
1941
- register({
1942
- baseUrl: loadedConfig.absoluteBaseUrl,
1943
- paths: loadedConfig.paths,
1944
- addMatchAll: false
1945
- });
1946
- tsconfigPathsRegisteredByCwd.add(projectRoot);
2337
+ };
1947
2338
  };
1948
- var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1949
- const projectRoot = getTradejsProjectCwd(cwd);
1950
- const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
1951
- if (cachedMatcher) {
1952
- const resolved2 = cachedMatcher(moduleName);
1953
- return resolved2 || null;
1954
- }
1955
- const tsconfigPathsModule = await import("tsconfig-paths");
1956
- const loadConfig = tsconfigPathsModule.loadConfig;
1957
- const createMatchPath = tsconfigPathsModule.createMatchPath;
1958
- if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
1959
- return null;
1960
- }
1961
- const loadedConfig = loadConfig(projectRoot);
1962
- if (loadedConfig.resultType !== "success") {
1963
- return null;
2339
+ var assessSignalWithJev = async ({
2340
+ signal,
2341
+ config,
2342
+ userName,
2343
+ projectRoot,
2344
+ strict,
2345
+ scope = "runtime"
2346
+ }) => {
2347
+ let assessment;
2348
+ try {
2349
+ const input = buildJevInput(signal);
2350
+ ({ assessment } = await evaluateJevInput({
2351
+ input,
2352
+ config,
2353
+ userName,
2354
+ projectRoot
2355
+ }));
2356
+ } catch (error) {
2357
+ if (strict) throw error;
2358
+ assessment = {
2359
+ questionSet: import_jev4.JEV_QUESTION_SET,
2360
+ schema: "tradejs-signal-assessment/v5",
2361
+ source: config.source,
2362
+ mode: config.mode,
2363
+ status: "unavailable",
2364
+ inputHash: "",
2365
+ questionsHash: "",
2366
+ geometryStatus: "absent",
2367
+ levelsValid: null,
2368
+ model: config.provider?.model ?? config.modelSha256 ?? "",
2369
+ scores: emptyScores(),
2370
+ confidence: emptyScores()
2371
+ };
1964
2372
  }
1965
- const matchPath = createMatchPath(
1966
- loadedConfig.absoluteBaseUrl,
1967
- loadedConfig.paths
2373
+ signal.assessment = assessment;
2374
+ signal.additionalIndicators = {
2375
+ ...signal.additionalIndicators,
2376
+ jev: (0, import_jev4.jevAssessmentFeatures)(assessment)
2377
+ };
2378
+ const identity = {
2379
+ signalId: signal.signalId,
2380
+ strategy: signal.strategy,
2381
+ symbol: signal.symbol,
2382
+ timestamp: signal.timestamp,
2383
+ scope,
2384
+ user: (0, import_jev5.jevHash)(userName),
2385
+ config,
2386
+ assessment
2387
+ };
2388
+ await (0, import_jev5.writeJevArtifact)(
2389
+ import_node_path.default.resolve(
2390
+ projectRoot,
2391
+ config.recordsDir ?? "data/ai/jev",
2392
+ "decisions",
2393
+ `${(0, import_jev5.jevHash)(identity)}.json`
2394
+ ),
2395
+ identity
1968
2396
  );
1969
- const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
1970
- ".ts",
1971
- ".tsx",
1972
- ".mts",
1973
- ".cts",
1974
- ".js",
1975
- ".jsx",
1976
- ".mjs",
1977
- ".cjs",
1978
- ".json"
1979
- ]) || "";
1980
- tsconfigPathMatchersByCwd.set(projectRoot, matcher);
1981
- const resolved = matcher(moduleName);
1982
- return resolved || null;
2397
+ return assessment;
1983
2398
  };
1984
- var toImportSpecifier = (moduleName) => {
1985
- if (moduleName.startsWith("file://")) {
1986
- return moduleName;
1987
- }
1988
- if (import_path.default.isAbsolute(moduleName)) {
1989
- return (0, import_url.pathToFileURL)(moduleName).href;
2399
+
2400
+ // src/strategyRuntime.ts
2401
+ var import_constants5 = require("@tradejs/core/constants");
2402
+ var import_strategies6 = require("@tradejs/core/strategies");
2403
+ var import_logger11 = require("@tradejs/infra/logger");
2404
+
2405
+ // src/strategyHelpers/runtime.ts
2406
+ var import_logger9 = require("@tradejs/infra/logger");
2407
+ var import_constants3 = require("@tradejs/core/constants");
2408
+ var import_ml2 = require("@tradejs/infra/ml");
2409
+
2410
+ // src/strategy/entrySubmissionValidation.ts
2411
+ var import_logger3 = require("@tradejs/infra/logger");
2412
+
2413
+ // src/strategy/entryExecutionValidation.ts
2414
+ var EntrySubmissionValidationError = class extends Error {
2415
+ constructor() {
2416
+ super(...arguments);
2417
+ this.name = "EntrySubmissionValidationError";
1990
2418
  }
1991
- return moduleName;
1992
2419
  };
1993
- var isTsModulePath = (moduleName) => TS_MODULE_RE.test(moduleName.split("?")[0]);
1994
- var isRelativeModulePath = (moduleName) => moduleName.startsWith("./") || moduleName.startsWith("../");
1995
- var isBareModuleSpecifier = (moduleName) => {
1996
- const normalized = String(moduleName ?? "").trim();
1997
- if (!normalized) {
1998
- return false;
1999
- }
2000
- if (normalized.startsWith("file://") || import_path.default.isAbsolute(normalized) || isRelativeModulePath(normalized)) {
2001
- return false;
2002
- }
2003
- return true;
2420
+ var resolveEntryExecutionLimits = (config, direction) => {
2421
+ const side = config[direction];
2422
+ const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
2423
+ const maximum = config.MAX_LOSS_VALUE;
2424
+ if (minimum == null && maximum == null) return void 0;
2425
+ const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
2426
+ const maxLossValue = maximum == null ? void 0 : Number(maximum);
2427
+ const feeRate = Number(config.RISK_FEE_RATE ?? 0);
2428
+ const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
2429
+ const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
2430
+ const slippageBps = slippage + impact;
2431
+ 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)
2432
+ throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
2433
+ return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
2004
2434
  };
2005
- var importConfigFile = async (configFilePath) => {
2006
- const ext = import_path.default.extname(configFilePath).toLowerCase();
2007
- const configFileUrl = `${toImportSpecifier(configFilePath)}?t=${Date.now()}`;
2008
- if (ext === ".ts" || ext === ".mts") {
2009
- const requireFn = getRequireFn(import_path.default.dirname(configFilePath));
2010
- await ensureTsNodeRegistered();
2011
- await ensureTsconfigPathsRegistered(import_path.default.dirname(configFilePath));
2012
- return requireFn(configFilePath);
2013
- }
2014
- return import(
2015
- /* webpackIgnore: true */
2016
- configFileUrl
2435
+ var validateEntryExecution = ({
2436
+ direction,
2437
+ price,
2438
+ qty,
2439
+ stopLossPrice,
2440
+ takeProfitPrice,
2441
+ limits
2442
+ }) => {
2443
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
2444
+ throw new EntrySubmissionValidationError(
2445
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2446
+ );
2447
+ const side = direction === "LONG" ? 1 : -1;
2448
+ if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
2449
+ throw new EntrySubmissionValidationError(
2450
+ "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
2451
+ );
2452
+ const slippage = limits.slippageBps / 1e4;
2453
+ const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
2454
+ const entryCost = cost(price, side);
2455
+ const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
2456
+ const rewardPerUnit = Math.max(
2457
+ 0,
2458
+ Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
2017
2459
  );
2460
+ const netRiskRatio = rewardPerUnit / lossPerUnit;
2461
+ const plannedLossValue = qty * lossPerUnit;
2462
+ if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
2463
+ throw new EntrySubmissionValidationError(
2464
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2465
+ );
2466
+ const belowMinimumNetRiskRatio = limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio;
2467
+ const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
2468
+ return {
2469
+ netRiskRatio,
2470
+ plannedLossValue,
2471
+ riskExceeded,
2472
+ belowMinimumNetRiskRatio
2473
+ };
2018
2474
  };
2019
- var importTradejsModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
2020
- const normalized = String(moduleName ?? "").trim();
2021
- if (!normalized) {
2022
- return {};
2023
- }
2024
- let modulePath = normalized;
2025
- if (normalized.startsWith("file://")) {
2026
- try {
2027
- modulePath = (0, import_url.fileURLToPath)(normalized);
2028
- } catch {
2029
- modulePath = normalized;
2030
- }
2031
- }
2032
- const requireFn = getRequireFn(
2033
- import_path.default.isAbsolute(modulePath) ? import_path.default.dirname(modulePath) : cwd
2034
- );
2035
- if (isTsModulePath(modulePath)) {
2036
- await ensureTsNodeRegistered();
2037
- await ensureTsconfigPathsRegistered(cwd);
2038
- return requireFn(modulePath);
2039
- }
2040
- if (isBareModuleSpecifier(normalized)) {
2041
- await ensureTsconfigPathsRegistered(cwd);
2042
- try {
2043
- return requireFn(normalized);
2044
- } catch (error) {
2045
- const resolvedByTsconfig = await resolveTsconfigPathModule(
2046
- normalized,
2047
- cwd
2048
- );
2049
- if (resolvedByTsconfig && resolvedByTsconfig !== normalized) {
2050
- return requireFn(resolvedByTsconfig);
2051
- }
2052
- throw error;
2053
- }
2475
+
2476
+ // src/strategy/entrySubmissionValidation.ts
2477
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
2478
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
2479
+ var getOrderArrivalSnapshot = async ({
2480
+ connector,
2481
+ symbol,
2482
+ requireTimestamp = false
2483
+ }) => {
2484
+ if (typeof connector.getTopOfBookTicker !== "function") {
2485
+ return {
2486
+ arrivalSnapshotTime: Date.now(),
2487
+ arrivalSource: "unavailable",
2488
+ bid: null,
2489
+ ask: null,
2490
+ arrivalMid: null,
2491
+ spreadBps: null
2492
+ };
2054
2493
  }
2055
2494
  try {
2056
- return await import(
2057
- /* webpackIgnore: true */
2058
- toImportSpecifier(normalized)
2059
- );
2495
+ const ticker = await connector.getTopOfBookTicker(symbol);
2496
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
2497
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
2498
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
2499
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
2500
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
2501
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
2502
+ return {
2503
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
2504
+ arrivalSource: "top_of_book",
2505
+ bid,
2506
+ ask,
2507
+ arrivalMid,
2508
+ spreadBps
2509
+ };
2060
2510
  } catch (error) {
2061
- if (isTsModulePath(modulePath)) {
2062
- await ensureTsNodeRegistered();
2063
- await ensureTsconfigPathsRegistered(cwd);
2064
- return requireFn(modulePath);
2065
- }
2066
- throw error;
2067
- }
2068
- };
2069
- var resolveExportedConfig = (moduleExports) => {
2070
- const candidate = moduleExports && typeof moduleExports === "object" && "default" in moduleExports ? moduleExports.default : moduleExports;
2071
- return normalizeConfig(candidate);
2072
- };
2073
- var findConfigFilePath = (cwd) => {
2074
- let currentDir = import_path.default.resolve(cwd);
2075
- while (true) {
2076
- for (const fileName of CONFIG_FILE_NAMES) {
2077
- const fullPath = import_path.default.join(currentDir, fileName);
2078
- if (import_fs.default.existsSync(fullPath) && import_fs.default.statSync(fullPath).isFile()) {
2079
- return fullPath;
2080
- }
2081
- }
2082
- const parentDir = import_path.default.dirname(currentDir);
2083
- if (parentDir === currentDir) {
2084
- return null;
2085
- }
2086
- currentDir = parentDir;
2511
+ import_logger3.logger.warn(
2512
+ "runtime order arrival snapshot failed: %s %s",
2513
+ symbol,
2514
+ error?.message || String(error)
2515
+ );
2516
+ return {
2517
+ arrivalSnapshotTime: Date.now(),
2518
+ arrivalSource: "top_of_book_error",
2519
+ bid: null,
2520
+ ask: null,
2521
+ arrivalMid: null,
2522
+ spreadBps: null
2523
+ };
2087
2524
  }
2088
2525
  };
2089
- var resolvePluginModuleSpecifier = (moduleName, cwd = getTradejsProjectCwd()) => {
2090
- const normalized = String(moduleName ?? "").trim();
2091
- if (!normalized) {
2092
- return "";
2526
+ var validateEntryProtectionAtArrival = ({
2527
+ direction,
2528
+ signalPrice,
2529
+ bid,
2530
+ ask,
2531
+ arrivalMid,
2532
+ takeProfits,
2533
+ stopLossPrice
2534
+ }) => {
2535
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
2536
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
2537
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
2538
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
2093
2539
  }
2094
- if (normalized.startsWith("file://")) {
2095
- try {
2096
- return (0, import_url.fileURLToPath)(normalized);
2097
- } catch {
2098
- return normalized;
2540
+ for (const takeProfit of takeProfits) {
2541
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
2542
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
2099
2543
  }
2100
2544
  }
2101
- if (import_path.default.isAbsolute(normalized)) {
2102
- return normalized;
2103
- }
2104
- if (isRelativeModulePath(normalized)) {
2105
- return import_path.default.resolve(cwd, normalized);
2545
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
2546
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
2106
2547
  }
2107
- return normalized;
2108
2548
  };
2109
- var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
2110
- const cached = cachedByCwd.get(cwd);
2111
- if (cached) {
2112
- return cached;
2113
- }
2114
- const configFilePath = findConfigFilePath(cwd);
2115
- if (!configFilePath) {
2116
- cachedByCwd.set(cwd, {});
2117
- return {};
2118
- }
2119
- try {
2120
- const moduleExports = await importConfigFile(configFilePath);
2121
- const config = resolveExportedConfig(moduleExports);
2122
- cachedByCwd.set(cwd, config);
2123
- if (!announcedConfigFile.has(configFilePath)) {
2124
- announcedConfigFile.add(configFilePath);
2125
- import_logger2.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
2549
+ var createEntrySubmissionValidation = ({
2550
+ connector,
2551
+ symbol,
2552
+ direction,
2553
+ takeProfits,
2554
+ stopLossPrice,
2555
+ limits,
2556
+ onValidated
2557
+ }) => {
2558
+ if (!limits) return void 0;
2559
+ if (!connector.capabilities?.orderSubmissionValidation)
2560
+ throw new EntrySubmissionValidationError(
2561
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
2562
+ );
2563
+ return async ({ qty, executionPrice }) => {
2564
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
2565
+ const snapshot2 = simulated && executionPrice != null ? {
2566
+ arrivalSnapshotTime: Date.now(),
2567
+ arrivalSource: "simulation",
2568
+ bid: executionPrice,
2569
+ ask: executionPrice,
2570
+ arrivalMid: executionPrice,
2571
+ spreadBps: 0
2572
+ } : await getOrderArrivalSnapshot({
2573
+ connector,
2574
+ symbol,
2575
+ requireTimestamp: true
2576
+ });
2577
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
2578
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
2579
+ throw new EntrySubmissionValidationError(
2580
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
2581
+ );
2582
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
2583
+ throw new EntrySubmissionValidationError(
2584
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
2585
+ );
2586
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
2587
+ validateEntryProtectionAtArrival({
2588
+ direction,
2589
+ signalPrice: price,
2590
+ bid: snapshot2.bid,
2591
+ ask: snapshot2.ask,
2592
+ arrivalMid: snapshot2.arrivalMid,
2593
+ takeProfits,
2594
+ stopLossPrice
2595
+ });
2596
+ const prices = takeProfits.map((tp) => tp.price);
2597
+ const economics2 = validateEntryExecution({
2598
+ direction,
2599
+ price,
2600
+ qty,
2601
+ stopLossPrice,
2602
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
2603
+ limits
2604
+ });
2605
+ if (economics2.belowMinimumNetRiskRatio) {
2606
+ import_logger3.logger.warn(
2607
+ "entry submission warning: %s %s ENTRY_RR_BELOW_MIN_AT_SUBMISSION (RR %s, threshold %s)",
2608
+ symbol,
2609
+ direction,
2610
+ economics2.netRiskRatio,
2611
+ limits.minimumNetRiskRatio
2612
+ );
2613
+ }
2614
+ onValidated?.(snapshot2, economics2);
2615
+ };
2616
+ };
2617
+
2618
+ // src/ai.ts
2619
+ var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
2620
+ var import_redis3 = require("@tradejs/infra/redis");
2621
+
2622
+ // src/strategyAdapters/ai.ts
2623
+ var import_indicators2 = require("@tradejs/core/indicators");
2624
+ var import_aiFeatures = require("@tradejs/core/aiFeatures");
2625
+
2626
+ // src/aiShared.ts
2627
+ var MAX_AI_SERIES_POINTS = 5;
2628
+ var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
2629
+ "tradejs.indicators.compactSnapshot"
2630
+ );
2631
+ var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
2632
+ var trimSeriesDeep = (value) => {
2633
+ if (Array.isArray(value)) {
2634
+ const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
2635
+ const isMatrix = trimmed.every((item) => Array.isArray(item));
2636
+ if (isMatrix) {
2637
+ return trimmed;
2126
2638
  }
2127
- return config;
2128
- } catch (error) {
2129
- import_logger2.logger.log(
2130
- "warn",
2131
- "Failed to load TradeJS config from %s: %s",
2132
- configFilePath,
2133
- String(error)
2639
+ return trimmed.map(
2640
+ (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
2134
2641
  );
2135
- cachedByCwd.set(cwd, {});
2136
- return {};
2137
2642
  }
2643
+ if (value && typeof value === "object") {
2644
+ return Object.fromEntries(
2645
+ Object.entries(value).map(([key, nested]) => [
2646
+ key,
2647
+ trimSeriesDeep(nested)
2648
+ ])
2649
+ );
2650
+ }
2651
+ return value;
2652
+ };
2653
+ var buildCompactAiIndicatorsSnapshot = (value) => {
2654
+ const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
2655
+ if (typeof compactSnapshot === "function") {
2656
+ return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
2657
+ }
2658
+ return trimSeriesDeep(value);
2138
2659
  };
2139
2660
 
2140
- // src/strategy/manifests.ts
2141
- var SHARED_STRATEGY_REGISTRY_KEY = "__tradejsNodeSharedStrategyRegistryV1__";
2142
- var sharedRegistryScope = globalThis;
2143
- var sharedStrategyRegistry = sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] ?? (sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] = {
2144
- registryStateByProjectRoot: /* @__PURE__ */ new Map()
2145
- });
2146
- var createStrategyRegistryState = () => ({
2147
- strategyCreators: /* @__PURE__ */ new Map(),
2148
- strategyManifestsMap: /* @__PURE__ */ new Map(),
2149
- strategyEntriesMap: /* @__PURE__ */ new Map(),
2150
- strategySourcesMap: /* @__PURE__ */ new Map(),
2151
- strategyModuleResolutions: /* @__PURE__ */ new Map(),
2152
- pluginsLoadPromise: null
2661
+ // src/aiMarketContext.ts
2662
+ var toRecord = (value) => {
2663
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2664
+ return null;
2665
+ }
2666
+ return value;
2667
+ };
2668
+ var toFiniteNumber = (value) => {
2669
+ const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
2670
+ return Number.isFinite(numeric) ? numeric : null;
2671
+ };
2672
+ var roundTo = (value, decimals) => {
2673
+ const factor = 10 ** decimals;
2674
+ return Math.round(value * factor) / factor;
2675
+ };
2676
+ var buildMissingSpreadContext = () => ({
2677
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2678
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2679
+ available: false,
2680
+ value: null,
2681
+ zScore: null,
2682
+ bps: null,
2683
+ absBps: null,
2684
+ bias: null,
2685
+ severity: null
2153
2686
  });
2154
- var registryStateByProjectRoot = sharedStrategyRegistry.registryStateByProjectRoot;
2155
- var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
2156
- const projectRoot = getTradejsProjectCwd(cwd);
2157
- let state = registryStateByProjectRoot.get(projectRoot);
2158
- if (!state) {
2159
- state = createStrategyRegistryState();
2160
- registryStateByProjectRoot.set(projectRoot, state);
2687
+ var buildSpreadContextFromSignal = (signal) => {
2688
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2689
+ const relative = toRecord(baseContext?.relative);
2690
+ const execution = toRecord(relative?.execution);
2691
+ const spread = toFiniteNumber(execution?.venueSpread);
2692
+ const zScore = toFiniteNumber(execution?.venueSpreadZScore);
2693
+ if (spread == null) {
2694
+ return buildMissingSpreadContext();
2161
2695
  }
2696
+ const value = roundTo(spread, 8);
2697
+ const bps = roundTo(value * 1e4, 2);
2698
+ const absBps = Math.abs(bps);
2699
+ const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
2700
+ const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
2162
2701
  return {
2163
- projectRoot,
2164
- state
2702
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2703
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2704
+ available: true,
2705
+ value,
2706
+ zScore,
2707
+ bps,
2708
+ absBps,
2709
+ bias,
2710
+ severity
2165
2711
  };
2166
2712
  };
2167
- var toUniqueModules = (modules = []) => [
2168
- ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
2169
- ];
2170
- var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
2171
- const config = await loadTradejsConfig(cwd);
2713
+ var buildTrueDeltaContextFromSignal = (signal) => {
2714
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2715
+ const participation = toRecord(baseContext?.participation);
2716
+ const delta = toRecord(participation?.delta);
2717
+ const source = String(delta?.source ?? "");
2718
+ const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
2719
+ if (!delta || !isTrueDeltaSource) {
2720
+ return {
2721
+ source: source || null,
2722
+ available: false,
2723
+ buyPressurePct: null,
2724
+ buyVolume: null,
2725
+ sellVolume: null,
2726
+ netDelta: null,
2727
+ deltaPct: null,
2728
+ signedVolumeZScore: null
2729
+ };
2730
+ }
2172
2731
  return {
2173
- strategyModules: toUniqueModules(config.strategies),
2174
- indicatorModules: toUniqueModules(config.indicators)
2732
+ source,
2733
+ available: true,
2734
+ buyPressurePct: toFiniteNumber(delta.buyPressurePct),
2735
+ buyVolume: toFiniteNumber(delta.buyVolume),
2736
+ sellVolume: toFiniteNumber(delta.sellVolume),
2737
+ netDelta: toFiniteNumber(delta.netDelta),
2738
+ deltaPct: toFiniteNumber(delta.deltaPct),
2739
+ signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
2175
2740
  };
2176
2741
  };
2177
- var extractModuleEntries = (moduleExport, key) => {
2178
- if (!moduleExport || typeof moduleExport !== "object") {
2179
- return null;
2180
- }
2181
- const candidate = moduleExport;
2182
- if (Array.isArray(candidate[key])) {
2183
- return candidate[key];
2184
- }
2185
- const defaultExport = candidate.default;
2186
- if (defaultExport && Array.isArray(defaultExport[key])) {
2187
- return defaultExport[key];
2742
+ var buildTradeFlowContextFromSignal = (signal) => {
2743
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2744
+ const participation = toRecord(baseContext?.participation);
2745
+ const tradeFlow = toRecord(participation?.tradeFlow);
2746
+ if (!tradeFlow) {
2747
+ return {
2748
+ source: null,
2749
+ available: false,
2750
+ interval: null,
2751
+ stale: null,
2752
+ trades: null,
2753
+ buyPressurePct: null,
2754
+ netBaseDelta: null,
2755
+ netQuoteDelta: null
2756
+ };
2188
2757
  }
2189
- return null;
2190
- };
2191
- var extractStrategyPluginDefinition = (moduleExport) => {
2192
- const strategyEntries = extractModuleEntries(
2193
- moduleExport,
2194
- "strategyEntries"
2195
- );
2196
- return strategyEntries ? { strategyEntries } : null;
2758
+ return {
2759
+ source: String(tradeFlow.source ?? ""),
2760
+ available: true,
2761
+ interval: String(tradeFlow.interval ?? ""),
2762
+ stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
2763
+ trades: toFiniteNumber(tradeFlow.trades),
2764
+ buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
2765
+ netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
2766
+ netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
2767
+ };
2197
2768
  };
2198
- var extractIndicatorPluginDefinition = (moduleExport) => {
2199
- const indicatorEntries = extractModuleEntries(
2200
- moduleExport,
2201
- "indicatorEntries"
2202
- );
2203
- return indicatorEntries ? { indicatorEntries } : null;
2769
+ var buildMarketBreadthContextFromSignal = (signal) => {
2770
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2771
+ const relative = toRecord(baseContext?.relative);
2772
+ const breadth = toRecord(relative?.marketBreadth);
2773
+ if (!breadth) {
2774
+ return {
2775
+ source: null,
2776
+ available: false,
2777
+ universe: null,
2778
+ interval: null,
2779
+ stale: null,
2780
+ symbolsCount: null,
2781
+ advanceDeclineRatio: null,
2782
+ pctAboveMa20: null,
2783
+ pctAboveMa50: null,
2784
+ equalWeightedReturn: null,
2785
+ volumeWeightedReturn: null,
2786
+ dispersion: null
2787
+ };
2788
+ }
2789
+ return {
2790
+ source: String(breadth.source ?? ""),
2791
+ available: true,
2792
+ universe: String(breadth.universe ?? ""),
2793
+ interval: String(breadth.interval ?? ""),
2794
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
2795
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
2796
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
2797
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
2798
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
2799
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
2800
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
2801
+ dispersion: toFiniteNumber(breadth.dispersion)
2802
+ };
2204
2803
  };
2205
- var validateStrategyEntries = (moduleName, entries, state) => {
2206
- const issues = [];
2207
- const names = /* @__PURE__ */ new Set();
2208
- entries.forEach((entry, index) => {
2209
- const entryPath = `${moduleName}.strategyEntries[${index}]`;
2210
- const strategyName = entry?.manifest?.name;
2211
- if (typeof strategyName !== "string" || !strategyName.trim()) {
2212
- issues.push(`${entryPath}: manifest.name is required`);
2213
- } else if (names.has(strategyName) || state?.strategyEntriesMap.has(strategyName)) {
2214
- issues.push(`${entryPath}: duplicate strategy ${strategyName}`);
2215
- } else {
2216
- names.add(strategyName);
2217
- }
2218
- if (!entry || typeof entry !== "object" || !entry.defaults || typeof entry.defaults !== "object" || Array.isArray(entry.defaults)) {
2219
- issues.push(`${entryPath}: defaults must be an object`);
2220
- }
2221
- if (typeof entry?.parseConfig !== "function") {
2222
- issues.push(`${entryPath}: parseConfig is required`);
2223
- }
2224
- if (typeof entry?.createCore !== "function") {
2225
- issues.push(`${entryPath}: createCore is required`);
2804
+ var buildMarketBreadthsContextFromSignal = (signal) => {
2805
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2806
+ const relative = toRecord(baseContext?.relative);
2807
+ const breadths = toRecord(relative?.marketBreadths);
2808
+ const build = (key) => {
2809
+ const breadth = toRecord(breadths?.[key]);
2810
+ if (!breadth) {
2811
+ return {
2812
+ source: null,
2813
+ available: false,
2814
+ universe: null,
2815
+ interval: null,
2816
+ stale: null,
2817
+ symbolsCount: null,
2818
+ advanceDeclineRatio: null,
2819
+ pctAboveMa20: null,
2820
+ pctAboveMa50: null,
2821
+ equalWeightedReturn: null,
2822
+ volumeWeightedReturn: null,
2823
+ dispersion: null
2824
+ };
2226
2825
  }
2227
- });
2228
- return issues;
2229
- };
2230
- var registerEntries = (entries, source, state) => {
2231
- for (const entry of entries) {
2232
- const strategyName = entry.manifest.name;
2233
- state.strategyManifestsMap.set(strategyName, entry.manifest);
2234
- state.strategyEntriesMap.set(strategyName, entry);
2235
- state.strategySourcesMap.set(strategyName, source);
2236
- materializeStrategyCreator(strategyName, state);
2237
- }
2826
+ return {
2827
+ source: String(breadth.source ?? ""),
2828
+ available: true,
2829
+ universe: String(breadth.universe ?? ""),
2830
+ interval: String(breadth.interval ?? ""),
2831
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
2832
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
2833
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
2834
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
2835
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
2836
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
2837
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
2838
+ dispersion: toFiniteNumber(breadth.dispersion)
2839
+ };
2840
+ };
2841
+ return {
2842
+ top5: build("top5"),
2843
+ top10: build("top10"),
2844
+ top30: build("top30"),
2845
+ top50: build("top50"),
2846
+ top100: build("top100")
2847
+ };
2238
2848
  };
2239
- var materializeStrategyCreator = (strategyName, state) => {
2240
- if (state.strategyCreators.has(strategyName) || !sharedStrategyRegistry.strategyRuntimeFactory) {
2241
- return;
2242
- }
2243
- const entry = state.strategyEntriesMap.get(strategyName);
2244
- if (!entry) return;
2245
- state.strategyCreators.set(
2246
- strategyName,
2247
- sharedStrategyRegistry.strategyRuntimeFactory({
2248
- strategyName,
2249
- defaults: entry.defaults,
2250
- createCore: entry.createCore,
2251
- manifest: entry.manifest,
2252
- detectorKey: entry.detectorKey,
2253
- detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
2254
- resolveRegisteredManifest: (name) => state.strategyManifestsMap.get(name)
2255
- })
2256
- );
2849
+ var buildTargetVsReference = (signal, asset) => {
2850
+ const base = toRecord(signal.additionalIndicators?.baseContext);
2851
+ const relative = toRecord(base?.relative);
2852
+ const target = toRecord(relative?.[`targetVs${asset}`]);
2853
+ return {
2854
+ source: target ? String(target.source ?? "") : null,
2855
+ available: Boolean(target),
2856
+ ratioReturn1h: toFiniteNumber(target?.ratioReturn1h),
2857
+ ratioReturn4h: toFiniteNumber(target?.ratioReturn4h),
2858
+ ratioReturn24h: toFiniteNumber(target?.ratioReturn24h),
2859
+ alpha1h: toFiniteNumber(target?.[`alphaVs${asset}1h`]),
2860
+ alpha4h: toFiniteNumber(target?.[`alphaVs${asset}4h`]),
2861
+ alpha24h: toFiniteNumber(target?.[`alphaVs${asset}24h`]),
2862
+ beta: toFiniteNumber(target?.[`betaTo${asset}20`]),
2863
+ correlation: toFiniteNumber(target?.[`correlationTo${asset}20`]),
2864
+ ratioTrend: typeof target?.ratioTrend === "string" ? target.ratioTrend : null
2865
+ };
2257
2866
  };
2258
- var setStrategyRuntimeFactory = (factory) => {
2259
- sharedStrategyRegistry.strategyRuntimeFactory = factory;
2260
- for (const state of registryStateByProjectRoot.values()) {
2261
- for (const strategyName of state.strategyEntriesMap.keys()) {
2262
- materializeStrategyCreator(strategyName, state);
2263
- }
2264
- }
2867
+ var buildTargetVsBtcContextFromSignal = (signal) => {
2868
+ const { alpha1h, alpha4h, alpha24h, beta, correlation, ...shared } = buildTargetVsReference(signal, "Btc");
2869
+ return {
2870
+ ...shared,
2871
+ alphaVsBtc1h: alpha1h,
2872
+ alphaVsBtc4h: alpha4h,
2873
+ alphaVsBtc24h: alpha24h,
2874
+ betaToBtc20: beta,
2875
+ correlationToBtc20: correlation
2876
+ };
2265
2877
  };
2266
- var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
2267
- if (typeof importTradejsModule === "function") {
2268
- return importTradejsModule(moduleName, cwd);
2269
- }
2270
- return import(
2271
- /* webpackIgnore: true */
2272
- moduleName
2273
- );
2878
+ var buildTargetVsEthContextFromSignal = (signal) => {
2879
+ const { alpha1h, alpha4h, alpha24h, beta, correlation, ...shared } = buildTargetVsReference(signal, "Eth");
2880
+ return {
2881
+ ...shared,
2882
+ alphaVsEth1h: alpha1h,
2883
+ alphaVsEth4h: alpha4h,
2884
+ alphaVsEth24h: alpha24h,
2885
+ betaToEth20: beta,
2886
+ correlationToEth20: correlation
2887
+ };
2274
2888
  };
2275
- var resolveStrategyModuleEntry = async ({
2276
- strategyName,
2277
- moduleName,
2278
- cwd
2279
- }) => {
2280
- const { projectRoot, state } = getStrategyRegistryState(cwd);
2281
- const source = moduleName.trim();
2282
- const resolvedModuleName = resolvePluginModuleSpecifier(source, projectRoot);
2283
- const cacheKey = `${resolvedModuleName}\0${strategyName}`;
2284
- const cached = state.strategyModuleResolutions.get(cacheKey);
2285
- if (cached) return cached;
2286
- const resolution = (async () => {
2287
- const moduleExport = await importStrategyPluginModule(
2288
- resolvedModuleName,
2289
- projectRoot
2290
- );
2291
- const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
2292
- if (!pluginDefinition) {
2293
- throw new Error(`${source}: export { strategyEntries } is missing`);
2294
- }
2295
- const issues = validateStrategyEntries(
2296
- source,
2297
- pluginDefinition.strategyEntries
2298
- );
2299
- if (issues.length > 0) {
2300
- throw new Error(
2301
- ["Invalid TradeJS strategy module:", ...issues].join("\n")
2302
- );
2303
- }
2304
- const entry = pluginDefinition.strategyEntries.find(
2305
- (candidate) => candidate.manifest.name === strategyName
2306
- );
2307
- if (!entry) {
2308
- throw new Error(`${source}: strategy ${strategyName} is missing`);
2309
- }
2310
- const factory = sharedStrategyRegistry.strategyRuntimeFactory;
2311
- if (!factory) {
2312
- throw new Error("Strategy runtime factory is not configured");
2313
- }
2314
- const manifestsByName = new Map(
2315
- pluginDefinition.strategyEntries.map(
2316
- (candidate) => [candidate.manifest.name, candidate.manifest]
2317
- )
2318
- );
2889
+ var buildBtcAltRegimeContextFromSignal = (signal) => {
2890
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2891
+ const relative = toRecord(baseContext?.relative);
2892
+ const btcAltRegime = toRecord(relative?.btcAltRegime);
2893
+ if (!btcAltRegime) {
2319
2894
  return {
2320
- entry,
2321
- creator: factory({
2322
- strategyName,
2323
- defaults: entry.defaults,
2324
- createCore: entry.createCore,
2325
- manifest: entry.manifest,
2326
- detectorKey: entry.detectorKey,
2327
- detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
2328
- resolveRegisteredManifest: (name) => manifestsByName.get(name) ?? state.strategyManifestsMap.get(name)
2329
- }),
2330
- source
2895
+ source: null,
2896
+ available: false,
2897
+ universe: null,
2898
+ interval: null,
2899
+ stale: null,
2900
+ regime: null,
2901
+ btcReturn24h: null,
2902
+ altBasketReturn24h: null,
2903
+ btcVsAltReturn24h: null,
2904
+ btcTurnoverShare24h: null,
2905
+ btcTurnoverShareChange24h: null,
2906
+ altVolToBtcVol24h: null,
2907
+ altDispersion24h: null
2331
2908
  };
2332
- })();
2333
- state.strategyModuleResolutions.set(cacheKey, resolution);
2334
- try {
2335
- return await resolution;
2336
- } catch (error) {
2337
- state.strategyModuleResolutions.delete(cacheKey);
2338
- throw error;
2339
2909
  }
2910
+ return {
2911
+ source: String(btcAltRegime.source ?? ""),
2912
+ available: true,
2913
+ universe: String(btcAltRegime.universe ?? ""),
2914
+ interval: String(btcAltRegime.interval ?? ""),
2915
+ stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
2916
+ regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
2917
+ btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
2918
+ altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
2919
+ btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
2920
+ btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
2921
+ btcTurnoverShareChange24h: toFiniteNumber(
2922
+ btcAltRegime.btcTurnoverShareChange24h
2923
+ ),
2924
+ altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
2925
+ altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
2926
+ };
2340
2927
  };
2341
- var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
2342
- const { projectRoot, state } = getStrategyRegistryState(cwd);
2343
- if (!state.pluginsLoadPromise) {
2344
- (0, import_indicators.resetIndicatorRegistryCache)(projectRoot);
2345
- state.pluginsLoadPromise = (async () => {
2346
- const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
2347
- const strategySet = new Set(strategyModules);
2348
- const indicatorSet = new Set(indicatorModules);
2349
- const pluginModuleNames = [
2350
- .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
2351
- ];
2352
- if (!pluginModuleNames.length) {
2353
- return;
2354
- }
2355
- const issues = [];
2356
- for (const moduleName of pluginModuleNames) {
2357
- try {
2358
- const resolvedModuleName = resolvePluginModuleSpecifier(
2359
- moduleName,
2360
- projectRoot
2361
- );
2362
- const moduleExport = await importStrategyPluginModule(
2363
- resolvedModuleName,
2364
- projectRoot
2365
- );
2366
- if (strategySet.has(moduleName)) {
2367
- const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
2368
- if (!pluginDefinition) {
2369
- issues.push(
2370
- `${moduleName}: export { strategyEntries } is missing`
2371
- );
2372
- } else {
2373
- const entryIssues = validateStrategyEntries(
2374
- moduleName,
2375
- pluginDefinition.strategyEntries,
2376
- state
2377
- );
2378
- issues.push(...entryIssues);
2379
- if (entryIssues.length === 0) {
2380
- registerEntries(
2381
- pluginDefinition.strategyEntries,
2382
- moduleName,
2383
- state
2384
- );
2385
- }
2386
- }
2387
- }
2388
- if (indicatorSet.has(moduleName)) {
2389
- const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
2390
- if (!indicatorPluginDefinition) {
2391
- issues.push(
2392
- `${moduleName}: export { indicatorEntries } is missing`
2393
- );
2394
- } else {
2395
- (0, import_indicators.registerIndicatorEntries)(
2396
- indicatorPluginDefinition.indicatorEntries,
2397
- moduleName,
2398
- projectRoot
2399
- );
2400
- }
2401
- }
2402
- if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
2403
- issues.push(`${moduleName}: plugin is not declared in config`);
2404
- }
2405
- } catch (error) {
2406
- issues.push(`${moduleName}: failed to import: ${String(error)}`);
2407
- }
2408
- }
2409
- if (issues.length > 0) {
2410
- throw new Error(
2411
- ["Invalid TradeJS plugin catalog:", ...issues].join("\n")
2412
- );
2413
- }
2414
- })();
2928
+ var buildCmcGlobalContextFromSignal = (signal) => {
2929
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2930
+ const relative = toRecord(baseContext?.relative);
2931
+ const cmcGlobal = toRecord(relative?.cmcGlobal);
2932
+ if (!cmcGlobal) {
2933
+ return {
2934
+ source: null,
2935
+ available: false,
2936
+ interval: null,
2937
+ asOfTs: null,
2938
+ stale: null,
2939
+ totalMarketCapUsd: null,
2940
+ totalVolumeUsd: null,
2941
+ totalVolumeReportedUsd: null,
2942
+ altMarketCapUsd: null,
2943
+ altVolumeUsd: null,
2944
+ altVolumeReportedUsd: null,
2945
+ btcDominancePct: null,
2946
+ ethDominancePct: null,
2947
+ btcDominanceChange24hPct: null,
2948
+ ethDominanceChange24hPct: null,
2949
+ altMarketCapChange24hPct: null,
2950
+ altVolumeChange24hPct: null,
2951
+ activeCryptocurrencies: null,
2952
+ activeExchanges: null,
2953
+ activeMarketPairs: null,
2954
+ altLiquidityRegime: null
2955
+ };
2415
2956
  }
2416
- await state.pluginsLoadPromise;
2957
+ return {
2958
+ source: String(cmcGlobal.source ?? ""),
2959
+ available: true,
2960
+ interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
2961
+ asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
2962
+ stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
2963
+ totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
2964
+ totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
2965
+ totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
2966
+ altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
2967
+ altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
2968
+ altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
2969
+ btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
2970
+ ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
2971
+ btcDominanceChange24hPct: toFiniteNumber(
2972
+ cmcGlobal.btcDominanceChange24hPct
2973
+ ),
2974
+ ethDominanceChange24hPct: toFiniteNumber(
2975
+ cmcGlobal.ethDominanceChange24hPct
2976
+ ),
2977
+ altMarketCapChange24hPct: toFiniteNumber(
2978
+ cmcGlobal.altMarketCapChange24hPct
2979
+ ),
2980
+ altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
2981
+ activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
2982
+ activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
2983
+ activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
2984
+ altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
2985
+ };
2417
2986
  };
2418
- var ensureIndicatorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => ensureStrategyPluginsLoaded(cwd);
2419
- var getStrategyCreator = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2420
- if (moduleName?.trim()) {
2421
- await ensureStrategyPluginsLoaded(cwd);
2422
- return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).creator;
2987
+ var buildCmcReferenceAssetsContextFromSignal = (signal) => {
2988
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2989
+ const relative = toRecord(baseContext?.relative);
2990
+ const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
2991
+ if (!cmcReferenceAssets) {
2992
+ return {
2993
+ source: null,
2994
+ available: false,
2995
+ interval: null,
2996
+ asOfTs: null,
2997
+ stale: null,
2998
+ btcMarketCapUsd: null,
2999
+ ethMarketCapUsd: null,
3000
+ btcVolumeUsd: null,
3001
+ ethVolumeUsd: null,
3002
+ btcVolumeToMarketCap: null,
3003
+ ethVolumeToMarketCap: null,
3004
+ ethBtcMarketCapRatio: null,
3005
+ ethBtcMarketCapRatioChange24hPct: null,
3006
+ ethVsBtcVolumeRatio: null,
3007
+ referenceLiquidityRegime: null
3008
+ };
2423
3009
  }
2424
- await ensureStrategyPluginsLoaded(cwd);
2425
- const { state } = getStrategyRegistryState(cwd);
2426
- return state.strategyCreators.get(name);
3010
+ return {
3011
+ source: String(cmcReferenceAssets.source ?? ""),
3012
+ available: true,
3013
+ interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
3014
+ asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
3015
+ stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
3016
+ btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
3017
+ ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
3018
+ btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
3019
+ ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
3020
+ btcVolumeToMarketCap: toFiniteNumber(
3021
+ cmcReferenceAssets.btcVolumeToMarketCap
3022
+ ),
3023
+ ethVolumeToMarketCap: toFiniteNumber(
3024
+ cmcReferenceAssets.ethVolumeToMarketCap
3025
+ ),
3026
+ ethBtcMarketCapRatio: toFiniteNumber(
3027
+ cmcReferenceAssets.ethBtcMarketCapRatio
3028
+ ),
3029
+ ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
3030
+ cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
3031
+ ),
3032
+ ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
3033
+ referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
3034
+ };
2427
3035
  };
2428
- var getStrategyEntry = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2429
- if (moduleName?.trim()) {
2430
- await ensureStrategyPluginsLoaded(cwd);
2431
- return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).entry;
3036
+ var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
3037
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3038
+ const relative = toRecord(baseContext?.relative);
3039
+ const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
3040
+ if (!cmcExchangeLiquidity) {
3041
+ return {
3042
+ source: null,
3043
+ available: false,
3044
+ interval: null,
3045
+ asOfTs: null,
3046
+ stale: null,
3047
+ exchangesCount: null,
3048
+ totalVolumeUsd: null,
3049
+ totalVolumeChange24hPct: null,
3050
+ binanceVolumeUsd: null,
3051
+ binanceVolumeShare: null,
3052
+ topExchangeVolumeShare: null,
3053
+ liquidityRegime: null
3054
+ };
2432
3055
  }
2433
- await ensureStrategyPluginsLoaded(cwd);
2434
- const { state } = getStrategyRegistryState(cwd);
2435
- return state.strategyEntriesMap.get(name);
3056
+ return {
3057
+ source: String(cmcExchangeLiquidity.source ?? ""),
3058
+ available: true,
3059
+ interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
3060
+ asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
3061
+ stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
3062
+ exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
3063
+ totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
3064
+ totalVolumeChange24hPct: toFiniteNumber(
3065
+ cmcExchangeLiquidity.totalVolumeChange24hPct
3066
+ ),
3067
+ binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
3068
+ binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
3069
+ topExchangeVolumeShare: toFiniteNumber(
3070
+ cmcExchangeLiquidity.topExchangeVolumeShare
3071
+ ),
3072
+ liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
3073
+ };
2436
3074
  };
2437
- var getStrategyPluginSource = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2438
- if (moduleName?.trim()) {
2439
- await ensureStrategyPluginsLoaded(cwd);
2440
- return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).source;
3075
+ var buildCmcFearGreedContextFromSignal = (signal) => {
3076
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3077
+ const relative = toRecord(baseContext?.relative);
3078
+ const cmcFearGreed = toRecord(relative?.cmcFearGreed);
3079
+ if (!cmcFearGreed) {
3080
+ return {
3081
+ source: null,
3082
+ available: false,
3083
+ interval: null,
3084
+ asOfTs: null,
3085
+ stale: null,
3086
+ value: null,
3087
+ valueChange24h: null,
3088
+ valueChange7d: null,
3089
+ classification: null,
3090
+ sentimentRegime: null
3091
+ };
2441
3092
  }
2442
- await ensureStrategyPluginsLoaded(cwd);
2443
- const { state } = getStrategyRegistryState(cwd);
2444
- return state.strategySourcesMap.get(name);
3093
+ return {
3094
+ source: String(cmcFearGreed.source ?? ""),
3095
+ available: true,
3096
+ interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
3097
+ asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
3098
+ stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
3099
+ value: toFiniteNumber(cmcFearGreed.value),
3100
+ valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
3101
+ valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
3102
+ classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
3103
+ sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
3104
+ };
2445
3105
  };
2446
- var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
2447
- if (!name) {
2448
- return void 0;
3106
+ var buildCmcIndexesContextFromSignal = (signal) => {
3107
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3108
+ const relative = toRecord(baseContext?.relative);
3109
+ const cmcIndexes = toRecord(relative?.cmcIndexes);
3110
+ if (!cmcIndexes) {
3111
+ return {
3112
+ source: null,
3113
+ available: false,
3114
+ interval: null,
3115
+ asOfTs: null,
3116
+ stale: null,
3117
+ cmc100Value: null,
3118
+ cmc100Change24hPct: null,
3119
+ cmc100TopConstituentSymbol: null,
3120
+ cmc100TopConstituentWeightPct: null,
3121
+ cmc20Value: null,
3122
+ cmc20Change24hPct: null,
3123
+ cmc20TopConstituentSymbol: null,
3124
+ cmc20TopConstituentWeightPct: null,
3125
+ cmc20ToCmc100Ratio: null,
3126
+ cmc20ToCmc100RatioChange24hPct: null,
3127
+ indexRegime: null
3128
+ };
3129
+ }
3130
+ return {
3131
+ source: String(cmcIndexes.source ?? ""),
3132
+ available: true,
3133
+ interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
3134
+ asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
3135
+ stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
3136
+ cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
3137
+ cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
3138
+ cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
3139
+ cmc100TopConstituentWeightPct: toFiniteNumber(
3140
+ cmcIndexes.cmc100TopConstituentWeightPct
3141
+ ),
3142
+ cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
3143
+ cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
3144
+ cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
3145
+ cmc20TopConstituentWeightPct: toFiniteNumber(
3146
+ cmcIndexes.cmc20TopConstituentWeightPct
3147
+ ),
3148
+ cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
3149
+ cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
3150
+ cmcIndexes.cmc20ToCmc100RatioChange24hPct
3151
+ ),
3152
+ indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
3153
+ };
3154
+ };
3155
+ var buildReferenceTradeFlowContextFromSignal = (signal) => {
3156
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3157
+ const relative = toRecord(baseContext?.relative);
3158
+ const refs = toRecord(relative?.referenceTradeFlow);
3159
+ const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
3160
+ const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
3161
+ const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
3162
+ if (!refs) {
3163
+ return {
3164
+ source: null,
3165
+ available: false,
3166
+ primaryReferenceSymbol: null,
3167
+ referenceSymbols: [],
3168
+ primaryTradeFlowBuyPressurePct: null,
3169
+ primaryTradeFlowStale: null
3170
+ };
2449
3171
  }
2450
- const { state } = getStrategyRegistryState(cwd);
2451
- return state.strategyManifestsMap.get(name);
3172
+ return {
3173
+ source: String(refs.source ?? ""),
3174
+ available: true,
3175
+ primaryReferenceSymbol,
3176
+ referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
3177
+ primaryTradeFlowBuyPressurePct: toFiniteNumber(
3178
+ primaryTradeFlow?.buyPressurePct
3179
+ ),
3180
+ primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
3181
+ };
2452
3182
  };
2453
- var strategies = new Proxy(
2454
- {},
2455
- {
2456
- get: (_target, property) => {
2457
- if (typeof property !== "string") {
2458
- return void 0;
2459
- }
2460
- return getStrategyRegistryState().state.strategyCreators.get(property);
2461
- },
2462
- ownKeys: () => {
2463
- return [...getStrategyRegistryState().state.strategyCreators.keys()];
2464
- },
2465
- getOwnPropertyDescriptor: () => ({
2466
- enumerable: true,
2467
- configurable: true
2468
- })
3183
+ var buildAiMarketContext = (signal) => ({
3184
+ execution: {
3185
+ binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
3186
+ },
3187
+ participation: {
3188
+ trueDelta: buildTrueDeltaContextFromSignal(signal),
3189
+ tradeFlow: buildTradeFlowContextFromSignal(signal)
3190
+ },
3191
+ relative: {
3192
+ marketBreadth: buildMarketBreadthContextFromSignal(signal),
3193
+ marketBreadths: buildMarketBreadthsContextFromSignal(signal),
3194
+ targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
3195
+ targetVsEth: buildTargetVsEthContextFromSignal(signal),
3196
+ btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
3197
+ cmcGlobal: buildCmcGlobalContextFromSignal(signal),
3198
+ cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
3199
+ cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
3200
+ cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
3201
+ cmcIndexes: buildCmcIndexesContextFromSignal(signal),
3202
+ referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
2469
3203
  }
2470
- );
3204
+ });
2471
3205
 
2472
3206
  // src/strategy/policyProfiles.ts
2473
3207
  var profileMatches = (profile, universe, assetClass) => {
@@ -2569,12 +3303,61 @@ var buildBaseAiPayload = (signal) => {
2569
3303
  };
2570
3304
  };
2571
3305
  var defaultAiAdapter = {};
3306
+ var tickerProfileFeatureIds = new Set(
3307
+ import_aiFeatures.TICKER_PROFILE_AI_FEATURES.map(({ id }) => id)
3308
+ );
2572
3309
  var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
2573
3310
  var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
2574
3311
  var buildAiPayloadByStrategy = (signal) => {
2575
3312
  const basePayload = buildBaseAiPayload(signal);
2576
3313
  const adapter = getSignalAiAdapter(signal);
2577
- return adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
3314
+ const payload = adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
3315
+ const custom = getStrategyManifest(signal.strategy)?.aiFeatures ?? [];
3316
+ const frozen = signal.aiFeatureReplay?.snapshot;
3317
+ if (frozen) (0, import_aiFeatures.validateAiFeatureSnapshot)(frozen);
3318
+ const tickerProfile = signal.additionalIndicators?.tickerProfile ?? signal.additionalIndicators?.baseContext?.tickerProfile;
3319
+ const hasProfile = Boolean(
3320
+ tickerProfile || frozen?.definitions.some(({ id }) => tickerProfileFeatureIds.has(id))
3321
+ );
3322
+ const indicatorFeatures = (0, import_indicators2.getRegisteredIndicatorEntries)(
3323
+ getTradejsProjectCwd()
3324
+ ).flatMap((entry) => entry.indicator.enabled ? entry.aiFeatures ?? [] : []);
3325
+ const hasPortfolio = Boolean(
3326
+ signal.additionalIndicators?.portfolioContext || frozen?.definitions.some(
3327
+ ({ id }) => import_aiFeatures.PORTFOLIO_AI_FEATURES.some((d) => d.id === id)
3328
+ )
3329
+ );
3330
+ const definitions = [
3331
+ ...hasPortfolio ? import_aiFeatures.PORTFOLIO_AI_FEATURES : [],
3332
+ ...hasProfile ? import_aiFeatures.TICKER_PROFILE_AI_FEATURES : [],
3333
+ ...indicatorFeatures,
3334
+ ...custom
3335
+ ];
3336
+ if (!definitions.length && !signal.aiFeatureReplay?.snapshot) return payload;
3337
+ (0, import_aiFeatures.validateAiFeatureDefinitions)(definitions);
3338
+ if (signal.aiFeatureReplay && !frozen) {
3339
+ throw new Error(
3340
+ "Dataset has no frozen AI features; regenerate the decision-time export"
3341
+ );
3342
+ }
3343
+ if (frozen && JSON.stringify(frozen.definitions) !== JSON.stringify(definitions)) {
3344
+ throw new Error(
3345
+ "AI feature definition changed; regenerate the decision-time export"
3346
+ );
3347
+ }
3348
+ return {
3349
+ ...payload,
3350
+ featureSnapshot: frozen ?? (0, import_aiFeatures.projectAiFeatures)(definitions, {
3351
+ signal: tickerProfile ? {
3352
+ ...signal,
3353
+ additionalIndicators: {
3354
+ ...signal.additionalIndicators,
3355
+ tickerProfile
3356
+ }
3357
+ } : signal,
3358
+ payload
3359
+ })
3360
+ };
2578
3361
  };
2579
3362
  var buildAiSystemPromptAddonByStrategy = (signal) => getSignalAiAdapter(signal).buildSystemPromptAddon?.({ signal }) ?? "";
2580
3363
  var buildAiHumanPromptAddonByStrategy = (signal, payload) => getSignalAiAdapter(signal).buildHumanPromptAddon?.({
@@ -2988,352 +3771,104 @@ var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
2988
3771
  {
2989
3772
  role: "system",
2990
3773
  content: `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
2991
- },
2992
- { role: "user", content: humanPrompt }
2993
- ]
2994
- });
2995
- const parsed = parseAIResponse(response.content);
2996
- const normalized = normalizeAnalysis(parsed);
2997
- if (!options.signal) {
2998
- return normalized;
2999
- }
3000
- return postProcessAiAnalysisByStrategy(
3001
- options.signal,
3002
- normalized,
3003
- options.payload
3004
- );
3005
- };
3006
- var runAiPromptLocal = async (signal, options = {}) => {
3007
- const payload = options.payload ?? buildAiPayload(signal);
3008
- const gateContext = getDeterministicAiGateContext(payload);
3009
- const signalDirection = getSignalDirection(signal);
3010
- const deterministicQuality = getDeterministicQuality(gateContext);
3011
- const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
3012
- return postProcessLocalAiAnalysisByStrategy(
3013
- signal,
3014
- {
3015
- direction: approvalAllowedNow ? signalDirection : null,
3016
- quality: deterministicQuality,
3017
- needRetest: !approvalAllowedNow,
3018
- retestPrice: null,
3019
- takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
3020
- stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
3021
- },
3022
- payload
3023
- );
3024
- };
3025
- var askAI = async (signal, options = {}) => {
3026
- const { symbol } = signal;
3027
- const payload = buildAiPayload(signal);
3028
- const content = await runAiPrompt(
3029
- {
3030
- systemPrompt: buildAiSystemPrompt(signal),
3031
- humanPrompt: buildAiHumanPrompt(signal, payload)
3032
- },
3033
- {
3034
- ...options,
3035
- signal,
3036
- payload
3037
- }
3038
- );
3039
- await (0, import_redis.setData)(import_redis.redisKeys.analysis(symbol, signal.signalId), content);
3040
- return content;
3041
- };
3042
-
3043
- // src/strategyAdapters/ml.ts
3044
- var defaultMlAdapter = {
3045
- normalizeStrategyConfig: (strategyConfig) => strategyConfig
3046
- };
3047
- var getStrategyMlAdapter = (strategy, profileId) => {
3048
- const strategyAdapter = getStrategyProfileMlAdapter(
3049
- getStrategyManifest(strategy),
3050
- profileId
3051
- );
3052
- if (!strategyAdapter) return defaultMlAdapter;
3053
- return {
3054
- ...defaultMlAdapter,
3055
- ...strategyAdapter
3056
- };
3057
- };
3058
-
3059
- // src/mlPayload.ts
3060
- var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
3061
- return getStrategyMlAdapter(
3062
- strategyName,
3063
- profileId
3064
- ).normalizeStrategyConfig?.(strategyConfig);
3065
- };
3066
- var buildMlPayload = (payload) => {
3067
- const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
3068
- const profileId = payload.signal?.policyProfileId;
3069
- const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
3070
- const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
3071
- const nextSignal = {
3072
- ...normalizedSignal,
3073
- indicators: {
3074
- ...normalizedSignal?.indicators ?? {}
3075
- }
3076
- };
3077
- const nextContext = payload.context ? {
3078
- ...payload.context,
3079
- strategyConfig: normalizeStrategyConfig(
3080
- payload.context.strategyConfig,
3081
- strategyName,
3082
- profileId
3083
- )
3084
- } : void 0;
3085
- return {
3086
- signal: nextSignal,
3087
- context: nextContext
3088
- };
3089
- };
3090
-
3091
- // src/runtimeJournal.ts
3092
- var import_node_crypto = require("crypto");
3093
- var import_constants = require("@tradejs/core/constants");
3094
- var import_time = require("@tradejs/core/time");
3095
- var import_trade = require("@tradejs/core/trade");
3096
- var import_logger3 = require("@tradejs/infra/logger");
3097
- var import_redis2 = require("@tradejs/infra/redis");
3098
- var now = () => Date.now();
3099
- var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
3100
- var calculateClosedPnl = ({
3101
- direction,
3102
- entryPrice,
3103
- exitPrice,
3104
- qty
3105
- }) => {
3106
- const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
3107
- return Number.isFinite(pnl) ? pnl : null;
3108
- };
3109
- var createRuntimeOrderId = (strategy) => {
3110
- const prefix = (0, import_trade.createRuntimeOrderLinkPrefix)(strategy);
3111
- if (prefix === "tjs-") {
3112
- return `tjs-${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
3113
- }
3114
- return `${prefix}${toRandomOrderSuffix()}`;
3115
- };
3116
- var recordRuntimeTradeOpen = async (params) => {
3117
- const { userName } = params;
3118
- if (!userName) {
3119
- return null;
3120
- }
3121
- const record = {
3122
- ...params,
3123
- entryCount: 1,
3124
- lastEntryPrice: params.entryPrice,
3125
- lastEntryQty: params.qty,
3126
- lastEntryTimestamp: params.entryTimestamp,
3127
- status: "active",
3128
- currentPrice: params.entryPrice,
3129
- currentPnl: 0,
3130
- closedPnl: null,
3131
- exitPrice: null,
3132
- exitTimestamp: null,
3133
- lastSyncedAt: now()
3134
- };
3135
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(record.entryTimestamp);
3136
- const runtimeScopeId = record.deploymentId ?? record.accountId;
3137
- try {
3138
- await Promise.all([
3139
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, record.orderId), record, {
3140
- expire: 0
3141
- }),
3142
- (0, import_redis2.setHashJsonField)(
3143
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3144
- record.orderId,
3145
- record,
3146
- { expire: 0 }
3147
- ),
3148
- (0, import_redis2.setData)(
3149
- import_redis2.redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
3150
- { orderId: record.orderId },
3151
- { expire: 0 }
3152
- )
3153
- ]);
3154
- } catch (error) {
3155
- import_logger3.logger.error(
3156
- "runtime trade open journal failed: %s %s",
3157
- record.symbol,
3158
- error?.message || String(error)
3159
- );
3160
- }
3161
- return record;
3162
- };
3163
- var recordRuntimeTradeIncrease = async (params) => {
3164
- const {
3165
- userName,
3166
- strategy,
3167
- symbol,
3168
- direction,
3169
- resultingQty,
3170
- resultingEntryPrice,
3171
- addedQty,
3172
- addedEntryPrice,
3173
- entryTimestamp,
3174
- fee,
3175
- accountId,
3176
- deploymentId
3177
- } = params;
3178
- if (!userName) {
3179
- return null;
3180
- }
3181
- const existing = await getActiveRuntimeTrade({
3182
- userName,
3183
- symbol,
3184
- accountId,
3185
- deploymentId
3774
+ },
3775
+ { role: "user", content: humanPrompt }
3776
+ ]
3186
3777
  });
3187
- if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
3188
- return null;
3189
- }
3190
- const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
3191
- const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
3192
- const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
3193
- const next = {
3194
- ...existing,
3195
- ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
3196
- qty: resultingQty,
3197
- entryPrice: resultingEntryPrice,
3198
- entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
3199
- lastEntryPrice: addedEntryPrice,
3200
- lastEntryQty: addedQty,
3201
- lastEntryTimestamp: entryTimestamp,
3202
- currentPrice: resultingEntryPrice,
3203
- currentPnl: 0,
3204
- fee: openFee,
3205
- openFee,
3206
- totalFee,
3207
- lastSyncedAt: now()
3208
- };
3209
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3210
- try {
3211
- await Promise.all([
3212
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, existing.orderId), next, {
3213
- expire: 0
3214
- }),
3215
- (0, import_redis2.setHashJsonField)(
3216
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3217
- existing.orderId,
3218
- next,
3219
- { expire: 0 }
3220
- )
3221
- ]);
3222
- } catch (error) {
3223
- import_logger3.logger.error(
3224
- "runtime trade increase journal failed: %s %s",
3225
- symbol,
3226
- error?.message || String(error)
3227
- );
3778
+ const parsed = parseAIResponse(response.content);
3779
+ const normalized = normalizeAnalysis(parsed);
3780
+ if (!options.signal) {
3781
+ return normalized;
3228
3782
  }
3229
- return next;
3783
+ return postProcessAiAnalysisByStrategy(
3784
+ options.signal,
3785
+ normalized,
3786
+ options.payload
3787
+ );
3230
3788
  };
3231
- var getActiveRuntimeTrade = async (params) => {
3232
- const { userName, symbol, accountId, deploymentId } = params;
3233
- if (!userName) {
3234
- return null;
3235
- }
3236
- const activeRef = await (0, import_redis2.getData)(
3237
- import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
3238
- null
3789
+ var runAiPromptLocal = async (signal, options = {}) => {
3790
+ const payload = options.payload ?? buildAiPayload(signal);
3791
+ const gateContext = getDeterministicAiGateContext(payload);
3792
+ const signalDirection = getSignalDirection(signal);
3793
+ const deterministicQuality = getDeterministicQuality(gateContext);
3794
+ const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
3795
+ return postProcessLocalAiAnalysisByStrategy(
3796
+ signal,
3797
+ {
3798
+ direction: approvalAllowedNow ? signalDirection : null,
3799
+ quality: deterministicQuality,
3800
+ needRetest: !approvalAllowedNow,
3801
+ retestPrice: null,
3802
+ takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
3803
+ stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
3804
+ },
3805
+ payload
3239
3806
  );
3240
- const orderId = String(activeRef?.orderId || "").trim();
3241
- if (!orderId) {
3242
- return null;
3243
- }
3244
- const existing = await (0, import_redis2.getData)(
3245
- import_redis2.redisKeys.runtimeTrade(userName, orderId),
3246
- null
3807
+ };
3808
+ var askAI = async (signal, options = {}) => {
3809
+ const { symbol } = signal;
3810
+ const payload = buildAiPayload(signal);
3811
+ const content = await runAiPrompt(
3812
+ {
3813
+ systemPrompt: buildAiSystemPrompt(signal),
3814
+ humanPrompt: buildAiHumanPrompt(signal, payload)
3815
+ },
3816
+ {
3817
+ ...options,
3818
+ signal,
3819
+ payload
3820
+ }
3247
3821
  );
3248
- if (!existing || existing.status !== "active") {
3249
- await (0, import_redis2.delKey)(
3250
- import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
3251
- );
3252
- return null;
3253
- }
3254
- return existing;
3822
+ await (0, import_redis3.setData)(import_redis3.redisKeys.analysis(symbol, signal.signalId), content);
3823
+ return content;
3255
3824
  };
3256
- var markRuntimeTradeClosed = async (params) => {
3257
- const {
3258
- userName,
3259
- symbol,
3260
- strategy,
3261
- exitPrice,
3262
- exitTimestamp,
3263
- closedPnl,
3264
- exitType,
3265
- accountId,
3266
- deploymentId
3267
- } = params;
3268
- if (!userName) {
3269
- return null;
3270
- }
3271
- const existing = await getActiveRuntimeTrade({
3272
- userName,
3273
- symbol,
3274
- accountId,
3275
- deploymentId
3276
- });
3277
- if (!existing) {
3278
- return null;
3279
- }
3280
- const orderId = existing.orderId;
3281
- if (strategy && existing.strategy !== strategy) {
3282
- return null;
3283
- }
3284
- const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
3285
- const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
3286
- direction: existing.direction,
3287
- entryPrice: existing.entryPrice,
3288
- exitPrice: resolvedExitPrice,
3289
- qty: existing.qty
3290
- }) : existing.closedPnl ?? existing.currentPnl ?? null;
3291
- const next = {
3292
- ...existing,
3293
- status: "closed",
3294
- currentPrice: resolvedExitPrice,
3295
- currentPnl: resolvedClosedPnl,
3296
- closedPnl: resolvedClosedPnl,
3297
- exitPrice: resolvedExitPrice,
3298
- exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
3299
- exitType: exitType ?? existing.exitType ?? null,
3300
- lastSyncedAt: now()
3825
+
3826
+ // src/strategyAdapters/ml.ts
3827
+ var defaultMlAdapter = {
3828
+ normalizeStrategyConfig: (strategyConfig) => strategyConfig
3829
+ };
3830
+ var getStrategyMlAdapter = (strategy, profileId) => {
3831
+ const strategyAdapter = getStrategyProfileMlAdapter(
3832
+ getStrategyManifest(strategy),
3833
+ profileId
3834
+ );
3835
+ if (!strategyAdapter) return defaultMlAdapter;
3836
+ return {
3837
+ ...defaultMlAdapter,
3838
+ ...strategyAdapter
3839
+ };
3840
+ };
3841
+
3842
+ // src/mlPayload.ts
3843
+ var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
3844
+ return getStrategyMlAdapter(
3845
+ strategyName,
3846
+ profileId
3847
+ ).normalizeStrategyConfig?.(strategyConfig);
3848
+ };
3849
+ var buildMlPayload = (payload) => {
3850
+ const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
3851
+ const profileId = payload.signal?.policyProfileId;
3852
+ const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
3853
+ const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
3854
+ const nextSignal = {
3855
+ ...normalizedSignal,
3856
+ indicators: {
3857
+ ...normalizedSignal?.indicators ?? {}
3858
+ }
3859
+ };
3860
+ const nextContext = payload.context ? {
3861
+ ...payload.context,
3862
+ strategyConfig: normalizeStrategyConfig(
3863
+ payload.context.strategyConfig,
3864
+ strategyName,
3865
+ profileId
3866
+ )
3867
+ } : void 0;
3868
+ return {
3869
+ signal: nextSignal,
3870
+ context: nextContext
3301
3871
  };
3302
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3303
- const closeDayKey = (0, import_time.getRuntimeStorageDayKey)(next.exitTimestamp);
3304
- try {
3305
- await Promise.all([
3306
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, orderId), next, {
3307
- expire: import_constants.TTL_1M
3308
- }),
3309
- (0, import_redis2.setHashJsonField)(
3310
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3311
- orderId,
3312
- next,
3313
- { expire: import_constants.TTL_1M }
3314
- ),
3315
- (0, import_redis2.setHashJsonField)(
3316
- import_redis2.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
3317
- orderId,
3318
- next,
3319
- { expire: import_constants.TTL_1M }
3320
- ),
3321
- (0, import_redis2.delKey)(
3322
- import_redis2.redisKeys.runtimeActiveTrade(
3323
- userName,
3324
- symbol,
3325
- deploymentId ?? accountId
3326
- )
3327
- )
3328
- ]);
3329
- } catch (error) {
3330
- import_logger3.logger.error(
3331
- "runtime trade close journal failed: %s %s",
3332
- symbol,
3333
- error?.message || String(error)
3334
- );
3335
- }
3336
- return next;
3337
3872
  };
3338
3873
 
3339
3874
  // src/strategyHelpers/marketContextStages.ts
@@ -3345,7 +3880,7 @@ var import_logger4 = require("@tradejs/infra/logger");
3345
3880
  var import_strategies = require("@tradejs/core/strategies");
3346
3881
 
3347
3882
  // src/binanceBreadthUniverses.ts
3348
- var import_node_crypto2 = require("crypto");
3883
+ var import_node_crypto3 = require("crypto");
3349
3884
 
3350
3885
  // src/config/binanceBreadthUniverses.json
3351
3886
  var binanceBreadthUniverses_default = {
@@ -3584,7 +4119,7 @@ var BINANCE_BREADTH_UNIVERSE_KEYS = [
3584
4119
  "top50",
3585
4120
  "top100"
3586
4121
  ];
3587
- var fingerprint = (value, length) => (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
4122
+ var fingerprint = (value, length) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
3588
4123
  var normalizeSymbols = (symbols) => symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean);
3589
4124
  var buildBinanceBreadthUniverseSnapshot = ({
3590
4125
  rankedSymbols,
@@ -4608,7 +5143,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4608
5143
  };
4609
5144
 
4610
5145
  // src/strategyHelpers/derivativesContext.ts
4611
- var import_indicators2 = require("@tradejs/core/indicators");
5146
+ var import_indicators3 = require("@tradejs/core/indicators");
4612
5147
  var import_data2 = require("@tradejs/core/data");
4613
5148
  var import_strategies3 = require("@tradejs/core/strategies");
4614
5149
  var import_constants2 = require("@tradejs/core/constants");
@@ -4642,7 +5177,7 @@ var parseLookbackMs = () => {
4642
5177
  };
4643
5178
  var withHourlyFallbackRows = (rowsByInterval) => ({
4644
5179
  "15m": rowsByInterval["15m"] ?? [],
4645
- "1h": (0, import_indicators2.buildCoinalyzeHourlyRowsWithFallback)({
5180
+ "1h": (0, import_indicators3.buildCoinalyzeHourlyRowsWithFallback)({
4646
5181
  rows15m: rowsByInterval["15m"],
4647
5182
  fallbackRows1h: rowsByInterval["1h"]
4648
5183
  })
@@ -4770,7 +5305,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4770
5305
  const targetSymbol = normalizeSymbol(signal.symbol);
4771
5306
  const lookbackMs = parseLookbackMs();
4772
5307
  const decisionTimeMs = signal.timestamp + (0, import_data2.intervalToMs)(signal.interval);
4773
- const derivativesEndMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
5308
+ const derivativesEndMs = (0, import_indicators3.getLastClosedDerivativesBarStartMs)(
4774
5309
  decisionTimeMs,
4775
5310
  "15m"
4776
5311
  );
@@ -4785,7 +5320,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4785
5320
  });
4786
5321
  return [
4787
5322
  symbol,
4788
- (0, import_indicators2.buildDerivativesContext)({
5323
+ (0, import_indicators3.buildDerivativesContext)({
4789
5324
  symbol,
4790
5325
  direction: signal.direction,
4791
5326
  timestamp: derivativesEndMs,
@@ -4812,7 +5347,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4812
5347
  lookbackMs,
4813
5348
  ...params.abortSignal ? { signal: params.abortSignal } : {}
4814
5349
  });
4815
- const context = (0, import_indicators2.buildDerivativesContext)({
5350
+ const context = (0, import_indicators3.buildDerivativesContext)({
4816
5351
  symbol: targetSymbol,
4817
5352
  direction: signal.direction,
4818
5353
  timestamp: derivativesEndMs,
@@ -4860,7 +5395,7 @@ var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWha
4860
5395
  var import_logger7 = require("@tradejs/infra/logger");
4861
5396
 
4862
5397
  // src/hyperliquidWhaleUniverse.ts
4863
- var import_node_crypto3 = require("crypto");
5398
+ var import_node_crypto4 = require("crypto");
4864
5399
 
4865
5400
  // src/config/hyperliquidPerpUniverse.json
4866
5401
  var hyperliquidPerpUniverse_default = {
@@ -5032,7 +5567,7 @@ var hyperliquidWhales_default = {
5032
5567
  };
5033
5568
 
5034
5569
  // src/hyperliquidWhaleUniverse.ts
5035
- var fingerprint2 = (value) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
5570
+ var fingerprint2 = (value) => (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
5036
5571
  var validateUnique = (values, label) => {
5037
5572
  if (new Set(values).size !== values.length) {
5038
5573
  throw new Error(`Hyperliquid ${label} snapshot contains duplicates`);
@@ -5519,6 +6054,7 @@ var enrichSignalWithMarketContextStages = async ({
5519
6054
  })
5520
6055
  });
5521
6056
  }
6057
+ await enrichSignalWithTickerProfile(signal);
5522
6058
  const results = [];
5523
6059
  for (const stage of stages) {
5524
6060
  if (abortSignal?.aborted) break;
@@ -5566,12 +6102,12 @@ var findReplayAiAnalysis = ({
5566
6102
  direction,
5567
6103
  ai
5568
6104
  }) => {
5569
- const snapshots = ai?.replayAnalyses;
5570
- if (!Array.isArray(snapshots) || !snapshots.length) {
6105
+ const snapshots2 = ai?.replayAnalyses;
6106
+ if (!Array.isArray(snapshots2) || !snapshots2.length) {
5571
6107
  return void 0;
5572
6108
  }
5573
6109
  let best = null;
5574
- for (const snapshot2 of snapshots) {
6110
+ for (const snapshot2 of snapshots2) {
5575
6111
  if (snapshot2.symbol !== signal.symbol || snapshot2.direction !== direction || snapshot2.strategy && snapshot2.strategy !== signal.strategy) {
5576
6112
  continue;
5577
6113
  }
@@ -5731,7 +6267,9 @@ var executeEntryOrder = async ({
5731
6267
  beforePlaceOrder,
5732
6268
  recordRuntimeTrade = true,
5733
6269
  leverage,
5734
- executionLimits
6270
+ executionLimits,
6271
+ markPortfolioSubmitted,
6272
+ rejectPortfolioSubmission
5735
6273
  }) => {
5736
6274
  await beforePlaceOrder?.();
5737
6275
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
@@ -5804,7 +6342,13 @@ var executeEntryOrder = async ({
5804
6342
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
5805
6343
  orderId,
5806
6344
  signal,
5807
- ...beforeSubmit ? { beforeSubmit } : {}
6345
+ ...rejectPortfolioSubmission ? { onRejected: rejectPortfolioSubmission } : {},
6346
+ ...beforeSubmit || markPortfolioSubmitted ? {
6347
+ beforeSubmit: async (context) => {
6348
+ await beforeSubmit?.(context);
6349
+ await markPortfolioSubmitted?.(context.qty);
6350
+ }
6351
+ } : {}
5808
6352
  });
5809
6353
  const orderAckTime = Date.now();
5810
6354
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -6792,7 +7336,7 @@ var handleProtectDecision = async ({
6792
7336
  }
6793
7337
  return decision.code;
6794
7338
  };
6795
- var executeEntryDecision = async ({
7339
+ var executeAdmittedEntryDecision = async ({
6796
7340
  connector,
6797
7341
  symbol,
6798
7342
  decision,
@@ -6806,7 +7350,9 @@ var executeEntryDecision = async ({
6806
7350
  ai,
6807
7351
  recordRuntimeJournal,
6808
7352
  invokeStageHooks,
6809
- notifyRuntimeError
7353
+ notifyRuntimeError,
7354
+ markPortfolioSubmitted,
7355
+ rejectPortfolioSubmission
6810
7356
  }) => {
6811
7357
  const signal = decision.signal;
6812
7358
  const beforePlaceOrder = async () => {
@@ -6853,6 +7399,8 @@ var executeEntryDecision = async ({
6853
7399
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
6854
7400
  signal,
6855
7401
  beforePlaceOrder,
7402
+ markPortfolioSubmitted,
7403
+ rejectPortfolioSubmission,
6856
7404
  recordRuntimeTrade: recordRuntimeJournal,
6857
7405
  executionLimits: resolveEntryExecutionLimits(
6858
7406
  hookCtx.strategyConfig,
@@ -6993,6 +7541,37 @@ var executeEntryDecision = async ({
6993
7541
  }
6994
7542
  return signal ?? decision.code;
6995
7543
  };
7544
+ async function executeEntryDecision(params) {
7545
+ if (!params.portfolio) return executeAdmittedEntryDecision(params);
7546
+ const { decision, portfolio } = params;
7547
+ const signal = decision.signal;
7548
+ if (!signal || decision.orderPlan.positionIntent === "increase")
7549
+ return "PORTFOLIO_REQUIRES_NEW_SIGNAL_POSITION";
7550
+ signal.orderId ??= createRuntimeOrderId(signal.strategy);
7551
+ const result = await portfolio.submit(
7552
+ params.portfolioTime ?? decision.entryContext.timestamp,
7553
+ {
7554
+ id: signal.orderId,
7555
+ signalId: signal.signalId,
7556
+ strategy: signal.strategy,
7557
+ symbol: params.symbol,
7558
+ direction: decision.entryContext.direction,
7559
+ qty: decision.orderPlan.qty,
7560
+ timestamp: decision.entryContext.timestamp
7561
+ },
7562
+ (markPortfolioSubmitted, rejectPortfolioSubmission) => executeAdmittedEntryDecision({
7563
+ ...params,
7564
+ markPortfolioSubmitted,
7565
+ rejectPortfolioSubmission
7566
+ })
7567
+ );
7568
+ if (result.reason) {
7569
+ signal.orderStatus = "failed";
7570
+ signal.orderFailureReason = result.reason;
7571
+ return signal;
7572
+ }
7573
+ return result.value;
7574
+ }
6996
7575
 
6997
7576
  // src/strategyRuntime.ts
6998
7577
  var cloneWithPropertyDescriptors = (value) => Object.create(
@@ -7020,6 +7599,7 @@ var createStrategyRuntime = ({
7020
7599
  };
7021
7600
  const creator = async ({
7022
7601
  userName,
7602
+ connectorName,
7023
7603
  config: baseConfig,
7024
7604
  symbol,
7025
7605
  universe: requestedUniverse,
@@ -7058,6 +7638,17 @@ var createStrategyRuntime = ({
7058
7638
  const projectConfig = await loadTradejsConfig(projectRoot);
7059
7639
  const projectHooks = projectConfig.hooks;
7060
7640
  const env = String(config.ENV ?? "BACKTEST");
7641
+ const portfolio = getPortfolioRuntime({
7642
+ connectorName,
7643
+ value: baseConfig?.PORTFOLIO_POLICY ?? config.PORTFOLIO_POLICY,
7644
+ strategy: strategyName,
7645
+ connector,
7646
+ env,
7647
+ userName,
7648
+ accountId,
7649
+ deploymentId
7650
+ });
7651
+ const portfolioContext = portfolio ? (now2) => isRuntimeOrderExecutionEnabled({ config, env, connector }) ? portfolio.tick(now2) : portfolio.context(now2) : void 0;
7061
7652
  const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
7062
7653
  const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
7063
7654
  if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
@@ -7356,6 +7947,20 @@ var createStrategyRuntime = ({
7356
7947
  interval: decisionInterval
7357
7948
  }) => {
7358
7949
  if (!baseContext) return void 0;
7950
+ const profileSource = strategyManifest?.contextRequirements?.tickerProfiles;
7951
+ if (profileSource) {
7952
+ const profile = await loadTickerProfileContext({
7953
+ ...profileSource,
7954
+ projectRoot,
7955
+ symbol: decisionSymbol,
7956
+ timestamp: candle.timestamp
7957
+ });
7958
+ if (!profile)
7959
+ throw new Error(
7960
+ "Required ticker profile is unavailable at decision time"
7961
+ );
7962
+ baseContext = withTickerProfile(baseContext, profile);
7963
+ }
7359
7964
  if (!coreContextRequirements.has("hyperliquidWhales")) {
7360
7965
  return baseContext;
7361
7966
  }
@@ -7523,6 +8128,8 @@ var createStrategyRuntime = ({
7523
8128
  runtime: pending2.runtime
7524
8129
  });
7525
8130
  return executeEntryDecision({
8131
+ portfolio,
8132
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
7526
8133
  connector,
7527
8134
  symbol,
7528
8135
  decision: pending2.decision,
@@ -7540,6 +8147,10 @@ var createStrategyRuntime = ({
7540
8147
  });
7541
8148
  };
7542
8149
  const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
8150
+ if (portfolioContext)
8151
+ await portfolioContext(
8152
+ env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now()
8153
+ );
7543
8154
  appendCurrentMarketData(candle, btcCandle, options.ethCandle);
7544
8155
  const ethCandle = resolveEthCandle(candle, options.ethCandle);
7545
8156
  indicatorsState.setCurrentBar(candle, btcCandle, ethCandle);
@@ -7710,6 +8321,13 @@ var createStrategyRuntime = ({
7710
8321
  });
7711
8322
  const signal = decision.signal;
7712
8323
  if (signal) {
8324
+ if (portfolio)
8325
+ signal.additionalIndicators = {
8326
+ ...signal.additionalIndicators,
8327
+ portfolioContext: await portfolioContext(
8328
+ env === "BACKTEST" || env === "PARITY" ? decision.entryContext.timestamp : Date.now()
8329
+ )
8330
+ };
7713
8331
  if (runtimeLineage) signal.runtimeLineage = runtimeLineage;
7714
8332
  if (universe) signal.universe = universe;
7715
8333
  if (assetClass) signal.assetClass = assetClass;
@@ -7949,6 +8567,8 @@ var createStrategyRuntime = ({
7949
8567
  });
7950
8568
  }
7951
8569
  return executeEntryDecision({
8570
+ portfolio,
8571
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
7952
8572
  connector,
7953
8573
  symbol,
7954
8574
  decision,
@@ -8516,7 +9136,7 @@ var DEFAULT_CONNECTOR_NAME = BUILTIN_CONNECTOR_NAMES.ByBit;
8516
9136
 
8517
9137
  // src/runtimeStrategies.ts
8518
9138
  var import_jev8 = require("@tradejs/core/jev");
8519
- var import_node_crypto4 = require("crypto");
9139
+ var import_node_crypto5 = require("crypto");
8520
9140
  var import_promises2 = require("fs/promises");
8521
9141
  var import_node_path2 = __toESM(require("path"));
8522
9142
  var import_runtimeControls = require("@tradejs/infra/runtimeControls");
@@ -8575,7 +9195,7 @@ var normalizeForCanonicalJson = (value) => {
8575
9195
  }
8576
9196
  return value;
8577
9197
  };
8578
- var revision = (prefix, value) => `${prefix}:${(0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(normalizeForCanonicalJson(value))).digest("hex").slice(0, 16)}`;
9198
+ var revision = (prefix, value) => `${prefix}:${(0, import_node_crypto5.createHash)("sha256").update(JSON.stringify(normalizeForCanonicalJson(value))).digest("hex").slice(0, 16)}`;
8579
9199
  var computeStrategyRevision = ({
8580
9200
  strategyName,
8581
9201
  strategyPackage,
@@ -9051,13 +9671,13 @@ var loadResolvedRuntimeStrategies = async ({
9051
9671
  };
9052
9672
 
9053
9673
  // src/mcpJobs.ts
9054
- var import_node_crypto5 = require("crypto");
9674
+ var import_node_crypto6 = require("crypto");
9055
9675
  var import_promises3 = require("fs/promises");
9056
9676
  var import_node_path3 = __toESM(require("path"));
9057
9677
  var import_node_util = require("util");
9058
- var import_redis3 = require("@tradejs/infra/redis");
9678
+ var import_redis4 = require("@tradejs/infra/redis");
9059
9679
  var MCP_JOB_TTL = 7 * 86400;
9060
- var hash = (value) => (0, import_node_crypto5.createHash)("sha256").update(value).digest("hex");
9680
+ var hash = (value) => (0, import_node_crypto6.createHash)("sha256").update(value).digest("hex");
9061
9681
  var mcpJobStorageId = (userName, id) => `jobs:${hash(userName)}:${id}`;
9062
9682
  var mcpJobDirectory = (job) => import_node_path3.default.join(
9063
9683
  String(process.env.PROJECT_CWD || process.cwd()),
@@ -9087,17 +9707,17 @@ var enqueueMcpJob = async (principal, kind, request, idempotencyKey) => {
9087
9707
  `${principal.userName}:${principal.clientId}:${idempotencyKey}`
9088
9708
  );
9089
9709
  const storageId = mcpJobStorageId(principal.userName, id);
9090
- const existing = await import_redis3.mcpStorage.get(storageId);
9710
+ const existing = await import_redis4.mcpStorage.get(storageId);
9091
9711
  if (existing) {
9092
9712
  if (existing.kind !== kind || existing.request.requestFingerprint !== hash(JSON.stringify(request)))
9093
9713
  throw new Error("Idempotency key was used for another request");
9094
9714
  return existing;
9095
9715
  }
9096
- const worker = await import_redis3.mcpStorage.get(
9716
+ const worker = await import_redis4.mcpStorage.get(
9097
9717
  "worker:heartbeat"
9098
9718
  );
9099
9719
  if (!worker) throw new Error("MCP worker is unavailable; no job was queued");
9100
- if (!await import_redis3.mcpStorage.rateLimit(`jobs:${hash(principal.userName)}`, 10, 3600))
9720
+ if (!await import_redis4.mcpStorage.rateLimit(`jobs:${hash(principal.userName)}`, 10, 3600))
9101
9721
  throw new Error("Hourly job limit reached");
9102
9722
  const job = {
9103
9723
  id,
@@ -9119,8 +9739,8 @@ var enqueueMcpJob = async (principal, kind, request, idempotencyKey) => {
9119
9739
  const configId = String(request.configId || "");
9120
9740
  if (!/^[A-Za-z][A-Za-z0-9]*:[A-Za-z0-9_-]+$/.test(configId))
9121
9741
  throw new Error("Select a named Strategy:config");
9122
- const grid = await (0, import_redis3.getDataStrict)(
9123
- import_redis3.redisKeys.backtestConfig(principal.userName, configId)
9742
+ const grid = await (0, import_redis4.getDataStrict)(
9743
+ import_redis4.redisKeys.backtestConfig(principal.userName, configId)
9124
9744
  );
9125
9745
  if (!grid || typeof grid !== "object" || Array.isArray(grid))
9126
9746
  throw new Error("Backtest config not found");
@@ -9197,7 +9817,7 @@ var enqueueMcpJob = async (principal, kind, request, idempotencyKey) => {
9197
9817
  "Unsupported diagnostic kind; use verified runtime feedback for deployment-bound parity"
9198
9818
  );
9199
9819
  }
9200
- if (!await import_redis3.mcpStorage.put(storageId, job, MCP_JOB_TTL, true)) {
9820
+ if (!await import_redis4.mcpStorage.put(storageId, job, MCP_JOB_TTL, true)) {
9201
9821
  const winner = await readMcpJob(principal.userName, id);
9202
9822
  if (winner.kind !== kind || winner.request.requestFingerprint !== job.request.requestFingerprint)
9203
9823
  throw new Error("Idempotency key was used for another request");
@@ -9207,7 +9827,7 @@ var enqueueMcpJob = async (principal, kind, request, idempotencyKey) => {
9207
9827
  };
9208
9828
  var readMcpJob = async (userName, id) => {
9209
9829
  if (!/^[a-f0-9]{64}$/.test(id)) throw new Error("Invalid job id");
9210
- const job = await import_redis3.mcpStorage.get(mcpJobStorageId(userName, id));
9830
+ const job = await import_redis4.mcpStorage.get(mcpJobStorageId(userName, id));
9211
9831
  if (!job || job.userName !== userName) throw new Error("Job not found");
9212
9832
  return job;
9213
9833
  };
@@ -9283,7 +9903,7 @@ var cancelMcpJob = async (principal, id) => {
9283
9903
  updatedAt: Date.now(),
9284
9904
  ...job.status === "queued" ? { status: "cancelled", finishedAt: Date.now() } : {}
9285
9905
  };
9286
- if (await import_redis3.mcpStorage.compare(
9906
+ if (await import_redis4.mcpStorage.compare(
9287
9907
  mcpJobStorageId(job.userName, job.id),
9288
9908
  job,
9289
9909
  next,
@@ -9309,7 +9929,7 @@ var publicMcpJob = (job) => {
9309
9929
  // src/mcp.ts
9310
9930
  var mcpProjectRoot = () => String(process.env.PROJECT_CWD || process.cwd());
9311
9931
  var isMcpRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
9312
- var mcpId = (value) => (0, import_node_crypto6.createHash)("sha256").update(value).digest("hex");
9932
+ var mcpId = (value) => (0, import_node_crypto7.createHash)("sha256").update(value).digest("hex");
9313
9933
  var redactMcpData = (value) => {
9314
9934
  if (Array.isArray(value)) return value.map(redactMcpData);
9315
9935
  if (!isMcpRecord(value)) return value;
@@ -9372,7 +9992,7 @@ var readMcpChart = async (userName, request) => {
9372
9992
  });
9373
9993
  const visibleCandles = candles.filter((candle) => candle.timestamp >= visibleStart).slice(-request.bars);
9374
9994
  await ensureIndicatorPluginsLoaded(mcpProjectRoot());
9375
- const allIndicators = (0, import_indicators3.createIndicators)(
9995
+ const allIndicators = (0, import_indicators4.createIndicators)(
9376
9996
  candles,
9377
9997
  btc.filter((candle) => candle.timestamp < end),
9378
9998
  {
@@ -9461,7 +10081,7 @@ var McpCursorError = class extends Error {
9461
10081
  var RUNTIME_CURSOR_TTL = 86400;
9462
10082
  var readMcpRuntimePage = async (userName, kind, request) => {
9463
10083
  const deployment = await requireMcpDeployment(userName, request.deploymentId);
9464
- const prefix = kind === "signals" ? import_redis4.redisKeys.runtimeSignalBuckets(userName) : kind === "evaluations" ? import_redis4.redisKeys.runtimeSignalEvaluationBuckets(userName) : import_redis4.redisKeys.runtimeTrades(userName);
10084
+ const prefix = kind === "signals" ? import_redis5.redisKeys.runtimeSignalBuckets(userName) : kind === "evaluations" ? import_redis5.redisKeys.runtimeSignalEvaluationBuckets(userName) : import_redis5.redisKeys.runtimeTrades(userName);
9465
10085
  const limit = request.limit ?? 50;
9466
10086
  if (!Number.isInteger(limit) || limit < 1 || limit > 50)
9467
10087
  throw new Error("Select a page limit of 1\u201350");
@@ -9486,7 +10106,7 @@ var readMcpRuntimePage = async (userName, kind, request) => {
9486
10106
  scanned: false
9487
10107
  };
9488
10108
  if (request.cursor !== "0") {
9489
- const saved = await import_redis4.mcpStorage.get(
10109
+ const saved = await import_redis5.mcpStorage.get(
9490
10110
  `cursor:${request.cursor}`
9491
10111
  );
9492
10112
  if (!saved) throw new McpCursorError("CURSOR_EXPIRED");
@@ -9501,7 +10121,7 @@ var readMcpRuntimePage = async (userName, kind, request) => {
9501
10121
  if (!state.values.length) {
9502
10122
  if (!state.keys.length) {
9503
10123
  if (state.scanned && state.scan === "0") break;
9504
- const page = await (0, import_redis4.scanDataKeys)(prefix, state.scan);
10124
+ const page = await (0, import_redis5.scanDataKeys)(prefix, state.scan);
9505
10125
  storageReads++;
9506
10126
  state.scan = page.cursor;
9507
10127
  state.scanned = true;
@@ -9518,9 +10138,9 @@ var readMcpRuntimePage = async (userName, kind, request) => {
9518
10138
  storageReads++;
9519
10139
  if (kind === "orders") {
9520
10140
  state.keys.shift();
9521
- state.values = [await (0, import_redis4.getDataStrict)(key)];
10141
+ state.values = [await (0, import_redis5.getDataStrict)(key)];
9522
10142
  } else {
9523
- const page = await (0, import_redis4.scanHashJson)(key, state.hash);
10143
+ const page = await (0, import_redis5.scanHashJson)(key, state.hash);
9524
10144
  state.values = page.values;
9525
10145
  state.hash = page.cursor;
9526
10146
  if (page.cursor === "0") state.keys.shift();
@@ -9530,8 +10150,8 @@ var readMcpRuntimePage = async (userName, kind, request) => {
9530
10150
  let row = state.values.shift();
9531
10151
  inspected++;
9532
10152
  if (kind === "signals" && isMcpRecord(row) && typeof row.symbol === "string" && typeof row.signalId === "string")
9533
- row = await (0, import_redis4.getDataStrict)(
9534
- import_redis4.redisKeys.storeSignal(row.symbol, row.signalId)
10153
+ row = await (0, import_redis5.getDataStrict)(
10154
+ import_redis5.redisKeys.storeSignal(row.symbol, row.signalId)
9535
10155
  );
9536
10156
  if (!isMcpRecord(row) || row.deploymentId !== request.deploymentId || row.accountId !== deployment.accountId || row.userName !== void 0 && row.userName !== userName || request.strategy && row.strategy !== request.strategy || request.symbol && row.symbol !== request.symbol)
9537
10157
  continue;
@@ -9561,7 +10181,7 @@ var readMcpRuntimePage = async (userName, kind, request) => {
9561
10181
  let next = "0";
9562
10182
  if (state.values.length || state.keys.length || state.scan !== "0") {
9563
10183
  next = mcpId(JSON.stringify(state));
9564
- if (!await import_redis4.mcpStorage.put(`cursor:${next}`, state, RUNTIME_CURSOR_TTL))
10184
+ if (!await import_redis5.mcpStorage.put(`cursor:${next}`, state, RUNTIME_CURSOR_TTL))
9565
10185
  throw new Error("MCP cursor persistence failed");
9566
10186
  }
9567
10187
  return {
@@ -9587,8 +10207,8 @@ var readMcpRuntimePage = async (userName, kind, request) => {
9587
10207
  };
9588
10208
  var readMcpSignal = async (userName, signalId, deploymentId, timestamp, strategy) => {
9589
10209
  const deployment = await requireMcpDeployment(userName, deploymentId);
9590
- const ref = await (0, import_redis4.getHashJsonField)(
9591
- import_redis4.redisKeys.runtimeSignalBucket(
10210
+ const ref = await (0, import_redis5.getHashJsonField)(
10211
+ import_redis5.redisKeys.runtimeSignalBucket(
9592
10212
  userName,
9593
10213
  (0, import_time3.getRuntimeStorageDayKey)(timestamp),
9594
10214
  strategy
@@ -9598,8 +10218,8 @@ var readMcpSignal = async (userName, signalId, deploymentId, timestamp, strategy
9598
10218
  );
9599
10219
  if (!isMcpRecord(ref) || typeof ref.symbol !== "string")
9600
10220
  throw new Error("Signal not found");
9601
- const signal = await (0, import_redis4.getDataStrict)(
9602
- import_redis4.redisKeys.storeSignal(ref.symbol, signalId)
10221
+ const signal = await (0, import_redis5.getDataStrict)(
10222
+ import_redis5.redisKeys.storeSignal(ref.symbol, signalId)
9603
10223
  );
9604
10224
  if (!isMcpRecord(signal) || signal.deploymentId !== deploymentId || signal.accountId !== deployment.accountId || signal.userName !== void 0 && signal.userName !== userName)
9605
10225
  throw new Error("Signal not found");
@@ -9607,19 +10227,19 @@ var readMcpSignal = async (userName, signalId, deploymentId, timestamp, strategy
9607
10227
  };
9608
10228
  var readMcpEvaluationStats = async (userName, deploymentId, cursor) => {
9609
10229
  await requireMcpDeployment(userName, deploymentId);
9610
- const page = await (0, import_redis4.scanDataKeys)(
9611
- import_redis4.redisKeys.runtimeSignalEvaluationStatsBuckets(userName),
10230
+ const page = await (0, import_redis5.scanDataKeys)(
10231
+ import_redis5.redisKeys.runtimeSignalEvaluationStatsBuckets(userName),
9612
10232
  cursor
9613
10233
  );
9614
10234
  const buckets = [];
9615
10235
  for (const key of page.keys) {
9616
- const parts = key.slice(import_redis4.redisKeys.runtimeSignalEvaluationStatsBuckets(userName).length).split(":");
10236
+ const parts = key.slice(import_redis5.redisKeys.runtimeSignalEvaluationStatsBuckets(userName).length).split(":");
9617
10237
  if (parts.slice(1, -1).join(":") !== deploymentId) continue;
9618
10238
  buckets.push({
9619
10239
  bucket: key.slice(
9620
- import_redis4.redisKeys.runtimeSignalEvaluationStatsBuckets(userName).length
10240
+ import_redis5.redisKeys.runtimeSignalEvaluationStatsBuckets(userName).length
9621
10241
  ),
9622
- counters: await (0, import_redis4.getHashData)(key)
10242
+ counters: await (0, import_redis5.getHashData)(key)
9623
10243
  });
9624
10244
  }
9625
10245
  return {
@@ -9633,10 +10253,10 @@ var readMcpEvaluationStats = async (userName, deploymentId, cursor) => {
9633
10253
  };
9634
10254
  var readMcpBacktestPage = async (userName, kind, cursor) => {
9635
10255
  const prefix = kind === "configs" ? `users:${userName}:backtests:configs:` : `users:${userName}:backtests:results:`;
9636
- const page = await (0, import_redis4.scanDataKeys)(prefix, cursor);
10256
+ const page = await (0, import_redis5.scanDataKeys)(prefix, cursor);
9637
10257
  const items = [];
9638
10258
  for (const key of page.keys) {
9639
- const value = await (0, import_redis4.getDataStrict)(key);
10259
+ const value = await (0, import_redis5.getDataStrict)(key);
9640
10260
  items.push({
9641
10261
  id: key.slice(prefix.length),
9642
10262
  value: kind === "results" && isMcpRecord(value) ? {
@@ -9660,7 +10280,7 @@ var readMcpBacktestPage = async (userName, kind, cursor) => {
9660
10280
  var readMcpBacktestResult = async (userName, id, offset = 0, limit = 20) => {
9661
10281
  if (id.includes("*") || id.includes("?") || id.includes("["))
9662
10282
  throw new Error("Invalid result identifier");
9663
- const result = await (0, import_redis4.getDataStrict)(
10283
+ const result = await (0, import_redis5.getDataStrict)(
9664
10284
  `users:${userName}:backtests:results:${id}`
9665
10285
  );
9666
10286
  if (!result) throw new Error("Result not found");