Edited at

日本株システムトレードプラットフォーム QuantX でn日足(終値)を扱うアルゴリズムを実装する


日本株システムトレードプラットフォーム QuantX でn日足(終値)を扱うアルゴリズムを実装する


背景


  • Python による日本株システムトレードプラットフォームである QuantX ではデフォルトでアルゴリズムの実装をする際に、1日足の株価しか取得することができません

  • なので、n日分の株価を扱えるようにしてみようと思い、実装してみました


実装したアルゴリズム

下記がその内容のプログラムとなります。

ちなみに、BETWEEN_DAY の値を n にすることで n日足の終値を取得することができます。

ボリンジャーバンドの上端、下端を跨いだときに売買をするアルゴリズムです。

株価が下がりすぎた時を検出した場合は買い、逆の場合は売り、というのを行っております。

import pandas as pd

import talib as ta
import numpy as np

BETWEEN_DAY = 2

def initialize(ctx):

# 設定
ctx.configure(
target="jp.stock.daily",
channels={ # 利用チャンネル
"jp.stock": {
"symbols": [
'jp.stock.1332', # 日本水産(株)
'jp.stock.1605', # 国際石油開発帝石(株)
],
"columns": [ "close_price_adj",]
}
}
)

def _TALIB_CALL(data):
cp = data["close_price_adj"].fillna(method="ffill")
cp_between_n = pd.DataFrame(data=0,columns=[], index=cp.index)
buy_sig = pd.DataFrame(data=0,columns=[], index=cp.index)
sell_sig = pd.DataFrame(data=0,columns=[], index=cp.index)

tmp = 0
for (sym,val) in cp.items():
count = 0
for [day,val_day] in val.items():
if count % BETWEEN_DAY == 0:
cp_between_n.loc[day,sym] = val_day
else:
cp_between_n.loc[day,sym] = np.nan
count += 1

cp_between_n.dropna(inplace=True)

upperband = {}
middleband = {}
lowerband = {}

rsi = pd.DataFrame(data=0,columns=[], index=cp_between_n.index)
uband = pd.DataFrame(data=0,columns=[], index=cp_between_n.index)
lband = pd.DataFrame(data=0,columns=[], index=cp_between_n.index)

for (sym,val) in cp_between_n.items():
upperband[sym], middleband[sym], lowerband[sym] = ta.BBANDS(
cp_between_n[sym].values.astype(np.double),
timeperiod=10,
nbdevup=1,
nbdevdn=1,
matype=0)

lband[sym] = lowerband[sym]
uband[sym] = upperband[sym]

lband2 = cp.copy()
lband2[:] = 0
uband2 = cp.copy()
uband2[:] = 0
uband2 = uband + uband2
lband2 = lband + lband2

for (sym,val) in cp.items():
buy_sig[sym] = lband2[sym] > cp[sym]
sell_sig[sym] = cp[sym] > uband2[sym]

buy_sig = (buy_sig == True)
sell_sig = ((sell_sig == True) & (buy_sig == False))

return {
"upperband:price": uband2,
"lowerband:price": lband2,
"buy:sig": buy_sig,
"sell:sig": sell_sig,
}

# シグナル登録
ctx.regist_signal("TALIB", _TALIB_CALL)

def handle_signals(ctx, date, current):

done_syms = set([])
for (sym,val) in ctx.portfolio.positions.items():
amount = val["amount"]
if amount > 0:
done_syms.add(sym)

ratio = current["buy:sig"]
df = ratio[ratio == 1.0]
for (sym,val) in df.items():
if sym in done_syms:
continue

sec = ctx.getSecurity(sym)
sec.order_target_percent(0.4, comment="シグナル買(%f)" % val)
pass

ratio = current["sell:sig"]
df = ratio[ratio == 1.0]
for (sym,val) in df.items():

sec = ctx.getSecurity(sym)
sec.order_target_percent(0, comment="シグナル売(%f)" % val)
pass

pass

うん、長い。


解説


  • 上記コードに至るまで、Jupyter 上で模擬データを使って検証したのでそれに沿って解説していきます。


模擬データを準備する

まずは、QuantX で扱われている時系列データを模擬したデータを定義します。

これは、下記のように定義できます。

実際には jp.stock.XXXXXXXX が銘柄コードとなります。

import pandas as pd

import numpy as np

# indexに名前を指定しています
cp = pd.DataFrame([
[2000, 2015],
[2096, 2018],
[2066, 2034],
[2076, 2054],
[2026, 2074],
[2076, 2044]],
index = ['2015-01-05 ', '2015-01-06', '2015-01-07', '2015-01-08', '2015-01-09', '2015-01-12', '2015-01-13'],
columns=["jp.stock.XXXX",'jp.stock.YYYY']
)
#cp




