January 25

PriceGenerator — платформа для генерации биржевых цен

Содержание статьи:

Введение

Иногда для целей тестирования автоматизированных торговых алгоритмов бывает необходимо получить набор данных с биржевыми ценами. Обычно трейдеры и аналитики используют модель цен вида OHLCV-candlesticks (open, high, low, close, volume), так называемые японские свечи. Одна строка таких данных представляет собой набор цен для построения одной японской свечи: дата открытия, цена открытия, наибольшая цена, наименьшая цена, цена закрытия на данном временном интервале и значение объёма торгов.

📈PriceGenerator — это платформа для генерации цен, которую можно использовать как Python-модуль или запускать из командной строки и генерировать случайные ценовые данные, максимально похожие на «настоящие цены», но с заранее заданными статистическими характеристиками. Можно задать интервал цен, таймфрейм, максимальное и минимальное значения для диапазона цен, максимальный размер для свечей, вероятность направления для очередной свечи, вероятность ценовых выбросов, количество генерируемых свечей и некоторые другие параметры.

Для дальнейшего статистического анализа цен в Python очень часто используют библиотеку Pandas. Цены, сохранённые в виде Pandas DataFrame, могут выглядеть примерно так:

Пример ценового ряда в модели OHLCV-candlesticks, сохранённые в виде Pandas DataFrame

Библиотека PriceGenerator позволяет:

  • сохранить сгенерированные цены в .csv-формате (пример: ./media/test.csv);
  • сохранить сгенерированные цены в переменную формата Pandas DataFrame для дальнейшего использования в скриптах автоматизации;
  • автоматически рассчитать некоторые статистические и вероятностные характеристики сгенерированных цен и сохранить их в Markdown-формате (пример: ./media/index.html.md);
  • загрузить цены реальных инструментов по модели OHLCV-candlesticks из .csv-файла и провести их статистический анализ;
  • нарисовать график сгенерированных или загруженных реальных цен и сохранить его в html-формате (пример: ./media/index.html);
  • сгенерированные цены, график и некоторые данные о поведении цен можно сохранить в виде обычной png-картинки (пример: ./media/index.html.png). Дополнительно на графике можно включить отображение некоторых популярных индикаторов (скользящие средние, полосы Боллинджера, ZigZag и другие).
Пример графика, построенного из цепочки сгенерированных цен, с выводом информации по некоторым статистикам ряда

Метод генерации цен

Генерация набора свечей заданной длины horizon происходит по следующему (упрощённому) алгоритму:

  1. Определяется или случайным образом генерируется в заданном диапазоне [minClose, maxClose] цена открытия open первой свечи.
  2. Случайным образом, в зависимости от значения вероятности направления очередной свечи upCandlesProb (по умолчанию 50%), определяется направление свечи. Если random() ≤ upCandlesProb, то будет генерироваться up-свеча (у которой open ≤ close), иначе будет генерироваться down-свеча (у которой open > close).
  3. После определения направления значение close свечи случайным образом генерируется так, чтобы для получившегося «тела» свечи было справедливо |open − close| ≤ maxCandleBody.
  4. Случайным образом, в зависимости от значения вероятности ценовых выбросов свечи maxOutlier (по умолчанию 3%), генерируются значения high и low свечи. Если random() ≤ maxOutlier, то будет генерироваться свеча с ценовыми выбросами: «хвосты» свечи могут получиться достаточно большими, что будет имитировать реальные ценовые «выбросы» на рынке. Если свеча без аномалий, то «хвосты» будут генерироваться в диапазоне не более половины тела свечи.
  5. Значение close сгенерированной свечи становится значением цены открытия open следующей свечи.
  6. Далее повторяются шаги 2–5 пока не будет сгенерирована вся цепочка цен заданной длины horizon.

Все параметры можно задать после инициализации экземпляра класса PriceGenerator(). Результат генерации цен сохраняется в переменной класса prices в формате Pandas DataFrame и может использоваться для дальнейшего анализа.

