Backtrader Platform Conceptsを読んでみる。

Quickstart を自己流で読んでみたけど、あまり何かができるようになった気がしません。ゴールデンクロスでうんちゃらかんちゃら、ぐらいだと実戦では使えないですもんね。なんか無駄なことをやっているような気もしてきましたが、もう少し勉強してみようと思います。

プラットフォームの考え方

これはプラットフォームを使う上で役立つ情報を集めたものだよ。

はじめる前に

コード例は全部、以下がインポートされることを前提にしているよ。

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

<注意事項>
インジケータやフィードなどのサブモジュールにアクセスするための代替構文だね。

import backtrader as bt

そしてその後このように使うね。

thefeed = bt.feeds.OneOfTheFeeds(...)
theind = bt.indicators.SimpleMovingAverage(...)

データフィード - データの受け渡し

プラットフォームとの作業の基礎は、ストラテジーによって行われるよ。そして、これらはデータフィードを渡される。プラットフォームのエンドユーザーは、それらを受け取ることを気にする必要はないけどね。
データフィードは、配列と配列の位置へのショートカットの形で、ストラテジーに自動的にメンバー変数を提供するよ。

以下は、ストラテジーの派生クラス宣言とプラットフォームの実行のクイックプレビューだよ。

class MyStrategy(bt.Strategy):
   params = dict(period=20)

   def __init__(self):

       sma = btind.SimpleMovingAverage(self.datas[0], period=self.params.period)

   ...

cerebro = bt.Cerebro()

...

data = btfeeds.MyFeed(...)
cerebro.adddata(data)

...

cerebro.addstrategy(MyStrategy, period=30)

...

以下のことに注意してね。
・ストラテジーの __init__ メソッドでは *args や **kwargs が受信されていない。 (使用中かもしれない)。

・少なくとも一つの項目を保持する配列/リスト/文字列可能なメンバ変数 self.datas が存在する。 (うまくいけば例外が発生する)。

そう。データフィードはプラットフォームに追加され、システムに追加された順にストラテジーの中に表示されるよ。

<注意事項>
これは、エンドユーザーが独自のインジケータを開発したり、既存のインジケータリファレンスのソースコードを見たりする場合にも適用されるよ。

データフィードのショートカット

self.datasの配列項目は、追加の自動メンバ変数で直接アクセスすることができるよ。

・self.data targets self.datas[0]
・self.dataX targets self.datas[X]

例はこんな感じ

class MyStrategy(bt.Strategy):
   params = dict(period=20)

   def __init__(self):

       sma = btind.SimpleMovingAverage(self.data, period=self.params.period)

   ...

データフィードの省略

上の例をさらに簡略化すると、以下のようになるよ。

class MyStrategy(bt.Strategy):
   params = dict(period=20)

   def __init__(self):

       sma = btind.SimpleMovingAverage(period=self.params.period)

   ...

SimpleMovingAverage の呼び出しから self.data が完全に削除されたね。こうなるとインディケータ(この場合はSimpleMovingAverage)は、作成されているオブジェクト(ストラテジー)の最初のデータであるself.data(別名:self.data0またはself.datas[0])を受信するよ。

ほとんど全てがデータフィード

データフィードはデータというだけでなく、データを受け渡すこともできるよ。インジケータや操作の結果もまたデータだね。さっきの例では SimpleMovingAverage は入力データとして self.datas[0]を受け取っていたよ。インジケータの出力をデータとして扱っている例は以下の通り。

class MyStrategy(bt.Strategy):
   params = dict(period1=20, period2=25, period3=10, period4)

   def __init__(self):

       sma1 = btind.SimpleMovingAverage(self.datas[0], period=self.p.period1)

       # This 2nd Moving Average operates using sma1 as "data"
       sma2 = btind.SimpleMovingAverage(sma1, period=self.p.period2)

       # New data created via arithmetic operation
       something = sma2 - sma1 + self.data.close

       # This 3rd Moving Average operates using something  as "data"
       sma3 = btind.SimpleMovingAverage(something, period=self.p.period3)

       # Comparison operators work too ...
       greater = sma3 > sma1

       # Pointless Moving Average of True/False values but valid
       # This 4th Moving Average operates using greater  as "data"
       sma3 = btind.SimpleMovingAverage(greater, period=self.p.period4)

   ...

