BackTrader 中文文件(二十六)

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

原文:www.backtrader.com/

條形圖同步

www.backtrader.com/blog/posts/2015-10-04-bar-synchronization/bar-synchronization/

文獻和/或行業中缺乏標準公式並不是問題,因為問題實際上可以總結為:

  • 條形圖同步

工單 #23 提出了一些關於backtrader是否可以計算相對成交量指標的疑問。

請求者需要比較特定時刻的成交量與前一個交易日相同時刻的成交量。包括:

  • 一些未知長度的預市資料

有這樣的要求會使大多數指標構建的基本原則無效:

  • 有一個固定的用於向後檢視的週期

此外,考慮到比較是在盤內進行的,還必須考慮其他因素:

  • 一些“盤內”瞬間可能會缺失(無論是分鐘還是秒)

    資料來源缺失每日條形圖的可能性很小,但缺失分鐘或秒條形圖並不罕見。

    主要原因是可能根本沒有進行任何交易。或者在交易所談判中可能出現問題,實際上阻止了條形圖被記錄。

考慮到前述所有要點,對指標開發得出一些結論:

  • 這裡的週期不是指一個週期,而是一個緩衝區,以確保有足夠的條形圖使指標儘快生效

  • 一些條形圖可能會缺失

  • 主要問題是同步

幸運的是,有一個關鍵可以幫助解決同步問題:

  • 比較的條形圖是“盤內”的,因此計算已經看到的天數和給定時刻已經看到的“條形圖”數量可以實現同步

前一天的值儲存在字典中,因為如前所述的“向後檢視”期限是未知的。

一些早期的想法可以被拋棄,比如實現一個DataFilter資料來源,因為這實際上會使資料來源與系統的其他部分不同步,透過刪除預市資料。同步問題也會存在。

探索的一個想法是建立一個DataFiller,透過使用最後的收盤價填補缺失的分鐘/秒,並將成交量設定為 0。

透過實踐發現,有必要在backtrader中識別一些額外需求,比如一個time2num函式(日期 2 數字和數字 2 日期系列的補充),以及將成為lines的額外方法:

  • 從浮點表示的日期中提取“日”和“時間”部分

    被稱為“dt”和“tm”

與此同時,RelativeVolumeByBar 指標的程式碼如下所示。在指標內部進行“period”/“buffer” 計算不是首選模式,但在這種情況下它能夠達到目的。

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

import collections
import datetime
import math

import backtrader as bt

def time2num(tm):
    """
    Convert :mod:`time` to the to the preserving hours, minutes, seconds
    and microseconds.  Return value is a :func:`float`.
    """
    HOURS_PER_DAY = 24.0
    MINUTES_PER_HOUR = 60.0
    SECONDS_PER_MINUTE = 60.0
    MUSECONDS_PER_SECOND = 1e6
    MINUTES_PER_DAY = MINUTES_PER_HOUR * HOURS_PER_DAY
    SECONDS_PER_DAY = SECONDS_PER_MINUTE * MINUTES_PER_DAY
    MUSECONDS_PER_DAY = MUSECONDS_PER_SECOND * SECONDS_PER_DAY

    tm_num = (tm.hour / HOURS_PER_DAY +
              tm.minute / MINUTES_PER_DAY +
              tm.second / SECONDS_PER_DAY +
              tm.microsecond / MUSECONDS_PER_DAY)

    return tm_num

def dtime_dt(dt):
    return math.trunc(dt)

def dtime_tm(dt):
    return math.modf(dt)[0]

