Python в трейдинге: анализ данных, автоматизация, исследования

Python помогает упорядочить рыночные данные, анализировать журнал и повторять расчёты без ручного копирования формул. Особенно полезен, когда таблица становится тесной или сложной в сопровождении. Статья ведёт от настройки среды к скрипту загрузки, проверки качества и сохранения результатов. Также объясняет отличие исследований в блокноте от ПО, подключённого к торговому счёту, и необходимые меры защиты интеграции.

Python помогает повторять анализ и проверки, не заменяя качество данных и метод тестов.
Главное за 60 секунд
  • Python для исследований и исполнения — разные уровни ответственности — статистический блокнот может упасть без последствий. Код счёта требует лимитов, журнала, тайм-аутов, тестового режима и контролируемой остановки
  • Среда должна быть воспроизводимой — версия Python, изолированная виртуальная среда, зафиксированные версии библиотек, источник данных, часовой пояс и диапазон дат. Без этого «у меня работает» — история, а не результат
  • pandas и numpy заменяют таблицу в анализе OHLCV — переход с M1 к H4, скользящее среднее, просадка, корреляции между парами. Лист ограничен 1 048 576 строками; объём тиковых данных начинается гораздо выше
  • Ключи и пароли не попадают в код — только переменная окружения либо конфигурация вне репозитория. Ключ в скрипте остаётся в истории Git после каждого «удаления»
  • API — не неограниченный доступ — есть аутентификация, лимит частоты и ошибки; тестовая среда иногда, но не всегда, на отдельном хосте. Код без обработки HTTP 429 и тайм-аута рано или поздно оборвётся в худший момент
  • Заглядывание вперёд — один сдвиг в коде — решение на закрытии свечи работает лишь на доходности следующей. Умножение сигнала на доходность той же строки создаёт не существовавший на рынке результат
  • Незапущенный код — непроверенный код — компиляция и «похоже на правильное» не равны выполнению на данных. Шесть исполняемых блоков проверены офлайн на раскрытых данных и заглушках. Выводы показывают реальные результаты; команды установки описывают среду, а отправка заявки — явно неисполняемый структурный эскиз

1. Где Python помогает, а где лишь выглядит решением

Таблица справляется с торговым журналом, пока хватает вашего терпения. Эта граница зависит от компьютера, числа формул и устройства листа, поэтому её не описать круглым числом сделок. Но один предел жёстко задан спецификацией формата и ни от чего не зависит: один лист Excel вмещает 1 048 576 строк и 16 384 столбца [16]. Год тиковых данных одной валютной пары многократно превышает этот объём, и никакая хитрость не обходит предел.

Python здесь не волшебство. Он решает четыре конкретные задачи, с которыми таблица либо не справляется, либо требует часов работы:

  • Масштаб данных — ограничения задают память компьютера и формат хранения, а не число строк листа
  • Повторяемость — тот же скрипт завтра на тех же данных даёт тот же итог. Таблица с вручную протянутыми формулами этого не гарантирует
  • Смена интервала без ручной работы — агрегация M1 в H4 с OHLCV — один вызов resample, а не столбец вспомогательных формул
  • Автоматизация получения и упорядочивания данных — скрипт по расписанию скачает, проверит и сохранит данные без вашего присутствия

Вот и всё. Python не даёт рыночного преимущества, не улучшает качество стратегии и не меняет условия исполнения у брокера. Он сокращает путь от вопроса к ответу и именно поэтому быстрее показывает, что идея не работает.

Таблицы и Python по пяти рабочим критериям
Выбор по масштабу и повторяемости; стратегия отдельно.

Чего Python не делает. Python не подходит для торговли со сверхмалыми задержками: там важны микросекунды, и интерпретируемый язык со сборщиком мусора — плохой выбор. Но фраза «Python слишком медленный для торговли» тоже неверна. При входе на открытии следующей свечи H1 время вычислений Python ничтожно по сравнению с прохождением заявки через сеть и сервер брокера.

Ускорение векторизации объясняется механизмом, а не магией: операция над всем столбцом выполняется скомпилированным кодом numpy вместо цикла интерпретатора. Порядок разницы зависит от данных, вида операции и оборудования. Если хотите назвать конкретную величину, измерьте её на своём компьютере модулем timeit стандартной библиотеки [4] и запишите результат вместе с версиями библиотек. Чужой сравнительный тест без описания среды — не измерение.

Сравнение инструментов анализа трейдера:

СвойствоExcelPythonPine ScriptMQL4/MQL5
Предел данных1 048 576 строк на листОграничен памятью машиныЗависит от плана TradingViewИстория, предоставленная брокером
Где выполняется кодЛокальноЛокально, на сервере или в облакеВ среде TradingViewВ терминале MetaTrader
Доступ к счётуНетТолько через API брокера либо пакет-посредникНет: оповещение может запустить дальнейшую цепочкуВстроен в терминал
Источник данныхВсё, что загрузитеЛюбой, но его необходимо назватьПоток TradingViewПоток брокера
ВоспроизводимостьНизкая (ручные формулы)Высокая при фиксации версийСредняя (версия Pine и данные платформы)Средняя (сборка терминала и данные брокера)
Сложность освоенияНизкаяСредняяОт низкой до среднейОт средней до высокой

Различие в последней строке важнее, чем кажется. Pine Script работает в TradingView, MQL — в терминале MetaTrader, Python — там, где вы его запустите. Свобода делает вас ответственным за версии библиотек, системный пояс, непрерывность работы и само право скрипта обращаться к счёту.

Границы статьи. Это не курс Python или бэктестов. Методология — обучающая и проверочная выборки, форвард, модель затрат — относится к статье Демосчета, бэктесты и форвардное тестирование. Код терминала и его непрерывная работа на сервере рассмотрены в Автоматизации торговли: советники, скрипты и VPS. Языковые модели и их ограничения — ИИ и LLM в трейдинге. Здесь рассматриваем Python исключительно как слой данных, расчётов и интеграции.
Отказываться от таблицы не нужно. Экспортируйте результаты Python в CSV и открывайте в таблице. Или наоборот: ведите торговый журнал в таблице, загружая в pandas для анализа. Библиотека openpyxl позволяет напрямую читать и писать .xlsx из Python; последний выпуск — июнь 2024 года (3.1.5) [13], и здесь это не тревожный признак: .xlsx устоялся, библиотека не зависит от NumPy и pandas; правило раздела 2 не исключает её, а требует знать состояние [ПО СОСТОЯНИЮ НА 2026-08-22]. Python расширяет таблицу там, где она заканчивается.

2. Воспроизводимая среда: версии, venv, requirements

Очень часто причина неработающего чужого скрипта находится вне кода: автор использовал другие версии библиотек и нигде этого не записал. В анализе рыночных данных это не просто неудобство. Результат становится недействительным, поскольку его невозможно воспроизвести.

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

Виртуальная среда обязательна. Глобальная установка библиотек ведёт к тому, что обновление одного проекта ломает другой. Изоляция бесплатна, занимает около дюжины секунд и опирается на модуль venv стандартной библиотеки [4]:

# Windows, PowerShell
py -3.12 -m venv .venv
.venv\Scripts\Activate.ps1

# macOS / Linux
python3.12 -m venv .venv
source .venv/bin/activate

# общее для обеих систем
python -m pip install --upgrade pip
python -m pip install pandas numpy matplotlib requests python-dotenv
python -m pip freeze > requirements.txt

Чаще всего пропускают последнюю строку. pip freeze фиксирует точные версии всего установленного, включая неизвестные вам зависимости. Файл помещают рядом с кодом в репозиторий. На другой машине среда воссоздаётся командой python -m pip install -r requirements.txt, а не гаданием.

Среда текущей офлайн-проверки вычислительных примеров разделов 4 и 9: Windows 11, Python 3.12.10, pandas 3.0.5, NumPy 2.5.3; проверено 8 октября 2026 года. Ниже ограниченный перечень пакетов этих расчётов, не экспорт всех пакетов машины[1][3].

# Версии зависимостей локальной вычислительной проверки
numpy==2.5.3
pandas==3.0.5
python-dateutil==2.9.0.post0
six==1.17.0
tzdata==2026.4

# интерпретатор
Python 3.12.10

Заметьте, что pandas использует и другие пакеты: описание среды не заканчивается версией одной библиотеки. Зависимости способны создавать различия между компьютерами. Команды установки выше иллюстрируют настройку проекта, а не подтверждают установку во время этого аудита. Полный файл requirements.txt создайте в собственной среде командой python -m pip freeze и храните с кодом.

Этот файл тоже описывает не среду всей статьи, а только вычислительных разделов; именно так его нужно понимать. Фрагмент проверки обработки ошибок HTTP в разделе 7 запускался в среде с добавленным пакетом requests 2.34.2; тест защитного шлюза раздела 8 не требует сторонних пакетов. Версии matplotlib и python-dotenv не приведены, поскольку эти библиотеки не использовались ни в одном реально выполненном примере. Перечислить их «для порядка» означало бы записать непроверенные числа. Для среды всей статьи считайте команду установки выше отправной точкой и зафиксируйте собственный файл pip freeze; лишь такой файл зависимостей описывает ваши результаты.

Исследовательский блокнот с UTC, проверкой пропусков и экспортом без заявок.
Блокнот поддерживает воспроизводимость; вывод проверяют до отдельной системы исполнения.

Блокнот или .py? Оба варианта полезны для разных задач. Jupyter подходит, когда вы ещё не знаете, что ищете: запускаете ячейку, смотрите результат, меняете параметр. Но состояние переменных зависит от порядка запуска ячеек, а не от их записи в файле. Это исключает блокнот как место для кода исполнения. Блокнот с выполнением 1–3–2 выглядит так же, как выполненный последовательно. Код с доступом к счёту должен быть обычным файлом .py, выполняемым сверху вниз, с тестами и журналом.

Anaconda и Google Colab упрощают начало, но имеют свою цену. Anaconda устанавливает большой набор пакетов сразу и использует собственный менеджер зависимостей. Начать проще, но точно описать среду труднее, если смешать conda с pip. Colab работает в браузере без установки и подходит для обучения, но среда создаётся заново в каждой сессии, версии меняются без вас. Для повторения через год оба требуют дополнительной дисциплины.

Библиотеки для начала — четыре пункта, не сорок (пакетов пять: первый — пара):

  • pandas и numpy — структуры данных и вычисления
  • matplotlib — графики
  • requests — HTTP-обмен с API
  • python-dotenv — конфигурация из файла вне репозитория

