てけとーぶろぐ。

ソフトウェアの開発と、お絵かきと、雑記と。

Pythonでビットコインの自動取引をシミュレーションする(2)

前回は簡単な自動取引のアルゴリズムをつくって
シミュレーター上でそのアルゴリズムを動かしてみました。
取引の結果は大敗でしたがシミュレーション自体はできました。

ここからアルゴリズムを改良していくのもいいのですが
記事としては自動取引の実現を優先したいと思います。

今回は前回作成したプログラムに手を入れて
実際の取引所での自動取引に対応しやすくします。

実際の取引所での取引への対応とは?

実際の取引所での取引への対応とはどういうことでしょうか?
これはシミュレーションに使った自動取引のアルゴリズムをそのまま使って
実際の取引所で取引ができるようにすることと言えます。

前回作成したプログラムで言えば
自動取引のアルゴリズムはTraderクラスとして実装しました。

TraderクラスがSimulatorクラスとやり取りをして取引をしていました。
図にすると以下になります。

f:id:kurimayoshida:20200717214141p:plain
Simulatorクラスとのやり取り

これをTraderクラスはそのままに、やり取りの相手を取引所にして取引ができればいいわけです。
図にすると以下になります。

f:id:kurimayoshida:20200717214206p:plain
取引所とのやり取り

Apiクラスの追加

Traderクラスをそのままにやり取りの相手をシミュレーターにしたり取引所にしたりできればいいと言ったのですが、そのためには前回のプログラムから少し改修が必要です。
なぜならSimulatorクラスと取引所ではやり取りの口、すなわちインターフェースが違うからです。
例えばSimulatorクラスは get_cur_ticker() メソッドを呼ぶことで現在のビットコインの価格を返すようになっていましたが、取引所は、この連載で使っているGMOコインであればWeb API の GET /public/v1/ticker を呼ぶことで現在のビットコインの価格を返します。
また今のTraderクラスはシミュレーターとのやり取りに特化した作りになってしまっています。

これらに対してApiクラスを追加して対応します。
TraderクラスがSimulatorクラスや取引所とやり取りする際にApiクラスを介するようにして、Traderクラスからはどちらとやり取りする場合も同じメソッド群でApiクラスとやり取りすることでその後ろにあるSimulatorクラスや取引所とやり取りできるようにするのです。
図にすると以下の通りです。

f:id:kurimayoshida:20200717214220p:plain
Apiクラス

SimulatorApiクラス、GmoCoinApiクラス、どちらのクラスもTraderクラスから取引をするためのインターフェースが同じになるよう、共通の基底クラスとなるApiクラスを作ります。
Apiクラスには以下のようなメソッドを用意しています。

  • 余力情報の取得
  • 最新レートの取得
  • 成行の買い注文の発注
  • 成行の売り注文の発注
  • 注文の状態の取得
  • 資産情報の取得
from datetime import datetime
from dataclasses import dataclass
from enum import Enum, auto
from typing import List
import json
import requests
import os
import hmac
import time
from datetime_util import DatetimeUtil
import hashlib
from abc import ABCMeta, abstractmethod

class OrderStatus(Enum):
    WAITING = auto()
    ORDERED = auto()
    MODIFYING = auto()
    CANCELLING = auto()
    CANCELED = auto()
    EXECUTED = auto()
    EXPIRED = auto()

class Symbol(Enum):
    JPY = auto()
    BTC = auto()
    ETH = auto()
    BCH = auto()
    LTC = auto()
    XRP = auto()

@dataclass
class Ticker:
    ask_price: int
    bid_price: int
    high_price: int
    last_price: int
    low_price: int
    symbol: str
    timestamp: datetime
    volume: float

@dataclass
class Asset:
    amount: float
    symbol: Symbol


class Api(metaclass=ABCMeta):
    @abstractmethod
    def get_available_amount(self) -> int:
        pass

    @abstractmethod
    def get_ticker(self, symbol: Symbol) -> Ticker:
        pass

    @abstractmethod
    def place_market_buying_order(self, symbol: Symbol, size: float) -> int:
        pass

    @abstractmethod
    def place_market_selling_order(self, symbol: Symbol, size: float) -> int:
        pass

    @abstractmethod
    def get_order_status(self, order_id: int) -> List[OrderStatus]:
        pass

    @abstractmethod
    def get_assets(self) -> List[Asset]:
        pass

Traderクラスの改修

Traderクラスを改修します。改修のポイントは以下です。

  • 直接SimulatorクラスとではなくApiクラスを介してSimulatorクラスとやりとりするため、コンストラクターでSimulatorクラスの代わりにApiクラスを渡し、それを使います。
  • 前回は資産の量(日本円やビットコインの量)をTraderクラスに持たせて、注文にあわせてその量をTraderクラス上で更新していましたが、実際の資産は取引所にあるということにあわせて、Simulatorクラスや取引所側に資産の量を持たせるようにします。そして、発注をAPIを介してSimulatorクラスや取引所に対して行うようにし、Simulatorクラスや取引所側で注文を処理して資産の量を更新します。
  • 前回は発注後即座に必ず約定する前提でした。実際の取引では必ずしもそうではなく、そして約定は取引所側で決まるので、発注をApiクラスを介してSimulatorクラスや取引所に対して行うことに加えて、Apiクラスを介して約定の確認を行うようにします。それに伴いトレードの状態に「買い注文約定待ち」「売り注文約定待ち」を追加します。