Также, для наилучшей реалистичности, значения объёма генерируются в зависимости от предыдущего значения и вероятности выбросов. Алгоритм генератора может создавать серию свечей, образующих сложные тренды, включая различные выбросы для верхних и нижних теней свечей, их тел и объёмов (пример: ./media/long-series-example-with-realistic-volumes.png).

Пример долгосрочного ряда (5000 свечей) с реалистичным разбросом значений объёмов и различными трендами

Примеры использования из командной строки

Как установить

Проще всего использовать установку через PyPI:

pip install pricegenerator

После этого можно проверить установку командой:

pip show pricegenerator

Внутренняя справка по ключам:

pricegenerator --help

Генерация цен с параметрами по умолчанию

Давайте попробуем сгенерировать случайные ценовые данные (ключ --generate) со всеми параметрам и по умолчанию и сохранить их в файл test.csv (ключ --save-to имя_csv_файла). Команда может выглядеть так:

pricegenerator --debug-level 10 --generate --save-to test.csv

Должен получиться вывод логов примерно следующего содержания:

Логи генерации

Также рядом будет сохранён файл test.csv, пример которого можно посмотреть здесь: ./media/test.csv.

Генерация цен, получение статистики и отрисовка графика

В следующем примере давайте не только сгенерируем файл с данными по ценам, но и получим некоторые статистические параметры цен, а также отрисуем цены на графике (ключ --render-bokeh имя_html_файла). Команда может быть примерно такой:

pricegenerator --debug-level 20 --generate --save-to test.csv --render-bokeh index.html

В случае успеха вы получите вывод в лог, похожий на этот:

Логи генерации

После выполнения команды выше вы получите три файла:

  • test.csv — файл в .csv-формате, который содержит случайный набор цен, похожих на настоящие (пример: ./media/test.csv);
  • index.html — график цен и статистику, отрисованные при помощи библиотеки Bokeh и сохранённые в .html-файл (пример: ./media/index.html);
  • index.html.md — статистика в текстовом виде, сохранённая в маркдаун-формате (пример: ./media/index.html.md).

Статистика и график из сохранённых цен

Если вам нужно получить статистику по уже сгенерированным или реальным ценам, вы можете просто загрузить файл (ключ --load-from имя_csv_файла) и отрисовать график (ключ --render-bokeh имя_html_файла):

pricegenerator --debug-level 20 --load-from test.csv --render-bokeh index.html

В результате выполнения команды вы получите аналогичный график в index.html и статистику в index.html.md.

Статистика и график на упрощённом шаблоне

В примерах выше вы можете использовать отображение цен на простом, не интерактивном графике цен. Для этого используется библиотека Google Candlestick chart и простейший jinja2 шаблон. Давайте опять загрузим цены (ключ --load-from имя_csv_файла), но отрисуем график через Google библиотеку (ключ --render-google имя_html_файла):

pricegenerator --debug-level 20 --load-from test.csv --render-google index_google.html

В результате выполнения команды вы получите график (пример: ./media/index_google.html) и статистику в Markdown файле. Выглядеть он будет примерно так:

Свечной график сгенерированных цен на простом, не интерактивном графике

Переопределение параметров

Давайте изменим некоторые параметры по умолчанию, которые влияют на генерацию цен и нарисуем свой уникальный график:

pricegenerator --debug-level 10 --ticker "MY_PRICES" --precision 2 --timeframe 240 --start "2020-01-01 00:00" --horizon 150 --max-close 18000 --min-close 14000 --init-close 15000 --max-outlier 1000 --max-body 500 --max-volume 400000 --up-candles-prob 0.48 --outliers-prob 0.05 --trend-deviation 0.03 --zigzag 0.03 --generate --render-bokeh index_custom.html

