@tradejs/node 3.1.29-beta.289 → 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,1650 +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
- if (limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9))
1125
- throw new EntrySubmissionValidationError(
1126
- "ENTRY_RISK_EXCEEDS_MAX_LOSS_AT_SUBMISSION"
1127
- );
1128
- return { netRiskRatio, plannedLossValue };
1005
+ const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
1006
+ return Number.isFinite(pnl) ? pnl : null;
1129
1007
  };
1130
-
1131
- // src/strategy/entrySubmissionValidation.ts
1132
- var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
1133
- var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
1134
- var getOrderArrivalSnapshot = async ({
1135
- connector,
1136
- symbol,
1137
- requireTimestamp = false
1138
- }) => {
1139
- if (typeof connector.getTopOfBookTicker !== "function") {
1140
- return {
1141
- arrivalSnapshotTime: Date.now(),
1142
- arrivalSource: "unavailable",
1143
- bid: null,
1144
- ask: null,
1145
- arrivalMid: null,
1146
- spreadBps: null
1147
- };
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;
1148
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;
1149
1036
  try {
1150
- const ticker = await connector.getTopOfBookTicker(symbol);
1151
- const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
1152
- const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
1153
- const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
1154
- const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
1155
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
1156
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
1157
- return {
1158
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
1159
- arrivalSource: "top_of_book",
1160
- bid,
1161
- ask,
1162
- arrivalMid,
1163
- spreadBps
1164
- };
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
+ ]);
1165
1053
  } catch (error) {
1166
- import_logger.logger.warn(
1167
- "runtime order arrival snapshot failed: %s %s",
1168
- symbol,
1054
+ import_logger.logger.error(
1055
+ "runtime trade open journal failed: %s %s",
1056
+ record.symbol,
1169
1057
  error?.message || String(error)
1170
1058
  );
1171
- return {
1172
- arrivalSnapshotTime: Date.now(),
1173
- arrivalSource: "top_of_book_error",
1174
- bid: null,
1175
- ask: null,
1176
- arrivalMid: null,
1177
- spreadBps: null
1178
- };
1179
1059
  }
1060
+ return record;
1180
1061
  };
1181
- var validateEntryProtectionAtArrival = ({
1182
- direction,
1183
- signalPrice,
1184
- bid,
1185
- ask,
1186
- arrivalMid,
1187
- takeProfits,
1188
- stopLossPrice
1189
- }) => {
1190
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
1191
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
1192
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
1193
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
1194
- }
1195
- for (const takeProfit of takeProfits) {
1196
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
1197
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
1198
- }
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;
1199
1079
  }
1200
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
1201
- 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;
1202
1088
  }
1203
- };
1204
- var createEntrySubmissionValidation = ({
1205
- connector,
1206
- symbol,
1207
- direction,
1208
- takeProfits,
1209
- stopLossPrice,
1210
- limits,
1211
- onValidated
1212
- }) => {
1213
- if (!limits) return void 0;
1214
- if (!connector.capabilities?.orderSubmissionValidation)
1215
- throw new EntrySubmissionValidationError(
1216
- "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
1217
- );
1218
- return async ({ qty, executionPrice }) => {
1219
- const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
1220
- const snapshot2 = simulated && executionPrice != null ? {
1221
- arrivalSnapshotTime: Date.now(),
1222
- arrivalSource: "simulation",
1223
- bid: executionPrice,
1224
- ask: executionPrice,
1225
- arrivalMid: executionPrice,
1226
- spreadBps: 0
1227
- } : await getOrderArrivalSnapshot({
1228
- connector,
1229
- symbol,
1230
- requireTimestamp: true
1231
- });
1232
- const age = Date.now() - snapshot2.arrivalSnapshotTime;
1233
- if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
1234
- throw new EntrySubmissionValidationError(
1235
- "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
1236
- );
1237
- if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
1238
- throw new EntrySubmissionValidationError(
1239
- "ENTRY_QUOTE_STALE_AT_SUBMISSION"
1240
- );
1241
- const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
1242
- validateEntryProtectionAtArrival({
1243
- direction,
1244
- signalPrice: price,
1245
- bid: snapshot2.bid,
1246
- ask: snapshot2.ask,
1247
- arrivalMid: snapshot2.arrivalMid,
1248
- takeProfits,
1249
- stopLossPrice
1250
- });
1251
- const prices = takeProfits.map((tp) => tp.price);
1252
- const economics2 = validateEntryExecution({
1253
- direction,
1254
- price,
1255
- qty,
1256
- stopLossPrice,
1257
- takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
1258
- limits
1259
- });
1260
- 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()
1261
1107
  };
1262
- };
1263
-
1264
- // src/ai.ts
1265
- var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
1266
- var import_redis = require("@tradejs/infra/redis");
1267
-
1268
- // src/aiShared.ts
1269
- var MAX_AI_SERIES_POINTS = 5;
1270
- var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
1271
- "tradejs.indicators.compactSnapshot"
1272
- );
1273
- var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
1274
- var trimSeriesDeep = (value) => {
1275
- if (Array.isArray(value)) {
1276
- const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
1277
- const isMatrix = trimmed.every((item) => Array.isArray(item));
1278
- if (isMatrix) {
1279
- return trimmed;
1280
- }
1281
- return trimmed.map(
1282
- (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
1283
- );
1284
- }
1285
- if (value && typeof value === "object") {
1286
- return Object.fromEntries(
1287
- Object.entries(value).map(([key, nested]) => [
1288
- key,
1289
- trimSeriesDeep(nested)
1290
- ])
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)
1291
1126
  );
1292
1127
  }
1293
- return value;
1128
+ return next;
1294
1129
  };
1295
- var buildCompactAiIndicatorsSnapshot = (value) => {
1296
- const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
1297
- if (typeof compactSnapshot === "function") {
1298
- 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;
1299
1134
  }
1300
- return trimSeriesDeep(value);
1301
- };
1302
-
1303
- // src/aiMarketContext.ts
1304
- var toRecord = (value) => {
1305
- 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) {
1306
1141
  return null;
1307
1142
  }
1308
- return value;
1309
- };
1310
- var toFiniteNumber = (value) => {
1311
- const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
1312
- return Number.isFinite(numeric) ? numeric : null;
1313
- };
1314
- var roundTo = (value, decimals) => {
1315
- const factor = 10 ** decimals;
1316
- return Math.round(value * factor) / factor;
1317
- };
1318
- var buildMissingSpreadContext = () => ({
1319
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1320
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1321
- available: false,
1322
- value: null,
1323
- zScore: null,
1324
- bps: null,
1325
- absBps: null,
1326
- bias: null,
1327
- severity: null
1328
- });
1329
- var buildSpreadContextFromSignal = (signal) => {
1330
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1331
- const relative = toRecord(baseContext?.relative);
1332
- const execution = toRecord(relative?.execution);
1333
- const spread = toFiniteNumber(execution?.venueSpread);
1334
- const zScore = toFiniteNumber(execution?.venueSpreadZScore);
1335
- if (spread == null) {
1336
- 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;
1337
1152
  }
1338
- const value = roundTo(spread, 8);
1339
- const bps = roundTo(value * 1e4, 2);
1340
- const absBps = Math.abs(bps);
1341
- const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
1342
- const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
1343
- return {
1344
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1345
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1346
- available: true,
1347
- value,
1348
- zScore,
1349
- bps,
1350
- absBps,
1351
- bias,
1352
- severity
1353
- };
1153
+ return existing;
1354
1154
  };
1355
- var buildTrueDeltaContextFromSignal = (signal) => {
1356
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1357
- const participation = toRecord(baseContext?.participation);
1358
- const delta = toRecord(participation?.delta);
1359
- const source = String(delta?.source ?? "");
1360
- const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
1361
- if (!delta || !isTrueDeltaSource) {
1362
- return {
1363
- source: source || null,
1364
- available: false,
1365
- buyPressurePct: null,
1366
- buyVolume: null,
1367
- sellVolume: null,
1368
- netDelta: null,
1369
- deltaPct: null,
1370
- signedVolumeZScore: null
1371
- };
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;
1372
1170
  }
1373
- return {
1374
- source,
1375
- available: true,
1376
- buyPressurePct: toFiniteNumber(delta.buyPressurePct),
1377
- buyVolume: toFiniteNumber(delta.buyVolume),
1378
- sellVolume: toFiniteNumber(delta.sellVolume),
1379
- netDelta: toFiniteNumber(delta.netDelta),
1380
- deltaPct: toFiniteNumber(delta.deltaPct),
1381
- signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
1382
- };
1383
- };
1384
- var buildTradeFlowContextFromSignal = (signal) => {
1385
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1386
- const participation = toRecord(baseContext?.participation);
1387
- const tradeFlow = toRecord(participation?.tradeFlow);
1388
- if (!tradeFlow) {
1389
- return {
1390
- source: null,
1391
- available: false,
1392
- interval: null,
1393
- stale: null,
1394
- trades: null,
1395
- buyPressurePct: null,
1396
- netBaseDelta: null,
1397
- netQuoteDelta: null
1398
- };
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;
1399
1181
  }
1400
- return {
1401
- source: String(tradeFlow.source ?? ""),
1402
- available: true,
1403
- interval: String(tradeFlow.interval ?? ""),
1404
- stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
1405
- trades: toFiniteNumber(tradeFlow.trades),
1406
- buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
1407
- netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
1408
- 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()
1409
1203
  };
1410
- };
1411
- var buildMarketBreadthContextFromSignal = (signal) => {
1412
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1413
- const relative = toRecord(baseContext?.relative);
1414
- const breadth = toRecord(relative?.marketBreadth);
1415
- if (!breadth) {
1416
- return {
1417
- source: null,
1418
- available: false,
1419
- universe: null,
1420
- interval: null,
1421
- stale: null,
1422
- symbolsCount: null,
1423
- advanceDeclineRatio: null,
1424
- pctAboveMa20: null,
1425
- pctAboveMa50: null,
1426
- equalWeightedReturn: null,
1427
- volumeWeightedReturn: null,
1428
- dispersion: null
1429
- };
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
+ );
1430
1249
  }
1431
- return {
1432
- source: String(breadth.source ?? ""),
1433
- available: true,
1434
- universe: String(breadth.universe ?? ""),
1435
- interval: String(breadth.interval ?? ""),
1436
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
1437
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
1438
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
1439
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
1440
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
1441
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
1442
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
1443
- dispersion: toFiniteNumber(breadth.dispersion)
1444
- };
1250
+ return next;
1445
1251
  };
