Существующие решения на GitHub имеют фатальные изъяны. Разберём несколько примеров — плюсы и минусы.

TauricResearch/TradingAgents

Ссылка на исходный код

Пайплайн, который тянет посты из Reddit, X, Bloomberg, Reuters и Yahoo Finance. Два агента затем спорят друг с другом: один доказывает, что цена вырастет, другой — что упадёт. Вы задаёте количество раундов дебатов — в конце побеждают аргументы одного из агентов.

Эта программа создаёт иллюзию работы. Если залезть в код, видно, что рою агентов скармливается дамп сырых индикаторов. LLM технически не способны правильно обработать это в текстовом диалоге из-за проблемы приоритизации.

LLM учат быстрее обосновать ответ и не рассуждать тщательно. Когда им скармливают сырые индикаторы — скажем, RSI и Stoch RSI одновременно — ответ агента определяется тем, какой из них он подхватит первым. При этом ситуация, когда один индикатор говорит о перекупленности, а другой — о перепроданности, крайне распространена. Цепочка рассуждений выглядит так:

  1. RSI — перепродан, теоретически цена должна вырасти

  2. Инструмента для проверки этой гипотезы через бэктест нет, так что мы об этом не думаем

  3. Другой индикатор перекуплен — я на это больше не смотрю, ответ уже есть

Если жёстко прописать веса приоритетов для индикаторов, система перестаёт быть адаптивной и начинает сливать деньги. В этой статье я показал, как построить среду, позволяющую агенту динамически выставлять приоритеты индикаторов, — но цена высока: 200k токенов на один бэктест ($1.20 для Haiku 4.5, $3.60 для Sonnet 4.6, $6.00 для Opus 4.6). Обновляйте редко — возвращаетесь к статичным приоритетам. Иными словами, TauricResearch — это просто подбрасывание монетки.

Уберите индикаторы совсем — получите эффект пылесоса. У вас есть посты с Reddit, но вы понятия не имеете, один ли это человек пишет своё мнение с 10 аккаунтов или 10 по-настоящему разных людей. Есть ещё проблема платных API: подключиться к X (Twitter) стоит $200/месяц. Если вы полагаетесь на бесплатные API (вроде Mastodon), блогер, чьи прогнозы отслеживал ваш агент, может выгореть и перестать постить.

node-ccxt-backtest

Ссылка на исходный код

Опираясь на их неудачный опыт, я пошёл другим путём: 8 агентов ищут целевые новости по разным темам.

  • Баланс — On-chain резервы. Отток с бирж, предложение LTH, доля неликвидного предложения, HODL-волны.

  • Движение денег — Потоки капитала. Чистые притоки в ETF, давление продаж майнеров, притоки стейблкоинов на биржи, OTC-объём.

  • Фундаментальные метрики — Здоровье сети. Хэшрейт, MVRV ratio, NVT ratio, модель Stock-to-Flow.

  • Доходы сети — Комиссии транзакций, пропускная способность Lightning, активность бондов, TVL DeFi.

  • Инсайдерские транзакции — Умные деньги. Покупки MicroStrategy, активы ETF BlackRock, Grayscale GBTC, государственные кошельки.

  • Новости актива — Рыночный сентимент. Регуляторные события, взломы бирж, институциональное принятие.

  • Глобальный макро — Макросреда. Решения ФРС по ставке, сюрпризы CPI, индекс DXY, индекс Fear & Greed, денежная масса M2.

  • История цены — Аномальные объёмы, подтверждения пробоев, закрытые сделки.

В идеальном мире это должно работать. Однако есть метрики портфеля, которые необходимо учитывать: max drawdown и sharpe ratio. Проблема в том, что подобный анализ слишком фундаментален и слишком оторван от реальных рыночных условий — где президент США шитпостит в twitter.

Почему оба подхода не работают

Это одно и то же в разных масштабах: статичные запросы к умным источникам информации и статичные запросы к тупым источникам информации. Проблема в самом подходе: запрос должен адаптироваться под новые рыночные условия, которые каждый день разные.

Паттерн «Рассуждение + Действие»

Это паттерн LLM-агента, при котором модель чередует мышление и действие. Упрощённо: между шагами рассуждения дайте агенту возможность искать в ленте новостей — тогда он будет знать, на чём сосредоточиться: на фундаментальном сломе или локальном отскоке.

Решение

Я написал следующий код. Повезло с тайминг: президент США решил вмешаться в ситуацию с Ираном и любезно предоставил мне живой тест-кейс. Код — в конце статьи, сначала посмотрим на сигналы и ценовой график.

