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,202 @@
1
+ """
2
+ datasources/sources/alpha_vantage_source.py — Alpha Vantage 免费层数据
3
+ ======================================================================
4
+ 免费 API key 从 https://www.alphavantage.co/support/#api-key 申请(秒得)。
5
+ 免费限制: 25 请求/天,5 请求/分钟。
6
+
7
+ 功能: 美股/ETF 行情、技术指标、外汇汇率、大宗商品、基本面数据。
8
+
9
+ 配置: ALPHA_VANTAGE_KEY 环境变量 或 ~/.aria/.env
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import os
16
+ import time
17
+ import urllib.request
18
+ from datetime import date, timedelta
19
+ from pathlib import Path
20
+ from typing import Dict, Optional
21
+
22
+ from ..base import BaseDataSource, FundamentalsResult, HistoryResult, QuoteResult
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ _BASE = "https://www.alphavantage.co/query"
27
+
28
+
29
+ def _load_key() -> str:
30
+ key = os.getenv("ALPHA_VANTAGE_KEY", "") or os.getenv("ALPHAVANTAGE_KEY", "")
31
+ if not key:
32
+ for p in [Path.home() / ".aria" / ".env", Path.home() / ".arthera" / ".env"]:
33
+ if p.exists():
34
+ for line in p.read_text(encoding="utf-8").splitlines():
35
+ if line.startswith(("ALPHA_VANTAGE_KEY=", "ALPHAVANTAGE_KEY=")):
36
+ key = line.split("=", 1)[1].strip()
37
+ break
38
+ if key:
39
+ break
40
+ if not key:
41
+ # providers.json 中存储的键名是 "alphavantage"(无下划线)
42
+ try:
43
+ import json as _json
44
+ _p = Path.home() / ".arthera" / "providers.json"
45
+ if _p.exists():
46
+ _raw = _json.loads(_p.read_text(encoding="utf-8"))
47
+ key = (
48
+ _raw.get("data", {}).get("alphavantage", {}).get("api_key", "")
49
+ or _raw.get("data", {}).get("alpha_vantage", {}).get("api_key", "")
50
+ )
51
+ except Exception:
52
+ pass
53
+ return key
54
+
55
+
56
+ def _fetch(params: dict, timeout: int = 15) -> Optional[dict]:
57
+ import json
58
+ qs = "&".join(f"{k}={v}" for k, v in params.items())
59
+ url = f"{_BASE}?{qs}"
60
+ try:
61
+ req = urllib.request.Request(url, headers={"User-Agent": "aria-code/1.0"})
62
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
63
+ return json.loads(resp.read())
64
+ except Exception as e:
65
+ logger.debug(f"[alpha_vantage] fetch 失败: {e}")
66
+ return None
67
+
68
+
69
+ class AlphaVantageSource(BaseDataSource):
70
+ """
71
+ Alpha Vantage 数据源。
72
+ 免费 key 足够日常使用(25次/天);付费 key 无频率限制。
73
+ """
74
+
75
+ name = "alpha_vantage"
76
+ markets = ["us", "hk", "forex", "commodity"]
77
+ requires_key = True
78
+
79
+ def __init__(self, config=None):
80
+ super().__init__(config)
81
+ self._key = _load_key()
82
+
83
+ def is_configured(self) -> bool:
84
+ return bool(self._key)
85
+
86
+ def quote(self, symbol: str) -> Optional[QuoteResult]:
87
+ data = _fetch({"function": "GLOBAL_QUOTE", "symbol": symbol, "apikey": self._key})
88
+ if not data:
89
+ return None
90
+ q = data.get("Global Quote", {})
91
+ if not q or "05. price" not in q:
92
+ return None
93
+ price = float(q.get("05. price", 0))
94
+ change = float(q.get("09. change", 0))
95
+ pct = float(q.get("10. change percent", "0%").replace("%", ""))
96
+ vol = float(q.get("06. volume", 0))
97
+ return QuoteResult(
98
+ symbol = symbol,
99
+ price = price,
100
+ change = change,
101
+ change_pct = pct,
102
+ volume = vol,
103
+ market = "us",
104
+ source = self.name,
105
+ timestamp = q.get("07. latest trading day", ""),
106
+ )
107
+
108
+ def history(
109
+ self,
110
+ symbol: str,
111
+ days: int = 365,
112
+ interval: str = "1d",
113
+ ) -> Optional[HistoryResult]:
114
+ try:
115
+ import pandas as pd
116
+ size = "compact" if days <= 100 else "full"
117
+ func = "TIME_SERIES_DAILY_ADJUSTED"
118
+ data = _fetch({"function": func, "symbol": symbol,
119
+ "outputsize": size, "apikey": self._key})
120
+ if not data:
121
+ return None
122
+ ts = data.get("Time Series (Daily)", {})
123
+ if not ts:
124
+ return None
125
+ cutoff = (date.today() - timedelta(days=days)).isoformat()
126
+ rows = []
127
+ for d, v in ts.items():
128
+ if d < cutoff:
129
+ continue
130
+ rows.append({
131
+ "date": d,
132
+ "open": float(v.get("1. open", 0)),
133
+ "high": float(v.get("2. high", 0)),
134
+ "low": float(v.get("3. low", 0)),
135
+ "close": float(v.get("5. adjusted close", v.get("4. close", 0))),
136
+ "volume": float(v.get("6. volume", 0)),
137
+ })
138
+ if not rows:
139
+ return None
140
+ df = pd.DataFrame(rows)
141
+ df["date"] = pd.to_datetime(df["date"])
142
+ df = df.set_index("date").sort_index()
143
+ return HistoryResult(symbol=symbol, data=df, source=self.name, interval="1d")
144
+ except Exception as e:
145
+ logger.debug(f"[alpha_vantage] history {symbol} 失败: {e}")
146
+ return None
147
+
148
+ def fundamentals(self, symbol: str) -> Optional[FundamentalsResult]:
149
+ data = _fetch({"function": "OVERVIEW", "symbol": symbol, "apikey": self._key})
150
+ if not data or not data.get("Symbol"):
151
+ return None
152
+ def _f(k, mult: float = 1.0):
153
+ v = data.get(k, "None")
154
+ try:
155
+ fv = float(v) if v not in ("None", "-", "", "0") else None
156
+ return fv * mult if fv is not None else None
157
+ except ValueError:
158
+ return None
159
+ return FundamentalsResult(
160
+ symbol = symbol,
161
+ pe_ttm = _f("TrailingPE"),
162
+ pb = _f("PriceToBookRatio"),
163
+ roe = _f("ReturnOnEquityTTM", 100),
164
+ revenue_growth = _f("RevenueGrowthQtrlyYOY"),
165
+ dividend_yield = _f("DividendYield", 100),
166
+ source = self.name,
167
+ )
168
+
169
+ def get_forex(self, from_currency: str, to_currency: str) -> Optional[Dict]:
170
+ """实时外汇汇率。"""
171
+ data = _fetch({"function": "CURRENCY_EXCHANGE_RATE",
172
+ "from_currency": from_currency,
173
+ "to_currency": to_currency,
174
+ "apikey": self._key})
175
+ if not data:
176
+ return None
177
+ r = data.get("Realtime Currency Exchange Rate", {})
178
+ return {
179
+ "from": from_currency, "to": to_currency,
180
+ "rate": float(r.get("5. Exchange Rate", 0)),
181
+ "time": r.get("6. Last Refreshed", ""),
182
+ }
183
+
184
+ def get_commodity(self, symbol: str = "WTI") -> Optional[HistoryResult]:
185
+ """大宗商品历史价格(WTI/BRENT/GOLD/COPPER 等)。"""
186
+ _MAP = {"WTI": "WTI", "BRENT": "BRENT", "GOLD": "GOLD",
187
+ "COPPER": "COPPER", "ALUMINUM": "ALUMINUM", "WHEAT": "WHEAT"}
188
+ func = _MAP.get(symbol.upper(), symbol.upper())
189
+ data = _fetch({"function": func, "interval": "monthly", "apikey": self._key})
190
+ if not data:
191
+ return None
192
+ try:
193
+ import pandas as pd
194
+ rows = [{"date": r["date"], "close": float(r["value"])}
195
+ for r in data.get("data", []) if r.get("value") not in (".", None)]
196
+ df = pd.DataFrame(rows)
197
+ df["date"] = pd.to_datetime(df["date"])
198
+ df = df.set_index("date").sort_index()
199
+ return HistoryResult(symbol=symbol, data=df, source=self.name, interval="1mo")
200
+ except Exception as e:
201
+ logger.debug(f"[alpha_vantage] commodity {symbol} 失败: {e}")
202
+ return None
@@ -0,0 +1,218 @@
1
+ """
2
+ datasources/sources/edgar_source.py — SEC EDGAR 美国上市公司财务数据
3
+ ====================================================================
4
+ 完全免费,无需 API key。数据来源:https://data.sec.gov (SEC EDGAR API)
5
+ 覆盖:10-K / 10-Q 财报、公司基本信息、财务报表、内幕交易披露。
6
+
7
+ 用法:
8
+ src = EDGARSource()
9
+ facts = src.get_company_facts("AAPL") # → 财务指标历史
10
+ filings = src.get_recent_filings("MSFT") # → 最近10-K/10-Q
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ import time
17
+ import urllib.request
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ from ..base import BaseDataSource, FundamentalsResult, QuoteResult
21
+
22
+ import os as _os
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ _EDGAR_API = "https://data.sec.gov"
27
+ _EDGAR_WWW = "https://www.sec.gov"
28
+ _HEADERS = {
29
+ "User-Agent": _os.environ.get("EDGAR_USER_AGENT", "aria-code contact@example.com"), # SEC requires self-identification
30
+ "Accept-Encoding": "gzip, deflate",
31
+ }
32
+
33
+ # 主要美股 ticker → CIK 缓存(常用的直接查,避免每次API调用)
34
+ _TICKER_CIK_CACHE: Dict[str, str] = {}
35
+
36
+
37
+ def _fetch_json(url: str, timeout: int = 15) -> Optional[Dict]:
38
+ try:
39
+ import gzip, json
40
+ req = urllib.request.Request(url, headers=_HEADERS)
41
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
42
+ raw = resp.read()
43
+ if resp.headers.get("Content-Encoding") == "gzip":
44
+ raw = gzip.decompress(raw)
45
+ return json.loads(raw)
46
+ except Exception as e:
47
+ logger.debug(f"[edgar] fetch {url} 失败: {e}")
48
+ return None
49
+
50
+
51
+ class EDGARSource(BaseDataSource):
52
+ """
53
+ SEC EDGAR 数据源 — 美国上市公司财报和披露文件。
54
+ """
55
+
56
+ name = "edgar"
57
+ markets = ["us"]
58
+ requires_key = False
59
+
60
+ def __init__(self, config=None):
61
+ super().__init__(config)
62
+ self._tickers: Optional[Dict] = None
63
+ self._cik_map: Dict[str, str] = {}
64
+
65
+ def is_configured(self) -> bool:
66
+ return True
67
+
68
+ def _load_ticker_map(self) -> None:
69
+ """加载 SEC 全量 ticker→CIK 映射(首次调用时下载一次)"""
70
+ if self._tickers is not None:
71
+ return
72
+ data = _fetch_json(f"{_EDGAR_WWW}/files/company_tickers.json")
73
+ if data:
74
+ for v in data.values():
75
+ ticker = v.get("ticker", "").upper()
76
+ cik = str(v.get("cik_str", "")).zfill(10)
77
+ if ticker:
78
+ self._cik_map[ticker] = cik
79
+ self._tickers = self._cik_map
80
+
81
+ def ticker_to_cik(self, symbol: str) -> Optional[str]:
82
+ self._load_ticker_map()
83
+ return self._cik_map.get(symbol.upper())
84
+
85
+ def get_company_facts(self, symbol: str) -> Optional[Dict[str, Any]]:
86
+ """
87
+ 获取公司全部财务事实(XBRL 格式)。
88
+ 返回包含 EPS/Revenue/NetIncome/Assets 等历史序列的字典。
89
+ """
90
+ cik = self.ticker_to_cik(symbol)
91
+ if not cik:
92
+ return {"error": f"未找到 {symbol} 的 CIK"}
93
+ data = _fetch_json(f"{_EDGAR_API}/api/xbrl/companyfacts/CIK{cik}.json")
94
+ if not data:
95
+ return None
96
+
97
+ us_gaap = data.get("facts", {}).get("us-gaap", {})
98
+ result = {"symbol": symbol, "cik": cik, "metrics": {}}
99
+ wanted = {
100
+ "Revenues": "revenue",
101
+ "RevenueFromContractWithCustomerExcludingAssessedTax": "revenue",
102
+ "NetIncomeLoss": "net_income",
103
+ "EarningsPerShareBasic": "eps_basic",
104
+ "EarningsPerShareDiluted": "eps_diluted",
105
+ "Assets": "total_assets",
106
+ "LiabilitiesAndStockholdersEquity": "total_equity",
107
+ "OperatingIncomeLoss": "operating_income",
108
+ "CommonStockSharesOutstanding": "shares_outstanding",
109
+ }
110
+ for gaap_key, alias in wanted.items():
111
+ if gaap_key in us_gaap:
112
+ units = us_gaap[gaap_key].get("units", {})
113
+ unit_key = "USD" if "USD" in units else ("shares" if "shares" in units else next(iter(units), None))
114
+ if unit_key and unit_key in units:
115
+ entries = [
116
+ {"end": e["end"], "val": e["val"], "form": e.get("form", "")}
117
+ for e in units[unit_key]
118
+ if e.get("form") in ("10-K", "10-Q", "20-F")
119
+ ]
120
+ if entries:
121
+ entries.sort(key=lambda x: x["end"], reverse=True)
122
+ result["metrics"][alias] = entries[:20]
123
+ return result
124
+
125
+ def get_recent_filings(self, symbol: str, form_types: List[str] = None) -> List[Dict]:
126
+ """获取最近提交的财务报告(10-K、10-Q、8-K 等)。"""
127
+ cik = self.ticker_to_cik(symbol)
128
+ if not cik:
129
+ return [{"error": f"未找到 {symbol} 的 CIK"}]
130
+ form_types = form_types or ["10-K", "10-Q", "8-K"]
131
+ data = _fetch_json(f"{_EDGAR_API}/submissions/CIK{cik}.json")
132
+ if not data:
133
+ return []
134
+
135
+ recent = data.get("filings", {}).get("recent", {})
136
+ forms = recent.get("form", [])
137
+ dates = recent.get("filingDate", [])
138
+ accnos = recent.get("accessionNumber", [])
139
+ docs = recent.get("primaryDocument", [])
140
+
141
+ results = []
142
+ for form, filing_date, accno, doc in zip(forms, dates, accnos, docs):
143
+ if form in form_types:
144
+ accno_clean = accno.replace("-", "")
145
+ url = f"https://www.sec.gov/Archives/edgar/data/{int(cik)}/{accno_clean}/{doc}"
146
+ results.append({
147
+ "form": form,
148
+ "date": filing_date,
149
+ "accession": accno,
150
+ "url": url,
151
+ "cik": cik,
152
+ })
153
+ if len(results) >= 20:
154
+ break
155
+ return results
156
+
157
+ def get_insider_trades(self, symbol: str, days: int = 90) -> List[Dict]:
158
+ """获取内幕交易披露(Form 4)。"""
159
+ cik = self.ticker_to_cik(symbol)
160
+ if not cik:
161
+ return []
162
+ data = _fetch_json(f"{_EDGAR_API}/submissions/CIK{cik}.json")
163
+ if not data:
164
+ return []
165
+
166
+ recent = data.get("filings", {}).get("recent", {})
167
+ forms = recent.get("form", [])
168
+ dates = recent.get("filingDate", [])
169
+ results = []
170
+ from datetime import date as _date, timedelta as _td
171
+ cutoff = (_date.today() - _td(days=days)).isoformat()
172
+
173
+ for form, filing_date in zip(forms, dates):
174
+ if form == "4" and filing_date >= cutoff:
175
+ results.append({"form": "4", "date": filing_date, "type": "insider"})
176
+ if len(results) >= 30:
177
+ break
178
+ return results
179
+
180
+ def quote(self, symbol: str) -> Optional[QuoteResult]:
181
+ facts = self.get_company_facts(symbol)
182
+ if not facts or "metrics" in facts and not facts["metrics"]:
183
+ return None
184
+ m = facts.get("metrics", {})
185
+ rev = m.get("revenue", [{}])[0].get("val", 0) if m.get("revenue") else 0
186
+ return QuoteResult(
187
+ symbol = symbol,
188
+ name = f"EDGAR:{symbol}",
189
+ price = 0.0,
190
+ market = "us",
191
+ source = self.name,
192
+ extra = {"annual_revenue": rev},
193
+ )
194
+
195
+ def fundamentals(self, symbol: str) -> Optional[FundamentalsResult]:
196
+ facts = self.get_company_facts(symbol)
197
+ if not facts:
198
+ return None
199
+ m = facts.get("metrics", {})
200
+
201
+ def _latest(key: str) -> float:
202
+ entries = m.get(key, [])
203
+ return float(entries[0]["val"]) if entries else 0.0
204
+
205
+ net_income = _latest("net_income")
206
+ revenue = _latest("revenue")
207
+ rev_yoy = 0.0
208
+ if m.get("revenue") and len(m["revenue"]) >= 2:
209
+ cur = float(m["revenue"][0]["val"])
210
+ prev = float(m["revenue"][1]["val"])
211
+ if prev:
212
+ rev_yoy = (cur - prev) / abs(prev) * 100
213
+
214
+ return FundamentalsResult(
215
+ symbol = symbol,
216
+ revenue_growth = rev_yoy,
217
+ source = self.name,
218
+ )
@@ -0,0 +1,197 @@
1
+ """
2
+ datasources/sources/finnhub_source.py — Finnhub 美股/港股数据源
3
+ ================================================================
4
+ 使用 ~/.arthera/providers.json 中配置的 Finnhub API key,
5
+ 提供实时行情、历史 K 线、基本面数据。
6
+ 免费套餐:每分钟 60 次请求,支持美股 / ETF / 指数 / 外汇 / 加密。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ import math
14
+ import time
15
+ import urllib.request
16
+ from datetime import datetime, timedelta, date, timezone
17
+ from pathlib import Path
18
+ from typing import Optional
19
+
20
+ import pandas as pd
21
+
22
+ from ..base import BaseDataSource, FundamentalsResult, HistoryResult, QuoteResult
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ _PROVIDERS_FILE = Path.home() / ".arthera" / "providers.json"
27
+ _BASE = "https://finnhub.io/api/v1"
28
+
29
+
30
+ def _read_finnhub_key() -> str:
31
+ try:
32
+ import os
33
+ env = os.getenv("FINNHUB_API_KEY", "") or os.getenv("FINNHUB_KEY", "")
34
+ if env:
35
+ return env
36
+ if _PROVIDERS_FILE.exists():
37
+ raw = json.loads(_PROVIDERS_FILE.read_text(encoding="utf-8"))
38
+ key = raw.get("data", {}).get("finnhub", {}).get("api_key", "")
39
+ if key:
40
+ return key
41
+ except Exception:
42
+ pass
43
+ return ""
44
+
45
+
46
+ def _fh_get(path: str, key: str, params: dict = None, timeout: int = 8) -> dict:
47
+ qs = "&".join(f"{k}={v}" for k, v in (params or {}).items())
48
+ url = f"{_BASE}{path}?token={key}" + (f"&{qs}" if qs else "")
49
+ req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
50
+ with urllib.request.urlopen(req, timeout=timeout) as r:
51
+ return json.loads(r.read())
52
+
53
+
54
+ class FinnhubSource(BaseDataSource):
55
+
56
+ name = "finnhub"
57
+ markets = ["us", "hk", "crypto"]
58
+ requires_key = True
59
+
60
+ def __init__(self, config=None):
61
+ super().__init__(config)
62
+ self._key = _read_finnhub_key()
63
+
64
+ def is_configured(self) -> bool:
65
+ return bool(self._key)
66
+
67
+ # ── Quote ─────────────────────────────────────────────────────────────────
68
+
69
+ def quote(self, symbol: str) -> Optional[QuoteResult]:
70
+ if not self._key:
71
+ return None
72
+ sym = symbol.upper().replace(".HK", "")
73
+ try:
74
+ data = _fh_get("/quote", self._key, {"symbol": sym})
75
+ price = float(data.get("c") or 0)
76
+ if price <= 0:
77
+ return None
78
+ prev = float(data.get("pc") or price)
79
+ chg_pct = round((price - prev) / prev * 100, 2) if prev else 0
80
+
81
+ # Extra: company profile for name / market cap
82
+ name = sym
83
+ mkt_cap = 0.0
84
+ currency = "USD"
85
+ try:
86
+ prof = _fh_get("/stock/profile2", self._key, {"symbol": sym})
87
+ name = prof.get("name") or sym
88
+ mkt_cap = float(prof.get("marketCapitalization") or 0) * 1e6
89
+ currency = prof.get("currency") or "USD"
90
+ except Exception:
91
+ pass
92
+
93
+ return QuoteResult(
94
+ symbol = symbol,
95
+ name = name,
96
+ price = price,
97
+ change = round(price - prev, 4),
98
+ change_pct = chg_pct,
99
+ volume = float(data.get("v") or 0),
100
+ high_52w = float(data.get("h") or 0),
101
+ low_52w = float(data.get("l") or 0),
102
+ market_cap = mkt_cap,
103
+ currency = currency,
104
+ market = "us",
105
+ source = self.name,
106
+ )
107
+ except Exception as e:
108
+ logger.debug(f"[finnhub] quote({symbol}) 失败: {e}")
109
+ return None
110
+
111
+ # ── History ───────────────────────────────────────────────────────────────
112
+
113
+ def history(self, symbol: str, days: int = 90, interval: str = "1d") -> Optional[HistoryResult]:
114
+ if not self._key:
115
+ return None
116
+ sym = symbol.upper()
117
+ resolution = "D" if interval in ("1d", "day", "daily") else "60"
118
+ _end = int(time.time())
119
+ _start = int((datetime.now() - timedelta(days=days + 5)).timestamp())
120
+ try:
121
+ data = _fh_get("/stock/candle", self._key, {
122
+ "symbol": sym, "resolution": resolution,
123
+ "from": _start, "to": _end,
124
+ })
125
+ if data.get("s") != "ok":
126
+ return None
127
+ t = data.get("t", [])
128
+ o = data.get("o", [])
129
+ h = data.get("h", [])
130
+ l = data.get("l", [])
131
+ c = data.get("c", [])
132
+ v = data.get("v", [])
133
+ if not c:
134
+ return None
135
+ df = pd.DataFrame({
136
+ "日期": [datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d") for ts in t],
137
+ "开盘": o, "最高": h, "最低": l, "收盘": c, "成交量": v,
138
+ })
139
+ df.index = pd.to_datetime(df["日期"])
140
+ # Also add English column aliases for DataService compatibility
141
+ df["Open"] = df["开盘"]
142
+ df["High"] = df["最高"]
143
+ df["Low"] = df["最低"]
144
+ df["Close"] = df["收盘"]
145
+ df["Volume"] = df["成交量"]
146
+ return HistoryResult(symbol=symbol, data=df, source=self.name, interval=interval)
147
+ except Exception as e:
148
+ logger.debug(f"[finnhub] history({symbol}) 失败: {e}")
149
+ return None
150
+
151
+ # ── Fundamentals ──────────────────────────────────────────────────────────
152
+
153
+ def fundamentals(self, symbol: str) -> Optional[FundamentalsResult]:
154
+ if not self._key:
155
+ return None
156
+ sym = symbol.upper()
157
+ try:
158
+ # Basic financials (PE, market cap, etc.)
159
+ metrics_data = _fh_get("/stock/metric", self._key, {"symbol": sym, "metric": "all"})
160
+ m = metrics_data.get("metric") or {}
161
+
162
+ def _mf(key: str) -> Optional[float]:
163
+ v = m.get(key)
164
+ if v is None:
165
+ return None
166
+ try:
167
+ fv = float(v)
168
+ return None if (math.isnan(fv) or fv == 0) else fv
169
+ except (TypeError, ValueError):
170
+ return None
171
+
172
+ pe = _mf("peTTM") or _mf("peExclExtraItemsTTM")
173
+ pb = _mf("pbAnnual") or _mf("pbQuarterly")
174
+ roe = _mf("roeTTM") or _mf("roeAnnual")
175
+ div_yield = _mf("dividendYieldIndicatedAnnual")
176
+ rev_growth = _mf("revenueGrowthTTMYoy")
177
+ eps_growth = _mf("epsGrowthTTMYoy")
178
+ mktcap_raw = _mf("marketCapitalization")
179
+ total_mv = (mktcap_raw * 1e6) if mktcap_raw else None
180
+
181
+ if pe is None and pb is None and roe is None:
182
+ return None
183
+
184
+ return FundamentalsResult(
185
+ symbol = symbol,
186
+ pe_ttm = pe,
187
+ pb = pb,
188
+ roe = roe,
189
+ revenue_growth = rev_growth,
190
+ net_profit_growth = eps_growth,
191
+ dividend_yield = div_yield,
192
+ total_mv = total_mv,
193
+ source = self.name,
194
+ )
195
+ except Exception as e:
196
+ logger.debug(f"[finnhub] fundamentals({symbol}) 失败: {e}")
197
+ return None