基本的にすべてのものは、一度操作されるとデータフィードとして使用できるオブジェクトに変換されるよ。

パラメータ

プラットフォーム内のほとんどのクラスは、パラメータをサポートしているよ。

・パラメータはデフォルト値とともにクラス属性 (タプルのタプルまたは辞書的なオブジェクト) として宣言される。

・キーワード引数(**kwargs)は、一致するパラメータがないかスキャンされ、見つかった場合は**kwargsから削除され、対応するパラメータに値が代入される。

・最終的にパラメータは、メンバ変数 self.params (短縮形: self.p) にアクセスすることで、クラスのインスタンスで使用することができる。

前回のクイックストラテジーのプレビューには、すでにパラメータの例が含まれているんだけど、念のためもう一度、パラメータだけに焦点を当てているよ。
タプルを使うと

class MyStrategy(bt.Strategy):
   params = (('period', 20),)

   def __init__(self):
       sma = btind.SimpleMovingAverage(self.data, period=self.p.period)

辞書を使うと

class MyStrategy(bt.Strategy):
   params = dict(period=20)

   def __init__(self):
       sma = btind.SimpleMovingAverage(self.data, period=self.p.period)

ライン

プラットフォーム内の他のほとんどのオブジェクトは Line を有効にしたオブジェクトだよ。エンドユーザーの視点から見ると、これは意味があるよ。

・1つ以上のラインシリーズを保持できる。ラインシリーズは値の配列で、値がチャートにまとめられて線を形成する。

ライン(またはラインシリーズ)の良い例には、株式の終値によって形成されたラインがあるね。これは実際によく知られたチャートだよね。
プラットフォームの定期的な利用は、ラインへのアクセスのみに関係しているよ。前のミニストラテジーを少し拡張して、

class MyStrategy(bt.Strategy):
   params = dict(period=20)

   def __init__(self):

       self.movav = btind.SimpleMovingAverage(self.data, period=self.p.period)

   def next(self):
       if self.movav.lines.sma[0] > self.data.lines.close[0]:
           print('Simple Moving Average is greater than the closing price')

ラインを持つ2つのオブジェクトが公開されているよ。

・self.data これは lines 属性を持っていて、順番に close 属性を含むよ。
・self.movav は SimpleMovingAverage インジケータで、lines 属性を持っていて、順番に sma 属性を含むよ。

<注意事項>
このことからも明らかなように、ラインには名前がつけられるよ。これらは宣言の順序に従って順次アクセスすることもできるけど、それはインジケータの開発のときにだけ使われるべきだね。

そして、close と sma という2つのラインは、値を比較するためにポイント(index:0)を取得できるよ。

ラインへのショートカットアクセスが存在するよ。

・xxx.line は xxx.l に短縮できる。
・xxx.line.name は xxx.line_name に短縮できる。
・ストラテジーや指標のような複雑なオブジェクトは、データのラインに素早くアクセスすることができる。
 ・self.data_name は self.data.lines.name への直接アクセスを提供する。 
 ・これは番号付きデータ変数にも適用される
                : self.data1_name -> self.data1.lines.name

さらに、ライン名には直接アクセスすることができるよ。

・self.data.close and self.movav.sma

しかし、実際にラインにアクセスされているかどうかは、前のものと同じように表記が明確にならないね。

<注意事項>
この2つの表記での設定/代入はサポートされていないよ。

ラインの宣言

インジケータを開発している場合、そのインジケータが持つラインを宣言する必要があるよ。params の場合と同じように、これはクラス属性として宣言されるんだけど、今回はタプルでしかできないよ。辞書は、挿入順序に従った保存ができないため、サポートされてないよ。

単純移動平均の場合は次のようになるね。

class SimpleMovingAverage(Indicator):
   lines = ('sma',)

   ...

<注意事項>
宣言の後に続くカンマは、タプルに単一の文字列を渡す場合に必要だよ。Pythonの構文が間違っている数少ない1つかもしれないね。

