bitFlyerのビットコインFXでトラリピやループイフダンを構築(ソースも公開)

ビットコインの激しい値動きのチャートを見ていると、この上下の動きから利益を上げられないかなと考えたことあると思います。

最近の値動きでは、激しく下げてもしばらくすると元の価格に戻ってきていますので、下げたら買って戻るのを待ってから売るという裁量トレードで稼ぐことができました。

インジケーターも使わずに非常にシンプルなトレードですが、レバレッジ15倍を使って証拠金残高15万円→30万円のようなトレードを行うこともできました。

これもビットコインが非常に予測しやすい値動きをしているからですね。

ビットコインが下がったら買って上がったら売るを自動売買でできない?

さて、ここからが本題です。

単純に下がったら買って(ナンピン)、上がったら売るなら簡単にシステム化できそうで、細かい値動きにも追従できるはずと思いつきました。

下がったら買い増して上がったらそれを売るという戦略で思い出したのがトラリピやループイフダンでした。

早速これをbitFlyer FXで運用できるように実装してみました。

実装する前に、bitFlyer FXで気になることがいくつかありました。

      両建てはできないこと。
      決済注文はポジションに対して反対売買を行うことで実行されること。
      複数回の注文で作られたポジションに対して決済注文を行う場合、決済するポジションを指定することができず、古いものから決済されること。

特に3つ目の古いポジションから順番に決済されるというものが大きな懸念点です。

ただ、この時に想定していたのはビットコインは上昇トレンドにある(この時の私の思い込み)ので、買い方向にポジションを立てるようにすれば利益は出るだろうということです。

下がったら買って待っておいて、上がったら売るという裁量トレードをやっていましたので。

とりあえず作ってみようとコーディングしてみて実弾(少額)で試してみることにしました。

ビットコインFXでトラリピ・ループイフダン型のbot初期タイプを開発、運用開始

初期タイプは1日で出来ました。簡単なロジックでしたので。

出来上がったのが夜でデバッグした後に少し運用してみて意図通りに動いてくれたので深夜に少額(0.01 BTC)で運用開始しました。

その後就寝しましたが、今から振り返って考えるとよくそんなことしたなと自分をってやりたい・・・。

この時は絶対儲かると疑ってなかったんですね。

起きてみると、意図した通りに利益が積みあがっていました。

1時間で3,000円ほど利益が出ていましたので、ロットを増やして0.1 BTCで運用すれば1時間に3万円!と皮算用してほくそえんでいました。

利益が出ていたのはたまたま価格が上昇を続けていたからで、買いで入って指定の幅だけ上昇したら利確して次の買いを入れるというロジックにマッチしていたからでした。

朝起きて利益が出ていたので、そのまま運用して様子を見る事にしました。

昼過ぎからチャートは反転して下落傾向に移行していきました。

すると下がるたびに買いを入れていきます。

ナンピンするロジックなのでこれは想定通りです。

評価損は出ていますが、また反転してくると評価益になって売りも入ってくると待ちました。

ゆっくりと上昇を見せるようになりましたが、マイナスが出続けました。

下がった後、上昇に転じてすぐはマイナスになるだろうことは予想していました。

古いポジションから決済されるためです。

古いポジションが消化されるとプラスに転じるハズと運用状況を見守りましたが、マイナスが続いた後にプラスになっても極々わずかです。

最初の利益を食いつぶしてしまいました。

これはちょっとシミュレーションしてみれば分かることですが、古いポジションとして残っているのは値下がりを始めたころのポジションで、これは価格が高いところで買いを入れたものです。

一方、価格の方向が転換した直後は底値から上がっていくことになり、ここで売りで入れて解消されるポジションは一番価格の高いもので、損失が一番大きくなる条件で決済されてしまうということになります。

トレンドの方向が一致したときぐらいしか利益を出せないのでこれはボツにしようと思ったのですが、一つ追加しておきたい機能を実装することにしました。

ビットコインFXのトラリピ・ループイフダン型のbot第2期タイプを開発

追加したい機能とは、保持しているポジションのうち一番古いもの(次に決済されるもの)で利益が出るまで保持するというものです。

