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,1316 @@
1
+ """
2
+ PortfolioCommandsMixin — Portfolio commands: journal, report, portfolio, apply_plan, team.
3
+
4
+ Extracted from aria_cli.py. Methods' __globals__ are rebound to aria_cli's namespace
5
+ by _rebind_mixin_globals() called at module load time.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from aria_code.ui.render.output import file_uri as _file_uri
11
+
12
+ from aria_code.apps.cli.i18n import ui_text
13
+
14
+ from ._ui import Panel, print_error
15
+
16
+
17
+ def _detect_lang_for_team(text: str) -> str:
18
+ if not text:
19
+ return "zh"
20
+ zh_chars = sum(1 for c in text if '一' <= c <= '鿿')
21
+ return "zh" if zh_chars / max(len(text), 1) > 0.15 else "en"
22
+
23
+
24
+ import logging
25
+ import json
26
+ import asyncio
27
+ import datetime
28
+ import time
29
+ import shlex
30
+ from typing import Dict, Any, Optional
31
+
32
+ def parse_team_args(*args, **kwargs):
33
+ from aria_cli import parse_team_args as fn
34
+ return fn(*args, **kwargs)
35
+ def resolve_team_symbols(*args, **kwargs):
36
+ from aria_cli import resolve_team_symbols as fn
37
+ return fn(*args, **kwargs)
38
+ def team_agent_names(*args, **kwargs):
39
+ from aria_cli import team_agent_names as fn
40
+ return fn(*args, **kwargs)
41
+ # execute_aria_tool lives in apps/cli/tool_executor.py and needs nothing from
42
+ # aria_cli — the cache it reads is owned there now, so the hop was pointless.
43
+ from aria_code.apps.cli.tool_executor import execute_aria_tool
44
+ def export_report_pdf(*args, **kwargs):
45
+ from aria_cli import export_report_pdf as fn
46
+ return fn(*args, **kwargs)
47
+ def run_team_analysis(*args, **kwargs):
48
+ from aria_cli import run_team_analysis as fn
49
+ return fn(*args, **kwargs)
50
+ def save_markdown_report(*args, **kwargs):
51
+ from aria_cli import save_markdown_report as fn
52
+ return fn(*args, **kwargs)
53
+ def all_agents_failed(*args, **kwargs):
54
+ from aria_cli import all_agents_failed as fn
55
+ return fn(*args, **kwargs)
56
+ def _get_Panel():
57
+ # Resolved locally now — see ._ui; no aria_cli round-trip.
58
+ return Panel
59
+ def save_team_report(*args, **kwargs):
60
+ from aria_cli import save_team_report as fn
61
+ return fn(*args, **kwargs)
62
+ def _print_verdict_banner(*args, **kwargs):
63
+ from aria_cli import _print_verdict_banner as fn
64
+ return fn(*args, **kwargs)
65
+ def _get_mdc(*args, **kwargs):
66
+ from .._optional import get_mdc as fn
67
+ return fn(*args, **kwargs)
68
+ def report_agent_names(*args, **kwargs):
69
+ from aria_cli import report_agent_names as fn
70
+ return fn(*args, **kwargs)
71
+ def _tool_run_command(*args, **kwargs):
72
+ from aria_cli import _tool_run_command as fn
73
+ return fn(*args, **kwargs)
74
+ def run_deep_cli(*args, **kwargs):
75
+ from aria_cli import run_deep_cli as fn
76
+ return fn(*args, **kwargs)
77
+ def report_agent_health(*args, **kwargs):
78
+ from aria_cli import report_agent_health as fn
79
+ return fn(*args, **kwargs)
80
+ def report_file_size_kb(*args, **kwargs):
81
+ from aria_cli import report_file_size_kb as fn
82
+ return fn(*args, **kwargs)
83
+ def generate_html_report(*args, **kwargs):
84
+ from aria_cli import generate_html_report as fn
85
+ return fn(*args, **kwargs)
86
+ def _get__HAS_MDC():
87
+ from .._optional import HAS_MDC as val
88
+ return val
89
+ def _sanitize_team_result_with_market_data(*args, **kwargs):
90
+ from aria_cli import _sanitize_team_result_with_market_data as fn
91
+ return fn(*args, **kwargs)
92
+ def update_report_index(*args, **kwargs):
93
+ from aria_cli import update_report_index as fn
94
+ return fn(*args, **kwargs)
95
+ def build_markdown_report_prompt(*args, **kwargs):
96
+ from aria_cli import build_markdown_report_prompt as fn
97
+ return fn(*args, **kwargs)
98
+ def parse_report_args(*args, **kwargs):
99
+ from aria_cli import parse_report_args as fn
100
+ return fn(*args, **kwargs)
101
+
102
+ import json
103
+ import asyncio
104
+ import datetime
105
+ import time
106
+ import shlex
107
+ import sys
108
+ import os
109
+ from typing import Dict, Any, Optional
110
+
111
+
112
+ import json
113
+ import asyncio
114
+ import datetime
115
+ import time
116
+ import shlex
117
+ import sys
118
+ import os
119
+ from typing import Dict, Any, Optional
120
+
121
+ # A real logger. This was a `def logger(*args, **kwargs)` forwarding to
122
+ # aria_cli, so every `logger.debug(...)` raised AttributeError — and all but
123
+ # one of them sit in an except block, where the failure replaced whatever
124
+ # error was being reported.
125
+ logger = logging.getLogger(__name__)
126
+
127
+
128
+ class PortfolioCommandsMixin:
129
+ """Mixin: Portfolio commands: journal, report, portfolio, apply_plan, team."""
130
+
131
+ async def cmd_journal(self, args: str):
132
+ """
133
+ 本地持仓账本(SQLite,~/.arthera/portfolio.db)
134
+ Usage:
135
+ /journal → 当前持仓
136
+ /journal add buy AAPL 100 185.50 [理由]
137
+ /journal add sell AAPL 50 200.00 [理由]
138
+ /journal trades [SYMBOL] → 交易记录
139
+ /journal pnl → 含实时报价的未实现盈亏
140
+ /journal realized → 已实现盈亏(FIFO)
141
+ /journal export → 导出 CSV 到桌面
142
+ /journal delete <id> → 删除指定记录
143
+ """
144
+ try:
145
+ from portfolio_ledger import PortfolioLedger as _PL
146
+ except ImportError:
147
+ msg = "portfolio_ledger 模块未找到"
148
+ self.context.console.print(f"[red]{msg}[/red]") if self.context.has_rich else print(msg)
149
+ return
150
+
151
+ ledger = _PL()
152
+ parts = args.strip().split() if args.strip() else []
153
+ sub = parts[0].lower() if parts else "positions"
154
+
155
+ # ── add buy/sell ─────────────────────────────────────────────────────
156
+ if sub == "add":
157
+ # /journal add buy AAPL 100 185.50 [reason...]
158
+ if len(parts) < 5:
159
+ usage = "用法: /journal add <buy|sell> <symbol> <qty> <price> [理由]"
160
+ self.context.console.print(f"[yellow]{usage}[/yellow]") if self.context.has_rich else print(usage)
161
+ return
162
+ try:
163
+ side = parts[1].upper()
164
+ symbol = parts[2].upper()
165
+ qty = float(parts[3])
166
+ price = float(parts[4])
167
+ reason = " ".join(parts[5:]) if len(parts) > 5 else ""
168
+ tid = ledger.add_trade(symbol, side, qty, price, reason=reason)
169
+ amount = round(qty * price, 2)
170
+ msg = (f"✓ 已记录: #{tid} {side} {symbol} × {qty} @ {price}"
171
+ f" 总额 {amount:,.2f} {reason}")
172
+ self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
173
+ except Exception as e:
174
+ self.context.console.print(f"[red]记录失败: {e}[/red]") if self.context.has_rich else print(f"记录失败: {e}")
175
+ return
176
+
177
+ # ── delete ───────────────────────────────────────────────────────────
178
+ if sub == "delete" and len(parts) >= 2:
179
+ try:
180
+ tid = int(parts[1])
181
+ ok = ledger.delete_trade(tid)
182
+ msg = f"✓ 已删除记录 #{tid}" if ok else f"未找到记录 #{tid}"
183
+ self.context.console.print(f"[{'green' if ok else 'yellow'}]{msg}[/{'green' if ok else 'yellow'}]") if self.context.has_rich else print(msg)
184
+ except Exception as e:
185
+ self.context.console.print(f"[red]删除失败: {e}[/red]") if self.context.has_rich else print(f"删除失败: {e}")
186
+ return
187
+
188
+ # ── trades history ───────────────────────────────────────────────────
189
+ if sub == "trades":
190
+ sym = parts[1].upper() if len(parts) > 1 else None
191
+ trades = ledger.get_trades(symbol=sym, limit=30)
192
+ title = f"交易记录{f' — {sym}' if sym else ''} (最近 {len(trades)} 条)"
193
+ if self.context.has_rich:
194
+ from rich.table import Table
195
+ tbl = Table(title=title, box=None, show_header=True, header_style="bold")
196
+ tbl.add_column("#", style="dim", width=4)
197
+ tbl.add_column("日期", width=10)
198
+ tbl.add_column("方向", width=5)
199
+ tbl.add_column("标的", width=8)
200
+ tbl.add_column("数量", justify="right", width=10)
201
+ tbl.add_column("价格", justify="right", width=10)
202
+ tbl.add_column("总额", justify="right", width=12)
203
+ tbl.add_column("理由", width=20)
204
+ for t in trades:
205
+ side_color = "green" if t["side"] == "BUY" else "red"
206
+ tbl.add_row(
207
+ str(t["id"]),
208
+ t["date"],
209
+ f"[{side_color}]{t['side']}[/{side_color}]",
210
+ t["symbol"],
211
+ f"{t['qty']:,.4g}",
212
+ f"{t['price']:,.4f}",
213
+ f"{t['amount']:,.2f}",
214
+ (t["reason"] or "")[:18],
215
+ )
216
+ self.context.console.print(tbl)
217
+ if not trades:
218
+ self.context.console.print("[dim]无交易记录[/dim]")
219
+ else:
220
+ print(title)
221
+ for t in trades:
222
+ print(f" #{t['id']} {t['date']} {t['side']} {t['symbol']} "
223
+ f"× {t['qty']} @ {t['price']} {t['reason']}")
224
+ return
225
+
226
+ # ── export ───────────────────────────────────────────────────────────
227
+ if sub == "export":
228
+ try:
229
+ out = ledger.export_csv()
230
+ msg = f"✓ 已导出 {ledger.trade_count()} 条记录 → {out}"
231
+ self.context.console.print(f"[green]{msg}[/green]") if self.context.has_rich else print(msg)
232
+ except Exception as e:
233
+ self.context.console.print(f"[red]导出失败: {e}[/red]") if self.context.has_rich else print(f"导出失败: {e}")
234
+ return
235
+
236
+ # ── realized P&L ─────────────────────────────────────────────────────
237
+ if sub == "realized":
238
+ rows = ledger.get_realized_pnl()
239
+ if self.context.has_rich:
240
+ from rich.table import Table
241
+ tbl = Table(title="已实现盈亏(FIFO)", box=None, header_style="bold")
242
+ tbl.add_column("标的", width=8)
243
+ tbl.add_column("已实现盈亏", justify="right", width=14)
244
+ tbl.add_column("剩余持仓", justify="right", width=10)
245
+ for r in rows:
246
+ pnl = r["realized_pnl"]
247
+ color = "green" if pnl >= 0 else "red"
248
+ tbl.add_row(
249
+ r["symbol"],
250
+ f"[{color}]{pnl:+,.2f}[/{color}]",
251
+ f"{r['open_lots']:,.4g}" if r["has_open"] else "已平仓",
252
+ )
253
+ self.context.console.print(tbl)
254
+ total = sum(r["realized_pnl"] for r in rows)
255
+ tc = "green" if total >= 0 else "red"
256
+ self.context.console.print(f" [bold]合计已实现盈亏: [{tc}]{total:+,.2f}[/{tc}][/bold]")
257
+ else:
258
+ for r in rows:
259
+ print(f" {r['symbol']}: {r['realized_pnl']:+,.2f}")
260
+ return
261
+
262
+ # ── pnl with live prices ──────────────────────────────────────────────
263
+ if sub == "pnl":
264
+ positions = ledger.get_positions()
265
+ if not positions:
266
+ self.context.console.print("[dim]暂无持仓记录。用 /journal add buy … 添加。[/dim]") if self.context.has_rich else print("暂无持仓")
267
+ return
268
+ # fetch live prices via yfinance
269
+ live_prices: dict = {}
270
+ syms = [p["symbol"] for p in positions]
271
+ if self.context.has_rich:
272
+ self.context.console.print(f" [dim]获取 {len(syms)} 只股票实时报价…[/dim]")
273
+ try:
274
+ import yfinance as yf
275
+ for sym in syms:
276
+ try:
277
+ h = yf.Ticker(sym).history(period="1d")
278
+ if not h.empty:
279
+ live_prices[sym] = float(h["Close"].iloc[-1])
280
+ except Exception:
281
+ pass
282
+ except ImportError:
283
+ pass
284
+ rows = ledger.get_pnl_with_prices(live_prices)
285
+ if self.context.has_rich:
286
+ from rich.table import Table
287
+ tbl = Table(title="持仓盈亏", box=None, header_style="bold")
288
+ tbl.add_column("标的", width=8)
289
+ tbl.add_column("数量", justify="right", width=10)
290
+ tbl.add_column("均价", justify="right", width=10)
291
+ tbl.add_column("现价", justify="right", width=10)
292
+ tbl.add_column("市值", justify="right", width=12)
293
+ tbl.add_column("未实现盈亏", justify="right", width=14)
294
+ tbl.add_column("涨跌%", justify="right", width=8)
295
+ for r in rows:
296
+ has_price = "current_price" in r
297
+ pnl = r.get("unrealized_pnl", "")
298
+ pct = r.get("unrealized_pct", "")
299
+ color = ("green" if isinstance(pnl, (int, float)) and pnl >= 0 else "red") if has_price else "dim"
300
+ tbl.add_row(
301
+ r["symbol"],
302
+ f"{r['net_qty']:,.4g}",
303
+ f"{r['avg_cost']:,.4f}",
304
+ f"{r.get('current_price', 'N/A'):,.4f}" if has_price else "N/A",
305
+ f"{r.get('market_value', ''):,.2f}" if has_price else "N/A",
306
+ f"[{color}]{pnl:+,.2f}[/{color}]" if has_price else "—",
307
+ f"[{color}]{pct:+.2f}%[/{color}]" if has_price else "—",
308
+ )
309
+ self.context.console.print(tbl)
310
+ total_pnl = sum(r.get("unrealized_pnl", 0) for r in rows if "unrealized_pnl" in r)
311
+ total_mv = sum(r.get("market_value", 0) for r in rows if "market_value" in r)
312
+ total_cost = sum(r["cost_basis"] for r in rows)
313
+ tc = "green" if total_pnl >= 0 else "red"
314
+ self.context.console.print(
315
+ f" [bold]总持仓成本 {total_cost:,.2f} "
316
+ f"总市值 {total_mv:,.2f} "
317
+ f"未实现盈亏 [{tc}]{total_pnl:+,.2f}[/{tc}][/bold]"
318
+ )
319
+ # Portfolio status banner
320
+ _pnl_pct = (total_pnl / total_cost * 100) if total_cost else 0
321
+ _pnl_verdict = "HEALTHY" if _pnl_pct >= 0 else ("NEEDS_ATTENTION" if _pnl_pct >= -10 else "HIGH_RISK")
322
+ _pnl_sub = f"总盈亏 {total_pnl:+,.2f} ({_pnl_pct:+.1f}%)"
323
+ _print_verdict_banner(_pnl_verdict, subtitle=_pnl_sub)
324
+ else:
325
+ for r in rows:
326
+ pnl = r.get("unrealized_pnl", "N/A")
327
+ print(f" {r['symbol']}: {r['net_qty']} × avg {r['avg_cost']} pnl {pnl}")
328
+ return
329
+
330
+ # ── default: positions ────────────────────────────────────────────────
331
+ positions = ledger.get_positions()
332
+ if not positions:
333
+ hint = "暂无持仓记录。\n 添加示例: /journal add buy AAPL 100 185.50 首次建仓"
334
+ self.context.console.print(f"[dim]{hint}[/dim]") if self.context.has_rich else print(hint)
335
+ return
336
+ if self.context.has_rich:
337
+ from rich.table import Table
338
+ tbl = Table(
339
+ title=f"当前持仓({len(positions)} 只,共 {ledger.trade_count()} 条交易)",
340
+ box=None, header_style="bold",
341
+ )
342
+ tbl.add_column("标的", width=8)
343
+ tbl.add_column("持仓量", justify="right", width=12)
344
+ tbl.add_column("均价成本", justify="right", width=12)
345
+ tbl.add_column("持仓成本", justify="right", width=14)
346
+ tbl.add_column("首次建仓", width=12)
347
+ for pos in positions:
348
+ tbl.add_row(
349
+ pos["symbol"],
350
+ f"{pos['net_qty']:,.4g}",
351
+ f"{pos['avg_cost']:,.4f}",
352
+ f"{pos['cost_basis']:,.2f}",
353
+ pos.get("first_trade", ""),
354
+ )
355
+ self.context.console.print(tbl)
356
+ self.context.console.print(
357
+ " [dim]更多命令: /journal pnl | /journal trades | "
358
+ "/journal realized | /journal export[/dim]"
359
+ )
360
+ else:
361
+ print(f"当前持仓 ({len(positions)} 只):")
362
+ for pos in positions:
363
+ print(f" {pos['symbol']}: {pos['net_qty']} 股 均价 {pos['avg_cost']}")
364
+
365
+ async def cmd_report(self, args: str):
366
+ """生成综合投资报告(图表 + 多 Agent 分析 → HTML / Markdown 文件)。
367
+
368
+ Usage:
369
+ /report AAPL
370
+ /report 000333
371
+ /report AAPL --format md # Markdown 投研报告(离线可用)
372
+ /report AAPL --type deep # 深度研报(8页)
373
+ /report AAPL --type brief # 简评(1页)
374
+ /report AAPL --pdf # 同时导出 PDF(自动检测 Chrome/Edge,macOS 零安装)
375
+ """
376
+ from datetime import datetime as _dt
377
+
378
+ report_args = parse_report_args(args)
379
+ symbol = report_args.symbol
380
+ fmt = report_args.fmt
381
+ report_type = report_args.report_type
382
+ export_pdf_flag = report_args.export_pdf
383
+ out_dir = report_args.output_dir
384
+ if out_dir:
385
+ out_dir.mkdir(parents=True, exist_ok=True)
386
+ ts = _dt.now().strftime("%Y%m%d_%H%M")
387
+
388
+ # ── Markdown report mode (works fully offline) ────────────────────────
389
+ if fmt in ("md", "markdown"):
390
+ _gen = ui_text(self, f"生成 {symbol} Markdown 投研报告 ({report_type})", f"Writing the {symbol} Markdown report ({report_type})")
391
+ self.context.console.print(f"\n [dim]{_gen}…[/dim]") if self.context.has_rich else print(f"\n {_gen}…")
392
+
393
+ # Fetch real data through the service boundary so provenance and
394
+ # quality metadata travel with the report prompt and artifact.
395
+ mdc_data = {}
396
+ data_bundle = None
397
+ data_quality = {}
398
+ try:
399
+ from packages.aria_services.data import DataService as _ReportDataService
400
+ data_bundle = await asyncio.get_event_loop().run_in_executor(
401
+ None,
402
+ lambda: _ReportDataService().bundle(symbol, history_days=370, technical_days=120),
403
+ )
404
+ quote = data_bundle.quote or {}
405
+ technical = data_bundle.technical or {}
406
+ mdc_data = {**quote, **technical}
407
+ data_quality = data_bundle.quality or {}
408
+ except Exception as _ds_exc:
409
+ logger.debug("report markdown data service failed: %s", _ds_exc)
410
+ if _get__HAS_MDC():
411
+ try:
412
+ mdc = _get_mdc()
413
+ q = mdc.quote(symbol)
414
+ ti = mdc.technical_indicators(symbol, days=120)
415
+ mdc_data = {**q, **ti}
416
+ data_quality = {
417
+ "status": "partial",
418
+ "stale": False,
419
+ "providers": mdc_data.get("provider_chain") or list(dict.fromkeys(
420
+ str(v) for v in [mdc_data.get("provider"), mdc_data.get("source")] if v
421
+ )),
422
+ "warnings": [f"data service unavailable: {_ds_exc}"],
423
+ }
424
+ except Exception:
425
+ data_quality = {"status": "data_unavailable", "warnings": [str(_ds_exc)]}
426
+
427
+ ai_prompt = build_markdown_report_prompt(
428
+ symbol=symbol,
429
+ report_type=report_type,
430
+ market_data=mdc_data,
431
+ data_quality=data_quality,
432
+ data_bundle=data_bundle,
433
+ now=_dt.now(),
434
+ lang=ui_text(self, "zh", "en"),
435
+ )
436
+
437
+ await self.terminal.send_message(ai_prompt)
438
+
439
+ # Extract last AI response and save as markdown
440
+ last_ai = next(
441
+ (m["content"] for m in reversed(self.terminal.conversation)
442
+ if m.get("role") == "assistant"), ""
443
+ )
444
+ if last_ai:
445
+ saved = save_markdown_report(
446
+ symbol=symbol,
447
+ report_type=report_type,
448
+ markdown_text=last_ai,
449
+ timestamp=ts,
450
+ output_dir=out_dir,
451
+ market_data=mdc_data,
452
+ data_quality=data_quality,
453
+ data_bundle=data_bundle,
454
+ created_at=_dt.now(),
455
+ )
456
+ out_f = saved.path
457
+ # A short, clickable path: the full temp-dir path wrapped over
458
+ # three lines, and the "预览: open …" line repeated it.
459
+ from aria_code.ui.render.output import display_path as _short_path
460
+ _saved = ui_text(self, "报告已保存", "Report saved")
461
+ if self.context.has_rich:
462
+ self.context.console.print(f"\n [green]✓[/green] {_saved}: "
463
+ f"[link={_file_uri(str(out_f))}]{_short_path(str(out_f))}[/link]\n")
464
+ else:
465
+ print(f"\n {_saved}: {out_f}")
466
+
467
+ # ── Markdown 报告 → 排版 PDF(--pdf;中英文模板自动检测)──
468
+ if export_pdf_flag:
469
+ from pathlib import Path as _Path # mixin 重绑定后无模块级 Path
470
+ try:
471
+ from markdown_pdf import markdown_to_pdf as _md_to_pdf
472
+ _pdf_out = await asyncio.get_event_loop().run_in_executor(
473
+ None,
474
+ lambda: _md_to_pdf(_Path(out_f),
475
+ _Path(out_f).with_suffix(".pdf")),
476
+ )
477
+ if _pdf_out:
478
+ if self.context.has_rich:
479
+ self.context.console.print(
480
+ f" [green]PDF 导出成功[/green] "
481
+ f"[link={_file_uri(_pdf_out)}]{_pdf_out.name}[/link]"
482
+ )
483
+ else:
484
+ print(f" PDF: {_pdf_out}")
485
+ import subprocess as _subp_md
486
+ try:
487
+ _subp_md.Popen(["open", str(_pdf_out)])
488
+ except Exception:
489
+ pass
490
+ else:
491
+ _msg = "PDF 导出失败:安装 Chrome/Edge,或 pip install pyobjc-framework-WebKit"
492
+ self.context.console.print(f" [yellow]{_msg}[/yellow]") if self.context.has_rich else print(f" {_msg}")
493
+ except Exception as _pdf_exc:
494
+ logger.debug("[report] md pdf export error: %s", _pdf_exc)
495
+ return
496
+
497
+ # ── HTML 研报(Bloomberg 暗色主题)────────────────────────────────
498
+ if self.context.has_rich:
499
+ self.context.console.print(f"\n [dim]正在生成 [bold]{symbol}[/bold] 专业研报(数据清洗 + 图表 + Agent 分析)…[/dim]")
500
+ else:
501
+ print(f"\n 正在生成 {symbol} 研报…")
502
+
503
+ _agent_names_for_report = report_agent_names(report_type)
504
+ def _report_agent_done(name, result):
505
+ success = bool(getattr(result, "success", False))
506
+ degraded = bool(getattr(result, "degraded", False))
507
+ if success:
508
+ icon = "≈" if degraded else "✓"
509
+ detail = "降级结果" if degraded else "完成"
510
+ else:
511
+ icon = "✗"
512
+ error = str(getattr(result, "error", "") or "失败")
513
+ detail = "超时" if error == "timeout" else error[:60]
514
+ if self.context.has_rich:
515
+ color = "yellow" if degraded else "green" if success else "red"
516
+ self.context.console.print(f" [{color}]{icon}[/{color}] {name} [dim]{detail}[/dim]")
517
+ else:
518
+ print(f" {icon} {name} {detail}")
519
+
520
+ def _report_synthesis_start(results):
521
+ succeeded = sum(1 for result in results if getattr(result, "success", False))
522
+ total = len(results)
523
+ message = f"Agent 阶段完成 {succeeded}/{total},正在整理报告…"
524
+ self.context.console.print(f" [dim]{message}[/dim]") if self.context.has_rich else print(f" {message}")
525
+
526
+ try:
527
+ if self.context.has_rich:
528
+ self.context.console.print(f" [dim]{len(_agent_names_for_report)} agents 并行分析…[/dim]")
529
+ _html_report = await generate_html_report(
530
+ symbol=symbol,
531
+ report_type=report_type,
532
+ output_dir=out_dir,
533
+ config=self.terminal.config,
534
+ on_agent_done=_report_agent_done,
535
+ on_synthesis_start=_report_synthesis_start,
536
+ )
537
+ else:
538
+ _html_report = await generate_html_report(
539
+ symbol=symbol,
540
+ report_type=report_type,
541
+ output_dir=out_dir,
542
+ config=self.terminal.config,
543
+ on_agent_done=_report_agent_done,
544
+ on_synthesis_start=_report_synthesis_start,
545
+ )
546
+ out_f = _html_report.path
547
+ _team_result = _html_report.team_result
548
+ except Exception as e:
549
+ if self.context.has_rich:
550
+ self.context.console.print(f" [red]研报生成失败: {e}[/red]")
551
+ else:
552
+ print(f" 研报生成失败: {e}")
553
+ return
554
+
555
+ if not out_f:
556
+ self.context.console.print(" [red]研报生成失败(无输出文件)[/red]") if self.context.has_rich else print(" 研报生成失败")
557
+ return
558
+
559
+ path = str(out_f)
560
+ from ui.render.output import display_path as _display_path
561
+ path_label = _display_path(out_f, fallback="report")
562
+ _file_kb = report_file_size_kb(out_f)
563
+ # Check if all agents failed — show warning instead of false success
564
+ _agent_health = _html_report.agent_health or report_agent_health(
565
+ _team_result, _agent_names_for_report
566
+ )
567
+ _all_agents_failed = all_agents_failed(_team_result) or bool(
568
+ _agent_health.get("expected") and not _agent_health.get("succeeded")
569
+ )
570
+ _agents_partial = bool(
571
+ _agent_health.get("failed") and _agent_health.get("succeeded")
572
+ )
573
+ if self.context.has_rich:
574
+ if _all_agents_failed:
575
+ self.context.console.print(
576
+ f"\n [yellow]⚠ 研报已保存(所有 Agent 分析失败,内容仅含基础数据)[/yellow]"
577
+ f" [dim]{out_f.name} ({_file_kb}KB)[/dim]"
578
+ )
579
+ elif _agents_partial:
580
+ self.context.console.print(
581
+ f"\n [yellow]⚠ 研报已保存(Agent 部分完成 "
582
+ f"{_agent_health['succeeded']}/{_agent_health['expected']},结论置信度已降级)[/yellow]"
583
+ f" [dim]{out_f.name} ({_file_kb}KB)[/dim]"
584
+ )
585
+ else:
586
+ self.context.console.print(
587
+ f"\n [green]✅ 研报已保存[/green]"
588
+ f" [link={_file_uri(path)}]{path_label}[/link]"
589
+ f" [dim]({_file_kb}KB)[/dim]"
590
+ )
591
+ self.context.console.print(f" [dim]文件: {path_label}[/dim]")
592
+ if _team_result:
593
+ _health_suffix = (
594
+ f" · agents {_agent_health['succeeded']}/{_agent_health['expected']}"
595
+ if _agent_health.get("failed") else ""
596
+ )
597
+ _print_verdict_banner(
598
+ _team_result.final_signal,
599
+ subtitle=f"耗时 {_team_result.elapsed_sec:.1f}s{_health_suffix}",
600
+ confidence=_team_result.confidence,
601
+ )
602
+ else:
603
+ _pfx = "⚠ 研报已保存(Agent 全部失败)" if _all_agents_failed else "✅ 研报已保存"
604
+ print(f"\n {_pfx}: {path_label} ({_file_kb}KB)")
605
+
606
+ # ── PDF 导出 ──────────────────────────────────────────────────────────
607
+ if export_pdf_flag:
608
+ try:
609
+ if self.context.has_rich:
610
+ with self.context.console.status(f"[dim]{ui_text(self, '导出 PDF', 'Exporting PDF')}…[/dim]", spinner="dots"):
611
+ _pdf_path = await export_report_pdf(out_f)
612
+ else:
613
+ _pdf_path = await export_report_pdf(out_f)
614
+ if _pdf_path:
615
+ _pdf_kb = report_file_size_kb(_pdf_path)
616
+ if self.context.has_rich:
617
+ self.context.console.print(
618
+ f" [green]PDF 导出成功[/green]"
619
+ f" [link={_file_uri(_pdf_path)}]{_pdf_path.name}[/link]"
620
+ f" [dim]({_pdf_kb}KB)[/dim]"
621
+ )
622
+ else:
623
+ print(f" PDF: {_pdf_path} ({_pdf_kb}KB)")
624
+ import subprocess as _subp2
625
+ try:
626
+ _subp2.Popen(["open", str(_pdf_path)])
627
+ except Exception:
628
+ pass
629
+ else:
630
+ _hint = "安装 Chrome/Edge 即可(自动检测),或 pip install weasyprint / brew install wkhtmltopdf"
631
+ if self.context.has_rich:
632
+ self.context.console.print(
633
+ f" [yellow]PDF 导出失败[/yellow] "
634
+ f"[dim]请安装: {_hint} 或在浏览器按 Cmd+P → 存储为 PDF[/dim]"
635
+ )
636
+ else:
637
+ print(f" PDF 导出失败,请安装: {_hint}")
638
+ except Exception as _e:
639
+ logger.debug("[report] pdf export error: %s", _e)
640
+
641
+ # ── 更新研报索引 ──────────────────────────────────────────────────────
642
+ try:
643
+ _idx = await update_report_index(out_f.parent)
644
+ if _idx and self.context.has_rich:
645
+ self.context.console.print(
646
+ f" [dim]索引已更新: [link={_file_uri(_idx)}]{_idx.name}[/link][/dim]"
647
+ )
648
+ except Exception as _e:
649
+ logger.debug("[report] index update error: %s", _e)
650
+
651
+ import subprocess as _subp
652
+ try:
653
+ _subp.Popen(["open", path])
654
+ except Exception:
655
+ pass
656
+
657
+ async def cmd_portfolio(self, args: str):
658
+ """
659
+ 组合级跨标的分析(相关性/分散度/风险)
660
+ Usage:
661
+ /portfolio → 分析 watchlist(最多 10 只)
662
+ /portfolio analyze → 同上
663
+ /portfolio analyze AAPL TSLA MSFT
664
+ /portfolio rebalance → 生成再平衡建议(同 analyze,着重操作)
665
+ """
666
+ import sys as _sys
667
+ parts = args.strip().split()
668
+ sub = parts[0].lower() if parts else "analyze"
669
+ sym_parts = parts[1:] if parts else []
670
+ rebalance = (sub == "rebalance")
671
+
672
+ # ── holdings: 实盘持仓看板,按来源策略分组 ──────────────────────────
673
+ if sub in ("holdings", "book", "positions"):
674
+ await self._portfolio_holdings_by_strategy()
675
+ return
676
+
677
+ # 解析标的:命令行 > 真实持仓账本 > watchlist
678
+ # 之前这里没有命令行参数时永远用 watchlist(等权虚构组合),哪怕
679
+ # portfolio_ledger 里有真实持仓也不会用——risk verdict 算的是一个
680
+ # 假想的等权组合,不是用户真实的仓位暴露。现在优先读真实持仓
681
+ # (按成本加权),ledger 为空才退回 watchlist。
682
+ ledger_weights: Optional[Dict[str, float]] = None
683
+ if sym_parts:
684
+ symbols = [s.strip(",").upper() for s in sym_parts if s.strip(",")]
685
+ else:
686
+ try:
687
+ from portfolio_ledger import PortfolioLedger as _PL_default
688
+ positions = _PL_default().get_positions()
689
+ except ImportError:
690
+ positions = []
691
+ if positions:
692
+ symbols = [p["symbol"].upper() for p in positions]
693
+ ledger_weights = {p["symbol"].upper(): p["cost_basis"] for p in positions}
694
+ else:
695
+ symbols = self.terminal.config.get("watchlist", ["AAPL", "MSFT", "GOOGL", "NVDA", "TSLA"])[:10]
696
+
697
+ _lang = "zh" if str(self.terminal.config.get("ui_lang", "en")).lower().startswith("zh") else "en"
698
+ _T = (lambda a, b: a) if _lang == "zh" else (lambda a, b: b)
699
+
700
+ if not symbols:
701
+ msg = _T("请先设置 watchlist、记录持仓(/journal add buy ...)或指定标的:/portfolio analyze AAPL TSLA MSFT",
702
+ "Set a watchlist, record positions (/journal add buy ...) or name the symbols: "
703
+ "/portfolio analyze AAPL TSLA MSFT")
704
+ self.context.console.print(f"[yellow]{msg}[/yellow]") if self.context.has_rich else print(msg)
705
+ return
706
+
707
+ # 尝试使用新 PortfolioAgent
708
+ _use_new = False
709
+ try:
710
+ from agents.portfolio_agent import PortfolioAgent as _PA
711
+ _use_new = True
712
+ except ImportError:
713
+ pass
714
+
715
+ if _use_new:
716
+ if sym_parts:
717
+ hdr = _T(f"分析组合:{' '.join(symbols)}", f"portfolio of {' '.join(symbols)}")
718
+ elif ledger_weights:
719
+ hdr = _T("分析真实持仓组合(按成本加权)", "your positions, weighted by cost")
720
+ else:
721
+ hdr = _T("分析 watchlist 组合(等权,无真实持仓数据)",
722
+ "watchlist, equal weight (no positions recorded)")
723
+ if rebalance:
724
+ hdr = _T("再平衡方案:", "Rebalance · ") + hdr
725
+ _sym_label = _T("标的", "Symbols")
726
+ if self.context.has_rich:
727
+ self.context.console.print()
728
+ self.context.console.print(f" [bold cyan]━━━ /portfolio {hdr} ━━━[/bold cyan]")
729
+ self.context.console.print(f" [dim]{_sym_label} ({len(symbols)}): {', '.join(symbols)}[/dim]")
730
+ self.context.console.print()
731
+ else:
732
+ print(f"\n ━━━ /portfolio ━━━\n {_sym_label}: {', '.join(symbols)}\n")
733
+
734
+ # The model the session is configured for. This took the first
735
+ # local provider, so a Gemini session asked Ollama, which was not
736
+ # running, and printed the connection error as a log line.
737
+ from aria_code.apps.cli.commands.team import build_team_llm_provider
738
+ _llm = build_team_llm_provider(self.terminal.config)
739
+
740
+ tokens: list = []
741
+ def _on_tok(t):
742
+ tokens.append(t)
743
+ _sys.stdout.write(t); _sys.stdout.flush()
744
+
745
+ try:
746
+ agent = _PA(llm_provider=_llm, on_token=_on_tok, lang=_lang)
747
+ result = await agent.run_portfolio(symbols, weights=ledger_weights)
748
+ if tokens:
749
+ print() # 换行(流式输出后)
750
+
751
+ if not result:
752
+ if self.context.has_rich:
753
+ self.context.console.print(f"[yellow] {_T('组合分析返回空结果', 'The portfolio analysis came back empty')}[/yellow]")
754
+ return
755
+
756
+ if not tokens and getattr(agent, "llm_error", None):
757
+ _note = _T("模型不可用,以下仅为统计结果", "Model unavailable, so this is the statistics only")
758
+ if self.context.has_rich:
759
+ self.context.console.print(f" [dim]{_note}[/dim]")
760
+ else:
761
+ print(f" {_note}")
762
+
763
+ # The verdict the analysis reached. Rebuilt from the signal,
764
+ # NEEDS_ATTENTION (signal SELL) came out as HIGH_RISK.
765
+ _port_verdict = str((result.data_used or {}).get("verdict") or "NEEDS_ATTENTION")
766
+ if self.context.has_rich:
767
+ # The header already ends in a blank line, and the banner's
768
+ # panel opens with one: two blank lines each side before.
769
+ if tokens:
770
+ self.context.console.print()
771
+ for pt in (result.key_points or []):
772
+ self.context.console.print(f" [dim]• {pt}[/dim]")
773
+ _subtitle = " · ".join(result.key_points[:2]) if result.key_points else ""
774
+ _print_verdict_banner(_port_verdict, subtitle=_subtitle,
775
+ confidence=result.confidence, lang=_lang)
776
+ else:
777
+ for pt in result.key_points:
778
+ print(f" • {pt}")
779
+ print(f"\n {_T('置信度', 'Confidence')}: {result.confidence:.0%} {_port_verdict}")
780
+
781
+ if rebalance and self.context.has_rich:
782
+ self.context.console.print("\n [dim]" + _T(
783
+ "提示: 再平衡建议已包含在上方分析中。如需详细方案,可追问 Aria 具体操作步骤。",
784
+ "The rebalancing suggestions are in the analysis above; ask Aria for the steps.") + "[/dim]")
785
+
786
+ except Exception as e:
787
+ msg = _T(f"组合分析失败: {e}", f"Portfolio analysis failed: {e}")
788
+ self.context.console.print(f" [red]{msg}[/red]") if self.context.has_rich else print(f" {msg}")
789
+ return
790
+
791
+ # 旧路径回退(无新 agents 包时)
792
+ if self.context.has_rich:
793
+ self.context.console.print("[dim]Assessing portfolio risk...[/dim]")
794
+ else:
795
+ print("Assessing portfolio risk...")
796
+ result = await execute_aria_tool(self.terminal.api_url, "assess_portfolio_risk", {
797
+ "symbols": symbols[:10],
798
+ })
799
+ if result.get("success") and result.get("data"):
800
+ if self.context.has_rich:
801
+ self.context.console.print("\n [bold]Portfolio Risk[/bold]\n")
802
+ self.context.console.print(f"[dim]{json.dumps(result['data'], indent=2, ensure_ascii=False)[:1000]}[/dim]")
803
+ else:
804
+ print(json.dumps(result.get("data", {}), indent=2, ensure_ascii=False))
805
+ else:
806
+ self.context.console.print(f"[dim]No data: {result.get('error', '')}[/dim]" if self.context.has_rich
807
+ else f"No data: {result.get('error', '')}")
808
+
809
+ async def _portfolio_holdings_by_strategy(self):
810
+ """实盘持仓看板:按来源策略分组(策略名匹配交易 reason),含实时浮盈。"""
811
+ try:
812
+ from portfolio_ledger import PortfolioLedger
813
+ except ImportError:
814
+ self.context.console.print("[red]portfolio_ledger 模块未找到[/red]") if self.context.has_rich else print("portfolio_ledger 未找到")
815
+ return
816
+ ledger = PortfolioLedger()
817
+ names = []
818
+ try:
819
+ from strategy_vault import get_vault
820
+ names = get_vault().list_all_names()
821
+ except Exception:
822
+ pass
823
+ groups = ledger.positions_by_strategy(names)
824
+ if not groups:
825
+ self.context.console.print("[dim]暂无持仓。用 /deploy 或 /journal add 建仓后在此按策略查看。[/dim]"
826
+ if self.context.has_rich else "暂无持仓。")
827
+ return
828
+
829
+ syms = sorted({p["symbol"] for poss in groups.values() for p in poss})
830
+ if self.context.has_rich:
831
+ self.context.console.print(f" [dim]获取 {len(syms)} 只实时报价…[/dim]")
832
+ live = {}
833
+ try:
834
+ import yfinance as yf
835
+ for s in syms:
836
+ try:
837
+ h = yf.Ticker(s).history(period="1d")
838
+ if not h.empty:
839
+ live[s] = float(h["Close"].iloc[-1])
840
+ except Exception:
841
+ pass
842
+ except ImportError:
843
+ pass
844
+
845
+ g_cost = g_mv = 0.0
846
+ if self.context.has_rich:
847
+ from rich.table import Table
848
+ self.context.console.print()
849
+ self.context.console.print(" [bold cyan]持仓看板 · 按来源策略[/bold cyan]")
850
+ for grp, poss in groups.items():
851
+ tbl = Table(box=None, header_style="bold", pad_edge=False)
852
+ tbl.add_column("标的", width=8)
853
+ tbl.add_column("净持仓", justify="right", width=10)
854
+ tbl.add_column("均价", justify="right", width=10)
855
+ tbl.add_column("现价", justify="right", width=10)
856
+ tbl.add_column("市值", justify="right", width=12)
857
+ tbl.add_column("浮动盈亏", justify="right", width=13)
858
+ tbl.add_column("%", justify="right", width=8)
859
+ sub_cost = sub_mv = 0.0; have = False
860
+ for p in poss:
861
+ cost = p["cost_basis"]; sub_cost += cost
862
+ px = live.get(p["symbol"])
863
+ if px is not None:
864
+ have = True
865
+ mv = px * p["net_qty"]; sub_mv += mv
866
+ pnl = mv - cost; pct = (pnl / cost * 100) if cost else 0.0
867
+ c = "green" if pnl >= 0 else "red"
868
+ tbl.add_row(p["symbol"], f"{p['net_qty']:,.4g}", f"{p['avg_cost']:,.2f}",
869
+ f"{px:,.2f}", f"{mv:,.0f}", f"[{c}]{pnl:+,.0f}[/{c}]",
870
+ f"[{c}]{pct:+.1f}%[/{c}]")
871
+ else:
872
+ sub_mv += cost
873
+ tbl.add_row(p["symbol"], f"{p['net_qty']:,.4g}", f"{p['avg_cost']:,.2f}",
874
+ "N/A", f"{cost:,.0f}", "—", "—")
875
+ g_cost += sub_cost; g_mv += sub_mv
876
+ self.context.console.print(f"\n [bold]{grp}[/bold] [dim]{len(poss)} 持仓 · 成本 {sub_cost:,.0f}[/dim]")
877
+ self.context.console.print(tbl)
878
+ if have:
879
+ spnl = sub_mv - sub_cost; sc = "green" if spnl >= 0 else "red"
880
+ spct = spnl / sub_cost * 100 if sub_cost else 0.0
881
+ self.context.console.print(f" [dim]小计 市值 {sub_mv:,.0f} · 浮盈 [{sc}]{spnl:+,.0f} ({spct:+.1f}%)[/{sc}][/dim]")
882
+ gpnl = g_mv - g_cost; gc = "green" if gpnl >= 0 else "red"
883
+ gpct = gpnl / g_cost * 100 if g_cost else 0.0
884
+ self.context.console.print(f"\n [bold]合计[/bold] 成本 {g_cost:,.0f} · 市值 {g_mv:,.0f} · "
885
+ f"浮盈 [{gc}]{gpnl:+,.0f} ({gpct:+.1f}%)[/{gc}]")
886
+ self.context.console.print()
887
+ else:
888
+ for grp, poss in groups.items():
889
+ print(f"[{grp}]")
890
+ for p in poss:
891
+ px = live.get(p["symbol"])
892
+ pnl = (px * p["net_qty"] - p["cost_basis"]) if px else None
893
+ print(f" {p['symbol']} net={p['net_qty']:g} avg={p['avg_cost']:.2f}"
894
+ + (f" pnl={pnl:+.0f}" if pnl is not None else ""))
895
+
896
+ def cmd_apply_plan(self, args: str):
897
+ """Execute the pending command plan sequentially."""
898
+ plan = list(getattr(self.terminal, "pending_plan", []) or [])
899
+ arg_tokens = args.split()
900
+ start_idx = 0
901
+ if "--from" in arg_tokens:
902
+ idx = arg_tokens.index("--from")
903
+ if idx + 1 >= len(arg_tokens):
904
+ msg = "Usage: /apply-plan --from <step_number>"
905
+ self.context.console.print(f"[dim]{msg}[/dim]" if self.context.has_rich else msg)
906
+ return
907
+ try:
908
+ start_idx = max(0, int(arg_tokens[idx + 1]) - 1)
909
+ except ValueError:
910
+ msg = "Invalid step number for --from"
911
+ self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
912
+ return
913
+
914
+ if not plan:
915
+ self.context.console.print("[dim]No pending plan. Use /plan first.[/dim]" if self.context.has_rich
916
+ else "No pending plan. Use /plan first.")
917
+ return
918
+ if start_idx > 0:
919
+ if start_idx >= len(plan):
920
+ msg = f"--from {start_idx + 1} exceeds available steps ({len(plan)})"
921
+ self.context.console.print(f"[red]{msg}[/red]" if self.context.has_rich else msg)
922
+ return
923
+ plan = plan[start_idx:]
924
+ if "--resume" in arg_tokens and self.context.has_rich:
925
+ self.context.console.print(f"[dim]Resuming execution from step 1 of remaining {len(plan)} step(s).[/dim]")
926
+
927
+ policy = self.terminal.config.get("command_policy", "safe")
928
+ results = []
929
+ failed = None
930
+ for i, step in enumerate(plan, 1):
931
+ started_at = time.time()
932
+ if self.context.has_rich:
933
+ self.context.console.print(f"[dim]Step {i}/{len(plan)}:[/dim] [bold]{step}[/bold]")
934
+ else:
935
+ print(f"Step {i}/{len(plan)}: {step}")
936
+
937
+ # Was evaluate_command_policy(step, policy) — which silently
938
+ # evaluated every step as workspace-write with networking on,
939
+ # ignoring the session's permission_mode and network_enabled.
940
+ # Only .risk was read (and risk is mode-independent), so nothing
941
+ # executed that shouldn't have; the service closes the gap before
942
+ # anyone reads .allowed here and gets a wrong answer.
943
+ from aria_code.safety import SafetyService
944
+ step_decision = SafetyService(self.terminal.config).evaluate_command(step, policy)
945
+ if step_decision.risk == "high":
946
+ if not self._confirm_high_risk_command(step_decision.normalized_command, step_decision.risk, policy):
947
+ failed = (i, step, "Cancelled by user at high-risk step confirmation")
948
+ results.append({
949
+ "step": step,
950
+ "status": "blocked",
951
+ "duration": round(time.time() - started_at, 3),
952
+ "exit_code": None,
953
+ "error": failed[2],
954
+ })
955
+ break
956
+
957
+ res = _tool_run_command({"command": step, "policy": policy})
958
+ duration = time.time() - started_at
959
+ exit_code = res.get("data", {}).get("exit_code", None) if res.get("success") else None
960
+ status = "completed" if res.get("success") and exit_code == 0 else "failed"
961
+ results.append({
962
+ "step": step,
963
+ "status": status,
964
+ "duration": round(duration, 3),
965
+ "exit_code": exit_code,
966
+ "error": None if status == "completed" else (res.get("error") or f"Command exited {exit_code}"),
967
+ })
968
+ if not res.get("success"):
969
+ failed = (i, step, res.get("error", "Unknown error"))
970
+ break
971
+ exit_code = res.get("data", {}).get("exit_code", 0)
972
+ if exit_code != 0:
973
+ failed = (i, step, f"Command exited {exit_code}")
974
+ break
975
+
976
+ self.terminal.last_plan_results = results
977
+
978
+ if failed:
979
+ idx, step, err = failed
980
+ self.terminal.pending_plan = plan[idx - 1:]
981
+ if self.context.has_rich:
982
+ self.context.console.print(f"[red]Plan failed at step {idx}[/red]: [bold]{step}[/bold]")
983
+ self.context.console.print(f"[red]{err}[/red]")
984
+ self.context.console.print("[dim]Recovery hints:[/dim]")
985
+ if "blocked by policy" in (err or "").lower():
986
+ self.context.console.print(" [dim]> /run --dry-run <command> to inspect risk[/dim]")
987
+ self.context.console.print(" [dim]> /config set command_policy=balanced (or full) if needed[/dim]")
988
+ else:
989
+ self.context.console.print(" [dim]> Fix code/config, then rerun /apply-plan[/dim]")
990
+ self.context.console.print(" [dim]> Use /git diff to inspect changes[/dim]")
991
+ else:
992
+ print(f"Plan failed at step {idx}: {step}\n{err}")
993
+ if "blocked by policy" in (err or "").lower():
994
+ print("Recovery: /run --dry-run <command> and /config set command_policy=balanced")
995
+ else:
996
+ print("Recovery: fix issue, then rerun /apply-plan")
997
+ else:
998
+ if self.context.has_rich:
999
+ self.context.console.print(f"[green]Plan completed ({len(plan)} steps)[/green]")
1000
+ for i, row in enumerate(results, 1):
1001
+ self.context.console.print(f" [dim]{i}. {row['step']} ({row['duration']}s)[/dim]")
1002
+ else:
1003
+ print(f"Plan completed ({len(plan)} steps)")
1004
+ self.terminal.pending_plan = []
1005
+
1006
+ async def cmd_deep(self, args: str):
1007
+ """
1008
+ 深度多层研究(Claude-Code 架构 P0–P3):
1009
+ 团队并行 → 主题分组 → 工具深挖 → 量化融合+置信度校准 → Critic 自检 → 分级报告
1010
+ Usage: /deep NVDA ← 标准档
1011
+ /deep AAPL --deep ← 深度档(含量化地面真值/自检/数据血缘)
1012
+ /deep 000333 --brief ← 简报档
1013
+ /deep TSLA --agents technical,risk,macro
1014
+ /deep calibrate ← 用真实价回评历史预测,更新置信度校准
1015
+ """
1016
+ def _latest_close(symbol: str):
1017
+ try:
1018
+ import data_cleaner
1019
+ df, _ = data_cleaner.get_clean_prices(symbol, period="5d")
1020
+ if df is not None and len(df):
1021
+ for col in ("close", "Close", "adj_close", "收盘"):
1022
+ if col in df.columns:
1023
+ return float(df[col].iloc[-1])
1024
+ except Exception:
1025
+ pass
1026
+ return None
1027
+
1028
+ # /deep calibrate — score logged predictions against realised price (P2 loop)
1029
+ if args.strip().lower().startswith(("calibrate", "校准")):
1030
+ from agents.deep.calibration_loop import (
1031
+ PredictionLog, evaluate_due, evaluate_from_ledger)
1032
+ from agents.deep.quant_fusion import CalibrationStore
1033
+ store, log = CalibrationStore(), PredictionLog()
1034
+ led_res = {"evaluated": 0, "hits": 0}
1035
+ try: # actual realised P&L first — the strongest ground truth
1036
+ from portfolio_ledger import PortfolioLedger
1037
+ led_res = evaluate_from_ledger(store, log, PortfolioLedger())
1038
+ except Exception:
1039
+ pass
1040
+ px_res = evaluate_due(store, log, _latest_close) # market price for the rest
1041
+ total = led_res["evaluated"] + px_res["evaluated"]
1042
+ hits = led_res["hits"] + px_res["hits"]
1043
+ if total:
1044
+ msg = (f"校准完成:评估 {total} 条(实盘 {led_res['evaluated']} + "
1045
+ f"市价 {px_res['evaluated']}),命中 {hits},"
1046
+ f"命中率 {hits / total:.0%}(置信度校准已更新)")
1047
+ else:
1048
+ msg = "暂无到期预测可校准(先用 /deep 跑几次分析积累预测)。"
1049
+ self.context.console.print(f"[green]✓[/green] {msg}") if self.context.has_rich else print(msg)
1050
+ return
1051
+
1052
+ team_args = parse_team_args(args)
1053
+ symbols = resolve_team_symbols(team_args, self.terminal.config)
1054
+ agent_names = team_agent_names(team_args)
1055
+ _low = args.lower()
1056
+ tier = ("deep" if ("--deep" in _low or "--full" in _low)
1057
+ else "brief" if "--brief" in _low else "standard")
1058
+ _zh = sum(1 for c in args if '一' <= c <= '鿿')
1059
+ _lang = "zh" if _zh / max(len(args), 1) > 0.15 else "en"
1060
+
1061
+ from agents.deep.tiers import render_tier
1062
+ from ui.render.team import render_agent_tree_root, render_agent_node
1063
+
1064
+ for sym in symbols:
1065
+ def _on_agent_done(name, result):
1066
+ _kps = getattr(result, "key_points", None)
1067
+ _kp = (_kps[0] if isinstance(_kps, (list, tuple)) and _kps else "")
1068
+ if self.context.has_rich:
1069
+ render_agent_node(
1070
+ self.context.console, name, getattr(result, "signal", None), _kp,
1071
+ success=bool(getattr(result, "success", True)),
1072
+ error=getattr(result, "error", None),
1073
+ degraded=bool(getattr(result, "degraded", False)),
1074
+ lang=_lang,
1075
+ )
1076
+ else:
1077
+ print(f" ⎿ {name} {getattr(result, 'signal', '')} {_kp[:50]}")
1078
+
1079
+ if self.context.has_rich:
1080
+ render_agent_tree_root(self.context.console, sym, len(agent_names), lang=_lang)
1081
+ else:
1082
+ print(f"\n ⏺ 深度分析 {sym} {len(agent_names)} 个分析师")
1083
+
1084
+ try:
1085
+ result = await run_deep_cli(
1086
+ symbol=sym, args=team_args, config=self.terminal.config,
1087
+ lang=_lang, on_agent_done=_on_agent_done,
1088
+ )
1089
+ except Exception as e:
1090
+ print_error(self.context, str(e), "deep")
1091
+ continue
1092
+
1093
+ md = render_tier(result, tier)
1094
+ if self.context.has_rich:
1095
+ from rich import box as _box
1096
+ from rich.markdown import Markdown
1097
+ from rich.panel import Panel
1098
+ self.context.console.print(Panel(
1099
+ Markdown(md), border_style="dim", box=_box.ROUNDED,
1100
+ title=f"[bold]深度研究 · {sym}[/bold] [dim]({tier})[/dim]",
1101
+ title_align="left", padding=(1, 2),
1102
+ ))
1103
+ else:
1104
+ print("\n" + md)
1105
+
1106
+ # P2 closed loop: log the verdict so /deep calibrate can score it later
1107
+ try:
1108
+ from agents.deep.calibration_loop import PredictionLog
1109
+ _p = _latest_close(sym)
1110
+ if _p and result.final_signal:
1111
+ PredictionLog().log(sym, result.final_signal,
1112
+ result.calibrated_confidence, _p)
1113
+ except Exception:
1114
+ pass
1115
+
1116
+ async def cmd_team(self, args: str):
1117
+ """
1118
+ 多 Agent 金融研究团队:宏观 + 基本面 + 技术 + 风控 → 综合报告
1119
+ Usage: /team NVDA
1120
+ /team 000333 --agents technical,risk
1121
+ /team watchlist
1122
+ /team AAPL --full ← 7-agent 完整模式(+新闻/催化剂/行业)
1123
+ /team AAPL --pipeline ← 开启 DAG 串行协作模式(Context Sharing)
1124
+ """
1125
+
1126
+ import sys as _sys
1127
+ team_args = parse_team_args(args)
1128
+ symbols = resolve_team_symbols(team_args, self.terminal.config)
1129
+ agent_names = team_agent_names(team_args)
1130
+ # The UI language, unless the request itself is Chinese. Guessing from
1131
+ # the arguments alone made "/team AAPL" English in a Chinese session.
1132
+ _zh = sum(1 for c in args if '一' <= c <= '鿿')
1133
+ _ui_zh = str(self.terminal.config.get("ui_lang", "en")).lower().startswith("zh")
1134
+ _lang = "zh" if (_zh / max(len(args), 1) > 0.15 or _ui_zh) else "en"
1135
+ _T = (lambda a, b: a) if _lang == "zh" else (lambda a, b: b)
1136
+
1137
+ for sym in symbols:
1138
+ _agent_count = len(agent_names)
1139
+
1140
+ # ── Streaming nested agent tree (Claude Code-style) ──────────────
1141
+ from ui.render.team import (
1142
+ render_agent_tree_root, render_agent_node,
1143
+ render_agent_synthesis_leaf,
1144
+ )
1145
+
1146
+ def _on_agent_done(name, result):
1147
+ # Fires as each analyst finishes — render its leaf live.
1148
+ _kp = ""
1149
+ _kps = getattr(result, "key_points", None)
1150
+ if _kps:
1151
+ _kp = _kps[0] if isinstance(_kps, (list, tuple)) else str(_kps)
1152
+ if self.context.has_rich:
1153
+ render_agent_node(
1154
+ self.context.console, name,
1155
+ getattr(result, "signal", None), _kp,
1156
+ success=bool(getattr(result, "success", True)),
1157
+ error=getattr(result, "error", None),
1158
+ degraded=bool(getattr(result, "degraded", False)),
1159
+ lang=_lang,
1160
+ )
1161
+ else:
1162
+ print(f" ⎿ {name} {getattr(result, 'signal', '')} {_kp[:50]}")
1163
+
1164
+ if self.context.has_rich:
1165
+ render_agent_tree_root(self.context.console, sym, _agent_count, lang=_lang)
1166
+ else:
1167
+ print(f"\n ⏺ 多代理分析 {sym} {_agent_count} 个分析师并行")
1168
+
1169
+ try:
1170
+ # Create a stream consumer for tool rendering and thinking animations
1171
+ from apps.cli.runtime_consumer import TerminalRuntimeEventConsumer
1172
+ consumer = TerminalRuntimeEventConsumer(
1173
+ terminal=self.terminal,
1174
+ console=self.context.console,
1175
+ has_rich=self.context.has_rich,
1176
+ markdown_cls=None,
1177
+ live_cls=None,
1178
+ strip_latex=lambda x: x,
1179
+ fallback_from="local",
1180
+ ui_lang=_lang,
1181
+ )
1182
+
1183
+ # ── 新 Agent 系统(无 Ollama 依赖)────────────────────────
1184
+ _analysis = await run_team_analysis(
1185
+ symbol=sym,
1186
+ args=team_args,
1187
+ config=self.terminal.config,
1188
+ sanitize_result=_sanitize_team_result_with_market_data,
1189
+ lang=_lang,
1190
+ on_agent_done=_on_agent_done,
1191
+ on_token=consumer.on_token,
1192
+ on_thought=consumer.on_thinking,
1193
+ on_tool_start=consumer.on_tool_call,
1194
+ on_tool_end=consumer.on_tool_result,
1195
+ )
1196
+
1197
+ # A tool whose result never came back leaves its spinner up,
1198
+ # and the prompt does not return while it runs.
1199
+ consumer.close_tool_spinner()
1200
+ team_result = _analysis.team_result
1201
+ _data_bundle = _analysis.data_bundle
1202
+ _quality_notes = _analysis.quality_notes or []
1203
+
1204
+ if self.context.has_rich:
1205
+ # Synthesis leaf closes the tree, then the detailed _get_Panel()
1206
+ render_agent_synthesis_leaf(
1207
+ self.context.console,
1208
+ team_result.final_signal,
1209
+ team_result.confidence,
1210
+ team_result.elapsed_sec,
1211
+ lang=_lang,
1212
+ )
1213
+ if _quality_notes:
1214
+ from rich.markup import escape as _esc_q
1215
+ self.context.console.print(
1216
+ f" [yellow]{_T('数据质量警告', 'Data quality')}:[/yellow] "
1217
+ + _esc_q("; ".join(_quality_notes[:3]))
1218
+ )
1219
+
1220
+ # Signal divergence notice — only when DebateAgent ran
1221
+ _has_debate = any(
1222
+ getattr(r, "agent", "") == "debate"
1223
+ for r in (team_result.results or [])
1224
+ )
1225
+ if _has_debate:
1226
+ self.context.console.print(
1227
+ f" [#C08050]🔥 {_T('信号分歧已触发 DebateAgent 调解', 'Signals disagreed; the debate agent weighed in')}[/#C08050]"
1228
+ )
1229
+
1230
+ # Synthesis in a _get_Panel() for visual separation
1231
+ from rich import box as _rbox_team
1232
+ from ui.render.team import SIGNAL_COLORS as _SC, VERDICT_STYLE as _VS
1233
+ from apps.cli.commands.team import (
1234
+ build_team_terminal_summary as _team_terminal_summary,
1235
+ clean_team_synthesis_text as _clean_team_synthesis,
1236
+ )
1237
+ from rich.markup import escape as _esc_syn
1238
+ _syn = _esc_syn(_clean_team_synthesis(
1239
+ team_result.synthesis or _T("(无综合结论)", "(no synthesis)")))
1240
+ _market_summary = _team_terminal_summary(_data_bundle, lang=_lang)
1241
+ _elapsed = f" [dim]{_T('耗时', 'took')} {team_result.elapsed_sec:.1f}s[/dim]"
1242
+ _sig_str = team_result.final_signal or ""
1243
+ _conf_str = (f" [dim]{_T('置信度', 'confidence')} {team_result.confidence:.0%}[/dim]"
1244
+ if team_result.confidence else "")
1245
+ _sig_color = _SC.get(_sig_str.upper(), "dim")
1246
+ _sig_icon = _VS.get(_sig_str.upper(), ("dim", "●"))[1]
1247
+ _footer = (f"[{_sig_color}]{_sig_icon} {_sig_str}[/{_sig_color}]"
1248
+ f"{_conf_str}{_elapsed}")
1249
+ self.context.console.print(Panel(
1250
+ f"{_market_summary}\n\n{_syn}\n\n{_footer}",
1251
+ title=f"[bold]{_T('综合结论', 'Conclusion')}[/bold]",
1252
+ box=_rbox_team.ROUNDED,
1253
+ border_style="#C08050",
1254
+ padding=(0, 1),
1255
+ ))
1256
+ else:
1257
+ # agents already streamed via _on_agent_done (plain print)
1258
+ if _quality_notes:
1259
+ print(f" {_T('数据质量警告', 'Data quality')}: " + "; ".join(_quality_notes[:3]))
1260
+ print(f"\n ── {_T('综合结论', 'Conclusion')} ──")
1261
+ from apps.cli.commands.team import (
1262
+ build_team_terminal_summary as _team_terminal_summary,
1263
+ clean_team_synthesis_text as _clean_team_synthesis,
1264
+ )
1265
+ print(_team_terminal_summary(_data_bundle, lang=_lang))
1266
+ print()
1267
+ print(_clean_team_synthesis(team_result.synthesis or _T("(无综合结论)", "(no synthesis)")))
1268
+ print(f"\n {_T('耗时', 'took')} {team_result.elapsed_sec:.1f}s "
1269
+ f"Signal: {team_result.final_signal} "
1270
+ f"{_T('置信度', 'confidence')}: {team_result.confidence:.0%}")
1271
+
1272
+ # 保存报告
1273
+ await self._save_team_report(sym, team_result, _data_bundle, _quality_notes, lang=_lang)
1274
+
1275
+ # Record the directional call for outcome verification (DPO loop).
1276
+ # synthesis + final_signal → detect_direction; entry price fetched
1277
+ # by _record_prediction. Best-effort, never blocks.
1278
+ try:
1279
+ _call_text = f"{team_result.synthesis or ''} {team_result.final_signal or ''}"
1280
+ self.terminal._record_prediction(sym, _call_text)
1281
+ except Exception:
1282
+ pass
1283
+
1284
+ except ImportError as _imp_err:
1285
+ # agents 包不可用 — 不再回退到已废弃的 financial_agents
1286
+ _m = (f"多代理分析模块加载失败:{_imp_err}。"
1287
+ "请确认 agents 包完整(/install 或 pip install -e .)。")
1288
+ self.context.console.print(f"\n [red]{_m}[/red]") if self.context.has_rich else print(f"\n {_m}")
1289
+ continue
1290
+ except Exception as e:
1291
+ if "consumer" in locals():
1292
+ consumer.close_tool_spinner()
1293
+ msg = f"团队分析失败: {e}" if _lang == "zh" else f"Team analysis failed: {e}"
1294
+ self.context.console.print(f"\n [red]{msg}[/red]") if self.context.has_rich else print(f"\n {msg}")
1295
+ continue
1296
+
1297
+ async def _save_team_report(self, symbol: str, team_result, data_bundle=None, quality_notes: Optional[list] = None,
1298
+ lang: str = "zh") -> None:
1299
+ """将 /team 分析结果保存为 Markdown 报告"""
1300
+ saved = save_team_report(
1301
+ symbol=symbol,
1302
+ team_result=team_result,
1303
+ data_bundle=data_bundle,
1304
+ quality_notes=quality_notes,
1305
+ )
1306
+ try:
1307
+ parts = saved.path.parts
1308
+ short_path = "/".join(parts[-5:]) if len(parts) > 5 else str(saved.path)
1309
+ except Exception:
1310
+ short_path = str(saved.path)
1311
+ label = "报告已保存" if str(lang).startswith("zh") else "Report saved"
1312
+ if self.context.has_rich:
1313
+ self.context.console.print(
1314
+ f" [dim]{label}:[/dim] [link={_file_uri(saved.path)}]…/{short_path}[/link]")
1315
+ else:
1316
+ print(f" {label}: .../{short_path}")