«Робот за 5 000 рублей со 300 % годовых» вызывает у меня улыбку: простого торгового робота можно написать за вечер. Я Андрей Соколов, десять лет пишу бэкенд на Go, и недавно сделал такого бота на скользящих средних. Торговый робот — программа, которая по заданным правилам сама выставляет заявки: когда войти, когда выйти, где стоп. Моего я прогнал по реальным часовым свечам BTC/USDT с комиссией 0,1 % и проскальзыванием 0,05 % за сторону. Получилось минус 3 % за двенадцать с половиной суток, а просто лежащий биткоин за то же время потерял 0,7 %.

Повод — скриншот в одном чате: робот за 5 000 рублей, кривая капитала уходит вверх, подпись про 300 % в год. Продавца называть не буду, такие скриншоты друг на друга похожи. Мне захотелось понять, что можно выжать из самой простой идеи, если считать честно. Считать, как выяснилось, неприятно. Данные у меня до полуночи 12 октября 2026 года, и это учебный эксперимент, а не торговая идея.

Как устроен простой торговый робот

Правила у моего бота на две строки. Считаем две скользящие средние по цене закрытия, быструю на 10 свечей и медленную на 30. Быстрая выше медленной — сидим в биткоине, быстрая под медленной — сидим в деньгах. Сигнал считается по закрытию свечи, а сделка идёт по открытию следующей: иначе бот «видит» цену, по которой в жизни не успел бы торговать. В первой версии я торговал по открытию той же свечи, и результат выглядел лучше (+0,5 % до комиссий вместо −0,9 %). Заглянуть в будущее на одну свечу — самый дешёвый способ получить красивую кривую. Прототипы я пишу на Python, на Go переписываю то, что стоит оставить.

Python
import json

def load(name):
    return json.load(open(f"data/{name}.json"))  # [[ts, open, high, low, close, volume], ...]

def sma(xs, n):
    out, s = [None] * len(xs), 0.0
    for i, x in enumerate(xs):
        s += x
        if i >= n:
            s -= xs[i - n]
        if i >= n - 1:
            out[i] = s / n
    return out

def backtest(bars, fast, slow, lo, hi, fee=0.001, slip=0.0005):
    """Сигнал по закрытию свечи i, сделка по открытию i+1. Торгуем свечи [lo, hi)."""
    close = [b[4] for b in bars]
    f, s = sma(close, fast), sma(close, slow)
    eq, entry, start, trades = 1.0, None, 1.0, []
    for i in range(max(lo, slow - 1), hi - 1):
        px = bars[i + 1][1]
        if f[i] > s[i] and entry is None:        # быстрая выше медленной: покупаем
            entry, start = px * (1 + slip), eq
            eq *= 1 - fee
        elif f[i] <= s[i] and entry is not None:  # иначе выходим в деньги
            eq *= px * (1 - slip) / entry * (1 - fee)
            trades.append(eq / start - 1)
            entry = None
    if entry is not None:                         # в конце закрываем по последней цене
        eq *= bars[hi - 1][4] * (1 - slip) / entry * (1 - fee)
        trades.append(eq / start - 1)
    return eq - 1, trades

Это весь «робот»: функция backtest. Комиссия 0,1 % за каждую сторону — привычный уровень обычного тарифа, у вас может быть другой. Проскальзывание 0,05 % — допущение, что заявка исполняется чуть хуже цены открытия. Круг, вход плюс выход, обходится в 0,3 %. В живой торговле добавятся API брокера, ошибки сети, частичные исполнения. Ничего этого в коде нет.

Что показал бэктест на 300 часовых свечах

Данные — 300 часовых свечей BTC/USDT с 13:00 по Москве 29 сентября до полуночи 12 октября 2026 года, двенадцать с половиной суток. Запуск и вывод:

