【第5回】出来ず株価の補填と株式分割を自動調整してチャート表示

December 11, 2020

株価サーバーテクニカル分析python

無一物中Project記事一覧


はじめに

ネットで公開されている株価データ(例えば無尽蔵株価データ倉庫など)は、株式分割時の株価の調整の情報が欠落しているものがあります。

本サイトでは、無尽蔵で公開している株価データの利用例を公開していますが、無尽蔵も例外ではなく、株式分割時の株価の調整はなされていません。

また、これらのサイトでは、出来なかった銘柄の株価は0円として公開されているケースがあります。そのサイトから取得した株価をそのまま、チャート表示させると、正しいチャートは表示されません。

本記事では、株価が0の出来ず銘柄への前日終値の補填と、株価調整されていない日足時系列データから、自動で株価調整を試みます。

出来ず株価の補填

以下のような株価データがあったとします。出来高少ないですよね。出来高0の日は株価も0になっています。

fill_zero_1.png

株価が0の出来ず銘柄への前日終値の補填をしていきます。

まずは、pandasでcsvファイルを読み込みます。

import pandas as pd
df = pd.read_csv(path, header=None, names=['Date','Open','High','Low','Close','Volume'], encoding='UTF-8')
df['Date'] = pd.to_datetime(df['Date'])
df = df.set_index("Date")

DataFrameにはNaNのときだけ何かに置き換えるというメソッドが充実していますので、まずは補填対象となる’Close’列の0円をNaNに変更します。

import numpy as np
    f_zero2nan = lambda x: np.NaN if x==0 else x
    df['Close'] = df['Close'].map(f_zero2nan)

結果はこれ

fill_zero_2.png

そのNaNを前日の終値で補填します。1行目が補填対象であってっも前日終値がないので補填できないので、やむおえず、翌日株価で補填しています。(厳密には間違いですけど)

    df = df.fillna(method='ffill')
    if np.isnan(df['Close'][0]):
        df = df.fillna(method='bfill')

ここまでの結果はこれ

fill_zero_3.png

次に、補填対象となる’Open’,‘High’,‘Low’列を’Close’列の値で補填すれば完成です。

    df['Open'] = df['Open'].map(f_zero2nan)
    df['High'] = df['High'].map(f_zero2nan)
    df['Low'] = df['Low'].map(f_zero2nan)
    df['Open'] = df['Open'].fillna(df['Close'])
    df['High'] = df['High'].fillna(df['Close'])
    df['Low'] = df['Low'].fillna(df['Close'])

fill_zero_4.png

もっとよい書き方もあるのかもしれませんが、まぁ動くからいいか。ちなみに、当方python初心者ですので。。。

株式分割後株価の調整

例えば、日本版コストコとの呼び名もある「業務スーパー」でお馴染みの「3038 神戸物産」のチャートを見てみると、このようになります。

3038-1.pngg

2015年以降、なんと5回も分割してるんですね。恐るべし。

こんなチャートでは使い物にならないので、株価調整は必要ですね。証券会社のサイトには、株式分割の履歴がまとめられていますから、これを取り込んで調整するべきなんでしょうが、なるべくお手軽にやりたいんで、自動調整を試みます。

  • 値幅制限以上の変動があったら、株式分割があったものとみなし、自動調整する。

という方針でやってみます。分割比率が1:2とかだったらこれでいいんですが、分割比率が1:1.1とか1.1.2とかもある訳で、そのような場合は分割を見逃してしまいます。自動調整の限界ですかね。

値幅制限以上の変動を調べるコードを力づくで書きました。

adjust_close_value.py
from collections import namedtuple

PriceLimit = namedtuple("PriceLimit", "l h w")
price_limit_table1 = [
    PriceLimit(0, 100, 30),
    PriceLimit(100, 200, 50),
    PriceLimit(200, 500, 80),
    PriceLimit(500, 700, 100),
    PriceLimit(700, 1000, 150),
]
price_limit_table2 = [
    PriceLimit(1000, 1500, 300),
    PriceLimit(1500, 2000, 400),
    PriceLimit(2000, 3000, 500),
    PriceLimit(3000, 5000, 700),
    PriceLimit(5000, 7000, 1000),
    PriceLimit(7000, 10000, 1500),
]