Параметры означают:

  • --ticker "MY_PRICES" — установить название графика для ценовой последовательности как MY_PRICES;
  • --precision 2 — установить точность, то есть количество знаков после запятой;
  • --timeframe 240 — одна свеча должна отражать изменение цен за 4 часа (240 минут);
  • --start "2020-01-01 00:00" — дата и время первой свечи 2020-01-01 00:00;
  • --horizon 150 — сгенерировать 150 свечей;
  • --max-close 18000 — максимальная цена закрытия у любой свечи должна быть не больше 18000;
  • --min-close 14000 — минимальная цена закрытия у любой свечи должна быть не больше 14000;
  • --init-close 15000 — цена закрытия «предыдущей» и, соответственно, цена открытия первой генерируемой свечи должна быть равной 15000;
  • --max-outlier 1000 — если у свечи есть «выбросы» и «хвосты» то они должны быть не больше чем 1000;
  • --max-body 500 — максимальный размер «тела» свечи должен быть не более 500;
  • --max-volume 400000 — максимальный объём торгов для каждой свечи должен быть не более 400000;
  • --up-candles-prob 0.48 — установить вероятность того, что очередная свеча будет вверх, равной 0.48 (48%);
  • --outliers-prob 0.05 — установить вероятность появления выбросов равной 0.05 (5%);
  • --trend-deviation 0.03 — для определения тренда относительное изменение цен закрытия первой и последней свечей должно отличаться на ±0.03 (3%);
  • --zigzag 0.03 — относительная разница между двумя точками ZigZag индикатора;
  • --generate — запустить генерацию цен;
  • --render-bokeh index_custom.html — сохранить сгенерированные цены в файл index_custom.html и открыть его в браузере.

По умолчанию используется светлая тема для графиков. Но если вы используете ключ --render-bokeh, вы также можете добавить к команде ключ --dark. В этом случае график будет отрисован в тёмном стиле:

Свечной график сгенерированных цен с переопределёнными параметрами на интерактивном графике

В результате выполнения команды у вас получится свой уникальный график случайных цен с переопределёнными базовыми параметрами генератора. У нас получились вот такие артефакты:

Разделение данных по трендам

Для указания трендов существуют два консольных ключа: --split-trend и --split-count. Они указывают на внешний вид цепочки мини-трендов и количество свечей в каждом тренде.

Ключ --split-trend показывает вид движения, например --split-trend=/\- означает, что будет сгенерирована такая цепочка свечей, что сначала тренд будет возрастающий в её первой части, затем тренд сменится на нисходящий, и в последней цепочке свечей тренда не будет.

Также есть возможность указания направлений генерируемых трендов с помощью слов или букв. Слова могут быть следующими: up, down или no, а буквы могут быть такими: u, d или n. Эти слова и знаки могут использоваться вместе с ключом --split-trend, в дополнение к уже имеющейся возможности указывать тренд символами /\-. Для разделения слов и букв используется символ дефиса, например, --split-trend=up-down-no-up или --split-trend=u-d-n-u.

Ключ --split-count устанавливает количество свечей в каждом мини-тренде, например --split-count 5 10 15 означает, что в сгенерированной цепочке свечей будет три мини-тренда с 5, 10 и 15 свечами в каждом из них.

Чтобы понять, как это работает, попробуйте один из следующих примеров:

pricegenerator --horizon 300 --render-bokeh index.html --split-trend="/\-" --split-count 50 100 150 --generate
pricegenerator --horizon 300 --render-bokeh index.html --split-trend="\/\" --split-count 50 100 150 --generate
pricegenerator --horizon 300 --render-bokeh index.html --split-trend="\-/" --split-count 50 100 150 --generate
pricegenerator --horizon 100 --render-bokeh index.html --split-trend="/\/\" --split-count 20 30 30 20 --generate

Для последнего примера вы можете получить картинку движения цены, похожую на следующую (пример: ./media/index_with_trends.html.png).

Свечной график сгенерированных цен с указанием параметров трендов

Работа с PriceGenerator через импорт модуля

Давайте рассмотрим пример генерации цен с некоторыми изменёнными параметрами, сохраним их в Pandas DataFrame и нарисуем график. Просто сохраните и запустите следующий скрипт:

