リーク対策およびリスク更新を行った株価予測手法の概要

<https://canape2020.stars.ne.jp/python/python_start/honda.html> 以前の版(honda_macd_rsi_lstm_forecast_ema.py)は、未来情報を固定してしまう等の要因で リーク(未来を知った状態の予測)になり得るため、精度が「正しいとは言えない」可能性がありました。

以下のボタンは、「真の未来予測の完全改善版」に関するコードの詳細を表示します。 板情報やオルタナティブデータ、アルゴリズム執行を統合することで、予測から実際の執行に近い運用が可能となりますが、 本システムは学習用途を目的としています。

ターミナルでの実行手順(仮想環境 .venv)

cd ~/Desktop/q

source .venv/bin/activate

# 7営業日予測

python honda_macd_rsi_lstm_forecast_ema_last7.py

# 30営業日予測

python honda_macd_rsi_lstm_forecast_ema_last.py

  
 

アップロードすべきファイルはこの2枚 あなたのローカルではここにあります:
/Users/user1/Desktop/7267_latest_7d.png
/Users/user1/Desktop/7267_latest_30d.png
これを 必ず同じフォルダへアップしてください: /python/forecast_ema_last/

同じファイル名でアップロードすれば「上書き」されます。

予測チャート(最新)

7営業日予測

Honda 7日予測

30営業日予測

Honda 30日予測
30営業日版
          import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf

from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error, mean_absolute_error

import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, LSTM, Dense, Dropout
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau

# =========================
# 0) 再現性
# =========================
SEED = 42
np.random.seed(SEED)
tf.random.set_seed(SEED)

# =========================
# 日本語フォント(Mac対応)
# =========================
def set_japanese_font():
    mac_fonts = ['Hiragino Sans', 'AppleGothic', 'ヒラギノ角ゴシック']
    for f in mac_fonts:
        try:
            plt.rcParams['font.family'] = f
            return
        except Exception:
            continue
    print("⚠️ 日本語フォントが見つかりません(英数字のみ表示)。")

set_japanese_font()

# =========================
# 1) データ取得(end=Noneで最新まで)
# =========================
ticker = "7267.T"
start_date = "2020-01-01"
end_date = None  # ←最新まで

raw = yf.download(ticker, start=start_date, end=end_date, auto_adjust=False, progress=False)
if raw.empty:
    raise RuntimeError("株価データの取得に失敗しました(空)。")

def normalize_ohlcv(df: pd.DataFrame, ticker: str) -> pd.DataFrame:
    """
    yfinance が MultiIndex 列で返す場合でも、Open/High/Low/Close/Volume を単層に正規化する
    """
    df = df.copy()

    # MultiIndex -> 単層へ
    if isinstance(df.columns, pd.MultiIndex):
        # 例: ('Close','7267.T') のような形を想定
        if ticker in df.columns.get_level_values(-1):
            df = df.xs(ticker, axis=1, level=-1)
        else:
            # 念のため最初のティッカーに落とす
            first = df.columns.get_level_values(-1)[0]
            df = df.xs(first, axis=1, level=-1)

    # 必須列チェック+Series化
    need = ["Open", "High", "Low", "Close", "Volume"]
    for col in need:
        if col not in df.columns:
            raise RuntimeError(f"必要な列 {col} が見つかりません。現在の列: {list(df.columns)}")
        if isinstance(df[col], pd.DataFrame):
            df[col] = df[col].iloc[:, 0]
        df[col] = pd.to_numeric(df[col], errors="coerce")

    df = df.dropna(subset=["Close"]).copy()
    return df

df = normalize_ohlcv(raw, ticker)

# =========================
# 2) テクニカル計算(学習用:履歴分)
#    EMA12/EMA26 -> MACD -> RSI(14)
# =========================
df["EMA12"] = df["Close"].ewm(span=12, adjust=False).mean()
df["EMA26"] = df["Close"].ewm(span=26, adjust=False).mean()
df["MACD"] = df["EMA12"] - df["EMA26"]
df["Signal"] = df["MACD"].ewm(span=9, adjust=False).mean()

