Files
2026FIFAWorldCup/platform/backend/app/analytics/proof_of_yield.py

163 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""公開獲利帳本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),
)