Spaces:
Running
Running
| # -*- coding: utf-8 -*- | |
| """์ฑ์ ์์ง โ ํฌ์ง์ ๊ธฐ๋ก์์ ์์ต๋ฅ ๊ณผ ์์ ์ ์๋ฅผ ๋ธ๋ค. | |
| ๊ท์น (์ฌ์ ๋ฑ๋ก) | |
| ํฌ์ง์ ์ -1.0 ~ +1.0. ๋ฒ์๋ฅผ ๋ฒ์ด๋๋ฉด ์๋ผ๋ธ๋ค (๋ ๋ฒ๋ฆฌ์ง 1 ๊ณ ์ ) | |
| ํฌ์ง์ ๊ฐฑ์ ์ ๋งค์๊ฐ ๊ฐ๋ฅ. ๋ฏธ๊ฐฑ์ ๊ตฌ๊ฐ์ ์ง์ ํฌ์ง์ ์ด ์ ์ง๋๋ค | |
| ์์ต๋ฅ = ๊ฐ ๊ตฌ๊ฐ ํฌ์ง์ x ๊ทธ ๊ตฌ๊ฐ ์์ต๋ฅ ์ ๋ณต๋ฆฌ | |
| ๊ฑฐ๋๋น์ฉ = ํฌ์ง์ ๋ณ๊ฒฝ๋ถ์ ์๋ณต 0.02% (๋ฏธ๊ตญ์ฃผ์ ๋ฌด๋ฃ + ์คํ๋ ๋ ์ถ์ ) | |
| ์์ ์ ์ = -log10(1 - ๋ฐฑ๋ถ์) ๋ฐฑ๋ถ์๋ ๊ทธ ์ข ๋ชฉ์ '์ค๋ ฅ0 ๋ถํฌ' ๊ธฐ์ค | |
| ์ด ํ์ผ์ ๋ฐ๊พธ๋ฉด selftest() ๋ฅผ ๋ฐ๋์ ๋ค์ ๋๋ฆฐ๋ค. ์ฑ์ ์ฝ๋์ ์ค๋ฅ๋ ์์ธ๋ก | |
| ๋๋ฌ๋์ง ์๊ณ ๊ทธ๋ด๋ฏํ ์ซ์๋ก ๋์ค๋ฏ๋ก, ๋ซํ ํํ๋ก ๋ต์ ์๋ ๊ฒฝ์ฐ๋ค๋ก๋ง ๊ฒ์ฆํ๋ค. | |
| """ | |
| import os, json, math, bisect | |
| import numpy as np | |
| import pandas as pd | |
| ROOT = os.path.dirname(os.path.abspath(__file__)) | |
| # ์ข ๋ชฉ๋ณ ์ค๊ฑฐ๋ ๋น์ฉ. ํฌ์ง์ ๋ณ๊ฒฝ๋ถ(|ฮw|)์ ๊ณฑํ๋ค. | |
| # ๐ ์ด๊ฑธ ๋ฌผ๋ฆฌ์ง ์์ผ๋ฉด ํฌ์ง์ ์ ์ด๋น ๋ค์ง๋ ๋ฌด์ฐจ๋ณ ์ ์ถ์ด ์ด๋์ ๋ณธ๋ค. | |
| # ๋งค์๊ฐ ยฑ1์ ๋ค์ง์ผ๋ฉด 121์ผ์ |ฮw| ํฉ๊ณ๊ฐ 5,808์ด ๋๋๋ฐ, ์ฝ์ธ 0.06%๋ฉด | |
| # ์์๋ฃ๋ง ์๋ณธ์ 348%๋ค. ๊ท์น์ด ์๋๋ผ ๋น์ฉ์ด ๊ทธ๋ฐ ํ๋์ ๋ง๋๋ค. | |
| FEES = { | |
| # ์ฝ์ธ โ ๋ฐ์ด๋ธ์ค ํ ์ด์ปค 0.05% + ์ฌ๋ฆฌํผ์ง 0.01% | |
| "BTC": 0.0006, "ETH": 0.0006, "SOL": 0.0008, | |
| # ๋ฏธ๊ตญ ์ฃผ์ โ ์์๋ฃ ๋ฌด๋ฃ์ง๋ง ์คํ๋ ๋ยท์ฌ๋ฆฌํผ์ง๊ฐ ๋จ๋๋ค | |
| "NVDA": 0.0002, "TSLA": 0.0002, "AAPL": 0.0002, | |
| # ์ ๋ฌผ โ ๊ณ์ฝ๋น ์์๋ฃ + ํธ๊ฐํญ | |
| "GOLD": 0.0002, "OIL": 0.0003, | |
| } | |
| FEE_DEFAULT = 0.0006 | |
| FEE = FEE_DEFAULT # selftest์์ ์์๋ก ๋ฐ๊ฟ ์ฐ๋ ์ ์ญ๊ฐ | |
| def fee_of(asset=None): | |
| if asset is None: | |
| return FEE | |
| return FEES.get(asset, FEE_DEFAULT) | |
| POS_MIN, POS_MAX = -1.0, 1.0 | |
| def clip(p): | |
| """๋ ๋ฒ๋ฆฌ์ง 1 ์ํ. ๋๊ฒจ ์ ์ถํด๋ ์๋ผ๋ธ๋ค โ ๊ฑฐ๋ถํ์ง ์๋ ์ด์ ๋ | |
| ์์ด์ ํธ๊ฐ ๋ฐ์ฌ๋ฆผ ์ค์ฐจ๋ก 1.0000001์ ๋ณด๋ด๋ ์ผ์ด ํํ๊ธฐ ๋๋ฌธ์ด๋ค.""" | |
| return max(POS_MIN, min(POS_MAX, float(p))) | |
| def equity(positions, prices, asset=None): | |
| """ํฌ์ง์ ๊ธฐ๋ก + ๊ฐ๊ฒฉ์์ ์์ฐ ๊ณก์ ์ ๋ง๋ ๋ค. | |
| positions: ์๊ฐโํฌ์ง์ (์ ์ถ๋ ์์ ๋ง). ์ฌ์ด ๊ตฌ๊ฐ์ ์ง์ ๊ฐ์ด ์ ์ง๋๋ค | |
| prices: ์๊ฐโ๊ฐ๊ฒฉ (์ฑ์ ๊ฒฉ์. ๋ณดํต 1์๊ฐ ๊ฐ๊ฒฉ) | |
| """ | |
| pos = pd.Series(positions, dtype=float).sort_index().map(clip) | |
| px = pd.Series(prices, dtype=float).sort_index() | |
| w = pos.reindex(px.index, method="ffill").fillna(0.0) | |
| r = px.pct_change().fillna(0.0) | |
| cost = w.diff().abs().fillna(w.abs()) * fee_of(asset) | |
| g = w.shift(1).fillna(0.0) * r - cost # ๐ ํฌ์ง์ ์ ๋ค์ ๊ตฌ๊ฐ์ ์ ์ฉ๋๋ค | |
| return (1 + g).cumprod(), g | |
| def total_return(positions, prices, asset=None): | |
| eq, _ = equity(positions, prices, asset) | |
| return float(eq.iloc[-1] - 1.0) | |
| def turnover(positions, prices): | |
| """ํฌ์ง์ ๋ณ๊ฒฝ ์ด๋. ๋ฌด์ฐจ๋ณ ์ ์ถ ํ์ง์ ํ๋ฉด ํ์์ ์ด๋ค.""" | |
| pos = pd.Series(positions, dtype=float).sort_index().map(clip) | |
| px = pd.Series(prices, dtype=float).sort_index() | |
| w = pos.reindex(px.index, method="ffill").fillna(0.0) | |
| return float(w.diff().abs().fillna(w.abs()).sum()) | |
| def fee_paid(positions, prices, asset=None): | |
| return turnover(positions, prices) * fee_of(asset) | |
| def percentile(ret, refv): | |
| """๊ทธ ์ข ๋ชฉ์ '์ค๋ ฅ0 ๋ถํฌ'์์ ๋ด ์์ต๋ฅ ์ ๋ฐฑ๋ถ์. | |
| refv ๋ reference.live(๊ฐ๊ฒฉ, ์์๋ฃ, ๋ ธ์ถ) ์ด ๋ง๋ ์ ๋ ฌ๋ ๋ฐฐ์ด์ด๋ค. | |
| ์์ฆ ์ค์ ๊ฒฝ๋ก๋ก ๋งค์ผ ๋ค์ ๋ง๋ ๋ค โ ๊ณผ๊ฑฐ ๋ถํธ์คํธ๋ฉ์ผ๋ก ๊ณ ์ ํ๋ฉด | |
| ์์ฆ ์ค ๊ทธ ์ข ๋ชฉ์ด ์ค๋ฅผ ๋ ์ฐธ๊ฐ์ ์ ์์ด ๋ถํ๋ ค์ง๋ค. | |
| """ | |
| return bisect.bisect_left(refv, ret) / len(refv) | |
| def score(ret, refv): | |
| """์ด์ผ๋ก ์ด ์ฑ์ ์ด ๋์ฌ ํ๋ฅ ์ ์ญ์ ์ง์. 2.0์ด๋ฉด 1/100, 3.0์ด๋ฉด 1/1,000. | |
| ๐ ์์๋ ์ข ๋ชฉ ์์์๋ง ๋งค๊ธด๋ค. ์์ต๋ฅ ๋ถํฌ์ ๊ผฌ๋ฆฌ ๋ชจ์์ด ์ข ๋ชฉ๋ง๋ค ๋ฌ๋ผ | |
| ์ข ๋ชฉ ๊ฐ ์ง์ ๋น๊ต๋ ์ด๋ ํต๊ณ๋์ ๋ง์ถ์ด๋ ํธ์ค์ด ๋จ๋๋ค. ์ฐ์น์๋ | |
| ์ต๋๊ฐ์ด๊ณ ์ต๋๊ฐ์ ๊ผฌ๋ฆฌ๊ฐ ์ ํ๊ธฐ ๋๋ฌธ์ด๋ค. ์ข ๋ชฉ๋ณ ์์์ด ๊ทธ ๋น๊ต๋ฅผ ์์ค๋ค. | |
| ์ข ๋ชฉ ์์์๋ ์ ๊ต์ ๋๋ค โ ์ค๋ ฅ 0 ์ฐธ๊ฐ์์ ๋ฐฑ๋ถ์ ์ค์์ด 0.47~0.54๋ค. | |
| """ | |
| p = min(percentile(ret, refv), 1 - 1e-6) | |
| return -math.log10(max(1.0 - p, 1e-6)) | |
| def valid_ratio(positions, prices): | |
| """ํฌ์ง์ ์ด ์ ํจํ๋ ๊ตฌ๊ฐ ๋น์จ. ์ฐธ์ฌ ์๊ฑด ํ์ ์ ์ด๋ค.""" | |
| pos = pd.Series(positions, dtype=float).sort_index() | |
| if not len(pos): | |
| return 0.0 | |
| px = pd.Series(prices, dtype=float).sort_index() | |
| w = pos.reindex(px.index, method="ffill") | |
| return float(w.notna().mean()) | |
| # โโโโโโโโโโโโโโโโโโโโโโโ ์ฑ์ ๊ธฐ ์์ฒด ๊ฒ์ฆ โโโโโโโโโโโโโโโโโโโโโโโ | |
| def selftest(verbose=True): | |
| """๋ซํ ํํ๋ก ๋ต์ ์๋ ๊ฒฝ์ฐ๋ค. ํ๋๋ผ๋ ํ๋ฆฌ๋ฉด ์ฑ์ ์ ์ ๋ขฐํ ์ ์๋ค.""" | |
| out = [] | |
| idx = pd.date_range("2026-01-01", periods=101, freq="h") | |
| def chk(name, got, want, tol=1e-9): | |
| ok = abs(got - want) <= tol | |
| out.append((name, got, want, ok)) | |
| return ok | |
| # โ ํฌ์ง์ 0์ด๋ฉด ์์ต๋ 0 โ ๊ฐ๊ฒฉ์ด ์ด๋ป๊ฒ ์์ง์ด๋ | |
| px = pd.Series(100 * np.cumprod(1 + np.random.default_rng(0).normal(0, .01, 101)), index=idx) | |
| chk("ํฌ์ง์ 0 โ ์์ต 0", total_return({idx[0]: 0.0}, px), 0.0) | |
| # โก ํฌ์ง์ +1 ยท ๋ฌด๋น์ฉ์ด๋ฉด ๋จ์๋ณด์ ์ ์ ํํ ๊ฐ์์ผ | |
| global FEE | |
| keep = FEE | |
| FEE = 0.0 | |
| chk("ํฌ์ง์ +1 = ๋จ์๋ณด์ ", total_return({idx[0]: 1.0}, px), | |
| float(px.iloc[-1] / px.iloc[0] - 1), 1e-9) | |
| # โข ํฌ์ง์ -1 ์ ๋ถํธ๊ฐ ๋ค์งํ ๋ณต๋ฆฌ (๋จ์ -x ๊ฐ ์๋๋ค) | |
| w = -1.0 | |
| want = float(np.prod(1 + w * px.pct_change().fillna(0).values[1:]) - 1) | |
| chk("ํฌ์ง์ -1 = ์ญ๋ฐฉํฅ ๋ณต๋ฆฌ", total_return({idx[0]: -1.0}, px), want, 1e-9) | |
| FEE = keep | |
| # โฃ ๋ ๋ฒ๋ฆฌ์ง ์ํ โ 2.0์ ๋ด๋ 1.0์ผ๋ก ์๋ ค์ผ | |
| FEE = 0.0 | |
| chk("ํฌ์ง์ 2.0 โ 1.0์ผ๋ก ์ ๋จ", | |
| total_return({idx[0]: 2.0}, px), total_return({idx[0]: 1.0}, px), 1e-12) | |
| FEE = keep | |
| # โค ๋ฏธ๋๋ฅผ ๋ณด์ง ์๋๊ฐ โ ๊ฐ๊ฒฉ์ด ์ค๋ฅด๊ธฐ ์ง์ ์ ํฌ์ง์ ์ ๋ฃ์ด๋ | |
| # ๊ทธ ๊ตฌ๊ฐ ์์ต์ ๋ชป ๋จน๋๋ค (ํฌ์ง์ ์ ๋ค์ ๊ตฌ๊ฐ๋ถํฐ ์ ์ฉ) | |
| p2 = pd.Series([100.0] * 50 + [200.0] * 51, index=idx) | |
| FEE = 0.0 | |
| late = total_return({idx[50]: 1.0}, p2) # ์ ํ๊ฐ ์ผ์ด๋ ๋ด์ ์ง์ | |
| early = total_return({idx[49]: 1.0}, p2) # ํ ๋ด ๋จผ์ ์ง์ | |
| ok = abs(late) < 1e-9 and early > 0.9 | |
| out.append(("๋ฏธ๋ ์ฐจ๋จ (๋ฆ๊ฒ ๋ค์ด๊ฐ๋ฉด ๋ชป ๋จน์)", late, 0.0, ok)) | |
| FEE = keep | |
| # โฅ ๋น์ฉ์ด ์ค์ ๋ก ๋น ์ง๋๊ฐ | |
| a = total_return({idx[0]: 1.0}, px) | |
| FEE = 0.01 | |
| b = total_return({idx[0]: 1.0}, px) | |
| FEE = keep | |
| out.append(("๋น์ฉ์ด ์์ต์ ๋ฎ์ถค", b, a, b < a)) | |
| # โฆ ๋ฐฑ๋ถ์ยท์ ์๊ฐ ๋จ์กฐ์ธ๊ฐ โ ์ธ๊ณต ๊ธฐ์ค๋ถํฌ๋ก ํ์ธ | |
| fake = np.sort(np.random.default_rng(1).normal(0, .3, 4000)) | |
| out.append(("์ ์ ๋จ์กฐ ์ฆ๊ฐ", score(0.6, fake), score(0.1, fake), | |
| score(0.6, fake) > score(0.1, fake))) | |
| med = float(np.median(fake)) | |
| out.append(("์ค์๊ฐ ๊ทผ์ฒ ์ ์ ~0.3", score(med, fake), 0.3, | |
| abs(score(med, fake) - 0.30) < 0.05)) | |
| if verbose: | |
| print("=" * 72) | |
| print("์ฑ์ ๊ธฐ ์์ฒด ๊ฒ์ฆ") | |
| print("=" * 72) | |
| for n, g, w, ok in out: | |
| print(" %-32s %14.8f %s" % (n, g, "โ " if ok else "๐ด ์คํจ (๊ธฐ๋ %.8f)" % w)) | |
| bad = [x for x in out if not x[3]] | |
| print("\n %d/%d ํต๊ณผ%s" % (len(out) - len(bad), len(out), | |
| "" if not bad else " ๐ด ์คํจ %d๊ฑด โ ์ฑ์ ์ค๋จ" % len(bad))) | |
| return all(x[3] for x in out) | |
| if __name__ == "__main__": | |
| ok = selftest() | |
| raise SystemExit(0 if ok else 1) | |