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.
Files changed (284) hide show
  1. agents/__init__.py +32 -0
  2. agents/base.py +190 -0
  3. agents/deep/__init__.py +37 -0
  4. agents/deep/calibration_loop.py +144 -0
  5. agents/deep/critic.py +125 -0
  6. agents/deep/deepen.py +193 -0
  7. agents/deep/models.py +149 -0
  8. agents/deep/pipeline.py +164 -0
  9. agents/deep/quant_fusion.py +192 -0
  10. agents/deep/themes.py +95 -0
  11. agents/deep/tiers.py +106 -0
  12. agents/financial/__init__.py +10 -0
  13. agents/financial/catalyst.py +279 -0
  14. agents/financial/debate.py +145 -0
  15. agents/financial/earnings.py +303 -0
  16. agents/financial/fundamental.py +159 -0
  17. agents/financial/macro.py +99 -0
  18. agents/financial/news.py +207 -0
  19. agents/financial/risk.py +132 -0
  20. agents/financial/sector.py +279 -0
  21. agents/financial/synthesis.py +274 -0
  22. agents/financial/technical.py +258 -0
  23. agents/portfolio_agent.py +333 -0
  24. agents/realty/__init__.py +62 -0
  25. agents/realty/asset_diagnosis.py +150 -0
  26. agents/realty/business_match.py +165 -0
  27. agents/realty/cashflow_verify.py +208 -0
  28. agents/realty/contract_rules.py +209 -0
  29. agents/realty/energy_anomaly.py +188 -0
  30. agents/realty/exit_settlement.py +207 -0
  31. agents/realty/fulfillment_risk.py +205 -0
  32. agents/realty/ops_optimize.py +159 -0
  33. agents/realty/revenue_share.py +214 -0
  34. agents/registry.py +144 -0
  35. agents/sports/__init__.py +0 -0
  36. agents/sports/football_agent.py +169 -0
  37. agents/team.py +289 -0
  38. aliyun_data_client.py +660 -0
  39. apps/README.md +12 -0
  40. apps/__init__.py +2 -0
  41. apps/channels/README.md +15 -0
  42. apps/cli/README.md +13 -0
  43. apps/cli/__init__.py +2 -0
  44. apps/cli/bootstrap.py +99 -0
  45. apps/cli/codegen_paths.py +29 -0
  46. apps/cli/commands/__init__.py +16 -0
  47. apps/cli/commands/analysis_cmds.py +288 -0
  48. apps/cli/commands/backtest_cmds.py +1887 -0
  49. apps/cli/commands/broker_cmds.py +1154 -0
  50. apps/cli/commands/business_workflow_cmds.py +289 -0
  51. apps/cli/commands/catalog.py +84 -0
  52. apps/cli/commands/data_cmds.py +405 -0
  53. apps/cli/commands/diagnostic_cmds.py +179 -0
  54. apps/cli/commands/diagnostic_ops_cmds.py +696 -0
  55. apps/cli/commands/finance_render.py +12 -0
  56. apps/cli/commands/market.py +399 -0
  57. apps/cli/commands/market_cmds.py +1276 -0
  58. apps/cli/commands/market_context.py +425 -0
  59. apps/cli/commands/market_render.py +7 -0
  60. apps/cli/commands/model_cmds.py +1579 -0
  61. apps/cli/commands/ops_cmds.py +668 -0
  62. apps/cli/commands/portfolio_cmds.py +962 -0
  63. apps/cli/commands/report.py +377 -0
  64. apps/cli/commands/scaffold_templates.py +617 -0
  65. apps/cli/commands/session_cmds.py +179 -0
  66. apps/cli/commands/session_ux_cmds.py +280 -0
  67. apps/cli/commands/team.py +588 -0
  68. apps/cli/commands/team_render.py +8 -0
  69. apps/cli/commands/ui_cmds.py +358 -0
  70. apps/cli/commands/workflow_cmds.py +279 -0
  71. apps/cli/commands/workspace_cmds.py +1414 -0
  72. apps/cli/config_paths.py +70 -0
  73. apps/cli/config_store.py +61 -0
  74. apps/cli/deterministic.py +122 -0
  75. apps/cli/direct.py +48 -0
  76. apps/cli/github_app_auth.py +135 -0
  77. apps/cli/handlers/__init__.py +11 -0
  78. apps/cli/handlers/broker_handlers.py +122 -0
  79. apps/cli/handlers/chart_handlers.py +1309 -0
  80. apps/cli/handlers/market_handlers.py +2509 -0
  81. apps/cli/handlers/realty_handlers.py +114 -0
  82. apps/cli/handlers/strategy_advice.py +82 -0
  83. apps/cli/hooks.py +180 -0
  84. apps/cli/i18n.py +284 -0
  85. apps/cli/intent.py +136 -0
  86. apps/cli/intent_router.py +217 -0
  87. apps/cli/lifecycle_hooks.py +48 -0
  88. apps/cli/main.py +29 -0
  89. apps/cli/market_metadata.py +135 -0
  90. apps/cli/market_universe.py +265 -0
  91. apps/cli/message_processing.py +257 -0
  92. apps/cli/plan_mode.py +139 -0
  93. apps/cli/plotly_html.py +15 -0
  94. apps/cli/prediction_feedback.py +202 -0
  95. apps/cli/preflight.py +497 -0
  96. apps/cli/project_aria.py +60 -0
  97. apps/cli/prompts/__init__.py +0 -0
  98. apps/cli/prompts/coding.py +658 -0
  99. apps/cli/prompts/system_prompts.py +531 -0
  100. apps/cli/prompts/ui.py +434 -0
  101. apps/cli/providers/__init__.py +1 -0
  102. apps/cli/providers/base.py +271 -0
  103. apps/cli/providers/chat_routing.py +80 -0
  104. apps/cli/providers/llm/__init__.py +1 -0
  105. apps/cli/providers/llm/ollama_stream.py +1170 -0
  106. apps/cli/providers/llm/sse_stream.py +216 -0
  107. apps/cli/providers/runtime_bridge.py +185 -0
  108. apps/cli/runtime_consumer.py +489 -0
  109. apps/cli/session_export.py +87 -0
  110. apps/cli/session_jsonl.py +207 -0
  111. apps/cli/session_store.py +112 -0
  112. apps/cli/todo_tracker.py +190 -0
  113. apps/cli/tools/__init__.py +40 -0
  114. apps/cli/tools/context.py +46 -0
  115. apps/cli/tools/file_tools.py +112 -0
  116. apps/cli/tools/market_tools.py +549 -0
  117. apps/cli/tools/notebook_tools.py +111 -0
  118. apps/cli/tools/system_tools.py +669 -0
  119. apps/cli/tools/write_tools.py +715 -0
  120. apps/cli/tradingview_bridge.py +434 -0
  121. apps/cli/update_check.py +152 -0
  122. apps/cli/utils/__init__.py +0 -0
  123. apps/cli/utils/market_detect.py +1578 -0
  124. apps/daemon/README.md +14 -0
  125. apps/vscode/README.md +115 -0
  126. apps/vscode/package.json +70 -0
  127. aria_cli.py +11636 -0
  128. aria_code-4.1.3.dist-info/METADATA +952 -0
  129. aria_code-4.1.3.dist-info/RECORD +284 -0
  130. aria_code-4.1.3.dist-info/WHEEL +5 -0
  131. aria_code-4.1.3.dist-info/entry_points.txt +2 -0
  132. aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
  133. aria_code-4.1.3.dist-info/top_level.txt +50 -0
  134. aria_daemon.py +1295 -0
  135. aria_feishu_bot.py +1359 -0
  136. aria_relay_client.py +182 -0
  137. aria_relay_server.py +405 -0
  138. aria_telegram_bot.py +202 -0
  139. ariarc.py +328 -0
  140. artifacts.py +491 -0
  141. backtest_report.py +472 -0
  142. brokers/__init__.py +72 -0
  143. brokers/base.py +207 -0
  144. brokers/capabilities.py +264 -0
  145. brokers/cn/__init__.py +10 -0
  146. brokers/cn/easytrader_broker.py +193 -0
  147. brokers/cn/futu_broker.py +194 -0
  148. brokers/cn/longbridge_broker.py +190 -0
  149. brokers/cn/tiger_broker.py +196 -0
  150. brokers/cn/xtquant_broker.py +175 -0
  151. brokers/config.py +364 -0
  152. brokers/intl/__init__.py +5 -0
  153. brokers/intl/alpaca_broker.py +183 -0
  154. brokers/intl/ibkr_broker.py +215 -0
  155. brokers/intl/webull_broker.py +156 -0
  156. brokers/paper_broker.py +259 -0
  157. brokers/planning.py +296 -0
  158. brokers/registry.py +181 -0
  159. brokers/trading.py +237 -0
  160. change_store.py +127 -0
  161. command_safety.py +19 -0
  162. computer_use_tools.py +504 -0
  163. dashboard_generator.py +578 -0
  164. data_analysis_tools.py +808 -0
  165. data_cleaner.py +483 -0
  166. data_service.py +481 -0
  167. datasources/__init__.py +23 -0
  168. datasources/base.py +166 -0
  169. datasources/router.py +221 -0
  170. datasources/sources/__init__.py +15 -0
  171. datasources/sources/akshare_source.py +269 -0
  172. datasources/sources/alpha_vantage_source.py +202 -0
  173. datasources/sources/edgar_source.py +218 -0
  174. datasources/sources/finnhub_source.py +197 -0
  175. datasources/sources/fred_source.py +219 -0
  176. datasources/sources/tushare_source.py +141 -0
  177. datasources/sources/web_scraper_source.py +278 -0
  178. datasources/sources/world_bank_source.py +205 -0
  179. datasources/sources/yfinance_source.py +152 -0
  180. demo_player.py +204 -0
  181. doctor.py +508 -0
  182. file_analysis_tools.py +734 -0
  183. finance_formulas.py +389 -0
  184. football_data_client.py +1670 -0
  185. intent_classifier.py +358 -0
  186. local_finance_tools.py +3221 -0
  187. local_llm_provider.py +552 -0
  188. macro_tools.py +368 -0
  189. market_data_client.py +1899 -0
  190. mcp_client.py +506 -0
  191. memory_manager.py +245 -0
  192. model_capability.py +416 -0
  193. notification_tools.py +248 -0
  194. packages/__init__.py +23 -0
  195. packages/aria_agents/__init__.py +5 -0
  196. packages/aria_agents/manifest.py +69 -0
  197. packages/aria_core/__init__.py +34 -0
  198. packages/aria_core/architecture.py +192 -0
  199. packages/aria_core/export.py +124 -0
  200. packages/aria_core/manifest.py +65 -0
  201. packages/aria_infra/__init__.py +15 -0
  202. packages/aria_infra/arthera.py +52 -0
  203. packages/aria_infra/doctor.py +246 -0
  204. packages/aria_infra/product.py +37 -0
  205. packages/aria_mcp/__init__.py +25 -0
  206. packages/aria_mcp/bridge.py +38 -0
  207. packages/aria_mcp/config.py +97 -0
  208. packages/aria_mcp/tools.py +61 -0
  209. packages/aria_sdk/__init__.py +19 -0
  210. packages/aria_sdk/client.py +396 -0
  211. packages/aria_sdk/providers.py +70 -0
  212. packages/aria_sdk/streaming.py +73 -0
  213. packages/aria_sdk/types.py +86 -0
  214. packages/aria_services/__init__.py +55 -0
  215. packages/aria_services/context.py +258 -0
  216. packages/aria_services/data.py +11 -0
  217. packages/aria_services/provider_health.py +189 -0
  218. packages/aria_services/registry.py +213 -0
  219. packages/aria_services/usage.py +138 -0
  220. packages/aria_skills/__init__.py +5 -0
  221. packages/aria_skills/registry.py +59 -0
  222. packages/aria_tools/__init__.py +5 -0
  223. packages/aria_tools/registry.py +128 -0
  224. packages/quant_engine/__init__.py +6 -0
  225. packages/quant_engine/sports/__init__.py +72 -0
  226. packages/quant_engine/sports/calibrator.py +353 -0
  227. packages/quant_engine/sports/dixon_coles.py +234 -0
  228. packages/quant_engine/sports/elo.py +299 -0
  229. packages/quant_engine/sports/form.py +188 -0
  230. packages/quant_engine/sports/h2h.py +195 -0
  231. packages/quant_engine/sports/ml_model.py +354 -0
  232. packages/quant_engine/sports/predictor.py +311 -0
  233. packages/quant_engine/sports/tracker.py +664 -0
  234. packages/quant_engine/stochastic/__init__.py +27 -0
  235. packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  236. packages/quant_engine/stochastic/ito_calculus.py +477 -0
  237. packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  238. packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  239. packages/quant_engine/stochastic/options_pricing.py +573 -0
  240. packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  241. plan_utils.py +194 -0
  242. plugin_loader.py +328 -0
  243. portfolio_ledger.py +262 -0
  244. privacy/__init__.py +5 -0
  245. privacy/feedback.py +123 -0
  246. project_tools.py +525 -0
  247. providers/__init__.py +30 -0
  248. providers/llm/__init__.py +19 -0
  249. providers/llm/anthropic.py +184 -0
  250. providers/llm/base.py +139 -0
  251. providers/llm/ollama.py +128 -0
  252. providers/llm/openai_compat.py +282 -0
  253. providers/llm/registry.py +358 -0
  254. realty_data_tools.py +659 -0
  255. report_generator.py +1314 -0
  256. runtime/__init__.py +103 -0
  257. runtime/agent_loop.py +1183 -0
  258. runtime/approval.py +51 -0
  259. runtime/events.py +102 -0
  260. runtime/gateway.py +128 -0
  261. runtime/lsp.py +346 -0
  262. runtime/subagent.py +258 -0
  263. runtime/tool_executor.py +104 -0
  264. runtime/tool_policy.py +106 -0
  265. safety/__init__.py +21 -0
  266. safety/permissions.py +275 -0
  267. setup_wizard.py +653 -0
  268. strategy_vault.py +420 -0
  269. ui/__init__.py +100 -0
  270. ui/banner.py +310 -0
  271. ui/completer.py +391 -0
  272. ui/console.py +271 -0
  273. ui/image_render.py +243 -0
  274. ui/input_box.py +376 -0
  275. ui/picker.py +195 -0
  276. ui/render/__init__.py +11 -0
  277. ui/render/finance.py +1480 -0
  278. ui/render/market.py +225 -0
  279. ui/render/output.py +681 -0
  280. ui/render/team.py +346 -0
  281. ui/robot.py +235 -0
  282. workspace/__init__.py +6 -0
  283. workspace/files.py +170 -0
  284. workspace/verify.py +113 -0