jp.stock.XXXX
jp.stock.YYYY




2015-01-05
2000
2015


2015-01-06
2024
2025


2015-01-07
2096
2018


2015-01-08
2066
2034


2015-01-09
2076
2054


2015-01-12
2026
2074


2015-01-13
2076
2044


n日毎のデータとして整形する

次に、このデータセットを n 日ごとに整形するようにします。

ここでは、2日として、処理します。

BETWEEN_DAY = 2

cp_ndays = pd.DataFrame(data=0,columns=[], index=cp.index)

tmp = 0
for (sym,val) in cp.items():
count = 0
for [day,val_day] in val.items():
if count % BETWEEN_DAY == 0:
cp_ndays.loc[day,sym] = val_day
tmp = val_day
else:
cp_ndays.loc[day,sym] = np.nan

count += 1

cp_ndays.dropna(inplace=True)
cp_ndays




jp.stock.XXXX
jp.stock.YYYY




2015-01-05
2000.0
2015.0


2015-01-07
2096.0
2018.0


2015-01-09
2076.0
2054.0


2015-01-13
2076.0
2044.0


n日毎のデータからボリンジャーバンドの下端、上端を算出する

次に、TA-lib を利用して3日ボリンジャーバンドを実装します。

lband には ボリンジャーバンドの下端値

uband には ボリンジャーバンドの上端値

が代入されます。

import talib as ta

upperband = {}
middleband = {}
lowerband = {}

uband = pd.DataFrame(data=0,columns=[], index=cp_ndays.index)
lband = pd.DataFrame(data=0,columns=[], index=cp_ndays.index)

for (sym,val) in cp_ndays.items():
upperband[sym], middleband[sym], lowerband[sym] = ta.BBANDS(
cp_ndays[sym].values.astype(np.double),
timeperiod=3,
nbdevup=1,
nbdevdn=1,
matype=0)

lband[sym] = lowerband[sym]
uband[sym] = upperband[sym]

lband




jp.stock.XXXX
jp.stock.YYYY




2015-01-05
NaN
NaN


2015-01-07
NaN
NaN


2015-01-09
2015.978500
2011.279955


2015-01-13
2073.238576
2023.493591

uband




jp.stock.XXXX
jp.stock.YYYY




2015-01-05
NaN
NaN


2015-01-07
NaN
NaN


2015-01-09
2098.688166
2046.720045


2015-01-13
2092.094757
2053.839742


シグナル出力用のデータフレームに整形する

この状態のままでは、シグナルとして出力できないため

整形元のデータフレームと同じ形に当てはめる必要がある。

値が全て 0 のデータフレームとして lband2, uband2 を定義し、

lband, uband と同じ日付にて結合させている。

lband2 = cp.copy()

lband2[:] =0
uband2 = cp.copy()
uband2[:] = 0

uband2 = uband + uband2

uband2




jp.stock.XXXX
jp.stock.YYYY




2015-01-05
NaN
NaN


2015-01-06
NaN
NaN


2015-01-07
NaN
NaN


2015-01-08
NaN
NaN


2015-01-09
2098.688166
2046.720045


2015-01-12
NaN
NaN


2015-01-13
2092.094757
2053.839742

lband2 = lband + lband2

lband2




jp.stock.XXXX
jp.stock.YYYY




2015-01-05
NaN
NaN


2015-01-06
NaN
NaN


2015-01-07
NaN
NaN


2015-01-08
NaN
NaN


2015-01-09
2015.978500
2011.279955


2015-01-12
NaN
NaN


2015-01-13
2073.238576
2023.493591


終値との比較を行い、バンドを跨いだかどうか判定する

buy_sig = uband2 > cp

sell_sig = lband2 < cp

buy_sig




jp.stock.XXXX
jp.stock.YYYY




2015-01-05
False
False


2015-01-06
False
False


2015-01-07
False
False


2015-01-08
False
False


2015-01-09
True
True


2015-01-12
False
False


2015-01-13
True
True

sell_sig




jp.stock.XXXX
jp.stock.YYYY




2015-01-05
False
False


2015-01-06
False
False


2015-01-07
False
False


2015-01-08
False
False


2015-01-09
True
True


2015-01-12
False
False


2015-01-13
True
True

このデータフレームのデータがシグナルとなって、

売買タイミングの通知をユーザにしてくれる感じですね。

この検証を基に QuantX 上に組み込んだものが最初のコードとなります。

このやり方だと終値しか取得できないから、高値、安値を取得するには min とか max とかを合わせて使う必要がありそう。

今回はそこまでできませんでした。。