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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -808,1671 +808,2449 @@ __export(diagnostics_exports, {
808
808
  module.exports = __toCommonJS(diagnostics_exports);
809
809
 
810
810
  // src/diagnostics/runtimeEvidenceArtifacts.ts
811
- var import_node_crypto5 = require("crypto");
811
+ var import_node_crypto6 = require("crypto");
812
812
  var import_promises3 = __toESM(require("fs/promises"));
813
813
  var import_node_path3 = __toESM(require("path"));
814
814
 
815
815
  // src/runtimeStrategies.ts
816
816
  var import_jev8 = require("@tradejs/core/jev");
817
- var import_node_crypto4 = require("crypto");
817
+ var import_node_crypto5 = require("crypto");
818
818
  var import_promises2 = require("fs/promises");
819
819
  var import_node_path2 = __toESM(require("path"));
820
820
  var import_runtimeControls = require("@tradejs/infra/runtimeControls");
821
821
  var import_tradingAccounts = require("@tradejs/infra/tradingAccounts");
822
822
 
823
- // src/strategyRuntime.ts
824
- var import_jev6 = require("@tradejs/core/jev");
823
+ // src/strategy/portfolioRuntime.ts
824
+ var import_node_crypto2 = require("crypto");
825
+ var import_portfolio2 = require("@tradejs/core/portfolio");
826
+ var import_redis2 = require("@tradejs/infra/redis");
825
827
 
826
- // src/jev.ts
827
- var import_promises = __toESM(require("fs/promises"));
828
- var import_node_path = __toESM(require("path"));
829
- var import_jev4 = require("@tradejs/core/jev");
830
- var import_jev5 = require("@tradejs/infra/jev");
831
- var import_userSettings = require("@tradejs/infra/userSettings");
832
- init_jevInput();
833
- init_jevInput();
834
- init_jevTraining();
835
- var resolveJevProvider = async (userName) => {
836
- const settings = await (0, import_userSettings.getUserSettings)(userName);
837
- const provider = {
838
- endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
839
- model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
840
- };
841
- (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
842
- if (!settings.JEV_API_KEY)
843
- throw new Error("Configure the Jev API key in Account settings");
844
- return provider;
845
- };
846
- var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
847
- var pending = /* @__PURE__ */ new Map();
848
- var pendingResponses = /* @__PURE__ */ new Map();
849
- var evaluateJevInput = async ({
850
- input,
851
- config,
852
- userName,
853
- projectRoot
854
- }) => {
855
- config = (0, import_jev4.parseJevConfig)(config);
856
- const inputHash = (0, import_jev5.jevHash)(input);
857
- const questions = questionsForJevInput(input);
858
- const questionsHash = (0, import_jev5.jevHash)(questions);
859
- const base = {
860
- questionSet: import_jev4.JEV_QUESTION_SET,
861
- schema: "tradejs-signal-assessment/v5",
862
- source: config.source,
863
- mode: config.mode,
864
- inputHash,
865
- questionsHash,
866
- geometryStatus: input.geometryStatus,
867
- levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
868
- };
869
- if (!input.questions.length)
870
- return {
871
- assessment: {
872
- ...base,
873
- status: "unavailable",
874
- model: "",
875
- scores: emptyScores(),
876
- confidence: emptyScores()
877
- }
878
- };
879
- if (config.source === "local") {
880
- const contents = await import_promises.default.readFile(
881
- import_node_path.default.resolve(projectRoot, config.modelFile),
882
- "utf8"
883
- );
884
- if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
885
- throw new Error("Local Jev gate checksum mismatch");
886
- const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
887
- const model = validateJevGateModel2(JSON.parse(contents));
888
- if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
889
- throw new Error("Local Jev gate strategy or question version mismatch");
890
- const scores = Object.fromEntries(
891
- import_jev4.JEV_DIMENSIONS.map((key) => [
892
- key,
893
- input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
894
- ])
895
- );
896
- return {
897
- assessment: {
898
- ...base,
899
- status: "available",
900
- model: config.modelSha256,
901
- scores,
902
- confidence: emptyScores()
828
+ // src/portfolio.ts
829
+ var import_portfolio = require("@tradejs/core/portfolio");
830
+ function createPortfolioCoordinator({
831
+ policy,
832
+ strategy,
833
+ store,
834
+ execution
835
+ }) {
836
+ const fingerprint3 = (0, import_portfolio.portfolioFingerprint)(policy, strategy);
837
+ let cache;
838
+ async function transact(now2, change, force = false) {
839
+ for (let attempt = 0; attempt < 16; attempt++) {
840
+ const previous = await store.read();
841
+ if (previous && previous.fingerprint !== fingerprint3)
842
+ throw new Error(
843
+ "Portfolio policy changed under existing state; reconcile the persisted scope before changing configuration"
844
+ );
845
+ const base = previous ?? (0, import_portfolio.createPortfolioState)(fingerprint3);
846
+ const bucket = Math.floor(now2 / policy.observationIntervalMs);
847
+ const observation = !force && cache?.revision === base.revision && cache.bucket === bucket ? cache.observation : await execution.observe(now2, base.reservations);
848
+ const reconciled = (0, import_portfolio.reconcilePortfolio)(
849
+ policy,
850
+ strategy,
851
+ base,
852
+ observation
853
+ );
854
+ for (const intent of base.closing) {
855
+ if (!observation.positions.some(
856
+ (p) => p.symbol === intent.position.symbol
857
+ ))
858
+ await execution.confirmClosed?.(intent, observation.asOf);
903
859
  }
904
- };
905
- }
906
- const provider = config.provider ?? await resolveJevProvider(userName);
907
- const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
908
- const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
909
- const file = import_node_path.default.join(dir, "records", `${id}.json`);
910
- const getRecord = async () => {
911
- const existing = await (0, import_jev5.readJevArtifact)(file);
912
- if (existing) {
913
- 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))
914
- throw new Error("Jev recording identity mismatch");
915
- (0, import_jev5.validateJevResponse)(existing.response, input.questions);
916
- for (const key of import_jev4.JEV_DIMENSIONS) {
917
- const expected = input.questions.includes(key) ? existing.response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null;
918
- if (existing.scores[key] !== expected)
919
- throw new Error("Jev recording scores do not match the response");
860
+ const changed = change(reconciled, observation);
861
+ const next = { ...changed.state, revision: base.revision + 1 };
862
+ if (await store.compare(previous, next)) {
863
+ cache = { revision: next.revision, bucket, observation };
864
+ return { state: next, observation, result: changed.result };
920
865
  }
921
- return existing;
866
+ cache = void 0;
922
867
  }
923
- if (config.source === "recorded")
924
- throw new Error(
925
- `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.`
868
+ throw new Error("Portfolio state contention; entry was not submitted");
869
+ }
870
+ async function tick(now2, force = false) {
871
+ const claimed = await transact(
872
+ now2,
873
+ (state, observation) => {
874
+ const commands = (0, import_portfolio.portfolioCloseCommands)(policy, state, observation);
875
+ return {
876
+ state: {
877
+ ...state,
878
+ closing: state.closing.map(
879
+ (c) => commands.some((x) => x.intent.positionId === c.positionId) ? { ...c, lastAttemptAt: observation.asOf } : c
880
+ )
881
+ },
882
+ result: commands
883
+ };
884
+ },
885
+ force
886
+ );
887
+ for (const command of claimed.result)
888
+ await execution.close(
889
+ command.position,
890
+ command.intent,
891
+ claimed.observation.asOf
926
892
  );
927
- const settings = await (0, import_userSettings.getUserSettings)(userName);
928
- if (!settings.JEV_API_KEY)
929
- throw new Error("Configure the Jev API key in Account settings");
930
- const apiKey = settings.JEV_API_KEY;
931
- const configuredProvider = await resolveJevProvider(userName);
932
- if (configuredProvider.endpoint !== provider.endpoint)
933
- throw new Error(
934
- "Jev account endpoint changed; update the frozen provider configuration"
893
+ if (claimed.result.length) {
894
+ cache = void 0;
895
+ const refreshed = await transact(
896
+ now2,
897
+ (state) => ({ state, result: void 0 }),
898
+ true
899
+ );
900
+ return (0, import_portfolio.buildPortfolioContext)(
901
+ policy,
902
+ strategy,
903
+ refreshed.state,
904
+ refreshed.observation
935
905
  );
936
- const started = Date.now();
937
- const state = stateForJevInput(input);
938
- const responseId = (0, import_jev5.jevHash)({
939
- state,
940
- questionsHash,
941
- provider,
942
- user: (0, import_jev5.jevHash)(userName)
943
- });
944
- const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
945
- let responsePromise = pendingResponses.get(responseFile);
946
- if (!responsePromise) {
947
- responsePromise = (async () => {
948
- const saved = await (0, import_jev5.readJevArtifact)(responseFile);
949
- if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
950
- const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
951
- await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
952
- return fresh;
953
- })();
954
- pendingResponses.set(responseFile, responsePromise);
955
- }
956
- let response;
957
- try {
958
- response = await responsePromise;
959
- } finally {
960
- pendingResponses.delete(responseFile);
961
906
  }
962
- const scores = Object.fromEntries(
963
- import_jev4.JEV_DIMENSIONS.map((key) => [
964
- key,
965
- input.questions.includes(key) ? response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null
966
- ])
907
+ return (0, import_portfolio.buildPortfolioContext)(
908
+ policy,
909
+ strategy,
910
+ claimed.state,
911
+ claimed.observation
967
912
  );
968
- const value = {
969
- schema: "tradejs-jev-record/v4",
970
- id,
971
- inputHash,
972
- questionsHash,
973
- provider,
974
- input,
975
- response,
976
- scores,
977
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
978
- elapsedMs: Date.now() - started
979
- };
980
- await (0, import_jev5.writeJevArtifact)(file, value);
981
- return await (0, import_jev5.readJevArtifact)(file);
982
- };
983
- const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
984
- let promise = pending.get(pendingKey);
985
- if (!promise) {
986
- promise = getRecord();
987
- pending.set(pendingKey, promise);
988
913
  }
989
- let record2;
990
- try {
991
- record2 = await promise;
992
- } finally {
993
- pending.delete(pendingKey);
914
+ async function updateReservation(now2, id, submitted, qty) {
915
+ return transact(
916
+ now2,
917
+ (s, o) => {
918
+ const exists2 = s.reservations.some((r) => r.id === id);
919
+ const blocked = submitted && (!exists2 || s.closing.length > 0 || o.asOf <= s.blockedThrough);
920
+ return {
921
+ state: {
922
+ ...s,
923
+ reservations: s.reservations.flatMap(
924
+ (r) => r.id !== id ? [r] : submitted && !blocked ? [{ ...r, submitted: true, qty: qty ?? r.qty }] : []
925
+ )
926
+ },
927
+ result: !blocked
928
+ };
929
+ },
930
+ true
931
+ );
994
932
  }
995
933
  return {
996
- record: record2,
997
- assessment: {
998
- ...base,
999
- status: "available",
1000
- recordId: id,
1001
- model: record2.response.model,
1002
- scores: record2.scores,
1003
- confidence: Object.fromEntries(
1004
- import_jev4.JEV_DIMENSIONS.map((key) => [
1005
- key,
1006
- record2.response.answers[key]?.confidence ?? null
1007
- ])
1008
- )
934
+ async context(now2) {
935
+ const state = await store.read() ?? (0, import_portfolio.createPortfolioState)(fingerprint3);
936
+ if (state.fingerprint !== fingerprint3)
937
+ throw new Error("Portfolio policy changed under existing state");
938
+ const observation = await execution.observe(now2, state.reservations);
939
+ (0, import_portfolio.validatePortfolioObservation)(observation);
940
+ return (0, import_portfolio.buildPortfolioContext)(policy, strategy, state, observation);
941
+ },
942
+ tick,
943
+ async submit(now2, entry, send) {
944
+ await tick(now2);
945
+ const admitted = await transact(
946
+ now2,
947
+ (s, o) => {
948
+ const r = (0, import_portfolio.reservePortfolioEntry)(policy, strategy, s, o, {
949
+ ...entry,
950
+ submitted: false,
951
+ oppositeHandled: false
952
+ });
953
+ return { state: r.state, result: r.reason };
954
+ },
955
+ true
956
+ );
957
+ if (admitted.result) return { reason: admitted.result };
958
+ let submitted = false;
959
+ let retry = 0;
960
+ let attemptId = entry.id;
961
+ let needsReservation = false;
962
+ try {
963
+ const value = await send(
964
+ async (qty) => {
965
+ if (!Number.isFinite(qty) || qty <= 0)
966
+ throw new Error("Invalid submitted portfolio quantity");
967
+ if (needsReservation) {
968
+ attemptId = `${entry.id.slice(0, 30)}r${++retry}`;
969
+ const admission = await transact(
970
+ now2,
971
+ (s, o) => {
972
+ const r = (0, import_portfolio.reservePortfolioEntry)(policy, strategy, s, o, {
973
+ ...entry,
974
+ id: attemptId,
975
+ qty,
976
+ submitted: true,
977
+ oppositeHandled: false
978
+ });
979
+ return { state: r.state, result: r.reason };
980
+ },
981
+ true
982
+ );
983
+ if (admission.result) throw new Error(admission.result);
984
+ needsReservation = false;
985
+ submitted = true;
986
+ return attemptId;
987
+ }
988
+ const update = await updateReservation(now2, attemptId, true, qty);
989
+ if (!update.result)
990
+ throw new Error("PORTFOLIO_SUBMISSION_NO_LONGER_ADMITTED");
991
+ submitted = true;
992
+ return attemptId;
993
+ },
994
+ async (retrying = false) => {
995
+ await transact(
996
+ now2,
997
+ (s) => ({
998
+ state: {
999
+ ...s,
1000
+ reservations: s.reservations.filter(
1001
+ (r) => r.id !== attemptId
1002
+ ),
1003
+ completedSignals: {
1004
+ ...Object.fromEntries(
1005
+ Object.entries(s.completedSignals).filter(
1006
+ ([id]) => !retrying || id !== entry.signalId
1007
+ )
1008
+ ),
1009
+ ...retrying ? {} : { [entry.signalId]: now2 }
1010
+ }
1011
+ },
1012
+ result: void 0
1013
+ }),
1014
+ true
1015
+ );
1016
+ submitted = false;
1017
+ needsReservation = retrying;
1018
+ }
1019
+ );
1020
+ if (!submitted) await updateReservation(now2, attemptId, false);
1021
+ else await tick(now2, true);
1022
+ return { value };
1023
+ } catch (error) {
1024
+ if (!submitted) await updateReservation(now2, attemptId, false);
1025
+ throw error;
1026
+ }
1009
1027
  }
1010
1028
  };
1011
- };
1012
- var assessSignalWithJev = async ({
1013
- signal,
1014
- config,
1015
- userName,
1016
- projectRoot,
1017
- strict,
1018
- scope = "runtime"
1019
- }) => {
1020
- let assessment;
1021
- try {
1022
- const input = buildJevInput(signal);
1023
- ({ assessment } = await evaluateJevInput({
1024
- input,
1025
- config,
1026
- userName,
1027
- projectRoot
1028
- }));
1029
- } catch (error) {
1030
- if (strict) throw error;
1031
- assessment = {
1032
- questionSet: import_jev4.JEV_QUESTION_SET,
1033
- schema: "tradejs-signal-assessment/v5",
1034
- source: config.source,
1035
- mode: config.mode,
1036
- status: "unavailable",
1037
- inputHash: "",
1038
- questionsHash: "",
1039
- geometryStatus: "absent",
1040
- levelsValid: null,
1041
- model: config.provider?.model ?? config.modelSha256 ?? "",
1042
- scores: emptyScores(),
1043
- confidence: emptyScores()
1044
- };
1045
- }
1046
- signal.assessment = assessment;
1047
- signal.additionalIndicators = {
1048
- ...signal.additionalIndicators,
1049
- jev: (0, import_jev4.jevAssessmentFeatures)(assessment)
1029
+ }
1030
+ function createMemoryPortfolioStore() {
1031
+ let state = null;
1032
+ return {
1033
+ read: async () => state === null ? null : structuredClone(state),
1034
+ compare: async (expected, next) => {
1035
+ if ((state?.revision ?? -1) !== (expected?.revision ?? -1)) return false;
1036
+ state = structuredClone(next);
1037
+ return true;
1038
+ }
1050
1039
  };
1051
- const identity = {
1052
- signalId: signal.signalId,
1053
- strategy: signal.strategy,
1054
- symbol: signal.symbol,
1055
- timestamp: signal.timestamp,
1056
- scope,
1057
- user: (0, import_jev5.jevHash)(userName),
1058
- config,
1059
- assessment
1060
- };
1061
- await (0, import_jev5.writeJevArtifact)(
1062
- import_node_path.default.resolve(
1063
- projectRoot,
1064
- config.recordsDir ?? "data/ai/jev",
1065
- "decisions",
1066
- `${(0, import_jev5.jevHash)(identity)}.json`
1067
- ),
1068
- identity
1069
- );
1070
- return assessment;
1071
- };
1072
-
1073
- // src/strategyRuntime.ts
1074
- var import_constants5 = require("@tradejs/core/constants");
1075
- var import_strategies6 = require("@tradejs/core/strategies");
1076
- var import_logger11 = require("@tradejs/infra/logger");
1077
-
1078
- // src/strategyHelpers/runtime.ts
1079
- var import_logger9 = require("@tradejs/infra/logger");
1080
- var import_constants3 = require("@tradejs/core/constants");
1081
- var import_ml2 = require("@tradejs/infra/ml");
1040
+ }
1082
1041
 
1083
- // src/strategy/entrySubmissionValidation.ts
1042
+ // src/runtimeJournal.ts
1043
+ var import_node_crypto = require("crypto");
1044
+ var import_constants = require("@tradejs/core/constants");
1045
+ var import_time = require("@tradejs/core/time");
1046
+ var import_trade = require("@tradejs/core/trade");
1084
1047
  var import_logger = require("@tradejs/infra/logger");
1085
-
1086
- // src/strategy/entryExecutionValidation.ts
1087
- var EntrySubmissionValidationError = class extends Error {
1088
- constructor() {
1089
- super(...arguments);
1090
- this.name = "EntrySubmissionValidationError";
1091
- }
1092
- };
1093
- var resolveEntryExecutionLimits = (config, direction) => {
1094
- const side = config[direction];
1095
- const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
1096
- const maximum = config.MAX_LOSS_VALUE;
1097
- if (minimum == null && maximum == null) return void 0;
1098
- const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
1099
- const maxLossValue = maximum == null ? void 0 : Number(maximum);
1100
- const feeRate = Number(config.RISK_FEE_RATE ?? 0);
1101
- const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
1102
- const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
1103
- const slippageBps = slippage + impact;
1104
- 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)
1105
- throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
1106
- return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
1107
- };
1108
- var validateEntryExecution = ({
1048
+ var import_redis = require("@tradejs/infra/redis");
1049
+ var now = () => Date.now();
1050
+ var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
1051
+ var calculateClosedPnl = ({
1109
1052
  direction,
1110
- price,
1111
- qty,
1112
- stopLossPrice,
1113
- takeProfitPrice,
1114
- limits
1053
+ entryPrice,
1054
+ exitPrice,
1055
+ qty
1115
1056
  }) => {
1116
- if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
1117
- throw new EntrySubmissionValidationError(
1118
- "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1119
- );
1120
- const side = direction === "LONG" ? 1 : -1;
1121
- if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
1122
- throw new EntrySubmissionValidationError(
1123
- "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
1124
- );
1125
- const slippage = limits.slippageBps / 1e4;
1126
- const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
1127
- const entryCost = cost(price, side);
1128
- const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
1129
- const rewardPerUnit = Math.max(
1130
- 0,
1131
- Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
1132
- );
1133
- const netRiskRatio = rewardPerUnit / lossPerUnit;
1134
- const plannedLossValue = qty * lossPerUnit;
1135
- if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
1136
- throw new EntrySubmissionValidationError(
1137
- "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1138
- );
1139
- if (limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio)
1140
- throw new EntrySubmissionValidationError(
1141
- "ENTRY_RR_BELOW_MIN_AT_SUBMISSION"
1142
- );
1143
- const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
1144
- return { netRiskRatio, plannedLossValue, riskExceeded };
1057
+ const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
1058
+ return Number.isFinite(pnl) ? pnl : null;
1145
1059
  };
1146
-
1147
- // src/strategy/entrySubmissionValidation.ts
1148
- var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
1149
- var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
1150
- var getOrderArrivalSnapshot = async ({
1151
- connector,
1152
- symbol,
1153
- requireTimestamp = false
1154
- }) => {
1155
- if (typeof connector.getTopOfBookTicker !== "function") {
1156
- return {
1157
- arrivalSnapshotTime: Date.now(),
1158
- arrivalSource: "unavailable",
1159
- bid: null,
1160
- ask: null,
1161
- arrivalMid: null,
1162
- spreadBps: null
1163
- };
1060
+ var createRuntimeOrderId = (strategy) => {
1061
+ const prefix = (0, import_trade.createRuntimeOrderLinkPrefix)(strategy);
1062
+ if (prefix === "tjs-") {
1063
+ return `tjs-${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
1064
+ }
1065
+ return `${prefix}${toRandomOrderSuffix()}`;
1066
+ };
1067
+ var recordRuntimeTradeOpen = async (params) => {
1068
+ const { userName } = params;
1069
+ if (!userName) {
1070
+ return null;
1164
1071
  }
1072
+ const record2 = {
1073
+ ...params,
1074
+ entryCount: 1,
1075
+ lastEntryPrice: params.entryPrice,
1076
+ lastEntryQty: params.qty,
1077
+ lastEntryTimestamp: params.entryTimestamp,
1078
+ status: "active",
1079
+ currentPrice: params.entryPrice,
1080
+ currentPnl: 0,
1081
+ closedPnl: null,
1082
+ exitPrice: null,
1083
+ exitTimestamp: null,
1084
+ lastSyncedAt: now()
1085
+ };
1086
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(record2.entryTimestamp);
1087
+ const runtimeScopeId = record2.deploymentId ?? record2.accountId;
1165
1088
  try {
1166
- const ticker = await connector.getTopOfBookTicker(symbol);
1167
- const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
1168
- const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
1169
- const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
1170
- const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
1171
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
1172
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
1173
- return {
1174
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
1175
- arrivalSource: "top_of_book",
1176
- bid,
1177
- ask,
1178
- arrivalMid,
1179
- spreadBps
1180
- };
1089
+ await Promise.all([
1090
+ (0, import_redis.setData)(import_redis.redisKeys.runtimeTrade(userName, record2.orderId), record2, {
1091
+ expire: 0
1092
+ }),
1093
+ (0, import_redis.setHashJsonField)(
1094
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1095
+ record2.orderId,
1096
+ record2,
1097
+ { expire: 0 }
1098
+ ),
1099
+ (0, import_redis.setData)(
1100
+ import_redis.redisKeys.runtimeActiveTrade(userName, record2.symbol, runtimeScopeId),
1101
+ { orderId: record2.orderId },
1102
+ { expire: 0 }
1103
+ )
1104
+ ]);
1181
1105
  } catch (error) {
1182
- import_logger.logger.warn(
1183
- "runtime order arrival snapshot failed: %s %s",
1184
- symbol,
1106
+ import_logger.logger.error(
1107
+ "runtime trade open journal failed: %s %s",
1108
+ record2.symbol,
1185
1109
  error?.message || String(error)
1186
1110
  );
1187
- return {
1188
- arrivalSnapshotTime: Date.now(),
1189
- arrivalSource: "top_of_book_error",
1190
- bid: null,
1191
- ask: null,
1192
- arrivalMid: null,
1193
- spreadBps: null
1194
- };
1195
1111
  }
1112
+ return record2;
1196
1113
  };
1197
- var validateEntryProtectionAtArrival = ({
1198
- direction,
1199
- signalPrice,
1200
- bid,
1201
- ask,
1202
- arrivalMid,
1203
- takeProfits,
1204
- stopLossPrice
1205
- }) => {
1206
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
1207
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
1208
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
1209
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
1210
- }
1211
- for (const takeProfit of takeProfits) {
1212
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
1213
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
1214
- }
1114
+ var recordRuntimeTradeIncrease = async (params) => {
1115
+ const {
1116
+ userName,
1117
+ strategy,
1118
+ symbol,
1119
+ direction,
1120
+ resultingQty,
1121
+ resultingEntryPrice,
1122
+ addedQty,
1123
+ addedEntryPrice,
1124
+ entryTimestamp,
1125
+ fee,
1126
+ accountId,
1127
+ deploymentId
1128
+ } = params;
1129
+ if (!userName) {
1130
+ return null;
1215
1131
  }
1216
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
1217
- throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
1132
+ const existing = await getActiveRuntimeTrade({
1133
+ userName,
1134
+ symbol,
1135
+ accountId,
1136
+ deploymentId
1137
+ });
1138
+ if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
1139
+ return null;
1218
1140
  }
1219
- };
1220
- var createEntrySubmissionValidation = ({
1221
- connector,
1222
- symbol,
1223
- direction,
1224
- takeProfits,
1225
- stopLossPrice,
1226
- limits,
1227
- onValidated
1228
- }) => {
1229
- if (!limits) return void 0;
1230
- if (!connector.capabilities?.orderSubmissionValidation)
1231
- throw new EntrySubmissionValidationError(
1232
- "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
1233
- );
1234
- return async ({ qty, executionPrice }) => {
1235
- const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
1236
- const snapshot2 = simulated && executionPrice != null ? {
1237
- arrivalSnapshotTime: Date.now(),
1238
- arrivalSource: "simulation",
1239
- bid: executionPrice,
1240
- ask: executionPrice,
1241
- arrivalMid: executionPrice,
1242
- spreadBps: 0
1243
- } : await getOrderArrivalSnapshot({
1244
- connector,
1245
- symbol,
1246
- requireTimestamp: true
1247
- });
1248
- const age = Date.now() - snapshot2.arrivalSnapshotTime;
1249
- if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
1250
- throw new EntrySubmissionValidationError(
1251
- "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
1252
- );
1253
- if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
1254
- throw new EntrySubmissionValidationError(
1255
- "ENTRY_QUOTE_STALE_AT_SUBMISSION"
1256
- );
1257
- const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
1258
- validateEntryProtectionAtArrival({
1259
- direction,
1260
- signalPrice: price,
1261
- bid: snapshot2.bid,
1262
- ask: snapshot2.ask,
1263
- arrivalMid: snapshot2.arrivalMid,
1264
- takeProfits,
1265
- stopLossPrice
1266
- });
1267
- const prices = takeProfits.map((tp) => tp.price);
1268
- const economics2 = validateEntryExecution({
1269
- direction,
1270
- price,
1271
- qty,
1272
- stopLossPrice,
1273
- takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
1274
- limits
1275
- });
1276
- onValidated?.(snapshot2, economics2);
1141
+ const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
1142
+ const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
1143
+ const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
1144
+ const next = {
1145
+ ...existing,
1146
+ ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
1147
+ qty: resultingQty,
1148
+ entryPrice: resultingEntryPrice,
1149
+ entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
1150
+ lastEntryPrice: addedEntryPrice,
1151
+ lastEntryQty: addedQty,
1152
+ lastEntryTimestamp: entryTimestamp,
1153
+ currentPrice: resultingEntryPrice,
1154
+ currentPnl: 0,
1155
+ fee: openFee,
1156
+ openFee,
1157
+ totalFee,
1158
+ lastSyncedAt: now()
1277
1159
  };
1278
- };
1279
-
1280
- // src/ai.ts
1281
- var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
1282
- var import_redis = require("@tradejs/infra/redis");
1283
-
1284
- // src/aiShared.ts
1285
- var MAX_AI_SERIES_POINTS = 5;
1286
- var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
1287
- "tradejs.indicators.compactSnapshot"
1288
- );
1289
- var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
1290
- var trimSeriesDeep = (value) => {
1291
- if (Array.isArray(value)) {
1292
- const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
1293
- const isMatrix = trimmed.every((item) => Array.isArray(item));
1294
- if (isMatrix) {
1295
- return trimmed;
1296
- }
1297
- return trimmed.map(
1298
- (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
1160
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
1161
+ try {
1162
+ await Promise.all([
1163
+ (0, import_redis.setData)(import_redis.redisKeys.runtimeTrade(userName, existing.orderId), next, {
1164
+ expire: 0
1165
+ }),
1166
+ (0, import_redis.setHashJsonField)(
1167
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1168
+ existing.orderId,
1169
+ next,
1170
+ { expire: 0 }
1171
+ )
1172
+ ]);
1173
+ } catch (error) {
1174
+ import_logger.logger.error(
1175
+ "runtime trade increase journal failed: %s %s",
1176
+ symbol,
1177
+ error?.message || String(error)
1299
1178
  );
1300
1179
  }
1301
- if (value && typeof value === "object") {
1302
- return Object.fromEntries(
1303
- Object.entries(value).map(([key, nested]) => [
1304
- key,
1305
- trimSeriesDeep(nested)
1306
- ])
1180
+ return next;
1181
+ };
1182
+ var getActiveRuntimeTrade = async (params) => {
1183
+ const { userName, symbol, accountId, deploymentId } = params;
1184
+ if (!userName) {
1185
+ return null;
1186
+ }
1187
+ const activeRef = await (0, import_redis.getData)(
1188
+ import_redis.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
1189
+ null
1190
+ );
1191
+ const orderId = String(activeRef?.orderId || "").trim();
1192
+ if (!orderId) {
1193
+ return null;
1194
+ }
1195
+ const existing = await (0, import_redis.getData)(
1196
+ import_redis.redisKeys.runtimeTrade(userName, orderId),
1197
+ null
1198
+ );
1199
+ if (!existing || existing.status !== "active") {
1200
+ await (0, import_redis.delKey)(
1201
+ import_redis.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
1307
1202
  );
1203
+ return null;
1308
1204
  }
1309
- return value;
1205
+ return existing;
1310
1206
  };
1311
- var buildCompactAiIndicatorsSnapshot = (value) => {
1312
- const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
1313
- if (typeof compactSnapshot === "function") {
1314
- return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
1207
+ var markRuntimeTradeClosed = async (params) => {
1208
+ const {
1209
+ userName,
1210
+ expectedOrderId,
1211
+ symbol,
1212
+ strategy,
1213
+ exitPrice,
1214
+ exitTimestamp,
1215
+ closedPnl,
1216
+ exitType,
1217
+ accountId,
1218
+ deploymentId
1219
+ } = params;
1220
+ if (!userName) {
1221
+ return null;
1315
1222
  }
1316
- return trimSeriesDeep(value);
1317
- };
1318
-
1319
- // src/aiMarketContext.ts
1320
- var toRecord = (value) => {
1321
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1223
+ const existing = expectedOrderId ? await (0, import_redis.getDataStrict)(
1224
+ import_redis.redisKeys.runtimeTrade(userName, expectedOrderId)
1225
+ ) : await getActiveRuntimeTrade({
1226
+ userName,
1227
+ symbol,
1228
+ accountId,
1229
+ deploymentId
1230
+ });
1231
+ if (!existing || expectedOrderId && existing.orderId !== expectedOrderId) {
1322
1232
  return null;
1323
1233
  }
1324
- return value;
1325
- };
1326
- var toFiniteNumber = (value) => {
1327
- const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
1328
- return Number.isFinite(numeric) ? numeric : null;
1329
- };
1330
- var roundTo = (value, decimals) => {
1331
- const factor = 10 ** decimals;
1332
- return Math.round(value * factor) / factor;
1333
- };
1334
- var buildMissingSpreadContext = () => ({
1335
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1336
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1337
- available: false,
1338
- value: null,
1339
- zScore: null,
1340
- bps: null,
1341
- absBps: null,
1342
- bias: null,
1343
- severity: null
1344
- });
1345
- var buildSpreadContextFromSignal = (signal) => {
1346
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1347
- const relative = toRecord(baseContext?.relative);
1348
- const execution = toRecord(relative?.execution);
1349
- const spread = toFiniteNumber(execution?.venueSpread);
1350
- const zScore = toFiniteNumber(execution?.venueSpreadZScore);
1351
- if (spread == null) {
1352
- return buildMissingSpreadContext();
1234
+ const orderId = existing.orderId;
1235
+ if (strategy && existing.strategy !== strategy) {
1236
+ return null;
1353
1237
  }
1354
- const value = roundTo(spread, 8);
1355
- const bps = roundTo(value * 1e4, 2);
1356
- const absBps = Math.abs(bps);
1357
- const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
1358
- const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
1359
- return {
1360
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1361
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1362
- available: true,
1363
- value,
1364
- zScore,
1365
- bps,
1366
- absBps,
1367
- bias,
1368
- severity
1238
+ const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
1239
+ const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
1240
+ direction: existing.direction,
1241
+ entryPrice: existing.entryPrice,
1242
+ exitPrice: resolvedExitPrice,
1243
+ qty: existing.qty
1244
+ }) : existing.closedPnl ?? existing.currentPnl ?? null;
1245
+ const next = expectedOrderId && existing.status === "closed" ? existing : {
1246
+ ...existing,
1247
+ status: "closed",
1248
+ currentPrice: resolvedExitPrice,
1249
+ currentPnl: resolvedClosedPnl,
1250
+ closedPnl: resolvedClosedPnl,
1251
+ exitPrice: resolvedExitPrice,
1252
+ exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
1253
+ exitType: exitType ?? existing.exitType ?? null,
1254
+ lastSyncedAt: now()
1369
1255
  };
1370
- };
1371
- var buildTrueDeltaContextFromSignal = (signal) => {
1372
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1373
- const participation = toRecord(baseContext?.participation);
1374
- const delta = toRecord(participation?.delta);
1375
- const source = String(delta?.source ?? "");
1376
- const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
1377
- if (!delta || !isTrueDeltaSource) {
1378
- return {
1379
- source: source || null,
1380
- available: false,
1381
- buyPressurePct: null,
1382
- buyVolume: null,
1383
- sellVolume: null,
1384
- netDelta: null,
1385
- deltaPct: null,
1386
- signedVolumeZScore: null
1387
- };
1256
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
1257
+ const closeDayKey = (0, import_time.getRuntimeStorageDayKey)(next.exitTimestamp);
1258
+ try {
1259
+ await Promise.all([
1260
+ (expectedOrderId ? import_redis.setDataStrict : import_redis.setData)(
1261
+ import_redis.redisKeys.runtimeTrade(userName, orderId),
1262
+ next,
1263
+ {
1264
+ expire: import_constants.TTL_1M
1265
+ }
1266
+ ),
1267
+ (expectedOrderId ? import_redis.setHashJsonFieldStrict : import_redis.setHashJsonField)(
1268
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1269
+ orderId,
1270
+ next,
1271
+ { expire: import_constants.TTL_1M }
1272
+ ),
1273
+ (expectedOrderId ? import_redis.setHashJsonFieldStrict : import_redis.setHashJsonField)(
1274
+ import_redis.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
1275
+ orderId,
1276
+ next,
1277
+ { expire: import_constants.TTL_1M }
1278
+ ),
1279
+ expectedOrderId ? (0, import_redis.deleteRuntimeTradeReference)(
1280
+ import_redis.redisKeys.runtimeActiveTrade(
1281
+ userName,
1282
+ symbol,
1283
+ deploymentId ?? accountId
1284
+ ),
1285
+ expectedOrderId
1286
+ ) : (0, import_redis.delKey)(
1287
+ import_redis.redisKeys.runtimeActiveTrade(
1288
+ userName,
1289
+ symbol,
1290
+ deploymentId ?? accountId
1291
+ )
1292
+ )
1293
+ ]);
1294
+ } catch (error) {
1295
+ if (expectedOrderId) throw error;
1296
+ import_logger.logger.error(
1297
+ "runtime trade close journal failed: %s %s",
1298
+ symbol,
1299
+ error?.message || String(error)
1300
+ );
1388
1301
  }
1389
- return {
1390
- source,
1391
- available: true,
1392
- buyPressurePct: toFiniteNumber(delta.buyPressurePct),
1393
- buyVolume: toFiniteNumber(delta.buyVolume),
1394
- sellVolume: toFiniteNumber(delta.sellVolume),
1395
- netDelta: toFiniteNumber(delta.netDelta),
1396
- deltaPct: toFiniteNumber(delta.deltaPct),
1397
- signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
1398
- };
1399
- };
1400
- var buildTradeFlowContextFromSignal = (signal) => {
1401
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1402
- const participation = toRecord(baseContext?.participation);
1403
- const tradeFlow = toRecord(participation?.tradeFlow);
1404
- if (!tradeFlow) {
1405
- return {
1406
- source: null,
1407
- available: false,
1408
- interval: null,
1409
- stale: null,
1410
- trades: null,
1411
- buyPressurePct: null,
1412
- netBaseDelta: null,
1413
- netQuoteDelta: null
1414
- };
1415
- }
1416
- return {
1417
- source: String(tradeFlow.source ?? ""),
1418
- available: true,
1419
- interval: String(tradeFlow.interval ?? ""),
1420
- stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
1421
- trades: toFiniteNumber(tradeFlow.trades),
1422
- buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
1423
- netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
1424
- netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
1425
- };
1426
- };
1427
- var buildMarketBreadthContextFromSignal = (signal) => {
1428
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1429
- const relative = toRecord(baseContext?.relative);
1430
- const breadth = toRecord(relative?.marketBreadth);
1431
- if (!breadth) {
1432
- return {
1433
- source: null,
1434
- available: false,
1435
- universe: null,
1436
- interval: null,
1437
- stale: null,
1438
- symbolsCount: null,
1439
- advanceDeclineRatio: null,
1440
- pctAboveMa20: null,
1441
- pctAboveMa50: null,
1442
- equalWeightedReturn: null,
1443
- volumeWeightedReturn: null,
1444
- dispersion: null
1445
- };
1446
- }
1447
- return {
1448
- source: String(breadth.source ?? ""),
1449
- available: true,
1450
- universe: String(breadth.universe ?? ""),
1451
- interval: String(breadth.interval ?? ""),
1452
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
1453
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
1454
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
1455
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
1456
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
1457
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
1458
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
1459
- dispersion: toFiniteNumber(breadth.dispersion)
1460
- };
1302
+ return next;
1461
1303
  };
1462
- var buildMarketBreadthsContextFromSignal = (signal) => {
1463
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1464
- const relative = toRecord(baseContext?.relative);
1465
- const breadths = toRecord(relative?.marketBreadths);
1466
- const build = (key) => {
1467
- const breadth = toRecord(breadths?.[key]);
1468
- if (!breadth) {
1304
+
1305
+ // src/strategy/portfolioRuntime.ts
1306
+ var coordinators = /* @__PURE__ */ new WeakMap();
1307
+ async function observeRuntimePortfolio(connector, binding, asOf, reservations, observedNow = Date.now) {
1308
+ if (!connector.getOpenPositionPnl || !connector.getEntryExecutions)
1309
+ throw new Error(
1310
+ "Portfolio policies require position marks and entry executions"
1311
+ );
1312
+ const orders = await Promise.all(
1313
+ reservations.map(async (r) => {
1314
+ if (r.submitted && connector.getEntryOrderObservation) {
1315
+ const fact2 = await connector.getEntryOrderObservation({
1316
+ symbol: r.symbol,
1317
+ orderId: r.id
1318
+ });
1319
+ if (fact2) return fact2;
1320
+ }
1321
+ const executions = r.submitted ? await connector.getEntryExecutions({
1322
+ symbol: r.symbol,
1323
+ startTime: r.timestamp,
1324
+ endTime: asOf
1325
+ }) : [];
1326
+ const filledQty = executions.filter(
1327
+ (e) => (e.orderLinkId === r.id || e.orderId === r.id) && e.direction === r.direction
1328
+ ).reduce((sum, e) => sum + e.qty, 0);
1469
1329
  return {
1470
- source: null,
1471
- available: false,
1472
- universe: null,
1473
- interval: null,
1474
- stale: null,
1475
- symbolsCount: null,
1476
- advanceDeclineRatio: null,
1477
- pctAboveMa20: null,
1478
- pctAboveMa50: null,
1479
- equalWeightedReturn: null,
1480
- volumeWeightedReturn: null,
1481
- dispersion: null
1330
+ id: r.id,
1331
+ status: filledQty >= r.qty ? "filled" : filledQty > 0 ? "partial" : "pending",
1332
+ filledQty,
1333
+ positionId: r.id
1334
+ };
1335
+ })
1336
+ );
1337
+ const snapshots2 = await connector.getOpenPositionPnl();
1338
+ const positions = await Promise.all(
1339
+ snapshots2.map(async (p) => {
1340
+ const reserved = reservations.find(
1341
+ (r) => r.symbol === p.symbol && r.direction === p.direction && orders.some(
1342
+ (o) => o.id === r.id && o.filledQty > 0 && p.qty <= o.filledQty * (1 + 1e-8)
1343
+ )
1344
+ );
1345
+ const trade = await getActiveRuntimeTrade({
1346
+ ...binding,
1347
+ symbol: p.symbol
1348
+ });
1349
+ 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;
1350
+ return {
1351
+ id: reserved?.id ?? owner?.orderId ?? `unknown:${p.symbol}:${p.direction}`,
1352
+ strategy: reserved?.strategy ?? owner?.strategy ?? null,
1353
+ symbol: p.symbol,
1354
+ direction: p.direction,
1355
+ qty: p.qty,
1356
+ entryPrice: p.price,
1357
+ markPrice: p.currentPrice,
1358
+ markedAt: p.observedAt ?? null,
1359
+ stopLossPrice: p.slPrice ?? p.stopLossPrice ?? null
1482
1360
  };
1361
+ })
1362
+ );
1363
+ return { asOf: Math.max(asOf, observedNow()), positions, orders };
1364
+ }
1365
+ function persistentStore(key) {
1366
+ const raw = /* @__PURE__ */ new WeakMap();
1367
+ return {
1368
+ async read() {
1369
+ const value = await import_redis2.portfolioStateStorage.read(key);
1370
+ if (value === null) return null;
1371
+ const state = JSON.parse(value);
1372
+ (0, import_portfolio2.assertPortfolioState)(state);
1373
+ raw.set(state, value);
1374
+ return state;
1375
+ },
1376
+ async compare(previous, next) {
1377
+ const expected = previous === null ? null : raw.get(previous);
1378
+ if (expected === void 0)
1379
+ throw new Error("Portfolio CAS requires a storage snapshot");
1380
+ return import_redis2.portfolioStateStorage.compare(key, expected, JSON.stringify(next));
1483
1381
  }
1484
- return {
1485
- source: String(breadth.source ?? ""),
1486
- available: true,
1487
- universe: String(breadth.universe ?? ""),
1488
- interval: String(breadth.interval ?? ""),
1489
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
1490
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
1491
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
1492
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
1493
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
1494
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
1495
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
1496
- dispersion: toFiniteNumber(breadth.dispersion)
1497
- };
1498
1382
  };
1499
- return {
1500
- top5: build("top5"),
1501
- top10: build("top10"),
1502
- top30: build("top30"),
1503
- top50: build("top50"),
1504
- top100: build("top100")
1383
+ }
1384
+ function getPortfolioRuntime({
1385
+ value,
1386
+ connectorName,
1387
+ strategy,
1388
+ connector,
1389
+ env,
1390
+ userName,
1391
+ accountId,
1392
+ deploymentId,
1393
+ symbol,
1394
+ canClose
1395
+ }) {
1396
+ const policy = (0, import_portfolio2.parsePortfolioPolicy)(value);
1397
+ if (!policy) return void 0;
1398
+ const replay = (env === "BACKTEST" || env === "PARITY") && Reflect.get(connector, "__tradejsReplayConnector") === true;
1399
+ if ((env === "BACKTEST" || env === "PARITY") && (!replay || !connector.getPortfolioObservation))
1400
+ throw new Error(
1401
+ "Portfolio policies require shared portfolio replay; isolated symbol backtests are unsupported"
1402
+ );
1403
+ if (!replay && (!userName || !accountId || !deploymentId || !connector.getOpenPositionPnl || !connector.getEntryExecutions))
1404
+ throw new Error(
1405
+ "Portfolio runtime requires exact user/account/deployment binding, marks and executions"
1406
+ );
1407
+ const binding = {
1408
+ userName,
1409
+ accountId,
1410
+ deploymentId
1505
1411
  };
1412
+ const scope = policy.scope === "strategy" ? strategy : policy.scope === "account" ? "account" : [...policy.scope.strategies].sort();
1413
+ const key = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify([binding, connectorName, connector.universe, scope])).digest("hex");
1414
+ const fingerprint3 = (0, import_portfolio2.portfolioFingerprint)(policy, strategy);
1415
+ const cacheKey = key;
1416
+ let registry = coordinators.get(connector);
1417
+ if (!registry) {
1418
+ registry = /* @__PURE__ */ new Map();
1419
+ coordinators.set(connector, registry);
1420
+ }
1421
+ const existing = registry.get(cacheKey);
1422
+ if (existing) {
1423
+ if (existing.fingerprint !== fingerprint3)
1424
+ throw new Error("Conflicting portfolio configuration for the same scope");
1425
+ if (symbol && canClose)
1426
+ existing.permissions.set(JSON.stringify([strategy, symbol]), canClose);
1427
+ return existing.coordinator;
1428
+ }
1429
+ const permissions = /* @__PURE__ */ new Map();
1430
+ if (symbol && canClose)
1431
+ permissions.set(JSON.stringify([strategy, symbol]), canClose);
1432
+ const coordinator = createPortfolioCoordinator({
1433
+ policy,
1434
+ strategy,
1435
+ store: replay ? createMemoryPortfolioStore() : persistentStore(key),
1436
+ execution: {
1437
+ async confirmClosed(intent, asOf) {
1438
+ if (replay) return;
1439
+ const position = intent.position;
1440
+ await markRuntimeTradeClosed({
1441
+ ...binding,
1442
+ strategy: position.strategy,
1443
+ expectedOrderId: position.id,
1444
+ symbol: position.symbol,
1445
+ exitPrice: position.markPrice,
1446
+ exitTimestamp: asOf,
1447
+ exitType: "exit"
1448
+ });
1449
+ },
1450
+ observe: (asOf, reservations) => replay ? connector.getPortfolioObservation(asOf, reservations) : observeRuntimePortfolio(connector, binding, asOf, reservations),
1451
+ async close(position, intent, asOf) {
1452
+ const permission = permissions.get(
1453
+ JSON.stringify([position.strategy, position.symbol])
1454
+ );
1455
+ if (!permission || !await permission(position, intent, asOf)) return;
1456
+ if (!replay) {
1457
+ const trade = await getActiveRuntimeTrade({
1458
+ ...binding,
1459
+ symbol: position.symbol
1460
+ });
1461
+ if (!trade || trade.orderId !== position.id)
1462
+ throw new Error("Portfolio close ownership changed");
1463
+ }
1464
+ await connector.closePosition({
1465
+ symbol: position.symbol,
1466
+ direction: position.direction,
1467
+ price: position.markPrice,
1468
+ timestamp: asOf,
1469
+ signal: {
1470
+ signalId: `${position.id}:${intent.reason}`,
1471
+ strategy: position.strategy,
1472
+ symbol: position.symbol,
1473
+ direction: position.direction,
1474
+ interval: "1",
1475
+ timestamp: asOf,
1476
+ prices: {
1477
+ riskRatio: 0,
1478
+ currentPrice: position.markPrice,
1479
+ takeProfitPrice: position.markPrice,
1480
+ stopLossPrice: position.markPrice
1481
+ },
1482
+ figures: {},
1483
+ indicators: {},
1484
+ additionalIndicators: { portfolioExitReason: intent.reason }
1485
+ }
1486
+ });
1487
+ }
1488
+ }
1489
+ });
1490
+ registry.set(cacheKey, { fingerprint: fingerprint3, coordinator, permissions });
1491
+ return coordinator;
1492
+ }
1493
+
1494
+ // src/strategyHelpers/tickerProfiles.ts
1495
+ var import_tickerProfiles = require("@tradejs/infra/tickerProfiles");
1496
+ var import_tickerProfiles2 = require("@tradejs/core/tickerProfiles");
1497
+
1498
+ // src/tradejsConfig.ts
1499
+ var import_fs = __toESM(require("fs"));
1500
+ var import_path = __toESM(require("path"));
1501
+ var import_url = require("url");
1502
+ var import_config = require("@tradejs/core/config");
1503
+ var import_logger2 = require("@tradejs/infra/logger");
1504
+ var CONFIG_FILE_NAMES = [
1505
+ "tradejs.config.ts",
1506
+ "tradejs.config.mts",
1507
+ "tradejs.config.js",
1508
+ "tradejs.config.mjs",
1509
+ "tradejs.config.cjs"
1510
+ ];
1511
+ var TS_MODULE_RE = /\.(cts|mts|ts)$/i;
1512
+ var cachedByCwd = /* @__PURE__ */ new Map();
1513
+ var announcedConfigFile = /* @__PURE__ */ new Set();
1514
+ var tsNodeRegistered = false;
1515
+ var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
1516
+ var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
1517
+ var getTradejsProjectCwd = (cwd) => {
1518
+ const explicit = String(cwd ?? "").trim();
1519
+ if (explicit) {
1520
+ return import_path.default.resolve(explicit);
1521
+ }
1522
+ const fromEnv = String(process.env.PROJECT_CWD || "").trim();
1523
+ if (fromEnv) {
1524
+ return import_path.default.resolve(fromEnv);
1525
+ }
1526
+ return process.cwd();
1506
1527
  };
1507
- var buildTargetVsBtcContextFromSignal = (signal) => {
1508
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1509
- const relative = toRecord(baseContext?.relative);
1510
- const targetVsBtc = toRecord(relative?.targetVsBtc);
1511
- if (!targetVsBtc) {
1512
- return {
1513
- source: null,
1514
- available: false,
1515
- ratioReturn1h: null,
1516
- ratioReturn4h: null,
1517
- ratioReturn24h: null,
1518
- alphaVsBtc1h: null,
1519
- alphaVsBtc4h: null,
1520
- alphaVsBtc24h: null,
1521
- betaToBtc20: null,
1522
- correlationToBtc20: null,
1523
- ratioTrend: null
1524
- };
1528
+ var normalizeConfig = (rawConfig) => {
1529
+ if (!rawConfig || typeof rawConfig !== "object") {
1530
+ return {};
1525
1531
  }
1532
+ const config = rawConfig;
1533
+ const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
1534
+ const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
1535
+ const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
1536
+ const hooks = (0, import_config.normalizeTradejsConfigHooks)(
1537
+ config.hooks
1538
+ );
1539
+ const runtime = config.runtime && typeof config.runtime === "object" && !Array.isArray(config.runtime) ? config.runtime : void 0;
1526
1540
  return {
1527
- source: String(targetVsBtc.source ?? ""),
1528
- available: true,
1529
- ratioReturn1h: toFiniteNumber(targetVsBtc.ratioReturn1h),
1530
- ratioReturn4h: toFiniteNumber(targetVsBtc.ratioReturn4h),
1531
- ratioReturn24h: toFiniteNumber(targetVsBtc.ratioReturn24h),
1532
- alphaVsBtc1h: toFiniteNumber(targetVsBtc.alphaVsBtc1h),
1533
- alphaVsBtc4h: toFiniteNumber(targetVsBtc.alphaVsBtc4h),
1534
- alphaVsBtc24h: toFiniteNumber(targetVsBtc.alphaVsBtc24h),
1535
- betaToBtc20: toFiniteNumber(targetVsBtc.betaToBtc20),
1536
- correlationToBtc20: toFiniteNumber(targetVsBtc.correlationToBtc20),
1537
- ratioTrend: typeof targetVsBtc.ratioTrend === "string" ? targetVsBtc.ratioTrend : null
1541
+ strategies: strategies2,
1542
+ indicators,
1543
+ connectors,
1544
+ ...hooks ? { hooks } : {},
1545
+ ...runtime ? { runtime } : {}
1538
1546
  };
1539
1547
  };
1540
- var buildBtcAltRegimeContextFromSignal = (signal) => {
1541
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1542
- const relative = toRecord(baseContext?.relative);
1543
- const btcAltRegime = toRecord(relative?.btcAltRegime);
1544
- if (!btcAltRegime) {
1545
- return {
1546
- source: null,
1547
- available: false,
1548
- universe: null,
1549
- interval: null,
1550
- stale: null,
1551
- regime: null,
1552
- btcReturn24h: null,
1553
- altBasketReturn24h: null,
1554
- btcVsAltReturn24h: null,
1555
- btcTurnoverShare24h: null,
1556
- btcTurnoverShareChange24h: null,
1557
- altVolToBtcVol24h: null,
1558
- altDispersion24h: null
1559
- };
1548
+ var getNodeCreateRequire = () => {
1549
+ const builtinModule = process.getBuiltinModule?.("module");
1550
+ if (typeof builtinModule?.createRequire === "function") {
1551
+ return builtinModule.createRequire;
1560
1552
  }
1561
- return {
1562
- source: String(btcAltRegime.source ?? ""),
1563
- available: true,
1564
- universe: String(btcAltRegime.universe ?? ""),
1565
- interval: String(btcAltRegime.interval ?? ""),
1566
- stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
1567
- regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
1568
- btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
1569
- altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
1570
- btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
1571
- btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
1572
- btcTurnoverShareChange24h: toFiniteNumber(
1573
- btcAltRegime.btcTurnoverShareChange24h
1574
- ),
1575
- altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
1576
- altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
1577
- };
1553
+ throw new TypeError("module.createRequire is not available");
1578
1554
  };
1579
- var buildCmcGlobalContextFromSignal = (signal) => {
1580
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1581
- const relative = toRecord(baseContext?.relative);
1582
- const cmcGlobal = toRecord(relative?.cmcGlobal);
1583
- if (!cmcGlobal) {
1584
- return {
1585
- source: null,
1586
- available: false,
1587
- interval: null,
1588
- asOfTs: null,
1589
- stale: null,
1590
- totalMarketCapUsd: null,
1591
- totalVolumeUsd: null,
1592
- totalVolumeReportedUsd: null,
1593
- altMarketCapUsd: null,
1594
- altVolumeUsd: null,
1595
- altVolumeReportedUsd: null,
1596
- btcDominancePct: null,
1597
- ethDominancePct: null,
1598
- btcDominanceChange24hPct: null,
1599
- ethDominanceChange24hPct: null,
1600
- altMarketCapChange24hPct: null,
1601
- altVolumeChange24hPct: null,
1602
- activeCryptocurrencies: null,
1603
- activeExchanges: null,
1604
- activeMarketPairs: null,
1605
- altLiquidityRegime: null
1606
- };
1555
+ var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
1556
+ var ensureTsNodeRegistered = async () => {
1557
+ if (tsNodeRegistered) {
1558
+ return;
1607
1559
  }
1608
- return {
1609
- source: String(cmcGlobal.source ?? ""),
1610
- available: true,
1611
- interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
1612
- asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
1613
- stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
1614
- totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
1615
- totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
1616
- totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
1617
- altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
1618
- altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
1619
- altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
1620
- btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
1621
- ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
1622
- btcDominanceChange24hPct: toFiniteNumber(
1623
- cmcGlobal.btcDominanceChange24hPct
1624
- ),
1625
- ethDominanceChange24hPct: toFiniteNumber(
1626
- cmcGlobal.ethDominanceChange24hPct
1627
- ),
1628
- altMarketCapChange24hPct: toFiniteNumber(
1629
- cmcGlobal.altMarketCapChange24hPct
1630
- ),
1631
- altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
1632
- activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
1633
- activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
1634
- activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
1635
- altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
1636
- };
1560
+ const tsNodeModule = await import("ts-node");
1561
+ const tsNode = tsNodeModule.default ?? tsNodeModule;
1562
+ tsNode.register?.({
1563
+ transpileOnly: true,
1564
+ compilerOptions: {
1565
+ module: "Node16",
1566
+ moduleResolution: "node16"
1567
+ }
1568
+ });
1569
+ tsNodeRegistered = true;
1637
1570
  };
1638
- var buildCmcReferenceAssetsContextFromSignal = (signal) => {
1639
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1640
- const relative = toRecord(baseContext?.relative);
1641
- const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
1642
- if (!cmcReferenceAssets) {
1643
- return {
1644
- source: null,
1645
- available: false,
1646
- interval: null,
1647
- asOfTs: null,
1648
- stale: null,
1649
- btcMarketCapUsd: null,
1650
- ethMarketCapUsd: null,
1651
- btcVolumeUsd: null,
1652
- ethVolumeUsd: null,
1653
- btcVolumeToMarketCap: null,
1654
- ethVolumeToMarketCap: null,
1655
- ethBtcMarketCapRatio: null,
1656
- ethBtcMarketCapRatioChange24hPct: null,
1657
- ethVsBtcVolumeRatio: null,
1658
- referenceLiquidityRegime: null
1659
- };
1571
+ var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
1572
+ const projectRoot = getTradejsProjectCwd(cwd);
1573
+ if (tsconfigPathsRegisteredByCwd.has(projectRoot)) {
1574
+ return;
1575
+ }
1576
+ const tsconfigPathsModule = await import("tsconfig-paths");
1577
+ const loadConfig = tsconfigPathsModule.loadConfig;
1578
+ const register = tsconfigPathsModule.register;
1579
+ if (typeof loadConfig !== "function" || typeof register !== "function") {
1580
+ return;
1581
+ }
1582
+ const loadedConfig = loadConfig(projectRoot);
1583
+ if (loadedConfig.resultType !== "success") {
1584
+ return;
1585
+ }
1586
+ register({
1587
+ baseUrl: loadedConfig.absoluteBaseUrl,
1588
+ paths: loadedConfig.paths,
1589
+ addMatchAll: false
1590
+ });
1591
+ tsconfigPathsRegisteredByCwd.add(projectRoot);
1592
+ };
1593
+ var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1594
+ const projectRoot = getTradejsProjectCwd(cwd);
1595
+ const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
1596
+ if (cachedMatcher) {
1597
+ const resolved2 = cachedMatcher(moduleName);
1598
+ return resolved2 || null;
1599
+ }
1600
+ const tsconfigPathsModule = await import("tsconfig-paths");
1601
+ const loadConfig = tsconfigPathsModule.loadConfig;
1602
+ const createMatchPath = tsconfigPathsModule.createMatchPath;
1603
+ if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
1604
+ return null;
1605
+ }
1606
+ const loadedConfig = loadConfig(projectRoot);
1607
+ if (loadedConfig.resultType !== "success") {
1608
+ return null;
1609
+ }
1610
+ const matchPath = createMatchPath(
1611
+ loadedConfig.absoluteBaseUrl,
1612
+ loadedConfig.paths
1613
+ );
1614
+ const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
1615
+ ".ts",
1616
+ ".tsx",
1617
+ ".mts",
1618
+ ".cts",
1619
+ ".js",
1620
+ ".jsx",
1621
+ ".mjs",
1622
+ ".cjs",
1623
+ ".json"
1624
+ ]) || "";
1625
+ tsconfigPathMatchersByCwd.set(projectRoot, matcher);
1626
+ const resolved = matcher(moduleName);
1627
+ return resolved || null;
1628
+ };
1629
+ var toImportSpecifier = (moduleName) => {
1630
+ if (moduleName.startsWith("file://")) {
1631
+ return moduleName;
1632
+ }
1633
+ if (import_path.default.isAbsolute(moduleName)) {
1634
+ return (0, import_url.pathToFileURL)(moduleName).href;
1635
+ }
1636
+ return moduleName;
1637
+ };
1638
+ var isTsModulePath = (moduleName) => TS_MODULE_RE.test(moduleName.split("?")[0]);
1639
+ var isRelativeModulePath = (moduleName) => moduleName.startsWith("./") || moduleName.startsWith("../");
1640
+ var isBareModuleSpecifier = (moduleName) => {
1641
+ const normalized = String(moduleName ?? "").trim();
1642
+ if (!normalized) {
1643
+ return false;
1644
+ }
1645
+ if (normalized.startsWith("file://") || import_path.default.isAbsolute(normalized) || isRelativeModulePath(normalized)) {
1646
+ return false;
1647
+ }
1648
+ return true;
1649
+ };
1650
+ var importConfigFile = async (configFilePath) => {
1651
+ const ext = import_path.default.extname(configFilePath).toLowerCase();
1652
+ const configFileUrl = `${toImportSpecifier(configFilePath)}?t=${Date.now()}`;
1653
+ if (ext === ".ts" || ext === ".mts") {
1654
+ const requireFn = getRequireFn(import_path.default.dirname(configFilePath));
1655
+ await ensureTsNodeRegistered();
1656
+ await ensureTsconfigPathsRegistered(import_path.default.dirname(configFilePath));
1657
+ return requireFn(configFilePath);
1658
+ }
1659
+ return import(
1660
+ /* webpackIgnore: true */
1661
+ configFileUrl
1662
+ );
1663
+ };
1664
+ var importTradejsModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1665
+ const normalized = String(moduleName ?? "").trim();
1666
+ if (!normalized) {
1667
+ return {};
1668
+ }
1669
+ let modulePath = normalized;
1670
+ if (normalized.startsWith("file://")) {
1671
+ try {
1672
+ modulePath = (0, import_url.fileURLToPath)(normalized);
1673
+ } catch {
1674
+ modulePath = normalized;
1675
+ }
1676
+ }
1677
+ const requireFn = getRequireFn(
1678
+ import_path.default.isAbsolute(modulePath) ? import_path.default.dirname(modulePath) : cwd
1679
+ );
1680
+ if (isTsModulePath(modulePath)) {
1681
+ await ensureTsNodeRegistered();
1682
+ await ensureTsconfigPathsRegistered(cwd);
1683
+ return requireFn(modulePath);
1684
+ }
1685
+ if (isBareModuleSpecifier(normalized)) {
1686
+ await ensureTsconfigPathsRegistered(cwd);
1687
+ try {
1688
+ return requireFn(normalized);
1689
+ } catch (error) {
1690
+ const resolvedByTsconfig = await resolveTsconfigPathModule(
1691
+ normalized,
1692
+ cwd
1693
+ );
1694
+ if (resolvedByTsconfig && resolvedByTsconfig !== normalized) {
1695
+ return requireFn(resolvedByTsconfig);
1696
+ }
1697
+ throw error;
1698
+ }
1699
+ }
1700
+ try {
1701
+ return await import(
1702
+ /* webpackIgnore: true */
1703
+ toImportSpecifier(normalized)
1704
+ );
1705
+ } catch (error) {
1706
+ if (isTsModulePath(modulePath)) {
1707
+ await ensureTsNodeRegistered();
1708
+ await ensureTsconfigPathsRegistered(cwd);
1709
+ return requireFn(modulePath);
1710
+ }
1711
+ throw error;
1712
+ }
1713
+ };
1714
+ var resolveExportedConfig = (moduleExports) => {
1715
+ const candidate = moduleExports && typeof moduleExports === "object" && "default" in moduleExports ? moduleExports.default : moduleExports;
1716
+ return normalizeConfig(candidate);
1717
+ };
1718
+ var findConfigFilePath = (cwd) => {
1719
+ let currentDir = import_path.default.resolve(cwd);
1720
+ while (true) {
1721
+ for (const fileName of CONFIG_FILE_NAMES) {
1722
+ const fullPath = import_path.default.join(currentDir, fileName);
1723
+ if (import_fs.default.existsSync(fullPath) && import_fs.default.statSync(fullPath).isFile()) {
1724
+ return fullPath;
1725
+ }
1726
+ }
1727
+ const parentDir = import_path.default.dirname(currentDir);
1728
+ if (parentDir === currentDir) {
1729
+ return null;
1730
+ }
1731
+ currentDir = parentDir;
1732
+ }
1733
+ };
1734
+ var resolvePluginModuleSpecifier = (moduleName, cwd = getTradejsProjectCwd()) => {
1735
+ const normalized = String(moduleName ?? "").trim();
1736
+ if (!normalized) {
1737
+ return "";
1738
+ }
1739
+ if (normalized.startsWith("file://")) {
1740
+ try {
1741
+ return (0, import_url.fileURLToPath)(normalized);
1742
+ } catch {
1743
+ return normalized;
1744
+ }
1745
+ }
1746
+ if (import_path.default.isAbsolute(normalized)) {
1747
+ return normalized;
1748
+ }
1749
+ if (isRelativeModulePath(normalized)) {
1750
+ return import_path.default.resolve(cwd, normalized);
1751
+ }
1752
+ return normalized;
1753
+ };
1754
+ var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
1755
+ const cached = cachedByCwd.get(cwd);
1756
+ if (cached) {
1757
+ return cached;
1758
+ }
1759
+ const configFilePath = findConfigFilePath(cwd);
1760
+ if (!configFilePath) {
1761
+ cachedByCwd.set(cwd, {});
1762
+ return {};
1763
+ }
1764
+ try {
1765
+ const moduleExports = await importConfigFile(configFilePath);
1766
+ const config = resolveExportedConfig(moduleExports);
1767
+ cachedByCwd.set(cwd, config);
1768
+ if (!announcedConfigFile.has(configFilePath)) {
1769
+ announcedConfigFile.add(configFilePath);
1770
+ import_logger2.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
1771
+ }
1772
+ return config;
1773
+ } catch (error) {
1774
+ import_logger2.logger.log(
1775
+ "warn",
1776
+ "Failed to load TradeJS config from %s: %s",
1777
+ configFilePath,
1778
+ String(error)
1779
+ );
1780
+ cachedByCwd.set(cwd, {});
1781
+ return {};
1782
+ }
1783
+ };
1784
+
1785
+ // src/strategy/manifests.ts
1786
+ var import_indicators = require("@tradejs/core/indicators");
1787
+ var SHARED_STRATEGY_REGISTRY_KEY = "__tradejsNodeSharedStrategyRegistryV1__";
1788
+ var sharedRegistryScope = globalThis;
1789
+ var sharedStrategyRegistry = sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] ?? (sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] = {
1790
+ registryStateByProjectRoot: /* @__PURE__ */ new Map()
1791
+ });
1792
+ var createStrategyRegistryState = () => ({
1793
+ strategyCreators: /* @__PURE__ */ new Map(),
1794
+ strategyManifestsMap: /* @__PURE__ */ new Map(),
1795
+ strategyEntriesMap: /* @__PURE__ */ new Map(),
1796
+ strategySourcesMap: /* @__PURE__ */ new Map(),
1797
+ strategyModuleResolutions: /* @__PURE__ */ new Map(),
1798
+ pluginsLoadPromise: null
1799
+ });
1800
+ var registryStateByProjectRoot = sharedStrategyRegistry.registryStateByProjectRoot;
1801
+ var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
1802
+ const projectRoot = getTradejsProjectCwd(cwd);
1803
+ let state = registryStateByProjectRoot.get(projectRoot);
1804
+ if (!state) {
1805
+ state = createStrategyRegistryState();
1806
+ registryStateByProjectRoot.set(projectRoot, state);
1660
1807
  }
1661
1808
  return {
1662
- source: String(cmcReferenceAssets.source ?? ""),
1663
- available: true,
1664
- interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
1665
- asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
1666
- stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
1667
- btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
1668
- ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
1669
- btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
1670
- ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
1671
- btcVolumeToMarketCap: toFiniteNumber(
1672
- cmcReferenceAssets.btcVolumeToMarketCap
1673
- ),
1674
- ethVolumeToMarketCap: toFiniteNumber(
1675
- cmcReferenceAssets.ethVolumeToMarketCap
1676
- ),
1677
- ethBtcMarketCapRatio: toFiniteNumber(
1678
- cmcReferenceAssets.ethBtcMarketCapRatio
1679
- ),
1680
- ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
1681
- cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
1682
- ),
1683
- ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
1684
- referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
1809
+ projectRoot,
1810
+ state
1685
1811
  };
1686
1812
  };
1687
- var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
1688
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1689
- const relative = toRecord(baseContext?.relative);
1690
- const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
1691
- if (!cmcExchangeLiquidity) {
1692
- return {
1693
- source: null,
1694
- available: false,
1695
- interval: null,
1696
- asOfTs: null,
1697
- stale: null,
1698
- exchangesCount: null,
1699
- totalVolumeUsd: null,
1700
- totalVolumeChange24hPct: null,
1701
- binanceVolumeUsd: null,
1702
- binanceVolumeShare: null,
1703
- topExchangeVolumeShare: null,
1704
- liquidityRegime: null
1705
- };
1706
- }
1813
+ var toUniqueModules = (modules = []) => [
1814
+ ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
1815
+ ];
1816
+ var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
1817
+ const config = await loadTradejsConfig(cwd);
1707
1818
  return {
1708
- source: String(cmcExchangeLiquidity.source ?? ""),
1709
- available: true,
1710
- interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
1711
- asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
1712
- stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
1713
- exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
1714
- totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
1715
- totalVolumeChange24hPct: toFiniteNumber(
1716
- cmcExchangeLiquidity.totalVolumeChange24hPct
1717
- ),
1718
- binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
1719
- binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
1720
- topExchangeVolumeShare: toFiniteNumber(
1721
- cmcExchangeLiquidity.topExchangeVolumeShare
1722
- ),
1723
- liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
1819
+ strategyModules: toUniqueModules(config.strategies),
1820
+ indicatorModules: toUniqueModules(config.indicators)
1724
1821
  };
1725
1822
  };
1726
- var buildCmcFearGreedContextFromSignal = (signal) => {
1727
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1728
- const relative = toRecord(baseContext?.relative);
1729
- const cmcFearGreed = toRecord(relative?.cmcFearGreed);
1730
- if (!cmcFearGreed) {
1731
- return {
1732
- source: null,
1733
- available: false,
1734
- interval: null,
1735
- asOfTs: null,
1736
- stale: null,
1737
- value: null,
1738
- valueChange24h: null,
1739
- valueChange7d: null,
1740
- classification: null,
1741
- sentimentRegime: null
1823
+ var extractModuleEntries = (moduleExport, key) => {
1824
+ if (!moduleExport || typeof moduleExport !== "object") {
1825
+ return null;
1826
+ }
1827
+ const candidate = moduleExport;
1828
+ if (Array.isArray(candidate[key])) {
1829
+ return candidate[key];
1830
+ }
1831
+ const defaultExport = candidate.default;
1832
+ if (defaultExport && Array.isArray(defaultExport[key])) {
1833
+ return defaultExport[key];
1834
+ }
1835
+ return null;
1836
+ };
1837
+ var extractStrategyPluginDefinition = (moduleExport) => {
1838
+ const strategyEntries = extractModuleEntries(
1839
+ moduleExport,
1840
+ "strategyEntries"
1841
+ );
1842
+ return strategyEntries ? { strategyEntries } : null;
1843
+ };
1844
+ var extractIndicatorPluginDefinition = (moduleExport) => {
1845
+ const indicatorEntries = extractModuleEntries(
1846
+ moduleExport,
1847
+ "indicatorEntries"
1848
+ );
1849
+ return indicatorEntries ? { indicatorEntries } : null;
1850
+ };
1851
+ var validateStrategyEntries = (moduleName, entries, state) => {
1852
+ const issues = [];
1853
+ const names = /* @__PURE__ */ new Set();
1854
+ entries.forEach((entry, index) => {
1855
+ const entryPath = `${moduleName}.strategyEntries[${index}]`;
1856
+ const strategyName = entry?.manifest?.name;
1857
+ if (typeof strategyName !== "string" || !strategyName.trim()) {
1858
+ issues.push(`${entryPath}: manifest.name is required`);
1859
+ } else if (names.has(strategyName) || state?.strategyEntriesMap.has(strategyName)) {
1860
+ issues.push(`${entryPath}: duplicate strategy ${strategyName}`);
1861
+ } else {
1862
+ names.add(strategyName);
1863
+ }
1864
+ if (!entry || typeof entry !== "object" || !entry.defaults || typeof entry.defaults !== "object" || Array.isArray(entry.defaults)) {
1865
+ issues.push(`${entryPath}: defaults must be an object`);
1866
+ }
1867
+ if (typeof entry?.parseConfig !== "function") {
1868
+ issues.push(`${entryPath}: parseConfig is required`);
1869
+ }
1870
+ if (typeof entry?.createCore !== "function") {
1871
+ issues.push(`${entryPath}: createCore is required`);
1872
+ }
1873
+ });
1874
+ return issues;
1875
+ };
1876
+ var registerEntries = (entries, source, state) => {
1877
+ for (const entry of entries) {
1878
+ const strategyName = entry.manifest.name;
1879
+ state.strategyManifestsMap.set(strategyName, entry.manifest);
1880
+ state.strategyEntriesMap.set(strategyName, entry);
1881
+ state.strategySourcesMap.set(strategyName, source);
1882
+ materializeStrategyCreator(strategyName, state);
1883
+ }
1884
+ };
1885
+ var materializeStrategyCreator = (strategyName, state) => {
1886
+ if (state.strategyCreators.has(strategyName) || !sharedStrategyRegistry.strategyRuntimeFactory) {
1887
+ return;
1888
+ }
1889
+ const entry = state.strategyEntriesMap.get(strategyName);
1890
+ if (!entry) return;
1891
+ state.strategyCreators.set(
1892
+ strategyName,
1893
+ sharedStrategyRegistry.strategyRuntimeFactory({
1894
+ strategyName,
1895
+ defaults: entry.defaults,
1896
+ createCore: entry.createCore,
1897
+ manifest: entry.manifest,
1898
+ detectorKey: entry.detectorKey,
1899
+ detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
1900
+ resolveRegisteredManifest: (name) => state.strategyManifestsMap.get(name)
1901
+ })
1902
+ );
1903
+ };
1904
+ var setStrategyRuntimeFactory = (factory) => {
1905
+ sharedStrategyRegistry.strategyRuntimeFactory = factory;
1906
+ for (const state of registryStateByProjectRoot.values()) {
1907
+ for (const strategyName of state.strategyEntriesMap.keys()) {
1908
+ materializeStrategyCreator(strategyName, state);
1909
+ }
1910
+ }
1911
+ };
1912
+ var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1913
+ if (typeof importTradejsModule === "function") {
1914
+ return importTradejsModule(moduleName, cwd);
1915
+ }
1916
+ return import(
1917
+ /* webpackIgnore: true */
1918
+ moduleName
1919
+ );
1920
+ };
1921
+ var resolveStrategyModuleEntry = async ({
1922
+ strategyName,
1923
+ moduleName,
1924
+ cwd
1925
+ }) => {
1926
+ const { projectRoot, state } = getStrategyRegistryState(cwd);
1927
+ const source = moduleName.trim();
1928
+ const resolvedModuleName = resolvePluginModuleSpecifier(source, projectRoot);
1929
+ const cacheKey = `${resolvedModuleName}\0${strategyName}`;
1930
+ const cached = state.strategyModuleResolutions.get(cacheKey);
1931
+ if (cached) return cached;
1932
+ const resolution = (async () => {
1933
+ const moduleExport = await importStrategyPluginModule(
1934
+ resolvedModuleName,
1935
+ projectRoot
1936
+ );
1937
+ const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
1938
+ if (!pluginDefinition) {
1939
+ throw new Error(`${source}: export { strategyEntries } is missing`);
1940
+ }
1941
+ const issues = validateStrategyEntries(
1942
+ source,
1943
+ pluginDefinition.strategyEntries
1944
+ );
1945
+ if (issues.length > 0) {
1946
+ throw new Error(
1947
+ ["Invalid TradeJS strategy module:", ...issues].join("\n")
1948
+ );
1949
+ }
1950
+ const entry = pluginDefinition.strategyEntries.find(
1951
+ (candidate) => candidate.manifest.name === strategyName
1952
+ );
1953
+ if (!entry) {
1954
+ throw new Error(`${source}: strategy ${strategyName} is missing`);
1955
+ }
1956
+ const factory = sharedStrategyRegistry.strategyRuntimeFactory;
1957
+ if (!factory) {
1958
+ throw new Error("Strategy runtime factory is not configured");
1959
+ }
1960
+ const manifestsByName = new Map(
1961
+ pluginDefinition.strategyEntries.map(
1962
+ (candidate) => [candidate.manifest.name, candidate.manifest]
1963
+ )
1964
+ );
1965
+ return {
1966
+ entry,
1967
+ creator: factory({
1968
+ strategyName,
1969
+ defaults: entry.defaults,
1970
+ createCore: entry.createCore,
1971
+ manifest: entry.manifest,
1972
+ detectorKey: entry.detectorKey,
1973
+ detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
1974
+ resolveRegisteredManifest: (name) => manifestsByName.get(name) ?? state.strategyManifestsMap.get(name)
1975
+ }),
1976
+ source
1742
1977
  };
1978
+ })();
1979
+ state.strategyModuleResolutions.set(cacheKey, resolution);
1980
+ try {
1981
+ return await resolution;
1982
+ } catch (error) {
1983
+ state.strategyModuleResolutions.delete(cacheKey);
1984
+ throw error;
1743
1985
  }
1744
- return {
1745
- source: String(cmcFearGreed.source ?? ""),
1746
- available: true,
1747
- interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
1748
- asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
1749
- stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
1750
- value: toFiniteNumber(cmcFearGreed.value),
1751
- valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
1752
- valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
1753
- classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
1754
- sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
1755
- };
1756
1986
  };
1757
- var buildCmcIndexesContextFromSignal = (signal) => {
1758
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1759
- const relative = toRecord(baseContext?.relative);
1760
- const cmcIndexes = toRecord(relative?.cmcIndexes);
1761
- if (!cmcIndexes) {
1762
- return {
1763
- source: null,
1764
- available: false,
1765
- interval: null,
1766
- asOfTs: null,
1767
- stale: null,
1768
- cmc100Value: null,
1769
- cmc100Change24hPct: null,
1770
- cmc100TopConstituentSymbol: null,
1771
- cmc100TopConstituentWeightPct: null,
1772
- cmc20Value: null,
1773
- cmc20Change24hPct: null,
1774
- cmc20TopConstituentSymbol: null,
1775
- cmc20TopConstituentWeightPct: null,
1776
- cmc20ToCmc100Ratio: null,
1777
- cmc20ToCmc100RatioChange24hPct: null,
1778
- indexRegime: null
1779
- };
1987
+ var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
1988
+ const { projectRoot, state } = getStrategyRegistryState(cwd);
1989
+ if (!state.pluginsLoadPromise) {
1990
+ (0, import_indicators.resetIndicatorRegistryCache)(projectRoot);
1991
+ state.pluginsLoadPromise = (async () => {
1992
+ const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
1993
+ const strategySet = new Set(strategyModules);
1994
+ const indicatorSet = new Set(indicatorModules);
1995
+ const pluginModuleNames = [
1996
+ .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
1997
+ ];
1998
+ if (!pluginModuleNames.length) {
1999
+ return;
2000
+ }
2001
+ const issues = [];
2002
+ for (const moduleName of pluginModuleNames) {
2003
+ try {
2004
+ const resolvedModuleName = resolvePluginModuleSpecifier(
2005
+ moduleName,
2006
+ projectRoot
2007
+ );
2008
+ const moduleExport = await importStrategyPluginModule(
2009
+ resolvedModuleName,
2010
+ projectRoot
2011
+ );
2012
+ if (strategySet.has(moduleName)) {
2013
+ const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
2014
+ if (!pluginDefinition) {
2015
+ issues.push(
2016
+ `${moduleName}: export { strategyEntries } is missing`
2017
+ );
2018
+ } else {
2019
+ const entryIssues = validateStrategyEntries(
2020
+ moduleName,
2021
+ pluginDefinition.strategyEntries,
2022
+ state
2023
+ );
2024
+ issues.push(...entryIssues);
2025
+ if (entryIssues.length === 0) {
2026
+ registerEntries(
2027
+ pluginDefinition.strategyEntries,
2028
+ moduleName,
2029
+ state
2030
+ );
2031
+ }
2032
+ }
2033
+ }
2034
+ if (indicatorSet.has(moduleName)) {
2035
+ const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
2036
+ if (!indicatorPluginDefinition) {
2037
+ issues.push(
2038
+ `${moduleName}: export { indicatorEntries } is missing`
2039
+ );
2040
+ } else {
2041
+ (0, import_indicators.registerIndicatorEntries)(
2042
+ indicatorPluginDefinition.indicatorEntries,
2043
+ moduleName,
2044
+ projectRoot
2045
+ );
2046
+ }
2047
+ }
2048
+ if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
2049
+ issues.push(`${moduleName}: plugin is not declared in config`);
2050
+ }
2051
+ } catch (error) {
2052
+ issues.push(`${moduleName}: failed to import: ${String(error)}`);
2053
+ }
2054
+ }
2055
+ if (issues.length > 0) {
2056
+ throw new Error(
2057
+ ["Invalid TradeJS plugin catalog:", ...issues].join("\n")
2058
+ );
2059
+ }
2060
+ })();
1780
2061
  }
1781
- return {
1782
- source: String(cmcIndexes.source ?? ""),
1783
- available: true,
1784
- interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
1785
- asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
1786
- stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
1787
- cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
1788
- cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
1789
- cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
1790
- cmc100TopConstituentWeightPct: toFiniteNumber(
1791
- cmcIndexes.cmc100TopConstituentWeightPct
1792
- ),
1793
- cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
1794
- cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
1795
- cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
1796
- cmc20TopConstituentWeightPct: toFiniteNumber(
1797
- cmcIndexes.cmc20TopConstituentWeightPct
1798
- ),
1799
- cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
1800
- cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
1801
- cmcIndexes.cmc20ToCmc100RatioChange24hPct
1802
- ),
1803
- indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
1804
- };
2062
+ await state.pluginsLoadPromise;
1805
2063
  };
1806
- var buildReferenceTradeFlowContextFromSignal = (signal) => {
1807
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1808
- const relative = toRecord(baseContext?.relative);
1809
- const refs = toRecord(relative?.referenceTradeFlow);
1810
- const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
1811
- const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
1812
- const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
1813
- if (!refs) {
1814
- return {
1815
- source: null,
1816
- available: false,
1817
- primaryReferenceSymbol: null,
1818
- referenceSymbols: [],
1819
- primaryTradeFlowBuyPressurePct: null,
1820
- primaryTradeFlowStale: null
1821
- };
2064
+ var getStrategyCreator = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2065
+ if (moduleName?.trim()) {
2066
+ await ensureStrategyPluginsLoaded(cwd);
2067
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).creator;
1822
2068
  }
1823
- return {
1824
- source: String(refs.source ?? ""),
1825
- available: true,
1826
- primaryReferenceSymbol,
1827
- referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
1828
- primaryTradeFlowBuyPressurePct: toFiniteNumber(
1829
- primaryTradeFlow?.buyPressurePct
1830
- ),
1831
- primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
1832
- };
2069
+ await ensureStrategyPluginsLoaded(cwd);
2070
+ const { state } = getStrategyRegistryState(cwd);
2071
+ return state.strategyCreators.get(name);
1833
2072
  };
1834
- var buildAiMarketContext = (signal) => ({
1835
- execution: {
1836
- binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
1837
- },
1838
- participation: {
1839
- trueDelta: buildTrueDeltaContextFromSignal(signal),
1840
- tradeFlow: buildTradeFlowContextFromSignal(signal)
1841
- },
1842
- relative: {
1843
- marketBreadth: buildMarketBreadthContextFromSignal(signal),
1844
- marketBreadths: buildMarketBreadthsContextFromSignal(signal),
1845
- targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
1846
- btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
1847
- cmcGlobal: buildCmcGlobalContextFromSignal(signal),
1848
- cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
1849
- cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
1850
- cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
1851
- cmcIndexes: buildCmcIndexesContextFromSignal(signal),
1852
- referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
2073
+ var getStrategyEntry = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2074
+ if (moduleName?.trim()) {
2075
+ await ensureStrategyPluginsLoaded(cwd);
2076
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).entry;
1853
2077
  }
1854
- });
1855
-
1856
- // src/strategy/manifests.ts
1857
- var import_indicators = require("@tradejs/core/indicators");
1858
-
1859
- // src/tradejsConfig.ts
1860
- var import_fs = __toESM(require("fs"));
1861
- var import_path = __toESM(require("path"));
1862
- var import_url = require("url");
1863
- var import_config = require("@tradejs/core/config");
1864
- var import_logger2 = require("@tradejs/infra/logger");
1865
- var CONFIG_FILE_NAMES = [
1866
- "tradejs.config.ts",
1867
- "tradejs.config.mts",
1868
- "tradejs.config.js",
1869
- "tradejs.config.mjs",
1870
- "tradejs.config.cjs"
1871
- ];
1872
- var TS_MODULE_RE = /\.(cts|mts|ts)$/i;
1873
- var cachedByCwd = /* @__PURE__ */ new Map();
1874
- var announcedConfigFile = /* @__PURE__ */ new Set();
1875
- var tsNodeRegistered = false;
1876
- var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
1877
- var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
1878
- var getTradejsProjectCwd = (cwd) => {
1879
- const explicit = String(cwd ?? "").trim();
1880
- if (explicit) {
1881
- return import_path.default.resolve(explicit);
2078
+ await ensureStrategyPluginsLoaded(cwd);
2079
+ const { state } = getStrategyRegistryState(cwd);
2080
+ return state.strategyEntriesMap.get(name);
2081
+ };
2082
+ var getStrategyPluginSource = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2083
+ if (moduleName?.trim()) {
2084
+ await ensureStrategyPluginsLoaded(cwd);
2085
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).source;
1882
2086
  }
1883
- const fromEnv = String(process.env.PROJECT_CWD || "").trim();
1884
- if (fromEnv) {
1885
- return import_path.default.resolve(fromEnv);
2087
+ await ensureStrategyPluginsLoaded(cwd);
2088
+ const { state } = getStrategyRegistryState(cwd);
2089
+ return state.strategySourcesMap.get(name);
2090
+ };
2091
+ var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
2092
+ if (!name) {
2093
+ return void 0;
1886
2094
  }
1887
- return process.cwd();
2095
+ const { state } = getStrategyRegistryState(cwd);
2096
+ return state.strategyManifestsMap.get(name);
1888
2097
  };
1889
- var normalizeConfig = (rawConfig) => {
1890
- if (!rawConfig || typeof rawConfig !== "object") {
1891
- return {};
2098
+ var strategies = new Proxy(
2099
+ {},
2100
+ {
2101
+ get: (_target, property) => {
2102
+ if (typeof property !== "string") {
2103
+ return void 0;
2104
+ }
2105
+ return getStrategyRegistryState().state.strategyCreators.get(property);
2106
+ },
2107
+ ownKeys: () => {
2108
+ return [...getStrategyRegistryState().state.strategyCreators.keys()];
2109
+ },
2110
+ getOwnPropertyDescriptor: () => ({
2111
+ enumerable: true,
2112
+ configurable: true
2113
+ })
1892
2114
  }
1893
- const config = rawConfig;
1894
- const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
1895
- const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
1896
- const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
1897
- const hooks = (0, import_config.normalizeTradejsConfigHooks)(
1898
- config.hooks
1899
- );
1900
- const runtime = config.runtime && typeof config.runtime === "object" && !Array.isArray(config.runtime) ? config.runtime : void 0;
1901
- return {
1902
- strategies: strategies2,
1903
- indicators,
1904
- connectors,
1905
- ...hooks ? { hooks } : {},
1906
- ...runtime ? { runtime } : {}
1907
- };
2115
+ );
2116
+
2117
+ // src/strategyHelpers/tickerProfiles.ts
2118
+ var snapshots = /* @__PURE__ */ new Map();
2119
+ var resolveTickerProfileSource = (strategy) => {
2120
+ const declared = getStrategyManifest(strategy)?.contextRequirements?.tickerProfiles;
2121
+ if (declared) return declared;
2122
+ const provider = process.env.TICKER_PROFILES_PROVIDER?.trim();
2123
+ const universeId = process.env.TICKER_PROFILES_UNIVERSE?.trim();
2124
+ if (!provider && !universeId) return void 0;
2125
+ if (!provider || !universeId)
2126
+ throw new Error(
2127
+ "Set both TICKER_PROFILES_PROVIDER and TICKER_PROFILES_UNIVERSE"
2128
+ );
2129
+ return { provider, universeId };
1908
2130
  };
1909
- var getNodeCreateRequire = () => {
1910
- const builtinModule = process.getBuiltinModule?.("module");
1911
- if (typeof builtinModule?.createRequire === "function") {
1912
- return builtinModule.createRequire;
2131
+ var loadTickerProfileContext = async ({
2132
+ projectRoot,
2133
+ provider,
2134
+ universeId,
2135
+ symbol,
2136
+ timestamp
2137
+ }) => {
2138
+ const asOf = (0, import_tickerProfiles2.tickerProfileMonth)(timestamp), key = JSON.stringify([projectRoot, provider, universeId, asOf]);
2139
+ let cached = snapshots.get(key);
2140
+ if (!cached || cached.expires < Date.now()) {
2141
+ const value = (0, import_tickerProfiles.readTickerProfiles)(projectRoot, provider, universeId, asOf).then((envelope) => {
2142
+ if (!envelope) return null;
2143
+ const s = envelope.snapshot;
2144
+ if (!(0, import_tickerProfiles2.isTickerProfileSnapshot)(s) || s.asOf !== asOf || s.provider !== provider || s.universeId !== universeId)
2145
+ throw new Error("Ticker profile identity/schema mismatch");
2146
+ const entry = snapshots.get(key);
2147
+ if (entry) entry.expires = Infinity;
2148
+ return new Map(
2149
+ s.profiles.map((p) => [
2150
+ p.symbol,
2151
+ Object.freeze({
2152
+ ...p,
2153
+ definition: s.definition,
2154
+ provider,
2155
+ universeId,
2156
+ snapshotId: envelope.snapshotId
2157
+ })
2158
+ ])
2159
+ );
2160
+ }).catch((error) => {
2161
+ snapshots.delete(key);
2162
+ throw error;
2163
+ });
2164
+ cached = { expires: Date.now() + 6e4, value };
2165
+ snapshots.set(key, cached);
2166
+ if (snapshots.size > 24) {
2167
+ const oldest = snapshots.keys().next().value;
2168
+ if (oldest) snapshots.delete(oldest);
2169
+ }
1913
2170
  }
1914
- throw new TypeError("module.createRequire is not available");
2171
+ return (await cached.value)?.get(symbol);
1915
2172
  };
1916
- var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
1917
- var ensureTsNodeRegistered = async () => {
1918
- if (tsNodeRegistered) {
2173
+ var withTickerProfile = (baseContext, profile) => Object.defineProperty(
2174
+ Object.create(
2175
+ Object.getPrototypeOf(baseContext),
2176
+ Object.getOwnPropertyDescriptors(baseContext)
2177
+ ),
2178
+ "tickerProfile",
2179
+ { enumerable: true, configurable: true, value: profile }
2180
+ );
2181
+ var enrichSignalWithTickerProfile = async (signal) => {
2182
+ const source = resolveTickerProfileSource(signal.strategy);
2183
+ if (!source || signal.universe === "tradfi" || !signal.symbol.endsWith("USDT"))
1919
2184
  return;
1920
- }
1921
- const tsNodeModule = await import("ts-node");
1922
- const tsNode = tsNodeModule.default ?? tsNodeModule;
1923
- tsNode.register?.({
1924
- transpileOnly: true,
1925
- compilerOptions: {
1926
- module: "Node16",
1927
- moduleResolution: "node16"
1928
- }
2185
+ const profile = await loadTickerProfileContext({
2186
+ ...source,
2187
+ projectRoot: getTradejsProjectCwd(),
2188
+ symbol: signal.symbol,
2189
+ timestamp: signal.timestamp
1929
2190
  });
1930
- tsNodeRegistered = true;
2191
+ if (profile) {
2192
+ signal.additionalIndicators = {
2193
+ ...signal.additionalIndicators,
2194
+ tickerProfile: profile
2195
+ };
2196
+ }
1931
2197
  };
1932
- var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
1933
- const projectRoot = getTradejsProjectCwd(cwd);
1934
- if (tsconfigPathsRegisteredByCwd.has(projectRoot)) {
1935
- return;
2198
+
2199
+ // src/strategyRuntime.ts
2200
+ var import_jev6 = require("@tradejs/core/jev");
2201
+
2202
+ // src/jev.ts
2203
+ var import_promises = __toESM(require("fs/promises"));
2204
+ var import_node_path = __toESM(require("path"));
2205
+ var import_jev4 = require("@tradejs/core/jev");
2206
+ var import_jev5 = require("@tradejs/infra/jev");
2207
+ var import_userSettings = require("@tradejs/infra/userSettings");
2208
+ init_jevInput();
2209
+ init_jevInput();
2210
+ init_jevTraining();
2211
+ var resolveJevProvider = async (userName) => {
2212
+ const settings = await (0, import_userSettings.getUserSettings)(userName);
2213
+ const provider = {
2214
+ endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
2215
+ model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
2216
+ };
2217
+ (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
2218
+ if (!settings.JEV_API_KEY)
2219
+ throw new Error("Configure the Jev API key in Account settings");
2220
+ return provider;
2221
+ };
2222
+ var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
2223
+ var pending = /* @__PURE__ */ new Map();
2224
+ var pendingResponses = /* @__PURE__ */ new Map();
2225
+ var evaluateJevInput = async ({
2226
+ input,
2227
+ config,
2228
+ userName,
2229
+ projectRoot
2230
+ }) => {
2231
+ config = (0, import_jev4.parseJevConfig)(config);
2232
+ const inputHash = (0, import_jev5.jevHash)(input);
2233
+ const questions = questionsForJevInput(input);
2234
+ const questionsHash = (0, import_jev5.jevHash)(questions);
2235
+ const base = {
2236
+ questionSet: import_jev4.JEV_QUESTION_SET,
2237
+ schema: "tradejs-signal-assessment/v5",
2238
+ source: config.source,
2239
+ mode: config.mode,
2240
+ inputHash,
2241
+ questionsHash,
2242
+ geometryStatus: input.geometryStatus,
2243
+ levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
2244
+ };
2245
+ if (!input.questions.length)
2246
+ return {
2247
+ assessment: {
2248
+ ...base,
2249
+ status: "unavailable",
2250
+ model: "",
2251
+ scores: emptyScores(),
2252
+ confidence: emptyScores()
2253
+ }
2254
+ };
2255
+ if (config.source === "local") {
2256
+ const contents = await import_promises.default.readFile(
2257
+ import_node_path.default.resolve(projectRoot, config.modelFile),
2258
+ "utf8"
2259
+ );
2260
+ if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
2261
+ throw new Error("Local Jev gate checksum mismatch");
2262
+ const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
2263
+ const model = validateJevGateModel2(JSON.parse(contents));
2264
+ if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
2265
+ throw new Error("Local Jev gate strategy or question version mismatch");
2266
+ const scores = Object.fromEntries(
2267
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2268
+ key,
2269
+ input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
2270
+ ])
2271
+ );
2272
+ return {
2273
+ assessment: {
2274
+ ...base,
2275
+ status: "available",
2276
+ model: config.modelSha256,
2277
+ scores,
2278
+ confidence: emptyScores()
2279
+ }
2280
+ };
1936
2281
  }
1937
- const tsconfigPathsModule = await import("tsconfig-paths");
1938
- const loadConfig = tsconfigPathsModule.loadConfig;
1939
- const register = tsconfigPathsModule.register;
1940
- if (typeof loadConfig !== "function" || typeof register !== "function") {
1941
- return;
2282
+ const provider = config.provider ?? await resolveJevProvider(userName);
2283
+ const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
2284
+ const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
2285
+ const file = import_node_path.default.join(dir, "records", `${id}.json`);
2286
+ const getRecord = async () => {
2287
+ const existing = await (0, import_jev5.readJevArtifact)(file);
2288
+ if (existing) {
2289
+ 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))
2290
+ throw new Error("Jev recording identity mismatch");
2291
+ (0, import_jev5.validateJevResponse)(existing.response, input.questions);
2292
+ for (const key of import_jev4.JEV_DIMENSIONS) {
2293
+ const expected = input.questions.includes(key) ? existing.response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null;
2294
+ if (existing.scores[key] !== expected)
2295
+ throw new Error("Jev recording scores do not match the response");
2296
+ }
2297
+ return existing;
2298
+ }
2299
+ if (config.source === "recorded")
2300
+ throw new Error(
2301
+ `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.`
2302
+ );
2303
+ const settings = await (0, import_userSettings.getUserSettings)(userName);
2304
+ if (!settings.JEV_API_KEY)
2305
+ throw new Error("Configure the Jev API key in Account settings");
2306
+ const apiKey = settings.JEV_API_KEY;
2307
+ const configuredProvider = await resolveJevProvider(userName);
2308
+ if (configuredProvider.endpoint !== provider.endpoint)
2309
+ throw new Error(
2310
+ "Jev account endpoint changed; update the frozen provider configuration"
2311
+ );
2312
+ const started = Date.now();
2313
+ const state = stateForJevInput(input);
2314
+ const responseId = (0, import_jev5.jevHash)({
2315
+ state,
2316
+ questionsHash,
2317
+ provider,
2318
+ user: (0, import_jev5.jevHash)(userName)
2319
+ });
2320
+ const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
2321
+ let responsePromise = pendingResponses.get(responseFile);
2322
+ if (!responsePromise) {
2323
+ responsePromise = (async () => {
2324
+ const saved = await (0, import_jev5.readJevArtifact)(responseFile);
2325
+ if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
2326
+ const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
2327
+ await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
2328
+ return fresh;
2329
+ })();
2330
+ pendingResponses.set(responseFile, responsePromise);
2331
+ }
2332
+ let response;
2333
+ try {
2334
+ response = await responsePromise;
2335
+ } finally {
2336
+ pendingResponses.delete(responseFile);
2337
+ }
2338
+ const scores = Object.fromEntries(
2339
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2340
+ key,
2341
+ input.questions.includes(key) ? response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null
2342
+ ])
2343
+ );
2344
+ const value = {
2345
+ schema: "tradejs-jev-record/v4",
2346
+ id,
2347
+ inputHash,
2348
+ questionsHash,
2349
+ provider,
2350
+ input,
2351
+ response,
2352
+ scores,
2353
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2354
+ elapsedMs: Date.now() - started
2355
+ };
2356
+ await (0, import_jev5.writeJevArtifact)(file, value);
2357
+ return await (0, import_jev5.readJevArtifact)(file);
2358
+ };
2359
+ const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
2360
+ let promise = pending.get(pendingKey);
2361
+ if (!promise) {
2362
+ promise = getRecord();
2363
+ pending.set(pendingKey, promise);
1942
2364
  }
1943
- const loadedConfig = loadConfig(projectRoot);
1944
- if (loadedConfig.resultType !== "success") {
1945
- return;
2365
+ let record2;
2366
+ try {
2367
+ record2 = await promise;
2368
+ } finally {
2369
+ pending.delete(pendingKey);
1946
2370
  }
1947
- register({
1948
- baseUrl: loadedConfig.absoluteBaseUrl,
1949
- paths: loadedConfig.paths,
1950
- addMatchAll: false
1951
- });
1952
- tsconfigPathsRegisteredByCwd.add(projectRoot);
2371
+ return {
2372
+ record: record2,
2373
+ assessment: {
2374
+ ...base,
2375
+ status: "available",
2376
+ recordId: id,
2377
+ model: record2.response.model,
2378
+ scores: record2.scores,
2379
+ confidence: Object.fromEntries(
2380
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2381
+ key,
2382
+ record2.response.answers[key]?.confidence ?? null
2383
+ ])
2384
+ )
2385
+ }
2386
+ };
1953
2387
  };
1954
- var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1955
- const projectRoot = getTradejsProjectCwd(cwd);
1956
- const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
1957
- if (cachedMatcher) {
1958
- const resolved2 = cachedMatcher(moduleName);
1959
- return resolved2 || null;
1960
- }
1961
- const tsconfigPathsModule = await import("tsconfig-paths");
1962
- const loadConfig = tsconfigPathsModule.loadConfig;
1963
- const createMatchPath = tsconfigPathsModule.createMatchPath;
1964
- if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
1965
- return null;
1966
- }
1967
- const loadedConfig = loadConfig(projectRoot);
1968
- if (loadedConfig.resultType !== "success") {
1969
- return null;
2388
+ var assessSignalWithJev = async ({
2389
+ signal,
2390
+ config,
2391
+ userName,
2392
+ projectRoot,
2393
+ strict,
2394
+ scope = "runtime"
2395
+ }) => {
2396
+ let assessment;
2397
+ try {
2398
+ const input = buildJevInput(signal);
2399
+ ({ assessment } = await evaluateJevInput({
2400
+ input,
2401
+ config,
2402
+ userName,
2403
+ projectRoot
2404
+ }));
2405
+ } catch (error) {
2406
+ if (strict) throw error;
2407
+ assessment = {
2408
+ questionSet: import_jev4.JEV_QUESTION_SET,
2409
+ schema: "tradejs-signal-assessment/v5",
2410
+ source: config.source,
2411
+ mode: config.mode,
2412
+ status: "unavailable",
2413
+ inputHash: "",
2414
+ questionsHash: "",
2415
+ geometryStatus: "absent",
2416
+ levelsValid: null,
2417
+ model: config.provider?.model ?? config.modelSha256 ?? "",
2418
+ scores: emptyScores(),
2419
+ confidence: emptyScores()
2420
+ };
1970
2421
  }
1971
- const matchPath = createMatchPath(
1972
- loadedConfig.absoluteBaseUrl,
1973
- loadedConfig.paths
2422
+ signal.assessment = assessment;
2423
+ signal.additionalIndicators = {
2424
+ ...signal.additionalIndicators,
2425
+ jev: (0, import_jev4.jevAssessmentFeatures)(assessment)
2426
+ };
2427
+ const identity = {
2428
+ signalId: signal.signalId,
2429
+ strategy: signal.strategy,
2430
+ symbol: signal.symbol,
2431
+ timestamp: signal.timestamp,
2432
+ scope,
2433
+ user: (0, import_jev5.jevHash)(userName),
2434
+ config,
2435
+ assessment
2436
+ };
2437
+ await (0, import_jev5.writeJevArtifact)(
2438
+ import_node_path.default.resolve(
2439
+ projectRoot,
2440
+ config.recordsDir ?? "data/ai/jev",
2441
+ "decisions",
2442
+ `${(0, import_jev5.jevHash)(identity)}.json`
2443
+ ),
2444
+ identity
1974
2445
  );
1975
- const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
1976
- ".ts",
1977
- ".tsx",
1978
- ".mts",
1979
- ".cts",
1980
- ".js",
1981
- ".jsx",
1982
- ".mjs",
1983
- ".cjs",
1984
- ".json"
1985
- ]) || "";
1986
- tsconfigPathMatchersByCwd.set(projectRoot, matcher);
1987
- const resolved = matcher(moduleName);
1988
- return resolved || null;
2446
+ return assessment;
1989
2447
  };
1990
- var toImportSpecifier = (moduleName) => {
1991
- if (moduleName.startsWith("file://")) {
1992
- return moduleName;
1993
- }
1994
- if (import_path.default.isAbsolute(moduleName)) {
1995
- return (0, import_url.pathToFileURL)(moduleName).href;
2448
+
2449
+ // src/strategyRuntime.ts
2450
+ var import_constants5 = require("@tradejs/core/constants");
2451
+ var import_strategies6 = require("@tradejs/core/strategies");
2452
+ var import_logger11 = require("@tradejs/infra/logger");
2453
+
2454
+ // src/strategyHelpers/runtime.ts
2455
+ var import_logger9 = require("@tradejs/infra/logger");
2456
+ var import_constants3 = require("@tradejs/core/constants");
2457
+ var import_ml2 = require("@tradejs/infra/ml");
2458
+
2459
+ // src/strategy/entrySubmissionValidation.ts
2460
+ var import_logger3 = require("@tradejs/infra/logger");
2461
+
2462
+ // src/strategy/entryExecutionValidation.ts
2463
+ var EntrySubmissionValidationError = class extends Error {
2464
+ constructor() {
2465
+ super(...arguments);
2466
+ this.name = "EntrySubmissionValidationError";
1996
2467
  }
1997
- return moduleName;
1998
2468
  };
1999
- var isTsModulePath = (moduleName) => TS_MODULE_RE.test(moduleName.split("?")[0]);
2000
- var isRelativeModulePath = (moduleName) => moduleName.startsWith("./") || moduleName.startsWith("../");
2001
- var isBareModuleSpecifier = (moduleName) => {
2002
- const normalized = String(moduleName ?? "").trim();
2003
- if (!normalized) {
2004
- return false;
2005
- }
2006
- if (normalized.startsWith("file://") || import_path.default.isAbsolute(normalized) || isRelativeModulePath(normalized)) {
2007
- return false;
2008
- }
2009
- return true;
2469
+ var resolveEntryExecutionLimits = (config, direction) => {
2470
+ const side = config[direction];
2471
+ const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
2472
+ const maximum = config.MAX_LOSS_VALUE;
2473
+ if (minimum == null && maximum == null) return void 0;
2474
+ const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
2475
+ const maxLossValue = maximum == null ? void 0 : Number(maximum);
2476
+ const feeRate = Number(config.RISK_FEE_RATE ?? 0);
2477
+ const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
2478
+ const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
2479
+ const slippageBps = slippage + impact;
2480
+ 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)
2481
+ throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
2482
+ return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
2010
2483
  };
2011
- var importConfigFile = async (configFilePath) => {
2012
- const ext = import_path.default.extname(configFilePath).toLowerCase();
2013
- const configFileUrl = `${toImportSpecifier(configFilePath)}?t=${Date.now()}`;
2014
- if (ext === ".ts" || ext === ".mts") {
2015
- const requireFn = getRequireFn(import_path.default.dirname(configFilePath));
2016
- await ensureTsNodeRegistered();
2017
- await ensureTsconfigPathsRegistered(import_path.default.dirname(configFilePath));
2018
- return requireFn(configFilePath);
2484
+ var validateEntryExecution = ({
2485
+ direction,
2486
+ price,
2487
+ qty,
2488
+ stopLossPrice,
2489
+ takeProfitPrice,
2490
+ limits
2491
+ }) => {
2492
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
2493
+ throw new EntrySubmissionValidationError(
2494
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2495
+ );
2496
+ const side = direction === "LONG" ? 1 : -1;
2497
+ if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
2498
+ throw new EntrySubmissionValidationError(
2499
+ "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
2500
+ );
2501
+ const slippage = limits.slippageBps / 1e4;
2502
+ const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
2503
+ const entryCost = cost(price, side);
2504
+ const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
2505
+ const rewardPerUnit = Math.max(
2506
+ 0,
2507
+ Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
2508
+ );
2509
+ const netRiskRatio = rewardPerUnit / lossPerUnit;
2510
+ const plannedLossValue = qty * lossPerUnit;
2511
+ if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
2512
+ throw new EntrySubmissionValidationError(
2513
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2514
+ );
2515
+ const belowMinimumNetRiskRatio = limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio;
2516
+ const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
2517
+ return {
2518
+ netRiskRatio,
2519
+ plannedLossValue,
2520
+ riskExceeded,
2521
+ belowMinimumNetRiskRatio
2522
+ };
2523
+ };
2524
+
2525
+ // src/strategy/entrySubmissionValidation.ts
2526
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
2527
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
2528
+ var getOrderArrivalSnapshot = async ({
2529
+ connector,
2530
+ symbol,
2531
+ requireTimestamp = false
2532
+ }) => {
2533
+ if (typeof connector.getTopOfBookTicker !== "function") {
2534
+ return {
2535
+ arrivalSnapshotTime: Date.now(),
2536
+ arrivalSource: "unavailable",
2537
+ bid: null,
2538
+ ask: null,
2539
+ arrivalMid: null,
2540
+ spreadBps: null
2541
+ };
2542
+ }
2543
+ try {
2544
+ const ticker = await connector.getTopOfBookTicker(symbol);
2545
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
2546
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
2547
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
2548
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
2549
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
2550
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
2551
+ return {
2552
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
2553
+ arrivalSource: "top_of_book",
2554
+ bid,
2555
+ ask,
2556
+ arrivalMid,
2557
+ spreadBps
2558
+ };
2559
+ } catch (error) {
2560
+ import_logger3.logger.warn(
2561
+ "runtime order arrival snapshot failed: %s %s",
2562
+ symbol,
2563
+ error?.message || String(error)
2564
+ );
2565
+ return {
2566
+ arrivalSnapshotTime: Date.now(),
2567
+ arrivalSource: "top_of_book_error",
2568
+ bid: null,
2569
+ ask: null,
2570
+ arrivalMid: null,
2571
+ spreadBps: null
2572
+ };
2019
2573
  }
2020
- return import(
2021
- /* webpackIgnore: true */
2022
- configFileUrl
2023
- );
2024
2574
  };
2025
- var importTradejsModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
2026
- const normalized = String(moduleName ?? "").trim();
2027
- if (!normalized) {
2028
- return {};
2575
+ var validateEntryProtectionAtArrival = ({
2576
+ direction,
2577
+ signalPrice,
2578
+ bid,
2579
+ ask,
2580
+ arrivalMid,
2581
+ takeProfits,
2582
+ stopLossPrice
2583
+ }) => {
2584
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
2585
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
2586
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
2587
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
2029
2588
  }
2030
- let modulePath = normalized;
2031
- if (normalized.startsWith("file://")) {
2032
- try {
2033
- modulePath = (0, import_url.fileURLToPath)(normalized);
2034
- } catch {
2035
- modulePath = normalized;
2589
+ for (const takeProfit of takeProfits) {
2590
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
2591
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
2036
2592
  }
2037
2593
  }
2038
- const requireFn = getRequireFn(
2039
- import_path.default.isAbsolute(modulePath) ? import_path.default.dirname(modulePath) : cwd
2040
- );
2041
- if (isTsModulePath(modulePath)) {
2042
- await ensureTsNodeRegistered();
2043
- await ensureTsconfigPathsRegistered(cwd);
2044
- return requireFn(modulePath);
2594
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
2595
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
2045
2596
  }
2046
- if (isBareModuleSpecifier(normalized)) {
2047
- await ensureTsconfigPathsRegistered(cwd);
2048
- try {
2049
- return requireFn(normalized);
2050
- } catch (error) {
2051
- const resolvedByTsconfig = await resolveTsconfigPathModule(
2052
- normalized,
2053
- cwd
2597
+ };
2598
+ var createEntrySubmissionValidation = ({
2599
+ connector,
2600
+ symbol,
2601
+ direction,
2602
+ takeProfits,
2603
+ stopLossPrice,
2604
+ limits,
2605
+ onValidated
2606
+ }) => {
2607
+ if (!limits) return void 0;
2608
+ if (!connector.capabilities?.orderSubmissionValidation)
2609
+ throw new EntrySubmissionValidationError(
2610
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
2611
+ );
2612
+ return async ({ qty, executionPrice }) => {
2613
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
2614
+ const snapshot2 = simulated && executionPrice != null ? {
2615
+ arrivalSnapshotTime: Date.now(),
2616
+ arrivalSource: "simulation",
2617
+ bid: executionPrice,
2618
+ ask: executionPrice,
2619
+ arrivalMid: executionPrice,
2620
+ spreadBps: 0
2621
+ } : await getOrderArrivalSnapshot({
2622
+ connector,
2623
+ symbol,
2624
+ requireTimestamp: true
2625
+ });
2626
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
2627
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
2628
+ throw new EntrySubmissionValidationError(
2629
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
2054
2630
  );
2055
- if (resolvedByTsconfig && resolvedByTsconfig !== normalized) {
2056
- return requireFn(resolvedByTsconfig);
2057
- }
2058
- throw error;
2631
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
2632
+ throw new EntrySubmissionValidationError(
2633
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
2634
+ );
2635
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
2636
+ validateEntryProtectionAtArrival({
2637
+ direction,
2638
+ signalPrice: price,
2639
+ bid: snapshot2.bid,
2640
+ ask: snapshot2.ask,
2641
+ arrivalMid: snapshot2.arrivalMid,
2642
+ takeProfits,
2643
+ stopLossPrice
2644
+ });
2645
+ const prices = takeProfits.map((tp) => tp.price);
2646
+ const economics2 = validateEntryExecution({
2647
+ direction,
2648
+ price,
2649
+ qty,
2650
+ stopLossPrice,
2651
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
2652
+ limits
2653
+ });
2654
+ if (economics2.belowMinimumNetRiskRatio) {
2655
+ import_logger3.logger.warn(
2656
+ "entry submission warning: %s %s ENTRY_RR_BELOW_MIN_AT_SUBMISSION (RR %s, threshold %s)",
2657
+ symbol,
2658
+ direction,
2659
+ economics2.netRiskRatio,
2660
+ limits.minimumNetRiskRatio
2661
+ );
2662
+ }
2663
+ onValidated?.(snapshot2, economics2);
2664
+ };
2665
+ };
2666
+
2667
+ // src/ai.ts
2668
+ var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
2669
+ var import_redis3 = require("@tradejs/infra/redis");
2670
+
2671
+ // src/strategyAdapters/ai.ts
2672
+ var import_indicators2 = require("@tradejs/core/indicators");
2673
+ var import_aiFeatures = require("@tradejs/core/aiFeatures");
2674
+
2675
+ // src/aiShared.ts
2676
+ var MAX_AI_SERIES_POINTS = 5;
2677
+ var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
2678
+ "tradejs.indicators.compactSnapshot"
2679
+ );
2680
+ var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
2681
+ var trimSeriesDeep = (value) => {
2682
+ if (Array.isArray(value)) {
2683
+ const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
2684
+ const isMatrix = trimmed.every((item) => Array.isArray(item));
2685
+ if (isMatrix) {
2686
+ return trimmed;
2059
2687
  }
2688
+ return trimmed.map(
2689
+ (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
2690
+ );
2060
2691
  }
2061
- try {
2062
- return await import(
2063
- /* webpackIgnore: true */
2064
- toImportSpecifier(normalized)
2692
+ if (value && typeof value === "object") {
2693
+ return Object.fromEntries(
2694
+ Object.entries(value).map(([key, nested]) => [
2695
+ key,
2696
+ trimSeriesDeep(nested)
2697
+ ])
2065
2698
  );
2066
- } catch (error) {
2067
- if (isTsModulePath(modulePath)) {
2068
- await ensureTsNodeRegistered();
2069
- await ensureTsconfigPathsRegistered(cwd);
2070
- return requireFn(modulePath);
2071
- }
2072
- throw error;
2073
2699
  }
2700
+ return value;
2074
2701
  };
2075
- var resolveExportedConfig = (moduleExports) => {
2076
- const candidate = moduleExports && typeof moduleExports === "object" && "default" in moduleExports ? moduleExports.default : moduleExports;
2077
- return normalizeConfig(candidate);
2078
- };
2079
- var findConfigFilePath = (cwd) => {
2080
- let currentDir = import_path.default.resolve(cwd);
2081
- while (true) {
2082
- for (const fileName of CONFIG_FILE_NAMES) {
2083
- const fullPath = import_path.default.join(currentDir, fileName);
2084
- if (import_fs.default.existsSync(fullPath) && import_fs.default.statSync(fullPath).isFile()) {
2085
- return fullPath;
2086
- }
2087
- }
2088
- const parentDir = import_path.default.dirname(currentDir);
2089
- if (parentDir === currentDir) {
2090
- return null;
2091
- }
2092
- currentDir = parentDir;
2702
+ var buildCompactAiIndicatorsSnapshot = (value) => {
2703
+ const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
2704
+ if (typeof compactSnapshot === "function") {
2705
+ return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
2093
2706
  }
2707
+ return trimSeriesDeep(value);
2094
2708
  };
2095
- var resolvePluginModuleSpecifier = (moduleName, cwd = getTradejsProjectCwd()) => {
2096
- const normalized = String(moduleName ?? "").trim();
2097
- if (!normalized) {
2098
- return "";
2099
- }
2100
- if (normalized.startsWith("file://")) {
2101
- try {
2102
- return (0, import_url.fileURLToPath)(normalized);
2103
- } catch {
2104
- return normalized;
2105
- }
2106
- }
2107
- if (import_path.default.isAbsolute(normalized)) {
2108
- return normalized;
2109
- }
2110
- if (isRelativeModulePath(normalized)) {
2111
- return import_path.default.resolve(cwd, normalized);
2709
+
2710
+ // src/aiMarketContext.ts
2711
+ var toRecord = (value) => {
2712
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2713
+ return null;
2112
2714
  }
2113
- return normalized;
2715
+ return value;
2114
2716
  };
2115
- var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
2116
- const cached = cachedByCwd.get(cwd);
2117
- if (cached) {
2118
- return cached;
2119
- }
2120
- const configFilePath = findConfigFilePath(cwd);
2121
- if (!configFilePath) {
2122
- cachedByCwd.set(cwd, {});
2123
- return {};
2124
- }
2125
- try {
2126
- const moduleExports = await importConfigFile(configFilePath);
2127
- const config = resolveExportedConfig(moduleExports);
2128
- cachedByCwd.set(cwd, config);
2129
- if (!announcedConfigFile.has(configFilePath)) {
2130
- announcedConfigFile.add(configFilePath);
2131
- import_logger2.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
2132
- }
2133
- return config;
2134
- } catch (error) {
2135
- import_logger2.logger.log(
2136
- "warn",
2137
- "Failed to load TradeJS config from %s: %s",
2138
- configFilePath,
2139
- String(error)
2140
- );
2141
- cachedByCwd.set(cwd, {});
2142
- return {};
2143
- }
2717
+ var toFiniteNumber = (value) => {
2718
+ const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
2719
+ return Number.isFinite(numeric) ? numeric : null;
2144
2720
  };
2145
-
2146
- // src/strategy/manifests.ts
2147
- var SHARED_STRATEGY_REGISTRY_KEY = "__tradejsNodeSharedStrategyRegistryV1__";
2148
- var sharedRegistryScope = globalThis;
2149
- var sharedStrategyRegistry = sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] ?? (sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] = {
2150
- registryStateByProjectRoot: /* @__PURE__ */ new Map()
2151
- });
2152
- var createStrategyRegistryState = () => ({
2153
- strategyCreators: /* @__PURE__ */ new Map(),
2154
- strategyManifestsMap: /* @__PURE__ */ new Map(),
2155
- strategyEntriesMap: /* @__PURE__ */ new Map(),
2156
- strategySourcesMap: /* @__PURE__ */ new Map(),
2157
- strategyModuleResolutions: /* @__PURE__ */ new Map(),
2158
- pluginsLoadPromise: null
2721
+ var roundTo = (value, decimals) => {
2722
+ const factor = 10 ** decimals;
2723
+ return Math.round(value * factor) / factor;
2724
+ };
2725
+ var buildMissingSpreadContext = () => ({
2726
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2727
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2728
+ available: false,
2729
+ value: null,
2730
+ zScore: null,
2731
+ bps: null,
2732
+ absBps: null,
2733
+ bias: null,
2734
+ severity: null
2159
2735
  });
2160
- var registryStateByProjectRoot = sharedStrategyRegistry.registryStateByProjectRoot;
2161
- var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
2162
- const projectRoot = getTradejsProjectCwd(cwd);
2163
- let state = registryStateByProjectRoot.get(projectRoot);
2164
- if (!state) {
2165
- state = createStrategyRegistryState();
2166
- registryStateByProjectRoot.set(projectRoot, state);
2736
+ var buildSpreadContextFromSignal = (signal) => {
2737
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2738
+ const relative = toRecord(baseContext?.relative);
2739
+ const execution = toRecord(relative?.execution);
2740
+ const spread = toFiniteNumber(execution?.venueSpread);
2741
+ const zScore = toFiniteNumber(execution?.venueSpreadZScore);
2742
+ if (spread == null) {
2743
+ return buildMissingSpreadContext();
2167
2744
  }
2745
+ const value = roundTo(spread, 8);
2746
+ const bps = roundTo(value * 1e4, 2);
2747
+ const absBps = Math.abs(bps);
2748
+ const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
2749
+ const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
2168
2750
  return {
2169
- projectRoot,
2170
- state
2751
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2752
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2753
+ available: true,
2754
+ value,
2755
+ zScore,
2756
+ bps,
2757
+ absBps,
2758
+ bias,
2759
+ severity
2171
2760
  };
2172
2761
  };
2173
- var toUniqueModules = (modules = []) => [
2174
- ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
2175
- ];
2176
- var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
2177
- const config = await loadTradejsConfig(cwd);
2762
+ var buildTrueDeltaContextFromSignal = (signal) => {
2763
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2764
+ const participation = toRecord(baseContext?.participation);
2765
+ const delta = toRecord(participation?.delta);
2766
+ const source = String(delta?.source ?? "");
2767
+ const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
2768
+ if (!delta || !isTrueDeltaSource) {
2769
+ return {
2770
+ source: source || null,
2771
+ available: false,
2772
+ buyPressurePct: null,
2773
+ buyVolume: null,
2774
+ sellVolume: null,
2775
+ netDelta: null,
2776
+ deltaPct: null,
2777
+ signedVolumeZScore: null
2778
+ };
2779
+ }
2178
2780
  return {
2179
- strategyModules: toUniqueModules(config.strategies),
2180
- indicatorModules: toUniqueModules(config.indicators)
2781
+ source,
2782
+ available: true,
2783
+ buyPressurePct: toFiniteNumber(delta.buyPressurePct),
2784
+ buyVolume: toFiniteNumber(delta.buyVolume),
2785
+ sellVolume: toFiniteNumber(delta.sellVolume),
2786
+ netDelta: toFiniteNumber(delta.netDelta),
2787
+ deltaPct: toFiniteNumber(delta.deltaPct),
2788
+ signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
2181
2789
  };
2182
2790
  };
2183
- var extractModuleEntries = (moduleExport, key) => {
2184
- if (!moduleExport || typeof moduleExport !== "object") {
2185
- return null;
2186
- }
2187
- const candidate = moduleExport;
2188
- if (Array.isArray(candidate[key])) {
2189
- return candidate[key];
2190
- }
2191
- const defaultExport = candidate.default;
2192
- if (defaultExport && Array.isArray(defaultExport[key])) {
2193
- return defaultExport[key];
2791
+ var buildTradeFlowContextFromSignal = (signal) => {
2792
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2793
+ const participation = toRecord(baseContext?.participation);
2794
+ const tradeFlow = toRecord(participation?.tradeFlow);
2795
+ if (!tradeFlow) {
2796
+ return {
2797
+ source: null,
2798
+ available: false,
2799
+ interval: null,
2800
+ stale: null,
2801
+ trades: null,
2802
+ buyPressurePct: null,
2803
+ netBaseDelta: null,
2804
+ netQuoteDelta: null
2805
+ };
2194
2806
  }
2195
- return null;
2196
- };
2197
- var extractStrategyPluginDefinition = (moduleExport) => {
2198
- const strategyEntries = extractModuleEntries(
2199
- moduleExport,
2200
- "strategyEntries"
2201
- );
2202
- return strategyEntries ? { strategyEntries } : null;
2807
+ return {
2808
+ source: String(tradeFlow.source ?? ""),
2809
+ available: true,
2810
+ interval: String(tradeFlow.interval ?? ""),
2811
+ stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
2812
+ trades: toFiniteNumber(tradeFlow.trades),
2813
+ buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
2814
+ netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
2815
+ netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
2816
+ };
2203
2817
  };
2204
- var extractIndicatorPluginDefinition = (moduleExport) => {
2205
- const indicatorEntries = extractModuleEntries(
2206
- moduleExport,
2207
- "indicatorEntries"
2208
- );
2209
- return indicatorEntries ? { indicatorEntries } : null;
2818
+ var buildMarketBreadthContextFromSignal = (signal) => {
2819
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2820
+ const relative = toRecord(baseContext?.relative);
2821
+ const breadth = toRecord(relative?.marketBreadth);
2822
+ if (!breadth) {
2823
+ return {
2824
+ source: null,
2825
+ available: false,
2826
+ universe: null,
2827
+ interval: null,
2828
+ stale: null,
2829
+ symbolsCount: null,
2830
+ advanceDeclineRatio: null,
2831
+ pctAboveMa20: null,
2832
+ pctAboveMa50: null,
2833
+ equalWeightedReturn: null,
2834
+ volumeWeightedReturn: null,
2835
+ dispersion: null
2836
+ };
2837
+ }
2838
+ return {
2839
+ source: String(breadth.source ?? ""),
2840
+ available: true,
2841
+ universe: String(breadth.universe ?? ""),
2842
+ interval: String(breadth.interval ?? ""),
2843
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
2844
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
2845
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
2846
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
2847
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
2848
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
2849
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
2850
+ dispersion: toFiniteNumber(breadth.dispersion)
2851
+ };
2210
2852
  };
2211
- var validateStrategyEntries = (moduleName, entries, state) => {
2212
- const issues = [];
2213
- const names = /* @__PURE__ */ new Set();
2214
- entries.forEach((entry, index) => {
2215
- const entryPath = `${moduleName}.strategyEntries[${index}]`;
2216
- const strategyName = entry?.manifest?.name;
2217
- if (typeof strategyName !== "string" || !strategyName.trim()) {
2218
- issues.push(`${entryPath}: manifest.name is required`);
2219
- } else if (names.has(strategyName) || state?.strategyEntriesMap.has(strategyName)) {
2220
- issues.push(`${entryPath}: duplicate strategy ${strategyName}`);
2221
- } else {
2222
- names.add(strategyName);
2223
- }
2224
- if (!entry || typeof entry !== "object" || !entry.defaults || typeof entry.defaults !== "object" || Array.isArray(entry.defaults)) {
2225
- issues.push(`${entryPath}: defaults must be an object`);
2226
- }
2227
- if (typeof entry?.parseConfig !== "function") {
2228
- issues.push(`${entryPath}: parseConfig is required`);
2229
- }
2230
- if (typeof entry?.createCore !== "function") {
2231
- issues.push(`${entryPath}: createCore is required`);
2853
+ var buildMarketBreadthsContextFromSignal = (signal) => {
2854
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2855
+ const relative = toRecord(baseContext?.relative);
2856
+ const breadths = toRecord(relative?.marketBreadths);
2857
+ const build = (key) => {
2858
+ const breadth = toRecord(breadths?.[key]);
2859
+ if (!breadth) {
2860
+ return {
2861
+ source: null,
2862
+ available: false,
2863
+ universe: null,
2864
+ interval: null,
2865
+ stale: null,
2866
+ symbolsCount: null,
2867
+ advanceDeclineRatio: null,
2868
+ pctAboveMa20: null,
2869
+ pctAboveMa50: null,
2870
+ equalWeightedReturn: null,
2871
+ volumeWeightedReturn: null,
2872
+ dispersion: null
2873
+ };
2232
2874
  }
2233
- });
2234
- return issues;
2875
+ return {
2876
+ source: String(breadth.source ?? ""),
2877
+ available: true,
2878
+ universe: String(breadth.universe ?? ""),
2879
+ interval: String(breadth.interval ?? ""),
2880
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
2881
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
2882
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
2883
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
2884
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
2885
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
2886
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
2887
+ dispersion: toFiniteNumber(breadth.dispersion)
2888
+ };
2889
+ };
2890
+ return {
2891
+ top5: build("top5"),
2892
+ top10: build("top10"),
2893
+ top30: build("top30"),
2894
+ top50: build("top50"),
2895
+ top100: build("top100")
2896
+ };
2235
2897
  };
2236
- var registerEntries = (entries, source, state) => {
2237
- for (const entry of entries) {
2238
- const strategyName = entry.manifest.name;
2239
- state.strategyManifestsMap.set(strategyName, entry.manifest);
2240
- state.strategyEntriesMap.set(strategyName, entry);
2241
- state.strategySourcesMap.set(strategyName, source);
2242
- materializeStrategyCreator(strategyName, state);
2243
- }
2898
+ var buildTargetVsReference = (signal, asset) => {
2899
+ const base = toRecord(signal.additionalIndicators?.baseContext);
2900
+ const relative = toRecord(base?.relative);
2901
+ const target = toRecord(relative?.[`targetVs${asset}`]);
2902
+ return {
2903
+ source: target ? String(target.source ?? "") : null,
2904
+ available: Boolean(target),
2905
+ ratioReturn1h: toFiniteNumber(target?.ratioReturn1h),
2906
+ ratioReturn4h: toFiniteNumber(target?.ratioReturn4h),
2907
+ ratioReturn24h: toFiniteNumber(target?.ratioReturn24h),
2908
+ alpha1h: toFiniteNumber(target?.[`alphaVs${asset}1h`]),
2909
+ alpha4h: toFiniteNumber(target?.[`alphaVs${asset}4h`]),
2910
+ alpha24h: toFiniteNumber(target?.[`alphaVs${asset}24h`]),
2911
+ beta: toFiniteNumber(target?.[`betaTo${asset}20`]),
2912
+ correlation: toFiniteNumber(target?.[`correlationTo${asset}20`]),
2913
+ ratioTrend: typeof target?.ratioTrend === "string" ? target.ratioTrend : null
2914
+ };
2244
2915
  };
2245
- var materializeStrategyCreator = (strategyName, state) => {
2246
- if (state.strategyCreators.has(strategyName) || !sharedStrategyRegistry.strategyRuntimeFactory) {
2247
- return;
2248
- }
2249
- const entry = state.strategyEntriesMap.get(strategyName);
2250
- if (!entry) return;
2251
- state.strategyCreators.set(
2252
- strategyName,
2253
- sharedStrategyRegistry.strategyRuntimeFactory({
2254
- strategyName,
2255
- defaults: entry.defaults,
2256
- createCore: entry.createCore,
2257
- manifest: entry.manifest,
2258
- detectorKey: entry.detectorKey,
2259
- detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
2260
- resolveRegisteredManifest: (name) => state.strategyManifestsMap.get(name)
2261
- })
2262
- );
2916
+ var buildTargetVsBtcContextFromSignal = (signal) => {
2917
+ const { alpha1h, alpha4h, alpha24h, beta, correlation, ...shared } = buildTargetVsReference(signal, "Btc");
2918
+ return {
2919
+ ...shared,
2920
+ alphaVsBtc1h: alpha1h,
2921
+ alphaVsBtc4h: alpha4h,
2922
+ alphaVsBtc24h: alpha24h,
2923
+ betaToBtc20: beta,
2924
+ correlationToBtc20: correlation
2925
+ };
2263
2926
  };
2264
- var setStrategyRuntimeFactory = (factory) => {
2265
- sharedStrategyRegistry.strategyRuntimeFactory = factory;
2266
- for (const state of registryStateByProjectRoot.values()) {
2267
- for (const strategyName of state.strategyEntriesMap.keys()) {
2268
- materializeStrategyCreator(strategyName, state);
2269
- }
2270
- }
2927
+ var buildTargetVsEthContextFromSignal = (signal) => {
2928
+ const { alpha1h, alpha4h, alpha24h, beta, correlation, ...shared } = buildTargetVsReference(signal, "Eth");
2929
+ return {
2930
+ ...shared,
2931
+ alphaVsEth1h: alpha1h,
2932
+ alphaVsEth4h: alpha4h,
2933
+ alphaVsEth24h: alpha24h,
2934
+ betaToEth20: beta,
2935
+ correlationToEth20: correlation
2936
+ };
2271
2937
  };
2272
- var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
2273
- if (typeof importTradejsModule === "function") {
2274
- return importTradejsModule(moduleName, cwd);
2938
+ var buildBtcAltRegimeContextFromSignal = (signal) => {
2939
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2940
+ const relative = toRecord(baseContext?.relative);
2941
+ const btcAltRegime = toRecord(relative?.btcAltRegime);
2942
+ if (!btcAltRegime) {
2943
+ return {
2944
+ source: null,
2945
+ available: false,
2946
+ universe: null,
2947
+ interval: null,
2948
+ stale: null,
2949
+ regime: null,
2950
+ btcReturn24h: null,
2951
+ altBasketReturn24h: null,
2952
+ btcVsAltReturn24h: null,
2953
+ btcTurnoverShare24h: null,
2954
+ btcTurnoverShareChange24h: null,
2955
+ altVolToBtcVol24h: null,
2956
+ altDispersion24h: null
2957
+ };
2275
2958
  }
2276
- return import(
2277
- /* webpackIgnore: true */
2278
- moduleName
2279
- );
2959
+ return {
2960
+ source: String(btcAltRegime.source ?? ""),
2961
+ available: true,
2962
+ universe: String(btcAltRegime.universe ?? ""),
2963
+ interval: String(btcAltRegime.interval ?? ""),
2964
+ stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
2965
+ regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
2966
+ btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
2967
+ altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
2968
+ btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
2969
+ btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
2970
+ btcTurnoverShareChange24h: toFiniteNumber(
2971
+ btcAltRegime.btcTurnoverShareChange24h
2972
+ ),
2973
+ altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
2974
+ altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
2975
+ };
2280
2976
  };
2281
- var resolveStrategyModuleEntry = async ({
2282
- strategyName,
2283
- moduleName,
2284
- cwd
2285
- }) => {
2286
- const { projectRoot, state } = getStrategyRegistryState(cwd);
2287
- const source = moduleName.trim();
2288
- const resolvedModuleName = resolvePluginModuleSpecifier(source, projectRoot);
2289
- const cacheKey = `${resolvedModuleName}\0${strategyName}`;
2290
- const cached = state.strategyModuleResolutions.get(cacheKey);
2291
- if (cached) return cached;
2292
- const resolution = (async () => {
2293
- const moduleExport = await importStrategyPluginModule(
2294
- resolvedModuleName,
2295
- projectRoot
2296
- );
2297
- const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
2298
- if (!pluginDefinition) {
2299
- throw new Error(`${source}: export { strategyEntries } is missing`);
2300
- }
2301
- const issues = validateStrategyEntries(
2302
- source,
2303
- pluginDefinition.strategyEntries
2304
- );
2305
- if (issues.length > 0) {
2306
- throw new Error(
2307
- ["Invalid TradeJS strategy module:", ...issues].join("\n")
2308
- );
2309
- }
2310
- const entry = pluginDefinition.strategyEntries.find(
2311
- (candidate) => candidate.manifest.name === strategyName
2312
- );
2313
- if (!entry) {
2314
- throw new Error(`${source}: strategy ${strategyName} is missing`);
2315
- }
2316
- const factory = sharedStrategyRegistry.strategyRuntimeFactory;
2317
- if (!factory) {
2318
- throw new Error("Strategy runtime factory is not configured");
2319
- }
2320
- const manifestsByName = new Map(
2321
- pluginDefinition.strategyEntries.map(
2322
- (candidate) => [candidate.manifest.name, candidate.manifest]
2323
- )
2324
- );
2977
+ var buildCmcGlobalContextFromSignal = (signal) => {
2978
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2979
+ const relative = toRecord(baseContext?.relative);
2980
+ const cmcGlobal = toRecord(relative?.cmcGlobal);
2981
+ if (!cmcGlobal) {
2325
2982
  return {
2326
- entry,
2327
- creator: factory({
2328
- strategyName,
2329
- defaults: entry.defaults,
2330
- createCore: entry.createCore,
2331
- manifest: entry.manifest,
2332
- detectorKey: entry.detectorKey,
2333
- detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
2334
- resolveRegisteredManifest: (name) => manifestsByName.get(name) ?? state.strategyManifestsMap.get(name)
2335
- }),
2336
- source
2983
+ source: null,
2984
+ available: false,
2985
+ interval: null,
2986
+ asOfTs: null,
2987
+ stale: null,
2988
+ totalMarketCapUsd: null,
2989
+ totalVolumeUsd: null,
2990
+ totalVolumeReportedUsd: null,
2991
+ altMarketCapUsd: null,
2992
+ altVolumeUsd: null,
2993
+ altVolumeReportedUsd: null,
2994
+ btcDominancePct: null,
2995
+ ethDominancePct: null,
2996
+ btcDominanceChange24hPct: null,
2997
+ ethDominanceChange24hPct: null,
2998
+ altMarketCapChange24hPct: null,
2999
+ altVolumeChange24hPct: null,
3000
+ activeCryptocurrencies: null,
3001
+ activeExchanges: null,
3002
+ activeMarketPairs: null,
3003
+ altLiquidityRegime: null
2337
3004
  };
2338
- })();
2339
- state.strategyModuleResolutions.set(cacheKey, resolution);
2340
- try {
2341
- return await resolution;
2342
- } catch (error) {
2343
- state.strategyModuleResolutions.delete(cacheKey);
2344
- throw error;
2345
3005
  }
3006
+ return {
3007
+ source: String(cmcGlobal.source ?? ""),
3008
+ available: true,
3009
+ interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
3010
+ asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
3011
+ stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
3012
+ totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
3013
+ totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
3014
+ totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
3015
+ altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
3016
+ altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
3017
+ altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
3018
+ btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
3019
+ ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
3020
+ btcDominanceChange24hPct: toFiniteNumber(
3021
+ cmcGlobal.btcDominanceChange24hPct
3022
+ ),
3023
+ ethDominanceChange24hPct: toFiniteNumber(
3024
+ cmcGlobal.ethDominanceChange24hPct
3025
+ ),
3026
+ altMarketCapChange24hPct: toFiniteNumber(
3027
+ cmcGlobal.altMarketCapChange24hPct
3028
+ ),
3029
+ altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
3030
+ activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
3031
+ activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
3032
+ activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
3033
+ altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
3034
+ };
2346
3035
  };
2347
- var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
2348
- const { projectRoot, state } = getStrategyRegistryState(cwd);
2349
- if (!state.pluginsLoadPromise) {
2350
- (0, import_indicators.resetIndicatorRegistryCache)(projectRoot);
2351
- state.pluginsLoadPromise = (async () => {
2352
- const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
2353
- const strategySet = new Set(strategyModules);
2354
- const indicatorSet = new Set(indicatorModules);
2355
- const pluginModuleNames = [
2356
- .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
2357
- ];
2358
- if (!pluginModuleNames.length) {
2359
- return;
2360
- }
2361
- const issues = [];
2362
- for (const moduleName of pluginModuleNames) {
2363
- try {
2364
- const resolvedModuleName = resolvePluginModuleSpecifier(
2365
- moduleName,
2366
- projectRoot
2367
- );
2368
- const moduleExport = await importStrategyPluginModule(
2369
- resolvedModuleName,
2370
- projectRoot
2371
- );
2372
- if (strategySet.has(moduleName)) {
2373
- const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
2374
- if (!pluginDefinition) {
2375
- issues.push(
2376
- `${moduleName}: export { strategyEntries } is missing`
2377
- );
2378
- } else {
2379
- const entryIssues = validateStrategyEntries(
2380
- moduleName,
2381
- pluginDefinition.strategyEntries,
2382
- state
2383
- );
2384
- issues.push(...entryIssues);
2385
- if (entryIssues.length === 0) {
2386
- registerEntries(
2387
- pluginDefinition.strategyEntries,
2388
- moduleName,
2389
- state
2390
- );
2391
- }
2392
- }
2393
- }
2394
- if (indicatorSet.has(moduleName)) {
2395
- const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
2396
- if (!indicatorPluginDefinition) {
2397
- issues.push(
2398
- `${moduleName}: export { indicatorEntries } is missing`
2399
- );
2400
- } else {
2401
- (0, import_indicators.registerIndicatorEntries)(
2402
- indicatorPluginDefinition.indicatorEntries,
2403
- moduleName,
2404
- projectRoot
2405
- );
2406
- }
2407
- }
2408
- if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
2409
- issues.push(`${moduleName}: plugin is not declared in config`);
2410
- }
2411
- } catch (error) {
2412
- issues.push(`${moduleName}: failed to import: ${String(error)}`);
2413
- }
2414
- }
2415
- if (issues.length > 0) {
2416
- throw new Error(
2417
- ["Invalid TradeJS plugin catalog:", ...issues].join("\n")
2418
- );
2419
- }
2420
- })();
3036
+ var buildCmcReferenceAssetsContextFromSignal = (signal) => {
3037
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3038
+ const relative = toRecord(baseContext?.relative);
3039
+ const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
3040
+ if (!cmcReferenceAssets) {
3041
+ return {
3042
+ source: null,
3043
+ available: false,
3044
+ interval: null,
3045
+ asOfTs: null,
3046
+ stale: null,
3047
+ btcMarketCapUsd: null,
3048
+ ethMarketCapUsd: null,
3049
+ btcVolumeUsd: null,
3050
+ ethVolumeUsd: null,
3051
+ btcVolumeToMarketCap: null,
3052
+ ethVolumeToMarketCap: null,
3053
+ ethBtcMarketCapRatio: null,
3054
+ ethBtcMarketCapRatioChange24hPct: null,
3055
+ ethVsBtcVolumeRatio: null,
3056
+ referenceLiquidityRegime: null
3057
+ };
2421
3058
  }
2422
- await state.pluginsLoadPromise;
3059
+ return {
3060
+ source: String(cmcReferenceAssets.source ?? ""),
3061
+ available: true,
3062
+ interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
3063
+ asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
3064
+ stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
3065
+ btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
3066
+ ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
3067
+ btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
3068
+ ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
3069
+ btcVolumeToMarketCap: toFiniteNumber(
3070
+ cmcReferenceAssets.btcVolumeToMarketCap
3071
+ ),
3072
+ ethVolumeToMarketCap: toFiniteNumber(
3073
+ cmcReferenceAssets.ethVolumeToMarketCap
3074
+ ),
3075
+ ethBtcMarketCapRatio: toFiniteNumber(
3076
+ cmcReferenceAssets.ethBtcMarketCapRatio
3077
+ ),
3078
+ ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
3079
+ cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
3080
+ ),
3081
+ ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
3082
+ referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
3083
+ };
2423
3084
  };
2424
- var getStrategyCreator = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2425
- if (moduleName?.trim()) {
2426
- await ensureStrategyPluginsLoaded(cwd);
2427
- return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).creator;
3085
+ var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
3086
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3087
+ const relative = toRecord(baseContext?.relative);
3088
+ const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
3089
+ if (!cmcExchangeLiquidity) {
3090
+ return {
3091
+ source: null,
3092
+ available: false,
3093
+ interval: null,
3094
+ asOfTs: null,
3095
+ stale: null,
3096
+ exchangesCount: null,
3097
+ totalVolumeUsd: null,
3098
+ totalVolumeChange24hPct: null,
3099
+ binanceVolumeUsd: null,
3100
+ binanceVolumeShare: null,
3101
+ topExchangeVolumeShare: null,
3102
+ liquidityRegime: null
3103
+ };
2428
3104
  }
2429
- await ensureStrategyPluginsLoaded(cwd);
2430
- const { state } = getStrategyRegistryState(cwd);
2431
- return state.strategyCreators.get(name);
3105
+ return {
3106
+ source: String(cmcExchangeLiquidity.source ?? ""),
3107
+ available: true,
3108
+ interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
3109
+ asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
3110
+ stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
3111
+ exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
3112
+ totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
3113
+ totalVolumeChange24hPct: toFiniteNumber(
3114
+ cmcExchangeLiquidity.totalVolumeChange24hPct
3115
+ ),
3116
+ binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
3117
+ binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
3118
+ topExchangeVolumeShare: toFiniteNumber(
3119
+ cmcExchangeLiquidity.topExchangeVolumeShare
3120
+ ),
3121
+ liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
3122
+ };
2432
3123
  };
2433
- var getStrategyEntry = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2434
- if (moduleName?.trim()) {
2435
- await ensureStrategyPluginsLoaded(cwd);
2436
- return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).entry;
3124
+ var buildCmcFearGreedContextFromSignal = (signal) => {
3125
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3126
+ const relative = toRecord(baseContext?.relative);
3127
+ const cmcFearGreed = toRecord(relative?.cmcFearGreed);
3128
+ if (!cmcFearGreed) {
3129
+ return {
3130
+ source: null,
3131
+ available: false,
3132
+ interval: null,
3133
+ asOfTs: null,
3134
+ stale: null,
3135
+ value: null,
3136
+ valueChange24h: null,
3137
+ valueChange7d: null,
3138
+ classification: null,
3139
+ sentimentRegime: null
3140
+ };
2437
3141
  }
2438
- await ensureStrategyPluginsLoaded(cwd);
2439
- const { state } = getStrategyRegistryState(cwd);
2440
- return state.strategyEntriesMap.get(name);
3142
+ return {
3143
+ source: String(cmcFearGreed.source ?? ""),
3144
+ available: true,
3145
+ interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
3146
+ asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
3147
+ stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
3148
+ value: toFiniteNumber(cmcFearGreed.value),
3149
+ valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
3150
+ valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
3151
+ classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
3152
+ sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
3153
+ };
2441
3154
  };
2442
- var getStrategyPluginSource = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2443
- if (moduleName?.trim()) {
2444
- await ensureStrategyPluginsLoaded(cwd);
2445
- return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).source;
3155
+ var buildCmcIndexesContextFromSignal = (signal) => {
3156
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3157
+ const relative = toRecord(baseContext?.relative);
3158
+ const cmcIndexes = toRecord(relative?.cmcIndexes);
3159
+ if (!cmcIndexes) {
3160
+ return {
3161
+ source: null,
3162
+ available: false,
3163
+ interval: null,
3164
+ asOfTs: null,
3165
+ stale: null,
3166
+ cmc100Value: null,
3167
+ cmc100Change24hPct: null,
3168
+ cmc100TopConstituentSymbol: null,
3169
+ cmc100TopConstituentWeightPct: null,
3170
+ cmc20Value: null,
3171
+ cmc20Change24hPct: null,
3172
+ cmc20TopConstituentSymbol: null,
3173
+ cmc20TopConstituentWeightPct: null,
3174
+ cmc20ToCmc100Ratio: null,
3175
+ cmc20ToCmc100RatioChange24hPct: null,
3176
+ indexRegime: null
3177
+ };
2446
3178
  }
2447
- await ensureStrategyPluginsLoaded(cwd);
2448
- const { state } = getStrategyRegistryState(cwd);
2449
- return state.strategySourcesMap.get(name);
3179
+ return {
3180
+ source: String(cmcIndexes.source ?? ""),
3181
+ available: true,
3182
+ interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
3183
+ asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
3184
+ stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
3185
+ cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
3186
+ cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
3187
+ cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
3188
+ cmc100TopConstituentWeightPct: toFiniteNumber(
3189
+ cmcIndexes.cmc100TopConstituentWeightPct
3190
+ ),
3191
+ cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
3192
+ cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
3193
+ cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
3194
+ cmc20TopConstituentWeightPct: toFiniteNumber(
3195
+ cmcIndexes.cmc20TopConstituentWeightPct
3196
+ ),
3197
+ cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
3198
+ cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
3199
+ cmcIndexes.cmc20ToCmc100RatioChange24hPct
3200
+ ),
3201
+ indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
3202
+ };
2450
3203
  };
2451
- var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
2452
- if (!name) {
2453
- return void 0;
3204
+ var buildReferenceTradeFlowContextFromSignal = (signal) => {
3205
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3206
+ const relative = toRecord(baseContext?.relative);
3207
+ const refs = toRecord(relative?.referenceTradeFlow);
3208
+ const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
3209
+ const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
3210
+ const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
3211
+ if (!refs) {
3212
+ return {
3213
+ source: null,
3214
+ available: false,
3215
+ primaryReferenceSymbol: null,
3216
+ referenceSymbols: [],
3217
+ primaryTradeFlowBuyPressurePct: null,
3218
+ primaryTradeFlowStale: null
3219
+ };
2454
3220
  }
2455
- const { state } = getStrategyRegistryState(cwd);
2456
- return state.strategyManifestsMap.get(name);
3221
+ return {
3222
+ source: String(refs.source ?? ""),
3223
+ available: true,
3224
+ primaryReferenceSymbol,
3225
+ referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
3226
+ primaryTradeFlowBuyPressurePct: toFiniteNumber(
3227
+ primaryTradeFlow?.buyPressurePct
3228
+ ),
3229
+ primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
3230
+ };
2457
3231
  };
2458
- var strategies = new Proxy(
2459
- {},
2460
- {
2461
- get: (_target, property) => {
2462
- if (typeof property !== "string") {
2463
- return void 0;
2464
- }
2465
- return getStrategyRegistryState().state.strategyCreators.get(property);
2466
- },
2467
- ownKeys: () => {
2468
- return [...getStrategyRegistryState().state.strategyCreators.keys()];
2469
- },
2470
- getOwnPropertyDescriptor: () => ({
2471
- enumerable: true,
2472
- configurable: true
2473
- })
3232
+ var buildAiMarketContext = (signal) => ({
3233
+ execution: {
3234
+ binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
3235
+ },
3236
+ participation: {
3237
+ trueDelta: buildTrueDeltaContextFromSignal(signal),
3238
+ tradeFlow: buildTradeFlowContextFromSignal(signal)
3239
+ },
3240
+ relative: {
3241
+ marketBreadth: buildMarketBreadthContextFromSignal(signal),
3242
+ marketBreadths: buildMarketBreadthsContextFromSignal(signal),
3243
+ targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
3244
+ targetVsEth: buildTargetVsEthContextFromSignal(signal),
3245
+ btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
3246
+ cmcGlobal: buildCmcGlobalContextFromSignal(signal),
3247
+ cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
3248
+ cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
3249
+ cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
3250
+ cmcIndexes: buildCmcIndexesContextFromSignal(signal),
3251
+ referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
2474
3252
  }
2475
- );
3253
+ });
2476
3254
 
2477
3255
  // src/strategy/policyProfiles.ts
2478
3256
  var profileMatches = (profile, universe, assetClass) => {
@@ -2574,12 +3352,61 @@ var buildBaseAiPayload = (signal) => {
2574
3352
  };
2575
3353
  };
2576
3354
  var defaultAiAdapter = {};
3355
+ var tickerProfileFeatureIds = new Set(
3356
+ import_aiFeatures.TICKER_PROFILE_AI_FEATURES.map(({ id }) => id)
3357
+ );
2577
3358
  var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
2578
3359
  var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
2579
3360
  var buildAiPayloadByStrategy = (signal) => {
2580
3361
  const basePayload = buildBaseAiPayload(signal);
2581
3362
  const adapter = getSignalAiAdapter(signal);
2582
- return adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
3363
+ const payload = adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
3364
+ const custom = getStrategyManifest(signal.strategy)?.aiFeatures ?? [];
3365
+ const frozen = signal.aiFeatureReplay?.snapshot;
3366
+ if (frozen) (0, import_aiFeatures.validateAiFeatureSnapshot)(frozen);
3367
+ const tickerProfile = signal.additionalIndicators?.tickerProfile ?? signal.additionalIndicators?.baseContext?.tickerProfile;
3368
+ const hasProfile = Boolean(
3369
+ tickerProfile || frozen?.definitions.some(({ id }) => tickerProfileFeatureIds.has(id))
3370
+ );
3371
+ const indicatorFeatures = (0, import_indicators2.getRegisteredIndicatorEntries)(
3372
+ getTradejsProjectCwd()
3373
+ ).flatMap((entry) => entry.indicator.enabled ? entry.aiFeatures ?? [] : []);
3374
+ const hasPortfolio = Boolean(
3375
+ signal.additionalIndicators?.portfolioContext || frozen?.definitions.some(
3376
+ ({ id }) => import_aiFeatures.PORTFOLIO_AI_FEATURES.some((d) => d.id === id)
3377
+ )
3378
+ );
3379
+ const definitions = [
3380
+ ...hasPortfolio ? import_aiFeatures.PORTFOLIO_AI_FEATURES : [],
3381
+ ...hasProfile ? import_aiFeatures.TICKER_PROFILE_AI_FEATURES : [],
3382
+ ...indicatorFeatures,
3383
+ ...custom
3384
+ ];
3385
+ if (!definitions.length && !signal.aiFeatureReplay?.snapshot) return payload;
3386
+ (0, import_aiFeatures.validateAiFeatureDefinitions)(definitions);
3387
+ if (signal.aiFeatureReplay && !frozen) {
3388
+ throw new Error(
3389
+ "Dataset has no frozen AI features; regenerate the decision-time export"
3390
+ );
3391
+ }
3392
+ if (frozen && JSON.stringify(frozen.definitions) !== JSON.stringify(definitions)) {
3393
+ throw new Error(
3394
+ "AI feature definition changed; regenerate the decision-time export"
3395
+ );
3396
+ }
3397
+ return {
3398
+ ...payload,
3399
+ featureSnapshot: frozen ?? (0, import_aiFeatures.projectAiFeatures)(definitions, {
3400
+ signal: tickerProfile ? {
3401
+ ...signal,
3402
+ additionalIndicators: {
3403
+ ...signal.additionalIndicators,
3404
+ tickerProfile
3405
+ }
3406
+ } : signal,
3407
+ payload
3408
+ })
3409
+ };
2583
3410
  };
2584
3411
  var buildAiSystemPromptAddonByStrategy = (signal) => getSignalAiAdapter(signal).buildSystemPromptAddon?.({ signal }) ?? "";
2585
3412
  var buildAiHumanPromptAddonByStrategy = (signal, payload) => getSignalAiAdapter(signal).buildHumanPromptAddon?.({
@@ -2966,379 +3793,131 @@ var getDeterministicAiGateContext = (payload) => {
2966
3793
  const additionalIndicators = asRecord(payload.additionalIndicators);
2967
3794
  const candidates = [
2968
3795
  additionalIndicators,
2969
- ...Object.values(additionalIndicators ?? {}).map(asRecord)
2970
- ].filter((value) => Boolean(value));
2971
- return candidates.find(
2972
- (candidate) => Array.isArray(candidate.approvalBlockReasons) || Array.isArray(candidate.riskAnnotations) || Array.isArray(candidate.structuralHardBlockReasons) || typeof candidate.approvalAllowedNow === "boolean"
2973
- ) ?? null;
2974
- };
2975
- var buildAiHumanPrompt = (signal, payload = buildAiPayload(signal)) => `
2976
- Analyze the already computed internal signal for ${signal.symbol}. The original signal direction is ${signal.direction}.
2977
- This is a structure-classification and audit task, not execution advice. Determine whether the current structure confirms the existing signal, how structurally coherent it is right now, whether an extra confirmation level is needed, and whether the already supplied levels in \`payload.signal.prices\` still look internally coherent. Do not replace the original thesis with a new one and do not invent new levels; return only the requested JSON.
2978
-
2979
- Trade payload:
2980
- ${JSON.stringify(payload)}
2981
- ${buildAiHumanPromptAddonByStrategy(signal, payload)}
2982
- `;
2983
- var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
2984
- const settings = await getAiUserSettings(options.userName);
2985
- const responseLanguage = (0, import_aiLanguages2.getAiResponseLanguagePromptName)(
2986
- settings.AI_RESPONSE_LANGUAGE || import_aiLanguages2.DEFAULT_AI_RESPONSE_LANGUAGE
2987
- );
2988
- const response = await invokeAiPromptChat({
2989
- userName: options.userName,
2990
- model: options.model,
2991
- messages: [
2992
- { role: "system", content: systemPrompt },
2993
- {
2994
- role: "system",
2995
- content: `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
2996
- },
2997
- { role: "user", content: humanPrompt }
2998
- ]
2999
- });
3000
- const parsed = parseAIResponse(response.content);
3001
- const normalized = normalizeAnalysis(parsed);
3002
- if (!options.signal) {
3003
- return normalized;
3004
- }
3005
- return postProcessAiAnalysisByStrategy(
3006
- options.signal,
3007
- normalized,
3008
- options.payload
3009
- );
3010
- };
3011
- var runAiPromptLocal = async (signal, options = {}) => {
3012
- const payload = options.payload ?? buildAiPayload(signal);
3013
- const gateContext = getDeterministicAiGateContext(payload);
3014
- const signalDirection = getSignalDirection(signal);
3015
- const deterministicQuality = getDeterministicQuality(gateContext);
3016
- const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
3017
- return postProcessLocalAiAnalysisByStrategy(
3018
- signal,
3019
- {
3020
- direction: approvalAllowedNow ? signalDirection : null,
3021
- quality: deterministicQuality,
3022
- needRetest: !approvalAllowedNow,
3023
- retestPrice: null,
3024
- takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
3025
- stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
3026
- },
3027
- payload
3028
- );
3029
- };
3030
- var askAI = async (signal, options = {}) => {
3031
- const { symbol } = signal;
3032
- const payload = buildAiPayload(signal);
3033
- const content = await runAiPrompt(
3034
- {
3035
- systemPrompt: buildAiSystemPrompt(signal),
3036
- humanPrompt: buildAiHumanPrompt(signal, payload)
3037
- },
3038
- {
3039
- ...options,
3040
- signal,
3041
- payload
3042
- }
3043
- );
3044
- await (0, import_redis.setData)(import_redis.redisKeys.analysis(symbol, signal.signalId), content);
3045
- return content;
3046
- };
3047
-
3048
- // src/strategyAdapters/ml.ts
3049
- var defaultMlAdapter = {
3050
- normalizeStrategyConfig: (strategyConfig) => strategyConfig
3051
- };
3052
- var getStrategyMlAdapter = (strategy, profileId) => {
3053
- const strategyAdapter = getStrategyProfileMlAdapter(
3054
- getStrategyManifest(strategy),
3055
- profileId
3056
- );
3057
- if (!strategyAdapter) return defaultMlAdapter;
3058
- return {
3059
- ...defaultMlAdapter,
3060
- ...strategyAdapter
3061
- };
3062
- };
3063
-
3064
- // src/mlPayload.ts
3065
- var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
3066
- return getStrategyMlAdapter(
3067
- strategyName,
3068
- profileId
3069
- ).normalizeStrategyConfig?.(strategyConfig);
3070
- };
3071
- var buildMlPayload = (payload) => {
3072
- const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
3073
- const profileId = payload.signal?.policyProfileId;
3074
- const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
3075
- const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
3076
- const nextSignal = {
3077
- ...normalizedSignal,
3078
- indicators: {
3079
- ...normalizedSignal?.indicators ?? {}
3080
- }
3081
- };
3082
- const nextContext = payload.context ? {
3083
- ...payload.context,
3084
- strategyConfig: normalizeStrategyConfig(
3085
- payload.context.strategyConfig,
3086
- strategyName,
3087
- profileId
3088
- )
3089
- } : void 0;
3090
- return {
3091
- signal: nextSignal,
3092
- context: nextContext
3093
- };
3094
- };
3095
-
3096
- // src/runtimeJournal.ts
3097
- var import_node_crypto = require("crypto");
3098
- var import_constants = require("@tradejs/core/constants");
3099
- var import_time = require("@tradejs/core/time");
3100
- var import_trade = require("@tradejs/core/trade");
3101
- var import_logger3 = require("@tradejs/infra/logger");
3102
- var import_redis2 = require("@tradejs/infra/redis");
3103
- var now = () => Date.now();
3104
- var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
3105
- var calculateClosedPnl = ({
3106
- direction,
3107
- entryPrice,
3108
- exitPrice,
3109
- qty
3110
- }) => {
3111
- const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
3112
- return Number.isFinite(pnl) ? pnl : null;
3113
- };
3114
- var createRuntimeOrderId = (strategy) => {
3115
- const prefix = (0, import_trade.createRuntimeOrderLinkPrefix)(strategy);
3116
- if (prefix === "tjs-") {
3117
- return `tjs-${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
3118
- }
3119
- return `${prefix}${toRandomOrderSuffix()}`;
3120
- };
3121
- var recordRuntimeTradeOpen = async (params) => {
3122
- const { userName } = params;
3123
- if (!userName) {
3124
- return null;
3125
- }
3126
- const record2 = {
3127
- ...params,
3128
- entryCount: 1,
3129
- lastEntryPrice: params.entryPrice,
3130
- lastEntryQty: params.qty,
3131
- lastEntryTimestamp: params.entryTimestamp,
3132
- status: "active",
3133
- currentPrice: params.entryPrice,
3134
- currentPnl: 0,
3135
- closedPnl: null,
3136
- exitPrice: null,
3137
- exitTimestamp: null,
3138
- lastSyncedAt: now()
3139
- };
3140
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(record2.entryTimestamp);
3141
- const runtimeScopeId = record2.deploymentId ?? record2.accountId;
3142
- try {
3143
- await Promise.all([
3144
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, record2.orderId), record2, {
3145
- expire: 0
3146
- }),
3147
- (0, import_redis2.setHashJsonField)(
3148
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3149
- record2.orderId,
3150
- record2,
3151
- { expire: 0 }
3152
- ),
3153
- (0, import_redis2.setData)(
3154
- import_redis2.redisKeys.runtimeActiveTrade(userName, record2.symbol, runtimeScopeId),
3155
- { orderId: record2.orderId },
3156
- { expire: 0 }
3157
- )
3158
- ]);
3159
- } catch (error) {
3160
- import_logger3.logger.error(
3161
- "runtime trade open journal failed: %s %s",
3162
- record2.symbol,
3163
- error?.message || String(error)
3164
- );
3165
- }
3166
- return record2;
3167
- };
3168
- var recordRuntimeTradeIncrease = async (params) => {
3169
- const {
3170
- userName,
3171
- strategy,
3172
- symbol,
3173
- direction,
3174
- resultingQty,
3175
- resultingEntryPrice,
3176
- addedQty,
3177
- addedEntryPrice,
3178
- entryTimestamp,
3179
- fee,
3180
- accountId,
3181
- deploymentId
3182
- } = params;
3183
- if (!userName) {
3184
- return null;
3185
- }
3186
- const existing = await getActiveRuntimeTrade({
3187
- userName,
3188
- symbol,
3189
- accountId,
3190
- deploymentId
3191
- });
3192
- if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
3193
- return null;
3194
- }
3195
- const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
3196
- const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
3197
- const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
3198
- const next = {
3199
- ...existing,
3200
- ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
3201
- qty: resultingQty,
3202
- entryPrice: resultingEntryPrice,
3203
- entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
3204
- lastEntryPrice: addedEntryPrice,
3205
- lastEntryQty: addedQty,
3206
- lastEntryTimestamp: entryTimestamp,
3207
- currentPrice: resultingEntryPrice,
3208
- currentPnl: 0,
3209
- fee: openFee,
3210
- openFee,
3211
- totalFee,
3212
- lastSyncedAt: now()
3213
- };
3214
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3215
- try {
3216
- await Promise.all([
3217
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, existing.orderId), next, {
3218
- expire: 0
3219
- }),
3220
- (0, import_redis2.setHashJsonField)(
3221
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3222
- existing.orderId,
3223
- next,
3224
- { expire: 0 }
3225
- )
3226
- ]);
3227
- } catch (error) {
3228
- import_logger3.logger.error(
3229
- "runtime trade increase journal failed: %s %s",
3230
- symbol,
3231
- error?.message || String(error)
3232
- );
3233
- }
3234
- return next;
3796
+ ...Object.values(additionalIndicators ?? {}).map(asRecord)
3797
+ ].filter((value) => Boolean(value));
3798
+ return candidates.find(
3799
+ (candidate) => Array.isArray(candidate.approvalBlockReasons) || Array.isArray(candidate.riskAnnotations) || Array.isArray(candidate.structuralHardBlockReasons) || typeof candidate.approvalAllowedNow === "boolean"
3800
+ ) ?? null;
3235
3801
  };
3236
- var getActiveRuntimeTrade = async (params) => {
3237
- const { userName, symbol, accountId, deploymentId } = params;
3238
- if (!userName) {
3239
- return null;
3240
- }
3241
- const activeRef = await (0, import_redis2.getData)(
3242
- import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
3243
- null
3802
+ var buildAiHumanPrompt = (signal, payload = buildAiPayload(signal)) => `
3803
+ Analyze the already computed internal signal for ${signal.symbol}. The original signal direction is ${signal.direction}.
3804
+ This is a structure-classification and audit task, not execution advice. Determine whether the current structure confirms the existing signal, how structurally coherent it is right now, whether an extra confirmation level is needed, and whether the already supplied levels in \`payload.signal.prices\` still look internally coherent. Do not replace the original thesis with a new one and do not invent new levels; return only the requested JSON.
3805
+
3806
+ Trade payload:
3807
+ ${JSON.stringify(payload)}
3808
+ ${buildAiHumanPromptAddonByStrategy(signal, payload)}
3809
+ `;
3810
+ var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
3811
+ const settings = await getAiUserSettings(options.userName);
3812
+ const responseLanguage = (0, import_aiLanguages2.getAiResponseLanguagePromptName)(
3813
+ settings.AI_RESPONSE_LANGUAGE || import_aiLanguages2.DEFAULT_AI_RESPONSE_LANGUAGE
3244
3814
  );
3245
- const orderId = String(activeRef?.orderId || "").trim();
3246
- if (!orderId) {
3247
- return null;
3815
+ const response = await invokeAiPromptChat({
3816
+ userName: options.userName,
3817
+ model: options.model,
3818
+ messages: [
3819
+ { role: "system", content: systemPrompt },
3820
+ {
3821
+ role: "system",
3822
+ content: `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
3823
+ },
3824
+ { role: "user", content: humanPrompt }
3825
+ ]
3826
+ });
3827
+ const parsed = parseAIResponse(response.content);
3828
+ const normalized = normalizeAnalysis(parsed);
3829
+ if (!options.signal) {
3830
+ return normalized;
3248
3831
  }
3249
- const existing = await (0, import_redis2.getData)(
3250
- import_redis2.redisKeys.runtimeTrade(userName, orderId),
3251
- null
3832
+ return postProcessAiAnalysisByStrategy(
3833
+ options.signal,
3834
+ normalized,
3835
+ options.payload
3252
3836
  );
3253
- if (!existing || existing.status !== "active") {
3254
- await (0, import_redis2.delKey)(
3255
- import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
3256
- );
3257
- return null;
3258
- }
3259
- return existing;
3260
3837
  };
3261
- var markRuntimeTradeClosed = async (params) => {
3262
- const {
3263
- userName,
3264
- symbol,
3265
- strategy,
3266
- exitPrice,
3267
- exitTimestamp,
3268
- closedPnl,
3269
- exitType,
3270
- accountId,
3271
- deploymentId
3272
- } = params;
3273
- if (!userName) {
3274
- return null;
3275
- }
3276
- const existing = await getActiveRuntimeTrade({
3277
- userName,
3278
- symbol,
3279
- accountId,
3280
- deploymentId
3281
- });
3282
- if (!existing) {
3283
- return null;
3284
- }
3285
- const orderId = existing.orderId;
3286
- if (strategy && existing.strategy !== strategy) {
3287
- return null;
3288
- }
3289
- const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
3290
- const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
3291
- direction: existing.direction,
3292
- entryPrice: existing.entryPrice,
3293
- exitPrice: resolvedExitPrice,
3294
- qty: existing.qty
3295
- }) : existing.closedPnl ?? existing.currentPnl ?? null;
3296
- const next = {
3297
- ...existing,
3298
- status: "closed",
3299
- currentPrice: resolvedExitPrice,
3300
- currentPnl: resolvedClosedPnl,
3301
- closedPnl: resolvedClosedPnl,
3302
- exitPrice: resolvedExitPrice,
3303
- exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
3304
- exitType: exitType ?? existing.exitType ?? null,
3305
- lastSyncedAt: now()
3838
+ var runAiPromptLocal = async (signal, options = {}) => {
3839
+ const payload = options.payload ?? buildAiPayload(signal);
3840
+ const gateContext = getDeterministicAiGateContext(payload);
3841
+ const signalDirection = getSignalDirection(signal);
3842
+ const deterministicQuality = getDeterministicQuality(gateContext);
3843
+ const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
3844
+ return postProcessLocalAiAnalysisByStrategy(
3845
+ signal,
3846
+ {
3847
+ direction: approvalAllowedNow ? signalDirection : null,
3848
+ quality: deterministicQuality,
3849
+ needRetest: !approvalAllowedNow,
3850
+ retestPrice: null,
3851
+ takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
3852
+ stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
3853
+ },
3854
+ payload
3855
+ );
3856
+ };
3857
+ var askAI = async (signal, options = {}) => {
3858
+ const { symbol } = signal;
3859
+ const payload = buildAiPayload(signal);
3860
+ const content = await runAiPrompt(
3861
+ {
3862
+ systemPrompt: buildAiSystemPrompt(signal),
3863
+ humanPrompt: buildAiHumanPrompt(signal, payload)
3864
+ },
3865
+ {
3866
+ ...options,
3867
+ signal,
3868
+ payload
3869
+ }
3870
+ );
3871
+ await (0, import_redis3.setData)(import_redis3.redisKeys.analysis(symbol, signal.signalId), content);
3872
+ return content;
3873
+ };
3874
+
3875
+ // src/strategyAdapters/ml.ts
3876
+ var defaultMlAdapter = {
3877
+ normalizeStrategyConfig: (strategyConfig) => strategyConfig
3878
+ };
3879
+ var getStrategyMlAdapter = (strategy, profileId) => {
3880
+ const strategyAdapter = getStrategyProfileMlAdapter(
3881
+ getStrategyManifest(strategy),
3882
+ profileId
3883
+ );
3884
+ if (!strategyAdapter) return defaultMlAdapter;
3885
+ return {
3886
+ ...defaultMlAdapter,
3887
+ ...strategyAdapter
3888
+ };
3889
+ };
3890
+
3891
+ // src/mlPayload.ts
3892
+ var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
3893
+ return getStrategyMlAdapter(
3894
+ strategyName,
3895
+ profileId
3896
+ ).normalizeStrategyConfig?.(strategyConfig);
3897
+ };
3898
+ var buildMlPayload = (payload) => {
3899
+ const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
3900
+ const profileId = payload.signal?.policyProfileId;
3901
+ const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
3902
+ const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
3903
+ const nextSignal = {
3904
+ ...normalizedSignal,
3905
+ indicators: {
3906
+ ...normalizedSignal?.indicators ?? {}
3907
+ }
3908
+ };
3909
+ const nextContext = payload.context ? {
3910
+ ...payload.context,
3911
+ strategyConfig: normalizeStrategyConfig(
3912
+ payload.context.strategyConfig,
3913
+ strategyName,
3914
+ profileId
3915
+ )
3916
+ } : void 0;
3917
+ return {
3918
+ signal: nextSignal,
3919
+ context: nextContext
3306
3920
  };
3307
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3308
- const closeDayKey = (0, import_time.getRuntimeStorageDayKey)(next.exitTimestamp);
3309
- try {
3310
- await Promise.all([
3311
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, orderId), next, {
3312
- expire: import_constants.TTL_1M
3313
- }),
3314
- (0, import_redis2.setHashJsonField)(
3315
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3316
- orderId,
3317
- next,
3318
- { expire: import_constants.TTL_1M }
3319
- ),
3320
- (0, import_redis2.setHashJsonField)(
3321
- import_redis2.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
3322
- orderId,
3323
- next,
3324
- { expire: import_constants.TTL_1M }
3325
- ),
3326
- (0, import_redis2.delKey)(
3327
- import_redis2.redisKeys.runtimeActiveTrade(
3328
- userName,
3329
- symbol,
3330
- deploymentId ?? accountId
3331
- )
3332
- )
3333
- ]);
3334
- } catch (error) {
3335
- import_logger3.logger.error(
3336
- "runtime trade close journal failed: %s %s",
3337
- symbol,
3338
- error?.message || String(error)
3339
- );
3340
- }
3341
- return next;
3342
3921
  };
3343
3922
 
3344
3923
  // src/strategyHelpers/marketContextStages.ts
@@ -3350,7 +3929,7 @@ var import_logger4 = require("@tradejs/infra/logger");
3350
3929
  var import_strategies = require("@tradejs/core/strategies");
3351
3930
 
3352
3931
  // src/binanceBreadthUniverses.ts
3353
- var import_node_crypto2 = require("crypto");
3932
+ var import_node_crypto3 = require("crypto");
3354
3933
 
3355
3934
  // src/config/binanceBreadthUniverses.json
3356
3935
  var binanceBreadthUniverses_default = {
@@ -3589,7 +4168,7 @@ var BINANCE_BREADTH_UNIVERSE_KEYS = [
3589
4168
  "top50",
3590
4169
  "top100"
3591
4170
  ];
3592
- var fingerprint = (value, length) => (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
4171
+ var fingerprint = (value, length) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
3593
4172
  var normalizeSymbols = (symbols) => symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean);
3594
4173
  var buildBinanceBreadthUniverseSnapshot = ({
3595
4174
  rankedSymbols,
@@ -4613,7 +5192,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4613
5192
  };
4614
5193
 
4615
5194
  // src/strategyHelpers/derivativesContext.ts
4616
- var import_indicators2 = require("@tradejs/core/indicators");
5195
+ var import_indicators3 = require("@tradejs/core/indicators");
4617
5196
  var import_data2 = require("@tradejs/core/data");
4618
5197
  var import_strategies3 = require("@tradejs/core/strategies");
4619
5198
  var import_constants2 = require("@tradejs/core/constants");
@@ -4647,7 +5226,7 @@ var parseLookbackMs = () => {
4647
5226
  };
4648
5227
  var withHourlyFallbackRows = (rowsByInterval) => ({
4649
5228
  "15m": rowsByInterval["15m"] ?? [],
4650
- "1h": (0, import_indicators2.buildCoinalyzeHourlyRowsWithFallback)({
5229
+ "1h": (0, import_indicators3.buildCoinalyzeHourlyRowsWithFallback)({
4651
5230
  rows15m: rowsByInterval["15m"],
4652
5231
  fallbackRows1h: rowsByInterval["1h"]
4653
5232
  })
@@ -4775,7 +5354,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4775
5354
  const targetSymbol = normalizeSymbol(signal.symbol);
4776
5355
  const lookbackMs = parseLookbackMs();
4777
5356
  const decisionTimeMs = signal.timestamp + (0, import_data2.intervalToMs)(signal.interval);
4778
- const derivativesEndMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
5357
+ const derivativesEndMs = (0, import_indicators3.getLastClosedDerivativesBarStartMs)(
4779
5358
  decisionTimeMs,
4780
5359
  "15m"
4781
5360
  );
@@ -4790,7 +5369,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4790
5369
  });
4791
5370
  return [
4792
5371
  symbol,
4793
- (0, import_indicators2.buildDerivativesContext)({
5372
+ (0, import_indicators3.buildDerivativesContext)({
4794
5373
  symbol,
4795
5374
  direction: signal.direction,
4796
5375
  timestamp: derivativesEndMs,
@@ -4817,7 +5396,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4817
5396
  lookbackMs,
4818
5397
  ...params.abortSignal ? { signal: params.abortSignal } : {}
4819
5398
  });
4820
- const context = (0, import_indicators2.buildDerivativesContext)({
5399
+ const context = (0, import_indicators3.buildDerivativesContext)({
4821
5400
  symbol: targetSymbol,
4822
5401
  direction: signal.direction,
4823
5402
  timestamp: derivativesEndMs,
@@ -4865,7 +5444,7 @@ var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWha
4865
5444
  var import_logger7 = require("@tradejs/infra/logger");
4866
5445
 
4867
5446
  // src/hyperliquidWhaleUniverse.ts
4868
- var import_node_crypto3 = require("crypto");
5447
+ var import_node_crypto4 = require("crypto");
4869
5448
 
4870
5449
  // src/config/hyperliquidPerpUniverse.json
4871
5450
  var hyperliquidPerpUniverse_default = {
@@ -5037,7 +5616,7 @@ var hyperliquidWhales_default = {
5037
5616
  };
5038
5617
 
5039
5618
  // src/hyperliquidWhaleUniverse.ts
5040
- var fingerprint2 = (value) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
5619
+ var fingerprint2 = (value) => (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
5041
5620
  var validateUnique = (values, label) => {
5042
5621
  if (new Set(values).size !== values.length) {
5043
5622
  throw new Error(`Hyperliquid ${label} snapshot contains duplicates`);
@@ -5524,6 +6103,7 @@ var enrichSignalWithMarketContextStages = async ({
5524
6103
  })
5525
6104
  });
5526
6105
  }
6106
+ await enrichSignalWithTickerProfile(signal);
5527
6107
  const results = [];
5528
6108
  for (const stage of stages) {
5529
6109
  if (abortSignal?.aborted) break;
@@ -5571,12 +6151,12 @@ var findReplayAiAnalysis = ({
5571
6151
  direction,
5572
6152
  ai
5573
6153
  }) => {
5574
- const snapshots = ai?.replayAnalyses;
5575
- if (!Array.isArray(snapshots) || !snapshots.length) {
6154
+ const snapshots2 = ai?.replayAnalyses;
6155
+ if (!Array.isArray(snapshots2) || !snapshots2.length) {
5576
6156
  return void 0;
5577
6157
  }
5578
6158
  let best = null;
5579
- for (const snapshot2 of snapshots) {
6159
+ for (const snapshot2 of snapshots2) {
5580
6160
  if (snapshot2.symbol !== signal.symbol || snapshot2.direction !== direction || snapshot2.strategy && snapshot2.strategy !== signal.strategy) {
5581
6161
  continue;
5582
6162
  }
@@ -5736,11 +6316,13 @@ var executeEntryOrder = async ({
5736
6316
  beforePlaceOrder,
5737
6317
  recordRuntimeTrade = true,
5738
6318
  leverage,
5739
- executionLimits
6319
+ executionLimits,
6320
+ markPortfolioSubmitted,
6321
+ rejectPortfolioSubmission
5740
6322
  }) => {
5741
6323
  await beforePlaceOrder?.();
5742
6324
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
5743
- const orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
6325
+ let orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
5744
6326
  const signalTimestamp = signal.timestamp;
5745
6327
  const signalClosePrice = currentPrice;
5746
6328
  signal.orderId = orderId;
@@ -5809,9 +6391,16 @@ var executeEntryOrder = async ({
5809
6391
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
5810
6392
  orderId,
5811
6393
  signal,
5812
- ...beforeSubmit ? { beforeSubmit } : {}
6394
+ ...rejectPortfolioSubmission ? { onRejected: rejectPortfolioSubmission } : {},
6395
+ ...beforeSubmit || markPortfolioSubmitted ? {
6396
+ beforeSubmit: async (context) => {
6397
+ await beforeSubmit?.(context);
6398
+ return markPortfolioSubmitted?.(context.qty);
6399
+ }
6400
+ } : {}
5813
6401
  });
5814
6402
  const orderAckTime = Date.now();
6403
+ if (markPortfolioSubmitted && signal.orderId) orderId = signal.orderId;
5815
6404
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
5816
6405
  const currentPosition = await connector.getPosition(symbol);
5817
6406
  const fillTime = Date.now();
@@ -6797,7 +7386,7 @@ var handleProtectDecision = async ({
6797
7386
  }
6798
7387
  return decision.code;
6799
7388
  };
6800
- var executeEntryDecision = async ({
7389
+ var executeAdmittedEntryDecision = async ({
6801
7390
  connector,
6802
7391
  symbol,
6803
7392
  decision,
@@ -6811,7 +7400,9 @@ var executeEntryDecision = async ({
6811
7400
  ai,
6812
7401
  recordRuntimeJournal,
6813
7402
  invokeStageHooks,
6814
- notifyRuntimeError
7403
+ notifyRuntimeError,
7404
+ markPortfolioSubmitted,
7405
+ rejectPortfolioSubmission
6815
7406
  }) => {
6816
7407
  const signal = decision.signal;
6817
7408
  const beforePlaceOrder = async () => {
@@ -6858,6 +7449,8 @@ var executeEntryDecision = async ({
6858
7449
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
6859
7450
  signal,
6860
7451
  beforePlaceOrder,
7452
+ markPortfolioSubmitted,
7453
+ rejectPortfolioSubmission,
6861
7454
  recordRuntimeTrade: recordRuntimeJournal,
6862
7455
  executionLimits: resolveEntryExecutionLimits(
6863
7456
  hookCtx.strategyConfig,
@@ -6998,6 +7591,37 @@ var executeEntryDecision = async ({
6998
7591
  }
6999
7592
  return signal ?? decision.code;
7000
7593
  };
7594
+ async function executeEntryDecision(params) {
7595
+ if (!params.portfolio) return executeAdmittedEntryDecision(params);
7596
+ const { decision, portfolio } = params;
7597
+ const signal = decision.signal;
7598
+ if (!signal || decision.orderPlan.positionIntent === "increase")
7599
+ return "PORTFOLIO_REQUIRES_NEW_SIGNAL_POSITION";
7600
+ signal.orderId ??= createRuntimeOrderId(signal.strategy);
7601
+ const result = await portfolio.submit(
7602
+ params.portfolioTime ?? decision.entryContext.timestamp,
7603
+ {
7604
+ id: signal.orderId,
7605
+ signalId: signal.signalId,
7606
+ strategy: signal.strategy,
7607
+ symbol: params.symbol,
7608
+ direction: decision.entryContext.direction,
7609
+ qty: decision.orderPlan.qty,
7610
+ timestamp: decision.entryContext.timestamp
7611
+ },
7612
+ (markPortfolioSubmitted, rejectPortfolioSubmission) => executeAdmittedEntryDecision({
7613
+ ...params,
7614
+ markPortfolioSubmitted,
7615
+ rejectPortfolioSubmission
7616
+ })
7617
+ );
7618
+ if (result.reason) {
7619
+ signal.orderStatus = "failed";
7620
+ signal.orderFailureReason = result.reason;
7621
+ return signal;
7622
+ }
7623
+ return result.value;
7624
+ }
7001
7625
 
7002
7626
  // src/strategyRuntime.ts
7003
7627
  var cloneWithPropertyDescriptors = (value) => Object.create(
@@ -7025,6 +7649,7 @@ var createStrategyRuntime = ({
7025
7649
  };
7026
7650
  const creator = async ({
7027
7651
  userName,
7652
+ connectorName,
7028
7653
  config: baseConfig,
7029
7654
  symbol,
7030
7655
  universe: requestedUniverse,
@@ -7063,6 +7688,37 @@ var createStrategyRuntime = ({
7063
7688
  const projectConfig = await loadTradejsConfig(projectRoot);
7064
7689
  const projectHooks = projectConfig.hooks;
7065
7690
  const env = String(config.ENV ?? "BACKTEST");
7691
+ let portfolioMarket;
7692
+ const portfolio = getPortfolioRuntime({
7693
+ symbol,
7694
+ canClose: async (position, intent, asOf) => {
7695
+ if (!portfolioMarket || portfolioMarket.candle.timestamp > asOf || !isRuntimeOrderExecutionEnabled({ config, env, connector }))
7696
+ return false;
7697
+ const gate = await authorizeClose(
7698
+ {
7699
+ kind: "exit",
7700
+ code: intent.reason,
7701
+ closePlan: {
7702
+ direction: position.direction,
7703
+ price: position.markPrice,
7704
+ timestamp: asOf
7705
+ }
7706
+ },
7707
+ portfolioMarket,
7708
+ position.strategy
7709
+ );
7710
+ return gate?.allow !== false;
7711
+ },
7712
+ connectorName,
7713
+ value: baseConfig?.PORTFOLIO_POLICY ?? config.PORTFOLIO_POLICY,
7714
+ strategy: strategyName,
7715
+ connector,
7716
+ env,
7717
+ userName,
7718
+ accountId,
7719
+ deploymentId
7720
+ });
7721
+ const portfolioContext = portfolio ? (now2) => isRuntimeOrderExecutionEnabled({ config, env, connector }) ? portfolio.tick(now2) : portfolio.context(now2) : void 0;
7066
7722
  const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
7067
7723
  const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
7068
7724
  if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
@@ -7227,7 +7883,10 @@ var createStrategyRuntime = ({
7227
7883
  return invokeHook(stage, hook, params, errorContext);
7228
7884
  };
7229
7885
  const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
7230
- const failureResult = stage === "beforeEntryGate" ? { allow: false, reason: "ERROR" } : void 0;
7886
+ const failureResult = {
7887
+ allow: false,
7888
+ reason: "ERROR"
7889
+ };
7231
7890
  const projectResults = await invokeProjectHooks(
7232
7891
  stage,
7233
7892
  params,
@@ -7248,6 +7907,12 @@ var createStrategyRuntime = ({
7248
7907
  failureResult
7249
7908
  );
7250
7909
  };
7910
+ const authorizeClose = (decision, market, owner = strategyName) => invokeGateHooks(
7911
+ "beforeClosePosition",
7912
+ resolveManifest(owner)?.hooks?.beforeClosePosition,
7913
+ { ctx: getHookCtx(owner), market, decision },
7914
+ { decision, market }
7915
+ );
7251
7916
  const applyProjectAfterCoreDecisionHooks = async ({
7252
7917
  hookCtx,
7253
7918
  market,
@@ -7361,6 +8026,20 @@ var createStrategyRuntime = ({
7361
8026
  interval: decisionInterval
7362
8027
  }) => {
7363
8028
  if (!baseContext) return void 0;
8029
+ const profileSource = strategyManifest?.contextRequirements?.tickerProfiles;
8030
+ if (profileSource) {
8031
+ const profile = await loadTickerProfileContext({
8032
+ ...profileSource,
8033
+ projectRoot,
8034
+ symbol: decisionSymbol,
8035
+ timestamp: candle.timestamp
8036
+ });
8037
+ if (!profile)
8038
+ throw new Error(
8039
+ "Required ticker profile is unavailable at decision time"
8040
+ );
8041
+ baseContext = withTickerProfile(baseContext, profile);
8042
+ }
7364
8043
  if (!coreContextRequirements.has("hyperliquidWhales")) {
7365
8044
  return baseContext;
7366
8045
  }
@@ -7528,6 +8207,8 @@ var createStrategyRuntime = ({
7528
8207
  runtime: pending2.runtime
7529
8208
  });
7530
8209
  return executeEntryDecision({
8210
+ portfolio,
8211
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
7531
8212
  connector,
7532
8213
  symbol,
7533
8214
  decision: pending2.decision,
@@ -7545,6 +8226,11 @@ var createStrategyRuntime = ({
7545
8226
  });
7546
8227
  };
7547
8228
  const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
8229
+ portfolioMarket = { candle, btcCandle };
8230
+ if (portfolioContext)
8231
+ await portfolioContext(
8232
+ env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now()
8233
+ );
7548
8234
  appendCurrentMarketData(candle, btcCandle, options.ethCandle);
7549
8235
  const ethCandle = resolveEthCandle(candle, options.ethCandle);
7550
8236
  indicatorsState.setCurrentBar(candle, btcCandle, ethCandle);
@@ -7647,15 +8333,10 @@ var createStrategyRuntime = ({
7647
8333
  if (!makeOrdersEnabled) {
7648
8334
  return decision.code;
7649
8335
  }
7650
- const closeGate = await invokeGateHooks(
7651
- "beforeClosePosition",
7652
- decisionManifest?.hooks?.beforeClosePosition,
7653
- {
7654
- ctx: decisionHookCtx,
7655
- market,
7656
- decision
7657
- },
7658
- { decision, market }
8336
+ const closeGate = await authorizeClose(
8337
+ decision,
8338
+ market,
8339
+ decisionHookCtx.strategyName
7659
8340
  );
7660
8341
  if (closeGate?.allow === false) {
7661
8342
  return closeGate.reason ? `CLOSE_BLOCKED_BY_HOOK:${closeGate.reason}` : "CLOSE_BLOCKED_BY_HOOK";
@@ -7715,6 +8396,13 @@ var createStrategyRuntime = ({
7715
8396
  });
7716
8397
  const signal = decision.signal;
7717
8398
  if (signal) {
8399
+ if (portfolio)
8400
+ signal.additionalIndicators = {
8401
+ ...signal.additionalIndicators,
8402
+ portfolioContext: await portfolioContext(
8403
+ env === "BACKTEST" || env === "PARITY" ? decision.entryContext.timestamp : Date.now()
8404
+ )
8405
+ };
7718
8406
  if (runtimeLineage) signal.runtimeLineage = runtimeLineage;
7719
8407
  if (universe) signal.universe = universe;
7720
8408
  if (assetClass) signal.assetClass = assetClass;
@@ -7954,6 +8642,8 @@ var createStrategyRuntime = ({
7954
8642
  });
7955
8643
  }
7956
8644
  return executeEntryDecision({
8645
+ portfolio,
8646
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
7957
8647
  connector,
7958
8648
  symbol,
7959
8649
  decision,
@@ -8065,7 +8755,7 @@ var normalizeForCanonicalJson = (value) => {
8065
8755
  }
8066
8756
  return value;
8067
8757
  };
8068
- var revision = (prefix, value) => `${prefix}:${(0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(normalizeForCanonicalJson(value))).digest("hex").slice(0, 16)}`;
8758
+ var revision = (prefix, value) => `${prefix}:${(0, import_node_crypto5.createHash)("sha256").update(JSON.stringify(normalizeForCanonicalJson(value))).digest("hex").slice(0, 16)}`;
8069
8759
  var computeStrategyRevision = ({
8070
8760
  strategyName,
8071
8761
  strategyPackage,
@@ -8669,13 +9359,13 @@ var assertCurrentRuntimeEvidenceArtifact = (artifact) => {
8669
9359
  throw new Error(`Runtime evidence payload ${field} must be an array`);
8670
9360
  }
8671
9361
  }
8672
- const snapshots = new Map(
9362
+ const snapshots2 = new Map(
8673
9363
  activeStrategies.map((strategy) => [strategy.strategyName, strategy])
8674
9364
  );
8675
9365
  const assertRows = (label, rows, lineageField) => {
8676
9366
  for (const row of rows) {
8677
9367
  const strategyName = String(row.strategy ?? "").trim();
8678
- const strategy = snapshots.get(strategyName);
9368
+ const strategy = snapshots2.get(strategyName);
8679
9369
  if (!strategy || row.deploymentId !== deployment.id || row.accountId !== deployment.accountId || !runtimeLineageMatchesStrategySnapshot({
8680
9370
  lineage: isRecord2(row[lineageField]) ? row[lineageField] : void 0,
8681
9371
  deployment,
@@ -8849,7 +9539,7 @@ var RUNTIME_EVIDENCE_MANIFEST_FILE = "manifest.json";
8849
9539
  var RUNTIME_EVIDENCE_PAYLOAD_FILE = "runtime-evidence.json";
8850
9540
  var RUNTIME_EVIDENCE_COMPLETE_FILE = ".complete";
8851
9541
  var asRecord2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
8852
- var sha256 = (value) => (0, import_node_crypto5.createHash)("sha256").update(value).digest("hex");
9542
+ var sha256 = (value) => (0, import_node_crypto6.createHash)("sha256").update(value).digest("hex");
8853
9543
  var runtimeEvidenceIdentitySha256 = (artifact) => {
8854
9544
  const identityArtifact = { ...artifact };
8855
9545
  delete identityArtifact.generatedAt;
@@ -8966,7 +9656,7 @@ var publishRuntimeEvidenceBundle = async ({
8966
9656
  const bundleDir = import_node_path3.default.join(readyRoot, artifactId);
8967
9657
  const temporaryDir = import_node_path3.default.join(
8968
9658
  incomingRoot,
8969
- `${artifactId}.tmp-${(0, import_node_crypto5.randomUUID)()}`
9659
+ `${artifactId}.tmp-${(0, import_node_crypto6.randomUUID)()}`
8970
9660
  );
8971
9661
  await import_promises3.default.mkdir(temporaryDir, { recursive: true });
8972
9662
  try {
@@ -9067,11 +9757,11 @@ var discoverRuntimeEvidenceBundles = async (rootDir) => {
9067
9757
  };
9068
9758
 
9069
9759
  // src/diagnostics/runtimeEvidenceProducer.ts
9070
- var import_node_crypto6 = require("crypto");
9760
+ var import_node_crypto7 = require("crypto");
9071
9761
  var import_promises4 = __toESM(require("fs/promises"));
9072
9762
  var import_node_path4 = __toESM(require("path"));
9073
9763
  var isRecord3 = (value) => value != null && typeof value === "object" && !Array.isArray(value);
9074
- var sha2562 = (value) => (0, import_node_crypto6.createHash)("sha256").update(value).digest("hex");
9764
+ var sha2562 = (value) => (0, import_node_crypto7.createHash)("sha256").update(value).digest("hex");
9075
9765
  var parseRuntimeEvidenceProducer = (value) => {
9076
9766
  const producer = isRecord3(value) ? value : null;
9077
9767
  const runtimePackageManifest = isRecord3(producer?.runtimePackageManifest) ? producer.runtimePackageManifest : null;
@@ -9126,7 +9816,7 @@ var resolveRuntimeEvidenceProducer = async ({
9126
9816
  };
9127
9817
 
9128
9818
  // src/diagnostics/runtimeFeedbackArtifacts.ts
9129
- var import_node_crypto7 = require("crypto");
9819
+ var import_node_crypto8 = require("crypto");
9130
9820
  var import_promises5 = __toESM(require("fs/promises"));
9131
9821
  var import_node_path5 = __toESM(require("path"));
9132
9822
  var RUNTIME_FEEDBACK_MANIFEST_FILE = "manifest.json";
@@ -9134,7 +9824,7 @@ var RUNTIME_FEEDBACK_REPLAY_FILE = "replay-runtime-evidence.json";
9134
9824
  var RUNTIME_FEEDBACK_LOG_FILE = "replay.log";
9135
9825
  var RUNTIME_FEEDBACK_COMPLETE_FILE = ".complete";
9136
9826
  var isRecord4 = (value) => value != null && typeof value === "object" && !Array.isArray(value);
9137
- var sha2563 = (value) => (0, import_node_crypto7.createHash)("sha256").update(value).digest("hex");
9827
+ var sha2563 = (value) => (0, import_node_crypto8.createHash)("sha256").update(value).digest("hex");
9138
9828
  var safeSegment2 = (value, fallback) => {
9139
9829
  const normalized = value.trim().replace(/[^a-zA-Z0-9._-]+/g, "-");
9140
9830
  return normalized && normalized !== "." && normalized !== ".." ? normalized : fallback;
@@ -9409,7 +10099,7 @@ var assertMcpFeedbackContract = (version, tools) => {
9409
10099
  };
9410
10100
 
9411
10101
  // src/diagnostics/mcpBundleDownload.ts
9412
- var import_node_crypto8 = require("crypto");
10102
+ var import_node_crypto9 = require("crypto");
9413
10103
  var import_promises6 = __toESM(require("fs/promises"));
9414
10104
  var import_node_path6 = __toESM(require("path"));
9415
10105
  var McpReadTransportError = class extends Error {
@@ -9419,7 +10109,7 @@ var McpReadTransportError = class extends Error {
9419
10109
  }
9420
10110
  };
9421
10111
  var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
9422
- var digest = (bytes) => (0, import_node_crypto8.createHash)("sha256").update(bytes).digest("hex");
10112
+ var digest = (bytes) => (0, import_node_crypto9.createHash)("sha256").update(bytes).digest("hex");
9423
10113
  var exists = async (file) => import_promises6.default.lstat(file).catch((error) => {
9424
10114
  if (error.code === "ENOENT") return void 0;
9425
10115
  throw error;