"""
추세 전략에 ADX 필터를 걸면 — 필터 없음 / ADX 20 / 25 / 30 으로 바꿔 10년 돌린 실측
https://algolab.co.kr/blog/adx-filter-trend-breakout-backtest-2026

진입 : 종가가 직전 20거래일 고가를 넘으면(20일 돌파) -> 다음 날 시가 매수
       ADX 필터 : 신호일 ADX(14) >= 기준값일 때만 진입 (없음 = 모든 돌파 진입)
청산 : ① 손절 — 진입가 -5%. 장중 저가가 닿으면 손절가 체결
              (시가부터 손절가 아래로 갭이면 시가 체결)
       ② 추세 이탈 — 종가가 직전 10거래일 저가를 깨면 다음 날 시가 매도
비용 : 왕복 0.3% (수수료+세금+슬리피지 뭉뚱그린 가정)
종목 : 2016년 이전 상장 국내 종목 22개 (지금 살아 있는 종목 = 생존 편향 있음)
횡보 판정 : ADX 와 무관한 잣대로 — 신호일 기준 60거래일 효율비율(ER)
            ER = |C[t] - C[t-60]| / sum(|C[k] - C[k-1]|)  ,  ER < 0.2 를 '횡보'로 본다

ADX 는 TA-Lib ADX 와 같은 와일더 방식으로 pandas 만으로 계산한다.
앞 100개 봉은 초기값 영향이 남아 신호에서 뺀다(START 이전 데이터로 충분히 데운다).

ASOF 를 고정한 이유: 기준일 없이 돌리면 실행 시점까지 데이터를 받아
글의 숫자와 다르게 나온다. 최신으로 보려면 ASOF = None (숫자 달라짐).
수정주가(FinanceDataReader 기본값) 기준, 배당 미포함. 과거 결과는 미래를 보장하지 않는다.
"""
import numpy as np, pandas as pd, FinanceDataReader as fdr

ASOF = "2026-10-30"     # 기준일 고정 — None 이면 실행 시점까지
START = "2016-01-01"
COST = 0.003
STOP = 0.05
FILTERS = [None, 20, 25, 30]
SEED = 7

TICKERS = {
    "005930": "삼성전자", "000660": "SK하이닉스", "005380": "현대차", "000270": "기아",
    "012330": "현대모비스", "035420": "NAVER", "035720": "카카오", "051910": "LG화학",
    "006400": "삼성SDI", "068270": "셀트리온", "105560": "KB금융", "055550": "신한지주",
    "015760": "한국전력", "017670": "SK텔레콤", "030200": "KT", "033780": "KT&G",
    "009150": "삼성전기", "086520": "에코프로", "028300": "HLB", "196170": "알테오젠",
    "041510": "에스엠", "035900": "JYP Ent.",
}


def load(code):
    df = fdr.DataReader(code, "2014-01-01", ASOF)[["Open", "High", "Low", "Close"]].astype(float)
    if ASOF:
        df = df[df.index <= ASOF]          # 캐시·신규 다운로드 모두 같은 자르기
    return df[(df["Open"] > 0) & (df["Low"] > 0)]


def wilder_adx(df, n=14):
    """TA-Lib ADX 와 같은 계산: 첫 값은 n개 합/평균, 이후 와일더 평활."""
    h, l, c = (df[k].values for k in ("High", "Low", "Close"))
    m = len(df)
    tr = np.full(m, np.nan); pdm = np.full(m, np.nan); mdm = np.full(m, np.nan)
    for i in range(1, m):
        up, dn = h[i] - h[i - 1], l[i - 1] - l[i]
        pdm[i] = up if (up > dn and up > 0) else 0.0
        mdm[i] = dn if (dn > up and dn > 0) else 0.0
        tr[i] = max(h[i] - l[i], abs(h[i] - c[i - 1]), abs(l[i] - c[i - 1]))
    atr = np.full(m, np.nan); sp = np.full(m, np.nan); sm = np.full(m, np.nan)
    atr[n] = np.nansum(tr[1:n + 1]); sp[n] = np.nansum(pdm[1:n + 1]); sm[n] = np.nansum(mdm[1:n + 1])
    for i in range(n + 1, m):
        atr[i] = atr[i - 1] - atr[i - 1] / n + tr[i]
        sp[i] = sp[i - 1] - sp[i - 1] / n + pdm[i]
        sm[i] = sm[i - 1] - sm[i - 1] / n + mdm[i]
    pdi = 100 * sp / atr; mdi = 100 * sm / atr
    dx = 100 * np.abs(pdi - mdi) / (pdi + mdi)
    adx = np.full(m, np.nan)
    adx[2 * n - 1] = np.nanmean(dx[n:2 * n])
    for i in range(2 * n, m):
        adx[i] = (adx[i - 1] * (n - 1) + dx[i]) / n
    adx[:100] = np.nan                     # 초기값 영향 구간은 버린다
    return adx, pdi, mdi