class RelativeVolumeByBar(bt.Indicator):
    alias = ('RVBB',)
    lines = ('rvbb',)

    params = (
        ('prestart', datetime.time(8, 00)),
        ('start', datetime.time(9, 10)),
        ('end', datetime.time(17, 15)),
    )

    def _plotlabel(self):
        plabels = []
        for name, value in self.params._getitems():
            plabels.append('%s: %s' % (name, value.strftime('%H:%M')))

        return plabels

    def __init__(self):
        # Inform the platform about the minimum period needs
        minbuffer = self._calcbuffer()
        self.addminperiod(minbuffer)

        # Structures/variable to keep synchronization
        self.pvol = dict()
        self.vcount = collections.defaultdict(int)

        self.days = 0
        self.dtlast = 0

        # Keep the start/end times in numeric format for comparison
        self.start = time2num(self.p.start)
        self.end = time2num(self.p.end)

        # Done after calc to ensure coop inheritance and composition work
        super(RelativeVolumeByBar, self).__init__()

    def _barisvalid(self, tm):
        return self.start <= tm <= self.end

    def _daycount(self):
        dt = dtime_dt(self.data.datetime[0])
        if dt > self.dtlast:
            self.days += 1
            self.dtlast = dt

    def prenext(self):
        self._daycount()

        tm = dtime_tm(self.data.datetime[0])
        if self._barisvalid(tm):
            self.pvol[tm] = self.data.volume[0]
            self.vcount[tm] += 1

    def next(self):
        self._daycount()

        tm = dtime_tm(self.data.datetime[0])
        if not self._barisvalid(tm):
            return

        # Record the "minute/second" of this day has been seen
        self.vcount[tm] += 1

        # Get the bar's volume
        vol = self.data.volume[0]

        # If number of days is right, we saw the same "minute/second" last day
        if self.vcount[tm] == self.days:
            self.lines.rvbb[0] = vol / self.pvol[tm]

        # Synchronize the days and volume count for next cycle
        self.vcount[tm] = self.days

        # Record the volume for this bar for next cycle
        self.pvol[tm] = vol

    def _calcbuffer(self):
        # Period calculation
        minend = self.p.end.hour * 60 + self.p.end.minute
        # minstart = session_start.hour * 60 + session_start.minute
        # use prestart to account for market_data
        minstart = self.p.prestart.hour * 60 + self.p.prestart.minute

        minbuffer = minend - minstart

        tframe = self.data._timeframe
        tcomp = self.data._compression

        if tframe == bt.TimeFrame.Seconds:
            minbuffer = (minperiod * 60)

        minbuffer = (minbuffer // tcomp) + tcomp

        return minbuffer

透過指令碼呼叫,可以如下使用:

$ ./relative-volume.py --help
usage: relative-volume.py [-h] [--data DATA] [--prestart PRESTART]
                          [--start START] [--end END] [--fromdate FROMDATE]
                          [--todate TODATE] [--writer] [--wrcsv] [--plot]
                          [--numfigs NUMFIGS]

MultiData Strategy

optional arguments:
  -h, --help            show this help message and exit
  --data DATA, -d DATA  data to add to the system
  --prestart PRESTART   Start time for the Session Filter
  --start START         Start time for the Session Filter
  --end END, -te END    End time for the Session Filter
  --fromdate FROMDATE, -f FROMDATE
                        Starting date in YYYY-MM-DD format
  --todate TODATE, -t TODATE
                        Starting date in YYYY-MM-DD format
  --writer, -w          Add a writer to cerebro
  --wrcsv, -wc          Enable CSV Output in the writer
  --plot, -p            Plot the read data
  --numfigs NUMFIGS, -n NUMFIGS
                        Plot using numfigs figures

測試呼叫:

$ ./relative-volume.py --plot

生成此圖表:

圖片

指令碼程式碼。

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

import argparse
import datetime

# The above could be sent to an independent module
import backtrader as bt
import backtrader.feeds as btfeeds

from relvolbybar import RelativeVolumeByBar

def runstrategy():
    args = parse_args()

    # Create a cerebro
    cerebro = bt.Cerebro()

    # Get the dates from the args
    fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d')
    todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d')

    # Create the 1st data
    data = btfeeds.VChartCSVData(
        dataname=args.data,
        fromdate=fromdate,
        todate=todate,
        )

    # Add the 1st data to cerebro
    cerebro.adddata(data)

    # Add an empty strategy
    cerebro.addstrategy(bt.Strategy)

    # Get the session times to pass them to the indicator
    prestart = datetime.datetime.strptime(args.prestart, '%H:%M')
    start = datetime.datetime.strptime(args.start, '%H:%M')
    end = datetime.datetime.strptime(args.end, '%H:%M')

    # Add the Relative volume indicator
    cerebro.addindicator(RelativeVolumeByBar,
                         prestart=prestart, start=start, end=end)

    # Add a writer with CSV
    if args.writer:
        cerebro.addwriter(bt.WriterFile, csv=args.wrcsv)

    # And run it
    cerebro.run(stdstats=False)

    # Plot if requested
    if args.plot:
        cerebro.plot(numfigs=args.numfigs, volume=True)

def parse_args():
    parser = argparse.ArgumentParser(description='MultiData Strategy')

    parser.add_argument('--data', '-d',
                        default='../../datas/2006-01-02-volume-min-001.txt',
                        help='data to add to the system')

    parser.add_argument('--prestart',
                        default='08:00',
                        help='Start time for the Session Filter')

    parser.add_argument('--start',
                        default='09:15',
                        help='Start time for the Session Filter')

    parser.add_argument('--end', '-te',
                        default='17:15',
                        help='End time for the Session Filter')

    parser.add_argument('--fromdate', '-f',
                        default='2006-01-01',
                        help='Starting date in YYYY-MM-DD format')

    parser.add_argument('--todate', '-t',
                        default='2006-12-31',
                        help='Starting date in YYYY-MM-DD format')

    parser.add_argument('--writer', '-w', action='store_true',
                        help='Add a writer to cerebro')

    parser.add_argument('--wrcsv', '-wc', action='store_true',
                        help='Enable CSV Output in the writer')

    parser.add_argument('--plot', '-p', action='store_true',
                        help='Plot the read data')

    parser.add_argument('--numfigs', '-n', default=1,
                        help='Plot using numfigs figures')

    return parser.parse_args()

if __name__ == '__main__':
    runstrategy()

Tick 資料和重新取樣

原文:www.backtrader.com/blog/posts/2015-09-25-tickdata-resample/resample-tickdata/

backtrader 已經能夠從分鐘資料進行重新取樣。接受 tick 資料並不是問題,只需將 4 個常用欄位(open、high、low、close)設定為 tick 值即可。

但是將要重新取樣的 tick 資料傳遞給產生了相同的資料。從 1.1.11.88 版本開始不再是這樣。現在

  • TimeFrame(backtrader.TimeFrame)已經擴充套件,包含了“Ticks”、“MicroSeconds”和“Seconds”的常量和名稱。

  • 重新取樣可以管理這 3 個前述的時間框架並將其取樣。

注意

因為 tick 資料是最低可能的時間框架,它實際上可以被“壓縮”(n bars to 1 bar),但不能從最小的時間框架進行取樣。

新版本包含了一個小的tickdata.csv樣本,新增到了源資料中,以及一個新的樣本指令碼resample-tickdata.py來進行測試。

注意

更新了指令碼以使用新的Cerebro.resampledata方法,避免了手動例項化backtrader.DataResampler的需要

預設執行不會觸及資料:

$ ./resample-tickdata.py

生成這個圖表:

image

將 3 個 tick 壓縮為 1 個:

$ ./resample-tickdata.py --timeframe ticks --compression 3

生成這個圖表:

image

壓縮後,我們不再有單個的“ticks”,而是“bars”。

現在壓縮到秒和 5 個 bar 的壓縮:

$ ./resample-tickdata.py --timeframe seconds --compression 5

透過一個新的圖表:

image

最後轉換為分鐘。樣本資料包含來自 4 個不同分鐘的 tick 資料(檔案中的最後一個 tick 是第 4 分鐘的唯一一個 tick):

$ ./resample-tickdata.py --timeframe minutes

使用 4 個 bar(頂部可以看到最終價格為 3069)。第 4 個 bar 是一個單點,因為這一分鐘檔案中只有一個 tick。

image

指令碼用法:

$ ./resample-tickdata.py --help
usage: resample-tickdata.py [-h] [--dataname DATANAME]
                            [--timeframe {ticks,microseconds,seconds,minutes,daily,weekly,monthly}]
                            [--compression COMPRESSION]

Resampling script down to tick data

optional arguments:
  -h, --help            show this help message and exit
  --dataname DATANAME   File Data to Load
  --timeframe {ticks,microseconds,seconds,minutes,daily,weekly,monthly}
                        Timeframe to resample to
  --compression COMPRESSION
                        Compress n bars into 1

以及程式碼。

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/ticksample.csv'

    data = btfeeds.GenericCSVData(
        dataname=datapath,
        dtformat='%Y-%m-%dT%H:%M:%S.%f',
        timeframe=bt.TimeFrame.Ticks,
    )

    # Handy dictionary for the argument timeframe conversion
    tframes = dict(
        ticks=bt.TimeFrame.Ticks,
        microseconds=bt.TimeFrame.MicroSeconds,
        seconds=bt.TimeFrame.Seconds,
        minutes=bt.TimeFrame.Minutes,
        daily=bt.TimeFrame.Days,
        weekly=bt.TimeFrame.Weeks,
        monthly=bt.TimeFrame.Months)

    # Resample the data
    data = cerebro.resampledata(data,
                                timeframe=tframes[args.timeframe],
                                compression=args.compression)

    # add a writer
    cerebro.addwriter(bt.WriterFile, csv=True)

    # Run over everything
    cerebro.run()

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

def parse_args():
    parser = argparse.ArgumentParser(
        description='Resampling script down to tick data')

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

    parser.add_argument('--timeframe', default='ticks', required=False,
                        choices=['ticks', 'microseconds', 'seconds',
                                 'minutes', 'daily', 'weekly', 'monthly'],
                        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()

在同一軸線上繪圖。

原文:www.backtrader.com/blog/posts/2015-09-21-plotting-same-axis/plotting-same-axis/

根據部落格上的評論稍微增加了一點(幸運的是隻是幾行程式碼)來進行繪圖。

  • 能夠在任何其他指標上繪製任何指標。

一個潛在的用例:

  • 節省寶貴的螢幕空間,將一些指標繪製在一起,有更多的空間來欣賞 OHLC 柱狀圖。

    示例:將 Stochastic 和 RSI 繪圖合併。

當然,有些事情必須考慮進去:

  • 如果指標的縮放差異太大,一些指標可能不可見。

    示例:一個圍繞 0.0 加/減 0.5 波動的 MACD 繪製在一個橫跨 0-100 範圍的 Stochastic 上。

第一個實現在提交的開發分支上是 …14252c6

一個示例指令碼(見下面的完整程式碼)讓我們看到了效果。

注意

因為示例策略什麼都不做,標準觀察者被移除了,除非透過命令列開關啟用。

首先,指令碼在沒有任何開關的情況下執行。

  • 簡單移動平均線繪製在資料上。

  • MACD、Stochastic 和 RSI 分別繪製在各自的軸線/子圖上。

執行:

$ ./plot-same-axis.py

和圖表。

image

第二次執行改變了全景:

  • 簡單移動平均線移動到了一個子圖上。

  • MACD 被隱藏了。

  • RSI 繪製在 Stochastic 之上(y 軸範圍相容:0-100)。

    這透過將指標的plotinfo.plotmaster值設定為要繪製到的其他指標來實現。

    在這種情況下,由於__init__中的區域性變數被命名為stoc代表 Stochastic 和rsi代表 RSI,看起來像是:

    rsi.plotinfo.plotmaster = stoc` 
    

執行:

$ ./plot-same-axis.py --smasubplot --nomacdplot --rsioverstoc

圖表。

image

為了檢查尺度的不相容性,讓我們嘗試在 SMA 上繪製 RSI:

$ ./plot-same-axis.py --rsiovermacd

圖表。

image

RSI 標籤顯示出資料和 SMA,但是尺度在 3400-4200 範圍內,因此……RSI 沒有任何跡象。

進一步的徒勞嘗試是將 SMA 放在一個子圖上,然後再次在 SMA 上繪製 RSI。

$ ./plot-same-axis.py –rsiovermacd –smasubplot

圖表。

image

標籤清晰,但是除了 SMA 圖中底部的一條淡藍線外,RSI 的所有東西都消失了。

注意

新增了在另一個指標上繪製的多行指標。

沿著另一個方向,讓我們在另一個指標上繪製多行指標。讓我們將 Stochastic 繪製在 RSI 上:

$ ./plot-same-axis.py --stocrsi

image

它有效。Stochastic標籤顯示出來了,K%D%這兩條線也是如此。但是這些線沒有“名稱”,因為我們得到了指標的名稱。

在程式碼中,當前的設定將是:

stoc.plotinfo.plotmaster = rsi

要顯示隨機線的名稱而不是名稱,我們還需要:

stoc.plotinfo.plotlinelabels = True

這已經被引數化,新的執行結果顯示如下:

$ ./plot-same-axis.py --stocrsi --stocrsilabels

現在圖表顯示了隨機線的名稱在 RSI 線的名稱下方。

image

指令碼用法:

$ ./plot-same-axis.py --help
usage: plot-same-axis.py [-h] [--data DATA] [--fromdate FROMDATE]
                         [--todate TODATE] [--stdstats] [--smasubplot]
                         [--nomacdplot]
                         [--rsioverstoc | --rsioversma | --stocrsi]
                         [--stocrsilabels] [--numfigs NUMFIGS]

Plotting Example

optional arguments:
  -h, --help            show this help message and exit
  --data DATA, -d DATA  data to add to the system
  --fromdate FROMDATE, -f FROMDATE
                        Starting date in YYYY-MM-DD format
  --todate TODATE, -t TODATE
                        Starting date in YYYY-MM-DD format
  --stdstats, -st       Show standard observers
  --smasubplot, -ss     Put SMA on own subplot/axis
  --nomacdplot, -nm     Hide the indicator from the plot
  --rsioverstoc, -ros   Plot the RSI indicator on the Stochastic axis
  --rsioversma, -rom    Plot the RSI indicator on the SMA axis
  --stocrsi, -strsi     Plot the Stochastic indicator on the RSI axis
  --stocrsilabels       Plot line names instead of indicator name
  --numfigs NUMFIGS, -n NUMFIGS
                        Plot using numfigs figures

以及程式碼。

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

import argparse
import datetime

# The above could be sent to an independent module
import backtrader as bt
import backtrader.feeds as btfeeds
import backtrader.indicators as btind

class PlotStrategy(bt.Strategy):
    '''
    The strategy does nothing but create indicators for plotting purposes
    '''
    params = dict(
        smasubplot=False,  # default for Moving averages
        nomacdplot=False,
        rsioverstoc=False,
        rsioversma=False,
        stocrsi=False,
        stocrsilabels=False,
    )

    def __init__(self):
        sma = btind.SMA(subplot=self.params.smasubplot)

        macd = btind.MACD()
        # In SMA we passed plot directly as kwarg, here the plotinfo.plot
        # attribute is changed - same effect
        macd.plotinfo.plot = not self.params.nomacdplot

        # Let's put rsi on stochastic/sma or the other way round
        stoc = btind.Stochastic()
        rsi = btind.RSI()
        if self.params.stocrsi:
            stoc.plotinfo.plotmaster = rsi
            stoc.plotinfo.plotlinelabels = self.p.stocrsilabels
        elif self.params.rsioverstoc:
            rsi.plotinfo.plotmaster = stoc
        elif self.params.rsioversma:
            rsi.plotinfo.plotmaster = sma

def runstrategy():
    args = parse_args()

    # Create a cerebro
    cerebro = bt.Cerebro()

    # Get the dates from the args
    fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d')
    todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d')

    # Create the 1st data
    data = btfeeds.BacktraderCSVData(
        dataname=args.data,
        fromdate=fromdate,
        todate=todate)

    # Add the 1st data to cerebro
    cerebro.adddata(data)

    # Add the strategy
    cerebro.addstrategy(PlotStrategy,
                        smasubplot=args.smasubplot,
                        nomacdplot=args.nomacdplot,
                        rsioverstoc=args.rsioverstoc,
                        rsioversma=args.rsioversma,
                        stocrsi=args.stocrsi,
                        stocrsilabels=args.stocrsilabels)

    # And run it
    cerebro.run(stdstats=args.stdstats)

    # Plot
    cerebro.plot(numfigs=args.numfigs, volume=False)

def parse_args():
    parser = argparse.ArgumentParser(description='Plotting Example')

    parser.add_argument('--data', '-d',
                        default='../../datas/2006-day-001.txt',
                        help='data to add to the system')

    parser.add_argument('--fromdate', '-f',
                        default='2006-01-01',
                        help='Starting date in YYYY-MM-DD format')

    parser.add_argument('--todate', '-t',
                        default='2006-12-31',
                        help='Starting date in YYYY-MM-DD format')

    parser.add_argument('--stdstats', '-st', action='store_true',
                        help='Show standard observers')

    parser.add_argument('--smasubplot', '-ss', action='store_true',
                        help='Put SMA on own subplot/axis')

    parser.add_argument('--nomacdplot', '-nm', action='store_true',
                        help='Hide the indicator from the plot')

    group = parser.add_mutually_exclusive_group(required=False)

    group.add_argument('--rsioverstoc', '-ros', action='store_true',
                       help='Plot the RSI indicator on the Stochastic axis')

    group.add_argument('--rsioversma', '-rom', action='store_true',
                       help='Plot the RSI indicator on the SMA axis')

    group.add_argument('--stocrsi', '-strsi', action='store_true',
                       help='Plot the Stochastic indicator on the RSI axis')

    parser.add_argument('--stocrsilabels', action='store_true',
                        help='Plot line names instead of indicator name')

    parser.add_argument('--numfigs', '-n', default=1,
                        help='Plot using numfigs figures')

    return parser.parse_args()

if __name__ == '__main__':
    runstrategy()

Writers - 寫下來

原文:www.backtrader.com/blog/posts/2015-09-14-write-it-down/write-it-down/

隨著 1.1.7.88 版本的釋出,backtrader 增加了一個新的功能:writers

這可能是早該做的事情,應該一直都有,並且在問題#14的討論中也應該促進了發展。

遲做總比不做好。

Writer實現試圖與backtrader環境中的其他物件保持一致

  • 新增到 Cerebro

  • 提供最合理的預設值

  • 不要強迫使用者做太多事情

當然,更重要的是理解寫手實際上寫了什麼。 這就是:

  • CSV 輸出

    - `datas` added to the system (can be switched off)
    
    - `strategies` (a Strategy can have named lines)
    
    - `indicators` inside the strategies (only 1st level)
    
    - `observers` inside the strategies (only 1st level)
    
    Which `indicators` and `observers` output data to the CSV stream is
    controlled by the attribute:
    
      `csv` in each instance
    
    The defaults are:
    
      - Observers have `csv = True`
    
      - Indicators have `csv = False`
    
    The value can be overriden for any instance created inside a strategy` 
    

一旦回測階段結束,WritersCerebro例項新增一個新的部分,並新增以下子部分:

  • 系統中datas的屬性(名稱、壓縮、時間框架)

  • 系統中strategies的屬性(行、引數)

    • 策略中indicators的屬性(行、引數)

    • 策略中observers的屬性(行、引數)

    • 具有以下特性的分析器

    • 引數

    • 分析

在考慮所有這些的同時,一個例子可能是展示writers的功能(或弱點)的最簡單方法。

但是在瞭解如何將它們新增到 cerebro 之前。

  1. 使用writer引數到cerebro

    cerebro = bt.Cerebro(writer=True)` 
    

    這建立了一個預設例項。

  2. 具體新增:

    cerebro = bt.Cerebro()
    
    cerebro.addwriter(bt.WriterFile, csv=False)` 
    

    新增(現在是唯一的作者)一個WriterFile類到作者列表中,稍後用csv=False例項化(輸出中不會生成 csv 流。

具有多空策略的長期示例(請參閱下面的完整程式碼),使用 Close-SMA 交叉作為訊號進行執行:

$ ./writer-test.py

圖表:

image

具有以下輸出:

===============================================================================
Cerebro:
  -----------------------------------------------------------------------------
  - Datas:
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Data0:
      - Name: 2006-day-001
      - Timeframe: Days
      - Compression: 1
  -----------------------------------------------------------------------------
  - Strategies:
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - LongShortStrategy:
      *************************************************************************
      - Params:
        - csvcross: False
        - printout: False
        - onlylong: False
        - stake: 1
        - period: 15
      *************************************************************************
      - Indicators:
        .......................................................................
        - SMA:
          - Lines: sma
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Params:
            - period: 15
        .......................................................................
        - CrossOver:
          - Lines: crossover
          - Params: None
      *************************************************************************
      - Observers:
        .......................................................................
        - Broker:
          - Lines: cash, value
          - Params: None
        .......................................................................
        - BuySell:
          - Lines: buy, sell
          - Params: None
        .......................................................................
        - Trades:
          - Lines: pnlplus, pnlminus
          - Params: None
      *************************************************************************
      - Analyzers:
        .......................................................................
        - Value:
          - Begin: 100000
          - End: 100826.1
        .......................................................................
        - SQN:
          - Params: None
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Analysis:
            - sqn: 0.05
            - trades: 22

執行後,我們有了一個完整的摘要,顯示系統的設定以及分析器的意見。 在這種情況下,分析器是

  • Value是策略內部的一個虛擬分析器,它收集了組合的起始值和結束值

  • SQN(或 SystemQualityNumber)由 Van K. Tharp 定義(增加到backtrader 1.1.7.88,告訴我們它已經看到了 22 筆交易,並且計算了sqn為 0.05。

    這實際上相當低。 我們本可以透過觀察完整一年後的微薄利潤來發現這一點(幸運的是,系統不會虧損)

測試指令碼允許我們調整策略以變為僅多頭

$ ./writer-test.py --onlylong --plot

圖表:

image

現在的輸出是:

===============================================================================
Cerebro:
  -----------------------------------------------------------------------------
  - Datas:
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - Data0:
      - Name: 2006-day-001
      - Timeframe: Days
      - Compression: 1
  -----------------------------------------------------------------------------
  - Strategies:
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    - LongShortStrategy:
      *************************************************************************
      - Params:
        - csvcross: False
        - printout: False
        - onlylong: True
        - stake: 1
        - period: 15
      *************************************************************************
      - Indicators:
        .......................................................................
        - SMA:
          - Lines: sma
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Params:
            - period: 15
        .......................................................................
        - CrossOver:
          - Lines: crossover
          - Params: None
      *************************************************************************
      - Observers:
        .......................................................................
        - Broker:
          - Lines: cash, value
          - Params: None
        .......................................................................
        - BuySell:
          - Lines: buy, sell
          - Params: None
        .......................................................................
        - Trades:
          - Lines: pnlplus, pnlminus
          - Params: None
      *************************************************************************
      - Analyzers:
        .......................................................................
        - Value:
          - Begin: 100000
          - End: 102795.0
        .......................................................................
        - SQN:
          - Params: None
          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          - Analysis:
            - sqn: 0.91
            - trades: 11

“params”對策略的更改可見(onlylong 已轉為 True),分析器講述了一個不同的故事:

  • 結束值從 100826.1 提高到 102795.0

  • SQN 看到的交易從 22 減少到 11

  • SQN 分數從 0.05 增長到 0.91,好得多得多

但是仍然看不到 CSV 輸出。 讓我們執行指令碼來開啟它:

$ ./writer-test.py --onlylong --writercsv

帶有更新的輸出:

===============================================================================
Id,2006-day-001,len,datetime,open,high,low,close,volume,openinterest,LongShortStrategy,len,Broker,len,cash,value,Buy
Sell,len,buy,sell,Trades,len,pnlplus,pnlminus
1,2006-day-001,1,2006-01-02 23:59:59+00:00,3578.73,3605.95,3578.73,3604.33,0.0,0.0,LongShortStrategy,1,Broker,1,1000
00.0,100000.0,BuySell,1,,,Trades,1,,
2,2006-day-001,2,2006-01-03 23:59:59+00:00,3604.08,3638.42,3601.84,3614.34,0.0,0.0,LongShortStrategy,2,Broker,2,1000
00.0,100000.0,BuySell,2,,,Trades,2,,
...
...
...
255,2006-day-001,255,2006-12-29 23:59:59+00:00,4130.12,4142.01,4119.94,4119.94,0.0,0.0,LongShortStrategy,255,Broker,255,100795.0,102795.0,BuySell,255,,,Trades,255,,
===============================================================================
Cerebro:
  -----------------------------------------------------------------------------
...
...

我們可以跳過大部分的 CSV 流和已經看過的摘要。CSV 流已經列印出以下內容

  • 一個部分線分隔符在開頭

  • 一個標題行

  • 相應的資料

注意每個物件的“長度”是如何被列印出來的。雖然在這種情況下它並沒有提供太多資訊,但如果使用了多時間框資料或者資料被重播,它將提供資訊。

writer 的預設設定如下:

  • 沒有指標被列印出來(既沒有簡單移動平均線也沒有交叉點)

  • 觀察者被列印出來

讓我們執行指令碼並附加一個引數,將 CrossOver 指示器新增到 CSV 流中:

$ ./writer-test.py --onlylong --writercsv --csvcross

輸出:

===============================================================================
Id,2006-day-001,len,datetime,open,high,low,close,volume,openinterest,LongShortStrategy,len,CrossOver,len,crossover,B
roker,len,cash,value,BuySell,len,buy,sell,Trades,len,pnlplus,pnlminus
1,2006-day-001,1,2006-01-02 23:59:59+00:00,3578.73,3605.95,3578.73,3604.33,0.0,0.0,LongShortStrategy,1,CrossOver,1,,
Broker,1,100000.0,100000.0,BuySell,1,,,Trades,1,,
...
...

這展示了寫入器的一些功能。該類的進一步文件仍然是一個待辦事項。

與此同時是執行可能性和用於示例的程式碼。

用法:

$ ./writer-test.py --help
usage: writer-test.py [-h] [--data DATA] [--fromdate FROMDATE]
                      [--todate TODATE] [--period PERIOD] [--onlylong]
                      [--writercsv] [--csvcross] [--cash CASH] [--comm COMM]
                      [--mult MULT] [--margin MARGIN] [--stake STAKE] [--plot]
                      [--numfigs NUMFIGS]

MultiData Strategy

optional arguments:
  -h, --help            show this help message and exit
  --data DATA, -d DATA  data to add to the system
  --fromdate FROMDATE, -f FROMDATE
                        Starting date in YYYY-MM-DD format
  --todate TODATE, -t TODATE
                        Starting date in YYYY-MM-DD format
  --period PERIOD       Period to apply to the Simple Moving Average
  --onlylong, -ol       Do only long operations
  --writercsv, -wcsv    Tell the writer to produce a csv stream
  --csvcross            Output the CrossOver signals to CSV
  --cash CASH           Starting Cash
  --comm COMM           Commission for operation
  --mult MULT           Multiplier for futures
  --margin MARGIN       Margin for each future
  --stake STAKE         Stake to apply in each operation
  --plot, -p            Plot the read data
  --numfigs NUMFIGS, -n NUMFIGS
                        Plot using numfigs figures

和測試指令碼。

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

import argparse
import datetime

# The above could be sent to an independent module
import backtrader as bt
import backtrader.feeds as btfeeds
import backtrader.indicators as btind
from backtrader.analyzers import SQN

class LongShortStrategy(bt.Strategy):
    '''This strategy buys/sells upong the close price crossing
    upwards/downwards a Simple Moving Average.

    It can be a long-only strategy by setting the param "onlylong" to True
    '''
    params = dict(
        period=15,
        stake=1,
        printout=False,
        onlylong=False,
        csvcross=False,
    )

    def start(self):
        pass

    def stop(self):
        pass

    def log(self, txt, dt=None):
        if self.p.printout:
            dt = dt or self.data.datetime[0]
            dt = bt.num2date(dt)
            print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        # To control operation entries
        self.orderid = None

        # Create SMA on 2nd data
        sma = btind.MovAv.SMA(self.data, period=self.p.period)
        # Create a CrossOver Signal from close an moving average
        self.signal = btind.CrossOver(self.data.close, sma)
        self.signal.csv = self.p.csvcross

    def next(self):
        if self.orderid:
            return  # if an order is active, no new orders are allowed

        if self.signal > 0.0:  # cross upwards
            if self.position:
                self.log('CLOSE SHORT , %.2f' % self.data.close[0])
                self.close()

            self.log('BUY CREATE , %.2f' % self.data.close[0])
            self.buy(size=self.p.stake)

        elif self.signal < 0.0:
            if self.position:
                self.log('CLOSE LONG , %.2f' % self.data.close[0])
                self.close()

            if not self.p.onlylong:
                self.log('SELL CREATE , %.2f' % self.data.close[0])
                self.sell(size=self.p.stake)

    def notify_order(self, order):
        if order.status in [bt.Order.Submitted, bt.Order.Accepted]:
            return  # Await further notifications

        if order.status == order.Completed:
            if order.isbuy():
                buytxt = 'BUY COMPLETE, %.2f' % order.executed.price
                self.log(buytxt, order.executed.dt)
            else:
                selltxt = 'SELL COMPLETE, %.2f' % order.executed.price
                self.log(selltxt, order.executed.dt)

        elif order.status in [order.Expired, order.Canceled, order.Margin]:
            self.log('%s ,' % order.Status[order.status])
            pass  # Simply log

        # Allow new orders
        self.orderid = None

    def notify_trade(self, trade):
        if trade.isclosed:
            self.log('TRADE PROFIT, GROSS %.2f, NET %.2f' %
                     (trade.pnl, trade.pnlcomm))

        elif trade.justopened:
            self.log('TRADE OPENED, SIZE %2d' % trade.size)

def runstrategy():
    args = parse_args()

    # Create a cerebro
    cerebro = bt.Cerebro()

    # Get the dates from the args
    fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d')
    todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d')

    # Create the 1st data
    data = btfeeds.BacktraderCSVData(
        dataname=args.data,
        fromdate=fromdate,
        todate=todate)

    # Add the 1st data to cerebro
    cerebro.adddata(data)

    # Add the strategy
    cerebro.addstrategy(LongShortStrategy,
                        period=args.period,
                        onlylong=args.onlylong,
                        csvcross=args.csvcross,
                        stake=args.stake)

    # Add the commission - only stocks like a for each operation
    cerebro.broker.setcash(args.cash)

    # Add the commission - only stocks like a for each operation
    cerebro.broker.setcommission(commission=args.comm,
                                 mult=args.mult,
                                 margin=args.margin)

    cerebro.addanalyzer(SQN)

    cerebro.addwriter(bt.WriterFile, csv=args.writercsv, rounding=2)

    # And run it
    cerebro.run()

    # Plot if requested
    if args.plot:
        cerebro.plot(numfigs=args.numfigs, volume=False, zdown=False)

def parse_args():
    parser = argparse.ArgumentParser(description='MultiData Strategy')

    parser.add_argument('--data', '-d',
                        default='../../datas/2006-day-001.txt',
                        help='data to add to the system')

    parser.add_argument('--fromdate', '-f',
                        default='2006-01-01',
                        help='Starting date in YYYY-MM-DD format')

    parser.add_argument('--todate', '-t',
                        default='2006-12-31',
                        help='Starting date in YYYY-MM-DD format')

    parser.add_argument('--period', default=15, type=int,
                        help='Period to apply to the Simple Moving Average')

    parser.add_argument('--onlylong', '-ol', action='store_true',
                        help='Do only long operations')

    parser.add_argument('--writercsv', '-wcsv', action='store_true',
                        help='Tell the writer to produce a csv stream')

    parser.add_argument('--csvcross', action='store_true',
                        help='Output the CrossOver signals to CSV')

    parser.add_argument('--cash', default=100000, type=int,
                        help='Starting Cash')

    parser.add_argument('--comm', default=2, type=float,
                        help='Commission for operation')

    parser.add_argument('--mult', default=10, type=int,
                        help='Multiplier for futures')

    parser.add_argument('--margin', default=2000.0, type=float,
                        help='Margin for each future')

    parser.add_argument('--stake', default=1, type=int,
                        help='Stake to apply in each operation')

    parser.add_argument('--plot', '-p', action='store_true',
                        help='Plot the read data')

    parser.add_argument('--numfigs', '-n', default=1,
                        help='Plot using numfigs figures')

    return parser.parse_args()

if __name__ == '__main__':
    runstrategy()

多資料策略

原文:www.backtrader.com/blog/posts/2015-09-03-multidata-strategy/multidata-strategy/

因為世界上沒有任何事物是孤立存在的,很可能購買某項資產的觸發因素實際上是另一項資產。

使用不同的分析技術可能會發現兩個不同資料之間的相關性。

backtrader 支援同時使用不同資料來源,因此在大多數情況下可能會用於此目的。

讓我們假設已經發現了以下公司之間的相關性:

  • Oracle

  • Yahoo

人們可以想象,當雅虎公司運營良好時,該公司會從 Oracle 購買更多伺服器、更多資料庫和更多專業服務,從而推動股價上漲。

因此,經過深入分析後製定了一項策略:

  • 如果Yahoo的收盤價超過簡單移動平均線(週期 15)

  • 買入Oracle

退出頭寸:

  • 使用收盤價的下穿

訂單執行型別:

  • 市場

總結一下使用backtrader設定所需的內容:

  • 建立一個cerebro

  • 載入資料來源 1(Oracle)並將其新增到 cerebro

  • 載入資料來源 2(Yahoo)並將其新增到 cerebro

  • 載入我們設計的策略

策略的詳細資訊:

  • 在資料來源 2(Yahoo)上建立一個簡單移動平均線

  • 使用雅虎��收盤價和移動平均��建立一個 CrossOver 指標

然後按照上述描述在資料來源 1(Oracle)上執行買入/賣出訂單。

下面的指令碼使用以下預設值:

  • Oracle(資料來源 1)

  • 雅虎(資料來源 2)

  • 現金:10000(系統預設值)

  • 股份:10 股

  • 佣金:每輪 0.5%(表示為 0.005)

  • 週期:15 個交易日

  • 週期:2003 年、2004 年和 2005 年

該指令碼可以接受引數以修改上述設定,如幫助文字中所示:

$ ./multidata-strategy.py --help
usage: multidata-strategy.py [-h] [--data0 DATA0] [--data1 DATA1]
                             [--fromdate FROMDATE] [--todate TODATE]
                             [--period PERIOD] [--cash CASH]
                             [--commperc COMMPERC] [--stake STAKE] [--plot]
                             [--numfigs NUMFIGS]

MultiData Strategy

optional arguments:
  -h, --help            show this help message and exit
  --data0 DATA0, -d0 DATA0
                        1st data into the system
  --data1 DATA1, -d1 DATA1
                        2nd data into the system
  --fromdate FROMDATE, -f FROMDATE
                        Starting date in YYYY-MM-DD format
  --todate TODATE, -t TODATE
                        Starting date in YYYY-MM-DD format
  --period PERIOD       Period to apply to the Simple Moving Average
  --cash CASH           Starting Cash
  --commperc COMMPERC   Percentage commission for operation (0.005 is 0.5%
  --stake STAKE         Stake to apply in each operation
  --plot, -p            Plot the read data
  --numfigs NUMFIGS, -n NUMFIGS
                        Plot using numfigs figures

標準執行結果:

$ ./multidata-strategy.py
2003-02-11T23:59:59+00:00, BUY CREATE , 9.14
2003-02-12T23:59:59+00:00, BUY COMPLETE, 11.14
2003-02-12T23:59:59+00:00, SELL CREATE , 9.09
2003-02-13T23:59:59+00:00, SELL COMPLETE, 10.90
2003-02-14T23:59:59+00:00, BUY CREATE , 9.45
2003-02-18T23:59:59+00:00, BUY COMPLETE, 11.22
2003-03-06T23:59:59+00:00, SELL CREATE , 9.72
2003-03-07T23:59:59+00:00, SELL COMPLETE, 10.32
...
...
2005-12-22T23:59:59+00:00, BUY CREATE , 40.83
2005-12-23T23:59:59+00:00, BUY COMPLETE, 11.68
2005-12-23T23:59:59+00:00, SELL CREATE , 40.63
2005-12-27T23:59:59+00:00, SELL COMPLETE, 11.63
==================================================
Starting Value - 100000.00
Ending   Value - 99959.26
==================================================

經過兩年的執行後,該策略:

  • 損失了 40.74 貨幣單位

至於雅虎和 Oracle 之間的相關性

視覺化輸出(新增--plot以生成圖表)

圖片

以及指令碼(已新增到backtrader源分發的samples/multidata-strategy目錄下。

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

import argparse
import datetime

# The above could be sent to an independent module
import backtrader as bt
import backtrader.feeds as btfeeds
import backtrader.indicators as btind

class MultiDataStrategy(bt.Strategy):
    '''
    This strategy operates on 2 datas. The expectation is that the 2 datas are
    correlated and the 2nd data is used to generate signals on the 1st

      - Buy/Sell Operationss will be executed on the 1st data
      - The signals are generated using a Simple Moving Average on the 2nd data
        when the close price crosses upwwards/downwards

    The strategy is a long-only strategy
    '''
    params = dict(
        period=15,
        stake=10,
        printout=True,
    )

    def log(self, txt, dt=None):
        if self.p.printout:
            dt = dt or self.data.datetime[0]
            dt = bt.num2date(dt)
            print('%s, %s' % (dt.isoformat(), txt))

    def notify_order(self, order):
        if order.status in [bt.Order.Submitted, bt.Order.Accepted]:
            return  # Await further notifications

        if order.status == order.Completed:
            if order.isbuy():
                buytxt = 'BUY COMPLETE, %.2f' % order.executed.price
                self.log(buytxt, order.executed.dt)
            else:
                selltxt = 'SELL COMPLETE, %.2f' % order.executed.price
                self.log(selltxt, order.executed.dt)

        elif order.status in [order.Expired, order.Canceled, order.Margin]:
            self.log('%s ,' % order.Status[order.status])
            pass  # Simply log

        # Allow new orders
        self.orderid = None

    def __init__(self):
        # To control operation entries
        self.orderid = None

        # Create SMA on 2nd data
        sma = btind.MovAv.SMA(self.data1, period=self.p.period)
        # Create a CrossOver Signal from close an moving average
        self.signal = btind.CrossOver(self.data1.close, sma)

    def next(self):
        if self.orderid:
            return  # if an order is active, no new orders are allowed

        if not self.position:  # not yet in market
            if self.signal > 0.0:  # cross upwards
                self.log('BUY CREATE , %.2f' % self.data1.close[0])
                self.buy(size=self.p.stake)

        else:  # in the market
            if self.signal < 0.0:  # crosss downwards
                self.log('SELL CREATE , %.2f' % self.data1.close[0])
                self.sell(size=self.p.stake)

    def stop(self):
        print('==================================================')
        print('Starting Value - %.2f' % self.broker.startingcash)
        print('Ending   Value - %.2f' % self.broker.getvalue())
        print('==================================================')

def runstrategy():
    args = parse_args()

    # Create a cerebro
    cerebro = bt.Cerebro()

    # Get the dates from the args
    fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d')
    todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d')

    # Create the 1st data
    data0 = btfeeds.YahooFinanceCSVData(
        dataname=args.data0,
        fromdate=fromdate,
        todate=todate)

    # Add the 1st data to cerebro
    cerebro.adddata(data0)

    # Create the 2nd data
    data1 = btfeeds.YahooFinanceCSVData(
        dataname=args.data1,
        fromdate=fromdate,
        todate=todate)

    # Add the 2nd data to cerebro
    cerebro.adddata(data1)

    # Add the strategy
    cerebro.addstrategy(MultiDataStrategy,
                        period=args.period,
                        stake=args.stake)

    # Add the commission - only stocks like a for each operation
    cerebro.broker.setcash(args.cash)

    # Add the commission - only stocks like a for each operation
    cerebro.broker.setcommission(commission=args.commperc)

    # And run it
    cerebro.run()

    # Plot if requested
    if args.plot:
        cerebro.plot(numfigs=args.numfigs, volume=False, zdown=False)

def parse_args():
    parser = argparse.ArgumentParser(description='MultiData Strategy')

    parser.add_argument('--data0', '-d0',
                        default='../../datas/orcl-1995-2014.txt',
                        help='1st data into the system')

    parser.add_argument('--data1', '-d1',
                        default='../../datas/yhoo-1996-2014.txt',
                        help='2nd data into the system')

    parser.add_argument('--fromdate', '-f',
                        default='2003-01-01',
                        help='Starting date in YYYY-MM-DD format')

    parser.add_argument('--todate', '-t',
                        default='2005-12-31',
                        help='Starting date in YYYY-MM-DD format')

    parser.add_argument('--period', default=15, type=int,
                        help='Period to apply to the Simple Moving Average')

    parser.add_argument('--cash', default=100000, type=int,
                        help='Starting Cash')

    parser.add_argument('--commperc', default=0.005, type=float,
                        help='Percentage commission for operation (0.005 is 0.5%%')

    parser.add_argument('--stake', default=10, type=int,
                        help='Stake to apply in each operation')

    parser.add_argument('--plot', '-p', action='store_true',
                        help='Plot the read data')

    parser.add_argument('--numfigs', '-n', default=1,
                        help='Plot using numfigs figures')

    return parser.parse_args()

if __name__ == '__main__':
    runstrategy()

實際應用

原文:www.backtrader.com/blog/posts/2015-08-27-real-world-usage/real-world-usage/

最後,似乎將其紮根於開發 backtrader 是值得的。

在看到上週歐洲市場的情況看起來像世界末日之後,一個朋友問我是否可以檢視我們圖表軟體中的資料,看看下跌範圍與以前類似情況的比較如何。

當然我可以,但我說我可以做的不僅僅是看圖表,因為我可以迅速:

  • 建立一個快速的LegDown指標來測量下跌的範圍。它也可以被命名為HighLowRangeHiLoRange。幸運的是,如果認為有必要,可以透過alias來解決這個問題。

  • 建立一個LegDownAnalyzer,它將收集結果並對其進行排序。

這導致了一個額外的請求:

  • 在接下來的 5、10、15、20 天內(交易後…)的跌幅後的復甦。

    透過使用LegUp指標解決,它會將值寫回以與相應的LegDown對齊。

這項工作很快就完成了(在我空閒時間的允許範圍內),並與請求者共享了結果。但是…只有我看到了潛在問題:

  • 改進bt-run.py中的自動化程式

    • 多種策略/觀察者/分析器以及分開的 kwargs

    • 直接將指標註入策略中,每個指標都帶有 kwargs

    • 單一的繪圖引數也接受 kwargs。

  • Analyzer API 中進行改進,以實現對結果的自動列印功能(結果以dict-like 例項返回),並具有直接的data訪問別名。

儘管如此:

  • 由於我編寫了一個混合宣告並額外使用next來對齊LegDownLegUp值的實現組合,出現了一個隱晦的錯誤。

    這個錯誤是為了簡化傳遞多個Lines的單個資料而引入的,以便Indicators可以對每條線進行操作作為單獨的資料。

後者將我推向:

  • 新增一個與LineDelay相反的背景物件以“看”到“未來”。

    實際上這意味著實際值被寫入過去的陣列位置。

一旦所有這些都就位了,就是重新測試以上請求提出的(小?)挑戰,看看如何更輕鬆地解決以及更快地(在實現時間上)解決的時候了。

最後,執行和結果為從 1998 年至今的 Eurostoxx 50 期貨:

bt-run.py \
    --csvformat vchartcsv \
    --data ../datas/sample/1998-2015-estx50-vchart.txt \
    --analyzer legdownup \
    --pranalyzer \
    --nostdstats \
    --plot

====================
== Analyzers
====================
##########
legdownupanalyzer
##########
Date,LegDown,LegUp_5,LegUp_10,LegUp_15,LegUp_20
2008-10-10,901.0,331.0,69.0,336.0,335.0
2001-09-11,889.0,145.0,111.0,239.0,376.0
2008-01-22,844.0,328.0,360.0,302.0,344.0
2001-09-21,813.0,572.0,696.0,816.0,731.0
2002-07-24,799.0,515.0,384.0,373.0,572.0
2008-01-23,789.0,345.0,256.0,319.0,290.0
2001-09-17,769.0,116.0,339.0,405.0,522.0
2008-10-09,768.0,102.0,0.0,120.0,208.0
2001-09-12,764.0,137.0,126.0,169.0,400.0
2002-07-23,759.0,331.0,183.0,285.0,421.0
2008-10-16,758.0,102.0,222.0,310.0,201.0
2008-10-17,740.0,-48.0,219.0,218.0,116.0
2015-08-24,731.0,nan,nan,nan,nan
2002-07-22,729.0,292.0,62.0,262.0,368.0
...
...
...
2001-10-05,-364.0,228.0,143.0,286.0,230.0
1999-01-04,-370.0,219.0,99.0,-7.0,191.0
2000-03-06,-382.0,-60.0,-127.0,-39.0,-161.0
2000-02-14,-393.0,-92.0,90.0,340.0,230.0
2000-02-09,-400.0,-22.0,-46.0,96.0,270.0
1999-01-05,-438.0,3.0,5.0,-107.0,5.0
1999-01-07,-446.0,-196.0,-6.0,-82.0,-50.0
1999-01-06,-536.0,-231.0,-42.0,-174.0,-129.0

2015 年 8 月的下跌在第 13 個位置顯示出來。顯然是一個不常見的事件,儘管有更大的事件發生過。

針對指向上升的後續腿要做的事情對於統計學家和聰明的數學頭腦來說要多得多,而對我來說則要少得多。

關於LegUpDownAnalyzer的實現細節(在末尾看到整個模組程式碼):

  • 它在__init__中建立指標,就像其他物件一樣:StrategiesIndicators通常是常見的嫌疑人

    這些指標會自動註冊到附加了分析器的策略中

  • 就像策略一樣,Analyzerself.datas(一個資料陣列)和它的別名:self.dataself.data0self.data1

  • 類似策略:nexstartstop 鉤子(這些在指標中不存在)

    在這種情況下用於:

    • nextstart: 記錄策略的初始起始點

    • stop: 進行最終的計算,因為事情已經完成

  • 注意:在這種情況下不需要其他方法,如 startprenextnext

  • LegDownUpAnalyzer 方法 print 已經被重寫,不再呼叫 pprint 方法,而是建立計算的 CSV 列印輸出

經過許多討論,因為我們將 --plot 加入了混合中 … 圖表。

image

最後是由 bt-run 載入的 legupdown 模組。

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

import itertools
import operator

import six
from six.moves import map, xrange, zip

import backtrader as bt
import backtrader.indicators as btind
from backtrader.utils import OrderedDict

class LegDown(bt.Indicator):
    '''
    Calculates what the current legdown has been using:
      - Current low
      - High from ``period`` bars ago
    '''
    lines = ('legdown',)
    params = (('period', 10),)

    def __init__(self):
        self.lines.legdown = self.data.high(-self.p.period) - self.data.low

class LegUp(bt.Indicator):
    '''
    Calculates what the current legup has been using:
      - Current high
      - Low from ``period`` bars ago

    If param ``writeback`` is True the value will be written
    backwards ``period`` bars ago
    '''
    lines = ('legup',)
    params = (('period', 10), ('writeback', True),)

    def __init__(self):
        self.lu = self.data.high - self.data.low(-self.p.period)
        self.lines.legup = self.lu(self.p.period * self.p.writeback)

class LegDownUpAnalyzer(bt.Analyzer):
    params = (
        # If created indicators have to be plotteda along the data
        ('plotind', True),
        # period to consider for a legdown
        ('ldown', 10),
        # periods for the following legups after a legdown
        ('lups', [5, 10, 15, 20]),
        # How to sort: date-asc, date-desc, legdown-asc, legdown-desc
        ('sort', 'legdown-desc'),
    )

    sort_options = ['date-asc', 'date-des', 'legdown-desc', 'legdown-asc']

    def __init__(self):
        # Create the legdown indicator
        self.ldown = LegDown(self.data, period=self.p.ldown)
        self.ldown.plotinfo.plot = self.p.plotind

        # Create the legup indicators indicator - writeback is not touched
        # so the values will be written back the selected period and therefore
        # be aligned with the end of the legdown
        self.lups = list()
        for lup in self.p.lups:
            legup = LegUp(self.data, period=lup)
            legup.plotinfo.plot = self.p.plotind
            self.lups.append(legup)

    def nextstart(self):
        self.start = len(self.data) - 1

    def stop(self):
        # Calculate start and ending points with values
        start = self.start
        end = len(self.data)
        size = end - start

        # Prepare dates (key in the returned dictionary)
        dtnumslice = self.strategy.data.datetime.getzero(start, size)
        dtslice = map(lambda x: bt.num2date(x).date(), dtnumslice)
        keys = dtslice

        # Prepare the values, a list for each key item
        # leg down
        ldown = self.ldown.legdown.getzero(start, size)
        # as many legs up as requested
        lups = [up.legup.getzero(start, size) for up in self.lups]

        # put legs down/up together and interleave (zip)
        vals = [ldown] + lups
        zvals = zip(*vals)

        # Prepare sorting options
        if self.p.sort == 'date-asc':
            reverse, item = False, 0
        elif self.p.sort == 'date-desc':
            reverse, item = True, 0
        elif self.p.sort == 'legdown-asc':
            reverse, item = False, 1
        elif self.p.sort == 'legdown-desc':
            reverse, item = True, 1
        else:
            # Default ordering - date-asc
            reverse, item = False, 0

        # Prepare a sorted array of 2-tuples
        keyvals_sorted = sorted(zip(keys, zvals),
                                reverse=reverse,
                                key=operator.itemgetter(item))

        # Use it to build an ordereddict
        self.ret = OrderedDict(keyvals_sorted)

    def get_analysis(self):
        return self.ret

    def print(self, *args, **kwargs):
        # Overriden to change default behavior (call pprint)
        # provides a CSV printout of the legs down/up
        header_items = ['Date', 'LegDown']
        header_items.extend(['LegUp_%d' % x for x in self.p.lups])
        header_txt = ','.join(header_items)
        print(header_txt)

        for key, vals in six.iteritems(self.ret):
            keytxt = key.strftime('%Y-%m-%d')
            txt = ','.join(itertools.chain([keytxt], map(str, vals)))
            print(txt)

資料 - 重放

原文:www.backtrader.com/blog/posts/2015-08-25-data-replay/data-replay/

時間已經過去,針對一個完全形成和關閉的柱狀圖測試策略是好的,但可以更好。

這就是資料重放的用武之地。如果:

  • 策略在時間框架 X(例如:每日)上執行

  • 較小時間框架 Y(例如:1 分鐘)的資料可用

資料重放正如其名稱所示:

Replay a daily bar using the 1 minute data

當然,這並不是市場實際發展的方式,但比孤立地檢視每日完整和關閉的柱狀圖要好得多:

If the strategy operates in realtime during the formation of the daily bar,
the approximation of the formation of the bar gives a chance to replicate the
actual behavior of the strategy under real conditions

資料重放付諸實踐遵循backtrader的常規使用模式

  • 載入資料來源

  • 將資料傳遞給DataReplayer,這是另一個將在載入的資料來源上工作的資料來源

  • 將新的資料來源傳遞給 cerebro

  • 新增一個策略

  • 然後執行... 禁用預載入*

注意

當資料被重放時,無法支援預載入,因為每個柱狀圖實際上是實時構建的。

為了演示,將在每週基礎上重放標準的 2006 年日常資料。這意味著:

  • 最終將有 52 個柱狀圖,每週一個

  • Cerebro 將總共呼叫prenextnext 255 次,這是每日柱狀圖的原始計數

技巧:

  • 當每週柱狀圖形成時,策略的長度(len(self))將保持不變。

  • 每個新周,長度將增加一

以下是一些示例,但首先是測試指令碼的原始碼,其中載入資料並傳遞給重放器...並且使用preload=False來禁用預載入(強制)

 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
    data_replayed = bt.DataReplayer(
        dataname=data,
        timeframe=tframes[args.timeframe],
        compression=args.compression)

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

    # Run over everything
    cerebro.run(preload=False)

示例 - 將每日重放為每週

指令碼的呼叫:

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

不幸的是,圖表無法向我們展示背景中發生的真實情況,因此讓我們看看控制檯輸出:

prenext len 1 - counter 1
prenext len 1 - counter 2
prenext len 1 - counter 3
prenext len 1 - counter 4
prenext len 1 - counter 5
prenext len 2 - counter 6
...
...
prenext len 9 - counter 44
prenext len 9 - counter 45
---next len 10 - counter 46
---next len 10 - counter 47
---next len 10 - counter 48
---next len 10 - counter 49
---next len 10 - counter 50
---next len 11 - counter 51
---next len 11 - counter 52
---next len 11 - counter 53
...
...
---next len 51 - counter 248
---next len 51 - counter 249
---next len 51 - counter 250
---next len 51 - counter 251
---next len 51 - counter 252
---next len 52 - counter 253
---next len 52 - counter 254
---next len 52 - counter 255

正如我們所看到的,內部的self.counter變數正在跟蹤每次呼叫prenextnext。前者在應用簡單移動平均產生值之前呼叫。後者在簡單移動平均產生值時呼叫。

關鍵:

  • 策略的長度(len(self))每 5 個柱狀圖(每週 5 個交易日)發生變化

該策略實際上看到:

  • 每週柱狀圖是如何在 5 次快照中發展的。

    再次強調,這並不複製市場的實際逐筆(甚至不是分鐘、小時)發展,但比實際看到柱狀圖要好。

視覺化輸出是周線圖表,這是系統正在進行測試的最終結果。

圖片

示例 2 - 每日到每日帶壓縮

當然,“重放”也可以應用於相同的時間框架,但具有壓縮。

控制檯:

$ ./data-replay.py --timeframe daily --compression 2
prenext len 1 - counter 1
prenext len 1 - counter 2
prenext len 2 - counter 3
prenext len 2 - counter 4
prenext len 3 - counter 5
prenext len 3 - counter 6
prenext len 4 - counter 7
...
...
---next len 125 - counter 250
---next len 126 - counter 251
---next len 126 - counter 252
---next len 127 - counter 253
---next len 127 - counter 254
---next len 128 - counter 255

這次我們得到了預期的一半柱狀圖,因為請求的壓縮因子為 2。

圖表:

圖片

結論

可以對市場發展進行重建。通常會提供一組較小的時間範圍資料,可以用來離散地重播系統執行的時間範圍。

測試指令碼。

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 = btind.SMA(self.data, period=self.p.period)

    def start(self):
        self.counter = 0

    def prenext(self):
        self.counter += 1
        print('prenext len %d - counter %d' % (len(self), self.counter))

    def next(self):
        self.counter += 1
        print('---next len %d - counter %d' % (len(self), self.counter))

def runstrat():
    args = parse_args()

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

    cerebro.addstrategy(
        SMAStrategy,
        # args for the strategy
        period=args.period,
    )

    # 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
    data_replayed = bt.DataReplayer(
        dataname=data,
        timeframe=tframes[args.timeframe],
        compression=args.compression)

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

    # Run over everything
    cerebro.run(preload=False)

    # 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')

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

    return parser.parse_args()

if __name__ == '__main__':
    runstrat()

相關文章