@tradejs/node 2.0.21 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/backtest.js CHANGED
@@ -49,22 +49,20 @@ var import_strategies7 = require("@tradejs/core/strategies");
49
49
  var import_time2 = require("@tradejs/core/time");
50
50
  var import_logger13 = require("@tradejs/infra/logger");
51
51
 
52
- // src/strategy/manifests.ts
53
- var import_indicators2 = require("@tradejs/core/indicators");
54
- var import_logger11 = require("@tradejs/infra/logger");
55
-
56
52
  // src/strategyRuntime.ts
57
53
  var import_constants5 = require("@tradejs/core/constants");
58
54
  var import_strategies6 = require("@tradejs/core/strategies");
59
- var import_logger10 = require("@tradejs/infra/logger");
55
+ var import_logger11 = require("@tradejs/infra/logger");
60
56
 
61
57
  // src/strategyHelpers/runtime.ts
62
- var import_logger8 = require("@tradejs/infra/logger");
58
+ var import_logger9 = require("@tradejs/infra/logger");
63
59
  var import_constants3 = require("@tradejs/core/constants");
64
60
  var import_ml2 = require("@tradejs/infra/ml");
65
61
 
66
62
  // src/ai.ts
67
- var import_aiLanguages = require("@tradejs/infra/aiLanguages");
63
+ var import_aiLanguages = require("@tradejs/core/aiLanguages");
64
+ var import_aiEndpoints = require("@tradejs/core/aiEndpoints");
65
+ var import_aiModels = require("@tradejs/core/aiModels");
68
66
  var import_redis = require("@tradejs/infra/redis");
69
67
  var import_userSettings = require("@tradejs/infra/userSettings");
70
68
 
@@ -640,911 +638,1139 @@ var buildAiMarketContext = (signal) => ({
640
638
  }
641
639
  });
642
640
 
