К содержанию
CyberSoftLabRoman Leonov

Разработка

Разработка на Python

Исследования, подключение к биржам, расчёт риска и сопровождение бота. На Python лаборатория пишет большую часть кода.

Зачем Python здесь

Быстро проверять идеи, много готовых библиотек (pandas, numpy, ccxt, FastAPI), легко читать код. C++ и MQL5 забирают очень быстрые ордера. Python — данные, риск, отчёты, Telegram, тесты на истории.

Не пишите бота одним файлом. Семь слоёв ниже — минимальная карта для любого нового бота. Стратегия не должна знать про API-ключ.

подключениеСвязь с биржей: запросы, сокет, подпись, лимиты.
данныеСвечи и тики в одном формате, дыры в истории, часовые пояса.
стратегияДанные → сигнал. Без лота и без сети.
рискРазмер позиции, стопы, лимиты по инструменту и по дню.
ордераОтправка, повтор при сбое, частичное исполнение.
журналРешение, причина, номер ордера, результат с комиссией.
контрольПульс бота, алерты, ручная остановка.

Как устроен бот

Схема по слоям

На Python так обычно собирают всего бота: данные, правила, риск, ордера, лог. Если нужна очень быстрая отправка ордеров — этот кусок отдают в MQL5 или C++, остальное оставляют в Python.

  1. 01

    Данные

    Котировки с биржи или из терминала. Свечи и тики, проверка что в истории нет дыр.

  2. 02

    Стратегия

    Правила входа и выхода. Только «купить / продать / ждать» — без ключей и без размера лота.

  3. 03

    Риск

    Сколько лотов, где стоп, лимит на день. Если риск говорит нет — ордера нет.

  4. 04

    Исполнение

    Отправка ордера, повтор если сеть моргнула, сверка с реальной позицией.

  5. 05

    Журнал

    Что решили, почему, какой номер ордера, какой результат с комиссией.

  6. 06

    Контроль

    Пульс бота, сообщение в Telegram, кнопка «всё закрыть и остановиться».

Порядок жёсткий: стратегия не считает лот, риск не ходит в сеть, исполнение не «решает, торговать или нет». Если смешать слои — через месяц бота невозможно чинить.

Библиотека

Полезные функции

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

Сайзинг от риска, а не от «ощущения»

Лот считается от доли капитала и расстояния до стопа. Это первая функция, которую стоит покрыть тестом.

python
from dataclasses import dataclass

@dataclass(frozen=True)
class SizeResult:
    qty: float
    risk_cash: float
    clipped: bool

def position_size(
    equity: float,
    risk_pct: float,
    entry: float,
    stop: float,
    point_value: float = 1.0,
    qty_step: float = 0.001,
    max_qty: float | None = None,
) -> SizeResult:
    if equity <= 0:
        raise ValueError("equity must be positive")
    stop_dist = abs(entry - stop)
    if stop_dist <= 0:
        raise ValueError("stop must differ from entry")
    risk_cash = equity * (risk_pct / 100.0)
    raw = risk_cash / (stop_dist * point_value)
    steps = max(1, int(raw / qty_step))
    qty = steps * qty_step
    clipped = False
    if max_qty is not None and qty > max_qty:
        qty, clipped = max_qty, True
    return SizeResult(qty=qty, risk_cash=risk_cash, clipped=clipped)
Kill-switch по дневному убытку

Бот должен уметь замолчать. Просадка считается в валюте счёта, флаг живёт в состоянии, а не в голове трейдера.

python
from datetime import date, timezone, datetime

class KillSwitch:
    def __init__(self, daily_limit_pct: float):
        self.daily_limit_pct = daily_limit_pct
        self._day = date.min
        self._start_equity = 0.0
        self.tripped = False

    def on_mark(self, equity: float, now: datetime | None = None) -> bool:
        now = now or datetime.now(timezone.utc)
        d = now.date()
        if d != self._day:
            self._day = d
            self._start_equity = equity
            self.tripped = False
        if self._start_equity <= 0:
            return False
        dd = (self._start_equity - equity) / self._start_equity * 100
        if dd >= self.daily_limit_pct:
            self.tripped = True
        return self.tripped
Повтор запроса с backoff

Биржевой REST падает. Без политики повтора бот либо спамит, либо умирает на первом 429.

python
import random, time
import requests

def request_with_retry(method: str, url: str, **kwargs) -> requests.Response:
    delays = (0.4, 1.0, 2.5, 5.0)
    last_exc: Exception | None = None
    for i, delay in enumerate(delays, start=1):
        try:
            r = requests.request(method, url, timeout=8, **kwargs)
            if r.status_code in (429, 500, 502, 503, 504):
                raise requests.HTTPError(f"retryable {r.status_code}")
            r.raise_for_status()
            return r
        except (requests.Timeout, requests.ConnectionError, requests.HTTPError) as exc:
            last_exc = exc
            jitter = random.random() * 0.2
            time.sleep(delay + jitter)
    raise RuntimeError("exchange unreachable") from last_exc
ATR-стоп и трейлинг

Фиксированный стоп в пунктах ломается на волатильных сессиях. ATR даёт масштаб рынка.

python
def atr(high: list[float], low: list[float], close: list[float], n: int = 14) -> float:
    trs = []
    for i in range(1, len(close)):
        tr = max(
            high[i] - low[i],
            abs(high[i] - close[i - 1]),
            abs(low[i] - close[i - 1]),
        )
        trs.append(tr)
    window = trs[-n:]
    return sum(window) / len(window)

def trail_stop(side: str, price: float, atr_val: float, k: float = 2.0) -> float:
    dist = k * atr_val
    return price - dist if side == "long" else price + dist
Идемпотентный ключ ордера

Повторная отправка после обрыва сети не должна открыть вторую позицию. Клиентский id — закон.

python
import hashlib, json

def client_order_id(signal_id: str, bar_ts: int, side: str) -> str:
    payload = json.dumps({"s": signal_id, "t": bar_ts, "side": side}, sort_keys=True)
    return hashlib.sha1(payload.encode()).hexdigest()[:24]

def already_sent(store: set[str], oid: str) -> bool:
    if oid in store:
        return True
    store.add(oid)
    return False

Исследование

Считайте признаки отдельно от симулятора сделок. Иначе «сигнал» подглядывает в будущее.

Когда бот уже торгует

Сокеты через asyncio, запросы к REST — отдельно. Не считайте индикатор на каждом тике, если в этом нет нужды.

Типичные дыры

Часовые пояса, летнее время биржи, рестарты в середине позиции, повтор ордера после таймаута. Это лечится состоянием, не комментарием.

Подарок

Индикатор TradingView

Sigma Bands + RSI Filter MTF. Бесплатно: зоны, где цена слишком далеко ушла от средней. Ставите в Pine Editor и торгуете лимитками от полос, а не от каждого зигзага.

Средняя и сигмы

Индикатор считает среднюю цену и насколько рынок ушёл от неё. Это и есть сигма: обычное отклонение, а не «магический уровень».

Полосы покупки и продажи

Нижние полосы (−2σ и −3σ) — зона лимитной покупки. Верхние (+2σ и +3σ) — зона продажи. Это карта, не приказ купить прямо сейчас.

Фильтр RSI

Чтобы не ловить каждый шум у полосы, смотрит RSI: внизу рынок должен быть перепродан, вверху — перекуплен.

Несколько таймфреймов

Сверяет M15, H1, H4, день и неделю. Сильный сигнал — когда несколько графиков смотрят в одну сторону.

Связь

Нужен бот на Python под вашу площадку

Разберём идею, соберём бота, проверим риск. Пишите в Telegram — отвечаю лично.