始めに言っておくと、大損失を生むソースコードですので絶対に使用しないように!
また、本ソースコードで使用している取引所の売買を実行するメソッドは、Bot実装で有名なUKI氏のブログより参考にさせていただきました。
https://note.mu/magimagi1223/n/n5fba7501dcfd
まず仕組みから説明する。
強化学習におけるエージェントのアクションはたったの以下2つ。
・Long
・Short
学習モデルはモンテカルロ法で、利確したタイミングで初めてモデル(エージェントの脳みそ)を更新する。
そのため、利確するまではいつまでもモデルの更新はない。
これだけでもわかる通り、とても学習するのに時間がかかる!
流れは以下の通りだ。
1.エージェントが買う or 売る
2.利確されるまで3.をループ
3.以下5つをモデル更新用のメモリに蓄積
「リアルタイムの収益+-」
「ask」
「bid」
「spread」
「 total bid depth ( 売り注文総数) 」
「 total ask depth (買い注文総数) 」
4.利確されたらモデルを更新
以下、ソースコードになる。アドバイスがあればお願いしたい。
※このコードを使用する際に発生した損失について責任を負いません。このコードはサンプル目的のみを対象としています - 実際の取引にこのコードを使用しないでください。
#!/usr/bin/python3
# coding: utf-8
import datetime
import time
import os
import numpy as np
import ccxt
import pybitflyer
from collections import deque
key = 'xxxxxxxxxxxxxxxxxxxxxxx'
secret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxx'
bitflyer = ccxt.bitflyer({
'apiKey': key,
'secret': secret,
})
api = pybitflyer.API(key, secret)
# 取引する通貨、シンボルを設定
COIN = 'BTC'
PAIR = 'BTCJPY28SEP2018'
# ロット(単位はBTC)
LOT = 0.05
# 最小注文数(取引所の仕様に応じて設定)
AMOUNT_MIN = 0.001
# 数量X(この数量よりも下に指値をおく)
AMOUNT_THRU = 0.01
# 実効Ask/BidからDELTA離れた位置に指値をおく
DELTA = 1
# レバレッジ設定
LEVERAGE = 1.0
#------------------------------------------------------------------------------#
#log設定
import logging
logger = logging.getLogger('LoggingTest')
logger.setLevel(10)
fh = logging.FileHandler('log_mm_bf_' + datetime.datetime.now().strftime('%Y%m%d') + '_' + datetime.datetime.now().strftime('%H%M%S') + '.log')
logger.addHandler(fh)
sh = logging.StreamHandler()
logger.addHandler(sh)
formatter = logging.Formatter('%(asctime)s: %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
fh.setFormatter(formatter)
sh.setFormatter(formatter)
#------------------------------------------------------------------------------#
# [1]Q関数を離散化して定義する関数 ------------
# 観測した状態を離散値にデジタル変換する
def bins(clip_min, clip_max, num):
return np.linspace(clip_min, clip_max, num + 1)[1:-1]
# 各値を離散値に変換
def digitize_state(observation):
ask, bid, spread, total_ask_depth, total_bid_depth, balance = observation
digitized = [
np.digitize(ask, bins=bins(0.0, 1.0, num_dizitized)),
np.digitize(bid, bins=bins(0.0, 1.0, num_dizitized)),
np.digitize(spread, bins=bins(0.0, 1.0, num_dizitized)),
np.digitize(total_ask_depth, bins=bins(0.0, 1.0, num_dizitized)),
np.digitize(total_bid_depth, bins=bins(0.0, 1.0, num_dizitized)),
np.digitize(balance, bins=bins(0.0, 1.0, num_dizitized))
]
return sum([x * (num_dizitized**i) for i, x in enumerate(digitized)])
# [2]行動a(t)を求める関数 -------------------------------------
def get_action(next_state, episode):
#徐々に最適行動のみをとる、ε-greedy法
epsilon = 0.5 * (1 / (episode + 1))
if epsilon <= np.random.uniform(0, 1):
next_action = np.argmax(q_table[next_state])
else:
next_action = np.random.choice([0, 1])
return next_action
# [3]Qテーブルを更新する(モンテカルロ法) *Qlearningと異なる* -------------------------------------
def update_Qtable_montecarlo(q_table, memory):
gamma = 0.99
alpha = 0.5
total_reward_t = 0
while (memory.len() > 0):
(state, action, reward) = memory.sample()
total_reward_t = gamma * total_reward_t # 時間割引率をかける
# Q関数を更新
q_table[state, action] = q_table[state, action] + alpha*(reward+total_reward_t-q_table[state, action])
total_reward_t = total_reward_t + reward # ステップtより先でもらえた報酬の合計を更新
return q_table
# [4]1試行の各ステップの行動を保存しておくメモリクラス
class Memory:
def __init__(self, max_size=400):
self.buffer = deque(maxlen=max_size)
def add(self, experience):
self.buffer.append(experience)
def sample(self):
return self.buffer.pop() # 最後尾のメモリを取り出す
def len(self):
return len(self.buffer)
# エージェント パラメータ設定--------------------------------------------------------
# バッファーメモリの大きさ
memory = Memory(max_size=400)
num_dizitized = 10
# 前回のテーブルが存在すれば読み込み(状態を6分割^(5変数)にデジタル変換してQ関数(表)を作成)
if os.path.exists('q_table.csv'):
q_table = np.loadtxt('q_table.csv', dtype=float, delimiter=',')
print("parameter loaded.")
else:
q_table = np.random.uniform(low=0.0, high=1.0, size=(num_dizitized**6, 2))
# JPY残高を参照する関数 -------------------------------------
def get_asset():
while True:
try:
value = bitflyer.fetch_balance()
break
except Exception as e:
logger.info(e)
time.sleep(1)
return value
# JPY証拠金を参照する関数 -------------------------------------
def get_colla():
while True:
try:
value = bitflyer.privateGetGetcollateral()
break
except Exception as e:
logger.info(e)
time.sleep(1)
return value
# JPY証拠金変動履歴を参照する関数 -------------------------------------
def get_collateralhistory():
while True:
try:
value = api.getcollateralhistory(product_code=PAIR, count=1)[0]
break
except Exception as e:
logger.info(e)
time.sleep(1)
return value
# 板情報から実効Ask/Bid(=指値を入れる基準値)を計算する関数 -------------------------------------
def get_effective_tick(size_thru, rate_ask, size_ask, rate_bid, size_bid):
while True:
try:
value = bitflyer.fetchOrderBook(PAIR)
break
except Exception as e:
logger.info(e)
time.sleep(2)
i = 0
s = 0
while s <= size_thru:
if value['bids'][i][0] == rate_bid:
s += value['bids'][i][1] - size_bid
else:
s += value['bids'][i][1]
i += 1
j = 0
t = 0
while t <= size_thru:
if value['asks'][j][0] == rate_ask:
t += value['asks'][j][1] - size_ask
else:
t += value['asks'][j][1]
j += 1
time.sleep(0.5)
return {'bid': value['bids'][i-1][0], 'ask': value['asks'][j-1][0]}
# 自分のポジションを取得する関数 -------------------------------------
def getposition():
while True:
try:
logger.info('ポジション - 確認')
value = bitflyer.private_get_getpositions( params = { "product_code" : PAIR })
break
except Exception as e:
logger.info(e)
time.sleep(2)
time.sleep(0.5)
return value
# 成行注文する関数 -------------------------------------
def market(side, size):
while True:
try:
logger.info('成行注文 - 実行')
value = bitflyer.create_order(PAIR, type = 'market', side = side, amount = size)
break
except Exception as e:
logger.info(e)
time.sleep(2)
value = {'id' : 'null'}
break
time.sleep(0.5)
return value
# 指値注文する関数 -------------------------------------
def limit(side, size, price):
while True:
try:
logger.info('指値注文 - 実行')
value = bitflyer.create_order(PAIR, type = 'limit', side = side, amount = size, price = price)
break
except Exception as e:
logger.info(e)
time.sleep(1.5)
value = {'id' : 'null'}
break
time.sleep(1.5)
return value
# 注文をキャンセルする関数 -------------------------------------
def cancel(id):
try:
logger.info('キャンセル - 実行')
value = bitflyer.cancelOrder(symbol = PAIR, id = id)
except Exception as e:
logger.info(e)
# 指値が約定していた(=キャンセルが通らなかった)場合、
# 注文情報を更新(約定済み)して返す
value = get_status(id)
time.sleep(1.5)
return value
# 指定した注文idのステータスを参照する関数 -------------------------------------
def get_status(id):
err_cnt = 0
if PAIR == 'BTC/JPY':
PRODUCT = 'BTC_JPY'
else:
PRODUCT = PAIR
while True:
try:
value = bitflyer.private_get_getchildorders(params = {'product_code': PRODUCT, 'child_order_acceptance_id': id})[0]
break
except Exception as e:
logger.info('ステータスが受け取れない為61秒待機。')
logger.info(e)
time.sleep(61)
err_cnt += 1
if err_cnt == 5:
err_cnt = 0
return {'id' : 'null'}
# APIで受け取った値を読み換える
if value['child_order_state'] == 'ACTIVE':
status = 'open'
elif value['child_order_state'] == 'COMPLETED':
status = 'closed'
else:
status = value['child_order_state']
# 未約定量を計算する
remaining = float(value['size']) - float(value['executed_size'])
time.sleep(0.1)
return {'id': value['child_order_acceptance_id'], 'status': status, 'filled': value['executed_size'], 'remaining': remaining, 'amount': value['size'], 'price': value['price']}
#------------------------------------------------------------------------------#
# 未約定量が存在することを示すフラグ
remaining_ask_flag = 0
remaining_bid_flag = 0
#------------------------------------------------------------------------------#
logger.info('--------TradeStart--------')
logger.info('BOT TYPE : MarketMaker @ bitFlyer')
logger.info('SYMBOL : {0}'.format(PAIR))
logger.info('LOT : {0} {1}'.format(LOT, COIN))
# 残高取得
TOTAL = float(get_colla()['collateral']) + float(get_colla()['open_position_pnl'])
logger.info('--------------------------')
logger.info('TOTAL : {0}'.format(TOTAL))
# 現在の状態tを取得
tick = get_effective_tick(size_thru=AMOUNT_THRU, rate_ask=0, size_ask=0, rate_bid=0, size_bid=0)
ask = float(tick['ask'])
bid = float(tick['bid'])
spread = (ask - bid) / bid
result = bitflyer.fetch_ticker(symbol=PAIR)
total_ask_depth, total_bid_depth = result['info']['total_ask_depth'], result['info']['total_bid_depth']
observation = ask, bid, spread, total_ask_depth, total_bid_depth, TOTAL
state = digitize_state(observation)
prev_change = get_collateralhistory()['change']
trade_bid = {}
trade_ask = {}
trades = []
action = np.argmax(q_table[state])
before_position = float(get_colla()['open_position_pnl'])
episode = 0
total_episode = 0
reward = 0.0
episode_reward = 0
EARNING = 0
contract = 'false'
long_cnt = 0
short_cnt = 0
# 利確設定
CONTRACT_PRICE = 100
# 損切設定
FAILURE_PRICE = -100
position_flag = 0
#------------------------------- メインループ -------------------------------------#
while True:
#------------------------------- エージェントアクション -------------------------------------#
# 買い
if action == 0:
position_flag = 0
logger.info('------long-----')
trade_bid = market('buy', LOT)
trades.append(trade_bid)
# 売り
if action == 1:
position_flag = 1
logger.info('------short-----')
trade_ask = market('sell', LOT)
trades.append(trade_ask)
#------------------------------- 報酬 -------------------------------------#
logger.info('------経過観察中-----')
while True:
# 実行後の評価損益を記録
after_TOTAL = float(get_colla()['collateral']) + float(get_colla()['open_position_pnl'])
logger.info('--------エージェントの設定値--------')
logger.info('実行回数 : {0}'.format(episode))
logger.info('ACTION : {0}'.format(action))
logger.info('--------リアルタイム残高--------')
logger.info('TOTAL : {0}'.format(after_TOTAL))
# (利確)報酬を記録
change = get_collateralhistory()['change']
logger.info('利確報酬実行前 : {0}'.format(prev_change))
logger.info('利確報酬実行後 : {0}'.format(change))
if change != prev_change:
prev_change = change
reward = change
contract = 'true'
# (含益)報酬を記録
logger.info('含益報酬実行前 : {0}'.format(before_position))
logger.info('含益報酬実行後 : {0}'.format(get_colla()['open_position_pnl']))
if before_position != float(get_colla()['open_position_pnl']):
reward = float(reward) + float(get_colla()['open_position_pnl'])
before_position = float(get_colla()['open_position_pnl'])
# 総合利益パーセント算出
TOTAL_PERCENT = float(after_TOTAL/TOTAL) * 100.0
# 収益
EARNING = after_TOTAL - TOTAL
episode_reward = float(episode_reward) + reward
logger.info('--------評価--------')
logger.info('REWARD : {0}'.format(float(reward)))
logger.info('TOTAL REWARD : {0}'.format(float(episode_reward)))
logger.info('EARNING : {:+f}'.format(float(EARNING)))
logger.info('TOTAL PERCENT : {0} %'.format(TOTAL_PERCENT))
# 利益+2%または損失-2%に到達したら利確
if FAILURE_PRICE >= EARNING or EARNING >= CONTRACT_PRICE:
for trade in trades:
if 'null' != trade['id']:
if position_flag == 0:
trade = market('sell', LOT)
contract = 'true'
if position_flag == 1:
trade = market('buy', LOT)
contract = 'true'
logger.info('------約定完了-----')
# 現在の状態tを取得
tick = get_effective_tick(size_thru=AMOUNT_THRU, rate_ask=0, size_ask=0, rate_bid=0, size_bid=0)
ask = float(tick['ask'])
bid = float(tick['bid'])
spread = (ask - bid) / bid
result = bitflyer.fetch_ticker(symbol=PAIR)
total_ask_depth, total_bid_depth = result['info']['total_ask_depth'], result['info']['total_bid_depth']
observation = ask, bid, spread, total_ask_depth, total_bid_depth, after_TOTAL
state = digitize_state(observation)
# メモリに、現在の状態と行った行動、得た報酬を記録する
memory.add((state, action, reward))
# 約定されたタイミングでテーブル更新
if contract == 'true':
logger.info('約定確定')
q_table = update_Qtable_montecarlo(q_table, memory)
action = np.argmax(q_table[state])
data = np.array(q_table)
np.savetxt("q_table.csv", data, delimiter=',')
contract = 'false'
break
else:
# 次ステップへ行動と状態を更新
next_state = digitize_state(observation) # t+1での観測状態を、離散値に変換
next_action = get_action(next_state, episode) # 次の行動a_{t+1}を求める
action = next_action # a_{t+1}
state = next_state # s_{t+1}
# エピソード記録
episode += 1
total_episode += 1
# 報酬リセット
reward = 0