def normal_price(v0, v):
    if v==0 or v0==0:
        return True
    for price_limit in price_limit_table1:
        if price_limit.l<=v0 and v0<price_limit.h:
            if abs(v-v0)<=price_limit.w:
                return True
            else:
                return False
    for i in range(0, 5):
        a = pow(10, i)
        for price_limit in price_limit_table2:
            if price_limit.l*a<=v0 and v0<price_limit.h*a:
                if abs(v-v0)<=price_limit.w*a:
                    return True
                else:
                    return False
    if abs(v-v0)<=10000000:
        return True
    else:
        return False

normal_price()に前日と当日の終値を与えると、値幅制限以内ならTrueを返します。

実はこれは正確ではなく、以下のルールが未実装です。

  • 連続してストップ高やストップ安が続いた場合、値幅制限が拡大する場合がある
  • 値幅制限いっぱいになったとき、呼値以下の桁をまるめる

株価調整の力づくの実装例は以下です。pandasで読み込んだ表を渡します。

所詮素人が書いたコードなので、pandasの表を更新するところは、突っ込みどころ満載でしょうがお目こぼしを。

adjust_close_value.py
import pandas as pd
from decimal import Decimal, ROUND_HALF_EVEN

def adjust_price_value(df):
    n = len(df)
    adjust_rate = 1.0
    v0 = df['Close'][n-1]
    for i in reversed(range(0, n-2)):
        v = df['Close'][i]
        if not normal_price(v0, v):
            next_adjust_rate = v0 / v
            if next_adjust_rate>=1.0:
                next_adjust_rate = int(Decimal(str(next_adjust_rate)).quantize(Decimal('0'), rounding=ROUND_HALF_EVEN))
            else:
                rev_next_adjust_rate = v / v0
                rev_next_adjust_rate = int(Decimal(str(rev_next_adjust_rate)).quantize(Decimal('0'), rounding=ROUND_HALF_EVEN))
                next_adjust_rate = 1.0/rev_next_adjust_rate
            adjust_rate = adjust_rate * next_adjust_rate
            print('i={}, v0={}, v={} radjust={}'.format(i, v0, v, adjust_rate))
        v0 = v
        if adjust_rate!=1.0:
            df.iloc[i, 0] = int(df.Open[i]*adjust_rate)
            df.iloc[i, 1] = int(df.High[i]*adjust_rate)
            df.iloc[i, 2] = int(df.Low[i]*adjust_rate)
            df.iloc[i, 3] = int(df.Close[i]*adjust_rate)
            df.iloc[i, 4] = int(df.Volume[i]/adjust_rate)

このコードを使って調整したチャートをプロットします。

adjust_close_value.py

def load_stock_price_csv(path):
    df = pd.read_csv(path, header=None, names=['Date','Open','High','Low','Close','Volume'], encoding='UTF-8')
    df['Date'] = pd.to_datetime(df['Date'])
    df = df.set_index("Date")
    adjust_price_value(df)
    return df

def main():
    df = load_stock_price_csv('3038.csv')
    mpf.plot(df, type='candle', volume=True)

で、結果はこれ。

3038-2.png

業務スーパー恐るべし。

出来ず銘柄補填&株式分割自動調整モジュールのパッケージ化

前節で紹介した出来ず銘柄補填&株式分割の簡易自動調整をパッケージ化し、簡単に呼び出せるようにしておきます。

とりあえず、ベタですが、パッケージ名をmusinzoとし、musinzoサブディレクトリを切って、その中にadjust_close_value.pyを置きます。

ディレクトリ構成

working_directory/
    *.py  パッケージを利用する側のソース
    data/ 株価データベース
        1000/
            1001.csv
            ...
    muzinzo/
        adjust_close_value.py パッケージのソース
musinzo/adjust_close_value.py
# -*- coding: utf-8 -*-
import os
import numpy as np
import pandas as pd
import mplfinance as mpf
from collections import namedtuple
from decimal import Decimal, ROUND_HALF_EVEN

PriceLimit = namedtuple("PriceLimit", "l h w")
price_limit_table1 = [
    PriceLimit(0, 100, 30),
    PriceLimit(100, 200, 50),
    PriceLimit(200, 500, 80),
    PriceLimit(500, 700, 100),
    PriceLimit(700, 1000, 150),
]
price_limit_table2 = [
    PriceLimit(1000, 1500, 300),
    PriceLimit(1500, 2000, 400),
    PriceLimit(2000, 3000, 500),
    PriceLimit(3000, 5000, 700),
    PriceLimit(5000, 7000, 1000),
    PriceLimit(7000, 10000, 1500),
]