1446
- var buildMarketBreadthsContextFromSignal = (signal) => {
1447
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1448
- const relative = toRecord(baseContext?.relative);
1449
- const breadths = toRecord(relative?.marketBreadths);
1450
- const build = (key) => {
1451
- const breadth = toRecord(breadths?.[key]);
1452
- 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);
1453
1277
  return {
1454
- source: null,
1455
- available: false,
1456
- universe: null,
1457
- interval: null,
1458
- stale: null,
1459
- symbolsCount: null,
1460
- advanceDeclineRatio: null,
1461
- pctAboveMa20: null,
1462
- pctAboveMa50: null,
1463
- equalWeightedReturn: null,
1464
- volumeWeightedReturn: null,
1465
- dispersion: null
1278
+ id: r.id,
1279
+ status: filledQty >= r.qty ? "filled" : filledQty > 0 ? "partial" : "pending",
1280
+ filledQty,
1281
+ positionId: r.id
1466
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
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));
1467
1329
  }
1468
- return {
1469
- source: String(breadth.source ?? ""),
1470
- available: true,
1471
- universe: String(breadth.universe ?? ""),
1472
- interval: String(breadth.interval ?? ""),
1473
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
1474
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
1475
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
1476
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
1477
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
1478
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
1479
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
1480
- dispersion: toFiniteNumber(breadth.dispersion)
1481
- };
1482
1330
  };
1483
- return {
1484
- top5: build("top5"),
1485
- top10: build("top10"),
1486
- top30: build("top30"),
1487
- top50: build("top50"),
1488
- 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
1489
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();
1490
1464
  };
1491
- var buildTargetVsBtcContextFromSignal = (signal) => {
1492
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1493
- const relative = toRecord(baseContext?.relative);
1494
- const targetVsBtc = toRecord(relative?.targetVsBtc);
1495
- if (!targetVsBtc) {
1496
- return {
1497
- source: null,
1498
- available: false,
1499
- ratioReturn1h: null,
1500
- ratioReturn4h: null,
1501
- ratioReturn24h: null,
1502
- alphaVsBtc1h: null,
1503
- alphaVsBtc4h: null,
1504
- alphaVsBtc24h: null,
1505
- betaToBtc20: null,
1506
- correlationToBtc20: null,
1507
- ratioTrend: null
1508
- };
1465
+ var normalizeConfig = (rawConfig) => {
1466
+ if (!rawConfig || typeof rawConfig !== "object") {
1467
+ return {};
1509
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;
1510
1477
  return {
1511
- source: String(targetVsBtc.source ?? ""),
1512
- available: true,
1513
- ratioReturn1h: toFiniteNumber(targetVsBtc.ratioReturn1h),
1514
- ratioReturn4h: toFiniteNumber(targetVsBtc.ratioReturn4h),
1515
- ratioReturn24h: toFiniteNumber(targetVsBtc.ratioReturn24h),
1516
- alphaVsBtc1h: toFiniteNumber(targetVsBtc.alphaVsBtc1h),
1517
- alphaVsBtc4h: toFiniteNumber(targetVsBtc.alphaVsBtc4h),
1518
- alphaVsBtc24h: toFiniteNumber(targetVsBtc.alphaVsBtc24h),
1519
- betaToBtc20: toFiniteNumber(targetVsBtc.betaToBtc20),
1520
- correlationToBtc20: toFiniteNumber(targetVsBtc.correlationToBtc20),
1521
- ratioTrend: typeof targetVsBtc.ratioTrend === "string" ? targetVsBtc.ratioTrend : null
1478
+ strategies: strategies2,
1479
+ indicators,
1480
+ connectors,
1481
+ ...hooks ? { hooks } : {},
1482
+ ...runtime ? { runtime } : {}
1522
1483
  };
1523
1484
  };
1524
- var buildBtcAltRegimeContextFromSignal = (signal) => {
1525
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1526
- const relative = toRecord(baseContext?.relative);
1527
- const btcAltRegime = toRecord(relative?.btcAltRegime);
1528
- if (!btcAltRegime) {
1529
- return {
1530
- source: null,
1531
- available: false,
1532
- universe: null,
1533
- interval: null,
1534
- stale: null,
1535
- regime: null,
1536
- btcReturn24h: null,
1537
- altBasketReturn24h: null,
1538
- btcVsAltReturn24h: null,
1539
- btcTurnoverShare24h: null,
1540
- btcTurnoverShareChange24h: null,
1541
- altVolToBtcVol24h: null,
1542
- altDispersion24h: null
1543
- };
1485
+ var getNodeCreateRequire = () => {
1486
+ const builtinModule = process.getBuiltinModule?.("module");
1487
+ if (typeof builtinModule?.createRequire === "function") {
1488
+ return builtinModule.createRequire;
1544
1489
  }
1545
- return {
1546
- source: String(btcAltRegime.source ?? ""),
1547
- available: true,
1548
- universe: String(btcAltRegime.universe ?? ""),
1549
- interval: String(btcAltRegime.interval ?? ""),
1550
- stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
1551
- regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
1552
- btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
1553
- altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
1554
- btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
1555
- btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
1556
- btcTurnoverShareChange24h: toFiniteNumber(
1557
- btcAltRegime.btcTurnoverShareChange24h
1558
- ),
1559
- altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
1560
- altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
1561
- };
1490
+ throw new TypeError("module.createRequire is not available");
1562
1491
  };
1563
- var buildCmcGlobalContextFromSignal = (signal) => {
1564
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1565
- const relative = toRecord(baseContext?.relative);
1566
- const cmcGlobal = toRecord(relative?.cmcGlobal);
1567
- if (!cmcGlobal) {
1568
- return {
1569
- source: null,
1570
- available: false,
1571
- interval: null,
1572
- asOfTs: null,
1573
- stale: null,
1574
- totalMarketCapUsd: null,
1575
- totalVolumeUsd: null,
1576
- totalVolumeReportedUsd: null,
1577
- altMarketCapUsd: null,
1578
- altVolumeUsd: null,
1579
- altVolumeReportedUsd: null,
1580
- btcDominancePct: null,
1581
- ethDominancePct: null,
1582
- btcDominanceChange24hPct: null,
1583
- ethDominanceChange24hPct: null,
1584
- altMarketCapChange24hPct: null,
1585
- altVolumeChange24hPct: null,
1586
- activeCryptocurrencies: null,
1587
- activeExchanges: null,
1588
- activeMarketPairs: null,
1589
- altLiquidityRegime: null
1590
- };
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);
1591
1744
  }
1592
1745
  return {
1593
- source: String(cmcGlobal.source ?? ""),
1594
- available: true,
1595
- interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
1596
- asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
1597
- stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
1598
- totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
1599
- totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
1600
- totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
1601
- altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
1602
- altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
1603
- altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
1604
- btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
1605
- ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
1606
- btcDominanceChange24hPct: toFiniteNumber(
1607
- cmcGlobal.btcDominanceChange24hPct
1608
- ),
1609
- ethDominanceChange24hPct: toFiniteNumber(
1610
- cmcGlobal.ethDominanceChange24hPct
1611
- ),
1612
- altMarketCapChange24hPct: toFiniteNumber(
1613
- cmcGlobal.altMarketCapChange24hPct
1614
- ),
1615
- altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
1616
- activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
1617
- activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
1618
- activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
1619
- altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
1746
+ projectRoot,
1747
+ state
1620
1748
  };
1621
1749
  };
1622
- var buildCmcReferenceAssetsContextFromSignal = (signal) => {
1623
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1624
- const relative = toRecord(baseContext?.relative);
1625
- const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
1626
- if (!cmcReferenceAssets) {
1627
- return {
1628
- source: null,
1629
- available: false,
1630
- interval: null,
1631
- asOfTs: null,
1632
- stale: null,
1633
- btcMarketCapUsd: null,
1634
- ethMarketCapUsd: null,
1635
- btcVolumeUsd: null,
1636
- ethVolumeUsd: null,
1637
- btcVolumeToMarketCap: null,
1638
- ethVolumeToMarketCap: null,
1639
- ethBtcMarketCapRatio: null,
1640
- ethBtcMarketCapRatioChange24hPct: null,
1641
- ethVsBtcVolumeRatio: null,
1642
- referenceLiquidityRegime: null
1643
- };
1644
- }
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);
1645
1755
  return {
1646
- source: String(cmcReferenceAssets.source ?? ""),
1647
- available: true,
1648
- interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
1649
- asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
1650
- stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
1651
- btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
1652
- ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
1653
- btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
1654
- ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
1655
- btcVolumeToMarketCap: toFiniteNumber(
1656
- cmcReferenceAssets.btcVolumeToMarketCap
1657
- ),
1658
- ethVolumeToMarketCap: toFiniteNumber(
1659
- cmcReferenceAssets.ethVolumeToMarketCap
1660
- ),
1661
- ethBtcMarketCapRatio: toFiniteNumber(
1662
- cmcReferenceAssets.ethBtcMarketCapRatio
1663
- ),
1664
- ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
1665
- cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
1666
- ),
1667
- ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
1668
- referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
1756
+ strategyModules: toUniqueModules(config.strategies),
1757
+ indicatorModules: toUniqueModules(config.indicators)
1669
1758
  };
1670
1759
  };
1671
- var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
1672
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1673
- const relative = toRecord(baseContext?.relative);
1674
- const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
1675
- if (!cmcExchangeLiquidity) {
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
+ );
1676
1902
  return {
1677
- source: null,
1678
- available: false,
1679
- interval: null,
1680
- asOfTs: null,
1681
- stale: null,
1682
- exchangesCount: null,
1683
- totalVolumeUsd: null,
1684
- totalVolumeChange24hPct: null,
1685
- binanceVolumeUsd: null,
1686
- binanceVolumeShare: null,
1687
- topExchangeVolumeShare: null,
1688
- liquidityRegime: 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
1689
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;
1690
1922
  }
1691
- return {
1692
- source: String(cmcExchangeLiquidity.source ?? ""),
1693
- available: true,
1694
- interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
1695
- asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
1696
- stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
1697
- exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
1698
- totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
1699
- totalVolumeChange24hPct: toFiniteNumber(
1700
- cmcExchangeLiquidity.totalVolumeChange24hPct
1701
- ),
1702
- binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
1703
- binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
1704
- topExchangeVolumeShare: toFiniteNumber(
1705
- cmcExchangeLiquidity.topExchangeVolumeShare
1706
- ),
1707
- liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
1708
- };
1709
1923
  };
1710
- var buildCmcFearGreedContextFromSignal = (signal) => {
1711
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1712
- const relative = toRecord(baseContext?.relative);
1713
- const cmcFearGreed = toRecord(relative?.cmcFearGreed);
1714
- if (!cmcFearGreed) {
1715
- return {
1716
- source: null,
1717
- available: false,
1718
- interval: null,
1719
- asOfTs: null,
1720
- stale: null,
1721
- value: null,
1722
- valueChange24h: null,
1723
- valueChange7d: null,
1724
- classification: null,
1725
- sentimentRegime: null
1726
- };
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
+ })();
1727
1998
  }
