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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -789,1194 +789,789 @@ __export(runtimeStrategies_exports, {
789
789
  });
790
790
  module.exports = __toCommonJS(runtimeStrategies_exports);
791
791
  var import_jev8 = require("@tradejs/core/jev");
792
- var import_node_crypto4 = require("crypto");
792
+ var import_node_crypto5 = require("crypto");
793
793
  var import_promises2 = require("fs/promises");
794
794
  var import_node_path2 = __toESM(require("path"));
795
795
  var import_runtimeControls = require("@tradejs/infra/runtimeControls");
796
796
  var import_tradingAccounts = require("@tradejs/infra/tradingAccounts");
797
797
 
798
- // src/strategyRuntime.ts
799
- var import_jev6 = require("@tradejs/core/jev");
798
+ // src/strategy/portfolioRuntime.ts
799
+ var import_node_crypto2 = require("crypto");
800
+ var import_portfolio2 = require("@tradejs/core/portfolio");
801
+ var import_redis2 = require("@tradejs/infra/redis");
800
802
 
801
- // src/jev.ts
802
- var import_promises = __toESM(require("fs/promises"));
803
- var import_node_path = __toESM(require("path"));
804
- var import_jev4 = require("@tradejs/core/jev");
805
- var import_jev5 = require("@tradejs/infra/jev");
806
- var import_userSettings = require("@tradejs/infra/userSettings");
807
- init_jevInput();
808
- init_jevInput();
809
- init_jevTraining();
810
- var resolveJevProvider = async (userName) => {
811
- const settings = await (0, import_userSettings.getUserSettings)(userName);
812
- const provider = {
813
- endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
814
- model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
815
- };
816
- (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
817
- if (!settings.JEV_API_KEY)
818
- throw new Error("Configure the Jev API key in Account settings");
819
- return provider;
820
- };
821
- var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
822
- var pending = /* @__PURE__ */ new Map();
823
- var pendingResponses = /* @__PURE__ */ new Map();
824
- var evaluateJevInput = async ({
825
- input,
826
- config,
827
- userName,
828
- projectRoot
829
- }) => {
830
- config = (0, import_jev4.parseJevConfig)(config);
831
- const inputHash = (0, import_jev5.jevHash)(input);
832
- const questions = questionsForJevInput(input);
833
- const questionsHash = (0, import_jev5.jevHash)(questions);
834
- const base = {
835
- questionSet: import_jev4.JEV_QUESTION_SET,
836
- schema: "tradejs-signal-assessment/v5",
837
- source: config.source,
838
- mode: config.mode,
839
- inputHash,
840
- questionsHash,
841
- geometryStatus: input.geometryStatus,
842
- levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
843
- };
844
- if (!input.questions.length)
845
- return {
846
- assessment: {
847
- ...base,
848
- status: "unavailable",
849
- model: "",
850
- scores: emptyScores(),
851
- confidence: emptyScores()
852
- }
853
- };
854
- if (config.source === "local") {
855
- const contents = await import_promises.default.readFile(
856
- import_node_path.default.resolve(projectRoot, config.modelFile),
857
- "utf8"
858
- );
859
- if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
860
- throw new Error("Local Jev gate checksum mismatch");
861
- const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
862
- const model = validateJevGateModel2(JSON.parse(contents));
863
- if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
864
- throw new Error("Local Jev gate strategy or question version mismatch");
865
- const scores = Object.fromEntries(
866
- import_jev4.JEV_DIMENSIONS.map((key) => [
867
- key,
868
- input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
869
- ])
870
- );
871
- return {
872
- assessment: {
873
- ...base,
874
- status: "available",
875
- model: config.modelSha256,
876
- scores,
877
- confidence: emptyScores()
803
+ // src/portfolio.ts
804
+ var import_portfolio = require("@tradejs/core/portfolio");
805
+ function createPortfolioCoordinator({
806
+ policy,
807
+ strategy,
808
+ store,
809
+ execution
810
+ }) {
811
+ const fingerprint3 = (0, import_portfolio.portfolioFingerprint)(policy, strategy);
812
+ let cache;
813
+ async function transact(now2, change, force = false) {
814
+ for (let attempt = 0; attempt < 16; attempt++) {
815
+ const previous = await store.read();
816
+ if (previous && previous.fingerprint !== fingerprint3)
817
+ throw new Error(
818
+ "Portfolio policy changed under existing state; reconcile the persisted scope before changing configuration"
819
+ );
820
+ const base = previous ?? (0, import_portfolio.createPortfolioState)(fingerprint3);
821
+ const bucket = Math.floor(now2 / policy.observationIntervalMs);
822
+ const observation = !force && cache?.revision === base.revision && cache.bucket === bucket ? cache.observation : await execution.observe(now2, base.reservations);
823
+ const reconciled = (0, import_portfolio.reconcilePortfolio)(
824
+ policy,
825
+ strategy,
826
+ base,
827
+ observation
828
+ );
829
+ for (const intent of base.closing) {
830
+ if (!observation.positions.some(
831
+ (p) => p.symbol === intent.position.symbol
832
+ ))
833
+ await execution.confirmClosed?.(intent, observation.asOf);
878
834
  }
879
- };
880
- }
881
- const provider = config.provider ?? await resolveJevProvider(userName);
882
- const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
883
- const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
884
- const file = import_node_path.default.join(dir, "records", `${id}.json`);
885
- const getRecord = async () => {
886
- const existing = await (0, import_jev5.readJevArtifact)(file);
887
- if (existing) {
888
- 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))
889
- throw new Error("Jev recording identity mismatch");
890
- (0, import_jev5.validateJevResponse)(existing.response, input.questions);
891
- for (const key of import_jev4.JEV_DIMENSIONS) {
892
- const expected = input.questions.includes(key) ? existing.response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null;
893
- if (existing.scores[key] !== expected)
894
- throw new Error("Jev recording scores do not match the response");
835
+ const changed = change(reconciled, observation);
836
+ const next = { ...changed.state, revision: base.revision + 1 };
837
+ if (await store.compare(previous, next)) {
838
+ cache = { revision: next.revision, bucket, observation };
839
+ return { state: next, observation, result: changed.result };
895
840
  }
896
- return existing;
841
+ cache = void 0;
897
842
  }
898
- if (config.source === "recorded")
899
- throw new Error(
900
- `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.`
843
+ throw new Error("Portfolio state contention; entry was not submitted");
844
+ }
845
+ async function tick(now2, force = false) {
846
+ const claimed = await transact(
847
+ now2,
848
+ (state, observation) => {
849
+ const commands = (0, import_portfolio.portfolioCloseCommands)(policy, state, observation);
850
+ return {
851
+ state: {
852
+ ...state,
853
+ closing: state.closing.map(
854
+ (c) => commands.some((x) => x.intent.positionId === c.positionId) ? { ...c, lastAttemptAt: observation.asOf } : c
855
+ )
856
+ },
857
+ result: commands
858
+ };
859
+ },
860
+ force
861
+ );
862
+ for (const command of claimed.result)
863
+ await execution.close(
864
+ command.position,
865
+ command.intent,
866
+ claimed.observation.asOf
901
867
  );
902
- const settings = await (0, import_userSettings.getUserSettings)(userName);
903
- if (!settings.JEV_API_KEY)
904
- throw new Error("Configure the Jev API key in Account settings");
905
- const apiKey = settings.JEV_API_KEY;
906
- const configuredProvider = await resolveJevProvider(userName);
907
- if (configuredProvider.endpoint !== provider.endpoint)
908
- throw new Error(
909
- "Jev account endpoint changed; update the frozen provider configuration"
868
+ if (claimed.result.length) {
869
+ cache = void 0;
870
+ const refreshed = await transact(
871
+ now2,
872
+ (state) => ({ state, result: void 0 }),
873
+ true
874
+ );
875
+ return (0, import_portfolio.buildPortfolioContext)(
876
+ policy,
877
+ strategy,
878
+ refreshed.state,
879
+ refreshed.observation
910
880
  );
911
- const started = Date.now();
912
- const state = stateForJevInput(input);
913
- const responseId = (0, import_jev5.jevHash)({
914
- state,
915
- questionsHash,
916
- provider,
917
- user: (0, import_jev5.jevHash)(userName)
918
- });
919
- const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
920
- let responsePromise = pendingResponses.get(responseFile);
921
- if (!responsePromise) {
922
- responsePromise = (async () => {
923
- const saved = await (0, import_jev5.readJevArtifact)(responseFile);
924
- if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
925
- const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
926
- await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
927
- return fresh;
928
- })();
929
- pendingResponses.set(responseFile, responsePromise);
930
- }
931
- let response;
932
- try {
933
- response = await responsePromise;
934
- } finally {
935
- pendingResponses.delete(responseFile);
936
881
  }
937
- const scores = Object.fromEntries(
938
- import_jev4.JEV_DIMENSIONS.map((key) => [
939
- key,
940
- input.questions.includes(key) ? response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null
941
- ])
882
+ return (0, import_portfolio.buildPortfolioContext)(
883
+ policy,
884
+ strategy,
885
+ claimed.state,
886
+ claimed.observation
942
887
  );
943
- const value = {
944
- schema: "tradejs-jev-record/v4",
945
- id,
946
- inputHash,
947
- questionsHash,
948
- provider,
949
- input,
950
- response,
951
- scores,
952
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
953
- elapsedMs: Date.now() - started
954
- };
955
- await (0, import_jev5.writeJevArtifact)(file, value);
956
- return await (0, import_jev5.readJevArtifact)(file);
957
- };
958
- const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
959
- let promise = pending.get(pendingKey);
960
- if (!promise) {
961
- promise = getRecord();
962
- pending.set(pendingKey, promise);
963
888
  }
964
- let record;
965
- try {
966
- record = await promise;
967
- } finally {
968
- pending.delete(pendingKey);
889
+ async function updateReservation(now2, id, submitted, qty) {
890
+ return transact(
891
+ now2,
892
+ (s, o) => {
893
+ const exists = s.reservations.some((r) => r.id === id);
894
+ const blocked = submitted && (!exists || s.closing.length > 0 || o.asOf <= s.blockedThrough);
895
+ return {
896
+ state: {
897
+ ...s,
898
+ reservations: s.reservations.flatMap(
899
+ (r) => r.id !== id ? [r] : submitted && !blocked ? [{ ...r, submitted: true, qty: qty ?? r.qty }] : []
900
+ )
901
+ },
902
+ result: !blocked
903
+ };
904
+ },
905
+ true
906
+ );
969
907
  }
970
908
  return {
971
- record,
972
- assessment: {
973
- ...base,
974
- status: "available",
975
- recordId: id,
976
- model: record.response.model,
977
- scores: record.scores,
978
- confidence: Object.fromEntries(
979
- import_jev4.JEV_DIMENSIONS.map((key) => [
980
- key,
981
- record.response.answers[key]?.confidence ?? null
982
- ])
983
- )
909
+ async context(now2) {
910
+ const state = await store.read() ?? (0, import_portfolio.createPortfolioState)(fingerprint3);
911
+ if (state.fingerprint !== fingerprint3)
912
+ throw new Error("Portfolio policy changed under existing state");
913
+ const observation = await execution.observe(now2, state.reservations);
914
+ (0, import_portfolio.validatePortfolioObservation)(observation);
915
+ return (0, import_portfolio.buildPortfolioContext)(policy, strategy, state, observation);
916
+ },
917
+ tick,
918
+ async submit(now2, entry, send) {
919
+ await tick(now2);
920
+ const admitted = await transact(
921
+ now2,
922
+ (s, o) => {
923
+ const r = (0, import_portfolio.reservePortfolioEntry)(policy, strategy, s, o, {
924
+ ...entry,
925
+ submitted: false,
926
+ oppositeHandled: false
927
+ });
928
+ return { state: r.state, result: r.reason };
929
+ },
930
+ true
931
+ );
932
+ if (admitted.result) return { reason: admitted.result };
933
+ let submitted = false;
934
+ try {
935
+ const value = await send(
936
+ async (qty) => {
937
+ if (!Number.isFinite(qty) || qty <= 0)
938
+ throw new Error("Invalid submitted portfolio quantity");
939
+ submitted = true;
940
+ const update = await updateReservation(now2, entry.id, true, qty);
941
+ if (!update.result)
942
+ throw new Error("PORTFOLIO_SUBMISSION_NO_LONGER_ADMITTED");
943
+ },
944
+ async () => {
945
+ await transact(
946
+ now2,
947
+ (s) => ({
948
+ state: {
949
+ ...s,
950
+ reservations: s.reservations.filter((r) => r.id !== entry.id),
951
+ completedSignals: {
952
+ ...s.completedSignals,
953
+ [entry.signalId]: now2
954
+ }
955
+ },
956
+ result: void 0
957
+ }),
958
+ true
959
+ );
960
+ }
961
+ );
962
+ if (!submitted) await updateReservation(now2, entry.id, false);
963
+ else await tick(now2, true);
964
+ return { value };
965
+ } catch (error) {
966
+ if (!submitted) await updateReservation(now2, entry.id, false);
967
+ throw error;
968
+ }
984
969
  }
985
970
  };
986
- };
987
- var assessSignalWithJev = async ({
988
- signal,
989
- config,
990
- userName,
991
- projectRoot,
992
- strict,
993
- scope = "runtime"
994
- }) => {
995
- let assessment;
996
- try {
997
- const input = buildJevInput(signal);
998
- ({ assessment } = await evaluateJevInput({
999
- input,
1000
- config,
1001
- userName,
1002
- projectRoot
1003
- }));
1004
- } catch (error) {
1005
- if (strict) throw error;
1006
- assessment = {
1007
- questionSet: import_jev4.JEV_QUESTION_SET,
1008
- schema: "tradejs-signal-assessment/v5",
1009
- source: config.source,
1010
- mode: config.mode,
1011
- status: "unavailable",
1012
- inputHash: "",
1013
- questionsHash: "",
1014
- geometryStatus: "absent",
1015
- levelsValid: null,
1016
- model: config.provider?.model ?? config.modelSha256 ?? "",
1017
- scores: emptyScores(),
1018
- confidence: emptyScores()
1019
- };
1020
- }
1021
- signal.assessment = assessment;
1022
- signal.additionalIndicators = {
1023
- ...signal.additionalIndicators,
1024
- jev: (0, import_jev4.jevAssessmentFeatures)(assessment)
971
+ }
972
+ function createMemoryPortfolioStore() {
973
+ let state = null;
974
+ return {
975
+ read: async () => state === null ? null : structuredClone(state),
976
+ compare: async (expected, next) => {
977
+ if ((state?.revision ?? -1) !== (expected?.revision ?? -1)) return false;
978
+ state = structuredClone(next);
979
+ return true;
980
+ }
1025
981
  };
1026
- const identity = {
1027
- signalId: signal.signalId,
1028
- strategy: signal.strategy,
1029
- symbol: signal.symbol,
1030
- timestamp: signal.timestamp,
1031
- scope,
1032
- user: (0, import_jev5.jevHash)(userName),
1033
- config,
1034
- assessment
1035
- };
1036
- await (0, import_jev5.writeJevArtifact)(
1037
- import_node_path.default.resolve(
1038
- projectRoot,
1039
- config.recordsDir ?? "data/ai/jev",
1040
- "decisions",
1041
- `${(0, import_jev5.jevHash)(identity)}.json`
1042
- ),
1043
- identity
1044
- );
1045
- return assessment;
1046
- };
1047
-
1048
- // src/strategyRuntime.ts
1049
- var import_constants5 = require("@tradejs/core/constants");
1050
- var import_strategies6 = require("@tradejs/core/strategies");
1051
- var import_logger11 = require("@tradejs/infra/logger");
1052
-
1053
- // src/strategyHelpers/runtime.ts
1054
- var import_logger9 = require("@tradejs/infra/logger");
1055
- var import_constants3 = require("@tradejs/core/constants");
1056
- var import_ml2 = require("@tradejs/infra/ml");
982
+ }
1057
983
 
1058
- // src/strategy/entrySubmissionValidation.ts
984
+ // src/runtimeJournal.ts
985
+ var import_node_crypto = require("crypto");
986
+ var import_constants = require("@tradejs/core/constants");
987
+ var import_time = require("@tradejs/core/time");
988
+ var import_trade = require("@tradejs/core/trade");
1059
989
  var import_logger = require("@tradejs/infra/logger");
1060
-
1061
- // src/strategy/entryExecutionValidation.ts
1062
- var EntrySubmissionValidationError = class extends Error {
1063
- constructor() {
1064
- super(...arguments);
1065
- this.name = "EntrySubmissionValidationError";
1066
- }
1067
- };
1068
- var resolveEntryExecutionLimits = (config, direction) => {
1069
- const side = config[direction];
1070
- const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
1071
- const maximum = config.MAX_LOSS_VALUE;
1072
- if (minimum == null && maximum == null) return void 0;
1073
- const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
1074
- const maxLossValue = maximum == null ? void 0 : Number(maximum);
1075
- const feeRate = Number(config.RISK_FEE_RATE ?? 0);
1076
- const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
1077
- const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
1078
- const slippageBps = slippage + impact;
1079
- 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)
1080
- throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
1081
- return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
1082
- };
1083
- var validateEntryExecution = ({
990
+ var import_redis = require("@tradejs/infra/redis");
991
+ var now = () => Date.now();
992
+ var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
993
+ var calculateClosedPnl = ({
1084
994
  direction,
1085
- price,
1086
- qty,
1087
- stopLossPrice,
1088
- takeProfitPrice,
1089
- limits
995
+ entryPrice,
996
+ exitPrice,
997
+ qty
1090
998
  }) => {
1091
- if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
1092
- throw new EntrySubmissionValidationError(
1093
- "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1094
- );
1095
- const side = direction === "LONG" ? 1 : -1;
1096
- if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
1097
- throw new EntrySubmissionValidationError(
1098
- "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
1099
- );
1100
- const slippage = limits.slippageBps / 1e4;
1101
- const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
1102
- const entryCost = cost(price, side);
1103
- const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
1104
- const rewardPerUnit = Math.max(
1105
- 0,
1106
- Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
1107
- );
1108
- const netRiskRatio = rewardPerUnit / lossPerUnit;
1109
- const plannedLossValue = qty * lossPerUnit;
1110
- if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
1111
- throw new EntrySubmissionValidationError(
1112
- "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1113
- );
1114
- if (limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio)
1115
- throw new EntrySubmissionValidationError(
1116
- "ENTRY_RR_BELOW_MIN_AT_SUBMISSION"
1117
- );
1118
- const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
1119
- return { netRiskRatio, plannedLossValue, riskExceeded };
999
+ const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
1000
+ return Number.isFinite(pnl) ? pnl : null;
1120
1001
  };
1121
-
1122
- // src/strategy/entrySubmissionValidation.ts
1123
- var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
1124
- var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
1125
- var getOrderArrivalSnapshot = async ({
1126
- connector,
1127
- symbol,
1128
- requireTimestamp = false
1129
- }) => {
1130
- if (typeof connector.getTopOfBookTicker !== "function") {
1131
- return {
1132
- arrivalSnapshotTime: Date.now(),
1133
- arrivalSource: "unavailable",
1134
- bid: null,
1135
- ask: null,
1136
- arrivalMid: null,
1137
- spreadBps: null
1138
- };
1002
+ var createRuntimeOrderId = (strategy) => {
1003
+ const prefix = (0, import_trade.createRuntimeOrderLinkPrefix)(strategy);
1004
+ if (prefix === "tjs-") {
1005
+ return `tjs-${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
1006
+ }
1007
+ return `${prefix}${toRandomOrderSuffix()}`;
1008
+ };
1009
+ var recordRuntimeTradeOpen = async (params) => {
1010
+ const { userName } = params;
1011
+ if (!userName) {
1012
+ return null;
1139
1013
  }
1014
+ const record = {
1015
+ ...params,
1016
+ entryCount: 1,
1017
+ lastEntryPrice: params.entryPrice,
1018
+ lastEntryQty: params.qty,
1019
+ lastEntryTimestamp: params.entryTimestamp,
1020
+ status: "active",
1021
+ currentPrice: params.entryPrice,
1022
+ currentPnl: 0,
1023
+ closedPnl: null,
1024
+ exitPrice: null,
1025
+ exitTimestamp: null,
1026
+ lastSyncedAt: now()
1027
+ };
1028
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(record.entryTimestamp);
1029
+ const runtimeScopeId = record.deploymentId ?? record.accountId;
1140
1030
  try {
1141
- const ticker = await connector.getTopOfBookTicker(symbol);
1142
- const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
1143
- const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
1144
- const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
1145
- const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
1146
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
1147
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
1148
- return {
1149
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
1150
- arrivalSource: "top_of_book",
1151
- bid,
1152
- ask,
1153
- arrivalMid,
1154
- spreadBps
1155
- };
1031
+ await Promise.all([
1032
+ (0, import_redis.setData)(import_redis.redisKeys.runtimeTrade(userName, record.orderId), record, {
1033
+ expire: 0
1034
+ }),
1035
+ (0, import_redis.setHashJsonField)(
1036
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1037
+ record.orderId,
1038
+ record,
1039
+ { expire: 0 }
1040
+ ),
1041
+ (0, import_redis.setData)(
1042
+ import_redis.redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
1043
+ { orderId: record.orderId },
1044
+ { expire: 0 }
1045
+ )
1046
+ ]);
1156
1047
  } catch (error) {
1157
- import_logger.logger.warn(
1158
- "runtime order arrival snapshot failed: %s %s",
1159
- symbol,
1048
+ import_logger.logger.error(
1049
+ "runtime trade open journal failed: %s %s",
1050
+ record.symbol,
1160
1051
  error?.message || String(error)
1161
1052
  );
1162
- return {
1163
- arrivalSnapshotTime: Date.now(),
1164
- arrivalSource: "top_of_book_error",
1165
- bid: null,
1166
- ask: null,
1167
- arrivalMid: null,
1168
- spreadBps: null
1169
- };
1170
1053
  }
1054
+ return record;
1171
1055
  };
1172
- var validateEntryProtectionAtArrival = ({
1173
- direction,
1174
- signalPrice,
1175
- bid,
1176
- ask,
1177
- arrivalMid,
1178
- takeProfits,
1179
- stopLossPrice
1180
- }) => {
1181
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
1182
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
1183
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
1184
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
1185
- }
1186
- for (const takeProfit of takeProfits) {
1187
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
1188
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
1189
- }
1056
+ var recordRuntimeTradeIncrease = async (params) => {
1057
+ const {
1058
+ userName,
1059
+ strategy,
1060
+ symbol,
1061
+ direction,
1062
+ resultingQty,
1063
+ resultingEntryPrice,
1064
+ addedQty,
1065
+ addedEntryPrice,
1066
+ entryTimestamp,
1067
+ fee,
1068
+ accountId,
1069
+ deploymentId
1070
+ } = params;
1071
+ if (!userName) {
1072
+ return null;
1190
1073
  }
1191
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
1192
- throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
1074
+ const existing = await getActiveRuntimeTrade({
1075
+ userName,
1076
+ symbol,
1077
+ accountId,
1078
+ deploymentId
1079
+ });
1080
+ if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
1081
+ return null;
1193
1082
  }
1194
- };
1195
- var createEntrySubmissionValidation = ({
1196
- connector,
1197
- symbol,
1198
- direction,
1199
- takeProfits,
1200
- stopLossPrice,
1201
- limits,
1202
- onValidated
1203
- }) => {
1204
- if (!limits) return void 0;
1205
- if (!connector.capabilities?.orderSubmissionValidation)
1206
- throw new EntrySubmissionValidationError(
1207
- "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
1208
- );
1209
- return async ({ qty, executionPrice }) => {
1210
- const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
1211
- const snapshot2 = simulated && executionPrice != null ? {
1212
- arrivalSnapshotTime: Date.now(),
1213
- arrivalSource: "simulation",
1214
- bid: executionPrice,
1215
- ask: executionPrice,
1216
- arrivalMid: executionPrice,
1217
- spreadBps: 0
1218
- } : await getOrderArrivalSnapshot({
1219
- connector,
1220
- symbol,
1221
- requireTimestamp: true
1222
- });
1223
- const age = Date.now() - snapshot2.arrivalSnapshotTime;
1224
- if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
1225
- throw new EntrySubmissionValidationError(
1226
- "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
1227
- );
1228
- if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
1229
- throw new EntrySubmissionValidationError(
1230
- "ENTRY_QUOTE_STALE_AT_SUBMISSION"
1231
- );
1232
- const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
1233
- validateEntryProtectionAtArrival({
1234
- direction,
1235
- signalPrice: price,
1236
- bid: snapshot2.bid,
1237
- ask: snapshot2.ask,
1238
- arrivalMid: snapshot2.arrivalMid,
1239
- takeProfits,
1240
- stopLossPrice
1241
- });
1242
- const prices = takeProfits.map((tp) => tp.price);
1243
- const economics2 = validateEntryExecution({
1244
- direction,
1245
- price,
1246
- qty,
1247
- stopLossPrice,
1248
- takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
1249
- limits
1250
- });
1251
- onValidated?.(snapshot2, economics2);
1083
+ const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
1084
+ const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
1085
+ const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
1086
+ const next = {
1087
+ ...existing,
1088
+ ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
1089
+ qty: resultingQty,
1090
+ entryPrice: resultingEntryPrice,
1091
+ entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
1092
+ lastEntryPrice: addedEntryPrice,
1093
+ lastEntryQty: addedQty,
1094
+ lastEntryTimestamp: entryTimestamp,
1095
+ currentPrice: resultingEntryPrice,
1096
+ currentPnl: 0,
1097
+ fee: openFee,
1098
+ openFee,
1099
+ totalFee,
1100
+ lastSyncedAt: now()
1252
1101
  };
1253
- };
1254
-
1255
- // src/ai.ts
1256
- var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
1257
- var import_redis = require("@tradejs/infra/redis");
1258
-
1259
- // src/aiShared.ts
1260
- var MAX_AI_SERIES_POINTS = 5;
1261
- var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
1262
- "tradejs.indicators.compactSnapshot"
1263
- );
1264
- var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
1265
- var trimSeriesDeep = (value) => {
1266
- if (Array.isArray(value)) {
1267
- const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
1268
- const isMatrix = trimmed.every((item) => Array.isArray(item));
1269
- if (isMatrix) {
1270
- return trimmed;
1271
- }
1272
- return trimmed.map(
1273
- (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
1274
- );
1275
- }
1276
- if (value && typeof value === "object") {
1277
- return Object.fromEntries(
1278
- Object.entries(value).map(([key, nested]) => [
1279
- key,
1280
- trimSeriesDeep(nested)
1281
- ])
1102
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
1103
+ try {
1104
+ await Promise.all([
1105
+ (0, import_redis.setData)(import_redis.redisKeys.runtimeTrade(userName, existing.orderId), next, {
1106
+ expire: 0
1107
+ }),
1108
+ (0, import_redis.setHashJsonField)(
1109
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1110
+ existing.orderId,
1111
+ next,
1112
+ { expire: 0 }
1113
+ )
1114
+ ]);
1115
+ } catch (error) {
1116
+ import_logger.logger.error(
1117
+ "runtime trade increase journal failed: %s %s",
1118
+ symbol,
1119
+ error?.message || String(error)
1282
1120
  );
1283
1121
  }
1284
- return value;
1122
+ return next;
1285
1123
  };
1286
- var buildCompactAiIndicatorsSnapshot = (value) => {
1287
- const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
1288
- if (typeof compactSnapshot === "function") {
1289
- return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
1124
+ var getActiveRuntimeTrade = async (params) => {
1125
+ const { userName, symbol, accountId, deploymentId } = params;
1126
+ if (!userName) {
1127
+ return null;
1290
1128
  }
1291
- return trimSeriesDeep(value);
1292
- };
1293
-
1294
- // src/aiMarketContext.ts
1295
- var toRecord = (value) => {
1296
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1129
+ const activeRef = await (0, import_redis.getData)(
1130
+ import_redis.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
1131
+ null
1132
+ );
1133
+ const orderId = String(activeRef?.orderId || "").trim();
1134
+ if (!orderId) {
1297
1135
  return null;
1298
1136
  }
1299
- return value;
1300
- };
1301
- var toFiniteNumber = (value) => {
1302
- const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
1303
- return Number.isFinite(numeric) ? numeric : null;
1304
- };
1305
- var roundTo = (value, decimals) => {
1306
- const factor = 10 ** decimals;
1307
- return Math.round(value * factor) / factor;
1308
- };
1309
- var buildMissingSpreadContext = () => ({
1310
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1311
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1312
- available: false,
1313
- value: null,
1314
- zScore: null,
1315
- bps: null,
1316
- absBps: null,
1317
- bias: null,
1318
- severity: null
1319
- });
1320
- var buildSpreadContextFromSignal = (signal) => {
1321
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1322
- const relative = toRecord(baseContext?.relative);
1323
- const execution = toRecord(relative?.execution);
1324
- const spread = toFiniteNumber(execution?.venueSpread);
1325
- const zScore = toFiniteNumber(execution?.venueSpreadZScore);
1326
- if (spread == null) {
1327
- return buildMissingSpreadContext();
1137
+ const existing = await (0, import_redis.getData)(
1138
+ import_redis.redisKeys.runtimeTrade(userName, orderId),
1139
+ null
1140
+ );
1141
+ if (!existing || existing.status !== "active") {
1142
+ await (0, import_redis.delKey)(
1143
+ import_redis.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
1144
+ );
1145
+ return null;
1328
1146
  }
1329
- const value = roundTo(spread, 8);
1330
- const bps = roundTo(value * 1e4, 2);
1331
- const absBps = Math.abs(bps);
1332
- const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
1333
- const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
1334
- return {
1335
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1336
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1337
- available: true,
1338
- value,
1339
- zScore,
1340
- bps,
1341
- absBps,
1342
- bias,
1343
- severity
1344
- };
1147
+ return existing;
1345
1148
  };
1346
- var buildTrueDeltaContextFromSignal = (signal) => {
1347
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1348
- const participation = toRecord(baseContext?.participation);
1349
- const delta = toRecord(participation?.delta);
1350
- const source = String(delta?.source ?? "");
1351
- const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
1352
- if (!delta || !isTrueDeltaSource) {
1353
- return {
1354
- source: source || null,
1355
- available: false,
1356
- buyPressurePct: null,
1357
- buyVolume: null,
1358
- sellVolume: null,
1359
- netDelta: null,
1360
- deltaPct: null,
1361
- signedVolumeZScore: null
1362
- };
1149
+ var markRuntimeTradeClosed = async (params) => {
1150
+ const {
1151
+ userName,
1152
+ expectedOrderId,
1153
+ symbol,
1154
+ strategy,
1155
+ exitPrice,
1156
+ exitTimestamp,
1157
+ closedPnl,
1158
+ exitType,
1159
+ accountId,
1160
+ deploymentId
1161
+ } = params;
1162
+ if (!userName) {
1163
+ return null;
1363
1164
  }
1364
- return {
1365
- source,
1366
- available: true,
1367
- buyPressurePct: toFiniteNumber(delta.buyPressurePct),
1368
- buyVolume: toFiniteNumber(delta.buyVolume),
1369
- sellVolume: toFiniteNumber(delta.sellVolume),
1370
- netDelta: toFiniteNumber(delta.netDelta),
1371
- deltaPct: toFiniteNumber(delta.deltaPct),
1372
- signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
1373
- };
1374
- };
1375
- var buildTradeFlowContextFromSignal = (signal) => {
1376
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1377
- const participation = toRecord(baseContext?.participation);
1378
- const tradeFlow = toRecord(participation?.tradeFlow);
1379
- if (!tradeFlow) {
1380
- return {
1381
- source: null,
1382
- available: false,
1383
- interval: null,
1384
- stale: null,
1385
- trades: null,
1386
- buyPressurePct: null,
1387
- netBaseDelta: null,
1388
- netQuoteDelta: null
1389
- };
1165
+ const existing = expectedOrderId ? await (0, import_redis.getDataStrict)(
1166
+ import_redis.redisKeys.runtimeTrade(userName, expectedOrderId)
1167
+ ) : await getActiveRuntimeTrade({
1168
+ userName,
1169
+ symbol,
1170
+ accountId,
1171
+ deploymentId
1172
+ });
1173
+ if (!existing || expectedOrderId && existing.orderId !== expectedOrderId) {
1174
+ return null;
1390
1175
  }
1391
- return {
1392
- source: String(tradeFlow.source ?? ""),
1393
- available: true,
1394
- interval: String(tradeFlow.interval ?? ""),
1395
- stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
1396
- trades: toFiniteNumber(tradeFlow.trades),
1397
- buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
1398
- netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
1399
- netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
1400
- };
1401
- };
1402
- var buildMarketBreadthContextFromSignal = (signal) => {
1403
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1404
- const relative = toRecord(baseContext?.relative);
1405
- const breadth = toRecord(relative?.marketBreadth);
1406
- if (!breadth) {
1407
- return {
1408
- source: null,
1409
- available: false,
1410
- universe: null,
1411
- interval: null,
1412
- stale: null,
1413
- symbolsCount: null,
1414
- advanceDeclineRatio: null,
1415
- pctAboveMa20: null,
1416
- pctAboveMa50: null,
1417
- equalWeightedReturn: null,
1418
- volumeWeightedReturn: null,
1419
- dispersion: null
1420
- };
1176
+ const orderId = existing.orderId;
1177
+ if (strategy && existing.strategy !== strategy) {
1178
+ return null;
1421
1179
  }
1422
- return {
1423
- source: String(breadth.source ?? ""),
1424
- available: true,
1425
- universe: String(breadth.universe ?? ""),
1426
- interval: String(breadth.interval ?? ""),
1427
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
1428
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
1429
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
1430
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
1431
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
1432
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
1433
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
1434
- dispersion: toFiniteNumber(breadth.dispersion)
1180
+ const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
1181
+ const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
1182
+ direction: existing.direction,
1183
+ entryPrice: existing.entryPrice,
1184
+ exitPrice: resolvedExitPrice,
1185
+ qty: existing.qty
1186
+ }) : existing.closedPnl ?? existing.currentPnl ?? null;
1187
+ const next = expectedOrderId && existing.status === "closed" ? existing : {
1188
+ ...existing,
1189
+ status: "closed",
1190
+ currentPrice: resolvedExitPrice,
1191
+ currentPnl: resolvedClosedPnl,
1192
+ closedPnl: resolvedClosedPnl,
1193
+ exitPrice: resolvedExitPrice,
1194
+ exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
1195
+ exitType: exitType ?? existing.exitType ?? null,
1196
+ lastSyncedAt: now()
1435
1197
  };
1198
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
1199
+ const closeDayKey = (0, import_time.getRuntimeStorageDayKey)(next.exitTimestamp);
1200
+ try {
1201
+ await Promise.all([
1202
+ (expectedOrderId ? import_redis.setDataStrict : import_redis.setData)(
1203
+ import_redis.redisKeys.runtimeTrade(userName, orderId),
1204
+ next,
1205
+ {
1206
+ expire: import_constants.TTL_1M
1207
+ }
1208
+ ),
1209
+ (expectedOrderId ? import_redis.setHashJsonFieldStrict : import_redis.setHashJsonField)(
1210
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1211
+ orderId,
1212
+ next,
1213
+ { expire: import_constants.TTL_1M }
1214
+ ),
1215
+ (expectedOrderId ? import_redis.setHashJsonFieldStrict : import_redis.setHashJsonField)(
1216
+ import_redis.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
1217
+ orderId,
1218
+ next,
1219
+ { expire: import_constants.TTL_1M }
1220
+ ),
1221
+ expectedOrderId ? (0, import_redis.deleteRuntimeTradeReference)(
1222
+ import_redis.redisKeys.runtimeActiveTrade(
1223
+ userName,
1224
+ symbol,
1225
+ deploymentId ?? accountId
1226
+ ),
1227
+ expectedOrderId
1228
+ ) : (0, import_redis.delKey)(
1229
+ import_redis.redisKeys.runtimeActiveTrade(
1230
+ userName,
1231
+ symbol,
1232
+ deploymentId ?? accountId
1233
+ )
1234
+ )
1235
+ ]);
1236
+ } catch (error) {
1237
+ if (expectedOrderId) throw error;
1238
+ import_logger.logger.error(
1239
+ "runtime trade close journal failed: %s %s",
1240
+ symbol,
1241
+ error?.message || String(error)
1242
+ );
1243
+ }
1244
+ return next;
1436
1245
  };
1437
- var buildMarketBreadthsContextFromSignal = (signal) => {
1438
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1439
- const relative = toRecord(baseContext?.relative);
1440
- const breadths = toRecord(relative?.marketBreadths);
1441
- const build = (key) => {
1442
- const breadth = toRecord(breadths?.[key]);
1443
- if (!breadth) {
1246
+
1247
+ // src/strategy/portfolioRuntime.ts
1248
+ var coordinators = /* @__PURE__ */ new WeakMap();
1249
+ async function observeRuntimePortfolio(connector, binding, asOf, reservations, observedNow = Date.now) {
1250
+ if (!connector.getOpenPositionPnl || !connector.getEntryExecutions)
1251
+ throw new Error(
1252
+ "Portfolio policies require position marks and entry executions"
1253
+ );
1254
+ const orders = await Promise.all(
1255
+ reservations.map(async (r) => {
1256
+ if (r.submitted && connector.getEntryOrderObservation) {
1257
+ const fact2 = await connector.getEntryOrderObservation({
1258
+ symbol: r.symbol,
1259
+ orderId: r.id
1260
+ });
1261
+ if (fact2) return fact2;
1262
+ }
1263
+ const executions = r.submitted ? await connector.getEntryExecutions({
1264
+ symbol: r.symbol,
1265
+ startTime: r.timestamp,
1266
+ endTime: asOf
1267
+ }) : [];
1268
+ const filledQty = executions.filter(
1269
+ (e) => (e.orderLinkId === r.id || e.orderId === r.id) && e.direction === r.direction
1270
+ ).reduce((sum, e) => sum + e.qty, 0);
1444
1271
  return {
1445
- source: null,
1446
- available: false,
1447
- universe: null,
1448
- interval: null,
1449
- stale: null,
1450
- symbolsCount: null,
1451
- advanceDeclineRatio: null,
1452
- pctAboveMa20: null,
1453
- pctAboveMa50: null,
1454
- equalWeightedReturn: null,
1455
- volumeWeightedReturn: null,
1456
- dispersion: null
1272
+ id: r.id,
1273
+ status: filledQty >= r.qty ? "filled" : filledQty > 0 ? "partial" : "pending",
1274
+ filledQty,
1275
+ positionId: r.id
1276
+ };
1277
+ })
1278
+ );
1279
+ const snapshots2 = await connector.getOpenPositionPnl();
1280
+ const positions = await Promise.all(
1281
+ snapshots2.map(async (p) => {
1282
+ const reserved = reservations.find(
1283
+ (r) => r.symbol === p.symbol && r.direction === p.direction && orders.some(
1284
+ (o) => o.id === r.id && o.filledQty > 0 && p.qty <= o.filledQty * (1 + 1e-8)
1285
+ )
1286
+ );
1287
+ const trade = await getActiveRuntimeTrade({
1288
+ ...binding,
1289
+ symbol: p.symbol
1290
+ });
1291
+ 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;
1292
+ return {
1293
+ id: reserved?.id ?? owner?.orderId ?? `unknown:${p.symbol}:${p.direction}`,
1294
+ strategy: reserved?.strategy ?? owner?.strategy ?? null,
1295
+ symbol: p.symbol,
1296
+ direction: p.direction,
1297
+ qty: p.qty,
1298
+ entryPrice: p.price,
1299
+ markPrice: p.currentPrice,
1300
+ markedAt: p.observedAt ?? null,
1301
+ stopLossPrice: p.slPrice ?? p.stopLossPrice ?? null
1457
1302
  };
1303
+ })
1304
+ );
1305
+ return { asOf: Math.max(asOf, observedNow()), positions, orders };
1306
+ }
1307
+ function persistentStore(key) {
1308
+ const raw = /* @__PURE__ */ new WeakMap();
1309
+ return {
1310
+ async read() {
1311
+ const value = await import_redis2.portfolioStateStorage.read(key);
1312
+ if (value === null) return null;
1313
+ const state = JSON.parse(value);
1314
+ (0, import_portfolio2.assertPortfolioState)(state);
1315
+ raw.set(state, value);
1316
+ return state;
1317
+ },
1318
+ async compare(previous, next) {
1319
+ const expected = previous === null ? null : raw.get(previous);
1320
+ if (expected === void 0)
1321
+ throw new Error("Portfolio CAS requires a storage snapshot");
1322
+ return import_redis2.portfolioStateStorage.compare(key, expected, JSON.stringify(next));
1458
1323
  }
1459
- return {
1460
- source: String(breadth.source ?? ""),
1461
- available: true,
1462
- universe: String(breadth.universe ?? ""),
1463
- interval: String(breadth.interval ?? ""),
1464
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
1465
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
1466
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
1467
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
1468
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
1469
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
1470
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
1471
- dispersion: toFiniteNumber(breadth.dispersion)
1472
- };
1473
1324
  };
1474
- return {
1475
- top5: build("top5"),
1476
- top10: build("top10"),
1477
- top30: build("top30"),
1478
- top50: build("top50"),
1479
- top100: build("top100")
1325
+ }
1326
+ function getPortfolioRuntime({
1327
+ value,
1328
+ connectorName,
1329
+ strategy,
1330
+ connector,
1331
+ env,
1332
+ userName,
1333
+ accountId,
1334
+ deploymentId
1335
+ }) {
1336
+ const policy = (0, import_portfolio2.parsePortfolioPolicy)(value);
1337
+ if (!policy) return void 0;
1338
+ const replay = (env === "BACKTEST" || env === "PARITY") && Reflect.get(connector, "__tradejsReplayConnector") === true;
1339
+ if ((env === "BACKTEST" || env === "PARITY") && (!replay || !connector.getPortfolioObservation))
1340
+ throw new Error(
1341
+ "Portfolio policies require shared portfolio replay; isolated symbol backtests are unsupported"
1342
+ );
1343
+ if (!replay && (!userName || !accountId || !deploymentId || !connector.getOpenPositionPnl || !connector.getEntryExecutions))
1344
+ throw new Error(
1345
+ "Portfolio runtime requires exact user/account/deployment binding, marks and executions"
1346
+ );
1347
+ const binding = {
1348
+ userName,
1349
+ accountId,
1350
+ deploymentId
1480
1351
  };
1352
+ const scope = policy.scope === "strategy" ? strategy : policy.scope === "account" ? "account" : [...policy.scope.strategies].sort();
1353
+ const key = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify([binding, connectorName, connector.universe, scope])).digest("hex");
1354
+ const fingerprint3 = (0, import_portfolio2.portfolioFingerprint)(policy, strategy);
1355
+ const cacheKey = key;
1356
+ let registry = coordinators.get(connector);
1357
+ if (!registry) {
1358
+ registry = /* @__PURE__ */ new Map();
1359
+ coordinators.set(connector, registry);
1360
+ }
1361
+ const existing = registry.get(cacheKey);
1362
+ if (existing) {
1363
+ if (existing.fingerprint !== fingerprint3)
1364
+ throw new Error("Conflicting portfolio configuration for the same scope");
1365
+ return existing.coordinator;
1366
+ }
1367
+ const coordinator = createPortfolioCoordinator({
1368
+ policy,
1369
+ strategy,
1370
+ store: replay ? createMemoryPortfolioStore() : persistentStore(key),
1371
+ execution: {
1372
+ async confirmClosed(intent, asOf) {
1373
+ if (replay) return;
1374
+ const position = intent.position;
1375
+ await markRuntimeTradeClosed({
1376
+ ...binding,
1377
+ strategy: position.strategy,
1378
+ expectedOrderId: position.id,
1379
+ symbol: position.symbol,
1380
+ exitPrice: position.markPrice,
1381
+ exitTimestamp: asOf,
1382
+ exitType: "exit"
1383
+ });
1384
+ },
1385
+ observe: (asOf, reservations) => replay ? connector.getPortfolioObservation(asOf, reservations) : observeRuntimePortfolio(connector, binding, asOf, reservations),
1386
+ async close(position, intent, asOf) {
1387
+ if (!replay) {
1388
+ const trade = await getActiveRuntimeTrade({
1389
+ ...binding,
1390
+ symbol: position.symbol
1391
+ });
1392
+ if (!trade || trade.orderId !== position.id)
1393
+ throw new Error("Portfolio close ownership changed");
1394
+ }
1395
+ await connector.closePosition({
1396
+ symbol: position.symbol,
1397
+ direction: position.direction,
1398
+ price: position.markPrice,
1399
+ timestamp: asOf,
1400
+ signal: {
1401
+ signalId: `${position.id}:${intent.reason}`,
1402
+ strategy: position.strategy,
1403
+ symbol: position.symbol,
1404
+ direction: position.direction,
1405
+ interval: "1",
1406
+ timestamp: asOf,
1407
+ prices: {
1408
+ riskRatio: 0,
1409
+ currentPrice: position.markPrice,
1410
+ takeProfitPrice: position.markPrice,
1411
+ stopLossPrice: position.markPrice
1412
+ },
1413
+ figures: {},
1414
+ indicators: {},
1415
+ additionalIndicators: { portfolioExitReason: intent.reason }
1416
+ }
1417
+ });
1418
+ }
1419
+ }
1420
+ });
1421
+ registry.set(cacheKey, { fingerprint: fingerprint3, coordinator });
1422
+ return coordinator;
1423
+ }
1424
+
1425
+ // src/strategyHelpers/tickerProfiles.ts
1426
+ var import_tickerProfiles = require("@tradejs/infra/tickerProfiles");
1427
+ var import_tickerProfiles2 = require("@tradejs/core/tickerProfiles");
1428
+
1429
+ // src/tradejsConfig.ts
1430
+ var import_fs = __toESM(require("fs"));
1431
+ var import_path = __toESM(require("path"));
1432
+ var import_url = require("url");
1433
+ var import_config = require("@tradejs/core/config");
1434
+ var import_logger2 = require("@tradejs/infra/logger");
1435
+ var CONFIG_FILE_NAMES = [
1436
+ "tradejs.config.ts",
1437
+ "tradejs.config.mts",
1438
+ "tradejs.config.js",
1439
+ "tradejs.config.mjs",
1440
+ "tradejs.config.cjs"
1441
+ ];
1442
+ var TS_MODULE_RE = /\.(cts|mts|ts)$/i;
1443
+ var cachedByCwd = /* @__PURE__ */ new Map();
1444
+ var announcedConfigFile = /* @__PURE__ */ new Set();
1445
+ var tsNodeRegistered = false;
1446
+ var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
1447
+ var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
1448
+ var getTradejsProjectCwd = (cwd) => {
1449
+ const explicit = String(cwd ?? "").trim();
1450
+ if (explicit) {
1451
+ return import_path.default.resolve(explicit);
1452
+ }
1453
+ const fromEnv = String(process.env.PROJECT_CWD || "").trim();
1454
+ if (fromEnv) {
1455
+ return import_path.default.resolve(fromEnv);
1456
+ }
1457
+ return process.cwd();
1481
1458
  };
1482
- var buildTargetVsBtcContextFromSignal = (signal) => {
1483
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1484
- const relative = toRecord(baseContext?.relative);
1485
- const targetVsBtc = toRecord(relative?.targetVsBtc);
1486
- if (!targetVsBtc) {
1487
- return {
1488
- source: null,
1489
- available: false,
1490
- ratioReturn1h: null,
1491
- ratioReturn4h: null,
1492
- ratioReturn24h: null,
1493
- alphaVsBtc1h: null,
1494
- alphaVsBtc4h: null,
1495
- alphaVsBtc24h: null,
1496
- betaToBtc20: null,
1497
- correlationToBtc20: null,
1498
- ratioTrend: null
1499
- };
1459
+ var normalizeConfig = (rawConfig) => {
1460
+ if (!rawConfig || typeof rawConfig !== "object") {
1461
+ return {};
1500
1462
  }
1463
+ const config = rawConfig;
1464
+ const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
1465
+ const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
1466
+ const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
1467
+ const hooks = (0, import_config.normalizeTradejsConfigHooks)(
1468
+ config.hooks
1469
+ );
1470
+ const runtime = config.runtime && typeof config.runtime === "object" && !Array.isArray(config.runtime) ? config.runtime : void 0;
1501
1471
  return {
1502
- source: String(targetVsBtc.source ?? ""),
1503
- available: true,
1504
- ratioReturn1h: toFiniteNumber(targetVsBtc.ratioReturn1h),
1505
- ratioReturn4h: toFiniteNumber(targetVsBtc.ratioReturn4h),
1506
- ratioReturn24h: toFiniteNumber(targetVsBtc.ratioReturn24h),
1507
- alphaVsBtc1h: toFiniteNumber(targetVsBtc.alphaVsBtc1h),
1508
- alphaVsBtc4h: toFiniteNumber(targetVsBtc.alphaVsBtc4h),
1509
- alphaVsBtc24h: toFiniteNumber(targetVsBtc.alphaVsBtc24h),
1510
- betaToBtc20: toFiniteNumber(targetVsBtc.betaToBtc20),
1511
- correlationToBtc20: toFiniteNumber(targetVsBtc.correlationToBtc20),
1512
- ratioTrend: typeof targetVsBtc.ratioTrend === "string" ? targetVsBtc.ratioTrend : null
1472
+ strategies: strategies2,
1473
+ indicators,
1474
+ connectors,
1475
+ ...hooks ? { hooks } : {},
1476
+ ...runtime ? { runtime } : {}
1513
1477
  };
1514
1478
  };
1515
- var buildBtcAltRegimeContextFromSignal = (signal) => {
1516
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1517
- const relative = toRecord(baseContext?.relative);
1518
- const btcAltRegime = toRecord(relative?.btcAltRegime);
1519
- if (!btcAltRegime) {
1520
- return {
1521
- source: null,
1522
- available: false,
1523
- universe: null,
1524
- interval: null,
1525
- stale: null,
1526
- regime: null,
1527
- btcReturn24h: null,
1528
- altBasketReturn24h: null,
1529
- btcVsAltReturn24h: null,
1530
- btcTurnoverShare24h: null,
1531
- btcTurnoverShareChange24h: null,
1532
- altVolToBtcVol24h: null,
1533
- altDispersion24h: null
1534
- };
1479
+ var getNodeCreateRequire = () => {
1480
+ const builtinModule = process.getBuiltinModule?.("module");
1481
+ if (typeof builtinModule?.createRequire === "function") {
1482
+ return builtinModule.createRequire;
1535
1483
  }
1536
- return {
1537
- source: String(btcAltRegime.source ?? ""),
1538
- available: true,
1539
- universe: String(btcAltRegime.universe ?? ""),
1540
- interval: String(btcAltRegime.interval ?? ""),
1541
- stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
1542
- regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
1543
- btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
1544
- altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
1545
- btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
1546
- btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
1547
- btcTurnoverShareChange24h: toFiniteNumber(
1548
- btcAltRegime.btcTurnoverShareChange24h
1549
- ),
1550
- altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
1551
- altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
1552
- };
1484
+ throw new TypeError("module.createRequire is not available");
1553
1485
  };
1554
- var buildCmcGlobalContextFromSignal = (signal) => {
1555
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1556
- const relative = toRecord(baseContext?.relative);
1557
- const cmcGlobal = toRecord(relative?.cmcGlobal);
1558
- if (!cmcGlobal) {
1559
- return {
1560
- source: null,
1561
- available: false,
1562
- interval: null,
1563
- asOfTs: null,
1564
- stale: null,
1565
- totalMarketCapUsd: null,
1566
- totalVolumeUsd: null,
1567
- totalVolumeReportedUsd: null,
1568
- altMarketCapUsd: null,
1569
- altVolumeUsd: null,
1570
- altVolumeReportedUsd: null,
1571
- btcDominancePct: null,
1572
- ethDominancePct: null,
1573
- btcDominanceChange24hPct: null,
1574
- ethDominanceChange24hPct: null,
1575
- altMarketCapChange24hPct: null,
1576
- altVolumeChange24hPct: null,
1577
- activeCryptocurrencies: null,
1578
- activeExchanges: null,
1579
- activeMarketPairs: null,
1580
- altLiquidityRegime: null
1581
- };
1486
+ var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
1487
+ var ensureTsNodeRegistered = async () => {
1488
+ if (tsNodeRegistered) {
1489
+ return;
1582
1490
  }
1583
- return {
1584
- source: String(cmcGlobal.source ?? ""),
1585
- available: true,
1586
- interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
1587
- asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
1588
- stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
1589
- totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
1590
- totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
1591
- totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
1592
- altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
1593
- altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
1594
- altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
1595
- btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
1596
- ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
1597
- btcDominanceChange24hPct: toFiniteNumber(
1598
- cmcGlobal.btcDominanceChange24hPct
1599
- ),
1600
- ethDominanceChange24hPct: toFiniteNumber(
1601
- cmcGlobal.ethDominanceChange24hPct
1602
- ),
1603
- altMarketCapChange24hPct: toFiniteNumber(
1604
- cmcGlobal.altMarketCapChange24hPct
1605
- ),
1606
- altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
1607
- activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
1608
- activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
1609
- activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
1610
- altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
1611
- };
1491
+ const tsNodeModule = await import("ts-node");
1492
+ const tsNode = tsNodeModule.default ?? tsNodeModule;
1493
+ tsNode.register?.({
1494
+ transpileOnly: true,
1495
+ compilerOptions: {
1496
+ module: "Node16",
1497
+ moduleResolution: "node16"
1498
+ }
1499
+ });
1500
+ tsNodeRegistered = true;
1612
1501
  };
1613
- var buildCmcReferenceAssetsContextFromSignal = (signal) => {
1614
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1615
- const relative = toRecord(baseContext?.relative);
1616
- const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
1617
- if (!cmcReferenceAssets) {
1618
- return {
1619
- source: null,
1620
- available: false,
1621
- interval: null,
1622
- asOfTs: null,
1623
- stale: null,
1624
- btcMarketCapUsd: null,
1625
- ethMarketCapUsd: null,
1626
- btcVolumeUsd: null,
1627
- ethVolumeUsd: null,
1628
- btcVolumeToMarketCap: null,
1629
- ethVolumeToMarketCap: null,
1630
- ethBtcMarketCapRatio: null,
1631
- ethBtcMarketCapRatioChange24hPct: null,
1632
- ethVsBtcVolumeRatio: null,
1633
- referenceLiquidityRegime: null
1634
- };
1502
+ var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
1503
+ const projectRoot = getTradejsProjectCwd(cwd);
1504
+ if (tsconfigPathsRegisteredByCwd.has(projectRoot)) {
1505
+ return;
1635
1506
  }
1636
- return {
1637
- source: String(cmcReferenceAssets.source ?? ""),
1638
- available: true,
1639
- interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
1640
- asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
1641
- stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
1642
- btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
1643
- ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
1644
- btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
1645
- ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
1646
- btcVolumeToMarketCap: toFiniteNumber(
1647
- cmcReferenceAssets.btcVolumeToMarketCap
1648
- ),
1649
- ethVolumeToMarketCap: toFiniteNumber(
1650
- cmcReferenceAssets.ethVolumeToMarketCap
1651
- ),
1652
- ethBtcMarketCapRatio: toFiniteNumber(
1653
- cmcReferenceAssets.ethBtcMarketCapRatio
1654
- ),
1655
- ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
1656
- cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
1657
- ),
1658
- ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
1659
- referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
1660
- };
1507
+ const tsconfigPathsModule = await import("tsconfig-paths");
1508
+ const loadConfig = tsconfigPathsModule.loadConfig;
1509
+ const register = tsconfigPathsModule.register;
1510
+ if (typeof loadConfig !== "function" || typeof register !== "function") {
1511
+ return;
1512
+ }
1513
+ const loadedConfig = loadConfig(projectRoot);
1514
+ if (loadedConfig.resultType !== "success") {
1515
+ return;
1516
+ }
1517
+ register({
1518
+ baseUrl: loadedConfig.absoluteBaseUrl,
1519
+ paths: loadedConfig.paths,
1520
+ addMatchAll: false
1521
+ });
1522
+ tsconfigPathsRegisteredByCwd.add(projectRoot);
1661
1523
  };
1662
- var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
1663
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1664
- const relative = toRecord(baseContext?.relative);
1665
- const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
1666
- if (!cmcExchangeLiquidity) {
1667
- return {
1668
- source: null,
1669
- available: false,
1670
- interval: null,
1671
- asOfTs: null,
1672
- stale: null,
1673
- exchangesCount: null,
1674
- totalVolumeUsd: null,
1675
- totalVolumeChange24hPct: null,
1676
- binanceVolumeUsd: null,
1677
- binanceVolumeShare: null,
1678
- topExchangeVolumeShare: null,
1679
- liquidityRegime: null
1680
- };
1524
+ var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1525
+ const projectRoot = getTradejsProjectCwd(cwd);
1526
+ const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
1527
+ if (cachedMatcher) {
1528
+ const resolved2 = cachedMatcher(moduleName);
1529
+ return resolved2 || null;
1681
1530
  }
1682
- return {
1683
- source: String(cmcExchangeLiquidity.source ?? ""),
1684
- available: true,
1685
- interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
1686
- asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
1687
- stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
1688
- exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
1689
- totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
1690
- totalVolumeChange24hPct: toFiniteNumber(
1691
- cmcExchangeLiquidity.totalVolumeChange24hPct
1692
- ),
1693
- binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
1694
- binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
1695
- topExchangeVolumeShare: toFiniteNumber(
1696
- cmcExchangeLiquidity.topExchangeVolumeShare
1697
- ),
1698
- liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
1699
- };
1531
+ const tsconfigPathsModule = await import("tsconfig-paths");
1532
+ const loadConfig = tsconfigPathsModule.loadConfig;
1533
+ const createMatchPath = tsconfigPathsModule.createMatchPath;
1534
+ if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
1535
+ return null;
1536
+ }
1537
+ const loadedConfig = loadConfig(projectRoot);
1538
+ if (loadedConfig.resultType !== "success") {
1539
+ return null;
1540
+ }
1541
+ const matchPath = createMatchPath(
1542
+ loadedConfig.absoluteBaseUrl,
1543
+ loadedConfig.paths
1544
+ );
1545
+ const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
1546
+ ".ts",
1547
+ ".tsx",
1548
+ ".mts",
1549
+ ".cts",
1550
+ ".js",
1551
+ ".jsx",
1552
+ ".mjs",
1553
+ ".cjs",
1554
+ ".json"
1555
+ ]) || "";
1556
+ tsconfigPathMatchersByCwd.set(projectRoot, matcher);
1557
+ const resolved = matcher(moduleName);
1558
+ return resolved || null;
1700
1559
  };
1701
- var buildCmcFearGreedContextFromSignal = (signal) => {
1702
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1703
- const relative = toRecord(baseContext?.relative);
1704
- const cmcFearGreed = toRecord(relative?.cmcFearGreed);
1705
- if (!cmcFearGreed) {
1706
- return {
1707
- source: null,
1708
- available: false,
1709
- interval: null,
1710
- asOfTs: null,
1711
- stale: null,
1712
- value: null,
1713
- valueChange24h: null,
1714
- valueChange7d: null,
1715
- classification: null,
1716
- sentimentRegime: null
1717
- };
1560
+ var toImportSpecifier = (moduleName) => {
1561
+ if (moduleName.startsWith("file://")) {
1562
+ return moduleName;
1718
1563
  }
1719
- return {
1720
- source: String(cmcFearGreed.source ?? ""),
1721
- available: true,
1722
- interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
1723
- asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
1724
- stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
1725
- value: toFiniteNumber(cmcFearGreed.value),
1726
- valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
1727
- valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
1728
- classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
1729
- sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
1730
- };
1564
+ if (import_path.default.isAbsolute(moduleName)) {
1565
+ return (0, import_url.pathToFileURL)(moduleName).href;
1566
+ }
1567
+ return moduleName;
1731
1568
  };
1732
- var buildCmcIndexesContextFromSignal = (signal) => {
1733
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1734
- const relative = toRecord(baseContext?.relative);
1735
- const cmcIndexes = toRecord(relative?.cmcIndexes);
1736
- if (!cmcIndexes) {
1737
- return {
1738
- source: null,
1739
- available: false,
1740
- interval: null,
1741
- asOfTs: null,
1742
- stale: null,
1743
- cmc100Value: null,
1744
- cmc100Change24hPct: null,
1745
- cmc100TopConstituentSymbol: null,
1746
- cmc100TopConstituentWeightPct: null,
1747
- cmc20Value: null,
1748
- cmc20Change24hPct: null,
1749
- cmc20TopConstituentSymbol: null,
1750
- cmc20TopConstituentWeightPct: null,
1751
- cmc20ToCmc100Ratio: null,
1752
- cmc20ToCmc100RatioChange24hPct: null,
1753
- indexRegime: null
1754
- };
1755
- }
1756
- return {
1757
- source: String(cmcIndexes.source ?? ""),
1758
- available: true,
1759
- interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
1760
- asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
1761
- stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
1762
- cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
1763
- cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
1764
- cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
1765
- cmc100TopConstituentWeightPct: toFiniteNumber(
1766
- cmcIndexes.cmc100TopConstituentWeightPct
1767
- ),
1768
- cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
1769
- cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
1770
- cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
1771
- cmc20TopConstituentWeightPct: toFiniteNumber(
1772
- cmcIndexes.cmc20TopConstituentWeightPct
1773
- ),
1774
- cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
1775
- cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
1776
- cmcIndexes.cmc20ToCmc100RatioChange24hPct
1777
- ),
1778
- indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
1779
- };
1780
- };
1781
- var buildReferenceTradeFlowContextFromSignal = (signal) => {
1782
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1783
- const relative = toRecord(baseContext?.relative);
1784
- const refs = toRecord(relative?.referenceTradeFlow);
1785
- const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
1786
- const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
1787
- const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
1788
- if (!refs) {
1789
- return {
1790
- source: null,
1791
- available: false,
1792
- primaryReferenceSymbol: null,
1793
- referenceSymbols: [],
1794
- primaryTradeFlowBuyPressurePct: null,
1795
- primaryTradeFlowStale: null
1796
- };
1797
- }
1798
- return {
1799
- source: String(refs.source ?? ""),
1800
- available: true,
1801
- primaryReferenceSymbol,
1802
- referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
1803
- primaryTradeFlowBuyPressurePct: toFiniteNumber(
1804
- primaryTradeFlow?.buyPressurePct
1805
- ),
1806
- primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
1807
- };
1808
- };
1809
- var buildAiMarketContext = (signal) => ({
1810
- execution: {
1811
- binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
1812
- },
1813
- participation: {
1814
- trueDelta: buildTrueDeltaContextFromSignal(signal),
1815
- tradeFlow: buildTradeFlowContextFromSignal(signal)
1816
- },
1817
- relative: {
1818
- marketBreadth: buildMarketBreadthContextFromSignal(signal),
1819
- marketBreadths: buildMarketBreadthsContextFromSignal(signal),
1820
- targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
1821
- btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
1822
- cmcGlobal: buildCmcGlobalContextFromSignal(signal),
1823
- cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
1824
- cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
1825
- cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
1826
- cmcIndexes: buildCmcIndexesContextFromSignal(signal),
1827
- referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
1828
- }
1829
- });
1830
-
1831
- // src/strategy/manifests.ts
1832
- var import_indicators = require("@tradejs/core/indicators");
1833
-
1834
- // src/tradejsConfig.ts
1835
- var import_fs = __toESM(require("fs"));
1836
- var import_path = __toESM(require("path"));
1837
- var import_url = require("url");
1838
- var import_config = require("@tradejs/core/config");
1839
- var import_logger2 = require("@tradejs/infra/logger");
1840
- var CONFIG_FILE_NAMES = [
1841
- "tradejs.config.ts",
1842
- "tradejs.config.mts",
1843
- "tradejs.config.js",
1844
- "tradejs.config.mjs",
1845
- "tradejs.config.cjs"
1846
- ];
1847
- var TS_MODULE_RE = /\.(cts|mts|ts)$/i;
1848
- var cachedByCwd = /* @__PURE__ */ new Map();
1849
- var announcedConfigFile = /* @__PURE__ */ new Set();
1850
- var tsNodeRegistered = false;
1851
- var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
1852
- var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
1853
- var getTradejsProjectCwd = (cwd) => {
1854
- const explicit = String(cwd ?? "").trim();
1855
- if (explicit) {
1856
- return import_path.default.resolve(explicit);
1857
- }
1858
- const fromEnv = String(process.env.PROJECT_CWD || "").trim();
1859
- if (fromEnv) {
1860
- return import_path.default.resolve(fromEnv);
1861
- }
1862
- return process.cwd();
1863
- };
1864
- var normalizeConfig = (rawConfig) => {
1865
- if (!rawConfig || typeof rawConfig !== "object") {
1866
- return {};
1867
- }
1868
- const config = rawConfig;
1869
- const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
1870
- const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
1871
- const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
1872
- const hooks = (0, import_config.normalizeTradejsConfigHooks)(
1873
- config.hooks
1874
- );
1875
- const runtime = config.runtime && typeof config.runtime === "object" && !Array.isArray(config.runtime) ? config.runtime : void 0;
1876
- return {
1877
- strategies: strategies2,
1878
- indicators,
1879
- connectors,
1880
- ...hooks ? { hooks } : {},
1881
- ...runtime ? { runtime } : {}
1882
- };
1883
- };
1884
- var getNodeCreateRequire = () => {
1885
- const builtinModule = process.getBuiltinModule?.("module");
1886
- if (typeof builtinModule?.createRequire === "function") {
1887
- return builtinModule.createRequire;
1888
- }
1889
- throw new TypeError("module.createRequire is not available");
1890
- };
1891
- var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
1892
- var ensureTsNodeRegistered = async () => {
1893
- if (tsNodeRegistered) {
1894
- return;
1895
- }
1896
- const tsNodeModule = await import("ts-node");
1897
- const tsNode = tsNodeModule.default ?? tsNodeModule;
1898
- tsNode.register?.({
1899
- transpileOnly: true,
1900
- compilerOptions: {
1901
- module: "Node16",
1902
- moduleResolution: "node16"
1903
- }
1904
- });
1905
- tsNodeRegistered = true;
1906
- };
1907
- var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
1908
- const projectRoot = getTradejsProjectCwd(cwd);
1909
- if (tsconfigPathsRegisteredByCwd.has(projectRoot)) {
1910
- return;
1911
- }
1912
- const tsconfigPathsModule = await import("tsconfig-paths");
1913
- const loadConfig = tsconfigPathsModule.loadConfig;
1914
- const register = tsconfigPathsModule.register;
1915
- if (typeof loadConfig !== "function" || typeof register !== "function") {
1916
- return;
1917
- }
1918
- const loadedConfig = loadConfig(projectRoot);
1919
- if (loadedConfig.resultType !== "success") {
1920
- return;
1921
- }
1922
- register({
1923
- baseUrl: loadedConfig.absoluteBaseUrl,
1924
- paths: loadedConfig.paths,
1925
- addMatchAll: false
1926
- });
1927
- tsconfigPathsRegisteredByCwd.add(projectRoot);
1928
- };
1929
- var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1930
- const projectRoot = getTradejsProjectCwd(cwd);
1931
- const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
1932
- if (cachedMatcher) {
1933
- const resolved2 = cachedMatcher(moduleName);
1934
- return resolved2 || null;
1935
- }
1936
- const tsconfigPathsModule = await import("tsconfig-paths");
1937
- const loadConfig = tsconfigPathsModule.loadConfig;
1938
- const createMatchPath = tsconfigPathsModule.createMatchPath;
1939
- if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
1940
- return null;
1941
- }
1942
- const loadedConfig = loadConfig(projectRoot);
1943
- if (loadedConfig.resultType !== "success") {
1944
- return null;
1945
- }
1946
- const matchPath = createMatchPath(
1947
- loadedConfig.absoluteBaseUrl,
1948
- loadedConfig.paths
1949
- );
1950
- const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
1951
- ".ts",
1952
- ".tsx",
1953
- ".mts",
1954
- ".cts",
1955
- ".js",
1956
- ".jsx",
1957
- ".mjs",
1958
- ".cjs",
1959
- ".json"
1960
- ]) || "";
1961
- tsconfigPathMatchersByCwd.set(projectRoot, matcher);
1962
- const resolved = matcher(moduleName);
1963
- return resolved || null;
1964
- };
1965
- var toImportSpecifier = (moduleName) => {
1966
- if (moduleName.startsWith("file://")) {
1967
- return moduleName;
1968
- }
1969
- if (import_path.default.isAbsolute(moduleName)) {
1970
- return (0, import_url.pathToFileURL)(moduleName).href;
1971
- }
1972
- return moduleName;
1973
- };
1974
- var isTsModulePath = (moduleName) => TS_MODULE_RE.test(moduleName.split("?")[0]);
1975
- var isRelativeModulePath = (moduleName) => moduleName.startsWith("./") || moduleName.startsWith("../");
1976
- var isBareModuleSpecifier = (moduleName) => {
1977
- const normalized = String(moduleName ?? "").trim();
1978
- if (!normalized) {
1979
- return false;
1569
+ var isTsModulePath = (moduleName) => TS_MODULE_RE.test(moduleName.split("?")[0]);
1570
+ var isRelativeModulePath = (moduleName) => moduleName.startsWith("./") || moduleName.startsWith("../");
1571
+ var isBareModuleSpecifier = (moduleName) => {
1572
+ const normalized = String(moduleName ?? "").trim();
1573
+ if (!normalized) {
1574
+ return false;
1980
1575
  }
1981
1576
  if (normalized.startsWith("file://") || import_path.default.isAbsolute(normalized) || isRelativeModulePath(normalized)) {
1982
1577
  return false;
@@ -2119,6 +1714,7 @@ var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
2119
1714
  };
2120
1715
 
2121
1716
  // src/strategy/manifests.ts
1717
+ var import_indicators = require("@tradejs/core/indicators");
2122
1718
  var SHARED_STRATEGY_REGISTRY_KEY = "__tradejsNodeSharedStrategyRegistryV1__";
2123
1719
  var sharedRegistryScope = globalThis;
2124
1720
  var sharedStrategyRegistry = sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] ?? (sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] = {
@@ -2394,60 +1990,1198 @@ var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
2394
1990
  }
2395
1991
  })();
2396
1992
  }
2397
- await state.pluginsLoadPromise;
1993
+ await state.pluginsLoadPromise;
1994
+ };
1995
+ var getStrategyCreator = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
1996
+ if (moduleName?.trim()) {
1997
+ await ensureStrategyPluginsLoaded(cwd);
1998
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).creator;
1999
+ }
2000
+ await ensureStrategyPluginsLoaded(cwd);
2001
+ const { state } = getStrategyRegistryState(cwd);
2002
+ return state.strategyCreators.get(name);
2003
+ };
2004
+ var getStrategyEntry = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2005
+ if (moduleName?.trim()) {
2006
+ await ensureStrategyPluginsLoaded(cwd);
2007
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).entry;
2008
+ }
2009
+ await ensureStrategyPluginsLoaded(cwd);
2010
+ const { state } = getStrategyRegistryState(cwd);
2011
+ return state.strategyEntriesMap.get(name);
2012
+ };
2013
+ var getStrategyPluginSource = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2014
+ if (moduleName?.trim()) {
2015
+ await ensureStrategyPluginsLoaded(cwd);
2016
+ return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).source;
2017
+ }
2018
+ await ensureStrategyPluginsLoaded(cwd);
2019
+ const { state } = getStrategyRegistryState(cwd);
2020
+ return state.strategySourcesMap.get(name);
2021
+ };
2022
+ var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
2023
+ if (!name) {
2024
+ return void 0;
2025
+ }
2026
+ const { state } = getStrategyRegistryState(cwd);
2027
+ return state.strategyManifestsMap.get(name);
2028
+ };
2029
+ var strategies = new Proxy(
2030
+ {},
2031
+ {
2032
+ get: (_target, property) => {
2033
+ if (typeof property !== "string") {
2034
+ return void 0;
2035
+ }
2036
+ return getStrategyRegistryState().state.strategyCreators.get(property);
2037
+ },
2038
+ ownKeys: () => {
2039
+ return [...getStrategyRegistryState().state.strategyCreators.keys()];
2040
+ },
2041
+ getOwnPropertyDescriptor: () => ({
2042
+ enumerable: true,
2043
+ configurable: true
2044
+ })
2045
+ }
2046
+ );
2047
+
2048
+ // src/strategyHelpers/tickerProfiles.ts
2049
+ var snapshots = /* @__PURE__ */ new Map();
2050
+ var resolveTickerProfileSource = (strategy) => {
2051
+ const declared = getStrategyManifest(strategy)?.contextRequirements?.tickerProfiles;
2052
+ if (declared) return declared;
2053
+ const provider = process.env.TICKER_PROFILES_PROVIDER?.trim();
2054
+ const universeId = process.env.TICKER_PROFILES_UNIVERSE?.trim();
2055
+ if (!provider && !universeId) return void 0;
2056
+ if (!provider || !universeId)
2057
+ throw new Error(
2058
+ "Set both TICKER_PROFILES_PROVIDER and TICKER_PROFILES_UNIVERSE"
2059
+ );
2060
+ return { provider, universeId };
2061
+ };
2062
+ var loadTickerProfileContext = async ({
2063
+ projectRoot,
2064
+ provider,
2065
+ universeId,
2066
+ symbol,
2067
+ timestamp
2068
+ }) => {
2069
+ const asOf = (0, import_tickerProfiles2.tickerProfileMonth)(timestamp), key = JSON.stringify([projectRoot, provider, universeId, asOf]);
2070
+ let cached = snapshots.get(key);
2071
+ if (!cached || cached.expires < Date.now()) {
2072
+ const value = (0, import_tickerProfiles.readTickerProfiles)(projectRoot, provider, universeId, asOf).then((envelope) => {
2073
+ if (!envelope) return null;
2074
+ const s = envelope.snapshot;
2075
+ if (!(0, import_tickerProfiles2.isTickerProfileSnapshot)(s) || s.asOf !== asOf || s.provider !== provider || s.universeId !== universeId)
2076
+ throw new Error("Ticker profile identity/schema mismatch");
2077
+ const entry = snapshots.get(key);
2078
+ if (entry) entry.expires = Infinity;
2079
+ return new Map(
2080
+ s.profiles.map((p) => [
2081
+ p.symbol,
2082
+ Object.freeze({
2083
+ ...p,
2084
+ definition: s.definition,
2085
+ provider,
2086
+ universeId,
2087
+ snapshotId: envelope.snapshotId
2088
+ })
2089
+ ])
2090
+ );
2091
+ }).catch((error) => {
2092
+ snapshots.delete(key);
2093
+ throw error;
2094
+ });
2095
+ cached = { expires: Date.now() + 6e4, value };
2096
+ snapshots.set(key, cached);
2097
+ if (snapshots.size > 24) {
2098
+ const oldest = snapshots.keys().next().value;
2099
+ if (oldest) snapshots.delete(oldest);
2100
+ }
2101
+ }
2102
+ return (await cached.value)?.get(symbol);
2103
+ };
2104
+ var withTickerProfile = (baseContext, profile) => Object.defineProperty(
2105
+ Object.create(
2106
+ Object.getPrototypeOf(baseContext),
2107
+ Object.getOwnPropertyDescriptors(baseContext)
2108
+ ),
2109
+ "tickerProfile",
2110
+ { enumerable: true, configurable: true, value: profile }
2111
+ );
2112
+ var enrichSignalWithTickerProfile = async (signal) => {
2113
+ const source = resolveTickerProfileSource(signal.strategy);
2114
+ if (!source || signal.universe === "tradfi" || !signal.symbol.endsWith("USDT"))
2115
+ return;
2116
+ const profile = await loadTickerProfileContext({
2117
+ ...source,
2118
+ projectRoot: getTradejsProjectCwd(),
2119
+ symbol: signal.symbol,
2120
+ timestamp: signal.timestamp
2121
+ });
2122
+ if (profile) {
2123
+ signal.additionalIndicators = {
2124
+ ...signal.additionalIndicators,
2125
+ tickerProfile: profile
2126
+ };
2127
+ }
2128
+ };
2129
+
2130
+ // src/strategyRuntime.ts
2131
+ var import_jev6 = require("@tradejs/core/jev");
2132
+
2133
+ // src/jev.ts
2134
+ var import_promises = __toESM(require("fs/promises"));
2135
+ var import_node_path = __toESM(require("path"));
2136
+ var import_jev4 = require("@tradejs/core/jev");
2137
+ var import_jev5 = require("@tradejs/infra/jev");
2138
+ var import_userSettings = require("@tradejs/infra/userSettings");
2139
+ init_jevInput();
2140
+ init_jevInput();
2141
+ init_jevTraining();
2142
+ var resolveJevProvider = async (userName) => {
2143
+ const settings = await (0, import_userSettings.getUserSettings)(userName);
2144
+ const provider = {
2145
+ endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
2146
+ model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
2147
+ };
2148
+ (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
2149
+ if (!settings.JEV_API_KEY)
2150
+ throw new Error("Configure the Jev API key in Account settings");
2151
+ return provider;
2152
+ };
2153
+ var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
2154
+ var pending = /* @__PURE__ */ new Map();
2155
+ var pendingResponses = /* @__PURE__ */ new Map();
2156
+ var evaluateJevInput = async ({
2157
+ input,
2158
+ config,
2159
+ userName,
2160
+ projectRoot
2161
+ }) => {
2162
+ config = (0, import_jev4.parseJevConfig)(config);
2163
+ const inputHash = (0, import_jev5.jevHash)(input);
2164
+ const questions = questionsForJevInput(input);
2165
+ const questionsHash = (0, import_jev5.jevHash)(questions);
2166
+ const base = {
2167
+ questionSet: import_jev4.JEV_QUESTION_SET,
2168
+ schema: "tradejs-signal-assessment/v5",
2169
+ source: config.source,
2170
+ mode: config.mode,
2171
+ inputHash,
2172
+ questionsHash,
2173
+ geometryStatus: input.geometryStatus,
2174
+ levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
2175
+ };
2176
+ if (!input.questions.length)
2177
+ return {
2178
+ assessment: {
2179
+ ...base,
2180
+ status: "unavailable",
2181
+ model: "",
2182
+ scores: emptyScores(),
2183
+ confidence: emptyScores()
2184
+ }
2185
+ };
2186
+ if (config.source === "local") {
2187
+ const contents = await import_promises.default.readFile(
2188
+ import_node_path.default.resolve(projectRoot, config.modelFile),
2189
+ "utf8"
2190
+ );
2191
+ if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
2192
+ throw new Error("Local Jev gate checksum mismatch");
2193
+ const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
2194
+ const model = validateJevGateModel2(JSON.parse(contents));
2195
+ if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
2196
+ throw new Error("Local Jev gate strategy or question version mismatch");
2197
+ const scores = Object.fromEntries(
2198
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2199
+ key,
2200
+ input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
2201
+ ])
2202
+ );
2203
+ return {
2204
+ assessment: {
2205
+ ...base,
2206
+ status: "available",
2207
+ model: config.modelSha256,
2208
+ scores,
2209
+ confidence: emptyScores()
2210
+ }
2211
+ };
2212
+ }
2213
+ const provider = config.provider ?? await resolveJevProvider(userName);
2214
+ const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
2215
+ const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
2216
+ const file = import_node_path.default.join(dir, "records", `${id}.json`);
2217
+ const getRecord = async () => {
2218
+ const existing = await (0, import_jev5.readJevArtifact)(file);
2219
+ if (existing) {
2220
+ 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))
2221
+ throw new Error("Jev recording identity mismatch");
2222
+ (0, import_jev5.validateJevResponse)(existing.response, input.questions);
2223
+ for (const key of import_jev4.JEV_DIMENSIONS) {
2224
+ const expected = input.questions.includes(key) ? existing.response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null;
2225
+ if (existing.scores[key] !== expected)
2226
+ throw new Error("Jev recording scores do not match the response");
2227
+ }
2228
+ return existing;
2229
+ }
2230
+ if (config.source === "recorded")
2231
+ throw new Error(
2232
+ `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.`
2233
+ );
2234
+ const settings = await (0, import_userSettings.getUserSettings)(userName);
2235
+ if (!settings.JEV_API_KEY)
2236
+ throw new Error("Configure the Jev API key in Account settings");
2237
+ const apiKey = settings.JEV_API_KEY;
2238
+ const configuredProvider = await resolveJevProvider(userName);
2239
+ if (configuredProvider.endpoint !== provider.endpoint)
2240
+ throw new Error(
2241
+ "Jev account endpoint changed; update the frozen provider configuration"
2242
+ );
2243
+ const started = Date.now();
2244
+ const state = stateForJevInput(input);
2245
+ const responseId = (0, import_jev5.jevHash)({
2246
+ state,
2247
+ questionsHash,
2248
+ provider,
2249
+ user: (0, import_jev5.jevHash)(userName)
2250
+ });
2251
+ const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
2252
+ let responsePromise = pendingResponses.get(responseFile);
2253
+ if (!responsePromise) {
2254
+ responsePromise = (async () => {
2255
+ const saved = await (0, import_jev5.readJevArtifact)(responseFile);
2256
+ if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
2257
+ const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
2258
+ await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
2259
+ return fresh;
2260
+ })();
2261
+ pendingResponses.set(responseFile, responsePromise);
2262
+ }
2263
+ let response;
2264
+ try {
2265
+ response = await responsePromise;
2266
+ } finally {
2267
+ pendingResponses.delete(responseFile);
2268
+ }
2269
+ const scores = Object.fromEntries(
2270
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2271
+ key,
2272
+ input.questions.includes(key) ? response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null
2273
+ ])
2274
+ );
2275
+ const value = {
2276
+ schema: "tradejs-jev-record/v4",
2277
+ id,
2278
+ inputHash,
2279
+ questionsHash,
2280
+ provider,
2281
+ input,
2282
+ response,
2283
+ scores,
2284
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2285
+ elapsedMs: Date.now() - started
2286
+ };
2287
+ await (0, import_jev5.writeJevArtifact)(file, value);
2288
+ return await (0, import_jev5.readJevArtifact)(file);
2289
+ };
2290
+ const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
2291
+ let promise = pending.get(pendingKey);
2292
+ if (!promise) {
2293
+ promise = getRecord();
2294
+ pending.set(pendingKey, promise);
2295
+ }
2296
+ let record;
2297
+ try {
2298
+ record = await promise;
2299
+ } finally {
2300
+ pending.delete(pendingKey);
2301
+ }
2302
+ return {
2303
+ record,
2304
+ assessment: {
2305
+ ...base,
2306
+ status: "available",
2307
+ recordId: id,
2308
+ model: record.response.model,
2309
+ scores: record.scores,
2310
+ confidence: Object.fromEntries(
2311
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2312
+ key,
2313
+ record.response.answers[key]?.confidence ?? null
2314
+ ])
2315
+ )
2316
+ }
2317
+ };
2318
+ };
2319
+ var assessSignalWithJev = async ({
2320
+ signal,
2321
+ config,
2322
+ userName,
2323
+ projectRoot,
2324
+ strict,
2325
+ scope = "runtime"
2326
+ }) => {
2327
+ let assessment;
2328
+ try {
2329
+ const input = buildJevInput(signal);
2330
+ ({ assessment } = await evaluateJevInput({
2331
+ input,
2332
+ config,
2333
+ userName,
2334
+ projectRoot
2335
+ }));
2336
+ } catch (error) {
2337
+ if (strict) throw error;
2338
+ assessment = {
2339
+ questionSet: import_jev4.JEV_QUESTION_SET,
2340
+ schema: "tradejs-signal-assessment/v5",
2341
+ source: config.source,
2342
+ mode: config.mode,
2343
+ status: "unavailable",
2344
+ inputHash: "",
2345
+ questionsHash: "",
2346
+ geometryStatus: "absent",
2347
+ levelsValid: null,
2348
+ model: config.provider?.model ?? config.modelSha256 ?? "",
2349
+ scores: emptyScores(),
2350
+ confidence: emptyScores()
2351
+ };
2352
+ }
2353
+ signal.assessment = assessment;
2354
+ signal.additionalIndicators = {
2355
+ ...signal.additionalIndicators,
2356
+ jev: (0, import_jev4.jevAssessmentFeatures)(assessment)
2357
+ };
2358
+ const identity = {
2359
+ signalId: signal.signalId,
2360
+ strategy: signal.strategy,
2361
+ symbol: signal.symbol,
2362
+ timestamp: signal.timestamp,
2363
+ scope,
2364
+ user: (0, import_jev5.jevHash)(userName),
2365
+ config,
2366
+ assessment
2367
+ };
2368
+ await (0, import_jev5.writeJevArtifact)(
2369
+ import_node_path.default.resolve(
2370
+ projectRoot,
2371
+ config.recordsDir ?? "data/ai/jev",
2372
+ "decisions",
2373
+ `${(0, import_jev5.jevHash)(identity)}.json`
2374
+ ),
2375
+ identity
2376
+ );
2377
+ return assessment;
2378
+ };
2379
+
2380
+ // src/strategyRuntime.ts
2381
+ var import_constants5 = require("@tradejs/core/constants");
2382
+ var import_strategies6 = require("@tradejs/core/strategies");
2383
+ var import_logger11 = require("@tradejs/infra/logger");
2384
+
2385
+ // src/strategyHelpers/runtime.ts
2386
+ var import_logger9 = require("@tradejs/infra/logger");
2387
+ var import_constants3 = require("@tradejs/core/constants");
2388
+ var import_ml2 = require("@tradejs/infra/ml");
2389
+
2390
+ // src/strategy/entrySubmissionValidation.ts
2391
+ var import_logger3 = require("@tradejs/infra/logger");
2392
+
2393
+ // src/strategy/entryExecutionValidation.ts
2394
+ var EntrySubmissionValidationError = class extends Error {
2395
+ constructor() {
2396
+ super(...arguments);
2397
+ this.name = "EntrySubmissionValidationError";
2398
+ }
2399
+ };
2400
+ var resolveEntryExecutionLimits = (config, direction) => {
2401
+ const side = config[direction];
2402
+ const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
2403
+ const maximum = config.MAX_LOSS_VALUE;
2404
+ if (minimum == null && maximum == null) return void 0;
2405
+ const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
2406
+ const maxLossValue = maximum == null ? void 0 : Number(maximum);
2407
+ const feeRate = Number(config.RISK_FEE_RATE ?? 0);
2408
+ const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
2409
+ const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
2410
+ const slippageBps = slippage + impact;
2411
+ 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)
2412
+ throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
2413
+ return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
2414
+ };
2415
+ var validateEntryExecution = ({
2416
+ direction,
2417
+ price,
2418
+ qty,
2419
+ stopLossPrice,
2420
+ takeProfitPrice,
2421
+ limits
2422
+ }) => {
2423
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
2424
+ throw new EntrySubmissionValidationError(
2425
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2426
+ );
2427
+ const side = direction === "LONG" ? 1 : -1;
2428
+ if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
2429
+ throw new EntrySubmissionValidationError(
2430
+ "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
2431
+ );
2432
+ const slippage = limits.slippageBps / 1e4;
2433
+ const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
2434
+ const entryCost = cost(price, side);
2435
+ const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
2436
+ const rewardPerUnit = Math.max(
2437
+ 0,
2438
+ Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
2439
+ );
2440
+ const netRiskRatio = rewardPerUnit / lossPerUnit;
2441
+ const plannedLossValue = qty * lossPerUnit;
2442
+ if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
2443
+ throw new EntrySubmissionValidationError(
2444
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2445
+ );
2446
+ const belowMinimumNetRiskRatio = limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio;
2447
+ const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
2448
+ return {
2449
+ netRiskRatio,
2450
+ plannedLossValue,
2451
+ riskExceeded,
2452
+ belowMinimumNetRiskRatio
2453
+ };
2454
+ };
2455
+
2456
+ // src/strategy/entrySubmissionValidation.ts
2457
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
2458
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
2459
+ var getOrderArrivalSnapshot = async ({
2460
+ connector,
2461
+ symbol,
2462
+ requireTimestamp = false
2463
+ }) => {
2464
+ if (typeof connector.getTopOfBookTicker !== "function") {
2465
+ return {
2466
+ arrivalSnapshotTime: Date.now(),
2467
+ arrivalSource: "unavailable",
2468
+ bid: null,
2469
+ ask: null,
2470
+ arrivalMid: null,
2471
+ spreadBps: null
2472
+ };
2473
+ }
2474
+ try {
2475
+ const ticker = await connector.getTopOfBookTicker(symbol);
2476
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
2477
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
2478
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
2479
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
2480
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
2481
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
2482
+ return {
2483
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
2484
+ arrivalSource: "top_of_book",
2485
+ bid,
2486
+ ask,
2487
+ arrivalMid,
2488
+ spreadBps
2489
+ };
2490
+ } catch (error) {
2491
+ import_logger3.logger.warn(
2492
+ "runtime order arrival snapshot failed: %s %s",
2493
+ symbol,
2494
+ error?.message || String(error)
2495
+ );
2496
+ return {
2497
+ arrivalSnapshotTime: Date.now(),
2498
+ arrivalSource: "top_of_book_error",
2499
+ bid: null,
2500
+ ask: null,
2501
+ arrivalMid: null,
2502
+ spreadBps: null
2503
+ };
2504
+ }
2505
+ };
2506
+ var validateEntryProtectionAtArrival = ({
2507
+ direction,
2508
+ signalPrice,
2509
+ bid,
2510
+ ask,
2511
+ arrivalMid,
2512
+ takeProfits,
2513
+ stopLossPrice
2514
+ }) => {
2515
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
2516
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
2517
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
2518
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
2519
+ }
2520
+ for (const takeProfit of takeProfits) {
2521
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
2522
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
2523
+ }
2524
+ }
2525
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
2526
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
2527
+ }
2528
+ };
2529
+ var createEntrySubmissionValidation = ({
2530
+ connector,
2531
+ symbol,
2532
+ direction,
2533
+ takeProfits,
2534
+ stopLossPrice,
2535
+ limits,
2536
+ onValidated
2537
+ }) => {
2538
+ if (!limits) return void 0;
2539
+ if (!connector.capabilities?.orderSubmissionValidation)
2540
+ throw new EntrySubmissionValidationError(
2541
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
2542
+ );
2543
+ return async ({ qty, executionPrice }) => {
2544
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
2545
+ const snapshot2 = simulated && executionPrice != null ? {
2546
+ arrivalSnapshotTime: Date.now(),
2547
+ arrivalSource: "simulation",
2548
+ bid: executionPrice,
2549
+ ask: executionPrice,
2550
+ arrivalMid: executionPrice,
2551
+ spreadBps: 0
2552
+ } : await getOrderArrivalSnapshot({
2553
+ connector,
2554
+ symbol,
2555
+ requireTimestamp: true
2556
+ });
2557
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
2558
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
2559
+ throw new EntrySubmissionValidationError(
2560
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
2561
+ );
2562
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
2563
+ throw new EntrySubmissionValidationError(
2564
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
2565
+ );
2566
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
2567
+ validateEntryProtectionAtArrival({
2568
+ direction,
2569
+ signalPrice: price,
2570
+ bid: snapshot2.bid,
2571
+ ask: snapshot2.ask,
2572
+ arrivalMid: snapshot2.arrivalMid,
2573
+ takeProfits,
2574
+ stopLossPrice
2575
+ });
2576
+ const prices = takeProfits.map((tp) => tp.price);
2577
+ const economics2 = validateEntryExecution({
2578
+ direction,
2579
+ price,
2580
+ qty,
2581
+ stopLossPrice,
2582
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
2583
+ limits
2584
+ });
2585
+ if (economics2.belowMinimumNetRiskRatio) {
2586
+ import_logger3.logger.warn(
2587
+ "entry submission warning: %s %s ENTRY_RR_BELOW_MIN_AT_SUBMISSION (RR %s, threshold %s)",
2588
+ symbol,
2589
+ direction,
2590
+ economics2.netRiskRatio,
2591
+ limits.minimumNetRiskRatio
2592
+ );
2593
+ }
2594
+ onValidated?.(snapshot2, economics2);
2595
+ };
2596
+ };
2597
+
2598
+ // src/ai.ts
2599
+ var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
2600
+ var import_redis3 = require("@tradejs/infra/redis");
2601
+
2602
+ // src/strategyAdapters/ai.ts
2603
+ var import_indicators2 = require("@tradejs/core/indicators");
2604
+ var import_aiFeatures = require("@tradejs/core/aiFeatures");
2605
+
2606
+ // src/aiShared.ts
2607
+ var MAX_AI_SERIES_POINTS = 5;
2608
+ var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
2609
+ "tradejs.indicators.compactSnapshot"
2610
+ );
2611
+ var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
2612
+ var trimSeriesDeep = (value) => {
2613
+ if (Array.isArray(value)) {
2614
+ const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
2615
+ const isMatrix = trimmed.every((item) => Array.isArray(item));
2616
+ if (isMatrix) {
2617
+ return trimmed;
2618
+ }
2619
+ return trimmed.map(
2620
+ (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
2621
+ );
2622
+ }
2623
+ if (value && typeof value === "object") {
2624
+ return Object.fromEntries(
2625
+ Object.entries(value).map(([key, nested]) => [
2626
+ key,
2627
+ trimSeriesDeep(nested)
2628
+ ])
2629
+ );
2630
+ }
2631
+ return value;
2632
+ };
2633
+ var buildCompactAiIndicatorsSnapshot = (value) => {
2634
+ const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
2635
+ if (typeof compactSnapshot === "function") {
2636
+ return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
2637
+ }
2638
+ return trimSeriesDeep(value);
2639
+ };
2640
+
2641
+ // src/aiMarketContext.ts
2642
+ var toRecord = (value) => {
2643
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2644
+ return null;
2645
+ }
2646
+ return value;
2647
+ };
2648
+ var toFiniteNumber = (value) => {
2649
+ const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
2650
+ return Number.isFinite(numeric) ? numeric : null;
2651
+ };
2652
+ var roundTo = (value, decimals) => {
2653
+ const factor = 10 ** decimals;
2654
+ return Math.round(value * factor) / factor;
2655
+ };
2656
+ var buildMissingSpreadContext = () => ({
2657
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2658
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2659
+ available: false,
2660
+ value: null,
2661
+ zScore: null,
2662
+ bps: null,
2663
+ absBps: null,
2664
+ bias: null,
2665
+ severity: null
2666
+ });
2667
+ var buildSpreadContextFromSignal = (signal) => {
2668
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2669
+ const relative = toRecord(baseContext?.relative);
2670
+ const execution = toRecord(relative?.execution);
2671
+ const spread = toFiniteNumber(execution?.venueSpread);
2672
+ const zScore = toFiniteNumber(execution?.venueSpreadZScore);
2673
+ if (spread == null) {
2674
+ return buildMissingSpreadContext();
2675
+ }
2676
+ const value = roundTo(spread, 8);
2677
+ const bps = roundTo(value * 1e4, 2);
2678
+ const absBps = Math.abs(bps);
2679
+ const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
2680
+ const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
2681
+ return {
2682
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2683
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2684
+ available: true,
2685
+ value,
2686
+ zScore,
2687
+ bps,
2688
+ absBps,
2689
+ bias,
2690
+ severity
2691
+ };
2692
+ };
2693
+ var buildTrueDeltaContextFromSignal = (signal) => {
2694
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2695
+ const participation = toRecord(baseContext?.participation);
2696
+ const delta = toRecord(participation?.delta);
2697
+ const source = String(delta?.source ?? "");
2698
+ const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
2699
+ if (!delta || !isTrueDeltaSource) {
2700
+ return {
2701
+ source: source || null,
2702
+ available: false,
2703
+ buyPressurePct: null,
2704
+ buyVolume: null,
2705
+ sellVolume: null,
2706
+ netDelta: null,
2707
+ deltaPct: null,
2708
+ signedVolumeZScore: null
2709
+ };
2710
+ }
2711
+ return {
2712
+ source,
2713
+ available: true,
2714
+ buyPressurePct: toFiniteNumber(delta.buyPressurePct),
2715
+ buyVolume: toFiniteNumber(delta.buyVolume),
2716
+ sellVolume: toFiniteNumber(delta.sellVolume),
2717
+ netDelta: toFiniteNumber(delta.netDelta),
2718
+ deltaPct: toFiniteNumber(delta.deltaPct),
2719
+ signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
2720
+ };
2721
+ };
2722
+ var buildTradeFlowContextFromSignal = (signal) => {
2723
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2724
+ const participation = toRecord(baseContext?.participation);
2725
+ const tradeFlow = toRecord(participation?.tradeFlow);
2726
+ if (!tradeFlow) {
2727
+ return {
2728
+ source: null,
2729
+ available: false,
2730
+ interval: null,
2731
+ stale: null,
2732
+ trades: null,
2733
+ buyPressurePct: null,
2734
+ netBaseDelta: null,
2735
+ netQuoteDelta: null
2736
+ };
2737
+ }
2738
+ return {
2739
+ source: String(tradeFlow.source ?? ""),
2740
+ available: true,
2741
+ interval: String(tradeFlow.interval ?? ""),
2742
+ stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
2743
+ trades: toFiniteNumber(tradeFlow.trades),
2744
+ buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
2745
+ netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
2746
+ netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
2747
+ };
2748
+ };
2749
+ var buildMarketBreadthContextFromSignal = (signal) => {
2750
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2751
+ const relative = toRecord(baseContext?.relative);
2752
+ const breadth = toRecord(relative?.marketBreadth);
2753
+ if (!breadth) {
2754
+ return {
2755
+ source: null,
2756
+ available: false,
2757
+ universe: null,
2758
+ interval: null,
2759
+ stale: null,
2760
+ symbolsCount: null,
2761
+ advanceDeclineRatio: null,
2762
+ pctAboveMa20: null,
2763
+ pctAboveMa50: null,
2764
+ equalWeightedReturn: null,
2765
+ volumeWeightedReturn: null,
2766
+ dispersion: null
2767
+ };
2768
+ }
2769
+ return {
2770
+ source: String(breadth.source ?? ""),
2771
+ available: true,
2772
+ universe: String(breadth.universe ?? ""),
2773
+ interval: String(breadth.interval ?? ""),
2774
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
2775
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
2776
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
2777
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
2778
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
2779
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
2780
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
2781
+ dispersion: toFiniteNumber(breadth.dispersion)
2782
+ };
2783
+ };
2784
+ var buildMarketBreadthsContextFromSignal = (signal) => {
2785
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2786
+ const relative = toRecord(baseContext?.relative);
2787
+ const breadths = toRecord(relative?.marketBreadths);
2788
+ const build = (key) => {
2789
+ const breadth = toRecord(breadths?.[key]);
2790
+ if (!breadth) {
2791
+ return {
2792
+ source: null,
2793
+ available: false,
2794
+ universe: null,
2795
+ interval: null,
2796
+ stale: null,
2797
+ symbolsCount: null,
2798
+ advanceDeclineRatio: null,
2799
+ pctAboveMa20: null,
2800
+ pctAboveMa50: null,
2801
+ equalWeightedReturn: null,
2802
+ volumeWeightedReturn: null,
2803
+ dispersion: null
2804
+ };
2805
+ }
2806
+ return {
2807
+ source: String(breadth.source ?? ""),
2808
+ available: true,
2809
+ universe: String(breadth.universe ?? ""),
2810
+ interval: String(breadth.interval ?? ""),
2811
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
2812
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
2813
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
2814
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
2815
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
2816
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
2817
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
2818
+ dispersion: toFiniteNumber(breadth.dispersion)
2819
+ };
2820
+ };
2821
+ return {
2822
+ top5: build("top5"),
2823
+ top10: build("top10"),
2824
+ top30: build("top30"),
2825
+ top50: build("top50"),
2826
+ top100: build("top100")
2827
+ };
2828
+ };
2829
+ var buildTargetVsReference = (signal, asset) => {
2830
+ const base = toRecord(signal.additionalIndicators?.baseContext);
2831
+ const relative = toRecord(base?.relative);
2832
+ const target = toRecord(relative?.[`targetVs${asset}`]);
2833
+ return {
2834
+ source: target ? String(target.source ?? "") : null,
2835
+ available: Boolean(target),
2836
+ ratioReturn1h: toFiniteNumber(target?.ratioReturn1h),
2837
+ ratioReturn4h: toFiniteNumber(target?.ratioReturn4h),
2838
+ ratioReturn24h: toFiniteNumber(target?.ratioReturn24h),
2839
+ alpha1h: toFiniteNumber(target?.[`alphaVs${asset}1h`]),
2840
+ alpha4h: toFiniteNumber(target?.[`alphaVs${asset}4h`]),
2841
+ alpha24h: toFiniteNumber(target?.[`alphaVs${asset}24h`]),
2842
+ beta: toFiniteNumber(target?.[`betaTo${asset}20`]),
2843
+ correlation: toFiniteNumber(target?.[`correlationTo${asset}20`]),
2844
+ ratioTrend: typeof target?.ratioTrend === "string" ? target.ratioTrend : null
2845
+ };
2846
+ };
2847
+ var buildTargetVsBtcContextFromSignal = (signal) => {
2848
+ const { alpha1h, alpha4h, alpha24h, beta, correlation, ...shared } = buildTargetVsReference(signal, "Btc");
2849
+ return {
2850
+ ...shared,
2851
+ alphaVsBtc1h: alpha1h,
2852
+ alphaVsBtc4h: alpha4h,
2853
+ alphaVsBtc24h: alpha24h,
2854
+ betaToBtc20: beta,
2855
+ correlationToBtc20: correlation
2856
+ };
2857
+ };
2858
+ var buildTargetVsEthContextFromSignal = (signal) => {
2859
+ const { alpha1h, alpha4h, alpha24h, beta, correlation, ...shared } = buildTargetVsReference(signal, "Eth");
2860
+ return {
2861
+ ...shared,
2862
+ alphaVsEth1h: alpha1h,
2863
+ alphaVsEth4h: alpha4h,
2864
+ alphaVsEth24h: alpha24h,
2865
+ betaToEth20: beta,
2866
+ correlationToEth20: correlation
2867
+ };
2868
+ };
2869
+ var buildBtcAltRegimeContextFromSignal = (signal) => {
2870
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2871
+ const relative = toRecord(baseContext?.relative);
2872
+ const btcAltRegime = toRecord(relative?.btcAltRegime);
2873
+ if (!btcAltRegime) {
2874
+ return {
2875
+ source: null,
2876
+ available: false,
2877
+ universe: null,
2878
+ interval: null,
2879
+ stale: null,
2880
+ regime: null,
2881
+ btcReturn24h: null,
2882
+ altBasketReturn24h: null,
2883
+ btcVsAltReturn24h: null,
2884
+ btcTurnoverShare24h: null,
2885
+ btcTurnoverShareChange24h: null,
2886
+ altVolToBtcVol24h: null,
2887
+ altDispersion24h: null
2888
+ };
2889
+ }
2890
+ return {
2891
+ source: String(btcAltRegime.source ?? ""),
2892
+ available: true,
2893
+ universe: String(btcAltRegime.universe ?? ""),
2894
+ interval: String(btcAltRegime.interval ?? ""),
2895
+ stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
2896
+ regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
2897
+ btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
2898
+ altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
2899
+ btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
2900
+ btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
2901
+ btcTurnoverShareChange24h: toFiniteNumber(
2902
+ btcAltRegime.btcTurnoverShareChange24h
2903
+ ),
2904
+ altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
2905
+ altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
2906
+ };
2907
+ };
2908
+ var buildCmcGlobalContextFromSignal = (signal) => {
2909
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2910
+ const relative = toRecord(baseContext?.relative);
2911
+ const cmcGlobal = toRecord(relative?.cmcGlobal);
2912
+ if (!cmcGlobal) {
2913
+ return {
2914
+ source: null,
2915
+ available: false,
2916
+ interval: null,
2917
+ asOfTs: null,
2918
+ stale: null,
2919
+ totalMarketCapUsd: null,
2920
+ totalVolumeUsd: null,
2921
+ totalVolumeReportedUsd: null,
2922
+ altMarketCapUsd: null,
2923
+ altVolumeUsd: null,
2924
+ altVolumeReportedUsd: null,
2925
+ btcDominancePct: null,
2926
+ ethDominancePct: null,
2927
+ btcDominanceChange24hPct: null,
2928
+ ethDominanceChange24hPct: null,
2929
+ altMarketCapChange24hPct: null,
2930
+ altVolumeChange24hPct: null,
2931
+ activeCryptocurrencies: null,
2932
+ activeExchanges: null,
2933
+ activeMarketPairs: null,
2934
+ altLiquidityRegime: null
2935
+ };
2936
+ }
2937
+ return {
2938
+ source: String(cmcGlobal.source ?? ""),
2939
+ available: true,
2940
+ interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
2941
+ asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
2942
+ stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
2943
+ totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
2944
+ totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
2945
+ totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
2946
+ altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
2947
+ altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
2948
+ altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
2949
+ btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
2950
+ ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
2951
+ btcDominanceChange24hPct: toFiniteNumber(
2952
+ cmcGlobal.btcDominanceChange24hPct
2953
+ ),
2954
+ ethDominanceChange24hPct: toFiniteNumber(
2955
+ cmcGlobal.ethDominanceChange24hPct
2956
+ ),
2957
+ altMarketCapChange24hPct: toFiniteNumber(
2958
+ cmcGlobal.altMarketCapChange24hPct
2959
+ ),
2960
+ altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
2961
+ activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
2962
+ activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
2963
+ activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
2964
+ altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
2965
+ };
2966
+ };
2967
+ var buildCmcReferenceAssetsContextFromSignal = (signal) => {
2968
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2969
+ const relative = toRecord(baseContext?.relative);
2970
+ const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
2971
+ if (!cmcReferenceAssets) {
2972
+ return {
2973
+ source: null,
2974
+ available: false,
2975
+ interval: null,
2976
+ asOfTs: null,
2977
+ stale: null,
2978
+ btcMarketCapUsd: null,
2979
+ ethMarketCapUsd: null,
2980
+ btcVolumeUsd: null,
2981
+ ethVolumeUsd: null,
2982
+ btcVolumeToMarketCap: null,
2983
+ ethVolumeToMarketCap: null,
2984
+ ethBtcMarketCapRatio: null,
2985
+ ethBtcMarketCapRatioChange24hPct: null,
2986
+ ethVsBtcVolumeRatio: null,
2987
+ referenceLiquidityRegime: null
2988
+ };
2989
+ }
2990
+ return {
2991
+ source: String(cmcReferenceAssets.source ?? ""),
2992
+ available: true,
2993
+ interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
2994
+ asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
2995
+ stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
2996
+ btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
2997
+ ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
2998
+ btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
2999
+ ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
3000
+ btcVolumeToMarketCap: toFiniteNumber(
3001
+ cmcReferenceAssets.btcVolumeToMarketCap
3002
+ ),
3003
+ ethVolumeToMarketCap: toFiniteNumber(
3004
+ cmcReferenceAssets.ethVolumeToMarketCap
3005
+ ),
3006
+ ethBtcMarketCapRatio: toFiniteNumber(
3007
+ cmcReferenceAssets.ethBtcMarketCapRatio
3008
+ ),
3009
+ ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
3010
+ cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
3011
+ ),
3012
+ ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
3013
+ referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
3014
+ };
2398
3015
  };
2399
- var getStrategyCreator = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2400
- if (moduleName?.trim()) {
2401
- await ensureStrategyPluginsLoaded(cwd);
2402
- return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).creator;
3016
+ var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
3017
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3018
+ const relative = toRecord(baseContext?.relative);
3019
+ const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
3020
+ if (!cmcExchangeLiquidity) {
3021
+ return {
3022
+ source: null,
3023
+ available: false,
3024
+ interval: null,
3025
+ asOfTs: null,
3026
+ stale: null,
3027
+ exchangesCount: null,
3028
+ totalVolumeUsd: null,
3029
+ totalVolumeChange24hPct: null,
3030
+ binanceVolumeUsd: null,
3031
+ binanceVolumeShare: null,
3032
+ topExchangeVolumeShare: null,
3033
+ liquidityRegime: null
3034
+ };
2403
3035
  }
2404
- await ensureStrategyPluginsLoaded(cwd);
2405
- const { state } = getStrategyRegistryState(cwd);
2406
- return state.strategyCreators.get(name);
3036
+ return {
3037
+ source: String(cmcExchangeLiquidity.source ?? ""),
3038
+ available: true,
3039
+ interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
3040
+ asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
3041
+ stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
3042
+ exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
3043
+ totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
3044
+ totalVolumeChange24hPct: toFiniteNumber(
3045
+ cmcExchangeLiquidity.totalVolumeChange24hPct
3046
+ ),
3047
+ binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
3048
+ binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
3049
+ topExchangeVolumeShare: toFiniteNumber(
3050
+ cmcExchangeLiquidity.topExchangeVolumeShare
3051
+ ),
3052
+ liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
3053
+ };
2407
3054
  };
2408
- var getStrategyEntry = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2409
- if (moduleName?.trim()) {
2410
- await ensureStrategyPluginsLoaded(cwd);
2411
- return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).entry;
3055
+ var buildCmcFearGreedContextFromSignal = (signal) => {
3056
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3057
+ const relative = toRecord(baseContext?.relative);
3058
+ const cmcFearGreed = toRecord(relative?.cmcFearGreed);
3059
+ if (!cmcFearGreed) {
3060
+ return {
3061
+ source: null,
3062
+ available: false,
3063
+ interval: null,
3064
+ asOfTs: null,
3065
+ stale: null,
3066
+ value: null,
3067
+ valueChange24h: null,
3068
+ valueChange7d: null,
3069
+ classification: null,
3070
+ sentimentRegime: null
3071
+ };
2412
3072
  }
2413
- await ensureStrategyPluginsLoaded(cwd);
2414
- const { state } = getStrategyRegistryState(cwd);
2415
- return state.strategyEntriesMap.get(name);
3073
+ return {
3074
+ source: String(cmcFearGreed.source ?? ""),
3075
+ available: true,
3076
+ interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
3077
+ asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
3078
+ stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
3079
+ value: toFiniteNumber(cmcFearGreed.value),
3080
+ valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
3081
+ valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
3082
+ classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
3083
+ sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
3084
+ };
2416
3085
  };
2417
- var getStrategyPluginSource = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2418
- if (moduleName?.trim()) {
2419
- await ensureStrategyPluginsLoaded(cwd);
2420
- return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).source;
3086
+ var buildCmcIndexesContextFromSignal = (signal) => {
3087
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3088
+ const relative = toRecord(baseContext?.relative);
3089
+ const cmcIndexes = toRecord(relative?.cmcIndexes);
3090
+ if (!cmcIndexes) {
3091
+ return {
3092
+ source: null,
3093
+ available: false,
3094
+ interval: null,
3095
+ asOfTs: null,
3096
+ stale: null,
3097
+ cmc100Value: null,
3098
+ cmc100Change24hPct: null,
3099
+ cmc100TopConstituentSymbol: null,
3100
+ cmc100TopConstituentWeightPct: null,
3101
+ cmc20Value: null,
3102
+ cmc20Change24hPct: null,
3103
+ cmc20TopConstituentSymbol: null,
3104
+ cmc20TopConstituentWeightPct: null,
3105
+ cmc20ToCmc100Ratio: null,
3106
+ cmc20ToCmc100RatioChange24hPct: null,
3107
+ indexRegime: null
3108
+ };
2421
3109
  }
2422
- await ensureStrategyPluginsLoaded(cwd);
2423
- const { state } = getStrategyRegistryState(cwd);
2424
- return state.strategySourcesMap.get(name);
3110
+ return {
3111
+ source: String(cmcIndexes.source ?? ""),
3112
+ available: true,
3113
+ interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
3114
+ asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
3115
+ stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
3116
+ cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
3117
+ cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
3118
+ cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
3119
+ cmc100TopConstituentWeightPct: toFiniteNumber(
3120
+ cmcIndexes.cmc100TopConstituentWeightPct
3121
+ ),
3122
+ cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
3123
+ cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
3124
+ cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
3125
+ cmc20TopConstituentWeightPct: toFiniteNumber(
3126
+ cmcIndexes.cmc20TopConstituentWeightPct
3127
+ ),
3128
+ cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
3129
+ cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
3130
+ cmcIndexes.cmc20ToCmc100RatioChange24hPct
3131
+ ),
3132
+ indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
3133
+ };
2425
3134
  };
2426
- var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
2427
- if (!name) {
2428
- return void 0;
3135
+ var buildReferenceTradeFlowContextFromSignal = (signal) => {
3136
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3137
+ const relative = toRecord(baseContext?.relative);
3138
+ const refs = toRecord(relative?.referenceTradeFlow);
3139
+ const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
3140
+ const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
3141
+ const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
3142
+ if (!refs) {
3143
+ return {
3144
+ source: null,
3145
+ available: false,
3146
+ primaryReferenceSymbol: null,
3147
+ referenceSymbols: [],
3148
+ primaryTradeFlowBuyPressurePct: null,
3149
+ primaryTradeFlowStale: null
3150
+ };
2429
3151
  }
2430
- const { state } = getStrategyRegistryState(cwd);
2431
- return state.strategyManifestsMap.get(name);
3152
+ return {
3153
+ source: String(refs.source ?? ""),
3154
+ available: true,
3155
+ primaryReferenceSymbol,
3156
+ referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
3157
+ primaryTradeFlowBuyPressurePct: toFiniteNumber(
3158
+ primaryTradeFlow?.buyPressurePct
3159
+ ),
3160
+ primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
3161
+ };
2432
3162
  };
2433
- var strategies = new Proxy(
2434
- {},
2435
- {
2436
- get: (_target, property) => {
2437
- if (typeof property !== "string") {
2438
- return void 0;
2439
- }
2440
- return getStrategyRegistryState().state.strategyCreators.get(property);
2441
- },
2442
- ownKeys: () => {
2443
- return [...getStrategyRegistryState().state.strategyCreators.keys()];
2444
- },
2445
- getOwnPropertyDescriptor: () => ({
2446
- enumerable: true,
2447
- configurable: true
2448
- })
3163
+ var buildAiMarketContext = (signal) => ({
3164
+ execution: {
3165
+ binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
3166
+ },
3167
+ participation: {
3168
+ trueDelta: buildTrueDeltaContextFromSignal(signal),
3169
+ tradeFlow: buildTradeFlowContextFromSignal(signal)
3170
+ },
3171
+ relative: {
3172
+ marketBreadth: buildMarketBreadthContextFromSignal(signal),
3173
+ marketBreadths: buildMarketBreadthsContextFromSignal(signal),
3174
+ targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
3175
+ targetVsEth: buildTargetVsEthContextFromSignal(signal),
3176
+ btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
3177
+ cmcGlobal: buildCmcGlobalContextFromSignal(signal),
3178
+ cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
3179
+ cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
3180
+ cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
3181
+ cmcIndexes: buildCmcIndexesContextFromSignal(signal),
3182
+ referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
2449
3183
  }
2450
- );
3184
+ });
2451
3185
 
2452
3186
  // src/strategy/policyProfiles.ts
2453
3187
  var profileMatches = (profile, universe, assetClass) => {
@@ -2549,12 +3283,61 @@ var buildBaseAiPayload = (signal) => {
2549
3283
  };
2550
3284
  };
2551
3285
  var defaultAiAdapter = {};
3286
+ var tickerProfileFeatureIds = new Set(
3287
+ import_aiFeatures.TICKER_PROFILE_AI_FEATURES.map(({ id }) => id)
3288
+ );
2552
3289
  var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
2553
3290
  var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
2554
3291
  var buildAiPayloadByStrategy = (signal) => {
2555
3292
  const basePayload = buildBaseAiPayload(signal);
2556
3293
  const adapter = getSignalAiAdapter(signal);
2557
- return adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
3294
+ const payload = adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
3295
+ const custom = getStrategyManifest(signal.strategy)?.aiFeatures ?? [];
3296
+ const frozen = signal.aiFeatureReplay?.snapshot;
3297
+ if (frozen) (0, import_aiFeatures.validateAiFeatureSnapshot)(frozen);
3298
+ const tickerProfile = signal.additionalIndicators?.tickerProfile ?? signal.additionalIndicators?.baseContext?.tickerProfile;
3299
+ const hasProfile = Boolean(
3300
+ tickerProfile || frozen?.definitions.some(({ id }) => tickerProfileFeatureIds.has(id))
3301
+ );
3302
+ const indicatorFeatures = (0, import_indicators2.getRegisteredIndicatorEntries)(
3303
+ getTradejsProjectCwd()
3304
+ ).flatMap((entry) => entry.indicator.enabled ? entry.aiFeatures ?? [] : []);
3305
+ const hasPortfolio = Boolean(
3306
+ signal.additionalIndicators?.portfolioContext || frozen?.definitions.some(
3307
+ ({ id }) => import_aiFeatures.PORTFOLIO_AI_FEATURES.some((d) => d.id === id)
3308
+ )
3309
+ );
3310
+ const definitions = [
3311
+ ...hasPortfolio ? import_aiFeatures.PORTFOLIO_AI_FEATURES : [],
3312
+ ...hasProfile ? import_aiFeatures.TICKER_PROFILE_AI_FEATURES : [],
3313
+ ...indicatorFeatures,
3314
+ ...custom
3315
+ ];
3316
+ if (!definitions.length && !signal.aiFeatureReplay?.snapshot) return payload;
3317
+ (0, import_aiFeatures.validateAiFeatureDefinitions)(definitions);
3318
+ if (signal.aiFeatureReplay && !frozen) {
3319
+ throw new Error(
3320
+ "Dataset has no frozen AI features; regenerate the decision-time export"
3321
+ );
3322
+ }
3323
+ if (frozen && JSON.stringify(frozen.definitions) !== JSON.stringify(definitions)) {
3324
+ throw new Error(
3325
+ "AI feature definition changed; regenerate the decision-time export"
3326
+ );
3327
+ }
3328
+ return {
3329
+ ...payload,
3330
+ featureSnapshot: frozen ?? (0, import_aiFeatures.projectAiFeatures)(definitions, {
3331
+ signal: tickerProfile ? {
3332
+ ...signal,
3333
+ additionalIndicators: {
3334
+ ...signal.additionalIndicators,
3335
+ tickerProfile
3336
+ }
3337
+ } : signal,
3338
+ payload
3339
+ })
3340
+ };
2558
3341
  };
2559
3342
  var buildAiSystemPromptAddonByStrategy = (signal) => getSignalAiAdapter(signal).buildSystemPromptAddon?.({ signal }) ?? "";
2560
3343
  var buildAiHumanPromptAddonByStrategy = (signal, payload) => getSignalAiAdapter(signal).buildHumanPromptAddon?.({
@@ -3016,7 +3799,7 @@ var askAI = async (signal, options = {}) => {
3016
3799
  payload
3017
3800
  }
3018
3801
  );
3019
- await (0, import_redis.setData)(import_redis.redisKeys.analysis(symbol, signal.signalId), content);
3802
+ await (0, import_redis3.setData)(import_redis3.redisKeys.analysis(symbol, signal.signalId), content);
3020
3803
  return content;
3021
3804
  };
3022
3805
 
@@ -3068,254 +3851,6 @@ var buildMlPayload = (payload) => {
3068
3851
  };
3069
3852
  };
3070
3853
 
3071
- // src/runtimeJournal.ts
3072
- var import_node_crypto = require("crypto");
3073
- var import_constants = require("@tradejs/core/constants");
3074
- var import_time = require("@tradejs/core/time");
3075
- var import_trade = require("@tradejs/core/trade");
3076
- var import_logger3 = require("@tradejs/infra/logger");
3077
- var import_redis2 = require("@tradejs/infra/redis");
3078
- var now = () => Date.now();
3079
- var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
3080
- var calculateClosedPnl = ({
3081
- direction,
3082
- entryPrice,
3083
- exitPrice,
3084
- qty
3085
- }) => {
3086
- const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
3087
- return Number.isFinite(pnl) ? pnl : null;
3088
- };
3089
- var createRuntimeOrderId = (strategy) => {
3090
- const prefix = (0, import_trade.createRuntimeOrderLinkPrefix)(strategy);
3091
- if (prefix === "tjs-") {
3092
- return `tjs-${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
3093
- }
3094
- return `${prefix}${toRandomOrderSuffix()}`;
3095
- };
3096
- var recordRuntimeTradeOpen = async (params) => {
3097
- const { userName } = params;
3098
- if (!userName) {
3099
- return null;
3100
- }
3101
- const record = {
3102
- ...params,
3103
- entryCount: 1,
3104
- lastEntryPrice: params.entryPrice,
3105
- lastEntryQty: params.qty,
3106
- lastEntryTimestamp: params.entryTimestamp,
3107
- status: "active",
3108
- currentPrice: params.entryPrice,
3109
- currentPnl: 0,
3110
- closedPnl: null,
3111
- exitPrice: null,
3112
- exitTimestamp: null,
3113
- lastSyncedAt: now()
3114
- };
3115
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(record.entryTimestamp);
3116
- const runtimeScopeId = record.deploymentId ?? record.accountId;
3117
- try {
3118
- await Promise.all([
3119
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, record.orderId), record, {
3120
- expire: 0
3121
- }),
3122
- (0, import_redis2.setHashJsonField)(
3123
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3124
- record.orderId,
3125
- record,
3126
- { expire: 0 }
3127
- ),
3128
- (0, import_redis2.setData)(
3129
- import_redis2.redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
3130
- { orderId: record.orderId },
3131
- { expire: 0 }
3132
- )
3133
- ]);
3134
- } catch (error) {
3135
- import_logger3.logger.error(
3136
- "runtime trade open journal failed: %s %s",
3137
- record.symbol,
3138
- error?.message || String(error)
3139
- );
3140
- }
3141
- return record;
3142
- };
3143
- var recordRuntimeTradeIncrease = async (params) => {
3144
- const {
3145
- userName,
3146
- strategy,
3147
- symbol,
3148
- direction,
3149
- resultingQty,
3150
- resultingEntryPrice,
3151
- addedQty,
3152
- addedEntryPrice,
3153
- entryTimestamp,
3154
- fee,
3155
- accountId,
3156
- deploymentId
3157
- } = params;
3158
- if (!userName) {
3159
- return null;
3160
- }
3161
- const existing = await getActiveRuntimeTrade({
3162
- userName,
3163
- symbol,
3164
- accountId,
3165
- deploymentId
3166
- });
3167
- if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
3168
- return null;
3169
- }
3170
- const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
3171
- const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
3172
- const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
3173
- const next = {
3174
- ...existing,
3175
- ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
3176
- qty: resultingQty,
3177
- entryPrice: resultingEntryPrice,
3178
- entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
3179
- lastEntryPrice: addedEntryPrice,
3180
- lastEntryQty: addedQty,
3181
- lastEntryTimestamp: entryTimestamp,
3182
- currentPrice: resultingEntryPrice,
3183
- currentPnl: 0,
3184
- fee: openFee,
3185
- openFee,
3186
- totalFee,
3187
- lastSyncedAt: now()
3188
- };
3189
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3190
- try {
3191
- await Promise.all([
3192
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, existing.orderId), next, {
3193
- expire: 0
3194
- }),
3195
- (0, import_redis2.setHashJsonField)(
3196
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3197
- existing.orderId,
3198
- next,
3199
- { expire: 0 }
3200
- )
3201
- ]);
3202
- } catch (error) {
3203
- import_logger3.logger.error(
3204
- "runtime trade increase journal failed: %s %s",
3205
- symbol,
3206
- error?.message || String(error)
3207
- );
3208
- }
3209
- return next;
3210
- };
3211
- var getActiveRuntimeTrade = async (params) => {
3212
- const { userName, symbol, accountId, deploymentId } = params;
3213
- if (!userName) {
3214
- return null;
3215
- }
3216
- const activeRef = await (0, import_redis2.getData)(
3217
- import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
3218
- null
3219
- );
3220
- const orderId = String(activeRef?.orderId || "").trim();
3221
- if (!orderId) {
3222
- return null;
3223
- }
3224
- const existing = await (0, import_redis2.getData)(
3225
- import_redis2.redisKeys.runtimeTrade(userName, orderId),
3226
- null
3227
- );
3228
- if (!existing || existing.status !== "active") {
3229
- await (0, import_redis2.delKey)(
3230
- import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
3231
- );
3232
- return null;
3233
- }
3234
- return existing;
3235
- };
3236
- var markRuntimeTradeClosed = async (params) => {
3237
- const {
3238
- userName,
3239
- symbol,
3240
- strategy,
3241
- exitPrice,
3242
- exitTimestamp,
3243
- closedPnl,
3244
- exitType,
3245
- accountId,
3246
- deploymentId
3247
- } = params;
3248
- if (!userName) {
3249
- return null;
3250
- }
3251
- const existing = await getActiveRuntimeTrade({
3252
- userName,
3253
- symbol,
3254
- accountId,
3255
- deploymentId
3256
- });
3257
- if (!existing) {
3258
- return null;
3259
- }
3260
- const orderId = existing.orderId;
3261
- if (strategy && existing.strategy !== strategy) {
3262
- return null;
3263
- }
3264
- const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
3265
- const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
3266
- direction: existing.direction,
3267
- entryPrice: existing.entryPrice,
3268
- exitPrice: resolvedExitPrice,
3269
- qty: existing.qty
3270
- }) : existing.closedPnl ?? existing.currentPnl ?? null;
3271
- const next = {
3272
- ...existing,
3273
- status: "closed",
3274
- currentPrice: resolvedExitPrice,
3275
- currentPnl: resolvedClosedPnl,
3276
- closedPnl: resolvedClosedPnl,
3277
- exitPrice: resolvedExitPrice,
3278
- exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
3279
- exitType: exitType ?? existing.exitType ?? null,
3280
- lastSyncedAt: now()
3281
- };
3282
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3283
- const closeDayKey = (0, import_time.getRuntimeStorageDayKey)(next.exitTimestamp);
3284
- try {
3285
- await Promise.all([
3286
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, orderId), next, {
3287
- expire: import_constants.TTL_1M
3288
- }),
3289
- (0, import_redis2.setHashJsonField)(
3290
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3291
- orderId,
3292
- next,
3293
- { expire: import_constants.TTL_1M }
3294
- ),
3295
- (0, import_redis2.setHashJsonField)(
3296
- import_redis2.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
3297
- orderId,
3298
- next,
3299
- { expire: import_constants.TTL_1M }
3300
- ),
3301
- (0, import_redis2.delKey)(
3302
- import_redis2.redisKeys.runtimeActiveTrade(
3303
- userName,
3304
- symbol,
3305
- deploymentId ?? accountId
3306
- )
3307
- )
3308
- ]);
3309
- } catch (error) {
3310
- import_logger3.logger.error(
3311
- "runtime trade close journal failed: %s %s",
3312
- symbol,
3313
- error?.message || String(error)
3314
- );
3315
- }
3316
- return next;
3317
- };
3318
-
3319
3854
  // src/strategyHelpers/marketContextStages.ts
3320
3855
  var import_logger8 = require("@tradejs/infra/logger");
3321
3856
 
@@ -3325,7 +3860,7 @@ var import_logger4 = require("@tradejs/infra/logger");
3325
3860
  var import_strategies = require("@tradejs/core/strategies");
3326
3861
 
3327
3862
  // src/binanceBreadthUniverses.ts
3328
- var import_node_crypto2 = require("crypto");
3863
+ var import_node_crypto3 = require("crypto");
3329
3864
 
3330
3865
  // src/config/binanceBreadthUniverses.json
3331
3866
  var binanceBreadthUniverses_default = {
@@ -3564,7 +4099,7 @@ var BINANCE_BREADTH_UNIVERSE_KEYS = [
3564
4099
  "top50",
3565
4100
  "top100"
3566
4101
  ];
3567
- var fingerprint = (value, length) => (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
4102
+ var fingerprint = (value, length) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
3568
4103
  var normalizeSymbols = (symbols) => symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean);
3569
4104
  var buildBinanceBreadthUniverseSnapshot = ({
3570
4105
  rankedSymbols,
@@ -4588,7 +5123,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4588
5123
  };
4589
5124
 
4590
5125
  // src/strategyHelpers/derivativesContext.ts
4591
- var import_indicators2 = require("@tradejs/core/indicators");
5126
+ var import_indicators3 = require("@tradejs/core/indicators");
4592
5127
  var import_data2 = require("@tradejs/core/data");
4593
5128
  var import_strategies3 = require("@tradejs/core/strategies");
4594
5129
  var import_constants2 = require("@tradejs/core/constants");
@@ -4622,7 +5157,7 @@ var parseLookbackMs = () => {
4622
5157
  };
4623
5158
  var withHourlyFallbackRows = (rowsByInterval) => ({
4624
5159
  "15m": rowsByInterval["15m"] ?? [],
4625
- "1h": (0, import_indicators2.buildCoinalyzeHourlyRowsWithFallback)({
5160
+ "1h": (0, import_indicators3.buildCoinalyzeHourlyRowsWithFallback)({
4626
5161
  rows15m: rowsByInterval["15m"],
4627
5162
  fallbackRows1h: rowsByInterval["1h"]
4628
5163
  })
@@ -4750,7 +5285,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4750
5285
  const targetSymbol = normalizeSymbol(signal.symbol);
4751
5286
  const lookbackMs = parseLookbackMs();
4752
5287
  const decisionTimeMs = signal.timestamp + (0, import_data2.intervalToMs)(signal.interval);
4753
- const derivativesEndMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
5288
+ const derivativesEndMs = (0, import_indicators3.getLastClosedDerivativesBarStartMs)(
4754
5289
  decisionTimeMs,
4755
5290
  "15m"
4756
5291
  );
@@ -4765,7 +5300,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4765
5300
  });
4766
5301
  return [
4767
5302
  symbol,
4768
- (0, import_indicators2.buildDerivativesContext)({
5303
+ (0, import_indicators3.buildDerivativesContext)({
4769
5304
  symbol,
4770
5305
  direction: signal.direction,
4771
5306
  timestamp: derivativesEndMs,
@@ -4792,7 +5327,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4792
5327
  lookbackMs,
4793
5328
  ...params.abortSignal ? { signal: params.abortSignal } : {}
4794
5329
  });
4795
- const context = (0, import_indicators2.buildDerivativesContext)({
5330
+ const context = (0, import_indicators3.buildDerivativesContext)({
4796
5331
  symbol: targetSymbol,
4797
5332
  direction: signal.direction,
4798
5333
  timestamp: derivativesEndMs,
@@ -4840,7 +5375,7 @@ var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWha
4840
5375
  var import_logger7 = require("@tradejs/infra/logger");
4841
5376
 
4842
5377
  // src/hyperliquidWhaleUniverse.ts
4843
- var import_node_crypto3 = require("crypto");
5378
+ var import_node_crypto4 = require("crypto");
4844
5379
 
4845
5380
  // src/config/hyperliquidPerpUniverse.json
4846
5381
  var hyperliquidPerpUniverse_default = {
@@ -5012,7 +5547,7 @@ var hyperliquidWhales_default = {
5012
5547
  };
5013
5548
 
5014
5549
  // src/hyperliquidWhaleUniverse.ts
5015
- var fingerprint2 = (value) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
5550
+ var fingerprint2 = (value) => (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
5016
5551
  var validateUnique = (values, label) => {
5017
5552
  if (new Set(values).size !== values.length) {
5018
5553
  throw new Error(`Hyperliquid ${label} snapshot contains duplicates`);
@@ -5499,6 +6034,7 @@ var enrichSignalWithMarketContextStages = async ({
5499
6034
  })
5500
6035
  });
5501
6036
  }
6037
+ await enrichSignalWithTickerProfile(signal);
5502
6038
  const results = [];
5503
6039
  for (const stage of stages) {
5504
6040
  if (abortSignal?.aborted) break;
@@ -5546,12 +6082,12 @@ var findReplayAiAnalysis = ({
5546
6082
  direction,
5547
6083
  ai
5548
6084
  }) => {
5549
- const snapshots = ai?.replayAnalyses;
5550
- if (!Array.isArray(snapshots) || !snapshots.length) {
6085
+ const snapshots2 = ai?.replayAnalyses;
6086
+ if (!Array.isArray(snapshots2) || !snapshots2.length) {
5551
6087
  return void 0;
5552
6088
  }
5553
6089
  let best = null;
5554
- for (const snapshot2 of snapshots) {
6090
+ for (const snapshot2 of snapshots2) {
5555
6091
  if (snapshot2.symbol !== signal.symbol || snapshot2.direction !== direction || snapshot2.strategy && snapshot2.strategy !== signal.strategy) {
5556
6092
  continue;
5557
6093
  }
@@ -5711,7 +6247,9 @@ var executeEntryOrder = async ({
5711
6247
  beforePlaceOrder,
5712
6248
  recordRuntimeTrade = true,
5713
6249
  leverage,
5714
- executionLimits
6250
+ executionLimits,
6251
+ markPortfolioSubmitted,
6252
+ rejectPortfolioSubmission
5715
6253
  }) => {
5716
6254
  await beforePlaceOrder?.();
5717
6255
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
@@ -5784,7 +6322,13 @@ var executeEntryOrder = async ({
5784
6322
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
5785
6323
  orderId,
5786
6324
  signal,
5787
- ...beforeSubmit ? { beforeSubmit } : {}
6325
+ ...rejectPortfolioSubmission ? { onRejected: rejectPortfolioSubmission } : {},
6326
+ ...beforeSubmit || markPortfolioSubmitted ? {
6327
+ beforeSubmit: async (context) => {
6328
+ await beforeSubmit?.(context);
6329
+ await markPortfolioSubmitted?.(context.qty);
6330
+ }
6331
+ } : {}
5788
6332
  });
5789
6333
  const orderAckTime = Date.now();
5790
6334
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -6772,7 +7316,7 @@ var handleProtectDecision = async ({
6772
7316
  }
6773
7317
  return decision.code;
6774
7318
  };
6775
- var executeEntryDecision = async ({
7319
+ var executeAdmittedEntryDecision = async ({
6776
7320
  connector,
6777
7321
  symbol,
6778
7322
  decision,
@@ -6786,7 +7330,9 @@ var executeEntryDecision = async ({
6786
7330
  ai,
6787
7331
  recordRuntimeJournal,
6788
7332
  invokeStageHooks,
6789
- notifyRuntimeError
7333
+ notifyRuntimeError,
7334
+ markPortfolioSubmitted,
7335
+ rejectPortfolioSubmission
6790
7336
  }) => {
6791
7337
  const signal = decision.signal;
6792
7338
  const beforePlaceOrder = async () => {
@@ -6833,6 +7379,8 @@ var executeEntryDecision = async ({
6833
7379
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
6834
7380
  signal,
6835
7381
  beforePlaceOrder,
7382
+ markPortfolioSubmitted,
7383
+ rejectPortfolioSubmission,
6836
7384
  recordRuntimeTrade: recordRuntimeJournal,
6837
7385
  executionLimits: resolveEntryExecutionLimits(
6838
7386
  hookCtx.strategyConfig,
@@ -6973,6 +7521,37 @@ var executeEntryDecision = async ({
6973
7521
  }
6974
7522
  return signal ?? decision.code;
6975
7523
  };
7524
+ async function executeEntryDecision(params) {
7525
+ if (!params.portfolio) return executeAdmittedEntryDecision(params);
7526
+ const { decision, portfolio } = params;
7527
+ const signal = decision.signal;
7528
+ if (!signal || decision.orderPlan.positionIntent === "increase")
7529
+ return "PORTFOLIO_REQUIRES_NEW_SIGNAL_POSITION";
7530
+ signal.orderId ??= createRuntimeOrderId(signal.strategy);
7531
+ const result = await portfolio.submit(
7532
+ params.portfolioTime ?? decision.entryContext.timestamp,
7533
+ {
7534
+ id: signal.orderId,
7535
+ signalId: signal.signalId,
7536
+ strategy: signal.strategy,
7537
+ symbol: params.symbol,
7538
+ direction: decision.entryContext.direction,
7539
+ qty: decision.orderPlan.qty,
7540
+ timestamp: decision.entryContext.timestamp
7541
+ },
7542
+ (markPortfolioSubmitted, rejectPortfolioSubmission) => executeAdmittedEntryDecision({
7543
+ ...params,
7544
+ markPortfolioSubmitted,
7545
+ rejectPortfolioSubmission
7546
+ })
7547
+ );
7548
+ if (result.reason) {
7549
+ signal.orderStatus = "failed";
7550
+ signal.orderFailureReason = result.reason;
7551
+ return signal;
7552
+ }
7553
+ return result.value;
7554
+ }
6976
7555
 
6977
7556
  // src/strategyRuntime.ts
6978
7557
  var cloneWithPropertyDescriptors = (value) => Object.create(
@@ -7000,6 +7579,7 @@ var createStrategyRuntime = ({
7000
7579
  };
7001
7580
  const creator = async ({
7002
7581
  userName,
7582
+ connectorName,
7003
7583
  config: baseConfig,
7004
7584
  symbol,
7005
7585
  universe: requestedUniverse,
@@ -7038,6 +7618,17 @@ var createStrategyRuntime = ({
7038
7618
  const projectConfig = await loadTradejsConfig(projectRoot);
7039
7619
  const projectHooks = projectConfig.hooks;
7040
7620
  const env = String(config.ENV ?? "BACKTEST");
7621
+ const portfolio = getPortfolioRuntime({
7622
+ connectorName,
7623
+ value: baseConfig?.PORTFOLIO_POLICY ?? config.PORTFOLIO_POLICY,
7624
+ strategy: strategyName,
7625
+ connector,
7626
+ env,
7627
+ userName,
7628
+ accountId,
7629
+ deploymentId
7630
+ });
7631
+ const portfolioContext = portfolio ? (now2) => isRuntimeOrderExecutionEnabled({ config, env, connector }) ? portfolio.tick(now2) : portfolio.context(now2) : void 0;
7041
7632
  const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
7042
7633
  const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
7043
7634
  if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
@@ -7336,6 +7927,20 @@ var createStrategyRuntime = ({
7336
7927
  interval: decisionInterval
7337
7928
  }) => {
7338
7929
  if (!baseContext) return void 0;
7930
+ const profileSource = strategyManifest?.contextRequirements?.tickerProfiles;
7931
+ if (profileSource) {
7932
+ const profile = await loadTickerProfileContext({
7933
+ ...profileSource,
7934
+ projectRoot,
7935
+ symbol: decisionSymbol,
7936
+ timestamp: candle.timestamp
7937
+ });
7938
+ if (!profile)
7939
+ throw new Error(
7940
+ "Required ticker profile is unavailable at decision time"
7941
+ );
7942
+ baseContext = withTickerProfile(baseContext, profile);
7943
+ }
7339
7944
  if (!coreContextRequirements.has("hyperliquidWhales")) {
7340
7945
  return baseContext;
7341
7946
  }
@@ -7503,6 +8108,8 @@ var createStrategyRuntime = ({
7503
8108
  runtime: pending2.runtime
7504
8109
  });
7505
8110
  return executeEntryDecision({
8111
+ portfolio,
8112
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
7506
8113
  connector,
7507
8114
  symbol,
7508
8115
  decision: pending2.decision,
@@ -7520,6 +8127,10 @@ var createStrategyRuntime = ({
7520
8127
  });
7521
8128
  };
7522
8129
  const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
8130
+ if (portfolioContext)
8131
+ await portfolioContext(
8132
+ env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now()
8133
+ );
7523
8134
  appendCurrentMarketData(candle, btcCandle, options.ethCandle);
7524
8135
  const ethCandle = resolveEthCandle(candle, options.ethCandle);
7525
8136
  indicatorsState.setCurrentBar(candle, btcCandle, ethCandle);
@@ -7690,6 +8301,13 @@ var createStrategyRuntime = ({
7690
8301
  });
7691
8302
  const signal = decision.signal;
7692
8303
  if (signal) {
8304
+ if (portfolio)
8305
+ signal.additionalIndicators = {
8306
+ ...signal.additionalIndicators,
8307
+ portfolioContext: await portfolioContext(
8308
+ env === "BACKTEST" || env === "PARITY" ? decision.entryContext.timestamp : Date.now()
8309
+ )
8310
+ };
7693
8311
  if (runtimeLineage) signal.runtimeLineage = runtimeLineage;
7694
8312
  if (universe) signal.universe = universe;
7695
8313
  if (assetClass) signal.assetClass = assetClass;
@@ -7929,6 +8547,8 @@ var createStrategyRuntime = ({
7929
8547
  });
7930
8548
  }
7931
8549
  return executeEntryDecision({
8550
+ portfolio,
8551
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
7932
8552
  connector,
7933
8553
  symbol,
7934
8554
  decision,
@@ -8040,7 +8660,7 @@ var normalizeForCanonicalJson = (value) => {
8040
8660
  }
8041
8661
  return value;
8042
8662
  };
8043
- var revision = (prefix, value) => `${prefix}:${(0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(normalizeForCanonicalJson(value))).digest("hex").slice(0, 16)}`;
8663
+ var revision = (prefix, value) => `${prefix}:${(0, import_node_crypto5.createHash)("sha256").update(JSON.stringify(normalizeForCanonicalJson(value))).digest("hex").slice(0, 16)}`;
8044
8664
  var computeStrategyRevision = ({
8045
8665
  strategyName,
8046
8666
  strategyPackage,