Backtrader動量策略

  |  

在另一篇偉大的文章中,泰迪·科克(Teddy Koker)再次展示了演算法交易策略的發展之路:

  • 研究優先應用 pandas
  • 回溯測試,然後使用 backtrader

榮譽!!!

該帖子可以在以下位置找到:

泰迪·科克(Teddy Koker)給我留言,問我是否可以評論 backtrader的用法。我的觀點可以 在下面看到。這隻是我個人的拙見,因為作為 backtrader 的作者,我對如何最好地使用這個平臺有偏見。

而我個人對如何制定某些結構的品味,不必與其他人喜歡使用平臺的方式相匹配。

注意

實際上,讓平臺 open 插入幾乎任何東西,並用不同的方式做同樣的事情,是一個有意識的決定,讓人們以他們認為合適的方式使用它(在平臺目標,語言可能性和我做出的失敗的設計決策的限制範圍內)。

在這裡,我們將只關注本來可以以不同的方式完成的事情。“不同”是否更好總是一個觀點問題。 backtrader 的作者並不總是必須正確使用「backtrader」進行開發實際上「更好」(因為實際開發必須適合開發人員而不是」backtrader」的作者)

參數:dict vs tuple of tuples

文件和/或博客中提供backtrader 的許多範例都使用 tuple of tuples 該模式作為參數。例如,從代碼中:

class Momentum(bt.Indicator):
    lines = ('trend',)
    params = (('period', 90),)

與這種範式一起,人們總是有機會使用adict

class Momentum(bt.Indicator):
    lines = ('trend',)
    params = dict(period=90)  # or params = {'period': 90}

隨著時間的流逝,這已經變得更容易使用,並成為作者的首選模式。

注意

作者更喜歡dict(period=90),更容易輸入,不需要引號。但是大括弧表示法 {'period': 90},是許多其他人的首選。

tuple 方法之間的dict根本區別:

  • tuple of tuples使用參數保留聲明的順序,這在枚舉它們時可能很重要。

    提示

    對於Python3.7 中的預設有序字典,聲明順序應該沒有問題( 3.6 如果使用CPython,即使它是一個實現細節)

在下面作者修改的範例中dict ,將使用符號。

指標Momentum

在本文中,這是指標的定義方式

class Momentum(bt.Indicator):
    lines = ('trend',)
    params = (('period', 90),)

    def __init__(self):
        self.addminperiod(self.params.period)

    def next(self):
        returns = np.log(self.data.get(size=self.p.period))
        x = np.arange(len(returns))
        slope, _, rvalue, _, _ = linregress(x, returns)
        annualized = (1 + slope) ** 252
        self.lines.trend[0] = annualized * (rvalue ** 2)

使用力,即:使用已經存在的東西, PeriodN 如指標,它:

  • 已經定義了一個 period 參數,並知道如何將其傳遞給系統

因此,這可以更好

class Momentum(bt.ind.PeriodN):
    lines = ('trend',)
    params = dict(period=50)

    def next(self):
        ...

我們已經跳過了為使用 addminperiod的唯一目的而定義__init__的需求,這應該只在特殊情況下使用。

為了繼續, backtrader 定義了一個OperationN 指標,該指標必須定義一個屬性,該屬性 funcperiod 作為參數傳遞柱線,並將返回值放入定義的 line中。

考慮到這一點,人們可以想像以下內容是潛在的代碼

def momentum_func(the_array):
    r = np.log(the_array)
    slope, _, rvalue, _, _ = linregress(np.arange(len(r)), r)
    annualized = (1 + slope) ** 252
    return annualized * (rvalue ** 2)


class Momentum(bt.ind.OperationN):
    lines = ('trend',)
    params = dict(period=50)
    func = momentum_func

這意味著我們已經將指標的複雜性排除在指標之外。我們甚至可以從外部庫導入momentum_func ,如果底層函數發生變化,指標也無需更改即可反映新行為。作為獎金,我們有 純粹的 聲明性指標。否 __init__、否 addminperiod 和否 next

