楽天証券でレバナスを自分の買い増し基準通りに自動注文する


# 背景
「つみたてNISA枠で月○○円積立」という風に定額・定周期で積立する場合、各種証券会社サイトに元々備わっている積立設定を利用すれば十分でした。一方、「自分の買い増し基準に従って、好きな銘柄を好きな金額・タイミングで購入したい」と思った時、そんなてんこ盛り機能は見当たらず…ならば実装しようと思ったのでした。

# この記事に書いてあること
以下のような自動注文プログラムの実装例
- 株価情報サイト（stooq）から株価を取得
- 自分の買い増し基準通りに購入額を算出
- タスクスケジューラを利用し、任意のタイミングで楽天証券サイトから自動で買い注文を実行
- 24時間つけっぱなしのPC上で動かすことを想定

ソフトウェア構成
- OS: Windows
- ブラウザ: Firefox
- 言語: Python
- ドライバ: Selenium

# この記事に書いていないこと
- 楽天証券以外の証券会社や前項のソフトウェア構成以外を利用した実装例
- 株式自動売買AIのように、買い増し基準自体を自動で調整するような機構
- 1日のほとんどの時間眠ったままのPCや、スマホの上で自動注文プログラムを動かす方法

# おことわり
- この記事で紹介するプログラムはwebスクレイピングと呼ばれる挙動を含みます。実装例では各操作の間に1秒以上の遅延を入れて高負荷対策していますが、実装に手を加える場合は高負荷とならないよう十分注意し、自己責任でお願いします。
- この記事は特定の証券会社の利用や株式の売買自体を推奨するものではありません。投資は自己責任でお願いします。

# 方針
前置きが長くなりました。私がやりたかったのは、とある買い増し基準に従い大和アセットのiFreeレバレッジNASDAQ100を毎日ちまちま買い増すことです。また、普段から自室のPCを起動しっぱなしにしておく習慣があったため、そのPC上で自動注文プログラムを動かせばいいやということで実装に着手しました。

