# -*- coding: utf-8 -*-
"""
random_bot_review_test.py - 동전 던지기 봇 1,000개로 '후기감 캡처'가 몇 개 나오는지 재는 코드

글: https://algolab.co.kr/blog/algotrading-program-review-random-bot-test-2026

무엇을 하나
  전략이 전혀 없는 봇을 만든다. 들고 있는 종목이 없으면 매일 10% 확률로
  22종목 중 하나를 무작위로 사서 5~20거래일 뒤 판다. 왕복 비용 0.3%.
  3개월(63거래일) 구간 N개에서 봇 1,000개씩 돌려
    (1) 3개월 +20% 이상, 즉 후기로 올릴 만한 캡처가 나오는 봇의 비율
    (2) 그 봇들의 '다음 3개월' 성적이 나머지 봇과 다른지
  를 센다. 이어서 봇 1개를 전 기간 돌려 '가장 좋은 3개월'과 '전체'를 비교한다.

가정과 한계
  - FinanceDataReader 수정주가, 배당은 반영 안 함(수정주가의 조정분 제외).
  - 22종목은 지금 시점에 고른 생존 종목이다. 상장폐지 종목이 없어 결과가 좋게 나온다.
  - 체결은 해당일 시가, 호가 충격은 미반영.
  - 투자 권유가 아니며 과거 결과는 미래를 보장하지 않는다.

ASOF 를 고정한 이유: 기준일 없이 돌리면 실행 시점까지 데이터를 받아
글의 숫자와 다르게 나온다. 최신으로 보려면 ASOF = None (숫자 달라짐).

필요: pip install finance-datareader pandas numpy
"""
import os
import numpy as np
import pandas as pd
import FinanceDataReader as fdr

ASOF = "2026-10-30"      # 기준일 고정 - None 이면 실행 시점까지
START = "2016-01-01"
SEED = 20261105          # 무작위 시드 고정 - 바꾸면 숫자가 조금 달라진다
N_BOTS = 1000
WINDOW = 63              # 3개월 = 63거래일
COST = 0.003             # 왕복 비용 0.3%
P_ENTER = 0.10           # 빈손일 때 하루 진입 확률
HOLD_MIN, HOLD_MAX = 5, 20
REVIEW_LINE = 0.20       # 후기로 올릴 만한 기준: 3개월 +20%
CACHE = "random_bot_prices.pkl"

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_opens():
    """종목별 시가 표. 자르기는 캐시를 쓰든 새로 받든 똑같이 적용한다."""
    if os.path.exists(CACHE):
        px = pd.read_pickle(CACHE)
    else:
        px = pd.DataFrame({c: fdr.DataReader(c, START, ASOF)["Open"].astype(float)
                           for c in TICKERS})
        px.to_pickle(CACHE)
    px = px.sort_index()
    if ASOF:
        px = px[px.index <= pd.Timestamp(ASOF)]
    return px[px.index >= pd.Timestamp(START)].replace(0, np.nan).ffill()


def run_bot(opens, start, end, rng):
    """start~end 인덱스 구간을 봇 1개로 돌린 총수익률. 끝에 들고 있으면 그날 시가로 정리."""
    equity, i = 1.0, start
    n_stock = opens.shape[1]
    while i < end:
        if rng.random() < P_ENTER:
            col = rng.integers(n_stock)
            buy = opens[i, col]
            j = min(i + int(rng.integers(HOLD_MIN, HOLD_MAX + 1)), end)
            sell = opens[j, col]
            if np.isfinite(buy) and np.isfinite(sell) and buy > 0:
                equity *= (sell / buy) * (1 - COST)
            i = j + 1
        else:
            i += 1
    return equity - 1