1728
- return {
1729
- source: String(cmcFearGreed.source ?? ""),
1730
- available: true,
1731
- interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
1732
- asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
1733
- stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
1734
- value: toFiniteNumber(cmcFearGreed.value),
1735
- valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
1736
- valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
1737
- classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
1738
- sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
1739
- };
1999
+ await state.pluginsLoadPromise;
1740
2000
  };
1741
- var buildCmcIndexesContextFromSignal = (signal) => {
1742
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1743
- const relative = toRecord(baseContext?.relative);
1744
- const cmcIndexes = toRecord(relative?.cmcIndexes);
1745
- if (!cmcIndexes) {
1746
- return {
1747
- source: null,
1748
- available: false,
1749
- interval: null,
1750
- asOfTs: null,
1751
- stale: null,
1752
- cmc100Value: null,
1753
- cmc100Change24hPct: null,
1754
- cmc100TopConstituentSymbol: null,
1755
- cmc100TopConstituentWeightPct: null,
1756
- cmc20Value: null,
1757
- cmc20Change24hPct: null,
1758
- cmc20TopConstituentSymbol: null,
1759
- cmc20TopConstituentWeightPct: null,
1760
- cmc20ToCmc100Ratio: null,
1761
- cmc20ToCmc100RatioChange24hPct: null,
1762
- indexRegime: null
1763
- };
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;
1764
2005
  }
1765
- return {
1766
- source: String(cmcIndexes.source ?? ""),
1767
- available: true,
1768
- interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
1769
- asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
1770
- stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
1771
- cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
1772
- cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
1773
- cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
1774
- cmc100TopConstituentWeightPct: toFiniteNumber(
1775
- cmcIndexes.cmc100TopConstituentWeightPct
1776
- ),
1777
- cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
1778
- cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
1779
- cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
1780
- cmc20TopConstituentWeightPct: toFiniteNumber(
1781
- cmcIndexes.cmc20TopConstituentWeightPct
1782
- ),
1783
- cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
1784
- cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
1785
- cmcIndexes.cmc20ToCmc100RatioChange24hPct
1786
- ),
1787
- indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
1788
- };
2006
+ await ensureStrategyPluginsLoaded(cwd);
2007
+ const { state } = getStrategyRegistryState(cwd);
2008
+ return state.strategyCreators.get(name);
1789
2009
  };
1790
- var buildReferenceTradeFlowContextFromSignal = (signal) => {
1791
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1792
- const relative = toRecord(baseContext?.relative);
1793
- const refs = toRecord(relative?.referenceTradeFlow);
1794
- const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
1795
- const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
1796
- const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
1797
- if (!refs) {
1798
- return {
1799
- source: null,
1800
- available: false,
1801
- primaryReferenceSymbol: null,
1802
- referenceSymbols: [],
1803
- primaryTradeFlowBuyPressurePct: null,
1804
- primaryTradeFlowStale: null
1805
- };
2010
+ var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
2011
+ if (!name) {
2012
+ return void 0;
1806
2013
  }
1807
- return {
1808
- source: String(refs.source ?? ""),
1809
- available: true,
1810
- primaryReferenceSymbol,
1811
- referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
1812
- primaryTradeFlowBuyPressurePct: toFiniteNumber(
1813
- primaryTradeFlow?.buyPressurePct
1814
- ),
1815
- primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
1816
- };
2014
+ const { state } = getStrategyRegistryState(cwd);
2015
+ return state.strategyManifestsMap.get(name);
1817
2016
  };
1818
- var buildAiMarketContext = (signal) => ({
1819
- execution: {
1820
- binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
1821
- },
1822
- participation: {
1823
- trueDelta: buildTrueDeltaContextFromSignal(signal),
1824
- tradeFlow: buildTradeFlowContextFromSignal(signal)
1825
- },
1826
- relative: {
1827
- marketBreadth: buildMarketBreadthContextFromSignal(signal),
1828
- marketBreadths: buildMarketBreadthsContextFromSignal(signal),
1829
- targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
1830
- btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
1831
- cmcGlobal: buildCmcGlobalContextFromSignal(signal),
1832
- cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
1833
- cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
1834
- cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
1835
- cmcIndexes: buildCmcIndexesContextFromSignal(signal),
1836
- referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
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
+ })
1837
2033
  }
1838
- });
1839
-
1840
- // src/strategy/manifests.ts
1841
- var import_indicators = require("@tradejs/core/indicators");
2034
+ );
1842
2035
 
1843
- // src/tradejsConfig.ts
1844
- var import_fs = __toESM(require("fs"));
1845
- var import_path = __toESM(require("path"));
1846
- var import_url = require("url");
1847
- var import_config = require("@tradejs/core/config");
1848
- var import_logger2 = require("@tradejs/infra/logger");
1849
- var CONFIG_FILE_NAMES = [
1850
- "tradejs.config.ts",
1851
- "tradejs.config.mts",
1852
- "tradejs.config.js",
1853
- "tradejs.config.mjs",
1854
- "tradejs.config.cjs"
1855
- ];
1856
- var TS_MODULE_RE = /\.(cts|mts|ts)$/i;
1857
- var cachedByCwd = /* @__PURE__ */ new Map();
1858
- var announcedConfigFile = /* @__PURE__ */ new Set();
1859
- var tsNodeRegistered = false;
1860
- var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
1861
- var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
1862
- var getTradejsProjectCwd = (cwd) => {
1863
- const explicit = String(cwd ?? "").trim();
1864
- if (explicit) {
1865
- return import_path.default.resolve(explicit);
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
+ }
1866
2089
  }
1867
- const fromEnv = String(process.env.PROJECT_CWD || "").trim();
1868
- if (fromEnv) {
1869
- return import_path.default.resolve(fromEnv);
2090
+ return (await cached.value)?.get(symbol);
2091
+ };
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
2114
+ };
1870
2115
  }
1871
- return process.cwd();
1872
2116
  };
1873
- var normalizeConfig = (rawConfig) => {
1874
- if (!rawConfig || typeof rawConfig !== "object") {
1875
- return {};
2117
+
2118
+ // src/strategyRuntime.ts
2119
+ var import_jev6 = require("@tradejs/core/jev");
2120
+
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;
2140
+ };
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
+ };
1876
2200
  }
1877
- const config = rawConfig;
1878
- const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
1879
- const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
1880
- const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
1881
- const hooks = (0, import_config.normalizeTradejsConfigHooks)(
1882
- config.hooks
1883
- );
1884
- const runtime = config.runtime && typeof config.runtime === "object" && !Array.isArray(config.runtime) ? config.runtime : void 0;
1885
- return {
1886
- strategies: strategies2,
1887
- indicators,
1888
- connectors,
1889
- ...hooks ? { hooks } : {},
1890
- ...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);
1891
2277
  };
1892
- };
1893
- var getNodeCreateRequire = () => {
1894
- const builtinModule = process.getBuiltinModule?.("module");
1895
- if (typeof builtinModule?.createRequire === "function") {
1896
- 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);
1897
2283
  }
1898
- throw new TypeError("module.createRequire is not available");
1899
- };
1900
- var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
1901
- var ensureTsNodeRegistered = async () => {
1902
- if (tsNodeRegistered) {
1903
- return;
2284
+ let record;
2285
+ try {
2286
+ record = await promise;
2287
+ } finally {
2288
+ pending.delete(pendingKey);
1904
2289
  }
1905
- const tsNodeModule = await import("ts-node");
1906
- const tsNode = tsNodeModule.default ?? tsNodeModule;
1907
- tsNode.register?.({
1908
- transpileOnly: true,
1909
- compilerOptions: {
1910
- module: "Node16",
1911
- 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
+ )
1912
2304
  }
1913
- });
1914
- tsNodeRegistered = true;
1915
- };
1916
- var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
1917
- const projectRoot = getTradejsProjectCwd(cwd);
1918
- if (tsconfigPathsRegisteredByCwd.has(projectRoot)) {
1919
- return;
1920
- }
1921
- const tsconfigPathsModule = await import("tsconfig-paths");
1922
- const loadConfig = tsconfigPathsModule.loadConfig;
1923
- const register = tsconfigPathsModule.register;
1924
- if (typeof loadConfig !== "function" || typeof register !== "function") {
1925
- return;
1926
- }
1927
- const loadedConfig = loadConfig(projectRoot);
1928
- if (loadedConfig.resultType !== "success") {
1929
- return;
1930
- }
1931
- register({
1932
- baseUrl: loadedConfig.absoluteBaseUrl,
1933
- paths: loadedConfig.paths,
1934
- addMatchAll: false
1935
- });
1936
- tsconfigPathsRegisteredByCwd.add(projectRoot);
2305
+ };
1937
2306
  };
1938
- var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1939
- const projectRoot = getTradejsProjectCwd(cwd);
1940
- const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
1941
- if (cachedMatcher) {
1942
- const resolved2 = cachedMatcher(moduleName);
1943
- return resolved2 || null;
1944
- }
1945
- const tsconfigPathsModule = await import("tsconfig-paths");
1946
- const loadConfig = tsconfigPathsModule.loadConfig;
1947
- const createMatchPath = tsconfigPathsModule.createMatchPath;
1948
- if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
1949
- return null;
1950
- }
1951
- const loadedConfig = loadConfig(projectRoot);
1952
- if (loadedConfig.resultType !== "success") {
1953
- 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
+ };
1954
2340
  }