Фундаментальный анализ за весь апрель 2026

Казалось бы, можно просто открыть ШОРТ и ждать

{
  "reasoning": "Все предоставленные отчёты фокусируются на негативных сигналах: 1) макроэкономика – ФРС удерживает высокие ставки, сильный доллар, рост занятости → риск‑офф настрой, ограниченный спрос на риск‑активы. 2) он‑чейн показатели – снижение LTH‑субмента, рост короткосрочных держателей, падение hashprice до уровня, близкого к безубыточности, отток из ETF и отсутствие притоков в крупнейший Spot‑ETF BlackRock. 3) рыночные метрики – низкий объём при росте цены, высокий funding rate (≈15 %), отсутствие подтверждающих объёмов при пробое сопротивления, падение цены до $57‑$66 k и сильный страх‑индекс (28). 4) институциональные потоки – хотя есть отдельные крупные покупки (MicroStrategy, крупные \"whales\"), их суммарный объём меньше отрицательных давлений (отток из ETF, отсутствие активных покупок кита, отток из крупных фондов). 5) технические сигналы – возможный фальшивый пробой $68.5k без объёма, слабая ликвидность, высокий funding rate указывает на переоценку бычьих позиций.\n\nТаким образом, доминирующая тема – усиление медвежьего давления со стороны макроэкономики и он‑чейн фундаментальных факторов, а также отсутствие убедительного институционального спроса. Противоречия (некоторые институциональные покупки и рост доминации BTC) недостаточны, чтобы переопределить общую картину.\n\nСледовательно, оптимальный единственный сигнал – **SELL**, так как текущая ситуация указывает на высокий риск падения цены в ближайшие недели.\n\n",
  "signal": "SELL"
}

5 апреля 2026

Залетает рыжий клоун. Для кого в Twitter, для кого на вертолёте/истребителе

PS C:\Users\User\Documents\GitHub\node-ccxt-backtest-final> bun .\scripts\run_signal.ts
Searching Bitcoin news April 5 2026
Searching Bitcoin flash crash April 5 2026
Searching April 5 2026 Bitcoin Trump tweet
{
  id: "4cc66bf6-4443-4800-aa43-28b6cb9f8800",
  reasoning: "Острое событие — ультиматум США Ирану, что повышает геополитическую напряжённость и исторически ведёт к снижению цены в среднесрочной перспективе (медвежий риск-сценарий — вероятность ≈45%). Технические сигналы также указывают на слабость: цена торгуется ниже всех скользящих средних, RSI на 44, объём низкий, индекс страха на «экстремальном страхе» (12) — что могло бы спровоцировать быстрый шорт-сквиз, но без подтверждающего новостного катализатора это маловероятно. Поскольку сигналы противоречивы — возможен как резкий провал ниже $65 500, так и быстрое ралли при пробое $68 200 с подтверждением объёмом, — картина размытая. Лучше пока оставаться в стороне.\n\nВывод: сигнал WAIT.",
  signal: "WAIT",
}

8 апреля 2026

Локальный отскок

PS C:\Users\User\Documents\GitHub\backtest-kit\example> bun .\scripts\run_research.ts
Searching April 8 2026 Bitcoin breaking news
Searching April 8 2026 Bitcoin hack withdrawal suspended
Searching April 8 2026 Trump tweet Bitcoin
Searching April 8 2026 Bitcoin flash crash
Searching Decrypt April 8 2026 Bitcoin Trump ceasefire
{
  id: "c5e27ed0-4bba-4000-a7a1-879b822d6000",
  signal: "BUY",
  reasoning: "Острый событийный драйвер — объявление Трампа о двухнедельном перемирии с Ираном (04:30 UTC) — вызвал мгновенное ралли Bitcoin до $72 000, ликвидацию ~$425M коротких позиций и массовый всплеск объёма (>2M BTC/час). Эти факты указывают на резкий бычий импульс, который должен поддержать рост цены в ближайшие часы. Оптимальный ход: открыть лонг с жёстким стоп-лоссом около $70 000.",
}

9 апреля 2026

Дальше идём в ад по плану

