見出し画像

バックテスト補助ツール「BacktestAssistant v3.0」

はじめに

BacktestAssistantはpythonによるシステムトレードの分析・検証を行うためのバックテストツールです。
いわゆるbotter向けのツールのため、pythonにて取引所APIを使ってデータ収集や取引ができる、できれば簡単なbotなどの作成経験があるという方が対象となります。
(上記に当てはまらない場合、本ツールが有効とならない可能性がありますのでご注意ください。)
冒頭から少し厳し目な記載となっていますが、利用者・著者双方にとって不利益とならないように対象ユーザーを明示しています。

以降の解説やコードサンプルにて本ツールが提供する機能や制約事項をご確認頂き、ご自身にとって有用なものかご判断ください。

本ツールはBitMEXやbybitに代表されるBTC/USDトレードに最適化した機能構成となっており、botのコーティングのようにテストコードを作成・検証することができます。
<2020/10/09>
v2.0アップデートにより、BTC/JPYや他アセットでも使用できるように拡張しました。

---------------------------------------------------------------
【BacktestAssistantの目的】
① 収集が容易なデータを使い、
② 本番運用を意識したコード構成で、
③ リアルトレードに近い取引と統計情報を取得する
---------------------------------------------------------------

① 収集が容易なデータ
本ツールは誤解を恐れずに言うと取引所システムの簡易シミュレーションを行いますが、その実行に必要なデータは時系列価格データ(tickデータ)のみになります。
tickerやohlcvデータはどの取引所でも大抵は提供されているため、容易に収集することができます。

② 本番運用を意識したコード構成
取引所APIで提供されている主要な注文データ取得と類似した機能(関数)を実装しているため、botに近いコーディングができます。
(注文は成行、指値、逆指値、ストップリミットなど主要な方式を網羅)
バックテストのコードから、本番移行も容易になります。

③ リアルトレードに近い取引と統計情報
本ツールを使用したバックテストではテスト期間内の時系列データに沿って注文や約定を執行していくため、取引損益はもちろんTrade / Funding手数料、建玉の未実現損益、証拠金維持率、ロスカットなど、単純なバックテストによる獲得値幅からは把握できない統計情報を算出します。


ここまで良い面をアピールしてきましたが、本ツールは万能ではないため、
重要な「注意・制約事項」があります。

【注意・制約事項】 (必ずご確認ください)

■ テストに使用する各種データの収集・加工を行う機能はありません。
本ツールが提供する機能にデータ収集は含まれません。
ご自身でデータ収集できない場合はバックテストを実施できません。

■ 板情報を考慮した約定・損益計算は行いません。
板情報はデータ量や形式から収集難易度・コストが高く、テストの敷居が一気に高くなってしまうこと、遅延や外的要因などの不確定要素により再現性が低いことなどから本ツールでは除外しています。
そのため、スプレッドやスリッページ、テイク時のセルフインパクトなどは考慮されません。
必然的に高頻度取引や大口取引では実際とは乖離した結果になる可能性が高いです。

■ 指値・逆指値・ストップリミットを複数または高頻度で使用する場合はtickデータの解像度を上げないと正しい順に処理されない場合があります。
注文のトリガーや約定はtickデータを基に判定するため、tickデータの時間間隔内にトリガーや約定する注文が複数存在すると順序の判別ができず、実際とは異なる処理になる可能性があります。
(この問題は成行注文や単一の注文などでは影響はありません。)
テストするロジックの注文方式や判定タイミングより細かい粒度のtickデータを設定してください。

■ 証拠金が取引通貨かつ注文が決済通貨でないと正しく計算されません。
内部計算が上記に従って行われているため、現状では実質的に証拠金BTCで注文サイズUSDであるBTC/USDを想定した構成になっています。
(今後、BTC/JPYやアルト取引にも対応を検討しています。)
<2020/10/09>
v2.0アップデートにより、契約タイプ(contract_type)をインバース型 or リニア型から選択できるようになり、BTC/JPYや他アセットでも使用できるようになりました。

【免責事項】
当noteやプログラムコードの利用により被る損失や被害について、筆者は一切の責任を負うことはできません。
その他、noteご利用規約の「禁止事項」に準拠します。
https://note.mu/terms

前置きが長くなりましたが、以下より具体的な機能や使用方法の解説になります。


1. 初期設定

本ツールの動作環境はPython3.6以降となります。
もし環境がない場合は、以下を参考にPython3.6以降の環境構築を行ってください。
(参考までに筆者の本ツール開発環境はPython3.8.1となります。)

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

環境構築ができたら、必要なパッケージをインストールしてください。
必要なパッケージは「requirements.txt」を参照ください。
コンソールにて「requirements.txt」があるディレクトリに移動し、以下のコマンドで一括インストールできます。

pip install -U -r requirements.txt


2. バックテスト概要

BacktestAssistantを使用したバックテストの流れになります。

BacktestAssistant概要2

上図の流れをコードにすると下記のようになります。
botをコーディングしたことがある方なら、大まかな使い方やできることが把握できると思います。
コード内に使用可能な関数を一通り埋め込んでいるため、テンプレートとしてご活用ください。

from backtest_assistant import BacktestAssistant

# 1.BacktestAssistant生成
test = BacktestAssistant(
       balance           = 1.0,      # 初期証拠金
       max_leverage      = 100,      # 取引所 最大レバレッジ
       trade_leverage    = 0,        # 取引 レバレッジ (0:クロスマージン, 1~:分離マージン)
       losscut           = 50,       # 最低証拠金維持率(%) ロスカットライン
       taker             = 0.075,    # テイカー手数料(%)
       maker             = -0.025,   # メイカー手数料(%)
       tick_mode         = 'ltp',    # ティックモード ('ltp' or 'bidask')
       take_cost         = 0.5,      # 'ltp'の場合のみ 成行注文時の取引コスト(スプレッド)
       contract_type     = 'inverse',# 契約タイプ (inverse:BTC/USD, linear:BTC/JPY etc.)
       trans_digits      = 8,        # 取引通貨 小数桁数 (BTC:8桁)
       settle_digits     = 1,        # 決済通貨 小数桁数 (USD:1桁)
       sharpe_ratio_unit = 86400)    # シャープレシオ期間単位 (秒)

# 2.tickデータ設定
# (複数から扱いやすい関数を使用->詳しくは後述の機能解説参照)
# tick_mode='ltp'の場合
test.preset_ltp_ticks(lst_unixtime, lst_price)
# test.preset_ltp_ticks_list(lst_unixtime_price)
# test.preset_ltp_ticks_df(df_unixtime_price)
# test.preset_ltp_ticks_from_ohlcv_list(lst_ohlcv)
# test.preset_ltp_ticks_from_ohlcv_df(df_ohlcv)
# tick_mode='bidask'の場合
# test.preset_bidask_ticks(lst_unixtime, lst_ltp, lst_bid, lst_ask)
# test.preset_bidask_ticks_list(lst_unixtime_price)
# test.preset_bidask_ticks_df(df_unixtime_price)

# Funding Rate設定
# (必要な場合のみ設定)
test.set_funding_rate(rate=0.01)

# Funding rate事前設定
test.preset_funding_rates(lst_unixtime, lst_fr)
test.preset_funding_rates_list(lst_unixtime_fr)
test.preset_funding_rates_df(df_unixtime_fr)

# 出力オプション設定
# (必要な場合のみ設定)
test.set_output_option(disp_progress=False,
                      tick_csv=False, order_csv=True,
                      execution_csv=True, position_csv=True,
                      balance_csv=True, chart_data=True)

# 初期処理
test.initialize(start_time=start_time)

# 3.テスト実行 (メインループ) ---------------------------------------
while test.time() <= end_time:

   # 現在時刻
   now_unixtime = test.time()   # Unix Time
   now_dt_jst   = test.now()    # DateTime(JST)
   now_dt_utc   = test.nowutc() # DateTime(UTC)

   # データ取得
   price   = test.get_price()       # 価格
   pos     = test.get_position()   # 建玉
   orders  = test.get_open_orders() # アクティブ注文
   balance = test.get_balance()     # 残高

    # 注文発行
   res = test.market_order(side, size)       # 成行注文
   res = test.limit_order(side, price, size) # 指値注文
   res = test.stop_order(side, price, size)  # 逆指値注文
   res = test.stop_limit_order(side, trigger_price,
                               price, size)  # ストップリミット注文

   # 注文キャンセル
   res = test.cancel_order()

   # 状態出力
   test.print_status()

   # 時間を進める
   test.sleep(sec)
   test.sleep_to_time(to_time)
#----------------------------------------------------------------

# 4.終了処理 (テスト結果&統計情報)
results = test.terminate()

# 5.結果チャート出力
file_path = 'result_chart.png'
test.save_result_chart(file_path, tick_unit=86400, result_type='pnl',
                       show_price=True, price_type='ohlcv',
                       show_pos=True, show_fiat=True)


