2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【日本株分析プラットフォーム】株式分割のあと、過去の株価が分割前の値のまま残っていた話(前半)(2026-10-13)

2
Last updated at Posted at 2026-10-12

このプロジェクトについて

EDINET(有価証券報告書等)の開示データをもとに、日本株の財務指標・投資判断スコアを日次で算出・公開しているサービスです。

公開サイトはこちら → https://stocks.happy-life-design.net
前回の記事(2026-10-12公開)はこちら → https://qiita.com/rightcross-ae86/items/4281b9e529f4838dfa0a

この記事について

株式分割(1株を2株にするように、株を分けること)があった会社の過去の株価が、分割前の値のまま残っていた件の記録です。株価のグラフに、価格が分割の前後で急に半分や数分の1になる段差ができていました。

調べると、同じ型の会社は96社ありました。この記事は2部構成の前半です。最初の1社の調査、原因、全社から探した方法、96社の直し方、直した所以外が変わっていないことの確かめ方、直していない会社を書きます。新しい分割のたびに同じ段差ができないようにした仕組みは、後半(公開日は決めていません)に書く予定です。

この記事で扱う修正は、2026-10-09と2026-10-10に本番へ反映しました。開発の記録は2026-10-12の夜までのものです。

この記事では、次のように呼びます。

  • 「行」: 1社の1日分の株価(始値・高値・安値・終値・出来高)。出来高は、その日に売買された株数です
  • 「取得元」: 株価データを取り込んでいる元のサービス
  • 「外部サイト」: 値の照合に使う、別の株価サイト
  • 「分割比」: 1株が何株になったか。1株を2株に分けたなら2、100株に分けたなら100です
  • 「段差」: 価格が分割の前後で、急に半分や数分の1になること
  • 「指標」: 株価から計算している数値。移動平均・MACD・RSI・OBV(出来高の累積)など
  • 「指紋」: 会社ごとに作る目印。行数と、全ての行の中身から作った値(ハッシュ値)をまとめたもの

先に要点

  • 株式分割のあと、過去の株価が分割前の値のまま残っていた会社が、96社ありました。株価のグラフと指標が、分割の前後で歪んでいました
  • 原因は、取得元が分割のあと過去の全日付を調整して返すのに対し、こちらの毎日の取り込みが直近7日分しか取り直さないことでした。7日の窓の中の行だけが調整後の値になり、窓の外の過去の行は分割前の値のまま残りました
  • 最初の1社で原因を確かめ、全社から同じ型を探して、合計96社・407,111行を、価格を分割比で割り、出来高を分割比でかけて直しました
  • 直す前に、バックアップから元に戻せることを本番で確かめ(往復試験)、直したあとは、直した所以外が変わっていないことを、全社の指紋・取得元との全日付の突き合わせ・指標の計算し直しで確かめました
  • 最初は「95社」と数えていましたが、数え漏れが分かり、96社に訂正しました
  • 直したのは今ある会社だけで、新しい分割が起きれば同じ段差ができる状態でした。これを防ぐ仕組みは後半に書きます。未確認事項もあります(取得元に値が存在しない13社など)

何が起きたか(最初の1社)

2026-10-09、ある1社の株価に、2026-09-18の終値6,830円から、2026-09-24の終値3,339.5円まで、約半分になる段差が見つかりました。

取得元の現在の値と、データベースの値を、全日付で突き合わせました。

期間 行数 データベースの終値 ÷ 取得元の終値 出来高の比
2026-09-18以前 6,633 ちょうど2倍(例外0) ちょうど0.5倍(例外0)
2026-09-24以降 11 ちょうど1倍 ちょうど1倍

取得元の分割の記録は、「1株を2株に分割・効力発生日2026-09-29」でした。2026-09-18以前の行は、分割前の値のまま残っていたことになります。

段差は、効力発生日の2026-09-29ではなく、2026-09-18と2026-09-24の間にできていました。その理由が、原因です。

原因: 取得元は過去を調整して返し、取り込みは直近7日しか取り直さない

  • 取得元は、株式分割が起きると、効力発生日より前の全ての日付の株価を、分割後の基準にさかのぼって調整して返します(遡及調整)
  • データベースは、取り込んだ時点の値を保存します。毎日の取り込みは、直近7日分だけを取り直して上書きします(7日ローリング更新。取り直す直近7日を、以下「窓」と呼びます)
  • 分割のあとの取り込みでは、窓の中の行だけが調整後の値になり、窓の外の過去の行は、分割前の値のまま残ります
  • 最初の1社は、効力発生日の2026-09-29の取り込みで、窓の中にあった2026-09-24〜09-28の3行だけが調整後の値に更新されました。2026-09-18以前は分割前のままです。段差は、効力発生日でなく、窓の外と中の境目にできました
  • 分割を記録する表はありましたが、中身は0件で使われておらず、分割の調整をする仕組みは、データベースの側にありませんでした