V字型に価格が変動する場合は、一番上まで価格が回復してから決済を始めるというものです。

今回公開するソースはこの2期型のものです。

注意点としては、価格が回復するまでポジションを保持することになるので、どれぐらいの価格差ごとにポジションを発生させるのかということと、証拠金額と取引数量を上手く調整する必要があります。

価格差が狭すぎると多数のポジションを抱えることになり、より多くの証拠金が必要になります。

広すぎると機会損失になります。

bitFlyerビットコインFX用のトラリピ・ループイフダン型のbotソースコード

作成したbitFlyer用botですが、現在は運用を停止しています。

その理由は、レバレッジが最大4倍になってしまったからです。

ずっと15倍でトレードしていたのですが、4倍になってしまい驚くほど資金効率が落ちてしまいました。

同じロジックでbitMEXで稼働するbotも作成したので、bitMEXでの運用に移っていくことになると思います。

# -*- coding:utf-8 -*-

from django.core.management.base import BaseCommand
from ...models import Param, Bitflyermanage, Bitflyerorder

import ccxt
from .bitflyer import bitflyerApi

from decimal import *
import logging
import time
import datetime
import sys

symbol = 'FX_BTC_JPY'      # 対象通貨
open_side = 'buy'       # エントリー注文タイプ buy or sell
close_side = 'sell'     # 決済注文タイプ buy or sell
order_type = 'limit'    # 注文種別 limit or market

price_diff = Decimal("2000.0")    # 注文間の価格差
amount = Decimal("0.01")        # 注文サイズ

now = datetime.datetime.now()
logic_started = now.strftime("%Y%m%d%H%M%S")

bitflyer = bitflyerApi.bitflyer()


