Backtrader Quickstart を読んでみる。

株でも先物でもあまりに負けて、完全に資産がなくなりました。
手をつけちゃいけないお金に手をつけようとは思っているのですが、その前にもうこれは自分の手でやるのは無理だと悟ったので、なんかのルールで自動でトレードしようと思っています。
やりたい手法は決まっているのですが、パラメータ調整とかバックテストとかしてバクチ感をなるべく無くしたいので、pythonでバックテストができるというBacktraderについて勉強してみたいと思います。
とりあえずDeepLさん・Google翻訳さんを頼りにまずはBacktraderのQuickstartを大胆に超意訳していこうと思います。

英語もpythonもたいしてできないので内容の正しさには全く自信ありません。

クイックスタート

<注意事項>
ファイルとかは色々更新されていくから、これを書いた時の出力とは変わってるかもしれないけど、まあいいよね。

プラットフォームの利用

簡単なプログラムから本格的なやつまで、例を示しながら見ていくよ。ただ、その前にBacktraderで作業するときの2つの基本的な概念を大まかに説明するね。

1.Lines
データフィード、指標、ストラテジーは「ライン」を持つことになるよ。
「ライン」っていうのはポイントの連続したもので、マーケットで言えば、データフィードには普通、毎日以下のようなポイントがあるよ。
・始値、高値、安値、終値、出来高、オープンインタレスト(建玉にかかる金利)
例えば時間にそって「始値」を結んだものが「ライン」になるよ。だから、データフィードは普通6つのラインを持つことになるね。「日付」(1点を基準にしている)も考慮すると7つだね。


2.Index 0 アプローチ
「ライン」の中の値にアクセスする場合は、現在の値を「index:0」と考えるよ。そして、「直前の」出力値には「−1」でアクセスできる。Pythonの配列のアレと一緒だよね。我々がアクセスするのは「ライン」の最後の出力だから、「0」を使って「ライン」の中の現在の瞬間にアクセスするよ。
このことを頭に入れて、単純移動平均線の値にアクセスするのをイメージしてみよう。

self.sma = SimpleMovingAverage(......)

この移動平均線の現在値にアクセスするにはこうすればいいよね。

av = self.sma[0]

「0」は現在の瞬間を固有に識別するから、何本の小節/分/日/月が処理されたかを知る必要はないよ。pythonのアレと同じで「直前の」出力値には「−1」を使えばアクセスできるね。

previous_value = self.sma[-1]

もちろんさらに前の値にアクセスしたかったら −2, −3, ... とすればいいね。

From 0 to 100 : いろんなサンプルスクリプト

基本設定
さっそく実行してみよう。

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())

実行の後出力されるのは以下の通り。

Starting Portfolio Value: 10000.00
Final Portfolio Value: 10000.00

この例では以下の処理が行われているよ。
・backtraderのインポート
・Cerebro エンジンをインスタンス化
・Cerebro インスタンスがデータをループオーバーして実行するよう指示
・結果のプリントアウト

たいしたことではないけど、
・Cerebroエンジンはバックグラウンドでブローカーのインスタンスを作成している
・インスタンスにはすでに現金が入っている

ということについて指摘しておくね。

このブローカーのインスタンス化は、ユーザーの利便性のための、プラットフォームの一定の特徴となっているよ。ユーザーによってブローカーが設定されていない場合は、デフォルトのブローカーが配置されるよ。10K通貨単位っていうのは、いろんなブローカーで使われている一般的な値だね。

資金量の設定

相場の世界ではたった10kってのは「負け組」だよね。資金の量を変えて、もう一度例を実行してみよう。

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())

結果は以下の通りになるよ。

Starting Portfolio Value: 1000000.00
Final Portfolio Value: 1000000.00

うまくいったね。どんどん次の例に進もう。

データフィードの追加

我々の目的は、株価や指数などのデータを元に自動売買で資産を増やしていくことだよね。だから、データがなくちゃはじまらないよね。データを追加する方法をみてみよう。

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

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())

実行後の出力は以下の通り。

Starting Portfolio Value: 1000000.00
Final Portfolio Value: 1000000.00

