【第8回】株価チャート配信サーバー

February 11, 2021

株価サーバーpython

無一物中Project記事一覧


はじめに

本連載の【第5回】【第6回】で、mplfinanceを使用して株価チャートを表示させる方法を紹介しました。これはいずれもpythonが動作するデスクトップ環境でチャートを表示させるものであり、あまり実用性はありませんでした。

そこで今回は、株価チャート配信サーバを構築し、webブラウザでどこからでも株価チャートを表示させることを目指します。

実装には、pythonでwebサイトサーバ側の振る舞いを記述できるFlaskというフレームワークを使用します。python+Flask+mplfinanceの実行環境を、docker-composeで動かし、システムとしてまとめます。

百聞は一見に如かず、まずはデモサイトで動作を確認してみてください。レスポンシブデザインではないので、PC専用ですが。。

尚、今回のソースコードは、GitHubに置いています。

実行環境

ディレクトリ構成

chart_demo/
    Dockerfile
    docker-compose.yml
    opt/
        flask_chart.py  # 株価配信サーバのソース
        muzinzo/        # チャートを表示するための私設パッケージ
        share/          # 株価データを置く場所
            data/
                kdic.csv # 銘柄辞書
                1000/
                    1001.csv
                    1002.csv
                    ...
                2000/
                ...
                9000/
                    9001.csv
                    ...
        static/         # Flaskで使用するcssファイル等
        templates/      # Flaskで使用するhtmlファイル

data/ ディレクトリの中に、1000/ ~ 9000/ のサブディレクトリを切ってその中に、「証券コード.csv」という名前の株価日足時系列データを置きます。

kdic.csvは、以下の形式の銘柄名の辞書csvファイル

kdic.csv
1001,日経225,東証1部
1002,TOPIX,東証1部
1301,極洋,東証1部
  ...
9995,グローセル,東証1部
9996,サトー商会,JAQ
9997,ベルーナ,東証1部

株価日足時系列データのcsvファイルの例は以下。

1332.csv
2015/01/05,275,277,274,275,239000000
2015/01/06,274,275,270,272,480000000
2015/01/07,270,273,270,271,217000000
  ...
2021/02/05,454,494,452,493,7411100000
2021/02/08,498,528,498,512,6054100000
2021/02/09,521,530,505,510,4063500000

このデータ形式による株価データの取得方法は、は本連載の【第2回】~【第4回】に載せていますので、参考にしてください。

Flaskによる株価チャート配信サーバ

以下はFlaskによる株価チャート配信サーバのソースです。たったこれだけ。。。チャート表示に私設のMuzinzoクラスを使っていて中身が隠ぺいされていますが、全貌はGitHubのソースを見てください。 私設Muzinzoクラスを使用しないバージョンは、Qiitaで以前公開しました。

flask_chart.py
import os
import glob
import io
from flask import Flask, send_file, request, render_template
from flask_cors import CORS
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from muzinzo.Muzinzo import Muzinzo

app = Flask(__name__)
CORS(app)

DATA_PATH = "/root/opt/share/data/"
mz = Muzinzo(DATA_PATH)

# http://サーバ:5000/
@app.route('/')
def hello():
    return "Chart Server"

# http://サーバ:5000/candle2?code=1001&term=200&volume=True
@app.route('/candle2')
def candle2():
    code = request.args.get('code', default=None, type=str)
    term = request.args.get('term', default=200, type=int)
    volume = request.args.get('volume', default="True", type=str)
    return render_template("candle2.html", title="chart", code=code, term=term, volume=volume)


# http://サーバ:5000/candle?code=1001&term=200&volume=True
@app.route("/candle")
def candle():
    code = request.args.get('code', default=None, type=str)
    term = request.args.get('term', default=200, type=int)
    volume = str2bool(request.args.get('volume', default="True", type=str))
    stochastic = str2bool(request.args.get('stochastic', default="True", type=str))
    macd = str2bool(request.args.get('macd', default="True", type=str))
    mav = str2bool(request.args.get('mav', default="True", type=str))
    
    image = io.BytesIO()
    mz.plot_chart(code, term, volume=volume, stochastic=stochastic, macd=macd, mav=mav)
    plt.savefig(image, format='png')
    image.seek(0)
    return send_file(image, attachment_filename="image.png")

class StockList:
    def __init__(self, code, name):
        self.code = code
        self.name = name[:8]

def walk_around(dic, sl):
    for i in range(1, 10):
        input_path = mz.get_database_path() + str(i*1000) + "/*.csv"
        csv_files = sorted(glob.glob(input_path))
        for csv_file in csv_files:
            filename = os.path.basename(csv_file)
            code = filename[0:4]
            s = StockList(code, dic[code].name)
            sl.append(s)
            
# http://サーバ:5000/list/
@app.route("/list")
def list():
    dic = mz.get_dict()
    sl = []
    walk_around(dic, sl)
    return render_template("list.html", title='Stock List', members=sl)


