太郎チャートを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
ここから先は
¥ 100
この記事が気に入ったらチップで応援してみませんか?