27
21

More than 1 year has passed since last update.

バックテスト時のパラメータの保持と文字表現に関わる工夫

Last updated at Posted at 2021-12-03

みなさん、はじめまして!
兼業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'とかって打ち間違えても、実行時にしか気が付かないのでね・・・。

こうした背景からも、設定のコード化とそれを一意に特定する仕組みには一定の便利さがあるなと感じています。

実際のサンプル

image.png

まとめ

botterの皆さんは、表には出ていないけれど小さい工夫を積み重ねていると思うんですよね。
エンジニアリングは、「うっわっ。ダッるー。」みたいなことを、完全でなくても少しずつ効率化して、昨日の自分より今日の自分の方が効率的、という環境を作り出していくものなので、引き続き何かネタを提供できるようにしていきたいと思います。
他の人の工夫も是非是非学んでいきたいので、この記事がきっかけになればと思います。
うっわっ。slackでアラート飛んでる。それでは今回はこの辺で!

27
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
21