【第4回】無尽蔵から自動で株価データを取得し株価データベースを更新

December 11, 2020

株価サーバーpython

無一物中Project記事一覧


はじめに

毎日の日本株の株価データを公開している無尽蔵というサイトがあります。 自分で株価データを持つために、ここから、毎日定時に、その日の株価一覧の取得を試みます。なお、稼働させるには、python3が使える環境が必要ですが、本記事では、python3実行環境をdocker-composeで用意します。従って、本記事を追試する場合は、python3実行環境は特に必要なく、docker, docker-composeが稼働する環境が必要になります。

無尽蔵では、以下の形式のcsvファイルで毎日の株価一覧が公開されています。

T220701.csv
2022/7/1,1001,10,1001 日経225,26460,26531,25841,25936,1349730000,東証
2022/7/1,1002,10,1002 TOPIX,1875,1881,1836,1845,1349730000,東証
2022/7/1,1301,11,1301 極洋,3490,3490,3435,3445,16900,東証P
2022/7/1,1305,10,1305 ダイワTPX,1996.5,2008,1962,1970.5,415170,東証
2022/7/1,1306,10,1306 TOPIX投,1976,1987,1938.5,1946.5,3697750,東証
  ...
2022/7/1,1712,30,1712 ダイセキ環境ソリューション,0,0,0,0,0,名証
2022/7/1,1712,11,1712 ダイセキ環境ソリューション,790,836,788,826,472800,東証P
  ...
2022/7/1,9996,12,9996 サトー商会,1254,1256,1254,1254,700,東証S
2022/7/1,9997,11,9997 ベルーナ,747,756,739,743,252700,東証P

無尽蔵サイトから株価データを取得するpythonスクリプト

無尽蔵サイトのデータ取得のurlは、例えば2022/07/01の株価データなら、

http://mujinzou.com/k_data/2022/22_07/T220701.zip

以下のスクリプトは、当日の株価データを取得し、zipを展開するスクリプトの例です。

get_todays_csv_zip_from_muzinzo.py
# -*- coding: utf-8 -*-
import urllib.request
import zipfile
import datetime
import os
import logging
import sys

def download_csv_zip(output_dir, year, month, day):
    base_url = 'http://mujinzou.com/k_data/'+str(year)+'/'
    subdir = '{0:02d}_{1:02d}/'.format(year-2000, month)
    filename = 'T20{0:02d}{1:02d}.zip'.format(month, day)
    url = base_url + subdir + filename
    now = datetime.datetime.now()
    try:
        urllib.request.urlretrieve(url, filename)
        with zipfile.ZipFile(filename, 'r')as zf:
            zf.extractall(output_dir)
        os.remove(filename)
        logging.info(str(now)+' success to get file: '+filename)
    except urllib.error.HTTPError:
        logging.error(str(now)+' error')
                

def main():
    args = sys.argv
    if len(args)==2:
        output_dir = args[1]
        if output_dir[-1]!='/':
            output_dir = output_dir + '/'
    else:
        output_dir = './'
    logging.basicConfig(level=logging.INFO)
    today = datetime.date.today()
    download_csv_zip(output_dir, today.year, today.month, today.day)

if __name__ == '__main__':
    main()

使い方の例:

$ python get_todays_csv_zip_from_muzinzo.py ~/tmp

引数としてダウンロード先のディレクトリを指定できます。指定しなければ、ダウンロード先はカレントディレクトリになります。

サイトに当日の株価データが置かれるには概ね夕方以降であることを留意してください。つまり、このスクリプトで当日の株価データが取得できるのは、当日の夕方から深夜0時までです。

docker-composeでpythonスクリプトを動かす

pythonスクリプトの定時実行の布石として、docker-composeでpythonスクリプトを動かします。

docker-compose.ymlとDockerfileは、以下から借用。

https://qiita.com/reflet/items/4b3f91661a54ec70a7dc dockerで簡易にpython3の環境を作ってみる

ディレクトリ構成

