BackTrader 中文文件(二十七)

绝不原创的飞龙發表於2024-04-15

原文:www.backtrader.com/

資料 - 多個時間框架

原文:www.backtrader.com/blog/posts/2015-08-24-data-multitimeframe/data-multitimeframe/

有時,使用不同的時間框架進行投資決策:

  • 周線用於評估趨勢

  • 每日執行進入

或者 5 分鐘對比 60 分鐘。

這意味著在 backtrader 中需要組合多個時間框架的資料以支援這樣的組合。

本地支援已經內建。終端使用者只需遵循以下規則:

  • 具有最小時間框架的資料(因此是較大數量的條)必須是新增到 Cerebro 例項的第一個資料

  • 資料必須正確地對齊日期時間,以便平臺能夠理解它們的任何含義

此外,終端使用者可以自由地在較短/較大的時間框架上應用指標。當然:

  • 應用於較大時間框架的指標將產生較少的條

平臺也將考慮以下內容

  • 較大時間框架的最小週期

最小週期可能會導致在策略新增到 Cerebro 之前需要消耗幾個數量級的較小時間框架的資料。

內建的DataResampler將用於建立較大的時間框架。

一些示例如下,但首先是測試指令碼的來源。

 # Load the Data
    datapath = args.dataname or '../datas/sample/2006-day-001.txt'
    data = btfeeds.BacktraderCSVData(
        dataname=datapath)

    tframes = dict(
        daily=bt.TimeFrame.Days,
        weekly=bt.TimeFrame.Weeks,
        monthly=bt.TimeFrame.Months)

    # Handy dictionary for the argument timeframe conversion
    # Resample the data
    if args.noresample:
        datapath = args.dataname2 or '../datas/sample/2006-week-001.txt'
        data2 = btfeeds.BacktraderCSVData(
            dataname=datapath)
    else:
        data2 = bt.DataResampler(
            dataname=data,
            timeframe=tframes[args.timeframe],
            compression=args.compression)

步驟:

  • 載入資料

  • 根據使用者指定的引數重新取樣它

    指令碼還允許載入第二個資料

  • 將資料新增到 cerebro

  • 將重新取樣的資料(較大的時間框架)新增到 cerebro

  • 執行

示例 1 - 每日和每週

指令碼的呼叫:

$ ./data-multitimeframe.py --timeframe weekly --compression 1

和輸出圖表:

image

示例 2 - 日間和日間壓縮(2 根變成 1 根)

指令碼的呼叫:

$ ./data-multitimeframe.py --timeframe daily --compression 2

和輸出圖表:

image

示例 3 - 帶有 SMA 的策略

雖然繪圖很好,但這裡的關鍵問題是顯示較大的時間框架如何影響系統,特別是當涉及到起始點時

指令碼可以採用--indicators來新增一個策略,該策略在較小時間框架和較大時間框架的資料上建立10 週期的簡單移動平均線。

如果只考慮較小的時間框架:

  • next將在 10 個條之後首先被呼叫,這是簡單移動平均需要產生值的時間

    注意

    請記住,策略監視建立的指標,並且只有在所有指標都產生值時才呼叫next。理由是終端使用者已經新增了指標以在邏輯中使用它們,因此如果指標尚未產生值,則不應進行任何邏輯

但在這種情況下,較大的時間框架(每週)會延遲呼叫next,直到每週資料的簡單移動平均產生值為止,這需要... 10 周。

指令碼覆蓋了nextstart,它只被呼叫一次,預設呼叫next以顯示首次呼叫的時間。

呼叫 1:

只有較小的時間框架,即每日,才有一個簡單移動平均值。

命令列和輸出

$ ./data-multitimeframe.py --timeframe weekly --compression 1 --indicators --onlydaily
--------------------------------------------------
nextstart called with len 10
--------------------------------------------------

以及圖表。

圖片

呼叫 2:

兩個時間框架都有一個簡單移動平均。

命令列:

$ ./data-multitimeframe.py --timeframe weekly --compression 1 --indicators
--------------------------------------------------
nextstart called with len 50
--------------------------------------------------
--------------------------------------------------
nextstart called with len 51
--------------------------------------------------
--------------------------------------------------
nextstart called with len 52
--------------------------------------------------
--------------------------------------------------
nextstart called with len 53
--------------------------------------------------
--------------------------------------------------
nextstart called with len 54
--------------------------------------------------

注意這裡的兩件事:

  • 不是在10個週期之後被呼叫,而是在 50 個週期之後第一次被呼叫。

    這是因為在較大(周)時間框架上應用簡單移動平均值後產生了一個值,... 這是 10 周* 5 天/周... 50 天。

  • nextstart被呼叫了 5 次,而不是僅 1 次。

    這是將時間框架混合並(在這種情況下僅有一個)指標應用於較大時間框架的自然副作用。

    較大時間框架的簡單移動平均值在消耗 5 個日間條時產生 5 倍相同的值。

    由於週期的開始由較大的時間框架控制,nextstart被呼叫了 5 次。

以及圖表。

圖片

結論

多時間框架資料可以在backtrader中使用,無需特殊物件或調整:只需先新增較小的時間框架。

測試指令碼。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import argparse

import backtrader as bt
import backtrader.feeds as btfeeds
import backtrader.indicators as btind

class SMAStrategy(bt.Strategy):
    params = (
        ('period', 10),
        ('onlydaily', False),
    )

    def __init__(self):
        self.sma_small_tf = btind.SMA(self.data, period=self.p.period)
        if not self.p.onlydaily:
            self.sma_large_tf = btind.SMA(self.data1, period=self.p.period)

    def nextstart(self):
        print('--------------------------------------------------')
        print('nextstart called with len', len(self))
        print('--------------------------------------------------')

        super(SMAStrategy, self).nextstart()

def runstrat():
    args = parse_args()

    # Create a cerebro entity
    cerebro = bt.Cerebro(stdstats=False)

    # Add a strategy
    if not args.indicators:
        cerebro.addstrategy(bt.Strategy)
    else:
        cerebro.addstrategy(
            SMAStrategy,

            # args for the strategy
            period=args.period,
            onlydaily=args.onlydaily,
        )

    # Load the Data
    datapath = args.dataname or '../datas/sample/2006-day-001.txt'
    data = btfeeds.BacktraderCSVData(
        dataname=datapath)

    tframes = dict(
        daily=bt.TimeFrame.Days,
        weekly=bt.TimeFrame.Weeks,
        monthly=bt.TimeFrame.Months)

    # Handy dictionary for the argument timeframe conversion
    # Resample the data
    if args.noresample:
        datapath = args.dataname2 or '../datas/sample/2006-week-001.txt'
        data2 = btfeeds.BacktraderCSVData(
            dataname=datapath)
    else:
        data2 = bt.DataResampler(
            dataname=data,
            timeframe=tframes[args.timeframe],
            compression=args.compression)

    # First add the original data - smaller timeframe
    cerebro.adddata(data)

    # And then the large timeframe
    cerebro.adddata(data2)

    # Run over everything
    cerebro.run()

    # Plot the result
    cerebro.plot(style='bar')