・サンプルスクリプトがデータを見つけられるように、サンプルスクリプトがコンピュータのどこにあるのか見つけておくこと
・与えられたデータのどの部分を扱うのかをフィルタリングするためのdatetimeオブジェクトを作成する

これでデータフィードが作成されcerebroに追加されるね。出力はもちろん何も変わっていないよ。

<注意事項>
ヤフーオンラインでは、CSVデータは標準的な慣習ではない日付降順で送信されるよ。reversed=Trueは、ファイル内のCSVデータが既に反転しており、標準的な期待される日付昇順であることを考慮しているよ。

# 自分の環境では上のコードで指定されているフォルダにデータ(オラクルの株価?)はありませんでした。以下の記事を参考にしたら無事サンプルプログラムが動きました。
https://qiita.com/xxssxxx/items/e915b8afe51facc47a55
なんか終値が「Close」「Adj Close」と2つあって「Adj Close」の方を読み取ってるようですね。

最初のストラテジー

資金はブローカーにあって、データフィードもあれば準備万端だね。スクリプトの中にストラテジーを入れて、各日ごと(バー)の「終値」を表示してみよう。DataSeries(データフィードの基礎となるクラス)オブジェクトは、よく知られているOHLC(Open High Low Close)の日ごとの値にアクセスするためのエイリアスを持っているよ。これで、出力ロジックの作成が楽になるはず。


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 Strategy
class TestStrategy(bt.Strategy):
   def log(self, txt, dt=None):
       ''' Logging function for this strategy'''
       dt = dt or self.gatas[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 __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-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

とっても簡単だよね。いくつか内容をみていこうね。

・init が呼び出されると、ストラテジーはすでにプラットフォームに存在しているデータのリストを持っているよ。これはPythonの標準的なリストで、データは挿入された順にアクセスすることができるよ。リストの最初のデータ self.datas[0] は、取引操作のためのデフォルトのデータで、すべてのストラテジー要素を同期させるためのデータだよ(システムクロック)。

・self.dataclose = self.datas[0].close は、現在の終値への参照を保ち続けるよ。他の終値にアクセスするためには、「−1」「−2」などのインデックスを使えばいいよね。

・ストラテジーのnextメソッドは、システムクロック(self.datas [0])のバーごとに呼び出されるよ。これは、値を計算するためにいくつかのバーを必要とするインジケーターのようなものが登場するまで「true」だよ。これについては後で詳しく説明するね。

# 「true」の意味がわかりません。まあよしとして先へ進みます。

ストラテジーにロジックを追加する

ストラテジーに売買のロジックを追加していくよ。

・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


いくつかの 「買い」注文が発行され、我々の資産が減少したね。このスクリプトには重要なことがいくつか足りないよね。

・注文は作成されたけど、それがいつ、いくらで約定したかのか

次の例では、注文状況の通知を聞くことで、この点を改善してみよう。好奇心旺盛な読者は、どのくらいの株が買われているのか、どのような資産が買われているのか、注文はどのように執行されているのかを知りたいと思うかもしれないね。可能であれば(この例題ではそう)、プラットフォームがその穴を埋めてくれるよ。

・self.datas[0] (メインデータ、別名システムクロック)は、他のものが指定されていない場合、ターゲット資産である。

・ポジションのサイズは、デフォルトで固定値「1」を使用するポジションサイザーによって提供される。これは後の例題で修正される。

・注文は「成行」で執行される。ブローカー(前の例題で示した)は、次のバーの始値を使用して執行する。

・注文は、これまでのところ手数料なしで執行されている。 (詳細は後ほど)

買いだけではなく...売りも

マーケットへの参入方法(ロング)を知った後は、「出口戦略」が必要だし、また、ストラテジーがマーケットにあるかどうかを理解する必要があるよね。

・幸運なことに、ストラテジーオブジェクトは、デフォルトのデータフィードのポジション属性へのアクセスを提供する。

・メソッド「 買い」と「売り」は、作成された(まだ約定していない)注文を返す。

・注文状況の変更は、通知メソッドを介してストラテジーに通知される。

「出口戦略」は簡単なものだよ。

・良くも悪くも足5本(6本目)が経過したら決済
バーの本数自体には元々「時間」や「タイムフレーム」が存在しないことに注意してね。バーは1分、1時間、1日、1週間、または他の期間を表すことができるよ。我々はデータが日々のものであることを知っていますが、ストラテジーはそれについての仮定をしていないよ。

さらに単純化するために。
・ポジションがまだ市場にない場合にのみ買い注文を許可する。

<注意事項>
次のメソッドには「bar index」が渡されないから、5本のバーが経過したかどうかを理解する方法が不明瞭に思えるかもしれないけど、これはパイソン的なアレでモデル化されているよ。 オブジェクトで 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].close
       # 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)
           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 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)
   # 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-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, 23.61
