Когда закончил писать механизм своего торгового робота обнаружил, что самое главное всё таки не сам механизм, а стратегия, по которой этот механизм будет работать.
Первый тесты на истории показали что с доходностью и тем более с тем как доходность портфеля компенсирует принимаемый риск (коэффициент Шарпа) проблемы, но неудачный опыт тоже опыт, поэтому решил описать его в статье.
Первый и самый важный вопрос — при помощи чего проводить тесты торговой стратегии на исторических данных? В какой программе или при помощи какой библиотеки создавать стратегию и потом прогонять её на истории?
Раз мой торговый робот создан в среде исполнения JavaScript Node.js, то и тесты в идеале должны проводится на чём-то схожем. Но забегая немного вперёд скажу что получилось по другому.
Windows? macOS? Linux?
Раз сам механизм робота кросс-платформенный, то хотелось чтобы и тесты можно было проводить при помощи кросс-платформенной утилиты. Однако когда рассматривал самые популярные программы, то обнаружилось что все программы из списка только для Windows. Кроме TradingView, который является веб-сервисом и Excel — который есть и для macOS.
Но похоже что веб-вервис и тем более Microsoft Excel — не лучший выбор. Тем не менее вот варианты, которые я рассматривал:
-
TradeStation: комплексная торговая и аналитическая платформа; идеально подходит для построения графиков, автоматизации стратегий и бэктестинга для акций, опционов, фьючерсов и криптовалют.
-
NinjaTrader: торговое программное обеспечение для фьючерсов и форекс; отлично подходит для расширенного построения графиков, бэктестинга и автоматизированной торговли.
-
MetaStock: фокусируется на техническом анализе и бэктестинге с обширными инструментами для построения графиков и индикаторов, популярен среди трейдеров акциями.
-
Wealth-Lab: платформа, известная расширенным бэктестингом и разработкой торговых стратегий с мощной поддержкой портфелей из нескольких активов.
-
TradingView: удобная в использовании платформа для построения графиков с социальными функциями; отлично подходит для технического анализа, обмена идеями и базового бэктестинга стратегий.
-
RealTest: легкое программное обеспечение для бэктестинга и разработки стратегий, известное своей скоростью и простотой, ориентированное на системных трейдеров.
-
Neuroshell Trader: специализируется на прогнозном моделировании и анализе на основе нейронных сетей; идеально подходит для трейдеров, интересующихся машинным обучением.
-
TSLab: платформа позволяет разрабатывать, тестировать и оптимизировать торговые системы без необходимости глубокого знания программирования.
-
The Zorro Project: бесплатная, легкая и скриптовая платформа, предназначенная для автоматизированной торговли, бэктестинга и исследований, популярная среди алгоритмических трейдеров.
-
и даже Microsoft Excel: универсальный инструмент для работы с электронными таблицами, часто используемый для анализа портфеля, пользовательского бэктестинга и организации данных в торговле.
Ни один из этих вариантов мне не приглянулся из-за отсутствия кросс-платформенности или этот вариант был Экселем.
Node.js библиотеки — не смог ❌
После этого стал смотреть библиотеки для Node.js. Выбор оказался небольшой и более-менее живыми мне показались:
-
grademark: https://github.com/Grademark/grademark
Библиотека Node.js для бэктестинга торговых стратегий на исторических данных. -
Fugle Backtest: https://github.com/fugle-dev/fugle-backtest-node
Библиотека Node.js для бэктестинга стратегий торговли акциями. -
CCXT — CryptoCurrency eXchange Trading Library: https://github.com/ccxt/ccxt
Библиотека Node.js для торговли криптовалютой, которая предоставляет унифицированный API для подключения и торговли на нескольких криптовалютных биржах, поддерживая как торговлю в реальном времени, так и доступ к историческим данным.
Для Grademark набросал через ChatGPT конкретный пример использования:
Пример с Grademark
const fs = require('fs'); const csvParser = require('csv-parser'); const grademark = require('grademark'); // Параметры стратегии let trailingStopPercent = 1; // Процент падения для трейлинг-стопа let buyThreshold = 1; // Минимальный процент для покупки // Функция для чтения данных из CSV-файла function readCsvData(filePath) { return new Promise((resolve, reject) => { let data = []; fs.createReadStream(filePath) .pipe(csvParser()) .on('data', (row) => { // Преобразуем каждую строку CSV в объект данных data.push({ time: new Date(row[1]), open: parseFloat(row[2]), high: parseFloat(row[3]), low: parseFloat(row[4]), close: parseFloat(row[5]), volume: parseInt(row[6]) }); }) .on('end', () => resolve(data)) .on('error', reject); }); } // Функция для агрегирования минутных данных в 5-минутные и часовые свечи function aggregateData(minuteData, interval) { const aggregated = []; let temp = []; minuteData.forEach((entry, index) => { temp.push(entry); if ((index + 1) % interval === 0) { const aggCandle = { open: temp[0].open, high: Math.max(...temp.map(t => t.high)), low: Math.min(...temp.map(t => t.low)), close: temp[temp.length - 1].close, volume: temp.reduce((acc, t) => acc + t.volume, 0), time: temp[temp.length - 1].time }; aggregated.push(aggCandle); temp = []; } }); return aggregated; } // Функция для запуска стратегии async function runBacktest(startMonth, testMonth) { let strategy = grademark({ buy: ({ fiveMinuteCandle, hourlyCandle }) => { return ( fiveMinuteCandle.close > calculateSMA(fiveMinuteCandle, 5) && hourlyCandle.close > calculateSMA(hourlyCandle, 60) ); }, sell: ({ price, maxPrice }) => { return price < maxPrice * (1 - trailingStopPercent / 100); }, }); // Сначала оптимизируем стратегию на данных за месяц let januaryData = await readCsvData(`data/e6123145-9665-43e0-8413-cd61b8aa9b13_2024${startMonth}.csv`); let fiveMinuteCandles = aggregateData(januaryData, 5); let hourlyCandles = aggregateData(januaryData, 60); strategy.optimize({ fiveMinuteCandles, hourlyCandles }); // Далее, проводим тестирование на следующем месяце (например, февраль) let februaryData = await readCsvData(`data/e6123145-9665-43e0-8413-cd61b8aa9b13_2024${testMonth}.csv`); let testFiveMinuteCandles = aggregateData(februaryData, 5); let testHourlyCandles = aggregateData(februaryData, 60); let testResults = strategy.run({ fiveMinuteCandles: testFiveMinuteCandles, hourlyCandles: testHourlyCandles }); console.log('Результаты теста:', testResults); } // Пример вызова функции для тестирования runBacktest('09', '10'); // Оптимизация на сентябрьских данных и тестирование на октябрьских данных // Функция для вычисления скользящей средней (SMA) function calculateSMA(candles, period) { if (candles.length < period) return 0; const sum = candles.slice(-period).reduce((acc, candle) => acc + candle.close, 0); return sum / period; }
При этом криптовалюты мне не подходили, Grademark почему-то не смог установить, а Fugle Backtest не приглянулся.
Python библиотеки — заработало! ✅
В Python есть несколько популярных библиотек для бэктестинга торговых стратегий, рассчитанных на разные уровни сложности и типы активов. Вот найденные варианты:
-
Backtesting.py https://github.com/kernc/backtesting.py
Легкая, интуитивно понятная библиотека для векторизованного бэктестинга, включающая популярные индикаторы и метрики.
❌ 4 года не обновлялась. -
Backtrader https://github.com/mementum/backtrader
Одна из самых популярных и многофункциональных библиотек для бэктестинга. Поддерживает несколько активов, таймфреймов, индикаторов и оптимизацию стратегий. -
PyAlgoTrade https://github.com/gbeced/pyalgotrade
Простая библиотека бэктестинга со встроенной поддержкой технических индикаторов и создания базовой стратегии.
❌ Этот репозиторий был заархивирован владельцем 13 ноября 2023 г. -
Zipline https://github.com/quantopian/zipline
Разработанная Quantopian (теперь поддерживаемая сообществом), Zipline — это надежная библиотека бэктестинга, ориентированная на событийно-управляемое бэктестирование, используемая профессионалами.
❌ 4 года не обновлялась. -
QuantConnect/Lean https://github.com/QuantConnect/Lean
Движок с открытым исходным кодом, лежащий в основе QuantConnect; поддерживает бэктестинг и торговлю в реальном времени для нескольких классов активов. -
VectorBT https://github.com/polakowo/vectorbt
Разработан для быстрого векторизованного бэктестинга и анализа стратегий непосредственно на Pandas DataFrames. -
Fastquant https://github.com/enzoampil/fastquant
Удобная библиотека бэктестинга, разработанная для быстрого тестирования с минимальной настройкой, вдохновленная Prophet от Facebook.
❌ 3 года не обновлялась. -
MibianLib https://github.com/yassinemaaroufi/MibianLib
Фокусируется на ценообразовании и волатильности опционов, а не на полном бэктестинге, но полезен для стратегий, связанных с опционами.
❌ 11 лет не обновлялась.
Сначала выбрал использовать Backtesting.py, потому что она упоминалась на многих сайтах, но уже на первоначальном этапе использования стали вылазит проблемы. Ошибка возникла из-за несоответствия в том, как новые версии pandas
обрабатывают метод get_loc()
. Аргумент method='nearest'
больше не поддерживается в последних версиях pandas
. Эта проблема связана с тем, как библиотека Backtesting.py взаимодействует с новыми версиями pandas
, в частности, при повторной выборке данных для построения графиков. А новой версии Backtesting.py, которая решает эту проблему и поддерживает последние изменения API pandas
просто нет.
Следующий в списке был Backtrader — с ним и продолжил работать.
Идея моей торговой стратегии 💡
Хотя считается что торговая стратегия необязательно должна быть «человекочитаемой» — это вполне может быть результат обучения алгоритма, основанного на интеллектуальных технологиях (нейросети, машинное обучение и т.п.), но я решил начать с простого.
Мои условия:
-
Торговать только в лонг (длинная позиция) — покупать акции с целью их последующей продажи по более высокой цене.
-
Торговать только 15 лучших акций по объему на Московской бирже.
-
Использовать два разных таймфрейма для тестов — это временные интервалы на которых отображается движение цен на графике финансового инструмента.
Планирую использовать 5 минут и час. Это из-за того что моё АПИ медленное.
Моя торговая стратегия основана на пересечении скользящих средних двух разных таймфреймов со скользящим стоп-лоссом для продажи.
Условие покупки представляет собой комбинацию двух пересечений скользящих средних:
-
Краткосрочное подтверждение: цена закрытия на пятиминутном интервале выше пятиминутной скользящей средней.
-
Долгосрочное подтверждение: цена закрытия на часовом интервале выше часовой скользящей средней.
Требуя выполнения обоих этих условий, гарантирую что акция будет иметь бычий импульс как на коротких, так и на длинных таймфреймах перед входом в позицию. Такое выравнивание двух таймфреймов помогает избегать покупок во время временного шума или незначительных колебаний на более коротком таймфрейме, отфильтровывая менее стабильные движения.
Условие продажи: трейлинг стоп, который предназначен для защиты прибыли и ограничения риска падения. Как работает лучше всего показано на картинке:
Бэктестинг моей торговой стратегии с помощью библиотеки backtrader на Python
Моя, описанная выше стратегия для двух таймфреймов на нескольких бумагах, выглядит в библиотеке backtrader на Python следующим образом:
strategy0_ma_5min_hourly.py:
import sys sys.stdout.reconfigure(encoding='utf-8') import backtrader as bt # Стратегия скользящие средние на двух разных временных интервалах class MovingAveragesOnDifferentTimeIntervalsStrategy(bt.Strategy): params = ( ('ma_period_5min', 30), # Период для скользящей средней на 5-минутках ('ma_period_hourly', 45), # Период для скользящей средней на часовом интервале ('trailing_stop', 0.03) # Процент для трейлинг-стопа ) # https://habr.com/ru/articles/857402/ def __init__(self): print(f"\nРасчет для параметров: {self.params.ma_period_5min} / {self.params.ma_period_hourly} / {self.params.trailing_stop}") # Создаем списки для хранения индикаторов по каждому инструменту self.ma_5min = {} self.ma_hourly = {} # Для каждого инструмента добавляем скользящие средние по разным интервалам for i, data in enumerate(self.datas): if i % 2 == 0: # Четные индексы - 5-минутные данные ticker = data._name.replace('_5min', '') self.ma_5min[ticker] = bt.indicators.SimpleMovingAverage(data.close, period=self.params.ma_period_5min) else: # Нечетные индексы - часовые данные ticker = data._name.replace('_hourly', '') self.ma_hourly[ticker] = bt.indicators.SimpleMovingAverage(data.close, period=self.params.ma_period_hourly) # Переменные для отслеживания максимальной цены после покупки по каждому инструменту self.buy_price = {} self.max_price = {} self.order = {} # Словарь для отслеживания ордеров по каждому инструменту def next(self): # Для каждого инструмента проверяем условия покупки и продажи for i in range(0, len(self.datas), 2): # Проходим по 5-минутным данным ticker = self.datas[i]._name.replace('_5min', '') data_5min = self.datas[i] data_hourly = self.datas[i + 1] # Проверяем, есть ли открытый ордер для этого инструмента if ticker in self.order and self.order[ticker]: continue # Пропускаем, если есть открытый ордер # Проверяем условия покупки: # цена на 5 мин таймфрейме выше скользящей средней на 5 мин + часовая цена тоже выше часовой скользящей средней if not self.getposition(data_5min): # Открываем сделку только если нет открытой позиции if data_5min.close[0] > self.ma_5min[ticker][0] and data_hourly.close[0] > self.ma_hourly[ticker][0]: self.order[ticker] = self.buy(data=data_5min) self.buy_price[ticker] = data_5min.close[0] self.max_price[ticker] = self.buy_price[ticker] # Получаем текущий тикер и дату покупки buy_date = data_5min.datetime.date(0) buy_time = data_5min.datetime.time(0) print(f"{buy_date} в {buy_time}: покупка за {self.buy_price[ticker]} для {ticker}") # Если уже есть открытая позиция elif self.getposition(data_5min): current_price = data_5min.close[0] # Обновляем максимальную цену, если текущая выше if current_price > self.max_price[ticker]: self.max_price[ticker] = current_price # Рассчитываем уровень стоп-лосса stop_loss_level = self.max_price[ticker] * (1 - self.params.trailing_stop) # Проверяем условие для продажи по трейлинг-стопу if current_price < stop_loss_level: self.order[ticker] = self.sell(data=data_5min) sell_date = data_5min.datetime.date(0) sell_time = data_5min.datetime.time(0) print(f"{sell_date} в {sell_time}: продажа за {current_price} для {ticker}") # Обрабатываем уведомления по ордерам def notify_order(self, order): ticker = order.data._name.replace('_5min', '') if order.status in [order.Completed, order.Canceled, order.Margin]: self.order[ticker] = None # Очищаем ордер после завершения
Сделал переключатель одиночный тест или оптимизация: singleTest / optimization
для основного файла запуска: SingleTestOrOptimization = "optimization"
Основной файл запуска main.py:
import sys import time sys.stdout.reconfigure(encoding='utf-8') from datetime import datetime from src.data_loader import load_data_for_ticker, load_ticker_mapping import pandas as pd import backtrader as bt import backtrader.analyzers as btanalyzers # https://habr.com/ru/articles/857402/ from src.strategy0_ma_5min_hourly import MovingAveragesOnDifferentTimeIntervalsStrategy # отобразить имена всех столбцов в большом фреймворке данных pandas pd.set_option('display.max_columns', None) pd.set_option('display.width', 1000) # Начало времени start_time = time.perf_counter() # Путь к JSON файлу с сопоставлениями mapping_file = "./data/+mappings.json" # Загрузка сопоставлений тикеров ticker_mapping = load_ticker_mapping(mapping_file) # Промежуточное время выполнения total_end_time = time.perf_counter() elapsed_time = total_end_time - start_time print(f"Промежуточное время выполнения: {elapsed_time:.4f} секунд.") current_time = datetime.now().strftime("%Y-%m-%d %H-%M") # Генерируем текущее время в формате 'yyyy-mm-dd HH-mm' # Следующая часть кода запускается только если это основной модуль if __name__ == '__main__': # Исправление для работы с multiprocessing # Создаем объект Cerebro cerebro = bt.Cerebro(optreturn=False) # Получаем количество бумаг в ticker_mapping.items() num_securities = len(ticker_mapping.items()) # Рассчитываем процент капитала на одну бумагу percent_per_security = 100 / num_securities print(f"Процент капитала на одну бумагу: {percent_per_security:.2f}%") # Условия капитала cerebro.broker.set_cash(100000) # Устанавливаем стартовый капитал cerebro.broker.setcommission(commission=0.005) # Комиссия 0.5% cerebro.addsizer(bt.sizers.PercentSizer, percents=percent_per_security) # Настраиваем размер позиций как процент от капитала # Для каждого инструмента добавляем оба временных интервала for uid, ticker in ticker_mapping.items(): print(f"Загружаем данные для {ticker}") # Загрузка данных с таймфреймами 5 минут и час data_5min, data_hourly = load_data_for_ticker(ticker) # Пропуск, если данные не были загружены if data_5min is None or data_hourly is None: continue # Добавляем 5-минутные данные в Cerebro data_5min_bt = bt.feeds.PandasData(dataname=data_5min, timeframe=bt.TimeFrame.Minutes, compression=5) cerebro.adddata(data_5min_bt, name=f"{ticker}_5min") # Добавляем часовые данные в Cerebro data_hourly_bt = bt.feeds.PandasData(dataname=data_hourly, timeframe=bt.TimeFrame.Minutes, compression=60) # Совмещаем графики 5 минут и часа на одном виде data_hourly_bt.plotinfo.plotmaster = data_5min_bt # Связываем графики data_hourly_bt.plotinfo.sameaxis = True # Отображаем на той же оси cerebro.adddata(data_hourly_bt, name=f"{ticker}_hourly") # Переключатель одиночный тест или оптимизация SingleTestOrOptimization = "optimization" # singleTest / optimization if SingleTestOrOptimization == "singleTest": print(f"{current_time} Проводим одиночный тест стратегии.") # Добавляем стратегию для одичного теста MovingAveragesOnDifferentTimeIntervalsStrategy cerebro.addstrategy(MovingAveragesOnDifferentTimeIntervalsStrategy, ma_period_5min = 30, # Период для скользящей средней на 5-минутках ma_period_hourly = 45, # Период для скользящей средней на часовом интервале trailing_stop = 0.03) # Процент для трейлинг-стопа # Writer только для одиночного теста для вывода результатов в CSV-файл cerebro.addwriter(bt.WriterFile, csv=True, out=f"./results/{current_time}_log.csv") # Добавляем анализаторы cerebro.addanalyzer(btanalyzers.TradeAnalyzer, _name='trade_analyzer') cerebro.addanalyzer(btanalyzers.DrawDown, _name="drawdown") cerebro.addanalyzer(btanalyzers.Returns, _name="returns") cerebro.addanalyzer(btanalyzers.SharpeRatio, _name='sharpe_ratio', timeframe=bt.TimeFrame.Days, riskfreerate=10) cerebro.addanalyzer(btanalyzers.SQN, _name='sqn') cerebro.addanalyzer(btanalyzers.PyFolio, _name='PyFolio') # Запуск тестирования results = cerebro.run(maxcpus=1) # Ограничение одним ядром для избежания многопроцессорности # Выводим результаты анализа одиночного теста print(f"\nОкончательная стоимость портфеля: {cerebro.broker.getvalue()}") returnsAnalyzer = results[0].analyzers.returns.get_analysis() print(f"Годовая/нормализованная доходность: {returnsAnalyzer['rnorm100']}%") drawdownAnalyzer = results[0].analyzers.drawdown.get_analysis() print(f"Максимальное значение просадки: {drawdownAnalyzer['max']['drawdown']}%") trade_analyzer = results[0].analyzers.trade_analyzer.get_analysis() print(f"Всего сделок: {trade_analyzer.total.closed} шт.") print(f"Выигрышные сделки: {trade_analyzer.won.total} шт.") print(f"Убыточные сделки: {trade_analyzer.lost.total} шт.") sharpe_ratio = results[0].analyzers.sharpe_ratio.get_analysis().get('sharperatio') print(f"Коэффициент Шарпа: {sharpe_ratio}") sqnAnalyzer = results[0].analyzers.sqn.get_analysis().get('sqn') print(f"Мера доходности с поправкой на риск: {sqnAnalyzer}") # Время выполнения total_end_time = time.perf_counter() elapsed_time = (total_end_time - start_time) / 60 print(f"\nВремя выполнения: {elapsed_time:.4f} минут.") # Построение графика для одиночного теста cerebro.plot() else: print(f"{current_time} Проводим оптимизацию статегии.") # Оптимизация стратегии start_date = 2024-10_MovingAveragesOnDifferentTimeIntervalsStrategy cerebro.optstrategy(MovingAveragesOnDifferentTimeIntervalsStrategy, ma_period_5min=range(10, 61, 5), # Диапазон для 5-минутной скользящей средней ma_period_hourly=range(15, 61, 2), # Диапазон для часовой скользящей средней trailing_stop=[0.03]) # Разные проценты для трейлинг-стопа 0.03, 0.05, 0.07 print(f"\nКоличество варинатов оптимизации: {(( (61-10)/10 * (61-15))/2 )}\n") # Добавляем анализаторы cerebro.addanalyzer(btanalyzers.TradeAnalyzer, _name='trade_analyzer') cerebro.addanalyzer(btanalyzers.DrawDown, _name="drawdown") cerebro.addanalyzer(btanalyzers.Returns, _name="returns") cerebro.addanalyzer(btanalyzers.SharpeRatio, _name='sharpe_ratio', timeframe=bt.TimeFrame.Days, riskfreerate=10) cerebro.addanalyzer(btanalyzers.SQN, _name='sqn') # Запуск тестирования results = cerebro.run(maxcpus=1) # Ограничение одним ядром для избежания многопроцессорности для оптимизации # Выводим результаты оптимизации par_list = [ [ # MovingAveragesOnDifferentTimeIntervalsStrategy x[0].params.ma_period_5min, x[0].params.ma_period_hourly, x[0].params.trailing_stop, x[0].analyzers.trade_analyzer.get_analysis().pnl.net.total, x[0].analyzers.returns.get_analysis()['rnorm100'], x[0].analyzers.drawdown.get_analysis()['max']['drawdown'], x[0].analyzers.trade_analyzer.get_analysis().total.closed, x[0].analyzers.trade_analyzer.get_analysis().won.total, x[0].analyzers.trade_analyzer.get_analysis().lost.total, x[0].analyzers.sharpe_ratio.get_analysis()['sharperatio'], x[0].analyzers.sqn.get_analysis().get('sqn') ] for x in results] # MovingAveragesOnDifferentTimeIntervalsStrategy par_df = pd.DataFrame(par_list, columns = ['ma_period_5min', 'ma_period_hourly', 'trailing_stop', 'pnl net', 'return', 'drawdown', 'total closed', 'won total', 'lost total', 'sharpe', 'sqn']) # Формируем имя файла с текущей датой и временем filename = f"./results/{current_time}_optimization.csv" # Сохраняем DataFrame в CSV файл с динамическим именем par_df.to_csv(filename, index=False) print(f"\n\nРезультаты оптимизации:\n{par_df}") # Время выполнения total_end_time = time.perf_counter() elapsed_time = (total_end_time - start_time) / 60 print(f"\nВремя выполнения: {elapsed_time:.4f} минут.") # Общее время выполнения total_end_time = time.perf_counter() elapsed_time = (total_end_time - start_time) / 60 print(f"\nОбщее время выполнения: {elapsed_time:.4f} минут.")
В данные загрузил котировки за октябрь 2024:
-
AFLT_1hour.csv
-
AFLT_5min.csv
-
EUTR_1hour.csv
-
EUTR_5min.csv
-
GAZP_1hour.csv
-
GAZP_5min.csv
-
MTLR_1hour.csv
-
MTLR_5min.csv
-
RNFT_1hour.csv
-
RNFT_5min.csv
-
ROSN_1hour.csv
-
ROSN_5min.csv
-
RUAL_1hour.csv
-
RUAL_5min.csv
-
SBER_1hour.csv
-
SBER_5min.csv
-
SGZH_1hour.csv
-
SGZH_5min.csv
-
SNGSP_1hour.csv
-
SNGSP_5min.csv
-
UWGN_1hour.csv
-
UWGN_5min.csv
-
VKCO_1hour.csv
-
VKCO_5min.csv
-
VTBR_1hour.csv
-
VTBR_5min.csv
Время выполнения оптимизации для таких параметров составило 74 минуты:
# Оптимизация стратегии start_date = 2024-10_MovingAveragesOnDifferentTimeIntervalsStrategy cerebro.optstrategy(MovingAveragesOnDifferentTimeIntervalsStrategy, ma_period_5min=range(10, 61, 5), # Диапазон для 5-минутной скользящей средней ma_period_hourly=range(15, 61, 2), # Диапазон для часовой скользящей средней trailing_stop=[0.03]) # Разные проценты для трейлинг-стопа 0.03, 0.05, 0.07
Для того чтобы визуально представить результаты оптимизации написал модуль, который строит трехмерный график.
Модуль 3dchart.py:
import pandas as pd import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import numpy as np from matplotlib.widgets import Slider from datetime import datetime # https://habr.com/ru/articles/857402/ # Чтение данных из CSV файла data = pd.read_csv('./results/2024-11-12 16-12_optimization_2024-10_MovingAveragesOnDifferentTimeIntervalsStrategy.csv') parameter1 = 'ma_period_5min' parameter2 = 'ma_period_hourly' # Извлечение необходимых колонок для построения графика x = data[parameter1] # по оси X y = data[parameter2] # по оси Y z = data['pnl net'] # по оси Z (PNL net) # Создание 3D-графика fig = plt.figure() ax = fig.add_subplot(111, projection='3d') # Построение поверхности с использованием триангуляции surf = ax.plot_trisurf(x, y, z, cmap='viridis', edgecolor='none') # Подписи к осям ax.set_xlabel(parameter1) ax.set_ylabel(parameter2) ax.set_zlabel('PNL Net') # Заголовок графика current_time = datetime.now().strftime("%Y-%m-%d %H:%M") # Генерируем текущее время в формате 'yyyy-mm-dd HH:mm' ax.set_title(f"3D Optimization Chart, {current_time}") # Добавление плоскости, которая будет двигаться вдоль оси Z # Начальное значение плоскости по оси Z z_plane = np.mean(z) # Плоскость - запоминаем ее как отдельный объект x_plane = np.array([[min(x), max(x)], [min(x), max(x)]]) y_plane = np.array([[min(y), min(y)], [max(y), max(y)]]) z_plane_values = np.array([[z_plane, z_plane], [z_plane, z_plane]]) # Отображение плоскости plane = ax.plot_surface(x_plane, y_plane, z_plane_values, color='red', alpha=0.5) # Создание слайдера для управления позицией плоскости по оси Z ax_slider = plt.axes([0.25, 0.02, 0.50, 0.03], facecolor='lightgoldenrodyellow') z_slider = Slider(ax_slider, 'Z Plane', min(z), max(z), valinit=z_plane) # Функция обновления положения плоскости при перемещении слайдера def update(val): new_z_plane = z_slider.val z_plane_values[:] = new_z_plane # Обновляем значения Z для плоскости ax.collections[-1].remove() # Удаляем старую плоскость ax.plot_surface(x_plane, y_plane, z_plane_values, color='red', alpha=0.5) # Рисуем новую плоскость fig.canvas.draw_idle() # Обновляем график # Привязка слайдера к функции обновления z_slider.on_changed(update) # Отображение графика plt.show()
Результат оптимизации в виде графика:
Выводы из этой оптимизации
Цифры по шкале Z показывают лишь степень убытков в рублях. Они со знаком минус.
Вы можете сами полностью повторить мой опыт потому что код загружен на GitHub:
https://github.com/empenoso/SilverFir-TradingBot_backtesting
Тем не менее:
-
Некоторые стратегии эффективны только в определенных рыночных условиях. Например, стратегии следования за трендом, как правило, хорошо работают на трендовых рынках, но не работают на боковых рынках.
-
Курвефитинг, подгонка под историю. Не хочу вводить много параметров, чтобы этого избежать. Переобучение прошлыми данными: если стратегия хорошо работает на исторических данных, но плохо на будущих данных в режиме скользящего окна, она может быть слишком адаптирована к историческим моделям, которые не будут повторяться.
-
Транзакционные затраты: хорошо, если тестирование учитывает реалистичное проскальзывание, комиссии и спреды.
Будущие шаги — где искать прибыльные торговые стратегии📝
Я хочу использовать подход скользящего окна — когда данные разбиваются на более мелкие последовательные периоды например по месяцам, за которым следует период тестирования вне этой выборки. Например, оптимизация идёт на месячных данных, а тестировать уже на следующем месяце. То есть происходит сдвиг вперед: после каждого периода тестирования окно «скользит» вперед на указанный интервал, и процесс повторяется. Таким образом, каждый сегмент данных используется как для обучения, так и для тестирования с течением времени, но никогда одновременно. Это помогает проверить, что стратегия работает стабильно в меняющихся рыночных условиях.
Также планирую использовать Technical Analysis of STOCKS & COMMODITIES для поиска новых идей. Их советы трейдерам доступны в открытом доступе.
А ещё планирую использовать ChatGPT, отправляя запросы вроде:
Действуй как опытный издатель. Отобрази 10 ведущих авторов в области алгоритмической торговли на рынке Америки. Для каждого автора перечисли три самые популярные книги, включая сведения о книге (дату публикации, издателя и ISBN), и предоставь русские переводы для каждого названия книги.
и дальше после ответа:
Действуй как опытный пользователь библиотеки backtrader на Python.
Хочу использовать торговую стратегию из книги Yves Hilpisch «Python for Finance: Mastering Data-Driven Finance» для тестов.Добавляй все комментарии на русском языке, продолжай со мной общение на английском.
И дальше подобные промты.
Итоги
Несмотря на то, что первоначальный выбор стратегии на двух разных таймфреймах и сразу для 15 активов был не самый удачный — впереди ещё очень большое поле исследований и тестов.
Автор: Михаил Шардин
18 ноября 2024 г.
ссылка на оригинал статьи https://habr.com/ru/articles/857402/
Добавить комментарий