前の例で見たように、この宣言は、インジケータにsma ラインを作成して、後でストラテジーのロジックで(そしておそらく他のインジケータによってさらに複雑なインジケータを作成するために)アクセスできるよ。

開発では、名前のない一般的な方法でラインにアクセスすると便利な場合があるね。この場合、番号付きのアクセスが便利だよ。

・self.line[0] は self.line.sma を指す。

より多くの行が定義されていた場合、それらはインデックス1,2,およびそれ以上でアクセスされるよ。もちろん、追加の短縮形も存在するよ。

・self.line は self.line[0] を指す。
・self.lineX は self.line[X] を指す。
・self.line_X は self.line[X] を指す。

データフィードを受信しているオブジェクトの内部では、これらのデータフィードの下にあるラインにも番号で素早くアクセスすることができます。

・self.dataY は self.data.line[Y] を指す。
・self.dataX_Y は self.dataX.lines[X] を指し、これは self.datas[X].lines[Y] の完全な短縮版である。

データフィードの中のラインへのアクセス

データフィードの内部では、ラインを省略してアクセスすることもできるよ。これにより、終値のような考え方で作業することがより自然になるね。
例えば、

data = btfeeds.BacktraderCSVData(dataname='mydata.csv')

...

class MyStrategy(bt.Strategy):

   ...

   def next(self):

       if self.data.close[0] > 30.0:
           ...

これは、同じように有効な次のような場合よりも自然なように思えるよ。

 if self.data.lines.close[0] > 30.0

 同じことは、推論があるインジケータには当てはまらないよ。

・インジケータは、中間計算を保持するclose属性を持つことができる。これは後にcloseという名前で実際のラインに配信される。

データフィードの場合、それは単なるデータソースであるため、計算は行われないね。

ラインの長さ

ラインには一連の点があり、実行中に動的に成長するので、Python標準のlen関数を呼び出すことによっていつでも長さを測定することができるよ。
それは例えば以下の例に当てはまるよ。

・データフィード
・ストラテジー
・インジケータ

データがプリロードされると、追加のプロパティがデータフィードに適用されるよ。

・buflen メソッド

buflen はデータフィード用に読み込まれたバーの総数を報告するよ。

len と buflen の違い

・len は処理されたバーの数を返す
・buflen はデータフィードにロードされたバーの総数を返す

両方が同じ値を返すときは、データがプリロードされていないか、プリロードされたバーが全て処理されたかのどちらかだよ。(システムがライブフィードに接続していない限りこれは処理の終了を意味するよ。)

ラインとパラメーターの継承

パラメーターとラインの宣言をサポートするため、ある種のメタ言語が存在するよ。Pythonの標準的な継承ルールと互換性を持たせるためにあらゆる努力がされているよ。

パラメータの継承
継承は期待通りに動作するはずだよ。

・複数の継承に対応
・基底クラスからのパラメータは継承される
・複数の基底クラスが同じパラメーターを定義している場合は、継承リストの最後のクラスのデフォルト値が使用される
・同じパラメーターが子クラスで再定義された場合、新しいデフォルト値は基底クラスを引き継ぐことになる

ラインの継承
・複数の継承に対応
・すべての基底クラスからのラインは継承される。名前付きのラインであるため、同じ名前が基底クラスで複数回使われている場合は、1つのバージョンだけが存在する

# クラスとか継承とか正直しんどいです…。

インデックス:0 と −1

前に見たように、ラインはラインシリーズで、プロットされたとき線(時間軸にそって終値を結んだような)を形作るポイントの集合を持っているよね。
通常のコードでこれらのポイントにアクセスするために、現在の get/set インスタントに0ベースのアプローチを使用することが選択されているよ。

ストラテジーは値を取得するだけ。インジケータも値を設定するよ。
前の例から次の方法が簡潔に見られるよ。

def next(self):
   if self.movav.lines.sma[0] > self.data.lines.close[0]:
       print('Simple Moving Average is greater than the closing price')

インデックス0を適用して、移動平均の現在の値と現在の終値を取得しているね。

