見出し画像

時系列データ出力ツール「TradingTracer v1.1」

--------------------------------------------------------------------------------------
[2019/04/19] v1.1
【すでにファイルが存在する場合は追加するように変更】
pythonスクリプト実行毎にsd_idで指定したファイル名(csv)で新規作成(上書き)していましたが、BOT再起動等でも継続してstoreするというご意見を頂きましたので、追加するように変更しました。
(実行毎にファイルを分けたい場合はsd_idに起動時のtimestampを含めるなどユニークな(重複しない)ファイル名になるようにしてください。)
--------------------------------------------------------------------------------------

BOTやバックテストなど動的な処理から必要なデータを柔軟にファイル出力するためのpythonツールです。
BOT運用では遅延やエラーなど不測の事態が起こり、想定したパフォーマンスどおりにはいかないことが(よく)あります。
これらを検証するには実際にその時点でどのようなデータを処理したのかということが重要になります。
事後に取引所等から取得できるデータもありますが、ロジックによっては(オーダーブックのように)その時にしか判定に使用したデータ内容が分からないものもあります。

本ツールではこのような状況に対応するため、BOTやバックテスト内の任意タイミングで容易にデータ出力を行えるよう作成しました。
少ないコードでロジック内に組み込めるようにシンプルなインターフェース(呼び出し関数)で柔軟なデータ形式に対応しています。
また、本ツールは1ファイルで完結しているため、環境への組み込みも簡単に行えます。

そのため、
・BOTやロジックは実装しているけど、十分な検証データを取得していない
・データ出力のためにロジック内に余計なコードをあまり入れたくない
・事後検証で各種データを取得するのが手間
といった方にお勧めのツールとなります。

また、出力したデータは時系列データ可視化ツール「ChartCreator」の入力形式を意識しており、そちらと併用することでデータの可視化もスムーズに行うことができます。

【1. 機能概要】

■ store関数にて任意タイミングで様々なデータをファイル出力
■ 出力データは都度個別でも、listやDataFrameにまとめて一括でも可能
■ 指定したデータの種類別にファイルを分けて出力
■ 出力処理は非同期で行っているため、メイン処理への影響は少ない
■ スポット的な少ないコードでロジックへの組み込みが可能

下図はTradingTracerの概要図になります。
内部構造に興味がない方は読み飛ばして頂いて構いません。

緑色部分がロジックのメイン処理になり、青色で示した部分が本ツールの処理部分になります。
TradingTracerクラスをインスタンス化するとサブプロセスを生成し、Queueにてデータの受け渡しを行います。
サブプロセス内では出力ファイル毎にスレッドを立てて、並列に非同期でファイル出力を行っていきます。
メイン処理ではTradingTracerが提供するいくつかのstore関数を任意のタイミングで呼び出すだけで、その後の処理を意識する必要はありません。
また、メイン処理としてはQueueにデータをPUTするだけなのでパフォーマンスへの影響は少ないです。

【2. 初期設定】

本ツールの動作環境はpython3となります。
もしない場合は、以下を参考にpython3の環境構築を行ってください。

python 開発環境構築手順(Windows)
python 開発環境構築手順(Mac)
python 開発環境構築手順(AWS)

[重要]
本ツールはmultiprocessingを使用しており、pythonとOSの処理構造上、Windows環境ではpython3.3以降かつ一部特殊処理(freeze_support)を記述する必要があります。
詳しくは【6. Windowsで使用する場合】を参照してください。
(Linux等の環境では特に制約条件はありません。)

① 必要なパッケージをインストールします。

pip install numpy pandas datetime simplejson

②「trading_tracer.py」ファイルを適切なディレクトリに配置します。
 特にこだわりがなければ実行するpythonスクリプトファイルと同じディレクトリで問題ありません。
(TradingTracerはこの1ファイルで完結しているため、他は不要です。)

③ 使用するコード内で「TradingTracer」をimportします。

from trading_tracer import TradingTracer

【3. データ出力方法】

処理開始時にTradingTracerインスタンスを生成します。

tt = TradingTracer(output_dir="data/")

引数output_dirに指定したディレクトリに出力データが作成されます。
(output_dirを省略した場合、「data/」が設定されます。)
本note内ではインスタンス変数は"tt"として扱っていきます。

データ出力は下記5種類の出力関数があります。(store_***)
要件に合わせて必要なタイミングで関数をご使用ください。

各store関数の引数には共通で「sd_id」「data」があります。
sd_id:データを識別する任意ID
   ※このID毎にファイルが作成され、IDはファイル名に使用されます。
