天天看點

Python量化交易學習筆記(33)——backtrader倉位管理

本文将對backtrader的倉位管理進行介紹,具體以同時回測交易3隻股票為例,檢視每日倉位情況。

政策

  • 買入條件:5日線金叉60日線
  • 賣出條件:5日線死叉60日線

示例

倉位資訊輸出的核心代碼位于政策類的next的方法中:

def next(self):
        for i, d in enumerate(self.datas):
            dt, dn = self.datetime.date(), d._name           # 擷取時間及股票代碼
            pos = self.getposition(d)
            if not len(pos):                                 # 不在場内,則可以買入
                if self.inds[d]['cross'] > 0:                # 如果金叉
                    self.buy(data = d, size = self.p.pstake) # 買買買
            elif self.inds[d]['cross'] < 0:                  # 在場内,且死叉
                self.close(data = d)                         # 賣賣賣

        # 列印倉位資訊
        print('**************************************************************', file = self.log_file)
        print(self.data.datetime.date(), file = self.log_file)
        for i, d in enumerate(self.datas):
            pos = self.getposition(d)
            if len(pos):
                print('{}, 持倉:{}, 成本價:{}, 目前價:{}, 盈虧:{:.2f}'.format(
                    d._name, pos.size, pos.price, pos.adjbase, pos.size * (pos.adjbase - pos.price)),
                     file = self.log_file)      

部分輸出結果為:

...
*************************************************************************************************
2019-06-24
000001, 持倉:100, 成本價:13.69, 目前價:13.69, 盈虧:0.00
*************************************************************************************************
2019-06-25
000001, 持倉:100, 成本價:13.69, 目前價:13.43, 盈虧:-26.00
*************************************************************************************************
2019-06-26
000001, 持倉:100, 成本價:13.69, 目前價:13.37, 盈虧:-32.00
*************************************************************************************************
2019-06-27
000001, 持倉:100, 成本價:13.69, 目前價:13.71, 盈虧:2.00
*************************************************************************************************
2019-06-28
000001, 持倉:100, 成本價:13.69, 目前價:13.78, 盈虧:9.00
*************************************************************************************************
2019-07-01
000001, 持倉:100, 成本價:13.69, 目前價:13.93, 盈虧:24.00
*************************************************************************************************
2019-07-02
000001, 持倉:100, 成本價:13.69, 目前價:14.18, 盈虧:49.00
*************************************************************************************************
2019-07-03
000001, 持倉:100, 成本價:13.69, 目前價:14.01, 盈虧:32.00
*************************************************************************************************
2019-07-04
000001, 持倉:100, 成本價:13.69, 目前價:13.99, 盈虧:30.00
*************************************************************************************************
2019-07-05
000001, 持倉:100, 成本價:13.69, 目前價:13.92, 盈虧:23.00
000002, 持倉:100, 成本價:29.45, 目前價:29.45, 盈虧:0.00
*************************************************************************************************
2019-07-08
000001, 持倉:100, 成本價:13.69, 目前價:13.59, 盈虧:-10.00
000002, 持倉:100, 成本價:29.45, 目前價:29.15, 盈虧:-30.00
...      

輸出圖形為:

Python量化交易學習筆記(33)——backtrader倉位管理

解析

上面的代碼用到了多隻股票同時進行政策回測,具體内容可以參見​​筆記(17)​​。

在backtrader中,使用類Position來管理倉位。Position的重要屬性包括:

  • price:資産成本單價
  • size:倉位大小
  • adjbase:資産目前收盤價格
  • 在類Strategy中,可以使用屬性position或者方法getposition(self, data=None, broker=None)來通路倉位資訊。
  • 當使用getposition(self, data=None, broker=None)方法時,如果不指定參數data值,該方法會預設傳回datas[0]的倉位資訊,若指定參數data,則傳回對應資産的倉位資訊,如示例所示。