def parse_args():
    parser = argparse.ArgumentParser(
        description='Pandas test script')

    parser.add_argument('--dataname', default='', required=False,
                        help='File Data to Load')

    parser.add_argument('--dataname2', default='', required=False,
                        help='Larger timeframe file to load')

    parser.add_argument('--noresample', action='store_true',
                        help='Do not resample, rather load larger timeframe')

    parser.add_argument('--timeframe', default='weekly', required=False,
                        choices=['daily', 'weekly', 'monhtly'],
                        help='Timeframe to resample to')

    parser.add_argument('--compression', default=1, required=False, type=int,
                        help='Compress n bars into 1')

    parser.add_argument('--indicators', action='store_true',
                        help='Wether to apply Strategy with indicators')

    parser.add_argument('--onlydaily', action='store_true',
                        help='Indicator only to be applied to daily timeframe')

    parser.add_argument('--period', default=10, required=False, type=int,
                        help='Period to apply to indicator')

    return parser.parse_args()

if __name__ == '__main__':
    runstrat()

資料重新取樣

原文:www.backtrader.com/blog/posts/2015-08-23-data-resampling/data-resampling/

當資料僅在一個時間段可用,而分析必須針對不同的時間段進行時,就是時候進行一些重新取樣了。

“重新取樣”實際上應該稱為“向上取樣”,因為要從一個源時間段轉換到一個較大的時間段(例如:從天到周)

“向下取樣”目前還不可能。

backtrader 透過將原始資料傳遞給一個智慧命名為 DataResampler 的過濾器物件來支援重新取樣。

該類具有兩個功能:

  • 更改時間框架

  • 壓縮條柱

為此,DataResampler 在構造過程中使用標準的 feed.DataBase 引數:

  • timeframe(預設:bt.TimeFrame.Days)

    目標時間段必須與源時間段相等或更大才能有用

  • compression(預設:1)

    將所選值 “n” 壓縮到 1 條柱

讓我們看一個從每日到每週的手工指令碼示例:

$ ./data-resampling.py --timeframe weekly --compression 1

輸出結果:

image

我們可以將其與原始每日資料進行比較:

$ ./data-resampling.py --timeframe daily --compression 1

輸出結果:

image

這是透過執行以下步驟來完成的魔術:

  • 像往常一樣載入資料

  • 將資料饋送到具有所需的 DataResampler

    • 時間框架

    • 壓縮

示例程式碼(底部的整個指令碼)。

 # Load the Data
    datapath = args.dataname or '../datas/sample/2006-day-001.txt'
    data = btfeeds.BacktraderCSVData(
        dataname=datapath)

    # Handy dictionary for the argument timeframe conversion
    tframes = dict(
        daily=bt.TimeFrame.Days,
        weekly=bt.TimeFrame.Weeks,
        monthly=bt.TimeFrame.Months)

    # Resample the data
    data_resampled = bt.DataResampler(
        dataname=data,
        timeframe=tframes[args.timeframe],
        compression=args.compression)

    # Add the resample data instead of the original
    cerebro.adddata(data_resampled)

最後一個例子中,我們首先將時間框架從每日更改為每週,然後應用 3 到 1 的壓縮:

$ ./data-resampling.py --timeframe weekly --compression 3

輸出結果:

image

從原始的 256 個每日條柱變為 18 個 3 周條柱。具體情況如下:

  • 52 周

  • 52 / 3 = 17.33 因此為 18 條柱

這不需要太多。當然,分鐘資料也可以進行重新取樣。

重新取樣測試指令碼的示例程式碼。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import argparse

import backtrader as bt
import backtrader.feeds as btfeeds

def runstrat():
    args = parse_args()

    # Create a cerebro entity
    cerebro = bt.Cerebro(stdstats=False)

    # Add a strategy
    cerebro.addstrategy(bt.Strategy)

    # Load the Data
    datapath = args.dataname or '../datas/sample/2006-day-001.txt'
    data = btfeeds.BacktraderCSVData(
        dataname=datapath)

    # Handy dictionary for the argument timeframe conversion
    tframes = dict(
        daily=bt.TimeFrame.Days,
        weekly=bt.TimeFrame.Weeks,
        monthly=bt.TimeFrame.Months)

    # Resample the data
    data_resampled = bt.DataResampler(
        dataname=data,
        timeframe=tframes[args.timeframe],
        compression=args.compression)

    # Add the resample data instead of the original
    cerebro.adddata(data_resampled)

    # Run over everything
    cerebro.run()

    # Plot the result
    cerebro.plot(style='bar')

def parse_args():
    parser = argparse.ArgumentParser(
        description='Pandas test script')

    parser.add_argument('--dataname', default='', required=False,
                        help='File Data to Load')

    parser.add_argument('--timeframe', default='weekly', required=False,
                        choices=['daily', 'weekly', 'monhtly'],
                        help='Timeframe to resample to')

    parser.add_argument('--compression', default=1, required=False, type=int,
                        help='Compress n bars into 1')

    return parser.parse_args()

if __name__ == '__main__':
    runstrat()

Pandas DataFeed 支援

原文:www.backtrader.com/blog/posts/2015-08-21-pandas-datafeed/pandas-datafeed/

在一些小的增強和一些有序字典調整以更好地支援 Python 2.6 的情況下,backtrader 的最新版本增加了對從 Pandas Dataframe 或時間序列分析資料的支援。

注意

顯然必須安裝 pandas 及其依賴項。

這似乎引起了很多人的關注,他們依賴於已經可用的用於不同資料來源(包括 CSV)的解析程式碼。

class PandasData(feed.DataBase):
    '''
    The ``dataname`` parameter inherited from ``feed.DataBase``  is the pandas
    Time Series
    '''

    params = (
        # Possible values for datetime (must always be present)
        #  None : datetime is the "index" in the Pandas Dataframe
        #  -1 : autodetect position or case-wise equal name
        #  >= 0 : numeric index to the colum in the pandas dataframe
        #  string : column name (as index) in the pandas dataframe
        ('datetime', None),

        # Possible values below:
        #  None : column not present
        #  -1 : autodetect position or case-wise equal name
        #  >= 0 : numeric index to the colum in the pandas dataframe
        #  string : column name (as index) in the pandas dataframe
        ('open', -1),
        ('high', -1),
        ('low', -1),
        ('close', -1),
        ('volume', -1),
        ('openinterest', -1),
    )

上述從 PandasData 類中摘錄的片段顯示了鍵:

  • 例項化期間 dataname 引數對應 Pandas Dataframe

    此引數繼承自基類 feed.DataBase

  • 新引數使用了 DataSeries 中常規欄位的名稱,並遵循這些約定

    • datetime(預設:無)

    • 無:datetime 是 Pandas Dataframe 中的“index”

    • -1:自動檢測位置或大小寫相等的名稱

    • = 0:Pandas 資料框中列的數值索引

    • string:Pandas 資料框中的列名(作為索引)

    • open, high, low, high, close, volume, openinterest(預設:全部為 -1)

    • 無:列不存在

    • -1:自動檢測位置或大小寫相等的名稱

    • = 0:Pandas 資料框中列的數值索引

    • string:Pandas 資料框中的列名(作為索引)

一個小的樣本應該能夠載入標準 2006 樣本,已被 Pandas 解析,而不是直接由 backtrader 解析。

執行示例以使用 CSV 資料中的現有“headers”:

$ ./panda-test.py
--------------------------------------------------
               Open     High      Low    Close  Volume  OpenInterest
Date
2006-01-02  3578.73  3605.95  3578.73  3604.33       0             0
2006-01-03  3604.08  3638.42  3601.84  3614.34       0             0
2006-01-04  3615.23  3652.46  3615.23  3652.46       0             0