3. 機能解説 (関数一覧)

BacktestAssistantで使用できる関数をそれぞれ解説します。

【コンストラクタ】

BacktestAssistantインスタンスを生成します。
[params]
・output_dir        :テスト結果データ保存先ディレクトリ
・balance             :テスト開始時の残高
・max_leverage  :取引所の最大レバレッジ
・trade_leverage:取引に使用するレバレッジ
         0                               :クロスマージン(全残高 最大レバレッジ)
         1〜max_leverage分離マージン(注文サイズ 指定レバレッジ)
※ クロス/分離マージンの詳細はbit仙人さんの記事がわかりやすいです。
Bybitレバレッジ調整方法|クロスマージンと分離マージンの使い分け解説

・losscut              :最低証拠金維持率 (%) ※ロスカットライン
・taker                  :テイカー手数料 (%)
・maker                :メイカー手数料 (%)
・tick_mode        :設定するtickデータ形式
         'ltp'           :[unixtime, ltp]
         'bidask'   :[unixtime, ltp, bid, ask]
・take_cost          :テイク時の現在価格からの約定コスト (価格幅)
         (tick_mode='ltp'の場合のみ有効)
・contract_type  :取引通貨の契約タイプ
         'inverse'  :証拠金=取引通貨、注文サイズ=決済通貨
                             (例)BTC/USD
                                    取引通貨:BTC、決済通貨:USD
                                    証拠金:BTC、注文サイズ:USD
         'linear'     :証拠金=決済通貨、注文サイズ=取引通貨
                             (例)BTC/JPY
                                    取引通貨:BTC、決済通貨:JPY
                                    証拠金:JPY、注文サイズ:BTC
・trans_digits      :取引通貨 小数桁数 
・settle_digits     :決済通貨 小数桁数
・sharpe_ratio_unit:シャープレシオ期間単位 (秒)
                                    (指定期間毎の残高推移からシャープレシオを計算)

<2020/10/09>
v2.0アップデートにより、契約タイプ(contract_type)をインバース型/リニア型から選択できるようにしました。
これにより、BTC/USD以外のBTC/JPYや他アセットでもバックテストを行うことができます。
また、取引レバレッジ(trade_leverage)を0にすることでクロスマージン、1以上で分離マージンとして取引できるようにしました。

[重要] 取引レバレッジ(trade_leverage)の扱いが変わったため、以前のバージョン(〜v1.1)をお使いの場合、同じ扱いでテストを行うにはクロスマージンの0を指定する必要があります。
(以前のバージョンのtrade_leverage初期値は0ではないのでご注意ください。)

<2021/07/13>
v3.0アップデートにより、統計情報にシャープレシオを追加しました。
sharpe_ratio_unitにて指定した期間毎の残高推移より、シャープレシオを計算します。
(sharpe_ratio_unit省略時は86400(秒)=1日単位となります。)

def __init__(
   output_dir:str='./result/', balance:float=1.0,
   max_leverage:float=100, trade_leverage:float=0, losscut:float=50,
   taker:float=0.075, maker:float=-0.025, tick_mode='ltp', take_cost=0.5,
   contract_type='inverse', trans_digits=8, settle_digits=1, sharpe_ratio_unit=86400)


# 例) BitMEX:XBT/USD, bybit:BTC/USD (デフォルト)
test = BacktestAssistant(
       balance           = 1.0,      # 初期証拠金 (BTC)
       max_leverage      = 100,      # 取引所 最大レバレッジ
       trade_leverage    = 0,        # 取引 レバレッジ (0:クロスマージン, 1~:分離マージン)
       losscut           = 50,       # 最低証拠金維持率(%) ロスカットライン
       taker             = 0.075,    # テイカー手数料(%)
       maker             = -0.025,   # メイカー手数料(%)
       tick_mode         = 'ltp',    # ティックモード ('ltp' or 'bidask')
       take_cost         = 0.5,      # 'ltp'の場合のみ 成行注文時の取引コスト(スプレッド)
       contract_type     = 'inverse',# 契約タイプ (inverse:BTC/USD, linear:BTC/JPY etc.)
       trans_digits      = 8,        # 取引通貨 小数桁数 (BTC:8桁)
       settle_digits     = 1,        # 決済通貨 小数桁数 (USD:1桁)
       sharpe_ratio_unit = 86400)    # シャープレシオ期間単位 (秒)


# 例) bitFlyer:FXBTCJPY
test = BacktestAssistant(
       balance        = 1000000,     # 初期証拠金 (円)
       max_leverage   = 4,           # 取引所 最大レバレッジ
       trade_leverage = 0,           # 取引 レバレッジ (0:クロスマージン, 1~:分離マージン)
       losscut        = 50,          # 最低証拠金維持率(%) ロスカットライン
       taker          = 0.0,         # テイカー手数料(%)
       maker          = 0.0,         # メイカー手数料(%)
       tick_mode      = 'ltp',       # ティックモード ('ltp' or 'bidask')
       take_cost      = 100,         # 'ltp'の場合のみ 成行注文時の取引コスト(スプレッド)
       contract_type  = 'linear',    # 契約タイプ (inverse:BTC/USD, linear:BTC/JPY etc.)
       trans_digits   = 8,           # 取引通貨 小数桁数 (BTC:8桁)
       settle_digits  = 0,           # 決済通貨 小数桁数 (JPY:0桁)
       sharpe_ratio_unit = 86400)    # シャープレシオ期間単位 (秒)

【tickデータ設定】

テスト期間の時系列(Unix Time)価格データ(tickデータ)を設定します。

設定するtickデータは2種類('ltp' or 'bidask')から選択します。
(どちらの形式を設定するかコンストラクタのtick_modeで設定)

■ tick_mode='ltp'
 設定するtickデータは[unixtime, price(ltp)]のリスト形式
 →成行注文時のスプレッドとして約定価格にtake_costを使用します。

  unixtime      ltp
[[1596240000.0, 11361.0],
 [1596240001.0, 11361.5],
 [1596240002.0, 11363.0],
 [1596240003.0, 11362.5],
 ・・・]

■ tick_mode='bidask'
 設定するtickデータは[unixtime, ltp, bid, ask]のリスト形式
 →注文時のスプレッドはbid / askを使用します。

  unixtime      ltp      bid      ask
[[1600905963.2, 10204.0, 10203.5, 10204.0],
 [1600905964.2, 10203.5, 10203.5, 10204.0],
 [1600905965.4, 10204.0, 10203.5, 10204.0],
 [1600905966.4, 10203.5, 10203.5, 10204.0],
 ・・・]

テストはこのtickデータに沿って約定や損益の計算・判定を行います。
設定するtickの時間間隔はロジックに合わせてに設定してください。

・精度の高い値動きを必要とする場合は秒tick数秒tick
・スイングなど特定時の価格でよければ判定時刻単位1時間tick
etc.

# tickデータは[unixtime, price(ltp)]のリスト形式 (tick_mode='ltp'の場合)
# ロジックに合わせて自由な時間間隔でOK

# 1秒tick
[[1596240000.0, 11361.0],
 [1596240001.0, 11361.5],
 [1596240002.0, 11363.0],
 [1596240003.0, 11362.5],
 ・・・]

# 1時間tick
[[1596243600.0, 11272.5],
 [1596247200.0, 11288.5],
 [1596250800.0, 11354.0],
 [1596254400.0, 11388.0],
 ・・・]

# ランダム間隔tick
[[1598918401.154071, 11660.0],
 [1598918402.841267, 11659.5],
 [1598918403.347175, 11659.0],
 [1598918407.043221, 11658.0],
 [1598918413.113105, 11657.5],
 ・・・]

tickデータの粒度を細かくすれば、値動きの精度も上がりますが、データ量やテスト時間も比例して大きくなります。

ロジックに必要な粒度にてテスト期間のtickデータを設定してください。
少なくとも発行する注文頻度より細かい時間間隔のtickデータが必要です。

tickデータを設定する関数は複数パターン用意しています。
(様々なinput形式に対応するため。)
設定する際は保持しているデータから扱いやすい形式の関数をご使用ください。

[tick_mode='ltp']の場合

# unixtimeリスト, ltpリストをそれぞれ設定
def preset_ltp_ticks(unixtime:list, ltp:list)

# [unixtime, ltp]の2次元リストを設定
def preset_ltp_ticks_list(lst_unixtime_price:list)

# columns=[unixtime, ltp]のDataFrameを設定
def preset_ltp_ticks_df(df_unixtime_price:pd.DataFrame)

[tick_mode='bidask']の場合

# unixtimeリスト, lptリスト, bidリスト, askリストをそれぞれ設定
def preset_bidask_ticks(unixtime:list, ltp:list, bid:list, ask:list)

