Какая страховка нужна трейдеру? Бэктест, не просто риск менеджемент

8 мин
Какая страховка нужна трейдеру? Бэктест, не просто риск менеджемент

Посмотреть исходник движка бектеста можно по ссылке

Я взял два платных телеграм-канала с сигналами, прочитал месяц их постов (август 2026) через LLM и проиграл каждый сигнал на минутных свечах с комиссиями и слиппеджем. Первый канал дал +10.3% за месяц, второй −18.1%. Обе цифры без плеча. Разница объясняется форматом постов, и её видно ещё до запуска бэктеста.

Зачем это всё

Представим трейдера, который собирается платить за сигналы из телеграма и следовать им. От чего ему нужна страховка?

Стоп-лосс сам по себе не защищает. Можно поставить стоп, который выбивает в 100% случаев, и потерять депозит, формально соблюдая риск-менеджмент. Работают правила автора или нет, показывает только прогон по истории: реальные посты, минутные свечи, комиссии, слиппедж. На выходе получается цифра, которую можно проверить.

Этим я и занялся. Каналы выбрал с противоположными форматами:

  • Канал A в каждом посте публикует цельный торговый план.

  • Канал B строит подачу на личной дисциплине: «беру по 1% от депозита», «принимаю убытки и двигаюсь дальше».

Результаты

Канал A

Канал B

Структура сигнала

диапазон входа + 5 целей + стоп

«работаю с BTC в шорт» + скриншот

Сделок за месяц

21

24

Результат (без плеча, за вычетом комиссий)

+10.3%

−18.1%

Прибыльных сделок (win rate)

76% (16 из 21)

29% (7 из 24)

Profit factor

1.78

0.57

Средняя длительность позиции

~17 часов

~44 часа

Худшая сделка

−4.6% (стоп автора)

−7.9% (наш стоп, своих у автора нет)

Сегмент, месяц и схема в пайплайне одинаковые, а разница составила 28 процентных пунктов. Дальше разберу, как это посчитано и откуда она взялась.

LLM разбирает посты, решения принимает код

Главное решение в проекте: модель не принимает торговых решений. Ей досталась задача, с которой LLM справляются хорошо, - чтение неструктурированных данных.

Пост вида «Открыть ЛОНГ в диапазоне $78600 - $79400, цели..., стоп $77400» - это текст с опечатками, эмодзи и рекламными вставками. Иногда сигнал вообще не текст, а скриншот терминала со словом «Шорт» на картинке. Превратить это в типизированный JSON должна модель. Решать, входить ли в позицию и где резать убыток, должен исходник.

import { generateObject, InferenceName, type FormatModel } from "json-inference"; const TradingPositionFormat = { type: "object", required: ["id", "symbol", "position", "entryRange", "targets", "stoploss", "reasoning"], properties: { id: { type: "number", description: "ID сообщения-источника сигнала. Если сигнала нет, верни -1", }, position: { type: "string", enum: ["long", "short", "wait"], description: "Тип позиции. Если позиции нет, верни wait", }, entryRange: { type: "object", required: ["from", "to"], properties: { from: { type: "number" }, to: { type: "number" } }, }, targets: { type: "array", description: "Цели позиции, 5 уровней. Если сигнала нет, верни []", items: { type: "number" }, }, stoploss: { type: "number" }, reasoning: { type: "string", description: "Обоснование - для отладки" }, },
} satisfies FormatModel;

Насколько это надёжно

Я сравнил два поколения кеша на одинаковых входах. Вердикты и все числовые поля совпали бит в бит. Отличался только свободный текст в reasoning, вплоть до языка ответа. Структуру держит грамматика, цены схема копирует из поста. Для парсера большего и не нужно.

Пайплайн: пост -> сигнал -> сделка

Стратегия тикает каждую минуту по каждому символу. Если на каждом тике звать LLM и Telegram, получим тысячи лишних запросов и флуд-бан. Поэтому получение сигнала обёрнуто в Cache.file: для канала A канал проверяется раз в 5 минут на символ.

Кеш хранится в Mongo. Побочный эффект ценный: коллекция measure-items становится датасетом всех ответов модели за месяц.