python3/
    Dockerfile
    docker-compose.yml
    opt/
        get_todays_csv_zip_from_muzinzo.py   ※1
        import_todays_data.py   ※2
        exec_cmd.sh   ※3
        fetch.log   ※4
        update.log   ※5
        data/   ※6
            1000/
                1001.csv
                ...
            2000/
            ...
            9000/
                9001.csv
                ...
        Tyymmdd.csv   ※7
  • ※1; 無尽蔵サイトから当日の株価を取得するスクリプト
  • ※2; 当日の株価を銘柄毎の時系列データに変換するスクリプト
  • ※3; 毎日定時に一連の処理を実行するスクリプト
  • ※4; 株価取得のログ
  • ※5; 変換のログ
  • ※6; 時系列データが入った株価データベース
  • ※7; 取得した株価データ

Dockerfileはpython3を動かすためのイメージです

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 less cron
RUN pip install --upgrade pip
RUN pip install --upgrade setuptools
#RUN echo '0 20 * * 1-5 root /root/opt/exec_cmd.sh' >> /etc/crontab
#CMD ["cron", "-f"]

docker-composeファイルは以下。volumeとして、ホストの/etc/group, /etc/passwdをマウントしていますが、これはスクリプトで生成するファイルのパーミッションがroot:rootになってしまうので、これをホストのアカウントの権限に変えるため、アカウントを参照するためのものです。

docker-compose.yml
version: '3'
services:
  python3:
    restart: always
    build: .
    container_name: 'python3'
    working_dir: '/root/opt'
    tty: true
    volumes:
      - ./opt:/root/opt
      - /etc/group:/etc/group:ro
      - /etc/passwd:/etc/passwd:ro

起動

$ docker-compose up -d --build

pythonスクリプトの実行

$ docker-compose exec python3 python /root/opt/get_todays_csv_zip_from_muzinzo.py

これでoptディレクトリ内に、当日の株価データ「Tyymmdd.csv」が生成されます。

pythonスクリプトを毎日定時実行する

先のDockerfileの末尾2行をコメントアウトします。 これは、cronにより、月曜日~金曜日の20時に、スクリプトexec_cmd.shを実行するための設定です。

Dockerfile
  ...
RUN echo '0 20 * * 1-5 root /root/opt/exec_cmd.sh' >> /etc/crontab
CMD ["cron", "-f"]

毎日定時実行するスクリプトは以下。スクリプトで生成するファイルのオーナーとパーミッションがroot:rootになるのを無理やり変更しています。本来ならもっとスマートにアカウント名を環境変数でコンテナに渡して云々とかすべきなんでしょうが、それはやりたい方がやったらいいだけで、これでお手軽で十分です。カッコ悪いですが。