Текст
bars = load("BTCUSDT_1h")
for fee, slip in [(0, 0), (0.001, 0), (0.001, 0.0005)]:
    r, trades = backtest(bars, 10, 30, 0, len(bars), fee, slip)
    print(f"комиссия {fee:.1%}, проскальзывание {slip:.2%}: {r:+.2%}, сделок {len(trades)}, в плюсе {sum(t > 0 for t in trades)}")
Текст
комиссия 0.0%, проскальзывание 0.00%: -0.90%, сделок 7, в плюсе 3
комиссия 0.1%, проскальзывание 0.00%: -2.28%, сделок 7, в плюсе 2
комиссия 0.1%, проскальзывание 0.05%: -2.96%, сделок 7, в плюсе 2

Комиссия и проскальзывание забирают 0,3 % за круг. Семь сделок — это 2,1 п. п., из −0,90 % получается −2,96 %. До издержек идея в плюс тоже не выходит. Из семи сделок в плюсе две. Первая — №5: вход 3 октября в 22:00, выход 5-го, +0,54 %. Вторая, +0,75 %, — №7: код закрыл её по последней цене, потому что данные кончились.

Нагляднее всего серия 1 октября: бот трижды зашёл в монету, в 10:00, 15:00 и 21:00 (на графике это сделки №2–№4), и трижды вышел в минус: −1,14 %, −0,14 % и −0,75 %. Скользящие средние зарабатывают на тренде и теряют на пиле. За эти две недели цена ходила в диапазоне от 80,4 до 87,3 тысячи, толком не выбрав направления, и пилы хватило на всё.

Часовой график BTCUSDT с 29 сентября по 12 октября 2026 года, две скользящие средние и отметки входов семи сделок бота
Часовые свечи, по которым бежал бот: оранжевая линия — SMA 10, синяя — SMA 30. Стрелки — входы по номерам сделок из лога, у №5 и №6 отмечены и выходы. №2–№4 — серия 1 октября, №5 — единственная заметная прибыльная, №6 — самая убыточная, №7 закрыта кодом по последней цене.

Переобучение торгового робота: подбираем параметры, которые «сработали»

Числа 10 и 30 я взял с потолка. Что будет, если подобрать лучшие? Это и есть переобучение, и я прогнал его по-честному. Перебрал быструю среднюю от 2 до 30 свечей и медленную от быстрой плюс 3 до 120: 2 958 пар. Оставил те, где на обучающем куске есть хотя бы пять сделок. Обучающий кусок — первые 200 свечей, проверочный — последние 100. Лучшую пару выбираем по обучению и смотрим, что с ней на проверке.

Python
def sweep(bars, cut, fee=0.001, slip=0.0005):
    """Перебираем пары на первых cut свечах, лучшую проверяем на остальных."""
    rows = []
    for fast in range(2, 31):
        for slow in range(fast + 3, 121):
            r, trades = backtest(bars, fast, slow, 0, cut, fee, slip)
            if len(trades) >= 5:
                rows.append((r, fast, slow))
    rows.sort(reverse=True)
    r, fast, slow = rows[0]
    test = [backtest(bars, f, s, cut, len(bars), fee, slip)[0] for _, f, s in rows]
    best_test = backtest(bars, fast, slow, cut, len(bars), fee, slip)[0]
    return rows, fast, slow, r, best_test, test
Python
import statistics
for name in ("BTCUSDT_1h", "BTCUSDT_4h"):
    bars = load(name)
    rows, fast, slow, r, best_test, test = sweep(bars, 200)
    print(name, f"пар с >=5 сделок: {len(rows)}, лучшая {fast}/{slow}: {r:+.2%} на обучении, {best_test:+.2%} на проверке")
    print("  в плюсе на обучении:", sum(x[0] > 0 for x in rows), "на проверке:", sum(t > 0 for t in test), "медиана проверки:", f"{statistics.median(test):+.2%}")
Текст
BTCUSDT_1h пар с >=5 сделок: 408, лучшая 5/19: +1.33% на обучении, -0.40% на проверке
  в плюсе на обучении: 2 на проверке: 273 медиана проверки: +0.35%
