[{"data":1,"prerenderedAt":611},["ShallowReactive",2],{"content-/beat-monitoring-ntm-per-pipeline":3,"all-pages-for-dir":609,"og-image-/beat-monitoring-ntm-per-pipeline":610},{"id":4,"title":5,"body":6,"category":590,"description":591,"extension":592,"meta":593,"navigation":566,"ogImage":594,"path":595,"project_name":596,"published":597,"publishedAt":598,"seo":599,"stem":600,"tags":601,"todo":594,"unpublished":597,"updatedAt":594,"__hash__":608},"pages/2026-05/2026-05-29/beat-monitoring-ntm-per-pipeline.md","ビートモニタリングをフォワードPER軸に刷新 — Koyfin→TursoでNTM EPSを取り込み散布図まで実装",{"type":7,"value":8,"toc":579},"minimark",[9,14,23,27,30,63,66,81,84,87,91,109,115,126,133,137,152,317,321,324,344,347,350,360,363,376,382,502,514,517,528,531,552,555,575],[10,11,13],"h1",{"id":12},"ビートモニタリングをフォワードper軸に刷新する","ビートモニタリングをフォワードPER軸に刷新する",[15,16,17,18,22],"p",{},"朝、ビートモニタリングのカードを見ながら手が止まった。「売上ビート率」「YoY」「翌日株価」の3指標が並んでいるのに、結局この銘柄が割安なのか割高なのかが一目で分からない。決算の瞬間風速は見えても、いま株を買うべきかの判断材料になっていない。そこで指標を全部入れ替えることにした。",[19,20,21],"strong",{},"直近株価 + NTM EPS（次の4四半期予想の合計）+ フォワードPER","。この3つに刷新して、最終的に成長率 × フォワードPER の散布図ページまで一気に通した。",[24,25,26],"h2",{"id":26},"まず計画を固める",[15,28,29],{},"いきなり手を動かさず、プランモードで要件を確定させた。",[31,32,33,37,53,56],"ul",{},[34,35,36],"li",{},"カード指標を入れ替える（売上ビート/YoY/翌日株価 → 直近株価/NTM EPS/フォワードPER）",[34,38,39,40,48,49,52],{},"NTM EPS は ",[19,41,42,43,47],{},"次の4四半期（fq0〜3）の ",[44,45,46],"code",{},"eps_adj"," 予想の合計"," として ",[44,50,51],{},"consensus_estimates"," から算出する",[34,54,55],{},"過去実績の LTM 4Q と、予想の NTM 4Q を並べて表示する設計にする",[34,57,58,59,62],{},"Turso のマイグレーション → Koyfin取込スクリプト → ",[44,60,61],{},"valuation.ts"," 生成 → ページ作成 の Phase 1〜4 で進める",[15,64,65],{},"計画書を Codex でレビューさせたら、致命的な指摘が2点返ってきた。",[67,68,69,75],"ol",{},[34,70,71,74],{},[19,72,73],{},"マイグレーション適用がハードコードになっている"," — 個別のSQLファイル名を直書きしていたので、追加するたびに壊れる構造だった",[34,76,77,80],{},[19,78,79],{},"NTM EPS を fq0〜3 の「相対インデックス」で取り込み時に計算していた"," — スナップショットを撮った時点の相対位置で固定してしまうと、四半期がまたいだ瞬間に意味がずれる。取り込み時ではなくクエリ時（ビュー側）で算出すべき",[15,82,83],{},"この2点を計画に反映してから承認し、memo に保存した。指摘の8割は瑣末なクソリプなので無視するが、この2つは設計の土台に関わるので拾った。判断するのは自分、レビューを回すのは Codex、という分担。",[15,85,86],{},"特に2点目は地味だが効く。スナップショットを撮った日が「3Q FY2026 の直前」なのか「3Q を発表した直後」なのかで、fq0 が指す四半期は変わる。取り込み時の相対インデックスで NTM EPS を固定してしまうと、決算をまたいだ次の日には「もう過ぎた四半期」を未来予想として足し続けることになる。ビュー側で「今より先の4Q」を毎回引き直す方式にして、この罠を塞いだ。",[24,88,90],{"id":89},"phase-14-を実装する","Phase 1〜4 を実装する",[15,92,93,94,97,98,101,102,104,105,108],{},"順番にマイグレーションから積んだ。Turso に ",[44,95,96],{},"0002","〜",[44,99,100],{},"0004"," を追加して、",[44,103,51],{}," に価格・NTM/LTM EPS・四半期内訳のカラムを足し、最新スナップショット1行を返す ",[44,106,107],{},"v_latest_valuation"," ビューを定義した。NTM EPS の合計はこのビューの中で計算する（取り込み時ではなく）。",[15,110,111,112,114],{},"Koyfin取込スクリプトには、価格と NTM/LTM EPS の算出、四半期内訳カラムへの書き込みを追加させた。",[44,113,61],{}," 生成スクリプトはビルド前にビューを読んで TypeScript のデータファイルに吐く構成。実行時には Turso に繋がない（SSG前提）。",[15,116,117,118,121,122,125],{},"散布図ページ ",[44,119,120],{},"scatter.vue"," は、構造転換済み9銘柄を ",[19,123,124],{},"横軸 = NTM EPS 成長率（次4Q合計 ÷ 直近4Q実績 − 1）、縦軸 = フォワードPER（直近株価 ÷ NTM EPS）"," でプロットする。LTM が赤字や NTM 4Q が未充足の銘柄は成長率が定義できないので除外し、除外理由を画面に列挙するようにした。「右下＝成長率の割に PER が安いゾーン」が一目で拾えるのが狙い。",[15,127,128,129,132],{},"なぜ過去実績（LTM）と予想（NTM）を",[19,130,131],{},"並べて","出すかというと、フォワードPER は予想 EPS が分母なので、予想が現実離れしていると簡単に「割安に見える」からだ。MU のように直近4Qで EPS が 1.91 → 12.20 へ跳ねている銘柄は、NTM の予想がさらに上を見ていてもおかしくないが、その妥当性は過去の実績の伸びと並べないと判断できない。だから1行に LTM 4Q と NTM 4Q を両方置いた。",[24,134,136],{"id":135},"kid解決とデータ取り込み","KID解決とデータ取り込み",[15,138,139,140,143,144,147,148,151],{},"Koyfin の銘柄は内部 ID（KID）で引く必要がある。これを Chrome DevTools MCP 経由で解決させた。",[44,141,142],{},"POST /api/v1/bfc/tickers/search"," に ",[44,145,146],{},"categories:['Equity']"," を付けて投げる。16銘柄分を1回の ",[44,149,150],{},"evaluate_script"," でまとめて解決させ、続けて19銘柄分の estimates + 価格を Koyfin から取得して Turso に格納した。",[153,154,159],"pre",{"className":155,"code":156,"language":157,"meta":158,"style":158},"language-js shiki shiki-themes vitesse-light vitesse-light","// 1回の evaluate_script で16銘柄を一括解決（KID → ticker のマップを返す）\nconst search = async (q) =>\n  (await fetch('/api/v1/bfc/tickers/search', {\n    method: 'POST',\n    body: JSON.stringify({ query: q, categories: ['Equity'] }),\n  })).json()\n","js","",[44,160,161,170,201,233,254,305],{"__ignoreMap":158},[162,163,166],"span",{"class":164,"line":165},"line",1,[162,167,169],{"class":168},"sxvE3","// 1回の evaluate_script で16銘柄を一括解決（KID → ticker のマップを返す）\n",[162,171,173,177,181,185,188,191,195,198],{"class":164,"line":172},2,[162,174,176],{"class":175},"stQ0i","const",[162,178,180],{"class":179},"senZ8"," search",[162,182,184],{"class":183},"shFtX"," =",[162,186,187],{"class":175}," async",[162,189,190],{"class":183}," (",[162,192,194],{"class":193},"s4oTP","q",[162,196,197],{"class":183},")",[162,199,200],{"class":183}," =>\n",[162,202,204,207,211,214,217,221,225,227,230],{"class":164,"line":203},3,[162,205,206],{"class":183},"  (",[162,208,210],{"class":209},"sHkkW","await",[162,212,213],{"class":179}," fetch",[162,215,216],{"class":183},"(",[162,218,220],{"class":219},"sMJiu","'",[162,222,224],{"class":223},"sdGka","/api/v1/bfc/tickers/search",[162,226,220],{"class":219},[162,228,229],{"class":183},",",[162,231,232],{"class":183}," {\n",[162,234,236,240,243,246,249,251],{"class":164,"line":235},4,[162,237,239],{"class":238},"sz8Xr","    method",[162,241,242],{"class":183},":",[162,244,245],{"class":219}," '",[162,247,248],{"class":223},"POST",[162,250,220],{"class":219},[162,252,253],{"class":183},",\n",[162,255,257,260,262,265,268,271,274,277,279,282,284,287,289,292,294,297,299,302],{"class":164,"line":256},5,[162,258,259],{"class":238},"    body",[162,261,242],{"class":183},[162,263,264],{"class":193}," JSON",[162,266,267],{"class":183},".",[162,269,270],{"class":179},"stringify",[162,272,273],{"class":183},"({",[162,275,276],{"class":238}," query",[162,278,242],{"class":183},[162,280,281],{"class":193}," q",[162,283,229],{"class":183},[162,285,286],{"class":238}," categories",[162,288,242],{"class":183},[162,290,291],{"class":183}," [",[162,293,220],{"class":219},[162,295,296],{"class":223},"Equity",[162,298,220],{"class":219},[162,300,301],{"class":183},"]",[162,303,304],{"class":183}," }),\n",[162,306,308,311,314],{"class":164,"line":307},6,[162,309,310],{"class":183},"  })).",[162,312,313],{"class":179},"json",[162,315,316],{"class":183},"()\n",[24,318,320],{"id":319},"muで検算する","MUで検算する",[15,322,323],{},"数字が正しいか、MU で答え合わせをした。Koyfin の Earnings Matrix と突き合わせる。",[31,325,326,335],{},[34,327,328,331,332],{},[19,329,330],{},"NTM EPS"," = 19.29 + 23.04 + 25.70 + 26.35 = ",[19,333,334],{},"94.38",[34,336,337,340,341],{},[19,338,339],{},"LTM EPS"," = 1.91 + 3.03 + 4.78 + 12.20 = ",[19,342,343],{},"21.92",[15,345,346],{},"Koyfin の表示と完全に一致した。ビュー側で合計を取る設計が正しく効いている。あとから自分でも検算できるように、4四半期の内訳を散布図のデータテーブルに併記した。「LTM 1.91＋3.03＋4.78＋12.20＝21.92」のように足し算がそのまま見える形にしておくと、画面を開いた瞬間に数字を疑える。",[24,348,349],{"id":349},"純粋関数を切り出してテストする",[15,351,352,355,356,359],{},[44,353,354],{},"compute_ntm_eps"," / ",[44,357,358],{},"compute_ltm_eps"," / 各フォーマッタを utils に切り出した。EPS合計のロジックと表示整形を、副作用（DB・fetch）から完全に分離する。これで Python 側は unittest、TypeScript 側は Vitest でテストでき、合計20件超が pass した。境界ケース（4Q未充足、LTM赤字でゼロ除算回避、null混入）も入れた。",[24,361,362],{"id":362},"ルーティング衝突を踏む",[15,364,365,368,369,143,372,375],{},[44,366,367],{},"/beat-monitoring/scatter"," を開いたら、",[44,370,371],{},"[ticker].vue",[44,373,374],{},"ticker='SCATTER'"," として吸われていた。静的ルートよりキャッチオールが勝ってしまう。原因を追うと、こちら（自分の）dev server の HMR が取りこぼしていただけで、フルリロードすると静的ルートが正しく優先された。設定の問題ではなかった。",[15,377,378,379,381],{},"ただ、本番で同じ衝突が起きると怖いので、防御線として ",[44,380,371],{}," 側に予約名のガードを入れた。",[153,383,387],{"className":384,"code":385,"language":386,"meta":158,"style":158},"language-ts shiki shiki-themes vitesse-light vitesse-light","const RESERVED_NAMES = new Set(['scatter'])\nif (RESERVED_NAMES.has(rawParam.toLowerCase())) {\n  await navigateTo(`/beat-monitoring/${rawParam.toLowerCase()}`, { replace: true })\n}\n","ts",[44,388,389,418,447,497],{"__ignoreMap":158},[162,390,391,394,397,399,402,405,408,410,413,415],{"class":164,"line":165},[162,392,393],{"class":175},"const ",[162,395,396],{"class":193},"RESERVED_NAMES",[162,398,184],{"class":183},[162,400,401],{"class":175}," new ",[162,403,404],{"class":179},"Set",[162,406,407],{"class":183},"([",[162,409,220],{"class":219},[162,411,412],{"class":223},"scatter",[162,414,220],{"class":219},[162,416,417],{"class":183},"])\n",[162,419,420,423,425,427,429,432,434,437,439,442,445],{"class":164,"line":172},[162,421,422],{"class":209},"if",[162,424,190],{"class":183},[162,426,396],{"class":193},[162,428,267],{"class":183},[162,430,431],{"class":179},"has",[162,433,216],{"class":183},[162,435,436],{"class":193},"rawParam",[162,438,267],{"class":183},[162,440,441],{"class":179},"toLowerCase",[162,443,444],{"class":183},"()))",[162,446,232],{"class":183},[162,448,449,452,455,457,460,463,466,468,470,472,475,478,480,482,485,488,491,494],{"class":164,"line":203},[162,450,451],{"class":209},"  await",[162,453,454],{"class":179}," navigateTo",[162,456,216],{"class":183},[162,458,459],{"class":219},"`",[162,461,462],{"class":223},"/beat-monitoring/",[162,464,465],{"class":209},"${",[162,467,436],{"class":223},[162,469,267],{"class":183},[162,471,441],{"class":179},[162,473,474],{"class":183},"()",[162,476,477],{"class":209},"}",[162,479,459],{"class":219},[162,481,229],{"class":183},[162,483,484],{"class":183}," { ",[162,486,487],{"class":238},"replace",[162,489,490],{"class":183},": ",[162,492,493],{"class":209},"true",[162,495,496],{"class":183}," })\n",[162,498,499],{"class":164,"line":235},[162,500,501],{"class":183},"}\n",[15,503,504,505,507,508,510,511,513],{},"予約名が来たら静的ルートへ ",[44,506,487],{}," でリダイレクトする。HMRの取りこぼしが真因と分かっていても、防御線は1本引いておく。",[44,509,487],{}," にしたのは、ブラウザの戻るボタンで ",[44,512,367],{}," に戻ったときに無限ループの履歴を作らないため。最初は普通の遷移にしていて、戻る操作で違和感が出たので置き換えた。",[24,515,516],{"id":516},"文字コードで足をすくわれる",[15,518,519,520,523,524,527],{},"取り込みログの途中で、",[44,521,522],{},"print"," が cp932 のエンコードエラーを吐いて一部銘柄のログが化けた。一瞬「取り込みも失敗したか」と焦ったが、Turso を直接確認すると書き込み自体は commit 済みだった。",[19,525,526],{},"ログの表示が壊れただけで、データは無事","。print 側のエンコーディングを直して再発を止めた。ログの化けと処理の失敗を混同しないこと、確認はログではなく実体（DB）を見ること。",[24,529,530],{"id":530},"今日の学び",[31,532,533,540,543,546,549],{},[34,534,535,536,539],{},"NTM EPS の合計は",[19,537,538],{},"取り込み時に固定するとずれる","。四半期がまたいだ瞬間に相対インデックスの意味が変わるので、クエリ時（ビュー側）で算出する。Codex の指摘で踏みとどまった",[34,541,542],{},"マイグレーション適用をハードコードすると、ファイルを足すたびに壊れる。最初から汎用的に積む構造にしておく",[34,544,545],{},"数字の検算は「画面に内訳を併記する」のが一番速い。MU の足し算がそのまま見えるので、開いた瞬間に疑える",[34,547,548],{},"cp932 のログ化けは処理失敗ではない。ログを見て焦る前に、Turso の実体を確認する",[34,550,551],{},"純粋関数（合計・フォーマッタ）を副作用から切り離すと、Python と TypeScript の両方でテストが書ける。20件超を pass させて安心して刷新できた",[24,553,554],{"id":554},"明日やること",[31,556,559,569],{"className":557},[558],"contains-task-list",[34,560,563,568],{"className":561},[562],"task-list-item",[564,565],"input",{"disabled":566,"type":567},true,"checkbox"," 除外銘柄（NTM 4Q未充足・LTM赤字）の翌日再取得を仕組み化する",[34,570,572,574],{"className":571},[562],[564,573],{"disabled":566,"type":567}," フォワードPERのスナップショットを日次で蓄積し、PERの推移を時系列で見られるようにする",[576,577,578],"style",{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":158,"searchDepth":172,"depth":172,"links":580},[581,582,583,584,585,586,587,588,589],{"id":26,"depth":172,"text":26},{"id":89,"depth":172,"text":90},{"id":135,"depth":172,"text":136},{"id":319,"depth":172,"text":320},{"id":349,"depth":172,"text":349},{"id":362,"depth":172,"text":362},{"id":516,"depth":172,"text":516},{"id":530,"depth":172,"text":530},{"id":554,"depth":172,"text":554},"dev","決算ビートの監視カードを『売上ビート/YoY/翌日株価』から『直近株価+NTM EPS+フォワードPER』へ刷新。Koyfinのコンセンサスから次4QのNTM EPSを算出してTursoに格納し、成長率×フォワードPERの散布図ページまで作った1日の記録。","md",{},null,"/beat-monitoring-ntm-per-pipeline","financial-data",false,"2026-05-29T00:00:00.000Z",{"title":5,"description":591},"2026-05/2026-05-29/beat-monitoring-ntm-per-pipeline",[602,603,604,605,606,607],"NTM PER","フォワードPER","Koyfin","Turso","決算モニタリング","散布図","QzzUbhg72JGEuqEKTqivRYQgTorZOhq10rVzY9dMEE4",[],"https://log.eurekapu.com/og/blog/beat-monitoring-ntm-per-pipeline.png?v=2026-05-29T00%3A00%3A00.000Z&title=%E3%83%93%E3%83%BC%E3%83%88%E3%83%A2%E3%83%8B%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%92%E3%83%95%E3%82%A9%E3%83%AF%E3%83%BC%E3%83%89PER%E8%BB%B8%E3%81%AB%E5%88%B7%E6%96%B0%20%E2%80%94%20Koyfin%E2%86%92Turso%E3%81%A7NTM%20EPS%E3%82%92%E5%8F%96%E3%82%8A%E8%BE%BC%E3%81%BF%E6%95%A3%E5%B8%83%E5%9B%B3%E3%81%BE%E3%81%A7%E5%AE%9F%E8%A3%85&author=Kei%20Komatsu&sig=920a807adced46ab",1782528844345]