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
datasources/router.py ADDED
@@ -0,0 +1,221 @@
1
+ """
2
+ datasources/router.py — 数据源路由器
3
+ =====================================
4
+ 读取 ~/.aria/datasources.yaml 配置,按优先级依次尝试每个数据源,
5
+ 首个成功的结果直接返回;所有失败则返回 None。
6
+
7
+ 配置示例 (~/.aria/datasources.yaml):
8
+ a_shares:
9
+ - akshare
10
+ - tushare
11
+ us_stocks:
12
+ - yfinance
13
+ - finnhub
14
+ crypto:
15
+ - ccxt/binance
16
+ - yfinance
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ import threading
23
+ from pathlib import Path
24
+ from typing import Any, Dict, List, Optional, Type
25
+
26
+ from .base import (
27
+ BaseDataSource, QuoteResult, HistoryResult, FundamentalsResult, _detect_market
28
+ )
29
+ from .sources import (
30
+ AkshareSource, YFinanceSource, TushareSource,
31
+ FREDSource, EDGARSource, AlphaVantageSource, WorldBankSource,
32
+ FinnhubSource, WebScraperSource,
33
+ )
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ # ── 数据源目录 ────────────────────────────────────────────────────────────────
38
+ _SOURCE_REGISTRY: Dict[str, Type[BaseDataSource]] = {
39
+ "akshare": AkshareSource,
40
+ "yfinance": YFinanceSource,
41
+ "finnhub": FinnhubSource,
42
+ "tushare": TushareSource,
43
+ "fred": FREDSource,
44
+ "edgar": EDGARSource,
45
+ "alpha_vantage": AlphaVantageSource,
46
+ "world_bank": WorldBankSource,
47
+ "web_scraper": WebScraperSource,
48
+ }
49
+
50
+
51
+ def register_datasource(name: str, cls: Type[BaseDataSource]) -> None:
52
+ """注册自定义数据源(供插件/用户扩展)"""
53
+ _SOURCE_REGISTRY[name.lower()] = cls
54
+ logger.info(f"✓ 注册自定义数据源: {name}")
55
+
56
+
57
+ # ── 默认优先级链 ──────────────────────────────────────────────────────────────
58
+ _DEFAULT_CHAINS: Dict[str, List[str]] = {
59
+ "a_share": ["tushare", "akshare"],
60
+ "us": ["yfinance", "finnhub", "alpha_vantage", "edgar"],
61
+ "hk": ["yfinance", "finnhub", "akshare"],
62
+ "crypto": ["yfinance"],
63
+ "macro": ["fred", "world_bank"],
64
+ "forex": ["alpha_vantage", "yfinance"],
65
+ "commodity": ["alpha_vantage"],
66
+ }
67
+
68
+ # ── 配置文件路径 ──────────────────────────────────────────────────────────────
69
+ _CONFIG_PATHS = [
70
+ Path.home() / ".aria" / "datasources.yaml",
71
+ Path.home() / ".aria" / "datasources.json",
72
+ Path(".aria.json"),
73
+ ]
74
+
75
+
76
+ def _load_user_chains() -> Dict[str, List[str]]:
77
+ for p in _CONFIG_PATHS:
78
+ if not p.exists():
79
+ continue
80
+ try:
81
+ if p.suffix in (".yaml", ".yml"):
82
+ import yaml
83
+ with open(p, encoding="utf-8") as f:
84
+ data = yaml.safe_load(f) or {}
85
+ else:
86
+ import json
87
+ with open(p, encoding="utf-8") as f:
88
+ data = json.load(f)
89
+ ds = data.get("datasources", data)
90
+ if isinstance(ds, dict):
91
+ chains = {}
92
+ key_map = {
93
+ "a_shares": "a_share", "a_share": "a_share",
94
+ "us_stocks": "us", "us": "us",
95
+ "hk_stocks": "hk", "hk": "hk",
96
+ "crypto": "crypto",
97
+ }
98
+ for k, v in ds.items():
99
+ market = key_map.get(k.lower(), k.lower())
100
+ if isinstance(v, list):
101
+ chains[market] = v
102
+ return chains
103
+ except Exception as e:
104
+ logger.debug(f"加载 datasources 配置 {p} 失败: {e}")
105
+ return {}
106
+
107
+
108
+ class DataRouter:
109
+ """
110
+ 统一数据路由器。线程安全(内部缓存用锁保护)。
111
+
112
+ 用法:
113
+ router = DataRouter()
114
+ q = router.quote("600519")
115
+ h = router.history("AAPL", days=90)
116
+ f = router.fundamentals("000858")
117
+ """
118
+
119
+ def __init__(self):
120
+ self._user_chains = _load_user_chains()
121
+ self._source_cache: Dict[str, BaseDataSource] = {}
122
+ self._lock = threading.Lock()
123
+
124
+ def _get_chain(self, market: str) -> List[str]:
125
+ user = self._user_chains.get(market) or self._user_chains.get(
126
+ {"a_share": "a_shares"}.get(market, market)
127
+ )
128
+ if user:
129
+ return user
130
+ return _DEFAULT_CHAINS.get(market, ["yfinance"])
131
+
132
+ def _get_source(self, name: str) -> Optional[BaseDataSource]:
133
+ with self._lock:
134
+ if name not in self._source_cache:
135
+ cls = _SOURCE_REGISTRY.get(name.lower())
136
+ if not cls:
137
+ logger.debug(f"未知数据源: {name}")
138
+ return None
139
+ src = cls()
140
+ if not src.is_configured():
141
+ logger.debug(f"数据源 {name} 未配置(缺少 API key)")
142
+ return None
143
+ self._source_cache[name] = src
144
+ return self._source_cache[name]
145
+
146
+ def quote(self, symbol: str) -> Optional[QuoteResult]:
147
+ market = _detect_market(symbol)
148
+ for src_name in self._get_chain(market):
149
+ if src_name == "edgar":
150
+ continue
151
+ src = self._get_source(src_name)
152
+ if not src or not src.supports(symbol):
153
+ continue
154
+ try:
155
+ result = src.quote(symbol)
156
+ try:
157
+ valid_price = result is not None and float(getattr(result, "price", 0) or 0) > 0
158
+ except Exception:
159
+ valid_price = False
160
+ if result and valid_price:
161
+ logger.debug(f"quote({symbol}) ← {src_name}")
162
+ return result
163
+ except Exception as e:
164
+ logger.debug(f"[{src_name}] quote {symbol} 异常: {e}")
165
+ logger.debug(f"所有数据源均无法获取 {symbol} 行情")
166
+ return None
167
+
168
+ def history(
169
+ self, symbol: str, days: int = 90, interval: str = "1d"
170
+ ) -> Optional[HistoryResult]:
171
+ market = _detect_market(symbol)
172
+ for src_name in self._get_chain(market):
173
+ src = self._get_source(src_name)
174
+ if not src or not src.supports(symbol):
175
+ continue
176
+ try:
177
+ result = src.history(symbol, days=days, interval=interval)
178
+ if result is not None:
179
+ logger.debug(f"history({symbol}) ← {src_name}")
180
+ return result
181
+ except Exception as e:
182
+ logger.debug(f"[{src_name}] history {symbol} 异常: {e}")
183
+ return None
184
+
185
+ def fundamentals(self, symbol: str) -> Optional[FundamentalsResult]:
186
+ market = _detect_market(symbol)
187
+ for src_name in self._get_chain(market):
188
+ src = self._get_source(src_name)
189
+ if not src or not src.supports(symbol):
190
+ continue
191
+ try:
192
+ result = src.fundamentals(symbol)
193
+ if result:
194
+ logger.debug(f"fundamentals({symbol}) ← {src_name}")
195
+ return result
196
+ except Exception as e:
197
+ logger.debug(f"[{src_name}] fundamentals {symbol} 异常: {e}")
198
+ return None
199
+
200
+ def list_sources(self) -> List[Dict[str, Any]]:
201
+ """列出所有数据源及其状态(用于 /config 展示)"""
202
+ result = []
203
+ for name, cls in _SOURCE_REGISTRY.items():
204
+ src = cls()
205
+ result.append({
206
+ "name": name,
207
+ "markets": cls.markets,
208
+ "needs_key": cls.requires_key,
209
+ "configured": src.is_configured(),
210
+ })
211
+ return result
212
+
213
+
214
+ # ── 单例 ──────────────────────────────────────────────────────────────────────
215
+ _router: Optional[DataRouter] = None
216
+
217
+ def get_router() -> DataRouter:
218
+ global _router
219
+ if _router is None:
220
+ _router = DataRouter()
221
+ return _router
@@ -0,0 +1,15 @@
1
+ from .akshare_source import AkshareSource
2
+ from .yfinance_source import YFinanceSource
3
+ from .tushare_source import TushareSource
4
+ from .fred_source import FREDSource
5
+ from .edgar_source import EDGARSource
6
+ from .alpha_vantage_source import AlphaVantageSource
7
+ from .world_bank_source import WorldBankSource
8
+ from .web_scraper_source import WebScraperSource
9
+ from .finnhub_source import FinnhubSource
10
+
11
+ __all__ = [
12
+ "AkshareSource", "YFinanceSource", "TushareSource",
13
+ "FREDSource", "EDGARSource", "AlphaVantageSource",
14
+ "WorldBankSource", "WebScraperSource", "FinnhubSource",
15
+ ]
@@ -0,0 +1,269 @@
1
+ """
2
+ datasources/sources/akshare_source.py — Akshare A股数据源
3
+ ==========================================================
4
+ 免费,无需 API key,覆盖 A股实时行情、历史数据、北向资金等。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from datetime import date, timedelta
11
+ from typing import Any, Dict, Optional
12
+
13
+ from ..base import BaseDataSource, FundamentalsResult, HistoryResult, QuoteResult
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class AkshareSource(BaseDataSource):
19
+
20
+ name = "akshare"
21
+ markets = ["a_share"]
22
+ requires_key = False
23
+
24
+ def _normalize_code(self, symbol: str) -> str:
25
+ """'sh600519' / '600519' / '600519.SH' → '600519'"""
26
+ s = symbol.upper().replace("SH", "").replace("SZ", "").replace("BJ", "")
27
+ s = s.replace(".", "").strip()
28
+ if s.startswith("6") or s.startswith("0") or s.startswith("3") or s.startswith("8"):
29
+ return s[:6]
30
+ return s
31
+
32
+ def _yf_symbol(self, code: str) -> str:
33
+ """600519 → 600519.SS,000858 → 000858.SZ,300xxx → 300xxx.SZ"""
34
+ if code.startswith("6") or code.startswith("9"):
35
+ return f"{code}.SS"
36
+ return f"{code}.SZ"
37
+
38
+ def quote(self, symbol: str) -> Optional[QuoteResult]:
39
+ """
40
+ 三级降级:
41
+ 1. akshare stock_zh_a_spot_em (全市场快照)
42
+ 2. akshare stock_individual_info_em (单股)
43
+ 3. yfinance .SS/.SZ (国际用户兜底)
44
+ 4. 最近交易日收盘价 from history
45
+ """
46
+ import warnings
47
+ warnings.filterwarnings("ignore")
48
+ code = self._normalize_code(symbol)
49
+
50
+ # ── 方法1: 全市场快照 ────────────────────────────────────────────────
51
+ try:
52
+ import akshare as ak
53
+ df = ak.stock_zh_a_spot_em()
54
+ row = df[df["代码"] == code]
55
+ if not row.empty:
56
+ r = row.iloc[0]
57
+ price = float(r.get("最新价", 0) or 0)
58
+ if price > 0:
59
+ return QuoteResult(
60
+ symbol = symbol,
61
+ name = str(r.get("名称", "")),
62
+ price = price,
63
+ change = float(r.get("涨跌额", 0) or 0),
64
+ change_pct = float(r.get("涨跌幅", 0) or 0),
65
+ volume = float(r.get("成交量", 0) or 0),
66
+ market_cap = float(r.get("总市值", 0) or 0),
67
+ pe_ttm = float(r.get("市盈率-动态", 0) or 0),
68
+ pb = float(r.get("市净率", 0) or 0),
69
+ high_52w = float(r.get("52周最高", 0) or 0),
70
+ low_52w = float(r.get("52周最低", 0) or 0),
71
+ currency = "CNY",
72
+ market = "a_share",
73
+ source = self.name,
74
+ )
75
+ except Exception:
76
+ pass
77
+
78
+ # ── 方法2: 单股信息(EastMoney 单接口) ────────────────────────────
79
+ try:
80
+ import akshare as ak
81
+ df2 = ak.stock_individual_info_em(symbol=code)
82
+ if df2 is not None and not df2.empty:
83
+ info = dict(zip(df2["item"], df2["value"]))
84
+ price = float(str(info.get("最新", info.get("收盘", 0))).replace(",", "") or 0)
85
+ prev = float(str(info.get("昨收", price)).replace(",", "") or price)
86
+ if price > 0:
87
+ return QuoteResult(
88
+ symbol = symbol,
89
+ name = str(info.get("股票简称", "")),
90
+ price = price,
91
+ change = round(price - prev, 4),
92
+ change_pct = round((price - prev) / prev * 100, 2) if prev else 0,
93
+ currency = "CNY",
94
+ market = "a_share",
95
+ source = self.name,
96
+ )
97
+ except Exception:
98
+ pass
99
+
100
+ # ── 方法3: yfinance .SS/.SZ(海外访问兜底) ─────────────────────────
101
+ try:
102
+ import yfinance as yf
103
+ yf_sym = self._yf_symbol(code)
104
+ ticker = yf.Ticker(yf_sym)
105
+
106
+ price = None
107
+ prev = None
108
+ info = {}
109
+ try:
110
+ fi = ticker.fast_info
111
+ price = getattr(fi, "last_price", None) or getattr(fi, "previous_close", None)
112
+ prev = getattr(fi, "previous_close", None) or price
113
+ except Exception:
114
+ pass
115
+ if not price:
116
+ try:
117
+ info = ticker.info or {}
118
+ price = info.get("currentPrice") or info.get("regularMarketPrice") or info.get("previousClose")
119
+ prev = info.get("previousClose") or price
120
+ except Exception:
121
+ pass
122
+ if price and float(price) > 0:
123
+ if not info:
124
+ try:
125
+ info = ticker.info or {}
126
+ except Exception:
127
+ pass
128
+ chg = float(price) - float(prev) if prev else 0
129
+ chg_p = chg / float(prev) * 100 if prev else 0
130
+ return QuoteResult(
131
+ symbol = symbol,
132
+ name = info.get("shortName") or info.get("longName") or symbol,
133
+ price = float(price),
134
+ change = chg,
135
+ change_pct = chg_p,
136
+ volume = float(info.get("volume") or 0),
137
+ market_cap = float(info.get("marketCap") or 0),
138
+ pe_ttm = float(info.get("trailingPE") or 0),
139
+ pb = float(info.get("priceToBook") or 0),
140
+ currency = "CNY",
141
+ market = "a_share",
142
+ source = f"{self.name}+yfinance",
143
+ )
144
+ except Exception:
145
+ pass
146
+
147
+ # ── 方法4: 最近历史收盘价 ────────────────────────────────────────────
148
+ try:
149
+ h = self.history(symbol, days=5)
150
+ if h and h.data is not None and not h.data.empty:
151
+ df = h.data
152
+ # Flexible column detection (akshare returns Chinese column names)
153
+ close_col = next(
154
+ (c for c in df.columns if "收盘" in str(c) or "close" in str(c).lower()),
155
+ df.columns[-1]
156
+ )
157
+ price = float(df[close_col].iloc[-1])
158
+ prev = float(df[close_col].iloc[-2]) if len(df) > 1 else price
159
+ if price > 0:
160
+ return QuoteResult(
161
+ symbol = symbol,
162
+ price = price,
163
+ change = round(price - prev, 4),
164
+ change_pct = round((price - prev) / prev * 100, 2) if prev else 0,
165
+ currency = "CNY",
166
+ market = "a_share",
167
+ source = f"{self.name}(last_close)",
168
+ )
169
+ except Exception as e:
170
+ logger.debug(f"[akshare] quote history fallback {symbol} 失败: {e}")
171
+
172
+ logger.debug(f"[akshare] quote {symbol}: 所有方法均失败")
173
+ return None
174
+
175
+ def history(self, symbol: str, days: int = 90, interval: str = "1d") -> Optional[HistoryResult]:
176
+ # ── 方法1: akshare stock_zh_a_hist ──────────────────────────────────
177
+ try:
178
+ import akshare as ak
179
+ code = self._normalize_code(symbol)
180
+ end = date.today().strftime("%Y%m%d")
181
+ start = (date.today() - timedelta(days=days + 10)).strftime("%Y%m%d")
182
+ df = ak.stock_zh_a_hist(
183
+ symbol=code, period="daily",
184
+ start_date=start, end_date=end, adjust="qfq"
185
+ )
186
+ if df is not None and not df.empty:
187
+ return HistoryResult(symbol=symbol, data=df, source=self.name, interval=interval)
188
+ except Exception as e:
189
+ logger.debug(f"[akshare] history(akshare) {symbol} 失败: {e}")
190
+
191
+ # ── 方法2: yfinance .SS/.SZ (海外兜底) ──────────────────────────────
192
+ try:
193
+ import yfinance as yf
194
+ import pandas as pd
195
+ code = self._normalize_code(symbol)
196
+ yf_sym = self._yf_symbol(code)
197
+ ticker = yf.Ticker(yf_sym)
198
+ period = f"{days}d" if days <= 730 else "2y"
199
+ df = ticker.history(period=period, auto_adjust=True)
200
+ if df is not None and not df.empty:
201
+ df.index = pd.to_datetime(df.index)
202
+ return HistoryResult(symbol=symbol, data=df, source=f"{self.name}+yfinance", interval=interval)
203
+ except Exception as e:
204
+ logger.debug(f"[akshare] history(yfinance) {symbol} 失败: {e}")
205
+
206
+ return None
207
+
208
+ def fundamentals(self, symbol: str) -> Optional[FundamentalsResult]:
209
+ """A股基本面:优先 akshare,降级 yfinance .SS/.SZ"""
210
+ # ── 方法1: akshare 市场快照字段 ──────────────────────────────────────
211
+ try:
212
+ import akshare as ak
213
+ import math
214
+ code = self._normalize_code(symbol)
215
+ df = ak.stock_zh_a_spot_em()
216
+ row = df[df["代码"] == code]
217
+ if not row.empty:
218
+ r = row.iloc[0]
219
+ def _ak(key: str) -> Optional[float]:
220
+ v = r.get(key)
221
+ if v is None:
222
+ return None
223
+ try:
224
+ fv = float(v)
225
+ return None if (math.isnan(fv) or fv == 0) else fv
226
+ except (TypeError, ValueError):
227
+ return None
228
+ pe = _ak("市盈率-动态")
229
+ pb = _ak("市净率")
230
+ mv = _ak("总市值")
231
+ if pe or pb:
232
+ return FundamentalsResult(
233
+ symbol=symbol, pe_ttm=pe, pb=pb,
234
+ total_mv=mv, source=self.name,
235
+ )
236
+ except Exception:
237
+ pass
238
+
239
+ # ── 方法2: yfinance .SS/.SZ ───────────────────────────────────────────
240
+ try:
241
+ from .yfinance_source import YFinanceSource
242
+ code = self._normalize_code(symbol)
243
+ yf_sym = self._yf_symbol(code)
244
+ fund = YFinanceSource().fundamentals(yf_sym)
245
+ if fund:
246
+ fund.symbol = symbol
247
+ fund.source = f"{self.name}+yfinance"
248
+ return fund
249
+ except Exception as e:
250
+ logger.debug(f"[akshare] fundamentals yfinance fallback {symbol} 失败: {e}")
251
+
252
+ return None
253
+
254
+ def northbound_flow(self) -> Optional[Dict]:
255
+ """北向资金净流入(akshare 特有)"""
256
+ try:
257
+ import akshare as ak
258
+ df = ak.stock_hsgt_north_net_flow_in_em(symbol="北向资金")
259
+ if df is None or df.empty:
260
+ return None
261
+ latest = df.iloc[-1]
262
+ return {
263
+ "date": str(latest.get("日期", "")),
264
+ "net_flow": float(latest.get("当日净流入", 0) or 0),
265
+ "source": self.name,
266
+ }
267
+ except Exception as e:
268
+ logger.debug(f"[akshare] northbound_flow 失败: {e}")
269
+ return None