みなさん、はじめまして!
兼業botterの8oki(やおき)です。
今年の 仮想通貨botter Advent Calendar 2021 では、バックテスト時に工夫している点を紹介します。
なぜ工夫しようと思ったか
バックテストにおいて、設定やパラメータの過剰最適化はあまりよろしくないのですが、
傾向を探るために過剰でなくても、複数の設定を試すことはよくやると思います。
例えば、短期と長期の移動平均のクロスでドテンするストラテジーを試すとして、
取引対象の銘柄・時間軸では順張り傾向が強いか、などということを外観として知りたいとします。
すると、以下のような形で集計することになるのですよね。
短期移動平均 | 長期移動平均 | 総損益 |
---|---|---|
5 | 25 | -200 |
5 | 50 | -100 |
5 | 75 | 50 |
10 | 25 | 100 |
10 | 50 | 200 |
10 | 75 | 170 |
バックテストの出力について上記のように集計だけならいいのですが、
- 細かく取引履歴を保存しておきたい
- 損益グラフを保存しておきたい
ということもあると、行ごとに、
つまりは パラメータの組合せごとに名前を決めて ファイル名として使用する必要があります。
ファイルではなくDBであっても一つの検証IDとして名前を定める必要があるでしょう
例えば、次のように対応づけたくなるということです。
短期移動平均 | 長期移動平均 | 検証名称 |
---|---|---|
5 | 25 | MACROSS_shortperiod-5_longperiod-25 |
5 | 50 | MACROSS_shortperiod-5_longperiod-50 |
5 | 75 | MACROSS_shortperiod-5_longperiod-75 |
取引履歴のファイル名をMACROSS_shortperiod-5_longperiod-25_history.csv
にしたり、
損益グラフのファイル名をMACROSS_shortperiod-5_longperiod-25_graph.png
という感じです。
こういった 複数の設定の文字表現をうまいこと自動生成する仕組みを作ってあげないと、
設定名が変わるごとに、設定項目が増えるごとに、設定を司るコード部分の改修が必要になってしまいます。
この辺に面倒臭さを感じて、一つ便利そうなクラスを作ってみました。
パラメータの保持と文字表現を実現するクラス
- BaseConfigクラス
class BaseConfig():
"""バックテストの設定を保持して見やすい文字列で表現するクラス
* 【使い方】
* 当該クラスを継承する個別の設定クラスを用意
* 設定をメンバ変数として定義
* 必要に応じてフォーマットを指定
* __repr__で文字列により設定と値の関係を連ねた文字列で取得
"""
def __init__(
self,
name,
sep_valiable='_',
sep_value='-',
ignore_list=[
'name',
'sep_valiable',
'sep_value',
'ignore_list',
'init_counter'],
init_counter=2):
"""コンストラクタ
Args:
name (str): 設定名(ストラテジー名)
sep_valiable (str, optional): 設定と設定の間の区切り文字. Defaults to '_'.
sep_value (str, optional): 設定と設定値の間の区切り文字. Defaults to '-'.
ignore_list (list, optional): メンバ変数の中で文字列表現の対象から除外する変数名のリスト. Defaults to [ 'name', 'sep_valiable', 'sep_value', 'ignore_list', 'init_counter'].
init_counter (int, optional): 文字列表現を生成する中で重複した場合の初期カウンター. Defaults to 2.
"""
self.name = name
self.sep_valiable = sep_valiable
self.sep_value = sep_value
self.ignore_list = ignore_list
self.init_counter = init_counter
def __repr__(self):
class_str = self.name
valiables = self.__dict__
multiple_cnt = dict()
for key, value in valiables.items():
if self.should_ignore(key):
continue
# 変数が存在するので区切り文字を追加
class_str += self.sep_valiable
key_elements = key.split(self.sep_valiable)
key_str = ''
for elements in key_elements:
# 先頭の文字を追加
key_str += elements[0]
# keyの重複を確認
key_cnt = multiple_cnt.get(key_str)
if key_cnt is None:
# keyが存在しない場合
multiple_cnt[key_str] = self.init_counter
else:
# keyが存在した場合 -> カウントアップ
multiple_cnt[key_str] = key_cnt + 1
key_str += str(key_cnt)
# 生成したkeyを追記
class_str += key_str
# 変数文字列を生成したあとに値を追加
class_str += self.sep_value
key_format = valiables.get(key + '_format')
if key_format is None:
class_str += str(value)
else:
class_str += key_format.format(value)
return class_str
def __str__(self):
return self.__repr__()
def should_ignore(self, key):
if key in self.ignore_list:
return True
elif '_format' in key:
return True
else:
return False
def to_string(self):
return self.__str__()
def to_csv(self, sep=','):
csv_str = self.name
valiables = self.__dict__
for key, value in valiables.items():
if self.should_ignore(key):
continue
csv_str += sep
csv_str += str(value)
return csv_str
def to_header(self, sep=',', add_name=True):
header_str = ''
if add_name:
header_str += self.name
valiables = self.__dict__
for key, value in valiables.items():
if self.should_ignore(key):
continue
header_str += sep
header_str += key
return header_str
使い方のサンプル
BaseConfigを継承した個別の設定クラスを用意します。
設定をメンバ変数として定義し、必要に応じてフォーマットを指定します。
class SampleConfig(BaseConfig):
def __init__(self):
super().__init__('MACROSS')
self.long_period = 105
self.long_period_format = '{:03d}'
self.short_period = 9
self.short_period_format = '{:03d}'
self.large_performance = 10
self.large_performance_format = '{:03d}'
このクラスを実体化して文字列として出力すると
config = SampleConfig()
print(config)
次のようになります。
MACROSS_lp-105_sp-009_lp2-010
様式は、次のような構成にしていて
[ストラテジー名]_[設定1]-[設定1の値]_[設定2]-[設定2の値]・・・
設定項目が増えるとOSのファイル名やファイルパスの上限にひっかかることもあるので、
設定項目の変数名をアンダースコアで区切り、先頭文字だけつなげて省略設定名として出力しています。
また、省略することで重複する場合があるため、名前を分けるための数字を後付けしてあげるようにもしています。
設定をクラスとして記述することで、エディタの補完機能を使ってシンボルを間違えないとういメリットもあるんですよね。
dictで設定を記述すると'long_perod'とかって打ち間違えても、実行時にしか気が付かないのでね・・・。
こうした背景からも、設定のコード化とそれを一意に特定する仕組みには一定の便利さがあるなと感じています。
実際のサンプル
まとめ
botterの皆さんは、表には出ていないけれど小さい工夫を積み重ねていると思うんですよね。
エンジニアリングは、「うっわっ。ダッるー。」みたいなことを、完全でなくても少しずつ効率化して、昨日の自分より今日の自分の方が効率的、という環境を作り出していくものなので、引き続き何かネタを提供できるようにしていきたいと思います。
他の人の工夫も是非是非学んでいきたいので、この記事がきっかけになればと思います。
うっわっ。slackでアラート飛んでる。それでは今回はこの辺で!