BTCUSDT_4h пар с >=5 сделок: 438, лучшая 3/6: +11.35% на обучении, -2.71% на проверке
  в плюсе на обучении: 72 на проверке: 11 медиана проверки: -3.16%

Лучшая пара на часовиках, 5/19, на обучении даёт +1,33 % за шесть сделок. Выглядит как находка. С учётом издержек в плюсе на обучении оказались только две пары из 408, и я бы выбрал именно её. На проверочных 100 свечах она показала −0,40 %. Медиана остальных вариантов на тех же свечах +0,35 %, в плюсе были 273 из 408, а выбранную пару обошли 328 пар из 408. Пара, выбранная по прошлому, проиграла среднему варианту.

Сто часов — это четыре дня, шум. Чтобы не делать выводов по четырём дням, взял те же 300 свечей, но четырёхчасовые: 50 дней, с 23 августа по 11 октября. Лучшая на обучении пара 3/6 заработала +11,35 % за 15 сделок, чуть больше, чем просто держать биткоин на тех же 200 свечах (+9,0 %). На проверке она дала −2,71 %. В плюсе на обучении были 72 пары из 438, на проверке — 11. Здесь выбранная пара вышла лучше большинства соседей (её обошли 170 из 438, медиана проверки −3,16 %), но всё равно в минусе.

1 час, 12 суток4 часа, 50 суток
Пар с пятью и более сделками408 из 2 958438 из 2 958
Лучшая пара на первых 200 свечах5/193/6
Её результат на обучении+1,33 %+11,35 %
Она же на последних 100 свечах−0,40 %−2,71 %
Пар, обошедших её на проверке328 из 408170 из 438
Пар в плюсе на обучении2 из 40872 из 438
Пар в плюсе на проверке273 из 40811 из 438
Просто держать на проверке+0,50 %−0,55 %

Без комиссий та же пара 3/6 дала бы на проверке −0,05 %. Идея не работала и до комиссий, комиссии её только добили. Переобучение без терминов выглядит так: из почти четырёх сотен вариантов вы выбираете тот, что лучше всех подошёл под прошлое, и подошёл он под шум. Это тест, который проходит на моём ноутбуке и падает в CI: он подогнан под одну среду.

Робот за 5 000 рублей: вопросы, которые я задал бы продавцу

После этого эксперимента мне есть чем ответить на скриншот из чата. Вот вопросы к продавцу; без ответов на них я не стал бы смотреть даже на цену.

  1. Какой период и сколько сделок? Меньше нескольких сотен сделок — анекдот, статистики тут нет. У меня их семь.
  2. На каком куске подбирали параметры и на каком проверяли? Если показан один график на всей истории, это обучающий кусок.
  3. Какие комиссии и проскальзывание заложены? У меня 0,3 % за круг съели больше двух пунктов за двенадцать дней.
  4. Есть ли выписка по реальному счёту? Картинку можно нарисовать за вечер.
  5. Можно ли увидеть правила? Если нет, вы покупаете чёрный ящик, и сам продавец, возможно, не знает, что в нём.

Есть ещё простая экономика: продавец зарабатывает на продаже, и эта выручка не зависит от того, прибыльный робот или нет. Поэтому я смотрю на ответы на вопросы выше, а не на красоту кривой на лендинге.

Что в моём эксперименте упрощено

В нём нет фандинга и спреда, частичных исполнений, больших объёмов, при которых проскальзывание растёт, и проверки на разных режимах рынка: рост, падение, боковик. Главное — нет достаточной выборки. Для серьёзной проверки нужны годы данных и сотни сделок. Пока их нет, любой результат, мой в том числе, я воспринимаю как иллюстрацию механики, о рынке он ничего надёжного не говорит. Код после этого лежит у меня в папке с экспериментами. К реальному счёту он не подключён.