2000-01-06T00:00:00, Close, 22.63
2000-01-07T00:00:00, Close, 24.37
2000-01-10T00:00:00, Close, 27.29
2000-01-11T00:00:00, Close, 26.49
2000-01-12T00:00:00, Close, 24.90
2000-01-13T00:00:00, Close, 24.77
2000-01-13T00:00:00, SELL CREATE, 24.77
2000-01-14T00:00:00, SELL EXECUTED, 25.70
2000-01-14T00:00:00, Close, 25.18
...
...
...
2000-12-15T00:00:00, SELL CREATE, 26.93
2000-12-18T00:00:00, SELL EXECUTED, 28.29
2000-12-18T00:00:00, Close, 30.18
2000-12-19T00:00:00, Close, 28.88
2000-12-20T00:00:00, Close, 26.88
2000-12-20T00:00:00, BUY CREATE, 26.88
2000-12-21T00:00:00, BUY EXECUTED, 26.23
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: 100018.53

スクリプトがお金を稼いだね。信じられないけど。

ブローカーが言う。「お金を見せてくれ!」

そしてそのお金は「手数料」と呼ばれているね。操作ごとに合理的な0.1%の手数料率を追加してみよう。ブローカーは熱心だから、売買の両方に手数料がかかるよ。
1行で十分だよ。

# 0.1% ... divide by 100 to remove the %
cerebro.broker.setcommission(commission=0.001)

プラットフォームで経験を積んだ我々は、手数料の有無にかかわらず、買い/売り後の損益を見てみたいと思うよね。

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
               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())

実行後の出力は以下の通り。

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, Price: 23.61, Cost: 23.61, Commission 0.02
2000-01-06T00:00:00, Close, 22.63
2000-01-07T00:00:00, Close, 24.37
2000-01-10T00:00:00, Close, 27.29
2000-01-11T00:00:00, Close, 26.49
2000-01-12T00:00:00, Close, 24.90
2000-01-13T00:00:00, Close, 24.77
2000-01-13T00:00:00, SELL CREATE, 24.77
2000-01-14T00:00:00, SELL EXECUTED, Price: 25.70, Cost: 25.70, Commission 0.03
2000-01-14T00:00:00, OPERATION PROFIT, GROSS 2.09, NET 2.04
2000-01-14T00:00:00, Close, 25.18
...
...
...
2000-12-15T00:00:00, SELL CREATE, 26.93
2000-12-18T00:00:00, SELL EXECUTED, Price: 28.29, Cost: 28.29, Commission 0.03
2000-12-18T00:00:00, OPERATION PROFIT, GROSS -0.06, NET -0.12
2000-12-18T00:00:00, Close, 30.18
2000-12-19T00:00:00, Close, 28.88
2000-12-20T00:00:00, Close, 26.88
2000-12-20T00:00:00, BUY CREATE, 26.88
2000-12-21T00:00:00, BUY EXECUTED, Price: 26.23, Cost: 26.23, Commission 0.03
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: 100016.98

なんとシステムはまだ儲かっているよ。

先に進む前に「OPERATION PROFIT」の行をフィルタリングして、あることに気付こう。

