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
ui/render/finance.py ADDED
@@ -0,0 +1,1480 @@
1
+ """
2
+ apps/cli/commands/finance_render.py — Finance tool result renderers
3
+ ===================================================================
4
+ All public functions accept ``console`` and ``has_rich`` as keyword-only
5
+ arguments following the same contract as team_render.py:
6
+
7
+ render_finance_result(tool_name, result, console=console, has_rich=HAS_RICH)
8
+
9
+ ``aria_cli.py`` keeps thin wrappers that supply its module-level globals.
10
+ No imports from aria_cli.py — dependency flows one way only.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import logging
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ try:
21
+ from rich.text import Text
22
+ HAS_RICH = True
23
+ except ImportError:
24
+ HAS_RICH = False
25
+
26
+
27
+ def _clean_error_msg(error: object) -> str:
28
+ """Convert provider/runtime errors into short user-facing messages."""
29
+ raw = str(error or "failed").strip()
30
+ low = raw.lower()
31
+ if not raw:
32
+ return "操作失败"
33
+ if "curl: (28)" in low or "timed out" in low or "timeout" in low:
34
+ return "请求超时,数据源暂时不可用。请稍后重试或运行 /health 检查服务。"
35
+ if "connection refused" in low:
36
+ return "连接被拒绝,服务暂时不可用。请检查本地服务或网络。"
37
+ if "connection aborted" in low or "remotedisconnected" in low:
38
+ return "网络连接中断,数据源未完成响应。请稍后重试。"
39
+ if "rate" in low or "429" in low or "too many requests" in low:
40
+ return "数据源请求频率受限,请稍后重试。"
41
+ if "traceback" in low:
42
+ return raw.splitlines()[-1][:160] if raw.splitlines() else "运行失败"
43
+ return raw[:240]
44
+
45
+
46
+ def render_finance_result(tool_name: str, result: dict, *, console=None, has_rich: bool = True, bot_mode: bool = False) -> None:
47
+ """
48
+ Rich-formatted display for all finance tool results.
49
+ Shows structured tables instead of raw dicts.
50
+ """
51
+ if bot_mode:
52
+ return
53
+ if not result or not isinstance(result, dict):
54
+ return
55
+ if not result.get("success"):
56
+ err = _clean_error_msg(
57
+ result.get("error") or result.get("message") or "数据暂不可用(服务离线或无数据)"
58
+ )
59
+ chain = result.get("provider_chain") or []
60
+ chain_text = f"\n[dim]已尝试: {' -> '.join(chain)}[/dim]" if chain else ""
61
+ if has_rich:
62
+ from rich.panel import Panel
63
+ from rich import box as rich_box
64
+ console.print(Panel(
65
+ f"[yellow]⚠ {err}[/yellow]{chain_text}",
66
+ border_style="yellow",
67
+ box=rich_box.ROUNDED,
68
+ padding=(0, 1),
69
+ ))
70
+ else:
71
+ print(f" ⚠ {err}")
72
+ return
73
+
74
+ provider = result.get("provider", "")
75
+ prov_tag = f" [dim][{provider}][/dim]" if provider else ""
76
+
77
+ # ── Market data / quote ────────────────────────────────────────────
78
+ if tool_name in ("get_market_data", "get_crypto_data", "get_forex_data"):
79
+ sym = result.get("symbol", "")
80
+ px = result.get("latest_close", result.get("price", 0))
81
+ chg = result.get("change_pct", result.get("change_pct_24h", 0)) or 0
82
+ vol = result.get("volume", 0)
83
+ name = result.get("name", "")
84
+ curr = result.get("currency", "")
85
+ color = "green" if chg >= 0 else "red"
86
+ arrow = "▲" if chg >= 0 else "▼"
87
+ if has_rich:
88
+ from rich.table import Table
89
+ t = Table(show_header=False, box=None, padding=(0, 1))
90
+ t.add_column(style="dim", width=20)
91
+ t.add_column()
92
+ title_str = f"[bold]{sym}[/bold]" + (f" {name}" if name else "")
93
+ t.add_row("标的", title_str)
94
+ px_disp = f"{curr} {px:,.4g}" if curr else f"{px:,.4g}"
95
+ t.add_row("最新价", f"[bold]{px_disp}[/bold]")
96
+ t.add_row("涨跌幅", f"[{color}]{arrow} {chg:+.2f}%[/{color}]")
97
+ _hi = result.get("high"); _lo = result.get("low")
98
+ if _hi and _lo:
99
+ t.add_row("日内区间", f"{_lo:,.4g} — {_hi:,.4g}")
100
+ if vol:
101
+ t.add_row("成交量", f"{int(vol):,}")
102
+ # Technical indicators from local tool
103
+ _rsi = result.get("rsi")
104
+ if _rsi is not None:
105
+ _rsi_color = "red" if _rsi >= 70 else ("cyan" if _rsi <= 30 else "white")
106
+ t.add_row("RSI(14)", f"[{_rsi_color}]{_rsi:.1f}[/{_rsi_color}]")
107
+ _mh = result.get("macd_hist")
108
+ if _mh is not None:
109
+ _mh_color = "green" if _mh > 0 else "red"
110
+ t.add_row("MACD hist", f"[{_mh_color}]{_mh:+.4f}[/{_mh_color}]")
111
+ _ma20 = result.get("ma20"); _ma60 = result.get("ma60")
112
+ if _ma20:
113
+ t.add_row("MA20", f"{_ma20:,.4g}")
114
+ if _ma60:
115
+ t.add_row("MA60", f"{_ma60:,.4g}")
116
+ # Legacy cloud fields
117
+ for k in ("high_52w", "low_52w", "bid", "ask"):
118
+ v = result.get(k)
119
+ if v is not None:
120
+ t.add_row(k.replace("_", " ").title(), f"{v:,.4g}")
121
+ console.print(t)
122
+ if prov_tag:
123
+ console.print(f" {prov_tag}")
124
+ else:
125
+ print(f" {sym}: {px} ({chg:+.2f}%)")
126
+ return
127
+
128
+ # ── Commodity data ─────────────────────────────────────────────────
129
+ if tool_name == "get_commodities_data":
130
+ sym = result.get("symbol", "")
131
+ px = result.get("latest_close", 0)
132
+ chg = result.get("change_pct", 0) or 0
133
+ unit = result.get("unit", "")
134
+ color = "green" if chg >= 0 else "red"
135
+ arrow = "▲" if chg >= 0 else "▼"
136
+ if has_rich:
137
+ console.print(
138
+ f" [bold]{sym}[/bold] {px:,.3g} {unit} "
139
+ f"[{color}]{arrow} {chg:+.3f}%[/{color}]{prov_tag}"
140
+ )
141
+ for k in ("pct_from_52w_high", "pct_from_52w_low", "year_return"):
142
+ v = result.get(k)
143
+ if v is not None:
144
+ console.print(f" [dim]{k:<25s}[/dim] {v:+.3%}")
145
+ else:
146
+ print(f" {sym}: {px} ({chg:+.3f}%)")
147
+ return
148
+
149
+ # ── AI signal ──────────────────────────────────────────────────────
150
+ if tool_name == "get_ai_signal":
151
+ action = result.get("action", "HOLD")
152
+ conf = result.get("confidence", 0)
153
+ reason = result.get("reasoning", "")
154
+ sl = result.get("stop_loss")
155
+ tp = result.get("take_profit")
156
+ color = {"BUY": "green", "SELL": "red", "HOLD": "yellow"}.get(action, "white")
157
+ if has_rich:
158
+ console.print(f" Signal: [{color}][bold]{action}[/bold][/{color}] "
159
+ f"Confidence: [bold]{conf:.1%}[/bold]{prov_tag}")
160
+ if reason:
161
+ console.print(f" [dim]{reason[:120]}[/dim]")
162
+ if sl is not None:
163
+ console.print(f" [dim]Stop-loss: {sl} Take-profit: {tp}[/dim]")
164
+ else:
165
+ print(f" {action} ({conf:.1%}) — {reason[:80]}")
166
+ return
167
+
168
+ # ── Factors ────────────────────────────────────────────────────────
169
+ if tool_name == "calculate_factors":
170
+ sym = result.get("symbol", "")
171
+ if has_rich:
172
+ from rich.table import Table
173
+ t = Table(title=f"Factors — {sym}", show_header=True, box=None, padding=(0, 1))
174
+ t.add_column("Factor", style="dim", width=24)
175
+ t.add_column("Value", justify="right")
176
+ t.add_column("Signal", width=6)
177
+ def _sig(v, neutral_lo=-0.1, neutral_hi=0.1):
178
+ if v is None: return ""
179
+ return "[green]▲[/green]" if v > neutral_hi else "[red]▼[/red]" if v < neutral_lo else "[yellow]─[/yellow]"
180
+ FACTOR_ROWS = [
181
+ ("rsi_14", "RSI(14)", lambda v: "[red]OB[/red]" if v and v > 70 else "[green]OS[/green]" if v and v < 30 else "[dim]─[/dim]"),
182
+ ("macd_hist", "MACD Hist", lambda v: "[green]▲[/green]" if v and v > 0 else "[red]▼[/red]"),
183
+ ("trend_score", "Trend Score", lambda v: _sig(v, -0.2, 0.2)),
184
+ ("bb_position", "BB Position", lambda v: "[red]OB[/red]" if v and v > 0.9 else "[green]OS[/green]" if v and v < 0.1 else "[dim]─[/dim]"),
185
+ ("ma_20_gap", "vs MA20", lambda v: "[green]▲[/green]" if v and v > 0 else "[red]▼[/red]"),
186
+ ("ma_60_gap", "vs MA60", lambda v: "[green]▲[/green]" if v and v > 0 else "[red]▼[/red]"),
187
+ ("volatility_20d", "Vol(20d)", lambda v: ""),
188
+ ("volume_ratio_20d","Vol Ratio", lambda v: "[green]⬆[/green]" if v and v > 1.5 else ""),
189
+ ("return_5d", "Return 5d", lambda v: "[green]▲[/green]" if v and v > 0 else "[red]▼[/red]"),
190
+ ("return_20d", "Return 20d", lambda v: "[green]▲[/green]" if v and v > 0 else "[red]▼[/red]"),
191
+ ]
192
+ for key, label, sig_fn in FACTOR_ROWS:
193
+ v = result.get(key)
194
+ if v is not None:
195
+ val_str = f"{v:+.4f}" if isinstance(v, float) else str(v)
196
+ t.add_row(label, val_str, sig_fn(v))
197
+ console.print(t)
198
+ console.print(f" {prov_tag}")
199
+ else:
200
+ for k, v in result.items():
201
+ if k not in ("success", "symbol", "provider") and isinstance(v, (int, float)):
202
+ print(f" {k:<25s} {v:.5g}")
203
+ return
204
+
205
+ # ── Backtest ───────────────────────────────────────────────────────
206
+ if tool_name in ("backtest_strategy", "cloud_backtest"):
207
+ sym = result.get("symbol", result.get("symbols", ""))
208
+ strat = result.get("strategy", result.get("model_type", ""))
209
+ if has_rich:
210
+ from rich.table import Table
211
+ t = Table(title=f"Backtest — {sym} [{strat}]", show_header=True, box=None)
212
+ t.add_column("Metric", style="dim", width=24)
213
+ t.add_column("Value", justify="right")
214
+ PERF_ROWS = [
215
+ ("total_return", "Total Return", lambda v: f"[{'green' if v >= 0 else 'red'}]{v:+.2%}[/]"),
216
+ ("annual_return", "Annual Return", lambda v: f"[{'green' if v >= 0 else 'red'}]{v:+.2%}[/]"),
217
+ ("sharpe_ratio", "Sharpe Ratio", lambda v: f"[{'green' if v >= 1 else 'yellow' if v >= 0.5 else 'red'}]{v:.3f}[/]"),
218
+ ("sortino_ratio", "Sortino Ratio", lambda v: f"{v:.3f}"),
219
+ ("max_drawdown", "Max Drawdown", lambda v: f"[red]{v:.2%}[/red]"),
220
+ ("win_rate", "Win Rate", lambda v: f"{v:.1%}"),
221
+ ("total_trades", "Trades", lambda v: str(int(v))),
222
+ ("benchmark_return","Benchmark (B&H)", lambda v: f"{v:+.2%}"),
223
+ ("alpha", "Alpha", lambda v: f"[{'green' if v >= 0 else 'red'}]{v:+.2%}[/]"),
224
+ ]
225
+ for key, label, fmt_fn in PERF_ROWS:
226
+ v = result.get(key)
227
+ if v is not None:
228
+ t.add_row(label, fmt_fn(v))
229
+ console.print(t)
230
+ console.print(f" {result.get('start', '')} → {result.get('end', '')} "
231
+ f"[dim]{result.get('bars', '')} bars[/dim]{prov_tag}")
232
+ else:
233
+ for k in ("total_return", "sharpe_ratio", "max_drawdown", "win_rate"):
234
+ v = result.get(k)
235
+ if v is not None:
236
+ print(f" {k:<20s} {v:.4g}")
237
+ return
238
+
239
+ # ── Predictions ────────────────────────────────────────────────────
240
+ if tool_name == "get_predictions":
241
+ preds = result.get("predictions", [])
242
+ days = result.get("prediction_days", 5)
243
+ if has_rich and preds:
244
+ from rich.table import Table
245
+ t = Table(title=f"ML Predictions ({days}d)", show_header=True, box=None)
246
+ t.add_column("Symbol", style="bold", width=12)
247
+ t.add_column("Predicted Return", justify="right")
248
+ t.add_column("Confidence", justify="right")
249
+ for p in preds:
250
+ ret = p.get("predicted_return", 0)
251
+ conf = p.get("confidence", 0)
252
+ color = "green" if ret >= 0 else "red"
253
+ t.add_row(p["symbol"], f"[{color}]{ret:+.2%}[/{color}]", f"{conf:.0%}")
254
+ console.print(t)
255
+ console.print(f" {prov_tag}")
256
+ else:
257
+ for p in preds:
258
+ print(f" {p.get('symbol')}: {p.get('predicted_return',0):+.2%}")
259
+ return
260
+
261
+ # ── Northbound flow ────────────────────────────────────────────────
262
+ if tool_name == "get_northbound_flow":
263
+ latest = result.get("latest_net_buy_yi", 0)
264
+ total = result.get("total_net_buy_yi", 0)
265
+ trend = result.get("trend", "")
266
+ color = "green" if latest >= 0 else "red"
267
+ if has_rich:
268
+ console.print(f" 北向资金 Today: [{color}][bold]{latest:+.2f}亿[/bold][/{color}] "
269
+ f"Period Total: {total:+.2f}亿 [{trend}]{prov_tag}")
270
+ else:
271
+ print(f" 北向 Today: {latest:+.2f}亿 Period: {total:+.2f}亿")
272
+ return
273
+
274
+ # ── Market indices ────────────────────────────────────────────────
275
+ if tool_name == "get_market_indices":
276
+ indices = result.get("indices", result)
277
+ if has_rich:
278
+ from rich.table import Table
279
+ t = Table(title="全球市场指数", show_header=True, box=None, padding=(0, 1))
280
+ t.add_column("指数", style="bold", width=16)
281
+ t.add_column("最新价", justify="right")
282
+ t.add_column("涨跌", justify="right")
283
+ # Handles both list-of-dicts (yfinance) and dict-of-dicts (legacy) formats
284
+ rows = (
285
+ [(d.get("name",""), d) for d in indices]
286
+ if isinstance(indices, list)
287
+ else [(k, v) for k, v in indices.items() if isinstance(v, dict)]
288
+ )
289
+ for name, d in rows:
290
+ px = d.get("price", d.get("latest_close", d.get("close", 0))) or 0
291
+ chg = d.get("change_pct", d.get("changePercent", 0)) or 0
292
+ color = "green" if chg >= 0 else "red"
293
+ t.add_row(name or d.get("ticker",""), f"{px:,.2f}",
294
+ f"[{color}]{chg:+.2f}%[/{color}]")
295
+ console.print(t)
296
+ console.print(f" [dim]{result.get('date','')} {prov_tag}[/dim]")
297
+ else:
298
+ rows = indices if isinstance(indices, list) else list(indices.values())
299
+ for d in rows[:10]:
300
+ nm = d.get("name", d.get("ticker", ""))
301
+ print(f" {nm:<16} {d.get('price',0):>10,.2f} {d.get('change_pct',0):+.2f}%")
302
+ return
303
+
304
+ # ── Risk metrics ──────────────────────────────────────────────────
305
+ if tool_name == "get_risk_metrics":
306
+ sym = result.get("symbol", "")
307
+ if has_rich:
308
+ from rich.table import Table
309
+ t = Table(title=f"[bold]{sym}[/bold] 风险指标", show_header=False, box=None, padding=(0,1))
310
+ t.add_column(style="dim", width=22)
311
+ t.add_column(justify="right")
312
+ conf = result.get("confidence_level", 0.95)
313
+ rows_r = [
314
+ ("年化波动率", f"{result.get('annual_volatility',0):.2%}"),
315
+ ("年化收益率", f"{result.get('annual_return',0):+.2%}"),
316
+ (f"VaR({conf:.0%}) 日", f"[red]{result.get('var_daily',0):.2%}[/red]"),
317
+ (f"VaR({conf:.0%}) 月", f"[red]{result.get('var_monthly',0):.2%}[/red]"),
318
+ ("CVaR 日", f"[red]{result.get('cvar_daily',0):.2%}[/red]"),
319
+ ("最大回撤", f"[red]{result.get('max_drawdown',0):.2%}[/red]"),
320
+ ("Sharpe Ratio", f"{result.get('sharpe_ratio',0):.3f}"),
321
+ ("Calmar Ratio", f"{result.get('calmar_ratio',0):.3f}"),
322
+ ("偏度", f"{result.get('skewness',0):.3f}"),
323
+ ("峰度", f"{result.get('kurtosis',0):.3f}"),
324
+ ]
325
+ for label, val in rows_r:
326
+ t.add_row(label, val)
327
+ console.print(t)
328
+ console.print(f" [dim]{prov_tag}[/dim]")
329
+ else:
330
+ for k in ("annual_volatility","var_daily","max_drawdown","sharpe_ratio"):
331
+ print(f" {k:<25} {result.get(k,0):.4g}")
332
+ return
333
+
334
+ # ── optimize_positions ────────────────────────────────────────────
335
+ if tool_name == "optimize_positions":
336
+ weights = result.get("weights", {})
337
+ method = result.get("method", "max_sharpe")
338
+ if has_rich:
339
+ from rich.table import Table
340
+ t = Table(title=f"组合优化 [{method}]", show_header=True, box=None, padding=(0,1))
341
+ t.add_column("代码", style="bold")
342
+ t.add_column("权重", justify="right")
343
+ t.add_column("占比", justify="right")
344
+ for sym_k, w in sorted(weights.items(), key=lambda x: -x[1]):
345
+ bar = "█" * int(w * 20)
346
+ t.add_row(sym_k, f"{w:.2%}", f"[cyan]{bar}[/cyan]")
347
+ console.print(t)
348
+ p_ret = result.get("portfolio_return", 0)
349
+ p_vol = result.get("portfolio_vol", 0)
350
+ sr = result.get("sharpe_ratio", 0)
351
+ console.print(
352
+ f" 期望收益 [bold]{p_ret:+.2%}[/bold] "
353
+ f"波动率 {p_vol:.2%} "
354
+ f"Sharpe [bold]{sr:.3f}[/bold] [dim]{prov_tag}[/dim]"
355
+ )
356
+ else:
357
+ for sym_k, w in weights.items():
358
+ print(f" {sym_k:<10} {w:.2%}")
359
+ return
360
+
361
+ # ── get_sector_performance ────────────────────────────────────────
362
+ if tool_name == "get_sector_performance":
363
+ top = result.get("top_sectors", [])
364
+ bottom = result.get("bottom_sectors", [])
365
+ all_s = result.get("sectors", top + bottom)
366
+ mkt = result.get("market", "")
367
+ if has_rich:
368
+ from rich.table import Table
369
+ t = Table(title=f"板块表现 [{mkt.upper()}]", show_header=True, box=None, padding=(0,1))
370
+ t.add_column("板块", style="bold", min_width=14)
371
+ t.add_column("涨跌", justify="right")
372
+ for row in sorted(all_s, key=lambda x: -(x.get("change_pct") or 0)):
373
+ chg = row.get("change_pct") or 0
374
+ color = "green" if chg >= 0 else "red"
375
+ t.add_row(row.get("sector",""), f"[{color}]{chg:+.2f}%[/{color}]")
376
+ console.print(t)
377
+ console.print(f" [dim]{result.get('date','')} {prov_tag}[/dim]")
378
+ else:
379
+ for row in sorted(all_s, key=lambda x: -(x.get("change_pct") or 0))[:10]:
380
+ print(f" {row.get('sector',''):<16} {row.get('change_pct',0):+.2f}%")
381
+ return
382
+
383
+ # ── screen_ashare ─────────────────────────────────────────────────
384
+ if tool_name == "screen_ashare":
385
+ stocks = result.get("stocks", [])
386
+ count = result.get("count", len(stocks))
387
+ if has_rich:
388
+ from rich.table import Table
389
+ t = Table(title=f"A股筛选 共 {count} 只", show_header=True, box=None, padding=(0,1))
390
+ t.add_column("代码", style="bold", width=8)
391
+ t.add_column("名称", width=10)
392
+ t.add_column("价格", justify="right")
393
+ t.add_column("涨跌%", justify="right")
394
+ t.add_column("PE", justify="right", style="dim")
395
+ t.add_column("市值(亿)",justify="right", style="dim")
396
+ for s in stocks[:30]:
397
+ chg = s.get("change_pct") or 0
398
+ color = "green" if chg >= 0 else "red"
399
+ pe = f"{s.get('pe_dynamic',0):.1f}" if s.get("pe_dynamic") else "—"
400
+ mc = f"{s.get('market_cap_yi', (s.get('market_cap') or 0)/1e8):.0f}"
401
+ t.add_row(
402
+ str(s.get("code","")), str(s.get("name",""))[:10],
403
+ f"{s.get('price',0):.2f}",
404
+ f"[{color}]{chg:+.2f}%[/{color}]",
405
+ pe, mc,
406
+ )
407
+ console.print(t)
408
+ console.print(f" [dim]{prov_tag}[/dim]")
409
+ else:
410
+ for s in stocks[:20]:
411
+ print(f" {s.get('code','')} {s.get('name','')} {s.get('change_pct',0):+.2f}%")
412
+ return
413
+
414
+ # ── get_limit_up_pool ─────────────────────────────────────────────
415
+ if tool_name == "get_limit_up_pool":
416
+ stocks = result.get("stocks", [])
417
+ count = result.get("count", len(stocks))
418
+ date_s = result.get("date", "")
419
+ if has_rich:
420
+ from rich.table import Table
421
+ t = Table(title=f"涨停板池 {date_s} 共 {count} 只",
422
+ show_header=True, box=None, padding=(0,1))
423
+ t.add_column("代码", style="bold", width=8)
424
+ t.add_column("名称", width=10)
425
+ t.add_column("连板", justify="right")
426
+ t.add_column("首封时间", style="dim")
427
+ t.add_column("类型", style="dim")
428
+ for s in stocks[:30]:
429
+ consec = s.get("consecutive") or s.get("limit_streak") or ""
430
+ t.add_row(
431
+ str(s.get("code","")), str(s.get("name",""))[:10],
432
+ str(consec), str(s.get("first_lock_time",""))[:8],
433
+ str(s.get("limit_type",""))[:6],
434
+ )
435
+ console.print(t)
436
+ console.print(f" [dim]{prov_tag}[/dim]")
437
+ else:
438
+ for s in stocks[:20]:
439
+ print(f" {s.get('code','')} {s.get('name','')} 连板:{s.get('consecutive','')}")
440
+ return
441
+
442
+ # ── get_futures_data / get_bonds_data ─────────────────────────────
443
+ if tool_name == "get_futures_data":
444
+ sym = result.get("symbol", result.get("ticker", ""))
445
+ price = result.get("price", result.get("current_price", 0)) or 0
446
+ chg = result.get("change_pct", result.get("changePercent", 0)) or 0
447
+ vol = result.get("volume", 0)
448
+ if has_rich:
449
+ color = "green" if chg >= 0 else "red"
450
+ console.print(
451
+ f" [bold]{sym}[/bold] {price:,.2f} "
452
+ f"[{color}]{chg:+.2f}%[/{color}]"
453
+ + (f" vol {vol:,.0f}" if vol else "")
454
+ + f" [dim]{prov_tag}[/dim]"
455
+ )
456
+ else:
457
+ print(f" {sym} {price} {chg:+.2f}%")
458
+ return
459
+
460
+ if tool_name == "get_bonds_data":
461
+ yields = result.get("yields", {})
462
+ spread = yields.get("10Y_2Y_spread")
463
+ curve = yields.get("curve_shape", "")
464
+ if has_rich:
465
+ from rich.table import Table
466
+ t = Table(title="美国国债收益率", show_header=False, box=None, padding=(0,1))
467
+ t.add_column(style="dim", width=6)
468
+ t.add_column(justify="right")
469
+ for tenor in ("2Y", "5Y", "10Y", "30Y"):
470
+ y = yields.get(tenor)
471
+ if y is not None:
472
+ t.add_row(tenor, f"[bold]{y:.3f}%[/bold]")
473
+ console.print(t)
474
+ if spread is not None:
475
+ color = "green" if spread >= 0 else "red"
476
+ console.print(
477
+ f" 10Y-2Y spread: [{color}]{spread:+.3f}%[/{color}] "
478
+ f"[dim]{curve} {prov_tag}[/dim]"
479
+ )
480
+ else:
481
+ for tenor, y in yields.items():
482
+ if isinstance(y, float):
483
+ print(f" {tenor}: {y:.3f}%")
484
+ return
485
+
486
+ # ── get_market_insights ───────────────────────────────────────────
487
+ if tool_name == "get_market_insights":
488
+ summaries = result.get("summaries", [])
489
+ note = result.get("note", "")
490
+ if has_rich:
491
+ from rich.table import Table
492
+ t = Table(title="市场洞察", show_header=True, box=None, padding=(0,1))
493
+ t.add_column("代码", style="bold", width=10)
494
+ t.add_column("RSI", justify="right")
495
+ t.add_column("MACD Hist", justify="right")
496
+ t.add_column("量比", justify="right")
497
+ t.add_column("趋势", justify="right")
498
+ for s in summaries:
499
+ rsi_v = s.get("rsi_14") or 0
500
+ mh = s.get("macd_hist") or 0
501
+ tr = s.get("trend_score") or 0
502
+ vr = s.get("vol_ratio") or 1.0
503
+ rsi_c = "red" if rsi_v > 70 else "green" if rsi_v < 30 else ""
504
+ mh_c = "green" if mh > 0 else "red"
505
+ t.add_row(
506
+ s.get("symbol",""),
507
+ f"[{rsi_c}]{rsi_v:.1f}[/{rsi_c}]" if rsi_c else f"{rsi_v:.1f}",
508
+ f"[{mh_c}]{mh:+.4f}[/{mh_c}]",
509
+ f"{vr:.2f}x",
510
+ f"{'↑' if tr > 0 else '↓' if tr < 0 else '→'} {tr:.2f}" if tr else "—",
511
+ )
512
+ console.print(t)
513
+ if note:
514
+ console.print(f" [dim]{note}[/dim]")
515
+ console.print(f" [dim]{prov_tag}[/dim]")
516
+ else:
517
+ for s in summaries:
518
+ print(f" {s.get('symbol','')} RSI:{s.get('rsi_14','')} MACD:{s.get('macd_hist','')}")
519
+ return
520
+
521
+ # ── calculate_factors ─────────────────────────────────────────────
522
+ if tool_name == "calculate_factors":
523
+ sym = result.get("symbol", "")
524
+ if has_rich:
525
+ from rich.table import Table
526
+ t = Table(title=f"[bold]{sym}[/bold] 因子分析", show_header=False, box=None, padding=(0,1))
527
+ t.add_column(style="dim", width=22)
528
+ t.add_column(justify="right")
529
+ _FACTOR_LABELS = {
530
+ "rsi_14": "RSI(14)",
531
+ "macd_hist": "MACD Hist",
532
+ "trend_score": "趋势评分",
533
+ "volume_ratio_20d": "量比(20d)",
534
+ "volatility_20d": "波动率(20d)",
535
+ "bb_position": "布林带位置",
536
+ "return_5d": "5日收益",
537
+ "return_20d": "20日收益",
538
+ "return_60d": "60日收益",
539
+ }
540
+ for key, label in _FACTOR_LABELS.items():
541
+ val = result.get(key)
542
+ if val is None:
543
+ continue
544
+ if key in ("return_5d","return_20d","return_60d","volatility_20d"):
545
+ color = "green" if val > 0 else "red"
546
+ t.add_row(label, f"[{color}]{val:+.2%}[/{color}]")
547
+ elif key == "rsi_14":
548
+ color = "red" if val > 70 else "green" if val < 30 else ""
549
+ t.add_row(label, f"[{color}]{val:.1f}[/{color}]" if color else f"{val:.1f}")
550
+ elif key == "macd_hist":
551
+ color = "green" if val > 0 else "red"
552
+ t.add_row(label, f"[{color}]{val:+.4f}[/{color}]")
553
+ elif key == "bb_position":
554
+ t.add_row(label, f"{val:.1%} {'超买区' if val > 0.8 else '超卖区' if val < 0.2 else '中间带'}")
555
+ else:
556
+ t.add_row(label, f"{val:.4g}")
557
+ console.print(t)
558
+ console.print(f" [dim]{prov_tag}[/dim]")
559
+ else:
560
+ for k in ("rsi_14","macd_hist","trend_score","return_20d"):
561
+ v = result.get(k)
562
+ if v is not None:
563
+ print(f" {k:<22} {v:.4g}")
564
+ return
565
+
566
+ # ── Generic fallback ──────────────────────────────────────────────
567
+ if has_rich:
568
+ # Show key=value pairs, skip large nested objects
569
+ out = Text()
570
+ for k, v in result.items():
571
+ if k in ("success", "provider", "history_tail", "equity_curve", "trades"):
572
+ continue
573
+ if isinstance(v, (int, float)):
574
+ color = "green" if v > 0 else "red" if v < 0 else ""
575
+ out.append(f" {k.replace('_',' ').title():<24s}", style="dim")
576
+ out.append(f"{v:,.5g}\n", style=color)
577
+ elif isinstance(v, str) and len(v) < 80:
578
+ out.append(f" {k.replace('_',' ').title():<24s}", style="dim")
579
+ out.append(f"{v}\n")
580
+ if str(out):
581
+ console.print(out)
582
+ if provider:
583
+ console.print(f" {prov_tag}")
584
+ else:
585
+ print(json.dumps({k: v for k, v in result.items()
586
+ if k not in ("success",) and not isinstance(v, list)},
587
+ indent=2, ensure_ascii=False, default=str)[:400])
588
+
589
+ # ── Broker query results ───────────────────────────────────────────
590
+ if tool_name in ("broker_query", "broker_order"):
591
+ query = result.get("query", "")
592
+ broker = result.get("broker", "券商")
593
+
594
+ if query == "account":
595
+ currency = result.get("currency", "CNY")
596
+ total = result.get("total_assets", 0)
597
+ cash = result.get("cash", 0)
598
+ mv = result.get("market_value", 0)
599
+ pnl_day = result.get("pnl_today", 0)
600
+ pnl_tot = result.get("pnl_total", 0)
601
+ if has_rich:
602
+ pday_c = "green" if pnl_day >= 0 else "red"
603
+ ptot_c = "green" if pnl_tot >= 0 else "red"
604
+ from rich.table import Table
605
+ t = Table(show_header=False, box=None, padding=(0, 1))
606
+ t.add_column(style="dim", width=16)
607
+ t.add_column()
608
+ t.add_row("账户", f"[bold]{broker}[/bold] [dim]{result.get('account_id','****')}[/dim]")
609
+ t.add_row("总资产", f"[bold]{currency} {total:,.2f}[/bold]")
610
+ t.add_row("持仓市值", f"{mv:,.2f}")
611
+ t.add_row("可用现金", f"{cash:,.2f}")
612
+ if pnl_day:
613
+ t.add_row("当日盈亏", f"[{pday_c}]{pnl_day:+,.2f}[/{pday_c}]")
614
+ if pnl_tot:
615
+ t.add_row("累计盈亏", f"[{ptot_c}]{pnl_tot:+,.2f}[/{ptot_c}]")
616
+ console.print(t)
617
+ else:
618
+ print(f"{broker}: 总资产 {total:,.2f} 可用 {cash:,.2f}")
619
+ return
620
+
621
+ if query == "positions":
622
+ positions = result.get("positions", [])
623
+ if not positions:
624
+ if has_rich:
625
+ console.print(f"[dim]{broker} — 当前无持仓[/dim]")
626
+ return
627
+ if has_rich:
628
+ from rich.table import Table
629
+ t = Table(title=f"[bold]{broker}[/bold] 持仓", show_header=True, header_style="bold")
630
+ t.add_column("代码", style="bold", no_wrap=True)
631
+ t.add_column("名称", max_width=10)
632
+ t.add_column("持仓", justify="right")
633
+ t.add_column("成本", justify="right", style="dim")
634
+ t.add_column("现价", justify="right")
635
+ t.add_column("市值", justify="right")
636
+ t.add_column("盈亏", justify="right")
637
+ t.add_column("盈亏%", justify="right")
638
+ for p in sorted(positions, key=lambda x: -abs(x.get("market_value", 0))):
639
+ pnl = p.get("pnl", 0)
640
+ pct = p.get("pnl_pct", 0)
641
+ c = "green" if pnl >= 0 else "red"
642
+ t.add_row(
643
+ p.get("symbol",""), p.get("name","—")[:10],
644
+ f"{p.get('quantity',0):,.0f}",
645
+ f"{p.get('cost',0):.3f}", f"{p.get('price',0):.3f}",
646
+ f"{p.get('market_value',0):,.2f}",
647
+ f"[{c}]{pnl:+,.2f}[/{c}]",
648
+ f"[{c}]{pct:+.2f}%[/{c}]",
649
+ )
650
+ console.print(t)
651
+ total_mv = sum(p.get("market_value",0) for p in positions)
652
+ total_pnl = sum(p.get("pnl",0) for p in positions)
653
+ tc = "green" if total_pnl >= 0 else "red"
654
+ console.print(f" [dim]{len(positions)} 只 市值 {total_mv:,.2f} 总盈亏 [{tc}]{total_pnl:+,.2f}[/{tc}][/dim]")
655
+ else:
656
+ for p in positions:
657
+ print(f" {p.get('symbol',''):<8} {p.get('name',''):<10} 持仓:{p.get('quantity',0):.0f} 盈亏:{p.get('pnl',0):+,.2f}")
658
+ return
659
+
660
+ if query == "orders":
661
+ orders = result.get("orders", [])
662
+ if not orders:
663
+ if has_rich:
664
+ console.print(f"[dim]{broker} — 无订单记录[/dim]")
665
+ return
666
+ if has_rich:
667
+ from rich.table import Table
668
+ t = Table(title=f"[bold]{broker}[/bold] 订单", show_header=True, header_style="bold")
669
+ t.add_column("代码", style="bold")
670
+ t.add_column("方向", justify="center")
671
+ t.add_column("委托量", justify="right")
672
+ t.add_column("成交量", justify="right")
673
+ t.add_column("委托价", justify="right", style="dim")
674
+ t.add_column("均价", justify="right")
675
+ t.add_column("状态")
676
+ t.add_column("时间", style="dim", max_width=14)
677
+ _ss = {"filled":"[green]成交[/green]","partial":"[yellow]部成[/yellow]",
678
+ "open":"[cyan]委托中[/cyan]","cancelled":"[dim]已撤[/dim]"}
679
+ _sd = {"buy":"[green]买入[/green]","sell":"[red]卖出[/red]"}
680
+ for o in orders:
681
+ t.add_row(
682
+ o.get("symbol",""),
683
+ _sd.get(o.get("side",""), o.get("side","")),
684
+ f"{o.get('quantity',0):,.0f}", f"{o.get('filled',0):,.0f}",
685
+ f"{o.get('price',0):.3f}",
686
+ f"{o.get('avg_price',0):.3f}" if o.get("avg_price") else "—",
687
+ _ss.get(o.get("status",""), o.get("status","")),
688
+ str(o.get("time",""))[:14],
689
+ )
690
+ console.print(t)
691
+ else:
692
+ for o in orders:
693
+ print(f" {o.get('symbol','')} {o.get('side','')} {o.get('quantity',0):.0f} @ {o.get('price',0):.3f} [{o.get('status','')}]")
694
+ return
695
+
696
+ # ── broker_order: confirmation required ────────────────────────
697
+ if tool_name == "broker_order" and result.get("confirmation_required"):
698
+ preview = result.get("order_preview", {})
699
+ if has_rich:
700
+ from rich.panel import Panel
701
+ from rich import box as _rbox
702
+ _side_cn = preview.get("side_cn", preview.get("side", ""))
703
+ _side_color = "green" if preview.get("side") == "buy" else "red"
704
+ _preview_id = preview.get("preview_id") or result.get("preview_id") or ""
705
+ _mode = preview.get("mode") or (result.get("trade_preview") or {}).get("mode") or ""
706
+ _broker = preview.get("broker") or (result.get("trade_preview") or {}).get("broker_label") or ""
707
+ _blockers = (result.get("trade_preview") or {}).get("execution_blockers") or []
708
+ _body = (
709
+ f"preview_id: [bold]{_preview_id}[/bold]\n"
710
+ f"模式: [bold]{_mode or '—'}[/bold] 券商: [bold]{_broker or '—'}[/bold]\n\n"
711
+ f"[bold]{_side_cn}[/bold] "
712
+ f"[bold]{preview.get('symbol','')}[/bold] "
713
+ f"数量: [bold]{preview.get('qty', 0):,}[/bold] "
714
+ f"价格: [bold]{preview.get('price_display','市价')}[/bold]\n\n"
715
+ "[yellow]确认执行时必须携带 preview_id · 其他任何回复取消[/yellow]"
716
+ )
717
+ if _blockers:
718
+ _body += "\n\n[red]执行限制:[/red]\n" + "\n".join(f" - {b}" for b in _blockers)
719
+ console.print(Panel(
720
+ _body,
721
+ title=f"[yellow]⚠ 订单确认[/yellow]",
722
+ border_style="yellow",
723
+ box=_rbox.ROUNDED,
724
+ padding=(0, 1),
725
+ ))
726
+ else:
727
+ msg = result.get("message", "请确认订单")
728
+ print(f"\n⚠ 订单确认\n{msg}\n")
729
+ return
730
+
731
+ # ── broker_order: placed successfully ──────────────────────────
732
+ if tool_name == "broker_order" and result.get("success"):
733
+ if has_rich:
734
+ _side_cn = "买入" if result.get("side") == "buy" else "卖出"
735
+ console.print(
736
+ f"[green]✓ 订单已提交[/green] {result.get('broker','')} — "
737
+ f"{_side_cn} [bold]{result.get('symbol','')}[/bold] "
738
+ f"× {result.get('qty',0):,} "
739
+ f"[dim]#{result.get('order_id','—')} {result.get('status','')}[/dim]"
740
+ )
741
+ else:
742
+ print(f"✓ 订单已提交: {result}")
743
+ return
744
+
745
+
746
+ def render_macro_result(r: dict, title: str, *, console=None, has_rich: bool = True) -> None:
747
+ """Render US or CN macro result dict."""
748
+ if not r.get("success"):
749
+ if has_rich: console.print(f" [red]{r.get('error','数据获取失败')}[/red]")
750
+ else: print(f" {r.get('error','failed')}")
751
+ return
752
+ data = r.get("data", {})
753
+ if has_rich:
754
+ from rich.table import Table
755
+ from rich.rule import Rule
756
+ console.print(Rule(f"[bold]{title}[/bold]", style="dim"))
757
+ t = Table(show_header=True, box=None, padding=(0, 1))
758
+ t.add_column("指标", style="dim", min_width=20)
759
+ t.add_column("最新值", justify="right", min_width=10)
760
+ t.add_column("环比变化", justify="right")
761
+ t.add_column("时间", style="dim")
762
+ for key, item in data.items():
763
+ if key.startswith("_"): continue
764
+ if not isinstance(item, dict): continue
765
+ latest = item.get("latest", {}) or {}
766
+ val = latest.get("value")
767
+ date = latest.get("date", "")
768
+ change = item.get("change")
769
+ unit = item.get("unit", "")
770
+ label = item.get("label", key)
771
+ if val is None: continue
772
+ val_str = f"{val:.2f}{unit}"
773
+ if change is not None:
774
+ color = "green" if change > 0 else "red" if change < 0 else ""
775
+ chg_str = f"[{color}]{change:+.3f}[/{color}]" if color else f"{change:+.3f}"
776
+ else:
777
+ chg_str = "—"
778
+ t.add_row(label, val_str, chg_str, str(date)[:10])
779
+ console.print(t)
780
+ yc = data.get("_yield_curve", {})
781
+ if yc:
782
+ sp = yc.get("spread_10y_2y", 0)
783
+ shape = yc.get("shape", "")
784
+ color = "green" if sp > 0 else "red"
785
+ console.print(f" 收益率曲线: [{color}]{shape}[/{color}] 10Y-2Y利差: [{color}]{sp:+.3f}%[/{color}]")
786
+ else:
787
+ print(f"\n{title}")
788
+ for key, item in data.items():
789
+ if not isinstance(item, dict) or key.startswith("_"): continue
790
+ v = (item.get("latest") or {}).get("value")
791
+ if v is not None:
792
+ print(f" {item.get('label',key):<28} {v:.3g}")
793
+
794
+
795
+ def render_cb_rates(r: dict, *, console=None, has_rich: bool = True) -> None:
796
+ """Render central bank rates."""
797
+ if not r.get("success"):
798
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
799
+ return
800
+ rates = r.get("rates", {})
801
+ if has_rich:
802
+ from rich.rule import Rule
803
+ from rich.table import Table
804
+ console.print(Rule("[bold]🏦 主要央行政策利率[/bold]", style="dim"))
805
+ t = Table(show_header=False, box=None, padding=(0,1))
806
+ t.add_column(style="dim", min_width=28)
807
+ t.add_column(justify="right")
808
+ for name, val in rates.items():
809
+ if val is not None:
810
+ t.add_row(name, f"[bold]{val:.2f}%[/bold]")
811
+ console.print(t)
812
+ else:
813
+ print("\n央行利率")
814
+ for name, val in rates.items():
815
+ if val is not None:
816
+ print(f" {name:<30} {val:.2f}%")
817
+
818
+
819
+ def render_econ_calendar(r: dict, *, console=None, has_rich: bool = True) -> None:
820
+ """Render economic calendar."""
821
+ events = r.get("events", [])
822
+ if has_rich:
823
+ from rich.rule import Rule
824
+ from rich.table import Table
825
+ console.print(Rule("[bold]📅 经济事件日历[/bold]", style="dim"))
826
+ t = Table(show_header=True, box=None, padding=(0,1))
827
+ t.add_column("时间", style="dim", width=12)
828
+ t.add_column("事件", min_width=30)
829
+ t.add_column("重要性", justify="center")
830
+ for ev in events[:15]:
831
+ imp = ev.get("importance", ev.get("importance_level",""))
832
+ imp_colored = (
833
+ "[red]HIGH[/red]" if str(imp).upper() in ("HIGH","3","★★★")
834
+ else "[yellow]MED[/yellow]" if str(imp).upper() in ("MEDIUM","2","★★")
835
+ else f"[dim]{imp}[/dim]"
836
+ )
837
+ console.print
838
+ t.add_row(
839
+ str(ev.get("time","") or ev.get("date",""))[:12],
840
+ str(ev.get("event","") or ev.get("title",""))[:45],
841
+ imp_colored,
842
+ )
843
+ console.print(t)
844
+ else:
845
+ for ev in events[:10]:
846
+ print(f" {ev.get('event','')} [{ev.get('importance','')}]")
847
+
848
+
849
+ def render_options_chain(r: dict, *, console=None, has_rich: bool = True) -> None:
850
+ """Render options chain."""
851
+ if not r.get("success"):
852
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
853
+ return
854
+ symbol = r.get("symbol","")
855
+ price = r.get("price", 0)
856
+ expiry = r.get("expiry","")
857
+ all_exp = r.get("all_expiries", [])
858
+ if has_rich:
859
+ from rich.table import Table
860
+ from rich.rule import Rule
861
+ console.print(Rule(f"[bold]{symbol}[/bold] 期权链 到期: [cyan]{expiry}[/cyan] 现价: [bold]{price:.2f}[/bold]", style="dim"))
862
+ if all_exp:
863
+ console.print(f" [dim]可用到期日: {', '.join(all_exp)}[/dim]")
864
+ for side in ("calls", "puts"):
865
+ rows = r.get(side, [])
866
+ if not rows: continue
867
+ label = "认购期权 (Calls)" if side == "calls" else "认沽期权 (Puts)"
868
+ t = Table(title=f"[bold]{label}[/bold]", show_header=True, box=None, padding=(0,1))
869
+ t.add_column("行权价", justify="right", style="bold")
870
+ t.add_column("最新价", justify="right")
871
+ t.add_column("买/卖", justify="right", style="dim")
872
+ t.add_column("IV%", justify="right")
873
+ t.add_column("OI", justify="right")
874
+ t.add_column("价内?", justify="center")
875
+ for row in rows:
876
+ itm = row.get("inTheMoney", False)
877
+ itm_s = "[green]✓[/green]" if itm else "[dim]—[/dim]"
878
+ bid = row.get("bid", 0) or 0
879
+ ask = row.get("ask", 0) or 0
880
+ t.add_row(
881
+ f"{row.get('strike',0):.2f}",
882
+ f"{row.get('lastPrice',0):.2f}",
883
+ f"{bid:.2f}/{ask:.2f}",
884
+ f"{row.get('iv_pct',0):.1f}%",
885
+ f"{int(row.get('openInterest',0)):,}",
886
+ itm_s,
887
+ )
888
+ console.print(t)
889
+ else:
890
+ for side in ("calls","puts"):
891
+ print(f"\n{side.upper()}")
892
+ for row in r.get(side, []):
893
+ print(f" K={row.get('strike')} last={row.get('lastPrice')} IV={row.get('iv_pct')}%")
894
+
895
+
896
+ def render_quality_scores(symbol: str, f_r: dict, z_r: dict, *, console=None, has_rich: bool = True) -> None:
897
+ """Render Piotroski F-Score + Altman Z-Score side by side."""
898
+ if has_rich:
899
+ from rich.table import Table
900
+ from rich.rule import Rule
901
+ from rich.columns import Columns
902
+ from rich.panel import Panel
903
+ from rich import box as _rbox
904
+
905
+ console.print(Rule(f"[bold]{symbol}[/bold] 财务质量双维评估", style="dim"))
906
+
907
+ # F-Score panel
908
+ if f_r.get("success"):
909
+ fs = f_r["f_score"]
910
+ sig = f_r.get("signal","")
911
+ color = "green" if sig == "bullish" else "red" if sig == "bearish" else "yellow"
912
+ bars = "█" * fs + "░" * (9 - fs)
913
+ f_body = (
914
+ f"[bold {color}]{fs}/9[/bold {color}] [{color}]{f_r.get('verdict','')}[/{color}]\n"
915
+ f"[{color}]{bars}[/{color}]\n\n"
916
+ )
917
+ scores = f_r.get("scores", {})
918
+ categories = [
919
+ ("盈利能力", ["F1_ROA_positive","F2_CFO_positive","F3_ROA_increasing","F4_CFO_gt_ROA"]),
920
+ ("杠杆/流动性", ["F5_Leverage_lower","F6_CurrentRatio_up","F7_NoDilution"]),
921
+ ("运营效率", ["F8_GrossMargin_up","F9_AssetTurnover_up"]),
922
+ ]
923
+ for cat, keys in categories:
924
+ f_body += f"[dim]{cat}[/dim]\n"
925
+ for k in keys:
926
+ v = scores.get(k, 0)
927
+ check = "[green]✓[/green]" if v else "[dim]✗[/dim]"
928
+ f_body += f" {check} {k[3:].replace('_',' ')}\n"
929
+ f_panel = Panel(f_body.strip(), title="[bold]Piotroski F-Score[/bold]",
930
+ border_style=color, box=_rbox.ROUNDED, padding=(0,1))
931
+ else:
932
+ f_panel = Panel(f"[red]{f_r.get('error','失败')}[/red]",
933
+ title="Piotroski F-Score", border_style="red")
934
+
935
+ # Z-Score panel
936
+ if z_r.get("success"):
937
+ zs = z_r["z_score"]
938
+ risk = z_r.get("risk","medium")
939
+ zone = z_r.get("zone","")
940
+ zcolor = "green" if risk == "low" else "red" if risk == "high" else "yellow"
941
+ z_body = (
942
+ f"[bold {zcolor}]Z'' = {zs:.3f}[/bold {zcolor}]\n"
943
+ f"[{zcolor}]{zone}[/{zcolor}]\n\n"
944
+ f"[dim]安全区 >2.6 | 灰色区 1.1-2.6 | 风险区 <1.1[/dim]\n\n"
945
+ )
946
+ comp = z_r.get("components", {})
947
+ labels = {
948
+ "X1_working_capital_ratio": "X1 营运资本/总资产",
949
+ "X2_retained_earnings_ratio": "X2 留存收益/总资产",
950
+ "X3_ebit_ratio": "X3 EBIT/总资产",
951
+ "X4_equity_to_debt": "X4 权益/负债",
952
+ }
953
+ weights = {"X1": 6.56, "X2": 3.26, "X3": 6.72, "X4": 1.05}
954
+ for k, label in labels.items():
955
+ v = comp.get(k, 0)
956
+ prefix = k[:2]
957
+ contrib = round(v * weights.get(prefix, 1), 3)
958
+ c = "green" if v >= 0 else "red"
959
+ z_body += f" [dim]{label}[/dim]: [{c}]{v:.4f}[/{c}] (贡献 {contrib:+.3f})\n"
960
+ z_body += f"\n[dim]{z_r.get('formula','')}[/dim]"
961
+ z_panel = Panel(z_body.strip(), title="[bold]Altman Z''-Score[/bold]",
962
+ border_style=zcolor, box=_rbox.ROUNDED, padding=(0,1))
963
+ else:
964
+ z_panel = Panel(f"[red]{z_r.get('error','失败')}[/red]",
965
+ title="Altman Z-Score", border_style="red")
966
+
967
+ console.print(Columns([f_panel, z_panel]))
968
+ else:
969
+ if f_r.get("success"):
970
+ print(f"\nPiotroski F-Score: {f_r['f_score']}/9 {f_r.get('verdict','')}")
971
+ if z_r.get("success"):
972
+ print(f"Altman Z''-Score: {z_r['z_score']} {z_r.get('zone','')}")
973
+
974
+
975
+ def render_ichimoku(r: dict, *, console=None, has_rich: bool = True) -> None:
976
+ """Render Ichimoku Cloud analysis."""
977
+ if not r.get("success"):
978
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
979
+ return
980
+ if has_rich:
981
+ from rich.table import Table
982
+ from rich.rule import Rule
983
+ sym = r.get("symbol","")
984
+ sig = r.get("signal","")
985
+ price = r.get("price", 0)
986
+ is_bull = r.get("above_cloud", False)
987
+ sig_color = "green" if is_bull else "red" if r.get("below_cloud") else "yellow"
988
+ console.print(Rule(f"[bold]{sym}[/bold] 一目均衡表 [{sig_color}]{sig}[/{sig_color}]", style="dim"))
989
+ t = Table(show_header=False, box=None, padding=(0,1))
990
+ t.add_column(style="dim", width=18)
991
+ t.add_column(justify="right")
992
+ t.add_column(style="dim")
993
+ t.add_row("现价", f"[bold]{price:.3f}[/bold]", "")
994
+ t.add_row("转换线 (9)", f"{r.get('tenkan',0):.3f}",
995
+ "[green]↑ 多头[/green]" if r.get('tenkan',0) > r.get('kijun',0) else "[red]↓ 空头[/red]")
996
+ t.add_row("基准线 (26)",f"{r.get('kijun',0):.3f}", "")
997
+ if r.get("senkou_a"):
998
+ t.add_row("先行带A", f"{r.get('senkou_a',0):.3f}", "")
999
+ if r.get("senkou_b"):
1000
+ t.add_row("先行带B", f"{r.get('senkou_b',0):.3f}", "")
1001
+ t.add_row("云层厚度", f"{r.get('cloud_thickness',0):.3f}", r.get("cloud_color",""))
1002
+ t.add_row("TK交叉", r.get("tk_cross",""), "")
1003
+ console.print(t)
1004
+ console.print(f" [bold {sig_color}]结论: {sig}[/bold {sig_color}] [dim]先行带偏移 26期,迟行线(Chikou)={r.get('chikou',0):.3f}[/dim]")
1005
+ else:
1006
+ print(f"\n{r.get('symbol','')} 一目均衡表")
1007
+ for k in ("tenkan","kijun","senkou_a","senkou_b","signal"):
1008
+ print(f" {k}: {r.get(k,'')}")
1009
+
1010
+
1011
+ def render_fear_greed(r: dict, *, console=None, has_rich: bool = True) -> None:
1012
+ """Render Fear & Greed Index with ASCII gauge."""
1013
+ if not r.get("success"):
1014
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
1015
+ return
1016
+ val = r.get("value", 50)
1017
+ label = r.get("label", "")
1018
+ sig = r.get("signal", "中性")
1019
+ if has_rich:
1020
+ from rich.rule import Rule
1021
+ color = "green" if val <= 25 else "red" if val >= 75 else "yellow"
1022
+ # ASCII gauge
1023
+ filled = int(val / 5)
1024
+ bar = "█" * filled + "░" * (20 - filled)
1025
+ console.print(Rule("[bold]₿ 加密恐惧贪婪指数[/bold]", style="dim"))
1026
+ console.print(f"\n [{color}]{bar}[/{color}] [bold {color}]{val}/100[/bold {color}] [{color}]{label}[/{color}]\n")
1027
+ console.print(f" 操作信号: [bold]{sig}[/bold] [dim]>75 极度贪婪(卖出信号) <25 极度恐惧(买入信号)[/dim]\n")
1028
+ hist = r.get("history", [])
1029
+ if len(hist) > 1:
1030
+ hist_str = " [dim]近7天: "
1031
+ for h in hist[:7]:
1032
+ v = h.get("value", 0)
1033
+ c = "green" if v <= 25 else "red" if v >= 75 else "yellow"
1034
+ hist_str += f"[{c}]{v}[/{c}] "
1035
+ console.print(hist_str + "[/dim]")
1036
+ else:
1037
+ print(f"\n恐惧贪婪指数: {val}/100 ({label}) 信号: {sig}")
1038
+
1039
+
1040
+ def render_funding_rates(r: dict, *, console=None, has_rich: bool = True) -> None:
1041
+ """Render perpetual funding rates."""
1042
+ if not r.get("success"):
1043
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
1044
+ return
1045
+ rates = r.get("rates", [])
1046
+ if has_rich:
1047
+ from rich.table import Table
1048
+ from rich.rule import Rule
1049
+ exchange = r.get("exchange","")
1050
+ bias = r.get("market_bias","")
1051
+ console.print(Rule(f"[bold]{exchange.upper()}[/bold] 永续合约资金费率", style="dim"))
1052
+ t = Table(show_header=True, box=None, padding=(0,1))
1053
+ t.add_column("合约", style="bold", width=12)
1054
+ t.add_column("费率", justify="right")
1055
+ t.add_column("年化", justify="right")
1056
+ t.add_column("下次结算", style="dim")
1057
+ t.add_column("信号", justify="center")
1058
+ for rt in rates:
1059
+ rate = rt.get("rate", 0)
1060
+ color = "red" if rate > 0.05 else "green" if rate < -0.01 else "dim"
1061
+ sig_s = rt.get("signal","中性")
1062
+ sig_c = "red" if sig_s == "空" else "green" if sig_s == "多" else "dim"
1063
+ t.add_row(
1064
+ rt.get("symbol",""),
1065
+ f"[{color}]{rt.get('rate_pct','')}[/{color}]",
1066
+ rt.get("annualized",""),
1067
+ rt.get("next_funding","")[:12],
1068
+ f"[{sig_c}]{sig_s}[/{sig_c}]",
1069
+ )
1070
+ console.print(t)
1071
+ console.print(f" [dim]市场偏向: [bold]{bias}[/bold] 正费率=多头付费给空头,负费率=空头付费给多头[/dim]")
1072
+ else:
1073
+ for rt in rates:
1074
+ print(f" {rt.get('symbol','')} {rt.get('rate_pct','')} (年化{rt.get('annualized','')})")
1075
+
1076
+
1077
+ def render_peer_comparison(r: dict, *, console=None, has_rich: bool = True) -> None:
1078
+ """Render peer comparison table."""
1079
+ if not r.get("success"):
1080
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
1081
+ return
1082
+ rows = r.get("table", [])
1083
+ if has_rich:
1084
+ from rich.table import Table
1085
+ from rich.rule import Rule
1086
+ symbol = r.get("symbol","")
1087
+ console.print(Rule(f"[bold]{symbol}[/bold] 同行估值对比", style="dim"))
1088
+ t = Table(show_header=True, box=None, padding=(0,1))
1089
+ t.add_column("代码", style="bold", width=8)
1090
+ t.add_column("名称", width=14)
1091
+ t.add_column("PE", justify="right")
1092
+ t.add_column("PB", justify="right")
1093
+ t.add_column("ROE%", justify="right")
1094
+ t.add_column("股息%", justify="right")
1095
+ t.add_column("市值(B)", justify="right", style="dim")
1096
+ for row in rows:
1097
+ is_t = row.get("is_target", False)
1098
+ pe = f"{row['pe']:.1f}" if row.get("pe") else "—"
1099
+ pb = f"{row['pb']:.2f}" if row.get("pb") else "—"
1100
+ roe = f"{row['roe_pct']:.1f}" if row.get("roe_pct") else "—"
1101
+ dy = f"{row['div_yield']:.2f}" if row.get("div_yield") else "—"
1102
+ mc = f"{row['market_cap_b']:.0f}" if row.get("market_cap_b") else "—"
1103
+ # Highlight target row
1104
+ style = "bold cyan" if is_t else ""
1105
+ t.add_row(
1106
+ f"[{style}]{row['symbol']}[/{style}]" if style else row["symbol"],
1107
+ (row.get("name","") or "")[:14],
1108
+ pe, pb, roe, dy, mc,
1109
+ )
1110
+ console.print(t)
1111
+ analysis = r.get("analysis", [])
1112
+ for line in analysis:
1113
+ console.print(f" [dim]▸ {line}[/dim]")
1114
+ else:
1115
+ print(f"\n{r.get('symbol','')} 同行对比")
1116
+ for row in rows:
1117
+ print(f" {row['symbol']:<8} PE:{row.get('pe','—')} PB:{row.get('pb','—')}")
1118
+
1119
+
1120
+ # ── 不动产渲染函数 ─────────────────────────────────────────────────────────────
1121
+
1122
+ def render_house_price(r: dict, *, console=None, has_rich: bool = True) -> None:
1123
+ if not has_rich:
1124
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1125
+ if not r.get("success"):
1126
+ console.print(f"[red]{r.get('error','获取失败')}[/red]"); return
1127
+ from rich.table import Table as _T
1128
+ from rich import box as _box
1129
+ c1 = str(r.get("city1") or "城市1")
1130
+ c2 = str(r.get("city2") or "城市2")
1131
+ lc1, lc2 = r.get("latest_city1") or {}, r.get("latest_city2") or {}
1132
+
1133
+ def _fmt(v):
1134
+ if v is None: return "[dim]—[/dim]"
1135
+ fv = float(v) if not isinstance(v, float) else v
1136
+ color = "green" if fv > 0 else "red" if fv < 0 else "dim"
1137
+ return f"[{color}]{fv:+.2f}%[/{color}]"
1138
+
1139
+ tb = _T(title=f"[bold]🏠 房价指数对比[/bold]", box=_box.ROUNDED, show_header=True)
1140
+ tb.add_column("指标", style="dim")
1141
+ tb.add_column(c1, justify="right")
1142
+ tb.add_column(c2, justify="right")
1143
+ tb.add_row("新建商品房同比", _fmt(lc1.get("new_yoy")), _fmt(lc2.get("new_yoy")))
1144
+ tb.add_row("新建商品房环比", _fmt(lc1.get("new_mom")), _fmt(lc2.get("new_mom")))
1145
+ tb.add_row("二手房价同比", _fmt(lc1.get("second_yoy")), _fmt(lc2.get("second_yoy")))
1146
+ tb.add_row("二手房价环比", _fmt(lc1.get("second_mom")), _fmt(lc2.get("second_mom")))
1147
+ tb.add_row("[dim]数据期[/dim]", str(lc1.get("date") or "—"), str(lc2.get("date") or "—"))
1148
+ console.print(tb)
1149
+ console.print("[dim]数据来源:国家统计局 via akshare[/dim]")
1150
+
1151
+
1152
+ def render_reits_list(r: dict, *, console=None, has_rich: bool = True) -> None:
1153
+ if not has_rich:
1154
+ for row in r.get("reits", [])[:10]: print(row); return
1155
+ if not r.get("success"):
1156
+ console.print(f"[red]{r.get('error','获取失败')}[/red]"); return
1157
+ from rich.table import Table as _T
1158
+ from rich import box as _box
1159
+ tb = _T(title=f"[bold]🏗 中国 REITs 实时行情[/bold]", box=_box.ROUNDED)
1160
+ tb.add_column("代码", style="cyan")
1161
+ tb.add_column("名称")
1162
+ tb.add_column("最新价", justify="right")
1163
+ tb.add_column("涨跌幅", justify="right")
1164
+ tb.add_column("昨收", justify="right", style="dim")
1165
+ tb.add_column("成交额(万)", justify="right", style="dim")
1166
+ for row in r.get("reits", [])[:20]:
1167
+ chg = row.get("涨跌幅") or 0
1168
+ try: chg_f = float(chg)
1169
+ except Exception: chg_f = 0
1170
+ color = "green" if chg_f > 0 else "red" if chg_f < 0 else "dim"
1171
+ vol_wan = ""
1172
+ try: vol_wan = f"{float(row.get('成交额',0))/10000:.0f}"
1173
+ except Exception as _e: logger.debug("vol_wan parse error: %s", _e)
1174
+ tb.add_row(
1175
+ str(row.get("代码","")),
1176
+ str(row.get("名称",""))[:12],
1177
+ str(row.get("最新价","")),
1178
+ f"[{color}]{chg}%[/{color}]",
1179
+ str(row.get("昨收","")),
1180
+ vol_wan,
1181
+ )
1182
+ console.print(tb)
1183
+ console.print(f"[dim]共 {r.get('count',0)} 只 REITs · 数据来源:东方财富[/dim]")
1184
+
1185
+
1186
+ def render_rental_yield(r: dict, *, console=None, has_rich: bool = True) -> None:
1187
+ if not has_rich:
1188
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1189
+ if not r.get("success"):
1190
+ console.print(f"[red]{r.get('error','计算失败')}[/red]"); return
1191
+ from rich.panel import Panel as _P
1192
+ from rich.columns import Columns as _C
1193
+ from rich import box as _box
1194
+ assess_color = "green" if "优质" in str(r.get("assessment","")) else \
1195
+ "yellow" if "合理" in str(r.get("assessment","")) else "red"
1196
+ lines = [
1197
+ f"[bold cyan]购入价格[/bold cyan] {r['purchase_price_wan']:.1f} 万元",
1198
+ f"[bold cyan]月租金[/bold cyan] {r['monthly_rent']:.0f} 元/月",
1199
+ f"[dim]───────────────────────────────[/dim]",
1200
+ f"[bold]毛租金收益率[/bold] [{assess_color}]{r['gross_yield_pct']:.2f}%[/{assess_color}]",
1201
+ f"[bold]净收益率[/bold] {r['net_yield_pct']:.2f}%",
1202
+ f"[bold]资本化率[/bold] {r['cap_rate_pct']:.2f}%",
1203
+ f"[bold]回本年限[/bold] {r['payback_years']:.1f} 年",
1204
+ ]
1205
+ if r.get("leveraged_yield_pct") is not None:
1206
+ lines.append(f"[bold]杠杆收益率[/bold] {r['leveraged_yield_pct']:.2f}% [dim](含贷款)[/dim]")
1207
+ lines += [
1208
+ f"[dim]───────────────────────────────[/dim]",
1209
+ f"[{assess_color}]综合评级:{r.get('assessment','')}[/{assess_color}]",
1210
+ f"[dim]{r.get('benchmark','')}[/dim]",
1211
+ ]
1212
+ console.print(_P("\n".join(lines), title="[bold]💰 租金收益率分析[/bold]",
1213
+ border_style="cyan"))
1214
+
1215
+
1216
+ def render_property_val(r: dict, *, console=None, has_rich: bool = True) -> None:
1217
+ if not has_rich:
1218
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1219
+ if not r.get("success"):
1220
+ console.print(f"[red]{r.get('error','估值失败')}[/red]"); return
1221
+ from rich.table import Table as _T
1222
+ from rich import box as _box
1223
+ from rich.panel import Panel as _P
1224
+ verd = r.get("verdict","")
1225
+ verd_color = "green" if "低估" in verd else "red" if "高估" in verd else "yellow"
1226
+ tb = _T(title="[bold]🏢 物业三合一估值[/bold]", box=_box.ROUNDED)
1227
+ tb.add_column("方法", style="dim")
1228
+ tb.add_column("估值(万元)", justify="right")
1229
+ tb.add_column("说明", style="dim")
1230
+ tb.add_row("收益法 (Cap Rate)", f"{r['income_approach']:.1f}", f"资本化率 {r['cap_rate_used']:.1f}%")
1231
+ tb.add_row("DCF 折现法", f"{r['dcf_approach']:.1f}", f"折现率 {r['discount_rate_used']:.1f}%")
1232
+ tb.add_row("市场比较法", f"{r['market_approach']:.1f}", "基于租金倍数推算")
1233
+ tb.add_row("[bold]综合估值[/bold]", f"[bold cyan]{r['blended_value_wan']:.1f}[/bold cyan]", "权重 4:4:2")
1234
+ console.print(tb)
1235
+ lo, hi = r.get("market_range_wan", [0, 0])
1236
+ console.print(f" 区位参考区间: [dim]{lo:.0f} — {hi:.0f} 万元[/dim]")
1237
+ console.print(f" 单价参考: [bold]{r.get('price_per_sqm',0):,.0f}[/bold] 元/㎡")
1238
+ console.print(f" [{verd_color}]{verd}[/{verd_color}]")
1239
+
1240
+
1241
+ def render_multi_city(r: dict, *, console=None, has_rich: bool = True) -> None:
1242
+ if not has_rich:
1243
+ for c in r.get("cities", []): print(c); return
1244
+ if not r.get("success"):
1245
+ console.print(f"[red]{r.get('error','获取失败')}[/red]"); return
1246
+ from rich.table import Table as _T
1247
+ from rich import box as _box
1248
+ tb = _T(title="[bold]🗺 多城市房价对比[/bold]", box=_box.ROUNDED)
1249
+ tb.add_column("城市", style="bold")
1250
+ tb.add_column("等级", style="dim")
1251
+ tb.add_column("数据期", style="dim")
1252
+ tb.add_column("新房同比", justify="right")
1253
+ tb.add_column("新房环比", justify="right")
1254
+ tb.add_column("二手同比", justify="right")
1255
+ for city in r.get("cities", []):
1256
+ def _fc(v):
1257
+ if v is None: return "[dim]—[/dim]"
1258
+ c = "green" if v > 0 else "red" if v < 0 else "dim"
1259
+ return f"[{c}]{v:+.2f}%[/{c}]"
1260
+ tb.add_row(
1261
+ city["city"], city.get("tier",""),
1262
+ city.get("date",""),
1263
+ _fc(city.get("new_yoy")), _fc(city.get("new_mom")),
1264
+ _fc(city.get("second_yoy")),
1265
+ )
1266
+ console.print(tb)
1267
+ console.print(f" 涨幅最高: [green]{r.get('top_riser','')}[/green] "
1268
+ f"涨幅最低/下跌: [red]{r.get('top_faller','')}[/red]")
1269
+
1270
+
1271
+ def render_asset_score(r: dict, *, console=None, has_rich: bool = True) -> None:
1272
+ if not has_rich:
1273
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1274
+ if not r.get("success"):
1275
+ console.print(f"[red]{r.get('error','评分失败')}[/red]"); return
1276
+ from rich.panel import Panel as _P
1277
+ score = r.get("score", 0)
1278
+ rating = r.get("rating", "")
1279
+ color = "green" if score >= 75 else "yellow" if score >= 60 else "red"
1280
+ bar_len = int(score / 5)
1281
+ bar = "█" * bar_len + "░" * (20 - bar_len)
1282
+ lines = [
1283
+ f"综合评分: [{color}]{score}[/{color}] / 100",
1284
+ f"评级: [{color}]{rating}[/{color}]",
1285
+ f"[{color}]{bar}[/{color}]",
1286
+ "[dim]─────────────────────────────[/dim]",
1287
+ ]
1288
+ for k, v in r.get("breakdown", {}).items():
1289
+ lines.append(f" {k:<12} [dim]{v}[/dim]")
1290
+ if r.get("suitable_businesses"):
1291
+ lines.append("[dim]─────────────────────────────[/dim]")
1292
+ lines.append(f"推荐业态: [cyan]{' / '.join(r['suitable_businesses'])}[/cyan]")
1293
+ console.print(_P("\n".join(lines), title="[bold]📍 资产区位评分[/bold]",
1294
+ border_style=color))
1295
+
1296
+
1297
+ # ── 数据分析渲染函数 ────────────────────────────────────────────────────────────
1298
+
1299
+ def render_corr_matrix(r: dict, *, console=None, has_rich: bool = True) -> None:
1300
+ if not has_rich:
1301
+ print(json.dumps(r.get("corr_matrix", {}), ensure_ascii=False, indent=2)); return
1302
+ if not r.get("success"):
1303
+ console.print(f"[red]{r.get('error','计算失败')}[/red]"); return
1304
+ from rich.table import Table as _T
1305
+ from rich import box as _box
1306
+ syms = r.get("symbols", [])
1307
+ corr = r.get("corr_matrix", {})
1308
+ tb = _T(title=f"[bold]📊 相关性矩阵 ({r.get('period','')}/{r.get('interval','1d')})[/bold]",
1309
+ box=_box.SIMPLE_HEAVY)
1310
+ tb.add_column("", style="bold")
1311
+ for s in syms:
1312
+ tb.add_column(s, justify="right")
1313
+ for s1 in syms:
1314
+ row_vals = []
1315
+ for s2 in syms:
1316
+ v = corr.get(s1, {}).get(s2)
1317
+ if v is None:
1318
+ row_vals.append("[dim]—[/dim]")
1319
+ elif s1 == s2:
1320
+ row_vals.append("[dim]1.00[/dim]")
1321
+ else:
1322
+ abs_v = abs(v)
1323
+ color = "red" if abs_v > 0.8 else "yellow" if abs_v > 0.5 else "green"
1324
+ row_vals.append(f"[{color}]{v:+.3f}[/{color}]")
1325
+ tb.add_row(f"[bold]{s1}[/bold]", *row_vals)
1326
+ console.print(tb)
1327
+ console.print("[dim]红 > 0.8 高相关 | 黄 0.5-0.8 中度 | 绿 < 0.5 低相关[/dim]")
1328
+ # Stats table
1329
+ stats = r.get("stats", {})
1330
+ if stats:
1331
+ st = _T(title="[dim]个股统计[/dim]", box=_box.MINIMAL)
1332
+ st.add_column("标的"); st.add_column("总收益%", justify="right")
1333
+ st.add_column("年化波动%", justify="right"); st.add_column("夏普", justify="right")
1334
+ st.add_column("最大回撤%", justify="right")
1335
+ for sym, sv in stats.items():
1336
+ ret = sv.get("return_total")
1337
+ rcolor = "green" if (ret or 0) > 0 else "red"
1338
+ st.add_row(sym,
1339
+ f"[{rcolor}]{ret:+.1f}[/{rcolor}]" if ret is not None else "—",
1340
+ f"{sv.get('volatility',0):.1f}" if sv.get("volatility") else "—",
1341
+ f"{sv.get('sharpe',0):.2f}" if sv.get("sharpe") else "—",
1342
+ f"[red]{sv.get('max_drawdown',0):.1f}[/red]")
1343
+ console.print(st)
1344
+
1345
+
1346
+ def render_portfolio_bt(r: dict, *, console=None, has_rich: bool = True) -> None:
1347
+ if not has_rich:
1348
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1349
+ if not r.get("success"):
1350
+ console.print(f"[red]{r.get('error','回测失败')}[/red]"); return
1351
+ from rich.table import Table as _T
1352
+ from rich import box as _box
1353
+ from rich.panel import Panel as _P
1354
+ pf = r.get("portfolio", {})
1355
+ bm = r.get("benchmark", {})
1356
+ ret = pf.get("total_return_pct", 0)
1357
+ rcolor = "green" if ret > 0 else "red"
1358
+ lines = [
1359
+ f" [bold]总收益率[/bold] [{rcolor}]{ret:+.2f}%[/{rcolor}]",
1360
+ f" [bold]年化波动率[/bold] {pf.get('annual_vol_pct',0):.2f}%",
1361
+ f" [bold]夏普比率[/bold] {pf.get('sharpe_ratio','—')}",
1362
+ f" [bold]最大回撤[/bold] [red]{pf.get('max_drawdown_pct',0):.2f}%[/red]",
1363
+ f" [bold]卡玛比率[/bold] {pf.get('calmar_ratio','—')}",
1364
+ ]
1365
+ if bm:
1366
+ br = bm.get("total_return_pct", 0)
1367
+ bc = "green" if br > 0 else "red"
1368
+ alpha = round(ret - br, 2)
1369
+ ac = "green" if alpha > 0 else "red"
1370
+ lines += [
1371
+ f" [dim]─────────────────────────────────[/dim]",
1372
+ f" [dim]基准 {bm['symbol']}[/dim] [{bc}]{br:+.2f}%[/{bc}]",
1373
+ f" [bold]超额收益[/bold] [{ac}]{alpha:+.2f}%[/{ac}]",
1374
+ ]
1375
+ console.print(_P("\n".join(lines), title="[bold]📈 组合回测结果[/bold]",
1376
+ border_style=rcolor))
1377
+ # Allocation table
1378
+ alloc = r.get("allocation", [])
1379
+ if alloc:
1380
+ ta = _T(title=f"[dim]持仓分配 · 回测区间 {r.get('period','N/A')} · 再平衡 {r.get('rebalance','—')}[/dim]",
1381
+ box=_box.MINIMAL)
1382
+ ta.add_column("标的"); ta.add_column("权重%", justify="right")
1383
+ for a in alloc:
1384
+ ta.add_row(a["symbol"], f"{a['weight_pct']:.1f}%")
1385
+ console.print(ta)
1386
+
1387
+
1388
+ def render_sql_result(r: dict, *, console=None, has_rich: bool = True) -> None:
1389
+ if not has_rich:
1390
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1391
+ if not r.get("success"):
1392
+ console.print(f"[red]{r.get('error','查询失败')}[/red]"); return
1393
+ from rich.table import Table as _T
1394
+ from rich import box as _box
1395
+ rows = r.get("rows", [])
1396
+ cols = r.get("columns", [])
1397
+ if not rows:
1398
+ console.print(f"[dim]查询返回 0 行[/dim]"); return
1399
+ tb = _T(title=f"[bold]🦆 DuckDB 查询结果[/bold] [dim]({r.get('row_count',0)} 行)[/dim]",
1400
+ box=_box.ROUNDED)
1401
+ for c in cols:
1402
+ tb.add_column(str(c))
1403
+ for row in rows[:100]:
1404
+ tb.add_row(*[str(row.get(c, "")) for c in cols])
1405
+ console.print(tb)
1406
+ if r.get("tables_loaded"):
1407
+ console.print(f"[dim]已加载表: {', '.join(r['tables_loaded'])}[/dim]")
1408
+
1409
+
1410
+ def render_alerts(r: dict, *, console=None, has_rich: bool = True) -> None:
1411
+ if not has_rich:
1412
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1413
+ from rich.table import Table as _T
1414
+ from rich import box as _box
1415
+ active = r.get("active_alerts", [])
1416
+ triggered = r.get("triggered_alerts", [])
1417
+ cond_lbl = {"gt": "高于", "lt": "低于", "cross_up": "向上突破", "cross_down": "向下跌破"}
1418
+ if active:
1419
+ ta = _T(title="[bold]🔔 活跃预警[/bold]", box=_box.ROUNDED)
1420
+ ta.add_column("标的", style="cyan"); ta.add_column("条件")
1421
+ ta.add_column("触发价", justify="right"); ta.add_column("备注", style="dim")
1422
+ ta.add_column("ID", style="dim")
1423
+ for a in active:
1424
+ ta.add_row(a["symbol"],
1425
+ cond_lbl.get(a["condition"], a["condition"]),
1426
+ str(a["price"]), a.get("note",""),
1427
+ a["id"][:16]+"…")
1428
+ console.print(ta)
1429
+ if triggered:
1430
+ tt = _T(title="[dim]已触发预警[/dim]", box=_box.MINIMAL)
1431
+ tt.add_column("标的"); tt.add_column("触发价"); tt.add_column("触发时间", style="dim")
1432
+ for a in triggered:
1433
+ tt.add_row(a["symbol"], str(a.get("triggered_price","")),
1434
+ str(a.get("triggered_at",""))[:16])
1435
+ console.print(tt)
1436
+ if not active and not triggered:
1437
+ console.print("[dim]暂无预警记录。使用 /alert add AAPL gt 200 设置预警[/dim]")
1438
+
1439
+
1440
+ def _prompt_float(label: str, default: float) -> float:
1441
+ """交互式数字输入,失败时返回 default。"""
1442
+
1443
+
1444
+ # Moved from aria_cli.py
1445
+ def format_backtest_output(data: dict):
1446
+ """Format backtest results as clean rows."""
1447
+ if not HAS_RICH:
1448
+ return json.dumps(data, indent=2, ensure_ascii=False)
1449
+
1450
+ d = data.get("data", data.get("backtest", data))
1451
+ total_ret = d.get("total_return", 0)
1452
+ ann_ret = d.get("annualized_return", 0)
1453
+ sharpe = d.get("sharpe_ratio", 0)
1454
+ max_dd = d.get("max_drawdown", 0)
1455
+ win_rate = d.get("win_rate", 0)
1456
+ trades = d.get("num_trades", 0)
1457
+ bh_ret = d.get("buy_hold_return", 0)
1458
+ outperf = d.get("outperformance", 0)
1459
+
1460
+ def _c(v): return "green" if v >= 0 else "red"
1461
+
1462
+ out = Text()
1463
+ out.append(" Backtest Results\n", style="bold")
1464
+ out.append(f" {'Total Return':<18s}", style="dim")
1465
+ out.append(f"{total_ret*100:+.2f}%", style=_c(total_ret))
1466
+ out.append(f" vs B&H ", style="dim")
1467
+ out.append(f"{bh_ret*100:+.2f}%\n", style=_c(bh_ret))
1468
+ out.append(f" {'Annualized':<18s}", style="dim")
1469
+ out.append(f"{ann_ret*100:+.2f}%\n")
1470
+ out.append(f" {'Sharpe Ratio':<18s}", style="dim")
1471
+ out.append(f"{sharpe:.2f}\n")
1472
+ out.append(f" {'Max Drawdown':<18s}", style="dim")
1473
+ out.append(f"{max_dd*100:.2f}%\n", style="red")
1474
+ out.append(f" {'Win Rate':<18s}", style="dim")
1475
+ out.append(f"{win_rate*100:.1f}%\n")
1476
+ out.append(f" {'Trades':<18s}", style="dim")
1477
+ out.append(f"{trades}\n")
1478
+ out.append(f" {'Outperformance':<18s}", style="dim")
1479
+ out.append(f"{outperf*100:+.2f}%\n", style=_c(outperf))
1480
+ return out