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,425 @@
1
+ """Context builders for market-analysis commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import time
8
+ from typing import Any, Callable, Optional
9
+
10
+ from apps.cli.prompts.system_prompts import build_response_style_rule
11
+
12
+
13
+ TA_SESSION_CACHE: dict[str, dict[str, Any]] = {}
14
+ TA_SESSION_CACHE_TTL = 600
15
+
16
+
17
+ def _cached_ta(symbol: str) -> dict[str, Any]:
18
+ cached = TA_SESSION_CACHE.get(symbol)
19
+ if cached and (time.time() - float(cached.get("ts", 0))) < TA_SESSION_CACHE_TTL:
20
+ return {**(cached.get("data") or {}), "_cached": True}
21
+ return {}
22
+
23
+
24
+ def _store_ta(symbol: str, data: dict[str, Any]) -> None:
25
+ if data.get("success"):
26
+ TA_SESSION_CACHE[symbol] = {"data": data, "ts": time.time()}
27
+
28
+
29
+ def build_analyze_prompt(symbol: str, context: str, is_cn: bool, response_lang: str | None = None) -> str:
30
+ """Build the LLM prompt for /analyze from a prepared market context."""
31
+
32
+ symbol = symbol.upper()
33
+ lang = response_lang if response_lang in ("zh", "en") else ("zh" if is_cn else "en")
34
+ _no_explain = (
35
+ "注意:如某字段数据缺失,请直接省略该项,不要写'数据缺失'或'未提供'的说明文字。"
36
+ "对于无数据的章节整体跳过即可。\n"
37
+ if lang == "zh" else
38
+ "Note: If any data field is missing, skip that item entirely. Do not write 'data unavailable' or explain why it is missing.\n"
39
+ )
40
+ if lang == "zh":
41
+ return (
42
+ f"{context}\n\n"
43
+ f"{build_response_style_rule('zh')}"
44
+ f"请对以上 {symbol} 进行综合分析,包括:\n"
45
+ f"1. 技术面分析(趋势判断、支撑/阻力、指标信号)\n"
46
+ f"2. 基本面评估(估值合理性、盈利能力)\n"
47
+ f"3. 风险提示(简要 2-3 条)\n"
48
+ f"4. 综合建议(操作方向 + 关键价位参考)\n"
49
+ f"\n{_no_explain}"
50
+ )
51
+ return (
52
+ f"{context}\n\n"
53
+ f"{build_response_style_rule('en')}"
54
+ f"Please provide a comprehensive analysis of {symbol}, covering:\n"
55
+ f"1. Technical analysis (trend, support/resistance, indicator signals)\n"
56
+ f"2. Fundamentals (valuation, profitability — only if data is available)\n"
57
+ f"3. Risk assessment (2-3 key points)\n"
58
+ f"4. Summary outlook with key price levels\n"
59
+ f"\n{_no_explain}"
60
+ )
61
+
62
+
63
+ async def build_analyze_context(
64
+ symbol: str,
65
+ is_cn: bool,
66
+ *,
67
+ has_mdc: bool = False,
68
+ get_mdc: Callable[[], Any] | None = None,
69
+ ashare_name_lookup: Callable[[str], str | None] | None = None,
70
+ has_brokers: bool = False,
71
+ get_broker_registry: Callable[[], Any] | None = None,
72
+ logger: logging.Logger | None = None,
73
+ ) -> str:
74
+ """Fetch real market data and build a structured LLM context."""
75
+
76
+ log = logger or logging.getLogger(__name__)
77
+ loop = asyncio.get_event_loop()
78
+ ctx_lines: list[str] = [f"## {symbol} 市场数据" if is_cn else f"## {symbol} Market Data"]
79
+
80
+ quote: dict[str, Any] = {}
81
+ technical: dict[str, Any] = {}
82
+ quality: dict[str, Any] = {}
83
+
84
+ # ── Primary: cloud DataService bundle ────────────────────────────────────
85
+ try:
86
+ from packages.aria_services import data as service_data
87
+ try:
88
+ from datasources.router import get_router as get_data_router
89
+ router = get_data_router()
90
+ except Exception:
91
+ router = None
92
+ bundle = await loop.run_in_executor(
93
+ None,
94
+ lambda: service_data.DataService(router=router).bundle(
95
+ symbol, history_days=370, technical_days=120,
96
+ ),
97
+ )
98
+ quote = bundle.quote or {}
99
+ technical = bundle.technical or {}
100
+ quality = dict(bundle.quality or {})
101
+ if not quality.get("providers") and getattr(bundle, "provider_chain", None):
102
+ quality["providers"] = list(getattr(bundle, "provider_chain", []) or [])
103
+ if not quality.get("missing_fields") and getattr(bundle, "missing_fields", None):
104
+ quality["missing_fields"] = list(getattr(bundle, "missing_fields", []) or [])
105
+ _store_ta(symbol, technical)
106
+ except Exception as exc:
107
+ log.debug("analyze data service failed for %s: %s", symbol, exc)
108
+
109
+ # ── Fallback 1: market data client ────────────────────────────────────────
110
+ if not quote and has_mdc and get_mdc:
111
+ try:
112
+ mdc = get_mdc()
113
+ raw_q = await loop.run_in_executor(None, mdc.quote, symbol)
114
+ if raw_q:
115
+ quote = raw_q if isinstance(raw_q, dict) else (raw_q.to_dict() if hasattr(raw_q, "to_dict") else vars(raw_q))
116
+ if quote.get("price"):
117
+ raw_ta = await loop.run_in_executor(None, mdc.technical_indicators, symbol, 120)
118
+ if isinstance(raw_ta, dict) and raw_ta.get("success"):
119
+ technical = raw_ta
120
+ _store_ta(symbol, technical)
121
+ else:
122
+ technical = _cached_ta(symbol)
123
+ except Exception:
124
+ technical = _cached_ta(symbol)
125
+
126
+ # ── Fallback 2: DataRouter (yfinance direct) ─────────────────────────────
127
+ if not quote or not quote.get("price"):
128
+ try:
129
+ from datasources.router import DataRouter
130
+ router_direct = DataRouter()
131
+ raw_q = await loop.run_in_executor(None, router_direct.quote, symbol)
132
+ if raw_q:
133
+ quote = raw_q.to_dict() if hasattr(raw_q, "to_dict") else vars(raw_q)
134
+ except Exception as exc:
135
+ log.debug("DataRouter.quote fallback failed for %s: %s", symbol, exc)
136
+
137
+ # If no technical from above, try cached
138
+ if not technical:
139
+ technical = _cached_ta(symbol)
140
+
141
+ if quality:
142
+ ctx_lines.append(f"\n### {'数据质量' if is_cn else 'Data Quality'}")
143
+ status = quality.get("status")
144
+ if status:
145
+ ctx_lines.append(f"- {'状态' if is_cn else 'Status'}: {status}")
146
+ providers = quality.get("providers") or []
147
+ if providers:
148
+ ctx_lines.append(f"- {'数据源' if is_cn else 'Providers'}: {', '.join(map(str, providers))}")
149
+ missing = quality.get("missing_fields") or []
150
+ if missing:
151
+ ctx_lines.append(f"- {'缺失字段' if is_cn else 'Missing fields'}: {', '.join(map(str, missing))}")
152
+ warnings = quality.get("warnings") or []
153
+ if warnings:
154
+ ctx_lines.append(f"- {'警告' if is_cn else 'Warnings'}: {'; '.join(map(str, warnings[:3]))}")
155
+
156
+ # ── Price / header line ───────────────────────────────────────────────────
157
+ price = quote.get("price") if quote else None
158
+ change_pct = quote.get("change_pct") if quote else None
159
+ name = (quote.get("name") or symbol) if quote else symbol
160
+
161
+ if price and float(price) == 0.0:
162
+ price = None # treat 0 as missing
163
+
164
+ if is_cn and (not name or name == symbol or str(name).isascii()):
165
+ try:
166
+ cn_name = ashare_name_lookup(symbol) if ashare_name_lookup else None
167
+ if cn_name:
168
+ name = cn_name
169
+ except Exception as exc:
170
+ log.debug("ashare name lookup failed for %s: %s", symbol, exc)
171
+
172
+ if price:
173
+ chg_str = f"{float(change_pct):+.2f}%" if change_pct is not None else ""
174
+ header = f"- 价格: {float(price):.2f}" if is_cn else f"- Price: {float(price):.2f}"
175
+ if chg_str:
176
+ header += f" ({chg_str})"
177
+ if name and name != symbol:
178
+ header += f" [{name}]"
179
+ ctx_lines.append(header)
180
+ # 52-week range
181
+ h52 = quote.get("high_52w", 0)
182
+ l52 = quote.get("low_52w", 0)
183
+ if h52 and l52 and float(h52) > 0:
184
+ ctx_lines.append(
185
+ f"- 52周区间: {float(l52):.2f} — {float(h52):.2f}"
186
+ if is_cn else
187
+ f"- 52-week range: {float(l52):.2f} — {float(h52):.2f}"
188
+ )
189
+ else:
190
+ ctx_lines.append(
191
+ "- 价格: 获取失败(稍后重试或配置数据服务 key)"
192
+ if is_cn else
193
+ "- Price: unavailable (configure a data service key via /apikey)"
194
+ )
195
+
196
+ # ── Technical indicators ──────────────────────────────────────────────────
197
+ rsi = technical.get("rsi")
198
+ macd_hist= technical.get("macd_hist")
199
+ ma20 = technical.get("ma20")
200
+ ma60 = technical.get("ma60")
201
+ bb_upper = technical.get("bb_upper")
202
+ bb_lower = technical.get("bb_lower")
203
+
204
+ has_tech = any(v is not None for v in (rsi, macd_hist, ma20))
205
+ if has_tech:
206
+ ctx_lines.append(f"\n### {'技术指标' if is_cn else 'Technical Indicators'}")
207
+ if rsi is not None:
208
+ rsi_desc = ("超买" if rsi > 70 else "超卖" if rsi < 30 else "中性") if is_cn else (
209
+ "Overbought" if rsi > 70 else "Oversold" if rsi < 30 else "Neutral")
210
+ ctx_lines.append(f"- RSI (14): {rsi:.1f} [{rsi_desc}]")
211
+ if macd_hist is not None:
212
+ trend = ("多头 Bullish" if macd_hist > 0 else "空头 Bearish") if is_cn else (
213
+ "Bullish" if macd_hist > 0 else "Bearish")
214
+ ctx_lines.append(f"- MACD 柱状图: {macd_hist:.4f} [{trend}]" if is_cn else
215
+ f"- MACD histogram: {macd_hist:.4f} [{trend}]")
216
+ if ma20 and price:
217
+ rel = ("上方 ↑" if float(price) > ma20 else "下方 ↓") if is_cn else (
218
+ "above ↑" if float(price) > ma20 else "below ↓")
219
+ ctx_lines.append(f"- MA20: {ma20:.2f} [价格在{rel}]" if is_cn else
220
+ f"- MA20: {ma20:.2f} [Price {rel}]")
221
+ if ma60 and price:
222
+ rel = ("上方 ↑" if float(price) > ma60 else "下方 ↓") if is_cn else (
223
+ "above ↑" if float(price) > ma60 else "below ↓")
224
+ ctx_lines.append(f"- MA60: {ma60:.2f} [价格在{rel}]" if is_cn else
225
+ f"- MA60: {ma60:.2f} [Price {rel}]")
226
+ if bb_upper and bb_lower:
227
+ ctx_lines.append(f"- 布林带: {bb_lower:.2f} — {bb_upper:.2f}" if is_cn else
228
+ f"- Bollinger Bands: {bb_lower:.2f} — {bb_upper:.2f}")
229
+ if technical.get("_cached"):
230
+ ctx_lines.append(" [以上技术指标来自缓存]" if is_cn else " [Technical data from session cache]")
231
+
232
+ # ── Support / resistance (computed from history) ──────────────────────────
233
+ await _append_support_resistance(ctx_lines, symbol, is_cn, price, ma20, ma60, bb_upper, bb_lower, loop, log)
234
+
235
+ # ── Fundamentals ──────────────────────────────────────────────────────────
236
+ await _append_fundamentals(ctx_lines, symbol, is_cn, loop, log, quote)
237
+
238
+ # ── Broker position ───────────────────────────────────────────────────────
239
+ _append_broker_position(
240
+ ctx_lines, symbol, is_cn,
241
+ has_brokers=has_brokers, get_broker_registry=get_broker_registry, logger=log,
242
+ )
243
+
244
+ return "\n".join(ctx_lines)
245
+
246
+
247
+ async def _append_support_resistance(
248
+ ctx_lines: list[str],
249
+ symbol: str,
250
+ is_cn: bool,
251
+ price: Optional[float],
252
+ ma20: Optional[float],
253
+ ma60: Optional[float],
254
+ bb_upper: Optional[float],
255
+ bb_lower: Optional[float],
256
+ loop,
257
+ log: logging.Logger,
258
+ ) -> None:
259
+ """Compute support/resistance from price history and key MAs."""
260
+ levels: dict[str, list[float]] = {"support": [], "resistance": []}
261
+
262
+ try:
263
+ from datasources.router import DataRouter
264
+ hist_result = await loop.run_in_executor(None, DataRouter().history, symbol, 90)
265
+ if hist_result and hist_result.data is not None and not hist_result.data.empty:
266
+ df = hist_result.data
267
+ close_col = next((c for c in df.columns if "close" in c.lower()), None)
268
+ high_col = next((c for c in df.columns if "high" in c.lower()), None)
269
+ low_col = next((c for c in df.columns if "low" in c.lower()), None)
270
+
271
+ if close_col and len(df) >= 10:
272
+ closes = df[close_col].dropna()
273
+ highs = df[high_col].dropna() if high_col else closes
274
+ lows = df[low_col].dropna() if low_col else closes
275
+
276
+ # Rolling 10-day swing highs (local maxima)
277
+ for i in range(5, len(highs) - 5):
278
+ window = highs.iloc[i-5:i+5]
279
+ if float(highs.iloc[i]) == float(window.max()):
280
+ levels["resistance"].append(float(highs.iloc[i]))
281
+ # Rolling 10-day swing lows (local minima)
282
+ for i in range(5, len(lows) - 5):
283
+ window = lows.iloc[i-5:i+5]
284
+ if float(lows.iloc[i]) == float(window.min()):
285
+ levels["support"].append(float(lows.iloc[i]))
286
+ except Exception as exc:
287
+ log.debug("support/resistance history failed for %s: %s", symbol, exc)
288
+
289
+ # Add MA lines as dynamic support/resistance
290
+ if price:
291
+ p = float(price)
292
+ if ma20:
293
+ (levels["support"] if p > ma20 else levels["resistance"]).append(ma20)
294
+ if ma60:
295
+ (levels["support"] if p > ma60 else levels["resistance"]).append(ma60)
296
+ # Bollinger bands
297
+ if bb_lower:
298
+ levels["support"].append(bb_lower)
299
+ if bb_upper:
300
+ levels["resistance"].append(bb_upper)
301
+
302
+ if levels["support"] or levels["resistance"]:
303
+ ctx_lines.append(f"\n### {'关键价位' if is_cn else 'Key Price Levels'}")
304
+
305
+ # Pick the 3 nearest support levels below price
306
+ sup = sorted(set(round(v, 2) for v in levels["support"] if v < p), reverse=True)[:3]
307
+ # Pick the 3 nearest resistance levels above price
308
+ res = sorted(set(round(v, 2) for v in levels["resistance"] if v > p))[:3]
309
+
310
+ if sup:
311
+ sup_str = " / ".join(f"{v:.2f}" for v in sup)
312
+ ctx_lines.append(f"- 支撑位: {sup_str}" if is_cn else f"- Support: {sup_str}")
313
+ if res:
314
+ res_str = " / ".join(f"{v:.2f}" for v in res)
315
+ ctx_lines.append(f"- 阻力位: {res_str}" if is_cn else f"- Resistance: {res_str}")
316
+
317
+
318
+ async def _append_fundamentals(
319
+ ctx_lines: list[str],
320
+ symbol: str,
321
+ is_cn: bool,
322
+ loop,
323
+ log: logging.Logger,
324
+ quote: dict[str, Any],
325
+ ) -> None:
326
+ fund_lines: list[str] = []
327
+
328
+ # Try DataRouter (yfinance / alpha_vantage / edgar chain)
329
+ try:
330
+ from datasources.router import DataRouter
331
+ fund = await loop.run_in_executor(None, DataRouter().fundamentals, symbol)
332
+ if fund:
333
+ def _row(label_cn: str, label_en: str, val: Optional[float], fmt: str = ".2f") -> None:
334
+ if val is not None and val != 0.0:
335
+ formatted = f"{val:{fmt}}"
336
+ fund_lines.append(f"- {label_cn}: {formatted}" if is_cn else f"- {label_en}: {formatted}")
337
+
338
+ _row("市盈率 (TTM)", "P/E ratio (TTM)", fund.pe_ttm)
339
+ _row("市净率", "Price-to-Book", fund.pb)
340
+ _row("ROE", "Return on Equity", fund.roe, ".2f")
341
+ _row("营收增速", "Revenue Growth (YoY)", fund.revenue_growth, ".2f")
342
+ _row("净利增速", "Earnings Growth (YoY)", fund.net_profit_growth, ".2f")
343
+ _row("股息率", "Dividend Yield", fund.dividend_yield, ".2f")
344
+
345
+ # Market cap: format nicely (USD or CNY)
346
+ if fund.total_mv and fund.total_mv > 0:
347
+ mv = fund.total_mv
348
+ if mv >= 1e12:
349
+ mv_str = f"{mv/1e12:.2f}T"
350
+ elif mv >= 1e9:
351
+ mv_str = f"{mv/1e9:.2f}B"
352
+ elif mv >= 1e8:
353
+ mv_str = f"{mv/1e8:.2f}亿" if is_cn else f"{mv/1e9:.2f}B"
354
+ else:
355
+ mv_str = f"{mv:,.0f}"
356
+ fund_lines.append(f"- 总市值: {mv_str}" if is_cn else f"- Market Cap: {mv_str}")
357
+
358
+ if fund.source:
359
+ fund_lines.append(f" [数据源: {fund.source}]" if is_cn else f" [source: {fund.source}]")
360
+ except Exception as exc:
361
+ log.debug("fundamentals fetch failed for %s: %s", symbol, exc)
362
+
363
+ # Also try quote-level PE/PB if fundamentals didn't yield anything
364
+ if not fund_lines and quote:
365
+ pe = quote.get("pe_ttm", 0)
366
+ pb = quote.get("pb", 0)
367
+ if pe and float(pe) > 0:
368
+ fund_lines.append(f"- 市盈率 (TTM): {float(pe):.2f}" if is_cn else f"- P/E ratio (TTM): {float(pe):.2f}")
369
+ if pb and float(pb) > 0:
370
+ fund_lines.append(f"- 市净率: {float(pb):.2f}" if is_cn else f"- Price-to-Book: {float(pb):.2f}")
371
+
372
+ if fund_lines:
373
+ ctx_lines.append(f"\n### {'基本面' if is_cn else 'Fundamentals'}")
374
+ ctx_lines.extend(fund_lines)
375
+
376
+
377
+ def _append_broker_position(
378
+ ctx_lines: list[str],
379
+ symbol: str,
380
+ is_cn: bool,
381
+ *,
382
+ has_brokers: bool,
383
+ get_broker_registry: Callable[[], Any] | None,
384
+ logger: logging.Logger,
385
+ ) -> None:
386
+ if not has_brokers or not get_broker_registry:
387
+ return
388
+ try:
389
+ registry = get_broker_registry()
390
+ broker = registry.active()
391
+ if not broker or not broker.is_connected:
392
+ return
393
+ positions = broker.positions()
394
+ symbol_norm = symbol.lstrip("0").upper()
395
+ match = None
396
+ for position in positions:
397
+ pos_symbol = str(position.symbol or "").lstrip("0").upper()
398
+ if pos_symbol == symbol_norm or pos_symbol.startswith(symbol_norm) or symbol_norm.startswith(pos_symbol):
399
+ match = position
400
+ break
401
+ ctx_lines.append(f"\n### {'我的持仓' if is_cn else 'Your Position'}")
402
+ if not match:
403
+ ctx_lines.append(f"- 当前未持有此股 [{broker.label}]" if is_cn else f"- Not currently held [{broker.label}]")
404
+ return
405
+ qty = getattr(match, "quantity", None) or getattr(match, "qty", None)
406
+ cost = getattr(match, "cost_price", None) or getattr(match, "avg_cost", None)
407
+ pnl = getattr(match, "pnl", None)
408
+ pnl_pct = getattr(match, "pnl_pct", None)
409
+ market_value = getattr(match, "market_value", None)
410
+ ctx_lines.append(f"- 持有: 是 [{broker.label}]" if is_cn else f"- Held: Yes [{broker.label}]")
411
+ if qty is not None:
412
+ ctx_lines.append(f"- 持仓量: {qty:,}" if is_cn else f"- Quantity: {qty:,}")
413
+ if cost is not None:
414
+ ctx_lines.append(f"- 成本价: {cost:.3f}" if is_cn else f"- Avg Cost: {cost:.3f}")
415
+ if market_value is not None:
416
+ ctx_lines.append(f"- 市值: {market_value:,.2f}" if is_cn else f"- Market Value: {market_value:,.2f}")
417
+ if pnl is not None and pnl_pct is not None:
418
+ sign = "+" if pnl >= 0 else ""
419
+ ctx_lines.append(
420
+ f"- 浮动盈亏: {sign}{pnl:,.2f} ({sign}{pnl_pct:.2f}%)"
421
+ if is_cn else
422
+ f"- Unrealized P&L: {sign}{pnl:,.2f} ({sign}{pnl_pct:.2f}%)"
423
+ )
424
+ except Exception as exc:
425
+ logger.debug("broker position lookup failed for %s: %s", symbol, exc)
@@ -0,0 +1,7 @@
1
+ """Backwards-compatibility shim — canonical code lives in ui/render/market.py."""
2
+ from ui.render.market import * # noqa: F401, F403
3
+ from ui.render.market import (
4
+ print_quote_result, print_ta_result,
5
+ render_quote_plain, render_ta_plain,
6
+ compact_quote_market_cap,
7
+ )