import { Cache } from "backtest-kit";
import { scrapeLookback } from "telegram-reader"; const getSignal = Cache.file( async (symbol: string, when: Date) => { const coin = `#${symbol.replace("USDT", "")}`; // окно 15 минут назад от «сейчас» бэктеста. // Пост, опубликованный позже when, стратегия не увидит let messages = await scrapeLookback({ channel: "crypto_yoda_channel", limit: 15, dimension: "minute", when, }); messages = messages.filter(({ content }) => content.includes(coin)); if (!messages.length) return { entry: null }; // LLM не вызываем const entry = await generateObject( InferenceName.OllamaInference, { format: TradingPositionFormat, messages: [ { role: "user", content: PARSER_PROMPT }, // строка промпта с правилами разбора поста ...messages.map(({ id, date, content }) => ({ role: "user" as const, content: `ID ${id}\n\n${content}\n\n[${date.toISOString()}]`, })), ], }, "gemma4:31b-cloud", ); return { entry, message }; }, { interval: "5m", name: "crypto_yoda_entry_v3" },
);

Канал A: дисциплина в структуре поста

Каждый сигнальный пост - законченный торговый план:

СИГНАЛ #BTC/USDT
🔑 Открыть ЛОНГ в диапазоне $78600 - $79400 - 6% депозита, плечо X15
🍒 Цели: $80300 / $80900 / $82000 / $83200 / $84500
❗️ СТОП ЛОСС: $77400

Диапазон, пять целей и стоп есть в каждом посте весь месяц. Такой сигнал можно исполнить кодом, ничего не додумывая. Это и есть дисциплина в машиночитаемом виде.

Как стратегия его исполняет

  1. Вход - по рынку на любом посте не старше 15 минут. Преимущество сигнала живёт рядом с моментом публикации.

  2. Тейк-профит - на самую дальнюю цель.

  3. Стоп-лосс - из поста.

  4. Сопровождение - «достижение уровней». Каждая взятая цель становится полом. Если цена откатилась за новейший взятый уровень больше чем на 30% шага между целями, позиция закрывается.

Шаг считается по сетке целей конкретного поста, поэтому допуск подстраивается сам: у PENGU шаги приблизительно 0.3–0.9%, у BTC - сотни долларов.

listenActivePing(async ({ data, currentPrice }) => { const levels = data.payload.levels as number[]; // цели из поста const currentLevel = getCurrentLevel(data.position, levels, currentPrice); const { lastLevel } = await LEVEL_STATE.getState(); // состояние конкретной позиции if (currentLevel > lastLevel) { await LEVEL_STATE.setState({ lastLevel: currentLevel }); // свежий пол return; } if (!lastLevel) return; // ни одной цели не взяли - ждём стопа или тейка const drift = getLevelDrift(data.position, levels, lastLevel, currentPrice); const step = getLevelStep(levels, lastLevel); // шаг между соседними целями if (drift > step * LEVEL_DRIFT_RATIO) { await commitClosePending(data.symbol); // откатились за взятый уровень - выходим }
});

Итог

21 сделка, +10.3% без плеча за вычетом комиссий, 16 из 21 в плюсе.

  • 3 раза сработал стоп автора: от −3.8% до −4.6%, разумный размер.

  • 1 раз цена дошла до дальнего тейка.

  • Остальные сделки закрыл рэтчет.

Откуда взялся плюс

Собственный манименеджмент автора я не копировал. Его план - разгружаться по пяти целям с плечом X15–25 - на этих же данных убыточен. Дальние цели почти не достигаются, а один стоп при таком плече сжигает 60–100% маржи.

Плюс получился на стыке: структурированный сигнал автора и наш риск-менеджмент. Направление автора приносит деньги, его плечо сжигает депозит.

Канал B: дисциплина на словах

У Вершинина формат противоположный. Диапазонов, целей и стопов нет. Есть посты:

Вход в позицию

Работаю с BTC - в шорт, взял с учетом возможного добора

Появление из позиции

И так, берем BTC, ETH, SOL в шорт по 1% от депозита,
биток и эфир 100 плечо, Солана 50 плечо.

Направление и тикер часто видны только на картинке: скриншоте позиции, графике TradingView или карточке сделки с биржи. Поэтому модель получает не только текст поста, но и изображение:

const signal = await generateObject( InferenceName.OllamaInference, { format: PositionOpenFormat, // { id, symbol, position: long|short|wait, reasoning } messages: [ { role: "user", content: promptOpen }, ...messages.map(({ id, date, content, photo }) => ({ role: "user" as const, content: `ID ${id}\n\n${content}\n\n[${date.toISOString()}]`, ...(photo && { images: [photo] }), // картинка поста в base64 - часть сигнала })), ], }, "gemma4:31b-cloud",
);

Правила для распознавания картинок я писал только после того, как просмотрел реальные скриншоты канала. На одной ловушке модель ошибалась регулярно: зелёная цифра P&L у шорта означает прибыль, а не «лонг». Направление определяется только по слову «Лонг/Шорт» или по положению зон тейка и стопа на графике. Теперь это правило прописано в промпте явно.

Как стратегия его исполняет

Автор не публикует стопов, следовательно стоп свой: 7.5% от цены входа.

Вход и выход у этого канала - отдельные решения, принятые в разное время. Поэтому пайплайнов два:

  • пока позиции нет, модель ищет в постах вход;

  • пока позиция открыта, модель ищет пост, где автор её закрывает.

addStrategySchema({ strategyName: "main_strategy" }); // схема пустая, без getSignal listenIdlePing(async ({ symbol, when, currentPrice }) => { // тикает, пока позиции нет const { signal } = await getOpenSignal(symbol, when); // Cache.file, 4h if (!signal || signal.position === "wait") return; await commitCreateSignal(symbol, { id: `${signal.id}-${signal.symbol.toLowerCase()}`, symbol: signal.symbol, ...Position.moonbag({ position: signal.position, currentPrice, percentStopLoss: 7.5, // свой стоп - автор своих не публикует }), });
}); listenActivePing(async ({ symbol, when }) => { // тикает, пока позиция открыта const { signal } = await getCloseSignal(symbol, when); if (signal?.action === "close") await commitClosePending(symbol);
});