# [unixtime, ltp, bid, ask]の2次元リストを設定
def preset_bidask_ticks_list(lst_unixtime_price:list)

# column=[unixtime, ltp, bid, ask]のDataFrameを設定
def preset_bidask_ticks_df(df_unixtime_price:pd.DataFrame)

また、ohlcvデータを設定することで自動的にtickデータに変換&設定します。(open, high, low, closeをtick展開)
ohlcvデータからtickデータの作成は、bid / askが求まらないため、tick_mode='ltp'のみで使用可能です。
(tick_mode='bidask'で使用するとエラーとなります。)

例)
OHLCV
[[1598886000(t), 10000(o), 10150(h), 10080(l), 100120(c), 1000000(v)],
 [1598886060(t), 10120(o), 10130(h), 10110(l), 100140(c), 1500000(v)],…]
↓
tick
[[1598886000(t), 10000(o)],
 [1598886001(t), 10150(h)],
 [1598886002(t), 10080(l)],
 [1598886003(t), 10120(c)],
 [1598886060(t), 10120(o)],
 [1598886061(t), 10130(h)],
 [1598886062(t), 10110(l)],
 [1598886063(t), 10140(c)],…]

時間足ベースのロジックなどではohlcvを使用しているため、収集したohlcvデータをそのままにテストデータに使用することができます。

# [unixtime, open, high, low, close, volume]の2次元リストを設定
def preset_ltp_ticks_from_ohlcv_list(ohlcvs:list)

# columns=[unixtime, open, high, low, close, volume]のDataFrameを設定
def preset_ltp_ticks_from_ohlcv_df(ohlcvs:pd.DataFrame)

【出力オプション設定】

バックテストの進捗や結果を出力するか設定します。
テストデータが大きい場合に不要なデータを出力しないように制御します。(出力しないデータはFalseを設定)
設定する場合はinitialize関数を呼び出す前に設定しておく必要があります。
[params]
・disp_progress :バックテストの進捗を10%毎にコンソール出力
・tick_csv           :ticks.csvを出力 (tick履歴)
・order_csv        :orders.csvを出力 (注文履歴)
・execution_csv :executions.csvを出力 (約定履歴)
・position_csv   :positions.csvを出力 (建玉履歴)
・balance_csv   :balances.csvを出力 (残高履歴)
・chart_data     :終了処理でチャート連携データを作成

def set_output_option(disp_progress:bool=False,
                      tick_csv:bool=False, order_csv:bool=True,
                      execution_csv:bool=True, position_csv:bool=True,
                      balance_csv:bool=True, chart_data:bool=True)

【Funding rate設定】

Fundingを考慮した損益を求める場合にレート発生タイミングを設定します。
使用する場合はinitialize関数を呼び出す前に設定しておく必要があります。
(使用しない場合は未設定)
[params]
・rate      :Fundingレート (%)
・first      :24時間内で最初にFunding feeが発生するタイミング (時)
・interval:Funding feeの発生間隔 (時間)

def set_funding_rate(rate:float=0.01, first:int=1, interval:int=8)

<2021/04/27>
v2.2アップデートにより、Funding rate事前設定ができるようにしました。
これにより、時系列Funding rateを初期処理前に設定することで自動的に時刻に応じたFunding rateが適用されます。

def preset_funding_rates(self, unixtime: list, fr: list)
def preset_funding_rates_list(self, lst_unixtime_fr: list)
def preset_funding_rates_df(self, df_unixtime_fr: pd.DataFrame)


【初期処理】

テストを実施するための初期化処理を行います。
テスト実行前に必ず呼び出してください。
[params]
・start_time           :テスト開始時刻 (Unix Time)
                                    (省略時はtickデータの先頭からテスト開始)

def initialize(start_time:float=0)

【Backtestの時間を進める】

テスト内の時刻を進めるには以下2つのsleepを使用します。

・sleep_to_time(to_time)
→to_timeの時刻までテスト内の時刻を進める
 (to_timeはUnix Time指定)

・sleep(sec)
sec秒間だけテスト内の時刻を進める
 (secは秒指定)

# Sleep to time
def sleep_to_time(to_time:float)

# Sleep
def sleep(sec:float)

【注文発行】

各種注文を発行します。
成行注文は即約定しますが他はアクティブ注文になり、トリガーや約定価格に達すると処理されます。

注文発行やキャンセル、約定など注文データの状態が変わる度に注文履歴に記録されます。
同様に約定すると約定履歴ポジション履歴残高履歴に記録されます。
各履歴はテスト終了時にCSV出力されます。

注文発行の戻り値はdict形式です。
注文が成功するとdictのidに注文を識別するID番号が設定されます。
idはアクティブ注文の取得や注文キャンセルで指定する場合に使用します。

[return]
成功:{'status_code':200, 'id':1, 'message':'[success market_order]'}
失敗:{'status_code':400, 'id':0, 'message':'[failed market_order] error内容'}

# 成行注文
def market_order(side:str, size:float) -> dict

# 指値注文
def limit_order(side:str, price:float, size:float) -> dict

# 逆指値注文
def stop_order(side:str, price:float, size:float) -> dict

# ストップリミット注文
def stop_limit_order(side:str, trigger_price:float, price:float, size:float) -> dict

【注文キャンセル】

アクティブ注文のキャンセルを行います。
order_idを指定した場合は該当の注文のみキャンセルします。
省略または0を設定した場合は全てのアクティブ注文をキャンセルします。

[return]
成功:{'status_code':200, 'id':0, 'message':'[success cancel_order]'}
失敗:{'status_code':400, 'id':0, 'message':'[failed cancel_order] error内容'}

def cancel_order(order_id=0)

【現在時刻取得】

テスト実行中の現在時刻を取得します。
取得する時刻のタイプに合わせて3種類の関数があります。

# Unix Time
def time() -> float

# DateTime(JST)
def now() -> datetime

# DateTime(UTC)
def nowutc() -> datetime

【データ取得】

テスト実行中の各データを取得します。

・アクティブ注文 (オープンオーダー)
 [status]
 ・Waiting   :トリガー待ち状態 (StopLimitのみ)
 ・Active      :約定待ち状態 (オープンオーダー)
 ・Canceled:キャンセル済み状態 (Closed)
 ・Filled       :約定済み状態 (Closed)

def get_open_orders() -> list

[return]
open_orders = [
 {
  'id':1,
  'timestamp':    1598886000,
  'status':       'Active',
  'type':         'Limit',
  'side':         'Buy',
  'size':         5000,
  'price':        10000.0,
  'trigger_price':0.0
 },
 ・・・
]

・現在建玉

def get_position() -> dict

[return]
position = {
 'size':          10000,   # ロング:正の値(+), ショート:負の値(-)
 'avr_entry':     10000.0, # 平均参入価格
 'margin':        1.0,     # 建玉証拠金
 'margin_rate':   5.0,     # 証拠金維持率
 'liq_price':     9000.0,  # 清算価格
 'unrealised_pnl':0.01     # 未実現損益
}

・現在価格

def get_price() -> dict

[return]
price = {
  'ltp':  10000.0, # 最終価格
  'bid':  10000.0, # ベスト bid価格 (tick_mode='ltp'では0)
  'ask':  10000.5, # ベスト ask価格 (tick_mode='ltp'では0)
}

・現在残高

def get_balance() -> float

[return]
balance

【状態出力】

現在の価格や残高、建玉、アクティブ注文の状態をコンソール出力します。
デバッグ時など状態を確認したい場合にご使用ください。

def print_status()

コンソール出力(アクティブ注文がある状態)

[Time] 2020/09/04 17:06:00  [Ltp] 10429.0  [Balance] 0.1010  [Leverage] 5.0
[Position] Size:0  AvrEntry:0.00  Margin:0.0000(0.00%)  Unrealized:0.0000  Liquidation:0.00
[Open orders]
 [id:19]  Limit Buy(Active)  Size:4000  Price:10304.5  Trigger:0.0  Update:1599206460.0

コンソール出力(建玉がある状態)

[Time] 2020/09/05 01:06:00  [Ltp] 10400.0  [Balance] 0.1012  [Leverage] 5.0
[Position] Size:-4000  AvrEntry:10436.50  Margin:-0.3833(533.08%)  Unrealized:0.0013  Liquidation:8463.37

【終了処理】

テスト結果の集計および出力を行います。
テスト実行後に呼び出してください。

def terminate() -> dict

戻り値

終了処理の戻り値はdict形式です。
テスト結果データ統計情報チャート作成用データが含まれています。

<2021/04/27>
v2.2アップデートにより、最大DD/含み損情報が追加されました。
また、統計情報に最大リスクを設け、上記を表示するように変更しました。

<2021/07/13>
v3.0アップデートにより、シャープレシオが追加されました。
また、統計情報にも上記を表示するように変更しました。

