このプロジェクトについて
EDINET(有価証券報告書等)の開示データをもとに、日本株の財務指標・投資判断スコアを日次で算出・公開しているサービスです。
公開サイトはこちら → https://stocks.happy-life-design.net
前回の記事(2026-10-11公開)はこちら → https://qiita.com/rightcross-ae86/items/62ab81f505124b0a6613
この記事について
毎朝の保守処理には、データベースの株価と指標が決めた条件に合っているかを確かめる「完全性チェック」があります。このチェックが、新規上場の銘柄で2日続けて失敗した件の記録です。
データが壊れていたのではありませんでした。原因は、チェックの判定と、毎日の指標の計算の2か所にありました。何が起きたか、原因が2つ重なっていたこと、どう直したか、どう確かめたかを書きます。直したあとに、追加したSQLでチェックが遅くなることも見つかったので、その対処も書きます。
この記事で扱う修正は、2026-10-09に本番へ反映しました。開発の記録は2026-10-11の夜までのものです。
この記事では、次のように呼びます。
- 「完全性チェック」: データベースの株価と指標が、決めた条件に合っているかを、毎朝確かめる処理。条件に合わないと失敗して、通知が来ます
- 「指標」: 株価から計算している数値。移動平均・MACD・RSI・ボリンジャーバンド・OBV(出来高の累積)など
- 「新規上場銘柄」: 上場して間もなく、株価の履歴が短い銘柄
- 「日次の計算」: 毎日の取り込みのあとに、指標を計算してデータベースに書く処理。通常は、最新の1日分だけを書きます
- 「行」: 1社の1日分の株価、または1社の1日分の指標
先に要点
- 新規上場の銘柄で、完全性チェックが2日続けて失敗しました。株価ではなく、指標の過去分が欠けていました
- 原因は2つ重なっていました。(1)株価が5行未満の銘柄は指標を作らない設計なのに、チェックがそれを異常と判定していた (2)日次の計算が、株価が5行未満のときに登録された銘柄の過去分の指標を、あとから埋めない作りだった
- (1)だけ直しても、数日後に(2)で同じ通知が再発します。そこで、両方を直しました。日次の計算は、株価が30行以下の銘柄だけ、全履歴を書くようにしました
- 直したあと、追加したSQLのせいでチェック全体の実行時間が約77秒から約180秒に延びることが分かり、SQLを実行する直前に、コンピュータが直接動かせる形へ変換して速くする仕組み(JITコンパイル)を切って対処しました
- 確かめは3通りです。直した銘柄の指標を株価から計算し直す・新旧のチェックを本番のデータベースで5つの状態に当てはめて比べる・本番で穴を再現して日次の計算を実行する
- 未確認事項があります。全履歴を書き込む処理が、本番の毎日の取り込みで実際に動いた記録の確認は、その1つです
何が起きたか
2026-10-08の朝、完全性チェックが失敗して、通知が来ました。2026-10-01に上場した6銘柄が、株価4行・指標0行でした。
翌2026-10-09の朝も、2銘柄で失敗しました。こちらは株価5行・指標1行で、欠けている指標の過去分は、各4行でした。
| 日 | 失敗した銘柄 | 株価 | 指標 |
|---|---|---|---|
| 2026-10-08の朝 | 6銘柄 | 4行 | 0行 |
| 2026-10-09の朝 | 2銘柄 | 5行 | 1行 |
原因は2つ重なっていた
原因1: チェックが、新規上場の途中の状態を異常と判定した
指標は、株価が5行以上ある銘柄にだけ作ります。5行未満の銘柄には、指標を作らない設計です。
一方、完全性チェックは、2026-09-18に入れたもので、「株価のある企業は、株価と指標が1対1になっている」ことを確かめます。ずれている企業が1社でもあれば異常とする設定です。
新規上場の銘柄が株価4行・指標0行なのは、設計どおりの途中の状態です。それなのに、このチェックは異常と判定しました。新規上場の銘柄が引っかかったのは、2026-10-08が初めてでした。
原因2: 日次の計算が、過去分の指標をあとから埋めない
こちらが、実害のある原因です。新規上場の銘柄で、指標の過去分が欠けたままになります。
- 日次の計算は、通常、最新の1行だけを書きます。書く行を絞って、計算の負荷を抑えているためです
- 株価が5行未満のときに企業が登録されると、指標は作られません。5行目が「当日の行」として届いたとき、書かれる指標は、その日の1行だけです
- 過去の日の株価は更新されていないので、「過去の日の株価が訂正された銘柄は、全履歴を計算し直す」という既存の仕組みの対象にもなりません
- 定期的に全履歴を計算し直す処理も、ありません。そのため、自然には埋まりません
(1)だけ直すと、チェックは通るようになります。けれども、指標の過去分が欠けたままの銘柄が、静かに増えます。数日後、別の銘柄の株価が5行に届いたときに、同じ形の通知が、今度は(1)の誤判定ではなく本物の穴として出ます。(1)と(2)は、両方を直す必要がありました。
直したこと
直したのは3か所です。
- 欠けていた指標を埋めた: 失敗していた2銘柄の指標を、全履歴で計算し直しました(各1行から5行へ)
- チェックの判定を直した: 「株価が5行未満で、指標が0行」の銘柄を、異常から除きました。指標が1行でもあるのに株価が5行未満なのは、矛盾なので、これまでどおり異常にします
- 日次の計算を直した: 株価が30行以下の銘柄は、最新の1行ではなく、全履歴を書くようにしました。全履歴を書き直しても、同じ株価からは同じ値になるので、何度書いても結果は変わりません。対象は、上場してから約6週間の銘柄で、約30社と見積もっています
チェックの判定は、次のように変わりました。
| 株価の行数 | 指標の行数 | 直す前 | 直したあと |
|---|---|---|---|
| 十分にある | 株価と同じ | 正常 | 正常 |
| 4行 | 0行 | 異常 | 正常(新規上場の途中の状態) |
| 5行 | 1行 | 異常 | 異常(本物の穴) |
| 4行 | 1行 | 異常 | 異常(矛盾) |
| 4行 | 5行 | 異常 | 異常(株価に無い日の指標がある) |
追加したSQLで、チェックが遅くなった
チェックを直すとき、「株価が5行未満の銘柄を除く」ために、株価の通算の行数を数えるSQLを足しました。ところが、足したSQLのせいで、チェック全体の実行時間が約77秒から約180秒に延びることを、実測で見つけました。このチェックには240秒の上限があります。上限に近づく延び方でした。
原因は2つありました。
-
数える範囲: 株価の表は、古い期間を週ごとのまとまり(チャンク)に圧縮して保存していて、1,400を超えるチャンクがあります。通算の行数を数える条件を、株価の全行に対して付けると、約4秒のSQLが約100秒になりました(実測)。そこで、先に「指標に無い株価の行を持つ会社」だけを確定させ(
MATERIALIZEDは、先に結果を確定させて保存しておく指定です)、その少数の会社にだけ、通算の行数を数えるようにしました - 実行直前の変換の仕組み: データベース(PostgreSQL)には、SQLを実行する直前に、コンピュータが直接動かせる形へ変換して速くする仕組み(JITコンパイル)があります。コストの見積もりがしきい値を超えると、働きます。足したSQLは、見積もりが大きくなり、約5,800の関数の変換に約27秒かかっていました。サブクエリ自体が実行されない場合でも、変換は先に行われます。接続の直後にこの仕組みを切ると、このSQLは約4秒でした
最終的に、チェックの接続の直後にSET jit = offを入れました。この仕組みを切る範囲は、そのチェックの接続だけです。
確かめたこと
確かめは3通りです。
- 直した2銘柄の指標を、株価から計算し直した: 指標の日付が、株価の日付と一致しました。株価だけから別に計算し直した値との差は、最大0.00005でした(データベースの小数4桁の丸めの範囲です)。もともとあった1行は、全ての列が変わっていません。2銘柄以外の4,618社は、株価・指標・企業情報とも、直す前と完全に一致しました
- 新旧のチェックのSQLを、本番のデータベースで5つの状態に当てはめて比べた: 「十分な株価と指標がそろった状態」「株価4行・指標0行」「株価5行・指標1行」「株価4行・指標5行」「株価4行・指標1行」を、本番のデータベースの中に作って、最後に取り消し(ROLLBACK)ました。新規上場の途中の状態だけが除かれ、本物の穴と矛盾は、これまでどおり検知されました
- 本番で穴を再現して、日次の計算を実行した: 直す前のコードでは、穴が埋まらず、指標が1行のままでした。直したあとのコードでは、穴が埋まり、穴を作る前の行と全ての列が一致しました。終わったあとの全社の指紋は、再現する前と完全に一致しました
あわせて、テストを足しました(この修正の時点で、全体で279件が通りました)。中でも次の1つは、直した内容の要です。チェックが除く条件の「5」と、指標を作る側の「5」が、同じ値であることを確かめます。どちらか片方だけを変えると、除外が効かなくなるか、本物の穴を隠してしまうためです。
技術的な補足(実際に使ったコード)
ここに載せるのは、実際に使ったスクリプトからの抜粋です。実物と変えたのは、長い箇所を...で省略したことと、共通のインデントを除いたことだけです。コメントとSQLは、実物のままです。SQLのコメントにある「TI」は、指標のことです。データベースの接続情報の行は載せていません。
1. 日次の計算: 株価が少ない銘柄は、全履歴を書く
指標を作るのは、株価が5行以上のときだけです(calc_indicatorsの冒頭)。
if df.empty or len(df) < 5:
return []
全履歴を書くかどうかを決める判定です。corrected_idsは、過去の日の株価が訂正された銘柄です。
NEW_LISTING_FULL_REWRITE_ROWS = 30
def needs_full_history_write(cid: int, df: pd.DataFrame, corrected_ids: set) -> bool:
...
return cid in corrected_ids or len(df) <= NEW_LISTING_FULL_REWRITE_ROWS
日次の計算の中での使われ方です。全履歴を書く銘柄はtail=None、そうでない銘柄はtail=1(最新の1行だけ)で計算します。
if needs_full_history_write(cid, df, corrected_ids):
rows = calc_indicators(df, cid, tail=None)
if rows:
all_latest_rows.extend(rows) # 全履歴を書き込み
success += 1
else:
skipped += 1
continue
rows = calc_indicators(df, cid, tail=1)
if rows:
all_latest_rows.append(rows[-1]) # 最新1行のみ
success += 1
else:
skipped += 1
2. チェック: 新規上場の途中の状態を除く
1つ目のチェックです。「株価が5行未満で、指標が0行」の企業を除きます。sp.cが株価の行数、ti.cが指標の行数です。
FROM companies c
LEFT JOIN sp ON sp.company_id = c.id
LEFT JOIN ti ON ti.company_id = c.id
WHERE c.is_active = TRUE AND sp.company_id IS NOT NULL
-- 2026/10/09 新規上場銘柄の誤検知対策: 株価が5行未満の銘柄はcalc_indicators()が
-- TIを作らない設計(calc_technical_indicators.py `len(df) < 5`)のため、株価<5行かつTI=0行は
-- 正常な過渡状態として除外する。TIが1行でもあるのに株価<5行、は矛盾なので従来どおり検知する。
AND NOT (sp.c < 5 AND COALESCE(ti.c,0) = 0)
2つ目のチェック(直近30日の行単位の突き合わせ)です。通算の行数を数えるのは、先に確定させた少数の会社(hole_co)だけです。
-- 2026/10/09 新規上場銘柄の誤検知対策: 株価が通算5行未満の銘柄はTIが作られない設計なので
-- 「穴」とみなさない(上のti_sp_company_mismatchと同じ基準)。通算行数の確認(圧縮チャンクを含む
-- stock_prices への問い合わせ)は重いので、MATERIALIZEDで「TIに無い株価行を持つ会社」を先に確定させ、
-- その少数の会社にだけ行う(これをsp全行に対する条件にすると約4秒→約100秒になる。実測済み)。
hole_co AS MATERIALIZED (
SELECT DISTINCT company_id FROM sp
WHERE NOT EXISTS (SELECT 1 FROM ti WHERE ti.company_id = sp.company_id AND ti.d = sp.d)
),
bad AS (
SELECT company_id FROM ti WHERE NOT EXISTS (SELECT 1 FROM sp WHERE sp.company_id = ti.company_id AND sp.d = ti.d)
UNION
SELECT company_id FROM hole_co
WHERE (SELECT COUNT(*) FROM (SELECT 1 FROM stock_prices x WHERE x.company_id = hole_co.company_id LIMIT 5) t) >= 5
)
3. チェックの接続の直後に、実行直前の変換の仕組み(JITコンパイル)を切る
# 2026/10/09: JITを切る(このセッションのみ)。ti_orphan_or_hole_rows_recentは通算行数の確認で
# stock_prices(1,400超のチャンク)を参照するサブクエリを持ち、コスト見積もりがJITのしきい値を超えて
# 約5,800関数のコンパイルに約27秒かかっていた(サブクエリ自体は実行されない場合でも)。実測: JITオフで約4秒。
cur.execute("SET jit = off")
未確認事項
- 全履歴を書き込む処理が、本番の毎日の取り込みで実際に動いた記録: 修正のあとの2日間(2026-10-10と2026-10-11)の朝のチェックは、成功しています。ただし、新規上場の銘柄が対象になった日の取り込みで、「全履歴を書き込みます」という表示が出たかどうかは、開発の記録からは確認できていません
- 2026-10-08に失敗した6銘柄のうち、翌日の失敗が2銘柄になった経過: 6銘柄のうち、翌日も失敗した2銘柄以外の4銘柄が、なぜ翌日は失敗しなかったのかは、開発の記録から読み切れていません
- 実行時間の内訳: 実行直前の変換(JITコンパイル)にかかった約27秒と、チェック全体の延び(約77秒から約180秒)の差の内訳は、記録から読み切れていません。2つは別々の測定として書きました
- 30行という値: 全履歴を書く銘柄の上限の30行と、対象が約30社という数は、見積もりです。上場から約6週間の銘柄を対象にしたものです
まとめ
- 新規上場の銘柄で、完全性チェックが2日続けて失敗しました。データは壊れておらず、指標の過去分が欠けていました
- 原因は、株価が5行未満の途中の状態を異常と判定するチェックと、過去分をあとから埋めない日次の計算の、2つでした。片方だけ直すと、数日後に同じ形の通知が再発する関係でした
- チェックは新規上場の途中の状態だけを除き、日次の計算は株価30行以下の銘柄の全履歴を書くようにしました。本物の穴と矛盾は、これまでどおり検知します
- 追加したSQLで、チェック全体の実行時間が約77秒から約180秒に延びたため、数える範囲を絞り、SQLを実行する直前に変換して速くする仕組み(JITコンパイル)を切りました
- 全履歴を書き込む処理が、本番の毎日の取り込みで実際に動いた記録の確認は、まだです
気になる点・ご質問があればコメントでお知らせください。