Пакет bt для тестирования торговых систем на языке Python

Проверялось в Python 2.7.

Установка пакета bt

Для установки пакета bt в системе Linux/Ubuntu достаточно выполнить в командной строке::

sudo pip install bt

Для системы Windows sudo опускаем.

Если лень открывать терминал с командной строкой, то можно выполнить установку прямо на странице IPython Notebook::

!pip install bt

Вместе с пакетом bt будет установлен также ffn.

In [ ]:
# Для установки пакета:
# !pip install bt
In [1]:
# Чтобы графики выводились внутри страницы:
%matplotlib inline

Загрузка котировок

В пакете bt имеется собственная функция для загрузки котировок с сайта Yahoo Finance. Для примера скачаем цены закрытия (adjusted close) двух биржевых фондов (ETF): GDX и SPY:

In [4]:
import bt
data = bt.get('GDX, SPY', start='2012-01-01')
print data.head(3) # Вывели первые 3 записи для проверки.
                  gdx         spy
Date                             
2012-01-03  51.964246  117.497250
2012-01-04  51.906295  117.681556
2012-01-05  52.060836  117.994879

Класс Strategy

Для тестирования торговой системы надо сначала создать экземпляр класса Strategy, в котором задать нужную комбинацию алгоритмов (algos). Алгоритмы указывают, как выбирать бумаги для покупки и продажи, и в каких пропорциях распределять имеющиеся средства между бумагами (т.е. способ вычисления весовых коэффициентов).

In [5]:
s1 = bt.Strategy('s1', [bt.algos.RunMonthly(),  # Выполняем ежемесячно;
                       bt.algos.SelectAll(),    # выбираем все бумаги;
                       bt.algos.WeighEqually(), # в равной пропорции;
                       bt.algos.Rebalance()])   # выполняем ребалансировку согласно выбранной пропорции.

Здесь мы указали, что будем проводить ежемесячную (RunMonthly) ребалансировку (Rebalance) портфеля, распределяя все имеющиеся средства поровну (WeighEqually) между всеми выбранными бумагами (SelectAll).

Усложним стратегию. Будем проводить еженедельную ребалансировку (RunWeekly), а весовые коэффициенты для каждой бумаги рассчитывать обратно пропорционально волатильности (WeighInvVol):

In [6]:
s2 = bt.Strategy('s2', [bt.algos.RunWeekly(),   # Еженедельно;
                        bt.algos.SelectAll(),   # выбираем все бумаги;
                        bt.algos.WeighInvVol(), # обратно пропорционально волатильности;
                        bt.algos.Rebalance()])  # выполняем ребалансировку согласно весовым коэффициентам.

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

In [7]:
import pandas as pd
sma1 = pd.rolling_mean(data, 250) # Простая скользящая средняя.
s3 = bt.Strategy('SMA1', [ 
         bt.algos.SelectWhere(data > sma1), # Выбираем только те бумаги, которые выше скользящей средней;
         bt.algos.WeighEqually(),           # делим средства в равной попорции.  
         bt.algos.Rebalance()] )

Результаты торговли по любой системе обычно сравнивают с каким-нибудь бенчмарком. Часто в качестве бенчмарка выбирают стратегию "купи и держи" (buy & hold):

In [8]:
s0 = bt.Strategy('bh', [bt.algos.RunOnce(),       # Только один раз;
                        bt.algos.SelectAll(),     # выбираем все бумаги;
                        bt.algos.WeighEqually(),  # в равной пропорции.
                        bt.algos.Rebalance()])

Итак, мы определили четыре торговые стратегии. Пора их протестировать.

Тестирование торговых стратегий

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

In [9]:
test1 = bt.Backtest(s1, data)
res1 = bt.run(test1)

Результат можно отобразить в виде графика эквити:

In [10]:
res1.plot()

Таблица результатов:

In [11]:
res1.display()
Stat                 s1
-------------------  ----------
Start                2012-01-03
End                  2015-12-24
Risk-free rate       0.00%

Total Return         -28.53%
Daily Sharpe         -0.27
CAGR                 -8.11%
Max Drawdown         -37.61%

MTD                  1.81%
3m                   4.26%
6m                   -10.86%
YTD                  -8.78%
1Y                   -7.82%
3Y (ann.)            -8.98%
5Y (ann.)            -8.11%
10Y (ann.)           -8.11%
Since Incep. (ann.)  -8.11%

Daily Sharpe         -0.27
Daily Mean (ann.)    -6.01%
Daily Vol (ann.)     22.09%
Daily Skew           -0.06
Daily Kurt           1.34
Best Day             4.80%
Worst Day            -6.40%

Monthly Sharpe       -0.37
Monthly Mean (ann.)  -6.86%
Monthly Vol (ann.)   18.49%
Monthly Skew         0.11
Monthly Kurt         -0.57
Best Month           9.87%
Worst Month          -10.79%

