【AtCoder】ABC172-Dで学ぶOEISの使い方
競プロerのみなさんも,そうでないみなさんもこんにちは.
この記事ではAtCoderBeginnerContest 172で出題された D - Sum of Divisors を例にして,OEISの競プロでの活用法を紹介していきます.
OEISとは
OEISとはオンライン整数列大辞典というもので,このページが詳しいですので詳しいことを知りたい方はそちらを御覧ください.
簡単に説明すると,数列を検索することができて,先人たちが発見した公式やそれに関するメモを見ることができるサイトです.
実際に使ってみる
今回扱っていく問題はこちらです.
パット見た感じでは,1からNまでいい感じに素因数分解して計算したらよさそう,と思いますが間に合いません.
なので工夫しなくてはいけないのですが,OEISを使うことでこの問題は解決できます.
まずは検索する数列を用意するために,プログラムを書くなど愚直に計算するなどして第8,9項あたりまで求めます.
実際に計算したものがこちらです.(はい!?)
というわけでOEISに実際にアクセスして検索していきます.
ところで,これは一般的に言えることなのですが,数列を検索する際は第3項めあたりから5,6項を入れて検索するといいです.Hintsにも書いてあります.*1
なので,求めた数列を第3項からカンマ区切りで入力して検索してみると,数列が出てきてくれます.
数列のタイトルも,「Sum of k*d(k) over k=1,2,...,n, where d(k) is the number of divisors of k.」と,問題が問うていることと一緒です.この情報を引き当てたのはとてもデカイです.
そしてありがたいことに,公式まで掲載されています.
今回の場合では,上から三番目の公式を使うことで整数の範囲かつ計算量もの範囲で簡単に解くことができそうです.
ここまでくれば,書いてあるとおりに実装するだけで問題はとけたようなものです.
OEISのさらなる使い方?
今回扱った問題は,そのまま数列を検索することでOEISから出てきますが,強い方の記事によると,二次元を一次元に落としたり,全体を2で割るなどしないと出てこないような数列もあるようです.
扱ったサイト
OEIS: https://oeis.org/
扱った問題: https://atcoder.jp/contests/abc172/tasks/abc172_d
ABCのD問題のような比較的難しく思える問題でも,OEISを使って検索することで答えが出ることを学べました.
【JOI】情報オリンピック本選C問題解説 C - 最古の遺跡
こんにちは.
今日は2007年に行われた情報オリンピックの本選で出題された,C問題の最古の遺跡を解いたので解説していきます.
問題文
https://www.ioi-jp.org/joi/2006/2007-ho-prob_and_sol/2007-ho.pdf#page=5
オンラインジャッジ(AtCoder) atcoder.jp
問題概要
かつての集落にあった神殿の記述がある. 神殿は上から見ると正方形であり,四隅には柱があった. 考古学者たちは遺跡から見つかった柱の中で正方形になっているもののうち,面積が最大のものが神殿に違いないと考えた.
柱の位置が座標平面上の点として個与えられるので,4本の柱でできる正方形のうち面積が最大のものの面積を出力せよ.
制約
考察
4つの点が与えられたら,正方形を作るかどうかの判定,正方形の面積の算出は でできる.
しかし,n個の柱から4つを選ぶ方法は,通りもあり,およそ通りであるため,全探索していては間に合わない.
3つの点が与えられた場合,2つの点が与えられた場合を考えてみる.
3つの点が与えられた場合は,正方形を作るために必要な一点があるべき位置がわかるが,でもおよそ通りあるために間に合わない.
2つ点が与えられた場合,正方形を作るために必要な2点があるべき位置がわかり, 2点の組み合わせを全探索した場合はおよそであるため時間制限に間に合う.
2つの点が与えられた場合の残りの2点の位置候補はO(1)で求められる → で全探索できる!!
画像のように,2つの点A,Bが与えられた場合にC,Dが存在する場合や,E,Fが存在する場合には正方形を作ることができます.
, , , とすると,E,Fの座標を , , , と表すことができます.
実装
すべての点を受け取り,点が存在するかを配列でbool型にして記録しました.
C++コード例
#include <iostream> #include <vector> #include <map> using namespace std; #define _GLIBCXX_DEBUG #define rep(i, n) for (int i = 0; i < (int)(n); i++) #define ll long long #define all(x) (x).begin(),(x).end() template<class T> inline bool chmax(T& a, T b) {if (a < b) {a = b;return true;}return false;} template<class T>bool chmin(T &a, const T &b) { if (b<a) { a=b; return 1; } return 0; } vector<vector<bool>>is_there(5001, vector<bool>(5001)); bool find(pair<int,int> p){ if (p.first > 5000 || p.second > 5000 or p.first < 0 or p.second < 0)return false; else return is_there[p.first][p.second]; } int main() { int N; cin >> N; vector<pair<int,int> >xys(N); for (int i=0; i<N; i++){ int xi, yi; cin >> xi >> yi; pair<int,int>p; p.first = xi; p.second = yi; is_there[p.first][p.second] = 1; xys[i] = p; } ll ans = 0; for (int i=0; i<N-1; i++){ for (int j=i+1; j<N; j++){ ll ax,ay,bx,by,dx,dy; ax = xys[i].first; ay = xys[i].second; bx = xys[j].first; by = xys[j].second; dx = bx-ax; dy = by-ay; pair<int,int> C, D, E, F; C.first = dy+ax; C.second = ax-dx; D.first = ax+dx+dy; D.second=ay+dy-dx; E.first=ax-dy; E.second=ay+dx; F.first=ax+dx-dy; F.second = ay+dx+dy; if ((find(C) and find(D)) or (find(E) and find(F))){ chmax(ans, dx*dx + dy*dy); } } } cout << ans << endl; }
まとめ
単純な全探索だと間に合わなくても,正方形という性質を利用することで探索する範囲を狭めることができる問題でした. JOI本選にしては簡単な問題に分類されるのではないでしょうか.
JOIなどではDPなどがよく出ますが,こういった工夫をして計算量を落とす問題を解けるようにすることで,力がつくのかもしれないですね.
BitMEXのデータでドルバーを作りたい【ファイナンス機械学習】
みなさん,機械学習していますか?
僕はしていません.まだデータ前処理の段階だからです.
今日はBotterの中でも有名なUKIさんが推し,界隈のBotterは皆イナゴ買いしたと言われる「ファイナンス機械学習」についての記事です.
ファイナンス機械学習では金融時系列データをどのようにして学習データとするかの解説のなかで,よく知られている一定時間ごとのOHLCをとったタイムバーではなく, 一定量取引されたら新しいバーを作る,出来高バーやドルバーを推奨していました.
今日の記事では,BitMEXの価格データから,ドルバーを作るまでを解説していきます.
1から実装していては大変なため,「mlfinlab」というオープンソースパッケージを使っていきます.
ドルバーを作りたいモチベーション
UKIさんの記事や,ファイナンス機械学習にも書いてあるように,ドルバーを使うことで標準バーよりも統計的性質が改善します. UKIさんの記事によると,仮想通貨市場においても同じことが言えるようです.
参照: https://note.com/uki_profit/n/ne218672309cf#rsYqG
mlfinlabのインストール
mlfinlabは,ファイナンス機械学習の中での様々な実装に基づくパッケージであり,このパッケージを使うことによって簡単に本書の中での実装を再現することができます.
Windowsなどを使っている方にはAnaconda環境での使用を推奨するとGithubにあったので,今回はAnaconda上でインストールしていきます.
mlfinlabのインストールはpipを使って簡単に行うことができます.: pip install mlfinlab
こちらのインストールに失敗する方は,mlfinlabのdocumentationのInstallationの中にある,"Installation for Developers"を試してみてください.
https://mlfinlab.readthedocs.io/en/latest/getting_started/installation.html
ドルバーを作成してみる
今回の記事では以前私のブログで紹介した,「BitMEXの秒足を約定履歴から30秒で自動生成するプログラム✨」を一部変更して作成していきます.
実際に変更したプログラムがこちらです. こちらのプログラムの実行にはpandasのインストールが必要であり, BitMEXからの約定データをダウンロードすることに留意してください.
# 💩💩💩💩 uncoding=utf-8 💩💩💩💩 import gzip import pandas as pd from mlfinlab.data_structures import standard_data_structures from urllib import request # 参考: https://qiita.com/yuukiclass/items/88e9ac6c5a3b5ab56cc4 def main(): date, symbol, threshold= input('yyyymmdd symbol threshold\n').split() date, threshold = int(date), int(threshold) baseurl = 'https://s3-eu-west-1.amazonaws.com/public.bitmex.com/data/trade/' print('Downloading...') filepath = '{}.csv.gz'.format(date) request.urlretrieve(baseurl + '{}.csv.gz'.format(date), filepath) print('Making candles...') df = unzip(filepath) df_dollar = makeCandles(df, symbol) print('Done!') file_title = 'bitmex-{}.csv'.format(date) display(df_dollar) df_dollar.to_csv(file_title) dollar = standard_data_structures.get_dollar_bars(df_dollar, threshold=threshold,batch_size=1000000, verbose=True, to_csv=True, output_path=file_title) return def makeCandles(df, symbol): # 参考: https://note.com/nagi7692/n/ne674d117d1b6?magazine_key=m0b2a506bf904 df = df.query('symbol == "{}"'.format(symbol)) df.drop(["symbol", "side", 'tickDirection', 'trdMatchID', 'grossValue', 'homeNotional', 'foreignNotional'], axis=1, inplace=True) df = df.sort_index() df['timestamp'] = pd.to_datetime(df['timestamp'], format="%Y-%m-%dD%H:%M:%S.%f") df = df.rename(columns={'timestamp': 'date_time', "size": "volume"}) df = df[["date_time", "price", "volume"]] return df def unzip(filepath): with gzip.open(filepath, 'rt') as f: df = pd.read_csv(f) return df if __name__ == "__main__": main()
こちらのプログラムも,実行すると標準入力での入力が求められるので,入力します. e.g)20200401 XBTUSD 4000000
すると,同じディレクトリにdollar-bitmex-2020XXXX.csvというファイルにドルバーのデータが生成されます.
おわりに
このプログラムを使用してできることはファイナンス機械学習の全容の1割にも満たず,実際に機械学習モデルを動かすこともできません. 次の記事では,実際にラベリングなどをして機械学習してみたいと思っています.
参考にした記事
mlfinlabのDocumentation: Data Structures — mlfinlab 0.12.3 documentation
Bybitの板情報をPythonで扱いたい
みなさんこんにちは.
みなさんはBitMEXクローンの仮想通貨取引所は数多あることをご存知だと思います.
今回はあのMAXも使っているBybitで板情報をwebsocketからとって扱おうとして苦労したことなどについて書いていきます
Bybit公式のWebsocketコネクタを使ってみる
BybitはBitMEXと同じように,公式でPythonコネクタが用意されています.
試しにOrderBook情報を扱おうとこんなコードを書いてみます.
bybitws = BybitWebsocket(wsURL="wss://stream-testnet.bybit.com/realtime", api_key=None, api_secret=None, symbol='BTCUSD') bybitws.subscribe_orderBookL2() while True: sleep(5) print(bybitws.get_data('orderBookL2_25.BTCUSD'))
すると返ってくるのは...
'delete': [{'price': '6774.00', 'symbol': 'BTCUSD', 'id': 67740000, 'side': 'Buy'}], 'update': [], 'insert': [{'price': '6773.50', 'symbol': 'BTCUSD', 'id': 67735000, 'side': 'Buy', 'size': 1633}], 'transactTimeE6': 0} {'delete': [], 'update': [{'price': '6772.00', 'symbol': 'BTCUSD', 'id': 67720000, 'side': 'Buy', 'size': 35784}], 'insert': [], 'transactTimeE6': 0} {'delete': [{'price': '6776.50', 'symbol': 'BTCUSD', 'id': 67765000, 'side': 'Buy'}, {'price': '6776.00', 'symbol': 'BTCUSD', 'id': 67760000, 'side': 'Buy'}, {'price': '6777.00', 'symbol': 'BTCUSD', 'id': 67770000, 'side': 'Buy'}], 'update': [{'price': '6787.00', 'symbol': 'BTCUSD', 'id': 67870000, 'side': 'Sell', 'size': 17609}, {'price': '6786.50', 'symbol': 'BTCUSD', 'id': 67865000, 'side': 'Sell', 'size': 21198}, {'price': '6785.00', 'symbol': 'BTCUSD', 'id': 67850000, 'side': 'Sell', 'size': 23983}, {'price': '6775.00', 'symbol': 'BTCUSD', 'id': 67750000, 'side': 'Buy', 'size': 2266}], 'insert': [{'price': '6762.00', 'symbol': 'BTCUSD', 'id': 67620000, 'side': 'Buy', 'size': 8432}, {'price': '6775.50', 'symbol': 'BTCUSD', 'id': 67755000, 'side': 'Buy', 'size': 23871}, {'price': '6777.50', 'symbol': 'BTCUSD', 'id': 67775000, 'side': 'Buy', 'size': 45646}], 'transactTimeE6': 0} {'delete': [{'price': '6798.50', 'symbol': 'BTCUSD', 'id': 67985000, 'side': 'Sell'}, {'price': '6765.50', 'symbol': 'BTCUSD', 'id': 67655000, 'side': 'Buy'}, {'price': '6765.00', 'symbol': 'BTCUSD', 'id': 67650000, 'side': 'Buy'}], 'update': [{'price': '6778.00', 'symbol': 'BTCUSD', 'id': 67780000, 'side': 'Buy', 'size': 22444}, {'price': '6778.50', 'symbol': 'BTCUSD', 'id': 67785000, 'side': 'Buy', 'size': 37785}, {'price': '6779.00', 'symbol': 'BTCUSD', 'id': 67790000, 'side': 'Buy', 'size': 45131}, {'price': '6786.50', 'symbol': 'BTCUSD', 'id': 67865000, 'side': 'Sell', 'size': 27567}], 'insert': [{'price': '6780.50', 'symbol': 'BTCUSD', 'id': 67805000, 'side': 'Buy', 'size': 40484}, {'price': '6780.00', 'symbol': 'BTCUSD', 'id': 67800000, 'side': 'Buy', 'size': 43470}, {'price': '6787.50', 'symbol': 'BTCUSD', 'id': 67875000, 'side': 'Sell', 'size': 17584}], 'transactTimeE6': 0}
...
...
...
...
...
....
...
そう. 受け取った情報をそのまま流してくるのである!
これを扱いたいBotterは,削除やアップデートの各クエリに対してのプログラムを書かなくてはいけない. BitMEXとは大違いである.
仕方ないので自分で書く
なので,自分の記事 を参考に,板情報をpandasのDataFrame型で返す関数を追加したBybitWebsocketクラスを書いた.
import sys import websocket import threading import traceback import pandas as pd from time import sleep import json import logging import urllib import math import time import hmac from logging import Formatter, getLogger, StreamHandler, DEBUG, INFO, WARNING, basicConfig # This is a simple adapters for connecting to Bybit's websocket API. # You could use methods whose names begin with “subscribe”, and get result by "get_data" method. # All the provided websocket APIs are implemented, includes public and private topic. # Private methods are only available after authorization, but public methods do not. # If you have any questions during use, please contact us vim the e-mail "support@bybit.com". class BybitWebsocket: #User can ues MAX_DATA_CAPACITY to control memory usage. MAX_DATA_CAPACITY = 200 PRIVATE_TOPIC = ['position', 'execution', 'order'] def __init__(self, wsURL, symbol, api_key, api_secret): self.df = pd.DataFrame() self.formatter = Formatter('%(asctime)-15s - %(levelname)-8s - %(message)s') self.logger = getLogger(__name__) self.handler = StreamHandler(sys.stdout) self.handler.setLevel(DEBUG) self.handler.setFormatter(self.formatter) self.logger.setLevel(DEBUG) self.logger.addHandler(self.handler) self.logger.debug("Initializing WebSocket.") self.symbol = symbol if api_key is not None and api_secret is None: raise ValueError('api_secret is required if api_key is provided') if api_key is None and api_secret is not None: raise ValueError('api_key is required if api_secret is provided') self.api_key = api_key self.api_secret = api_secret self.data = {} self.exited = False self.auth = False # We can subscribe right in the connection querystring, so let's build that. # Subscribe to all pertinent endpoints self.logger.info("Connecting to %s" % wsURL) self.__connect(wsURL) def exit(self): '''Call this to exit - will close websocket.''' self.exited = True self.ws.close() def __connect(self, wsURL): '''Connect to the websocket in a thread.''' self.logger.debug("Starting thread") self.ws = websocket.WebSocketApp(wsURL, on_message=self.__on_message, on_close=self.__on_close, on_open=self.__on_open, on_error=self.__on_error, keep_running=True) self.wst = threading.Thread(target=lambda: self.ws.run_forever()) self.wst.daemon = True self.wst.start() self.logger.debug("Started thread") # Wait for connect before continuing retry_times = 5 while not self.ws.sock or not self.ws.sock.connected and retry_times: sleep(1) retry_times -= 1 if retry_times == 0 and not self.ws.sock.connected: self.logger.error("Couldn't connect to WebSocket! Exiting.") self.exit() raise websocket.WebSocketTimeoutException('Error!Couldn not connect to WebSocket!.') if self.api_key and self.api_secret: self.__do_auth() def generate_signature(self, expires): """Generate a request signature.""" _val = 'GET/realtime' + expires return str(hmac.new(bytes(self.api_secret, "utf-8"), bytes(_val, "utf-8"), digestmod="sha256").hexdigest()) def __do_auth(self): expires = str(int(round(time.time())+1))+"000" signature = self.generate_signature(expires) auth = {} auth["op"] = "auth" auth["args"] = [self.api_key, expires, signature] args = json.dumps(auth) self.ws.send(args) def __on_message(self, message): '''Handler for parsing WS messages.''' message = json.loads(message) if 'success' in message and message["success"]: if 'request' in message and message["request"]["op"] == 'auth': self.auth = True self.logger.info("Authentication success.") if 'ret_msg' in message and message["ret_msg"] == 'pong': self.data["pong"].append("PING success") if 'topic' in message: self.data[message["topic"]].append(message["data"]) if message['topic'] == 'orderBookL2_25.{}'.format(self.symbol): typ = message['type'] data = message['data'] if typ == 'snapshot': self.df = pd.io.json.json_normalize(message['data']) elif typ == 'delta': # self.logger.debug(data) for key, value in data.items(): if key == 'transactTimeE6': continue elif key == 'update': df_data = pd.io.json.json_normalize(value) indicies, Is = self.search_index(self.df, df_data) for i in range(len(indicies)): index = indicies[i] id = self.df.iloc[index]['id'] price = self.df.loc[index, 'price'] self.df.loc[index] = df_data.iloc[Is[i]] self.df.loc[index, 'price'] = price elif key == 'delete': df_data = pd.io.json.json_normalize(value) indicies, Is = self.search_index(self.df, df_data) self.df.drop(index=self.df.index[indicies], inplace=True) self.df = self.df.sort_values('price', ascending=False) self.df.reset_index(inplace=True, drop=True) elif key == "insert": df_data = pd.io.json.json_normalize(value) self.df = pd.concat([self.df, df_data], sort=True) self.df = self.df.sort_values('price', ascending=False) self.df.reset_index(inplace=True, drop=True) if len(self.data[message["topic"]]) > BybitWebsocket.MAX_DATA_CAPACITY: self.data[message["topic"]] = self.data[message["topic"]][BybitWebsocket.MAX_DATA_CAPACITY//2:] def __on_error(self, error): '''Called on fatal websocket errors. We exit on these.''' if not self.exited: self.logger.error("Error : %s" % error) raise websocket.WebSocketException(error) def __on_open(self): '''Called when the WS opens.''' self.logger.debug("Websocket Opened.") def __on_close(self): '''Called on websocket close.''' self.logger.info('Websocket Closed') def ping(self): self.ws.send('{"op":"ping"}') if 'pong' not in self.data: self.data['pong'] = [] def subscribe_kline(self, interval:str): param = {} param['op'] = 'subscribe' param['args'] = ['kline.' + self.symbol + '.' + interval] self.ws.send(json.dumps(param)) if 'kline.' + self.symbol + '.' + interval not in self.data: self.data['kline.' + self.symbol + '.' + interval] = [] def subscribe_trade(self): self.ws.send('{"op":"subscribe","args":["trade"]}') if "trade.BTCUSD" not in self.data: self.data["trade.BTCUSD"] = [] self.data["trade.ETHUSD"] = [] self.data["trade.EOSUSD"] = [] self.data["trade.XRPUSD"] = [] def subscribe_insurance(self): self.ws.send('{"op":"subscribe","args":["insurance"]}') if 'insurance.BTC' not in self.data: self.data['insurance.BTC'] = [] self.data['insurance.XRP'] = [] self.data['insurance.EOS'] = [] self.data['insurance.ETH'] = [] def subscribe_orderBookL2(self): param = {} param['op'] = 'subscribe' param['args'] = ['orderBookL2_25.' + self.symbol] self.ws.send(json.dumps(param)) if 'orderBookL2_25.' + self.symbol not in self.data: self.data['orderBookL2_25.' + self.symbol] = [] def subscribe_instrument_info(self): param = {} param['op'] = 'subscribe' param['args'] = ['instrument_info.100ms.' + self.symbol] self.ws.send(json.dumps(param)) if 'instrument_info.100ms.' + self.symbol not in self.data: self.data['instrument_info.100ms.' + self.symbol] = [] def subscribe_position(self): self.ws.send('{"op":"subscribe","args":["position"]}') if 'position' not in self.data: self.data['position'] = [] def subscribe_execution(self): self.ws.send('{"op":"subscribe","args":["execution"]}') if 'execution' not in self.data: self.data['execution'] = [] def subscribe_order(self): self.ws.send('{"op":"subscribe","args":["order"]}') if 'order' not in self.data: self.data['order'] = [] def get_data(self, topic): if topic not in self.data: self.logger.info(" The topic %s is not subscribed." % topic) return [] if topic.split('.')[0] in BybitWebsocket.PRIVATE_TOPIC and not self.auth: self.logger.info("Authentication failed. Please check your api_key and api_secret. Topic: %s" % topic) return [] else: if len(self.data[topic]) == 0: # self.logger.info(" The topic %s is empty." % topic) return [] # while len(self.data[topic]) == 0 : # sleep(0.1) return self.data[topic].pop() def get_df(self): return self.df def search_index(self, a, b): # DataFrame a から DataFrame Bと一致する行を探す indicies = list() Is = list() for i in range(len(b)): id = b.iloc[i]['id'] side = b.iloc[i]['side'] index = a.query('id == {} and side == "{}"'.format(id, side)).index[0] indicies.append(index) Is.append(i) return indicies, Is if __name__ == "__main__": bybitws = BybitWebsocket(wsURL="wss://stream-testnet.bybit.com/realtime", api_key=None, api_secret=None, symbol='BTCUSD') bybitws.subscribe_orderBookL2() while True: sleep(5) print(bybitws.get_data('orderBookL2_25.BTCUSD'))
使い方
もとのコネクタと同じように
bybitws = BybitWebsocket(wsURL="wss://stream-testnet.bybit.com/realtime", api_key=None, api_secret=None, symbol='BTCUSD')
として,get_df
関数を呼ぶことで,OrderBook情報がpandasのDataFrame型として返ってくる.
bybitws.get_df()
id price side size symbol 0 67980000 6798.00 Sell 4168 BTCUSD 1 67975000 6797.50 Sell 16294 BTCUSD 2 67970000 6797.00 Sell 342 BTCUSD 3 67965000 6796.50 Sell 17166 BTCUSD 4 67960000 6796.00 Sell 10858 BTCUSD 5 67955000 6795.50 Sell 8516 BTCUSD 6 67950000 6795.00 Sell 27528 BTCUSD 7 67945000 6794.50 Sell 26413 BTCUSD 8 67940000 6794.00 Sell 23465 BTCUSD 9 67935000 6793.50 Sell 21457 BTCUSD 10 67930000 6793.00 Sell 6215 BTCUSD 11 67925000 6792.50 Sell 18226 BTCUSD 12 67920000 6792.00 Sell 4577 BTCUSD 13 67915000 6791.50 Sell 12060 BTCUSD 14 67910000 6791.00 Sell 16535 BTCUSD 15 67905000 6790.50 Sell 22345 BTCUSD 16 67900000 6790.00 Sell 7112 BTCUSD 17 67895000 6789.50 Sell 23136 BTCUSD 18 67890000 6789.00 Sell 18644 BTCUSD 19 67885000 6788.50 Sell 8303 BTCUSD 20 67880000 6788.00 Sell 322 BTCUSD 21 67865000 6786.50 Sell 8690 BTCUSD 22 67860000 6786.00 Sell 15285 BTCUSD 23 67855000 6785.50 Sell 14570 BTCUSD 24 67850000 6785.00 Sell 10832 BTCUSD 25 67805000 6780.50 Buy 9987 BTCUSD 26 67800000 6780.00 Buy 14040 BTCUSD 27 67795000 6779.50 Buy 3923 BTCUSD 28 67790000 6779.00 Buy 9540 BTCUSD 29 67780000 6778.00 Buy 513 BTCUSD 30 67775000 6777.50 Buy 30129 BTCUSD 31 67770000 6777.00 Buy 29851 BTCUSD 32 67765000 6776.50 Buy 11973 BTCUSD 33 67760000 6776.00 Buy 11096 BTCUSD 34 67755000 6775.50 Buy 8102 BTCUSD 35 67745000 6774.50 Buy 16637 BTCUSD 36 67740000 6774.00 Buy 31585 BTCUSD 37 67735000 6773.50 Buy 10602 BTCUSD 38 67730000 6773.00 Buy 14331 BTCUSD 39 67725000 6772.50 Buy 31113 BTCUSD 40 67720000 6772.00 Buy 26608 BTCUSD 41 67715000 6771.50 Buy 5231 BTCUSD 42 67710000 6771.00 Buy 44615 BTCUSD 43 67705000 6770.50 Buy 13054 BTCUSD 44 67700000 6770.00 Buy 28312 BTCUSD 45 67695000 6769.50 Buy 3325 BTCUSD 46 67690000 6769.00 Buy 16470 BTCUSD 47 67685000 6768.50 Buy 13675 BTCUSD 48 67680000 6768.00 Buy 22811 BTCUSD 49 67675000 6767.50 Buy 16363 BTCUSD
おわりに
過去のコードがほとんどそのまま使えたので助かった.
もしなにかこのコードにバグや不明な点などがあったら気軽にコメントお願いします.
では.
BitMEXの秒足を約定履歴から30秒で自動生成するプログラム✨
BitMEXという荒野を駆け抜けるBotterの皆さんこんにちは.
そんなBotterの皆さんは日々バックテストを重ねて,新しい戦略を構築していることと思います.
mmbotなどのバックテストには高い解像度の情報が必要で,皆さんそのようなことに頭を悩ませているでしょう.
ほとんどの高頻度Botterは秒足などに対してバックテストをしていると思いますが,なにせ秒足を用意するのは意外と面倒くさいものです.
約定履歴を収集するプログラムを書いても,24時間稼働させなければいけないため,管理コストがかかります.
というわけで今回の記事では,BitMEXに公開されてる約定履歴から特定のシンボル(XBTUSDなど)の任意の秒足を作るプログラムを公開します.
今回使うデータ供給元
意外と知られていませんが,BitMEXは日々の約定履歴を集めたファイルをここで公開しています. ですが,このデータは様々なシンボルでの約定が一緒くたにされているために,特定のシンボルだけを抽出する加工が必要になります.
今回のプログラムではその作業も行います.
プログラム
import gzip import pandas as pd from urllib import request #参考: https://qiita.com/yuukiclass/items/88e9ac6c5a3b5ab56cc4 def main(): date, symbol, sec= input('yyyymmdd symbol sec\n').split() date, sec = int(date), int(sec) baseurl = 'https://s3-eu-west-1.amazonaws.com/public.bitmex.com/data/trade/' print('Downloading...') filepath = '{}.csv.gz'.format(date) request.urlretrieve(baseurl + '{}.csv.gz'.format(date), filepath) print('Making candles...') df = unzip(filepath) df_ohlcv = makeCandles(df, symbol, sec,date) print('Done!') file_title = 'ohlc_bitmex-{}.csv'.format(date) df_ohlcv.to_csv(file_title) return def makeCandles(df, symbol, sec, date): # 参考: https://note.com/nagi7692/n/ne674d117d1b6?magazine_key=m0b2a506bf904 df = df.query('symbol == "{}"'.format(symbol)) df.drop(['tickDirection', 'trdMatchID', 'grossValue', 'homeNotional', 'foreignNotional'], axis=1, inplace=True) #86400本の秒足ができるように0秒に約定を入れる year = str(date)[:4] month = str(date)[4:6] day = str(date)[6:] side = df.iloc[0]['side'] price = df.iloc[0]['price'] ser = pd.Series(['{}-{}-{}D00:00:00.000000000'.format(year,month,day), symbol, side, 0,price], index=df.columns, name=0) df = df.append(ser) df = df.sort_index() df['timestamp'] = pd.to_datetime(df['timestamp'], format="%Y-%m-%dD%H:%M:%S.%f") df = df.rename(columns={'timestamp': 'exec_date'}) df = df.set_index('exec_date') df['buy_size'] = df['size'].where(df['side'] == 'Buy', 0) df['buy_flag'] = df['side'] == 'Buy' df['sell_size'] = df['size'].where(df['side'] == 'Sell', 0) df['sell_flag'] = df['side'] == 'Sell' df_ohlcv = df.resample('{}S'.format(sec)).agg({"price": "ohlc", "size": "sum", "buy_size": "sum", "buy_flag": "sum", "sell_size": "sum", "sell_flag": "sum", }) df_ohlcv.columns = ['open', 'high', 'low', 'close', 'volume', 'buy_vol', 'buy_num', 'sell_vol', 'sell_num'] df_ohlcv['buy_num'] = df_ohlcv['buy_num'].astype(int) df_ohlcv['sell_num'] = df_ohlcv['sell_num'].astype(int) df_ohlcv.ffill(inplace=True) return df_ohlcv def unzip(filepath): with gzip.open(filepath, 'rt') as f: df = pd.read_csv(f) return df if __name__ == "__main__": main()
使い方
Pythonなどでプログラムを開くと
yyyymmdd symbol sec
と出るので
20200301 XBTUSD 1
とすることで,2020年3月1日のXBTUSDの1秒足が生成されます.
最後に
なにかプログラムのエラーやわからないこと等ありましたら気軽にコメントお願いします.
プライバシーポリシー
プライバシーポリシー
広告の配信について
広告配信事業者は、ユーザーの興味に応じた広告を表示するために「Cookie(クッキー)」を使用することがあります。
第三者がコンテンツおよび宣伝を提供し、訪問者から直接情報を収集し、訪問者のブラウザにCookie(クッキー)を設定したりこれを認識したりする場合があります。
アクセス解析ツールについて
当サイトでは、Googleによるアクセス解析ツール「Googleアナリティクス」を利用しています。
このGoogleアナリティクスはトラフィックデータの収集のためにCookieを使用しています。
このトラフィックデータは匿名で収集されており、個人を特定するものではありません。
この機能はCookieを無効にすることで収集を拒否することが出来ますので、お使いのブラウザの設定をご確認ください。
当サイトへのコメントについて
当サイトでは、スパム・荒らしへの対応として、コメントの際に使用されたIPアドレスを記録しています。
これはブログの標準機能としてサポートされている機能で、スパム・荒らしへの対応以外にこのIPアドレスを使用することはありません。
当サイトでは、次の各号に掲げる内容を含むコメントは管理人の裁量によって承認せず、削除する事があります。
・特定の自然人または法人を誹謗し、中傷するもの。
・極度にわいせつな内容を含むもの。
・禁制品の取引に関するものや、他者を害する行為の依頼など、法律によって禁止されている物品、行為の依頼や斡旋などに関するもの。
・その他、公序良俗に反し、または管理人によって承認すべきでないと認められるもの。
免責事項
当サイトで掲載している画像の著作権・肖像権等は各権利所有者に帰属致します。権利を侵害する目的ではございません。
記事の内容や掲載画像等に問題がございましたら、各権利所有者様本人が直接メールでご連絡下さい。確認後、対応させて頂きます。
当サイトからリンクやバナーなどによって他のサイトに移動された場合、移動先サイトで提供される情報、サービス等について一切の責任を負いません。
当サイトのコンテンツ・情報につきまして、可能な限り正確な情報を掲載するよう努めておりますが、誤情報が入り込んだり、情報が古くなっていることもございます。
当サイトに掲載された内容によって生じた損害等の一切の責任を負いかねますのでご了承ください。
運営者:KabukiMining
初出掲載:2020年01月16日
BitMEXのWebsocket Connectorが遅い! OrderBookのバグを直す篇
この記事では、前回の記事で作ったプログラムで、OrderBookの BestBidとBestAskを取る際に、NaNが入ってしまうバグの原因と対策を考えてきいきます。
目次:
とりあえずLogをファイル出力してみる
前回の記事で書いたinmemmorydb_bitmex_websocket.pyの10行あたりを、
from logging import Formatter, getLogger, StreamHandler, DEBUG, INFO, WARNING, basicConfig
と変えます。
そして、
basicConfig(filename='logfile/logger.log', level=DEBUG)
を追加します。 そして、inmemmorydb_bitmex_websocket.pyがあるディレクトリに、logfileディレクトリを作成します。 すると、いい感じにログがたまると思います。
そうして眺めているうちに、なんとなく「これ、データを受け取って反映するまでの僅かな時間に、無が存在していて、それを参照しているのでは?」 と思いました。
init関数に,
self.wip = False
を追加、 on_message関数の一番初めに
self.wip = True
を追加し、on_message関数の最後に
self.wip = False
を追加します。
そして、get_orderbook関数を
def get_orderbook(self): while True: if not self.wip: return self.df sleep(0.001)
と変更してみます。 私の環境では、数日ほどOrderBookを記録していますが、ズレはないように思えます。 また、get_orderbook関数のsleepの秒数は、実行環境の性能にもだいぶ左右されると思いますので、ログとにらめっこしながら職人技で調整して下さい。 すると、いい感じにキレイにOrderBookをとれるようになったとおもいます。
一応全体ファイルを置くと、
import websocket import threading from time import sleep import json import urllib import sys import pandas as pd #おまじない from logging import Formatter, getLogger, StreamHandler, DEBUG, INFO, WARNING, basicConfig class BitMEXWebsocket: max_table_len = 200 def __init__(self, endpoint, symbol): self.formatter = Formatter('%(asctime)-15s - %(levelname)-8s - %(message)s') self.logger = getLogger(__name__) self.handler = StreamHandler(sys.stdout) self.handler.setLevel(DEBUG) self.handler.setFormatter(self.formatter) self.logger.setLevel(DEBUG) self.logger.addHandler(self.handler) basicConfig(filename='logfile/logger.log', level=DEBUG) self.endpoint = endpoint self.symbol = symbol self.exited = False self.wip = False self.df = pd.DataFrame() wsurl = self.__get_url() self.logger.info('Connecting to %s' % wsurl) self.__connect(wsurl, symbol) self.logger.info('Connected to WS.') self.__wait_for_symbol(symbol) def __wait_for_symbol(self, symbol): while self.df.empty: sleep(0.1) def __get_url(self): symbolSubs = ["orderBookL2_25"] subscriptions = [sub + ':' + self.symbol for sub in symbolSubs] urlparts = list(urllib.parse.urlparse(self.endpoint)) urlparts[0] = urlparts[0].replace('http', 'ws') urlparts[2] = "/realtime?subscribe={}".format(','.join(subscriptions)) return urllib.parse.urlunparse(urlparts) def __connect(self, wsurl, symbol): self.logger.debug('Starting thread') self.ws = websocket.WebSocketApp(wsurl, on_message=self.__on_message, on_close=self.__on_close, on_open=self.__on_open, on_error=self.__on_error) self.wst = threading.Thread(target=lambda: self.ws.run_forever()) self.wst.daemon = True self.wst.start() self.logger.debug('Started thread') sleep(5) def __on_message(self, message): self.wip = True message = json.loads(message) action = message.get('action') data = message.get('data') if action == 'partial': self.df = pd.io.json.json_normalize(data) elif action == 'update': df_data = pd.io.json.json_normalize(data) indicies, Is = self.search_index(self.df, df_data) for i in range(len(indicies)): index = indicies[i] id = self.df.iloc[index]['id'] price = self.df.loc[index, 'price'] self.df.loc[index] = df_data.iloc[Is[i]] self.df.loc[index, 'price'] = price # price = ((100000000 * symbolIdx) - ID) * instrumentTickSize elif action == "delete": df_data = pd.io.json.json_normalize(data) indicies, Is = self.search_index(self.df, df_data) self.df.drop(index=self.df.index[indicies], inplace=True) self.df = self.df.sort_values('price', ascending=False) self.df.reset_index(inplace=True, drop=True) elif action == "insert": df_data = pd.io.json.json_normalize(data) self.df = pd.concat([self.df, df_data], sort=True) self.df = self.df.sort_values('price', ascending=False) self.df.reset_index(inplace=True, drop=True) self.wip = False # print('best bid: {}, best ask: {}'.format(self.df.iloc[25]['price'], self.df.iloc[24]['price'])) # print('best bid size: {}, best ask size: {}'.format(self.df.iloc[25]['size'], self.df.iloc[24]['size'])) def __on_error(self, error): if not self.exited: self.logger.error("Error : %s" % error) raise websocket.WebSocketException(error) def __on_open(self): self.logger.debug('Websocket Opened') def __on_close(self): self.logger.info('Websocket Closed') def get_orderbook(self): while True: if not self.wip: return self.df.copy() sleep(0.001) def get_best_bid(self): return self.get_orderbook().loc[25, 'price'] def get_best_ask(self): return self.get_orderbook().loc[24, 'price'] def search_index(self, a, b): # DataFrame a から DataFrame Bと一致する行を探す indicies = list() Is = list() for i in range(len(b)): id = b.iloc[i]['id'] side = b.iloc[i]['side'] index = a.query('id == {} and side == "{}"'.format(id, side)).index[0] indicies.append(index) Is.append(i) return indicies, Is if __name__ == "__main__": bmw = BitMEXWebsocket(endpoint='wss://testnet.bitmex.com/realtime', symbol='XBTUSD') while True: print(bmw.get_orderbook()) # print('best_bid: {}, best_ask: {}'.format(bmw.get_best_bid(), bmw.get_best_ask())) sleep(1)
となります。 雑な解説でしたが、参考になったら幸いです。 前回版では、websocketに接続した際に返ってくるメッセージを処理する際にエラーが出るバグがありましたが、それは修正いたしました。
いーや、もっと大きなバグがあるね
bot開発のために、いろいろと弄っていたところ、あきらかにおかしな数値を出してくるバグに遭遇しました。
これはtestnetの板のように、スプレッドがあいている故に起こっているバグだと思われます。 近いうちに修正します
2020/1/6 追記
idからpriceを求める際の計算式の違いによるバグのものでした。 現在は修正されています。
もしバグ報告などがあれば、気軽にコメントお願いします。