1)コードアセスメント　２）予測チャート出力機能追加　３）#!/usr/bin/env python3
"""
Daily Ensemble Prophet Forecast Tool
週足→日足バージョン
"""
import os, re, logging, warnings
from datetime import date, timedelta
from functools import reduce
from concurrent.futures import ThreadPoolExecutor
import warnings
import numpy as np
import pandas as pd
import yfinance as yf
from prophet import Prophet


# ─── 表示 & ワーニング設定 ────────────────────────────────────
pd.set_option("display.width", 1200)
pd.set_option("display.max_rows", 2000)
pd.set_option("display.max_columns", None)
pd.options.display.float_format = "{:.2f}".format
warnings.filterwarnings("ignore", category=FutureWarning)
warnings.filterwarnings("ignore", category=DeprecationWarning)
# ----------------- ロガー -----------------
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - [%(funcName)s] - %(message)s",
    handlers=[logging.FileHandler("prophet_daily.log", mode="w"),
              logging.StreamHandler()],
)
logger = logging.getLogger(__name__)
for noisy in ["prophet", "cmdstanpy", "yfinance"]:
    logging.getLogger(noisy).setLevel(logging.WARNING)

# ----------------- 定数 -----------------
DEFAULT_CPS = 0.1
USE_MULTIPLICATIVE = False
CLIP_MIN_Y = 1
_valid_sym = re.compile(r"[A-Z0-9.\-^]+")

# ----------------- Util -----------------
def clean_symbol(raw: str) -> str | None:
    m = _valid_sym.search(raw.upper())
    return m.group(0) if m else None

def calculate_mape(a, p):
    a, p = np.asarray(a), np.asarray(p)
    mask = (a != 0) & np.isfinite(a) & np.isfinite(p)
    return np.nan if mask.sum() == 0 else np.mean(np.abs((a[mask]-p[mask])/a[mask]))*100

def get_mape_assessment(m):
    if np.isnan(m): return "Unknown/Invalid"
    if m < 2: return "Very Good (<2%)"
    if m < 5: return "Acceptable (2-5%)"
    return "Unreliable (>5%)"

def get_trend_direction(pred, cur):
    if pred > cur*1.03: return "UP"
    if pred < cur*0.97: return "DOWN"
    return "FLAT"

# ------------- Prophet Ensemble ----------
def process_ticker_ensemble(ticker, df, training_years, end_date,
                            forecast_days, short_name=""):
    logger.info(f"Processing {ticker} ...")
    df = df.copy()
    df["y_orig"] = df["y"]
    df["y"] = np.log(df["y"].clip(lower=CLIP_MIN_Y))

    forecasts = []
    for y in training_years:
        cut = pd.to_datetime(end_date) - pd.DateOffset(years=y)
        train = df[df["ds"] >= cut].copy()
        if len(train) < 80:  # 日足なら80営業日≒4ヶ月確保
            continue
        model = Prophet(
            changepoint_prior_scale=DEFAULT_CPS,
            yearly_seasonality=True,
            weekly_seasonality=True,
            daily_seasonality=False,
            seasonality_mode="multiplicative" if USE_MULTIPLICATIVE else "additive",
        )
        model.fit(train[["ds", "y"]])

        future = model.make_future_dataframe(periods=forecast_days, freq="D")
        fcst = model.predict(future)

        lbl = f"yhat_{y}y"
        sub = fcst[["ds", "yhat", "yhat_upper", "yhat_lower"]].rename(
            columns={"yhat": lbl, "yhat_upper": f"{lbl}_upper",
                     "yhat_lower": f"{lbl}_lower"})
        mape = calculate_mape(np.exp(train["y"]),
                              np.exp(model.predict(train[["ds"]])["yhat"]))
        forecasts.append(dict(forecast=sub, label=lbl, mape=mape))

    if not forecasts:
        return None

    # 逆MAPE重み
    eps = 1e-6
    total_inv = sum(1/(f["mape"]+eps) for f in forecasts)
    for f in forecasts:
        f["weight"] = (1/(f["mape"]+eps)) / total_inv

    # 重み付き合算
    weighted = []
    for f in forecasts:
        df_f = f["forecast"].copy()
        w = f["weight"]
        df_f.iloc[:, 1:] *= w
        weighted.append(df_f)

    ens = reduce(lambda l, r: pd.merge(l, r, on="ds", how="outer"), weighted)
    ens = ens.sort_values("ds").ffill().bfill()

    y_cols = [c for c in ens.columns if re.match(r"^yhat_.*[^_](?<!upper|lower)$", c)]
    up_cols = [c for c in ens.columns if "_upper" in c]
    lo_cols = [c for c in ens.columns if "_lower" in c]

    ens["yhat_log"] = ens[y_cols].sum(axis=1)
    ens["yhat_upper"] = np.exp(ens[up_cols].sum(axis=1))
    ens["yhat_lower"] = np.exp(ens[lo_cols].sum(axis=1))
    ens["yhat"] = np.exp(ens["yhat_log"])

    last = ens[ens["ds"] > pd.to_datetime(end_date)].iloc[-1]

    cur = float(df["y_orig"].iloc[-1])
    pred = float(last["yhat"])
    upper, lower = float(last["yhat_upper"]), float(last["yhat_lower"])

    trend = (pred - cur)/cur*100
    up_ret = (upper - cur)/cur*100
    lo_ret = (lower - cur)/cur*100
    mar = up_ret / abs(lo_ret) if lo_ret else np.nan

    avg_mape = np.average([f["mape"] for f in forecasts],
                          weights=[f["weight"] for f in forecasts])

    return dict(
        Ticker=ticker, **{"Short Name": short_name},
        **{"Current Price": cur, "Pred. Price": pred,
           "Trend Change%": trend, "Trend Direction": get_trend_direction(pred, cur),
           "Price Upper": upper, "Price Lower": lower,
           "Return Upper%": up_ret, "Return Lower%": lo_ret,
           "Predicted MAR": mar, "Avg MAPE%": avg_mape,
           "Accuracy": get_mape_assessment(avg_mape)}
    )

