見出し画像

チャートパターンの自動検出を作ったら0件 — 実運用で直した3ヶ所

nitekabu をリリースしてから、2 週間が経ちました。
356 アクセス、検索リクエスト数百件、リピートで毎日使ってくれる人が数人。

https://nitekabu.com

「日本株のチャートから、形が似ている銘柄を 3 秒で探す」サービスです。
ダブルボトム、カップウィズハンドル、三角もちあい — 10 パターンを実装しています。

数字としてはまだ小さいです。
ただ、2 週間動かしてみて、ひとつ確信したことがあります。

チャートパターン検出は、「教科書通りに数式化しても、実銘柄が 1 件も出てこない」

これが今日の話のすべてです。

「形が似ている」を Z-score 正規化 + ユークリッド距離で実装するところまでは、
ネット上の解説記事を 3 本読めばたどり着けます。

その「数行のコード」と「実銘柄が出てくる検索エンジン」の間には、
2 週間で書き直した 3 ヶ所がある。

順番に書きます。


「形が似ている」を数値化する 3 ステップ (アルゴリズムの骨格)

詰まったポイントの前に、ベースになる仕組みだけ先に置きます。

ざっくりこんな流れ:

[1] 銘柄の価格列を Z-score で正規化

[2] パターンの「理想形」とのユークリッド距離を計算

[3] 1 - 距離/√2 を「類似度スコア (0〜1)」として出力

Z-score で正規化するのは、価格の絶対値を消す ためです。
トヨタ自動車 (株価 約 ¥2,000) と任天堂 (約 ¥10,000) を比べたら、
任天堂のほうが振れ幅が大きく見えてしまう。

カラオケで例えるなら、キーを合わせる作業 に近いです。
原曲キーがバラバラの 2 曲を「メロディの形」だけで比べたい時、
まず両方を同じ高さに移調しないと比較になりません。
Z-score は、株価における「キー合わせ」です。

public static double[] Normalize(double[] prices)
{
    var mean = prices.Average();
    var std  = Math.Sqrt(prices.Select(p => Math.Pow(p - mean, 2)).Average());
    return prices.Select(p => (p - mean) / std).ToArray();
}

正規化したら、ユークリッド距離をとって、最後にスコアに変換します。

ユークリッド距離は、2 つの波形を透明シートに描いて重ねたときの「ズレの大きさ」 だと思ってください。
ピッタリ重なれば距離 = 0、てんでバラバラなら距離が大きい。
これを「ズレが小さいほど 1 に近づく」スコアに変換しているのが下の式です。

double rmse  = Math.Sqrt(sumSquares / N);
double score = Math.Max(0.0, 1.0 - rmse / Math.Sqrt(2));
// 1.0 = 完全一致 / 0.0 = 無相関

これを 窓幅を変えながら過去 1 年の価格列をスライディング して、
最も高いスコアになるウィンドウを「最も該当する期間」として返す。

イメージとしては、虫眼鏡を地図の上で少しずつズラしながら覗き込む 感じ。
30 日窓・60 日窓・90 日窓と、虫眼鏡の倍率を変えながら、
1 年分の価格列を端から端までなめる。
一番「形が似ている瞬間」を見つけたら、そこをその銘柄の答えにする。

窓幅はパターンごとに違います:

  • ダブルボトム: 30〜90 日 (6 週〜3 ヶ月)

  • トリプルボトム: 65〜130 日 (3〜6 ヶ月)

  • カップウィズハンドル: 35〜200 日 (7 週〜10 ヶ月)

  • 三角もちあい: 20〜100 日

このあたりは Bulkowski や O'Neil のテクニカル分析書から取った値です。

ここまでで「中核ロジック」は終わり。
コード量にして 100 行ちょい。

問題はここから始まりました。


詰まりポイント ① 三角もちあいで 0 件 — 教科書の理想形は「黒板用」だった

最初に実装した「三角もちあい」の理想形は、こうでした。

教科書通りの、上下に振れながら収束する 鋭い zigzag

高値 → 安値 → 高値 → 安値 → 中央
1.0 →  0.0  → 0.9 → 0.1 → 0.5

実データに当てると、最高スコアが 0.364
しきい値 (完成パターンで 0.55) には遠く届かず、ヒット 0 件。

最初は「日本株に三角もちあいが少ないだけかな」と思いました。
違いました。

実チャートには、教科書みたいな鋭い zigzag は ほぼ存在しない
あるのは、もっとなめらかな、振幅がだんだん小さくなる波。

