Note
The data files used in the quickstart guide are updated from time to time, which means that the adjusted close changes and with it the close (and the other components). That means that the actual output may be different to what was put in the documentation at the time of writing.
注意:
由於調整收盤價可能發生變化,所以后續示例代碼的運行結果可能與本教程的輸出不一致
Using the platform
使用平台
Let’s run through a series of examples (from almost an empty one to a fully fledged strategy) but not without before roughly explaining 2 basic concepts when working with backtrader
讓我們通過一系列示例(從一個幾乎是空空如也的戰略變成一個全面成熟的戰略)。但是在使用backtrader之前我將講兩個基本概念。
1、Lines 航線(數據線)
Data Feeds, Indicators and Strategies have lines.
數據源,指標,策略都有lines屬性
策略對象的lines對象里面就只有一個時間線。
A line is a succession of points that when joined together form this line. When talking about the markets, a Data Feed has usually the following set of points per day:
一條線是由一連串的點組成的,這些點聯合在一起就變成了這條線。當我們在討論市場的時候,每天的數據反饋的點通常由以下的幾點:
Open(開盤價),High(最高價),Low(最低價),Close(收盤價),Volume(成交量),OpenInterset(未平倉量)
The series of “Open”s along time is a Line. And therefore a Data Feed has usually 6 lines.
這一系列的“Open”沿着時間稱為一條線,因此數據反饋通常有6條線
If we also consider “DateTime” (which is the actual reference for a single point), we could count 7 lines.
如果我們也考慮"時間"(他是單個點的實際引用),我們也可以統計7行
Index 0 Approach
2、索引0的方法
When accessing the values in a line, the current value is accessed with index: 0
當訪問線上的值,可以使用索引0訪問當前的值。
And the “last” output value is accessed with -1. This in line with Python conventions for iterables (and a line can be iterated and is therefore an iterable) where index -1 is used to access the “last” item of the iterable/array.
當訪問最后的一次輸出值,可以用-1.這符合Python對可迭代對象的定義(一條線的數據能被迭代,因此它是可迭代的)當用索引-1時就是訪問可迭代對象/數組的最后一個元素
In our case is the last output value what’s getting accessed.
在我們的例子中,訪問的是最后一次輸出的值
As such and being index 0 right after -1, it is used to access the current moment in line.
因此,索引0的數據在索引-1之后,它用於對線上當前時刻的數據訪問。
With that in mind and if we imagine a Strategy featuring a Simple Moving average created during initialization:
考慮到這一點,如果我們設想一個策略,在初始化時創建一個簡單的移動平均線:
self.sma = SimpleMovingAverage(.....)
The easiest and simplest way to access the current value of this moving average:
最簡單與方便的訪問當前移動平均值的方法
av = self.sma[0]
There is no need to know how many bars/minutes/days/months have been processed, because “0” uniquely identifies the current instant.
我們不需要知道已經處理了多少個bars/分/天/月,因為0獨特的標識了當前的既可狀態.
Following pythonic tradition, the “last” output value is accessed using -1:
按照Python的慣例,訪問最后一次輸出的值可以使用-1:
previous_value = self.sma[-1]
Of course earlier output values can be accessed with -2, -3, …
當然更早的輸出值可以使用-2,-3.
From 0 to 100: the samples
從0開始:樣本
Let’s get running.
讓我們跑起來
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import backtrader as bt
if __name__ == '__main__':
cerebro = bt.Cerebro()
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue()
After the execution the output is:
運行結束后的輸出:
Starting Portfolio Value: 10000.00 Final Portfolio Value: 10000.00
In this example:
在這個示例:
-
backtrader was imported
- backtrader被導入
-
The Cerebro engine was instantiated
- Cerebro被實例化
-
The resulting cerebro instance was told to run (loop over data)
-
生成的cerebro的實例運行run方法(循環處理數據)
-
And the resulting outcome was printed out
- 結果被輸出
Although it doesn’t seem much, let’s point out something explicitly shown:
雖然看起來輸出不多,讓我們指出一些明確顯示的東西:
-
The Cerebro engine has created a broker instance in the background
- 在后台Cerebro引擎將創建經紀人(broker)實例
-
The instance already has some cash to start with
- 實例一開始就擁有一些現金
This behind the scenes broker instantiation is a constant trait in the platform to simplify the life of the user. If no broker is set by the user, a default one is put in place.
這種后台實現經紀人實例是平台中常規的特征,它簡化了用戶的使用。如果用戶沒有設置經紀人,平台會默認的給予設置。
And 10K monetary units is a usual value with some brokers to begin with.
1萬貨幣單位是經紀人通常的價格,或初始價格。
Setting the Cash
設置現金
In the world of finance, for sure only “losers” start with 10k. Let’s change the cash and run the example again.
在金融世界里,肯定只有“失敗者”才會從10000美元開始。讓我們更改現金,再運行一遍這個例子。
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import backtrader as bt
if __name__ == '__main__':
cerebro = bt.Cerebro()
cerebro.broker.setcash(100000.0)
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
After the execution the output is:
運行以后的輸出:
Starting Portfolio Value: 1000000.00 Final Portfolio Value: 1000000.00
Mission accomplished. Let’s move to tempestuous waters.
任務完成,讓我們來點更加猛料。
Adding a Data Feed
添加數據源
Having cash is fun, but the purpose behind all this is to let an automated strategy multiply the cash without moving a finger by operating on an asset which we see as a Data Feed
擁有現金很快樂,但這一切背后的目的是讓一種自動策略在不動手指的情況下,通過操作我們視之為數據源,使資產、現金成倍增長
Ergo … No Data Feed -> No Fun. Let’s add one to the ever growing example.
因此... 沒有數據源沒意思, 讓我們在不斷增加的示例中添加一個
import datetime # For datetime objects
import os.path # To manage paths
import sys # To find out the script name (in argv[0])
# Import the backtrader platform
import backtrader as bt
if __name__ == '__main__':
# Create a cerebro entity
cerebro = bt.Cerebro()
# Datas are in a subfolder of the samples. Need to find where the script is
# because it could have been called from anywhere
# 獲取運行腳本的絕對路徑,用於拼接數據的路徑
modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt')
# Create a Data Feed
data = bt.feeds.YahooFinanceCSVData(
dataname=datapath,
# Do not pass values before this date
fromdate=datetime.datetime(2000, 1, 1),
# Do not pass values after this date
todate=datetime.datetime(2000, 12, 31),
reverse=False)
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Set our desired cash start
cerebro.broker.setcash(100000.0)
# Print out the starting conditions
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
# Run over everything
cerebro.run()
# Print out the final result
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
After the execution the output is:
運行輸出:
Starting Portfolio Value: 1000000.00 Final Portfolio Value: 1000000.00
The amount of boilerplate has grown slightly, because we added:
樣板代碼量有所增加,因為我們增加了:
-
Finding out where our example script is to be able to locate the sample Data Feed file
- 找出我們的示例腳本可以定位示例數據源文件的位置
-
Having datetime objects to filter on which data from the Data Feed we will be operating
- 讓datetime對象篩選我們將要操作的數據源中的哪些數據
Aside from that, the Data Feed is created and added to cerebro.
除此之外,數據Feed被創建並添加到cerebro中。
The output has not changed and it would be a miracle if it had.
輸出沒有改變,如果改變了,那就是奇跡了。
Note
Yahoo Online sends the CSV data in date descending order, which is not the standard convention. The reversed=True prameter takes into account that the CSV data in the file has already been reversed and has the standard expected date ascending order.
注意
feed里面有一個參數reversed,如果按照升序的情況,不需要設置為True,如果數據反轉的話,需要設置為True
本來經過測試,實際反轉的情況輸出會少一條行數據。
Our First Strategy
我們的第一個策略
The cash is in the broker and the Data Feed is there. It seems like risky business is just around the corner.
現金在經紀人那里,數據源也在這里了,危險的買賣就在眼前了。
Let’s put a Strategy into the equation and print the “Close” price of each day (bar).
讓我們把一個策略放入公式,並輸出每天的收盤價
DataSeries (the underlying class in Data Feeds) objects have aliases to access the well known OHLC (Open High Low Close) daily values. This should ease up the creation of our printing logic.
DataSeries(數據提要中的基礎類)對象具有別名來訪問已知的OHLC(開盤價,最高價,最低價,收盤價)的值。
這將簡化輸出邏輯的創建。
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime # For datetime objects
import os.path # To manage paths
import sys # To find out the script name (in argv[0])
# Import the backtrader platform
import backtrader as bt
# Create a Stratey
class TestStrategy(bt.Strategy):
def log(self, txt, dt=None):
''' Logging function for this strategy'''
# 用了默認參數並用了短路原則,如果沒有輸入參數,就輸出數據的日期,一個函數多用
# 通過數據對象的屬性方式取出線對象
dt = dt or self.datas[0].datetime.date(0)
# 生成ISO 8601日期
print('%s, %s' % (dt.isoformat(), txt))
def __init__(self):
# Keep a reference to the "close" line in the data[0] dataseries
# 一般情況下,通過self.datas[0]選出數據線集合的對象,通過屬性選定指定的數據線,就是lines里面的一個對象
self.dataclose = self.datas[0].close
def next(self):
# Simply log the closing price of the series from the reference
# 這里將執行每個數據點位狀態的輸出,所以具體的邏輯后續也在這里寫
self.log('Close, %.2f' % self.dataclose[0])
if __name__ == '__main__':
# Create a cerebro entity
cerebro = bt.Cerebro()
# Add a strategy
cerebro.addstrategy(TestStrategy)
# Datas are in a subfolder of the samples. Need to find where the script is
# because it could have been called from anywhere
modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt')
# Create a Data Feed
data = bt.feeds.YahooFinanceCSVData(
dataname=datapath,
# Do not pass values before this date
fromdate=datetime.datetime(2000, 1, 1),
# Do not pass values before this date
todate=datetime.datetime(2000, 12, 31),
# Do not pass values after this date
reverse=False)
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Set our desired cash start
cerebro.broker.setcash(100000.0)
# Print out the starting conditions
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
# Run over everything
cerebro.run()
# Print out the final result
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
After the execution the output is:
執行后的輸出:
Starting Portfolio Value: 100000.00 2000-01-03T00:00:00, Close, 27.85 2000-01-04T00:00:00, Close, 25.39 2000-01-05T00:00:00, Close, 24.05 ... ... ... 2000-12-26T00:00:00, Close, 29.17 2000-12-27T00:00:00, Close, 28.94 2000-12-28T00:00:00, Close, 29.29 2000-12-29T00:00:00, Close, 27.41 Final Portfolio Value: 100000.00
Someone said the stockmarket was risky business, but it doesn’t seem so.
有人說股市有風險,但看上去並不是這樣
Let’s explain some of the magic:
讓我們來解釋一下他的魔力:
Upon init being called the strategy already has a list of datas that are present in the platform
在調用__init__時,策略已經在平台中有一個數據列表
This is a standard Python list and datas can be accessed in the order they were inserted.
這是一個標准的Python列表,可以按照插入的順序訪問數據。
The first data in the list self.datas[0] is the default data for trading operations and to keep all strategy elements synchronized (it’s the system clock)
列表中的第一個數據self.datas[0] 是交易操作的默認數據,用於保持所有策略元素同步(它是系統時鍾)
self.dataclose = self.datas[0].close keeps a reference to the close line. Only one level of indirection is later needed to access the close values.
self.dataclose = self.datas[0].close將保持對close線的引用,稍后只需要一個級別的間接尋址來訪問close值。
The strategy next method will be called on each bar of the system clock (self.datas[0]). This is true until other things come into play like indicators, which need some bars to start producing an output. More on that later.
策略的next方法將在系統時鍾的每一個bar上面調用。這是真的,知道另外的東西出來,比如指標(indicators),它需要一些bar開始生產,稍后詳細介紹。
Adding some Logic to the Strategy
給策略添加一些邏輯
Let’s try some crazy idea we had by looking at some charts
我們通過看一些圖標,嘗試一些瘋狂的想法
If the price has been falling 3 sessions in a row … BUY BUY BUY!!!
如果價格已經連續下跌3個交易日。。。買買買
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime # For datetime objects
import os.path # To manage paths
import sys # To find out the script name (in argv[0])
# Import the backtrader platform
import backtrader as bt
# Create a Stratey
class TestStrategy(bt.Strategy):
def log(self, txt, dt=None):
''' Logging function fot this strategy'''
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def __init__(self):
# Keep a reference to the "close" line in the data[0] dataseries
self.dataclose = self.datas[0].close
def next(self):
# Simply log the closing price of the series from the reference
self.log('Close, %.2f' % self.dataclose[0])
# 現在的收盤價低於上一個點的收盤價
if self.dataclose[0] < self.dataclose[-1]:
# current close less than previous close
# 上一個收盤價,低於上上一個收盤價,邏輯成立
if self.dataclose[-1] < self.dataclose[-2]:
# previous close less than the previous close
# BUY, BUY, BUY!!! (with all possible default parameters)
# 輸出成交信息
self.log('BUY CREATE, %.2f' % self.dataclose[0])
# 買買買
self.buy()
if __name__ == '__main__':
# Create a cerebro entity
cerebro = bt.Cerebro()
# Add a strategy
cerebro.addstrategy(TestStrategy)
# Datas are in a subfolder of the samples. Need to find where the script is
# because it could have been called from anywhere
modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt')
# Create a Data Feed
data = bt.feeds.YahooFinanceCSVData(
dataname=datapath,
# Do not pass values before this date
fromdate=datetime.datetime(2000, 1, 1),
# Do not pass values before this date
todate=datetime.datetime(2000, 12, 31),
# Do not pass values after this date
reverse=False)
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Set our desired cash start
cerebro.broker.setcash(100000.0)
# Print out the starting conditions
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
# Run over everything
cerebro.run()
# Print out the final result
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
運行以后的輸出
Starting Portfolio Value: 100000.00 2000-01-03, Close, 27.85 2000-01-04, Close, 25.39 2000-01-05, Close, 24.05 2000-01-05, BUY CREATE, 24.05 2000-01-06, Close, 22.63 2000-01-06, BUY CREATE, 22.63 2000-01-07, Close, 24.37 ... ... ... 2000-12-20, BUY CREATE, 26.88 2000-12-21, Close, 27.82 2000-12-22, Close, 30.06 2000-12-26, Close, 29.17 2000-12-27, Close, 28.94 2000-12-27, BUY CREATE, 28.94 2000-12-28, Close, 29.29 2000-12-29, Close, 27.41 Final Portfolio Value: 99725.08
Several “BUY” creation orders were issued, our porftolio value was decremented. A couple of important things are clearly missing.
我們發布了幾個買的訂單,我們的金錢下降了。一些重要的信息明顯被遺漏了。
The order was created but it is unknown if it was executed, when and at what price.
訂單已經被創建,但不知道何時執行,操作的時候的價格。
The next example will build upon that by listening to notifications of order status.
下一個示例將在此基礎上監聽訂單狀態的通知。
The curious reader may ask how many shares are being bought, what asset is being bought and how are orders being executed. Where possible (and in this case it is) the platform fills in the gaps:
好奇的讀者可能會問,購買了多少股票,正在購買什么資產,指令是如何執行的。在可能的情況下(在本例中)平台將彌補這些空白:
self.datas[0] (the main data aka system clock) is the target asset if no other one is specified
如果沒有指定其他數據,self.data[0](主數據既系統時鍾)是目標資產.
The stake is provided behind the scenes by a position sizer which uses a fixed stake, being the default “1”. It will be modified in a later example
交易的股份默認使用的是1,也就是每次交易1份股票。它將在后面進行修改。
The order is executed “At Market”. The broker (shown in previous examples) executes this using the opening price of the next bar, because that’s the 1st tick after the current under examination bar.
命令以"市場"的方式執行。經濟人(如前面的示例所示)使用將執行下一個bar的開盤價來操作,因為這是檢查滿足條件bar以后的第一個刻度(理解為觸發滿足條件后的第一個數據)
The order is executed so far without any commission (more on that later)
訂單的交易沒有任何的佣金(后續完善)
Do not only buy … but SELL
不要一直買, 也要賣
After knowing how to enter the market (long), an “exit concept” is needed and also understanding whether the strategy is in the market.
在知道如何進入市場(long)之后,需要一個"退出概念",並知道這個策略是否在市場
-
Luckily a Strategy object offers access to a position attribute for the default data feed
- 幸運的是,一個策略對象提供了對默認數據提要的位置屬性的訪問
-
Methods buy and sell return the created (not yet executed) order
- 方法buy和sell返回創建的(尚未執行的)訂單
-
Changes in orders’ status will be notified to the strategy via a notify method
- 訂單狀態的變化將通過notify方法通知策略
The “exit concept” will be an easy one:
退出將是一個非常簡單的邏輯
Exit after 5 bars (on the 6th bar) have elapsed for good or for worse
無論好壞,在5個bar之后(第6個bar)
Please notice that there is no “time” or “timeframe” implied: number of bars. The bars can represent 1 minute, 1 hour, 1 day, 1 week or any other time period.
請注意,這里沒有提起“時間”或“時間框架”:條的數量。條形圖可以表示1分鍾、1小時、1天、1周或任何其他時間段。
Although we know the data source is a daily one, the strategy makes no assumption about that.
雖然我們知道數據源是日常數據源,但該策略沒有對此做任何假設。
Additionally and to simplify:
此外,為了簡化:
- Do only allow a Buy order if not yet in the market
買單只有在沒有持倉的情況下才交易。
Note
The next method gets no “bar index” passed and therefore it seems obscure how to understand when 5 bars may have elapsed, but this has been modeled in pythonic way: call len on an object and it will tell you the length of its lines. Just write down (save in a variable) at which length in an operation took place and see if the current length is 5 bars away.
注意
next方法沒有傳遞“bar索引”,因此它似乎難以理解什么時候5個條會消失,但這是用python的方式建模的:在一個對象上調用len,它會告訴你它的線的長度。只需寫下(保存在一個變量中)操作發生的長度,看看當前長度是否在5條之外。
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime # For datetime objects
import os.path # To manage paths
import sys # To find out the script name (in argv[0])
# Import the backtrader platform
import backtrader as bt
# Create a Stratey
class TestStrategy(bt.Strategy):
def log(self, txt, dt=None):
''' Logging function fot this strategy'''
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def __init__(self):
# Keep a reference to the "close" line in the data[0] dataseries
self.dataclose = self.datas[0].open
# To keep track of pending orders
self.order = None
# 監控訂單情況
def notify_order(self, order):
# 如果訂單在提交或者允許的情況下,直接返回
if order.status in [order.Submitted, order.Accepted]:
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
return
# Check if an order has been completed
# Attention: broker could reject order if not enough cash
# 如果下單完成了
if order.status in [order.Completed]:
if order.isbuy():
self.log('BUY EXECUTED, %.2f' % order.executed.price)
elif order.issell():
self.log('SELL EXECUTED, %.2f' % order.executed.price)
# 記錄當前bar的狀態,並復制給自身屬性bar_executed
self.bar_executed = len(self)
# 如果下單的情況在另外狀態
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order Canceled/Margin/Rejected')
# Write down: no pending order
# 將訂單狀態重置為None
self.order = None
def next(self):
# Simply log the closing price of the series from the reference
self.log('Close, %.2f' % self.dataclose[0])
# Check if an order is pending ... if yes, we cannot send a 2nd one
if self.order:
return
# Check if we are in the market
# 空倉的狀態下,可以去判斷是否可以去買
if not self.position:
# Not yet ... we MIGHT BUY if ...
if self.dataclose[0] < self.dataclose[-1]:
# current close less than previous close
if self.dataclose[-1] < self.dataclose[-2]:
# previous close less than the previous close
# BUY, BUY, BUY!!! (with default parameters)
self.log('BUY CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.buy()
else:
# Already in the market ... we might sell
# 持倉的狀態下判斷當前的bar狀態點,超過5日進行賣出
if len(self) >= (self.bar_executed + 5):
# SELL, SELL, SELL!!! (with all possible default parameters)
self.log('SELL CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.sell()
if __name__ == '__main__':
# Create a cerebro entity
cerebro = bt.Cerebro()
# Add a strategy
cerebro.addstrategy(TestStrategy)
# Datas are in a subfolder of the samples. Need to find where the script is
# because it could have been called from anywhere
modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
datapath = os.path.join(modpath, '../datas/orcl-1995-2014.txt')
# Create a Data Feed
data = bt.feeds.YahooFinanceCSVData(
dataname=datapath,
# Do not pass values before this date
fromdate=datetime.datetime(1995, 1, 1),
# Do not pass values before this date
todate=datetime.datetime(1995, 3, 31),
# Do not pass values after this date
reverse=False)
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Set our desired cash start
cerebro.broker.setcash(100000.0)
# Print out the starting conditions
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
# Run over everything
cerebro.run()
# Print out the final result
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
After the execution the output is:
輸出
Starting Portfolio Value: 100000.00 2000-01-03, Close, 26.27 2000-01-04, Close, 23.95 2000-01-05, Close, 22.68 2000-01-05, BUY CREATE, 22.68 2000-01-06, BUY EXECUTED, 22.27 ... 2000-12-26, Close, 27.52 2000-12-27, Close, 27.30 2000-12-28, Close, 27.63 2000-12-29, Close, 25.85 2000-12-29, SELL CREATE, 25.85 Final Portfolio Value: 100017.52
Blistering Barnacles!!! The system made money … something must be wrong
我X,這個系統賺錢了…一定出了什么問題
The broker says: Show me the money!
經濟人說:"給我鈔票"
And the money is called “commission”.
這筆錢就是所謂的"佣金"
Let’s add a reasonable 0.1% commision rate per operation (both for buying and selling … yes the broker is avid …)
讓我們給每次操作設立一筆佣金千分之一0.1%(買進賣出都要,這個券商有點好)
A single line will suffice for it:
一行代碼就夠了。
# 0.1% ... divide by 100 to remove the % cerebro.broker.setcommission(commission=0.001)
Being experienced with the platform we want to see the profit or loss after a buy/sell cycle, with and without commission.
作為一個經驗豐富的平台,我們想知道每次交易買進,賣出的利潤,包括包含手續費與沒有手續費的情況
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime # For datetime objects
import os.path # To manage paths
import sys # To find out the script name (in argv[0])
# Import the backtrader platform
import backtrader as bt
# Create a Stratey
class TestStrategy(bt.Strategy):
# 這是一個輸出日志的小助手。
def log(self, txt, dt=None):
''' Logging function fot this strategy'''
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def __init__(self):
# Keep a reference to the "close" line in the data[0] dataseries
self.dataclose = self.datas[0].close
# To keep track of pending orders and buy price/commission
self.order = None
self.buyprice = None
self.buycomm = None
# 這是監控下單情況的方法。
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
return
# Check if an order has been completed
# Attention: broker could reject order if not enough cash
if 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))
# 賦值兩個價格,一個是買入價,一個是買入的時候手續費
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
else: # Sell Price為
self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
self.bar_executed = len(self)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order Canceled/Margin/Rejected')
self.order = None
# 監控交易周期是否完成
def notify_trade(self, trade):
# 正在交易返回空
if not trade.isclosed:
return
# 一堆交易結束,輸出信息,第一個為毛利潤,第二個為凈利潤(扣除手續費)
self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
(trade.pnl, trade.pnlcomm))
def next(self):
# Simply log the closing price of the series from the reference
self.log('Close, %.2f' % self.dataclose[0])
# Check if an order is pending ... if yes, we cannot send a 2nd one
# 是否用定單還有未完成的,有的話,不生成新的下單命令
if self.order:
return
# Check if we are in the market
if not self.position:
# Not yet ... we MIGHT BUY if ...
if self.dataclose[0] < self.dataclose[-1]:
# current close less than previous close
if self.dataclose[-1] < self.dataclose[-2]:
# previous close less than the previous close
# BUY, BUY, BUY!!! (with default parameters)
self.log('BUY CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.buy()
else:
# Already in the market ... we might sell
if len(self) >= (self.bar_executed + 5):
# SELL, SELL, SELL!!! (with all possible default parameters)
self.log('SELL CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.sell()
if __name__ == '__main__':
# Create a cerebro entity
cerebro = bt.Cerebro()
# Add a strategy
cerebro.addstrategy(TestStrategy)
# Datas are in a subfolder of the samples. Need to find where the script is
# because it could have been called from anywhere
modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
datapath = os.path.join(modpath, '../datas/orcl-1995-2014.txt')
# Create a Data Feed
data = bt.feeds.YahooFinanceCSVData(
dataname=datapath,
# Do not pass values before this date
fromdate=datetime.datetime(2000, 1, 1),
# Do not pass values before this date
todate=datetime.datetime(2000, 12, 31),
# Do not pass values after this date
reverse=False)
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Set our desired cash start
cerebro.broker.setcash(100000.0)
# Set the commission - 0.1% ... divide by 100 to remove the %
cerebro.broker.setcommission(commission=0.001)
# Print out the starting conditions
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
# Run over everything
cerebro.run()
# Print out the final result
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
After the execution the output is:
執行完成后的輸出:
Starting Portfolio Value: 100000.00 2000-01-03, Close, 26.27 2000-01-04, Close, 23.95 2000-01-05, Close, 22.68 2000-01-05, BUY CREATE, 22.68 2000-01-06, BUY EXECUTED, Price: 22.27, Cost: 22.27, Comm 0.02 2000-01-06, Close, 21.35 2000-01-07, Close, 22.99 ... 2000-12-21, BUY EXECUTED, Price: 24.74, Cost: 24.74, Comm 0.02 2000-12-21, Close, 26.24 2000-12-22, Close, 28.35 2000-12-26, Close, 27.52 2000-12-27, Close, 27.30 2000-12-28, Close, 27.63 2000-12-29, Close, 25.85 2000-12-29, SELL CREATE, 25.85 Final Portfolio Value: 100016.06
God Save the Queen!!! The system still made money.
上帝保佑女王!!這個系統仍然在賺錢。
Before moving on, let’s notice something by filtering the “OPERATION PROFIT” lines:
在繼續之前,讓我們通過過濾“運營利潤”來注意一些事情(只留下每對交易的盈虧數據):
Starting Portfolio Value: 100000.00 2000-01-14, OPERATION PROFIT, GROSS 1.97, NET 1.92 2000-02-07, OPERATION PROFIT, GROSS 3.48, NET 3.43 2000-02-28, OPERATION PROFIT, GROSS 4.23, NET 4.17 2000-03-13, OPERATION PROFIT, GROSS 3.28, NET 3.21 2000-03-22, OPERATION PROFIT, GROSS -0.39, NET -0.46 2000-04-07, OPERATION PROFIT, GROSS 2.31, NET 2.24 2000-04-20, OPERATION PROFIT, GROSS -1.83, NET -1.90 2000-05-02, OPERATION PROFIT, GROSS 5.15, NET 5.08 2000-05-11, OPERATION PROFIT, GROSS -3.53, NET -3.59 2000-05-30, OPERATION PROFIT, GROSS -1.39, NET -1.45 2000-07-05, OPERATION PROFIT, GROSS -1.53, NET -1.60 2000-07-14, OPERATION PROFIT, GROSS 1.97, NET 1.90 2000-07-28, OPERATION PROFIT, GROSS 0.14, NET 0.07 2000-08-08, OPERATION PROFIT, GROSS 4.11, NET 4.04 2000-08-21, OPERATION PROFIT, GROSS 0.97, NET 0.90 2000-09-15, OPERATION PROFIT, GROSS -4.00, NET -4.08 2000-09-27, OPERATION PROFIT, GROSS 1.22, NET 1.15 2000-10-13, OPERATION PROFIT, GROSS -2.81, NET -2.87 2000-10-26, OPERATION PROFIT, GROSS 2.84, NET 2.78 2000-11-06, OPERATION PROFIT, GROSS -3.39, NET -3.45 2000-11-16, OPERATION PROFIT, GROSS 1.22, NET 1.17 2000-12-01, OPERATION PROFIT, GROSS 2.45, NET 2.41 2000-12-18, OPERATION PROFIT, GROSS -0.06, NET -0.11 Final Portfolio Value: 100016.06
Adding up the “NET” profits the final figure is:
所有的凈利潤之和為
14.97
But the system said the following at the end:
但是系統的最后說了
2000-12-29T00:00:00, SELL CREATE, 27.41 Final Portfolio Value: 100016.98
And obviously 15.83 is not 16.98. There is no error whatsoever. The “NET” profit of 15.83 is already cash in the bag.
顯然15.83不是16.98。沒有任何錯誤。15.83的“凈”利潤已經是囊中之物了。(本人運行輸出為14.97與16.06)
Unfortunately (or fortunately to better understand the platform) there is an open position on the last day of the Data Feed. Even if a SELL operation has been sent … IT HAS NOT YET BEEN EXECUTED.
不幸的是(或幸運的是,更好地理解這個平台),在最后一天的數據Feed有一個開放的定位(滿足BUY的條件)。即使SELL操作已經被發送,它也還沒有被執行。(根據我手工的計算,按照最后截止日的收盤價計算的,實際該訂單應該在第二天開盤買入)
The “Final Portfolio Value” calculated by the broker takes into account the “Close” price on 2000-12-29. The actual execution price would have been set on the next trading day which happened to be 2001-01-02. Extending the Data Feed” to take into account this day the output is:
由經紀人計算的“最終投資組合價值”參考了2000年12月29日的“收盤”價格。實際執行價格將在下一個交易日確定也就是2001-01-02。擴展feed數據考慮到這一天的輸出是:
2001-01-02T00:00:00, SELL EXECUTED, Price: 27.87, Cost: 27.87, Commission 0.03 2001-01-02T00:00:00, OPERATION PROFIT, GROSS 1.64, NET 1.59 2001-01-02T00:00:00, Close, 24.87 2001-01-02T00:00:00, BUY CREATE, 24.87 Final Portfolio Value: 100017.41
Now adding the previous NET profit to the completed operation’s net profit:
現將之前的凈利潤與最后一筆已完成的業務凈利潤相加:
15.83 + 1.59 = 17.42
Which (discarding rounding errors in the “print” statements) is the extra Portfolio above the initial 100000 monetary units the strategy started with.
其中(剔除“打印”報表中的舍入誤差)是策略開始時超過最初10萬個貨幣單位的額外投資組合。
Customizing the Strategy: Parameters
定制策略:參數
It would a bit unpractical to hardcode some of the values in the strategy and have no chance to change them easily. Parameters come in handy to help.
在策略中一些固定值是不常用的,並且沒有機會輕易地更改它們。參數可以派上用場。
Definition of parameters is easy and looks like:
定義參數很簡單,就像下面這樣:
params = (('myparam', 27), ('exitbars', 5),)
Being this a standard Python tuple with some tuples inside it, the following may look more appealling to some:
這是一個標准的Python元組,里面有一些元組,下面的看起來可能更受一些人歡迎:
params = (
('myparam', 27),
('exitbars', 5),
)
With either formatting parametrization of the strategy is allowed when adding the strategy to the Cerebro engine:
另外一種添加參數的方式,在將策略添加到Cerebro引擎時,允許對策略進行格式化參數化:
# Add a strategy cerebro.addstrategy(TestStrategy, myparam=20, exitbars=7)
Note
The setsizing method below is deprecated. This content is kept here for anyone looking at old samples of the sources. The sources have been update to use:
下面的setsizing方法已棄用。此內容保存在這里,供任何查看源代碼的舊示例的人使用。已更新源以使用:
cerebro.addsizer(bt.sizers.FixedSize, stake=10)``
請閱讀關於sizers的部分
Using the parameters in the strategy is easy, as they are stored in a “params” attribute. If we for example want to set the stake fix, we can pass the stake parameter to the position sizer like this durint init:
在策略里面使用定義的參數很簡單,它將參數保存在對象的"params"屬性里面。讓我們做個示例,來修復下單的數量,我們可以在init初始化過程中,通過參數讀取具體的值
下面這個已經被棄用了
# Set the sizer stake from the params self.sizer.setsizing(self.params.stake)
We could have also called buy and sell with a stake parameter and self.params.stake as the value.
當我們要使用買和賣的時候,也可以通過參數self.paramd.stake來使用。
The logic to exit gets modified:
退出邏輯通過內置參數的調用進行修改:
# Already in the market ... we might sell if len(self) >= (self.bar_executed + self.params.exitbars):
With all this in mind the example evolves to look like:
所有一點點意見演變成現在的樣子
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime # For datetime objects
import os.path # To manage paths
import sys # To find out the script name (in argv[0])
# Import the backtrader platform
import backtrader as bt
# Create a Stratey
class TestStrategy(bt.Strategy):
params = (
('exitbars', 5),
('stake', 10),
)
def log(self, txt, dt=None):
''' Logging function fot this strategy'''
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def __init__(self):
# Keep a reference to the "close" line in the data[0] dataseries
self.dataclose = self.datas[0].close
# To keep track of pending orders and buy price/commission
self.order = None
self.buyprice = None
self.buycomm = None
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
return
# Check if an order has been completed
# Attention: broker could reject order if not enough cash
if 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))
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
else: # Sell
self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
self.bar_executed = len(self)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order Canceled/Margin/Rejected')
self.order = None
def notify_trade(self, trade):
if not trade.isclosed:
return
self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
(trade.pnl, trade.pnlcomm))
def next(self):
# Simply log the closing price of the series from the reference
self.log('Close, %.2f' % self.dataclose[0])
# Check if an order is pending ... if yes, we cannot send a 2nd one
if self.order:
return
# Check if we are in the market
if not self.position:
# Not yet ... we MIGHT BUY if ...
if self.dataclose[0] < self.dataclose[-1]:
# current close less than previous close
if self.dataclose[-1] < self.dataclose[-2]:
# previous close less than the previous close
# BUY, BUY, BUY!!! (with default parameters)
self.log('BUY CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.buy()
else:
# Already in the market ... we might sell
if len(self) >= (self.bar_executed + self.params.exitbars):
# SELL, SELL, SELL!!! (with all possible default parameters)
self.log('SELL CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.sell()
if __name__ == '__main__':
# Create a cerebro entity
cerebro = bt.Cerebro()
# Add a strategy
cerebro.addstrategy(TestStrategy)
# Datas are in a subfolder of the samples. Need to find where the script is
# because it could have been called from anywhere
modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
datapath = os.path.join(modpath, '../datas/orcl-1995-2014.txt')
# Create a Data Feed
data = bt.feeds.YahooFinanceCSVData(
dataname=datapath,
# Do not pass values before this date
fromdate=datetime.datetime(2000, 1, 1),
# Do not pass values before this date
todate=datetime.datetime(2000, 12, 31),
# Do not pass values after this date
reverse=False)
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Set our desired cash start
cerebro.broker.setcash(100000.0)
# Add a FixedSize sizer according to the stake
# 通過在實例中添加每次交易筆數,書中說這種快被淘汰了
cerebro.addsizer(bt.sizers.FixedSize, stake=10)
# Set the commission - 0.1% ... divide by 100 to remove the %
cerebro.broker.setcommission(commission=0.001)
# Print out the starting conditions
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
# Run over everything
cerebro.run()
# Print out the final result
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
After the execution the output is:
執行輸出的結果(就直接參照案例中的):
Starting Portfolio Value: 100000.00 2000-01-03T00:00:00, Close, 27.85 2000-01-04T00:00:00, Close, 25.39 2000-01-05T00:00:00, Close, 24.05 2000-01-05T00:00:00, BUY CREATE, 24.05 2000-01-06T00:00:00, BUY EXECUTED, Size 10, Price: 23.61, Cost: 236.10, Commission 0.24 2000-01-06T00:00:00, Close, 22.63 ... ... ... 2000-12-20T00:00:00, BUY CREATE, 26.88 2000-12-21T00:00:00, BUY EXECUTED, Size 10, Price: 26.23, Cost: 262.30, Commission 0.26 2000-12-21T00:00:00, Close, 27.82 2000-12-22T00:00:00, Close, 30.06 2000-12-26T00:00:00, Close, 29.17 2000-12-27T00:00:00, Close, 28.94 2000-12-28T00:00:00, Close, 29.29 2000-12-29T00:00:00, Close, 27.41 2000-12-29T00:00:00, SELL CREATE, 27.41 Final Portfolio Value: 100169.80
In order to see the difference, the print outputs have also been extended to show the execution size.
為了查看差異,還擴展了打印輸出以顯示執行大小。
Having multiplied the stake by 10, the obvious has happened: the profit and loss has been multiplied by 10. Instead of 16.98, the surplus is now 169.80
把股份交易增加了10倍之后,明顯的事情發生了:利潤和損失增加了10倍。盈余從原來的16.98變成了169.80
Adding an indicator
添加一些指標
Having heard of indicators, the next thing anyone would add to the strategy is one of them. For sure they must be much better than a simple “3 lower closes” strategy.
聽說過指標之后,接下來任何人都可以在策略中添加。當然它們必須比"3低收盤"策略要好的多
Inspired in one of the examples from PyAlgoTrade a strategy using a Simple Moving Average.
從PyAlgoTrade的一個例子例子中得到啟發,使用簡單移動平均線策略
-
Buy “AtMarket” if the close is greater than the Average
- 如果收盤價大於平均價格,在市場買入
-
If in the market, sell if the close is smaller than the Average
-
如果收盤價小於平均價格,在市場賣出
-
Only 1 active operation is allowed in the market
- 在市場中只允許一個活躍的操作
Most of the existing code can be kept in place. Let’s add the average during init and keep a reference to it:
大多數現有代碼可以無須更改。讓我們在init中添加平均值並保持對它的引用:
self.sma = bt.indicators.MovingAverageSimple(self.datas[0], period=self.params.maperiod)
And of course the logic to enter and exit the market will rely on the Average values. Look in the code for the logic.
當然進入和退出市場的邏輯依賴與平均值。在代碼中尋找邏輯
Note
The starting cash will be 1000 monetary units to be in line with the PyAlgoTrade example and no commission will be applied
注意
根據PyAlgoTrade的例子,初始現金為1000個貨幣單位,不收取佣金
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime # For datetime objects
import os.path # To manage paths
import sys # To find out the script name (in argv[0])
# Import the backtrader platform
import backtrader as bt
# Create a Stratey
class TestStrategy(bt.Strategy):
# 參數設置
params = (
('maperiod', 15),
)
# 日志輸出
def log(self, txt, dt=None):
''' Logging function fot this strategy'''
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def __init__(self):
# Keep a reference to the "close" line in the data[0] dataseries
self.dataclose = self.datas[0].close
# To keep track of pending orders and buy price/commission
self.order = None
self.buyprice = None
self.buycomm = None
# Add a MovingAverageSimple indicator
# 初始化日均線,第一個參數是數據來源,第二個參數為日均線的時間
self.sma = bt.indicators.SimpleMovingAverage(
self.datas[0], period=self.params.maperiod)
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
return
# Check if an order has been completed
# Attention: broker could reject order if not enough cash
if 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))
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
else: # Sell
self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
self.bar_executed = len(self)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order Canceled/Margin/Rejected')
self.order = None
def notify_trade(self, trade):
if not trade.isclosed:
return
self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
(trade.pnl, trade.pnlcomm))
def next(self):
# Simply log the closing price of the series from the reference
self.log('Close, %.2f' % self.dataclose[0])
# Check if an order is pending ... if yes, we cannot send a 2nd one
if self.order:
return
# Check if we are in the market
if not self.position:
# Not yet ... we MIGHT BUY if ...
# 收盤價大於當日的均線價格
if self.dataclose[0] > self.sma[0]:
# BUY, BUY, BUY!!! (with all possible default parameters)
self.log('BUY CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.buy()
else:
# 收盤價小於當日均線的價格
if self.dataclose[0] < self.sma[0]:
# SELL, SELL, SELL!!! (with all possible default parameters)
self.log('SELL CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.sell()
if __name__ == '__main__':
# Create a cerebro entity
cerebro = bt.Cerebro()
# Add a strategy
cerebro.addstrategy(TestStrategy)
# Datas are in a subfolder of the samples. Need to find where the script is
# because it could have been called from anywhere
modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
datapath = os.path.join(modpath, '../datas/orcl-1995-2014.txt')
# Create a Data Feed
data = bt.feeds.YahooFinanceCSVData(
dataname=datapath,
# Do not pass values before this date
fromdate=datetime.datetime(2000, 1, 1),
# Do not pass values before this date
todate=datetime.datetime(2000, 12, 31),
# Do not pass values after this date
reverse=False)
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Set our desired cash start
cerebro.broker.setcash(1000.0)
# Add a FixedSize sizer according to the stake
cerebro.addsizer(bt.sizers.FixedSize, stake=10)
# Set the commission
cerebro.broker.setcommission(commission=0.0)
# Print out the starting conditions
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
# Run over everything
cerebro.run()
# Print out the final result
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
Now, before skipping to the next section LOOK CAREFULLY to the first date which is shown in the log:
現在,在跳到下一章節之前,仔細的看一下日志中顯示的第一條數據
-
It’ no longer 2000-01-03, the first trading day in the year 2K.
- 開始不再是2000年01月03日,公元2000年的第一個交易日。
-
It’s 2000-01-24 … Who has stolen my cheese?
- 開始是2000-01-24,誰偷了我的奶酪?
The missing days are not missing. The platform has adapted to the new circumstances:
消失的日子並沒有消失。平台為了適應新的環境:
-
An indicator (SimpleMovingAverage) has been added to the Strategy.
- 策略中增加了一個指標(SimpleMovingAverage)。
-
This indicator needs X bars to produce an output: in the example: 15
- 這個指示器需要X條來產生輸出:在示例中為15條
-
2000-01-24 is the day in which the 15th bar occurs
2000-01-24是第15個bar出現的日子
The backtrader platform assumes that the Strategy has the indicator in place for a good reason, to use it in the decision making process. And it makes no sense to try to make decisions if the indicator is not yet ready and producing values.
backtrader平台,假設策略有一個很好的指標,在決策過程中將使用它。如果指標還沒有准備好、還沒有產生價值,那么試圖做出決定就沒有意義。
-
next will be 1st called when all indicators have already reached the minimum needed period to produce a value
- 當所有的指標已經達到產生一個值所需的最小周期,將首先調用next方法
-
In the example there is a single indicator, but the strategy could have any number of them.
- 在本示例中只有一個指標,但一個策略可以有很多指標
After the execution the output is:
最后的執行輸出:
Starting Portfolio Value: 1000.00 2000-01-24T00:00:00, Close, 25.55 2000-01-25T00:00:00, Close, 26.61 2000-01-25T00:00:00, BUY CREATE, 26.61 2000-01-26T00:00:00, BUY EXECUTED, Size 10, Price: 26.76, Cost: 267.60, Commission 0.00 2000-01-26T00:00:00, Close, 25.96 2000-01-27T00:00:00, Close, 24.43 2000-01-27T00:00:00, SELL CREATE, 24.43 2000-01-28T00:00:00, SELL EXECUTED, Size 10, Price: 24.28, Cost: 242.80, Commission 0.00 2000-01-28T00:00:00, OPERATION PROFIT, GROSS -24.80, NET -24.80 2000-01-28T00:00:00, Close, 22.34 2000-01-31T00:00:00, Close, 23.55 2000-02-01T00:00:00, Close, 25.46 2000-02-02T00:00:00, Close, 25.61 2000-02-02T00:00:00, BUY CREATE, 25.61 2000-02-03T00:00:00, BUY EXECUTED, Size 10, Price: 26.11, Cost: 261.10, Commission 0.00 ... ... ... 2000-12-20T00:00:00, SELL CREATE, 26.88 2000-12-21T00:00:00, SELL EXECUTED, Size 10, Price: 26.23, Cost: 262.30, Commission 0.00 2000-12-21T00:00:00, OPERATION PROFIT, GROSS -20.60, NET -20.60 2000-12-21T00:00:00, Close, 27.82 2000-12-21T00:00:00, BUY CREATE, 27.82 2000-12-22T00:00:00, BUY EXECUTED, Size 10, Price: 28.65, Cost: 286.50, Commission 0.00 2000-12-22T00:00:00, Close, 30.06 2000-12-26T00:00:00, Close, 29.17 2000-12-27T00:00:00, Close, 28.94 2000-12-28T00:00:00, Close, 29.29 2000-12-29T00:00:00, Close, 27.41 2000-12-29T00:00:00, SELL CREATE, 27.41 Final Portfolio Value: 973.90
In the name of the King!!! A winning system turned into a losing one … and that with no commission. It may well be that simply adding an indicator is not the universal panacea.
傻了吧,一個贏錢的變成了一個輸錢的系統,而且沒有佣金。簡單增加指標並不是萬能的添加劑
Note
注意
The same logic and data with PyAlgoTrade yields a slightly different result (slightly off). Looking at the entire printout reveals that some operations are not exactly the same. Being the culprit again the usual suspect: rounding.
使用相同的邏輯和數據,PyAlgoTrade會產生稍微不同的結果(稍微差一點)。查看整個打印輸出,可以發現有些操作並不完全相同。再次成為罪魁禍首的是通常的疑點:四舍五入。
PyAlgoTrade does not round the datafeed values when applying the divided “adjusted close” to the data feed values.
當對數據提要值應用划分的“調整關閉”時,PyAlgoTrade沒有四舍五入datafeed值。
The Yahoo Data Feed provided by backtrader rounds the values down to 2 decimals after applying the adjusted close. Upon printing the values everything seems the same, but it’s obvious that sometimes that 5th place decimal plays a role.
在應用調整后的收盤價后,backtrader提供的雅虎數據將價值四舍五入到2小數。在打印值的時候,一切看起來都是一樣的,但是很明顯,有時候第5位的小數起了作用。
Rounding down to 2 decimals seems more realistic, because Market Exchanges do only allow a number of decimals per asset (being that 2 decimals usually for stocks)
四舍五入到2小數似乎更現實一些,因為市場交易只允許每項資產有幾個小數(股票通常是2個小數)
backtrader會自動四舍五入數值
Note
The Yahoo Data Feed (starting with version 1.8.11.99 allows to specify if rounding has to happen and how many decimals)
注意:
Yahoo Data Feed(從1.8.11.99版本開始,允許指定是否需要四舍五入以及需要多少小數)
Visual Inspection: Plotting
視覺檢查:繪圖
A printout or log of the actual whereabouts of the system at each bar-instant is good but humans tend to be visual and therefore it seems right to offer a view of the same whereabouts as chart.
為每個bar打印輸出或者記錄實際位置是很好的系統,但是人類往往更加喜歡可視化的,因此它似乎是正確的,提供相同的行蹤作為圖標的視圖。
Note
注意:
To plot you need to have matplotlib installed
畫圖要先裝matplotlib
Once again defaults for plotting are there to assist the platform user. Plotting is incredibly a 1 line operation:
同樣,繪圖的默認設置是為了幫助平台用戶。繪圖是令人難以置信的1個命令操作:
cerebro.plot()
Being the location for sure after cerebro.run() has been called.
這個命令確保在cerebro.run()之后執行
In order to display the automatic plotting capabilities and a couple of easy customizations, the following will be done:
為了顯示自動繪圖功能和一對簡單的自定義,以下將做:
-
A 2nd MovingAverage (Exponential) will be added. The defaults will plot it (just like the 1st) with the data.
- 第二條平均線會被添加。默認會跟第一條一樣被畫出
-
A 3rd MovingAverage (Weighted) will be added. Customized to plot in an own plot (even if not sensible)
- 第三條平均線被添加。自定義在自己的plot中畫出(即使不合理)
- 簡單的說,本來均線應該跟bar在一起,現在分開了,他有自己的小畫框了。
-
A Stochastic (Slow) will be added. No change to the defaults.
-
A MACD will be added. No change to the defaults.
-
A RSI will be added. No change to the defaults.
-
A MovingAverage (Simple) will be applied to the RSI. No change to the defaults (it will be plotted with the RSI)
-
An AverageTrueRange will be added. Changed defaults to avoid it being plotted.
- 一個AverageTrueRange指標被添加。改變了默認的參數,避免被畫出
The entire set of additions to the init method of the Strategy:
策略的init方法的整個添加:
# Indicators for the plotting show bt.indicators.ExponentialMovingAverage(self.datas[0], period=25) bt.indicators.WeightedMovingAverage(self.datas[0], period=25,subplot = True) bt.indicators.StochasticSlow(self.datas[0]) bt.indicators.MACDHisto(self.datas[0]) rsi = bt.indicators.RSI(self.datas[0]) bt.indicators.SmoothedMovingAverage(rsi, period=10) bt.indicators.ATR(self.datas[0]).plot = False
Note
注意
Even if indicators are not explicitly added to a member variable of the strategy (like self.sma = MovingAverageSimple…), they will autoregister with the strategy and will influence the minimum period for next and will be part of the plotting.
即使指標沒有顯式地添加到策略的成員變量(如self.sma = MovingAverageSimple…)。,他們將自動注冊與策略,並將影響下一個最小周期,並將成為繪圖的一部分。
In the example only RSI is added to a temporary variable rsi with the only intention to create a MovingAverageSmoothed on it.
在這個例子中,只有RSI被添加到臨時變量RSI中,只是為了在其上創建一個MovingAverageSmoothed。
示例如下:
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime # For datetime objects
import os.path # To manage paths
import sys # To find out the script name (in argv[0])
# Import the backtrader platform
import backtrader as bt
# Create a Stratey
class TestStrategy(bt.Strategy):
params = (
('maperiod', 15),
)
def log(self, txt, dt=None):
''' Logging function fot this strategy'''
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def __init__(self):
# Keep a reference to the "close" line in the data[0] dataseries
self.dataclose = self.datas[0].close
# To keep track of pending orders and buy price/commission
self.order = None
self.buyprice = None
self.buycomm = None
# Add a MovingAverageSimple indicator
self.sma = bt.indicators.SimpleMovingAverage(
self.datas[0], period=self.params.maperiod)
# Indicators for the plotting show
# 添加的指標線
bt.indicators.ExponentialMovingAverage(self.datas[0], period=25)
bt.indicators.WeightedMovingAverage(self.datas[0], period=25,
subplot=True)
bt.indicators.StochasticSlow(self.datas[0])
bt.indicators.MACDHisto(self.datas[0])
# 神奇的地方
rsi = bt.indicators.RSI(self.datas[0])
bt.indicators.SmoothedMovingAverage(rsi, period=10)
bt.indicators.ATR(self.datas[0], plot=False)
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
return
# Check if an order has been completed
# Attention: broker could reject order if not enough cash
if 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))
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
else: # Sell
self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
self.bar_executed = len(self)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order Canceled/Margin/Rejected')
# Write down: no pending order
self.order = None
def notify_trade(self, trade):
if not trade.isclosed:
return
self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
(trade.pnl, trade.pnlcomm))
def next(self):
# Simply log the closing price of the series from the reference
self.log('Close, %.2f' % self.dataclose[0])
# Check if an order is pending ... if yes, we cannot send a 2nd one
if self.order:
return
# Check if we are in the market
if not self.position:
# Not yet ... we MIGHT BUY if ...
if self.dataclose[0] > self.sma[0]:
# BUY, BUY, BUY!!! (with all possible default parameters)
self.log('BUY CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.buy()
else:
if self.dataclose[0] < self.sma[0]:
# SELL, SELL, SELL!!! (with all possible default parameters)
self.log('SELL CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.sell()
if __name__ == '__main__':
# Create a cerebro entity
cerebro = bt.Cerebro()
# Add a strategy
cerebro.addstrategy(TestStrategy)
# Datas are in a subfolder of the samples. Need to find where the script is
# because it could have been called from anywhere
modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
datapath = os.path.join(modpath, '../datas/orcl-1995-2014.txt')
# Create a Data Feed
data = bt.feeds.YahooFinanceCSVData(
dataname=datapath,
# Do not pass values before this date
fromdate=datetime.datetime(2000, 1, 1),
# Do not pass values before this date
todate=datetime.datetime(2000, 12, 31),
# Do not pass values after this date
reverse=False)
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Set our desired cash start
cerebro.broker.setcash(1000.0)
# Add a FixedSize sizer according to the stake
cerebro.addsizer(bt.sizers.FixedSize, stake=10)
# Set the commission
cerebro.broker.setcommission(commission=0.0)
# Print out the starting conditions
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
# Run over everything
cerebro.run()
# Print out the final result
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
# Plot the result
cerebro.plot()
After the execution the output is:
執行輸出
Starting Portfolio Value: 1000.00 2000-02-18T00:00:00, Close, 27.61 2000-02-22T00:00:00, Close, 27.97 2000-02-22T00:00:00, BUY CREATE, 27.97 2000-02-23T00:00:00, BUY EXECUTED, Size 10, Price: 28.38, Cost: 283.80, Commission 0.00 2000-02-23T00:00:00, Close, 29.73 ... ... ... 2000-12-21T00:00:00, BUY CREATE, 27.82 2000-12-22T00:00:00, BUY EXECUTED, Size 10, Price: 28.65, Cost: 286.50, Commission 0.00 2000-12-22T00:00:00, Close, 30.06 2000-12-26T00:00:00, Close, 29.17 2000-12-27T00:00:00, Close, 28.94 2000-12-28T00:00:00, Close, 29.29 2000-12-29T00:00:00, Close, 27.41 2000-12-29T00:00:00, SELL CREATE, 27.41 Final Portfolio Value: 981.00
The final result has changed even if the logic hasn’t. This is true but the logic has not been applied to the same number of bars.
即使邏輯沒有改變,最終的結果還是改變了。這是正確的,因為這種邏輯並沒有應用到相同數量的bar上。
Note
注意:
As explained before, the platform will first call next when all indicators are ready to produce a value. In this plotting example (very clear in the chart) the MACD is the last indicator to be fully ready (all 3 lines producing an output). The 1st BUY order is no longer scheduled during Jan 2000 but close to the end of Feb 2000.
如前所述,當所有指標都准備好產生一個值時,平台將首先調用next。在這個繪圖示例中(在圖表中非常清楚),MACD是最后一個完全准備好了的指示符(所有3行生成一個輸出)。第一個訂單不再安排在2000年1月,而是接近2000年2月底。
圖:

Let’s Optimize
讓我們來進行優化
Many trading books say each market and each traded stock (or commodity or ..) have different rythms. That there is no such thing as a one size fits all.
許多交易書籍說每個市場和每個交易的股票(或商品或...)擁有不同的規律。沒有一刀切的辦法。
Before the plotting sample, when the strategy started using an indicator the period default value was 15 bars. It’s a strategy parameter and this can be used in an optimization to change the value of the parameter and see which one better fits the market.
在繪制樣本之前,當策略開始使用指示符時,周期默認值為15條。這是一個策略參數,可以用於優化,改變參數的值,看看哪個更適合市場。
Note
注意
There is plenty of literature about Optimization and associated pros and cons. But the advice will always point in the same direction: do not overoptimize. If a trading idea is not sound, optimizing may end producing a positive result which is only valid for the backtested dataset.
關於優化的相關優缺點有很多文獻。但是這些建議總是指向同一個方向:不要過度優化。如果一個交易的想法不健全,優化可能產生一個積極的結果,但也僅適合與這些回測數據。
The sample is modified to optimize the period of the Simple Moving Average. For the sake of clarity any output with regards to Buy/Sell orders has been removed
對樣本進行修改,以優化簡單移動平均的周期。為了清楚起見,任何有關買賣訂單的輸出都被刪除了
The example now:
代碼示例如下:
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime # For datetime objects
import os.path # To manage paths
import sys # To find out the script name (in argv[0])
# Import the backtrader platform
import backtrader as bt
# Create a Stratey
class TestStrategy(bt.Strategy):
params = (
('maperiod', 15),
# 打印輸出開關
('printlog', False),
)
def log(self, txt, dt=None, doprint=False):
''' Logging function fot this strategy'''
# 很聰明的設置了一個默認參數,並使用了短路原則是否輸出log
if self.params.printlog or doprint:
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def __init__(self):
# Keep a reference to the "close" line in the data[0] dataseries
self.dataclose = self.datas[0].close
# To keep track of pending orders and buy price/commission
self.order = None
self.buyprice = None
self.buycomm = None
# Add a MovingAverageSimple indicator
self.sma = bt.indicators.SimpleMovingAverage(
self.datas[0], period=self.params.maperiod)
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
return
# Check if an order has been completed
# Attention: broker could reject order if not enough cash
if 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))
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
else: # Sell
self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
self.bar_executed = len(self)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order Canceled/Margin/Rejected')
# Write down: no pending order
self.order = None
def notify_trade(self, trade):
if not trade.isclosed:
return
self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
(trade.pnl, trade.pnlcomm))
def next(self):
# Simply log the closing price of the series from the reference
self.log('Close, %.2f' % self.dataclose[0])
# Check if an order is pending ... if yes, we cannot send a 2nd one
if self.order:
return
# Check if we are in the market
if not self.position:
# Not yet ... we MIGHT BUY if ...
if self.dataclose[0] > self.sma[0]:
# BUY, BUY, BUY!!! (with all possible default parameters)
self.log('BUY CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.buy()
else:
if self.dataclose[0] < self.sma[0]:
# SELL, SELL, SELL!!! (with all possible default parameters)
self.log('SELL CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.sell()
# 定義該方法,用於一次測試結束后的輸出。
def stop(self):
self.log('(MA Period %2d) Ending Value %.2f' %
(self.params.maperiod, self.broker.getvalue()), doprint=True)
if __name__ == '__main__':
# Create a cerebro entity
cerebro = bt.Cerebro()
# Add a strategy
# 添加策略,並輸入參數范圍
strats = cerebro.optstrategy(
TestStrategy,
maperiod=range(10, 31))
# Datas are in a subfolder of the samples. Need to find where the script is
# because it could have been called from anywhere
modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
datapath = os.path.join(modpath, '../datas/orcl-1995-2014.txt')
# Create a Data Feed
data = bt.feeds.YahooFinanceCSVData(
dataname=datapath,
# Do not pass values before this date
fromdate=datetime.datetime(2000, 1, 1),
# Do not pass values before this date
todate=datetime.datetime(2000, 12, 31),
# Do not pass values after this date
reverse=False)
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Set our desired cash start
cerebro.broker.setcash(1000.0)
# Add a FixedSize sizer according to the stake
cerebro.addsizer(bt.sizers.FixedSize, stake=10)
# Set the commission
cerebro.broker.setcommission(commission=0.0)
# Run over everything
# 單核跑,按照順序輸出
cerebro.run(maxcpus=1)
Instead of calling addstrategy to add a stratey class to Cerebro, the call is made to optstrategy. And instead of passing a value a range of values is passed.
調用optstrategy不是調用addstrategy來向大腦添加一個stratey類。不是傳遞一個值,而是傳遞一個值的范圍。
One of the “Strategy” hooks is added, the stop method, which will be called when the data has been exhausted and backtesting is over. It’s used to print the final net value of the portfolio in the broker (it was done in Cerebro previously)
添加了一個“策略”鈎子,stop方法,當數據耗盡並且回溯測試結束時,將調用該方法。它用於打印經紀人中投資組合的最終凈值(以前在大腦中是這樣做的)
The system will execute the strategy for each value of the range. The following will be output:
系統將對范圍內的每個值執行策略。將輸出以下內容:
2000-12-29, (MA Period 10) Ending Value 880.30 2000-12-29, (MA Period 11) Ending Value 880.00 2000-12-29, (MA Period 12) Ending Value 830.30 2000-12-29, (MA Period 13) Ending Value 893.90 2000-12-29, (MA Period 14) Ending Value 896.90 2000-12-29, (MA Period 15) Ending Value 973.90 2000-12-29, (MA Period 16) Ending Value 959.40 2000-12-29, (MA Period 17) Ending Value 949.80 2000-12-29, (MA Period 18) Ending Value 1011.90 2000-12-29, (MA Period 19) Ending Value 1041.90 2000-12-29, (MA Period 20) Ending Value 1078.00 2000-12-29, (MA Period 21) Ending Value 1058.80 2000-12-29, (MA Period 22) Ending Value 1061.50 2000-12-29, (MA Period 23) Ending Value 1023.00 2000-12-29, (MA Period 24) Ending Value 1020.10 2000-12-29, (MA Period 25) Ending Value 1013.30 2000-12-29, (MA Period 26) Ending Value 998.30 2000-12-29, (MA Period 27) Ending Value 982.20 2000-12-29, (MA Period 28) Ending Value 975.70 2000-12-29, (MA Period 29) Ending Value 983.30 2000-12-29, (MA Period 30) Ending Value 979.80
不是調用addstrategy將一個stratey類添加到Cerebro,而是調用optstrategy。而不是傳遞一個值,而是傳遞一個范圍的值。 添加了一個“策略”鈎子,即stop方法,該方法將在耗盡數據和返回測試結束時被調用。它用於在經紀人中打印投資組合的最終凈值(之前在Cerebro中做過) 系統將對范圍內的每個值執行策略。輸出內容如下:
Results:
結果:
-
For periods below 18 the strategy (commissionless) loses money.
- 在18以下的時期,該策略(無佣金)虧損。
-
For periods between 18 and 26 (both included) the strategy makes money.
- 在18到26之間(包括兩者),這種策略會賺錢。
-
Above 26 money is lost again.
- 26以上的又虧錢了。
而該策略和給定數據集的獲勝周期為:
20的數據,獲得78.00個單位超過1000美元/歐元(7.8%)
Note
注意:
The extra indicators from the plotting example have been removed and the start of operations is only influenced by the Simple Moving Average which is being optimized. Hence the slightly different results for period 15
繪圖示例中的額外指標已被刪除,操作的開始只受正在優化的簡單移動平均線的影響。只有第15期的結果略有不同
Conclusion
結論
The incremental samples have shown how to go from a barebones script to a fully working trading system which even plots the results and can be optimized.
遞增的示例展示了如何從一個基本的腳本變成一個完全工作的交易系統,該系統甚至可以繪制結果並進行優化。
A lot more can be done to try to improve the chances of winning:
為了提高獲勝的幾率,我們可以做的還有很多:
Self defined Indicators
自定義指標
Creating an indicator is easy (and even plotting them is easy)
創建指示符很容易(甚至繪制它們也很容易)
Sizers
分級器
Money Management is for many the key to success
對許多人來說,資金管理是成功的關鍵
Order Types (limit, stop, stoplimit)
訂單類型(限制、停止、止損)
Some others
其他一些方法
To ensure all the above items can be fully utilized the documentation provides an insight into them (and other topics)
為了確保上述所有內容都能得到充分利用,文檔提供了對它們(以及其他主題)的深入了解
Look in the table of contents and keep on reading … and developing.
查看目錄,繼續閱讀……並發展。
Best of luck
最好的運氣
OVER
二次回看,確實寫的不錯的官方文檔。
