見出し画像

太郎チャートをbotに手を加えないで活用するPythonスクリプト

更新情報

 - 2018/6/8 メリット、デメリット、仕様等を追加
 - 2018/9/17 再起動用シェルスクリプトを追加
 - 2018/9/18 有償化(*実質無料)

はじめに

このノートは、無償ですべての内容をご覧いただけますので、あえて購入する必要はございません。もし内容にご満足いただけたなら、購入いただくか、あるいはサポートしていただけるとやる気が出ます。

対象

- 太郎チャート(BotView)を購入済みの方、これから購入する予定のある方
- 購入したけどbotへの仕込み方がわからなくてお蔵に入れてしまった方

太郎チャート:https://note.mu/tarodesu/n/n8f40589ff735

開発動機

かくいう私も、衝動的に太郎チャートを購入した1人でした。しかし、いろいろなBotを取っ替え引っ替えするのに、いちいちBotViewへの通知APIを呼び出す処理をbotにいれるなんて、とても面倒です。

また、たとえ入れたとしても、約定スリップした結果をきちんと取得してBotViewのAPIを呼ばない限り、正確な損益グラフにはなりません。

更に、コミッション手数料等も加味しなければ、「グラフは右肩上がりなんだけれども……」みたいなことになりかねません。

そんなわけで、結局使わずに時間だけが過ぎていきました。

ただ、折角購入したBotViewを利用できないでいるのはなんだか悔しい、ということで、サポートスクリプトを作成しました。

対応取引所は、BitFlyerとBitMEXの2つになります。

仕様

設定ファイル (botview_updater.json) に記述された取引所(Bitflyer, BitMEXのみ対応)の証拠金情報の変化を検出し、entry, exit 情報を勝手にでっち上げてBotViewにぶち込みます。

XBTUSD, JPYUSDのレートは、最新のレートを取得してUSD換算します。

監視間隔はデフォルト30秒。0.5USD以上の変化でBotViewに反映します。(※どちらも設定によって変更可能)

金額はBitflyerの場合でもUSD換算になります。

メリット・デメリット

太郎チャートを通常通りに使用した場合と、このスクリプトを使った場合のメリット・デメリットを挙げると下記のようになります。

メリット
 * BOTに手を加える必要がない。裁量Onlyの方も利用可能。
 * 実際の証拠金ベースなので、スリッページ等で太郎チャート上の
   表示と現実の損益が乖離しない。手数料なども考慮されたグラフ
   を見ることが可能。
 * トレンドフォロー系BOTは長期に渡ってPositionを保有することが
   多いが、未実現損益も含めての損益が表示可能。

デメリット
 * 1取引所で複数BOTを動かしている場合などで、BOT毎の損益の
   分析には使用できない

 * BotViewの取引履歴は無用の長物と化す。(Long固定、妙なLotで
   の取引で満たされるため)

使い方

まずは、Taroさんの導入方法の説明に従い、BotViewを導入、稼働してください。

次に、ページ最後にあるソースコード2つをそれぞれ保存して、botview_updater.jsonを適宜お使いの取引所に応じた設定を記入、不要部分は削除してください。

例)BitFlyerのみの場合、botview_updater.jsonは下記のようになります。

{
    "bitflyer": {
        "__comment__": "This is sample for bitflyer",
        "ex": "BF",
        "key": "pppdddddfiohfwefewff", << あなたがお使いのAPI key
        "secret": "jopwehhfofwehowe;fhwejw" << あなたがお使いのAPI secret
    }
}

あとは、botview_updater.pyの

URL = 'http://0.0.0.0:8000/'

の部分を、ご自分の環境に合わせ変更してください。

Cloud9をお使いで、同一EC2インスタンス内でBotViewも動作させている方であれば、ここは変更しなくてもそのまま動作すると思います。

最後に、Python3環境で実行してください。もしかしたらモジュール関連でエラーが出るかもしれません。その場合は、

