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

更新 2019年5月15日
ストップ指値注文とストップ成行注文とが選べるようになりました。初期値はストップ指値注文です。

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

"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'
FIRST_PERIOD_STICKS = 18
SECOND_PERIOD_STICKS = 18

# 後述
OC_MODE = False

例えば1時間足にして期間1と期間2の両方を18本で取るだけで、あの著名なドテンbotに早変わりします。なお有料パートに掲載したコードでは、SECONDSは自動設定するようになっています。

# 発注数量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]

そこでこの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

細かい点ですが、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で動作しました。

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

更新履歴

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本逆行で退出するくらいを手始めに調整ができそうです。

更新 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のお勧めの使い方です。私自身もそうして稼働させています。

- -

追記 2019年4月30日
有料パートのbotスクリプトにおまけの機能をつけました。
ヒント: 上図がおまけ機能での2018年8月からのバックテストです。

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

for the Disciplined Trading.

Channel breakout strategy bot for BitMEX
system'cRe5520' ver.2.7.515d

この続きをみるには

この続き:41,643文字/画像3枚

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

ottimista

5,520円

この記事が気に入ったら、サポートをしてみませんか?気軽にクリエイターを支援できます。

note.user.nickname || note.user.urlname

待って。サポートはそのあとで。 https://www.bitmex.com/register/2ueMAY You will receive a 10% fee discount for the first six months.

INFO - last_price: 3939
13

ottimista

コメントを投稿するには、 ログイン または 会員登録 をする必要があります。