PythonでFXのランダムウォーク検証を高速化

PythonでのFXのランダムウォークを検証で作成した、「2017年の1年間分のヒストリカルデータを使って、毎時0分に常に買いのオーダーを入れたら勝率5割になるか?」と検証したプログラムを高速化しました。

高速化にあたって、手法をPythonの高速化のまとめ Ver.1にまとめています。

結果、PythonでのFXのランダムウォークを検証では3分かかっていた1年間分のシミュレーションが、0.1秒で終わるようになりました。なんと、1,500倍はやくなったことになります。こんなに効果があるので、逆に言えば、Pyhtonではちゃんと考えて書かないと、無駄に遅いプログラムを書いてしまいやすいといえます。

過去バージョンのプログラムでの計測

まず、PythonでのFXのランダムウォークを検証のプログラムの計測結果は、

  1. 売買フラグを立てるロジック33秒くらい
  2. 売買シミュレーション150秒くらい

だったので、合計3分かかっていました

高速化のポイント

Pythonの高速化のまとめ Ver.1から抜粋。

  • Forなどループは書かない。
  • シンプルなオブジェクトで扱う
  • どうしてもループするのであればnumbaでコンパイル

高速化:売買フラグを立てるロジック

汎用化を考えて、クロージャを作り、ヒストリカルデータをループしていくように作ってましが、汎用化は考えないようにします。とにかくループを回さなくても売買タイミングを計算できるように、各アルゴリズムを実装時に工夫していきます。

例えば、「PythonでのFXのランダムウォークを検証」では毎時0分に、常に買いのポジションを取るだけなので、下記のようにループを回さずともフィルタで計算できます。

buys = ticks.index.minute == 0    #array([ True, False, False, ..., False, False, False])

ticksはDataFrameでインデックスに時刻(datetime64)のシリーズDateTimeIndexになっているので、minuteで分のndarrayが取り出せます。==0で、True or Flaseの配列になり、Trueの位置が分が0ということになります。

このステートメントであれば、0.03秒で処理が終わりました。ループしないことで、33秒が0.03で、約1000倍早くなりました。

今回は毎時0分というシンプルなアルゴなので、上記のようなシンプルなステートメントで実装できたかもしれませんが、移動平均のゴールデンクロス・デッドクロスなどの他のアルゴリズムも上記のようにループを書かずに実装していく工夫をしていきます。とにかく、高速化に向けてはループを書かないよう工夫することが思っています。

もはや汎用化ではないくらいの汎用化ですが、決めごとは、買いや売りのタイミングをBooleanのndarrayで作成するというだけです。後続の注文のシミュレーションではnumbaを使う予定なので、シンプルなオブジェクトで保持するようにしました。

高速化:売買シミュレーション

複雑になってしまったので、ソースは最後に貼ります。

ポイントは、どうしてもループとIF分が必要だったので、numbaでコンパイルする部分を作りました。numbaに外出しするために、DataFrameからndarrayに変換して、それを受け取ってループしていく処理を外出ししています。

結果、0.09秒で処理できるようになりました。もともと150秒かかっていたので、約1500倍速くなりました。

長いですが、参考までにソースを貼り付けておきます。_fast_ordering_simulationがnumbaでコンパイルする関数です。

import numpy as np
from pandas import DataFrame
from numba import jit

"""
"""
def ordering_simulation(ticks, buy_flags, sell_flags, order_options):

    # ヒストリカルデータから判定用の最高値、安値を取り出す
    ticks_high = ticks.high.values.round(order_options.max_digit)
    ticks_low  = ticks.low.values.round(order_options.max_digit)

    # ヒストリカルデータと売買フラグから注文履歴の初期化
    buy_order = _create_order(ticks.open[buy_flags], 1, order_options)
    sell_order = _create_order(ticks.open[sell_flags], -1, order_options)
    order_history = buy_order.append(sell_order).sort_index()

    ## close_time, close_price, profitをここから作成

    # シミュレーションに必要な情報作成
    ## 注文の買い(1)or売り(-1)のフラグ
    order_pos = order_history.index.get_level_values('pos').values
    ## 注文の買い(1)or売り(-1)のフラグ
    order_open_index = np.arange(len(ticks))[buy_flags | sell_flags]
    # シミュレーションの結果を格納するndarrayを作成
    ## 注文の決済価格
    order_close_price = np.zeros(len(order_history), dtype=np.float64)
    ## 注文の決済の配列の位置
    order_close_index = np.full(len(order_history), -1, dtype=np.int64)

    # シミュレーション
    _fast_ordering_simulation(
        ticks_low, ticks_high,
        order_history.stoploss.values, order_history.takeprofit.values,
        order_open_index,
        order_pos,
        order_close_index,
        order_close_price)

    ## close_time
    order_history['close_time'] = ticks.index[order_close_index]
    ## close_price
    order_history['close_price'] = order_close_price
    ## close_priceの補正
    order_history[order_close_index == -1].close_price = ticks.iloc[-1].close
    ## profit
    order_history['profit'] = ((order_history.close_price - order_history.open_price) * order_pos).round(order_options.max_digit)

    return order_history