<注意事項>
実際には、インデックス0の場合や論理/算術演算子を適用する場合、次のように直接比較できるよ。

if self.movav.lines.sma > self.data.lines.close:
   ...

このドキュメントの後半で、演算子の説明を参照してね。
現在の出力はインジケータによって設定される必要があるため、設定は例えばインジケータを開発するときに使用されることを意図しているよ。

SimpleMovingAverage は次のように現在の get/set ポイントにたいして計算できるよ。

def next(self):
 self.line[0] = math.fsum(self.data.get(0, size=self.p.period)) / self.p.period

以前のセットポイントへのアクセスは、Pythonが配列/イテラブルオブジェクトにアクセスするときの−1に従ってモデル化しているよ。

・配列の最後の項目を指す

プラットフォームは(現在のライブの get/set ポイントの前の)最後の項目を−1とみなすよ。そのため、現在の終値とひとつ前の終値を比較するには、0と−1を比較することになるよ。ストラテジーでは例えば、

def next(self):
   if self.data.close[0] > self.data.close[-1]:
       print('Closing price is higher today')

もちろん、−1より前の価格には−2,−3,…でアクセスできるよ。

スライシング

backtrader はラインオブジェクトのスライスをサポートしていないよ。これは「0」および「−1」インデックススキームに従う設計上の決定だよ。通常のインデックス可能なPythonオブジェクトでは次のようになるよ。

myslice = self.my_sma[0:]  # slice from the beginning til the end

ただし、0…の選択では、それが実際に現在提供されている値で、その後には何もないことを覚えておいてね。また、

myslice = self.my_sma[0:-1]  # slice from the beginning til the end

繰り返しになるけど、0は現在の値で、−1は直近の(前の)値だよ。そのため、backtrader のシステムでは0から−1というスライスは意味がないね。
スライスがサポートされるとすると、次のようになるね。

myslice = self.my_sma[:0]  # slice from current point backwards to the beginning

もしくは

myslice = self.my_sma[-1:0]  # last value and current value

もしくは

myslice = self.my_sma[-3:-1]  # from last value backwards to the 3rd last value

スライスを取得する
最新の値を持つ配列を得ることができるよ。

myslice = self.my_sma.get(ago=0, size=1)  # default values shown

これはサイズ1の配列を返し、現時点を0としてさかのぼるスタートとしているよ。
現在の時点から10個の値を取得するには(つまり、最後の10個の値を取得するには)以下の通り。

myslice = self.my_sma.get(size=10)

もちろん配列には期待通りの順序があるよ。左端の値は最も古い値で、右端の値は最新の値だね。(通常のPython配列で、ラインオブジェクトではないよ。)
現在の点だけを抜いて最新の10個の値を取得するには以下の通りにすればいいよ。

myslice = self.my_sma.get(ago=-1, size=10)

ライン:DELAYEDインデックス

[ ]演算子構文は、next ロジックフェーズで個々の値を抽出するためにあるよ。ラインオブジェクトは --init-- フェーズ中に遅延ラインオブジェクトを介して値をアドレス指定するための追加表記をサポートしているよ。
ロジックへの関心が、前の終値と単純移動平均の実際の値とを比較することだとしようか。next の反復ごとに手動で実行するのではなく、あらかじめ用意されたラインオブジェクトを生成できるよ。

class MyStrategy(bt.Strategy):
   params = dict(period=20)

   def __init__(self):

       self.movav = btind.SimpleMovingAverage(self.data, period=self.p.period)
       self.cmpval = self.data.close(-1) > self.sma

   def next(self):
       if self.cmpval[0]:
           print('Previous close is higher than the moving average')

ここで(delay)表記が使われているよ。

・これは終値のレプリカを提供するが、−1によって遅延する。
 そして、 self.data.close(-1) > self.sma の比較は条件が真の時は1,偽の時は0返すラインオブジェクトを生成するよ。

ラインのカップリング

( )演算子を上で示されたように遅延値とともに使用して、遅延されたバージョンのラインオブジェクトを提供できるよ。
遅延値を設定せずに構文を使用すると、 LinesCoupler ラインオブジェクトが返されるよ。これは、異なるタイムフレームのデータを操作するインジケータ間の結合を意味するよ。

