てけとーぶろぐ。

ソフトウェアの開発と、お絵かきと、雑記と。

Pythonで株価アラートをつくる(2)

Raspberry Pi + Pythonビットコイン自動取引」を目指して
まずは株価アラート、の2回目。

前回はアラート条件をソースコードに埋め込んでしまっていたので
それを外部のアラート条件定義ファイルで定義できるようにする。
複数銘柄の監視にも対応させる。

アラート条件定義ファイル

アラート条件定義ファイルは、手での編集のしやすさ、プログラムからの扱いやすさ、表現力を考えて以下のようなjsonファイルとする。

{
    "alertConditions": [
        {
            "alertConditionId": "f870215c-944a-11ea-8747-185e0fdcc982",
            "stockCode": "4755",
            "conditionType": "isEqualToOrHigher",
            "values": [
                1000
            ],
            "isEnabled": true
        },
        {
            "alertConditionId": "f870215d-944a-11ea-80ac-185e0fdcc982",
            "stockCode": "6758",
            "conditionType": "isEqualToOrHigher",
            "values": [
                5000
            ],
            "isEnabled": false
        }
    ]
}

各プロパティの説明は以下の通り。
これで複数銘柄のアラート条件を定義できる。
条件の種類としてはとりあえず基本的な「X円以上になったら」「X円以下になったら」の2種類に対応させることにする。

プロパティ 説明
alertConditions アラート条件の配列
alertConditionId アラート条件のID
stockCode 銘柄コード
conditionType アラート条件の種類
"isEqualToOrHigher":X円以上になったら
"isEqualToOrLower":X円以下になったら
values アラート条件に使う値の配列(現状1つの値しか使わない)
isEnabled 有効か否か(このアラート条件について監視を行うか)
アラート条件のクラス化

ファイルのフォーマットが決まったので
まずはファイルを扱うクラスを「alert_condition.py」としてつくる。

dataclass, dataclasses_jsonを使うと
そのクラスのインスタンスjsonにしたり
jsonからそのクラスのインスタンスをつくったり
といった機能を持つクラスが簡単につくれる。

そこでdataclass, dataclasses_jsonを使ってアラート条件定義ファイルのjsonに対応したAlertConditionクラス、AlertConditionListクラスを作成する。

これだけの定義でもう alert_condition_list.to_json() でクラスのインスタンスjsonにしたり、AlertConditionList.from_json() でjsonからクラスのインスタンスをつくったりできてしまう。

from typing import List, Dict
from dataclasses import dataclass
from dataclasses_json import LetterCase, dataclass_json
import uuid
from collections import OrderedDict


class AlertConditionType:
    IS_EQUAL_TO_OR_HIGHER = 'isEqualToOrHigher'
    IS_EQUAL_TO_OR_LOWER = 'isEqualToOrLower'

@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class AlertCondition:
    alert_condition_id: str
    stock_code: str
    condition_type: str
    values: List[int]
    is_enabled: bool

@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class AlertConditionList:
    alert_conditions: List[AlertCondition]


class AlertConditionUtil:
    JSON_FILE_NAME = 'conditions.json'
    
    @classmethod
    def save(cls, alert_condition_list: AlertConditionList):
        with open(cls.JSON_FILE_NAME, 'w', encoding='utf-8') as f:
            f.write(alert_condition_list.to_json(indent=4, ensure_ascii=False))

    @classmethod
    def load(cls) -> AlertConditionList:
        with open(cls.JSON_FILE_NAME, 'r', encoding='utf-8') as f:
            return AlertConditionList.from_json(f.read())
    
    @staticmethod
    def generate_id() -> str:
        return uuid.uuid1()


if __name__ == '__main__':

    ac1 = AlertCondition(AlertConditionUtil.generate_id(), '4755', AlertConditionType.IS_EQUAL_TO_OR_HIGHER, [1000], True)
    ac2 = AlertCondition(AlertConditionUtil.generate_id(), '6758', AlertConditionType.IS_EQUAL_TO_OR_HIGHER, [5000], True)

    acl = AlertConditionList([ac1, ac2])

    AlertConditionUtil.save(acl)
    new_acl = AlertConditionUtil.load()
    print(new_acl.to_json(indent=4, ensure_ascii=False))

