aria-code 0.52.0__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 (478) hide show
  1. aria_code/__init__.py +42 -0
  2. aria_code/_version.py +20 -0
  3. aria_code/adk_apps/__init__.py +1 -0
  4. aria_code/adk_apps/aria_code_review/__init__.py +1 -0
  5. aria_code/adk_apps/aria_code_review/agent.py +32 -0
  6. aria_code/adk_apps/aria_research/__init__.py +1 -0
  7. aria_code/adk_apps/aria_research/agent.py +31 -0
  8. aria_code/agents/__init__.py +34 -0
  9. aria_code/agents/base.py +318 -0
  10. aria_code/agents/code_review.py +200 -0
  11. aria_code/agents/deep/__init__.py +37 -0
  12. aria_code/agents/deep/calibration_loop.py +144 -0
  13. aria_code/agents/deep/critic.py +125 -0
  14. aria_code/agents/deep/deepen.py +193 -0
  15. aria_code/agents/deep/models.py +149 -0
  16. aria_code/agents/deep/pipeline.py +164 -0
  17. aria_code/agents/deep/quant_fusion.py +192 -0
  18. aria_code/agents/deep/themes.py +95 -0
  19. aria_code/agents/deep/tiers.py +106 -0
  20. aria_code/agents/engineering/__init__.py +14 -0
  21. aria_code/agents/engineering/coder.py +222 -0
  22. aria_code/agents/engineering/tester.py +145 -0
  23. aria_code/agents/financial/__init__.py +21 -0
  24. aria_code/agents/financial/cashflow_burn.py +119 -0
  25. aria_code/agents/financial/catalyst.py +279 -0
  26. aria_code/agents/financial/corporate_finance.py +175 -0
  27. aria_code/agents/financial/debate.py +224 -0
  28. aria_code/agents/financial/earnings.py +303 -0
  29. aria_code/agents/financial/fundamental.py +191 -0
  30. aria_code/agents/financial/macro.py +99 -0
  31. aria_code/agents/financial/news.py +207 -0
  32. aria_code/agents/financial/risk.py +165 -0
  33. aria_code/agents/financial/sector.py +279 -0
  34. aria_code/agents/financial/strategist.py +121 -0
  35. aria_code/agents/financial/stripe_revenue.py +127 -0
  36. aria_code/agents/financial/synthesis.py +274 -0
  37. aria_code/agents/financial/technical.py +258 -0
  38. aria_code/agents/orchestrator.py +44 -0
  39. aria_code/agents/portfolio_agent.py +363 -0
  40. aria_code/agents/realty/__init__.py +62 -0
  41. aria_code/agents/realty/asset_diagnosis.py +152 -0
  42. aria_code/agents/realty/business_match.py +166 -0
  43. aria_code/agents/realty/cashflow_verify.py +209 -0
  44. aria_code/agents/realty/contract_rules.py +210 -0
  45. aria_code/agents/realty/energy_anomaly.py +189 -0
  46. aria_code/agents/realty/exit_settlement.py +208 -0
  47. aria_code/agents/realty/fulfillment_risk.py +206 -0
  48. aria_code/agents/realty/ops_optimize.py +160 -0
  49. aria_code/agents/realty/revenue_share.py +215 -0
  50. aria_code/agents/registry.py +158 -0
  51. aria_code/agents/signal_scheme.py +111 -0
  52. aria_code/agents/sports/__init__.py +0 -0
  53. aria_code/agents/sports/football_agent.py +197 -0
  54. aria_code/agents/supervisor.py +113 -0
  55. aria_code/agents/team.py +492 -0
  56. aria_code/agents/warehouse/__init__.py +25 -0
  57. aria_code/agents/warehouse/contracts.py +26 -0
  58. aria_code/agents/warehouse/dashboard.py +155 -0
  59. aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
  60. aria_code/agents/warehouse/inbound_exceptions.py +25 -0
  61. aria_code/agents/warehouse/inventory_health.py +24 -0
  62. aria_code/agents/warehouse/local_run.py +71 -0
  63. aria_code/agents/warehouse/local_snapshot.py +132 -0
  64. aria_code/agents/warehouse/logistics_cost.py +84 -0
  65. aria_code/agents/warehouse/logistics_sync.py +21 -0
  66. aria_code/agents/warehouse/workflow.py +31 -0
  67. aria_code/aliyun_data_client.py +4 -0
  68. aria_code/apps/README.md +12 -0
  69. aria_code/apps/__init__.py +2 -0
  70. aria_code/apps/channels/README.md +15 -0
  71. aria_code/apps/channels/__init__.py +17 -0
  72. aria_code/apps/channels/intake.py +92 -0
  73. aria_code/apps/channels/registry.py +95 -0
  74. aria_code/apps/channels/tradingview.py +94 -0
  75. aria_code/apps/cli/README.md +13 -0
  76. aria_code/apps/cli/__init__.py +2 -0
  77. aria_code/apps/cli/_optional.py +120 -0
  78. aria_code/apps/cli/bootstrap.py +145 -0
  79. aria_code/apps/cli/broker_render.py +206 -0
  80. aria_code/apps/cli/codegen_paths.py +29 -0
  81. aria_code/apps/cli/commands/__init__.py +16 -0
  82. aria_code/apps/cli/commands/_business_render.py +78 -0
  83. aria_code/apps/cli/commands/_ui.py +71 -0
  84. aria_code/apps/cli/commands/analysis_cmds.py +369 -0
  85. aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
  86. aria_code/apps/cli/commands/auth_cmds.py +239 -0
  87. aria_code/apps/cli/commands/backtest_cmds.py +2543 -0
  88. aria_code/apps/cli/commands/broker_cmds.py +1234 -0
  89. aria_code/apps/cli/commands/business_workflow_cmds.py +319 -0
  90. aria_code/apps/cli/commands/canvas_cmds.py +77 -0
  91. aria_code/apps/cli/commands/catalog.py +87 -0
  92. aria_code/apps/cli/commands/core_cmds.py +2520 -0
  93. aria_code/apps/cli/commands/data_cmds.py +457 -0
  94. aria_code/apps/cli/commands/diagnostic_cmds.py +230 -0
  95. aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
  96. aria_code/apps/cli/commands/file_cmds.py +312 -0
  97. aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
  98. aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
  99. aria_code/apps/cli/commands/market.py +434 -0
  100. aria_code/apps/cli/commands/market_cmds.py +1381 -0
  101. aria_code/apps/cli/commands/market_context.py +425 -0
  102. aria_code/apps/cli/commands/market_render.py +7 -0
  103. aria_code/apps/cli/commands/model_cmds.py +2153 -0
  104. aria_code/apps/cli/commands/ops_cmds.py +721 -0
  105. aria_code/apps/cli/commands/orchestrator_cmds.py +179 -0
  106. aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
  107. aria_code/apps/cli/commands/portfolio_cmds.py +1278 -0
  108. aria_code/apps/cli/commands/report.py +437 -0
  109. aria_code/apps/cli/commands/scaffold_templates.py +617 -0
  110. aria_code/apps/cli/commands/session_cmds.py +236 -0
  111. aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
  112. aria_code/apps/cli/commands/team.py +669 -0
  113. aria_code/apps/cli/commands/team_render.py +8 -0
  114. aria_code/apps/cli/commands/ui_cmds.py +401 -0
  115. aria_code/apps/cli/commands/warehouse_cmds.py +79 -0
  116. aria_code/apps/cli/commands/workflow_cmds.py +480 -0
  117. aria_code/apps/cli/commands/workspace_cmds.py +1494 -0
  118. aria_code/apps/cli/config_paths.py +73 -0
  119. aria_code/apps/cli/config_store.py +78 -0
  120. aria_code/apps/cli/constants.py +88 -0
  121. aria_code/apps/cli/context.py +34 -0
  122. aria_code/apps/cli/deterministic.py +191 -0
  123. aria_code/apps/cli/direct.py +48 -0
  124. aria_code/apps/cli/finance_service_catalog.py +111 -0
  125. aria_code/apps/cli/football_reports.py +231 -0
  126. aria_code/apps/cli/github_app_auth.py +135 -0
  127. aria_code/apps/cli/google_login.py +196 -0
  128. aria_code/apps/cli/handlers/__init__.py +11 -0
  129. aria_code/apps/cli/handlers/broker_handlers.py +122 -0
  130. aria_code/apps/cli/handlers/chart_handlers.py +1309 -0
  131. aria_code/apps/cli/handlers/market_handlers.py +2513 -0
  132. aria_code/apps/cli/handlers/realty_handlers.py +114 -0
  133. aria_code/apps/cli/handlers/sports_handlers.py +22 -0
  134. aria_code/apps/cli/handlers/strategy_advice.py +82 -0
  135. aria_code/apps/cli/helpers.py +302 -0
  136. aria_code/apps/cli/hooks.py +180 -0
  137. aria_code/apps/cli/i18n.py +297 -0
  138. aria_code/apps/cli/intent.py +136 -0
  139. aria_code/apps/cli/intent_router.py +276 -0
  140. aria_code/apps/cli/intent_signals.py +344 -0
  141. aria_code/apps/cli/lifecycle_hooks.py +48 -0
  142. aria_code/apps/cli/local_tool_schemas.py +536 -0
  143. aria_code/apps/cli/main.py +29 -0
  144. aria_code/apps/cli/market_metadata.py +135 -0
  145. aria_code/apps/cli/market_universe.py +487 -0
  146. aria_code/apps/cli/message_processing.py +310 -0
  147. aria_code/apps/cli/model_catalog.py +620 -0
  148. aria_code/apps/cli/pdf_report.py +977 -0
  149. aria_code/apps/cli/plan_mode.py +139 -0
  150. aria_code/apps/cli/plotly_html.py +15 -0
  151. aria_code/apps/cli/prediction_feedback.py +202 -0
  152. aria_code/apps/cli/preflight.py +502 -0
  153. aria_code/apps/cli/project_aria.py +60 -0
  154. aria_code/apps/cli/prompt_assembly.py +59 -0
  155. aria_code/apps/cli/prompts/__init__.py +0 -0
  156. aria_code/apps/cli/prompts/coding.py +53 -0
  157. aria_code/apps/cli/prompts/select.py +142 -0
  158. aria_code/apps/cli/prompts/system_prompts.py +552 -0
  159. aria_code/apps/cli/prompts/ui.py +436 -0
  160. aria_code/apps/cli/provider_endpoints.py +237 -0
  161. aria_code/apps/cli/provider_keys.py +73 -0
  162. aria_code/apps/cli/providers/__init__.py +1 -0
  163. aria_code/apps/cli/providers/base.py +521 -0
  164. aria_code/apps/cli/providers/chat_routing.py +128 -0
  165. aria_code/apps/cli/providers/collaboration.py +98 -0
  166. aria_code/apps/cli/providers/llm/__init__.py +1 -0
  167. aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
  168. aria_code/apps/cli/providers/llm/ollama_stream.py +1594 -0
  169. aria_code/apps/cli/providers/llm/response_cache.py +60 -0
  170. aria_code/apps/cli/providers/llm/sse_stream.py +257 -0
  171. aria_code/apps/cli/providers/runtime_bridge.py +353 -0
  172. aria_code/apps/cli/providers/vertexai_stream.py +316 -0
  173. aria_code/apps/cli/runtime_consumer.py +657 -0
  174. aria_code/apps/cli/sandbox.py +32 -0
  175. aria_code/apps/cli/session_export.py +120 -0
  176. aria_code/apps/cli/session_jsonl.py +208 -0
  177. aria_code/apps/cli/session_store.py +112 -0
  178. aria_code/apps/cli/skills_catalog.py +273 -0
  179. aria_code/apps/cli/todo_tracker.py +190 -0
  180. aria_code/apps/cli/tool_executor.py +823 -0
  181. aria_code/apps/cli/tool_registry.py +34 -0
  182. aria_code/apps/cli/tool_scope.py +106 -0
  183. aria_code/apps/cli/tools/__init__.py +40 -0
  184. aria_code/apps/cli/tools/context.py +46 -0
  185. aria_code/apps/cli/tools/file_tools.py +180 -0
  186. aria_code/apps/cli/tools/market_tools.py +620 -0
  187. aria_code/apps/cli/tools/notebook_tools.py +111 -0
  188. aria_code/apps/cli/tools/system_tools.py +726 -0
  189. aria_code/apps/cli/tools/write_tools.py +845 -0
  190. aria_code/apps/cli/tradingview_bridge.py +529 -0
  191. aria_code/apps/cli/turn_planning.py +137 -0
  192. aria_code/apps/cli/update_check.py +153 -0
  193. aria_code/apps/cli/utils/__init__.py +0 -0
  194. aria_code/apps/cli/utils/market_detect.py +1597 -0
  195. aria_code/apps/daemon/README.md +14 -0
  196. aria_code/apps/vscode/README.md +115 -0
  197. aria_code/apps/vscode/package.json +70 -0
  198. aria_code/aria_cli.py +6743 -0
  199. aria_code/aria_daemon.py +1398 -0
  200. aria_code/aria_feishu_bot.py +1423 -0
  201. aria_code/aria_mcp_server.py +7 -0
  202. aria_code/aria_relay_client.py +4 -0
  203. aria_code/aria_relay_server.py +490 -0
  204. aria_code/aria_telegram_bot.py +202 -0
  205. aria_code/ariarc.py +385 -0
  206. aria_code/artifacts.py +581 -0
  207. aria_code/backtest_engine.py +4 -0
  208. aria_code/backtest_report.py +4 -0
  209. aria_code/brokers/__init__.py +78 -0
  210. aria_code/brokers/automation.py +193 -0
  211. aria_code/brokers/base.py +231 -0
  212. aria_code/brokers/capabilities.py +264 -0
  213. aria_code/brokers/cn/__init__.py +10 -0
  214. aria_code/brokers/cn/easytrader_broker.py +193 -0
  215. aria_code/brokers/cn/futu_broker.py +194 -0
  216. aria_code/brokers/cn/longbridge_broker.py +190 -0
  217. aria_code/brokers/cn/tiger_broker.py +196 -0
  218. aria_code/brokers/cn/xtquant_broker.py +225 -0
  219. aria_code/brokers/config.py +414 -0
  220. aria_code/brokers/intl/__init__.py +5 -0
  221. aria_code/brokers/intl/alpaca_broker.py +183 -0
  222. aria_code/brokers/intl/ibkr_broker.py +215 -0
  223. aria_code/brokers/intl/trading212_broker.py +183 -0
  224. aria_code/brokers/intl/webull_broker.py +156 -0
  225. aria_code/brokers/paper_broker.py +266 -0
  226. aria_code/brokers/planning.py +296 -0
  227. aria_code/brokers/registry.py +213 -0
  228. aria_code/brokers/trading.py +267 -0
  229. aria_code/canva_client.py +4 -0
  230. aria_code/change_store.py +127 -0
  231. aria_code/clients/__init__.py +0 -0
  232. aria_code/clients/aliyun_data_client.py +660 -0
  233. aria_code/clients/aria_relay_client.py +182 -0
  234. aria_code/clients/canva_client.py +284 -0
  235. aria_code/clients/figma_client.py +100 -0
  236. aria_code/clients/football_data_client.py +1671 -0
  237. aria_code/clients/kling_video_client.py +182 -0
  238. aria_code/clients/market_data_client.py +1976 -0
  239. aria_code/clients/openai_image_client.py +242 -0
  240. aria_code/clients/runway_video_client.py +157 -0
  241. aria_code/clients/warehouse_erp_client.py +105 -0
  242. aria_code/cloud_memory.py +181 -0
  243. aria_code/command_safety.py +19 -0
  244. aria_code/computer_use_tools.py +4 -0
  245. aria_code/dashboard_generator.py +579 -0
  246. aria_code/data_analysis_tools.py +4 -0
  247. aria_code/data_cleaner.py +483 -0
  248. aria_code/data_service.py +513 -0
  249. aria_code/datasources/__init__.py +23 -0
  250. aria_code/datasources/base.py +166 -0
  251. aria_code/datasources/router.py +253 -0
  252. aria_code/datasources/sources/__init__.py +15 -0
  253. aria_code/datasources/sources/akshare_source.py +269 -0
  254. aria_code/datasources/sources/alpha_vantage_source.py +203 -0
  255. aria_code/datasources/sources/edgar_source.py +219 -0
  256. aria_code/datasources/sources/finnhub_source.py +198 -0
  257. aria_code/datasources/sources/fred_source.py +223 -0
  258. aria_code/datasources/sources/tushare_source.py +143 -0
  259. aria_code/datasources/sources/web_scraper_source.py +278 -0
  260. aria_code/datasources/sources/world_bank_source.py +206 -0
  261. aria_code/datasources/sources/yfinance_source.py +152 -0
  262. aria_code/demo_player.py +207 -0
  263. aria_code/doctor.py +813 -0
  264. aria_code/domain/__init__.py +0 -0
  265. aria_code/domain/backtest_engine.py +354 -0
  266. aria_code/domain/backtest_report.py +472 -0
  267. aria_code/domain/finance_formulas.py +389 -0
  268. aria_code/domain/portfolio_ledger.py +307 -0
  269. aria_code/domain/strategy_vault.py +421 -0
  270. aria_code/evals/__init__.py +37 -0
  271. aria_code/evals/harness.py +598 -0
  272. aria_code/evals/runner.py +238 -0
  273. aria_code/external_agent_runner.py +115 -0
  274. aria_code/figma_client.py +4 -0
  275. aria_code/file_analysis_tools.py +4 -0
  276. aria_code/finance_formulas.py +4 -0
  277. aria_code/football_data_client.py +4 -0
  278. aria_code/image_gen_tools.py +4 -0
  279. aria_code/intent_classifier.py +386 -0
  280. aria_code/kling_video_client.py +4 -0
  281. aria_code/licensing.py +151 -0
  282. aria_code/local_finance_tools.py +4 -0
  283. aria_code/local_image_provider.py +4 -0
  284. aria_code/local_llm_provider.py +4 -0
  285. aria_code/macro_tools.py +4 -0
  286. aria_code/markdown_pdf.py +289 -0
  287. aria_code/market_data_client.py +4 -0
  288. aria_code/mcp_client.py +639 -0
  289. aria_code/memory_manager.py +246 -0
  290. aria_code/model_capability.py +486 -0
  291. aria_code/notification_tools.py +4 -0
  292. aria_code/openai_image_client.py +4 -0
  293. aria_code/packages/__init__.py +23 -0
  294. aria_code/packages/adk_bridge/__init__.py +11 -0
  295. aria_code/packages/adk_bridge/code_review_tools.py +49 -0
  296. aria_code/packages/adk_bridge/market_tools.py +125 -0
  297. aria_code/packages/aria_agents/__init__.py +5 -0
  298. aria_code/packages/aria_agents/manifest.py +78 -0
  299. aria_code/packages/aria_core/__init__.py +34 -0
  300. aria_code/packages/aria_core/architecture.py +269 -0
  301. aria_code/packages/aria_core/export.py +147 -0
  302. aria_code/packages/aria_core/manifest.py +65 -0
  303. aria_code/packages/aria_core/paths.py +57 -0
  304. aria_code/packages/aria_core/secure_file.py +68 -0
  305. aria_code/packages/aria_infra/__init__.py +15 -0
  306. aria_code/packages/aria_infra/arthera.py +52 -0
  307. aria_code/packages/aria_infra/doctor.py +246 -0
  308. aria_code/packages/aria_infra/product.py +40 -0
  309. aria_code/packages/aria_mcp/__init__.py +25 -0
  310. aria_code/packages/aria_mcp/bridge.py +201 -0
  311. aria_code/packages/aria_mcp/circuit.py +82 -0
  312. aria_code/packages/aria_mcp/config.py +111 -0
  313. aria_code/packages/aria_mcp/installer.py +95 -0
  314. aria_code/packages/aria_mcp/server.py +1550 -0
  315. aria_code/packages/aria_mcp/tools.py +81 -0
  316. aria_code/packages/aria_sdk/__init__.py +19 -0
  317. aria_code/packages/aria_sdk/client.py +396 -0
  318. aria_code/packages/aria_sdk/providers.py +70 -0
  319. aria_code/packages/aria_sdk/streaming.py +79 -0
  320. aria_code/packages/aria_sdk/types.py +86 -0
  321. aria_code/packages/aria_services/__init__.py +92 -0
  322. aria_code/packages/aria_services/cache.py +60 -0
  323. aria_code/packages/aria_services/context.py +356 -0
  324. aria_code/packages/aria_services/context_checkpoints.py +157 -0
  325. aria_code/packages/aria_services/data.py +11 -0
  326. aria_code/packages/aria_services/provider_health.py +268 -0
  327. aria_code/packages/aria_services/references.py +247 -0
  328. aria_code/packages/aria_services/registry.py +249 -0
  329. aria_code/packages/aria_services/research_protocol.py +57 -0
  330. aria_code/packages/aria_services/research_quality.py +186 -0
  331. aria_code/packages/aria_services/settings.py +157 -0
  332. aria_code/packages/aria_services/usage.py +151 -0
  333. aria_code/packages/aria_skills/__init__.py +46 -0
  334. aria_code/packages/aria_skills/catalog.py +149 -0
  335. aria_code/packages/aria_skills/loader.py +455 -0
  336. aria_code/packages/aria_skills/registry.py +84 -0
  337. aria_code/packages/aria_tools/__init__.py +5 -0
  338. aria_code/packages/aria_tools/financial/__init__.py +8 -0
  339. aria_code/packages/aria_tools/financial/_paths.py +130 -0
  340. aria_code/packages/aria_tools/financial/compliance.py +31 -0
  341. aria_code/packages/aria_tools/financial/factors.py +33 -0
  342. aria_code/packages/aria_tools/financial/risk_tools.py +33 -0
  343. aria_code/packages/aria_tools/financial/strategy.py +31 -0
  344. aria_code/packages/aria_tools/registry.py +128 -0
  345. aria_code/packages/quant_engine/__init__.py +34 -0
  346. aria_code/packages/quant_engine/_namespace.py +62 -0
  347. aria_code/packages/quant_engine/backtest/__init__.py +14 -0
  348. aria_code/packages/quant_engine/backtest/core.py +53 -0
  349. aria_code/packages/quant_engine/backtest/engine.py +96 -0
  350. aria_code/packages/quant_engine/portfolio/__init__.py +13 -0
  351. aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
  352. aria_code/packages/quant_engine/sports/__init__.py +80 -0
  353. aria_code/packages/quant_engine/sports/calibrator.py +354 -0
  354. aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
  355. aria_code/packages/quant_engine/sports/elo.py +300 -0
  356. aria_code/packages/quant_engine/sports/form.py +188 -0
  357. aria_code/packages/quant_engine/sports/h2h.py +195 -0
  358. aria_code/packages/quant_engine/sports/ml_model.py +377 -0
  359. aria_code/packages/quant_engine/sports/predictor.py +312 -0
  360. aria_code/packages/quant_engine/sports/tracker.py +665 -0
  361. aria_code/packages/quant_engine/stochastic/__init__.py +35 -0
  362. aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  363. aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
  364. aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  365. aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  366. aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
  367. aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  368. aria_code/packs/__init__.py +85 -0
  369. aria_code/packs/base.py +168 -0
  370. aria_code/packs/finance/__init__.py +250 -0
  371. aria_code/packs/logistics/__init__.py +169 -0
  372. aria_code/packs/payments/__init__.py +139 -0
  373. aria_code/packs/realty/__init__.py +190 -0
  374. aria_code/packs/registry.py +202 -0
  375. aria_code/packs/rules.py +90 -0
  376. aria_code/plan_utils.py +194 -0
  377. aria_code/plugin_loader.py +328 -0
  378. aria_code/portfolio_ledger.py +4 -0
  379. aria_code/preview_server.py +449 -0
  380. aria_code/privacy/__init__.py +5 -0
  381. aria_code/privacy/feedback.py +123 -0
  382. aria_code/project_review.py +278 -0
  383. aria_code/project_review_api.py +235 -0
  384. aria_code/project_review_server.py +18 -0
  385. aria_code/project_tools.py +4 -0
  386. aria_code/providers/__init__.py +30 -0
  387. aria_code/providers/llm/__init__.py +19 -0
  388. aria_code/providers/llm/anthropic.py +184 -0
  389. aria_code/providers/llm/autoconfig.py +213 -0
  390. aria_code/providers/llm/base.py +139 -0
  391. aria_code/providers/llm/custom.py +158 -0
  392. aria_code/providers/llm/ollama.py +129 -0
  393. aria_code/providers/llm/openai_compat.py +313 -0
  394. aria_code/providers/llm/registry.py +444 -0
  395. aria_code/providers/local_image_provider.py +202 -0
  396. aria_code/providers/local_llm_provider.py +589 -0
  397. aria_code/realty_data_tools.py +4 -0
  398. aria_code/report_exporters.py +150 -0
  399. aria_code/report_generator.py +1764 -0
  400. aria_code/runtime/__init__.py +169 -0
  401. aria_code/runtime/acceptance.py +481 -0
  402. aria_code/runtime/agent_loop.py +1520 -0
  403. aria_code/runtime/approval.py +57 -0
  404. aria_code/runtime/budget.py +232 -0
  405. aria_code/runtime/checkpoints.py +432 -0
  406. aria_code/runtime/crash_report.py +174 -0
  407. aria_code/runtime/events.py +113 -0
  408. aria_code/runtime/gateway.py +150 -0
  409. aria_code/runtime/lsp.py +484 -0
  410. aria_code/runtime/orchestrator.py +55 -0
  411. aria_code/runtime/repo_map.py +700 -0
  412. aria_code/runtime/run_state.py +127 -0
  413. aria_code/runtime/run_store.py +450 -0
  414. aria_code/runtime/self_healing.py +422 -0
  415. aria_code/runtime/subagent.py +547 -0
  416. aria_code/runtime/task_ledger.py +86 -0
  417. aria_code/runtime/task_orchestrator.py +90 -0
  418. aria_code/runtime/tool_executor.py +217 -0
  419. aria_code/runtime/tool_policy.py +107 -0
  420. aria_code/runtime/worktrees.py +215 -0
  421. aria_code/runway_video_client.py +4 -0
  422. aria_code/safety/__init__.py +23 -0
  423. aria_code/safety/permissions.py +306 -0
  424. aria_code/safety/service.py +102 -0
  425. aria_code/setup_wizard.py +673 -0
  426. aria_code/spreadsheet_tools.py +4 -0
  427. aria_code/strategy_vault.py +4 -0
  428. aria_code/tools/__init__.py +0 -0
  429. aria_code/tools/artifact_tools.py +252 -0
  430. aria_code/tools/broker_tools.py +92 -0
  431. aria_code/tools/build_quant_engine.py +97 -0
  432. aria_code/tools/code_audit_tools.py +192 -0
  433. aria_code/tools/computer_use_tools.py +504 -0
  434. aria_code/tools/data_analysis_tools.py +809 -0
  435. aria_code/tools/enterprise_finance_tools.py +156 -0
  436. aria_code/tools/extended_tools.py +240 -0
  437. aria_code/tools/file_analysis_tools.py +885 -0
  438. aria_code/tools/image_gen_tools.py +114 -0
  439. aria_code/tools/local_finance_tools.py +3417 -0
  440. aria_code/tools/logistics_tools.py +135 -0
  441. aria_code/tools/macro_tools.py +369 -0
  442. aria_code/tools/notification_tools.py +249 -0
  443. aria_code/tools/project_tools.py +525 -0
  444. aria_code/tools/realty_data_tools.py +659 -0
  445. aria_code/tools/spreadsheet_tools.py +388 -0
  446. aria_code/tools/stripe_tools.py +115 -0
  447. aria_code/ui/__init__.py +105 -0
  448. aria_code/ui/banner.py +508 -0
  449. aria_code/ui/completer.py +501 -0
  450. aria_code/ui/console.py +281 -0
  451. aria_code/ui/image_render.py +243 -0
  452. aria_code/ui/input_box.py +488 -0
  453. aria_code/ui/picker.py +201 -0
  454. aria_code/ui/render/__init__.py +11 -0
  455. aria_code/ui/render/finance.py +1705 -0
  456. aria_code/ui/render/market.py +225 -0
  457. aria_code/ui/render/output.py +775 -0
  458. aria_code/ui/render/responsive.py +75 -0
  459. aria_code/ui/render/team.py +354 -0
  460. aria_code/ui/robot.py +235 -0
  461. aria_code/ui/startup_dashboard.py +109 -0
  462. aria_code/utils/vscode_uri.py +37 -0
  463. aria_code/video_analysis.py +147 -0
  464. aria_code/video_editor.py +271 -0
  465. aria_code/workspace/__init__.py +6 -0
  466. aria_code/workspace/files.py +193 -0
  467. aria_code/workspace/verify.py +113 -0
  468. aria_code/workspace_changes.py +199 -0
  469. aria_code/workspace_context.py +233 -0
  470. aria_code/workspace_service.py +196 -0
  471. aria_code/workspace_tools.py +150 -0
  472. aria_code-0.52.0.dist-info/METADATA +237 -0
  473. aria_code-0.52.0.dist-info/RECORD +478 -0
  474. aria_code-0.52.0.dist-info/WHEEL +5 -0
  475. aria_code-0.52.0.dist-info/entry_points.txt +3 -0
  476. aria_code-0.52.0.dist-info/licenses/LICENSE +201 -0
  477. aria_code-0.52.0.dist-info/licenses/NOTICE +49 -0
  478. aria_code-0.52.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1705 @@
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
+ as_of = result.get("as_of")
103
+ retrieved_at = result.get("retrieved_at")
104
+ if as_of:
105
+ t.add_row("数据时点", str(as_of))
106
+ elif retrieved_at:
107
+ t.add_row("获取时间", f"{retrieved_at}(源未提供成交时点)")
108
+ if provider:
109
+ t.add_row("数据来源", str(provider))
110
+ # Technical indicators from local tool
111
+ _rsi = result.get("rsi")
112
+ if _rsi is not None:
113
+ _rsi_color = "red" if _rsi >= 70 else ("cyan" if _rsi <= 30 else "white")
114
+ t.add_row("RSI(14)", f"[{_rsi_color}]{_rsi:.1f}[/{_rsi_color}]")
115
+ _mh = result.get("macd_hist")
116
+ if _mh is not None:
117
+ _mh_color = "green" if _mh > 0 else "red"
118
+ t.add_row("MACD hist", f"[{_mh_color}]{_mh:+.4f}[/{_mh_color}]")
119
+ _ma20 = result.get("ma20"); _ma60 = result.get("ma60")
120
+ if _ma20:
121
+ t.add_row("MA20", f"{_ma20:,.4g}")
122
+ if _ma60:
123
+ t.add_row("MA60", f"{_ma60:,.4g}")
124
+ # Legacy cloud fields
125
+ for k in ("high_52w", "low_52w", "bid", "ask"):
126
+ v = result.get(k)
127
+ if v is not None:
128
+ t.add_row(k.replace("_", " ").title(), f"{v:,.4g}")
129
+ console.print(t)
130
+ if prov_tag:
131
+ console.print(f" {prov_tag}")
132
+ else:
133
+ print(f" {sym}: {px} ({chg:+.2f}%)")
134
+ return
135
+
136
+ # ── Market history (OHLC summary + recent candles) ──────────────────
137
+ if tool_name == "get_market_history":
138
+ sym = result.get("symbol", "")
139
+ name = result.get("name", "")
140
+ s = result.get("summary", {}) or {}
141
+ chg = s.get("change_pct") or 0
142
+ color = "green" if chg >= 0 else "red"
143
+ arrow = "▲" if chg >= 0 else "▼"
144
+ candles = result.get("recent_candles", []) or []
145
+ closes = [c.get("close") for c in candles if c.get("close") is not None]
146
+ # Unicode sparkline of the recent closes
147
+ spark = ""
148
+ if len(closes) >= 2:
149
+ blocks = "▁▂▃▄▅▆▇█"
150
+ lo, hi = min(closes), max(closes)
151
+ rng = (hi - lo) or 1
152
+ spark = "".join(blocks[min(7, int((c - lo) / rng * 7))] for c in closes)
153
+ if has_rich:
154
+ from rich.table import Table
155
+ t = Table(show_header=False, box=None, padding=(0, 1))
156
+ t.add_column(style="dim", width=20)
157
+ t.add_column()
158
+ title_str = f"[bold]{sym}[/bold]" + (f" {name}" if name and name != sym else "")
159
+ t.add_row("标的", title_str)
160
+ interval = result.get("interval", "1d")
161
+ n = result.get("total_points", len(candles))
162
+ t.add_row("区间", f"{s.get('start_date','')} → {s.get('end_date','')} ({n} 根 · {interval})")
163
+ sc, ec = s.get("start_close"), s.get("end_close")
164
+ if sc is not None and ec is not None:
165
+ t.add_row("期间涨跌", f"{sc:,.4g} → {ec:,.4g} [{color}]{arrow} {chg:+.2f}%[/{color}]")
166
+ ph, pl = s.get("period_high"), s.get("period_low")
167
+ if ph is not None and pl is not None:
168
+ t.add_row("期间高低", f"{pl:,.4g} — {ph:,.4g}")
169
+ ma_parts = []
170
+ for label, key in (("MA5", "ma5"), ("MA20", "ma20"), ("MA60", "ma60")):
171
+ v = s.get(key)
172
+ if v is not None:
173
+ ma_parts.append(f"{label} {v:,.4g}")
174
+ if ma_parts:
175
+ t.add_row("均线", " ".join(ma_parts))
176
+ av = s.get("avg_volume")
177
+ if av:
178
+ t.add_row("平均成交量", f"{int(av):,}")
179
+ if spark:
180
+ t.add_row(f"近{len(closes)}日走势", f"[{color}]{spark}[/{color}]")
181
+ console.print(t)
182
+ if prov_tag:
183
+ console.print(f" {prov_tag}")
184
+ else:
185
+ print(f" {sym} {s.get('start_date','')}→{s.get('end_date','')}: "
186
+ f"{s.get('end_close')} ({chg:+.2f}%)")
187
+ return
188
+
189
+ # ── Commodity data ─────────────────────────────────────────────────
190
+ if tool_name == "get_commodities_data":
191
+ sym = result.get("symbol", "")
192
+ px = result.get("latest_close", 0)
193
+ chg = result.get("change_pct", 0) or 0
194
+ unit = result.get("unit", "")
195
+ color = "green" if chg >= 0 else "red"
196
+ arrow = "▲" if chg >= 0 else "▼"
197
+ if has_rich:
198
+ console.print(
199
+ f" [bold]{sym}[/bold] {px:,.3g} {unit} "
200
+ f"[{color}]{arrow} {chg:+.3f}%[/{color}]{prov_tag}"
201
+ )
202
+ for k in ("pct_from_52w_high", "pct_from_52w_low", "year_return"):
203
+ v = result.get(k)
204
+ if v is not None:
205
+ console.print(f" [dim]{k:<25s}[/dim] {v:+.3%}")
206
+ else:
207
+ print(f" {sym}: {px} ({chg:+.3f}%)")
208
+ return
209
+
210
+ # ── AI signal ──────────────────────────────────────────────────────
211
+ if tool_name == "get_ai_signal":
212
+ action = result.get("action", "HOLD")
213
+ conf = result.get("confidence", 0)
214
+ reason = result.get("reasoning", "")
215
+ sl = result.get("stop_loss")
216
+ tp = result.get("take_profit")
217
+ color = {"BUY": "green", "SELL": "red", "HOLD": "yellow"}.get(action, "white")
218
+ if has_rich:
219
+ console.print(f" Signal: [{color}][bold]{action}[/bold][/{color}] "
220
+ f"Confidence: [bold]{conf:.1%}[/bold]{prov_tag}")
221
+ if reason:
222
+ console.print(f" [dim]{reason[:120]}[/dim]")
223
+ if sl is not None:
224
+ console.print(f" [dim]Stop-loss: {sl} Take-profit: {tp}[/dim]")
225
+ else:
226
+ print(f" {action} ({conf:.1%}) — {reason[:80]}")
227
+ return
228
+
229
+ # ── Factors ────────────────────────────────────────────────────────
230
+ if tool_name == "calculate_factors":
231
+ sym = result.get("symbol", "")
232
+ if has_rich:
233
+ from rich.table import Table
234
+ t = Table(title=f"Factors — {sym}", show_header=True, box=None, padding=(0, 1))
235
+ t.add_column("Factor", style="dim", width=24)
236
+ t.add_column("Value", justify="right")
237
+ t.add_column("Signal", width=6)
238
+ def _sig(v, neutral_lo=-0.1, neutral_hi=0.1):
239
+ if v is None: return ""
240
+ return "[green]▲[/green]" if v > neutral_hi else "[red]▼[/red]" if v < neutral_lo else "[yellow]─[/yellow]"
241
+ FACTOR_ROWS = [
242
+ ("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]"),
243
+ ("macd_hist", "MACD Hist", lambda v: "[green]▲[/green]" if v and v > 0 else "[red]▼[/red]"),
244
+ ("trend_score", "Trend Score", lambda v: _sig(v, -0.2, 0.2)),
245
+ ("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]"),
246
+ ("ma_20_gap", "vs MA20", lambda v: "[green]▲[/green]" if v and v > 0 else "[red]▼[/red]"),
247
+ ("ma_60_gap", "vs MA60", lambda v: "[green]▲[/green]" if v and v > 0 else "[red]▼[/red]"),
248
+ ("volatility_20d", "Vol(20d)", lambda v: ""),
249
+ ("volume_ratio_20d","Vol Ratio", lambda v: "[green]⬆[/green]" if v and v > 1.5 else ""),
250
+ ("return_5d", "Return 5d", lambda v: "[green]▲[/green]" if v and v > 0 else "[red]▼[/red]"),
251
+ ("return_20d", "Return 20d", lambda v: "[green]▲[/green]" if v and v > 0 else "[red]▼[/red]"),
252
+ ]
253
+ for key, label, sig_fn in FACTOR_ROWS:
254
+ v = result.get(key)
255
+ if v is not None:
256
+ val_str = f"{v:+.4f}" if isinstance(v, float) else str(v)
257
+ t.add_row(label, val_str, sig_fn(v))
258
+ console.print(t)
259
+ console.print(f" {prov_tag}")
260
+ else:
261
+ for k, v in result.items():
262
+ if k not in ("success", "symbol", "provider") and isinstance(v, (int, float)):
263
+ print(f" {k:<25s} {v:.5g}")
264
+ return
265
+
266
+ # ── Portfolio backtest (event-driven, multi-asset) ──────────────────
267
+ if tool_name == "run_portfolio_backtest":
268
+ syms = ", ".join(result.get("symbols", []) or [])
269
+ strat = result.get("strategy", "")
270
+ m = result.get("metrics", {}) or {}
271
+ bench = (result.get("benchmark", {}) or {}).get("total_return")
272
+ alpha = result.get("alpha_vs_buyhold")
273
+ if has_rich:
274
+ from rich.table import Table
275
+ t = Table(title=f"Portfolio Backtest — {syms} [{strat}]", show_header=True, box=None)
276
+ t.add_column("Metric", style="dim", width=24)
277
+ t.add_column("Value", justify="right")
278
+ ROWS = [
279
+ ("total_return", "Total Return", lambda v: f"[{'green' if v >= 0 else 'red'}]{v:+.2%}[/]"),
280
+ ("annual_return", "Annual Return", lambda v: f"[{'green' if v >= 0 else 'red'}]{v:+.2%}[/]"),
281
+ ("sharpe", "Sharpe", lambda v: f"[{'green' if v >= 1 else 'yellow' if v >= 0.5 else 'red'}]{v:.3f}[/]"),
282
+ ("max_drawdown", "Max Drawdown", lambda v: f"[red]{v:.2%}[/red]"),
283
+ ("win_rate", "Win Rate", lambda v: f"{v:.1%}"),
284
+ ("n_trades", "Trades", lambda v: str(int(v))),
285
+ ]
286
+ for key, label, fmt_fn in ROWS:
287
+ v = m.get(key)
288
+ if v is not None:
289
+ t.add_row(label, fmt_fn(v))
290
+ if bench is not None:
291
+ t.add_row("Benchmark (B&H)", f"{bench:+.2%}")
292
+ if alpha is not None:
293
+ t.add_row("Alpha vs B&H", f"[{'green' if alpha >= 0 else 'red'}]{alpha:+.2%}[/]")
294
+ console.print(t)
295
+ miss = result.get("missing") or []
296
+ if miss:
297
+ console.print(f" [yellow]未获取到数据: {', '.join(miss)}[/yellow]")
298
+ console.print(f" [dim]{result.get('n_points', 0)} bars[/dim]{prov_tag}")
299
+ else:
300
+ print(f" {syms} [{strat}]: return={m.get('total_return')} sharpe={m.get('sharpe')} "
301
+ f"mdd={m.get('max_drawdown')} trades={m.get('n_trades')}")
302
+ return
303
+
304
+ # ── Backtest ───────────────────────────────────────────────────────
305
+ if tool_name in ("backtest_strategy", "cloud_backtest"):
306
+ sym = result.get("symbol", result.get("symbols", ""))
307
+ strat = result.get("strategy", result.get("model_type", ""))
308
+ if has_rich:
309
+ from rich.table import Table
310
+ t = Table(title=f"Backtest — {sym} [{strat}]", show_header=True, box=None)
311
+ t.add_column("Metric", style="dim", width=24)
312
+ t.add_column("Value", justify="right")
313
+ PERF_ROWS = [
314
+ ("total_return", "Total Return", lambda v: f"[{'green' if v >= 0 else 'red'}]{v:+.2%}[/]"),
315
+ ("annual_return", "Annual Return", lambda v: f"[{'green' if v >= 0 else 'red'}]{v:+.2%}[/]"),
316
+ ("sharpe_ratio", "Sharpe Ratio", lambda v: f"[{'green' if v >= 1 else 'yellow' if v >= 0.5 else 'red'}]{v:.3f}[/]"),
317
+ ("sortino_ratio", "Sortino Ratio", lambda v: f"{v:.3f}"),
318
+ ("max_drawdown", "Max Drawdown", lambda v: f"[red]{v:.2%}[/red]"),
319
+ ("win_rate", "Win Rate", lambda v: f"{v:.1%}"),
320
+ ("total_trades", "Trades", lambda v: str(int(v))),
321
+ ("benchmark_return","Benchmark (B&H)", lambda v: f"{v:+.2%}"),
322
+ ("alpha", "Alpha", lambda v: f"[{'green' if v >= 0 else 'red'}]{v:+.2%}[/]"),
323
+ ]
324
+ for key, label, fmt_fn in PERF_ROWS:
325
+ v = result.get(key)
326
+ if v is not None:
327
+ t.add_row(label, fmt_fn(v))
328
+ console.print(t)
329
+ console.print(f" {result.get('start', '')} → {result.get('end', '')} "
330
+ f"[dim]{result.get('bars', '')} bars[/dim]{prov_tag}")
331
+ else:
332
+ for k in ("total_return", "sharpe_ratio", "max_drawdown", "win_rate"):
333
+ v = result.get(k)
334
+ if v is not None:
335
+ print(f" {k:<20s} {v:.4g}")
336
+ return
337
+
338
+ # ── Predictions ────────────────────────────────────────────────────
339
+ if tool_name == "get_predictions":
340
+ preds = result.get("predictions", [])
341
+ days = result.get("prediction_days", 5)
342
+ if has_rich and preds:
343
+ from rich.table import Table
344
+ t = Table(title=f"ML Predictions ({days}d)", show_header=True, box=None)
345
+ t.add_column("Symbol", style="bold", width=12)
346
+ t.add_column("Predicted Return", justify="right")
347
+ t.add_column("Confidence", justify="right")
348
+ for p in preds:
349
+ ret = p.get("predicted_return", 0)
350
+ conf = p.get("confidence", 0)
351
+ color = "green" if ret >= 0 else "red"
352
+ t.add_row(p["symbol"], f"[{color}]{ret:+.2%}[/{color}]", f"{conf:.0%}")
353
+ console.print(t)
354
+ console.print(f" {prov_tag}")
355
+ else:
356
+ for p in preds:
357
+ print(f" {p.get('symbol')}: {p.get('predicted_return',0):+.2%}")
358
+ return
359
+
360
+ # ── Northbound flow ────────────────────────────────────────────────
361
+ if tool_name == "get_northbound_flow":
362
+ latest = result.get("latest_net_buy_yi", 0)
363
+ total = result.get("total_net_buy_yi", 0)
364
+ trend = result.get("trend", "")
365
+ color = "green" if latest >= 0 else "red"
366
+ if has_rich:
367
+ console.print(f" 北向资金 Today: [{color}][bold]{latest:+.2f}亿[/bold][/{color}] "
368
+ f"Period Total: {total:+.2f}亿 [{trend}]{prov_tag}")
369
+ else:
370
+ print(f" 北向 Today: {latest:+.2f}亿 Period: {total:+.2f}亿")
371
+ return
372
+
373
+ # ── Market indices ────────────────────────────────────────────────
374
+ if tool_name == "get_market_indices":
375
+ indices = result.get("indices", result)
376
+ if has_rich:
377
+ from rich.table import Table
378
+ t = Table(title="全球市场指数", show_header=True, box=None, padding=(0, 1))
379
+ t.add_column("指数", style="bold", width=16)
380
+ t.add_column("最新价", justify="right")
381
+ t.add_column("涨跌", justify="right")
382
+ # Handles both list-of-dicts (yfinance) and dict-of-dicts (legacy) formats
383
+ rows = (
384
+ [(d.get("name",""), d) for d in indices]
385
+ if isinstance(indices, list)
386
+ else [(k, v) for k, v in indices.items() if isinstance(v, dict)]
387
+ )
388
+ for name, d in rows:
389
+ px = d.get("price", d.get("latest_close", d.get("close", 0))) or 0
390
+ chg = d.get("change_pct", d.get("changePercent", 0)) or 0
391
+ color = "green" if chg >= 0 else "red"
392
+ t.add_row(name or d.get("ticker",""), f"{px:,.2f}",
393
+ f"[{color}]{chg:+.2f}%[/{color}]")
394
+ console.print(t)
395
+ console.print(f" [dim]{result.get('date','')} {prov_tag}[/dim]")
396
+ else:
397
+ rows = indices if isinstance(indices, list) else list(indices.values())
398
+ for d in rows[:10]:
399
+ nm = d.get("name", d.get("ticker", ""))
400
+ print(f" {nm:<16} {d.get('price',0):>10,.2f} {d.get('change_pct',0):+.2f}%")
401
+ return
402
+
403
+ # ── Risk metrics ──────────────────────────────────────────────────
404
+ if tool_name == "get_risk_metrics":
405
+ sym = result.get("symbol", "")
406
+ if has_rich:
407
+ from rich.table import Table
408
+ t = Table(title=f"[bold]{sym}[/bold] 风险指标", show_header=False, box=None, padding=(0,1))
409
+ t.add_column(style="dim", width=22)
410
+ t.add_column(justify="right")
411
+ conf = result.get("confidence_level", 0.95)
412
+ rows_r = [
413
+ ("年化波动率", f"{result.get('annual_volatility',0):.2%}"),
414
+ ("年化收益率", f"{result.get('annual_return',0):+.2%}"),
415
+ (f"VaR({conf:.0%}) 日", f"[red]{result.get('var_daily',0):.2%}[/red]"),
416
+ (f"VaR({conf:.0%}) 月", f"[red]{result.get('var_monthly',0):.2%}[/red]"),
417
+ ("CVaR 日", f"[red]{result.get('cvar_daily',0):.2%}[/red]"),
418
+ ("最大回撤", f"[red]{result.get('max_drawdown',0):.2%}[/red]"),
419
+ ("Sharpe Ratio", f"{result.get('sharpe_ratio',0):.3f}"),
420
+ ("Calmar Ratio", f"{result.get('calmar_ratio',0):.3f}"),
421
+ ("偏度", f"{result.get('skewness',0):.3f}"),
422
+ ("峰度", f"{result.get('kurtosis',0):.3f}"),
423
+ ]
424
+ for label, val in rows_r:
425
+ t.add_row(label, val)
426
+ console.print(t)
427
+ console.print(f" [dim]{prov_tag}[/dim]")
428
+ else:
429
+ for k in ("annual_volatility","var_daily","max_drawdown","sharpe_ratio"):
430
+ print(f" {k:<25} {result.get(k,0):.4g}")
431
+ return
432
+
433
+ # ── optimize_positions ────────────────────────────────────────────
434
+ if tool_name == "optimize_positions":
435
+ weights = result.get("weights", {})
436
+ method = result.get("method", "max_sharpe")
437
+ if has_rich:
438
+ from rich.table import Table
439
+ t = Table(title=f"组合优化 [{method}]", show_header=True, box=None, padding=(0,1))
440
+ t.add_column("代码", style="bold")
441
+ t.add_column("权重", justify="right")
442
+ t.add_column("占比", justify="right")
443
+ for sym_k, w in sorted(weights.items(), key=lambda x: -x[1]):
444
+ bar = "█" * int(w * 20)
445
+ t.add_row(sym_k, f"{w:.2%}", f"[cyan]{bar}[/cyan]")
446
+ console.print(t)
447
+ p_ret = result.get("portfolio_return", 0)
448
+ p_vol = result.get("portfolio_vol", 0)
449
+ sr = result.get("sharpe_ratio", 0)
450
+ console.print(
451
+ f" 期望收益 [bold]{p_ret:+.2%}[/bold] "
452
+ f"波动率 {p_vol:.2%} "
453
+ f"Sharpe [bold]{sr:.3f}[/bold] [dim]{prov_tag}[/dim]"
454
+ )
455
+ else:
456
+ for sym_k, w in weights.items():
457
+ print(f" {sym_k:<10} {w:.2%}")
458
+ return
459
+
460
+ # ── get_sector_performance ────────────────────────────────────────
461
+ if tool_name == "get_sector_performance":
462
+ top = result.get("top_sectors", [])
463
+ bottom = result.get("bottom_sectors", [])
464
+ all_s = result.get("sectors", top + bottom)
465
+ mkt = result.get("market", "")
466
+ if has_rich:
467
+ from rich.table import Table
468
+ t = Table(title=f"板块表现 [{mkt.upper()}]", show_header=True, box=None, padding=(0,1))
469
+ t.add_column("板块", style="bold", min_width=14)
470
+ t.add_column("涨跌", justify="right")
471
+ for row in sorted(all_s, key=lambda x: -(x.get("change_pct") or 0)):
472
+ chg = row.get("change_pct") or 0
473
+ color = "green" if chg >= 0 else "red"
474
+ t.add_row(row.get("sector",""), f"[{color}]{chg:+.2f}%[/{color}]")
475
+ console.print(t)
476
+ console.print(f" [dim]{result.get('date','')} {prov_tag}[/dim]")
477
+ else:
478
+ for row in sorted(all_s, key=lambda x: -(x.get("change_pct") or 0))[:10]:
479
+ print(f" {row.get('sector',''):<16} {row.get('change_pct',0):+.2f}%")
480
+ return
481
+
482
+ # ── screen_ashare ─────────────────────────────────────────────────
483
+ if tool_name == "screen_ashare":
484
+ stocks = result.get("stocks", [])
485
+ count = result.get("count", len(stocks))
486
+ if has_rich:
487
+ from rich.markup import escape
488
+ from rich.table import Table
489
+ from ui.render.responsive import StackedRecord, render_stacked_records, structured_layout
490
+
491
+ layout = structured_layout(console)
492
+ if layout == "stacked":
493
+ records = []
494
+ for stock in stocks[:30]:
495
+ change = stock.get("change_pct") or 0
496
+ color = "green" if change >= 0 else "red"
497
+ pe = f"{stock.get('pe_dynamic', 0):.1f}" if stock.get("pe_dynamic") else "—"
498
+ market_cap = stock.get("market_cap_yi", (stock.get("market_cap") or 0) / 1e8)
499
+ records.append(StackedRecord(
500
+ headline=(
501
+ f"{escape(str(stock.get('code', '')))} "
502
+ f"{escape(str(stock.get('name', '')))}"
503
+ ),
504
+ lines=(
505
+ f"[dim]价格[/dim] {stock.get('price', 0):.2f} · "
506
+ f"[dim]涨跌[/dim] [{color}]{change:+.2f}%[/{color}]",
507
+ f"[dim]PE[/dim] {pe} · [dim]市值[/dim] {market_cap:.0f} 亿",
508
+ ),
509
+ ))
510
+ render_stacked_records(
511
+ console,
512
+ title=f"A股筛选 · 共 {count} 只",
513
+ records=records,
514
+ footer=str(prov_tag).strip(),
515
+ )
516
+ return
517
+ t = Table(title=f"A股筛选 共 {count} 只", show_header=True, box=None, padding=(0,1))
518
+ t.add_column("代码", style="bold", width=8)
519
+ t.add_column("名称", width=10)
520
+ t.add_column("价格", justify="right")
521
+ t.add_column("涨跌%", justify="right")
522
+ t.add_column("PE", justify="right", style="dim")
523
+ t.add_column("市值(亿)",justify="right", style="dim")
524
+ for s in stocks[:30]:
525
+ chg = s.get("change_pct") or 0
526
+ color = "green" if chg >= 0 else "red"
527
+ pe = f"{s.get('pe_dynamic',0):.1f}" if s.get("pe_dynamic") else "—"
528
+ mc = f"{s.get('market_cap_yi', (s.get('market_cap') or 0)/1e8):.0f}"
529
+ t.add_row(
530
+ str(s.get("code","")), str(s.get("name",""))[:10],
531
+ f"{s.get('price',0):.2f}",
532
+ f"[{color}]{chg:+.2f}%[/{color}]",
533
+ pe, mc,
534
+ )
535
+ console.print(t)
536
+ console.print(f" [dim]{prov_tag}[/dim]")
537
+ else:
538
+ for s in stocks[:20]:
539
+ print(f" {s.get('code','')} {s.get('name','')} {s.get('change_pct',0):+.2f}%")
540
+ return
541
+
542
+ # ── get_limit_up_pool ─────────────────────────────────────────────
543
+ if tool_name == "get_limit_up_pool":
544
+ stocks = result.get("stocks", [])
545
+ count = result.get("count", len(stocks))
546
+ date_s = result.get("date", "")
547
+ if has_rich:
548
+ from rich.markup import escape
549
+ from rich.table import Table
550
+ from ui.render.responsive import StackedRecord, render_stacked_records, structured_layout
551
+
552
+ if structured_layout(console) == "stacked":
553
+ records = []
554
+ for stock in stocks[:30]:
555
+ streak = stock.get("consecutive") or stock.get("limit_streak") or "—"
556
+ records.append(StackedRecord(
557
+ headline=(
558
+ f"{escape(str(stock.get('code', '')))} "
559
+ f"{escape(str(stock.get('name', '')))}"
560
+ ),
561
+ lines=(
562
+ f"[dim]连板[/dim] {escape(str(streak))} · "
563
+ f"[dim]类型[/dim] {escape(str(stock.get('limit_type') or '—'))}",
564
+ f"[dim]首封时间[/dim] {escape(str(stock.get('first_lock_time') or '—'))}",
565
+ ),
566
+ ))
567
+ render_stacked_records(
568
+ console,
569
+ title=f"涨停板池 · {escape(str(date_s))} · 共 {count} 只",
570
+ records=records,
571
+ footer=str(prov_tag).strip(),
572
+ )
573
+ return
574
+ t = Table(title=f"涨停板池 {date_s} 共 {count} 只",
575
+ show_header=True, box=None, padding=(0,1))
576
+ t.add_column("代码", style="bold", width=8)
577
+ t.add_column("名称", width=10)
578
+ t.add_column("连板", justify="right")
579
+ t.add_column("首封时间", style="dim")
580
+ t.add_column("类型", style="dim")
581
+ for s in stocks[:30]:
582
+ consec = s.get("consecutive") or s.get("limit_streak") or ""
583
+ t.add_row(
584
+ str(s.get("code","")), str(s.get("name",""))[:10],
585
+ str(consec), str(s.get("first_lock_time",""))[:8],
586
+ str(s.get("limit_type",""))[:6],
587
+ )
588
+ console.print(t)
589
+ console.print(f" [dim]{prov_tag}[/dim]")
590
+ else:
591
+ for s in stocks[:20]:
592
+ print(f" {s.get('code','')} {s.get('name','')} 连板:{s.get('consecutive','')}")
593
+ return
594
+
595
+ # ── get_futures_data / get_bonds_data ─────────────────────────────
596
+ if tool_name == "get_futures_data":
597
+ sym = result.get("symbol", result.get("ticker", ""))
598
+ price = result.get("price", result.get("current_price", 0)) or 0
599
+ chg = result.get("change_pct", result.get("changePercent", 0)) or 0
600
+ vol = result.get("volume", 0)
601
+ if has_rich:
602
+ color = "green" if chg >= 0 else "red"
603
+ console.print(
604
+ f" [bold]{sym}[/bold] {price:,.2f} "
605
+ f"[{color}]{chg:+.2f}%[/{color}]"
606
+ + (f" vol {vol:,.0f}" if vol else "")
607
+ + f" [dim]{prov_tag}[/dim]"
608
+ )
609
+ else:
610
+ print(f" {sym} {price} {chg:+.2f}%")
611
+ return
612
+
613
+ if tool_name == "get_bonds_data":
614
+ yields = result.get("yields", {})
615
+ spread = yields.get("10Y_2Y_spread")
616
+ curve = yields.get("curve_shape", "")
617
+ if has_rich:
618
+ from rich.table import Table
619
+ t = Table(title="美国国债收益率", show_header=False, box=None, padding=(0,1))
620
+ t.add_column(style="dim", width=6)
621
+ t.add_column(justify="right")
622
+ for tenor in ("2Y", "5Y", "10Y", "30Y"):
623
+ y = yields.get(tenor)
624
+ if y is not None:
625
+ t.add_row(tenor, f"[bold]{y:.3f}%[/bold]")
626
+ console.print(t)
627
+ if spread is not None:
628
+ color = "green" if spread >= 0 else "red"
629
+ console.print(
630
+ f" 10Y-2Y spread: [{color}]{spread:+.3f}%[/{color}] "
631
+ f"[dim]{curve} {prov_tag}[/dim]"
632
+ )
633
+ else:
634
+ for tenor, y in yields.items():
635
+ if isinstance(y, float):
636
+ print(f" {tenor}: {y:.3f}%")
637
+ return
638
+
639
+ # ── get_market_insights ───────────────────────────────────────────
640
+ if tool_name == "get_market_insights":
641
+ summaries = result.get("summaries", [])
642
+ note = result.get("note", "")
643
+ if has_rich:
644
+ from rich.table import Table
645
+ t = Table(title="市场洞察", show_header=True, box=None, padding=(0,1))
646
+ t.add_column("代码", style="bold", width=10)
647
+ t.add_column("RSI", justify="right")
648
+ t.add_column("MACD Hist", justify="right")
649
+ t.add_column("量比", justify="right")
650
+ t.add_column("趋势", justify="right")
651
+ for s in summaries:
652
+ rsi_v = s.get("rsi_14") or 0
653
+ mh = s.get("macd_hist") or 0
654
+ tr = s.get("trend_score") or 0
655
+ vr = s.get("vol_ratio") or 1.0
656
+ rsi_c = "red" if rsi_v > 70 else "green" if rsi_v < 30 else ""
657
+ mh_c = "green" if mh > 0 else "red"
658
+ t.add_row(
659
+ s.get("symbol",""),
660
+ f"[{rsi_c}]{rsi_v:.1f}[/{rsi_c}]" if rsi_c else f"{rsi_v:.1f}",
661
+ f"[{mh_c}]{mh:+.4f}[/{mh_c}]",
662
+ f"{vr:.2f}x",
663
+ f"{'↑' if tr > 0 else '↓' if tr < 0 else '→'} {tr:.2f}" if tr else "—",
664
+ )
665
+ console.print(t)
666
+ if note:
667
+ console.print(f" [dim]{note}[/dim]")
668
+ console.print(f" [dim]{prov_tag}[/dim]")
669
+ else:
670
+ for s in summaries:
671
+ print(f" {s.get('symbol','')} RSI:{s.get('rsi_14','')} MACD:{s.get('macd_hist','')}")
672
+ return
673
+
674
+ # ── calculate_factors ─────────────────────────────────────────────
675
+ if tool_name == "calculate_factors":
676
+ sym = result.get("symbol", "")
677
+ if has_rich:
678
+ from rich.table import Table
679
+ t = Table(title=f"[bold]{sym}[/bold] 因子分析", show_header=False, box=None, padding=(0,1))
680
+ t.add_column(style="dim", width=22)
681
+ t.add_column(justify="right")
682
+ _FACTOR_LABELS = {
683
+ "rsi_14": "RSI(14)",
684
+ "macd_hist": "MACD Hist",
685
+ "trend_score": "趋势评分",
686
+ "volume_ratio_20d": "量比(20d)",
687
+ "volatility_20d": "波动率(20d)",
688
+ "bb_position": "布林带位置",
689
+ "return_5d": "5日收益",
690
+ "return_20d": "20日收益",
691
+ "return_60d": "60日收益",
692
+ }
693
+ for key, label in _FACTOR_LABELS.items():
694
+ val = result.get(key)
695
+ if val is None:
696
+ continue
697
+ if key in ("return_5d","return_20d","return_60d","volatility_20d"):
698
+ color = "green" if val > 0 else "red"
699
+ t.add_row(label, f"[{color}]{val:+.2%}[/{color}]")
700
+ elif key == "rsi_14":
701
+ color = "red" if val > 70 else "green" if val < 30 else ""
702
+ t.add_row(label, f"[{color}]{val:.1f}[/{color}]" if color else f"{val:.1f}")
703
+ elif key == "macd_hist":
704
+ color = "green" if val > 0 else "red"
705
+ t.add_row(label, f"[{color}]{val:+.4f}[/{color}]")
706
+ elif key == "bb_position":
707
+ t.add_row(label, f"{val:.1%} {'超买区' if val > 0.8 else '超卖区' if val < 0.2 else '中间带'}")
708
+ else:
709
+ t.add_row(label, f"{val:.4g}")
710
+ console.print(t)
711
+ console.print(f" [dim]{prov_tag}[/dim]")
712
+ else:
713
+ for k in ("rsi_14","macd_hist","trend_score","return_20d"):
714
+ v = result.get(k)
715
+ if v is not None:
716
+ print(f" {k:<22} {v:.4g}")
717
+ return
718
+
719
+ # ── Generic fallback ──────────────────────────────────────────────
720
+ if has_rich:
721
+ # Show key=value pairs, skip large nested objects
722
+ out = Text()
723
+ for k, v in result.items():
724
+ if k in ("success", "provider", "history_tail", "equity_curve", "trades"):
725
+ continue
726
+ if isinstance(v, (int, float)):
727
+ color = "green" if v > 0 else "red" if v < 0 else ""
728
+ out.append(f" {k.replace('_',' ').title():<24s}", style="dim")
729
+ out.append(f"{v:,.5g}\n", style=color)
730
+ elif isinstance(v, str) and len(v) < 80:
731
+ out.append(f" {k.replace('_',' ').title():<24s}", style="dim")
732
+ out.append(f"{v}\n")
733
+ if str(out):
734
+ console.print(out)
735
+ if provider:
736
+ console.print(f" {prov_tag}")
737
+ else:
738
+ print(json.dumps({k: v for k, v in result.items()
739
+ if k not in ("success",) and not isinstance(v, list)},
740
+ indent=2, ensure_ascii=False, default=str)[:400])
741
+
742
+ # ── Broker query results ───────────────────────────────────────────
743
+ if tool_name in ("broker_query", "broker_order"):
744
+ query = result.get("query", "")
745
+ broker = result.get("broker", "券商")
746
+
747
+ if query == "account":
748
+ currency = result.get("currency", "CNY")
749
+ total = result.get("total_assets", 0)
750
+ cash = result.get("cash", 0)
751
+ mv = result.get("market_value", 0)
752
+ pnl_day = result.get("pnl_today", 0)
753
+ pnl_tot = result.get("pnl_total", 0)
754
+ if has_rich:
755
+ pday_c = "green" if pnl_day >= 0 else "red"
756
+ ptot_c = "green" if pnl_tot >= 0 else "red"
757
+ from rich.table import Table
758
+ t = Table(show_header=False, box=None, padding=(0, 1))
759
+ t.add_column(style="dim", width=16)
760
+ t.add_column()
761
+ t.add_row("账户", f"[bold]{broker}[/bold] [dim]{result.get('account_id','****')}[/dim]")
762
+ t.add_row("总资产", f"[bold]{currency} {total:,.2f}[/bold]")
763
+ t.add_row("持仓市值", f"{mv:,.2f}")
764
+ t.add_row("可用现金", f"{cash:,.2f}")
765
+ if pnl_day:
766
+ t.add_row("当日盈亏", f"[{pday_c}]{pnl_day:+,.2f}[/{pday_c}]")
767
+ if pnl_tot:
768
+ t.add_row("累计盈亏", f"[{ptot_c}]{pnl_tot:+,.2f}[/{ptot_c}]")
769
+ console.print(t)
770
+ else:
771
+ print(f"{broker}: 总资产 {total:,.2f} 可用 {cash:,.2f}")
772
+ return
773
+
774
+ if query == "positions":
775
+ positions = result.get("positions", [])
776
+ if not positions:
777
+ if has_rich:
778
+ console.print(f"[dim]{broker} — 当前无持仓[/dim]")
779
+ return
780
+ if has_rich:
781
+ from rich.markup import escape
782
+ from rich.table import Table
783
+ from ui.render.responsive import StackedRecord, render_stacked_records, structured_layout
784
+
785
+ ordered = sorted(positions, key=lambda x: -abs(x.get("market_value", 0)))
786
+ total_mv = sum(p.get("market_value", 0) for p in positions)
787
+ total_pnl = sum(p.get("pnl", 0) for p in positions)
788
+ total_color = "green" if total_pnl >= 0 else "red"
789
+ layout = structured_layout(console)
790
+ if layout == "stacked":
791
+ records = []
792
+ for p in ordered:
793
+ pnl = p.get("pnl", 0)
794
+ pct = p.get("pnl_pct", 0)
795
+ color = "green" if pnl >= 0 else "red"
796
+ records.append(StackedRecord(
797
+ headline=(
798
+ f"{escape(str(p.get('symbol', '')))} "
799
+ f"{escape(str(p.get('name') or '—'))}"
800
+ ),
801
+ lines=(
802
+ f"[dim]持仓[/dim] {p.get('quantity', 0):,.0f}",
803
+ f"[dim]成本[/dim] {p.get('cost', 0):.3f} · "
804
+ f"[dim]现价[/dim] {p.get('price', 0):.3f}",
805
+ f"[dim]市值[/dim] {p.get('market_value', 0):,.2f} · "
806
+ f"[dim]盈亏[/dim] [{color}]{pnl:+,.2f} ({pct:+.2f}%)[/{color}]",
807
+ ),
808
+ ))
809
+ render_stacked_records(
810
+ console,
811
+ title=f"{escape(str(broker))} 持仓",
812
+ records=records,
813
+ footer=(
814
+ f"共 {len(positions)} 只 · 总市值 {total_mv:,.2f} · "
815
+ f"总盈亏 [{total_color}]{total_pnl:+,.2f}[/{total_color}]"
816
+ ),
817
+ )
818
+ return
819
+ t = Table(title=f"[bold]{broker}[/bold] 持仓", show_header=True, header_style="bold")
820
+ t.add_column("代码", style="bold", no_wrap=True)
821
+ t.add_column("名称", max_width=10)
822
+ t.add_column("持仓", justify="right")
823
+ if layout == "full":
824
+ t.add_column("成本", justify="right", style="dim")
825
+ t.add_column("现价", justify="right")
826
+ t.add_column("市值", justify="right")
827
+ t.add_column("盈亏", justify="right")
828
+ t.add_column("盈亏%", justify="right")
829
+ for p in ordered:
830
+ pnl = p.get("pnl", 0)
831
+ pct = p.get("pnl_pct", 0)
832
+ c = "green" if pnl >= 0 else "red"
833
+ row = [
834
+ p.get("symbol", ""), p.get("name", "—")[:10],
835
+ f"{p.get('quantity', 0):,.0f}",
836
+ ]
837
+ if layout == "full":
838
+ row.extend([f"{p.get('cost', 0):.3f}", f"{p.get('price', 0):.3f}"])
839
+ row.extend([
840
+ f"{p.get('market_value',0):,.2f}",
841
+ f"[{c}]{pnl:+,.2f}[/{c}]",
842
+ f"[{c}]{pct:+.2f}%[/{c}]",
843
+ ])
844
+ t.add_row(*row)
845
+ console.print(t)
846
+ console.print(
847
+ f" [dim]{len(positions)} 只 市值 {total_mv:,.2f} "
848
+ f"总盈亏 [{total_color}]{total_pnl:+,.2f}[/{total_color}][/dim]"
849
+ )
850
+ else:
851
+ for p in positions:
852
+ print(f" {p.get('symbol',''):<8} {p.get('name',''):<10} 持仓:{p.get('quantity',0):.0f} 盈亏:{p.get('pnl',0):+,.2f}")
853
+ return
854
+
855
+ if query == "orders":
856
+ orders = result.get("orders", [])
857
+ if not orders:
858
+ if has_rich:
859
+ console.print(f"[dim]{broker} — 无订单记录[/dim]")
860
+ return
861
+ if has_rich:
862
+ from rich.markup import escape
863
+ from rich.table import Table
864
+ from ui.render.responsive import StackedRecord, render_stacked_records, structured_layout
865
+
866
+ layout = structured_layout(console)
867
+ _ss = {"filled":"[green]成交[/green]","partial":"[yellow]部成[/yellow]",
868
+ "open":"[cyan]委托中[/cyan]","cancelled":"[dim]已撤[/dim]"}
869
+ _sd = {"buy":"[green]买入[/green]","sell":"[red]卖出[/red]"}
870
+ if layout == "stacked":
871
+ records = []
872
+ for o in orders:
873
+ avg_price = f"{o.get('avg_price', 0):.3f}" if o.get("avg_price") else "—"
874
+ records.append(StackedRecord(
875
+ headline=f"{escape(str(o.get('symbol', '')))}",
876
+ lines=(
877
+ f"[dim]方向[/dim] {_sd.get(o.get('side', ''), escape(str(o.get('side', ''))))} · "
878
+ f"[dim]状态[/dim] {_ss.get(o.get('status', ''), escape(str(o.get('status', ''))))}",
879
+ f"[dim]委托[/dim] {o.get('quantity', 0):,.0f} @ {o.get('price', 0):.3f}",
880
+ f"[dim]成交[/dim] {o.get('filled', 0):,.0f} @ {avg_price}",
881
+ f"[dim]时间[/dim] {escape(str(o.get('time') or '—'))}",
882
+ ),
883
+ ))
884
+ render_stacked_records(
885
+ console,
886
+ title=f"{escape(str(broker))} 订单",
887
+ records=records,
888
+ footer=f"共 {len(orders)} 笔",
889
+ )
890
+ return
891
+ t = Table(title=f"[bold]{broker}[/bold] 订单", show_header=True, header_style="bold")
892
+ t.add_column("代码", style="bold")
893
+ t.add_column("方向", justify="center")
894
+ t.add_column("委托量", justify="right")
895
+ t.add_column("委托价", justify="right", style="dim")
896
+ t.add_column("状态")
897
+ t.add_column("时间", style="dim", max_width=14)
898
+ if layout == "full":
899
+ t.add_column("成交量", justify="right")
900
+ t.add_column("均价", justify="right")
901
+ for o in orders:
902
+ row = [
903
+ o.get("symbol",""),
904
+ _sd.get(o.get("side",""), o.get("side","")),
905
+ f"{o.get('quantity',0):,.0f}",
906
+ f"{o.get('price',0):.3f}",
907
+ _ss.get(o.get("status",""), o.get("status","")),
908
+ str(o.get("time",""))[:14],
909
+ ]
910
+ if layout == "full":
911
+ row.extend([
912
+ f"{o.get('filled', 0):,.0f}",
913
+ f"{o.get('avg_price', 0):.3f}" if o.get("avg_price") else "—",
914
+ ])
915
+ t.add_row(*row)
916
+ console.print(t)
917
+ else:
918
+ for o in orders:
919
+ print(f" {o.get('symbol','')} {o.get('side','')} {o.get('quantity',0):.0f} @ {o.get('price',0):.3f} [{o.get('status','')}]")
920
+ return
921
+
922
+ # ── broker_order: confirmation required ────────────────────────
923
+ if tool_name == "broker_order" and result.get("confirmation_required"):
924
+ preview = result.get("order_preview", {})
925
+ if has_rich:
926
+ from rich.panel import Panel
927
+ from rich import box as _rbox
928
+ _side_cn = preview.get("side_cn", preview.get("side", ""))
929
+ _side_color = "green" if preview.get("side") == "buy" else "red"
930
+ _preview_id = preview.get("preview_id") or result.get("preview_id") or ""
931
+ _mode = preview.get("mode") or (result.get("trade_preview") or {}).get("mode") or ""
932
+ _broker = preview.get("broker") or (result.get("trade_preview") or {}).get("broker_label") or ""
933
+ _blockers = (result.get("trade_preview") or {}).get("execution_blockers") or []
934
+ _body = (
935
+ f"preview_id: [bold]{_preview_id}[/bold]\n"
936
+ f"模式: [bold]{_mode or '—'}[/bold] 券商: [bold]{_broker or '—'}[/bold]\n\n"
937
+ f"[bold]{_side_cn}[/bold] "
938
+ f"[bold]{preview.get('symbol','')}[/bold] "
939
+ f"数量: [bold]{preview.get('qty', 0):,}[/bold] "
940
+ f"价格: [bold]{preview.get('price_display','市价')}[/bold]\n\n"
941
+ "[yellow]确认执行时必须携带 preview_id · 其他任何回复取消[/yellow]"
942
+ )
943
+ if _blockers:
944
+ _body += "\n\n[red]执行限制:[/red]\n" + "\n".join(f" - {b}" for b in _blockers)
945
+ console.print(Panel(
946
+ _body,
947
+ title="[yellow]⚠ 订单确认[/yellow]",
948
+ border_style="yellow",
949
+ box=_rbox.ROUNDED,
950
+ padding=(0, 1),
951
+ ))
952
+ else:
953
+ msg = result.get("message", "请确认订单")
954
+ print(f"\n⚠ 订单确认\n{msg}\n")
955
+ return
956
+
957
+ # ── broker_order: placed successfully ──────────────────────────
958
+ if tool_name == "broker_order" and result.get("success"):
959
+ if has_rich:
960
+ _side_cn = "买入" if result.get("side") == "buy" else "卖出"
961
+ console.print(
962
+ f"[green]✓ 订单已提交[/green] {result.get('broker','')} — "
963
+ f"{_side_cn} [bold]{result.get('symbol','')}[/bold] "
964
+ f"× {result.get('qty',0):,} "
965
+ f"[dim]#{result.get('order_id','—')} {result.get('status','')}[/dim]"
966
+ )
967
+ else:
968
+ print(f"✓ 订单已提交: {result}")
969
+ return
970
+
971
+
972
+ def render_macro_result(r: dict, title: str, *, console=None, has_rich: bool = True) -> None:
973
+ """Render US or CN macro result dict."""
974
+ if not r.get("success"):
975
+ if has_rich: console.print(f" [red]{r.get('error','数据获取失败')}[/red]")
976
+ else: print(f" {r.get('error','failed')}")
977
+ return
978
+ data = r.get("data", {})
979
+ if has_rich:
980
+ from rich.table import Table
981
+ from rich.rule import Rule
982
+ console.print(Rule(f"[bold]{title}[/bold]", style="dim"))
983
+ t = Table(show_header=True, box=None, padding=(0, 1))
984
+ t.add_column("指标", style="dim", min_width=20)
985
+ t.add_column("最新值", justify="right", min_width=10)
986
+ t.add_column("环比变化", justify="right")
987
+ t.add_column("时间", style="dim")
988
+ for key, item in data.items():
989
+ if key.startswith("_"): continue
990
+ if not isinstance(item, dict): continue
991
+ latest = item.get("latest", {}) or {}
992
+ val = latest.get("value")
993
+ date = latest.get("date", "")
994
+ change = item.get("change")
995
+ unit = item.get("unit", "")
996
+ label = item.get("label", key)
997
+ if val is None: continue
998
+ val_str = f"{val:.2f}{unit}"
999
+ if change is not None:
1000
+ color = "green" if change > 0 else "red" if change < 0 else ""
1001
+ chg_str = f"[{color}]{change:+.3f}[/{color}]" if color else f"{change:+.3f}"
1002
+ else:
1003
+ chg_str = "—"
1004
+ t.add_row(label, val_str, chg_str, str(date)[:10])
1005
+ console.print(t)
1006
+ yc = data.get("_yield_curve", {})
1007
+ if yc:
1008
+ sp = yc.get("spread_10y_2y", 0)
1009
+ shape = yc.get("shape", "")
1010
+ color = "green" if sp > 0 else "red"
1011
+ console.print(f" 收益率曲线: [{color}]{shape}[/{color}] 10Y-2Y利差: [{color}]{sp:+.3f}%[/{color}]")
1012
+ else:
1013
+ print(f"\n{title}")
1014
+ for key, item in data.items():
1015
+ if not isinstance(item, dict) or key.startswith("_"): continue
1016
+ v = (item.get("latest") or {}).get("value")
1017
+ if v is not None:
1018
+ print(f" {item.get('label',key):<28} {v:.3g}")
1019
+
1020
+
1021
+ def render_cb_rates(r: dict, *, console=None, has_rich: bool = True) -> None:
1022
+ """Render central bank rates."""
1023
+ if not r.get("success"):
1024
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
1025
+ return
1026
+ rates = r.get("rates", {})
1027
+ if has_rich:
1028
+ from rich.rule import Rule
1029
+ from rich.table import Table
1030
+ console.print(Rule("[bold]🏦 主要央行政策利率[/bold]", style="dim"))
1031
+ t = Table(show_header=False, box=None, padding=(0,1))
1032
+ t.add_column(style="dim", min_width=28)
1033
+ t.add_column(justify="right")
1034
+ for name, val in rates.items():
1035
+ if val is not None:
1036
+ t.add_row(name, f"[bold]{val:.2f}%[/bold]")
1037
+ console.print(t)
1038
+ else:
1039
+ print("\n央行利率")
1040
+ for name, val in rates.items():
1041
+ if val is not None:
1042
+ print(f" {name:<30} {val:.2f}%")
1043
+
1044
+
1045
+ def render_econ_calendar(r: dict, *, console=None, has_rich: bool = True) -> None:
1046
+ """Render economic calendar."""
1047
+ events = r.get("events", [])
1048
+ if has_rich:
1049
+ from rich.rule import Rule
1050
+ from rich.table import Table
1051
+ console.print(Rule("[bold]📅 经济事件日历[/bold]", style="dim"))
1052
+ t = Table(show_header=True, box=None, padding=(0,1))
1053
+ t.add_column("时间", style="dim", width=12)
1054
+ t.add_column("事件", min_width=30)
1055
+ t.add_column("重要性", justify="center")
1056
+ for ev in events[:15]:
1057
+ imp = ev.get("importance", ev.get("importance_level",""))
1058
+ imp_colored = (
1059
+ "[red]HIGH[/red]" if str(imp).upper() in ("HIGH","3","★★★")
1060
+ else "[yellow]MED[/yellow]" if str(imp).upper() in ("MEDIUM","2","★★")
1061
+ else f"[dim]{imp}[/dim]"
1062
+ )
1063
+ t.add_row(
1064
+ str(ev.get("time","") or ev.get("date",""))[:12],
1065
+ str(ev.get("event","") or ev.get("title",""))[:45],
1066
+ imp_colored,
1067
+ )
1068
+ console.print(t)
1069
+ else:
1070
+ for ev in events[:10]:
1071
+ print(f" {ev.get('event','')} [{ev.get('importance','')}]")
1072
+
1073
+
1074
+ def render_options_chain(r: dict, *, console=None, has_rich: bool = True) -> None:
1075
+ """Render options chain."""
1076
+ if not r.get("success"):
1077
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
1078
+ return
1079
+ symbol = r.get("symbol","")
1080
+ price = r.get("price", 0)
1081
+ expiry = r.get("expiry","")
1082
+ all_exp = r.get("all_expiries", [])
1083
+ if has_rich:
1084
+ from rich.table import Table
1085
+ from rich.rule import Rule
1086
+ console.print(Rule(f"[bold]{symbol}[/bold] 期权链 到期: [cyan]{expiry}[/cyan] 现价: [bold]{price:.2f}[/bold]", style="dim"))
1087
+ if all_exp:
1088
+ console.print(f" [dim]可用到期日: {', '.join(all_exp)}[/dim]")
1089
+ for side in ("calls", "puts"):
1090
+ rows = r.get(side, [])
1091
+ if not rows: continue
1092
+ label = "认购期权 (Calls)" if side == "calls" else "认沽期权 (Puts)"
1093
+ t = Table(title=f"[bold]{label}[/bold]", show_header=True, box=None, padding=(0,1))
1094
+ t.add_column("行权价", justify="right", style="bold")
1095
+ t.add_column("最新价", justify="right")
1096
+ t.add_column("买/卖", justify="right", style="dim")
1097
+ t.add_column("IV%", justify="right")
1098
+ t.add_column("OI", justify="right")
1099
+ t.add_column("价内?", justify="center")
1100
+ for row in rows:
1101
+ itm = row.get("inTheMoney", False)
1102
+ itm_s = "[green]✓[/green]" if itm else "[dim]—[/dim]"
1103
+ bid = row.get("bid", 0) or 0
1104
+ ask = row.get("ask", 0) or 0
1105
+ t.add_row(
1106
+ f"{row.get('strike',0):.2f}",
1107
+ f"{row.get('lastPrice',0):.2f}",
1108
+ f"{bid:.2f}/{ask:.2f}",
1109
+ f"{row.get('iv_pct',0):.1f}%",
1110
+ f"{int(row.get('openInterest',0)):,}",
1111
+ itm_s,
1112
+ )
1113
+ console.print(t)
1114
+ else:
1115
+ for side in ("calls","puts"):
1116
+ print(f"\n{side.upper()}")
1117
+ for row in r.get(side, []):
1118
+ print(f" K={row.get('strike')} last={row.get('lastPrice')} IV={row.get('iv_pct')}%")
1119
+
1120
+
1121
+ def render_quality_scores(symbol: str, f_r: dict, z_r: dict, *, console=None, has_rich: bool = True) -> None:
1122
+ """Render Piotroski F-Score + Altman Z-Score side by side."""
1123
+ if has_rich:
1124
+ from rich.table import Table
1125
+ from rich.rule import Rule
1126
+ from rich.columns import Columns
1127
+ from rich.panel import Panel
1128
+ from rich import box as _rbox
1129
+
1130
+ console.print(Rule(f"[bold]{symbol}[/bold] 财务质量双维评估", style="dim"))
1131
+
1132
+ # F-Score panel
1133
+ if f_r.get("success"):
1134
+ fs = f_r["f_score"]
1135
+ sig = f_r.get("signal","")
1136
+ color = "green" if sig == "bullish" else "red" if sig == "bearish" else "yellow"
1137
+ bars = "█" * fs + "░" * (9 - fs)
1138
+ f_body = (
1139
+ f"[bold {color}]{fs}/9[/bold {color}] [{color}]{f_r.get('verdict','')}[/{color}]\n"
1140
+ f"[{color}]{bars}[/{color}]\n\n"
1141
+ )
1142
+ scores = f_r.get("scores", {})
1143
+ categories = [
1144
+ ("盈利能力", ["F1_ROA_positive","F2_CFO_positive","F3_ROA_increasing","F4_CFO_gt_ROA"]),
1145
+ ("杠杆/流动性", ["F5_Leverage_lower","F6_CurrentRatio_up","F7_NoDilution"]),
1146
+ ("运营效率", ["F8_GrossMargin_up","F9_AssetTurnover_up"]),
1147
+ ]
1148
+ for cat, keys in categories:
1149
+ f_body += f"[dim]{cat}[/dim]\n"
1150
+ for k in keys:
1151
+ v = scores.get(k, 0)
1152
+ check = "[green]✓[/green]" if v else "[dim]✗[/dim]"
1153
+ f_body += f" {check} {k[3:].replace('_',' ')}\n"
1154
+ f_panel = Panel(f_body.strip(), title="[bold]Piotroski F-Score[/bold]",
1155
+ border_style=color, box=_rbox.ROUNDED, padding=(0,1))
1156
+ else:
1157
+ f_panel = Panel(f"[red]{f_r.get('error','失败')}[/red]",
1158
+ title="Piotroski F-Score", border_style="red")
1159
+
1160
+ # Z-Score panel
1161
+ if z_r.get("success"):
1162
+ zs = z_r["z_score"]
1163
+ risk = z_r.get("risk","medium")
1164
+ zone = z_r.get("zone","")
1165
+ zcolor = "green" if risk == "low" else "red" if risk == "high" else "yellow"
1166
+ z_body = (
1167
+ f"[bold {zcolor}]Z'' = {zs:.3f}[/bold {zcolor}]\n"
1168
+ f"[{zcolor}]{zone}[/{zcolor}]\n\n"
1169
+ f"[dim]安全区 >2.6 | 灰色区 1.1-2.6 | 风险区 <1.1[/dim]\n\n"
1170
+ )
1171
+ comp = z_r.get("components", {})
1172
+ labels = {
1173
+ "X1_working_capital_ratio": "X1 营运资本/总资产",
1174
+ "X2_retained_earnings_ratio": "X2 留存收益/总资产",
1175
+ "X3_ebit_ratio": "X3 EBIT/总资产",
1176
+ "X4_equity_to_debt": "X4 权益/负债",
1177
+ }
1178
+ weights = {"X1": 6.56, "X2": 3.26, "X3": 6.72, "X4": 1.05}
1179
+ for k, label in labels.items():
1180
+ v = comp.get(k, 0)
1181
+ prefix = k[:2]
1182
+ contrib = round(v * weights.get(prefix, 1), 3)
1183
+ c = "green" if v >= 0 else "red"
1184
+ z_body += f" [dim]{label}[/dim]: [{c}]{v:.4f}[/{c}] (贡献 {contrib:+.3f})\n"
1185
+ z_body += f"\n[dim]{z_r.get('formula','')}[/dim]"
1186
+ z_panel = Panel(z_body.strip(), title="[bold]Altman Z''-Score[/bold]",
1187
+ border_style=zcolor, box=_rbox.ROUNDED, padding=(0,1))
1188
+ else:
1189
+ z_panel = Panel(f"[red]{z_r.get('error','失败')}[/red]",
1190
+ title="Altman Z-Score", border_style="red")
1191
+
1192
+ console.print(Columns([f_panel, z_panel]))
1193
+ else:
1194
+ if f_r.get("success"):
1195
+ print(f"\nPiotroski F-Score: {f_r['f_score']}/9 {f_r.get('verdict','')}")
1196
+ if z_r.get("success"):
1197
+ print(f"Altman Z''-Score: {z_r['z_score']} {z_r.get('zone','')}")
1198
+
1199
+
1200
+ def render_ichimoku(r: dict, *, console=None, has_rich: bool = True) -> None:
1201
+ """Render Ichimoku Cloud analysis."""
1202
+ if not r.get("success"):
1203
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
1204
+ return
1205
+ if has_rich:
1206
+ from rich.table import Table
1207
+ from rich.rule import Rule
1208
+ sym = r.get("symbol","")
1209
+ sig = r.get("signal","")
1210
+ price = r.get("price", 0)
1211
+ is_bull = r.get("above_cloud", False)
1212
+ sig_color = "green" if is_bull else "red" if r.get("below_cloud") else "yellow"
1213
+ console.print(Rule(f"[bold]{sym}[/bold] 一目均衡表 [{sig_color}]{sig}[/{sig_color}]", style="dim"))
1214
+ t = Table(show_header=False, box=None, padding=(0,1))
1215
+ t.add_column(style="dim", width=18)
1216
+ t.add_column(justify="right")
1217
+ t.add_column(style="dim")
1218
+ t.add_row("现价", f"[bold]{price:.3f}[/bold]", "")
1219
+ t.add_row("转换线 (9)", f"{r.get('tenkan',0):.3f}",
1220
+ "[green]↑ 多头[/green]" if r.get('tenkan',0) > r.get('kijun',0) else "[red]↓ 空头[/red]")
1221
+ t.add_row("基准线 (26)",f"{r.get('kijun',0):.3f}", "")
1222
+ if r.get("senkou_a"):
1223
+ t.add_row("先行带A", f"{r.get('senkou_a',0):.3f}", "")
1224
+ if r.get("senkou_b"):
1225
+ t.add_row("先行带B", f"{r.get('senkou_b',0):.3f}", "")
1226
+ t.add_row("云层厚度", f"{r.get('cloud_thickness',0):.3f}", r.get("cloud_color",""))
1227
+ t.add_row("TK交叉", r.get("tk_cross",""), "")
1228
+ console.print(t)
1229
+ console.print(f" [bold {sig_color}]结论: {sig}[/bold {sig_color}] [dim]先行带偏移 26期,迟行线(Chikou)={r.get('chikou',0):.3f}[/dim]")
1230
+ else:
1231
+ print(f"\n{r.get('symbol','')} 一目均衡表")
1232
+ for k in ("tenkan","kijun","senkou_a","senkou_b","signal"):
1233
+ print(f" {k}: {r.get(k,'')}")
1234
+
1235
+
1236
+ def render_fear_greed(r: dict, *, console=None, has_rich: bool = True) -> None:
1237
+ """Render Fear & Greed Index with ASCII gauge."""
1238
+ if not r.get("success"):
1239
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
1240
+ return
1241
+ val = r.get("value", 50)
1242
+ label = r.get("label", "")
1243
+ sig = r.get("signal", "中性")
1244
+ if has_rich:
1245
+ from rich.rule import Rule
1246
+ color = "green" if val <= 25 else "red" if val >= 75 else "yellow"
1247
+ # ASCII gauge
1248
+ filled = int(val / 5)
1249
+ bar = "█" * filled + "░" * (20 - filled)
1250
+ console.print(Rule("[bold]₿ 加密恐惧贪婪指数[/bold]", style="dim"))
1251
+ console.print(f"\n [{color}]{bar}[/{color}] [bold {color}]{val}/100[/bold {color}] [{color}]{label}[/{color}]\n")
1252
+ console.print(f" 操作信号: [bold]{sig}[/bold] [dim]>75 极度贪婪(卖出信号) <25 极度恐惧(买入信号)[/dim]\n")
1253
+ hist = r.get("history", [])
1254
+ if len(hist) > 1:
1255
+ hist_str = " [dim]近7天: "
1256
+ for h in hist[:7]:
1257
+ v = h.get("value", 0)
1258
+ c = "green" if v <= 25 else "red" if v >= 75 else "yellow"
1259
+ hist_str += f"[{c}]{v}[/{c}] "
1260
+ console.print(hist_str + "[/dim]")
1261
+ else:
1262
+ print(f"\n恐惧贪婪指数: {val}/100 ({label}) 信号: {sig}")
1263
+
1264
+
1265
+ def render_funding_rates(r: dict, *, console=None, has_rich: bool = True) -> None:
1266
+ """Render perpetual funding rates."""
1267
+ if not r.get("success"):
1268
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
1269
+ return
1270
+ rates = r.get("rates", [])
1271
+ if has_rich:
1272
+ from rich.table import Table
1273
+ from rich.rule import Rule
1274
+ exchange = r.get("exchange","")
1275
+ bias = r.get("market_bias","")
1276
+ console.print(Rule(f"[bold]{exchange.upper()}[/bold] 永续合约资金费率", style="dim"))
1277
+ t = Table(show_header=True, box=None, padding=(0,1))
1278
+ t.add_column("合约", style="bold", width=12)
1279
+ t.add_column("费率", justify="right")
1280
+ t.add_column("年化", justify="right")
1281
+ t.add_column("下次结算", style="dim")
1282
+ t.add_column("信号", justify="center")
1283
+ for rt in rates:
1284
+ rate = rt.get("rate", 0)
1285
+ color = "red" if rate > 0.05 else "green" if rate < -0.01 else "dim"
1286
+ sig_s = rt.get("signal","中性")
1287
+ sig_c = "red" if sig_s == "空" else "green" if sig_s == "多" else "dim"
1288
+ t.add_row(
1289
+ rt.get("symbol",""),
1290
+ f"[{color}]{rt.get('rate_pct','')}[/{color}]",
1291
+ rt.get("annualized",""),
1292
+ rt.get("next_funding","")[:12],
1293
+ f"[{sig_c}]{sig_s}[/{sig_c}]",
1294
+ )
1295
+ console.print(t)
1296
+ console.print(f" [dim]市场偏向: [bold]{bias}[/bold] 正费率=多头付费给空头,负费率=空头付费给多头[/dim]")
1297
+ else:
1298
+ for rt in rates:
1299
+ print(f" {rt.get('symbol','')} {rt.get('rate_pct','')} (年化{rt.get('annualized','')})")
1300
+
1301
+
1302
+ def render_peer_comparison(r: dict, *, console=None, has_rich: bool = True) -> None:
1303
+ """Render peer comparison table."""
1304
+ if not r.get("success"):
1305
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
1306
+ return
1307
+ rows = r.get("table", [])
1308
+ if has_rich:
1309
+ from rich.table import Table
1310
+ from rich.rule import Rule
1311
+ symbol = r.get("symbol","")
1312
+ console.print(Rule(f"[bold]{symbol}[/bold] 同行估值对比", style="dim"))
1313
+ t = Table(show_header=True, box=None, padding=(0,1))
1314
+ t.add_column("代码", style="bold", width=8)
1315
+ t.add_column("名称", width=14)
1316
+ t.add_column("PE", justify="right")
1317
+ t.add_column("PB", justify="right")
1318
+ t.add_column("ROE%", justify="right")
1319
+ t.add_column("股息%", justify="right")
1320
+ t.add_column("市值(B)", justify="right", style="dim")
1321
+ for row in rows:
1322
+ is_t = row.get("is_target", False)
1323
+ pe = f"{row['pe']:.1f}" if row.get("pe") else "—"
1324
+ pb = f"{row['pb']:.2f}" if row.get("pb") else "—"
1325
+ roe = f"{row['roe_pct']:.1f}" if row.get("roe_pct") else "—"
1326
+ dy = f"{row['div_yield']:.2f}" if row.get("div_yield") else "—"
1327
+ mc = f"{row['market_cap_b']:.0f}" if row.get("market_cap_b") else "—"
1328
+ # Highlight target row
1329
+ style = "bold cyan" if is_t else ""
1330
+ t.add_row(
1331
+ f"[{style}]{row['symbol']}[/{style}]" if style else row["symbol"],
1332
+ (row.get("name","") or "")[:14],
1333
+ pe, pb, roe, dy, mc,
1334
+ )
1335
+ console.print(t)
1336
+ analysis = r.get("analysis", [])
1337
+ for line in analysis:
1338
+ console.print(f" [dim]▸ {line}[/dim]")
1339
+ else:
1340
+ print(f"\n{r.get('symbol','')} 同行对比")
1341
+ for row in rows:
1342
+ print(f" {row['symbol']:<8} PE:{row.get('pe','—')} PB:{row.get('pb','—')}")
1343
+
1344
+
1345
+ # ── 不动产渲染函数 ─────────────────────────────────────────────────────────────
1346
+
1347
+ def render_house_price(r: dict, *, console=None, has_rich: bool = True) -> None:
1348
+ if not has_rich:
1349
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1350
+ if not r.get("success"):
1351
+ console.print(f"[red]{r.get('error','获取失败')}[/red]"); return
1352
+ from rich.table import Table as _T
1353
+ from rich import box as _box
1354
+ c1 = str(r.get("city1") or "城市1")
1355
+ c2 = str(r.get("city2") or "城市2")
1356
+ lc1, lc2 = r.get("latest_city1") or {}, r.get("latest_city2") or {}
1357
+
1358
+ def _fmt(v):
1359
+ if v is None: return "[dim]—[/dim]"
1360
+ fv = float(v) if not isinstance(v, float) else v
1361
+ color = "green" if fv > 0 else "red" if fv < 0 else "dim"
1362
+ return f"[{color}]{fv:+.2f}%[/{color}]"
1363
+
1364
+ tb = _T(title="[bold]🏠 房价指数对比[/bold]", box=_box.ROUNDED, show_header=True)
1365
+ tb.add_column("指标", style="dim")
1366
+ tb.add_column(c1, justify="right")
1367
+ tb.add_column(c2, justify="right")
1368
+ tb.add_row("新建商品房同比", _fmt(lc1.get("new_yoy")), _fmt(lc2.get("new_yoy")))
1369
+ tb.add_row("新建商品房环比", _fmt(lc1.get("new_mom")), _fmt(lc2.get("new_mom")))
1370
+ tb.add_row("二手房价同比", _fmt(lc1.get("second_yoy")), _fmt(lc2.get("second_yoy")))
1371
+ tb.add_row("二手房价环比", _fmt(lc1.get("second_mom")), _fmt(lc2.get("second_mom")))
1372
+ tb.add_row("[dim]数据期[/dim]", str(lc1.get("date") or "—"), str(lc2.get("date") or "—"))
1373
+ console.print(tb)
1374
+ console.print("[dim]数据来源:国家统计局 via akshare[/dim]")
1375
+
1376
+
1377
+ def render_reits_list(r: dict, *, console=None, has_rich: bool = True) -> None:
1378
+ if not has_rich:
1379
+ for row in r.get("reits", [])[:10]: print(row); return
1380
+ if not r.get("success"):
1381
+ console.print(f"[red]{r.get('error','获取失败')}[/red]"); return
1382
+ from rich.table import Table as _T
1383
+ from rich import box as _box
1384
+ tb = _T(title="[bold]🏗 中国 REITs 实时行情[/bold]", box=_box.ROUNDED)
1385
+ tb.add_column("代码", style="cyan")
1386
+ tb.add_column("名称")
1387
+ tb.add_column("最新价", justify="right")
1388
+ tb.add_column("涨跌幅", justify="right")
1389
+ tb.add_column("昨收", justify="right", style="dim")
1390
+ tb.add_column("成交额(万)", justify="right", style="dim")
1391
+ for row in r.get("reits", [])[:20]:
1392
+ chg = row.get("涨跌幅") or 0
1393
+ try: chg_f = float(chg)
1394
+ except Exception: chg_f = 0
1395
+ color = "green" if chg_f > 0 else "red" if chg_f < 0 else "dim"
1396
+ vol_wan = ""
1397
+ try: vol_wan = f"{float(row.get('成交额',0))/10000:.0f}"
1398
+ except Exception as _e: logger.debug("vol_wan parse error: %s", _e)
1399
+ tb.add_row(
1400
+ str(row.get("代码","")),
1401
+ str(row.get("名称",""))[:12],
1402
+ str(row.get("最新价","")),
1403
+ f"[{color}]{chg}%[/{color}]",
1404
+ str(row.get("昨收","")),
1405
+ vol_wan,
1406
+ )
1407
+ console.print(tb)
1408
+ console.print(f"[dim]共 {r.get('count',0)} 只 REITs · 数据来源:东方财富[/dim]")
1409
+
1410
+
1411
+ def render_rental_yield(r: dict, *, console=None, has_rich: bool = True) -> None:
1412
+ if not has_rich:
1413
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1414
+ if not r.get("success"):
1415
+ console.print(f"[red]{r.get('error','计算失败')}[/red]"); return
1416
+ from rich.panel import Panel as _P
1417
+ from rich.columns import Columns as _C
1418
+ from rich import box as _box
1419
+ assess_color = "green" if "优质" in str(r.get("assessment","")) else \
1420
+ "yellow" if "合理" in str(r.get("assessment","")) else "red"
1421
+ lines = [
1422
+ f"[bold cyan]购入价格[/bold cyan] {r['purchase_price_wan']:.1f} 万元",
1423
+ f"[bold cyan]月租金[/bold cyan] {r['monthly_rent']:.0f} 元/月",
1424
+ "[dim]───────────────────────────────[/dim]",
1425
+ f"[bold]毛租金收益率[/bold] [{assess_color}]{r['gross_yield_pct']:.2f}%[/{assess_color}]",
1426
+ f"[bold]净收益率[/bold] {r['net_yield_pct']:.2f}%",
1427
+ f"[bold]资本化率[/bold] {r['cap_rate_pct']:.2f}%",
1428
+ f"[bold]回本年限[/bold] {r['payback_years']:.1f} 年",
1429
+ ]
1430
+ if r.get("leveraged_yield_pct") is not None:
1431
+ lines.append(f"[bold]杠杆收益率[/bold] {r['leveraged_yield_pct']:.2f}% [dim](含贷款)[/dim]")
1432
+ lines += [
1433
+ "[dim]───────────────────────────────[/dim]",
1434
+ f"[{assess_color}]综合评级:{r.get('assessment','')}[/{assess_color}]",
1435
+ f"[dim]{r.get('benchmark','')}[/dim]",
1436
+ ]
1437
+ console.print(_P("\n".join(lines), title="[bold]💰 租金收益率分析[/bold]",
1438
+ border_style="cyan"))
1439
+
1440
+
1441
+ def render_property_val(r: dict, *, console=None, has_rich: bool = True) -> None:
1442
+ if not has_rich:
1443
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1444
+ if not r.get("success"):
1445
+ console.print(f"[red]{r.get('error','估值失败')}[/red]"); return
1446
+ from rich.table import Table as _T
1447
+ from rich import box as _box
1448
+ from rich.panel import Panel as _P
1449
+ verd = r.get("verdict","")
1450
+ verd_color = "green" if "低估" in verd else "red" if "高估" in verd else "yellow"
1451
+ tb = _T(title="[bold]🏢 物业三合一估值[/bold]", box=_box.ROUNDED)
1452
+ tb.add_column("方法", style="dim")
1453
+ tb.add_column("估值(万元)", justify="right")
1454
+ tb.add_column("说明", style="dim")
1455
+ tb.add_row("收益法 (Cap Rate)", f"{r['income_approach']:.1f}", f"资本化率 {r['cap_rate_used']:.1f}%")
1456
+ tb.add_row("DCF 折现法", f"{r['dcf_approach']:.1f}", f"折现率 {r['discount_rate_used']:.1f}%")
1457
+ tb.add_row("市场比较法", f"{r['market_approach']:.1f}", "基于租金倍数推算")
1458
+ tb.add_row("[bold]综合估值[/bold]", f"[bold cyan]{r['blended_value_wan']:.1f}[/bold cyan]", "权重 4:4:2")
1459
+ console.print(tb)
1460
+ lo, hi = r.get("market_range_wan", [0, 0])
1461
+ console.print(f" 区位参考区间: [dim]{lo:.0f} — {hi:.0f} 万元[/dim]")
1462
+ console.print(f" 单价参考: [bold]{r.get('price_per_sqm',0):,.0f}[/bold] 元/㎡")
1463
+ console.print(f" [{verd_color}]{verd}[/{verd_color}]")
1464
+
1465
+
1466
+ def render_multi_city(r: dict, *, console=None, has_rich: bool = True) -> None:
1467
+ if not has_rich:
1468
+ for c in r.get("cities", []): print(c); return
1469
+ if not r.get("success"):
1470
+ console.print(f"[red]{r.get('error','获取失败')}[/red]"); return
1471
+ from rich.table import Table as _T
1472
+ from rich import box as _box
1473
+ tb = _T(title="[bold]🗺 多城市房价对比[/bold]", box=_box.ROUNDED)
1474
+ tb.add_column("城市", style="bold")
1475
+ tb.add_column("等级", style="dim")
1476
+ tb.add_column("数据期", style="dim")
1477
+ tb.add_column("新房同比", justify="right")
1478
+ tb.add_column("新房环比", justify="right")
1479
+ tb.add_column("二手同比", justify="right")
1480
+ for city in r.get("cities", []):
1481
+ def _fc(v):
1482
+ if v is None: return "[dim]—[/dim]"
1483
+ c = "green" if v > 0 else "red" if v < 0 else "dim"
1484
+ return f"[{c}]{v:+.2f}%[/{c}]"
1485
+ tb.add_row(
1486
+ city["city"], city.get("tier",""),
1487
+ city.get("date",""),
1488
+ _fc(city.get("new_yoy")), _fc(city.get("new_mom")),
1489
+ _fc(city.get("second_yoy")),
1490
+ )
1491
+ console.print(tb)
1492
+ console.print(f" 涨幅最高: [green]{r.get('top_riser','')}[/green] "
1493
+ f"涨幅最低/下跌: [red]{r.get('top_faller','')}[/red]")
1494
+
1495
+
1496
+ def render_asset_score(r: dict, *, console=None, has_rich: bool = True) -> None:
1497
+ if not has_rich:
1498
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1499
+ if not r.get("success"):
1500
+ console.print(f"[red]{r.get('error','评分失败')}[/red]"); return
1501
+ from rich.panel import Panel as _P
1502
+ score = r.get("score", 0)
1503
+ rating = r.get("rating", "")
1504
+ color = "green" if score >= 75 else "yellow" if score >= 60 else "red"
1505
+ bar_len = int(score / 5)
1506
+ bar = "█" * bar_len + "░" * (20 - bar_len)
1507
+ lines = [
1508
+ f"综合评分: [{color}]{score}[/{color}] / 100",
1509
+ f"评级: [{color}]{rating}[/{color}]",
1510
+ f"[{color}]{bar}[/{color}]",
1511
+ "[dim]─────────────────────────────[/dim]",
1512
+ ]
1513
+ for k, v in r.get("breakdown", {}).items():
1514
+ lines.append(f" {k:<12} [dim]{v}[/dim]")
1515
+ if r.get("suitable_businesses"):
1516
+ lines.append("[dim]─────────────────────────────[/dim]")
1517
+ lines.append(f"推荐业态: [cyan]{' / '.join(r['suitable_businesses'])}[/cyan]")
1518
+ console.print(_P("\n".join(lines), title="[bold]📍 资产区位评分[/bold]",
1519
+ border_style=color))
1520
+
1521
+
1522
+ # ── 数据分析渲染函数 ────────────────────────────────────────────────────────────
1523
+
1524
+ def render_corr_matrix(r: dict, *, console=None, has_rich: bool = True) -> None:
1525
+ if not has_rich:
1526
+ print(json.dumps(r.get("corr_matrix", {}), ensure_ascii=False, indent=2)); return
1527
+ if not r.get("success"):
1528
+ console.print(f"[red]{r.get('error','计算失败')}[/red]"); return
1529
+ from rich.table import Table as _T
1530
+ from rich import box as _box
1531
+ syms = r.get("symbols", [])
1532
+ corr = r.get("corr_matrix", {})
1533
+ tb = _T(title=f"[bold]📊 相关性矩阵 ({r.get('period','')}/{r.get('interval','1d')})[/bold]",
1534
+ box=_box.SIMPLE_HEAVY)
1535
+ tb.add_column("", style="bold")
1536
+ for s in syms:
1537
+ tb.add_column(s, justify="right")
1538
+ for s1 in syms:
1539
+ row_vals = []
1540
+ for s2 in syms:
1541
+ v = corr.get(s1, {}).get(s2)
1542
+ if v is None:
1543
+ row_vals.append("[dim]—[/dim]")
1544
+ elif s1 == s2:
1545
+ row_vals.append("[dim]1.00[/dim]")
1546
+ else:
1547
+ abs_v = abs(v)
1548
+ color = "red" if abs_v > 0.8 else "yellow" if abs_v > 0.5 else "green"
1549
+ row_vals.append(f"[{color}]{v:+.3f}[/{color}]")
1550
+ tb.add_row(f"[bold]{s1}[/bold]", *row_vals)
1551
+ console.print(tb)
1552
+ console.print("[dim]红 > 0.8 高相关 | 黄 0.5-0.8 中度 | 绿 < 0.5 低相关[/dim]")
1553
+ # Stats table
1554
+ stats = r.get("stats", {})
1555
+ if stats:
1556
+ st = _T(title="[dim]个股统计[/dim]", box=_box.MINIMAL)
1557
+ st.add_column("标的"); st.add_column("总收益%", justify="right")
1558
+ st.add_column("年化波动%", justify="right"); st.add_column("夏普", justify="right")
1559
+ st.add_column("最大回撤%", justify="right")
1560
+ for sym, sv in stats.items():
1561
+ ret = sv.get("return_total")
1562
+ rcolor = "green" if (ret or 0) > 0 else "red"
1563
+ st.add_row(sym,
1564
+ f"[{rcolor}]{ret:+.1f}[/{rcolor}]" if ret is not None else "—",
1565
+ f"{sv.get('volatility',0):.1f}" if sv.get("volatility") else "—",
1566
+ f"{sv.get('sharpe',0):.2f}" if sv.get("sharpe") else "—",
1567
+ f"[red]{sv.get('max_drawdown',0):.1f}[/red]")
1568
+ console.print(st)
1569
+
1570
+
1571
+ def render_portfolio_bt(r: dict, *, console=None, has_rich: bool = True) -> None:
1572
+ if not has_rich:
1573
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1574
+ if not r.get("success"):
1575
+ console.print(f"[red]{r.get('error','回测失败')}[/red]"); return
1576
+ from rich.table import Table as _T
1577
+ from rich import box as _box
1578
+ from rich.panel import Panel as _P
1579
+ pf = r.get("portfolio", {})
1580
+ bm = r.get("benchmark", {})
1581
+ ret = pf.get("total_return_pct", 0)
1582
+ rcolor = "green" if ret > 0 else "red"
1583
+ lines = [
1584
+ f" [bold]总收益率[/bold] [{rcolor}]{ret:+.2f}%[/{rcolor}]",
1585
+ f" [bold]年化波动率[/bold] {pf.get('annual_vol_pct',0):.2f}%",
1586
+ f" [bold]夏普比率[/bold] {pf.get('sharpe_ratio','—')}",
1587
+ f" [bold]最大回撤[/bold] [red]{pf.get('max_drawdown_pct',0):.2f}%[/red]",
1588
+ f" [bold]卡玛比率[/bold] {pf.get('calmar_ratio','—')}",
1589
+ ]
1590
+ if bm:
1591
+ br = bm.get("total_return_pct", 0)
1592
+ bc = "green" if br > 0 else "red"
1593
+ alpha = round(ret - br, 2)
1594
+ ac = "green" if alpha > 0 else "red"
1595
+ lines += [
1596
+ " [dim]─────────────────────────────────[/dim]",
1597
+ f" [dim]基准 {bm['symbol']}[/dim] [{bc}]{br:+.2f}%[/{bc}]",
1598
+ f" [bold]超额收益[/bold] [{ac}]{alpha:+.2f}%[/{ac}]",
1599
+ ]
1600
+ console.print(_P("\n".join(lines), title="[bold]📈 组合回测结果[/bold]",
1601
+ border_style=rcolor))
1602
+ # Allocation table
1603
+ alloc = r.get("allocation", [])
1604
+ if alloc:
1605
+ ta = _T(title=f"[dim]持仓分配 · 回测区间 {r.get('period','N/A')} · 再平衡 {r.get('rebalance','—')}[/dim]",
1606
+ box=_box.MINIMAL)
1607
+ ta.add_column("标的"); ta.add_column("权重%", justify="right")
1608
+ for a in alloc:
1609
+ ta.add_row(a["symbol"], f"{a['weight_pct']:.1f}%")
1610
+ console.print(ta)
1611
+
1612
+
1613
+ def render_sql_result(r: dict, *, console=None, has_rich: bool = True) -> None:
1614
+ if not has_rich:
1615
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1616
+ if not r.get("success"):
1617
+ console.print(f"[red]{r.get('error','查询失败')}[/red]"); return
1618
+ from rich.table import Table as _T
1619
+ from rich import box as _box
1620
+ rows = r.get("rows", [])
1621
+ cols = r.get("columns", [])
1622
+ if not rows:
1623
+ console.print("[dim]查询返回 0 行[/dim]"); return
1624
+ tb = _T(title=f"[bold]🦆 DuckDB 查询结果[/bold] [dim]({r.get('row_count',0)} 行)[/dim]",
1625
+ box=_box.ROUNDED)
1626
+ for c in cols:
1627
+ tb.add_column(str(c))
1628
+ for row in rows[:100]:
1629
+ tb.add_row(*[str(row.get(c, "")) for c in cols])
1630
+ console.print(tb)
1631
+ if r.get("tables_loaded"):
1632
+ console.print(f"[dim]已加载表: {', '.join(r['tables_loaded'])}[/dim]")
1633
+
1634
+
1635
+ def render_alerts(r: dict, *, console=None, has_rich: bool = True) -> None:
1636
+ if not has_rich:
1637
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1638
+ from rich.table import Table as _T
1639
+ from rich import box as _box
1640
+ active = r.get("active_alerts", [])
1641
+ triggered = r.get("triggered_alerts", [])
1642
+ cond_lbl = {"gt": "高于", "lt": "低于", "cross_up": "向上突破", "cross_down": "向下跌破"}
1643
+ if active:
1644
+ ta = _T(title="[bold]🔔 活跃预警[/bold]", box=_box.ROUNDED)
1645
+ ta.add_column("标的", style="cyan"); ta.add_column("条件")
1646
+ ta.add_column("触发价", justify="right"); ta.add_column("备注", style="dim")
1647
+ ta.add_column("ID", style="dim")
1648
+ for a in active:
1649
+ ta.add_row(a["symbol"],
1650
+ cond_lbl.get(a["condition"], a["condition"]),
1651
+ str(a["price"]), a.get("note",""),
1652
+ a["id"][:16]+"…")
1653
+ console.print(ta)
1654
+ if triggered:
1655
+ tt = _T(title="[dim]已触发预警[/dim]", box=_box.MINIMAL)
1656
+ tt.add_column("标的"); tt.add_column("触发价"); tt.add_column("触发时间", style="dim")
1657
+ for a in triggered:
1658
+ tt.add_row(a["symbol"], str(a.get("triggered_price","")),
1659
+ str(a.get("triggered_at",""))[:16])
1660
+ console.print(tt)
1661
+ if not active and not triggered:
1662
+ console.print("[dim]暂无预警记录。使用 /alert add AAPL gt 200 设置预警[/dim]")
1663
+
1664
+
1665
+ def _prompt_float(label: str, default: float) -> float:
1666
+ """交互式数字输入,失败时返回 default。"""
1667
+
1668
+
1669
+ # Moved from aria_cli.py
1670
+ def format_backtest_output(data: dict):
1671
+ """Format backtest results as clean rows."""
1672
+ if not HAS_RICH:
1673
+ return json.dumps(data, indent=2, ensure_ascii=False)
1674
+
1675
+ d = data.get("data", data.get("backtest", data))
1676
+ total_ret = d.get("total_return", 0)
1677
+ ann_ret = d.get("annualized_return", 0)
1678
+ sharpe = d.get("sharpe_ratio", 0)
1679
+ max_dd = d.get("max_drawdown", 0)
1680
+ win_rate = d.get("win_rate", 0)
1681
+ trades = d.get("num_trades", 0)
1682
+ bh_ret = d.get("buy_hold_return", 0)
1683
+ outperf = d.get("outperformance", 0)
1684
+
1685
+ def _c(v): return "green" if v >= 0 else "red"
1686
+
1687
+ out = Text()
1688
+ out.append(" Backtest Results\n", style="bold")
1689
+ out.append(f" {'Total Return':<18s}", style="dim")
1690
+ out.append(f"{total_ret*100:+.2f}%", style=_c(total_ret))
1691
+ out.append(" vs B&H ", style="dim")
1692
+ out.append(f"{bh_ret*100:+.2f}%\n", style=_c(bh_ret))
1693
+ out.append(f" {'Annualized':<18s}", style="dim")
1694
+ out.append(f"{ann_ret*100:+.2f}%\n")
1695
+ out.append(f" {'Sharpe Ratio':<18s}", style="dim")
1696
+ out.append(f"{sharpe:.2f}\n")
1697
+ out.append(f" {'Max Drawdown':<18s}", style="dim")
1698
+ out.append(f"{max_dd*100:.2f}%\n", style="red")
1699
+ out.append(f" {'Win Rate':<18s}", style="dim")
1700
+ out.append(f"{win_rate*100:.1f}%\n")
1701
+ out.append(f" {'Trades':<18s}", style="dim")
1702
+ out.append(f"{trades}\n")
1703
+ out.append(f" {'Outperformance':<18s}", style="dim")
1704
+ out.append(f"{outperf*100:+.2f}%\n", style=_c(outperf))
1705
+ return out