200 lines
8.8 KiB
Python
200 lines
8.8 KiB
Python
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())
|