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

Я взял два платных телеграм‑канала с сигналами, прочитал месяц их постов (август 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
Диапазон, пять целей и стоп есть в каждом посте весь месяц. Такой сигнал можно исполнить кодом, ничего не додумывая. Это и есть дисциплина в машиночитаемом виде.
Как стратегия его исполняет
Вход — по рынку на любом посте не старше 15 минут. Преимущество сигнала живёт рядом с моментом публикации.
Тейк‑профит — на самую дальнюю цель.
Стоп‑лосс — из поста.
Сопровождение — «достижение уровней». Каждая взятая цель становится полом. Если цена откатилась за последний взятый уровень больше чем на 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 — репозиторий с обеими стратегиями и скриптами анализа.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.