2000-01-14T00:00:00, OPERATION PROFIT, GROSS 2.09, NET 2.04
2000-02-07T00:00:00, OPERATION PROFIT, GROSS 3.68, NET 3.63
2000-02-28T00:00:00, OPERATION PROFIT, GROSS 4.48, NET 4.42
2000-03-13T00:00:00, OPERATION PROFIT, GROSS 3.48, NET 3.41
2000-03-22T00:00:00, OPERATION PROFIT, GROSS -0.41, NET -0.49
2000-04-07T00:00:00, OPERATION PROFIT, GROSS 2.45, NET 2.37
2000-04-20T00:00:00, OPERATION PROFIT, GROSS -1.95, NET -2.02
2000-05-02T00:00:00, OPERATION PROFIT, GROSS 5.46, NET 5.39
2000-05-11T00:00:00, OPERATION PROFIT, GROSS -3.74, NET -3.81
2000-05-30T00:00:00, OPERATION PROFIT, GROSS -1.46, NET -1.53
2000-07-05T00:00:00, OPERATION PROFIT, GROSS -1.62, NET -1.69
2000-07-14T00:00:00, OPERATION PROFIT, GROSS 2.08, NET 2.01
2000-07-28T00:00:00, OPERATION PROFIT, GROSS 0.14, NET 0.07
2000-08-08T00:00:00, OPERATION PROFIT, GROSS 4.36, NET 4.29
2000-08-21T00:00:00, OPERATION PROFIT, GROSS 1.03, NET 0.95
2000-09-15T00:00:00, OPERATION PROFIT, GROSS -4.26, NET -4.34
2000-09-27T00:00:00, OPERATION PROFIT, GROSS 1.29, NET 1.22
2000-10-13T00:00:00, OPERATION PROFIT, GROSS -2.98, NET -3.04
2000-10-26T00:00:00, OPERATION PROFIT, GROSS 3.01, NET 2.95
2000-11-06T00:00:00, OPERATION PROFIT, GROSS -3.59, NET -3.65
2000-11-16T00:00:00, OPERATION PROFIT, GROSS 1.28, NET 1.23
2000-12-01T00:00:00, OPERATION PROFIT, GROSS 2.59, NET 2.54
2000-12-18T00:00:00, OPERATION PROFIT, GROSS -0.06, NET -0.12

「NET」の利益を足していくと、最終的な数字が出てくるね。

15.83

しかし、システムは最後に次のように言っているよ。

2000-12-29T00:00:00, SELL CREATE, 27.41
Final Portfolio Value: 100016.98

明らかに15.83は16.98ではないよね。しかし何の誤差もないよ。15.83の「NET」利益はすでに決裁後の現金だよ。

残念ながら(またはプラットフォームをよりよく理解するためには幸いにもだね)データフィードの最終日にオープンポジションがまだあるよ。SELL操作が送信されている場合でも、それはまだ実行されてないね。

ブローカーによって計算された「最終ポートフォリオ価格」は、2000年12月29日の「終値」を考慮に入れているよ。実際の約定価格は、2001-01-02になった次の取引日に設定されているよ。この日を考慮に入れて「データフィードを拡張」すると出力されるよ。

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

今、完成した操作の純利益に前回のNET利益を追加するよ。

15.83 + 1.59 = 17.42

これは(丸め誤差は破棄した)、戦略を開始した最初の10K通貨単位を超える分の資産だよ。

ストラテジーのカスタマイズ : パラメータ

ストラテジーの一部の値をハードコード化することは、簡単に変更できないから少し実用的じゃないよね。その点パラメータは便利だね。パラメータの定義は簡単で、次のようになるよ。

params = (('myparam', 27), ('exitbars', 5),)

これは Python の標準的なタプルで、いくつかのタプルが入れ子になっているので、次のような書き方の方が魅力的かもしれないね。

params = (
   ('myparam', 27),
   ('exitbars', 5),
)

Cerebroエンジンにストラテジーを追加する場合、どちらのフォーマットでも、戦略のパラメーター化が許可されるよ。

# Add a strategy
cerebro.addstrategy(TestStrategy, myparam=20, exitbars=7)

<注意事項>
以下の setsizing メソッドは非推奨だよ。この内容は、ソースの古いサンプルを見ている人のためにここに保存されているよ。ソースは更新されて使用されているよ。

cerebro.addsizer(bt.sizers.FixedSize, stake=10)

サイザーについての項を読んでね。パラメータは「params」属性に格納されているから、ストラテジーのパラメータを使うのは簡単だよ。例えば、ステークフィックスを設定したい場合は、initのところで次のようにステークパラメータをポジションサイザーに渡すことができるよ。

# Set the sizer stake from the params
self.sizer.setsizing(self.params.stake)

