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

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