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,181 @@
"""自訂策略回測引擎。"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
@dataclass(frozen=True)
class BacktestTradeRecord:
"""單筆策略投注歷史資料。"""
trade_id: str
settled_at: datetime
odds: float
is_win: bool
stake: float = 100.0
altitude_meters: int | None = None
handicap: float | None = None
weather: str | None = None
recent_form_win_rate: float | None = None
market_type: str = '1x2'
selection: str = 'home'
@dataclass(frozen=True)
class StrategyFilter:
"""回測條件(前端 JSON 可直接對映)。"""
weather: str | None = None
altitude_min_meters: int | None = None
altitude_max_meters: int | None = None
handicap_min: float | None = None
handicap_max: float | None = None
recent_win_rate_min: float | None = None
recent_win_rate_max: float | None = None
market_types: list[str] | None = None
start_at: datetime | None = None
end_at: datetime | None = None
def _match_filter(record: BacktestTradeRecord, condition: StrategyFilter) -> bool:
"""判斷單筆交易是否符合使用者條件。"""
if condition.weather and (record.weather or '').lower() != condition.weather.lower():
return False
if condition.altitude_min_meters is not None and (
record.altitude_meters is None or record.altitude_meters < condition.altitude_min_meters
):
return False
if condition.altitude_max_meters is not None and (
record.altitude_meters is None or record.altitude_meters > condition.altitude_max_meters
):
return False
if condition.handicap_min is not None and (
record.handicap is None or record.handicap < condition.handicap_min
):
return False
if condition.handicap_max is not None and (
record.handicap is None or record.handicap > condition.handicap_max
):
return False
if condition.recent_win_rate_min is not None and (
record.recent_form_win_rate is None or record.recent_form_win_rate < condition.recent_win_rate_min
):
return False
if condition.recent_win_rate_max is not None and (
record.recent_form_win_rate is None or record.recent_form_win_rate > condition.recent_win_rate_max
):
return False
if condition.market_types and record.market_type not in condition.market_types:
return False
if condition.start_at is not None and record.settled_at < condition.start_at:
return False
if condition.end_at is not None and record.settled_at > condition.end_at:
return False
return True
def filter_trades(
trades: list[BacktestTradeRecord],
condition: StrategyFilter,
) -> list[BacktestTradeRecord]:
"""回傳符合條件的策略明細子集合。"""
return [t for t in trades if _match_filter(t, condition)]
def compute_max_drawdown(equity_curve: list[float]) -> float:
"""計算最大回撤(百分比)。"""
if not equity_curve:
return 0.0
peak = equity_curve[0]
max_drawdown = 0.0
for value in equity_curve:
if value > peak:
peak = value
continue
drawdown = (peak - value) / peak if peak else 0.0
max_drawdown = max(max_drawdown, drawdown)
return round(max_drawdown * 100, 4)
def run_flat_stake_backtest(
trades: list[BacktestTradeRecord],
initial_capital: float = 10000,
) -> dict[str, float | int | list[dict[str, float | str]]]:
"""固定單注本金Flat betting回測。
回傳:
- trade_count總注單數
- hit_count中獎注數
- win_rate中獎率
- final_capital最終資金
- net_profit淨利潤
- roi_percentROI
- max_drawdown_percent最大回撤百分比
- equity_curve資產曲線
"""
if initial_capital <= 0:
raise ValueError('initial_capital 必須大於 0')
if not trades:
return {
'trade_count': 0,
'hit_count': 0,
'win_rate': 0.0,
'final_capital': initial_capital,
'net_profit': 0.0,
'roi_percent': 0.0,
'max_drawdown_percent': 0.0,
'equity_curve': [{'ts': datetime.utcnow().isoformat() + 'Z', 'capital': initial_capital}],
}
# 確保輸入依賴的時序,回測才有金融合理性
ordered = sorted(trades, key=lambda row: row.settled_at)
equity = float(initial_capital)
equity_curve: list[dict[str, float | str]] = [
{'ts': ordered[0].settled_at.isoformat(), 'capital': equity},
]
hit = 0
total_stake = 0.0
for trade in ordered:
if trade.odds <= 1:
raise ValueError(f'賠率錯誤 trade={trade.trade_id}, odds={trade.odds}')
stake = trade.stake
profit = stake * (trade.odds - 1) if trade.is_win else -stake
equity += profit
total_stake += stake
if trade.is_win:
hit += 1
equity_curve.append({'ts': trade.settled_at.isoformat(), 'capital': equity})
if total_stake <= 0:
roi = 0.0
else:
roi = (equity - initial_capital) / total_stake * 100
win_rate = round(hit / len(ordered) * 100, 4) if ordered else 0.0
return {
'trade_count': len(ordered),
'hit_count': hit,
'win_rate': win_rate,
'final_capital': round(equity, 4),
'net_profit': round(equity - initial_capital, 4),
'roi_percent': round(roi, 4),
'max_drawdown_percent': compute_max_drawdown([float(point['capital']) for point in equity_curve]),
'equity_curve': equity_curve,
}
__all__ = [
'BacktestTradeRecord',
'StrategyFilter',
'filter_trades',
'run_flat_stake_backtest',
]