finchal / scoring.py
SeaWolf-AI's picture
show received entries before scoring begins
cab04ab verified
Raw
History Blame Contribute Delete
8.3 kB
# -*- 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)