Добавляйте индикаторы, когда нужны. Поправка к старым руководствам: TA-Lib публикует официальные бинарные wheel с библиотекой C на PyPI; для поддерживаемой комбинации ОС, архитектуры и Python простая команда pip install TA-Lib не требует неофициальных загрузок или собственной компиляции[9]. Проверьте конкретный wheel: работа на любом компьютере не обещана. Метаданные от 8 октября 2026 года указывают TA-Lib 0.8.1 и pandas-ta всё ещё 0.4.71b0 от сентября 2025 года[13]. Последняя фиксирует numba==0.61.2, поэтому одной проверки pandas недостаточно. Исторический тест установки Python 3.14.3 от 22 августа 2026 года завершился ошибкой зависимости only versions >=3.10,<3.14 are supported. Это не переносится автоматически на текущий Python 3.12.10. Аудит лишь прочёл PyPI, не устанавливал и не запускал TA-Lib/pandas-ta. Соседнее правило узко: свежий выпуск и совместимый pandas не доказывают установку и работу всех зависимостей. Проверяйте фиксированные компилируемые библиотеки, wheel интерпретатора и тест малых данных. Последние два выпуска не описывают всю историю: прежние паузы оценивайте в репозитории.

Проверьте дату последнего выпуска до добавления библиотеки. Давно не развиваемый проект рано или поздно отстанет от новой версии NumPy или pandas. Тогда придётся зафиксировать старые версии в отдельной среде либо переписать код. Дата последнего выпуска видна на странице пакета PyPI [13] и говорит больше звёзд GitHub. Особенно у старых бэктест-фреймворков.

Начальное значение генератора случайных чисел. Всюду, где результат зависит от случайности — разделение выборки, бутстрэп, моделирование, инициализация модели, — задавайте и записывайте начальное значение генератора. В numpy: rng = np.random.default_rng(20260822) [3], затем используйте только этот объект. Иначе два запуска дают разные итоги и непонятно, какой описан.

3. pandas и numpy: OHLCV, временной индекс, пояса

numpy выполняет вычисления над числовыми массивами, а pandas задаёт структуру: DataFrame — таблица с именованными столбцами и индексом, чаще временным. При работе с рынком временной индекс важнее самих расчётов: он определяет, считаете ли вы вообще то, что предполагаете.

Загрузка – pd.read_csv() обрабатывает файлы с миллионами строк, распознаёт разделители и самостоятельно разбирает даты, если указан столбец. Но по умолчанию ничего не знает о часовом поясе; с этого и нужно начать.

Смена интервала — агрегация M1 в H4 с OHLCV одним вызовом: df.resample('4h').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'tick_volume':'sum'}). Два часто незадаваемых аргумента определяют интерпретацию: label определяет метку начала или конца окна, а closed — принадлежность границы окну. Обычно метка стоит на левом краю [1], то есть время свечи — открытие. В pandas 3.0.5 resample('4h') на восьми часовых свечах от полуночи даёт 00:00 и 04:00. Если сигнал подразумевает закрытие, а метки — открытие, незаметно сдвигается весь итог на интервал.

Скользящая статистика – df['close'].rolling(20).mean() даёт скользящее среднее,, .rolling(20).std() — отклонение для полос Боллинджера,, .ewm(span=20).mean() — экспоненциальное среднее [1]. Все они вычисляются только по данным, прошлым относительно текущей строки. На этом свойстве держится корректность всего остального.

Фильтрация — составное условие пишется один раз: df[(df['sma20'] > df['sma50']) & (df['rsi'] < 30)]. Скобки каждого условия обязательны, поскольку & имеет больший приоритет, чем сравнение: пропуск даёт ошибку, не молчаливый неверный итог. Здесь это хорошо.

Четыре условные OHLCV-свечи и проверки
DataFrame не подтверждает качество: значения, время, объём проверяются.

Часовой пояс — решение, не деталь. Данные брокера обычно содержат время его сервера, данные внешнего поставщика чаще даны в UTC, а операционная система может использовать местное время с переводом часов по выбранному поясу. Смешав эти три шкалы, при анализе «лондонской сессии» вы исследуете что-то другое.

Сначала установите пояс источника, считайте в UTC. Для отметок с явным смещением или Z используйте pd.to_datetime(column, utc=True). Серверные отметки без смещения сначала разберите и назначьте подтверждённый пояс через tz_localize() с обработкой неоднозначных и несуществующих часов, затем примените tz_convert("UTC"). Один лишь utc=True считает значения без пояса UTC, не выводя пояс брокера. В Europe/London переводите лишь для представления[1]. Индекс с явным поясом и записанный источник позволяют согласованно объединять ряды.

Время сервера брокера — отдельная величина. Многие брокеры используют серверный пояс со смещением относительно UTC. Иногда смещение меняется вместе с переводом часов в США или Европе. Не берите его по памяти: проверьте спецификацию брокера и запишите параметром конфигурации проекта, а не числом внутри кода. [ФУНКЦИЯ ЗАВИСИТ ОТ БРОКЕРА/СЧЁТА]

Пропуски данных. История OHLCV имеет разрывы: выходные, праздники, технические паузы, отсутствие котирования. pandas предлагает df.ffill(), который заполняет пробелы последним известным значением. Для цен это обычно плохая идея: возникают свечи с нулевой волатильностью и занижается стандартное отклонение. Лучше удалить отсутствующие периоды и подсчитать их число.

Поправка к старым материалам. Форма df.fillna(method='ffill') из прежних руководств удалена: параметр method функции fillna() объявлен устаревшим в pandas 2.1 [2] и удалён в pandas 3.0. Теперь старая форма не предупреждает, а завершает программу сообщением TypeError: NDFrame.fillna() got an unexpected keyword argument 'method'. Верные формы — df.ffill() и df.bfill(). Сигнатура pandas 3.0.5 не содержит параметра: DataFrame.fillna(value, *, axis=None, inplace=False, limit=None) [1]. Сигнатура и сообщение проверены запуском на pandas 3.0.5, не чтением документации. [ПО СОСТОЯНИЮ НА 2026-08-22]
Тиковый объём — не рыночный объём. На децентрализованном валютном рынке розничный клиент не получает консолидированный оборот рынка: ему доступно число изменений котировок потока конкретного брокера. Это можно осторожно использовать как косвенный показатель активности, но столбец «volume» не является рыночным объёмом и несопоставим между брокерами.

Несколько инструментов одновременно. Для анализа валютного портфеля pandas предлагает иерархический индекс MultiIndex: один DataFrame хранит множество пар с индексами пары и времени. Корреляцию инструментов можно считать на общей временной оси, если заранее привести все ряды к одному поясу и сетке временных меток. Иначе вы вычисляете корреляцию данных, сдвинутых друг относительно друга.

Сохранение результатов. df.to_csv() человекочитаем и универсален. df.to_parquet() даёт меньший файл, быстрее читается, сохраняет типы столбцов и пояс индекса [1] — CSV пишет текст и требует повторного разбора. Для больших рабочих данных Parquet удобнее, но нужен движок записи: добавьте его в requirements.txt.

Макроконтекст в Python. Отчёты о позиционировании и данные календаря загружаются скриптом так же, как цены, с той же дисциплиной часового пояса и момента публикации. Их интерпретация относится к другой части курса: Отчёты COT и позиционирование.

4. Минимальный пример: CSV → UTC → доходности → пропуски → сохранение

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

Условия. CSV со столбцами time,open,high,low,close,tick_volume. Столбец time в ISO 8601 с маркером пояса (Z означает UTC). Объявлен часовой интервал. Файл для прогона намеренно содержит три дефекта реальных экспортов: дубликат времени, пропущенный час и пустую ячейку объёма. Эти шесть строк встроены прямо в код, чтобы без скачивания файлов получить точно тот же результат. Невоспроизводимый результат не является результатом. Среда: Python 3.12.10, pandas 3.0.5, numpy 2.5.3, Windows 11, [ПО СОСТОЯНИЮ НА 2026-10-08].

"""Минимальный пример: CSV -> UTC -> лог-доходность -> пропуски -> сохранение."""
import io
from pathlib import Path

import numpy as np
import pandas as pd

# Данные в коде для воспроизведения без скачивания файла.
# Шесть строк с тремя дефектами: дубль отметки 01:00,
# пропуск 03:00 и пустой объём в 02:00.
CSV_DATA = """time,open,high,low,close,tick_volume
2026-06-01T00:00:00Z,1.08140,1.08170,1.08131,1.08153,812
2026-06-01T01:00:00Z,1.08153,1.08190,1.08150,1.08176,640
2026-06-01T01:00:00Z,1.08153,1.08195,1.08149,1.08182,655
2026-06-01T02:00:00Z,1.08182,1.08188,1.08130,1.08141,
2026-06-01T04:00:00Z,1.08120,1.08125,1.08040,1.08050,903
2026-06-01T05:00:00Z,1.08050,1.08061,1.07980,1.07993,774
"""

OUTPUT = Path("EURUSD_H1_returns.csv")
INTERVAL = "1h"  # объявленная частота входных данных


def load_data(source) -> pd.DataFrame:
    """Загружает CSV и задаёт индекс с часовым поясом UTC.

    Намеренно без сортировки: порядок строк файла сам сообщает
    о качестве экспорта. Сортировка после отчёта.
    """
    df = pd.read_csv(source)
    # utc=True задаёт единый пояс, иначе pandas может вернуть индекс без пояса
    df["time"] = pd.to_datetime(df["time"], utc=True, format="ISO8601")
    return df.set_index("time")


def quality_report(df: pd.DataFrame, interval: str) -> dict:
    """Счётчики, которые нужно увидеть до вычислений."""
    expected = pd.date_range(df.index.min(), df.index.max(), freq=interval, tz="UTC")
    return {
        "rows": int(len(df)),
        "index_duplicates": int(df.index.duplicated().sum()),
        "missing_cells": int(df.isna().sum().sum()),
        "missing_timestamps": int(len(expected.difference(df.index))),
        # Измеряется ДО сортировки: после sort_index() проверка всегда
        # истинна и не найдёт нарушенную хронологию файла.
        "file_chronology_ok": bool(df.index.is_monotonic_increasing),
    }


def add_returns(df: pd.DataFrame) -> pd.DataFrame:
    """Лог-доходность = ln(close_t / close_(t-1)). Единица: натуральный логарифм, безразмерная."""
    df = df[~df.index.duplicated(keep="last")].copy()
    df["log_return"] = np.log(df["close"]).diff()
    return df