def main():
    px = load_opens()
    opens = px.values
    n = len(px)
    rng = np.random.default_rng(SEED)
    print(f"데이터 {px.index[0].date()} ~ {px.index[-1].date()} / {n}거래일 / 종목 {px.shape[1]}개")

    # 겹치지 않는 '캡처 구간 + 다음 3개월' 쌍
    rows, keep_all, keep_win = [], [], []
    starts = list(range(0, n - 2 * WINDOW, WINDOW))
    for s in starts:
        first = np.array([run_bot(opens, s, s + WINDOW, rng) for _ in range(N_BOTS)])
        # 같은 봇이 같은 규칙으로 다음 3개월을 또 돈다(규칙에 기억이 없다는 것이 핵심)
        second = np.array([run_bot(opens, s + WINDOW, s + 2 * WINDOW, rng) for _ in range(N_BOTS)])
        win = first >= REVIEW_LINE
        rows.append({
            "시작": px.index[s].date(),
            "후기감_비율": win.mean(),
            "최고": first.max(),
            "중앙값": np.median(first),
            "손실봇_비율": (first < 0).mean(),
            "후기봇_다음3개월_평균": second[win].mean() if win.any() else np.nan,
            "나머지_다음3개월_평균": second[~win].mean() if (~win).any() else np.nan,
        })
        keep_all.append(first)
        keep_win.append(second[win])
    t = pd.DataFrame(rows)
    pd.set_option("display.width", 200)
    print(t.round(3).to_string(index=False))

    all_first = np.concatenate(keep_all)
    print(f"\n구간 {len(starts)}개 x 봇 {N_BOTS}개 = {all_first.size:,}회")
    print(f"3개월 +{REVIEW_LINE:.0%} 이상: {(all_first >= REVIEW_LINE).mean():.1%}")
    print(f"3개월 +50% 이상: {(all_first >= 0.5).mean():.1%}")
    print(f"3개월 손실: {(all_first < 0).mean():.1%}")
    print(f"3개월 수익률 중앙값: {np.median(all_first):.1%}")
    print(f"구간별 후기감 비율 최소/최대: {t['후기감_비율'].min():.1%} / {t['후기감_비율'].max():.1%}")
    print(f"후기감 봇의 다음 3개월 평균: {np.concatenate(keep_win).mean():.1%}")
    print(f"후기감 봇의 다음 3개월 중앙값: {np.median(np.concatenate(keep_win)):.1%}")
    print(f"후기감 봇 중 다음 3개월도 +{REVIEW_LINE:.0%} 이상: "
          f"{(np.concatenate(keep_win) >= REVIEW_LINE).mean():.1%}")
    print(f"나머지 봇 다음 3개월 평균(구간 평균): {t['나머지_다음3개월_평균'].mean():.1%}")

    # 봇 1개 전 기간: 가장 좋은 3개월 vs 전체
    print("\n[봇 1개를 전 기간 돌려 캡처 구간을 고르면]")
    best_list, total_list = [], []
    for _ in range(N_BOTS):
        eq, i = [1.0], 0
        while i < n - 1:
            if rng.random() < P_ENTER:
                col = rng.integers(px.shape[1])
                j = min(i + int(rng.integers(HOLD_MIN, HOLD_MAX + 1)), n - 1)
                buy, sell = opens[i, col], opens[j, col]
                r = (sell / buy) * (1 - COST) if np.isfinite(buy) and np.isfinite(sell) and buy > 0 else 1.0
                eq.extend([eq[-1]] * (j - i - 1) + [eq[-1] * r])
                i = j
            else:
                eq.append(eq[-1])
                i += 1
        e = np.array(eq)
        best_list.append((e[WINDOW:] / e[:-WINDOW]).max() - 1)
        total_list.append(e[-1] - 1)
    best, total = np.array(best_list), np.array(total_list)
    years = (px.index[-1] - px.index[0]).days / 365.25
    cagr = (1 + total) ** (1 / years) - 1
    print(f"가장 좋은 3개월 중앙값: {np.median(best):.1%}")
    print(f"전체 {years:.1f}년 CAGR 중앙값: {np.median(cagr):.1%}")
    print(f"최고 3개월이 +{REVIEW_LINE:.0%} 이상인 봇: {(best >= REVIEW_LINE).mean():.1%}")
    print(f"최고 3개월이 +{REVIEW_LINE:.0%} 이상인데 전체는 손실인 봇: "
          f"{((best >= REVIEW_LINE) & (total < 0)).mean():.1%}")
    print(f"전체 손실 봇: {(total < 0).mean():.1%}")

    # 비교 기준: 22종목 동일비중 보유, 구간 수익률 중앙값
    bench = []
    for s in starts:
        r = opens[s + WINDOW] / opens[s] - 1
        bench.append(np.nanmean(r))
    print(f"\n22종목 동일비중 보유의 3개월 수익률 중앙값: {np.median(bench):.1%}")


if __name__ == "__main__":
    main()