とりあえずTraderクラスで資産の量を持つ必要はなくなったためTraderクラスから資産の量を取り除いています。しかし例えば取引のアルゴリズムが今の資産の量を加味する場合は度々Apiクラスを通じて今の資産の量を取得するのも時間がかかるのでTraderクラスが資産の量をもつこともありえます。その場合であっても本当の資産の量はSimulatorクラスや取引所にあるものであり、Traderクラスが持っているものはその写しということになります。

from enum import Enum, auto
from api import Api, Symbol, OrderStatus
from price_data_manager import PriceDataManager
import datetime


class TradingStatus(Enum):
    # 買い発注待ち
    STANDING_BY_FOR_BUYING = auto()
    # 買い注文約定待ち
    ORDERED_BUYING_ORDER = auto()
    # 売り発注待ち
    STANDING_BY_FOR_SELLING = auto()
    # 売り注文約定待ち
    ORDERED_SELLING_ORDER = auto()


class SellingType(Enum):
    DEADLINE = auto()
    PROFIT_TAKING = auto()
    STOP_LOSS = auto()


class Trader:
    SHORT_MA_PERIOD_SEC = 1 * 60
    LONG_MA_PERIOD_SEC = 10 * 60

    MAX_HOLDING_SEC = 5 * 60 * 60
    SELLING_TH_PRICE_RATE = 1.01
    STOP_LOSS_SELLING_TH_PRICE_RATE = 0.995

    def __init__(self, api: Api) :
        self.state = TradingStatus.STANDING_BY_FOR_BUYING

        self.api = api
        self.order_id = None


        # 取引判断用データ ----

        self.price_data_manager = PriceDataManager()

        # 前回の移動平均
        self.last_long_ma_price = None
        self.last_short_ma_price = None

        self.bought_price = None
        self.bought_datetime = None


    def act(self):
        ticker = self.api.get_ticker(Symbol.BTC)
        self.price_data_manager.add_ticker(ticker)

        short_ma_price = self.price_data_manager.get_moving_average_ask_price(self.SHORT_MA_PERIOD_SEC)
        long_ma_price = self.price_data_manager.get_moving_average_ask_price(self.LONG_MA_PERIOD_SEC)

        if self.state == TradingStatus.STANDING_BY_FOR_BUYING:
            # ゴールデンクロスのチェック
            if (self.last_short_ma_price is not None and 
                self.last_short_ma_price < self.last_long_ma_price and 
                short_ma_price > long_ma_price):

                # 買い
                self.order_id = self.api.place_market_buying_order(Symbol.BTC, 1)
                self.state = TradingStatus.ORDERED_BUYING_ORDER
                self.bought_price = ticker.ask_price
                self.bought_datetime = ticker.timestamp
                print(f'{ticker.timestamp} 買い {ticker.ask_price}')

        elif self.state == TradingStatus.ORDERED_BUYING_ORDER:
            order_status_list = self.api.get_order_status(self.order_id)
            is_executed = False
            for status in order_status_list:
                if status == OrderStatus.EXECUTED:
                    is_executed = True
            if is_executed:
                self.state = TradingStatus.STANDING_BY_FOR_SELLING

        elif self.state == TradingStatus.STANDING_BY_FOR_SELLING:
            # 時間経過、指定の割合上昇、指定の割合下降で売る
            selling_type = None
            if ticker.timestamp >= self.bought_datetime + datetime.timedelta(seconds=self.MAX_HOLDING_SEC):
                selling_type = SellingType.DEADLINE
            if ticker.bid_price >= self.bought_price * self.SELLING_TH_PRICE_RATE:
                selling_type = SellingType.PROFIT_TAKING
            if ticker.bid_price <= self.bought_price * self.STOP_LOSS_SELLING_TH_PRICE_RATE:
                selling_type = SellingType.STOP_LOSS

            if selling_type:
                # 売り
                self.order_id = self.api.place_market_selling_order(Symbol.BTC, 1)
                self.state = TradingStatus.ORDERED_SELLING_ORDER

                if selling_type == SellingType.DEADLINE:
                    action = '売り(期限)'
                elif selling_type == SellingType.PROFIT_TAKING:
                    action = '売り(利確)'
                elif selling_type == SellingType.STOP_LOSS:
                    action = '売り(ロスカット)'
                print(f'{ticker.timestamp} {action} {ticker.bid_price}')

        elif self.state == TradingStatus.ORDERED_SELLING_ORDER:
            order_status_list = self.api.get_order_status(self.order_id)
            is_executed = False
            for status in order_status_list:
                if status == OrderStatus.EXECUTED:
                    is_executed = True
            if is_executed:
                self.state = TradingStatus.STANDING_BY_FOR_BUYING

        self.last_short_ma_price = short_ma_price
        self.last_long_ma_price = long_ma_price