PS C:\Users\User\Documents\GitHub\backtest-kit\example> bun .\scripts\run_research.ts
Searching Bitcoin breaking news April 9 2026
Searching Bitcoin Supply Shock: Long-Term Investors Now Control 21% Of Total BTC April 9 2026
Searching Bitcoin breaking news April 8 2026 20:00 UTC
{
  id: "8708ddc6-57aa-4800-a114-787029fbd000",
  reasoning: "Отчёт показывает противоречивую картину: с одной стороны — острое событие — пробой цены выше $71 000 на фоне новостей о перемирии, указывающий на краткосрочный рост; с другой — сильные медвежьи факторы: значительное давление продаж майнеров, снижение хэшрейта, доминирование пут-опционов (премия ~17%), риск пробоя поддержки $70 000 и потенциальный откат к $58–63k, плюс новости об уязвимости к квантовым вычислениям, нервирующие инвесторов. При наличии одновременно сильных бычьих и медвежьих сигналов решение трейдера — осторожность без чёткого направления. Поэтому наиболее точный сигнал — WAIT.",
  signal: "WAIT",
}

Исходный код

Агент веб-поиска

import { addAgent } from "agent-swarm-kit";
...
import { str } from "functools-kit";

addAgent({
  agentName: AgentName.WebSearchAgent,
  completion: CompletionName.OllamaTextCompletion,
  keepMessages: Infinity,
  prompt: str.newline(
    "Ты — агент веб-поиска в рое агентов торговой системы.",
    "",
    "Твоя задача — составить объективный отчёт на основе запроса пользователя:",
    " * фокусируйся на негативных новостях/метриках",
    " * без маркетинговых прикрас",
    " * не выдумывай",
    " * пиши только то, что реально нашёл",
    "",
    "Критические требования:",
    " * Пользователь указывает ДАТУ для отчёта — избегай заглядывания в будущее",
    " * Избегай предвзятости статей из интернета: анализируй картину объективно, не копируй одно мнение",
    " * Если не можешь явно определить дату интернет-источника — не используй его в выводе",
    " * Выполняй несколько поисковых запросов — собирай всю доступную информацию",
    "",
    "Не останавливайся, пока не придёшь к ответу на вопрос пользователя с обоснованием",
    "Отвечай как профессиональный трейдер, в формате, готовом для вставки в файл",
    "Не пиши преамбулу вроде 'Конечно, вот ваш отчёт' — только содержимое файла",
    ""
  ),
  tools: [
    ToolName.WebSearchTool,
  ],
});

Генератор торговых сигналов

import {
  addOutline,
  commitAssistantMessage,
  commitUserMessage,
  dumpOutlineResult,
  execute,
  fork,
  IOutlineHistory,
  IOutlineResult,
} from "agent-swarm-kit";

...

import { str } from "functools-kit";

const DISPLAY_NAME_MAP = {
  BTCUSDT: "Bitcoin",
  ETHUSDT: "Ethereum",
  BNBUSDT: "Binance Coin (BNB)",
  XRPUSDT: "Ripple",
  SOLUSDT: "Solana",
};

const SEARCH_PROMPT = str.newline(
  "Ты ищешь триггеры острых событий за последние несколько часов — то, что только что произошло и ещё не заложено в цену.",
  "Не ищи фундаментальные данные (ставки финансирования, ликвидации, кошельки китов) — они запаздывающие и уже в цене.",
  "",
  "Уровень 1 — Острые события (искать в первую очередь):",
  " - {asset} breaking news {date}",
  " - {asset} SEC CFTC DOJ enforcement action {date}",
  " - {asset} exchange hack withdrawal suspended {date}",
  " - {asset} flash crash reason {date}",
  " - Trump tweet statement Bitcoin crypto {date}",
  " - Bitcoin ETF approval rejection decision {date}",
  "",
  "Уровень 2 — Макро-отклонения от ожиданий (только если уже произошли):",
  " - Federal Reserve decision surprise Bitcoin reaction {date}",
  " - CPI inflation data surprise {date} Bitcoin",
  " - dollar DXY sudden move Bitcoin correlation {date}",
  "",
  "Уровень 3 — Аномалии объёма:",
  " - {asset} unusual volume spike {date}",
  " - {asset} price sudden move reason {date}",
  "",
  "Уровень 4 — Готовые прогнозы аналитиков:",
  " - {asset} price forecast today {date}",
  " - {asset} price target analyst {date}",
  "",
  "Правила:",
  " * Только события за последние 4–12 часов — никаких недельных запаздывающих разборов",
  " * Если не можешь явно определить дату источника — не используй его",
  " * Не копируй мнение одной статьи — ищи подтверждение из нескольких источников",
  " * Пиши только то, что нашёл, без домыслов",
);