数学にはこの形の名前があります。

減衰正弦波 (damped sine wave)

y = e^(-αt) × sin(ωt)

イメージは、ブランコをひと押しした後の動き
最初は大きく揺れて、だんだん振り幅が小さくなって、最後は中央で止まる。
あるいは、鐘を一度叩いた後の余韻。最初は強く鳴って、徐々に減衰する。

三角もちあいで起きていることは、まさにこれです。
最初は売り買いの綱引きが激しくて値幅が大きい。
時間とともに、買い方と売り方の温度差が小さくなって、値動きが収束していく。
教科書のジグザグ図は「説明用の単純化」で、実際の市場で起きているのは
鐘の余韻のようななめらかな減衰のほうです。

実装はこう書き換えました。

"triangle" => new List<double>
{
    0.5, 0.84, 0.95, 0.80,
    0.5, 0.24, 0.15, 0.27,
    0.5, 0.69, 0.75, 0.66, 0.5
},

中心 0.5 を軸に振幅 1.0 → 0.4 で逓減する、1.5 サイクルの波。

書き直して再計算。

最高スコアが 0.364 → 0.653 に。

しきい値 0.55 を超えて、該当銘柄がちゃんと出るようになりました。

学び: 教科書の理想形は「人間が黒板で説明する用」に最適化されていて、
「コンピュータが実データから探す用」とは別物。
理想形を「数学的に自然な形」に置き換えると、実データとの整合がぐっと上がる。

数式の側にチャートを寄せるのではなく、
チャートの自然な形に数式を寄せる。


詰まりポイント ② ダブルボトムを 3 つに分けた — パターンは 1 つの真理ではない

次に詰まったのが、ダブルボトム。

教科書的なダブルボトムは、「2 つの同水準の安値 + ネックライン上抜け」。
理想形をそのまま 1 個書きました。

これで実銘柄を検索すると、スコア上位がほぼ「教科書みたいに綺麗な銘柄」だけ
でも、実際にユーザーが知りたいのは「教科書から少しズレた、形成中の銘柄」も含まれるはず。

実チャートを 100 銘柄くらい目視で見て、わかったこと。

ダブルボトムには 3 つの典型形 がある:

  1. shallow (浅い): 谷が浅い・小型株や低ボラ銘柄に多い

  2. textbook (教科書): 谷が深い・はっきりした底

  3. breakout (上抜け後): ネックラインを抜けて右肩上がりに移行

「犬」と一口に言っても、柴犬・ゴールデンレトリバー・チワワでは顔の形がぜんぜん違うのと同じです。
「ダブルボトムですね」と人間が指差せるチャートは、
教科書 1 種類ではなく、少なくとも 3 つの典型に分かれている。

1 つの理想形だけだと、shallow と breakout がヒットしない。
だから 3 つの variations を持たせて、最大スコアを採用する形に変えました。

"double_bottom" => new[]
{
    ("shallow",   new List<double> { 0.8, 0.4, 0.0, 0.2, 0.35, 0.2,  0.0,  0.4, 0.75 }),
    ("textbook",  new List<double> { /* 20 点の精密な波形 */ }),
    ("breakout",  new List<double> { 0.75,0.35,0.0, 0.4, 0.65, 0.4,  0.05, 0.65, 1.1 }),
},

同じパターン名でも、内側に「教科書」「実銘柄」「展開後」の 3 視点を持つ。

これで、「綺麗な textbook 形」も「ズレた shallow 形」も拾えるようになりました。

学び: パターンは 1 つの真理ではなく、複数の典型の集合。
教科書の 1 個に縛られると、実銘柄の半分以上を取りこぼす。


詰まりポイント ③ 75% を消した — Z-score では「ほぼ完成」と「完成」を分けられない

これがいちばん面白い詰まりでした。

nitekabu は「完成済みパターン」だけでなく、「形成中のパターン」も検索できます。
要は「これからダブルボトムが完成しそうな銘柄」を、4 段階で見つけたい。

最初の設計はこうでした:

  • 25% 形成: 第 2 ボトム形成中・反発前

  • 50% 形成: 反発確認・ネックライン途中

  • 75% 形成: ネックライン半分超え (ほぼ完成)

  • 100% 形成: ネックライン上抜け (完成)

きれいに 4 段階。実装も簡単。
理想形を「前半 25% / 50% / 75% / 100% に切り取り → 全期間に引き伸ばす」だけ。

リリース直後、75% で検索しても銘柄が 1 件も出ない