Yearly Sharpe        -0.85
Yearly Mean          -9.06%
Yearly Vol           10.69%
Yearly Skew          -0.12
Yearly Kurt          -
Best Year            1.48%
Worst Year           -19.90%

Avg. Drawdown        -8.51%
Avg. Drawdown Days   201.29
Avg. Up Month        4.41%
Avg. Down Month      -4.26%
Win Year %           33.33%
Win 12m %            24.32%

Гистограмма распределения прибылей/убытков (return):

In [12]:
res1.plot_histogram()

Весовые коэффициенты для каждой бумаги:

In [13]:
res1.plot_security_weights()

Можно запустить тест сразу для нескольких стратегий:

In [14]:
test0 = bt.Backtest(s0, data)
test1 = bt.Backtest(s1, data)
test2 = bt.Backtest(s2, data)
test3 = bt.Backtest(s3, data)
res = bt.run(test1, test2, test3, test0) # Запустили четыре теста для последующего сравнения.
res.plot()

Сравним результаты тестирования всех четырёх стратегий:

In [15]:
res.display()
Stat                 s1          s2          SMA1        bh
-------------------  ----------  ----------  ----------  ----------
Start                2012-01-03  2012-01-03  2012-01-03  2012-01-03
End                  2015-12-24  2015-12-24  2015-12-24  2015-12-24
Risk-free rate       0.00%       0.00%       0.00%       0.00%

Total Return         -28.53%     14.46%      26.69%      1.25%
Daily Sharpe         -0.27       0.30        0.63        0.10
CAGR                 -8.11%      3.46%       6.14%       0.31%
Max Drawdown         -37.61%     -18.92%     -12.25%     -21.40%

MTD                  1.81%       0.45%       -5.24%      -0.15%
3m                   4.26%       5.41%       -5.11%      6.40%
6m                   -10.86%     -7.65%      -10.83%     -4.57%
YTD                  -8.78%      -5.22%      -7.82%      -1.91%
1Y                   -7.82%      -5.43%      -8.81%      -2.24%
3Y (ann.)            -8.98%      2.20%       8.21%       0.69%
5Y (ann.)            -8.11%      3.46%       6.14%       0.31%
10Y (ann.)           -8.11%      3.46%       6.14%       0.31%
Since Incep. (ann.)  -8.11%      3.46%       6.14%       0.31%

Daily Sharpe         -0.27       0.30        0.63        0.10
Daily Mean (ann.)    -6.01%      4.52%       6.49%       1.53%
Daily Vol (ann.)     22.09%      14.97%      10.36%      15.62%
Daily Skew           -0.06       -0.22       -0.35       -0.23
Daily Kurt           1.34        1.83        2.54        1.76
Best Day             4.80%       3.26%       2.74%       3.61%
Worst Day            -6.40%      -5.06%      -3.02%      -4.81%

Monthly Sharpe       -0.37       0.27        0.65        0.01
Monthly Mean (ann.)  -6.86%      3.35%       6.55%       0.11%
Monthly Vol (ann.)   18.49%      12.48%      10.05%      12.69%
Monthly Skew         0.11        0.25        -0.46       0.37
Monthly Kurt         -0.57       -0.59       0.34        -0.40
Best Month           9.87%       8.71%       5.62%       8.56%
Worst Month          -10.79%     -5.99%      -7.16%      -5.97%

Yearly Sharpe        -0.85       0.31        0.46        0.05
Yearly Mean          -9.06%      1.98%       9.46%       0.35%
Yearly Vol           10.69%      6.28%       20.63%      6.84%
Yearly Skew          -0.12       -1.60       1.13        1.33
Yearly Kurt          -           -           -           -
Best Year            1.48%       6.39%       32.30%      8.03%
Worst Year           -19.90%     -5.22%      -7.82%      -5.06%

Avg. Drawdown        -8.51%      -2.32%      -1.71%      -6.13%
Avg. Drawdown Days   201.29      47.31       19.60       143.10
Avg. Up Month        4.41%       2.96%       2.91%       3.42%
Avg. Down Month      -4.26%      -2.77%      -1.54%      -2.52%
Win Year %           33.33%      66.67%      66.67%      33.33%
Win 12m %            24.32%      70.27%      83.78%      48.65%

Построение графиков

Отобразим на графике скользящую среднюю:

In [16]:
plot = bt.merge(data, sma1).plot(figsize=(17, 6)).legend(['GDX','SPY','GDX SMA', 'SPY SMA'], loc='upper left')

При построении графика можно указать имя какой-то одной стратегии:

In [17]:
res.plot_security_weights('SMA1')

Оптимизация

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

