前回は簡単な自動取引のアルゴリズムをつくって
シミュレーター上でそのアルゴリズムを動かしてみました。
取引の結果は大敗でしたがシミュレーション自体はできました。
ここからアルゴリズムを改良していくのもいいのですが
記事としては自動取引の実現を優先したいと思います。
今回は前回作成したプログラムに手を入れて
実際の取引所での自動取引に対応しやすくします。
実際の取引所での取引への対応とは?
実際の取引所での取引への対応とはどういうことでしょうか?
これはシミュレーションに使った自動取引のアルゴリズムをそのまま使って
実際の取引所で取引ができるようにすることと言えます。
前回作成したプログラムで言えば
自動取引のアルゴリズムはTraderクラスとして実装しました。
TraderクラスがSimulatorクラスとやり取りをして取引をしていました。
図にすると以下になります。
これをTraderクラスはそのままに、やり取りの相手を取引所にして取引ができればいいわけです。
図にすると以下になります。
Apiクラスの追加
Traderクラスをそのままにやり取りの相手をシミュレーターにしたり取引所にしたりできればいいと言ったのですが、そのためには前回のプログラムから少し改修が必要です。
なぜならSimulatorクラスと取引所ではやり取りの口、すなわちインターフェースが違うからです。
例えばSimulatorクラスは get_cur_ticker() メソッドを呼ぶことで現在のビットコインの価格を返すようになっていましたが、取引所は、この連載で使っているGMOコインであればWeb API の GET /public/v1/ticker を呼ぶことで現在のビットコインの価格を返します。
また今のTraderクラスはシミュレーターとのやり取りに特化した作りになってしまっています。
これらに対してApiクラスを追加して対応します。
TraderクラスがSimulatorクラスや取引所とやり取りする際にApiクラスを介するようにして、Traderクラスからはどちらとやり取りする場合も同じメソッド群でApiクラスとやり取りすることでその後ろにあるSimulatorクラスや取引所とやり取りできるようにするのです。
図にすると以下の通りです。
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クラスを作成して、いよいよ自動取引を実現させましょう。