delta = df["Close"].diff()
gain = delta.where(delta > 0, 0.0).rolling(14).mean()
loss = (-delta.where(delta < 0, 0.0)).rolling(14).mean()
rs = gain / (loss + 1e-12)
df["RSI"] = 100 - (100 / (1 + rs))

df["log_close"] = np.log(df["Close"])
df["log_ret"] = df["log_close"].diff()
df["y_next_logret"] = df["log_ret"].shift(-1)

df = df.dropna().copy()

# =========================
# 3) 特徴量(リークしないもののみ)
# =========================
df["ema12_ratio"] = df["EMA12"] / df["Close"] - 1.0
df["ema26_ratio"] = df["EMA26"] / df["Close"] - 1.0
df["rsi01"] = (df["RSI"] / 100.0).clip(0, 1)

feature_cols = ["log_ret", "ema12_ratio", "ema26_ratio", "MACD", "Signal", "rsi01"]
X_all = df[feature_cols].values.astype(np.float32)
y_all = df["y_next_logret"].values.astype(np.float32).reshape(-1, 1)
dates = df.index
close_series = df["Close"].copy()

# =========================
# 4) 時系列分割(リーク無し)
# =========================
test_ratio = 0.2
n_total = len(df)
split = int(n_total * (1 - test_ratio))

X_train_raw = X_all[:split]
y_train_raw = y_all[:split]
X_test_raw  = X_all[split:]
y_test_raw  = y_all[split:]

# =========================
# 5) スケーリング(必ずtrainでfit)
# =========================
x_scaler = MinMaxScaler()
X_train_scaled = x_scaler.fit_transform(X_train_raw)
X_test_scaled  = x_scaler.transform(X_test_raw)

y_scaler = MinMaxScaler()
y_train_scaled = y_scaler.fit_transform(y_train_raw).flatten()
y_test_scaled  = y_scaler.transform(y_test_raw).flatten()

# =========================
# 6) シーケンス化
# =========================
def make_seq_1step(Xs, ys, time_step):
    X_seq, y_seq = [], []
    for i in range(len(Xs) - time_step):
        X_seq.append(Xs[i:i + time_step])
        y_seq.append(ys[i + time_step])
    return np.array(X_seq), np.array(y_seq)

time_step = 60
X_train, y_train = make_seq_1step(X_train_scaled, y_train_scaled, time_step)

X_test_for_seq = np.vstack([X_train_scaled[-time_step:], X_test_scaled])
y_test_for_seq = np.concatenate([y_train_scaled[-time_step:], y_test_scaled])

X_test, y_test = make_seq_1step(X_test_for_seq, y_test_for_seq, time_step)
test_dates = dates[split:][0:len(y_test)]

# =========================
# 7) モデル
# =========================
def build_model(time_step, n_features):
    m = Sequential([
        Input(shape=(time_step, n_features)),
        LSTM(64, return_sequences=True),
        Dropout(0.2),
        LSTM(64),
        Dropout(0.2),
        Dense(32, activation="relu"),
        Dense(1)
    ])
    m.compile(optimizer=tf.keras.optimizers.Adam(1e-3), loss=tf.keras.losses.Huber())
    return m

model = build_model(time_step, X_train.shape[2])

es = EarlyStopping(monitor="val_loss", patience=12, restore_best_weights=True)
rlr = ReduceLROnPlateau(monitor="val_loss", factor=0.5, patience=5, min_lr=1e-5)

model.fit(
    X_train, y_train,
    validation_split=0.1,
    epochs=300,
    batch_size=32,
    callbacks=[es, rlr],
    verbose=0,
    shuffle=False
)

# =========================
# 8) テスト評価(真の精度)
# =========================
pred_test_scaled = model.predict(X_test, verbose=0).flatten()

pred_test_logret = y_scaler.inverse_transform(pred_test_scaled.reshape(-1, 1)).flatten()
true_test_logret = y_scaler.inverse_transform(y_test.reshape(-1, 1)).flatten()

rmse_ret = float(np.sqrt(mean_squared_error(true_test_logret, pred_test_logret)))
mae_ret  = float(mean_absolute_error(true_test_logret, pred_test_logret))
resid_std = float(np.std(true_test_logret - pred_test_logret))

prev_prices = close_series.shift(1).loc[test_dates].to_numpy()
true_prices = close_series.loc[test_dates].to_numpy()
pred_prices = prev_prices * np.exp(pred_test_logret)

rmse_price = float(np.sqrt(mean_squared_error(true_prices, pred_prices)))
mae_price  = float(mean_absolute_error(true_prices, pred_prices))

# =========================
# 9) 未来30営業日予測(リーク無し)
# =========================
alpha12 = 2 / (12 + 1)
alpha26 = 2 / (26 + 1)
alpha9  = 2 / (9 + 1)

horizon = 30
last_close = float(df["Close"].iloc[-1])
last_ema12 = float(df["EMA12"].iloc[-1])
last_ema26 = float(df["EMA26"].iloc[-1])
last_signal = float(df["Signal"].iloc[-1])

last14 = df["Close"].iloc[-15:]
d14 = last14.diff().dropna()
avg_gain = float(d14.where(d14 > 0, 0.0).mean())
avg_loss = float((-d14.where(d14 < 0, 0.0)).mean())

def compute_rsi_from_avgs(avg_gain, avg_loss):
    rs = avg_gain / (avg_loss + 1e-12)
    rsi = 100 - (100 / (1 + rs))
    return float(np.clip(rsi, 0, 100))

X_all_scaled = x_scaler.transform(X_all)
seq = X_all_scaled[-time_step:].copy()

future_prices = []
upper_band = []
lower_band = []

p = last_close

y_train_logret = y_train_raw.flatten()
lo, hi = np.quantile(y_train_logret, [0.01, 0.99])

for t in range(1, horizon + 1):
    x_in = seq.reshape(1, time_step, len(feature_cols))

    pred_scaled = float(model.predict(x_in, verbose=0)[0, 0])

    # ★MinMax外挿チェック(0〜1外に出ると暴走しやすい)
    raw_pred_scaled = pred_scaled
    # ★応急処置:0〜1にクリップ
    pred_scaled = float(np.clip(pred_scaled, 0.0, 1.0))

    pred_logret = float(y_scaler.inverse_transform(np.array([[pred_scaled]], dtype=np.float32))[0, 0])

    # ★追加①:学習分布に収める(分位クリップ)
    pred_logret = float(np.clip(pred_logret, lo, hi))

    # ★追加②:平均回帰(ドリフト抑制)
    pred_logret *= 0.7
    if t <= 5:
        print(f"[t={t}] raw_scaled={raw_pred_scaled:.4f} clipped={pred_scaled:.4f} logret={pred_logret:+.5f} p={p:.2f}")

       #★価格更新は if の外に出す(全日で必要)
    p = float(p * np.exp(pred_logret))
        # EMA/MACD/Signal 更新
    last_ema12 = float(alpha12 * p + (1 - alpha12) * last_ema12)
    last_ema26 = float(alpha26 * p + (1 - alpha26) * last_ema26)
    macd = float(last_ema12 - last_ema26)
    last_signal = float(alpha9 * macd + (1 - alpha9) * last_signal)

    # RSI更新
    prev_p = float(p / np.exp(pred_logret))
    change = p - prev_p
    gain_t = max(change, 0.0)
    loss_t = max(-change, 0.0)
    n = 14
    avg_gain = (avg_gain * (n - 1) + gain_t) / n
    avg_loss = (avg_loss * (n - 1) + loss_t) / n
    rsi01 = float(compute_rsi_from_avgs(avg_gain, avg_loss) / 100.0)

    # 特徴量を再構成
    ema12_ratio = float(last_ema12 / p - 1.0)
    ema26_ratio = float(last_ema26 / p - 1.0)

    feat = np.array([pred_logret, ema12_ratio, ema26_ratio, macd, last_signal, rsi01], dtype=np.float32)
    feat_scaled = x_scaler.transform(feat.reshape(1, -1))[0]

    seq = np.vstack([seq[1:], feat_scaled])

    future_prices.append(p)

    sigma_t = resid_std * np.sqrt(t)
    upper_band.append(p * np.exp(sigma_t))
    lower_band.append(p * np.exp(-sigma_t))

future_prices = np.array(future_prices, dtype=float)
upper_band = np.array(upper_band, dtype=float)
lower_band = np.array(lower_band, dtype=float)

future_dates = pd.bdate_range(df.index[-1] + pd.tseries.offsets.BDay(1), periods=horizon)

# =========================
# 10) 可視化&保存
# =========================
plt.figure(figsize=(14, 8))
plt.plot(df.index, df["Close"].values, label="実株価", linewidth=1)
plt.plot(test_dates, pred_prices, label="テスト期間の予測(1日先)", linewidth=2)
plt.plot(future_dates, future_prices, marker="o", markersize=3.5, linewidth=2, label=f"未来{horizon}営業日予測")
plt.fill_between(future_dates, lower_band, upper_band, alpha=0.25, label="予測バンド(logret誤差±1σ)")

plt.title(f"{ticker} 真の未来予測(リーク無し): テスト検証 + 未来{horizon}営業日")
plt.xlabel("日付")
plt.ylabel("株価(円)")
plt.grid(True)
plt.legend(loc="best")

out = os.path.expanduser("~/Desktop/7267_latest_30d.png")
plt.savefig(out, dpi=150)
plt.close()

# =========================
# 11) 結果表示
# =========================
print("========== 真のテスト精度(過去で検証) ==========")
print(f"期間: {test_dates[0].date()} 〜 {test_dates[-1].date()}(テスト{len(test_dates)}日)")
print(f"リターンRMSE: {rmse_ret:.6f}")
print(f"リターンMAE : {mae_ret:.6f}")
print(f"リターン誤差σ: {resid_std:.6f}(バンド用)")
print(f"価格RMSE(参考): {rmse_price:.2f} 円")
print(f"価格MAE(参考) : {mae_price:.2f} 円")

print("\n========== 未来予測 ==========")
print(f"データ最終日(起点日): {df.index[-1].date()}")
print(f"起点の終値: {last_close:.2f} 円")
print(f"予測期間: {future_dates[0].date()} 〜 {future_dates[-1].date()}({horizon}営業日)")
print(f"最終日の予測: {future_prices[-1]:.2f} 円")
print(f"最終日のバンド: {lower_band[-1]:.2f} 円 〜 {upper_band[-1]:.2f} 円")
print(f"✅ 保存しました: {out}")
      
短期7営業日版
          import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf

from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error, mean_absolute_error

import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, LSTM, Dense, Dropout
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau

# =========================
# 0) 再現性
# =========================
SEED = 42
np.random.seed(SEED)
tf.random.set_seed(SEED)

# =========================
# 日本語フォント(Mac対応)
# =========================
def set_japanese_font():
    mac_fonts = ['Hiragino Sans', 'AppleGothic', 'ヒラギノ角ゴシック']
    for f in mac_fonts:
        try:
            plt.rcParams['font.family'] = f
            return
        except Exception:
            continue
    print("⚠️ 日本語フォントが見つかりません(英数字のみ表示)。")

set_japanese_font()

# =========================
# 1) データ取得(end=Noneで最新まで)
# =========================
ticker = "7267.T"
start_date = "2020-01-01"
end_date = None  # ←最新まで

raw = yf.download(ticker, start=start_date, end=end_date, auto_adjust=False, progress=False)
if raw.empty:
    raise RuntimeError("株価データの取得に失敗しました(空)。")

def normalize_ohlcv(df: pd.DataFrame, ticker: str) -> pd.DataFrame:
    """
    yfinance が MultiIndex 列で返す場合でも、Open/High/Low/Close/Volume を単層に正規化する
    """
    df = df.copy()

    # MultiIndex -> 単層へ
    if isinstance(df.columns, pd.MultiIndex):
        # 例: ('Close','7267.T') のような形を想定
        if ticker in df.columns.get_level_values(-1):
            df = df.xs(ticker, axis=1, level=-1)
        else:
            # 念のため最初のティッカーに落とす
            first = df.columns.get_level_values(-1)[0]
            df = df.xs(first, axis=1, level=-1)

    # 必須列チェック+Series化
    need = ["Open", "High", "Low", "Close", "Volume"]
    for col in need:
        if col not in df.columns:
            raise RuntimeError(f"必要な列 {col} が見つかりません。現在の列: {list(df.columns)}")
        if isinstance(df[col], pd.DataFrame):
            df[col] = df[col].iloc[:, 0]
        df[col] = pd.to_numeric(df[col], errors="coerce")

    df = df.dropna(subset=["Close"]).copy()
    return df

df = normalize_ohlcv(raw, ticker)

# =========================
# 2) テクニカル計算(学習用:履歴分)
#    EMA12/EMA26 -> MACD -> RSI(14)
# =========================
df["EMA12"] = df["Close"].ewm(span=12, adjust=False).mean()
df["EMA26"] = df["Close"].ewm(span=26, adjust=False).mean()
df["MACD"] = df["EMA12"] - df["EMA26"]
df["Signal"] = df["MACD"].ewm(span=9, adjust=False).mean()

delta = df["Close"].diff()
gain = delta.where(delta > 0, 0.0).rolling(14).mean()
loss = (-delta.where(delta < 0, 0.0)).rolling(14).mean()
rs = gain / (loss + 1e-12)
df["RSI"] = 100 - (100 / (1 + rs))

df["log_close"] = np.log(df["Close"])
df["log_ret"] = df["log_close"].diff()
df["y_next_logret"] = df["log_ret"].shift(-1)

df = df.dropna().copy()

# =========================
# 3) 特徴量(リークしないもののみ)
# =========================
df["ema12_ratio"] = df["EMA12"] / df["Close"] - 1.0
df["ema26_ratio"] = df["EMA26"] / df["Close"] - 1.0
df["rsi01"] = (df["RSI"] / 100.0).clip(0, 1)

feature_cols = ["log_ret", "ema12_ratio", "ema26_ratio", "MACD", "Signal", "rsi01"]
X_all = df[feature_cols].values.astype(np.float32)
y_all = df["y_next_logret"].values.astype(np.float32).reshape(-1, 1)
dates = df.index
close_series = df["Close"].copy()

# =========================
# 4) 時系列分割(リーク無し)
# =========================
test_ratio = 0.2
n_total = len(df)
split = int(n_total * (1 - test_ratio))

X_train_raw = X_all[:split]
y_train_raw = y_all[:split]
X_test_raw  = X_all[split:]
y_test_raw  = y_all[split:]

# =========================
# 5) スケーリング(必ずtrainでfit)
# =========================
x_scaler = MinMaxScaler()
X_train_scaled = x_scaler.fit_transform(X_train_raw)
X_test_scaled  = x_scaler.transform(X_test_raw)

y_scaler = MinMaxScaler()
y_train_scaled = y_scaler.fit_transform(y_train_raw).flatten()
y_test_scaled  = y_scaler.transform(y_test_raw).flatten()

# =========================
# 6) シーケンス化
# =========================
def make_seq_1step(Xs, ys, time_step):
    X_seq, y_seq = [], []
    for i in range(len(Xs) - time_step):
        X_seq.append(Xs[i:i + time_step])
        y_seq.append(ys[i + time_step])
    return np.array(X_seq), np.array(y_seq)

time_step = 60
X_train, y_train = make_seq_1step(X_train_scaled, y_train_scaled, time_step)

X_test_for_seq = np.vstack([X_train_scaled[-time_step:], X_test_scaled])
y_test_for_seq = np.concatenate([y_train_scaled[-time_step:], y_test_scaled])