[return]
results = {
  'data':       各履歴DataFrameのdict,    ※以下のdic_data
  'statistics': 統計情報のdict,           ※以下のdic_stat
  'chart':      チャート用DataFrameのdict,  ※以下のdic_chart
}

# 上記のdic_data内訳
dic_data = {
  'tick':      tick履歴 DataFrame,
  'order':     order履歴 DataFrame,
  'execution': execution履歴 DataFrame,
  'position':  position履歴 DataFrame,
  'balance':   balance履歴 DataFrame,
}

# 上記のdic_stat内訳
dic_stat = {
  'trades' : {
     'count'        : 0, # 総取引回数
     'pf'           : 0, # PF
     'sharpe_ratio' : 0, # シャープレシオ
     'sum'          : 0, # 総損益
     'mean'         : 0, # 平均損益
     'maxdd'        : 0, # 最大DD
     'maxdd_ratio'  : 0, # 最大DD率
     'maxdd_ut'     : 0, # 最大DD UnixTime
     'sum_size'     : 0, # 総取引高
     'price_range'  : 0, # 総獲得値幅
     'max_unrealised_loss'       : 0, # 最大含み損
     'max_unrealised_loss_ratio' : 0, # 最大含み損率
     'max_unrealised_loss_ut'    : 0, # 最大含み損 UnixTime
  },
  'profit' : {
     'count'        : 0, # 勝取引数
     'sum'          : 0, # 総利益
     'max'          : 0, # 最大利益(1取引あたり)
     'mean'         : 0, # 平均利益
     'maxlen_count' : 0, # 最大連勝数
     'maxlen_sum'   : 0, # 最大連勝利益
  },
  'loss' : {
     'count'        : 0, # 負取引数
     'sum'          : 0, # 総損失
     'max'          : 0, # 最大損失(1取引あたり)
     'mean'         : 0, # 平均損失
     'maxlen_count' : 0, # 最大連敗数
     'maxlen_sum'   : 0, # 最大連敗損失
  },
  'fee' : {
     'trade'        : 0, # 総取引手数料
     'funding'      : 0, # 総Funding手数料
  },
}

# 上記のdic_chart内訳
dic_chart = {
  'buy':      Buy注文 約定履歴 DataFrame,
  'sell':     Sell注文 約定履歴 DataFrame,
  'position': 建玉履歴 DataFrame,
  'pnl':      累積損益履歴 DataFrame,
}


4. テスト結果

終了処理(terminate())を実行すると
 ・コンソールにテスト結果
 ・出力ディレクトリに各履歴CSV
を出力します。

① コンソール出力

テスト結果(Result)統計情報(Execution statistics)が出力されます。
[Result]
・Time      :テスト期間
・Balance :テスト開始時と終了時の残高
・Sharpe Ratio:シャープレシオ
・Orders   :テスト期間中の注文数 ※()内は終了時点の状態別の注文数
・Position :テスト期間中の最大建玉サイズ(L/S)、最低証拠金維持率(%)

[Execution statistics]
・PF      :Abs(Total Profit / Total Loss)
・Total  :総約定数(総サイズ), 総損益, 平均損益, 総獲得値幅
・Profit :利益約定数, 総利益, 平均利益, 最大利益, 最大連勝数(利益)
・Loss   :損失約定数, 総損失, 平均損失, 最大損失, 最大連敗数(損失)
・Fee    :総取引手数料, 総Funding手数料
・Drawdown:最大DD率, 発生時刻
・Unrealized loss:最大含み損率, 発生時刻
※上記Total / Profit / Lossは手数料を除いた取引を集計しています。
そのため、薄利、テイカー、高頻度などの条件下では獲得値幅やPnLがプラスでも残高ベースでは損失となる場合があります。(手数料負け状態)

<2020/10/09>
v2.0アップデートにて分離マージンではロスカットが発生しても残高で取引ができるため、テストを中断せずに継続するようにしました。
また、テスト中にロスカットが発生するとロスカットされた精算回数も最後に出力されます。

<2021/04/27>
v2.2アップデートにより、最大DD/含み損情報が追加されました。
統計情報に最大リスクを設け、上記を表示するように変更しました。
また、フィアットベースの統計情報も表示するように
変更しました。

<2021/07/13>
v3.0アップデートにてシャープレシオを追加しました。
テスト期間の残高をsharpe_ratio_unit単位に取得し、その変動率からシャープレシオを計算しています。

[Result]
 [Time    ] 2020/09/06 09:00:00 - 2020/09/07 09:00:00
 [Balance ] 1.0 -> 1.0843 (+8.43%)  Sharpe Ratio:0.0000
 [Orders  ] Total:8909 (Open:0  Executed:121  Canceled:8788)
 [Position] Max Long:45,000.0  Max Short:-45,000.0  Min Margin:2210.36%

[Execution statistics]  PF:2.25
 [Total   ] Count:121(Size:1,230,000)  PnL:+0.0582  Avr:+0.0005  Price Range:+751.4
 [Profit  ] Count:48(96.00%)  Sum:+0.1047  Avr:+0.0022  Max:+0.0089  MaxLen:34(+0.0764)
 [Loss    ] Count:2(4.00%)  Sum:-0.0466  Avr:-0.0233  Max:-0.0465  MaxLen:1(-0.0465)
 [Fee     ] Trade:-0.0252  Funding:-0.0009
 [Max risk]
   Drawdown       :-4.81%(-0.0499) 2020/09/06 14:09:15
   Unrealised loss:-4.22%(-0.0438) 2020/09/06 14:09:12

[Fiat balance]
 [Start ] 10,158.0 (Balance:1.0 Price:10158.0)
 [End   ] 11,116.2 (Balance:1.0843 Price:10252.0)
 [Result] 958.2 (+9.43%)  Sharpe Ratio:0.0000

[Fiat execution statistics]  PF:1.85
 [Total   ] Count:121(Size:0)  PnL:+955  Avr:+8.0  Price Range:+0
 [Profit  ] Count:79(66.95%)  Sum:+2,075  Avr:+26.0  Max:+149  MaxLen:9(+348)
 [Loss    ] Count:39(33.05%)  Sum:-1,120  Avr:-29.0  Max:-577  MaxLen:3(-668)
 [Fee     ] Trade:0  Funding:0
 [Max risk]
   Drawdown       :-6.33%(-668) 2020/09/06 14:09:15
   Unrealised loss:-4.22%(-438) 2020/09/06 14:09:12

② 履歴CSV出力

コンストラクタで指定した「output_dir」に各履歴CSVファイルが作成されます。​

・ticks.csv:tick履歴 (tick_mode='ltp'ではbid / askは0となります。)

timestamp,ltp,bid,ask
1598918400.0,11659.5,11659.5,11660.0
1598918401.0,11668.5,11668.5,11669.0
1598918402.0,11657.5,11657.5,11658.0
1598918460.0,11668.0,11667.5,11668.0
・・・

・orders.csv:注文履歴

timestamp,id,status,type,side,size,price,trigger_price
1598947260.0,1,Active,Limit,Sell,4000,11946.0,0.0
1598947261.0,1,Filled,Limit,Sell,4000,11946.0,0.0
1598948880.0,2,Filled,Market,Buy,4000,11923.5,0.0
1598976060.0,3,Active,Limit,Buy,4000,12048.0,0.0
1598976062.0,3,Filled,Limit,Buy,4000,12048.0,0.0
・・・

・executions.csv:約定履歴

timestamp,id,type,side,size,price,cost,fee,pnl,price_range
1598947261.0,1,Limit,Sell,4000,11946.0,-0.33484011,-8.371e-05,0.0,0.0
1598947261.0,0,Funding,Sell,-4000,11950.0,0.0,-3.3472803347280334e-05,0.0,0.0
1598948880.0,2,Market,Buy,4000,11923.5,0.33547197,0.0002516,0.00063185,22.5
1598976062.0,3,Limit,Buy,4000,12048.0,0.33200531,-8.3e-05,0.0,0.0
1598976062.0,0,Funding,Buy,4000,12031.0,0.0,3.32474441027346e-05,0.0,0.0
1598977680.0,4,Market,Sell,4000,12008.5,-0.33309739,0.00024982,-0.00109208,-39.5
・・・

・positions.csv:建玉履歴
<2020/10/09>
v2.0アップデートで証拠金内訳、レバレッジなどより詳細な履歴データを出力するように変更しました。

timestamp,size,avr_entry,pos_cost,order_margin,pos_margin,margin_rate,liq_price,unrealised_pnl,leverage
1596240000.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0
1596286800.0,-10000.0,11688.5,-0.85554177,0.0,0.00855542,116.88572,0.0,-0.0000366,0
1596288600.0,-10000.0,11688.5,-0.85554177,0.0,0.00855542,116.53093,0.0,0.01496965,0
1596289500.0,-10000.0,11688.5,-0.85554177,0.0,0.00855542,116.62223,0.0,0.01070909,0
1596291300.0,-10000.0,11688.5,-0.85554177,0.0,0.00855542,116.73095,0.0,0.00508046,0
・・・