def main() -> None:
    df = load_data(io.StringIO(CSV_DATA))  # на практике: load_data(Path("EURUSD_H1.csv"))
    print("проверка качества:", quality_report(df, INTERVAL))
    df = add_returns(df.sort_index())
    print("свечей после удаления дублей:", len(df))
    print("сумма лог-доходностей:", round(float(df["log_return"].sum()), 6))
    print("отклонение лог-доходностей (интервалы 1h и 2h):",
          round(float(df["log_return"].std()), 6))
    df.to_csv(OUTPUT)
    print("сохранено:", OUTPUT)


if __name__ == "__main__":
    main()

Вывод запуска на встроенных данных в указанной среде; воспроизводится точно:

проверка качества: {'rows': 6, 'index_duplicates': 1, 'missing_cells': 1, 'missing_timestamps': 1, 'file_chronology_ok': True}
свечей после удаления дублей: 5
сумма лог-доходностей: -0.00148
отклонение лог-доходностей (интервалы 1h и 2h): 0.000467
сохранено: EURUSD_H1_returns.csv

Интерпретация. До каких-либо вычислений скрипт обнаружил все три внесённых дефекта; в этом его ценность. Сумма логарифмических доходностей −0,00148 при пяти свечах после удаления дубликата, то есть четырёх интервалах между наблюдаемыми ценами, означает снижение курса примерно на 0,15 процента за окно. Шесть — число строк файла, а не свечей: одна строка дублируется. Стандартное отклонение 0,000467 — величина на одну свечу H1, а не годовая и не дневная. Сравнивать её без указания интервала бессмысленно. Логарифмическая доходность безразмерна и складывается во времени; поэтому её суммируют, а не перемножают, в отличие от простой.

Ограничения. Пять свечей иллюстрируют механику, а не создают статистическую выборку. Из такого числа наблюдений ничего о рынке не следует. Скрипт не моделирует спред, комиссию или проскальзывание, поскольку это не бэктест: вычисляется доходность цены, а не результат счёта. Поиск отсутствующей временной метки предполагает реальную часовую частоту данных. На участке с выходными множество «пропусков» совершенно нормально, поэтому порог тревоги учитывает торговый календарь. Отдельная ловушка — сама мера: скрипт находит пробел, но не заполняет его, и доходность через отсутствующий час охватывает два интервала. Здесь такова одна из четырёх доходностей, хотя весь показатель назван величиной «на свечу H1». Это упрощение, а не точная мера. Честных пути два: восстановить полную часовую сетку с пустым значением в пробеле либо явно указать смешение горизонтов как ограничение измерения.

Почему хронологию проверяют до сортировки. Если load_data() заканчивалась вызовом sort_index() то поле file_chronology_ok всегда было бы истинным. Заявленная в отчёте проверка не могла бы ничего обнаружить, и файл с перепутанным порядком прошёл бы бесследно. Проверка, неспособная завершиться неудачей, хуже её отсутствия: создаёт ложную уверенность в валидации. Поэтому порядок измеряется на данных в исходном виде из файла, а сортировка выполняется только после отчёта.

Статус проверки: фрагмент запускался в указанной среде; вывод настоящий. В других pandas формат словаря или округление может отличаться.

5. Визуализация: один вопрос на график

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

matplotlib — основная библиотека графиков Python: статичная, с громоздким синтаксисом. plotly даёт интерактивные графики: дату и значение при наведении, увеличение участка, экспорт в HTML. На очень больших наборах она может быть медленнее и требует отдельной установки. Для исследования в блокноте удобнее plotly, для сохраняемых в отчёт графиков — matplotlib.

Четыре полезных графика с разными вопросами:

  1. Кривая эквити — накопленный итог. Отвечает «росло ли и когда», не «сколько заработаю»
  2. График просадки — расстояние от достигнутого к каждому моменту максимума. Показывает глубочайшую просадку и время восстановления. Всегда указывайте основу расчёта — баланс или текущие средства — и единицы: проценты либо валюту
  3. Тепловая карта сессий — результат по часам и дням недели. Сначала нужно определить часовой пояс, иначе карта описывает несуществующие часы
  4. Распределение доходностей — гистограмма сделок: толстые хвосты и асимметрия, невидимые одной метрике риска
Условные эквити и пик с минус десять процентов просадки
Просадка от текущего максимума с явным учётом капитальных потоков.

Для свечей прямо из DataFrame есть mplfinance— надстройка matplotlib с оговоркой раздела 2: последний выпуск август 2023 года, номер предварительный (0.12.10b0) [13] [ПО СОСТОЯНИЮ НА 2026-10-08]. Это не исключает её, но до добавления в requirements.txt, проверьте на своих данных совместимость с вашими версиями matplotlib и pandas. Новый выпуск не сделает этого за вас. Для простой панели со слайдерами и раскрывающимися списками есть streamlit— регулярно выпускаемый проект [13], запускаемый командой streamlit run dashboard.py. Обе экономят время и добавляют зависимости в requirements.txt, добавляйте осознанно.

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

6. Бэктест Python: код и методология

Здесь легко пропустить границу. Python отвечает за реализацию теста: загрузку данных, расчёт сигнала, отнесение результата к нужному периоду, учёт издержек и вычисление показателей. Он не отвечает за методологию: разделение выборки, число проверенных вариантов, модель исполнения и оценку устойчивости результата. Методология — предмет отдельной статьи, «Демосчета, бэктестинг и форвардное тестирование» — её не заменит лучшая библиотека.

Один pandas — не бэктест. Умножение столбца сигнала на доходности даёт ряд результатов, но не моделирует ничего между решением и исполнением: момент и цену входа, спред, комиссию, своп, частичное исполнение и отклонения заявок. На этапе отбора идей такое упрощение допустимо, если прямо назвать его упрощением, а не результатом.

Спред-затраты трёх условных сделок: постоянные и переменные
По допущениям USD 3.00 или USD 8.80, не типичный спред.

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

СвойствоПолный событийный подходВекторизованный подходМинимальный событийный подход
Пример библиотекиbacktradervectorbtbacktesting.py
Как считаетПо свечам со своей моделью заявок и портфеляОперации над целыми массивамиПо свечам, упрощённая модель
Код простой стратегииМного (классы, наследование)Мало, если мыслить столбцамиМало
Естественное применениеПортфельная логика, много инструментов, сложные заявкиМассовый поиск вариантов, первичный отборОбучение и быстрая проверка идеи
Главная ценаСложное освоениеБолее сложная отладка; легко незаметно ошибиться со сдвигомОграничения сложных стратегий
Проверить до примененияДата последнего выпуска PyPI, совместимость с вашими версиями pandas и NumPy, моделирование издержек и момента исполнения
Темпы разработки трёх проектов сильно различаются. Это не мелочь для коллекционеров: фреймворк, переставший успевать за выпусками pandas и NumPy, рано или поздно вынудит зафиксировать старые версии в отдельной среде. Перед выбором откройте его страницу PyPI и проверьте дату последнего выпуска и диапазон поддерживаемых версий Python. По этим критериям три проекта различаются гораздо сильнее, чем предполагает общая таблица: backtrader имеет многолетние паузы, последний выпуск апрель 2023 года; рядом существуют форки разной поддержки; vectorbt существует открыто и коммерчески с отдельной разработкой (открытый выпуск июль 2026 года); уточняйте версию документации; backtesting.py имела многолетний перерыв, но прервала его выпуском июля 2026 года: прошедшее время важно. Даты трёх — из реестра пакетов [13]. Ничто из перечисленного заранее не исключает библиотеку: исключает её несовместимость с версиями, на которых вы работаете. [ПО СОСТОЯНИЮ НА 2026-08-22]

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

Две условные кривые с явной границей IS/OOS
Разделите отбор и позднюю оценку, сообщайте все пробы.

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

7. API брокеров: аутентификация, лимиты, ошибки, тестовая среда

Фразу «брокер предоставляет API» иногда понимают как неограниченный доступ к счёту из скрипта. На деле API — набор конечных точек с аутентификацией, правами, ограничением частоты запросов и определёнными кодами ошибок. Иногда есть разделение тестовой и рабочей сред, но не всегда; способ разделения нужно выяснять по документации, а не предполагать. До первой строки кода нужны ответы на девять вопросов: как устроена аутентификация, какие права у токена, сколько запросов в секунду разрешено, как отличить тестовую среду от рабочей, какие типы данных возвращаются, какие коды ошибок предусмотрены, есть ли региональные ограничения, как поддерживается соединение и имеет ли токен вообще право размещать заявки.

Архитектуры MT5 IPC, TWS/IB Gateway, REST
Библиотека не устраняет обязательный терминал или посредника.

Лимиты конкретны и опубликованы, но нужно выяснить, какой именно лимит перед вами. Упускаемое руководствами различие решает дизайн цикла. В OANDA v20 два числа из разных документов значат разное. Руководство разработчика даёт принудительный лимит REST: «120 запросов в секунду, лишние получают HTTP 429», плюс двадцать активных потоков и два новых соединения в секунду, всё по IP отправителя [15]. А лучшие практики дают рекомендацию: ограничиться сотней запросов в секунду существующего соединения и двумя новыми соединениями в секунду [5]. [ПО СОСТОЯНИЮ НА 2026-08-22]

Сто запросов в секунду — порог, ниже которого поставщик рекомендует оставаться; сто двадцать — порог ответа сервера с ошибкой 429. Предположение «максимум 100/s» само по себе не навредит. Но код, принявший рекомендацию за жёсткий предел и не обрабатывающий 429, сломается при первом превышении: принудительный предел находится не там, где рекомендация. Это числа одного поставщика, а не правило рынка; у другого брокера они будут иными и [ФУНКЦИЯ ЗАВИСИТ ОТ БРОКЕРА/СЧЁТА]. Проверьте их в документации собственного поставщика до написания цикла опроса.

Поправка к популярным руководствам и прежнему абзацу. «Около 30 в секунду» OANDA устарело. Чаще и незаметнее — назвать рекомендуемые 100 максимумом. Так было здесь до исправления. Рекомендация и ограничение — разные величины разных документов одного поставщика [5] [15], а код, построенный на смешении лимитов, либо теряет пропускную способность, либо сталкивается с блокировкой без подготовленной обработки. Проверяйте документацию, а не учебное руководство; выясняйте, является число советом или порогом отсечения.

Тестовая среда — иной хост, не режим. У OANDA различие в адресе: api-fxpractice.oanda.com для практики и api-fxtrade.oanda.com для эксплуатации, аналогичная пара для потока [15] [ПО СОСТОЯНИЮ НА 2026-08-22]. Это удобно: ошибку можно перехватить в коде, оградив рабочий хост дополнительным условием. Хуже, если поставщик использует один адрес и различает среды только по токену: тогда единственным барьером остаётся ваша конфигурация.