1955
- const matchPath = createMatchPath(
1956
- loadedConfig.absoluteBaseUrl,
1957
- 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
1958
2364
  );
1959
- const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
1960
- ".ts",
1961
- ".tsx",
1962
- ".mts",
1963
- ".cts",
1964
- ".js",
1965
- ".jsx",
1966
- ".mjs",
1967
- ".cjs",
1968
- ".json"
1969
- ]) || "";
1970
- tsconfigPathMatchersByCwd.set(projectRoot, matcher);
1971
- const resolved = matcher(moduleName);
1972
- return resolved || null;
1973
- };
1974
- var toImportSpecifier = (moduleName) => {
1975
- if (moduleName.startsWith("file://")) {
1976
- return moduleName;
1977
- }
1978
- if (import_path.default.isAbsolute(moduleName)) {
1979
- return (0, import_url.pathToFileURL)(moduleName).href;
1980
- }
1981
- return moduleName;
2365
+ return assessment;
1982
2366
  };
1983
- var isTsModulePath = (moduleName) => TS_MODULE_RE.test(moduleName.split("?")[0]);
1984
- var isRelativeModulePath = (moduleName) => moduleName.startsWith("./") || moduleName.startsWith("../");
1985
- var isBareModuleSpecifier = (moduleName) => {
1986
- const normalized = String(moduleName ?? "").trim();
1987
- if (!normalized) {
1988
- return false;
1989
- }
1990
- if (normalized.startsWith("file://") || import_path.default.isAbsolute(normalized) || isRelativeModulePath(normalized)) {
1991
- 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";
1992
2386
  }
1993
- return true;
1994
2387
  };
1995
- var importConfigFile = async (configFilePath) => {
1996
- const ext = import_path.default.extname(configFilePath).toLowerCase();
1997
- const configFileUrl = `${toImportSpecifier(configFilePath)}?t=${Date.now()}`;
1998
- if (ext === ".ts" || ext === ".mts") {
1999
- const requireFn = getRequireFn(import_path.default.dirname(configFilePath));
2000
- await ensureTsNodeRegistered();
2001
- await ensureTsconfigPathsRegistered(import_path.default.dirname(configFilePath));
2002
- return requireFn(configFilePath);
2003
- }
2004
- return import(
2005
- /* webpackIgnore: true */
2006
- configFileUrl
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 };
2402
+ };
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)
2007
2427
  );
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
+ };
2008
2442
  };
2009
- var importTradejsModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
2010
- const normalized = String(moduleName ?? "").trim();
2011
- if (!normalized) {
2012
- return {};
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
+ };
2013
2461
  }
2014
- let modulePath = normalized;
2015
- if (normalized.startsWith("file://")) {
2016
- try {
2017
- modulePath = (0, import_url.fileURLToPath)(normalized);
2018
- } catch {
2019
- modulePath = normalized;
2020
- }
2462
+ try {
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
+ };
2478
+ } catch (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
+ };
2021
2492
  }
2022
- const requireFn = getRequireFn(
2023
- import_path.default.isAbsolute(modulePath) ? import_path.default.dirname(modulePath) : cwd
2024
- );
2025
- if (isTsModulePath(modulePath)) {
2026
- await ensureTsNodeRegistered();
2027
- await ensureTsconfigPathsRegistered(cwd);
2028
- return requireFn(modulePath);
2493
+ };
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");
2029
2507
  }
2030
- if (isBareModuleSpecifier(normalized)) {
2031
- await ensureTsconfigPathsRegistered(cwd);
2032
- try {
2033
- return requireFn(normalized);
2034
- } catch (error) {
2035
- const resolvedByTsconfig = await resolveTsconfigPathModule(
2036
- normalized,
2037
- cwd
2038
- );
2039
- if (resolvedByTsconfig && resolvedByTsconfig !== normalized) {
2040
- return requireFn(resolvedByTsconfig);
2041
- }
2042
- throw error;
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");
2043
2511
  }
2044
2512
  }
2045
- try {
2046
- return await import(
2047
- /* webpackIgnore: true */
2048
- toImportSpecifier(normalized)
2049
- );
2050
- } catch (error) {
2051
- if (isTsModulePath(modulePath)) {
2052
- await ensureTsNodeRegistered();
2053
- await ensureTsconfigPathsRegistered(cwd);
2054
- return requireFn(modulePath);
2055
- }
2056
- throw error;
2513
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
2514
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
2057
2515
  }
2058
2516
  };
2059
- var resolveExportedConfig = (moduleExports) => {
2060
- const candidate = moduleExports && typeof moduleExports === "object" && "default" in moduleExports ? moduleExports.default : moduleExports;
2061
- return normalizeConfig(candidate);
2062
- };
2063
- var findConfigFilePath = (cwd) => {
2064
- let currentDir = import_path.default.resolve(cwd);
2065
- while (true) {
2066
- for (const fileName of CONFIG_FILE_NAMES) {
2067
- const fullPath = import_path.default.join(currentDir, fileName);
2068
- if (import_fs.default.existsSync(fullPath) && import_fs.default.statSync(fullPath).isFile()) {
2069
- return fullPath;
2070
- }
2071
- }
2072
- const parentDir = import_path.default.dirname(currentDir);
2073
- if (parentDir === currentDir) {
2074
- return null;
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
+ );
2075
2581
  }
2076
- currentDir = parentDir;
2077
- }
2582
+ onValidated?.(snapshot2, economics2);
2583
+ };
2078
2584
  };
2079
- var resolvePluginModuleSpecifier = (moduleName, cwd = getTradejsProjectCwd()) => {
2080
- const normalized = String(moduleName ?? "").trim();
2081
- if (!normalized) {
2082
- return "";
2083
- }
2084
- if (normalized.startsWith("file://")) {
2085
- try {
2086
- return (0, import_url.fileURLToPath)(normalized);
2087
- } catch {
2088
- return normalized;
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;
2089
2606
  }
2607
+ return trimmed.map(
2608
+ (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
2609
+ );
2090
2610
  }
2091
- if (import_path.default.isAbsolute(normalized)) {
2092
- return normalized;
2093
- }
2094
- if (isRelativeModulePath(normalized)) {
2095
- 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
+ );
2096
2618
  }
2097
- return normalized;
2619
+ return value;
2098
2620
  };
2099
- var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
2100
- const cached = cachedByCwd.get(cwd);
2101
- if (cached) {
2102
- return cached;
2103
- }
2104
- const configFilePath = findConfigFilePath(cwd);
2105
- if (!configFilePath) {
2106
- cachedByCwd.set(cwd, {});
2107
- return {};
2108
- }
2109
- try {
2110
- const moduleExports = await importConfigFile(configFilePath);
2111
- const config = resolveExportedConfig(moduleExports);
2112
- cachedByCwd.set(cwd, config);
2113
- if (!announcedConfigFile.has(configFilePath)) {
2114
- announcedConfigFile.add(configFilePath);
2115
- import_logger2.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
2116
- }
2117
- return config;
2118
- } catch (error) {
2119
- import_logger2.logger.log(
2120
- "warn",
2121
- "Failed to load TradeJS config from %s: %s",
2122
- configFilePath,
2123
- String(error)
2124
- );
2125
- cachedByCwd.set(cwd, {});
2126
- 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 });
2127
2625
  }
2626
+ return trimSeriesDeep(value);
2128
2627
  };
2129
2628
 
2130
- // src/strategy/manifests.ts
2131
- var SHARED_STRATEGY_REGISTRY_KEY = "__tradejsNodeSharedStrategyRegistryV1__";
2132
- var sharedRegistryScope = globalThis;
2133
- var sharedStrategyRegistry = sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] ?? (sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] = {
2134
- registryStateByProjectRoot: /* @__PURE__ */ new Map()
2135
- });
2136
- var createStrategyRegistryState = () => ({
2137
- strategyCreators: /* @__PURE__ */ new Map(),
2138
- strategyManifestsMap: /* @__PURE__ */ new Map(),
2139
- strategyEntriesMap: /* @__PURE__ */ new Map(),
2140
- strategySourcesMap: /* @__PURE__ */ new Map(),
2141
- strategyModuleResolutions: /* @__PURE__ */ new Map(),
2142
- 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
2143
2654
  });
2144
- var registryStateByProjectRoot = sharedStrategyRegistry.registryStateByProjectRoot;
2145
- var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
2146
- const projectRoot = getTradejsProjectCwd(cwd);
2147
- let state = registryStateByProjectRoot.get(projectRoot);
2148
- if (!state) {
2149
- state = createStrategyRegistryState();
2150
- 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();
2151
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";
2152
2669
  return {
2153
- projectRoot,
2154
- 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
2155
2679
  };
2156
2680
  };
2157
- var toUniqueModules = (modules = []) => [
2158
- ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
2159
- ];
2160
- var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
2161
- 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
+ }
2162
2699
  return {
2163
- strategyModules: toUniqueModules(config.strategies),
2164
- 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)
2165
2708
  };
2166
2709
  };
2167
- var extractModuleEntries = (moduleExport, key) => {
2168
- if (!moduleExport || typeof moduleExport !== "object") {
2169
- return null;
2170
- }
2171
- const candidate = moduleExport;
2172
- if (Array.isArray(candidate[key])) {
2173
- return candidate[key];
2174
- }
2175
- const defaultExport = candidate.default;
2176
- if (defaultExport && Array.isArray(defaultExport[key])) {
2177
- 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
+ };
2178
2725
  }
2179
- return null;
2180
- };
2181
- var extractStrategyPluginDefinition = (moduleExport) => {
2182
- const strategyEntries = extractModuleEntries(
2183
- moduleExport,
2184
- "strategyEntries"
2185
- );
2186
- return strategyEntries ? { strategyEntries } : null;
2187
- };
2188
- var extractIndicatorPluginDefinition = (moduleExport) => {
2189
- const indicatorEntries = extractModuleEntries(
2190
- moduleExport,
2191
- "indicatorEntries"
2192
- );
2193
- 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
+ };
2194
2736
  };
