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,299 @@
1
+ """
2
+ sports/elo.py — World Football Elo Rating System
3
+ =================================================
4
+ 动态 Elo 评分系统,替代静态 FIFA 排名表。
5
+
6
+ 特性:
7
+ - 初始评分基于 FIFA 排名(幂律映射,斜率更陡)
8
+ - 每场赛果后自动更新
9
+ - K 因子按赛事重要性调整(世界杯 > 洲际杯 > 友谊赛)
10
+ - 主场优势 +100 Elo 加成(中性场地为 0)
11
+ - 支持从本地 JSON 持久化加载/保存
12
+
13
+ Elo 公式:
14
+ E = 1 / (1 + 10^((Rb - Ra - home_adv) / 400))
15
+ R' = R + K * (W - E)
16
+
17
+ 参考: World Football Elo Ratings (eloratings.net) 方法论
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import math
24
+ import os
25
+ from pathlib import Path
26
+ from typing import Dict, Optional, Tuple
27
+
28
+ # ── 默认初始 Elo(基于 FIFA 排名,幂律映射)──────────────────────────────────
29
+ # 公式: elo = BASE - SCALE * (ranking ^ POWER)
30
+ # 经验参数:阿根廷#1→2050, 德国#11→1850, 库拉索#70→1520, 菲律宾#134→1280
31
+ _ELO_BASE = 2100.0
32
+ _ELO_SCALE = 50.0
33
+ _ELO_POWER = 0.58
34
+
35
+ _DEFAULT_ELO = 1500.0 # 未知队伍
36
+
37
+
38
+ def ranking_to_elo(ranking: int) -> float:
39
+ """FIFA 排名 → 初始 Elo 评分(幂律映射)。"""
40
+ if ranking <= 0:
41
+ return _DEFAULT_ELO
42
+ raw = _ELO_BASE - _ELO_SCALE * (ranking ** _ELO_POWER)
43
+ return max(900.0, round(raw, 1))
44
+
45
+
46
+ # K 因子(赛事权重)
47
+ _K_FACTORS: Dict[str, float] = {
48
+ "wc_final": 60,
49
+ "wc_semifinal": 56,
50
+ "wc_quarterfinal":52,
51
+ "wc_r16": 48,
52
+ "wc_group": 40,
53
+ "confederation": 35,
54
+ "euro_final": 40,
55
+ "euro": 35,
56
+ "copa_america": 35,
57
+ "afcon": 30,
58
+ "qualifier": 25,
59
+ "friendly": 15,
60
+ "default": 20,
61
+ }
62
+
63
+ # 内置 FIFA 排名表(覆盖主要队伍)
64
+ _FIFA_RANKING: Dict[str, int] = {
65
+ "argentina": 1, "france": 2, "england": 3, "brazil": 4,
66
+ "portugal": 5, "belgium": 6, "spain": 7, "netherlands": 8,
67
+ "croatia": 9, "italy": 10, "germany": 11, "colombia": 12,
68
+ "united states": 13, "usa": 13, "mexico": 14, "morocco": 16,
69
+ "uruguay": 20, "denmark": 22, "switzerland": 23, "serbia": 24,
70
+ "austria": 25, "norway": 26, "ukraine": 27, "turkey": 28,
71
+ "senegal": 19, "japan": 18, "iran": 21, "south korea": 23,
72
+ "egypt": 33, "nigeria": 35, "cameroon": 37, "ghana": 60,
73
+ "australia": 23, "new zealand": 90, "canada": 47,
74
+ "costa rica": 53, "panama": 55, "jamaica": 60,
75
+ "curacao": 70, "haiti": 80, "trinidad": 85,
76
+ "china": 87, "thailand": 111, "philippines": 134,
77
+ "india": 124, "vietnam": 95, "indonesia": 130,
78
+ "saudi arabia": 58, "iraq": 62, "uae": 68, "bahrain": 80,
79
+ "romania": 45, "slovakia": 48, "hungary": 50,
80
+ "greece": 46, "albania": 66, "north macedonia": 70,
81
+ "sweden": 30, "finland": 43, "russia": 26,
82
+ "bolivia": 79, "venezuela": 75, "paraguay": 65,
83
+ "chile": 55, "ecuador": 45, "peru": 69,
84
+ "tunisia": 30, "algeria": 32, "mali": 56, "guinea": 64,
85
+ "ivory coast": 51, "south africa": 70, "zimbabwe": 120,
86
+ }
87
+
88
+
89
+ class EloRatingSystem:
90
+ """
91
+ World Football Elo 评分引擎。
92
+
93
+ 用法:
94
+ elo = EloRatingSystem()
95
+ p = elo.win_probability("germany", "curacao")
96
+ # → {'home_win': 0.74, 'draw': 0.16, 'away_win': 0.10}
97
+ """
98
+
99
+ _STORE_PATH = Path.home() / ".arthera" / "football_elo.json"
100
+
101
+ def __init__(self, load_from_disk: bool = True):
102
+ self._ratings: Dict[str, float] = {}
103
+ self._match_log: list = []
104
+ if load_from_disk:
105
+ self._load()
106
+ # 如果没有持久化数据,用 FIFA 排名初始化
107
+ if not self._ratings:
108
+ self._init_from_rankings()
109
+
110
+ # ── 初始化 ────────────────────────────────────────────────────────────────
111
+
112
+ def _init_from_rankings(self) -> None:
113
+ for team, rank in _FIFA_RANKING.items():
114
+ self._ratings[team] = ranking_to_elo(rank)
115
+
116
+ # ── 核心 Elo 计算 ─────────────────────────────────────────────────────────
117
+
118
+ def get_rating(self, team: str) -> float:
119
+ key = team.lower().strip()
120
+ if key in self._ratings:
121
+ return self._ratings[key]
122
+ # 尝试 FIFA 排名推算
123
+ for stored_key, rank in _FIFA_RANKING.items():
124
+ if stored_key in key or key in stored_key:
125
+ return ranking_to_elo(rank)
126
+ return _DEFAULT_ELO
127
+
128
+ def expected_score(
129
+ self,
130
+ rating_a: float,
131
+ rating_b: float,
132
+ home_advantage: float = 100.0,
133
+ ) -> float:
134
+ """E_A = 1 / (1 + 10^((Rb - Ra - home_adv) / 400))"""
135
+ return 1.0 / (1.0 + 10.0 ** ((rating_b - rating_a - home_advantage) / 400.0))
136
+
137
+ def update(
138
+ self,
139
+ home_team: str,
140
+ away_team: str,
141
+ home_goals: int,
142
+ away_goals: int,
143
+ match_type: str = "default",
144
+ neutral_venue: bool = False,
145
+ ) -> Tuple[float, float]:
146
+ """
147
+ 用一场赛果更新双方 Elo 评分。
148
+ 返回 (home_delta, away_delta)。
149
+ """
150
+ k = _K_FACTORS.get(match_type, _K_FACTORS["default"])
151
+ home_adv = 0.0 if neutral_venue else 100.0
152
+
153
+ ra = self.get_rating(home_team)
154
+ rb = self.get_rating(away_team)
155
+
156
+ ea = self.expected_score(ra, rb, home_adv)
157
+ eb = 1.0 - ea
158
+
159
+ if home_goals > away_goals:
160
+ wa, wb = 1.0, 0.0
161
+ elif home_goals == away_goals:
162
+ wa, wb = 0.5, 0.5
163
+ else:
164
+ wa, wb = 0.0, 1.0
165
+
166
+ # 进球差加成(World Football Elo 标准公式)
167
+ # GD=1→×1.0, GD=2→×1.5, GD=3→×1.75, GD=6→×2.125, 上限 2.5
168
+ goal_diff = abs(home_goals - away_goals)
169
+ if goal_diff <= 1:
170
+ gd_mult = 1.0
171
+ elif goal_diff == 2:
172
+ gd_mult = 1.5
173
+ else:
174
+ gd_mult = min(2.5, (11 + goal_diff) / 8.0)
175
+
176
+ da = round(k * gd_mult * (wa - ea), 2)
177
+ db = round(k * gd_mult * (wb - eb), 2)
178
+
179
+ h_key = home_team.lower().strip()
180
+ a_key = away_team.lower().strip()
181
+ self._ratings[h_key] = round(ra + da, 1)
182
+ self._ratings[a_key] = round(rb + db, 1)
183
+
184
+ self._match_log.append({
185
+ "home": h_key, "away": a_key,
186
+ "score": f"{home_goals}-{away_goals}",
187
+ "type": match_type, "da": da, "db": db,
188
+ })
189
+ return da, db
190
+
191
+ def win_probability(
192
+ self,
193
+ home_team: str,
194
+ away_team: str,
195
+ neutral_venue: bool = True,
196
+ ) -> Dict[str, float]:
197
+ """
198
+ 基于 Elo 差距计算胜/平/负概率。
199
+ 使用 Dixon & Robinson (1998) 的转换公式。
200
+ """
201
+ home_adv = 0.0 if neutral_venue else 100.0
202
+ ra = self.get_rating(home_team)
203
+ rb = self.get_rating(away_team)
204
+
205
+ e_home = self.expected_score(ra, rb, home_adv)
206
+ diff = ra + home_adv - rb
207
+
208
+ # 平局概率:差距越大平局越少
209
+ # 经验公式:draw_prob ≈ 0.30 * exp(-|diff| / 720)
210
+ draw_base = 0.295 * math.exp(-abs(diff) / 720.0)
211
+ draw_base = max(0.04, min(draw_base, 0.295))
212
+
213
+ home_win = e_home * (1.0 - draw_base / 2)
214
+ away_win = (1.0 - e_home) * (1.0 - draw_base / 2)
215
+ draw = 1.0 - home_win - away_win
216
+ draw = max(0.04, draw)
217
+
218
+ total = home_win + draw + away_win
219
+ return {
220
+ "home_win": round(home_win / total, 4),
221
+ "draw": round(draw / total, 4),
222
+ "away_win": round(away_win / total, 4),
223
+ "home_elo": round(ra, 0),
224
+ "away_elo": round(rb, 0),
225
+ "elo_diff": round(ra + home_adv - rb, 0),
226
+ }
227
+
228
+ def get_attack_defense(self, team: str, base_avg: float = 1.35) -> Dict[str, float]:
229
+ """
230
+ 从 Elo 评分推导 attack / defense 参数供泊松模型使用。
231
+ 斜率参数优先从 calibrator 读取(自动优化),不存在则用默认值。
232
+
233
+ 标定默认目标:
234
+ Elo 2050 (阿根廷) → attack≈2.50, defense≈0.42
235
+ Elo 1912 (德国) → attack≈3.01, defense≈0.42
236
+ Elo 1800 (日本) → attack≈1.75, defense≈0.65
237
+ Elo 1500 (平均) → attack≈1.10, defense≈0.95
238
+ Elo 1200 (弱队) → attack≈0.62, defense≈1.22
239
+ """
240
+ elo = self.get_rating(team)
241
+ si = (elo - 1500) / 400.0
242
+
243
+ # 读取自动校准斜率(若无则用默认值)
244
+ a1, a2, d1, d2 = 1.05, 0.35, -0.42, -0.10
245
+ try:
246
+ from .calibrator import get_calibrated_params
247
+ p = get_calibrated_params()
248
+ a1 = p.get("a1", a1)
249
+ a2 = p.get("a2", a2)
250
+ d1 = p.get("d1", d1)
251
+ d2 = p.get("d2", d2)
252
+ except Exception:
253
+ pass
254
+
255
+ attack = 1.10 + si * a1 + max(0, si) * si * a2
256
+ defense = 0.95 + si * d1 + max(0, si) * si * d2
257
+
258
+ attack = max(0.45, min(attack, 2.60))
259
+ defense = max(0.40, min(defense, 1.25))
260
+ return {
261
+ "attack": round(attack, 3),
262
+ "defense": round(defense, 3),
263
+ "elo": round(elo, 0),
264
+ }
265
+
266
+ # ── 持久化 ────────────────────────────────────────────────────────────────
267
+
268
+ def save(self) -> None:
269
+ try:
270
+ self._STORE_PATH.parent.mkdir(parents=True, exist_ok=True)
271
+ with open(self._STORE_PATH, "w", encoding="utf-8") as f:
272
+ json.dump({
273
+ "ratings": self._ratings,
274
+ "match_count": len(self._match_log),
275
+ }, f, ensure_ascii=False, indent=2)
276
+ except Exception:
277
+ pass
278
+
279
+ def _load(self) -> None:
280
+ try:
281
+ if self._STORE_PATH.exists():
282
+ data = json.loads(self._STORE_PATH.read_text(encoding="utf-8"))
283
+ self._ratings = data.get("ratings", {})
284
+ except Exception:
285
+ self._ratings = {}
286
+
287
+ def top_n(self, n: int = 10) -> list:
288
+ """返回评分最高的 n 支队伍。"""
289
+ return sorted(self._ratings.items(), key=lambda x: -x[1])[:n]
290
+
291
+
292
+ _elo_instance: Optional[EloRatingSystem] = None
293
+
294
+
295
+ def get_elo() -> EloRatingSystem:
296
+ global _elo_instance
297
+ if _elo_instance is None:
298
+ _elo_instance = EloRatingSystem()
299
+ return _elo_instance
@@ -0,0 +1,188 @@
1
+ """
2
+ sports/form.py — 球队近期状态分析
3
+ ===================================
4
+ 用指数衰减权重分析近 N 场比赛,
5
+ 动态调整攻守参数,捕捉球队当前状态。
6
+
7
+ 状态因子 (form_factor) 定义:
8
+ - 近5场全胜 → 1.12(上调 12%)
9
+ - 近5场全败 → 0.88(下调 12%)
10
+ - 近5场持平 → 1.00
11
+ - 中间值线性插值
12
+
13
+ 影响方式:
14
+ attack_adjusted = attack_base * form_factor_attack
15
+ defense_adjusted = defense_base * form_factor_defense
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import math
21
+ from typing import Dict, List, Optional, Tuple
22
+
23
+
24
+ def _decay_weight(match_index: int, total: int, decay: float = 0.85) -> float:
25
+ """
26
+ 越近期的比赛权重越高。
27
+ match_index=0 是最新一场,match_index=total-1 是最早一场。
28
+ """
29
+ return decay ** match_index
30
+
31
+
32
+ def analyze_form(
33
+ results: List[Dict],
34
+ n: int = 6,
35
+ decay: float = 0.85,
36
+ ) -> Dict:
37
+ """
38
+ 分析球队近期状态。
39
+
40
+ Args:
41
+ results: 近期比赛结果列表,每项格式:
42
+ {"is_win": bool, "is_draw": bool, "goals_for": int,
43
+ "goals_against": int, "is_home": bool}
44
+ 按时间倒序(最新在前)。
45
+ n: 分析最近 n 场(默认6场)。
46
+ decay: 指数衰减系数(0.85 → 最新权重是最早的 0.85^5 ≈ 44%)。
47
+
48
+ Returns:
49
+ {
50
+ "form_string": "WWDLW",
51
+ "weighted_win_rate": float,
52
+ "form_factor_attack": float,
53
+ "form_factor_defense": float,
54
+ "avg_goals_for": float,
55
+ "avg_goals_against": float,
56
+ "momentum": str, # "rising" | "declining" | "stable"
57
+ }
58
+ """
59
+ recent = results[:n]
60
+ if not recent:
61
+ return _neutral_form()
62
+
63
+ total_weight = 0.0
64
+ weighted_wins = 0.0
65
+ weighted_draws = 0.0
66
+ weighted_gf = 0.0
67
+ weighted_ga = 0.0
68
+ form_chars = []
69
+
70
+ for i, m in enumerate(recent):
71
+ w = _decay_weight(i, len(recent), decay)
72
+ total_weight += w
73
+ if m.get("is_win"):
74
+ weighted_wins += w
75
+ form_chars.append("W")
76
+ elif m.get("is_draw"):
77
+ weighted_draws += w
78
+ form_chars.append("D")
79
+ else:
80
+ form_chars.append("L")
81
+ weighted_gf += w * m.get("goals_for", 1.0)
82
+ weighted_ga += w * m.get("goals_against", 1.0)
83
+
84
+ if total_weight <= 0:
85
+ return _neutral_form()
86
+
87
+ w_win_rate = weighted_wins / total_weight
88
+ avg_gf = weighted_gf / total_weight
89
+ avg_ga = weighted_ga / total_weight
90
+
91
+ # 攻击状态因子:进球越多上调越多
92
+ # 基准:1.35进球/场=1.0,每±0.35调整±0.08
93
+ gf_baseline = 1.35
94
+ form_attack = 1.0 + (avg_gf - gf_baseline) / gf_baseline * 0.12
95
+ form_attack = max(0.82, min(form_attack, 1.18))
96
+
97
+ # 防守状态因子:失球越少上调越多
98
+ # 注意:defense 参数越小代表越强(对手进球少),失球少→factor下调(防守更好)
99
+ ga_baseline = 1.20
100
+ form_defense = 1.0 - (avg_ga - ga_baseline) / ga_baseline * 0.10
101
+ form_defense = max(0.85, min(form_defense, 1.15))
102
+
103
+ # 势头分析(对比前半段 vs 后半段胜率)
104
+ half = max(1, len(recent) // 2)
105
+ recent_half_wins = sum(1 for m in recent[:half] if m.get("is_win"))
106
+ earlier_half_wins = sum(1 for m in recent[half:] if m.get("is_win"))
107
+ if recent_half_wins > earlier_half_wins:
108
+ momentum = "rising"
109
+ elif recent_half_wins < earlier_half_wins:
110
+ momentum = "declining"
111
+ else:
112
+ momentum = "stable"
113
+
114
+ return {
115
+ "form_string": "".join(form_chars),
116
+ "weighted_win_rate": round(w_win_rate, 3),
117
+ "form_factor_attack": round(form_attack, 3),
118
+ "form_factor_defense": round(form_defense, 3),
119
+ "avg_goals_for": round(avg_gf, 2),
120
+ "avg_goals_against": round(avg_ga, 2),
121
+ "momentum": momentum,
122
+ "matches_analyzed": len(recent),
123
+ }
124
+
125
+
126
+ def _neutral_form() -> Dict:
127
+ return {
128
+ "form_string": "?????",
129
+ "weighted_win_rate": 0.5,
130
+ "form_factor_attack": 1.0,
131
+ "form_factor_defense": 1.0,
132
+ "avg_goals_for": 1.35,
133
+ "avg_goals_against": 1.20,
134
+ "momentum": "stable",
135
+ "matches_analyzed": 0,
136
+ }
137
+
138
+
139
+ def parse_api_results(matches: List[Dict], team_name: str) -> List[Dict]:
140
+ """
141
+ 将 football-data.org API 返回的比赛记录转换为 form 分析格式。
142
+
143
+ team_name: 用于判断主客队角色(小写)
144
+ """
145
+ parsed = []
146
+ for m in matches:
147
+ ft = m.get("score", {}).get("fullTime", {})
148
+ home_team = (m.get("homeTeam") or {}).get("name", "").lower()
149
+ away_team = (m.get("awayTeam") or {}).get("name", "").lower()
150
+ home_goals = ft.get("home")
151
+ away_goals = ft.get("away")
152
+ if home_goals is None or away_goals is None:
153
+ continue
154
+
155
+ team_low = team_name.lower()
156
+ is_home = team_low in home_team
157
+
158
+ if is_home:
159
+ gf, ga = int(home_goals), int(away_goals)
160
+ else:
161
+ gf, ga = int(away_goals), int(home_goals)
162
+
163
+ is_win = gf > ga
164
+ is_draw = gf == ga
165
+
166
+ parsed.append({
167
+ "is_win": is_win,
168
+ "is_draw": is_draw,
169
+ "is_home": is_home,
170
+ "goals_for": gf,
171
+ "goals_against": ga,
172
+ "date": m.get("utcDate", ""),
173
+ })
174
+
175
+ # 按日期倒序(最新在前)
176
+ parsed.sort(key=lambda x: x.get("date", ""), reverse=True)
177
+ return parsed
178
+
179
+
180
+ def form_bar(form_string: str) -> str:
181
+ """近期状态可视化条。"""
182
+ _MAP = {"W": "●", "D": "◑", "L": "○"}
183
+ return " ".join(_MAP.get(c, "?") for c in form_string[:6])
184
+
185
+
186
+ def momentum_label(momentum: str) -> str:
187
+ _LABELS = {"rising": "↑ 上升", "declining": "↓ 下滑", "stable": "→ 平稳"}
188
+ return _LABELS.get(momentum, "?")
@@ -0,0 +1,195 @@
1
+ """
2
+ sports/h2h.py — 历史交锋 (Head-to-Head) 分析与调整
3
+ ===================================================
4
+ 分析两队历史对阵记录,提供调整系数。
5
+
6
+ 理论依据:
7
+ 某些队伍存在"心理/战术克制"效应,超出 Elo 差距能解释的范围。
8
+ H2H 调整系数在实际市场定价中通常权重约 8-12%。
9
+
10
+ 调整范围:
11
+ h2h_advantage: -0.08 ~ +0.08(相对于主队/队1)
12
+ 代入预测公式:lambda_home *= (1 + h2h_advantage)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from typing import Dict, List, Optional, Tuple
19
+
20
+
21
+ def analyze_h2h(
22
+ matches: List[Dict],
23
+ team1: str,
24
+ team2: str,
25
+ max_matches: int = 10,
26
+ decay: float = 0.90,
27
+ ) -> Dict:
28
+ """
29
+ 分析两队历史交锋记录。
30
+
31
+ Args:
32
+ matches: 比赛记录列表(football-data.org 格式)
33
+ team1: 队1名称(通常是"主队"或查询方)
34
+ team2: 队2名称
35
+ max_matches: 最多分析 N 场
36
+ decay: 时间衰减系数
37
+
38
+ Returns:
39
+ {
40
+ "total_matches": int,
41
+ "team1_wins": int,
42
+ "draws": int,
43
+ "team2_wins": int,
44
+ "team1_goals": int,
45
+ "team2_goals": int,
46
+ "h2h_advantage": float, # team1 相对优势 (-0.08 ~ +0.08)
47
+ "win_rate_team1": float,
48
+ "dominant_team": str,
49
+ "summary": str,
50
+ }
51
+ """
52
+ if not matches:
53
+ return _neutral_h2h(team1, team2)
54
+
55
+ t1_low = team1.lower()
56
+ t2_low = team2.lower()
57
+
58
+ results = []
59
+ for m in matches[:max_matches]:
60
+ ht = (m.get("homeTeam") or {}).get("name", "").lower()
61
+ at = (m.get("awayTeam") or {}).get("name", "").lower()
62
+ ft = m.get("score", {}).get("fullTime", {})
63
+ hg = ft.get("home")
64
+ ag = ft.get("away")
65
+ if hg is None or ag is None:
66
+ continue
67
+
68
+ t1_is_home = t1_low in ht
69
+ if t1_is_home:
70
+ t1g, t2g = int(hg), int(ag)
71
+ else:
72
+ t1g, t2g = int(ag), int(hg)
73
+
74
+ results.append({
75
+ "t1_goals": t1g, "t2_goals": t2g,
76
+ "date": m.get("utcDate", ""),
77
+ })
78
+
79
+ if not results:
80
+ return _neutral_h2h(team1, team2)
81
+
82
+ results.sort(key=lambda x: x.get("date", ""), reverse=True)
83
+
84
+ total_w = 0.0
85
+ t1_win_w = 0.0
86
+ draw_w = 0.0
87
+ t2_win_w = 0.0
88
+ t1_goals = 0
89
+ t2_goals = 0
90
+ t1_wins = draws = t2_wins = 0
91
+
92
+ for i, r in enumerate(results):
93
+ w = decay ** i
94
+ total_w += w
95
+ t1g, t2g = r["t1_goals"], r["t2_goals"]
96
+ t1_goals += t1g
97
+ t2_goals += t2g
98
+ if t1g > t2g:
99
+ t1_win_w += w
100
+ t1_wins += 1
101
+ elif t1g == t2g:
102
+ draw_w += w
103
+ draws += 1
104
+ else:
105
+ t2_win_w += w
106
+ t2_wins += 1
107
+
108
+ if total_w <= 0:
109
+ return _neutral_h2h(team1, team2)
110
+
111
+ t1_wr = t1_win_w / total_w
112
+ t2_wr = t2_win_w / total_w
113
+
114
+ # H2H 优势调整系数:偏离0.5的部分映射到 ±0.08
115
+ # t1_wr=1.0 → +0.08, t1_wr=0.5 → 0.0, t1_wr=0.0 → -0.08
116
+ h2h_adv = (t1_wr - 0.5) * 0.16
117
+ h2h_adv = max(-0.08, min(h2h_adv, 0.08))
118
+
119
+ n = len(results)
120
+ if t1_wins > t2_wins + 1:
121
+ dominant = team1
122
+ elif t2_wins > t1_wins + 1:
123
+ dominant = team2
124
+ else:
125
+ dominant = "平分秋色"
126
+
127
+ summary = (
128
+ f"{team1} {t1_wins}胜 {draws}平 {t2_wins}负 "
129
+ f"({t1_goals}:{t2_goals}) "
130
+ f"近{n}场"
131
+ )
132
+
133
+ return {
134
+ "total_matches": n,
135
+ "team1_wins": t1_wins,
136
+ "draws": draws,
137
+ "team2_wins": t2_wins,
138
+ "team1_goals": t1_goals,
139
+ "team2_goals": t2_goals,
140
+ "h2h_advantage": round(h2h_adv, 4),
141
+ "win_rate_team1": round(t1_wr, 3),
142
+ "dominant_team": dominant,
143
+ "summary": summary,
144
+ }
145
+
146
+
147
+ def _neutral_h2h(team1: str, team2: str) -> Dict:
148
+ return {
149
+ "total_matches": 0,
150
+ "team1_wins": 0,
151
+ "draws": 0,
152
+ "team2_wins": 0,
153
+ "team1_goals": 0,
154
+ "team2_goals": 0,
155
+ "h2h_advantage": 0.0,
156
+ "win_rate_team1": 0.5,
157
+ "dominant_team": "数据不足",
158
+ "summary": f"{team1} vs {team2} — 无历史数据",
159
+ }
160
+
161
+
162
+ def fetch_h2h_from_api(
163
+ home_team: str,
164
+ away_team: str,
165
+ api_get_fn,
166
+ limit: int = 10,
167
+ ) -> List[Dict]:
168
+ """
169
+ 从 football-data.org API 获取历史交锋数据(需要 API key)。
170
+ api_get_fn: 封装好的 GET 函数,如 football_data_client._get()
171
+ """
172
+ try:
173
+ data = api_get_fn("/teams", {"name": home_team})
174
+ if not data:
175
+ return []
176
+ teams = data.get("teams", [])
177
+ if not teams:
178
+ return []
179
+ team_id = teams[0]["id"]
180
+
181
+ h2h_data = api_get_fn(f"/teams/{team_id}/matches", {
182
+ "competitions": "WC,CL,PL,BL1,SA,FL1,PD",
183
+ "limit": str(limit),
184
+ })
185
+ if not h2h_data:
186
+ return []
187
+ matches = h2h_data.get("matches", [])
188
+ at_low = away_team.lower()
189
+ return [
190
+ m for m in matches
191
+ if at_low in (m.get("homeTeam") or {}).get("name", "").lower()
192
+ or at_low in (m.get("awayTeam") or {}).get("name", "").lower()
193
+ ]
194
+ except Exception:
195
+ return []