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

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

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

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

LLM учатся обосновывать ответ быстрее, чем рассуждать тщательно. Когда им скармливают сырые индикаторы — скажем, RSI и Stoch RSI одновременно — ответ агента определяется тем, какой из них он подхватит первым. При этом ситуация, когда один индикатор говорит о перекупленности, а другой — о перепроданности, крайне распространена. Цепочка рассуждений выглядит так:
-
RSI — перепродан, теоретически цена должна вырасти
-
Инструмента для проверки этой гипотезы через бэктест нет, так что мы об этом не думаем
-
Stoch RSI перекуплен — я на это больше не смотрю, ответ уже есть
Если жёстко прописать веса приоритетов для индикаторов, система перестаёт быть адаптивной и начинает сливать деньги. В этой статье я показал, как построить среду, позволяющую агенту динамически выставлять приоритеты индикаторов, — но цена высока: 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
PS C:\Users\User\Documents\GitHub\node-ccxt-backtest-final> bun .\scripts\run_signal.tsSearching Bitcoin breaking news April 6 2026Searching Bitcoin SEC enforcement action April 6 2026Searching Bitcoin flash crash April 6 2026Searching April 6 2026 Bitcoin price dip{ id: "5f7b9988-c41b-4000-a848-cf8712a84000", reasoning: "В отчёте доминирует негативный драйвер: твит Дональда Трампа о военной эскалации с Ираном вызвал скачок нефти, укрепление доллара и резкое падение Bitcoin ниже $69 000. На этом фоне ставки финансирования ушли в глубокий минус — сигнализируя о росте коротких позиций, — а объём ликвидаций достиг $400M и может ускорить снижение, особенно при пробое уровня $66 000. Позитивные сигналы (план покупок Майкла Сэйлора, рост цены ETF Grayscale) ограничены и не способны компенсировать усилившийся risk-off сентимент и давление коротких позиций. В ближайшие часы ожидается дальнейшее снижение цены.", signal: "SELL",}
5 апреля 2026
PS C:\Users\User\Documents\GitHub\node-ccxt-backtest-final> bun .\scripts\run_signal.tsSearching Bitcoin news April 5 2026Searching Bitcoin flash crash April 5 2026Searching 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.tsSearching April 8 2026 Bitcoin breaking newsSearching April 8 2026 Bitcoin hack withdrawal suspendedSearching April 8 2026 Trump tweet BitcoinSearching April 8 2026 Bitcoin flash crashSearching 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.tsSearching Bitcoin breaking news April 9 2026Searching Bitcoin Supply Shock: Long-Term Investors Now Control 21% Of Total BTC April 9 2026Searching 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-агент подключается к эмулятору/боевой бирже
ссылка на оригинал статьи https://habr.com/ru/articles/1022562/