def efficiency_ratio(close, n=60):
    c = pd.Series(close)
    return (c.diff(n).abs() / c.diff().abs().rolling(n).sum()).values


def run(df, th):
    o, h, l, c = (df[x].values for x in ("Open", "High", "Low", "Close"))
    hi20 = df["High"].rolling(20).max().shift(1).values
    lo10 = df["Low"].rolling(10).min().shift(1).values
    adx, pdi, mdi = wilder_adx(df)
    er = efficiency_ratio(c)
    start_i = max(int(np.searchsorted(df.index.values, np.datetime64(START))), 100)
    trades, i, n = [], start_i, len(df)
    while i < n - 1:
        ok = c[i] > hi20[i] and (th is None or (not np.isnan(adx[i]) and adx[i] >= th))
        if ok:
            e = i + 1
            entry = o[e]
            stop_px = entry * (1 - STOP)
            j, exit_px, why = e, None, None
            while j < n:
                if l[j] <= stop_px:
                    exit_px, why = (o[j] if o[j] < stop_px else stop_px), "stop"
                    break
                if j + 1 < n and c[j] < lo10[j]:
                    exit_px, why = o[j + 1], "trend"
                    j += 1
                    break
                j += 1
            if exit_px is None:                           # 기준일까지 보유 중 -> 마지막 종가
                exit_px, why, j = c[n - 1], "open", n - 1
            ret = exit_px / entry - 1 - COST
            trades.append(dict(sig_dt=df.index[i], entry_dt=df.index[e], exit_dt=df.index[j], ret=ret,
                               why=why, adx=adx[i], er=er[i], hold=j - e, di_up=pdi[i] > mdi[i]))
            i = j + 1 if why == "stop" else j
            continue
        i += 1
    return trades


def equity_stats(tr):
    eq = np.cumprod(1 + np.array([t["ret"] for t in tr])) if tr else np.array([1.0])
    peak = np.maximum.accumulate(np.r_[1.0, eq])
    return eq[-1] - 1, (np.r_[1.0, eq] / peak - 1).min()


def lab(th):
    return "none" if th is None else f"ADX{th}"


