見出し画像

チャネルブレイクアウト戦略botの解説

告知 2024年8月11日
公開から年数が経ち情報価値も失われたので、無料記事にしました。
これまでに購入くださった方々に、あらためてお礼を申し上げます。

更新 2022年4月4日
FTX用のコードを無料でご覧いただけるようにしました。

告知 2021年11月28日
BitMEXでのバルクオーダーエンドポイントの廃止に伴い、同取引所用のコードは動作しなくなりました。コード修正の予定はありません。

更新 2021年6月6日
BitMEXでのXBTペアにおける最小売買単位変更に対応しました。

更新 2021年4月13日
ロングとショートで異なる発注サイズを指定できるようにしました。

更新 2021年1月11日
BitMEX用のコードを無料でご覧いただけるようにしました。

更新 2020年7月23日

Discordへの約定通知にmentionを付加するか選べるようにしました。

更新 2020年6月9日
大半の無期限契約で発注サイズの自動調整(定数'UNIT'の設定)が機能するようにしました。

更新 2020年5月2日,5日,17日

FTX用、Bybit用、Binance Futures用のコードを公開しました。

初出 2019年3月16日


チャネルブレイクアウト戦略とは

"The trend is your friend, until the end when it bends."
Ed Seykota

久しくトレンドらしいトレンドが出ず、かつて流行した非元祖ドテン君のようなトレンドフォロー型のbotには厳しい値動きがビットコインでは続いています。しかしながら狭レンジという一種のトレンドも、いつかは終わるのではないでしょうか。上向きか下向きかはともかくとして。

そろそろ次の一手を仕込んでおきませんか。


かつてタートルズと呼ばれた投資集団があり、彼らはドンチアン・チャネル・ブレイクアウト戦略で成功を収めました。詳細は以下の書籍で詳しく語られています。

チャネルブレイクアウト戦略とは一般に、一定期間の高値(安値)を現値が抜いたら買い(売り)参入し、別のあるいは同一の期間の安値(高値)を現値が抜いたら退出をするというものです。

つまり方向感がない相場では高値を買わされ安く売らされを続けることになり、資金とストレスを律するのが大変です。小さな損に何度も耐えながらいつか来るであろう大きな波を待ちます。そしていざトレンドに乗れても、今度は含み益が消える不安に抗いながらポジションを維持しなければならないのです。

しかし、ひとたびトレンドが継続すると大きく利益を伸ばせることが、この戦略の魅力となっています。

- - -

このスクリプトでできること

ここに掲載したPythonスクリプトはタートル流トレーディング規則を参考に、チャネルブレイクアウト部分のみをルール化してBitMEXのXBTUSD Perpetualマーケットにおいて自動売買をするよう作られています。
「損がなかなか切れない」「益をすぐ確定したくなる」のが人間心理です。しかしbotにならば人間の感情と真逆であるところの、規律ある投資行動を任せてしまうことができます。

本来のタートル流規則ではATR(アベレージ・トゥルー・レンジ; ある期間の指数平均値幅)によるサイズ設定やストップロスルールが決められていますが、このスクリプトでは単純なチャネルブレイクアウトのみを再現しています。

追記 2019年3月30日
ATRをもとに発注ロットを自動計算する機能をつけました。


ここから先は、このbotの機能を紹介していきます。該当する部分のコードを抜粋して掲載しますので、無料部分だけでもbot製作の参考にしていただければ幸いです。

有料エリアには動作するコード全体を掲載しています。無料エリアとはバージョンが異なることがあります。ご了承ください。

- - -

# 戦略に採用する足 ccxt依存なので'1m', '5m', '1h', '1d'のいずれか
RESOLUTION = '1d'
SECONDS = 86400

# 未確定足を除く何本でチャネルを判定するか 取得上限499本
# 期間1st: entry条件 >= 期間2nd: exit条件
FIRST_PERIOD_STICKS = 55
SECOND_PERIOD_STICKS = 20

"""中略"""