・balances.csv:残高履歴

<2021/04/27>
v2.2アップデートで未実現損益、価格、履歴種別などより詳細な履歴データを出力するように変更しました。

timestamp,balance,unrealised_pnl,price,type
1596240000.0,1.0,0.0,11361.0,Stamp
1596286800.0,0.99935834,-3.66e-05,11689.0,Trade
1596288600.0,0.99944539,-3.66e-05,11487.5,Funding
1596288600.0,0.99944539,0.01496965,11487.5,Stamp
・・・


5. チャート連携データ

終了処理(terminate())の戻り値のdictの'chart'にはチャート作成用の各種データ(DataFrame形式)が含まれています。
各DataFrameはtimestamp列を持つ時系列データとなっているため、スムーズに各種チャート作成ライブラリなどに展開できます。

以下はサンプルとして時系列データ可視化ツール「ChartCreator」を使用した結果チャートを作成するコードになります。
※ ChartCreatorは本ツールに同梱されていません。

matplotlibなど使い慣れたチャートライブラリがある場合は、そちらにデータをご活用ください。

<2020/10/09>
v2.0アップデートにて分離マージンではロスカットが発生しても残高で取引ができるため、テストを中断せずに継続するようにしました。
それに伴い、ロスカットされたポイントもチャート上に表示できるようにしました。

<2020/11/04>
v2.1アップデートにてBTC/USDなど証拠金や損益が取引通貨(BTC)の場合に決済通貨(USD)単位での損益推移もチャート上に表示できるようにしました。

from chart_creator import ChartCreator as cc

~テストコード~

# テスト終了処理
results = test.terminate()

dict_data = results['data']
dict_statistics = results['statistics']
dict_chart = results['chart']

#---------------------------------------------------------------------------
# レイアウトカスタマイズ
#---------------------------------------------------------------------------
cc.initialize()
cc.settings['title'] = 'Result chart'

#---------------------------------------------------------------------------
# X軸設定
#---------------------------------------------------------------------------
cc.set_xaxis(start=0, end=0, is_unixtime=True)

#---------------------------------------------------------------------------
# メインチャート(ax:0)
#---------------------------------------------------------------------------
cc.add_subchart(ax=0, label="USD", grid=True)

# ローソクバー設定(OHLCV)
cc.set_ohlcv_df(df_ohlcv)  # ohlcvはresultsに含まれませんので別で取得

# 買い注文ポイント設定
cc.set_marker(dict_chart['buy']['timestamp'], dict_chart['buy']['price'], ax=0, color='blue', size=30.0, mark='^', name='Buy')

# 売り注文ポイント設定
cc.set_marker(dict_chart['sell']['timestamp'], dict_chart['sell']['price'], ax=0, color='red', size=30.0, mark='v', name='Sell')

# 清算注文ポイント設定
if len(dict_chart['liquid']['timestamp']) > 0:
   cc.set_marker(dict_chart['liquid']['timestamp'], dict_chart['liquid']['price'], ax=0, color='magenta', size=30.0, mark='D', name='Liquidation')

#---------------------------------------------------------------------------
# ポジション推移サブチャート(ax:1)
#---------------------------------------------------------------------------
cc.add_subchart(ax=1, label="Position", grid=True)

# ポジション設定
cc.set_band(dict_chart['position']['unixtime'], dict_chart['position']['size'], dict_chart['position']['zero'], ax=1, up_color='blue', down_color='red',
           alpha=0.5, edge_width=1.0, edge_color='dimgray', line_mode='hv', name='Position')

#---------------------------------------------------------------------------
# 損益推移サブチャート(ax:2)
#---------------------------------------------------------------------------
cc.add_subchart(ax=2, label='PnL', grid=True)

# 損益設定
cc.set_line(dict_chart['pnl']['unixtime'], dict_chart['pnl']['unrealised_pnl'], ax=2, color='green', width=0.8, name='Unrealised')
cc.set_band(dict_chart['pnl']['unixtime'], dict_chart['pnl']['pnl'], dict_chart['pnl']['zero'], ax=2, up_color='skyblue', down_color='red',
           alpha=0.2, edge_width=1.0, edge_color='dimgray', name='PL')

#---------------------------------------------------------------------------
# フィアット損益推移サブチャート(ax:3) ※インバース型のみ
#---------------------------------------------------------------------------
cc.add_subchart(ax=3, label='Fiat PnL', grid=True)

# フィアット損益設定
fiat_blc = dict_chart['pnl']['balance'] * dict_chart['pnl']['price']
fiat_start = fiat_blc[0]
fiat_blc = fiat_blc - fiat_start
fiat_unrel_blc = dict_chart['pnl']['unrealised_balance'] * dict_chart['pnl']['price']
fiat_unrel_blc = fiat_unrel_blc - fiat_start
cc.set_line(dict_chart['pnl']['unixtime'], fiat_unrel_blc, ax=3, color='green', width=0.8, name='Include unrealised')
cc.set_band(dict_chart['pnl']['unixtime'], fiat_blc, dict_chart['pnl']['zero'],
           ax=3, up_color='orange', down_color='red',
           alpha=0.2, edge_width=1.0, edge_color='dimgray', name='Balance')

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

3段目サブチャートはBTCベースの損益 (取引通貨=Margin)
4段目サブチャートはUSDベースの損益 (決済通貨=Fiat)

画像7

ロスカットが発生したポイントは紫◆でプロットされます。

画像6

<2021/07/13>
v3.0アップデートにて損益や建玉の推移を簡易チャートで出力する機能を追加しました。これにより統計情報を容易に可視化できるようになります。
(サマリされたチャートのため、約定ポイント表示などはできません。
より詳細な可視化が必要な場合はこれまで同様ChartCreatorをご使用ください。)
terminate関数の後にsave_result_chart関数にてチャート出力できます。

# 終了処理
results = test.terminate()

# 結果チャート出力
file_path = 'result_chart.png'
test.save_result_chart(file_path, tick_unit=3600, result_type='pnl',
                      show_price=True, price_type='ohlcv', show_pos=True, show_fiat=True)

画像12


6. サンプルテストコード

以下に2つのサンプルロジックを示します。

【サンプルロジック1】はRSIを指標にして定期的に条件判定を行い、成行注文のみでエントリーとイグジットを繰り返すシンプルなロジックです。
テスト構築のイメージと理解を深めることができると思います。

【サンプルロジック2】は、シンプルなMarket Makeロジックです。
指値逆指値の活用、ナンピン可変ロットなどを使った応用編で少し複雑になります。
こちらを理解できれば、本ツールを使用した様々なテストを行うことができるようになると思います。
(botとしてはそこまで難しいコードではないのでbotterの方は容易に理解できると思います。)

※ わかっているとは思いますが、サンプルロジックはBacktestAssistantの使用方法の解説を目的としており、利益が上がるようなものではありませんのでご注意ください。

【サンプルロジック 1】
 RSIから売買過熱を判断して逆張りする
 対象データ    :BitMEX OHLCV (1h)
 テスト期間    :2020/08/01~2020/08/31 (JST)
 ロジック概要
  ・RSIが20未満で買いエントリー / 80より大きいと売りエントリー
  ・RSIが50に到達したらイグジット
  ・毎時、上記判定を繰り返す

# coding: utf-8
import requests
from datetime import datetime
from collections import OrderedDict
import numpy as np
import pandas as pd
from backtest_assistant import BacktestAssistant
 #テスト期間 
TEST_START = '2020/08/01 09:00:00+0900'
TEST_END   = '2020/08/31 09:00:00+0900'

#-------------------------------------------------------------------------------
# テスト前準備
#-------------------------------------------------------------------------------
dt_start = datetime.strptime(TEST_START, '%Y/%m/%d %H:%M:%S%z')
dt_end = datetime.strptime(TEST_END, '%Y/%m/%d %H:%M:%S%z')
ut_start = dt_start.timestamp()
ut_end = dt_end.timestamp()

# 1h OHLCV取得
url = 'https://www.bitmex.com/api/udf/history'
params = {
   'symbol':'XBTUSD',
   'resolution': '60',
   'from':ut_start - 3600 * 24,
   'to':ut_end,
}
res = requests.get(url, params, timeout=10)
data = res.json()

df_ohlcv = pd.DataFrame(
   OrderedDict(
       unixtime=data['t'], open=data['o'], high=data['h'], low=data['l'], close=data['c'], volume=data['v']
   )
)