異なるタイムフレームのデータフィードには異なる長さがあり、それらを操作するインジケータはデータの長さを複製するよ。例えば、

・日足のデータには、年間約250のバーがある。
・週足のデータには、年間52のバーがある。

2つの単純移動平均の比較する操作を作成しようとすると、それぞれ日足・週足のデータで操作をすると破綻するね。どうやって日足の250ものバーを週足の52のバーに一致させるのかは不明確だよね。
これを読んでる人は、日足と週足の対応を見つけるために、バックグラウンドで日付の比較が行われることを想像するけど、

・インジケータは単なる数式であり、日時情報を持たない
 彼らは環境について何も知らないよ。データが十分な値を提供する場合、計算が行われる可能性があるだけだね。

( )(空欄)表記が役に立つよ。

class MyStrategy(bt.Strategy):
   params = dict(period=20)

   def __init__(self):

       # data0 is a daily data
       sma0 = btind.SMA(self.data0, period=15)  # 15 days sma
       # data1 is a weekly data
       sma1 = btind.SMA(self.data1, period=5)  # 5 weeks sma

       self.buysig = sma0 > sma1()

   def next(self):
       if self.buysig[0]:
           print('daily sma is greater than weekly sma1')

ここで、より大きな(週足)タイムフレームインジケータである sma1 は sma1() で日足のタイムフレームに結合されているよ。これは sma0 のより多くのバーと互換性のあるオブジェクトを返し、sma1 によって生成された値をコピーして52のバーを250のバーに効率的に分散するよ。

自然な構造を使用した演算子

使いやすさのため、プラットフォームは演算子の使用を Python の制約内で許可しているよ。さらに使いやすくするために、演算子の使用は2つのステージに分かれているよ。

ステージ1 演算子がオブジェクトを作成する
これについてはっきりと意図されていなくても、例にはすでに出て来ていたよ。インジケータやストラテジーのようなオブジェクトの初期化フェーズ(--init-- メソッド)の中に、演算子はストラテジーのロジックの評価フェーズ中に後で使用するために操作、割当、または参照として保持できるオブジェクトを作成するよ。
ここでも、SimpleMovingAverage の実装の可能性があり、さらにステップに分けられるよ。SimpleMovingAverageインジケータ --init-- 内のコードは次のようになるよ。

def __init__(self):
   # Sum N period values - datasum is now a *Lines* object
   # that when queried with the operator [] and index 0
   # returns the current sum

   datasum = btind.SumN(self.data, period=self.params.period)

   # datasum (being *Lines* object although single line) can be
   # naturally divided by an int/float as in this case. It could
   # actually be divided by anothr *Lines* object.
   # The operation returns an object assigned to "av" which again
   # returns the current average at the current instant in time
   # when queried with [0]

   av = datasum / self.params.period

   # The av *Lines* object can be naturally assigned to the named
   # line this indicator delivers. Other objects using this
   # indicator will have direct access to the calculation

   self.line.sma = av

ストラテジーの初期化中に、より完全なユースケースが表示されるよ。

class MyStrategy(bt.Strategy):

   def __init__(self):

       sma = btind.SimpleMovinAverage(self.data, period=20)

       close_over_sma = self.data.close > sma
       sma_dist_to_high = self.data.high - sma

       sma_dist_small = sma_dist_to_high < 3.5

       # Unfortunately "and" cannot be overridden in Python being
       # a language construct and not an operator and thus a
       # function has to be provided by the platform to emulate it

       sell_sig = bt.And(close_over_sma, sma_dist_small)

上の操作が行われたあと、sell_sig はラインオブジェクトで、後でストラテジーのロジックで使用でき、条件が満たされているかどうかを示すよ。

ステージ2 演算子の本領発揮
最初にストラテジーはシステムが処理するすべてのバーに対して呼び出される next メソッドがあることを覚えておいてね。これは演算子が実際にステージ2モードにある場所だよ。先程の例を元に組み立てると

