見出し画像

マルチタイムフレームと平均足を使ったBOT for bF

かぴぱら(@kapipara180)です。

今回はマルチタイムフレーム(複数の時間足を用いた手法)と平均足を用いたBOTを作成しました。Tradingviewを用いたバックテスト結果は以下の通りです。

画像1

画像2

画像が小さいので成績を書いておきます。
純利益率:102404%(1000倍)
トレード数:1457
勝率:69.05%
PF:8.323
最大DD:5.41

ストラテジーの概要は以下の通り。
期間:6/20~6/30(10日)
足:1分と5分
資金管理:資金に対して固定レバレッジ分全プッシュ(5倍)

今回はこのストラテジーのpine scriptbitflyer用のpythonコードを公開します!先に白状すると、このバックテスト結果はTradingviewの欠陥がはらんでいて、実際にはこの利益はでません。フロントテストを長期間していないので正確にはわかりませんが、ヨコヨコ相場だと負けて、乱高下したらちょっと勝つぐらいです。ただ、このストラテジーを通して、より正確にテストを実施する方法や、マルチタイムフレームのスクリプトの組み方などいろいろな学びがあったので、ソースコードを全部公開します!本書の要素は以下の通りです。

・pine script
マルチタイムフレーム/平均足を使ったストラテジー/資金に対してレバレッジをかけたロット計算/バックテストのtips

・bitflyer用Bot(pybitflyer)
きちんと動く自動売買BOTのソース/マルチタイムフレーム/平均足を使ったストラテジー/資金に対してレバレッジをかけたロット計算/childorderの確認・キャンセル/try catchを使ったエラーハンドリング/log出力
※BOTとして稼働させるための要素は揃っているので、シグナルの箇所を変更すれば好きなBOTを作ることができるソースです!(parentorderの出し方も知りたければサンプルソース提供します)

ではさっそくpine scriptから行ってみましょう。
まずは以下のソースをpine scriptのエディタに張り付けてストラテジーの結果が出ることを確認してください。重要なポイントを説明していきます。

//@version=2
//Heikinashi strategy by kapipara

strategy("Heikin Ashi Strategy ver kapipara",shorttitle="HAS kapipara",overlay=true ,default_qty_value=100,initial_capital=1000000,currency=currency.JPY)

time_frame_short = input(title="Heikin Ashi Short Time Frame", type=resolution, defval="1") //平均足を1分足で作成
time_frame_long = input(title="Heikin Ashi Long Time Frame", type=resolution, defval="5") //平均足を5分足で作成
shift = input(defval=0, title="Candle Shift") //ローソクをずらすとき用(最終的に使わず)
fama = input(defval=1, title="fast SMA Period") //移動平均の期間short
sama = input(defval=10, title="Slow SMA Period") //移動平均の期間long

以降は有料となります!感想お待ちしています!

2020/1/15:無料に変更しました
//@version=2
//Heikinashi strategy by kapipara

strategy("Heikin Ashi Strategy ver kapipara",shorttitle="HAS kapipara",overlay=true ,default_qty_value=100,initial_capital=1000000,currency=currency.JPY)

time_frame_short = input(title="Heikin Ashi Short Time Frame", type=resolution, defval="1") //平均足を1分足で作成
time_frame_long = input(title="Heikin Ashi Long Time Frame", type=resolution, defval="5") //平均足を5分足で作成
shift = input(defval=0, title="Candle Shift") //ローソクをずらすとき用(最終的に使わず)
fama = input(defval=1, title="fast SMA Period") //移動平均の期間short
sama = input(defval=10, title="Slow SMA Period") //移動平均の期間long

//平均足を取得
ha_t = heikinashi(tickerid) //追加したチャートの平均足を取得

ha_open = security(ha_t, time_frame_short, open[shift] ) //1分足の平均足のopen[0]を取得
ha_close = security(ha_t, time_frame_short, close[shift]) //1分足の平均足のclose[0]を取得
ha_high = security(ha_t, time_frame_short, high[shift] ) //1分足の平均足のhigh[0]を取得
ha_low = security(ha_t, time_frame_short, low[shift] ) //1分足の平均足のlow[0]を取得
mha_close = security(ha_t, time_frame_long , close[shift]) //5分足の平均足のclose[0]を取得
plotcandle(ha_open, ha_high, ha_low, ha_close, title='heikin ashi', color = ha_close>= ha_open ? green : red) //平均足を表示

