Читать книгу Алгоритмический трейдинг: Создание, тестирование и запуск роботов на рынке Форекс - Иван Алексеевич Евдокимов - Страница 4

Часть 2: Галерея стратегий: 10 роботов для Форекса

Оглавление

Робот 1: «Следователь за трендом» (Trend Follower) – Python версия для Backtesting


Вот полная реализация стратегии Trend Follower на Python с использованием библиотеки backtrader для бэктестинга и анализа:


```python

# trend_follower_backtest.py

import backtrader as bt

import backtrader.analyzers as btanalyzers

import pandas as pd

import yfinance as yf

from datetime import datetime, timedelta

import matplotlib.pyplot as plt


# =====================================================================

# 1. СОЗДАНИЕ СТРАТЕГИИ

# =====================================================================

class TrendFollowerStrategy(bt.Strategy):

"""

Стратегия следования за трендом

Правила:

1. Покупка: когда быстрая MA (50) пересекает медленную (200) снизу вверх И ADX > 25

2. Закрытие: при обратном пересечении MA или по стоп-лоссу/тейк-профиту

"""


params = (

('fast_ma', 50), # Период быстрой скользящей средней

('slow_ma', 200), # Период медленной скользящей средней

('adx_period', 14), # Период ADX

('adx_threshold', 25),# Пороговое значение ADX для силы тренда

('stop_loss', 0.02), # Стоп-лосс 2%

('take_profit', 0.04),# Тейк-профит 4%

('risk_per_trade', 0.01), # Риск на сделку 1% от капитала

('use_atr_filter', True), # Использовать фильтр ATR

('atr_period', 14), # Период ATR

('atr_multiplier', 2),# Множитель ATR для стоп-лосса

('printlog', False), # Вывод логов

)


def __init__(self):

"""Инициализация индикаторов"""

# Создаем индикаторы

self.fast_ma = bt.indicators.SimpleMovingAverage(

self.data.close, period=self.params.fast_ma

)

self.slow_ma = bt.indicators.SimpleMovingAverage(

self.data.close, period=self.params.slow_ma

)


# ADX индикатор (направленность тренда)

self.adx = bt.indicators.AverageDirectionalMovementIndex(

self.data, period=self.params.adx_period

)


# ATR для фильтра волатильности и расчета стоп-лосса

self.atr = bt.indicators.AverageTrueRange(

self.data, period=self.params.atr_period

)


# Переменные для отслеживания состояния

self.order = None

self.buyprice = None

self.buycomm = None


# Для логирования

self.trades = []

self.current_trade = {}


def log(self, txt, dt=None, doprint=False):

"""Метод для логирования"""

if self.params.printlog or doprint:

dt = dt or self.datas[0].datetime.date(0)

print(f'{dt.isoformat()} {txt}')


def notify_order(self, order):

"""Обработка изменения статуса ордера"""

if order.status in [order.Submitted, order.Accepted]:

# Ордер отправлен/принят – ничего не делаем

return


if order.status in [order.Completed]:

if order.isbuy():

self.log(

f'ПОКУПКА ИСПОЛНЕНА, Цена: {order.executed.price:.2f}, '

f'Стоимость: {order.executed.value:.2f}, '

f'Комиссия: {order.executed.comm:.2f}'

)

self.buyprice = order.executed.price

self.buycomm = order.executed.comm


# Устанавливаем стоп-лосс и тейк-профит

if self.params.use_atr_filter:

sl = self.buyprice – self.atr[0] * self.params.atr_multiplier

tp = self.buyprice + self.atr[0] * self.params.atr_multiplier * 2

else:

sl = self.buyprice * (1 – self.params.stop_loss)

tp = self.buyprice * (1 + self.params.take_profit)


self.sell(exectype=bt.Order.StopTrail, price=sl)

self.sell(exectype=bt.Order.Limit, price=tp)


# Сохраняем информацию о сделке

self.current_trade = {

'entry_date': self.datas[0].datetime.date(0),

'entry_price': order.executed.price,

'stop_loss': sl,

'take_profit': tp,

'size': order.executed.size

}


elif order.issell():

self.log(

f'ПРОДАЖА ИСПОЛНЕНА, Цена: {order.executed.price:.2f}, '

f'Стоимость: {order.executed.value:.2f}, '

f'Комиссия: {order.executed.comm:.2f}'

)


# Фиксируем завершение сделки

if self.current_trade:

self.current_trade['exit_date'] = self.datas[0].datetime.date(0)

self.current_trade['exit_price'] = order.executed.price

self.current_trade['pnl'] = (

(order.executed.price – self.current_trade['entry_price']) *

self.current_trade['size']

)

self.current_trade['pnl_percent'] = (

(order.executed.price / self.current_trade['entry_price'] – 1) * 100

)

self.trades.append(self.current_trade.copy())

self.current_trade = {}


elif order.status in [order.Canceled, order.Margin, order.Rejected]:

self.log(f'Ордер отменен/отклонен: {order.getstatusname()}')


self.order = None


def notify_trade(self, trade):

"""Обработка изменения статуса сделки"""

if not trade.isclosed:

return


self.log(f'СДЕЛКА ЗАКРЫТА, Прибыль: {trade.pnl:.2f}, Чистая прибыль: {trade.pnlcomm:.2f}')


def next(self):

"""Основная логика на каждом новом баре"""

# Пропускаем если ордер уже открыт

if self.order:

return


# Проверяем, есть ли открытая позиция

if not self.position:

# УСЛОВИЕ ДЛЯ ПОКУПКИ:

# 1. Быстрая MA пересекает медленную снизу вверх

# 2. ADX > порогового значения (сила тренда)

crossover_up = (self.fast_ma[-1] < self.slow_ma[-1] and

self.fast_ma[0] > self.slow_ma[0])


strong_trend = self.adx[0] > self.params.adx_threshold


if crossover_up and strong_trend:

# Рассчитываем размер позиции на основе риска

capital = self.broker.getvalue()

risk_amount = capital * self.params.risk_per_trade


if self.params.use_atr_filter:

stop_distance = self.atr[0] * self.params.atr_multiplier

else:

stop_distance = self.data.close[0] * self.params.stop_loss


if stop_distance > 0:

size = risk_amount / stop_distance

size = min(size, capital * 0.1 / self.data.close[0]) # Не более 10% капитала


self.log(f'СИГНАЛ НА ПОКУПКУ: цена={self.data.close[0]:.2f}, '

f'ADX={self.adx[0]:.2f}, размер={size:.2f}')

self.order = self.buy(size=size)


else:

# УСЛОВИЕ ДЛЯ ЗАКРЫТИЯ ПОЗИЦИИ:

# Быстрая MA пересекает медленную сверху вниз

crossover_down = (self.fast_ma[-1] > self.slow_ma[-1] and

self.fast_ma[0] < self.slow_ma[0])


if crossover_down:

self.log(f'СИГНАЛ НА ЗАКРЫТИЕ: цена={self.data.close[0]:.2f}')

self.order = self.close()


# =====================================================================

# 2. КЛАСС ДЛЯ ОПТИМИЗАЦИИ ПАРАМЕТРОВ

# =====================================================================

class TrendFollowerOptimizer(bt.Strategy):

"""Версия стратегии для оптимизации параметров"""


params = (

('fast_ma', 50),

('slow_ma', 200),

('adx_threshold', 25),

('risk_per_trade', 0.01),

)


def __init__(self):

self.fast_ma = bt.indicators.SMA(self.data.close, period=self.p.fast_ma)

self.slow_ma = bt.indicators.SMA(self.data.close, period=self.p.slow_ma)

self.adx = bt.indicators.ADX(self.data, period=14)

self.order = None


def next(self):

if self.order:

return


if not self.position:

if (self.fast_ma[-1] < self.slow_ma[-1] and

self.fast_ma[0] > self.slow_ma[0] and

self.adx[0] > self.p.adx_threshold):


size = self.broker.getvalue() * self.p.risk_per_trade / self.data.close[0]

self.order = self.buy(size=size)

else:

if self.fast_ma[-1] > self.slow_ma[-1] and self.fast_ma[0] < self.slow_ma[0]:

self.order = self.close()


# =====================================================================

# 3. ФУНКЦИИ ДЛЯ ТЕСТИРОВАНИЯ И АНАЛИЗА

# =====================================================================

def download_data(symbol='EURUSD=X', start_date='2020-01-01', end_date='2023-12-31'):

"""Загрузка данных с Yahoo Finance"""

print(f"Загрузка данных для {symbol}…")

data = yf.download(symbol, start=start_date, end=end_date, progress=False)


# Преобразуем индексы для backtrader

data.index = pd.to_datetime(data.index)

data = data[['Open', 'High', 'Low', 'Close', 'Volume']]


return data


def run_backtest(data, initial_cash=10000, commission=0.001, **strategy_params):

"""Запуск бэктеста"""

cerebro = bt.Cerebro()


# Добавляем данные

data_feed = bt.feeds.PandasData(dataname=data)

cerebro.adddata(data_feed)


# Добавляем стратегию

cerebro.addstrategy(TrendFollowerStrategy, **strategy_params)


# Настройки брокера

cerebro.broker.setcash(initial_cash)

cerebro.broker.setcommission(commission=commission)


# Добавляем анализаторы

cerebro.addanalyzer(btanalyzers.Returns, _name='returns')

cerebro.addanalyzer(btanalyzers.SharpeRatio, _name='sharpe', riskfreerate=0.0)

cerebro.addanalyzer(btanalyzers.DrawDown, _name='drawdown')

cerebro.addanalyzer(btanalyzers.TradeAnalyzer, _name='trades')

cerebro.addanalyzer(btanalyzers.SQN, _name='sqn')


print(f'Начальный капитал: {initial_cash:.2f}')

print(f'Комиссия: {commission*100:.2f}%')


# Запуск

results = cerebro.run()

strat = results[0]


# Вывод результатов

print('\n' + '='*50)

print('РЕЗУЛЬТАТЫ БЭКТЕСТА')

print('='*50)


final_value = cerebro.broker.getvalue()

total_return = (final_value / initial_cash – 1) * 100


print(f'Конечный капитал: {final_value:.2f}')

print(f'Общая доходность: {total_return:.2f}%')


# Анализ результатов

if hasattr(strat, 'analyzers'):

returns = strat.analyzers.returns.get_analysis()

sharpe = strat.analyzers.sharpe.get_analysis()

drawdown = strat.analyzers.drawdown.get_analysis()

trades = strat.analyzers.trades.get_analysis()

sqn = strat.analyzers.sqn.get_analysis()


print(f'\nАНАЛИТИКА:')

print(f'Sharpe Ratio: {sharpe.get("sharperatio", 0):.3f}')

print(f'Максимальная просадка: {drawdown.max.drawdown:.2f}%')

print(f'Продолжительность просадки: {drawdown.max.len} дней')


if trades.total.total:

print(f'\nСТАТИСТИКА СДЕЛОК:')

print(f'Всего сделок: {trades.total.total}')

print(f'Прибыльных: {trades.won.total} ({trades.won.total/trades.total.total*100:.1f}%)')

print(f'Убыточных: {trades.lost.total} ({trades.lost.total/trades.total.total*100:.1f}%)')

print(f'Profit Factor: {trades.won.pnl.total/abs(trades.lost.pnl.total):.2f}')

print(f'Средняя прибыль: {trades.won.pnl.average:.2f}')

print(f'Средний убыток: {trades.lost.pnl.average:.2f}')

print(f'SQN: {sqn.sqn:.2f}')


# Возвращаем детали сделок для дальнейшего анализа

trades_details = getattr(strat, 'trades', [])


return cerebro, strat, trades_details


def optimize_parameters(data, parameter_ranges):

"""Оптимизация параметров стратегии"""

cerebro = bt.Cerebro(optreturn=False)


# Добавляем данные

data_feed = bt.feeds.PandasData(dataname=data)

cerebro.adddata(data_feed)


# Добавляем стратегию с параметрами для оптимизации

cerebro.optstrategy(

TrendFollowerOptimizer,

fast_ma=parameter_ranges.get('fast_ma', range(10, 101, 10)),

slow_ma=parameter_ranges.get('slow_ma', range(50, 301, 50)),

adx_threshold=parameter_ranges.get('adx_threshold', range(20, 41, 5)),

risk_per_trade=parameter_ranges.get('risk_per_trade', [0.005, 0.01, 0.02])

)


# Настройки

cerebro.broker.setcash(10000)

cerebro.broker.setcommission(commission=0.001)


# Оптимизация

print("Запуск оптимизации параметров…")

opt_results = cerebro.run(maxcpus=1)


# Анализ результатов оптимизации

results = []

for run in opt_results:

for strat in run:

# Собираем статистику для каждого набора параметров

value = cerebro.broker.getvalue()

returns = (value / 10000 – 1) * 100


results.append({

'fast_ma': strat.params.fast_ma,

'slow_ma': strat.params.slow_ma,

'adx_threshold': strat.params.adx_threshold,

'risk_per_trade': strat.params.risk_per_trade,

'final_value': value,

'returns': returns

})


# Создаем DataFrame с результатами

results_df = pd.DataFrame(results)

results_df = results_df.sort_values('final_value', ascending=False)


return results_df


def plot_results(cerebro, save_path='trend_follower_results.png'):

"""Визуализация результатов"""

# График 1: Кривая баланса

fig = cerebro.plot(style='candlestick', iplot=False)[0][0]

fig.set_size_inches(14, 8)

fig.suptitle('Trend Follower Strategy – Equity Curve', fontsize=16)


# Сохраняем график

plt.tight_layout()

plt.savefig(save_path, dpi=100, bbox_inches='tight')

plt.show()


return fig


def generate_report(strategy_params, trades_details, filename='trend_follower_report.txt'):

"""Генерация детального отчета"""

with open(filename, 'w', encoding='utf-8') as f:

f.write("="*60 + "\n")

f.write("ОТЧЕТ ПО СТРАТЕГИИ TREND FOLLOWER\n")

f.write("="*60 + "\n\n")


f.write("ПАРАМЕТРЫ СТРАТЕГИИ:\n")

f.write("-"*40 + "\n")

for key, value in strategy_params.items():

f.write(f"{key}: {value}\n")


f.write("\n" + "="*60 + "\n\n")


if trades_details:

f.write("ДЕТАЛИ СДЕЛОК:\n")

f.write("-"*40 + "\n")


total_pnl = 0

winning_trades = 0


for i, trade in enumerate(trades_details, 1):

pnl = trade.get('pnl', 0)

pnl_percent = trade.get('pnl_percent', 0)


f.write(f"\nСделка #{i}:\n")

f.write(f" Дата входа: {trade.get('entry_date')}\n")

f.write(f" Цена входа: {trade.get('entry_price', 0):.4f}\n")

f.write(f" Дата выхода: {trade.get('exit_date')}\n")

f.write(f" Цена выхода: {trade.get('exit_price', 0):.4f}\n")

f.write(f" P&L: {pnl:.2f} ({pnl_percent:.2f}%)\n")


total_pnl += pnl

if pnl > 0:

winning_trades += 1


f.write("\n" + "="*60 + "\n")

f.write(f"ИТОГИ:\n")

f.write(f"Всего сделок: {len(trades_details)}\n")

f.write(f"Прибыльных сделок: {winning_trades} ({winning_trades/len(trades_details)*100:.1f}%)\n")

f.write(f"Общий P&L: {total_pnl:.2f}\n")


print(f"Отчет сохранен в файл: {filename}")


# =====================================================================

# 4. ОСНОВНОЙ СКРИПТ ДЛЯ ЗАПУСКА

# =====================================================================

def main():

"""Основная функция для запуска бэктеста"""

print("="*60)

print("TREND FOLLOWER STRATEGY BACKTEST")

print("="*60)


# 1. Загружаем данные

symbol = 'EURUSD=X' # EUR/USD

data = download_data(

symbol=symbol,

start_date='2020-01-01',

end_date='2023-12-31'

)


if data.empty:

print("Ошибка загрузки данных!")

Алгоритмический трейдинг: Создание, тестирование и запуск роботов на рынке Форекс

Подняться наверх