# RSI計算
rsi_term = 12
diff = df_ohlcv['close'].diff()
diff = diff[1:]
up, down = diff.copy(), diff.copy()
up[up < 0] = 0
down[down > 0] = 0
up_sma = up.rolling(window=rsi_term, center=False).mean()
down_sma = down.abs().rolling(window=rsi_term, center=False).mean()
rs = up_sma / down_sma
df_ohlcv['rsi'] = 100.0 - (100.0 / (1.0 + rs))

# OHLCVをテスト期間でfiltering
df_ohlcv = df_ohlcv[((df_ohlcv['unixtime'] >= ut_start) & (df_ohlcv['unixtime'] <= ut_end))]

np_ut = df_ohlcv['unixtime'].values
np_open = df_ohlcv['open'].values
np_rsi = df_ohlcv['rsi'].values

#-------------------------------------------------------------------------------
# テスト実行
#-------------------------------------------------------------------------------
start_time    = np_ut[0]
start_price   = np_open[0]
start_balance = 1.0
leverage      = 5
lot           = 10000
rsi_line      = 20

# BacktestAssistant生成
test = BacktestAssistant(
           balance=start_balance,
           trade_leverage=leverage,
           tick_mode='ltp',
           take_cost=0.5)

# tickデータプリセット
test.preset_ltp_ticks_from_ohlcv_df(df_ohlcv)

# Funding Rate設定
test.set_funding_rate(rate=0.01)

# 初期処理
test.initialize(start_time=start_time)

# テストループ
for i, t in enumerate(np_ut):

   # 指定時刻まで進める
   if test.time() < t:
       test.sleep_to_time(t)

   # 直前足RSI
   rsi = np_rsi[i-1] if i > 0 else 50

   # 建玉取得
   pos = test.get_position()

   # ノーポジション
   if pos['size'] == 0:
       # 買われ過ぎ
       if rsi > 100 - rsi_line:
           res = test.market_order('Sell', lot)
           if res['status_code'] == 400:
               print(res['message'])
       # 売られ過ぎ
       elif rsi < rsi_line:
           res = test.market_order('Buy', lot)
           if res['status_code'] == 400:
               print(res['message'])

   # ロングポジション
   elif pos['size'] > 0:
       # 売られ過ぎ解消
       if rsi > 50:
           res = test.market_order('Sell', abs(pos['size']))
           if res['status_code'] == 400:
               print(res['message'])

   # ショートポジション
   elif pos['size'] < 0:
       # 買われ過ぎ解消
       if rsi < 50:
           res = test.market_order('Buy', abs(pos['size']))
           if res['status_code'] == 400:
               print(res['message'])

# 建玉があればクローズ
pos = test.get_position()
if pos['size'] < 0:
   test.market_order('Buy', abs(pos['size']))
elif pos['size'] > 0:
   test.market_order('Sell', abs(pos['size']))

# 終了処理
results = test.terminate()

# 結果チャート出力
file_path = 'result_chart.png'
test.save_result_chart(file_path, tick_unit=3600, result_type='pnl',
                      show_price=True, price_type='ohlcv', show_pos=True, show_fiat=True)

<2020/11/04>
v2.1アップデートにてBTC/USDなど証拠金や損益が取引通貨(BTC)の場合に決済通貨(USD)単位での損益も表示できるようにしました。

<2021/04/27>
v2.2アップデートにより、最大DD/含み損情報が追加されました。
統計情報に最大リスクを設け、上記を表示するように変更しました。
また、フィアットベースの統計情報も表示するように変更しました。

<2021/07/13>
v3.0アップデートにてシャープレシオを追加しました。
テスト期間の残高をsharpe_ratio_unit単位に取得し、その変動率からシャープレシオを計算しています。

[Result]
 [Time    ] 2020/08/01 09:00:00 - 2020/08/31 09:00:00
 [Balance ] 1.0 -> 1.0201 (+2.01%)  Sharpe Ratio:-0.0463
 [Orders  ] Total:28 (Open:0  Executed:28  Canceled:0)
 [Position] Max Long:10,000.0  Max Short:-10,000.0  Min Margin:11115.76%

[Execution statistics]  PF:1.58
 [Total   ] Count:28(Size:280,000)  PnL:+0.0354  Avr:+0.0013  Price Range:+497.5
 [Profit  ] Count:6(42.86%)  Sum:+0.096  Avr:+0.016  Max:+0.0388  MaxLen:2(+0.0464)
 [Loss    ] Count:8(57.14%)  Sum:-0.0607  Avr:-0.0076  Max:-0.0181  MaxLen:3(-0.0261)
 [Fee     ] Trade:0.018  Funding:-0.0027
 [Max risk]
   Drawdown       :-2.79%(-0.0293) 2020/08/31 00:00:00
   Unrealised loss:-4.28%(-0.0449) 2020/08/12 15:30:00

[Fiat balance]
 [Start ] 11,361.0 (Balance:1.0 Price:11361.0)
 [End   ] 11,860.7 (Balance:1.0201 Price:11627.0)
 [Result] 499.7 (+4.40%)  Sharpe Ratio:-0.2182

[Fiat execution statistics]  PF:1.08
 [Total   ] Count:28(Size:0)  PnL:+179  Avr:+6.0  Price Range:+0
 [Profit  ] Count:11(42.31%)  Sum:+2,481  Avr:+226.0  Max:+678  MaxLen:3(+176)
 [Loss    ] Count:15(57.69%)  Sum:-2,302  Avr:-153.0  Max:-939  MaxLen:3(-520)
 [Fee     ] Trade:0  Funding:0
 [Max risk]
   Drawdown       :-8.87%(-1,138) 2020/08/26 14:00:00
   Unrealised loss:-4.28%(-497) 2020/08/12 15:30:00

画像7

<2021/07/13>
v3.0アップデートにて損益や建玉の推移を簡易チャートで出力する機能を追加しました。これにより統計情報を容易に可視化できるようになります。
(サマリされたチャートのため、約定ポイント表示などはできません。
より詳細な可視化が必要な場合はこれまで同様ChartCreatorをご使用ください。)
terminate関数の後にsave_result_chart関数にてチャート出力できます。

画像13


【サンプルロジック 2】
 現在価格の上下に複数の売買指値を発行し、
 値動きの上下動とメイカー手数料を積み重ねる
 対象データ    :bybit OHLCV (1sec)
 テスト期間    :2020/09/06~2020/09/07 (JST)
 ロジック概要
  ・LTP±$1(Lot×1) / ±$50(Lot×3) / ±$100(Lot×5)にBuy / Sell指値
  ・建玉を持った場合、建玉と逆方向の指値をキャンセル
   ・建玉の平均価格から利益方向$20利確指値
   ・建玉の平均価格から損失方向$100損切逆指値
  ・20sec毎に上記判定を繰り返す

# coding: utf-8
from datetime import datetime
import numpy as np
import pandas as pd
from backtest_assistant import BacktestAssistant
 #テスト期間 
TEST_START = '2020/09/06 09:00:00+0900'
TEST_END   = '2020/09/07 09:00:00+0900'

#-------------------------------------------------------------------------------
# テスト前準備
#-------------------------------------------------------------------------------
dt_start = datetime.strptime(TEST_START, '%Y/%m/%d %H:%M:%S%z')
dt_end = datetime.strptime(TEST_END, '%Y/%m/%d %H:%M:%S%z')
ut_start = dt_start.timestamp()
ut_end = dt_end.timestamp()

# bybit約定履歴取得(bybitは公式サイトで日別の約定履歴データを提供しています)
df_trade = pd.read_csv(f'https://public.bybit.com/trading/BTCUSD/BTCUSD{dt_start:%Y-%m-%d}.csv.gz',
                compression='gzip',
                usecols=['timestamp', 'side', 'price', 'size'],
                dtype={'timestamp':'float', 'side':'str', 'price':'float', 'size':'int'})

df_trade.rename(columns={'timestamp': 'unixtime'}, inplace=True)
df_trade['datetime'] = pd.to_datetime(df_trade['unixtime'], unit='s', utc=True)
df_trade.set_index('datetime', inplace=True)

# 約定履歴から1sec OHLCV作成
df_ohlcv = df_trade.resample('1S').agg({
                   'price' : 'ohlc',
                   'size'  : 'sum',}).ffill()
df_ohlcv.columns = ['open', 'high', 'low', 'close', 'volume']
df_ohlcv['unixtime'] = df_ohlcv.index.astype(np.int64) / 10**9
df_ohlcv = df_ohlcv[['unixtime', 'open', 'high', 'low', 'close', 'volume']]

# テスト期間でFiltering
df_ohlcv = df_ohlcv[((df_ohlcv.index >= dt_start) & (df_ohlcv.index < dt_end))]

np_ut = df_ohlcv['unixtime'].values
np_open = df_ohlcv['open'].values

