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

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