また、stakeパラメータとself.params.stakeを値として、「買い」や「売り」を呼び出すこともできるよ。

決済するロジックが修正されるよ。

# Already in the market ... we might sell
if len(self) >= (self.bar_executed + self.params.exitbars):

これをすべて念頭に置いて、サンプルプログラムは以下のように進化するよ。

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),
   )

   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())

実行後の出力は以下の通り。

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

違いを確認するために、出力もポジションサイズを表示するように拡張されているよ。賭け金を10倍にすると、当然だけど利益と損失が10倍になったよ。16.98の代わりに、利益は169.80になったね。

インジケータの追加

インジケータの話を聞いたら誰もがストラテジーに追加するよね。確かに、単純な「3連続下落作戦」よりもはるかに優れているに違いないもんね。PyAlgoTradeの例の一つに触発されて、単純移動平均線を使った戦略を考えてみたよ。

・終値が平均値より上だったら「時価」で買い。

・ポジションがある場合、終値が移動平均値平均値を下回ったら、売り。

・同時に持つポジションは1つだけ。

既存のコードのほとんどはそのままで大丈夫だよ。init の所で単純移動平均を追加して、それを参照しておこう。

self.sma = bt.indicators.MovingAverageSimple(self.datas[0], period=self.params.maperiod)

そしてもちろん、市場でポジションをとって、ポジションを外すためのロジックは、平均値に依存するよ。ロジックのコードを見てね。

<注意事項>
開始時の現金は、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())

さて、次のセクションにスキップする前に、ログに表示されている最初の日付に注意してみてね。

・それはもはや2000-01-03ではなく、2000-01-24である。

抜けてるように見える日付は別に抜けているわけじゃないよ。プラットフォームは新しい状況に適応しているよ。

・インディケータ(SimpleMovingAverage)がストラテジーに追加された。

・このインジケーターは、出力を生成するためにX本のバーを必要とする。
 この例では15本だよ。

・2000-01-24は、15番目の足が発生する日である。

バックトレーダープラットフォームは、ストラテジーが適切な理由でインジケーターを備えており、意思決定プロセスでそれを使用することを前提としているよ。インジケータがまだ準備ができておらず、値を生成していない場合に意思決定をしようとするのは意味がないもんね。

・next は、すべてのインジケーターが値を生成するために必要な最小期間に達したときに最初に呼び出されるよ。

・この例では、インジケーターの数は1つだけど、ストラテジーは任意の数のインジケーターを持つことができるよ。

実行後の出力は次のようになるよ。

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

買っていたシステムが手数料抜きでも負けてしまったね。単にインジケーターを追加するだけでは万能ではないということかもね。

<注意事項>
PyAlgoTrade で同じロジックとデータを使用すると、わずかに異なる結果が得られるよ。出力全体を見ると、いくつかの操作がまったく同じではないことがわかるよ。原因はいつもの容疑者、端数処理のせいだよ。

PyAlgoTrade は、データフィードの値に分割された「調整済み終値」を適用する際に、データフィードの値を丸めないよ。

backtrader が提供する Yahoo データフィードは、調整後の終値を適用した後、値を小数第二位に丸めるよ。値を出力してみると、すべて同じように見えるけど、明らかに小数点以下5桁目の数値が影響していることがあるよ。

市場取引所は、固有の資産ごとに小数の値を許可していないから、小数第二位に丸めたほうが現実的だよね(株では小数点以下2桁が普通だよ)。

<注意事項>
ヤフーデータフィード(バージョン1.8.11.99から)は、小数を丸めるか、また何桁で丸めるかを指定することができるよ。

視覚化 : プロッティング

各バーインスタンスごとのシステムの出力やログも良いんだけど、人間は視覚的な傾向があるので、チャートのような表示を提供するのが正しいようだね。

<注意事項>
プロットするには matplotlib がインストールされている必要があるよ。

もう一度言うけど、プロットのデフォルト値はプラットフォームのユーザーを支援するために存在するよ。プロットは信じられないほど1行でできるよ。

cerebro.plot()

cerebro.run() が呼ばれた後に置くことを確実にしてね。
自動プロット機能といくつかの簡単なカスタマイズを表示するために、以下のようになるよ。