#-------------------------------------------------------------------------------
# テスト実行
#-------------------------------------------------------------------------------
start_time    = np_ut[0]
end_time      = np_ut[-1]
start_price   = np_open[0]
start_balance = 1.0
leverage      = 5
lot           = 1000

# BacktestAssistant生成
test = BacktestAssistant(
           balance=start_balance,
           trade_leverage=leverage,
           tick_mode='ltp',
           take_cost=0.5)

# tickデータプリセット
test.preset_ltp_ticks_from_ohlcv_df(df_ohlcv)

# Funding Rate設定
test.set_funding_rate(rate=0.01)

# 初期処理
test.initialize()

# テストループ
while test.time() <= end_time:

   price  = test.get_price()['ltp'] # 現在価格
   pos    = test.get_position()     # 建玉
   orders = test.get_open_orders()  # アクティブ注文

   # ノーポジション
   if pos['size'] == 0:
       # 全注文キャンセル
       test.cancel_order()
       # 買い指値注文
       test.limit_order('Buy', price - 1, lot)
       test.limit_order('Buy', price - 50, lot * 3)
       test.limit_order('Buy', price - 100, lot * 5)
       # 売り指値注文
       test.limit_order('Sell', price + 1, lot)
       test.limit_order('Sell', price + 50, lot * 3)
       test.limit_order('Sell', price + 100, lot * 5)

   # ロングポジション
   elif pos['size'] > 0:
       # 売り注文キャンセル
       for o in orders:
           if o['side'] == 'Sell':
               test.cancel_order(o['id'])
       # 建玉の平均単価+$20を利確価格に設定
       tp = max(int(pos['avr_entry']) + 20, price + 0.5)
       test.limit_order('Sell', tp, abs(pos['size'])) # 利確指値
       # 建玉の平均単価-$100を損切価格に設定
       sl = int(pos['avr_entry']) - 100
       if price <= sl:
           test.market_order('Sell', abs(pos['size'])) # 損切成行
       else:
           test.stop_order('Sell', sl, abs(pos['size'])) # 損切逆指値

   # ショートポジション
   else:
       # 買い注文キャンセル
       for o in orders:
           if o['side'] == 'Buy':
               test.cancel_order(o['id'])
       # 建玉の平均単価-$20を利確価格に設定
       tp = min(int(pos['avr_entry']) - 20, price - 0.5)
       test.limit_order('Buy', tp, abs(pos['size'])) # 利確指値
       # 建玉の平均単価+$100を損切価格に設定
       sl = int(pos['avr_entry']) + 100
       if price >= sl:
           test.market_order('Buy', abs(pos['size'])) # 損切成行
       else:
           test.stop_order('Buy', sl, abs(pos['size'])) # 損切逆指値

   # 20sec待機
   test.sleep(20)

# 全注文キャンセル
test.cancel_order()
# 建玉取得
pos = test.get_position()

# 最後にポジションクローズ
if pos['size'] < 0:
   test.market_order('Buy', abs(pos['size']))
elif pos['size'] > 0:
   test.market_order('Sell', abs(pos['size']))

# 終了処理
results = test.terminate()

# 結果チャート出力
file_path = 'result_chart2.png'
test.save_result_chart(file_path, tick_unit=300, result_type='pnl',
                      show_price=True, price_type='ohlcv', show_pos=True, show_fiat=True)

<2020/11/04>
v2.1アップデートにてBTC/USDなど証拠金や損益が取引通貨(BTC)の場合に決済通貨(USD)単位での損益も表示できるようにしました。

<2021/04/27>
v2.2アップデートにより、最大DD/含み損情報が追加されました。
統計情報に最大リスクを設け、上記を表示するように変更しました。
また、フィアットベースの統計情報も表示するように変更しました。

<2021/07/13>
v3.0アップデートにてシャープレシオを追加しました。
テスト期間の残高をsharpe_ratio_unit単位に取得し、その変動率からシャープレシオを計算しています。

[Result]
 [Time    ] 2020/09/06 09:00:00 - 2020/09/07 09:00:00
 [Balance ] 1.0 -> 1.0843 (+8.43%)  Sharpe Ratio:0.0000
 [Orders  ] Total:8909 (Open:0  Executed:121  Canceled:8788)
 [Position] Max Long:45,000.0  Max Short:-45,000.0  Min Margin:2210.36%

[Execution statistics]  PF:2.25
 [Total   ] Count:121(Size:1,230,000)  PnL:+0.0582  Avr:+0.0005  Price Range:+751.4
 [Profit  ] Count:48(96.00%)  Sum:+0.1047  Avr:+0.0022  Max:+0.0089  MaxLen:34(+0.0764)
 [Loss    ] Count:2(4.00%)  Sum:-0.0466  Avr:-0.0233  Max:-0.0465  MaxLen:1(-0.0465)
 [Fee     ] Trade:-0.0252  Funding:-0.0009
 [Max risk]
   Drawdown       :-4.81%(-0.0499) 2020/09/06 14:09:15
   Unrealised loss:-4.22%(-0.0438) 2020/09/06 14:09:12

[Fiat balance]
 [Start ] 10,158.0 (Balance:1.0 Price:10158.0)
 [End   ] 11,116.2 (Balance:1.0843 Price:10252.0)
 [Result] 958.2 (+9.43%)  Sharpe Ratio:0.0000

[Fiat execution statistics]  PF:1.85
 [Total   ] Count:121(Size:0)  PnL:+955  Avr:+8.0  Price Range:+0
 [Profit  ] Count:79(66.95%)  Sum:+2,075  Avr:+26.0  Max:+149  MaxLen:9(+348)
 [Loss    ] Count:39(33.05%)  Sum:-1,120  Avr:-29.0  Max:-577  MaxLen:3(-668)
 [Fee     ] Trade:0  Funding:0
 [Max risk]
   Drawdown       :-6.33%(-668) 2020/09/06 14:09:15
   Unrealised loss:-4.22%(-438) 2020/09/06 14:09:12

画像7

<2021/07/13>
v3.0アップデートにて損益や建玉の推移を簡易チャートで出力する機能を追加しました。これにより統計情報を容易に可視化できるようになります。
(サマリされたチャートのため、約定ポイント表示などはできません。
より詳細な可視化が必要な場合はこれまで同様ChartCreatorをご使用ください。)
terminate関数の後にsave_result_chart関数にてチャート出力できます。

画像14


長くなるため省略しますが、サンプルロジック2をtick_mode='bidask'で実施したサンプルコードも同梱します。
(テスト期間やパラメータは一部変えています。)

画像7

<2020/10/09>
v2.0アップデートにて契約タイプ(contract_type)をインバース型/リニア型から選択できるようにしました。
これにより、様々な通貨ペア、アセットにてバックテストが行えるようになります。
その使い方サンプルとして上記サンプルロジック1,2についてbitFlyer FXBTCJPY版サンプルコードも同梱します。


以下は、より実践的なバックテストコードになります。
様々な公開ロジックを基に
・テスト前準備(データ取得&加工)
・テスト作成
・テスト結果確認(出力データ、チャート)

一連の処理をnotebook形式(Google Colab)で解説します。

【平均様 バックテスト】
れたすさん著 【BTC自動取引bot】HEIKIN_ORACLE_ver2.0

【時刻アノマリー バックテスト】
Hohetoさん著 ビットコイン価格における時刻アノマリーの存在

【ドテンくんバックテスト】
ビットコイン自動売買bot「ドテンくん」の特徴と評判
※ドテンくんbotはAKAGAMIさんが公開していましたが、現在noteが非公開のため、解説サイトになります。

【バブル相場用の回転ボット バックテスト】
Hohetoさん著 バブル相場用の回転ボットのコンセプトとQuantZoneロジック


7. BacktestAssistantに含まれるもの

■ backtest_assistant.py       :BacktestAssistant本体
■ backtest_sample1.py       :6.サンプルロジック1
■ backtest_sample2.py       :6.サンプルロジック2
■ backtest_sample2_bidask:6.サンプルロジック2のbidaskバージョン
■ backtest_sample1_for_bf.py:6.サンプルロジック1のbitFlyerバージョン
■ backtest_sample2_for_bf.py:6.サンプルロジック2のbitFlyerバージョン
■ requirements.txt              :必要パッケージリスト

note内に添付しているチャートは
時系列データ可視化ツール「ChartCreator」
を使用して作成していますが、本ツールには含まれません。

本ツールではChartCreatorへの連携を考慮したデータ出力を行っていますので、手軽に可視化したい場合は合わせてご検討ください。


8. リリースノート

----------------------------------------------------------------------------
[2021/07/13] v3.0
【バックテスト結果チャート出力】
バックテスト完了後に損益や建玉の推移を簡易チャートで出力する機能を追加しました。これにより統計情報を容易に可視化できるようになります。
(サマリされたチャートのため、約定ポイント表示などはできません。
より詳細な可視化が必要な場合はこれまで同様ChartCreatorをご使用ください。)
terminate関数の後にsave_result_chart関数にてチャート出力できます。