def fetch_candles():
    """candlesを取得して最大値最小値を取り出す"""
    for i in range(RETRY):
        try:
            now = time() * 1000
            since = FIRST_PERIOD_STICKS
            since = (
                now
                - (since if RESOLUTION != '1d' else
                   since if since > 40 else
                   40)
                * SECONDS
                * 1000
            )
            candles = bitmex.fetchOHLCV(
                'BTC/USD',
                timeframe=RESOLUTION,
                since=since,
                limit=500,
            )
        except Exception as e:
            logger.error('fetch_candles_error: {}'.format(e))
            sleep(ERROR_SLEEP)
        else:
            candles = np.array(candles)
            entry_line = candles[
                -FIRST_PERIOD_STICKS-1:-1,
                OHLC_INDEX
            ]
            exit_line = candles[
                -SECOND_PERIOD_STICKS-1:-1,
                OHLC_INDEX
            ]

            hL_channel = [
                np.amax(entry_line),
                np.amin(entry_line),
                np.amax(exit_line),
                np.amin(exit_line),
            ]
            return hL_channel

未確定足を除いた55本と20本の最高値最安値をそれぞれ取り出します。この4つの数値を元にストップ注文を出すというのがこのbotの動作の根幹となります。設定は容易に変更することができます。

# 有名なあのbot
RESOLUTION = '1h'
SECONDS = 3600
FIRST_PERIOD_STICKS = 18
SECOND_PERIOD_STICKS = 18

# 後述
OC_MODE = False

例えば1時間足にして期間1と期間2の両方を18本で取るだけで、あの著名なドテンbotに早変わりします。

# 発注数量USD 0なら自動計算(fixed or calculated)
LOT = 0

# 証拠金残高に対するレバレッジ倍率 LOTが0の時に有効
LEVERAGE = 1.0

"""中略"""

def fetch_position():
    """現値と建玉を取得しLOTをセットする"""
    for i in range(RETRY):
        try:
            inst_dict = bitmex.publicGetInstrument({
                'symbol': 'XBTUSD',
                'columns': json.dumps(['lastPrice']),
            })[0]
            pos_dict = bitmex.privateGetPosition({
                'symbol': 'XBTUSD',
                'columns': json.dumps(
                    ['avgEntryPrice', 'currentQty']),
            })[0]
        except Exception as e:
            logger.error('fetch_position_error: {}'.format(e))
            sleep(ERROR_SLEEP)
        else:
            last_price = inst_dict['lastPrice']
            entry_price = pos_dict['avgEntryPrice']
            current_qty = pos_dict['currentQty']
            break

    for i in range(RETRY):
        try:
            bal_dict = bitmex.privateGetUserMargin({
                'columns': json.dumps(['marginBalance'])
            })
        except Exception as e:
            logger.error('fetch_balance_error: {}'.format(e))
            sleep(ERROR_SLEEP)
        else:
            total_BTC = bal_dict['marginBalance'] * 0.00000001
            total_USD = total_BTC * last_price
            order_lot = LOT if LOT else int(total_USD * LEVERAGE)
            break

    position = [
        last_price,
        entry_price,
        current_qty,
        total_BTC,
        total_USD,
        order_lot,
    ]
    return position

発注数量を口座残高の何倍にするか設定すると、含み損益も含めた数値をもとにロットが計算されます。倍率は1倍未満でも構いません。もちろん固定ロットにもできます。発注量の多すぎ少なすぎにはお気をつけください。

- - -