あとはjsonファイルへの保存、jsonファイルからの読み込み、アラート条件IDの生成といったユーティリティメソッドを持ったAlertConditionUtilクラスもつくっておく。

実行してみるとアラート条件定義ファイル「conditions.json」が作成される。

アラート条件に従った監視

アラート条件定義ファイルからアラート条件を読み込むことができるようになったので
このアラート条件を使って株価の監視をしてみる。

import os
from apscheduler.schedulers.background import BackgroundScheduler
from mail_sender import MailSender
from stock_price import StockPrice
from time import sleep
from alert_condition import AlertConditionUtil, AlertConditionType
import datetime

GMAIL_USER = os.environ.get("GMAIL_USER")
GMAIL_PASSWORD = os.environ.get("GMAIL_PASSWORD")
TO_ADDRESS = os.environ.get("TO_ADDRESS")

mail_sender = MailSender(GMAIL_USER, GMAIL_PASSWORD, TO_ADDRESS)

is_condition_enabled = True
is_exception_raised = False


def update():
    global is_exception_raised

    acl = AlertConditionUtil.load()

    # 有効なアラート条件の銘柄コードのユニークなリストを取得
    stock_codes = [ac.stock_code for ac in acl.alert_conditions if ac.is_enabled]
    stock_codes = list(set(stock_codes))

    # 株価を取得
    ticker_dict = {}
    try:
        for stock_code in stock_codes:
            ticker = StockPrice.get_ticker(stock_code)
            ticker_dict[stock_code] = ticker
    except Exception as exception:
        mail_sender.send_mail('update() Exception Alert', f'exception:\n{exception}')
        is_exception_raised = True
        return

    # アラート条件の確認
    for alert_condition in acl.alert_conditions:
        if not alert_condition.is_enabled:
            continue

        stock_code = alert_condition.stock_code
        values = alert_condition.values
        ticker = ticker_dict[stock_code]
        if alert_condition.condition_type == AlertConditionType.IS_EQUAL_TO_OR_HIGHER:
            if ticker.price >= values[0]:
                mail_sender.send_mail(f'[株価アラート] 銘柄コード:{stock_code}', 
                                      f'銘柄コード:{stock_code}の株価が{values[0]}円以上になりました。')
                alert_condition.is_enabled = False

        elif alert_condition.condition_type == AlertConditionType.IS_EQUAL_TO_OR_LOWER:
            value = alert_condition.values[0]
            if ticker.price <= value:
                mail_sender.send_mail(f'[株価アラート] 銘柄コード:{stock_code}', 
                                      f'銘柄コード:{stock_code}の株価が{values[0]}円以下になりました。')
                alert_condition.is_enabled = False

    AlertConditionUtil.save(acl)


if __name__ == '__main__':
    scheduler = BackgroundScheduler()

    # 毎分0秒に実行する
    scheduler.add_job(update, 'cron', second=0, max_instances=10)
    scheduler.start()

    try:
        while True:
            if is_exception_raised:
                break

            sleep(0.001)

    except KeyboardInterrupt:
        pass

    scheduler.shutdown()

前回作成した監視と基本は同じ。主な違いは以下の点。

  • アラート条件をアラート条件定義ファイルから読み込んだものを使っていること
  • 複数銘柄対応のためにループしていること

それから一定時間ごとの処理は毎分0秒のタイミングで実行するようにした。
apschedulerだとこの変更がパラメーターの変更だけでできてしまう。

実行すると、プログラムは毎分0秒のタイミングでアラート条件に従った監視を行い、条件を満たすとその旨を知らせるメールを送信する。
一度条件を満たしたアラート条件については無効になる。

だいぶ実用的になってきたのではないだろうか。

しかし実際に使うとなると、プログラムを実行しつづけなくてはならない。
PCをずっと起動しておくの…?
電気代大丈夫…?

ということで、次回はどう動かし続けるかについて書きたい。