# 終了処理
results = test.terminate()

# 結果チャート出力
file_path = 'result_chart.png'
test.save_result_chart(file_path, tick_unit=3600, result_type='pnl',
                       show_price=True, price_type='ohlcv', show_pos=True, show_fiat=True)

画像9

【参考】ChartCreatorを使用した詳細チャート

画像10

【統計情報にシャープレシオを追加
バックテスト結果の統計情報にシャープレシオを追加しました。
損益etc.と合わせて安定性の評価にご活用ください。

画像11

----------------------------------------------------------------------------
[2021/05/12] v2.3
【バックテスト高速化】
コードのリファクタリング、JIT導入などにより、処理時間を約20~40%削減しました。
『重要』
使用ライブラリに「numba, sortedcontainers」が追加となります。
「requirements.txt」または以下より、追加ライブラリのインストールを行ってください。

pip install -U numba
pip install -U sortedcontainers

バブル相場用の回転ボットのバックテストコード解説】
公開ロジックでの実践的な解説に「バブル相場用の回転ボット」を追加しました。

----------------------------------------------------------------------------
[2021/04/27] v2.2
【Funding rate事前設定に対応】
時系列Funding rateを初期処理前に設定することで自動的に時刻に応じたFunding rateが適用されます。

【統計情報にリスク情報/フィアット情報表示】
統計情報に最大リスクを設け、上記を表示するように変更しました。
また、フィアットベースの統計情報も表示するように変更しました。

[Result]
 [Time    ] 2020/09/06 09:00:00 - 2020/09/07 09:00:00
 [Balance ] 1.0 -> 1.0843 (+8.43%)
 [Orders  ] Total:8909 (Open:0  Executed:121  Canceled:8788)
 [Position] Max Long:45,000.0  Max Short:-45,000.0  Min Margin:2210.36%

[Execution statistics]  PF:2.25
 [Total   ] Count:121(Size:1,230,000)  PnL:+0.0582  Avr:+0.0005  Price Range:+751.4
 [Profit  ] Count:48(96.00%)  Sum:+0.1047  Avr:+0.0022  Max:+0.0089  MaxLen:34(+0.0764)
 [Loss    ] Count:2(4.00%)  Sum:-0.0466  Avr:-0.0233  Max:-0.0465  MaxLen:1(-0.0465)
 [Fee     ] Trade:-0.0252  Funding:-0.0009
 [Max risk]
   Drawdown       :-4.81%(-0.0499) 2020/09/06 14:09:15
   Unrealised loss:-4.22%(-0.0438) 2020/09/06 14:09:12

[Fiat balance]
 [Start ] 10,158.0 (Balance:1.0 Price:10158.0)
 [End   ] 11,116.2 (Balance:1.0843 Price:10252.0)
 [Result] 958.2 (+9.43%)

[Fiat execution statistics]  PF:1.85
 [Total   ] Count:121(Size:0)  PnL:+955  Avr:+8.0  Price Range:+0
 [Profit  ] Count:79(66.95%)  Sum:+2,075  Avr:+26.0  Max:+149  MaxLen:9(+348)
 [Loss    ] Count:39(33.05%)  Sum:-1,120  Avr:-29.0  Max:-577  MaxLen:3(-668)
 [Fee     ] Trade:0  Funding:0
 [Max risk]
   Drawdown       :-6.33%(-668) 2020/09/06 14:09:15
   Unrealised loss:-4.22%(-438) 2020/09/06 14:09:12

----------------------------------------------------------------------------
[2020/11/04] v2.1
【証拠金が取引通貨(BTC)の場合に決済通貨(USD)単位の損益推移に対応】
BTC/USDなど(契約タイプ:インバース型)は証拠金が取引通貨(BTC)のため、証拠金自体の値動きによる資産変動が生じます。
フィアット(USD)単位の資産や損益を把握できるように、バックテスト結果([Fiat balance])とチャートにフィアット損益(3段目サブチャート)を追加しました。

[Result]
 [Time    ] 2020/09/06 09:00:00 - 2020/09/07 09:00:00
 [Balance ] 1.0 -> 1.0843 (+8.43%)
 [Orders  ] Total:8909 (Open:0  Executed:121  Canceled:8788)
 [Position] Max Long:45,000.0  Max Short:-45,000.0  Min Margin:2210.36%

[Execution statistics]  PF:2.25
 [Total   ] Count:121(Size:1,230,000)  PnL:+0.0582  Avr:+0.0005  DD:-0.0465(-4.52%)  Price Range:+751.4
 [Profit  ] Count:48(96.00%)  Sum:+0.1047  Avr:+0.0022  Max:+0.0089  MaxLen:34(+0.0764)
 [Loss    ] Count:2(4.00%)  Sum:-0.0466  Avr:-0.0233  Max:-0.0465  MaxLen:1(-0.0465)
 [Fee     ] Trade:-0.0252  Funding:-0.0009

[Fiat balance]
 [Start ] 10,158.0 (Balance:1.0 Price:10158.0)
 [End   ] 11,116.2 (Balance:1.0843 Price:10252.0)
 [Result] 958.2 (+9.43%)

画像8

----------------------------------------------------------------------------
[2020/10/09] v2.0
【BTC/USD以外の様々な通貨ペア・アセットに対応
契約タイプ(contract_type)をインバース型/リニア型から選択できるようになったため、BTC/JPYや他アセットにも柔軟に対応できるようになりました。
 ■ インバース型:証拠金=取引通貨、注文サイズ=決済通貨
                             (例)BTC/USD
                                    取引通貨:BTC、決済通貨:USD
                                    証拠金:BTC、注文サイズ:USD
 ■ リニア型       :証拠金=決済通貨、注文サイズ=取引通貨
                             (例)BTC/JPY
                                    取引通貨:BTC、決済通貨:JPY
                                    証拠金:JPY、注文サイズ:BTC
※ 詳細は「3. 機能解説 (関数一覧)」のコンストラクタ参照

【レバレッジ取引のクロスマージン・分離マージン対応】
これまでは全残高を証拠金として取引するクロスマージンのみでしたが、取引レバレッジの指定方法により、分離マージンと切り替えができるようになりました。
 ■ 取引レバレッジ=0            :クロスマージン
  全残高を証拠金として使用し、最大レバレッジで取引します。
  少ない証拠金で大きな取引ができますが、
  ロスカットでは残高のほぼ全てを失うリクスがあります。
 ■ 取引レバレッジ=1〜Max:分離マージン
  注文サイズを指定したレバレッジで証拠金を割り当てて取引します。
  残高の一部を分離して証拠金とするため、
  ロスカットされても残った残高で取引を継続できます。
※ 詳細は「3. 機能解説 (関数一覧)」のコンストラクタ参照

【証拠金および維持率、ロスカット判定の精度向上】
クロスマージン・分離マージンの対応に合わせて証拠金の計算方法や注文判定、ロスカット判定などを見直したことで、判定精度が向上しました。
それに伴い、建玉履歴CSVでは証拠金内訳、レバレッジなどより詳細な履歴データを出力するようになりました。

【チャート連携データにロスカットデータを追加】
分離マージンではロスカットが発生しても残高で取引が継続できます。
そのため、ロスカットされたポイントが把握できるようにチャート連携データに精算データ(liquid)を追加しました。
ChartCreatorサンプルではロスカットポイントを紫◆で表示しています。

画像7

【bitFlyer FXBTCJPYサンプルコード追加】
契約タイプ(contract_type)の選択が可能となったため、リニア型のサンプルとしてbitFlyer FXBTCJPYのデータにてサンプルロジック1,2をテストしたpythonスクリプトファイルを新たに同梱しました。

【ドテンくんのバックテストコード解説】
公開ロジックでの実践的な解説に「ドテンくん」を追加しました。

【バグフィックス】
パフォーマンス向上、細かなバグフィックスを行いました。

----------------------------------------------------------------------------
[2020/09/30] v1.1
【テスト前処理の高速化・省メモリ化によるパフォーマンス向上】
OHLCV→tickデータ変換を改善することで全体として
・20〜30%高速化
・使用メモリ50〜60%削減

※ テストデータ・設定によりますのであくまで参考となります。

【実践ロジックでのバックテストコード解説】
ロジック公開されている「平均様」「時刻アノマリー」についてデータ取得からバックテスト作成テスト結果確認まで一連の流れに沿った実践的な解説をnotebook形式で公開しました。

【バグフィックス】
sleep/sleep_to_time関数 実行時にtick間の時刻を指定すると価格にずれが生じる場合がある不具合を修正しました。

----------------------------------------------------------------------------
[2020/09/24] v1.0
初版リリース
----------------------------------------------------------------------------


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

ここから先は

310字

¥ 5,000

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