"""公開獲利帳本(Proof of Yield)模組。""" from __future__ import annotations from dataclasses import dataclass import json from pathlib import Path from typing import Any from uuid import uuid4 from datetime import datetime def _as_float(value: Any, *, default: float = 0.0) -> float: try: return float(value) except (TypeError, ValueError): return default @dataclass(frozen=True) class ProofYieldRecord: recommendation_id: str match_id: str market_type: str selection: str stake: float recommended_odds: float closing_odds: float | None is_win: bool settled_at: str clv_ratio: float | None clv_percent: float | None pnl: float created_at: str def compute_clv(recommended_odds: float, closing_odds: float) -> float: """CLV = (推薦賠率 / 收盤賠率) - 1。""" if recommended_odds <= 0 or closing_odds <= 0: raise ValueError('推薦賠率與收盤賠率都必須大於 0') return (recommended_odds / closing_odds) - 1 def compute_pnl(stake: float, is_win: bool, closing_odds: float | None) -> float: if closing_odds is None or stake <= 0: return 0.0 return stake * (closing_odds - 1) if is_win else -stake @dataclass(frozen=True) class LedgerSummary: total_recommendations: int hit_count: int win_rate_percent: float total_stake: float total_pnl: float roi_percent: float avg_clv_percent: float class ProofOfYieldStore: """本地持久化透明帳本(先以 JSON 做可追溯快啟動)。""" def __init__(self, file_path: str | None = None) -> None: self.path = Path(file_path or 'data/proof_of_yield_ledger.json') self.path.parent.mkdir(parents=True, exist_ok=True) def _load(self) -> list[dict[str, Any]]: if not self.path.exists(): return [] raw = self.path.read_text(encoding='utf-8') if not raw.strip(): return [] parsed = json.loads(raw) if not isinstance(parsed, list): return [] return parsed def _save(self, rows: list[dict[str, Any]]) -> None: self.path.write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding='utf-8') def upsert_settlements(self, items: list[dict[str, Any]]) -> list[ProofYieldRecord]: current = self._load() idx = {row['recommendation_id']: i for i, row in enumerate(current)} for item in items: recommendation_id = str(item.get('recommendation_id') or uuid4().hex) stake = _as_float(item.get('stake'), default=100.0) recommended_odds = _as_float(item.get('recommended_odds')) closing_odds = item.get('closing_odds') is_win = bool(item.get('is_win', False)) closing = _as_float(closing_odds) if closing_odds is not None else None clv = None clv_pct = None if closing is not None and recommended_odds > 0: clv = compute_clv(recommended_odds, closing) clv_pct = clv * 100 pnl = compute_pnl(stake, is_win, closing) record = { 'recommendation_id': recommendation_id, 'match_id': str(item.get('match_id', 'UNKNOWN')), 'market_type': str(item.get('market_type', '1x2')), 'selection': str(item.get('selection', 'home')), 'stake': round(stake, 4), 'recommended_odds': round(recommended_odds, 6), 'closing_odds': round(closing, 6) if closing is not None else None, 'is_win': is_win, 'settled_at': str(item.get('settled_at') or datetime.utcnow().isoformat()), 'clv_ratio': round(clv, 6) if clv is not None else None, 'clv_percent': round(clv_pct, 4) if clv_pct is not None else None, 'pnl': round(pnl, 4), 'created_at': str(item.get('created_at') or datetime.utcnow().isoformat()), } if recommendation_id in idx: current[idx[recommendation_id]] = record else: current.append(record) self._save(current) return [ProofYieldRecord(**row) for row in current] def query_ledger(self, *, limit: int = 200) -> list[ProofYieldRecord]: rows = sorted(self._load(), key=lambda row: row.get('created_at', ''), reverse=True) return [ProofYieldRecord(**row) for row in rows[:limit]] @staticmethod def summarize(records: list[ProofYieldRecord]) -> LedgerSummary: total = len(records) if total == 0: return LedgerSummary( total_recommendations=0, hit_count=0, win_rate_percent=0.0, total_stake=0.0, total_pnl=0.0, roi_percent=0.0, avg_clv_percent=0.0, ) hit = sum(1 for row in records if row.is_win) total_stake = sum(row.stake for row in records) total_pnl = sum(row.pnl for row in records) clv_values = [row.clv_percent for row in records if row.clv_percent is not None] avg_clv = sum(clv_values) / len(clv_values) if clv_values else 0.0 roi = (total_pnl / total_stake) * 100 if total_stake > 0 else 0.0 win_rate = (hit / total) * 100 return LedgerSummary( total_recommendations=total, hit_count=hit, win_rate_percent=round(win_rate, 4), total_stake=round(total_stake, 4), total_pnl=round(total_pnl, 4), roi_percent=round(roi, 4), avg_clv_percent=round(avg_clv, 4), )