def post_orders(
        position_sign,
        order_side,
        last_price,
        current_qty,
        order_lot,
        break_price1,
        break_price2,
        **kwargs):
    """条件に応じた注文を出す"""
    # ポジションがあればexit_stopを入れる
    if position_sign != 'NONE':
        for i in range(RETRY):
            # closeを出すとともに次のbreakを出しておく
            order_params = [{
                'symbol': 'XBTUSD',
                'ordType': 'Stop',
                'side': order_side,
                'orderQty': None,
                'stopPx': break_price2,
                'execInst': 'LastPrice, Close',
            }, {
                'symbol': 'XBTUSD',
                'ordType': 'Stop',
                'side': order_side,
                'orderQty': order_lot,
                'stopPx': break_price1,
                'execInst': 'LastPrice',
            }]
            try:
                bitmex.privatePostOrderBulk({
                    'orders': json.dumps(order_params),
                })
            except Exception as e:
                logger.error('exit_orders_error: {}'.format(e))
                sleep(ERROR_SLEEP)
            else:
                return True

    # ポジションがなければチャネルの外側にentry_stopを入れる
    else:
        for i in range(RETRY):
            order_params = [{
                'symbol': 'XBTUSD',
                'ordType': 'Stop',
                'side': 'Buy',
                'orderQty': order_lot,
                'stopPx': break_price1,
                'execInst': 'LastPrice',
            }, {
                'symbol': 'XBTUSD',
                'ordType': 'Stop',
                'side': 'Sell',
                'orderQty': order_lot,
                'stopPx': break_price2,
                'execInst': 'LastPrice',
            }]
            try:
                bitmex.privatePostOrderBulk({
                    'orders': json.dumps(order_params),
                })
            except Exception as e:
                logger.error('entry_orders_error: {}'.format(e))
                sleep(ERROR_SLEEP)
            else:
                return True

    return False

注文部分の一部抜粋です。チャネルブレイクアウト戦略では高値と安値や参入価格と退出価格など、複数の注文を同時に出すことになります。このコードではバルク注文としてまとめて送信しています。

def amend_orders(
        order_id_HN,
        order_id_LF,
        break_price_HN,
        break_price_LF,
        order_lot_HN,
        order_lot_LF,
        **kwargs):
    """チャネル変化にそって注文を修正する"""
    for i in range(RETRY):
        order_params = [{
            'symbol': 'XBTUSD',
            'orderID': order_id_HN,
            'leavesQty': order_lot_HN,
            'stopPx': break_price_HN,
        }, {
            'symbol': 'XBTUSD',
            'orderID': order_id_LF,
            'leavesQty': order_lot_LF,
            'stopPx': break_price_LF,
        }]
        try:
            bitmex.privatePutOrderBulk({
                'orders': json.dumps(order_params),
            })
        except Exception as e:
            logger.error('amend_orders_error: {}'.format(e))
            sleep(ERROR_SLEEP)
        else:
            return True

    return False

高値安値が変化した場合は、注文を出し直すのではなく注文修正でストップ価格をずらしています。それと同時に次のブレイク時のロットサイズも調整します。

追記 2019年4月6日
退出注文が約定してポジションがなくなったとき、以前はチャネル上下とも注文を出し直していましたが、有効な位置に出ている参入注文はそのまま活かし反対側の参入注文だけを追加するようにしました。

- - -

このbotの特徴的な機能 

レンジ相場であってもビットコインの値動きは大きく、時にローソク足の上下に長いヒゲを作ることがあります。そのため通常のチャネルブレイクアウト戦略のように最高値と最安値でチャネル判定をすると、レンジの実勢に比べて参入退出価格が大きく離れてしまうことがあります。

# OpenClose_MODE チャネルを実体(除ヒゲ)で判定する
OC_MODE = False

OHLC_INDEX = [1, 4] if OC_MODE else [2, 3]
画像1

そこでこのbotでは、ローソク足の実体である始値と終値だけを結んでチャネル判定ができるOC_MODEを備えています。Over_ClockedではなくOpen_Closeです。通常のHigh_Lowよりも狭い値幅でチャネル判定をするので参入退出の機会が増える一方で、だましにも掛かりやすくなることには注意がいります。掲載コードの初期値ではこの機能はオフです。

暗号通貨の取引所ではチャネルブレイクするような価格変動があった時に、混雑からか注文が通らなかったりポジションが取得できないことがままあります。そこでこのスクリプトでは注文はあらかじめ逆指値で出しておくように統一しています。

# 建玉数が規定LOTを大きく(e.g. 1.5倍)超えていたら
# 現値にstopを入れて超過分を削る(maintain the qty limit)
# (API混雑時などの意図しない注文重複を想定)
if abs(current_qty) > order_lot * 1.5:
    decrease_lot = abs(current_qty) - order_lot
    order_params = [{
        'symbol': 'XBTUSD',
        'ordType': 'Stop',
        'side': order_side,
        'orderQty': decrease_lot,
        'stopPx': last_price,
        'execInst': 'LastPrice, ReduceOnly',
    }]