軽く調べた感じ、^NDXの株価は[Stooq](https://stooq.com/)から引っ張ってくれば良さそう。また、楽天証券サイトの自動操作にはWebアプリの自動試験ツールである[Selenium](https://www.selenium.dev/ja/documentation/)が使えそう。これらがライブラリAPIとして提供されていて、かつ、将来AIとかも絡めた実装ができれば良いなと思い言語はPythonに決定。ブラウザはFirefox。

一応手動でも起動できるように、Pythonプログラム自体は買い注文1ショット用の実装とする。そして日々の注文タイミング（＝Pythonプログラム起動タイミング）はWindows標準のタスクスケジューラで制御する。

# 買い増し基準
前項で触れたとある買い増し基準というのが、某投資系インフルエンサー様⛄提唱の下記基準です。

【基準】
NDXが100MA（100日移動平均値）を下回っている間、乖離率に応じて毎日レバナス買付

【1日あたりの買付金額】
-1%未満：500円
-1%台：1,000円
-2%台：2,000円
-3%台：3,000円
-4%台：4,000円
-5%以上：5,000円

とてもシンプルですよね。お買い得なら多く買う。
絶対値で基準額を決めちゃうと一生そこまで下がらないかもしれないので100MAを使う。今が底だァ！と思って数万円とか突っ込みたくなるけど底は誰にもわからないので少額で毎日コツコツと買い増す。

まずはこれを実装したい。そして乖離率や買付金額を自分用にチューニングしよう。

# 実装
コードの全容は以下です。
```python:autobuy.py
import time
import datetime
import matplotlib.pyplot as plt
from pandas_datareader.stooq import StooqDailyReader
from selenium import webdriver
from selenium.webdriver.firefox import service as fs
from selenium.webdriver.common.by import By

DRIVER_PATH = "geckodriver.exe"
FUND_URL = "https://www.rakuten-sec.co.jp/web/fund/detail/?ID=JP90C000H0S5"
SYMBOL = "^ndx"
LOGIN_ID = "HOGEHOGE"
LOGIN_PASS = "FUGAFUGA"
ORDER_PASS = "PIYOPIYO"


class AutoBuyBot:

    def __init__(self, symbol, driver_path):
        # 株価情報（終値）取得
        stooq = StooqDailyReader(symbols=symbol, start='2021/1/1')
        self.stock_price = stooq.read()["Close"].iloc[::-1]

        # 100日移動平均
        self.ma100 = self.stock_price.rolling(100).mean()

        # 25日移動平均
        self.ma25 = self.stock_price.rolling(25).mean()

        # firefox用ドライバを初期化
        service = fs.Service(executable_path=driver_path)
        self.driver = webdriver.Firefox(service=service)

    def dump_chart(self, label, figname):
        range = 30
        fig = plt.figure()
        plt.plot(self.stock_price.tail(range), label=label)
        plt.plot(self.ma100.tail(range),
                 label='100days MA', linestyle='dashed')
        plt.plot(self.ma25.tail(range), label='25days MA', linestyle='dashed')
        plt.legend(loc="upper right", fontsize=8)
        plt.xlabel('Date', fontsize=10)
        plt.ylabel('Price', fontsize=10)
        plt.tick_params(labelsize=10)  # 軸ラベルの目盛りサイズ
        plt.xticks(rotation=90)
        plt.tight_layout()
        fig.savefig(figname)

    def calc_price_with_hiyono_method(self):
        # 昨日の終値
        stock_price_latest = self.stock_price.tail(1).iloc[-1]

        # 100日移動平均値
        ma100_latest = self.ma100.tail(1).iloc[-1]

        # 乖離率を計算
        rate = stock_price_latest / ma100_latest

        # 乖離率5%以上
        if rate <= 0.95:
            price = 5000
        # 4%台
        elif rate <= 0.96:
            price = 4000
        # 3%台
        elif rate <= 0.97:
            price = 3000
        # 2%台
        elif rate <= 0.98:
            price = 2000
        # 1%台
        elif rate <= 0.99:
            price = 1000
        # 1%未満
        elif rate < 1.00:
            price = 500
        # 割高
        else:
            price = 0

        return price

    def run_selenium_sequence(self, fund_url, login_id, login_pass, price, order_pass, dryrun=False):
        # 楽天証券の商品ページを開く
        self.driver.get(fund_url)
        self.driver.maximize_window()
        time.sleep(3)

        # ログインフォームをクリック
        elem = self.driver.find_element(by=By.CLASS_NAME, value="login-form")
        elem.click()
        time.sleep(1)

        # ログインIDを入力
        elem = self.driver.find_element(by=By.ID, value="form-login-id")
        elem.send_keys(login_id)
        time.sleep(1)

        # パスワードを入力
        elem = self.driver.find_element(by=By.ID, value="form-login-pass")
        elem.send_keys(login_pass)
        time.sleep(1)

        # ログインボタンをクリック
        elem = self.driver.find_element(
            by=By.CLASS_NAME, value="login-form__login-button")
        elem.click()
        time.sleep(1)

        # スポット購入ボタンをクリック
        elem = self.driver.find_element(
            by=By.CSS_SELECTOR, value="a[href*='JavaScript:showOrder(']")
        elem.click()
        time.sleep(1)

        # 買付金額を入力
        elem = self.driver.find_element(by=By.NAME, value="orderPriceUnit")
        elem.send_keys(price)
        time.sleep(1)

        # ポイント利用、分配金コース、口座区分はデフォルト値を使用

        # 目論見書チェックをクリック
        elem = self.driver.find_element(
            by=By.NAME, value="prospectusAgreementCheck")
        elem.click()
        time.sleep(1)

        # 確認ボタンをクリック
        elem = self.driver.find_element(
            by=By.CSS_SELECTOR, value="a[onclick*='if(checkDoubleSend()==false)']")
        elem.click()
        time.sleep(1)

        # 取引暗証番号を入力
        elem = self.driver.find_element(by=By.NAME, value="password")
        elem.send_keys(order_pass)
        time.sleep(1)

        # dryrun指定の場合、実際には注文しない
        if dryrun == True:
            return

        # 注文ボタンをクリック
        elem = self.driver.find_element(by=By.ID, value="sbm")
        elem.click()
        time.sleep(1)

    def __del__(self):
        time.sleep(3)
        self.driver.close()


def main():
    bot = AutoBuyBot(SYMBOL, DRIVER_PATH)
    figname = datetime.datetime.now().strftime('%Y%m%d_%H%M%S.png')
    bot.dump_chart(SYMBOL, figname)
    price = bot.calc_price_with_hiyono_method()
    bot.run_selenium_sequence(
        FUND_URL, LOGIN_ID, LOGIN_PASS, price, ORDER_PASS)


if __name__ == "__main__":
    main()
```

# 解説と注意点
```python
import time
import datetime
import matplotlib.pyplot as plt
from pandas_datareader.stooq import StooqDailyReader
from selenium import webdriver
from selenium.webdriver.firefox import service as fs
from selenium.webdriver.common.by import By
```
不足パッケージがあればpip installして下さい。私の手元の環境ではmatplotlib等既にインストール済みであったため、今回はpandas_datareaderとseleniumだけ新たに足しました。

```python
DRIVER_PATH = "geckodriver.exe"
```
PythonからSeleniumを呼び出すにはPythonライブラリ以外にブラウザ毎の専用ドライバ（.exe）が必要で、[ここ](https://www.seleniumqref.com/introduction/ruby/Ruby_GeckoDriver_Ins.html)を参考にダウンロードし配置する必要がある。DRIVER_PATHの絶対パス指定で参照するためどこに置いても良いが、実装例ではautobuy.pyの隣に置く想定になっている。

```python
FUND_URL = "https://www.rakuten-sec.co.jp/web/fund/detail/?ID=JP90C000H0S5"
SYMBOL = "^ndx"
LOGIN_ID = "HOGEHOGE"
LOGIN_PASS = "FUGAFUGA"
ORDER_PASS = "PIYOPIYO"
```
今回はiFreeレバナスを想定した実装になっているが、別銘柄を購入する場合はFUND_URLとSYMBOLを適宜変更すればいけるはず。とりあえず動けばいっかということでログインIDとかパスワードとかべた書きしちゃいました。流出させないように注意！

```python
        stooq = StooqDailyReader(symbols=symbol, start='2021/1/1')
```
今回Stooqから株価を取得しているのですが、買い注文のタイミングを最初朝6時とかに設定していたら株価の更新がまだだったみたいで、前々日の価額を拾ってきて計算してしまいました。買い注文のタイミングを正午に変更したところ、今のところ問題なく前日の価額が取れています。

```python
    def dump_chart(self, label, figname):
        range = 30
```
![20220327_011750.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2606405/60360cd5-525f-1663-ebfd-b8e2a36f177c.png)
直近30日の価額と移動平均線をプロットしたグラフをyyyymmdd_hhmmss.pngのファイル名で保存します。プログラムがタスクスケジューラに呼び出されたことの確認も兼ねて。

```python
    def calc_price_with_hiyono_method(self):
```
実装の要です。買い増し基準に従い購入額を決定する関数。色んな買い増し基準の関数を並べて使い分けたりするとよさそう。

```python
    def run_selenium_sequence(self, fund_url, login_id, login_pass, price, order_pass, dryrun=False):
```
Seleniumは、自動操作対象ページのhtmlタグ等を利用して操作を実行していきます。そのため対象ページのデザインにがっつり依存していて、証券会社ページのデザインがちょっとでも変更になると途端に動作しなくなるかもしれません。

当然、今回の実装例は楽天証券のページのデザインを意識していますので他の証券会社のURLを指定したとて動きません。他の証券会社を利用したい場合はrun_selenium_sequence関数の中身を参考に書き換えてみて下さい。

```python
    def __del__(self):
        time.sleep(3)
        self.driver.close()
```
autobuy.pyの初回起動時はブラウザからbot操作（＝新規ユーザ）扱いされるので、初手で証券会社のページに飛べなくてエラー終了します。ブラウザの案内に従って手動で初期設定を済ませて下さい。エラー終了した場合、デストラクタ内で3秒待ってからブラウザを閉じるようになっているため、閉じるのが早すぎる場合はここで調整下さい。

# タスクスケジューラ設定
Windowsのタスクスケジューラで新規のタスクを作成し、好きなタイミングで自動注文プログラムを起動します。タスクスケジューラの設定自体は一般的な話なので省略します。私は米国市場に合わせて火水木金土の正午に起動するよう設定しました。

注意点として、Pythonインタプリタのパスが通っていればautobuy.pyを直接呼び出しても行けるかもしれませんが、私の手元の環境ではAnacondaで仮想環境を切り替えたりしていて複数の実行環境があったため、バッチファイルを一段噛ませてそこからautobuy.pyを呼び出すようにしました。内容は一例です。
```autobuy.bat
call C:\Users\hogehoge\AppData\Local\Continuum\anaconda3\Scripts\activate.bat
python C:\Users\hogehoge\Desktop\auto_trade\autobuy.py
```

また、タスクスケジューラの設定で一個ハマったところとしては、「ユーザがログオンしているかどうかにかかわらず実行する」を選択していると何故かautobuy.batの呼び出しが不発になりました。とりあえず「ユーザがログオンしているときのみ実行する」に変更したら動いたので深追いしていません。

# その他
結局Seleniumを使ってブラウザを自動でぽちぽちしているだけなので、OS・ブラウザ・証券会社等に依存せず汎用的に実装できると思う。Linuxでやる場合はcronとか使えばいけそう。一度設定してしまえば基本放置でOKのはずだが、お金が絡む話なので一応毎日注文受付メールが飛んできていることくらいは確認しよう。