Итог: 24 сделки, −18.1%, в плюсе 7 из 24.

Выводы

Считайте сами!

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

import { readFileSync } from "node:fs"; const trades = readFileSync("backtest_yoda.jsonl", "utf8") .split("\n").filter(Boolean).map(JSON.parse) .filter((e) => e.data.action === "closed"); const pnl = trades.reduce((sum, e) => sum + e.data.pnl, 0);
const wins = trades.filter((e) => e.data.pnl > 0).length;
const grossProfit = trades.filter((e) => e.data.pnl > 0).reduce((s, e) => s + e.data.pnl, 0);
const grossLoss = trades.filter((e) => e.data.pnl <= 0).reduce((s, e) => s + e.data.pnl, 0); console.log({ trades: trades.length, totalPnl: pnl.toFixed(2), // +10.26 winRate: (wins / trades.length * 100).toFixed(0) + "%", // 76% profitFactor: (grossProfit / -grossLoss).toFixed(2), // 1.78
});

В каждом событии closed есть:

  • цены входа и выхода;

  • причина закрытия: take_profit, stop_loss, closed или time_expired;

  • максимальная прибыль и максимальная просадка по позиции;

  • в поле note - полный ответ модели и пост-источник.

Любую сделку можно отмотать до конкретного телеграм-поста и прочитать reasoning модели. Метод переносится на любой иной канал за вечер.

Инструменты

  • backtest-kit - ядро бэктеста и live-торговли с защитой от look-ahead.

  • telegram-reader - чтение телеграм-каналов через MTProto.

  • json-inference - structured output для ollama.

  • backtest-ollama-casual - репозиторий с обеими стратегиями и скриптами анализа.

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

Читают сейчас

39 минут назад

OpenAI добавила плагины в голосовой режим ChatGPT

OpenAI расширила возможности Live Voice в ChatGPT: теперь во время голосового разговора можно применять подключённые плагины и приложения. Функция доступна в веб-версии, а также на iOS и Android. Асси

Учёные выдвинули теорию о тёмных измерениях и первичных чёрных дырах

1 час назад

Учёные выдвинули теорию о тёмных измерениях и первичных чёрных дырах

Все мы заперты в трёх привычных пространственных и одном временном измерении, однако это не означает, что гравитация не может проникать в другие измерения, если они существуют. И у нас может появиться

DrivingBench проверили GPT-6 Astra, GPT-5.6 Sol, Grok 4.6 и Claude Fable 5.1 на функция управлять авто

1 час назад

DrivingBench проверили GPT-6 Astra, GPT-5.6 Sol, Grok 4.6 и Claude Fable 5.1 на функция управлять авто

Исследователи из DrivingBench проверили, как современные нейросети умеют водить автомобиль. В контексте этого проекта ИИ-системы OpenAI GPT-6 Astra и GPT-5.6 Sol, SpaceXAI Grok 4.6 и Anthropic Claude

Beyond Good &amp; Evil 2 все еще жива: создание продолжается спустя 18 лет после анонса

4 часа назад

Beyond Good &amp; Evil 2 все еще жива: создание продолжается спустя 18 лет после анонса

Долгострой Beyond Good & Evil 2 продолжает подавать признаки жизни. Ведущий сценарист и помощник нарративного директора Ubisoft Bordeaux Рик Годвин рассказал, что последние некоторое количество месяце

950 Claude-агентов за 21 час нашли новую биологическую систему

4 часа назад

950 Claude-агентов за 21 час нашли новую биологическую систему

Anthropic запустили приблизительно 950 агентов, которые изучили более 200 тысяч обратных транскриптаз, то есть ферментов, способных синтезировать ДНК на основе РНК. За 21 час и 210 млн токенов они ото