ポジション取得ができず二重注文となった場合などに建玉を持ちすぎないように、建玉が規定ロットを超えていたら規定数まで減らすようにコーディングしています。

- - -

"""設定ここから"""
"""edit settings to add your Keys and change parameters"""

APIKEY = 'APIKeyAPIKeyAPIKeyAPIKeyAPIKeyAPIKey'
SECRET = 'SecretSecretSecretSecretSecretSecretSecretSecret'
TESTNET_APIKEY = 'APIKeyAPIKeyAPIKeyAPIKeyAPIKeyAPIKey'
TESTNET_SECRET = 'SecretSecretSecretSecretSecretSecretSecretSecret'

TESTNET = False

DISCORD_URL = ''
# DISCORD_URL = (
#     'https://discordapp.com/api/webhooks/'
#     'IdIdIdIdId/'
#     'TokenTokenTokenTokenTokenToken'
# )

# stopをチャネル上下端の何ドル外側に仕掛けるか 0.5USD単位
# how many $ outside of the channel(0 or int multiple of 0.5)
OFFSET = 0.5

# order直後の待機秒数(seconds),
# ロジック全体のループ待ち秒数(seconds)
ORDER_WAIT = 40
LOOP_WAIT = 20

# error時の再試行回数(times),
# error時の休止秒数(seconds)
RETRY = 10
ERROR_SLEEP = 6

# ログファイル名をお好みで
LOGFILE_DIR = './log/'
LOGFILE_PREFIX = 'cRe'
LOGFILE_DATE = strftime('%Y%m%d%H%M')
2019-03-23 04:40:37,623 - INFO - system'cRe5520' ver.2.1.323 cRe201903230440.log
2019-03-23 04:40:37,624 - INFO - RESO: '1h', 1ST: 20, 2ND: 10, OFFSET: 0.5, OC: False
2019-03-23 04:40:37,627 - INFO - 
2019-03-23 04:40:37,683 - INFO - hL_channel: [4037.0, 3938.5, 4000.0, 3965.0]
2019-03-23 04:40:37,705 - INFO - last_price: 3983.5, entry_price None, current_qty: 0
2019-03-23 04:40:37,718 - INFO - balance: 1.06723304
2019-03-23 04:40:37,748 - INFO - 'put_amend_orders': High: 4037.5, Low: 3938.0
2019-03-23 04:41:17,977 - INFO - 
2019-03-23 04:41:17,995 - INFO - hL_channel: [4037.0, 3938.5, 4000.0, 3965.0]
2019-03-23 04:41:18,016 - INFO - last_price: 3978.5, entry_price None, current_qty: 0
2019-03-23 04:41:38,062 - INFO - 
2019-03-23 04:41:38,150 - INFO - hL_channel: [4037.0, 3938.5, 4000.0, 3965.0]
2019-03-23 04:41:38,170 - INFO - last_price: 3978.5, entry_price None, current_qty: 0
画像2

MAINNETとTESTNETとを容易に切り替えられるようにしてあります。また動作ログを残しますので稼働状況が後からも確認できます。注文処理の直後に簡単なレポート通知をDiscordに飛ばすこともできます。

### debug ###
RESTARTER = 0

"""中略"""

### debug ###
restart_counter = RESTARTER if RESTARTER else 0

while True:

    ### debug ###
    if RESTARTER:
        logger.debug(
            'loop_counter: {}'.format(restart_counter))
        restart_counter -= 1
        if restart_counter <= 0:
            logger.warning('counter_value_has_reached')
            logger.warning('restart_script')
            os.execv(sys.executable, (
                sys.executable,
                os.path.abspath(__file__),
            ))

ログファイルを一定サイズに細分化したり、コード修正を都度自動的に反映させたりと、開発上の便宜のためにスクリプトの自動再起動機能を埋め込みました。開発用のつもりですが、掲載コードにもそのまま残してありますので設定をすればお使いいただけます。変数RESTARTERがスイッチでありカウンターです。