//移動平均を取得
sma = sma(ha_close[shift],sama) //1分足の平均足のclose[0]の10平均
fma = sma(mha_close[shift],fama) //5分足の平均足のclose[0]の1平均
plot(fma,title="FMA",color=green,linewidth=3,style=line)
plot(sma,title="SMA",color=red ,linewidth=2,style=line)

//売買シグナル
golong = crossover(fma,sma) 
goshort = crossunder(fma,sma)

//ロット計算
Leverage = input(title='leverage', type=integer, defval = 5)
Lot = strategy.equity * Leverage / close

strategy.entry("Buy",strategy.long ,when = golong, qty=Lot)
strategy.entry("Sell",strategy.short ,when = goshort, qty=Lot)

ここから解説です。

//平均足を取得
ha_t = heikinashi(tickerid) //追加したチャートの平均足を取得

ha_open   = security(ha_t, time_frame_short, open[shift] ) //1分足の平均足のopen[0]を取得
ha_close  = security(ha_t, time_frame_short, close[shift]) //1分足の平均足のclose[0]を取得
ha_high   = security(ha_t, time_frame_short, high[shift] ) //1分足の平均足のhigh[0]を取得
ha_low    = security(ha_t, time_frame_short, low[shift]  ) //1分足の平均足のlow[0]を取得
mha_close = security(ha_t, time_frame_long , close[shift]) //5分足の平均足のclose[0]を取得

pine scriptにはheikinashiという便利なビルトイン機能があります。
今回はこのheikinashiに、元となるtickeridを渡すことで平均足の値セットを作成します。tickeridというコマンドはpine scriptを追加したチャートのidを取得するコマンドです。追加したチャート以外の値を基にした平均足を取得したい場合は、このIDを所望のIDに変更すればOKです。

次に、取得した値セットから、欲しい時間足の値セットを取得します。
これにはsecurityというビルトイン機能を使用します。
これを使えば、サンプルコードのように好きな時間軸の足情報を取得することができます。
pine script内で時間軸をすべて指定すれば、どの時間軸のチャートにscriptを追加しても同じ値をとるようにできますが、追加するチャートの時間軸によって計算が行われる頻度が変わる(計算は足の更新ごとに実行されるため)ので、最終的な損益も変化します。

今回のコードでは、1分足の平均足と5分足の平均足をそれぞれ取得しています。どの時間軸の平均足を取得するかは「time_frame_short」「time_frame_long」のdefvalに指定しているので、ここを変更すれば平均足の取得時間軸を変更することができます。

//移動平均を取得
sma = sma(ha_close[shift],sama) //1分足の平均足のclose[0]の10平均
fma = sma(mha_close[shift],fama) //5分足の平均足のclose[0]の1平均

次に、それぞれの平均足の単純移動平均(SMA)を取得しています。
色々試した結果、fmaについては1(移動平均ではなくcloseそのもの)が最もよかったです。fmaがsmaを横切ることをシグナルとしているため、移動平均にするとどうしてもシグナルの発生が遅くなりますので、その分乗り遅れて利益が減ってしまうことが要因と思われます。

//売買シグナル
golong = crossover(fma,sma) 
goshort = crossunder(fma,sma)

売買シグナルは非常に単純で、fmaがsmaを上抜けば買いシグナル。逆なら売りシグナルです。

画像3

上記の①では、fma(緑)がsma(赤)を上抜いています。この上抜く足が確定するのが、Buyを発注している足のopenした時なので、この足でBuyの発注が行われます。発注価格はこの足のopenです。逆に②ではfma(緑)がsma(赤)を下抜いているため、sellの発注が行われます。後述しますが、ドテン売買をルールにしているため、常に建玉を決済するロット+資金から算出したロットを足した値がグラフに表示されます。(+xxxxxx,-xxxxxxと表示されている値)