戰略

讓我們看一下這部分__init__

class Strategy(bt.Strategy):
    def __init__(self):
        self.i = 0
        self.inds = {}
        self.spy = self.datas[0]
        self.stocks = self.datas[1:]

        self.spy_sma200 = bt.indicators.SimpleMovingAverage(self.spy.close,
                                                            period=200)
        for d in self.stocks:
            self.inds[d] = {}
            self.inds[d]["momentum"] = Momentum(d.close,
                                                period=90)
            self.inds[d]["sma100"] = bt.indicators.SimpleMovingAverage(d.close,
                                                                       period=100)
            self.inds[d]["atr20"] = bt.indicators.ATR(d,
                                                      period=20)

關於樣式的一些內容:

  • 盡可能使用參數,而不是固定值

  • 使用較短的名稱和較短的名稱(例如,對於導入),在大多數情況下,它將提高可讀性

  • 充分利用 Python

  • 不要用於closedata feed。通常傳遞data feed,它將使用 close。這可能看起來不相關,但是當嘗試在任何地方保持代碼通用時(如在指標中),它確實有所説明。

人們應該/應該考慮的第一件事是:如果可能的話,將所有內容作為參數。因此

class Strategy(bt.Strategy):
    params = dict(
        momentum=Momentum,  # parametrize the momentum and its period
        momentum_period=90,

        movav=bt.ind.SMA,  # parametrize the moving average and its periods
        idx_period=200,
        stock_period=100,

        volatr=bt.ind.ATR,  # parametrize the volatility and its period
        vol_period=20,
    )


    def __init__(self):
        # self.i = 0  # See below as to why the counter is commented out
        self.inds = collections.defaultdict(dict)  # avoid per data dct in for

        # Use "self.data0" (or self.data) in the script to make the naming not
        # fixed on this being a "spy" strategy. Keep things generic
        # self.spy = self.datas[0]
        self.stocks = self.datas[1:]

        # Again ... remove the name "spy"
        self.idx_mav = self.p.movav(self.data0, period=self.p.idx_period)
        for d in self.stocks:
            self.inds[d]['mom'] = self.p.momentum(d, period=self.momentum_period)
            self.inds[d]['mav'] = self.p.movav(d, period=self.p.stock_period)
            self.inds[d]['vol'] = self.p.volatr(d, period=self.p.vol_period)

params通過使用和更改一些命名約定,我們使__init__(以及隨之而來的策略)完全可定製和通用(沒有spy引用任何hwere)

next 及其 len

backtrader 嘗試在可能的情況下使用Python範例。它肯定會失敗,但它會嘗試。

讓我們看看在next

    def next(self):
        if self.i % 5 == 0:
            self.rebalance_portfolio()
        if self.i % 10 == 0:
            self.rebalance_positions()
        self.i += 1

這就是Pythonlen 範式説明的地方。讓我們使用它

    def next(self):
        l = len(self)
        if l % 5 == 0:
            self.rebalance_portfolio()
        if l % 10 == 0:
            self.rebalance_positions()

如您所見,沒有必要保留self.i 計數器。策略和大多數對象的長度由系統一直提供,計算和更新。

nextprenext

代碼包含此轉發

    def prenext(self):
        # call next() even when data is not available for all tickers
        self.next()

而且進入時沒有 保障 next

    def next(self):
        if self.i % 5 == 0:
            self.rebalance_portfolio()
        ...

好吧,我們知道正在使用無生存偏差的數據集,但通常不保護prenext => next 轉發不是一個好主意。

  • 當所有緩衝區(指標、data feeds)至少可以提供數據點時,backtrader調用next100-bar移動平均線顯然只有在具有來自data feed的100個數據點時才會提供。

    這意味著在進入next時,必須100 data points檢查data feed,移動平均線1 data point

  • backtrader提供prenext作為鉤子,讓開發人員在滿足上述保證之前訪問內容。例如,當多個data feeds正在運行並且它們的開始日期不同時,這很有用。開發人員可能希望在滿足next所有data feeds(和相關指標)的所有保證並首次被要求之前進行一些檢查或採取行動。