Четыре типичных пути подключения Python к счёту: различаются архитектурой, а не степенью «продвинутости»:

РешениеГде идёт соединениеУсловияПримечания
OANDA v20 (REST + поток)Скрипт → сервер брокера напрямую HTTPСчёт у этого брокера; отдельные хосты тестовой и рабочей средПубличные документы и лимиты; API одного поставщика, не стандарт отрасли
Interactive Brokers (TWS / IB Gateway)Скрипт → локальное приложение-посредник → серверы брокераТребуется работающее приложение TWS или IB Gateway; асинхронная модельЗакрытие посредника рвёт соединение. Обёртки упрощают, не устраняют зависимость. Внимание имени: популярный ib_insync не выпускался с июля 2023 года, основан на старом TWS API; поддерживаемое продолжение — ib_async [13] [ПО СОСТОЯНИЮ НА 2026-10-08]
Официальный MetaTrader5 пакетСкрипт → MT5 на той же машине → сервер брокераMT5 должен быть на той же машине; вызов initialize() запускает его при необходимости, используя сохранённые либо переданные реквизиты [18]; интеграция с build 2085 (2019) [7]Это связь с терминалом, не независимый API брокера [6]. Зависимость — свойство, не дефект. PyPI-пакет только для Windows; macOS/Linux требуют терминал и Python под Wine либо отдельный посредник. [ЗАВИСИТ ОТ ВЕРСИИ/СРЕДЫ]
Внешний облачный мост (например MetaApi)Скрипт → сервис → терминал облака поставщика → брокерПередача доступа третьей стороне, цены и лимиты по плануДобавляет звено цепочки и доверенную сторону. Мост для отправки заявок требует основного, а не инвесторского пароля: права распоряжаться счётом, а не только читать его. Цены проверяйте у поставщика [ЗАВИСИТ ОТ ПЛАНА]
Инвесторский и основной пароли — разные решения. У счёта MetaTrader два пароля. Инвесторский пароль даёт чтение позиций и истории, не размещение и изменение. Основной пароль даёт полный контроль. Журналу, верификатору, панели статистики — максимум инвесторский. Облачный мост для отправки заявок по определению не может работать с инвесторским паролем: ему нужен основной. Поэтому подключение такого моста качественно отличается от подключения журнала. Выясните реальные требования конкретного поставщика в текущей документации. Если основной пароль уже передан и вы хотите прекратить доступ, смените его в терминале или кабинете брокера — это аннулирует доступ третьей стороны. Проверьте историю заявок на незнакомые операции и включите дополнительную аутентификацию везде, где брокер её предлагает. Даже доступ только для чтения раскрывает стиль, часы работы и объёмы позиций: это решение о приватности, а не формальность.

Вне валют полезна и ccxt унифицирует криптобиржи; к FX/CFD розничного брокера не относится, хотя её считают универсальной.

cTrader. В cTrader алгоритмы пишут в Algo с нативными C# и Python, включая cBot, индикаторы, плагины [8] [ПО СОСТОЯНИЮ НА 2026-08-22]. Это иная модель: код выполняется внутри платформы, а не отдельным процессом, подключённым к API. Проверяйте состав функций и способ запуска в текущей документации производителя, поскольку продукт активно развивается.

Ключи и пароли: никогда в коде

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

"""Конфигурация и секреты: ничего чувствительного в коде."""
import os
from dataclasses import dataclass

HOSTS = {
    "practice": "https://api-fxpractice.oanda.com",
    "live": "https://api-fxtrade.oanda.com",
}


class MissingConfiguration(RuntimeError):
    """Возникает при отсутствии обязательных для старта данных."""


@dataclass(frozen=True)
class Configuration:
    token: str
    account: str
    environment: str

    @property
    def host(self) -> str:
        return HOSTS[self.environment]


def _required(name: str) -> str:
    value = os.environ.get(name, "").strip()
    if not value:
        # Отсутствие умолчания намеренно: пустой токен отправил бы запрос
        # без авторизации вместо остановки.
        raise MissingConfiguration(f"Нет переменной окружения {name}")
    return value


def load_config() -> Configuration:
    environment = os.environ.get("BROKER_ENV", "practice").strip().lower()
    if environment not in HOSTS:
        raise MissingConfiguration(f"BROKER_ENV должен быть из {sorted(HOSTS)}")
    return Configuration(
        token=_required("BROKER_API_TOKEN"),
        account=_required("BROKER_ACCOUNT_ID"),
        environment=environment,
    )

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

Упомянутый python-dotenv не заменяет код, а предшествует. Вызов load_dotenv() (после from dotenv import load_dotenv) загружает файл .env вне репозитория в окружение процесса; затем load_config() читает их. Функции конфигурации не нужно знать, откуда пришло значение: из файла, системных настроек или менеджера секретов. Сам файл .env идёт в .gitignore вместе со всей папкой конфигурации.

Загрузка: тайм-аут, повторы, коды

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

import logging
import time

import requests

TIMEOUT_S = 10.0        # секунды бездействия подключения/чтения, не общий дедлайн
MAX_ATTEMPTS = 4            # число попыток вместе с первой
BACKOFF_S = 1.0         # основа экспоненциальной задержки в секундах

log = logging.getLogger("data")


class ApiError(RuntimeError):
    """Ошибка, неисправимая повтором."""


def fetch_candles(session: requests.Session, host: str, token: str,
                   instrument: str, parameters: dict) -> dict:
    """Повторяется лишь GET, не меняющий состояние брокера."""
    url = f"{host}/v3/instruments/{instrument}/candles"
    headers = {"Authorization": f"Bearer {token}"}

    for attempt in range(1, MAX_ATTEMPTS + 1):
        delay = BACKOFF_S * 2 ** (attempt - 1)
        try:
            response = session.get(url, headers=headers, params=parameters, timeout=TIMEOUT_S)
        except requests.RequestException as error:
            if attempt == MAX_ATTEMPTS:
                raise ApiError(f"Нет соединения после {MAX_ATTEMPTS} попыток") from error
            log.warning("попытка %s: %s", attempt, error)
            time.sleep(delay)
            continue

        if response.status_code == 429:
            # Лимит частоты. Заголовок Retry-After, если возвращается,
            # надёжнее догадки.
            try:
                wait = float(response.headers.get("Retry-After", delay))
            except (TypeError, ValueError):
                # Спецификация допускает HTTP-дату вместо секунд;
                # без ветви float() завершит всю загрузку.
                wait = delay
            log.warning("лимит частоты, ожидание %.1f s", wait)
            time.sleep(wait)
            continue

        if 500 <= response.status_code < 600:
            log.warning("ошибка сервера %s, повтор", response.status_code)
            time.sleep(delay)
            continue

        if response.status_code != 200:
            raise ApiError(f"HTTP {response.status_code}: {response.text[:200]}")

        try:
            data = response.json()
        except ValueError as error:
            raise ApiError("Неверный ответ JSON") from error
        if not isinstance(data, dict):
            raise ApiError("Ответ JSON должен быть объектом")
        if not isinstance(data.get("candles"), list) or not data["candles"]:
            raise ApiError("В ответе нет свечей: проверьте инструмент и период")
        return data

    raise ApiError(f"Нет верного ответа после {MAX_ATTEMPTS} попыток")

Статус: проверено офлайн без брокера. Опубликованная функция fetch_candles() проверена на имитации HTTP-сессии в памяти, не сервере. Первый из девяти сценариев: 429 с Retry-After в секундах, затем 503 и успех в одной последовательности; остальные — пустые свечи, 404, HTTP-дата, исчерпание попыток, исключение соединения, неверный JSON, не-объект и неверный тип candles. Вызовы sleep() заменены записью задержки, без сокетов и сетевых замеров. Среда: Python 3.12.10, requests 2.34.2, Windows 11, проверено 2026-10-08. Реальный отчёт сценариев:

A: 429 секунды -> 503 -> 200: вызовы=3; ожидания=[3.0, 2.0]; одна свеча
B: пустые свечи: вызовы=1; ожидания=[]; ожидаемая ошибка
C: HTTP 404: вызовы=1; ожидания=[]; ожидаемая ошибка
D: запасной путь HTTP-даты: вызовы=2; ожидания=[1.0]; одна свеча
E: лимит попыток: вызовы=4; ожидания=[0.0, 0.0, 0.0, 0.0]; ожидаемая ошибка
F: отказ транспорта: вызовы=4; ожидания=[1.0, 2.0, 4.0]; ожидаемая ошибка
G: неверный JSON: вызовы=1; ожидания=[]; ожидаемая ошибка
H: JSON не объект: вызовы=1; ожидания=[]; ожидаемая ошибка
I: неверный тип свечей: вызовы=1; ожидания=[]; ожидаемая ошибка

Что результат подтверждает и чего нет. Девять ожидаемых сценариев. A — три секунды сервера при 429 вместо одной кода; D — запасной путь, когда float() не разбирает HTTP-дату. F проверяет передачу timeout=10, выполнение четырёх попыток и завершение имитированных транспортных сбоев исключением. Это не устанавливает предельное время реального соединения: тайм-аут requests не является сроком всей задачи, а ожидание здесь имитировалось. B, C и G–I проверяют отклонение пустых или неверных ответов. Тест не проверяет доступность API, каждое поле данных или сетевое поведение конкретного брокера.

Сам брокер не проверен: endpoint, параметры, форма ответа и наличие Retry-After — свойства отдельного API [ЗАВИСИТ ОТ БРОКЕРА/СЧЁТА] и требуют проверки по документации и на тестовом счёте. Имитация доказывает корректность вашего кода, а не допущений о чужом сервере. В этом различии и заключается вся ценность теста.

Опрос и поток с раздельными часами котировки, запроса и приёма
Быстрый приём не доказывает свежесть или время исполнения.

Задержку измеряют, не цитируют. Диапазоны из руководств — «50–200 ms для этого API, 100–500 ms для другого» — бесполезны без местоположения клиента, канала связи, времени суток и метода измерения. Вместо копирования чужих чисел разделите показатели: время ответа конечной точки данных, время от отправки заявки до подтверждения приёма, время до подтверждения исполнения и возраст рыночных данных в момент решения. Это четыре разные величины, лишь одну из которых обычно называют «задержкой». Измерьте каждую в своей цепочке, сохраните в журнале и указывайте вместе с датой, брокером и числом попыток. ICMP ping не равен ни одной из этих величин.

Опрос или поток. При периодическом опросе REST возраст данных зависит от интервала запросов: опрос раз в пять секунд в худшем случае даёт решение по цене, которой уже пять секунд. Более частый опрос расходует лимит запросов. Поток WebSocket или HTTP, в зависимости от API, меняет направление: сервер сам отправляет обновление при его появлении. Это полезно для реакции внутри свечи или когда опрос исчерпал бы квоту. Для входа на открытии следующей свечи H1 обычного опроса достаточно, и поддерживать его проще. Поток добавляет три проблемы, которых у опроса нет. Первая — провал после восстановления: после тридцатисекундного обрыва сокет сам не воспроизведёт пропущенные тридцать секунд. Перед признанием ряда полным этот участок нужно отдельно запросить из истории. Вторая — обнаружение мёртвого соединения: переставший передавать данные сокет выглядит для кода как рынок без событий. Нужен сигнал присутствия — heartbeat или keep-alive — и собственный счётчик тишины, по которому соединение признаётся оборванным и восстанавливается. Третья — порядок и дубли: после восстановления может повторно прийти уже обработанное сообщение. Как и для заявок, проблему решают идентификатор события и проверка, записано ли оно ранее.

Отдельные лимиты данных и заявок. Полноценный API разделяет поток цен и транзакционный путь; лимиты операций, меняющих состояние счёта, иногда гораздо ниже лимитов чтения. Код, пытающийся в нервном рынке менять стоп-лосс дюжину раз в секунду, может получить блокировку именно торгового пути и оставить открытую позицию без временного программного доступа. Поэтому ограничивайте частоту модификаций у себя, не рассчитывая на снисхождение брокера.

Автоматизация не меняет исполнение. Модель исполнения брокера, обработка потока заявок и качество котировок влияют на результат независимо от отправителя — человека или скрипта. При чувствительности к стоимости входа они могут определить весь итог. Этому посвящён раздел курса о выборе брокера: модели исполнения и качество исполнения. Для кода достаточно вывода: API ускоряет отправку запроса на исполнение, но не создаёт отсутствующую ликвидность. Особенно наглядны дни резких движений. 15 января 2015 года Национальный банк Швейцарии отменил минимальный курс 1,20 франка за евро и одновременно снизил ставку до −0,75 процента [17]. Механизм универсален и не требует доверия к отдельной истории: если на уровне защитной заявки нет встречной ликвидности, исполнение происходит по первой доступной цене, а не по записанной. Её удалённость определяет стакан, а не ваше соединение. Масштаб событий у отдельных брокеров описан в части курса о выборе брокера. Здесь достаточно вывода для кода: скорость отправки не создаёт цену, по которой никто не хочет торговать.

8. От блокнота к сервису: код исполнения и скрипт

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

Блокнот исследования ≠ программа исполнения. До прав на заявки нужен полный набор: конфигурация вне кода · секреты вне репозитория · журнал каждого решения и каждого ответа сервера · тесты логики без сети · повторы с ограничением и только для идемпотентных операций · тайм-ауты каждого сетевого вызова · состояние переживающее перезапуск · мониторинг с сигналом присутствия · аварийный выключатель, останавливающий новые входы · процедура развёртывания, описывающая выпуск новой версии. При отсутствии хотя бы одного элемента это прототип, а не программа исполнения. Именно так его следует воспринимать, какой бы хорошей ни была тестовая кривая средств.

Три уровня автоматизации — различаются тем, кто принимает окончательное решение, а не технологией:

Уровень 1: уведомления. Скрипт проверяет условия и отправляет сообщение. Доступа к счёту нет, токен для размещения заявок не нужен. Решение и исполнение остаются за человеком. Только на этом уровне ошибка кода стоит лишь раздражения.

Уровень 2: подготовка с подтверждением. Скрипт собирает заявку — инструмент, направление и объём — и ждёт явного подтверждения оператора. Лишь затем отправляет её. Это позволяет обнаружить арифметические ошибки до появления позиции и сохраняет участие человека в решении. Для розничного трейдера, не собирающегося сопровождать программное обеспечение, обычно это конечный уровень, а не переходный. Это редакционная оценка, а не результат исследования.

Уровень 3: без подтверждения. Скрипт сам открывает и закрывает позиции. Нужны весь набор из вставки, проверка на тестовом счёте, затем на счёте с минимальным объёмом позиции и наблюдение в начальный период работы. Достаточное для перехода число сделок определяется размером выборки и статистической мощностью, а не круглым числом; этот вопрос разбирает статья о тестировании.

Офлайн-анализ отделён от шлюзов и адаптера исполнения
Сначала корректность анализа, для отправки отдельные дизайн, тесты и состояние.

Слой исполнения с защитными шлюзами

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

"""Исполнение: валидация, тест и явный шлюз реала."""
import logging
import os
import sys
import time
import uuid

ALLOWED_INSTRUMENTS = {"EUR_USD"}
MAX_UNITS = 1_000          # единицы базовой валюты, не лоты; соглашение зависит от API

# Подтверждения реала здесь нет, прочесть из файла нельзя.
# Программа берёт из окружения, верным считает
# номер сознательно выбранного счёта, введённый вручную в shell
# до запуска. Предел барьера описан под выводом теста.
CONFIRMATION_VARIABLE = "BROKER_LIVE_CONFIRMATION"

# Без настройки log.info не создаётся: уровень корня
# WARNING и назначения нет. Файл оставляет след сбоя,
# поток показывает живьём. В большой программе настройка
# в точке входа, не модуле.
# Временные метки аудита в UTC с явным маркером Z.
logging.Formatter.converter = time.gmtime
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s.%(msecs)03dZ %(levelname)s %(name)s %(message)s",
    datefmt="%Y-%m-%dT%H:%M:%S",
    handlers=[logging.FileHandler("execution.log", encoding="utf-8"),
              logging.StreamHandler(sys.stdout)],
)

log = logging.getLogger("execution")


class SafetyGateError(RuntimeError):
    """Останавливает заявку до выхода из процесса."""


def new_order_id() -> str:
    """Вызывается ОДИН РАЗ на намерение и сохраняется до отправки.

    Каждая следующая попытка той же заявки использует тот же ID;
    иначе дубль не распознать.
    """
    return str(uuid.uuid4())


def build_order(instrument: str, units: int, order_id: str) -> dict:
    if instrument not in ALLOWED_INSTRUMENTS:
        raise SafetyGateError(f"Инструмент не разрешён: {instrument}")
    # bool — подкласс int, isinstance(True, int) истинен; без
    # явного отказа bool прошёл бы валидацию в запрос.
    if isinstance(units, bool) or not isinstance(units, int) or units == 0:
        raise SafetyGateError("Размер должен быть ненулевым целым")
    if abs(units) > MAX_UNITS:
        raise SafetyGateError(
            f"Размер {abs(units)} превышает лимит {MAX_UNITS}")
    return {
        "order": {
            "type": "MARKET",
            "instrument": instrument,
            "units": str(units),
            # Идентификатор внешний, не меняется между попытками.
            # Созданный здесь менялся бы на каждом повторе.
            # Имя поля и ограничения конкретного API.
            "clientExtensions": {"id": order_id},
        }
    }


def _operator_confirmation() -> str:
    """Читает извне файла; код значения не знает и не хранит."""
    return os.environ.get(CONFIRMATION_VARIABLE, "").strip()


def submit_order(session, cfg, instrument: str, units: int, order_id: str,
                  test_mode: bool = True) -> dict | None:
    """По умолчанию без отправки. Реальный хост требует два решения."""
    order = build_order(instrument, units, order_id)

    if test_mode:
        log.info("ТЕСТ [%s] заявка построена, не отправлена: %s",
                 cfg.environment, order)
        return None

    if cfg.environment == "live" and _operator_confirmation() != cfg.account:
        raise SafetyGateError(
            "Реальный хост требует явного подтверждения оператора")

    # Шлюзы пройдены, модуль кончается без отправки,
    # так как честно запустить нельзя; структура в эскизе ниже.
    raise NotImplementedError(
        "Отправка не реализована: см. структурный эскиз")

ID заявки создаётся один раз, не на попытку. Чтобы после обрыва вообще можно было распознать дубликат, значение создают в момент возникновения намерения разместить заявку, надёжно сохраняют до отправки и повторно используют при каждой последующей попытке той же заявки. Если вызов uuid.uuid4() находился бы внутри построения заявки, каждая попытка отправляла бы иное значение и не было бы оснований считать второй запрос повтором первого. Поэтому функция build_order() получает идентификатор аргументом, а не генерирует самостоятельно.

Чего ID не решает. Поле clientExtensions в запросе — решение одного поставщика, не стандарт и не гарантия идемпотентности. OANDA называет его необязательными данными заявки и предупреждает: «Не устанавливайте, не меняйте, не удаляйте tradeClientExtensions для счёта MT4» [14] (проверено 2026-08-22). Документация не обещает отклонения повторного рыночного ордера с тем же идентификатором как дубликата. Рыночный ордер исполняется сразу, и окно распознавания повтора минимально. Поэтому обнаружение дублей опирается не на поле запроса, а на две ваши меры: надёжный реестр намерений, записанных до отправки, и сверку с перечнем заявок и транзакций, полученным у брокера после сбоя. Имя поля и ограничения уточняйте в документации своего API: у другого поставщика оно называется иначе или отсутствует.

Отправка заявки: структурный эскиз

Ниже нет функции для копирования. Есть структурный эскиз: порядок шагов и названия операций, пригодные для переноса в собственный код, но не для вставки и запуска. Такая форма выбрана намеренно. Редакция не запускала эту ветвь и не собирается: отправка требует брокерского счёта, а проверка транзакционного кода на чужом счёте — чужой риск, а не тест. Полная функция оказалась бы здесь артефактом, который никто не запускал, и прежде всего учила бы доверию к непроверенному коду. Так же представлены эскизы MQL в статье 11.5 и Pine Script в статье 11.2.

СТРУКТУРНЫЙ ЭСКИЗ — отправка заявки и отсутствие ответа
Направляющая запись HTTP-слоя, не полный исполняемый код.

объявление: StanNieznany — ответа нет, существование заявки неизвестно.
            Отдельный тип, не подкласс сетевой ошибки: её можно повторить,
            это состояние повторять нельзя.

вход: построенная и проверенная заявка, order_id, конфигурация, HTTP-сессия
выход: ответ брокера или StanNieznany, никогда не молчаливый успех

