← Issues一覧 完了 暗号資産

Issue #008: 方向予測ロジック再設計

報告日

2026-02-24


関連issue

004 方向予測の強化done)— 8-9シグナル統合投票方式を導入
005 方向予測パラメータ最適化done)— Optuna最適化でpred_weight/閾値を調整
007 15M/H精度向上done)— シグナル重みバイアスを再最適化
008 方向予測ロジック再設計  このissue

問題

発見: 予測価格と方向予測が矛盾する

30M足 2/23 18:30 +2本先の予測で確認:

予測Open:  $66,405
予測Close: $66,303  ← base_priceより下 = 価格は下落予測
方向予測:  ▲(上昇)← 矛盾

根本原因: 加重投票方式の構造的欠陥

現在の方向決定ロジック(predict_direction): 1. pred(価格予測)を含む8-9シグナルがそれぞれ buy/sell を投票 2. 加重投票の多数決で方向を決定 3. predは1票に過ぎず、他シグナルに覆される

30M足の設定(007で最適化済み):

シグナル重み:
  pred:         2    価格予測(この場合 sell
  daily_trend:  3    最大重み
  stoch_level:  2
  macd_cross:   2
  trend_4h:     1

バイアス: +2(常に買い側に加算)

矛盾が起きるメカニズム

            buy    sell
bias        +2
pred               +2    (価格は下落予測)
daily_trend +3            (日足トレンドが上昇)
─────────────────────────
合計         5      2     → 方向 = ▲(買い)

bias(+2)だけでpredのsell(2)を相殺。daily_trendが1つ買いなら確定で覆る。

問題の本質

現行設計では「価格予測」と「シグナル方向」を1つの ▲▼ に混ぜている。 2つの異なる情報を1つに潰すため矛盾が生じる。

解決: 価格予測とシグナル方向を別の情報として分離し、 それぞれ独立に表示・評価する。


再設計方針: 価格予測とシグナル方向の分離

概念

情報 意味 表示 評価
価格予測 モデルが予測した OHLC 数値 $金額 乖離率(%)
シグナル方向 テクニカル指標が示す方向感 ↗ ↘ → 的中/外れ

表示例

                シグナル    実績
+1 (19:00)      ↗ 72%      ↗ 的中
+2 (19:30)      ↗ 39%      ↘ 外れ      ← 予測Closeは下落だがシグナルは強気

トレーダーの解釈: - 「予測価格は微小下落(-0.15%)だが、テクニカルは強気寄り」 - 「上昇トレンド中の一時的な押し目の可能性」


要件定義

基本原則

1. シグナル方向 = テクニカル指標の加重投票で決定predシグナルを除外
2. 信頼度 = シグナル一致度 + 変化率の合成スコア0-100
3. 価格予測OHLCはシグナル方向と独立
4. 的中/外れの判定はシグナル方向 vs 実績方向で行う

R1: シグナル方向の決定

ルール 内容
R1-1 シグナル方向はテクニカル指標の加重投票で決定する
R1-2 predシグナル(価格予測の方向)は投票から除外する
R1-3 優勢率 = max(buy, sell) / (buy + sell) で判定
R1-4 優勢率 ≥ 65% → 優勢側の方向(↗ or ↘)
R1-5 優勢率 < 65% → レンジ(→)
R1-6 バイアス補正(direction_bias_*)は廃止する

R2: レンジ判定

ルール 内容
R2-1 優勢率 < 65% → レンジ(→)(シグナル拮抗)
R2-2 有効シグナルがゼロ(全て none)→ レンジ(→)
R2-3 レンジ時も信頼度は算出する
R2-4 65%閾値はTF別にconfig定義(direction_range_threshold_{TF})、Optuna最適化対象

レンジ判定の例

30M足の有効シグナルpred除外:
  stoch_level(2), macd_cross(2), trend_4h(1), daily_trend(3)
  合計最大 = 8

buy=5, sell=3  優勢率 62.5% < 65%   レンジ
buy=6, sell=2  優勢率 75.0%  65%   強気
buy=1, sell=7  優勢率 87.5%  65%   弱気
buy=0, sell=0  シグナルなし           レンジ

R3: 信頼度スコア(0-100)

ルール 内容
R3-1 信頼度 = signal_score + strength_score(上限100)
R3-2 signal_score = agreement_ratio * signal_ratio
R3-3 agreement_ratio = agree / total(predを除外した加重一致率)
R3-4 strength_score = min(strength_ratio, abs(change_pct) * strength_ratio)
R3-5 signal_ratio + strength_ratio = 100(TF別にconfig定義、例: 70 + 30)
R3-6 total=0(有効シグナルなし)の場合: agreement_ratio = 0.5(中立)
R3-7 direction=0(レンジ)の場合: agree=0(どの方向とも一致しない)→ agreement_ratio=0/total

計算式(現行_calculate_continuous_confidenceを踏襲、pred除外のみ変更):

signal_ratio = config('direction_confidence_signal_ratio', TF, 70)
strength_ratio = 100 - signal_ratio

total = 0; agree = 0
for key, sig in conditions.items():
    if key == 'pred':        # ← 008で追加
        continue              # ← 008で追加
    if sig['signal'] in ('buy', 'sell'):
        weight = signal_weights[key]
        total += weight
        if (sig['signal'] == 'buy' and direction == 1) or \
           (sig['signal'] == 'sell' and direction == -1):
            agree += weight

agreement_ratio = agree / total if total > 0 else 0.5
signal_score = agreement_ratio * signal_ratio
strength_score = min(strength_ratio, abs(change_pct) * strength_ratio)
confidence = min(100, signal_score + strength_score)

R4: シグナル評価とJSON保存

ルール 内容
R4-1 既存のシグナル評価関数はそのまま維持する(レベル/クロス/TF固有)
R4-2 シグナル重み(config)もそのまま維持する
R4-3 predシグナルは conditionsに記録するが投票には参加しない(バッジ表示用に残す)
R4-4 conditionsに _summary を追加し、投票集計結果を保存する
R4-5 _summary には buy_count, sell_count, total, dominance, threshold, result を含める
R4-6 これによりconfigなしでJSON単体から優勢率・判定結果を再現できる

R5: 的中/外れの判定

ルール 内容
R5-1 的中 = シグナル方向と実績方向が一致
R5-2 実績方向は determine_direction(actual_OHLC, base_price=actual_open, atr) で判定
R5-3 シグナル=↗(1)、実績=1(陽線) → 的中
R5-4 シグナル=↗(1)、実績=-1(陰線) → 外れ
R5-5 シグナル=→(0)、実績=0(横ばい) → 的中
R5-6 シグナル=→(0)、実績=1 or -1 → 外れ

実績方向の判定ロジック(既存: determine_direction

def determine_direction(open, high, low, close, base_price=None, atr=None):
    """ローソク足の方向を判定(予測・実績共通)"""
    if base_price is None:
        base_price = open

    # 横ばい判定: レンジが小さく変化率も小さい
    if atr is not None and atr > 0:
        range_ratio = (high - low) / atr
        change_pct = abs(close - base_price) / base_price * 100
        if range_ratio < SIDEWAYS_RANGE_RATIO and change_pct < SIDEWAYS_CHANGE_PCT:
            return 0  # 横ばい

    # 陽線/陰線
    if close > base_price:  return 1   # 上昇
    elif close < base_price: return -1  # 下落
    else: return 0                      # 変わらず

実績確定時の呼び出し(confirm_predictions.py):

actual_direction = determine_direction(
    actual_open, actual_high, actual_low, actual_close,
    base_price=actual_open,  # 実績は始値基準(ローソク足の陰陽)
    atr=atr
)
is_direction_hit = (direction == actual_direction)
# direction = DB保存済みのシグナル方向

変更不要 — 既存の determine_direction + confirm_predictions がそのまま使える。

R6: アイコン

ルール 内容
R6-1 シグナル予測列のみ ▲▼ → ↗↘→ に変更する
R6-2 実績方向列は ▲▼ のまま維持する(上昇/下落 = 陽線/陰線)
R6-3 目付列のアイコンも ▲▼ のまま維持する
R6-4 ヘッダ列名: 「方向予測」→「シグナル」に変更。「方向実績」「目付実績」等は変更しない
R6-5 tooltip凡例のみ更新: 方向予測列「↗=強気 ↘=弱気 →=レンジ」
R6-6 色: ↗ = 緑系(text-success)、↘ = 赤系(text-danger)、→ = 黄色(text-warning)

R7: step +2以降の扱い

ルール 内容
R7-1 シグナル方向はstep +1と同じ指標値から算出(実績値は更新されないため)
R7-2 変化率ボーナスのchange_pctはstep固有の予測値を使う
R7-3 的中判定のbase_priceは常にlatest_close(予測起点の実績終値)

DB保存

スキーマ: 変更なし(マイグレーション不要)

BtcPredictionDetail の既存フィールドをそのまま使用:

フィールド 現在の意味 008後の意味
direction SmallInteger 価格予測の方向 (1/-1/0) シグナル方向 (1=↗/-1=↘/0=→)
direction_confidence Decimal 信頼度 (0-100) 同じ(算出ロジックのみ変更)
direction_conditions JSON シグナル詳細 同じ(predシグナルも含む)
is_direction_hit Boolean 方向的中 シグナル方向 vs 実績方向
predicted_target Decimal 目付価格 同じ

direction_conditions JSON構造

{
  "pred":        {"value": -0.15, "signal": "sell", "desc": "pred -0.15%"},
  "rsi_level":   {"value": 42.1,  "signal": "none", "desc": "RSI=42"},
  "stoch_level": {"value": 18.3,  "signal": "buy",  "desc": "Stoch=18<25"},
  "macd_hist":   {"value": 0.003, "signal": "buy",  "desc": "MACD hist>0"},
  "stoch_cross": {"value": 22.1,  "signal": "buy",  "desc": "Stoch GC"},
  "macd_cross":  {"value": null,  "signal": "none", "desc": ""},
  "daily_trend": {"value": 1.2,   "signal": "buy",  "desc": "Daily uptrend"},
  "trend_4h":    {"value": -1,    "signal": "sell", "desc": "4H downtrend"},
  "_summary": {
    "buy_count": 5,
    "sell_count": 3,
    "total": 8,
    "dominance": 0.625,
    "threshold": 0.65,
    "result": "range"
  }
}

保存フロー(変更なし)

predict_ohlc.py
  → predict_direction()  ← ロジック変更ここだけ
    → return (direction, confidence, conditions)  ← conditionsに_summary含む
  → _save_detail_record(direction, confidence, conditions)
    → BtcPredictionDetail に保存

的中判定(confirm_predictions.py — 変更なし)

# 実績方向: actual_close vs actual_open(ローソク足の陰陽)
actual_direction = determine_direction(
    actual_open, actual_high, actual_low, actual_close,
    base_price=actual_open, atr=atr
)
# 的中 = 保存済みのdirection(=シグナル方向) == actual_direction
is_direction_hit = (direction == actual_direction)

direction の中身がシグナル方向に変わるだけで、比較ロジックは同一。

help_text更新(任意)

# models.py — help_textの更新のみ(動作に影響なし)
direction = models.SmallIntegerField(
    "シグナル方向", null=True, blank=True,
    help_text="1=強気↗, -1=弱気↘, 0=レンジ→"
)
is_direction_hit = models.BooleanField(
    "シグナル的中", null=True, blank=True
)

設計変更

変更対象: predict_direction() in direction_prediction.py

Before(pred含む投票 → 方向決定):

# predシグナルも投票に参加
for key, c in conditions.items():
    sig = c.get('signal')
    weight = signal_weights.get(key, 1)
    if sig == 'buy':
        buy_count += weight
    elif sig == 'sell':
        sell_count += weight

# 多数決で方向決定
if buy_count > sell_count:
    direction = 1

After(pred除外投票 + 優勢率閾値 → シグナル方向決定):

# predシグナルは投票から除外(conditionsには残す)
for key, c in conditions.items():
    if key == 'pred':
        continue  # 投票に参加しない
    sig = c.get('signal')
    weight = signal_weights.get(key, 1)
    if sig == 'buy':
        buy_count += weight
    elif sig == 'sell':
        sell_count += weight

# 優勢率でシグナル方向を決定
total = buy_count + sell_count
range_threshold = _get_dir_config('direction_range_threshold', timeframe, 0.65)

if total == 0:
    direction = 0   # → シグナルなし
elif max(buy_count, sell_count) / total >= range_threshold:
    direction = 1 if buy_count > sell_count else -1  # ↗ or ↘
else:
    direction = 0   # → レンジ(拮抗)

削除するもの

項目 理由
direction_bias_{TF} (config) 構造的買い/売りバイアスは除去
投票のbias初期値加算 同上
predシグナルの投票参加 価格予測とシグナルを分離するため

維持するもの

項目 理由
シグナル評価関数(evaluate*) 信頼度計算とバッジ表示に引き続き必要
predシグナルのconditions登録 バッジ表示用(「pred: -0.15%」の表示)
シグナル重み(config) 投票の重み付けに使用(pred以外)
_calculate_continuous_confidence 微修正のみ(pred除外した同意率計算)
calculate_target シグナル方向=targetの方向で自然に対応するため変更不要
横ばい判定の閾値(config) sideways判定はR2に従い簡略化

テンプレート変更

ファイル 変更箇所 変更内容
prediction_ohlc.html p{N}_direction セル(方向予測列) ▲▼ → ↗↘→
prediction_ohlc.html p{N}_actual_direction セル(方向実績列) 変更しない(▲▼維持)
prediction_ohlc.html 目付列のアイコン 変更しない(▲▼維持)
prediction_ohlc.html ヘッダ列名「方向予測」 「シグナル」に変更(「方向実績」等は維持)
prediction_ohlc.html 方向予測列のtooltip 凡例を「↗=強気 ↘=弱気 →=レンジ」に更新
prediction_detail.html row.direction セル ▲▼ → ↗↘→

影響

項目 内容
的中率 変化する。predを除外するため、純粋なテクニカル指標の的中率になる
信頼度 意味が明確化:「テクニカル指標の合意度」
価格予測との矛盾 構造的に解消(別情報として扱うため)
バッジ表示 変更不要(conditions構造は同じ)
config.py 下記4行削除: direction_bias_D(-1), direction_bias_30M(2), direction_bias_H(0), direction_bias_15M(-2)。下記4行追加: direction_range_threshold_D(0.65), _30M(0.65), _H(0.65), _15M(0.65)

R8: 一致度(value_confidence)の改善

R8-1: スコア計算の感度調整

3モデル(XGBoost, LightGBM, CatBoost)の予測ばらつきから算出する一致度スコアの感度を上げる。

ルール 内容
R8-1 ratio^2ratio^4 に変更(高止まり防止の強化)
R8-2 ratio = 1 - min(model_std / atr_pct, 1.0) は変更なし
R8-3 score = ratio^4 * 100(0-100にクリップ)

変更理由: ratio^2 ではスコアが高止まりしやすく、3モデルのばらつきの差が十分に反映されない。4乗にすることで、ばらつきが大きいケースのスコアがより明確に低下する。

感度比較(ratio値 → スコア):

ratio    ratio^2  ratio^4
1.00     100      100     ← 完全一致
0.95      90       81
0.90      81       66
0.80      64       41     ← 差が顕著
0.70      49       24
0.50      25        6
0.30       9        1

対象ファイル: crypto/management/commands/predict_ohlc.py L166

R8-2: 3モデル特徴(分析結果)

274件の実績データから分析した3モデルの特性:

特性 XGBoost LightGBM CatBoost
バイアス +0.0006(中立) +0.0086(やや強気) -0.0125(弱気)
方向精度 56.2% 56.9%(最高) 53.6%
予測振幅(Stdev) 0.1139% 0.1225%(最大・大胆) 0.0858%(最小・保守的)
実績との相関 +0.028 +0.025 -0.084(負の相関)
急変動時MagRatio 0.078 0.081 0.063
不一致時正答率 52.5% 55.9% 40.7%

主要な知見: 1. LightGBMが総合的に最も優秀 — 方向精度・急変動追従・不一致時の信頼度すべてでリード 2. CatBoostに問題あり — 実績と負の相関、最低の方向精度、不一致時に間違いやすい 3. 3モデルとも急変動に追従できない — 急変動(上位25%)の6-8%しか捉えられない(平均回帰型予測の限界) 4. 3モデル一致率78.5% — 一致時の方向精度57.2%

TF別の急変動方向精度: - 15M: 47%(ランダム以下)→ 平常時は65% - 30M: 61-63%(バランス良好) - H: LGBM 77.8%(方向は優秀だが振幅をほぼ無視)

tooltip表示案: 一致度カラムのtooltipに「XGB/LGBM/CAT」の各予測値を表示。 CatBoostの負の相関や保守性をユーザーが判断材料にできるようにする。

R8-3: 急変動検知の課題(未解決)

現行モデルは急変動の「大きさ」を捉えるメカニズムを持たない。 別途アプローチの検討が必要(ボラティリティ予測の分離、急変動フラグ等)。


検証計画

ロジック検証

表示検証(runserver)


対象ファイル

ファイル 変更内容
_predict_ohlc/direction_prediction.py pred除外投票、bias廃止
_predict_ohlc/config.py direction_bias_* 削除、direction_range_threshold 追加(0.65)
templates/crypto/prediction_ohlc.html シグナル予測列のみ ▲▼→↗↘→、tooltip凡例更新
templates/crypto/prediction_detail.html シグナル予測セルのみ ▲▼→↗↘→
crypto/views.py direction_label 更新(L388, L740の2箇所。現在テンプレート未使用だが意味を合わせる)
confirm_predictions.py actual_direction の判定ロジック確認
tool_optimize_direction.py bias/pred_weight削除、range_threshold追加