> sudo pip-3.6 install iso8601

※すでにデフォルトがPython 3.xの環境であれば、上記「pip-3.6」は「pip」で構いません。

などとして適宜インストールしてください。

ソースコード

botview_updater.py

#!/usr/bin/python3
#
# botview_updater.py
#
# -*- coding: utf-8 -*-

# Copyright (c) 2018, Baron.Valium <danshaku@mayoi.net>
# All rights reserved.
#
# Released under the BSD License
#
# http://opensource.org/licenses/BSD-3-Clause
#
# Redistribution and use in source and binary forms, with or without modification, 
# are permitted provided that the following conditions are met:
# 
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its contributors
#    may be used to endorse or promote products derived from this software 
#    without specific prior written permission
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# ※日本語訳
#
# BSDライセンス(3条項)
#
# ソースコード形式かバイナリ形式か、変更するかしないかを問わず、
# 以下の条件を満たす場合に限り、再頒布および使用が許可されます。
#
# 1. ソースコードを再頒布する場合、上記の著作権表示、本条件一覧、
#    および下記免責条項を含めること。
# 2. バイナリ形式で再頒布する場合、頒布物に付属のドキュメント等の資料に、
#    上記の著作権表示、本条件一覧、および下記免責条項を含めること。
# 3. 書面による特別の許可なしに、本ソフトウェアから派生した製品の宣伝
#    または販売促進に、著作権者の名前またはコントリビューターの名前を
#    使用してはならない。
#
# 本ソフトウェアは、著作権者およびコントリビューターによって「現状のまま」
# 提供されており、明示黙示を問わず、商業的な使用可能性、および特定の目的に
# 対する適合性に関する暗黙の保証も含め、またそれに限定されない、いかなる
# 保証もありません。著作権者もコントリビューターも、事由のいかんを問わず、
# 損害発生の原因いかんを問わず、かつ責任の根拠が契約であるか厳格責任であるか
#(過失その他の)不法行為であるかを問わず、仮にそのような損害が発生する可能性
# を知らされていたとしても、本ソフトウェアの使用によって発生した(代替品または
# 代用サービスの調達、使用の喪失、データの喪失、利益の喪失、業務の中断も含め、
# またそれに限定されない)直接損害、間接損害、偶発的な損害、特別損害、
# 懲罰的損害、または結果損害について、一切責任を負わないものとします。
#
import requests
import time
import ccxt
import pybitflyer
import json
import sys
import os
import iso8601
from pytz import timezone, utc
from datetime import datetime
from pprint import pprint

VERSION='1.0.0.0'

URL = 'http://0.0.0.0:8000/'    # URL of BotView
POLLING_INTERVAL = 30           # Polling interval for checking balance (sec)
ENTRY = 1000                    # pseudo ENTRY price
USDJPY_RATE = .0                # USD/JPY rate : e.g. USDJPY_RATE = 109.0
                                #    if this value is 0 then get via internet automatically.
XBTUSD_RATE = .0                # USD/XBT rate : e.g. XBTUSD_RATE = 8000.0
                                #    if this value is 0 then get via internet automatically.
HISTORY_MAX = 0                 # Historical data max count (0 : all 0< : historical data count)
USE_HISTORY = False             # Turn off (False) if you don't need using historical data
TIMEZONE = 'Asia/Tokyo'         # Timezone setting
USE_UNREALISED_PNL = True       # post profit including unrealised profit
DIFF_PROFIT = .5                # diff to send (unit : USD)

