見出し画像

Pythonの例外処理で止まりにくいbot作りを考える

botは止められない

チャネルブレイクアウト戦略は期待値の勝負です。正の期待値があるロジックも、常に市場に参加していなければ果実を得ることはできません。これは他の戦略にも言えることです。

例外処理をしていないスクリプトはあまりないと思いますが、エラーだから通知をして止めましたとか、何回エラーが続いたので止めましただけでは、止まってばかりのbotになってしまいます。
止まる原因に合った適切な例外処理をすることで、止まりにくいbotにすることができます。

今回は、try節で起きた例外をexcept節でキャッチするという、コードの話ではありません。このケースではこういうことが起きていると想定されるから、こう対処しています、という話をします。

それでは、明らかなコードのエラーをも含め、botが止まる要因を想定して分類してみます。

case1. 取引所のサーバーが混雑している。
case2. 取引所のサーバーが停止している。
case3. botが不正な値を送信している。
case4. いずれに該当するか分からない。

それぞれのケースについて、botではどのように例外処理をするのがよいか考えてみます。botを止めるべき例外なのかそれとも動かし続けてよいのかを、切り分けます。

case1. 取引所のサーバーが混雑している

混雑はなぜ起きるのか。それは多くの人が同時に行動を起こしているからかもしれません。
混雑はいずれ解消すると考えられますから、データ取得なり注文なりを繰り返しリクエストしてみます。
多くの人が行動するような局面なら、ゆっくり待ってはいられません。戦いです。許される限りの短い間隔で繰り返し要求を出します

case2. 取引所のサーバーが停止している

計画されたメンテナンスなのか外部からの一時的な攻撃対策なのか、停止の理由は一般にbotでは分かりません。あまり高頻度に問い合わせることで、bot自体が停止要因とみなされても困ります。
サーバーが停止してしまえば、botができることは待つのみです。復旧したことに気づけるように定期的にリクエストを出し続けます

case3. botが不正な値を送信している

このケースは更に3つに分類できます。
a. ひとつめはコードが誤っている場合です。同じリクエストをし続けても、正しく処理されることは永遠にありません。一定回数のリクエストをしても処理が進まなければ、見込みがないと判断してコードの実行を止めます。

b. ふたつめは、ネットワークの負荷などの理由でサーバーから値が取れていなかったり不正な値が戻ることで、結果的に処理を誤る場合です。これは再取得をすることで、正しく処理が進むかもしれません。このケースでも一定回数の再リクエストを試みます。

c. もうひとつは、アカウントの状況によりこちらからの送信値がそもそも規定外になっている場合です。設定を誤ったり残高が不足したらすみやかにコードを止めます

case4. いずれに該当するか分からない

例外中の例外です。想定していないことは起こるものです。何度かリクエストをしても進まなければ、botを止めて何が起きたか確認するのが賢明です。

例外処理を仕分ける

例外が起きたときにbotが例外をどうさばくかは、次の4つに整理できました。

pattern1. 要求が通るまで叩き続ける(case1.)
pattern2. 再開するまで問い合わせ続ける(case2.)
pattern3. 何度か試してだめならあきらめる(case3.a, case3.b,  case4.)
pattern4. すかさず止める(case3.c)

まとめると、取引所側に問題がありそうなときはbotを止めずに復旧を待ち、bot側に問題がありそうなときはbotを止めて対処をするという、考えてみれば当たり前の方針が確認できました。

エラーの戻り値をサンプリングし続けて、ケース判定に使います。
case1.がcase3.bに変化したり、case2.から復旧したりと、状況が変わっても対応できるようにケース判定と処理パターンの順序を考えて、コードにしていきます。

判定が難しいのはcase3.bの値の破損が疑われるケースです。現実には何度かリクエストし直すと復旧する可能性が高く、復旧しなければcase3.aのバグの存在が疑われるので、一定数のトライでだめなら停止させます。

悩ましいのはもうひとつ、'502 Bad Gateway'の扱いです。まるで'Please try again later.'くらいの軽いニュアンスで毎日のようにお目にかかります。
502はサーバーや経路のダウンを疑いたい、場合によっては重ためのステータスコードです。しかしたいてい1回のリトライで復旧するので、瞬間的な過負荷とみなして処理しています。

OVERLOADED = [
    'Read timed out.',
    '502 Server Error: Bad Gateway',
    'The system is currently overloaded.',
]

RESPONSES = [
    'Engine Error: 503',
]

REASONS = [
    'client',
    'invalid',
    'insufficient',
]

def check_condition(e):
    if any(response in str(e)
            for response in OVERLOADED):
        sleep(POKE)
        return 'not_accepted'
    elif any(response in str(e)
             for response in RESPONSES):
        sleep(PAUSE)
        return 'no_service'
    elif any(response in str(e).lower()
             for response in REASONS):
        return 'criticality'
    else:
        sleep(SLEEP)
        return ''

def function():
    i = 0
    while i < RETRY:
        try:
            """例外が起きそうな処理"""
        except Exception as e:
            logger.error(f'{e}')
            condition = check_condition(e)
            if 'not_accepted' == condition:
                continue
            elif 'no_service' == condition:
                continue
            elif 'criticality' == condition:
                raise
            else:
                i += 1
                """以下略"""

私のbotでは例えば上記のように、シンプルにif~elifでどのケースに該当するかを順に評価しています。

関数の戻り値によって、即リトライ、無限ループ、即停止、有限ループと処理パターンを4通りに分岐させています。

botは止まらない

Pythonスクリプトで例外処理をした上で、仮想サーバーのターミナルを閉じてもかまわないように、botをバックグラウンドに追いやります。cronで定期的に再起動をしておけば、ログの管理も便利になります。

./cRe5520/cReboot.sh

#!/bin/bash
pkill -f 'cRe5520\.py'
cd ./cRe5520/
nohup python3 ./cRe5520.py >/dev/null 2>&1 &
cd ~
[ec2-user ~]$ crontab -e

MAILTO=""
12 3 * * * bash ./cRe5520/cReboot.sh

ここまでしておけば、あとは時々ログを確認したり、バックテストをやり直したりと、簡単なメンテナンスでbotを動かし続けることができます。

最後に"タートル流投資の魔術"の一節を引用しておきます。

タートルたちは、首尾一貫して参入シグナルを受け取るよう教えられた。1年間の利益のほとんどが、ほんのふたつか3つの大きな勝ちトレードによってもたらされることもあるからだ。ひとつのシグナルを除外したり逃したりすれば、その年のリターンに大きな影響を与えるかもしれない。

それが正の期待値を持つ戦略であることが前提です。バックテストスクリプトも活用してぜひ見つけてください。

RESOLUTION = '1d' FIRST_PERIOD_STICKS = 14 SECOND_PERIOD_STICKS = 3 MARKET_ID = 'ETHUSD' OC_MODE = True