# BaseCommandを継承して作成
class Command(BaseCommand):

    # python manage.py help count_entryで表示されるメッセージ
    help = 'Display the number of blog articles'

    # コマンドライン引数を指定します。(argparseモジュール https://docs.python.org/2.7/library/argparse.html)
    # 今回はaccount_idという名前で取得する。(引数は最低でも1個, int型)
    def add_arguments(self, parser):
        parser.add_argument('account_id', nargs='+', type=int)

    # コマンドが実行された際に呼ばれるメソッド
    def handle(self, *args, **options):
        if len(options['account_id']) != 1:
            sys.exit()
        account_id = options['account_id'][0]

        # balance = bitmex.fetch_balance()
        manage_id = self.create_bitflyermanage().pk

        base_price = Decimal("0.0")  # 基準価格 この価格を更新していく
        position = Decimal("0.00")
        average_price = Decimal("0.0")

        while True:
            while True:
                try:
                    ticker = bitflyer.fetch_ticker(symbol)
                    break
                except ccxt.BaseError as e:
                    self.logging('error', e)
                    time.sleep(10)

            oldest_profit_order = self.get_oldest_profit_price(manage_id)
            price, base_price, profit_price, entry_price = self.get_price(ticker, base_price)

            if position == 0:
                open_order_id, order_result, position = self.open_order(manage_id,
                                                                        account_id, price, profit_price, position)
                time.sleep(10)
                if order_result == 'success':
                    self.logging('debug', 'First Entry buy Price:%s, Position:%s' % (price, position))
                    self.close_order(manage_id, open_order_id, account_id, profit_price)
                    self.logging('debug', 'Close order to First Entry sell Price:%s' % profit_price)
                    time.sleep(10)

            else:
                if oldest_profit_order is not None:
                    close_price = Decimal(oldest_profit_order.price)
                    close_amount = Decimal(oldest_profit_order.amount)
                    # 現在価格が利確ラインを超えた場合
                    if (open_side == 'buy' and close_price <= price) or \
                            (open_side == 'sell' and close_price >= price):
                        position = self.update_close_ordered(oldest_profit_order.id, close_amount, price, position)
                        time.sleep(10)

                # エントリーラインを超えた場合、次の注文を入れる
                if (open_side == 'buy' and entry_price >= price) or \
                        (open_side == 'sell' and entry_price <= price):
                    open_order_id, order_result, position = self.open_order(manage_id,
                                                                            account_id, price, base_price, position)
                    time.sleep(10)
                    if order_result == 'success':
                        self.close_order(manage_id, open_order_id, account_id, base_price)
                        self.logging('debug', 'Close order to sell Price:%s' % base_price)
                        time.sleep(10)

                base_price = self.check_base_price(base_price, price)
            time.sleep(10)

    def get_price(self, ticker, base_price):
        if open_side == 'buy':
            price = Decimal(ticker['ask'])
        elif open_side == 'sell':
            price = Decimal(ticker['bid'])

        if base_price == 0:
            base_price = price

        if open_side == 'buy':
            profit_price = base_price + price_diff
            entry_price = base_price - price_diff
        elif open_side == 'sell':
            profit_price = base_price - price_diff
            entry_price = base_price + price_diff

        return price, base_price, profit_price, entry_price

    def get_params(self, account_id, logic_id):
        params = Param.objects.filter(
            account_id=account_id, logic_id=logic_id, ended_at__isnull=True).order_by('id').reverse().first()
        return params

    def create_bitflyermanage(self):
        manage = Bitflyermanage(
            status=0,
            count_open_ordered= 0,
            count_close_ordered=0,
            total_profit=0,
        )
        manage.save()
        return Bitflyermanage.objects.latest('id')

    def open_order(self, manage_id, account_id, price, price_profit, position):
        params = {"product_code": "FX_BTC_JPY"}
        try:
            open_result = bitflyer.create_order(
                symbol, order_type, open_side, float(amount), float(price), params)
            order_result = 'success'
            position = position + amount
        except ccxt.BaseError as e:
            order_result = 'error'
            self.logging('error', e)
            open_result = e

        order = Bitflyerorder(
            manage_id=manage_id,
            account_id=account_id,
            product_code=symbol,
            order_type=order_type,
            open_or_close='open',
            order_side=open_side,
            amount=amount,
            price=price,
            price_result=price,
            price_profit=price_profit,
            message=open_result,
            order_result=order_result,
            status=0,
            profit_ordered=0,
            logic_started=logic_started,
        )
        order.save()
        return Bitflyerorder.objects.latest('id').pk, order_result, position

    def close_order(self, manage_id, open_order_id, account_id, price):
        order = Bitflyerorder(
            manage_id=manage_id,
            open_order_id=open_order_id,
            account_id=account_id,
            product_code=symbol,
            order_type=order_type,
            open_or_close='close',
            order_side=close_side,
            amount=amount,
            price=price,
            status=0,
            profit_ordered=0,
            logic_started=logic_started,
        )
        order.save()

    def get_oldest_profit_price(self, manage_id):
        if open_side == 'buy':
            order = Bitflyerorder.objects.filter(
                manage_id=manage_id, order_side=close_side, status=0).order_by('id').first()
        elif open_side == 'sell':
            order = Bitflyerorder.objects.filter(
                manage_id=manage_id, order_side=close_side, status=0).order_by('id').first()
        return order

    def update_close_ordered(self, order_id, close_amount, price, position):
        params = { "product_code": "FX_BTC_JPY" }
        while True:
            try:
                close_result = bitflyer.create_order(
                    symbol, order_type, close_side, float(close_amount), float(price), params)
                order_result = 'success'
                Bitflyerorder.objects.filter(id=order_id).update(
                    price_result=price, status=2, message=close_result, order_result=order_result)
                position = position - close_amount
                return position
            except ccxt.BaseError as e:
                self.logging('error', e)
                time.sleep(10)

    def check_base_price(self, base_price, price):
        if base_price - price_diff >= price:
            base_price = base_price - price_diff
        elif base_price + price_diff <= price:
            base_price = base_price + price_diff
        return base_price

    def logging(self, level, message):
        if level == 'debug':
            logger = logging.getLogger('debug')
            logger.debug(message)
        else:
            logger = logging.getLogger('error')
            logger.error(message)

botの実装について

Django上で動くように実装しています。
Djangoで動かす方法は以下をご覧ください。

以下のようにDBを設定します。

from django.db import models
from datetime import datetime
from django.utils import timezone


