@tradejs/node 1.0.5 → 1.0.8

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/cli.js CHANGED
@@ -36,9 +36,9 @@ var require_lodash = __commonJS({
36
36
  "use strict";
37
37
  (function() {
38
38
  var undefined2;
39
- var VERSION = "4.17.21";
39
+ var VERSION = "4.18.1";
40
40
  var LARGE_ARRAY_SIZE = 200;
41
- var CORE_ERROR_TEXT = "Unsupported core-js use. Try https://npms.io/search?q=ponyfill.", FUNC_ERROR_TEXT = "Expected a function", INVALID_TEMPL_VAR_ERROR_TEXT = "Invalid `variable` option passed into `_.template`";
41
+ var CORE_ERROR_TEXT = "Unsupported core-js use. Try https://npms.io/search?q=ponyfill.", FUNC_ERROR_TEXT = "Expected a function", INVALID_TEMPL_VAR_ERROR_TEXT = "Invalid `variable` option passed into `_.template`", INVALID_TEMPL_IMPORTS_ERROR_TEXT = "Invalid `imports` option passed into `_.template`";
42
42
  var HASH_UNDEFINED = "__lodash_hash_undefined__";
43
43
  var MAX_MEMOIZE_SIZE = 500;
44
44
  var PLACEHOLDER = "__lodash_placeholder__";
@@ -1964,8 +1964,21 @@ var require_lodash = __commonJS({
1964
1964
  }
1965
1965
  function baseUnset(object, path3) {
1966
1966
  path3 = castPath(path3, object);
1967
- object = parent(object, path3);
1968
- return object == null || delete object[toKey(last(path3))];
1967
+ var index = -1, length = path3.length;
1968
+ if (!length) {
1969
+ return true;
1970
+ }
1971
+ while (++index < length) {
1972
+ var key = toKey(path3[index]);
1973
+ if (key === "__proto__" && !hasOwnProperty.call(object, "__proto__")) {
1974
+ return false;
1975
+ }
1976
+ if ((key === "constructor" || key === "prototype") && index < length - 1) {
1977
+ return false;
1978
+ }
1979
+ }
1980
+ var obj = parent(object, path3);
1981
+ return obj == null || delete obj[toKey(last(path3))];
1969
1982
  }
1970
1983
  function baseUpdate(object, path3, updater, customizer) {
1971
1984
  return baseSet(object, path3, updater(baseGet(object, path3)), customizer);
@@ -2309,7 +2322,7 @@ var require_lodash = __commonJS({
2309
2322
  index = wrapper ? index : length;
2310
2323
  while (++index < length) {
2311
2324
  func = funcs[index];
2312
- var funcName = getFuncName(func), data = funcName == "wrapper" ? getData3(func) : undefined2;
2325
+ var funcName = getFuncName(func), data = funcName == "wrapper" ? getData2(func) : undefined2;
2313
2326
  if (data && isLaziable(data[0]) && data[1] == (WRAP_ARY_FLAG | WRAP_CURRY_FLAG | WRAP_PARTIAL_FLAG | WRAP_REARG_FLAG) && !data[4].length && data[9] == 1) {
2314
2327
  wrapper = wrapper[getFuncName(data[0])].apply(wrapper, data[3]);
2315
2328
  } else {
@@ -2538,7 +2551,7 @@ var require_lodash = __commonJS({
2538
2551
  var partialsRight = partials, holdersRight = holders;
2539
2552
  partials = holders = undefined2;
2540
2553
  }
2541
- var data = isBindKey ? undefined2 : getData3(func);
2554
+ var data = isBindKey ? undefined2 : getData2(func);
2542
2555
  var newData = [
2543
2556
  func,
2544
2557
  bitmask,
@@ -2733,7 +2746,7 @@ var require_lodash = __commonJS({
2733
2746
  function getAllKeysIn(object) {
2734
2747
  return baseGetAllKeys(object, keysIn, getSymbolsIn);
2735
2748
  }
2736
- var getData3 = !metaMap ? noop : function(func) {
2749
+ var getData2 = !metaMap ? noop : function(func) {
2737
2750
  return metaMap.get(func);
2738
2751
  };
2739
2752
  function getFuncName(func) {
@@ -2961,7 +2974,7 @@ var require_lodash = __commonJS({
2961
2974
  if (func === other) {
2962
2975
  return true;
2963
2976
  }
2964
- var data = getData3(other);
2977
+ var data = getData2(other);
2965
2978
  return !!data && func === data[0];
2966
2979
  }
2967
2980
  function isMasked(func) {
@@ -3292,7 +3305,7 @@ var require_lodash = __commonJS({
3292
3305
  var index = -1, length = pairs == null ? 0 : pairs.length, result2 = {};
3293
3306
  while (++index < length) {
3294
3307
  var pair = pairs[index];
3295
- result2[pair[0]] = pair[1];
3308
+ baseAssignValue(result2, pair[0], pair[1]);
3296
3309
  }
3297
3310
  return result2;
3298
3311
  }
@@ -4676,8 +4689,13 @@ var require_lodash = __commonJS({
4676
4689
  options = undefined2;
4677
4690
  }
4678
4691
  string = toString(string);
4679
- options = assignInWith({}, options, settings, customDefaultsAssignIn);
4680
- var imports = assignInWith({}, options.imports, settings.imports, customDefaultsAssignIn), importsKeys = keys(imports), importsValues = baseValues(imports, importsKeys);
4692
+ options = assignWith({}, options, settings, customDefaultsAssignIn);
4693
+ var imports = assignWith({}, options.imports, settings.imports, customDefaultsAssignIn), importsKeys = keys(imports), importsValues = baseValues(imports, importsKeys);
4694
+ arrayEach(importsKeys, function(key) {
4695
+ if (reForbiddenIdentifierChars.test(key)) {
4696
+ throw new Error2(INVALID_TEMPL_IMPORTS_ERROR_TEXT);
4697
+ }
4698
+ });
4681
4699
  var isEscaping, isEvaluating, index = 0, interpolate = options.interpolate || reNoMatch, source = "__p += '";
4682
4700
  var reDelimiters = RegExp2(
4683
4701
  (options.escape || reNoMatch).source + "|" + interpolate.source + "|" + (interpolate === reInterpolate ? reEsTemplate : reNoMatch).source + "|" + (options.evaluate || reNoMatch).source + "|$",
@@ -5518,7 +5536,9 @@ __export(cli_exports, {
5518
5536
  cleanRedis: () => cleanRedis,
5519
5537
  drawStatInCLI: () => drawStatInCLI,
5520
5538
  getTickers: () => getTickers,
5539
+ loadTradejsConfig: () => loadTradejsConfig,
5521
5540
  makeScreenshots: () => makeScreenshots,
5541
+ sendTextToTG: () => sendTextToTG,
5522
5542
  sendToAI: () => sendToAI,
5523
5543
  sendToTG: () => sendToTG,
5524
5544
  update: () => update
@@ -5541,12 +5561,11 @@ var getPositiveIntegerEnv = (name, fallback) => {
5541
5561
  const parsedValue = Number(rawValue);
5542
5562
  return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : fallback;
5543
5563
  };
5544
- var DEFAULT_KLINE_CONCURRENCY_LIMIT = NODE_ENV === "production" ? 1 : 10;
5564
+ var DEFAULT_KLINE_CONCURRENCY_LIMIT = NODE_ENV === "production" ? 5 : 10;
5545
5565
  var KLINE_CONCURRENCY_LIMIT = getPositiveIntegerEnv(
5546
5566
  "KLINE_CONCURRENCY_LIMIT",
5547
5567
  DEFAULT_KLINE_CONCURRENCY_LIMIT
5548
5568
  );
5549
- var TG_CONCURRENCY_LIMIT = 3;
5550
5569
  var AI_CONCURRENCY_LIMIT = 3;
5551
5570
  var SCREENSHOT_CONCURRENCY_LIMIT = NODE_ENV === "production" ? 1 : 2;
5552
5571
 
@@ -5556,7 +5575,9 @@ var import_redis3 = require("@tradejs/infra/redis");
5556
5575
  var import_logger5 = require("@tradejs/infra/logger");
5557
5576
 
5558
5577
  // src/ai.ts
5578
+ var import_aiLanguages = require("@tradejs/infra/aiLanguages");
5559
5579
  var import_redis = require("@tradejs/infra/redis");
5580
+ var import_userSettings = require("@tradejs/infra/userSettings");
5560
5581
 
5561
5582
  // src/aiShared.ts
5562
5583
  var MAX_AI_SERIES_POINTS = 5;
@@ -5582,13 +5603,132 @@ var trimSeriesDeep = (value) => {
5582
5603
  return value;
5583
5604
  };
5584
5605
 
5606
+ // src/aiMarketContext.ts
5607
+ var SESSION_WINDOWS = [
5608
+ { name: "asia", startMinuteUtc: 0, endMinuteUtc: 8 * 60 },
5609
+ { name: "europe", startMinuteUtc: 7 * 60, endMinuteUtc: 16 * 60 },
5610
+ { name: "us", startMinuteUtc: 13 * 60, endMinuteUtc: 22 * 60 }
5611
+ ];
5612
+ var toRecord = (value) => {
5613
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5614
+ return null;
5615
+ }
5616
+ return value;
5617
+ };
5618
+ var toFiniteNumber = (value) => {
5619
+ const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
5620
+ return Number.isFinite(numeric) ? numeric : null;
5621
+ };
5622
+ var getLastFiniteNumber = (value) => {
5623
+ const numeric = toFiniteNumber(value);
5624
+ if (numeric != null) {
5625
+ return numeric;
5626
+ }
5627
+ if (!Array.isArray(value)) {
5628
+ return null;
5629
+ }
5630
+ for (let i = value.length - 1; i >= 0; i -= 1) {
5631
+ const nested = getLastFiniteNumber(value[i]);
5632
+ if (nested != null) {
5633
+ return nested;
5634
+ }
5635
+ }
5636
+ return null;
5637
+ };
5638
+ var roundTo = (value, decimals) => {
5639
+ const factor = 10 ** decimals;
5640
+ return Math.round(value * factor) / factor;
5641
+ };
5642
+ var isInsideSession = (minuteUtc, startMinuteUtc, endMinuteUtc) => startMinuteUtc <= endMinuteUtc ? minuteUtc >= startMinuteUtc && minuteUtc < endMinuteUtc : minuteUtc >= startMinuteUtc || minuteUtc < endMinuteUtc;
5643
+ var buildTradingSessionContext = (timestamp) => {
5644
+ const date = new Date(timestamp);
5645
+ const utcHour = date.getUTCHours();
5646
+ const utcMinute = date.getUTCMinutes();
5647
+ const minuteUtc = utcHour * 60 + utcMinute;
5648
+ const activeSessions = SESSION_WINDOWS.filter(
5649
+ (session) => isInsideSession(minuteUtc, session.startMinuteUtc, session.endMinuteUtc)
5650
+ ).map((session) => session.name);
5651
+ const primarySession = activeSessions.includes("us") ? "us" : activeSessions.includes("europe") ? "europe" : activeSessions.includes("asia") ? "asia" : "off_hours";
5652
+ return {
5653
+ timezone: "UTC",
5654
+ utcHour,
5655
+ utcMinute,
5656
+ primarySession,
5657
+ activeSessions,
5658
+ isOverlap: activeSessions.length > 1,
5659
+ overlap: activeSessions.length > 1 ? `${activeSessions.join("_")}_overlap` : null
5660
+ };
5661
+ };
5662
+ var buildMissingSpreadContext = () => ({
5663
+ source: "binance_coinbase_btc",
5664
+ indicatorKey: "payload.indicators.spread",
5665
+ available: false,
5666
+ value: null,
5667
+ bps: null,
5668
+ absBps: null,
5669
+ bias: null,
5670
+ severity: null
5671
+ });
5672
+ var buildSpreadContextFromValue = (spread) => {
5673
+ const value = roundTo(spread, 8);
5674
+ const bps = roundTo(value * 1e4, 2);
5675
+ const absBps = Math.abs(bps);
5676
+ const bias = Math.abs(bps) < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
5677
+ const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
5678
+ return {
5679
+ source: "binance_coinbase_btc",
5680
+ indicatorKey: "payload.indicators.spread",
5681
+ available: true,
5682
+ value,
5683
+ bps,
5684
+ absBps,
5685
+ bias,
5686
+ severity
5687
+ };
5688
+ };
5689
+ var readSpreadFromSignal = (signal) => {
5690
+ const indicatorSpread = getLastFiniteNumber(signal.indicators?.spread);
5691
+ if (indicatorSpread != null) {
5692
+ return indicatorSpread;
5693
+ }
5694
+ return getLastFiniteNumber(signal.additionalIndicators?.spread);
5695
+ };
5696
+ var buildAiMarketContext = (signal) => {
5697
+ const existingMarketContext = toRecord(
5698
+ signal.additionalIndicators?.marketContext
5699
+ );
5700
+ const existingSpread = toRecord(existingMarketContext?.binanceCoinbaseSpread);
5701
+ const spread = readSpreadFromSignal(signal);
5702
+ return {
5703
+ ...existingMarketContext ?? {},
5704
+ tradingSession: buildTradingSessionContext(signal.timestamp),
5705
+ binanceCoinbaseSpread: spread != null ? buildSpreadContextFromValue(spread) : existingSpread ?? buildMissingSpreadContext()
5706
+ };
5707
+ };
5708
+
5585
5709
  // src/strategy/manifests.ts
5586
5710
  var import_indicators = require("@tradejs/core/indicators");
5587
5711
  var import_logger2 = require("@tradejs/infra/logger");
5588
5712
 
5589
5713
  // src/tradejsConfig.ts
5714
+ var import_fs = __toESM(require("fs"));
5590
5715
  var import_path = __toESM(require("path"));
5716
+ var import_module = require("module");
5717
+ var import_url = require("url");
5718
+ var import_config = require("@tradejs/core/config");
5591
5719
  var import_logger = require("@tradejs/infra/logger");
5720
+ var CONFIG_FILE_NAMES = [
5721
+ "tradejs.config.ts",
5722
+ "tradejs.config.mts",
5723
+ "tradejs.config.js",
5724
+ "tradejs.config.mjs",
5725
+ "tradejs.config.cjs"
5726
+ ];
5727
+ var TS_MODULE_RE = /\.(cts|mts|ts)$/i;
5728
+ var cachedByCwd = /* @__PURE__ */ new Map();
5729
+ var announcedConfigFile = /* @__PURE__ */ new Set();
5730
+ var tsNodeRegistered = false;
5731
+ var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
5592
5732
  var getTradejsProjectCwd = (cwd) => {
5593
5733
  const explicit = String(cwd ?? "").trim();
5594
5734
  if (explicit) {
@@ -5600,6 +5740,206 @@ var getTradejsProjectCwd = (cwd) => {
5600
5740
  }
5601
5741
  return process.cwd();
5602
5742
  };
5743
+ var normalizeConfig = (rawConfig) => {
5744
+ if (!rawConfig || typeof rawConfig !== "object") {
5745
+ return {};
5746
+ }
5747
+ const config = rawConfig;
5748
+ const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
5749
+ const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
5750
+ const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
5751
+ const hooks = (0, import_config.normalizeTradejsConfigHooks)(
5752
+ config.hooks
5753
+ );
5754
+ return {
5755
+ strategies: strategies2,
5756
+ indicators,
5757
+ connectors,
5758
+ ...hooks ? { hooks } : {}
5759
+ };
5760
+ };
5761
+ var getRequireFn = (cwd = getTradejsProjectCwd()) => (0, import_module.createRequire)(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
5762
+ var ensureTsNodeRegistered = async () => {
5763
+ if (tsNodeRegistered) {
5764
+ return;
5765
+ }
5766
+ const tsNodeModule = await import("ts-node");
5767
+ const tsNode = tsNodeModule.default ?? tsNodeModule;
5768
+ tsNode.register?.({
5769
+ transpileOnly: true,
5770
+ compilerOptions: {
5771
+ module: "commonjs",
5772
+ moduleResolution: "node"
5773
+ }
5774
+ });
5775
+ tsNodeRegistered = true;
5776
+ };
5777
+ var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
5778
+ const projectRoot = getTradejsProjectCwd(cwd);
5779
+ if (tsconfigPathsRegisteredByCwd.has(projectRoot)) {
5780
+ return;
5781
+ }
5782
+ const tsconfigPathsModule = await import("tsconfig-paths");
5783
+ const loadConfig = tsconfigPathsModule.loadConfig;
5784
+ const register = tsconfigPathsModule.register;
5785
+ if (typeof loadConfig !== "function" || typeof register !== "function") {
5786
+ return;
5787
+ }
5788
+ const loadedConfig = loadConfig(projectRoot);
5789
+ if (loadedConfig.resultType !== "success") {
5790
+ return;
5791
+ }
5792
+ register({
5793
+ baseUrl: loadedConfig.absoluteBaseUrl,
5794
+ paths: loadedConfig.paths,
5795
+ addMatchAll: false
5796
+ });
5797
+ tsconfigPathsRegisteredByCwd.add(projectRoot);
5798
+ };
5799
+ var toImportSpecifier = (moduleName) => {
5800
+ if (moduleName.startsWith("file://")) {
5801
+ return moduleName;
5802
+ }
5803
+ if (import_path.default.isAbsolute(moduleName)) {
5804
+ return (0, import_url.pathToFileURL)(moduleName).href;
5805
+ }
5806
+ return moduleName;
5807
+ };
5808
+ var isTsModulePath = (moduleName) => TS_MODULE_RE.test(moduleName.split("?")[0]);
5809
+ var isRelativeModulePath = (moduleName) => moduleName.startsWith("./") || moduleName.startsWith("../");
5810
+ var isBareModuleSpecifier = (moduleName) => {
5811
+ const normalized = String(moduleName ?? "").trim();
5812
+ if (!normalized) {
5813
+ return false;
5814
+ }
5815
+ if (normalized.startsWith("file://") || import_path.default.isAbsolute(normalized) || isRelativeModulePath(normalized)) {
5816
+ return false;
5817
+ }
5818
+ return true;
5819
+ };
5820
+ var importConfigFile = async (configFilePath) => {
5821
+ const ext = import_path.default.extname(configFilePath).toLowerCase();
5822
+ const configFileUrl = `${toImportSpecifier(configFilePath)}?t=${Date.now()}`;
5823
+ if (ext === ".ts" || ext === ".mts") {
5824
+ const requireFn = getRequireFn(import_path.default.dirname(configFilePath));
5825
+ await ensureTsNodeRegistered();
5826
+ await ensureTsconfigPathsRegistered(import_path.default.dirname(configFilePath));
5827
+ return requireFn(configFilePath);
5828
+ }
5829
+ return import(
5830
+ /* webpackIgnore: true */
5831
+ configFileUrl
5832
+ );
5833
+ };
5834
+ var importTradejsModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
5835
+ const normalized = String(moduleName ?? "").trim();
5836
+ if (!normalized) {
5837
+ return {};
5838
+ }
5839
+ let modulePath = normalized;
5840
+ if (normalized.startsWith("file://")) {
5841
+ try {
5842
+ modulePath = (0, import_url.fileURLToPath)(normalized);
5843
+ } catch {
5844
+ modulePath = normalized;
5845
+ }
5846
+ }
5847
+ const requireFn = getRequireFn(
5848
+ import_path.default.isAbsolute(modulePath) ? import_path.default.dirname(modulePath) : cwd
5849
+ );
5850
+ if (isTsModulePath(modulePath)) {
5851
+ await ensureTsNodeRegistered();
5852
+ await ensureTsconfigPathsRegistered(cwd);
5853
+ return requireFn(modulePath);
5854
+ }
5855
+ if (isBareModuleSpecifier(normalized)) {
5856
+ await ensureTsconfigPathsRegistered(cwd);
5857
+ return requireFn(normalized);
5858
+ }
5859
+ try {
5860
+ return await import(
5861
+ /* webpackIgnore: true */
5862
+ toImportSpecifier(normalized)
5863
+ );
5864
+ } catch (error) {
5865
+ if (isTsModulePath(modulePath)) {
5866
+ await ensureTsNodeRegistered();
5867
+ await ensureTsconfigPathsRegistered(cwd);
5868
+ return requireFn(modulePath);
5869
+ }
5870
+ throw error;
5871
+ }
5872
+ };
5873
+ var resolveExportedConfig = (moduleExports) => {
5874
+ const candidate = moduleExports && typeof moduleExports === "object" && "default" in moduleExports ? moduleExports.default : moduleExports;
5875
+ return normalizeConfig(candidate);
5876
+ };
5877
+ var findConfigFilePath = (cwd) => {
5878
+ let currentDir = import_path.default.resolve(cwd);
5879
+ while (true) {
5880
+ for (const fileName of CONFIG_FILE_NAMES) {
5881
+ const fullPath = import_path.default.join(currentDir, fileName);
5882
+ if (import_fs.default.existsSync(fullPath) && import_fs.default.statSync(fullPath).isFile()) {
5883
+ return fullPath;
5884
+ }
5885
+ }
5886
+ const parentDir = import_path.default.dirname(currentDir);
5887
+ if (parentDir === currentDir) {
5888
+ return null;
5889
+ }
5890
+ currentDir = parentDir;
5891
+ }
5892
+ };
5893
+ var resolvePluginModuleSpecifier = (moduleName, cwd = getTradejsProjectCwd()) => {
5894
+ const normalized = String(moduleName ?? "").trim();
5895
+ if (!normalized) {
5896
+ return "";
5897
+ }
5898
+ if (normalized.startsWith("file://")) {
5899
+ try {
5900
+ return (0, import_url.fileURLToPath)(normalized);
5901
+ } catch {
5902
+ return normalized;
5903
+ }
5904
+ }
5905
+ if (import_path.default.isAbsolute(normalized)) {
5906
+ return normalized;
5907
+ }
5908
+ if (isRelativeModulePath(normalized)) {
5909
+ return import_path.default.resolve(cwd, normalized);
5910
+ }
5911
+ return normalized;
5912
+ };
5913
+ var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
5914
+ const cached = cachedByCwd.get(cwd);
5915
+ if (cached) {
5916
+ return cached;
5917
+ }
5918
+ const configFilePath = findConfigFilePath(cwd);
5919
+ if (!configFilePath) {
5920
+ cachedByCwd.set(cwd, {});
5921
+ return {};
5922
+ }
5923
+ try {
5924
+ const moduleExports = await importConfigFile(configFilePath);
5925
+ const config = resolveExportedConfig(moduleExports);
5926
+ cachedByCwd.set(cwd, config);
5927
+ if (!announcedConfigFile.has(configFilePath)) {
5928
+ announcedConfigFile.add(configFilePath);
5929
+ import_logger.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
5930
+ }
5931
+ return config;
5932
+ } catch (error) {
5933
+ import_logger.logger.log(
5934
+ "warn",
5935
+ "Failed to load TradeJS config from %s: %s",
5936
+ configFilePath,
5937
+ String(error)
5938
+ );
5939
+ cachedByCwd.set(cwd, {});
5940
+ return {};
5941
+ }
5942
+ };
5603
5943
 
5604
5944
  // src/strategy/manifests.ts
5605
5945
  var createStrategyRegistryState = () => ({
@@ -5620,6 +5960,144 @@ var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
5620
5960
  state
5621
5961
  };
5622
5962
  };
5963
+ var toUniqueModules = (modules = []) => [
5964
+ ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
5965
+ ];
5966
+ var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
5967
+ const config = await loadTradejsConfig(cwd);
5968
+ return {
5969
+ strategyModules: toUniqueModules(config.strategies),
5970
+ indicatorModules: toUniqueModules(config.indicators)
5971
+ };
5972
+ };
5973
+ var extractModuleEntries = (moduleExport, key) => {
5974
+ if (!moduleExport || typeof moduleExport !== "object") {
5975
+ return null;
5976
+ }
5977
+ const candidate = moduleExport;
5978
+ if (Array.isArray(candidate[key])) {
5979
+ return candidate[key];
5980
+ }
5981
+ const defaultExport = candidate.default;
5982
+ if (defaultExport && Array.isArray(defaultExport[key])) {
5983
+ return defaultExport[key];
5984
+ }
5985
+ return null;
5986
+ };
5987
+ var extractStrategyPluginDefinition = (moduleExport) => {
5988
+ const strategyEntries = extractModuleEntries(
5989
+ moduleExport,
5990
+ "strategyEntries"
5991
+ );
5992
+ return strategyEntries ? { strategyEntries } : null;
5993
+ };
5994
+ var extractIndicatorPluginDefinition = (moduleExport) => {
5995
+ const indicatorEntries = extractModuleEntries(
5996
+ moduleExport,
5997
+ "indicatorEntries"
5998
+ );
5999
+ return indicatorEntries ? { indicatorEntries } : null;
6000
+ };
6001
+ var registerEntries = (entries, source, state) => {
6002
+ for (const entry of entries) {
6003
+ const strategyName = entry.manifest?.name;
6004
+ if (!strategyName) {
6005
+ import_logger2.logger.warn("Skip strategy entry without name from %s", source);
6006
+ continue;
6007
+ }
6008
+ if (state.strategyCreators.has(strategyName)) {
6009
+ import_logger2.logger.warn(
6010
+ 'Skip duplicate strategy "%s" from %s: already registered',
6011
+ strategyName,
6012
+ source
6013
+ );
6014
+ continue;
6015
+ }
6016
+ state.strategyCreators.set(strategyName, entry.creator);
6017
+ state.strategyManifestsMap.set(strategyName, entry.manifest);
6018
+ }
6019
+ };
6020
+ var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
6021
+ if (typeof importTradejsModule === "function") {
6022
+ return importTradejsModule(moduleName, cwd);
6023
+ }
6024
+ return import(
6025
+ /* webpackIgnore: true */
6026
+ moduleName
6027
+ );
6028
+ };
6029
+ var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
6030
+ const { projectRoot, state } = getStrategyRegistryState(cwd);
6031
+ if (!state.pluginsLoadPromise) {
6032
+ (0, import_indicators.resetIndicatorRegistryCache)(projectRoot);
6033
+ state.pluginsLoadPromise = (async () => {
6034
+ const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
6035
+ const strategySet = new Set(strategyModules);
6036
+ const indicatorSet = new Set(indicatorModules);
6037
+ const pluginModuleNames = [
6038
+ .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
6039
+ ];
6040
+ if (!pluginModuleNames.length) {
6041
+ return;
6042
+ }
6043
+ for (const moduleName of pluginModuleNames) {
6044
+ try {
6045
+ const resolvedModuleName = resolvePluginModuleSpecifier(
6046
+ moduleName,
6047
+ projectRoot
6048
+ );
6049
+ const moduleExport = await importStrategyPluginModule(
6050
+ resolvedModuleName,
6051
+ projectRoot
6052
+ );
6053
+ if (strategySet.has(moduleName)) {
6054
+ const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
6055
+ if (!pluginDefinition) {
6056
+ import_logger2.logger.warn(
6057
+ 'Skip strategy plugin "%s": export { strategyEntries } is missing',
6058
+ moduleName
6059
+ );
6060
+ } else {
6061
+ registerEntries(
6062
+ pluginDefinition.strategyEntries,
6063
+ moduleName,
6064
+ state
6065
+ );
6066
+ }
6067
+ }
6068
+ if (indicatorSet.has(moduleName)) {
6069
+ const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
6070
+ if (!indicatorPluginDefinition) {
6071
+ import_logger2.logger.warn(
6072
+ 'Skip indicator plugin "%s": export { indicatorEntries } is missing',
6073
+ moduleName
6074
+ );
6075
+ } else {
6076
+ (0, import_indicators.registerIndicatorEntries)(
6077
+ indicatorPluginDefinition.indicatorEntries,
6078
+ moduleName,
6079
+ projectRoot
6080
+ );
6081
+ }
6082
+ }
6083
+ if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
6084
+ import_logger2.logger.warn(
6085
+ 'Skip plugin "%s": no strategy/indicator sections requested in config',
6086
+ moduleName
6087
+ );
6088
+ }
6089
+ } catch (error) {
6090
+ import_logger2.logger.warn(
6091
+ 'Failed to load plugin "%s": %s',
6092
+ moduleName,
6093
+ String(error)
6094
+ );
6095
+ }
6096
+ }
6097
+ })();
6098
+ }
6099
+ await state.pluginsLoadPromise;
6100
+ };
5623
6101
  var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
5624
6102
  if (!name) {
5625
6103
  return void 0;
@@ -5647,24 +6125,36 @@ var strategies = new Proxy(
5647
6125
  );
5648
6126
 
5649
6127
  // src/strategyAdapters/ai.ts
5650
- var buildBaseAiPayload = (signal) => ({
5651
- signal: {
5652
- symbol: signal.symbol,
5653
- signalId: signal.signalId,
5654
- interval: signal.interval,
5655
- direction: signal.direction,
5656
- timestamp: signal.timestamp,
5657
- strategy: signal.strategy,
5658
- prices: {
5659
- currentPrice: signal.prices.currentPrice,
5660
- takeProfitPrice: signal.prices.takeProfitPrice,
5661
- stopLossPrice: signal.prices.stopLossPrice
5662
- }
5663
- },
5664
- figures: trimSeriesDeep(signal.figures ?? {}),
5665
- indicators: trimSeriesDeep(signal.indicators),
5666
- additionalIndicators: trimSeriesDeep(signal.additionalIndicators ?? {})
5667
- });
6128
+ var toRecord2 = (value) => {
6129
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6130
+ return {};
6131
+ }
6132
+ return value;
6133
+ };
6134
+ var buildBaseAiPayload = (signal) => {
6135
+ const additionalIndicators = {
6136
+ ...toRecord2(signal.additionalIndicators),
6137
+ marketContext: buildAiMarketContext(signal)
6138
+ };
6139
+ return {
6140
+ signal: {
6141
+ symbol: signal.symbol,
6142
+ signalId: signal.signalId,
6143
+ interval: signal.interval,
6144
+ direction: signal.direction,
6145
+ timestamp: signal.timestamp,
6146
+ strategy: signal.strategy,
6147
+ prices: {
6148
+ currentPrice: signal.prices.currentPrice,
6149
+ takeProfitPrice: signal.prices.takeProfitPrice,
6150
+ stopLossPrice: signal.prices.stopLossPrice
6151
+ }
6152
+ },
6153
+ figures: trimSeriesDeep(signal.figures ?? {}),
6154
+ indicators: trimSeriesDeep(signal.indicators),
6155
+ additionalIndicators: trimSeriesDeep(additionalIndicators)
6156
+ };
6157
+ };
5668
6158
  var defaultAiAdapter = {};
5669
6159
  var getStrategyAiAdapter = (strategy) => getStrategyManifest(strategy)?.aiAdapter ?? defaultAiAdapter;
5670
6160
  var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy);
@@ -5678,6 +6168,11 @@ var buildAiHumanPromptAddonByStrategy = (signal, payload) => getSignalAiAdapter(
5678
6168
  signal,
5679
6169
  payload
5680
6170
  }) ?? "";
6171
+ var postProcessAiAnalysisByStrategy = (signal, analysis, payload = buildAiPayloadByStrategy(signal)) => getSignalAiAdapter(signal).postProcessAnalysis?.({
6172
+ signal,
6173
+ payload,
6174
+ analysis
6175
+ }) ?? analysis;
5681
6176
 
5682
6177
  // src/ai.ts
5683
6178
  var parseAIResponse = (input) => {
@@ -5687,8 +6182,8 @@ var parseAIResponse = (input) => {
5687
6182
  if (!match) throw new Error("JSON block not found");
5688
6183
  return JSON.parse(match[0]);
5689
6184
  } catch (err) {
5690
- console.error("\u274C \u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0430\u0440\u0441\u0438\u043D\u0433\u0430 AI-\u043E\u0442\u0432\u0435\u0442\u0430:", err);
5691
- console.log("\u{1F50D} \u0418\u0441\u0445\u043E\u0434\u043D\u044B\u0439 \u0442\u0435\u043A\u0441\u0442:", input);
6185
+ console.error("Failed to parse AI response:", err);
6186
+ console.log("Raw AI response:", input);
5692
6187
  return {};
5693
6188
  }
5694
6189
  };
@@ -5734,20 +6229,22 @@ var normalizeAnalysis = (raw) => {
5734
6229
  };
5735
6230
  };
5736
6231
  var buildAiSystemPrompt = (signal) => `
5737
- \u0422\u044B \u2014 \u043F\u043E\u043C\u043E\u0449\u043D\u0438\u043A \u043A\u0440\u0438\u043F\u0442\u043E-\u0442\u0440\u0435\u0439\u0434\u0435\u0440\u0430.
5738
- \u0410\u043D\u0430\u043B\u0438\u0437\u0438\u0440\u0443\u0439 \u043F\u0440\u0438\u0441\u043B\u0430\u043D\u043D\u044B\u0439 JSON \u0441\u043E \u0441\u0434\u0435\u043B\u043A\u043E\u0439, \u0441\u0432\u0435\u0447\u0430\u043C\u0438, \u0438\u043D\u0434\u0438\u043A\u0430\u0442\u043E\u0440\u0430\u043C\u0438 (\u043F\u043E \u043C\u043E\u043D\u0435\u0442\u0435 \u0438 BTC \u043D\u0430 \u0440\u0430\u0437\u043D\u044B\u0445 \u0422\u0424) \u0438 \u0444\u0438\u0433\u0443\u0440\u0430\u043C\u0438/\u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u043E\u043C \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u0438.
5739
- \u0414\u0430\u043D\u043D\u044B\u0435 \u0440\u044F\u0434\u043E\u0432 \u0443\u0436\u0435 \u0443\u043A\u043E\u0440\u043E\u0447\u0435\u043D\u044B \u0434\u043E \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0445 5 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0439.
6232
+ You are an internal market-structure classifier for an already computed system signal.
6233
+ Analyze the provided JSON containing the trade, candles, indicators (for the coin and BTC across multiple timeframes), and strategy figures/context.
6234
+ Series data is already trimmed to the latest 5 values.
5740
6235
 
5741
- \u0412\u0410\u0416\u041D\u041E:
5742
- - \u041D\u0435 \u043F\u0440\u0438\u0434\u0443\u043C\u044B\u0432\u0430\u0439 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u0435 \u0434\u0430\u043D\u043D\u044B\u0435.
5743
- - \u041E\u043F\u0438\u0440\u0430\u0439\u0441\u044F \u043D\u0430 \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u0438 \u0443\u0440\u043E\u0432\u043D\u0438 \u0438\u0441\u0445\u043E\u0434\u043D\u043E\u0433\u043E \u0441\u0438\u0433\u043D\u0430\u043B\u0430, \u043D\u043E \u043C\u043E\u0436\u0435\u0448\u044C \u043D\u0435 \u0441\u043E\u0433\u043B\u0430\u0441\u0438\u0442\u044C\u0441\u044F \u0441 \u043D\u0438\u043C\u0438.
5744
- - \u0423\u0447\u0438\u0442\u044B\u0432\u0430\u0439, \u0447\u0442\u043E \u0441\u0438\u0433\u043D\u0430\u043B \u043F\u043E\u0441\u0442\u0440\u043E\u0435\u043D \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u0435\u0439, \u0443\u043A\u0430\u0437\u0430\u043D\u043D\u043E\u0439 \u0432 \u043F\u043E\u043B\u0435 signal.strategy.
5745
- - \u0422\u0432\u043E\u044F \u0446\u0435\u043B\u044C: \u0434\u0430\u0442\u044C \u041F\u0420\u0410\u041A\u0422\u0418\u0427\u041D\u042B\u0419 short-term trade plan, \u0430 \u043D\u0435 \u043E\u0431\u0449\u0438\u0435 \u0441\u043B\u043E\u0432\u0430.
5746
- - \u041D\u0435 \u043F\u0438\u0448\u0438 \u0430\u0431\u0441\u0442\u0440\u0430\u043A\u0442\u043D\u043E \u0432\u0440\u043E\u0434\u0435 "\u0435\u0441\u0442\u044C momentum/slope" \u0431\u0435\u0437 \u043F\u0440\u0438\u0432\u044F\u0437\u043A\u0438 \u043A \u0440\u0435\u0448\u0435\u043D\u0438\u044E.
5747
- - \u041F\u0438\u0448\u0438 comment \u043F\u043E-\u0440\u0443\u0441\u0441\u043A\u0438.
5748
- - \u0415\u0441\u043B\u0438 \u0443\u0432\u0435\u0440\u0435\u043D\u043D\u043E\u0441\u0442\u044C \u043D\u0435\u043F\u043E\u043B\u043D\u0430\u044F, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 \u043E\u0441\u0442\u043E\u0440\u043E\u0436\u043D\u044B\u0435 \u0444\u043E\u0440\u043C\u0443\u043B\u0438\u0440\u043E\u0432\u043A\u0438 ("\u0441\u043A\u043E\u0440\u0435\u0435", "\u043F\u043E\u043A\u0430 \u043D\u0435\u0442 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F", "\u0432\u0435\u0440\u043E\u044F\u0442\u043D\u043E"), \u0430 \u043D\u0435 \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u0447\u043D\u044B\u0435 \u0443\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F.
6236
+ Important:
6237
+ - Do not invent missing data.
6238
+ - This is an internal audit/classification task, not user-facing trading advice.
6239
+ - Do not generate execution instructions, do not replace the original thesis with a new one, and do not provide personalized investment advice.
6240
+ - Use the original signal direction and levels as the anchor, but you may state that the current structure does not support them.
6241
+ - Respect the source strategy specified in \`signal.strategy\`.
6242
+ - Your goal is to explain how well the observed structure matches the existing signal and how structurally confirmed it is right now.
6243
+ - Do not write vague statements like "there is momentum/slope" without tying them to the decision.
6244
+ - Write all user-visible text fields in the requested response language. If no explicit language instruction is provided later, default to English.
6245
+ - If confidence is incomplete, prefer cautious wording such as "likely", "not confirmed yet", or "probably" instead of categorical claims.
5749
6246
 
5750
- \u041E\u0442\u0432\u0435\u0447\u0430\u0439 \u0441\u0442\u0440\u043E\u0433\u043E \u041E\u0414\u041D\u0418\u041C JSON-\u043E\u0431\u044A\u0435\u043A\u0442\u043E\u043C \u0431\u0435\u0437 \u0442\u0435\u043A\u0441\u0442\u0430 \u0432\u043E\u043A\u0440\u0443\u0433:
6247
+ Return exactly one JSON object and nothing else:
5751
6248
 
5752
6249
  {
5753
6250
  "direction": payload.signal.direction | null,
@@ -5765,129 +6262,233 @@ var buildAiSystemPrompt = (signal) => `
5765
6262
  "triggerInvalidation": string
5766
6263
  }
5767
6264
 
5768
- - \u041D\u0435 \u0434\u043E\u0431\u0430\u0432\u043B\u044F\u0439 \u0434\u0440\u0443\u0433\u0438\u0435 \u043F\u043E\u043B\u044F.
5769
- - \u0412\u0441\u0435 \u0447\u0438\u0441\u043B\u0430 \u0434\u043E\u043B\u0436\u043D\u044B \u0431\u044B\u0442\u044C \u043A\u043E\u043D\u0435\u0447\u043D\u044B\u043C\u0438 (finite), \u0431\u0435\u0437 NaN/Infinity.
5770
- - \u0412\u0441\u0435 \u0442\u0435\u043A\u0441\u0442\u043E\u0432\u044B\u0435 \u043F\u043E\u043B\u044F \u2014 \u043A\u043E\u0440\u043E\u0442\u043A\u0438\u0435 \u0441\u0442\u0440\u043E\u043A\u0438 (\u0431\u0435\u0437 \u043F\u0435\u0440\u0435\u043D\u043E\u0441\u043E\u0432, \u0431\u0435\u0437 markdown-\u0441\u043F\u0438\u0441\u043A\u043E\u0432).
5771
- - "direction" \u2014 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0441\u0434\u0435\u043B\u043A\u0438: \u043B\u0438\u0431\u043E \u0420\u041E\u0412\u041D\u041E payload.signal.direction, \u043B\u0438\u0431\u043E null (\u0435\u0441\u043B\u0438 \u0441\u0434\u0435\u043B\u043A\u0443 \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435 \u043E\u0434\u043E\u0431\u0440\u044F\u0435\u0448\u044C). \u041D\u0438\u043A\u043E\u0433\u0434\u0430 \u043D\u0435 \u043F\u0440\u0435\u0434\u043B\u0430\u0433\u0430\u0439 \u043F\u0440\u043E\u0442\u0438\u0432\u043E\u043F\u043E\u043B\u043E\u0436\u043D\u043E\u0435 \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435.
5772
- - "quality" \u2014 \u043A\u0430\u0447\u0435\u0441\u0442\u0432\u043E \u0412\u0425\u041E\u0414\u0410 \u0418\u041C\u0415\u041D\u041D\u041E \u0421\u0415\u0419\u0427\u0410\u0421 (timing + \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F), \u0430 \u043D\u0435 \u043E\u0431\u0449\u0430\u044F \u0438\u0434\u0435\u044F \u0441\u0435\u0442\u0430\u043F\u0430.
5773
- - "needRetest" \u2014 \u043D\u0443\u0436\u043D\u043E \u043B\u0438 \u0434\u043E\u0436\u0434\u0430\u0442\u044C\u0441\u044F \u0440\u0435\u0442\u0435\u0441\u0442\u0430 \u043F\u0435\u0440\u0435\u0434 \u0432\u0445\u043E\u0434\u043E\u043C \u043F\u043E \u0442\u0435\u043A\u0443\u0449\u0435\u043C\u0443 \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044E.
5774
- - "retestPrice" \u2014 \u0446\u0435\u043D\u0430/\u0443\u0440\u043E\u0432\u0435\u043D\u044C, \u043F\u043E \u043A\u043E\u0442\u043E\u0440\u043E\u043C\u0443 \u0431\u0443\u0434\u0435\u043C \u043E\u0446\u0435\u043D\u0438\u0432\u0430\u0442\u044C \u0440\u0435\u0442\u0435\u0441\u0442 (\u0438\u043B\u0438 null, \u0435\u0441\u043B\u0438 \u0440\u0435\u0442\u0435\u0441\u0442 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D / \u0443\u0440\u043E\u0432\u0435\u043D\u044C \u043D\u0435 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0451\u043D).
5775
- - "takeProfitPrice" \u0438 "stopLossPrice" \u2014 \u0442\u0432\u043E\u0438 \u0443\u0440\u043E\u0432\u043D\u0438 \u0434\u043B\u044F \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0433\u043E \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F. \u0415\u0441\u043B\u0438 \u0441\u0434\u0435\u043B\u043A\u0438 \u043D\u0435\u0442, \u043E\u0431\u0430 null.
5776
- - \u0420\u0430\u0437\u0431\u0438\u0432\u0430\u0439 \u0430\u043D\u0430\u043B\u0438\u0437 \u043D\u0430 \u043F\u043E\u043B\u044F:
5777
- - "setup" \u2014 \u0447\u0442\u043E \u0437\u0430 \u0441\u0435\u0442\u0430\u043F \u043F\u043E \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435/\u0442\u0440\u0435\u043D\u0434\u043E\u0432\u043E\u0439 \u043B\u0438\u043D\u0438\u0438 \u0441\u0435\u0439\u0447\u0430\u0441.
5778
- - "confirmations" \u2014 2-4 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F/\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0430 \u043F\u043E \u0438\u043D\u0434\u0438\u043A\u0430\u0442\u043E\u0440\u0430\u043C \u043C\u043E\u043D\u0435\u0442\u044B.
5779
- - "btcContext" \u2014 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043B\u0438 BTC \u0438\u0434\u0435\u044E \u0438\u043B\u0438 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u0435\u0442.
5780
- - "retestPlan" \u2014 \u0447\u0442\u043E \u0436\u0434\u0430\u0442\u044C \u043F\u043E \u0440\u0435\u0442\u0435\u0441\u0442\u0443 (\u0438\u043B\u0438 \u043F\u043E\u0447\u0435\u043C\u0443 \u0440\u0435\u0442\u0435\u0441\u0442 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D).
5781
- - "riskLevels" \u2014 \u043A\u0440\u0430\u0442\u043A\u043E \u043F\u0440\u043E TP/SL \u0438 \u0440\u0438\u0441\u043A-\u043F\u0440\u043E\u0444\u0438\u043B\u044C.
5782
- - "qualityReason" \u2014 \u043F\u043E\u0447\u0435\u043C\u0443 quality \u0438\u043C\u0435\u043D\u043D\u043E \u0442\u0430\u043A\u043E\u0439.
5783
- - "triggerInvalidation" \u2014 \u0447\u0442\u043E \u0434\u043E\u043B\u0436\u043D\u043E \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C \u0432\u0445\u043E\u0434 / \u0447\u0442\u043E \u043E\u0442\u043C\u0435\u043D\u044F\u0435\u0442 \u0438\u0434\u0435\u044E.
5784
- - "comment" \u043D\u0435 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D. \u0415\u0441\u043B\u0438 \u0443\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0448\u044C, \u043D\u0435 \u0434\u0443\u0431\u043B\u0438\u0440\u0443\u0439 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u044B\u0435 \u043F\u043E\u043B\u044F.
6265
+ - Do not add any other fields.
6266
+ - All numbers must be finite, with no \`NaN\` or \`Infinity\`.
6267
+ - All text fields must be short strings with no line breaks and no markdown lists.
6268
+ - \`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.
6269
+ - \`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.
6270
+ - \`needRetest\` indicates whether an additional confirmation level is required before the current signal can be treated as structurally confirmed.
6271
+ - \`retestPrice\` is the key level that would confirm or invalidate the structure, or \`null\` if no extra level is needed or available.
6272
+ - \`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\`.
6273
+ - Use these fields as separate parts of the analysis:
6274
+ - \`setup\`: the current structural setup or trendline state.
6275
+ - \`confirmations\`: 2-4 concrete confirmations or conflicts from the coin indicators.
6276
+ - \`btcContext\`: whether BTC supports the idea, is neutral, or conflicts with it.
6277
+ - \`retestPlan\`: what must happen at the key level to confirm the structure, or why no extra level is needed.
6278
+ - \`riskLevels\`: a short note on whether the existing levels and risk structure are internally coherent, without creating a new trade plan.
6279
+ - \`qualityReason\`: why the quality score is what it is.
6280
+ - \`triggerInvalidation\`: what must happen to confirm the signal or what invalidates the current structural thesis.
6281
+ - \`comment\` is optional. If you include it, do not just duplicate the structured fields.
5785
6282
 
5786
- \u0415\u0441\u043B\u0438 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435\u0434\u043E\u0441\u0442\u0430\u0442\u043E\u0447\u043D\u043E \u0438\u043B\u0438 \u0441\u0435\u0442\u0430\u043F \u0441\u043B\u0430\u0431\u044B\u0439 \u2014 \u0432\u0435\u0440\u043D\u0438 "direction": null, quality <= 2 \u0438 \u043E\u0431\u044A\u044F\u0441\u043D\u0438 \u043F\u043E\u0447\u0435\u043C\u0443.
6283
+ If the data is insufficient or the setup is weak, return \`"direction": null\`, \`quality <= 2\`, and explain why.
5787
6284
 
5788
- \u0421\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430 \u0432\u0445\u043E\u0434\u043D\u043E\u0433\u043E payload (JSON \u0432 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0438 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F):
6285
+ Input payload structure:
5789
6286
  - payload.signal:
5790
6287
  symbol, signalId, interval, direction, timestamp, strategy, prices
5791
6288
  - payload.signal.prices:
5792
6289
  currentPrice, takeProfitPrice, stopLossPrice
5793
6290
  - payload.figures:
5794
- \u0441\u043B\u043E\u0432\u0430\u0440\u044C strategy-specific \u0444\u0438\u0433\u0443\u0440/\u0433\u0435\u043E\u043C\u0435\u0442\u0440\u0438\u0438 (\u0435\u0441\u043B\u0438 \u0435\u0441\u0442\u044C). \u041A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u044B\u0435 \u043F\u043E\u043B\u044F \u0437\u0430\u0432\u0438\u0441\u044F\u0442 \u043E\u0442 \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u0438.
6291
+ strategy-specific figures or geometry when available. Fields vary by strategy.
5795
6292
  - payload.indicators:
5796
- \u0441\u043B\u043E\u0432\u0430\u0440\u044C \u0438\u043D\u0434\u0438\u043A\u0430\u0442\u043E\u0440\u043E\u0432/\u0440\u044F\u0434\u043E\u0432 \u043F\u043E \u043C\u043E\u043D\u0435\u0442\u0435 \u0438 BTC; \u0440\u044F\u0434\u044B \u0443\u0436\u0435 \u043E\u0431\u0440\u0435\u0437\u0430\u043D\u044B \u0434\u043E \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0445 5 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0439.
5797
- \u041F\u0430\u0442\u0442\u0435\u0440\u043D\u044B \u043A\u043B\u044E\u0447\u0435\u0439:
5798
- \u2022 \u043C\u043E\u043D\u0435\u0442\u0430: maFast, atrPct, macd..., candles15m/candles1h/candles4h/candles1d, \u0430 \u0442\u0430\u043A\u0436\u0435 *1h/*4h/*1d
5799
- \u2022 BTC: btcMaFast, btcAtr, btcMacd..., btcCandles*, \u0430 \u0442\u0430\u043A\u0436\u0435 btc*1h/*4h/*1d
5800
- \u2022 \u0441\u043B\u0443\u0436\u0435\u0431\u043D\u044B\u0435 \u043A\u043B\u044E\u0447\u0438 \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u0438 \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u044B (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 correlation, spread, touches, distance)
6293
+ indicator dictionaries and series for the coin and BTC; all series are already trimmed to the latest 5 values.
6294
+ - payload.additionalIndicators:
6295
+ strategy-specific summary/context fields. This is not noise; it contains derived fields deliberately passed by the strategy to help the decision.
6296
+ Examples: helperFlags, structureContext, spread, correlation, volatilitySummary.
6297
+ Always inspect \`payload.additionalIndicators.marketContext\` when present:
6298
+ \u2022 \`marketContext.tradingSession\`: UTC session at signal time: asia / europe / us / overlap / off_hours.
6299
+ \u2022 \`marketContext.binanceCoinbaseSpread\`: BTC spread between Coinbase and Binance from \`payload.indicators.spread\`; \`value=(Coinbase-Binance)/Binance\`, \`bps=value*10000\`.
6300
+ 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.
6301
+ If \`derivativesContext\` exists, it is a derived Coinalyze summary for the time of the signal. Coinalyze context is built only from \`BTCUSDT\` and \`ETHUSDT\` reference symbols, not for every target coin. \`targetSymbol\` is just the source signal coin. Use BTC/ETH open interest, funding, liquidations, and pressure/riskFlags as positioning context, not as an independent trade idea.
6302
+ Key patterns:
6303
+ \u2022 coin: \`maFast\`, \`atrPct\`, \`macd...\`, \`candles15m/candles1h/candles4h/candles1d\`, and \`*1h/*4h/*1d\`
6304
+ \u2022 BTC: \`btcMaFast\`, \`btcAtr\`, \`btcMacd...\`, \`btcCandles*\`, and \`btc*1h/*4h/*1d\`
6305
+ \u2022 strategy service keys are possible as well, for example \`correlation\`, \`spread\`, \`touches\`, \`distance\`
5801
6306
 
5802
- \u041A\u0430\u043A \u0430\u043D\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C (\u043F\u0440\u0438\u043E\u0440\u0438\u0442\u0435\u0442\u044B):
5803
- 1) \u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443 \u0446\u0435\u043D\u044B \u0438 \u0433\u0435\u043E\u043C\u0435\u0442\u0440\u0438\u044E/\u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 \u0441\u0435\u0442\u0430\u043F\u0430 \u0438\u0437 payload.figures. \u042D\u0442\u043E \u043F\u0440\u0438\u043E\u0440\u0438\u0442\u0435\u0442\u043D\u0435\u0435 \u0438\u043D\u0434\u0438\u043A\u0430\u0442\u043E\u0440\u043E\u0432.
5804
- 2) \u0417\u0430\u0442\u0435\u043C \u043E\u0446\u0435\u043D\u0438 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435/\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043F\u043E \u0438\u043D\u0434\u0438\u043A\u0430\u0442\u043E\u0440\u0430\u043C \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u043C\u043E\u043D\u0435\u0442\u044B.
5805
- 3) \u0417\u0430\u0442\u0435\u043C \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 BTC (\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0438\u043B\u0438 \u043B\u043E\u043C\u0430\u0435\u0442 \u0438\u0434\u0435\u044E).
5806
- 4) \u0422\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0441\u043B\u0435 \u044D\u0442\u043E\u0433\u043E \u0432\u044B\u0431\u0435\u0440\u0438 direction, quality \u0438 \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u043F\u043E \u0440\u0435\u0442\u0435\u0441\u0442\u0443.
5807
- 5) \u0415\u0441\u043B\u0438 \u0435\u0441\u0442\u044C \u0441\u0438\u043B\u044C\u043D\u044B\u0435 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u044B, \u0441\u043D\u0438\u0436\u0430\u0439 quality \u0438\u043B\u0438 \u0441\u0442\u0430\u0432\u044C null.
6307
+ How to analyze, in order:
6308
+ 1. Start with price structure and the setup geometry or context in \`payload.figures\`. This has higher priority than indicators.
6309
+ 2. Then use \`payload.additionalIndicators\` when it contains explicit strategy-specific context such as line state, spread, correlation, and similar fields.
6310
+ 3. Then assess confirmation or conflict from the current coin indicators.
6311
+ 4. Then evaluate BTC context.
6312
+ 5. Only after that choose \`direction\`, \`quality\`, and whether an extra confirmation level is required.
6313
+ 6. If strong conflicts exist, reduce quality or set direction to \`null\`.
5808
6314
 
5809
- \u042F\u0432\u043D\u044B\u0435 \u043F\u0440\u0430\u0432\u0438\u043B\u0430 \u043F\u0440\u0438 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0435 \u0441\u0438\u0433\u043D\u0430\u043B\u043E\u0432:
5810
- - \u0415\u0441\u043B\u0438 \u0444\u0438\u0433\u0443\u0440\u0430/\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430 \u0446\u0435\u043D\u044B \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u044B \u0438\u043B\u0438 \u0441\u043E\u043C\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B, \u0438\u043D\u0434\u0438\u043A\u0430\u0442\u043E\u0440\u044B \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u044B "\u0441\u043F\u0430\u0441\u0430\u0442\u044C" \u0441\u0435\u0442\u0430\u043F.
5811
- - \u0415\u0441\u043B\u0438 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430 \u043E\u043A, \u043D\u043E BTC \u0438/\u0438\u043B\u0438 \u043A\u043B\u044E\u0447\u0435\u0432\u044B\u0435 \u0438\u043D\u0434\u0438\u043A\u0430\u0442\u043E\u0440\u044B \u0437\u0430\u043C\u0435\u0442\u043D\u043E \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u044E\u0442, \u043E\u0431\u044B\u0447\u043D\u043E quality <= 3.
5812
- - \u0415\u0441\u043B\u0438 \u043D\u0435 \u043E\u0434\u043E\u0431\u0440\u044F\u0435\u0448\u044C \u0442\u0435\u043A\u0443\u0449\u0443\u044E \u0441\u0434\u0435\u043B\u043A\u0443 (direction=null), \u0432 comment \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u043A\u0440\u0430\u0442\u043A\u043E \u043D\u0430\u0437\u043E\u0432\u0438 \u0433\u043B\u0430\u0432\u043D\u0443\u044E \u043F\u0440\u0438\u0447\u0438\u043D\u0443.
5813
- \u0415\u0441\u043B\u0438 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0448\u044C \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u044B\u0435 \u043F\u043E\u043B\u044F, \u0443\u043A\u0430\u0436\u0438 \u0433\u043B\u0430\u0432\u043D\u0443\u044E \u043F\u0440\u0438\u0447\u0438\u043D\u0443 \u0432 "qualityReason" \u0438/\u0438\u043B\u0438 "triggerInvalidation".
6315
+ Explicit conflict rules:
6316
+ - If the figure or price structure is invalid or doubtful, indicators must not rescue the setup.
6317
+ - If strategy-specific helper fields explicitly say the signal is not confirmed yet, lacks margin, or requires waiting, do not overstate quality.
6318
+ - If the structure is acceptable but BTC or key indicators noticeably conflict, quality is usually \`<= 3\`.
6319
+ - If \`derivativesContext.referenceContexts\` exists, check \`primaryReferenceSymbol\` first, then compare \`BTCUSDT\` and \`ETHUSDT\` as broad-market derivatives context. Do not search for Coinalyze data for \`targetSymbol\` unless \`targetSymbol\` itself is \`BTCUSDT\` or \`ETHUSDT\`.
6320
+ - If \`derivativesContext.summary.riskFlags\` contains \`crowded_long\` for a LONG or \`crowded_short\` for a SHORT, treat that as crowded positioning and do not overstate quality without strong structural confirmation.
6321
+ - If \`derivativesContext.summary.directionAligned=false\`, explicitly mention the derivatives conflict in \`confirmations\` or \`qualityReason\`.
6322
+ - If \`derivativesContext\` is absent, stale, or \`missing_derivatives\`, do not infer Coinalyze conclusions and do not penalize the signal just because that data is missing.
6323
+ - If \`marketContext.tradingSession\` exists, treat the session as a liquidity and volatility 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\`.
6324
+ - If \`marketContext.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.
6325
+ - If \`marketContext.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.
6326
+ - If the current signal is not confirmed (\`direction=null\`), name the main reason briefly in \`comment\`.
6327
+ If you use the structured fields, include the main reason in \`qualityReason\` or \`triggerInvalidation\`.
5814
6328
 
5815
- \u041F\u0440\u0430\u0432\u0438\u043B\u0430 \u0434\u043B\u044F direction / TP / SL:
5816
- - direction = LONG \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u043E\u0436\u0438\u0434\u0430\u0435\u043C\u043E\u0435 \u0434\u0432\u0438\u0436\u0435\u043D\u0438\u0435 \u0432\u0432\u0435\u0440\u0445 \u043E\u0431\u043E\u0441\u043D\u043E\u0432\u0430\u043D\u043E; SHORT \u2014 \u0432\u043D\u0438\u0437; \u0438\u043D\u0430\u0447\u0435 null.
5817
- - \u0414\u043B\u044F LONG \u043E\u0431\u044B\u0447\u043D\u043E stopLossPrice < currentPrice < takeProfitPrice.
5818
- - \u0414\u043B\u044F SHORT \u043E\u0431\u044B\u0447\u043D\u043E takeProfitPrice < currentPrice < stopLossPrice.
5819
- - TP/SL \u0434\u043E\u043B\u0436\u043D\u044B \u0431\u044B\u0442\u044C \u0440\u0435\u0430\u043B\u0438\u0441\u0442\u0438\u0447\u043D\u044B\u043C\u0438 \u043E\u0442\u043D\u043E\u0441\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0446\u0435\u043D\u044B (\u043D\u0435 \u0441\u0442\u0430\u0432\u044C absurdly far/near \u0443\u0440\u043E\u0432\u043D\u0438).
5820
- - \u0422\u0440\u0435\u0431\u0443\u0435\u043C\u043E\u0435 \u0441\u043E\u043E\u0442\u043D\u043E\u0448\u0435\u043D\u0438\u0435 reward:risk \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435 \u0445\u0443\u0436\u0435 1:3 (\u0442\u043E \u0435\u0441\u0442\u044C reward/risk >= 0.33).
5821
- - \u0415\u0441\u043B\u0438 direction = null, \u0442\u043E takeProfitPrice = null \u0438 stopLossPrice = null.
5822
- - \u0415\u0441\u043B\u0438 needRetest = false, \u0442\u043E retestPrice = null.
5823
- - \u0415\u0441\u043B\u0438 needRetest = true, retestPrice \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0443\u043A\u0430\u0437\u0430\u043D (finite number) \u0438 \u0441\u0432\u044F\u0437\u0430\u043D \u0441 \u0443\u0440\u043E\u0432\u043D\u0435\u043C \u0440\u0435\u0442\u0435\u0441\u0442\u0430/\u043F\u0440\u043E\u0431\u043E\u044F.
5824
- - Sanity-check \u043F\u0435\u0440\u0435\u0434 \u043E\u0442\u0432\u0435\u0442\u043E\u043C: \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u043D\u043E\u0441\u0442\u044C direction \u0441 TP/SL \u0438 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0446\u0435\u043D\u043E\u0439.
6329
+ Rules for \`direction\` / TP / SL:
6330
+ - \`direction = LONG\` only if the data confirms the existing LONG signal; \`SHORT\` only if the data confirms the existing SHORT signal; otherwise \`null\`.
6331
+ - For LONG, the expected relation is usually \`stopLossPrice < currentPrice < takeProfitPrice\`.
6332
+ - For SHORT, the expected relation is usually \`takeProfitPrice < currentPrice < stopLossPrice\`.
6333
+ - Do not optimize or recalculate TP/SL for a "better trade"; only assess whether the already supplied levels are coherent.
6334
+ - If \`direction = null\`, then \`takeProfitPrice = null\` and \`stopLossPrice = null\`.
6335
+ - If \`needRetest = false\`, then \`retestPrice = null\`.
6336
+ - If \`needRetest = true\`, \`retestPrice\` must be a finite number tied to a meaningful retest or breakout level.
6337
+ - Before responding, sanity-check the consistency of \`direction\`, TP/SL, and the current price.
5825
6338
 
5826
- \u0428\u043A\u0430\u043B\u0430 quality (\u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 \u0432\u0441\u044E \u0448\u043A\u0430\u043B\u0443, \u043D\u0435 \u0437\u0430\u0432\u044B\u0448\u0430\u0439):
5827
- - 1: \u043F\u043B\u043E\u0445\u043E\u0439/\u0445\u0430\u043E\u0442\u0438\u0447\u043D\u044B\u0439 \u0441\u0435\u0442\u0430\u043F, \u0441\u0438\u043B\u044C\u043D\u044B\u0435 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u044B, \u0432\u0445\u043E\u0434 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D
5828
- - 2: \u0441\u043B\u0430\u0431\u044B\u0439 \u0441\u0435\u0442\u0430\u043F, \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0439 \u043C\u0430\u043B\u043E, \u043B\u0443\u0447\u0448\u0435 \u043F\u0440\u043E\u043F\u0443\u0441\u043A
5829
- - 3: \u0441\u0440\u0435\u0434\u043D\u0438\u0439 \u0441\u0435\u0442\u0430\u043F, \u0435\u0441\u0442\u044C \u0438\u0434\u0435\u044F, \u043D\u043E \u0435\u0441\u0442\u044C \u0437\u0430\u043C\u0435\u0442\u043D\u044B\u0435 \u0440\u0438\u0441\u043A\u0438/\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u044B
5830
- - 4: \u0445\u043E\u0440\u043E\u0448\u0438\u0439 \u0441\u0435\u0442\u0430\u043F, \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0439, \u0440\u0438\u0441\u043A\u0438 \u043F\u043E\u043D\u044F\u0442\u043D\u044B
5831
- - 5: \u043E\u0447\u0435\u043D\u044C \u0441\u0438\u043B\u044C\u043D\u044B\u0439 \u0441\u0435\u0442\u0430\u043F, \u0447\u0438\u0441\u0442\u0430\u044F \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430 + \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F + \u0430\u0434\u0435\u043A\u0432\u0430\u0442\u043D\u044B\u0439 \u0440\u0438\u0441\u043A
6339
+ Quality scale:
6340
+ - 1: poor or chaotic setup, strong conflicts, signal not structurally confirmed
6341
+ - 2: weak setup, few confirmations, more of a watch or reject
6342
+ - 3: average setup, some structure exists, but notable conflicts remain
6343
+ - 4: good setup, several confirmations, structure is mostly coherent
6344
+ - 5: very strong setup, clean structure, confirmations, and internally coherent levels
5832
6345
 
5833
- \u0422\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u043A \u043F\u043E\u043B\u0435\u0437\u043D\u043E\u043C\u0443 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u043E\u043C\u0443 \u0430\u043D\u0430\u043B\u0438\u0437\u0443 (\u0431\u0435\u0437 \u0432\u043E\u0434\u044B):
5834
- - \u0423\u043A\u0430\u0436\u0438 2-4 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u044B\u0445 \u0444\u0430\u043A\u0442\u043E\u0440\u0430 "\u0437\u0430" \u0438\u043B\u0438 "\u043F\u0440\u043E\u0442\u0438\u0432" \u0441\u0434\u0435\u043B\u043A\u0443 \u0432 "confirmations".
5835
- - \u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u0443\u043F\u043E\u043C\u044F\u043D\u0438 \u0440\u043E\u043B\u044C \u043A\u043B\u044E\u0447\u0435\u0432\u043E\u0439 \u0444\u0438\u0433\u0443\u0440\u044B/\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u044B \u0441\u0435\u0442\u0430\u043F\u0430 (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 \u043F\u0440\u043E\u0431\u043E\u0439/\u0440\u0435\u0442\u0435\u0441\u0442/\u043B\u043E\u0436\u043D\u044B\u0439 \u043F\u0440\u043E\u0431\u043E\u0439/\u043A\u0430\u0441\u0430\u043D\u0438\u0435/\u043D\u0435\u0442 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F).
5836
- - \u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u0443\u043F\u043E\u043C\u044F\u043D\u0438 BTC-\u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 (\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442, \u043D\u0435\u0439\u0442\u0440\u0430\u043B\u0435\u043D \u0438\u043B\u0438 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u0435\u0442).
5837
- - \u041E\u0431\u044A\u044F\u0441\u043D\u0438, \u043F\u043E\u0447\u0435\u043C\u0443 quality \u0438\u043C\u0435\u043D\u043D\u043E \u0442\u0430\u043A\u043E\u0439.
5838
- - \u0415\u0441\u043B\u0438 \u043D\u0435 \u0432\u0445\u043E\u0434\u0438\u0448\u044C (direction=null), \u043F\u0440\u044F\u043C\u043E \u0443\u043A\u0430\u0436\u0438 \u0447\u0442\u043E \u0434\u043E\u043B\u0436\u043D\u043E \u0438\u0437\u043C\u0435\u043D\u0438\u0442\u044C\u0441\u044F \u0434\u043B\u044F \u0432\u0445\u043E\u0434\u0430.
5839
- - \u0412 "retestPlan" \u043D\u0435 \u043F\u0438\u0448\u0438 \u0442\u0435\u0445\u043D\u0438\u0447\u0435\u0441\u043A\u0438\u0439 \u0448\u0430\u0431\u043B\u043E\u043D \u0432\u0440\u043E\u0434\u0435 "needRetest=false @ null"; \u043F\u0438\u0448\u0438 \u0447\u0435\u043B\u043E\u0432\u0435\u0447\u0435\u0441\u043A\u043E\u0435 \u043E\u0431\u044A\u044F\u0441\u043D\u0435\u043D\u0438\u0435.
5840
- - \u041D\u0435 \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u0439 \u043F\u0440\u043E\u0441\u0442\u043E \u043F\u043E\u043B\u044F JSON; \u0434\u0430\u0439 \u0441\u043C\u044B\u0441\u043B \u0438 \u0440\u0435\u0448\u0435\u043D\u0438\u0435.
6346
+ Requirements for useful structured analysis:
6347
+ - Include 2-4 concrete factors for or against confirmation in \`confirmations\`.
6348
+ - Explicitly mention the role of the key figure or structural state, for example breakout, retest, false break, touch, or lack of confirmation.
6349
+ - Explicitly mention BTC context as supportive, neutral, or conflicting.
6350
+ - Explain why the quality score is what it is.
6351
+ - If the signal is not confirmed (\`direction=null\`), state clearly what must change for confirmation.
6352
+ - In \`retestPlan\`, avoid technical placeholders like \`needRetest=false @ null\`; write a human explanation.
6353
+ - Do not simply restate JSON fields; add interpretation and decision logic.
5841
6354
 
5842
- \u041F\u0440\u0430\u0432\u0438\u043B\u0430 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u044F \u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043D\u044B\u0445 \u0440\u044F\u0434\u043E\u0432 (last 5 values):
5843
- - \u041D\u0435 \u0434\u0435\u043B\u0430\u0439 \u0441\u0438\u043B\u044C\u043D\u044B\u0445 \u0432\u044B\u0432\u043E\u0434\u043E\u0432 \u043E \u0434\u043E\u043B\u0433\u043E\u0441\u0440\u043E\u0447\u043D\u043E\u0439 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u043E 5 \u0442\u043E\u0447\u043A\u0430\u043C.
5844
- - \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 4h/1d \u0440\u044F\u0434\u044B \u043A\u0430\u043A \u043A\u0440\u0430\u0442\u043A\u0438\u0439 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442, \u0430 \u043D\u0435 \u043A\u0430\u043A \u043F\u043E\u043B\u043D\u0443\u044E \u0438\u0441\u0442\u043E\u0440\u0438\u044E.
5845
- - \u0415\u0441\u043B\u0438 \u0434\u0430\u043D\u043D\u044B\u0445 \u043C\u0430\u043B\u043E \u0434\u043B\u044F \u0443\u0432\u0435\u0440\u0435\u043D\u043D\u043E\u0433\u043E \u0432\u044B\u0432\u043E\u0434\u0430, \u0441\u043D\u0438\u0436\u0430\u0439 quality \u0438 \u0444\u043E\u0440\u043C\u0443\u043B\u0438\u0440\u0443\u0439 \u0432\u044B\u0432\u043E\u0434 \u043E\u0441\u0442\u043E\u0440\u043E\u0436\u043D\u043E.
6355
+ Rules for using trimmed series (last 5 values):
6356
+ - Do not make strong long-term conclusions from only 5 points.
6357
+ - Use 4h and 1d series as brief context, not full history.
6358
+ - If the data is too limited for confidence, reduce quality and use cautious wording.
5846
6359
 
5847
- \u041A\u043E\u0440\u043E\u0442\u043A\u0438\u0435 \u043F\u0440\u0438\u043C\u0435\u0440\u044B (few-shot, \u0444\u043E\u0440\u043C\u0430\u0442 \u043E\u0442\u0432\u0435\u0442\u0430):
5848
- {"direction":"LONG","quality":4,"needRetest":true,"retestPrice":100.2,"takeProfitPrice":101.5,"stopLossPrice":98.9,"setup":"\u0412\u0435\u0440\u043E\u044F\u0442\u043D\u044B\u0439 \u043F\u0440\u043E\u0431\u043E\u0439 \u0442\u0440\u0435\u043D\u0434\u043E\u0432\u043E\u0439 \u0432\u0432\u0435\u0440\u0445, \u043D\u043E \u0432\u0445\u043E\u0434 \u0441\u0435\u0439\u0447\u0430\u0441 \u043B\u0443\u0447\u0448\u0435 \u043F\u043E\u0441\u043B\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0443\u0440\u043E\u0432\u043D\u044F.","confirmations":"\u041F\u043E \u043C\u043E\u043D\u0435\u0442\u0435 \u0435\u0441\u0442\u044C \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u043A\u0430 \u0438\u043C\u043F\u0443\u043B\u044C\u0441\u0430 \u0431\u0435\u0437 \u044F\u0432\u043D\u043E\u0433\u043E \u043F\u0435\u0440\u0435\u0433\u0440\u0435\u0432\u0430, \u043D\u043E \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u0435\u0449\u0435 \u043D\u0435 \u0438\u0434\u0435\u0430\u043B\u044C\u043D\u043E\u0435.","btcContext":"BTC \u043D\u0435\u0439\u0442\u0440\u0430\u043B\u044C\u043D\u043E \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0438\u0434\u0435\u044E \u0438 \u043D\u0435 \u043C\u0435\u0448\u0430\u0435\u0442 LONG.","retestPlan":"\u0416\u0434\u0430\u0442\u044C \u0440\u0435\u0442\u0435\u0441\u0442 \u0437\u043E\u043D\u044B 100.2 \u0438 \u0443\u0434\u0435\u0440\u0436\u0430\u043D\u0438\u0435 \u0432\u044B\u0448\u0435 \u043D\u0435\u0435 \u043F\u0435\u0440\u0435\u0434 \u0432\u0445\u043E\u0434\u043E\u043C.","riskLevels":"TP/SL \u043F\u043E \u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u044B\u0435 \u0441\u0442\u043E\u0440\u043E\u043D\u044B \u043E\u0442 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0446\u0435\u043D\u044B, \u0440\u0438\u0441\u043A \u043A\u043E\u043D\u0442\u0440\u043E\u043B\u0438\u0440\u0443\u0435\u043C\u044B\u0439.","qualityReason":"Quality=4: \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430 \u0445\u043E\u0440\u043E\u0448\u0430\u044F, \u043D\u043E \u043D\u0443\u0436\u0435\u043D \u0440\u0435\u0442\u0435\u0441\u0442 \u0434\u043B\u044F \u0431\u043E\u043B\u0435\u0435 \u0447\u0438\u0441\u0442\u043E\u0433\u043E timing.","triggerInvalidation":"\u0412\u0445\u043E\u0434 \u043F\u043E\u0441\u043B\u0435 \u0440\u0435\u0442\u0435\u0441\u0442\u0430 \u0438 \u0443\u0434\u0435\u0440\u0436\u0430\u043D\u0438\u044F \u0443\u0440\u043E\u0432\u043D\u044F; \u043E\u0442\u043C\u0435\u043D\u0430 \u043F\u0440\u0438 \u0432\u043E\u0437\u0432\u0440\u0430\u0442\u0435 \u043F\u043E\u0434 \u043B\u0438\u043D\u0438\u044E."}
5849
- {"direction":null,"quality":2,"needRetest":false,"retestPrice":null,"takeProfitPrice":null,"stopLossPrice":null,"setup":"\u041A\u0430\u0441\u0430\u043D\u0438\u0435/\u0448\u0443\u043C \u0443 \u0442\u0440\u0435\u043D\u0434\u043E\u0432\u043E\u0439 \u0431\u0435\u0437 \u0443\u0432\u0435\u0440\u0435\u043D\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0431\u043E\u044F.","confirmations":"\u0418\u043D\u0434\u0438\u043A\u0430\u0442\u043E\u0440\u044B \u0441\u043C\u0435\u0448\u0430\u043D\u043D\u044B\u0435 \u0438 \u043D\u0435 \u0434\u0430\u044E\u0442 \u0441\u0438\u043B\u044C\u043D\u043E\u0433\u043E \u043F\u0440\u0435\u0438\u043C\u0443\u0449\u0435\u0441\u0442\u0432\u0430.","btcContext":"BTC \u0441\u043A\u043E\u0440\u0435\u0435 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u0435\u0442 \u0438\u043B\u0438 \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0438\u0434\u0435\u044E.","retestPlan":"\u0420\u0435\u0442\u0435\u0441\u0442 \u043F\u043E\u043A\u0430 \u043D\u0435 \u043E\u0446\u0435\u043D\u0438\u0432\u0430\u0435\u043C, \u043F\u043E\u0442\u043E\u043C\u0443 \u0447\u0442\u043E \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u043D\u0443\u0436\u0435\u043D \u0441\u0430\u043C \u0444\u0430\u043A\u0442 \u043A\u0430\u0447\u0435\u0441\u0442\u0432\u0435\u043D\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0431\u043E\u044F.","riskLevels":"\u0421\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u0442 \u043A\u0430\u0447\u0435\u0441\u0442\u0432\u0435\u043D\u043D\u043E\u0433\u043E \u0441\u043E\u043E\u0442\u043D\u043E\u0448\u0435\u043D\u0438\u044F \u0434\u043B\u044F \u0432\u0445\u043E\u0434\u0430.","qualityReason":"Quality=2: timing \u0441\u043B\u0430\u0431\u044B\u0439 \u0438 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0439 \u043C\u0430\u043B\u043E.","triggerInvalidation":"\u0416\u0434\u0430\u0442\u044C \u044F\u0432\u043D\u044B\u0439 \u043F\u0440\u043E\u0431\u043E\u0439 \u0438 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u043F\u043E \u043C\u043E\u043D\u0435\u0442\u0435 \u0438 BTC."}
6360
+ Short few-shot examples:
6361
+ {"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."}
6362
+ {"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."}
5850
6363
 
5851
- \u0412\u0435\u0440\u043D\u0438 \u0442\u043E\u043B\u044C\u043A\u043E JSON-\u043E\u0431\u044A\u0435\u043A\u0442, \u0431\u0435\u0437 \u043B\u0438\u0448\u043D\u0438\u0445 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432.
6364
+ Return only the JSON object, with no extra characters.
5852
6365
  ${signal ? buildAiSystemPromptAddonByStrategy(signal) : ""}
5853
6366
  `;
5854
6367
  var buildAiPayload = (signal) => buildAiPayloadByStrategy(signal);
5855
6368
  var buildAiHumanPrompt = (signal, payload = buildAiPayload(signal)) => `
5856
- \u041F\u0440\u043E\u0430\u043D\u0430\u043B\u0438\u0437\u0438\u0440\u0443\u0439 \u0441\u0434\u0435\u043B\u043A\u0443 \u043F\u043E ${signal.symbol}. \u0418\u0441\u0445\u043E\u0434\u043D\u044B\u0439 \u0441\u0438\u0433\u043D\u0430\u043B \u0438\u043C\u0435\u0435\u0442 \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435 ${signal.direction}.
5857
- \u041E\u0446\u0435\u043D\u0438 \u0442\u0435\u043A\u0443\u0449\u0443\u044E \u0441\u0434\u0435\u043B\u043A\u0443 (\u0431\u0435\u0437 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u043F\u0440\u043E\u0442\u0438\u0432\u043E\u043F\u043E\u043B\u043E\u0436\u043D\u043E\u0433\u043E \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F): \u043E\u0434\u043E\u0431\u0440\u044F\u0435\u0448\u044C \u043B\u0438 \u0432\u0445\u043E\u0434 \u0441\u0435\u0439\u0447\u0430\u0441, \u043A\u0430\u0447\u0435\u0441\u0442\u0432\u043E \u0432\u0445\u043E\u0434\u0430 \u0441\u0435\u0439\u0447\u0430\u0441, \u043D\u0443\u0436\u0435\u043D \u043B\u0438 \u0440\u0435\u0442\u0435\u0441\u0442 \u0438 \u043F\u043E \u043A\u0430\u043A\u043E\u0439 \u0446\u0435\u043D\u0435 \u0435\u0433\u043E \u043E\u0446\u0435\u043D\u0438\u0432\u0430\u0442\u044C, \u0430 \u0442\u0430\u043A\u0436\u0435 \u0434\u0430\u0439 TP/SL \u0438 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u0441\u0442\u0440\u043E\u0433\u043E \u0432 \u0437\u0430\u0434\u0430\u043D\u043D\u043E\u043C JSON-\u0444\u043E\u0440\u043C\u0430\u0442\u0435.
6369
+ Analyze the already computed internal signal for ${signal.symbol}. The original signal direction is ${signal.direction}.
6370
+ 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.
5858
6371
 
5859
- \u0414\u0430\u043D\u043D\u044B\u0435 \u0441\u0434\u0435\u043B\u043A\u0438:
6372
+ Trade payload:
5860
6373
  ${JSON.stringify(payload)}
5861
6374
  ${buildAiHumanPromptAddonByStrategy(signal, payload)}
5862
6375
  `;
5863
- var askAI = async (signal) => {
5864
- const [{ ChatOpenAI }, { HumanMessage, SystemMessage }] = await Promise.all([
5865
- import("@langchain/openai"),
5866
- import("@langchain/core/messages")
6376
+ var DEFAULT_AI_MODEL = "openai/gpt-5-mini";
6377
+ var userSettingsCache = /* @__PURE__ */ new Map();
6378
+ var aiModelCache = /* @__PURE__ */ new Map();
6379
+ var getAiModelCacheKey = (userName, modelName) => `${userName}::${modelName}`;
6380
+ var resolveAiModelName = (settings, requestedModelName) => {
6381
+ const explicitModelName = typeof requestedModelName === "string" ? requestedModelName.trim() : "";
6382
+ if (explicitModelName) {
6383
+ return explicitModelName;
6384
+ }
6385
+ const settingsModelName = typeof settings.AI_MODEL === "string" ? settings.AI_MODEL.trim() : "";
6386
+ return settingsModelName || DEFAULT_AI_MODEL;
6387
+ };
6388
+ var getOpenRouterModelKwargs = (apiEndpoint) => {
6389
+ const endpoint = String(apiEndpoint ?? "").trim();
6390
+ if (!endpoint) {
6391
+ return {};
6392
+ }
6393
+ let hostname = "";
6394
+ try {
6395
+ hostname = new URL(endpoint).hostname;
6396
+ } catch {
6397
+ hostname = endpoint;
6398
+ }
6399
+ if (!hostname.toLowerCase().includes("openrouter")) {
6400
+ return {};
6401
+ }
6402
+ return {
6403
+ provider: {
6404
+ ignore: ["azure"]
6405
+ }
6406
+ };
6407
+ };
6408
+ var getAiSettings = async (userName = "root") => {
6409
+ let settingsPromise = userSettingsCache.get(userName);
6410
+ if (!settingsPromise) {
6411
+ settingsPromise = (0, import_userSettings.getUserSettings)(userName);
6412
+ settingsPromise.catch(() => {
6413
+ userSettingsCache.delete(userName);
6414
+ });
6415
+ userSettingsCache.set(userName, settingsPromise);
6416
+ }
6417
+ const settings = await settingsPromise;
6418
+ if (!settings.AI_API_KEY || !settings.AI_API_ENDPOINT) {
6419
+ throw new Error(`AI settings are incomplete for user ${userName}`);
6420
+ }
6421
+ return settings;
6422
+ };
6423
+ var createAiModel = async (userName = "root", requestedModelName) => {
6424
+ const settings = await getAiSettings(userName);
6425
+ const modelName = resolveAiModelName(settings, requestedModelName);
6426
+ const cacheKey = getAiModelCacheKey(userName, modelName);
6427
+ let modelPromise = aiModelCache.get(cacheKey);
6428
+ if (!modelPromise) {
6429
+ modelPromise = (async () => {
6430
+ const { ChatOpenAI } = await import("@langchain/openai");
6431
+ const modelKwargs = getOpenRouterModelKwargs(settings.AI_API_ENDPOINT);
6432
+ return new ChatOpenAI({
6433
+ temperature: 0.2,
6434
+ modelName,
6435
+ apiKey: settings.AI_API_KEY,
6436
+ ...Object.keys(modelKwargs).length ? { modelKwargs } : {},
6437
+ configuration: {
6438
+ baseURL: settings.AI_API_ENDPOINT,
6439
+ defaultHeaders: {
6440
+ "HTTP-Referer": "https://tradejs.dev",
6441
+ "X-Title": "Inv"
6442
+ }
6443
+ }
6444
+ });
6445
+ })();
6446
+ modelPromise.catch(() => {
6447
+ aiModelCache.delete(cacheKey);
6448
+ });
6449
+ aiModelCache.set(cacheKey, modelPromise);
6450
+ }
6451
+ return modelPromise;
6452
+ };
6453
+ var getAiModel = async (userName = "root", requestedModelName) => {
6454
+ const settings = await getAiSettings(userName);
6455
+ const resolvedModelName = resolveAiModelName(settings, requestedModelName);
6456
+ try {
6457
+ return await createAiModel(userName, resolvedModelName);
6458
+ } catch (error) {
6459
+ aiModelCache.delete(getAiModelCacheKey(userName, resolvedModelName));
6460
+ userSettingsCache.delete(userName);
6461
+ throw error;
6462
+ }
6463
+ };
6464
+ var ensureAiStrategyPluginsLoaded = async () => {
6465
+ await ensureStrategyPluginsLoaded();
6466
+ };
6467
+ var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
6468
+ if (options.signal) {
6469
+ await ensureAiStrategyPluginsLoaded();
6470
+ }
6471
+ const [{ HumanMessage, SystemMessage }, model, settings] = await Promise.all([
6472
+ import("@langchain/core/messages"),
6473
+ getAiModel(options.userName, options.model),
6474
+ getAiSettings(options.userName)
5867
6475
  ]);
5868
- const { symbol } = signal;
5869
6476
  const messages = [];
5870
- const model = new ChatOpenAI({
5871
- temperature: 0.2,
5872
- //modelName: 'anthropic/claude-sonnet-4.5',
5873
- modelName: "google/gemini-3.1-pro-preview",
5874
- openAIApiKey: process.env.OPENAI_API_KEY,
5875
- configuration: {
5876
- baseURL: process.env.OPENAI_API_ENDPOINT || "https://api.openai.com/v1",
5877
- defaultHeaders: {
5878
- "HTTP-Referer": "https://tradejs.dev",
5879
- "X-Title": "Inv"
5880
- }
5881
- }
5882
- });
5883
- messages.push(new SystemMessage(buildAiSystemPrompt(signal)));
5884
- const payload = buildAiPayload(signal);
6477
+ const responseLanguage = (0, import_aiLanguages.getAiResponseLanguagePromptName)(
6478
+ settings.AI_RESPONSE_LANGUAGE || import_aiLanguages.DEFAULT_AI_RESPONSE_LANGUAGE
6479
+ );
6480
+ messages.push(new SystemMessage(systemPrompt));
6481
+ messages.push(
6482
+ new SystemMessage(
6483
+ `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
6484
+ )
6485
+ );
5885
6486
  messages.push(
5886
6487
  new HumanMessage({
5887
6488
  content: [
5888
6489
  {
5889
6490
  type: "text",
5890
- text: buildAiHumanPrompt(signal, payload)
6491
+ text: humanPrompt
5891
6492
  }
5892
6493
  ]
5893
6494
  })
@@ -5896,7 +6497,31 @@ var askAI = async (signal) => {
5896
6497
  const parsed = parseAIResponse(
5897
6498
  normalizeResponseContent(response.content)
5898
6499
  );
5899
- const content = normalizeAnalysis(parsed);
6500
+ const normalized = normalizeAnalysis(parsed);
6501
+ if (!options.signal) {
6502
+ return normalized;
6503
+ }
6504
+ return postProcessAiAnalysisByStrategy(
6505
+ options.signal,
6506
+ normalized,
6507
+ options.payload
6508
+ );
6509
+ };
6510
+ var askAI = async (signal, options = {}) => {
6511
+ const { symbol } = signal;
6512
+ await ensureAiStrategyPluginsLoaded();
6513
+ const payload = buildAiPayload(signal);
6514
+ const content = await runAiPrompt(
6515
+ {
6516
+ systemPrompt: buildAiSystemPrompt(signal),
6517
+ humanPrompt: buildAiHumanPrompt(signal, payload)
6518
+ },
6519
+ {
6520
+ ...options,
6521
+ signal,
6522
+ payload
6523
+ }
6524
+ );
5900
6525
  await (0, import_redis.setData)(import_redis.redisKeys.analysis(symbol, signal.signalId), content);
5901
6526
  return content;
5902
6527
  };
@@ -5926,14 +6551,60 @@ var SCREENSHOT_VIEWPORT = {
5926
6551
  };
5927
6552
  var getProjectRoot = (projectRoot) => import_path2.default.resolve(getTradejsProjectCwd(projectRoot));
5928
6553
  var getScreenshotsDir = (projectRoot) => import_path2.default.join(getProjectRoot(projectRoot), "data", "screenshots");
5929
- var maskTokenInUrl = (url) => url.replace(/([?&]token=)[^&]+/i, "$1<hidden>");
6554
+ var maskTokenInUrl = (url) => url.replace(/([?&]screenshotToken=)[^&]+/i, "$1<hidden>");
6555
+ var getErrorFields = (value) => {
6556
+ if (!value || typeof value !== "object") {
6557
+ return [];
6558
+ }
6559
+ const record = value;
6560
+ const fields = [
6561
+ ["name", record.name],
6562
+ ["code", record.code],
6563
+ ["errno", record.errno],
6564
+ ["type", record.type],
6565
+ ["syscall", record.syscall],
6566
+ ["hostname", record.hostname],
6567
+ ["host", record.host],
6568
+ ["address", record.address],
6569
+ ["port", record.port]
6570
+ ];
6571
+ return fields.filter(([, fieldValue]) => fieldValue != null && String(fieldValue).trim()).map(([key, fieldValue]) => `${key}=${String(fieldValue)}`);
6572
+ };
6573
+ var describeErrorValue = (value) => {
6574
+ if (value == null) {
6575
+ return "";
6576
+ }
6577
+ if (typeof value === "string") {
6578
+ return value;
6579
+ }
6580
+ if (value instanceof Error) {
6581
+ const message = value.message?.trim() || "";
6582
+ const fields = getErrorFields(value);
6583
+ return [message, ...fields].filter(Boolean).join(" ");
6584
+ }
6585
+ if (typeof value === "object") {
6586
+ const fields = getErrorFields(value);
6587
+ if (fields.length) {
6588
+ return fields.join(" ");
6589
+ }
6590
+ try {
6591
+ return JSON.stringify(value);
6592
+ } catch {
6593
+ return String(value);
6594
+ }
6595
+ }
6596
+ return String(value);
6597
+ };
5930
6598
  var getErrorMessage = (error) => {
5931
6599
  const maybeError = error;
5932
- const message = maybeError?.message || String(error);
6600
+ const message = describeErrorValue(error) || String(error);
5933
6601
  if (maybeError?.cause == null) {
5934
6602
  return message;
5935
6603
  }
5936
- const cause = maybeError.cause instanceof Error ? maybeError.cause.message : String(maybeError.cause);
6604
+ const cause = describeErrorValue(maybeError.cause);
6605
+ if (!cause) {
6606
+ return message;
6607
+ }
5937
6608
  return `${message}; cause: ${cause}`;
5938
6609
  };
5939
6610
  var truncateText = (value, maxLength = SCREENSHOT_CONSOLE_TEXT_LIMIT) => value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
@@ -5960,14 +6631,17 @@ var getScreenshotPath = ({ symbol, signalId, interval }, projectRoot) => {
5960
6631
  getScreenshotFilename({ symbol, signalId, interval })
5961
6632
  );
5962
6633
  };
5963
- var screenDashboard = async (signal, projectRoot) => {
6634
+ var screenDashboard = async (signal, projectRoot, userName = "root") => {
5964
6635
  const { symbol, signalId, interval } = signal;
5965
6636
  const screenshotBaseUrl = getScreenshotRenderBaseUrl();
5966
6637
  const screenshotPath = getScreenshotPath(signal, projectRoot);
5967
- const rootUser = await (0, import_redis2.getData)(import_redis2.redisKeys.user("root"), null);
5968
- const token2 = rootUser && typeof rootUser === "object" ? rootUser.token : null;
5969
- const tokenParam = typeof token2 === "string" && token2.length > 0 ? `&token=${encodeURIComponent(token2)}` : "";
5970
- const dashboardUrl = `${screenshotBaseUrl}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}&autoZoom=true&screenshot=1${tokenParam}`;
6638
+ const screenshotToken = await (0, import_redis2.createScreenshotSessionToken)(userName);
6639
+ if (!screenshotToken) {
6640
+ throw new Error(
6641
+ `Failed to create screenshot session token for ${userName}`
6642
+ );
6643
+ }
6644
+ const dashboardUrl = `${screenshotBaseUrl}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}&autoZoom=true&screenshot=1&screenshotToken=${encodeURIComponent(screenshotToken)}`;
5971
6645
  const maskedDashboardUrl = maskTokenInUrl(dashboardUrl);
5972
6646
  import_logger3.logger.info(
5973
6647
  "screenshot start: %s %sm url=%s path=%s",
@@ -5991,6 +6665,7 @@ var screenDashboard = async (signal, projectRoot) => {
5991
6665
  ]
5992
6666
  });
5993
6667
  const browserProcess = browser.process();
6668
+ let browserCloseRequested = false;
5994
6669
  try {
5995
6670
  if (browserProcess) {
5996
6671
  import_logger3.logger.info(
@@ -6010,6 +6685,9 @@ var screenDashboard = async (signal, projectRoot) => {
6010
6685
  await browser.version()
6011
6686
  );
6012
6687
  browser.on("disconnected", () => {
6688
+ if (browserCloseRequested) {
6689
+ return;
6690
+ }
6013
6691
  import_logger3.logger.error(
6014
6692
  "screenshot browser disconnected: %s %sm captureAttempt=%d",
6015
6693
  symbol,
@@ -6208,6 +6886,7 @@ var screenDashboard = async (signal, projectRoot) => {
6208
6886
  await (0, import_async.delay)(SCREENSHOT_CAPTURE_RETRY_DELAY_MS);
6209
6887
  }
6210
6888
  } finally {
6889
+ browserCloseRequested = true;
6211
6890
  await browser.close().catch(() => void 0);
6212
6891
  }
6213
6892
  }
@@ -6218,14 +6897,105 @@ var screenDashboard = async (signal, projectRoot) => {
6218
6897
  var import_async2 = require("@tradejs/core/async");
6219
6898
  var import_math = require("@tradejs/core/math");
6220
6899
  var import_logger4 = require("@tradejs/infra/logger");
6900
+ var import_userSettings2 = require("@tradejs/infra/userSettings");
6221
6901
  var escapeHtml = (s) => {
6222
6902
  if (!s) return "";
6223
6903
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6224
6904
  };
6225
- var { APP_URL: APP_URL2, TG_BOT_TOKEN: token, TG_CHAT_ID: chatId } = process.env;
6905
+ var { APP_URL: APP_URL2 } = process.env;
6226
6906
  var TG_REQUEST_ATTEMPTS = 3;
6227
6907
  var TG_REQUEST_RETRY_DELAY_MS = 2e3;
6908
+ var getErrorFields2 = (value) => {
6909
+ if (!value || typeof value !== "object") {
6910
+ return [];
6911
+ }
6912
+ const record = value;
6913
+ const fields = [
6914
+ ["name", record.name],
6915
+ ["code", record.code],
6916
+ ["errno", record.errno],
6917
+ ["type", record.type],
6918
+ ["syscall", record.syscall],
6919
+ ["hostname", record.hostname],
6920
+ ["host", record.host],
6921
+ ["address", record.address],
6922
+ ["port", record.port]
6923
+ ];
6924
+ return fields.filter(([, fieldValue]) => fieldValue != null && String(fieldValue).trim()).map(([key, fieldValue]) => `${key}=${String(fieldValue)}`);
6925
+ };
6926
+ var describeErrorValue2 = (value) => {
6927
+ if (value == null) {
6928
+ return "";
6929
+ }
6930
+ if (typeof value === "string") {
6931
+ return value;
6932
+ }
6933
+ if (value instanceof Error) {
6934
+ const message = value.message?.trim() || "";
6935
+ const fields = getErrorFields2(value);
6936
+ return [message, ...fields].filter(Boolean).join(" ");
6937
+ }
6938
+ if (typeof value === "object") {
6939
+ const fields = getErrorFields2(value);
6940
+ if (fields.length) {
6941
+ return fields.join(" ");
6942
+ }
6943
+ try {
6944
+ return JSON.stringify(value);
6945
+ } catch {
6946
+ return String(value);
6947
+ }
6948
+ }
6949
+ return String(value);
6950
+ };
6228
6951
  var normalizeQuality = (value) => typeof value === "number" ? Math.max(1, Math.min(5, Math.round(value))) : null;
6952
+ var formatAnalysisLevel = (value) => {
6953
+ if (value == null || !Number.isFinite(value)) return null;
6954
+ if (Math.abs(value) >= 1e3) {
6955
+ return value.toFixed(0);
6956
+ }
6957
+ const digits = Math.abs(value) < 1 ? 8 : 6;
6958
+ return value.toFixed(digits).replace(/\.?0+$/, "");
6959
+ };
6960
+ var normalizeAnalysisText = (value) => {
6961
+ if (typeof value !== "string") return null;
6962
+ const clean = value.replace(/\s+/g, " ").trim();
6963
+ return clean || null;
6964
+ };
6965
+ var getReadableAnalysisComment = (value) => {
6966
+ const clean = normalizeAnalysisText(value);
6967
+ if (!clean) return null;
6968
+ if (/^(ok|approved|approved by ai|historical approval|historical reject)$/i.test(
6969
+ clean
6970
+ )) {
6971
+ return null;
6972
+ }
6973
+ return clean;
6974
+ };
6975
+ var takeUniqueAnalysisText = (usedValues, ...values) => {
6976
+ for (const value of values) {
6977
+ const clean = normalizeAnalysisText(value);
6978
+ if (!clean) continue;
6979
+ const key = clean.toLowerCase();
6980
+ if (usedValues.has(key)) continue;
6981
+ usedValues.add(key);
6982
+ return clean;
6983
+ }
6984
+ return null;
6985
+ };
6986
+ var formatOrderSkipReason = (reason) => {
6987
+ if (!reason) return "";
6988
+ if (reason.startsWith("AI_QUALITY_BELOW_MIN")) {
6989
+ return "AI_QUALITY_BELOW_MIN";
6990
+ }
6991
+ if (reason.startsWith("ML_THRESHOLD_NOT_MET")) {
6992
+ return "ML_THRESHOLD_NOT_MET";
6993
+ }
6994
+ if (reason === "ML_RESULT_UNAVAILABLE") {
6995
+ return "ML_RESULT_UNAVAILABLE";
6996
+ }
6997
+ return reason;
6998
+ };
6229
6999
  var getLastNumber = (value) => {
6230
7000
  if (Array.isArray(value)) {
6231
7001
  const last = value[value.length - 1];
@@ -6265,11 +7035,14 @@ var getTelegramErrorReason = (data) => {
6265
7035
  };
6266
7036
  var getErrorMessage2 = (error) => {
6267
7037
  const maybeError = error;
6268
- const message = maybeError?.message || String(error);
7038
+ const message = describeErrorValue2(error) || String(error);
6269
7039
  if (maybeError?.cause == null) {
6270
7040
  return message;
6271
7041
  }
6272
- const cause = maybeError.cause instanceof Error ? maybeError.cause.message : String(maybeError.cause);
7042
+ const cause = describeErrorValue2(maybeError.cause);
7043
+ if (!cause) {
7044
+ return message;
7045
+ }
6273
7046
  return `${message}; cause: ${cause}`;
6274
7047
  };
6275
7048
  var parseTelegramResponse = async (response) => {
@@ -6292,7 +7065,20 @@ var parseTelegramResponse = async (response) => {
6292
7065
  }
6293
7066
  }
6294
7067
  };
6295
- var requestTelegram = async (method, init) => {
7068
+ var getTelegramSettings = async (userName = "root") => {
7069
+ const settings = await (0, import_userSettings2.getUserSettings)(userName);
7070
+ const token = settings.TG_BOT_TOKEN;
7071
+ const chatId = settings.TG_CHAT_ID;
7072
+ if (!token || !chatId) {
7073
+ throw new Error(`Telegram settings are incomplete for user ${userName}`);
7074
+ }
7075
+ return { token, chatId };
7076
+ };
7077
+ var requestTelegram = async ({
7078
+ method,
7079
+ token,
7080
+ init
7081
+ }) => {
6296
7082
  let lastError = null;
6297
7083
  for (let attempt = 1; attempt <= TG_REQUEST_ATTEMPTS; attempt += 1) {
6298
7084
  try {
@@ -6320,17 +7106,23 @@ var requestTelegram = async (method, init) => {
6320
7106
  };
6321
7107
  var sendTelegramMessage = async ({
6322
7108
  message,
6323
- markup
7109
+ markup,
7110
+ token,
7111
+ chatId
6324
7112
  }) => {
6325
- const data = await requestTelegram("sendMessage", {
6326
- method: "POST",
6327
- headers: { "Content-Type": "application/json" },
6328
- body: JSON.stringify({
6329
- chat_id: chatId,
6330
- text: message,
6331
- reply_markup: markup,
6332
- parse_mode: "HTML"
6333
- })
7113
+ const data = await requestTelegram({
7114
+ method: "sendMessage",
7115
+ token,
7116
+ init: {
7117
+ method: "POST",
7118
+ headers: { "Content-Type": "application/json" },
7119
+ body: JSON.stringify({
7120
+ chat_id: chatId,
7121
+ text: message,
7122
+ reply_markup: markup,
7123
+ parse_mode: "HTML"
7124
+ })
7125
+ }
6334
7126
  });
6335
7127
  import_logger4.logger.info(
6336
7128
  "tg sendMessage: %s",
@@ -6338,6 +7130,15 @@ var sendTelegramMessage = async ({
6338
7130
  );
6339
7131
  return data;
6340
7132
  };
7133
+ var sendTextToTG = async (message, options = {}) => {
7134
+ const { token, chatId } = await getTelegramSettings(options.userName);
7135
+ return sendTelegramMessage({
7136
+ message,
7137
+ markup: options.markup,
7138
+ token,
7139
+ chatId
7140
+ });
7141
+ };
6341
7142
  var formatMessage = (signal, analysis) => {
6342
7143
  const {
6343
7144
  symbol,
@@ -6389,7 +7190,9 @@ var formatMessage = (signal, analysis) => {
6389
7190
  }
6390
7191
  lines.push(orderStatusText);
6391
7192
  if ((orderStatus === "skipped" || orderStatus === "canceled") && orderSkipReason) {
6392
- lines.push(`Skip reason: <b>${escapeHtml(orderSkipReason)}</b>`);
7193
+ lines.push(
7194
+ `Skip reason: <b>${escapeHtml(formatOrderSkipReason(orderSkipReason))}</b>`
7195
+ );
6393
7196
  }
6394
7197
  }
6395
7198
  if (isConfigFromBacktest) {
@@ -6407,21 +7210,9 @@ var formatMessage = (signal, analysis) => {
6407
7210
  lines.push(aiQualityLine);
6408
7211
  }
6409
7212
  lines.push("");
6410
- if (touches) {
6411
- lines.push(`Points: ${touches}`);
6412
- }
6413
- if (atrPct != null && Number.isFinite(atrPct)) {
6414
- lines.push(`ATR: ${atrPct.toFixed(2)}`);
6415
- }
6416
- if (distance) {
6417
- lines.push(`Distance: ${distance}`);
6418
- }
6419
7213
  if (correlation) {
6420
7214
  lines.push(`BTC correlation: ${correlation}`);
6421
7215
  }
6422
- if (spread != null && Number.isFinite(spread)) {
6423
- lines.push(`BTC spread (CB-BN)/BN: ${spread.toFixed(6)}`);
6424
- }
6425
7216
  const prices = formatPrices();
6426
7217
  if (prices) {
6427
7218
  lines.push("");
@@ -6435,8 +7226,9 @@ var formatMessage = (signal, analysis) => {
6435
7226
  \u0414\u0435\u0442\u0430\u043B\u0438: ${err.message || String(err)}`;
6436
7227
  }
6437
7228
  };
6438
- var sendSignal = async (signal, imgInterval, analysis) => {
7229
+ var sendSignal = async (signal, imgInterval, analysis, options = {}) => {
6439
7230
  const { symbol, signalId, interval } = signal;
7231
+ const { token, chatId } = await getTelegramSettings(options.userName);
6440
7232
  const message = formatMessage(signal, analysis);
6441
7233
  const publicAppUrl = APP_URL2?.startsWith("https") ? APP_URL2 : null;
6442
7234
  const dashboardUrl = publicAppUrl ? `${APP_URL2}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}` : null;
@@ -6462,7 +7254,7 @@ var sendSignal = async (signal, imgInterval, analysis) => {
6462
7254
  symbol,
6463
7255
  error?.message || String(error)
6464
7256
  );
6465
- await sendTelegramMessage({ message, markup });
7257
+ await sendTelegramMessage({ message, markup, token, chatId });
6466
7258
  return;
6467
7259
  }
6468
7260
  const photoBody = new FormData();
@@ -6479,9 +7271,13 @@ var sendSignal = async (signal, imgInterval, analysis) => {
6479
7271
  }
6480
7272
  let data;
6481
7273
  try {
6482
- data = await requestTelegram("sendPhoto", {
6483
- method: "POST",
6484
- body: photoBody
7274
+ data = await requestTelegram({
7275
+ method: "sendPhoto",
7276
+ token,
7277
+ init: {
7278
+ method: "POST",
7279
+ body: photoBody
7280
+ }
6485
7281
  });
6486
7282
  } catch (error) {
6487
7283
  const reason = getErrorMessage2(error);
@@ -6491,7 +7287,9 @@ var sendSignal = async (signal, imgInterval, analysis) => {
6491
7287
 
6492
7288
  \u26A0\uFE0F <b>Photo delivery failed</b>
6493
7289
  Reason: <code>${escapeHtml(reason)}</code>`,
6494
- markup
7290
+ markup,
7291
+ token,
7292
+ chatId
6495
7293
  });
6496
7294
  return;
6497
7295
  }
@@ -6503,7 +7301,9 @@ Reason: <code>${escapeHtml(reason)}</code>`,
6503
7301
 
6504
7302
  \u26A0\uFE0F <b>Photo delivery failed</b>
6505
7303
  Reason: <code>${escapeHtml(reason)}</code>`,
6506
- markup
7304
+ markup,
7305
+ token,
7306
+ chatId
6507
7307
  });
6508
7308
  return;
6509
7309
  }
@@ -6511,56 +7311,96 @@ Reason: <code>${escapeHtml(reason)}</code>`,
6511
7311
  };
6512
7312
  var formatAnalysisMessage = (signal, analysis) => {
6513
7313
  const lines = [];
6514
- const blocks = [];
6515
7314
  const quality = normalizeQuality(analysis.quality);
7315
+ const readableComment = getReadableAnalysisComment(analysis.comment);
7316
+ const approvedDirection = analysis.direction != null && analysis.direction === signal.direction && analysis.needRetest !== true;
7317
+ const oppositeDirection = analysis.direction != null && analysis.direction !== signal.direction;
7318
+ const verdict = approvedDirection ? `Approved ${signal.direction}` : oppositeDirection ? `Not approved: AI prefers ${analysis.direction}` : analysis.needRetest ? "Not approved yet" : "Not approved";
7319
+ const summary = approvedDirection ? `AI approves this ${signal.direction} setup right now.` : oppositeDirection ? `AI does not approve this ${signal.direction} setup and currently leans ${analysis.direction}.` : analysis.needRetest ? `The ${signal.direction} setup is visible, but confirmation is still missing before entry.` : `AI does not approve this ${signal.direction} setup right now.`;
7320
+ const usedValues = /* @__PURE__ */ new Set();
6516
7321
  lines.push(`<b>AI analysis ${signal.symbol}</b>`);
6517
- lines.push(`Signal direction: <b>${signal.direction}</b>`);
6518
- lines.push(`AI direction: <b>${analysis.direction ?? "NO TRADE"}</b>`);
7322
+ lines.push(`Verdict: <b>${escapeHtml(verdict)}</b>`);
7323
+ lines.push(escapeHtml(summary));
6519
7324
  if (quality) {
6520
7325
  lines.push(`Quality: <b>${quality}/5</b>`);
6521
7326
  }
6522
- if (typeof analysis.needRetest === "boolean") {
6523
- lines.push(`Need retest: <b>${analysis.needRetest ? "YES" : "NO"}</b>`);
7327
+ if (analysis.gateDecision && analysis.llmDecision) {
7328
+ lines.push(
7329
+ `Gate vs LLM: <b>${analysis.gateContradictsLlm ? "conflict" : "aligned"}</b> (gate ${analysis.gateDecision}, LLM ${analysis.llmDecision})`
7330
+ );
6524
7331
  }
6525
- if (typeof analysis.retestPrice === "number") {
6526
- lines.push(`Retest price: <b>${(0, import_math.formatNumber)(analysis.retestPrice)}</b>`);
7332
+ const happeningText = takeUniqueAnalysisText(
7333
+ usedValues,
7334
+ analysis.setup,
7335
+ readableComment
7336
+ );
7337
+ if (happeningText) {
7338
+ lines.push(`What's happening: ${escapeHtml(happeningText)}`);
7339
+ }
7340
+ const reasonText = approvedDirection ? takeUniqueAnalysisText(
7341
+ usedValues,
7342
+ analysis.qualityReason,
7343
+ analysis.confirmations,
7344
+ readableComment
7345
+ ) : takeUniqueAnalysisText(
7346
+ usedValues,
7347
+ analysis.qualityReason,
7348
+ analysis.confirmations,
7349
+ readableComment,
7350
+ analysis.btcContext
7351
+ );
7352
+ if (reasonText) {
7353
+ lines.push(
7354
+ `${approvedDirection ? "Why approved" : "Why not approved"}: ${escapeHtml(reasonText)}`
7355
+ );
7356
+ }
7357
+ const nextText = analysis.needRetest ? takeUniqueAnalysisText(
7358
+ usedValues,
7359
+ analysis.retestPlan,
7360
+ analysis.triggerInvalidation,
7361
+ typeof analysis.retestPrice === "number" ? `Wait for confirmation around ${formatAnalysisLevel(analysis.retestPrice)}.` : null
7362
+ ) : takeUniqueAnalysisText(
7363
+ usedValues,
7364
+ analysis.triggerInvalidation,
7365
+ analysis.retestPlan
7366
+ );
7367
+ if (nextText) {
7368
+ lines.push(`Next: ${escapeHtml(nextText)}`);
7369
+ }
7370
+ const btcText = takeUniqueAnalysisText(usedValues, analysis.btcContext);
7371
+ if (btcText) {
7372
+ lines.push(`BTC context: ${escapeHtml(btcText)}`);
6527
7373
  }
7374
+ const levels = [];
6528
7375
  if (typeof analysis.takeProfitPrice === "number") {
6529
- lines.push(`AI TP: <b>${(0, import_math.formatNumber)(analysis.takeProfitPrice)}</b>`);
7376
+ levels.push(`TP <b>${formatAnalysisLevel(analysis.takeProfitPrice)}</b>`);
6530
7377
  }
6531
7378
  if (typeof analysis.stopLossPrice === "number") {
6532
- lines.push(`AI SL: <b>${(0, import_math.formatNumber)(analysis.stopLossPrice)}</b>`);
7379
+ levels.push(`SL <b>${formatAnalysisLevel(analysis.stopLossPrice)}</b>`);
6533
7380
  }
6534
- const pushBlock = (title, value) => {
6535
- if (!value) return;
6536
- const clean = value.trim();
6537
- if (!clean) return;
6538
- blocks.push(`<b>${title}:</b>
6539
- ${escapeHtml(clean)}`);
6540
- };
6541
- pushBlock("Setup", analysis.setup);
6542
- pushBlock("Confirmations", analysis.confirmations);
6543
- pushBlock("BTC", analysis.btcContext);
6544
- pushBlock("Retest", analysis.retestPlan);
6545
- pushBlock("Risk/Levels", analysis.riskLevels);
6546
- pushBlock("Why Quality", analysis.qualityReason);
6547
- pushBlock("Trigger/Invalidation", analysis.triggerInvalidation);
6548
- if (blocks.length > 0) {
6549
- lines.push("");
6550
- lines.push(blocks.join("\n\n"));
7381
+ if (typeof analysis.retestPrice === "number") {
7382
+ levels.push(`Retest <b>${formatAnalysisLevel(analysis.retestPrice)}</b>`);
7383
+ }
7384
+ if (levels.length > 0) {
7385
+ lines.push(`Levels: ${levels.join(" | ")}`);
6551
7386
  }
6552
7387
  return lines.join("\n");
6553
7388
  };
6554
- var sendSignalAnalysis = async (signal, analysis) => {
7389
+ var sendSignalAnalysis = async (signal, analysis, options = {}) => {
7390
+ const { token, chatId } = await getTelegramSettings(options.userName);
6555
7391
  const message = formatAnalysisMessage(signal, analysis);
6556
- const data = await requestTelegram("sendMessage", {
6557
- method: "POST",
6558
- headers: { "Content-Type": "application/json" },
6559
- body: JSON.stringify({
6560
- chat_id: chatId,
6561
- text: message,
6562
- parse_mode: "HTML"
6563
- })
7392
+ const data = await requestTelegram({
7393
+ method: "sendMessage",
7394
+ token,
7395
+ init: {
7396
+ method: "POST",
7397
+ headers: { "Content-Type": "application/json" },
7398
+ body: JSON.stringify({
7399
+ chat_id: chatId,
7400
+ text: message,
7401
+ parse_mode: "HTML"
7402
+ })
7403
+ }
6564
7404
  });
6565
7405
  import_logger4.logger.info(
6566
7406
  "tg sendMessage (analysis): %s",
@@ -6625,9 +7465,18 @@ var cleanRedis = async (area) => {
6625
7465
  }
6626
7466
  import_logger5.logger.info("");
6627
7467
  };
6628
- var update = async (connector, interval, tickers) => {
6629
- const PRELOAD_START = (0, import_time.getTimestamp)(import_constants.PRELOAD_DAYS);
6630
- const PRELOAD_END = (0, import_time.getTimestamp)();
7468
+ var update = async (connector, interval, tickers, preloadDays = import_constants.PRELOAD_DAYS, options = {}) => {
7469
+ const preloadStart = Math.trunc(
7470
+ options.preloadStart ?? (0, import_time.getTimestamp)(preloadDays)
7471
+ );
7472
+ const preloadEnd = Math.trunc(options.preloadEnd ?? (0, import_time.getTimestamp)());
7473
+ const connectorLabel = String(options.connectorLabel || "").trim();
7474
+ const preloadLabel = options.preloadStart != null || options.preloadEnd != null ? `preloadStart=${preloadStart}, preloadEnd=${preloadEnd}` : `preloadDays=${preloadDays}`;
7475
+ if (preloadStart >= preloadEnd) {
7476
+ throw new Error(
7477
+ `Invalid update preload window: start (${preloadStart}) must be less than end (${preloadEnd})`
7478
+ );
7479
+ }
6631
7480
  const bar = new import_progress.default(
6632
7481
  ":current/:total [:bar][:percent] :eta(s) :symbol",
6633
7482
  {
@@ -6637,7 +7486,7 @@ var update = async (connector, interval, tickers) => {
6637
7486
  );
6638
7487
  import_logger5.logger.info(
6639
7488
  import_chalk.default.yellow(
6640
- `update: ${tickers.length} (klineConcurrency=${KLINE_CONCURRENCY_LIMIT})`
7489
+ `update: ${tickers.length} (connector=${connectorLabel || "unknown"}, klineConcurrency=${KLINE_CONCURRENCY_LIMIT}, ${preloadLabel})`
6641
7490
  )
6642
7491
  );
6643
7492
  const queue = tickers.slice();
@@ -6648,10 +7497,11 @@ var update = async (connector, interval, tickers) => {
6648
7497
  try {
6649
7498
  await connector.kline({
6650
7499
  symbol,
6651
- start: PRELOAD_START,
6652
- end: PRELOAD_END,
7500
+ start: preloadStart,
7501
+ end: preloadEnd,
6653
7502
  interval,
6654
- silent: true
7503
+ silent: true,
7504
+ warmOnly: true
6655
7505
  });
6656
7506
  } catch {
6657
7507
  import_logger5.logger.error("Failed loading: %s", symbol);
@@ -6704,7 +7554,7 @@ var getTickers = async (connector, include = "", exclude = "", limit, chunk) =>
6704
7554
  }
6705
7555
  return tickers.filter((t) => !excludeTickers.includes(t));
6706
7556
  };
6707
- var makeScreenshots = async (signals, interval) => {
7557
+ var makeScreenshots = async (signals, interval, userName = "root") => {
6708
7558
  const projectRoot = getProjectRoot2();
6709
7559
  const bar = new import_progress.default(
6710
7560
  ":current/:total [:bar][:percent] :eta(s) :symbol",
@@ -6719,7 +7569,7 @@ var makeScreenshots = async (signals, interval) => {
6719
7569
  SCREENSHOT_CONCURRENCY_LIMIT,
6720
7570
  async (signal) => {
6721
7571
  try {
6722
- await screenDashboard({ ...signal, interval }, projectRoot);
7572
+ await screenDashboard({ ...signal, interval }, projectRoot, userName);
6723
7573
  } catch (error) {
6724
7574
  import_logger5.logger.error(
6725
7575
  "Failed screenshot: %s (%s)",
@@ -6733,7 +7583,7 @@ var makeScreenshots = async (signals, interval) => {
6733
7583
  );
6734
7584
  import_logger5.logger.info("");
6735
7585
  };
6736
- var sendToAI = async (signals) => {
7586
+ var sendToAI = async (signals, userName = "root") => {
6737
7587
  const bar = new import_progress.default(
6738
7588
  ":current/:total [:bar][:percent] :eta(s) :symbol",
6739
7589
  {
@@ -6744,7 +7594,7 @@ var sendToAI = async (signals) => {
6744
7594
  import_logger5.logger.info(import_chalk.default.yellow("AI:", signals.length));
6745
7595
  await (0, import_async3.runWithConcurrency)(signals, AI_CONCURRENCY_LIMIT, async (signal) => {
6746
7596
  try {
6747
- await askAI(signal);
7597
+ await askAI(signal, { userName });
6748
7598
  } catch {
6749
7599
  import_logger5.logger.error("Failed ask: %s", signal.symbol);
6750
7600
  } finally {
@@ -6753,24 +7603,31 @@ var sendToAI = async (signals) => {
6753
7603
  });
6754
7604
  import_logger5.logger.info("");
6755
7605
  };
6756
- var sendToTG = async (signals, imgInterval) => {
7606
+ var sendToTG = async (signals, imgInterval, userName = "root") => {
7607
+ const deliverableSignals = signals.filter(
7608
+ (signal) => signal.orderStatus !== "skipped" && signal.orderStatus !== "canceled"
7609
+ );
7610
+ import_logger5.logger.info(import_chalk.default.yellow("messages:", deliverableSignals.length));
7611
+ if (!deliverableSignals.length) {
7612
+ import_logger5.logger.info("");
7613
+ return;
7614
+ }
6757
7615
  const bar = new import_progress.default(
6758
7616
  ":current/:total [:bar][:percent] :eta(s) :symbol",
6759
7617
  {
6760
- total: signals.length,
7618
+ total: deliverableSignals.length,
6761
7619
  width: 30
6762
7620
  }
6763
7621
  );
6764
- import_logger5.logger.info(import_chalk.default.yellow("messages:", signals.length));
6765
- await (0, import_async3.runWithConcurrency)(signals, TG_CONCURRENCY_LIMIT, async (signal) => {
7622
+ await (0, import_async3.runWithConcurrency)(deliverableSignals, 1, async (signal) => {
6766
7623
  try {
6767
7624
  const analysis = await (0, import_redis3.getData)(
6768
7625
  import_redis3.redisKeys.analysis(signal.symbol, signal.signalId),
6769
7626
  null
6770
7627
  );
6771
- await sendSignal(signal, imgInterval, analysis);
7628
+ await sendSignal(signal, imgInterval, analysis, { userName });
6772
7629
  if (analysis && typeof analysis === "object" && Object.keys(analysis).length > 0) {
6773
- await sendSignalAnalysis(signal, analysis);
7630
+ await sendSignalAnalysis(signal, analysis, { userName });
6774
7631
  }
6775
7632
  } catch (err) {
6776
7633
  import_logger5.logger.error(
@@ -6790,7 +7647,9 @@ var sendToTG = async (signals, imgInterval) => {
6790
7647
  cleanRedis,
6791
7648
  drawStatInCLI,
6792
7649
  getTickers,
7650
+ loadTradejsConfig,
6793
7651
  makeScreenshots,
7652
+ sendTextToTG,
6794
7653
  sendToAI,
6795
7654
  sendToTG,
6796
7655
  update