ブル・ベアファンド自動裁定 〜QuantX APIで買い増し機能実装〜

![115 [更新済み]-01.jpg](https://qiita-image-store.s3.amazonaws.com/0/318424/2ddff4af-d160-958b-bdc1-f2930bb9a7c3.jpeg)
## 導入
人気のあるETFブルベア型の取引の自動裁定システムをQuantX Factory上で開発します。
シリーズ化していますので

「QuantXって何？」
「ブルベア取引って？」
「まず名を名乗れよ。」

って方は以下のURLの１を参照下さい。

1. [ブル・ベアファンド自動裁定 〜はじめの一歩〜](https://qiita.com/Ric418/items/ac539b72f6368079b400)
2. [ブル・ベアファンド自動裁定 〜利確損切り機能実装〜（前回）](https://qiita.com/Ric418/items/ac539b72f6368079b400)

##今回の目標
今回は取引結果の優劣はおいておくとして以下を目標とします。

**前回の完成コードに買い増し機能を追加**
[前回のコードはこちら](https://factory.quantx.io/developer/d9c9192d1e9e4cf4b64a740ca4721fac)

具体的には以下のようなアルゴリズムです

- 利確損切りが発生しておらず、シグナルが連続した時１0単元株を買い増しする
- 何かしらで売りが出たら買い増しをリセット、総資産買いからスタートする

###早速やっていく
一応コードの全体像を眺めておきましょう、今回はこのコードの

`initialize`
`my_signals`
`handle_signals`

の部分をイジっていく形になります。

<details><summary>コードの全体像</summary><div>

```py
#必要なライブラリの読み込み
import numpy as np
import pandas as pd
import talib as ta

#RCIを計算する関数
def get_rci(close, period):
      rank_period = np.arange(period, 0, -1)
      length = len(close)
      rci = np.zeros(length)
      for i in range(length):
          if i < period - 1:
              rci[i] = 0
          else :
              rank_price = close[i - period + 1: i + 1].rank(method='min', ascending = False).values
              rci[i] = (1 - (6 * sum((rank_period - rank_price)**2)) / (period**3 - period)) * 100
      return rci

def initialize(ctx):
    # 設定
    ctx.logger.debug("initialize() called")
    ctx.configure(
      channels={          # 利用チャンネル
        	"jp.stock": {
        		"symbols": [
        			"jp.stock.1321", #日経225連動型上場投資信託
        			"jp.stock.1357", #日経ダブルインバース上場投信 bear
        			"jp.stock.1570", #日経平均レバレッジ上場投信 bull
        		],
        		"columns": [
        		  "close_price_adj"
        		]
        	}
        
      }
    )

    def _my_signal(data):
      cp = data["close_price_adj"].fillna(method='ffill')
      # ctx.logger.debug(cp)
      rci9 = pd.DataFrame(0, index=cp.index, columns=cp.columns)
      rsi7 = pd.DataFrame(0,index=cp.index, columns=cp.columns)
      
      for (sym,val) in cp.items():
        rci9[sym]=get_rci(cp[sym], 9)
        rsi7[sym] = ta.RSI(cp[sym].values.astype(np.double), timeperiod=7)
      # ctx.logger.debug(rci9)
      # ctx.logger.debug(rsi7)
        buy_sig= (rci9 < -80) | (rsi7 < 30)
        sell_sig= (rci9 > 80) | (rsi7 > 70)
      return {
        "buy:sig":buy_sig,
        "sell:sig":sell_sig,
        "rci9:g2":rci9,
        "rsi7:g2":rsi7,
      }

    # シグナル登録
    ctx.regist_signal("my_signal", _my_signal)

def handle_signals(ctx, date, current):
    '''
    current: pd.DataFrame
    '''
    done_syms = set([])
    # ctx.logger.debug(current)
    bear = ctx.getSecurity("jp.stock.1357")
    bull = ctx.getSecurity("jp.stock.1570")
    #利確損切り
    for (sym,val) in ctx.portfolio.positions.items():
        if sym == 'jp.stock.1321':
          continue
        returns = val["returns"]
        if returns > 0.05:
          sec = ctx.getSecurity(sym)
          sec.order(-val["amount"], comment="利益確定売(%f)" % returns)
          done_syms.add(sym)
        elif returns < -0.01:
          sec = ctx.getSecurity(sym)
          sec.order(-val["amount"], comment="損切り(%f)" % returns)
          done_syms.add(sym)
    # 買いシグナル
    df_buy = current["buy:sig"][0]
    if df_buy:
      if not ('jp.stock.1357' in done_syms) | ('jp.stock.1570' in done_syms):
        bull.order_target_percent(0.50, comment="BULL BUY")
        bear.order_target_percent(0, comment="BEAR SELL")
        done_syms.add("jp.stock.1570")
      
    # 売りシグナル
    df_sell = current["sell:sig"][0]
    if df_sell:
      if not ('jp.stock.1357' in done_syms) | ('jp.stock.1570' in done_syms):
        bear.order_target_percent(0.50, comment="BEAR BUY")
        bull.order_target_percent(0, comment="BULL SELL") 
        done_syms.add("jp.stock.1357")
    
```
</div></details>

####リセット用のフラグ機能を追加
買い増し機能と言っても

総資産割合買い→シグナル連続したら買い増し→売りがでたらリセット→シグナルが出たら総資産割合買い→…

以上の繰り返しと言う形にしたいので、買い増ししたいのか総資産割合買いしたいのかをフラグで管理します。
そのために`initialize`にAPI機能の`localStorage`を用いて以下のコードを追加してブルベアそれぞれのフラグを作ります。

```py
ctx.localStorage = {
      "jp.stock.1570":0,
      "jp.stock.1357":0
    }
```

ちなみに
0 : 総資産割合買いをしたいとき
1 : 買い増しを行いたいとき
となります、APIの詳細については[こちら](https://factory.quantx.io/handbook/ja/api.html#ctx)を参照ください。

以上のコードを追加した`initialize`が以下の様になります。

```py
def initialize(ctx):
    # 設定
    ctx.logger.debug("initialize() called")
    ctx.configure(
      channels={          # 利用チャンネル
        "jp.stock": {
          "symbols": [
            "jp.stock.1321", #日経225連動型上場投資信託
            "jp.stock.1357", #日経ダブルインバース上場投信bear
            "jp.stock.1570"  #日経平均レバレッジ上場投信bull
          ],
          "columns": [
            #"open_price_adj",    # 始値(株式分割調整後)
            "high_price_adj",    # 高値(株式分割調整後)
            "low_price_adj",     # 安値(株式分割調整後)
            #"volume_adj",         # 出来高
            #"txn_volume",         # 売買代金
            "close_price",        # 終値
            "close_price_adj",    # 終値(株式分割調整後) 
          ]
        }
      }
    )
    
    ctx.localStorage = {
      "jp.stock.1570":0,
      "jp.stock.1357":0
    }

    def _my_signal(data):
#中略...
```

これを後ほど`handle_signal`で使い買い増しを行います。

####１日前のシグナルを使えるようにする

ここでは以下の_my_signals関数をいじって行きます

```py
def _my_signal(data):
      cp = data["close_price_adj"].fillna(method='ffill')
      # ctx.logger.debug(cp)
      rci9 = pd.DataFrame(0, index=cp.index, columns=cp.columns)
      rsi7 = pd.DataFrame(0,index=cp.index, columns=cp.columns)
      
      for (sym,val) in cp.items():
        rci9[sym]=get_rci(cp[sym], 9)
        rsi7[sym] = ta.RSI(cp[sym].values.astype(np.double), timeperiod=7)
      # ctx.logger.debug(rci9)
      # ctx.logger.debug(rsi7)
        buy_sig= (rci9 < -80) | (rsi7 < 30)
        sell_sig= (rci9 > 80) | (rsi7 > 70)
      return {
        "buy:sig":buy_sig,
        "sell:sig":sell_sig,
        "rci9:g2":rci9,
        "rsi7:g2":rsi7,
      }

    # シグナル登録
    ctx.regist_signal("my_signal", _my_signal)
```

このコードでは日毎のシグナルが出ているかを`buy_signal`,`sell_signal`に格納しています。
これを以下のコードによって日にちをズラします。

```py
buy_sig.shift(1)
sell_sig.shift(1)
```

実際にずれているか確認してみましょう。
元の日にちのシグナルが以下のようになっています。

<img width="276" alt="signa.png" src="https://qiita-image-store.s3.amazonaws.com/0/318424/afc849be-4252-7657-9798-c01b9d3060d0.png">

また`.shift(1)`によってズラしたものがこちらになります。

<img width="276" alt="signa_shift.png" src="https://qiita-image-store.s3.amazonaws.com/0/318424/5ae35ff8-feea-900b-de93-fd6571ec789e.png">

2018/01/09のシグナル`True`が2018/01/10のシグナルにずれている事が確認できます。

これをreturnしてhandle_signalで使えるようにします。
これらのコードを追加すると_my_signalsは以下のようになります。

```py
def _my_signal(data):
      cp = data["close_price_adj"].fillna(method='ffill')
      # ctx.logger.debug(cp)
      rci9 = pd.DataFrame(0, index=cp.index, columns=cp.columns)
      rsi7 = pd.DataFrame(0,index=cp.index, columns=cp.columns)
      
      for (sym,val) in cp.items():
        rci9[sym]=get_rci(cp[sym], 9)
        rsi7[sym] = ta.RSI(cp[sym].values.astype(np.double), timeperiod=7)
      # ctx.logger.debug(rci9)
      # ctx.logger.debug(rsi7) 
      buy_sig= (rci9 < -80) | (rsi7 < 30)
      sell_sig= (rci9 > 80) | (rsi7 > 70)
      
      # ctx.logger.debug(buy_sig)
      # ctx.logger.debug(buy_sig.shift(1))
      return {
        "incbull:sig":buy_sig.shift(1),
        "incbear:sig":sell_sig.shift(1),
        "buy:sig":buy_sig,
        "sell:sig":sell_sig,
        "rci9:g2":rci9,
        "rsi7:g2":rsi7,
      }

    # シグナル登録
    ctx.regist_signal("my_signal", _my_signal)
```

####１日前のシグナル用いて買い増しを行う
ここで`handle_signal`で１日前のシグナルが使える様になったのでいよいよ実際に買い増しの実装を行いたいと思います。

ここは少し長いので最初に完成のコードを載せ、前回のコードとの差分を説明していきたいと思います。

```py
def handle_signals(ctx, date, current):
    '''
    current: pd.DataFrame
    '''
    done_syms = set([])
    
    # ctx.logger.debug(current)
    # ctx.logger.debug(ctx.localStorage["bulldone"])
    bear = ctx.getSecurity("jp.stock.1357")
    bull = ctx.getSecurity("jp.stock.1570")
    df_buy = current["buy:sig"][0]
    df_sell = current["sell:sig"][0]
    df_inc_bull = current["incbull:sig"][0]
    df_inc_bear = current["incbear:sig"][0]
        
    #利確損切り
    for (sym,val) in ctx.portfolio.positions.items():
        if (sym == 'jp.stock.1321') | ('jp.stock.1357' in done_syms) | ('jp.stock.1570' in done_syms):
          continue
        returns = val["returns"]
        if returns > 0.05:
          sec = ctx.getSecurity(sym)
          sec.order(-val["amount"], comment="利益確定売(%f)" % returns)
          ctx.localStorage[sym] = 0
          done_syms.add(sym)
          # ctx.logger.debug(ctx.localStorage[sym])
        elif returns < -0.01:
          sec = ctx.getSecurity(sym)
          sec.order(-val["amount"], comment="損切り(%f)" % returns)
          ctx.localStorage[sym] = 0
          done_syms.add(sym)
    # 買いシグナル
    # ctx.logger.debug(df_buy)
    # ctx.logger.debug(ctx.localStorage["jp.stock.1570"])
    if df_buy & df_inc_bull & (ctx.localStorage["jp.stock.1570"] == 1):
      if not ('jp.stock.1357' in done_syms) | ('jp.stock.1570' in done_syms):
        bull.order(bull.unit() * 10, comment="BULL買い増し")
        done_syms.add("jp.stock.1570")
    if df_buy & (ctx.localStorage["jp.stock.1570"] == 0):
      if not ('jp.stock.1357' in done_syms) | ('jp.stock.1570' in done_syms):
        bull.order_target_percent(0.50, comment="BULL BUY")
        bear.order_target_percent(0, comment="BEAR SELL")
        done_syms.add("jp.stock.1570")
        ctx.localStorage["jp.stock.1570"] = 1
        ctx.localStorage["jp.stock.1357"] = 0
    
    # 売りシグナル
    if df_sell & df_inc_bear & (ctx.localStorage["jp.stock.1357"] == 1):
      if not ('jp.stock.1357' in done_syms) | ('jp.stock.1570' in done_syms):
        bear.order(bear.unit() * 10, comment="BEAR買い増し")
        done_syms.add("jp.stock.1357")
    if df_sell & (ctx.localStorage["jp.stock.1357"] == 0):
      if not ('jp.stock.1357' in done_syms) | ('jp.stock.1570' in done_syms):
        bear.order_target_percent(0.50, comment="BEAR BUY")
        bull.order_target_percent(0, comment="BULL SELL") 
        done_syms.add("jp.stock.1357")
        ctx.localStorage["jp.stock.1357"] = 1
        ctx.localStorage["jp.stock.1570"] = 0
```

まずは`_my_signals`でreturn、登録した１日前のシグナルを以下のコードで取得します

```py
df_inc_bull = current["incbull:sig"][0]
df_inc_bear = current["incbear:sig"][0]
```
次に売りが出された銘柄に関しては以下のコードによってフラグを0とします。

```py
ctx.localStorage[sym] = 0  #symには"jp.stock.1357"などの銘柄が入ります
```
また、以下のコードでそれぞれの銘柄において`ctx.localStorage[sym]`が1の時に買い増しを行う様にします。

```py
      if df_buy & df_inc_bull & (ctx.localStorage["jp.stock.1570"] == 1):
      if not ('jp.stock.1357' in done_syms) | ('jp.stock.1570' in done_syms):
        bull.order(bull.unit() * 10, comment="BULL買い増し")
        done_syms.add("jp.stock.1570")

      if df_sell & df_inc_bear & (ctx.localStorage["jp.stock.1357"] == 1):
      if not ('jp.stock.1357' in done_syms) | ('jp.stock.1570' in done_syms):
        bear.order(bear.unit() * 10, comment="BEAR買い増し")
        done_syms.add("jp.stock.1357")
```

最後にそれぞれの銘柄の総資産割合注文の部分の条件式などを以下の様にする事で
`ctx.localStorage[sym]`が0の時に総資産割合買いをする様にします。

```py
    if df_buy & (ctx.localStorage["jp.stock.1570"] == 0):
      if not ('jp.stock.1357' in done_syms) | ('jp.stock.1570' in done_syms):
        bull.order_target_percent(0.50, comment="BULL BUY")
        bear.order_target_percent(0, comment="BEAR SELL")
        done_syms.add("jp.stock.1570")
        ctx.localStorage["jp.stock.1570"] = 1 #総資産割合買いしたので１にする（今後連続でシグナル出たら買い増し）
        ctx.localStorage["jp.stock.1357"] = 0 #売ったので0にする（今後シグナルが出たら総資産割合買いする）

    if df_sell & (ctx.localStorage["jp.stock.1357"] == 0):
      if not ('jp.stock.1357' in done_syms) | ('jp.stock.1570' in done_syms):
        bear.order_target_percent(0.50, comment="BEAR BUY")
        bull.order_target_percent(0, comment="BULL SELL") 
        done_syms.add("jp.stock.1357")
        ctx.localStorage["jp.stock.1357"] = 1 #総資産割合買いしたので１にする（今後連続でシグナル出たら買い増し）
        ctx.localStorage["jp.stock.1570"] = 0 #売ったので0にする（今後シグナルが出たら総資産割合買いする）
```

**これで完成です！**

完成のコードは[こちら](https://factory.quantx.io/developer/3fb085dfd8e64531a65386ae2ae78725)

##実行・結果
###前回の結果（買い増し機能無し）
<img width="818" alt="hutuuu.png" src="https://qiita-image-store.s3.amazonaws.com/0/318424/ba9e1ab6-ff5a-68b1-c38b-2a2f9f21b32a.png">

###今回の結果（買い増し機能あり）
<img width="815" alt="kaimasi.png" src="https://qiita-image-store.s3.amazonaws.com/0/318424/6ed1491b-ca49-a41e-2827-b7295103af46.png">

ちなみにシグナルの出方は以下の様であり色の濃いところが買い増しの期間になっています。

<img width="782" alt="スクリーンショット 2019-03-31 2.24.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/318424/eeb994a1-4235-57f6-bb51-846b3c664c37.png">

###考察
買い増し機能を入れたことにより今まで単純な総資産割合指定だった為に買いシグナルが続いても大した数注文できていなかった。これが数量指定で買い増しすることが可能になった事で収益とシグナル数が増加したと考えられる。

その一方保有割合は全期間において大きくなっているのでMaxDrawDown（最大損失）が大きくなっていると推定できる。

###今後の展望
ただ単純に買い増しを行うだけでなく保有日数や保有割合によって保有率を調整しリスク管理・MaxDrawDown抑制を狙う。

連続日数を2日だけでなく3日なども試してみる？

##宣伝

###もくもく会もやってるよ
**日時**：毎週水曜日18時〜
**場所**：神田　千代田共同ビル４階　SmartTrade社オフィス
**内容**：基本黙々と自習しながら猛者の方に質問して強くなっていく会
**備考**：お菓子と終わりにお酒が出るよ

###詳細はこちらだよ
Pythonアルゴリズム勉強会HP：[https://python-algo.connpass.com/](https://python-algo.connpass.com/)
(connpassって言うイベントサイトに飛びます)

##免責注意事項
このコード・知識を使った実際の取引で生じた損益に関しては一切の責任を負いかねますので御了承下さい。

## materials
bull-bear:<a href='https://jp.freepik.com/free-vector/ベクター画像_2461918.htm'>Designed by Rawpixel.com</a>