aria-code 0.167.0__py3-none-win_amd64.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 (601) hide show
  1. aria_bots/__init__.py +7 -0
  2. aria_bots/channels/README.md +97 -0
  3. aria_bots/channels/__init__.py +1 -0
  4. aria_bots/channels/approvals.py +263 -0
  5. aria_bots/channels/conversation.py +280 -0
  6. aria_bots/channels/digest.py +186 -0
  7. aria_bots/channels/registry.py +95 -0
  8. aria_bots/engine.py +144 -0
  9. aria_bots/feishu_bot.py +1385 -0
  10. aria_bots/logistics/__init__.py +1 -0
  11. aria_bots/logistics/carriers.py +253 -0
  12. aria_bots/logistics/freight.py +154 -0
  13. aria_bots/logistics/inventory.py +242 -0
  14. aria_bots/logistics/mcp_server.py +156 -0
  15. aria_bots/logistics/records.py +119 -0
  16. aria_bots/logistics/tenancy.py +152 -0
  17. aria_bots/market.py +91 -0
  18. aria_bots/relay_client.py +326 -0
  19. aria_bots/telegram_bot.py +299 -0
  20. aria_code/__init__.py +111 -0
  21. aria_code/_bin/aria-native.exe +0 -0
  22. aria_code/_bin/aria-runtime.exe +0 -0
  23. aria_code/_version.py +20 -0
  24. aria_code/account_memory.py +296 -0
  25. aria_code/account_session.py +166 -0
  26. aria_code/adk_apps/__init__.py +1 -0
  27. aria_code/adk_apps/aria_code_review/__init__.py +1 -0
  28. aria_code/adk_apps/aria_code_review/agent.py +32 -0
  29. aria_code/adk_apps/aria_research/__init__.py +1 -0
  30. aria_code/adk_apps/aria_research/agent.py +31 -0
  31. aria_code/agents/__init__.py +49 -0
  32. aria_code/agents/base.py +329 -0
  33. aria_code/agents/code_review.py +200 -0
  34. aria_code/agents/deep/__init__.py +37 -0
  35. aria_code/agents/deep/calibration_loop.py +144 -0
  36. aria_code/agents/deep/critic.py +125 -0
  37. aria_code/agents/deep/deepen.py +193 -0
  38. aria_code/agents/deep/models.py +149 -0
  39. aria_code/agents/deep/pipeline.py +164 -0
  40. aria_code/agents/deep/quant_fusion.py +192 -0
  41. aria_code/agents/deep/themes.py +95 -0
  42. aria_code/agents/deep/tiers.py +106 -0
  43. aria_code/agents/engineering/__init__.py +14 -0
  44. aria_code/agents/engineering/coder.py +222 -0
  45. aria_code/agents/engineering/tester.py +145 -0
  46. aria_code/agents/financial/__init__.py +21 -0
  47. aria_code/agents/financial/cashflow_burn.py +119 -0
  48. aria_code/agents/financial/catalyst.py +279 -0
  49. aria_code/agents/financial/corporate_finance.py +175 -0
  50. aria_code/agents/financial/debate.py +224 -0
  51. aria_code/agents/financial/earnings.py +315 -0
  52. aria_code/agents/financial/fundamental.py +178 -0
  53. aria_code/agents/financial/macro.py +111 -0
  54. aria_code/agents/financial/news.py +231 -0
  55. aria_code/agents/financial/risk.py +146 -0
  56. aria_code/agents/financial/sector.py +279 -0
  57. aria_code/agents/financial/strategist.py +121 -0
  58. aria_code/agents/financial/stripe_revenue.py +127 -0
  59. aria_code/agents/financial/synthesis.py +274 -0
  60. aria_code/agents/financial/technical.py +277 -0
  61. aria_code/agents/orchestrator.py +44 -0
  62. aria_code/agents/portfolio_agent.py +392 -0
  63. aria_code/agents/realty/__init__.py +62 -0
  64. aria_code/agents/realty/asset_diagnosis.py +152 -0
  65. aria_code/agents/realty/business_match.py +166 -0
  66. aria_code/agents/realty/cashflow_verify.py +209 -0
  67. aria_code/agents/realty/contract_rules.py +210 -0
  68. aria_code/agents/realty/energy_anomaly.py +189 -0
  69. aria_code/agents/realty/exit_settlement.py +208 -0
  70. aria_code/agents/realty/fulfillment_risk.py +206 -0
  71. aria_code/agents/realty/ops_optimize.py +160 -0
  72. aria_code/agents/realty/revenue_share.py +215 -0
  73. aria_code/agents/registry.py +168 -0
  74. aria_code/agents/signal_scheme.py +111 -0
  75. aria_code/agents/sports/__init__.py +0 -0
  76. aria_code/agents/sports/football_agent.py +197 -0
  77. aria_code/agents/supervisor.py +113 -0
  78. aria_code/agents/team.py +500 -0
  79. aria_code/agents/video/__init__.py +16 -0
  80. aria_code/agents/video/director.py +376 -0
  81. aria_code/agents/video/qa_agent.py +143 -0
  82. aria_code/agents/video/script_doctor.py +112 -0
  83. aria_code/agents/warehouse/__init__.py +25 -0
  84. aria_code/agents/warehouse/contracts.py +26 -0
  85. aria_code/agents/warehouse/dashboard.py +155 -0
  86. aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
  87. aria_code/agents/warehouse/inbound_exceptions.py +25 -0
  88. aria_code/agents/warehouse/inventory_health.py +24 -0
  89. aria_code/agents/warehouse/local_run.py +71 -0
  90. aria_code/agents/warehouse/local_snapshot.py +132 -0
  91. aria_code/agents/warehouse/logistics_cost.py +84 -0
  92. aria_code/agents/warehouse/logistics_sync.py +21 -0
  93. aria_code/agents/warehouse/navigation.schema.json +110 -0
  94. aria_code/agents/warehouse/task_agent.py +172 -0
  95. aria_code/agents/warehouse/workflow.py +31 -0
  96. aria_code/aliyun_data_client.py +4 -0
  97. aria_code/apps/README.md +12 -0
  98. aria_code/apps/__init__.py +2 -0
  99. aria_code/apps/channels/README.md +9 -0
  100. aria_code/apps/channels/__init__.py +17 -0
  101. aria_code/apps/channels/approvals.py +11 -0
  102. aria_code/apps/channels/conversation.py +11 -0
  103. aria_code/apps/channels/digest.py +11 -0
  104. aria_code/apps/channels/intake.py +92 -0
  105. aria_code/apps/channels/registry.py +11 -0
  106. aria_code/apps/channels/tradingview.py +94 -0
  107. aria_code/apps/cli/README.md +13 -0
  108. aria_code/apps/cli/__init__.py +2 -0
  109. aria_code/apps/cli/_optional.py +120 -0
  110. aria_code/apps/cli/app_server.py +656 -0
  111. aria_code/apps/cli/auto_update.py +125 -0
  112. aria_code/apps/cli/bootstrap.py +206 -0
  113. aria_code/apps/cli/broker_render.py +206 -0
  114. aria_code/apps/cli/chat_turn.py +1327 -0
  115. aria_code/apps/cli/cloud_models.py +231 -0
  116. aria_code/apps/cli/codegen_paths.py +29 -0
  117. aria_code/apps/cli/commands/__init__.py +34 -0
  118. aria_code/apps/cli/commands/_business_render.py +78 -0
  119. aria_code/apps/cli/commands/_ui.py +71 -0
  120. aria_code/apps/cli/commands/analysis_cmds.py +378 -0
  121. aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
  122. aria_code/apps/cli/commands/auth_cmds.py +215 -0
  123. aria_code/apps/cli/commands/backtest_cmds.py +2518 -0
  124. aria_code/apps/cli/commands/broker_cmds.py +1236 -0
  125. aria_code/apps/cli/commands/business_workflow_cmds.py +379 -0
  126. aria_code/apps/cli/commands/canvas_cmds.py +77 -0
  127. aria_code/apps/cli/commands/catalog.py +189 -0
  128. aria_code/apps/cli/commands/core_cmds.py +2748 -0
  129. aria_code/apps/cli/commands/data_cmds.py +521 -0
  130. aria_code/apps/cli/commands/diagnostic_cmds.py +285 -0
  131. aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
  132. aria_code/apps/cli/commands/engineering_cmds.py +399 -0
  133. aria_code/apps/cli/commands/file_cmds.py +312 -0
  134. aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
  135. aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
  136. aria_code/apps/cli/commands/market.py +544 -0
  137. aria_code/apps/cli/commands/market_cmds.py +1419 -0
  138. aria_code/apps/cli/commands/market_context.py +432 -0
  139. aria_code/apps/cli/commands/market_render.py +7 -0
  140. aria_code/apps/cli/commands/model_cmds.py +2374 -0
  141. aria_code/apps/cli/commands/ops_cmds.py +721 -0
  142. aria_code/apps/cli/commands/orchestrator_cmds.py +181 -0
  143. aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
  144. aria_code/apps/cli/commands/portfolio_cmds.py +1316 -0
  145. aria_code/apps/cli/commands/report.py +476 -0
  146. aria_code/apps/cli/commands/scaffold_templates.py +617 -0
  147. aria_code/apps/cli/commands/session_cmds.py +236 -0
  148. aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
  149. aria_code/apps/cli/commands/team.py +723 -0
  150. aria_code/apps/cli/commands/team_render.py +8 -0
  151. aria_code/apps/cli/commands/ui_cmds.py +401 -0
  152. aria_code/apps/cli/commands/warehouse_cmds.py +297 -0
  153. aria_code/apps/cli/commands/workflow_cmds.py +629 -0
  154. aria_code/apps/cli/commands/workspace_cmds.py +1543 -0
  155. aria_code/apps/cli/config_paths.py +73 -0
  156. aria_code/apps/cli/config_store.py +158 -0
  157. aria_code/apps/cli/constants.py +88 -0
  158. aria_code/apps/cli/context.py +34 -0
  159. aria_code/apps/cli/conversation_policy.py +53 -0
  160. aria_code/apps/cli/deterministic.py +200 -0
  161. aria_code/apps/cli/direct.py +48 -0
  162. aria_code/apps/cli/earnings_view.py +172 -0
  163. aria_code/apps/cli/endpoint_migration.py +64 -0
  164. aria_code/apps/cli/exec_events.py +255 -0
  165. aria_code/apps/cli/file_references.py +155 -0
  166. aria_code/apps/cli/finance_service_catalog.py +111 -0
  167. aria_code/apps/cli/football_reports.py +231 -0
  168. aria_code/apps/cli/github_app_auth.py +135 -0
  169. aria_code/apps/cli/google_login.py +239 -0
  170. aria_code/apps/cli/handlers/__init__.py +11 -0
  171. aria_code/apps/cli/handlers/broker_handlers.py +122 -0
  172. aria_code/apps/cli/handlers/chart_handlers.py +1314 -0
  173. aria_code/apps/cli/handlers/market_handlers.py +2570 -0
  174. aria_code/apps/cli/handlers/realty_handlers.py +114 -0
  175. aria_code/apps/cli/handlers/sports_handlers.py +22 -0
  176. aria_code/apps/cli/handlers/strategy_advice.py +82 -0
  177. aria_code/apps/cli/headless.py +352 -0
  178. aria_code/apps/cli/helpers.py +326 -0
  179. aria_code/apps/cli/hooks.py +180 -0
  180. aria_code/apps/cli/i18n.py +310 -0
  181. aria_code/apps/cli/intent.py +136 -0
  182. aria_code/apps/cli/intent_router.py +305 -0
  183. aria_code/apps/cli/intent_shadow.py +72 -0
  184. aria_code/apps/cli/intent_signals.py +347 -0
  185. aria_code/apps/cli/lifecycle_hooks.py +48 -0
  186. aria_code/apps/cli/local_tool_schemas.py +550 -0
  187. aria_code/apps/cli/main.py +39 -0
  188. aria_code/apps/cli/market_hours.py +100 -0
  189. aria_code/apps/cli/market_metadata.py +135 -0
  190. aria_code/apps/cli/market_universe.py +487 -0
  191. aria_code/apps/cli/message_processing.py +310 -0
  192. aria_code/apps/cli/mixin_binding.py +32 -0
  193. aria_code/apps/cli/model_catalog.py +652 -0
  194. aria_code/apps/cli/model_probe.py +169 -0
  195. aria_code/apps/cli/model_retirement.py +65 -0
  196. aria_code/apps/cli/native_branding.py +12 -0
  197. aria_code/apps/cli/native_bridge.py +118 -0
  198. aria_code/apps/cli/native_entry.py +58 -0
  199. aria_code/apps/cli/pdf_report.py +977 -0
  200. aria_code/apps/cli/plan_mode.py +139 -0
  201. aria_code/apps/cli/plotly_html.py +15 -0
  202. aria_code/apps/cli/prediction_feedback.py +202 -0
  203. aria_code/apps/cli/preflight.py +502 -0
  204. aria_code/apps/cli/project_aria.py +60 -0
  205. aria_code/apps/cli/prompt_assembly.py +78 -0
  206. aria_code/apps/cli/prompts/__init__.py +0 -0
  207. aria_code/apps/cli/prompts/coding.py +71 -0
  208. aria_code/apps/cli/prompts/select.py +193 -0
  209. aria_code/apps/cli/prompts/system_prompts.py +562 -0
  210. aria_code/apps/cli/prompts/ui.py +436 -0
  211. aria_code/apps/cli/provider_endpoints.py +237 -0
  212. aria_code/apps/cli/provider_keys.py +73 -0
  213. aria_code/apps/cli/providers/__init__.py +1 -0
  214. aria_code/apps/cli/providers/base.py +775 -0
  215. aria_code/apps/cli/providers/chat_routing.py +139 -0
  216. aria_code/apps/cli/providers/collaboration.py +98 -0
  217. aria_code/apps/cli/providers/llm/__init__.py +1 -0
  218. aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
  219. aria_code/apps/cli/providers/llm/ollama_stream.py +1574 -0
  220. aria_code/apps/cli/providers/llm/response_cache.py +60 -0
  221. aria_code/apps/cli/providers/llm/sse_stream.py +298 -0
  222. aria_code/apps/cli/providers/runtime_bridge.py +586 -0
  223. aria_code/apps/cli/providers/vertexai_stream.py +592 -0
  224. aria_code/apps/cli/release_download.py +159 -0
  225. aria_code/apps/cli/review_runner.py +111 -0
  226. aria_code/apps/cli/runtime_consumer.py +769 -0
  227. aria_code/apps/cli/sandbox.py +32 -0
  228. aria_code/apps/cli/session_export.py +120 -0
  229. aria_code/apps/cli/session_jsonl.py +208 -0
  230. aria_code/apps/cli/session_refresh.py +123 -0
  231. aria_code/apps/cli/session_store.py +125 -0
  232. aria_code/apps/cli/skills_catalog.py +273 -0
  233. aria_code/apps/cli/task_isolation.py +211 -0
  234. aria_code/apps/cli/terminal_chrome.py +412 -0
  235. aria_code/apps/cli/todo_tracker.py +190 -0
  236. aria_code/apps/cli/tool_executor.py +932 -0
  237. aria_code/apps/cli/tool_registry.py +34 -0
  238. aria_code/apps/cli/tool_scope.py +113 -0
  239. aria_code/apps/cli/tools/__init__.py +40 -0
  240. aria_code/apps/cli/tools/context.py +46 -0
  241. aria_code/apps/cli/tools/file_tools.py +181 -0
  242. aria_code/apps/cli/tools/market_tools.py +626 -0
  243. aria_code/apps/cli/tools/notebook_tools.py +111 -0
  244. aria_code/apps/cli/tools/system_tools.py +656 -0
  245. aria_code/apps/cli/tools/write_tools.py +961 -0
  246. aria_code/apps/cli/tradingview_bridge.py +529 -0
  247. aria_code/apps/cli/transactions.py +269 -0
  248. aria_code/apps/cli/turn_planning.py +137 -0
  249. aria_code/apps/cli/update_check.py +257 -0
  250. aria_code/apps/cli/updater.py +373 -0
  251. aria_code/apps/cli/utils/__init__.py +0 -0
  252. aria_code/apps/cli/utils/market_detect.py +1663 -0
  253. aria_code/apps/cli/web_login.py +136 -0
  254. aria_code/apps/cli/workflow_runner.py +127 -0
  255. aria_code/apps/cli/workspace_route.py +121 -0
  256. aria_code/apps/daemon/README.md +14 -0
  257. aria_code/apps/vscode/README.md +115 -0
  258. aria_code/apps/vscode/package.json +70 -0
  259. aria_code/aria_cli.py +5244 -0
  260. aria_code/aria_daemon.py +1372 -0
  261. aria_code/aria_feishu_bot.py +16 -0
  262. aria_code/aria_mcp_server.py +61 -0
  263. aria_code/aria_relay_client.py +4 -0
  264. aria_code/aria_telegram_bot.py +11 -0
  265. aria_code/ariarc.py +385 -0
  266. aria_code/artifacts.py +581 -0
  267. aria_code/backtest_engine.py +4 -0
  268. aria_code/backtest_report.py +4 -0
  269. aria_code/brokers/__init__.py +78 -0
  270. aria_code/brokers/automation.py +193 -0
  271. aria_code/brokers/base.py +231 -0
  272. aria_code/brokers/capabilities.py +264 -0
  273. aria_code/brokers/cn/__init__.py +10 -0
  274. aria_code/brokers/cn/easytrader_broker.py +193 -0
  275. aria_code/brokers/cn/futu_broker.py +194 -0
  276. aria_code/brokers/cn/longbridge_broker.py +190 -0
  277. aria_code/brokers/cn/tiger_broker.py +196 -0
  278. aria_code/brokers/cn/xtquant_broker.py +225 -0
  279. aria_code/brokers/config.py +414 -0
  280. aria_code/brokers/intl/__init__.py +5 -0
  281. aria_code/brokers/intl/alpaca_broker.py +183 -0
  282. aria_code/brokers/intl/ibkr_broker.py +215 -0
  283. aria_code/brokers/intl/trading212_broker.py +183 -0
  284. aria_code/brokers/intl/webull_broker.py +156 -0
  285. aria_code/brokers/paper_broker.py +266 -0
  286. aria_code/brokers/planning.py +296 -0
  287. aria_code/brokers/registry.py +213 -0
  288. aria_code/brokers/trading.py +267 -0
  289. aria_code/canva_client.py +4 -0
  290. aria_code/change_store.py +127 -0
  291. aria_code/clients/__init__.py +0 -0
  292. aria_code/clients/aliyun_data_client.py +660 -0
  293. aria_code/clients/aria_relay_client.py +16 -0
  294. aria_code/clients/canva_client.py +284 -0
  295. aria_code/clients/figma_client.py +100 -0
  296. aria_code/clients/football_data_client.py +1671 -0
  297. aria_code/clients/kling_video_client.py +182 -0
  298. aria_code/clients/market_data_client.py +1985 -0
  299. aria_code/clients/openai_image_client.py +242 -0
  300. aria_code/clients/runway_video_client.py +157 -0
  301. aria_code/clients/warehouse_erp_client.py +105 -0
  302. aria_code/cloud_memory.py +190 -0
  303. aria_code/command_safety.py +19 -0
  304. aria_code/computer_use_server.py +276 -0
  305. aria_code/computer_use_tools.py +4 -0
  306. aria_code/dashboard_generator.py +579 -0
  307. aria_code/data_analysis_tools.py +4 -0
  308. aria_code/data_cleaner.py +483 -0
  309. aria_code/data_service.py +513 -0
  310. aria_code/datasources/__init__.py +23 -0
  311. aria_code/datasources/base.py +166 -0
  312. aria_code/datasources/router.py +253 -0
  313. aria_code/datasources/sec_statements.py +217 -0
  314. aria_code/datasources/sources/__init__.py +15 -0
  315. aria_code/datasources/sources/akshare_source.py +269 -0
  316. aria_code/datasources/sources/alpha_vantage_source.py +203 -0
  317. aria_code/datasources/sources/edgar_source.py +219 -0
  318. aria_code/datasources/sources/finnhub_source.py +198 -0
  319. aria_code/datasources/sources/fred_source.py +223 -0
  320. aria_code/datasources/sources/tushare_source.py +143 -0
  321. aria_code/datasources/sources/web_scraper_source.py +278 -0
  322. aria_code/datasources/sources/world_bank_source.py +206 -0
  323. aria_code/datasources/sources/yfinance_source.py +152 -0
  324. aria_code/doctor.py +860 -0
  325. aria_code/domain/__init__.py +0 -0
  326. aria_code/domain/_finance_core/__init__.py +5 -0
  327. aria_code/domain/_finance_core/backtest_engine.py +325 -0
  328. aria_code/domain/_finance_core/performance_stats.py +101 -0
  329. aria_code/domain/backtest_engine.py +75 -0
  330. aria_code/domain/backtest_report.py +488 -0
  331. aria_code/domain/finance_formulas.py +389 -0
  332. aria_code/domain/performance_stats.py +6 -0
  333. aria_code/domain/portfolio_ledger.py +307 -0
  334. aria_code/domain/strategy_vault.py +421 -0
  335. aria_code/evals/__init__.py +37 -0
  336. aria_code/evals/behavior.py +113 -0
  337. aria_code/evals/gap.py +93 -0
  338. aria_code/evals/harness.py +949 -0
  339. aria_code/evals/review_probe.py +132 -0
  340. aria_code/evals/runner.py +425 -0
  341. aria_code/evals/scorecard.py +322 -0
  342. aria_code/evals/summary.py +125 -0
  343. aria_code/external_agent_runner.py +115 -0
  344. aria_code/figma_client.py +4 -0
  345. aria_code/file_analysis_tools.py +4 -0
  346. aria_code/finance_formulas.py +4 -0
  347. aria_code/football_data_client.py +4 -0
  348. aria_code/image_gen_tools.py +4 -0
  349. aria_code/intent_classifier.py +459 -0
  350. aria_code/kling_video_client.py +4 -0
  351. aria_code/licensing.py +151 -0
  352. aria_code/local_finance_tools.py +4 -0
  353. aria_code/local_image_provider.py +4 -0
  354. aria_code/local_llm_provider.py +4 -0
  355. aria_code/macro_tools.py +4 -0
  356. aria_code/markdown_pdf.py +289 -0
  357. aria_code/market_data_client.py +4 -0
  358. aria_code/mcp_client.py +744 -0
  359. aria_code/memory_manager.py +273 -0
  360. aria_code/model_capability.py +512 -0
  361. aria_code/notification_tools.py +4 -0
  362. aria_code/openai_image_client.py +4 -0
  363. aria_code/packages/__init__.py +26 -0
  364. aria_code/packages/adk_bridge/__init__.py +11 -0
  365. aria_code/packages/adk_bridge/code_review_tools.py +49 -0
  366. aria_code/packages/adk_bridge/market_tools.py +125 -0
  367. aria_code/packages/aria_agents/__init__.py +5 -0
  368. aria_code/packages/aria_agents/manifest.py +78 -0
  369. aria_code/packages/aria_core/__init__.py +34 -0
  370. aria_code/packages/aria_core/architecture.py +269 -0
  371. aria_code/packages/aria_core/export.py +147 -0
  372. aria_code/packages/aria_core/manifest.py +65 -0
  373. aria_code/packages/aria_core/paths.py +57 -0
  374. aria_code/packages/aria_core/secure_file.py +68 -0
  375. aria_code/packages/aria_infra/__init__.py +15 -0
  376. aria_code/packages/aria_infra/arthera.py +58 -0
  377. aria_code/packages/aria_infra/doctor.py +246 -0
  378. aria_code/packages/aria_infra/product.py +40 -0
  379. aria_code/packages/aria_mcp/__init__.py +25 -0
  380. aria_code/packages/aria_mcp/bridge.py +232 -0
  381. aria_code/packages/aria_mcp/circuit.py +82 -0
  382. aria_code/packages/aria_mcp/config.py +162 -0
  383. aria_code/packages/aria_mcp/installer.py +95 -0
  384. aria_code/packages/aria_mcp/server.py +1705 -0
  385. aria_code/packages/aria_mcp/tools.py +81 -0
  386. aria_code/packages/aria_sdk/__init__.py +19 -0
  387. aria_code/packages/aria_sdk/client.py +393 -0
  388. aria_code/packages/aria_sdk/providers.py +70 -0
  389. aria_code/packages/aria_sdk/streaming.py +75 -0
  390. aria_code/packages/aria_sdk/types.py +86 -0
  391. aria_code/packages/aria_services/__init__.py +92 -0
  392. aria_code/packages/aria_services/cache.py +60 -0
  393. aria_code/packages/aria_services/context.py +356 -0
  394. aria_code/packages/aria_services/context_checkpoints.py +157 -0
  395. aria_code/packages/aria_services/data.py +11 -0
  396. aria_code/packages/aria_services/provider_health.py +304 -0
  397. aria_code/packages/aria_services/references.py +247 -0
  398. aria_code/packages/aria_services/registry.py +249 -0
  399. aria_code/packages/aria_services/research_protocol.py +137 -0
  400. aria_code/packages/aria_services/research_quality.py +186 -0
  401. aria_code/packages/aria_services/settings.py +160 -0
  402. aria_code/packages/aria_services/usage.py +151 -0
  403. aria_code/packages/aria_skills/__init__.py +46 -0
  404. aria_code/packages/aria_skills/catalog.py +149 -0
  405. aria_code/packages/aria_skills/loader.py +455 -0
  406. aria_code/packages/aria_skills/registry.py +84 -0
  407. aria_code/packages/aria_tools/__init__.py +5 -0
  408. aria_code/packages/aria_tools/financial/__init__.py +5 -0
  409. aria_code/packages/aria_tools/financial/_paths.py +92 -0
  410. aria_code/packages/aria_tools/registry.py +129 -0
  411. aria_code/packages/quant_engine/__init__.py +33 -0
  412. aria_code/packages/quant_engine/_namespace.py +24 -0
  413. aria_code/packages/quant_engine/backtest/__init__.py +10 -0
  414. aria_code/packages/quant_engine/backtest/core.py +53 -0
  415. aria_code/packages/quant_engine/backtest/engine.py +102 -0
  416. aria_code/packages/quant_engine/portfolio/__init__.py +9 -0
  417. aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
  418. aria_code/packages/quant_engine/sports/__init__.py +76 -0
  419. aria_code/packages/quant_engine/sports/calibrator.py +354 -0
  420. aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
  421. aria_code/packages/quant_engine/sports/elo.py +300 -0
  422. aria_code/packages/quant_engine/sports/form.py +188 -0
  423. aria_code/packages/quant_engine/sports/h2h.py +195 -0
  424. aria_code/packages/quant_engine/sports/predictor.py +312 -0
  425. aria_code/packages/quant_engine/sports/tracker.py +665 -0
  426. aria_code/packages/quant_engine/stochastic/__init__.py +31 -0
  427. aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  428. aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
  429. aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  430. aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  431. aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
  432. aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  433. aria_code/packs/__init__.py +85 -0
  434. aria_code/packs/base.py +168 -0
  435. aria_code/packs/finance/__init__.py +250 -0
  436. aria_code/packs/healthcare/__init__.py +40 -0
  437. aria_code/packs/logistics/__init__.py +171 -0
  438. aria_code/packs/payments/__init__.py +139 -0
  439. aria_code/packs/realty/__init__.py +190 -0
  440. aria_code/packs/registry.py +202 -0
  441. aria_code/packs/rules.py +90 -0
  442. aria_code/plan_utils.py +194 -0
  443. aria_code/plugin_loader.py +328 -0
  444. aria_code/portfolio_ledger.py +4 -0
  445. aria_code/preview_server.py +449 -0
  446. aria_code/privacy/__init__.py +5 -0
  447. aria_code/privacy/feedback.py +123 -0
  448. aria_code/project_review.py +278 -0
  449. aria_code/project_review_api.py +235 -0
  450. aria_code/project_review_server.py +18 -0
  451. aria_code/project_tools.py +4 -0
  452. aria_code/providers/__init__.py +30 -0
  453. aria_code/providers/google_video_provider.py +256 -0
  454. aria_code/providers/llm/__init__.py +19 -0
  455. aria_code/providers/llm/anthropic.py +184 -0
  456. aria_code/providers/llm/arthera_gateway.py +90 -0
  457. aria_code/providers/llm/autoconfig.py +213 -0
  458. aria_code/providers/llm/base.py +139 -0
  459. aria_code/providers/llm/custom.py +158 -0
  460. aria_code/providers/llm/ollama.py +137 -0
  461. aria_code/providers/llm/openai_compat.py +350 -0
  462. aria_code/providers/llm/registry.py +457 -0
  463. aria_code/providers/local_image_provider.py +202 -0
  464. aria_code/providers/local_llm_provider.py +549 -0
  465. aria_code/providers/ollama_models.py +79 -0
  466. aria_code/providers/openai_params.py +32 -0
  467. aria_code/providers/tool_messages.py +100 -0
  468. aria_code/realty_data_tools.py +4 -0
  469. aria_code/report_exporters.py +150 -0
  470. aria_code/report_generator.py +1764 -0
  471. aria_code/review_service.py +497 -0
  472. aria_code/runtime/__init__.py +176 -0
  473. aria_code/runtime/acceptance.py +632 -0
  474. aria_code/runtime/agent_loop.py +1851 -0
  475. aria_code/runtime/agent_profiles.py +75 -0
  476. aria_code/runtime/approval.py +71 -0
  477. aria_code/runtime/baseline.py +219 -0
  478. aria_code/runtime/budget.py +232 -0
  479. aria_code/runtime/checkpoints.py +481 -0
  480. aria_code/runtime/consult.py +65 -0
  481. aria_code/runtime/contract.py +282 -0
  482. aria_code/runtime/delivery.py +432 -0
  483. aria_code/runtime/events.py +113 -0
  484. aria_code/runtime/gateway.py +167 -0
  485. aria_code/runtime/lsp.py +484 -0
  486. aria_code/runtime/native_index.py +153 -0
  487. aria_code/runtime/orchestrator.py +55 -0
  488. aria_code/runtime/processes.py +343 -0
  489. aria_code/runtime/project_graph.py +614 -0
  490. aria_code/runtime/repo_map.py +712 -0
  491. aria_code/runtime/research_bundle.py +105 -0
  492. aria_code/runtime/review.py +269 -0
  493. aria_code/runtime/run_state.py +127 -0
  494. aria_code/runtime/run_store.py +522 -0
  495. aria_code/runtime/scoped_runs.py +113 -0
  496. aria_code/runtime/self_healing.py +422 -0
  497. aria_code/runtime/semantic_diff.py +171 -0
  498. aria_code/runtime/subagent.py +721 -0
  499. aria_code/runtime/symbol_edit.py +117 -0
  500. aria_code/runtime/task_brief.py +46 -0
  501. aria_code/runtime/task_context.py +145 -0
  502. aria_code/runtime/task_ledger.py +108 -0
  503. aria_code/runtime/task_orchestrator.py +90 -0
  504. aria_code/runtime/task_outcome.py +33 -0
  505. aria_code/runtime/task_verification.py +266 -0
  506. aria_code/runtime/task_worktree.py +448 -0
  507. aria_code/runtime/tool_executor.py +329 -0
  508. aria_code/runtime/tool_policy.py +107 -0
  509. aria_code/runtime/transactions.py +216 -0
  510. aria_code/runtime/windows_job.py +115 -0
  511. aria_code/runtime/workflows.py +222 -0
  512. aria_code/runtime/worktrees.py +215 -0
  513. aria_code/runway_video_client.py +4 -0
  514. aria_code/safety/__init__.py +27 -0
  515. aria_code/safety/client_isolation.py +178 -0
  516. aria_code/safety/permissions.py +339 -0
  517. aria_code/safety/risk.py +699 -0
  518. aria_code/safety/sandbox.py +148 -0
  519. aria_code/safety/service.py +102 -0
  520. aria_code/service_endpoints.py +14 -0
  521. aria_code/setup_wizard.py +684 -0
  522. aria_code/spreadsheet_tools.py +4 -0
  523. aria_code/strategy_vault.py +4 -0
  524. aria_code/tools/__init__.py +0 -0
  525. aria_code/tools/artifact_tools.py +252 -0
  526. aria_code/tools/broker_tools.py +92 -0
  527. aria_code/tools/build_quant_engine.py +97 -0
  528. aria_code/tools/code_audit_tools.py +192 -0
  529. aria_code/tools/computer_use_tools.py +509 -0
  530. aria_code/tools/data_analysis_tools.py +831 -0
  531. aria_code/tools/enterprise_finance_tools.py +202 -0
  532. aria_code/tools/extended_tools.py +240 -0
  533. aria_code/tools/file_analysis_tools.py +880 -0
  534. aria_code/tools/image_gen_tools.py +114 -0
  535. aria_code/tools/local_finance_tools.py +3488 -0
  536. aria_code/tools/logistics_carriers.py +11 -0
  537. aria_code/tools/logistics_inventory.py +11 -0
  538. aria_code/tools/logistics_io.py +11 -0
  539. aria_code/tools/logistics_tenancy.py +11 -0
  540. aria_code/tools/logistics_tools.py +11 -0
  541. aria_code/tools/macro_tools.py +412 -0
  542. aria_code/tools/notification_tools.py +249 -0
  543. aria_code/tools/project_tools.py +525 -0
  544. aria_code/tools/realty_data_tools.py +662 -0
  545. aria_code/tools/spreadsheet_tools.py +399 -0
  546. aria_code/tools/stripe_tools.py +115 -0
  547. aria_code/tools/video_tools.py +500 -0
  548. aria_code/training/__init__.py +1 -0
  549. aria_code/training/compare.py +117 -0
  550. aria_code/training/prelude_commands.py +111 -0
  551. aria_code/training/prelude_eval.py +390 -0
  552. aria_code/training/prelude_real.py +183 -0
  553. aria_code/training/prelude_train.py +248 -0
  554. aria_code/training/sft_dataset.py +230 -0
  555. aria_code/training/trajectory_dataset.py +263 -0
  556. aria_code/ui/__init__.py +105 -0
  557. aria_code/ui/banner.py +404 -0
  558. aria_code/ui/completer.py +571 -0
  559. aria_code/ui/console.py +285 -0
  560. aria_code/ui/input_box.py +505 -0
  561. aria_code/ui/mentions.py +206 -0
  562. aria_code/ui/picker.py +248 -0
  563. aria_code/ui/render/__init__.py +11 -0
  564. aria_code/ui/render/actions.py +297 -0
  565. aria_code/ui/render/finance.py +1732 -0
  566. aria_code/ui/render/market.py +253 -0
  567. aria_code/ui/render/output.py +954 -0
  568. aria_code/ui/render/responsive.py +75 -0
  569. aria_code/ui/render/team.py +367 -0
  570. aria_code/ui/robot.py +239 -0
  571. aria_code/ui/startup_dashboard.py +110 -0
  572. aria_code/video_analysis.py +147 -0
  573. aria_code/video_editor.py +518 -0
  574. aria_code/video_workflow/__init__.py +8 -0
  575. aria_code/video_workflow/__main__.py +61 -0
  576. aria_code/video_workflow/engines/__init__.py +18 -0
  577. aria_code/video_workflow/engines/base.py +69 -0
  578. aria_code/video_workflow/engines/custom_model.py +204 -0
  579. aria_code/video_workflow/engines/google_veo.py +94 -0
  580. aria_code/video_workflow/engines/local_engine.py +71 -0
  581. aria_code/video_workflow/engines/registry.py +98 -0
  582. aria_code/video_workflow/google_models.py +278 -0
  583. aria_code/video_workflow/music/__init__.py +5 -0
  584. aria_code/video_workflow/music/foley_service.py +132 -0
  585. aria_code/video_workflow/music/service.py +212 -0
  586. aria_code/video_workflow/pipeline.py +620 -0
  587. aria_code/video_workflow/plan.py +202 -0
  588. aria_code/video_workflow/session.py +297 -0
  589. aria_code/workspace/__init__.py +6 -0
  590. aria_code/workspace/files.py +219 -0
  591. aria_code/workspace/verify.py +167 -0
  592. aria_code/workspace_changes.py +199 -0
  593. aria_code/workspace_service.py +196 -0
  594. aria_code/workspace_tools.py +150 -0
  595. aria_code-0.167.0.dist-info/METADATA +330 -0
  596. aria_code-0.167.0.dist-info/RECORD +601 -0
  597. aria_code-0.167.0.dist-info/WHEEL +5 -0
  598. aria_code-0.167.0.dist-info/entry_points.txt +5 -0
  599. aria_code-0.167.0.dist-info/licenses/LICENSE +201 -0
  600. aria_code-0.167.0.dist-info/licenses/NOTICE +49 -0
  601. aria_code-0.167.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,1732 @@
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,
973
+ lang: str = "zh") -> None:
974
+ """Render US or CN macro result dict."""
975
+ zh = str(lang).lower().startswith("zh")
976
+ if not r.get("success"):
977
+ error = r.get("error", "数据获取失败" if zh else "data unavailable")
978
+ if "akshare" in str(error):
979
+ # China data is optional; a missing package is a note, not a red error.
980
+ error = ("中国宏观数据需要 akshare:pip install akshare" if zh
981
+ else "China macro data needs akshare: pip install akshare")
982
+ if has_rich: console.print(f" [dim]{error}[/dim]")
983
+ else: print(f" {error}")
984
+ return
985
+ if has_rich: console.print(f" [red]{error}[/red]")
986
+ else: print(f" {error}")
987
+ return
988
+ data = r.get("data", {})
989
+ if has_rich:
990
+ from rich.table import Table
991
+ from rich.rule import Rule
992
+ console.print(Rule(f"[bold]{title}[/bold]", style="dim"))
993
+ t = Table(show_header=True, box=None, padding=(0, 1))
994
+ t.add_column("指标" if zh else "Indicator", style="dim", min_width=20)
995
+ t.add_column("最新值" if zh else "Latest", justify="right", min_width=10)
996
+ t.add_column("环比变化" if zh else "Change", justify="right")
997
+ t.add_column("时间" if zh else "Date", style="dim")
998
+ for key, item in data.items():
999
+ if key.startswith("_"): continue
1000
+ if not isinstance(item, dict): continue
1001
+ latest = item.get("latest", {}) or {}
1002
+ val = latest.get("value")
1003
+ date = latest.get("date", "")
1004
+ change = item.get("change")
1005
+ unit = item.get("unit", "")
1006
+ label = item.get("label", key) if zh else item.get("label_en", item.get("label", key))
1007
+ if val is None: continue
1008
+ val_str = f"{val:.2f}{unit}"
1009
+ if change is not None:
1010
+ color = "green" if change > 0 else "red" if change < 0 else ""
1011
+ chg_str = f"[{color}]{change:+.3f}[/{color}]" if color else f"{change:+.3f}"
1012
+ else:
1013
+ chg_str = "—"
1014
+ t.add_row(label, val_str, chg_str, str(date)[:10])
1015
+ console.print(t)
1016
+ yc = data.get("_yield_curve", {})
1017
+ if yc:
1018
+ sp = yc.get("spread_10y_2y", 0)
1019
+ shape = yc.get("shape", "") if zh else yc.get("shape_en", yc.get("shape", ""))
1020
+ color = "green" if sp > 0 else "red"
1021
+ if zh:
1022
+ console.print(f" 收益率曲线: [{color}]{shape}[/{color}] 10Y-2Y利差: [{color}]{sp:+.3f}%[/{color}]")
1023
+ else:
1024
+ console.print(f" Yield curve: [{color}]{shape}[/{color}] 10Y-2Y spread: [{color}]{sp:+.3f}%[/{color}]")
1025
+ else:
1026
+ print(f"\n{title}")
1027
+ for key, item in data.items():
1028
+ if not isinstance(item, dict) or key.startswith("_"): continue
1029
+ v = (item.get("latest") or {}).get("value")
1030
+ if v is not None:
1031
+ print(f" {(item.get('label', key) if zh else item.get('label_en', key)):<28} {v:.3g}")
1032
+
1033
+
1034
+ def render_cb_rates(r: dict, *, console=None, has_rich: bool = True, lang: str = "zh") -> None:
1035
+ """Render central bank rates, each with its date; a stale value is marked, not shown as current."""
1036
+ zh = str(lang).lower().startswith("zh")
1037
+ if not r.get("success"):
1038
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
1039
+ return
1040
+ details = r.get("details") or [
1041
+ {"label_zh": name, "label_en": name, "value": val, "date": "", "stale": False}
1042
+ for name, val in (r.get("rates") or {}).items()
1043
+ ]
1044
+ title = "主要央行政策利率" if zh else "Central bank rates"
1045
+ if has_rich:
1046
+ from rich.rule import Rule
1047
+ from rich.table import Table
1048
+ console.print(Rule(f"[bold]{title}[/bold]", style="dim"))
1049
+ t = Table(show_header=False, box=None, padding=(0,1))
1050
+ t.add_column(style="dim", min_width=28)
1051
+ t.add_column(justify="right")
1052
+ t.add_column(style="dim")
1053
+ for row in details:
1054
+ if row.get("value") is None:
1055
+ continue
1056
+ label = row["label_zh"] if zh else row["label_en"]
1057
+ if row.get("stale"):
1058
+ note = (f"数据停在 {row['date']},未显示" if zh else f"no data since {row['date']}, not shown")
1059
+ t.add_row(label, "—", note)
1060
+ else:
1061
+ t.add_row(label, f"[bold]{row['value']:.2f}%[/bold]", row.get("date", ""))
1062
+ console.print(t)
1063
+ else:
1064
+ print("\n" + title)
1065
+ for row in details:
1066
+ if row.get("value") is not None and not row.get("stale"):
1067
+ print(f" {(row['label_zh'] if zh else row['label_en']):<30} {row['value']:.2f}% {row.get('date', '')}")
1068
+
1069
+
1070
+ def render_econ_calendar(r: dict, *, console=None, has_rich: bool = True) -> None:
1071
+ """Render economic calendar."""
1072
+ events = r.get("events", [])
1073
+ if has_rich:
1074
+ from rich.rule import Rule
1075
+ from rich.table import Table
1076
+ console.print(Rule("[bold]📅 经济事件日历[/bold]", style="dim"))
1077
+ t = Table(show_header=True, box=None, padding=(0,1))
1078
+ t.add_column("时间", style="dim", width=12)
1079
+ t.add_column("事件", min_width=30)
1080
+ t.add_column("重要性", justify="center")
1081
+ for ev in events[:15]:
1082
+ imp = ev.get("importance", ev.get("importance_level",""))
1083
+ imp_colored = (
1084
+ "[red]HIGH[/red]" if str(imp).upper() in ("HIGH","3","★★★")
1085
+ else "[yellow]MED[/yellow]" if str(imp).upper() in ("MEDIUM","2","★★")
1086
+ else f"[dim]{imp}[/dim]"
1087
+ )
1088
+ t.add_row(
1089
+ str(ev.get("time","") or ev.get("date",""))[:12],
1090
+ str(ev.get("event","") or ev.get("title",""))[:45],
1091
+ imp_colored,
1092
+ )
1093
+ console.print(t)
1094
+ else:
1095
+ for ev in events[:10]:
1096
+ print(f" {ev.get('event','')} [{ev.get('importance','')}]")
1097
+
1098
+
1099
+ def render_options_chain(r: dict, *, console=None, has_rich: bool = True) -> None:
1100
+ """Render options chain."""
1101
+ if not r.get("success"):
1102
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
1103
+ return
1104
+ symbol = r.get("symbol","")
1105
+ price = r.get("price", 0)
1106
+ expiry = r.get("expiry","")
1107
+ all_exp = r.get("all_expiries", [])
1108
+ if has_rich:
1109
+ from rich.table import Table
1110
+ from rich.rule import Rule
1111
+ console.print(Rule(f"[bold]{symbol}[/bold] 期权链 到期: [cyan]{expiry}[/cyan] 现价: [bold]{price:.2f}[/bold]", style="dim"))
1112
+ if all_exp:
1113
+ console.print(f" [dim]可用到期日: {', '.join(all_exp)}[/dim]")
1114
+ for side in ("calls", "puts"):
1115
+ rows = r.get(side, [])
1116
+ if not rows: continue
1117
+ label = "认购期权 (Calls)" if side == "calls" else "认沽期权 (Puts)"
1118
+ t = Table(title=f"[bold]{label}[/bold]", show_header=True, box=None, padding=(0,1))
1119
+ t.add_column("行权价", justify="right", style="bold")
1120
+ t.add_column("最新价", justify="right")
1121
+ t.add_column("买/卖", justify="right", style="dim")
1122
+ t.add_column("IV%", justify="right")
1123
+ t.add_column("OI", justify="right")
1124
+ t.add_column("价内?", justify="center")
1125
+ for row in rows:
1126
+ itm = row.get("inTheMoney", False)
1127
+ itm_s = "[green]✓[/green]" if itm else "[dim]—[/dim]"
1128
+ bid = row.get("bid", 0) or 0
1129
+ ask = row.get("ask", 0) or 0
1130
+ t.add_row(
1131
+ f"{row.get('strike',0):.2f}",
1132
+ f"{row.get('lastPrice',0):.2f}",
1133
+ f"{bid:.2f}/{ask:.2f}",
1134
+ f"{row.get('iv_pct',0):.1f}%",
1135
+ f"{int(row.get('openInterest',0)):,}",
1136
+ itm_s,
1137
+ )
1138
+ console.print(t)
1139
+ else:
1140
+ for side in ("calls","puts"):
1141
+ print(f"\n{side.upper()}")
1142
+ for row in r.get(side, []):
1143
+ print(f" K={row.get('strike')} last={row.get('lastPrice')} IV={row.get('iv_pct')}%")
1144
+
1145
+
1146
+ def render_quality_scores(symbol: str, f_r: dict, z_r: dict, *, console=None, has_rich: bool = True) -> None:
1147
+ """Render Piotroski F-Score + Altman Z-Score side by side."""
1148
+ if has_rich:
1149
+ from rich.table import Table
1150
+ from rich.rule import Rule
1151
+ from rich.columns import Columns
1152
+ from rich.panel import Panel
1153
+ from rich import box as _rbox
1154
+
1155
+ console.print(Rule(f"[bold]{symbol}[/bold] 财务质量双维评估", style="dim"))
1156
+
1157
+ # F-Score panel
1158
+ if f_r.get("success"):
1159
+ fs = f_r["f_score"]
1160
+ sig = f_r.get("signal","")
1161
+ color = "green" if sig == "bullish" else "red" if sig == "bearish" else "yellow"
1162
+ bars = "█" * fs + "░" * (9 - fs)
1163
+ f_body = (
1164
+ f"[bold {color}]{fs}/9[/bold {color}] [{color}]{f_r.get('verdict','')}[/{color}]\n"
1165
+ f"[{color}]{bars}[/{color}]\n\n"
1166
+ )
1167
+ scores = f_r.get("scores", {})
1168
+ categories = [
1169
+ ("盈利能力", ["F1_ROA_positive","F2_CFO_positive","F3_ROA_increasing","F4_CFO_gt_ROA"]),
1170
+ ("杠杆/流动性", ["F5_Leverage_lower","F6_CurrentRatio_up","F7_NoDilution"]),
1171
+ ("运营效率", ["F8_GrossMargin_up","F9_AssetTurnover_up"]),
1172
+ ]
1173
+ for cat, keys in categories:
1174
+ f_body += f"[dim]{cat}[/dim]\n"
1175
+ for k in keys:
1176
+ v = scores.get(k, 0)
1177
+ check = "[green]✓[/green]" if v else "[dim]✗[/dim]"
1178
+ f_body += f" {check} {k[3:].replace('_',' ')}\n"
1179
+ f_panel = Panel(f_body.strip(), title="[bold]Piotroski F-Score[/bold]",
1180
+ border_style=color, box=_rbox.ROUNDED, padding=(0,1))
1181
+ else:
1182
+ f_panel = Panel(f"[red]{f_r.get('error','失败')}[/red]",
1183
+ title="Piotroski F-Score", border_style="red")
1184
+
1185
+ # Z-Score panel
1186
+ if z_r.get("success"):
1187
+ zs = z_r["z_score"]
1188
+ risk = z_r.get("risk","medium")
1189
+ zone = z_r.get("zone","")
1190
+ zcolor = "green" if risk == "low" else "red" if risk == "high" else "yellow"
1191
+ z_body = (
1192
+ f"[bold {zcolor}]Z'' = {zs:.3f}[/bold {zcolor}]\n"
1193
+ f"[{zcolor}]{zone}[/{zcolor}]\n\n"
1194
+ f"[dim]安全区 >2.6 | 灰色区 1.1-2.6 | 风险区 <1.1[/dim]\n\n"
1195
+ )
1196
+ comp = z_r.get("components", {})
1197
+ labels = {
1198
+ "X1_working_capital_ratio": "X1 营运资本/总资产",
1199
+ "X2_retained_earnings_ratio": "X2 留存收益/总资产",
1200
+ "X3_ebit_ratio": "X3 EBIT/总资产",
1201
+ "X4_equity_to_debt": "X4 权益/负债",
1202
+ }
1203
+ weights = {"X1": 6.56, "X2": 3.26, "X3": 6.72, "X4": 1.05}
1204
+ for k, label in labels.items():
1205
+ v = comp.get(k, 0)
1206
+ prefix = k[:2]
1207
+ contrib = round(v * weights.get(prefix, 1), 3)
1208
+ c = "green" if v >= 0 else "red"
1209
+ z_body += f" [dim]{label}[/dim]: [{c}]{v:.4f}[/{c}] (贡献 {contrib:+.3f})\n"
1210
+ z_body += f"\n[dim]{z_r.get('formula','')}[/dim]"
1211
+ z_panel = Panel(z_body.strip(), title="[bold]Altman Z''-Score[/bold]",
1212
+ border_style=zcolor, box=_rbox.ROUNDED, padding=(0,1))
1213
+ else:
1214
+ z_panel = Panel(f"[red]{z_r.get('error','失败')}[/red]",
1215
+ title="Altman Z-Score", border_style="red")
1216
+
1217
+ console.print(Columns([f_panel, z_panel]))
1218
+ else:
1219
+ if f_r.get("success"):
1220
+ print(f"\nPiotroski F-Score: {f_r['f_score']}/9 {f_r.get('verdict','')}")
1221
+ if z_r.get("success"):
1222
+ print(f"Altman Z''-Score: {z_r['z_score']} {z_r.get('zone','')}")
1223
+
1224
+
1225
+ def render_ichimoku(r: dict, *, console=None, has_rich: bool = True) -> None:
1226
+ """Render Ichimoku Cloud analysis."""
1227
+ if not r.get("success"):
1228
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
1229
+ return
1230
+ if has_rich:
1231
+ from rich.table import Table
1232
+ from rich.rule import Rule
1233
+ sym = r.get("symbol","")
1234
+ sig = r.get("signal","")
1235
+ price = r.get("price", 0)
1236
+ is_bull = r.get("above_cloud", False)
1237
+ sig_color = "green" if is_bull else "red" if r.get("below_cloud") else "yellow"
1238
+ console.print(Rule(f"[bold]{sym}[/bold] 一目均衡表 [{sig_color}]{sig}[/{sig_color}]", style="dim"))
1239
+ t = Table(show_header=False, box=None, padding=(0,1))
1240
+ t.add_column(style="dim", width=18)
1241
+ t.add_column(justify="right")
1242
+ t.add_column(style="dim")
1243
+ t.add_row("现价", f"[bold]{price:.3f}[/bold]", "")
1244
+ t.add_row("转换线 (9)", f"{r.get('tenkan',0):.3f}",
1245
+ "[green]↑ 多头[/green]" if r.get('tenkan',0) > r.get('kijun',0) else "[red]↓ 空头[/red]")
1246
+ t.add_row("基准线 (26)",f"{r.get('kijun',0):.3f}", "")
1247
+ if r.get("senkou_a"):
1248
+ t.add_row("先行带A", f"{r.get('senkou_a',0):.3f}", "")
1249
+ if r.get("senkou_b"):
1250
+ t.add_row("先行带B", f"{r.get('senkou_b',0):.3f}", "")
1251
+ t.add_row("云层厚度", f"{r.get('cloud_thickness',0):.3f}", r.get("cloud_color",""))
1252
+ t.add_row("TK交叉", r.get("tk_cross",""), "")
1253
+ console.print(t)
1254
+ console.print(f" [bold {sig_color}]结论: {sig}[/bold {sig_color}] [dim]先行带偏移 26期,迟行线(Chikou)={r.get('chikou',0):.3f}[/dim]")
1255
+ else:
1256
+ print(f"\n{r.get('symbol','')} 一目均衡表")
1257
+ for k in ("tenkan","kijun","senkou_a","senkou_b","signal"):
1258
+ print(f" {k}: {r.get(k,'')}")
1259
+
1260
+
1261
+ def render_fear_greed(r: dict, *, console=None, has_rich: bool = True) -> None:
1262
+ """Render Fear & Greed Index with ASCII gauge."""
1263
+ if not r.get("success"):
1264
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
1265
+ return
1266
+ val = r.get("value", 50)
1267
+ label = r.get("label", "")
1268
+ sig = r.get("signal", "中性")
1269
+ if has_rich:
1270
+ from rich.rule import Rule
1271
+ color = "green" if val <= 25 else "red" if val >= 75 else "yellow"
1272
+ # ASCII gauge
1273
+ filled = int(val / 5)
1274
+ bar = "█" * filled + "░" * (20 - filled)
1275
+ console.print(Rule("[bold]₿ 加密恐惧贪婪指数[/bold]", style="dim"))
1276
+ console.print(f"\n [{color}]{bar}[/{color}] [bold {color}]{val}/100[/bold {color}] [{color}]{label}[/{color}]\n")
1277
+ console.print(f" 操作信号: [bold]{sig}[/bold] [dim]>75 极度贪婪(卖出信号) <25 极度恐惧(买入信号)[/dim]\n")
1278
+ hist = r.get("history", [])
1279
+ if len(hist) > 1:
1280
+ hist_str = " [dim]近7天: "
1281
+ for h in hist[:7]:
1282
+ v = h.get("value", 0)
1283
+ c = "green" if v <= 25 else "red" if v >= 75 else "yellow"
1284
+ hist_str += f"[{c}]{v}[/{c}] "
1285
+ console.print(hist_str + "[/dim]")
1286
+ else:
1287
+ print(f"\n恐惧贪婪指数: {val}/100 ({label}) 信号: {sig}")
1288
+
1289
+
1290
+ def render_funding_rates(r: dict, *, console=None, has_rich: bool = True) -> None:
1291
+ """Render perpetual funding rates."""
1292
+ if not r.get("success"):
1293
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
1294
+ return
1295
+ rates = r.get("rates", [])
1296
+ if has_rich:
1297
+ from rich.table import Table
1298
+ from rich.rule import Rule
1299
+ exchange = r.get("exchange","")
1300
+ bias = r.get("market_bias","")
1301
+ console.print(Rule(f"[bold]{exchange.upper()}[/bold] 永续合约资金费率", style="dim"))
1302
+ t = Table(show_header=True, box=None, padding=(0,1))
1303
+ t.add_column("合约", style="bold", width=12)
1304
+ t.add_column("费率", justify="right")
1305
+ t.add_column("年化", justify="right")
1306
+ t.add_column("下次结算", style="dim")
1307
+ t.add_column("信号", justify="center")
1308
+ for rt in rates:
1309
+ rate = rt.get("rate", 0)
1310
+ color = "red" if rate > 0.05 else "green" if rate < -0.01 else "dim"
1311
+ sig_s = rt.get("signal","中性")
1312
+ sig_c = "red" if sig_s == "空" else "green" if sig_s == "多" else "dim"
1313
+ t.add_row(
1314
+ rt.get("symbol",""),
1315
+ f"[{color}]{rt.get('rate_pct','')}[/{color}]",
1316
+ rt.get("annualized",""),
1317
+ rt.get("next_funding","")[:12],
1318
+ f"[{sig_c}]{sig_s}[/{sig_c}]",
1319
+ )
1320
+ console.print(t)
1321
+ console.print(f" [dim]市场偏向: [bold]{bias}[/bold] 正费率=多头付费给空头,负费率=空头付费给多头[/dim]")
1322
+ else:
1323
+ for rt in rates:
1324
+ print(f" {rt.get('symbol','')} {rt.get('rate_pct','')} (年化{rt.get('annualized','')})")
1325
+
1326
+
1327
+ def render_peer_comparison(r: dict, *, console=None, has_rich: bool = True) -> None:
1328
+ """Render peer comparison table."""
1329
+ if not r.get("success"):
1330
+ if has_rich: console.print(f" [red]{r.get('error')}[/red]")
1331
+ return
1332
+ rows = r.get("table", [])
1333
+ if has_rich:
1334
+ from rich.table import Table
1335
+ from rich.rule import Rule
1336
+ symbol = r.get("symbol","")
1337
+ zh = str(r.get("lang", "zh")).lower().startswith("zh")
1338
+ console.print(Rule(f"[bold]{symbol}[/bold] " + ("同行估值对比" if zh else "peer valuation"), style="dim"))
1339
+ t = Table(show_header=True, box=None, padding=(0,1))
1340
+ t.add_column("代码" if zh else "Symbol", style="bold", width=8)
1341
+ t.add_column("名称" if zh else "Name", width=14)
1342
+ t.add_column("PE", justify="right")
1343
+ t.add_column("PB", justify="right")
1344
+ t.add_column("ROE%", justify="right")
1345
+ t.add_column("股息%" if zh else "Div%", justify="right")
1346
+ t.add_column("市值(B)" if zh else "Mkt cap (B)", justify="right", style="dim")
1347
+ for row in rows:
1348
+ is_t = row.get("is_target", False)
1349
+ pe = f"{row['pe']:.1f}" if row.get("pe") else "—"
1350
+ pb = f"{row['pb']:.2f}" if row.get("pb") else "—"
1351
+ roe = f"{row['roe_pct']:.1f}" if row.get("roe_pct") else "—"
1352
+ dy = f"{row['div_yield']:.2f}" if row.get("div_yield") else "—"
1353
+ mc = f"{row['market_cap_b']:.0f}" if row.get("market_cap_b") else "—"
1354
+ # Highlight target row
1355
+ style = "bold cyan" if is_t else ""
1356
+ t.add_row(
1357
+ f"[{style}]{row['symbol']}[/{style}]" if style else row["symbol"],
1358
+ (row.get("name","") or "")[:14],
1359
+ pe, pb, roe, dy, mc,
1360
+ )
1361
+ console.print(t)
1362
+ analysis = r.get("analysis", [])
1363
+ for line in analysis:
1364
+ console.print(f" [dim]▸ {line}[/dim]")
1365
+ else:
1366
+ zh = str(r.get("lang", "zh")).lower().startswith("zh")
1367
+ print(f"\n{r.get('symbol','')} " + ("同行对比" if zh else "peer comparison"))
1368
+ for row in rows:
1369
+ print(f" {row['symbol']:<8} PE:{row.get('pe','—')} PB:{row.get('pb','—')}")
1370
+
1371
+
1372
+ # ── 不动产渲染函数 ─────────────────────────────────────────────────────────────
1373
+
1374
+ def render_house_price(r: dict, *, console=None, has_rich: bool = True) -> None:
1375
+ if not has_rich:
1376
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1377
+ if not r.get("success"):
1378
+ console.print(f"[red]{r.get('error','获取失败')}[/red]"); return
1379
+ from rich.table import Table as _T
1380
+ from rich import box as _box
1381
+ c1 = str(r.get("city1") or "城市1")
1382
+ c2 = str(r.get("city2") or "城市2")
1383
+ lc1, lc2 = r.get("latest_city1") or {}, r.get("latest_city2") or {}
1384
+
1385
+ def _fmt(v):
1386
+ if v is None: return "[dim]—[/dim]"
1387
+ fv = float(v) if not isinstance(v, float) else v
1388
+ color = "green" if fv > 0 else "red" if fv < 0 else "dim"
1389
+ return f"[{color}]{fv:+.2f}%[/{color}]"
1390
+
1391
+ tb = _T(title="[bold]🏠 房价指数对比[/bold]", box=_box.ROUNDED, show_header=True)
1392
+ tb.add_column("指标", style="dim")
1393
+ tb.add_column(c1, justify="right")
1394
+ tb.add_column(c2, justify="right")
1395
+ tb.add_row("新建商品房同比", _fmt(lc1.get("new_yoy")), _fmt(lc2.get("new_yoy")))
1396
+ tb.add_row("新建商品房环比", _fmt(lc1.get("new_mom")), _fmt(lc2.get("new_mom")))
1397
+ tb.add_row("二手房价同比", _fmt(lc1.get("second_yoy")), _fmt(lc2.get("second_yoy")))
1398
+ tb.add_row("二手房价环比", _fmt(lc1.get("second_mom")), _fmt(lc2.get("second_mom")))
1399
+ tb.add_row("[dim]数据期[/dim]", str(lc1.get("date") or "—"), str(lc2.get("date") or "—"))
1400
+ console.print(tb)
1401
+ console.print("[dim]数据来源:国家统计局 via akshare[/dim]")
1402
+
1403
+
1404
+ def render_reits_list(r: dict, *, console=None, has_rich: bool = True) -> None:
1405
+ if not has_rich:
1406
+ for row in r.get("reits", [])[:10]: print(row); return
1407
+ if not r.get("success"):
1408
+ console.print(f"[red]{r.get('error','获取失败')}[/red]"); return
1409
+ from rich.table import Table as _T
1410
+ from rich import box as _box
1411
+ tb = _T(title="[bold]🏗 中国 REITs 实时行情[/bold]", box=_box.ROUNDED)
1412
+ tb.add_column("代码", style="cyan")
1413
+ tb.add_column("名称")
1414
+ tb.add_column("最新价", justify="right")
1415
+ tb.add_column("涨跌幅", justify="right")
1416
+ tb.add_column("昨收", justify="right", style="dim")
1417
+ tb.add_column("成交额(万)", justify="right", style="dim")
1418
+ for row in r.get("reits", [])[:20]:
1419
+ chg = row.get("涨跌幅") or 0
1420
+ try: chg_f = float(chg)
1421
+ except Exception: chg_f = 0
1422
+ color = "green" if chg_f > 0 else "red" if chg_f < 0 else "dim"
1423
+ vol_wan = ""
1424
+ try: vol_wan = f"{float(row.get('成交额',0))/10000:.0f}"
1425
+ except Exception as _e: logger.debug("vol_wan parse error: %s", _e)
1426
+ tb.add_row(
1427
+ str(row.get("代码","")),
1428
+ str(row.get("名称",""))[:12],
1429
+ str(row.get("最新价","")),
1430
+ f"[{color}]{chg}%[/{color}]",
1431
+ str(row.get("昨收","")),
1432
+ vol_wan,
1433
+ )
1434
+ console.print(tb)
1435
+ console.print(f"[dim]共 {r.get('count',0)} 只 REITs · 数据来源:东方财富[/dim]")
1436
+
1437
+
1438
+ def render_rental_yield(r: dict, *, console=None, has_rich: bool = True) -> None:
1439
+ if not has_rich:
1440
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1441
+ if not r.get("success"):
1442
+ console.print(f"[red]{r.get('error','计算失败')}[/red]"); return
1443
+ from rich.panel import Panel as _P
1444
+ from rich.columns import Columns as _C
1445
+ from rich import box as _box
1446
+ assess_color = "green" if "优质" in str(r.get("assessment","")) else \
1447
+ "yellow" if "合理" in str(r.get("assessment","")) else "red"
1448
+ lines = [
1449
+ f"[bold cyan]购入价格[/bold cyan] {r['purchase_price_wan']:.1f} 万元",
1450
+ f"[bold cyan]月租金[/bold cyan] {r['monthly_rent']:.0f} 元/月",
1451
+ "[dim]───────────────────────────────[/dim]",
1452
+ f"[bold]毛租金收益率[/bold] [{assess_color}]{r['gross_yield_pct']:.2f}%[/{assess_color}]",
1453
+ f"[bold]净收益率[/bold] {r['net_yield_pct']:.2f}%",
1454
+ f"[bold]资本化率[/bold] {r['cap_rate_pct']:.2f}%",
1455
+ f"[bold]回本年限[/bold] {r['payback_years']:.1f} 年",
1456
+ ]
1457
+ if r.get("leveraged_yield_pct") is not None:
1458
+ lines.append(f"[bold]杠杆收益率[/bold] {r['leveraged_yield_pct']:.2f}% [dim](含贷款)[/dim]")
1459
+ lines += [
1460
+ "[dim]───────────────────────────────[/dim]",
1461
+ f"[{assess_color}]综合评级:{r.get('assessment','')}[/{assess_color}]",
1462
+ f"[dim]{r.get('benchmark','')}[/dim]",
1463
+ ]
1464
+ console.print(_P("\n".join(lines), title="[bold]💰 租金收益率分析[/bold]",
1465
+ border_style="cyan"))
1466
+
1467
+
1468
+ def render_property_val(r: dict, *, console=None, has_rich: bool = True) -> None:
1469
+ if not has_rich:
1470
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1471
+ if not r.get("success"):
1472
+ console.print(f"[red]{r.get('error','估值失败')}[/red]"); return
1473
+ from rich.table import Table as _T
1474
+ from rich import box as _box
1475
+ from rich.panel import Panel as _P
1476
+ verd = r.get("verdict","")
1477
+ verd_color = "green" if "低估" in verd else "red" if "高估" in verd else "yellow"
1478
+ tb = _T(title="[bold]🏢 物业三合一估值[/bold]", box=_box.ROUNDED)
1479
+ tb.add_column("方法", style="dim")
1480
+ tb.add_column("估值(万元)", justify="right")
1481
+ tb.add_column("说明", style="dim")
1482
+ tb.add_row("收益法 (Cap Rate)", f"{r['income_approach']:.1f}", f"资本化率 {r['cap_rate_used']:.1f}%")
1483
+ tb.add_row("DCF 折现法", f"{r['dcf_approach']:.1f}", f"折现率 {r['discount_rate_used']:.1f}%")
1484
+ tb.add_row("市场比较法", f"{r['market_approach']:.1f}", "基于租金倍数推算")
1485
+ tb.add_row("[bold]综合估值[/bold]", f"[bold cyan]{r['blended_value_wan']:.1f}[/bold cyan]", "权重 4:4:2")
1486
+ console.print(tb)
1487
+ lo, hi = r.get("market_range_wan", [0, 0])
1488
+ console.print(f" 区位参考区间: [dim]{lo:.0f} — {hi:.0f} 万元[/dim]")
1489
+ console.print(f" 单价参考: [bold]{r.get('price_per_sqm',0):,.0f}[/bold] 元/㎡")
1490
+ console.print(f" [{verd_color}]{verd}[/{verd_color}]")
1491
+
1492
+
1493
+ def render_multi_city(r: dict, *, console=None, has_rich: bool = True) -> None:
1494
+ if not has_rich:
1495
+ for c in r.get("cities", []): print(c); return
1496
+ if not r.get("success"):
1497
+ console.print(f"[red]{r.get('error','获取失败')}[/red]"); return
1498
+ from rich.table import Table as _T
1499
+ from rich import box as _box
1500
+ tb = _T(title="[bold]🗺 多城市房价对比[/bold]", box=_box.ROUNDED)
1501
+ tb.add_column("城市", style="bold")
1502
+ tb.add_column("等级", style="dim")
1503
+ tb.add_column("数据期", style="dim")
1504
+ tb.add_column("新房同比", justify="right")
1505
+ tb.add_column("新房环比", justify="right")
1506
+ tb.add_column("二手同比", justify="right")
1507
+ for city in r.get("cities", []):
1508
+ def _fc(v):
1509
+ if v is None: return "[dim]—[/dim]"
1510
+ c = "green" if v > 0 else "red" if v < 0 else "dim"
1511
+ return f"[{c}]{v:+.2f}%[/{c}]"
1512
+ tb.add_row(
1513
+ city["city"], city.get("tier",""),
1514
+ city.get("date",""),
1515
+ _fc(city.get("new_yoy")), _fc(city.get("new_mom")),
1516
+ _fc(city.get("second_yoy")),
1517
+ )
1518
+ console.print(tb)
1519
+ console.print(f" 涨幅最高: [green]{r.get('top_riser','')}[/green] "
1520
+ f"涨幅最低/下跌: [red]{r.get('top_faller','')}[/red]")
1521
+
1522
+
1523
+ def render_asset_score(r: dict, *, console=None, has_rich: bool = True) -> None:
1524
+ if not has_rich:
1525
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1526
+ if not r.get("success"):
1527
+ console.print(f"[red]{r.get('error','评分失败')}[/red]"); return
1528
+ from rich.panel import Panel as _P
1529
+ score = r.get("score", 0)
1530
+ rating = r.get("rating", "")
1531
+ color = "green" if score >= 75 else "yellow" if score >= 60 else "red"
1532
+ bar_len = int(score / 5)
1533
+ bar = "█" * bar_len + "░" * (20 - bar_len)
1534
+ lines = [
1535
+ f"综合评分: [{color}]{score}[/{color}] / 100",
1536
+ f"评级: [{color}]{rating}[/{color}]",
1537
+ f"[{color}]{bar}[/{color}]",
1538
+ "[dim]─────────────────────────────[/dim]",
1539
+ ]
1540
+ for k, v in r.get("breakdown", {}).items():
1541
+ lines.append(f" {k:<12} [dim]{v}[/dim]")
1542
+ if r.get("suitable_businesses"):
1543
+ lines.append("[dim]─────────────────────────────[/dim]")
1544
+ lines.append(f"推荐业态: [cyan]{' / '.join(r['suitable_businesses'])}[/cyan]")
1545
+ console.print(_P("\n".join(lines), title="[bold]📍 资产区位评分[/bold]",
1546
+ border_style=color))
1547
+
1548
+
1549
+ # ── 数据分析渲染函数 ────────────────────────────────────────────────────────────
1550
+
1551
+ def render_corr_matrix(r: dict, *, console=None, has_rich: bool = True) -> None:
1552
+ if not has_rich:
1553
+ print(json.dumps(r.get("corr_matrix", {}), ensure_ascii=False, indent=2)); return
1554
+ if not r.get("success"):
1555
+ console.print(f"[red]{r.get('error','计算失败')}[/red]"); return
1556
+ from rich.table import Table as _T
1557
+ from rich import box as _box
1558
+ syms = r.get("symbols", [])
1559
+ corr = r.get("corr_matrix", {})
1560
+ tb = _T(title=f"[bold]📊 相关性矩阵 ({r.get('period','')}/{r.get('interval','1d')})[/bold]",
1561
+ box=_box.SIMPLE_HEAVY)
1562
+ tb.add_column("", style="bold")
1563
+ for s in syms:
1564
+ tb.add_column(s, justify="right")
1565
+ for s1 in syms:
1566
+ row_vals = []
1567
+ for s2 in syms:
1568
+ v = corr.get(s1, {}).get(s2)
1569
+ if v is None:
1570
+ row_vals.append("[dim]—[/dim]")
1571
+ elif s1 == s2:
1572
+ row_vals.append("[dim]1.00[/dim]")
1573
+ else:
1574
+ abs_v = abs(v)
1575
+ color = "red" if abs_v > 0.8 else "yellow" if abs_v > 0.5 else "green"
1576
+ row_vals.append(f"[{color}]{v:+.3f}[/{color}]")
1577
+ tb.add_row(f"[bold]{s1}[/bold]", *row_vals)
1578
+ console.print(tb)
1579
+ console.print("[dim]红 > 0.8 高相关 | 黄 0.5-0.8 中度 | 绿 < 0.5 低相关[/dim]")
1580
+ # Stats table
1581
+ stats = r.get("stats", {})
1582
+ if stats:
1583
+ st = _T(title="[dim]个股统计[/dim]", box=_box.MINIMAL)
1584
+ st.add_column("标的"); st.add_column("总收益%", justify="right")
1585
+ st.add_column("年化波动%", justify="right"); st.add_column("夏普", justify="right")
1586
+ st.add_column("最大回撤%", justify="right")
1587
+ for sym, sv in stats.items():
1588
+ ret = sv.get("return_total")
1589
+ rcolor = "green" if (ret or 0) > 0 else "red"
1590
+ st.add_row(sym,
1591
+ f"[{rcolor}]{ret:+.1f}[/{rcolor}]" if ret is not None else "—",
1592
+ f"{sv.get('volatility',0):.1f}" if sv.get("volatility") else "—",
1593
+ f"{sv.get('sharpe',0):.2f}" if sv.get("sharpe") else "—",
1594
+ f"[red]{sv.get('max_drawdown',0):.1f}[/red]")
1595
+ console.print(st)
1596
+
1597
+
1598
+ def render_portfolio_bt(r: dict, *, console=None, has_rich: bool = True) -> None:
1599
+ if not has_rich:
1600
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1601
+ if not r.get("success"):
1602
+ console.print(f"[red]{r.get('error','回测失败')}[/red]"); return
1603
+ from rich.table import Table as _T
1604
+ from rich import box as _box
1605
+ from rich.panel import Panel as _P
1606
+ pf = r.get("portfolio", {})
1607
+ bm = r.get("benchmark", {})
1608
+ ret = pf.get("total_return_pct", 0)
1609
+ rcolor = "green" if ret > 0 else "red"
1610
+ lines = [
1611
+ f" [bold]总收益率[/bold] [{rcolor}]{ret:+.2f}%[/{rcolor}]",
1612
+ f" [bold]年化波动率[/bold] {pf.get('annual_vol_pct',0):.2f}%",
1613
+ f" [bold]夏普比率[/bold] {pf.get('sharpe_ratio','—')}",
1614
+ f" [bold]最大回撤[/bold] [red]{pf.get('max_drawdown_pct',0):.2f}%[/red]",
1615
+ f" [bold]卡玛比率[/bold] {pf.get('calmar_ratio','—')}",
1616
+ ]
1617
+ if bm:
1618
+ br = bm.get("total_return_pct", 0)
1619
+ bc = "green" if br > 0 else "red"
1620
+ alpha = round(ret - br, 2)
1621
+ ac = "green" if alpha > 0 else "red"
1622
+ lines += [
1623
+ " [dim]─────────────────────────────────[/dim]",
1624
+ f" [dim]基准 {bm['symbol']}[/dim] [{bc}]{br:+.2f}%[/{bc}]",
1625
+ f" [bold]超额收益[/bold] [{ac}]{alpha:+.2f}%[/{ac}]",
1626
+ ]
1627
+ console.print(_P("\n".join(lines), title="[bold]📈 组合回测结果[/bold]",
1628
+ border_style=rcolor))
1629
+ # Allocation table
1630
+ alloc = r.get("allocation", [])
1631
+ if alloc:
1632
+ ta = _T(title=f"[dim]持仓分配 · 回测区间 {r.get('period','N/A')} · 再平衡 {r.get('rebalance','—')}[/dim]",
1633
+ box=_box.MINIMAL)
1634
+ ta.add_column("标的"); ta.add_column("权重%", justify="right")
1635
+ for a in alloc:
1636
+ ta.add_row(a["symbol"], f"{a['weight_pct']:.1f}%")
1637
+ console.print(ta)
1638
+
1639
+
1640
+ def render_sql_result(r: dict, *, console=None, has_rich: bool = True) -> None:
1641
+ if not has_rich:
1642
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1643
+ if not r.get("success"):
1644
+ console.print(f"[red]{r.get('error','查询失败')}[/red]"); return
1645
+ from rich.table import Table as _T
1646
+ from rich import box as _box
1647
+ rows = r.get("rows", [])
1648
+ cols = r.get("columns", [])
1649
+ if not rows:
1650
+ console.print("[dim]查询返回 0 行[/dim]"); return
1651
+ tb = _T(title=f"[bold]🦆 DuckDB 查询结果[/bold] [dim]({r.get('row_count',0)} 行)[/dim]",
1652
+ box=_box.ROUNDED)
1653
+ for c in cols:
1654
+ tb.add_column(str(c))
1655
+ for row in rows[:100]:
1656
+ tb.add_row(*[str(row.get(c, "")) for c in cols])
1657
+ console.print(tb)
1658
+ if r.get("tables_loaded"):
1659
+ console.print(f"[dim]已加载表: {', '.join(r['tables_loaded'])}[/dim]")
1660
+
1661
+
1662
+ def render_alerts(r: dict, *, console=None, has_rich: bool = True) -> None:
1663
+ if not has_rich:
1664
+ print(json.dumps(r, ensure_ascii=False, indent=2)); return
1665
+ from rich.table import Table as _T
1666
+ from rich import box as _box
1667
+ active = r.get("active_alerts", [])
1668
+ triggered = r.get("triggered_alerts", [])
1669
+ cond_lbl = {"gt": "高于", "lt": "低于", "cross_up": "向上突破", "cross_down": "向下跌破"}
1670
+ if active:
1671
+ ta = _T(title="[bold]🔔 活跃预警[/bold]", box=_box.ROUNDED)
1672
+ ta.add_column("标的", style="cyan"); ta.add_column("条件")
1673
+ ta.add_column("触发价", justify="right"); ta.add_column("备注", style="dim")
1674
+ ta.add_column("ID", style="dim")
1675
+ for a in active:
1676
+ ta.add_row(a["symbol"],
1677
+ cond_lbl.get(a["condition"], a["condition"]),
1678
+ str(a["price"]), a.get("note",""),
1679
+ a["id"][:16]+"…")
1680
+ console.print(ta)
1681
+ if triggered:
1682
+ tt = _T(title="[dim]已触发预警[/dim]", box=_box.MINIMAL)
1683
+ tt.add_column("标的"); tt.add_column("触发价"); tt.add_column("触发时间", style="dim")
1684
+ for a in triggered:
1685
+ tt.add_row(a["symbol"], str(a.get("triggered_price","")),
1686
+ str(a.get("triggered_at",""))[:16])
1687
+ console.print(tt)
1688
+ if not active and not triggered:
1689
+ console.print("[dim]暂无预警记录。使用 /alert add AAPL gt 200 设置预警[/dim]")
1690
+
1691
+
1692
+ def _prompt_float(label: str, default: float) -> float:
1693
+ """交互式数字输入,失败时返回 default。"""
1694
+
1695
+
1696
+ # Moved from aria_cli.py
1697
+ def format_backtest_output(data: dict):
1698
+ """Format backtest results as clean rows."""
1699
+ if not HAS_RICH:
1700
+ return json.dumps(data, indent=2, ensure_ascii=False)
1701
+
1702
+ d = data.get("data", data.get("backtest", data))
1703
+ total_ret = d.get("total_return", 0)
1704
+ ann_ret = d.get("annualized_return", 0)
1705
+ sharpe = d.get("sharpe_ratio", 0)
1706
+ max_dd = d.get("max_drawdown", 0)
1707
+ win_rate = d.get("win_rate", 0)
1708
+ trades = d.get("num_trades", 0)
1709
+ bh_ret = d.get("buy_hold_return", 0)
1710
+ outperf = d.get("outperformance", 0)
1711
+
1712
+ def _c(v): return "green" if v >= 0 else "red"
1713
+
1714
+ out = Text()
1715
+ out.append(" Backtest Results\n", style="bold")
1716
+ out.append(f" {'Total Return':<18s}", style="dim")
1717
+ out.append(f"{total_ret*100:+.2f}%", style=_c(total_ret))
1718
+ out.append(" vs B&H ", style="dim")
1719
+ out.append(f"{bh_ret*100:+.2f}%\n", style=_c(bh_ret))
1720
+ out.append(f" {'Annualized':<18s}", style="dim")
1721
+ out.append(f"{ann_ret*100:+.2f}%\n")
1722
+ out.append(f" {'Sharpe Ratio':<18s}", style="dim")
1723
+ out.append(f"{sharpe:.2f}\n")
1724
+ out.append(f" {'Max Drawdown':<18s}", style="dim")
1725
+ out.append(f"{max_dd*100:.2f}%\n", style="red")
1726
+ out.append(f" {'Win Rate':<18s}", style="dim")
1727
+ out.append(f"{win_rate*100:.1f}%\n")
1728
+ out.append(f" {'Trades':<18s}", style="dim")
1729
+ out.append(f"{trades}\n")
1730
+ out.append(f" {'Outperformance':<18s}", style="dim")
1731
+ out.append(f"{outperf*100:+.2f}%\n", style=_c(outperf))
1732
+ return out