opt/exec_cmd.sh
#!/bin/bash
/usr/local/bin/python /root/opt/get_todays_csv_zip_from_muzinzo.py /root/opt >> /root/opt/fetch.log 2>&1
chmod a+rw /root/opt/fetch.log
chown nobody:nobody /root/opt/fetch.log
chmod a+rw /root/opt/daily_data/*.csv
chown nobody:nobody /root/opt/daily_data/*.csv

実行権限を忘れずに

$ chmod a+x exec_cmd.sh

ここまで用意したら、常時稼働サーバで起動しておく。

$ docker-compose up -d --build

以上で株価データを無尽蔵から毎日定時に取得する環境が整いました。

次節では、取得した株価データで、自前の株価データベースを更新する機能を追加します。

株価データベースの更新

自前の株価データベースを無尽蔵サイトから一括取得した株価データを元に生成する例を、こちらの記事で解説しました。

無尽蔵サイトから取得した当日の株価データを使用して、この株価データベースを自動で日々更新する機能を追加していきます。 今回想定する株価データベースは、ファイルシステム上の簡易なもので、以下のような構成のファイルです。dataディレクトリには、証券コードの1000の桁毎のサブディレクトリを切り、その中に、銘柄毎の株価日足時系列データのcsvファイルが置かれるものとします。

working_root/
    muzinzo_data/
        Tyymmdd.csv
        ...
    data/
        1000/
            1001.csv
            1002.csv
            ...
        2000/
        ...
        9000/
            9001.csv
            ...

以下はスクリプト例です。

import_todays_data.py
import datetime
import os
import sys
import logging

def add_data(data_dir, code, date_str, cv1, cv2, cv3, cv4, cvc):
    code_dir = code[0]+'000'
    out_path = data_dir + code_dir + '/' + code + '.csv'
    line = date_str+','+cv1+','+cv2+','+cv3+','+cv4+','+str(int(float(cvc)*1000))+'\n'
    with open(out_path,'a') as f:
        f.write(line)

def import_daily_data(daily_file, data_dir):
    now = datetime.datetime.now()
    filename = os.path.basename(daily_file)
    yy = 2000 + int(filename[1:3])
    mm = int(filename[3:5])
    dd = int(filename[5:7])
    date_str = '{0:4d}/{1:02d}/{2:02d}'.format(yy, mm, dd)
    with open(daily_file, mode='rb') as fd:
        lines = fd.readlines()
        for i, line in enumerate(lines):
            try:
                line = line.decode('cp932')
            except:
                logging.error('{} error file {}: line {}'.format(str(now), daily_file, i))
                continue
            line = line.rstrip()
            line_list = line.split(',')
            if len(line_list)==10:
                if len(line_list[1])==4:
                    code = str(line_list[1])
                    name = line_list[3]
                    market = line_list[9]
                    if '名証' in market:
                        next_line = lines[i+1]
                        next_line = next_line.decode('cp932')
                        next_line_list = next_line.split(',')
                        next_code = str(next_line_list[1])
                        if next_code!=code:
                            add_data(data_dir, code, date_str, line_list[4], line_list[5], line_list[6], line_list[7], line_list[8])
                    else:
                        add_data(data_dir, code, date_str, line_list[4], line_list[5], line_list[6], line_list[7], line_list[8])
            else:
                logging.error('{} error file {}: line {} : {}'.format(str(now), daily_file, i, line))
    logging.info(str(now)+' success to update by: '+daily_file)

def main():
    logging.basicConfig(level=logging.INFO)
    args = sys.argv
    if len(args)==3:
        if os.path.isdir(args[1]):
            input_dir = args[1]
            if input_dir[-1]!='/':
                input_dir = input_dir + '/'
            today = datetime.date.today()
            filename = 'T{0:02d}{1:02d}{2:02d}.csv'.format(today.year-2000, today.month, today.day)
            daily_file = input_dir + filename
        else:
            daily_file = args[1]
        if os.path.isdir(args[2]):
            data_dir = args[2]
            if data_dir[-1]!='/':
                data_dir = data_dir + '/'
        else:
            return
        import_daily_data(daily_file, data_dir)

if __name__ == '__main__':
    main()

使用例

$ python import_todays_data.py /full/pathof/working_root/muzinzo_data /full/pathof/working_root/data

第1引数で無尽蔵サイトから取得したファイルが置かれたディレクトリをフルパスで指定し、第2引数で株価データベースのディレクトリをフルパスで指定します。

このスクリプトを、無尽蔵サイトから当日の株価データを取得した直後に引き続き実行させるには、先のcron実行スクリプトexec_cmd.shを次のように修正します。

opt/exec_cmd.sh
#!/bin/bash
/usr/local/bin/python /root/opt/get_todays_csv_zip_from_muzinzo.py /root/opt >> /root/opt/fetch.log 2>&1
chmod a+rw /root/opt/fetch.log
chown nobody:nobody /root/opt/fetch.log
chmod a+rw /root/opt/daily_data/*.csv
chown nobody:nobody /root/opt/daily_data/*.csv
/usr/local/bin/python /root/opt/import_todays_data.py /root/opt /root/opt/data >> /root/opt/update.log 2>&1
chmod a+rw /root/opt/update.log
chown nobody:nobody /root/opt/update.log

以上で、毎日自動で無尽蔵サイトから当日の株価データを取得した後株価データベースを更新する環境が整いました。

まとめ

無一物中Project第4回では、毎日自動で無尽蔵サイトから当日の株価データを取得した後、株価データベースを更新するdocker-composeによる環境構築方法を紹介しました。


Written by questions6768 who lives in Uji, Kyoto.