- - -

このbotを動かすには

私はこのbotをAmazon Web ServicesのEC2で動作させています。
有料エリアのコードを動かすにはPython3スクリプトの動作環境を構築できるスキルが必要です。

必須の外部モジュールはccxtとnumpyです。必要に応じてrequests(Discord通知用)とWebSocket公式コネクタを導入してください。WebSocketで現在価格や証拠金残高を取得するコードが実験的に埋めてあります。

sudo /usr/bin/pip-3.6 install --upgrade pip

sudo /usr/local/bin/pip3 install ccxt
sudo /usr/local/bin/pip3 install numpy

sudo /usr/local/bin/pip3 install requests
sudo /usr/local/bin/pip3 install bitmex-ws

上記の数行を見て何をすべきかわかる方なら動作設定はできそうです。逆に自分で解決する自信のない方はこのコードに大切なお金を支払ってはいけません。

冒頭で述べたように、対象となる取引サービスはBitMEXのXBTUSD Perpetualです。BitMEXの口座開設やAPIKEY取得についてもこの記事では解説しません。

追記 2019年3月28日
Windows 10上のPython 3.7.3で動作しました。

画像3
PS C:\Users\home> pip install ccxt
PS C:\Users\home> pip install numpy

(optional)
PS C:\Users\home> pip install request
PS C:\Users\home> pip install bitmex-ws
画像4

- - -

過去の更新履歴

2020年4月25日
例外処理の一部を見直しました。

2020年2月21日
ストップ注文のトリガー判定を、マーク価格でするか直近価格でするか選べるようにしました。
チャネル変化のDiscord通知に加えて、1時間毎の定期通知もできるようにしました。

2020年2月13日

XBTUSD以外の契約でもレバレッジ指定ができるようにしました。
Discord通知の文言を短縮しました。

2020年2月8日

取得する足数を明示的に多くして、外部ライブラリの挙動に依存せず仕様どおりに動作するようにしました。

2019年9月28日

コントラクト廃止時の処理などを見直し、動作をより安定させました。

2019年8月11日

ATR計算用に取った日足本数を確かめる処理を入れました。

2019年7月12日

例外処理の分岐条件を追加しました。
建玉数を規定まで満たす際の注文処理を見直しました。

2019年6月1日

約定のしやすさを優先するように調整をしました。
チャネル変化の判定を見直して不必要な注文訂正を減らしました。
通知の表記を見直しました。

2019年5月15日

ストップ指値注文とストップ成行注文とが選べるようになりました。初期値はストップ指値注文です。

2019年4月27日
太郎チャートに対応しました。
ログファイル名から戦略の足幅足数が分かるようにしました。
掲載コードの初期値を、55日参入20日退出から、50時間参入10時間退出に変更しました。
ATR取得タイミングを微調整しました。

2019年4月20日
ローソク足の取得頻度を見直し、1時間足と日足の動作でAPIアクセスを大幅に減らしました。

2019年4月17日

WebSocket切断検出時に、スクリプトの再起動ではなくWebSocketを再接続するようにしました。

2019年4月11日
動作ループの中で毎回していたATR計算用の日足取得を、1日1回だけするようにしました。
スクリプトが停止する例外発生時に、記録と通知をするようにしました。

2019年4月6日
退出注文が約定してポジションがなくなったとき、有効な位置に出ている参入注文は消さず、反対側の参入注文だけを追加するようにしました。

2019年3月30日
20日間ATR(SMMA)をもとに発注ロットを自動計算する機能をつけ、該当コードの解説記事を公開しました。

2019年3月28日
Windows 10上のPython 3.7.3で動作した旨を追記しました。

2019年3月24日
固定ロット設定の場合に残高が取得できていなかったバグを修正しました。
ポジション取得と残高取得の関数を分離し、注文の必要がないときは残高確認をしないようにしました。
ローソク足を全般的にnumpyで処理するようにコードを整理しました。

2019年3月23日
ポジションがないときは一律に注文発注プロセスに遷移していましたが、オーダーが既に出ている場合はその注文を修正するようにしました。