class MyStrategy(bt.Strategy):

   def __init__(self):

       self.sma = sma = btind.SimpleMovinAverage(self.data, period=20)

       close_over_sma = self.data.close > sma
       self.sma_dist_to_high = self.data.high - sma

       sma_dist_small = sma_dist_to_high < 3.5

       # Unfortunately "and" cannot be overridden in Python being
       # a language construct and not an operator and thus a
       # function has to be provided by the platform to emulate it

       self.sell_sig = bt.And(close_over_sma, sma_dist_small)

   def next(self):

       # Although this does not seem like an "operator" it actually is
       # in the sense that the object is being tested for a True/False
       # response

       if self.sma > 30.0:
           print('sma is greater than 30.0')

       if self.sma > self.data.close:
           print('sma is above the close price')

       if self.sell_sig:  # if sell_sig == True: would also be valid
           print('sell sig is True')
       else:
           print('sell sig is False')

       if self.sma_dist_to_high > 5.0:
           print('distance from sma to hig is greater than 5.0')

あまり使えるストラテジーではなく、単なる例だよ。ステージ2の間、演算子は期待値(真偽をテストするならブール値、浮動小数点を比較するなら浮動小数点数)を返し、算術演算も返すよ。

<注意事項>
比較では[ ]演算子を使用していないことに注意してね。これは簡素化のためだよ。

if self.sma > 30.0 は self.sma[0] と 30.0 を比較するよ。
if self.sma > self.data.close は self.sma[0] と self.data.close[0] を比較するよ。

いくつかの non-overrides の演算子/関数
Python はすべてをオーバーライドすることを許可しないから、事例に対処するためにいくつかの関数が提供されているよ。

<注意事項>
ステージ1で、あとで値を提供するオブジェクトを作成するためにのみ使用することを意図しているよ。

Operators:
・and -> And
・or -> Or

Logic Control:
・if -> If

Functions:
・any -> Any
・all -> All
・cmp -> Cmp
・max -> Max
・min -> Min
・sum -> Sum
プラットフォームは浮動小数点数で動作し、通常の合計を適用すると精度に影響を与える可能性があるため、Sum は実際には math.fsum を基になる操作として使用するよ。
・reduce -> Reduce

これらのユーテリティ演算子/関数はイテラブルで動作するよ。イテラブルの要素は、通常のPython数値型(int, float, ...)やラインを持つオブジェクトにすることができるよ。
とってもバカな買いシグナルを生成する例を挙げると

class MyStrategy(bt.Strategy):

   def __init__(self):

       sma1 = btind.SMA(self.data.close, period=15)
       self.buysig = bt.And(sma1 > self.data.close, sma1 > self.data.high)

   def next(self):
       if self.buysig[0]:
           pass  # do something here

sma1 が高値を高い場合、それは終値よりも高くなくてはならないことは明らかだよね。でも、ポイントは bt.And の使用を示しているよ。
bt.If の使用は

class MyStrategy(bt.Strategy):

   def __init__(self):

       sma1 = btind.SMA(self.data.close, period=15)
       high_or_low = bt.If(sma1 > self.data.close, self.data.low, self.data.high)
       sma2 = btind.SMA(high_or_low, period=15)

分析すると

・終値15本での単純移動平均を生成
・その後
  ・bt.If は単純移動平均の値が終値を上回るときには安値を返し、そうでないときには高音を返すよ。bt.If が呼び出されているときには実際の値は返されないことに注意してね。単純移動平均のようなラインオブジェクトを返すよ。
値はシステムが実行されるときに計算されるよ。
・生成された bt.If ラインオブジェクトは sma2 に送られて、高音と安値が計算に使用されるね。これらの関数は数値も受け取るよ。
同じ例に変更を加えると、

class MyStrategy(bt.Strategy):

   def __init__(self):

       sma1 = btind.SMA(self.data.close, period=15)
       high_or_30 = bt.If(sma1 > self.data.close, 30.0, self.data.high)
       sma2 = btind.SMA(high_or_30, period=15)

これで、2つめの移動平均は sma と 終値の比較結果に応じて、30.0または高値を用いて計算を実行するよ。

<注意事項>
30という値は、常に30を返す疑似反復可能関数に内部的に変換されるよ。


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