# -------- ランキング (変更なし) ----------
def apply_balance_score_ranking(df):
    ...

# ---------------- MAIN ------------------
def main():
    print("Daily Ensemble Prophet Forecast Tool")
    yrs = input("Training years (e.g. 2,3,5): ") or "2,3,5"
    training_years = [int(x) for x in yrs.split(",") if x.strip().isdigit()]
    forecast_days = int(input("Days to forecast? (default=30): ") or 30)
    tickers = [clean_symbol(s) for s in input("Tickers (comma): ").upper().split(",") if s.strip()]
    if not tickers:
        print("No valid tickers.")
        return

    end_date = date.today()
    start_date = pd.to_datetime(end_date) - pd.DateOffset(years=max(training_years), days=30)

    print(f"📥 Downloading: {', '.join(tickers)}")
    raw = yf.download(tickers, start=start_date,
                      end=end_date+timedelta(days=1),
                      interval="1d", auto_adjust=True,
                      group_by="ticker", threads=True)

    data_by_ticker = {}
    for t in tickers:
        try:
            s = raw[t]["Close"] if isinstance(raw.columns, pd.MultiIndex) else raw["Close"]
            df = s.reset_index().rename(columns={"Date":"ds", "Close":"y"})
            df["ds"] = pd.to_datetime(df["ds"])
            df["y"] = df["y"].interpolate()
            if len(df) >= 80:
                data_by_ticker[t] = df
        except Exception as e:
            logger.warning(f"{t}: preprocess fail → {e}")

    if not data_by_ticker:
        print("All tickers failed.")
        return

    short_names = {t: yf.Ticker(t).info.get("shortName", t) for t in data_by_ticker}

    results = []
    with ThreadPoolExecutor(max_workers=min(8, len(data_by_ticker))) as ex:
        futs = {ex.submit(process_ticker_ensemble, t, df, training_years,
                          end_date, forecast_days, short_names[t]): t
                for t, df in data_by_ticker.items()}
        for i, (fut, t) in enumerate(futs.items(), 1):
            print(f"[{i}/{len(futs)}] {t}")
            try:
                r = fut.result()
                if r: results.append(r)
            except Exception as e:
                logger.error(f"{t}: {e}")

    if not results:
        print("No results.")
        return

    df_res = pd.DataFrame(results)
    cols = ["Ticker","Short Name","Current Price","Pred. Price","Trend Change%","Trend Direction",
            "Price Upper","Price Lower","Return Upper%","Return Lower%","Predicted MAR",
            "Avg MAPE%","Accuracy"]
    df_res = df_res[cols]
    df_res.to_csv("daily_forecast_output.csv", index=False)
    print("\n--- Final Daily Forecasts ---\n")
    print(df_res.to_string(index=False))
    print("\n✅ daily_forecast_output.csv saved.")

    apply_balance_score_ranking(df_res)

if __name__ == "__main__":
    main()