@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.
package/dist/backtest.js CHANGED
@@ -795,1647 +795,2425 @@ module.exports = __toCommonJS(backtest_exports);
795
795
  __reExport(backtest_exports, require("@tradejs/core/backtest"), module.exports);
796
796
 
797
797
  // src/testing.ts
798
- var import_indicators3 = require("@tradejs/core/indicators");
798
+ var import_indicators4 = require("@tradejs/core/indicators");
799
799
  var import_constants9 = require("@tradejs/core/constants");
800
800
  var import_strategies7 = require("@tradejs/core/strategies");
801
801
  var import_time3 = require("@tradejs/core/time");
802
802
  var import_logger15 = require("@tradejs/infra/logger");
803
803
 
804
- // src/strategyRuntime.ts
805
- var import_jev6 = require("@tradejs/core/jev");
804
+ // src/strategy/portfolioRuntime.ts
805
+ var import_node_crypto2 = require("crypto");
806
+ var import_portfolio2 = require("@tradejs/core/portfolio");
807
+ var import_redis2 = require("@tradejs/infra/redis");
806
808
 
807
- // src/jev.ts
808
- var import_promises = __toESM(require("fs/promises"));
809
- var import_node_path = __toESM(require("path"));
810
- var import_jev4 = require("@tradejs/core/jev");
811
- var import_jev5 = require("@tradejs/infra/jev");
812
- var import_userSettings = require("@tradejs/infra/userSettings");
813
- init_jevInput();
814
- init_jevInput();
815
- init_jevTraining();
816
- var resolveJevProvider = async (userName) => {
817
- const settings = await (0, import_userSettings.getUserSettings)(userName);
818
- const provider = {
819
- endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
820
- model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
821
- };
822
- (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
823
- if (!settings.JEV_API_KEY)
824
- throw new Error("Configure the Jev API key in Account settings");
825
- return provider;
826
- };
827
- var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
828
- var pending = /* @__PURE__ */ new Map();
829
- var pendingResponses = /* @__PURE__ */ new Map();
830
- var evaluateJevInput = async ({
831
- input,
832
- config,
833
- userName,
834
- projectRoot
835
- }) => {
836
- config = (0, import_jev4.parseJevConfig)(config);
837
- const inputHash = (0, import_jev5.jevHash)(input);
838
- const questions = questionsForJevInput(input);
839
- const questionsHash = (0, import_jev5.jevHash)(questions);
840
- const base = {
841
- questionSet: import_jev4.JEV_QUESTION_SET,
842
- schema: "tradejs-signal-assessment/v5",
843
- source: config.source,
844
- mode: config.mode,
845
- inputHash,
846
- questionsHash,
847
- geometryStatus: input.geometryStatus,
848
- levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
849
- };
850
- if (!input.questions.length)
851
- return {
852
- assessment: {
853
- ...base,
854
- status: "unavailable",
855
- model: "",
856
- scores: emptyScores(),
857
- confidence: emptyScores()
858
- }
859
- };
860
- if (config.source === "local") {
861
- const contents = await import_promises.default.readFile(
862
- import_node_path.default.resolve(projectRoot, config.modelFile),
863
- "utf8"
864
- );
865
- if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
866
- throw new Error("Local Jev gate checksum mismatch");
867
- const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
868
- const model = validateJevGateModel2(JSON.parse(contents));
869
- if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
870
- throw new Error("Local Jev gate strategy or question version mismatch");
871
- const scores = Object.fromEntries(
872
- import_jev4.JEV_DIMENSIONS.map((key) => [
873
- key,
874
- input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
875
- ])
876
- );
877
- return {
878
- assessment: {
879
- ...base,
880
- status: "available",
881
- model: config.modelSha256,
882
- scores,
883
- confidence: emptyScores()
809
+ // src/portfolio.ts
810
+ var import_portfolio = require("@tradejs/core/portfolio");
811
+ function createPortfolioCoordinator({
812
+ policy,
813
+ strategy,
814
+ store,
815
+ execution
816
+ }) {
817
+ const fingerprint3 = (0, import_portfolio.portfolioFingerprint)(policy, strategy);
818
+ let cache;
819
+ async function transact(now2, change, force = false) {
820
+ for (let attempt = 0; attempt < 16; attempt++) {
821
+ const previous = await store.read();
822
+ if (previous && previous.fingerprint !== fingerprint3)
823
+ throw new Error(
824
+ "Portfolio policy changed under existing state; reconcile the persisted scope before changing configuration"
825
+ );
826
+ const base = previous ?? (0, import_portfolio.createPortfolioState)(fingerprint3);
827
+ const bucket = Math.floor(now2 / policy.observationIntervalMs);
828
+ const observation = !force && cache?.revision === base.revision && cache.bucket === bucket ? cache.observation : await execution.observe(now2, base.reservations);
829
+ const reconciled = (0, import_portfolio.reconcilePortfolio)(
830
+ policy,
831
+ strategy,
832
+ base,
833
+ observation
834
+ );
835
+ for (const intent of base.closing) {
836
+ if (!observation.positions.some(
837
+ (p) => p.symbol === intent.position.symbol
838
+ ))
839
+ await execution.confirmClosed?.(intent, observation.asOf);
884
840
  }
885
- };
886
- }
887
- const provider = config.provider ?? await resolveJevProvider(userName);
888
- const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
889
- const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
890
- const file = import_node_path.default.join(dir, "records", `${id}.json`);
891
- const getRecord = async () => {
892
- const existing = await (0, import_jev5.readJevArtifact)(file);
893
- if (existing) {
894
- 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))
895
- throw new Error("Jev recording identity mismatch");
896
- (0, import_jev5.validateJevResponse)(existing.response, input.questions);
897
- for (const key of import_jev4.JEV_DIMENSIONS) {
898
- const expected = input.questions.includes(key) ? existing.response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null;
899
- if (existing.scores[key] !== expected)
900
- throw new Error("Jev recording scores do not match the response");
841
+ const changed = change(reconciled, observation);
842
+ const next = { ...changed.state, revision: base.revision + 1 };
843
+ if (await store.compare(previous, next)) {
844
+ cache = { revision: next.revision, bucket, observation };
845
+ return { state: next, observation, result: changed.result };
901
846
  }
902
- return existing;
847
+ cache = void 0;
903
848
  }
904
- if (config.source === "recorded")
905
- throw new Error(
906
- `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.`
849
+ throw new Error("Portfolio state contention; entry was not submitted");
850
+ }
851
+ async function tick(now2, force = false) {
852
+ const claimed = await transact(
853
+ now2,
854
+ (state, observation) => {
855
+ const commands = (0, import_portfolio.portfolioCloseCommands)(policy, state, observation);
856
+ return {
857
+ state: {
858
+ ...state,
859
+ closing: state.closing.map(
860
+ (c) => commands.some((x) => x.intent.positionId === c.positionId) ? { ...c, lastAttemptAt: observation.asOf } : c
861
+ )
862
+ },
863
+ result: commands
864
+ };
865
+ },
866
+ force
867
+ );
868
+ for (const command of claimed.result)
869
+ await execution.close(
870
+ command.position,
871
+ command.intent,
872
+ claimed.observation.asOf
907
873
  );
908
- const settings = await (0, import_userSettings.getUserSettings)(userName);
909
- if (!settings.JEV_API_KEY)
910
- throw new Error("Configure the Jev API key in Account settings");
911
- const apiKey = settings.JEV_API_KEY;
912
- const configuredProvider = await resolveJevProvider(userName);
913
- if (configuredProvider.endpoint !== provider.endpoint)
914
- throw new Error(
915
- "Jev account endpoint changed; update the frozen provider configuration"
874
+ if (claimed.result.length) {
875
+ cache = void 0;
876
+ const refreshed = await transact(
877
+ now2,
878
+ (state) => ({ state, result: void 0 }),
879
+ true
880
+ );
881
+ return (0, import_portfolio.buildPortfolioContext)(
882
+ policy,
883
+ strategy,
884
+ refreshed.state,
885
+ refreshed.observation
916
886
  );
917
- const started = Date.now();
918
- const state = stateForJevInput(input);
919
- const responseId = (0, import_jev5.jevHash)({
920
- state,
921
- questionsHash,
922
- provider,
923
- user: (0, import_jev5.jevHash)(userName)
924
- });
925
- const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
926
- let responsePromise = pendingResponses.get(responseFile);
927
- if (!responsePromise) {
928
- responsePromise = (async () => {
929
- const saved = await (0, import_jev5.readJevArtifact)(responseFile);
930
- if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
931
- const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
932
- await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
933
- return fresh;
934
- })();
935
- pendingResponses.set(responseFile, responsePromise);
936
- }
937
- let response;
938
- try {
939
- response = await responsePromise;
940
- } finally {
941
- pendingResponses.delete(responseFile);
942
887
  }
943
- const scores = Object.fromEntries(
944
- import_jev4.JEV_DIMENSIONS.map((key) => [
945
- key,
946
- input.questions.includes(key) ? response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null
947
- ])
888
+ return (0, import_portfolio.buildPortfolioContext)(
889
+ policy,
890
+ strategy,
891
+ claimed.state,
892
+ claimed.observation
948
893
  );
949
- const value = {
950
- schema: "tradejs-jev-record/v4",
951
- id,
952
- inputHash,
953
- questionsHash,
954
- provider,
955
- input,
956
- response,
957
- scores,
958
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
959
- elapsedMs: Date.now() - started
960
- };
961
- await (0, import_jev5.writeJevArtifact)(file, value);
962
- return await (0, import_jev5.readJevArtifact)(file);
963
- };
964
- const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
965
- let promise = pending.get(pendingKey);
966
- if (!promise) {
967
- promise = getRecord();
968
- pending.set(pendingKey, promise);
969
894
  }
970
- let record;
971
- try {
972
- record = await promise;
973
- } finally {
974
- pending.delete(pendingKey);
895
+ async function updateReservation(now2, id, submitted, qty) {
896
+ return transact(
897
+ now2,
898
+ (s, o) => {
899
+ const exists = s.reservations.some((r) => r.id === id);
900
+ const blocked = submitted && (!exists || s.closing.length > 0 || o.asOf <= s.blockedThrough);
901
+ return {
902
+ state: {
903
+ ...s,
904
+ reservations: s.reservations.flatMap(
905
+ (r) => r.id !== id ? [r] : submitted && !blocked ? [{ ...r, submitted: true, qty: qty ?? r.qty }] : []
906
+ )
907
+ },
908
+ result: !blocked
909
+ };
910
+ },
911
+ true
912
+ );
975
913
  }
976
914
  return {
977
- record,
978
- assessment: {
979
- ...base,
980
- status: "available",
981
- recordId: id,
982
- model: record.response.model,
983
- scores: record.scores,
984
- confidence: Object.fromEntries(
985
- import_jev4.JEV_DIMENSIONS.map((key) => [
986
- key,
987
- record.response.answers[key]?.confidence ?? null
988
- ])
989
- )
915
+ async context(now2) {
916
+ const state = await store.read() ?? (0, import_portfolio.createPortfolioState)(fingerprint3);
917
+ if (state.fingerprint !== fingerprint3)
918
+ throw new Error("Portfolio policy changed under existing state");
919
+ const observation = await execution.observe(now2, state.reservations);
920
+ (0, import_portfolio.validatePortfolioObservation)(observation);
921
+ return (0, import_portfolio.buildPortfolioContext)(policy, strategy, state, observation);
922
+ },
923
+ tick,
924
+ async submit(now2, entry, send) {
925
+ await tick(now2);
926
+ const admitted = await transact(
927
+ now2,
928
+ (s, o) => {
929
+ const r = (0, import_portfolio.reservePortfolioEntry)(policy, strategy, s, o, {
930
+ ...entry,
931
+ submitted: false,
932
+ oppositeHandled: false
933
+ });
934
+ return { state: r.state, result: r.reason };
935
+ },
936
+ true
937
+ );
938
+ if (admitted.result) return { reason: admitted.result };
939
+ let submitted = false;
940
+ let retry = 0;
941
+ let attemptId = entry.id;
942
+ let needsReservation = false;
943
+ try {
944
+ const value = await send(
945
+ async (qty) => {
946
+ if (!Number.isFinite(qty) || qty <= 0)
947
+ throw new Error("Invalid submitted portfolio quantity");
948
+ if (needsReservation) {
949
+ attemptId = `${entry.id.slice(0, 30)}r${++retry}`;
950
+ const admission = await transact(
951
+ now2,
952
+ (s, o) => {
953
+ const r = (0, import_portfolio.reservePortfolioEntry)(policy, strategy, s, o, {
954
+ ...entry,
955
+ id: attemptId,
956
+ qty,
957
+ submitted: true,
958
+ oppositeHandled: false
959
+ });
960
+ return { state: r.state, result: r.reason };
961
+ },
962
+ true
963
+ );
964
+ if (admission.result) throw new Error(admission.result);
965
+ needsReservation = false;
966
+ submitted = true;
967
+ return attemptId;
968
+ }
969
+ const update = await updateReservation(now2, attemptId, true, qty);
970
+ if (!update.result)
971
+ throw new Error("PORTFOLIO_SUBMISSION_NO_LONGER_ADMITTED");
972
+ submitted = true;
973
+ return attemptId;
974
+ },
975
+ async (retrying = false) => {
976
+ await transact(
977
+ now2,
978
+ (s) => ({
979
+ state: {
980
+ ...s,
981
+ reservations: s.reservations.filter(
982
+ (r) => r.id !== attemptId
983
+ ),
984
+ completedSignals: {
985
+ ...Object.fromEntries(
986
+ Object.entries(s.completedSignals).filter(
987
+ ([id]) => !retrying || id !== entry.signalId
988
+ )
989
+ ),
990
+ ...retrying ? {} : { [entry.signalId]: now2 }
991
+ }
992
+ },
993
+ result: void 0
994
+ }),
995
+ true
996
+ );
997
+ submitted = false;
998
+ needsReservation = retrying;
999
+ }
1000
+ );
1001
+ if (!submitted) await updateReservation(now2, attemptId, false);
1002
+ else await tick(now2, true);
1003
+ return { value };
1004
+ } catch (error) {
1005
+ if (!submitted) await updateReservation(now2, attemptId, false);
1006
+ throw error;
1007
+ }
990
1008
  }
991
1009
  };
992
- };
993
- var assessSignalWithJev = async ({
994
- signal,
995
- config,
996
- userName,
997
- projectRoot,
998
- strict,
999
- scope = "runtime"
1000
- }) => {
1001
- let assessment;
1002
- try {
1003
- const input = buildJevInput(signal);
1004
- ({ assessment } = await evaluateJevInput({
1005
- input,
1006
- config,
1007
- userName,
1008
- projectRoot
1009
- }));
1010
- } catch (error) {
1011
- if (strict) throw error;
1012
- assessment = {
1013
- questionSet: import_jev4.JEV_QUESTION_SET,
1014
- schema: "tradejs-signal-assessment/v5",
1015
- source: config.source,
1016
- mode: config.mode,
1017
- status: "unavailable",
1018
- inputHash: "",
1019
- questionsHash: "",
1020
- geometryStatus: "absent",
1021
- levelsValid: null,
1022
- model: config.provider?.model ?? config.modelSha256 ?? "",
1023
- scores: emptyScores(),
1024
- confidence: emptyScores()
1025
- };
1026
- }
1027
- signal.assessment = assessment;
1028
- signal.additionalIndicators = {
1029
- ...signal.additionalIndicators,
1030
- jev: (0, import_jev4.jevAssessmentFeatures)(assessment)
1010
+ }
1011
+ function createMemoryPortfolioStore() {
1012
+ let state = null;
1013
+ return {
1014
+ read: async () => state === null ? null : structuredClone(state),
1015
+ compare: async (expected, next) => {
1016
+ if ((state?.revision ?? -1) !== (expected?.revision ?? -1)) return false;
1017
+ state = structuredClone(next);
1018
+ return true;
1019
+ }
1031
1020
  };
1032
- const identity = {
1033
- signalId: signal.signalId,
1034
- strategy: signal.strategy,
1035
- symbol: signal.symbol,
1036
- timestamp: signal.timestamp,
1037
- scope,
1038
- user: (0, import_jev5.jevHash)(userName),
1039
- config,
1040
- assessment
1041
- };
1042
- await (0, import_jev5.writeJevArtifact)(
1043
- import_node_path.default.resolve(
1044
- projectRoot,
1045
- config.recordsDir ?? "data/ai/jev",
1046
- "decisions",
1047
- `${(0, import_jev5.jevHash)(identity)}.json`
1048
- ),
1049
- identity
1050
- );
1051
- return assessment;
1052
- };
1053
-
1054
- // src/strategyRuntime.ts
1055
- var import_constants5 = require("@tradejs/core/constants");
1056
- var import_strategies6 = require("@tradejs/core/strategies");
1057
- var import_logger11 = require("@tradejs/infra/logger");
1058
-
1059
- // src/strategyHelpers/runtime.ts
1060
- var import_logger9 = require("@tradejs/infra/logger");
1061
- var import_constants3 = require("@tradejs/core/constants");
1062
- var import_ml2 = require("@tradejs/infra/ml");
1021
+ }
1063
1022
 
1064
- // src/strategy/entrySubmissionValidation.ts
1023
+ // src/runtimeJournal.ts
1024
+ var import_node_crypto = require("crypto");
1025
+ var import_constants = require("@tradejs/core/constants");
1026
+ var import_time = require("@tradejs/core/time");
1027
+ var import_trade = require("@tradejs/core/trade");
1065
1028
  var import_logger = require("@tradejs/infra/logger");
1066
-
1067
- // src/strategy/entryExecutionValidation.ts
1068
- var EntrySubmissionValidationError = class extends Error {
1069
- constructor() {
1070
- super(...arguments);
1071
- this.name = "EntrySubmissionValidationError";
1072
- }
1073
- };
1074
- var resolveEntryExecutionLimits = (config, direction) => {
1075
- const side = config[direction];
1076
- const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
1077
- const maximum = config.MAX_LOSS_VALUE;
1078
- if (minimum == null && maximum == null) return void 0;
1079
- const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
1080
- const maxLossValue = maximum == null ? void 0 : Number(maximum);
1081
- const feeRate = Number(config.RISK_FEE_RATE ?? 0);
1082
- const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
1083
- const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
1084
- const slippageBps = slippage + impact;
1085
- 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)
1086
- throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
1087
- return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
1088
- };
1089
- var validateEntryExecution = ({
1029
+ var import_redis = require("@tradejs/infra/redis");
1030
+ var now = () => Date.now();
1031
+ var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
1032
+ var calculateClosedPnl = ({
1090
1033
  direction,
1091
- price,
1092
- qty,
1093
- stopLossPrice,
1094
- takeProfitPrice,
1095
- limits
1034
+ entryPrice,
1035
+ exitPrice,
1036
+ qty
1096
1037
  }) => {
1097
- if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
1098
- throw new EntrySubmissionValidationError(
1099
- "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1100
- );
1101
- const side = direction === "LONG" ? 1 : -1;
1102
- if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
1103
- throw new EntrySubmissionValidationError(
1104
- "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
1105
- );
1106
- const slippage = limits.slippageBps / 1e4;
1107
- const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
1108
- const entryCost = cost(price, side);
1109
- const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
1110
- const rewardPerUnit = Math.max(
1111
- 0,
1112
- Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
1113
- );
1114
- const netRiskRatio = rewardPerUnit / lossPerUnit;
1115
- const plannedLossValue = qty * lossPerUnit;
1116
- if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
1117
- throw new EntrySubmissionValidationError(
1118
- "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1119
- );
1120
- if (limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio)
1121
- throw new EntrySubmissionValidationError(
1122
- "ENTRY_RR_BELOW_MIN_AT_SUBMISSION"
1123
- );
1124
- const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
1125
- return { netRiskRatio, plannedLossValue, riskExceeded };
1038
+ const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
1039
+ return Number.isFinite(pnl) ? pnl : null;
1126
1040
  };
1127
-
1128
- // src/strategy/entrySubmissionValidation.ts
1129
- var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
1130
- var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
1131
- var getOrderArrivalSnapshot = async ({
1132
- connector,
1133
- symbol,
1134
- requireTimestamp = false
1135
- }) => {
1136
- if (typeof connector.getTopOfBookTicker !== "function") {
1137
- return {
1138
- arrivalSnapshotTime: Date.now(),
1139
- arrivalSource: "unavailable",
1140
- bid: null,
1141
- ask: null,
1142
- arrivalMid: null,
1143
- spreadBps: null
1144
- };
1041
+ var createRuntimeOrderId = (strategy) => {
1042
+ const prefix = (0, import_trade.createRuntimeOrderLinkPrefix)(strategy);
1043
+ if (prefix === "tjs-") {
1044
+ return `tjs-${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
1045
+ }
1046
+ return `${prefix}${toRandomOrderSuffix()}`;
1047
+ };
1048
+ var recordRuntimeTradeOpen = async (params) => {
1049
+ const { userName } = params;
1050
+ if (!userName) {
1051
+ return null;
1145
1052
  }
1053
+ const record = {
1054
+ ...params,
1055
+ entryCount: 1,
1056
+ lastEntryPrice: params.entryPrice,
1057
+ lastEntryQty: params.qty,
1058
+ lastEntryTimestamp: params.entryTimestamp,
1059
+ status: "active",
1060
+ currentPrice: params.entryPrice,
1061
+ currentPnl: 0,
1062
+ closedPnl: null,
1063
+ exitPrice: null,
1064
+ exitTimestamp: null,
1065
+ lastSyncedAt: now()
1066
+ };
1067
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(record.entryTimestamp);
1068
+ const runtimeScopeId = record.deploymentId ?? record.accountId;
1146
1069
  try {
1147
- const ticker = await connector.getTopOfBookTicker(symbol);
1148
- const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
1149
- const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
1150
- const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
1151
- const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
1152
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
1153
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
1154
- return {
1155
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
1156
- arrivalSource: "top_of_book",
1157
- bid,
1158
- ask,
1159
- arrivalMid,
1160
- spreadBps
1161
- };
1070
+ await Promise.all([
1071
+ (0, import_redis.setData)(import_redis.redisKeys.runtimeTrade(userName, record.orderId), record, {
1072
+ expire: 0
1073
+ }),
1074
+ (0, import_redis.setHashJsonField)(
1075
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1076
+ record.orderId,
1077
+ record,
1078
+ { expire: 0 }
1079
+ ),
1080
+ (0, import_redis.setData)(
1081
+ import_redis.redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
1082
+ { orderId: record.orderId },
1083
+ { expire: 0 }
1084
+ )
1085
+ ]);
1162
1086
  } catch (error) {
1163
- import_logger.logger.warn(
1164
- "runtime order arrival snapshot failed: %s %s",
1165
- symbol,
1087
+ import_logger.logger.error(
1088
+ "runtime trade open journal failed: %s %s",
1089
+ record.symbol,
1166
1090
  error?.message || String(error)
1167
1091
  );
1168
- return {
1169
- arrivalSnapshotTime: Date.now(),
1170
- arrivalSource: "top_of_book_error",
1171
- bid: null,
1172
- ask: null,
1173
- arrivalMid: null,
1174
- spreadBps: null
1175
- };
1176
1092
  }
1093
+ return record;
1177
1094
  };
1178
- var validateEntryProtectionAtArrival = ({
1179
- direction,
1180
- signalPrice,
1181
- bid,
1182
- ask,
1183
- arrivalMid,
1184
- takeProfits,
1185
- stopLossPrice
1186
- }) => {
1187
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
1188
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
1189
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
1190
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
1191
- }
1192
- for (const takeProfit of takeProfits) {
1193
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
1194
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
1195
- }
1095
+ var recordRuntimeTradeIncrease = async (params) => {
1096
+ const {
1097
+ userName,
1098
+ strategy,
1099
+ symbol,
1100
+ direction,
1101
+ resultingQty,
1102
+ resultingEntryPrice,
1103
+ addedQty,
1104
+ addedEntryPrice,
1105
+ entryTimestamp,
1106
+ fee,
1107
+ accountId,
1108
+ deploymentId
1109
+ } = params;
1110
+ if (!userName) {
1111
+ return null;
1196
1112
  }
1197
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
1198
- throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
1113
+ const existing = await getActiveRuntimeTrade({
1114
+ userName,
1115
+ symbol,
1116
+ accountId,
1117
+ deploymentId
1118
+ });
1119
+ if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
1120
+ return null;
1199
1121
  }
1200
- };
1201
- var createEntrySubmissionValidation = ({
1202
- connector,
1203
- symbol,
1204
- direction,
1205
- takeProfits,
1206
- stopLossPrice,
1207
- limits,
1208
- onValidated
1209
- }) => {
1210
- if (!limits) return void 0;
1211
- if (!connector.capabilities?.orderSubmissionValidation)
1212
- throw new EntrySubmissionValidationError(
1213
- "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
1214
- );
1215
- return async ({ qty, executionPrice }) => {
1216
- const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
1217
- const snapshot2 = simulated && executionPrice != null ? {
1218
- arrivalSnapshotTime: Date.now(),
1219
- arrivalSource: "simulation",
1220
- bid: executionPrice,
1221
- ask: executionPrice,
1222
- arrivalMid: executionPrice,
1223
- spreadBps: 0
1224
- } : await getOrderArrivalSnapshot({
1225
- connector,
1226
- symbol,
1227
- requireTimestamp: true
1228
- });
1229
- const age = Date.now() - snapshot2.arrivalSnapshotTime;
1230
- if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
1231
- throw new EntrySubmissionValidationError(
1232
- "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
1233
- );
1234
- if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
1235
- throw new EntrySubmissionValidationError(
1236
- "ENTRY_QUOTE_STALE_AT_SUBMISSION"
1237
- );
1238
- const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
1239
- validateEntryProtectionAtArrival({
1240
- direction,
1241
- signalPrice: price,
1242
- bid: snapshot2.bid,
1243
- ask: snapshot2.ask,
1244
- arrivalMid: snapshot2.arrivalMid,
1245
- takeProfits,
1246
- stopLossPrice
1247
- });
1248
- const prices = takeProfits.map((tp) => tp.price);
1249
- const economics2 = validateEntryExecution({
1250
- direction,
1251
- price,
1252
- qty,
1253
- stopLossPrice,
1254
- takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
1255
- limits
1256
- });
1257
- onValidated?.(snapshot2, economics2);
1122
+ const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
1123
+ const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
1124
+ const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
1125
+ const next = {
1126
+ ...existing,
1127
+ ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
1128
+ qty: resultingQty,
1129
+ entryPrice: resultingEntryPrice,
1130
+ entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
1131
+ lastEntryPrice: addedEntryPrice,
1132
+ lastEntryQty: addedQty,
1133
+ lastEntryTimestamp: entryTimestamp,
1134
+ currentPrice: resultingEntryPrice,
1135
+ currentPnl: 0,
1136
+ fee: openFee,
1137
+ openFee,
1138
+ totalFee,
1139
+ lastSyncedAt: now()
1258
1140
  };
1259
- };
1260
-
1261
- // src/ai.ts
1262
- var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
1263
- var import_redis = require("@tradejs/infra/redis");
1264
-
1265
- // src/aiShared.ts
1266
- var MAX_AI_SERIES_POINTS = 5;
1267
- var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
1268
- "tradejs.indicators.compactSnapshot"
1269
- );
1270
- var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
1271
- var trimSeriesDeep = (value) => {
1272
- if (Array.isArray(value)) {
1273
- const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
1274
- const isMatrix = trimmed.every((item) => Array.isArray(item));
1275
- if (isMatrix) {
1276
- return trimmed;
1277
- }
1278
- return trimmed.map(
1279
- (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
1280
- );
1281
- }
1282
- if (value && typeof value === "object") {
1283
- return Object.fromEntries(
1284
- Object.entries(value).map(([key, nested]) => [
1285
- key,
1286
- trimSeriesDeep(nested)
1287
- ])
1141
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
1142
+ try {
1143
+ await Promise.all([
1144
+ (0, import_redis.setData)(import_redis.redisKeys.runtimeTrade(userName, existing.orderId), next, {
1145
+ expire: 0
1146
+ }),
1147
+ (0, import_redis.setHashJsonField)(
1148
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1149
+ existing.orderId,
1150
+ next,
1151
+ { expire: 0 }
1152
+ )
1153
+ ]);
1154
+ } catch (error) {
1155
+ import_logger.logger.error(
1156
+ "runtime trade increase journal failed: %s %s",
1157
+ symbol,
1158
+ error?.message || String(error)
1288
1159
  );
1289
1160
  }
1290
- return value;
1161
+ return next;
1291
1162
  };
1292
- var buildCompactAiIndicatorsSnapshot = (value) => {
1293
- const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
1294
- if (typeof compactSnapshot === "function") {
1295
- return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
1163
+ var getActiveRuntimeTrade = async (params) => {
1164
+ const { userName, symbol, accountId, deploymentId } = params;
1165
+ if (!userName) {
1166
+ return null;
1296
1167
  }
1297
- return trimSeriesDeep(value);
1298
- };
1299
-
1300
- // src/aiMarketContext.ts
1301
- var toRecord = (value) => {
1302
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1168
+ const activeRef = await (0, import_redis.getData)(
1169
+ import_redis.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
1170
+ null
1171
+ );
1172
+ const orderId = String(activeRef?.orderId || "").trim();
1173
+ if (!orderId) {
1303
1174
  return null;
1304
1175
  }
1305
- return value;
1306
- };
1307
- var toFiniteNumber = (value) => {
1308
- const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
1309
- return Number.isFinite(numeric) ? numeric : null;
1310
- };
1311
- var roundTo = (value, decimals) => {
1312
- const factor = 10 ** decimals;
1313
- return Math.round(value * factor) / factor;
1314
- };
1315
- var buildMissingSpreadContext = () => ({
1316
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1317
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1318
- available: false,
1319
- value: null,
1320
- zScore: null,
1321
- bps: null,
1322
- absBps: null,
1323
- bias: null,
1324
- severity: null
1325
- });
1326
- var buildSpreadContextFromSignal = (signal) => {
1327
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1328
- const relative = toRecord(baseContext?.relative);
1329
- const execution = toRecord(relative?.execution);
1330
- const spread = toFiniteNumber(execution?.venueSpread);
1331
- const zScore = toFiniteNumber(execution?.venueSpreadZScore);
1332
- if (spread == null) {
1333
- return buildMissingSpreadContext();
1176
+ const existing = await (0, import_redis.getData)(
1177
+ import_redis.redisKeys.runtimeTrade(userName, orderId),
1178
+ null
1179
+ );
1180
+ if (!existing || existing.status !== "active") {
1181
+ await (0, import_redis.delKey)(
1182
+ import_redis.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
1183
+ );
1184
+ return null;
1334
1185
  }
1335
- const value = roundTo(spread, 8);
1336
- const bps = roundTo(value * 1e4, 2);
1337
- const absBps = Math.abs(bps);
1338
- const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
1339
- const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
1340
- return {
1341
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1342
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1343
- available: true,
1344
- value,
1345
- zScore,
1346
- bps,
1347
- absBps,
1348
- bias,
1349
- severity
1350
- };
1186
+ return existing;
1351
1187
  };
1352
- var buildTrueDeltaContextFromSignal = (signal) => {
1353
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1354
- const participation = toRecord(baseContext?.participation);
1355
- const delta = toRecord(participation?.delta);
1356
- const source = String(delta?.source ?? "");
1357
- const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
1358
- if (!delta || !isTrueDeltaSource) {
1359
- return {
1360
- source: source || null,
1361
- available: false,
1362
- buyPressurePct: null,
1363
- buyVolume: null,
1364
- sellVolume: null,
1365
- netDelta: null,
1366
- deltaPct: null,
1367
- signedVolumeZScore: null
1368
- };
1188
+ var markRuntimeTradeClosed = async (params) => {
1189
+ const {
1190
+ userName,
1191
+ expectedOrderId,
1192
+ symbol,
1193
+ strategy,
1194
+ exitPrice,
1195
+ exitTimestamp,
1196
+ closedPnl,
1197
+ exitType,
1198
+ accountId,
1199
+ deploymentId
1200
+ } = params;
1201
+ if (!userName) {
1202
+ return null;
1369
1203
  }
1370
- return {
1371
- source,
1372
- available: true,
1373
- buyPressurePct: toFiniteNumber(delta.buyPressurePct),
1374
- buyVolume: toFiniteNumber(delta.buyVolume),
1375
- sellVolume: toFiniteNumber(delta.sellVolume),
1376
- netDelta: toFiniteNumber(delta.netDelta),
1377
- deltaPct: toFiniteNumber(delta.deltaPct),
1378
- signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
1379
- };
1380
- };
1381
- var buildTradeFlowContextFromSignal = (signal) => {
1382
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1383
- const participation = toRecord(baseContext?.participation);
1384
- const tradeFlow = toRecord(participation?.tradeFlow);
1385
- if (!tradeFlow) {
1386
- return {
1387
- source: null,
1388
- available: false,
1389
- interval: null,
1390
- stale: null,
1391
- trades: null,
1392
- buyPressurePct: null,
1393
- netBaseDelta: null,
1394
- netQuoteDelta: null
1395
- };
1204
+ const existing = expectedOrderId ? await (0, import_redis.getDataStrict)(
1205
+ import_redis.redisKeys.runtimeTrade(userName, expectedOrderId)
1206
+ ) : await getActiveRuntimeTrade({
1207
+ userName,
1208
+ symbol,
1209
+ accountId,
1210
+ deploymentId
1211
+ });
1212
+ if (!existing || expectedOrderId && existing.orderId !== expectedOrderId) {
1213
+ return null;
1396
1214
  }
1397
- return {
1398
- source: String(tradeFlow.source ?? ""),
1399
- available: true,
1400
- interval: String(tradeFlow.interval ?? ""),
1401
- stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
1402
- trades: toFiniteNumber(tradeFlow.trades),
1403
- buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
1404
- netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
1405
- netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
1215
+ const orderId = existing.orderId;
1216
+ if (strategy && existing.strategy !== strategy) {
1217
+ return null;
1218
+ }
1219
+ const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
1220
+ const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
1221
+ direction: existing.direction,
1222
+ entryPrice: existing.entryPrice,
1223
+ exitPrice: resolvedExitPrice,
1224
+ qty: existing.qty
1225
+ }) : existing.closedPnl ?? existing.currentPnl ?? null;
1226
+ const next = expectedOrderId && existing.status === "closed" ? existing : {
1227
+ ...existing,
1228
+ status: "closed",
1229
+ currentPrice: resolvedExitPrice,
1230
+ currentPnl: resolvedClosedPnl,
1231
+ closedPnl: resolvedClosedPnl,
1232
+ exitPrice: resolvedExitPrice,
1233
+ exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
1234
+ exitType: exitType ?? existing.exitType ?? null,
1235
+ lastSyncedAt: now()
1406
1236
  };
1407
- };
1408
- var buildMarketBreadthContextFromSignal = (signal) => {
1409
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1410
- const relative = toRecord(baseContext?.relative);
1411
- const breadth = toRecord(relative?.marketBreadth);
1412
- if (!breadth) {
1413
- return {
1414
- source: null,
1415
- available: false,
1416
- universe: null,
1417
- interval: null,
1418
- stale: null,
1419
- symbolsCount: null,
1420
- advanceDeclineRatio: null,
1421
- pctAboveMa20: null,
1422
- pctAboveMa50: null,
1423
- equalWeightedReturn: null,
1424
- volumeWeightedReturn: null,
1425
- dispersion: null
1426
- };
1237
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
1238
+ const closeDayKey = (0, import_time.getRuntimeStorageDayKey)(next.exitTimestamp);
1239
+ try {
1240
+ await Promise.all([
1241
+ (expectedOrderId ? import_redis.setDataStrict : import_redis.setData)(
1242
+ import_redis.redisKeys.runtimeTrade(userName, orderId),
1243
+ next,
1244
+ {
1245
+ expire: import_constants.TTL_1M
1246
+ }
1247
+ ),
1248
+ (expectedOrderId ? import_redis.setHashJsonFieldStrict : import_redis.setHashJsonField)(
1249
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1250
+ orderId,
1251
+ next,
1252
+ { expire: import_constants.TTL_1M }
1253
+ ),
1254
+ (expectedOrderId ? import_redis.setHashJsonFieldStrict : import_redis.setHashJsonField)(
1255
+ import_redis.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
1256
+ orderId,
1257
+ next,
1258
+ { expire: import_constants.TTL_1M }
1259
+ ),
1260
+ expectedOrderId ? (0, import_redis.deleteRuntimeTradeReference)(
1261
+ import_redis.redisKeys.runtimeActiveTrade(
1262
+ userName,
1263
+ symbol,
1264
+ deploymentId ?? accountId
1265
+ ),
1266
+ expectedOrderId
1267
+ ) : (0, import_redis.delKey)(
1268
+ import_redis.redisKeys.runtimeActiveTrade(
1269
+ userName,
1270
+ symbol,
1271
+ deploymentId ?? accountId
1272
+ )
1273
+ )
1274
+ ]);
1275
+ } catch (error) {
1276
+ if (expectedOrderId) throw error;
1277
+ import_logger.logger.error(
1278
+ "runtime trade close journal failed: %s %s",
1279
+ symbol,
1280
+ error?.message || String(error)
1281
+ );
1427
1282
  }
1428
- return {
1429
- source: String(breadth.source ?? ""),
1430
- available: true,
1431
- universe: String(breadth.universe ?? ""),
1432
- interval: String(breadth.interval ?? ""),
1433
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
1434
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
1435
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
1436
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
1437
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
1438
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
1439
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
1440
- dispersion: toFiniteNumber(breadth.dispersion)
1441
- };
1283
+ return next;
1442
1284
  };
1443
- var buildMarketBreadthsContextFromSignal = (signal) => {
1444
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1445
- const relative = toRecord(baseContext?.relative);
1446
- const breadths = toRecord(relative?.marketBreadths);
1447
- const build = (key) => {
1448
- const breadth = toRecord(breadths?.[key]);
1449
- if (!breadth) {
1285
+
1286
+ // src/strategy/portfolioRuntime.ts
1287
+ var coordinators = /* @__PURE__ */ new WeakMap();
1288
+ async function observeRuntimePortfolio(connector, binding, asOf, reservations, observedNow = Date.now) {
1289
+ if (!connector.getOpenPositionPnl || !connector.getEntryExecutions)
1290
+ throw new Error(
1291
+ "Portfolio policies require position marks and entry executions"
1292
+ );
1293
+ const orders = await Promise.all(
1294
+ reservations.map(async (r) => {
1295
+ if (r.submitted && connector.getEntryOrderObservation) {
1296
+ const fact2 = await connector.getEntryOrderObservation({
1297
+ symbol: r.symbol,
1298
+ orderId: r.id
1299
+ });
1300
+ if (fact2) return fact2;
1301
+ }
1302
+ const executions = r.submitted ? await connector.getEntryExecutions({
1303
+ symbol: r.symbol,
1304
+ startTime: r.timestamp,
1305
+ endTime: asOf
1306
+ }) : [];
1307
+ const filledQty = executions.filter(
1308
+ (e) => (e.orderLinkId === r.id || e.orderId === r.id) && e.direction === r.direction
1309
+ ).reduce((sum, e) => sum + e.qty, 0);
1450
1310
  return {
1451
- source: null,
1452
- available: false,
1453
- universe: null,
1454
- interval: null,
1455
- stale: null,
1456
- symbolsCount: null,
1457
- advanceDeclineRatio: null,
1458
- pctAboveMa20: null,
1459
- pctAboveMa50: null,
1460
- equalWeightedReturn: null,
1461
- volumeWeightedReturn: null,
1462
- dispersion: null
1311
+ id: r.id,
1312
+ status: filledQty >= r.qty ? "filled" : filledQty > 0 ? "partial" : "pending",
1313
+ filledQty,
1314
+ positionId: r.id
1315
+ };
1316
+ })
1317
+ );
1318
+ const snapshots2 = await connector.getOpenPositionPnl();
1319
+ const positions = await Promise.all(
1320
+ snapshots2.map(async (p) => {
1321
+ const reserved = reservations.find(
1322
+ (r) => r.symbol === p.symbol && r.direction === p.direction && orders.some(
1323
+ (o) => o.id === r.id && o.filledQty > 0 && p.qty <= o.filledQty * (1 + 1e-8)
1324
+ )
1325
+ );
1326
+ const trade = await getActiveRuntimeTrade({
1327
+ ...binding,
1328
+ symbol: p.symbol
1329
+ });
1330
+ 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;
1331
+ return {
1332
+ id: reserved?.id ?? owner?.orderId ?? `unknown:${p.symbol}:${p.direction}`,
1333
+ strategy: reserved?.strategy ?? owner?.strategy ?? null,
1334
+ symbol: p.symbol,
1335
+ direction: p.direction,
1336
+ qty: p.qty,
1337
+ entryPrice: p.price,
1338
+ markPrice: p.currentPrice,
1339
+ markedAt: p.observedAt ?? null,
1340
+ stopLossPrice: p.slPrice ?? p.stopLossPrice ?? null
1463
1341
  };
1342
+ })
1343
+ );
1344
+ return { asOf: Math.max(asOf, observedNow()), positions, orders };
1345
+ }
1346
+ function persistentStore(key) {
1347
+ const raw = /* @__PURE__ */ new WeakMap();
1348
+ return {
1349
+ async read() {
1350
+ const value = await import_redis2.portfolioStateStorage.read(key);
1351
+ if (value === null) return null;
1352
+ const state = JSON.parse(value);
1353
+ (0, import_portfolio2.assertPortfolioState)(state);
1354
+ raw.set(state, value);
1355
+ return state;
1356
+ },
1357
+ async compare(previous, next) {
1358
+ const expected = previous === null ? null : raw.get(previous);
1359
+ if (expected === void 0)
1360
+ throw new Error("Portfolio CAS requires a storage snapshot");
1361
+ return import_redis2.portfolioStateStorage.compare(key, expected, JSON.stringify(next));
1464
1362
  }
1465
- return {
1466
- source: String(breadth.source ?? ""),
1467
- available: true,
1468
- universe: String(breadth.universe ?? ""),
1469
- interval: String(breadth.interval ?? ""),
1470
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
1471
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
1472
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
1473
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
1474
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
1475
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
1476
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
1477
- dispersion: toFiniteNumber(breadth.dispersion)
1478
- };
1479
1363
  };
1480
- return {
1481
- top5: build("top5"),
1482
- top10: build("top10"),
1483
- top30: build("top30"),
1484
- top50: build("top50"),
1485
- top100: build("top100")
1364
+ }
1365
+ function getPortfolioRuntime({
1366
+ value,
1367
+ connectorName,
1368
+ strategy,
1369
+ connector,
1370
+ env,
1371
+ userName,
1372
+ accountId,
1373
+ deploymentId,
1374
+ symbol,
1375
+ canClose
1376
+ }) {
1377
+ const policy = (0, import_portfolio2.parsePortfolioPolicy)(value);
1378
+ if (!policy) return void 0;
1379
+ const replay = (env === "BACKTEST" || env === "PARITY") && Reflect.get(connector, "__tradejsReplayConnector") === true;
1380
+ if ((env === "BACKTEST" || env === "PARITY") && (!replay || !connector.getPortfolioObservation))
1381
+ throw new Error(
1382
+ "Portfolio policies require shared portfolio replay; isolated symbol backtests are unsupported"
1383
+ );
1384
+ if (!replay && (!userName || !accountId || !deploymentId || !connector.getOpenPositionPnl || !connector.getEntryExecutions))
1385
+ throw new Error(
1386
+ "Portfolio runtime requires exact user/account/deployment binding, marks and executions"
1387
+ );
1388
+ const binding = {
1389
+ userName,
1390
+ accountId,
1391
+ deploymentId
1486
1392
  };
1393
+ const scope = policy.scope === "strategy" ? strategy : policy.scope === "account" ? "account" : [...policy.scope.strategies].sort();
1394
+ const key = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify([binding, connectorName, connector.universe, scope])).digest("hex");
1395
+ const fingerprint3 = (0, import_portfolio2.portfolioFingerprint)(policy, strategy);
1396
+ const cacheKey = key;
1397
+ let registry = coordinators.get(connector);
1398
+ if (!registry) {
1399
+ registry = /* @__PURE__ */ new Map();
1400
+ coordinators.set(connector, registry);
1401
+ }
1402
+ const existing = registry.get(cacheKey);
1403
+ if (existing) {
1404
+ if (existing.fingerprint !== fingerprint3)
1405
+ throw new Error("Conflicting portfolio configuration for the same scope");
1406
+ if (symbol && canClose)
1407
+ existing.permissions.set(JSON.stringify([strategy, symbol]), canClose);
1408
+ return existing.coordinator;
1409
+ }
1410
+ const permissions = /* @__PURE__ */ new Map();
1411
+ if (symbol && canClose)
1412
+ permissions.set(JSON.stringify([strategy, symbol]), canClose);
1413
+ const coordinator = createPortfolioCoordinator({
1414
+ policy,
1415
+ strategy,
1416
+ store: replay ? createMemoryPortfolioStore() : persistentStore(key),
1417
+ execution: {
1418
+ async confirmClosed(intent, asOf) {
1419
+ if (replay) return;
1420
+ const position = intent.position;
1421
+ await markRuntimeTradeClosed({
1422
+ ...binding,
1423
+ strategy: position.strategy,
1424
+ expectedOrderId: position.id,
1425
+ symbol: position.symbol,
1426
+ exitPrice: position.markPrice,
1427
+ exitTimestamp: asOf,
1428
+ exitType: "exit"
1429
+ });
1430
+ },
1431
+ observe: (asOf, reservations) => replay ? connector.getPortfolioObservation(asOf, reservations) : observeRuntimePortfolio(connector, binding, asOf, reservations),
1432
+ async close(position, intent, asOf) {
1433
+ const permission = permissions.get(
1434
+ JSON.stringify([position.strategy, position.symbol])
1435
+ );
1436
+ if (!permission || !await permission(position, intent, asOf)) return;
1437
+ if (!replay) {
1438
+ const trade = await getActiveRuntimeTrade({
1439
+ ...binding,
1440
+ symbol: position.symbol
1441
+ });
1442
+ if (!trade || trade.orderId !== position.id)
1443
+ throw new Error("Portfolio close ownership changed");
1444
+ }
1445
+ await connector.closePosition({
1446
+ symbol: position.symbol,
1447
+ direction: position.direction,
1448
+ price: position.markPrice,
1449
+ timestamp: asOf,
1450
+ signal: {
1451
+ signalId: `${position.id}:${intent.reason}`,
1452
+ strategy: position.strategy,
1453
+ symbol: position.symbol,
1454
+ direction: position.direction,
1455
+ interval: "1",
1456
+ timestamp: asOf,
1457
+ prices: {
1458
+ riskRatio: 0,
1459
+ currentPrice: position.markPrice,
1460
+ takeProfitPrice: position.markPrice,
1461
+ stopLossPrice: position.markPrice
1462
+ },
1463
+ figures: {},
1464
+ indicators: {},
1465
+ additionalIndicators: { portfolioExitReason: intent.reason }
1466
+ }
1467
+ });
1468
+ }
1469
+ }
1470
+ });
1471
+ registry.set(cacheKey, { fingerprint: fingerprint3, coordinator, permissions });
1472
+ return coordinator;
1473
+ }
1474
+
1475
+ // src/strategyHelpers/tickerProfiles.ts
1476
+ var import_tickerProfiles = require("@tradejs/infra/tickerProfiles");
1477
+ var import_tickerProfiles2 = require("@tradejs/core/tickerProfiles");
1478
+
1479
+ // src/tradejsConfig.ts
1480
+ var import_fs = __toESM(require("fs"));
1481
+ var import_path = __toESM(require("path"));
1482
+ var import_url = require("url");
1483
+ var import_config = require("@tradejs/core/config");
1484
+ var import_logger2 = require("@tradejs/infra/logger");
1485
+ var CONFIG_FILE_NAMES = [
1486
+ "tradejs.config.ts",
1487
+ "tradejs.config.mts",
1488
+ "tradejs.config.js",
1489
+ "tradejs.config.mjs",
1490
+ "tradejs.config.cjs"
1491
+ ];
1492
+ var TS_MODULE_RE = /\.(cts|mts|ts)$/i;
1493
+ var cachedByCwd = /* @__PURE__ */ new Map();
1494
+ var announcedConfigFile = /* @__PURE__ */ new Set();
1495
+ var tsNodeRegistered = false;
1496
+ var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
1497
+ var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
1498
+ var getTradejsProjectCwd = (cwd) => {
1499
+ const explicit = String(cwd ?? "").trim();
1500
+ if (explicit) {
1501
+ return import_path.default.resolve(explicit);
1502
+ }
1503
+ const fromEnv = String(process.env.PROJECT_CWD || "").trim();
1504
+ if (fromEnv) {
1505
+ return import_path.default.resolve(fromEnv);
1506
+ }
1507
+ return process.cwd();
1487
1508
  };
1488
- var buildTargetVsBtcContextFromSignal = (signal) => {
1489
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1490
- const relative = toRecord(baseContext?.relative);
1491
- const targetVsBtc = toRecord(relative?.targetVsBtc);
1492
- if (!targetVsBtc) {
1493
- return {
1494
- source: null,
1495
- available: false,
1496
- ratioReturn1h: null,
1497
- ratioReturn4h: null,
1498
- ratioReturn24h: null,
1499
- alphaVsBtc1h: null,
1500
- alphaVsBtc4h: null,
1501
- alphaVsBtc24h: null,
1502
- betaToBtc20: null,
1503
- correlationToBtc20: null,
1504
- ratioTrend: null
1505
- };
1509
+ var normalizeConfig = (rawConfig) => {
1510
+ if (!rawConfig || typeof rawConfig !== "object") {
1511
+ return {};
1506
1512
  }
1513
+ const config = rawConfig;
1514
+ const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
1515
+ const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
1516
+ const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
1517
+ const hooks = (0, import_config.normalizeTradejsConfigHooks)(
1518
+ config.hooks
1519
+ );
1520
+ const runtime = config.runtime && typeof config.runtime === "object" && !Array.isArray(config.runtime) ? config.runtime : void 0;
1507
1521
  return {
1508
- source: String(targetVsBtc.source ?? ""),
1509
- available: true,
1510
- ratioReturn1h: toFiniteNumber(targetVsBtc.ratioReturn1h),
1511
- ratioReturn4h: toFiniteNumber(targetVsBtc.ratioReturn4h),
1512
- ratioReturn24h: toFiniteNumber(targetVsBtc.ratioReturn24h),
1513
- alphaVsBtc1h: toFiniteNumber(targetVsBtc.alphaVsBtc1h),
1514
- alphaVsBtc4h: toFiniteNumber(targetVsBtc.alphaVsBtc4h),
1515
- alphaVsBtc24h: toFiniteNumber(targetVsBtc.alphaVsBtc24h),
1516
- betaToBtc20: toFiniteNumber(targetVsBtc.betaToBtc20),
1517
- correlationToBtc20: toFiniteNumber(targetVsBtc.correlationToBtc20),
1518
- ratioTrend: typeof targetVsBtc.ratioTrend === "string" ? targetVsBtc.ratioTrend : null
1522
+ strategies: strategies2,
1523
+ indicators,
1524
+ connectors,
1525
+ ...hooks ? { hooks } : {},
1526
+ ...runtime ? { runtime } : {}
1519
1527
  };
1520
1528
  };
1521
- var buildBtcAltRegimeContextFromSignal = (signal) => {
1522
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1523
- const relative = toRecord(baseContext?.relative);
1524
- const btcAltRegime = toRecord(relative?.btcAltRegime);
1525
- if (!btcAltRegime) {
1526
- return {
1527
- source: null,
1528
- available: false,
1529
- universe: null,
1530
- interval: null,
1531
- stale: null,
1532
- regime: null,
1533
- btcReturn24h: null,
1534
- altBasketReturn24h: null,
1535
- btcVsAltReturn24h: null,
1536
- btcTurnoverShare24h: null,
1537
- btcTurnoverShareChange24h: null,
1538
- altVolToBtcVol24h: null,
1539
- altDispersion24h: null
1540
- };
1529
+ var getNodeCreateRequire = () => {
1530
+ const builtinModule = process.getBuiltinModule?.("module");
1531
+ if (typeof builtinModule?.createRequire === "function") {
1532
+ return builtinModule.createRequire;
1541
1533
  }
1542
- return {
1543
- source: String(btcAltRegime.source ?? ""),
1544
- available: true,
1545
- universe: String(btcAltRegime.universe ?? ""),
1546
- interval: String(btcAltRegime.interval ?? ""),
1547
- stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
1548
- regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
1549
- btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
1550
- altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
1551
- btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
1552
- btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
1553
- btcTurnoverShareChange24h: toFiniteNumber(
1554
- btcAltRegime.btcTurnoverShareChange24h
1555
- ),
1556
- altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
1557
- altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
1558
- };
1534
+ throw new TypeError("module.createRequire is not available");
1559
1535
  };
1560
- var buildCmcGlobalContextFromSignal = (signal) => {
1561
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1562
- const relative = toRecord(baseContext?.relative);
1563
- const cmcGlobal = toRecord(relative?.cmcGlobal);
1564
- if (!cmcGlobal) {
1565
- return {
1566
- source: null,
1567
- available: false,
1568
- interval: null,
1569
- asOfTs: null,
1570
- stale: null,
1571
- totalMarketCapUsd: null,
1572
- totalVolumeUsd: null,
1573
- totalVolumeReportedUsd: null,
1574
- altMarketCapUsd: null,
1575
- altVolumeUsd: null,
1576
- altVolumeReportedUsd: null,
1577
- btcDominancePct: null,
1578
- ethDominancePct: null,
1579
- btcDominanceChange24hPct: null,
1580
- ethDominanceChange24hPct: null,
1581
- altMarketCapChange24hPct: null,
1582
- altVolumeChange24hPct: null,
1583
- activeCryptocurrencies: null,
1584
- activeExchanges: null,
1585
- activeMarketPairs: null,
1586
- altLiquidityRegime: null
1536
+ var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
1537
+ var ensureTsNodeRegistered = async () => {
1538
+ if (tsNodeRegistered) {
1539
+ return;
1540
+ }
1541
+ const tsNodeModule = await import("ts-node");
1542
+ const tsNode = tsNodeModule.default ?? tsNodeModule;
1543
+ tsNode.register?.({
1544
+ transpileOnly: true,
1545
+ compilerOptions: {
1546
+ module: "Node16",
1547
+ moduleResolution: "node16"
1548
+ }
1549
+ });
1550
+ tsNodeRegistered = true;
1551
+ };
1552
+ var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
1553
+ const projectRoot = getTradejsProjectCwd(cwd);
1554
+ if (tsconfigPathsRegisteredByCwd.has(projectRoot)) {
1555
+ return;
1556
+ }
1557
+ const tsconfigPathsModule = await import("tsconfig-paths");
1558
+ const loadConfig = tsconfigPathsModule.loadConfig;
1559
+ const register = tsconfigPathsModule.register;
1560
+ if (typeof loadConfig !== "function" || typeof register !== "function") {
1561
+ return;
1562
+ }
1563
+ const loadedConfig = loadConfig(projectRoot);
1564
+ if (loadedConfig.resultType !== "success") {
1565
+ return;
1566
+ }
1567
+ register({
1568
+ baseUrl: loadedConfig.absoluteBaseUrl,
1569
+ paths: loadedConfig.paths,
1570
+ addMatchAll: false
1571
+ });
1572
+ tsconfigPathsRegisteredByCwd.add(projectRoot);
1573
+ };
1574
+ var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1575
+ const projectRoot = getTradejsProjectCwd(cwd);
1576
+ const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
1577
+ if (cachedMatcher) {
1578
+ const resolved2 = cachedMatcher(moduleName);
1579
+ return resolved2 || null;
1580
+ }
1581
+ const tsconfigPathsModule = await import("tsconfig-paths");
1582
+ const loadConfig = tsconfigPathsModule.loadConfig;
1583
+ const createMatchPath = tsconfigPathsModule.createMatchPath;
1584
+ if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
1585
+ return null;
1586
+ }
1587
+ const loadedConfig = loadConfig(projectRoot);
1588
+ if (loadedConfig.resultType !== "success") {
1589
+ return null;
1590
+ }
1591
+ const matchPath = createMatchPath(
1592
+ loadedConfig.absoluteBaseUrl,
1593
+ loadedConfig.paths
1594
+ );
1595
+ const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
1596
+ ".ts",
1597
+ ".tsx",
1598
+ ".mts",
1599
+ ".cts",
1600
+ ".js",
1601
+ ".jsx",
1602
+ ".mjs",
1603
+ ".cjs",
1604
+ ".json"
1605
+ ]) || "";
1606
+ tsconfigPathMatchersByCwd.set(projectRoot, matcher);
1607
+ const resolved = matcher(moduleName);
1608
+ return resolved || null;
1609
+ };
1610
+ var toImportSpecifier = (moduleName) => {
1611
+ if (moduleName.startsWith("file://")) {
1612
+ return moduleName;
1613
+ }
1614
+ if (import_path.default.isAbsolute(moduleName)) {
1615
+ return (0, import_url.pathToFileURL)(moduleName).href;
1616
+ }
1617
+ return moduleName;
1618
+ };
1619
+ var isTsModulePath = (moduleName) => TS_MODULE_RE.test(moduleName.split("?")[0]);
1620
+ var isRelativeModulePath = (moduleName) => moduleName.startsWith("./") || moduleName.startsWith("../");
1621
+ var isBareModuleSpecifier = (moduleName) => {
1622
+ const normalized = String(moduleName ?? "").trim();
1623
+ if (!normalized) {
1624
+ return false;
1625
+ }
1626
+ if (normalized.startsWith("file://") || import_path.default.isAbsolute(normalized) || isRelativeModulePath(normalized)) {
1627
+ return false;
1628
+ }
1629
+ return true;
1630
+ };
1631
+ var importConfigFile = async (configFilePath) => {
1632
+ const ext = import_path.default.extname(configFilePath).toLowerCase();
1633
+ const configFileUrl = `${toImportSpecifier(configFilePath)}?t=${Date.now()}`;
1634
+ if (ext === ".ts" || ext === ".mts") {
1635
+ const requireFn = getRequireFn(import_path.default.dirname(configFilePath));
1636
+ await ensureTsNodeRegistered();
1637
+ await ensureTsconfigPathsRegistered(import_path.default.dirname(configFilePath));
1638
+ return requireFn(configFilePath);
1639
+ }
1640
+ return import(
1641
+ /* webpackIgnore: true */
1642
+ configFileUrl
1643
+ );
1644
+ };
1645
+ var importTradejsModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1646
+ const normalized = String(moduleName ?? "").trim();
1647
+ if (!normalized) {
1648
+ return {};
1649
+ }
1650
+ let modulePath = normalized;
1651
+ if (normalized.startsWith("file://")) {
1652
+ try {
1653
+ modulePath = (0, import_url.fileURLToPath)(normalized);
1654
+ } catch {
1655
+ modulePath = normalized;
1656
+ }
1657
+ }
1658
+ const requireFn = getRequireFn(
1659
+ import_path.default.isAbsolute(modulePath) ? import_path.default.dirname(modulePath) : cwd
1660
+ );
1661
+ if (isTsModulePath(modulePath)) {
1662
+ await ensureTsNodeRegistered();
1663
+ await ensureTsconfigPathsRegistered(cwd);
1664
+ return requireFn(modulePath);
1665
+ }
1666
+ if (isBareModuleSpecifier(normalized)) {
1667
+ await ensureTsconfigPathsRegistered(cwd);
1668
+ try {
1669
+ return requireFn(normalized);
1670
+ } catch (error) {
1671
+ const resolvedByTsconfig = await resolveTsconfigPathModule(
1672
+ normalized,
1673
+ cwd
1674
+ );
1675
+ if (resolvedByTsconfig && resolvedByTsconfig !== normalized) {
1676
+ return requireFn(resolvedByTsconfig);
1677
+ }
1678
+ throw error;
1679
+ }
1680
+ }
1681
+ try {
1682
+ return await import(
1683
+ /* webpackIgnore: true */
1684
+ toImportSpecifier(normalized)
1685
+ );
1686
+ } catch (error) {
1687
+ if (isTsModulePath(modulePath)) {
1688
+ await ensureTsNodeRegistered();
1689
+ await ensureTsconfigPathsRegistered(cwd);
1690
+ return requireFn(modulePath);
1691
+ }
1692
+ throw error;
1693
+ }
1694
+ };
1695
+ var resolveExportedConfig = (moduleExports) => {
1696
+ const candidate = moduleExports && typeof moduleExports === "object" && "default" in moduleExports ? moduleExports.default : moduleExports;
1697
+ return normalizeConfig(candidate);
1698
+ };
1699
+ var findConfigFilePath = (cwd) => {
1700
+ let currentDir = import_path.default.resolve(cwd);
1701
+ while (true) {
1702
+ for (const fileName of CONFIG_FILE_NAMES) {
1703
+ const fullPath = import_path.default.join(currentDir, fileName);
1704
+ if (import_fs.default.existsSync(fullPath) && import_fs.default.statSync(fullPath).isFile()) {
1705
+ return fullPath;
1706
+ }
1707
+ }
1708
+ const parentDir = import_path.default.dirname(currentDir);
1709
+ if (parentDir === currentDir) {
1710
+ return null;
1711
+ }
1712
+ currentDir = parentDir;
1713
+ }
1714
+ };
1715
+ var resolvePluginModuleSpecifier = (moduleName, cwd = getTradejsProjectCwd()) => {
1716
+ const normalized = String(moduleName ?? "").trim();
1717
+ if (!normalized) {
1718
+ return "";
1719
+ }
1720
+ if (normalized.startsWith("file://")) {
1721
+ try {
1722
+ return (0, import_url.fileURLToPath)(normalized);
1723
+ } catch {
1724
+ return normalized;
1725
+ }
1726
+ }
1727
+ if (import_path.default.isAbsolute(normalized)) {
1728
+ return normalized;
1729
+ }
1730
+ if (isRelativeModulePath(normalized)) {
1731
+ return import_path.default.resolve(cwd, normalized);
1732
+ }
1733
+ return normalized;
1734
+ };
1735
+ var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
1736
+ const cached = cachedByCwd.get(cwd);
1737
+ if (cached) {
1738
+ return cached;
1739
+ }
1740
+ const configFilePath = findConfigFilePath(cwd);
1741
+ if (!configFilePath) {
1742
+ cachedByCwd.set(cwd, {});
1743
+ return {};
1744
+ }
1745
+ try {
1746
+ const moduleExports = await importConfigFile(configFilePath);
1747
+ const config = resolveExportedConfig(moduleExports);
1748
+ cachedByCwd.set(cwd, config);
1749
+ if (!announcedConfigFile.has(configFilePath)) {
1750
+ announcedConfigFile.add(configFilePath);
1751
+ import_logger2.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
1752
+ }
1753
+ return config;
1754
+ } catch (error) {
1755
+ import_logger2.logger.log(
1756
+ "warn",
1757
+ "Failed to load TradeJS config from %s: %s",
1758
+ configFilePath,
1759
+ String(error)
1760
+ );
1761
+ cachedByCwd.set(cwd, {});
1762
+ return {};
1763
+ }
1764
+ };
1765
+
1766
+ // src/strategy/manifests.ts
1767
+ var import_indicators = require("@tradejs/core/indicators");
1768
+ var SHARED_STRATEGY_REGISTRY_KEY = "__tradejsNodeSharedStrategyRegistryV1__";
1769
+ var sharedRegistryScope = globalThis;
1770
+ var sharedStrategyRegistry = sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] ?? (sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] = {
1771
+ registryStateByProjectRoot: /* @__PURE__ */ new Map()
1772
+ });
1773
+ var createStrategyRegistryState = () => ({
1774
+ strategyCreators: /* @__PURE__ */ new Map(),
1775
+ strategyManifestsMap: /* @__PURE__ */ new Map(),
1776
+ strategyEntriesMap: /* @__PURE__ */ new Map(),
1777
+ strategySourcesMap: /* @__PURE__ */ new Map(),
1778
+ strategyModuleResolutions: /* @__PURE__ */ new Map(),
1779
+ pluginsLoadPromise: null
1780
+ });
1781
+ var registryStateByProjectRoot = sharedStrategyRegistry.registryStateByProjectRoot;
1782
+ var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
1783
+ const projectRoot = getTradejsProjectCwd(cwd);
1784
+ let state = registryStateByProjectRoot.get(projectRoot);
1785
+ if (!state) {
1786
+ state = createStrategyRegistryState();
1787
+ registryStateByProjectRoot.set(projectRoot, state);
1788
+ }
1789
+ return {
1790
+ projectRoot,
1791
+ state
1792
+ };
1793
+ };
1794
+ var toUniqueModules = (modules = []) => [
1795
+ ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
1796
+ ];
1797
+ var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
1798
+ const config = await loadTradejsConfig(cwd);
1799
+ return {
1800
+ strategyModules: toUniqueModules(config.strategies),
1801
+ indicatorModules: toUniqueModules(config.indicators)
1802
+ };
1803
+ };
1804
+ var extractModuleEntries = (moduleExport, key) => {
1805
+ if (!moduleExport || typeof moduleExport !== "object") {
1806
+ return null;
1807
+ }
1808
+ const candidate = moduleExport;
1809
+ if (Array.isArray(candidate[key])) {
1810
+ return candidate[key];
1811
+ }
1812
+ const defaultExport = candidate.default;
1813
+ if (defaultExport && Array.isArray(defaultExport[key])) {
1814
+ return defaultExport[key];
1815
+ }
1816
+ return null;
1817
+ };
1818
+ var extractStrategyPluginDefinition = (moduleExport) => {
1819
+ const strategyEntries = extractModuleEntries(
1820
+ moduleExport,
1821
+ "strategyEntries"
1822
+ );
1823
+ return strategyEntries ? { strategyEntries } : null;
1824
+ };
1825
+ var extractIndicatorPluginDefinition = (moduleExport) => {
1826
+ const indicatorEntries = extractModuleEntries(
1827
+ moduleExport,
1828
+ "indicatorEntries"
1829
+ );
1830
+ return indicatorEntries ? { indicatorEntries } : null;
1831
+ };
1832
+ var validateStrategyEntries = (moduleName, entries, state) => {
1833
+ const issues = [];
1834
+ const names = /* @__PURE__ */ new Set();
1835
+ entries.forEach((entry, index) => {
1836
+ const entryPath = `${moduleName}.strategyEntries[${index}]`;
1837
+ const strategyName = entry?.manifest?.name;
1838
+ if (typeof strategyName !== "string" || !strategyName.trim()) {
1839
+ issues.push(`${entryPath}: manifest.name is required`);
1840
+ } else if (names.has(strategyName) || state?.strategyEntriesMap.has(strategyName)) {
1841
+ issues.push(`${entryPath}: duplicate strategy ${strategyName}`);
1842
+ } else {
1843
+ names.add(strategyName);
1844
+ }
1845
+ if (!entry || typeof entry !== "object" || !entry.defaults || typeof entry.defaults !== "object" || Array.isArray(entry.defaults)) {
1846
+ issues.push(`${entryPath}: defaults must be an object`);
1847
+ }
1848
+ if (typeof entry?.parseConfig !== "function") {
1849
+ issues.push(`${entryPath}: parseConfig is required`);
1850
+ }
1851
+ if (typeof entry?.createCore !== "function") {
1852
+ issues.push(`${entryPath}: createCore is required`);
1853
+ }
1854
+ });
1855
+ return issues;
1856
+ };
1857
+ var registerEntries = (entries, source, state) => {
1858
+ for (const entry of entries) {
1859
+ const strategyName = entry.manifest.name;
1860
+ state.strategyManifestsMap.set(strategyName, entry.manifest);
1861
+ state.strategyEntriesMap.set(strategyName, entry);
1862
+ state.strategySourcesMap.set(strategyName, source);
1863
+ materializeStrategyCreator(strategyName, state);
1864
+ }
1865
+ };
1866
+ var materializeStrategyCreator = (strategyName, state) => {
1867
+ if (state.strategyCreators.has(strategyName) || !sharedStrategyRegistry.strategyRuntimeFactory) {
1868
+ return;
1869
+ }
1870
+ const entry = state.strategyEntriesMap.get(strategyName);
1871
+ if (!entry) return;
1872
+ state.strategyCreators.set(
1873
+ strategyName,
1874
+ sharedStrategyRegistry.strategyRuntimeFactory({
1875
+ strategyName,
1876
+ defaults: entry.defaults,
1877
+ createCore: entry.createCore,
1878
+ manifest: entry.manifest,
1879
+ detectorKey: entry.detectorKey,
1880
+ detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
1881
+ resolveRegisteredManifest: (name) => state.strategyManifestsMap.get(name)
1882
+ })
1883
+ );
1884
+ };
1885
+ var setStrategyRuntimeFactory = (factory) => {
1886
+ sharedStrategyRegistry.strategyRuntimeFactory = factory;
1887
+ for (const state of registryStateByProjectRoot.values()) {
1888
+ for (const strategyName of state.strategyEntriesMap.keys()) {
1889
+ materializeStrategyCreator(strategyName, state);
1890
+ }
1891
+ }
1892
+ };
1893
+ var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1894
+ if (typeof importTradejsModule === "function") {
1895
+ return importTradejsModule(moduleName, cwd);
1896
+ }
1897
+ return import(
1898
+ /* webpackIgnore: true */
1899
+ moduleName
1900
+ );
1901
+ };
1902
+ var resolveStrategyModuleEntry = async ({
1903
+ strategyName,
1904
+ moduleName,
1905
+ cwd
1906
+ }) => {
1907
+ const { projectRoot, state } = getStrategyRegistryState(cwd);
1908
+ const source = moduleName.trim();
1909
+ const resolvedModuleName = resolvePluginModuleSpecifier(source, projectRoot);
1910
+ const cacheKey = `${resolvedModuleName}\0${strategyName}`;
1911
+ const cached = state.strategyModuleResolutions.get(cacheKey);
1912
+ if (cached) return cached;
1913
+ const resolution = (async () => {
1914
+ const moduleExport = await importStrategyPluginModule(
1915
+ resolvedModuleName,
1916
+ projectRoot
1917
+ );
1918
+ const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
1919
+ if (!pluginDefinition) {
1920
+ throw new Error(`${source}: export { strategyEntries } is missing`);
1921
+ }
1922
+ const issues = validateStrategyEntries(
1923
+ source,
1924
+ pluginDefinition.strategyEntries
1925
+ );
1926
+ if (issues.length > 0) {
1927
+ throw new Error(
1928
+ ["Invalid TradeJS strategy module:", ...issues].join("\n")
1929
+ );
1930
+ }
1931
+ const entry = pluginDefinition.strategyEntries.find(
1932
+ (candidate) => candidate.manifest.name === strategyName
1933
+ );
1934
+ if (!entry) {
1935
+ throw new Error(`${source}: strategy ${strategyName} is missing`);
1936
+ }
1937
+ const factory = sharedStrategyRegistry.strategyRuntimeFactory;
1938
+ if (!factory) {
1939
+ throw new Error("Strategy runtime factory is not configured");
1940
+ }
1941
+ const manifestsByName = new Map(
1942
+ pluginDefinition.strategyEntries.map(
1943
+ (candidate) => [candidate.manifest.name, candidate.manifest]
1944
+ )
1945
+ );
1946
+ return {
1947
+ entry,
1948
+ creator: factory({
1949
+ strategyName,
1950
+ defaults: entry.defaults,
1951
+ createCore: entry.createCore,
1952
+ manifest: entry.manifest,
1953
+ detectorKey: entry.detectorKey,
1954
+ detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
1955
+ resolveRegisteredManifest: (name) => manifestsByName.get(name) ?? state.strategyManifestsMap.get(name)
1956
+ }),
1957
+ source
1587
1958
  };
1959
+ })();
1960
+ state.strategyModuleResolutions.set(cacheKey, resolution);
1961
+ try {
1962
+ return await resolution;
1963
+ } catch (error) {
1964
+ state.strategyModuleResolutions.delete(cacheKey);
1965
+ throw error;
1966
+ }
1967
+ };
1968
+ var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
1969
+ const { projectRoot, state } = getStrategyRegistryState(cwd);
1970
+ if (!state.pluginsLoadPromise) {
1971
+ (0, import_indicators.resetIndicatorRegistryCache)(projectRoot);
1972
+ state.pluginsLoadPromise = (async () => {
1973
+ const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
1974
+ const strategySet = new Set(strategyModules);
1975
+ const indicatorSet = new Set(indicatorModules);
1976
+ const pluginModuleNames = [
1977
+ .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
1978
+ ];
1979
+ if (!pluginModuleNames.length) {
1980
+ return;
1981
+ }
1982
+ const issues = [];
1983
+ for (const moduleName of pluginModuleNames) {
1984
+ try {
1985
+ const resolvedModuleName = resolvePluginModuleSpecifier(
1986
+ moduleName,
1987
+ projectRoot
1988
+ );
1989
+ const moduleExport = await importStrategyPluginModule(
1990
+ resolvedModuleName,
1991
+ projectRoot
1992
+ );
1993
+ if (strategySet.has(moduleName)) {
1994
+ const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
1995
+ if (!pluginDefinition) {
1996
+ issues.push(
1997
+ `${moduleName}: export { strategyEntries } is missing`
1998
+ );
1999
+ } else {
2000
+ const entryIssues = validateStrategyEntries(
2001
+ moduleName,
2002
+ pluginDefinition.strategyEntries,
2003
+ state
2004
+ );
2005
+ issues.push(...entryIssues);
2006
+ if (entryIssues.length === 0) {
2007
+ registerEntries(
2008
+ pluginDefinition.strategyEntries,
2009
+ moduleName,
2010
+ state
2011
+ );
2012
+ }
2013
+ }
2014
+ }
2015
+ if (indicatorSet.has(moduleName)) {
2016
+ const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
2017
+ if (!indicatorPluginDefinition) {
2018
+ issues.push(
2019
+ `${moduleName}: export { indicatorEntries } is missing`
2020
+ );
2021
+ } else {
2022
+ (0, import_indicators.registerIndicatorEntries)(
2023
+ indicatorPluginDefinition.indicatorEntries,
2024
+ moduleName,
2025
+ projectRoot
2026
+ );
2027
+ }
2028
+ }
2029
+ if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
2030
+ issues.push(`${moduleName}: plugin is not declared in config`);
2031
+ }
2032
+ } catch (error) {
2033
+ issues.push(`${moduleName}: failed to import: ${String(error)}`);
2034
+ }
2035
+ }
2036
+ if (issues.length > 0) {
2037
+ throw new Error(
2038
+ ["Invalid TradeJS plugin catalog:", ...issues].join("\n")
2039
+ );
2040
+ }
2041
+ })();
1588
2042
  }
1589
- return {
1590
- source: String(cmcGlobal.source ?? ""),
1591
- available: true,
1592
- interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
1593
- asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
1594
- stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
1595
- totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
1596
- totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
1597
- totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
1598
- altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
1599
- altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
1600
- altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
1601
- btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
1602
- ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
1603
- btcDominanceChange24hPct: toFiniteNumber(
1604
- cmcGlobal.btcDominanceChange24hPct
1605
- ),
1606
- ethDominanceChange24hPct: toFiniteNumber(
1607
- cmcGlobal.ethDominanceChange24hPct
1608
- ),
1609
- altMarketCapChange24hPct: toFiniteNumber(
1610
- cmcGlobal.altMarketCapChange24hPct
1611
- ),
1612
- altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
1613
- activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
1614
- activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
1615
- activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
1616
- altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
1617
- };
2043
+ await state.pluginsLoadPromise;
1618
2044
  };
1619
- var buildCmcReferenceAssetsContextFromSignal = (signal) => {
1620
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1621
- const relative = toRecord(baseContext?.relative);
1622
- const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
1623
- if (!cmcReferenceAssets) {
1624
- return {
1625
- source: null,
1626
- available: false,
1627
- interval: null,
1628
- asOfTs: null,
1629
- stale: null,
1630
- btcMarketCapUsd: null,
1631
- ethMarketCapUsd: null,
1632
- btcVolumeUsd: null,
1633
- ethVolumeUsd: null,
1634
- btcVolumeToMarketCap: null,
1635
- ethVolumeToMarketCap: null,
1636
- ethBtcMarketCapRatio: null,
1637
- ethBtcMarketCapRatioChange24hPct: null,
1638
- ethVsBtcVolumeRatio: null,
1639
- referenceLiquidityRegime: null
1640
- };
2045
+ var getStrategyCreator = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2046
+ if (moduleName?.trim()) {
2047
+ await ensureStrategyPluginsLoaded(cwd);
2048
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).creator;
1641
2049
  }
1642
- return {
1643
- source: String(cmcReferenceAssets.source ?? ""),
1644
- available: true,
1645
- interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
1646
- asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
1647
- stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
1648
- btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
1649
- ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
1650
- btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
1651
- ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
1652
- btcVolumeToMarketCap: toFiniteNumber(
1653
- cmcReferenceAssets.btcVolumeToMarketCap
1654
- ),
1655
- ethVolumeToMarketCap: toFiniteNumber(
1656
- cmcReferenceAssets.ethVolumeToMarketCap
1657
- ),
1658
- ethBtcMarketCapRatio: toFiniteNumber(
1659
- cmcReferenceAssets.ethBtcMarketCapRatio
1660
- ),
1661
- ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
1662
- cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
1663
- ),
1664
- ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
1665
- referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
1666
- };
2050
+ await ensureStrategyPluginsLoaded(cwd);
2051
+ const { state } = getStrategyRegistryState(cwd);
2052
+ return state.strategyCreators.get(name);
1667
2053
  };
1668
- var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
1669
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1670
- const relative = toRecord(baseContext?.relative);
1671
- const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
1672
- if (!cmcExchangeLiquidity) {
1673
- return {
1674
- source: null,
1675
- available: false,
1676
- interval: null,
1677
- asOfTs: null,
1678
- stale: null,
1679
- exchangesCount: null,
1680
- totalVolumeUsd: null,
1681
- totalVolumeChange24hPct: null,
1682
- binanceVolumeUsd: null,
1683
- binanceVolumeShare: null,
1684
- topExchangeVolumeShare: null,
1685
- liquidityRegime: null
1686
- };
2054
+ var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
2055
+ if (!name) {
2056
+ return void 0;
1687
2057
  }
1688
- return {
1689
- source: String(cmcExchangeLiquidity.source ?? ""),
1690
- available: true,
1691
- interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
1692
- asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
1693
- stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
1694
- exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
1695
- totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
1696
- totalVolumeChange24hPct: toFiniteNumber(
1697
- cmcExchangeLiquidity.totalVolumeChange24hPct
1698
- ),
1699
- binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
1700
- binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
1701
- topExchangeVolumeShare: toFiniteNumber(
1702
- cmcExchangeLiquidity.topExchangeVolumeShare
1703
- ),
1704
- liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
1705
- };
2058
+ const { state } = getStrategyRegistryState(cwd);
2059
+ return state.strategyManifestsMap.get(name);
1706
2060
  };
1707
- var buildCmcFearGreedContextFromSignal = (signal) => {
1708
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1709
- const relative = toRecord(baseContext?.relative);
1710
- const cmcFearGreed = toRecord(relative?.cmcFearGreed);
1711
- if (!cmcFearGreed) {
1712
- return {
1713
- source: null,
1714
- available: false,
1715
- interval: null,
1716
- asOfTs: null,
1717
- stale: null,
1718
- value: null,
1719
- valueChange24h: null,
1720
- valueChange7d: null,
1721
- classification: null,
1722
- sentimentRegime: null
1723
- };
2061
+ var strategies = new Proxy(
2062
+ {},
2063
+ {
2064
+ get: (_target, property) => {
2065
+ if (typeof property !== "string") {
2066
+ return void 0;
2067
+ }
2068
+ return getStrategyRegistryState().state.strategyCreators.get(property);
2069
+ },
2070
+ ownKeys: () => {
2071
+ return [...getStrategyRegistryState().state.strategyCreators.keys()];
2072
+ },
2073
+ getOwnPropertyDescriptor: () => ({
2074
+ enumerable: true,
2075
+ configurable: true
2076
+ })
1724
2077
  }
1725
- return {
1726
- source: String(cmcFearGreed.source ?? ""),
1727
- available: true,
1728
- interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
1729
- asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
1730
- stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
1731
- value: toFiniteNumber(cmcFearGreed.value),
1732
- valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
1733
- valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
1734
- classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
1735
- sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
1736
- };
2078
+ );
2079
+
2080
+ // src/strategyHelpers/tickerProfiles.ts
2081
+ var snapshots = /* @__PURE__ */ new Map();
2082
+ var resolveTickerProfileSource = (strategy) => {
2083
+ const declared = getStrategyManifest(strategy)?.contextRequirements?.tickerProfiles;
2084
+ if (declared) return declared;
2085
+ const provider = process.env.TICKER_PROFILES_PROVIDER?.trim();
2086
+ const universeId = process.env.TICKER_PROFILES_UNIVERSE?.trim();
2087
+ if (!provider && !universeId) return void 0;
2088
+ if (!provider || !universeId)
2089
+ throw new Error(
2090
+ "Set both TICKER_PROFILES_PROVIDER and TICKER_PROFILES_UNIVERSE"
2091
+ );
2092
+ return { provider, universeId };
1737
2093
  };
1738
- var buildCmcIndexesContextFromSignal = (signal) => {
1739
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1740
- const relative = toRecord(baseContext?.relative);
1741
- const cmcIndexes = toRecord(relative?.cmcIndexes);
1742
- if (!cmcIndexes) {
1743
- return {
1744
- source: null,
1745
- available: false,
1746
- interval: null,
1747
- asOfTs: null,
1748
- stale: null,
1749
- cmc100Value: null,
1750
- cmc100Change24hPct: null,
1751
- cmc100TopConstituentSymbol: null,
1752
- cmc100TopConstituentWeightPct: null,
1753
- cmc20Value: null,
1754
- cmc20Change24hPct: null,
1755
- cmc20TopConstituentSymbol: null,
1756
- cmc20TopConstituentWeightPct: null,
1757
- cmc20ToCmc100Ratio: null,
1758
- cmc20ToCmc100RatioChange24hPct: null,
1759
- indexRegime: null
1760
- };
2094
+ var loadTickerProfileContext = async ({
2095
+ projectRoot,
2096
+ provider,
2097
+ universeId,
2098
+ symbol,
2099
+ timestamp
2100
+ }) => {
2101
+ const asOf = (0, import_tickerProfiles2.tickerProfileMonth)(timestamp), key = JSON.stringify([projectRoot, provider, universeId, asOf]);
2102
+ let cached = snapshots.get(key);
2103
+ if (!cached || cached.expires < Date.now()) {
2104
+ const value = (0, import_tickerProfiles.readTickerProfiles)(projectRoot, provider, universeId, asOf).then((envelope) => {
2105
+ if (!envelope) return null;
2106
+ const s = envelope.snapshot;
2107
+ if (!(0, import_tickerProfiles2.isTickerProfileSnapshot)(s) || s.asOf !== asOf || s.provider !== provider || s.universeId !== universeId)
2108
+ throw new Error("Ticker profile identity/schema mismatch");
2109
+ const entry = snapshots.get(key);
2110
+ if (entry) entry.expires = Infinity;
2111
+ return new Map(
2112
+ s.profiles.map((p) => [
2113
+ p.symbol,
2114
+ Object.freeze({
2115
+ ...p,
2116
+ definition: s.definition,
2117
+ provider,
2118
+ universeId,
2119
+ snapshotId: envelope.snapshotId
2120
+ })
2121
+ ])
2122
+ );
2123
+ }).catch((error) => {
2124
+ snapshots.delete(key);
2125
+ throw error;
2126
+ });
2127
+ cached = { expires: Date.now() + 6e4, value };
2128
+ snapshots.set(key, cached);
2129
+ if (snapshots.size > 24) {
2130
+ const oldest = snapshots.keys().next().value;
2131
+ if (oldest) snapshots.delete(oldest);
2132
+ }
1761
2133
  }
1762
- return {
1763
- source: String(cmcIndexes.source ?? ""),
1764
- available: true,
1765
- interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
1766
- asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
1767
- stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
1768
- cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
1769
- cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
1770
- cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
1771
- cmc100TopConstituentWeightPct: toFiniteNumber(
1772
- cmcIndexes.cmc100TopConstituentWeightPct
1773
- ),
1774
- cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
1775
- cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
1776
- cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
1777
- cmc20TopConstituentWeightPct: toFiniteNumber(
1778
- cmcIndexes.cmc20TopConstituentWeightPct
1779
- ),
1780
- cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
1781
- cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
1782
- cmcIndexes.cmc20ToCmc100RatioChange24hPct
1783
- ),
1784
- indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
1785
- };
2134
+ return (await cached.value)?.get(symbol);
1786
2135
  };
1787
- var buildReferenceTradeFlowContextFromSignal = (signal) => {
1788
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1789
- const relative = toRecord(baseContext?.relative);
1790
- const refs = toRecord(relative?.referenceTradeFlow);
1791
- const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
1792
- const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
1793
- const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
1794
- if (!refs) {
1795
- return {
1796
- source: null,
1797
- available: false,
1798
- primaryReferenceSymbol: null,
1799
- referenceSymbols: [],
1800
- primaryTradeFlowBuyPressurePct: null,
1801
- primaryTradeFlowStale: null
2136
+ var withTickerProfile = (baseContext, profile) => Object.defineProperty(
2137
+ Object.create(
2138
+ Object.getPrototypeOf(baseContext),
2139
+ Object.getOwnPropertyDescriptors(baseContext)
2140
+ ),
2141
+ "tickerProfile",
2142
+ { enumerable: true, configurable: true, value: profile }
2143
+ );
2144
+ var enrichSignalWithTickerProfile = async (signal) => {
2145
+ const source = resolveTickerProfileSource(signal.strategy);
2146
+ if (!source || signal.universe === "tradfi" || !signal.symbol.endsWith("USDT"))
2147
+ return;
2148
+ const profile = await loadTickerProfileContext({
2149
+ ...source,
2150
+ projectRoot: getTradejsProjectCwd(),
2151
+ symbol: signal.symbol,
2152
+ timestamp: signal.timestamp
2153
+ });
2154
+ if (profile) {
2155
+ signal.additionalIndicators = {
2156
+ ...signal.additionalIndicators,
2157
+ tickerProfile: profile
1802
2158
  };
1803
2159
  }
1804
- return {
1805
- source: String(refs.source ?? ""),
1806
- available: true,
1807
- primaryReferenceSymbol,
1808
- referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
1809
- primaryTradeFlowBuyPressurePct: toFiniteNumber(
1810
- primaryTradeFlow?.buyPressurePct
1811
- ),
1812
- primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
1813
- };
1814
2160
  };
1815
- var buildAiMarketContext = (signal) => ({
1816
- execution: {
1817
- binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
1818
- },
1819
- participation: {
1820
- trueDelta: buildTrueDeltaContextFromSignal(signal),
1821
- tradeFlow: buildTradeFlowContextFromSignal(signal)
1822
- },
1823
- relative: {
1824
- marketBreadth: buildMarketBreadthContextFromSignal(signal),
1825
- marketBreadths: buildMarketBreadthsContextFromSignal(signal),
1826
- targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
1827
- btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
1828
- cmcGlobal: buildCmcGlobalContextFromSignal(signal),
1829
- cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
1830
- cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
1831
- cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
1832
- cmcIndexes: buildCmcIndexesContextFromSignal(signal),
1833
- referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
1834
- }
1835
- });
1836
2161
 
1837
- // src/strategy/manifests.ts
1838
- var import_indicators = require("@tradejs/core/indicators");
2162
+ // src/strategyRuntime.ts
2163
+ var import_jev6 = require("@tradejs/core/jev");
1839
2164
 
1840
- // src/tradejsConfig.ts
1841
- var import_fs = __toESM(require("fs"));
1842
- var import_path = __toESM(require("path"));
1843
- var import_url = require("url");
1844
- var import_config = require("@tradejs/core/config");
1845
- var import_logger2 = require("@tradejs/infra/logger");
1846
- var CONFIG_FILE_NAMES = [
1847
- "tradejs.config.ts",
1848
- "tradejs.config.mts",
1849
- "tradejs.config.js",
1850
- "tradejs.config.mjs",
1851
- "tradejs.config.cjs"
1852
- ];
1853
- var TS_MODULE_RE = /\.(cts|mts|ts)$/i;
1854
- var cachedByCwd = /* @__PURE__ */ new Map();
1855
- var announcedConfigFile = /* @__PURE__ */ new Set();
1856
- var tsNodeRegistered = false;
1857
- var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
1858
- var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
1859
- var getTradejsProjectCwd = (cwd) => {
1860
- const explicit = String(cwd ?? "").trim();
1861
- if (explicit) {
1862
- return import_path.default.resolve(explicit);
2165
+ // src/jev.ts
2166
+ var import_promises = __toESM(require("fs/promises"));
2167
+ var import_node_path = __toESM(require("path"));
2168
+ var import_jev4 = require("@tradejs/core/jev");
2169
+ var import_jev5 = require("@tradejs/infra/jev");
2170
+ var import_userSettings = require("@tradejs/infra/userSettings");
2171
+ init_jevInput();
2172
+ init_jevInput();
2173
+ init_jevTraining();
2174
+ var resolveJevProvider = async (userName) => {
2175
+ const settings = await (0, import_userSettings.getUserSettings)(userName);
2176
+ const provider = {
2177
+ endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
2178
+ model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
2179
+ };
2180
+ (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
2181
+ if (!settings.JEV_API_KEY)
2182
+ throw new Error("Configure the Jev API key in Account settings");
2183
+ return provider;
2184
+ };
2185
+ var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
2186
+ var pending = /* @__PURE__ */ new Map();
2187
+ var pendingResponses = /* @__PURE__ */ new Map();
2188
+ var evaluateJevInput = async ({
2189
+ input,
2190
+ config,
2191
+ userName,
2192
+ projectRoot
2193
+ }) => {
2194
+ config = (0, import_jev4.parseJevConfig)(config);
2195
+ const inputHash = (0, import_jev5.jevHash)(input);
2196
+ const questions = questionsForJevInput(input);
2197
+ const questionsHash = (0, import_jev5.jevHash)(questions);
2198
+ const base = {
2199
+ questionSet: import_jev4.JEV_QUESTION_SET,
2200
+ schema: "tradejs-signal-assessment/v5",
2201
+ source: config.source,
2202
+ mode: config.mode,
2203
+ inputHash,
2204
+ questionsHash,
2205
+ geometryStatus: input.geometryStatus,
2206
+ levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
2207
+ };
2208
+ if (!input.questions.length)
2209
+ return {
2210
+ assessment: {
2211
+ ...base,
2212
+ status: "unavailable",
2213
+ model: "",
2214
+ scores: emptyScores(),
2215
+ confidence: emptyScores()
2216
+ }
2217
+ };
2218
+ if (config.source === "local") {
2219
+ const contents = await import_promises.default.readFile(
2220
+ import_node_path.default.resolve(projectRoot, config.modelFile),
2221
+ "utf8"
2222
+ );
2223
+ if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
2224
+ throw new Error("Local Jev gate checksum mismatch");
2225
+ const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
2226
+ const model = validateJevGateModel2(JSON.parse(contents));
2227
+ if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
2228
+ throw new Error("Local Jev gate strategy or question version mismatch");
2229
+ const scores = Object.fromEntries(
2230
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2231
+ key,
2232
+ input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
2233
+ ])
2234
+ );
2235
+ return {
2236
+ assessment: {
2237
+ ...base,
2238
+ status: "available",
2239
+ model: config.modelSha256,
2240
+ scores,
2241
+ confidence: emptyScores()
2242
+ }
2243
+ };
1863
2244
  }
1864
- const fromEnv = String(process.env.PROJECT_CWD || "").trim();
1865
- if (fromEnv) {
1866
- return import_path.default.resolve(fromEnv);
2245
+ const provider = config.provider ?? await resolveJevProvider(userName);
2246
+ const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
2247
+ const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
2248
+ const file = import_node_path.default.join(dir, "records", `${id}.json`);
2249
+ const getRecord = async () => {
2250
+ const existing = await (0, import_jev5.readJevArtifact)(file);
2251
+ if (existing) {
2252
+ 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))
2253
+ throw new Error("Jev recording identity mismatch");
2254
+ (0, import_jev5.validateJevResponse)(existing.response, input.questions);
2255
+ for (const key of import_jev4.JEV_DIMENSIONS) {
2256
+ const expected = input.questions.includes(key) ? existing.response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null;
2257
+ if (existing.scores[key] !== expected)
2258
+ throw new Error("Jev recording scores do not match the response");
2259
+ }
2260
+ return existing;
2261
+ }
2262
+ if (config.source === "recorded")
2263
+ throw new Error(
2264
+ `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.`
2265
+ );
2266
+ const settings = await (0, import_userSettings.getUserSettings)(userName);
2267
+ if (!settings.JEV_API_KEY)
2268
+ throw new Error("Configure the Jev API key in Account settings");
2269
+ const apiKey = settings.JEV_API_KEY;
2270
+ const configuredProvider = await resolveJevProvider(userName);
2271
+ if (configuredProvider.endpoint !== provider.endpoint)
2272
+ throw new Error(
2273
+ "Jev account endpoint changed; update the frozen provider configuration"
2274
+ );
2275
+ const started = Date.now();
2276
+ const state = stateForJevInput(input);
2277
+ const responseId = (0, import_jev5.jevHash)({
2278
+ state,
2279
+ questionsHash,
2280
+ provider,
2281
+ user: (0, import_jev5.jevHash)(userName)
2282
+ });
2283
+ const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
2284
+ let responsePromise = pendingResponses.get(responseFile);
2285
+ if (!responsePromise) {
2286
+ responsePromise = (async () => {
2287
+ const saved = await (0, import_jev5.readJevArtifact)(responseFile);
2288
+ if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
2289
+ const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
2290
+ await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
2291
+ return fresh;
2292
+ })();
2293
+ pendingResponses.set(responseFile, responsePromise);
2294
+ }
2295
+ let response;
2296
+ try {
2297
+ response = await responsePromise;
2298
+ } finally {
2299
+ pendingResponses.delete(responseFile);
2300
+ }
2301
+ const scores = Object.fromEntries(
2302
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2303
+ key,
2304
+ input.questions.includes(key) ? response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null
2305
+ ])
2306
+ );
2307
+ const value = {
2308
+ schema: "tradejs-jev-record/v4",
2309
+ id,
2310
+ inputHash,
2311
+ questionsHash,
2312
+ provider,
2313
+ input,
2314
+ response,
2315
+ scores,
2316
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2317
+ elapsedMs: Date.now() - started
2318
+ };
2319
+ await (0, import_jev5.writeJevArtifact)(file, value);
2320
+ return await (0, import_jev5.readJevArtifact)(file);
2321
+ };
2322
+ const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
2323
+ let promise = pending.get(pendingKey);
2324
+ if (!promise) {
2325
+ promise = getRecord();
2326
+ pending.set(pendingKey, promise);
1867
2327
  }
1868
- return process.cwd();
1869
- };
1870
- var normalizeConfig = (rawConfig) => {
1871
- if (!rawConfig || typeof rawConfig !== "object") {
1872
- return {};
2328
+ let record;
2329
+ try {
2330
+ record = await promise;
2331
+ } finally {
2332
+ pending.delete(pendingKey);
1873
2333
  }
1874
- const config = rawConfig;
1875
- const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
1876
- const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
1877
- const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
1878
- const hooks = (0, import_config.normalizeTradejsConfigHooks)(
1879
- config.hooks
1880
- );
1881
- const runtime = config.runtime && typeof config.runtime === "object" && !Array.isArray(config.runtime) ? config.runtime : void 0;
1882
2334
  return {
1883
- strategies: strategies2,
1884
- indicators,
1885
- connectors,
1886
- ...hooks ? { hooks } : {},
1887
- ...runtime ? { runtime } : {}
2335
+ record,
2336
+ assessment: {
2337
+ ...base,
2338
+ status: "available",
2339
+ recordId: id,
2340
+ model: record.response.model,
2341
+ scores: record.scores,
2342
+ confidence: Object.fromEntries(
2343
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2344
+ key,
2345
+ record.response.answers[key]?.confidence ?? null
2346
+ ])
2347
+ )
2348
+ }
1888
2349
  };
1889
2350
  };
1890
- var getNodeCreateRequire = () => {
1891
- const builtinModule = process.getBuiltinModule?.("module");
1892
- if (typeof builtinModule?.createRequire === "function") {
1893
- return builtinModule.createRequire;
2351
+ var assessSignalWithJev = async ({
2352
+ signal,
2353
+ config,
2354
+ userName,
2355
+ projectRoot,
2356
+ strict,
2357
+ scope = "runtime"
2358
+ }) => {
2359
+ let assessment;
2360
+ try {
2361
+ const input = buildJevInput(signal);
2362
+ ({ assessment } = await evaluateJevInput({
2363
+ input,
2364
+ config,
2365
+ userName,
2366
+ projectRoot
2367
+ }));
2368
+ } catch (error) {
2369
+ if (strict) throw error;
2370
+ assessment = {
2371
+ questionSet: import_jev4.JEV_QUESTION_SET,
2372
+ schema: "tradejs-signal-assessment/v5",
2373
+ source: config.source,
2374
+ mode: config.mode,
2375
+ status: "unavailable",
2376
+ inputHash: "",
2377
+ questionsHash: "",
2378
+ geometryStatus: "absent",
2379
+ levelsValid: null,
2380
+ model: config.provider?.model ?? config.modelSha256 ?? "",
2381
+ scores: emptyScores(),
2382
+ confidence: emptyScores()
2383
+ };
1894
2384
  }
1895
- throw new TypeError("module.createRequire is not available");
2385
+ signal.assessment = assessment;
2386
+ signal.additionalIndicators = {
2387
+ ...signal.additionalIndicators,
2388
+ jev: (0, import_jev4.jevAssessmentFeatures)(assessment)
2389
+ };
2390
+ const identity = {
2391
+ signalId: signal.signalId,
2392
+ strategy: signal.strategy,
2393
+ symbol: signal.symbol,
2394
+ timestamp: signal.timestamp,
2395
+ scope,
2396
+ user: (0, import_jev5.jevHash)(userName),
2397
+ config,
2398
+ assessment
2399
+ };
2400
+ await (0, import_jev5.writeJevArtifact)(
2401
+ import_node_path.default.resolve(
2402
+ projectRoot,
2403
+ config.recordsDir ?? "data/ai/jev",
2404
+ "decisions",
2405
+ `${(0, import_jev5.jevHash)(identity)}.json`
2406
+ ),
2407
+ identity
2408
+ );
2409
+ return assessment;
1896
2410
  };
1897
- var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
1898
- var ensureTsNodeRegistered = async () => {
1899
- if (tsNodeRegistered) {
1900
- return;
2411
+
2412
+ // src/strategyRuntime.ts
2413
+ var import_constants5 = require("@tradejs/core/constants");
2414
+ var import_strategies6 = require("@tradejs/core/strategies");
2415
+ var import_logger11 = require("@tradejs/infra/logger");
2416
+
2417
+ // src/strategyHelpers/runtime.ts
2418
+ var import_logger9 = require("@tradejs/infra/logger");
2419
+ var import_constants3 = require("@tradejs/core/constants");
2420
+ var import_ml2 = require("@tradejs/infra/ml");
2421
+
2422
+ // src/strategy/entrySubmissionValidation.ts
2423
+ var import_logger3 = require("@tradejs/infra/logger");
2424
+
2425
+ // src/strategy/entryExecutionValidation.ts
2426
+ var EntrySubmissionValidationError = class extends Error {
2427
+ constructor() {
2428
+ super(...arguments);
2429
+ this.name = "EntrySubmissionValidationError";
1901
2430
  }
1902
- const tsNodeModule = await import("ts-node");
1903
- const tsNode = tsNodeModule.default ?? tsNodeModule;
1904
- tsNode.register?.({
1905
- transpileOnly: true,
1906
- compilerOptions: {
1907
- module: "Node16",
1908
- moduleResolution: "node16"
1909
- }
1910
- });
1911
- tsNodeRegistered = true;
1912
2431
  };
1913
- var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
1914
- const projectRoot = getTradejsProjectCwd(cwd);
1915
- if (tsconfigPathsRegisteredByCwd.has(projectRoot)) {
1916
- return;
1917
- }
1918
- const tsconfigPathsModule = await import("tsconfig-paths");
1919
- const loadConfig = tsconfigPathsModule.loadConfig;
1920
- const register = tsconfigPathsModule.register;
1921
- if (typeof loadConfig !== "function" || typeof register !== "function") {
1922
- return;
1923
- }
1924
- const loadedConfig = loadConfig(projectRoot);
1925
- if (loadedConfig.resultType !== "success") {
1926
- return;
1927
- }
1928
- register({
1929
- baseUrl: loadedConfig.absoluteBaseUrl,
1930
- paths: loadedConfig.paths,
1931
- addMatchAll: false
1932
- });
1933
- tsconfigPathsRegisteredByCwd.add(projectRoot);
2432
+ var resolveEntryExecutionLimits = (config, direction) => {
2433
+ const side = config[direction];
2434
+ const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
2435
+ const maximum = config.MAX_LOSS_VALUE;
2436
+ if (minimum == null && maximum == null) return void 0;
2437
+ const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
2438
+ const maxLossValue = maximum == null ? void 0 : Number(maximum);
2439
+ const feeRate = Number(config.RISK_FEE_RATE ?? 0);
2440
+ const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
2441
+ const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
2442
+ const slippageBps = slippage + impact;
2443
+ 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)
2444
+ throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
2445
+ return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
1934
2446
  };
1935
- var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1936
- const projectRoot = getTradejsProjectCwd(cwd);
1937
- const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
1938
- if (cachedMatcher) {
1939
- const resolved2 = cachedMatcher(moduleName);
1940
- return resolved2 || null;
1941
- }
1942
- const tsconfigPathsModule = await import("tsconfig-paths");
1943
- const loadConfig = tsconfigPathsModule.loadConfig;
1944
- const createMatchPath = tsconfigPathsModule.createMatchPath;
1945
- if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
1946
- return null;
1947
- }
1948
- const loadedConfig = loadConfig(projectRoot);
1949
- if (loadedConfig.resultType !== "success") {
1950
- return null;
1951
- }
1952
- const matchPath = createMatchPath(
1953
- loadedConfig.absoluteBaseUrl,
1954
- loadedConfig.paths
2447
+ var validateEntryExecution = ({
2448
+ direction,
2449
+ price,
2450
+ qty,
2451
+ stopLossPrice,
2452
+ takeProfitPrice,
2453
+ limits
2454
+ }) => {
2455
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
2456
+ throw new EntrySubmissionValidationError(
2457
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2458
+ );
2459
+ const side = direction === "LONG" ? 1 : -1;
2460
+ if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
2461
+ throw new EntrySubmissionValidationError(
2462
+ "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
2463
+ );
2464
+ const slippage = limits.slippageBps / 1e4;
2465
+ const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
2466
+ const entryCost = cost(price, side);
2467
+ const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
2468
+ const rewardPerUnit = Math.max(
2469
+ 0,
2470
+ Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
1955
2471
  );
1956
- const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
1957
- ".ts",
1958
- ".tsx",
1959
- ".mts",
1960
- ".cts",
1961
- ".js",
1962
- ".jsx",
1963
- ".mjs",
1964
- ".cjs",
1965
- ".json"
1966
- ]) || "";
1967
- tsconfigPathMatchersByCwd.set(projectRoot, matcher);
1968
- const resolved = matcher(moduleName);
1969
- return resolved || null;
1970
- };
1971
- var toImportSpecifier = (moduleName) => {
1972
- if (moduleName.startsWith("file://")) {
1973
- return moduleName;
1974
- }
1975
- if (import_path.default.isAbsolute(moduleName)) {
1976
- return (0, import_url.pathToFileURL)(moduleName).href;
1977
- }
1978
- return moduleName;
2472
+ const netRiskRatio = rewardPerUnit / lossPerUnit;
2473
+ const plannedLossValue = qty * lossPerUnit;
2474
+ if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
2475
+ throw new EntrySubmissionValidationError(
2476
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2477
+ );
2478
+ const belowMinimumNetRiskRatio = limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio;
2479
+ const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
2480
+ return {
2481
+ netRiskRatio,
2482
+ plannedLossValue,
2483
+ riskExceeded,
2484
+ belowMinimumNetRiskRatio
2485
+ };
1979
2486
  };
1980
- var isTsModulePath = (moduleName) => TS_MODULE_RE.test(moduleName.split("?")[0]);
1981
- var isRelativeModulePath = (moduleName) => moduleName.startsWith("./") || moduleName.startsWith("../");
1982
- var isBareModuleSpecifier = (moduleName) => {
1983
- const normalized = String(moduleName ?? "").trim();
1984
- if (!normalized) {
1985
- return false;
1986
- }
1987
- if (normalized.startsWith("file://") || import_path.default.isAbsolute(normalized) || isRelativeModulePath(normalized)) {
1988
- return false;
2487
+
2488
+ // src/strategy/entrySubmissionValidation.ts
2489
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
2490
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
2491
+ var getOrderArrivalSnapshot = async ({
2492
+ connector,
2493
+ symbol,
2494
+ requireTimestamp = false
2495
+ }) => {
2496
+ if (typeof connector.getTopOfBookTicker !== "function") {
2497
+ return {
2498
+ arrivalSnapshotTime: Date.now(),
2499
+ arrivalSource: "unavailable",
2500
+ bid: null,
2501
+ ask: null,
2502
+ arrivalMid: null,
2503
+ spreadBps: null
2504
+ };
1989
2505
  }
1990
- return true;
1991
- };
1992
- var importConfigFile = async (configFilePath) => {
1993
- const ext = import_path.default.extname(configFilePath).toLowerCase();
1994
- const configFileUrl = `${toImportSpecifier(configFilePath)}?t=${Date.now()}`;
1995
- if (ext === ".ts" || ext === ".mts") {
1996
- const requireFn = getRequireFn(import_path.default.dirname(configFilePath));
1997
- await ensureTsNodeRegistered();
1998
- await ensureTsconfigPathsRegistered(import_path.default.dirname(configFilePath));
1999
- return requireFn(configFilePath);
2506
+ try {
2507
+ const ticker = await connector.getTopOfBookTicker(symbol);
2508
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
2509
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
2510
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
2511
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
2512
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
2513
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
2514
+ return {
2515
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
2516
+ arrivalSource: "top_of_book",
2517
+ bid,
2518
+ ask,
2519
+ arrivalMid,
2520
+ spreadBps
2521
+ };
2522
+ } catch (error) {
2523
+ import_logger3.logger.warn(
2524
+ "runtime order arrival snapshot failed: %s %s",
2525
+ symbol,
2526
+ error?.message || String(error)
2527
+ );
2528
+ return {
2529
+ arrivalSnapshotTime: Date.now(),
2530
+ arrivalSource: "top_of_book_error",
2531
+ bid: null,
2532
+ ask: null,
2533
+ arrivalMid: null,
2534
+ spreadBps: null
2535
+ };
2000
2536
  }
2001
- return import(
2002
- /* webpackIgnore: true */
2003
- configFileUrl
2004
- );
2005
2537
  };
2006
- var importTradejsModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
2007
- const normalized = String(moduleName ?? "").trim();
2008
- if (!normalized) {
2009
- return {};
2538
+ var validateEntryProtectionAtArrival = ({
2539
+ direction,
2540
+ signalPrice,
2541
+ bid,
2542
+ ask,
2543
+ arrivalMid,
2544
+ takeProfits,
2545
+ stopLossPrice
2546
+ }) => {
2547
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
2548
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
2549
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
2550
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
2010
2551
  }
2011
- let modulePath = normalized;
2012
- if (normalized.startsWith("file://")) {
2013
- try {
2014
- modulePath = (0, import_url.fileURLToPath)(normalized);
2015
- } catch {
2016
- modulePath = normalized;
2552
+ for (const takeProfit of takeProfits) {
2553
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
2554
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
2017
2555
  }
2018
2556
  }
2019
- const requireFn = getRequireFn(
2020
- import_path.default.isAbsolute(modulePath) ? import_path.default.dirname(modulePath) : cwd
2021
- );
2022
- if (isTsModulePath(modulePath)) {
2023
- await ensureTsNodeRegistered();
2024
- await ensureTsconfigPathsRegistered(cwd);
2025
- return requireFn(modulePath);
2557
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
2558
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
2026
2559
  }
2027
- if (isBareModuleSpecifier(normalized)) {
2028
- await ensureTsconfigPathsRegistered(cwd);
2029
- try {
2030
- return requireFn(normalized);
2031
- } catch (error) {
2032
- const resolvedByTsconfig = await resolveTsconfigPathModule(
2033
- normalized,
2034
- cwd
2560
+ };
2561
+ var createEntrySubmissionValidation = ({
2562
+ connector,
2563
+ symbol,
2564
+ direction,
2565
+ takeProfits,
2566
+ stopLossPrice,
2567
+ limits,
2568
+ onValidated
2569
+ }) => {
2570
+ if (!limits) return void 0;
2571
+ if (!connector.capabilities?.orderSubmissionValidation)
2572
+ throw new EntrySubmissionValidationError(
2573
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
2574
+ );
2575
+ return async ({ qty, executionPrice }) => {
2576
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
2577
+ const snapshot2 = simulated && executionPrice != null ? {
2578
+ arrivalSnapshotTime: Date.now(),
2579
+ arrivalSource: "simulation",
2580
+ bid: executionPrice,
2581
+ ask: executionPrice,
2582
+ arrivalMid: executionPrice,
2583
+ spreadBps: 0
2584
+ } : await getOrderArrivalSnapshot({
2585
+ connector,
2586
+ symbol,
2587
+ requireTimestamp: true
2588
+ });
2589
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
2590
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
2591
+ throw new EntrySubmissionValidationError(
2592
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
2035
2593
  );
2036
- if (resolvedByTsconfig && resolvedByTsconfig !== normalized) {
2037
- return requireFn(resolvedByTsconfig);
2038
- }
2039
- throw error;
2594
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
2595
+ throw new EntrySubmissionValidationError(
2596
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
2597
+ );
2598
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
2599
+ validateEntryProtectionAtArrival({
2600
+ direction,
2601
+ signalPrice: price,
2602
+ bid: snapshot2.bid,
2603
+ ask: snapshot2.ask,
2604
+ arrivalMid: snapshot2.arrivalMid,
2605
+ takeProfits,
2606
+ stopLossPrice
2607
+ });
2608
+ const prices = takeProfits.map((tp) => tp.price);
2609
+ const economics2 = validateEntryExecution({
2610
+ direction,
2611
+ price,
2612
+ qty,
2613
+ stopLossPrice,
2614
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
2615
+ limits
2616
+ });
2617
+ if (economics2.belowMinimumNetRiskRatio) {
2618
+ import_logger3.logger.warn(
2619
+ "entry submission warning: %s %s ENTRY_RR_BELOW_MIN_AT_SUBMISSION (RR %s, threshold %s)",
2620
+ symbol,
2621
+ direction,
2622
+ economics2.netRiskRatio,
2623
+ limits.minimumNetRiskRatio
2624
+ );
2625
+ }
2626
+ onValidated?.(snapshot2, economics2);
2627
+ };
2628
+ };
2629
+
2630
+ // src/ai.ts
2631
+ var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
2632
+ var import_redis3 = require("@tradejs/infra/redis");
2633
+
2634
+ // src/strategyAdapters/ai.ts
2635
+ var import_indicators2 = require("@tradejs/core/indicators");
2636
+ var import_aiFeatures = require("@tradejs/core/aiFeatures");
2637
+
2638
+ // src/aiShared.ts
2639
+ var MAX_AI_SERIES_POINTS = 5;
2640
+ var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
2641
+ "tradejs.indicators.compactSnapshot"
2642
+ );
2643
+ var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
2644
+ var trimSeriesDeep = (value) => {
2645
+ if (Array.isArray(value)) {
2646
+ const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
2647
+ const isMatrix = trimmed.every((item) => Array.isArray(item));
2648
+ if (isMatrix) {
2649
+ return trimmed;
2040
2650
  }
2651
+ return trimmed.map(
2652
+ (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
2653
+ );
2041
2654
  }
2042
- try {
2043
- return await import(
2044
- /* webpackIgnore: true */
2045
- toImportSpecifier(normalized)
2655
+ if (value && typeof value === "object") {
2656
+ return Object.fromEntries(
2657
+ Object.entries(value).map(([key, nested]) => [
2658
+ key,
2659
+ trimSeriesDeep(nested)
2660
+ ])
2046
2661
  );
2047
- } catch (error) {
2048
- if (isTsModulePath(modulePath)) {
2049
- await ensureTsNodeRegistered();
2050
- await ensureTsconfigPathsRegistered(cwd);
2051
- return requireFn(modulePath);
2052
- }
2053
- throw error;
2054
2662
  }
2663
+ return value;
2055
2664
  };
2056
- var resolveExportedConfig = (moduleExports) => {
2057
- const candidate = moduleExports && typeof moduleExports === "object" && "default" in moduleExports ? moduleExports.default : moduleExports;
2058
- return normalizeConfig(candidate);
2059
- };
2060
- var findConfigFilePath = (cwd) => {
2061
- let currentDir = import_path.default.resolve(cwd);
2062
- while (true) {
2063
- for (const fileName of CONFIG_FILE_NAMES) {
2064
- const fullPath = import_path.default.join(currentDir, fileName);
2065
- if (import_fs.default.existsSync(fullPath) && import_fs.default.statSync(fullPath).isFile()) {
2066
- return fullPath;
2067
- }
2068
- }
2069
- const parentDir = import_path.default.dirname(currentDir);
2070
- if (parentDir === currentDir) {
2071
- return null;
2072
- }
2073
- currentDir = parentDir;
2665
+ var buildCompactAiIndicatorsSnapshot = (value) => {
2666
+ const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
2667
+ if (typeof compactSnapshot === "function") {
2668
+ return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
2074
2669
  }
2670
+ return trimSeriesDeep(value);
2075
2671
  };
2076
- var resolvePluginModuleSpecifier = (moduleName, cwd = getTradejsProjectCwd()) => {
2077
- const normalized = String(moduleName ?? "").trim();
2078
- if (!normalized) {
2079
- return "";
2080
- }
2081
- if (normalized.startsWith("file://")) {
2082
- try {
2083
- return (0, import_url.fileURLToPath)(normalized);
2084
- } catch {
2085
- return normalized;
2086
- }
2087
- }
2088
- if (import_path.default.isAbsolute(normalized)) {
2089
- return normalized;
2090
- }
2091
- if (isRelativeModulePath(normalized)) {
2092
- return import_path.default.resolve(cwd, normalized);
2672
+
2673
+ // src/aiMarketContext.ts
2674
+ var toRecord = (value) => {
2675
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2676
+ return null;
2093
2677
  }
2094
- return normalized;
2678
+ return value;
2095
2679
  };
2096
- var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
2097
- const cached = cachedByCwd.get(cwd);
2098
- if (cached) {
2099
- return cached;
2100
- }
2101
- const configFilePath = findConfigFilePath(cwd);
2102
- if (!configFilePath) {
2103
- cachedByCwd.set(cwd, {});
2104
- return {};
2105
- }
2106
- try {
2107
- const moduleExports = await importConfigFile(configFilePath);
2108
- const config = resolveExportedConfig(moduleExports);
2109
- cachedByCwd.set(cwd, config);
2110
- if (!announcedConfigFile.has(configFilePath)) {
2111
- announcedConfigFile.add(configFilePath);
2112
- import_logger2.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
2113
- }
2114
- return config;
2115
- } catch (error) {
2116
- import_logger2.logger.log(
2117
- "warn",
2118
- "Failed to load TradeJS config from %s: %s",
2119
- configFilePath,
2120
- String(error)
2121
- );
2122
- cachedByCwd.set(cwd, {});
2123
- return {};
2124
- }
2680
+ var toFiniteNumber = (value) => {
2681
+ const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
2682
+ return Number.isFinite(numeric) ? numeric : null;
2125
2683
  };
2126
-
2127
- // src/strategy/manifests.ts
2128
- var SHARED_STRATEGY_REGISTRY_KEY = "__tradejsNodeSharedStrategyRegistryV1__";
2129
- var sharedRegistryScope = globalThis;
2130
- var sharedStrategyRegistry = sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] ?? (sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] = {
2131
- registryStateByProjectRoot: /* @__PURE__ */ new Map()
2132
- });
2133
- var createStrategyRegistryState = () => ({
2134
- strategyCreators: /* @__PURE__ */ new Map(),
2135
- strategyManifestsMap: /* @__PURE__ */ new Map(),
2136
- strategyEntriesMap: /* @__PURE__ */ new Map(),
2137
- strategySourcesMap: /* @__PURE__ */ new Map(),
2138
- strategyModuleResolutions: /* @__PURE__ */ new Map(),
2139
- pluginsLoadPromise: null
2684
+ var roundTo = (value, decimals) => {
2685
+ const factor = 10 ** decimals;
2686
+ return Math.round(value * factor) / factor;
2687
+ };
2688
+ var buildMissingSpreadContext = () => ({
2689
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2690
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2691
+ available: false,
2692
+ value: null,
2693
+ zScore: null,
2694
+ bps: null,
2695
+ absBps: null,
2696
+ bias: null,
2697
+ severity: null
2140
2698
  });
2141
- var registryStateByProjectRoot = sharedStrategyRegistry.registryStateByProjectRoot;
2142
- var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
2143
- const projectRoot = getTradejsProjectCwd(cwd);
2144
- let state = registryStateByProjectRoot.get(projectRoot);
2145
- if (!state) {
2146
- state = createStrategyRegistryState();
2147
- registryStateByProjectRoot.set(projectRoot, state);
2699
+ var buildSpreadContextFromSignal = (signal) => {
2700
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2701
+ const relative = toRecord(baseContext?.relative);
2702
+ const execution = toRecord(relative?.execution);
2703
+ const spread = toFiniteNumber(execution?.venueSpread);
2704
+ const zScore = toFiniteNumber(execution?.venueSpreadZScore);
2705
+ if (spread == null) {
2706
+ return buildMissingSpreadContext();
2148
2707
  }
2708
+ const value = roundTo(spread, 8);
2709
+ const bps = roundTo(value * 1e4, 2);
2710
+ const absBps = Math.abs(bps);
2711
+ const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
2712
+ const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
2149
2713
  return {
2150
- projectRoot,
2151
- state
2714
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2715
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2716
+ available: true,
2717
+ value,
2718
+ zScore,
2719
+ bps,
2720
+ absBps,
2721
+ bias,
2722
+ severity
2152
2723
  };
2153
2724
  };
2154
- var toUniqueModules = (modules = []) => [
2155
- ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
2156
- ];
2157
- var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
2158
- const config = await loadTradejsConfig(cwd);
2725
+ var buildTrueDeltaContextFromSignal = (signal) => {
2726
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2727
+ const participation = toRecord(baseContext?.participation);
2728
+ const delta = toRecord(participation?.delta);
2729
+ const source = String(delta?.source ?? "");
2730
+ const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
2731
+ if (!delta || !isTrueDeltaSource) {
2732
+ return {
2733
+ source: source || null,
2734
+ available: false,
2735
+ buyPressurePct: null,
2736
+ buyVolume: null,
2737
+ sellVolume: null,
2738
+ netDelta: null,
2739
+ deltaPct: null,
2740
+ signedVolumeZScore: null
2741
+ };
2742
+ }
2159
2743
  return {
2160
- strategyModules: toUniqueModules(config.strategies),
2161
- indicatorModules: toUniqueModules(config.indicators)
2744
+ source,
2745
+ available: true,
2746
+ buyPressurePct: toFiniteNumber(delta.buyPressurePct),
2747
+ buyVolume: toFiniteNumber(delta.buyVolume),
2748
+ sellVolume: toFiniteNumber(delta.sellVolume),
2749
+ netDelta: toFiniteNumber(delta.netDelta),
2750
+ deltaPct: toFiniteNumber(delta.deltaPct),
2751
+ signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
2162
2752
  };
2163
2753
  };
2164
- var extractModuleEntries = (moduleExport, key) => {
2165
- if (!moduleExport || typeof moduleExport !== "object") {
2166
- return null;
2167
- }
2168
- const candidate = moduleExport;
2169
- if (Array.isArray(candidate[key])) {
2170
- return candidate[key];
2754
+ var buildTradeFlowContextFromSignal = (signal) => {
2755
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2756
+ const participation = toRecord(baseContext?.participation);
2757
+ const tradeFlow = toRecord(participation?.tradeFlow);
2758
+ if (!tradeFlow) {
2759
+ return {
2760
+ source: null,
2761
+ available: false,
2762
+ interval: null,
2763
+ stale: null,
2764
+ trades: null,
2765
+ buyPressurePct: null,
2766
+ netBaseDelta: null,
2767
+ netQuoteDelta: null
2768
+ };
2171
2769
  }
2172
- const defaultExport = candidate.default;
2173
- if (defaultExport && Array.isArray(defaultExport[key])) {
2174
- return defaultExport[key];
2770
+ return {
2771
+ source: String(tradeFlow.source ?? ""),
2772
+ available: true,
2773
+ interval: String(tradeFlow.interval ?? ""),
2774
+ stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
2775
+ trades: toFiniteNumber(tradeFlow.trades),
2776
+ buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
2777
+ netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
2778
+ netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
2779
+ };
2780
+ };
2781
+ var buildMarketBreadthContextFromSignal = (signal) => {
2782
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2783
+ const relative = toRecord(baseContext?.relative);
2784
+ const breadth = toRecord(relative?.marketBreadth);
2785
+ if (!breadth) {
2786
+ return {
2787
+ source: null,
2788
+ available: false,
2789
+ universe: null,
2790
+ interval: null,
2791
+ stale: null,
2792
+ symbolsCount: null,
2793
+ advanceDeclineRatio: null,
2794
+ pctAboveMa20: null,
2795
+ pctAboveMa50: null,
2796
+ equalWeightedReturn: null,
2797
+ volumeWeightedReturn: null,
2798
+ dispersion: null
2799
+ };
2175
2800
  }
2176
- return null;
2801
+ return {
2802
+ source: String(breadth.source ?? ""),
2803
+ available: true,
2804
+ universe: String(breadth.universe ?? ""),
2805
+ interval: String(breadth.interval ?? ""),
2806
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
2807
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
2808
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
2809
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
2810
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
2811
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
2812
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
2813
+ dispersion: toFiniteNumber(breadth.dispersion)
2814
+ };
2177
2815
  };
2178
- var extractStrategyPluginDefinition = (moduleExport) => {
2179
- const strategyEntries = extractModuleEntries(
2180
- moduleExport,
2181
- "strategyEntries"
2182
- );
2183
- return strategyEntries ? { strategyEntries } : null;
2816
+ var buildMarketBreadthsContextFromSignal = (signal) => {
2817
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2818
+ const relative = toRecord(baseContext?.relative);
2819
+ const breadths = toRecord(relative?.marketBreadths);
2820
+ const build = (key) => {
2821
+ const breadth = toRecord(breadths?.[key]);
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
+ };
2852
+ };
2853
+ return {
2854
+ top5: build("top5"),
2855
+ top10: build("top10"),
2856
+ top30: build("top30"),
2857
+ top50: build("top50"),
2858
+ top100: build("top100")
2859
+ };
2184
2860
  };
2185
- var extractIndicatorPluginDefinition = (moduleExport) => {
2186
- const indicatorEntries = extractModuleEntries(
2187
- moduleExport,
2188
- "indicatorEntries"
2189
- );
2190
- return indicatorEntries ? { indicatorEntries } : null;
2861
+ var buildTargetVsReference = (signal, asset) => {
2862
+ const base = toRecord(signal.additionalIndicators?.baseContext);
2863
+ const relative = toRecord(base?.relative);
2864
+ const target = toRecord(relative?.[`targetVs${asset}`]);
2865
+ return {
2866
+ source: target ? String(target.source ?? "") : null,
2867
+ available: Boolean(target),
2868
+ ratioReturn1h: toFiniteNumber(target?.ratioReturn1h),
2869
+ ratioReturn4h: toFiniteNumber(target?.ratioReturn4h),
2870
+ ratioReturn24h: toFiniteNumber(target?.ratioReturn24h),
2871
+ alpha1h: toFiniteNumber(target?.[`alphaVs${asset}1h`]),
2872
+ alpha4h: toFiniteNumber(target?.[`alphaVs${asset}4h`]),
2873
+ alpha24h: toFiniteNumber(target?.[`alphaVs${asset}24h`]),
2874
+ beta: toFiniteNumber(target?.[`betaTo${asset}20`]),
2875
+ correlation: toFiniteNumber(target?.[`correlationTo${asset}20`]),
2876
+ ratioTrend: typeof target?.ratioTrend === "string" ? target.ratioTrend : null
2877
+ };
2191
2878
  };
2192
- var validateStrategyEntries = (moduleName, entries, state) => {
2193
- const issues = [];
2194
- const names = /* @__PURE__ */ new Set();
2195
- entries.forEach((entry, index) => {
2196
- const entryPath = `${moduleName}.strategyEntries[${index}]`;
2197
- const strategyName = entry?.manifest?.name;
2198
- if (typeof strategyName !== "string" || !strategyName.trim()) {
2199
- issues.push(`${entryPath}: manifest.name is required`);
2200
- } else if (names.has(strategyName) || state?.strategyEntriesMap.has(strategyName)) {
2201
- issues.push(`${entryPath}: duplicate strategy ${strategyName}`);
2202
- } else {
2203
- names.add(strategyName);
2204
- }
2205
- if (!entry || typeof entry !== "object" || !entry.defaults || typeof entry.defaults !== "object" || Array.isArray(entry.defaults)) {
2206
- issues.push(`${entryPath}: defaults must be an object`);
2207
- }
2208
- if (typeof entry?.parseConfig !== "function") {
2209
- issues.push(`${entryPath}: parseConfig is required`);
2210
- }
2211
- if (typeof entry?.createCore !== "function") {
2212
- issues.push(`${entryPath}: createCore is required`);
2213
- }
2214
- });
2215
- return issues;
2879
+ var buildTargetVsBtcContextFromSignal = (signal) => {
2880
+ const { alpha1h, alpha4h, alpha24h, beta, correlation, ...shared } = buildTargetVsReference(signal, "Btc");
2881
+ return {
2882
+ ...shared,
2883
+ alphaVsBtc1h: alpha1h,
2884
+ alphaVsBtc4h: alpha4h,
2885
+ alphaVsBtc24h: alpha24h,
2886
+ betaToBtc20: beta,
2887
+ correlationToBtc20: correlation
2888
+ };
2216
2889
  };
2217
- var registerEntries = (entries, source, state) => {
2218
- for (const entry of entries) {
2219
- const strategyName = entry.manifest.name;
2220
- state.strategyManifestsMap.set(strategyName, entry.manifest);
2221
- state.strategyEntriesMap.set(strategyName, entry);
2222
- state.strategySourcesMap.set(strategyName, source);
2223
- materializeStrategyCreator(strategyName, state);
2890
+ var buildTargetVsEthContextFromSignal = (signal) => {
2891
+ const { alpha1h, alpha4h, alpha24h, beta, correlation, ...shared } = buildTargetVsReference(signal, "Eth");
2892
+ return {
2893
+ ...shared,
2894
+ alphaVsEth1h: alpha1h,
2895
+ alphaVsEth4h: alpha4h,
2896
+ alphaVsEth24h: alpha24h,
2897
+ betaToEth20: beta,
2898
+ correlationToEth20: correlation
2899
+ };
2900
+ };
2901
+ var buildBtcAltRegimeContextFromSignal = (signal) => {
2902
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2903
+ const relative = toRecord(baseContext?.relative);
2904
+ const btcAltRegime = toRecord(relative?.btcAltRegime);
2905
+ if (!btcAltRegime) {
2906
+ return {
2907
+ source: null,
2908
+ available: false,
2909
+ universe: null,
2910
+ interval: null,
2911
+ stale: null,
2912
+ regime: null,
2913
+ btcReturn24h: null,
2914
+ altBasketReturn24h: null,
2915
+ btcVsAltReturn24h: null,
2916
+ btcTurnoverShare24h: null,
2917
+ btcTurnoverShareChange24h: null,
2918
+ altVolToBtcVol24h: null,
2919
+ altDispersion24h: null
2920
+ };
2224
2921
  }
2922
+ return {
2923
+ source: String(btcAltRegime.source ?? ""),
2924
+ available: true,
2925
+ universe: String(btcAltRegime.universe ?? ""),
2926
+ interval: String(btcAltRegime.interval ?? ""),
2927
+ stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
2928
+ regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
2929
+ btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
2930
+ altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
2931
+ btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
2932
+ btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
2933
+ btcTurnoverShareChange24h: toFiniteNumber(
2934
+ btcAltRegime.btcTurnoverShareChange24h
2935
+ ),
2936
+ altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
2937
+ altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
2938
+ };
2225
2939
  };
2226
- var materializeStrategyCreator = (strategyName, state) => {
2227
- if (state.strategyCreators.has(strategyName) || !sharedStrategyRegistry.strategyRuntimeFactory) {
2228
- return;
2940
+ var buildCmcGlobalContextFromSignal = (signal) => {
2941
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2942
+ const relative = toRecord(baseContext?.relative);
2943
+ const cmcGlobal = toRecord(relative?.cmcGlobal);
2944
+ if (!cmcGlobal) {
2945
+ return {
2946
+ source: null,
2947
+ available: false,
2948
+ interval: null,
2949
+ asOfTs: null,
2950
+ stale: null,
2951
+ totalMarketCapUsd: null,
2952
+ totalVolumeUsd: null,
2953
+ totalVolumeReportedUsd: null,
2954
+ altMarketCapUsd: null,
2955
+ altVolumeUsd: null,
2956
+ altVolumeReportedUsd: null,
2957
+ btcDominancePct: null,
2958
+ ethDominancePct: null,
2959
+ btcDominanceChange24hPct: null,
2960
+ ethDominanceChange24hPct: null,
2961
+ altMarketCapChange24hPct: null,
2962
+ altVolumeChange24hPct: null,
2963
+ activeCryptocurrencies: null,
2964
+ activeExchanges: null,
2965
+ activeMarketPairs: null,
2966
+ altLiquidityRegime: null
2967
+ };
2229
2968
  }
2230
- const entry = state.strategyEntriesMap.get(strategyName);
2231
- if (!entry) return;
2232
- state.strategyCreators.set(
2233
- strategyName,
2234
- sharedStrategyRegistry.strategyRuntimeFactory({
2235
- strategyName,
2236
- defaults: entry.defaults,
2237
- createCore: entry.createCore,
2238
- manifest: entry.manifest,
2239
- detectorKey: entry.detectorKey,
2240
- detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
2241
- resolveRegisteredManifest: (name) => state.strategyManifestsMap.get(name)
2242
- })
2243
- );
2969
+ return {
2970
+ source: String(cmcGlobal.source ?? ""),
2971
+ available: true,
2972
+ interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
2973
+ asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
2974
+ stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
2975
+ totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
2976
+ totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
2977
+ totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
2978
+ altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
2979
+ altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
2980
+ altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
2981
+ btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
2982
+ ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
2983
+ btcDominanceChange24hPct: toFiniteNumber(
2984
+ cmcGlobal.btcDominanceChange24hPct
2985
+ ),
2986
+ ethDominanceChange24hPct: toFiniteNumber(
2987
+ cmcGlobal.ethDominanceChange24hPct
2988
+ ),
2989
+ altMarketCapChange24hPct: toFiniteNumber(
2990
+ cmcGlobal.altMarketCapChange24hPct
2991
+ ),
2992
+ altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
2993
+ activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
2994
+ activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
2995
+ activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
2996
+ altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
2997
+ };
2244
2998
  };
2245
- var setStrategyRuntimeFactory = (factory) => {
2246
- sharedStrategyRegistry.strategyRuntimeFactory = factory;
2247
- for (const state of registryStateByProjectRoot.values()) {
2248
- for (const strategyName of state.strategyEntriesMap.keys()) {
2249
- materializeStrategyCreator(strategyName, state);
2250
- }
2999
+ var buildCmcReferenceAssetsContextFromSignal = (signal) => {
3000
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3001
+ const relative = toRecord(baseContext?.relative);
3002
+ const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
3003
+ if (!cmcReferenceAssets) {
3004
+ return {
3005
+ source: null,
3006
+ available: false,
3007
+ interval: null,
3008
+ asOfTs: null,
3009
+ stale: null,
3010
+ btcMarketCapUsd: null,
3011
+ ethMarketCapUsd: null,
3012
+ btcVolumeUsd: null,
3013
+ ethVolumeUsd: null,
3014
+ btcVolumeToMarketCap: null,
3015
+ ethVolumeToMarketCap: null,
3016
+ ethBtcMarketCapRatio: null,
3017
+ ethBtcMarketCapRatioChange24hPct: null,
3018
+ ethVsBtcVolumeRatio: null,
3019
+ referenceLiquidityRegime: null
3020
+ };
2251
3021
  }
3022
+ return {
3023
+ source: String(cmcReferenceAssets.source ?? ""),
3024
+ available: true,
3025
+ interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
3026
+ asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
3027
+ stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
3028
+ btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
3029
+ ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
3030
+ btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
3031
+ ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
3032
+ btcVolumeToMarketCap: toFiniteNumber(
3033
+ cmcReferenceAssets.btcVolumeToMarketCap
3034
+ ),
3035
+ ethVolumeToMarketCap: toFiniteNumber(
3036
+ cmcReferenceAssets.ethVolumeToMarketCap
3037
+ ),
3038
+ ethBtcMarketCapRatio: toFiniteNumber(
3039
+ cmcReferenceAssets.ethBtcMarketCapRatio
3040
+ ),
3041
+ ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
3042
+ cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
3043
+ ),
3044
+ ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
3045
+ referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
3046
+ };
2252
3047
  };
2253
- var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
2254
- if (typeof importTradejsModule === "function") {
2255
- return importTradejsModule(moduleName, cwd);
3048
+ var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
3049
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3050
+ const relative = toRecord(baseContext?.relative);
3051
+ const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
3052
+ if (!cmcExchangeLiquidity) {
3053
+ return {
3054
+ source: null,
3055
+ available: false,
3056
+ interval: null,
3057
+ asOfTs: null,
3058
+ stale: null,
3059
+ exchangesCount: null,
3060
+ totalVolumeUsd: null,
3061
+ totalVolumeChange24hPct: null,
3062
+ binanceVolumeUsd: null,
3063
+ binanceVolumeShare: null,
3064
+ topExchangeVolumeShare: null,
3065
+ liquidityRegime: null
3066
+ };
2256
3067
  }
2257
- return import(
2258
- /* webpackIgnore: true */
2259
- moduleName
2260
- );
3068
+ return {
3069
+ source: String(cmcExchangeLiquidity.source ?? ""),
3070
+ available: true,
3071
+ interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
3072
+ asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
3073
+ stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
3074
+ exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
3075
+ totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
3076
+ totalVolumeChange24hPct: toFiniteNumber(
3077
+ cmcExchangeLiquidity.totalVolumeChange24hPct
3078
+ ),
3079
+ binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
3080
+ binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
3081
+ topExchangeVolumeShare: toFiniteNumber(
3082
+ cmcExchangeLiquidity.topExchangeVolumeShare
3083
+ ),
3084
+ liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
3085
+ };
2261
3086
  };
2262
- var resolveStrategyModuleEntry = async ({
2263
- strategyName,
2264
- moduleName,
2265
- cwd
2266
- }) => {
2267
- const { projectRoot, state } = getStrategyRegistryState(cwd);
2268
- const source = moduleName.trim();
2269
- const resolvedModuleName = resolvePluginModuleSpecifier(source, projectRoot);
2270
- const cacheKey = `${resolvedModuleName}\0${strategyName}`;
2271
- const cached = state.strategyModuleResolutions.get(cacheKey);
2272
- if (cached) return cached;
2273
- const resolution = (async () => {
2274
- const moduleExport = await importStrategyPluginModule(
2275
- resolvedModuleName,
2276
- projectRoot
2277
- );
2278
- const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
2279
- if (!pluginDefinition) {
2280
- throw new Error(`${source}: export { strategyEntries } is missing`);
2281
- }
2282
- const issues = validateStrategyEntries(
2283
- source,
2284
- pluginDefinition.strategyEntries
2285
- );
2286
- if (issues.length > 0) {
2287
- throw new Error(
2288
- ["Invalid TradeJS strategy module:", ...issues].join("\n")
2289
- );
2290
- }
2291
- const entry = pluginDefinition.strategyEntries.find(
2292
- (candidate) => candidate.manifest.name === strategyName
2293
- );
2294
- if (!entry) {
2295
- throw new Error(`${source}: strategy ${strategyName} is missing`);
2296
- }
2297
- const factory = sharedStrategyRegistry.strategyRuntimeFactory;
2298
- if (!factory) {
2299
- throw new Error("Strategy runtime factory is not configured");
2300
- }
2301
- const manifestsByName = new Map(
2302
- pluginDefinition.strategyEntries.map(
2303
- (candidate) => [candidate.manifest.name, candidate.manifest]
2304
- )
2305
- );
3087
+ var buildCmcFearGreedContextFromSignal = (signal) => {
3088
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3089
+ const relative = toRecord(baseContext?.relative);
3090
+ const cmcFearGreed = toRecord(relative?.cmcFearGreed);
3091
+ if (!cmcFearGreed) {
2306
3092
  return {
2307
- entry,
2308
- creator: factory({
2309
- strategyName,
2310
- defaults: entry.defaults,
2311
- createCore: entry.createCore,
2312
- manifest: entry.manifest,
2313
- detectorKey: entry.detectorKey,
2314
- detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
2315
- resolveRegisteredManifest: (name) => manifestsByName.get(name) ?? state.strategyManifestsMap.get(name)
2316
- }),
2317
- source
3093
+ source: null,
3094
+ available: false,
3095
+ interval: null,
3096
+ asOfTs: null,
3097
+ stale: null,
3098
+ value: null,
3099
+ valueChange24h: null,
3100
+ valueChange7d: null,
3101
+ classification: null,
3102
+ sentimentRegime: null
2318
3103
  };
2319
- })();
2320
- state.strategyModuleResolutions.set(cacheKey, resolution);
2321
- try {
2322
- return await resolution;
2323
- } catch (error) {
2324
- state.strategyModuleResolutions.delete(cacheKey);
2325
- throw error;
2326
- }
2327
- };
2328
- var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
2329
- const { projectRoot, state } = getStrategyRegistryState(cwd);
2330
- if (!state.pluginsLoadPromise) {
2331
- (0, import_indicators.resetIndicatorRegistryCache)(projectRoot);
2332
- state.pluginsLoadPromise = (async () => {
2333
- const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
2334
- const strategySet = new Set(strategyModules);
2335
- const indicatorSet = new Set(indicatorModules);
2336
- const pluginModuleNames = [
2337
- .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
2338
- ];
2339
- if (!pluginModuleNames.length) {
2340
- return;
2341
- }
2342
- const issues = [];
2343
- for (const moduleName of pluginModuleNames) {
2344
- try {
2345
- const resolvedModuleName = resolvePluginModuleSpecifier(
2346
- moduleName,
2347
- projectRoot
2348
- );
2349
- const moduleExport = await importStrategyPluginModule(
2350
- resolvedModuleName,
2351
- projectRoot
2352
- );
2353
- if (strategySet.has(moduleName)) {
2354
- const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
2355
- if (!pluginDefinition) {
2356
- issues.push(
2357
- `${moduleName}: export { strategyEntries } is missing`
2358
- );
2359
- } else {
2360
- const entryIssues = validateStrategyEntries(
2361
- moduleName,
2362
- pluginDefinition.strategyEntries,
2363
- state
2364
- );
2365
- issues.push(...entryIssues);
2366
- if (entryIssues.length === 0) {
2367
- registerEntries(
2368
- pluginDefinition.strategyEntries,
2369
- moduleName,
2370
- state
2371
- );
2372
- }
2373
- }
2374
- }
2375
- if (indicatorSet.has(moduleName)) {
2376
- const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
2377
- if (!indicatorPluginDefinition) {
2378
- issues.push(
2379
- `${moduleName}: export { indicatorEntries } is missing`
2380
- );
2381
- } else {
2382
- (0, import_indicators.registerIndicatorEntries)(
2383
- indicatorPluginDefinition.indicatorEntries,
2384
- moduleName,
2385
- projectRoot
2386
- );
2387
- }
2388
- }
2389
- if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
2390
- issues.push(`${moduleName}: plugin is not declared in config`);
2391
- }
2392
- } catch (error) {
2393
- issues.push(`${moduleName}: failed to import: ${String(error)}`);
2394
- }
2395
- }
2396
- if (issues.length > 0) {
2397
- throw new Error(
2398
- ["Invalid TradeJS plugin catalog:", ...issues].join("\n")
2399
- );
2400
- }
2401
- })();
2402
3104
  }
2403
- await state.pluginsLoadPromise;
3105
+ return {
3106
+ source: String(cmcFearGreed.source ?? ""),
3107
+ available: true,
3108
+ interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
3109
+ asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
3110
+ stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
3111
+ value: toFiniteNumber(cmcFearGreed.value),
3112
+ valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
3113
+ valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
3114
+ classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
3115
+ sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
3116
+ };
2404
3117
  };
2405
- var getStrategyCreator = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2406
- if (moduleName?.trim()) {
2407
- await ensureStrategyPluginsLoaded(cwd);
2408
- return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).creator;
3118
+ var buildCmcIndexesContextFromSignal = (signal) => {
3119
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3120
+ const relative = toRecord(baseContext?.relative);
3121
+ const cmcIndexes = toRecord(relative?.cmcIndexes);
3122
+ if (!cmcIndexes) {
3123
+ return {
3124
+ source: null,
3125
+ available: false,
3126
+ interval: null,
3127
+ asOfTs: null,
3128
+ stale: null,
3129
+ cmc100Value: null,
3130
+ cmc100Change24hPct: null,
3131
+ cmc100TopConstituentSymbol: null,
3132
+ cmc100TopConstituentWeightPct: null,
3133
+ cmc20Value: null,
3134
+ cmc20Change24hPct: null,
3135
+ cmc20TopConstituentSymbol: null,
3136
+ cmc20TopConstituentWeightPct: null,
3137
+ cmc20ToCmc100Ratio: null,
3138
+ cmc20ToCmc100RatioChange24hPct: null,
3139
+ indexRegime: null
3140
+ };
2409
3141
  }
2410
- await ensureStrategyPluginsLoaded(cwd);
2411
- const { state } = getStrategyRegistryState(cwd);
2412
- return state.strategyCreators.get(name);
3142
+ return {
3143
+ source: String(cmcIndexes.source ?? ""),
3144
+ available: true,
3145
+ interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
3146
+ asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
3147
+ stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
3148
+ cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
3149
+ cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
3150
+ cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
3151
+ cmc100TopConstituentWeightPct: toFiniteNumber(
3152
+ cmcIndexes.cmc100TopConstituentWeightPct
3153
+ ),
3154
+ cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
3155
+ cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
3156
+ cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
3157
+ cmc20TopConstituentWeightPct: toFiniteNumber(
3158
+ cmcIndexes.cmc20TopConstituentWeightPct
3159
+ ),
3160
+ cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
3161
+ cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
3162
+ cmcIndexes.cmc20ToCmc100RatioChange24hPct
3163
+ ),
3164
+ indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
3165
+ };
2413
3166
  };
2414
- var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
2415
- if (!name) {
2416
- return void 0;
3167
+ var buildReferenceTradeFlowContextFromSignal = (signal) => {
3168
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3169
+ const relative = toRecord(baseContext?.relative);
3170
+ const refs = toRecord(relative?.referenceTradeFlow);
3171
+ const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
3172
+ const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
3173
+ const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
3174
+ if (!refs) {
3175
+ return {
3176
+ source: null,
3177
+ available: false,
3178
+ primaryReferenceSymbol: null,
3179
+ referenceSymbols: [],
3180
+ primaryTradeFlowBuyPressurePct: null,
3181
+ primaryTradeFlowStale: null
3182
+ };
2417
3183
  }
2418
- const { state } = getStrategyRegistryState(cwd);
2419
- return state.strategyManifestsMap.get(name);
3184
+ return {
3185
+ source: String(refs.source ?? ""),
3186
+ available: true,
3187
+ primaryReferenceSymbol,
3188
+ referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
3189
+ primaryTradeFlowBuyPressurePct: toFiniteNumber(
3190
+ primaryTradeFlow?.buyPressurePct
3191
+ ),
3192
+ primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
3193
+ };
2420
3194
  };
2421
- var strategies = new Proxy(
2422
- {},
2423
- {
2424
- get: (_target, property) => {
2425
- if (typeof property !== "string") {
2426
- return void 0;
2427
- }
2428
- return getStrategyRegistryState().state.strategyCreators.get(property);
2429
- },
2430
- ownKeys: () => {
2431
- return [...getStrategyRegistryState().state.strategyCreators.keys()];
2432
- },
2433
- getOwnPropertyDescriptor: () => ({
2434
- enumerable: true,
2435
- configurable: true
2436
- })
3195
+ var buildAiMarketContext = (signal) => ({
3196
+ execution: {
3197
+ binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
3198
+ },
3199
+ participation: {
3200
+ trueDelta: buildTrueDeltaContextFromSignal(signal),
3201
+ tradeFlow: buildTradeFlowContextFromSignal(signal)
3202
+ },
3203
+ relative: {
3204
+ marketBreadth: buildMarketBreadthContextFromSignal(signal),
3205
+ marketBreadths: buildMarketBreadthsContextFromSignal(signal),
3206
+ targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
3207
+ targetVsEth: buildTargetVsEthContextFromSignal(signal),
3208
+ btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
3209
+ cmcGlobal: buildCmcGlobalContextFromSignal(signal),
3210
+ cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
3211
+ cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
3212
+ cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
3213
+ cmcIndexes: buildCmcIndexesContextFromSignal(signal),
3214
+ referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
2437
3215
  }
2438
- );
3216
+ });
2439
3217
 
2440
3218
  // src/strategy/policyProfiles.ts
2441
3219
  var profileMatches = (profile, universe, assetClass) => {
@@ -2537,12 +3315,61 @@ var buildBaseAiPayload = (signal) => {
2537
3315
  };
2538
3316
  };
2539
3317
  var defaultAiAdapter = {};
3318
+ var tickerProfileFeatureIds = new Set(
3319
+ import_aiFeatures.TICKER_PROFILE_AI_FEATURES.map(({ id }) => id)
3320
+ );
2540
3321
  var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
2541
3322
  var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
2542
3323
  var buildAiPayloadByStrategy = (signal) => {
2543
3324
  const basePayload = buildBaseAiPayload(signal);
2544
3325
  const adapter = getSignalAiAdapter(signal);
2545
- return adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
3326
+ const payload = adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
3327
+ const custom = getStrategyManifest(signal.strategy)?.aiFeatures ?? [];
3328
+ const frozen = signal.aiFeatureReplay?.snapshot;
3329
+ if (frozen) (0, import_aiFeatures.validateAiFeatureSnapshot)(frozen);
3330
+ const tickerProfile = signal.additionalIndicators?.tickerProfile ?? signal.additionalIndicators?.baseContext?.tickerProfile;
3331
+ const hasProfile = Boolean(
3332
+ tickerProfile || frozen?.definitions.some(({ id }) => tickerProfileFeatureIds.has(id))
3333
+ );
3334
+ const indicatorFeatures = (0, import_indicators2.getRegisteredIndicatorEntries)(
3335
+ getTradejsProjectCwd()
3336
+ ).flatMap((entry) => entry.indicator.enabled ? entry.aiFeatures ?? [] : []);
3337
+ const hasPortfolio = Boolean(
3338
+ signal.additionalIndicators?.portfolioContext || frozen?.definitions.some(
3339
+ ({ id }) => import_aiFeatures.PORTFOLIO_AI_FEATURES.some((d) => d.id === id)
3340
+ )
3341
+ );
3342
+ const definitions = [
3343
+ ...hasPortfolio ? import_aiFeatures.PORTFOLIO_AI_FEATURES : [],
3344
+ ...hasProfile ? import_aiFeatures.TICKER_PROFILE_AI_FEATURES : [],
3345
+ ...indicatorFeatures,
3346
+ ...custom
3347
+ ];
3348
+ if (!definitions.length && !signal.aiFeatureReplay?.snapshot) return payload;
3349
+ (0, import_aiFeatures.validateAiFeatureDefinitions)(definitions);
3350
+ if (signal.aiFeatureReplay && !frozen) {
3351
+ throw new Error(
3352
+ "Dataset has no frozen AI features; regenerate the decision-time export"
3353
+ );
3354
+ }
3355
+ if (frozen && JSON.stringify(frozen.definitions) !== JSON.stringify(definitions)) {
3356
+ throw new Error(
3357
+ "AI feature definition changed; regenerate the decision-time export"
3358
+ );
3359
+ }
3360
+ return {
3361
+ ...payload,
3362
+ featureSnapshot: frozen ?? (0, import_aiFeatures.projectAiFeatures)(definitions, {
3363
+ signal: tickerProfile ? {
3364
+ ...signal,
3365
+ additionalIndicators: {
3366
+ ...signal.additionalIndicators,
3367
+ tickerProfile
3368
+ }
3369
+ } : signal,
3370
+ payload
3371
+ })
3372
+ };
2546
3373
  };
2547
3374
  var buildAiSystemPromptAddonByStrategy = (signal) => getSignalAiAdapter(signal).buildSystemPromptAddon?.({ signal }) ?? "";
2548
3375
  var buildAiHumanPromptAddonByStrategy = (signal, payload) => getSignalAiAdapter(signal).buildHumanPromptAddon?.({
@@ -2931,377 +3758,129 @@ var getDeterministicAiGateContext = (payload) => {
2931
3758
  additionalIndicators,
2932
3759
  ...Object.values(additionalIndicators ?? {}).map(asRecord)
2933
3760
  ].filter((value) => Boolean(value));
2934
- return candidates.find(
2935
- (candidate) => Array.isArray(candidate.approvalBlockReasons) || Array.isArray(candidate.riskAnnotations) || Array.isArray(candidate.structuralHardBlockReasons) || typeof candidate.approvalAllowedNow === "boolean"
2936
- ) ?? null;
2937
- };
2938
- var buildAiHumanPrompt = (signal, payload = buildAiPayload(signal)) => `
2939
- Analyze the already computed internal signal for ${signal.symbol}. The original signal direction is ${signal.direction}.
2940
- 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.
2941
-
2942
- Trade payload:
2943
- ${JSON.stringify(payload)}
2944
- ${buildAiHumanPromptAddonByStrategy(signal, payload)}
2945
- `;
2946
- var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
2947
- const settings = await getAiUserSettings(options.userName);
2948
- const responseLanguage = (0, import_aiLanguages2.getAiResponseLanguagePromptName)(
2949
- settings.AI_RESPONSE_LANGUAGE || import_aiLanguages2.DEFAULT_AI_RESPONSE_LANGUAGE
2950
- );
2951
- const response = await invokeAiPromptChat({
2952
- userName: options.userName,
2953
- model: options.model,
2954
- messages: [
2955
- { role: "system", content: systemPrompt },
2956
- {
2957
- role: "system",
2958
- content: `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
2959
- },
2960
- { role: "user", content: humanPrompt }
2961
- ]
2962
- });
2963
- const parsed = parseAIResponse(response.content);
2964
- const normalized = normalizeAnalysis(parsed);
2965
- if (!options.signal) {
2966
- return normalized;
2967
- }
2968
- return postProcessAiAnalysisByStrategy(
2969
- options.signal,
2970
- normalized,
2971
- options.payload
2972
- );
2973
- };
2974
- var runAiPromptLocal = async (signal, options = {}) => {
2975
- const payload = options.payload ?? buildAiPayload(signal);
2976
- const gateContext = getDeterministicAiGateContext(payload);
2977
- const signalDirection = getSignalDirection(signal);
2978
- const deterministicQuality = getDeterministicQuality(gateContext);
2979
- const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
2980
- return postProcessLocalAiAnalysisByStrategy(
2981
- signal,
2982
- {
2983
- direction: approvalAllowedNow ? signalDirection : null,
2984
- quality: deterministicQuality,
2985
- needRetest: !approvalAllowedNow,
2986
- retestPrice: null,
2987
- takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
2988
- stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
2989
- },
2990
- payload
2991
- );
2992
- };
2993
- var askAI = async (signal, options = {}) => {
2994
- const { symbol } = signal;
2995
- const payload = buildAiPayload(signal);
2996
- const content = await runAiPrompt(
2997
- {
2998
- systemPrompt: buildAiSystemPrompt(signal),
2999
- humanPrompt: buildAiHumanPrompt(signal, payload)
3000
- },
3001
- {
3002
- ...options,
3003
- signal,
3004
- payload
3005
- }
3006
- );
3007
- await (0, import_redis.setData)(import_redis.redisKeys.analysis(symbol, signal.signalId), content);
3008
- return content;
3009
- };
3010
-
3011
- // src/strategyAdapters/ml.ts
3012
- var defaultMlAdapter = {
3013
- normalizeStrategyConfig: (strategyConfig) => strategyConfig
3014
- };
3015
- var getStrategyMlAdapter = (strategy, profileId) => {
3016
- const strategyAdapter = getStrategyProfileMlAdapter(
3017
- getStrategyManifest(strategy),
3018
- profileId
3019
- );
3020
- if (!strategyAdapter) return defaultMlAdapter;
3021
- return {
3022
- ...defaultMlAdapter,
3023
- ...strategyAdapter
3024
- };
3025
- };
3026
-
3027
- // src/mlPayload.ts
3028
- var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
3029
- return getStrategyMlAdapter(
3030
- strategyName,
3031
- profileId
3032
- ).normalizeStrategyConfig?.(strategyConfig);
3033
- };
3034
- var buildMlPayload = (payload) => {
3035
- const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
3036
- const profileId = payload.signal?.policyProfileId;
3037
- const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
3038
- const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
3039
- const nextSignal = {
3040
- ...normalizedSignal,
3041
- indicators: {
3042
- ...normalizedSignal?.indicators ?? {}
3043
- }
3044
- };
3045
- const nextContext = payload.context ? {
3046
- ...payload.context,
3047
- strategyConfig: normalizeStrategyConfig(
3048
- payload.context.strategyConfig,
3049
- strategyName,
3050
- profileId
3051
- )
3052
- } : void 0;
3053
- return {
3054
- signal: nextSignal,
3055
- context: nextContext
3056
- };
3057
- };
3058
-
3059
- // src/runtimeJournal.ts
3060
- var import_node_crypto = require("crypto");
3061
- var import_constants = require("@tradejs/core/constants");
3062
- var import_time = require("@tradejs/core/time");
3063
- var import_trade = require("@tradejs/core/trade");
3064
- var import_logger3 = require("@tradejs/infra/logger");
3065
- var import_redis2 = require("@tradejs/infra/redis");
3066
- var now = () => Date.now();
3067
- var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
3068
- var calculateClosedPnl = ({
3069
- direction,
3070
- entryPrice,
3071
- exitPrice,
3072
- qty
3073
- }) => {
3074
- const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
3075
- return Number.isFinite(pnl) ? pnl : null;
3076
- };
3077
- var createRuntimeOrderId = (strategy) => {
3078
- const prefix = (0, import_trade.createRuntimeOrderLinkPrefix)(strategy);
3079
- if (prefix === "tjs-") {
3080
- return `tjs-${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
3081
- }
3082
- return `${prefix}${toRandomOrderSuffix()}`;
3083
- };
3084
- var recordRuntimeTradeOpen = async (params) => {
3085
- const { userName } = params;
3086
- if (!userName) {
3087
- return null;
3088
- }
3089
- const record = {
3090
- ...params,
3091
- entryCount: 1,
3092
- lastEntryPrice: params.entryPrice,
3093
- lastEntryQty: params.qty,
3094
- lastEntryTimestamp: params.entryTimestamp,
3095
- status: "active",
3096
- currentPrice: params.entryPrice,
3097
- currentPnl: 0,
3098
- closedPnl: null,
3099
- exitPrice: null,
3100
- exitTimestamp: null,
3101
- lastSyncedAt: now()
3102
- };
3103
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(record.entryTimestamp);
3104
- const runtimeScopeId = record.deploymentId ?? record.accountId;
3105
- try {
3106
- await Promise.all([
3107
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, record.orderId), record, {
3108
- expire: 0
3109
- }),
3110
- (0, import_redis2.setHashJsonField)(
3111
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3112
- record.orderId,
3113
- record,
3114
- { expire: 0 }
3115
- ),
3116
- (0, import_redis2.setData)(
3117
- import_redis2.redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
3118
- { orderId: record.orderId },
3119
- { expire: 0 }
3120
- )
3121
- ]);
3122
- } catch (error) {
3123
- import_logger3.logger.error(
3124
- "runtime trade open journal failed: %s %s",
3125
- record.symbol,
3126
- error?.message || String(error)
3127
- );
3128
- }
3129
- return record;
3130
- };
3131
- var recordRuntimeTradeIncrease = async (params) => {
3132
- const {
3133
- userName,
3134
- strategy,
3135
- symbol,
3136
- direction,
3137
- resultingQty,
3138
- resultingEntryPrice,
3139
- addedQty,
3140
- addedEntryPrice,
3141
- entryTimestamp,
3142
- fee,
3143
- accountId,
3144
- deploymentId
3145
- } = params;
3146
- if (!userName) {
3147
- return null;
3148
- }
3149
- const existing = await getActiveRuntimeTrade({
3150
- userName,
3151
- symbol,
3152
- accountId,
3153
- deploymentId
3154
- });
3155
- if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
3156
- return null;
3157
- }
3158
- const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
3159
- const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
3160
- const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
3161
- const next = {
3162
- ...existing,
3163
- ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
3164
- qty: resultingQty,
3165
- entryPrice: resultingEntryPrice,
3166
- entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
3167
- lastEntryPrice: addedEntryPrice,
3168
- lastEntryQty: addedQty,
3169
- lastEntryTimestamp: entryTimestamp,
3170
- currentPrice: resultingEntryPrice,
3171
- currentPnl: 0,
3172
- fee: openFee,
3173
- openFee,
3174
- totalFee,
3175
- lastSyncedAt: now()
3176
- };
3177
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3178
- try {
3179
- await Promise.all([
3180
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, existing.orderId), next, {
3181
- expire: 0
3182
- }),
3183
- (0, import_redis2.setHashJsonField)(
3184
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3185
- existing.orderId,
3186
- next,
3187
- { expire: 0 }
3188
- )
3189
- ]);
3190
- } catch (error) {
3191
- import_logger3.logger.error(
3192
- "runtime trade increase journal failed: %s %s",
3193
- symbol,
3194
- error?.message || String(error)
3195
- );
3196
- }
3197
- return next;
3761
+ return candidates.find(
3762
+ (candidate) => Array.isArray(candidate.approvalBlockReasons) || Array.isArray(candidate.riskAnnotations) || Array.isArray(candidate.structuralHardBlockReasons) || typeof candidate.approvalAllowedNow === "boolean"
3763
+ ) ?? null;
3198
3764
  };
3199
- var getActiveRuntimeTrade = async (params) => {
3200
- const { userName, symbol, accountId, deploymentId } = params;
3201
- if (!userName) {
3202
- return null;
3203
- }
3204
- const activeRef = await (0, import_redis2.getData)(
3205
- import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
3206
- null
3765
+ var buildAiHumanPrompt = (signal, payload = buildAiPayload(signal)) => `
3766
+ Analyze the already computed internal signal for ${signal.symbol}. The original signal direction is ${signal.direction}.
3767
+ 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.
3768
+
3769
+ Trade payload:
3770
+ ${JSON.stringify(payload)}
3771
+ ${buildAiHumanPromptAddonByStrategy(signal, payload)}
3772
+ `;
3773
+ var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
3774
+ const settings = await getAiUserSettings(options.userName);
3775
+ const responseLanguage = (0, import_aiLanguages2.getAiResponseLanguagePromptName)(
3776
+ settings.AI_RESPONSE_LANGUAGE || import_aiLanguages2.DEFAULT_AI_RESPONSE_LANGUAGE
3207
3777
  );
3208
- const orderId = String(activeRef?.orderId || "").trim();
3209
- if (!orderId) {
3210
- return null;
3778
+ const response = await invokeAiPromptChat({
3779
+ userName: options.userName,
3780
+ model: options.model,
3781
+ messages: [
3782
+ { role: "system", content: systemPrompt },
3783
+ {
3784
+ role: "system",
3785
+ content: `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
3786
+ },
3787
+ { role: "user", content: humanPrompt }
3788
+ ]
3789
+ });
3790
+ const parsed = parseAIResponse(response.content);
3791
+ const normalized = normalizeAnalysis(parsed);
3792
+ if (!options.signal) {
3793
+ return normalized;
3211
3794
  }
3212
- const existing = await (0, import_redis2.getData)(
3213
- import_redis2.redisKeys.runtimeTrade(userName, orderId),
3214
- null
3795
+ return postProcessAiAnalysisByStrategy(
3796
+ options.signal,
3797
+ normalized,
3798
+ options.payload
3215
3799
  );
3216
- if (!existing || existing.status !== "active") {
3217
- await (0, import_redis2.delKey)(
3218
- import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
3219
- );
3220
- return null;
3221
- }
3222
- return existing;
3223
3800
  };
3224
- var markRuntimeTradeClosed = async (params) => {
3225
- const {
3226
- userName,
3227
- symbol,
3228
- strategy,
3229
- exitPrice,
3230
- exitTimestamp,
3231
- closedPnl,
3232
- exitType,
3233
- accountId,
3234
- deploymentId
3235
- } = params;
3236
- if (!userName) {
3237
- return null;
3238
- }
3239
- const existing = await getActiveRuntimeTrade({
3240
- userName,
3241
- symbol,
3242
- accountId,
3243
- deploymentId
3244
- });
3245
- if (!existing) {
3246
- return null;
3247
- }
3248
- const orderId = existing.orderId;
3249
- if (strategy && existing.strategy !== strategy) {
3250
- return null;
3251
- }
3252
- const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
3253
- const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
3254
- direction: existing.direction,
3255
- entryPrice: existing.entryPrice,
3256
- exitPrice: resolvedExitPrice,
3257
- qty: existing.qty
3258
- }) : existing.closedPnl ?? existing.currentPnl ?? null;
3259
- const next = {
3260
- ...existing,
3261
- status: "closed",
3262
- currentPrice: resolvedExitPrice,
3263
- currentPnl: resolvedClosedPnl,
3264
- closedPnl: resolvedClosedPnl,
3265
- exitPrice: resolvedExitPrice,
3266
- exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
3267
- exitType: exitType ?? existing.exitType ?? null,
3268
- lastSyncedAt: now()
3801
+ var runAiPromptLocal = async (signal, options = {}) => {
3802
+ const payload = options.payload ?? buildAiPayload(signal);
3803
+ const gateContext = getDeterministicAiGateContext(payload);
3804
+ const signalDirection = getSignalDirection(signal);
3805
+ const deterministicQuality = getDeterministicQuality(gateContext);
3806
+ const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
3807
+ return postProcessLocalAiAnalysisByStrategy(
3808
+ signal,
3809
+ {
3810
+ direction: approvalAllowedNow ? signalDirection : null,
3811
+ quality: deterministicQuality,
3812
+ needRetest: !approvalAllowedNow,
3813
+ retestPrice: null,
3814
+ takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
3815
+ stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
3816
+ },
3817
+ payload
3818
+ );
3819
+ };
3820
+ var askAI = async (signal, options = {}) => {
3821
+ const { symbol } = signal;
3822
+ const payload = buildAiPayload(signal);
3823
+ const content = await runAiPrompt(
3824
+ {
3825
+ systemPrompt: buildAiSystemPrompt(signal),
3826
+ humanPrompt: buildAiHumanPrompt(signal, payload)
3827
+ },
3828
+ {
3829
+ ...options,
3830
+ signal,
3831
+ payload
3832
+ }
3833
+ );
3834
+ await (0, import_redis3.setData)(import_redis3.redisKeys.analysis(symbol, signal.signalId), content);
3835
+ return content;
3836
+ };
3837
+
3838
+ // src/strategyAdapters/ml.ts
3839
+ var defaultMlAdapter = {
3840
+ normalizeStrategyConfig: (strategyConfig) => strategyConfig
3841
+ };
3842
+ var getStrategyMlAdapter = (strategy, profileId) => {
3843
+ const strategyAdapter = getStrategyProfileMlAdapter(
3844
+ getStrategyManifest(strategy),
3845
+ profileId
3846
+ );
3847
+ if (!strategyAdapter) return defaultMlAdapter;
3848
+ return {
3849
+ ...defaultMlAdapter,
3850
+ ...strategyAdapter
3851
+ };
3852
+ };
3853
+
3854
+ // src/mlPayload.ts
3855
+ var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
3856
+ return getStrategyMlAdapter(
3857
+ strategyName,
3858
+ profileId
3859
+ ).normalizeStrategyConfig?.(strategyConfig);
3860
+ };
3861
+ var buildMlPayload = (payload) => {
3862
+ const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
3863
+ const profileId = payload.signal?.policyProfileId;
3864
+ const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
3865
+ const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
3866
+ const nextSignal = {
3867
+ ...normalizedSignal,
3868
+ indicators: {
3869
+ ...normalizedSignal?.indicators ?? {}
3870
+ }
3871
+ };
3872
+ const nextContext = payload.context ? {
3873
+ ...payload.context,
3874
+ strategyConfig: normalizeStrategyConfig(
3875
+ payload.context.strategyConfig,
3876
+ strategyName,
3877
+ profileId
3878
+ )
3879
+ } : void 0;
3880
+ return {
3881
+ signal: nextSignal,
3882
+ context: nextContext
3269
3883
  };
3270
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3271
- const closeDayKey = (0, import_time.getRuntimeStorageDayKey)(next.exitTimestamp);
3272
- try {
3273
- await Promise.all([
3274
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, orderId), next, {
3275
- expire: import_constants.TTL_1M
3276
- }),
3277
- (0, import_redis2.setHashJsonField)(
3278
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3279
- orderId,
3280
- next,
3281
- { expire: import_constants.TTL_1M }
3282
- ),
3283
- (0, import_redis2.setHashJsonField)(
3284
- import_redis2.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
3285
- orderId,
3286
- next,
3287
- { expire: import_constants.TTL_1M }
3288
- ),
3289
- (0, import_redis2.delKey)(
3290
- import_redis2.redisKeys.runtimeActiveTrade(
3291
- userName,
3292
- symbol,
3293
- deploymentId ?? accountId
3294
- )
3295
- )
3296
- ]);
3297
- } catch (error) {
3298
- import_logger3.logger.error(
3299
- "runtime trade close journal failed: %s %s",
3300
- symbol,
3301
- error?.message || String(error)
3302
- );
3303
- }
3304
- return next;
3305
3884
  };
3306
3885
 
3307
3886
  // src/strategyHelpers/marketContextStages.ts
@@ -3313,7 +3892,7 @@ var import_logger4 = require("@tradejs/infra/logger");
3313
3892
  var import_strategies = require("@tradejs/core/strategies");
3314
3893
 
3315
3894
  // src/binanceBreadthUniverses.ts
3316
- var import_node_crypto2 = require("crypto");
3895
+ var import_node_crypto3 = require("crypto");
3317
3896
 
3318
3897
  // src/config/binanceBreadthUniverses.json
3319
3898
  var binanceBreadthUniverses_default = {
@@ -3552,7 +4131,7 @@ var BINANCE_BREADTH_UNIVERSE_KEYS = [
3552
4131
  "top50",
3553
4132
  "top100"
3554
4133
  ];
3555
- var fingerprint = (value, length) => (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
4134
+ var fingerprint = (value, length) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
3556
4135
  var normalizeSymbols = (symbols) => symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean);
3557
4136
  var buildBinanceBreadthUniverseSnapshot = ({
3558
4137
  rankedSymbols,
@@ -4576,7 +5155,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4576
5155
  };
4577
5156
 
4578
5157
  // src/strategyHelpers/derivativesContext.ts
4579
- var import_indicators2 = require("@tradejs/core/indicators");
5158
+ var import_indicators3 = require("@tradejs/core/indicators");
4580
5159
  var import_data2 = require("@tradejs/core/data");
4581
5160
  var import_strategies3 = require("@tradejs/core/strategies");
4582
5161
  var import_constants2 = require("@tradejs/core/constants");
@@ -4610,7 +5189,7 @@ var parseLookbackMs = () => {
4610
5189
  };
4611
5190
  var withHourlyFallbackRows = (rowsByInterval) => ({
4612
5191
  "15m": rowsByInterval["15m"] ?? [],
4613
- "1h": (0, import_indicators2.buildCoinalyzeHourlyRowsWithFallback)({
5192
+ "1h": (0, import_indicators3.buildCoinalyzeHourlyRowsWithFallback)({
4614
5193
  rows15m: rowsByInterval["15m"],
4615
5194
  fallbackRows1h: rowsByInterval["1h"]
4616
5195
  })
@@ -4738,7 +5317,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4738
5317
  const targetSymbol = normalizeSymbol(signal.symbol);
4739
5318
  const lookbackMs = parseLookbackMs();
4740
5319
  const decisionTimeMs = signal.timestamp + (0, import_data2.intervalToMs)(signal.interval);
4741
- const derivativesEndMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
5320
+ const derivativesEndMs = (0, import_indicators3.getLastClosedDerivativesBarStartMs)(
4742
5321
  decisionTimeMs,
4743
5322
  "15m"
4744
5323
  );
@@ -4753,7 +5332,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4753
5332
  });
4754
5333
  return [
4755
5334
  symbol,
4756
- (0, import_indicators2.buildDerivativesContext)({
5335
+ (0, import_indicators3.buildDerivativesContext)({
4757
5336
  symbol,
4758
5337
  direction: signal.direction,
4759
5338
  timestamp: derivativesEndMs,
@@ -4780,7 +5359,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4780
5359
  lookbackMs,
4781
5360
  ...params.abortSignal ? { signal: params.abortSignal } : {}
4782
5361
  });
4783
- const context = (0, import_indicators2.buildDerivativesContext)({
5362
+ const context = (0, import_indicators3.buildDerivativesContext)({
4784
5363
  symbol: targetSymbol,
4785
5364
  direction: signal.direction,
4786
5365
  timestamp: derivativesEndMs,
@@ -4828,7 +5407,7 @@ var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWha
4828
5407
  var import_logger7 = require("@tradejs/infra/logger");
4829
5408
 
4830
5409
  // src/hyperliquidWhaleUniverse.ts
4831
- var import_node_crypto3 = require("crypto");
5410
+ var import_node_crypto4 = require("crypto");
4832
5411
 
4833
5412
  // src/config/hyperliquidPerpUniverse.json
4834
5413
  var hyperliquidPerpUniverse_default = {
@@ -5000,7 +5579,7 @@ var hyperliquidWhales_default = {
5000
5579
  };
5001
5580
 
5002
5581
  // src/hyperliquidWhaleUniverse.ts
5003
- var fingerprint2 = (value) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
5582
+ var fingerprint2 = (value) => (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
5004
5583
  var validateUnique = (values, label) => {
5005
5584
  if (new Set(values).size !== values.length) {
5006
5585
  throw new Error(`Hyperliquid ${label} snapshot contains duplicates`);
@@ -5487,6 +6066,7 @@ var enrichSignalWithMarketContextStages = async ({
5487
6066
  })
5488
6067
  });
5489
6068
  }
6069
+ await enrichSignalWithTickerProfile(signal);
5490
6070
  const results = [];
5491
6071
  for (const stage of stages) {
5492
6072
  if (abortSignal?.aborted) break;
@@ -5534,12 +6114,12 @@ var findReplayAiAnalysis = ({
5534
6114
  direction,
5535
6115
  ai
5536
6116
  }) => {
5537
- const snapshots = ai?.replayAnalyses;
5538
- if (!Array.isArray(snapshots) || !snapshots.length) {
6117
+ const snapshots2 = ai?.replayAnalyses;
6118
+ if (!Array.isArray(snapshots2) || !snapshots2.length) {
5539
6119
  return void 0;
5540
6120
  }
5541
6121
  let best = null;
5542
- for (const snapshot2 of snapshots) {
6122
+ for (const snapshot2 of snapshots2) {
5543
6123
  if (snapshot2.symbol !== signal.symbol || snapshot2.direction !== direction || snapshot2.strategy && snapshot2.strategy !== signal.strategy) {
5544
6124
  continue;
5545
6125
  }
@@ -5699,11 +6279,13 @@ var executeEntryOrder = async ({
5699
6279
  beforePlaceOrder,
5700
6280
  recordRuntimeTrade = true,
5701
6281
  leverage,
5702
- executionLimits
6282
+ executionLimits,
6283
+ markPortfolioSubmitted,
6284
+ rejectPortfolioSubmission
5703
6285
  }) => {
5704
6286
  await beforePlaceOrder?.();
5705
6287
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
5706
- const orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
6288
+ let orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
5707
6289
  const signalTimestamp = signal.timestamp;
5708
6290
  const signalClosePrice = currentPrice;
5709
6291
  signal.orderId = orderId;
@@ -5772,9 +6354,16 @@ var executeEntryOrder = async ({
5772
6354
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
5773
6355
  orderId,
5774
6356
  signal,
5775
- ...beforeSubmit ? { beforeSubmit } : {}
6357
+ ...rejectPortfolioSubmission ? { onRejected: rejectPortfolioSubmission } : {},
6358
+ ...beforeSubmit || markPortfolioSubmitted ? {
6359
+ beforeSubmit: async (context) => {
6360
+ await beforeSubmit?.(context);
6361
+ return markPortfolioSubmitted?.(context.qty);
6362
+ }
6363
+ } : {}
5776
6364
  });
5777
6365
  const orderAckTime = Date.now();
6366
+ if (markPortfolioSubmitted && signal.orderId) orderId = signal.orderId;
5778
6367
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
5779
6368
  const currentPosition = await connector.getPosition(symbol);
5780
6369
  const fillTime = Date.now();
@@ -6763,7 +7352,7 @@ var handleProtectDecision = async ({
6763
7352
  }
6764
7353
  return decision.code;
6765
7354
  };
6766
- var executeEntryDecision = async ({
7355
+ var executeAdmittedEntryDecision = async ({
6767
7356
  connector,
6768
7357
  symbol,
6769
7358
  decision,
@@ -6777,7 +7366,9 @@ var executeEntryDecision = async ({
6777
7366
  ai,
6778
7367
  recordRuntimeJournal,
6779
7368
  invokeStageHooks,
6780
- notifyRuntimeError
7369
+ notifyRuntimeError,
7370
+ markPortfolioSubmitted,
7371
+ rejectPortfolioSubmission
6781
7372
  }) => {
6782
7373
  const signal = decision.signal;
6783
7374
  const beforePlaceOrder = async () => {
@@ -6824,6 +7415,8 @@ var executeEntryDecision = async ({
6824
7415
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
6825
7416
  signal,
6826
7417
  beforePlaceOrder,
7418
+ markPortfolioSubmitted,
7419
+ rejectPortfolioSubmission,
6827
7420
  recordRuntimeTrade: recordRuntimeJournal,
6828
7421
  executionLimits: resolveEntryExecutionLimits(
6829
7422
  hookCtx.strategyConfig,
@@ -6964,6 +7557,37 @@ var executeEntryDecision = async ({
6964
7557
  }
6965
7558
  return signal ?? decision.code;
6966
7559
  };
7560
+ async function executeEntryDecision(params) {
7561
+ if (!params.portfolio) return executeAdmittedEntryDecision(params);
7562
+ const { decision, portfolio } = params;
7563
+ const signal = decision.signal;
7564
+ if (!signal || decision.orderPlan.positionIntent === "increase")
7565
+ return "PORTFOLIO_REQUIRES_NEW_SIGNAL_POSITION";
7566
+ signal.orderId ??= createRuntimeOrderId(signal.strategy);
7567
+ const result = await portfolio.submit(
7568
+ params.portfolioTime ?? decision.entryContext.timestamp,
7569
+ {
7570
+ id: signal.orderId,
7571
+ signalId: signal.signalId,
7572
+ strategy: signal.strategy,
7573
+ symbol: params.symbol,
7574
+ direction: decision.entryContext.direction,
7575
+ qty: decision.orderPlan.qty,
7576
+ timestamp: decision.entryContext.timestamp
7577
+ },
7578
+ (markPortfolioSubmitted, rejectPortfolioSubmission) => executeAdmittedEntryDecision({
7579
+ ...params,
7580
+ markPortfolioSubmitted,
7581
+ rejectPortfolioSubmission
7582
+ })
7583
+ );
7584
+ if (result.reason) {
7585
+ signal.orderStatus = "failed";
7586
+ signal.orderFailureReason = result.reason;
7587
+ return signal;
7588
+ }
7589
+ return result.value;
7590
+ }
6967
7591
 
6968
7592
  // src/strategyRuntime.ts
6969
7593
  var cloneWithPropertyDescriptors = (value) => Object.create(
@@ -6991,6 +7615,7 @@ var createStrategyRuntime = ({
6991
7615
  };
6992
7616
  const creator = async ({
6993
7617
  userName,
7618
+ connectorName,
6994
7619
  config: baseConfig,
6995
7620
  symbol,
6996
7621
  universe: requestedUniverse,
@@ -7029,6 +7654,37 @@ var createStrategyRuntime = ({
7029
7654
  const projectConfig = await loadTradejsConfig(projectRoot);
7030
7655
  const projectHooks = projectConfig.hooks;
7031
7656
  const env = String(config.ENV ?? "BACKTEST");
7657
+ let portfolioMarket;
7658
+ const portfolio = getPortfolioRuntime({
7659
+ symbol,
7660
+ canClose: async (position, intent, asOf) => {
7661
+ if (!portfolioMarket || portfolioMarket.candle.timestamp > asOf || !isRuntimeOrderExecutionEnabled({ config, env, connector }))
7662
+ return false;
7663
+ const gate = await authorizeClose(
7664
+ {
7665
+ kind: "exit",
7666
+ code: intent.reason,
7667
+ closePlan: {
7668
+ direction: position.direction,
7669
+ price: position.markPrice,
7670
+ timestamp: asOf
7671
+ }
7672
+ },
7673
+ portfolioMarket,
7674
+ position.strategy
7675
+ );
7676
+ return gate?.allow !== false;
7677
+ },
7678
+ connectorName,
7679
+ value: baseConfig?.PORTFOLIO_POLICY ?? config.PORTFOLIO_POLICY,
7680
+ strategy: strategyName,
7681
+ connector,
7682
+ env,
7683
+ userName,
7684
+ accountId,
7685
+ deploymentId
7686
+ });
7687
+ const portfolioContext = portfolio ? (now2) => isRuntimeOrderExecutionEnabled({ config, env, connector }) ? portfolio.tick(now2) : portfolio.context(now2) : void 0;
7032
7688
  const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
7033
7689
  const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
7034
7690
  if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
@@ -7193,7 +7849,10 @@ var createStrategyRuntime = ({
7193
7849
  return invokeHook(stage, hook, params, errorContext);
7194
7850
  };
7195
7851
  const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
7196
- const failureResult = stage === "beforeEntryGate" ? { allow: false, reason: "ERROR" } : void 0;
7852
+ const failureResult = {
7853
+ allow: false,
7854
+ reason: "ERROR"
7855
+ };
7197
7856
  const projectResults = await invokeProjectHooks(
7198
7857
  stage,
7199
7858
  params,
@@ -7214,6 +7873,12 @@ var createStrategyRuntime = ({
7214
7873
  failureResult
7215
7874
  );
7216
7875
  };
7876
+ const authorizeClose = (decision, market, owner = strategyName) => invokeGateHooks(
7877
+ "beforeClosePosition",
7878
+ resolveManifest(owner)?.hooks?.beforeClosePosition,
7879
+ { ctx: getHookCtx(owner), market, decision },
7880
+ { decision, market }
7881
+ );
7217
7882
  const applyProjectAfterCoreDecisionHooks = async ({
7218
7883
  hookCtx,
7219
7884
  market,
@@ -7327,6 +7992,20 @@ var createStrategyRuntime = ({
7327
7992
  interval: decisionInterval
7328
7993
  }) => {
7329
7994
  if (!baseContext) return void 0;
7995
+ const profileSource = strategyManifest?.contextRequirements?.tickerProfiles;
7996
+ if (profileSource) {
7997
+ const profile = await loadTickerProfileContext({
7998
+ ...profileSource,
7999
+ projectRoot,
8000
+ symbol: decisionSymbol,
8001
+ timestamp: candle.timestamp
8002
+ });
8003
+ if (!profile)
8004
+ throw new Error(
8005
+ "Required ticker profile is unavailable at decision time"
8006
+ );
8007
+ baseContext = withTickerProfile(baseContext, profile);
8008
+ }
7330
8009
  if (!coreContextRequirements.has("hyperliquidWhales")) {
7331
8010
  return baseContext;
7332
8011
  }
@@ -7494,6 +8173,8 @@ var createStrategyRuntime = ({
7494
8173
  runtime: pending2.runtime
7495
8174
  });
7496
8175
  return executeEntryDecision({
8176
+ portfolio,
8177
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
7497
8178
  connector,
7498
8179
  symbol,
7499
8180
  decision: pending2.decision,
@@ -7511,6 +8192,11 @@ var createStrategyRuntime = ({
7511
8192
  });
7512
8193
  };
7513
8194
  const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
8195
+ portfolioMarket = { candle, btcCandle };
8196
+ if (portfolioContext)
8197
+ await portfolioContext(
8198
+ env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now()
8199
+ );
7514
8200
  appendCurrentMarketData(candle, btcCandle, options.ethCandle);
7515
8201
  const ethCandle = resolveEthCandle(candle, options.ethCandle);
7516
8202
  indicatorsState.setCurrentBar(candle, btcCandle, ethCandle);
@@ -7613,15 +8299,10 @@ var createStrategyRuntime = ({
7613
8299
  if (!makeOrdersEnabled) {
7614
8300
  return decision.code;
7615
8301
  }
7616
- const closeGate = await invokeGateHooks(
7617
- "beforeClosePosition",
7618
- decisionManifest?.hooks?.beforeClosePosition,
7619
- {
7620
- ctx: decisionHookCtx,
7621
- market,
7622
- decision
7623
- },
7624
- { decision, market }
8302
+ const closeGate = await authorizeClose(
8303
+ decision,
8304
+ market,
8305
+ decisionHookCtx.strategyName
7625
8306
  );
7626
8307
  if (closeGate?.allow === false) {
7627
8308
  return closeGate.reason ? `CLOSE_BLOCKED_BY_HOOK:${closeGate.reason}` : "CLOSE_BLOCKED_BY_HOOK";
@@ -7681,6 +8362,13 @@ var createStrategyRuntime = ({
7681
8362
  });
7682
8363
  const signal = decision.signal;
7683
8364
  if (signal) {
8365
+ if (portfolio)
8366
+ signal.additionalIndicators = {
8367
+ ...signal.additionalIndicators,
8368
+ portfolioContext: await portfolioContext(
8369
+ env === "BACKTEST" || env === "PARITY" ? decision.entryContext.timestamp : Date.now()
8370
+ )
8371
+ };
7684
8372
  if (runtimeLineage) signal.runtimeLineage = runtimeLineage;
7685
8373
  if (universe) signal.universe = universe;
7686
8374
  if (assetClass) signal.assetClass = assetClass;
@@ -7920,6 +8608,8 @@ var createStrategyRuntime = ({
7920
8608
  });
7921
8609
  }
7922
8610
  return executeEntryDecision({
8611
+ portfolio,
8612
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
7923
8613
  connector,
7924
8614
  symbol,
7925
8615
  decision,
@@ -8702,7 +9392,7 @@ var resolveExecutionCosts = async (params) => {
8702
9392
  };
8703
9393
 
8704
9394
  // src/testConnector.ts
8705
- var import_node_crypto4 = require("crypto");
9395
+ var import_node_crypto5 = require("crypto");
8706
9396
  var import_constants8 = require("@tradejs/core/constants");
8707
9397
  var import_backtest2 = require("@tradejs/core/backtest");
8708
9398
  var import_trade2 = require("@tradejs/core/trade");
@@ -9175,7 +9865,7 @@ var createTestConnector = (connector, context) => {
9175
9865
  },
9176
9866
  kline: async (options) => connector.kline(options),
9177
9867
  getResult: async () => {
9178
- const orderLogId = (0, import_node_crypto4.randomUUID)().slice(-12);
9868
+ const orderLogId = (0, import_node_crypto5.randomUUID)().slice(-12);
9179
9869
  const fullStat = fastMode ? (0, import_backtest2.calculateStatsFull)(positionLog) : null;
9180
9870
  return {
9181
9871
  stat: fullStat ? {
@@ -10591,19 +11281,19 @@ var prepareTestingData = async (params) => {
10591
11281
  if (shouldLoadExecutionCandles && !cachedExecutionBtcData) {
10592
11282
  state.btcKlineCache.set(executionBtcCacheKey, executionBtcDataRaw);
10593
11283
  }
10594
- const aligned = universe === "tradfi" ? { alignedCoinCandles: dataRaw, alignedBtcCandles: dataRaw } : (0, import_indicators3.alignSortedCandlesByTimestamp)(dataRaw, btcDataRaw);
11284
+ const aligned = universe === "tradfi" ? { alignedCoinCandles: dataRaw, alignedBtcCandles: dataRaw } : (0, import_indicators4.alignSortedCandlesByTimestamp)(dataRaw, btcDataRaw);
10595
11285
  const { data, btcData } = filterClosedAlignedCandles(
10596
11286
  aligned.alignedCoinCandles,
10597
11287
  aligned.alignedBtcCandles,
10598
11288
  interval
10599
11289
  );
10600
- const btcBinanceData = universe === "tradfi" ? [] : (0, import_indicators3.alignSortedCandlesByTimestamp)(data, btcBinanceDataRaw).alignedBtcCandles;
10601
- const btcCoinbaseData = universe === "tradfi" ? [] : (0, import_indicators3.alignSortedCandlesByTimestamp)(data, btcCoinbaseDataRaw).alignedBtcCandles;
10602
- const ethData = universe === "tradfi" ? [] : (0, import_indicators3.alignSortedCandlesByTimestamp)(data, ethDataRaw).alignedBtcCandles;
11290
+ const btcBinanceData = universe === "tradfi" ? [] : (0, import_indicators4.alignSortedCandlesByTimestamp)(data, btcBinanceDataRaw).alignedBtcCandles;
11291
+ const btcCoinbaseData = universe === "tradfi" ? [] : (0, import_indicators4.alignSortedCandlesByTimestamp)(data, btcCoinbaseDataRaw).alignedBtcCandles;
11292
+ const ethData = universe === "tradfi" ? [] : (0, import_indicators4.alignSortedCandlesByTimestamp)(data, ethDataRaw).alignedBtcCandles;
10603
11293
  const alignedExecution = shouldLoadExecutionCandles ? universe === "tradfi" ? {
10604
11294
  alignedCoinCandles: executionDataRaw,
10605
11295
  alignedBtcCandles: executionDataRaw
10606
- } : (0, import_indicators3.alignSortedCandlesByTimestamp)(executionDataRaw, executionBtcDataRaw) : { alignedCoinCandles: [], alignedBtcCandles: [] };
11296
+ } : (0, import_indicators4.alignSortedCandlesByTimestamp)(executionDataRaw, executionBtcDataRaw) : { alignedCoinCandles: [], alignedBtcCandles: [] };
10607
11297
  const { data: backtestExecutionData, btcData: backtestExecutionBtcData } = filterClosedAlignedCandles(
10608
11298
  alignedExecution.alignedCoinCandles,
10609
11299
  alignedExecution.alignedBtcCandles,