for i, d in enumerate(self.datas):
            pos = self.getposition(d)      
  • 倉位資訊positions實際儲存在代理broker中,positions是一個Python的字典,key是一個種子資料Data Feed,value是一個類Position的對象。也就是說,positions[datas[0]]就表示第一隻股票datas[0]對應的倉位資訊,positions[datas[1]]就表示第二隻股票datas[1]對應的倉位資訊,依次類推。
  • 類Position重寫了__len__方法,可以使用len(position)來判斷倉位大小是否為0。
  • 類Position重寫了__str__方法,可以通過print(position)來列印倉位的具體資訊。
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
import datetime  # 用于datetime對象操作
import os.path  # 用于管理路徑
import sys  # 用于在argvTo[0]中找到腳本名稱
import backtrader as bt # 引入backtrader架構
import pandas as pd


stk_num = 3  # 回測股票數目
# 建立政策
class SmaCross(bt.Strategy):
    # 可配置政策參數
    params = dict(
        pfast=5,  # 短期均線周期
        pslow=60,   # 長期均線周期
        poneplot = False,  # 是否列印到同一張圖
        pstake = 100 # 單筆交易股票數目
    )
    def __init__(self):
        self.log_file = open('position_log.txt', 'w') # 用于輸出倉位資訊
        self.inds = dict()
        for i, d in enumerate(self.datas):
            self.inds[d] = dict()
            self.inds[d]['sma1'] = bt.ind.SMA(d.close, period=self.p.pfast)  # 短期均線
            self.inds[d]['sma2'] = bt.ind.SMA(d.close, period=self.p.pslow)  # 長期均線
            self.inds[d]['cross'] = bt.ind.CrossOver(self.inds[d]['sma1'], self.inds[d]['sma2'], plot = False)  # 交叉信号
            # 跳過第一隻股票data,第一隻股票data作為主圖資料
            if i > 0:
                if self.p.poneplot:
                    d.plotinfo.plotmaster = self.datas[0]
    def next(self):
        for i, d in enumerate(self.datas):
            dt, dn = self.datetime.date(), d._name           # 擷取時間及股票代碼
            pos = self.getposition(d)
            if not len(pos):                                 # 不在場内,則可以買入
                if self.inds[d]['cross'] > 0:                # 如果金叉
                    self.buy(data = d, size = self.p.pstake) # 買買買
            elif self.inds[d]['cross'] < 0:                  # 在場内,且死叉
                self.close(data = d)                         # 賣賣賣

        # 列印倉位資訊
        print('*****************************************************************************', file = self.log_file)
        print(self.data.datetime.date(), file = self.log_file)
        for i, d in enumerate(self.datas):
            pos = self.getposition(d)
            if len(pos):
                print('{}, 持倉:{}, 成本價:{}, 目前價:{}, 盈虧:{:.2f}'.format(
                    d._name, pos.size, pos.price, pos.adjbase, pos.size * (pos.adjbase - pos.price)),
                     file = self.log_file)

    def stop(self):
        self.log_file.close()
        pass

cerebro = bt.Cerebro()  # 建立cerebro


# 讀入股票代碼
stk_code_file = '../TQDat/TQDown2020v1/data/tq_wrk_code2019.csv'
stk_pools = pd.read_csv(stk_code_file, encoding = 'gbk')
if stk_num > stk_pools.shape[0]:
    print('股票數目不能大于%d' % stk_pools.shape[0])
    exit()
for i in range(stk_num):
    stk_code = stk_pools['code'][stk_pools.index[i]]
    stk_code = '%06d' % stk_code
    # 讀入資料
    datapath = '../TQDat/day/stk/' + stk_code + '.csv'
    # 建立價格資料
    data = bt.feeds.GenericCSVData(
            dataname = datapath,
            fromdate = datetime.datetime(2019, 1, 1),
            todate = datetime.datetime(2019, 12, 31),
            nullvalue = 0.0,
            dtformat = ('%Y-%m-%d'),
            datetime = 0,
            open = 1,
            high = 2,
            low = 3,
            close = 4,
            volume = 5,
            openinterest = -1
            )
    # 在Cerebro中添加股票資料
    cerebro.adddata(data, name = stk_code)
# 設定啟動資金
cerebro.broker.setcash(100000.0)
# 設定傭金為零
cerebro.broker.setcommission(commission=0.00)
cerebro.addstrategy(SmaCross, poneplot = False)  # 添加政策
cerebro.run()  # 周遊所有資料
# 列印最後結果
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.plot(style = "candlestick")  # 繪圖