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}")