相同,但告訴指令碼跳過標題:

$ ./panda-test.py --noheaders
--------------------------------------------------
                  1        2        3        4  5  6
0
2006-01-02  3578.73  3605.95  3578.73  3604.33  0  0
2006-01-03  3604.08  3638.42  3601.84  3614.34  0  0
2006-01-04  3615.23  3652.46  3615.23  3652.46  0  0

第二次執行是使用 tells pandas.read_csv

  • 跳過第一個輸入行(skiprows 關鍵字引數設定為 1)

  • 不要尋找標題行(header 關鍵字引數設定為 None)

backtrader 對 Pandas 的支援嘗試自動檢測是否已使用列名,否則使用數值索引,並相應地採取行動,嘗試提供最佳匹配。

以下圖表是成功的致敬。Pandas Dataframe 已被正確載入(在兩種情況下)。

image

測試的示例程式碼。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import argparse

import backtrader as bt
import backtrader.feeds as btfeeds

import pandas

def runstrat():
    args = parse_args()

    # Create a cerebro entity
    cerebro = bt.Cerebro(stdstats=False)

    # Add a strategy
    cerebro.addstrategy(bt.Strategy)

    # Get a pandas dataframe
    datapath = ('../datas/sample/2006-day-001.txt')

    # Simulate the header row isn't there if noheaders requested
    skiprows = 1 if args.noheaders else 0
    header = None if args.noheaders else 0

    dataframe = pandas.read_csv(datapath,
                                skiprows=skiprows,
                                header=header,
                                parse_dates=True,
                                index_col=0)

    if not args.noprint:
        print('--------------------------------------------------')
        print(dataframe)
        print('--------------------------------------------------')

    # Pass it to the backtrader datafeed and add it to the cerebro
    data = bt.feeds.PandasData(dataname=dataframe)

    cerebro.adddata(data)

    # Run over everything
    cerebro.run()

    # Plot the result
    cerebro.plot(style='bar')

def parse_args():
    parser = argparse.ArgumentParser(
        description='Pandas test script')

    parser.add_argument('--noheaders', action='store_true', default=False,
                        required=False,
                        help='Do not use header rows')

    parser.add_argument('--noprint', action='store_true', default=False,
                        help='Print the dataframe')

    return parser.parse_args()

if __name__ == '__main__':
    runstrat()

自動化 backtrader 回測。

原文:www.backtrader.com/blog/posts/2015-08-16-backtesting-with-almost-no-programming/backtesting-with-almost-no-programming/

到目前為止,所有 backtrader 的示例和工作樣本都是從頭開始建立一個主要的 Python 模組,載入資料、策略、觀察者,並準備現金和佣金方案。

演算法交易的一個目標是交易的自動化,鑑於 bactrader 是一個用於檢查交易演算法的回測平臺(因此是一個演算法交易平臺),自動化使用 backtrader 是一個顯而易見的目標。

注意

2015 年 8 月 22 日

bt-run.py 中包含了對 Analyzer 的支援。

backtrader 的開發版本現在包含了 bt-run.py 指令碼,它自動化了大多數任務,並將作為常規軟體包的一部分與 backtrader 一起安裝。

bt-run.py 允許終端使用者:

  • 指明必須載入的資料。

  • 設定載入資料的格式。

  • 指定資料的日期範圍

  • 禁用標準觀察者。

  • 從內建的或 Python 模組中載入一個或多個觀察者(例如:回撤)

  • 為經紀人設定現金和佣金方案引數(佣金、保證金、倍數)

  • 啟用繪圖,控制圖表數量和資料呈現風格。

最後:

  • 載入策略(內建的或來自 Python 模組)

  • 向載入的策略傳遞引數。

請參閱下面關於指令碼的用法*。

應用使用者定義的策略。

讓我們考慮以下策略:

  • 簡單地載入一個 SimpleMovingAverage(預設週期 15)

  • 列印輸出。

  • 檔名為 mymod.py。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import backtrader as bt
import backtrader.indicators as btind

class MyTest(bt.Strategy):
    params = (('period', 15),)

    def log(self, txt, dt=None):
        ''' Logging function fot this strategy'''
        dt = dt or self.data.datetime[0]
        if isinstance(dt, float):
            dt = bt.num2date(dt)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        sma = btind.SMA(period=self.p.period)

    def next(self):
        ltxt = '%d, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f'

        self.log(ltxt %
                 (len(self),
                  self.data.open[0], self.data.high[0],
                  self.data.low[0], self.data.close[0],
                  self.data.volume[0], self.data.openinterest[0]))

用通常的測試樣本執行策略很容易:簡單:

./bt-run.py --csvformat btcsv \
            --data ../samples/data/sample/2006-day-001.txt \
            --strategy ./mymod.py

圖表輸出。

image

控制檯輸出:

2006-01-20T23:59:59+00:00, 15, 3593.16, 3612.37, 3550.80, 3550.80, 0.00, 0.00
2006-01-23T23:59:59+00:00, 16, 3550.24, 3550.24, 3515.07, 3544.31, 0.00, 0.00
2006-01-24T23:59:59+00:00, 17, 3544.78, 3553.16, 3526.37, 3532.68, 0.00, 0.00
2006-01-25T23:59:59+00:00, 18, 3532.72, 3578.00, 3532.72, 3578.00, 0.00, 0.00
...
...
2006-12-22T23:59:59+00:00, 252, 4109.86, 4109.86, 4072.62, 4073.50, 0.00, 0.00
2006-12-27T23:59:59+00:00, 253, 4079.70, 4134.86, 4079.70, 4134.86, 0.00, 0.00
2006-12-28T23:59:59+00:00, 254, 4137.44, 4142.06, 4125.14, 4130.66, 0.00, 0.00
2006-12-29T23:59:59+00:00, 255, 4130.12, 4142.01, 4119.94, 4119.94, 0.00, 0.00

同樣的策略但是:

  • 將引數 period 設定為 50。

命令列:

./bt-run.py --csvformat btcsv \
            --data ../samples/data/sample/2006-day-001.txt \
            --strategy ./mymod.py \
            period 50

圖表輸出。

image

使用內建策略。

backtrader 將逐漸包含示例(教科書)策略。隨著 bt-run.py 指令碼一起,一個標準的簡單移動平均線交叉策略已包含在內。名稱:

  • SMA_CrossOver

  • 引數。

    • 快速(預設 10)快速移動平均的週期

    • 慢(預設 30)慢速移動平均的週期

如果快速移動平均線向上穿過快速移動平均線並且在慢速移動平均線向下穿過快速移動平均線後賣出(僅在之前已購買的情況下)。

程式碼。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import backtrader as bt
import backtrader.indicators as btind

class SMA_CrossOver(bt.Strategy):

    params = (('fast', 10), ('slow', 30))

    def __init__(self):

        sma_fast = btind.SMA(period=self.p.fast)
        sma_slow = btind.SMA(period=self.p.slow)

        self.buysig = btind.CrossOver(sma_fast, sma_slow)

    def next(self):
        if self.position.size:
            if self.buysig < 0:
                self.sell()

        elif self.buysig > 0:
            self.buy()

標準執行:

./bt-run.py --csvformat btcsv \
            --data ../samples/data/sample/2006-day-001.txt \
            --strategy :SMA_CrossOver

注意‘:’。載入策略的標準表示法(見下文)是:

  • 模組:策略。