def _cnv_currency(_balance, _profit, _currency):
    global USDJPY_RATE
    global XBTUSD_RATE
    
    if (_currency == 'JPY'): # BF
        if USDJPY_RATE == 0:
            try:
                r_ = requests.get('https://www.gaitameonline.com/rateaj/getrate') # Get JPYUSD from Gaitame-online
                json_ = r_.json()
                for q_ in json_['quotes']:
                    if (q_['currencyPairCode'] == 'USDJPY'):
                        ave_usdjpy_rate_ = (float(q_['bid']) + float(q_['ask'])) / 2.0
                        break
            except Exception as x_:
                print(x_)
                pass
        else:
            ave_usdjpy_rate_ = USDJPY_RATE

        return _profit * (1.0 / ave_usdjpy_rate_)
        
    if (_currency == 'XBt'): # MEX
        if XBTUSD_RATE == 0:
            now_ = datetime.now().strftime('%s')
            try:
                r_ = requests.get('https://www.bitmex.com/api/udf/history?symbol=XBTUSD&resolution=60&from=' + 
                    str(int(now_)-1) + '&to=' + now_)
                ohlcv_ = r_.json()
                ave_xbtusd_ave_ = (ohlcv_['h'][0] + ohlcv_['l'][0]) / 2.0
            except Exception as x_:
                print(x_)
                pass
        else:
            ave_xbtusd_ave_ = XBTUSD_RATE

        return _profit * ave_xbtusd_ave_ / 100000000.0     
    
    return 0 # unknown currency
    
## Send to BotView
def _send(_name, _id, _date, _balance, _profit, _currency):
    global URL
    #global SIDE
    global ENTRY
    global TIMEZONE
    
    if _profit == 0.0:
        return False
    
    ep_ = URL
    if ep_[-1] != '/':
        ep_ += '/'
    ep_ += '/api/newTrade'
    
    # conv UTC to local
    ds_ = iso8601.parse_date(_date).astimezone(timezone(TIMEZONE)).strftime('%Y-%m-%d %H:%M:%S')
    
    pf_ = _cnv_currency(_balance, _profit, _currency)
    if not (abs(pf_) >= DIFF_PROFIT):
        return False
        
    params = {
        'name': _name,
        'side': 'Long',
        'lot': round(ENTRY+pf_, 2),
        'entry_price': ENTRY,
        'exit_price': round(ENTRY+pf_, 2),
        'closed_at': ds_,
    }
    
    print("sending:", _name, _id, ds_, _balance, _profit, _currency, '=>', round(pf_, 2), 'USD')
    try:
        requests.post(ep_, params=params)
        time.sleep(0.5)
    except Exception as x_:
        print(x_)
        return False
    
    #time.sleep(0.1)
    return True

## send historical data to BotView
def _send_history(_name, _cache, _dict):
    global JPYUSD_RATE
    global XBTUSD_RATE
    
    id_ = _get_history_item(_cache, _dict, 'id')
    date_ = _get_history_item(_cache, _dict, 'date')
    balance_ = _get_history_item(_cache, _dict, 'balance')
    profit_ = _get_history_item(_cache, _dict, 'profit')
    currency_ = _get_history_item(_cache, _dict, 'currency')

    if _cache['ex'] == 'BF':
        # convert to UTC
        date_ = iso8601.parse_date(date_).astimezone(timezone('UTC')).isoformat()
 
    return _send(_name, id_, date_, balance_, profit_, currency_)

# getter for historical data from dict
def _get_history_item(_cache, _dict, _key):
    hist_keys = {
                    'BF' :
                        {
                            'id' : 'id',
                            'date' : 'date',
                            'balance' : 'amount',
                            'profit' : 'change',
                            'currency' : 'currency_code'
                        },
                    'MEX' :
                        {
                            'id' : 'transactID',
                            'date' : 'transactTime',
                            'balance' : 'walletBalance',
                            'profit' : 'amount',
                            'currency' : 'currency'
                        }
                }

    return _dict[hist_keys[_cache['ex']][_key]]