1. ДОЛГОВЕЧНЫЙ РЕЕСТР — до всего
   вне памяти процесса запишите order_id, время UTC,
   instrument, units, environment, status := INTENT
   принудительно сохраните на диск до продолжения
   иначе перезапуск теряет намерение,
   программа не знает уже испробованного

2. ЖУРНАЛ — также до отправки
   запишите order_id + environment + содержание заявки
   токен никогда не попадает в журнал
   маскируйте счёт до последних четырёх символов
   журнал читает человек после события; реестр шага 1 — программа

3. ОДИН ЗАПРОС, НОЛЬ ПОВТОРОВ
   POST по пути заявки из документации API
   авторизация токеном конфигурации, никогда кода
   тайм-аут обязателен, иначе вызов зависнет навсегда

4. ОТВЕТ НЕ ПРИШЁЛ (тайм-аут или транспорт)
   НЕ повторяйте — POST не идемпотентен
   в реестре status := UNKNOWN
   вызовите StanNieznany
   СВЕРКА СОСТОЯНИЯ: запросите заявки и транзакции последних минут
   найдите order_id; новое решение лишь после установления существования
   (процедуры здесь нет: см. абзац под эскизом)

5. ОТВЕТ ПРИШЁЛ
   запишите HTTP-код + усечённое тело
   код вне (200, 201) -> status := REJECTED -> вызов ошибки
   тело не JSON      -> status := UNKNOWN -> StanNieznany
   иначе             -> status := ACCEPTED -> верните ответ

чего эскиз не содержит и не может:
   готовой функции для вставки и запуска на счёте,
   ни имени поля защиты: оно из конкретной документации API,
   не чужого примера.

Чего эскиз не делает — столь же важно, как описанные действия. Здесь нет защитных уровней: ни стоп-лосса, ни тейк-профита. Это не упущение: API по-разному выражают их в отдельных полях запроса, расстоянием или абсолютной ценой, со своим сроком действия. Такую часть нужно брать из документации своего брокера, а не чужого примера. Практическое последствие касается именно шага 4: после состояния StanNieznany у вас может быть открытая позиция без защиты и даже без уверенности в её существовании. В коде с правом отправки заявок защитный уровень передаётся тем же запросом, что и вход, иначе возникает окно жизни позиции без стопа.

Не хватает и других элементов. Из списка «Исследовательский блокнот ≠ программа исполнения» ни исполнимый модуль выше, ни эскиз пока не имеют мониторинга, аварийного отключения и процедуры развёртывания. Состояние, переживающее перезапуск, обозначено в шаге 1 эскиза только как требование, а не реализация. Раздел о журналах, состоянии и тестах говорит об этих элементах, но не показывает их в коде. Вывод тот же: это материал для изучения архитектуры, а не программа исполнения.

Шлюзы проверены офлайн — без сети, брокера и секретов, на имитации конфигурации. Далее приведён весь тест, а не только вывод; иначе читателю пришлось бы верить на слово, а именно от такого доверия предостерегает статья.

"""Проверка шлюзов без сети: имитация, без секретов и POST."""
from dataclasses import dataclass


@dataclass(frozen=True)
class MockConfiguration:
    environment: str = "practice"
    host: str = "https://api-fxpractice.oanda.com"
    token: str = "dummy-token-for-offline-test"
    account: str = "000-000-0000000-000"


def test_gates() -> None:
    cfg = MockConfiguration()
    order_id = new_order_id()
    print("конфигурация:", cfg.environment, cfg.host, "token_length=", len(cfg.token))

    # session=None намеренно: попытка POST
    # вызовет AttributeError, не тихий успех.
    result = submit_order(None, cfg, "EUR_USD", 100, order_id)
    assert result is None
    print("1. тест: ничего не отправлено - OK")

    for description, instrument, units in [
        ("инструмент не разрешён", "GBP_JPY", 100),
        ("размер выше лимита", "EUR_USD", 5000),
        ("нулевой размер", "EUR_USD", 0),
        ("логическое вместо числа", "EUR_USD", True),
    ]:
        try:
            build_order(instrument, units, order_id)
        except SafetyGateError as error:
            print(f"2. шлюз [{description}]:", error)
        else:
            raise AssertionError(f"Ожидаемый шлюз не сработал: {description}")

    cfg_live = MockConfiguration(environment="live", host="https://api-fxtrade.oanda.com")
    try:
        submit_order(None, cfg_live, "EUR_USD", 100, order_id, test_mode=False)
    except SafetyGateError as error:
        print("3. шлюз [реал без подтверждения]:", error)
    else:
        raise AssertionError("Реал прошёл без подтверждения")

    print("все шлюзы проверены")


if __name__ == "__main__":
    test_gates()

Вывод Python 3.12.10 из раздела 2 без сети и сторонних пакетов: без отправки модуль не требует requests. Вторая строка — журнал, одновременно в файл execution.log и стандартный вывод. Время и ID меняются, остальное повторяется посимвольно:

конфигурация: practice https://api-fxpractice.oanda.com token_length= 28
2026-11-02T12:00:00.011Z INFO execution ТЕСТ [practice] заявка построена, не отправлена: {'order': {'type': 'MARKET', 'instrument': 'EUR_USD', 'units': '100', 'clientExtensions': {'id': '4ae82fd8-2de4-4117-97fc-c4aac3d11d99'}}}
1. тест: ничего не отправлено - OK
2. шлюз [инструмент не разрешён]: Инструмент не разрешён: GBP_JPY
2. шлюз [размер выше лимита]: Размер 5000 превышает лимит 1000
2. шлюз [нулевой размер]: Размер должен быть ненулевым целым
2. шлюз [логическое вместо числа]: Размер должен быть ненулевым целым
3. шлюз [реал без подтверждения]: Реальный хост требует явного подтверждения оператора
все шлюзы проверены

Что подтверждает этот вывод и чего он не подтверждает. Пять отрицательных случаев остановлены: инструмент, размер, ноль, bool, реал без подтверждения. Assertions проверяют тестовый режим; аудит проверил ±1000, превышения, типы. Первый аргумент None, а отправка не реализована, поэтому тест не может разместить заявку. Вывод не проверяет API брокера, устойчивость журналирования или маскирование каждого возможного чувствительного поля. Искусственный токен не печатается: показана только длина. В своём проекте отдельно проверяйте запись и восстановление журналов, правила сокрытия данных и отказы диска.

Однако о поведении с реальным API тест ничего не подтверждает и не может подтвердить: ветви отправки в модуле нет. Её заменил эскиз, убрав единственное место возможной отправки. В этом разница между «не запускали» и «нечего запускать»: первое оставляет готовый для вставки артефакт, второе — структуру для самостоятельной реализации с обязательным чтением документации своего брокера.

Тест должен ловить отсутствие шлюза. Поэтому тест проверяет результат тестового режима и вызывает AssertionError после каждого отрицательного случая, если ожидаемое исключение не возникло. Финальное сообщение появляется только после успешных проверок. Это небольшой пример: он не охватывает форматы ответов брокера, каждый тип данных или восстановление состояния. В большом проекте перенесите случаи в pytest, добавьте положительные, запускайте при изменении валидатора.

Шлюз — против ошибки, не программы. Нужно прямо обозначить границы этой защиты, чтобы не приписать ей лишнего. Значение подтверждения — не константа файла: программа читает переменную среды BROKER_LIVE_CONFIRMATION и сравнивает с номером счёта. Оператор должен сознательно ввести в оболочке номер счёта для торговли. Это защищает от трёх ошибок: запуска рабочей конфигурации «на пробу», путаницы счетов и случайного отключения тестового режима. От большего не защищает: код того же процесса способен сам задать переменную, и проверка его не остановит. Подтверждение из собственной среды программы не является защитой от программы. Реальное ограничение прав находится снаружи: токен без права заявок, если брокер различает такие права, и лимиты объёма позиций на стороне счёта [ЗАВИСИТ ОТ БРОКЕРА/СЧЁТА].

Единицы — не лоты. Комментарий у MAX_UNITS не украшение. Разные API задают объём в единицах базовой валюты, лотах или контрактах. Ошибка не вызывает синтаксического сбоя или исключения: при корректно работающем коде возникает экспозиция на два-три порядка выше задуманной. До отправки проверьте соглашение в документации и подтвердите одной сделкой минимального объёма на тестовом счёте. До подключения валидатора к своим расчётам учтите ещё два ограничения. Объём из pandas или NumPy имеет тип numpy.int64 и это не подкласс встроенного int — валидатор отвергнет его сообщением о целых числах, хотя значение действительно целое. Это безопасный, но сбивающий с толку отказ. Преобразуйте число из DataFrame через int(...) до build_order(). Второе ограничение: order_id здесь вообще не проверяется, а аннотация типа ничего не обеспечивает. Поскольку распознавание дублей зависит от этого идентификатора, выясните допустимые длину и формат в документации API и проверяйте их так же строго, как предел объёма.

Журнал, состояние, тесты

Журнал вместо print(). Модуль logging даёт уровни важности, временную метку и запись в файл, но всё это не включается само. Без настройки уровень по умолчанию — WARNING, а назначение вывода вообще не задано, поэтому вызовы log.info(...) молча отбрасываются: код выглядит журналирующим, но ничего не записывает. Поэтому после сбоя журнал нередко оказывается пустым. Настройку задают один раз и только в точке входа программы: в запускаемом скрипте, а не импортируемом модуле. Вызов logging.basicConfig(...) находится в модуле исполнения выше лишь потому, что там модуль одновременно является всей программой. Комментарий прямо предупреждает: в большом проекте это неверное место. Настройка в модуле выполняется при импорте, перехватывает журналирование всего процесса, и последующий вызов logging.basicConfig(...) в настоящей точке входа уже ничего не меняет: функция бездействует, если у корневого логгера заданы назначения, кроме вызова с force=True). При этом никакой ошибки не возникает [4]. Ваши настройки просто исчезают, а записи идут туда, куда указал импортированный модуль: тот же незаметный дефект, но с другой стороны. Записывайте решение с основаниями, содержимое запроса и ответ сервера. Последний сокращайте, чтобы HTML-страница ошибки не заполнила журнал; при диагностике увеличивайте лимит. Единственное исключение — токен: его не записывайте никогда. Без ответа брокера журнал не позволит затем отличить ошибку логики, сети и сервера. Временные метки используют UTC благодаря logging.Formatter.converter = time.gmtime и явному маркеру Z. Без этой настройки форматтер обычно использует местное время системы. Сопоставляйте журналы брокера и системы на общей основе UTC; другую зону применяйте только для отображения.