# Create your models here.
class Param(models.Model):
    account_id = models.IntegerField()
    logic_id = models.IntegerField(null=True)
    started_at = models.DateTimeField(null=True)
    ended_at = models.DateTimeField(null=True)
    product_code = models.CharField(max_length=50, null=True)
    order_side = models.CharField(max_length=5, null=True)
    price_diff = models.DecimalField(null=True,max_digits=8, decimal_places=2)
    stop_loss = models.DecimalField(null=True,max_digits=8, decimal_places=2)
    size = models.DecimalField(max_digits=8, decimal_places=4, null=True)
    order_num = models.IntegerField(null=True)
    leverage_level = models.IntegerField(null=True)
    price_range = models.IntegerField(null=True)
    min_period = models.IntegerField(null=True)
    stop_period = models.IntegerField(null=True)
    short_hour_period = models.IntegerField(null=True)
    middle_hour_period = models.IntegerField(null=True)
    long_hour_period = models.IntegerField(null=True)
    price_buffer = models.IntegerField(null=True)
    order_expire = models.IntegerField(null=True)
    position_expire = models.IntegerField(null=True)
    min_size = models.DecimalField(null=True,max_digits=8, decimal_places=4)
    perk_period = models.IntegerField(null=True)
    perd_period = models.IntegerField(null=True)
    slowd_period = models.IntegerField(order_side, null=True)
    memo = models.TextField(null=True)
    created_at = models.DateTimeField(default=timezone.now)
    updated_at = models.DateTimeField(auto_now=True)


class Bitflyermanage(models.Model):
    status = models.CharField(max_length=20, null=True)
    count_open_ordered = models.IntegerField(null=True)
    count_close_ordered = models.IntegerField(null=True)
    total_profit = models.DecimalField(max_digits=15, decimal_places=8, null=True)
    created_at = models.DateTimeField(default=timezone.now)
    updated_at = models.DateTimeField(auto_now=True)


class Bitflyerorder(models.Model):
    manage_id = models.IntegerField()
    open_order_id = models.IntegerField(null=True)
    account_id = models.IntegerField()
    product_code = models.CharField(max_length=10, null=True)
    order_type = models.CharField(max_length=20, null=True)
    open_or_close = models.CharField(max_length=6, null=True)
    order_side = models.CharField(max_length=4, null=True)
    amount = models.DecimalField(max_digits=12, decimal_places=8, null=True)
    price = models.DecimalField(max_digits=15, decimal_places=8, null=True)
    price_result = models.DecimalField(max_digits=15, decimal_places=8, null=True)
    price_profit = models.DecimalField(max_digits=15, decimal_places=8, null=True)
    price_loss = models.DecimalField(max_digits=15, decimal_places=8, null=True)
    message = models.TextField(null=True)
    order_result = models.CharField(max_length=10, null=True)
    status = models.CharField(max_length=20, null=True)
    profit_ordered = models.SmallIntegerField(null=True)
    logic_started = models.CharField(max_length=20, null=True)
    created_at = models.DateTimeField(default=timezone.now)
    updated_at = models.DateTimeField(auto_now=True)

bitFlyerに接続するためのAPIキー、シークレットを設定します。

# -*- coding: utf-8 -*-
"""
Created on Thu Mar  9 12:00:27 2017

@author: zaihack
"""

import ccxt

class bitflyerApi:

    def bitflyer():
        bitflyer = ccxt.bitflyer({
            # APIキーをご自分のものに差し替えてください
            'apiKey': '自分のAPIキー',
            'secret': '自分のAPIシークレット',
        })
        return bitflyer

botの使い方

最初にDBに接続してテーブルを作るため、以下を実行します。
manage.pyは、Django上のファイルです。

python manage.py makemigrations trades
python manage.py migrate

上記のコマンドでエラーがなくDBにテーブルが作成されていたらbotを実行します。

nohup python manage.py loop_trade 1 &

nohupと末尾の&はsshのコンソールを閉じた後でもbotが終了しないようにするためのものです。
loop_tradeはbotの名前(loop_trade.py)で、1は引数のaccount_idです。