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
aliyun_data_client.py ADDED
@@ -0,0 +1,660 @@
1
+ """
2
+ aliyun_data_client.py — Arthera Alibaba Cloud data services client.
3
+
4
+ Architecture
5
+ ------------
6
+ The Arthera quant backend runs two HTTP services on Alibaba Cloud:
7
+
8
+ ┌─────────────────────────────────────────────────────────────────┐
9
+ │ cloud_api_server.py (FastAPI, default port 8000) │
10
+ │ /api/v1/quant/factors/{symbol} → calculate_factors │
11
+ │ /api/v1/quant/ai/signal → AI trading signal │
12
+ │ /api/v1/quant/backtest → run backtest │
13
+ │ /api/v1/quant/predict → ML predictions │
14
+ │ /api/v1/ai/market-insights → market insights (AI) │
15
+ │ /api/v1/ai/portfolio-analysis → portfolio analysis │
16
+ │ /api/v1/ai/investment-decision → investment decision │
17
+ │ /api/v1/market/quote/{symbol} → real-time quote │
18
+ │ /api/v1/market/search → stock search │
19
+ │ /api/v1/market/popular → popular stocks list │
20
+ │ /health → health check │
21
+ └─────────────────────────────────────────────────────────────────┘
22
+
23
+ ┌─────────────────────────────────────────────────────────────────┐
24
+ │ akshare_data_server.py (aiohttp, default port 8002) │
25
+ │ /stock/{symbol} → A股历史 OHLCV │
26
+ │ /stocks → multi-symbol batch │
27
+ │ /cn/indices → 上证/深成/沪深300 indices │
28
+ │ /hk/realtime → Hong Kong real-time │
29
+ │ /health → health check │
30
+ └─────────────────────────────────────────────────────────────────┘
31
+
32
+ Configuration (env vars or ~/.arthera/config.json)
33
+ ---------------------------------------------------
34
+ ARTHERA_CLOUD_URL base URL of cloud_api_server (default: http://127.0.0.1:8000)
35
+ ARTHERA_DATA_URL base URL of akshare_data_server (default: http://127.0.0.1:8002)
36
+ ARTHERA_API_TOKEN JWT Bearer token for authenticated endpoints
37
+
38
+ Circuit-breaker fallback
39
+ -------------------------
40
+ If cloud services are unreachable, all methods silently fall back to local
41
+ yfinance / akshare calls so the CLI never hard-errors on connectivity.
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ import asyncio
47
+ import json
48
+ import logging
49
+ import os
50
+ import time
51
+ from dataclasses import dataclass, field
52
+ from typing import Any, Dict, List, Optional
53
+
54
+ logger = logging.getLogger(__name__)
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class CloudHealthSummary:
59
+ schema: str
60
+ total: int
61
+ ok: int
62
+ warn: int
63
+ err: int
64
+ breaker_open: int
65
+ token_set: bool
66
+ status: str
67
+ detail: str
68
+ suggestion: str
69
+
70
+ def to_dict(self) -> Dict[str, Any]:
71
+ return {
72
+ "schema": self.schema,
73
+ "total": self.total,
74
+ "ok": self.ok,
75
+ "warn": self.warn,
76
+ "err": self.err,
77
+ "breaker_open": self.breaker_open,
78
+ "token_set": self.token_set,
79
+ "status": self.status,
80
+ "detail": self.detail,
81
+ "suggestion": self.suggestion,
82
+ }
83
+
84
+
85
+ def summarize_cloud_health(
86
+ cloud_health: Optional[Dict[str, Any]] = None,
87
+ data_health: Optional[Dict[str, Any]] = None,
88
+ status: Optional[Dict[str, Any]] = None,
89
+ ) -> CloudHealthSummary:
90
+ cloud_health = dict(cloud_health or {})
91
+ data_health = dict(data_health or {})
92
+ status = dict(status or {})
93
+
94
+ checks = [
95
+ ("cloud_api_server", cloud_health),
96
+ ("akshare_data_server", data_health),
97
+ ]
98
+ ok = warn = err = breaker_open = 0
99
+ detail_bits: list[str] = []
100
+ for name, health in checks:
101
+ svc_status = str(health.get("status") or "unknown")
102
+ breaker_value = status.get("cloud_cb") if name == "cloud_api_server" else status.get("data_cb")
103
+ breaker = str(breaker_value or "closed")
104
+ breaker_is_open = breaker == "open"
105
+ if breaker_is_open:
106
+ breaker_open += 1
107
+ if svc_status in ("healthy", "ok", "ready", "online"):
108
+ ok += 1
109
+ elif svc_status == "unreachable" or breaker_is_open:
110
+ err += 1
111
+ else:
112
+ warn += 1
113
+ detail_bits.append(f"{name}={svc_status}")
114
+
115
+ if err:
116
+ overall = "err"
117
+ elif warn or breaker_open:
118
+ overall = "warn"
119
+ else:
120
+ overall = "ok"
121
+
122
+ token_set = bool(status.get("has_token"))
123
+ if overall == "ok":
124
+ suggestion = "All cloud services healthy."
125
+ elif token_set:
126
+ suggestion = "Retry /cloud health or /doctor --network after cooldown."
127
+ else:
128
+ suggestion = "Check /cloud set, /cloud data, and /cloud token."
129
+
130
+ return CloudHealthSummary(
131
+ schema="aria.cloud_health_summary.v1",
132
+ total=2,
133
+ ok=ok,
134
+ warn=warn,
135
+ err=err,
136
+ breaker_open=breaker_open,
137
+ token_set=token_set,
138
+ status=overall,
139
+ detail=", ".join(detail_bits),
140
+ suggestion=suggestion,
141
+ )
142
+
143
+ # ---------------------------------------------------------------------------
144
+ # Configuration helpers
145
+ # ---------------------------------------------------------------------------
146
+
147
+ def _cfg_path() -> str:
148
+ return os.path.join(os.path.expanduser("~"), ".arthera", "config.json")
149
+
150
+
151
+ def _load_cloud_config() -> Dict[str, str]:
152
+ """Load cloud config from ~/.arthera/config.json, override with env vars."""
153
+ cfg: Dict[str, str] = {
154
+ "cloud_url": "http://127.0.0.1:8000",
155
+ "data_url": "http://127.0.0.1:8002",
156
+ "api_token": "",
157
+ }
158
+ path = _cfg_path()
159
+ if os.path.exists(path):
160
+ try:
161
+ with open(path) as f:
162
+ saved = json.load(f)
163
+ cfg["cloud_url"] = saved.get("cloud_url", cfg["cloud_url"])
164
+ cfg["data_url"] = saved.get("data_url", cfg["data_url"])
165
+ cfg["api_token"] = saved.get("api_token", cfg["api_token"])
166
+ except Exception:
167
+ pass
168
+ # Env-var overrides (highest priority)
169
+ if os.environ.get("ARTHERA_CLOUD_URL"):
170
+ cfg["cloud_url"] = os.environ["ARTHERA_CLOUD_URL"].rstrip("/")
171
+ if os.environ.get("ARTHERA_DATA_URL"):
172
+ cfg["data_url"] = os.environ["ARTHERA_DATA_URL"].rstrip("/")
173
+ if os.environ.get("ARTHERA_API_TOKEN"):
174
+ cfg["api_token"] = os.environ["ARTHERA_API_TOKEN"]
175
+ return cfg
176
+
177
+
178
+ def save_cloud_config(cloud_url: str = "", data_url: str = "",
179
+ api_token: str = "") -> None:
180
+ """Persist cloud configuration to ~/.arthera/config.json."""
181
+ import pathlib
182
+ p = pathlib.Path(_cfg_path())
183
+ p.parent.mkdir(parents=True, exist_ok=True)
184
+ existing: dict = {}
185
+ if p.exists():
186
+ try:
187
+ existing = json.loads(p.read_text())
188
+ except Exception:
189
+ pass
190
+ if cloud_url:
191
+ existing["cloud_url"] = cloud_url.rstrip("/")
192
+ if data_url:
193
+ existing["data_url"] = data_url.rstrip("/")
194
+ if api_token:
195
+ existing["api_token"] = api_token
196
+ p.write_text(json.dumps(existing, indent=2, ensure_ascii=False))
197
+
198
+
199
+ # ---------------------------------------------------------------------------
200
+ # Circuit breaker (lightweight, no external deps)
201
+ # ---------------------------------------------------------------------------
202
+
203
+ @dataclass
204
+ class _CircuitBreaker:
205
+ """Simple state-machine circuit breaker."""
206
+ failure_threshold: int = 4
207
+ recovery_timeout: float = 120.0 # seconds before trying again
208
+ _failures: int = field(default=0, repr=False)
209
+ _last_failure: float = field(default=0.0, repr=False)
210
+ _open: bool = field(default=False, repr=False)
211
+
212
+ def allow(self) -> bool:
213
+ if not self._open:
214
+ return True
215
+ if time.monotonic() - self._last_failure > self.recovery_timeout:
216
+ self._open = False # enter half-open
217
+ return True
218
+ return False
219
+
220
+ def record_success(self) -> None:
221
+ self._failures = 0
222
+ self._open = False
223
+
224
+ def record_failure(self) -> None:
225
+ self._failures += 1
226
+ self._last_failure = time.monotonic()
227
+ if self._failures >= self.failure_threshold:
228
+ self._open = True
229
+ logger.debug("AliyunDataClient: circuit breaker OPEN for this endpoint")
230
+
231
+ @property
232
+ def is_open(self) -> bool:
233
+ return self._open
234
+
235
+
236
+ # ---------------------------------------------------------------------------
237
+ # Main client
238
+ # ---------------------------------------------------------------------------
239
+
240
+ class AliyunDataClient:
241
+ """
242
+ Async HTTP client for Arthera's Alibaba Cloud quant services.
243
+
244
+ Usage (inside an async context)::
245
+
246
+ client = AliyunDataClient()
247
+ result = await client.get_quote("600519") # A股
248
+ result = await client.get_quote("AAPL") # US
249
+ """
250
+
251
+ _instance: Optional["AliyunDataClient"] = None
252
+
253
+ def __init__(self):
254
+ cfg = _load_cloud_config()
255
+ self.cloud_url = cfg["cloud_url"] # cloud_api_server
256
+ self.data_url = cfg["data_url"] # akshare_data_server
257
+ self.api_token = cfg["api_token"]
258
+
259
+ self._cb_cloud = _CircuitBreaker()
260
+ self._cb_data = _CircuitBreaker()
261
+
262
+ # Cached aiohttp session — created lazily
263
+ self._session: Any = None
264
+
265
+ # ── singleton ──────────────────────────────────────────────────────────
266
+
267
+ @classmethod
268
+ def get(cls) -> "AliyunDataClient":
269
+ if cls._instance is None:
270
+ cls._instance = cls()
271
+ return cls._instance
272
+
273
+ @classmethod
274
+ def reset(cls) -> None:
275
+ """Force re-read config (useful after /cloud config)."""
276
+ cls._instance = None
277
+
278
+ # ── HTTP helpers ───────────────────────────────────────────────────────
279
+
280
+ async def _get_session(self):
281
+ try:
282
+ import aiohttp
283
+ except ImportError:
284
+ return None
285
+ if self._session is None or self._session.closed:
286
+ timeout = aiohttp.ClientTimeout(total=15, connect=5)
287
+ self._session = aiohttp.ClientSession(timeout=timeout)
288
+ return self._session
289
+
290
+ def _headers(self, auth: bool = False) -> Dict[str, str]:
291
+ h = {"Content-Type": "application/json", "Accept": "application/json"}
292
+ if auth and self.api_token:
293
+ h["Authorization"] = f"Bearer {self.api_token}"
294
+ return h
295
+
296
+ async def _get(self, base: str, path: str,
297
+ params: Optional[Dict] = None,
298
+ auth: bool = False,
299
+ cb: Optional[_CircuitBreaker] = None) -> Optional[Dict]:
300
+ if cb and not cb.allow():
301
+ logger.debug("Circuit breaker open — skipping %s%s", base, path)
302
+ return None
303
+ session = await self._get_session()
304
+ if session is None:
305
+ return None
306
+ url = f"{base}{path}"
307
+ try:
308
+ async with session.get(url, params=params, headers=self._headers(auth)) as r:
309
+ if r.status == 200:
310
+ data = await r.json(content_type=None)
311
+ if cb:
312
+ cb.record_success()
313
+ return data
314
+ else:
315
+ logger.debug("GET %s → HTTP %d", url, r.status)
316
+ if cb:
317
+ cb.record_failure()
318
+ return None
319
+ except Exception as exc:
320
+ logger.debug("GET %s failed: %s", url, exc)
321
+ if cb:
322
+ cb.record_failure()
323
+ return None
324
+
325
+ async def _post(self, base: str, path: str,
326
+ body: Dict,
327
+ auth: bool = True,
328
+ cb: Optional[_CircuitBreaker] = None) -> Optional[Dict]:
329
+ if cb and not cb.allow():
330
+ logger.debug("Circuit breaker open — skipping %s%s", base, path)
331
+ return None
332
+ session = await self._get_session()
333
+ if session is None:
334
+ return None
335
+ url = f"{base}{path}"
336
+ try:
337
+ async with session.post(url, json=body, headers=self._headers(auth)) as r:
338
+ if r.status == 200:
339
+ data = await r.json(content_type=None)
340
+ if cb:
341
+ cb.record_success()
342
+ return data
343
+ else:
344
+ text = await r.text()
345
+ logger.debug("POST %s → HTTP %d: %s", url, r.status, text[:200])
346
+ if cb:
347
+ cb.record_failure()
348
+ return None
349
+ except Exception as exc:
350
+ logger.debug("POST %s failed: %s", url, exc)
351
+ if cb:
352
+ cb.record_failure()
353
+ return None
354
+
355
+ # ── Public API ─────────────────────────────────────────────────────────
356
+
357
+ async def health_cloud(self) -> Dict[str, Any]:
358
+ """Check cloud_api_server health."""
359
+ data = await self._get(self.cloud_url, "/health", cb=self._cb_cloud)
360
+ return data or {"status": "unreachable", "cloud_url": self.cloud_url}
361
+
362
+ async def health_data(self) -> Dict[str, Any]:
363
+ """Check akshare_data_server health."""
364
+ data = await self._get(self.data_url, "/health", cb=self._cb_data)
365
+ return data or {"status": "unreachable", "data_url": self.data_url}
366
+
367
+ # ── Market data ────────────────────────────────────────────────────────
368
+
369
+ async def get_quote(self, symbol: str) -> Optional[Dict[str, Any]]:
370
+ """
371
+ Fetch real-time quote from cloud_api_server.
372
+
373
+ Returns: { symbol, name, price, change, change_percent, volume,
374
+ high, low, open, prev_close, market, timestamp }
375
+ """
376
+ return await self._get(
377
+ self.cloud_url,
378
+ f"/api/v1/market/quote/{symbol}",
379
+ cb=self._cb_cloud,
380
+ )
381
+
382
+ async def get_stock_history(self, symbol: str,
383
+ start: str = "", end: str = "",
384
+ period: str = "daily") -> Optional[Dict[str, Any]]:
385
+ """
386
+ Fetch OHLCV history from akshare_data_server.
387
+
388
+ Returns: { symbol, data: [{date, open, high, low, close, volume},...] }
389
+ """
390
+ params: Dict[str, str] = {}
391
+ if start:
392
+ params["start_date"] = start
393
+ if end:
394
+ params["end_date"] = end
395
+ if period:
396
+ params["period"] = period
397
+ # Normalise to bare 6-digit code that akshare server expects
398
+ sym = symbol.lower()
399
+ for prefix in ("sh", "sz", ".ss", ".sz"):
400
+ sym = sym.replace(prefix, "")
401
+ return await self._get(
402
+ self.data_url,
403
+ f"/stock/{sym}",
404
+ params=params or None,
405
+ cb=self._cb_data,
406
+ )
407
+
408
+ async def get_multiple_stocks(self, symbols: List[str],
409
+ start: str = "", end: str = "") -> Optional[Dict[str, Any]]:
410
+ """Batch-fetch history for multiple symbols."""
411
+ syms = ",".join(
412
+ s.lower().replace("sh", "").replace("sz", "").replace(".ss", "").replace(".sz", "")
413
+ for s in symbols
414
+ )
415
+ params: Dict[str, str] = {"symbols": syms}
416
+ if start:
417
+ params["start_date"] = start
418
+ if end:
419
+ params["end_date"] = end
420
+ return await self._get(self.data_url, "/stocks", params=params, cb=self._cb_data)
421
+
422
+ async def get_cn_indices(self) -> Optional[Dict[str, Any]]:
423
+ """Fetch 上证/深成/沪深300/创业板 index quotes."""
424
+ return await self._get(self.data_url, "/cn/indices", cb=self._cb_data)
425
+
426
+ async def get_popular_stocks(self, limit: int = 20) -> Optional[List[Dict]]:
427
+ """沪深300 热门成分股列表。"""
428
+ data = await self._get(
429
+ self.cloud_url,
430
+ "/api/v1/market/popular",
431
+ params={"limit": str(limit)},
432
+ cb=self._cb_cloud,
433
+ )
434
+ if data and "stocks" in data:
435
+ return data["stocks"]
436
+ return None
437
+
438
+ async def search_stocks(self, q: str, limit: int = 10) -> Optional[List[Dict]]:
439
+ """搜索股票(按代码或名称)。"""
440
+ data = await self._get(
441
+ self.cloud_url,
442
+ "/api/v1/market/search",
443
+ params={"q": q, "limit": str(limit)},
444
+ cb=self._cb_cloud,
445
+ )
446
+ if data:
447
+ return data.get("results", [])
448
+ return None
449
+
450
+ # ── Factor / signal analysis ───────────────────────────────────────────
451
+
452
+ async def get_factors(self, symbol: str) -> Optional[Dict[str, Any]]:
453
+ """
454
+ Call /api/v1/quant/factors/{symbol} — returns enhanced factor snapshot.
455
+ Falls back to local if cloud unavailable.
456
+ """
457
+ return await self._get(
458
+ self.cloud_url,
459
+ f"/api/v1/quant/factors/{symbol}",
460
+ auth=bool(self.api_token),
461
+ cb=self._cb_cloud,
462
+ )
463
+
464
+ async def get_ai_signal(self, symbol: str,
465
+ market: str = "CN") -> Optional[Dict[str, Any]]:
466
+ """
467
+ POST /api/v1/quant/ai/signal — DeepSeek-powered signal generation.
468
+
469
+ Returns: { symbol, action, confidence, reasoning, stop_loss, take_profit }
470
+ """
471
+ return await self._post(
472
+ self.cloud_url,
473
+ "/api/v1/quant/ai/signal",
474
+ body={"symbol": symbol, "market": market},
475
+ auth=bool(self.api_token),
476
+ cb=self._cb_cloud,
477
+ )
478
+
479
+ async def get_predictions(self, symbols: List[str],
480
+ prediction_days: int = 5,
481
+ market: str = "CN") -> Optional[Dict[str, Any]]:
482
+ """
483
+ POST /api/v1/quant/predict — ML model predictions.
484
+
485
+ Returns: { predictions: [{symbol, predicted_return, confidence, factors},...] }
486
+ """
487
+ return await self._post(
488
+ self.cloud_url,
489
+ "/api/v1/quant/predict",
490
+ body={
491
+ "symbols": symbols,
492
+ "prediction_days": prediction_days,
493
+ "market": market,
494
+ },
495
+ auth=bool(self.api_token),
496
+ cb=self._cb_cloud,
497
+ )
498
+
499
+ async def run_backtest(self, symbols: List[str],
500
+ strategy_config: Dict[str, Any],
501
+ start_date: str = "",
502
+ end_date: str = "",
503
+ market: str = "CN") -> Optional[Dict[str, Any]]:
504
+ """
505
+ POST /api/v1/quant/backtest — run full ML-powered backtest.
506
+
507
+ Returns: { backtest_id, status, result: { performance, equity_curve, trades } }
508
+ """
509
+ body: Dict[str, Any] = {
510
+ "symbols": symbols,
511
+ "strategy_config": strategy_config,
512
+ "market": market,
513
+ }
514
+ if start_date:
515
+ body["start_date"] = start_date
516
+ if end_date:
517
+ body["end_date"] = end_date
518
+ return await self._post(
519
+ self.cloud_url,
520
+ "/api/v1/quant/backtest",
521
+ body=body,
522
+ auth=bool(self.api_token),
523
+ cb=self._cb_cloud,
524
+ )
525
+
526
+ # ── AI analysis ────────────────────────────────────────────────────────
527
+
528
+ async def get_market_insights(self, symbols: List[str],
529
+ market: str = "CN") -> Optional[Dict[str, Any]]:
530
+ """
531
+ POST /api/v1/ai/market-insights — AI narrative market analysis.
532
+
533
+ Returns: { insights, sentiment, key_risks, opportunities }
534
+ """
535
+ return await self._post(
536
+ self.cloud_url,
537
+ "/api/v1/ai/market-insights",
538
+ body={"symbols": symbols, "market": market},
539
+ auth=bool(self.api_token),
540
+ cb=self._cb_cloud,
541
+ )
542
+
543
+ async def get_portfolio_analysis(self, portfolio: List[Dict[str, Any]],
544
+ market: str = "CN") -> Optional[Dict[str, Any]]:
545
+ """
546
+ POST /api/v1/ai/portfolio-analysis.
547
+
548
+ portfolio: [{ symbol, weight }]
549
+ Returns: { risk_metrics, diversification_score, recommendations }
550
+ """
551
+ return await self._post(
552
+ self.cloud_url,
553
+ "/api/v1/ai/portfolio-analysis",
554
+ body={"portfolio": portfolio, "market": market},
555
+ auth=bool(self.api_token),
556
+ cb=self._cb_cloud,
557
+ )
558
+
559
+ async def get_investment_decision(self, symbol: str,
560
+ context: str = "",
561
+ market: str = "CN") -> Optional[Dict[str, Any]]:
562
+ """
563
+ POST /api/v1/ai/investment-decision — full AI investment analysis.
564
+ """
565
+ return await self._post(
566
+ self.cloud_url,
567
+ "/api/v1/ai/investment-decision",
568
+ body={"symbol": symbol, "context": context, "market": market},
569
+ auth=bool(self.api_token),
570
+ cb=self._cb_cloud,
571
+ )
572
+
573
+ # ── Utility ────────────────────────────────────────────────────────────
574
+
575
+ async def close(self) -> None:
576
+ """Close the underlying aiohttp session."""
577
+ if self._session and not self._session.closed:
578
+ await self._session.close()
579
+ self._session = None
580
+
581
+ def status(self) -> Dict[str, Any]:
582
+ """Return current circuit breaker status for /cloud status."""
583
+ payload = {
584
+ "cloud_url": self.cloud_url,
585
+ "data_url": self.data_url,
586
+ "has_token": bool(self.api_token),
587
+ "cloud_cb": "open" if self._cb_cloud.is_open else "closed",
588
+ "data_cb": "open" if self._cb_data.is_open else "closed",
589
+ }
590
+ try:
591
+ payload["health_summary"] = summarize_cloud_health(status=payload).to_dict()
592
+ except Exception:
593
+ pass
594
+ return payload
595
+
596
+
597
+ # ---------------------------------------------------------------------------
598
+ # Sync helper for use in non-async contexts (e.g. local_finance_tools.py)
599
+ # ---------------------------------------------------------------------------
600
+
601
+ def run_async(coro) -> Any:
602
+ """
603
+ Run an async coroutine from sync code (e.g. inside tool handlers).
604
+
605
+ Uses the running event loop's run_in_executor pattern so we never
606
+ accidentally create nested event loops.
607
+ """
608
+ if not hasattr(coro, "__await__"):
609
+ close = getattr(coro, "close", None)
610
+ if callable(close):
611
+ close()
612
+ return None
613
+
614
+ async def _run_and_close():
615
+ try:
616
+ return await coro
617
+ finally:
618
+ try:
619
+ await AliyunDataClient.get().close()
620
+ except Exception:
621
+ pass
622
+
623
+ try:
624
+ try:
625
+ asyncio.get_running_loop()
626
+ except RuntimeError:
627
+ return asyncio.run(_run_and_close())
628
+
629
+ # We're already inside an async context — run the coroutine on a fresh
630
+ # event loop in a worker thread to avoid nested-loop errors.
631
+ import concurrent.futures
632
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
633
+ future = pool.submit(asyncio.run, _run_and_close())
634
+ return future.result(timeout=15)
635
+ except Exception as exc:
636
+ logger.debug("run_async failed: %s", exc)
637
+ close = getattr(coro, "close", None)
638
+ if callable(close):
639
+ close()
640
+ return None
641
+
642
+
643
+ def cloud_get_quote_sync(symbol: str) -> Optional[Dict[str, Any]]:
644
+ """Blocking wrapper for AliyunDataClient.get_quote — safe to call from sync code."""
645
+ return run_async(AliyunDataClient.get().get_quote(symbol))
646
+
647
+
648
+ def cloud_get_history_sync(symbol: str, start: str = "", end: str = "") -> Optional[Dict[str, Any]]:
649
+ """Blocking wrapper for AliyunDataClient.get_stock_history."""
650
+ return run_async(AliyunDataClient.get().get_stock_history(symbol, start=start, end=end))
651
+
652
+
653
+ def cloud_get_factors_sync(symbol: str) -> Optional[Dict[str, Any]]:
654
+ """Blocking wrapper for AliyunDataClient.get_factors."""
655
+ return run_async(AliyunDataClient.get().get_factors(symbol))
656
+
657
+
658
+ def cloud_get_ai_signal_sync(symbol: str, market: str = "CN") -> Optional[Dict[str, Any]]:
659
+ """Blocking wrapper for AliyunDataClient.get_ai_signal."""
660
+ return run_async(AliyunDataClient.get().get_ai_signal(symbol, market=market))
apps/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # Aria Code Apps
2
+
3
+ `apps/` contains product entrypoints. Keep business logic in `packages/` and
4
+ make each app a thin adapter over service contracts.
5
+
6
+ - `cli/`: interactive terminal client.
7
+ - `daemon/`: local-first background gateway.
8
+ - `channels/`: external channel adapters such as relay, Feishu, Telegram, and webhooks.
9
+
10
+ Current implementation still lives mostly in root modules such as `aria_cli.py`.
11
+ New work should add service code behind `packages/` first, then migrate app
12
+ entrypoints here in small steps.
apps/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """Product entrypoints for Aria Code."""
2
+
@@ -0,0 +1,15 @@
1
+ # Channel Apps
2
+
3
+ Target role: adapters for non-terminal entrypoints.
4
+
5
+ Examples:
6
+
7
+ - relay server/client;
8
+ - Feishu bot;
9
+ - Telegram bot;
10
+ - webhooks;
11
+ - future desktop or browser UI.
12
+
13
+ Each channel should translate inbound messages into gateway requests and render
14
+ gateway responses back to the channel. It should not bypass safety, runtime, or
15
+ artifact policies.