X_test, y_test = make_seq_1step(X_test_for_seq, y_test_for_seq, time_step)
test_dates = dates[split:][0:len(y_test)]

# =========================
# 7) モデル
# =========================
def build_model(time_step, n_features):
    m = Sequential([
        Input(shape=(time_step, n_features)),
        LSTM(64, return_sequences=True),
        Dropout(0.2),
        LSTM(64),
        Dropout(0.2),
        Dense(32, activation="relu"),
        Dense(1)
    ])
    m.compile(optimizer=tf.keras.optimizers.Adam(1e-3), loss=tf.keras.losses.Huber())
    return m

model = build_model(time_step, X_train.shape[2])

es = EarlyStopping(monitor="val_loss", patience=12, restore_best_weights=True)
rlr = ReduceLROnPlateau(monitor="val_loss", factor=0.5, patience=5, min_lr=1e-5)

model.fit(
    X_train, y_train,
    validation_split=0.1,
    epochs=300,
    batch_size=32,
    callbacks=[es, rlr],
    verbose=0,
    shuffle=False
)

# =========================
# 8) テスト評価(真の精度)
# =========================
pred_test_scaled = model.predict(X_test, verbose=0).flatten()

pred_test_logret = y_scaler.inverse_transform(pred_test_scaled.reshape(-1, 1)).flatten()
true_test_logret = y_scaler.inverse_transform(y_test.reshape(-1, 1)).flatten()

rmse_ret = float(np.sqrt(mean_squared_error(true_test_logret, pred_test_logret)))
mae_ret  = float(mean_absolute_error(true_test_logret, pred_test_logret))
resid_std = float(np.std(true_test_logret - pred_test_logret))

prev_prices = close_series.shift(1).loc[test_dates].to_numpy()
true_prices = close_series.loc[test_dates].to_numpy()
pred_prices = prev_prices * np.exp(pred_test_logret)

rmse_price = float(np.sqrt(mean_squared_error(true_prices, pred_prices)))
mae_price  = float(mean_absolute_error(true_prices, pred_prices))

# =========================
# 9) 未来7営業日予測(リーク無し)
# =========================
alpha12 = 2 / (12 + 1)
alpha26 = 2 / (26 + 1)
alpha9  = 2 / (9 + 1)

horizon = 7  # ★ 30 → 7 に変更

last_close = float(df["Close"].iloc[-1])
last_ema12 = float(df["EMA12"].iloc[-1])
last_ema26 = float(df["EMA26"].iloc[-1])
last_signal = float(df["Signal"].iloc[-1])

# RSI初期化(直近14日の平均gain/loss)
last14 = df["Close"].iloc[-15:]
d14 = last14.diff().dropna()
avg_gain = float(d14.where(d14 > 0, 0.0).mean())
avg_loss = float((-d14.where(d14 < 0, 0.0)).mean())

def compute_rsi_from_avgs(avg_gain, avg_loss):
    rs = avg_gain / (avg_loss + 1e-12)
    rsi = 100 - (100 / (1 + rs))
    return float(np.clip(rsi, 0, 100))
# ★学習分布の分位(予測リターンを常識範囲に収める)
y_train_logret = y_train_raw.flatten()
lo, hi = np.quantile(y_train_logret, [0.01, 0.99])

# ★平均回帰(7日版は弱め推奨。30日版より大きめ)
DRIFT = 0.90   # 0.85〜0.95で調整
# 入力シーケンス(最後のtime_step分)
X_all_scaled = x_scaler.transform(X_all)
seq = X_all_scaled[-time_step:].copy()

future_prices = []
upper_band = []
lower_band = []

p = last_close