from pricegenerator.PriceGenerator import PriceGenerator, uLogger
from datetime import datetime, timedelta
# Отключаем логирование, чтобы не мешалось:
uLogger.setLevel(0)
# --- Инициализируем экземпляр класса генератора и настраиваем некоторые параметры:
priceModel = PriceGenerator()
priceModel.precision = 1  # сколько знаков после запятой должно быть в сгенерированных ценах
priceModel.ticker = "MY_GENERATED_PRICES"  # произвольное имя (тикер) генерируемых цен
priceModel.timeframe = timedelta(days=1)  # временной интервал между генерируемыми свечами, 1 час по умолчанию
priceModel.timeStart = datetime.today()  # с какой даты начинать генерацию свечей, по умолчанию с текущего времени
priceModel.horizon = 60  # сколько сгенерировать свечей, их должно быть не меньше 5-ти, по умолчанию 100
priceModel.maxClose = 16000  # максимальная цена закрытия свечи во всей цепочке цен, по умолчанию генерируется случайно в интервале (70, 90), это немного похоже на цены USDRUB
priceModel.minClose = 13800  # минимальная цена закрытия свечи во всей цепочке цен, по умолчанию генерируется случайно в интервале (60, 70), это немного похоже на цены USDRUB
priceModel.initClose = 14400  # если цена указана, то она будет ценой close (как бы "предыдущей" свечи) и одновременно ценой open самой первой свечи в генерируемой цепочке. None по умолчанию означает, что цена open первой свечи будет сгенерирована случайно в интервале (minClose, maxClose)
priceModel.maxOutlier = 500  # Максимальное значение для ценовых выбросов "хвостов" у свечей. None по умолчанию означает, что выбросы будут не более чем на (maxClose - minClose) / 10
priceModel.maxCandleBody = 300  # Максимальное значение для размера тел свечей abs(open - close). None по умолчанию означает, что тело свечи может быть не более чем 90% от размера максимального выброса: maxOutlier * 90%
priceModel.maxVolume = 400000  # максимальное значение объёма торгов для одной свечи, по умолчанию берётся случайным образом из интервала (0, 100000)
priceModel.upCandlesProb = 0.46  # вероятность того, что очередная генерируемая свеча будет вверх, 50% по умолчанию
priceModel.outliersProb = 0.11  # вероятность того, что очередная генерируемая свеча будет иметь ценовой "выброс", 3% по умолчанию
priceModel.trendDeviation = 0.005  # колебание цены close первой и последней свечей для определения тренда. "NO trend" если разница меньше этого значения, по умолчанию ±0.005 или ±0.5%.
priceModel.zigzag = 0.05  # относительная разница между двумя точками ZigZag индикатора, 0.03 по умолчанию
priceModel._chartTitle = ""  # произвольный заголовок графика, обычно генерируется автоматически
# Цены пока не сгенерированы и не загружены, проверим это:
print("Current prices:\n{}".format(priceModel.prices))
# Запускаем генератор цен, при этом они будут сохранены
# в переменной priceModel.prices в формате Pandas DataFrame:
priceModel.Generate()
# Убеждаемся, что цены сгенерированы:
print("Generated prices as Pandas DataFrame:\n{}".format(priceModel.prices))
# Словарь с посчитанной статистикой сохраняется в переменную self.stat:
print("Dict with statistics:\n{}".format(priceModel.stat))
# Сохраняем OHLCV-цены в .csv-файл
priceModel.SaveToFile(fileName="test.csv")
# Сохраняем график цен в html-файл и сразу открываем его в браузере.
# Статистика в текстовом виде будет автоматически сохранена в Markdown-файле с именем fileName + ".md".
priceModel.RenderBokeh(fileName="index.html", viewInBrowser=True)
# Вместо библиотеки Bokeh вы можете отрисовать цены на простом, не интерактивном графике,
# через библиотеку Google Candlestick chart. Просто раскомментируйте строчки ниже.
# Перед вызовом priceModel.RenderGoogle(), вы можете задать свой шаблон в переменной self.j2template
# priceModel.j2template = "google_template_example.j2"  # полный путь до шаблона или мультистроковая переменная с jinja2-шаблоном
# priceModel.RenderGoogle(fileName="index.html", viewInBrowser=True)