if __name__ == "__main__":
    data = {k: load(k) for k in TICKERS}
    res = {th: {k: run(df, th) for k, df in data.items()} for th in FILTERS}
    flat = {th: [t for v in res[th].values() for t in v] for th in FILTERS}
    print(f"ASOF {ASOF}  tickers {len(TICKERS)}  stop -{STOP:.0%}  cost {COST:.1%}\n")

    print("[1] 전체 (22종목 거래 합산)")
    print("filter   trades   win%  stop_hit%  expectancy  avg_hold  med_cum   med_mdd")
    for th in FILTERS:
        tr = flat[th]; r = np.array([t["ret"] for t in tr])
        st = [equity_stats(v) for v in res[th].values()]
        print(f"{lab(th):>6}  {len(tr):>6} {np.mean(r > 0):6.1%}  {np.mean([t['why'] == 'stop' for t in tr]):8.1%}"
              f"  {r.mean():9.2%}  {np.mean([t['hold'] for t in tr]):7.1f}d"
              f"  {np.median([s[0] for s in st]):8.1%}  {np.median([s[1] for s in st]):7.1%}")

    print("\n[2] 필터 없음 거래를 신호일 ADX 구간으로 나누면")
    print("adx_bin   trades   win%  expectancy  sum_ret")
    base = flat[None]
    for lo, hi in ((0, 20), (20, 25), (25, 30), (30, 40), (40, 1000)):
        g = [t for t in base if lo <= t["adx"] < hi]
        r = np.array([t["ret"] for t in g])
        print(f"{lo:>3}-{hi if hi < 1000 else '':<4}  {len(g):>6} {np.mean(r > 0):6.1%}  {r.mean():9.2%}  {r.sum():8.1%}")

    print("\n[3] 횡보(ER<0.2) vs 추세(ER>=0.2) — 신호일 60일 효율비율 기준")
    print("filter  regime  trades   win%  expectancy  sum_ret")
    for th in FILTERS:
        for reg, f in (("chop", lambda t: t["er"] < 0.2), ("trend", lambda t: t["er"] >= 0.2)):
            g = [t for t in flat[th] if f(t)]
            r = np.array([t["ret"] for t in g])
            print(f"{lab(th):>6}  {reg:>6}  {len(g):>6} {np.mean(r > 0):6.1%}  {r.mean():9.2%}  {r.sum():8.1%}")

    print("\n[4] '거래가 줄어서' vs '나쁜 거래가 빠져서' — 같은 비율을 무작위로 뺀 1,000회와 비교")
    print("    (필터 없음 거래 목록에서 ADX<th 인 것만 뺀 결과 vs 같은 개수를 무작위로 뺀 결과)")
    rng = np.random.default_rng(SEED)
    r0 = np.array([t["ret"] for t in base]); a0 = np.array([t["adx"] for t in base])
    for th in (20, 25, 30):
        keep = a0 >= th
        k = int(keep.sum())
        sims = np.array([r0[rng.choice(len(r0), k, replace=False)].mean() for _ in range(1000)])
        pct = np.mean(sims >= r0[keep].mean())
        print(f"  ADX{th}: kept {k}/{len(r0)}  kept_exp {r0[keep].mean():.2%}  dropped_exp {r0[~keep].mean():.2%}"
              f"  random_exp p5 {np.percentile(sims, 5):.2%} p50 {np.median(sims):.2%} p95 {np.percentile(sims, 95):.2%}"
              f"  share_random>=kept {pct:.1%}")

    print("\n[5] 기간별 거래당 기대값")
    for name, a, b in (("2016-2020", "2016-01-01", "2020-12-31"), ("2021-", "2021-01-01", "2100-01-01")):
        row = []
        for th in FILTERS:
            r = np.array([t["ret"] for t in flat[th] if a <= str(t["entry_dt"].date()) <= b])
            row.append(f"{lab(th)} {r.mean():.2%}({len(r)})")
        print(f"{name:>10}: " + "  ".join(row))

    print("\n[6] 종목별 누적수익 1등 횟수 (동률 제외)")
    wins = {th: 0 for th in FILTERS}
    for k in TICKERS:
        cums = {th: equity_stats(res[th][k])[0] for th in FILTERS}
        best = max(cums.values()); top = [th for th, v in cums.items() if v == best]
        if len(top) == 1:
            wins[top[0]] += 1
    print("  ".join(f"{lab(th)}:{wins[th]}" for th in FILTERS))

    print("\n[7] 에코프로 제외 + 상위 1% 거래 제외 기대값")
    for th in FILTERS:
        r = np.array([t["ret"] for k, v in res[th].items() if k != "086520" for t in v])
        q = np.sort(r)[:-max(1, int(len(r) * 0.01))]
        print(f"  {lab(th):>6}  {len(r):>5}  {r.mean():.2%}  top1%_out {q.mean():.2%}")

    print("\n[8] ADX25 + 방향(+DI > -DI) 를 같이 건 경우")
    tr = [t for t in base if t["adx"] >= 25 and t["di_up"]]
    r = np.array([t["ret"] for t in tr])
    print(f"  (필터 없음 목록 기준 재분류) trades {len(tr)}  win {np.mean(r > 0):.1%}  exp {r.mean():.2%}")
    print(f"  돌파 신호 중 +DI>-DI 비율 {np.mean([t['di_up'] for t in base]):.1%}")

    print("\n[9] 종목별 누적수익 — 필터 없음 vs ADX25")
    for k in TICKERS:
        a, b = equity_stats(res[None][k]), equity_stats(res[25][k])
        print(f"  {TICKERS[k]:<8} {a[0]:+8.1%} {b[0]:+8.1%}   mdd {a[1]:.1%} {b[1]:.1%}   n {len(res[None][k])} {len(res[25][k])}")