在一般情況下,prenext => next 轉發應該有一個這樣的保護裝置:

    def prenext(self):
        # call next() even when data is not available for all tickers
        self.next()

    def next(self):
        d_with_len = [d for d in self.datas if len(d)]
        ...

這意味著只有 來自的self.datas子集d_with_len可以保證使用。

注意

類似的保護必須用於指標。

因為在策略的整個生命週期內進行這種計算似乎毫無意義,因此可以進行這樣的優化。

    def __init__(self):
        ...
        self.d_with_len = []


    def prenext(self):
        # Populate d_with_len
        self.d_with_len = [d for d in self.datas if len(d)]
        # call next() even when data is not available for all tickers
        self.next()

    def nextstart(self):
        # This is called exactly ONCE, when next is 1st called and defaults to
        # call `next`
        self.d_with_len = self.datas  # all data sets fulfill the guarantees now

        self.next()  # delegate the work to next

    def next(self):
        # we can now always work with self.d_with_len with no calculation
        ...

當滿足保證時,將停止調用保護計算prenextnextstart 然後調用它,通過覆蓋它,我們可以重置 list 保存數據集的哪個,作為完整的數據集,即: self.datas

有了這個,所有的警衛都被移除了next

next 帶計時器

雖然作者在這裡的意圖是每5/10天重新平衡(投資組合/頭寸),但這可能意味著每周/每兩周重新平衡。

如果出現以下情況,該len(self) % period 方法將失敗:

  • 數據集不是在星期一開始的

  • 在交易假期期間,這將使再平衡變得不一致

為了克服這一點,可以使用內置功能 backtrader

使用它們將確保在預期發生時進行再平衡。讓我們想像一下,其目的是在星期五重新平衡。

讓我們在我們的策略中params__init__加入一些魔力

class Strategy(bt.Strategy):
    params = dict(
       ...
       rebal_weekday=5,  # rebalance 5 is Friday
    )

    def __init__(self):
        ...
        self.add_timer(
            when=bt.Timer.SESSION_START,
            weekdays=[self.p.rebal_weekday],
            weekcarry=True,  # if a day isn't there, execute on the next
        )
        ...