## get historical data from exchange
def _get_history(_name, _ex, _cache):
    global HISTORY_MAX
    
    print('updating history :', _name)

    res_ = None
    last_id_ = ''
    last_balance_ = 0
    total_count_ = 0
    try:
        c_ = 10
        if (_cache['last_id'] == ''):
            # get all history
            c_ = 100

        if (_cache['ex'] == 'BF'):
            res_ = _ex.getcollateralhistory(count=c_)
            last_id_ = str(_get_history_item(_cache, res_[0], 'id'))
            last_balance_ = _get_history_item(_cache, res_[0], 'balance')
        else:
            res_ = _ex.private_get_user_wallethistory() #{ 'count' : c_ })
            for dict_ in res_:
                if (_get_history_item(_cache, dict_, 'id') == '00000000-0000-0000-0000-000000000000'):
                    continue
                last_id_ = _get_history_item(_cache, dict_, 'id')
                last_balance_ = _get_history_item(_cache, dict_, 'balance')
                break
        
            
        while True:
            id_ = ''
            for dict_ in res_:
                id_ = str(_get_history_item(_cache, dict_, 'id'))
                if (_cache['ex'] == 'MEX' and id_ == '00000000-0000-0000-0000-000000000000'):
                    total_count_ += 1
                    continue
                
                if (id_ == _cache['last_id']):
                    print('got history data', _name, id_)
                    return (last_id_, last_balance_)
                
                _send_history(_name, _cache, dict_)
                
                total_count_ += 1
                if (HISTORY_MAX != 0 and total_count_ >= HISTORY_MAX):
                    return (last_id_, last_balance_)

            if len(res_) < c_ or id_ == '1':
                break;

            #time.sleep(1)
            if (_cache['ex'] == 'BF'):
                id_ = _get_history_item(_cache, res_[-1], 'id')
                if (id_ == 1):
                    return (last_id_, last_balance_)
                for i_ in range(0,100):
                    res_ = _ex.getcollateralhistory(count=c_, before=id_)
                    if not ('Message' in res_):
                        break;
                    print('getcollateralhistory api error and retry :', i_)
                    time.sleep(10)
                    
        return (last_id_, last_balance_)
        
    except Exception as x_:
        print(x_)
        print(res_)
        return ('', -1)
    return ('', -1)

# get balance from exchange
def _get_balance(_name, _ex, _cache):
    global JPYUSD_RATE

    print("checking balance data :", _name)
    
    res_ = {}
    last_balance_ = _cache['last_balance']
    if (_cache['ex'] == 'BF'):
        res_ = _ex.getcollateral()
        if (last_balance_ < 0):   # may be first time call
            return res_['collateral']

        total_ = res_['collateral']
        if (USE_UNREALISED_PNL):
            total_ += res_['open_position_pnl']

        pf_ = round(total_ - last_balance_, 2)
        if (pf_ != 0.0):
            print("balance data was changed :", _name, 'profit =', pf_, 'JPY')
            if _send(_name, '0', datetime.now(timezone('UTC')).isoformat(), total_, pf_, 'JPY'):
                last_balance_ =  total_
    else: # MEX
        res_ = _ex.fetch_balance()
        if (last_balance_ < 0):   # may be first time call
            return res_['info'][0]['walletBalance']
        
        total_ = res_['info'][0]['walletBalance']
        if (USE_UNREALISED_PNL):
            total_ += res_['info'][0]['unrealisedPnl']
        
        pf_ = total_ - last_balance_
        if (pf_ != 0.0):
            dict_ = res_['info'][0]
            print("balance data was changed :", _name, 'profit =', pf_, 'XBt')
            if _send(_name, '0', dict_['timestamp'], total_, pf_, res_['info'][0]['currency']):
                last_balance_ =  total_
    
    print ('last_balance :', _name, "=", last_balance_)
    
    return last_balance_