Simulatorクラスの改修

Apiクラスの追加、Traderクラスの改修にあわせてSimulatorクラスも改修します。改修のポイントは以下です。

  • シミュレーター用のApiクラスであるSimulatorApiクラスを持つようにします。
  • 資産の量を持つようにします。
  • 注文を処理するようにします。

前回はTraderクラス上で資産の量を更新するだけで済ませていた注文の処理をSimulatorクラスが仮想の取引所としての役割を担って行うようにします。
place_market_buying_order()メソッドとplace_market_selling_order()メソッドで、それぞれ買い注文と売り注文を受けます。注文を受けたらそれらの注文は即座に約定したこととし、資産の量を更新し、order_status_list_dict として持つ各注文の状態も全量約定とします。
get_order_status()メソッドで注文の状態を問われたらorder_status_list_dictから返します。

from api import Ticker, OrderStatus
import pandas as pd
from typing import List


class Simulator:
    def __init__(self):
        self.cur_ticker = None

        self.yen_amount = 2000000
        self.btc_amount = 0

        self.next_order_id = 0
        self.order_status_list_dict = {}

        from simulator_api import SimulatorApi
        self.simulator_api = SimulatorApi(self)

    def simulate(self):
        from trader import Trader
        self.trader = Trader(self.simulator_api)

        print(f'開始 資産: {self.yen_amount}')

        # CSVファイルを読み込む
        df = pd.read_csv('ticker_20200514.csv')
        df['timestamp'] = pd.to_datetime(df['timestamp'])

        # 一行ずつ処理
        for _, row in df.iterrows():
            self.cur_ticker = Ticker(row['ask'], row['bid'], row['high'], row['last'],
                    row['low'], row['symbol'], row['timestamp'], row['volume'])
            self.trader.act()

        # 全行処理が終わったら結果を表示する
        print(f'終了 資産: {self.yen_amount + self.btc_amount * self.cur_ticker.bid_price}')

    def get_cur_ticker(self) -> Ticker:
        return self.cur_ticker

    def place_market_buying_order(self, symbol: str, size: float) -> int:
        # 簡易的に即約定とする
        order_id = self.next_order_id

        self.yen_amount -= self.cur_ticker.ask_price
        self.btc_amount += 1

        self.order_status_list_dict[order_id] = [OrderStatus.EXECUTED]
        self.next_order_id += 1
        return order_id

    def place_market_selling_order(self, symbol: str, size: float) -> int:
        # 簡易的に即約定とする
        order_id = self.next_order_id

        self.yen_amount += self.cur_ticker.bid_price
        self.btc_amount -= 1

        self.order_status_list_dict[order_id] = [OrderStatus.EXECUTED]
        self.next_order_id += 1
        return order_id

    def get_order_status(self, order_id: int) -> List[OrderStatus]:
        if order_id not in self.order_status_list_dict:
            return []
        return self.order_status_list_dict[order_id]

SimulatorApiクラスの作成

Apiクラスの各メソッドでSimulatorクラスに対して取引を行うSimulatorApiクラスを作成します。

from api import Api, Ticker, OrderStatus, Symbol, Asset
from simulator import Simulator
from typing import List


class SimulatorApi(Api):
    def __init__(self, simulator: Simulator) :
        self.simulator = simulator

    def get_available_amount(self) -> int:
        return self.simulator.yen_amount
    
    def get_ticker(self, symbol) -> Ticker:
        return self.simulator.get_cur_ticker()
    
    def place_market_buying_order(self, symbol: str, size: float) -> int:
        return self.simulator.place_market_buying_order(symbol, size)

    def place_market_selling_order(self, symbol: str, size: float) -> int:
        return self.simulator.place_market_selling_order(symbol, size)

    def get_order_status(self, order_id: int) -> List[OrderStatus]:
        return self.simulator.get_order_status(order_id)

    def get_assets(self) -> List[Asset]:
        assets = []
        assets.append(self.simulator.yen_amount, Symbol.JPY)
        assets.append(self.simulator.btc_amount, Symbol.BTC)
        return assets

シミュレーションの実行

シミュレーションの実行を行う main.py は変更なしです。シミュレーションを実行してみましょう。
Traderクラスがトレードの状態として「買い注文約定待ち」「売り注文約定待ち」という状態を持つようになり、3秒間約定の確認に取られるため若干結果に違いがでましたが特に大きな違いはありません。
相変わらずの大敗です…。

from simulator import Simulator


if __name__ == '__main__':

    simulator = Simulator()
    simulator.simulate()

次回はTraderクラスはそのままに、GMOコインのAPIを介して取引を行うGmoCoinApiクラスを作成して、いよいよ自動取引を実現させましょう。