遵循以下規則:

  • 如果模組存在且指定了策略,則將使用該策略。

  • 如果模組存在但未指定策略,則將返回模組中找到的第一個策略。

  • 如果未指定模組,則假定“strategy”是指 backtrader 包中的策略

後者是我們的情況。

輸出。

image

最後一個示例新增佣金方案、現金並更改引數:

./bt-run.py --csvformat btcsv \
            --data ../samples/data/sample/2006-day-001.txt \
            --cash 20000 \
            --commission 2.0 \
            --mult 10 \
            --margin 2000 \
            --strategy :SMA_CrossOver \
            fast 5 slow 20

輸出。

image

我們已經對策略進行了回測:

  • 更改移動平均週期

  • 設定新的起始資金

  • 為期貨類工具設定佣金方案

    檢視每根柱子中現金的連續變化,因為現金會根據期貨類工具的每日變動進行調整。

新增分析器

注意

新增了分析器示例

bt-run.py還支援使用與策略相同的語法新增Analyzers來選擇內部/外部分析器。

SharpeRatio分析 2005-2006 年為例:

./bt-run.py --csvformat btcsv \
            --data ../samples/data/sample/2005-2006-day-001.txt \
            --strategy :SMA_CrossOver \
            --analyzer :SharpeRatio

輸出:

====================
== Analyzers
====================
##  sharperatio
--  sharperatio : 11.6473326097

良好的策略!!!(實際示例中純粹是運氣,而且也沒有佣金)

圖表(僅顯示分析器不在圖表中,因為分析器無法繪製,它們不是線物件)

image

指令碼的用法

直接從指令碼中:

$ ./bt-run.py --help
usage: bt-run.py [-h] --data DATA
                 [--csvformat {yahoocsv_unreversed,vchart,sierracsv,yahoocsv,vchartcsv,btcsv}]
                 [--fromdate FROMDATE] [--todate TODATE] --strategy STRATEGY
                 [--nostdstats] [--observer OBSERVERS] [--analyzer ANALYZERS]
                 [--cash CASH] [--commission COMMISSION] [--margin MARGIN]
                 [--mult MULT] [--noplot] [--plotstyle {bar,line,candle}]
                 [--plotfigs PLOTFIGS]
                 ...

Backtrader Run Script

positional arguments:
  args                  args to pass to the loaded strategy

optional arguments:
  -h, --help            show this help message and exit

Data options:
  --data DATA, -d DATA  Data files to be added to the system
  --csvformat {yahoocsv_unreversed,vchart,sierracsv,yahoocsv,vchartcsv,btcsv}, -c {yahoocsv_unreversed,vchart,sierracsv,yahoocsv,vchartcsv,btcsv}
                        CSV Format
  --fromdate FROMDATE, -f FROMDATE
                        Starting date in YYYY-MM-DD[THH:MM:SS] format
  --todate TODATE, -t TODATE
                        Ending date in YYYY-MM-DD[THH:MM:SS] format

Strategy options:
  --strategy STRATEGY, -st STRATEGY
                        Module and strategy to load with format
                        module_path:strategy_name. module_path:strategy_name
                        will load strategy_name from the given module_path
                        module_path will load the module and return the first
                        available strategy in the module :strategy_name will
                        load the given strategy from the set of built-in
                        strategies

Observers and statistics:
  --nostdstats          Disable the standard statistics observers
  --observer OBSERVERS, -ob OBSERVERS
                        This option can be specified multiple times Module and
                        observer to load with format
                        module_path:observer_name. module_path:observer_name
                        will load observer_name from the given module_path
                        module_path will load the module and return all
                        available observers in the module :observer_name will
                        load the given strategy from the set of built-in
                        strategies

Analyzers:
  --analyzer ANALYZERS, -an ANALYZERS
                        This option can be specified multiple times Module and
                        analyzer to load with format
                        module_path:analzyer_name. module_path:analyzer_name
                        will load observer_name from the given module_path
                        module_path will load the module and return all
                        available analyzers in the module :anaylzer_name will
                        load the given strategy from the set of built-in
                        strategies

Cash and Commission Scheme Args:
  --cash CASH, -cash CASH
                        Cash to set to the broker
  --commission COMMISSION, -comm COMMISSION
                        Commission value to set
  --margin MARGIN, -marg MARGIN
                        Margin type to set
  --mult MULT, -mul MULT
                        Multiplier to use

Plotting options:
  --noplot, -np         Do not plot the read data
  --plotstyle {bar,line,candle}, -ps {bar,line,candle}
                        Plot style for the input data
  --plotfigs PLOTFIGS, -pn PLOTFIGS
                        Plot using n figures

以及程式碼:

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import argparse
import datetime
import inspect
import itertools
import random
import string
import sys

import backtrader as bt
import backtrader.feeds as btfeeds
import backtrader.indicators as btinds
import backtrader.observers as btobs
import backtrader.strategies as btstrats
import backtrader.analyzers as btanalyzers

DATAFORMATS = dict(
    btcsv=btfeeds.BacktraderCSVData,
    vchartcsv=btfeeds.VChartCSVData,
    vchart=btfeeds.VChartData,
    sierracsv=btfeeds.SierraChartCSVData,
    yahoocsv=btfeeds.YahooFinanceCSVData,
    yahoocsv_unreversed=btfeeds.YahooFinanceCSVData
)

def runstrat():
    args = parse_args()

    stdstats = not args.nostdstats

    cerebro = bt.Cerebro(stdstats=stdstats)

    for data in getdatas(args):
        cerebro.adddata(data)

    # Prepare a dictionary of extra args passed to push them to the strategy

    # pack them in pairs
    packedargs = itertools.izip_longest(*[iter(args.args)] * 2, fillvalue='')

    # prepare a string for evaluation, eval and store the result
    evalargs = 'dict('
    for key, value in packedargs:
        evalargs += key + '=' + value + ','
    evalargs += ')'
    stratkwargs = eval(evalargs)

    # Get the strategy and add it with any arguments
    strat = getstrategy(args)
    cerebro.addstrategy(strat, **stratkwargs)

    obs = getobservers(args)
    for ob in obs:
        cerebro.addobserver(ob)

    ans = getanalyzers(args)
    for an in ans:
        cerebro.addanalyzer(an)

    setbroker(args, cerebro)

    runsts = cerebro.run()
    runst = runsts[0]  # single strategy and no optimization

    if runst.analyzers:
        print('====================')
        print('== Analyzers')
        print('====================')
        for name, analyzer in runst.analyzers.getitems():
            print('## ', name)
            analysis = analyzer.get_analysis()
            for key, val in analysis.items():
                print('-- ', key, ':', val)

    if not args.noplot:
        cerebro.plot(numfigs=args.plotfigs, style=args.plotstyle)

def setbroker(args, cerebro):
    broker = cerebro.getbroker()

    if args.cash is not None:
        broker.setcash(args.cash)

    commkwargs = dict()
    if args.commission is not None:
        commkwargs['commission'] = args.commission
    if args.margin is not None:
        commkwargs['margin'] = args.margin
    if args.mult is not None:
        commkwargs['mult'] = args.mult

    if commkwargs:
        broker.setcommission(**commkwargs)

