"""只用訊號日前已入庫的停券事件，不把歷史回填事件提前使用。"""
from functools import lru_cache

import numpy as np
import pandas as pd
from finlab import data
from finlab.backtest import sim

START, END = "2025-01-16", "2026-11-10"


@lru_cache(None)
def inputs():
    close = pd.DataFrame(data.get("price:收盤價"))
    cols = [s for s in close if len(s) == 4 and s.isdigit()
            and not s.startswith(("0", "91"))]
    close = close[cols].loc[START:END]
    short = pd.DataFrame(data.get("margin_transactions:融券今日餘額")).reindex_like(close)
    margin = pd.DataFrame(data.get("margin_transactions:融資今日餘額")).reindex_like(close)
    ratio = short.div(margin.where(margin > 0)).mul(100)
    value = pd.DataFrame(data.get("price:成交金額"))[cols].rolling(20).mean().reindex_like(close)
    events = pd.DataFrame(data.get("margin_short_sale_suspension")).copy()
    events["date"] = pd.to_datetime(events["停券起日(最後回補日)"], errors="coerce")
    events["known"] = pd.to_datetime(events["key_date"], errors="coerce")
    events = events.dropna(subset=["date", "known"]).drop_duplicates(["stock_id", "date"])
    return close, short, ratio, value, events


@lru_cache(None)
def build_position(lead=10, ratio_min=30, lag=2, control=False):
    close, short, ratio, value, events = inputs()
    days, cols = close.index, close.columns
    col = {s: j for j, s in enumerate(cols)}
    position = np.zeros(close.shape, dtype=bool)
    used = {}
    count = 0
    event_by_stock = {s: g for s, g in events.groupby("stock_id")}
    for row in events.sort_values(["date", "stock_id"]).itertuples():
        d = days.searchsorted(row.date)
        entry, exit_day = d - lead, d + lag
        signal = entry - 1
        if row.stock_id not in col or signal < 0 or exit_day >= len(days):
            continue
        # Conservative one-calendar-day lag on the ingestion timestamp.
        if row.known >= days[signal]:
            continue
        s = row.stock_id
        eligible = (short.iloc[signal].ge(1000) & ratio.iloc[signal].ge(ratio_min)
                    & value.iloc[signal].ge(20_000_000) & close.iloc[signal].notna())
        if not eligible[s]:
            continue
        if control:
            candidates = ratio.iloc[signal][eligible].drop(index=s, errors="ignore")
            candidates = (candidates - ratio.iloc[signal][s]).abs().sort_values(kind="stable")
            chosen = None
            for candidate in candidates.index:
                known = event_by_stock.get(candidate)
                has_event = known is not None and (
                    (known.known < days[signal]) & known.date.between(days[signal], days[exit_day])
                ).any()
                if not has_event and candidate not in used.get(signal, set()):
                    chosen = candidate
                    break
            if chosen is None:
                continue
            s = chosen
            used.setdefault(signal, set()).add(s)
        # sim shifts each instruction to the next open: exit on D+lag open.
        position[signal:exit_day - 1, col[s]] = True
        count += 1
    return pd.DataFrame(position, index=days, columns=cols), count


def build_report(lead=10, ratio_min=30, lag=2, control=False):
    position, _ = build_position(lead, ratio_min, lag, control)
    return sim(position, trade_at_price="open", position_limit=0.1, upload=False)


if __name__ == "__main__":
    build_report().to_html("report_strategy.html")
