finchal / app /ledger.py
SeaWolf-AI's picture
intraday time axis; baselines seeded at season open
83d14d7 verified
Raw
History Blame Contribute Delete
11 kB
# -*- coding: utf-8 -*-
"""์›์žฅ โ€” ์ฐธ๊ฐ€์žยทํฌ์ง€์…˜ยท์ ์ˆ˜๋ฅผ ๋‹ด๋Š”๋‹ค.
ํŒŒ์ผ ๊ธฐ๋ฐ˜์ด๋‹ค. HF Space ์žฌ์‹œ์ž‘ ์‹œ ์‚ฌ๋ผ์ง€๋ฏ€๋กœ ์šด์˜์—์„œ๋Š” ์™ธ๋ถ€ ์ €์žฅ์†Œ๋กœ ์˜ฎ๊ธด๋‹ค.
์ง€๊ธˆ์€ ๊ตฌ์กฐ๋ฅผ ํ™•์ •ํ•˜๋Š” ๊ฒŒ ๋ชฉ์ ์ด๊ณ , ์ €์žฅ ์œ„์น˜๋งŒ ๋ฐ”๊พธ๋ฉด ๋˜๋„๋ก ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ข๊ฒŒ ๋‘”๋‹ค.
๐Ÿ”ด ํฌ์ง€์…˜์€ ์ ˆ๋Œ€ ๋ฎ์–ด์“ฐ์ง€ ์•Š๋Š”๋‹ค. ๋งค ์ œ์ถœ์„ append ํ•œ๋‹ค.
๋ฎ์–ด์“ฐ๋ฉด "์–ธ์ œ ๋ฌด์—‡์„ ๋ƒˆ๋Š”๊ฐ€"๊ฐ€ ์‚ฌ๋ผ์ง€๊ณ , ๊ทธ๊ฒŒ ์‚ฌ๋ผ์ง€๋ฉด ์ฑ„์ ์„ ์žฌํ˜„ํ•  ์ˆ˜ ์—†๋‹ค.
"""
import os, json, time, hashlib, threading
from datetime import datetime, timezone
def _root():
"""์›์žฅ ์œ„์น˜. ์˜์† ์Šคํ† ๋ฆฌ์ง€๊ฐ€ ์žˆ์œผ๋ฉด ๊ฑฐ๊ธฐ, ์—†์œผ๋ฉด ์ปจํ…Œ์ด๋„ˆ ์•ˆ.
๐Ÿ”ด HF Space์— ์˜์† ์Šคํ† ๋ฆฌ์ง€๋ฅผ ์•ˆ ๋ถ™์ด๋ฉด /data ๊ฐ€ ์—†๋‹ค. ๊ทธ๋•Œ ์ฃฝ์–ด ๋ฒ„๋ฆฌ๋ฉด
ํ™”๋ฉด ์ž์ฒด๊ฐ€ ์•ˆ ๋œฌ๋‹ค. ๋–  ์žˆ๋˜ '์žฌ์‹œ์ž‘ํ•˜๋ฉด ์‚ฌ๋ผ์ง„๋‹ค'๋Š” ์‚ฌ์‹ค์ด ๋ณด์ด๊ฒŒ ํ•œ๋‹ค.
์›์žฅ์ด ํœ˜๋ฐœ๋˜๋Š” ๊ฒƒ๊ณผ ์„œ๋น„์Šค๊ฐ€ ์•ˆ ๋œจ๋Š” ๊ฒƒ์€ ๋‘˜ ๋‹ค ๋‚˜์˜์ง€๋งŒ, ํ›„์ž๋Š”
๋ฌด์—‡์ด ๋ฌธ์ œ์ธ์ง€์กฐ์ฐจ ์•Œ ์ˆ˜ ์—†๋‹ค.
"""
want = os.environ.get("FINCHAL_DATA")
if want:
try:
os.makedirs(want, exist_ok=True)
t = os.path.join(want, ".w")
open(t, "w").close(); os.remove(t)
return want
except Exception:
pass
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "_data")
ROOT = _root()
LOCK = threading.Lock()
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ์›์žฅ ๋ฏธ๋Ÿฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# ๐Ÿ”ด HF Space์˜ /data ๋Š” ์˜์† ์Šคํ† ๋ฆฌ์ง€๋ฅผ ๋ถ™์ด์ง€ ์•Š์œผ๋ฉด ์žฌ์‹œ์ž‘์— ์‚ฌ๋ผ์ง„๋‹ค.
# ์‹ค์ธกํ–ˆ๋‹ค โ€” ์žฌ์‹œ์ž‘ ํ›„ ๋ถ€ํŒ… ํ‘œ์‹์ด ์—†์–ด์กŒ๋‹ค(boots 1 -> 1).
# ์œ ๋ฃŒ ์Šคํ† ๋ฆฌ์ง€ ๋Œ€์‹  ๋น„๊ณต๊ฐœ ๋ฐ์ดํ„ฐ์…‹ ์ €์žฅ์†Œ์— ์›์žฅ์„ ๋ฏธ๋Ÿฌ๋งํ•œ๋‹ค.
# ์‹œ์ฆŒ 4๊ฐœ์›”์น˜ ๊ธฐ๋ก์ด ์ปจํ…Œ์ด๋„ˆ ์žฌ์‹œ์ž‘ ํ•œ ๋ฒˆ์— ๋‚ ์•„๊ฐ€๋ฉด ๋Œ€ํšŒ๊ฐ€ ๋๋‚œ๋‹ค.
MIRROR = os.environ.get("FINCHAL_MIRROR") # ์˜ˆ: FINAL-Bench/finchal-ledger
_MIR = {"ok": False, "dirty": False, "at": 0.0, "n": 0, "err": None}
def _api():
from huggingface_hub import HfApi
tok = os.environ.get("HF_TOKEN") or os.environ.get("FINCHAL_HF_TOKEN")
return HfApi(token=tok) if tok else None
def restore():
"""๋ถ€ํŒ… ์‹œ ๋ฏธ๋Ÿฌ์—์„œ ์›์žฅ์„ ๋˜์‚ด๋ฆฐ๋‹ค. ์ด๊ฒŒ ์‹คํŒจํ•˜๋ฉด ๋ฏธ๋Ÿฌ์— ์“ฐ์ง€ ์•Š๋Š”๋‹ค.
๐Ÿ”‘ ๋ณต์› ์‹คํŒจ ํ›„ flush ํ•˜๋ฉด ๋นˆ ์›์žฅ์ด ๋ฏธ๋Ÿฌ๋ฅผ ๋ฎ์–ด ์‹œ์ฆŒ์ด ํ†ต์งธ๋กœ ์‚ฌ๋ผ์ง„๋‹ค.
ํ•œ ๋ฒˆ์˜ ์ž˜๋ชป๋œ ๋ฎ์–ด์“ฐ๊ธฐ๊ฐ€ 4๊ฐœ์›”์„ ์ง€์šด๋‹ค.
"""
if not MIRROR:
return "๋ฏธ๋Ÿฌ ์—†์Œ"
try:
from huggingface_hub import snapshot_download
a = _api()
if a is None:
_MIR["err"] = "HF_TOKEN ์—†์Œ"
return _MIR["err"]
try:
a.repo_info(MIRROR, repo_type="dataset")
except Exception:
a.create_repo(MIRROR, repo_type="dataset", private=True, exist_ok=True)
# ๐Ÿ”ด ๊ธฐ๋ณธ ์บ์‹œ๋กœ ๋ฐ›์œผ๋ฉด ๊ฒฝ๋กœ์— .cache ๊ฐ€ ๋“ค์–ด๊ฐ„๋‹ค. ๊ทธ๊ฑธ ๊ฑธ๋Ÿฌ๋‚ด๋Š”
# ํ•„ํ„ฐ๋ฅผ ๋‘๋ฉด **๋ฐ›์€ ํŒŒ์ผ์ด ์ „๋ถ€ ๊ฑธ๋Ÿฌ์ง„๋‹ค** โ€” ์‹ค์ œ๋กœ ๊ทธ๋ ‡๊ฒŒ ๋˜์–ด
# "๋ณต์› 0๊ฐœ"๊ฐ€ ๋‚˜์™”๋‹ค. ๋ชฉ์ ์ง€๋ฅผ ์šฐ๋ฆฌ๊ฐ€ ์ •ํ•ด ๊ทธ ํ•จ์ •์„ ์—†์•ค๋‹ค.
tmp = os.path.join(ROOT, ".restore")
d = snapshot_download(MIRROR, repo_type="dataset", token=a.token,
local_dir=tmp)
import shutil, glob
os.makedirs(os.path.join(ROOT, "positions"), exist_ok=True)
n = 0
for src in glob.glob(os.path.join(d, "**", "*"), recursive=True):
if not os.path.isfile(src):
continue
rel = os.path.relpath(src, d)
if rel.startswith(".git") or rel.startswith(".huggingface"):
continue
dst = os.path.join(ROOT, rel)
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.copyfile(src, dst)
n += 1
shutil.rmtree(tmp, ignore_errors=True)
_MIR["ok"] = True
_MIR["n"] = n
return "๋ณต์› %d๊ฐœ" % n
except Exception as e:
_MIR["err"] = "%s: %s" % (type(e).__name__, e)
return _MIR["err"]
def flush(force=False):
"""์›์žฅ์„ ๋ฏธ๋Ÿฌ์— ์˜ฌ๋ฆฐ๋‹ค. ์ œ์ถœ๋งˆ๋‹ค ์˜ฌ๋ฆฌ๋ฉด ์ปค๋ฐ‹์ด ํญ์ฃผํ•˜๋ฏ€๋กœ ๋ชจ์•„์„œ ๋ณด๋‚ธ๋‹ค."""
if not (MIRROR and _MIR["ok"]) or (not _MIR["dirty"] and not force):
return False
try:
a = _api()
a.upload_folder(repo_id=MIRROR, repo_type="dataset", folder_path=ROOT,
commit_message="ledger sync",
ignore_patterns=["*.tmp", ".cache/*", ".restore/*",
".huggingface/*"])
_MIR["dirty"] = False
_MIR["at"] = time.time()
return True
except Exception as e:
_MIR["err"] = "%s: %s" % (type(e).__name__, e)
return False
def mirror_status():
return {"repo": MIRROR, "restored": _MIR["ok"], "files": _MIR["n"],
"pending": _MIR["dirty"], "error": _MIR["err"]}
def boot_check():
"""์›์žฅ์ด ์žฌ์‹œ์ž‘์„ ๊ฒฌ๋””๋Š”์ง€ **์‹ค์ธก**ํ•œ๋‹ค.
๐Ÿ”ด '์“ฐ๊ธฐ๊ฐ€ ๋˜๋Š”๊ฐ€'๋กœ ํŒ์ •ํ•˜๋ฉด ์•ˆ ๋œ๋‹ค. ์˜์† ์Šคํ† ๋ฆฌ์ง€๊ฐ€ ์—†์–ด๋„ /data ๋Š”
๋งŒ๋“ค์–ด์ง€๊ณ  ์จ์ง„๋‹ค โ€” ์žฌ์‹œ์ž‘ํ•  ๋•Œ ์‚ฌ๋ผ์งˆ ๋ฟ์ด๋‹ค. ๊ทธ๋ž˜์„œ ์“ฐ๊ธฐ ๊ฒ€์‚ฌ๋Š”
ํ•ญ์ƒ ํ†ต๊ณผํ•˜๋Š” ๊ฐ€์งœ ์‹ ํ˜ธ๋‹ค. ์‹ค์ œ๋กœ ํ™•์ธํ•˜๋ ค๋ฉด ์ด์ „ ๋ถ€ํŒ…์˜ ํ”์ ์ด
๋‚จ์•„ ์žˆ๋Š”์ง€๋ฅผ ๋ด์•ผ ํ•œ๋‹ค.
๋ฐ˜ํ™˜: (ํŒ์ •, ๋ถ€ํŒ…ํšŸ์ˆ˜)
"ํ™•์ธ๋จ" ์ด์ „ ๋ถ€ํŒ… ๊ธฐ๋ก์ด ์‚ด์•„๋‚จ์•˜๋‹ค = ์˜์†
"๋ฏธํ™•์ธ" ์ฒซ ๋ถ€ํŒ…์ด๋ผ ์•„์ง ์•Œ ์ˆ˜ ์—†๋‹ค
"ํœ˜๋ฐœ๋จ" ๊ธฐ๋ก์„ ๋‚จ๊ฒผ๋Š”๋ฐ ์‚ฌ๋ผ์กŒ๋‹ค = ์žฌ์‹œ์ž‘๋งˆ๋‹ค ์›์žฅ์ด ๋‚ ์•„๊ฐ„๋‹ค
"""
f = os.path.join(ROOT, "_boot.json")
prev = _read(f, None)
n = (prev or {}).get("n", 0) + 1
if prev is None:
# ์ปจํ…Œ์ด๋„ˆ๋Š” ์‹œ์ž‘๋๋Š”๋ฐ ํ”์ ์ด ์—†๋‹ค. ์ฒซ ๋ถ€ํŒ…์ธ์ง€ ๋‚ ์•„๊ฐ„ ๊ฑด์ง€๋Š”
# ์ด ์‹œ์ ์— ๊ตฌ๋ถ„๋˜์ง€ ์•Š๋Š”๋‹ค. ํŒ์ •์€ ๋‹ค์Œ ๋ถ€ํŒ…์œผ๋กœ ๋ฏธ๋ฃฌ๋‹ค.
verdict = "๋ฏธํ™•์ธ"
else:
verdict = "ํ™•์ธ๋จ"
try:
_write(f, {"n": n, "at": now().isoformat(), "root": ROOT})
except Exception:
pass
return verdict, n
def _p(*a):
p = os.path.join(ROOT, *a)
os.makedirs(os.path.dirname(p), exist_ok=True)
return p
def now():
return datetime.now(timezone.utc).replace(microsecond=0)
def _read(path, default):
try:
with open(path, encoding="utf-8") as f:
return json.load(f)
except Exception:
return default
def _write(path, obj):
tmp = path + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(obj, f, ensure_ascii=False)
os.replace(tmp, path) # ์›์ž์  ๊ต์ฒด โ€” ์ค‘๊ฐ„์— ์ฃฝ์–ด๋„ ๋ฐ˜์ชฝ ํŒŒ์ผ์ด ์•ˆ ๋‚จ๋Š”๋‹ค
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ์ฐธ๊ฐ€์ž โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def make_key(user_id):
raw = "%s|%s|finchal" % (user_id, os.environ.get("FINCHAL_SALT", "s1"))
return "fc_" + hashlib.sha256(raw.encode()).hexdigest()[:24]
def register(user_id, strategy, ptype, assets):
"""์ฐธ๊ฐ€ ๋“ฑ๋ก. ์ด๋ฏธ ์žˆ์œผ๋ฉด ์ „๋žต๋ช…ยท์œ ํ˜•๋งŒ ๊ฐฑ์‹ ํ•˜๊ณ  ํ‚ค๋Š” ์œ ์ง€ํ•œ๋‹ค."""
with LOCK:
u = _read(_p("users.json"), {})
key = make_key(user_id)
prev = u.get(user_id, {})
u[user_id] = {
"user": user_id, "key": key, "strategy": strategy, "type": ptype,
"assets": sorted(set(assets)),
"joined": prev.get("joined", now().isoformat()),
"visits": prev.get("visits", []),
}
_write(_p("users.json"), u)
_MIR["dirty"] = True
return u[user_id]
def user_by_key(key):
for v in _read(_p("users.json"), {}).values():
if v.get("key") == key:
return v
return None
def touch(user_id):
"""์ ‘์† ๊ธฐ๋ก โ€” ์‚ฌ๋žŒ ๋ถ€๋ฌธ์˜ '์ฃผ 1ํšŒ ์ ‘์†' ์š”๊ฑด ํŒ์ •์— ์“ด๋‹ค."""
with LOCK:
u = _read(_p("users.json"), {})
if user_id in u:
v = u[user_id].setdefault("visits", [])
d = now().date().isoformat()
if not v or v[-1] != d:
v.append(d)
v[:] = v[-400:]
_write(_p("users.json"), u)
def all_users():
return list(_read(_p("users.json"), {}).values())
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ํฌ์ง€์…˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def submit(user_id, asset, position, source="api"):
"""ํฌ์ง€์…˜ ์ œ์ถœ. ๋ฎ์–ด์“ฐ์ง€ ์•Š๊ณ  ์ค„ ๋‹จ์œ„๋กœ ์Œ“๋Š”๋‹ค."""
pos = max(-1.0, min(1.0, float(position)))
rec = {"t": now().isoformat(), "u": user_id, "a": asset,
"p": pos, "src": source}
with LOCK:
with open(_p("positions", "%s.jsonl" % asset), "a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
_MIR["dirty"] = True
touch(user_id)
return rec
def seed(user_id, asset, position, at, source="seed"):
"""์ง€์ •ํ•œ ์‹œ๊ฐ์œผ๋กœ ํฌ์ง€์…˜์„ ๊ธฐ๋กํ•œ๋‹ค. ๊ธฐ์ค€์„ ์„ ์‹œ์ฆŒ ๊ฐœ๋ง‰ ์‹œ์ ์— ์‹ฌ์„ ๋•Œ๋งŒ ์“ด๋‹ค.
๐Ÿ”ด ์ฐธ๊ฐ€์ž์—๊ฒŒ๋Š” ์ ˆ๋Œ€ ์—ด์–ด ์ฃผ์ง€ ์•Š๋Š”๋‹ค. ๊ณผ๊ฑฐ ์‹œ๊ฐ์œผ๋กœ ํฌ์ง€์…˜์„ ๋„ฃ์„ ์ˆ˜ ์žˆ์œผ๋ฉด
๊ฒฐ๊ณผ๋ฅผ ๋ณด๊ณ  ๋‚˜์„œ ์œ ๋ฆฌํ•œ ์‹œ์ ์— ์ง„์ž…ํ•œ ๊ฒƒ์œผ๋กœ ๊พธ๋ฐ€ ์ˆ˜ ์žˆ๋‹ค.
"""
pos = max(-1.0, min(1.0, float(position)))
rec = {"t": str(at), "u": user_id, "a": asset, "p": pos, "src": source}
with LOCK:
with open(_p("positions", "%s.jsonl" % asset), "a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
_MIR["dirty"] = True
return rec
def positions(asset, user_id=None):
out = []
p = _p("positions", "%s.jsonl" % asset)
if not os.path.exists(p):
return out
with open(p, encoding="utf-8") as f:
for line in f:
try:
r = json.loads(line)
except Exception:
continue
if user_id is None or r.get("u") == user_id:
out.append(r)
return out
def latest(asset, user_id):
ps = positions(asset, user_id)
return ps[-1] if ps else None
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ์ ์ˆ˜ ์Šค๋ƒ…์ƒท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
def save_scores(asset, rows):
_write(_p("scores", "%s.json" % asset),
{"at": now().isoformat(), "rows": rows})
def load_scores(asset):
return _read(_p("scores", "%s.json" % asset), {"at": None, "rows": []})