4th Street Bar Hive-Bar

Hive-Bar Powered by Hive beta-fdb5b5b

Community post

強化学習によるBitcoin FXの攻略 ソースコード公開

始めに言っておくと、大損失を生むソースコードですので絶対に使用しないように!


また、本ソースコードで使用している取引所の売買を実行するメソッドは、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







8 upvotes $0.27

Replies (1)

Review before signing

Posting as . Signing with . Keychain permission: Posting. Hive Keychain will ask you to approve this action next.


  
Technical details

Operation fingerprint: