始めに言っておくと、大損失を生むソースコードですので絶対に使用しないように!
また、本ソースコードで使用している取引所の売買を実行するメソッドは、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
Congratulations @jianty! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :
You got your First payout
Click on the badge to view your Board of Honor.
If you no longer want to receive notifications, reply to this comment with the word
STOP
Do not miss the last post from @steemitboard: