File size: 5,900 Bytes
cab04ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# -*- coding: utf-8 -*-
"""기쀀뢄포 β€” 'μ‹€λ ₯ 0μ΄μ—ˆλ‹€λ©΄ μ–΄λ””κΉŒμ§€ κ°”μ„κΉŒ'λ₯Ό κ³„μ‚°ν•œλ‹€.



두 κ°€μ§€λ₯Ό κ΅¬λΆ„ν•œλ‹€.



  κ°œλ§‰ μ „ μΆ”μ •μΉ˜ (announce)

    μ°Έκ°€μžμ—κ²Œ "이 μ’…λͺ©μ€ 운으둜 μ΄λ§ŒνΌκΉŒμ§€ λ‚˜μ˜¨λ‹€"κ³  미리 μ•Œλ €μ£ΌλŠ” κ°’.

    κ³Όκ±° 5년을 블둝 λΆ€νŠΈμŠ€νŠΈλž©ν•΄μ„œ λ‚Έλ‹€. ν™”λ©΄μ˜ '운의 ν•œκ³„μ„ '에 μ“΄λ‹€.



  μ±„μ μš© κΈ°μ€€ (live)

    μ‹€μ œ μˆœμœ„λ₯Ό μ •ν•˜λŠ” κ°’. μ‹œμ¦Œμ΄ μ§„ν–‰λœ 만큼의 **μ‹€μ œ 경둜** μœ„μ—μ„œ

    μ‹€λ ₯ 0 μ°Έκ°€μžλ₯Ό 돌렀 λ§Œλ“ λ‹€. 맀일 λ‹€μ‹œ κ³„μ‚°ν•œλ‹€.



μ™œ λ‚˜λˆ„λŠ”κ°€

  기쀀을 κ³Όκ±° λΆ€νŠΈμŠ€νŠΈλž©μœΌλ‘œ κ³ μ •ν•˜λ©΄, μ‹œμ¦Œ 쀑 κ·Έ μ’…λͺ©μ΄ 크게 였λ₯Ό λ•Œ μ°Έκ°€μž

  μ „μ›μ˜ 성적이 ν•¨κ»˜ λΆ€ν’€λ €μ Έ μˆœμœ„κ°€ μ‹œμž₯ λ°©ν–₯을 따라간닀. μ˜¬λ°”λ₯Έ λ°˜μ‚¬μ‹€μ€

  'μ‹œμž₯이 μ‹€μ œλ‘œ κ·Έλ ‡κ²Œ μ›€μ§μ˜€μ„ λ•Œ 운으둜 λ‚Ό 수 μžˆμ—ˆλ˜ 성적'이닀.



πŸ”‘ κ°œλ§‰ 전에 κ³ μ •Β·κ³΅κ°œν•˜λŠ” 것은 μˆ«μžκ°€ μ•„λ‹ˆλΌ **방법**이닀.

   숫자λ₯Ό κ³ μ •ν•˜λ©΄ μ‹œμž₯이 μ˜ˆμƒκ³Ό λ‹€λ₯΄κ²Œ μ›€μ§μ˜€μ„ λ•Œ 채점이 ν†΅μ§Έλ‘œ μ–΄κΈ‹λ‚œλ‹€.



πŸ”‘ κΈ°μ€€ μ°Έκ°€μžλ„ μ°Έκ°€μžμ™€ λ˜‘κ°™μ΄ 수수료λ₯Ό λ‚Έλ‹€. μ•ˆ 물리면 μ°Έκ°€μžλ§Œ λΉ„μš©μ„

   μ§€κ³  μ‹œμž‘ν•œλ‹€. μˆ˜μˆ˜λ£Œκ°€ 높은 μ’…λͺ©μΌμˆ˜λ‘ κ·Έ 차이가 컀진닀.

"""
import os, json, warnings
import numpy as np, pandas as pd
warnings.filterwarnings("ignore")

ROOT = os.path.dirname(os.path.abspath(__file__))
BLOCK = 5
N_LIVE = 4000
N_ANNOUNCE = 20000


def _sim(returns, n, expo, fee, seed, block=None):
    """μ‹€λ ₯ 0 μ°Έκ°€μž nλͺ…μ˜ μ΅œμ’… 수읡λ₯ . expo 만큼의 μ‹œκ°„μ„ ν¬μ§€μ…˜μ— λ…ΈμΆœν•œλ‹€."""
    g = np.random.default_rng(seed)
    r = np.asarray(returns, dtype=float)
    L = len(r)
    out = np.empty(n)
    for i in range(n):
        if block:                                   # λΆ€νŠΈμŠ€νŠΈλž© (κ°œλ§‰ μ „ μΆ”μ •)
            nb = int(np.ceil(L / block))
            st = g.integers(0, max(len(r) - block, 1), size=nb)
            path = np.concatenate([r[j:j+block] for j in st])[:L]
        else:                                       # μ‹€μ œ 경둜 (채점)
            path = r
        act = g.random(L) < expo
        pos = np.where(act, g.choice([-1.0, 1.0], size=L), 0.0)
        cost = np.abs(np.diff(np.r_[0.0, pos])) * fee
        out[i] = np.prod(1 + np.r_[0.0, pos[:-1]] * path - cost) - 1
    return np.sort(out)


def live(prices, fee, expo=2/3, n=N_LIVE, seed=None):
    """μ±„μ μš© β€” μ‹œμ¦Œ κ²½κ³ΌλΆ„μ˜ μ‹€μ œ 경둜 μœ„μ—μ„œ λ§Œλ“ λ‹€. 맀일 λ‹€μ‹œ λΆ€λ₯Έλ‹€.



    seed λ₯Ό λ‚ μ§œλ‘œ μ£Όλ©΄ 같은 λ‚  같은 값이 λ‚˜μ™€ μž¬ν˜„μ΄ λœλ‹€.

    """
    r = pd.Series(prices, dtype=float).sort_index().pct_change().fillna(0.0).values
    s = seed if seed is not None else int(pd.Timestamp.utcnow().strftime("%Y%m%d"))
    return _sim(r, n, expo, fee, s)


def announce(returns, fee, days, expo=2/3, n=N_ANNOUNCE, seed=20260824):
    """κ°œλ§‰ μ „ μΆ”μ • β€” κ³Όκ±°λ₯Ό 블둝 λΆ€νŠΈμŠ€νŠΈλž©ν•΄μ„œ 'μ΄λ§ŒνΌκΉŒμ§€ λ‚˜μ˜¨λ‹€'λ₯Ό λ‚Έλ‹€."""
    r = np.asarray(returns, dtype=float)
    # μ‹œμ¦Œ 길이만큼만 쓰도둝 잘라 뢙인닀
    reps = int(np.ceil(days / len(r))) + 1
    pool = np.tile(r, reps)[:max(len(r), days * 3)]
    return _sim(pool[:days] if len(pool) >= days else pool, n, expo, fee, seed, block=BLOCK)


def pct_of(ret, refv):
    import bisect
    return bisect.bisect_left(refv, ret) / len(refv)


def score_of(ret, refv):
    import math
    p = min(pct_of(ret, refv), 1 - 1e-6)
    return -math.log10(max(1.0 - p, 1e-6))


# ─────────────────────── κ°œλ§‰ 곡지값 생성 ───────────────────────
if __name__ == "__main__":
    import seasons as S
    import scoring as SC
    import yfinance as yf

    s1 = S.SEASONS[1]
    days = (pd.Timestamp(s1["closes"]) - pd.Timestamp(s1["opens"])).days
    print("μ‹œμ¦Œ 1 κ°œλ§‰ 곡지값 β€” %d일 Β· 수수료 반영\n" % days)
    print("  %-6s %-8s %8s %11s %11s %11s"
          % ("μ’…λͺ©", "데이터", "수수료", "운 쀑앙", "운 μƒμœ„5%", "운 μƒμœ„1%"))
    out = {}
    for code, a in s1["assets"].items():
        src = a["src"]
        if code == "BTC":
            p = os.path.join(os.path.dirname(ROOT), "btc-oracle", "data", "hourly.parquet")
            H = pd.read_parquet(p).sort_index()
            if "close" not in H.columns:
                H = H.rename(columns={"c": "close"})
            px = H.close.groupby(H.index.normalize()).last()
        else:
            d = yf.download(src, period="5y", interval="1d",
                            progress=False, auto_adjust=True)["Close"]
            px = (d.iloc[:, 0] if isinstance(d, pd.DataFrame) else d).dropna()
        r = px.pct_change().dropna().values
        fee = SC.fee_of(code)
        c = announce(r, fee, days)
        out[code] = {"p50": float(np.median(c)), "p95": float(np.percentile(c, 95)),
                     "p99": float(np.percentile(c, 99)), "fee": fee, "days": days}
        print("  %-6s %-8s %7.2f%% %10.1f%% %10.1f%% %10.1f%%"
              % (code, src, fee * 100, np.median(c) * 100,
                 np.percentile(c, 95) * 100, np.percentile(c, 99) * 100))
    json.dump({"season": 1, "opens": s1["opens"], "closes": s1["closes"], "days": days,
               "method": "λΈ”λ‘λΆ€νŠΈμŠ€νŠΈλž© 5일 Β· λ¬΄μž‘μœ„ ν¬μ§€μ…˜ Β· λ ˆλ²„λ¦¬μ§€1 Β· 수수료 반영",
               "note": "κ°œλ§‰ κ³΅μ§€μš© μΆ”μ •μΉ˜. μ‹€μ œ 채점은 μ‹œμ¦Œ μ‹€μ œ 경둜둜 맀일 μž¬κ³„μ‚°ν•œλ‹€.",
               "ceiling": out},
              open(os.path.join(ROOT, "announce_ceiling.json"), "w"),
              ensure_ascii=False, indent=1)
    print("\n  ν™”λ©΄μ˜ '운의 ν•œκ³„μ„ 'μ—λŠ” μƒμœ„5%λ₯Ό μ“΄λ‹€.")
    print("  μ €μž₯: finchal/announce_ceiling.json")