def getdatas(args):
    # Get the data feed class from the global dictionary
    dfcls = DATAFORMATS[args.csvformat]

    # Prepare some args
    dfkwargs = dict()
    if args.csvformat == 'yahoo_unreversed':
        dfkwargs['reverse'] = True

    fmtstr = '%Y-%m-%d'
    if args.fromdate:
        dtsplit = args.fromdate.split('T')
        if len(dtsplit) > 1:
            fmtstr += 'T%H:%M:%S'

        fromdate = datetime.datetime.strptime(args.fromdate, fmtstr)
        dfkwargs['fromdate'] = fromdate

    fmtstr = '%Y-%m-%d'
    if args.todate:
        dtsplit = args.todate.split('T')
        if len(dtsplit) > 1:
            fmtstr += 'T%H:%M:%S'
        todate = datetime.datetime.strptime(args.todate, fmtstr)
        dfkwargs['todate'] = todate

    datas = list()
    for dname in args.data:
        dfkwargs['dataname'] = dname
        data = dfcls(**dfkwargs)
        datas.append(data)

    return datas

def getmodclasses(mod, clstype, clsname=None):
    clsmembers = inspect.getmembers(mod, inspect.isclass)

    clslist = list()
    for name, cls in clsmembers:
        if not issubclass(cls, clstype):
            continue

        if clsname:
            if clsname == name:
                clslist.append(cls)
                break
        else:
            clslist.append(cls)

    return clslist

def loadmodule(modpath, modname=''):
    # generate a random name for the module
    if not modname:
        chars = string.ascii_uppercase + string.digits
        modname = ''.join(random.choice(chars) for _ in range(10))

    version = (sys.version_info[0], sys.version_info[1])

    if version < (3, 3):
        mod, e = loadmodule2(modpath, modname)
    else:
        mod, e = loadmodule3(modpath, modname)

    return mod, e

def loadmodule2(modpath, modname):
    import imp

    try:
        mod = imp.load_source(modname, modpath)
    except Exception, e:
        return (None, e)

    return (mod, None)

def loadmodule3(modpath, modname):
    import importlib.machinery

    try:
        loader = importlib.machinery.SourceFileLoader(modname, modpath)
        mod = loader.load_module()
    except Exception, e:
        return (None, e)

    return (mod, None)

def getstrategy(args):
    sttokens = args.strategy.split(':')

    if len(sttokens) == 1:
        modpath = sttokens[0]
        stname = None
    else:
        modpath, stname = sttokens

    if modpath:
        mod, e = loadmodule(modpath)

        if not mod:
            print('')
            print('Failed to load module %s:' % modpath, e)
            sys.exit(1)
    else:
        mod = btstrats

    strats = getmodclasses(mod=mod, clstype=bt.Strategy, clsname=stname)

    if not strats:
        print('No strategy %s / module %s' % (str(stname), modpath))
        sys.exit(1)

    return strats[0]

def getanalyzers(args):
    analyzers = list()
    for anspec in args.analyzers or []:

        tokens = anspec.split(':')

        if len(tokens) == 1:
            modpath = tokens[0]
            name = None
        else:
            modpath, name = tokens

        if modpath:
            mod, e = loadmodule(modpath)

            if not mod:
                print('')
                print('Failed to load module %s:' % modpath, e)
                sys.exit(1)
        else:
            mod = btanalyzers

        loaded = getmodclasses(mod=mod, clstype=bt.Analyzer, clsname=name)

        if not loaded:
            print('No analyzer %s / module %s' % ((str(name), modpath)))
            sys.exit(1)

        analyzers.extend(loaded)

    return analyzers

def getobservers(args):
    observers = list()
    for obspec in args.observers or []:

        tokens = obspec.split(':')

        if len(tokens) == 1:
            modpath = tokens[0]
            name = None
        else:
            modpath, name = tokens

        if modpath:
            mod, e = loadmodule(modpath)

            if not mod:
                print('')
                print('Failed to load module %s:' % modpath, e)
                sys.exit(1)
        else:
            mod = btobs

        loaded = getmodclasses(mod=mod, clstype=bt.Observer, clsname=name)

        if not loaded:
            print('No observer %s / module %s' % ((str(name), modpath)))
            sys.exit(1)

        observers.extend(loaded)

    return observers

def parse_args():
    parser = argparse.ArgumentParser(
        description='Backtrader Run Script')

    group = parser.add_argument_group(title='Data options')
    # Data options
    group.add_argument('--data', '-d', action='append', required=True,
                       help='Data files to be added to the system')

    datakeys = list(DATAFORMATS.keys())
    group.add_argument('--csvformat', '-c', required=False,
                       default='btcsv', choices=datakeys,
                       help='CSV Format')

    group.add_argument('--fromdate', '-f', required=False, default=None,
                       help='Starting date in YYYY-MM-DD[THH:MM:SS] format')

    group.add_argument('--todate', '-t', required=False, default=None,
                       help='Ending date in YYYY-MM-DD[THH:MM:SS] format')

    # Module where to read the strategy from
    group = parser.add_argument_group(title='Strategy options')
    group.add_argument('--strategy', '-st', required=True,
                       help=('Module and strategy to load with format '
                             'module_path:strategy_name.\n'
                             '\n'
                             'module_path:strategy_name will load '
                             'strategy_name from the given module_path\n'
                             '\n'
                             'module_path will load the module and return '
                             'the first available strategy in the module\n'
                             '\n'
                             ':strategy_name will load the given strategy '
                             'from the set of built-in strategies'))

    # Observers
    group = parser.add_argument_group(title='Observers and statistics')
    group.add_argument('--nostdstats', action='store_true',
                       help='Disable the standard statistics observers')

    group.add_argument('--observer', '-ob', dest='observers',
                       action='append', required=False,
                       help=('This option can be specified multiple times\n'
                             '\n'
                             'Module and observer to load with format '
                             'module_path:observer_name.\n'
                             '\n'
                             'module_path:observer_name will load '
                             'observer_name from the given module_path\n'
                             '\n'
                             'module_path will load the module and return '
                             'all available observers in the module\n'
                             '\n'
                             ':observer_name will load the given strategy '
                             'from the set of built-in strategies'))

    # Anaylzers
    group = parser.add_argument_group(title='Analyzers')
    group.add_argument('--analyzer', '-an', dest='analyzers',
                       action='append', required=False,
                       help=('This option can be specified multiple times\n'
                             '\n'
                             'Module and analyzer to load with format '
                             'module_path:analzyer_name.\n'
                             '\n'
                             'module_path:analyzer_name will load '
                             'observer_name from the given module_path\n'
                             '\n'
                             'module_path will load the module and return '
                             'all available analyzers in the module\n'
                             '\n'
                             ':anaylzer_name will load the given strategy '
                             'from the set of built-in strategies'))

    # Broker/Commissions
    group = parser.add_argument_group(title='Cash and Commission Scheme Args')
    group.add_argument('--cash', '-cash', required=False, type=float,
                       help='Cash to set to the broker')
    group.add_argument('--commission', '-comm', required=False, type=float,
                       help='Commission value to set')
    group.add_argument('--margin', '-marg', required=False, type=float,
                       help='Margin type to set')

    group.add_argument('--mult', '-mul', required=False, type=float,
                       help='Multiplier to use')

    # Plot options
    group = parser.add_argument_group(title='Plotting options')
    group.add_argument('--noplot', '-np', action='store_true', required=False,
                       help='Do not plot the read data')

    group.add_argument('--plotstyle', '-ps', required=False, default='bar',
                       choices=['bar', 'line', 'candle'],
                       help='Plot style for the input data')

    group.add_argument('--plotfigs', '-pn', required=False, default=1,
                       type=int, help='Plot using n figures')

    # Extra arguments
    parser.add_argument('args', nargs=argparse.REMAINDER,
                        help='args to pass to the loaded strategy')

    return parser.parse_args()

