【第5回】出来ず株価の補填と株式分割を自動調整してチャート表示
December 11, 2020
株価サーバー テクニカル分析 python無一物中Project記事一覧
- 【第1回】株価サーバーの構築の概要
- 【第2回】無尽蔵から株価データの一括取得
- 【第3回】無尽蔵株価データから銘柄毎の時系列データを生成
- 【第4回】無尽蔵から自動で株価データを取得し株価データベースを更新
- 【第5回】出来ず株価の補填と株式分割を自動調整してチャート表示(本記事)
- 【第6回】mplfinanceを使用してテクニカルチャート表示
- 【第7回】テクニカル指標による銘柄スクリーニング
- 【第8回】株価チャート配信サーバー
- 【第9回】チャート形状の認識
はじめに
ネットで公開されている株価データ(例えば無尽蔵や株価データ倉庫など)は、株式分割時の株価の調整の情報が欠落しているものがあります。
本サイトでは、無尽蔵で公開している株価データの利用例を公開していますが、無尽蔵も例外ではなく、株式分割時の株価の調整はなされていません。
また、これらのサイトでは、出来なかった銘柄の株価は0円として公開されているケースがあります。そのサイトから取得した株価をそのまま、チャート表示させると、正しいチャートは表示されません。
本記事では、株価が0の出来ず銘柄への前日終値の補填と、株価調整されていない日足時系列データから、自動で株価調整を試みます。
出来ず株価の補填
以下のような株価データがあったとします。出来高少ないですよね。出来高0の日は株価も0になっています。
株価が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)
結果はこれ
そのNaNを前日の終値で補填します。1行目が補填対象であってっも前日終値がないので補填できないので、やむおえず、翌日株価で補填しています。(厳密には間違いですけど)
df = df.fillna(method='ffill')
if np.isnan(df['Close'][0]):
df = df.fillna(method='bfill')
ここまでの結果はこれ
次に、補填対象となる’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'])
もっとよい書き方もあるのかもしれませんが、まぁ動くからいいか。ちなみに、当方python初心者ですので。。。
株式分割後株価の調整
例えば、日本版コストコとの呼び名もある「業務スーパー」でお馴染みの「3038 神戸物産」のチャートを見てみると、このようになります。
2015年以降、なんと5回も分割してるんですね。恐るべし。
こんなチャートでは使い物にならないので、株価調整は必要ですね。証券会社のサイトには、株式分割の履歴がまとめられていますから、これを取り込んで調整するべきなんでしょうが、なるべくお手軽にやりたいんで、自動調整を試みます。
- 値幅制限以上の変動があったら、株式分割があったものとみなし、自動調整する。
という方針でやってみます。分割比率が1:2とかだったらこれでいいんですが、分割比率が1:1.1とか1.1.2とかもある訳で、そのような場合は分割を見逃してしまいます。自動調整の限界ですかね。
値幅制限以上の変動を調べるコードを力づくで書きました。
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の表を更新するところは、突っ込みどころ満載でしょうがお目こぼしを。
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)
このコードを使って調整したチャートをプロットします。
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)
で、結果はこれ。
業務スーパー恐るべし。
出来ず銘柄補填&株式分割自動調整モジュールのパッケージ化
前節で紹介した出来ず銘柄補填&株式分割の簡易自動調整をパッケージ化し、簡単に呼び出せるようにしておきます。
とりあえず、ベタですが、パッケージ名をmusinzoとし、musinzoサブディレクトリを切って、その中にadjust_close_value.pyを置きます。
ディレクトリ構成
working_directory/
*.py パッケージを利用する側のソース
data/ 株価データベース
1000/
1001.csv
...
muzinzo/
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の)出来ず銘柄への前日終値の補填と、株式分割銘柄の株価を自動調整する方法を提案しました。ただし、自動調整は、分割比率が小さい場合は誤動作の可能性があり、この方法には限界があります。