def str2bool(s):
     return s.lower() in ["true", "t", "yes", "y", "1"]

if __name__ == '__main__':
     app.run(host='0.0.0.0', port=5000, debug=True)

http://サーバ名:5000/
によりroot()が走ります。ここでは、クライアントに”Chart Server”という文字列を表示させるだけです。

http://サーバ名:5000/candle?code=6897&term=200&volume=True
によりcandle()が走ります。

candle()では、urlパラメータを抽出し、それに従いmpl.plot()でチャートを表示し、それを(メモリ上の)imageにpngでセーブし、クライアントに返します。

同じパラメータでcandle2()もありますが、candle()では画像を直接返すのに対し、candle2()では、画像をimgタグで貼り付けたhtmlページを返すようにしています。ページをデコるような場合は、こちらのほうがよろしいかと。

http://サーバ名:5000/list
によりlist()が走ります。

list()では、左側に銘柄のselector、右側にチャートの2分割画面のhtmlを生成してクライアントに返します。Flaskでhtmlを返すには、render_template()により、あらかじめテンプレートとして用意したhtmlファイル(ここではlist.html)に、必要な値を挿入したページを作成します。

以下、銘柄リストを選択するselectとチャートを表示するiframeを並べただけのhtmlですが、銘柄毎にoptionが置換されます。

list.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>{{ title }}</title>
        <link rel="stylesheet" type= "text/css" href= "{{ url_for('static', filename='css/list.css') }}">
    </head>
    <body onload="document.forms.frm.sel.size = 30;">
    <script>
        window.onresize = function change_height() {
            document.forms.frm.select_code.size = window.innerHeight/24;
        }
        function disp_chart() {
            var value = document.getElementById("select_code").value;
            var url = '/candle2?code='+value+'&term=100';
            document.getElementById('inline-frame').contentWindow.location.replace(url);
        }
    </script>    
    <form id="frm" width=20%>
    <select id="select_code" size="30" onchange="disp_chart()">
{% for member in members %}
    <option value="{{ member.code}}">{{ member.code}} {{ member.name }}</option>
{% endfor %}
    </select>
    </form>
    <iframe id="inline-frame"
       align="top"
       width=80%
       height=600
       src="/candle?code=1001">
    </iframe>
    </body>
</html>

docker-composeによる実装

pythonの実行イメージを作って、そこでFlaskサーバを稼働させています。

Dockerfile
FROM python:3.9-slim
USER root

RUN apt-get update
RUN apt-get -y install locales && \
    localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9
ENV TERM xterm
ENV DEBIAN_FRONTEND noninteractive
ENV DEBCONF_NOWARNINGS yes

RUN apt-get install -y vim
RUN pip install --upgrade pip && \
    pip install --upgrade setuptools && \
    pip install --upgrade Flask flask_cors mplfinance japanize-matplotlib
RUN cp -p /usr/local/lib/python3.9/site-packages/japanize_matplotlib/fonts/ipaexg.ttf /usr/local/lib/python3.9/site-packages/matplotlib/mpl-data/fonts/ttf/
RUN echo "font.family : IPAexGothic">>/usr/local/lib/python3.9/site-packages/matplotlib/mpl-data/matplotlibrc
CMD ["python", "/root/opt/flask_chart.py"]

mplfinanceで株価チャートのタイトルや軸名などを日本語で表示させる場合、日本語フォントを別途インストールする必要があります。日本語フォントとしてIPAフォントをインストールする場合が多いのですが、IPAフォント単独でpipでインストールすることができません。matplotlibの日本語対応版のjapanize-matplotlibをインストールすると、IPAフォントがいっしょにインストールされるので、pipでjapanize-matplotlibをインストールし、その後、IPAフォントをmplfinanceのフォントフォルダーにコピーするようにしました。

docker-compose.yml

docker-compose.yml
version: '3'
services:
  chart:
    restart: always
    build: .
    container_name: 'chart'
    working_dir: '/root/opt'
    tty: true
    ports:
      - 5000:5000
    volumes:
      - ./opt:/root/opt

株価サーバの起動

$ docker-compose up -d --build

必要に応じてサーバの5000番ポートを開けておいてください。

株価チャートを画像として単独表示させる場合は、webブラウザで、

http://サーバ名:5000/candle?code=1301&term=100&volume=True

を叩けば表示されます。

1301.png

http://サーバ名:5000/candle2?code=1301&term=100&volume=True

を叩けば、株価チャートをimgタグで埋め込んだhtmlが表示されます。ブラウザのウィンドウサイズに応じて画像が拡大縮小するようにしています。

http://サーバ名:5000/list

を叩けば、銘柄選択selector付チャートページが表示されます。デモサイトはこちらです。

chart.png

まとめ

無一物中Project第8回では、サーバで株価チャートを配信する方法を紹介しました。


Written by questions6768 who lives in Uji, Kyoto.