if __name__ == '__main__':
    runstrat()

觀察者和統計

原文:www.backtrader.com/blog/posts/2015-08-12-observers-and-statistics/observers-and-statistics/

執行在 backtrader 內部的策略主要處理資料指標

資料被新增到Cerebro例項中,並最終成為策略的輸入的一部分(被解析並作為例項的屬性提供),而指標是由策略本身宣告和管理的。

到目前為止,backtrader 的所有示例圖表都有 3 個似乎被視為理所當然的東西,因為它們沒有在任何地方宣告:

  • 現金和價值(經紀人的資金情況)

  • 交易(也稱為操作)

  • 買入/賣出訂單

它們是觀察者,存在於子模組backtrader.observers中。它們在那裡是因為Cerebro支援一個引數,可以自動將它們新增(或不新增)到策略中:

  • stdstats(預設值:True)

如果預設值被遵守,Cerebro執行以下等效使用者程式碼:

import backtrader as bt

...

cerebro = bt.Cerebro()  # default kwarg: stdstats=True

cerebro.addobserver(backtrader.observers.Broker)
cerebro.addobserver(backtrader.observers.Trades)
cerebro.addobserver(backtrader.observers.BuySell)

讓我們看看具有這 3 個預設觀察者的通常圖表(即使沒有發出訂單,因此沒有交易發生,也沒有現金和投資組合價值的變化)

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import backtrader as bt
import backtrader.feeds as btfeeds

if __name__ == '__main__':
    cerebro = bt.Cerebro(stdstats=False)
    cerebro.addstrategy(bt.Strategy)

    data = bt.feeds.BacktraderCSVData(dataname='../datas/2006-day-001.txt')
    cerebro.adddata(data)

    cerebro.run()
    cerebro.plot()

image

現在讓我們在建立Cerebro例項時將stdstats的值更改為False(也可以在呼叫run時完成):

cerebro = bt.Cerebro(stdstats=False)

現在圖表不同了。

image

訪問觀察者

如上所述,觀察者已經存在於預設情況下,並收集可用於統計目的的資訊,這就是為什麼可以透過策略的一個屬性來訪問觀察者的原因:

  • stats

它只是一個佔位符。如果我們回想一下如何新增預設觀察者之一,就像上面描述的那樣:

...
cerebro.addobserver(backtrader.observers.Broker)
...

顯而易見的問題是如何訪問Broker觀察者。以下是一個示例,展示瞭如何從策略的next方法中完成這個操作:

class MyStrategy(bt.Strategy):

    def next(self):

        if self.stats.broker.value[0] < 1000.0:
           print('WHITE FLAG ... I LOST TOO MUCH')
        elif self.stats.broker.value[0] > 10000000.0:
           print('TIME FOR THE VIRGIN ISLANDS ....!!!')

Broker觀察者就像一個資料、一個指標和策略本身一樣,也是一個Lines物件。在這種情況下,Broker有 2 條線:

  • cash

  • value

觀察者實現

實現非常類似於指標的實現:

class Broker(Observer):
    alias = ('CashValue',)
    lines = ('cash', 'value')

    plotinfo = dict(plot=True, subplot=True)

    def next(self):
        self.lines.cash[0] = self._owner.broker.getcash()
        self.lines.value[0] = value = self._owner.broker.getvalue()

步驟:

  • Observer派生(而不是從Indicator派生)

  • 根據需要宣告線和引數(Broker有 2 條線但沒有引數)

  • 將會有一個自動屬性_owner,它是持有觀察者的策略。

觀察者開始行動:

  • 所有指標計算完成後

  • 策略的next方法執行完成後

  • 這意味著:在週期結束時...他們觀察發生了什麼

Broker情況下,它只是盲目地記錄了每個時間點的經紀人現金和投資組合價值。

將觀察者新增到策略中

如上所指出,Cerebro 使用stdstats引數來決定是否新增 3 個預設的觀察者,減輕了終端使用者的工作量。

將其他觀察者新增到混合中是可能的,無論是沿著stdstats還是移除那些。

讓我們繼續使用通常的策略,當close價格高於SimpleMovingAverage時購買,反之亦然時賣出。

有一個“新增”:

  • DrawDown,這是backtrader生態系統中已經存在的觀察者
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import argparse
import datetime
import os.path
import time
import sys

import backtrader as bt
import backtrader.feeds as btfeeds
import backtrader.indicators as btind

class MyStrategy(bt.Strategy):
    params = (('smaperiod', 15),)

    def log(self, txt, dt=None):
        ''' Logging function fot this strategy'''
        dt = dt or self.data.datetime[0]
        if isinstance(dt, float):
            dt = bt.num2date(dt)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):

視覺輸出顯示了回撤的演變

image

以及部分文字輸出:

...
2006-12-14T23:59:59+00:00, MaxDrawDown: 2.62
2006-12-15T23:59:59+00:00, DrawDown: 0.22
2006-12-15T23:59:59+00:00, MaxDrawDown: 2.62
2006-12-18T23:59:59+00:00, DrawDown: 0.00
2006-12-18T23:59:59+00:00, MaxDrawDown: 2.62
2006-12-19T23:59:59+00:00, DrawDown: 0.00
2006-12-19T23:59:59+00:00, MaxDrawDown: 2.62
2006-12-20T23:59:59+00:00, DrawDown: 0.10
2006-12-20T23:59:59+00:00, MaxDrawDown: 2.62
2006-12-21T23:59:59+00:00, DrawDown: 0.39
2006-12-21T23:59:59+00:00, MaxDrawDown: 2.62
2006-12-22T23:59:59+00:00, DrawDown: 0.21
2006-12-22T23:59:59+00:00, MaxDrawDown: 2.62
2006-12-27T23:59:59+00:00, DrawDown: 0.28
2006-12-27T23:59:59+00:00, MaxDrawDown: 2.62
2006-12-28T23:59:59+00:00, DrawDown: 0.65
2006-12-28T23:59:59+00:00, MaxDrawDown: 2.62
2006-12-29T23:59:59+00:00, DrawDown: 0.06
2006-12-29T23:59:59+00:00, MaxDrawDown: 2.62

注意

如文字輸出和程式碼中所見,DrawDown觀察者實際上有 2 行:

  • drawdown

  • maxdrawdown

選擇不繪製maxdrawdown線,但仍然使其對使用者可用。

實際上,maxdrawdown的最後一個值也可以透過名為maxdd的直接屬性(而不是一行)獲得

開發觀察者

上面展示了Broker觀察者的實現。為了生成有意義的觀察者,實現可以使用以下資訊:

  • self._owner是當前執行的策略

    因此,觀察者可以訪問策略中的任何內容

  • 策略中可用的預設內部內容可能會有用:

    • broker -> 屬性,提供對策略建立訂單的經紀人例項的訪問

    如在Broker中所見,透過呼叫getcashgetvalue方法收集現金和投資組合價值

    • _orderspending -> 列出由策略建立並經紀人已通知策略的事件的訂單。

    BuySell觀察者遍歷列表,尋找已執行(完全或部分)的訂單,以建立給定時間點(索引 0)的平均執行價格

    • _tradespending -> 交易列表(一組已完成的買入/賣出或賣出/買入對),從買入/賣出訂單編譯而成

