見出し画像

[仮想通貨bot]いくつかの手法を試してみた所感

はじめに

以前の記事で、私が初めて継続的に利益を出せた bot を紹介しました。

トレンドフォロー型の bot に至るまでに、いろんな bot を作っては失敗してきました。
この記事では私が今までに作ってきた bot 達を紹介しようと思います。
失敗例ばかりですが何かのお役に立てれば幸いです。

bot 紹介

オープニングレンジ・ブレイクアウト(ドテン君)

とりあえず bot を作ろうと思い、右も左も分からない状態でとにかくググりまくり、ドテン君というものを見つけました。

まずそのまま実装して、その後係数 k や直近の足の数 n をいじってみてバックテストし、一番よさそうなパラメータのモデルで本番投入しました。
当時のことはあまり覚えていないのですが、負けが続いたため辞めた記憶があります。
ただ、ブレイクアウトの手法はトレンドフォローの一種なため、細かい負けを取り戻す大きな勝ちを忍耐強く待ちきれなかったのが敗因かもしれません。
ブレイクアウトの手法自体は私が今使っている bot の売買判断ロジックに組み込めるので、また折りをみて試してみたいと思っています。

当時のソースコードを漁っていたらこんなコードが出てきました。

from decimal import Decimal
import polars as pl
from strategy.strategy import Strategy


class BreakoutStrategy(Strategy):

    def __init__(self, k: Decimal, n: int):
        self.k = k
        self.n = n

    def handle(self, df: pl.DataFrame) -> list[bool]:
        df_tmp = (
            df
            .with_columns([(pl.col('close') - pl.col('open')).alias('diff')])
            .with_columns([pl.col('diff').abs().alias('abs'), ])
            .with_columns([pl.col('abs').rolling_mean(self.n).alias('mean')])
            .with_columns([pl.col('mean').shift(1).alias('shift')])
            .with_columns([
                (pl.col('open') + pl.col('shift') * self.k).alias('breakout_price_buy'),
            ])
        )

polars を使って breakout する値を計算する方法です。
参考になるか分かりませんが、、

あと、注文方法に関しても成行と指値を試していました。
プログラムを書くときは成行の方が圧倒的に楽です。
成行で注文すれば基本的には注文が通るので、注文状態を常に気にしないといけない指値より考えることが少ないです。
指値のプログラムも書きましたが、上記のように考えることが複雑になったり、約定回数が減ることによる施行や考察回数の低下もあり、微妙だなあと思ってしまいました。
ただ、BitBank などの一部取引所では指値注文が約定した時にマイナス手数料で利益が出るため、そこは強みだと思いました。

ブレイクアウトした次のローソク足の開始タイミングで注文を入れるか、それとも相場を常に監視してブレイクアウトした瞬間に注文を入れるかも悩みました。
これに関しても次のローソク足で注文を入れる方がロジックが簡単で、API を叩く回数も減ります。
常に監視する場合は、例えば一分単位で API を叩いてローソク足を取得して売買判断をする必要があります(もしくは web socket を使ってリアルタイムで相場状況を監視します)。
また、ブレイクアウトした瞬間に買うパターンは、ローソク足形成途中で買うことになるため、だましに引っかかったり、正確にバックテストすることが難しくなってきます。

私がもしまたブレイクアウトを試すとしたら、成行注文とローソク足単位での注文でやると思います。

アービトラージ(裁定取引)

取引所間の価格の差異を利用して利益を得る手法です。

例えば、二つの取引所 A と B で 10 BTC ずつ持っているとします。
ある瞬間に、A では 1 BTC が 100 円、B では 1BTC が 50 円 で取引されているとすると、B で 10 BTC 買い、A で 10 BTC 売り、B から A に 10 BTC 送金すれば、両取引所で保持している BTC の数は変わらず、単純計算で 500 円の利益を得ることができます。

この単純計算には手数料が含まれていないので、ダメですね。
実際には、取引所での注文に掛かる手数料や、取引所間で BTC を送金するために掛かる手数料があります。
取引所間の BTC の価格がこれらの手数料に打ち勝つレベルで開かないといけないのですが、そのようなタイミングはなかなかやってきません。

また、手法が単純なため多くの人が同じプログラムを実行していることも考えられます。
世の中には処理速度に命をかけている人達もいるため、自分では太刀打ちできないと思って辞めました。

三角アービトラージ
一つの取引所内で、「USDT で BTC を買う」「BTC で ETHを買う」「ETH で USDT を買う」という流れで利益を得る手法です。
同一の取引所を使うので、送金手数料はかかりません。
三角で取引できる銘柄をある買っている取引所の剪定は必要です。
この手法はちょっと面白そうだったので実装してやってみました。
ただやはり注文手数料まで考えるとなかなかチャンスはないですね、、
当時は現物でやっていたのですが、信用取引等でやれば、3 つの注文を同時並行で進められるので可能性が広がるような気はします。

アノマリー

