Files
2026FIFAWorldCup/platform/backend/app/db/models.py
QuantBot aa7e3bba76
Some checks failed
2026 World Cup Quant Platform - Production Deployment / Code Quality & Testing (push) Failing after 1m49s
2026 World Cup Quant Platform - Production Deployment / Deploy to Production VM via Rsync (push) Has been skipped
chore: migrate deployment to Gitea Actions with zero-trust rsync
2026-06-16 19:06:50 +08:00

223 lines
10 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.
from __future__ import annotations
from datetime import datetime
from enum import Enum
from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Integer, JSON, 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_score: Mapped[int | None] = mapped_column(Integer, nullable=True)
away_score: Mapped[int | None] = mapped_column(Integer, nullable=True)
result_synced_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), 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)
market_line: Mapped[float | None] = mapped_column(Float, nullable=True)
handicap: Mapped[float | None] = mapped_column(Float, nullable=True)
decimal_odds: Mapped[float] = mapped_column(Float, nullable=False)
implied_probability: Mapped[float] = mapped_column(Float, nullable=False)
source_market_key: Mapped[str | None] = mapped_column(String(80), nullable=True)
source_outcome_name: Mapped[str | None] = mapped_column(String(140), nullable=True)
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 DailyRecommendationSnapshot(Base):
"""每日作戰室賽前推薦快照,用於賽後真實稽核。"""
__tablename__ = 'daily_recommendation_snapshots'
id: Mapped[str] = mapped_column(String(64), primary_key=True)
target_date: Mapped[str] = mapped_column(String(10), nullable=False, index=True)
generated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
items_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
live_market_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
pre_market_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
payload: Mapped[dict] = mapped_column(JSON, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=func.now(), onupdate=func.now())
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())