【Optuna】仮想通貨自動取引のパラメータの最適化に挑戦

仮想通貨

機械学習のハイパーパラメータ最適化に使われるOptunaを活用して、仮想通貨自動取引で最も利益が出るパラメータを探索しようという試みです。

Optunaとは?

Preferred Network社が機械学習のハイパーパラメータ最適化に開発したpythonライブラリです。ベイズ最適化により複数のハイパーパラメータを持つ目的関数の値を最小化/最大化することができます。詳しい使い方は公式やQiitaの記事などが参考になります。

一般的には機械学習の損失関数を最小化するハイパーパラメータ探索に使うものですが、目的関数に仮想通貨自動取引の収益を渡すことで、自動取引アルゴリズムのパラメータ調整に使えそうだったので使ってみました。

仮想通貨自動取引のパラメータの最適化

自動取引アルゴリズムを決める

今回はOptunaの練習ということで、超シンプルに4つのパラメータを持つ売買アルゴリズムを考えました。

  • 一定期間毎(interval)に成行買
  • 買い成立と同時に購入価格 × 利確売り倍率(sell_rate)で指値売り
  • それと同時に購入価格 × ロスカット倍率(loss_cut_rate)の逆指値売り
  • 指定期間以内(duration)に指値/逆指値売り成立しない場合は価格問わず成行売り

上記の4種類のパラメータをもつ売買アルゴリズムを実装し、Coincheck APIから取得した過去の1分足データで損益を計算してみます。

ここで愛用しているCoincheckのやや使い勝手が悪い点として、逆指値売と指値売を同時に発注するOCO注文ができないことが挙げられます。そこでCoincheck APIで実装可能なアルゴリズムにするため、自動売買プログラムのアルゴリズムは次のように仮定します。

  1. intervalの始値で購入したことにする
  2. 購入と同時に利確価格で指値売り注文
  3. 利確価格に達した場合は取引が成立、購入価格との差額が利益となる
  4. 逆に1分足の終値がロスカット価格以下になった場合は利確指値売りをキャンセル
  5. 即座に成行売りすることで終値で取引成立したことにして損失として計上
  6. 指定期間を過ぎた場合も利確指値売りをキャンセル
  7. 即座に成行売りすることで指定期間最後の終値で取引成立したことにして損益に計上

このアルゴリズム通りに自動売買が成立したと仮定して、過去データで損益がどうなったかを分析していきます。

過去の1分足データの準備

データの取得方法は過去の記事で紹介していますので参考にしてください。

【Python】Coincheck APIからビットコイン1分ローソク足データを自動取得 | 素人がデータサイエンスを始める (datascience-beginer.com)

今回使うOHLCVの1分足データはこちらに置いておきます。

2023/07/24 ~ 2023/08/06のデータですが、7/24と8/6のデータ数が少ないので7/25~8/5に絞りました。

# data準備
file = '1min_20230806.csv'
df = pd.read_csv(file)
df['date'] = pd.to_datetime(df['date'])
df.set_index('date', inplace=True)

# dataの範囲を確認
start = df.index.tolist()[0].strftime('%Y-%m-%d %H:%M')
end = df.index.tolist()[-1].strftime('%Y-%m-%d %H:%M')

print('data start:', start)
print('data end:', end)

# 最初と最後の日はデータ数が中途半端なので落とす
df = df[(df.index > '2023-07-25') & (df.index < '2023-08-06')]

適当なパラメータでとりあえず損益を計算する

パラメータの現実的な値の範囲を確認するために、まずは適当なパラメータで上記アルゴリズムで取引した場合の損益を計算してみます。

# パラメータを適当に設定
interval = 15          # 買い付け間隔15分
sell_rate = 1.01       # 買い付け価格の101%に達したら利確売り
loss_cut_rate = 0.995  # 買い付け価格の99.5%に達したらロスカットで成行売り
duration = 120         # 最長120分ホールドして成行売り

# openのindex値を取得
open_idx = df.columns.get_loc('open')

# 結果を出力するリストを用意
profits = []
loss_cuts = []
time_outs = []

# intervalごとにfor loopして損益を格納していく
for start_time in range(0, len(df), 15):
    data = df.iloc[start_time:start_time+duration]           # durationにスライス

    buy_price = data.iloc[0, open_idx]                       # 買い付け価格
    sell_price = buy_price * sell_rate                       # 利確価格
    loss_cut_price = buy_price * loss_cut_rate               # ロスカット価格

    IsTradeDone = False
 
    for i, r in data.iterrows():
        if r['high'] > sell_price:                           # 高値が利確価格以上の場合は指値が成立している
            profits.append(sell_price - buy_price)
            IsTradeDone = True
            break

        elif r['close'] < loss_cut_price:                    # 終値がロスカット価格以下の場合は終値で売り
            loss_cuts.append(r['close'] - buy_price)
            IsTradeDone = True
            break

    if not IsTradeDone:                                      # 取引が成立していない場合はduration最後の終値で売り
        time_outs.append(r['close'] - buy_price)

