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,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
+ ]