Observer顯然可以透過self._owner.stats路徑訪問其他觀察者。

自定義OrderObserver

標準的BuySell觀察者只關心已執行的操作。我們可以建立一個觀察者,顯示訂單何時建立以及是否已過期。

為了可見性,顯示將不沿價格繪製,而是在單獨的軸上。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import math

import backtrader as bt

class OrderObserver(bt.observer.Observer):
    lines = ('created', 'expired',)

    plotinfo = dict(plot=True, subplot=True, plotlinelabels=True)

    plotlines = dict(
        created=dict(marker='*', markersize=8.0, color='lime', fillstyle='full'),
        expired=dict(marker='s', markersize=8.0, color='red', fillstyle='full')
    )

    def next(self):
        for order in self._owner._orderspending:
            if order.data is not self.data:
                continue

            if not order.isbuy():
                continue

            # Only interested in "buy" orders, because the sell orders
            # in the strategy are Market orders and will be immediately
            # executed

            if order.status in [bt.Order.Accepted, bt.Order.Submitted]:
                self.lines.created[0] = order.created.price

            elif order.status in [bt.Order.Expired]:
                self.lines.expired[0] = order.created.price

自定義觀察者只關心買入訂單,因為這是一個只購買以試圖獲利的策略。賣出訂單是市價訂單,將立即執行。

Close-SMA CrossOver 策略已更改為:

  • 建立一個限價訂單,價格低於訊號時的收盤價的 1.0%

  • 訂單有效期為 7(日曆)天

結果圖表。

image

如新子圖中所見,有幾個訂單已過期(紅色方塊),我們還可以看到在“建立”和“執行”之間有幾天的時間。

注意

從提交 1560fa8802 開始,在 development 分支中,如果在訂單建立時價格未設定,則會使用收盤價格作為參考價格。

這不會影響市場訂單,但始終保持 order.create.price 可用,並簡化了 buy 的使用。

最後,應用新的觀察者的策略程式碼。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import datetime

import backtrader as bt
import backtrader.feeds as btfeeds
import backtrader.indicators as btind

from orderobserver import OrderObserver

class MyStrategy(bt.Strategy):
    params = (
        ('smaperiod', 15),
        ('limitperc', 1.0),
        ('valid', 7),
    )

    def log(self, txt, dt=None):
        ''' Logging function fot this strategy'''
        dt = dt or self.data.datetime[0]
        if isinstance(dt, float):
            dt = bt.num2date(dt)
        print('%s, %s' % (dt.isoformat(), txt))

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            self.log('ORDER ACCEPTED/SUBMITTED', dt=order.created.dt)
            self.order = order
            return

        if order.status in [order.Expired]:
            self.log('BUY EXPIRED')

        elif order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order.executed.price,
                     order.executed.value,
                     order.executed.comm))

            else:  # Sell
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))

        # Sentinel to None: new orders allowed
        self.order = None

    def __init__(self):
        # SimpleMovingAverage on main data
        # Equivalent to -> sma = btind.SMA(self.data, period=self.p.smaperiod)
        sma = btind.SMA(period=self.p.smaperiod)

        # CrossOver (1: up, -1: down) close / sma
        self.buysell = btind.CrossOver(self.data.close, sma, plot=True)

        # Sentinel to None: new ordersa allowed
        self.order = None

    def next(self):
        if self.order:
            # pending order ... do nothing
            return

        # Check if we are in the market
        if self.position:
            if self.buysell < 0:
                self.log('SELL CREATE, %.2f' % self.data.close[0])
                self.sell()

        elif self.buysell > 0:
            plimit = self.data.close[0] * (1.0 - self.p.limitperc / 100.0)
            valid = self.data.datetime.date(0) + \
                datetime.timedelta(days=self.p.valid)
            self.log('BUY CREATE, %.2f' % plimit)
            self.buy(exectype=bt.Order.Limit, price=plimit, valid=valid)

def runstrat():
    cerebro = bt.Cerebro()

    data = bt.feeds.BacktraderCSVData(dataname='../datas/2006-day-001.txt')
    cerebro.adddata(data)

    cerebro.addobserver(OrderObserver)

    cerebro.addstrategy(MyStrategy)
    cerebro.run()

    cerebro.plot()

if __name__ == '__main__':
    runstrat()

儲存/保持統計資訊。

目前為止,backtrader 還沒有實現任何跟蹤觀察者值並將它們儲存到檔案中的機制。最好的方法是:

  • 在策略的 start 方法中開啟一個檔案。

  • 在策略的 next 方法中寫入值。

考慮到 DrawDown 觀察者,可以這樣做:

class MyStrategy(bt.Strategy):

    def start(self):

        self.mystats = open('mystats.csv', 'wb')
        self.mystats.write('datetime,drawdown, maxdrawdown\n')

    def next(self):
        self.mystats.write(self.data.datetime.date(0).strftime('%Y-%m-%d'))
        self.mystats.write(',%.2f' % self.stats.drawdown.drawdown[-1])
        self.mystats.write(',%.2f' % self.stats.drawdown.maxdrawdown-1])
        self.mystats.write('\n')

要儲存索引 0 的值,在所有觀察者都被處理後,可以將一個自定義觀察者新增到系統中作為最後一個觀察者,將值儲存到一個 csv 檔案中。

資料來源開發

原文:www.backtrader.com/blog/posts/2015-08-11-datafeed-development/datafeed-development/

新增一個基於 CSV 的新資料來源很容易。現有的基類 CSVDataBase 提供了框架,大部分工作都由子類完成,這在大多數情況下可以簡單地完成:

def _loadline(self, linetokens):

  # parse the linetokens here and put them in self.lines.close,
  # self.lines.high, etc

  return True # if data was parsed, else ... return False

這在 CSV 資料來源開發中已經展示過了。

基類負責引數、初始化、檔案開啟、讀取行、將行拆分為標記以及跳過不符合使用者定義的日期範圍(fromdatetodate)的行等其他事項。

開發非 CSV 資料來源遵循相同的模式,而不需要深入到已拆分的行標記。

要做的事情:

  • 源自 backtrader.feed.DataBase

  • 新增任何可能需要的引數

  • 如果需要初始化,請重寫__init__(self)和/或start(self)

  • 如果需要任何清理程式碼,請重寫stop(self)

  • 工作發生在必須始終被重寫的方法內:_load(self)

讓我們使用backtrader.feed.DataBase已經提供的引數:

class DataBase(six.with_metaclass(MetaDataBase, dataseries.OHLCDateTime)):

    params = (('dataname', None),
        ('fromdate', datetime.datetime.min),
        ('todate', datetime.datetime.max),
        ('name', ''),
        ('compression', 1),
        ('timeframe', TimeFrame.Days),
        ('sessionend', None))

具有���下含義:

  • dataname是允許資料來源識別如何獲取資料的內容。在CSVDataBase的情況下,此引數應該是檔案的路徑或已經是類似檔案的物件。

  • fromdatetodate定義了將傳遞給策略的日期範圍。資料來源提供的任何超出此範圍的值將被忽略

  • name是用於繪圖目的的裝飾性內容

  • timeframecompression是裝飾性和資訊性的。它們在資料重取樣和資料重播中真正發揮作用。

  • 如果傳遞了sessionend(一個 datetime.time 物件),它將被新增到資料來源的datetime行中,從而可以識別會話結束的時間

示例二進位制資料來源

