Initial commit with 2026 World Cup Quant Platform core modules and CI/CD
This commit is contained in:
199
platform/backend/app/db/models.py
Normal file
199
platform/backend/app/db/models.py
Normal file
@@ -0,0 +1,199 @@
|
||||
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())
|
||||
Reference in New Issue
Block a user