data :出力するデータオブジェクト
   ※list / 2次元list / dict / DataFrame等(詳細は各関数を参照)

【3-1. 自注文履歴を出力(store_order)】

発行する各注文のライフサイクルを出力します。
[関数]
 store_order(sd_id, data)
[使用タイミング]

 注文発行 / 取り消し / (部分)約定時
[data形式]
 unixtime:注文(約定)時刻(UnixTime 秒)
 id    :注文毎に一意のID(このIDで注文データがまとめられる)
 type  :注文タイプ("Market", "Limit", "Stop" etc.)
 side   :注文サイド("Buy" or "Sell")
 kind  :履歴種別("order" : 新規発行, "exec" : (部分)約定, "cancel" : 取消)
 price    :注文(約定)価格
 size   :注文(約定)数

[出力コードサンプル]

sd_id = "order_001" # これにより、"order_001.csv"ファイルに出力される

# 個別に1件だけ出力
data1 = [unixtime, order_id, "Limit", "Buy", "order", 3800, 500] # 注文発行
tt.store_order(sd_id, data1)

# 複数をまとめて出力
# 2次元リスト
data2 = [
  [unixtime1, order_id1, "Market", "Buy",  "order", 3800, 500], # 注文発行
  [unixtime2, order_id1, "Market", "Buy",  "exec",  3800, 200], # 部分約定
  [unixtime3, order_id1, "Market", "Buy",  "exec",  3800, 300], # 全約定
  [unixtime4, order_id2, "Limit",  "Sell", "order", 3800, 400], # 注文発行
  [unixtime5, order_id2, "Limit",  "Sell", "exec",  3800, 400], # 全約定
]
tt.store_order(sd_id, data2)

# DataFrame
data3 = pd.DataFrame(data2, columns=["unixtime", "id", "type", "side", "kind", "price", "size"])
tt.store_order(sd_id, data3)

[出力データサンプル(csv)] ※見やすいように一部整形しています。

【3-2. 約定履歴を出力(store_trade)】

約定履歴を出力します。
[関数]
 store_trade(sd_id, data)

[使用タイミング]
 WebSocket受信時 / REST APIで取得時
[data形式]
 unixtime:約定時刻(UnixTime 秒)
 trade_id :任意の約定ID
 side   :Taker約定サイド("Buy" or "Sell")
 price    :約定価格
 size   :約定数

[出力コードサンプル]

sd_id = "trade_001" # これにより、"trade_001.csv"ファイルに出力される

# 個別に1件だけ出力
data1 = [unixtime, trade_id, "Buy", 3800, 500]
tt.store_trade(sd_id, data1)

# 複数をまとめて出力
# 2次元リスト
data2 = [
  [unixtime1, trade_id1, "Sell", 3800, 500],
  [unixtime2, trade_id2, "Buy",  3800, 1500],
  [unixtime3, trade_id3, "Sell", 3800.5, 300],
  [unixtime4, trade_id4, "Buy",  3800.5, 2000],
  [unixtime5, trade_id5, "Buy",  3801, 40000],
]
tt.store_trade(sd_id, data2)

# DataFrame
data3 = pd.DataFrame(data2, columns=["unixtime", "id", "side", "price", "size"])
tt.store_trade(sd_id, data3)

[出力データサンプル(csv)] ※見やすいように一部整形しています。

【3-3. OHLCVを出力(store_ohlcv)】

任意時間足のOHLCVを出力します。
[関数]
 store_ohlcv(sd_id, data)

[使用タイミング]
 時間足切り替わり時 / REST APIで取得時
[data形式]
 unixtime :時間足timestamp(UnixTime 秒)
 open  :始値
 high   :高値
 low    :安値
 close  :終値
 volume   :出来高

[出力コードサンプル]

sd_id = "ohlcv_001" # これにより、"ohlcv_001.csv"ファイルに出力される

# 個別に1件だけ出力
data1 = [unixtime, open, high, low, close, volume]
tt.store_ohlcv(sd_id, data1)

# 複数をまとめて出力
# 2次元リスト
data2 = [
  [unixtime1, open, high, low, close, volume],
  [unixtime2, open, high, low, close, volume],
  [unixtime3, open, high, low, close, volume],
  [unixtime4, open, high, low, close, volume],
  [unixtime5, open, high, low, close, volume],
]
tt.store_ohlcv(sd_id, data2)

# DataFrame
data3 = pd.DataFrame(data2, columns=["unixtime", "open", "high", "low", "close", "volume"])
tt.store_ohlcv(sd_id, data3)

[出力データサンプル(csv)] ※見やすいように一部整形しています。

【3-4. オーダーブックを出力(store_orderbook)】