backtrader已經為VisualChart的匯出定義了一個 CSV 資料來源(VChartCSVData),但也可以直接讀取二進位制資料檔案。

讓我們開始吧(完整的資料來源程式碼可以在底部找到)

初始化

二進位制 VisualChart 資料檔案可以包含每日資料(.fd 副檔名)或分鐘資料(.min 副檔名)。這裡,資訊性引數timeframe將用於區分正在讀取的檔案型別。

__init__期間,為每種型別設定不同的常量。

 def __init__(self):
        super(VChartData, self).__init__()

        # Use the informative "timeframe" parameter to understand if the
        # code passed as "dataname" refers to an intraday or daily feed
        if self.p.timeframe >= TimeFrame.Days:
            self.barsize = 28
            self.dtsize = 1
            self.barfmt = 'IffffII'
        else:
            self.dtsize = 2
            self.barsize = 32
            self.barfmt = 'IIffffII'

開始

當開始回測時,資料來源將啟動(在最佳化過程中實際上可以啟動多次)

start方法中,除非傳遞了類似檔案的物件,否則將開啟二進位制檔案。

 def start(self):
        # the feed must start ... get the file open (or see if it was open)
        self.f = None
        if hasattr(self.p.dataname, 'read'):
            # A file has been passed in (ex: from a GUI)
            self.f = self.p.dataname
        else:
            # Let an exception propagate
            self.f = open(self.p.dataname, 'rb')

停止

當回測完成時呼叫。

如果檔案已開啟,則將關閉

 def stop(self):
        # Close the file if any
        if self.f is not None:
            self.f.close()
            self.f = None

實際載入

實際工作是在_load中完成的。呼叫以載入下一組資料,此處為下一個:日期時間、開盤價、最高價、最低價、收盤價、成交量、持倉量。在backtrader中,“實際”時刻對應於索引 0。

一定數量的位元組將從開啟的檔案中讀取(由__init__期間設定的常量確定),使用struct模組解析,如果需要進一步處理(例如使用 divmod 操作處理日期和時間),則儲存在資料來源的lines中:日期時間、開盤價、最高價、最低價、收盤價、成交量、持倉量。

如果無法從檔案中讀取任何資料,則假定已達到檔案結尾(EOF)。

  • 返回False表示無更多資料可用的事實

否則,如果資料已載入並解析:

  • 返回True表示資料集載入成功
 def _load(self):
        if self.f is None:
            # if no file ... no parsing
            return False

        # Read the needed amount of binary data
        bardata = self.f.read(self.barsize)
        if not bardata:
            # if no data was read ... game over say "False"
            return False

        # use struct to unpack the data
        bdata = struct.unpack(self.barfmt, bardata)

        # Years are stored as if they had 500 days
        y, md = divmod(bdata[0], 500)
        # Months are stored as if they had 32 days
        m, d = divmod(md, 32)
        # put y, m, d in a datetime
        dt = datetime.datetime(y, m, d)

        if self.dtsize > 1:  # Minute Bars
            # Daily Time is stored in seconds
            hhmm, ss = divmod(bdata[1], 60)
            hh, mm = divmod(hhmm, 60)
            # add the time to the existing atetime
            dt = dt.replace(hour=hh, minute=mm, second=ss)

        self.lines.datetime[0] = date2num(dt)

        # Get the rest of the unpacked data
        o, h, l, c, v, oi = bdata[self.dtsize:]
        self.lines.open[0] = o
        self.lines.high[0] = h
        self.lines.low[0] = l
        self.lines.close[0] = c
        self.lines.volume[0] = v
        self.lines.openinterest[0] = oi

        # Say success
        return True

其他二進位制格式

相同的模型可以應用於任何其他二進位制源:

  • 資料庫

  • 分層資料儲存

  • 線上資料來源

步驟再次說明:

  • __init__ -> 例項的任何初始化程式碼,僅執行一次

  • start -> 回測的開始(如果將進行最佳化,則可能發生一次或多次)

    例如,這將開啟與資料庫的連線或與線上服務的套接字連線。

  • stop -> 清理工作,如關閉資料庫連線或開啟套接字

  • _load -> 查詢資料庫或線上資料來源以獲取下一組資料,並將其載入到物件的lines中。標準欄位包括:日期時間、開盤價、最高價、最低價、收盤價、成交量、持倉量

VChartData 測試

VCharData 從本地“.fd”檔案載入谷歌 2006 年的資料。

這隻涉及載入資料,因此甚至不需要Strategy的子類。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import datetime

import backtrader as bt
from vchart import VChartData

if __name__ == '__main__':
    # Create a cerebro entity
    cerebro = bt.Cerebro(stdstats=False)

    # Add a strategy
    cerebro.addstrategy(bt.Strategy)

    # Create a Data Feed
    datapath = '../datas/goog.fd'
    data = VChartData(
        dataname=datapath,
        fromdate=datetime.datetime(2006, 1, 1),
        todate=datetime.datetime(2006, 12, 31),
        timeframe=bt.TimeFrame.Days
    )

    # Add the Data Feed to Cerebro
    cerebro.adddata(data)

    # Run over everything
    cerebro.run()

    # Plot the result
    cerebro.plot(style='bar')

image

VChartData 完整程式碼

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import datetime
import struct

from backtrader.feed import DataBase
from backtrader import date2num
from backtrader import TimeFrame

class VChartData(DataBase):
    def __init__(self):
        super(VChartData, self).__init__()

        # Use the informative "timeframe" parameter to understand if the
        # code passed as "dataname" refers to an intraday or daily feed
        if self.p.timeframe >= TimeFrame.Days:
            self.barsize = 28
            self.dtsize = 1
            self.barfmt = 'IffffII'
        else:
            self.dtsize = 2
            self.barsize = 32
            self.barfmt = 'IIffffII'

    def start(self):
        # the feed must start ... get the file open (or see if it was open)
        self.f = None
        if hasattr(self.p.dataname, 'read'):
            # A file has been passed in (ex: from a GUI)
            self.f = self.p.dataname
        else:
            # Let an exception propagate
            self.f = open(self.p.dataname, 'rb')

    def stop(self):
        # Close the file if any
        if self.f is not None:
            self.f.close()
            self.f = None

    def _load(self):
        if self.f is None:
            # if no file ... no parsing
            return False

        # Read the needed amount of binary data
        bardata = self.f.read(self.barsize)
        if not bardata:
            # if no data was read ... game over say "False"
            return False

        # use struct to unpack the data
        bdata = struct.unpack(self.barfmt, bardata)

        # Years are stored as if they had 500 days
        y, md = divmod(bdata[0], 500)
        # Months are stored as if they had 32 days
        m, d = divmod(md, 32)
        # put y, m, d in a datetime
        dt = datetime.datetime(y, m, d)

        if self.dtsize > 1:  # Minute Bars
            # Daily Time is stored in seconds
            hhmm, ss = divmod(bdata[1], 60)
            hh, mm = divmod(hhmm, 60)
            # add the time to the existing atetime
            dt = dt.replace(hour=hh, minute=mm, second=ss)

        self.lines.datetime[0] = date2num(dt)

        # Get the rest of the unpacked data
        o, h, l, c, v, oi = bdata[self.dtsize:]
        self.lines.open[0] = o
        self.lines.high[0] = h
        self.lines.low[0] = l
        self.lines.close[0] = c
        self.lines.volume[0] = v
        self.lines.openinterest[0] = oi

        # Say success
        return True

相關文章