Журнал исполнения тоже чувствителен. Полный ордер идёт в execution.log, а в полной программе и ответ брокера: номер счёта, идентификатор заявки, объём позиции, цены. Токена там нет и быть не должно: десять запусков теста дали одиннадцать записей и ни одного вхождения токена. Но остальных данных достаточно, чтобы защищать файл как конфигурацию с секретом: внесите execution.log в .gitignore рядом с .env. Но .gitignore сам по себе не контролирует доступ: не ограничивает права на файл, не удаляет его по истечении срока и не скрывает содержимое. Поэтому эскиз отправки требует маскировать номер счёта до последних четырёх символов и журналировать поля по заранее составленному списку, а не всё, что вернул сервер. До работы без присмотра нужно решить ещё два вопроса. Путь: execution.log задан относительно: файл создаётся в рабочем каталоге процесса, а не рядом со скриптом. При запуске из планировщика Task Scheduler или cron — именно так запускают подобные скрипты, как объяснено ниже — рабочий каталог может сильно отличаться от каталога скрипта. Журнал, на котором держится восстановление после сбоя, окажется не там, где его ищут. Задайте абсолютный путь из расположения скрипта или конфигурации. Размер: при указанном выше объёме записей файл растёт неограниченно, поскольку в примере нет ротации. logging.handlers.RotatingFileHandler из стандартной библиотеки после import logging.handlers, поскольку import logging не открывает подмодуль, заменяет FileHandler без прочих изменений.

Состояние должно переживать перезапуск. Процесс когда-нибудь остановится из-за обновления системы, сбоя питания или вашего нажатия Ctrl+C. Если сведения об отправленных заявках хранятся только в памяти процесса, после перезапуска программа не знает, что уже сделала. Минимальное решение: записывать идентификаторы в файл или базу до отправки, а при старте сверять запись с состоянием счёта из API. Именно это предотвращает двойной вход после обрыва соединения.

Тесты без сети. Логику сигнала и проверку лимитов можно тестировать на подготовленных данных без каких-либо соединений, как выше с защитными проверками. Тест, требующий работающего API, не является модульным и не подходит для запуска при каждом изменении кода. Начните с проверяемого офлайн: условия входа, расчёта объёма позиции, поведения на пустых данных и данных с пробелами.

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

Где запускать. Непрерывная работа сервера, синхронизация часов, продолжение после перезапуска и восстановление после сбоя рассмотрены в статье Автоматизация: советники, скрипты и VPS. Способы запуска различаются не удобством, а последствиями отказа. Планировщик Task Scheduler в Windows или cron в Unix периодически запускают скрипт и завершают его после выполнения. Системная служба, например модуль systemd, поддерживает процесс постоянно и может перезапустить после выхода; менеджер процессов делает то же на уровне приложения. Выбор коротких периодических запусков или постоянного процесса определяет хранение состояния и контроль единственного экземпляра. Для Python важно одно: при запуске планировщиком нужно допускать, что предыдущий запуск ещё не завершился. Перед действием проверяйте состояние, а не предполагайте старт с нуля.

См. также: Алгоритмическая торговля — архитектура системы и место автоматизации во всём процессе принятия решений.

9. Заглядывание в будущее и утечки данных в Python-коде

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

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

Условия примера: восемь синтетических часовых наблюдений, индекс UTC, иллюстративные значения, без транзакционных издержек. Цель — показать механизм сдвига, а не оценить правило. Среда как в разделе 2: Python 3.12.10, pandas 3.0.5.

"""Заглядывание вперёд в строке: одно правило, два итога."""
import pandas as pd

index = pd.date_range("2026-06-01", periods=8, freq="1h", tz="UTC")
df = pd.DataFrame(
    {"close": [1.0800, 1.0810, 1.0825, 1.0815, 1.0830, 1.0845, 1.0840, 1.0855]},
    index=index,
)
df["sma"] = df["close"].rolling(3).mean()
df["return"] = df["close"].pct_change()

# Правило: лонг, когда закрытие выше среднего трёх свечей.
df["position"] = (df["close"] > df["sma"]).astype(int)

# НЕВЕРНО: позиция t умножена на доходность той же строки.
# Доходность t возникла между закрытиями t-1 и t: решение
# использует не существовавшую при входе цену.
df["result_with_lookahead"] = df["position"] * df["return"]

# ВЕРНО: решение на закрытии t работает только с доходностью t+1.
df["result_without_lookahead"] = df["position"].shift(1) * df["return"]

print(df[["result_with_lookahead", "result_without_lookahead"]].sum().round(6))

Вывод запуска:

result_with_lookahead       0.005082
result_without_lookahead    0.001384
dtype: float64

Интерпретация. То же правило, данные, один вызов shift(1) составляет всю разницу, а сумма простых периодических доходностей падает с 0,005082 до 0,001384 — ниже 30 процентов исходной величины. Восемь наблюдений — иллюстрация, а не измерение. На многолетних данных такая ошибка может превратить среднюю стратегию в выдающуюся, а выдающуюся — в невозможную. Обе величины безразмерны и не учитывают спред и комиссию; после их вычета корректный результат был бы ещё ниже.

Оговорка об арифметике. pct_change() даёт простые доходности, которые, как отмечено в разделе 4, накапливаются перемножением, а не сложением. Складывать можно лишь логарифмические. Здесь сумма — сознательное приближение: при величинах порядка тысячных её отличие от правильного накопления в примере меньше 0,00001, то есть более чем на два порядка ниже показываемого эффекта. Для расчёта результата счёта, а не механизма сдвига, перемножайте (1 + r) либо переходите к логарифмическим доходностям и только затем суммируйте.

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

Таблица цен смещённой и сдвинутой доходности
Будущее закрытие меняет −0.98% на мнимые +2.00%.

Другие формы заглядывания вперёд — один вопрос: было ли известно при решении?

  • Максимум или минимум текущей свечи в условии входа, хотя окончательно они известны лишь после закрытия
  • Масштабирование или нормализация всего набора до разделения обучающей и проверочной частей: параметры преобразования переносят информацию из будущего
  • Индикаторы, сглаживающие ряд или отмечающие развороты по более поздним свечам: историческое значение отличается от увиденного в реальном времени
  • Пересмотренная макростатистика вместо исходной
  • Исполнение заявки по закрытию той свечи, на которой возник сигнал

Утечка подготовки данных. Второй пункт заслуживает отдельного пояснения: в Python его нарушают почти автоматически. Масштабирование всего DataFrame до разделения периодов занимает одну безобидную с виду строку, но переносит знание будущего распределения данных в проверочную часть. Параметры преобразования определяют только по более ранним данным и применяют к поздним, а не наоборот.

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

Множественные проверки. Чем больше вариантов проверено на одних данных, тем выше шанс случайности лучшего. В явно заданной нулевой модели при 10 000 тестов с вероятностью ложноположительного результата 0,05 каждый ожидаемое число «значимых» результатов равно 0,05 × 10 000 = 500. Это следует из сложения вероятностей и не требует независимости. Зависимость меняет распределение числа результатов, а не его среднее при этих допущениях. Масштаб объясняет, почему число попыток нужно публиковать рядом с результатом.

Формальная поправка Deflated Sharpe Ratio предложена Bailey и López de Prado в 2014 году [12]. Перед использованием готовой реализации проверьте состояние и лицензию библиотеки: некоторые проекты этой области меняли модель распространения. Формула описана в исходной публикации и может быть реализована самостоятельно.

Чего раздел не охватывает. Переобучение, отбор выборки, скользящее тестирование, смещение выживших и устойчивость к росту издержек относятся к методологии, описанной в статье о бэктестинге и форвардном тестировании. Здесь рассматриваются только ошибки, допускаемые и проверяемые в коде.

Проверка кода до доверия:

  1. Использует ли результат строки t только данные, доступные до момента решения?
  2. Применён ли сдвиг ровно один раз — к сигналу или данным, а не к обоим?
  3. Явно ли записаны момент и цена исполнения?
  4. Включены ли транзакционные издержки в модель либо их отсутствие сознательно оговорено?
  5. Выборки разделены хронологически?
  6. Определены ли преобразования исключительно на более ранней части данных?
  7. Число вариантов записано с итогом?
  8. Имеет ли временной индекс явный часовой пояс и возрастает ли без дубликатов?
Правило: удивительно хороший итог — гипотеза ошибки, не преимущества. Прежде чем объявить находку исключительной, пройдите список по пунктам и запустите код на данных с известным ожидаемым ответом. Например, при постоянной цене доходность от её изменения до издержек равна нулю, а после издержек сделки могут быть убыточными. Тест с известным ответом выявляет больше ошибок, чем созерцание кривой средств.

10. Сколько кода нужно

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

УровеньНавыкиЧто даётПримерный срок
0: без кодаНетАнализ доступными платформе способами и в навязанных ею рамках—
1: чтениеПонимание чужого скрипта, изменение параметров, запуск в изолированной средеПроверка скрипта из репозитория на заглядывание в будущее и наличие издержекНесколько недель
2: свой анализpandas, numpy, matplotlib, свои скриптыАнализ своего журнала и рыночных данных без ограничений таблицыНесколько месяцев
3: интеграцияAPI, обработка ошибок, журналирование, планирование запусков, тестыУведомления и подготовка заявок с подтверждениемОт полугода
4: исполнениеАвтотесты, состояние, развёртывание, мониторингПрограмма без подтверждения каждого решенияГоды практики

Сроки приблизительны и зависят от исходных знаний и реально вложенных часов в неделю. Это порядок величины, а не обещание.

Уровни 1 и 2 не дают преимущества. Они позволяют проверить осмысленность чужого решения и перестать покупать непроверяемые системы. Это реальная ценность другого рода, чем обещают продавцы готовых инструментов. Уровень 4 нужен для сопровождения программы исполнения, а не улучшения торговли. Применения Python в финансах обозревает учебник Хильпиша [11], с оговоркой для любой книги о коде: перед использованием проверяйте примеры на текущих версиях библиотек, поскольку со времени публикации некоторые интерфейсы изменились.

Код языковой модели. Ассистенты могут написать значительную часть аналитического кода быстрее вас. Они также способны породить внешне правильный код с заглядыванием в будущее или сослаться на функцию, отсутствующую в вашей версии библиотеки. Без навыков уровня 1 вы не обнаружите ни того ни другого. Возможности и ограничения языковых моделей описаны отдельно: ИИ и LLM в трейдинге.