最初はバグを疑いました。コードを 3 回読み直しました。
バグじゃなかった。

理由がわかったとき、ちょっと感動しました。

75% の理想形 (ほぼ完成) は、100% の理想形 (完成) と数学的に分離できない。

例えるなら、こう。

マラソンの「ゴール 5 メートル手前」と「ゴール」を、真上からのドローン映像で見分ける ような話です。
人間の目でも難しい。コンピュータに「形だけ見て分類して」と頼んだら、ほぼ全員「ゴールしてる」と答える。

partial 0.9〜0.95 の canonical (引き伸ばした波形) を Z-score 正規化すると、
完成 (partial 1.0) の波形と「ほぼ同じ正規化結果」になる。
キーを合わせて (Z-score)、形の差を測る (Euclidean) というやり方では、
「ほぼゴール」と「ゴール」を別物として扱う情報がそもそも入っていない

つまり、ある銘柄が「75% 形成中」と「100% 完成」のどちらに近いかを聞いたとき、
ほぼ全銘柄が 100% に寄ってしまう

実銘柄分布で「best = 75% 形成中」と判定される銘柄が、原理的に 0 件。

これは Z-score 正規化を使う限り、根本的に分離不可能。
特徴量を別の方法 (進捗 progress 計算など) で表現しないと再導入できない。

判断: 75% を削除して 3 段階 (25% / 50% / 100%) に変更

UI からも該当ボタンを消しました。

public static double FormationLevelToPartial(double formationLevel)
{
    if (formationLevel >= 1.0)  return 1.0;   // 完成
    if (formationLevel >= 0.5)  return 0.85;  // 反発確認
    if (formationLevel >= 0.25) return 0.7;   // 第 2 ボトム形成中
    return formationLevel * 2.8;
}

「ボタンが減るのはユーザー体験的に損」ではあるけど、
動かない機能を残すほうが信頼を失う

学び: アルゴリズムの「自然な解像度」は、UI で決められない。
「4 段階にしたいから 4 段階にする」のではなく、
「数学的に分離できる段階数だけ提供する」。

中期的には、Z-score とは別の特徴量 (パターン進行度を別軸で計測する仕組み) を
入れて 4 段階に戻したいと思っています。
ドローン映像だけで判定するのをやめて、「ゴールテープを切ったかどうか」を別のセンサーで測る ようなイメージ。
それまでは 3 段階。


2 週間運用の数字

ざっくりまとめると:

  • リリース: 2026-05-11

  • 計測期間: 約 2 週間 (5/11 → 5/25)

  • アクセス: 約 356 visits (新規ドメイン直アクセス + shindan からの送客が約 4 割)

  • モバイル比率: 60% (iPhone 中心)

  • 検索リクエスト: のべ数百件

  • 平均レスポンス: 約 3 秒以内 (リサンプル + 全銘柄スキャン)

  • 実装パターン: 10 種

  • しきい値: 完成 0.55 / 形成中 0.45

数字としてはまだ小さい。
でも、「数式の正しさ」と「実銘柄での挙動」がズレるポイントは、動かさないと見えない

これは個人開発でずっと同じだなと思います。


これから

優先度の高いものから:

  1. パターン追加: フラッグ / ペナント / ウェッジ (要望次第)

  2. 出来高加重型 (v2): 価格のみのマッチングに出来高を重ねる

  3. 進捗特徴量の独立化 → 形成中 4 段階の再導入

次回は 「フラッグ / ペナントを damped 形式で書き直してみた」 か、
「出来高加重型 v2 を作るための設計メモ」 のどちらかを書く予定です。
続きを読みたい方は、note フォロー or X (@kabueng55) でつかまえてもらえると嬉しいです。


おわりに

「形が似ている」を数式で書くこと自体は、3 ステップで終わります。
本当の面倒くささは、その数式と実銘柄の間にある「翻訳」のほうにある。

教科書の理想形を、damped sine wave に直す。
1 つの理想形を、3 つの variations に分ける。
「ほぼ完成」を、原理的な理由で削る。

どれも、動かしてみないと気づかなかった話です。

数学が好きな方、テクニカル分析が好きな方、
教科書通りに作ったら動かなくて頭を抱えたことがある個人開発者の方。

それぞれに、何か届くものがあれば嬉しいです。

→ nitekabu: https://nitekabu.com
→ X: https://x.com/kabueng55

#nitekabu #個人開発 #数学 #テクニカル分析 #チャート分析

いいなと思ったら応援しよう!