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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -789,1182 +789,821 @@ __export(runtimeStrategies_exports, {
789
789
  });
790
790
  module.exports = __toCommonJS(runtimeStrategies_exports);
791
791
  var import_jev8 = require("@tradejs/core/jev");
792
- var import_node_crypto4 = require("crypto");
792
+ var import_node_crypto5 = require("crypto");
793
793
  var import_promises2 = require("fs/promises");
794
794
  var import_node_path2 = __toESM(require("path"));
795
795
  var import_runtimeControls = require("@tradejs/infra/runtimeControls");
796
796
  var import_tradingAccounts = require("@tradejs/infra/tradingAccounts");
797
797
 
798
- // src/strategyRuntime.ts
799
- var import_jev6 = require("@tradejs/core/jev");
798
+ // src/strategy/portfolioRuntime.ts
799
+ var import_node_crypto2 = require("crypto");
800
+ var import_portfolio2 = require("@tradejs/core/portfolio");
801
+ var import_redis2 = require("@tradejs/infra/redis");
800
802
 
801
- // src/jev.ts
802
- var import_promises = __toESM(require("fs/promises"));
803
- var import_node_path = __toESM(require("path"));
804
- var import_jev4 = require("@tradejs/core/jev");
805
- var import_jev5 = require("@tradejs/infra/jev");
806
- var import_userSettings = require("@tradejs/infra/userSettings");
807
- init_jevInput();
808
- init_jevInput();
809
- init_jevTraining();
810
- var resolveJevProvider = async (userName) => {
811
- const settings = await (0, import_userSettings.getUserSettings)(userName);
812
- const provider = {
813
- endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
814
- model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
815
- };
816
- (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
817
- if (!settings.JEV_API_KEY)
818
- throw new Error("Configure the Jev API key in Account settings");
819
- return provider;
820
- };
821
- var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
822
- var pending = /* @__PURE__ */ new Map();
823
- var pendingResponses = /* @__PURE__ */ new Map();
824
- var evaluateJevInput = async ({
825
- input,
826
- config,
827
- userName,
828
- projectRoot
829
- }) => {
830
- config = (0, import_jev4.parseJevConfig)(config);
831
- const inputHash = (0, import_jev5.jevHash)(input);
832
- const questions = questionsForJevInput(input);
833
- const questionsHash = (0, import_jev5.jevHash)(questions);
834
- const base = {
835
- questionSet: import_jev4.JEV_QUESTION_SET,
836
- schema: "tradejs-signal-assessment/v5",
837
- source: config.source,
838
- mode: config.mode,
839
- inputHash,
840
- questionsHash,
841
- geometryStatus: input.geometryStatus,
842
- levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
843
- };
844
- if (!input.questions.length)
845
- return {
846
- assessment: {
847
- ...base,
848
- status: "unavailable",
849
- model: "",
850
- scores: emptyScores(),
851
- confidence: emptyScores()
852
- }
853
- };
854
- if (config.source === "local") {
855
- const contents = await import_promises.default.readFile(
856
- import_node_path.default.resolve(projectRoot, config.modelFile),
857
- "utf8"
858
- );
859
- if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
860
- throw new Error("Local Jev gate checksum mismatch");
861
- const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
862
- const model = validateJevGateModel2(JSON.parse(contents));
863
- if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
864
- throw new Error("Local Jev gate strategy or question version mismatch");
865
- const scores = Object.fromEntries(
866
- import_jev4.JEV_DIMENSIONS.map((key) => [
867
- key,
868
- input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
869
- ])
870
- );
871
- return {
872
- assessment: {
873
- ...base,
874
- status: "available",
875
- model: config.modelSha256,
876
- scores,
877
- confidence: emptyScores()
803
+ // src/portfolio.ts
804
+ var import_portfolio = require("@tradejs/core/portfolio");
805
+ function createPortfolioCoordinator({
806
+ policy,
807
+ strategy,
808
+ store,
809
+ execution
810
+ }) {
811
+ const fingerprint3 = (0, import_portfolio.portfolioFingerprint)(policy, strategy);
812
+ let cache;
813
+ async function transact(now2, change, force = false) {
814
+ for (let attempt = 0; attempt < 16; attempt++) {
815
+ const previous = await store.read();
816
+ if (previous && previous.fingerprint !== fingerprint3)
817
+ throw new Error(
818
+ "Portfolio policy changed under existing state; reconcile the persisted scope before changing configuration"
819
+ );
820
+ const base = previous ?? (0, import_portfolio.createPortfolioState)(fingerprint3);
821
+ const bucket = Math.floor(now2 / policy.observationIntervalMs);
822
+ const observation = !force && cache?.revision === base.revision && cache.bucket === bucket ? cache.observation : await execution.observe(now2, base.reservations);
823
+ const reconciled = (0, import_portfolio.reconcilePortfolio)(
824
+ policy,
825
+ strategy,
826
+ base,
827
+ observation
828
+ );
829
+ for (const intent of base.closing) {
830
+ if (!observation.positions.some(
831
+ (p) => p.symbol === intent.position.symbol
832
+ ))
833
+ await execution.confirmClosed?.(intent, observation.asOf);
878
834
  }
879
- };
880
- }
881
- const provider = config.provider ?? await resolveJevProvider(userName);
882
- const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
883
- const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
884
- const file = import_node_path.default.join(dir, "records", `${id}.json`);
885
- const getRecord = async () => {
886
- const existing = await (0, import_jev5.readJevArtifact)(file);
887
- if (existing) {
888
- if (existing.schema !== "tradejs-jev-record/v4" || existing.id !== id || existing.inputHash !== inputHash || existing.questionsHash !== questionsHash || (0, import_jev5.jevHash)(existing.input) !== inputHash || (0, import_jev5.jevHash)(existing.provider) !== (0, import_jev5.jevHash)(provider))
889
- throw new Error("Jev recording identity mismatch");
890
- (0, import_jev5.validateJevResponse)(existing.response, input.questions);
891
- for (const key of import_jev4.JEV_DIMENSIONS) {
892
- const expected = input.questions.includes(key) ? existing.response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null;
893
- if (existing.scores[key] !== expected)
894
- throw new Error("Jev recording scores do not match the response");
835
+ const changed = change(reconciled, observation);
836
+ const next = { ...changed.state, revision: base.revision + 1 };
837
+ if (await store.compare(previous, next)) {
838
+ cache = { revision: next.revision, bucket, observation };
839
+ return { state: next, observation, result: changed.result };
895
840
  }
896
- return existing;
841
+ cache = void 0;
897
842
  }
898
- if (config.source === "recorded")
899
- throw new Error(
900
- `Missing Jev recording ${id} for ${input.strategy} ${input.symbol} at ${new Date(input.timestamp).toISOString()}. Recorded mode requires the same signal-time facts and provider; a shorter backtest can change warm-up indicators or prior trade state.`
843
+ throw new Error("Portfolio state contention; entry was not submitted");
844
+ }
845
+ async function tick(now2, force = false) {
846
+ const claimed = await transact(
847
+ now2,
848
+ (state, observation) => {
849
+ const commands = (0, import_portfolio.portfolioCloseCommands)(policy, state, observation);
850
+ return {
851
+ state: {
852
+ ...state,
853
+ closing: state.closing.map(
854
+ (c) => commands.some((x) => x.intent.positionId === c.positionId) ? { ...c, lastAttemptAt: observation.asOf } : c
855
+ )
856
+ },
857
+ result: commands
858
+ };
859
+ },
860
+ force
861
+ );
862
+ for (const command of claimed.result)
863
+ await execution.close(
864
+ command.position,
865
+ command.intent,
866
+ claimed.observation.asOf
901
867
  );
902
- const settings = await (0, import_userSettings.getUserSettings)(userName);
903
- if (!settings.JEV_API_KEY)
904
- throw new Error("Configure the Jev API key in Account settings");
905
- const apiKey = settings.JEV_API_KEY;
906
- const configuredProvider = await resolveJevProvider(userName);
907
- if (configuredProvider.endpoint !== provider.endpoint)
908
- throw new Error(
909
- "Jev account endpoint changed; update the frozen provider configuration"
868
+ if (claimed.result.length) {
869
+ cache = void 0;
870
+ const refreshed = await transact(
871
+ now2,
872
+ (state) => ({ state, result: void 0 }),
873
+ true
874
+ );
875
+ return (0, import_portfolio.buildPortfolioContext)(
876
+ policy,
877
+ strategy,
878
+ refreshed.state,
879
+ refreshed.observation
910
880
  );
911
- const started = Date.now();
912
- const state = stateForJevInput(input);
913
- const responseId = (0, import_jev5.jevHash)({
914
- state,
915
- questionsHash,
916
- provider,
917
- user: (0, import_jev5.jevHash)(userName)
918
- });
919
- const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
920
- let responsePromise = pendingResponses.get(responseFile);
921
- if (!responsePromise) {
922
- responsePromise = (async () => {
923
- const saved = await (0, import_jev5.readJevArtifact)(responseFile);
924
- if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
925
- const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
926
- await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
927
- return fresh;
928
- })();
929
- pendingResponses.set(responseFile, responsePromise);
930
- }
931
- let response;
932
- try {
933
- response = await responsePromise;
934
- } finally {
935
- pendingResponses.delete(responseFile);
936
881
  }
937
- const scores = Object.fromEntries(
938
- import_jev4.JEV_DIMENSIONS.map((key) => [
939
- key,
940
- input.questions.includes(key) ? response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null
941
- ])
882
+ return (0, import_portfolio.buildPortfolioContext)(
883
+ policy,
884
+ strategy,
885
+ claimed.state,
886
+ claimed.observation
942
887
  );
943
- const value = {
944
- schema: "tradejs-jev-record/v4",
945
- id,
946
- inputHash,
947
- questionsHash,
948
- provider,
949
- input,
950
- response,
951
- scores,
952
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
953
- elapsedMs: Date.now() - started
954
- };
955
- await (0, import_jev5.writeJevArtifact)(file, value);
956
- return await (0, import_jev5.readJevArtifact)(file);
957
- };
958
- const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
959
- let promise = pending.get(pendingKey);
960
- if (!promise) {
961
- promise = getRecord();
962
- pending.set(pendingKey, promise);
963
888
  }
964
- let record;
965
- try {
966
- record = await promise;
967
- } finally {
968
- pending.delete(pendingKey);
889
+ async function updateReservation(now2, id, submitted, qty) {
890
+ return transact(
891
+ now2,
892
+ (s, o) => {
893
+ const exists = s.reservations.some((r) => r.id === id);
894
+ const blocked = submitted && (!exists || s.closing.length > 0 || o.asOf <= s.blockedThrough);
895
+ return {
896
+ state: {
897
+ ...s,
898
+ reservations: s.reservations.flatMap(
899
+ (r) => r.id !== id ? [r] : submitted && !blocked ? [{ ...r, submitted: true, qty: qty ?? r.qty }] : []
900
+ )
901
+ },
902
+ result: !blocked
903
+ };
904
+ },
905
+ true
906
+ );
969
907
  }
970
908
  return {
971
- record,
972
- assessment: {
973
- ...base,
974
- status: "available",
975
- recordId: id,
976
- model: record.response.model,
977
- scores: record.scores,
978
- confidence: Object.fromEntries(
979
- import_jev4.JEV_DIMENSIONS.map((key) => [
980
- key,
981
- record.response.answers[key]?.confidence ?? null
982
- ])
983
- )
909
+ async context(now2) {
910
+ const state = await store.read() ?? (0, import_portfolio.createPortfolioState)(fingerprint3);
911
+ if (state.fingerprint !== fingerprint3)
912
+ throw new Error("Portfolio policy changed under existing state");
913
+ const observation = await execution.observe(now2, state.reservations);
914
+ (0, import_portfolio.validatePortfolioObservation)(observation);
915
+ return (0, import_portfolio.buildPortfolioContext)(policy, strategy, state, observation);
916
+ },
917
+ tick,
918
+ async submit(now2, entry, send) {
919
+ await tick(now2);
920
+ const admitted = await transact(
921
+ now2,
922
+ (s, o) => {
923
+ const r = (0, import_portfolio.reservePortfolioEntry)(policy, strategy, s, o, {
924
+ ...entry,
925
+ submitted: false,
926
+ oppositeHandled: false
927
+ });
928
+ return { state: r.state, result: r.reason };
929
+ },
930
+ true
931
+ );
932
+ if (admitted.result) return { reason: admitted.result };
933
+ let submitted = false;
934
+ let retry = 0;
935
+ let attemptId = entry.id;
936
+ let needsReservation = false;
937
+ try {
938
+ const value = await send(
939
+ async (qty) => {
940
+ if (!Number.isFinite(qty) || qty <= 0)
941
+ throw new Error("Invalid submitted portfolio quantity");
942
+ if (needsReservation) {
943
+ attemptId = `${entry.id.slice(0, 30)}r${++retry}`;
944
+ const admission = await transact(
945
+ now2,
946
+ (s, o) => {
947
+ const r = (0, import_portfolio.reservePortfolioEntry)(policy, strategy, s, o, {
948
+ ...entry,
949
+ id: attemptId,
950
+ qty,
951
+ submitted: true,
952
+ oppositeHandled: false
953
+ });
954
+ return { state: r.state, result: r.reason };
955
+ },
956
+ true
957
+ );
958
+ if (admission.result) throw new Error(admission.result);
959
+ needsReservation = false;
960
+ submitted = true;
961
+ return attemptId;
962
+ }
963
+ const update = await updateReservation(now2, attemptId, true, qty);
964
+ if (!update.result)
965
+ throw new Error("PORTFOLIO_SUBMISSION_NO_LONGER_ADMITTED");
966
+ submitted = true;
967
+ return attemptId;
968
+ },
969
+ async (retrying = false) => {
970
+ await transact(
971
+ now2,
972
+ (s) => ({
973
+ state: {
974
+ ...s,
975
+ reservations: s.reservations.filter(
976
+ (r) => r.id !== attemptId
977
+ ),
978
+ completedSignals: {
979
+ ...Object.fromEntries(
980
+ Object.entries(s.completedSignals).filter(
981
+ ([id]) => !retrying || id !== entry.signalId
982
+ )
983
+ ),
984
+ ...retrying ? {} : { [entry.signalId]: now2 }
985
+ }
986
+ },
987
+ result: void 0
988
+ }),
989
+ true
990
+ );
991
+ submitted = false;
992
+ needsReservation = retrying;
993
+ }
994
+ );
995
+ if (!submitted) await updateReservation(now2, attemptId, false);
996
+ else await tick(now2, true);
997
+ return { value };
998
+ } catch (error) {
999
+ if (!submitted) await updateReservation(now2, attemptId, false);
1000
+ throw error;
1001
+ }
984
1002
  }
985
1003
  };
986
- };
987
- var assessSignalWithJev = async ({
988
- signal,
989
- config,
990
- userName,
991
- projectRoot,
992
- strict,
993
- scope = "runtime"
994
- }) => {
995
- let assessment;
996
- try {
997
- const input = buildJevInput(signal);
998
- ({ assessment } = await evaluateJevInput({
999
- input,
1000
- config,
1001
- userName,
1002
- projectRoot
1003
- }));
1004
- } catch (error) {
1005
- if (strict) throw error;
1006
- assessment = {
1007
- questionSet: import_jev4.JEV_QUESTION_SET,
1008
- schema: "tradejs-signal-assessment/v5",
1009
- source: config.source,
1010
- mode: config.mode,
1011
- status: "unavailable",
1012
- inputHash: "",
1013
- questionsHash: "",
1014
- geometryStatus: "absent",
1015
- levelsValid: null,
1016
- model: config.provider?.model ?? config.modelSha256 ?? "",
1017
- scores: emptyScores(),
1018
- confidence: emptyScores()
1019
- };
1020
- }
1021
- signal.assessment = assessment;
1022
- signal.additionalIndicators = {
1023
- ...signal.additionalIndicators,
1024
- jev: (0, import_jev4.jevAssessmentFeatures)(assessment)
1004
+ }
1005
+ function createMemoryPortfolioStore() {
1006
+ let state = null;
1007
+ return {
1008
+ read: async () => state === null ? null : structuredClone(state),
1009
+ compare: async (expected, next) => {
1010
+ if ((state?.revision ?? -1) !== (expected?.revision ?? -1)) return false;
1011
+ state = structuredClone(next);
1012
+ return true;
1013
+ }
1025
1014
  };
1026
- const identity = {
1027
- signalId: signal.signalId,
1028
- strategy: signal.strategy,
1029
- symbol: signal.symbol,
1030
- timestamp: signal.timestamp,
1031
- scope,
1032
- user: (0, import_jev5.jevHash)(userName),
1033
- config,
1034
- assessment
1035
- };
1036
- await (0, import_jev5.writeJevArtifact)(
1037
- import_node_path.default.resolve(
1038
- projectRoot,
1039
- config.recordsDir ?? "data/ai/jev",
1040
- "decisions",
1041
- `${(0, import_jev5.jevHash)(identity)}.json`
1042
- ),
1043
- identity
1044
- );
1045
- return assessment;
1046
- };
1047
-
1048
- // src/strategyRuntime.ts
1049
- var import_constants5 = require("@tradejs/core/constants");
1050
- var import_strategies6 = require("@tradejs/core/strategies");
1051
- var import_logger11 = require("@tradejs/infra/logger");
1052
-
1053
- // src/strategyHelpers/runtime.ts
1054
- var import_logger9 = require("@tradejs/infra/logger");
1055
- var import_constants3 = require("@tradejs/core/constants");
1056
- var import_ml2 = require("@tradejs/infra/ml");
1015
+ }
1057
1016
 
1058
- // src/strategy/entrySubmissionValidation.ts
1017
+ // src/runtimeJournal.ts
1018
+ var import_node_crypto = require("crypto");
1019
+ var import_constants = require("@tradejs/core/constants");
1020
+ var import_time = require("@tradejs/core/time");
1021
+ var import_trade = require("@tradejs/core/trade");
1059
1022
  var import_logger = require("@tradejs/infra/logger");
1060
-
1061
- // src/strategy/entryExecutionValidation.ts
1062
- var EntrySubmissionValidationError = class extends Error {
1063
- constructor() {
1064
- super(...arguments);
1065
- this.name = "EntrySubmissionValidationError";
1066
- }
1067
- };
1068
- var resolveEntryExecutionLimits = (config, direction) => {
1069
- const side = config[direction];
1070
- const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
1071
- const maximum = config.MAX_LOSS_VALUE;
1072
- if (minimum == null && maximum == null) return void 0;
1073
- const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
1074
- const maxLossValue = maximum == null ? void 0 : Number(maximum);
1075
- const feeRate = Number(config.RISK_FEE_RATE ?? 0);
1076
- const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
1077
- const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
1078
- const slippageBps = slippage + impact;
1079
- if (minimumNetRiskRatio != null && (!Number.isFinite(minimumNetRiskRatio) || minimumNetRiskRatio < 0) || maxLossValue != null && (!Number.isFinite(maxLossValue) || maxLossValue <= 0) || !Number.isFinite(feeRate) || feeRate < 0 || feeRate >= 1 || !Number.isFinite(slippage) || slippage < 0 || !Number.isFinite(impact) || impact < 0 || !Number.isFinite(slippageBps) || slippageBps < 0 || slippageBps >= 1e4)
1080
- throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
1081
- return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
1082
- };
1083
- var validateEntryExecution = ({
1023
+ var import_redis = require("@tradejs/infra/redis");
1024
+ var now = () => Date.now();
1025
+ var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
1026
+ var calculateClosedPnl = ({
1084
1027
  direction,
1085
- price,
1086
- qty,
1087
- stopLossPrice,
1088
- takeProfitPrice,
1089
- limits
1028
+ entryPrice,
1029
+ exitPrice,
1030
+ qty
1090
1031
  }) => {
1091
- if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
1092
- throw new EntrySubmissionValidationError(
1093
- "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1094
- );
1095
- const side = direction === "LONG" ? 1 : -1;
1096
- if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
1097
- throw new EntrySubmissionValidationError(
1098
- "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
1099
- );
1100
- const slippage = limits.slippageBps / 1e4;
1101
- const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
1102
- const entryCost = cost(price, side);
1103
- const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
1104
- const rewardPerUnit = Math.max(
1105
- 0,
1106
- Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
1107
- );
1108
- const netRiskRatio = rewardPerUnit / lossPerUnit;
1109
- const plannedLossValue = qty * lossPerUnit;
1110
- if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
1111
- throw new EntrySubmissionValidationError(
1112
- "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
1113
- );
1114
- if (limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio)
1115
- throw new EntrySubmissionValidationError(
1116
- "ENTRY_RR_BELOW_MIN_AT_SUBMISSION"
1117
- );
1118
- const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
1119
- return { netRiskRatio, plannedLossValue, riskExceeded };
1032
+ const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
1033
+ return Number.isFinite(pnl) ? pnl : null;
1120
1034
  };
1121
-
1122
- // src/strategy/entrySubmissionValidation.ts
1123
- var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
1124
- var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
1125
- var getOrderArrivalSnapshot = async ({
1126
- connector,
1127
- symbol,
1128
- requireTimestamp = false
1129
- }) => {
1130
- if (typeof connector.getTopOfBookTicker !== "function") {
1131
- return {
1132
- arrivalSnapshotTime: Date.now(),
1133
- arrivalSource: "unavailable",
1134
- bid: null,
1135
- ask: null,
1136
- arrivalMid: null,
1137
- spreadBps: null
1138
- };
1035
+ var createRuntimeOrderId = (strategy) => {
1036
+ const prefix = (0, import_trade.createRuntimeOrderLinkPrefix)(strategy);
1037
+ if (prefix === "tjs-") {
1038
+ return `tjs-${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
1139
1039
  }
1040
+ return `${prefix}${toRandomOrderSuffix()}`;
1041
+ };
1042
+ var recordRuntimeTradeOpen = async (params) => {
1043
+ const { userName } = params;
1044
+ if (!userName) {
1045
+ return null;
1046
+ }
1047
+ const record = {
1048
+ ...params,
1049
+ entryCount: 1,
1050
+ lastEntryPrice: params.entryPrice,
1051
+ lastEntryQty: params.qty,
1052
+ lastEntryTimestamp: params.entryTimestamp,
1053
+ status: "active",
1054
+ currentPrice: params.entryPrice,
1055
+ currentPnl: 0,
1056
+ closedPnl: null,
1057
+ exitPrice: null,
1058
+ exitTimestamp: null,
1059
+ lastSyncedAt: now()
1060
+ };
1061
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(record.entryTimestamp);
1062
+ const runtimeScopeId = record.deploymentId ?? record.accountId;
1140
1063
  try {
1141
- const ticker = await connector.getTopOfBookTicker(symbol);
1142
- const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
1143
- const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
1144
- const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
1145
- const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
1146
- const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
1147
- const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
1148
- return {
1149
- arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
1150
- arrivalSource: "top_of_book",
1151
- bid,
1152
- ask,
1153
- arrivalMid,
1154
- spreadBps
1155
- };
1064
+ await Promise.all([
1065
+ (0, import_redis.setData)(import_redis.redisKeys.runtimeTrade(userName, record.orderId), record, {
1066
+ expire: 0
1067
+ }),
1068
+ (0, import_redis.setHashJsonField)(
1069
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1070
+ record.orderId,
1071
+ record,
1072
+ { expire: 0 }
1073
+ ),
1074
+ (0, import_redis.setData)(
1075
+ import_redis.redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
1076
+ { orderId: record.orderId },
1077
+ { expire: 0 }
1078
+ )
1079
+ ]);
1156
1080
  } catch (error) {
1157
- import_logger.logger.warn(
1158
- "runtime order arrival snapshot failed: %s %s",
1159
- symbol,
1081
+ import_logger.logger.error(
1082
+ "runtime trade open journal failed: %s %s",
1083
+ record.symbol,
1160
1084
  error?.message || String(error)
1161
1085
  );
1162
- return {
1163
- arrivalSnapshotTime: Date.now(),
1164
- arrivalSource: "top_of_book_error",
1165
- bid: null,
1166
- ask: null,
1167
- arrivalMid: null,
1168
- spreadBps: null
1169
- };
1170
1086
  }
1087
+ return record;
1171
1088
  };
1172
- var validateEntryProtectionAtArrival = ({
1173
- direction,
1174
- signalPrice,
1175
- bid,
1176
- ask,
1177
- arrivalMid,
1178
- takeProfits,
1179
- stopLossPrice
1180
- }) => {
1181
- const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
1182
- const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
1183
- if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
1184
- throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
1185
- }
1186
- for (const takeProfit of takeProfits) {
1187
- if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
1188
- throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
1189
- }
1089
+ var recordRuntimeTradeIncrease = async (params) => {
1090
+ const {
1091
+ userName,
1092
+ strategy,
1093
+ symbol,
1094
+ direction,
1095
+ resultingQty,
1096
+ resultingEntryPrice,
1097
+ addedQty,
1098
+ addedEntryPrice,
1099
+ entryTimestamp,
1100
+ fee,
1101
+ accountId,
1102
+ deploymentId
1103
+ } = params;
1104
+ if (!userName) {
1105
+ return null;
1190
1106
  }
1191
- if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
1192
- throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
1107
+ const existing = await getActiveRuntimeTrade({
1108
+ userName,
1109
+ symbol,
1110
+ accountId,
1111
+ deploymentId
1112
+ });
1113
+ if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
1114
+ return null;
1193
1115
  }
1194
- };
1195
- var createEntrySubmissionValidation = ({
1196
- connector,
1197
- symbol,
1198
- direction,
1199
- takeProfits,
1200
- stopLossPrice,
1201
- limits,
1202
- onValidated
1203
- }) => {
1204
- if (!limits) return void 0;
1205
- if (!connector.capabilities?.orderSubmissionValidation)
1206
- throw new EntrySubmissionValidationError(
1207
- "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
1208
- );
1209
- return async ({ qty, executionPrice }) => {
1210
- const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
1211
- const snapshot2 = simulated && executionPrice != null ? {
1212
- arrivalSnapshotTime: Date.now(),
1213
- arrivalSource: "simulation",
1214
- bid: executionPrice,
1215
- ask: executionPrice,
1216
- arrivalMid: executionPrice,
1217
- spreadBps: 0
1218
- } : await getOrderArrivalSnapshot({
1219
- connector,
1220
- symbol,
1221
- requireTimestamp: true
1222
- });
1223
- const age = Date.now() - snapshot2.arrivalSnapshotTime;
1224
- if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
1225
- throw new EntrySubmissionValidationError(
1226
- "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
1227
- );
1228
- if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
1229
- throw new EntrySubmissionValidationError(
1230
- "ENTRY_QUOTE_STALE_AT_SUBMISSION"
1231
- );
1232
- const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
1233
- validateEntryProtectionAtArrival({
1234
- direction,
1235
- signalPrice: price,
1236
- bid: snapshot2.bid,
1237
- ask: snapshot2.ask,
1238
- arrivalMid: snapshot2.arrivalMid,
1239
- takeProfits,
1240
- stopLossPrice
1241
- });
1242
- const prices = takeProfits.map((tp) => tp.price);
1243
- const economics2 = validateEntryExecution({
1244
- direction,
1245
- price,
1246
- qty,
1247
- stopLossPrice,
1248
- takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
1249
- limits
1250
- });
1251
- onValidated?.(snapshot2, economics2);
1116
+ const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
1117
+ const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
1118
+ const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
1119
+ const next = {
1120
+ ...existing,
1121
+ ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
1122
+ qty: resultingQty,
1123
+ entryPrice: resultingEntryPrice,
1124
+ entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
1125
+ lastEntryPrice: addedEntryPrice,
1126
+ lastEntryQty: addedQty,
1127
+ lastEntryTimestamp: entryTimestamp,
1128
+ currentPrice: resultingEntryPrice,
1129
+ currentPnl: 0,
1130
+ fee: openFee,
1131
+ openFee,
1132
+ totalFee,
1133
+ lastSyncedAt: now()
1252
1134
  };
1253
- };
1254
-
1255
- // src/ai.ts
1256
- var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
1257
- var import_redis = require("@tradejs/infra/redis");
1258
-
1259
- // src/aiShared.ts
1260
- var MAX_AI_SERIES_POINTS = 5;
1261
- var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
1262
- "tradejs.indicators.compactSnapshot"
1263
- );
1264
- var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
1265
- var trimSeriesDeep = (value) => {
1266
- if (Array.isArray(value)) {
1267
- const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
1268
- const isMatrix = trimmed.every((item) => Array.isArray(item));
1269
- if (isMatrix) {
1270
- return trimmed;
1271
- }
1272
- return trimmed.map(
1273
- (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
1274
- );
1275
- }
1276
- if (value && typeof value === "object") {
1277
- return Object.fromEntries(
1278
- Object.entries(value).map(([key, nested]) => [
1279
- key,
1280
- trimSeriesDeep(nested)
1281
- ])
1135
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
1136
+ try {
1137
+ await Promise.all([
1138
+ (0, import_redis.setData)(import_redis.redisKeys.runtimeTrade(userName, existing.orderId), next, {
1139
+ expire: 0
1140
+ }),
1141
+ (0, import_redis.setHashJsonField)(
1142
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1143
+ existing.orderId,
1144
+ next,
1145
+ { expire: 0 }
1146
+ )
1147
+ ]);
1148
+ } catch (error) {
1149
+ import_logger.logger.error(
1150
+ "runtime trade increase journal failed: %s %s",
1151
+ symbol,
1152
+ error?.message || String(error)
1282
1153
  );
1283
1154
  }
1284
- return value;
1155
+ return next;
1285
1156
  };
1286
- var buildCompactAiIndicatorsSnapshot = (value) => {
1287
- const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
1288
- if (typeof compactSnapshot === "function") {
1289
- return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
1157
+ var getActiveRuntimeTrade = async (params) => {
1158
+ const { userName, symbol, accountId, deploymentId } = params;
1159
+ if (!userName) {
1160
+ return null;
1290
1161
  }
1291
- return trimSeriesDeep(value);
1292
- };
1293
-
1294
- // src/aiMarketContext.ts
1295
- var toRecord = (value) => {
1296
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1162
+ const activeRef = await (0, import_redis.getData)(
1163
+ import_redis.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
1164
+ null
1165
+ );
1166
+ const orderId = String(activeRef?.orderId || "").trim();
1167
+ if (!orderId) {
1297
1168
  return null;
1298
1169
  }
1299
- return value;
1300
- };
1301
- var toFiniteNumber = (value) => {
1302
- const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
1303
- return Number.isFinite(numeric) ? numeric : null;
1304
- };
1305
- var roundTo = (value, decimals) => {
1306
- const factor = 10 ** decimals;
1307
- return Math.round(value * factor) / factor;
1308
- };
1309
- var buildMissingSpreadContext = () => ({
1310
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1311
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1312
- available: false,
1313
- value: null,
1314
- zScore: null,
1315
- bps: null,
1316
- absBps: null,
1317
- bias: null,
1318
- severity: null
1319
- });
1320
- var buildSpreadContextFromSignal = (signal) => {
1321
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1322
- const relative = toRecord(baseContext?.relative);
1323
- const execution = toRecord(relative?.execution);
1324
- const spread = toFiniteNumber(execution?.venueSpread);
1325
- const zScore = toFiniteNumber(execution?.venueSpreadZScore);
1326
- if (spread == null) {
1327
- return buildMissingSpreadContext();
1170
+ const existing = await (0, import_redis.getData)(
1171
+ import_redis.redisKeys.runtimeTrade(userName, orderId),
1172
+ null
1173
+ );
1174
+ if (!existing || existing.status !== "active") {
1175
+ await (0, import_redis.delKey)(
1176
+ import_redis.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
1177
+ );
1178
+ return null;
1328
1179
  }
1329
- const value = roundTo(spread, 8);
1330
- const bps = roundTo(value * 1e4, 2);
1331
- const absBps = Math.abs(bps);
1332
- const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
1333
- const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
1334
- return {
1335
- source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1336
- indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
1337
- available: true,
1338
- value,
1339
- zScore,
1340
- bps,
1341
- absBps,
1342
- bias,
1343
- severity
1344
- };
1180
+ return existing;
1345
1181
  };
1346
- var buildTrueDeltaContextFromSignal = (signal) => {
1347
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1348
- const participation = toRecord(baseContext?.participation);
1349
- const delta = toRecord(participation?.delta);
1350
- const source = String(delta?.source ?? "");
1351
- const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
1352
- if (!delta || !isTrueDeltaSource) {
1353
- return {
1354
- source: source || null,
1355
- available: false,
1356
- buyPressurePct: null,
1357
- buyVolume: null,
1358
- sellVolume: null,
1359
- netDelta: null,
1360
- deltaPct: null,
1361
- signedVolumeZScore: null
1362
- };
1182
+ var markRuntimeTradeClosed = async (params) => {
1183
+ const {
1184
+ userName,
1185
+ expectedOrderId,
1186
+ symbol,
1187
+ strategy,
1188
+ exitPrice,
1189
+ exitTimestamp,
1190
+ closedPnl,
1191
+ exitType,
1192
+ accountId,
1193
+ deploymentId
1194
+ } = params;
1195
+ if (!userName) {
1196
+ return null;
1363
1197
  }
1364
- return {
1365
- source,
1366
- available: true,
1367
- buyPressurePct: toFiniteNumber(delta.buyPressurePct),
1368
- buyVolume: toFiniteNumber(delta.buyVolume),
1369
- sellVolume: toFiniteNumber(delta.sellVolume),
1370
- netDelta: toFiniteNumber(delta.netDelta),
1371
- deltaPct: toFiniteNumber(delta.deltaPct),
1372
- signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
1373
- };
1374
- };
1375
- var buildTradeFlowContextFromSignal = (signal) => {
1376
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1377
- const participation = toRecord(baseContext?.participation);
1378
- const tradeFlow = toRecord(participation?.tradeFlow);
1379
- if (!tradeFlow) {
1380
- return {
1381
- source: null,
1382
- available: false,
1383
- interval: null,
1384
- stale: null,
1385
- trades: null,
1386
- buyPressurePct: null,
1387
- netBaseDelta: null,
1388
- netQuoteDelta: null
1389
- };
1198
+ const existing = expectedOrderId ? await (0, import_redis.getDataStrict)(
1199
+ import_redis.redisKeys.runtimeTrade(userName, expectedOrderId)
1200
+ ) : await getActiveRuntimeTrade({
1201
+ userName,
1202
+ symbol,
1203
+ accountId,
1204
+ deploymentId
1205
+ });
1206
+ if (!existing || expectedOrderId && existing.orderId !== expectedOrderId) {
1207
+ return null;
1390
1208
  }
1391
- return {
1392
- source: String(tradeFlow.source ?? ""),
1393
- available: true,
1394
- interval: String(tradeFlow.interval ?? ""),
1395
- stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
1396
- trades: toFiniteNumber(tradeFlow.trades),
1397
- buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
1398
- netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
1399
- netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
1209
+ const orderId = existing.orderId;
1210
+ if (strategy && existing.strategy !== strategy) {
1211
+ return null;
1212
+ }
1213
+ const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
1214
+ const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
1215
+ direction: existing.direction,
1216
+ entryPrice: existing.entryPrice,
1217
+ exitPrice: resolvedExitPrice,
1218
+ qty: existing.qty
1219
+ }) : existing.closedPnl ?? existing.currentPnl ?? null;
1220
+ const next = expectedOrderId && existing.status === "closed" ? existing : {
1221
+ ...existing,
1222
+ status: "closed",
1223
+ currentPrice: resolvedExitPrice,
1224
+ currentPnl: resolvedClosedPnl,
1225
+ closedPnl: resolvedClosedPnl,
1226
+ exitPrice: resolvedExitPrice,
1227
+ exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
1228
+ exitType: exitType ?? existing.exitType ?? null,
1229
+ lastSyncedAt: now()
1400
1230
  };
1401
- };
1402
- var buildMarketBreadthContextFromSignal = (signal) => {
1403
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1404
- const relative = toRecord(baseContext?.relative);
1405
- const breadth = toRecord(relative?.marketBreadth);
1406
- if (!breadth) {
1407
- return {
1408
- source: null,
1409
- available: false,
1410
- universe: null,
1411
- interval: null,
1412
- stale: null,
1413
- symbolsCount: null,
1414
- advanceDeclineRatio: null,
1415
- pctAboveMa20: null,
1416
- pctAboveMa50: null,
1417
- equalWeightedReturn: null,
1418
- volumeWeightedReturn: null,
1419
- dispersion: null
1420
- };
1231
+ const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
1232
+ const closeDayKey = (0, import_time.getRuntimeStorageDayKey)(next.exitTimestamp);
1233
+ try {
1234
+ await Promise.all([
1235
+ (expectedOrderId ? import_redis.setDataStrict : import_redis.setData)(
1236
+ import_redis.redisKeys.runtimeTrade(userName, orderId),
1237
+ next,
1238
+ {
1239
+ expire: import_constants.TTL_1M
1240
+ }
1241
+ ),
1242
+ (expectedOrderId ? import_redis.setHashJsonFieldStrict : import_redis.setHashJsonField)(
1243
+ import_redis.redisKeys.runtimeTradeBucket(userName, dayKey),
1244
+ orderId,
1245
+ next,
1246
+ { expire: import_constants.TTL_1M }
1247
+ ),
1248
+ (expectedOrderId ? import_redis.setHashJsonFieldStrict : import_redis.setHashJsonField)(
1249
+ import_redis.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
1250
+ orderId,
1251
+ next,
1252
+ { expire: import_constants.TTL_1M }
1253
+ ),
1254
+ expectedOrderId ? (0, import_redis.deleteRuntimeTradeReference)(
1255
+ import_redis.redisKeys.runtimeActiveTrade(
1256
+ userName,
1257
+ symbol,
1258
+ deploymentId ?? accountId
1259
+ ),
1260
+ expectedOrderId
1261
+ ) : (0, import_redis.delKey)(
1262
+ import_redis.redisKeys.runtimeActiveTrade(
1263
+ userName,
1264
+ symbol,
1265
+ deploymentId ?? accountId
1266
+ )
1267
+ )
1268
+ ]);
1269
+ } catch (error) {
1270
+ if (expectedOrderId) throw error;
1271
+ import_logger.logger.error(
1272
+ "runtime trade close journal failed: %s %s",
1273
+ symbol,
1274
+ error?.message || String(error)
1275
+ );
1421
1276
  }
1422
- return {
1423
- source: String(breadth.source ?? ""),
1424
- available: true,
1425
- universe: String(breadth.universe ?? ""),
1426
- interval: String(breadth.interval ?? ""),
1427
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
1428
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
1429
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
1430
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
1431
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
1432
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
1433
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
1434
- dispersion: toFiniteNumber(breadth.dispersion)
1435
- };
1277
+ return next;
1436
1278
  };
1437
- var buildMarketBreadthsContextFromSignal = (signal) => {
1438
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1439
- const relative = toRecord(baseContext?.relative);
1440
- const breadths = toRecord(relative?.marketBreadths);
1441
- const build = (key) => {
1442
- const breadth = toRecord(breadths?.[key]);
1443
- if (!breadth) {
1279
+
1280
+ // src/strategy/portfolioRuntime.ts
1281
+ var coordinators = /* @__PURE__ */ new WeakMap();
1282
+ async function observeRuntimePortfolio(connector, binding, asOf, reservations, observedNow = Date.now) {
1283
+ if (!connector.getOpenPositionPnl || !connector.getEntryExecutions)
1284
+ throw new Error(
1285
+ "Portfolio policies require position marks and entry executions"
1286
+ );
1287
+ const orders = await Promise.all(
1288
+ reservations.map(async (r) => {
1289
+ if (r.submitted && connector.getEntryOrderObservation) {
1290
+ const fact2 = await connector.getEntryOrderObservation({
1291
+ symbol: r.symbol,
1292
+ orderId: r.id
1293
+ });
1294
+ if (fact2) return fact2;
1295
+ }
1296
+ const executions = r.submitted ? await connector.getEntryExecutions({
1297
+ symbol: r.symbol,
1298
+ startTime: r.timestamp,
1299
+ endTime: asOf
1300
+ }) : [];
1301
+ const filledQty = executions.filter(
1302
+ (e) => (e.orderLinkId === r.id || e.orderId === r.id) && e.direction === r.direction
1303
+ ).reduce((sum, e) => sum + e.qty, 0);
1444
1304
  return {
1445
- source: null,
1446
- available: false,
1447
- universe: null,
1448
- interval: null,
1449
- stale: null,
1450
- symbolsCount: null,
1451
- advanceDeclineRatio: null,
1452
- pctAboveMa20: null,
1453
- pctAboveMa50: null,
1454
- equalWeightedReturn: null,
1455
- volumeWeightedReturn: null,
1456
- dispersion: null
1305
+ id: r.id,
1306
+ status: filledQty >= r.qty ? "filled" : filledQty > 0 ? "partial" : "pending",
1307
+ filledQty,
1308
+ positionId: r.id
1309
+ };
1310
+ })
1311
+ );
1312
+ const snapshots2 = await connector.getOpenPositionPnl();
1313
+ const positions = await Promise.all(
1314
+ snapshots2.map(async (p) => {
1315
+ const reserved = reservations.find(
1316
+ (r) => r.symbol === p.symbol && r.direction === p.direction && orders.some(
1317
+ (o) => o.id === r.id && o.filledQty > 0 && p.qty <= o.filledQty * (1 + 1e-8)
1318
+ )
1319
+ );
1320
+ const trade = await getActiveRuntimeTrade({
1321
+ ...binding,
1322
+ symbol: p.symbol
1323
+ });
1324
+ 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;
1325
+ return {
1326
+ id: reserved?.id ?? owner?.orderId ?? `unknown:${p.symbol}:${p.direction}`,
1327
+ strategy: reserved?.strategy ?? owner?.strategy ?? null,
1328
+ symbol: p.symbol,
1329
+ direction: p.direction,
1330
+ qty: p.qty,
1331
+ entryPrice: p.price,
1332
+ markPrice: p.currentPrice,
1333
+ markedAt: p.observedAt ?? null,
1334
+ stopLossPrice: p.slPrice ?? p.stopLossPrice ?? null
1457
1335
  };
1336
+ })
1337
+ );
1338
+ return { asOf: Math.max(asOf, observedNow()), positions, orders };
1339
+ }
1340
+ function persistentStore(key) {
1341
+ const raw = /* @__PURE__ */ new WeakMap();
1342
+ return {
1343
+ async read() {
1344
+ const value = await import_redis2.portfolioStateStorage.read(key);
1345
+ if (value === null) return null;
1346
+ const state = JSON.parse(value);
1347
+ (0, import_portfolio2.assertPortfolioState)(state);
1348
+ raw.set(state, value);
1349
+ return state;
1350
+ },
1351
+ async compare(previous, next) {
1352
+ const expected = previous === null ? null : raw.get(previous);
1353
+ if (expected === void 0)
1354
+ throw new Error("Portfolio CAS requires a storage snapshot");
1355
+ return import_redis2.portfolioStateStorage.compare(key, expected, JSON.stringify(next));
1458
1356
  }
1459
- return {
1460
- source: String(breadth.source ?? ""),
1461
- available: true,
1462
- universe: String(breadth.universe ?? ""),
1463
- interval: String(breadth.interval ?? ""),
1464
- stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
1465
- symbolsCount: toFiniteNumber(breadth.symbolsCount),
1466
- advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
1467
- pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
1468
- pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
1469
- equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
1470
- volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
1471
- dispersion: toFiniteNumber(breadth.dispersion)
1472
- };
1473
1357
  };
1474
- return {
1475
- top5: build("top5"),
1476
- top10: build("top10"),
1477
- top30: build("top30"),
1478
- top50: build("top50"),
1479
- top100: build("top100")
1358
+ }
1359
+ function getPortfolioRuntime({
1360
+ value,
1361
+ connectorName,
1362
+ strategy,
1363
+ connector,
1364
+ env,
1365
+ userName,
1366
+ accountId,
1367
+ deploymentId,
1368
+ symbol,
1369
+ canClose
1370
+ }) {
1371
+ const policy = (0, import_portfolio2.parsePortfolioPolicy)(value);
1372
+ if (!policy) return void 0;
1373
+ const replay = (env === "BACKTEST" || env === "PARITY") && Reflect.get(connector, "__tradejsReplayConnector") === true;
1374
+ if ((env === "BACKTEST" || env === "PARITY") && (!replay || !connector.getPortfolioObservation))
1375
+ throw new Error(
1376
+ "Portfolio policies require shared portfolio replay; isolated symbol backtests are unsupported"
1377
+ );
1378
+ if (!replay && (!userName || !accountId || !deploymentId || !connector.getOpenPositionPnl || !connector.getEntryExecutions))
1379
+ throw new Error(
1380
+ "Portfolio runtime requires exact user/account/deployment binding, marks and executions"
1381
+ );
1382
+ const binding = {
1383
+ userName,
1384
+ accountId,
1385
+ deploymentId
1480
1386
  };
1387
+ const scope = policy.scope === "strategy" ? strategy : policy.scope === "account" ? "account" : [...policy.scope.strategies].sort();
1388
+ const key = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify([binding, connectorName, connector.universe, scope])).digest("hex");
1389
+ const fingerprint3 = (0, import_portfolio2.portfolioFingerprint)(policy, strategy);
1390
+ const cacheKey = key;
1391
+ let registry = coordinators.get(connector);
1392
+ if (!registry) {
1393
+ registry = /* @__PURE__ */ new Map();
1394
+ coordinators.set(connector, registry);
1395
+ }
1396
+ const existing = registry.get(cacheKey);
1397
+ if (existing) {
1398
+ if (existing.fingerprint !== fingerprint3)
1399
+ throw new Error("Conflicting portfolio configuration for the same scope");
1400
+ if (symbol && canClose)
1401
+ existing.permissions.set(JSON.stringify([strategy, symbol]), canClose);
1402
+ return existing.coordinator;
1403
+ }
1404
+ const permissions = /* @__PURE__ */ new Map();
1405
+ if (symbol && canClose)
1406
+ permissions.set(JSON.stringify([strategy, symbol]), canClose);
1407
+ const coordinator = createPortfolioCoordinator({
1408
+ policy,
1409
+ strategy,
1410
+ store: replay ? createMemoryPortfolioStore() : persistentStore(key),
1411
+ execution: {
1412
+ async confirmClosed(intent, asOf) {
1413
+ if (replay) return;
1414
+ const position = intent.position;
1415
+ await markRuntimeTradeClosed({
1416
+ ...binding,
1417
+ strategy: position.strategy,
1418
+ expectedOrderId: position.id,
1419
+ symbol: position.symbol,
1420
+ exitPrice: position.markPrice,
1421
+ exitTimestamp: asOf,
1422
+ exitType: "exit"
1423
+ });
1424
+ },
1425
+ observe: (asOf, reservations) => replay ? connector.getPortfolioObservation(asOf, reservations) : observeRuntimePortfolio(connector, binding, asOf, reservations),
1426
+ async close(position, intent, asOf) {
1427
+ const permission = permissions.get(
1428
+ JSON.stringify([position.strategy, position.symbol])
1429
+ );
1430
+ if (!permission || !await permission(position, intent, asOf)) return;
1431
+ if (!replay) {
1432
+ const trade = await getActiveRuntimeTrade({
1433
+ ...binding,
1434
+ symbol: position.symbol
1435
+ });
1436
+ if (!trade || trade.orderId !== position.id)
1437
+ throw new Error("Portfolio close ownership changed");
1438
+ }
1439
+ await connector.closePosition({
1440
+ symbol: position.symbol,
1441
+ direction: position.direction,
1442
+ price: position.markPrice,
1443
+ timestamp: asOf,
1444
+ signal: {
1445
+ signalId: `${position.id}:${intent.reason}`,
1446
+ strategy: position.strategy,
1447
+ symbol: position.symbol,
1448
+ direction: position.direction,
1449
+ interval: "1",
1450
+ timestamp: asOf,
1451
+ prices: {
1452
+ riskRatio: 0,
1453
+ currentPrice: position.markPrice,
1454
+ takeProfitPrice: position.markPrice,
1455
+ stopLossPrice: position.markPrice
1456
+ },
1457
+ figures: {},
1458
+ indicators: {},
1459
+ additionalIndicators: { portfolioExitReason: intent.reason }
1460
+ }
1461
+ });
1462
+ }
1463
+ }
1464
+ });
1465
+ registry.set(cacheKey, { fingerprint: fingerprint3, coordinator, permissions });
1466
+ return coordinator;
1467
+ }
1468
+
1469
+ // src/strategyHelpers/tickerProfiles.ts
1470
+ var import_tickerProfiles = require("@tradejs/infra/tickerProfiles");
1471
+ var import_tickerProfiles2 = require("@tradejs/core/tickerProfiles");
1472
+
1473
+ // src/tradejsConfig.ts
1474
+ var import_fs = __toESM(require("fs"));
1475
+ var import_path = __toESM(require("path"));
1476
+ var import_url = require("url");
1477
+ var import_config = require("@tradejs/core/config");
1478
+ var import_logger2 = require("@tradejs/infra/logger");
1479
+ var CONFIG_FILE_NAMES = [
1480
+ "tradejs.config.ts",
1481
+ "tradejs.config.mts",
1482
+ "tradejs.config.js",
1483
+ "tradejs.config.mjs",
1484
+ "tradejs.config.cjs"
1485
+ ];
1486
+ var TS_MODULE_RE = /\.(cts|mts|ts)$/i;
1487
+ var cachedByCwd = /* @__PURE__ */ new Map();
1488
+ var announcedConfigFile = /* @__PURE__ */ new Set();
1489
+ var tsNodeRegistered = false;
1490
+ var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
1491
+ var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
1492
+ var getTradejsProjectCwd = (cwd) => {
1493
+ const explicit = String(cwd ?? "").trim();
1494
+ if (explicit) {
1495
+ return import_path.default.resolve(explicit);
1496
+ }
1497
+ const fromEnv = String(process.env.PROJECT_CWD || "").trim();
1498
+ if (fromEnv) {
1499
+ return import_path.default.resolve(fromEnv);
1500
+ }
1501
+ return process.cwd();
1481
1502
  };
1482
- var buildTargetVsBtcContextFromSignal = (signal) => {
1483
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1484
- const relative = toRecord(baseContext?.relative);
1485
- const targetVsBtc = toRecord(relative?.targetVsBtc);
1486
- if (!targetVsBtc) {
1487
- return {
1488
- source: null,
1489
- available: false,
1490
- ratioReturn1h: null,
1491
- ratioReturn4h: null,
1492
- ratioReturn24h: null,
1493
- alphaVsBtc1h: null,
1494
- alphaVsBtc4h: null,
1495
- alphaVsBtc24h: null,
1496
- betaToBtc20: null,
1497
- correlationToBtc20: null,
1498
- ratioTrend: null
1499
- };
1503
+ var normalizeConfig = (rawConfig) => {
1504
+ if (!rawConfig || typeof rawConfig !== "object") {
1505
+ return {};
1500
1506
  }
1507
+ const config = rawConfig;
1508
+ const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
1509
+ const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
1510
+ const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
1511
+ const hooks = (0, import_config.normalizeTradejsConfigHooks)(
1512
+ config.hooks
1513
+ );
1514
+ const runtime = config.runtime && typeof config.runtime === "object" && !Array.isArray(config.runtime) ? config.runtime : void 0;
1501
1515
  return {
1502
- source: String(targetVsBtc.source ?? ""),
1503
- available: true,
1504
- ratioReturn1h: toFiniteNumber(targetVsBtc.ratioReturn1h),
1505
- ratioReturn4h: toFiniteNumber(targetVsBtc.ratioReturn4h),
1506
- ratioReturn24h: toFiniteNumber(targetVsBtc.ratioReturn24h),
1507
- alphaVsBtc1h: toFiniteNumber(targetVsBtc.alphaVsBtc1h),
1508
- alphaVsBtc4h: toFiniteNumber(targetVsBtc.alphaVsBtc4h),
1509
- alphaVsBtc24h: toFiniteNumber(targetVsBtc.alphaVsBtc24h),
1510
- betaToBtc20: toFiniteNumber(targetVsBtc.betaToBtc20),
1511
- correlationToBtc20: toFiniteNumber(targetVsBtc.correlationToBtc20),
1512
- ratioTrend: typeof targetVsBtc.ratioTrend === "string" ? targetVsBtc.ratioTrend : null
1516
+ strategies: strategies2,
1517
+ indicators,
1518
+ connectors,
1519
+ ...hooks ? { hooks } : {},
1520
+ ...runtime ? { runtime } : {}
1513
1521
  };
1514
1522
  };
1515
- var buildBtcAltRegimeContextFromSignal = (signal) => {
1516
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1517
- const relative = toRecord(baseContext?.relative);
1518
- const btcAltRegime = toRecord(relative?.btcAltRegime);
1519
- if (!btcAltRegime) {
1520
- return {
1521
- source: null,
1522
- available: false,
1523
- universe: null,
1524
- interval: null,
1525
- stale: null,
1526
- regime: null,
1527
- btcReturn24h: null,
1528
- altBasketReturn24h: null,
1529
- btcVsAltReturn24h: null,
1530
- btcTurnoverShare24h: null,
1531
- btcTurnoverShareChange24h: null,
1532
- altVolToBtcVol24h: null,
1533
- altDispersion24h: null
1534
- };
1523
+ var getNodeCreateRequire = () => {
1524
+ const builtinModule = process.getBuiltinModule?.("module");
1525
+ if (typeof builtinModule?.createRequire === "function") {
1526
+ return builtinModule.createRequire;
1535
1527
  }
1536
- return {
1537
- source: String(btcAltRegime.source ?? ""),
1538
- available: true,
1539
- universe: String(btcAltRegime.universe ?? ""),
1540
- interval: String(btcAltRegime.interval ?? ""),
1541
- stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
1542
- regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
1543
- btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
1544
- altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
1545
- btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
1546
- btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
1547
- btcTurnoverShareChange24h: toFiniteNumber(
1548
- btcAltRegime.btcTurnoverShareChange24h
1549
- ),
1550
- altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
1551
- altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
1552
- };
1528
+ throw new TypeError("module.createRequire is not available");
1553
1529
  };
1554
- var buildCmcGlobalContextFromSignal = (signal) => {
1555
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1556
- const relative = toRecord(baseContext?.relative);
1557
- const cmcGlobal = toRecord(relative?.cmcGlobal);
1558
- if (!cmcGlobal) {
1559
- return {
1560
- source: null,
1561
- available: false,
1562
- interval: null,
1563
- asOfTs: null,
1564
- stale: null,
1565
- totalMarketCapUsd: null,
1566
- totalVolumeUsd: null,
1567
- totalVolumeReportedUsd: null,
1568
- altMarketCapUsd: null,
1569
- altVolumeUsd: null,
1570
- altVolumeReportedUsd: null,
1571
- btcDominancePct: null,
1572
- ethDominancePct: null,
1573
- btcDominanceChange24hPct: null,
1574
- ethDominanceChange24hPct: null,
1575
- altMarketCapChange24hPct: null,
1576
- altVolumeChange24hPct: null,
1577
- activeCryptocurrencies: null,
1578
- activeExchanges: null,
1579
- activeMarketPairs: null,
1580
- altLiquidityRegime: null
1581
- };
1530
+ var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
1531
+ var ensureTsNodeRegistered = async () => {
1532
+ if (tsNodeRegistered) {
1533
+ return;
1582
1534
  }
1583
- return {
1584
- source: String(cmcGlobal.source ?? ""),
1585
- available: true,
1586
- interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
1587
- asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
1588
- stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
1589
- totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
1590
- totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
1591
- totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
1592
- altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
1593
- altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
1594
- altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
1595
- btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
1596
- ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
1597
- btcDominanceChange24hPct: toFiniteNumber(
1598
- cmcGlobal.btcDominanceChange24hPct
1599
- ),
1600
- ethDominanceChange24hPct: toFiniteNumber(
1601
- cmcGlobal.ethDominanceChange24hPct
1602
- ),
1603
- altMarketCapChange24hPct: toFiniteNumber(
1604
- cmcGlobal.altMarketCapChange24hPct
1605
- ),
1606
- altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
1607
- activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
1608
- activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
1609
- activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
1610
- altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
1611
- };
1535
+ const tsNodeModule = await import("ts-node");
1536
+ const tsNode = tsNodeModule.default ?? tsNodeModule;
1537
+ tsNode.register?.({
1538
+ transpileOnly: true,
1539
+ compilerOptions: {
1540
+ module: "Node16",
1541
+ moduleResolution: "node16"
1542
+ }
1543
+ });
1544
+ tsNodeRegistered = true;
1612
1545
  };
1613
- var buildCmcReferenceAssetsContextFromSignal = (signal) => {
1614
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1615
- const relative = toRecord(baseContext?.relative);
1616
- const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
1617
- if (!cmcReferenceAssets) {
1618
- return {
1619
- source: null,
1620
- available: false,
1621
- interval: null,
1622
- asOfTs: null,
1623
- stale: null,
1624
- btcMarketCapUsd: null,
1625
- ethMarketCapUsd: null,
1626
- btcVolumeUsd: null,
1627
- ethVolumeUsd: null,
1628
- btcVolumeToMarketCap: null,
1629
- ethVolumeToMarketCap: null,
1630
- ethBtcMarketCapRatio: null,
1631
- ethBtcMarketCapRatioChange24hPct: null,
1632
- ethVsBtcVolumeRatio: null,
1633
- referenceLiquidityRegime: null
1634
- };
1546
+ var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
1547
+ const projectRoot = getTradejsProjectCwd(cwd);
1548
+ if (tsconfigPathsRegisteredByCwd.has(projectRoot)) {
1549
+ return;
1635
1550
  }
1636
- return {
1637
- source: String(cmcReferenceAssets.source ?? ""),
1638
- available: true,
1639
- interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
1640
- asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
1641
- stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
1642
- btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
1643
- ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
1644
- btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
1645
- ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
1646
- btcVolumeToMarketCap: toFiniteNumber(
1647
- cmcReferenceAssets.btcVolumeToMarketCap
1648
- ),
1649
- ethVolumeToMarketCap: toFiniteNumber(
1650
- cmcReferenceAssets.ethVolumeToMarketCap
1651
- ),
1652
- ethBtcMarketCapRatio: toFiniteNumber(
1653
- cmcReferenceAssets.ethBtcMarketCapRatio
1654
- ),
1655
- ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
1656
- cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
1657
- ),
1658
- ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
1659
- referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
1660
- };
1661
- };
1662
- var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
1663
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1664
- const relative = toRecord(baseContext?.relative);
1665
- const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
1666
- if (!cmcExchangeLiquidity) {
1667
- return {
1668
- source: null,
1669
- available: false,
1670
- interval: null,
1671
- asOfTs: null,
1672
- stale: null,
1673
- exchangesCount: null,
1674
- totalVolumeUsd: null,
1675
- totalVolumeChange24hPct: null,
1676
- binanceVolumeUsd: null,
1677
- binanceVolumeShare: null,
1678
- topExchangeVolumeShare: null,
1679
- liquidityRegime: null
1680
- };
1551
+ const tsconfigPathsModule = await import("tsconfig-paths");
1552
+ const loadConfig = tsconfigPathsModule.loadConfig;
1553
+ const register = tsconfigPathsModule.register;
1554
+ if (typeof loadConfig !== "function" || typeof register !== "function") {
1555
+ return;
1681
1556
  }
1682
- return {
1683
- source: String(cmcExchangeLiquidity.source ?? ""),
1684
- available: true,
1685
- interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
1686
- asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
1687
- stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
1688
- exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
1689
- totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
1690
- totalVolumeChange24hPct: toFiniteNumber(
1691
- cmcExchangeLiquidity.totalVolumeChange24hPct
1692
- ),
1693
- binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
1694
- binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
1695
- topExchangeVolumeShare: toFiniteNumber(
1696
- cmcExchangeLiquidity.topExchangeVolumeShare
1697
- ),
1698
- liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
1699
- };
1557
+ const loadedConfig = loadConfig(projectRoot);
1558
+ if (loadedConfig.resultType !== "success") {
1559
+ return;
1560
+ }
1561
+ register({
1562
+ baseUrl: loadedConfig.absoluteBaseUrl,
1563
+ paths: loadedConfig.paths,
1564
+ addMatchAll: false
1565
+ });
1566
+ tsconfigPathsRegisteredByCwd.add(projectRoot);
1700
1567
  };
1701
- var buildCmcFearGreedContextFromSignal = (signal) => {
1702
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1703
- const relative = toRecord(baseContext?.relative);
1704
- const cmcFearGreed = toRecord(relative?.cmcFearGreed);
1705
- if (!cmcFearGreed) {
1706
- return {
1707
- source: null,
1708
- available: false,
1709
- interval: null,
1710
- asOfTs: null,
1711
- stale: null,
1712
- value: null,
1713
- valueChange24h: null,
1714
- valueChange7d: null,
1715
- classification: null,
1716
- sentimentRegime: null
1717
- };
1568
+ var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1569
+ const projectRoot = getTradejsProjectCwd(cwd);
1570
+ const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
1571
+ if (cachedMatcher) {
1572
+ const resolved2 = cachedMatcher(moduleName);
1573
+ return resolved2 || null;
1718
1574
  }
1719
- return {
1720
- source: String(cmcFearGreed.source ?? ""),
1721
- available: true,
1722
- interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
1723
- asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
1724
- stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
1725
- value: toFiniteNumber(cmcFearGreed.value),
1726
- valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
1727
- valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
1728
- classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
1729
- sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
1730
- };
1575
+ const tsconfigPathsModule = await import("tsconfig-paths");
1576
+ const loadConfig = tsconfigPathsModule.loadConfig;
1577
+ const createMatchPath = tsconfigPathsModule.createMatchPath;
1578
+ if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
1579
+ return null;
1580
+ }
1581
+ const loadedConfig = loadConfig(projectRoot);
1582
+ if (loadedConfig.resultType !== "success") {
1583
+ return null;
1584
+ }
1585
+ const matchPath = createMatchPath(
1586
+ loadedConfig.absoluteBaseUrl,
1587
+ loadedConfig.paths
1588
+ );
1589
+ const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
1590
+ ".ts",
1591
+ ".tsx",
1592
+ ".mts",
1593
+ ".cts",
1594
+ ".js",
1595
+ ".jsx",
1596
+ ".mjs",
1597
+ ".cjs",
1598
+ ".json"
1599
+ ]) || "";
1600
+ tsconfigPathMatchersByCwd.set(projectRoot, matcher);
1601
+ const resolved = matcher(moduleName);
1602
+ return resolved || null;
1731
1603
  };
1732
- var buildCmcIndexesContextFromSignal = (signal) => {
1733
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1734
- const relative = toRecord(baseContext?.relative);
1735
- const cmcIndexes = toRecord(relative?.cmcIndexes);
1736
- if (!cmcIndexes) {
1737
- return {
1738
- source: null,
1739
- available: false,
1740
- interval: null,
1741
- asOfTs: null,
1742
- stale: null,
1743
- cmc100Value: null,
1744
- cmc100Change24hPct: null,
1745
- cmc100TopConstituentSymbol: null,
1746
- cmc100TopConstituentWeightPct: null,
1747
- cmc20Value: null,
1748
- cmc20Change24hPct: null,
1749
- cmc20TopConstituentSymbol: null,
1750
- cmc20TopConstituentWeightPct: null,
1751
- cmc20ToCmc100Ratio: null,
1752
- cmc20ToCmc100RatioChange24hPct: null,
1753
- indexRegime: null
1754
- };
1755
- }
1756
- return {
1757
- source: String(cmcIndexes.source ?? ""),
1758
- available: true,
1759
- interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
1760
- asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
1761
- stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
1762
- cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
1763
- cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
1764
- cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
1765
- cmc100TopConstituentWeightPct: toFiniteNumber(
1766
- cmcIndexes.cmc100TopConstituentWeightPct
1767
- ),
1768
- cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
1769
- cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
1770
- cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
1771
- cmc20TopConstituentWeightPct: toFiniteNumber(
1772
- cmcIndexes.cmc20TopConstituentWeightPct
1773
- ),
1774
- cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
1775
- cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
1776
- cmcIndexes.cmc20ToCmc100RatioChange24hPct
1777
- ),
1778
- indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
1779
- };
1780
- };
1781
- var buildReferenceTradeFlowContextFromSignal = (signal) => {
1782
- const baseContext = toRecord(signal.additionalIndicators?.baseContext);
1783
- const relative = toRecord(baseContext?.relative);
1784
- const refs = toRecord(relative?.referenceTradeFlow);
1785
- const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
1786
- const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
1787
- const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
1788
- if (!refs) {
1789
- return {
1790
- source: null,
1791
- available: false,
1792
- primaryReferenceSymbol: null,
1793
- referenceSymbols: [],
1794
- primaryTradeFlowBuyPressurePct: null,
1795
- primaryTradeFlowStale: null
1796
- };
1797
- }
1798
- return {
1799
- source: String(refs.source ?? ""),
1800
- available: true,
1801
- primaryReferenceSymbol,
1802
- referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
1803
- primaryTradeFlowBuyPressurePct: toFiniteNumber(
1804
- primaryTradeFlow?.buyPressurePct
1805
- ),
1806
- primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
1807
- };
1808
- };
1809
- var buildAiMarketContext = (signal) => ({
1810
- execution: {
1811
- binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
1812
- },
1813
- participation: {
1814
- trueDelta: buildTrueDeltaContextFromSignal(signal),
1815
- tradeFlow: buildTradeFlowContextFromSignal(signal)
1816
- },
1817
- relative: {
1818
- marketBreadth: buildMarketBreadthContextFromSignal(signal),
1819
- marketBreadths: buildMarketBreadthsContextFromSignal(signal),
1820
- targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
1821
- btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
1822
- cmcGlobal: buildCmcGlobalContextFromSignal(signal),
1823
- cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
1824
- cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
1825
- cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
1826
- cmcIndexes: buildCmcIndexesContextFromSignal(signal),
1827
- referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
1828
- }
1829
- });
1830
-
1831
- // src/strategy/manifests.ts
1832
- var import_indicators = require("@tradejs/core/indicators");
1833
-
1834
- // src/tradejsConfig.ts
1835
- var import_fs = __toESM(require("fs"));
1836
- var import_path = __toESM(require("path"));
1837
- var import_url = require("url");
1838
- var import_config = require("@tradejs/core/config");
1839
- var import_logger2 = require("@tradejs/infra/logger");
1840
- var CONFIG_FILE_NAMES = [
1841
- "tradejs.config.ts",
1842
- "tradejs.config.mts",
1843
- "tradejs.config.js",
1844
- "tradejs.config.mjs",
1845
- "tradejs.config.cjs"
1846
- ];
1847
- var TS_MODULE_RE = /\.(cts|mts|ts)$/i;
1848
- var cachedByCwd = /* @__PURE__ */ new Map();
1849
- var announcedConfigFile = /* @__PURE__ */ new Set();
1850
- var tsNodeRegistered = false;
1851
- var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
1852
- var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
1853
- var getTradejsProjectCwd = (cwd) => {
1854
- const explicit = String(cwd ?? "").trim();
1855
- if (explicit) {
1856
- return import_path.default.resolve(explicit);
1857
- }
1858
- const fromEnv = String(process.env.PROJECT_CWD || "").trim();
1859
- if (fromEnv) {
1860
- return import_path.default.resolve(fromEnv);
1861
- }
1862
- return process.cwd();
1863
- };
1864
- var normalizeConfig = (rawConfig) => {
1865
- if (!rawConfig || typeof rawConfig !== "object") {
1866
- return {};
1867
- }
1868
- const config = rawConfig;
1869
- const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
1870
- const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
1871
- const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
1872
- const hooks = (0, import_config.normalizeTradejsConfigHooks)(
1873
- config.hooks
1874
- );
1875
- const runtime = config.runtime && typeof config.runtime === "object" && !Array.isArray(config.runtime) ? config.runtime : void 0;
1876
- return {
1877
- strategies: strategies2,
1878
- indicators,
1879
- connectors,
1880
- ...hooks ? { hooks } : {},
1881
- ...runtime ? { runtime } : {}
1882
- };
1883
- };
1884
- var getNodeCreateRequire = () => {
1885
- const builtinModule = process.getBuiltinModule?.("module");
1886
- if (typeof builtinModule?.createRequire === "function") {
1887
- return builtinModule.createRequire;
1888
- }
1889
- throw new TypeError("module.createRequire is not available");
1890
- };
1891
- var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
1892
- var ensureTsNodeRegistered = async () => {
1893
- if (tsNodeRegistered) {
1894
- return;
1895
- }
1896
- const tsNodeModule = await import("ts-node");
1897
- const tsNode = tsNodeModule.default ?? tsNodeModule;
1898
- tsNode.register?.({
1899
- transpileOnly: true,
1900
- compilerOptions: {
1901
- module: "Node16",
1902
- moduleResolution: "node16"
1903
- }
1904
- });
1905
- tsNodeRegistered = true;
1906
- };
1907
- var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
1908
- const projectRoot = getTradejsProjectCwd(cwd);
1909
- if (tsconfigPathsRegisteredByCwd.has(projectRoot)) {
1910
- return;
1911
- }
1912
- const tsconfigPathsModule = await import("tsconfig-paths");
1913
- const loadConfig = tsconfigPathsModule.loadConfig;
1914
- const register = tsconfigPathsModule.register;
1915
- if (typeof loadConfig !== "function" || typeof register !== "function") {
1916
- return;
1917
- }
1918
- const loadedConfig = loadConfig(projectRoot);
1919
- if (loadedConfig.resultType !== "success") {
1920
- return;
1921
- }
1922
- register({
1923
- baseUrl: loadedConfig.absoluteBaseUrl,
1924
- paths: loadedConfig.paths,
1925
- addMatchAll: false
1926
- });
1927
- tsconfigPathsRegisteredByCwd.add(projectRoot);
1928
- };
1929
- var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1930
- const projectRoot = getTradejsProjectCwd(cwd);
1931
- const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
1932
- if (cachedMatcher) {
1933
- const resolved2 = cachedMatcher(moduleName);
1934
- return resolved2 || null;
1935
- }
1936
- const tsconfigPathsModule = await import("tsconfig-paths");
1937
- const loadConfig = tsconfigPathsModule.loadConfig;
1938
- const createMatchPath = tsconfigPathsModule.createMatchPath;
1939
- if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
1940
- return null;
1941
- }
1942
- const loadedConfig = loadConfig(projectRoot);
1943
- if (loadedConfig.resultType !== "success") {
1944
- return null;
1945
- }
1946
- const matchPath = createMatchPath(
1947
- loadedConfig.absoluteBaseUrl,
1948
- loadedConfig.paths
1949
- );
1950
- const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
1951
- ".ts",
1952
- ".tsx",
1953
- ".mts",
1954
- ".cts",
1955
- ".js",
1956
- ".jsx",
1957
- ".mjs",
1958
- ".cjs",
1959
- ".json"
1960
- ]) || "";
1961
- tsconfigPathMatchersByCwd.set(projectRoot, matcher);
1962
- const resolved = matcher(moduleName);
1963
- return resolved || null;
1964
- };
1965
- var toImportSpecifier = (moduleName) => {
1966
- if (moduleName.startsWith("file://")) {
1967
- return moduleName;
1604
+ var toImportSpecifier = (moduleName) => {
1605
+ if (moduleName.startsWith("file://")) {
1606
+ return moduleName;
1968
1607
  }
1969
1608
  if (import_path.default.isAbsolute(moduleName)) {
1970
1609
  return (0, import_url.pathToFileURL)(moduleName).href;
@@ -2119,6 +1758,7 @@ var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
2119
1758
  };
2120
1759
 
2121
1760
  // src/strategy/manifests.ts
1761
+ var import_indicators = require("@tradejs/core/indicators");
2122
1762
  var SHARED_STRATEGY_REGISTRY_KEY = "__tradejsNodeSharedStrategyRegistryV1__";
2123
1763
  var sharedRegistryScope = globalThis;
2124
1764
  var sharedStrategyRegistry = sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] ?? (sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] = {
@@ -2449,871 +2089,1810 @@ var strategies = new Proxy(
2449
2089
  }
2450
2090
  );
2451
2091
 
2452
- // src/strategy/policyProfiles.ts
2453
- var profileMatches = (profile, universe, assetClass) => {
2454
- const { appliesTo } = profile;
2455
- if (!appliesTo) return true;
2456
- if (appliesTo.universes?.length && (!universe || !appliesTo.universes.includes(universe))) {
2457
- return false;
2458
- }
2459
- if (appliesTo.assetClasses?.length && (!assetClass || !appliesTo.assetClasses.includes(assetClass))) {
2460
- return false;
2461
- }
2462
- return true;
2092
+ // src/strategyHelpers/tickerProfiles.ts
2093
+ var snapshots = /* @__PURE__ */ new Map();
2094
+ var resolveTickerProfileSource = (strategy) => {
2095
+ const declared = getStrategyManifest(strategy)?.contextRequirements?.tickerProfiles;
2096
+ if (declared) return declared;
2097
+ const provider = process.env.TICKER_PROFILES_PROVIDER?.trim();
2098
+ const universeId = process.env.TICKER_PROFILES_UNIVERSE?.trim();
2099
+ if (!provider && !universeId) return void 0;
2100
+ if (!provider || !universeId)
2101
+ throw new Error(
2102
+ "Set both TICKER_PROFILES_PROVIDER and TICKER_PROFILES_UNIVERSE"
2103
+ );
2104
+ return { provider, universeId };
2463
2105
  };
2464
- var resolveStrategyPolicyProfile = (manifest, params) => {
2465
- const profiles = manifest?.policyProfiles ?? [];
2466
- if (!profiles.length) {
2467
- const inferredId = params.profileId ?? (params.universe === "tradfi" ? "tradfi" : void 0);
2468
- if (!inferredId) return void 0;
2469
- if (inferredId !== "crypto" && inferredId !== "tradfi") {
2470
- throw new Error(
2471
- `Unknown policy profile "${inferredId}" for strategy "${manifest?.name}"`
2472
- );
2473
- }
2474
- if (params.universe && inferredId !== params.universe) {
2475
- throw new Error(
2476
- `Policy profile "${inferredId}" is not compatible with ${params.universe}`
2106
+ var loadTickerProfileContext = async ({
2107
+ projectRoot,
2108
+ provider,
2109
+ universeId,
2110
+ symbol,
2111
+ timestamp
2112
+ }) => {
2113
+ const asOf = (0, import_tickerProfiles2.tickerProfileMonth)(timestamp), key = JSON.stringify([projectRoot, provider, universeId, asOf]);
2114
+ let cached = snapshots.get(key);
2115
+ if (!cached || cached.expires < Date.now()) {
2116
+ const value = (0, import_tickerProfiles.readTickerProfiles)(projectRoot, provider, universeId, asOf).then((envelope) => {
2117
+ if (!envelope) return null;
2118
+ const s = envelope.snapshot;
2119
+ if (!(0, import_tickerProfiles2.isTickerProfileSnapshot)(s) || s.asOf !== asOf || s.provider !== provider || s.universeId !== universeId)
2120
+ throw new Error("Ticker profile identity/schema mismatch");
2121
+ const entry = snapshots.get(key);
2122
+ if (entry) entry.expires = Infinity;
2123
+ return new Map(
2124
+ s.profiles.map((p) => [
2125
+ p.symbol,
2126
+ Object.freeze({
2127
+ ...p,
2128
+ definition: s.definition,
2129
+ provider,
2130
+ universeId,
2131
+ snapshotId: envelope.snapshotId
2132
+ })
2133
+ ])
2477
2134
  );
2135
+ }).catch((error) => {
2136
+ snapshots.delete(key);
2137
+ throw error;
2138
+ });
2139
+ cached = { expires: Date.now() + 6e4, value };
2140
+ snapshots.set(key, cached);
2141
+ if (snapshots.size > 24) {
2142
+ const oldest = snapshots.keys().next().value;
2143
+ if (oldest) snapshots.delete(oldest);
2478
2144
  }
2479
- return {
2480
- id: inferredId,
2481
- appliesTo: { universes: [inferredId] },
2482
- marketDataRequirements: inferredId === "crypto" ? ["crypto.btcReference"] : [],
2483
- ...manifest?.mlAdapter ? {
2484
- entryRuntimeDefaults: {
2485
- ml: {
2486
- modelKey: inferredId === "crypto" ? manifest.name : `${manifest.name}:tradfi`
2487
- }
2488
- }
2489
- } : {}
2490
- };
2491
2145
  }
2492
- if (params.profileId) {
2493
- const profile = profiles.find(({ id }) => id === params.profileId);
2494
- if (!profile) {
2146
+ return (await cached.value)?.get(symbol);
2147
+ };
2148
+ var withTickerProfile = (baseContext, profile) => Object.defineProperty(
2149
+ Object.create(
2150
+ Object.getPrototypeOf(baseContext),
2151
+ Object.getOwnPropertyDescriptors(baseContext)
2152
+ ),
2153
+ "tickerProfile",
2154
+ { enumerable: true, configurable: true, value: profile }
2155
+ );
2156
+ var enrichSignalWithTickerProfile = async (signal) => {
2157
+ const source = resolveTickerProfileSource(signal.strategy);
2158
+ if (!source || signal.universe === "tradfi" || !signal.symbol.endsWith("USDT"))
2159
+ return;
2160
+ const profile = await loadTickerProfileContext({
2161
+ ...source,
2162
+ projectRoot: getTradejsProjectCwd(),
2163
+ symbol: signal.symbol,
2164
+ timestamp: signal.timestamp
2165
+ });
2166
+ if (profile) {
2167
+ signal.additionalIndicators = {
2168
+ ...signal.additionalIndicators,
2169
+ tickerProfile: profile
2170
+ };
2171
+ }
2172
+ };
2173
+
2174
+ // src/strategyRuntime.ts
2175
+ var import_jev6 = require("@tradejs/core/jev");
2176
+
2177
+ // src/jev.ts
2178
+ var import_promises = __toESM(require("fs/promises"));
2179
+ var import_node_path = __toESM(require("path"));
2180
+ var import_jev4 = require("@tradejs/core/jev");
2181
+ var import_jev5 = require("@tradejs/infra/jev");
2182
+ var import_userSettings = require("@tradejs/infra/userSettings");
2183
+ init_jevInput();
2184
+ init_jevInput();
2185
+ init_jevTraining();
2186
+ var resolveJevProvider = async (userName) => {
2187
+ const settings = await (0, import_userSettings.getUserSettings)(userName);
2188
+ const provider = {
2189
+ endpoint: settings.JEV_API_ENDPOINT || import_jev4.JEV_PROVIDERS[0].endpoint,
2190
+ model: settings.JEV_MODEL || import_jev4.JEV_PROVIDERS[0].model
2191
+ };
2192
+ (0, import_jev4.parseJevConfig)({ source: "provider", mode: "observe", provider });
2193
+ if (!settings.JEV_API_KEY)
2194
+ throw new Error("Configure the Jev API key in Account settings");
2195
+ return provider;
2196
+ };
2197
+ var emptyScores = () => Object.fromEntries(import_jev4.JEV_DIMENSIONS.map((key) => [key, null]));
2198
+ var pending = /* @__PURE__ */ new Map();
2199
+ var pendingResponses = /* @__PURE__ */ new Map();
2200
+ var evaluateJevInput = async ({
2201
+ input,
2202
+ config,
2203
+ userName,
2204
+ projectRoot
2205
+ }) => {
2206
+ config = (0, import_jev4.parseJevConfig)(config);
2207
+ const inputHash = (0, import_jev5.jevHash)(input);
2208
+ const questions = questionsForJevInput(input);
2209
+ const questionsHash = (0, import_jev5.jevHash)(questions);
2210
+ const base = {
2211
+ questionSet: import_jev4.JEV_QUESTION_SET,
2212
+ schema: "tradejs-signal-assessment/v5",
2213
+ source: config.source,
2214
+ mode: config.mode,
2215
+ inputHash,
2216
+ questionsHash,
2217
+ geometryStatus: input.geometryStatus,
2218
+ levelsValid: typeof input.features["signal.validLevels"] === "boolean" ? input.features["signal.validLevels"] : null
2219
+ };
2220
+ if (!input.questions.length)
2221
+ return {
2222
+ assessment: {
2223
+ ...base,
2224
+ status: "unavailable",
2225
+ model: "",
2226
+ scores: emptyScores(),
2227
+ confidence: emptyScores()
2228
+ }
2229
+ };
2230
+ if (config.source === "local") {
2231
+ const contents = await import_promises.default.readFile(
2232
+ import_node_path.default.resolve(projectRoot, config.modelFile),
2233
+ "utf8"
2234
+ );
2235
+ if ((0, import_jev5.jevFileHash)(contents) !== config.modelSha256)
2236
+ throw new Error("Local Jev gate checksum mismatch");
2237
+ const { validateJevGateModel: validateJevGateModel2 } = await Promise.resolve().then(() => (init_jevTraining(), jevTraining_exports));
2238
+ const model = validateJevGateModel2(JSON.parse(contents));
2239
+ if (model.strategy !== input.strategy || model.questionsHash !== (0, import_jev5.jevHash)(JEV_QUESTIONS))
2240
+ throw new Error("Local Jev gate strategy or question version mismatch");
2241
+ const scores = Object.fromEntries(
2242
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2243
+ key,
2244
+ input.questions.includes(key) && model.trees[key] ? (0, import_jev4.predictJevTree)(model.trees[key], input.features) : null
2245
+ ])
2246
+ );
2247
+ return {
2248
+ assessment: {
2249
+ ...base,
2250
+ status: "available",
2251
+ model: config.modelSha256,
2252
+ scores,
2253
+ confidence: emptyScores()
2254
+ }
2255
+ };
2256
+ }
2257
+ const provider = config.provider ?? await resolveJevProvider(userName);
2258
+ const id = (0, import_jev5.jevHash)({ inputHash, questionsHash, provider });
2259
+ const dir = import_node_path.default.resolve(projectRoot, config.recordsDir ?? "data/ai/jev");
2260
+ const file = import_node_path.default.join(dir, "records", `${id}.json`);
2261
+ const getRecord = async () => {
2262
+ const existing = await (0, import_jev5.readJevArtifact)(file);
2263
+ if (existing) {
2264
+ 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))
2265
+ throw new Error("Jev recording identity mismatch");
2266
+ (0, import_jev5.validateJevResponse)(existing.response, input.questions);
2267
+ for (const key of import_jev4.JEV_DIMENSIONS) {
2268
+ const expected = input.questions.includes(key) ? existing.response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null;
2269
+ if (existing.scores[key] !== expected)
2270
+ throw new Error("Jev recording scores do not match the response");
2271
+ }
2272
+ return existing;
2273
+ }
2274
+ if (config.source === "recorded")
2495
2275
  throw new Error(
2496
- `Unknown policy profile "${params.profileId}" for strategy "${manifest?.name}"`
2276
+ `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.`
2497
2277
  );
2498
- }
2499
- if (!profileMatches(profile, params.universe, params.assetClass)) {
2278
+ const settings = await (0, import_userSettings.getUserSettings)(userName);
2279
+ if (!settings.JEV_API_KEY)
2280
+ throw new Error("Configure the Jev API key in Account settings");
2281
+ const apiKey = settings.JEV_API_KEY;
2282
+ const configuredProvider = await resolveJevProvider(userName);
2283
+ if (configuredProvider.endpoint !== provider.endpoint)
2500
2284
  throw new Error(
2501
- `Policy profile "${params.profileId}" is not compatible with ${params.universe ?? "unknown"}:${params.assetClass ?? "unknown"}`
2285
+ "Jev account endpoint changed; update the frozen provider configuration"
2502
2286
  );
2287
+ const started = Date.now();
2288
+ const state = stateForJevInput(input);
2289
+ const responseId = (0, import_jev5.jevHash)({
2290
+ state,
2291
+ questionsHash,
2292
+ provider,
2293
+ user: (0, import_jev5.jevHash)(userName)
2294
+ });
2295
+ const responseFile = import_node_path.default.join(dir, "responses", `${responseId}.json`);
2296
+ let responsePromise = pendingResponses.get(responseFile);
2297
+ if (!responsePromise) {
2298
+ responsePromise = (async () => {
2299
+ const saved = await (0, import_jev5.readJevArtifact)(responseFile);
2300
+ if (saved) return (0, import_jev5.validateJevResponse)(saved, input.questions);
2301
+ const fresh = await (0, import_jev5.requestJev)(provider, apiKey, state, questions);
2302
+ await (0, import_jev5.writeJevArtifact)(responseFile, fresh);
2303
+ return fresh;
2304
+ })();
2305
+ pendingResponses.set(responseFile, responsePromise);
2503
2306
  }
2504
- return profile;
2307
+ let response;
2308
+ try {
2309
+ response = await responsePromise;
2310
+ } finally {
2311
+ pendingResponses.delete(responseFile);
2312
+ }
2313
+ const scores = Object.fromEntries(
2314
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2315
+ key,
2316
+ input.questions.includes(key) ? response.answers[key].score / (0, import_jev5.jevScoreMaximum)(key) : null
2317
+ ])
2318
+ );
2319
+ const value = {
2320
+ schema: "tradejs-jev-record/v4",
2321
+ id,
2322
+ inputHash,
2323
+ questionsHash,
2324
+ provider,
2325
+ input,
2326
+ response,
2327
+ scores,
2328
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2329
+ elapsedMs: Date.now() - started
2330
+ };
2331
+ await (0, import_jev5.writeJevArtifact)(file, value);
2332
+ return await (0, import_jev5.readJevArtifact)(file);
2333
+ };
2334
+ const pendingKey = `${file}:${(0, import_jev5.jevHash)(userName)}`;
2335
+ let promise = pending.get(pendingKey);
2336
+ if (!promise) {
2337
+ promise = getRecord();
2338
+ pending.set(pendingKey, promise);
2505
2339
  }
2506
- const matching = profiles.filter(
2507
- (profile) => profileMatches(profile, params.universe, params.assetClass)
2508
- );
2509
- const defaultProfile = matching.find(
2510
- ({ id }) => id === manifest?.defaultPolicyProfileId
2511
- );
2512
- return defaultProfile ?? matching[0];
2513
- };
2514
- var getStrategyProfileAiAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.aiAdapter ?? manifest?.aiAdapter;
2515
- var getStrategyProfileMlAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.mlAdapter ?? manifest?.mlAdapter;
2516
-
2517
- // src/strategyAdapters/ai.ts
2518
- var toRecord2 = (value) => {
2519
- if (!value || typeof value !== "object" || Array.isArray(value)) {
2520
- return {};
2340
+ let record;
2341
+ try {
2342
+ record = await promise;
2343
+ } finally {
2344
+ pending.delete(pendingKey);
2521
2345
  }
2522
- return value;
2523
- };
2524
- var buildBaseAiPayload = (signal) => {
2525
- const { jevEvidence: _jevEvidence, ...signalAdditionalIndicators } = toRecord2(
2526
- signal.additionalIndicators
2527
- );
2528
- const additionalIndicators = {
2529
- ...signalAdditionalIndicators,
2530
- marketContext: buildAiMarketContext(signal)
2531
- };
2532
2346
  return {
2533
- signal: {
2534
- symbol: signal.symbol,
2535
- signalId: signal.signalId,
2536
- interval: signal.interval,
2537
- direction: signal.direction,
2538
- timestamp: signal.timestamp,
2539
- strategy: signal.strategy,
2540
- prices: {
2541
- currentPrice: signal.prices.currentPrice,
2542
- takeProfitPrice: signal.prices.takeProfitPrice,
2543
- stopLossPrice: signal.prices.stopLossPrice
2544
- }
2545
- },
2546
- figures: trimSeriesDeep(signal.figures ?? {}),
2547
- indicators: buildCompactAiIndicatorsSnapshot(signal.indicators),
2548
- additionalIndicators: trimSeriesDeep(additionalIndicators)
2347
+ record,
2348
+ assessment: {
2349
+ ...base,
2350
+ status: "available",
2351
+ recordId: id,
2352
+ model: record.response.model,
2353
+ scores: record.scores,
2354
+ confidence: Object.fromEntries(
2355
+ import_jev4.JEV_DIMENSIONS.map((key) => [
2356
+ key,
2357
+ record.response.answers[key]?.confidence ?? null
2358
+ ])
2359
+ )
2360
+ }
2549
2361
  };
2550
2362
  };
2551
- var defaultAiAdapter = {};
2552
- var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
2553
- var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
2554
- var buildAiPayloadByStrategy = (signal) => {
2555
- const basePayload = buildBaseAiPayload(signal);
2556
- const adapter = getSignalAiAdapter(signal);
2557
- return adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
2558
- };
2559
- var buildAiSystemPromptAddonByStrategy = (signal) => getSignalAiAdapter(signal).buildSystemPromptAddon?.({ signal }) ?? "";
2560
- var buildAiHumanPromptAddonByStrategy = (signal, payload) => getSignalAiAdapter(signal).buildHumanPromptAddon?.({
2561
- signal,
2562
- payload
2563
- }) ?? "";
2564
- var postProcessAiAnalysisByStrategy = (signal, analysis, payload = buildAiPayloadByStrategy(signal)) => getSignalAiAdapter(signal).postProcessAnalysis?.({
2363
+ var assessSignalWithJev = async ({
2565
2364
  signal,
2566
- payload,
2567
- analysis
2568
- }) ?? analysis;
2569
- var postProcessLocalAiAnalysisByStrategy = (signal, analysis, payload = buildAiPayloadByStrategy(signal)) => {
2570
- const adapter = getSignalAiAdapter(signal);
2571
- const strategyAnalysis = adapter.postProcessAnalysis?.({ signal, payload, analysis }) ?? analysis;
2572
- return adapter.postProcessLocalAnalysis?.({
2573
- signal,
2574
- payload,
2575
- analysis: strategyAnalysis
2576
- }) ?? strategyAnalysis;
2365
+ config,
2366
+ userName,
2367
+ projectRoot,
2368
+ strict,
2369
+ scope = "runtime"
2370
+ }) => {
2371
+ let assessment;
2372
+ try {
2373
+ const input = buildJevInput(signal);
2374
+ ({ assessment } = await evaluateJevInput({
2375
+ input,
2376
+ config,
2377
+ userName,
2378
+ projectRoot
2379
+ }));
2380
+ } catch (error) {
2381
+ if (strict) throw error;
2382
+ assessment = {
2383
+ questionSet: import_jev4.JEV_QUESTION_SET,
2384
+ schema: "tradejs-signal-assessment/v5",
2385
+ source: config.source,
2386
+ mode: config.mode,
2387
+ status: "unavailable",
2388
+ inputHash: "",
2389
+ questionsHash: "",
2390
+ geometryStatus: "absent",
2391
+ levelsValid: null,
2392
+ model: config.provider?.model ?? config.modelSha256 ?? "",
2393
+ scores: emptyScores(),
2394
+ confidence: emptyScores()
2395
+ };
2396
+ }
2397
+ signal.assessment = assessment;
2398
+ signal.additionalIndicators = {
2399
+ ...signal.additionalIndicators,
2400
+ jev: (0, import_jev4.jevAssessmentFeatures)(assessment)
2401
+ };
2402
+ const identity = {
2403
+ signalId: signal.signalId,
2404
+ strategy: signal.strategy,
2405
+ symbol: signal.symbol,
2406
+ timestamp: signal.timestamp,
2407
+ scope,
2408
+ user: (0, import_jev5.jevHash)(userName),
2409
+ config,
2410
+ assessment
2411
+ };
2412
+ await (0, import_jev5.writeJevArtifact)(
2413
+ import_node_path.default.resolve(
2414
+ projectRoot,
2415
+ config.recordsDir ?? "data/ai/jev",
2416
+ "decisions",
2417
+ `${(0, import_jev5.jevHash)(identity)}.json`
2418
+ ),
2419
+ identity
2420
+ );
2421
+ return assessment;
2577
2422
  };
2578
2423
 
2579
- // src/aiProvider.ts
2580
- var import_aiEndpoints = require("@tradejs/core/aiEndpoints");
2581
- var import_aiModels = require("@tradejs/core/aiModels");
2582
- var import_aiLanguages = require("@tradejs/core/aiLanguages");
2583
- var import_userSettings2 = require("@tradejs/infra/userSettings");
2584
- var DEFAULT_AI_MODEL = "openai/gpt-5-mini";
2585
- var userSettingsCache = /* @__PURE__ */ new Map();
2586
- var aiModelCache = /* @__PURE__ */ new Map();
2587
- var normalizeResponseContent = (content) => {
2588
- if (typeof content === "string") return content;
2589
- if (content && typeof content === "object" && !Array.isArray(content)) {
2590
- return content;
2591
- }
2592
- if (Array.isArray(content)) {
2593
- return content.map(
2594
- (part) => typeof part?.text === "string" ? part.text : ""
2595
- ).join("\n").trim();
2424
+ // src/strategyRuntime.ts
2425
+ var import_constants5 = require("@tradejs/core/constants");
2426
+ var import_strategies6 = require("@tradejs/core/strategies");
2427
+ var import_logger11 = require("@tradejs/infra/logger");
2428
+
2429
+ // src/strategyHelpers/runtime.ts
2430
+ var import_logger9 = require("@tradejs/infra/logger");
2431
+ var import_constants3 = require("@tradejs/core/constants");
2432
+ var import_ml2 = require("@tradejs/infra/ml");
2433
+
2434
+ // src/strategy/entrySubmissionValidation.ts
2435
+ var import_logger3 = require("@tradejs/infra/logger");
2436
+
2437
+ // src/strategy/entryExecutionValidation.ts
2438
+ var EntrySubmissionValidationError = class extends Error {
2439
+ constructor() {
2440
+ super(...arguments);
2441
+ this.name = "EntrySubmissionValidationError";
2596
2442
  }
2597
- return String(content ?? "");
2598
2443
  };
2599
- var getAiInvocationError = (error) => {
2600
- const details = error instanceof Error && error.message.trim() ? error.message.trim() : String(error);
2601
- const isEmptyCompletion = error instanceof TypeError && /Cannot read properties of undefined \(reading ['"]message['"]\)/.test(
2602
- details
2603
- );
2604
- const wrapped = new Error(
2605
- isEmptyCompletion ? "AI provider returned an empty chat completion" : `AI model invocation failed: ${details}`
2606
- );
2607
- wrapped.cause = error;
2608
- return wrapped;
2444
+ var resolveEntryExecutionLimits = (config, direction) => {
2445
+ const side = config[direction];
2446
+ const minimum = side && typeof side === "object" && "minRiskRatio" in side ? side.minRiskRatio : void 0;
2447
+ const maximum = config.MAX_LOSS_VALUE;
2448
+ if (minimum == null && maximum == null) return void 0;
2449
+ const minimumNetRiskRatio = minimum == null ? void 0 : Number(minimum);
2450
+ const maxLossValue = maximum == null ? void 0 : Number(maximum);
2451
+ const feeRate = Number(config.RISK_FEE_RATE ?? 0);
2452
+ const slippage = Number(config.RISK_SLIPPAGE_BPS ?? 0);
2453
+ const impact = Number(config.RISK_MARKET_IMPACT_BPS ?? 0);
2454
+ const slippageBps = slippage + impact;
2455
+ 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)
2456
+ throw new Error("INVALID_ENTRY_EXECUTION_LIMITS");
2457
+ return { minimumNetRiskRatio, maxLossValue, feeRate, slippageBps };
2609
2458
  };
2610
- var isEmptyResponseContent = (content) => typeof content === "string" ? content.trim().length === 0 : Object.keys(content).length === 0;
2611
- var getAiModelCacheKey = (userName, modelName, temperature) => `${userName}::${modelName}::${temperature}`;
2612
- var resolveAiModelName = (settings, requestedModelName) => {
2613
- const explicitModelName = requestedModelName?.trim() ?? "";
2614
- if (explicitModelName) return explicitModelName;
2615
- return settings.AI_MODEL?.trim() || DEFAULT_AI_MODEL;
2459
+ var validateEntryExecution = ({
2460
+ direction,
2461
+ price,
2462
+ qty,
2463
+ stopLossPrice,
2464
+ takeProfitPrice,
2465
+ limits
2466
+ }) => {
2467
+ if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(qty) || qty <= 0 || stopLossPrice == null || !Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || !Number.isFinite(takeProfitPrice) || takeProfitPrice <= 0)
2468
+ throw new EntrySubmissionValidationError(
2469
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2470
+ );
2471
+ const side = direction === "LONG" ? 1 : -1;
2472
+ if (side * (price - stopLossPrice) <= 0 || side * (takeProfitPrice - price) <= 0)
2473
+ throw new EntrySubmissionValidationError(
2474
+ "INVALID_ENTRY_PROTECTION_AT_SUBMISSION"
2475
+ );
2476
+ const slippage = limits.slippageBps / 1e4;
2477
+ const cost = (value, sign) => value * slippage + value * (1 + sign * slippage) * limits.feeRate;
2478
+ const entryCost = cost(price, side);
2479
+ const lossPerUnit = Math.abs(price - stopLossPrice) + entryCost + cost(stopLossPrice, -side);
2480
+ const rewardPerUnit = Math.max(
2481
+ 0,
2482
+ Math.abs(takeProfitPrice - price) - entryCost - cost(takeProfitPrice, -side)
2483
+ );
2484
+ const netRiskRatio = rewardPerUnit / lossPerUnit;
2485
+ const plannedLossValue = qty * lossPerUnit;
2486
+ if (!Number.isFinite(netRiskRatio) || !Number.isFinite(plannedLossValue))
2487
+ throw new EntrySubmissionValidationError(
2488
+ "ENTRY_ECONOMICS_INPUTS_UNAVAILABLE"
2489
+ );
2490
+ const belowMinimumNetRiskRatio = limits.minimumNetRiskRatio != null && netRiskRatio <= limits.minimumNetRiskRatio;
2491
+ const riskExceeded = limits.maxLossValue != null && plannedLossValue > limits.maxLossValue * (1 + 1e-9);
2492
+ return {
2493
+ netRiskRatio,
2494
+ plannedLossValue,
2495
+ riskExceeded,
2496
+ belowMinimumNetRiskRatio
2497
+ };
2616
2498
  };
2617
- var getOpenRouterModelKwargs = (apiEndpoint) => {
2618
- const endpoint = String(apiEndpoint ?? "").trim();
2619
- if (!endpoint) return {};
2620
- let hostname = "";
2499
+
2500
+ // src/strategy/entrySubmissionValidation.ts
2501
+ var MAX_ENTRY_QUOTE_AGE_MS = 5e3;
2502
+ var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
2503
+ var getOrderArrivalSnapshot = async ({
2504
+ connector,
2505
+ symbol,
2506
+ requireTimestamp = false
2507
+ }) => {
2508
+ if (typeof connector.getTopOfBookTicker !== "function") {
2509
+ return {
2510
+ arrivalSnapshotTime: Date.now(),
2511
+ arrivalSource: "unavailable",
2512
+ bid: null,
2513
+ ask: null,
2514
+ arrivalMid: null,
2515
+ spreadBps: null
2516
+ };
2517
+ }
2621
2518
  try {
2622
- hostname = new URL(endpoint).hostname;
2623
- } catch {
2624
- hostname = endpoint;
2519
+ const ticker = await connector.getTopOfBookTicker(symbol);
2520
+ const arrivalSnapshotTime = toFiniteNumberOrNull(ticker?.timestamp);
2521
+ const timestampUnavailable = requireTimestamp && arrivalSnapshotTime == null;
2522
+ const bid = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.bidPrice);
2523
+ const ask = timestampUnavailable ? null : toFiniteNumberOrNull(ticker?.askPrice);
2524
+ const arrivalMid = bid != null && ask != null ? (bid + ask) / 2 : null;
2525
+ const spreadBps = bid != null && ask != null && arrivalMid != null && arrivalMid > 0 ? (ask - bid) / arrivalMid * 1e4 : null;
2526
+ return {
2527
+ arrivalSnapshotTime: arrivalSnapshotTime ?? Date.now(),
2528
+ arrivalSource: "top_of_book",
2529
+ bid,
2530
+ ask,
2531
+ arrivalMid,
2532
+ spreadBps
2533
+ };
2534
+ } catch (error) {
2535
+ import_logger3.logger.warn(
2536
+ "runtime order arrival snapshot failed: %s %s",
2537
+ symbol,
2538
+ error?.message || String(error)
2539
+ );
2540
+ return {
2541
+ arrivalSnapshotTime: Date.now(),
2542
+ arrivalSource: "top_of_book_error",
2543
+ bid: null,
2544
+ ask: null,
2545
+ arrivalMid: null,
2546
+ spreadBps: null
2547
+ };
2625
2548
  }
2626
- return hostname.toLowerCase().includes("openrouter") ? { provider: { ignore: ["azure"] } } : {};
2627
2549
  };
2628
- var getAiUserSettings = async (userName = "root") => {
2629
- let settingsPromise = userSettingsCache.get(userName);
2630
- if (!settingsPromise) {
2631
- settingsPromise = (0, import_userSettings2.getUserSettings)(userName).then((settings2) => {
2632
- const endpoint = (0, import_aiEndpoints.normalizeAiEndpoint)(settings2.AI_API_ENDPOINT);
2633
- return {
2634
- ...settings2,
2635
- AI_API_ENDPOINT: endpoint,
2636
- AI_MODEL: (0, import_aiModels.normalizeAiModel)(settings2.AI_MODEL, endpoint),
2637
- AI_RESPONSE_LANGUAGE: (0, import_aiLanguages.normalizeAiResponseLanguage)(
2638
- settings2.AI_RESPONSE_LANGUAGE
2639
- )
2640
- };
2641
- });
2642
- settingsPromise.catch(() => userSettingsCache.delete(userName));
2643
- userSettingsCache.set(userName, settingsPromise);
2550
+ var validateEntryProtectionAtArrival = ({
2551
+ direction,
2552
+ signalPrice,
2553
+ bid,
2554
+ ask,
2555
+ arrivalMid,
2556
+ takeProfits,
2557
+ stopLossPrice
2558
+ }) => {
2559
+ const entryReference = direction === "LONG" ? ask ?? arrivalMid ?? signalPrice : bid ?? arrivalMid ?? signalPrice;
2560
+ const stopReference = direction === "LONG" ? bid ?? arrivalMid ?? signalPrice : ask ?? arrivalMid ?? signalPrice;
2561
+ if (!Number.isFinite(entryReference) || entryReference <= 0 || !Number.isFinite(stopReference) || stopReference <= 0) {
2562
+ throw new Error("INVALID_ENTRY_REFERENCE_AT_ARRIVAL");
2644
2563
  }
2645
- const settings = await settingsPromise;
2646
- if (!settings.AI_API_KEY || !settings.AI_API_ENDPOINT) {
2647
- throw new Error(`AI settings are incomplete for user ${userName}`);
2564
+ for (const takeProfit of takeProfits) {
2565
+ if (!Number.isFinite(takeProfit.price) || takeProfit.price <= 0 || (direction === "LONG" ? takeProfit.price <= entryReference : takeProfit.price >= entryReference)) {
2566
+ throw new Error("TAKE_PROFIT_CROSSED_BEFORE_ENTRY");
2567
+ }
2648
2568
  }
2649
- return settings;
2650
- };
2651
- var getAiModel = async (userName = "root", requestedModelName, temperature = 0.2) => {
2652
- const settings = await getAiUserSettings(userName);
2653
- const modelName = resolveAiModelName(settings, requestedModelName);
2654
- const cacheKey = getAiModelCacheKey(userName, modelName, temperature);
2655
- let modelPromise = aiModelCache.get(cacheKey);
2656
- if (!modelPromise) {
2657
- modelPromise = import("@langchain/openai").then(({ ChatOpenAI }) => {
2658
- const modelKwargs = getOpenRouterModelKwargs(settings.AI_API_ENDPOINT);
2659
- return new ChatOpenAI({
2660
- temperature,
2661
- modelName,
2662
- apiKey: settings.AI_API_KEY,
2663
- ...Object.keys(modelKwargs).length ? { modelKwargs } : {},
2664
- configuration: {
2665
- baseURL: settings.AI_API_ENDPOINT,
2666
- defaultHeaders: {
2667
- "HTTP-Referer": "https://tradejs.dev",
2668
- "X-Title": "Inv"
2669
- }
2670
- }
2671
- });
2672
- });
2673
- modelPromise.catch(() => aiModelCache.delete(cacheKey));
2674
- aiModelCache.set(cacheKey, modelPromise);
2675
- }
2676
- try {
2677
- return await modelPromise;
2678
- } catch (error) {
2679
- aiModelCache.delete(cacheKey);
2680
- userSettingsCache.delete(userName);
2681
- throw error;
2569
+ if (stopLossPrice != null && (!Number.isFinite(stopLossPrice) || stopLossPrice <= 0 || (direction === "LONG" ? stopLossPrice >= stopReference : stopLossPrice <= stopReference))) {
2570
+ throw new Error("STOP_LOSS_CROSSED_BEFORE_ENTRY");
2682
2571
  }
2683
2572
  };
2684
- var invokeAiChatWithUserMessageEncoding = async ({
2685
- messages,
2686
- userName = "root",
2687
- model,
2688
- temperature = 0.2
2689
- }, resolveUserMessageEncoding) => {
2690
- const [{ HumanMessage, SystemMessage }, aiModel] = await Promise.all([
2691
- import("@langchain/core/messages"),
2692
- getAiModel(userName, model, temperature)
2693
- ]);
2694
- const providerMessages = messages.map(
2695
- (message) => message.role === "system" ? new SystemMessage(message.content) : new HumanMessage(
2696
- resolveUserMessageEncoding(message) === "text-block" ? { content: [{ type: "text", text: message.content }] } : message.content
2697
- )
2698
- );
2699
- try {
2700
- const response = await aiModel.invoke(providerMessages);
2701
- const content = normalizeResponseContent(response?.content);
2702
- if (isEmptyResponseContent(content)) {
2703
- throw new Error("AI provider returned an empty chat completion");
2573
+ var createEntrySubmissionValidation = ({
2574
+ connector,
2575
+ symbol,
2576
+ direction,
2577
+ takeProfits,
2578
+ stopLossPrice,
2579
+ limits,
2580
+ onValidated
2581
+ }) => {
2582
+ if (!limits) return void 0;
2583
+ if (!connector.capabilities?.orderSubmissionValidation)
2584
+ throw new EntrySubmissionValidationError(
2585
+ "ENTRY_SUBMISSION_VALIDATION_UNSUPPORTED"
2586
+ );
2587
+ return async ({ qty, executionPrice }) => {
2588
+ const simulated = Reflect.get(connector, "__tradejsTestConnector") === true;
2589
+ const snapshot2 = simulated && executionPrice != null ? {
2590
+ arrivalSnapshotTime: Date.now(),
2591
+ arrivalSource: "simulation",
2592
+ bid: executionPrice,
2593
+ ask: executionPrice,
2594
+ arrivalMid: executionPrice,
2595
+ spreadBps: 0
2596
+ } : await getOrderArrivalSnapshot({
2597
+ connector,
2598
+ symbol,
2599
+ requireTimestamp: true
2600
+ });
2601
+ const age = Date.now() - snapshot2.arrivalSnapshotTime;
2602
+ if (snapshot2.bid == null || snapshot2.ask == null || !Number.isFinite(snapshot2.bid) || !Number.isFinite(snapshot2.ask) || snapshot2.bid <= 0 || snapshot2.ask < snapshot2.bid)
2603
+ throw new EntrySubmissionValidationError(
2604
+ "ENTRY_QUOTE_UNAVAILABLE_AT_SUBMISSION"
2605
+ );
2606
+ if (!Number.isFinite(age) || age < -MAX_ENTRY_QUOTE_AGE_MS || age > MAX_ENTRY_QUOTE_AGE_MS)
2607
+ throw new EntrySubmissionValidationError(
2608
+ "ENTRY_QUOTE_STALE_AT_SUBMISSION"
2609
+ );
2610
+ const price = direction === "LONG" ? snapshot2.ask : snapshot2.bid;
2611
+ validateEntryProtectionAtArrival({
2612
+ direction,
2613
+ signalPrice: price,
2614
+ bid: snapshot2.bid,
2615
+ ask: snapshot2.ask,
2616
+ arrivalMid: snapshot2.arrivalMid,
2617
+ takeProfits,
2618
+ stopLossPrice
2619
+ });
2620
+ const prices = takeProfits.map((tp) => tp.price);
2621
+ const economics2 = validateEntryExecution({
2622
+ direction,
2623
+ price,
2624
+ qty,
2625
+ stopLossPrice,
2626
+ takeProfitPrice: direction === "LONG" ? Math.max(...prices) : Math.min(...prices),
2627
+ limits
2628
+ });
2629
+ if (economics2.belowMinimumNetRiskRatio) {
2630
+ import_logger3.logger.warn(
2631
+ "entry submission warning: %s %s ENTRY_RR_BELOW_MIN_AT_SUBMISSION (RR %s, threshold %s)",
2632
+ symbol,
2633
+ direction,
2634
+ economics2.netRiskRatio,
2635
+ limits.minimumNetRiskRatio
2636
+ );
2704
2637
  }
2705
- return { content };
2706
- } catch (error) {
2707
- throw getAiInvocationError(error);
2708
- }
2638
+ onValidated?.(snapshot2, economics2);
2639
+ };
2709
2640
  };
2710
- var invokeAiPromptChat = (options) => invokeAiChatWithUserMessageEncoding(options, () => "text-block");
2711
2641
 
2712
2642
  // src/ai.ts
2713
- var parseAIResponse = (input) => {
2714
- try {
2715
- if (typeof input === "object" && input !== null) return input;
2716
- const match = input.match(/\{[\s\S]*\}/);
2717
- if (!match) throw new Error("JSON block not found");
2718
- return JSON.parse(match[0]);
2719
- } catch (err) {
2720
- console.error("Failed to parse AI response:", err);
2721
- console.log("Raw AI response:", input);
2722
- return {};
2643
+ var import_aiLanguages2 = require("@tradejs/core/aiLanguages");
2644
+ var import_redis3 = require("@tradejs/infra/redis");
2645
+
2646
+ // src/strategyAdapters/ai.ts
2647
+ var import_indicators2 = require("@tradejs/core/indicators");
2648
+ var import_aiFeatures = require("@tradejs/core/aiFeatures");
2649
+
2650
+ // src/aiShared.ts
2651
+ var MAX_AI_SERIES_POINTS = 5;
2652
+ var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
2653
+ "tradejs.indicators.compactSnapshot"
2654
+ );
2655
+ var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
2656
+ var trimSeriesDeep = (value) => {
2657
+ if (Array.isArray(value)) {
2658
+ const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
2659
+ const isMatrix = trimmed.every((item) => Array.isArray(item));
2660
+ if (isMatrix) {
2661
+ return trimmed;
2662
+ }
2663
+ return trimmed.map(
2664
+ (item) => item && typeof item === "object" ? trimSeriesDeep(item) : item
2665
+ );
2666
+ }
2667
+ if (value && typeof value === "object") {
2668
+ return Object.fromEntries(
2669
+ Object.entries(value).map(([key, nested]) => [
2670
+ key,
2671
+ trimSeriesDeep(nested)
2672
+ ])
2673
+ );
2723
2674
  }
2675
+ return value;
2724
2676
  };
2725
- var normalizeAnalysis = (raw) => {
2726
- const direction = raw?.direction === "LONG" || raw?.direction === "SHORT" ? raw.direction : null;
2727
- const qualityNum = typeof raw?.quality === "number" ? Math.max(1, Math.min(5, Math.round(raw.quality))) : void 0;
2728
- const toNumberOrNull = (value) => {
2729
- if (typeof value === "number" && Number.isFinite(value)) return value;
2730
- if (typeof value === "string" && value.trim()) {
2731
- const parsed = Number(value);
2732
- if (Number.isFinite(parsed)) return parsed;
2733
- }
2734
- return null;
2735
- };
2736
- const toText = (value) => typeof value === "string" ? value.slice(0, 400) : void 0;
2737
- return {
2738
- direction,
2739
- quality: qualityNum,
2740
- needRetest: Boolean(raw?.needRetest),
2741
- retestPrice: toNumberOrNull(raw?.retestPrice),
2742
- takeProfitPrice: toNumberOrNull(raw?.takeProfitPrice),
2743
- stopLossPrice: toNumberOrNull(raw?.stopLossPrice),
2744
- setup: toText(raw?.setup),
2745
- confirmations: toText(raw?.confirmations),
2746
- btcContext: toText(raw?.btcContext),
2747
- retestPlan: toText(raw?.retestPlan),
2748
- riskLevels: toText(raw?.riskLevels),
2749
- qualityReason: toText(raw?.qualityReason),
2750
- triggerInvalidation: toText(raw?.triggerInvalidation),
2751
- comment: typeof raw?.comment === "string" ? raw.comment.slice(0, 1024) : ""
2752
- };
2677
+ var buildCompactAiIndicatorsSnapshot = (value) => {
2678
+ const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
2679
+ if (typeof compactSnapshot === "function") {
2680
+ return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
2681
+ }
2682
+ return trimSeriesDeep(value);
2753
2683
  };
2754
- var asRecord = (value) => {
2684
+
2685
+ // src/aiMarketContext.ts
2686
+ var toRecord = (value) => {
2755
2687
  if (!value || typeof value !== "object" || Array.isArray(value)) {
2756
2688
  return null;
2757
2689
  }
2758
2690
  return value;
2759
2691
  };
2760
- var getSignalDirection = (signal) => signal.direction === "LONG" || signal.direction === "SHORT" ? signal.direction : null;
2761
- var getDeterministicQuality = (gateContext) => {
2762
- const deterministicQuality = Number(gateContext?.deterministicQuality);
2763
- if (Number.isFinite(deterministicQuality)) {
2764
- return Math.max(1, Math.min(5, Math.round(deterministicQuality)));
2765
- }
2766
- const maxAllowedQuality = Number(gateContext?.maxAllowedQuality);
2767
- if (Number.isFinite(maxAllowedQuality)) {
2768
- return Math.max(1, Math.min(5, Math.round(maxAllowedQuality)));
2692
+ var toFiniteNumber = (value) => {
2693
+ const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
2694
+ return Number.isFinite(numeric) ? numeric : null;
2695
+ };
2696
+ var roundTo = (value, decimals) => {
2697
+ const factor = 10 ** decimals;
2698
+ return Math.round(value * factor) / factor;
2699
+ };
2700
+ var buildMissingSpreadContext = () => ({
2701
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2702
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2703
+ available: false,
2704
+ value: null,
2705
+ zScore: null,
2706
+ bps: null,
2707
+ absBps: null,
2708
+ bias: null,
2709
+ severity: null
2710
+ });
2711
+ var buildSpreadContextFromSignal = (signal) => {
2712
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2713
+ const relative = toRecord(baseContext?.relative);
2714
+ const execution = toRecord(relative?.execution);
2715
+ const spread = toFiniteNumber(execution?.venueSpread);
2716
+ const zScore = toFiniteNumber(execution?.venueSpreadZScore);
2717
+ if (spread == null) {
2718
+ return buildMissingSpreadContext();
2769
2719
  }
2770
- return Array.isArray(gateContext?.approvalBlockReasons) && gateContext.approvalBlockReasons.length > 0 || Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length > 0 ? 2 : 3;
2720
+ const value = roundTo(spread, 8);
2721
+ const bps = roundTo(value * 1e4, 2);
2722
+ const absBps = Math.abs(bps);
2723
+ const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
2724
+ const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
2725
+ return {
2726
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2727
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
2728
+ available: true,
2729
+ value,
2730
+ zScore,
2731
+ bps,
2732
+ absBps,
2733
+ bias,
2734
+ severity
2735
+ };
2771
2736
  };
2772
- var buildAiSystemPrompt = (signal) => `
2773
- You are an internal market-structure classifier for an already computed system signal.
2774
- Analyze the provided JSON containing the trade, candles, indicators (for the coin and BTC across multiple timeframes), and strategy figures/context.
2775
- Series data is already trimmed to the latest 5 values.
2776
-
2777
- Important:
2778
- - Do not invent missing data.
2779
- - This is an internal audit/classification task, not user-facing trading advice.
2780
- - Do not generate execution instructions, do not replace the original thesis with a new one, and do not provide personalized investment advice.
2781
- - Use the original signal direction and levels as the anchor, but you may state that the current structure does not support them.
2782
- - Respect the source strategy specified in \`signal.strategy\`.
2783
- - Your goal is to explain how well the observed structure matches the existing signal and how structurally confirmed it is right now.
2784
- - Do not write vague statements like "there is momentum/slope" without tying them to the decision.
2785
- - Write all user-visible text fields in the requested response language. If no explicit language instruction is provided later, default to English.
2786
- - If confidence is incomplete, prefer cautious wording such as "likely", "not confirmed yet", or "probably" instead of categorical claims.
2787
-
2788
- Return exactly one JSON object and nothing else:
2789
-
2790
- {
2791
- "direction": payload.signal.direction | null,
2792
- "quality": 1 | 2 | 3 | 4 | 5,
2793
- "needRetest": boolean,
2794
- "retestPrice": number | null,
2795
- "takeProfitPrice": number | null,
2796
- "stopLossPrice": number | null,
2797
- "setup": string,
2798
- "confirmations": string,
2799
- "btcContext": string,
2800
- "retestPlan": string,
2801
- "riskLevels": string,
2802
- "qualityReason": string,
2803
- "triggerInvalidation": string
2804
- }
2805
-
2806
- - Do not add any other fields.
2807
- - All numbers must be finite, with no \`NaN\` or \`Infinity\`.
2808
- - All text fields must be short strings with no line breaks and no markdown lists.
2809
- - \`direction\` is not a new trade idea. It is only a compatibility flag for the existing signal: either exactly \`payload.signal.direction\` or \`null\` if the current structure does not confirm that signal. Never propose the opposite direction.
2810
- - \`quality\` is the structural confirmation level of the current signal right now, including timing and confirmations. It is not a general attractiveness score and not investment advice.
2811
- - \`needRetest\` indicates whether an additional confirmation level is required before the current signal can be treated as structurally confirmed.
2812
- - \`retestPrice\` is the key level that would confirm or invalidate the structure, or \`null\` if no extra level is needed or available.
2813
- - \`takeProfitPrice\` and \`stopLossPrice\` must not be newly invented levels. If the levels already supplied in \`payload.signal.prices\` still look internally coherent relative to the current price and the confirmed signal, you may return them as an audit of existing levels; otherwise return \`null\`.
2814
- - Use these fields as separate parts of the analysis:
2815
- - \`setup\`: the current structural setup or trendline state.
2816
- - \`confirmations\`: 2-4 concrete confirmations or conflicts from the coin indicators.
2817
- - \`btcContext\`: whether BTC supports the idea, is neutral, or conflicts with it.
2818
- - \`retestPlan\`: what must happen at the key level to confirm the structure, or why no extra level is needed.
2819
- - \`riskLevels\`: a short note on whether the existing levels and risk structure are internally coherent, without creating a new trade plan.
2820
- - \`qualityReason\`: why the quality score is what it is.
2821
- - \`triggerInvalidation\`: what must happen to confirm the signal or what invalidates the current structural thesis.
2822
- - \`comment\` is optional. If you include it, do not just duplicate the structured fields.
2823
-
2824
- If the data is insufficient or the setup is weak, return \`"direction": null\`, \`quality <= 2\`, and explain why.
2825
-
2826
- Input payload structure:
2827
- - payload.signal:
2828
- symbol, signalId, interval, direction, timestamp, strategy, prices
2829
- - payload.signal.prices:
2830
- currentPrice, takeProfitPrice, stopLossPrice
2831
- - payload.figures:
2832
- strategy-specific figures or geometry when available. Fields vary by strategy.
2833
- - payload.indicators:
2834
- historical indicator dictionaries and series for the coin and BTC; all series are already trimmed to the latest 5 values. Treat this block as recent-history transport, not as the primary source of the current shared context.
2835
- - payload.additionalIndicators:
2836
- strategy-specific summary/context fields plus the canonical current shared context snapshot.
2837
- This is not noise; it contains derived fields deliberately passed by the strategy to help the decision.
2838
- Examples: baseContext, helperFlags, structureContext, volatilitySummary.
2839
- Always inspect \`payload.additionalIndicators.baseContext\` first for the current shared state:
2840
- \u2022 \`baseContext.raw\`: current MA, ATR, BB, OBV, price stats, levels, BTC correlation.
2841
- \u2022 \`baseContext.regime\`: derived trend / volatility / momentum / session regime fields.
2842
- \u2022 \`baseContext.structure\`: local range position, breakout freshness/quality, level-touch counts, rejection wick context.
2843
- \u2022 \`baseContext.participation\`: volume/turnover participation, effort-vs-result context, Binance aggTrades, and fingerprinted Hyperliquid whale-flow when available.
2844
- \u2022 \`baseContext.relative\`: BTC/ETH relative-strength, benchmark MA bias context, Binance alt-basket breadth, and CoinMarketCap historical global/exchange/index context when available.
2845
- \u2022 \`baseContext.derivatives\`: Coinalyze-aligned derivatives summary when available.
2846
- \u2022 \`baseContext.mtf\`: compact multi-timeframe summary plus only the latest few candles for each timeframe.
2847
- \u2022 \`baseContext.gateFeatures\`: compact direction-aware fields derived from baseContext; prefer \`setup\`, \`scores\`, \`conflicts\`, \`risk\`, \`decisionHints\`, \`mtf\`, \`volatility\`, \`participation\`, and \`relative\` for quick gate checks before inspecting raw nested context.
2848
- Always inspect \`payload.additionalIndicators.marketContext\` when present:
2849
- \u2022 \`marketContext.execution.binanceCoinbaseSpread\`: AI-friendly BTC spread view projected from \`payload.additionalIndicators.baseContext.relative.execution.venueSpread\`; \`value=(Coinbase-Binance)/Binance\`, \`bps=value*10000\`.
2850
- \u2022 \`marketContext.participation.trueDelta\`: Binance taker buy/sell volume delta from kline payload when \`source=kline_taker_volume\`; otherwise absent/unavailable.
2851
- \u2022 \`marketContext.participation.tradeFlow\`: Binance aggTrades buy/sell pressure buckets when available.
2852
- \u2022 \`marketContext.relative.marketBreadths.top5|top10|top30|top50|top100\`: equal/volume-weighted alt-basket return, advance/decline ratio, and MA breadth for the five versioned Binance breadth universes. \`marketBreadth\` remains the top30 primary view used by existing gates.
2853
- \u2022 \`marketContext.relative.targetVsBtc\`: target/BTC ratio returns, alpha, beta, and short-window correlation; use it to decide whether the target is leading or lagging BTC in the signal direction.
2854
- \u2022 \`marketContext.relative.btcAltRegime\`: Binance-derived BTC-vs-alt basket regime, BTC/alt 24h returns, BTC turnover share, and alt dispersion; use it as a broad alt-market risk pocket.
2855
- \u2022 \`marketContext.relative.cmcGlobal\`: historical CoinMarketCap global market metrics: total/alt market cap, total/alt volume, BTC/ETH dominance and 24h changes, active markets, \`interval\`, and \`altLiquidityRegime\`.
2856
- \u2022 \`marketContext.relative.cmcReferenceAssets\`: historical CoinMarketCap BTC/ETH market-cap and volume context, ETH/BTC market-cap ratio, ETH-vs-BTC volume ratio, \`interval\`, and \`referenceLiquidityRegime\`.
2857
- \u2022 \`marketContext.relative.cmcExchangeLiquidity\`: historical CoinMarketCap major-exchange liquidity aggregate: total volume, 24h volume change, Binance share, concentration, and \`liquidityRegime\`.
2858
- \u2022 \`marketContext.relative.cmcFearGreed\`: historical daily CoinMarketCap Fear & Greed sentiment index: value, classification, 24h/7d value changes, and \`sentimentRegime\`.
2859
- \u2022 \`marketContext.relative.cmcIndexes\`: historical daily CoinMarketCap CMC100/CMC20 index values, 24h changes, top constituents, CMC20/CMC100 ratio, and \`indexRegime\`.
2860
- \u2022 \`marketContext.relative.referenceTradeFlow\`: BTC/ETH reference trade-flow summary used for broad market pressure when the target symbol itself is not BTC/ETH.
2861
- If those fields exist, use them as a more explicit hint instead of trying to re-derive the same idea from raw lines or points.
2862
- If \`baseContext.derivatives\` exists, its top-level \`summary\` and \`intervals\` are the primary BTCUSDT Coinalyze benchmark context for the time of the signal. \`secondaryReferenceSymbol\` identifies the ETHUSDT secondary benchmark, and \`referenceContexts\` contains BTCUSDT/ETHUSDT plus configured extra reference symbols such as BNBUSDT/SOLUSDT/TRXUSDT/XRPUSDT. If \`targetContext\` or \`targetDerived\` exists, those fields are the Coinalyze context for the actual target coin; use them as target-specific positioning evidence, but do not infer target-coin derivatives when they are absent.
2863
- Key patterns:
2864
- \u2022 current shared state: prefer \`payload.additionalIndicators.baseContext\`
2865
- \u2022 recent historical series: \`payload.indicators\`
2866
- \u2022 strategy service keys are possible as well, for example \`touches\`, \`distance\`, timing flags, and other setup-specific summaries
2867
-
2868
- How to analyze, in order:
2869
- 1. Start with price structure and the setup geometry or context in \`payload.figures\`. This has higher priority than indicators.
2870
- 2. Then use \`payload.additionalIndicators.baseContext\` and other explicit strategy-specific context fields.
2871
- 3. Then assess confirmation or conflict from the current shared state and recent coin indicator history.
2872
- 4. Then evaluate BTC context.
2873
- 5. Only after that choose \`direction\`, \`quality\`, and whether an extra confirmation level is required.
2874
- 6. If strong conflicts exist, reduce quality or set direction to \`null\`.
2875
-
2876
- Explicit conflict rules:
2877
- - If the figure or price structure is invalid or doubtful, indicators must not rescue the setup.
2878
- - If strategy-specific helper fields explicitly say the signal is not confirmed yet, lacks margin, or requires waiting, do not overstate quality.
2879
- - If the structure is acceptable but BTC or key indicators noticeably conflict, quality is usually \`<= 3\`.
2880
- - If \`baseContext.derivatives.referenceContexts\` exists, check \`primaryReferenceSymbol\` first as the BTC benchmark, then compare \`secondaryReferenceSymbol\`/ETHUSDT and any target-specific \`targetDerived\`. If \`targetDerived\` exists, compare it to the primary reference instead of treating reference pressure as the target coin's own pressure.
2881
- - If top-level \`baseContext.derivatives.summary.riskFlags\` contains \`crowded_long\` for a LONG or \`crowded_short\` for a SHORT, treat that as broad-market crowded positioning. If \`targetDerived.riskFlags\` contains the same directional crowding, treat that as target-specific crowded positioning.
2882
- - If top-level \`baseContext.derivatives.summary.directionAligned=false\`, explicitly mention the broad-market derivatives conflict in \`confirmations\` or \`qualityReason\`. If \`targetDerived.directionAligned=false\`, explicitly mention the target-specific derivatives conflict.
2883
- - If \`baseContext.derivatives\` is absent, stale, or \`missing_derivatives\`, do not infer Coinalyze conclusions and do not penalize the signal just because that data is missing.
2884
- - Use \`baseContext.regime.session\` directly as the canonical session/liquidity regime: asia is often thinner, europe/us are more active, and overlaps can amplify both momentum and noise. Do not reject a signal solely because of session, but mention clear session support or conflict in \`confirmations\` or \`qualityReason\`.
2885
- - If \`marketContext.execution.binanceCoinbaseSpread.available=true\` and \`severity=elevated/wide\`, treat it as cross-exchange divergence or BTC liquidity risk. Do not use the spread as a standalone long/short signal, but reduce confidence or require more confirmation when the rest of the structure is weak or BTC context conflicts.
2886
- - If \`marketContext.execution.binanceCoinbaseSpread\` is missing or \`available=false\`, do not infer anything from Binance/Coinbase spread and do not penalize the signal just because it is absent.
2887
- - If \`marketContext.participation.trueDelta.available=true\`, use it as better participation evidence than OHLCV-derived proxy delta; still do not let delta override invalid price structure.
2888
- - If \`marketContext.participation.tradeFlow.available=true\` and \`stale=false\`, use it as direct lower-timeframe participation evidence. Treat stale or missing tradeFlow as absent, not as negative evidence.
2889
- - If \`marketContext.relative.marketBreadth.available=true\` and \`stale=false\`, use it as broad alt-market support/conflict. Breadth is contextual; do not let it override the target symbol structure.
2890
- - If \`marketContext.relative.targetVsBtc.available=true\`, treat positive target/BTC ratio trend as support for alt LONGs and negative ratio trend as support for alt SHORTs; ignore it when the target structure is stronger and clearly explains the setup.
2891
- - If \`marketContext.relative.btcAltRegime.available=true\` and \`stale=false\`, treat \`alt_lead\`/\`risk_on\` as broad support for alt LONGs and \`btc_lead\`/\`risk_off\` as pressure against alt LONGs or support for cautious alt SHORTs. Do not use it as a standalone entry reason.
2892
- - If \`marketContext.relative.cmcGlobal.available=true\` and \`stale=false\`, use falling alt market cap/volume or rising BTC dominance as broad risk pressure for alt LONGs. Treat missing CMC history as absent context, not a bearish signal.
2893
- - If \`marketContext.relative.cmcReferenceAssets.available=true\` and \`stale=false\`, use \`eth_led\` as broad support for ETH/high-beta alt strength and \`btc_led\`/\`thin\` as broad caution. Do not describe BTC/ETH reference history as target-symbol flow.
2894
- - If \`marketContext.relative.cmcExchangeLiquidity.available=true\` and \`stale=false\`, treat \`contracting\`, \`thin\`, or \`concentrated\` as broad liquidity risk; \`expanding\` or \`balanced\` supports cleaner execution context but is not a standalone entry reason.
2895
- - If \`marketContext.relative.cmcFearGreed.available=true\` and \`stale=false\`, use \`risk_on\` as broad support for LONGs and \`risk_off\`/\`capitulation\` as broad pressure. Treat \`euphoric\` as overheating/chase caution, not as standalone SHORT proof.
2896
- - If \`marketContext.relative.cmcIndexes.available=true\` and \`stale=false\`, use \`top20_led\` as broad support for mega-cap leadership, \`large_cap_led\` as broader CMC100 participation, and \`risk_off\` as broad pressure. Do not use CMC index history as a standalone entry reason.
2897
- - If \`marketContext.relative.referenceTradeFlow.available=true\`, treat BTC/ETH trade-flow as broad market context only. For alt symbols, do not describe it as the target coin's own flow.
2898
- - If the current signal is not confirmed (\`direction=null\`), name the main reason briefly in \`comment\`.
2899
- If you use the structured fields, include the main reason in \`qualityReason\` or \`triggerInvalidation\`.
2900
-
2901
- Rules for \`direction\` / TP / SL:
2902
- - \`direction = LONG\` only if the data confirms the existing LONG signal; \`SHORT\` only if the data confirms the existing SHORT signal; otherwise \`null\`.
2903
- - For LONG, the expected relation is usually \`stopLossPrice < currentPrice < takeProfitPrice\`.
2904
- - For SHORT, the expected relation is usually \`takeProfitPrice < currentPrice < stopLossPrice\`.
2905
- - Do not optimize or recalculate TP/SL for a "better trade"; only assess whether the already supplied levels are coherent.
2906
- - If \`direction = null\`, then \`takeProfitPrice = null\` and \`stopLossPrice = null\`.
2907
- - If \`needRetest = false\`, then \`retestPrice = null\`.
2908
- - If \`needRetest = true\`, \`retestPrice\` must be a finite number tied to a meaningful retest or breakout level.
2909
- - Before responding, sanity-check the consistency of \`direction\`, TP/SL, and the current price.
2910
-
2911
- Quality scale:
2912
- - 1: poor or chaotic setup, strong conflicts, signal not structurally confirmed
2913
- - 2: weak setup, few confirmations, more of a watch or reject
2914
- - 3: average setup, some structure exists, but notable conflicts remain
2915
- - 4: good setup, several confirmations, structure is mostly coherent
2916
- - 5: very strong setup, clean structure, confirmations, and internally coherent levels
2917
-
2918
- Requirements for useful structured analysis:
2919
- - Include 2-4 concrete factors for or against confirmation in \`confirmations\`.
2920
- - Explicitly mention the role of the key figure or structural state, for example breakout, retest, false break, touch, or lack of confirmation.
2921
- - Explicitly mention BTC context as supportive, neutral, or conflicting.
2922
- - Explain why the quality score is what it is.
2923
- - If the signal is not confirmed (\`direction=null\`), state clearly what must change for confirmation.
2924
- - In \`retestPlan\`, avoid technical placeholders like \`needRetest=false @ null\`; write a human explanation.
2925
- - Do not simply restate JSON fields; add interpretation and decision logic.
2926
-
2927
- Rules for using trimmed series (last 5 values):
2928
- - Do not make strong long-term conclusions from only 5 points.
2929
- - Use 4h and 1d series as brief context, not full history.
2930
- - If the data is too limited for confidence, reduce quality and use cautious wording.
2931
-
2932
- Short few-shot examples:
2933
- {"direction":"LONG","quality":4,"needRetest":true,"retestPrice":100.2,"takeProfitPrice":101.5,"stopLossPrice":98.9,"setup":"Likely trendline breakout upward, but the signal still needs a level check for confirmation.","confirmations":"The coin shows momentum support without obvious overheating, but confirmation is not fully clean yet.","btcContext":"BTC is neutral-to-supportive and does not conflict with the current LONG signal.","retestPlan":"The key level is 100.2; holding above it would confirm the signal structure.","riskLevels":"The supplied TP and SL remain on the correct sides of the current price and still look internally coherent.","qualityReason":"Quality=4 because the structure is solid, but an extra level confirmation is still preferable.","triggerInvalidation":"The structure confirms on a hold above the level and weakens on a move back under the line."}
2934
- {"direction":null,"quality":2,"needRetest":false,"retestPrice":null,"takeProfitPrice":null,"stopLossPrice":null,"setup":"Touch or noise around the trendline without a convincing breakout.","confirmations":"Indicators are mixed and do not provide strong structural support.","btcContext":"BTC is either conflicting or not supportive of the current thesis.","retestPlan":"It is too early to define an extra level because a quality breakout is not present yet.","riskLevels":"The supplied levels should not be treated as confirmed while the structure remains weak.","qualityReason":"Quality=2 because timing is weak and confirmations are limited.","triggerInvalidation":"Wait for a clear breakout and confirmation from both the coin and BTC."}
2935
-
2936
- Return only the JSON object, with no extra characters.
2937
- ${signal ? buildAiSystemPromptAddonByStrategy(signal) : ""}
2938
- `;
2939
- var buildAiPayload = (signal) => buildAiPayloadByStrategy(signal);
2940
- var getDeterministicAiGateContext = (payload) => {
2941
- const additionalIndicators = asRecord(payload.additionalIndicators);
2942
- const candidates = [
2943
- additionalIndicators,
2944
- ...Object.values(additionalIndicators ?? {}).map(asRecord)
2945
- ].filter((value) => Boolean(value));
2946
- return candidates.find(
2947
- (candidate) => Array.isArray(candidate.approvalBlockReasons) || Array.isArray(candidate.riskAnnotations) || Array.isArray(candidate.structuralHardBlockReasons) || typeof candidate.approvalAllowedNow === "boolean"
2948
- ) ?? null;
2737
+ var buildTrueDeltaContextFromSignal = (signal) => {
2738
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2739
+ const participation = toRecord(baseContext?.participation);
2740
+ const delta = toRecord(participation?.delta);
2741
+ const source = String(delta?.source ?? "");
2742
+ const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
2743
+ if (!delta || !isTrueDeltaSource) {
2744
+ return {
2745
+ source: source || null,
2746
+ available: false,
2747
+ buyPressurePct: null,
2748
+ buyVolume: null,
2749
+ sellVolume: null,
2750
+ netDelta: null,
2751
+ deltaPct: null,
2752
+ signedVolumeZScore: null
2753
+ };
2754
+ }
2755
+ return {
2756
+ source,
2757
+ available: true,
2758
+ buyPressurePct: toFiniteNumber(delta.buyPressurePct),
2759
+ buyVolume: toFiniteNumber(delta.buyVolume),
2760
+ sellVolume: toFiniteNumber(delta.sellVolume),
2761
+ netDelta: toFiniteNumber(delta.netDelta),
2762
+ deltaPct: toFiniteNumber(delta.deltaPct),
2763
+ signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
2764
+ };
2765
+ };
2766
+ var buildTradeFlowContextFromSignal = (signal) => {
2767
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2768
+ const participation = toRecord(baseContext?.participation);
2769
+ const tradeFlow = toRecord(participation?.tradeFlow);
2770
+ if (!tradeFlow) {
2771
+ return {
2772
+ source: null,
2773
+ available: false,
2774
+ interval: null,
2775
+ stale: null,
2776
+ trades: null,
2777
+ buyPressurePct: null,
2778
+ netBaseDelta: null,
2779
+ netQuoteDelta: null
2780
+ };
2781
+ }
2782
+ return {
2783
+ source: String(tradeFlow.source ?? ""),
2784
+ available: true,
2785
+ interval: String(tradeFlow.interval ?? ""),
2786
+ stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
2787
+ trades: toFiniteNumber(tradeFlow.trades),
2788
+ buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
2789
+ netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
2790
+ netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
2791
+ };
2792
+ };
2793
+ var buildMarketBreadthContextFromSignal = (signal) => {
2794
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2795
+ const relative = toRecord(baseContext?.relative);
2796
+ const breadth = toRecord(relative?.marketBreadth);
2797
+ if (!breadth) {
2798
+ return {
2799
+ source: null,
2800
+ available: false,
2801
+ universe: null,
2802
+ interval: null,
2803
+ stale: null,
2804
+ symbolsCount: null,
2805
+ advanceDeclineRatio: null,
2806
+ pctAboveMa20: null,
2807
+ pctAboveMa50: null,
2808
+ equalWeightedReturn: null,
2809
+ volumeWeightedReturn: null,
2810
+ dispersion: null
2811
+ };
2812
+ }
2813
+ return {
2814
+ source: String(breadth.source ?? ""),
2815
+ available: true,
2816
+ universe: String(breadth.universe ?? ""),
2817
+ interval: String(breadth.interval ?? ""),
2818
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
2819
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
2820
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
2821
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
2822
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
2823
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
2824
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
2825
+ dispersion: toFiniteNumber(breadth.dispersion)
2826
+ };
2827
+ };
2828
+ var buildMarketBreadthsContextFromSignal = (signal) => {
2829
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2830
+ const relative = toRecord(baseContext?.relative);
2831
+ const breadths = toRecord(relative?.marketBreadths);
2832
+ const build = (key) => {
2833
+ const breadth = toRecord(breadths?.[key]);
2834
+ if (!breadth) {
2835
+ return {
2836
+ source: null,
2837
+ available: false,
2838
+ universe: null,
2839
+ interval: null,
2840
+ stale: null,
2841
+ symbolsCount: null,
2842
+ advanceDeclineRatio: null,
2843
+ pctAboveMa20: null,
2844
+ pctAboveMa50: null,
2845
+ equalWeightedReturn: null,
2846
+ volumeWeightedReturn: null,
2847
+ dispersion: null
2848
+ };
2849
+ }
2850
+ return {
2851
+ source: String(breadth.source ?? ""),
2852
+ available: true,
2853
+ universe: String(breadth.universe ?? ""),
2854
+ interval: String(breadth.interval ?? ""),
2855
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
2856
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
2857
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
2858
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
2859
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
2860
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
2861
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
2862
+ dispersion: toFiniteNumber(breadth.dispersion)
2863
+ };
2864
+ };
2865
+ return {
2866
+ top5: build("top5"),
2867
+ top10: build("top10"),
2868
+ top30: build("top30"),
2869
+ top50: build("top50"),
2870
+ top100: build("top100")
2871
+ };
2872
+ };
2873
+ var buildTargetVsReference = (signal, asset) => {
2874
+ const base = toRecord(signal.additionalIndicators?.baseContext);
2875
+ const relative = toRecord(base?.relative);
2876
+ const target = toRecord(relative?.[`targetVs${asset}`]);
2877
+ return {
2878
+ source: target ? String(target.source ?? "") : null,
2879
+ available: Boolean(target),
2880
+ ratioReturn1h: toFiniteNumber(target?.ratioReturn1h),
2881
+ ratioReturn4h: toFiniteNumber(target?.ratioReturn4h),
2882
+ ratioReturn24h: toFiniteNumber(target?.ratioReturn24h),
2883
+ alpha1h: toFiniteNumber(target?.[`alphaVs${asset}1h`]),
2884
+ alpha4h: toFiniteNumber(target?.[`alphaVs${asset}4h`]),
2885
+ alpha24h: toFiniteNumber(target?.[`alphaVs${asset}24h`]),
2886
+ beta: toFiniteNumber(target?.[`betaTo${asset}20`]),
2887
+ correlation: toFiniteNumber(target?.[`correlationTo${asset}20`]),
2888
+ ratioTrend: typeof target?.ratioTrend === "string" ? target.ratioTrend : null
2889
+ };
2890
+ };
2891
+ var buildTargetVsBtcContextFromSignal = (signal) => {
2892
+ const { alpha1h, alpha4h, alpha24h, beta, correlation, ...shared } = buildTargetVsReference(signal, "Btc");
2893
+ return {
2894
+ ...shared,
2895
+ alphaVsBtc1h: alpha1h,
2896
+ alphaVsBtc4h: alpha4h,
2897
+ alphaVsBtc24h: alpha24h,
2898
+ betaToBtc20: beta,
2899
+ correlationToBtc20: correlation
2900
+ };
2901
+ };
2902
+ var buildTargetVsEthContextFromSignal = (signal) => {
2903
+ const { alpha1h, alpha4h, alpha24h, beta, correlation, ...shared } = buildTargetVsReference(signal, "Eth");
2904
+ return {
2905
+ ...shared,
2906
+ alphaVsEth1h: alpha1h,
2907
+ alphaVsEth4h: alpha4h,
2908
+ alphaVsEth24h: alpha24h,
2909
+ betaToEth20: beta,
2910
+ correlationToEth20: correlation
2911
+ };
2912
+ };
2913
+ var buildBtcAltRegimeContextFromSignal = (signal) => {
2914
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2915
+ const relative = toRecord(baseContext?.relative);
2916
+ const btcAltRegime = toRecord(relative?.btcAltRegime);
2917
+ if (!btcAltRegime) {
2918
+ return {
2919
+ source: null,
2920
+ available: false,
2921
+ universe: null,
2922
+ interval: null,
2923
+ stale: null,
2924
+ regime: null,
2925
+ btcReturn24h: null,
2926
+ altBasketReturn24h: null,
2927
+ btcVsAltReturn24h: null,
2928
+ btcTurnoverShare24h: null,
2929
+ btcTurnoverShareChange24h: null,
2930
+ altVolToBtcVol24h: null,
2931
+ altDispersion24h: null
2932
+ };
2933
+ }
2934
+ return {
2935
+ source: String(btcAltRegime.source ?? ""),
2936
+ available: true,
2937
+ universe: String(btcAltRegime.universe ?? ""),
2938
+ interval: String(btcAltRegime.interval ?? ""),
2939
+ stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
2940
+ regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
2941
+ btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
2942
+ altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
2943
+ btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
2944
+ btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
2945
+ btcTurnoverShareChange24h: toFiniteNumber(
2946
+ btcAltRegime.btcTurnoverShareChange24h
2947
+ ),
2948
+ altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
2949
+ altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
2950
+ };
2951
+ };
2952
+ var buildCmcGlobalContextFromSignal = (signal) => {
2953
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
2954
+ const relative = toRecord(baseContext?.relative);
2955
+ const cmcGlobal = toRecord(relative?.cmcGlobal);
2956
+ if (!cmcGlobal) {
2957
+ return {
2958
+ source: null,
2959
+ available: false,
2960
+ interval: null,
2961
+ asOfTs: null,
2962
+ stale: null,
2963
+ totalMarketCapUsd: null,
2964
+ totalVolumeUsd: null,
2965
+ totalVolumeReportedUsd: null,
2966
+ altMarketCapUsd: null,
2967
+ altVolumeUsd: null,
2968
+ altVolumeReportedUsd: null,
2969
+ btcDominancePct: null,
2970
+ ethDominancePct: null,
2971
+ btcDominanceChange24hPct: null,
2972
+ ethDominanceChange24hPct: null,
2973
+ altMarketCapChange24hPct: null,
2974
+ altVolumeChange24hPct: null,
2975
+ activeCryptocurrencies: null,
2976
+ activeExchanges: null,
2977
+ activeMarketPairs: null,
2978
+ altLiquidityRegime: null
2979
+ };
2980
+ }
2981
+ return {
2982
+ source: String(cmcGlobal.source ?? ""),
2983
+ available: true,
2984
+ interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
2985
+ asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
2986
+ stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
2987
+ totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
2988
+ totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
2989
+ totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
2990
+ altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
2991
+ altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
2992
+ altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
2993
+ btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
2994
+ ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
2995
+ btcDominanceChange24hPct: toFiniteNumber(
2996
+ cmcGlobal.btcDominanceChange24hPct
2997
+ ),
2998
+ ethDominanceChange24hPct: toFiniteNumber(
2999
+ cmcGlobal.ethDominanceChange24hPct
3000
+ ),
3001
+ altMarketCapChange24hPct: toFiniteNumber(
3002
+ cmcGlobal.altMarketCapChange24hPct
3003
+ ),
3004
+ altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
3005
+ activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
3006
+ activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
3007
+ activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
3008
+ altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
3009
+ };
3010
+ };
3011
+ var buildCmcReferenceAssetsContextFromSignal = (signal) => {
3012
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3013
+ const relative = toRecord(baseContext?.relative);
3014
+ const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
3015
+ if (!cmcReferenceAssets) {
3016
+ return {
3017
+ source: null,
3018
+ available: false,
3019
+ interval: null,
3020
+ asOfTs: null,
3021
+ stale: null,
3022
+ btcMarketCapUsd: null,
3023
+ ethMarketCapUsd: null,
3024
+ btcVolumeUsd: null,
3025
+ ethVolumeUsd: null,
3026
+ btcVolumeToMarketCap: null,
3027
+ ethVolumeToMarketCap: null,
3028
+ ethBtcMarketCapRatio: null,
3029
+ ethBtcMarketCapRatioChange24hPct: null,
3030
+ ethVsBtcVolumeRatio: null,
3031
+ referenceLiquidityRegime: null
3032
+ };
3033
+ }
3034
+ return {
3035
+ source: String(cmcReferenceAssets.source ?? ""),
3036
+ available: true,
3037
+ interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
3038
+ asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
3039
+ stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
3040
+ btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
3041
+ ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
3042
+ btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
3043
+ ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
3044
+ btcVolumeToMarketCap: toFiniteNumber(
3045
+ cmcReferenceAssets.btcVolumeToMarketCap
3046
+ ),
3047
+ ethVolumeToMarketCap: toFiniteNumber(
3048
+ cmcReferenceAssets.ethVolumeToMarketCap
3049
+ ),
3050
+ ethBtcMarketCapRatio: toFiniteNumber(
3051
+ cmcReferenceAssets.ethBtcMarketCapRatio
3052
+ ),
3053
+ ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
3054
+ cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
3055
+ ),
3056
+ ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
3057
+ referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
3058
+ };
3059
+ };
3060
+ var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
3061
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3062
+ const relative = toRecord(baseContext?.relative);
3063
+ const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
3064
+ if (!cmcExchangeLiquidity) {
3065
+ return {
3066
+ source: null,
3067
+ available: false,
3068
+ interval: null,
3069
+ asOfTs: null,
3070
+ stale: null,
3071
+ exchangesCount: null,
3072
+ totalVolumeUsd: null,
3073
+ totalVolumeChange24hPct: null,
3074
+ binanceVolumeUsd: null,
3075
+ binanceVolumeShare: null,
3076
+ topExchangeVolumeShare: null,
3077
+ liquidityRegime: null
3078
+ };
3079
+ }
3080
+ return {
3081
+ source: String(cmcExchangeLiquidity.source ?? ""),
3082
+ available: true,
3083
+ interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
3084
+ asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
3085
+ stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
3086
+ exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
3087
+ totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
3088
+ totalVolumeChange24hPct: toFiniteNumber(
3089
+ cmcExchangeLiquidity.totalVolumeChange24hPct
3090
+ ),
3091
+ binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
3092
+ binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
3093
+ topExchangeVolumeShare: toFiniteNumber(
3094
+ cmcExchangeLiquidity.topExchangeVolumeShare
3095
+ ),
3096
+ liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
3097
+ };
3098
+ };
3099
+ var buildCmcFearGreedContextFromSignal = (signal) => {
3100
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3101
+ const relative = toRecord(baseContext?.relative);
3102
+ const cmcFearGreed = toRecord(relative?.cmcFearGreed);
3103
+ if (!cmcFearGreed) {
3104
+ return {
3105
+ source: null,
3106
+ available: false,
3107
+ interval: null,
3108
+ asOfTs: null,
3109
+ stale: null,
3110
+ value: null,
3111
+ valueChange24h: null,
3112
+ valueChange7d: null,
3113
+ classification: null,
3114
+ sentimentRegime: null
3115
+ };
3116
+ }
3117
+ return {
3118
+ source: String(cmcFearGreed.source ?? ""),
3119
+ available: true,
3120
+ interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
3121
+ asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
3122
+ stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
3123
+ value: toFiniteNumber(cmcFearGreed.value),
3124
+ valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
3125
+ valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
3126
+ classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
3127
+ sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
3128
+ };
3129
+ };
3130
+ var buildCmcIndexesContextFromSignal = (signal) => {
3131
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3132
+ const relative = toRecord(baseContext?.relative);
3133
+ const cmcIndexes = toRecord(relative?.cmcIndexes);
3134
+ if (!cmcIndexes) {
3135
+ return {
3136
+ source: null,
3137
+ available: false,
3138
+ interval: null,
3139
+ asOfTs: null,
3140
+ stale: null,
3141
+ cmc100Value: null,
3142
+ cmc100Change24hPct: null,
3143
+ cmc100TopConstituentSymbol: null,
3144
+ cmc100TopConstituentWeightPct: null,
3145
+ cmc20Value: null,
3146
+ cmc20Change24hPct: null,
3147
+ cmc20TopConstituentSymbol: null,
3148
+ cmc20TopConstituentWeightPct: null,
3149
+ cmc20ToCmc100Ratio: null,
3150
+ cmc20ToCmc100RatioChange24hPct: null,
3151
+ indexRegime: null
3152
+ };
3153
+ }
3154
+ return {
3155
+ source: String(cmcIndexes.source ?? ""),
3156
+ available: true,
3157
+ interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
3158
+ asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
3159
+ stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
3160
+ cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
3161
+ cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
3162
+ cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
3163
+ cmc100TopConstituentWeightPct: toFiniteNumber(
3164
+ cmcIndexes.cmc100TopConstituentWeightPct
3165
+ ),
3166
+ cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
3167
+ cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
3168
+ cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
3169
+ cmc20TopConstituentWeightPct: toFiniteNumber(
3170
+ cmcIndexes.cmc20TopConstituentWeightPct
3171
+ ),
3172
+ cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
3173
+ cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
3174
+ cmcIndexes.cmc20ToCmc100RatioChange24hPct
3175
+ ),
3176
+ indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
3177
+ };
2949
3178
  };
2950
- var buildAiHumanPrompt = (signal, payload = buildAiPayload(signal)) => `
2951
- Analyze the already computed internal signal for ${signal.symbol}. The original signal direction is ${signal.direction}.
2952
- This is a structure-classification and audit task, not execution advice. Determine whether the current structure confirms the existing signal, how structurally coherent it is right now, whether an extra confirmation level is needed, and whether the already supplied levels in \`payload.signal.prices\` still look internally coherent. Do not replace the original thesis with a new one and do not invent new levels; return only the requested JSON.
2953
-
2954
- Trade payload:
2955
- ${JSON.stringify(payload)}
2956
- ${buildAiHumanPromptAddonByStrategy(signal, payload)}
2957
- `;
2958
- var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
2959
- const settings = await getAiUserSettings(options.userName);
2960
- const responseLanguage = (0, import_aiLanguages2.getAiResponseLanguagePromptName)(
2961
- settings.AI_RESPONSE_LANGUAGE || import_aiLanguages2.DEFAULT_AI_RESPONSE_LANGUAGE
2962
- );
2963
- const response = await invokeAiPromptChat({
2964
- userName: options.userName,
2965
- model: options.model,
2966
- messages: [
2967
- { role: "system", content: systemPrompt },
2968
- {
2969
- role: "system",
2970
- content: `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
2971
- },
2972
- { role: "user", content: humanPrompt }
2973
- ]
2974
- });
2975
- const parsed = parseAIResponse(response.content);
2976
- const normalized = normalizeAnalysis(parsed);
2977
- if (!options.signal) {
2978
- return normalized;
3179
+ var buildReferenceTradeFlowContextFromSignal = (signal) => {
3180
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
3181
+ const relative = toRecord(baseContext?.relative);
3182
+ const refs = toRecord(relative?.referenceTradeFlow);
3183
+ const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
3184
+ const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
3185
+ const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
3186
+ if (!refs) {
3187
+ return {
3188
+ source: null,
3189
+ available: false,
3190
+ primaryReferenceSymbol: null,
3191
+ referenceSymbols: [],
3192
+ primaryTradeFlowBuyPressurePct: null,
3193
+ primaryTradeFlowStale: null
3194
+ };
2979
3195
  }
2980
- return postProcessAiAnalysisByStrategy(
2981
- options.signal,
2982
- normalized,
2983
- options.payload
2984
- );
3196
+ return {
3197
+ source: String(refs.source ?? ""),
3198
+ available: true,
3199
+ primaryReferenceSymbol,
3200
+ referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
3201
+ primaryTradeFlowBuyPressurePct: toFiniteNumber(
3202
+ primaryTradeFlow?.buyPressurePct
3203
+ ),
3204
+ primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
3205
+ };
2985
3206
  };
2986
- var runAiPromptLocal = async (signal, options = {}) => {
2987
- const payload = options.payload ?? buildAiPayload(signal);
2988
- const gateContext = getDeterministicAiGateContext(payload);
2989
- const signalDirection = getSignalDirection(signal);
2990
- const deterministicQuality = getDeterministicQuality(gateContext);
2991
- const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
2992
- return postProcessLocalAiAnalysisByStrategy(
2993
- signal,
2994
- {
2995
- direction: approvalAllowedNow ? signalDirection : null,
2996
- quality: deterministicQuality,
2997
- needRetest: !approvalAllowedNow,
2998
- retestPrice: null,
2999
- takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
3000
- stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
3001
- },
3002
- payload
3003
- );
3207
+ var buildAiMarketContext = (signal) => ({
3208
+ execution: {
3209
+ binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
3210
+ },
3211
+ participation: {
3212
+ trueDelta: buildTrueDeltaContextFromSignal(signal),
3213
+ tradeFlow: buildTradeFlowContextFromSignal(signal)
3214
+ },
3215
+ relative: {
3216
+ marketBreadth: buildMarketBreadthContextFromSignal(signal),
3217
+ marketBreadths: buildMarketBreadthsContextFromSignal(signal),
3218
+ targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
3219
+ targetVsEth: buildTargetVsEthContextFromSignal(signal),
3220
+ btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
3221
+ cmcGlobal: buildCmcGlobalContextFromSignal(signal),
3222
+ cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
3223
+ cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
3224
+ cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
3225
+ cmcIndexes: buildCmcIndexesContextFromSignal(signal),
3226
+ referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
3227
+ }
3228
+ });
3229
+
3230
+ // src/strategy/policyProfiles.ts
3231
+ var profileMatches = (profile, universe, assetClass) => {
3232
+ const { appliesTo } = profile;
3233
+ if (!appliesTo) return true;
3234
+ if (appliesTo.universes?.length && (!universe || !appliesTo.universes.includes(universe))) {
3235
+ return false;
3236
+ }
3237
+ if (appliesTo.assetClasses?.length && (!assetClass || !appliesTo.assetClasses.includes(assetClass))) {
3238
+ return false;
3239
+ }
3240
+ return true;
3004
3241
  };
3005
- var askAI = async (signal, options = {}) => {
3006
- const { symbol } = signal;
3007
- const payload = buildAiPayload(signal);
3008
- const content = await runAiPrompt(
3009
- {
3010
- systemPrompt: buildAiSystemPrompt(signal),
3011
- humanPrompt: buildAiHumanPrompt(signal, payload)
3012
- },
3013
- {
3014
- ...options,
3015
- signal,
3016
- payload
3242
+ var resolveStrategyPolicyProfile = (manifest, params) => {
3243
+ const profiles = manifest?.policyProfiles ?? [];
3244
+ if (!profiles.length) {
3245
+ const inferredId = params.profileId ?? (params.universe === "tradfi" ? "tradfi" : void 0);
3246
+ if (!inferredId) return void 0;
3247
+ if (inferredId !== "crypto" && inferredId !== "tradfi") {
3248
+ throw new Error(
3249
+ `Unknown policy profile "${inferredId}" for strategy "${manifest?.name}"`
3250
+ );
3251
+ }
3252
+ if (params.universe && inferredId !== params.universe) {
3253
+ throw new Error(
3254
+ `Policy profile "${inferredId}" is not compatible with ${params.universe}`
3255
+ );
3256
+ }
3257
+ return {
3258
+ id: inferredId,
3259
+ appliesTo: { universes: [inferredId] },
3260
+ marketDataRequirements: inferredId === "crypto" ? ["crypto.btcReference"] : [],
3261
+ ...manifest?.mlAdapter ? {
3262
+ entryRuntimeDefaults: {
3263
+ ml: {
3264
+ modelKey: inferredId === "crypto" ? manifest.name : `${manifest.name}:tradfi`
3265
+ }
3266
+ }
3267
+ } : {}
3268
+ };
3269
+ }
3270
+ if (params.profileId) {
3271
+ const profile = profiles.find(({ id }) => id === params.profileId);
3272
+ if (!profile) {
3273
+ throw new Error(
3274
+ `Unknown policy profile "${params.profileId}" for strategy "${manifest?.name}"`
3275
+ );
3276
+ }
3277
+ if (!profileMatches(profile, params.universe, params.assetClass)) {
3278
+ throw new Error(
3279
+ `Policy profile "${params.profileId}" is not compatible with ${params.universe ?? "unknown"}:${params.assetClass ?? "unknown"}`
3280
+ );
3017
3281
  }
3282
+ return profile;
3283
+ }
3284
+ const matching = profiles.filter(
3285
+ (profile) => profileMatches(profile, params.universe, params.assetClass)
3018
3286
  );
3019
- await (0, import_redis.setData)(import_redis.redisKeys.analysis(symbol, signal.signalId), content);
3020
- return content;
3287
+ const defaultProfile = matching.find(
3288
+ ({ id }) => id === manifest?.defaultPolicyProfileId
3289
+ );
3290
+ return defaultProfile ?? matching[0];
3021
3291
  };
3292
+ var getStrategyProfileAiAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.aiAdapter ?? manifest?.aiAdapter;
3293
+ var getStrategyProfileMlAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.mlAdapter ?? manifest?.mlAdapter;
3022
3294
 
3023
- // src/strategyAdapters/ml.ts
3024
- var defaultMlAdapter = {
3025
- normalizeStrategyConfig: (strategyConfig) => strategyConfig
3295
+ // src/strategyAdapters/ai.ts
3296
+ var toRecord2 = (value) => {
3297
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3298
+ return {};
3299
+ }
3300
+ return value;
3026
3301
  };
3027
- var getStrategyMlAdapter = (strategy, profileId) => {
3028
- const strategyAdapter = getStrategyProfileMlAdapter(
3029
- getStrategyManifest(strategy),
3030
- profileId
3302
+ var buildBaseAiPayload = (signal) => {
3303
+ const { jevEvidence: _jevEvidence, ...signalAdditionalIndicators } = toRecord2(
3304
+ signal.additionalIndicators
3031
3305
  );
3032
- if (!strategyAdapter) return defaultMlAdapter;
3033
- return {
3034
- ...defaultMlAdapter,
3035
- ...strategyAdapter
3306
+ const additionalIndicators = {
3307
+ ...signalAdditionalIndicators,
3308
+ marketContext: buildAiMarketContext(signal)
3036
3309
  };
3037
- };
3038
-
3039
- // src/mlPayload.ts
3040
- var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
3041
- return getStrategyMlAdapter(
3042
- strategyName,
3043
- profileId
3044
- ).normalizeStrategyConfig?.(strategyConfig);
3045
- };
3046
- var buildMlPayload = (payload) => {
3047
- const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
3048
- const profileId = payload.signal?.policyProfileId;
3049
- const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
3050
- const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
3051
- const nextSignal = {
3052
- ...normalizedSignal,
3053
- indicators: {
3054
- ...normalizedSignal?.indicators ?? {}
3055
- }
3310
+ return {
3311
+ signal: {
3312
+ symbol: signal.symbol,
3313
+ signalId: signal.signalId,
3314
+ interval: signal.interval,
3315
+ direction: signal.direction,
3316
+ timestamp: signal.timestamp,
3317
+ strategy: signal.strategy,
3318
+ prices: {
3319
+ currentPrice: signal.prices.currentPrice,
3320
+ takeProfitPrice: signal.prices.takeProfitPrice,
3321
+ stopLossPrice: signal.prices.stopLossPrice
3322
+ }
3323
+ },
3324
+ figures: trimSeriesDeep(signal.figures ?? {}),
3325
+ indicators: buildCompactAiIndicatorsSnapshot(signal.indicators),
3326
+ additionalIndicators: trimSeriesDeep(additionalIndicators)
3056
3327
  };
3057
- const nextContext = payload.context ? {
3058
- ...payload.context,
3059
- strategyConfig: normalizeStrategyConfig(
3060
- payload.context.strategyConfig,
3061
- strategyName,
3062
- profileId
3328
+ };
3329
+ var defaultAiAdapter = {};
3330
+ var tickerProfileFeatureIds = new Set(
3331
+ import_aiFeatures.TICKER_PROFILE_AI_FEATURES.map(({ id }) => id)
3332
+ );
3333
+ var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
3334
+ var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
3335
+ var buildAiPayloadByStrategy = (signal) => {
3336
+ const basePayload = buildBaseAiPayload(signal);
3337
+ const adapter = getSignalAiAdapter(signal);
3338
+ const payload = adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
3339
+ const custom = getStrategyManifest(signal.strategy)?.aiFeatures ?? [];
3340
+ const frozen = signal.aiFeatureReplay?.snapshot;
3341
+ if (frozen) (0, import_aiFeatures.validateAiFeatureSnapshot)(frozen);
3342
+ const tickerProfile = signal.additionalIndicators?.tickerProfile ?? signal.additionalIndicators?.baseContext?.tickerProfile;
3343
+ const hasProfile = Boolean(
3344
+ tickerProfile || frozen?.definitions.some(({ id }) => tickerProfileFeatureIds.has(id))
3345
+ );
3346
+ const indicatorFeatures = (0, import_indicators2.getRegisteredIndicatorEntries)(
3347
+ getTradejsProjectCwd()
3348
+ ).flatMap((entry) => entry.indicator.enabled ? entry.aiFeatures ?? [] : []);
3349
+ const hasPortfolio = Boolean(
3350
+ signal.additionalIndicators?.portfolioContext || frozen?.definitions.some(
3351
+ ({ id }) => import_aiFeatures.PORTFOLIO_AI_FEATURES.some((d) => d.id === id)
3063
3352
  )
3064
- } : void 0;
3353
+ );
3354
+ const definitions = [
3355
+ ...hasPortfolio ? import_aiFeatures.PORTFOLIO_AI_FEATURES : [],
3356
+ ...hasProfile ? import_aiFeatures.TICKER_PROFILE_AI_FEATURES : [],
3357
+ ...indicatorFeatures,
3358
+ ...custom
3359
+ ];
3360
+ if (!definitions.length && !signal.aiFeatureReplay?.snapshot) return payload;
3361
+ (0, import_aiFeatures.validateAiFeatureDefinitions)(definitions);
3362
+ if (signal.aiFeatureReplay && !frozen) {
3363
+ throw new Error(
3364
+ "Dataset has no frozen AI features; regenerate the decision-time export"
3365
+ );
3366
+ }
3367
+ if (frozen && JSON.stringify(frozen.definitions) !== JSON.stringify(definitions)) {
3368
+ throw new Error(
3369
+ "AI feature definition changed; regenerate the decision-time export"
3370
+ );
3371
+ }
3065
3372
  return {
3066
- signal: nextSignal,
3067
- context: nextContext
3373
+ ...payload,
3374
+ featureSnapshot: frozen ?? (0, import_aiFeatures.projectAiFeatures)(definitions, {
3375
+ signal: tickerProfile ? {
3376
+ ...signal,
3377
+ additionalIndicators: {
3378
+ ...signal.additionalIndicators,
3379
+ tickerProfile
3380
+ }
3381
+ } : signal,
3382
+ payload
3383
+ })
3068
3384
  };
3069
3385
  };
3386
+ var buildAiSystemPromptAddonByStrategy = (signal) => getSignalAiAdapter(signal).buildSystemPromptAddon?.({ signal }) ?? "";
3387
+ var buildAiHumanPromptAddonByStrategy = (signal, payload) => getSignalAiAdapter(signal).buildHumanPromptAddon?.({
3388
+ signal,
3389
+ payload
3390
+ }) ?? "";
3391
+ var postProcessAiAnalysisByStrategy = (signal, analysis, payload = buildAiPayloadByStrategy(signal)) => getSignalAiAdapter(signal).postProcessAnalysis?.({
3392
+ signal,
3393
+ payload,
3394
+ analysis
3395
+ }) ?? analysis;
3396
+ var postProcessLocalAiAnalysisByStrategy = (signal, analysis, payload = buildAiPayloadByStrategy(signal)) => {
3397
+ const adapter = getSignalAiAdapter(signal);
3398
+ const strategyAnalysis = adapter.postProcessAnalysis?.({ signal, payload, analysis }) ?? analysis;
3399
+ return adapter.postProcessLocalAnalysis?.({
3400
+ signal,
3401
+ payload,
3402
+ analysis: strategyAnalysis
3403
+ }) ?? strategyAnalysis;
3404
+ };
3070
3405
 
3071
- // src/runtimeJournal.ts
3072
- var import_node_crypto = require("crypto");
3073
- var import_constants = require("@tradejs/core/constants");
3074
- var import_time = require("@tradejs/core/time");
3075
- var import_trade = require("@tradejs/core/trade");
3076
- var import_logger3 = require("@tradejs/infra/logger");
3077
- var import_redis2 = require("@tradejs/infra/redis");
3078
- var now = () => Date.now();
3079
- var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
3080
- var calculateClosedPnl = ({
3081
- direction,
3082
- entryPrice,
3083
- exitPrice,
3084
- qty
3085
- }) => {
3086
- const pnl = direction === "LONG" ? (exitPrice - entryPrice) * qty : (entryPrice - exitPrice) * qty;
3087
- return Number.isFinite(pnl) ? pnl : null;
3406
+ // src/aiProvider.ts
3407
+ var import_aiEndpoints = require("@tradejs/core/aiEndpoints");
3408
+ var import_aiModels = require("@tradejs/core/aiModels");
3409
+ var import_aiLanguages = require("@tradejs/core/aiLanguages");
3410
+ var import_userSettings2 = require("@tradejs/infra/userSettings");
3411
+ var DEFAULT_AI_MODEL = "openai/gpt-5-mini";
3412
+ var userSettingsCache = /* @__PURE__ */ new Map();
3413
+ var aiModelCache = /* @__PURE__ */ new Map();
3414
+ var normalizeResponseContent = (content) => {
3415
+ if (typeof content === "string") return content;
3416
+ if (content && typeof content === "object" && !Array.isArray(content)) {
3417
+ return content;
3418
+ }
3419
+ if (Array.isArray(content)) {
3420
+ return content.map(
3421
+ (part) => typeof part?.text === "string" ? part.text : ""
3422
+ ).join("\n").trim();
3423
+ }
3424
+ return String(content ?? "");
3088
3425
  };
3089
- var createRuntimeOrderId = (strategy) => {
3090
- const prefix = (0, import_trade.createRuntimeOrderLinkPrefix)(strategy);
3091
- if (prefix === "tjs-") {
3092
- return `tjs-${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 24).toLowerCase()}`;
3426
+ var getAiInvocationError = (error) => {
3427
+ const details = error instanceof Error && error.message.trim() ? error.message.trim() : String(error);
3428
+ const isEmptyCompletion = error instanceof TypeError && /Cannot read properties of undefined \(reading ['"]message['"]\)/.test(
3429
+ details
3430
+ );
3431
+ const wrapped = new Error(
3432
+ isEmptyCompletion ? "AI provider returned an empty chat completion" : `AI model invocation failed: ${details}`
3433
+ );
3434
+ wrapped.cause = error;
3435
+ return wrapped;
3436
+ };
3437
+ var isEmptyResponseContent = (content) => typeof content === "string" ? content.trim().length === 0 : Object.keys(content).length === 0;
3438
+ var getAiModelCacheKey = (userName, modelName, temperature) => `${userName}::${modelName}::${temperature}`;
3439
+ var resolveAiModelName = (settings, requestedModelName) => {
3440
+ const explicitModelName = requestedModelName?.trim() ?? "";
3441
+ if (explicitModelName) return explicitModelName;
3442
+ return settings.AI_MODEL?.trim() || DEFAULT_AI_MODEL;
3443
+ };
3444
+ var getOpenRouterModelKwargs = (apiEndpoint) => {
3445
+ const endpoint = String(apiEndpoint ?? "").trim();
3446
+ if (!endpoint) return {};
3447
+ let hostname = "";
3448
+ try {
3449
+ hostname = new URL(endpoint).hostname;
3450
+ } catch {
3451
+ hostname = endpoint;
3093
3452
  }
3094
- return `${prefix}${toRandomOrderSuffix()}`;
3453
+ return hostname.toLowerCase().includes("openrouter") ? { provider: { ignore: ["azure"] } } : {};
3095
3454
  };
3096
- var recordRuntimeTradeOpen = async (params) => {
3097
- const { userName } = params;
3098
- if (!userName) {
3099
- return null;
3455
+ var getAiUserSettings = async (userName = "root") => {
3456
+ let settingsPromise = userSettingsCache.get(userName);
3457
+ if (!settingsPromise) {
3458
+ settingsPromise = (0, import_userSettings2.getUserSettings)(userName).then((settings2) => {
3459
+ const endpoint = (0, import_aiEndpoints.normalizeAiEndpoint)(settings2.AI_API_ENDPOINT);
3460
+ return {
3461
+ ...settings2,
3462
+ AI_API_ENDPOINT: endpoint,
3463
+ AI_MODEL: (0, import_aiModels.normalizeAiModel)(settings2.AI_MODEL, endpoint),
3464
+ AI_RESPONSE_LANGUAGE: (0, import_aiLanguages.normalizeAiResponseLanguage)(
3465
+ settings2.AI_RESPONSE_LANGUAGE
3466
+ )
3467
+ };
3468
+ });
3469
+ settingsPromise.catch(() => userSettingsCache.delete(userName));
3470
+ userSettingsCache.set(userName, settingsPromise);
3471
+ }
3472
+ const settings = await settingsPromise;
3473
+ if (!settings.AI_API_KEY || !settings.AI_API_ENDPOINT) {
3474
+ throw new Error(`AI settings are incomplete for user ${userName}`);
3475
+ }
3476
+ return settings;
3477
+ };
3478
+ var getAiModel = async (userName = "root", requestedModelName, temperature = 0.2) => {
3479
+ const settings = await getAiUserSettings(userName);
3480
+ const modelName = resolveAiModelName(settings, requestedModelName);
3481
+ const cacheKey = getAiModelCacheKey(userName, modelName, temperature);
3482
+ let modelPromise = aiModelCache.get(cacheKey);
3483
+ if (!modelPromise) {
3484
+ modelPromise = import("@langchain/openai").then(({ ChatOpenAI }) => {
3485
+ const modelKwargs = getOpenRouterModelKwargs(settings.AI_API_ENDPOINT);
3486
+ return new ChatOpenAI({
3487
+ temperature,
3488
+ modelName,
3489
+ apiKey: settings.AI_API_KEY,
3490
+ ...Object.keys(modelKwargs).length ? { modelKwargs } : {},
3491
+ configuration: {
3492
+ baseURL: settings.AI_API_ENDPOINT,
3493
+ defaultHeaders: {
3494
+ "HTTP-Referer": "https://tradejs.dev",
3495
+ "X-Title": "Inv"
3496
+ }
3497
+ }
3498
+ });
3499
+ });
3500
+ modelPromise.catch(() => aiModelCache.delete(cacheKey));
3501
+ aiModelCache.set(cacheKey, modelPromise);
3100
3502
  }
3101
- const record = {
3102
- ...params,
3103
- entryCount: 1,
3104
- lastEntryPrice: params.entryPrice,
3105
- lastEntryQty: params.qty,
3106
- lastEntryTimestamp: params.entryTimestamp,
3107
- status: "active",
3108
- currentPrice: params.entryPrice,
3109
- currentPnl: 0,
3110
- closedPnl: null,
3111
- exitPrice: null,
3112
- exitTimestamp: null,
3113
- lastSyncedAt: now()
3114
- };
3115
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(record.entryTimestamp);
3116
- const runtimeScopeId = record.deploymentId ?? record.accountId;
3117
3503
  try {
3118
- await Promise.all([
3119
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, record.orderId), record, {
3120
- expire: 0
3121
- }),
3122
- (0, import_redis2.setHashJsonField)(
3123
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3124
- record.orderId,
3125
- record,
3126
- { expire: 0 }
3127
- ),
3128
- (0, import_redis2.setData)(
3129
- import_redis2.redisKeys.runtimeActiveTrade(userName, record.symbol, runtimeScopeId),
3130
- { orderId: record.orderId },
3131
- { expire: 0 }
3132
- )
3133
- ]);
3504
+ return await modelPromise;
3134
3505
  } catch (error) {
3135
- import_logger3.logger.error(
3136
- "runtime trade open journal failed: %s %s",
3137
- record.symbol,
3138
- error?.message || String(error)
3139
- );
3506
+ aiModelCache.delete(cacheKey);
3507
+ userSettingsCache.delete(userName);
3508
+ throw error;
3140
3509
  }
3141
- return record;
3142
3510
  };
3143
- var recordRuntimeTradeIncrease = async (params) => {
3144
- const {
3145
- userName,
3146
- strategy,
3147
- symbol,
3511
+ var invokeAiChatWithUserMessageEncoding = async ({
3512
+ messages,
3513
+ userName = "root",
3514
+ model,
3515
+ temperature = 0.2
3516
+ }, resolveUserMessageEncoding) => {
3517
+ const [{ HumanMessage, SystemMessage }, aiModel] = await Promise.all([
3518
+ import("@langchain/core/messages"),
3519
+ getAiModel(userName, model, temperature)
3520
+ ]);
3521
+ const providerMessages = messages.map(
3522
+ (message) => message.role === "system" ? new SystemMessage(message.content) : new HumanMessage(
3523
+ resolveUserMessageEncoding(message) === "text-block" ? { content: [{ type: "text", text: message.content }] } : message.content
3524
+ )
3525
+ );
3526
+ try {
3527
+ const response = await aiModel.invoke(providerMessages);
3528
+ const content = normalizeResponseContent(response?.content);
3529
+ if (isEmptyResponseContent(content)) {
3530
+ throw new Error("AI provider returned an empty chat completion");
3531
+ }
3532
+ return { content };
3533
+ } catch (error) {
3534
+ throw getAiInvocationError(error);
3535
+ }
3536
+ };
3537
+ var invokeAiPromptChat = (options) => invokeAiChatWithUserMessageEncoding(options, () => "text-block");
3538
+
3539
+ // src/ai.ts
3540
+ var parseAIResponse = (input) => {
3541
+ try {
3542
+ if (typeof input === "object" && input !== null) return input;
3543
+ const match = input.match(/\{[\s\S]*\}/);
3544
+ if (!match) throw new Error("JSON block not found");
3545
+ return JSON.parse(match[0]);
3546
+ } catch (err) {
3547
+ console.error("Failed to parse AI response:", err);
3548
+ console.log("Raw AI response:", input);
3549
+ return {};
3550
+ }
3551
+ };
3552
+ var normalizeAnalysis = (raw) => {
3553
+ const direction = raw?.direction === "LONG" || raw?.direction === "SHORT" ? raw.direction : null;
3554
+ const qualityNum = typeof raw?.quality === "number" ? Math.max(1, Math.min(5, Math.round(raw.quality))) : void 0;
3555
+ const toNumberOrNull = (value) => {
3556
+ if (typeof value === "number" && Number.isFinite(value)) return value;
3557
+ if (typeof value === "string" && value.trim()) {
3558
+ const parsed = Number(value);
3559
+ if (Number.isFinite(parsed)) return parsed;
3560
+ }
3561
+ return null;
3562
+ };
3563
+ const toText = (value) => typeof value === "string" ? value.slice(0, 400) : void 0;
3564
+ return {
3148
3565
  direction,
3149
- resultingQty,
3150
- resultingEntryPrice,
3151
- addedQty,
3152
- addedEntryPrice,
3153
- entryTimestamp,
3154
- fee,
3155
- accountId,
3156
- deploymentId
3157
- } = params;
3158
- if (!userName) {
3566
+ quality: qualityNum,
3567
+ needRetest: Boolean(raw?.needRetest),
3568
+ retestPrice: toNumberOrNull(raw?.retestPrice),
3569
+ takeProfitPrice: toNumberOrNull(raw?.takeProfitPrice),
3570
+ stopLossPrice: toNumberOrNull(raw?.stopLossPrice),
3571
+ setup: toText(raw?.setup),
3572
+ confirmations: toText(raw?.confirmations),
3573
+ btcContext: toText(raw?.btcContext),
3574
+ retestPlan: toText(raw?.retestPlan),
3575
+ riskLevels: toText(raw?.riskLevels),
3576
+ qualityReason: toText(raw?.qualityReason),
3577
+ triggerInvalidation: toText(raw?.triggerInvalidation),
3578
+ comment: typeof raw?.comment === "string" ? raw.comment.slice(0, 1024) : ""
3579
+ };
3580
+ };
3581
+ var asRecord = (value) => {
3582
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3159
3583
  return null;
3160
3584
  }
3161
- const existing = await getActiveRuntimeTrade({
3162
- userName,
3163
- symbol,
3164
- accountId,
3165
- deploymentId
3166
- });
3167
- if (!existing || existing.strategy !== strategy || existing.direction !== direction) {
3168
- return null;
3585
+ return value;
3586
+ };
3587
+ var getSignalDirection = (signal) => signal.direction === "LONG" || signal.direction === "SHORT" ? signal.direction : null;
3588
+ var getDeterministicQuality = (gateContext) => {
3589
+ const deterministicQuality = Number(gateContext?.deterministicQuality);
3590
+ if (Number.isFinite(deterministicQuality)) {
3591
+ return Math.max(1, Math.min(5, Math.round(deterministicQuality)));
3169
3592
  }
3170
- const addedFee = typeof fee === "number" && Number.isFinite(fee) ? fee : 0;
3171
- const openFee = (existing.openFee ?? existing.fee ?? 0) + addedFee;
3172
- const totalFee = (existing.totalFee ?? existing.fee ?? 0) + addedFee;
3173
- const next = {
3174
- ...existing,
3175
- ...params.orderLeverage ? { orderLeverage: params.orderLeverage } : {},
3176
- qty: resultingQty,
3177
- entryPrice: resultingEntryPrice,
3178
- entryCount: Math.max(1, existing.entryCount ?? 1) + 1,
3179
- lastEntryPrice: addedEntryPrice,
3180
- lastEntryQty: addedQty,
3181
- lastEntryTimestamp: entryTimestamp,
3182
- currentPrice: resultingEntryPrice,
3183
- currentPnl: 0,
3184
- fee: openFee,
3185
- openFee,
3186
- totalFee,
3187
- lastSyncedAt: now()
3188
- };
3189
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3190
- try {
3191
- await Promise.all([
3192
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, existing.orderId), next, {
3193
- expire: 0
3194
- }),
3195
- (0, import_redis2.setHashJsonField)(
3196
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3197
- existing.orderId,
3198
- next,
3199
- { expire: 0 }
3200
- )
3201
- ]);
3202
- } catch (error) {
3203
- import_logger3.logger.error(
3204
- "runtime trade increase journal failed: %s %s",
3205
- symbol,
3206
- error?.message || String(error)
3207
- );
3593
+ const maxAllowedQuality = Number(gateContext?.maxAllowedQuality);
3594
+ if (Number.isFinite(maxAllowedQuality)) {
3595
+ return Math.max(1, Math.min(5, Math.round(maxAllowedQuality)));
3208
3596
  }
3209
- return next;
3597
+ return Array.isArray(gateContext?.approvalBlockReasons) && gateContext.approvalBlockReasons.length > 0 || Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length > 0 ? 2 : 3;
3598
+ };
3599
+ var buildAiSystemPrompt = (signal) => `
3600
+ You are an internal market-structure classifier for an already computed system signal.
3601
+ Analyze the provided JSON containing the trade, candles, indicators (for the coin and BTC across multiple timeframes), and strategy figures/context.
3602
+ Series data is already trimmed to the latest 5 values.
3603
+
3604
+ Important:
3605
+ - Do not invent missing data.
3606
+ - This is an internal audit/classification task, not user-facing trading advice.
3607
+ - Do not generate execution instructions, do not replace the original thesis with a new one, and do not provide personalized investment advice.
3608
+ - Use the original signal direction and levels as the anchor, but you may state that the current structure does not support them.
3609
+ - Respect the source strategy specified in \`signal.strategy\`.
3610
+ - Your goal is to explain how well the observed structure matches the existing signal and how structurally confirmed it is right now.
3611
+ - Do not write vague statements like "there is momentum/slope" without tying them to the decision.
3612
+ - Write all user-visible text fields in the requested response language. If no explicit language instruction is provided later, default to English.
3613
+ - If confidence is incomplete, prefer cautious wording such as "likely", "not confirmed yet", or "probably" instead of categorical claims.
3614
+
3615
+ Return exactly one JSON object and nothing else:
3616
+
3617
+ {
3618
+ "direction": payload.signal.direction | null,
3619
+ "quality": 1 | 2 | 3 | 4 | 5,
3620
+ "needRetest": boolean,
3621
+ "retestPrice": number | null,
3622
+ "takeProfitPrice": number | null,
3623
+ "stopLossPrice": number | null,
3624
+ "setup": string,
3625
+ "confirmations": string,
3626
+ "btcContext": string,
3627
+ "retestPlan": string,
3628
+ "riskLevels": string,
3629
+ "qualityReason": string,
3630
+ "triggerInvalidation": string
3631
+ }
3632
+
3633
+ - Do not add any other fields.
3634
+ - All numbers must be finite, with no \`NaN\` or \`Infinity\`.
3635
+ - All text fields must be short strings with no line breaks and no markdown lists.
3636
+ - \`direction\` is not a new trade idea. It is only a compatibility flag for the existing signal: either exactly \`payload.signal.direction\` or \`null\` if the current structure does not confirm that signal. Never propose the opposite direction.
3637
+ - \`quality\` is the structural confirmation level of the current signal right now, including timing and confirmations. It is not a general attractiveness score and not investment advice.
3638
+ - \`needRetest\` indicates whether an additional confirmation level is required before the current signal can be treated as structurally confirmed.
3639
+ - \`retestPrice\` is the key level that would confirm or invalidate the structure, or \`null\` if no extra level is needed or available.
3640
+ - \`takeProfitPrice\` and \`stopLossPrice\` must not be newly invented levels. If the levels already supplied in \`payload.signal.prices\` still look internally coherent relative to the current price and the confirmed signal, you may return them as an audit of existing levels; otherwise return \`null\`.
3641
+ - Use these fields as separate parts of the analysis:
3642
+ - \`setup\`: the current structural setup or trendline state.
3643
+ - \`confirmations\`: 2-4 concrete confirmations or conflicts from the coin indicators.
3644
+ - \`btcContext\`: whether BTC supports the idea, is neutral, or conflicts with it.
3645
+ - \`retestPlan\`: what must happen at the key level to confirm the structure, or why no extra level is needed.
3646
+ - \`riskLevels\`: a short note on whether the existing levels and risk structure are internally coherent, without creating a new trade plan.
3647
+ - \`qualityReason\`: why the quality score is what it is.
3648
+ - \`triggerInvalidation\`: what must happen to confirm the signal or what invalidates the current structural thesis.
3649
+ - \`comment\` is optional. If you include it, do not just duplicate the structured fields.
3650
+
3651
+ If the data is insufficient or the setup is weak, return \`"direction": null\`, \`quality <= 2\`, and explain why.
3652
+
3653
+ Input payload structure:
3654
+ - payload.signal:
3655
+ symbol, signalId, interval, direction, timestamp, strategy, prices
3656
+ - payload.signal.prices:
3657
+ currentPrice, takeProfitPrice, stopLossPrice
3658
+ - payload.figures:
3659
+ strategy-specific figures or geometry when available. Fields vary by strategy.
3660
+ - payload.indicators:
3661
+ historical indicator dictionaries and series for the coin and BTC; all series are already trimmed to the latest 5 values. Treat this block as recent-history transport, not as the primary source of the current shared context.
3662
+ - payload.additionalIndicators:
3663
+ strategy-specific summary/context fields plus the canonical current shared context snapshot.
3664
+ This is not noise; it contains derived fields deliberately passed by the strategy to help the decision.
3665
+ Examples: baseContext, helperFlags, structureContext, volatilitySummary.
3666
+ Always inspect \`payload.additionalIndicators.baseContext\` first for the current shared state:
3667
+ \u2022 \`baseContext.raw\`: current MA, ATR, BB, OBV, price stats, levels, BTC correlation.
3668
+ \u2022 \`baseContext.regime\`: derived trend / volatility / momentum / session regime fields.
3669
+ \u2022 \`baseContext.structure\`: local range position, breakout freshness/quality, level-touch counts, rejection wick context.
3670
+ \u2022 \`baseContext.participation\`: volume/turnover participation, effort-vs-result context, Binance aggTrades, and fingerprinted Hyperliquid whale-flow when available.
3671
+ \u2022 \`baseContext.relative\`: BTC/ETH relative-strength, benchmark MA bias context, Binance alt-basket breadth, and CoinMarketCap historical global/exchange/index context when available.
3672
+ \u2022 \`baseContext.derivatives\`: Coinalyze-aligned derivatives summary when available.
3673
+ \u2022 \`baseContext.mtf\`: compact multi-timeframe summary plus only the latest few candles for each timeframe.
3674
+ \u2022 \`baseContext.gateFeatures\`: compact direction-aware fields derived from baseContext; prefer \`setup\`, \`scores\`, \`conflicts\`, \`risk\`, \`decisionHints\`, \`mtf\`, \`volatility\`, \`participation\`, and \`relative\` for quick gate checks before inspecting raw nested context.
3675
+ Always inspect \`payload.additionalIndicators.marketContext\` when present:
3676
+ \u2022 \`marketContext.execution.binanceCoinbaseSpread\`: AI-friendly BTC spread view projected from \`payload.additionalIndicators.baseContext.relative.execution.venueSpread\`; \`value=(Coinbase-Binance)/Binance\`, \`bps=value*10000\`.
3677
+ \u2022 \`marketContext.participation.trueDelta\`: Binance taker buy/sell volume delta from kline payload when \`source=kline_taker_volume\`; otherwise absent/unavailable.
3678
+ \u2022 \`marketContext.participation.tradeFlow\`: Binance aggTrades buy/sell pressure buckets when available.
3679
+ \u2022 \`marketContext.relative.marketBreadths.top5|top10|top30|top50|top100\`: equal/volume-weighted alt-basket return, advance/decline ratio, and MA breadth for the five versioned Binance breadth universes. \`marketBreadth\` remains the top30 primary view used by existing gates.
3680
+ \u2022 \`marketContext.relative.targetVsBtc\`: target/BTC ratio returns, alpha, beta, and short-window correlation; use it to decide whether the target is leading or lagging BTC in the signal direction.
3681
+ \u2022 \`marketContext.relative.btcAltRegime\`: Binance-derived BTC-vs-alt basket regime, BTC/alt 24h returns, BTC turnover share, and alt dispersion; use it as a broad alt-market risk pocket.
3682
+ \u2022 \`marketContext.relative.cmcGlobal\`: historical CoinMarketCap global market metrics: total/alt market cap, total/alt volume, BTC/ETH dominance and 24h changes, active markets, \`interval\`, and \`altLiquidityRegime\`.
3683
+ \u2022 \`marketContext.relative.cmcReferenceAssets\`: historical CoinMarketCap BTC/ETH market-cap and volume context, ETH/BTC market-cap ratio, ETH-vs-BTC volume ratio, \`interval\`, and \`referenceLiquidityRegime\`.
3684
+ \u2022 \`marketContext.relative.cmcExchangeLiquidity\`: historical CoinMarketCap major-exchange liquidity aggregate: total volume, 24h volume change, Binance share, concentration, and \`liquidityRegime\`.
3685
+ \u2022 \`marketContext.relative.cmcFearGreed\`: historical daily CoinMarketCap Fear & Greed sentiment index: value, classification, 24h/7d value changes, and \`sentimentRegime\`.
3686
+ \u2022 \`marketContext.relative.cmcIndexes\`: historical daily CoinMarketCap CMC100/CMC20 index values, 24h changes, top constituents, CMC20/CMC100 ratio, and \`indexRegime\`.
3687
+ \u2022 \`marketContext.relative.referenceTradeFlow\`: BTC/ETH reference trade-flow summary used for broad market pressure when the target symbol itself is not BTC/ETH.
3688
+ If those fields exist, use them as a more explicit hint instead of trying to re-derive the same idea from raw lines or points.
3689
+ If \`baseContext.derivatives\` exists, its top-level \`summary\` and \`intervals\` are the primary BTCUSDT Coinalyze benchmark context for the time of the signal. \`secondaryReferenceSymbol\` identifies the ETHUSDT secondary benchmark, and \`referenceContexts\` contains BTCUSDT/ETHUSDT plus configured extra reference symbols such as BNBUSDT/SOLUSDT/TRXUSDT/XRPUSDT. If \`targetContext\` or \`targetDerived\` exists, those fields are the Coinalyze context for the actual target coin; use them as target-specific positioning evidence, but do not infer target-coin derivatives when they are absent.
3690
+ Key patterns:
3691
+ \u2022 current shared state: prefer \`payload.additionalIndicators.baseContext\`
3692
+ \u2022 recent historical series: \`payload.indicators\`
3693
+ \u2022 strategy service keys are possible as well, for example \`touches\`, \`distance\`, timing flags, and other setup-specific summaries
3694
+
3695
+ How to analyze, in order:
3696
+ 1. Start with price structure and the setup geometry or context in \`payload.figures\`. This has higher priority than indicators.
3697
+ 2. Then use \`payload.additionalIndicators.baseContext\` and other explicit strategy-specific context fields.
3698
+ 3. Then assess confirmation or conflict from the current shared state and recent coin indicator history.
3699
+ 4. Then evaluate BTC context.
3700
+ 5. Only after that choose \`direction\`, \`quality\`, and whether an extra confirmation level is required.
3701
+ 6. If strong conflicts exist, reduce quality or set direction to \`null\`.
3702
+
3703
+ Explicit conflict rules:
3704
+ - If the figure or price structure is invalid or doubtful, indicators must not rescue the setup.
3705
+ - If strategy-specific helper fields explicitly say the signal is not confirmed yet, lacks margin, or requires waiting, do not overstate quality.
3706
+ - If the structure is acceptable but BTC or key indicators noticeably conflict, quality is usually \`<= 3\`.
3707
+ - If \`baseContext.derivatives.referenceContexts\` exists, check \`primaryReferenceSymbol\` first as the BTC benchmark, then compare \`secondaryReferenceSymbol\`/ETHUSDT and any target-specific \`targetDerived\`. If \`targetDerived\` exists, compare it to the primary reference instead of treating reference pressure as the target coin's own pressure.
3708
+ - If top-level \`baseContext.derivatives.summary.riskFlags\` contains \`crowded_long\` for a LONG or \`crowded_short\` for a SHORT, treat that as broad-market crowded positioning. If \`targetDerived.riskFlags\` contains the same directional crowding, treat that as target-specific crowded positioning.
3709
+ - If top-level \`baseContext.derivatives.summary.directionAligned=false\`, explicitly mention the broad-market derivatives conflict in \`confirmations\` or \`qualityReason\`. If \`targetDerived.directionAligned=false\`, explicitly mention the target-specific derivatives conflict.
3710
+ - If \`baseContext.derivatives\` is absent, stale, or \`missing_derivatives\`, do not infer Coinalyze conclusions and do not penalize the signal just because that data is missing.
3711
+ - Use \`baseContext.regime.session\` directly as the canonical session/liquidity regime: asia is often thinner, europe/us are more active, and overlaps can amplify both momentum and noise. Do not reject a signal solely because of session, but mention clear session support or conflict in \`confirmations\` or \`qualityReason\`.
3712
+ - If \`marketContext.execution.binanceCoinbaseSpread.available=true\` and \`severity=elevated/wide\`, treat it as cross-exchange divergence or BTC liquidity risk. Do not use the spread as a standalone long/short signal, but reduce confidence or require more confirmation when the rest of the structure is weak or BTC context conflicts.
3713
+ - If \`marketContext.execution.binanceCoinbaseSpread\` is missing or \`available=false\`, do not infer anything from Binance/Coinbase spread and do not penalize the signal just because it is absent.
3714
+ - If \`marketContext.participation.trueDelta.available=true\`, use it as better participation evidence than OHLCV-derived proxy delta; still do not let delta override invalid price structure.
3715
+ - If \`marketContext.participation.tradeFlow.available=true\` and \`stale=false\`, use it as direct lower-timeframe participation evidence. Treat stale or missing tradeFlow as absent, not as negative evidence.
3716
+ - If \`marketContext.relative.marketBreadth.available=true\` and \`stale=false\`, use it as broad alt-market support/conflict. Breadth is contextual; do not let it override the target symbol structure.
3717
+ - If \`marketContext.relative.targetVsBtc.available=true\`, treat positive target/BTC ratio trend as support for alt LONGs and negative ratio trend as support for alt SHORTs; ignore it when the target structure is stronger and clearly explains the setup.
3718
+ - If \`marketContext.relative.btcAltRegime.available=true\` and \`stale=false\`, treat \`alt_lead\`/\`risk_on\` as broad support for alt LONGs and \`btc_lead\`/\`risk_off\` as pressure against alt LONGs or support for cautious alt SHORTs. Do not use it as a standalone entry reason.
3719
+ - If \`marketContext.relative.cmcGlobal.available=true\` and \`stale=false\`, use falling alt market cap/volume or rising BTC dominance as broad risk pressure for alt LONGs. Treat missing CMC history as absent context, not a bearish signal.
3720
+ - If \`marketContext.relative.cmcReferenceAssets.available=true\` and \`stale=false\`, use \`eth_led\` as broad support for ETH/high-beta alt strength and \`btc_led\`/\`thin\` as broad caution. Do not describe BTC/ETH reference history as target-symbol flow.
3721
+ - If \`marketContext.relative.cmcExchangeLiquidity.available=true\` and \`stale=false\`, treat \`contracting\`, \`thin\`, or \`concentrated\` as broad liquidity risk; \`expanding\` or \`balanced\` supports cleaner execution context but is not a standalone entry reason.
3722
+ - If \`marketContext.relative.cmcFearGreed.available=true\` and \`stale=false\`, use \`risk_on\` as broad support for LONGs and \`risk_off\`/\`capitulation\` as broad pressure. Treat \`euphoric\` as overheating/chase caution, not as standalone SHORT proof.
3723
+ - If \`marketContext.relative.cmcIndexes.available=true\` and \`stale=false\`, use \`top20_led\` as broad support for mega-cap leadership, \`large_cap_led\` as broader CMC100 participation, and \`risk_off\` as broad pressure. Do not use CMC index history as a standalone entry reason.
3724
+ - If \`marketContext.relative.referenceTradeFlow.available=true\`, treat BTC/ETH trade-flow as broad market context only. For alt symbols, do not describe it as the target coin's own flow.
3725
+ - If the current signal is not confirmed (\`direction=null\`), name the main reason briefly in \`comment\`.
3726
+ If you use the structured fields, include the main reason in \`qualityReason\` or \`triggerInvalidation\`.
3727
+
3728
+ Rules for \`direction\` / TP / SL:
3729
+ - \`direction = LONG\` only if the data confirms the existing LONG signal; \`SHORT\` only if the data confirms the existing SHORT signal; otherwise \`null\`.
3730
+ - For LONG, the expected relation is usually \`stopLossPrice < currentPrice < takeProfitPrice\`.
3731
+ - For SHORT, the expected relation is usually \`takeProfitPrice < currentPrice < stopLossPrice\`.
3732
+ - Do not optimize or recalculate TP/SL for a "better trade"; only assess whether the already supplied levels are coherent.
3733
+ - If \`direction = null\`, then \`takeProfitPrice = null\` and \`stopLossPrice = null\`.
3734
+ - If \`needRetest = false\`, then \`retestPrice = null\`.
3735
+ - If \`needRetest = true\`, \`retestPrice\` must be a finite number tied to a meaningful retest or breakout level.
3736
+ - Before responding, sanity-check the consistency of \`direction\`, TP/SL, and the current price.
3737
+
3738
+ Quality scale:
3739
+ - 1: poor or chaotic setup, strong conflicts, signal not structurally confirmed
3740
+ - 2: weak setup, few confirmations, more of a watch or reject
3741
+ - 3: average setup, some structure exists, but notable conflicts remain
3742
+ - 4: good setup, several confirmations, structure is mostly coherent
3743
+ - 5: very strong setup, clean structure, confirmations, and internally coherent levels
3744
+
3745
+ Requirements for useful structured analysis:
3746
+ - Include 2-4 concrete factors for or against confirmation in \`confirmations\`.
3747
+ - Explicitly mention the role of the key figure or structural state, for example breakout, retest, false break, touch, or lack of confirmation.
3748
+ - Explicitly mention BTC context as supportive, neutral, or conflicting.
3749
+ - Explain why the quality score is what it is.
3750
+ - If the signal is not confirmed (\`direction=null\`), state clearly what must change for confirmation.
3751
+ - In \`retestPlan\`, avoid technical placeholders like \`needRetest=false @ null\`; write a human explanation.
3752
+ - Do not simply restate JSON fields; add interpretation and decision logic.
3753
+
3754
+ Rules for using trimmed series (last 5 values):
3755
+ - Do not make strong long-term conclusions from only 5 points.
3756
+ - Use 4h and 1d series as brief context, not full history.
3757
+ - If the data is too limited for confidence, reduce quality and use cautious wording.
3758
+
3759
+ Short few-shot examples:
3760
+ {"direction":"LONG","quality":4,"needRetest":true,"retestPrice":100.2,"takeProfitPrice":101.5,"stopLossPrice":98.9,"setup":"Likely trendline breakout upward, but the signal still needs a level check for confirmation.","confirmations":"The coin shows momentum support without obvious overheating, but confirmation is not fully clean yet.","btcContext":"BTC is neutral-to-supportive and does not conflict with the current LONG signal.","retestPlan":"The key level is 100.2; holding above it would confirm the signal structure.","riskLevels":"The supplied TP and SL remain on the correct sides of the current price and still look internally coherent.","qualityReason":"Quality=4 because the structure is solid, but an extra level confirmation is still preferable.","triggerInvalidation":"The structure confirms on a hold above the level and weakens on a move back under the line."}
3761
+ {"direction":null,"quality":2,"needRetest":false,"retestPrice":null,"takeProfitPrice":null,"stopLossPrice":null,"setup":"Touch or noise around the trendline without a convincing breakout.","confirmations":"Indicators are mixed and do not provide strong structural support.","btcContext":"BTC is either conflicting or not supportive of the current thesis.","retestPlan":"It is too early to define an extra level because a quality breakout is not present yet.","riskLevels":"The supplied levels should not be treated as confirmed while the structure remains weak.","qualityReason":"Quality=2 because timing is weak and confirmations are limited.","triggerInvalidation":"Wait for a clear breakout and confirmation from both the coin and BTC."}
3762
+
3763
+ Return only the JSON object, with no extra characters.
3764
+ ${signal ? buildAiSystemPromptAddonByStrategy(signal) : ""}
3765
+ `;
3766
+ var buildAiPayload = (signal) => buildAiPayloadByStrategy(signal);
3767
+ var getDeterministicAiGateContext = (payload) => {
3768
+ const additionalIndicators = asRecord(payload.additionalIndicators);
3769
+ const candidates = [
3770
+ additionalIndicators,
3771
+ ...Object.values(additionalIndicators ?? {}).map(asRecord)
3772
+ ].filter((value) => Boolean(value));
3773
+ return candidates.find(
3774
+ (candidate) => Array.isArray(candidate.approvalBlockReasons) || Array.isArray(candidate.riskAnnotations) || Array.isArray(candidate.structuralHardBlockReasons) || typeof candidate.approvalAllowedNow === "boolean"
3775
+ ) ?? null;
3210
3776
  };
3211
- var getActiveRuntimeTrade = async (params) => {
3212
- const { userName, symbol, accountId, deploymentId } = params;
3213
- if (!userName) {
3214
- return null;
3215
- }
3216
- const activeRef = await (0, import_redis2.getData)(
3217
- import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId),
3218
- null
3777
+ var buildAiHumanPrompt = (signal, payload = buildAiPayload(signal)) => `
3778
+ Analyze the already computed internal signal for ${signal.symbol}. The original signal direction is ${signal.direction}.
3779
+ This is a structure-classification and audit task, not execution advice. Determine whether the current structure confirms the existing signal, how structurally coherent it is right now, whether an extra confirmation level is needed, and whether the already supplied levels in \`payload.signal.prices\` still look internally coherent. Do not replace the original thesis with a new one and do not invent new levels; return only the requested JSON.
3780
+
3781
+ Trade payload:
3782
+ ${JSON.stringify(payload)}
3783
+ ${buildAiHumanPromptAddonByStrategy(signal, payload)}
3784
+ `;
3785
+ var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
3786
+ const settings = await getAiUserSettings(options.userName);
3787
+ const responseLanguage = (0, import_aiLanguages2.getAiResponseLanguagePromptName)(
3788
+ settings.AI_RESPONSE_LANGUAGE || import_aiLanguages2.DEFAULT_AI_RESPONSE_LANGUAGE
3219
3789
  );
3220
- const orderId = String(activeRef?.orderId || "").trim();
3221
- if (!orderId) {
3222
- return null;
3790
+ const response = await invokeAiPromptChat({
3791
+ userName: options.userName,
3792
+ model: options.model,
3793
+ messages: [
3794
+ { role: "system", content: systemPrompt },
3795
+ {
3796
+ role: "system",
3797
+ content: `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
3798
+ },
3799
+ { role: "user", content: humanPrompt }
3800
+ ]
3801
+ });
3802
+ const parsed = parseAIResponse(response.content);
3803
+ const normalized = normalizeAnalysis(parsed);
3804
+ if (!options.signal) {
3805
+ return normalized;
3223
3806
  }
3224
- const existing = await (0, import_redis2.getData)(
3225
- import_redis2.redisKeys.runtimeTrade(userName, orderId),
3226
- null
3807
+ return postProcessAiAnalysisByStrategy(
3808
+ options.signal,
3809
+ normalized,
3810
+ options.payload
3227
3811
  );
3228
- if (!existing || existing.status !== "active") {
3229
- await (0, import_redis2.delKey)(
3230
- import_redis2.redisKeys.runtimeActiveTrade(userName, symbol, deploymentId ?? accountId)
3231
- );
3232
- return null;
3233
- }
3234
- return existing;
3235
3812
  };
3236
- var markRuntimeTradeClosed = async (params) => {
3237
- const {
3238
- userName,
3239
- symbol,
3240
- strategy,
3241
- exitPrice,
3242
- exitTimestamp,
3243
- closedPnl,
3244
- exitType,
3245
- accountId,
3246
- deploymentId
3247
- } = params;
3248
- if (!userName) {
3249
- return null;
3250
- }
3251
- const existing = await getActiveRuntimeTrade({
3252
- userName,
3253
- symbol,
3254
- accountId,
3255
- deploymentId
3256
- });
3257
- if (!existing) {
3258
- return null;
3259
- }
3260
- const orderId = existing.orderId;
3261
- if (strategy && existing.strategy !== strategy) {
3262
- return null;
3263
- }
3264
- const resolvedExitPrice = typeof exitPrice === "number" && Number.isFinite(exitPrice) ? exitPrice : existing.currentPrice ?? existing.entryPrice;
3265
- const resolvedClosedPnl = typeof closedPnl === "number" && Number.isFinite(closedPnl) ? closedPnl : typeof resolvedExitPrice === "number" && Number.isFinite(resolvedExitPrice) ? calculateClosedPnl({
3266
- direction: existing.direction,
3267
- entryPrice: existing.entryPrice,
3268
- exitPrice: resolvedExitPrice,
3269
- qty: existing.qty
3270
- }) : existing.closedPnl ?? existing.currentPnl ?? null;
3271
- const next = {
3272
- ...existing,
3273
- status: "closed",
3274
- currentPrice: resolvedExitPrice,
3275
- currentPnl: resolvedClosedPnl,
3276
- closedPnl: resolvedClosedPnl,
3277
- exitPrice: resolvedExitPrice,
3278
- exitTimestamp: typeof exitTimestamp === "number" && Number.isFinite(exitTimestamp) ? exitTimestamp : now(),
3279
- exitType: exitType ?? existing.exitType ?? null,
3280
- lastSyncedAt: now()
3813
+ var runAiPromptLocal = async (signal, options = {}) => {
3814
+ const payload = options.payload ?? buildAiPayload(signal);
3815
+ const gateContext = getDeterministicAiGateContext(payload);
3816
+ const signalDirection = getSignalDirection(signal);
3817
+ const deterministicQuality = getDeterministicQuality(gateContext);
3818
+ const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
3819
+ return postProcessLocalAiAnalysisByStrategy(
3820
+ signal,
3821
+ {
3822
+ direction: approvalAllowedNow ? signalDirection : null,
3823
+ quality: deterministicQuality,
3824
+ needRetest: !approvalAllowedNow,
3825
+ retestPrice: null,
3826
+ takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
3827
+ stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
3828
+ },
3829
+ payload
3830
+ );
3831
+ };
3832
+ var askAI = async (signal, options = {}) => {
3833
+ const { symbol } = signal;
3834
+ const payload = buildAiPayload(signal);
3835
+ const content = await runAiPrompt(
3836
+ {
3837
+ systemPrompt: buildAiSystemPrompt(signal),
3838
+ humanPrompt: buildAiHumanPrompt(signal, payload)
3839
+ },
3840
+ {
3841
+ ...options,
3842
+ signal,
3843
+ payload
3844
+ }
3845
+ );
3846
+ await (0, import_redis3.setData)(import_redis3.redisKeys.analysis(symbol, signal.signalId), content);
3847
+ return content;
3848
+ };
3849
+
3850
+ // src/strategyAdapters/ml.ts
3851
+ var defaultMlAdapter = {
3852
+ normalizeStrategyConfig: (strategyConfig) => strategyConfig
3853
+ };
3854
+ var getStrategyMlAdapter = (strategy, profileId) => {
3855
+ const strategyAdapter = getStrategyProfileMlAdapter(
3856
+ getStrategyManifest(strategy),
3857
+ profileId
3858
+ );
3859
+ if (!strategyAdapter) return defaultMlAdapter;
3860
+ return {
3861
+ ...defaultMlAdapter,
3862
+ ...strategyAdapter
3863
+ };
3864
+ };
3865
+
3866
+ // src/mlPayload.ts
3867
+ var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
3868
+ return getStrategyMlAdapter(
3869
+ strategyName,
3870
+ profileId
3871
+ ).normalizeStrategyConfig?.(strategyConfig);
3872
+ };
3873
+ var buildMlPayload = (payload) => {
3874
+ const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
3875
+ const profileId = payload.signal?.policyProfileId;
3876
+ const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
3877
+ const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
3878
+ const nextSignal = {
3879
+ ...normalizedSignal,
3880
+ indicators: {
3881
+ ...normalizedSignal?.indicators ?? {}
3882
+ }
3883
+ };
3884
+ const nextContext = payload.context ? {
3885
+ ...payload.context,
3886
+ strategyConfig: normalizeStrategyConfig(
3887
+ payload.context.strategyConfig,
3888
+ strategyName,
3889
+ profileId
3890
+ )
3891
+ } : void 0;
3892
+ return {
3893
+ signal: nextSignal,
3894
+ context: nextContext
3281
3895
  };
3282
- const dayKey = (0, import_time.getRuntimeStorageDayKey)(existing.entryTimestamp);
3283
- const closeDayKey = (0, import_time.getRuntimeStorageDayKey)(next.exitTimestamp);
3284
- try {
3285
- await Promise.all([
3286
- (0, import_redis2.setData)(import_redis2.redisKeys.runtimeTrade(userName, orderId), next, {
3287
- expire: import_constants.TTL_1M
3288
- }),
3289
- (0, import_redis2.setHashJsonField)(
3290
- import_redis2.redisKeys.runtimeTradeBucket(userName, dayKey),
3291
- orderId,
3292
- next,
3293
- { expire: import_constants.TTL_1M }
3294
- ),
3295
- (0, import_redis2.setHashJsonField)(
3296
- import_redis2.redisKeys.runtimeClosedTradeBucket(userName, closeDayKey),
3297
- orderId,
3298
- next,
3299
- { expire: import_constants.TTL_1M }
3300
- ),
3301
- (0, import_redis2.delKey)(
3302
- import_redis2.redisKeys.runtimeActiveTrade(
3303
- userName,
3304
- symbol,
3305
- deploymentId ?? accountId
3306
- )
3307
- )
3308
- ]);
3309
- } catch (error) {
3310
- import_logger3.logger.error(
3311
- "runtime trade close journal failed: %s %s",
3312
- symbol,
3313
- error?.message || String(error)
3314
- );
3315
- }
3316
- return next;
3317
3896
  };
3318
3897
 
3319
3898
  // src/strategyHelpers/marketContextStages.ts
@@ -3325,7 +3904,7 @@ var import_logger4 = require("@tradejs/infra/logger");
3325
3904
  var import_strategies = require("@tradejs/core/strategies");
3326
3905
 
3327
3906
  // src/binanceBreadthUniverses.ts
3328
- var import_node_crypto2 = require("crypto");
3907
+ var import_node_crypto3 = require("crypto");
3329
3908
 
3330
3909
  // src/config/binanceBreadthUniverses.json
3331
3910
  var binanceBreadthUniverses_default = {
@@ -3564,7 +4143,7 @@ var BINANCE_BREADTH_UNIVERSE_KEYS = [
3564
4143
  "top50",
3565
4144
  "top100"
3566
4145
  ];
3567
- var fingerprint = (value, length) => (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
4146
+ var fingerprint = (value, length) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, length);
3568
4147
  var normalizeSymbols = (symbols) => symbols.map((symbol) => symbol.trim().toUpperCase()).filter(Boolean);
3569
4148
  var buildBinanceBreadthUniverseSnapshot = ({
3570
4149
  rankedSymbols,
@@ -4588,7 +5167,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
4588
5167
  };
4589
5168
 
4590
5169
  // src/strategyHelpers/derivativesContext.ts
4591
- var import_indicators2 = require("@tradejs/core/indicators");
5170
+ var import_indicators3 = require("@tradejs/core/indicators");
4592
5171
  var import_data2 = require("@tradejs/core/data");
4593
5172
  var import_strategies3 = require("@tradejs/core/strategies");
4594
5173
  var import_constants2 = require("@tradejs/core/constants");
@@ -4622,7 +5201,7 @@ var parseLookbackMs = () => {
4622
5201
  };
4623
5202
  var withHourlyFallbackRows = (rowsByInterval) => ({
4624
5203
  "15m": rowsByInterval["15m"] ?? [],
4625
- "1h": (0, import_indicators2.buildCoinalyzeHourlyRowsWithFallback)({
5204
+ "1h": (0, import_indicators3.buildCoinalyzeHourlyRowsWithFallback)({
4626
5205
  rows15m: rowsByInterval["15m"],
4627
5206
  fallbackRows1h: rowsByInterval["1h"]
4628
5207
  })
@@ -4750,7 +5329,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4750
5329
  const targetSymbol = normalizeSymbol(signal.symbol);
4751
5330
  const lookbackMs = parseLookbackMs();
4752
5331
  const decisionTimeMs = signal.timestamp + (0, import_data2.intervalToMs)(signal.interval);
4753
- const derivativesEndMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
5332
+ const derivativesEndMs = (0, import_indicators3.getLastClosedDerivativesBarStartMs)(
4754
5333
  decisionTimeMs,
4755
5334
  "15m"
4756
5335
  );
@@ -4765,7 +5344,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4765
5344
  });
4766
5345
  return [
4767
5346
  symbol,
4768
- (0, import_indicators2.buildDerivativesContext)({
5347
+ (0, import_indicators3.buildDerivativesContext)({
4769
5348
  symbol,
4770
5349
  direction: signal.direction,
4771
5350
  timestamp: derivativesEndMs,
@@ -4792,7 +5371,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
4792
5371
  lookbackMs,
4793
5372
  ...params.abortSignal ? { signal: params.abortSignal } : {}
4794
5373
  });
4795
- const context = (0, import_indicators2.buildDerivativesContext)({
5374
+ const context = (0, import_indicators3.buildDerivativesContext)({
4796
5375
  symbol: targetSymbol,
4797
5376
  direction: signal.direction,
4798
5377
  timestamp: derivativesEndMs,
@@ -4840,7 +5419,7 @@ var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWha
4840
5419
  var import_logger7 = require("@tradejs/infra/logger");
4841
5420
 
4842
5421
  // src/hyperliquidWhaleUniverse.ts
4843
- var import_node_crypto3 = require("crypto");
5422
+ var import_node_crypto4 = require("crypto");
4844
5423
 
4845
5424
  // src/config/hyperliquidPerpUniverse.json
4846
5425
  var hyperliquidPerpUniverse_default = {
@@ -5012,7 +5591,7 @@ var hyperliquidWhales_default = {
5012
5591
  };
5013
5592
 
5014
5593
  // src/hyperliquidWhaleUniverse.ts
5015
- var fingerprint2 = (value) => (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
5594
+ var fingerprint2 = (value) => (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
5016
5595
  var validateUnique = (values, label) => {
5017
5596
  if (new Set(values).size !== values.length) {
5018
5597
  throw new Error(`Hyperliquid ${label} snapshot contains duplicates`);
@@ -5499,6 +6078,7 @@ var enrichSignalWithMarketContextStages = async ({
5499
6078
  })
5500
6079
  });
5501
6080
  }
6081
+ await enrichSignalWithTickerProfile(signal);
5502
6082
  const results = [];
5503
6083
  for (const stage of stages) {
5504
6084
  if (abortSignal?.aborted) break;
@@ -5546,12 +6126,12 @@ var findReplayAiAnalysis = ({
5546
6126
  direction,
5547
6127
  ai
5548
6128
  }) => {
5549
- const snapshots = ai?.replayAnalyses;
5550
- if (!Array.isArray(snapshots) || !snapshots.length) {
6129
+ const snapshots2 = ai?.replayAnalyses;
6130
+ if (!Array.isArray(snapshots2) || !snapshots2.length) {
5551
6131
  return void 0;
5552
6132
  }
5553
6133
  let best = null;
5554
- for (const snapshot2 of snapshots) {
6134
+ for (const snapshot2 of snapshots2) {
5555
6135
  if (snapshot2.symbol !== signal.symbol || snapshot2.direction !== direction || snapshot2.strategy && snapshot2.strategy !== signal.strategy) {
5556
6136
  continue;
5557
6137
  }
@@ -5711,11 +6291,13 @@ var executeEntryOrder = async ({
5711
6291
  beforePlaceOrder,
5712
6292
  recordRuntimeTrade = true,
5713
6293
  leverage,
5714
- executionLimits
6294
+ executionLimits,
6295
+ markPortfolioSubmitted,
6296
+ rejectPortfolioSubmission
5715
6297
  }) => {
5716
6298
  await beforePlaceOrder?.();
5717
6299
  const previousPosition = positionIntent === "increase" ? await connector.getPosition(symbol) : null;
5718
- const orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
6300
+ let orderId = signal.orderId || createRuntimeOrderId(signal.strategy);
5719
6301
  const signalTimestamp = signal.timestamp;
5720
6302
  const signalClosePrice = currentPrice;
5721
6303
  signal.orderId = orderId;
@@ -5784,9 +6366,16 @@ var executeEntryOrder = async ({
5784
6366
  ...typeof leverage === "number" && Number.isFinite(leverage) ? { leverage } : {},
5785
6367
  orderId,
5786
6368
  signal,
5787
- ...beforeSubmit ? { beforeSubmit } : {}
6369
+ ...rejectPortfolioSubmission ? { onRejected: rejectPortfolioSubmission } : {},
6370
+ ...beforeSubmit || markPortfolioSubmitted ? {
6371
+ beforeSubmit: async (context) => {
6372
+ await beforeSubmit?.(context);
6373
+ return markPortfolioSubmitted?.(context.qty);
6374
+ }
6375
+ } : {}
5788
6376
  });
5789
6377
  const orderAckTime = Date.now();
6378
+ if (markPortfolioSubmitted && signal.orderId) orderId = signal.orderId;
5790
6379
  const placedQty = typeof signal.orderQty === "number" && Number.isFinite(signal.orderQty) && signal.orderQty > 0 ? signal.orderQty : qty;
5791
6380
  const currentPosition = await connector.getPosition(symbol);
5792
6381
  const fillTime = Date.now();
@@ -6772,7 +7361,7 @@ var handleProtectDecision = async ({
6772
7361
  }
6773
7362
  return decision.code;
6774
7363
  };
6775
- var executeEntryDecision = async ({
7364
+ var executeAdmittedEntryDecision = async ({
6776
7365
  connector,
6777
7366
  symbol,
6778
7367
  decision,
@@ -6786,7 +7375,9 @@ var executeEntryDecision = async ({
6786
7375
  ai,
6787
7376
  recordRuntimeJournal,
6788
7377
  invokeStageHooks,
6789
- notifyRuntimeError
7378
+ notifyRuntimeError,
7379
+ markPortfolioSubmitted,
7380
+ rejectPortfolioSubmission
6790
7381
  }) => {
6791
7382
  const signal = decision.signal;
6792
7383
  const beforePlaceOrder = async () => {
@@ -6833,6 +7424,8 @@ var executeEntryDecision = async ({
6833
7424
  ...Number.isFinite(Number(hookCtx.strategyConfig.LEVERAGE)) ? { leverage: Number(hookCtx.strategyConfig.LEVERAGE) } : {},
6834
7425
  signal,
6835
7426
  beforePlaceOrder,
7427
+ markPortfolioSubmitted,
7428
+ rejectPortfolioSubmission,
6836
7429
  recordRuntimeTrade: recordRuntimeJournal,
6837
7430
  executionLimits: resolveEntryExecutionLimits(
6838
7431
  hookCtx.strategyConfig,
@@ -6973,6 +7566,37 @@ var executeEntryDecision = async ({
6973
7566
  }
6974
7567
  return signal ?? decision.code;
6975
7568
  };
7569
+ async function executeEntryDecision(params) {
7570
+ if (!params.portfolio) return executeAdmittedEntryDecision(params);
7571
+ const { decision, portfolio } = params;
7572
+ const signal = decision.signal;
7573
+ if (!signal || decision.orderPlan.positionIntent === "increase")
7574
+ return "PORTFOLIO_REQUIRES_NEW_SIGNAL_POSITION";
7575
+ signal.orderId ??= createRuntimeOrderId(signal.strategy);
7576
+ const result = await portfolio.submit(
7577
+ params.portfolioTime ?? decision.entryContext.timestamp,
7578
+ {
7579
+ id: signal.orderId,
7580
+ signalId: signal.signalId,
7581
+ strategy: signal.strategy,
7582
+ symbol: params.symbol,
7583
+ direction: decision.entryContext.direction,
7584
+ qty: decision.orderPlan.qty,
7585
+ timestamp: decision.entryContext.timestamp
7586
+ },
7587
+ (markPortfolioSubmitted, rejectPortfolioSubmission) => executeAdmittedEntryDecision({
7588
+ ...params,
7589
+ markPortfolioSubmitted,
7590
+ rejectPortfolioSubmission
7591
+ })
7592
+ );
7593
+ if (result.reason) {
7594
+ signal.orderStatus = "failed";
7595
+ signal.orderFailureReason = result.reason;
7596
+ return signal;
7597
+ }
7598
+ return result.value;
7599
+ }
6976
7600
 
6977
7601
  // src/strategyRuntime.ts
6978
7602
  var cloneWithPropertyDescriptors = (value) => Object.create(
@@ -7000,6 +7624,7 @@ var createStrategyRuntime = ({
7000
7624
  };
7001
7625
  const creator = async ({
7002
7626
  userName,
7627
+ connectorName,
7003
7628
  config: baseConfig,
7004
7629
  symbol,
7005
7630
  universe: requestedUniverse,
@@ -7038,6 +7663,37 @@ var createStrategyRuntime = ({
7038
7663
  const projectConfig = await loadTradejsConfig(projectRoot);
7039
7664
  const projectHooks = projectConfig.hooks;
7040
7665
  const env = String(config.ENV ?? "BACKTEST");
7666
+ let portfolioMarket;
7667
+ const portfolio = getPortfolioRuntime({
7668
+ symbol,
7669
+ canClose: async (position, intent, asOf) => {
7670
+ if (!portfolioMarket || portfolioMarket.candle.timestamp > asOf || !isRuntimeOrderExecutionEnabled({ config, env, connector }))
7671
+ return false;
7672
+ const gate = await authorizeClose(
7673
+ {
7674
+ kind: "exit",
7675
+ code: intent.reason,
7676
+ closePlan: {
7677
+ direction: position.direction,
7678
+ price: position.markPrice,
7679
+ timestamp: asOf
7680
+ }
7681
+ },
7682
+ portfolioMarket,
7683
+ position.strategy
7684
+ );
7685
+ return gate?.allow !== false;
7686
+ },
7687
+ connectorName,
7688
+ value: baseConfig?.PORTFOLIO_POLICY ?? config.PORTFOLIO_POLICY,
7689
+ strategy: strategyName,
7690
+ connector,
7691
+ env,
7692
+ userName,
7693
+ accountId,
7694
+ deploymentId
7695
+ });
7696
+ const portfolioContext = portfolio ? (now2) => isRuntimeOrderExecutionEnabled({ config, env, connector }) ? portfolio.tick(now2) : portfolio.context(now2) : void 0;
7041
7697
  const researchPolicyValue = baseConfig?.RESEARCH_ENTRY_POLICY ?? config.RESEARCH_ENTRY_POLICY;
7042
7698
  const researchEntryPolicy = researchPolicyValue == null ? void 0 : parseResearchEntryPolicy(researchPolicyValue);
7043
7699
  if (researchEntryPolicy && (env !== "BACKTEST" || Reflect.get(connector, "__tradejsTestConnector") !== true))
@@ -7202,7 +7858,10 @@ var createStrategyRuntime = ({
7202
7858
  return invokeHook(stage, hook, params, errorContext);
7203
7859
  };
7204
7860
  const invokeGateHooks = async (stage, hook, params, errorContext = {}) => {
7205
- const failureResult = stage === "beforeEntryGate" ? { allow: false, reason: "ERROR" } : void 0;
7861
+ const failureResult = {
7862
+ allow: false,
7863
+ reason: "ERROR"
7864
+ };
7206
7865
  const projectResults = await invokeProjectHooks(
7207
7866
  stage,
7208
7867
  params,
@@ -7223,6 +7882,12 @@ var createStrategyRuntime = ({
7223
7882
  failureResult
7224
7883
  );
7225
7884
  };
7885
+ const authorizeClose = (decision, market, owner = strategyName) => invokeGateHooks(
7886
+ "beforeClosePosition",
7887
+ resolveManifest(owner)?.hooks?.beforeClosePosition,
7888
+ { ctx: getHookCtx(owner), market, decision },
7889
+ { decision, market }
7890
+ );
7226
7891
  const applyProjectAfterCoreDecisionHooks = async ({
7227
7892
  hookCtx,
7228
7893
  market,
@@ -7336,6 +8001,20 @@ var createStrategyRuntime = ({
7336
8001
  interval: decisionInterval
7337
8002
  }) => {
7338
8003
  if (!baseContext) return void 0;
8004
+ const profileSource = strategyManifest?.contextRequirements?.tickerProfiles;
8005
+ if (profileSource) {
8006
+ const profile = await loadTickerProfileContext({
8007
+ ...profileSource,
8008
+ projectRoot,
8009
+ symbol: decisionSymbol,
8010
+ timestamp: candle.timestamp
8011
+ });
8012
+ if (!profile)
8013
+ throw new Error(
8014
+ "Required ticker profile is unavailable at decision time"
8015
+ );
8016
+ baseContext = withTickerProfile(baseContext, profile);
8017
+ }
7339
8018
  if (!coreContextRequirements.has("hyperliquidWhales")) {
7340
8019
  return baseContext;
7341
8020
  }
@@ -7503,6 +8182,8 @@ var createStrategyRuntime = ({
7503
8182
  runtime: pending2.runtime
7504
8183
  });
7505
8184
  return executeEntryDecision({
8185
+ portfolio,
8186
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
7506
8187
  connector,
7507
8188
  symbol,
7508
8189
  decision: pending2.decision,
@@ -7520,6 +8201,11 @@ var createStrategyRuntime = ({
7520
8201
  });
7521
8202
  };
7522
8203
  const runWithDecisionOverride = async (candle, btcCandle, options = {}) => {
8204
+ portfolioMarket = { candle, btcCandle };
8205
+ if (portfolioContext)
8206
+ await portfolioContext(
8207
+ env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now()
8208
+ );
7523
8209
  appendCurrentMarketData(candle, btcCandle, options.ethCandle);
7524
8210
  const ethCandle = resolveEthCandle(candle, options.ethCandle);
7525
8211
  indicatorsState.setCurrentBar(candle, btcCandle, ethCandle);
@@ -7622,15 +8308,10 @@ var createStrategyRuntime = ({
7622
8308
  if (!makeOrdersEnabled) {
7623
8309
  return decision.code;
7624
8310
  }
7625
- const closeGate = await invokeGateHooks(
7626
- "beforeClosePosition",
7627
- decisionManifest?.hooks?.beforeClosePosition,
7628
- {
7629
- ctx: decisionHookCtx,
7630
- market,
7631
- decision
7632
- },
7633
- { decision, market }
8311
+ const closeGate = await authorizeClose(
8312
+ decision,
8313
+ market,
8314
+ decisionHookCtx.strategyName
7634
8315
  );
7635
8316
  if (closeGate?.allow === false) {
7636
8317
  return closeGate.reason ? `CLOSE_BLOCKED_BY_HOOK:${closeGate.reason}` : "CLOSE_BLOCKED_BY_HOOK";
@@ -7690,6 +8371,13 @@ var createStrategyRuntime = ({
7690
8371
  });
7691
8372
  const signal = decision.signal;
7692
8373
  if (signal) {
8374
+ if (portfolio)
8375
+ signal.additionalIndicators = {
8376
+ ...signal.additionalIndicators,
8377
+ portfolioContext: await portfolioContext(
8378
+ env === "BACKTEST" || env === "PARITY" ? decision.entryContext.timestamp : Date.now()
8379
+ )
8380
+ };
7693
8381
  if (runtimeLineage) signal.runtimeLineage = runtimeLineage;
7694
8382
  if (universe) signal.universe = universe;
7695
8383
  if (assetClass) signal.assetClass = assetClass;
@@ -7929,6 +8617,8 @@ var createStrategyRuntime = ({
7929
8617
  });
7930
8618
  }
7931
8619
  return executeEntryDecision({
8620
+ portfolio,
8621
+ portfolioTime: env === "BACKTEST" || env === "PARITY" ? candle.timestamp : Date.now(),
7932
8622
  connector,
7933
8623
  symbol,
7934
8624
  decision,
@@ -8040,7 +8730,7 @@ var normalizeForCanonicalJson = (value) => {
8040
8730
  }
8041
8731
  return value;
8042
8732
  };
8043
- var revision = (prefix, value) => `${prefix}:${(0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(normalizeForCanonicalJson(value))).digest("hex").slice(0, 16)}`;
8733
+ var revision = (prefix, value) => `${prefix}:${(0, import_node_crypto5.createHash)("sha256").update(JSON.stringify(normalizeForCanonicalJson(value))).digest("hex").slice(0, 16)}`;
8044
8734
  var computeStrategyRevision = ({
8045
8735
  strategyName,
8046
8736
  strategyPackage,