クリプトHFTとか競プロとか

競技プログラミングや仮想通貨に関することを中心にブログを書いていきます.

【AtCoder】ABC172-Dで学ぶOEISの使い方

競プロerのみなさんも,そうでないみなさんもこんにちは.

この記事ではAtCoderBeginnerContest 172で出題された D - Sum of Divisors を例にして,OEISの競プロでの活用法を紹介していきます.

OEISとは

OEISとはオンライン整数列大辞典というもので,このページが詳しいですので詳しいことを知りたい方はそちらを御覧ください.

簡単に説明すると,数列を検索することができて,先人たちが発見した公式やそれに関するメモを見ることができるサイトです.

実際に使ってみる

今回扱っていく問題はこちらです.

atcoder.jp

パット見た感じでは,1からNまでいい感じに素因数分解して計算したらよさそう,と思いますが間に合いません.

なので工夫しなくてはいけないのですが,OEISを使うことでこの問題は解決できます.

まずは検索する数列を用意するために,プログラムを書くなど愚直に計算するなどして第8,9項あたりまで求めます.

実際に計算したものがこちらです.(はい!?)

f:id:KabukiMining:20200703214509p:plain
N=1からN=9までの計算結果

というわけでOEISに実際にアクセスして検索していきます.

f:id:KabukiMining:20200703214204p:plain
oeis.orgのページ

ところで,これは一般的に言えることなのですが,数列を検索する際は第3項めあたりから5,6項を入れて検索するといいです.Hintsにも書いてあります.*1

なので,求めた数列を第3項からカンマ区切りで入力して検索してみると,数列が出てきてくれます.

f:id:KabukiMining:20200703214748p:plain
検索結果

数列のタイトルも,「Sum of k*d(k) over k=1,2,...,n, where d(k) is the number of divisors of k.」と,問題が問うていることと一緒です.この情報を引き当てたのはとてもデカイです.

そしてありがたいことに,公式まで掲載されています. f:id:KabukiMining:20200703214920p:plain

今回の場合では,上から三番目の公式を使うことで整数の範囲かつ計算量もO(N)の範囲で簡単に解くことができそうです.

ここまでくれば,書いてあるとおりに実装するだけで問題はとけたようなものです.

OEISのさらなる使い方?

今回扱った問題は,そのまま数列を検索することでOEISから出てきますが,強い方の記事によると,二次元を一次元に落としたり,全体を2で割るなどしないと出てこないような数列もあるようです.

扱ったサイト

OEIS: https://oeis.org/

扱った問題: https://atcoder.jp/contests/abc172/tasks/abc172_d

ABCのD問題のような比較的難しく思える問題でも,OEISを使って検索することで答えが出ることを学べました.

f:id:KabukiMining:20200703215923j:plain
https://www.pexels.com/ja-jp/photo/4006567/

【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

f:id:KabukiMining:20200507162745j:plain
古代の街で座る男性

問題概要

かつての集落にあった神殿の記述がある. 神殿は上から見ると正方形であり,四隅には柱があった. 考古学者たちは遺跡から見つかった柱の中で正方形になっているもののうち,面積が最大のものが神殿に違いないと考えた.

柱の位置が座標平面上の点としてn個与えられるので,4本の柱でできる正方形のうち面積が最大のものの面積を出力せよ.

制約

1 \leq n \leq 3000

0 \leq x_i, y_i \leq 5000

考察

  • 4つの点が与えられたら,正方形を作るかどうかの判定,正方形の面積の算出はO(1) でできる.

  • しかし,n個の柱から4つを選ぶ方法は,{}_n C_4{}通りもあり,およそ 2 \times 10^{13}通りであるため,全探索していては間に合わない.

  • 3つの点が与えられた場合,2つの点が与えられた場合を考えてみる.

  • 3つの点が与えられた場合は,正方形を作るために必要な一点があるべき位置がわかるが,{}_n C_3{}でもおよそ 2.0 \times 10^{10}通りあるために間に合わない.

  • 2つ点が与えられた場合,正方形を作るために必要な2点があるべき位置がわかり, 2点の組み合わせを全探索した場合{}_n C_2{}はおよそ 1.2 \times 10^{7}であるため時間制限に間に合う.

  • 2つの点が与えられた場合の残りの2点の位置候補はO(1)で求められる → O({}_n C_2{})で全探索できる!!

画像のように,2つの点A,Bが与えられた場合にC,Dが存在する場合や,E,Fが存在する場合には正方形を作ることができます.

f:id:KabukiMining:20200507160607p:plain
2点と正方形を作るような点の図

A(x,y), B(x', y'), dx = x'-x , dy = y'-yとすると,E,Fの座標を C(x+dy , x-dx), D(x+dx+dy , y-dx+dy),  E(x-dy , y+dx) ,  F(x+dx-dy , y+dx+dy) と表すことができます.

実装

すべての点を受け取り,点が存在するかを配列で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」というオープンソースパッケージを使っていきます.

f:id:KabukiMining:20200409152738j:plain

ドルバーを作りたいモチベーション

UKIさんの記事や,ファイナンス機械学習にも書いてあるように,ドルバーを使うことで標準バーよりも統計的性質が改善します. UKIさんの記事によると,仮想通貨市場においても同じことが言えるようです.

参照: https://note.com/uki_profit/n/ne218672309cf#rsYqG

mlfinlabのインストール

mlfinlabは,ファイナンス機械学習の中での様々な実装に基づくパッケージであり,このパッケージを使うことによって簡単に本書の中での実装を再現することができます.

Windowsなどを使っている方にはAnaconda環境での使用を推奨するとGithubにあったので,今回はAnaconda上でインストールしていきます.

github.com

mlfinlabのインストールはpipを使って簡単に行うことができます.: pip install mlfinlab

こちらのインストールに失敗する方は,mlfinlabのdocumentationのInstallationの中にある,"Installation for Developers"を試してみてください.

https://mlfinlab.readthedocs.io/en/latest/getting_started/installation.html

ドルバーを作成してみる

今回の記事では以前私のブログで紹介した,「BitMEXの秒足を約定履歴から30秒で自動生成するプログラム✨」を一部変更して作成していきます.

kabukimining.hateblo.jp

実際に変更したプログラムがこちらです. こちらのプログラムの実行には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()

こちらのプログラムも,実行すると標準入力での入力が求められるので,入力します.

f:id:KabukiMining:20200409134333p:plain
コンソール画面のSS
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など)の任意の秒足を作るプログラムを公開します.

f:id:KabukiMining:20200320120142j:plain
チャートの画像

今回使うデータ供給元

意外と知られていませんが,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が入ってしまうバグの原因と対策を考えてきいきます。

f:id:KabukiMining:20200103225308p:plain
BitMEXの板の画像

目次:

とりあえず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開発のために、いろいろと弄っていたところ、あきらかにおかしな数値を出してくるバグに遭遇しました。

f:id:KabukiMining:20200105223855p:plain
バグってる板の画像

これはtestnetの板のように、スプレッドがあいている故に起こっているバグだと思われます。 近いうちに修正します

2020/1/6 追記

idからpriceを求める際の計算式の違いによるバグのものでした。 現在は修正されています。

もしバグ報告などがあれば、気軽にコメントお願いします。