2195
- var validateStrategyEntries = (moduleName, entries, state) => {
2196
- const issues = [];
2197
- const names = /* @__PURE__ */ new Set();
2198
- entries.forEach((entry, index) => {
2199
- const entryPath = `${moduleName}.strategyEntries[${index}]`;
2200
- const strategyName = entry?.manifest?.name;
2201
- if (typeof strategyName !== "string" || !strategyName.trim()) {
2202
- issues.push(`${entryPath}: manifest.name is required`);
2203
- } else if (names.has(strategyName) || state?.strategyEntriesMap.has(strategyName)) {
2204
- issues.push(`${entryPath}: duplicate strategy ${strategyName}`);
2205
- } else {
2206
- names.add(strategyName);
2207
- }
2208
- if (!entry || typeof entry !== "object" || !entry.defaults || typeof entry.defaults !== "object" || Array.isArray(entry.defaults)) {
2209
- issues.push(`${entryPath}: defaults must be an object`);
2210
- }
2211
- if (typeof entry?.parseConfig !== "function") {
2212
- issues.push(`${entryPath}: parseConfig is required`);
2213
- }
2214
- if (typeof entry?.createCore !== "function") {
2215
- 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
+ };
2216
2793
  }
2217
- });
2218
- 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
+ };
2219
2816
  };
2220
- var registerEntries = (entries, source, state) => {
2221
- for (const entry of entries) {
2222
- const strategyName = entry.manifest.name;
2223
- state.strategyManifestsMap.set(strategyName, entry.manifest);
2224
- state.strategyEntriesMap.set(strategyName, entry);
2225
- state.strategySourcesMap.set(strategyName, source);
2226
- materializeStrategyCreator(strategyName, state);
2227
- }
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
+ };
2228
2834
  };
2229
- var materializeStrategyCreator = (strategyName, state) => {
2230
- if (state.strategyCreators.has(strategyName) || !sharedStrategyRegistry.strategyRuntimeFactory) {
2231
- return;
2232
- }
2233
- const entry = state.strategyEntriesMap.get(strategyName);
2234
- if (!entry) return;
2235
- state.strategyCreators.set(
2236
- strategyName,
2237
- sharedStrategyRegistry.strategyRuntimeFactory({
2238
- strategyName,
2239
- defaults: entry.defaults,
2240
- createCore: entry.createCore,
2241
- manifest: entry.manifest,
2242
- detectorKey: entry.detectorKey,
2243
- detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
2244
- resolveRegisteredManifest: (name) => state.strategyManifestsMap.get(name)
2245
- })
2246
- );
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
+ };
2247
2845
  };
2248
- var setStrategyRuntimeFactory = (factory) => {
2249
- sharedStrategyRegistry.strategyRuntimeFactory = factory;
2250
- for (const state of registryStateByProjectRoot.values()) {
2251
- for (const strategyName of state.strategyEntriesMap.keys()) {
2252
- materializeStrategyCreator(strategyName, state);
2253
- }
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
+ };
2254
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
+ };
2255
2895
  };
2256
- var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
2257
- if (typeof importTradejsModule === "function") {
2258
- 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
+ };
2259
2924
  }
2260
- return import(
2261
- /* webpackIgnore: true */
2262
- moduleName
2263
- );
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
+ };
2264
2954
  };
2265
- var resolveStrategyModuleEntry = async ({
2266
- strategyName,
2267
- moduleName,
2268
- cwd
2269
- }) => {
2270
- const { projectRoot, state } = getStrategyRegistryState(cwd);
2271
- const source = moduleName.trim();
2272
- const resolvedModuleName = resolvePluginModuleSpecifier(source, projectRoot);
2273
- const cacheKey = `${resolvedModuleName}\0${strategyName}`;
2274
- const cached = state.strategyModuleResolutions.get(cacheKey);
2275
- if (cached) return cached;
2276
- const resolution = (async () => {
2277
- const moduleExport = await importStrategyPluginModule(
2278
- resolvedModuleName,
2279
- projectRoot
2280
- );
2281
- const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
2282
- if (!pluginDefinition) {
2283
- throw new Error(`${source}: export { strategyEntries } is missing`);
2284
- }
2285
- const issues = validateStrategyEntries(
2286
- source,
2287
- pluginDefinition.strategyEntries
2288
- );
2289
- if (issues.length > 0) {
2290
- throw new Error(
2291
- ["Invalid TradeJS strategy module:", ...issues].join("\n")
2292
- );
2293
- }
2294
- const entry = pluginDefinition.strategyEntries.find(
2295
- (candidate) => candidate.manifest.name === strategyName
2296
- );
2297
- if (!entry) {
2298
- throw new Error(`${source}: strategy ${strategyName} is missing`);
2299
- }
2300
- const factory = sharedStrategyRegistry.strategyRuntimeFactory;
2301
- if (!factory) {
2302
- throw new Error("Strategy runtime factory is not configured");
2303
- }
2304
- const manifestsByName = new Map(
2305
- pluginDefinition.strategyEntries.map(
2306
- (candidate) => [candidate.manifest.name, candidate.manifest]
2307
- )
2308
- );
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) {
2960
+ return {
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
2976
+ };
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
+ };
3003
+ };
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) {
2309
3009
  return {
2310
- entry,
2311
- creator: factory({
2312
- strategyName,
2313
- defaults: entry.defaults,
2314
- createCore: entry.createCore,
2315
- manifest: entry.manifest,
2316
- detectorKey: entry.detectorKey,
2317
- detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
2318
- resolveRegisteredManifest: (name) => manifestsByName.get(name) ?? state.strategyManifestsMap.get(name)
2319
- }),
2320
- source
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
2321
3022
  };
2322
- })();
2323
- state.strategyModuleResolutions.set(cacheKey, resolution);
2324
- try {
2325
- return await resolution;
2326
- } catch (error) {
2327
- state.strategyModuleResolutions.delete(cacheKey);
2328
- throw error;
2329
3023
  }
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
+ };
2330
3042
  };
2331
- var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
2332
- const { projectRoot, state } = getStrategyRegistryState(cwd);
2333
- if (!state.pluginsLoadPromise) {
2334
- (0, import_indicators.resetIndicatorRegistryCache)(projectRoot);
2335
- state.pluginsLoadPromise = (async () => {
2336
- const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
2337
- const strategySet = new Set(strategyModules);
2338
- const indicatorSet = new Set(indicatorModules);
2339
- const pluginModuleNames = [
2340
- .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
2341
- ];
2342
- if (!pluginModuleNames.length) {
2343
- return;
2344
- }
2345
- const issues = [];
2346
- for (const moduleName of pluginModuleNames) {
2347
- try {
2348
- const resolvedModuleName = resolvePluginModuleSpecifier(
2349
- moduleName,
2350
- projectRoot
2351
- );
2352
- const moduleExport = await importStrategyPluginModule(
2353
- resolvedModuleName,
2354
- projectRoot
2355
- );
2356
- if (strategySet.has(moduleName)) {
2357
- const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
2358
- if (!pluginDefinition) {
2359
- issues.push(
2360
- `${moduleName}: export { strategyEntries } is missing`
2361
- );
2362
- } else {
2363
- const entryIssues = validateStrategyEntries(
2364
- moduleName,
2365
- pluginDefinition.strategyEntries,
2366
- state
2367
- );
2368
- issues.push(...entryIssues);
2369
- if (entryIssues.length === 0) {
2370
- registerEntries(
2371
- pluginDefinition.strategyEntries,
2372
- moduleName,
2373
- state
2374
- );
2375
- }
2376
- }
2377
- }
2378
- if (indicatorSet.has(moduleName)) {
2379
- const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
2380
- if (!indicatorPluginDefinition) {
2381
- issues.push(
2382
- `${moduleName}: export { indicatorEntries } is missing`
2383
- );
2384
- } else {
2385
- (0, import_indicators.registerIndicatorEntries)(
2386
- indicatorPluginDefinition.indicatorEntries,
2387
- moduleName,
2388
- projectRoot
2389
- );
2390
- }
2391
- }
2392
- if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
2393
- issues.push(`${moduleName}: plugin is not declared in config`);
2394
- }
2395
- } catch (error) {
2396
- issues.push(`${moduleName}: failed to import: ${String(error)}`);
2397
- }
2398
- }
2399
- if (issues.length > 0) {
2400
- throw new Error(
2401
- ["Invalid TradeJS plugin catalog:", ...issues].join("\n")
2402
- );
2403
- }
2404
- })();
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
+ };
2405
3060
  }
2406
- await state.pluginsLoadPromise;
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
+ };
2407
3073
  };
2408
- var getStrategyCreator = async (name, cwd = getTradejsProjectCwd(), moduleName) => {
2409
- if (moduleName?.trim()) {
2410
- await ensureStrategyPluginsLoaded(cwd);
2411
- return (await resolveStrategyModuleEntry({ strategyName: name, moduleName, cwd })).creator;
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
+ };
2412
3097
  }
2413
- await ensureStrategyPluginsLoaded(cwd);
2414
- const { state } = getStrategyRegistryState(cwd);
2415
- return state.strategyCreators.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
+ };
2416
3122
  };
2417
- var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
2418
- if (!name) {
2419
- return void 0;
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
+ };
2420
3139
  }
2421
- const { state } = getStrategyRegistryState(cwd);
2422
- return state.strategyManifestsMap.get(name);
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
+ };
2423
3150
  };
2424
- var strategies = new Proxy(
2425
- {},
2426
- {
2427
- get: (_target, property) => {
2428
- if (typeof property !== "string") {
2429
- return void 0;
2430
- }
2431
- return getStrategyRegistryState().state.strategyCreators.get(property);
2432
- },
2433
- ownKeys: () => {
2434
- return [...getStrategyRegistryState().state.strategyCreators.keys()];
2435
- },
2436
- getOwnPropertyDescriptor: () => ({
2437
- enumerable: true,
2438
- configurable: true
2439
- })
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)
2440
3171
  }
2441
- );
3172
+ });
2442
3173
 
2443
3174
  // src/strategy/policyProfiles.ts
2444
3175
  var profileMatches = (profile, universe, assetClass) => {
@@ -2540,12 +3271,61 @@ var buildBaseAiPayload = (signal) => {
2540
3271
  };
2541
3272
  };
2542
3273
  var defaultAiAdapter = {};
3274
+ var tickerProfileFeatureIds = new Set(
3275
+ import_aiFeatures.TICKER_PROFILE_AI_FEATURES.map(({ id }) => id)
3276
+ );
2543
3277
  var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
2544
3278
  var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
