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
data_service.py ADDED
@@ -0,0 +1,481 @@
1
+ """
2
+ data_service.py — unified market data facade
3
+ ============================================
4
+
5
+ This module sits above MarketDataClient and datasources.DataRouter. It returns
6
+ one normalized shape for quotes, history, fundamentals, and technical signals
7
+ so report/backtest code does not need to know provider-specific schemas.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import time
13
+ from dataclasses import asdict, dataclass, field, is_dataclass
14
+ from datetime import datetime, timezone
15
+ from typing import Any, Dict, List, Optional, Tuple
16
+
17
+ from packages.aria_services.provider_health import (
18
+ GLOBAL_PROVIDER_HEALTH,
19
+ ProviderHealthRegistry,
20
+ ProviderIssue,
21
+ classify_provider_error,
22
+ )
23
+
24
+
25
+ def _dedupe(values: List[Any]) -> List[str]:
26
+ return list(dict.fromkeys(str(v) for v in values if v not in (None, "", [], {})))
27
+
28
+
29
+ def _is_present(value: Any) -> bool:
30
+ return value not in (None, "", [], {})
31
+
32
+
33
+ def _to_dict(value: Any) -> Dict[str, Any]:
34
+ if value is None:
35
+ return {}
36
+ if isinstance(value, dict):
37
+ return dict(value)
38
+ if is_dataclass(value):
39
+ return asdict(value)
40
+ if hasattr(value, "to_dict"):
41
+ try:
42
+ return dict(value.to_dict())
43
+ except Exception:
44
+ pass
45
+ if hasattr(value, "__dict__"):
46
+ return dict(value.__dict__)
47
+ return {}
48
+
49
+
50
+ def _provider_chain(data: Dict[str, Any], fallback: str) -> List[str]:
51
+ chain = data.get("provider_chain")
52
+ if isinstance(chain, list):
53
+ return _dedupe(chain)
54
+ return _dedupe([data.get("provider"), data.get("source"), fallback])
55
+
56
+
57
+ def _provider_from_call(method: str, data: Dict[str, Any] | None = None) -> str:
58
+ data = data or {}
59
+ provider = data.get("provider") or data.get("source")
60
+ if provider:
61
+ return str(provider)
62
+ return "market_data_client" if method in {"quote", "history", "fundamentals", "technical_indicators"} else method
63
+
64
+
65
+ def _data_with_success(data: Dict[str, Any], success: bool) -> Dict[str, Any]:
66
+ """Return payload with its embedded success flag matching DataServiceResult."""
67
+ if not data:
68
+ return {}
69
+ out = dict(data)
70
+ out["success"] = bool(success)
71
+ return out
72
+
73
+
74
+ def _utc_now() -> datetime:
75
+ return datetime.now(timezone.utc)
76
+
77
+
78
+ def _utc_timestamp() -> str:
79
+ return _utc_now().isoformat(timespec="seconds").replace("+00:00", "Z")
80
+
81
+
82
+ def _parse_timestamp(value: Any) -> Optional[datetime]:
83
+ if not value:
84
+ return None
85
+ if isinstance(value, datetime):
86
+ return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
87
+ text = str(value).strip()
88
+ if not text:
89
+ return None
90
+ if text.endswith("Z"):
91
+ text = text[:-1] + "+00:00"
92
+ try:
93
+ dt = datetime.fromisoformat(text)
94
+ return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
95
+ except ValueError:
96
+ return None
97
+
98
+
99
+ @dataclass
100
+ class DataServiceResult:
101
+ kind: str
102
+ symbol: str
103
+ success: bool
104
+ data: Dict[str, Any] = field(default_factory=dict)
105
+ provider_chain: List[str] = field(default_factory=list)
106
+ warnings: List[str] = field(default_factory=list)
107
+ errors: List[str] = field(default_factory=list)
108
+ missing_fields: List[str] = field(default_factory=list)
109
+ source: str = ""
110
+ stale: bool = False
111
+ quality: Dict[str, Any] = field(default_factory=dict)
112
+ timestamp: str = field(default_factory=_utc_timestamp)
113
+
114
+
115
+ @dataclass
116
+ class DataBundle:
117
+ symbol: str
118
+ quote: Dict[str, Any] = field(default_factory=dict)
119
+ history: Dict[str, Any] = field(default_factory=dict)
120
+ fundamentals: Dict[str, Any] = field(default_factory=dict)
121
+ technical: Dict[str, Any] = field(default_factory=dict)
122
+ provider_chain: List[str] = field(default_factory=list)
123
+ warnings: List[str] = field(default_factory=list)
124
+ errors: List[str] = field(default_factory=list)
125
+ missing_fields: List[str] = field(default_factory=list)
126
+ quality: Dict[str, Any] = field(default_factory=dict)
127
+ status: str = "data_unavailable"
128
+ timestamp: str = field(default_factory=_utc_timestamp)
129
+
130
+
131
+ class DataService:
132
+ """Unified data entrypoint with cache, fallback, provenance and validation."""
133
+
134
+ def __init__(
135
+ self,
136
+ market_client: Any = None,
137
+ router: Any = None,
138
+ ttl_seconds: int = 60,
139
+ max_quote_age_seconds: int = 900,
140
+ provider_health: ProviderHealthRegistry | None = None,
141
+ ):
142
+ if market_client is None:
143
+ from market_data_client import MarketDataClient
144
+ market_client = MarketDataClient()
145
+ self._router_disabled = router is False
146
+ if self._router_disabled:
147
+ router = None
148
+ if router is None:
149
+ if not self._router_disabled:
150
+ try:
151
+ from datasources.router import get_router
152
+ router = get_router()
153
+ except Exception:
154
+ router = None
155
+ self.market_client = market_client
156
+ self.router = router
157
+ self.ttl_seconds = ttl_seconds
158
+ self.max_quote_age_seconds = max_quote_age_seconds
159
+ self.provider_health = provider_health or GLOBAL_PROVIDER_HEALTH
160
+ self._cache: Dict[Tuple[Any, ...], Tuple[float, DataServiceResult]] = {}
161
+
162
+ def quote(self, symbol: str) -> DataServiceResult:
163
+ return self._cached(("quote", symbol), lambda: self._quote_uncached(symbol))
164
+
165
+ def history(self, symbol: str, days: int = 370, interval: str = "1d") -> DataServiceResult:
166
+ return self._cached(("history", symbol, days, interval), lambda: self._history_uncached(symbol, days, interval))
167
+
168
+ def fundamentals(self, symbol: str) -> DataServiceResult:
169
+ return self._cached(("fundamentals", symbol), lambda: self._fundamentals_uncached(symbol))
170
+
171
+ def technical_indicators(self, symbol: str, days: int = 120) -> DataServiceResult:
172
+ return self._cached(("technical", symbol, days), lambda: self._technical_uncached(symbol, days))
173
+
174
+ def bundle(self, symbol: str, history_days: int = 370, technical_days: int = 120) -> DataBundle:
175
+ quote = self.quote(symbol)
176
+ history = self.history(symbol, days=history_days)
177
+ fundamentals = self.fundamentals(symbol)
178
+ technical = self.technical_indicators(symbol, days=technical_days)
179
+
180
+ provider_chain = _dedupe(
181
+ quote.provider_chain + history.provider_chain + fundamentals.provider_chain + technical.provider_chain
182
+ )
183
+ warnings = quote.warnings + history.warnings + fundamentals.warnings + technical.warnings
184
+ errors = quote.errors + history.errors + fundamentals.errors + technical.errors
185
+ missing_fields = self._bundle_missing_fields(quote, history, fundamentals, technical)
186
+ stale = quote.stale or history.stale or technical.stale
187
+
188
+ core_success = quote.success and history.success and technical.success
189
+ any_success = quote.success or history.success or fundamentals.success or technical.success
190
+ status = "complete" if core_success and not missing_fields else "partial" if any_success else "data_unavailable"
191
+ if stale and status == "complete":
192
+ status = "stale"
193
+ return DataBundle(
194
+ symbol=symbol,
195
+ quote=quote.data,
196
+ history=history.data,
197
+ fundamentals=fundamentals.data,
198
+ technical=technical.data,
199
+ provider_chain=provider_chain,
200
+ warnings=warnings[:10],
201
+ errors=errors[:10],
202
+ missing_fields=missing_fields,
203
+ quality={
204
+ "status": status,
205
+ "stale": stale,
206
+ "providers": provider_chain,
207
+ "provider_health": self.provider_health.snapshot(),
208
+ "missing_fields": missing_fields,
209
+ "warnings": warnings[:10],
210
+ "errors": errors[:10],
211
+ },
212
+ status=status,
213
+ )
214
+
215
+ def _cached(self, key: Tuple[Any, ...], factory: Any) -> DataServiceResult:
216
+ now = time.time()
217
+ cached = self._cache.get(key)
218
+ if cached and now - cached[0] <= self.ttl_seconds:
219
+ return cached[1]
220
+ result = factory()
221
+ self._cache[key] = (now, result)
222
+ return result
223
+
224
+ def _quote_uncached(self, symbol: str) -> DataServiceResult:
225
+ warnings: List[str] = []
226
+ data = self._call_market("quote", symbol, warnings)
227
+ if not self._valid_quote(data):
228
+ primary = data
229
+ fallback = self._call_router("quote", symbol, warnings)
230
+ data = fallback or primary
231
+ return self._result("quote", symbol, data, warnings, required=["price"], validator=self._valid_quote)
232
+
233
+ def _history_uncached(self, symbol: str, days: int, interval: str) -> DataServiceResult:
234
+ warnings: List[str] = []
235
+ data = self._call_market("history", symbol, warnings, days=days, interval=interval)
236
+ if not self._valid_history(data):
237
+ data = self._call_router("history", symbol, warnings, days=days, interval=interval)
238
+ return self._result("history", symbol, data, warnings, required=["data"], validator=self._valid_history)
239
+
240
+ def _fundamentals_uncached(self, symbol: str) -> DataServiceResult:
241
+ warnings: List[str] = []
242
+ data = self._call_market("fundamentals", symbol, warnings)
243
+ if not self._valid_payload(data):
244
+ data = self._call_router("fundamentals", symbol, warnings)
245
+ return self._result("fundamentals", symbol, data, warnings, required=[])
246
+
247
+ def _technical_uncached(self, symbol: str, days: int) -> DataServiceResult:
248
+ warnings: List[str] = []
249
+ data = self._call_market("technical_indicators", symbol, warnings, days=days)
250
+
251
+ # Yahoo Finance v8 fallback — for US symbols when primary TA source is rate-limited
252
+ # or returns insufficient data (newly-listed stocks, ETFs without MDC coverage)
253
+ _sym = symbol.upper().strip()
254
+ _is_us = not (_sym.endswith((".SZ", ".SS", ".SH", ".HK")) or _sym.isdigit())
255
+ _needs_fallback = _is_us and (
256
+ not data or
257
+ data.get("success") is False or
258
+ (data.get("success") and data.get("rsi") is None)
259
+ )
260
+ if _needs_fallback:
261
+ try:
262
+ import json as _jv8, urllib.request as _uv8, statistics as _sv8
263
+ _url = (
264
+ f"https://query1.finance.yahoo.com/v8/finance/chart/{_sym}"
265
+ "?interval=1d&range=6mo"
266
+ )
267
+ _req = _uv8.Request(_url, headers={
268
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
269
+ "Accept": "application/json",
270
+ })
271
+ with _uv8.urlopen(_req, timeout=10) as _r:
272
+ _raw = _jv8.loads(_r.read())
273
+ _res = _raw["chart"]["result"][0]
274
+ _q = _res["indicators"]["quote"][0]
275
+ _c = [x for x in _q.get("close", []) if x is not None]
276
+ _v = [x for x in _q.get("volume", []) if x is not None]
277
+ if len(_c) >= 14:
278
+ def _ema(p, n):
279
+ k, r = 2/(n+1), [p[0]]
280
+ for x in p[1:]: r.append(x*k + r[-1]*(1-k))
281
+ return r
282
+ _d = [_c[i]-_c[i-1] for i in range(1, len(_c))]
283
+ _g = [max(x,0) for x in _d]; _l = [max(-x,0) for x in _d]
284
+ _ag = sum(_g[:14])/14; _al = sum(_l[:14])/14
285
+ for i in range(14, len(_g)):
286
+ _ag = (_ag*13+_g[i])/14; _al = (_al*13+_l[i])/14
287
+ _rsi = (100 - 100/(1+_ag/_al)) if _al else 100.0
288
+ n = len(_c)
289
+ _ma20 = sum(_c[-20:])/min(20, n)
290
+ _ma60 = sum(_c[-60:])/min(60, n) if n >= 14 else _ma20
291
+ _std = _sv8.stdev(_c[-min(20,n):]) if n >= 2 else 0
292
+ _v8_data: Dict[str, Any] = {
293
+ "success": True,
294
+ "price": round(_c[-1], 2),
295
+ "rsi": round(_rsi, 2),
296
+ "ma20": round(_ma20, 2),
297
+ "ma60": round(_ma60, 2),
298
+ "bb_upper": round(_ma20 + 2*_std, 2),
299
+ "bb_mid": round(_ma20, 2),
300
+ "bb_lower": round(_ma20 - 2*_std, 2),
301
+ "provider": "yahoo_v8",
302
+ "history_bars": n,
303
+ }
304
+ if n >= 26:
305
+ _e12 = _ema(_c, 12); _e26 = _ema(_c, 26)
306
+ _md = [a-b for a,b in zip(_e12, _e26)]
307
+ _sg = _ema(_md, 9)
308
+ _v8_data["macd"] = round(_md[-1], 4)
309
+ _v8_data["macd_signal"] = round(_sg[-1], 4)
310
+ _v8_data["macd_hist"] = round(_md[-1]-_sg[-1], 4)
311
+ if _v:
312
+ _v8_data["volume"] = int(_v[-1])
313
+ data = _v8_data
314
+ warnings.append(f"yahoo_v8 fallback ({n} bars)")
315
+ elif len(_c) > 0:
316
+ # Too few bars for TA — still surface current price and bar count
317
+ data = {
318
+ "success": False,
319
+ "price": round(_c[-1], 2),
320
+ "history_bars": len(_c),
321
+ "provider": "yahoo_v8",
322
+ }
323
+ if _v:
324
+ data["volume"] = int(_v[-1])
325
+ _bar_str = f"{len(_c)} 个交易日"
326
+ warnings.append(f"数据不足(仅 {_bar_str})— 新上市标的,TA 指标需至少 14 日历史")
327
+ except Exception:
328
+ pass
329
+
330
+ return self._result("technical", symbol, data, warnings, required=["rsi", "macd", "ma20"])
331
+
332
+ def _call_market(self, method: str, symbol: str, warnings: List[str], **kwargs: Any) -> Dict[str, Any]:
333
+ try:
334
+ fn = getattr(self.market_client, method)
335
+ data = _to_dict(fn(symbol, **kwargs))
336
+ provider = _provider_from_call(method, data)
337
+ if data.get("success") is False:
338
+ issue = classify_provider_error(provider, data.get("error") or "unavailable")
339
+ self.provider_health.mark_issue(issue)
340
+ warnings.append(self._format_issue(method, issue))
341
+ elif data:
342
+ self.provider_health.mark_success(provider)
343
+ return data
344
+ except Exception as exc:
345
+ issue = classify_provider_error("market_data_client", exc)
346
+ self.provider_health.mark_issue(issue)
347
+ warnings.append(self._format_issue(method, issue))
348
+ return {}
349
+
350
+ def _call_router(self, method: str, symbol: str, warnings: List[str], **kwargs: Any) -> Dict[str, Any]:
351
+ if self._router_disabled:
352
+ return {}
353
+ if self.router is None:
354
+ issue = classify_provider_error("data_router", "router unavailable")
355
+ self.provider_health.mark_issue(issue)
356
+ warnings.append(self._format_issue(method, issue))
357
+ return {}
358
+ try:
359
+ fn = getattr(self.router, method)
360
+ data = _to_dict(fn(symbol, **kwargs))
361
+ if data:
362
+ data.setdefault("success", True)
363
+ data.setdefault("provider", data.get("source") or f"data_router.{method}")
364
+ data.setdefault("provider_chain", _dedupe([data.get("provider"), data.get("source")]))
365
+ self.provider_health.mark_success(str(data.get("provider")))
366
+ return data
367
+ except Exception as exc:
368
+ issue = classify_provider_error("data_router", exc)
369
+ self.provider_health.mark_issue(issue)
370
+ warnings.append(self._format_issue(method, issue))
371
+ return {}
372
+
373
+ @staticmethod
374
+ def _format_issue(method: str, issue: ProviderIssue) -> str:
375
+ return f"{issue.provider}.{method}: {issue.category} — {issue.message}"
376
+
377
+ def _result(
378
+ self,
379
+ kind: str,
380
+ symbol: str,
381
+ data: Dict[str, Any],
382
+ warnings: List[str],
383
+ required: List[str],
384
+ validator: Any = None,
385
+ ) -> DataServiceResult:
386
+ validator = validator or self._valid_payload
387
+ success = validator(data)
388
+ missing = [key for key in required if not _is_present(data.get(key))]
389
+ if kind == "quote" and "price" in required and not self._valid_quote(data):
390
+ if "price" not in missing:
391
+ missing.append("price")
392
+ if data.get("success") is False:
393
+ success = False
394
+ payload = _data_with_success(data, success)
395
+ source = str(data.get("provider") or data.get("source") or "")
396
+ payload_ts = data.get("timestamp") or data.get("asof") or data.get("as_of")
397
+ timestamp = str(payload_ts or _utc_timestamp())
398
+ stale = self._is_stale(kind, payload_ts)
399
+ errors = []
400
+ if data.get("error"):
401
+ errors.append(str(data.get("error")))
402
+ status = "ok" if success and not missing and not stale else (
403
+ "stale" if success and stale else "partial" if success else "unavailable"
404
+ )
405
+ return DataServiceResult(
406
+ kind=kind,
407
+ symbol=symbol,
408
+ success=success,
409
+ data=payload if payload else {},
410
+ provider_chain=_provider_chain(data, kind) if data else [],
411
+ warnings=warnings[:5],
412
+ errors=errors[:5],
413
+ missing_fields=missing,
414
+ source=source,
415
+ stale=stale,
416
+ quality={
417
+ "status": status,
418
+ "stale": stale,
419
+ "source": source,
420
+ "providers": _provider_chain(data, kind) if data else [],
421
+ "provider_health": self.provider_health.snapshot(),
422
+ "missing_fields": missing,
423
+ "warnings": warnings[:5],
424
+ "errors": errors[:5],
425
+ },
426
+ timestamp=timestamp,
427
+ )
428
+
429
+ def _is_stale(self, kind: str, timestamp: Any) -> bool:
430
+ if kind != "quote":
431
+ return False
432
+ dt = _parse_timestamp(timestamp)
433
+ if dt is None:
434
+ return False
435
+ return (_utc_now() - dt).total_seconds() > self.max_quote_age_seconds
436
+
437
+ @staticmethod
438
+ def _valid_payload(data: Dict[str, Any]) -> bool:
439
+ return bool(data) and data.get("success") is not False
440
+
441
+ @staticmethod
442
+ def _valid_quote(data: Dict[str, Any]) -> bool:
443
+ if not data or data.get("success") is False:
444
+ return False
445
+ price = data.get("price")
446
+ try:
447
+ return price is not None and float(price) > 0
448
+ except (TypeError, ValueError):
449
+ return False
450
+
451
+ @staticmethod
452
+ def _valid_history(data: Dict[str, Any]) -> bool:
453
+ if not data or data.get("success") is False:
454
+ return False
455
+ rows = data.get("data")
456
+ try:
457
+ return rows is not None and len(rows) > 0
458
+ except TypeError:
459
+ return False
460
+
461
+ @staticmethod
462
+ def _bundle_missing_fields(
463
+ quote: DataServiceResult,
464
+ history: DataServiceResult,
465
+ fundamentals: DataServiceResult,
466
+ technical: DataServiceResult,
467
+ ) -> List[str]:
468
+ missing: List[str] = []
469
+ if not quote.success or not _is_present(quote.data.get("price")):
470
+ missing.append("price")
471
+ if not history.success:
472
+ missing.append("history")
473
+ for key in ("pe_ratio", "pe_ttm", "pb_ratio", "pb", "roe"):
474
+ if _is_present(fundamentals.data.get(key)):
475
+ break
476
+ else:
477
+ missing.append("fundamentals")
478
+ for key in ("rsi", "macd", "ma20"):
479
+ if not _is_present(technical.data.get(key)):
480
+ missing.append(key)
481
+ return _dedupe(missing)
@@ -0,0 +1,23 @@
1
+ """
2
+ datasources/ — Aria Code 统一市场数据层
3
+ ========================================
4
+ 用户在 ~/.aria/datasources.yaml 配置数据源优先级,
5
+ 路由器按序尝试,首个成功的结果直接返回。
6
+
7
+ 支持市场:
8
+ A股 → akshare / tushare / eastmoney
9
+ 美股 → yfinance / polygon / finnhub / alphavantage
10
+ 加密 → ccxt (binance/okx) / yfinance
11
+ 基本面 → tushare / yfinance
12
+
13
+ 快速使用:
14
+ from datasources.router import DataRouter
15
+ router = DataRouter()
16
+ print(router.quote("AAPL"))
17
+ print(router.quote("000001")) # A股
18
+ print(router.quote("BTC/USDT")) # 加密
19
+ """
20
+
21
+ from .router import DataRouter, get_router
22
+
23
+ __all__ = ["DataRouter", "get_router"]
datasources/base.py ADDED
@@ -0,0 +1,166 @@
1
+ """
2
+ datasources/base.py — 数据源统一接口
3
+ =====================================
4
+ 所有数据源实现 BaseDataSource,输出统一 schema,
5
+ 上层代码不关心具体是 akshare / yfinance / tushare。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from abc import ABC, abstractmethod
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Dict, List, Optional
13
+
14
+
15
+ # ── 统一输出 schema ────────────────────────────────────────────────────────────
16
+
17
+ @dataclass
18
+ class QuoteResult:
19
+ symbol: str
20
+ name: str = ""
21
+ price: float = 0.0
22
+ change: float = 0.0
23
+ change_pct: float = 0.0
24
+ volume: float = 0.0
25
+ market_cap: float = 0.0
26
+ pe_ttm: float = 0.0
27
+ pb: float = 0.0
28
+ high_52w: float = 0.0
29
+ low_52w: float = 0.0
30
+ currency: str = "CNY"
31
+ market: str = "" # "a_share" | "us" | "hk" | "crypto"
32
+ source: str = "" # 实际使用的数据源名称
33
+ timestamp: str = ""
34
+ extra: Dict[str, Any] = field(default_factory=dict)
35
+
36
+ def to_dict(self) -> Dict:
37
+ return {
38
+ "symbol": self.symbol, "name": self.name,
39
+ "price": self.price, "change": self.change,
40
+ "change_pct": self.change_pct, "volume": self.volume,
41
+ "market_cap": self.market_cap, "pe_ttm": self.pe_ttm,
42
+ "pb": self.pb, "high_52w": self.high_52w, "low_52w": self.low_52w,
43
+ "currency": self.currency, "market": self.market,
44
+ "source": self.source, "timestamp": self.timestamp,
45
+ **self.extra,
46
+ }
47
+
48
+
49
+ @dataclass
50
+ class HistoryResult:
51
+ symbol: str
52
+ data: Any = None # pandas DataFrame
53
+ source: str = ""
54
+ interval: str = "1d"
55
+
56
+
57
+ @dataclass
58
+ class FundamentalsResult:
59
+ symbol: str
60
+ pe_ttm: Optional[float] = None
61
+ pb: Optional[float] = None
62
+ roe: Optional[float] = None
63
+ revenue_growth: Optional[float] = None
64
+ net_profit_growth: Optional[float] = None
65
+ dividend_yield: Optional[float] = None
66
+ total_mv: Optional[float] = None
67
+ source: str = ""
68
+
69
+
70
+ # ── 基类 ──────────────────────────────────────────────────────────────────────
71
+
72
+ class BaseDataSource(ABC):
73
+ """
74
+ 所有数据源的抽象基类。
75
+
76
+ 子类实现 `supports()` 判断是否支持该 symbol,
77
+ 再实现具体的 `quote()` / `history()` / `fundamentals()`。
78
+ """
79
+
80
+ name: str = "base" # 数据源唯一标识
81
+ markets: List[str] = [] # 支持的市场: "a_share", "us", "hk", "crypto"
82
+ requires_key: bool = False # 是否需要 API key
83
+
84
+ def __init__(self, config: Dict[str, Any] = None):
85
+ self.config = config or {}
86
+ self._available: Optional[bool] = None
87
+
88
+ def supports(self, symbol: str) -> bool:
89
+ """判断该数据源是否能处理这个 symbol(子类可覆盖)"""
90
+ market = _detect_market(symbol)
91
+ return market in self.markets
92
+
93
+ def is_configured(self) -> bool:
94
+ """数据源是否已配置好(有 key 等)"""
95
+ return True
96
+
97
+ @abstractmethod
98
+ def quote(self, symbol: str) -> Optional[QuoteResult]:
99
+ """获取实时行情(同步)"""
100
+ ...
101
+
102
+ def history(
103
+ self,
104
+ symbol: str,
105
+ days: int = 90,
106
+ interval: str = "1d",
107
+ ) -> Optional[HistoryResult]:
108
+ """获取历史 OHLCV(同步,子类按需实现)"""
109
+ return None
110
+
111
+ def fundamentals(self, symbol: str) -> Optional[FundamentalsResult]:
112
+ """获取基本面数据(子类按需实现)"""
113
+ return None
114
+
115
+
116
+ # ── 工具函数 ──────────────────────────────────────────────────────────────────
117
+
118
+ def _detect_market(symbol: str) -> str:
119
+ """简单推断 symbol 所属市场"""
120
+ s = symbol.upper().replace(" ", "")
121
+
122
+ # 加密货币
123
+ if "/" in s or s in ("BTC", "ETH", "SOL", "DOGE", "BNB", "ADA", "XRP"):
124
+ return "crypto"
125
+ if s.endswith(("-USD", "-USDT", "-BTC")) or s.endswith("USDT"):
126
+ return "crypto"
127
+
128
+ # 大宗商品
129
+ _COMMODITIES = {"WTI", "BRENT", "GOLD", "SILVER", "COPPER",
130
+ "ALUMINUM", "WHEAT", "CORN", "SOYBEAN", "NATGAS", "GAS"}
131
+ if s in _COMMODITIES:
132
+ return "commodity"
133
+
134
+ # 外汇(格式: USDCNY, USD/CNY, EURUSD 等)
135
+ _FX_CURRENCIES = {"USD", "EUR", "GBP", "JPY", "CNY", "HKD",
136
+ "AUD", "CAD", "CHF", "KRW", "SGD", "INR"}
137
+ if len(s) == 6 and s[:3] in _FX_CURRENCIES and s[3:] in _FX_CURRENCIES:
138
+ return "forex"
139
+ if "/" in s:
140
+ parts = s.split("/")
141
+ if len(parts) == 2 and parts[0] in _FX_CURRENCIES and parts[1] in _FX_CURRENCIES:
142
+ return "forex"
143
+
144
+ # A股:数字代码 or 带前缀
145
+ if s.startswith(("SH", "SZ", "BJ")):
146
+ return "a_share"
147
+ try:
148
+ int(s[:6])
149
+ if len(s) == 6 or (len(s) > 6 and not s[6:].isalpha()):
150
+ return "a_share"
151
+ except ValueError:
152
+ pass
153
+
154
+ # 港股
155
+ if s.endswith(".HK") or (s.isdigit() and len(s) == 5):
156
+ return "hk"
157
+
158
+ # 宏观指标
159
+ _MACRO = {"GDP", "CPI", "CPIYOY", "PCE", "UNRATE", "NFP", "FEDFUNDS",
160
+ "US10Y", "US2Y", "US3M", "VIX", "M2", "MORTGAGE", "HOUSING",
161
+ "SP500", "NASDAQ", "WILSHIRE", "USDCNY", "USDINR", "USDEUR"}
162
+ if s in _MACRO:
163
+ return "macro"
164
+
165
+ # 默认美股
166
+ return "us"