Initial commit with 2026 World Cup Quant Platform core modules and CI/CD

This commit is contained in:
QuantBot
2026-06-13 23:18:18 +08:00
commit 073abf98c1
155 changed files with 19539 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
"""資料模型套件。"""
from .base import Base, get_engine, get_session_factory, SessionFactory
from .models import Bookmaker, Match, MatchStatus, OddsHistory, SmartMoneyFlow, Team, Venue
__all__ = [
'Base',
'Bookmaker',
'Match',
'MatchStatus',
'OddsHistory',
'SmartMoneyFlow',
'Team',
'Venue',
'get_engine',
'get_session_factory',
'SessionFactory',
]

View File

@@ -0,0 +1,26 @@
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase
import os
class Base(DeclarativeBase):
"""Project ORM base model."""
DATABASE_URL = os.getenv('DATABASE_URL', 'postgresql+asyncpg://fifa_user:change_me@fifa2026-postgres:5432/fifa2026')
def get_engine(database_url: str = DATABASE_URL):
"""Create asynchronous SQLAlchemy engine for production use."""
return create_async_engine(database_url, echo=False, pool_pre_ping=True)
def get_session_factory(database_url: str = DATABASE_URL):
"""Create session factory for async query operations."""
engine = get_engine(database_url)
return async_sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
SessionFactory = get_session_factory()

View 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())