2545
3279
  var buildAiPayloadByStrategy = (signal) => {
2546
3280
  const basePayload = buildBaseAiPayload(signal);
2547
3281
  const adapter = getSignalAiAdapter(signal);
2548
- 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
+ };
2549
3329
  };
2550
3330
  var buildAiSystemPromptAddonByStrategy = (signal) => getSignalAiAdapter(signal).buildSystemPromptAddon?.({ signal }) ?? "";
2551
3331
  var buildAiHumanPromptAddonByStrategy = (signal, payload) => getSignalAiAdapter(signal).buildHumanPromptAddon?.({
@@ -2958,353 +3738,105 @@ var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
2958
3738
  { role: "system", content: systemPrompt },
2959
3739
  {
2960
3740
  role: "system",
2961
- content: `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
2962
- },
2963
- { role: "user", content: humanPrompt }
2964
- ]
2965
- });
2966
- const parsed = parseAIResponse(response.content);
2967
- const normalized = normalizeAnalysis(parsed);
2968
- if (!options.signal) {
2969
- return normalized;
2970
- }
2971
- return postProcessAiAnalysisByStrategy(
2972
- options.signal,
2973
- normalized,
2974
- options.payload
2975
- );
2976
- };
2977
- var runAiPromptLocal = async (signal, options = {}) => {
2978
- const payload = options.payload ?? buildAiPayload(signal);
2979
- const gateContext = getDeterministicAiGateContext(payload);
2980
- const signalDirection = getSignalDirection(signal);
2981
- const deterministicQuality = getDeterministicQuality(gateContext);
2982
- const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
2983
- return postProcessLocalAiAnalysisByStrategy(
2984
- signal,
2985
- {
2986
- direction: approvalAllowedNow ? signalDirection : null,
2987
- quality: deterministicQuality,
2988
- needRetest: !approvalAllowedNow,
2989
- retestPrice: null,
2990
- takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
2991
- stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
2992
- },
2993
- payload
2994
- );
2995
- };
2996
- var askAI = async (signal, options = {}) => {
2997
- const { symbol } = signal;
2998
- const payload = buildAiPayload(signal);
2999
- const content = await runAiPrompt(
3000
- {
3001
- systemPrompt: buildAiSystemPrompt(signal),
3002
- humanPrompt: buildAiHumanPrompt(signal, payload)
3003
- },
3004
- {
3005
- ...options,
3006
- signal,
3007
- payload
3008
- }
3009
- );
3010
- await (0, import_redis.setData)(import_redis.redisKeys.analysis(symbol, signal.signalId), content);
3011
- return content;
3012
- };
3013
-
3014
- // src/strategyAdapters/ml.ts
3015
- var defaultMlAdapter = {
3016
- normalizeStrategyConfig: (strategyConfig) => strategyConfig
3017
- };
3018
- var getStrategyMlAdapter = (strategy, profileId) => {
3019
- const strategyAdapter = getStrategyProfileMlAdapter(
3020
- getStrategyManifest(strategy),
3021
- profileId
3022
- );
3023
- if (!strategyAdapter) return defaultMlAdapter;
3024
- return {
3025
- ...defaultMlAdapter,
3026
- ...strategyAdapter
3027
- };
3028
- };
3029
-
3030
- // src/mlPayload.ts
3031
- var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
3032
- return getStrategyMlAdapter(
3033
- strategyName,
3034
- profileId
3035
- ).normalizeStrategyConfig?.(strategyConfig);
3036
- };
3037
- var buildMlPayload = (payload) => {
3038
- const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
3039
- const profileId = payload.signal?.policyProfileId;
3040
- const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
3041
- const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
3042
- const nextSignal = {
3043
- ...normalizedSignal,
3044
- indicators: {
3045
- ...normalizedSignal?.indicators ?? {}
3046
- }
3047
- };
3048
- const nextContext = payload.context ? {
3049
- ...payload.context,
3050
- strategyConfig: normalizeStrategyConfig(
3051
- payload.context.strategyConfig,
3052
- strategyName,
3053
- profileId
3054
- )
3055
- } : void 0;
3056
- return {
3057
- signal: nextSignal,
3058
- context: nextContext
3059
- };
3060
- };
3061
-
3062
- // src/runtimeJournal.ts
3063
- var import_node_crypto = require("crypto");
3064
- var import_constants = require("@tradejs/core/constants");
3065
- var import_time = require("@tradejs/core/time");
3066
- var import_trade = require("@tradejs/core/trade");
3067
- var import_logger3 = require("@tradejs/infra/logger");
3068
- var import_redis2 = require("@tradejs/infra/redis");
3069
- var now = () => Date.now();
3070
- var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
3071
- var calculateClosedPnl = ({
3072
- direction,
3073
- entryPrice,
3074
- exitPrice,
3075
- qty
3076
- }) => {
3077
- const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
3078
- return Number.isFinite(pnl) ? pnl : null;
3079
- };
3080
- var createRuntimeOrderId = (strategy) => {
3081
- const prefix = (0, import_trade.createRuntimeOrderLinkPrefix)(strategy);
3082
- if (prefix === "tjs-") {
3083
- return `tjs-${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
3084
- }
3085
- return `${prefix}${toRandomOrderSuffix()}`;
3086
- };
3087
- var recordRuntimeTradeOpen = async (params) => {
3088
- const { userName } = params;
3089
- if (!userName) {
3090
- return null;
3091
- }
3092
- const record = {
3093
- ...params,
3094
- entryCount: 1,
3095
- lastEntryPrice: params.entryPrice,
3096
- lastEntryQty: params.qty,
3097
- lastEntryTimestamp: params.entryTimestamp,
3098
- status: "active",
3099
- currentPrice: params.entryPrice,
3100
- currentPnl: 0,
3101
- closedPnl: null,
3102
- exitPrice: null,
3103
- exitTimestamp: null,
3104
- lastSyncedAt: now()
3105
- };
3106
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(record.entryTimestamp);
3107
- const runtimeScopeId = record.deploymentId ?? record.accountId;
3108
- try {
3109
- await Promise.all([
3110
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, record.orderId), record, {
3111
- expire: 0
3112
- }),
3113
- (0, import_redis2.setHashJsonField)(
3114
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3115
- record.orderId,
3116
- record,
3117
- { expire: 0 }
3118
- ),
3119
- (0, import_redis2.setData)(
3120
- import_redis2.redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
3121
- { orderId: record.orderId },
3122
- { expire: 0 }
3123
- )
3124
- ]);
3125
- } catch (error) {
3126
- import_logger3.logger.error(
3127
- "runtime trade open journal failed: %s %s",
3128
- record.symbol,
3129
- error?.message || String(error)
3130
- );
3131
- }
3132
- return record;
3133
- };
3134
- var recordRuntimeTradeIncrease = async (params) => {
3135
- const {
3136
- userName,
3137
- strategy,
3138
- symbol,
3139
- direction,
3140
- resultingQty,
3141
- resultingEntryPrice,
3142
- addedQty,
3143
- addedEntryPrice,
3144
- entryTimestamp,
3145
- fee,
3146
- accountId,
3147
- deploymentId
3148
- } = params;
3149
- if (!userName) {
3150
- return null;
3151
- }
3152
- const existing = await getActiveRuntimeTrade({
3153
- userName,
3154
- symbol,
3155
- accountId,
3156
- deploymentId
3741
+ content: `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
3742
+ },
3743
+ { role: "user", content: humanPrompt }
3744
+ ]
3157
3745
  });
3158
- if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
3159
- return null;
3160
- }
3161
- const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
3162
- const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
3163
- const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
3164
- const next = {
3165
- ...existing,
3166
- ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
3167
- qty: resultingQty,
3168
- entryPrice: resultingEntryPrice,
3169
- entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
3170
- lastEntryPrice: addedEntryPrice,
3171
- lastEntryQty: addedQty,
3172
- lastEntryTimestamp: entryTimestamp,
3173
- currentPrice: resultingEntryPrice,
3174
- currentPnl: 0,
3175
- fee: openFee,
3176
- openFee,
3177
- totalFee,
3178
- lastSyncedAt: now()
3179
- };
3180
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3181
- try {
3182
- await Promise.all([
3183
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, existing.orderId), next, {
3184
- expire: 0
3185
- }),
3186
- (0, import_redis2.setHashJsonField)(
3187
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3188
- existing.orderId,
3189
- next,
3190
- { expire: 0 }
3191
- )
3192
- ]);
3193
- } catch (error) {
3194
- import_logger3.logger.error(
3195
- "runtime trade increase journal failed: %s %s",
3196
- symbol,
3197
- error?.message || String(error)
3198
- );
3746
+ const parsed = parseAIResponse(response.content);
3747
+ const normalized = normalizeAnalysis(parsed);
3748
+ if (!options.signal) {
3749
+ return normalized;
3199
3750
  }
3200
- return next;
3751
+ return postProcessAiAnalysisByStrategy(
3752
+ options.signal,
3753
+ normalized,
3754
+ options.payload
3755
+ );
3201
3756
  };
3202
- var getActiveRuntimeTrade = async (params) => {
3203
- const { userName, symbol, accountId, deploymentId } = params;
3204
- if (!userName) {
3205
- return null;
3206
- }
3207
- const activeRef = await (0, import_redis2.getData)(
3208
- import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
3209
- 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
3210
3774
  );
3211
- const orderId = String(activeRef?.orderId || "").trim();
3212
- if (!orderId) {
3213
- return null;
3214
- }
3215
- const existing = await (0, import_redis2.getData)(
3216
- import_redis2.redisKeys.runtimeTrade(userName, orderId),
3217
- 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
+ }
3218
3789
  );
3219
- if (!existing || existing.status !== "active") {
3220
- await (0, import_redis2.delKey)(
3221
- import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
3222
- );
3223
- return null;
3224
- }
3225
- return existing;
3790
+ await (0, import_redis3.setData)(import_redis3.redisKeys.analysis(symbol, signal.signalId), content);
3791
+ return content;
3226
3792
  };
3227
- var markRuntimeTradeClosed = async (params) => {
3228
- const {
3229
- userName,
3230
- symbol,
3231
- strategy,
3232
- exitPrice,
3233
- exitTimestamp,
3234
- closedPnl,
3235
- exitType,
3236
- accountId,
3237
- deploymentId
3238
- } = params;
3239
- if (!userName) {
3240
- return null;
3241
- }
3242
- const existing = await getActiveRuntimeTrade({
3243
- userName,
3244
- symbol,
3245
- accountId,
3246
- deploymentId
3247
- });
3248
- if (!existing) {
3249
- return null;
3250
- }
3251
- const orderId = existing.orderId;
3252
- if (strategy && existing.strategy !== strategy) {
3253
- return null;
3254
- }
3255
- const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
3256
- const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
3257
- direction: existing.direction,
3258
- entryPrice: existing.entryPrice,
3259
- exitPrice: resolvedExitPrice,
3260
- qty: existing.qty
3261
- }) : existing.closedPnl ?? existing.currentPnl ?? null;
3262
- const next = {
3263
- ...existing,
3264
- status: "closed",
3265
- currentPrice: resolvedExitPrice,
3266
- currentPnl: resolvedClosedPnl,
3267
- closedPnl: resolvedClosedPnl,
3268
- exitPrice: resolvedExitPrice,
3269
- exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
3270
- exitType: exitType ?? existing.exitType ?? null,
3271
- 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
3272
3839
  };
3273
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3274
- const closeDayKey = (0, import_time.getRuntimeStorageDayKey)(next.exitTimestamp);
3275
- try {
3276
- await Promise.all([
3277
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, orderId), next, {
3278
- expire: import_constants.TTL_1M
3279
- }),
3280
- (0, import_redis2.setHashJsonField)(
3281
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3282
- orderId,
3283
- next,
3284
- { expire: import_constants.TTL_1M }
3285
- ),
3286
- (0, import_redis2.setHashJsonField)(
3287
- import_redis2.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
3288
- orderId,
3289
- next,
3290
- { expire: import_constants.TTL_1M }
3291
- ),
3292
- (0, import_redis2.delKey)(
3293
- import_redis2.redisKeys.runtimeActiveTrade(
3294
- userName,
3295
- symbol,
3296
- deploymentId ?? accountId
3297
- )
3298
- )
3299
- ]);
3300
- } catch (error) {
3301
- import_logger3.logger.error(
3302
- "runtime trade close journal failed: %s %s",
3303
- symbol,
3304
- error?.message || String(error)
3305
- );
3306
- }
3307
- return next;
3308
3840
  };
3309
3841
 
3310
3842
  // src/strategyHelpers/marketContextStages.ts
@@ -3316,7 +3848,7 @@ var import_logger4 = require("@tradejs/infra/logger");
3316
3848
  var import_strategies = require("@tradejs/core/strategies");
3317
3849
 
3318
3850
  // src/binanceBreadthUniverses.ts
3319
- var import_node_crypto2 = require("crypto");
3851
+ var import_node_crypto3 = require("crypto");
3320
3852
 
3321
3853
  // src/config/binanceBreadthUniverses.json
3322
3854
  var binanceBreadthUniverses_default = {
@@ -3555,7 +4087,7 @@ var BINANCE_BREADTH_UNIVERSE_KEYS = [
3555
4087
  "top50",
3556
4088
  "top100"
3557
4089
  ];
3558
- 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);
3559
4091
  var normalizeSymbols = (symbols) => symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean);
3560
4092
  var buildBinanceBreadthUniverseSnapshot = ({
3561
4093
  rankedSymbols,
@@ -4579,7 +5111,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4579
5111
  };
4580
5112
 
4581
5113
  // src/strategyHelpers/derivativesContext.ts
4582
- var import_indicators2 = require("@tradejs/core/indicators");
5114
+ var import_indicators3 = require("@tradejs/core/indicators");
4583
5115
  var import_data2 = require("@tradejs/core/data");
4584
5116
  var import_strategies3 = require("@tradejs/core/strategies");
4585
5117
  var import_constants2 = require("@tradejs/core/constants");
@@ -4613,7 +5145,7 @@ var parseLookbackMs = () => {
4613
5145
  };
4614
5146
  var withHourlyFallbackRows = (rowsByInterval) => ({
4615
5147
  "15m": rowsByInterval["15m"] ?? [],
4616
- "1h": (0, import_indicators2.buildCoinalyzeHourlyRowsWithFallback)({
5148
+ "1h": (0, import_indicators3.buildCoinalyzeHourlyRowsWithFallback)({
4617
5149
  rows15m: rowsByInterval["15m"],
4618
5150
  fallbackRows1h: rowsByInterval["1h"]
4619
5151
  })
@@ -4741,7 +5273,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4741
5273
  const targetSymbol = normalizeSymbol(signal.symbol);
4742
5274
  const lookbackMs = parseLookbackMs();
4743
5275
  const decisionTimeMs = signal.timestamp + (0, import_data2.intervalToMs)(signal.interval);
4744
- const derivativesEndMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
5276
+ const derivativesEndMs = (0, import_indicators3.getLastClosedDerivativesBarStartMs)(
4745
5277
  decisionTimeMs,
4746
5278
  "15m"
4747
5279
  );
@@ -4756,7 +5288,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4756
5288
  });
4757
5289
  return [
4758
5290
  symbol,
4759
- (0, import_indicators2.buildDerivativesContext)({
5291
+ (0, import_indicators3.buildDerivativesContext)({
4760
5292
  symbol,
4761
5293
  direction: signal.direction,
4762
5294
  timestamp: derivativesEndMs,
@@ -4783,7 +5315,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4783
5315
  lookbackMs,
4784
5316
  ...params.abortSignal ? { signal: params.abortSignal } : {}
4785
5317
  });
4786
- const context = (0, import_indicators2.buildDerivativesContext)({
5318
+ const context = (0, import_indicators3.buildDerivativesContext)({
4787
5319
  symbol: targetSymbol,
4788
5320
  direction: signal.direction,
4789
5321
  timestamp: derivativesEndMs,
@@ -4831,7 +5363,7 @@ var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWha
4831
5363
  var import_logger7 = require("@tradejs/infra/logger");
4832
5364
 
4833
5365
  // src/hyperliquidWhaleUniverse.ts
4834
- var import_node_crypto3 = require("crypto");
5366
+ var import_node_crypto4 = require("crypto");
4835
5367
 
4836
5368
  // src/config/hyperliquidPerpUniverse.json
4837
5369
  var hyperliquidPerpUniverse_default = {
@@ -5003,7 +5535,7 @@ var hyperliquidWhales_default = {
5003
5535
  };
5004
5536
 
5005
5537
  // src/hyperliquidWhaleUniverse.ts
5006
- 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);
5007
5539
  var validateUnique = (values, label) => {
5008
5540
  if (new Set(values).size !== values.length) {
5009
5541
  throw new Error(`Hyperliquid ${label} snapshot contains duplicates`);
@@ -5490,6 +6022,7 @@ var enrichSignalWithMarketContextStages = async ({
5490
6022
  })
5491
6023
  });
5492
6024
  }
6025
+ await enrichSignalWithTickerProfile(signal);
5493
6026
  const results = [];
5494
6027
  for (const stage of stages) {
5495
6028
  if (abortSignal?.aborted) break;
@@ -5537,12 +6070,12 @@ var findReplayAiAnalysis = ({
5537
6070
  direction,
5538
6071
  ai
5539
6072
  }) => {
5540
- const snapshots = ai?.replayAnalyses;
5541
- if (!Array.isArray(snapshots) || !snapshots.length) {
6073
+ const snapshots2 = ai?.replayAnalyses;
6074
+ if (!Array.isArray(snapshots2) || !snapshots2.length) {
5542
6075
  return void 0;
5543
6076
  }
5544
6077
  let best = null;
5545
- for (const snapshot2 of snapshots) {
6078
+ for (const snapshot2 of snapshots2) {
5546
6079
  if (snapshot2.symbol !== signal.symbol || snapshot2.direction !== direction || snapshot2.strategy && snapshot2.strategy !== signal.strategy) {
5547
6080
  continue;
5548
6081
  }
@@ -5702,7 +6235,9 @@ var executeEntryOrder = async ({
5702
6235
  beforePlaceOrder,
5703
6236
  recordRuntimeTrade = true,
5704
6237
  leverage,
5705
- executionLimits
6238
+ executionLimits,
6239
+ markPortfolioSubmitted,
6240
+ rejectPortfolioSubmission
5706
6241
  }) => {
5707
6242
  await beforePlaceOrder?.();
5708
6243
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
@@ -5735,12 +6270,12 @@ var executeEntryOrder = async ({
5735
6270
  if (executionLimits?.maxLossValue != null) {
5736
6271
  signal.riskDecision = {
5737
6272
  stage: "order_plan",
5738
- status: "approved",
5739
- reason: "PLANNED_LOSS_WITHIN_MAX_LOSS_VALUE",
6273
+ status: economics2.riskExceeded ? "warning" : "approved",
6274
+ reason: economics2.riskExceeded ? "ENTRY_RISK_EXCEEDS_MAX_LOSS_AT_SUBMISSION" : "PLANNED_LOSS_WITHIN_MAX_LOSS_VALUE",
5740
6275
  plannedLossValue: economics2.plannedLossValue,
5741
6276
  maxLossValue: executionLimits.maxLossValue,
5742
6277
  limitUtilization: economics2.plannedLossValue / executionLimits.maxLossValue,
5743
- enforced: true
6278
+ enforced: false
5744
6279
  };
5745
6280
  }
5746
6281
  }
@@ -5775,7 +6310,13 @@ var executeEntryOrder = async ({
5775
6310
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
5776
6311
  orderId,
5777
6312
  signal,
5778
- ...beforeSubmit ? { beforeSubmit } : {}
6313
+ ...rejectPortfolioSubmission ? { onRejected: rejectPortfolioSubmission } : {},
6314
+ ...beforeSubmit || markPortfolioSubmitted ? {
6315
+ beforeSubmit: async (context) => {
6316
+ await beforeSubmit?.(context);
6317
+ await markPortfolioSubmitted?.(context.qty);
6318
+ }
6319
+ } : {}
5779
6320
  });
5780
6321
  const orderAckTime = Date.now();
5781
6322
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
@@ -6766,7 +7307,7 @@ var handleProtectDecision = async ({
6766
7307
  }
6767
7308
  return decision.code;
6768
7309
  };
6769
- var executeEntryDecision = async ({
7310
+ var executeAdmittedEntryDecision = async ({
6770
7311
  connector,
6771
7312
  symbol,
6772
7313
  decision,
@@ -6780,7 +7321,9 @@ var executeEntryDecision = async ({
6780
7321
  ai,
6781
7322
  recordRuntimeJournal,
6782
7323
  invokeStageHooks,
6783
- notifyRuntimeError
7324
+ notifyRuntimeError,
7325
+ markPortfolioSubmitted,
7326
+ rejectPortfolioSubmission
6784
7327
  }) => {
6785
7328
  const signal = decision.signal;
6786
7329
  const beforePlaceOrder = async () => {
@@ -6827,6 +7370,8 @@ var executeEntryDecision = async ({
6827
7370
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
6828
7371
  signal,
6829
7372
  beforePlaceOrder,
7373
+ markPortfolioSubmitted,
7374
+ rejectPortfolioSubmission,
6830
7375
  recordRuntimeTrade: recordRuntimeJournal,
6831
7376
  executionLimits: resolveEntryExecutionLimits(
6832
7377
  hookCtx.strategyConfig,
@@ -6967,6 +7512,37 @@ var executeEntryDecision = async ({
6967
7512
  }
6968
7513
  return signal ?? decision.code;
6969
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
+ }
6970
7546
 
6971
7547
  // src/strategyRuntime.ts
6972
7548
  var cloneWithPropertyDescriptors = (value) => Object.create(
@@ -6994,6 +7570,7 @@ var createStrategyRuntime = ({
6994
7570
  };
6995
7571
  const creator = async ({
6996
7572
  userName,
7573
+ connectorName,
6997
7574
  config: baseConfig,
6998
7575
  symbol,
6999
7576
  universe: requestedUniverse,
@@ -7032,6 +7609,17 @@ var createStrategyRuntime = ({
7032
7609
  const projectConfig = await loadTradejsConfig(projectRoot);
7033
7610
  const projectHooks = projectConfig.hooks;
7034
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;
7035
7623
  const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
7036
7624
  const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
7037
7625
  if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
@@ -7069,6 +7657,9 @@ var createStrategyRuntime = ({
7069
7657
  });
7070
7658
  const strategyPolicyProfile = getPolicyProfile();
7071
7659
  const indicatorPeriods = (0, import_strategies6.buildDefaultIndicatorPeriods)(config);
7660
+ const strategyConfig = { ...config };
7661
+ delete strategyConfig.RECORD_RUNTIME_TRADES;
7662
+ delete strategyConfig.SIMULATE_ORDERS;
7072
7663
  const hookBase = {
7073
7664
  connector,
7074
7665
  strategyName,
@@ -7079,7 +7670,7 @@ var createStrategyRuntime = ({
7079
7670
  accountId,
7080
7671
  deploymentId,
7081
7672
  policyProfileId: strategyPolicyProfile?.id ?? requestedPolicyProfileId,
7082
- strategyConfig: config,
7673
+ strategyConfig,
7083
7674
  env,
7084
7675
  isConfigFromBacktest
7085
7676
  };
@@ -7147,7 +7738,7 @@ var createStrategyRuntime = ({
7147
7738
  );
7148
7739
  }
7149
7740
  };
7150
- const invokeHook = async (stage, hook, params, errorContext = {}) => {
7741
+ const invokeHook = async (stage, hook, params, errorContext = {}, failureResult) => {
7151
7742
  if (!hook) {
7152
7743
  return void 0;
7153
7744
  }
@@ -7167,17 +7758,18 @@ var createStrategyRuntime = ({
7167
7758
  entry: errorContext.entry,
7168
7759
  market: errorContext.market
7169
7760
  });
7170
- return void 0;
7761
+ return failureResult;
7171
7762
  }
7172
7763
  };
7173
- const invokeProjectHooks = async (stage, params, errorContext = {}) => {
7764
+ const invokeProjectHooks = async (stage, params, errorContext = {}, failureResult) => {
7174
7765
  const results = [];
7175
7766
  for (const hook of getProjectHookList(stage)) {
7176
7767
  const result = await invokeHook(
7177
7768
  stage,
7178
7769
  hook,
7179
7770
  params,
7180
- errorContext
7771
+ errorContext,
7772
+ failureResult
7181
7773
  );
7182
7774
  if (result !== void 0) {
7183
7775
  results.push(result);
@@ -7192,10 +7784,12 @@ var createStrategyRuntime = ({
7192
7784
  return invokeHook(stage, hook, params, errorContext);
7193
7785
  };
7194
7786
  const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
7787
+ const failureResult = stage === "beforeEntryGate" ? { allow: false, reason: "ERROR" } : void 0;
7195
7788
  const projectResults = await invokeProjectHooks(
7196
7789
  stage,
7197
7790
  params,
7198
- errorContext
7791
+ errorContext,
7792
+ failureResult
7199
7793
  );
7200
7794
  const projectBlock = projectResults.find(
7201
7795
  (result) => result?.allow === false
@@ -7207,7 +7801,8 @@ var createStrategyRuntime = ({
7207
7801
  stage,
7208
7802
  hook,
7209
7803
  params,
7210
- errorContext
7804
+ errorContext,
7805
+ failureResult
7211
7806
  );
7212
7807
  };
7213
7808
  const applyProjectAfterCoreDecisionHooks = async ({
@@ -7323,6 +7918,20 @@ var createStrategyRuntime = ({
7323
7918
  interval: decisionInterval
7324
7919
  }) => {
7325
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
+ }
7326
7935
  if (!coreContextRequirements.has("hyperliquidWhales")) {
7327
7936
  return baseContext;
7328
7937
  }
@@ -7353,11 +7962,8 @@ var createStrategyRuntime = ({
7353
7962
  return enrichedBaseContext;
7354
7963
  }
7355
7964
  });
7356
- const coreConfig = { ...config };
7357
- delete coreConfig.RECORD_RUNTIME_TRADES;
7358
- delete coreConfig.SIMULATE_ORDERS;
7359
7965
  const core = await createCore({
7360
- config: coreConfig,
7966
+ config: strategyConfig,
7361
7967
  data,
7362
7968
  strategyApi,
7363
7969
  indicatorsState
@@ -7493,6 +8099,8 @@ var createStrategyRuntime = ({
7493
8099
  runtime: pending2.runtime
7494
8100
  });
7495
8101
  return executeEntryDecision({
8102
+ portfolio,
8103
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
7496
8104
  connector,
7497
8105
  symbol,
7498
8106
  decision: pending2.decision,
@@ -7510,6 +8118,10 @@ var createStrategyRuntime = ({
7510
8118
  });
7511
8119
  };
7512
8120
  const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
8121
+ if (portfolioContext)
8122
+ await portfolioContext(
8123
+ env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now()
8124
+ );
7513
8125
  appendCurrentMarketData(candle, btcCandle, options.ethCandle);
7514
8126
  const ethCandle = resolveEthCandle(candle, options.ethCandle);
7515
8127
  indicatorsState.setCurrentBar(candle, btcCandle, ethCandle);
@@ -7680,6 +8292,13 @@ var createStrategyRuntime = ({
7680
8292
  });
7681
8293
  const signal = decision.signal;
7682
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
+ };
7683
8302
  if (runtimeLineage) signal.runtimeLineage = runtimeLineage;
7684
8303
  if (universe) signal.universe = universe;
7685
8304
  if (assetClass) signal.assetClass = assetClass;
@@ -7919,6 +8538,8 @@ var createStrategyRuntime = ({
7919
8538
  });
7920
8539
  }
7921
8540
  return executeEntryDecision({
8541
+ portfolio,
8542
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
7922
8543
  connector,
7923
8544
  symbol,
7924
8545
  decision,
@@ -8701,7 +9322,7 @@ var resolveExecutionCosts = async (params) => {
8701
9322
  };
8702
9323
 
8703
9324
  // src/testConnector.ts
8704
- var import_node_crypto4 = require("crypto");
9325
+ var import_node_crypto5 = require("crypto");
8705
9326
  var import_constants8 = require("@tradejs/core/constants");
8706
9327
  var import_backtest2 = require("@tradejs/core/backtest");
8707
9328
  var import_trade2 = require("@tradejs/core/trade");
@@ -9174,7 +9795,7 @@ var createTestConnector = (connector, context) => {
9174
9795
  },
9175
9796
  kline: async (options) => connector.kline(options),
9176
9797
  getResult: async () => {
9177
- const orderLogId = (0, import_node_crypto4.randomUUID)().slice(-12);
9798
+ const orderLogId = (0, import_node_crypto5.randomUUID)().slice(-12);
9178
9799
  const fullStat = fastMode ? (0, import_backtest2.calculateStatsFull)(positionLog) : null;
9179
9800
  return {
9180
9801
  stat: fullStat ? {
@@ -10590,19 +11211,19 @@ var prepareTestingData = async (params) => {
10590
11211
  if (shouldLoadExecutionCandles && !cachedExecutionBtcData) {
10591
11212
  state.btcKlineCache.set(executionBtcCacheKey, executionBtcDataRaw);
10592
11213
  }
10593
- 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);
10594
11215
  const { data, btcData } = filterClosedAlignedCandles(
10595
11216
  aligned.alignedCoinCandles,
10596
11217
  aligned.alignedBtcCandles,
10597
11218
  interval
10598
11219
  );
10599
- const btcBinanceData = universe === "tradfi" ? [] : (0, import_indicators3.alignSortedCandlesByTimestamp)(data, btcBinanceDataRaw).alignedBtcCandles;
10600
- const btcCoinbaseData = universe === "tradfi" ? [] : (0, import_indicators3.alignSortedCandlesByTimestamp)(data, btcCoinbaseDataRaw).alignedBtcCandles;
10601
- 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;
10602
11223
  const alignedExecution = shouldLoadExecutionCandles ? universe === "tradfi" ? {
10603
11224
  alignedCoinCandles: executionDataRaw,
10604
11225
  alignedBtcCandles: executionDataRaw
10605
- } : (0, import_indicators3.alignSortedCandlesByTimestamp)(executionDataRaw, executionBtcDataRaw) : { alignedCoinCandles: [], alignedBtcCandles: [] };
11226
+ } : (0, import_indicators4.alignSortedCandlesByTimestamp)(executionDataRaw, executionBtcDataRaw) : { alignedCoinCandles: [], alignedBtcCandles: [] };
10606
11227
  const { data: backtestExecutionData, btcData: backtestExecutionBtcData } = filterClosedAlignedCandles(
10607
11228
  alignedExecution.alignedCoinCandles,
10608
11229
  alignedExecution.alignedBtcCandles,