2019年3月17日
バックテスト用Pineスクリプトを別記事で公開しました。

初出 2019年3月16日
私が動かしているbotについて、4本値取得とチャネル判定、状態取得と注文量設定、発注とその修正などの該当コードを公開し、簡単な説明をつけた記事を掲載しました。また有料エリアにコード全体を公開しました。

- - -

おわりに

RESOLUTION = '1h'
FIRST_PERIOD_STICKS = 60
SECOND_PERIOD_STICKS = 18

OC_MODE = True

このコードの当初設定である55日参入の20日退出では、参入の機会があまり発生しません。ですので例えば1時間足の数十時間で参入し十数時間での退出をするなど、足元の値動きを見ながら設定値を探ってはいかがでしょうか。一例としては1時間足60本で参入し18本逆行で退出するくらいを手始めに調整ができそうです。

画像5
画像9

更新 2019年4月27日
有料エリア掲載コードの初期値を、55日参入20日退出から、50時間参入10時間退出に変更しました。初期値でもそこそこ戦えるかと思います(上図)。別記事のPineスクリプトも活用して調整してみてください。

RESOLUTION = '1h'
FIRST_PERIOD_STICKS = 50
SECOND_PERIOD_STICKS = 10

OC_MODE = False

勝率が高くはない戦略ですから、小さく続く負けで消耗しないように、レバレッジは低めにしておいたほうがよいでしょう。

またBitMEXでは成行手数料が高いので、売買が高頻度になり値幅も小さい5分足以下でチャネルブレイクアウト戦略を試みても、利益を出すのは難しいかもしれません。

しかし時間軸を伸ばしてみるとどうでしょうか。

ProjectBBBさんによる上記の記事には、同戦略の特徴とそれにどう向き合えばよいかが分かりやすくまとまっています。ご一読をお勧めします。

日足や1時間足で戦略を組みトレンド発生をじっくり待つのが、このbotのお勧めの使い方です。私自身もそうして稼働させています。

- - -

おまけ

画像7
画像8

更新 2019年4月30日
XBTUSD以外のペアにも対応しました。

おまけの機能としてXBTUSD以外のペアも選べるようにしました。ミスプライシングやテイカーフィーのため、このbotのようなストップ成行の売買では扱いにくいペアが多いですが、ETHUSDならばフィーもXBTUSD同様ですし比較的流動性もあっておもしろそうです。

ペアに合わせて適切なLOT値とOFFSET値を設定してください。なおATRでのロット管理はXBTUSD専用です。

画像8

ひとつの口座で異なる複数のペアを同時に動かすこともできます。スクリプトのファイル名を変えて値を設定し、それぞれ別に起動してください。

- - -

解説記事はここまでです。以下の有料エリアに全体のコードを掲載しました。'cRe5520'というbot名称の由来は秘密です。皆さんのところにも大きなトレンドが訪れるよう願っています。

感情に左右されない規律あるトレードを、あなたも体験してみてください。

for the Disciplined Trading.


Channel breakout trading strategy bot for BitMEX 
- system'cRe5520' ver.2.8a.0606a

Channel breakout trading strategy bot for FTX
- system'cRe5520' ver.3.0a.0404

Channel breakout trading strategy bot for Bybit
- system'cRe5520' ver.3.3a.0404

Channel breakout trading strategy bot for Binance Futures
- system'cRe5520' ver.3.6a.0404

追記
現在はこの記事よりも、次のリンク先のほうがお得な価格になっています。見られるコードは完全に同じものですから、コード全体をご覧になりたい方や動かしてみたい方は価格が安い方をご購入ください。

-->  system'cRe5520' コード閲覧権

更新 2022年4月4日
FTX用のコードを無料でご覧いただけるようにしました。

更新 2021年1月11日
BitMEX用のコードを無料でご覧いただけるようにしました。

以下がFTX用とBybit用、そしてBinance Futures用コードへのリンクです。

ご購入いただき、ありがとうございます。
Thank you so much for your purchase.
I hope you will have good fortune!

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