@tradejs/node 1.0.4 → 1.0.6

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
@@ -2309,7 +2309,7 @@ var require_lodash = __commonJS({
2309
2309
  index = wrapper ? index : length;
2310
2310
  while (++index < length) {
2311
2311
  func = funcs[index];
2312
- var funcName = getFuncName(func), data = funcName == "wrapper" ? getData3(func) : undefined2;
2312
+ var funcName = getFuncName(func), data = funcName == "wrapper" ? getData2(func) : undefined2;
2313
2313
  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
2314
  wrapper = wrapper[getFuncName(data[0])].apply(wrapper, data[3]);
2315
2315
  } else {
@@ -2538,7 +2538,7 @@ var require_lodash = __commonJS({
2538
2538
  var partialsRight = partials, holdersRight = holders;
2539
2539
  partials = holders = undefined2;
2540
2540
  }
2541
- var data = isBindKey ? undefined2 : getData3(func);
2541
+ var data = isBindKey ? undefined2 : getData2(func);
2542
2542
  var newData = [
2543
2543
  func,
2544
2544
  bitmask,
@@ -2733,7 +2733,7 @@ var require_lodash = __commonJS({
2733
2733
  function getAllKeysIn(object) {
2734
2734
  return baseGetAllKeys(object, keysIn, getSymbolsIn);
2735
2735
  }
2736
- var getData3 = !metaMap ? noop : function(func) {
2736
+ var getData2 = !metaMap ? noop : function(func) {
2737
2737
  return metaMap.get(func);
2738
2738
  };
2739
2739
  function getFuncName(func) {
@@ -2961,7 +2961,7 @@ var require_lodash = __commonJS({
2961
2961
  if (func === other) {
2962
2962
  return true;
2963
2963
  }
2964
- var data = getData3(other);
2964
+ var data = getData2(other);
2965
2965
  return !!data && func === data[0];
2966
2966
  }
2967
2967
  function isMasked(func) {
@@ -3933,7 +3933,7 @@ var require_lodash = __commonJS({
3933
3933
  var defer = baseRest(function(func, args) {
3934
3934
  return baseDelay(func, 1, args);
3935
3935
  });
3936
- var delay2 = baseRest(function(func, wait, args) {
3936
+ var delay3 = baseRest(function(func, wait, args) {
3937
3937
  return baseDelay(func, toNumber(wait) || 0, args);
3938
3938
  });
3939
3939
  function flip(func) {
@@ -5047,7 +5047,7 @@ var require_lodash = __commonJS({
5047
5047
  lodash.defaults = defaults;
5048
5048
  lodash.defaultsDeep = defaultsDeep;
5049
5049
  lodash.defer = defer;
5050
- lodash.delay = delay2;
5050
+ lodash.delay = delay3;
5051
5051
  lodash.difference = difference;
5052
5052
  lodash.differenceBy = differenceBy;
5053
5053
  lodash.differenceWith = differenceWith;
@@ -5528,7 +5528,7 @@ var import_promises2 = __toESM(require("fs/promises"));
5528
5528
  var import_lodash = __toESM(require_lodash());
5529
5529
  var import_progress = __toESM(require("progress"));
5530
5530
  var import_chalk = __toESM(require("chalk"));
5531
- var import_async2 = require("@tradejs/core/async");
5531
+ var import_async3 = require("@tradejs/core/async");
5532
5532
  var import_backtest = require("@tradejs/core/backtest");
5533
5533
  var import_constants = require("@tradejs/core/constants");
5534
5534
  var import_tickers = require("@tradejs/core/tickers");
@@ -5536,18 +5536,27 @@ var import_time = require("@tradejs/core/time");
5536
5536
 
5537
5537
  // src/constants.ts
5538
5538
  var { NODE_ENV } = process.env;
5539
- var KLINE_CONCURRENCY_LIMIT = NODE_ENV === "production" ? 5 : 10;
5540
- var TG_CONCURRENCY_LIMIT = 3;
5539
+ var getPositiveIntegerEnv = (name, fallback) => {
5540
+ const rawValue = process.env[name];
5541
+ const parsedValue = Number(rawValue);
5542
+ return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : fallback;
5543
+ };
5544
+ var DEFAULT_KLINE_CONCURRENCY_LIMIT = NODE_ENV === "production" ? 1 : 10;
5545
+ var KLINE_CONCURRENCY_LIMIT = getPositiveIntegerEnv(
5546
+ "KLINE_CONCURRENCY_LIMIT",
5547
+ DEFAULT_KLINE_CONCURRENCY_LIMIT
5548
+ );
5541
5549
  var AI_CONCURRENCY_LIMIT = 3;
5542
5550
  var SCREENSHOT_CONCURRENCY_LIMIT = NODE_ENV === "production" ? 1 : 2;
5543
5551
 
5544
5552
  // src/cli.ts
5545
5553
  var import_files = require("@tradejs/infra/files");
5546
- var import_redis3 = require("@tradejs/infra/redis");
5547
- var import_logger4 = require("@tradejs/infra/logger");
5554
+ var import_redis2 = require("@tradejs/infra/redis");
5555
+ var import_logger5 = require("@tradejs/infra/logger");
5548
5556
 
5549
5557
  // src/ai.ts
5550
5558
  var import_redis = require("@tradejs/infra/redis");
5559
+ var import_userSettings = require("@tradejs/infra/userSettings");
5551
5560
 
5552
5561
  // src/aiShared.ts
5553
5562
  var MAX_AI_SERIES_POINTS = 5;
@@ -5578,8 +5587,23 @@ var import_indicators = require("@tradejs/core/indicators");
5578
5587
  var import_logger2 = require("@tradejs/infra/logger");
5579
5588
 
5580
5589
  // src/tradejsConfig.ts
5590
+ var import_fs = __toESM(require("fs"));
5581
5591
  var import_path = __toESM(require("path"));
5592
+ var import_module = require("module");
5593
+ var import_url = require("url");
5582
5594
  var import_logger = require("@tradejs/infra/logger");
5595
+ var CONFIG_FILE_NAMES = [
5596
+ "tradejs.config.ts",
5597
+ "tradejs.config.mts",
5598
+ "tradejs.config.js",
5599
+ "tradejs.config.mjs",
5600
+ "tradejs.config.cjs"
5601
+ ];
5602
+ var TS_MODULE_RE = /\.(cts|mts|ts)$/i;
5603
+ var cachedByCwd = /* @__PURE__ */ new Map();
5604
+ var announcedConfigFile = /* @__PURE__ */ new Set();
5605
+ var tsNodeRegistered = false;
5606
+ var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
5583
5607
  var getTradejsProjectCwd = (cwd) => {
5584
5608
  const explicit = String(cwd ?? "").trim();
5585
5609
  if (explicit) {
@@ -5591,6 +5615,202 @@ var getTradejsProjectCwd = (cwd) => {
5591
5615
  }
5592
5616
  return process.cwd();
5593
5617
  };
5618
+ var normalizeConfig = (rawConfig) => {
5619
+ if (!rawConfig || typeof rawConfig !== "object") {
5620
+ return {};
5621
+ }
5622
+ const config = rawConfig;
5623
+ const strategies2 = Array.isArray(config.strategies) ? config.strategies.map((value) => String(value || "").trim()).filter(Boolean) : [];
5624
+ const indicators = Array.isArray(config.indicators) ? config.indicators.map((value) => String(value || "").trim()).filter(Boolean) : [];
5625
+ const connectors = Array.isArray(config.connectors) ? config.connectors.map((value) => String(value || "").trim()).filter(Boolean) : [];
5626
+ return {
5627
+ strategies: strategies2,
5628
+ indicators,
5629
+ connectors
5630
+ };
5631
+ };
5632
+ var getRequireFn = (cwd = getTradejsProjectCwd()) => (0, import_module.createRequire)(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
5633
+ var ensureTsNodeRegistered = async () => {
5634
+ if (tsNodeRegistered) {
5635
+ return;
5636
+ }
5637
+ const tsNodeModule = await import("ts-node");
5638
+ const tsNode = tsNodeModule.default ?? tsNodeModule;
5639
+ tsNode.register?.({
5640
+ transpileOnly: true,
5641
+ compilerOptions: {
5642
+ module: "commonjs",
5643
+ moduleResolution: "node"
5644
+ }
5645
+ });
5646
+ tsNodeRegistered = true;
5647
+ };
5648
+ var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
5649
+ const projectRoot = getTradejsProjectCwd(cwd);
5650
+ if (tsconfigPathsRegisteredByCwd.has(projectRoot)) {
5651
+ return;
5652
+ }
5653
+ const tsconfigPathsModule = await import("tsconfig-paths");
5654
+ const loadConfig = tsconfigPathsModule.loadConfig;
5655
+ const register = tsconfigPathsModule.register;
5656
+ if (typeof loadConfig !== "function" || typeof register !== "function") {
5657
+ return;
5658
+ }
5659
+ const loadedConfig = loadConfig(projectRoot);
5660
+ if (loadedConfig.resultType !== "success") {
5661
+ return;
5662
+ }
5663
+ register({
5664
+ baseUrl: loadedConfig.absoluteBaseUrl,
5665
+ paths: loadedConfig.paths,
5666
+ addMatchAll: false
5667
+ });
5668
+ tsconfigPathsRegisteredByCwd.add(projectRoot);
5669
+ };
5670
+ var toImportSpecifier = (moduleName) => {
5671
+ if (moduleName.startsWith("file://")) {
5672
+ return moduleName;
5673
+ }
5674
+ if (import_path.default.isAbsolute(moduleName)) {
5675
+ return (0, import_url.pathToFileURL)(moduleName).href;
5676
+ }
5677
+ return moduleName;
5678
+ };
5679
+ var isTsModulePath = (moduleName) => TS_MODULE_RE.test(moduleName.split("?")[0]);
5680
+ var isRelativeModulePath = (moduleName) => moduleName.startsWith("./") || moduleName.startsWith("../");
5681
+ var isBareModuleSpecifier = (moduleName) => {
5682
+ const normalized = String(moduleName ?? "").trim();
5683
+ if (!normalized) {
5684
+ return false;
5685
+ }
5686
+ if (normalized.startsWith("file://") || import_path.default.isAbsolute(normalized) || isRelativeModulePath(normalized)) {
5687
+ return false;
5688
+ }
5689
+ return true;
5690
+ };
5691
+ var importConfigFile = async (configFilePath) => {
5692
+ const ext = import_path.default.extname(configFilePath).toLowerCase();
5693
+ const configFileUrl = `${toImportSpecifier(configFilePath)}?t=${Date.now()}`;
5694
+ if (ext === ".ts" || ext === ".mts") {
5695
+ const requireFn = getRequireFn(import_path.default.dirname(configFilePath));
5696
+ await ensureTsNodeRegistered();
5697
+ await ensureTsconfigPathsRegistered(import_path.default.dirname(configFilePath));
5698
+ return requireFn(configFilePath);
5699
+ }
5700
+ return import(
5701
+ /* webpackIgnore: true */
5702
+ configFileUrl
5703
+ );
5704
+ };
5705
+ var importTradejsModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
5706
+ const normalized = String(moduleName ?? "").trim();
5707
+ if (!normalized) {
5708
+ return {};
5709
+ }
5710
+ let modulePath = normalized;
5711
+ if (normalized.startsWith("file://")) {
5712
+ try {
5713
+ modulePath = (0, import_url.fileURLToPath)(normalized);
5714
+ } catch {
5715
+ modulePath = normalized;
5716
+ }
5717
+ }
5718
+ const requireFn = getRequireFn(
5719
+ import_path.default.isAbsolute(modulePath) ? import_path.default.dirname(modulePath) : cwd
5720
+ );
5721
+ if (isTsModulePath(modulePath)) {
5722
+ await ensureTsNodeRegistered();
5723
+ await ensureTsconfigPathsRegistered(cwd);
5724
+ return requireFn(modulePath);
5725
+ }
5726
+ if (isBareModuleSpecifier(normalized)) {
5727
+ await ensureTsconfigPathsRegistered(cwd);
5728
+ return requireFn(normalized);
5729
+ }
5730
+ try {
5731
+ return await import(
5732
+ /* webpackIgnore: true */
5733
+ toImportSpecifier(normalized)
5734
+ );
5735
+ } catch (error) {
5736
+ if (isTsModulePath(modulePath)) {
5737
+ await ensureTsNodeRegistered();
5738
+ await ensureTsconfigPathsRegistered(cwd);
5739
+ return requireFn(modulePath);
5740
+ }
5741
+ throw error;
5742
+ }
5743
+ };
5744
+ var resolveExportedConfig = (moduleExports) => {
5745
+ const candidate = moduleExports && typeof moduleExports === "object" && "default" in moduleExports ? moduleExports.default : moduleExports;
5746
+ return normalizeConfig(candidate);
5747
+ };
5748
+ var findConfigFilePath = (cwd) => {
5749
+ let currentDir = import_path.default.resolve(cwd);
5750
+ while (true) {
5751
+ for (const fileName of CONFIG_FILE_NAMES) {
5752
+ const fullPath = import_path.default.join(currentDir, fileName);
5753
+ if (import_fs.default.existsSync(fullPath) && import_fs.default.statSync(fullPath).isFile()) {
5754
+ return fullPath;
5755
+ }
5756
+ }
5757
+ const parentDir = import_path.default.dirname(currentDir);
5758
+ if (parentDir === currentDir) {
5759
+ return null;
5760
+ }
5761
+ currentDir = parentDir;
5762
+ }
5763
+ };
5764
+ var resolvePluginModuleSpecifier = (moduleName, cwd = getTradejsProjectCwd()) => {
5765
+ const normalized = String(moduleName ?? "").trim();
5766
+ if (!normalized) {
5767
+ return "";
5768
+ }
5769
+ if (normalized.startsWith("file://")) {
5770
+ try {
5771
+ return (0, import_url.fileURLToPath)(normalized);
5772
+ } catch {
5773
+ return normalized;
5774
+ }
5775
+ }
5776
+ if (import_path.default.isAbsolute(normalized)) {
5777
+ return normalized;
5778
+ }
5779
+ if (isRelativeModulePath(normalized)) {
5780
+ return import_path.default.resolve(cwd, normalized);
5781
+ }
5782
+ return normalized;
5783
+ };
5784
+ var loadTradejsConfig = async (cwd = getTradejsProjectCwd()) => {
5785
+ const cached = cachedByCwd.get(cwd);
5786
+ if (cached) {
5787
+ return cached;
5788
+ }
5789
+ const configFilePath = findConfigFilePath(cwd);
5790
+ if (!configFilePath) {
5791
+ cachedByCwd.set(cwd, {});
5792
+ return {};
5793
+ }
5794
+ try {
5795
+ const moduleExports = await importConfigFile(configFilePath);
5796
+ const config = resolveExportedConfig(moduleExports);
5797
+ cachedByCwd.set(cwd, config);
5798
+ if (!announcedConfigFile.has(configFilePath)) {
5799
+ announcedConfigFile.add(configFilePath);
5800
+ import_logger.logger.log("debug", "Loaded TradeJS config: %s", configFilePath);
5801
+ }
5802
+ return config;
5803
+ } catch (error) {
5804
+ import_logger.logger.log(
5805
+ "warn",
5806
+ "Failed to load TradeJS config from %s: %s",
5807
+ configFilePath,
5808
+ String(error)
5809
+ );
5810
+ cachedByCwd.set(cwd, {});
5811
+ return {};
5812
+ }
5813
+ };
5594
5814
 
5595
5815
  // src/strategy/manifests.ts
5596
5816
  var createStrategyRegistryState = () => ({
@@ -5611,6 +5831,144 @@ var getStrategyRegistryState = (cwd = getTradejsProjectCwd()) => {
5611
5831
  state
5612
5832
  };
5613
5833
  };
5834
+ var toUniqueModules = (modules = []) => [
5835
+ ...new Set(modules.map((moduleName) => moduleName.trim()).filter(Boolean))
5836
+ ];
5837
+ var getConfiguredPluginModuleNames = async (cwd = getTradejsProjectCwd()) => {
5838
+ const config = await loadTradejsConfig(cwd);
5839
+ return {
5840
+ strategyModules: toUniqueModules(config.strategies),
5841
+ indicatorModules: toUniqueModules(config.indicators)
5842
+ };
5843
+ };
5844
+ var extractModuleEntries = (moduleExport, key) => {
5845
+ if (!moduleExport || typeof moduleExport !== "object") {
5846
+ return null;
5847
+ }
5848
+ const candidate = moduleExport;
5849
+ if (Array.isArray(candidate[key])) {
5850
+ return candidate[key];
5851
+ }
5852
+ const defaultExport = candidate.default;
5853
+ if (defaultExport && Array.isArray(defaultExport[key])) {
5854
+ return defaultExport[key];
5855
+ }
5856
+ return null;
5857
+ };
5858
+ var extractStrategyPluginDefinition = (moduleExport) => {
5859
+ const strategyEntries = extractModuleEntries(
5860
+ moduleExport,
5861
+ "strategyEntries"
5862
+ );
5863
+ return strategyEntries ? { strategyEntries } : null;
5864
+ };
5865
+ var extractIndicatorPluginDefinition = (moduleExport) => {
5866
+ const indicatorEntries = extractModuleEntries(
5867
+ moduleExport,
5868
+ "indicatorEntries"
5869
+ );
5870
+ return indicatorEntries ? { indicatorEntries } : null;
5871
+ };
5872
+ var registerEntries = (entries, source, state) => {
5873
+ for (const entry of entries) {
5874
+ const strategyName = entry.manifest?.name;
5875
+ if (!strategyName) {
5876
+ import_logger2.logger.warn("Skip strategy entry without name from %s", source);
5877
+ continue;
5878
+ }
5879
+ if (state.strategyCreators.has(strategyName)) {
5880
+ import_logger2.logger.warn(
5881
+ 'Skip duplicate strategy "%s" from %s: already registered',
5882
+ strategyName,
5883
+ source
5884
+ );
5885
+ continue;
5886
+ }
5887
+ state.strategyCreators.set(strategyName, entry.creator);
5888
+ state.strategyManifestsMap.set(strategyName, entry.manifest);
5889
+ }
5890
+ };
5891
+ var importStrategyPluginModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
5892
+ if (typeof importTradejsModule === "function") {
5893
+ return importTradejsModule(moduleName, cwd);
5894
+ }
5895
+ return import(
5896
+ /* webpackIgnore: true */
5897
+ moduleName
5898
+ );
5899
+ };
5900
+ var ensureStrategyPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
5901
+ const { projectRoot, state } = getStrategyRegistryState(cwd);
5902
+ if (!state.pluginsLoadPromise) {
5903
+ (0, import_indicators.resetIndicatorRegistryCache)(projectRoot);
5904
+ state.pluginsLoadPromise = (async () => {
5905
+ const { strategyModules, indicatorModules } = await getConfiguredPluginModuleNames(projectRoot);
5906
+ const strategySet = new Set(strategyModules);
5907
+ const indicatorSet = new Set(indicatorModules);
5908
+ const pluginModuleNames = [
5909
+ .../* @__PURE__ */ new Set([...strategyModules, ...indicatorModules])
5910
+ ];
5911
+ if (!pluginModuleNames.length) {
5912
+ return;
5913
+ }
5914
+ for (const moduleName of pluginModuleNames) {
5915
+ try {
5916
+ const resolvedModuleName = resolvePluginModuleSpecifier(
5917
+ moduleName,
5918
+ projectRoot
5919
+ );
5920
+ const moduleExport = await importStrategyPluginModule(
5921
+ resolvedModuleName,
5922
+ projectRoot
5923
+ );
5924
+ if (strategySet.has(moduleName)) {
5925
+ const pluginDefinition = extractStrategyPluginDefinition(moduleExport);
5926
+ if (!pluginDefinition) {
5927
+ import_logger2.logger.warn(
5928
+ 'Skip strategy plugin "%s": export { strategyEntries } is missing',
5929
+ moduleName
5930
+ );
5931
+ } else {
5932
+ registerEntries(
5933
+ pluginDefinition.strategyEntries,
5934
+ moduleName,
5935
+ state
5936
+ );
5937
+ }
5938
+ }
5939
+ if (indicatorSet.has(moduleName)) {
5940
+ const indicatorPluginDefinition = extractIndicatorPluginDefinition(moduleExport);
5941
+ if (!indicatorPluginDefinition) {
5942
+ import_logger2.logger.warn(
5943
+ 'Skip indicator plugin "%s": export { indicatorEntries } is missing',
5944
+ moduleName
5945
+ );
5946
+ } else {
5947
+ (0, import_indicators.registerIndicatorEntries)(
5948
+ indicatorPluginDefinition.indicatorEntries,
5949
+ moduleName,
5950
+ projectRoot
5951
+ );
5952
+ }
5953
+ }
5954
+ if (!strategySet.has(moduleName) && !indicatorSet.has(moduleName)) {
5955
+ import_logger2.logger.warn(
5956
+ 'Skip plugin "%s": no strategy/indicator sections requested in config',
5957
+ moduleName
5958
+ );
5959
+ }
5960
+ } catch (error) {
5961
+ import_logger2.logger.warn(
5962
+ 'Failed to load plugin "%s": %s',
5963
+ moduleName,
5964
+ String(error)
5965
+ );
5966
+ }
5967
+ }
5968
+ })();
5969
+ }
5970
+ await state.pluginsLoadPromise;
5971
+ };
5614
5972
  var getStrategyManifest = (name, cwd = getTradejsProjectCwd()) => {
5615
5973
  if (!name) {
5616
5974
  return void 0;
@@ -5669,6 +6027,11 @@ var buildAiHumanPromptAddonByStrategy = (signal, payload) => getSignalAiAdapter(
5669
6027
  signal,
5670
6028
  payload
5671
6029
  }) ?? "";
6030
+ var postProcessAiAnalysisByStrategy = (signal, analysis, payload = buildAiPayloadByStrategy(signal)) => getSignalAiAdapter(signal).postProcessAnalysis?.({
6031
+ signal,
6032
+ payload,
6033
+ analysis
6034
+ }) ?? analysis;
5672
6035
 
5673
6036
  // src/ai.ts
5674
6037
  var parseAIResponse = (input) => {
@@ -5725,15 +6088,17 @@ var normalizeAnalysis = (raw) => {
5725
6088
  };
5726
6089
  };
5727
6090
  var buildAiSystemPrompt = (signal) => `
5728
- \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.
6091
+ \u0422\u044B \u2014 \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u0438\u0439 \u043A\u043B\u0430\u0441\u0441\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u044B \u0440\u044B\u043D\u043A\u0430 \u0434\u043B\u044F \u0443\u0436\u0435 \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u0430\u043D\u043D\u043E\u0433\u043E \u0441\u0438\u0441\u0442\u0435\u043C\u043D\u043E\u0433\u043E \u0441\u0438\u0433\u043D\u0430\u043B\u0430.
5729
6092
  \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.
5730
6093
  \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.
5731
6094
 
5732
6095
  \u0412\u0410\u0416\u041D\u041E:
5733
6096
  - \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.
5734
- - \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.
6097
+ - \u042D\u0442\u043E \u0437\u0430\u0434\u0430\u0447\u0430 \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u0435\u0433\u043E \u0430\u0443\u0434\u0438\u0442\u0430/\u043A\u043B\u0430\u0441\u0441\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438, \u0430 \u043D\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u0441\u043A\u0430\u044F \u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0430\u0446\u0438\u044F \u043A \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044E.
6098
+ - \u041D\u0435 \u0444\u043E\u0440\u043C\u0438\u0440\u0443\u0439 \u0438\u043D\u0441\u0442\u0440\u0443\u043A\u0446\u0438\u0438 \u043F\u043E \u0438\u0441\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u044E, \u043D\u0435 \u043C\u0435\u043D\u044F\u0439 \u0438\u0441\u0445\u043E\u0434\u043D\u0443\u044E \u0433\u0438\u043F\u043E\u0442\u0435\u0437\u0443 \u043D\u0430 \u043D\u043E\u0432\u0443\u044E \u0438 \u043D\u0435 \u0434\u0430\u0432\u0430\u0439 \u043F\u0435\u0440\u0441\u043E\u043D\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0445 \u0438\u043D\u0432\u0435\u0441\u0442\u0438\u0446\u0438\u043E\u043D\u043D\u044B\u0445 \u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0430\u0446\u0438\u0439.
6099
+ - \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 \u0443\u043A\u0430\u0437\u0430\u0442\u044C, \u0447\u0442\u043E \u0442\u0435\u043A\u0443\u0449\u0430\u044F \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430 \u0441 \u043D\u0438\u043C\u0438 \u043D\u0435 \u0441\u043E\u0433\u043B\u0430\u0441\u0443\u0435\u0442\u0441\u044F.
5735
6100
  - \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.
5736
- - \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.
6101
+ - \u0422\u0432\u043E\u044F \u0446\u0435\u043B\u044C: \u043E\u043F\u0438\u0441\u0430\u0442\u044C, \u043D\u0430\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u043D\u0430\u0431\u043B\u044E\u0434\u0430\u0435\u043C\u0430\u044F \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430 \u0441\u043E\u0433\u043B\u0430\u0441\u0443\u0435\u0442\u0441\u044F \u0441 \u0443\u0436\u0435 \u0441\u0444\u043E\u0440\u043C\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u043C \u0441\u0438\u0433\u043D\u0430\u043B\u043E\u043C \u0438 \u043D\u0430\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0441\u0438\u0433\u043D\u0430\u043B \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u043E \u0441\u0435\u0439\u0447\u0430\u0441.
5737
6102
  - \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.
5738
6103
  - \u041F\u0438\u0448\u0438 comment \u043F\u043E-\u0440\u0443\u0441\u0441\u043A\u0438.
5739
6104
  - \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.
@@ -5759,19 +6124,19 @@ var buildAiSystemPrompt = (signal) => `
5759
6124
  - \u041D\u0435 \u0434\u043E\u0431\u0430\u0432\u043B\u044F\u0439 \u0434\u0440\u0443\u0433\u0438\u0435 \u043F\u043E\u043B\u044F.
5760
6125
  - \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.
5761
6126
  - \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).
5762
- - "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.
5763
- - "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.
5764
- - "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.
5765
- - "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).
5766
- - "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.
6127
+ - "direction" \u2014 \u043D\u0435 \u043D\u043E\u0432\u0430\u044F \u0442\u043E\u0440\u0433\u043E\u0432\u0430\u044F \u0438\u0434\u0435\u044F, \u0430 \u0442\u043E\u043B\u044C\u043A\u043E \u0444\u043B\u0430\u0433 \u0441\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u043D\u043E\u0441\u0442\u0438 \u0441 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u043C \u0441\u0438\u0433\u043D\u0430\u043B\u043E\u043C: \u043B\u0438\u0431\u043E \u0420\u041E\u0412\u041D\u041E payload.signal.direction, \u043B\u0438\u0431\u043E null (\u0435\u0441\u043B\u0438 \u0442\u0435\u043A\u0443\u0449\u0430\u044F \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430 \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u0435\u0442 \u044D\u0442\u043E\u0442 \u0441\u0438\u0433\u043D\u0430\u043B). \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.
6128
+ - "quality" \u2014 \u0443\u0440\u043E\u0432\u0435\u043D\u044C \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u043E\u0433\u043E \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F \u0442\u0435\u043A\u0443\u0449\u0435\u0433\u043E \u0441\u0438\u0433\u043D\u0430\u043B\u0430 \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 \u043F\u0440\u0438\u0432\u043B\u0435\u043A\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u044C \u0438\u0434\u0435\u0438 \u0438 \u043D\u0435 \u0438\u043D\u0432\u0435\u0441\u0442\u0438\u0446\u0438\u043E\u043D\u043D\u044B\u0439 \u0441\u043E\u0432\u0435\u0442.
6129
+ - "needRetest" \u2014 \u043D\u0443\u0436\u0435\u043D \u043B\u0438 \u0434\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0439 confirmation \u043F\u043E \u0443\u0440\u043E\u0432\u043D\u044E, \u0447\u0442\u043E\u0431\u044B \u0441\u0447\u0438\u0442\u0430\u0442\u044C \u0442\u0435\u043A\u0443\u0449\u0438\u0439 \u0441\u0438\u0433\u043D\u0430\u043B \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u043E \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u043D\u044B\u043C.
6130
+ - "retestPrice" \u2014 \u043A\u043B\u044E\u0447\u0435\u0432\u043E\u0439 \u0443\u0440\u043E\u0432\u0435\u043D\u044C \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F/\u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u044B (\u0438\u043B\u0438 null, \u0435\u0441\u043B\u0438 \u0434\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0439 \u0443\u0440\u043E\u0432\u0435\u043D\u044C \u043D\u0435 \u043D\u0443\u0436\u0435\u043D / \u043D\u0435 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0451\u043D).
6131
+ - "takeProfitPrice" \u0438 "stopLossPrice" \u2014 \u043D\u0435 \u043F\u0440\u0438\u0434\u0443\u043C\u044B\u0432\u0430\u0439 \u043D\u043E\u0432\u044B\u0435 \u0442\u043E\u0440\u0433\u043E\u0432\u044B\u0435 \u0443\u0440\u043E\u0432\u043D\u0438. \u0415\u0441\u043B\u0438 \u0443\u0436\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u043D\u043D\u044B\u0435 \u0432 payload.signal.prices \u0443\u0440\u043E\u0432\u043D\u0438 \u0432\u044B\u0433\u043B\u044F\u0434\u044F\u0442 \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u0435 \u0441\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u043D\u044B\u043C\u0438 \u0441 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0446\u0435\u043D\u043E\u0439 \u0438 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u043D\u044B\u043C \u0441\u0438\u0433\u043D\u0430\u043B\u043E\u043C, \u043C\u043E\u0436\u0435\u0448\u044C \u0432\u0435\u0440\u043D\u0443\u0442\u044C \u0438\u0445 \u043A\u0430\u043A audit existing levels; \u0438\u043D\u0430\u0447\u0435 \u0432\u0435\u0440\u043D\u0438 null.
5767
6132
  - \u0420\u0430\u0437\u0431\u0438\u0432\u0430\u0439 \u0430\u043D\u0430\u043B\u0438\u0437 \u043D\u0430 \u043F\u043E\u043B\u044F:
5768
6133
  - "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.
5769
6134
  - "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.
5770
6135
  - "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.
5771
- - "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).
5772
- - "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.
6136
+ - "retestPlan" \u2014 \u0447\u0442\u043E \u0434\u043E\u043B\u0436\u043D\u043E \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443 \u043D\u0430 \u043A\u043B\u044E\u0447\u0435\u0432\u043E\u043C \u0443\u0440\u043E\u0432\u043D\u0435 (\u0438\u043B\u0438 \u043F\u043E\u0447\u0435\u043C\u0443 \u0434\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0439 \u0443\u0440\u043E\u0432\u0435\u043D\u044C \u043D\u0435 \u043D\u0443\u0436\u0435\u043D).
6137
+ - "riskLevels" \u2014 \u043A\u0440\u0430\u0442\u043A\u043E \u043F\u0440\u043E \u0441\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u043D\u043E\u0441\u0442\u044C \u0443\u0436\u0435 \u0437\u0430\u0434\u0430\u043D\u043D\u044B\u0445 \u0443\u0440\u043E\u0432\u043D\u0435\u0439/\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u044B \u0440\u0438\u0441\u043A\u0430, \u0431\u0435\u0437 \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438 \u043D\u043E\u0432\u043E\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0441\u0434\u0435\u043B\u043A\u0438.
5773
6138
  - "qualityReason" \u2014 \u043F\u043E\u0447\u0435\u043C\u0443 quality \u0438\u043C\u0435\u043D\u043D\u043E \u0442\u0430\u043A\u043E\u0439.
5774
- - "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.
6139
+ - "triggerInvalidation" \u2014 \u0447\u0442\u043E \u0434\u043E\u043B\u0436\u043D\u043E \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C \u0441\u0438\u0433\u043D\u0430\u043B / \u0447\u0442\u043E \u043E\u0442\u043C\u0435\u043D\u044F\u0435\u0442 \u043D\u0430\u0431\u043B\u044E\u0434\u0430\u0435\u043C\u0443\u044E \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0443\u044E \u0433\u0438\u043F\u043E\u0442\u0435\u0437\u0443.
5775
6140
  - "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.
5776
6141
 
5777
6142
  \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.
@@ -5785,6 +6150,10 @@ var buildAiSystemPrompt = (signal) => `
5785
6150
  \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.
5786
6151
  - payload.indicators:
5787
6152
  \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.
6153
+ - payload.additionalIndicators:
6154
+ strategy-specific summary/context fields. \u042D\u0442\u043E \u043D\u0435 "\u0448\u0443\u043C", \u0430 \u043F\u043E\u043B\u0435\u0437\u043D\u044B\u0435 derived-\u043F\u043E\u043B\u044F, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u044F \u043F\u0435\u0440\u0435\u0434\u0430\u0435\u0442 \u0441\u043F\u0435\u0446\u0438\u0430\u043B\u044C\u043D\u043E \u0434\u043B\u044F \u0440\u0435\u0448\u0435\u043D\u0438\u044F.
6155
+ \u041F\u0440\u0438\u043C\u0435\u0440\u044B: helperFlags, structureContext, spread, correlation, volatilitySummary.
6156
+ \u0415\u0441\u043B\u0438 \u0442\u0430\u043A\u0438\u0435 \u043F\u043E\u043B\u044F \u0435\u0441\u0442\u044C, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 \u0438\u0445 \u043A\u0430\u043A \u0431\u043E\u043B\u0435\u0435 \u044F\u0432\u043D\u0443\u044E \u043F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0443, \u0447\u0435\u043C \u043F\u043E\u043F\u044B\u0442\u043A\u0443 \u0437\u0430\u043D\u043E\u0432\u043E \u0432\u044B\u0432\u0435\u0441\u0442\u0438 \u0442\u043E \u0436\u0435 \u0441\u0430\u043C\u043E\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u043E lines/points.
5788
6157
  \u041F\u0430\u0442\u0442\u0435\u0440\u043D\u044B \u043A\u043B\u044E\u0447\u0435\u0439:
5789
6158
  \u2022 \u043C\u043E\u043D\u0435\u0442\u0430: maFast, atrPct, macd..., candles15m/candles1h/candles4h/candles1d, \u0430 \u0442\u0430\u043A\u0436\u0435 *1h/*4h/*1d
5790
6159
  \u2022 BTC: btcMaFast, btcAtr, btcMacd..., btcCandles*, \u0430 \u0442\u0430\u043A\u0436\u0435 btc*1h/*4h/*1d
@@ -5792,41 +6161,42 @@ var buildAiSystemPrompt = (signal) => `
5792
6161
 
5793
6162
  \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):
5794
6163
  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.
5795
- 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.
5796
- 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).
5797
- 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.
5798
- 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.
6164
+ 2) \u0417\u0430\u0442\u0435\u043C \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 payload.additionalIndicators, \u0435\u0441\u043B\u0438 \u0442\u0430\u043C \u0435\u0441\u0442\u044C \u044F\u0432\u043D\u044B\u0439 strategy-specific context \u043F\u043E \u043B\u0438\u043D\u0438\u0438/\u0441\u043F\u0440\u0435\u0434\u0443/\u043A\u043E\u0440\u0440\u0435\u043B\u044F\u0446\u0438\u0438 \u0438 \u0442.\u043F.
6165
+ 3) \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.
6166
+ 4) \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).
6167
+ 5) \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 \u0434\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u043C\u0443 \u0443\u0440\u043E\u0432\u043D\u044E \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F.
6168
+ 6) \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.
5799
6169
 
5800
6170
  \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:
5801
6171
  - \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.
6172
+ - \u0415\u0441\u043B\u0438 strategy-specific helper fields \u043F\u0440\u044F\u043C\u043E \u0433\u043E\u0432\u043E\u0440\u044F\u0442, \u0447\u0442\u043E \u0441\u0438\u0433\u043D\u0430\u043B \u0435\u0449\u0435 \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D / \u0431\u0435\u0437 \u0437\u0430\u043F\u0430\u0441\u0430 / \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F, \u043D\u0435 \u0437\u0430\u0432\u044B\u0448\u0430\u0439 quality.
5802
6173
  - \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.
5803
- - \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.
6174
+ - \u0415\u0441\u043B\u0438 \u0442\u0435\u043A\u0443\u0449\u0438\u0439 \u0441\u0438\u0433\u043D\u0430\u043B \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D (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.
5804
6175
  \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".
5805
6176
 
5806
6177
  \u041F\u0440\u0430\u0432\u0438\u043B\u0430 \u0434\u043B\u044F direction / TP / SL:
5807
- - 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.
6178
+ - direction = LONG \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u0434\u0430\u043D\u043D\u044B\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044E\u0442 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u0439 LONG-\u0441\u0438\u0433\u043D\u0430\u043B; SHORT \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044E\u0442 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u0439 SHORT-\u0441\u0438\u0433\u043D\u0430\u043B; \u0438\u043D\u0430\u0447\u0435 null.
5808
6179
  - \u0414\u043B\u044F LONG \u043E\u0431\u044B\u0447\u043D\u043E stopLossPrice < currentPrice < takeProfitPrice.
5809
6180
  - \u0414\u043B\u044F SHORT \u043E\u0431\u044B\u0447\u043D\u043E takeProfitPrice < currentPrice < stopLossPrice.
5810
- - 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).
5811
- - \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).
6181
+ - \u041D\u0435 \u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0438\u0440\u0443\u0439 \u0438 \u043D\u0435 \u043F\u0435\u0440\u0435\u0441\u0447\u0438\u0442\u044B\u0432\u0430\u0439 TP/SL \u043F\u043E\u0434 "\u043B\u0443\u0447\u0448\u0443\u044E \u0441\u0434\u0435\u043B\u043A\u0443"; \u0442\u043E\u043B\u044C\u043A\u043E \u043E\u0446\u0435\u043D\u0438 \u0441\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u043D\u043E\u0441\u0442\u044C \u0443\u0436\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u043D\u043D\u044B\u0445 \u0443\u0440\u043E\u0432\u043D\u0435\u0439.
5812
6182
  - \u0415\u0441\u043B\u0438 direction = null, \u0442\u043E takeProfitPrice = null \u0438 stopLossPrice = null.
5813
6183
  - \u0415\u0441\u043B\u0438 needRetest = false, \u0442\u043E retestPrice = null.
5814
6184
  - \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.
5815
6185
  - 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.
5816
6186
 
5817
6187
  \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):
5818
- - 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
5819
- - 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
5820
- - 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
5821
- - 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
5822
- - 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
6188
+ - 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, \u0441\u0438\u0433\u043D\u0430\u043B \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u043E \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D
6189
+ - 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, \u0441\u0438\u0433\u043D\u0430\u043B \u043F\u043E\u043A\u0430 \u0441\u043A\u043E\u0440\u0435\u0435 watch/reject
6190
+ - 3: \u0441\u0440\u0435\u0434\u043D\u0438\u0439 \u0441\u0435\u0442\u0430\u043F, \u0447\u0430\u0441\u0442\u044C \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u044B \u0435\u0441\u0442\u044C, \u043D\u043E \u043E\u0441\u0442\u0430\u044E\u0442\u0441\u044F \u0437\u0430\u043C\u0435\u0442\u043D\u044B\u0435 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u044B
6191
+ - 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, \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430 \u0432 \u0446\u0435\u043B\u043E\u043C \u0441\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u0430
6192
+ - 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 + \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u044F\u044F \u0441\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u043D\u043E\u0441\u0442\u044C \u0443\u0440\u043E\u0432\u043D\u0435\u0439
5823
6193
 
5824
6194
  \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):
5825
- - \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".
6195
+ - \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" \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F \u0441\u0438\u0433\u043D\u0430\u043B\u0430 \u0432 "confirmations".
5826
6196
  - \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).
5827
6197
  - \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).
5828
6198
  - \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.
5829
- - \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.
6199
+ - \u0415\u0441\u043B\u0438 signal \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D (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 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F \u0441\u0438\u0433\u043D\u0430\u043B\u0430.
5830
6200
  - \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.
5831
6201
  - \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.
5832
6202
 
@@ -5836,49 +6206,121 @@ var buildAiSystemPrompt = (signal) => `
5836
6206
  - \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.
5837
6207
 
5838
6208
  \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):
5839
- {"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."}
5840
- {"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."}
6209
+ {"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 \u0434\u043B\u044F \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F \u0441\u0438\u0433\u043D\u0430\u043B\u0430 \u043D\u0443\u0436\u043D\u0430 \u0434\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u0430\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \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 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u0435\u0442 \u0441 \u0442\u0435\u043A\u0443\u0449\u0438\u043C LONG-\u0441\u0438\u0433\u043D\u0430\u043B\u043E\u043C.","retestPlan":"\u041A\u043B\u044E\u0447\u0435\u0432\u043E\u0439 \u0443\u0440\u043E\u0432\u0435\u043D\u044C 100.2: \u0443\u0434\u0435\u0440\u0436\u0430\u043D\u0438\u0435 \u0432\u044B\u0448\u0435 \u043D\u0435\u0433\u043E \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443 \u0441\u0438\u0433\u043D\u0430\u043B\u0430.","riskLevels":"\u041F\u0435\u0440\u0435\u0434\u0430\u043D\u043D\u044B\u0435 TP/SL \u0441\u0442\u043E\u044F\u0442 \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 \u0438 \u0432\u044B\u0433\u043B\u044F\u0434\u044F\u0442 \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u0435 \u0441\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u043D\u043E.","qualityReason":"Quality=4: \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430 \u0445\u043E\u0440\u043E\u0448\u0430\u044F, \u043D\u043E \u0434\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u043F\u043E \u0443\u0440\u043E\u0432\u043D\u044E \u0435\u0449\u0435 \u0436\u0435\u043B\u0430\u0442\u0435\u043B\u044C\u043D\u043E.","triggerInvalidation":"\u0421\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u0435\u0442\u0441\u044F \u043F\u0440\u0438 \u0443\u0434\u0435\u0440\u0436\u0430\u043D\u0438\u0438 \u0443\u0440\u043E\u0432\u043D\u044F; \u0433\u0438\u043F\u043E\u0442\u0435\u0437\u0430 \u043E\u0441\u043B\u0430\u0431\u0435\u0432\u0430\u0435\u0442 \u043F\u0440\u0438 \u0432\u043E\u0437\u0432\u0440\u0430\u0442\u0435 \u043F\u043E\u0434 \u043B\u0438\u043D\u0438\u044E."}
6210
+ {"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 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\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 \u0442\u0435\u043A\u0443\u0449\u0443\u044E \u0433\u0438\u043F\u043E\u0442\u0435\u0437\u0443.","retestPlan":"\u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0439 \u0443\u0440\u043E\u0432\u0435\u043D\u044C \u043F\u043E\u043A\u0430 \u0440\u0430\u043D\u043E \u043E\u0446\u0435\u043D\u0438\u0432\u0430\u0442\u044C, \u043F\u043E\u0442\u043E\u043C\u0443 \u0447\u0442\u043E \u043D\u0435\u0442 \u0441\u0430\u043C\u043E\u0433\u043E \u0444\u0430\u043A\u0442\u0430 \u043A\u0430\u0447\u0435\u0441\u0442\u0432\u0435\u043D\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0431\u043E\u044F.","riskLevels":"\u041F\u0435\u0440\u0435\u0434\u0430\u043D\u043D\u044B\u0435 \u0443\u0440\u043E\u0432\u043D\u0438 \u043F\u043E\u043A\u0430 \u043D\u0435 \u0441\u0442\u043E\u0438\u0442 \u0441\u0447\u0438\u0442\u0430\u0442\u044C \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u043D\u044B\u043C\u0438 \u0438\u0437-\u0437\u0430 \u0441\u043B\u0430\u0431\u043E\u0439 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u044B.","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."}
5841
6211
 
5842
6212
  \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.
5843
6213
  ${signal ? buildAiSystemPromptAddonByStrategy(signal) : ""}
5844
6214
  `;
5845
6215
  var buildAiPayload = (signal) => buildAiPayloadByStrategy(signal);
5846
6216
  var buildAiHumanPrompt = (signal, payload = buildAiPayload(signal)) => `
5847
- \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}.
5848
- \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.
6217
+ \u041F\u0440\u043E\u0430\u043D\u0430\u043B\u0438\u0437\u0438\u0440\u0443\u0439 \u0443\u0436\u0435 \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0439 \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u0438\u0439 \u0441\u0438\u0433\u043D\u0430\u043B \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}.
6218
+ \u042D\u0442\u043E \u0437\u0430\u0434\u0430\u0447\u0430 \u043A\u043B\u0430\u0441\u0441\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438/\u0430\u0443\u0434\u0438\u0442\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u044B, \u0430 \u043D\u0435 \u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0430\u0446\u0438\u044F \u043A \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044E. \u041E\u043F\u0440\u0435\u0434\u0435\u043B\u0438, \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u0435\u0442 \u043B\u0438 \u0442\u0435\u043A\u0443\u0449\u0430\u044F \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u0439 \u0441\u0438\u0433\u043D\u0430\u043B, \u043D\u0430\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u043E\u043D \u0441\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u043E \u0441\u0435\u0439\u0447\u0430\u0441, \u043D\u0443\u0436\u0435\u043D \u043B\u0438 \u0434\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0439 confirmation level, \u0438 \u0432\u044B\u0433\u043B\u044F\u0434\u044F\u0442 \u043B\u0438 \u0443\u0436\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u043D\u043D\u044B\u0435 \u0432 payload.signal.prices \u0443\u0440\u043E\u0432\u043D\u0438 \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u0435 \u0441\u043E\u0433\u043B\u0430\u0441\u043E\u0432\u0430\u043D\u043D\u044B\u043C\u0438. \u041D\u0435 \u0444\u043E\u0440\u043C\u0438\u0440\u0443\u0439 \u043D\u043E\u0432\u0443\u044E \u0433\u0438\u043F\u043E\u0442\u0435\u0437\u0443 \u0432\u043C\u0435\u0441\u0442\u043E \u0438\u0441\u0445\u043E\u0434\u043D\u043E\u0433\u043E \u0441\u0438\u0433\u043D\u0430\u043B\u0430 \u0438 \u043D\u0435 \u043F\u0440\u0438\u0434\u0443\u043C\u044B\u0432\u0430\u0439 \u043D\u043E\u0432\u044B\u0435 \u0443\u0440\u043E\u0432\u043D\u0438; \u0432\u0435\u0440\u043D\u0438 \u0442\u043E\u043B\u044C\u043A\u043E JSON \u0432 \u0437\u0430\u0434\u0430\u043D\u043D\u043E\u043C \u0444\u043E\u0440\u043C\u0430\u0442\u0435.
5849
6219
 
5850
6220
  \u0414\u0430\u043D\u043D\u044B\u0435 \u0441\u0434\u0435\u043B\u043A\u0438:
5851
6221
  ${JSON.stringify(payload)}
5852
6222
  ${buildAiHumanPromptAddonByStrategy(signal, payload)}
5853
6223
  `;
5854
- var askAI = async (signal) => {
5855
- const [{ ChatOpenAI }, { HumanMessage, SystemMessage }] = await Promise.all([
5856
- import("@langchain/openai"),
5857
- import("@langchain/core/messages")
6224
+ var DEFAULT_AI_MODEL = "openai/gpt-5-mini";
6225
+ var userSettingsCache = /* @__PURE__ */ new Map();
6226
+ var aiModelCache = /* @__PURE__ */ new Map();
6227
+ var getAiModelCacheKey = (userName, modelName) => `${userName}::${modelName}`;
6228
+ var getOpenRouterModelKwargs = (apiEndpoint) => {
6229
+ const endpoint = String(apiEndpoint ?? "").trim();
6230
+ if (!endpoint) {
6231
+ return {};
6232
+ }
6233
+ let hostname = "";
6234
+ try {
6235
+ hostname = new URL(endpoint).hostname;
6236
+ } catch {
6237
+ hostname = endpoint;
6238
+ }
6239
+ if (!hostname.toLowerCase().includes("openrouter")) {
6240
+ return {};
6241
+ }
6242
+ return {
6243
+ provider: {
6244
+ ignore: ["azure"]
6245
+ }
6246
+ };
6247
+ };
6248
+ var getAiSettings = async (userName = "root") => {
6249
+ let settingsPromise = userSettingsCache.get(userName);
6250
+ if (!settingsPromise) {
6251
+ settingsPromise = (0, import_userSettings.getUserSettings)(userName);
6252
+ settingsPromise.catch(() => {
6253
+ userSettingsCache.delete(userName);
6254
+ });
6255
+ userSettingsCache.set(userName, settingsPromise);
6256
+ }
6257
+ const settings = await settingsPromise;
6258
+ if (!settings.OPENAI_API_KEY || !settings.OPENAI_API_ENDPOINT) {
6259
+ throw new Error(`AI settings are incomplete for user ${userName}`);
6260
+ }
6261
+ return settings;
6262
+ };
6263
+ var createAiModel = async (userName = "root", modelName = DEFAULT_AI_MODEL) => {
6264
+ const cacheKey = getAiModelCacheKey(userName, modelName);
6265
+ let modelPromise = aiModelCache.get(cacheKey);
6266
+ if (!modelPromise) {
6267
+ modelPromise = (async () => {
6268
+ const [{ ChatOpenAI }, settings] = await Promise.all([
6269
+ import("@langchain/openai"),
6270
+ getAiSettings(userName)
6271
+ ]);
6272
+ const modelKwargs = getOpenRouterModelKwargs(
6273
+ settings.OPENAI_API_ENDPOINT
6274
+ );
6275
+ return new ChatOpenAI({
6276
+ temperature: 0.2,
6277
+ modelName,
6278
+ apiKey: settings.OPENAI_API_KEY,
6279
+ ...Object.keys(modelKwargs).length ? { modelKwargs } : {},
6280
+ configuration: {
6281
+ baseURL: settings.OPENAI_API_ENDPOINT,
6282
+ defaultHeaders: {
6283
+ "HTTP-Referer": "https://tradejs.dev",
6284
+ "X-Title": "Inv"
6285
+ }
6286
+ }
6287
+ });
6288
+ })();
6289
+ modelPromise.catch(() => {
6290
+ aiModelCache.delete(cacheKey);
6291
+ });
6292
+ aiModelCache.set(cacheKey, modelPromise);
6293
+ }
6294
+ return modelPromise;
6295
+ };
6296
+ var getAiModel = async (userName = "root", modelName = DEFAULT_AI_MODEL) => {
6297
+ try {
6298
+ return await createAiModel(userName, modelName);
6299
+ } catch (error) {
6300
+ aiModelCache.delete(getAiModelCacheKey(userName, modelName));
6301
+ userSettingsCache.delete(userName);
6302
+ throw error;
6303
+ }
6304
+ };
6305
+ var ensureAiStrategyPluginsLoaded = async () => {
6306
+ await ensureStrategyPluginsLoaded();
6307
+ };
6308
+ var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
6309
+ if (options.signal) {
6310
+ await ensureAiStrategyPluginsLoaded();
6311
+ }
6312
+ const [{ HumanMessage, SystemMessage }, model] = await Promise.all([
6313
+ import("@langchain/core/messages"),
6314
+ getAiModel(options.userName, options.model)
5858
6315
  ]);
5859
- const { symbol } = signal;
5860
6316
  const messages = [];
5861
- const model = new ChatOpenAI({
5862
- temperature: 0.2,
5863
- //modelName: 'anthropic/claude-sonnet-4.5',
5864
- modelName: "google/gemini-3.1-pro-preview",
5865
- openAIApiKey: process.env.OPENAI_API_KEY,
5866
- configuration: {
5867
- baseURL: process.env.OPENAI_API_ENDPOINT || "https://api.openai.com/v1",
5868
- defaultHeaders: {
5869
- "HTTP-Referer": "https://tradejs.dev",
5870
- "X-Title": "Inv"
5871
- }
5872
- }
5873
- });
5874
- messages.push(new SystemMessage(buildAiSystemPrompt(signal)));
5875
- const payload = buildAiPayload(signal);
6317
+ messages.push(new SystemMessage(systemPrompt));
5876
6318
  messages.push(
5877
6319
  new HumanMessage({
5878
6320
  content: [
5879
6321
  {
5880
6322
  type: "text",
5881
- text: buildAiHumanPrompt(signal, payload)
6323
+ text: humanPrompt
5882
6324
  }
5883
6325
  ]
5884
6326
  })
@@ -5887,7 +6329,31 @@ var askAI = async (signal) => {
5887
6329
  const parsed = parseAIResponse(
5888
6330
  normalizeResponseContent(response.content)
5889
6331
  );
5890
- const content = normalizeAnalysis(parsed);
6332
+ const normalized = normalizeAnalysis(parsed);
6333
+ if (!options.signal) {
6334
+ return normalized;
6335
+ }
6336
+ return postProcessAiAnalysisByStrategy(
6337
+ options.signal,
6338
+ normalized,
6339
+ options.payload
6340
+ );
6341
+ };
6342
+ var askAI = async (signal, options = {}) => {
6343
+ const { symbol } = signal;
6344
+ await ensureAiStrategyPluginsLoaded();
6345
+ const payload = buildAiPayload(signal);
6346
+ const content = await runAiPrompt(
6347
+ {
6348
+ systemPrompt: buildAiSystemPrompt(signal),
6349
+ humanPrompt: buildAiHumanPrompt(signal, payload)
6350
+ },
6351
+ {
6352
+ ...options,
6353
+ signal,
6354
+ payload
6355
+ }
6356
+ );
5891
6357
  await (0, import_redis.setData)(import_redis.redisKeys.analysis(symbol, signal.signalId), content);
5892
6358
  return content;
5893
6359
  };
@@ -5897,66 +6363,428 @@ var import_promises = __toESM(require("fs/promises"));
5897
6363
  var import_path2 = __toESM(require("path"));
5898
6364
  var import_puppeteer = __toESM(require("puppeteer"));
5899
6365
  var import_async = require("@tradejs/core/async");
5900
- var import_redis2 = require("@tradejs/infra/redis");
6366
+ var import_logger3 = require("@tradejs/infra/logger");
6367
+ var import_userSettings2 = require("@tradejs/infra/userSettings");
5901
6368
  var { APP_URL } = process.env;
6369
+ var SCREENSHOT_NAVIGATION_ATTEMPTS = 3;
6370
+ var SCREENSHOT_NAVIGATION_RETRY_DELAY_MS = 2e3;
6371
+ var SCREENSHOT_CAPTURE_ATTEMPTS = 2;
6372
+ var SCREENSHOT_CAPTURE_RETRY_DELAY_MS = 2e3;
6373
+ var SCREENSHOT_NAVIGATION_TIMEOUT_MS = 3e4;
6374
+ var SCREENSHOT_RENDER_DELAY_MS = 1e4;
6375
+ var SCREENSHOT_CONSOLE_LOG_LIMIT = 20;
6376
+ var SCREENSHOT_CONSOLE_TEXT_LIMIT = 500;
6377
+ var SCREENSHOT_BROWSER_STDERR = process.env.SCREENSHOT_BROWSER_STDERR === "1";
6378
+ var PUPPETEER_DUMPIO = process.env.PUPPETEER_DUMPIO === "1";
6379
+ var SCREENSHOT_VIEWPORT = {
6380
+ width: 1280,
6381
+ height: 800,
6382
+ deviceScaleFactor: process.env.NODE_ENV === "production" ? 1 : 2
6383
+ };
5902
6384
  var getProjectRoot = (projectRoot) => import_path2.default.resolve(getTradejsProjectCwd(projectRoot));
5903
6385
  var getScreenshotsDir = (projectRoot) => import_path2.default.join(getProjectRoot(projectRoot), "data", "screenshots");
5904
- var getImageUrl = ({ symbol, signalId, interval }) => `${APP_URL}/api/files/screenshot/${symbol}_${signalId}_${interval}`;
6386
+ var maskTokenInUrl = (url) => url.replace(/([?&]token=)[^&]+/i, "$1<hidden>");
6387
+ var getErrorFields = (value) => {
6388
+ if (!value || typeof value !== "object") {
6389
+ return [];
6390
+ }
6391
+ const record = value;
6392
+ const fields = [
6393
+ ["name", record.name],
6394
+ ["code", record.code],
6395
+ ["errno", record.errno],
6396
+ ["type", record.type],
6397
+ ["syscall", record.syscall],
6398
+ ["hostname", record.hostname],
6399
+ ["host", record.host],
6400
+ ["address", record.address],
6401
+ ["port", record.port]
6402
+ ];
6403
+ return fields.filter(([, fieldValue]) => fieldValue != null && String(fieldValue).trim()).map(([key, fieldValue]) => `${key}=${String(fieldValue)}`);
6404
+ };
6405
+ var describeErrorValue = (value) => {
6406
+ if (value == null) {
6407
+ return "";
6408
+ }
6409
+ if (typeof value === "string") {
6410
+ return value;
6411
+ }
6412
+ if (value instanceof Error) {
6413
+ const message = value.message?.trim() || "";
6414
+ const fields = getErrorFields(value);
6415
+ return [message, ...fields].filter(Boolean).join(" ");
6416
+ }
6417
+ if (typeof value === "object") {
6418
+ const fields = getErrorFields(value);
6419
+ if (fields.length) {
6420
+ return fields.join(" ");
6421
+ }
6422
+ try {
6423
+ return JSON.stringify(value);
6424
+ } catch {
6425
+ return String(value);
6426
+ }
6427
+ }
6428
+ return String(value);
6429
+ };
6430
+ var getErrorMessage = (error) => {
6431
+ const maybeError = error;
6432
+ const message = describeErrorValue(error) || String(error);
6433
+ if (maybeError?.cause == null) {
6434
+ return message;
6435
+ }
6436
+ const cause = describeErrorValue(maybeError.cause);
6437
+ if (!cause) {
6438
+ return message;
6439
+ }
6440
+ return `${message}; cause: ${cause}`;
6441
+ };
6442
+ var truncateText = (value, maxLength = SCREENSHOT_CONSOLE_TEXT_LIMIT) => value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
6443
+ var shouldLogConsoleMessage = (type) => ["error", "warning", "assert"].includes(type);
6444
+ var getScreenshotBuffer = async (signal, projectRoot) => {
6445
+ const screenshotPath = getScreenshotPath(signal, projectRoot);
6446
+ return import_promises.default.readFile(screenshotPath);
6447
+ };
6448
+ var getScreenshotFilename = ({
6449
+ symbol,
6450
+ signalId,
6451
+ interval
6452
+ }) => `${symbol}_${signalId}_${interval}.png`;
6453
+ var getScreenshotRenderBaseUrl = () => {
6454
+ const fallback = String(APP_URL || "").trim();
6455
+ if (fallback) {
6456
+ return fallback;
6457
+ }
6458
+ throw new Error("APP_URL is required for screenshots");
6459
+ };
5905
6460
  var getScreenshotPath = ({ symbol, signalId, interval }, projectRoot) => {
5906
6461
  return import_path2.default.join(
5907
6462
  getScreenshotsDir(projectRoot),
5908
- `${symbol}_${signalId}_${interval}.png`
6463
+ getScreenshotFilename({ symbol, signalId, interval })
5909
6464
  );
5910
6465
  };
5911
- var screenDashboard = async (signal, projectRoot) => {
6466
+ var screenDashboard = async (signal, projectRoot, userName = "root") => {
5912
6467
  const { symbol, signalId, interval } = signal;
5913
- const rootUser = await (0, import_redis2.getData)(import_redis2.redisKeys.user("root"), null);
5914
- const token2 = rootUser && typeof rootUser === "object" ? rootUser.token : null;
5915
- const tokenParam = typeof token2 === "string" && token2.length > 0 ? `&token=${encodeURIComponent(token2)}` : "";
5916
- const browser = await import_puppeteer.default.launch({
5917
- headless: true,
5918
- executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
5919
- args: [
5920
- "--no-sandbox",
5921
- "--disable-setuid-sandbox",
5922
- "--disable-dev-shm-usage",
5923
- "--disable-gpu",
5924
- "--font-render-hinting=medium"
5925
- ]
5926
- });
5927
- try {
5928
- const page = await browser.newPage();
6468
+ const screenshotBaseUrl = getScreenshotRenderBaseUrl();
6469
+ const screenshotPath = getScreenshotPath(signal, projectRoot);
6470
+ const settings = await (0, import_userSettings2.getUserSettings)(userName);
6471
+ const token = settings.token;
6472
+ const tokenParam = typeof token === "string" && token.length > 0 ? `&token=${encodeURIComponent(token)}` : "";
6473
+ const dashboardUrl = `${screenshotBaseUrl}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}&autoZoom=true&screenshot=1${tokenParam}`;
6474
+ const maskedDashboardUrl = maskTokenInUrl(dashboardUrl);
6475
+ import_logger3.logger.info(
6476
+ "screenshot start: %s %sm url=%s path=%s",
6477
+ symbol,
6478
+ interval,
6479
+ maskedDashboardUrl,
6480
+ screenshotPath
6481
+ );
6482
+ let captureError = null;
6483
+ for (let captureAttempt = 1; captureAttempt <= SCREENSHOT_CAPTURE_ATTEMPTS; captureAttempt += 1) {
6484
+ const browser = await import_puppeteer.default.launch({
6485
+ headless: true,
6486
+ dumpio: PUPPETEER_DUMPIO,
6487
+ executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
6488
+ args: [
6489
+ "--no-sandbox",
6490
+ "--disable-setuid-sandbox",
6491
+ "--disable-dev-shm-usage",
6492
+ "--disable-gpu",
6493
+ "--font-render-hinting=medium"
6494
+ ]
6495
+ });
6496
+ const browserProcess = browser.process();
6497
+ let browserCloseRequested = false;
5929
6498
  try {
5930
- await page.setViewport({
5931
- width: 1400,
5932
- height: 960,
5933
- deviceScaleFactor: 2
5934
- });
5935
- await page.goto(
5936
- `${APP_URL}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}&autoZoom=true${tokenParam}`
6499
+ if (browserProcess) {
6500
+ import_logger3.logger.info(
6501
+ "screenshot browser: %s %sm captureAttempt=%d pid=%s exec=%s",
6502
+ symbol,
6503
+ interval,
6504
+ captureAttempt,
6505
+ String(browserProcess.pid ?? "unknown"),
6506
+ browserProcess.spawnfile || "unknown"
6507
+ );
6508
+ }
6509
+ import_logger3.logger.info(
6510
+ "screenshot browser version: %s %sm captureAttempt=%d version=%s",
6511
+ symbol,
6512
+ interval,
6513
+ captureAttempt,
6514
+ await browser.version()
5937
6515
  );
5938
- await (0, import_async.delay)(1e4);
5939
- await import_promises.default.mkdir(getScreenshotsDir(projectRoot), { recursive: true });
5940
- await page.screenshot({
5941
- path: getScreenshotPath(signal, projectRoot)
6516
+ browser.on("disconnected", () => {
6517
+ if (browserCloseRequested) {
6518
+ return;
6519
+ }
6520
+ import_logger3.logger.error(
6521
+ "screenshot browser disconnected: %s %sm captureAttempt=%d",
6522
+ symbol,
6523
+ interval,
6524
+ captureAttempt
6525
+ );
5942
6526
  });
6527
+ if (SCREENSHOT_BROWSER_STDERR && browserProcess?.stderr) {
6528
+ browserProcess.stderr.on("data", (chunk) => {
6529
+ const text = String(chunk).trim();
6530
+ if (!text) {
6531
+ return;
6532
+ }
6533
+ import_logger3.logger.error(
6534
+ "screenshot browser stderr: %s %sm captureAttempt=%d (%s)",
6535
+ symbol,
6536
+ interval,
6537
+ captureAttempt,
6538
+ truncateText(text)
6539
+ );
6540
+ });
6541
+ }
6542
+ const page = await browser.newPage();
6543
+ try {
6544
+ let consoleMessagesLogged = 0;
6545
+ page.on("requestfailed", (request) => {
6546
+ if (!request.isNavigationRequest() && request.resourceType() !== "document") {
6547
+ return;
6548
+ }
6549
+ import_logger3.logger.error(
6550
+ "screenshot request failed: %s %sm %s (%s)",
6551
+ symbol,
6552
+ interval,
6553
+ maskTokenInUrl(request.url()),
6554
+ request.failure()?.errorText || "unknown"
6555
+ );
6556
+ });
6557
+ page.on("pageerror", (error) => {
6558
+ import_logger3.logger.error(
6559
+ "screenshot page error: %s %sm (%s)",
6560
+ symbol,
6561
+ interval,
6562
+ error.message || String(error)
6563
+ );
6564
+ });
6565
+ page.on("error", (error) => {
6566
+ import_logger3.logger.error(
6567
+ "screenshot target error: %s %sm (%s)",
6568
+ symbol,
6569
+ interval,
6570
+ error.message || String(error)
6571
+ );
6572
+ });
6573
+ page.on("close", () => {
6574
+ import_logger3.logger.info(
6575
+ "screenshot page closed: %s %sm captureAttempt=%d",
6576
+ symbol,
6577
+ interval,
6578
+ captureAttempt
6579
+ );
6580
+ });
6581
+ page.on("console", (message) => {
6582
+ if (!shouldLogConsoleMessage(message.type())) {
6583
+ return;
6584
+ }
6585
+ if (consoleMessagesLogged >= SCREENSHOT_CONSOLE_LOG_LIMIT) {
6586
+ return;
6587
+ }
6588
+ consoleMessagesLogged += 1;
6589
+ const location = message.location();
6590
+ const suffix = location.url ? ` ${maskTokenInUrl(location.url)}:${location.lineNumber ?? 0}:${location.columnNumber ?? 0}` : "";
6591
+ import_logger3.logger.error(
6592
+ "screenshot console %s: %s %sm captureAttempt=%d (%s%s)",
6593
+ message.type(),
6594
+ symbol,
6595
+ interval,
6596
+ captureAttempt,
6597
+ truncateText(message.text()),
6598
+ suffix
6599
+ );
6600
+ });
6601
+ page.on("response", (response) => {
6602
+ const status = response.status();
6603
+ if (status < 400) {
6604
+ return;
6605
+ }
6606
+ const request = response.request();
6607
+ const resourceType = request.resourceType();
6608
+ if (!["document", "script", "stylesheet", "fetch", "xhr"].includes(
6609
+ resourceType
6610
+ )) {
6611
+ return;
6612
+ }
6613
+ import_logger3.logger.error(
6614
+ "screenshot response failed: %s %sm captureAttempt=%d status=%d type=%s url=%s",
6615
+ symbol,
6616
+ interval,
6617
+ captureAttempt,
6618
+ status,
6619
+ resourceType,
6620
+ maskTokenInUrl(response.url())
6621
+ );
6622
+ });
6623
+ await page.setViewport(SCREENSHOT_VIEWPORT);
6624
+ let gotoError = null;
6625
+ import_logger3.logger.info(
6626
+ "screenshot render: %s %sm captureAttempt=%d viewport=%dx%d@%d",
6627
+ symbol,
6628
+ interval,
6629
+ captureAttempt,
6630
+ SCREENSHOT_VIEWPORT.width,
6631
+ SCREENSHOT_VIEWPORT.height,
6632
+ SCREENSHOT_VIEWPORT.deviceScaleFactor
6633
+ );
6634
+ for (let attempt = 1; attempt <= SCREENSHOT_NAVIGATION_ATTEMPTS; attempt += 1) {
6635
+ try {
6636
+ import_logger3.logger.info(
6637
+ "screenshot goto: %s %sm captureAttempt=%d attempt=%d url=%s",
6638
+ symbol,
6639
+ interval,
6640
+ captureAttempt,
6641
+ attempt,
6642
+ maskedDashboardUrl
6643
+ );
6644
+ const response = await page.goto(dashboardUrl, {
6645
+ waitUntil: "domcontentloaded",
6646
+ timeout: SCREENSHOT_NAVIGATION_TIMEOUT_MS
6647
+ });
6648
+ import_logger3.logger.info(
6649
+ "screenshot goto ok: %s %sm captureAttempt=%d attempt=%d status=%s finalUrl=%s",
6650
+ symbol,
6651
+ interval,
6652
+ captureAttempt,
6653
+ attempt,
6654
+ response ? String(response.status()) : "null",
6655
+ maskTokenInUrl(page.url())
6656
+ );
6657
+ import_logger3.logger.info(
6658
+ "screenshot page info: %s %sm captureAttempt=%d title=%s ua=%s",
6659
+ symbol,
6660
+ interval,
6661
+ captureAttempt,
6662
+ truncateText(await page.title(), 120),
6663
+ truncateText(await page.evaluate(() => navigator.userAgent), 240)
6664
+ );
6665
+ gotoError = null;
6666
+ break;
6667
+ } catch (error) {
6668
+ gotoError = error;
6669
+ import_logger3.logger.error(
6670
+ "screenshot goto failed: %s %sm captureAttempt=%d attempt=%d url=%s (%s)",
6671
+ symbol,
6672
+ interval,
6673
+ captureAttempt,
6674
+ attempt,
6675
+ maskedDashboardUrl,
6676
+ gotoError.message || String(gotoError)
6677
+ );
6678
+ if (attempt >= SCREENSHOT_NAVIGATION_ATTEMPTS) {
6679
+ break;
6680
+ }
6681
+ await (0, import_async.delay)(SCREENSHOT_NAVIGATION_RETRY_DELAY_MS);
6682
+ }
6683
+ }
6684
+ if (gotoError) {
6685
+ throw new Error(
6686
+ `Failed to open dashboard ${dashboardUrl}: ${gotoError.message || String(gotoError)}`
6687
+ );
6688
+ }
6689
+ await (0, import_async.delay)(SCREENSHOT_RENDER_DELAY_MS);
6690
+ await import_promises.default.mkdir(getScreenshotsDir(projectRoot), { recursive: true });
6691
+ await page.screenshot({
6692
+ path: screenshotPath,
6693
+ type: "png"
6694
+ });
6695
+ import_logger3.logger.info(
6696
+ "screenshot saved: %s %sm path=%s",
6697
+ symbol,
6698
+ interval,
6699
+ screenshotPath
6700
+ );
6701
+ return;
6702
+ } finally {
6703
+ await page.close().catch(() => void 0);
6704
+ }
6705
+ } catch (error) {
6706
+ captureError = error;
6707
+ import_logger3.logger.error(
6708
+ "screenshot capture failed: %s %sm captureAttempt=%d (%s)",
6709
+ symbol,
6710
+ interval,
6711
+ captureAttempt,
6712
+ getErrorMessage(captureError)
6713
+ );
6714
+ if (captureAttempt < SCREENSHOT_CAPTURE_ATTEMPTS) {
6715
+ await (0, import_async.delay)(SCREENSHOT_CAPTURE_RETRY_DELAY_MS);
6716
+ }
5943
6717
  } finally {
5944
- await page.close();
6718
+ browserCloseRequested = true;
6719
+ await browser.close().catch(() => void 0);
5945
6720
  }
5946
- } finally {
5947
- await browser.close();
5948
6721
  }
6722
+ throw captureError || new Error(`Failed screenshot: ${symbol} ${interval}`);
5949
6723
  };
5950
6724
 
5951
6725
  // src/signals.ts
6726
+ var import_async2 = require("@tradejs/core/async");
5952
6727
  var import_math = require("@tradejs/core/math");
5953
- var import_logger3 = require("@tradejs/infra/logger");
6728
+ var import_logger4 = require("@tradejs/infra/logger");
6729
+ var import_userSettings3 = require("@tradejs/infra/userSettings");
5954
6730
  var escapeHtml = (s) => {
5955
6731
  if (!s) return "";
5956
6732
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
5957
6733
  };
5958
- var { APP_URL: APP_URL2, TG_BOT_TOKEN: token, TG_CHAT_ID: chatId } = process.env;
6734
+ var { APP_URL: APP_URL2 } = process.env;
6735
+ var TG_REQUEST_ATTEMPTS = 3;
6736
+ var TG_REQUEST_RETRY_DELAY_MS = 2e3;
6737
+ var getErrorFields2 = (value) => {
6738
+ if (!value || typeof value !== "object") {
6739
+ return [];
6740
+ }
6741
+ const record = value;
6742
+ const fields = [
6743
+ ["name", record.name],
6744
+ ["code", record.code],
6745
+ ["errno", record.errno],
6746
+ ["type", record.type],
6747
+ ["syscall", record.syscall],
6748
+ ["hostname", record.hostname],
6749
+ ["host", record.host],
6750
+ ["address", record.address],
6751
+ ["port", record.port]
6752
+ ];
6753
+ return fields.filter(([, fieldValue]) => fieldValue != null && String(fieldValue).trim()).map(([key, fieldValue]) => `${key}=${String(fieldValue)}`);
6754
+ };
6755
+ var describeErrorValue2 = (value) => {
6756
+ if (value == null) {
6757
+ return "";
6758
+ }
6759
+ if (typeof value === "string") {
6760
+ return value;
6761
+ }
6762
+ if (value instanceof Error) {
6763
+ const message = value.message?.trim() || "";
6764
+ const fields = getErrorFields2(value);
6765
+ return [message, ...fields].filter(Boolean).join(" ");
6766
+ }
6767
+ if (typeof value === "object") {
6768
+ const fields = getErrorFields2(value);
6769
+ if (fields.length) {
6770
+ return fields.join(" ");
6771
+ }
6772
+ try {
6773
+ return JSON.stringify(value);
6774
+ } catch {
6775
+ return String(value);
6776
+ }
6777
+ }
6778
+ return String(value);
6779
+ };
5959
6780
  var normalizeQuality = (value) => typeof value === "number" ? Math.max(1, Math.min(5, Math.round(value))) : null;
6781
+ var formatOrderSkipReason = (reason) => {
6782
+ if (!reason) return "";
6783
+ if (reason.startsWith("AI_QUALITY_BELOW_MIN")) {
6784
+ return "AI_QUALITY_BELOW_MIN";
6785
+ }
6786
+ return reason;
6787
+ };
5960
6788
  var getLastNumber = (value) => {
5961
6789
  if (Array.isArray(value)) {
5962
6790
  const last = value[value.length - 1];
@@ -5976,6 +6804,121 @@ var getAiQualityLine = (analysis) => {
5976
6804
  }
5977
6805
  return `\u{1F534} AI Quality: ${quality}/5`;
5978
6806
  };
6807
+ var getTelegramErrorReason = (data) => {
6808
+ if (data && typeof data === "object") {
6809
+ const record = data;
6810
+ const description = typeof record.description === "string" ? record.description : "";
6811
+ const errorCode = typeof record.error_code === "number" ? record.error_code : void 0;
6812
+ if (description && errorCode != null) {
6813
+ return `${errorCode}: ${description}`;
6814
+ }
6815
+ if (description) {
6816
+ return description;
6817
+ }
6818
+ }
6819
+ try {
6820
+ return JSON.stringify(data);
6821
+ } catch {
6822
+ return String(data);
6823
+ }
6824
+ };
6825
+ var getErrorMessage2 = (error) => {
6826
+ const maybeError = error;
6827
+ const message = describeErrorValue2(error) || String(error);
6828
+ if (maybeError?.cause == null) {
6829
+ return message;
6830
+ }
6831
+ const cause = describeErrorValue2(maybeError.cause);
6832
+ if (!cause) {
6833
+ return message;
6834
+ }
6835
+ return `${message}; cause: ${cause}`;
6836
+ };
6837
+ var parseTelegramResponse = async (response) => {
6838
+ const fallback = {
6839
+ ok: response.ok,
6840
+ error_code: response.status,
6841
+ description: response.statusText
6842
+ };
6843
+ try {
6844
+ return await response.json();
6845
+ } catch {
6846
+ try {
6847
+ const text = await response.text();
6848
+ return text ? {
6849
+ ...fallback,
6850
+ description: text
6851
+ } : fallback;
6852
+ } catch {
6853
+ return fallback;
6854
+ }
6855
+ }
6856
+ };
6857
+ var getTelegramSettings = async (userName = "root") => {
6858
+ const settings = await (0, import_userSettings3.getUserSettings)(userName);
6859
+ const token = settings.TG_BOT_TOKEN;
6860
+ const chatId = settings.TG_CHAT_ID;
6861
+ if (!token || !chatId) {
6862
+ throw new Error(`Telegram settings are incomplete for user ${userName}`);
6863
+ }
6864
+ return { token, chatId };
6865
+ };
6866
+ var requestTelegram = async ({
6867
+ method,
6868
+ token,
6869
+ init
6870
+ }) => {
6871
+ let lastError = null;
6872
+ for (let attempt = 1; attempt <= TG_REQUEST_ATTEMPTS; attempt += 1) {
6873
+ try {
6874
+ const res = await fetch(
6875
+ `https://api.telegram.org/bot${token}/${method}`,
6876
+ {
6877
+ ...init
6878
+ }
6879
+ );
6880
+ return await parseTelegramResponse(res);
6881
+ } catch (error) {
6882
+ lastError = error;
6883
+ import_logger4.logger.error(
6884
+ "tg %s network failed: attempt=%d (%s)",
6885
+ method,
6886
+ attempt,
6887
+ getErrorMessage2(error)
6888
+ );
6889
+ if (attempt < TG_REQUEST_ATTEMPTS) {
6890
+ await (0, import_async2.delay)(TG_REQUEST_RETRY_DELAY_MS);
6891
+ }
6892
+ }
6893
+ }
6894
+ throw lastError || new Error(`Telegram ${method} request failed`);
6895
+ };
6896
+ var sendTelegramMessage = async ({
6897
+ message,
6898
+ markup,
6899
+ token,
6900
+ chatId
6901
+ }) => {
6902
+ const data = await requestTelegram({
6903
+ method: "sendMessage",
6904
+ token,
6905
+ init: {
6906
+ method: "POST",
6907
+ headers: { "Content-Type": "application/json" },
6908
+ body: JSON.stringify({
6909
+ chat_id: chatId,
6910
+ text: message,
6911
+ reply_markup: markup,
6912
+ parse_mode: "HTML"
6913
+ })
6914
+ }
6915
+ });
6916
+ import_logger4.logger.info(
6917
+ "tg sendMessage: %s",
6918
+ data?.ok ? "sent" : getTelegramErrorReason(data)
6919
+ );
6920
+ return data;
6921
+ };
5979
6922
  var formatMessage = (signal, analysis) => {
5980
6923
  const {
5981
6924
  symbol,
@@ -6027,7 +6970,9 @@ var formatMessage = (signal, analysis) => {
6027
6970
  }
6028
6971
  lines.push(orderStatusText);
6029
6972
  if ((orderStatus === "skipped" || orderStatus === "canceled") && orderSkipReason) {
6030
- lines.push(`Skip reason: <b>${escapeHtml(orderSkipReason)}</b>`);
6973
+ lines.push(
6974
+ `Skip reason: <b>${escapeHtml(formatOrderSkipReason(orderSkipReason))}</b>`
6975
+ );
6031
6976
  }
6032
6977
  }
6033
6978
  if (isConfigFromBacktest) {
@@ -6045,21 +6990,9 @@ var formatMessage = (signal, analysis) => {
6045
6990
  lines.push(aiQualityLine);
6046
6991
  }
6047
6992
  lines.push("");
6048
- if (touches) {
6049
- lines.push(`Points: ${touches}`);
6050
- }
6051
- if (atrPct != null && Number.isFinite(atrPct)) {
6052
- lines.push(`ATR: ${atrPct.toFixed(2)}`);
6053
- }
6054
- if (distance) {
6055
- lines.push(`Distance: ${distance}`);
6056
- }
6057
6993
  if (correlation) {
6058
6994
  lines.push(`BTC correlation: ${correlation}`);
6059
6995
  }
6060
- if (spread != null && Number.isFinite(spread)) {
6061
- lines.push(`BTC spread (CB-BN)/BN: ${spread.toFixed(6)}`);
6062
- }
6063
6996
  const prices = formatPrices();
6064
6997
  if (prices) {
6065
6998
  lines.push("");
@@ -6073,58 +7006,88 @@ var formatMessage = (signal, analysis) => {
6073
7006
  \u0414\u0435\u0442\u0430\u043B\u0438: ${err.message || String(err)}`;
6074
7007
  }
6075
7008
  };
6076
- var sendSignal = async (signal, imgInterval, analysis) => {
7009
+ var sendSignal = async (signal, imgInterval, analysis, options = {}) => {
6077
7010
  const { symbol, signalId, interval } = signal;
7011
+ const { token, chatId } = await getTelegramSettings(options.userName);
6078
7012
  const message = formatMessage(signal, analysis);
6079
- const markup = {
6080
- inline_keyboard: [
6081
- [
6082
- {
6083
- text: "Dashboard",
6084
- url: `${APP_URL2}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}`
6085
- }
6086
- ]
6087
- ]
6088
- };
6089
- if (!APP_URL2?.startsWith("https")) {
6090
- await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
6091
- method: "POST",
6092
- headers: { "Content-Type": "application/json" },
6093
- body: JSON.stringify({
6094
- chat_id: chatId,
6095
- text: message,
6096
- parse_mode: "HTML"
6097
- })
7013
+ const publicAppUrl = APP_URL2?.startsWith("https") ? APP_URL2 : null;
7014
+ const dashboardUrl = publicAppUrl ? `${APP_URL2}/routes/dashboard/bybit/${symbol}/${interval}/?signalId=${signalId}` : null;
7015
+ const actionButtons = [
7016
+ dashboardUrl ? { text: "Dashboard", url: dashboardUrl } : null
7017
+ ].filter(Boolean);
7018
+ const markup = actionButtons.length ? {
7019
+ inline_keyboard: [actionButtons]
7020
+ } : void 0;
7021
+ let screenshotBytes = null;
7022
+ try {
7023
+ const screenshot = await getScreenshotBuffer({
7024
+ ...signal,
7025
+ interval: imgInterval
7026
+ });
7027
+ screenshotBytes = screenshot.buffer.slice(
7028
+ screenshot.byteOffset,
7029
+ screenshot.byteOffset + screenshot.byteLength
7030
+ );
7031
+ } catch (error) {
7032
+ import_logger4.logger.error(
7033
+ "tg screenshot unavailable: %s (%s)",
7034
+ symbol,
7035
+ error?.message || String(error)
7036
+ );
7037
+ await sendTelegramMessage({ message, markup, token, chatId });
7038
+ return;
7039
+ }
7040
+ const photoBody = new FormData();
7041
+ photoBody.set("chat_id", String(chatId || ""));
7042
+ photoBody.set(
7043
+ "photo",
7044
+ new Blob([screenshotBytes], { type: "image/png" }),
7045
+ getScreenshotFilename({ ...signal, interval: imgInterval })
7046
+ );
7047
+ photoBody.set("caption", message);
7048
+ photoBody.set("parse_mode", "HTML");
7049
+ if (markup) {
7050
+ photoBody.set("reply_markup", JSON.stringify(markup));
7051
+ }
7052
+ let data;
7053
+ try {
7054
+ data = await requestTelegram({
7055
+ method: "sendPhoto",
7056
+ token,
7057
+ init: {
7058
+ method: "POST",
7059
+ body: photoBody
7060
+ }
7061
+ });
7062
+ } catch (error) {
7063
+ const reason = getErrorMessage2(error);
7064
+ import_logger4.logger.error("tg sendPhoto request failed: %s", reason);
7065
+ await sendTelegramMessage({
7066
+ message: `${message}
7067
+
7068
+ \u26A0\uFE0F <b>Photo delivery failed</b>
7069
+ Reason: <code>${escapeHtml(reason)}</code>`,
7070
+ markup,
7071
+ token,
7072
+ chatId
6098
7073
  });
6099
7074
  return;
6100
7075
  }
6101
- const imageUrl = getImageUrl({ ...signal, interval: imgInterval });
6102
- const res = await fetch(`https://api.telegram.org/bot${token}/sendPhoto`, {
6103
- method: "POST",
6104
- headers: { "Content-Type": "application/json" },
6105
- body: JSON.stringify({
6106
- chat_id: chatId,
6107
- photo: imageUrl,
6108
- caption: message,
6109
- reply_markup: markup,
6110
- parse_mode: "HTML"
6111
- })
6112
- });
6113
- const data = await res.json();
6114
7076
  if (!data?.ok) {
6115
- import_logger3.logger.error("tg sendPhoto failed: %s", JSON.stringify(data));
6116
- await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
6117
- method: "POST",
6118
- headers: { "Content-Type": "application/json" },
6119
- body: JSON.stringify({
6120
- chat_id: chatId,
6121
- text: message,
6122
- reply_markup: markup,
6123
- parse_mode: "HTML"
6124
- })
7077
+ const reason = getTelegramErrorReason(data);
7078
+ import_logger4.logger.error("tg sendPhoto failed: %s", JSON.stringify(data));
7079
+ await sendTelegramMessage({
7080
+ message: `${message}
7081
+
7082
+ \u26A0\uFE0F <b>Photo delivery failed</b>
7083
+ Reason: <code>${escapeHtml(reason)}</code>`,
7084
+ markup,
7085
+ token,
7086
+ chatId
6125
7087
  });
7088
+ return;
6126
7089
  }
6127
- import_logger3.logger.info("tg sendPhoto: %s", data?.ok ? "sent" : JSON.stringify(data));
7090
+ import_logger4.logger.info("tg sendPhoto: sent");
6128
7091
  };
6129
7092
  var formatAnalysisMessage = (signal, analysis) => {
6130
7093
  const lines = [];
@@ -6168,21 +7131,25 @@ ${escapeHtml(clean)}`);
6168
7131
  }
6169
7132
  return lines.join("\n");
6170
7133
  };
6171
- var sendSignalAnalysis = async (signal, analysis) => {
7134
+ var sendSignalAnalysis = async (signal, analysis, options = {}) => {
7135
+ const { token, chatId } = await getTelegramSettings(options.userName);
6172
7136
  const message = formatAnalysisMessage(signal, analysis);
6173
- const res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
6174
- method: "POST",
6175
- headers: { "Content-Type": "application/json" },
6176
- body: JSON.stringify({
6177
- chat_id: chatId,
6178
- text: message,
6179
- parse_mode: "HTML"
6180
- })
7137
+ const data = await requestTelegram({
7138
+ method: "sendMessage",
7139
+ token,
7140
+ init: {
7141
+ method: "POST",
7142
+ headers: { "Content-Type": "application/json" },
7143
+ body: JSON.stringify({
7144
+ chat_id: chatId,
7145
+ text: message,
7146
+ parse_mode: "HTML"
7147
+ })
7148
+ }
6181
7149
  });
6182
- const data = await res.json();
6183
- import_logger3.logger.info(
7150
+ import_logger4.logger.info(
6184
7151
  "tg sendMessage (analysis): %s",
6185
- data?.ok ? "sent" : JSON.stringify(data)
7152
+ data?.ok ? "sent" : getTelegramErrorReason(data)
6186
7153
  );
6187
7154
  };
6188
7155
 
@@ -6196,7 +7163,7 @@ var cleanFiles = async (dir) => {
6196
7163
  total: files.length,
6197
7164
  width: 30
6198
7165
  });
6199
- import_logger4.logger.info(import_chalk.default.yellow("clean:", dir));
7166
+ import_logger5.logger.info(import_chalk.default.yellow("clean:", dir));
6200
7167
  for await (const file of files) {
6201
7168
  completed++;
6202
7169
  const fullPath = `${dir}/${file}`;
@@ -6210,41 +7177,41 @@ var cleanFiles = async (dir) => {
6210
7177
  bar.tick(completed === files.length ? completed % 100 : 100);
6211
7178
  }
6212
7179
  }
6213
- import_logger4.logger.info("");
7180
+ import_logger5.logger.info("");
6214
7181
  };
6215
7182
  var cleanRedis = async (area) => {
6216
7183
  let completed = 0;
6217
- const keys = await (0, import_redis3.getKeys)(area);
7184
+ const keys = await (0, import_redis2.getKeys)(area);
6218
7185
  const bar = new import_progress.default(":current/:total [:bar][:percent] :eta(s)", {
6219
7186
  total: keys.length,
6220
7187
  width: 30
6221
7188
  });
6222
- import_logger4.logger.info(import_chalk.default.yellow("clean:", area));
7189
+ import_logger5.logger.info(import_chalk.default.yellow("clean:", area));
6223
7190
  try {
6224
7191
  for await (const key of keys) {
6225
7192
  completed++;
6226
- await (0, import_redis3.delKeyWithOptions)(key, { raiseOnMisconf: true });
7193
+ await (0, import_redis2.delKeyWithOptions)(key, { raiseOnMisconf: true });
6227
7194
  if (completed % 100 === 0 || completed === keys.length) {
6228
7195
  bar.tick(completed === keys.length ? completed % 100 : 100);
6229
7196
  }
6230
7197
  }
6231
7198
  } catch (e) {
6232
- if (e instanceof import_redis3.RedisWriteBlockedError || String(e).includes("MISCONF")) {
6233
- import_logger4.logger.error(
7199
+ if (e instanceof import_redis2.RedisWriteBlockedError || String(e).includes("MISCONF")) {
7200
+ import_logger5.logger.error(
6234
7201
  "Redis write is blocked by MISCONF (RDB save failure). Cleanup stopped early."
6235
7202
  );
6236
- import_logger4.logger.error(
7203
+ import_logger5.logger.error(
6237
7204
  "Check Redis logs and fix persistence/memory pressure before retry."
6238
7205
  );
6239
- import_logger4.logger.info("");
7206
+ import_logger5.logger.info("");
6240
7207
  return;
6241
7208
  }
6242
7209
  throw e;
6243
7210
  }
6244
- import_logger4.logger.info("");
7211
+ import_logger5.logger.info("");
6245
7212
  };
6246
- var update = async (connector, interval, tickers) => {
6247
- const PRELOAD_START = (0, import_time.getTimestamp)(import_constants.PRELOAD_DAYS);
7213
+ var update = async (connector, interval, tickers, preloadDays = import_constants.PRELOAD_DAYS) => {
7214
+ const PRELOAD_START = (0, import_time.getTimestamp)(preloadDays);
6248
7215
  const PRELOAD_END = (0, import_time.getTimestamp)();
6249
7216
  const bar = new import_progress.default(
6250
7217
  ":current/:total [:bar][:percent] :eta(s) :symbol",
@@ -6253,27 +7220,32 @@ var update = async (connector, interval, tickers) => {
6253
7220
  width: 30
6254
7221
  }
6255
7222
  );
6256
- import_logger4.logger.info(import_chalk.default.yellow("update:", tickers.length));
7223
+ import_logger5.logger.info(
7224
+ import_chalk.default.yellow(
7225
+ `update: ${tickers.length} (klineConcurrency=${KLINE_CONCURRENCY_LIMIT}, preloadDays=${preloadDays})`
7226
+ )
7227
+ );
6257
7228
  const queue = tickers.slice();
6258
7229
  if (!queue.includes("BTCUSDT")) {
6259
7230
  queue.unshift("BTCUSDT");
6260
7231
  }
6261
- await (0, import_async2.runWithConcurrency)(queue, KLINE_CONCURRENCY_LIMIT, async (symbol) => {
7232
+ await (0, import_async3.runWithConcurrency)(queue, KLINE_CONCURRENCY_LIMIT, async (symbol) => {
6262
7233
  try {
6263
7234
  await connector.kline({
6264
7235
  symbol,
6265
7236
  start: PRELOAD_START,
6266
7237
  end: PRELOAD_END,
6267
7238
  interval,
6268
- silent: true
7239
+ silent: true,
7240
+ warmOnly: true
6269
7241
  });
6270
7242
  } catch {
6271
- import_logger4.logger.error("Failed loading: %s", symbol);
7243
+ import_logger5.logger.error("Failed loading: %s", symbol);
6272
7244
  } finally {
6273
7245
  bar.tick(1, { symbol: import_chalk.default.gray(symbol) });
6274
7246
  }
6275
7247
  });
6276
- import_logger4.logger.info("");
7248
+ import_logger5.logger.info("");
6277
7249
  };
6278
7250
  var parseSymbolsFromCLI = (symbol = "") => symbol.split(",").map((s) => {
6279
7251
  const ticker = s.toUpperCase();
@@ -6311,14 +7283,14 @@ var getTickers = async (connector, include = "", exclude = "", limit, chunk) =>
6311
7283
  }
6312
7284
  if (chunk) {
6313
7285
  const [currentChunk, chunksCount] = chunk.split("/").map((c) => parseInt(c));
6314
- import_logger4.logger.info("chunk: %d / %d", currentChunk, chunksCount);
7286
+ import_logger5.logger.info("chunk: %d / %d", currentChunk, chunksCount);
6315
7287
  const chunkSize = Math.ceil(tickers.length / chunksCount);
6316
7288
  const chunks = import_lodash.default.chunk(tickers, chunkSize);
6317
7289
  tickers = chunks[currentChunk - 1];
6318
7290
  }
6319
7291
  return tickers.filter((t) => !excludeTickers.includes(t));
6320
7292
  };
6321
- var makeScreenshots = async (signals, interval) => {
7293
+ var makeScreenshots = async (signals, interval, userName = "root") => {
6322
7294
  const projectRoot = getProjectRoot2();
6323
7295
  const bar = new import_progress.default(
6324
7296
  ":current/:total [:bar][:percent] :eta(s) :symbol",
@@ -6327,15 +7299,15 @@ var makeScreenshots = async (signals, interval) => {
6327
7299
  width: 30
6328
7300
  }
6329
7301
  );
6330
- import_logger4.logger.info(import_chalk.default.yellow("screenshots:", `${interval}m`, signals.length));
6331
- await (0, import_async2.runWithConcurrency)(
7302
+ import_logger5.logger.info(import_chalk.default.yellow("screenshots:", `${interval}m`, signals.length));
7303
+ await (0, import_async3.runWithConcurrency)(
6332
7304
  signals,
6333
7305
  SCREENSHOT_CONCURRENCY_LIMIT,
6334
7306
  async (signal) => {
6335
7307
  try {
6336
- await screenDashboard({ ...signal, interval }, projectRoot);
7308
+ await screenDashboard({ ...signal, interval }, projectRoot, userName);
6337
7309
  } catch (error) {
6338
- import_logger4.logger.error(
7310
+ import_logger5.logger.error(
6339
7311
  "Failed screenshot: %s (%s)",
6340
7312
  signal.symbol,
6341
7313
  error?.message || String(error)
@@ -6345,9 +7317,9 @@ var makeScreenshots = async (signals, interval) => {
6345
7317
  }
6346
7318
  }
6347
7319
  );
6348
- import_logger4.logger.info("");
7320
+ import_logger5.logger.info("");
6349
7321
  };
6350
- var sendToAI = async (signals) => {
7322
+ var sendToAI = async (signals, userName = "root") => {
6351
7323
  const bar = new import_progress.default(
6352
7324
  ":current/:total [:bar][:percent] :eta(s) :symbol",
6353
7325
  {
@@ -6355,19 +7327,19 @@ var sendToAI = async (signals) => {
6355
7327
  width: 30
6356
7328
  }
6357
7329
  );
6358
- import_logger4.logger.info(import_chalk.default.yellow("AI:", signals.length));
6359
- await (0, import_async2.runWithConcurrency)(signals, AI_CONCURRENCY_LIMIT, async (signal) => {
7330
+ import_logger5.logger.info(import_chalk.default.yellow("AI:", signals.length));
7331
+ await (0, import_async3.runWithConcurrency)(signals, AI_CONCURRENCY_LIMIT, async (signal) => {
6360
7332
  try {
6361
- await askAI(signal);
7333
+ await askAI(signal, { userName });
6362
7334
  } catch {
6363
- import_logger4.logger.error("Failed ask: %s", signal.symbol);
7335
+ import_logger5.logger.error("Failed ask: %s", signal.symbol);
6364
7336
  } finally {
6365
7337
  bar.tick(1, { symbol: import_chalk.default.gray(signal.symbol) });
6366
7338
  }
6367
7339
  });
6368
- import_logger4.logger.info("");
7340
+ import_logger5.logger.info("");
6369
7341
  };
6370
- var sendToTG = async (signals, imgInterval) => {
7342
+ var sendToTG = async (signals, imgInterval, userName = "root") => {
6371
7343
  const bar = new import_progress.default(
6372
7344
  ":current/:total [:bar][:percent] :eta(s) :symbol",
6373
7345
  {
@@ -6375,19 +7347,19 @@ var sendToTG = async (signals, imgInterval) => {
6375
7347
  width: 30
6376
7348
  }
6377
7349
  );
6378
- import_logger4.logger.info(import_chalk.default.yellow("messages:", signals.length));
6379
- await (0, import_async2.runWithConcurrency)(signals, TG_CONCURRENCY_LIMIT, async (signal) => {
7350
+ import_logger5.logger.info(import_chalk.default.yellow("messages:", signals.length));
7351
+ await (0, import_async3.runWithConcurrency)(signals, 1, async (signal) => {
6380
7352
  try {
6381
- const analysis = await (0, import_redis3.getData)(
6382
- import_redis3.redisKeys.analysis(signal.symbol, signal.signalId),
7353
+ const analysis = await (0, import_redis2.getData)(
7354
+ import_redis2.redisKeys.analysis(signal.symbol, signal.signalId),
6383
7355
  null
6384
7356
  );
6385
- await sendSignal(signal, imgInterval, analysis);
7357
+ await sendSignal(signal, imgInterval, analysis, { userName });
6386
7358
  if (analysis && typeof analysis === "object" && Object.keys(analysis).length > 0) {
6387
- await sendSignalAnalysis(signal, analysis);
7359
+ await sendSignalAnalysis(signal, analysis, { userName });
6388
7360
  }
6389
7361
  } catch (err) {
6390
- import_logger4.logger.error(
7362
+ import_logger5.logger.error(
6391
7363
  "Failed sent: %s %s",
6392
7364
  signal.symbol,
6393
7365
  err?.message || String(err)
@@ -6396,7 +7368,7 @@ var sendToTG = async (signals, imgInterval) => {
6396
7368
  bar.tick(1, { symbol: import_chalk.default.gray(signal.symbol) });
6397
7369
  }
6398
7370
  });
6399
- import_logger4.logger.info("");
7371
+ import_logger5.logger.info("");
6400
7372
  };
6401
7373
  // Annotate the CommonJS export names for ESM import in node:
6402
7374
  0 && (module.exports = {