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,219 @@
1
+ """
2
+ datasources/sources/fred_source.py — FRED (Federal Reserve Economic Data)
3
+ =========================================================================
4
+ 完全免费,无需 API key(可选 key 提升频率限制)。
5
+ 覆盖:宏观经济指标、利率、CPI、GDP、就业数据等 800,000+ 系列。
6
+
7
+ 配置(可选): FRED_API_KEY 环境变量 或 ~/.aria/.env
8
+
9
+ FRED series 示例:
10
+ DGS10 — 10年美国国债收益率
11
+ FEDFUNDS — 联邦基金利率
12
+ CPIAUCSL — 消费者价格指数
13
+ GDP — 美国 GDP
14
+ UNRATE — 失业率
15
+ SP500 — S&P 500 指数
16
+ DEXCNUS — 人民币/美元汇率
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ import os
23
+ from datetime import date, timedelta
24
+ from pathlib import Path
25
+ from typing import Any, Dict, List, Optional
26
+
27
+ from ..base import BaseDataSource, HistoryResult, QuoteResult
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ _FRED_BASE = "https://fred.stlouisfed.org/graph/fredgraph.csv?id="
32
+ _FRED_API = "https://api.stlouisfed.org/fred"
33
+
34
+ # 常用宏观指标 symbol → FRED series id 映射
35
+ MACRO_ALIASES: Dict[str, str] = {
36
+ "US10Y": "DGS10",
37
+ "US2Y": "DGS2",
38
+ "US3M": "DTB3",
39
+ "FEDFUNDS": "FEDFUNDS",
40
+ "CPI": "CPIAUCSL",
41
+ "CPIYOY": "CPIAUCSL",
42
+ "PCE": "PCEPI",
43
+ "GDP": "GDP",
44
+ "UNRATE": "UNRATE",
45
+ "NFP": "PAYEMS",
46
+ "SP500": "SP500",
47
+ "NASDAQ": "NASDAQCOM",
48
+ "WILSHIRE": "WILL5000PR",
49
+ "USDINR": "DEXINUS",
50
+ "USDCNY": "DEXCHUS",
51
+ "USDEUR": "DEXUSEU",
52
+ "VIX": "VIXCLS",
53
+ "M2": "M2SL",
54
+ "MORTGAGE": "MORTGAGE30US",
55
+ "HOUSING": "HOUST",
56
+ }
57
+
58
+
59
+ def _load_api_key() -> str:
60
+ key = os.getenv("FRED_API_KEY", "")
61
+ if not key:
62
+ for p in [Path.home() / ".aria" / ".env", Path.home() / ".arthera" / ".env"]:
63
+ if p.exists():
64
+ for line in p.read_text(encoding="utf-8").splitlines():
65
+ if line.startswith("FRED_API_KEY="):
66
+ key = line.split("=", 1)[1].strip()
67
+ break
68
+ if key:
69
+ break
70
+ if not key:
71
+ try:
72
+ import json as _json
73
+ _p = Path.home() / ".arthera" / "providers.json"
74
+ if _p.exists():
75
+ _raw = _json.loads(_p.read_text(encoding="utf-8"))
76
+ key = _raw.get("data", {}).get("fred", {}).get("api_key", "")
77
+ except Exception:
78
+ pass
79
+ return key
80
+
81
+
82
+ class FREDSource(BaseDataSource):
83
+ """
84
+ Federal Reserve Economic Data — 宏观经济数据源。
85
+ 无需 key 即可使用 CSV 下载接口;有 key 可使用 JSON API 获得更多元数据。
86
+ """
87
+
88
+ name = "fred"
89
+ markets = ["us", "macro"]
90
+ requires_key = False
91
+
92
+ def __init__(self, config=None):
93
+ super().__init__(config)
94
+ self._api_key = _load_api_key()
95
+
96
+ def is_configured(self) -> bool:
97
+ return True # 无需 key,免费开放
98
+
99
+ def supports(self, symbol: str) -> bool:
100
+ s = symbol.upper()
101
+ return s in MACRO_ALIASES or s in MACRO_ALIASES.values()
102
+
103
+ def _resolve_series(self, symbol: str) -> str:
104
+ s = symbol.upper()
105
+ return MACRO_ALIASES.get(s, s)
106
+
107
+ def quote(self, symbol: str) -> Optional[QuoteResult]:
108
+ try:
109
+ h = self.history(symbol, days=30)
110
+ if h is None or h.data is None or h.data.empty:
111
+ return None
112
+ last = h.data.iloc[-1]
113
+ val = float(last.get("close", last.iloc[0]))
114
+ series_id = self._resolve_series(symbol)
115
+ return QuoteResult(
116
+ symbol = symbol,
117
+ name = f"FRED:{series_id}",
118
+ price = val,
119
+ currency = "USD",
120
+ market = "macro",
121
+ source = self.name,
122
+ timestamp = str(h.data.index[-1].date()),
123
+ )
124
+ except Exception as e:
125
+ logger.debug(f"[fred] quote {symbol} 失败: {e}")
126
+ return None
127
+
128
+ def history(
129
+ self,
130
+ symbol: str,
131
+ days: int = 365,
132
+ interval: str = "1d",
133
+ _timeout: int = 12,
134
+ ) -> Optional[HistoryResult]:
135
+ try:
136
+ import pandas as pd
137
+ import urllib.request, json
138
+
139
+ series_id = self._resolve_series(symbol)
140
+ start = (date.today() - timedelta(days=days)).strftime("%Y-%m-%d")
141
+ end = date.today().strftime("%Y-%m-%d")
142
+
143
+ def _parse_api(url: str) -> Optional[pd.DataFrame]:
144
+ req = urllib.request.Request(url, headers={"User-Agent": "aria-code/1.0"})
145
+ with urllib.request.urlopen(req, timeout=_timeout) as resp:
146
+ data = json.loads(resp.read())
147
+ obs = data.get("observations", [])
148
+ rows = []
149
+ for o in obs:
150
+ try:
151
+ rows.append({"date": o["date"], "close": float(o["value"])})
152
+ except (ValueError, KeyError):
153
+ pass
154
+ if not rows:
155
+ return None
156
+ _df = pd.DataFrame(rows)
157
+ _df["date"] = pd.to_datetime(_df["date"])
158
+ return _df.set_index("date").sort_index()
159
+
160
+ def _parse_csv(url: str) -> Optional[pd.DataFrame]:
161
+ req = urllib.request.Request(url, headers={"User-Agent": "aria-code/1.0"})
162
+ with urllib.request.urlopen(req, timeout=_timeout) as resp:
163
+ _df = pd.read_csv(resp, parse_dates=["DATE"], index_col="DATE")
164
+ _df.columns = ["close"]
165
+ _df = _df.replace(".", float("nan")).dropna()
166
+ _df["close"] = _df["close"].astype(float)
167
+ return _df if not _df.empty else None
168
+
169
+ df = None
170
+
171
+ # 优先使用 JSON API(有 key 速率更高)
172
+ if self._api_key:
173
+ api_url = (
174
+ f"{_FRED_API}/series/observations?series_id={series_id}"
175
+ f"&observation_start={start}&observation_end={end}"
176
+ f"&api_key={self._api_key}&file_type=json"
177
+ )
178
+ try:
179
+ df = _parse_api(api_url)
180
+ except Exception as _e1:
181
+ logger.debug(f"[fred] JSON API 失败,尝试 CSV: {_e1}")
182
+
183
+ # 回落:免费 CSV 下载接口(无需 key,但国内可能超时)
184
+ if df is None:
185
+ csv_url = (
186
+ f"{_FRED_BASE}{series_id}"
187
+ f"&cosd={start}&coed={end}"
188
+ )
189
+ try:
190
+ df = _parse_csv(csv_url)
191
+ except Exception as _e2:
192
+ logger.debug(f"[fred] CSV 也失败: {_e2}")
193
+
194
+ if df is None:
195
+ return None
196
+ return HistoryResult(symbol=symbol, data=df, source=self.name, interval="1d")
197
+ except Exception as e:
198
+ logger.debug(f"[fred] history {symbol} 失败: {e}")
199
+ return None
200
+
201
+ def search_series(self, query: str, limit: int = 10) -> List[Dict[str, Any]]:
202
+ """用关键词搜索 FRED 系列(需要 API key)。"""
203
+ if not self._api_key:
204
+ return [{"error": "需要 FRED_API_KEY 才能搜索系列"}]
205
+ try:
206
+ import urllib.request, json, urllib.parse
207
+ q = urllib.parse.quote(query)
208
+ url = (f"{_FRED_API}/series/search?search_text={q}"
209
+ f"&limit={limit}&api_key={self._api_key}&file_type=json")
210
+ req = urllib.request.Request(url, headers={"User-Agent": "aria-code/1.0"})
211
+ with urllib.request.urlopen(req, timeout=10) as resp:
212
+ data = json.loads(resp.read())
213
+ return [
214
+ {"id": s["id"], "title": s["title"], "frequency": s.get("frequency_short"),
215
+ "units": s.get("units_short"), "last_updated": s.get("last_updated")}
216
+ for s in data.get("seriess", [])
217
+ ]
218
+ except Exception as e:
219
+ return [{"error": str(e)}]
@@ -0,0 +1,141 @@
1
+ """
2
+ datasources/sources/tushare_source.py — Tushare A股数据源
3
+ ==========================================================
4
+ 需要 TUSHARE_TOKEN 环境变量(已在 .env 中配置)。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import os
11
+ from datetime import date, timedelta
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ from ..base import BaseDataSource, FundamentalsResult, HistoryResult, QuoteResult
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def _load_token() -> str:
21
+ token = os.getenv("TUSHARE_TOKEN", "")
22
+ if not token:
23
+ env_paths = [
24
+ Path.cwd() / ".env",
25
+ Path.cwd().parent / ".env",
26
+ Path.home() / ".aria" / ".env",
27
+ Path.home() / ".arthera" / ".env",
28
+ ]
29
+ for p in env_paths:
30
+ if p.exists():
31
+ for line in p.read_text(encoding="utf-8").splitlines():
32
+ if line.startswith("TUSHARE_TOKEN="):
33
+ token = line.split("=", 1)[1].strip()
34
+ break
35
+ if token:
36
+ break
37
+ return token
38
+
39
+
40
+ class TushareSource(BaseDataSource):
41
+
42
+ name = "tushare"
43
+ markets = ["a_share"]
44
+ requires_key = True
45
+
46
+ def __init__(self, config=None):
47
+ super().__init__(config)
48
+ self._token = _load_token()
49
+ self._pro = None
50
+
51
+ def is_configured(self) -> bool:
52
+ return bool(self._token)
53
+
54
+ def _get_pro(self):
55
+ if self._pro is None:
56
+ import tushare as ts
57
+ ts.set_token(self._token)
58
+ self._pro = ts.pro_api()
59
+ return self._pro
60
+
61
+ def _to_ts_code(self, symbol: str) -> str:
62
+ s = symbol.upper().replace("SH","").replace("SZ","").replace(".","").strip()
63
+ code = s[:6]
64
+ return f"{code}.SH" if code.startswith(("6","9")) else f"{code}.SZ"
65
+
66
+ def quote(self, symbol: str) -> Optional[QuoteResult]:
67
+ """
68
+ 使用 pro.daily 获取最近交易日收盘价(不调用 daily_basic 避免限频)。
69
+ PE/PB 留给 fundamentals() 单独获取。
70
+ """
71
+ try:
72
+ import time as _t
73
+ pro = self._get_pro()
74
+ ts_code = self._to_ts_code(symbol)
75
+ end = date.today().strftime("%Y%m%d")
76
+ start = (date.today() - timedelta(days=10)).strftime("%Y%m%d")
77
+ df = pro.daily(ts_code=ts_code, start_date=start, end_date=end)
78
+ _t.sleep(0.35)
79
+ if df is None or df.empty:
80
+ return None
81
+ # trade_date 格式 YYYYMMDD,字符串排序等价于日期排序
82
+ latest = df.sort_values("trade_date", ascending=False).iloc[0]
83
+ price = float(latest.get("close", 0))
84
+ prev = float(latest.get("pre_close", price))
85
+ return QuoteResult(
86
+ symbol = symbol,
87
+ price = price,
88
+ change = round(price - prev, 4),
89
+ change_pct = float(latest.get("pct_chg", 0)),
90
+ volume = float(latest.get("vol", 0)),
91
+ currency = "CNY",
92
+ market = "a_share",
93
+ source = self.name,
94
+ timestamp = str(latest.get("trade_date", "")),
95
+ )
96
+ except Exception as e:
97
+ logger.debug(f"[tushare] quote {symbol} 失败: {e}")
98
+ return None
99
+
100
+ def history(self, symbol: str, days: int = 90, interval: str = "1d") -> Optional[HistoryResult]:
101
+ try:
102
+ import time as _t, pandas as pd
103
+ pro = self._get_pro()
104
+ ts_code = self._to_ts_code(symbol)
105
+ end = date.today().strftime("%Y%m%d")
106
+ start = (date.today() - timedelta(days=days + 15)).strftime("%Y%m%d")
107
+ df = pro.daily(ts_code=ts_code, start_date=start, end_date=end)
108
+ _t.sleep(0.4)
109
+ if df is None or df.empty:
110
+ return None
111
+ df["trade_date"] = pd.to_datetime(df["trade_date"])
112
+ df = df.rename(columns={"trade_date": "date", "vol": "volume"})
113
+ df = df.set_index("date").sort_index()
114
+ return HistoryResult(symbol=symbol, data=df, source=self.name, interval=interval)
115
+ except Exception as e:
116
+ logger.debug(f"[tushare] history {symbol} 失败: {e}")
117
+ return None
118
+
119
+ def fundamentals(self, symbol: str) -> Optional[FundamentalsResult]:
120
+ try:
121
+ import time as _t
122
+ pro = self._get_pro()
123
+ ts_code = self._to_ts_code(symbol)
124
+ for period in ["20241231", "20231231"]:
125
+ df = pro.fina_indicator(
126
+ ts_code=ts_code, period=period,
127
+ fields="ts_code,roe,netprofit_yoy,revenue_yoy"
128
+ )
129
+ _t.sleep(0.4)
130
+ if df is not None and not df.empty:
131
+ r = df.iloc[0]
132
+ return FundamentalsResult(
133
+ symbol = symbol,
134
+ roe = float(r.get("roe", 0) or 0),
135
+ revenue_growth = float(r.get("revenue_yoy", 0) or 0),
136
+ net_profit_growth = float(r.get("netprofit_yoy", 0) or 0),
137
+ source = self.name,
138
+ )
139
+ except Exception as e:
140
+ logger.debug(f"[tushare] fundamentals {symbol} 失败: {e}")
141
+ return None
@@ -0,0 +1,278 @@
1
+ """
2
+ datasources/sources/web_scraper_source.py — 公开财务数据网络爬虫
3
+ ================================================================
4
+ 爬取完全公开的金融数据网站,无需账号或 API key。
5
+ 所有请求遵守 robots.txt 和频率限制。
6
+
7
+ 覆盖:
8
+ - Macrotrends — 历史财务数据(收入/利润/现金流/估值)
9
+ - Wisesheets — 免费财务摘要
10
+ - 东方财富 — A股公告/研报摘要
11
+ - 同花顺 — A股财务数据
12
+ - Yahoo Finance — 基本面摘要(HTML解析)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import re
19
+ import time
20
+ import urllib.parse
21
+ import urllib.request
22
+ from typing import Any, Dict, List, Optional
23
+
24
+ from ..base import BaseDataSource, FundamentalsResult, HistoryResult, QuoteResult
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ _DEFAULT_HEADERS = {
29
+ "User-Agent": (
30
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
31
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
32
+ "Chrome/120.0.0.0 Safari/537.36"
33
+ ),
34
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
35
+ "Accept": "text/html,application/xhtml+xml,application/json,*/*;q=0.8",
36
+ }
37
+
38
+
39
+ def _fetch_html(url: str, timeout: int = 12, encoding: str = "utf-8") -> Optional[str]:
40
+ try:
41
+ req = urllib.request.Request(url, headers=_DEFAULT_HEADERS)
42
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
43
+ raw = resp.read()
44
+ try:
45
+ return raw.decode(encoding)
46
+ except UnicodeDecodeError:
47
+ return raw.decode("gbk", errors="replace")
48
+ except Exception as e:
49
+ logger.debug(f"[web_scraper] fetch {url[:80]} 失败: {e}")
50
+ return None
51
+
52
+
53
+ def _fetch_json(url: str, timeout: int = 12, extra_headers: dict = None) -> Optional[Any]:
54
+ import json
55
+ try:
56
+ headers = {**_DEFAULT_HEADERS, **(extra_headers or {})}
57
+ req = urllib.request.Request(url, headers=headers)
58
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
59
+ return json.loads(resp.read())
60
+ except Exception as e:
61
+ logger.debug(f"[web_scraper] json {url[:80]} 失败: {e}")
62
+ return None
63
+
64
+
65
+ # ─── 东方财富 A股公告爬虫 ────────────────────────────────────────────────────
66
+
67
+ def scrape_eastmoney_announcements(symbol: str, count: int = 10) -> List[Dict]:
68
+ """
69
+ 爬取东方财富 A股公告列表(免费公开页面)。
70
+ symbol: 6位A股代码(如 '600519')
71
+ """
72
+ s = symbol.replace(".SS", "").replace(".SZ", "").zfill(6)
73
+ url = (
74
+ f"https://np-anotice-stock.eastmoney.com/api/security/ann?"
75
+ f"sr=-1&page_size={count}&page_index=1&ann_type=A&client_source=web"
76
+ f"&stock_list={s}&f_node=0&s_node=0"
77
+ )
78
+ data = _fetch_json(url, extra_headers={"Referer": "https://www.eastmoney.com/"})
79
+ if not data or "data" not in data:
80
+ return []
81
+ anns = data["data"].get("list", [])
82
+ result = []
83
+ for a in anns[:count]:
84
+ result.append({
85
+ "title": a.get("notice_title", ""),
86
+ "date": a.get("notice_date", ""),
87
+ "type": a.get("notice_type", ""),
88
+ "url": f"https://data.eastmoney.com/notices/detail/{s}/{a.get('art_code','')}.html",
89
+ "source": "eastmoney",
90
+ })
91
+ return result
92
+
93
+
94
+ def scrape_eastmoney_news(symbol: str, count: int = 10) -> List[Dict]:
95
+ """爬取东方财富个股新闻。"""
96
+ s = symbol.replace(".SS", "").replace(".SZ", "").zfill(6)
97
+ url = (
98
+ f"https://newsapi.eastmoney.com/kuaixun/v1/getlist_101_ajaxResult_50_{s},,50,1.html"
99
+ )
100
+ html = _fetch_html(url)
101
+ if not html:
102
+ return []
103
+ # 尝试从 JSON 响应中解析
104
+ try:
105
+ import json
106
+ data = json.loads(html)
107
+ items = data.get("LiveList", [])[:count]
108
+ return [{
109
+ "title": i.get("title", ""),
110
+ "time": i.get("showtime", ""),
111
+ "source": i.get("medianame", ""),
112
+ "url": i.get("url", ""),
113
+ } for i in items]
114
+ except Exception:
115
+ pass
116
+ return []
117
+
118
+
119
+ # ─── Macrotrends 历史财务数据爬虫 ────────────────────────────────────────────
120
+
121
+ def scrape_macrotrends_revenue(symbol: str) -> Optional[List[Dict]]:
122
+ """
123
+ 从 Macrotrends 爬取年度营收历史(免费公开页面,无需登录)。
124
+ 注意:依赖页面结构,如网站改版可能失效。
125
+ """
126
+ slug_map = {"AAPL": "apple", "MSFT": "microsoft", "GOOGL": "alphabet",
127
+ "AMZN": "amazon", "META": "meta-platforms", "TSLA": "tesla",
128
+ "NVDA": "nvidia", "JPM": "jpmorgan-chase", "V": "visa"}
129
+ slug = slug_map.get(symbol.upper())
130
+ if not slug:
131
+ slug = symbol.lower()
132
+
133
+ url = f"https://www.macrotrends.net/stocks/charts/{symbol.upper()}/{slug}/revenue"
134
+ html = _fetch_html(url)
135
+ if not html:
136
+ return None
137
+
138
+ # 提取嵌入的 JSON 数据
139
+ match = re.search(r'var originalData\s*=\s*(\[.*?\]);', html, re.DOTALL)
140
+ if not match:
141
+ return None
142
+ try:
143
+ import json
144
+ rows = json.loads(match.group(1))
145
+ return [
146
+ {"date": r.get("date", ""), "revenue_usd": r.get("v1", 0)}
147
+ for r in rows
148
+ ][-20:] # 最近20个数据点
149
+ except Exception:
150
+ return None
151
+
152
+
153
+ # ─── SSE/SZSE 交易所公开数据爬虫 ─────────────────────────────────────────────
154
+
155
+ def scrape_sse_financials(symbol: str) -> Optional[Dict]:
156
+ """
157
+ 爬取上交所公开的公司财务摘要(适用于 6 开头的A股)。
158
+ """
159
+ s = symbol.replace(".SS", "").zfill(6)
160
+ if not s.startswith(("6", "9")):
161
+ return None
162
+
163
+ url = (
164
+ f"https://query.sse.com.cn/commonSoaQuery.do?"
165
+ f"isPagination=false&sqlId=COMMON_SSE_CP_GPLB_GPXG_JBXX_L&stockCode={s}"
166
+ )
167
+ data = _fetch_json(url, extra_headers={
168
+ "Referer": "https://www.sse.com.cn/",
169
+ "Host": "query.sse.com.cn",
170
+ })
171
+ if not data or "result" not in data:
172
+ return None
173
+ r = data["result"]
174
+ if isinstance(r, list) and r:
175
+ r = r[0]
176
+ if not isinstance(r, dict):
177
+ return None
178
+ return {
179
+ "symbol": s,
180
+ "name": r.get("FULL_NAME", ""),
181
+ "industry": r.get("INDUSTRY", ""),
182
+ "listing_date": r.get("LISTING_DATE", ""),
183
+ "total_shares": r.get("TOTAL_SHARES", ""),
184
+ "source": "sse",
185
+ }
186
+
187
+
188
+ def scrape_szse_financials(symbol: str) -> Optional[Dict]:
189
+ """爬取深交所公开的公司基本信息(适用于 0/3 开头的A股)。"""
190
+ s = symbol.replace(".SZ", "").zfill(6)
191
+ if not s.startswith(("0", "3", "2")):
192
+ return None
193
+
194
+ url = (
195
+ f"https://www.szse.cn/api/report/show/nature/detail?"
196
+ f"id=&"
197
+ )
198
+ # 深交所 API
199
+ data = _fetch_json(
200
+ f"https://www.szse.cn/api/market/smstock/list?random=0.1&tab2PAGENUM=1"
201
+ f"&tab2PAGENO=1&tab2COUNT=1&tab2SORTTAB=tab2&tab2SORTKEY=",
202
+ extra_headers={"Referer": "https://www.szse.cn/"},
203
+ )
204
+ return None # 深交所API结构复杂,返回None让上层fallback
205
+
206
+
207
+ # ─── 统一爬虫接口 ─────────────────────────────────────────────────────────────
208
+
209
+ class WebScraperSource(BaseDataSource):
210
+ """
211
+ 公开网页数据爬虫 — 汇总多个免费公开金融数据网站。
212
+ 实现 BaseDataSource 接口以便接入路由器;quote/history 委托给主数据源,
213
+ fundamentals() 提供爬取的补充数据(公告、交易所基本信息等)。
214
+ """
215
+
216
+ name = "web_scraper"
217
+ markets = ["a_share", "us"]
218
+ requires_key = False
219
+
220
+ def is_configured(self) -> bool:
221
+ return True
222
+
223
+ def quote(self, symbol: str) -> Optional[QuoteResult]:
224
+ return None # 不提供实时行情,由 akshare/yfinance 负责
225
+
226
+ def fundamentals(self, symbol: str) -> Optional[FundamentalsResult]:
227
+ """从交易所公开页面补充基本信息(仅 A 股)。"""
228
+ s = symbol.replace(".SS", "").replace(".SZ", "").zfill(6)
229
+ info = None
230
+ if s.startswith(("6", "9")):
231
+ info = scrape_sse_financials(s)
232
+ elif s.startswith(("0", "3")):
233
+ info = scrape_szse_financials(s)
234
+ if not info:
235
+ return None
236
+ return FundamentalsResult(
237
+ symbol = symbol,
238
+ source = self.name,
239
+ )
240
+
241
+ # ── 扩展方法(供上层直接调用) ─────────────────────────────────────────────
242
+
243
+ def get_announcements(self, symbol: str, count: int = 10) -> List[Dict]:
244
+ """A股公告列表(东方财富)。"""
245
+ return scrape_eastmoney_announcements(symbol, count)
246
+
247
+ def get_news(self, symbol: str, count: int = 10) -> List[Dict]:
248
+ """个股新闻。"""
249
+ return scrape_eastmoney_news(symbol, count)
250
+
251
+ def get_historical_revenue(self, symbol: str) -> Optional[List[Dict]]:
252
+ """美股历史营收(Macrotrends)。"""
253
+ return scrape_macrotrends_revenue(symbol)
254
+
255
+ def get_exchange_info(self, symbol: str) -> Optional[Dict]:
256
+ """A股交易所公开信息。"""
257
+ s = symbol.replace(".SS", "").replace(".SZ", "").zfill(6)
258
+ if s.startswith(("6", "9")):
259
+ return scrape_sse_financials(s)
260
+ elif s.startswith(("0", "3")):
261
+ return scrape_szse_financials(s)
262
+ return None
263
+
264
+ def bulk_scrape(self, symbols: List[str], data_type: str = "announcements") -> Dict[str, Any]:
265
+ """批量爬取,带速率限制(每次请求间隔 1 秒)。"""
266
+ results = {}
267
+ for sym in symbols:
268
+ try:
269
+ if data_type == "announcements":
270
+ results[sym] = self.get_announcements(sym)
271
+ elif data_type == "news":
272
+ results[sym] = self.get_news(sym)
273
+ elif data_type == "revenue":
274
+ results[sym] = self.get_historical_revenue(sym)
275
+ time.sleep(1.0)
276
+ except Exception as e:
277
+ results[sym] = {"error": str(e)}
278
+ return results