"""
numbaによる注文シミュレーションの高速化ロジック
"""
@jit('void(float64[:],float64[:],float64[:],float64[:],int32[:],int64[:],int64[:],float64[:])', nopython=True)
def _fast_ordering_simulation(
        ticks_low, ticks_high,              # [in]ヒストリカルデータの高値、安値
        order_stoploss, order_takeprofit,   # [in]注文の上限値、下限値
        order_open_index,                   # [in]注文開始のヒストリカルデータの位置
        order_pos,                          # [in]注文の買い(1) or 売り(-1)
        order_close_index,                  # [out]注文決済のヒストリカルデータの位置
        order_close):                       # [out]注文決済の価格

    # ヒストリカルデータの数
    size_of_ticks = len(ticks_high)

    # 注文の数(代表でインデックスのサイズを使ってるだけ)
    size_of_order = len(order_open_index)

    # 注文でループ
    for index_of_order in range(size_of_order):
        # ヒストリカルデータを注文位置から後ろにループして決済チェック
        for index_of_ticks in range(order_open_index[index_of_order], size_of_ticks):
            #買いオーダー
            if order_pos[index_of_order] == 1:
                # 逆指値 = 損切り
                if order_stoploss[index_of_order] >= ticks_low[index_of_ticks]:
                    order_close_index[index_of_order] = index_of_ticks
                    order_close[index_of_order] = order_stoploss[index_of_order]
                    break
                # 指値 = 利確
                if order_takeprofit[index_of_order] <= ticks_high[index_of_ticks]:
                    order_close_index[index_of_order] = index_of_ticks
                    order_close[index_of_order] = order_takeprofit[index_of_order]
                    break
            #売りオーダー
            elif order_pos[index_of_order] == -1:
                # 逆指値 = 損切り
                if order_stoploss[index_of_order] <= ticks_high[index_of_ticks]:
                    order_close_index[index_of_order] = index_of_ticks
                    order_close[index_of_order] = order_stoploss[index_of_order]
                    break
                # 指値 = 利確
                if order_takeprofit[index_of_order] >= ticks_low[index_of_ticks]:
                    order_close_index[index_of_order] = index_of_ticks
                    order_close[index_of_order] = order_takeprofit[index_of_order]
                    break

"""
注文履歴のデータフレームを作る
"""
def _create_order(ticks_open, pos, options):
    order_history = DataFrame(ticks_open)
    # インデックス
    ## open_time
    order_history.index.names = ['open_time']
    ## pos
    order_history['pos'] = pos
    order_history.set_index('pos', append=True, inplace=True)
    # データ
    ## open_price
    order_history['open_price'] = (order_history.open + options.spread * pos).round(options.max_digit)
    ## stoploss
    order_history['stoploss'] = (order_history.open_price - options.stoploss_w * pos).round(options.max_digit)
    ## takeprofit
    order_history['takeprofit'] = (order_history.open_price + options.takeprofit_w * pos).round(options.max_digit)

    ## close_time, close_price, profitは後で作成
    return order_history

検証まとめ記事へのリンク

システムトレードの他のネタを纏めています。参考になれば嬉しいです。

↓↓↓検証まとめページはこちら↓↓↓
【保存版】PythonでのFXのシステムトレード検証

シェアする

  • このエントリーをはてなブックマークに追加

フォローする