Разумный порядок обучения — каждый шаг на предыдущем с рабочим итогом:

  1. Основы языка — переменные, списки, функции, условия: читать без догадок
  2. pandas на своих данных — загрузите экспорт своего торгового журнала и рассчитайте статистику по дням недели и инструментам. Если предпочитаете учебник документации, этот этап полностью охватывает книга Маккинни, автора pandas [10]
  3. График — постройте кривую средств и график просадки по расчётам предыдущего шага
  4. Качество данных — пример раздела 4 на своём файле: пояс, дубли, пробелы
  5. Интеграция на чтение — получите данные API на тестовом счёте с токеном без транзакционных прав, если поставщик различает такие права

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

Самое ценное — наименее зрелищно. Анализ своего журнала в pandas показывает, откуда действительно приходит результат, а где это лишь кажется. Обычно это полезнее построения программы исполнения и требует только уровня 2. Основание не в чьих-то измерениях, а в том, что такой анализ работает с вашими данными, а не чужими.

Начните с одной задачи: загрузите свои данные, вычислите то, что можно вручную проверить на трёх строках, и сравните ответы. Совпадение Python с ручным счётом даёт основу. Несовпадение означает первую найденную ошибку — лучший старт, чем сразу работающий скрипт, которого вы не понимаете.

Python не исправит плохую стратегию. В лучшем случае он быстрее покажет, что она была плохой.

FAQ: Python в трейдинге

Достаточно ли быстр Python для автоматизации торговли?
Зависит от узкого места. В стратегии, входящей на открытии следующей H1-свечи, время вычислений Python ничтожно по сравнению с прохождением заявки через сеть и сервер брокера. При торговле внутри минутной свечи важна вся цепочка: возраст данных при решении, время ответа конечной точки и подтверждения исполнения. Эти величины нельзя цитировать по памяти: измеряйте их у своего брокера в собственной цепочке и записывайте дату и число попыток. Python не подходит для сверхнизких задержек, но это иной класс задач, чем розничная торговля.
Сколько времени нужно, чтобы освоить Python до уровня, полезного в трейдинге?
Для чтения чужого кода и изменения параметров — несколько недель регулярной работы; для самостоятельного анализа журнала в pandas с графиками — несколько месяцев. Сроки зависят от исходной подготовки и часов в неделю, это лишь порядок величины. Полный курс языка не нужен: для загрузки файла и статистики не требуются классы, декораторы или конкурентность. Быстрее учиться на собственных данных, поскольку сразу понятно, имеет ли результат смысл.
Можно ли подключить Python к счёту MT4 или MT5?
Для MT5 есть официальный MetaTrader5 пакет через pip, но лишь для Windows. Официальной установки macOS и Linux нет: требуется терминал вместе с Python под Wine либо отдельный посредник [ЗАВИСИТ ОТ ВЕРСИИ/СРЕДЫ]. Нужно понимать его устройство: связь идёт с MetaTrader 5 на той же машине, не напрямую с сервером брокера. Терминал должен быть установлен, но заранее запускать его необязательно: документация указывает, что вызов initialize() сам запускает терминал при необходимости; параметры входа необязательны, без них используются сохранённые в терминале данные [18]. Это не косметика: скрипт планировщика может сам поднять терминал и войти, что удобно, но «терминал был закрыт» защитой не служит. Интеграция появилась в версии 2019 года, build 2085 [7]. Это не независимый от брокера REST API, так его описывать нельзя. Внешний облачный мост добавляет звено и требует доверить доступ третьей стороне. Для начала проще и безопаснее одно направление: экспорт терминала в файл, анализ Python, решение и исполнение человеком.
Где хранить ключи API и что делать, если ключ утёк?
Исключительно в переменных окружения либо в конфигурационном файле, который хранится вне репозитория и указан в .gitignore. Никогда в коде, комментарии или блокноте: ячейка блокнота сохраняет и вывод, поэтому напечатанный токен остаётся в файле. Если ключ попал в репозиторий, удаление строки ничего не даёт: значение остаётся в истории версий. Единственный действенный ответ — отозвать ключ на стороне брокера и создать новый, и только затем очищать историю. Независимо от этого стоит установить ограничения размера позиций на стороне счёта как второй слой защиты, не зависящий от вашего кода.
Проверен ли код в этой статье?
Да, текст указывает статус каждого блока отдельно. CSV-пример раздела 4 и сравнение сдвига раздела 9 выполнены в указанной среде (Python 3.12.10, pandas 3.0.5); показан реальный вывод. Загрузка конфигурации проверена во всех пяти ветвях от отсутствия переменных до неизвестного имени среды. Защитные шлюзы проверены офлайн, без сети и секретов. Чтение API проверялось с имитацией HTTP-сессии в памяти, без запуска сервера и без сетевых соединений, в том числе с 127.0.0.1 — и не с брокером. Проверены документированные сценарии обработки ответов, включая исчерпание лимита попыток и исключение соединения. Однако в статье нет ни одной строки, отправляющей заявку: ветвь отправки заменена структурным эскизом. Торговый код не испытывают на чужом счёте, а готовая функция без теста учила бы только доверию к непроверенному коду. Запуск кода не равнозначен подтверждению предположений о чужом сервере: проверяйте пути конечных точек, структуры ответов и лимиты в документации своего брокера и на учебном счёте.
Можно ли доверять стратегии из публичного репозитория?
Без собственной проверки — нет, и дело не в недобросовестности авторов. Типичные пробелы: непроверенный сдвиг сигнала, отсутствие модели затрат, неописанные среда и версии библиотек, а также отсутствие сведений, сколько вариантов проверили, прежде чем показать этот. До любого запуска прочтите, где возникает сигнал и к какому периоду отнесён результат; проверьте, есть ли затраты в коде вообще, восстановите среду по файлу зависимостей и запустите изолированно. Если репозиторий не указывает версии библиотек, результат всё равно невоспроизводим, а невоспроизводимый результат нельзя ни подтвердить, ни опровергнуть.

Источники и справочные материалы

  1. Документация pandas 3.0.5 — pandas.pydata.org/docs — структуры данных, временной индекс и часовые пояса, resample, обработка пропущенных значений; версия проверена 2026-08-22
  2. pandas — примечания к выпускам 2.2.0 и 3.0.0 — pandas.pydata.org/docs/whatsnew — объявление устаревшим параметра method в fillna() и изменения поведения по умолчанию
  3. NumPy: документация и выпуски — numpy.org/doc — векторизация, генераторы случайных чисел и установка начального значения
  4. Документация Python — docs.python.org/3 — модули venv, logging и timeit и обработка исключений
  5. OANDA v20: лучшие практики — developer.oanda.com/rest-live-v20/best-practices — рекомендуемая частота запросов по установленному соединению (100/s) и создания новых соединений (2/s); проверено 2026-08-22
  6. MQL5: интеграция Python — mql5.com/en/docs/python_metatrader5 — пакет MetaTrader5 и его взаимодействие с терминалом
  7. MetaTrader 5: выпуск build 2085 — metatrader5.com/en/releasenotes/terminal/2085 — появление интеграции Python в терминале
  8. Документация cTrader Algo — help.ctrader.com/ctrader-algo — встроенная поддержка C# и Python для cBot, индикаторов и плагинов; состояние на 2026-08-22
  9. TA-Lib (Python-привязки) — pypi.org/project/TA-Lib — бинарные пакеты на PyPI и установка без компиляции библиотеки C
  10. McKinney, W. — Python для анализа данных, O'Reilly, 3-е издание, 2022 — pandas, numpy и работа с временными рядами
  11. Hilpisch, Y. — Python для финансов, O'Reilly, 2-е издание, 2018 — применение Python в финансах; фрагменты кода следует проверять на актуальных версиях библиотек
  12. Bailey, D. H., López de Prado, M. — Скорректированный коэффициент Шарпа, Journal of Portfolio Management, 2014 — поправка коэффициента Шарпа на число проверенных вариантов
  13. PyPI — страницы пакетов и JSON-интерфейс реестра — pypi.org — даты и номера последних выпусков упомянутых в тексте библиотек (mplfinance, streamlit, openpyxl, pandas-ta, backtrader, vectorbt, backtesting.py, ib_insync, ib_async); проверено 2026-10-08
  14. OANDA v20: Order и Transaction API — developer.oanda.com/rest-live-v20/transaction-df – clientExtensions как необязательные пользовательские данные заявки и предупреждение не задавать их на счетах, связанных с MT4; документированной гарантии идемпотентности рыночных ордеров нет; проверено 2026-08-22
  15. OANDA v20: руководство разработки — developer.oanda.com/rest-live-v20/development-guide — принудительно применяемый REST-лимит 120 запросов в секунду с ответом HTTP 429, ограничение в 20 активных потоков и 2 новых соединения в секунду, учитываемые по IP-адресу отправителя запросов; проверено 2026-08-22
  16. Microsoft: спецификации и ограничения Excel — support.microsoft.com — размер рабочего листа: 1048576 строк на 16384 столбца; проверено 2026-08-22
  17. ШНБ: релиз 15 января 2015 года — snb.ch — отмена минимального курса 1.20 франка за евро и снижение ставки до −0.75 процента
  18. MQL5 — MetaTrader5.initialize() – mql5.com/en/docs/python_metatrader5/mt5initialize_py — запуск терминала при установлении соединения, если это необходимо, и необязательные параметры path, login, password и server с использованием данных, сохранённых в терминале; проверено 2026-08-22

Ваш прогресс обучения

Закончили урок? Отметьте это самостоятельно. Открытие или прокрутка страницы не засчитывают прохождение.

Для сохранения прогресса нужны JavaScript и хранилище браузера.

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

Jarosław Wasiński

Главный редактор, ForexMechanics.com • Макро- и валютный аналитик

Независимый аналитик и профессионал с более чем 20-летним опытом работы на финансовых рынках. Активно работает на Форексе с 2004 года, специализируется на фундаментальном анализе, структуре внебиржевого (OTC) рынка и дисциплинированном управлении риском капитала.

  • Основатель и главный редактор MyBank.pl, который с 2004 года предоставляет надёжное финансовое образование.
  • Автор сотен глубоких рыночных комментариев, структурных аналитических материалов и обучающих пособий для трейдеров и инвесторов.
  • Сторонник прозрачности рынка, продвигает обучение, основанное на конкретных данных и институциональных отчётах (BIS, ESMA, CFTC).

Все материалы носят исключительно образовательный и информационный характер – они не являются инвестиционной рекомендацией или каким-либо советом. Помните: торговля с кредитным плечом (Форекс/CFD) сопряжена с высоким риском быстрой потери капитала.