def load_stock_price_timeslice(code, adjust=True, term=0):
    code_dir = code[0]+"000"
    path = "./data/" + code_dir + "/" + code + ".csv"
    return load_stock_price_csv(path, adjust=adjust, term=term)
    
def load_stock_price_csv(path, adjust=True, term=0):
    if os.path.exists(path):
        df = pd.read_csv(path, header=None, names=['Date','Open','High','Low','Close','Volume'], encoding='UTF-8')
    else:
        df = pd.DataFrame([[0, 0, 0, 0, 0, 0]])
        df.columns = ['Date','Open','High','Low','Close','Volume']
    df['Date'] = pd.to_datetime(df['Date'])
    df = df.set_index("Date")
    
    # 株価が0の場合、前日の終値で埋める
    f_zero2nan = lambda x: np.NaN if x==0 else x
    df['Close'] = df['Close'].map(f_zero2nan)
    df = df.fillna(method='ffill')
    if np.isnan(df['Close'][0]):
        df = df.fillna(method='bfill')
    df['Open'] = df['Open'].map(f_zero2nan)
    df['High'] = df['High'].map(f_zero2nan)
    df['Low'] = df['Low'].map(f_zero2nan)
    df['Open'] = df['Open'].fillna(df['Close'])
    df['High'] = df['High'].fillna(df['Close'])
    df['Low'] = df['Low'].fillna(df['Close'])
    
    if adjust:
        adjust_price_value(df)
    if term>0:
        df = df.tail(term)
    return df

def normal_price(v0, v):
    if v==0 or v0==0:
        return True
    for price_limit in price_limit_table1:
        if price_limit.l<=v0 and v0<price_limit.h:
            if abs(v-v0)<=price_limit.w:
                return True
            else:
                return False
    for i in range(0, 5):
        a = pow(10, i)
        for price_limit in price_limit_table2:
            if price_limit.l*a<=v0 and v0<price_limit.h*a:
                if abs(v-v0)<=price_limit.w*a:
                    return True
                else:
                    return False
    if abs(v-v0)<=10000000:
        return True
    else:
        return False

def adjust_price_value(df):
    n = len(df)
    adjust_rate = 1.0
    v0 = df['Close'][n-1]
    for i in reversed(range(0, n-2)):
        v = df['Close'][i]
        if not normal_price(v0, v):
            next_adjust_rate = v0 / v
            if next_adjust_rate>=1.0:
                next_adjust_rate = int(Decimal(str(next_adjust_rate)).quantize(Decimal('0'), rounding=ROUND_HALF_EVEN))
            else:
                rev_next_adjust_rate = v / v0
                rev_next_adjust_rate = int(Decimal(str(rev_next_adjust_rate)).quantize(Decimal('0'), rounding=ROUND_HALF_EVEN))
                next_adjust_rate = 1.0/rev_next_adjust_rate
            adjust_rate = adjust_rate * next_adjust_rate
            #print('i={}, v0={}, v={} adjust={}'.format(i, v0, v, adjust_rate))
        v0 = v
        if adjust_rate!=1.0:
            df.iloc[i, 0] = int(df.Open[i]*adjust_rate)
            df.iloc[i, 1] = int(df.High[i]*adjust_rate)
            df.iloc[i, 2] = int(df.Low[i]*adjust_rate)
            df.iloc[i, 3] = int(df.Close[i]*adjust_rate)
            df.iloc[i, 4] = int(df.Volume[i]/adjust_rate)

パッケージを利用する側のコード例 この例では、データベースから証券コード3038の株価データを取りだし、mplfinanceでローソク足チャート表示する。

import mplfinance as mpf
import muzinzo.adjust_close_value as mz

df = mz.load_stock_price_timeslice('3038', adjust=True, term=100)
mpf.plot(df, type='candle', volume=True)

まとめ

無一物中Project第5回では、(株価が0の)出来ず銘柄への前日終値の補填と、株式分割銘柄の株価を自動調整する方法を提案しました。ただし、自動調整は、分割比率が小さい場合は誤動作の可能性があり、この方法には限界があります。


Written by questions6768 who lives in Uji, Kyoto.