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

in #bitcoin6 years ago (edited)

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


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







Sort:  

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:

SteemitBoard - Witness Update
SteemFest³ - SteemitBoard support the Travel Reimbursement Fund.

Support SteemitBoard's project! Vote for its witness and get one more award!