Простой event-driven бэктестер, или как быстро потерять деньги на бинарных опционах » Элитный трейдер
Элитный трейдер


Простой event-driven бэктестер, или как быстро потерять деньги на бинарных опционах

27 августа 2018 Румянцев Александр
В этот раз сделаем простой бэктестер. Начнём с бинарных опционов, так как у них примитивный принцип работы. Мы делаем ставку, а она на следующей свече выиграет или проиграет.

Также посмотрим на работу стратегии с Мартингейлом и опасность, которую она несёт. Часто, есть периоды, когда подобные стратегии рисуют красивый график с прибылью. Но заканчиваются чудеса молниеносно быстро, несколькими ставками в максимальный убыток.

Для проверки, проведём тесты на минутном таймфрейме за июль 2018 года на паре EUR/USD. Поможет нам в этом Jupyter и Python 3.6.

Событийно-ориентированный бэктестер

Это относительно медленный бэктестер, который позволяет тестировать историю цен исключая заглядывание в будущее. Для его работы нам требуется:
Класс (Account), отвечающий за баланс и обработку ставки.
Функция (стратегия), вызываемая на каждой свече, работающая с прошлой историей.
Pandas датафрейм с OHLC историей цен и цикл перебора тиков.

Account

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

class Account(object):
    def __init__(self, start=100000, bet_profit=0.82, max_bet=100000):
        self._bet = None
        self.start = start
        self._max_bet = max_bet
        self.equity = []
        self.loss = []
        self.bet_profit = 0.82
        
    def reset_bet(self):
        """
        Сбрасываем накопленный убыток и ставку
        """
        self.loss = []
        self.bet()
        
    def bet(self, direction=None, amount=None, ts=None):
        """
        Приём ставки
        
        return:
            Факт приёма ставки и статус
        """
        if self.start + np.sum([r['amount'] for r in self.equity]) < 0:
            return False, 'Not enough money'
        elif amount is not None and self._max_bet < amount:
            return False, 'Too high bet'
        self._bet = {'direction': direction, 'amount': amount, 'ts': ts}
        
        return True, 'Success'
    
    def tick(self, ticks):
        """
        Получение последнего тика цены
        
        return:
            Результат последней ставки и последовательно накопленный убыток
        """
        result = 0
        if self._bet is not None and self._bet['direction'] is not None:
            if self._bet['direction'] > 0 and ticks.close[-1] > ticks.close[-2]:
                result = self._bet['amount'] * self.bet_profit
                self.loss = []
            elif self._bet['direction'] < 0 and ticks.close[-1] < ticks.close[-2]:
                result = self._bet['amount'] * self.bet_profit
                self.loss = []
            else:
                result = -self._bet['amount']
                self.loss.append(result)
            self.bet() # reset bet
        
        self.equity.append({'ts': ticks.index[-1], 'amount': result})
        
        return result, np.sum(self.loss)


Стратегии

Эти функции мы будем использовать в цикле, чтобы через одну из них прошли все имеющиеся ценовые бары. Дополнительно, сопроводим каждый бар объектом account и историей цен без будущего. Тест останавливается при потере капитала.

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

def simple(tick, data=None, account=None, skip_dogi=False, martingale=False, reverse=False, worktime=False):
    """
    Простая стратегия отправки ставок по состоянию текущей свечи.
    Фильтрует дожи, применяем мартингейл к проигрышу и увеличивает ставку в случае выигрыша.
    
    param reverse:
        Разворачивает направление ставки на противоположное.
    param worktime:
        Фильтрует дни и часы для отправки ставок.
    """
    if account is None:
        print('Account is not available')
        return 0
    
    # send last ticks to account
    result, loss = account.tick(data[-5:].copy())
    
    if worktime and (tick.ts.weekday() in SKIP_DAYS or tick.ts.hour not in TRADE_HOURS):
        return result
    
    amount = 1000    
    if martingale:
        if loss < 0:
            amount += abs(loss) / account.bet_profit
    elif result > 0:
        amount += result / 2
         
    candle_growth = tick.open < tick.close    
    if reverse:
        candle_growth = not candle_growth
        
    bet_state = None
    if skip_dogi and tick.dogi <= 0.00005:
        pass
    elif not candle_growth:
        # sell on red
        bet_state, msg = account.bet(-1, amount=amount, ts=tick.ts)         
        pass
    elif candle_growth:
        # buy on green
        bet_state, msg = account.bet(1, amount=amount, ts=tick.ts)
        pass
    
    if bet_state is False:
        account.reset_bet()
        if msg == 'Not enough money':
            raise Exception(msg)
    
    return result


Стратегия, фильтрующая тренд:

def ta_sma(tick, data=None, account=None, skip_dogi=False, martingale=False, reverse=False, worktime=False):
    """
    Стратегия отправки ставок по состоянию текущей свечи, когда короткая скользящая средняя 
    находится над длинной скользящей средней. Надежда на тренд.
    Фильтрует дожи, применяем мартингейл к проигрышу и увеличивает ставку в случае выигрыша.
    
    param reverse:
        Разворачивает направление ставки на противоположное.
    param worktime:
        Фильтрует дни и часы для отправки ставок.
    """
    if account is None:
        print('Account is not available')
        return 0
    
    # send last ticks to account
    result, loss = account.tick(data[-5:].copy())
    
    if worktime and (tick.ts.weekday() in SKIP_DAYS or tick.ts.hour not in TRADE_HOURS):
        return result
    
    amount = 1000    
    if martingale:
        if loss < 0:
            amount += abs(loss) / account.bet_profit
    elif result > 0:
        amount += result / 2
        
    ma_short = talib.SMA(data[-210:].close.values, timeperiod=15)[-1]
    ma_long = talib.SMA(data[-210:].close.values, timeperiod=50)[-1]
    
    candle_growth = tick.open < tick.close    
    if reverse:
        candle_growth = not candle_growth
        
    bet_state = None
    if skip_dogi and tick.dogi <= 0.00005:
        pass
    elif ma_short < ma_long and tick.open > tick.close:
        # sell on red
        bet_state, msg = account.bet(-1, amount=amount, ts=tick.ts)         
        pass
    elif ma_short > ma_long and tick.open < tick.close:
        # buy on green
        bet_state, msg = account.bet(1, amount=amount, ts=tick.ts)
        pass
    
    if bet_state is False:
        account.reset_bet()
        if msg == 'Not enough money':
            raise Exception(msg)
    
    return result


Стратегия возврата к среднему:

def ta_rsi_reversal_high(tick, data=None, account=None, skip_dogi=False, martingale=False, reverse=False, worktime=False):
    """
    Стратегия отправки ставок по состоянию текущей свечи, когда появляется короткая 
    перепроданность/перекупленность рядом с максимумом/минимумом.
    Фильтрует дожи, применяет мартингейл к проигрышу и увеличивает ставку в случае выигрыша.
    
    param reverse:
        Разворачивает направление ставки на противоположное.
    param worktime:
        Фильтрует дни и часы для отправки ставок.
    """
    if account is None:
        print('Account is not available')
        return 0
    
    # send last ticks to account
    result, loss = account.tick(data[-5:].copy())
    
    if worktime and (tick.ts.weekday() in SKIP_DAYS or tick.ts.hour not in TRADE_HOURS):
        return result
    
    amount = 1000    
    if martingale:
        if loss < 0:
            amount += abs(loss) / account.bet_profit
    elif result > 0:
        amount += result / 2
    
    rsi = talib.RSI(data[-15:].close.values, timeperiod=3)[-1]
    high = data[-200:].close.max()
    low = data[-200:].close.min() 
    
    candle_growth = tick.open < tick.close    
    if reverse:
        candle_growth = not candle_growth
        
    bet_state = None
    if skip_dogi and tick.dogi <= 0.00005:
        pass
    elif rsi > 65 and abs(low / data.close[-1] - 1) < 0.15 and not candle_growth:
        # sell on red
        bet_state, msg = account.bet(-1, amount=amount, ts=tick.ts)         
        pass
    elif rsi < 35 and abs(low / data.close[-1] - 1) < 0.15 and candle_growth:
        # buy on green
        bet_state, msg = account.bet(1, amount=amount, ts=tick.ts)
        pass
    
    if bet_state is False:
        account.reset_bet()
        if msg == 'Not enough money':
            raise Exception(msg)
    
    return result


Цикл перебора истории

Осталось подготовить данные и запустить. Используем цикл, так как скорость его работы равна скорости использования метода pd.DataFrame().apply(). И, в отличии от метода, нам будут видны возникающие ошибки. Дополнительно добавим остановку при получении ошибки.

df = pd.read_csv('DAT_MT_EURUSD_M1_201807.csv', names=['date', 'time', 'open', 'high', 'low', 'close', 'volume'])
# ...
df = df[['ts', 'open', 'high', 'low', 'close']].set_index('ts', drop=False).sort_index()
df['dogi'] = (df.close / df.open - 1).abs()
 
account = Account()
for row in df.iterrows():
    fltr = df.index <= row[0]
    try:
        simple(row[1], data=df[fltr], account=account, skip_dogi=True, reverse=False, worktime=True)
    except Exception as ex:
        print(ex)
        break
results = pd.Series([r['amount'] for r in account.equity], index=df.index[:len(account.equity)])


Условия тестирования

Брокер бинарных опционов всегда накладывает дополнительные ограничения на игроков. Мы учтём только часть.
Начальный капитал 100К руб.
Минимальная ставка 1000 руб.
Коэффициент прибыли 0.82%.
Пара EUR/USD.
Принимаем решение каждую минуту.
Дожи фильтруем при разнице менее 0,005%.
Когда используем Мартингейл, то должны покрыть убыток и получить прибыль первой ставки.
Максимальная ставка при Мартингейле — 20К руб. Если больше, то обнуляем накопленный убыток и начинаем с минимальной ставки.
Ограничим время ставок. Будем играть в рабочие дни с понедельника до пятницы в рабочие часы с 10 до 18 (8 часов).

Что мы не можем учесть:
Остановку приема ставок в любое время, если брокеру не выгодна ситуация на рынке.
Изменение коэффициента прибыли с течением времени.

Простые тесты

Историю цен можно найти в интернете бесплатно. Ниже на графиках минутные свечи за первую неделю месяца и распределение величины свечей для фильтрации дожи.

Простой event-driven бэктестер, или как быстро потерять деньги на бинарных опционах


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



Добавление мартингейла позволяет слить капитал к середине первого дня.



Удача была на стороне брокера. Попробуем усложнить себе жизнь.

Фильтр по минутному тренду

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



Как мы видим, с мартингейлом удалось протянуть два дня. Без него — четыре. Но тренд на средних нам не помог.

Возврат к среднему

В заключение рассмотрим ставки по стратегии возврата к среднему. Проверяем максимум и минимум за последние 200 минут. Для ставки на рост проверяем, чтобы цена была не далее 15% от максимума, а RSI(3) показывал перепроданность. И что мы видим? Мы продержались 22 торговые сессии и даже вышли в плюс.



Добавление мартингейла с ограничением максимальной ставки до 20К руб. приносит нам 80% прибыли.



В июле нам сопутствовала удача, но вот май и июнь стабильно несли убытки. В те месяцы работали другие стратегии.

Все наблюдения

Ниже таблица со всеми результатами:



Колонки:
balance — итоговый результат.
max drawdown, % — максимальная просадка в процентах.
max win — максимальный выигрыш.
max loss — максимальный проигрыш.
bets — количество ставок.
wins — количество выигрышных ставок.
loss — количество проигрышных ставок.
lifetime — количество часов жизни капитала.

Сокращения:
RSI, SMA, MACD — названия индикаторов.
Reversal — возврат к среднему.
Min/Max — торговля рядом с минимумами и максимумами.
Mart — мартингейл.
Reverse candle — разворот сигнала на противоположный.

Заключение

Тесты показали, что стабильность тренда на минутках предсказать очень тяжело. А вот развороты дались легче. Возврат к среднему рядом с максимумами/минимумами позволил даже заработать. Мартингейл даёт возможность быстро вернуться к прибыли, но на длинных дистанциях стабильно ведёт к полной потере счёта.

Бэктестер доработаем в следующих статьях и реализуем механизм торговли акциями.

(C) Источник
Не является индивидуальной инвестиционной рекомендацией
При копировании ссылка обязательна Нашли ошибку: выделить и нажать Ctrl+Enter