const SIGNAL_PROMPT = str.newline(
  "Ты — трейдер, принимающий направленное решение прямо сейчас на основе свежих рыночных событий.",
  "",
  "Ты прочитал отчёт по краткосрочным сигналам. Твоя задача — выдать один сигнал на ближайшие несколько часов.",
  "",
  "**Как думать:**",
  " - Острые события перевешивают запаздывающий анализ: взлом биржи, решение регулятора, аномальный всплеск объёма — это факты, а не прогнозы",
  " - Если данных мало или чёткого события не произошло — выбирай WAIT",
  " - Если картина противоречивая — выбирай WAIT",
  "",
  "**Определения сигналов (выбрать ровно один):**",
  " - **BUY**:  Краткосрочные данные указывают на рост в ближайшие несколько часов",
  " - **SELL**: Краткосрочные данные указывают на снижение в ближайшие несколько часов",
  " - **WAIT**: Данных недостаточно или картина неясная — не входить",
  "",
  "**Обязательный вывод:**",
  "1. **signal**: BUY, SELL или WAIT.",
  "2. **reasoning**: какие конкретные события из отчёта привели к этому выводу.",
);

const commitSignalSearch = async (
  query: string,
  date: Date,
  resultId: string,
  history: IOutlineHistory,
) => {
  const report = await fork(
    async (clientId, agentName) => {
      await commitUserMessage(
        str.newline(
          "Прочитай, что именно нужно найти, и скажи OK",
          "",
          SEARCH_PROMPT,
        ),
        "user",
        clientId,
        agentName,
      );
      await commitAssistantMessage("OK", clientId, agentName);
      const request = str.newline(
        `Найди краткосрочные сигналы для ${query} в интернете`,
        `Только события актуальные по состоянию на ${dayjs(date).format("DD MMMM YYYY HH:mm Z")}`,
        `Составь отчёт по краткосрочным рискам и возможностям`,
      );
      return await execute(request, clientId, agentName);
    },
    {
      clientId: `${resultId}_signal`,
      swarmName: SwarmName.WebSearchSwarm,
      onError: (error) => console.error(`Error in SignalOutline search for ${query}:`, error),
    },
  );
  if (!report) {
    throw new Error("SignalOutline web search failed");
  }
  if (typeof report === "symbol") {
    throw new Error("SignalOutline web search failed");
  }
  await history.push(
    {
      role: "user",
      content: str.newline(
        "Прочитай отчёт по краткосрочным рыночным сигналам и скажи OK",
        "",
        report,
      ),
    },
    {
      role: "assistant",
      content: "OK",
    },
  );
};

addOutline<ResearchResponseContract>({
  outlineName: OutlineName.ResearchOutline,
  completion: CompletionName.OllamaOutlineToolCompletion,
  format: {
    type: "object",
    properties: {
      signal: {
        type: "string",
        description: "Краткосрочный торговый сигнал на ближайшие несколько часов.",
        enum: ["BUY", "SELL", "WAIT"],
      },
      reasoning: {
        type: "string",
        description: "Конкретные события из отчёта, обосновывающие сигнал.",
      },
    },
    required: ["signal", "reasoning"],
  },
  getOutlineHistory: async ({ resultId, history }, symbol: string, when: Date) => {
    const displayName = Reflect.get(DISPLAY_NAME_MAP, symbol) || symbol;
    await history.push({
      role: "system",
      content: str.newline(
        `Текущая дата и время: ${dayjs(when).format("DD MMMM YYYY HH:mm")}`,
        `Актив: ${displayName}`,
      ),
    });
    await commitSignalSearch(displayName, when, resultId, history);
    await history.push({
      role: "user",
      content: SIGNAL_PROMPT,
    });
  },
  validations: [
    {
      validate: ({ data }) => {
        if (!data.signal) {
          throw new Error("signal field is empty");
        }
      },
      docDescription: "Проверяет, что сигнал задан.",
    },
    {
      validate: ({ data }) => {
        if (data.signal === "BUY") {
          return;
        }
        if (data.signal === "SELL") {
          return;
        }
        if (data.signal === "WAIT") {
          return;
        }
        throw new Error("signal field must be BUY, SELL, or WAIT");
      },
      docDescription: "Проверяет, что сигнал содержит допустимое значение.",
    },
    {
      validate: ({ data }) => {
        if (!data.reasoning) {
          throw new Error("reasoning field is empty");
        }
      },
      docDescription: "Проверяет, что сигнал обоснован.",
    },
  ],
  callbacks: {
    async onValidDocument(result) {
      if (!result.data) {
        return;
      }
      await dumpOutlineResult(result, "./dump/outline/research");
    },
  },
});

Спасибо за внимание!

В следующей статье я покажу:

  • Реальные метрики. Sharpe ratio, max drawdown, win rate

  • Интеграцию с backtest-kit. Предыдущие статьи строили инфраструктуру бэктестинга. Следующая покажет, как этот research-агент подключается к эмулятору/боевой бирже