実害は、長期のチャートと、移動平均・RSI・MACD・OBVなどの指標が、分割の前後で歪むことです。この株価データを使っている別のシステムの計算にも影響します。

同じ型の会社を、全社から探した

2026-06-01以降で、前日の終値と比べて1.7倍以上、または0.6倍以下になった行を、全社から探しました(1つ目の抜粋)。121行見つかり、その会社を、取得元の分割の記録と、データベースと取得元の終値の比で分類しました。

分類 社数 内容
分割の未調整(最初の1社と同じ型) ほかに94社 取得元に分割の記録があり、段差より前のデータベースの値が、取得元の現在の値の分割比倍のまま
取得元の側にも同じ段差がある 11社 別の問題。この時点では調べていない
取得元から値を取れない等 6社 この時点では調べていない

分割の未調整は、最初の1社を含めて95社でした。効力発生日は、2026-06-29が28社、2026-07-30が7社、2026-08-27が1社、2026-08-28が4社、2026-09-14が1社、2026-09-29が53社(最初の1社を含む)、2026-10-05が1社(1株を100株に分割)です。分割比は2〜100倍で、全社で、データベースと取得元の比が分割比と一致しました。

勝手には直さなかった

最初の1社は、2026-10-09に直しました。残りの94社は、すぐには直しませんでした。この株価データを読んでいる別のシステムが、自前で分割の調整をしていると、二重に調整してしまうおそれがあったためです。確認と判断を先にしました。

別のシステム側で二重にならないと確認できたので、2026-10-10に、残りの94社を直すことにしました。

直し方

会社ごとに、取得元の現在の調整後の履歴とデータベースを、全日付で突き合わせて、行を3つに分けます(2つ目の抜粋)。

  • 旧スケール: データベースの終値 ÷ 取得元の終値が、分割比にほぼ等しい行(分割前の値のまま)
  • 調整済み: その比が1にほぼ等しい行
  • どちらでもない行

94社の全社で、直近の分割は1回で、旧スケールの行は、古い日付から連続した範囲になっていました。旧スケールの行の範囲を、始値・高値・安値・終値・調整後終値(分割や配当で調整した終値)を分割比で割り、出来高を分割比でかけて直します(3つ目の抜粋)。

段差ができた日が同じ会社を1グループとして、7グループ(1社・1社・1社・4社・7社・28社・52社)を、1グループずつ直しました。直した行数は、グループごとに281行から217,610行で、合計400,363行です。

各グループで、次の手順を、途中で失敗したら即停止する専用のシェルスクリプトで順に実行しました(パイプの末尾が成功を返して失敗が隠れるのを避けるため、set -eo pipefailを使います)。

  1. 触る行を全列のバックアップに保存する
  2. 全社の指紋を保存する
  3. 適用して検証し、必ず取り消す(dry。データベースの変更を取り消す操作を、ROLLBACKと呼びます)
  4. 適用して、バックアップから元に戻し、指紋が適用前と一致することを確かめてから、必ず取り消す(往復試験。4つ目の抜粋)
  5. 本番に適用する
  6. 補正した会社の指標を、補正後の株価から全履歴で計算し直す
  7. 検証し、全社の指紋を取り直す

例外が2種類あった

  • 別の取得元から補った行が混ざった4社: 旧スケールの範囲の中に、あとから別の外部サイトで補った行(計74行)がありました。取得元の該当日が途中経過値のため、比が分割比のちょうどにはならず、分割比の0.95倍〜1.05倍の範囲にありました。全て「補った行」であり、比が分割比の0.9倍〜1.1倍以内であることを確かめたうえで、他の行と同じく直しました
  • 履歴の先頭が、取得元自身で未調整の2社: 履歴の先頭の5行と8行は、取得元自身が分割未調整のまま返していました(データベースは取得元と一致)。この行は変更していません。そのため、先頭の行と次の行の間に、取得元と同じ段差が残っています

取得元に記録が無い1社

取得元に分割の記録が無い1社も、同じ型でした。この会社は、外部サイトで確かめました。生の終値5,200円に対し、外部サイトの調整後終値は1,733.3円で、3で割った値です。調整後終値が終値と一致する最初の日が、分割の効力発生日(2026-06-29)でした。

分割より前の範囲に、取引がなかった日の行が15行あり、この15行だけは別の経路で入れたため、最初から新しい値でした。日付の範囲で一括して直すと、この15行を壊します。そこで、始値が入っている行(取引日の行)だけを対象にして、115行を3で割り、出来高を3倍にしました。

確かめたこと

最初の1社は、本番で往復試験をしてから反映しました。6,633行が、変更前の値の半分(差は最大0.00005。データベースの小数4桁の丸めの範囲)、出来高が2倍(完全一致)になりました。補正の前後でつながり、2026-09-18から2026-09-24の前日比は0.978でした。

94社は、7グループとも、次の6つを確かめました。

確かめたこと 結果
直した行が、期待した値になっている 価格は変更前を分割比で割った値(差は最大0.00005)、出来高は変更前に分割比をかけて四捨五入した値と完全一致。他の列は変わらず、更新日時だけが更新された
直した範囲の外の行が、変わっていない 範囲の外の行の指紋が、適用前と同一
取得元の現在の値と、全日付で合っている 価格は、取得元の値が持つ丸めの誤差の範囲内で全行一致(別の外部サイトから補った行を除く)。範囲より後ろの行は、終値が取得元の0.99倍〜1.01倍以内。出来高は、取得元の側の既存の食い違い日と、補正の前後で同一
段差が残っていない 前日の終値の0.4倍未満、または1.6倍超になった日は、全て取得元の系列にも同じ日にある
指標が合っている 日付が株価と一致し、15種類の指標が、株価から独立に計算し直した値と許容内で一致
他の会社が変わっていない 全社の指紋で、対象外の会社は株価・指標・企業情報とも差分0・追加0・消滅0

取りこぼしの確認と、直していない会社

修正のあとに、開始日を2025-01-01まで広げ、しきい値を1.4倍と0.7倍に下げて(2株を3株にする程度の小さな分割も拾えるように)、全社を走査し直しました。候補は117社(156行)でした。

分類 社数
分割の未調整の型 あと1社(上場投資信託(ETF))
取得元の側にも同じ段差がある 103社
取得元に値が存在しない 13社

94社は、全て候補から消えていました。

残る1社(ETF)は、2026-03-30に前日比0.098の段差(3,827円から376.4円)があります。1株を10株に分割した日と推定していますが、取得元自身の系列が壊れていて(2009〜2014年が未調整、2015-01-05〜2026-03-27が10分の1済み、2026-03-30〜31がさらに10分の1、2026-04-01以降が正常)、基準にできません。分割の記録も空です。2026-03-30以降のデータベースの値は実際の価格で正しく、2026-03-27以前の全ての行を10分の1にするのが正しい修正だと見ていますが、「取得元と一致」を検証の基準にできないため、94社と同じ手順では直せません。扱いは未決です。

数え方の訂正: 95社でなく96社

開発の記録の一部に、直した会社を「95社」と書いたものがありました。別のシステムが、全社・全日付で、自分の保存値とこちらの値を突き合わせたときに、この数が誤りだと指摘されました。95社は「最初の1社と、取得元に分割の記録がある94社」で、取得元に記録が無く、外部サイトで別に見つけた1社が入っていませんでした。正しくは96社です。開発の記録は、96社に訂正しました。

技術的な補足(実際に使ったコード)

ここに載せるのは、実際に使ったスクリプトからの抜粋です。実物と変えたのは、共通のインデントを除いたことだけです。データベースの接続情報の行は載せていません。

1. 全社から段差を探す(読み取り専用)

lagは、会社ごとに日付順に並べて、1つ前の行の値を取る関数です。co.is_activeは、現在も上場している会社だけに絞る条件です。

cur.execute("""with p as (select s.company_id, s.trade_date, s.close_price c, s.volume v, lag(s.close_price) over (partition by s.company_id order by s.trade_date) pc
  from stock_prices s where s.trade_date >= '2026-06-01')
  select co.id, co.ticker, p.trade_date, p.pc, p.c from p join companies co on co.id=p.company_id and co.is_active where p.pc>0 and (p.c/p.pc >= 1.7 or p.c/p.pc <= 0.6) order by 2,3""")