@@ -0,0 +1,353 @@
1
+ """
2
+ sports/calibrator.py — 模型参数自动优化
3
+ =========================================
4
+ 基于历史预测 Brier Score 和实际进球,持续校准攻防斜率与 λ 偏差。
5
+
6
+ 自动优化流程(数据积累触发):
7
+ ≥ 5 场已结算 → λ 偏差修正(实际进球 / 预测 λ 的指数移动平均)
8
+ ≥ 10 场已结算 → 攻防斜率网格搜索(最小化 Brier Score)
9
+ ≥ 3 场单队数据 → 队伍专属进球偏差修正
10
+
11
+ 持久化:
12
+ ~/.arthera/wc_calibrated_params.json — 全局斜率 + λ 偏差
13
+ ~/.arthera/team_goal_bias.json — 各队进球偏差 EMA
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import time
20
+ from pathlib import Path
21
+ from typing import Dict, List, Optional, Tuple
22
+
23
+ _PARAMS_PATH = Path.home() / ".arthera" / "wc_calibrated_params.json"
24
+ _TEAM_BIAS_PATH = Path.home() / ".arthera" / "team_goal_bias.json"
25
+
26
+ _DEFAULT_PARAMS = {
27
+ "a1": 1.05, "a2": 0.35, # 攻击斜率(线性 + 二次)
28
+ "d1": -0.42, "d2": -0.10, # 防守斜率
29
+ "lambda_home_bias": 1.0, # λ 偏差修正因子
30
+ "lambda_away_bias": 1.0,
31
+ "conf_temp": 1.0, # 概率温度(<1 收敛过度自信的预测)
32
+ "calibrated_at": None,
33
+ "n_matches": 0,
34
+ "avg_brier": None,
35
+ }
36
+
37
+
38
+ # ── 持久化工具 ────────────────────────────────────────────────────────────────
39
+
40
+ def _load_json(path: Path, default):
41
+ try:
42
+ if path.exists():
43
+ return json.loads(path.read_text(encoding="utf-8"))
44
+ except Exception:
45
+ pass
46
+ return default
47
+
48
+
49
+ def _save_json(path: Path, data) -> None:
50
+ try:
51
+ path.parent.mkdir(parents=True, exist_ok=True)
52
+ path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
53
+ except Exception:
54
+ pass
55
+
56
+
57
+ # ── 1. λ 偏差修正 ─────────────────────────────────────────────────────────────
58
+
59
+ def optimize_lambda_bias(settled_records: List[Dict]) -> Dict[str, float]:
60
+ """
61
+ 从已结算预测计算主客队 λ 系统偏差。
62
+
63
+ 每条记录需包含: actual_home_goals, actual_away_goals, lambda_home, lambda_away。
64
+ 偏差 = mean(actual / predicted);> 1.0 说明模型系统性低估进球。
65
+ 需要 ≥ 5 条含实际进球的记录才修正。
66
+ """
67
+ home_ratios, away_ratios = [], []
68
+
69
+ for r in settled_records:
70
+ ah = r.get("actual_home_goals")
71
+ aa = r.get("actual_away_goals")
72
+ lh = r.get("lambda_home")
73
+ la = r.get("lambda_away")
74
+ if ah is not None and lh and lh > 0:
75
+ home_ratios.append(ah / lh)
76
+ if aa is not None and la and la > 0:
77
+ away_ratios.append(aa / la)
78
+
79
+ result = {"home_bias": 1.0, "away_bias": 1.0, "n_home": len(home_ratios), "n_away": len(away_ratios)}
80
+ if len(home_ratios) >= 5:
81
+ result["home_bias"] = round(sum(home_ratios) / len(home_ratios), 4)
82
+ if len(away_ratios) >= 5:
83
+ result["away_bias"] = round(sum(away_ratios) / len(away_ratios), 4)
84
+ return result
85
+
86
+
87
+ # ── 2. 攻防斜率网格搜索 ───────────────────────────────────────────────────────
88
+
89
+ def optimize_slopes_from_outcomes(settled_records: List[Dict]) -> Dict:
90
+ """
91
+ 从已结算预测记录(需含 home_elo / away_elo / result)搜索最优攻防斜率。
92
+
93
+ 策略:固定 d1 = -a1×0.40, d2 = -a2×0.29(对称性假设),
94
+ 2D 网格搜索 a1 × a2,最小化平均 Brier Score。
95
+ 需要 ≥ 10 条含 Elo 数据的记录。
96
+ """
97
+ records = [
98
+ r for r in settled_records
99
+ if r.get("home_elo") and r.get("away_elo") and r.get("result")
100
+ ]
101
+ if len(records) < 10:
102
+ return {**_DEFAULT_PARAMS, "status": "not_enough_data", "n": len(records)}
103
+
104
+ try:
105
+ from .dixon_coles import compute_match_probabilities
106
+ except ImportError:
107
+ return {**_DEFAULT_PARAMS, "status": "import_error"}
108
+
109
+ best_brier = float("inf")
110
+ best = {}
111
+
112
+ for a1 in [0.85, 0.95, 1.05, 1.15, 1.25, 1.35]:
113
+ for a2 in [0.20, 0.28, 0.35, 0.43, 0.50]:
114
+ d1 = round(-a1 * 0.40, 3)
115
+ d2 = round(-a2 * 0.29, 3)
116
+ total_brier = 0.0
117
+
118
+ for r in records:
119
+ h_elo = float(r["home_elo"])
120
+ a_elo = float(r["away_elo"])
121
+ si_h = (h_elo - 1500) / 400.0
122
+ si_a = (a_elo - 1500) / 400.0
123
+
124
+ h_atk = max(0.45, min(1.10 + si_h*a1 + max(0, si_h)*si_h*a2, 2.60))
125
+ a_atk = max(0.45, min(1.10 + si_a*a1 + max(0, si_a)*si_a*a2, 2.60))
126
+ h_def = max(0.40, min(0.95 + si_h*d1 + max(0, si_h)*si_h*d2, 1.25))
127
+ a_def = max(0.40, min(0.95 + si_a*d1 + max(0, si_a)*si_a*d2, 1.25))
128
+
129
+ league_avg = float(r.get("league_avg", 1.35))
130
+ lh = max(0.2, min(h_atk * a_def * league_avg, 8.0))
131
+ la = max(0.2, min(a_atk * h_def * league_avg, 8.0))
132
+ elo_diff = h_elo - a_elo
133
+
134
+ dc = compute_match_probabilities(lh, la, rho=-0.10, elo_diff=elo_diff)
135
+ res = r["result"]
136
+ ih = 1.0 if res == "home" else 0.0
137
+ id_ = 1.0 if res == "draw" else 0.0
138
+ ia = 1.0 if res == "away" else 0.0
139
+ total_brier += (dc["home_win"]-ih)**2 + (dc["draw"]-id_)**2 + (dc["away_win"]-ia)**2
140
+
141
+ avg_brier = total_brier / len(records)
142
+ if avg_brier < best_brier:
143
+ best_brier = avg_brier
144
+ best = {
145
+ "a1": a1, "a2": a2, "d1": d1, "d2": d2,
146
+ "avg_brier": round(avg_brier, 4),
147
+ "n": len(records),
148
+ "status": "optimized",
149
+ }
150
+
151
+ return best
152
+
153
+
154
+ # ── 2b. 概率温度校准(对抗过度自信)──────────────────────────────────────────
155
+
156
+ def _apply_temp(ph: float, pd: float, pa: float, temp: float) -> Tuple[float, float, float]:
157
+ """Temperature-scale a 1X2 distribution: p_i ∝ p_i**temp, renormalized.
158
+
159
+ temp < 1 flattens (less confident); temp = 1 is a no-op. Guards against
160
+ zero/negative inputs.
161
+ """
162
+ if temp == 1.0:
163
+ return ph, pd, pa
164
+ eps = 1e-9
165
+ h = max(ph, eps) ** temp
166
+ d = max(pd, eps) ** temp
167
+ a = max(pa, eps) ** temp
168
+ s = h + d + a
169
+ return h / s, d / s, a / s
170
+
171
+
172
+ def optimize_confidence_temp(settled_records: List[Dict]) -> Dict:
173
+ """Grid-search the probability temperature that minimizes Brier.
174
+
175
+ World-Cup favorites are systematically over-predicted (e.g. 88% → draw),
176
+ so the raw 1X2 distribution is too sharp. We find temp ∈ [0.55, 1.0] that
177
+ flattens predictions to best match observed outcomes.
178
+
179
+ Trains on RAW probabilities (raw_home_win/…) when present so it never
180
+ compounds a temperature already applied at output; falls back to the
181
+ stored home_win/… for legacy records. Needs ≥ 8 settled records.
182
+ """
183
+ rows = []
184
+ for r in settled_records:
185
+ if not r.get("result"):
186
+ continue
187
+ ph = r.get("raw_home_win", r.get("home_win"))
188
+ pd = r.get("raw_draw", r.get("draw"))
189
+ pa = r.get("raw_away_win", r.get("away_win"))
190
+ if ph is None or pd is None or pa is None:
191
+ continue
192
+ rows.append((float(ph), float(pd), float(pa), r["result"]))
193
+
194
+ if len(rows) < 8:
195
+ return {"temp": 1.0, "status": "not_enough_data", "n": len(rows)}
196
+
197
+ def mean_brier(temp: float) -> float:
198
+ tot = 0.0
199
+ for ph, pd, pa, res in rows:
200
+ h, d, a = _apply_temp(ph, pd, pa, temp)
201
+ ih = 1.0 if res == "home" else 0.0
202
+ idr = 1.0 if res == "draw" else 0.0
203
+ ia = 1.0 if res == "away" else 0.0
204
+ tot += (h - ih) ** 2 + (d - idr) ** 2 + (a - ia) ** 2
205
+ return tot / len(rows)
206
+
207
+ base = mean_brier(1.0)
208
+ best_temp, best_brier = 1.0, base
209
+ t = 0.55
210
+ while t <= 1.001:
211
+ b = mean_brier(t)
212
+ if b < best_brier:
213
+ best_brier, best_temp = b, round(t, 2)
214
+ t += 0.05
215
+
216
+ return {
217
+ "temp": best_temp,
218
+ "status": "optimized",
219
+ "n": len(rows),
220
+ "brier_before": round(base, 4),
221
+ "brier_after": round(best_brier, 4),
222
+ }
223
+
224
+
225
+ def get_confidence_temp() -> float:
226
+ return get_calibrated_params().get("conf_temp", 1.0)
227
+
228
+
229
+ # ── 3. 队伍专属进球偏差(EMA)─────────────────────────────────────────────────
230
+
231
+ def update_team_goal_bias(team: str, predicted_lambda: float, actual_goals: int) -> None:
232
+ """
233
+ 更新队伍历史进球偏差(指数移动平均,alpha=0.3)。
234
+ bias_ema > 1 表示该队实际进球通常高于模型预测。
235
+ """
236
+ if predicted_lambda <= 0:
237
+ return
238
+
239
+ biases: Dict = _load_json(_TEAM_BIAS_PATH, {})
240
+ key = team.lower().strip()
241
+ ratio = actual_goals / predicted_lambda
242
+
243
+ if key not in biases:
244
+ biases[key] = {"ema": round(ratio, 4), "n": 1, "updated": time.strftime("%Y-%m-%d")}
245
+ else:
246
+ alpha = 0.3
247
+ biases[key]["ema"] = round(alpha * ratio + (1 - alpha) * biases[key]["ema"], 4)
248
+ biases[key]["n"] += 1
249
+ biases[key]["updated"] = time.strftime("%Y-%m-%d")
250
+
251
+ _save_json(_TEAM_BIAS_PATH, biases)
252
+
253
+
254
+ def get_team_goal_bias(team: str) -> float:
255
+ """
256
+ 返回队伍进球偏差修正因子。
257
+ < 3 场数据时返回 1.0(不修正,防止过拟合)。
258
+ """
259
+ biases = _load_json(_TEAM_BIAS_PATH, {})
260
+ entry = biases.get(team.lower().strip(), {})
261
+ if entry.get("n", 0) < 3:
262
+ return 1.0
263
+ bias = entry.get("ema", 1.0)
264
+ # 限制修正幅度,防止极端值
265
+ return round(max(0.70, min(bias, 1.50)), 4)
266
+
267
+
268
+ def get_all_team_biases() -> Dict[str, Dict]:
269
+ """返回所有有数据的队伍偏差记录。"""
270
+ return _load_json(_TEAM_BIAS_PATH, {})
271
+
272
+
273
+ # ── 3b. λ 偏差:基于比分 MAE 的精化校准 ──────────────────────────────────────
274
+
275
+ def optimize_lambda_bias_from_scores(settled_records: List[Dict]) -> Dict:
276
+ """
277
+ 用实际比分优化 λ_home / λ_away 全局缩放因子,最小化总进球 MAE。
278
+
279
+ 与 optimize_lambda_bias 的区别:
280
+ - 后者用 actual/predicted 比值的均值(易受极端值拉偏)
281
+ - 本函数网格搜索 bias_h × bias_a,最小化
282
+ mean(|bias_h * lambda_h - actual_h| + |bias_a * lambda_a - actual_a|)
283
+ 需要 ≥ 8 条含实际进球记录才运行。
284
+
285
+ 返回 {"home_bias": ..., "away_bias": ..., "goals_mae": ..., "n": ..., "status": ...}
286
+ """
287
+ rows = [
288
+ r for r in settled_records
289
+ if r.get("actual_home_goals") is not None
290
+ and r.get("actual_away_goals") is not None
291
+ and r.get("lambda_home") and r.get("lambda_away")
292
+ ]
293
+ if len(rows) < 8:
294
+ return {"home_bias": 1.0, "away_bias": 1.0, "status": "not_enough_data", "n": len(rows)}
295
+
296
+ best_mae = float("inf")
297
+ best_bh, best_ba = 1.0, 1.0
298
+
299
+ for bh in [0.70, 0.75, 0.80, 0.85, 0.90, 0.95, 1.00, 1.05, 1.10, 1.15, 1.20]:
300
+ for ba in [0.70, 0.75, 0.80, 0.85, 0.90, 0.95, 1.00, 1.05, 1.10, 1.15]:
301
+ total = 0.0
302
+ for r in rows:
303
+ pred_h = float(r["lambda_home"]) * bh
304
+ pred_a = float(r["lambda_away"]) * ba
305
+ total += abs(pred_h - float(r["actual_home_goals"]))
306
+ total += abs(pred_a - float(r["actual_away_goals"]))
307
+ mae = total / (2 * len(rows))
308
+ if mae < best_mae:
309
+ best_mae, best_bh, best_ba = mae, bh, ba
310
+
311
+ return {
312
+ "home_bias": round(best_bh, 3),
313
+ "away_bias": round(best_ba, 3),
314
+ "goals_mae": round(best_mae, 4),
315
+ "n": len(rows),
316
+ "status": "optimized",
317
+ }
318
+
319
+
320
+ # ── 4. 参数读写 ───────────────────────────────────────────────────────────────
321
+
322
+ def get_calibrated_params() -> Dict:
323
+ """读取已保存的校准参数,不存在则返回默认值。"""
324
+ saved = _load_json(_PARAMS_PATH, {})
325
+ return {**_DEFAULT_PARAMS, **saved}
326
+
327
+
328
+ def save_calibration(params: Dict) -> None:
329
+ """保存校准结果(追加 timestamp)。"""
330
+ params["calibrated_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
331
+ existing = _load_json(_PARAMS_PATH, {})
332
+ existing.update(params)
333
+ _save_json(_PARAMS_PATH, existing)
334
+
335
+
336
+ def calibration_summary() -> str:
337
+ """打印可读的当前校准状态。"""
338
+ p = get_calibrated_params()
339
+ biases = get_all_team_biases()
340
+ lines = [
341
+ f"校准时间: {p.get('calibrated_at', '未校准')}",
342
+ f"攻击斜率: a1={p['a1']} a2={p['a2']}",
343
+ f"防守斜率: d1={p['d1']} d2={p['d2']}",
344
+ f"λ 偏差: 主队 ×{p['lambda_home_bias']} 客队 ×{p['lambda_away_bias']}",
345
+ f"平均 Brier: {p.get('avg_brier', 'N/A')}",
346
+ f"样本场次: {p.get('n_matches', 0)}",
347
+ "",
348
+ "队伍进球偏差 (≥3场数据):",
349
+ ]
350
+ for team, d in sorted(biases.items(), key=lambda x: -x[1].get("n", 0)):
351
+ if d.get("n", 0) >= 3:
352
+ lines.append(f" {team:<20} EMA={d['ema']:.3f} n={d['n']}")
353
+ return "\n".join(lines)
@@ -0,0 +1,234 @@
1
+ """
2
+ sports/dixon_coles.py — Dixon-Coles 足球比分预测模型
3
+ =====================================================
4
+ 实现 Dixon & Coles (1997) 经典论文的完整模型。
5
+
6
+ 核心改进 vs 纯泊松:
7
+ 1. τ (tau) 低比分修正:0-0 / 0-1 / 1-0 / 1-1 比分在现实中
8
+ 比独立泊松分布出现频率更高,DC 模型用相关性项修正。
9
+ 2. ρ (rho) 参数:控制修正强度,典型值 -0.13 ~ -0.08。
10
+ 3. 时间权重:近期比赛权重更高(可选)。
11
+
12
+ τ 修正矩阵:
13
+ τ(x, y, λ, μ, ρ) =
14
+ 1 - λμρ if x=0, y=0
15
+ 1 + λρ if x=0, y=1
16
+ 1 + μρ if x=1, y=0
17
+ 1 - ρ if x=1, y=1
18
+ 1 otherwise
19
+
20
+ 参考文献: Dixon, M.J. & Coles, S.G. (1997).
21
+ "Modelling Association Football Scores and Inefficiencies in the
22
+ Football Betting Market." Applied Statistics, 46(2), 265-280.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import math
28
+ from typing import Dict, List, Optional, Tuple
29
+
30
+
31
+ def _poisson_pmf(k: int, lam: float) -> float:
32
+ if lam <= 0:
33
+ return 1.0 if k == 0 else 0.0
34
+ return (lam ** k) * math.exp(-lam) / math.factorial(k)
35
+
36
+
37
+ def _nb_pmf(k: int, mu: float, r: float) -> float:
38
+ """
39
+ 负二项分布 PMF,均值 mu,离散参数 r。
40
+
41
+ 方差 = mu + mu²/r > mu(泊松)
42
+ 当 r→∞ 时退化为泊松分布。
43
+ r 越小,尾部越重(大比分概率越高)。
44
+ """
45
+ if mu <= 0:
46
+ return 1.0 if k == 0 else 0.0
47
+ p = r / (r + mu)
48
+ log_pmf = (
49
+ math.lgamma(k + r) - math.lgamma(r) - math.lgamma(k + 1)
50
+ + r * math.log(p)
51
+ + k * math.log(1.0 - p)
52
+ )
53
+ return math.exp(log_pmf)
54
+
55
+
56
+ def _auto_dispersion(elo_diff: float) -> float:
57
+ """
58
+ 根据 Elo 差距自动选择负二项离散参数 r。
59
+ 差距越大 → r 越小 → 尾部越重 → 大比分概率更高。
60
+
61
+ elo_diff=0 → r=80 (近似泊松)
62
+ elo_diff=200 → r=25
63
+ elo_diff=387 → r=13 (德国 vs 库拉索)
64
+ elo_diff=500 → r=10
65
+ """
66
+ r = max(6.0, 80.0 / (1.0 + abs(elo_diff) / 100.0))
67
+ return round(r, 1)
68
+
69
+
70
+ def tau_correction(
71
+ x: int,
72
+ y: int,
73
+ lambda_home: float,
74
+ lambda_away: float,
75
+ rho: float = -0.10,
76
+ ) -> float:
77
+ """
78
+ Dixon-Coles τ 修正因子。
79
+
80
+ rho < 0 意味着低比分(含 0 的)出现频率比独立泊松预测的更低,
81
+ 但 0-0 / 0-1 / 1-0 / 1-1 特别地有正相关(团队防守同进攻 jointly low)。
82
+ """
83
+ if x == 0 and y == 0:
84
+ return 1.0 - lambda_home * lambda_away * rho
85
+ if x == 0 and y == 1:
86
+ return 1.0 + lambda_home * rho
87
+ if x == 1 and y == 0:
88
+ return 1.0 + lambda_away * rho
89
+ if x == 1 and y == 1:
90
+ return 1.0 - rho
91
+ return 1.0
92
+
93
+
94
+ def predict_scoreline_matrix(
95
+ lambda_home: float,
96
+ lambda_away: float,
97
+ rho: float = -0.10,
98
+ max_goals: int = 10,
99
+ elo_diff: float = 0.0,
100
+ ) -> Dict[Tuple[int, int], float]:
101
+ """
102
+ 计算所有比分的概率矩阵。
103
+
104
+ 当 |elo_diff| > 150 时自动切换到负二项分布以处理大比分场景,
105
+ 否则使用标准泊松分布(DC 修正)。
106
+
107
+ Returns:
108
+ dict: {(home_goals, away_goals): probability},归一化为 1.0
109
+ """
110
+ use_nb = abs(elo_diff) > 150
111
+ if use_nb:
112
+ r_home = _auto_dispersion(elo_diff)
113
+ r_away = _auto_dispersion(-elo_diff)
114
+
115
+ raw: Dict[Tuple[int, int], float] = {}
116
+ for hg in range(max_goals):
117
+ ph = _nb_pmf(hg, lambda_home, r_home) if use_nb else _poisson_pmf(hg, lambda_home)
118
+ for ag in range(max_goals):
119
+ pa = _nb_pmf(ag, lambda_away, r_away) if use_nb else _poisson_pmf(ag, lambda_away)
120
+ tau = tau_correction(hg, ag, lambda_home, lambda_away, rho)
121
+ raw[(hg, ag)] = ph * pa * tau
122
+
123
+ total = sum(raw.values())
124
+ if total <= 0:
125
+ return raw
126
+ return {k: v / total for k, v in raw.items()}
127
+
128
+
129
+ def compute_match_probabilities(
130
+ lambda_home: float,
131
+ lambda_away: float,
132
+ rho: float = -0.10,
133
+ max_goals: int = 10,
134
+ elo_diff: float = 0.0,
135
+ ) -> Dict:
136
+ """
137
+ 从期望进球计算比赛结果概率(含 D-C 修正)。
138
+
139
+ Returns:
140
+ {
141
+ "home_win": float,
142
+ "draw": float,
143
+ "away_win": float,
144
+ "btts": float,
145
+ "over_2_5": float,
146
+ "top_scorelines": [...],
147
+ "score_matrix": {...},
148
+ }
149
+ """
150
+ matrix = predict_scoreline_matrix(lambda_home, lambda_away, rho, max_goals, elo_diff)
151
+
152
+ home_win = draw = away_win = 0.0
153
+ btts = 0.0
154
+ over_2_5 = 0.0
155
+
156
+ for (hg, ag), p in matrix.items():
157
+ if hg > ag:
158
+ home_win += p
159
+ elif hg == ag:
160
+ draw += p
161
+ else:
162
+ away_win += p
163
+ if hg > 0 and ag > 0:
164
+ btts += p
165
+ if hg + ag > 2:
166
+ over_2_5 += p
167
+
168
+ top_scores = sorted(matrix.items(), key=lambda x: -x[1])[:8]
169
+
170
+ return {
171
+ "home_win": round(home_win, 4),
172
+ "draw": round(draw, 4),
173
+ "away_win": round(away_win, 4),
174
+ "btts": round(btts, 4),
175
+ "over_2_5": round(over_2_5, 4),
176
+ "rho": rho,
177
+ "top_scorelines": [
178
+ {"score": f"{hg}-{ag}", "prob": round(p * 100, 2)}
179
+ for (hg, ag), p in top_scores
180
+ ],
181
+ "implied_odds": {
182
+ "home": round(1 / home_win, 2) if home_win > 0.01 else 99,
183
+ "draw": round(1 / draw, 2) if draw > 0.01 else 99,
184
+ "away": round(1 / away_win, 2) if away_win > 0.01 else 99,
185
+ },
186
+ }
187
+
188
+
189
+ def estimate_rho_from_results(match_results: List[Tuple[int, int]]) -> float:
190
+ """
191
+ 从历史赛果估计最优 ρ 参数(简化版 MLE 搜索)。
192
+
193
+ match_results: [(home_goals, away_goals), ...]
194
+ 返回: 最优 ρ(范围 -0.3 ~ 0.0)
195
+ """
196
+ if len(match_results) < 20:
197
+ return -0.10 # 数据不足,用默认值
198
+
199
+ def log_likelihood(rho: float) -> float:
200
+ ll = 0.0
201
+ for hg, ag in match_results:
202
+ # 用平均进球作简化 λ
203
+ lh = sum(h for h, _ in match_results) / len(match_results)
204
+ la = sum(a for _, a in match_results) / len(match_results)
205
+ p_h = _poisson_pmf(hg, lh)
206
+ p_a = _poisson_pmf(ag, la)
207
+ tau = tau_correction(hg, ag, lh, la, rho)
208
+ p = p_h * p_a * tau
209
+ if p > 0:
210
+ ll += math.log(p)
211
+ return ll
212
+
213
+ best_rho = -0.10
214
+ best_ll = float("-inf")
215
+ for rho_int in range(-30, 1, 2):
216
+ rho = rho_int / 100.0
217
+ ll = log_likelihood(rho)
218
+ if ll > best_ll:
219
+ best_ll = ll
220
+ best_rho = rho
221
+
222
+ return round(best_rho, 3)
223
+
224
+
225
+ def format_dc_result(result: Dict, home_cn: str, away_cn: str) -> str:
226
+ """格式化 Dixon-Coles 预测结果为 CLI 输出文本。"""
227
+ lines = [
228
+ f" 主队 {home_cn:<12} 获胜: {result['home_win']*100:5.1f}% 赔率: {result['implied_odds']['home']:5.2f}",
229
+ f" 平局 {result['draw']*100:5.1f}% 赔率: {result['implied_odds']['draw']:5.2f}",
230
+ f" 客队 {away_cn:<12} 获胜: {result['away_win']*100:5.1f}% 赔率: {result['implied_odds']['away']:5.2f}",
231
+ "",
232
+ f" 双方均进球 (BTTS): {result['btts']*100:.1f}% 大于2.5球: {result['over_2_5']*100:.1f}%",
233
+ ]
234
+ return "\n".join(lines)