aria-code 4.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/__init__.py +32 -0
- agents/base.py +190 -0
- agents/deep/__init__.py +37 -0
- agents/deep/calibration_loop.py +144 -0
- agents/deep/critic.py +125 -0
- agents/deep/deepen.py +193 -0
- agents/deep/models.py +149 -0
- agents/deep/pipeline.py +164 -0
- agents/deep/quant_fusion.py +192 -0
- agents/deep/themes.py +95 -0
- agents/deep/tiers.py +106 -0
- agents/financial/__init__.py +10 -0
- agents/financial/catalyst.py +279 -0
- agents/financial/debate.py +145 -0
- agents/financial/earnings.py +303 -0
- agents/financial/fundamental.py +159 -0
- agents/financial/macro.py +99 -0
- agents/financial/news.py +207 -0
- agents/financial/risk.py +132 -0
- agents/financial/sector.py +279 -0
- agents/financial/synthesis.py +274 -0
- agents/financial/technical.py +258 -0
- agents/portfolio_agent.py +333 -0
- agents/realty/__init__.py +62 -0
- agents/realty/asset_diagnosis.py +150 -0
- agents/realty/business_match.py +165 -0
- agents/realty/cashflow_verify.py +208 -0
- agents/realty/contract_rules.py +209 -0
- agents/realty/energy_anomaly.py +188 -0
- agents/realty/exit_settlement.py +207 -0
- agents/realty/fulfillment_risk.py +205 -0
- agents/realty/ops_optimize.py +159 -0
- agents/realty/revenue_share.py +214 -0
- agents/registry.py +144 -0
- agents/sports/__init__.py +0 -0
- agents/sports/football_agent.py +169 -0
- agents/team.py +289 -0
- aliyun_data_client.py +660 -0
- apps/README.md +12 -0
- apps/__init__.py +2 -0
- apps/channels/README.md +15 -0
- apps/cli/README.md +13 -0
- apps/cli/__init__.py +2 -0
- apps/cli/bootstrap.py +99 -0
- apps/cli/codegen_paths.py +29 -0
- apps/cli/commands/__init__.py +16 -0
- apps/cli/commands/analysis_cmds.py +288 -0
- apps/cli/commands/backtest_cmds.py +1887 -0
- apps/cli/commands/broker_cmds.py +1154 -0
- apps/cli/commands/business_workflow_cmds.py +289 -0
- apps/cli/commands/catalog.py +84 -0
- apps/cli/commands/data_cmds.py +405 -0
- apps/cli/commands/diagnostic_cmds.py +179 -0
- apps/cli/commands/diagnostic_ops_cmds.py +696 -0
- apps/cli/commands/finance_render.py +12 -0
- apps/cli/commands/market.py +399 -0
- apps/cli/commands/market_cmds.py +1276 -0
- apps/cli/commands/market_context.py +425 -0
- apps/cli/commands/market_render.py +7 -0
- apps/cli/commands/model_cmds.py +1579 -0
- apps/cli/commands/ops_cmds.py +668 -0
- apps/cli/commands/portfolio_cmds.py +962 -0
- apps/cli/commands/report.py +377 -0
- apps/cli/commands/scaffold_templates.py +617 -0
- apps/cli/commands/session_cmds.py +179 -0
- apps/cli/commands/session_ux_cmds.py +280 -0
- apps/cli/commands/team.py +588 -0
- apps/cli/commands/team_render.py +8 -0
- apps/cli/commands/ui_cmds.py +358 -0
- apps/cli/commands/workflow_cmds.py +279 -0
- apps/cli/commands/workspace_cmds.py +1414 -0
- apps/cli/config_paths.py +70 -0
- apps/cli/config_store.py +61 -0
- apps/cli/deterministic.py +122 -0
- apps/cli/direct.py +48 -0
- apps/cli/github_app_auth.py +135 -0
- apps/cli/handlers/__init__.py +11 -0
- apps/cli/handlers/broker_handlers.py +122 -0
- apps/cli/handlers/chart_handlers.py +1309 -0
- apps/cli/handlers/market_handlers.py +2509 -0
- apps/cli/handlers/realty_handlers.py +114 -0
- apps/cli/handlers/strategy_advice.py +82 -0
- apps/cli/hooks.py +180 -0
- apps/cli/i18n.py +284 -0
- apps/cli/intent.py +136 -0
- apps/cli/intent_router.py +217 -0
- apps/cli/lifecycle_hooks.py +48 -0
- apps/cli/main.py +29 -0
- apps/cli/market_metadata.py +135 -0
- apps/cli/market_universe.py +265 -0
- apps/cli/message_processing.py +257 -0
- apps/cli/plan_mode.py +139 -0
- apps/cli/plotly_html.py +15 -0
- apps/cli/prediction_feedback.py +202 -0
- apps/cli/preflight.py +497 -0
- apps/cli/project_aria.py +60 -0
- apps/cli/prompts/__init__.py +0 -0
- apps/cli/prompts/coding.py +658 -0
- apps/cli/prompts/system_prompts.py +531 -0
- apps/cli/prompts/ui.py +434 -0
- apps/cli/providers/__init__.py +1 -0
- apps/cli/providers/base.py +271 -0
- apps/cli/providers/chat_routing.py +80 -0
- apps/cli/providers/llm/__init__.py +1 -0
- apps/cli/providers/llm/ollama_stream.py +1170 -0
- apps/cli/providers/llm/sse_stream.py +216 -0
- apps/cli/providers/runtime_bridge.py +185 -0
- apps/cli/runtime_consumer.py +489 -0
- apps/cli/session_export.py +87 -0
- apps/cli/session_jsonl.py +207 -0
- apps/cli/session_store.py +112 -0
- apps/cli/todo_tracker.py +190 -0
- apps/cli/tools/__init__.py +40 -0
- apps/cli/tools/context.py +46 -0
- apps/cli/tools/file_tools.py +112 -0
- apps/cli/tools/market_tools.py +549 -0
- apps/cli/tools/notebook_tools.py +111 -0
- apps/cli/tools/system_tools.py +669 -0
- apps/cli/tools/write_tools.py +715 -0
- apps/cli/tradingview_bridge.py +434 -0
- apps/cli/update_check.py +152 -0
- apps/cli/utils/__init__.py +0 -0
- apps/cli/utils/market_detect.py +1578 -0
- apps/daemon/README.md +14 -0
- apps/vscode/README.md +115 -0
- apps/vscode/package.json +70 -0
- aria_cli.py +11636 -0
- aria_code-4.1.3.dist-info/METADATA +952 -0
- aria_code-4.1.3.dist-info/RECORD +284 -0
- aria_code-4.1.3.dist-info/WHEEL +5 -0
- aria_code-4.1.3.dist-info/entry_points.txt +2 -0
- aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
- aria_code-4.1.3.dist-info/top_level.txt +50 -0
- aria_daemon.py +1295 -0
- aria_feishu_bot.py +1359 -0
- aria_relay_client.py +182 -0
- aria_relay_server.py +405 -0
- aria_telegram_bot.py +202 -0
- ariarc.py +328 -0
- artifacts.py +491 -0
- backtest_report.py +472 -0
- brokers/__init__.py +72 -0
- brokers/base.py +207 -0
- brokers/capabilities.py +264 -0
- brokers/cn/__init__.py +10 -0
- brokers/cn/easytrader_broker.py +193 -0
- brokers/cn/futu_broker.py +194 -0
- brokers/cn/longbridge_broker.py +190 -0
- brokers/cn/tiger_broker.py +196 -0
- brokers/cn/xtquant_broker.py +175 -0
- brokers/config.py +364 -0
- brokers/intl/__init__.py +5 -0
- brokers/intl/alpaca_broker.py +183 -0
- brokers/intl/ibkr_broker.py +215 -0
- brokers/intl/webull_broker.py +156 -0
- brokers/paper_broker.py +259 -0
- brokers/planning.py +296 -0
- brokers/registry.py +181 -0
- brokers/trading.py +237 -0
- change_store.py +127 -0
- command_safety.py +19 -0
- computer_use_tools.py +504 -0
- dashboard_generator.py +578 -0
- data_analysis_tools.py +808 -0
- data_cleaner.py +483 -0
- data_service.py +481 -0
- datasources/__init__.py +23 -0
- datasources/base.py +166 -0
- datasources/router.py +221 -0
- datasources/sources/__init__.py +15 -0
- datasources/sources/akshare_source.py +269 -0
- datasources/sources/alpha_vantage_source.py +202 -0
- datasources/sources/edgar_source.py +218 -0
- datasources/sources/finnhub_source.py +197 -0
- datasources/sources/fred_source.py +219 -0
- datasources/sources/tushare_source.py +141 -0
- datasources/sources/web_scraper_source.py +278 -0
- datasources/sources/world_bank_source.py +205 -0
- datasources/sources/yfinance_source.py +152 -0
- demo_player.py +204 -0
- doctor.py +508 -0
- file_analysis_tools.py +734 -0
- finance_formulas.py +389 -0
- football_data_client.py +1670 -0
- intent_classifier.py +358 -0
- local_finance_tools.py +3221 -0
- local_llm_provider.py +552 -0
- macro_tools.py +368 -0
- market_data_client.py +1899 -0
- mcp_client.py +506 -0
- memory_manager.py +245 -0
- model_capability.py +416 -0
- notification_tools.py +248 -0
- packages/__init__.py +23 -0
- packages/aria_agents/__init__.py +5 -0
- packages/aria_agents/manifest.py +69 -0
- packages/aria_core/__init__.py +34 -0
- packages/aria_core/architecture.py +192 -0
- packages/aria_core/export.py +124 -0
- packages/aria_core/manifest.py +65 -0
- packages/aria_infra/__init__.py +15 -0
- packages/aria_infra/arthera.py +52 -0
- packages/aria_infra/doctor.py +246 -0
- packages/aria_infra/product.py +37 -0
- packages/aria_mcp/__init__.py +25 -0
- packages/aria_mcp/bridge.py +38 -0
- packages/aria_mcp/config.py +97 -0
- packages/aria_mcp/tools.py +61 -0
- packages/aria_sdk/__init__.py +19 -0
- packages/aria_sdk/client.py +396 -0
- packages/aria_sdk/providers.py +70 -0
- packages/aria_sdk/streaming.py +73 -0
- packages/aria_sdk/types.py +86 -0
- packages/aria_services/__init__.py +55 -0
- packages/aria_services/context.py +258 -0
- packages/aria_services/data.py +11 -0
- packages/aria_services/provider_health.py +189 -0
- packages/aria_services/registry.py +213 -0
- packages/aria_services/usage.py +138 -0
- packages/aria_skills/__init__.py +5 -0
- packages/aria_skills/registry.py +59 -0
- packages/aria_tools/__init__.py +5 -0
- packages/aria_tools/registry.py +128 -0
- packages/quant_engine/__init__.py +6 -0
- packages/quant_engine/sports/__init__.py +72 -0
- packages/quant_engine/sports/calibrator.py +353 -0
- packages/quant_engine/sports/dixon_coles.py +234 -0
- packages/quant_engine/sports/elo.py +299 -0
- packages/quant_engine/sports/form.py +188 -0
- packages/quant_engine/sports/h2h.py +195 -0
- packages/quant_engine/sports/ml_model.py +354 -0
- packages/quant_engine/sports/predictor.py +311 -0
- packages/quant_engine/sports/tracker.py +664 -0
- packages/quant_engine/stochastic/__init__.py +27 -0
- packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- packages/quant_engine/stochastic/ito_calculus.py +477 -0
- packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- packages/quant_engine/stochastic/options_pricing.py +573 -0
- packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- plan_utils.py +194 -0
- plugin_loader.py +328 -0
- portfolio_ledger.py +262 -0
- privacy/__init__.py +5 -0
- privacy/feedback.py +123 -0
- project_tools.py +525 -0
- providers/__init__.py +30 -0
- providers/llm/__init__.py +19 -0
- providers/llm/anthropic.py +184 -0
- providers/llm/base.py +139 -0
- providers/llm/ollama.py +128 -0
- providers/llm/openai_compat.py +282 -0
- providers/llm/registry.py +358 -0
- realty_data_tools.py +659 -0
- report_generator.py +1314 -0
- runtime/__init__.py +103 -0
- runtime/agent_loop.py +1183 -0
- runtime/approval.py +51 -0
- runtime/events.py +102 -0
- runtime/gateway.py +128 -0
- runtime/lsp.py +346 -0
- runtime/subagent.py +258 -0
- runtime/tool_executor.py +104 -0
- runtime/tool_policy.py +106 -0
- safety/__init__.py +21 -0
- safety/permissions.py +275 -0
- setup_wizard.py +653 -0
- strategy_vault.py +420 -0
- ui/__init__.py +100 -0
- ui/banner.py +310 -0
- ui/completer.py +391 -0
- ui/console.py +271 -0
- ui/image_render.py +243 -0
- ui/input_box.py +376 -0
- ui/picker.py +195 -0
- ui/render/__init__.py +11 -0
- ui/render/finance.py +1480 -0
- ui/render/market.py +225 -0
- ui/render/output.py +681 -0
- ui/render/team.py +346 -0
- ui/robot.py +235 -0
- workspace/__init__.py +6 -0
- workspace/files.py +170 -0
- workspace/verify.py +113 -0
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sports/tracker.py — 预测追踪、准确率统计、自动 Elo 同步
|
|
3
|
+
=========================================================
|
|
4
|
+
功能:
|
|
5
|
+
1. 记录每次预测(赛前调用 record_prediction)
|
|
6
|
+
— 自动计算并存储比分概率矩阵 top_scorelines + predicted_score
|
|
7
|
+
2. 赛后记录实际结果,自动计算:
|
|
8
|
+
— 1X2 Brier Score / Log-Loss
|
|
9
|
+
— 比分精确命中 score_exact / 比分排名 score_rank
|
|
10
|
+
— 比分 RPS(Ranked Probability Score over scoreline space)
|
|
11
|
+
— 进球 MAE(预期进球 vs 实际进球的平均绝对误差)
|
|
12
|
+
3. 从 football-data.org API 自动同步已结束 WC 比赛 Elo
|
|
13
|
+
4. 动态计算赛事实际场均进球(替换硬编码 1.32)
|
|
14
|
+
5. backfill_score_metrics() — 为历史无比分指标的记录补算
|
|
15
|
+
|
|
16
|
+
持久化路径:
|
|
17
|
+
~/.arthera/football_predictions.json — 预测记录
|
|
18
|
+
~/.arthera/elo_synced_matches.json — 已同步比赛 ID(防重复)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import math
|
|
25
|
+
import time
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Dict, List, Optional, Tuple
|
|
28
|
+
|
|
29
|
+
_PRED_PATH = Path.home() / ".arthera" / "football_predictions.json"
|
|
30
|
+
_SYNCED_PATH = Path.home() / ".arthera" / "elo_synced_matches.json"
|
|
31
|
+
_AVG_PATH = Path.home() / ".arthera" / "wc_league_avg.json"
|
|
32
|
+
|
|
33
|
+
_SCORE_MAX_G = 9 # 评估矩阵上限:0..8 进球
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── 比分概率工具 ──────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
def _poisson_pmf(k: int, lam: float) -> float:
|
|
39
|
+
if lam <= 0:
|
|
40
|
+
return 1.0 if k == 0 else 0.0
|
|
41
|
+
return (lam ** k) * math.exp(-lam) / math.factorial(k)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _scoreline_matrix(lh: float, la: float, max_g: int = _SCORE_MAX_G) -> Dict[Tuple[int, int], float]:
|
|
45
|
+
"""全比分概率矩阵 {(home_goals, away_goals): prob},0..max_g-1 进球。"""
|
|
46
|
+
matrix: Dict[Tuple[int, int], float] = {}
|
|
47
|
+
for h in range(max_g):
|
|
48
|
+
ph = _poisson_pmf(h, lh)
|
|
49
|
+
for a in range(max_g):
|
|
50
|
+
matrix[(h, a)] = ph * _poisson_pmf(a, la)
|
|
51
|
+
return matrix
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _top_scorelines(lh: float, la: float, n: int = 10) -> List[Dict]:
|
|
55
|
+
"""返回概率最高的 n 个比分,含排名。"""
|
|
56
|
+
matrix = _scoreline_matrix(lh, la)
|
|
57
|
+
ranked = sorted(matrix.items(), key=lambda x: -x[1])[:n]
|
|
58
|
+
return [
|
|
59
|
+
{"score": f"{h}-{a}", "h": h, "a": a, "prob": round(p * 100, 2), "rank": i + 1}
|
|
60
|
+
for i, ((h, a), p) in enumerate(ranked)
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _score_rps(matrix: Dict[Tuple[int, int], float], actual_h: int, actual_a: int) -> float:
|
|
65
|
+
"""
|
|
66
|
+
比分空间上的 Ranked Probability Score。
|
|
67
|
+
|
|
68
|
+
在进球总数维度上累积 CDF 误差:
|
|
69
|
+
RPS = mean_over_g( (F_pred(g) - F_actual(g))^2 )
|
|
70
|
+
其中 F(g) = P(total_goals ≤ g),范围 0..2*(max_g-1)。
|
|
71
|
+
值域 [0, 1],越低越好;完美预测 = 0。
|
|
72
|
+
"""
|
|
73
|
+
max_total = 2 * (_SCORE_MAX_G - 1)
|
|
74
|
+
actual_total = actual_h + actual_a
|
|
75
|
+
|
|
76
|
+
# CDF for predicted total goals
|
|
77
|
+
total_probs: Dict[int, float] = {}
|
|
78
|
+
for (h, a), p in matrix.items():
|
|
79
|
+
t = h + a
|
|
80
|
+
total_probs[t] = total_probs.get(t, 0.0) + p
|
|
81
|
+
|
|
82
|
+
cum_pred = 0.0
|
|
83
|
+
cum_actual = 0.0
|
|
84
|
+
rps = 0.0
|
|
85
|
+
for g in range(max_total + 1):
|
|
86
|
+
cum_pred += total_probs.get(g, 0.0)
|
|
87
|
+
cum_actual += 1.0 if g == actual_total else 0.0
|
|
88
|
+
rps += (cum_pred - cum_actual) ** 2
|
|
89
|
+
|
|
90
|
+
return round(rps / (max_total + 1), 5)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ── 持久化工具 ────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
def _load_json(path: Path, default):
|
|
96
|
+
try:
|
|
97
|
+
if path.exists():
|
|
98
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
return default
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _save_json(path: Path, data) -> None:
|
|
105
|
+
try:
|
|
106
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ── 1. 预测记录 ───────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
def record_prediction(
|
|
115
|
+
home_team: str,
|
|
116
|
+
away_team: str,
|
|
117
|
+
home_win: float,
|
|
118
|
+
draw: float,
|
|
119
|
+
away_win: float,
|
|
120
|
+
match_date: str = "",
|
|
121
|
+
competition: str = "WC",
|
|
122
|
+
extra: Optional[Dict] = None,
|
|
123
|
+
) -> str:
|
|
124
|
+
"""
|
|
125
|
+
赛前记录一次预测,返回 prediction_id。
|
|
126
|
+
|
|
127
|
+
用法:
|
|
128
|
+
pid = record_prediction("germany", "curacao", 0.714, 0.165, 0.121,
|
|
129
|
+
match_date="2026-06-14", competition="WC")
|
|
130
|
+
"""
|
|
131
|
+
records = _load_json(_PRED_PATH, [])
|
|
132
|
+
pid = f"{home_team}_vs_{away_team}_{match_date}".replace(" ", "_")
|
|
133
|
+
entry = {
|
|
134
|
+
"id": pid,
|
|
135
|
+
"home_team": home_team,
|
|
136
|
+
"away_team": away_team,
|
|
137
|
+
"home_win": round(home_win, 4),
|
|
138
|
+
"draw": round(draw, 4),
|
|
139
|
+
"away_win": round(away_win, 4),
|
|
140
|
+
"match_date": match_date,
|
|
141
|
+
"competition": competition,
|
|
142
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
143
|
+
"result": None,
|
|
144
|
+
"brier_score": None,
|
|
145
|
+
"log_loss": None,
|
|
146
|
+
**(extra or {}),
|
|
147
|
+
}
|
|
148
|
+
# Auto-compute scoreline distribution from lambdas if available
|
|
149
|
+
lh = (extra or {}).get("lambda_home")
|
|
150
|
+
la = (extra or {}).get("lambda_away")
|
|
151
|
+
if lh and la and lh > 0 and la > 0:
|
|
152
|
+
top = _top_scorelines(float(lh), float(la), n=10)
|
|
153
|
+
entry["top_scorelines"] = top
|
|
154
|
+
entry["predicted_score"] = top[0]["score"] if top else None
|
|
155
|
+
|
|
156
|
+
records = [r for r in records if r.get("id") != pid]
|
|
157
|
+
records.append(entry)
|
|
158
|
+
_save_json(_PRED_PATH, records)
|
|
159
|
+
return pid
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def record_result(
|
|
163
|
+
prediction_id: str,
|
|
164
|
+
actual_outcome: str, # "home" | "draw" | "away"
|
|
165
|
+
home_goals: Optional[int] = None,
|
|
166
|
+
away_goals: Optional[int] = None,
|
|
167
|
+
) -> Optional[Dict]:
|
|
168
|
+
"""
|
|
169
|
+
赛后填入实际结果,自动计算 Brier Score 和 Log-Loss。
|
|
170
|
+
|
|
171
|
+
Brier Score = (p_home-1_home)² + (p_draw-1_draw)² + (p_away-1_away)²
|
|
172
|
+
Log-Loss = -log(p_actual)
|
|
173
|
+
"""
|
|
174
|
+
records = _load_json(_PRED_PATH, [])
|
|
175
|
+
for r in records:
|
|
176
|
+
if r.get("id") == prediction_id:
|
|
177
|
+
oc = actual_outcome.lower()
|
|
178
|
+
i_home = 1.0 if oc == "home" else 0.0
|
|
179
|
+
i_draw = 1.0 if oc == "draw" else 0.0
|
|
180
|
+
i_away = 1.0 if oc == "away" else 0.0
|
|
181
|
+
|
|
182
|
+
brier = (
|
|
183
|
+
(r["home_win"] - i_home) ** 2
|
|
184
|
+
+ (r["draw"] - i_draw) ** 2
|
|
185
|
+
+ (r["away_win"] - i_away) ** 2
|
|
186
|
+
)
|
|
187
|
+
p_act = r["home_win"] * i_home + r["draw"] * i_draw + r["away_win"] * i_away
|
|
188
|
+
logloss = -math.log(max(p_act, 1e-7))
|
|
189
|
+
|
|
190
|
+
r["result"] = oc
|
|
191
|
+
r["brier_score"] = round(brier, 4)
|
|
192
|
+
r["log_loss"] = round(logloss, 4)
|
|
193
|
+
if home_goals is not None:
|
|
194
|
+
r["actual_home_goals"] = home_goals
|
|
195
|
+
if away_goals is not None:
|
|
196
|
+
r["actual_away_goals"] = away_goals
|
|
197
|
+
|
|
198
|
+
# ── 比分级别评估 ──────────────────────────────────────────────────
|
|
199
|
+
if home_goals is not None and away_goals is not None:
|
|
200
|
+
ah, aa = int(home_goals), int(away_goals)
|
|
201
|
+
actual_score_str = f"{ah}-{aa}"
|
|
202
|
+
|
|
203
|
+
# 1. 精确比分命中
|
|
204
|
+
r["score_exact"] = (r.get("predicted_score") == actual_score_str)
|
|
205
|
+
|
|
206
|
+
# 2. 比分排名(在存储的 top_scorelines 里的位置)
|
|
207
|
+
top = r.get("top_scorelines", [])
|
|
208
|
+
ranks = [s["rank"] for s in top if s["score"] == actual_score_str]
|
|
209
|
+
r["score_rank"] = ranks[0] if ranks else None
|
|
210
|
+
|
|
211
|
+
# 3. 用 lambda 重算完整矩阵 → RPS + 精确概率
|
|
212
|
+
lh = r.get("lambda_home")
|
|
213
|
+
la = r.get("lambda_away")
|
|
214
|
+
if lh and la and lh > 0 and la > 0:
|
|
215
|
+
matrix = _scoreline_matrix(float(lh), float(la))
|
|
216
|
+
r["score_prob"] = round(matrix.get((ah, aa), 0.0) * 100, 3)
|
|
217
|
+
r["score_rps"] = _score_rps(matrix, ah, aa)
|
|
218
|
+
r["goals_mae"] = round((abs(float(lh) - ah) + abs(float(la) - aa)) / 2, 3)
|
|
219
|
+
r["goals_mae_h"] = round(abs(float(lh) - ah), 3)
|
|
220
|
+
r["goals_mae_a"] = round(abs(float(la) - aa), 3)
|
|
221
|
+
# rank from full matrix (not just stored top 10)
|
|
222
|
+
ranked_all = sorted(matrix.items(), key=lambda x: -x[1])
|
|
223
|
+
for rank_idx, ((rh, ra), _) in enumerate(ranked_all):
|
|
224
|
+
if rh == ah and ra == aa:
|
|
225
|
+
r["score_rank_full"] = rank_idx + 1
|
|
226
|
+
break
|
|
227
|
+
else:
|
|
228
|
+
r["score_rank_full"] = len(ranked_all)
|
|
229
|
+
|
|
230
|
+
_save_json(_PRED_PATH, records)
|
|
231
|
+
return r
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def get_accuracy_stats() -> Dict:
|
|
236
|
+
"""返回所有已结算预测的准确率统计(含比分级别指标)。"""
|
|
237
|
+
records = _load_json(_PRED_PATH, [])
|
|
238
|
+
settled = [r for r in records if r.get("result") and r.get("brier_score") is not None]
|
|
239
|
+
if not settled:
|
|
240
|
+
return {"total": 0, "message": "暂无已结算预测"}
|
|
241
|
+
|
|
242
|
+
correct = sum(
|
|
243
|
+
1 for r in settled
|
|
244
|
+
if (r["result"] == "home" and r["home_win"] >= max(r["draw"], r["away_win"]))
|
|
245
|
+
or (r["result"] == "draw" and r["draw"] >= max(r["home_win"], r["away_win"]))
|
|
246
|
+
or (r["result"] == "away" and r["away_win"] >= max(r["home_win"], r["draw"]))
|
|
247
|
+
)
|
|
248
|
+
avg_brier = sum(r["brier_score"] for r in settled) / len(settled)
|
|
249
|
+
avg_logloss = sum(r["log_loss"] for r in settled) / len(settled)
|
|
250
|
+
|
|
251
|
+
stats: Dict = {
|
|
252
|
+
"total": len(settled),
|
|
253
|
+
"correct": correct,
|
|
254
|
+
"accuracy": round(correct / len(settled), 3),
|
|
255
|
+
"avg_brier_score": round(avg_brier, 4),
|
|
256
|
+
"avg_log_loss": round(avg_logloss, 4),
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# ── 比分级别统计 ──────────────────────────────────────────────────────────
|
|
260
|
+
has_score = [r for r in settled if r.get("actual_home_goals") is not None and r.get("score_rps") is not None]
|
|
261
|
+
if has_score:
|
|
262
|
+
exact_hits = [r for r in has_score if r.get("score_exact")]
|
|
263
|
+
rank_vals = [r["score_rank_full"] for r in has_score if r.get("score_rank_full")]
|
|
264
|
+
rps_vals = [r["score_rps"] for r in has_score if r.get("score_rps") is not None]
|
|
265
|
+
mae_vals = [r["goals_mae"] for r in has_score if r.get("goals_mae") is not None]
|
|
266
|
+
mae_h_vals = [r["goals_mae_h"] for r in has_score if r.get("goals_mae_h") is not None]
|
|
267
|
+
mae_a_vals = [r["goals_mae_a"] for r in has_score if r.get("goals_mae_a") is not None]
|
|
268
|
+
prob_vals = [r["score_prob"] for r in has_score if r.get("score_prob") is not None]
|
|
269
|
+
|
|
270
|
+
stats["score"] = {
|
|
271
|
+
"total_with_score": len(has_score),
|
|
272
|
+
"exact_hits": len(exact_hits),
|
|
273
|
+
"exact_rate": round(len(exact_hits) / len(has_score), 3),
|
|
274
|
+
"avg_score_rank": round(sum(rank_vals) / len(rank_vals), 1) if rank_vals else None,
|
|
275
|
+
"avg_score_rps": round(sum(rps_vals) / len(rps_vals), 5) if rps_vals else None,
|
|
276
|
+
"avg_goals_mae": round(sum(mae_vals) / len(mae_vals), 3) if mae_vals else None,
|
|
277
|
+
"avg_goals_mae_home": round(sum(mae_h_vals) / len(mae_h_vals), 3) if mae_h_vals else None,
|
|
278
|
+
"avg_goals_mae_away": round(sum(mae_a_vals) / len(mae_a_vals), 3) if mae_a_vals else None,
|
|
279
|
+
"avg_score_prob_pct": round(sum(prob_vals) / len(prob_vals), 3) if prob_vals else None,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
stats["records"] = settled
|
|
283
|
+
return stats
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ── 历史记录比分指标补算 ──────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
def backfill_score_metrics() -> Dict:
|
|
289
|
+
"""
|
|
290
|
+
为历史已结算但缺少比分指标的记录补算:
|
|
291
|
+
top_scorelines, predicted_score, score_exact, score_rank,
|
|
292
|
+
score_rps, goals_mae, score_rank_full, score_prob
|
|
293
|
+
|
|
294
|
+
幂等:已有完整指标的记录跳过。
|
|
295
|
+
返回 {"updated": n, "skipped": n}。
|
|
296
|
+
"""
|
|
297
|
+
records = _load_json(_PRED_PATH, [])
|
|
298
|
+
updated = 0
|
|
299
|
+
|
|
300
|
+
for r in records:
|
|
301
|
+
lh = r.get("lambda_home")
|
|
302
|
+
la = r.get("lambda_away")
|
|
303
|
+
if not (lh and la and lh > 0 and la > 0):
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
# 补 top_scorelines / predicted_score
|
|
307
|
+
if not r.get("top_scorelines"):
|
|
308
|
+
top = _top_scorelines(float(lh), float(la), n=10)
|
|
309
|
+
r["top_scorelines"] = top
|
|
310
|
+
r["predicted_score"] = top[0]["score"] if top else None
|
|
311
|
+
updated += 1
|
|
312
|
+
|
|
313
|
+
# 补比分评估(仅对已有实际进球的记录)
|
|
314
|
+
ah = r.get("actual_home_goals")
|
|
315
|
+
aa = r.get("actual_away_goals")
|
|
316
|
+
if ah is None or aa is None:
|
|
317
|
+
continue
|
|
318
|
+
if r.get("score_rps") is not None:
|
|
319
|
+
continue # 已有,跳过
|
|
320
|
+
|
|
321
|
+
ah, aa = int(ah), int(aa)
|
|
322
|
+
actual_score_str = f"{ah}-{aa}"
|
|
323
|
+
top = r.get("top_scorelines", [])
|
|
324
|
+
r["score_exact"] = (r.get("predicted_score") == actual_score_str)
|
|
325
|
+
ranks = [s["rank"] for s in top if s["score"] == actual_score_str]
|
|
326
|
+
r["score_rank"] = ranks[0] if ranks else None
|
|
327
|
+
|
|
328
|
+
matrix = _scoreline_matrix(float(lh), float(la))
|
|
329
|
+
r["score_prob"] = round(matrix.get((ah, aa), 0.0) * 100, 3)
|
|
330
|
+
r["score_rps"] = _score_rps(matrix, ah, aa)
|
|
331
|
+
r["goals_mae"] = round((abs(float(lh) - ah) + abs(float(la) - aa)) / 2, 3)
|
|
332
|
+
r["goals_mae_h"] = round(abs(float(lh) - ah), 3)
|
|
333
|
+
r["goals_mae_a"] = round(abs(float(la) - aa), 3)
|
|
334
|
+
|
|
335
|
+
ranked_all = sorted(matrix.items(), key=lambda x: -x[1])
|
|
336
|
+
for rank_idx, ((rh, ra), _) in enumerate(ranked_all):
|
|
337
|
+
if rh == ah and ra == aa:
|
|
338
|
+
r["score_rank_full"] = rank_idx + 1
|
|
339
|
+
break
|
|
340
|
+
else:
|
|
341
|
+
r["score_rank_full"] = len(ranked_all)
|
|
342
|
+
|
|
343
|
+
updated += 1
|
|
344
|
+
|
|
345
|
+
_save_json(_PRED_PATH, records)
|
|
346
|
+
return {"updated": updated, "skipped": len(records) - updated}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ── 队名规范化(结算匹配用)──────────────────────────────────────────────────
|
|
350
|
+
# football-data.org 的官方队名与预测记录里的简称/中文常常不一致,
|
|
351
|
+
# 例如 API "Cape Verde Islands" vs 预测 "cape verde",导致结算 ID 对不上,
|
|
352
|
+
# 模型永远学不到这场比赛。统一规范化后按「身份」匹配,而非字符串 ID。
|
|
353
|
+
|
|
354
|
+
_TEAM_ALIASES = {
|
|
355
|
+
"cape verde islands": "cape verde",
|
|
356
|
+
"korea republic": "south korea",
|
|
357
|
+
"korea dpr": "north korea",
|
|
358
|
+
"ir iran": "iran",
|
|
359
|
+
"iran islamic republic": "iran",
|
|
360
|
+
"cote d'ivoire": "ivory coast",
|
|
361
|
+
"côte d'ivoire": "ivory coast",
|
|
362
|
+
"usa": "united states",
|
|
363
|
+
"united states of america": "united states",
|
|
364
|
+
"curaçao": "curacao",
|
|
365
|
+
"türkiye": "turkey",
|
|
366
|
+
"turkiye": "turkey",
|
|
367
|
+
"czech republic": "czechia",
|
|
368
|
+
"china pr": "china",
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _canon(name: str) -> str:
|
|
373
|
+
"""Canonical team key: lowercase, alias-resolved, spaces→underscore."""
|
|
374
|
+
s = (name or "").strip().lower().replace("_", " ")
|
|
375
|
+
s = " ".join(s.split())
|
|
376
|
+
s = _TEAM_ALIASES.get(s, s)
|
|
377
|
+
return s.replace(" ", "_")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _find_pred_record(records: List[Dict], home: str, away: str, date: str) -> Optional[Dict]:
|
|
381
|
+
"""Match a prediction record by canonical (home, away, date) identity.
|
|
382
|
+
|
|
383
|
+
Tolerant of name variants (API vs short name vs alias) so name suffixes
|
|
384
|
+
like 'Cape Verde Islands' settle against a 'cape verde' prediction.
|
|
385
|
+
"""
|
|
386
|
+
ch, ca = _canon(home), _canon(away)
|
|
387
|
+
for r in records:
|
|
388
|
+
if r.get("match_date") != date:
|
|
389
|
+
continue
|
|
390
|
+
if _canon(r.get("home_team", "")) == ch and _canon(r.get("away_team", "")) == ca:
|
|
391
|
+
return r
|
|
392
|
+
return None
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# ── 2. 自动 Elo 同步 ──────────────────────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
def sync_elo_from_wc(api_get_fn, competition_code: str = "WC") -> Dict:
|
|
398
|
+
"""
|
|
399
|
+
从 football-data.org API 同步已结束的 WC 比赛到 Elo 系统。
|
|
400
|
+
防重复处理:已同步的 match_id 记录在 elo_synced_matches.json。
|
|
401
|
+
|
|
402
|
+
api_get_fn: football_data_client._get
|
|
403
|
+
返回: {"synced": int, "skipped": int, "details": [...]}
|
|
404
|
+
"""
|
|
405
|
+
from .elo import get_elo
|
|
406
|
+
|
|
407
|
+
synced_ids: List[int] = _load_json(_SYNCED_PATH, [])
|
|
408
|
+
synced_set = set(synced_ids)
|
|
409
|
+
|
|
410
|
+
data = api_get_fn(f"/competitions/{competition_code}/matches", {"status": "FINISHED"})
|
|
411
|
+
if not data:
|
|
412
|
+
return {"synced": 0, "skipped": 0, "error": "API 无响应"}
|
|
413
|
+
|
|
414
|
+
matches = data.get("matches", [])
|
|
415
|
+
elo = get_elo()
|
|
416
|
+
pred_records = _load_json(_PRED_PATH, [])
|
|
417
|
+
results = []
|
|
418
|
+
new_synced = 0
|
|
419
|
+
newly_settled = 0
|
|
420
|
+
|
|
421
|
+
for m in matches:
|
|
422
|
+
mid = m.get("id")
|
|
423
|
+
ht_name = m.get("homeTeam", {}).get("name", "").lower()
|
|
424
|
+
at_name = m.get("awayTeam", {}).get("name", "").lower()
|
|
425
|
+
ft = m.get("score", {}).get("fullTime", {})
|
|
426
|
+
hg = ft.get("home")
|
|
427
|
+
ag = ft.get("away")
|
|
428
|
+
stage = m.get("stage", "GROUP_STAGE").lower()
|
|
429
|
+
|
|
430
|
+
if hg is None or ag is None:
|
|
431
|
+
continue
|
|
432
|
+
|
|
433
|
+
winner = "home" if hg > ag else ("draw" if hg == ag else "away")
|
|
434
|
+
date_str = m.get("utcDate", "")[:10]
|
|
435
|
+
|
|
436
|
+
# ── Elo 更新:只对新比赛执行(防重复计分)──────────────────────────
|
|
437
|
+
if mid not in synced_set:
|
|
438
|
+
match_type = _stage_to_match_type(stage)
|
|
439
|
+
da, db = elo.update(
|
|
440
|
+
ht_name, at_name,
|
|
441
|
+
int(hg), int(ag),
|
|
442
|
+
match_type=match_type,
|
|
443
|
+
neutral_venue=True,
|
|
444
|
+
)
|
|
445
|
+
synced_ids.append(mid)
|
|
446
|
+
synced_set.add(mid)
|
|
447
|
+
new_synced += 1
|
|
448
|
+
results.append({
|
|
449
|
+
"match": f"{ht_name} {hg}-{ag} {at_name}",
|
|
450
|
+
"type": match_type,
|
|
451
|
+
"elo_chg": f"{ht_name} {da:+.1f} / {at_name} {db:+.1f}",
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
# ── 结算预测:对所有已结束比赛执行(幂等),按规范化身份匹配 ────────
|
|
455
|
+
# 与 Elo 同步解耦,确保名称变体(cape verde / cape verde islands)也能
|
|
456
|
+
# 结算,否则模型永远学不到这些「大冷门」。
|
|
457
|
+
rec = _find_pred_record(pred_records, ht_name, at_name, date_str)
|
|
458
|
+
if rec and rec.get("result") is None:
|
|
459
|
+
settled = record_result(rec["id"], winner, home_goals=int(hg), away_goals=int(ag))
|
|
460
|
+
if settled:
|
|
461
|
+
newly_settled += 1
|
|
462
|
+
try:
|
|
463
|
+
from .calibrator import update_team_goal_bias
|
|
464
|
+
if settled.get("lambda_home"):
|
|
465
|
+
update_team_goal_bias(ht_name, settled["lambda_home"], int(hg))
|
|
466
|
+
if settled.get("lambda_away"):
|
|
467
|
+
update_team_goal_bias(at_name, settled["lambda_away"], int(ag))
|
|
468
|
+
except Exception:
|
|
469
|
+
pass
|
|
470
|
+
|
|
471
|
+
elo.save()
|
|
472
|
+
_save_json(_SYNCED_PATH, synced_ids)
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
"synced": new_synced,
|
|
476
|
+
"newly_settled": newly_settled,
|
|
477
|
+
"skipped": len(matches) - new_synced,
|
|
478
|
+
"details": results,
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def auto_calibrate(api_get_fn=None) -> Dict:
|
|
483
|
+
"""
|
|
484
|
+
自动校准主函数 — 在 sync_elo_from_wc 后调用。
|
|
485
|
+
|
|
486
|
+
触发条件(累积满足即执行):
|
|
487
|
+
≥ 5 场已结算 → λ 偏差修正
|
|
488
|
+
≥ 10 场已结算 → 攻防斜率网格搜索
|
|
489
|
+
有 api_get_fn → ρ 重新校准
|
|
490
|
+
|
|
491
|
+
返回校准报告 dict。
|
|
492
|
+
"""
|
|
493
|
+
from .calibrator import (
|
|
494
|
+
optimize_lambda_bias,
|
|
495
|
+
optimize_slopes_from_outcomes,
|
|
496
|
+
optimize_confidence_temp,
|
|
497
|
+
save_calibration,
|
|
498
|
+
get_calibrated_params,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
records = _load_json(_PRED_PATH, [])
|
|
502
|
+
settled = [r for r in records if r.get("result") and r.get("brier_score") is not None]
|
|
503
|
+
n = len(settled)
|
|
504
|
+
report: Dict = {"settled_count": n, "actions": []}
|
|
505
|
+
|
|
506
|
+
if n < 5:
|
|
507
|
+
report["status"] = "waiting"
|
|
508
|
+
report["message"] = f"数据不足,当前 {n} 场(需 ≥5 场触发校准)"
|
|
509
|
+
return report
|
|
510
|
+
|
|
511
|
+
params = get_calibrated_params()
|
|
512
|
+
|
|
513
|
+
# ── 队伍进球偏差更新(遍历所有已结算含实际进球的记录)────────────────────
|
|
514
|
+
try:
|
|
515
|
+
from .calibrator import update_team_goal_bias
|
|
516
|
+
for r in settled:
|
|
517
|
+
if r.get("actual_home_goals") is not None and r.get("lambda_home"):
|
|
518
|
+
update_team_goal_bias(r["home_team"], r["lambda_home"], r["actual_home_goals"])
|
|
519
|
+
if r.get("actual_away_goals") is not None and r.get("lambda_away"):
|
|
520
|
+
update_team_goal_bias(r["away_team"], r["lambda_away"], r["actual_away_goals"])
|
|
521
|
+
except Exception:
|
|
522
|
+
pass
|
|
523
|
+
|
|
524
|
+
# ── λ 偏差修正(双路径:比值法 + MAE 网格搜索,取 MAE 更优者)────────────
|
|
525
|
+
bias = optimize_lambda_bias(settled)
|
|
526
|
+
try:
|
|
527
|
+
from .calibrator import optimize_lambda_bias_from_scores as _score_bias_fn
|
|
528
|
+
score_bias = _score_bias_fn(settled)
|
|
529
|
+
except Exception:
|
|
530
|
+
score_bias = {"status": "error"}
|
|
531
|
+
|
|
532
|
+
if score_bias.get("status") == "optimized":
|
|
533
|
+
# 使用 MAE 网格搜索结果(更稳健,对极端比分不敏感)
|
|
534
|
+
params["lambda_home_bias"] = score_bias["home_bias"]
|
|
535
|
+
params["lambda_away_bias"] = score_bias["away_bias"]
|
|
536
|
+
params["n_matches"] = n
|
|
537
|
+
report["actions"].append(
|
|
538
|
+
f"λ 偏差(MAE网格): 主队×{score_bias['home_bias']:.3f} "
|
|
539
|
+
f"客队×{score_bias['away_bias']:.3f} "
|
|
540
|
+
f"goals_MAE={score_bias['goals_mae']:.3f}(n={score_bias['n']})"
|
|
541
|
+
)
|
|
542
|
+
elif bias["n_home"] >= 5 or bias["n_away"] >= 5:
|
|
543
|
+
params["lambda_home_bias"] = bias["home_bias"]
|
|
544
|
+
params["lambda_away_bias"] = bias["away_bias"]
|
|
545
|
+
params["n_matches"] = n
|
|
546
|
+
report["actions"].append(
|
|
547
|
+
f"λ 偏差(比值法): 主队×{bias['home_bias']:.3f}(n={bias['n_home']}) "
|
|
548
|
+
f"客队×{bias['away_bias']:.3f}(n={bias['n_away']})"
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
# ── 斜率网格搜索(≥10 场)────────────────────────────────────────────────
|
|
552
|
+
if n >= 10:
|
|
553
|
+
slopes = optimize_slopes_from_outcomes(settled)
|
|
554
|
+
if slopes.get("status") == "optimized":
|
|
555
|
+
prev_brier = get_calibrated_params().get("avg_brier")
|
|
556
|
+
new_brier = slopes["avg_brier"]
|
|
557
|
+
if prev_brier is None or new_brier < prev_brier - 0.002:
|
|
558
|
+
params.update({k: slopes[k] for k in ("a1", "a2", "d1", "d2", "avg_brier")})
|
|
559
|
+
report["actions"].append(
|
|
560
|
+
f"斜率优化: a1={slopes['a1']} a2={slopes['a2']} "
|
|
561
|
+
f"Brier {prev_brier}→{new_brier}"
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# ── 概率温度校准(对抗过度自信,≥8 场)──────────────────────────────────
|
|
565
|
+
temp = optimize_confidence_temp(settled)
|
|
566
|
+
if temp.get("status") == "optimized" and temp["temp"] < 1.0:
|
|
567
|
+
params["conf_temp"] = temp["temp"]
|
|
568
|
+
report["actions"].append(
|
|
569
|
+
f"概率温度: ×{temp['temp']} "
|
|
570
|
+
f"(Brier {temp['brier_before']}→{temp['brier_after']}, n={temp['n']})"
|
|
571
|
+
)
|
|
572
|
+
else:
|
|
573
|
+
params.setdefault("conf_temp", 1.0)
|
|
574
|
+
|
|
575
|
+
# ── ρ 重新校准 ────────────────────────────────────────────────────────────
|
|
576
|
+
if api_get_fn:
|
|
577
|
+
rho = fetch_wc_rho(api_get_fn)
|
|
578
|
+
report["rho"] = rho
|
|
579
|
+
report["actions"].append(f"ρ 校准: {rho:.3f}")
|
|
580
|
+
|
|
581
|
+
save_calibration(params)
|
|
582
|
+
report["status"] = "ok"
|
|
583
|
+
report["message"] = f"校准完成 ({n} 场数据)"
|
|
584
|
+
report["params"] = {k: params[k] for k in ("a1", "a2", "lambda_home_bias", "lambda_away_bias")}
|
|
585
|
+
return report
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _stage_to_match_type(stage: str) -> str:
|
|
589
|
+
if "final" in stage and "semi" not in stage and "quarter" not in stage:
|
|
590
|
+
return "wc_final"
|
|
591
|
+
if "semi" in stage:
|
|
592
|
+
return "wc_semifinal"
|
|
593
|
+
if "quarter" in stage:
|
|
594
|
+
return "wc_quarterfinal"
|
|
595
|
+
if "round_of_16" in stage or "r16" in stage:
|
|
596
|
+
return "wc_r16"
|
|
597
|
+
return "wc_group"
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
# ── 3. 动态场均进球 ───────────────────────────────────────────────────────────
|
|
601
|
+
|
|
602
|
+
_CACHE_TTL = 1800 # 30 min
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def fetch_wc_rho(api_get_fn, competition_code: str = "WC") -> float:
|
|
606
|
+
"""
|
|
607
|
+
从已结束 WC 比赛估计最优 ρ,保存到 ~/.arthera/wc_rho.json。
|
|
608
|
+
需要 ≥20 场数据才校准,否则保持 -0.10。
|
|
609
|
+
"""
|
|
610
|
+
from .dixon_coles import estimate_rho_from_results
|
|
611
|
+
|
|
612
|
+
_RHO_PATH = Path.home() / ".arthera" / "wc_rho.json"
|
|
613
|
+
cached = _load_json(_RHO_PATH, {})
|
|
614
|
+
now = time.time()
|
|
615
|
+
if cached.get("ts", 0) + _CACHE_TTL > now:
|
|
616
|
+
return cached.get("rho", -0.10)
|
|
617
|
+
|
|
618
|
+
data = api_get_fn(f"/competitions/{competition_code}/matches", {"status": "FINISHED"})
|
|
619
|
+
if not data:
|
|
620
|
+
return cached.get("rho", -0.10)
|
|
621
|
+
|
|
622
|
+
results = []
|
|
623
|
+
for m in data.get("matches", []):
|
|
624
|
+
ft = m.get("score", {}).get("fullTime", {})
|
|
625
|
+
hg, ag = ft.get("home"), ft.get("away")
|
|
626
|
+
if hg is not None and ag is not None:
|
|
627
|
+
results.append((int(hg), int(ag)))
|
|
628
|
+
|
|
629
|
+
if len(results) < 20:
|
|
630
|
+
return -0.10
|
|
631
|
+
|
|
632
|
+
rho = estimate_rho_from_results(results)
|
|
633
|
+
_save_json(_RHO_PATH, {"rho": rho, "ts": now, "matches": len(results)})
|
|
634
|
+
return rho
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def fetch_wc_league_avg(api_get_fn, competition_code: str = "WC") -> float:
|
|
638
|
+
"""
|
|
639
|
+
从 API 计算本届赛事实际场均进球,缓存 30 分钟。
|
|
640
|
+
数据不足 5 场时返回默认值 1.35。
|
|
641
|
+
"""
|
|
642
|
+
cached = _load_json(_AVG_PATH, {})
|
|
643
|
+
now = time.time()
|
|
644
|
+
if cached.get("ts", 0) + _CACHE_TTL > now:
|
|
645
|
+
return cached.get("avg", 1.35)
|
|
646
|
+
|
|
647
|
+
data = api_get_fn(f"/competitions/{competition_code}/matches", {"status": "FINISHED"})
|
|
648
|
+
if not data:
|
|
649
|
+
return cached.get("avg", 1.35)
|
|
650
|
+
|
|
651
|
+
matches = data.get("matches", [])
|
|
652
|
+
totals = []
|
|
653
|
+
for m in matches:
|
|
654
|
+
ft = m.get("score", {}).get("fullTime", {})
|
|
655
|
+
hg, ag = ft.get("home"), ft.get("away")
|
|
656
|
+
if hg is not None and ag is not None:
|
|
657
|
+
totals.append(hg + ag)
|
|
658
|
+
|
|
659
|
+
if len(totals) < 5:
|
|
660
|
+
return 1.35
|
|
661
|
+
|
|
662
|
+
avg = sum(totals) / (len(totals) * 2)
|
|
663
|
+
_save_json(_AVG_PATH, {"avg": round(avg, 3), "ts": now, "matches": len(totals)})
|
|
664
|
+
return round(avg, 3)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Arthera Stochastic Calculus Module
|
|
3
|
+
随机微积分模块
|
|
4
|
+
|
|
5
|
+
Components:
|
|
6
|
+
- ito_calculus.py : 伊藤引理 / 伊藤积分 / 随机微分方程
|
|
7
|
+
- gbm_enhanced.py : 增强几何布朗运动(多资产/跳跃扩散/随机波动率)
|
|
8
|
+
- stochastic_processes : OU / CIR / Vasicek / Hull-White 过程
|
|
9
|
+
- monte_carlo_advanced : 方差缩减蒙特卡罗(Antithetic/Control/Quasi-MC)
|
|
10
|
+
- kelly_criterion : 凯利公式(连续时间 / 多资产 / Robust版本)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .ito_calculus import ItoCalculus, ItoProcess, apply_ito_lemma
|
|
14
|
+
from .gbm_enhanced import EnhancedGBM
|
|
15
|
+
from .stochastic_processes import (
|
|
16
|
+
OrnsteinUhlenbeck, CIRProcess, VasicekModel, HullWhiteModel
|
|
17
|
+
)
|
|
18
|
+
from .monte_carlo_advanced import MonteCarloEngine, VarianceReduction
|
|
19
|
+
from .kelly_criterion import KellyCriterion
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ItoCalculus", "ItoProcess", "apply_ito_lemma",
|
|
23
|
+
"EnhancedGBM",
|
|
24
|
+
"OrnsteinUhlenbeck", "CIRProcess", "VasicekModel", "HullWhiteModel",
|
|
25
|
+
"MonteCarloEngine", "VarianceReduction",
|
|
26
|
+
"KellyCriterion",
|
|
27
|
+
]
|