なぜかは説明できないけど、経験的に観測できる値動きをアノマリーと言うようです。
自分は、「特定の曜日、特定の時間」に対して、終値と始値の差がプラスになるローソク足を探すロジックを書いて実践してみました。
当時のコードを一部掲載します。

def calc_anomaly_df(service: KucoinService, _symbol: str):
    weekdays = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN']
    kline_type = '1hour'
    fee = 0.001
    df = service.get_kline_data(_symbol, kline_type)

    if df is None:
        print('[' + _symbol + '] data is None:')
        return {'weekday': None, 'hour': None, 'weekday_hour': None}

    if len(df) < 1500:
        print('[' + _symbol + '] data length is not enough: ' + str(len(df)))
        return {'weekday': None, 'hour': None, 'weekday_hour': None}

    df = df.with_columns([
        # 買った枚数 * 売値(fee 込み)
        ((1 - fee) / pl.col('open') * pl.col('close') * (1 - fee) - 1).alias('profit'),
        pl.col('datetime').apply(lambda x: x.hour).alias('hour'),
        pl.col('datetime').apply(lambda x: x.weekday()).alias('weekday'),
    ]).with_columns([
        (pl.col('profit') > 0).alias('hit'),
        pl.col('weekday').apply(lambda x: weekdays[x]).alias('weekday_en'),
    ])

    return {
        'weekday': calc_ratio(df, ['weekday']),
        'hour': calc_ratio(df, ['hour']),
        'weekday_hour': calc_ratio(df, ['weekday', 'hour']),
    }

def calc_ratio(_df, cols):
    df_agg = _df.groupby(cols).agg([
        pl.col('profit').min().alias('profit_min'),
        pl.col('profit').max().alias('profit_max'),
        pl.col('profit').var().alias('profit_var'),
        pl.col('profit').mean().alias('profit_mean'),
        pl.col('hit').mean().alias('ratio'),
    ])
    df_agg_filtered = df_agg.filter(pl.col('ratio') > 0.7)
    if len(df_agg_filtered > 0):
        return df_agg_filtered.sort(cols)
    else:
        return None

バックテストで利益が出る確率が 70% 以上の曜日と時間に対して買いを入れるようにして本番投入してみたのですが、思うように利益が出ませんでした。
1500 本程度のローソク足ではアノマリー判定が甘かったのかもしれません。
あと、「曜日と時間を説明変数に使って機械学習すれば良いのでは?」という考えに変わってきたため、次は機械学習 bot をやってみることにしました。

機械学習

ローソク足データの行列をもとに、機械学習を実施します。
説明変数には ta で算出した値や、アノマリー bot でも使用した「曜日」「時間」などを使いました。
以前の記事でも書きましたが、ta は TA-LIB ではなく こちらの ta です)
目的変数には「次のローソク足が上がるかどうか」を使って分類モデルを作ったり、「売買した時の利益」を使って回帰モデルを作ったりしました。

分類でも回帰でも、機械学習にはいろいろな手法があるので、PyCaret というライブラリを使ってどの手法が最も良い精度になるかを検証しました。

このあたりの試行錯誤は参考になるかもしれないので、また別記事で詳しく書こうと思います。

market maker

買い指値と売り指値の注文を両方出し、その両方が約定した時に差額で利益を得る手法です。
たとえば 1 BTC 100 円の時、99 円で買い注文、101 円で売り注文を出します。売り買いの両方が約定すれば、2 円の利益になります。

私は指値注文の手数料がマイナスである(つまり約定さえすれば手数料分利益が入ってくる)BitBank を利用しました。
注文を出すロジックとしては、最新の BTC の価格を取得し、そこから最小取引単位分上の価格で売り注文を出し、最小取引単位分下の価格で買い注文を出します。
このロジックでは「最小取引単位 * 2 + 手数料 * 2」の利益しか得られないため、大量に約定させないと現実的な利益にはならないのですが、一旦これで実際に利益が出るかを試してみました。
結果としては、これもとても微妙な感じで、続けていてもおそらくマイナスになります。

その理由の一つに、「自分が買い指値として注文を出そうとしている間に価格が変動し、自分の注文と同一の価格で売り指値が出されてしまい、自分の買い指値はその売り指値とぶつかって成行として処理されてしまう」というパターンがありました。
成行になると手数料を取られてしまうため、利益が下がります。
こういったパターンで成行になるのを防ぐため、「成行になる場合は注文自体出さない」という注文方法が BitBank では用意されていたためそれを使いましたが、つまりそれは片方だけ約定すると言うことになるので、どうあと処理するかを考える必要が出てきます。

market maker の記事を読んでいると、もっと数学的な差額の出し方や後処理の方法が述べられていて、自分はやめておこうと思いました、、

おわりに

どの手法もあまり深くまで分析せずにやめてしまった感があります。
もう少し突き詰めれば利益が出たかもしれませんが、いくら突き詰めても負け続ける可能性もあるので、どこまでやれば良いのか分からないのが辛いところですね。

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