In [18]:
def above_sma(tickers, period=250, start='2010-01-01', name='above_sma'):
    """
    Покупаем бумагу, если она выше скользящей средней.
    """
    data = bt.get(tickers, start=start)
    sma1 = pd.rolling_mean(data, period)
    s = bt.Strategy(name, [bt.algos.SelectWhere(data > sma1),
                           bt.algos.WeighEqually(),
                           bt.algos.Rebalance()])
    return bt.Backtest(s, data)

Также определим отдельную функцию для бенчмарка -- стратегии "купи и держи" (buy & hold), с которой будем сравнивать результаты всех остальных стратегий:

In [19]:
def bh(tickers, start='2010-01-01', name='buy&hold'):
    s = bt.Strategy(name, [bt.algos.RunOnce(),       # Только один раз;
                           bt.algos.SelectAll(),     # выбираем все бумаги;
                           bt.algos.WeighEqually(),  # в равной пропорции.
                           bt.algos.Rebalance()])
    data = bt.get(tickers, start=start)  # Получаем котировки.
    return bt.Backtest(s, data)          # Запускаем тестирование и возвращаем результат.

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

In [20]:
# Бумаги для торговли:
tickers = 'aapl,msft,c,gs,ge'

# Тесты с разными значениями периода скользящей средней:
sma1 = above_sma(tickers, period=10, name='sma10')
sma2 = above_sma(tickers, period=20, name='sma20')
sma3 = above_sma(tickers, period=50, name='sma50')
sma4 = above_sma(tickers, period=100, name='sma100')
sma5 = above_sma(tickers, period=150, name='sma150')
sma6 = above_sma(tickers, period=200, name='sma200')

# Бенчмарк:
benchmark = bh('spy', name='bh_spy')

# Выполняем все тесты:
res = bt.run(sma1, sma2, sma3, sma4, sma5, sma6, benchmark)
In [21]:
res.plot()
In [22]:
res.display()
Stat                 sma10       sma20       sma50       sma100      sma150      sma200      bh_spy
-------------------  ----------  ----------  ----------  ----------  ----------  ----------  ----------
Start                2010-01-04  2010-01-04  2010-01-04  2010-01-04  2010-01-04  2010-01-04  2010-01-04
End                  2015-12-24  2015-12-24  2015-12-24  2015-12-24  2015-12-24  2015-12-24  2015-12-24
Risk-free rate       0.00%       0.00%       0.00%       0.00%       0.00%       0.00%       0.00%

Total Return         12.69%      39.90%      37.43%      149.84%     171.23%     158.56%     105.02%
Daily Sharpe         0.20        0.40        0.38        0.93        1.06        1.04        0.84
CAGR                 2.02%       5.79%       5.47%       16.58%      18.20%      17.25%      12.78%
Max Drawdown         -25.04%     -28.65%     -33.79%     -17.87%     -19.15%     -19.90%     -18.61%

MTD                  -7.24%      -3.45%      -0.31%      -0.15%      1.85%       1.83%       -0.86%
3m                   3.14%       8.28%       15.01%      12.06%      17.76%      20.10%      7.26%
6m                   -7.14%      1.49%       5.32%       -0.99%      4.81%       5.61%       -1.20%
YTD                  -11.45%     2.16%       13.94%      11.71%      17.04%      16.80%      2.13%
1Y                   -13.09%     -0.06%      11.78%      9.69%       14.89%      14.65%      1.04%
3Y (ann.)            2.28%       8.14%       12.30%      20.43%      25.59%      26.18%      15.34%
5Y (ann.)            0.98%       4.49%       7.87%       19.18%      19.01%      18.96%      12.67%
10Y (ann.)           2.02%       5.79%       5.47%       16.58%      18.20%      17.25%      12.78%
Since Incep. (ann.)  2.02%       5.79%       5.47%       16.58%      18.20%      17.25%      12.78%

Daily Sharpe         0.20        0.40        0.38        0.93        1.06        1.04        0.84
Daily Mean (ann.)    3.80%       7.31%       7.06%       17.01%      18.19%      17.30%      13.27%
Daily Vol (ann.)     18.94%      18.35%      18.60%      18.27%      17.21%      16.67%      15.80%
Daily Skew           -0.41       -0.51       -0.61       -0.18       -0.09       -0.10       -0.37
Daily Kurt           6.60        4.24        4.34        3.95        3.59        3.17        3.99
Best Day             9.54%       5.78%       5.78%       5.86%       5.89%       5.89%       4.65%
Worst Day            -8.01%      -8.01%      -8.01%      -6.39%      -6.39%      -5.46%      -6.51%

Monthly Sharpe       0.26        0.38        0.36        1.00        1.10        1.09        1.07
Monthly Mean (ann.)  5.49%       7.75%       7.76%       16.96%      18.32%      17.39%      13.95%
Monthly Vol (ann.)   21.19%      20.51%      21.72%      16.90%      16.58%      15.96%      13.02%
Monthly Skew         -0.22       0.02        -0.55       0.11        0.08        0.29        -0.14
Monthly Kurt         0.97        -0.06       0.48        -0.26       0.14        0.54        0.23
Best Month           18.23%      14.28%      13.54%      11.60%      14.94%      15.85%      10.91%
Worst Month          -17.40%     -14.76%     -16.33%     -9.44%      -8.78%      -8.40%      -7.95%

Yearly Sharpe        0.13        0.27        0.47        1.03        1.04        1.01        1.05
Yearly Mean          2.57%       6.94%       9.54%       20.58%      20.44%      20.48%      13.16%
Yearly Vol           19.77%      25.74%      20.48%      19.92%      19.65%      20.21%      12.48%
Yearly Skew          0.62        0.79        -0.22       0.15        -0.48       -0.67       0.92
Yearly Kurt          -1.97       -0.50       -2.09       -2.07       0.54        0.78        0.52
Best Year            29.21%      44.86%      33.27%      43.80%      44.27%      43.95%      32.31%
Worst Year           -16.93%     -17.10%     -15.70%     -3.65%      -8.26%      -9.84%      1.90%

Avg. Drawdown        -6.51%      -5.11%      -3.89%      -2.67%      -2.44%      -2.31%      -1.82%
Avg. Drawdown Days   110.26      64.31       72.46       22.76       18.57       19.15       17.77
Avg. Up Month        4.41%       4.83%       4.90%       4.44%       4.42%       4.40%       3.26%
Avg. Down Month      -4.94%      -4.46%      -4.84%      -3.23%      -2.92%      -2.58%      -2.94%
Win Year %           40.00%      60.00%      60.00%      80.00%      80.00%      80.00%      100.00%
Win 12m %            63.93%      57.38%      65.57%      93.44%      93.44%      90.16%      95.08%

Теперь мы можем выбрать период, при котором достигается наибольшее значение итоговой прибыли или, например, коэффициента Шарпа.

Пересечение двух скользящих средних

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

In [23]:
data = bt.get('spy', start='2010-01-01')
sma1 = pd.rolling_mean(data, 50)   # Быстрая скользящая средняя.
sma2 = pd.rolling_mean(data, 150)  # Медленная скользящая средняя.
tw = sma2.copy()             # Создали новую колонку копированием.
tw[sma1 > sma2] = 1.0        # Весовой коэффициент для покупки.
tw[sma1 <= sma2] = -1.0      # Весовой коэффициент для короткой продажи.
tw[sma2.isnull()] = 0.0  # Первые значения, когда скользящая средняя не определена.
In [24]:
tmp = bt.merge(tw, data, sma1, sma2)                # Соединили колонки.
tmp.columns = ['tw', 'price', 'sma1', 'sma2']       # Имена колонок.
ax = tmp.plot(figsize=(15,5), secondary_y=['tw'])   # График.

Значения из колонки весовых коэффициентов tw отображены на графике синей линией; соответствующая шкала для вертикальной оси расположена справа. Шкала для остальных значений (цены и скользящих средних) нанесена слева.

In [25]:
ma_cross = bt.Strategy('ma_cross', [bt.algos.WeighTarget(tw),
                                    bt.algos.Rebalance()])
t = bt.Backtest(ma_cross, data)
res = bt.run(t)
In [26]:
res.plot()
In [27]:
res.display()
Stat                 ma_cross
-------------------  ----------
Start                2010-01-04
End                  2015-12-24
Risk-free rate       0.00%

Total Return         46.32%
Daily Sharpe         0.51
CAGR                 6.59%
Max Drawdown         -15.33%

MTD                  0.72%
3m                   -7.25%
6m                   -10.05%
YTD                  -7.02%
1Y                   -8.02%
3Y (ann.)            11.78%
5Y (ann.)            7.70%
10Y (ann.)           6.59%
Since Incep. (ann.)  6.59%

Daily Sharpe         0.51
Daily Mean (ann.)    7.42%
Daily Vol (ann.)     14.49%
Daily Skew           0.03
Daily Kurt           5.10
Best Day             6.51%
Worst Day            -4.65%

Monthly Sharpe       0.61
Monthly Mean (ann.)  7.15%
Monthly Vol (ann.)   11.81%
Monthly Skew         -0.76
Monthly Kurt         1.26
Best Month           6.71%
Worst Month          -10.49%

Yearly Sharpe        0.55
Yearly Mean          8.52%
Yearly Vol           15.58%
Yearly Skew          0.90
Yearly Kurt          0.44
Best Year            32.31%
Worst Year           -7.02%

Avg. Drawdown        -2.07%
Avg. Drawdown Days   26.21
Avg. Up Month        3.01%
Avg. Down Month      -2.19%
Win Year %           60.00%
Win 12m %            83.61%