# 損益を通算
profit = sum(profits)
loss_cut = sum(loss_cuts)
time_out = sum(time_outs)
total_profit = profit + loss_cut + time_out

# 結果
print(f'profit: {profit}, {len(profits)}')
print(f'loss_cut: {loss_cut}, {len(loss_cuts)}')
print(f'time_out: {time_out}, {len(time_outs)}')
print(f'total_profit: {total_profit}')

出力値は以下のようになりました。

profit: 1447410.3300000015, 35
loss_cut: -3259750.0, 136
time_out: 1970296.0, 936
total_profit: 157956.33000000147

利確価格まで達したデータ数が少なく、ほとんどタイムアウトしている点が気になりますが、とりあえずは現実的なパラメータ値だったようです。

パラメータをsell_rate = 1.03, loss_cut_rate = 0.98とすると結果はすべてタイムアウトになるため、今回の売買アルゴリズムではかなり狭い範囲でsell_rateとloss_cut_rateを設定する必要があるようです。

Optunaを使う

Optuna用に損益を関数化する

Optunaで最適化したいパラメータ(trial)を引数に持つ目的関数(objective)は以下になります。

def objective(trial):

    # trialからパラメータを取得
    interval = trial.suggest_int('interval', 5, 35, step=5)          # 5,10,15,20,25,30の整数で最適化
    sell_rate = trial.suggest_uniform('sell_rate', 1.005, 1.015)    # 1.005-1.015の連続値で最適化  
    loss_cut_rate = trial.suggest_uniform('loss_cut_rate', 0.99, 1)  # 0.99-1の連続値で最適化
    duration = trial.suggest_int('duration', 15, 195, step=15)       # 15-180, 15間隔の整数値で最適化

    # 各種index値を取得
    open_idx = df.columns.get_loc('open')

    # 結果を出力するリストを用意
    profits = []
    loss_cuts = []
    time_outs = []

    # intervalごとにfor loopして損益を格納していく
    for start_time in range(0, len(df), interval):
        data = df.iloc[start_time:start_time+duration]           # durationにスライス

        buy_price = data.iloc[0, open_idx]                       # 買い付け価格
        sell_price = buy_price * sell_rate                       # 利確価格
        loss_cut_price = buy_price * loss_cut_rate               # ロスカット価格

        IsTradeDone = False
    
        for i, r in data.iterrows():
            if r['high'] > sell_price:                           # 高値が利確価格以上の場合は指値が成立している
                profits.append(sell_price - buy_price)
                IsTradeDone = True
                break

            elif r['close'] < loss_cut_price:                    # 終値がロスカット価格以下の場合は終値で売り
                loss_cuts.append(r['close'] - buy_price)
                IsTradeDone = True
                break

        if not IsTradeDone:                                      # 取引が成立していない場合はduration最後の終値で売り
            time_outs.append(r['close'] - buy_price)

    # 損益を通算
    profit = sum(profits)
    loss_cut = sum(loss_cuts)
    time_out = sum(time_outs)
    total_profit = profit + loss_cut + time_out

    return total_profit * -1 

先ほど適当に設定したパラメータを関数内でtrial.suggestの形式に変更します。最適化したいパラメータに応じてsuggest_int(整数値)やsuggest_uniform(連続値)を指定します。

また目的関数を最小化するパラメータを探索するため、戻り値のtotal_profitは負の値にする必要があります。

Optunaでパラメータを最適化を実行

最適化の実行は非常にシンプルで、studyインスタンスを生成して、optimizeメソッドに目的関数とイテレーション回数を指定して実行するだけです。

import optuna

study = optuna.create_study()
study.optimize(objective, n_trials=100)

実行するとstudyインスタンスに結果が格納され、最適化パラメータと結果は以下のように呼び出せます。

print(study.best_params)
>> {'interval': 5, 'sell_rate': 1.012076105056648, 'loss_cut_rate': 0.9939572127656838, 'duration': 75}
print(study.best_value*-1)
>> 561365.6689404435

結果の推移を確認してみます。

values = [each.value for each in study.trials]
plt.plot(values)

ちゃんとイテレーションが進むごとに目的変数の最小化が進んでいました。次に目的関数を最小化したパラメータで自動売買を結果を確認します。

profit: 450164.72386534093, 9
loss_cut: -1431781.0, 49
time_out: 1169763.0, 1049
total_profit: 188146.72386534093

予測はしていましたが、ほとんどタイムアウトでした。このアルゴリズムでは買い付け時から少しでも価格が低下したら売ってしまい、durationも短いので価格が下がる前に売る、ということが最適なのだと解釈できます。

まとめ

以上がOptunaを使った自動売買パラメータの最適化の使用例になります!本来は機械学習のハイパーパラメータ最適化に使われるライブラリですが、関数ならなんでも最適化できるのでとても便利です。

今回はOptunaの使い方を紹介するために雑なアルゴリズムに適用しましたが、もう少し売買アルゴリズムを工夫して再挑戦してみます!

コメント