Bot

仮想通貨botの開発を本格的に始めてみる#30(2023/11/13)「MMBotの開発に着手する:ソースコードの構造解析:準備編」

2023年11月13日

前回の記事に引き続き、今回も仮想通貨botの開発状況をまとめていきます。

今回から、MMBotの開発を進めていきます。

参考にした記事はこちら。「Python3 MarketMaker(MM)BOTのサンプルロジックとソースコード」です。

「MMBotって何?」という方は、こちらの記事を読むことをオススメします。

ソースコードの解析

ChatGPTを使って、ソースコードの解析をしました。

1.ライブラリのインポートとAPIキーの設定

import datetime
import time
import ccxt

bitflyer = ccxt.bitflyer({
    'apiKey': 'APIKEYを入力',
    'secret': 'SECRETKEYを入力',
})
  • ccxt ライブラリを使ってBitFlyerのAPIを利用します。
  • APIキーとシークレットキーが必要で、これはBitFlyerのウェブサイトから取得します。

2.取引に関する基本情報の設定

COIN = 'BTC'
PAIR = 'BTCJPY28SEP2018'
LOT = 0.002
AMOUNT_MIN = 0.001
SPREAD_ENTRY = 0.0005
SPREAD_CANCEL = 0.0003
AMOUNT_THRU = 0.01
DELTA = 1

取引する通貨(COIN)、取引ペア(PAIR)、ロットサイズ(LOT)などの基本的な取引情報を設定します。

  1. COINPAIR:
    • COIN: 取引する仮想通貨の種類を指定します。この例では Bitcoin(BTC)が設定されています。
    • PAIR: 取引ペアを指定します。この例では BTCJPY28SEP2018 という特定のペアを対象にしています。通常、このペアはビットコイン対日本円の9月限月先物契約を意味しています。
  2. LOT:
    • ロットサイズを指定します。ロットは通常、取引単位のことで、このボットは0.002 BTC単位で取引を行います。
  3. AMOUNT_MIN:
    • 最小注文量を指定します。取引所によっては最小注文単位が決まっており、これを下回る注文は受け付けられない場合があります。このボットでは0.001 BTCとしています。
  4. SPREAD_ENTRYSPREAD_CANCEL:
    • SPREAD_ENTRY: 実効スプレッド(AskとBidの差額)がこの閾値を上回った場合、新たなエントリー(注文)を出す条件です。例えば、0.0005ならスプレッドが0.05%以上広がったら新たな取引を試みます。
    • SPREAD_CANCEL: 実効スプレッドがこの閾値を下回った場合、指値注文の更新を停止する条件です。例えば、0.0003ならスプレッドが0.03%以下に狭まったら指値注文を更新しなくなります。
  5. AMOUNT_THRU:
    • 実効Ask/Bidからどれだけ離れた位置に指値を置くかを指定します。例えば、0.01 BTCならば、実効Ask/Bidから0.01 BTC離れた位置に指値を入れます。
  6. DELTA:
    • 実効Ask/Bidからこの値だけ離れた位置に指値を置く条件です。例えば、1ならばAskから1円離れた位置に指値を入れます。

3.ログの設定

import logging