# create exchange depends on setting
def make_exchange(_name, _setting):
    ex_ = None
    if not (_name in _setting.keys()):
        print('bot name ['+_name+'] was not found in setting file')
        return None
         
    if (_setting[_name]['ex'] == 'BF'):
        ex_ = pybitflyer.API(
           api_key    = _setting[_name]['key'],
           api_secret = _setting[_name]['secret']
        )            
    elif (_setting[_name]['ex'] == 'MEX'): # 'MEX'
        ex_ = ccxt.bitmex({
            'apiKey': _setting[_name]['key'],
            'secret': _setting[_name]['secret'],
        })
        if ('test' in _setting[_name].keys() and _setting[_name]['test']):
            ex_.urls['api'] = ex_.urls['test']

    else:
        print('unsupported exchange name ['+_setting[_name]['ex']+'] in setting file')
        return None
    
    return ex_

# save cache to store
def _save_cache(_cache):
    cahce_path_ = './botview_updater_cache.json'
    with open(cahce_path_,'w') as f:
        f.write(json.dumps(_cache))

# load cache from store
def _load_cache():
    cache_ = {}
    cahce_path_ = './botview_updater_cache.json'
    if os.path.exists(cahce_path_):
        cache_ = json.load(open(cahce_path_, 'r'))
    return cache_
    
# update procedure
def update():
    global USE_HISTORY
    global URL
    
    print('updater start')

    setting_path_ = './botview_updater.json'
    setting_ = {}
    if os.path.exists(setting_path_):
        setting_ = json.load(open(setting_path_, 'r'))
    else:
        print('file', setting_path_, 'was not found')
        return
    
    # load cache if existing 
    cache_ = _load_cache()
    
    # del cache removed bot
    for name_ in cache_.keys():
        if not (name_ in setting_):
            # del removed bot
            del cache_[name_]
            _save_cache(cache_)
            continue

    # add cache new bot
    for name_ in setting_.keys():
        if not (name_ in cache_):
            # add new bot
            cache_[name_] = {'ex': setting_[name_]['ex'], "last_id": '', "last_balance": -1.0}
            _save_cache(cache_)
        
    try:

        ex_ = {}
        if USE_HISTORY:
            # send all history data
            for name_ in cache_.keys():
                ex_ = make_exchange(name_, setting_)
                (cache_[name_]['last_id'], cache_[name_]['last_balance']) = _get_history(name_, ex_, cache_[name_])
                _save_cache(cache_)
                time.sleep(5)

        # watch balance and update BotView
        while True:
            for name_ in cache_.keys():
                ex_ = make_exchange(name_, setting_)
                cache_[name_]['last_balance'] = _get_balance(name_, ex_, cache_[name_])
                _save_cache(cache_)
                time.sleep(5)
                
            time.sleep(POLLING_INTERVAL)

    except Exception as x_:
        print(x_)
        exit()

if __name__ == '__main__':
    update()

botview_updater.json

{
    "bitflyer": {
        "__comment__": "This is sample for bitflyer",
        "ex": "BF",
        "key": "<your api key>",
        "secret": "<your api_secret>"
    },
    "mex": {
        "__comment__": "This is sample for bitmex",
        "ex": "MEX",
        "key": "<your api key>",
        "secret": "<your api_secret>"
    },
    "mex_2": {
        "__comment__": "This is sample for bitmex second account",
        "ex": "MEX",
        "key": "<your api key>",
        "secret": "<your api_secret>"
    },
    "mex_testnet": {
        "__comment__": "This is sample for bitmex TestNet",
        "ex": "MEX",
        "__comment__": "if you want to use the BitMEX TestNet, the bot setting must include test:ture param like below.",
        "test": true,
        "key": "<your api key>",
        "secret": "<your api_secret>",
    }
}

再起動用Shellスクリプト

たまにサーバダウンや通信のエラーなどによる終了が気になる場合には、下記のようなShell Scriptを.pyと同じディレクトリに作成、保存し、そちらから起動してエラー終了時に再起動するようにしておくと良いと思います。

sleep 60は「60秒」なので、適宜変更してお使いください。

botview_updater.sh

for i in {0..99999}
do
   ./botview_updater.py; sleep 60;
done

ここから先は

0字

¥ 100

この記事が気に入ったらサポートをしてみませんか?