現在我們已經準備好知道什麼時候是星期五。即使星期五碰巧是交易假期,添加weekcarry=True 也可以確保我們在星期一收到通知(如果星期一也是假期,則為星期二或...

計時器的通知已接收notify_timer

def notify_timer(self, timer, when, *args, **kwargs):
    self.rebalance_portfolio()

因為原始代碼中的每個柱線10也會發生一個rebalance_positions,所以可以:

  • 添加第 2個計時器 ,也適用於星期五

  • 使用計數器僅對每個第 2 個 調用執行操作,這甚至可以在計時器本身中使用 allow=callable 參數

注意

計時器甚至可以更好地用於實現以下模式:

  • rebalance_portfolio 每月第2和第 4個星期

  • rebalance_positions 僅限每月 4個星期五

一些額外功能

其他一些事情可能純粹是個人品味的問題。

個人品味 1

始終使用預先構建的比較,而不是在過程中next比較內容。例如,從代碼(多次使用)

        if self.spy < self.spy_sma200:
            return

我們可以做以下事情。第一個期間__init__

    def __init__(self):
        ...
        self.spy_filter = self.spe < self.spy_sma200

以及後來的

        if self.spy_filter:
            return

考慮到這一點,如果我們想改變spy_filter 條件,我們只需要在代碼中的 __init__ 多個位置執行此操作一次,而不必這樣做。

這同樣適用於這裡的其他比較d < self.inds[d]["sma100"]

        # sell stocks based on criteria
        for i, d in enumerate(self.rankings):
            if self.getposition(self.data).size:
                if i > num_stocks * 0.2 or d < self.inds[d]["sma100"]:
                    self.close(d)

它也可以在期間__init__ 預先構建,因此更改為類似的東西

        # sell stocks based on criteria
        for i, d in enumerate(self.rankings):
            if self.getposition(self.data).size:
                if i > num_stocks * 0.2 or self.inds[d]['sma_signal']:
                    self.close(d)

個人品味 2

使所有內容都成為參數。例如,在上面的 lines 中,我們看到一個0.2 在代碼的幾個部分中使用的: 使其成為一個參數。與其他值(如 0.001100 )相同(實際上已經建議將其作為創建移動平均線的參數)

將所有內容作為參數允許打包代碼,並通過更改策略的實例化而不是策略本身來嘗試不同的事情。

推薦閱讀

相關文章

Backtrader向 OHLC 提供買入價/賣出價數據

最近,backtrader通過實現line覆蓋來執行從 ohlc-land 逃逸,這允許重新定義整個層次結構,例如,具有僅具有 bid,ask 和 datetime lines的data feeds。

Backtrader按日線交易

似乎在世界某個地方有一種权益(Interest)可以總結如下: 使用每日柱線引入訂單,但使用開盤價 這來自工單#105订单执行逻辑与当前数据和#101动态投注计算中的對話 backtrader 嘗試盡可能保持現實,並且在處理每日柱線時適用以下前提: 當每日柱被評估時,柱線已經結束 這是有道理的,

Backtrader對逐筆報價數據重新取樣

backtrader 已經可以從分鐘數據中重新採樣。接受價格變動數據不是問題,只需將 4 個常用欄位(open、 high、 low、 close)設置為價格變動值。 但是傳遞要重新採樣的逐筆報價數據再次生成相同的數據。作為或版本 1.1.11.88,情況已不再如此。

Backtrader教程:指標 - 開發

如果必須開發任何東西(除了一個或多個獲勝策略之外),那麼這個東西就是一個自定義指標。 根據作者的說法,平臺內的這種開發很容易。 需要滿足以下條件: 從指標派生的類(直接或從現有的子類派生) 定義它將保持lines 指標必須至少具有 1 line。

Backtrader教程:操作平臺

Line 反覆運算器 為了參與操作,plaftorm使用 line 反覆運算器的概念。它們已經鬆散地模仿了Python的反覆運算器,但實際上與它們無關。 策略和指標是 line 反覆運算器。

Backtrader股票篩選

在尋找其他一些東西時,我在StackOverlow家族網站之一上遇到了一個問題:Quantitative Finance aka Quant StackExchange。問題: 它被標記為Python,因此值得一看的是 backtrader 是否能夠勝任這項任務。 分析儀本身 該問題似乎適合用於簡單的分析器。

Backtrader教程:過濾器 - 參考

工作階段篩檢程式 類 backtrader.filters。

Backtrader跨越數位

《backtrader》的發佈1.9.27.105糾正了一個疏忽。這是一個疏忽,因為拼圖的所有部分都已到位,但啟動並不是在所有角落都進行的。 該機制使用一個名為的屬性_mindatas,因此讓我們將其稱為: mindatas。 社區問了這個問題,答案並不是很到位。

Backtrader教程:Cerebro - 優化 - 改進

backtrader版本1.8.12.99改進了在多處理過程中管理data feeds和結果的方式。

Backtrader教程:日期時間 - 管理

在 1.5.0 版之前, backtrader 使用直接的方法來進行時間管理,因為數據源計算的任何日期時間都只是按面值使用。 對於任何使用者輸入也是如此,例如可以提供給任何數據源的參數fromdate (或 sessionstart)的情況 考慮到直接控制凍結的數據源以進行回溯測試,這種方法很好。