2. 旧スケールの行を見分ける

bothは、データベースと取得元の両方にある日の表、Rは取得元の分割比です。ratioが分割比にほぼ等しい行をold(分割前の値のまま)、1にほぼ等しい行をnew(調整済み)、どちらでもない行をothに分けます。許容は0.002です。

ratio = both.c / both.Close
old = both[(ratio / R - 1).abs() <= 2e-3]; new = both[(ratio - 1).abs() <= 2e-3]
oth = both.drop(old.index.union(new.index))

3. 会社ごとに直す

Rが分割比です。価格の5つの列をRで割り、出来高をR倍して四捨五入し、更新日時を更新します。WHEREは会社と日付の範囲を定数で指定した1文で、圧縮された過去の行の更新で、一度に解凍する行数の上限に当たらないようにするための形です。assertは、更新した行数が計画と同じことと、範囲の外の行の指紋が適用前と同じことを確かめ、違えば例外で止まります。%sは、値を入れる場所です。

for r in P.itertuples():
    cid = int(r.company_id); d = pre[cid]
    cur.execute("""UPDATE stock_prices SET
        open_price = open_price / %s, high_price = high_price / %s, low_price = low_price / %s,
        close_price = close_price / %s, adjusted_close = adjusted_close / %s,
        volume = round(volume * %s), updated_at = now()
      WHERE company_id = %s AND time >= %s AND time < %s""", (d['R'],) * 5 + (d['R'], cid, d['lo'], d['hi']))
    assert cur.rowcount == d['n'], f'{r.ticker}: UPDATE行数 {cur.rowcount} != {d["n"]}'
    tot += cur.rowcount
    o2 = outside(cid, d['lo'], d['hi'])
    assert o2 == d['out'], f'{r.ticker}: 範囲外の行が変化している {o2} != {d["out"]}'

4. 往復試験: バックアップから戻して、元と同じになるかを確かめる

バックアップのCSVから、対象の行を削除して書き戻し(DELETEとCOPY)、会社ごとの指紋が適用前と一致することを確かめます。災害のときの復旧と同じ経路です。このあと、必ず取り消します。

raw = gzip.open(BASE + f'bk/{group}_stock_prices.csv.gz', 'rt').read()
assert raw.count('\n') - 1 == tot, 'バックアップ行数が対象と不一致'
for r in P.itertuples():
    cid = int(r.company_id); d = pre[cid]
    cur.execute("DELETE FROM stock_prices WHERE company_id=%s AND time>=%s AND time<%s", (cid, d['lo'], d['hi'])); assert cur.rowcount == d['n']
cur.copy_expert("COPY stock_prices FROM STDIN WITH CSV HEADER", io.StringIO(raw))
for r in P.itertuples():
    cid = int(r.company_id); assert one(FP_ALL, cid) == pre[cid]['all'], f'{r.ticker}: 復元後の指紋が適用前と不一致'

未確認事項

  • 取得元に値が存在しない13社: データベースに段差がありますが、確かめる手段がなく、手を付けていません
  • 取得元自身の系列が壊れているETF1社: 直し方は決まっていません。10分の1にするか、注記だけにするかは未決です
  • 取得元の現在の値を正としていること: 直した行は、取得元の現在の値と全日付で合っていることを確かめましたが、取得元の値そのものが正しいかは、別の確認が必要です
  • 走査の範囲: 走査したのは、2025-01-01以降の、前日の終値と比べて1.4倍以上または0.7倍以下の変化だけです。それより前に起きた分割と、1.4倍に届かない小さな変化は、走査の対象に入っていません
  • 新しい分割で同じ段差ができるか: この記事の時点の修正は、今ある96社だけを直したものです。再発を防ぐ仕組みは、後半に書きます

まとめ

  • 原因は、取得元が過去を調整して返すのに対し、取り込みが直近7日しか取り直さないことでした。分割のあと、窓の外の過去の行が分割前の値のまま残った会社は、96社でした(「95社」は数え漏れで訂正)
  • 直す前に往復試験で元に戻せることを確かめ、直したあとは、全社の指紋・取得元との全日付の突き合わせ・指標の計算し直しで、直した所以外が変わっていないことを確かめました
  • 取得元に値が存在しない13社と、取得元自身の系列が壊れているETF1社は、直していません。新しい分割で同じ段差ができないようにする仕組みは、後半に書きます

気になる点・ご質問があればコメントでお知らせください。

2
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?