for t in range(1, horizon + 1):
    x_in = seq.reshape(1, time_step, len(feature_cols))
    pred_scaled = float(model.predict(x_in, verbose=0)[0, 0])

    # ★MinMax外挿対策(0〜1の範囲に収める)
    pred_scaled = float(np.clip(pred_scaled, 0.0, 1.0))

    pred_logret = float(y_scaler.inverse_transform(np.array([[pred_scaled]], dtype=np.float32))[0, 0])

    # ★学習分布に収める(分位クリップ)
    pred_logret = float(np.clip(pred_logret, lo, hi))

    # ★平均回帰(ドリフト抑制)
    pred_logret *= DRIFT 
    # 価格更新
    p = float(p * np.exp(pred_logret))

    # EMA/MACD/Signal 更新
    last_ema12 = float(alpha12 * p + (1 - alpha12) * last_ema12)
    last_ema26 = float(alpha26 * p + (1 - alpha26) * last_ema26)
    macd = float(last_ema12 - last_ema26)
    last_signal = float(alpha9 * macd + (1 - alpha9) * last_signal)

    # RSI更新
    prev_p = float(p / np.exp(pred_logret))
    change = p - prev_p
    gain_t = max(change, 0.0)
    loss_t = max(-change, 0.0)
    n = 14
    avg_gain = (avg_gain * (n - 1) + gain_t) / n
    avg_loss = (avg_loss * (n - 1) + loss_t) / n
    rsi01 = float(compute_rsi_from_avgs(avg_gain, avg_loss) / 100.0)

    # 特徴量を再構成して次の入力へ
    ema12_ratio = float(last_ema12 / p - 1.0)
    ema26_ratio = float(last_ema26 / p - 1.0)

    feat = np.array([pred_logret, ema12_ratio, ema26_ratio, macd, last_signal, rsi01], dtype=np.float32)
    feat_scaled = x_scaler.transform(feat.reshape(1, -1))[0]

    seq = np.vstack([seq[1:], feat_scaled])

    future_prices.append(p)

    # バンド(logret誤差σ×√t)
    sigma_t = resid_std * np.sqrt(t)
    upper_band.append(p * np.exp(sigma_t))
    lower_band.append(p * np.exp(-sigma_t))

future_prices = np.array(future_prices, dtype=float)
upper_band = np.array(upper_band, dtype=float)
lower_band = np.array(lower_band, dtype=float)

future_dates = pd.bdate_range(df.index[-1] + pd.tseries.offsets.BDay(1), periods=horizon)

# =========================
# 10) 可視化&保存(7日版)
# =========================
plt.figure(figsize=(14, 8))
plt.plot(df.index, df["Close"].values, label="実株価", linewidth=1)
plt.plot(test_dates, pred_prices, label="テスト期間の予測(1日先)", linewidth=2)
plt.plot(future_dates, future_prices, marker="o", markersize=4, linewidth=2, label=f"未来{horizon}営業日予測")
plt.fill_between(future_dates, lower_band, upper_band, alpha=0.25, label="予測バンド(logret誤差±1σ)")

plt.title(f"{ticker} 真の未来予測(リーク無し): テスト検証 + 未来{horizon}営業日")
plt.xlabel("日付")
plt.ylabel("株価(円)")
plt.grid(True)
plt.legend(loc="best")

# 常に同じ名前で保存(HTML用)
out = os.path.expanduser("~/Desktop/7267_latest_7d.png")
plt.savefig(out, dpi=150)
plt.close()
#自動で日時付き保存
# out = os.path.expanduser(f"~/Desktop/{ticker}_true_forecast_{horizon}d_{pd.Timestamp.now().strftime('%Y%m%d_%H%M')}.png")

# =========================
# 11) 未来予測の表示(7日版)
# =========================
print("\n========== 未来予測 ==========")
print(f"データ最終日(起点日): {df.index[-1].date()}")
print(f"起点の終値: {last_close:.2f} 円")
print(f"予測期間: {future_dates[0].date()} 〜 {future_dates[-1].date()}({horizon}営業日)")
print(f"最終日の予測: {future_prices[-1]:.2f} 円")
print(f"最終日のバンド: {lower_band[-1]:.2f} 円 〜 {upper_band[-1]:.2f} 円")
print(f"✅ 保存しました: {out}")