from __future__ import annotations from datetime import datetime from enum import Enum from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Integer, String, func from sqlalchemy import Enum as SAEnum from sqlalchemy.orm import Mapped, mapped_column, relationship from .base import Base class MatchStatus(str, Enum): PRE_MATCH = 'pre-match' IN_PLAY = 'in-play' FINISHED = 'finished' class Venue(Base): """球場主資料:海拔與時區是 2026 世界盃關鍵參數。""" __tablename__ = 'venues' id: Mapped[str] = mapped_column(String(36), primary_key=True) name: Mapped[str] = mapped_column(String(200), nullable=False) city: Mapped[str] = mapped_column(String(120), nullable=False) country: Mapped[str] = mapped_column(String(120), nullable=False) altitude_meters: Mapped[int | None] = mapped_column(Integer, nullable=True) timezone: Mapped[str] = mapped_column(String(80), nullable=False) matches: Mapped[list[Match]] = relationship('Match', back_populates='venue', lazy='raise') class Team(Base): """球隊主表,保留排名與 Elo 給量化模型做能力修正。""" __tablename__ = 'teams' id: Mapped[str] = mapped_column(String(36), primary_key=True) name: Mapped[str] = mapped_column(String(140), nullable=False, unique=True) fifa_rank: Mapped[int | None] = mapped_column(Integer, nullable=True) current_elo_rating: Mapped[float | None] = mapped_column(Float, nullable=True) group_name: Mapped[str | None] = mapped_column(String(10), nullable=True) home_matches: Mapped[list[Match]] = relationship( 'Match', foreign_keys='Match.home_team_id', back_populates='home_team', ) away_matches: Mapped[list[Match]] = relationship( 'Match', foreign_keys='Match.away_team_id', back_populates='away_team', ) class Bookmaker(Base): """莊家主檔。""" __tablename__ = 'bookmakers' id: Mapped[str] = mapped_column(String(36), primary_key=True) name: Mapped[str] = mapped_column(String(120), nullable=False, unique=True) odds_rows: Mapped[list[OddsHistory]] = relationship('OddsHistory', back_populates='bookmaker') class Match(Base): """賽事基本結構,儲存 UTC 時間、場地與賽前 xG。""" __tablename__ = 'matches' id: Mapped[str] = mapped_column(String(64), primary_key=True) home_team_id: Mapped[str] = mapped_column(ForeignKey('teams.id'), nullable=False) away_team_id: Mapped[str] = mapped_column(ForeignKey('teams.id'), nullable=False) venue_id: Mapped[str] = mapped_column(ForeignKey('venues.id'), nullable=False) match_time_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) status: Mapped[MatchStatus] = mapped_column( SAEnum(MatchStatus, name='match_status', native_enum=False), default=MatchStatus.PRE_MATCH, ) home_xg: Mapped[float | None] = mapped_column(Float, nullable=True) away_xg: Mapped[float | None] = mapped_column(Float, nullable=True) home_team: Mapped[Team] = relationship('Team', foreign_keys=[home_team_id], back_populates='home_matches') away_team: Mapped[Team] = relationship('Team', foreign_keys=[away_team_id], back_populates='away_matches') venue: Mapped[Venue] = relationship('Venue', back_populates='matches') odds_history: Mapped[list[OddsHistory]] = relationship('OddsHistory', back_populates='match') recommendations: Mapped[list['ValueBetRecommendation']] = relationship( 'ValueBetRecommendation', back_populates='match', cascade='all, delete-orphan', ) class OddsHistory(Base): """時間序列賠率表(待轉為 TimescaleDB Hypertable)。""" __tablename__ = 'odds_history' id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) match_id: Mapped[str] = mapped_column(ForeignKey('matches.id'), nullable=False, index=True) bookmaker_id: Mapped[str] = mapped_column(ForeignKey('bookmakers.id'), nullable=False, index=True) market_type: Mapped[str] = mapped_column(String(30), nullable=False) selection: Mapped[str] = mapped_column(String(30), nullable=False) decimal_odds: Mapped[float] = mapped_column(Float, nullable=False) implied_probability: Mapped[float] = mapped_column(Float, nullable=False) recorded_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True) match: Mapped[Match] = relationship('Match', back_populates='odds_history') bookmaker: Mapped[Bookmaker] = relationship('Bookmaker', back_populates='odds_rows') class SmartMoneyFlow(Base): """聰明錢流向快照表。""" __tablename__ = 'smart_money_flow' id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) match_id: Mapped[str] = mapped_column(ForeignKey('matches.id'), nullable=False, index=True) market_type: Mapped[str] = mapped_column(String(30), nullable=False) selection: Mapped[str] = mapped_column(String(30), nullable=False) ticket_pct: Mapped[float] = mapped_column(Float, nullable=False) handle_pct: Mapped[float] = mapped_column(Float, nullable=False) sharp_indicator: Mapped[bool] = mapped_column(Boolean, nullable=False) recorded_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True) class ValueBetRecommendation(Base): """可驗證獲利帳本紀錄(公開透明)。""" __tablename__ = 'value_bet_recommendations' id: Mapped[str] = mapped_column(String(64), primary_key=True) match_id: Mapped[str] = mapped_column(ForeignKey('matches.id'), nullable=False, index=True) market_type: Mapped[str] = mapped_column(String(30), nullable=False) selection: Mapped[str] = mapped_column(String(30), nullable=False) stake: Mapped[float] = mapped_column(Float, nullable=False) recommended_odds: Mapped[float] = mapped_column(Float, nullable=False) closing_odds: Mapped[float] = mapped_column(Float, nullable=True) is_win: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) settled_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) clv_ratio: Mapped[float] = mapped_column(Float, nullable=True) pnl: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) note: Mapped[str | None] = mapped_column(String(240), nullable=True) match: Mapped[Match] = relationship('Match', back_populates='recommendations') class AffiliateBookmaker(Base): """聯盟行銷博彩公司追蹤碼設定。""" __tablename__ = 'affiliate_bookmakers' id: Mapped[str] = mapped_column(String(36), primary_key=True) name: Mapped[str] = mapped_column(String(120), nullable=False, unique=True) tracking_url: Mapped[str] = mapped_column(String(512), nullable=False) commission_rate: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) class AffiliateClick(Base): """聯盟行銷跳轉點擊紀錄(防廣告攔截)。""" __tablename__ = 'affiliate_clicks' id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) bookmaker_id: Mapped[str] = mapped_column(ForeignKey('affiliate_bookmakers.id'), nullable=False, index=True) user_ip_hash: Mapped[str] = mapped_column(String(128), nullable=False) user_agent: Mapped[str | None] = mapped_column(String(512), nullable=True) referrer: Mapped[str | None] = mapped_column(String(512), nullable=True) timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=func.now(), index=True) converted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) class UserProfile(Base): """量化大神排行榜與跟單系統的用戶資料。""" __tablename__ = 'user_profiles' id: Mapped[str] = mapped_column(String(36), primary_key=True) username: Mapped[str] = mapped_column(String(120), nullable=False, unique=True) clv_score: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) roi_30d: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) sharp_rating: Mapped[int] = mapped_column(Integer, nullable=False, default=0) class CopyBet(Base): """一鍵跟單交易紀錄。""" __tablename__ = 'copy_bets' id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) follower_id: Mapped[str] = mapped_column(ForeignKey('user_profiles.id'), nullable=False, index=True) leader_id: Mapped[str] = mapped_column(ForeignKey('user_profiles.id'), nullable=False, index=True) recommendation_id: Mapped[str] = mapped_column(ForeignKey('value_bet_recommendations.id'), nullable=False) follower_stake: Mapped[float] = mapped_column(Float, nullable=False) copied_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=func.now())