bids / asksリストをセットで出力します。
[関数]
 store_orderbook(sd_id, data)

[使用タイミング]
 任意タイミング(判定に使用時、定期間隔etc.)
[data形式] ※dict形式でjsonファイルにて出力します。
 unixtime :その時点の時刻(UnixTime 秒)
 asks    :[price, size]のaskリスト
 bids    :[price, size]のbidリスト

[出力コードサンプル]

sd_id = "orderbook_001" # これにより、"orderbook_001.csv"ファイルに出力される

# 個別に1件だけ出力
data1 = {unixtime : {"asks" : [[price,size],...], bids : [[price,size],...]}
tt.store_orderbook(sd_id, data1)

# 複数をまとめて出力
# dict
data2 = {
  unixtime1: {"asks":[[price, size],...], "bids":[[price, size],...]},
  unixtime2: {"asks":[[price, size],...], "bids":[[price, size],...]},
     :
}
tt.store_orderbook(sd_id, data2)

# list
data3 = [
  unixtime1: {"asks":[[price, size],...], "bids":[[price, size],...]},
  unixtime2: {"asks":[[price, size],...], "bids":[[price, size],...]},
     :
]
tt.store_orderbook(sd_id, data3)

[出力データサンプル(json)] ※見やすいように一部整形しています。

【3-5. 任意の指標データ等を出力(store_indicator)】

任意の指標データを出力します。
[関数]
 store_indicator(sd_id, data, columns)
 ※columns:data列見出しリスト(DataFrameの場合は不要)
[使用タイミング]
 任意タイミング(判定に使用時、定期間隔etc.)
[data形式]
 unixtime :時間足timestamp(UnixTime 秒)
 value  :値(複数列設定も可能)

[出力コードサンプル]

sd_id = "indicator_001" # これにより、"indicator_001.csv"ファイルに出力される

# 個別に1件だけ出力
data1 = [unixtime, value,...]
tt.store_indicator(sd_id, data1)

# 複数をまとめて出力
# 2次元リスト
data2 = [
  [unixtime1, value,...],
  [unixtime2, value,...],
  [unixtime3, value,...],
  [unixtime4, value,...],
  [unixtime5, value,...],
]
lst_cols = ["unixtime", "val1", "val2", ...]
tt.store_indicator(sd_id, data2, lst_cols)

# DataFrame
data3 = pd.DataFrame(data2, columns=lst_cols)
tt.store_indicator(sd_id, data3)

[出力データサンプル] ※見やすいように一部整形しています。

【4. 簡単なBOTでの実用例】

それでは、簡単なBOTへのTradingTracerの組み込みを例にして実用方法を解説します。
今回使用するサンプルBOTはBitMEX用で仕様は以下になります。

・起動時に(設定した方向の)ポジションを持つ
・短期 / 長期SMAの位置関係がポジションと逆方向になったらドテン
(ロングポジションで短期 < 長期 or ショートポジションで短期 > 長期)

解説のためのシンプルなロジックです。
ロジック部分のコードを抜粋して以下に記します。

def main():
    LOT = 20           # 注文サイズ
    START_SIDE = "Buy" # ポジションがない場合の初回ポジション

    # 現在のポジション取得
    pos = get_position()

    # ノーポジションの場合, 初回注文
    if pos == 0:
        size = LOT if START_SIDE is "Buy" else -LOT
        market(size)

    # メインループ
    last_timestamp = 0
    while True:
        # OHLCV取得
        df_ohlcv = get_ohlcv()

        # 最新timestamp(UnixTime)
        current_timestamp = df_ohlcv.timestamp.values[-1]

        # 1m足切り替わり時の処理
        if last_timestamp < current_timestamp:
            # SMA計算(期間: Short=5, Long=15)
            ltp   = df_ohlcv.close.values[-1]
            sma_s = df_ohlcv.close.rolling(5).mean().values[-1]
            sma_l = df_ohlcv.close.rolling(15).mean().values[-1]

            # 現在のポジション取得
            pos = get_position()

            # 注文判定
            size = 0
            if sma_s > sma_l and pos < 0:
                print("Market Buy")
                size = -pos + LOT
            elif sma_s < sma_l and pos > 0:
                print("Market Sell")
                size = -pos - LOT

            # ドテン注文
            if size != 0:
                market(size)

            # timestamp更新
            last_timestamp = current_timestamp

        # 一定時間(秒)待つ
        time.sleep(5)

このロジックにおいて必要なデータは
・OHLCV
・短期 / 長期SMA
・ポジション
・注文

になりますので、上記データをロジックループから出力してみます。
それぞれ取得時や計算時に実際に使用したデータをTradingTracerにて出力していきます。

[1] まずはTradingTracerを使用するため、importインスタンス化を行います。

#------------------------------------------------------------------
# [1] TradingTracer import & インスタンス生成
#------------------------------------------------------------------
from trading_tracer import TradingTracer

# TradingTracer生成
tt = TradingTracer()

[2] 次にメインループで時間足切り替わり時にOHLCV / 短期・長期SMA / ポジションを都度、出力します。

    while True:
        # OHLCV取得
        df_ohlcv = get_ohlcv()

        # 最新timestamp(UnixTime)
        current_timestamp = df_ohlcv.timestamp.values[-1]

        # 1m足切り替わり時の処理
        if last_timestamp < current_timestamp:
            # SMA計算(期間: Short=5, Long=15)
            ltp   = df_ohlcv.close.values[-1]
            sma_s = df_ohlcv.close.rolling(5).mean().values[-1]
            sma_l = df_ohlcv.close.rolling(15).mean().values[-1]

            # 現在のポジション取得
            pos = get_position()

            #------------------------------------------------------------------
            # [2] OHLCV / 短期・長期SMA / ポジションを出力
            #------------------------------------------------------------------
            # OHLCVをストア
            tt.store_ohlcv("ohlcv_" + dt, df_ohlcv.iloc[-1,:].values.tolist())

            # SMAをストア
            tt.store_indicator("sma_" + dt, [current_timestamp, sma_s, sma_l], ["unixtime", "short", "long"])

            # Positionをストア
            tt.store_indicator("pos_" + dt, [current_timestamp, pos], ["unixtime", "position"])

[3] 最後に注文データを出力します。
これは注文処理を関数化(market)しているため、関数内に出力処理を組み込みます。
(これにより、ロジックで注文処理で都度、記述する必要はありません。)
今回は成行注文のため、注文発行後に約定確認を行っています。
そのため、注文発行 -> 注文約定の2データを出力します。

# 成行注文
def market(size):
    ret = api.Order.Order_new(symbol="XBTUSD", orderQty=size, ordType="Market").result()[0]

    #-------------------------------------------------------------------
    # [3] 注文データを出力
    #-------------------------------------------------------------------
    order_id = ret["orderID"]

    # 注文発行をストア
    lst_order = [ret["transactTime"].timestamp(), order_id, ret["ordType"], ret["side"], "order", ret["price"], ret["orderQty"]]
    tt.store_order("order_" + dt, lst_order)

    # 約定確認
    while True:
        ret = get_order(order_id)
        if ret is not None and ret["ordStatus"] == "Filled":

            # 約定履歴をストア
            lst_order = [ret["transactTime"].timestamp(), order_id, ret["ordType"], ret["side"], "exec", ret["avgPx"], ret["cumQty"]]
            tt.store_order("order_" + dt, lst_order)

            break
        time.sleep(1)
    #-------------------------------------------------------------------

以上の3点の追加により、ロジックに必要なデータの出力を行うことができました。
参考までにTradingTracer組み込み後のコード全体を以下に記します。
サンプルコードは分かりやすく簡略化しているため、細かいエラー処理などは省いてあります。
また、実行するには以下のパッケージを追加でインストールする必要があります。

pip install pytz, bitmex

[全体コード]

# -*- coding: utf-8 -*-
import sys
import time
import json
import pandas as pd
import bitmex
from datetime import datetime
from pytz import timezone

#-------------------------------------------------------------------
# [1] TradingTracer import & インスタンス生成
#-------------------------------------------------------------------
from trading_tracer import TradingTracer

# TradingTracer生成
tt = TradingTracer()
dt = datetime.now().strftime("%Y%m%d_%H%M%S")
#-------------------------------------------------------------------

# BitMEX API生成(ご自身のAPI KEY/SECRETを入力してください)
api = bitmex.bitmex(test=False, api_key="", api_secret="")

# メイン処理
def main():
    LOT = 20           # 注文サイズ
    START_SIDE = "Buy" # ポジションがない場合の初回ポジション

    # 現在のポジション取得
    pos = get_position()

    # ノーポジションの場合, 初回注文
    if pos == 0:
        size = LOT if START_SIDE is "Buy" else -LOT
        market(size)

    # メインループ
    last_timestamp = 0
    while True:
        # OHLCV取得
        df_ohlcv = get_ohlcv()

        # 最新timestamp(UnixTime)
        current_timestamp = df_ohlcv.timestamp.values[-1]

        # 1m足切り替わり時の処理
        if last_timestamp < current_timestamp:
            # SMA計算(期間: Short=5, Long=15)
            sma_s = df_ohlcv.close.rolling(5).mean().values[-1]
            sma_l = df_ohlcv.close.rolling(15).mean().values[-1]

            # 現在のポジション取得
            pos = get_position()

            #-------------------------------------------------------------------
            # [2] OHLCV / 短期・長期SMA / ポジションを出力
            #-------------------------------------------------------------------
            # OHLCVをストア
            tt.store_ohlcv("ohlcv_" + dt, df_ohlcv.iloc[-1,:].values.tolist())

            # SMAをストア
            tt.store_indicator("sma_" + dt, [current_timestamp, sma_s, sma_l], ["unixtime", "short", "long"])

            # Positionをストア
            tt.store_indicator("pos_" + dt, [current_timestamp, pos], ["unixtime", "position"])
            #-------------------------------------------------------------------

            # 注文判定
            size = 0
            if sma_s > sma_l and pos < 0:
                print("Market Buy")
                size = -pos + LOT
            elif sma_s < sma_l and pos > 0:
                print("Market Sell")
                size = -pos - LOT

            # ドテン注文
            if size != 0:
                market(size)

            # timestamp更新
            last_timestamp = current_timestamp

        # 一定時間(秒)待つ
        time.sleep(5)

# OHLCV取得
def get_ohlcv(count=200):
    ret = api.Trade.Trade_getBucketed(symbol="XBTUSD", binSize="1m", reverse=True, partial=False, count=count).result()[0]
    lst_ohlcv = [[int(r["timestamp"].timestamp()), r["open"], r["high"], r["low"], r["close"], r["volume"]] for r in ret]
    return pd.DataFrame(lst_ohlcv[::-1], columns=["timestamp", "open", "high", "low", "close", "volume"])

# 成行注文
def market(size):
    ret = api.Order.Order_new(symbol="XBTUSD", orderQty=size, ordType="Market").result()[0]

    #-------------------------------------------------------------------
    # [3] 注文データを出力
    #-------------------------------------------------------------------
    order_id = ret["orderID"]

    # 注文発行をストア
    lst_order = [ret["transactTime"].timestamp(), order_id, ret["ordType"], ret["side"], "order", ret["price"], ret["orderQty"]]
    tt.store_order("order_" + dt, lst_order)

    # 約定確認
    while True:
        ret = get_order(order_id)
        if ret is not None and ret["ordStatus"] == "Filled":

            # 約定履歴をストア
            lst_order = [ret["transactTime"].timestamp(), order_id, ret["ordType"], ret["side"], "exec", ret["avgPx"], ret["cumQty"]]
            tt.store_order("order_" + dt, lst_order)

            break
        time.sleep(1)
    #-------------------------------------------------------------------

# 注文取得
def get_order(orderID):
    try:
        return api.Order.Order_getOrders(symbol="XBTUSD", filter=json.dumps({"orderID":orderID})).result()[0][0]
    except Exception:
        return None

# ポジション取得
def get_position():
    try:
        return api.Position.Position_get(filter=json.dumps({"symbol":"XBTUSD"})).result()[0][0]["currentQty"]
    except Exception:
        return 0

# メイン処理
if __name__ == "__main__":
    main()

上記サンプルを実行すると処理中が進む度に出力ファイルへデータが蓄積していきます。
以下は実際に出力されたcsvファイルの内容になります。

【OHLCV】

unixtime,open,high,low,close,volume
1552578240.0,3852.0,3852.0,3851.5,3851.5,109674.0
1552578300.0,3851.5,3852.0,3851.5,3852.0,56288.0
1552578360.0,3852.0,3852.0,3851.5,3852.0,94960.0
1552578420.0,3852.0,3852.0,3851.5,3851.5,362624.0
1552578480.0,3851.5,3852.0,3850.5,3851.0,2673888.0
1552578540.0,3851.0,3851.0,3850.5,3851.0,247034.0
1552578600.0,3851.0,3851.0,3850.5,3850.5,154033.0
1552578660.0,3850.5,3851.0,3850.0,3850.0,477884.0
1552578720.0,3850.0,3850.5,3850.0,3850.5,88015.0

【Order】

unixtime,id,type,side,kind,price,size
1552578284.249,d6e43e61-df39-87f5-8a4e-39db0557e184,Market,Buy,order,3852.0,20
1552578284.249,d6e43e61-df39-87f5-8a4e-39db0557e184,Market,Buy,exec,3852.0,20
1552578619.195,ea0aa0d8-8675-57e0-df60-e16df75fd61f,Market,Sell,order,3850.5,40
1552578619.195,ea0aa0d8-8675-57e0-df60-e16df75fd61f,Market,Sell,exec,3850.5,40
1552579096.066,2729d13e-538c-066a-3d97-e5ff9a17bb00,Market,Buy,order,3852.0,40
1552579096.066,2729d13e-538c-066a-3d97-e5ff9a17bb00,Market,Buy,exec,3852.0,40
1552579518.385,880cb022-1733-11af-a144-8a7158865b94,Market,Sell,order,3851.0,40
1552579518.385,880cb022-1733-11af-a144-8a7158865b94,Market,Sell,exec,3851.0,40

【Position】

unixtime,position
1552578240,20
1552578300,20
1552578360,20
1552578420,20
1552578480,20
1552578540,20
1552578600,-20
1552578660,-20
1552578720,-20

【SMA】

unixtime,short,long
1552578240,3851.9,3849.766666666667
1552578300,3851.9,3850.2
1552578360,3851.9,3850.633333333333
1552578420,3851.8,3851.0333333333333
1552578480,3851.6,3851.233333333333
1552578540,3851.5,3851.366666666667
1552578600,3851.2,3851.4666666666667
1552578660,3850.8,3851.5333333333333
1552578720,3850.6,3851.4666666666667

【5. ChartCreatorによる可視化】

本章の内容は時系列データ可視化ツール「ChartCreator」の使用を前提としています。
データ可視化の必要がない、ChartCreatorを使用しない方は読み飛ばしてください。

前章までで必要なデータの取得を行うことができました。
ここから検証を行う方法は様々ですが、処理結果を視覚的に把握できるようにすることは状況把握や気付きを得る上でも、とても有効です。
記事の冒頭でも書きましたが、TradingTracerで出力したデータは「ChartCreator」への連携を意識した構造になっています。

下記コード内の「convert_for_chart」関数で各ファイルを読み込み・変換することで煩雑なデータ加工を行うことなく、表示設定だけでチャート作成を行うことができます。
特に注文履歴データ(Order)やオーダーブックデータ(OrderBook)をチャートに表示する場合、dict型の専用形式に加工する必要がありますが、TradingTracerから出力したデータであれば、上記関数で簡単に加工済データを取得できます。
TradingTracerから出力した各ファイルからチャートを作成するコードを以下に記します。
(下記コードを実行するにはChartCreator v2.7以降が必要になります。)

# -*- coding: utf-8 -*-
import os
import time
import json
import numpy as np
import pandas as pd
from chart_creator import ChartCreator as cc

# TradingTracerで出力した各種ファイル
data_dir = "data/"
ohlcv_file = "ohlcv_20190315_004443.csv"
order_file = "order_20190315_004443.csv"
board_file = ""
sma_file   = "sma_20190315_004443.csv"
pos_file   = "pos_20190315_004443.csv"

#-------------------------------------------------------------------------------
# メイン処理
#-------------------------------------------------------------------------------
def main():
    #---------------------------------------------------------------------------
    # ファイル読み込み&データ加工
    #---------------------------------------------------------------------------
    # OHLCV取得
    df_ohlcv = convert_for_chart(data_dir+ohlcv_file, "ohlcv")
    print(df_ohlcv.head())

    # 注文履歴取得
    dict_order = convert_for_chart(data_dir+order_file, "order")
    print(dict_order)

    # オーダーブック取得
    #lst_utime, lst_bids, lst_asks = convert_for_chart(data_dir+board_file, "board")

    # SMA取得
    df_sma = convert_for_chart(data_dir+sma_file, "indicator")
    print(df_sma.head())

    # Position取得
    df_pos = convert_for_chart(data_dir+pos_file, "indicator")
    print(df_pos.head())

    #---------------------------------------------------------------------------
    # レイアウトカスタマイズ
    #---------------------------------------------------------------------------
    cc.initialize()                           # 設定データ初期化
    cc.settings["title"] = "Sample Chart(1m)" # チャートタイトル
    #cc.settings["label_size"] = 6            # ラベル文字サイズ

    #---------------------------------------------------------------------------
    # メインチャート(ax:0)
    #---------------------------------------------------------------------------
    cc.add_subchart(ax=0, label="USD", grid=True)
    # ローソクバー設定(OHLCV)
    cc.set_ohlcv_df(df_ohlcv, hover=False)
    # 注文履歴設定
    cc.set_executions(dict_order)
    # SMA設定
    cc.set_line(df_sma["unixtime"].values, df_sma["short"].values, ax=0, color="red", width=1.0, name="SMA Short")
    cc.set_line(df_sma["unixtime"].values, df_sma["long"].values, ax=0, color="blue", width=1.0, name="SMA Long")

    #---------------------------------------------------------------------------
    # サブチャート(ax:1)
    #---------------------------------------------------------------------------
    cc.add_subchart(ax=1, label="Position", grid=True)
    # Position設定
    np_zero = np.zeros(len(df_pos.index), dtype=float)
    cc.set_band(df_pos["unixtime"].values, df_pos["position"].values, np_zero, ax=1, up_color="skyblue", down_color="red",
                alpha=0.2, edge_width=1.0, edge_color="DimGray", name="Position")

    #---------------------------------------------------------------------------
    # チャート生成
    #---------------------------------------------------------------------------
    file_path = data_dir + "image/sample_chart"
    cc.create_chart(file_path + ".png", "png")
    cc.create_chart(file_path + ".html", "html")

#-------------------------------------------------------------------------------
# 入力ファイルデータをチャート入力形式に変換
#-------------------------------------------------------------------------------
def convert_for_chart(file_path, data_type):
    if file_path == None or os.path.isfile(file_path) == False:
        return None

    file_ext = os.path.splitext(file_path)[1][1:]

    if data_type == "board":
        if file_ext != "json":
            return None
        with open(file_path) as f:
            dict_board = json.load(f)
        lst_utime = dict_board.keys()
        lst_bids = [v for k,v in dict_board.values() if k=="bids"]
        lst_asks = [v for k,v in dict_board.values() if k=="asks"]
        return lst_utime, lst_bids, lst_asks

    elif data_type == "order":
        if file_ext != "csv":
            return None
        df = pd.read_csv(file_path)
        np_utime = df["unixtime"].values
        np_id    = df["id"].values
        np_type  = df["type"].values
        np_side  = df["side"].values
        np_kind  = df["kind"].values
        np_price = df["price"].values
        np_size  = df["size"].values

        dict_execs = {}

        length = len(np_id)
        if length < 1:
            return None

        curr_id = np_id[0]
        dict_exec = {
            "type" : np_type[0],
            "side" : np_side[0],
            "execs": []
        }

        for i in range(length):
            if curr_id is not np_id[i]:
                dict_execs[curr_id] = dict_exec
                curr_id = np_id[i]
                dict_exec = {
                    "type" : np_type[i],
                    "side" : np_side[i],
                    "execs": [],
                }
            lst = [np_kind[i], np_utime[i], np_price[i], np_size[i]]
            dict_exec["execs"].append(lst)
        dict_execs[curr_id] = dict_exec

        return dict_execs

    else:
        if file_ext != "csv":
            return None
        return pd.read_csv(file_path)

if __name__ == "__main__":
    main()

[matplotlibによるチャート画像] ※一部説明を追記しています。

[plotlyによるインタラクティブチャート]

【6. Windowsで使用する場合】

本ツールはmultiprocessingを使用しており、pythonとOSの処理構造上、Windows環境ではpython3.3以降かつ一部特殊処理(freeze_support)を記述する必要があります。

実行環境がWindowsの場合、pythonスクリプト実行時に「freeze_support」関数を呼び出します。

from multiprocessing import freeze_support
import sys

# メイン処理
def main():
    :
    :

if __name__ == "__main__":
    #Windows環境の場合, freeze_support呼び出し
    if sys.platform.startswith("win"):
        freeze_support()

    # メイン処理呼び出し
    main()

AWS Cloud9などLinux環境では上記対応は不要です。

[参考]
Python Document (freeze_support)
GitHub pyinstaller

【7. 注意事項】

本ツールは自動でデータ取得するようなものではありません。
あくまでご自身で取得・加工したデータを容易に出力するための補助ツールになります。
また、すでにデータ取得・収集の仕組みを構築されている方や実装できる方にとって使用機会がない可能性があります。

上記をご理解の上で要・不要をご検討ください。

【8. TradingTracerに含まれるもの】

■ trading_tracer.py      :本体ソース(TradingTracerクラス)
■ README.txt    :使用方法・解説etc.
■ requirements.txt  :必要なパッケージ一覧
■ sample_bot.py          :4章で使用したBOTサンプル
■ 出力データサンプル :BOTサンプルで出力されたcsvファイル
■ create_chart.py          :5章で使用したチャート作成ソース
■ チャートサンプル     :5章で使用したチャートサンプル
■ sample_bot_by_ws.py:BOTサンプル(WebSocket版)

【活用例 WebSocket連携から正確なデータ出力】

4章のBOTサンプルで各種データ出力を行いましたが、その方法は
「定期的にデータを取得して出力する」
という方法です。
そのため、(注文履歴やポジション推移など)実際に値に変更があったタイミングとデータ取得で時間差が生じる可能性があります。
これは「(自分から)データを取得」というPULL型の処理方式では避けられない現象になります。

それに対してWebSocketはサーバーと接続した後は購読するデータ(注文履歴や約定履歴etc.)がリアルタイムに送られてきます。(PUSH型)
TradingTracerは任意のタイミングでデータ出力を行うことができるため、WebSocketの受信データを出力することで、より正確なデータを出力することが可能になります。

また、WebSocketの受信処理はメイン処理とは別処理になるため、ロジック内から出力のためのコード(store関数)を省くことができます。

BitMEXのWebSocketでは「execution」にて注文履歴データを受信することができます。
4章の注文履歴データをWebSocketから出力するように変更すると、メッセージ受信処理(on_message関数)にstore_order関数を組み込むことになります。

#------------------------
# WebSocket受信時処理
#------------------------
def on_message(ws, message):
    message_json = json.loads(message)
    # 注文履歴をストア
    if message_json["table"] == "execution":
        for exect in message_json["data"]:
            utime = datetime.strptime(exect["transactTime"] + "+0000", "%Y-%m-%dT%H:%M:%S.%fZ+0000").timestamp()
            lst_order = []
            # 注文発行
            if exect["ordStatus"] == "New":
                lst_order = [utime, exect["orderID"], exect["ordType"], exect["side"], "order", exect["price"], exect["orderQty"]]
            # 注文取消
            elif exect["ordStatus"] == "Canceled":
                lst_order = [utime, exect["orderID"], exect["ordType"], exect["side"], "cancel", exect["price"], exect["orderQty"]]
            # 部分約定
            elif exect["ordStatus"] == "PartiallyFilled":
                lst_order = [utime, exect["orderID"], exect["ordType"], exect["side"], "exec", exect["lastPx"], exect["lastQty"]]
            # 全数約定
            elif exect["ordStatus"] == "Filled":
                lst_order = [utime, exect["orderID"], exect["ordType"], exect["side"], "exec", exect["lastPx"], exect["lastQty"]]
            if len(lst_order) > 0:
                tt.store_order("order_" + dt, lst_order)

これにより、以下の改善につながります。
・market関数の出力処理や約定確認が不要になる
・REST API呼び出しが削減できる(Rate Limitに余裕ができる)
・リアルタイムに正確な処理時刻、価格、数量を出力できる

BitMEXのWebSocketでは約定履歴オーダーブック、ポジション、ティックなど様々なデータ取得ができ、TradingTracerとも相性が良いです。
そのため、ロジックやデータ出力要件に合うなら、とても有効な手法になります。

上記のWebSocketを導入したBOTサンプルの全体コードは購入者ダウンロードファイルに同梱しますが、サンプルコードのため、細かいエラー処理、再接続処理などは省いてあります。
また、実行するには以下のパッケージを追加でインストールする必要があります。

pip install websocket-client==0.52.0

《以下、有料パートですが、解説はここまでで全てです。》

この続きをみるには

この続き:353文字

時系列データ出力ツール「TradingTracer v1.1」

Nagi

1,000円

この記事が気に入ったら、サポートをしてみませんか?気軽にクリエイターを支援できます。

ありがとうございます!よろしければフォロー&シェアも頂けたら幸いです。
24

Nagi

暗号通貨関連の投資、トレードを実践中。 最近はBTCFXのシステムトレードにハマってます! Twitterでも情報発信やってます。 よかったらフォローしてください。 https://twitter.com/Nagi7692

仮想通貨

2つ のマガジンに含まれています

コメント3件

bot再起動のたびにcsvファイルが新規に作られ、上書きされて古いデーターがなくなります。
ちなみに日付ごとにファイルを作ろうと思って、idもそうしてます。
PosiData_2019_04_19.csv とかになります。
おかしいなぁ、と思ったので、238行目に
if os.path.isfile(file_path) == False:
を追加しました。
私の使用目的にはこれでいいのですが、どうでしょう?
お問い合わせありがとうございます。
本ツールでは複数回のスクリプト実行から同一ファイルへの追加ということを想定しおりませんでした。
(現状ではcsvは上書き、jsonは追加となってしまっていますが・・・)

ご提示頂いたようにcsvの初回処理でファイル存在チェックをすることで意図した動作になることは問題ないです。
同一sd_idを指定して上書きするメリットはあまりないと思いますので、追加する仕様に修正しようと思います。
ありがとうございます!
便利にしっかり使わせていただいております(^^)
コメントを投稿するには、 ログイン または 会員登録 をする必要があります。