643
- // src/strategy/policyProfiles.ts
644
- var profileMatches = (profile, universe, assetClass) => {
645
- const { appliesTo } = profile;
646
- if (!appliesTo) return true;
647
- if (appliesTo.universes?.length && (!universe || !appliesTo.universes.includes(universe))) {
648
- return false;
649
- }
650
- if (appliesTo.assetClasses?.length && (!assetClass || !appliesTo.assetClasses.includes(assetClass))) {
651
- return false;
652
- }
653
- return true;
654
- };
655
- var resolveStrategyPolicyProfile = (manifest, params) => {
656
- const profiles = manifest?.policyProfiles ?? [];
657
- if (!profiles.length) {
658
- const inferredId = params.profileId ?? (params.universe === "tradfi" ? "tradfi" : void 0);
659
- if (!inferredId) return void 0;
660
- if (inferredId !== "crypto" && inferredId !== "tradfi") {
661
- throw new Error(
662
- `Unknown policy profile "${inferredId}" for strategy "${manifest?.name}"`
663
- );
664
- }
665
- if (params.universe && inferredId !== params.universe) {
666
- throw new Error(
667
- `Policy profile "${inferredId}" is not compatible with ${params.universe}`
668
- );
669
- }
670
- return {
671
- id: inferredId,
672
- appliesTo: { universes: [inferredId] },
673
- marketDataRequirements: inferredId === "crypto" ? ["crypto.btcReference"] : [],
674
- ...manifest?.mlAdapter ? {
675
- entryRuntimeDefaults: {
676
- ml: {
677
- modelKey: inferredId === "crypto" ? manifest.name : `${manifest.name}:tradfi`
678
- }
679
- }
680
- } : {}
681
- };
641
+ // src/strategy/manifests.ts
642
+ var import_indicators = require("@tradejs/core/indicators");
643
+ var import_logger2 = require("@tradejs/infra/logger");
644
+
645
+ // src/tradejsConfig.ts
646
+ var import_fs = __toESM(require("fs"));
647
+ var import_path = __toESM(require("path"));
648
+ var import_url = require("url");
649
+ var import_config = require("@tradejs/core/config");
650
+ var import_logger = require("@tradejs/infra/logger");
651
+ var CONFIG_FILE_NAMES = [
652
+ "tradejs.config.ts",
653
+ "tradejs.config.mts",
654
+ "tradejs.config.js",
655
+ "tradejs.config.mjs",
656
+ "tradejs.config.cjs"
657
+ ];
658
+ var TS_MODULE_RE = /\.(cts|mts|ts)$/i;
659
+ var cachedByCwd = /* @__PURE__ */ new Map();
660
+ var announcedConfigFile = /* @__PURE__ */ new Set();
661
+ var tsNodeRegistered = false;
662
+ var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
663
+ var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
664
+ var getTradejsProjectCwd = (cwd) => {
665
+ const explicit = String(cwd ?? "").trim();
666
+ if (explicit) {
667
+ return import_path.default.resolve(explicit);
682
668
  }
683
- if (params.profileId) {
684
- const profile = profiles.find(({ id }) => id === params.profileId);
685
- if (!profile) {
686
- throw new Error(
687
- `Unknown policy profile "${params.profileId}" for strategy "${manifest?.name}"`
688
- );
689
- }
690
- if (!profileMatches(profile, params.universe, params.assetClass)) {
691
- throw new Error(
692
- `Policy profile "${params.profileId}" is not compatible with ${params.universe ?? "unknown"}:${params.assetClass ?? "unknown"}`
693
- );
694
- }
695
- return profile;
669
+ const fromEnv = String(process.env.PROJECT_CWD || "").trim();
670
+ if (fromEnv) {
671
+ return import_path.default.resolve(fromEnv);
696
672
  }
697
- const matching = profiles.filter(
698
- (profile) => profileMatches(profile, params.universe, params.assetClass)
699
- );
700
- const defaultProfile = matching.find(
701
- ({ id }) => id === manifest?.defaultPolicyProfileId
702
- );
703
- return defaultProfile ?? matching[0];
673
+ return process.cwd();
704
674
  };
705
- var getStrategyProfileAiAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.aiAdapter ?? manifest?.aiAdapter;
706
- var getStrategyProfileMlAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.mlAdapter ?? manifest?.mlAdapter;
707
-
708
- // src/strategyAdapters/ai.ts
709
- var toRecord2 = (value) => {
710
- if (!value || typeof value !== "object" || Array.isArray(value)) {
675
+ var normalizeConfig = (rawConfig) => {
676
+ if (!rawConfig || typeof rawConfig !== "object") {
711
677
  return {};
712
678
  }
713
- return value;
714
- };
715
- var buildBaseAiPayload = (signal) => {
716
- const additionalIndicators = {
717
- ...toRecord2(signal.additionalIndicators),
718
- marketContext: buildAiMarketContext(signal)
719
- };
679
+ const config = rawConfig;
680
+ const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
681
+ const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
682
+ const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
683
+ const hooks = (0, import_config.normalizeTradejsConfigHooks)(
684
+ config.hooks
685
+ );
720
686
  return {
721
- signal: {
722
- symbol: signal.symbol,
723
- signalId: signal.signalId,
724
- interval: signal.interval,
725
- direction: signal.direction,
726
- timestamp: signal.timestamp,
727
- strategy: signal.strategy,
728
- prices: {
729
- currentPrice: signal.prices.currentPrice,
730
- takeProfitPrice: signal.prices.takeProfitPrice,
731
- stopLossPrice: signal.prices.stopLossPrice
732
- }
733
- },
734
- figures: trimSeriesDeep(signal.figures ?? {}),
735
- indicators: buildCompactAiIndicatorsSnapshot(signal.indicators),
736
- additionalIndicators: trimSeriesDeep(additionalIndicators)
687
+ strategies: strategies2,
688
+ indicators,
689
+ connectors,
690
+ ...hooks ? { hooks } : {}
737
691
  };
738
692
  };
739
- var defaultAiAdapter = {};
740
- var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
741
- var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
742
- var buildAiPayloadByStrategy = (signal) => {
743
- const basePayload = buildBaseAiPayload(signal);
744
- const adapter = getSignalAiAdapter(signal);
745
- return adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
746
- };
747
- var buildAiSystemPromptAddonByStrategy = (signal) => getSignalAiAdapter(signal).buildSystemPromptAddon?.({ signal }) ?? "";
748
- var buildAiHumanPromptAddonByStrategy = (signal, payload) => getSignalAiAdapter(signal).buildHumanPromptAddon?.({
749
- signal,
750
- payload
751
- }) ?? "";
752
- var postProcessAiAnalysisByStrategy = (signal, analysis, payload = buildAiPayloadByStrategy(signal)) => getSignalAiAdapter(signal).postProcessAnalysis?.({
753
- signal,
754
- payload,
755
- analysis
756
- }) ?? analysis;
757
- var postProcessLocalAiAnalysisByStrategy = (signal, analysis, payload = buildAiPayloadByStrategy(signal)) => {
758
- const adapter = getSignalAiAdapter(signal);
759
- const strategyAnalysis = adapter.postProcessAnalysis?.({ signal, payload, analysis }) ?? analysis;
760
- return adapter.postProcessLocalAnalysis?.({
761
- signal,
762
- payload,
763
- analysis: strategyAnalysis
764
- }) ?? strategyAnalysis;
765
- };
766
-
767
- // src/ai.ts
768
- var parseAIResponse = (input) => {
769
- try {
770
- if (typeof input === "object" && input !== null) return input;
771
- const match = input.match(/\{[\s\S]*\}/);
772
- if (!match) throw new Error("JSON block not found");
773
- return JSON.parse(match[0]);
774
- } catch (err) {
775
- console.error("Failed to parse AI response:", err);
776
- console.log("Raw AI response:", input);
777
- return {};
693
+ var getNodeCreateRequire = () => {
694
+ const builtinModule = process.getBuiltinModule?.("module");
695
+ if (typeof builtinModule?.createRequire === "function") {
696
+ return builtinModule.createRequire;
778
697
  }
698
+ throw new TypeError("module.createRequire is not available");
779
699
  };
780
- var normalizeResponseContent = (content) => {
781
- if (typeof content === "string" || content && typeof content === "object") {
782
- if (typeof content !== "object" || !Array.isArray(content)) {
783
- return content;
784
- }
785
- }
786
- if (Array.isArray(content)) {
787
- const text = content.map((part) => typeof part?.text === "string" ? part.text : "").join("\n").trim();
788
- return text;
700
+ var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
701
+ var ensureTsNodeRegistered = async () => {
702
+ if (tsNodeRegistered) {
703
+ return;
789
704
  }
790
- return String(content ?? "");
791
- };
792
- var normalizeAnalysis = (raw) => {
793
- const direction = raw?.direction === "LONG" || raw?.direction === "SHORT" ? raw.direction : null;
794
- const qualityNum = typeof raw?.quality === "number" ? Math.max(1, Math.min(5, Math.round(raw.quality))) : void 0;
795
- const toNumberOrNull = (value) => {
796
- if (typeof value === "number" && Number.isFinite(value)) return value;
797
- if (typeof value === "string" && value.trim()) {
798
- const parsed = Number(value);
799
- if (Number.isFinite(parsed)) return parsed;
705
+ const tsNodeModule = await import("ts-node");
706
+ const tsNode = tsNodeModule.default ?? tsNodeModule;
707
+ tsNode.register?.({
708
+ transpileOnly: true,
709
+ compilerOptions: {
710
+ module: "Node16",
711
+ moduleResolution: "node16"
800
712
  }
801
- return null;
802
- };
803
- const toText = (value) => typeof value === "string" ? value.slice(0, 400) : void 0;
804
- return {
805
- direction,
806
- quality: qualityNum,
807
- needRetest: Boolean(raw?.needRetest),
808
- retestPrice: toNumberOrNull(raw?.retestPrice),
809
- takeProfitPrice: toNumberOrNull(raw?.takeProfitPrice),
810
- stopLossPrice: toNumberOrNull(raw?.stopLossPrice),
811
- setup: toText(raw?.setup),
812
- confirmations: toText(raw?.confirmations),
813
- btcContext: toText(raw?.btcContext),
814
- retestPlan: toText(raw?.retestPlan),
815
- riskLevels: toText(raw?.riskLevels),
816
- qualityReason: toText(raw?.qualityReason),
817
- triggerInvalidation: toText(raw?.triggerInvalidation),
818
- comment: typeof raw?.comment === "string" ? raw.comment.slice(0, 1024) : ""
819
- };
713
+ });
714
+ tsNodeRegistered = true;
820
715
  };
821
- var asRecord = (value) => {
822
- if (!value || typeof value !== "object" || Array.isArray(value)) {
823
- return null;
716
+ var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
717
+ const projectRoot = getTradejsProjectCwd(cwd);
718
+ if (tsconfigPathsRegisteredByCwd.has(projectRoot)) {
719
+ return;
824
720
  }
825
- return value;
826
- };
827
- var getSignalDirection = (signal) => signal.direction === "LONG" || signal.direction === "SHORT" ? signal.direction : null;
828
- var getDeterministicQuality = (gateContext) => {
829
- const deterministicQuality = Number(gateContext?.deterministicQuality);
830
- if (Number.isFinite(deterministicQuality)) {
831
- return Math.max(1, Math.min(5, Math.round(deterministicQuality)));
721
+ const tsconfigPathsModule = await import("tsconfig-paths");
722
+ const loadConfig = tsconfigPathsModule.loadConfig;
723
+ const register = tsconfigPathsModule.register;
724
+ if (typeof loadConfig !== "function" || typeof register !== "function") {
725
+ return;
832
726
  }
833
- const maxAllowedQuality = Number(gateContext?.maxAllowedQuality);
834
- if (Number.isFinite(maxAllowedQuality)) {
835
- return Math.max(1, Math.min(5, Math.round(maxAllowedQuality)));
727
+ const loadedConfig = loadConfig(projectRoot);
728
+ if (loadedConfig.resultType !== "success") {
729
+ return;
836
730
  }
837
- return Array.isArray(gateContext?.approvalBlockReasons) && gateContext.approvalBlockReasons.length > 0 || Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length > 0 ? 2 : 3;
731
+ register({
732
+ baseUrl: loadedConfig.absoluteBaseUrl,
733
+ paths: loadedConfig.paths,
734
+ addMatchAll: false
735
+ });
736
+ tsconfigPathsRegisteredByCwd.add(projectRoot);
838
737
  };
839
- var buildAiSystemPrompt = (signal) => `
840
- You are an internal market-structure classifier for an already computed system signal.
841
- Analyze the provided JSON containing the trade, candles, indicators (for the coin and BTC across multiple timeframes), and strategy figures/context.
842
- Series data is already trimmed to the latest 5 values.
843
-
844
- Important:
845
- - Do not invent missing data.
846
- - This is an internal audit/classification task, not user-facing trading advice.
847
- - Do not generate execution instructions, do not replace the original thesis with a new one, and do not provide personalized investment advice.
848
- - Use the original signal direction and levels as the anchor, but you may state that the current structure does not support them.
849
- - Respect the source strategy specified in \`signal.strategy\`.
850
- - Your goal is to explain how well the observed structure matches the existing signal and how structurally confirmed it is right now.
851
- - Do not write vague statements like "there is momentum/slope" without tying them to the decision.
852
- - Write all user-visible text fields in the requested response language. If no explicit language instruction is provided later, default to English.
853
- - If confidence is incomplete, prefer cautious wording such as "likely", "not confirmed yet", or "probably" instead of categorical claims.
854
-
855
- Return exactly one JSON object and nothing else:
856
-
857
- {
858
- "direction": payload.signal.direction | null,
859
- "quality": 1 | 2 | 3 | 4 | 5,
860
- "needRetest": boolean,
861
- "retestPrice": number | null,
862
- "takeProfitPrice": number | null,
863
- "stopLossPrice": number | null,
864
- "setup": string,
865
- "confirmations": string,
866
- "btcContext": string,
867
- "retestPlan": string,
868
- "riskLevels": string,
869
- "qualityReason": string,
870
- "triggerInvalidation": string
871
- }
872
-
873
- - Do not add any other fields.
874
- - All numbers must be finite, with no \`NaN\` or \`Infinity\`.
875
- - All text fields must be short strings with no line breaks and no markdown lists.
876
- - \`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.
877
- - \`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.
878
- - \`needRetest\` indicates whether an additional confirmation level is required before the current signal can be treated as structurally confirmed.
879
- - \`retestPrice\` is the key level that would confirm or invalidate the structure, or \`null\` if no extra level is needed or available.
880
- - \`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\`.
881
- - Use these fields as separate parts of the analysis:
882
- - \`setup\`: the current structural setup or trendline state.
883
- - \`confirmations\`: 2-4 concrete confirmations or conflicts from the coin indicators.
884
- - \`btcContext\`: whether BTC supports the idea, is neutral, or conflicts with it.
885
- - \`retestPlan\`: what must happen at the key level to confirm the structure, or why no extra level is needed.
886
- - \`riskLevels\`: a short note on whether the existing levels and risk structure are internally coherent, without creating a new trade plan.
887
- - \`qualityReason\`: why the quality score is what it is.
888
- - \`triggerInvalidation\`: what must happen to confirm the signal or what invalidates the current structural thesis.
889
- - \`comment\` is optional. If you include it, do not just duplicate the structured fields.
890
-
891
- If the data is insufficient or the setup is weak, return \`"direction": null\`, \`quality <= 2\`, and explain why.
892
-
893
- Input payload structure:
894
- - payload.signal:
895
- symbol, signalId, interval, direction, timestamp, strategy, prices
896
- - payload.signal.prices:
897
- currentPrice, takeProfitPrice, stopLossPrice
898
- - payload.figures:
899
- strategy-specific figures or geometry when available. Fields vary by strategy.
900
- - payload.indicators:
901
- 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.
902
- - payload.additionalIndicators:
903
- strategy-specific summary/context fields plus the canonical current shared context snapshot.
904
- This is not noise; it contains derived fields deliberately passed by the strategy to help the decision.
905
- Examples: baseContext, helperFlags, structureContext, volatilitySummary.
906
- Always inspect \`payload.additionalIndicators.baseContext\` first for the current shared state:
907
- \u2022 \`baseContext.raw\`: current MA, ATR, BB, OBV, price stats, levels, BTC correlation.
908
- \u2022 \`baseContext.regime\`: derived trend / volatility / momentum / session regime fields.
909
- \u2022 \`baseContext.structure\`: local range position, breakout freshness/quality, level-touch counts, rejection wick context.
910
- \u2022 \`baseContext.participation\`: volume/turnover participation, effort-vs-result context, Binance aggTrades, and fingerprinted Hyperliquid whale-flow when available.
911
- \u2022 \`baseContext.relative\`: BTC/ETH relative-strength, benchmark MA bias context, Binance alt-basket breadth, and CoinMarketCap historical global/exchange/index context when available.
912
- \u2022 \`baseContext.derivatives\`: Coinalyze-aligned derivatives summary when available.
913
- \u2022 \`baseContext.mtf\`: compact multi-timeframe summary plus only the latest few candles for each timeframe.
914
- \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.
915
- Always inspect \`payload.additionalIndicators.marketContext\` when present:
916
- \u2022 \`marketContext.execution.binanceCoinbaseSpread\`: AI-friendly BTC spread view projected from \`payload.additionalIndicators.baseContext.relative.execution.venueSpread\`; \`value=(Coinbase-Binance)/Binance\`, \`bps=value*10000\`.
917
- \u2022 \`marketContext.participation.trueDelta\`: Binance taker buy/sell volume delta from kline payload when \`source=kline_taker_volume\`; otherwise absent/unavailable.
918
- \u2022 \`marketContext.participation.tradeFlow\`: Binance aggTrades buy/sell pressure buckets when available.
919
- \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.
920
- \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.
921
- \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.
922
- \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\`.
923
- \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\`.
924
- \u2022 \`marketContext.relative.cmcExchangeLiquidity\`: historical CoinMarketCap major-exchange liquidity aggregate: total volume, 24h volume change, Binance share, concentration, and \`liquidityRegime\`.
925
- \u2022 \`marketContext.relative.cmcFearGreed\`: historical daily CoinMarketCap Fear & Greed sentiment index: value, classification, 24h/7d value changes, and \`sentimentRegime\`.
926
- \u2022 \`marketContext.relative.cmcIndexes\`: historical daily CoinMarketCap CMC100/CMC20 index values, 24h changes, top constituents, CMC20/CMC100 ratio, and \`indexRegime\`.
927
- \u2022 \`marketContext.relative.referenceTradeFlow\`: BTC/ETH reference trade-flow summary used for broad market pressure when the target symbol itself is not BTC/ETH.
928
- 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.
929
- 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.
930
- Key patterns:
931
- \u2022 current shared state: prefer \`payload.additionalIndicators.baseContext\`
932
- \u2022 recent historical series: \`payload.indicators\`
933
- \u2022 strategy service keys are possible as well, for example \`touches\`, \`distance\`, timing flags, and other setup-specific summaries
934
-
935
- How to analyze, in order:
936
- 1. Start with price structure and the setup geometry or context in \`payload.figures\`. This has higher priority than indicators.
937
- 2. Then use \`payload.additionalIndicators.baseContext\` and other explicit strategy-specific context fields.
938
- 3. Then assess confirmation or conflict from the current shared state and recent coin indicator history.
939
- 4. Then evaluate BTC context.
940
- 5. Only after that choose \`direction\`, \`quality\`, and whether an extra confirmation level is required.
941
- 6. If strong conflicts exist, reduce quality or set direction to \`null\`.
942
-
943
- Explicit conflict rules:
944
- - If the figure or price structure is invalid or doubtful, indicators must not rescue the setup.
945
- - If strategy-specific helper fields explicitly say the signal is not confirmed yet, lacks margin, or requires waiting, do not overstate quality.
946
- - If the structure is acceptable but BTC or key indicators noticeably conflict, quality is usually \`<= 3\`.
947
- - 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.
948
- - 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.
949
- - 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.
950
- - 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.
951
- - 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\`.
952
- - 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.
953
- - 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.
954
- - 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.
955
- - 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.
956
- - 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.
957
- - 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.
958
- - 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.
959
- - 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.
960
- - 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.
961
- - 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.
962
- - 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.
963
- - 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.
964
- - 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.
965
- - If the current signal is not confirmed (\`direction=null\`), name the main reason briefly in \`comment\`.
966
- If you use the structured fields, include the main reason in \`qualityReason\` or \`triggerInvalidation\`.
967
-
968
- Rules for \`direction\` / TP / SL:
969
- - \`direction = LONG\` only if the data confirms the existing LONG signal; \`SHORT\` only if the data confirms the existing SHORT signal; otherwise \`null\`.
970
- - For LONG, the expected relation is usually \`stopLossPrice < currentPrice < takeProfitPrice\`.
971
- - For SHORT, the expected relation is usually \`takeProfitPrice < currentPrice < stopLossPrice\`.
972
- - Do not optimize or recalculate TP/SL for a "better trade"; only assess whether the already supplied levels are coherent.
973
- - If \`direction = null\`, then \`takeProfitPrice = null\` and \`stopLossPrice = null\`.
974
- - If \`needRetest = false\`, then \`retestPrice = null\`.
975
- - If \`needRetest = true\`, \`retestPrice\` must be a finite number tied to a meaningful retest or breakout level.
976
- - Before responding, sanity-check the consistency of \`direction\`, TP/SL, and the current price.
977
-
978
- Quality scale:
979
- - 1: poor or chaotic setup, strong conflicts, signal not structurally confirmed
980
- - 2: weak setup, few confirmations, more of a watch or reject
981
- - 3: average setup, some structure exists, but notable conflicts remain
982
- - 4: good setup, several confirmations, structure is mostly coherent
983
- - 5: very strong setup, clean structure, confirmations, and internally coherent levels
984
-
985
- Requirements for useful structured analysis:
986
- - Include 2-4 concrete factors for or against confirmation in \`confirmations\`.
987
- - Explicitly mention the role of the key figure or structural state, for example breakout, retest, false break, touch, or lack of confirmation.
988
- - Explicitly mention BTC context as supportive, neutral, or conflicting.
989
- - Explain why the quality score is what it is.
990
- - If the signal is not confirmed (\`direction=null\`), state clearly what must change for confirmation.
991
- - In \`retestPlan\`, avoid technical placeholders like \`needRetest=false @ null\`; write a human explanation.
992
- - Do not simply restate JSON fields; add interpretation and decision logic.
993
-
994
- Rules for using trimmed series (last 5 values):
995
- - Do not make strong long-term conclusions from only 5 points.
996
- - Use 4h and 1d series as brief context, not full history.
997
- - If the data is too limited for confidence, reduce quality and use cautious wording.
998
-
999
- Short few-shot examples:
1000
- {"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."}
1001
- {"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."}
1002
-
1003
- Return only the JSON object, with no extra characters.
1004
- ${signal ? buildAiSystemPromptAddonByStrategy(signal) : ""}
1005
- `;
1006
- var buildAiPayload = (signal) => buildAiPayloadByStrategy(signal);
1007
- var getDeterministicAiGateContext = (payload) => {
1008
- const additionalIndicators = asRecord(payload.additionalIndicators);
1009
- const candidates = [
1010
- additionalIndicators,
1011
- ...Object.values(additionalIndicators ?? {}).map(asRecord)
1012
- ].filter((value) => Boolean(value));
1013
- return candidates.find(
1014
- (candidate) => Array.isArray(candidate.approvalBlockReasons) || Array.isArray(candidate.riskAnnotations) || Array.isArray(candidate.structuralHardBlockReasons) || typeof candidate.approvalAllowedNow === "boolean"
1015
- ) ?? null;
738
+ var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
739
+ const projectRoot = getTradejsProjectCwd(cwd);
740
+ const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
741
+ if (cachedMatcher) {
742
+ const resolved2 = cachedMatcher(moduleName);
743
+ return resolved2 || null;
744
+ }
745
+ const tsconfigPathsModule = await import("tsconfig-paths");
746
+ const loadConfig = tsconfigPathsModule.loadConfig;
747
+ const createMatchPath = tsconfigPathsModule.createMatchPath;
748
+ if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
749
+ return null;
750
+ }
751
+ const loadedConfig = loadConfig(projectRoot);
752
+ if (loadedConfig.resultType !== "success") {
753
+ return null;
754
+ }
755
+ const matchPath = createMatchPath(
756
+ loadedConfig.absoluteBaseUrl,
757
+ loadedConfig.paths
758
+ );
759
+ const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
760
+ ".ts",
761
+ ".tsx",
762
+ ".mts",
763
+ ".cts",
764
+ ".js",
765
+ ".jsx",
766
+ ".mjs",
767
+ ".cjs",
768
+ ".json"
769
+ ]) || "";
770
+ tsconfigPathMatchersByCwd.set(projectRoot, matcher);
771
+ const resolved = matcher(moduleName);
772
+ return resolved || null;
1016
773
  };
1017
- var buildAiHumanPrompt = (signal, payload = buildAiPayload(signal)) => `
1018
- Analyze the already computed internal signal for ${signal.symbol}. The original signal direction is ${signal.direction}.
1019
- 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.
1020
-
1021
- Trade payload:
1022
- ${JSON.stringify(payload)}
1023
- ${buildAiHumanPromptAddonByStrategy(signal, payload)}
1024
- `;
1025
- var getAiInvocationError = (error) => {
1026
- const details = error instanceof Error && error.message.trim() ? error.message.trim() : String(error);
1027
- const isEmptyCompletion = error instanceof TypeError && /Cannot read properties of undefined \(reading ['"]message['"]\)/.test(
1028
- details
774
+ var toImportSpecifier = (moduleName) => {
775
+ if (moduleName.startsWith("file://")) {
776
+ return moduleName;
777
+ }
778
+ if (import_path.default.isAbsolute(moduleName)) {
779
+ return (0, import_url.pathToFileURL)(moduleName).href;
780
+ }
781
+ return moduleName;
782
+ };
783
+ var isTsModulePath = (moduleName) => TS_MODULE_RE.test(moduleName.split("?")[0]);
784
+ var isRelativeModulePath = (moduleName) => moduleName.startsWith("./") || moduleName.startsWith("../");
785
+ var isBareModuleSpecifier = (moduleName) => {
786
+ const normalized = String(moduleName ?? "").trim();
787
+ if (!normalized) {
788
+ return false;
789
+ }
790
+ if (normalized.startsWith("file://") || import_path.default.isAbsolute(normalized) || isRelativeModulePath(normalized)) {
791
+ return false;
792
+ }
793
+ return true;
794
+ };
795
+ var importConfigFile = async (configFilePath) => {
796
+ const ext = import_path.default.extname(configFilePath).toLowerCase();
797
+ const configFileUrl = `${toImportSpecifier(configFilePath)}?t=${Date.now()}`;
798
+ if (ext === ".ts" || ext === ".mts") {
799
+ const requireFn = getRequireFn(import_path.default.dirname(configFilePath));
800
+ await ensureTsNodeRegistered();
801
+ await ensureTsconfigPathsRegistered(import_path.default.dirname(configFilePath));
802
+ return requireFn(configFilePath);
803
+ }
804
+ return import(
805
+ /* webpackIgnore: true */
806
+ configFileUrl
1029
807
  );
1030
- const wrapped = new Error(
1031
- isEmptyCompletion ? "AI provider returned an empty chat completion" : `AI model invocation failed: ${details}`
808
+ };
809
+ var importTradejsModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
810
+ const normalized = String(moduleName ?? "").trim();
811
+ if (!normalized) {
812
+ return {};
813
+ }
814
+ let modulePath = normalized;
815
+ if (normalized.startsWith("file://")) {
816
+ try {
817
+ modulePath = (0, import_url.fileURLToPath)(normalized);
818
+ } catch {
819
+ modulePath = normalized;
820
+ }
821
+ }
822
+ const requireFn = getRequireFn(
823
+ import_path.default.isAbsolute(modulePath) ? import_path.default.dirname(modulePath) : cwd
1032
824
  );
1033
- wrapped.cause = error;
1034
- return wrapped;
825
+ if (isTsModulePath(modulePath)) {
826
+ await ensureTsNodeRegistered();
827
+ await ensureTsconfigPathsRegistered(cwd);
828
+ return requireFn(modulePath);
829
+ }
830
+ if (isBareModuleSpecifier(normalized)) {
831
+ await ensureTsconfigPathsRegistered(cwd);
832
+ try {
833
+ return requireFn(normalized);
834
+ } catch (error) {
835
+ const resolvedByTsconfig = await resolveTsconfigPathModule(
836
+ normalized,
837
+ cwd
838
+ );
839
+ if (resolvedByTsconfig && resolvedByTsconfig !== normalized) {
840
+ return requireFn(resolvedByTsconfig);
841
+ }
842
+ throw error;
843
+ }
844
+ }
845
+ try {
846
+ return await import(
847
+ /* webpackIgnore: true */
848
+ toImportSpecifier(normalized)
849
+ );
850
+ } catch (error) {
851
+ if (isTsModulePath(modulePath)) {
852
+ await ensureTsNodeRegistered();
853
+ await ensureTsconfigPathsRegistered(cwd);
854
+ return requireFn(modulePath);
855
+ }
856
+ throw error;
857
+ }
1035
858
  };
1036
- var isEmptyResponseContent = (content) => typeof content === "string" ? content.trim().length === 0 : Object.keys(content).length === 0;
1037
- var DEFAULT_AI_MODEL = "openai/gpt-5-mini";
1038
- var userSettingsCache = /* @__PURE__ */ new Map();
1039
- var aiModelCache = /* @__PURE__ */ new Map();
1040
- var getAiModelCacheKey = (userName, modelName) => `${userName}::${modelName}`;
1041
- var resolveAiModelName = (settings, requestedModelName) => {
1042
- const explicitModelName = typeof requestedModelName === "string" ? requestedModelName.trim() : "";
1043
- if (explicitModelName) {
1044
- return explicitModelName;
859
+ var resolveExportedConfig = (moduleExports) => {
860
+ const candidate = moduleExports && typeof moduleExports === "object" && "default" in moduleExports ? moduleExports.default : moduleExports;
861
+ return normalizeConfig(candidate);
862
+ };
863
+ var findConfigFilePath = (cwd) => {
864
+ let currentDir = import_path.default.resolve(cwd);
865
+ while (true) {
866
+ for (const fileName of CONFIG_FILE_NAMES) {
867
+ const fullPath = import_path.default.join(currentDir, fileName);
868
+ if (import_fs.default.existsSync(fullPath) && import_fs.default.statSync(fullPath).isFile()) {
869
+ return fullPath;
870
+ }
871
+ }
872
+ const parentDir = import_path.default.dirname(currentDir);
873
+ if (parentDir === currentDir) {
874
+ return null;
875
+ }
876
+ currentDir = parentDir;
1045
877
  }
1046
- const settingsModelName = typeof settings.AI_MODEL === "string" ? settings.AI_MODEL.trim() : "";
1047
- return settingsModelName || DEFAULT_AI_MODEL;
1048
878
  };
1049
- var getOpenRouterModelKwargs = (apiEndpoint) => {
1050
- const endpoint = String(apiEndpoint ?? "").trim();
1051
- if (!endpoint) {
879
+ var resolvePluginModuleSpecifier = (moduleName, cwd = getTradejsProjectCwd()) => {
880
+ const normalized = String(moduleName ?? "").trim();
881
+ if (!normalized) {
882
+ return "";
883
+ }
884
+ if (normalized.startsWith("file://")) {
885
+ try {
886
+ return (0, import_url.fileURLToPath)(normalized);
887
+ } catch {
888
+ return normalized;
889
+ }
890
+ }
891
+ if (import_path.default.isAbsolute(normalized)) {
892
+ return normalized;
893
+ }
894
+ if (isRelativeModulePath(normalized)) {
895
+ return import_path.default.resolve(cwd, normalized);
896
+ }
897
+ return normalized;
898
+ };
899
+ var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
900
+ const cached = cachedByCwd.get(cwd);
901
+ if (cached) {
902
+ return cached;
903
+ }
904
+ const configFilePath = findConfigFilePath(cwd);
905
+ if (!configFilePath) {
906
+ cachedByCwd.set(cwd, {});
1052
907
  return {};
1053
908
  }
1054
- let hostname = "";
1055
909
  try {
1056
- hostname = new URL(endpoint).hostname;
1057
- } catch {
1058
- hostname = endpoint;
1059
- }
1060
- if (!hostname.toLowerCase().includes("openrouter")) {
910
+ const moduleExports = await importConfigFile(configFilePath);
911
+ const config = resolveExportedConfig(moduleExports);
912
+ cachedByCwd.set(cwd, config);
913
+ if (!announcedConfigFile.has(configFilePath)) {
914
+ announcedConfigFile.add(configFilePath);
915
+ import_logger.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
916
+ }
917
+ return config;
918
+ } catch (error) {
919
+ import_logger.logger.log(
920
+ "warn",
921
+ "Failed to load TradeJS config from %s: %s",
922
+ configFilePath,
923
+ String(error)
924
+ );
925
+ cachedByCwd.set(cwd, {});
1061
926
  return {};
1062
927
  }
928
+ };
929
+
930
+ // src/strategy/manifests.ts
931
+ var SHARED_STRATEGY_REGISTRY_KEY = "__tradejsNodeSharedStrategyRegistryV1__";
932
+ var sharedRegistryScope = globalThis;
933
+ var sharedStrategyRegistry = sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] ?? (sharedRegistryScope[SHARED_STRATEGY_REGISTRY_KEY] = {
934
+ registryStateByProjectRoot: /* @__PURE__ */ new Map()
935
+ });
936
+ var createStrategyRegistryState = () => ({
937
+ strategyCreators: /* @__PURE__ */ new Map(),
938
+ strategyManifestsMap: /* @__PURE__ */ new Map(),
939
+ strategyEntriesMap: /* @__PURE__ */ new Map(),
940
+ pluginsLoadPromise: null
941
+ });
942
+ var registryStateByProjectRoot = sharedStrategyRegistry.registryStateByProjectRoot;
943
+ var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
944
+ const projectRoot = getTradejsProjectCwd(cwd);
945
+ let state = registryStateByProjectRoot.get(projectRoot);
946
+ if (!state) {
947
+ state = createStrategyRegistryState();
948
+ registryStateByProjectRoot.set(projectRoot, state);
949
+ }
1063
950
  return {
1064
- provider: {
1065
- ignore: ["azure"]
1066
- }
951
+ projectRoot,
952
+ state
1067
953
  };
1068
954
  };
1069
- var getAiSettings = async (userName = "root") => {
1070
- let settingsPromise = userSettingsCache.get(userName);
1071
- if (!settingsPromise) {
1072
- settingsPromise = (0, import_userSettings.getUserSettings)(userName);
1073
- settingsPromise.catch(() => {
1074
- userSettingsCache.delete(userName);
1075
- });
1076
- userSettingsCache.set(userName, settingsPromise);
955
+ var toUniqueModules = (modules = []) => [
956
+ ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
957
+ ];
958
+ var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
959
+ const config = await loadTradejsConfig(cwd);
960
+ return {
961
+ strategyModules: toUniqueModules(config.strategies),
962
+ indicatorModules: toUniqueModules(config.indicators)
963
+ };
964
+ };
965
+ var extractModuleEntries = (moduleExport, key) => {
966
+ if (!moduleExport || typeof moduleExport !== "object") {
967
+ return null;
968
+ }
969
+ const candidate = moduleExport;
970
+ if (Array.isArray(candidate[key])) {
971
+ return candidate[key];
972
+ }
973
+ const defaultExport = candidate.default;
974
+ if (defaultExport && Array.isArray(defaultExport[key])) {
975
+ return defaultExport[key];
976
+ }
977
+ return null;
978
+ };
979
+ var extractStrategyPluginDefinition = (moduleExport) => {
980
+ const strategyEntries = extractModuleEntries(
981
+ moduleExport,
982
+ "strategyEntries"
983
+ );
984
+ return strategyEntries ? { strategyEntries } : null;
985
+ };
986
+ var extractIndicatorPluginDefinition = (moduleExport) => {
987
+ const indicatorEntries = extractModuleEntries(
988
+ moduleExport,
989
+ "indicatorEntries"
990
+ );
991
+ return indicatorEntries ? { indicatorEntries } : null;
992
+ };
993
+ var registerEntries = (entries, source, state) => {
994
+ for (const entry of entries) {
995
+ const strategyName = entry.manifest?.name;
996
+ if (!strategyName) {
997
+ import_logger2.logger.warn("Skip strategy entry without name from %s", source);
998
+ continue;
999
+ }
1000
+ if (state.strategyCreators.has(strategyName)) {
1001
+ import_logger2.logger.warn(
1002
+ 'Skip duplicate strategy "%s" from %s: already registered',
1003
+ strategyName,
1004
+ source
1005
+ );
1006
+ continue;
1007
+ }
1008
+ state.strategyManifestsMap.set(strategyName, entry.manifest);
1009
+ state.strategyEntriesMap.set(strategyName, entry);
1010
+ materializeStrategyCreator(strategyName, state);
1011
+ }
1012
+ };
1013
+ var materializeStrategyCreator = (strategyName, state) => {
1014
+ if (state.strategyCreators.has(strategyName) || !sharedStrategyRegistry.strategyRuntimeFactory) {
1015
+ return;
1016
+ }
1017
+ const entry = state.strategyEntriesMap.get(strategyName);
1018
+ if (!entry) return;
1019
+ state.strategyCreators.set(
1020
+ strategyName,
1021
+ sharedStrategyRegistry.strategyRuntimeFactory({
1022
+ strategyName,
1023
+ defaults: entry.defaults,
1024
+ createCore: entry.createCore,
1025
+ manifest: entry.manifest,
1026
+ detectorKey: entry.detectorKey,
1027
+ detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
1028
+ resolveRegisteredManifest: (name) => state.strategyManifestsMap.get(name)
1029
+ })
1030
+ );
1031
+ };
1032
+ var setStrategyRuntimeFactory = (factory) => {
1033
+ sharedStrategyRegistry.strategyRuntimeFactory = factory;
1034
+ for (const state of registryStateByProjectRoot.values()) {
1035
+ for (const strategyName of state.strategyEntriesMap.keys()) {
1036
+ materializeStrategyCreator(strategyName, state);
1037
+ }
1077
1038
  }
1078
- const settings = await settingsPromise;
1079
- if (!settings.AI_API_KEY || !settings.AI_API_ENDPOINT) {
1080
- throw new Error(`AI settings are incomplete for user ${userName}`);
1039
+ };
1040
+ var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1041
+ if (typeof importTradejsModule === "function") {
1042
+ return importTradejsModule(moduleName, cwd);
1081
1043
  }
1082
- return settings;
1044
+ return import(
1045
+ /* webpackIgnore: true */
1046
+ moduleName
1047
+ );
1083
1048
  };
1084
- var createAiModel = async (userName = "root", requestedModelName) => {
1085
- const settings = await getAiSettings(userName);
1086
- const modelName = resolveAiModelName(settings, requestedModelName);
1087
- const cacheKey = getAiModelCacheKey(userName, modelName);
1088
- let modelPromise = aiModelCache.get(cacheKey);
1089
- if (!modelPromise) {
1090
- modelPromise = (async () => {
1091
- const { ChatOpenAI } = await import("@langchain/openai");
1092
- const modelKwargs = getOpenRouterModelKwargs(settings.AI_API_ENDPOINT);
1093
- return new ChatOpenAI({
1094
- temperature: 0.2,
1095
- modelName,
1096
- apiKey: settings.AI_API_KEY,
1097
- ...Object.keys(modelKwargs).length ? { modelKwargs } : {},
1098
- configuration: {
1099
- baseURL: settings.AI_API_ENDPOINT,
1100
- defaultHeaders: {
1101
- "HTTP-Referer": "https://tradejs.dev",
1102
- "X-Title": "Inv"
1049
+ var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
1050
+ const { projectRoot, state } = getStrategyRegistryState(cwd);
1051
+ if (!state.pluginsLoadPromise) {
1052
+ (0, import_indicators.resetIndicatorRegistryCache)(projectRoot);
1053
+ state.pluginsLoadPromise = (async () => {
1054
+ const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
1055
+ const strategySet = new Set(strategyModules);
1056
+ const indicatorSet = new Set(indicatorModules);
1057
+ const pluginModuleNames = [
1058
+ .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
1059
+ ];
1060
+ if (!pluginModuleNames.length) {
1061
+ return;
1062
+ }
1063
+ for (const moduleName of pluginModuleNames) {
1064
+ try {
1065
+ const resolvedModuleName = resolvePluginModuleSpecifier(
1066
+ moduleName,
1067
+ projectRoot
1068
+ );
1069
+ const moduleExport = await importStrategyPluginModule(
1070
+ resolvedModuleName,
1071
+ projectRoot
1072
+ );
1073
+ if (strategySet.has(moduleName)) {
1074
+ const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
1075
+ if (!pluginDefinition) {
1076
+ import_logger2.logger.warn(
1077
+ 'Skip strategy plugin "%s": export { strategyEntries } is missing',
1078
+ moduleName
1079
+ );
1080
+ } else {
1081
+ registerEntries(
1082
+ pluginDefinition.strategyEntries,
1083
+ moduleName,
1084
+ state
1085
+ );
1086
+ }
1087
+ }
1088
+ if (indicatorSet.has(moduleName)) {
1089
+ const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
1090
+ if (!indicatorPluginDefinition) {
1091
+ import_logger2.logger.warn(
1092
+ 'Skip indicator plugin "%s": export { indicatorEntries } is missing',
1093
+ moduleName
1094
+ );
1095
+ } else {
1096
+ (0, import_indicators.registerIndicatorEntries)(
1097
+ indicatorPluginDefinition.indicatorEntries,
1098
+ moduleName,
1099
+ projectRoot
1100
+ );
1101
+ }
1102
+ }
1103
+ if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
1104
+ import_logger2.logger.warn(
1105
+ 'Skip plugin "%s": no strategy/indicator sections requested in config',
1106
+ moduleName
1107
+ );
1103
1108
  }
1109
+ } catch (error) {
1110
+ import_logger2.logger.warn(
1111
+ 'Failed to load plugin "%s": %s',
1112
+ moduleName,
1113
+ String(error)
1114
+ );
1104
1115
  }
1105
- });
1116
+ }
1106
1117
  })();
1107
- modelPromise.catch(() => {
1108
- aiModelCache.delete(cacheKey);
1109
- });
1110
- aiModelCache.set(cacheKey, modelPromise);
1111
- }
1112
- return modelPromise;
1113
- };
1114
- var getAiModel = async (userName = "root", requestedModelName) => {
1115
- const settings = await getAiSettings(userName);
1116
- const resolvedModelName = resolveAiModelName(settings, requestedModelName);
1117
- try {
1118
- return await createAiModel(userName, resolvedModelName);
1119
- } catch (error) {
1120
- aiModelCache.delete(getAiModelCacheKey(userName, resolvedModelName));
1121
- userSettingsCache.delete(userName);
1122
- throw error;
1123
1118
  }
1119
+ await state.pluginsLoadPromise;
1124
1120
  };
1125
- var ensureAiStrategyPluginsLoaded = async () => {
1126
- await ensureStrategyPluginsLoaded();
1121
+ var getStrategyCreator = async (name, cwd = getTradejsProjectCwd()) => {
1122
+ await ensureStrategyPluginsLoaded(cwd);
1123
+ const { state } = getStrategyRegistryState(cwd);
1124
+ return state.strategyCreators.get(name);
1127
1125
  };
1128
- var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
1129
- if (options.signal) {
1130
- await ensureAiStrategyPluginsLoaded();
1126
+ var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
1127
+ if (!name) {
1128
+ return void 0;
1131
1129
  }
1132
- const [{ HumanMessage, SystemMessage }, model, settings] = await Promise.all([
1133
- import("@langchain/core/messages"),
1134
- getAiModel(options.userName, options.model),
1135
- getAiSettings(options.userName)
1136
- ]);
1137
- const messages = [];
1138
- const responseLanguage = (0, import_aiLanguages.getAiResponseLanguagePromptName)(
1139
- settings.AI_RESPONSE_LANGUAGE || import_aiLanguages.DEFAULT_AI_RESPONSE_LANGUAGE
1140
- );
1141
- messages.push(new SystemMessage(systemPrompt));
1142
- messages.push(
1143
- new SystemMessage(
1144
- `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
1145
- )
1146
- );
1147
- messages.push(
1148
- new HumanMessage({
1149
- content: [
1150
- {
1151
- type: "text",
1152
- text: humanPrompt
1153
- }
1154
- ]
1130
+ const { state } = getStrategyRegistryState(cwd);
1131
+ return state.strategyManifestsMap.get(name);
1132
+ };
1133
+ var strategies = new Proxy(
1134
+ {},
1135
+ {
1136
+ get: (_target, property) => {
1137
+ if (typeof property !== "string") {
1138
+ return void 0;
1139
+ }
1140
+ return getStrategyRegistryState().state.strategyCreators.get(property);
1141
+ },
1142
+ ownKeys: () => {
1143
+ return [...getStrategyRegistryState().state.strategyCreators.keys()];
1144
+ },
1145
+ getOwnPropertyDescriptor: () => ({
1146
+ enumerable: true,
1147
+ configurable: true
1155
1148
  })
1156
- );
1157
- let response;
1158
- try {
1159
- response = await model.invoke(messages);
1160
- } catch (error) {
1161
- throw getAiInvocationError(error);
1162
1149
  }
1163
- const responseContent = normalizeResponseContent(response?.content);
1164
- if (isEmptyResponseContent(responseContent)) {
1165
- throw new Error("AI provider returned an empty chat completion");
1150
+ );
1151
+
1152
+ // src/strategy/policyProfiles.ts
1153
+ var profileMatches = (profile, universe, assetClass) => {
1154
+ const { appliesTo } = profile;
1155
+ if (!appliesTo) return true;
1156
+ if (appliesTo.universes?.length && (!universe || !appliesTo.universes.includes(universe))) {
1157
+ return false;
1166
1158
  }
1167
- const parsed = parseAIResponse(responseContent);
1168
- const normalized = normalizeAnalysis(parsed);
1169
- if (!options.signal) {
1170
- return normalized;
1159
+ if (appliesTo.assetClasses?.length && (!assetClass || !appliesTo.assetClasses.includes(assetClass))) {
1160
+ return false;
1171
1161
  }
1172
- return postProcessAiAnalysisByStrategy(
1173
- options.signal,
1174
- normalized,
1175
- options.payload
1176
- );
1177
- };
1178
- var runAiPromptLocal = async (signal, options = {}) => {
1179
- await ensureAiStrategyPluginsLoaded();
1180
- const payload = options.payload ?? buildAiPayload(signal);
1181
- const gateContext = getDeterministicAiGateContext(payload);
1182
- const signalDirection = getSignalDirection(signal);
1183
- const deterministicQuality = getDeterministicQuality(gateContext);
1184
- const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
1185
- return postProcessLocalAiAnalysisByStrategy(
1186
- signal,
1187
- {
1188
- direction: approvalAllowedNow ? signalDirection : null,
1189
- quality: deterministicQuality,
1190
- needRetest: !approvalAllowedNow,
1191
- retestPrice: null,
1192
- takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
1193
- stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
1194
- },
1195
- payload
1196
- );
1162
+ return true;
1197
1163
  };
1198
- var askAI = async (signal, options = {}) => {
1199
- const { symbol } = signal;
1200
- await ensureAiStrategyPluginsLoaded();
1201
- const payload = buildAiPayload(signal);
1202
- const content = await runAiPrompt(
1203
- {
1204
- systemPrompt: buildAiSystemPrompt(signal),
1205
- humanPrompt: buildAiHumanPrompt(signal, payload)
1206
- },
1207
- {
1208
- ...options,
1209
- signal,
1210
- payload
1164
+ var resolveStrategyPolicyProfile = (manifest, params) => {
1165
+ const profiles = manifest?.policyProfiles ?? [];
1166
+ if (!profiles.length) {
1167
+ const inferredId = params.profileId ?? (params.universe === "tradfi" ? "tradfi" : void 0);
1168
+ if (!inferredId) return void 0;
1169
+ if (inferredId !== "crypto" && inferredId !== "tradfi") {
1170
+ throw new Error(
1171
+ `Unknown policy profile "${inferredId}" for strategy "${manifest?.name}"`
1172
+ );
1173
+ }
1174
+ if (params.universe && inferredId !== params.universe) {
1175
+ throw new Error(
1176
+ `Policy profile "${inferredId}" is not compatible with ${params.universe}`
1177
+ );
1178
+ }
1179
+ return {
1180
+ id: inferredId,
1181
+ appliesTo: { universes: [inferredId] },
1182
+ marketDataRequirements: inferredId === "crypto" ? ["crypto.btcReference"] : [],
1183
+ ...manifest?.mlAdapter ? {
1184
+ entryRuntimeDefaults: {
1185
+ ml: {
1186
+ modelKey: inferredId === "crypto" ? manifest.name : `${manifest.name}:tradfi`
1187
+ }
1188
+ }
1189
+ } : {}
1190
+ };
1191
+ }
1192
+ if (params.profileId) {
1193
+ const profile = profiles.find(({ id }) => id === params.profileId);
1194
+ if (!profile) {
1195
+ throw new Error(
1196
+ `Unknown policy profile "${params.profileId}" for strategy "${manifest?.name}"`
1197
+ );
1198
+ }
1199
+ if (!profileMatches(profile, params.universe, params.assetClass)) {
1200
+ throw new Error(
1201
+ `Policy profile "${params.profileId}" is not compatible with ${params.universe ?? "unknown"}:${params.assetClass ?? "unknown"}`
1202
+ );
1211
1203
  }
1204
+ return profile;
1205
+ }
1206
+ const matching = profiles.filter(
1207
+ (profile) => profileMatches(profile, params.universe, params.assetClass)
1212
1208
  );
1213
- await (0, import_redis.setData)(import_redis.redisKeys.analysis(symbol, signal.signalId), content);
1214
- return content;
1215
- };
1216
-
1217
- // src/strategyAdapters/ml.ts
1218
- var defaultMlAdapter = {
1219
- normalizeStrategyConfig: (strategyConfig) => strategyConfig
1220
- };
1221
- var getStrategyMlAdapter = (strategy, profileId) => {
1222
- const strategyAdapter = getStrategyProfileMlAdapter(
1223
- getStrategyManifest(strategy),
1224
- profileId
1209
+ const defaultProfile = matching.find(
1210
+ ({ id }) => id === manifest?.defaultPolicyProfileId
1225
1211
  );
1226
- if (!strategyAdapter) return defaultMlAdapter;
1227
- return {
1228
- ...defaultMlAdapter,
1229
- ...strategyAdapter
1230
- };
1212
+ return defaultProfile ?? matching[0];
1231
1213
  };
1214
+ var getStrategyProfileAiAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.aiAdapter ?? manifest?.aiAdapter;
1215
+ var getStrategyProfileMlAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.mlAdapter ?? manifest?.mlAdapter;
1232
1216
 
1233
- // src/mlPayload.ts
1234
- var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
1235
- return getStrategyMlAdapter(
1236
- strategyName,
1237
- profileId
1238
- ).normalizeStrategyConfig?.(strategyConfig);
1217
+ // src/strategyAdapters/ai.ts
1218
+ var toRecord2 = (value) => {
1219
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1220
+ return {};
1221
+ }
1222
+ return value;
1239
1223
  };
1240
- var buildMlPayload = (payload) => {
1241
- const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
1242
- const profileId = payload.signal?.policyProfileId;
1243
- const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
1244
- const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
1245
- const nextSignal = {
1246
- ...normalizedSignal,
1247
- indicators: {
1248
- ...normalizedSignal?.indicators ?? {}
1249
- }
1224
+ var buildBaseAiPayload = (signal) => {
1225
+ const additionalIndicators = {
1226
+ ...toRecord2(signal.additionalIndicators),
1227
+ marketContext: buildAiMarketContext(signal)
1250
1228
  };
1251
- const nextContext = payload.context ? {
1252
- ...payload.context,
1253
- strategyConfig: normalizeStrategyConfig(
1254
- payload.context.strategyConfig,
1255
- strategyName,
1256
- profileId
1257
- )
1258
- } : void 0;
1259
1229
  return {
1260
- signal: nextSignal,
1261
- context: nextContext
1230
+ signal: {
1231
+ symbol: signal.symbol,
1232
+ signalId: signal.signalId,
1233
+ interval: signal.interval,
1234
+ direction: signal.direction,
1235
+ timestamp: signal.timestamp,
1236
+ strategy: signal.strategy,
1237
+ prices: {
1238
+ currentPrice: signal.prices.currentPrice,
1239
+ takeProfitPrice: signal.prices.takeProfitPrice,
1240
+ stopLossPrice: signal.prices.stopLossPrice
1241
+ }
1242
+ },
1243
+ figures: trimSeriesDeep(signal.figures ?? {}),
1244
+ indicators: buildCompactAiIndicatorsSnapshot(signal.indicators),
1245
+ additionalIndicators: trimSeriesDeep(additionalIndicators)
1262
1246
  };
1263
1247
  };
1264
-
1265
- // src/tradejsConfig.ts
1266
- var import_fs = __toESM(require("fs"));
1267
- var import_path = __toESM(require("path"));
1268
- var import_url = require("url");
1269
- var import_config = require("@tradejs/core/config");
1270
- var import_logger = require("@tradejs/infra/logger");
1271
- var CONFIG_FILE_NAMES = [
1272
- "tradejs.config.ts",
1273
- "tradejs.config.mts",
1274
- "tradejs.config.js",
1275
- "tradejs.config.mjs",
1276
- "tradejs.config.cjs"
1277
- ];
1278
- var TS_MODULE_RE = /\.(cts|mts|ts)$/i;
1279
- var cachedByCwd = /* @__PURE__ */ new Map();
1280
- var announcedConfigFile = /* @__PURE__ */ new Set();
1281
- var tsNodeRegistered = false;
1282
- var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
1283
- var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
1284
- var getTradejsProjectCwd = (cwd) => {
1285
- const explicit = String(cwd ?? "").trim();
1286
- if (explicit) {
1287
- return import_path.default.resolve(explicit);
1288
- }
1289
- const fromEnv = String(process.env.PROJECT_CWD || "").trim();
1290
- if (fromEnv) {
1291
- return import_path.default.resolve(fromEnv);
1292
- }
1293
- return process.cwd();
1248
+ var defaultAiAdapter = {};
1249
+ var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
1250
+ var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
1251
+ var buildAiPayloadByStrategy = (signal) => {
1252
+ const basePayload = buildBaseAiPayload(signal);
1253
+ const adapter = getSignalAiAdapter(signal);
1254
+ return adapter.buildPayload?.({ signal, basePayload }) ?? basePayload;
1294
1255
  };
1295
- var normalizeConfig = (rawConfig) => {
1296
- if (!rawConfig || typeof rawConfig !== "object") {
1297
- return {};
1298
- }
1299
- const config = rawConfig;
1300
- const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
1301
- const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
1302
- const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
1303
- const hooks = (0, import_config.normalizeTradejsConfigHooks)(
1304
- config.hooks
1305
- );
1306
- return {
1307
- strategies: strategies2,
1308
- indicators,
1309
- connectors,
1310
- ...hooks ? { hooks } : {}
1311
- };
1256
+ var buildAiSystemPromptAddonByStrategy = (signal) => getSignalAiAdapter(signal).buildSystemPromptAddon?.({ signal }) ?? "";
1257
+ var buildAiHumanPromptAddonByStrategy = (signal, payload) => getSignalAiAdapter(signal).buildHumanPromptAddon?.({
1258
+ signal,
1259
+ payload
1260
+ }) ?? "";
1261
+ var postProcessAiAnalysisByStrategy = (signal, analysis, payload = buildAiPayloadByStrategy(signal)) => getSignalAiAdapter(signal).postProcessAnalysis?.({
1262
+ signal,
1263
+ payload,
1264
+ analysis
1265
+ }) ?? analysis;
1266
+ var postProcessLocalAiAnalysisByStrategy = (signal, analysis, payload = buildAiPayloadByStrategy(signal)) => {
1267
+ const adapter = getSignalAiAdapter(signal);
1268
+ const strategyAnalysis = adapter.postProcessAnalysis?.({ signal, payload, analysis }) ?? analysis;
1269
+ return adapter.postProcessLocalAnalysis?.({
1270
+ signal,
1271
+ payload,
1272
+ analysis: strategyAnalysis
1273
+ }) ?? strategyAnalysis;
1312
1274
  };
1313
- var getNodeCreateRequire = () => {
1314
- const builtinModule = process.getBuiltinModule?.("module");
1315
- if (typeof builtinModule?.createRequire === "function") {
1316
- return builtinModule.createRequire;
1275
+
1276
+ // src/ai.ts
1277
+ var parseAIResponse = (input) => {
1278
+ try {
1279
+ if (typeof input === "object" && input !== null) return input;
1280
+ const match = input.match(/\{[\s\S]*\}/);
1281
+ if (!match) throw new Error("JSON block not found");
1282
+ return JSON.parse(match[0]);
1283
+ } catch (err) {
1284
+ console.error("Failed to parse AI response:", err);
1285
+ console.log("Raw AI response:", input);
1286
+ return {};
1317
1287
  }
1318
- throw new TypeError("module.createRequire is not available");
1319
1288
  };
1320
- var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
1321
- var ensureTsNodeRegistered = async () => {
1322
- if (tsNodeRegistered) {
1323
- return;
1324
- }
1325
- const tsNodeModule = await import("ts-node");
1326
- const tsNode = tsNodeModule.default ?? tsNodeModule;
1327
- tsNode.register?.({
1328
- transpileOnly: true,
1329
- compilerOptions: {
1330
- module: "Node16",
1331
- moduleResolution: "node16"
1289
+ var normalizeResponseContent = (content) => {
1290
+ if (typeof content === "string" || content && typeof content === "object") {
1291
+ if (typeof content !== "object" || !Array.isArray(content)) {
1292
+ return content;
1332
1293
  }
1333
- });
1334
- tsNodeRegistered = true;
1335
- };
1336
- var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
1337
- const projectRoot = getTradejsProjectCwd(cwd);
1338
- if (tsconfigPathsRegisteredByCwd.has(projectRoot)) {
1339
- return;
1340
- }
1341
- const tsconfigPathsModule = await import("tsconfig-paths");
1342
- const loadConfig = tsconfigPathsModule.loadConfig;
1343
- const register = tsconfigPathsModule.register;
1344
- if (typeof loadConfig !== "function" || typeof register !== "function") {
1345
- return;
1346
1294
  }
1347
- const loadedConfig = loadConfig(projectRoot);
1348
- if (loadedConfig.resultType !== "success") {
1349
- return;
1295
+ if (Array.isArray(content)) {
1296
+ const text = content.map((part) => typeof part?.text === "string" ? part.text : "").join("\n").trim();
1297
+ return text;
1350
1298
  }
1351
- register({
1352
- baseUrl: loadedConfig.absoluteBaseUrl,
1353
- paths: loadedConfig.paths,
1354
- addMatchAll: false
1355
- });
1356
- tsconfigPathsRegisteredByCwd.add(projectRoot);
1299
+ return String(content ?? "");
1357
1300
  };
1358
- var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1359
- const projectRoot = getTradejsProjectCwd(cwd);
1360
- const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
1361
- if (cachedMatcher) {
1362
- const resolved2 = cachedMatcher(moduleName);
1363
- return resolved2 || null;
1364
- }
1365
- const tsconfigPathsModule = await import("tsconfig-paths");
1366
- const loadConfig = tsconfigPathsModule.loadConfig;
1367
- const createMatchPath = tsconfigPathsModule.createMatchPath;
1368
- if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
1301
+ var normalizeAnalysis = (raw) => {
1302
+ const direction = raw?.direction === "LONG" || raw?.direction === "SHORT" ? raw.direction : null;
1303
+ const qualityNum = typeof raw?.quality === "number" ? Math.max(1, Math.min(5, Math.round(raw.quality))) : void 0;
1304
+ const toNumberOrNull = (value) => {
1305
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1306
+ if (typeof value === "string" && value.trim()) {
1307
+ const parsed = Number(value);
1308
+ if (Number.isFinite(parsed)) return parsed;
1309
+ }
1369
1310
  return null;
1370
- }
1371
- const loadedConfig = loadConfig(projectRoot);
1372
- if (loadedConfig.resultType !== "success") {
1311
+ };
1312
+ const toText = (value) => typeof value === "string" ? value.slice(0, 400) : void 0;
1313
+ return {
1314
+ direction,
1315
+ quality: qualityNum,
1316
+ needRetest: Boolean(raw?.needRetest),
1317
+ retestPrice: toNumberOrNull(raw?.retestPrice),
1318
+ takeProfitPrice: toNumberOrNull(raw?.takeProfitPrice),
1319
+ stopLossPrice: toNumberOrNull(raw?.stopLossPrice),
1320
+ setup: toText(raw?.setup),
1321
+ confirmations: toText(raw?.confirmations),
1322
+ btcContext: toText(raw?.btcContext),
1323
+ retestPlan: toText(raw?.retestPlan),
1324
+ riskLevels: toText(raw?.riskLevels),
1325
+ qualityReason: toText(raw?.qualityReason),
1326
+ triggerInvalidation: toText(raw?.triggerInvalidation),
1327
+ comment: typeof raw?.comment === "string" ? raw.comment.slice(0, 1024) : ""
1328
+ };
1329
+ };
1330
+ var asRecord = (value) => {
1331
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1373
1332
  return null;
1374
1333
  }
1375
- const matchPath = createMatchPath(
1376
- loadedConfig.absoluteBaseUrl,
1377
- loadedConfig.paths
1378
- );
1379
- const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
1380
- ".ts",
1381
- ".tsx",
1382
- ".mts",
1383
- ".cts",
1384
- ".js",
1385
- ".jsx",
1386
- ".mjs",
1387
- ".cjs",
1388
- ".json"
1389
- ]) || "";
1390
- tsconfigPathMatchersByCwd.set(projectRoot, matcher);
1391
- const resolved = matcher(moduleName);
1392
- return resolved || null;
1334
+ return value;
1393
1335
  };
1394
- var toImportSpecifier = (moduleName) => {
1395
- if (moduleName.startsWith("file://")) {
1396
- return moduleName;
1336
+ var getSignalDirection = (signal) => signal.direction === "LONG" || signal.direction === "SHORT" ? signal.direction : null;
1337
+ var getDeterministicQuality = (gateContext) => {
1338
+ const deterministicQuality = Number(gateContext?.deterministicQuality);
1339
+ if (Number.isFinite(deterministicQuality)) {
1340
+ return Math.max(1, Math.min(5, Math.round(deterministicQuality)));
1397
1341
  }
1398
- if (import_path.default.isAbsolute(moduleName)) {
1399
- return (0, import_url.pathToFileURL)(moduleName).href;
1342
+ const maxAllowedQuality = Number(gateContext?.maxAllowedQuality);
1343
+ if (Number.isFinite(maxAllowedQuality)) {
1344
+ return Math.max(1, Math.min(5, Math.round(maxAllowedQuality)));
1400
1345
  }
1401
- return moduleName;
1346
+ return Array.isArray(gateContext?.approvalBlockReasons) && gateContext.approvalBlockReasons.length > 0 || Array.isArray(gateContext?.structuralHardBlockReasons) && gateContext.structuralHardBlockReasons.length > 0 ? 2 : 3;
1402
1347
  };
1403
- var isTsModulePath = (moduleName) => TS_MODULE_RE.test(moduleName.split("?")[0]);
1404
- var isRelativeModulePath = (moduleName) => moduleName.startsWith("./") || moduleName.startsWith("../");
1405
- var isBareModuleSpecifier = (moduleName) => {
1406
- const normalized = String(moduleName ?? "").trim();
1407
- if (!normalized) {
1408
- return false;
1409
- }
1410
- if (normalized.startsWith("file://") || import_path.default.isAbsolute(normalized) || isRelativeModulePath(normalized)) {
1411
- return false;
1412
- }
1413
- return true;
1348
+ var buildAiSystemPrompt = (signal) => `
1349
+ You are an internal market-structure classifier for an already computed system signal.
1350
+ Analyze the provided JSON containing the trade, candles, indicators (for the coin and BTC across multiple timeframes), and strategy figures/context.
1351
+ Series data is already trimmed to the latest 5 values.
1352
+
1353
+ Important:
1354
+ - Do not invent missing data.
1355
+ - This is an internal audit/classification task, not user-facing trading advice.
1356
+ - Do not generate execution instructions, do not replace the original thesis with a new one, and do not provide personalized investment advice.
1357
+ - Use the original signal direction and levels as the anchor, but you may state that the current structure does not support them.
1358
+ - Respect the source strategy specified in \`signal.strategy\`.
1359
+ - Your goal is to explain how well the observed structure matches the existing signal and how structurally confirmed it is right now.
1360
+ - Do not write vague statements like "there is momentum/slope" without tying them to the decision.
1361
+ - Write all user-visible text fields in the requested response language. If no explicit language instruction is provided later, default to English.
1362
+ - If confidence is incomplete, prefer cautious wording such as "likely", "not confirmed yet", or "probably" instead of categorical claims.
1363
+
1364
+ Return exactly one JSON object and nothing else:
1365
+
1366
+ {
1367
+ "direction": payload.signal.direction | null,
1368
+ "quality": 1 | 2 | 3 | 4 | 5,
1369
+ "needRetest": boolean,
1370
+ "retestPrice": number | null,
1371
+ "takeProfitPrice": number | null,
1372
+ "stopLossPrice": number | null,
1373
+ "setup": string,
1374
+ "confirmations": string,
1375
+ "btcContext": string,
1376
+ "retestPlan": string,
1377
+ "riskLevels": string,
1378
+ "qualityReason": string,
1379
+ "triggerInvalidation": string
1380
+ }
1381
+
1382
+ - Do not add any other fields.
1383
+ - All numbers must be finite, with no \`NaN\` or \`Infinity\`.
1384
+ - All text fields must be short strings with no line breaks and no markdown lists.
1385
+ - \`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.
1386
+ - \`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.
1387
+ - \`needRetest\` indicates whether an additional confirmation level is required before the current signal can be treated as structurally confirmed.
1388
+ - \`retestPrice\` is the key level that would confirm or invalidate the structure, or \`null\` if no extra level is needed or available.
1389
+ - \`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\`.
1390
+ - Use these fields as separate parts of the analysis:
1391
+ - \`setup\`: the current structural setup or trendline state.
1392
+ - \`confirmations\`: 2-4 concrete confirmations or conflicts from the coin indicators.
1393
+ - \`btcContext\`: whether BTC supports the idea, is neutral, or conflicts with it.
1394
+ - \`retestPlan\`: what must happen at the key level to confirm the structure, or why no extra level is needed.
1395
+ - \`riskLevels\`: a short note on whether the existing levels and risk structure are internally coherent, without creating a new trade plan.
1396
+ - \`qualityReason\`: why the quality score is what it is.
1397
+ - \`triggerInvalidation\`: what must happen to confirm the signal or what invalidates the current structural thesis.
1398
+ - \`comment\` is optional. If you include it, do not just duplicate the structured fields.
1399
+
1400
+ If the data is insufficient or the setup is weak, return \`"direction": null\`, \`quality <= 2\`, and explain why.
1401
+
1402
+ Input payload structure:
1403
+ - payload.signal:
1404
+ symbol, signalId, interval, direction, timestamp, strategy, prices
1405
+ - payload.signal.prices:
1406
+ currentPrice, takeProfitPrice, stopLossPrice
1407
+ - payload.figures:
1408
+ strategy-specific figures or geometry when available. Fields vary by strategy.
1409
+ - payload.indicators:
1410
+ 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.
1411
+ - payload.additionalIndicators:
1412
+ strategy-specific summary/context fields plus the canonical current shared context snapshot.
1413
+ This is not noise; it contains derived fields deliberately passed by the strategy to help the decision.
1414
+ Examples: baseContext, helperFlags, structureContext, volatilitySummary.
1415
+ Always inspect \`payload.additionalIndicators.baseContext\` first for the current shared state:
1416
+ \u2022 \`baseContext.raw\`: current MA, ATR, BB, OBV, price stats, levels, BTC correlation.
1417
+ \u2022 \`baseContext.regime\`: derived trend / volatility / momentum / session regime fields.
1418
+ \u2022 \`baseContext.structure\`: local range position, breakout freshness/quality, level-touch counts, rejection wick context.
1419
+ \u2022 \`baseContext.participation\`: volume/turnover participation, effort-vs-result context, Binance aggTrades, and fingerprinted Hyperliquid whale-flow when available.
1420
+ \u2022 \`baseContext.relative\`: BTC/ETH relative-strength, benchmark MA bias context, Binance alt-basket breadth, and CoinMarketCap historical global/exchange/index context when available.
1421
+ \u2022 \`baseContext.derivatives\`: Coinalyze-aligned derivatives summary when available.
1422
+ \u2022 \`baseContext.mtf\`: compact multi-timeframe summary plus only the latest few candles for each timeframe.
1423
+ \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.
1424
+ Always inspect \`payload.additionalIndicators.marketContext\` when present:
1425
+ \u2022 \`marketContext.execution.binanceCoinbaseSpread\`: AI-friendly BTC spread view projected from \`payload.additionalIndicators.baseContext.relative.execution.venueSpread\`; \`value=(Coinbase-Binance)/Binance\`, \`bps=value*10000\`.
1426
+ \u2022 \`marketContext.participation.trueDelta\`: Binance taker buy/sell volume delta from kline payload when \`source=kline_taker_volume\`; otherwise absent/unavailable.
1427
+ \u2022 \`marketContext.participation.tradeFlow\`: Binance aggTrades buy/sell pressure buckets when available.
1428
+ \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.
1429
+ \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.
1430
+ \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.
1431
+ \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\`.
1432
+ \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\`.
1433
+ \u2022 \`marketContext.relative.cmcExchangeLiquidity\`: historical CoinMarketCap major-exchange liquidity aggregate: total volume, 24h volume change, Binance share, concentration, and \`liquidityRegime\`.
1434
+ \u2022 \`marketContext.relative.cmcFearGreed\`: historical daily CoinMarketCap Fear & Greed sentiment index: value, classification, 24h/7d value changes, and \`sentimentRegime\`.
1435
+ \u2022 \`marketContext.relative.cmcIndexes\`: historical daily CoinMarketCap CMC100/CMC20 index values, 24h changes, top constituents, CMC20/CMC100 ratio, and \`indexRegime\`.
1436
+ \u2022 \`marketContext.relative.referenceTradeFlow\`: BTC/ETH reference trade-flow summary used for broad market pressure when the target symbol itself is not BTC/ETH.
1437
+ 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.
1438
+ 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.
1439
+ Key patterns:
1440
+ \u2022 current shared state: prefer \`payload.additionalIndicators.baseContext\`
1441
+ \u2022 recent historical series: \`payload.indicators\`
1442
+ \u2022 strategy service keys are possible as well, for example \`touches\`, \`distance\`, timing flags, and other setup-specific summaries
1443
+
1444
+ How to analyze, in order:
1445
+ 1. Start with price structure and the setup geometry or context in \`payload.figures\`. This has higher priority than indicators.
1446
+ 2. Then use \`payload.additionalIndicators.baseContext\` and other explicit strategy-specific context fields.
1447
+ 3. Then assess confirmation or conflict from the current shared state and recent coin indicator history.
1448
+ 4. Then evaluate BTC context.
1449
+ 5. Only after that choose \`direction\`, \`quality\`, and whether an extra confirmation level is required.
1450
+ 6. If strong conflicts exist, reduce quality or set direction to \`null\`.
1451
+
1452
+ Explicit conflict rules:
1453
+ - If the figure or price structure is invalid or doubtful, indicators must not rescue the setup.
1454
+ - If strategy-specific helper fields explicitly say the signal is not confirmed yet, lacks margin, or requires waiting, do not overstate quality.
1455
+ - If the structure is acceptable but BTC or key indicators noticeably conflict, quality is usually \`<= 3\`.
1456
+ - 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.
1457
+ - 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.
1458
+ - 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.
1459
+ - 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.
1460
+ - 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\`.
1461
+ - 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.
1462
+ - 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.
1463
+ - 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.
1464
+ - 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.
1465
+ - 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.
1466
+ - 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.
1467
+ - 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.
1468
+ - 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.
1469
+ - 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.
1470
+ - 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.
1471
+ - 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.
1472
+ - 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.
1473
+ - 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.
1474
+ - If the current signal is not confirmed (\`direction=null\`), name the main reason briefly in \`comment\`.
1475
+ If you use the structured fields, include the main reason in \`qualityReason\` or \`triggerInvalidation\`.
1476
+
1477
+ Rules for \`direction\` / TP / SL:
1478
+ - \`direction = LONG\` only if the data confirms the existing LONG signal; \`SHORT\` only if the data confirms the existing SHORT signal; otherwise \`null\`.
1479
+ - For LONG, the expected relation is usually \`stopLossPrice < currentPrice < takeProfitPrice\`.
1480
+ - For SHORT, the expected relation is usually \`takeProfitPrice < currentPrice < stopLossPrice\`.
1481
+ - Do not optimize or recalculate TP/SL for a "better trade"; only assess whether the already supplied levels are coherent.
1482
+ - If \`direction = null\`, then \`takeProfitPrice = null\` and \`stopLossPrice = null\`.
1483
+ - If \`needRetest = false\`, then \`retestPrice = null\`.
1484
+ - If \`needRetest = true\`, \`retestPrice\` must be a finite number tied to a meaningful retest or breakout level.
1485
+ - Before responding, sanity-check the consistency of \`direction\`, TP/SL, and the current price.
1486
+
1487
+ Quality scale:
1488
+ - 1: poor or chaotic setup, strong conflicts, signal not structurally confirmed
1489
+ - 2: weak setup, few confirmations, more of a watch or reject
1490
+ - 3: average setup, some structure exists, but notable conflicts remain
1491
+ - 4: good setup, several confirmations, structure is mostly coherent
1492
+ - 5: very strong setup, clean structure, confirmations, and internally coherent levels
1493
+
1494
+ Requirements for useful structured analysis:
1495
+ - Include 2-4 concrete factors for or against confirmation in \`confirmations\`.
1496
+ - Explicitly mention the role of the key figure or structural state, for example breakout, retest, false break, touch, or lack of confirmation.
1497
+ - Explicitly mention BTC context as supportive, neutral, or conflicting.
1498
+ - Explain why the quality score is what it is.
1499
+ - If the signal is not confirmed (\`direction=null\`), state clearly what must change for confirmation.
1500
+ - In \`retestPlan\`, avoid technical placeholders like \`needRetest=false @ null\`; write a human explanation.
1501
+ - Do not simply restate JSON fields; add interpretation and decision logic.
1502
+
1503
+ Rules for using trimmed series (last 5 values):
1504
+ - Do not make strong long-term conclusions from only 5 points.
1505
+ - Use 4h and 1d series as brief context, not full history.
1506
+ - If the data is too limited for confidence, reduce quality and use cautious wording.
1507
+
1508
+ Short few-shot examples:
1509
+ {"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."}
1510
+ {"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."}
1511
+
1512
+ Return only the JSON object, with no extra characters.
1513
+ ${signal ? buildAiSystemPromptAddonByStrategy(signal) : ""}
1514
+ `;
1515
+ var buildAiPayload = (signal) => buildAiPayloadByStrategy(signal);
1516
+ var getDeterministicAiGateContext = (payload) => {
1517
+ const additionalIndicators = asRecord(payload.additionalIndicators);
1518
+ const candidates = [
1519
+ additionalIndicators,
1520
+ ...Object.values(additionalIndicators ?? {}).map(asRecord)
1521
+ ].filter((value) => Boolean(value));
1522
+ return candidates.find(
1523
+ (candidate) => Array.isArray(candidate.approvalBlockReasons) || Array.isArray(candidate.riskAnnotations) || Array.isArray(candidate.structuralHardBlockReasons) || typeof candidate.approvalAllowedNow === "boolean"
1524
+ ) ?? null;
1414
1525
  };
1415
- var importConfigFile = async (configFilePath) => {
1416
- const ext = import_path.default.extname(configFilePath).toLowerCase();
1417
- const configFileUrl = `${toImportSpecifier(configFilePath)}?t=${Date.now()}`;
1418
- if (ext === ".ts" || ext === ".mts") {
1419
- const requireFn = getRequireFn(import_path.default.dirname(configFilePath));
1420
- await ensureTsNodeRegistered();
1421
- await ensureTsconfigPathsRegistered(import_path.default.dirname(configFilePath));
1422
- return requireFn(configFilePath);
1526
+ var buildAiHumanPrompt = (signal, payload = buildAiPayload(signal)) => `
1527
+ Analyze the already computed internal signal for ${signal.symbol}. The original signal direction is ${signal.direction}.
1528
+ 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.
1529
+
1530
+ Trade payload:
1531
+ ${JSON.stringify(payload)}
1532
+ ${buildAiHumanPromptAddonByStrategy(signal, payload)}
1533
+ `;
1534
+ var getAiInvocationError = (error) => {
1535
+ const details = error instanceof Error && error.message.trim() ? error.message.trim() : String(error);
1536
+ const isEmptyCompletion = error instanceof TypeError && /Cannot read properties of undefined \(reading ['"]message['"]\)/.test(
1537
+ details
1538
+ );
1539
+ const wrapped = new Error(
1540
+ isEmptyCompletion ? "AI provider returned an empty chat completion" : `AI model invocation failed: ${details}`
1541
+ );
1542
+ wrapped.cause = error;
1543
+ return wrapped;
1544
+ };
1545
+ var isEmptyResponseContent = (content) => typeof content === "string" ? content.trim().length === 0 : Object.keys(content).length === 0;
1546
+ var DEFAULT_AI_MODEL = "openai/gpt-5-mini";
1547
+ var userSettingsCache = /* @__PURE__ */ new Map();
1548
+ var aiModelCache = /* @__PURE__ */ new Map();
1549
+ var getAiModelCacheKey = (userName, modelName) => `${userName}::${modelName}`;
1550
+ var resolveAiModelName = (settings, requestedModelName) => {
1551
+ const explicitModelName = typeof requestedModelName === "string" ? requestedModelName.trim() : "";
1552
+ if (explicitModelName) {
1553
+ return explicitModelName;
1423
1554
  }
1424
- return import(
1425
- /* webpackIgnore: true */
1426
- configFileUrl
1427
- );
1555
+ const settingsModelName = typeof settings.AI_MODEL === "string" ? settings.AI_MODEL.trim() : "";
1556
+ return settingsModelName || DEFAULT_AI_MODEL;
1428
1557
  };
1429
- var importTradejsModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
1430
- const normalized = String(moduleName ?? "").trim();
1431
- if (!normalized) {
1558
+ var getOpenRouterModelKwargs = (apiEndpoint) => {
1559
+ const endpoint = String(apiEndpoint ?? "").trim();
1560
+ if (!endpoint) {
1432
1561
  return {};
1433
1562
  }
1434
- let modulePath = normalized;
1435
- if (normalized.startsWith("file://")) {
1436
- try {
1437
- modulePath = (0, import_url.fileURLToPath)(normalized);
1438
- } catch {
1439
- modulePath = normalized;
1440
- }
1563
+ let hostname = "";
1564
+ try {
1565
+ hostname = new URL(endpoint).hostname;
1566
+ } catch {
1567
+ hostname = endpoint;
1441
1568
  }
1442
- const requireFn = getRequireFn(
1443
- import_path.default.isAbsolute(modulePath) ? import_path.default.dirname(modulePath) : cwd
1444
- );
1445
- if (isTsModulePath(modulePath)) {
1446
- await ensureTsNodeRegistered();
1447
- await ensureTsconfigPathsRegistered(cwd);
1448
- return requireFn(modulePath);
1569
+ if (!hostname.toLowerCase().includes("openrouter")) {
1570
+ return {};
1449
1571
  }
1450
- if (isBareModuleSpecifier(normalized)) {
1451
- await ensureTsconfigPathsRegistered(cwd);
1452
- try {
1453
- return requireFn(normalized);
1454
- } catch (error) {
1455
- const resolvedByTsconfig = await resolveTsconfigPathModule(
1456
- normalized,
1457
- cwd
1458
- );
1459
- if (resolvedByTsconfig && resolvedByTsconfig !== normalized) {
1460
- return requireFn(resolvedByTsconfig);
1461
- }
1462
- throw error;
1572
+ return {
1573
+ provider: {
1574
+ ignore: ["azure"]
1463
1575
  }
1576
+ };
1577
+ };
1578
+ var getAiSettings = async (userName = "root") => {
1579
+ let settingsPromise = userSettingsCache.get(userName);
1580
+ if (!settingsPromise) {
1581
+ settingsPromise = (0, import_userSettings.getUserSettings)(userName).then((settings2) => {
1582
+ const endpoint = (0, import_aiEndpoints.normalizeAiEndpoint)(settings2.AI_API_ENDPOINT);
1583
+ return {
1584
+ ...settings2,
1585
+ AI_API_ENDPOINT: endpoint,
1586
+ AI_MODEL: (0, import_aiModels.normalizeAiModel)(settings2.AI_MODEL, endpoint),
1587
+ AI_RESPONSE_LANGUAGE: (0, import_aiLanguages.normalizeAiResponseLanguage)(
1588
+ settings2.AI_RESPONSE_LANGUAGE
1589
+ )
1590
+ };
1591
+ });
1592
+ settingsPromise.catch(() => {
1593
+ userSettingsCache.delete(userName);
1594
+ });
1595
+ userSettingsCache.set(userName, settingsPromise);
1464
1596
  }
1465
- try {
1466
- return await import(
1467
- /* webpackIgnore: true */
1468
- toImportSpecifier(normalized)
1469
- );
1470
- } catch (error) {
1471
- if (isTsModulePath(modulePath)) {
1472
- await ensureTsNodeRegistered();
1473
- await ensureTsconfigPathsRegistered(cwd);
1474
- return requireFn(modulePath);
1475
- }
1476
- throw error;
1597
+ const settings = await settingsPromise;
1598
+ if (!settings.AI_API_KEY || !settings.AI_API_ENDPOINT) {
1599
+ throw new Error(`AI settings are incomplete for user ${userName}`);
1477
1600
  }
1601
+ return settings;
1478
1602
  };
1479
- var resolveExportedConfig = (moduleExports) => {
1480
- const candidate = moduleExports && typeof moduleExports === "object" && "default" in moduleExports ? moduleExports.default : moduleExports;
1481
- return normalizeConfig(candidate);
1603
+ var createAiModel = async (userName = "root", requestedModelName) => {
1604
+ const settings = await getAiSettings(userName);
1605
+ const modelName = resolveAiModelName(settings, requestedModelName);
1606
+ const cacheKey = getAiModelCacheKey(userName, modelName);
1607
+ let modelPromise = aiModelCache.get(cacheKey);
1608
+ if (!modelPromise) {
1609
+ modelPromise = (async () => {
1610
+ const { ChatOpenAI } = await import("@langchain/openai");
1611
+ const modelKwargs = getOpenRouterModelKwargs(settings.AI_API_ENDPOINT);
1612
+ return new ChatOpenAI({
1613
+ temperature: 0.2,
1614
+ modelName,
1615
+ apiKey: settings.AI_API_KEY,
1616
+ ...Object.keys(modelKwargs).length ? { modelKwargs } : {},
1617
+ configuration: {
1618
+ baseURL: settings.AI_API_ENDPOINT,
1619
+ defaultHeaders: {
1620
+ "HTTP-Referer": "https://tradejs.dev",
1621
+ "X-Title": "Inv"
1622
+ }
1623
+ }
1624
+ });
1625
+ })();
1626
+ modelPromise.catch(() => {
1627
+ aiModelCache.delete(cacheKey);
1628
+ });
1629
+ aiModelCache.set(cacheKey, modelPromise);
1630
+ }
1631
+ return modelPromise;
1482
1632
  };
1483
- var findConfigFilePath = (cwd) => {
1484
- let currentDir = import_path.default.resolve(cwd);
1485
- while (true) {
1486
- for (const fileName of CONFIG_FILE_NAMES) {
1487
- const fullPath = import_path.default.join(currentDir, fileName);
1488
- if (import_fs.default.existsSync(fullPath) && import_fs.default.statSync(fullPath).isFile()) {
1489
- return fullPath;
1490
- }
1491
- }
1492
- const parentDir = import_path.default.dirname(currentDir);
1493
- if (parentDir === currentDir) {
1494
- return null;
1495
- }
1496
- currentDir = parentDir;
1633
+ var getAiModel = async (userName = "root", requestedModelName) => {
1634
+ const settings = await getAiSettings(userName);
1635
+ const resolvedModelName = resolveAiModelName(settings, requestedModelName);
1636
+ try {
1637
+ return await createAiModel(userName, resolvedModelName);
1638
+ } catch (error) {
1639
+ aiModelCache.delete(getAiModelCacheKey(userName, resolvedModelName));
1640
+ userSettingsCache.delete(userName);
1641
+ throw error;
1497
1642
  }
1498
1643
  };
1499
- var resolvePluginModuleSpecifier = (moduleName, cwd = getTradejsProjectCwd()) => {
1500
- const normalized = String(moduleName ?? "").trim();
1501
- if (!normalized) {
1502
- return "";
1644
+ var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
1645
+ const [{ HumanMessage, SystemMessage }, model, settings] = await Promise.all([
1646
+ import("@langchain/core/messages"),
1647
+ getAiModel(options.userName, options.model),
1648
+ getAiSettings(options.userName)
1649
+ ]);
1650
+ const messages = [];
1651
+ const responseLanguage = (0, import_aiLanguages.getAiResponseLanguagePromptName)(
1652
+ settings.AI_RESPONSE_LANGUAGE || import_aiLanguages.DEFAULT_AI_RESPONSE_LANGUAGE
1653
+ );
1654
+ messages.push(new SystemMessage(systemPrompt));
1655
+ messages.push(
1656
+ new SystemMessage(
1657
+ `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
1658
+ )
1659
+ );
1660
+ messages.push(
1661
+ new HumanMessage({
1662
+ content: [
1663
+ {
1664
+ type: "text",
1665
+ text: humanPrompt
1666
+ }
1667
+ ]
1668
+ })
1669
+ );
1670
+ let response;
1671
+ try {
1672
+ response = await model.invoke(messages);
1673
+ } catch (error) {
1674
+ throw getAiInvocationError(error);
1503
1675
  }
1504
- if (normalized.startsWith("file://")) {
1505
- try {
1506
- return (0, import_url.fileURLToPath)(normalized);
1507
- } catch {
1508
- return normalized;
1509
- }
1676
+ const responseContent = normalizeResponseContent(response?.content);
1677
+ if (isEmptyResponseContent(responseContent)) {
1678
+ throw new Error("AI provider returned an empty chat completion");
1510
1679
  }
1511
- if (import_path.default.isAbsolute(normalized)) {
1680
+ const parsed = parseAIResponse(responseContent);
1681
+ const normalized = normalizeAnalysis(parsed);
1682
+ if (!options.signal) {
1512
1683
  return normalized;
1513
1684
  }
1514
- if (isRelativeModulePath(normalized)) {
1515
- return import_path.default.resolve(cwd, normalized);
1516
- }
1517
- return normalized;
1685
+ return postProcessAiAnalysisByStrategy(
1686
+ options.signal,
1687
+ normalized,
1688
+ options.payload
1689
+ );
1690
+ };
1691
+ var runAiPromptLocal = async (signal, options = {}) => {
1692
+ const payload = options.payload ?? buildAiPayload(signal);
1693
+ const gateContext = getDeterministicAiGateContext(payload);
1694
+ const signalDirection = getSignalDirection(signal);
1695
+ const deterministicQuality = getDeterministicQuality(gateContext);
1696
+ const approvalAllowedNow = typeof gateContext?.approvalAllowedNow === "boolean" ? gateContext.approvalAllowedNow : deterministicQuality >= 4;
1697
+ return postProcessLocalAiAnalysisByStrategy(
1698
+ signal,
1699
+ {
1700
+ direction: approvalAllowedNow ? signalDirection : null,
1701
+ quality: deterministicQuality,
1702
+ needRetest: !approvalAllowedNow,
1703
+ retestPrice: null,
1704
+ takeProfitPrice: approvalAllowedNow ? signal.prices?.takeProfitPrice ?? null : null,
1705
+ stopLossPrice: approvalAllowedNow ? signal.prices?.stopLossPrice ?? null : null
1706
+ },
1707
+ payload
1708
+ );
1709
+ };
1710
+ var askAI = async (signal, options = {}) => {
1711
+ const { symbol } = signal;
1712
+ const payload = buildAiPayload(signal);
1713
+ const content = await runAiPrompt(
1714
+ {
1715
+ systemPrompt: buildAiSystemPrompt(signal),
1716
+ humanPrompt: buildAiHumanPrompt(signal, payload)
1717
+ },
1718
+ {
1719
+ ...options,
1720
+ signal,
1721
+ payload
1722
+ }
1723
+ );
1724
+ await (0, import_redis.setData)(import_redis.redisKeys.analysis(symbol, signal.signalId), content);
1725
+ return content;
1726
+ };
1727
+
1728
+ // src/strategyAdapters/ml.ts
1729
+ var defaultMlAdapter = {
1730
+ normalizeStrategyConfig: (strategyConfig) => strategyConfig
1731
+ };
1732
+ var getStrategyMlAdapter = (strategy, profileId) => {
1733
+ const strategyAdapter = getStrategyProfileMlAdapter(
1734
+ getStrategyManifest(strategy),
1735
+ profileId
1736
+ );
1737
+ if (!strategyAdapter) return defaultMlAdapter;
1738
+ return {
1739
+ ...defaultMlAdapter,
1740
+ ...strategyAdapter
1741
+ };
1742
+ };
1743
+
1744
+ // src/mlPayload.ts
1745
+ var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
1746
+ return getStrategyMlAdapter(
1747
+ strategyName,
1748
+ profileId
1749
+ ).normalizeStrategyConfig?.(strategyConfig);
1518
1750
  };
1519
- var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
1520
- const cached = cachedByCwd.get(cwd);
1521
- if (cached) {
1522
- return cached;
1523
- }
1524
- const configFilePath = findConfigFilePath(cwd);
1525
- if (!configFilePath) {
1526
- cachedByCwd.set(cwd, {});
1527
- return {};
1528
- }
1529
- try {
1530
- const moduleExports = await importConfigFile(configFilePath);
1531
- const config = resolveExportedConfig(moduleExports);
1532
- cachedByCwd.set(cwd, config);
1533
- if (!announcedConfigFile.has(configFilePath)) {
1534
- announcedConfigFile.add(configFilePath);
1535
- import_logger.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
1751
+ var buildMlPayload = (payload) => {
1752
+ const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
1753
+ const profileId = payload.signal?.policyProfileId;
1754
+ const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
1755
+ const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
1756
+ const nextSignal = {
1757
+ ...normalizedSignal,
1758
+ indicators: {
1759
+ ...normalizedSignal?.indicators ?? {}
1536
1760
  }
1537
- return config;
1538
- } catch (error) {
1539
- import_logger.logger.log(
1540
- "warn",
1541
- "Failed to load TradeJS config from %s: %s",
1542
- configFilePath,
1543
- String(error)
1544
- );
1545
- cachedByCwd.set(cwd, {});
1546
- return {};
1547
- }
1761
+ };
1762
+ const nextContext = payload.context ? {
1763
+ ...payload.context,
1764
+ strategyConfig: normalizeStrategyConfig(
1765
+ payload.context.strategyConfig,
1766
+ strategyName,
1767
+ profileId
1768
+ )
1769
+ } : void 0;
1770
+ return {
1771
+ signal: nextSignal,
1772
+ context: nextContext
1773
+ };
1548
1774
  };
1549
1775
 
1550
1776
  // src/runtimeJournal.ts
@@ -1552,7 +1778,7 @@ var import_node_crypto = require("crypto");
1552
1778
  var import_constants = require("@tradejs/core/constants");
1553
1779
  var import_time = require("@tradejs/core/time");
1554
1780
  var import_trade = require("@tradejs/core/trade");
1555
- var import_logger2 = require("@tradejs/infra/logger");
1781
+ var import_logger3 = require("@tradejs/infra/logger");
1556
1782
  var import_redis2 = require("@tradejs/infra/redis");
1557
1783
  var now = () => Date.now();
1558
1784
  var toRandomOrderSuffix = () => (0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 12).toLowerCase();
@@ -1611,7 +1837,7 @@ var recordRuntimeTradeOpen = async (params) => {
1611
1837
  )
1612
1838
  ]);
1613
1839
  } catch (error) {
1614
- import_logger2.logger.error(
1840
+ import_logger3.logger.error(
1615
1841
  "runtime trade open journal failed: %s %s",
1616
1842
  record.symbol,
1617
1843
  error?.message || String(error)
@@ -1678,7 +1904,7 @@ var recordRuntimeTradeIncrease = async (params) => {
1678
1904
  )
1679
1905
  ]);
1680
1906
  } catch (error) {
1681
- import_logger2.logger.error(
1907
+ import_logger3.logger.error(
1682
1908
  "runtime trade increase journal failed: %s %s",
1683
1909
  symbol,
1684
1910
  error?.message || String(error)
@@ -1785,7 +2011,7 @@ var markRuntimeTradeClosed = async (params) => {
1785
2011
  )
1786
2012
  ]);
1787
2013
  } catch (error) {
1788
- import_logger2.logger.error(
2014
+ import_logger3.logger.error(
1789
2015
  "runtime trade close journal failed: %s %s",
1790
2016
  symbol,
1791
2017
  error?.message || String(error)
@@ -1795,11 +2021,11 @@ var markRuntimeTradeClosed = async (params) => {
1795
2021
  };
1796
2022
 
1797
2023
  // src/strategyHelpers/marketContextStages.ts
1798
- var import_logger7 = require("@tradejs/infra/logger");
2024
+ var import_logger8 = require("@tradejs/infra/logger");
1799
2025
 
1800
2026
  // src/strategyHelpers/binanceMarketContext.ts
1801
2027
  var import_marketContext = require("@tradejs/infra/timescale/marketContext");
1802
- var import_logger3 = require("@tradejs/infra/logger");
2028
+ var import_logger4 = require("@tradejs/infra/logger");
1803
2029
  var import_strategies = require("@tradejs/core/strategies");
1804
2030
 
1805
2031
  // src/binanceBreadthUniverses.ts
@@ -2395,7 +2621,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
2395
2621
  throw error;
2396
2622
  }
2397
2623
  binanceMarketContextUnavailable = true;
2398
- import_logger3.logger.warn(
2624
+ import_logger4.logger.warn(
2399
2625
  "Binance market context disabled after Timescale read failure: %s",
2400
2626
  String(error)
2401
2627
  );
@@ -2405,7 +2631,7 @@ var enrichSignalWithBinanceMarketContext = async (params) => {
2405
2631
 
2406
2632
  // src/strategyHelpers/coinMarketCapContext.ts
2407
2633
  var import_strategies2 = require("@tradejs/core/strategies");
2408
- var import_logger4 = require("@tradejs/infra/logger");
2634
+ var import_logger5 = require("@tradejs/infra/logger");
2409
2635
  var import_marketContext2 = require("@tradejs/infra/timescale/marketContext");
2410
2636
  var DEFAULT_MAX_AGE_MS = 48 * 60 * 6e4;
2411
2637
  var SOURCE_GLOBAL_DAILY = "coinmarketcap_global";
@@ -2894,7 +3120,7 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
2894
3120
  throw error;
2895
3121
  }
2896
3122
  coinMarketCapContextUnavailable = true;
2897
- import_logger4.logger.warn(
3123
+ import_logger5.logger.warn(
2898
3124
  "CoinMarketCap context disabled after Timescale read failure: %s",
2899
3125
  String(error)
2900
3126
  );
@@ -2903,12 +3129,12 @@ var enrichSignalWithCoinMarketCapContext = async (params) => {
2903
3129
  };
2904
3130
 
2905
3131
  // src/strategyHelpers/derivativesContext.ts
2906
- var import_indicators = require("@tradejs/core/indicators");
3132
+ var import_indicators2 = require("@tradejs/core/indicators");
2907
3133
  var import_data = require("@tradejs/core/data");
2908
3134
  var import_strategies3 = require("@tradejs/core/strategies");
2909
3135
  var import_constants2 = require("@tradejs/core/constants");
2910
3136
  var import_derivatives = require("@tradejs/infra/timescale/derivatives");
2911
- var import_logger5 = require("@tradejs/infra/logger");
3137
+ var import_logger6 = require("@tradejs/infra/logger");
2912
3138
  var STORED_INTERVALS = ["15m", "1h"];
2913
3139
  var CONTEXT_INTERVALS = ["15m", "1h"];
2914
3140
  var DEFAULT_LOOKBACK_HOURS = 48;
@@ -2937,7 +3163,7 @@ var parseLookbackMs = () => {
2937
3163
  };
2938
3164
  var withHourlyFallbackRows = (rowsByInterval) => ({
2939
3165
  "15m": rowsByInterval["15m"] ?? [],
2940
- "1h": (0, import_indicators.buildCoinalyzeHourlyRowsWithFallback)({
3166
+ "1h": (0, import_indicators2.buildCoinalyzeHourlyRowsWithFallback)({
2941
3167
  rows15m: rowsByInterval["15m"],
2942
3168
  fallbackRows1h: rowsByInterval["1h"]
2943
3169
  })
@@ -3065,7 +3291,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
3065
3291
  const targetSymbol = normalizeSymbol(signal.symbol);
3066
3292
  const lookbackMs = parseLookbackMs();
3067
3293
  const decisionTimeMs = signal.timestamp + (0, import_data.intervalToMs)(signal.interval);
3068
- const derivativesEndMs = (0, import_indicators.getLastClosedDerivativesBarStartMs)(
3294
+ const derivativesEndMs = (0, import_indicators2.getLastClosedDerivativesBarStartMs)(
3069
3295
  decisionTimeMs,
3070
3296
  "15m"
3071
3297
  );
@@ -3080,7 +3306,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
3080
3306
  });
3081
3307
  return [
3082
3308
  symbol,
3083
- (0, import_indicators.buildDerivativesContext)({
3309
+ (0, import_indicators2.buildDerivativesContext)({
3084
3310
  symbol,
3085
3311
  direction: signal.direction,
3086
3312
  timestamp: derivativesEndMs,
@@ -3107,7 +3333,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
3107
3333
  lookbackMs,
3108
3334
  ...params.abortSignal ? { signal: params.abortSignal } : {}
3109
3335
  });
3110
- const context = (0, import_indicators.buildDerivativesContext)({
3336
+ const context = (0, import_indicators2.buildDerivativesContext)({
3111
3337
  symbol: targetSymbol,
3112
3338
  direction: signal.direction,
3113
3339
  timestamp: derivativesEndMs,
@@ -3140,7 +3366,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
3140
3366
  throw error;
3141
3367
  }
3142
3368
  derivativesContextUnavailable = true;
3143
- import_logger5.logger.warn(
3369
+ import_logger6.logger.warn(
3144
3370
  "Derivatives context disabled after Timescale read failure: %s",
3145
3371
  String(error)
3146
3372
  );
@@ -3152,7 +3378,7 @@ var enrichSignalWithDerivativesContext = async (params) => {
3152
3378
  var import_data2 = require("@tradejs/core/data");
3153
3379
  var import_strategies4 = require("@tradejs/core/strategies");
3154
3380
  var import_hyperliquidWhales2 = require("@tradejs/infra/timescale/hyperliquidWhales");
3155
- var import_logger6 = require("@tradejs/infra/logger");
3381
+ var import_logger7 = require("@tradejs/infra/logger");
3156
3382
 
3157
3383
  // src/hyperliquidWhaleUniverse.ts
3158
3384
  var import_node_crypto3 = require("crypto");
@@ -3672,7 +3898,7 @@ var loadHyperliquidWhaleFlowContext = async (params) => {
3672
3898
  throw error;
3673
3899
  }
3674
3900
  hyperliquidWhaleContextUnavailable = true;
3675
- import_logger6.logger.warn(
3901
+ import_logger7.logger.warn(
3676
3902
  "Hyperliquid whale context disabled after Timescale read failure: %s",
3677
3903
  String(error)
3678
3904
  );
@@ -3755,7 +3981,7 @@ var runMarketContextStage = async ({
3755
3981
  elapsedMs: Date.now() - startedAt
3756
3982
  };
3757
3983
  if (status === "timed_out") {
3758
- import_logger7.logger.warn(
3984
+ import_logger8.logger.warn(
3759
3985
  "Market context stage timed out: %s after %sms",
3760
3986
  stage,
3761
3987
  result.elapsedMs
@@ -3945,7 +4171,7 @@ var enrichSignalWithAi = async ({
3945
4171
  signal.aiAnalysis = analysis;
3946
4172
  return resolveAiQuality(analysis, direction);
3947
4173
  } catch (err) {
3948
- import_logger8.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
4174
+ import_logger9.logger.error("AI analysis error: %s %s", symbol, formatAiError(err));
3949
4175
  }
3950
4176
  return void 0;
3951
4177
  };
@@ -3980,7 +4206,7 @@ var getOrderArrivalSnapshot = async ({
3980
4206
  spreadBps
3981
4207
  };
3982
4208
  } catch (error) {
3983
- import_logger8.logger.warn(
4209
+ import_logger9.logger.warn(
3984
4210
  "runtime order arrival snapshot failed: %s %s",
3985
4211
  symbol,
3986
4212
  error?.message || String(error)
@@ -4773,7 +4999,7 @@ var canUseSharedReplayState = ({
4773
4999
  }) => (env === "BACKTEST" || env === "PARITY") && Boolean(sharedReplayKey);
4774
5000
 
4775
5001
  // src/strategy/runtimeExecution.ts
4776
- var import_logger9 = require("@tradejs/infra/logger");
5002
+ var import_logger10 = require("@tradejs/infra/logger");
4777
5003
  var import_types = require("@tradejs/types");
4778
5004
  var buildExitOrderSignal = ({
4779
5005
  strategyName,
@@ -4825,7 +5051,7 @@ var handleExitDecision = async ({
4825
5051
  deploymentId: connector.deploymentId
4826
5052
  });
4827
5053
  if (!activeTrade) {
4828
- import_logger9.logger.warn(
5054
+ import_logger10.logger.warn(
4829
5055
  "[%s] blocked closePosition for untracked runtime position: %s",
4830
5056
  strategyName ?? "unknown",
4831
5057
  symbol
@@ -4833,7 +5059,7 @@ var handleExitDecision = async ({
4833
5059
  return "CLOSE_BLOCKED_BY_UNTRACKED_POSITION";
4834
5060
  }
4835
5061
  if (!strategyName || activeTrade.strategy !== strategyName) {
4836
- import_logger9.logger.warn(
5062
+ import_logger10.logger.warn(
4837
5063
  "[%s] blocked closePosition for foreign runtime position: %s ownedBy=%s",
4838
5064
  strategyName ?? "unknown",
4839
5065
  symbol,
@@ -4885,7 +5111,7 @@ var handleExitDecision = async ({
4885
5111
  exitType: closedTrade?.exitType ?? "exit"
4886
5112
  });
4887
5113
  } catch (notificationError) {
4888
- import_logger9.logger.error(
5114
+ import_logger10.logger.error(
4889
5115
  "runtime close notification error: %s %s",
4890
5116
  symbol,
4891
5117
  notificationError
@@ -4899,7 +5125,7 @@ var handleExitDecision = async ({
4899
5125
  decision,
4900
5126
  market
4901
5127
  });
4902
- import_logger9.logger.error("close order error: %s %s", symbol, err);
5128
+ import_logger10.logger.error("close order error: %s %s", symbol, err);
4903
5129
  return "ORDER_ERROR";
4904
5130
  }
4905
5131
  return decision.code;
@@ -4926,7 +5152,7 @@ var handleProtectDecision = async ({
4926
5152
  decision,
4927
5153
  market
4928
5154
  });
4929
- import_logger9.logger.error("protect position error: %s %s", symbol, err);
5155
+ import_logger10.logger.error("protect position error: %s %s", symbol, err);
4930
5156
  return "ORDER_ERROR";
4931
5157
  }
4932
5158
  return decision.code;
@@ -5089,9 +5315,9 @@ var executeEntryDecision = async ({
5089
5315
  market
5090
5316
  });
5091
5317
  if (err?.message === import_types.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY) {
5092
- import_logger9.logger.warn("order warning: %s %s", symbol, err);
5318
+ import_logger10.logger.warn("order warning: %s %s", symbol, err);
5093
5319
  } else {
5094
- import_logger9.logger.error("order error: %s %s", symbol, err);
5320
+ import_logger10.logger.error("order error: %s %s", symbol, err);
5095
5321
  }
5096
5322
  return signal ?? "ORDER_ERROR";
5097
5323
  }
@@ -5243,7 +5469,7 @@ var createStrategyRuntime = ({
5243
5469
  try {
5244
5470
  await projectHook(errorParams);
5245
5471
  } catch (hookError) {
5246
- import_logger10.logger.error(
5472
+ import_logger11.logger.error(
5247
5473
  "project hook onRuntimeError failed: %s %s",
5248
5474
  strategyName,
5249
5475
  hookError
@@ -5257,7 +5483,7 @@ var createStrategyRuntime = ({
5257
5483
  try {
5258
5484
  await onRuntimeError(errorParams);
5259
5485
  } catch (hookError) {
5260
- import_logger10.logger.error(
5486
+ import_logger11.logger.error(
5261
5487
  "runtime hook onRuntimeError failed: %s %s",
5262
5488
  strategyName,
5263
5489
  hookError
@@ -5271,7 +5497,7 @@ var createStrategyRuntime = ({
5271
5497
  try {
5272
5498
  return await hook(params);
5273
5499
  } catch (error) {
5274
- import_logger10.logger.error(
5500
+ import_logger11.logger.error(
5275
5501
  'strategy hook "%s" failed for %s: %s',
5276
5502
  stage,
5277
5503
  strategyName,
@@ -6011,204 +6237,8 @@ var createStrategyRuntime = ({
6011
6237
  return creator;
6012
6238
  };
6013
6239
 
6014
- // src/strategy/manifests.ts
6015
- var createStrategyRegistryState = () => ({
6016
- strategyCreators: /* @__PURE__ */ new Map(),
6017
- strategyManifestsMap: /* @__PURE__ */ new Map(),
6018
- pluginsLoadPromise: null
6019
- });
6020
- var registryStateByProjectRoot = /* @__PURE__ */ new Map();
6021
- var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
6022
- const projectRoot = getTradejsProjectCwd(cwd);
6023
- let state = registryStateByProjectRoot.get(projectRoot);
6024
- if (!state) {
6025
- state = createStrategyRegistryState();
6026
- registryStateByProjectRoot.set(projectRoot, state);
6027
- }
6028
- return {
6029
- projectRoot,
6030
- state
6031
- };
6032
- };
6033
- var toUniqueModules = (modules = []) => [
6034
- ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
6035
- ];
6036
- var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
6037
- const config = await loadTradejsConfig(cwd);
6038
- return {
6039
- strategyModules: toUniqueModules(config.strategies),
6040
- indicatorModules: toUniqueModules(config.indicators)
6041
- };
6042
- };
6043
- var extractModuleEntries = (moduleExport, key) => {
6044
- if (!moduleExport || typeof moduleExport !== "object") {
6045
- return null;
6046
- }
6047
- const candidate = moduleExport;
6048
- if (Array.isArray(candidate[key])) {
6049
- return candidate[key];
6050
- }
6051
- const defaultExport = candidate.default;
6052
- if (defaultExport && Array.isArray(defaultExport[key])) {
6053
- return defaultExport[key];
6054
- }
6055
- return null;
6056
- };
6057
- var extractStrategyPluginDefinition = (moduleExport) => {
6058
- const strategyEntries = extractModuleEntries(
6059
- moduleExport,
6060
- "strategyEntries"
6061
- );
6062
- return strategyEntries ? { strategyEntries } : null;
6063
- };
6064
- var extractIndicatorPluginDefinition = (moduleExport) => {
6065
- const indicatorEntries = extractModuleEntries(
6066
- moduleExport,
6067
- "indicatorEntries"
6068
- );
6069
- return indicatorEntries ? { indicatorEntries } : null;
6070
- };
6071
- var registerEntries = (entries, source, state) => {
6072
- for (const entry of entries) {
6073
- const strategyName = entry.manifest?.name;
6074
- if (!strategyName) {
6075
- import_logger11.logger.warn("Skip strategy entry without name from %s", source);
6076
- continue;
6077
- }
6078
- if (state.strategyCreators.has(strategyName)) {
6079
- import_logger11.logger.warn(
6080
- 'Skip duplicate strategy "%s" from %s: already registered',
6081
- strategyName,
6082
- source
6083
- );
6084
- continue;
6085
- }
6086
- state.strategyManifestsMap.set(strategyName, entry.manifest);
6087
- state.strategyCreators.set(
6088
- strategyName,
6089
- createStrategyRuntime({
6090
- strategyName,
6091
- defaults: entry.defaults,
6092
- createCore: entry.createCore,
6093
- manifest: entry.manifest,
6094
- detectorKey: entry.detectorKey,
6095
- detectorNoSignalSkipReason: entry.detectorNoSignalSkipReason,
6096
- resolveRegisteredManifest: (name) => state.strategyManifestsMap.get(name)
6097
- })
6098
- );
6099
- }
6100
- };
6101
- var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
6102
- if (typeof importTradejsModule === "function") {
6103
- return importTradejsModule(moduleName, cwd);
6104
- }
6105
- return import(
6106
- /* webpackIgnore: true */
6107
- moduleName
6108
- );
6109
- };
6110
- var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
6111
- const { projectRoot, state } = getStrategyRegistryState(cwd);
6112
- if (!state.pluginsLoadPromise) {
6113
- (0, import_indicators2.resetIndicatorRegistryCache)(projectRoot);
6114
- state.pluginsLoadPromise = (async () => {
6115
- const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
6116
- const strategySet = new Set(strategyModules);
6117
- const indicatorSet = new Set(indicatorModules);
6118
- const pluginModuleNames = [
6119
- .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
6120
- ];
6121
- if (!pluginModuleNames.length) {
6122
- return;
6123
- }
6124
- for (const moduleName of pluginModuleNames) {
6125
- try {
6126
- const resolvedModuleName = resolvePluginModuleSpecifier(
6127
- moduleName,
6128
- projectRoot
6129
- );
6130
- const moduleExport = await importStrategyPluginModule(
6131
- resolvedModuleName,
6132
- projectRoot
6133
- );
6134
- if (strategySet.has(moduleName)) {
6135
- const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
6136
- if (!pluginDefinition) {
6137
- import_logger11.logger.warn(
6138
- 'Skip strategy plugin "%s": export { strategyEntries } is missing',
6139
- moduleName
6140
- );
6141
- } else {
6142
- registerEntries(
6143
- pluginDefinition.strategyEntries,
6144
- moduleName,
6145
- state
6146
- );
6147
- }
6148
- }
6149
- if (indicatorSet.has(moduleName)) {
6150
- const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
6151
- if (!indicatorPluginDefinition) {
6152
- import_logger11.logger.warn(
6153
- 'Skip indicator plugin "%s": export { indicatorEntries } is missing',
6154
- moduleName
6155
- );
6156
- } else {
6157
- (0, import_indicators2.registerIndicatorEntries)(
6158
- indicatorPluginDefinition.indicatorEntries,
6159
- moduleName,
6160
- projectRoot
6161
- );
6162
- }
6163
- }
6164
- if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
6165
- import_logger11.logger.warn(
6166
- 'Skip plugin "%s": no strategy/indicator sections requested in config',
6167
- moduleName
6168
- );
6169
- }
6170
- } catch (error) {
6171
- import_logger11.logger.warn(
6172
- 'Failed to load plugin "%s": %s',
6173
- moduleName,
6174
- String(error)
6175
- );
6176
- }
6177
- }
6178
- })();
6179
- }
6180
- await state.pluginsLoadPromise;
6181
- };
6182
- var getStrategyCreator = async (name, cwd = getTradejsProjectCwd()) => {
6183
- await ensureStrategyPluginsLoaded(cwd);
6184
- const { state } = getStrategyRegistryState(cwd);
6185
- return state.strategyCreators.get(name);
6186
- };
6187
- var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
6188
- if (!name) {
6189
- return void 0;
6190
- }
6191
- const { state } = getStrategyRegistryState(cwd);
6192
- return state.strategyManifestsMap.get(name);
6193
- };
6194
- var strategies = new Proxy(
6195
- {},
6196
- {
6197
- get: (_target, property) => {
6198
- if (typeof property !== "string") {
6199
- return void 0;
6200
- }
6201
- return getStrategyRegistryState().state.strategyCreators.get(property);
6202
- },
6203
- ownKeys: () => {
6204
- return [...getStrategyRegistryState().state.strategyCreators.keys()];
6205
- },
6206
- getOwnPropertyDescriptor: () => ({
6207
- enumerable: true,
6208
- configurable: true
6209
- })
6210
- }
6211
- );
6240
+ // src/strategy/index.ts
6241
+ setStrategyRuntimeFactory(createStrategyRuntime);
6212
6242
 
6213
6243
  // src/connectorsRegistry.ts
6214
6244
  var import_logger12 = require("@tradejs/infra/logger");
@@ -6695,6 +6725,7 @@ var createTestConnector = (connector, context) => {
6695
6725
  };
6696
6726
  const createOpenTradeResult = ({
6697
6727
  signalId,
6728
+ positionCycleId,
6698
6729
  direction,
6699
6730
  qty,
6700
6731
  timestamp,
@@ -6705,6 +6736,7 @@ var createTestConnector = (connector, context) => {
6705
6736
  entrySlippageCost
6706
6737
  }) => ({
6707
6738
  signalId,
6739
+ positionCycleId,
6708
6740
  direction,
6709
6741
  qty,
6710
6742
  closedQty: 0,
@@ -7411,6 +7443,7 @@ var createTestConnector = (connector, context) => {
7411
7443
  currentEntryLegResults.push(
7412
7444
  createOpenTradeResult({
7413
7445
  signalId: increaseSignalId,
7446
+ positionCycleId: currentSignalId ?? increaseSignalId,
7414
7447
  direction: order.direction,
7415
7448
  qty: orderQty,
7416
7449
  timestamp: order.timestamp,
@@ -7516,6 +7549,7 @@ var createTestConnector = (connector, context) => {
7516
7549
  currentPositionProfit = profit;
7517
7550
  const openTradeResult = createOpenTradeResult({
7518
7551
  signalId: currentSignalId ?? "",
7552
+ positionCycleId: currentSignalId ?? "",
7519
7553
  direction: order.direction,
7520
7554
  qty: orderQty,
7521
7555
  timestamp: order.timestamp,