logger = logging.getLogger('LoggingTest')
logger.setLevel(10)
fh = logging.FileHandler('log_mm_bf_' + datetime.datetime.now().strftime('%Y%m%d') + '_' + datetime.datetime.now().strftime('%H%M%S') + '.log')
logger.addHandler(fh)
sh = logging.StreamHandler()
logger.addHandler(sh)
formatter = logging.Formatter('%(asctime)s: %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
fh.setFormatter(formatter)
sh.setFormatter(formatter)

取引のログを記録するために、logging ライブラリを使用します。ログはファイルとコンソールに出力されます。

  1. import logging: Pythonの標準ライブラリである logging をインポートします。これはログを記録するための主要なライブラリです。
  2. logger = logging.getLogger('LoggingTest'):
    • 'LoggingTest' という名前のロガー(logger)オブジェクトを作成します。ロガーは、ログの出力を管理するオブジェクトで、同じ名前のロガーを共有することで、同じログに対する一貫性を保つことができます。
  3. logger.setLevel(10):
    • ロガーのレベルを設定します。ログの出力はこのレベルに基づいて制御されます。この例では、レベル10(DEBUG)に設定しています。DEBUGレベルでは最も低い重要度のログも含まれます。
  4. fh = logging.FileHandler('log_mm_bf_' + datetime.datetime.now().strftime('%Y%m%d') + '_' + datetime.datetime.now().strftime('%H%M%S') + '.log'):
    • ファイルハンドラを作成します。これはログをファイルに出力するためのハンドラです。
    • ファイル名は日付と時刻に基づいており、毎回異なるファイルになります。これにより、ログが日付ごとに分かれ、トラブルシューティングが容易になります。
  5. logger.addHandler(fh):
    • 先程作成したファイルハンドラをロガーに追加します。これにより、ログがファイルにも出力されるようになります。
  6. sh = logging.StreamHandler():
    • ストリームハンドラを作成します。これはログをコンソールに出力するためのハンドラです。
  7. logger.addHandler(sh):
    • ストリームハンドラをロガーに追加します。これにより、ログがコンソールにも出力されるようになります。
  8. formatter = logging.Formatter('%(asctime)s: %(message)s', datefmt="%Y-%m-%d %H:%M:%S"):
    • ログメッセージのフォーマットを設定します。%(asctime)s はログメッセージが発生した時刻を、%(message)s はメッセージ本体を表します。datefmt で時刻のフォーマットを指定しています。
  9. fh.setFormatter(formatter) および sh.setFormatter(formatter):
    • それぞれのハンドラにフォーマッターをセットします。これにより、ログの出力が指定したフォーマットに従います。

この設定により、プログラムが実行される際に logger.info() などでログメッセージを出力すると、指定したフォーマットでファイルとコンソールに同時にログが記録されます。ログは主にデバッグやトラブルシューティングの際に役立ちます。

4.取引に関する関数の定義

# JPY残高を参照する関数
def get_asset():
    # ...

# JPY証拠金を参照する関数
def get_colla():
    # ...

# 板情報から実効Ask/Bidを計算する関数
def get_effective_tick(size_thru, rate_ask, size_ask, rate_bid, size_bid):
    # ...

# 成行注文する関数
def market(side, size):
    # ...

# 指値注文する関数
def limit(side, size, price):
    # ...

# 注文をキャンセルする関数
def cancel(id):
    # ...

# 指定した注文idのステータスを参照する関数
def get_status(id):
    # ...
  • get_asset(): JPY残高を取得する関数
  • get_colla(): JPY証拠金を取得する関数
  • get_effective_tick(): 実効Ask/Bidを計算する関数
  • market(): 成行注文をする関数
  • limit(): 指値注文をする関数
  • cancel(): 注文をキャンセルする関数
  • get_status(): 注文のステータスを取得する関数
  1. get_asset() 関数:
    • 取引所の残高を取得するための関数です。
    • bitflyer.fetch_balance() を呼び出して取引所の残高情報を取得します。
    • 例外処理が組み込まれており、API通信に失敗した場合にはエラーをログに出力してから1秒待機して再試行します。
    • 取得した残高情報は関数の戻り値として返されます。
  2. get_colla() 関数:
    • 取引所の証拠金を取得するための関数です。
    • bitflyer.privateGetGetcollateral() を呼び出して証拠金情報を取得します。
    • 例外処理が組み込まれており、API通信に失敗した場合にはエラーをログに出力してから1秒待機して再試行します。
    • 取得した証拠金情報は関数の戻り値として返されます。
  3. get_effective_tick(size_thru, rate_ask, size_ask, rate_bid, size_bid) 関数:
    • 取引所の板情報から実効Ask/Bidを計算するための関数です。
    • bitflyer.fetchOrderBook(PAIR) を呼び出して板情報を取得します。
    • 例外処理が組み込まれており、API通信に失敗した場合にはエラーをログに出力してから2秒待機して再試行します。
    • 引数として与えられた条件(size_thru, rate_ask, size_ask, rate_bid, size_bid)に基づいて実効Ask/Bidを計算し、辞書形式で戻り値として返します。
  4. market(side, size) 関数:
    • 成行注文を出すための関数です。
    • bitflyer.create_order() を呼び出して成行注文を発行します。
    • 例外処理が組み込まれており、API通信に失敗した場合にはエラーをログに出力してから2秒待機して再試行します。
    • 注文が成功すると注文情報が戻り値として返されます。
  5. limit(side, size, price) 関数:
    • 指値注文を出すための関数です。
    • bitflyer.create_order() を呼び出して指値注文を発行します。
    • 例外処理が組み込まれており、API通信に失敗した場合にはエラーをログに出力してから2秒待機して再試行します。
    • 注文が成功すると注文情報が戻り値として返されます。
  6. cancel(id) 関数:
    • 注文のキャンセルを行うための関数です。
    • bitflyer.cancelOrder() を呼び出して注文をキャンセルします。
    • 例外処理が組み込まれており、API通信に失敗した場合にはエラーをログに出力しますが、処理は続行されます。
    • キャンセルが成功した場合は、キャンセルした注文の情報が戻り値として返されます。
  7. get_status(id) 関数:
    • 特定の注文のステータスを取得するための関数です。
    • bitflyer.private_get_getchildorders() を呼び出して注文の詳細情報を取得します。
    • 例外処理が組み込まれており、API通信に失敗した場合にはエラーをログに出力してから2秒待機して再試行します。
    • 取得した注文の情報を整形し、ステータス、約定済み量、未約定量などが含まれた辞書形式で戻り値として返します。

これらの関数は、取引所との通信や注文の発行など、実際の取引を行うための基本的な機能を提供しています。各関数が内部でAPI通信を行い、取引所との連携を実現しています。

5.メインの無限ループ

# メインループ
while True:

    # 未約定量の繰越がなければリセット
    if remaining_ask_flag == 0:
        remaining_ask = 0
    if remaining_bid_flag == 0:
        remaining_bid = 0

    # フラグリセット
    remaining_ask_flag = 0
    remaining_bid_flag = 0

    # 自分の指値が存在しないとき実行する
    if pos == 'none':

        # 板情報を取得、実効ask/bid(指値を入れる基準値)を決定する
        tick = get_effective_tick(size_thru=AMOUNT_THRU, rate_ask=0, size_ask=0, rate_bid=0, size_bid=0)
        ask = float(tick['ask'])
        bid = float(tick['bid'])
        # 実効スプレッドを計算する
        spread = (ask - bid) / bid

        logger.info('--------------------------')
        logger.info('ask:{0}, bid:{1}, spread:{2}%'.format(int(ask * 100) / 100, int(bid * 100) / 100, int(spread * 10000) / 100))

        # 実効スプレッドが閾値を超えた場合に実行する
        if spread > SPREAD_ENTRY:

            # 前回のサイクルにて未約定量が存在すれば今回の注文数に加える
            amount_int_ask = LOT + remaining_bid
            amount_int_bid = LOT + remaining_ask

            # 実効Ask/Bidからdelta離れた位置に指値を入れる
            trade_ask = limit('sell', amount_int_ask, ask - DELTA)
            trade_bid = limit('buy', amount_int_bid, bid + DELTA)
            trade_ask['status'] = 'open'
            trade_bid['status'] = 'open'
            pos = 'entry'

            logger.info('--------------------------')
            logger.info('entry')

            time.sleep(5)

    # 自分の指値が存在するとき実行する
    if pos == 'entry':
        # ...(以前のコードを省略)

        #スプレッドが閾値以上のときに実行する
        if spread > SPREAD_CANCEL:
            # ...(以前のコードを省略)

        # Ask/Bid両方の指値が約定したとき、1サイクル終了、最初の処理に戻る
        if trade_ask['status'] == 'closed' and trade_bid['status'] == 'closed':
            pos = 'none'

            logger.info('--------------------------')
            logger.info('completed.')

    time.sleep(5)

このメインの無限ループでは以下のような処理が行われています:

  1. 未約定量やフラグをリセットし、新たなサイクルの開始の準備をします。
  2. pos 変数が 'none' のとき、つまり自分の指値が存在しないときに以下の処理が行われます:
    • 取引所から板情報を取得し、実効Ask/Bidとスプレッドを計算します。
    • スプレッドが閾値を超えた場合、指値注文を出します。
    • 未約定量が前回のサイクルで存在した場合、その量を新しい注文数に加えています。
    • 注文が出されたら、pos'entry' に設定し、5秒間待機します。
  3. pos 変数が 'entry' のとき、つまり自分の指値が存在するときに以下の処理が行われます:
    • 注文のステータスを確認し、指値の更新やキャンセルを行います。
    • スプレッドが閾値を超えた場合、指値の更新やキャンセルを行います。
    • Ask/Bid両方の指値が約定した場合、1サイクルを終了し、pos'none' に戻します。
  4. 最後に5秒待機し、次のサイクルに移ります。

この無限ループによって、取引アルゴリズムは継続的に市場の状態を監視し、必要に応じて取引を行います。

6.メインの取引ロジック

# 自分の指値が存在しないとき実行する
if pos == 'none':

    # 板情報を取得、実効ask/bid(指値を入れる基準値)を決定する
    tick = get_effective_tick(size_thru=AMOUNT_THRU, rate_ask=0, size_ask=0, rate_bid=0, size_bid=0)
    ask = float(tick['ask'])
    bid = float(tick['bid'])
    # 実効スプレッドを計算する
    spread = (ask - bid) / bid

    logger.info('--------------------------')
    logger.info('ask:{0}, bid:{1}, spread:{2}%'.format(int(ask * 100) / 100, int(bid * 100) / 100, int(spread * 10000) / 100))

    # 実効スプレッドが閾値を超えた場合に実行する
    if spread > SPREAD_ENTRY:

        # 前回のサイクルにて未約定量が存在すれば今回の注文数に加える
        amount_int_ask = LOT + remaining_bid
        amount_int_bid = LOT + remaining_ask

        # 実効Ask/Bidからdelta離れた位置に指値を入れる
        trade_ask = limit('sell', amount_int_ask, ask - DELTA)
        trade_bid = limit('buy', amount_int_bid, bid + DELTA)
        trade_ask['status'] = 'open'
        trade_bid['status'] = 'open'
        pos = 'entry'

        logger.info('--------------------------')
        logger.info('entry')

        time.sleep(5)
  • ログに取引の開始を表示し、残高や証拠金を表示します。
  • 指定の条件に基づいて、指値注文や成行注文を行います。
  • 注文が約定した場合、そのステータスや残高、証拠金の変動などをログに記録します。

上記のコードブロックでは、pos'none' のとき、つまり自分の指値が存在しないときに実行されるロジックが示されています。

  1. get_effective_tick 関数を用いて、指定した数量(AMOUNT_THRU)よりも下の位置においたときの実効Ask/Bidを取得します。
  2. 取得した実効Ask/Bidを用いて、実効スプレッドを計算します。スプレッドは (Ask - Bid) / Bid で計算され、百分率に変換してログに記録されます。
  3. 計算した実効スプレッドが設定されたエントリー閾値 SPREAD_ENTRY を超えている場合、以下の処理が実行されます:
    • 前回のサイクルで未約定量が存在すれば、今回の注文数にその未約定量を加えます。
    • limit 関数を用いて、実効Askから DELTA 離れた位置に amount_int_ask の数量で指値注文を出します(trade_ask)。
    • limit 関数を用いて、実効Bidから DELTA 離れた位置に amount_int_bid の数量で指値注文を出します(trade_bid)。
    • 注文のステータスを 'open' に設定し、pos'entry' に更新します。
    • ログにエントリーのメッセージを記録し、5秒間待機します。

この取引ロジックは、市場のスプレッドが一定の閾値を超えた場合に指値注文を出し、次のサイクルまで待機するという基本的なアプローチを取っています。指定された条件が満たされない場合は、指値注文を行いません。

7.その他の要素

ログ出力、API呼び出し、エラーハンドリング、時間の取得などが含まれています。

まとめ

素人が一目見ただけではよく分からない部分のコードも詳細に解説してくれています。

AIにコードを読み込ませて解説をさせるという手法は、今後の勉強にも使えそうです。

特に、まだ勉強が進んでいない分野を大枠で理解する際には非常に役に立ちそうです。

次回は、このコードを詳細に解析していきます。

「MMBotの構造を解析する①」へ続く。

-Bot