・2つ目の MovingAverage (Exponential) が追加される。デフォルトでは、データと一緒に(1番目の移動平均と同様に)プロットされる。

・3番目の移動平均(加重)が追加される。独自のプロットでプロットするようにカスタマイズされる(感覚的でなくても)。

・ストキャスティクス(スロー)が追加される。デフォルトに変更なし。

・MACDが追加される。デフォルト値に変更なし。

・RSI が追加される。デフォルト値に変更なし。

・RSI に移動平均(単純)が適用される。デフォルトに変更なし(RSIと一緒にプロットされる

・AverageTrueRangeが追加される。プロットされないようにデフォルトを変更。

ストラテジーの 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

<注意事項>
インジケーターがストラテジーのメンバー変数に明示的に追加されていない場合(self.sma = MovingAverageSimple…など)でも、インジケーターはストラテジーに自動登録され、next の最小期間に影響を及ぼし、プロットの一部になるよ。

この例では、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()

実行後の出力は以下の通り。

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

ロジックが適用されていなくても、最終的な結果は変わったよ。これは正しいけど、ロジックは同じ数のバーに適用されていないよ。

<注意事項>
前に説明したように、プラットフォームは、すべてのインジケータが値を生成する準備ができているときに、最初に next の呼び出しを行うよ。このプロット例(チャートでは非常に明確)では、MACDは完全に準備ができた最後のインジケーターだね(3本のラインすべてが出力しています)。1回目のBUY注文は2000年1月中には予定されていなくて、2000年2月の終わり近くになっているね。

最適化しよう

多くの取引の本は、各市場と各取引された株式(または商品または...)は、異なるリズムを持っていると言うね。ワンサイズですべてが収まるというものはないってことだね。

サンプルをプロットする前、ストラテジーがインジケーターの使用を開始したとき、期間のデフォルト値は15バーだったね。これは戦略パラメータで、これを最適化で使用してパラメータの値を変更し、どれが市場により適しているかを確認できるよ。

<注意事項>
最適化に関連する長所と短所についての文献がたくさんあるね。しかし、どの本も過剰に最適化しないようにアドバイスしているよ。取引のアイデアが健全でない場合、最適化することで、バックテストされたデータセットのみに有効なよい結果が得られる可能性があるね。

サンプルは、単純移動平均の期間を最適化するように修正されているよ。分かりやすくするために、売買注文に関する出力は削除してあるよ。

現在のサンプルスクリプト

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

Cerebro に stratey クラスを追加するために addstrategy を呼び出す代わりに、  optstrategy を呼び出すよ。そして、値の代わりに値の範囲が渡されるよ。

「Strategy」フックの1つであるstopメソッドが追加されたよ。これは、データが使い果たされ、バックテストが終了したときに呼び出されるよ。ブローカーでポートフォリオの最終的な総資産を出力するために使用されるよ(以前はCerebroで行われていたね)。

システムは、範囲の各値に対してストラテジーを実行するよ。以下のように出力されるよ。

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

結果:
・MA periodが17以下の期間では、ストラテジー(手数料なし)は損をする。

・MA periodが18と25の間の期間については、ストラテジーは利益を出す。

・MA periodが26以上では再び資産が失われている。

そして、このストラテジーと与えられたデータセットに対する最高の期間は

・MA periodが20のときで利益は7.8%

<注意事項>
余分な指標は削除されているので、操作の開始は最適化されている単純移動平均にのみ影響されているね。したがって、期間15の結果は先の例とはわずかに異なっているよ。

結論

サンプルスクリプトでは、最低限のものから、結果をプロットし、最適化することができる完全に動作するトレーディングシステムへと移行する方法を示してきたよ。

勝利の可能性を高めるために、さらに多くのことができるよ。

・自己定義の指標
インジケータの作成は簡単だよ(プロットも簡単)。

・サイザー
お金の管理は多くの人にとって成功の鍵だね。

・注文の種類(指値、逆指値、ストップリミット)

・その他

上記のすべての項目を十分に活用できるようにするために、ドキュメントはそれら(およびその他のトピック)に関する造詣を提供するよ。

目次を見て、読み続けて・・・発展させてね。

幸運を祈るよ!

この記事が気に入ったらサポートをしてみませんか?