//ロット計算
Leverage = input(title='leverage', type=integer, defval = 5)
Lot      = strategy.equity * Leverage / close

if BackTest()
    strategy.entry("Buy",strategy.long ,when = golong, qty=Lot)
    strategy.entry("Sell",strategy.short ,when = goshort, qty=Lot)

strategy.equityで未実現損益を加味した現在の資金を取得できます。これにLeverageをかけて、現在値(close)で割ったものを次のロットに指定します。
これで、複利を聞かせたストラテジーを組むことができます。
if BackTestは計算期間を指定しているだけなので割愛します。
strategy.entryでは、whenでシグナルが立っていることを条件に指定しており、qtyに計算したロットを指定しています。このようにロジックを組めば、前述したように「建玉を決済するロット+資金から算出したロット」での発注が行われます。

以上でscriptの説明は終わりです。さて、なぜこのpine scriptは実際の値動きと異なる結果を生んでしまうのでしょうか?どこも不自然なところはありませんよね?私もそう思って意気揚々とbitflyerで売買を行いました。幸い大きな値動きがあったのでトントンでしたが、負けが多いです。さて、ではこの理由について説明していきましょう。

・マルチタイムフレームストラテジーをバックテストする場合の注意

結論から言うと、マルチタイムフレームを使ってpine scriptを組んだ場合、まだ確定していない未来の情報を織り込んでバックテストを実施してしまうため、恐ろしい勝率を叩き出してしまうこと。これが原因です。
詳しく見ていきましょう。

画像4

先ほどと同じチャートです。
先ほど①で示したBuyシグナルを良く見てみましょう。このシグナルがこの足で発動したのは、ここから5本の足(青線で示した足)が上昇したことによって5分平均足が大きく上昇したためです。....よく考えてみてください、これをBuyシグナルの足時点で知っていて、fmaとsmaがクロスしたということを知ることは現実には不可能なのです。ですが、pine scriptでは指定された時間指定された値を表示しその値を使用してバックテストを実施してしまいます。これにより、マルチタイムフレームストラテジーを小さい時間軸で実行した場合、未来の情報も織り込んでバックテストを実施してしまうのです。
緑で囲んだ部分は同様に、最後の3分で大きく下げたことを織り込んでいたためにfmaとsmaがクロスせず、良い値までSellを引っ張り、利益を増やせています。

・マルチタイムフレームストラテジーのテスト方法

安心してください。マルチタイムフレームのバックテスト結果がまっとうな結果か確かめる方法があります。それはTradingview上でフロントテストを実施してその結果とバックテスト結果を比べるというものです。Tradingviewは、ストラテジーをチャートに追加した状態で放置すると、そのストラテジーに沿って自動売買を実施する機能があり、それが随時バックテスト結果に反映されるようになっています。これを使って、ストラテジーのフロントテストを実施することができます。
では、今回のストラテジーのフロントテスト結果を見てみましょう。

上の画像がフロントテスト結果、下の画像が同じタイミングでのバックテストのみの結果です。損益グラフに線を引いた箇所から右がフロントテストなのですが、見事に負けています。対して、バックテストのみの方はその期間も利益が出ています。また、BuyとSellの箇所もそれぞれの画像で違います。この方法で双方に差がなければ、そのシステムは実際に利益を出してくれるでしょう。差がある場合は、期待した利益を出してくれないかもしれない。と考えることができます。

画像5

画像6

※取得したタイミングやレバレッジの設定が今までの画像と異なるため、勝率や利益率が異なります。気にしないでください。

以上で、pine script編は終わりです。続いてbitflyerの自動売買プログラムのソースを見ていきましょう。
※デバッグしていますが掲載にあたり、一部修正したため不整合があるかもしれません。あればご連絡いただければと思います。

#!/usr/bin/env python3
# coding: utf-8
import pybitflyer
import time
import datetime
import logging
from decimal import (Decimal, ROUND_DOWN)
from pytz import timezone, utc
import requests
import numpy as np
from pprint import pprint
import json

leverage = 5 #レバレッジ
SLEEP = 15 #プログラム実行インターバル
period = 300 #平均足を計算する期間
api_key = 'xxxxxx' #bitflyerで払い出したkey
api_secret = 'xxxxxx' #bitflyerで払い出したkey
api = pybitflyer.API(api_key=api_key, api_secret=api_secret) 

def customTime(*args):  #日本時間を指定するための関数
    utc_dt = utc.localize(datetime.datetime.utcnow())
    tz = timezone("Japan")
    converted = utc_dt.astimezone(tz)
    return converted.timetuple()

def position():  #保有ポジションの方向・数量を取得する関数
    try:
        ret = api.getpositions(product_code="FX_BTC_JPY")
    except Exception as e:
        logger.error("Error!")
        logger.error(e)
        return {'side': pos['side'], 'size': pos['size']}
    if len(ret) == 0: #pybitflyerではpositionがない場合[]が返ってくるのでlenで判定
        side = 'NO POSITION'
        size = 0
    elif ret[0]['side'] == 'BUY':
        side = 'LONG'
        sum = 0.0
        for o in ret: #ピラミッディングはしないが、部分約定する可能性があるのでsumする
            sum = sum + o['size']
        size = sum
    else:
        side = 'SHORT'
        sum = 0.0
        for o in ret:
            sum = sum + o['size']
        size = sum * -1.0 #ショートはポジションをマイナスとみなす
    return {'side': side, 'size': size}

def balance(): #資産を取得する関数
    try:
        ret = api.getcollateral()
        pnl = ret['open_position_pnl']
        jpy = ret['collateral']
        return {'pnl': pnl, 'jpy': jpy}
    except Exception as e:
        logger.error("Error!")
        logger.error(e)
        return {'pnl': pnl, 'jpy': jpy}

def get_lot(price_now):  #Lot計算用ルーチン
    Last_btc_value = price_now
    Free_jpy = balance()['jpy']
    Free_levaraged = Free_jpy * leverage
    Capacity = Free_levaraged / Last_btc_value
    value = float(Decimal(Capacity).quantize(Decimal('0.01'), rounding=ROUND_DOWN))
    logger.info('ロットサイズ:%f '% value)
    return value

def stop(side, price, size): #逆指値を発注する関数
    size = float(Decimal(size).quantize(Decimal('0.01'), rounding=ROUND_DOWN))
    o = api.sendparentorder(order_method="SIMPLE", parameters=[{"product_code": "FX_BTC_JPY", "condition_type": "STOP_LIMIT", "side": side, "price": price,"trigger_price":price,"size": size}])
    logger.info('新規注文:%s %s %d %f' % (o,side,price,size))
    return price

def market(side, size): #成行注文を発注する関数
    size = float(Decimal(size).quantize(Decimal('0.01'), rounding=ROUND_DOWN))
    o = api.sendchildorder(product_code="FX_BTC_JPY", child_order_type="MARKET", side=side, size=size)
    logger.info('新規注文:%s %s %f' % (o, side, size))
    return size

def limit(side, price,size): #指値を発注する関数
    size = float(Decimal(size).quantize(Decimal('0.01'), rounding=ROUND_DOWN))
    o = api.sendchildorder(product_code="FX_BTC_JPY", child_order_type="LIMIT", price=price, side=side, size=size)
    logger.info('新規注文:%s %s %f' % (o, side, size))
    return size

#log用
logger = logging.getLogger('LoggingTest')
logger.setLevel(10)
fh = logging.FileHandler('heikin_ashi_bitflyer.log')  #ログファイル名
logger.addHandler(fh)
sh = logging.StreamHandler()
logger.addHandler(sh)
formatter = logging.Formatter('%(asctime)s %(lineno)03d: %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
fh.setFormatter(formatter)
sh.setFormatter(formatter)
logging.Formatter.converter = customTime

pos = {'side': 'NO POSITION', 'size': 0}
bal = {'pnl': 0, 'jpy': 0}
sma = {'before': 0, 'now': 0}
sma_300 = {'before': 0, 'now': 0}
golong = 0
goshort = 0

logger.info('----- start -----')
while True:
    try:
        now = str(int(datetime.datetime.now().timestamp()))  #現在時刻の取得

        # CryptowatchのAPIで1分足を取得
        r = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc?periods=60") #直近500本が取得できる
        ohlcv = r.json()['result']['60'] #1:o 2:h 3:l 4:c 5:v
        #1分足のohlcvを分解
        tmp_high = []
        tmp_low = []
        tmp_close = []
        tmp_open = []
        for i in range(len(ohlcv)):
            tmp_open.append(ohlcv[i][1])
            tmp_high.append(ohlcv[i][2])
            tmp_low.append(ohlcv[i][3])
            tmp_close.append(ohlcv[i][4])
        #平均足を計算
        ha_open = []
        ha_close = []
        ha_high = []
        ha_low = []
        for i in range(-period, 0):
            if i == -period:
                ha_open.append(tmp_open[i])
                ha_close.append(tmp_close[i])
                ha_high.append(tmp_high[i])
                ha_low.append(tmp_low[i])
            else:
                ha_open.append((ha_open[-1] + ha_close[-1])/2)
                ha_close.append((tmp_open[i-1] + tmp_close[i-1] + tmp_high[i-1] + tmp_low[i-1])/4)
                ha_high.append(tmp_high[i])
                ha_low.append(tmp_low[i])
        sma_np = np.array(ha_close)
        sma['before'] = np.average(sma_np[-11:-1]) #12個前~1個前の平均
        sma['now'] = np.average(sma_np[-10:]) #11個前~直近の平均

        # CryptowatchのAPIで5分足を取得
        r_300 = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc?periods=300") #直近500本が取得できる
        ohlcv_300 = r_300.json()['result']['300'] #1:o 2:h 3:l 4:c 5:v
        #5分足のohlcvを分解
        tmp_high_300 = []
        tmp_low_300 = []
        tmp_close_300 = []
        tmp_open_300 = []
        for i in range(len(ohlcv_300)):
            tmp_open_300.append(ohlcv_300[i][1])
            tmp_high_300.append(ohlcv_300[i][2])
            tmp_low_300.append(ohlcv_300[i][3])
            tmp_close_300.append(ohlcv_300[i][4])
        #平均足を計算
        ha_open_300 = []
        ha_close_300 = []
        ha_high_300 = []
        ha_low_300 = []
        for i in range(-period,0):
            if i == -period:
                ha_open_300.append(tmp_open_300[i])
                ha_close_300.append(tmp_close_300[i])
                ha_high_300.append(tmp_high_300[i])
                ha_low_300.append(tmp_low_300[i])
            else:
                ha_open_300.append((ha_open_300[-1] + ha_close_300[-1])/2)
                ha_close_300.append((tmp_open_300[i-1] + tmp_close_300[i-1] + tmp_high_300[i-1] + tmp_low_300[i-1])/4)
                ha_high_300.append(tmp_high_300[i])
                ha_low_300.append(tmp_low_300[i])
        sma_300['before'] = sma_300['now']
        sma_300['now'] = (tmp_open_300[-1] + tmp_close_300[-1] + tmp_high_300[-1] + tmp_low_300[-1])/4

        price_now = tmp_close[-1]  # 一番後ろが一番最新
        pos = position()
        bal = balance()

        logger.info('ポジション:%s %f' % (pos['side'], pos['size']))
        logger.info('未実現損益:%+.6f' % bal['pnl'])
        logger.info('証拠金残高:%+.6f' % bal['jpy'])

        #売買判定
        if sma['now'] > sma_300['now']:
            goshort = 1
        if sma['now'] < sma_300['now']:
            golong = 1

        #古い注文をキャンセル
        orders = api.getchildorders(product_code="FX_BTC_JPY", child_order_state="ACTIVE")
        cancel = ''
        for o in orders:
            if o['product_code'] == 'FX_BTC_JPY' and o['child_order_type'] == 'limit' and o['side'] == 'BUY' and goshort == 1:
                cancel = api.cancelchildorder(product_code='FX_BTC_JPY', child_order_id=o['child_order_id'])
                logger.info('candcel:%s' % cancel)
            if o['product_code'] == 'FX_BTC_JPY' and o['child_order_type'] == 'limit' and o['side'] == 'SELL' and golong == 1:
                cancel = api.cancelchildorder(product_code='FX_BTC_JPY', child_order_id=o['child_order_id'])
                logger.info('candcel:%s' % cancel)

        if pos['side'] == 'LONG' and goshort == 1: #LONGポジションでgoshortなら指値売
            market_short = limit('SELL',price_now,  get_lot(price_now) + abs(pos['size']))

        if pos['side'] == 'SHORT' and golong == 1:  #SHORTポジションでgolongなら指値買
            market_long = limit('BUY', price_now, get_lot(price_now) + abs(pos['size']))

        if pos['side'] == 'NO POSITION' and golong == 1:  #NOポジションでgolongなら指値買
            market_long = limit('BUY', price_now, get_lot(price_now) + abs(pos['size']))

        if pos['side'] == 'NO POSITION' and goshort == 1:  #NOポジションでgoshortなら指値売
            market_short = limit('SELL',price_now,  get_lot(price_now) + abs(pos['size']))

        logger.info('終値:%.1f' % price_now)  # 最終価格
        logger.info('5分平均足 BEFORE:%.1f' % sma_300['before'])
        logger.info('5分平均足 NOW   :%.1f' % sma_300['now'])
        logger.info('1分平均足 BEFORE:%.1f' % sma['before'])
        logger.info('1分平均足 NOW   :%.1f' % sma['now'])
        logger.info('BEFOREの差分 %d : NOWの差分 %d' % (sma_300['before'] - sma['before'], sma_300['now'] - sma['now']))
        logger.info(' ')
        time.sleep(SLEEP)  #SLEEP分と待って繰り返し

    except Exception as e:
        logger.error("Error!")
        logger.error(e)
        time.sleep(SLEEP)

基本的にはpine scriptのロジックをそのままpybitflyerとpythonに書き換えただけです。売買シグナルが出た際の発注方法は成行か指値かトレールか悩みましたが、今回は指値にしています。

平均足の算出箇所だけ詳細に説明します。

        # CryptowatchのAPIで1分足を取得
        r = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc?periods=60") #直近500本が取得できる
        ohlcv = r.json()['result']['60'] #1:o 2:h 3:l 4:c 5:v
        #1分足のohlcvを分解
        tmp_high = []
        tmp_low = []
        tmp_close = []
        tmp_open = []
        for i in range(len(ohlcv)):
            tmp_open.append(ohlcv[i][1])
            tmp_high.append(ohlcv[i][2])
            tmp_low.append(ohlcv[i][3])
            tmp_close.append(ohlcv[i][4])
        #平均足を計算
        ha_open = []
        ha_close = []
        ha_high = []
        ha_low = []
        for i in range(-period, 0):
            if i == -period:
                ha_open.append(tmp_open[i])
                ha_close.append(tmp_close[i])
                ha_high.append(tmp_high[i])
                ha_low.append(tmp_low[i])
            else:
                ha_open.append((ha_open[-1] + ha_close[-1])/2)
                ha_close.append((tmp_open[i-1] + tmp_close[i-1] + tmp_high[i-1] + tmp_low[i-1])/4)
                ha_high.append(tmp_high[i])
                ha_low.append(tmp_low[i])
        sma_np = np.array(ha_close)
        sma['before'] = np.average(sma_np[-11:-1]) #12個前~1個前の平均
        sma['now'] = np.average(sma_np[-10:]) #11個前~直近の平均

まず、過去の足情報をcryptowatchから取得します。
URLの最後の「periods=60」が1分足(60秒)を指定しています。5分なら300を指定します。
平均足を計算するにあたり、cryptowatchから取得した配列を分解してtmp配列に一旦格納しています。平均足の始値はひとつ前の平均足の値を用いるため、一番最初の始値はローソク足の値を使います。よって、periodが長ければ長いほど直近の平均足が理論値に近づいていきます(今回は300にしました)。
cryptowatchから取得した配列は「0」が最も古く、「-1」が最も新しい足の情報なので、直近からperiod分の値を使って平均足を算出します。よってfor分の範囲は「 for i in range(-period, 0):」になります。

これで、pine scriptと同じ挙動をするプログラムを作成することができました。是非改良して利益がでるプログラムに改造してください。


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