При запуске скрипта вы получите аналогичный вывод в логи, три файла: test.csv, index.html и index.html.md. Вы можете самостоятельно поэкспериментировать с параметрами класса PriceGenerator() для генерации цен подходящих под ваши условия.

Также вы можете манипулировать графиком и добавлять новые линии или маркеры на основной график. Используйте для этого параметры markers и lines.

Pandas DataFrame markers содержит ряды, которые показывают, какой маркер нанести для той или иной свечи. Маркер представляет собой некоторый символ, например, ×, или или какой-либо другой. Датафрейм с маркерами должен содержать, как минимум, два ряда данных. Это столбец datetime, с датой и временем, и один из столбцов или все сразу: markersUpper, markersCenter или markersLower, с маркерами, которые нужно поставить сверху, по центру или снизу свечи соответственно. Длины рядов с маркерами должны быть равны длине основного ряда со свечами.

Лист lines содержит ряды Pandas DataFrame с точками новых линий, которые нужно разместить на основном свечном графике. Каждый датафрейм с линиями должен содержать, как минимум, два столбца. Это datetime (первый столбец), с датой и временем, и произвольно названный второй столбец custom_line_name, с y-координатами точек линии. Длины рядов с точками линий должны быть равны длине основного ряда со свечами.

Пример:

from pricegenerator.PriceGenerator import PriceGenerator, uLogger
from datetime import datetime, timedelta
import pandas as pd
uLogger.setLevel(0)  # Отключаем излишнее логирование для этого примера.
# Инициализируем PriceGenerator:
priceModel = PriceGenerator()
priceModel.ticker = "TEST_PRICES"
priceModel.precision = 0
priceModel.timeframe = timedelta(days=1)
priceModel.timeStart = datetime.today()
priceModel.horizon = 75
priceModel.maxClose = 140
priceModel.minClose = 40
priceModel.initClose = 50
priceModel.maxOutlier = 35
priceModel.maxCandleBody = 15
priceModel.maxVolume = 400000
priceModel.upCandlesProb = 0.51
priceModel.outliersProb = 0.1
priceModel.trendDeviation = 0.005
priceModel.trendSplit = "/\/"
priceModel.splitCount = [40, 10, 25]
priceModel.Generate()  # Генерируем основную серию свечей.
# Давайте построим новую среднюю линию на основном графике и установим маркеры сверху, по центру и снизу свечей:
priceModel.prices["avg"] = priceModel.prices.low + (priceModel.prices.high - priceModel.prices.low) / 2
priceModel.prices["markersUpper"] = pd.Series(["↓"] * len(priceModel.prices.high))
priceModel.prices["markersCenter"] = pd.Series(["×"] * len(priceModel.prices.avg))
priceModel.prices["markersLower"] = pd.Series(["↑"] * len(priceModel.prices.low))
priceModel.RenderBokeh(
    fileName="index1.html",
    viewInBrowser=True,
    darkTheme=True,  # Установите `False` для переключения светлой темы.
    markers=priceModel.prices[["datetime", "markersUpper", "markersCenter", "markersLower"]],
    lines=[priceModel.prices[["datetime", "avg"]]],
    showStatOnChart=True,
    showControlsOnChart=True,
    inline=False,  # Раскомментируйте, если скрипт запускается в Jupyter Notebook.
)

На выходе получаются промаркированными все свечи (пример: ./media/marked_dark.png). При желании в коде можно пропустить некоторые маркеры в соответствующих сериях.

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

На этом всё. Успехов вам в автоматизации и тестировании биржевых торговых стратегий! 🚀 😉✌️