aria-code 0.52.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (478) hide show
  1. aria_code/__init__.py +42 -0
  2. aria_code/_version.py +20 -0
  3. aria_code/adk_apps/__init__.py +1 -0
  4. aria_code/adk_apps/aria_code_review/__init__.py +1 -0
  5. aria_code/adk_apps/aria_code_review/agent.py +32 -0
  6. aria_code/adk_apps/aria_research/__init__.py +1 -0
  7. aria_code/adk_apps/aria_research/agent.py +31 -0
  8. aria_code/agents/__init__.py +34 -0
  9. aria_code/agents/base.py +318 -0
  10. aria_code/agents/code_review.py +200 -0
  11. aria_code/agents/deep/__init__.py +37 -0
  12. aria_code/agents/deep/calibration_loop.py +144 -0
  13. aria_code/agents/deep/critic.py +125 -0
  14. aria_code/agents/deep/deepen.py +193 -0
  15. aria_code/agents/deep/models.py +149 -0
  16. aria_code/agents/deep/pipeline.py +164 -0
  17. aria_code/agents/deep/quant_fusion.py +192 -0
  18. aria_code/agents/deep/themes.py +95 -0
  19. aria_code/agents/deep/tiers.py +106 -0
  20. aria_code/agents/engineering/__init__.py +14 -0
  21. aria_code/agents/engineering/coder.py +222 -0
  22. aria_code/agents/engineering/tester.py +145 -0
  23. aria_code/agents/financial/__init__.py +21 -0
  24. aria_code/agents/financial/cashflow_burn.py +119 -0
  25. aria_code/agents/financial/catalyst.py +279 -0
  26. aria_code/agents/financial/corporate_finance.py +175 -0
  27. aria_code/agents/financial/debate.py +224 -0
  28. aria_code/agents/financial/earnings.py +303 -0
  29. aria_code/agents/financial/fundamental.py +191 -0
  30. aria_code/agents/financial/macro.py +99 -0
  31. aria_code/agents/financial/news.py +207 -0
  32. aria_code/agents/financial/risk.py +165 -0
  33. aria_code/agents/financial/sector.py +279 -0
  34. aria_code/agents/financial/strategist.py +121 -0
  35. aria_code/agents/financial/stripe_revenue.py +127 -0
  36. aria_code/agents/financial/synthesis.py +274 -0
  37. aria_code/agents/financial/technical.py +258 -0
  38. aria_code/agents/orchestrator.py +44 -0
  39. aria_code/agents/portfolio_agent.py +363 -0
  40. aria_code/agents/realty/__init__.py +62 -0
  41. aria_code/agents/realty/asset_diagnosis.py +152 -0
  42. aria_code/agents/realty/business_match.py +166 -0
  43. aria_code/agents/realty/cashflow_verify.py +209 -0
  44. aria_code/agents/realty/contract_rules.py +210 -0
  45. aria_code/agents/realty/energy_anomaly.py +189 -0
  46. aria_code/agents/realty/exit_settlement.py +208 -0
  47. aria_code/agents/realty/fulfillment_risk.py +206 -0
  48. aria_code/agents/realty/ops_optimize.py +160 -0
  49. aria_code/agents/realty/revenue_share.py +215 -0
  50. aria_code/agents/registry.py +158 -0
  51. aria_code/agents/signal_scheme.py +111 -0
  52. aria_code/agents/sports/__init__.py +0 -0
  53. aria_code/agents/sports/football_agent.py +197 -0
  54. aria_code/agents/supervisor.py +113 -0
  55. aria_code/agents/team.py +492 -0
  56. aria_code/agents/warehouse/__init__.py +25 -0
  57. aria_code/agents/warehouse/contracts.py +26 -0
  58. aria_code/agents/warehouse/dashboard.py +155 -0
  59. aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
  60. aria_code/agents/warehouse/inbound_exceptions.py +25 -0
  61. aria_code/agents/warehouse/inventory_health.py +24 -0
  62. aria_code/agents/warehouse/local_run.py +71 -0
  63. aria_code/agents/warehouse/local_snapshot.py +132 -0
  64. aria_code/agents/warehouse/logistics_cost.py +84 -0
  65. aria_code/agents/warehouse/logistics_sync.py +21 -0
  66. aria_code/agents/warehouse/workflow.py +31 -0
  67. aria_code/aliyun_data_client.py +4 -0
  68. aria_code/apps/README.md +12 -0
  69. aria_code/apps/__init__.py +2 -0
  70. aria_code/apps/channels/README.md +15 -0
  71. aria_code/apps/channels/__init__.py +17 -0
  72. aria_code/apps/channels/intake.py +92 -0
  73. aria_code/apps/channels/registry.py +95 -0
  74. aria_code/apps/channels/tradingview.py +94 -0
  75. aria_code/apps/cli/README.md +13 -0
  76. aria_code/apps/cli/__init__.py +2 -0
  77. aria_code/apps/cli/_optional.py +120 -0
  78. aria_code/apps/cli/bootstrap.py +145 -0
  79. aria_code/apps/cli/broker_render.py +206 -0
  80. aria_code/apps/cli/codegen_paths.py +29 -0
  81. aria_code/apps/cli/commands/__init__.py +16 -0
  82. aria_code/apps/cli/commands/_business_render.py +78 -0
  83. aria_code/apps/cli/commands/_ui.py +71 -0
  84. aria_code/apps/cli/commands/analysis_cmds.py +369 -0
  85. aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
  86. aria_code/apps/cli/commands/auth_cmds.py +239 -0
  87. aria_code/apps/cli/commands/backtest_cmds.py +2543 -0
  88. aria_code/apps/cli/commands/broker_cmds.py +1234 -0
  89. aria_code/apps/cli/commands/business_workflow_cmds.py +319 -0
  90. aria_code/apps/cli/commands/canvas_cmds.py +77 -0
  91. aria_code/apps/cli/commands/catalog.py +87 -0
  92. aria_code/apps/cli/commands/core_cmds.py +2520 -0
  93. aria_code/apps/cli/commands/data_cmds.py +457 -0
  94. aria_code/apps/cli/commands/diagnostic_cmds.py +230 -0
  95. aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
  96. aria_code/apps/cli/commands/file_cmds.py +312 -0
  97. aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
  98. aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
  99. aria_code/apps/cli/commands/market.py +434 -0
  100. aria_code/apps/cli/commands/market_cmds.py +1381 -0
  101. aria_code/apps/cli/commands/market_context.py +425 -0
  102. aria_code/apps/cli/commands/market_render.py +7 -0
  103. aria_code/apps/cli/commands/model_cmds.py +2153 -0
  104. aria_code/apps/cli/commands/ops_cmds.py +721 -0
  105. aria_code/apps/cli/commands/orchestrator_cmds.py +179 -0
  106. aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
  107. aria_code/apps/cli/commands/portfolio_cmds.py +1278 -0
  108. aria_code/apps/cli/commands/report.py +437 -0
  109. aria_code/apps/cli/commands/scaffold_templates.py +617 -0
  110. aria_code/apps/cli/commands/session_cmds.py +236 -0
  111. aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
  112. aria_code/apps/cli/commands/team.py +669 -0
  113. aria_code/apps/cli/commands/team_render.py +8 -0
  114. aria_code/apps/cli/commands/ui_cmds.py +401 -0
  115. aria_code/apps/cli/commands/warehouse_cmds.py +79 -0
  116. aria_code/apps/cli/commands/workflow_cmds.py +480 -0
  117. aria_code/apps/cli/commands/workspace_cmds.py +1494 -0
  118. aria_code/apps/cli/config_paths.py +73 -0
  119. aria_code/apps/cli/config_store.py +78 -0
  120. aria_code/apps/cli/constants.py +88 -0
  121. aria_code/apps/cli/context.py +34 -0
  122. aria_code/apps/cli/deterministic.py +191 -0
  123. aria_code/apps/cli/direct.py +48 -0
  124. aria_code/apps/cli/finance_service_catalog.py +111 -0
  125. aria_code/apps/cli/football_reports.py +231 -0
  126. aria_code/apps/cli/github_app_auth.py +135 -0
  127. aria_code/apps/cli/google_login.py +196 -0
  128. aria_code/apps/cli/handlers/__init__.py +11 -0
  129. aria_code/apps/cli/handlers/broker_handlers.py +122 -0
  130. aria_code/apps/cli/handlers/chart_handlers.py +1309 -0
  131. aria_code/apps/cli/handlers/market_handlers.py +2513 -0
  132. aria_code/apps/cli/handlers/realty_handlers.py +114 -0
  133. aria_code/apps/cli/handlers/sports_handlers.py +22 -0
  134. aria_code/apps/cli/handlers/strategy_advice.py +82 -0
  135. aria_code/apps/cli/helpers.py +302 -0
  136. aria_code/apps/cli/hooks.py +180 -0
  137. aria_code/apps/cli/i18n.py +297 -0
  138. aria_code/apps/cli/intent.py +136 -0
  139. aria_code/apps/cli/intent_router.py +276 -0
  140. aria_code/apps/cli/intent_signals.py +344 -0
  141. aria_code/apps/cli/lifecycle_hooks.py +48 -0
  142. aria_code/apps/cli/local_tool_schemas.py +536 -0
  143. aria_code/apps/cli/main.py +29 -0
  144. aria_code/apps/cli/market_metadata.py +135 -0
  145. aria_code/apps/cli/market_universe.py +487 -0
  146. aria_code/apps/cli/message_processing.py +310 -0
  147. aria_code/apps/cli/model_catalog.py +620 -0
  148. aria_code/apps/cli/pdf_report.py +977 -0
  149. aria_code/apps/cli/plan_mode.py +139 -0
  150. aria_code/apps/cli/plotly_html.py +15 -0
  151. aria_code/apps/cli/prediction_feedback.py +202 -0
  152. aria_code/apps/cli/preflight.py +502 -0
  153. aria_code/apps/cli/project_aria.py +60 -0
  154. aria_code/apps/cli/prompt_assembly.py +59 -0
  155. aria_code/apps/cli/prompts/__init__.py +0 -0
  156. aria_code/apps/cli/prompts/coding.py +53 -0
  157. aria_code/apps/cli/prompts/select.py +142 -0
  158. aria_code/apps/cli/prompts/system_prompts.py +552 -0
  159. aria_code/apps/cli/prompts/ui.py +436 -0
  160. aria_code/apps/cli/provider_endpoints.py +237 -0
  161. aria_code/apps/cli/provider_keys.py +73 -0
  162. aria_code/apps/cli/providers/__init__.py +1 -0
  163. aria_code/apps/cli/providers/base.py +521 -0
  164. aria_code/apps/cli/providers/chat_routing.py +128 -0
  165. aria_code/apps/cli/providers/collaboration.py +98 -0
  166. aria_code/apps/cli/providers/llm/__init__.py +1 -0
  167. aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
  168. aria_code/apps/cli/providers/llm/ollama_stream.py +1594 -0
  169. aria_code/apps/cli/providers/llm/response_cache.py +60 -0
  170. aria_code/apps/cli/providers/llm/sse_stream.py +257 -0
  171. aria_code/apps/cli/providers/runtime_bridge.py +353 -0
  172. aria_code/apps/cli/providers/vertexai_stream.py +316 -0
  173. aria_code/apps/cli/runtime_consumer.py +657 -0
  174. aria_code/apps/cli/sandbox.py +32 -0
  175. aria_code/apps/cli/session_export.py +120 -0
  176. aria_code/apps/cli/session_jsonl.py +208 -0
  177. aria_code/apps/cli/session_store.py +112 -0
  178. aria_code/apps/cli/skills_catalog.py +273 -0
  179. aria_code/apps/cli/todo_tracker.py +190 -0
  180. aria_code/apps/cli/tool_executor.py +823 -0
  181. aria_code/apps/cli/tool_registry.py +34 -0
  182. aria_code/apps/cli/tool_scope.py +106 -0
  183. aria_code/apps/cli/tools/__init__.py +40 -0
  184. aria_code/apps/cli/tools/context.py +46 -0
  185. aria_code/apps/cli/tools/file_tools.py +180 -0
  186. aria_code/apps/cli/tools/market_tools.py +620 -0
  187. aria_code/apps/cli/tools/notebook_tools.py +111 -0
  188. aria_code/apps/cli/tools/system_tools.py +726 -0
  189. aria_code/apps/cli/tools/write_tools.py +845 -0
  190. aria_code/apps/cli/tradingview_bridge.py +529 -0
  191. aria_code/apps/cli/turn_planning.py +137 -0
  192. aria_code/apps/cli/update_check.py +153 -0
  193. aria_code/apps/cli/utils/__init__.py +0 -0
  194. aria_code/apps/cli/utils/market_detect.py +1597 -0
  195. aria_code/apps/daemon/README.md +14 -0
  196. aria_code/apps/vscode/README.md +115 -0
  197. aria_code/apps/vscode/package.json +70 -0
  198. aria_code/aria_cli.py +6743 -0
  199. aria_code/aria_daemon.py +1398 -0
  200. aria_code/aria_feishu_bot.py +1423 -0
  201. aria_code/aria_mcp_server.py +7 -0
  202. aria_code/aria_relay_client.py +4 -0
  203. aria_code/aria_relay_server.py +490 -0
  204. aria_code/aria_telegram_bot.py +202 -0
  205. aria_code/ariarc.py +385 -0
  206. aria_code/artifacts.py +581 -0
  207. aria_code/backtest_engine.py +4 -0
  208. aria_code/backtest_report.py +4 -0
  209. aria_code/brokers/__init__.py +78 -0
  210. aria_code/brokers/automation.py +193 -0
  211. aria_code/brokers/base.py +231 -0
  212. aria_code/brokers/capabilities.py +264 -0
  213. aria_code/brokers/cn/__init__.py +10 -0
  214. aria_code/brokers/cn/easytrader_broker.py +193 -0
  215. aria_code/brokers/cn/futu_broker.py +194 -0
  216. aria_code/brokers/cn/longbridge_broker.py +190 -0
  217. aria_code/brokers/cn/tiger_broker.py +196 -0
  218. aria_code/brokers/cn/xtquant_broker.py +225 -0
  219. aria_code/brokers/config.py +414 -0
  220. aria_code/brokers/intl/__init__.py +5 -0
  221. aria_code/brokers/intl/alpaca_broker.py +183 -0
  222. aria_code/brokers/intl/ibkr_broker.py +215 -0
  223. aria_code/brokers/intl/trading212_broker.py +183 -0
  224. aria_code/brokers/intl/webull_broker.py +156 -0
  225. aria_code/brokers/paper_broker.py +266 -0
  226. aria_code/brokers/planning.py +296 -0
  227. aria_code/brokers/registry.py +213 -0
  228. aria_code/brokers/trading.py +267 -0
  229. aria_code/canva_client.py +4 -0
  230. aria_code/change_store.py +127 -0
  231. aria_code/clients/__init__.py +0 -0
  232. aria_code/clients/aliyun_data_client.py +660 -0
  233. aria_code/clients/aria_relay_client.py +182 -0
  234. aria_code/clients/canva_client.py +284 -0
  235. aria_code/clients/figma_client.py +100 -0
  236. aria_code/clients/football_data_client.py +1671 -0
  237. aria_code/clients/kling_video_client.py +182 -0
  238. aria_code/clients/market_data_client.py +1976 -0
  239. aria_code/clients/openai_image_client.py +242 -0
  240. aria_code/clients/runway_video_client.py +157 -0
  241. aria_code/clients/warehouse_erp_client.py +105 -0
  242. aria_code/cloud_memory.py +181 -0
  243. aria_code/command_safety.py +19 -0
  244. aria_code/computer_use_tools.py +4 -0
  245. aria_code/dashboard_generator.py +579 -0
  246. aria_code/data_analysis_tools.py +4 -0
  247. aria_code/data_cleaner.py +483 -0
  248. aria_code/data_service.py +513 -0
  249. aria_code/datasources/__init__.py +23 -0
  250. aria_code/datasources/base.py +166 -0
  251. aria_code/datasources/router.py +253 -0
  252. aria_code/datasources/sources/__init__.py +15 -0
  253. aria_code/datasources/sources/akshare_source.py +269 -0
  254. aria_code/datasources/sources/alpha_vantage_source.py +203 -0
  255. aria_code/datasources/sources/edgar_source.py +219 -0
  256. aria_code/datasources/sources/finnhub_source.py +198 -0
  257. aria_code/datasources/sources/fred_source.py +223 -0
  258. aria_code/datasources/sources/tushare_source.py +143 -0
  259. aria_code/datasources/sources/web_scraper_source.py +278 -0
  260. aria_code/datasources/sources/world_bank_source.py +206 -0
  261. aria_code/datasources/sources/yfinance_source.py +152 -0
  262. aria_code/demo_player.py +207 -0
  263. aria_code/doctor.py +813 -0
  264. aria_code/domain/__init__.py +0 -0
  265. aria_code/domain/backtest_engine.py +354 -0
  266. aria_code/domain/backtest_report.py +472 -0
  267. aria_code/domain/finance_formulas.py +389 -0
  268. aria_code/domain/portfolio_ledger.py +307 -0
  269. aria_code/domain/strategy_vault.py +421 -0
  270. aria_code/evals/__init__.py +37 -0
  271. aria_code/evals/harness.py +598 -0
  272. aria_code/evals/runner.py +238 -0
  273. aria_code/external_agent_runner.py +115 -0
  274. aria_code/figma_client.py +4 -0
  275. aria_code/file_analysis_tools.py +4 -0
  276. aria_code/finance_formulas.py +4 -0
  277. aria_code/football_data_client.py +4 -0
  278. aria_code/image_gen_tools.py +4 -0
  279. aria_code/intent_classifier.py +386 -0
  280. aria_code/kling_video_client.py +4 -0
  281. aria_code/licensing.py +151 -0
  282. aria_code/local_finance_tools.py +4 -0
  283. aria_code/local_image_provider.py +4 -0
  284. aria_code/local_llm_provider.py +4 -0
  285. aria_code/macro_tools.py +4 -0
  286. aria_code/markdown_pdf.py +289 -0
  287. aria_code/market_data_client.py +4 -0
  288. aria_code/mcp_client.py +639 -0
  289. aria_code/memory_manager.py +246 -0
  290. aria_code/model_capability.py +486 -0
  291. aria_code/notification_tools.py +4 -0
  292. aria_code/openai_image_client.py +4 -0
  293. aria_code/packages/__init__.py +23 -0
  294. aria_code/packages/adk_bridge/__init__.py +11 -0
  295. aria_code/packages/adk_bridge/code_review_tools.py +49 -0
  296. aria_code/packages/adk_bridge/market_tools.py +125 -0
  297. aria_code/packages/aria_agents/__init__.py +5 -0
  298. aria_code/packages/aria_agents/manifest.py +78 -0
  299. aria_code/packages/aria_core/__init__.py +34 -0
  300. aria_code/packages/aria_core/architecture.py +269 -0
  301. aria_code/packages/aria_core/export.py +147 -0
  302. aria_code/packages/aria_core/manifest.py +65 -0
  303. aria_code/packages/aria_core/paths.py +57 -0
  304. aria_code/packages/aria_core/secure_file.py +68 -0
  305. aria_code/packages/aria_infra/__init__.py +15 -0
  306. aria_code/packages/aria_infra/arthera.py +52 -0
  307. aria_code/packages/aria_infra/doctor.py +246 -0
  308. aria_code/packages/aria_infra/product.py +40 -0
  309. aria_code/packages/aria_mcp/__init__.py +25 -0
  310. aria_code/packages/aria_mcp/bridge.py +201 -0
  311. aria_code/packages/aria_mcp/circuit.py +82 -0
  312. aria_code/packages/aria_mcp/config.py +111 -0
  313. aria_code/packages/aria_mcp/installer.py +95 -0
  314. aria_code/packages/aria_mcp/server.py +1550 -0
  315. aria_code/packages/aria_mcp/tools.py +81 -0
  316. aria_code/packages/aria_sdk/__init__.py +19 -0
  317. aria_code/packages/aria_sdk/client.py +396 -0
  318. aria_code/packages/aria_sdk/providers.py +70 -0
  319. aria_code/packages/aria_sdk/streaming.py +79 -0
  320. aria_code/packages/aria_sdk/types.py +86 -0
  321. aria_code/packages/aria_services/__init__.py +92 -0
  322. aria_code/packages/aria_services/cache.py +60 -0
  323. aria_code/packages/aria_services/context.py +356 -0
  324. aria_code/packages/aria_services/context_checkpoints.py +157 -0
  325. aria_code/packages/aria_services/data.py +11 -0
  326. aria_code/packages/aria_services/provider_health.py +268 -0
  327. aria_code/packages/aria_services/references.py +247 -0
  328. aria_code/packages/aria_services/registry.py +249 -0
  329. aria_code/packages/aria_services/research_protocol.py +57 -0
  330. aria_code/packages/aria_services/research_quality.py +186 -0
  331. aria_code/packages/aria_services/settings.py +157 -0
  332. aria_code/packages/aria_services/usage.py +151 -0
  333. aria_code/packages/aria_skills/__init__.py +46 -0
  334. aria_code/packages/aria_skills/catalog.py +149 -0
  335. aria_code/packages/aria_skills/loader.py +455 -0
  336. aria_code/packages/aria_skills/registry.py +84 -0
  337. aria_code/packages/aria_tools/__init__.py +5 -0
  338. aria_code/packages/aria_tools/financial/__init__.py +8 -0
  339. aria_code/packages/aria_tools/financial/_paths.py +130 -0
  340. aria_code/packages/aria_tools/financial/compliance.py +31 -0
  341. aria_code/packages/aria_tools/financial/factors.py +33 -0
  342. aria_code/packages/aria_tools/financial/risk_tools.py +33 -0
  343. aria_code/packages/aria_tools/financial/strategy.py +31 -0
  344. aria_code/packages/aria_tools/registry.py +128 -0
  345. aria_code/packages/quant_engine/__init__.py +34 -0
  346. aria_code/packages/quant_engine/_namespace.py +62 -0
  347. aria_code/packages/quant_engine/backtest/__init__.py +14 -0
  348. aria_code/packages/quant_engine/backtest/core.py +53 -0
  349. aria_code/packages/quant_engine/backtest/engine.py +96 -0
  350. aria_code/packages/quant_engine/portfolio/__init__.py +13 -0
  351. aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
  352. aria_code/packages/quant_engine/sports/__init__.py +80 -0
  353. aria_code/packages/quant_engine/sports/calibrator.py +354 -0
  354. aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
  355. aria_code/packages/quant_engine/sports/elo.py +300 -0
  356. aria_code/packages/quant_engine/sports/form.py +188 -0
  357. aria_code/packages/quant_engine/sports/h2h.py +195 -0
  358. aria_code/packages/quant_engine/sports/ml_model.py +377 -0
  359. aria_code/packages/quant_engine/sports/predictor.py +312 -0
  360. aria_code/packages/quant_engine/sports/tracker.py +665 -0
  361. aria_code/packages/quant_engine/stochastic/__init__.py +35 -0
  362. aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  363. aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
  364. aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  365. aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  366. aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
  367. aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  368. aria_code/packs/__init__.py +85 -0
  369. aria_code/packs/base.py +168 -0
  370. aria_code/packs/finance/__init__.py +250 -0
  371. aria_code/packs/logistics/__init__.py +169 -0
  372. aria_code/packs/payments/__init__.py +139 -0
  373. aria_code/packs/realty/__init__.py +190 -0
  374. aria_code/packs/registry.py +202 -0
  375. aria_code/packs/rules.py +90 -0
  376. aria_code/plan_utils.py +194 -0
  377. aria_code/plugin_loader.py +328 -0
  378. aria_code/portfolio_ledger.py +4 -0
  379. aria_code/preview_server.py +449 -0
  380. aria_code/privacy/__init__.py +5 -0
  381. aria_code/privacy/feedback.py +123 -0
  382. aria_code/project_review.py +278 -0
  383. aria_code/project_review_api.py +235 -0
  384. aria_code/project_review_server.py +18 -0
  385. aria_code/project_tools.py +4 -0
  386. aria_code/providers/__init__.py +30 -0
  387. aria_code/providers/llm/__init__.py +19 -0
  388. aria_code/providers/llm/anthropic.py +184 -0
  389. aria_code/providers/llm/autoconfig.py +213 -0
  390. aria_code/providers/llm/base.py +139 -0
  391. aria_code/providers/llm/custom.py +158 -0
  392. aria_code/providers/llm/ollama.py +129 -0
  393. aria_code/providers/llm/openai_compat.py +313 -0
  394. aria_code/providers/llm/registry.py +444 -0
  395. aria_code/providers/local_image_provider.py +202 -0
  396. aria_code/providers/local_llm_provider.py +589 -0
  397. aria_code/realty_data_tools.py +4 -0
  398. aria_code/report_exporters.py +150 -0
  399. aria_code/report_generator.py +1764 -0
  400. aria_code/runtime/__init__.py +169 -0
  401. aria_code/runtime/acceptance.py +481 -0
  402. aria_code/runtime/agent_loop.py +1520 -0
  403. aria_code/runtime/approval.py +57 -0
  404. aria_code/runtime/budget.py +232 -0
  405. aria_code/runtime/checkpoints.py +432 -0
  406. aria_code/runtime/crash_report.py +174 -0
  407. aria_code/runtime/events.py +113 -0
  408. aria_code/runtime/gateway.py +150 -0
  409. aria_code/runtime/lsp.py +484 -0
  410. aria_code/runtime/orchestrator.py +55 -0
  411. aria_code/runtime/repo_map.py +700 -0
  412. aria_code/runtime/run_state.py +127 -0
  413. aria_code/runtime/run_store.py +450 -0
  414. aria_code/runtime/self_healing.py +422 -0
  415. aria_code/runtime/subagent.py +547 -0
  416. aria_code/runtime/task_ledger.py +86 -0
  417. aria_code/runtime/task_orchestrator.py +90 -0
  418. aria_code/runtime/tool_executor.py +217 -0
  419. aria_code/runtime/tool_policy.py +107 -0
  420. aria_code/runtime/worktrees.py +215 -0
  421. aria_code/runway_video_client.py +4 -0
  422. aria_code/safety/__init__.py +23 -0
  423. aria_code/safety/permissions.py +306 -0
  424. aria_code/safety/service.py +102 -0
  425. aria_code/setup_wizard.py +673 -0
  426. aria_code/spreadsheet_tools.py +4 -0
  427. aria_code/strategy_vault.py +4 -0
  428. aria_code/tools/__init__.py +0 -0
  429. aria_code/tools/artifact_tools.py +252 -0
  430. aria_code/tools/broker_tools.py +92 -0
  431. aria_code/tools/build_quant_engine.py +97 -0
  432. aria_code/tools/code_audit_tools.py +192 -0
  433. aria_code/tools/computer_use_tools.py +504 -0
  434. aria_code/tools/data_analysis_tools.py +809 -0
  435. aria_code/tools/enterprise_finance_tools.py +156 -0
  436. aria_code/tools/extended_tools.py +240 -0
  437. aria_code/tools/file_analysis_tools.py +885 -0
  438. aria_code/tools/image_gen_tools.py +114 -0
  439. aria_code/tools/local_finance_tools.py +3417 -0
  440. aria_code/tools/logistics_tools.py +135 -0
  441. aria_code/tools/macro_tools.py +369 -0
  442. aria_code/tools/notification_tools.py +249 -0
  443. aria_code/tools/project_tools.py +525 -0
  444. aria_code/tools/realty_data_tools.py +659 -0
  445. aria_code/tools/spreadsheet_tools.py +388 -0
  446. aria_code/tools/stripe_tools.py +115 -0
  447. aria_code/ui/__init__.py +105 -0
  448. aria_code/ui/banner.py +508 -0
  449. aria_code/ui/completer.py +501 -0
  450. aria_code/ui/console.py +281 -0
  451. aria_code/ui/image_render.py +243 -0
  452. aria_code/ui/input_box.py +488 -0
  453. aria_code/ui/picker.py +201 -0
  454. aria_code/ui/render/__init__.py +11 -0
  455. aria_code/ui/render/finance.py +1705 -0
  456. aria_code/ui/render/market.py +225 -0
  457. aria_code/ui/render/output.py +775 -0
  458. aria_code/ui/render/responsive.py +75 -0
  459. aria_code/ui/render/team.py +354 -0
  460. aria_code/ui/robot.py +235 -0
  461. aria_code/ui/startup_dashboard.py +109 -0
  462. aria_code/utils/vscode_uri.py +37 -0
  463. aria_code/video_analysis.py +147 -0
  464. aria_code/video_editor.py +271 -0
  465. aria_code/workspace/__init__.py +6 -0
  466. aria_code/workspace/files.py +193 -0
  467. aria_code/workspace/verify.py +113 -0
  468. aria_code/workspace_changes.py +199 -0
  469. aria_code/workspace_context.py +233 -0
  470. aria_code/workspace_service.py +196 -0
  471. aria_code/workspace_tools.py +150 -0
  472. aria_code-0.52.0.dist-info/METADATA +237 -0
  473. aria_code-0.52.0.dist-info/RECORD +478 -0
  474. aria_code-0.52.0.dist-info/WHEEL +5 -0
  475. aria_code-0.52.0.dist-info/entry_points.txt +3 -0
  476. aria_code-0.52.0.dist-info/licenses/LICENSE +201 -0
  477. aria_code-0.52.0.dist-info/licenses/NOTICE +49 -0
  478. aria_code-0.52.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1381 @@
1
+ """
2
+ MarketCommandsMixin — Market commands: quote, realty, football, screen, news, screen_cn, limitup, north.
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 ._ui import print_error
11
+ from typing import Optional, Tuple
12
+
13
+
14
+ _FOOTBALL_CONNECTORS = (
15
+ "对阵", "对战", "对决", " vs ", " VS ", "vs", "VS", " v.s. ",
16
+ " versus ", "跟", "和", "与", "对", "pk", "PK",
17
+ )
18
+
19
+ _FOOTBALL_STRONG_INTENT_TERMS = (
20
+ "比分", "比赛预测", "比赛", "对阵", "交手", "胜负", "几比几",
21
+ "进球", "足球", "国家队", "世界杯", "欧洲杯", "欧冠", "英超",
22
+ "西甲", "德甲", "意甲", "法甲", "中超", "美职联",
23
+ "打败", "战胜", "击败", "打平", "晋级", "出线", "夺冠", "踢",
24
+ "score", "match", "football", "soccer", "beat",
25
+ )
26
+
27
+ _FOOTBALL_AMBIGUOUS_INTENT_TERMS = (
28
+ "预测", "谁赢", "谁能赢", "谁会赢", "结果预测",
29
+ "predict", "prediction", "win",
30
+ )
31
+
32
+ _MARKET_CONTEXT_TERMS = (
33
+ "股票", "股价", "成交量", "市值", "行情", "k线", "K线", "图表",
34
+ "技术指标", "均线", "支撑", "阻力", "涨跌", "涨幅", "跌幅",
35
+ "财报", "财务", "估值", "营收", "利润", "持仓", "风险", "基金",
36
+ "ETF", "etf", "期权", "债券", "外汇", "期货", "RSI", "MACD",
37
+ "买入", "卖出", "做多", "做空", "quote", "stock", "share",
38
+ "equity", "volume", "market cap", "earnings", "revenue", "price",
39
+ )
40
+
41
+
42
+ def _rss_items_from_xml(xml_text: str, limit: int = 5) -> list[dict]:
43
+ """Parse simple RSS item fields without external dependencies."""
44
+ import html as _html
45
+ import xml.etree.ElementTree as _ET
46
+
47
+ try:
48
+ root = _ET.fromstring(xml_text)
49
+ except Exception:
50
+ return []
51
+ items: list[dict] = []
52
+ for item in root.findall(".//item"):
53
+ title = item.findtext("title") or ""
54
+ link = item.findtext("link") or ""
55
+ pub_date = item.findtext("pubDate") or item.findtext("published") or ""
56
+ source = item.findtext("source") or ""
57
+ if not title:
58
+ continue
59
+ items.append({
60
+ "title": _html.unescape(title.strip()),
61
+ "url": link.strip(),
62
+ "published_at": pub_date.strip(),
63
+ "source": source.strip() or "RSS",
64
+ })
65
+ if len(items) >= limit:
66
+ break
67
+ return items
68
+
69
+
70
+ def _fetch_public_news_fallback(topic: str, limit: int = 5) -> list[dict]:
71
+ """Fetch public RSS news without API keys.
72
+
73
+ Yahoo Finance works well for tickers; Google News RSS covers private
74
+ companies such as SpaceX. This is a best-effort fallback, not a guaranteed
75
+ research source.
76
+ """
77
+ import re as _re
78
+ import urllib.parse as _parse
79
+ import urllib.request as _request
80
+
81
+ topic = (topic or "market").strip()
82
+ urls: list[str] = []
83
+ if _re.match(r"^[A-Z]{1,6}(?:[.-][A-Z]{1,3})?$", topic):
84
+ urls.append(
85
+ "https://feeds.finance.yahoo.com/rss/2.0/headline?"
86
+ f"s={_parse.quote(topic)}&region=US&lang=en-US"
87
+ )
88
+ query = f"{topic} latest news when:14d"
89
+ urls.append(
90
+ "https://news.google.com/rss/search?"
91
+ f"q={_parse.quote(query)}&hl=en-US&gl=US&ceid=US:en"
92
+ )
93
+ headers = {"User-Agent": "Mozilla/5.0 AriaCode/4.1"}
94
+ for url in urls:
95
+ try:
96
+ req = _request.Request(url, headers=headers)
97
+ with _request.urlopen(req, timeout=8) as resp:
98
+ text = resp.read().decode("utf-8", errors="replace")
99
+ items = _rss_items_from_xml(text, limit=limit)
100
+ if items:
101
+ return items
102
+ except Exception:
103
+ continue
104
+ return []
105
+
106
+
107
+ def _is_known_football_name(name: str) -> bool:
108
+ """Return True only when a fragment resolves to a known football team/country."""
109
+ n = (name or "").strip()
110
+ if not n:
111
+ return False
112
+ try:
113
+ from football_data_client import _CN_TEAM_MAP, _FIFA_RATINGS
114
+ except Exception:
115
+ return False
116
+ if n in _CN_TEAM_MAP:
117
+ return True
118
+ nl = n.lower()
119
+ for cn, en in _CN_TEAM_MAP.items():
120
+ if n == cn or nl == str(en).lower():
121
+ return True
122
+ for en_key, data in _FIFA_RATINGS.items():
123
+ if nl == str(en_key).lower() or n == str(data.get("name", "")):
124
+ return True
125
+ return False
126
+
127
+
128
+ def _is_probable_football_query(text: str, pair: Optional[Tuple[str, str]] = None) -> bool:
129
+ """Guard the NL football route so finance queries do not enter Poisson mode."""
130
+ raw = text or ""
131
+ if not raw.strip() or raw.strip().startswith("/"):
132
+ return False
133
+ if any(term in raw for term in _MARKET_CONTEXT_TERMS):
134
+ return False
135
+ pair = pair or _parse_nl_team_pair(raw)
136
+ if not pair:
137
+ return False
138
+ known_pair = _is_known_football_name(pair[0]) and _is_known_football_name(pair[1])
139
+ if any(term in raw for term in _FOOTBALL_STRONG_INTENT_TERMS):
140
+ return True
141
+ if any(term in raw for term in _FOOTBALL_AMBIGUOUS_INTENT_TERMS):
142
+ return known_pair
143
+ if not any(conn.lower() in raw.lower() for conn in _FOOTBALL_CONNECTORS):
144
+ return False
145
+ return known_pair
146
+
147
+
148
+ def _parse_nl_team_pair(text: str) -> Optional[Tuple[str, str]]:
149
+ """
150
+ Extract (home_cn, away_cn) from a natural-language football query.
151
+
152
+ Handles patterns like:
153
+ "葡萄牙和刚果比赛比分预测"
154
+ "巴西跟阿根廷谁赢"
155
+ "英格兰对阵法国"
156
+ "Germany vs France prediction" ← English also supported
157
+ Returns None if two teams cannot be confidently identified.
158
+ """
159
+ try:
160
+ from football_data_client import _CN_TEAM_MAP, _FIFA_RATINGS
161
+ except Exception:
162
+ return None
163
+
164
+ # Build reverse map: english_lower → cn_name (from _CN_TEAM_MAP values)
165
+ _EN_TO_CN: dict = {}
166
+ for cn, en in _CN_TEAM_MAP.items():
167
+ _EN_TO_CN.setdefault(en.lower(), cn)
168
+ # Also add direct FIFA rating keys → cn name
169
+ for en_key, data in _FIFA_RATINGS.items():
170
+ cn_name = data.get("name", "")
171
+ if cn_name and en_key.lower() not in _EN_TO_CN:
172
+ _EN_TO_CN[en_key.lower()] = cn_name
173
+
174
+ # Ordered connectors — longer ones first to avoid partial matches
175
+ _CONNECTORS = _FOOTBALL_CONNECTORS
176
+ # Words to strip from team-name fragments
177
+ _STRIP_WORDS = (
178
+ "预测", "分析", "比赛", "比分", "胜率", "结果", "谁赢", "谁会赢",
179
+ "今天", "今日", "明天", "的", "了", "吗", "呢",
180
+ "prediction", "match", "game", "preview", "who wins", "predict",
181
+ "football", "soccer", "score",
182
+ )
183
+
184
+ def _clean(s: str) -> str:
185
+ s = s.strip("?!,。、《》()[]【】::'\"-— \t")
186
+ for w in sorted(_STRIP_WORDS, key=len, reverse=True):
187
+ s = s.replace(w, "").strip()
188
+ return s.strip()
189
+
190
+ def _resolve(name: str) -> Optional[str]:
191
+ """Resolve a name (CN or EN) to its canonical Chinese name."""
192
+ name = name.strip()
193
+ if not name:
194
+ return None
195
+ # Direct CN lookup
196
+ if name in _CN_TEAM_MAP:
197
+ return name
198
+ # English → CN
199
+ nl = name.lower()
200
+ if nl in _EN_TO_CN:
201
+ return _EN_TO_CN[nl]
202
+ # Partial English match
203
+ for en_key, cn_n in _EN_TO_CN.items():
204
+ if nl in en_key or en_key in nl:
205
+ return cn_n
206
+ # Partial CN match
207
+ for cn in _CN_TEAM_MAP:
208
+ if name in cn or cn in name:
209
+ return cn
210
+ # Return as-is if it looks like a real name (≥2 chars)
211
+ return name if len(name) >= 2 else None
212
+
213
+ # ── Approach 1: split on connector ───────────────────────────────────────
214
+ for conn in _CONNECTORS:
215
+ if conn.lower() in text.lower():
216
+ idx = text.lower().index(conn.lower())
217
+ left = _resolve(_clean(text[:idx]))
218
+ right = _resolve(_clean(text[idx + len(conn):]))
219
+ if left and right and left != right:
220
+ return left, right
221
+
222
+ # ── Approach 2: scan for all known Chinese team names in text order ──────
223
+ found: list = []
224
+ for cn in _CN_TEAM_MAP:
225
+ if cn in text:
226
+ found.append((text.index(cn), cn))
227
+ # Also scan English names (word-boundary, case-insensitive)
228
+ import re as _re
229
+ tl = text.lower()
230
+ for en_key, cn_n in _EN_TO_CN.items():
231
+ if len(en_key) < 3:
232
+ continue
233
+ m = _re.search(r'\b' + _re.escape(en_key) + r'\b', tl)
234
+ if m:
235
+ found.append((m.start(), cn_n))
236
+ found.sort()
237
+ # Remove duplicates keeping earlier occurrence
238
+ seen_en: set = set()
239
+ unique: list = []
240
+ for pos, cn in found:
241
+ en = _CN_TEAM_MAP.get(cn, cn)
242
+ if en not in seen_en:
243
+ seen_en.add(en)
244
+ unique.append((pos, cn))
245
+ if len(unique) >= 2:
246
+ return unique[0][1], unique[1][1]
247
+
248
+ return None
249
+
250
+
251
+ import logging
252
+ import json
253
+ import asyncio
254
+ import datetime
255
+ import time
256
+ import shlex
257
+ from typing import Dict, Any, Optional
258
+
259
+ def print_quote_result(*args, **kwargs):
260
+ from aria_cli import print_quote_result as fn
261
+ return fn(*args, **kwargs)
262
+ def _football_fixtures(*args, **kwargs):
263
+ from aria_cli import _football_fixtures as fn
264
+ return fn(*args, **kwargs)
265
+ # execute_aria_tool lives in apps/cli/tool_executor.py and needs nothing from
266
+ # aria_cli — the cache it reads is owned there now, so the hop was pointless.
267
+ from aria_code.apps.cli.tool_executor import execute_aria_tool
268
+ def _is_ashare_symbol(*args, **kwargs):
269
+ from aria_code.apps.cli.helpers import _is_ashare_symbol as fn
270
+ return fn(*args, **kwargs)
271
+ def _prompt_str(*args, **kwargs):
272
+ from aria_cli import _prompt_str as fn
273
+ return fn(*args, **kwargs)
274
+ def _render_reits_list(*args, **kwargs):
275
+ from aria_cli import _render_reits_list as fn
276
+ return fn(*args, **kwargs)
277
+ def _render_multi_city(*args, **kwargs):
278
+ from aria_cli import _render_multi_city as fn
279
+ return fn(*args, **kwargs)
280
+ def _football_team(*args, **kwargs):
281
+ from aria_cli import _football_team as fn
282
+ return fn(*args, **kwargs)
283
+ def _get_LOCAL_TOOLS():
284
+ # Owned by apps/cli/tool_registry.py; aria_cli fills it in place.
285
+ from ..tool_registry import LOCAL_TOOLS as val
286
+ return val
287
+ def _render_property_val(*args, **kwargs):
288
+ from aria_cli import _render_property_val as fn
289
+ return fn(*args, **kwargs)
290
+ def _get_mdc(*args, **kwargs):
291
+ from .._optional import get_mdc as fn
292
+ return fn(*args, **kwargs)
293
+ def _load_data_keys(*args, **kwargs):
294
+ from aria_code.apps.cli.provider_keys import load_data_keys as fn
295
+ return fn(*args, **kwargs)
296
+ def _render_asset_score(*args, **kwargs):
297
+ from aria_cli import _render_asset_score as fn
298
+ return fn(*args, **kwargs)
299
+ def _render_rental_yield(*args, **kwargs):
300
+ from aria_cli import _render_rental_yield as fn
301
+ return fn(*args, **kwargs)
302
+ def _football_h2h(*args, **kwargs):
303
+ from aria_cli import _football_h2h as fn
304
+ return fn(*args, **kwargs)
305
+ def _football_standings(*args, **kwargs):
306
+ from aria_cli import _football_standings as fn
307
+ return fn(*args, **kwargs)
308
+ def _render_house_price(*args, **kwargs):
309
+ from aria_cli import _render_house_price as fn
310
+ return fn(*args, **kwargs)
311
+ def format_quote_output(*args, **kwargs):
312
+ from aria_cli import format_quote_output as fn
313
+ return fn(*args, **kwargs)
314
+ def _prompt_float(*args, **kwargs):
315
+ from aria_cli import _prompt_float as fn
316
+ return fn(*args, **kwargs)
317
+ def _ashare_code_to_name(*args, **kwargs):
318
+ from aria_cli import _ashare_code_to_name as fn
319
+ return fn(*args, **kwargs)
320
+ def _get__HAS_MDC():
321
+ from .._optional import HAS_MDC as val
322
+ return val
323
+ def parse_symbols(*args, **kwargs):
324
+ from aria_cli import parse_symbols as fn
325
+ return fn(*args, **kwargs)
326
+
327
+ import json
328
+ import asyncio
329
+ import datetime
330
+ import time
331
+ import shlex
332
+ import sys
333
+ import os
334
+ from typing import Dict, Any, Optional
335
+
336
+
337
+ import json
338
+ import asyncio
339
+ import datetime
340
+ import time
341
+ import shlex
342
+ import sys
343
+ import os
344
+ from typing import Dict, Any, Optional
345
+
346
+ # A real logger. This was a `def logger(*args, **kwargs)` forwarding to
347
+ # aria_cli, so every `logger.debug(...)` raised AttributeError — and all but
348
+ # one of them sit in an except block, where the failure replaced whatever
349
+ # error was being reported.
350
+ logger = logging.getLogger(__name__)
351
+
352
+
353
+ class MarketCommandsMixin:
354
+ """Mixin: Market commands: quote, realty, football, screen, news, screen_cn, limitup, north."""
355
+
356
+ async def cmd_realty(self, args: str):
357
+ """
358
+ /realty market [city1] [city2] — 城市房价指数
359
+ /realty reit [code] — REIT 列表或单只分析
360
+ /realty valuation — 物业估值计算器(交互式)
361
+ /realty rent — 租金收益率计算(交互式)
362
+ /realty compare [cities...] — 多城市对比
363
+ /realty score — 资产区位评分(交互式)
364
+ /realty us — 美国住房数据
365
+ """
366
+ import asyncio as _asyncio
367
+ loop = _asyncio.get_event_loop()
368
+ parts = args.strip().split() if args.strip() else []
369
+ sub = parts[0].lower() if parts else "market"
370
+
371
+ try:
372
+ from realty_data_tools import (
373
+ get_house_price_index, get_re_investment,
374
+ get_reits_list, get_reit_analysis, get_multi_city_comparison,
375
+ calc_rental_yield, property_valuation, asset_location_score,
376
+ get_us_housing_data,
377
+ )
378
+ except ImportError as e:
379
+ if self.context.has_rich:
380
+ self.context.console.print(f"[red]realty_data_tools 未加载: {e}[/red]")
381
+ return
382
+
383
+ if sub == "market":
384
+ city1 = parts[1] if len(parts) > 1 else "北京"
385
+ city2 = parts[2] if len(parts) > 2 else ("上海" if city1 != "上海" else "北京")
386
+ import functools as _functools
387
+ if self.context.has_rich:
388
+ with self.context.console.status(f"[dim]获取 {city1}/{city2} 房价指数...[/dim]", spinner="dots"):
389
+ r = await loop.run_in_executor(
390
+ None, _functools.partial(get_house_price_index, city1, city2)
391
+ )
392
+ else:
393
+ r = get_house_price_index(city1, city2)
394
+ _render_house_price(r)
395
+ # Also show investment data
396
+ if self.context.has_rich:
397
+ with self.context.console.status("[dim]获取房地产投资数据...[/dim]", spinner="dots"):
398
+ ri = await loop.run_in_executor(None, get_re_investment)
399
+ else:
400
+ ri = get_re_investment()
401
+ if ri.get("success") and ri.get("latest"):
402
+ lt = ri["latest"]
403
+ if self.context.has_rich:
404
+ self.context.console.print(f"\n [dim]房地产开发投资[/dim] {lt.get('日期','')} "
405
+ f"最新值 [bold]{lt.get('最新值','')}[/bold] "
406
+ f"涨跌 {lt.get('涨跌幅','')} "
407
+ f"近1年 {lt.get('近1年涨跌幅','')}")
408
+
409
+ elif sub == "reit":
410
+ code = parts[1] if len(parts) > 1 else None
411
+ if code:
412
+ if self.context.has_rich:
413
+ with self.context.console.status(f"[dim]分析 {code} REIT...[/dim]", spinner="dots"):
414
+ r = await loop.run_in_executor(None, get_reit_analysis, code)
415
+ else:
416
+ r = get_reit_analysis(code)
417
+ if r.get("success"):
418
+ if self.context.has_rich:
419
+ self.context.console.print(f"\n [bold cyan]{r.get('code','')}[/bold cyan] "
420
+ f"[dim]{r.get('name','')}[/dim]")
421
+ self.context.console.print(f" 现价 [bold]{r.get('price','')}[/bold] "
422
+ f"涨跌 {r.get('chg_pct','')}%")
423
+ if r.get("return_1y") is not None:
424
+ rc = "green" if r["return_1y"] > 0 else "red"
425
+ self.context.console.print(f" 近1年收益: [{rc}]{r['return_1y']:+.2f}%[/{rc}]")
426
+ if r.get("volatility_annual"):
427
+ self.context.console.print(f" 年化波动率: {r['volatility_annual']:.2f}%")
428
+ else:
429
+ self.context.console.print(f"[red]{r.get('error','分析失败')}[/red]") if self.context.has_rich else None
430
+ else:
431
+ if self.context.has_rich:
432
+ with self.context.console.status("[dim]获取 REIT 列表...[/dim]", spinner="dots"):
433
+ r = await loop.run_in_executor(None, get_reits_list)
434
+ else:
435
+ r = get_reits_list()
436
+ _render_reits_list(r)
437
+
438
+ elif sub == "compare":
439
+ cities = parts[1:] if len(parts) > 1 else None
440
+ if self.context.has_rich:
441
+ with self.context.console.status("[dim]对比多城市房价...[/dim]", spinner="dots"):
442
+ r = await loop.run_in_executor(None, get_multi_city_comparison, cities)
443
+ else:
444
+ r = get_multi_city_comparison(cities)
445
+ _render_multi_city(r)
446
+
447
+ elif sub in ("rent", "rental"):
448
+ if self.context.has_rich:
449
+ self.context.console.print("[bold]💰 租金收益率计算器[/bold] [dim](输入 0 跳过可选项)[/dim]")
450
+ price_wan = _prompt_float("购入价格(万元): ", 200.0)
451
+ monthly_rent = _prompt_float("月租金(元): ", 5000.0)
452
+ annual_costs = _prompt_float("年维护成本(元)[可选]: ", 0.0)
453
+ loan_ratio = _prompt_float("贷款成数 0-1 (如0.7=七成)[可选]: ", 0.0)
454
+ p = {"purchase_price": price_wan, "monthly_rent": monthly_rent,
455
+ "annual_costs": annual_costs, "loan_ratio": loan_ratio}
456
+ r = calc_rental_yield(p)
457
+ _render_rental_yield(r)
458
+
459
+ elif sub in ("valuation", "val"):
460
+ if self.context.has_rich:
461
+ self.context.console.print("[bold]🏢 物业估值计算器[/bold]")
462
+ area = _prompt_float("建筑面积(㎡): ", 100.0)
463
+ monthly_rent = _prompt_float("月租金(元): ", 5000.0)
464
+ tier = _prompt_str("区位层级 (tier1/tier2/tier3): ", "tier2")
465
+ p = {"area_sqm": area, "monthly_rent": monthly_rent, "location_tier": tier}
466
+ r = property_valuation(p)
467
+ _render_property_val(r)
468
+
469
+ elif sub == "score":
470
+ if self.context.has_rich:
471
+ self.context.console.print("[bold]📍 资产区位评分[/bold]")
472
+ city = _prompt_str("城市: ", "上海")
473
+ area = _prompt_float("建筑面积(㎡): ", 100.0)
474
+ floor_n = int(_prompt_float("楼层: ", 1.0))
475
+ traffic = _prompt_str("客流量 (high/medium/low): ", "medium")
476
+ fire_ok = _prompt_str("允许明火? (y/n): ", "n").lower() in ("y","yes","是")
477
+ reno_ok = _prompt_str("允许改造? (y/n): ", "y").lower() in ("y","yes","是")
478
+ p = {"city": city, "area_sqm": area, "floor": floor_n,
479
+ "foot_traffic": traffic, "open_fire_allowed": fire_ok,
480
+ "renovation_allowed": reno_ok}
481
+ r = asset_location_score(p)
482
+ _render_asset_score(r)
483
+
484
+ elif sub == "us":
485
+ if self.context.has_rich:
486
+ with self.context.console.status("[dim]获取美国住房数据...[/dim]", spinner="dots"):
487
+ r = await loop.run_in_executor(None, get_us_housing_data)
488
+ else:
489
+ r = get_us_housing_data()
490
+ if not r.get("success"):
491
+ if self.context.has_rich: self.context.console.print(f"[red]{r.get('error')}[/red]")
492
+ return
493
+ if self.context.has_rich:
494
+ from rich.table import Table as _T
495
+ from rich import box as _box
496
+ tb = _T(title="[bold]🏠 美国住房市场数据[/bold]", box=_box.ROUNDED)
497
+ tb.add_column("指标", style="dim"); tb.add_column("最新值"); tb.add_column("日期", style="dim")
498
+ for key, val in r.get("data", {}).items():
499
+ lt = val.get("latest", {})
500
+ v = lt.get("value")
501
+ tb.add_row(val.get("label", key), str(v) if v else "—", str(lt.get("date",""))[:7])
502
+ self.context.console.print(tb)
503
+ for line in r.get("assessment", []):
504
+ self.context.console.print(f" [dim]▸ {line}[/dim]")
505
+
506
+ else:
507
+ if self.context.has_rich:
508
+ self.context.console.print("[dim]用法: /realty [market|reit|compare|rent|valuation|score|us][/dim]")
509
+ self.context.console.print("[dim]示例: /realty market 北京 上海[/dim]")
510
+ self.context.console.print("[dim] /realty reit 508603[/dim]")
511
+ self.context.console.print("[dim] /realty rent (交互式租金计算)[/dim]")
512
+ self.context.console.print("[dim] /realty compare 北京 上海 成都 杭州[/dim]")
513
+
514
+ async def cmd_football(self, args: str):
515
+ """
516
+ 足球赛事分析和预测
517
+
518
+ 子命令:
519
+ /football standings <联赛> 联赛积分榜
520
+ /football fixtures <联赛> [days] 近期赛程(默认7天)
521
+ /football predict <主队> vs <客队> [联赛] 比赛预测
522
+ /football team <球队名> [联赛] 球队近期状态
523
+ /football h2h <队1> vs <队2> [联赛] 历史交锋
524
+
525
+ 联赛代码: pl/epl/英超 bl/德甲 ll/西甲 sa/意甲 fl1/法甲 cl/欧冠
526
+ 示例:
527
+ /football standings pl
528
+ /football predict Arsenal vs Chelsea pl
529
+ /football team Manchester City pl
530
+ /football fixtures cl 14
531
+ """
532
+ from rich.table import Table
533
+ from rich import box as rich_box
534
+ from rich.panel import Panel
535
+
536
+ parts = args.strip().split()
537
+ if not parts:
538
+ self.context.console.print(Panel(
539
+ "[bold]足球分析命令[/bold]\n\n"
540
+ " [cyan]/football standings pl[/cyan] 英超积分榜\n"
541
+ " [cyan]/football fixtures cl 14[/cyan] 欧冠未来14天赛程\n"
542
+ " [cyan]/football predict Arsenal vs Chelsea[/cyan] 预测比赛结果\n"
543
+ " [cyan]/football team Bayern Munich bl[/cyan] 球队近期状态\n"
544
+ " [cyan]/football h2h Barcelona vs Real Madrid[/cyan] 历史交锋\n\n"
545
+ "[dim]联赛: pl/英超 bl/德甲 ll/西甲 sa/意甲 fl1/法甲 cl/欧冠[/dim]\n"
546
+ "[dim]需要设置 FOOTBALL_DATA_API_KEY(football-data.org 免费注册)[/dim]",
547
+ title="[bold]⚽ Football Analyst[/bold]",
548
+ border_style="green",
549
+ ))
550
+ return
551
+
552
+ sub = parts[0].lower()
553
+
554
+ # ── standings ──────────────────────────────────────────────────────────
555
+ if sub == "standings":
556
+ league = parts[1] if len(parts) > 1 else "pl"
557
+ await self._run_in_executor(_football_standings, league)
558
+
559
+ # ── fixtures ──────────────────────────────────────────────────────────
560
+ elif sub in ("fixtures", "schedule", "赛程"):
561
+ league = parts[1] if len(parts) > 1 else "pl"
562
+ days = int(parts[2]) if len(parts) > 2 and parts[2].isdigit() else 7
563
+ await self._run_in_executor(_football_fixtures, league, days)
564
+
565
+ # ── predict ───────────────────────────────────────────────────────────
566
+ elif sub in ("predict", "预测", "prediction"):
567
+ raw = " ".join(parts[1:])
568
+ if " vs " in raw.lower():
569
+ idx = raw.lower().index(" vs ")
570
+ home = raw[:idx].strip()
571
+ rest = raw[idx + 4:].strip()
572
+ away_parts = rest.split()
573
+ # last token might be league code (including wc/世界杯)
574
+ from football_data_client import LEAGUE_IDS, TOURNAMENT_CODES
575
+ _all_codes = {**LEAGUE_IDS, **{k: v for k, v in TOURNAMENT_CODES.items()}}
576
+ if away_parts and away_parts[-1].lower().replace(" ", "") in _all_codes:
577
+ league = away_parts[-1]
578
+ away = " ".join(away_parts[:-1])
579
+ elif away_parts and away_parts[-1].lower() in ("wc", "worldcup", "世界杯", "ca", "ec"):
580
+ league = away_parts[-1]
581
+ away = " ".join(away_parts[:-1])
582
+ else:
583
+ league = "pl"
584
+ away = rest
585
+ await self._football_predict(home, away, league)
586
+ else:
587
+ self.context.console.print("[red]用法: /football predict <主队> vs <客队> [联赛/wc][/red]")
588
+
589
+ # ── team ──────────────────────────────────────────────────────────────
590
+ elif sub in ("team", "球队"):
591
+ rest = " ".join(parts[1:])
592
+ from football_data_client import LEAGUE_IDS
593
+ tokens = rest.split()
594
+ if tokens and tokens[-1].lower() in LEAGUE_IDS:
595
+ league = tokens[-1]
596
+ team = " ".join(tokens[:-1])
597
+ else:
598
+ league = "pl"
599
+ team = rest
600
+ await self._run_in_executor(_football_team, team, league)
601
+
602
+ # ── h2h ───────────────────────────────────────────────────────────────
603
+ elif sub in ("h2h", "历史", "对决"):
604
+ raw = " ".join(parts[1:])
605
+ if " vs " in raw.lower():
606
+ idx = raw.lower().index(" vs ")
607
+ t1 = raw[:idx].strip()
608
+ rest = raw[idx + 4:].strip()
609
+ from football_data_client import LEAGUE_IDS
610
+ tokens = rest.split()
611
+ if tokens and tokens[-1].lower() in LEAGUE_IDS:
612
+ league = tokens[-1]
613
+ t2 = " ".join(tokens[:-1])
614
+ else:
615
+ league = "pl"
616
+ t2 = rest
617
+ await self._run_in_executor(_football_h2h, t1, t2, league)
618
+ else:
619
+ self.context.console.print("[red]用法: /football h2h <队1> vs <队2> [联赛][/red]")
620
+
621
+ else:
622
+ # NL intent: /football 预测加拿大跟波黑... or /football 分析...
623
+ full_args = args.strip()
624
+ _has_cn = any('一' <= c <= '鿿' for c in full_args)
625
+ _has_kw = any(k in full_args.lower() for k in (
626
+ "predict", "preview", "analyze", "analysis", "who wins",
627
+ "预测", "分析", "谁赢", "比分", "胜率", "谁先", "开球",
628
+ ))
629
+ if _has_cn or _has_kw:
630
+ # ── Step 1: Parse two team names from NL text ─────────────────
631
+ _nl_pair = _parse_nl_team_pair(full_args)
632
+ if _nl_pair:
633
+ _h_cn, _a_cn = _nl_pair
634
+ # Determine league: national teams → wc, club → pl default
635
+ try:
636
+ from football_data_client import _CN_TEAM_MAP, _find_fifa_rating
637
+ _h_en = _CN_TEAM_MAP.get(_h_cn, _h_cn)
638
+ _a_en = _CN_TEAM_MAP.get(_a_cn, _a_cn)
639
+ _is_nat = bool(_find_fifa_rating(_h_en) or _find_fifa_rating(_a_en))
640
+ except Exception:
641
+ _is_nat = True
642
+ _nl_league = "wc" if _is_nat else "pl"
643
+ await self._football_predict(_h_cn, _a_cn, _nl_league)
644
+ return
645
+
646
+ # ── Step 2: Fall back to get_sports_context_for_query ─────────
647
+ try:
648
+ from football_data_client import get_sports_context_for_query
649
+ _sports_ctx = get_sports_context_for_query(full_args)
650
+ except Exception:
651
+ _sports_ctx = ""
652
+ if _sports_ctx:
653
+ _has_quant = "量化预测" in _sports_ctx
654
+ _title = "⚽ 赛事预测" if _has_quant else "⚽ 赛事数据"
655
+ self.context.console.print(Panel(
656
+ _sports_ctx,
657
+ title=f"[bold]{_title}[/bold]",
658
+ border_style="cyan" if _has_quant else "blue",
659
+ ))
660
+ else:
661
+ self.context.console.print(
662
+ "[yellow]⚽ 未能解析队名。[/yellow]\n"
663
+ "支持格式:\n"
664
+ " [cyan]/football predict 葡萄牙 vs 刚果 wc[/cyan]\n"
665
+ " [cyan]/football 葡萄牙和刚果比赛[/cyan] (自动识别)"
666
+ )
667
+ else:
668
+ self.context.console.print(f"[red]未知子命令: {sub}[/red] 使用 /football 查看帮助")
669
+
670
+ async def _football_predict(self, home: str, away: str, league: str):
671
+ """Run football match prediction with LLM analysis."""
672
+ from rich.panel import Panel
673
+ from rich.table import Table
674
+ from rich import box as rich_box
675
+ import types
676
+
677
+ self.context.console.print(f"[#57606a]⚽ 分析 {home} vs {away} ({league.upper()})…[/#57606a]")
678
+
679
+ # WC / national team prediction path
680
+ _wc_leagues = {"wc", "worldcup", "世界杯", "world_cup", "ca", "ec", "afc"}
681
+ _is_wc = league.lower().replace(" ", "") in _wc_leagues
682
+
683
+ if _is_wc:
684
+ try:
685
+ from football_data_client import (
686
+ predict_wc_match,
687
+ _find_fifa_rating,
688
+ team_display_name,
689
+ football_prediction_quality,
690
+ football_quality_missing_labels,
691
+ )
692
+ raw = predict_wc_match(home, away, neutral_venue=True)
693
+ _h_cn = team_display_name(raw.get("home_name_cn", home), "zh")
694
+ _a_cn = team_display_name(raw.get("away_name_cn", away), "zh")
695
+ # Build strength facts for display
696
+ _h_rank = raw.get("home_ranking", "?")
697
+ _a_rank = raw.get("away_ranking", "?")
698
+ _h_elo = raw.get("home_elo")
699
+ _a_elo = raw.get("away_elo")
700
+ _h_atk = raw.get("home_attack")
701
+ _a_atk = raw.get("away_attack")
702
+ _h_def = raw.get("home_defense")
703
+ _a_def = raw.get("away_defense")
704
+ _h_form = raw.get("home_form", "")
705
+ _a_form = raw.get("away_form", "")
706
+ _cal = raw.get("calibrated_matches", 0)
707
+
708
+ def _rank_label(value):
709
+ return f"#{value}" if value not in (None, "", "?") else "排名缺失"
710
+
711
+ _strength_facts = [
712
+ f"FIFA排名: {_h_cn} {_rank_label(_h_rank)} · {_a_cn} {_rank_label(_a_rank)}",
713
+ ]
714
+ if _h_elo and _a_elo:
715
+ _strength_facts.append(f"Elo评分: {_h_cn} {_h_elo:.0f} · {_a_cn} {_a_elo:.0f}")
716
+ if _h_atk is not None:
717
+ _atk_h = f"{_h_atk:.2f}" if isinstance(_h_atk, float) else str(_h_atk)
718
+ _atk_a = f"{_a_atk:.2f}" if isinstance(_a_atk, float) else str(_a_atk)
719
+ _def_h = f"{_h_def:.2f}" if isinstance(_h_def, float) else str(_h_def)
720
+ _def_a = f"{_a_def:.2f}" if isinstance(_a_def, float) else str(_a_def)
721
+ _strength_facts.append(f"进攻强度: {_h_cn} {_atk_h} · {_a_cn} {_atk_a}")
722
+ _strength_facts.append(f"防守强度: {_h_cn} {_def_h} · {_a_cn} {_def_a}")
723
+ _strength_facts.append(
724
+ f"数据基础: {_cal} 场已完赛 WC 数据校准" if _cal > 0
725
+ else "数据基础: FIFA排名 + Poisson引擎估算"
726
+ )
727
+ _quality = raw.get("data_quality") or football_prediction_quality(raw)
728
+ if _quality.get("missing"):
729
+ _missing_labels = football_quality_missing_labels(_quality["missing"], "zh")
730
+ _strength_facts.append(
731
+ f"数据质量: {_quality.get('status', 'estimated')} · 缺失/估算 {', '.join(_missing_labels)}"
732
+ )
733
+
734
+ pred = types.SimpleNamespace(
735
+ home_win = raw["home_win"],
736
+ draw = raw["draw"],
737
+ away_win = raw["away_win"],
738
+ btts = raw["btts"],
739
+ lambda_home = raw["lambda_home"],
740
+ lambda_away = raw["lambda_away"],
741
+ most_likely = raw["top_scorelines"][0]["score"] if raw["top_scorelines"] else "1-0",
742
+ top_scores = [{"score": s["score"], "prob": s["prob"]} for s in raw["top_scorelines"]],
743
+ implied_odds = raw["implied_odds"],
744
+ key_factors = _strength_facts,
745
+ home_form = _h_form,
746
+ away_form = _a_form,
747
+ home_elo = _h_elo,
748
+ away_elo = _a_elo,
749
+ data_quality = _quality,
750
+ analysis = "",
751
+ verdict = (
752
+ f"[green]预测: {_h_cn} 获胜 ({raw['home_win']:.0%})[/green]" if raw["home_win"] > raw["away_win"] + 0.05
753
+ else f"[green]预测: {_a_cn} 获胜 ({raw['away_win']:.0%})[/green]" if raw["away_win"] > raw["home_win"] + 0.05
754
+ else f"[yellow]预测: 双方势均力敌,平局概率 {raw['draw']:.0%}[/yellow]"
755
+ ),
756
+ ht_lambda_home = raw.get("ht_lambda_home", 0),
757
+ ht_lambda_away = raw.get("ht_lambda_away", 0),
758
+ st_lambda_home = raw.get("st_lambda_home", 0),
759
+ st_lambda_away = raw.get("st_lambda_away", 0),
760
+ ht_home_win = raw.get("ht_home_win", 0),
761
+ ht_draw = raw.get("ht_draw", 0),
762
+ ht_away_win = raw.get("ht_away_win", 0),
763
+ ht_top_scorelines = raw.get("ht_top_scorelines", []),
764
+ home_name_cn = _h_cn,
765
+ away_name_cn = _a_cn,
766
+ )
767
+ except Exception as exc:
768
+ self.context.console.print(f"[red]WC 预测失败: {exc}[/red]")
769
+ return
770
+ else:
771
+ try:
772
+ from agents.sports.football_agent import FootballAgent
773
+
774
+ agent = FootballAgent(llm_call=None)
775
+
776
+ import asyncio
777
+ pred = await agent.predict(home, away, league, with_llm=False)
778
+
779
+ # Try LLM enhancement
780
+ if hasattr(self, 'terminal') and self.terminal:
781
+ try:
782
+ _score_candidates = "、".join(
783
+ f"{s['score']}({s['prob']}%)"
784
+ for s in getattr(pred, "top_scores", [])[:5]
785
+ )
786
+ llm_prompt = (
787
+ f"你是专业足球分析师。简洁分析这场比赛(中文,不超过150字):\n"
788
+ f"{home} vs {away}\n"
789
+ f"主队胜: {pred.home_win:.0%} 平: {pred.draw:.0%} 客队胜: {pred.away_win:.0%}\n"
790
+ f"预期进球: {pred.lambda_home:.1f} - {pred.lambda_away:.1f}\n"
791
+ f"最可能比分: {pred.most_likely}\n"
792
+ f"候选比分(按概率降序): {_score_candidates}\n"
793
+ f"关键因素: {'; '.join(pred.key_factors)}\n"
794
+ "规则: 只引用上方概率和候选比分,不要编造射正率、历史交锋、近5场客场等具体数据。"
795
+ )
796
+ analysis_text = await asyncio.wait_for(
797
+ self.terminal._query_llm_async(llm_prompt),
798
+ timeout=30
799
+ )
800
+ if analysis_text:
801
+ pred.analysis = analysis_text
802
+ except Exception:
803
+ pass
804
+
805
+ except Exception as exc:
806
+ self.context.console.print(f"[red]预测失败: {exc}[/red]")
807
+ return
808
+
809
+ # ── 确定性分析文字:基于 Poisson 数字生成,不调用 LLM ────────────────
810
+ # 避免 gpt-oss:120b-cloud 忽略 enable_tools=False 并乱用工具
811
+ if not getattr(pred, "analysis", ""):
812
+ _h_n = getattr(pred, "home_name_cn", home)
813
+ _a_n = getattr(pred, "away_name_cn", away)
814
+ _hw = pred.home_win
815
+ _dw = pred.draw
816
+ _aw = pred.away_win
817
+ _lh = pred.lambda_home
818
+ _la = pred.lambda_away
819
+ _ml = pred.most_likely
820
+ # Determine favorite
821
+ if _hw > _aw + 0.08:
822
+ _tend = f"{_h_n} 胜算更大({_hw:.0%}),为本场热门"
823
+ elif _aw > _hw + 0.08:
824
+ _tend = f"{_a_n} 胜算更大({_aw:.0%}),为本场热门"
825
+ else:
826
+ _tend = f"双方势均力敌,{_h_n} 胜/平/负概率分别为 {_hw:.0%}/{_dw:.0%}/{_aw:.0%}"
827
+ # Expected goals narrative
828
+ _total = _lh + _la
829
+ if _total < 2.0:
830
+ _goal_desc = "预计进球偏少,防守型比赛"
831
+ elif _total < 3.0:
832
+ _goal_desc = "进球适中,攻防均衡"
833
+ else:
834
+ _goal_desc = "进球较多,进攻型对决"
835
+ pred.analysis = (
836
+ f"{_tend}。"
837
+ f"Poisson 模型预期进球 {_lh:.1f}–{_la:.1f}(共 {_total:.1f}),"
838
+ f"{_goal_desc},最可能比分为 {_ml}。"
839
+ )
840
+
841
+ # ── display ──────────────────────────────────────────────────────────
842
+ from rich.columns import Columns
843
+ from rich.text import Text
844
+ try:
845
+ from football_data_client import team_display_name as _football_display_name
846
+ except Exception:
847
+ def _football_display_name(value, locale="zh"):
848
+ return str(value or "-")
849
+
850
+ # Probability bars
851
+ _home_display = _football_display_name(getattr(pred, "home_name_cn", home), "zh")
852
+ _away_display = _football_display_name(getattr(pred, "away_name_cn", away), "zh")
853
+
854
+ def pct_bar(val: float, width: int = 12) -> str:
855
+ filled = int(val * width)
856
+ return "█" * filled + "░" * (width - filled)
857
+
858
+ hw_color = "green" if pred.home_win > pred.away_win else "dim"
859
+ aw_color = "green" if pred.away_win > pred.home_win else "dim"
860
+ dw_color = "yellow" if pred.draw > 0.28 else "dim"
861
+
862
+ prob_table = Table(box=rich_box.SIMPLE, show_header=False, padding=(0, 1))
863
+ prob_table.add_column("", style="bold", width=16)
864
+ prob_table.add_column("", width=14)
865
+ prob_table.add_column("", width=6)
866
+ prob_table.add_column("", width=8)
867
+
868
+ prob_table.add_row(
869
+ f"[{hw_color}]{_home_display}[/{hw_color}]",
870
+ f"[{hw_color}]{pct_bar(pred.home_win)}[/{hw_color}]",
871
+ f"[{hw_color}]{pred.home_win:.0%}[/{hw_color}]",
872
+ f"[dim]赔率 {pred.implied_odds['home']}[/dim]",
873
+ )
874
+ prob_table.add_row(
875
+ f"[{dw_color}]平局[/{dw_color}]",
876
+ f"[{dw_color}]{pct_bar(pred.draw)}[/{dw_color}]",
877
+ f"[{dw_color}]{pred.draw:.0%}[/{dw_color}]",
878
+ f"[dim]赔率 {pred.implied_odds['draw']}[/dim]",
879
+ )
880
+ prob_table.add_row(
881
+ f"[{aw_color}]{_away_display}[/{aw_color}]",
882
+ f"[{aw_color}]{pct_bar(pred.away_win)}[/{aw_color}]",
883
+ f"[{aw_color}]{pred.away_win:.0%}[/{aw_color}]",
884
+ f"[dim]赔率 {pred.implied_odds['away']}[/dim]",
885
+ )
886
+
887
+ title = f"⚽ {_home_display} vs {_away_display} [{league.upper()}]"
888
+ self.context.console.print(Panel(prob_table, title=f"[bold #3fb950]{title}[/bold #3fb950]", border_style="#3fb950"))
889
+
890
+ self.context.console.print(f" [#57606a]预期进球: {_home_display} {pred.lambda_home:.2f} / {_away_display} {pred.lambda_away:.2f}"
891
+ f" │ 双方均进球: {pred.btts:.0%}[/#57606a]")
892
+
893
+ # Top scorelines — show up to 8, colour-coded by outcome
894
+ _h_name = _home_display
895
+ _a_name = _away_display
896
+ if getattr(pred, "top_scores", None):
897
+ score_table = Table(box=rich_box.SIMPLE, show_header=True, padding=(0, 2))
898
+ score_table.add_column("可能比分", style="bold", width=12, no_wrap=True)
899
+ score_table.add_column("概率", justify="right", width=8, no_wrap=True)
900
+ score_table.add_column("结果", width=16, no_wrap=True)
901
+ for s in pred.top_scores[:8]:
902
+ _sc = s["score"]
903
+ _pr = s["prob"]
904
+ try:
905
+ _hg, _ag = (_sc.split("-") + ["0"])[:2]
906
+ _hg, _ag = int(_hg.strip()), int(_ag.strip())
907
+ except Exception:
908
+ _hg, _ag = 0, 0
909
+ if _hg > _ag:
910
+ _label = f"[#3fb950]{_h_name}胜[/#3fb950]"
911
+ _sc_fmt = f"[green]{_sc}[/green]"
912
+ elif _ag > _hg:
913
+ _label = f"[#f85149]{_a_name}胜[/#f85149]"
914
+ _sc_fmt = f"[red]{_sc}[/red]"
915
+ else:
916
+ _label = "[yellow]平局[/yellow]"
917
+ _sc_fmt = f"[yellow]{_sc}[/yellow]"
918
+ score_table.add_row(_sc_fmt, f"{_pr}%", _label)
919
+ self.context.console.print(score_table)
920
+
921
+ # Half-time / second-half breakdown
922
+ if getattr(pred, "ht_lambda_home", 0) > 0:
923
+ _h_lbl = getattr(pred, "home_name_cn", home)
924
+ _h_lbl = _football_display_name(_h_lbl, "zh")
925
+ _a_lbl = _football_display_name(getattr(pred, "away_name_cn", away), "zh")
926
+ _ht_best = pred.ht_top_scorelines[0]["score"] if pred.ht_top_scorelines else "0-0"
927
+ _ht_best_p = pred.ht_top_scorelines[0]["prob"] if pred.ht_top_scorelines else 0
928
+ _st_best_lh = getattr(pred, "st_lambda_home", 0)
929
+ _st_best_la = getattr(pred, "st_lambda_away", 0)
930
+ self.context.console.print()
931
+ self.context.console.print(
932
+ f" [bold]上半场[/bold] 预期进球 {_h_lbl} [cyan]{pred.ht_lambda_home:.2f}[/cyan] / "
933
+ f"{_a_lbl} [cyan]{pred.ht_lambda_away:.2f}[/cyan]"
934
+ f" │ 最可能: [bold]{_ht_best}[/bold] ({_ht_best_p}%)"
935
+ )
936
+ self.context.console.print(
937
+ f" [#57606a]上半场胜/平/负: {pred.ht_home_win:.0%} / {pred.ht_draw:.0%} / {pred.ht_away_win:.0%}[/#57606a]"
938
+ )
939
+ _ht_scores_str = " ".join(
940
+ f"[cyan]{s['score']}[/cyan] {s['prob']}%" for s in pred.ht_top_scorelines[:4]
941
+ )
942
+ if _ht_scores_str:
943
+ self.context.console.print(f" [#57606a]比分分布: {_ht_scores_str}[/#57606a]")
944
+ self.context.console.print(
945
+ f" [bold]下半场[/bold] 预期进球 {_h_lbl} [green]{_st_best_lh:.2f}[/green] / "
946
+ f"{_a_lbl} [green]{_st_best_la:.2f}[/green]"
947
+ f" [#57606a](全场 − 上半场)[/#57606a]"
948
+ )
949
+ self.context.console.print()
950
+
951
+ # ── 实力对比 & 近期表现 ───────────────────────────────────────────────
952
+ _h_name_d = _home_display
953
+ _a_name_d = _away_display
954
+ _hform = getattr(pred, "home_form", "")
955
+ _aform = getattr(pred, "away_form", "")
956
+
957
+ if pred.key_factors:
958
+ self.context.console.print("\n [bold]实力对比[/bold]")
959
+ for f_ in pred.key_factors:
960
+ self.context.console.print(f" [#57606a] • {f_}[/#57606a]")
961
+
962
+ # Form strings (W/D/L) from live API — only show when available
963
+ if _hform and _hform not in ("?????", ""):
964
+ def _form_colored(s: str) -> str:
965
+ out = []
966
+ for c in s.upper():
967
+ if c == "W": out.append("[green]W[/green]")
968
+ elif c == "D": out.append("[yellow]D[/yellow]")
969
+ elif c == "L": out.append("[red]L[/red]")
970
+ else: out.append(c)
971
+ return "".join(out)
972
+ self.context.console.print("\n [bold]近期表现[/bold]")
973
+ self.context.console.print(f" {_h_name_d} {_form_colored(_hform)}")
974
+ if _aform and _aform not in ("?????", ""):
975
+ self.context.console.print(f" {_a_name_d} {_form_colored(_aform)}")
976
+
977
+ if pred.analysis:
978
+ self.context.console.print(Panel(
979
+ pred.analysis,
980
+ title="[bold]量化分析[/bold]",
981
+ border_style="#8c959f",
982
+ padding=(0, 2),
983
+ ))
984
+
985
+ self.context.console.print(f"\n [bold green]{pred.verdict}[/bold green]")
986
+ self.context.console.print(" [#6e7781]提示:准确比分概率通常较分散,请按候选区间参考,不构成投注建议。[/#6e7781]\n")
987
+
988
+ async def cmd_screen(self, args: str):
989
+ """股票筛选: CN → screen_ashare; US → yfinance 大盘成分筛选."""
990
+ criteria = args.strip() or ""
991
+ low = criteria.lower()
992
+
993
+ # CN market detection
994
+ _cn_kw = ("a股", "沪深", "创业板", "科创板", "港股", "cn", "ashare", "沪市", "深市")
995
+ _is_cn = any(k in low for k in _cn_kw) or any(c.isdigit() for c in criteria[:6])
996
+
997
+ if _is_cn:
998
+ params: Dict[str, Any] = {}
999
+ for tok in args.split():
1000
+ if "=" in tok:
1001
+ k, v = tok.split("=", 1)
1002
+ params[k.strip()] = v.strip()
1003
+ if "screen_ashare" in _get_LOCAL_TOOLS():
1004
+ await self._run_local_tool("screen_ashare", params, "A股选股筛选")
1005
+ else:
1006
+ await self.terminal.send_message(f"帮我筛选A股股票,条件:{criteria or '市值>50亿,非ST,流动性好'}")
1007
+ return
1008
+
1009
+ # US / global: yfinance-based screening on a reference pool
1010
+ import asyncio as _asyncio
1011
+ _loop = _asyncio.get_event_loop()
1012
+
1013
+ _US_POOL = [
1014
+ "AAPL","MSFT","NVDA","GOOGL","AMZN","META","TSLA","BRK-B","JPM","V",
1015
+ "UNH","XOM","JNJ","WMT","MA","PG","LLY","HD","CVX","MRK",
1016
+ "ABBV","PEP","KO","AVGO","COST","BAC","TMO","MCD","ACN","ADBE",
1017
+ "CRM","NFLX","AMD","TXN","QCOM","INTC","CSCO","WFC","PM","VZ",
1018
+ "RTX","HON","AMGN","LIN","DHR","UNP","CAT","SBUX","GS","BA",
1019
+ ]
1020
+
1021
+ # Map common text criteria to filter presets
1022
+ _growth_kw = ("growth", "成长", "高增速", "tech", "科技", "ai", "人工智能")
1023
+ _value_kw = ("value", "价值", "低估", "dividend", "分红")
1024
+ _momentum_kw= ("momentum", "动量", "趋势", "breakout", "突破")
1025
+ _is_growth = any(k in low for k in _growth_kw)
1026
+ _is_value = any(k in low for k in _value_kw)
1027
+ _is_momentum = any(k in low for k in _momentum_kw)
1028
+
1029
+ def _fetch_pool():
1030
+ try:
1031
+ import yfinance as _yf
1032
+ tickers = _yf.Tickers(" ".join(_US_POOL))
1033
+ rows = []
1034
+ for sym in _US_POOL:
1035
+ try:
1036
+ info = tickers.tickers[sym].fast_info
1037
+ price = getattr(info, "last_price", None) or 0
1038
+ mktcap = getattr(info, "market_cap", None) or 0
1039
+ pe = getattr(info, "pe_ratio", None)
1040
+ yr_return = getattr(info, "year_change", None)
1041
+ rows.append({
1042
+ "symbol": sym, "price": price,
1043
+ "mktcap": mktcap, "pe": pe,
1044
+ "yr_return": yr_return,
1045
+ })
1046
+ except Exception:
1047
+ pass
1048
+ return rows
1049
+ except Exception as _e:
1050
+ logger.debug("screen US fetch error: %s", _e)
1051
+ return []
1052
+
1053
+ if self.context.has_rich:
1054
+ _status_msg = f"[dim]筛选 {len(_US_POOL)} 只美股 ({criteria or 'top market cap'})…[/dim]"
1055
+ with self.context.console.status(_status_msg, spinner="dots"):
1056
+ rows = await _loop.run_in_executor(None, _fetch_pool)
1057
+ else:
1058
+ print(" 筛选美股中…")
1059
+ rows = await _loop.run_in_executor(None, _fetch_pool)
1060
+
1061
+ if not rows:
1062
+ await self.terminal.send_message(
1063
+ f"Screen US stocks matching: {criteria or 'large-cap'}. "
1064
+ "Show top 10 with price, P/E, market cap, 1-year return."
1065
+ )
1066
+ return
1067
+
1068
+ # Apply simple filters
1069
+ if _is_growth:
1070
+ rows = [r for r in rows if (r.get("yr_return") or 0) > 0.15]
1071
+ elif _is_value:
1072
+ rows = [r for r in rows if r.get("pe") and 5 < r["pe"] < 20]
1073
+ elif _is_momentum:
1074
+ rows = sorted(rows, key=lambda r: r.get("yr_return") or 0, reverse=True)
1075
+ else:
1076
+ rows = sorted(rows, key=lambda r: r.get("mktcap") or 0, reverse=True)
1077
+
1078
+ rows = rows[:15]
1079
+
1080
+ if not rows:
1081
+ msg = f"[yellow]当前条件 '{criteria}' 无匹配标的(池: {len(_US_POOL)} 只)[/yellow]"
1082
+ self.context.console.print(msg) if self.context.has_rich else print(msg.replace("[yellow]","").replace("[/yellow]",""))
1083
+ return
1084
+
1085
+ if self.context.has_rich:
1086
+ from rich.table import Table as _Tbl
1087
+ t = _Tbl(title=f"美股筛选 {criteria or 'large-cap'} 共 {len(rows)} 只",
1088
+ show_header=True, box=None, padding=(0, 1))
1089
+ t.add_column("代码", style="bold", width=8)
1090
+ t.add_column("价格", justify="right")
1091
+ t.add_column("市值(B$)", justify="right", style="dim")
1092
+ t.add_column("PE", justify="right", style="dim")
1093
+ t.add_column("年涨跌%", justify="right")
1094
+ for r in rows:
1095
+ yr = r.get("yr_return")
1096
+ yr_s = f"{yr*100:+.1f}%" if yr is not None else "—"
1097
+ yr_color = "green" if (yr or 0) >= 0 else "red"
1098
+ pe_s = f"{r['pe']:.1f}" if r.get("pe") and r["pe"] == r["pe"] else "—"
1099
+ mc_s = f"{r['mktcap']/1e9:.0f}" if (r.get("mktcap") or 0) > 0 else "—"
1100
+ t.add_row(
1101
+ r["symbol"],
1102
+ f"{r['price']:.2f}" if r.get("price") else "—",
1103
+ mc_s, pe_s,
1104
+ f"[{yr_color}]{yr_s}[/{yr_color}]",
1105
+ )
1106
+ self.context.console.print(t)
1107
+ self.context.console.print(f" [dim]来源: yfinance · 池: {len(_US_POOL)} 只大市值美股[/dim]")
1108
+ else:
1109
+ print(f" 美股筛选 {criteria}")
1110
+ for r in rows:
1111
+ yr = r.get("yr_return")
1112
+ yr_s = f"{yr*100:+.1f}%" if yr is not None else "—"
1113
+ print(f" {r['symbol']:<8} ${r.get('price',0):.2f} {yr_s}")
1114
+
1115
+ async def cmd_news(self, args: str):
1116
+ """Fetch latest financial news for a topic or symbol.
1117
+
1118
+ Usage: /news [topic|symbol] [--limit N]
1119
+ Examples:
1120
+ /news AAPL
1121
+ /news earnings --limit 10
1122
+ /news crypto --limit 3
1123
+ """
1124
+ parts = args.split()
1125
+ limit = 5
1126
+ topic_parts = []
1127
+ i = 0
1128
+ while i < len(parts):
1129
+ if parts[i] == "--limit" and i + 1 < len(parts):
1130
+ try:
1131
+ limit = max(1, min(20, int(parts[i + 1])))
1132
+ i += 2
1133
+ continue
1134
+ except ValueError:
1135
+ pass
1136
+ topic_parts.append(parts[i])
1137
+ i += 1
1138
+ topic = " ".join(topic_parts) or "market"
1139
+
1140
+ self.context.console.print(f"[dim]Fetching {limit} news items for '{topic}'...[/dim]" if self.context.has_rich
1141
+ else f"Fetching news for {topic}...")
1142
+
1143
+ # Try backend first, then local tools (Finnhub / NewsAPI / AKShare fallback chain)
1144
+ result = await execute_aria_tool(self.terminal.api_url, "analyze_news", {
1145
+ "query": topic, "limit": limit,
1146
+ })
1147
+ if not result.get("success") and "analyze_news" in _get_LOCAL_TOOLS():
1148
+ # Local fallback: uses Finnhub → NewsAPI → AKShare depending on configured keys
1149
+ local_fn = _get_LOCAL_TOOLS()["analyze_news"][0]
1150
+ result = await asyncio.get_event_loop().run_in_executor(
1151
+ None, local_fn, {"query": topic, "symbol": topic, "limit": limit}
1152
+ )
1153
+ if result.get("success"):
1154
+ data = result.get("data", {})
1155
+ if isinstance(data, dict):
1156
+ articles = data.get("articles", data.get("news", []))
1157
+ sentiment = data.get("sentiment", data.get("overall_sentiment", ""))
1158
+ elif isinstance(data, list):
1159
+ articles = data
1160
+ sentiment = ""
1161
+ else:
1162
+ articles = []
1163
+ sentiment = ""
1164
+ if not (isinstance(articles, list) and articles):
1165
+ articles = await asyncio.get_event_loop().run_in_executor(
1166
+ None, _fetch_public_news_fallback, topic, limit
1167
+ )
1168
+ if articles:
1169
+ sentiment = "public RSS fallback"
1170
+ if isinstance(articles, list) and articles:
1171
+ if self.context.has_rich:
1172
+ self.context.console.print()
1173
+ if sentiment:
1174
+ sent_color = "green" if "positive" in sentiment.lower() or "bullish" in sentiment.lower() else (
1175
+ "red" if "negative" in sentiment.lower() or "bearish" in sentiment.lower() else "yellow"
1176
+ )
1177
+ self.context.console.print(f" Sentiment: [{sent_color}]{sentiment}[/{sent_color}]")
1178
+ self.context.console.print()
1179
+ for idx, a in enumerate(articles[:limit], 1):
1180
+ if isinstance(a, dict):
1181
+ title = a.get("title", "Untitled")
1182
+ source = a.get("source", a.get("publisher", ""))
1183
+ url_item = a.get("url", a.get("link", ""))
1184
+ pub_date = a.get("published_at", a.get("date", a.get("publishedAt", "")))
1185
+ if pub_date:
1186
+ pub_date = pub_date[:10] if len(pub_date) >= 10 else pub_date
1187
+ else:
1188
+ title = str(a)
1189
+ source = pub_date = url_item = ""
1190
+ if self.context.has_rich:
1191
+ self.context.console.print(f" [bold]{idx}.[/bold] {title}")
1192
+ meta_parts = [p for p in [source, pub_date] if p]
1193
+ if meta_parts:
1194
+ self.context.console.print(f" [dim]{' · '.join(meta_parts)}[/dim]")
1195
+ else:
1196
+ meta = f" ({source})" if source else ""
1197
+ print(f" {idx}. {title}{meta}")
1198
+ if self.context.has_rich:
1199
+ self.context.console.print()
1200
+ else:
1201
+ # Empty articles — show helpful config guidance
1202
+ _data_keys = _load_data_keys()
1203
+ if self.context.has_rich:
1204
+ self.context.console.print()
1205
+ self.context.console.print(f" [dim]未找到 '{topic}' 的相关新闻。[/dim]")
1206
+ if not _data_keys.get("finnhub") and not _data_keys.get("newsapi"):
1207
+ self.context.console.print(" [dim]配置数据服务 key 可获取更多新闻来源:[/dim]")
1208
+ self.context.console.print(" [dim] /apikey set finnhub <key> → https://finnhub.io/register[/dim]")
1209
+ self.context.console.print(" [dim] /apikey set newsapi <key> → https://newsapi.org/register[/dim]")
1210
+ self.context.console.print()
1211
+ else:
1212
+ articles = await asyncio.get_event_loop().run_in_executor(
1213
+ None, _fetch_public_news_fallback, topic, limit
1214
+ )
1215
+ if isinstance(articles, list) and articles:
1216
+ if self.context.has_rich:
1217
+ self.context.console.print()
1218
+ self.context.console.print(" [dim]新闻 API 不可用,已使用公共 RSS fallback。[/dim]")
1219
+ self.context.console.print()
1220
+ for idx, a in enumerate(articles[:limit], 1):
1221
+ title = a.get("title", "Untitled") if isinstance(a, dict) else str(a)
1222
+ source = a.get("source", "") if isinstance(a, dict) else ""
1223
+ pub_date = a.get("published_at", "") if isinstance(a, dict) else ""
1224
+ if pub_date:
1225
+ pub_date = pub_date[:10] if len(pub_date) >= 10 else pub_date
1226
+ if self.context.has_rich:
1227
+ self.context.console.print(f" [bold]{idx}.[/bold] {title}")
1228
+ meta_parts = [p for p in [source, pub_date] if p]
1229
+ if meta_parts:
1230
+ self.context.console.print(f" [dim]{' · '.join(meta_parts)}[/dim]")
1231
+ else:
1232
+ meta = f" ({source})" if source else ""
1233
+ print(f" {idx}. {title}{meta}")
1234
+ if self.context.has_rich:
1235
+ self.context.console.print()
1236
+ return
1237
+
1238
+ # Backend + all local fallbacks unavailable — show actionable config guide
1239
+ err = result.get("error", "")
1240
+ _data_keys = _load_data_keys()
1241
+ _has_finnhub = bool(_data_keys.get("finnhub"))
1242
+ _has_newsapi = bool(_data_keys.get("newsapi"))
1243
+ if self.context.has_rich:
1244
+ self.context.console.print()
1245
+ self.context.console.print(" [yellow]⚠ 新闻服务不可用[/yellow]")
1246
+ if not _has_finnhub and not _has_newsapi:
1247
+ self.context.console.print(" [dim]配置以下任意一个数据服务 key 即可获取新闻:[/dim]")
1248
+ self.context.console.print(" [dim] Finnhub (免费60次/分) → /apikey set finnhub <key> 注册: https://finnhub.io/register[/dim]")
1249
+ self.context.console.print(" [dim] NewsAPI (免费100次/天) → /apikey set newsapi <key> 注册: https://newsapi.org/register[/dim]")
1250
+ else:
1251
+ self.context.console.print(f" [dim]错误: {err[:120] if err else '获取失败'}[/dim]")
1252
+ self.context.console.print(f" [dim]或使用: /web {topic} latest news — 通过 Brave 搜索[/dim]")
1253
+ self.context.console.print()
1254
+ else:
1255
+ print(" News unavailable. Configure: /apikey set finnhub <key>")
1256
+
1257
+ async def cmd_quote(self, args: str):
1258
+ symbols = parse_symbols(args, self.terminal.config.get("watchlist", ["AAPL"]))
1259
+
1260
+ # 优先使用 MarketDataClient(真实实时数据,代理绕过)
1261
+ if _get__HAS_MDC():
1262
+ mdc = _get_mdc()
1263
+ if self.context.has_rich:
1264
+ self.context.console.print()
1265
+ for symbol in symbols:
1266
+ if self.context.has_rich:
1267
+ with self.context.console.status(f"[dim]{symbol}...[/dim]", spinner="dots"):
1268
+ loop = asyncio.get_event_loop()
1269
+ r = await loop.run_in_executor(None, mdc.quote, symbol)
1270
+ else:
1271
+ r = mdc.quote(symbol)
1272
+
1273
+ if r.get("success"):
1274
+ name = r.get("name", symbol)
1275
+ # Supplement Chinese name for A-shares where yfinance returns ASCII
1276
+ if _is_ashare_symbol(symbol) and (not name or name == symbol or name.replace(" ","").isascii()):
1277
+ _cn = _ashare_code_to_name(symbol)
1278
+ if _cn:
1279
+ name = _cn
1280
+ print_quote_result(console=self.context.console, has_rich=self.context.has_rich, symbol=symbol, quote=r, name=name)
1281
+ else:
1282
+ print_quote_result(console=self.context.console, has_rich=self.context.has_rich, symbol=symbol, quote=r)
1283
+ if self.context.has_rich:
1284
+ self.context.console.print()
1285
+ return
1286
+
1287
+ # Fallback:原有 Aria 工具
1288
+ for symbol in symbols:
1289
+ if self.context.has_rich:
1290
+ with self.context.console.status(f"[dim]Fetching {symbol}...[/dim]", spinner="dots"):
1291
+ result = await execute_aria_tool(self.terminal.api_url, "get_market_data", {
1292
+ "symbol": symbol, "market": "US", "period": "1mo"
1293
+ })
1294
+ else:
1295
+ print(f"Fetching {symbol}...")
1296
+ result = await execute_aria_tool(self.terminal.api_url, "get_market_data", {
1297
+ "symbol": symbol, "market": "US", "period": "1mo"
1298
+ })
1299
+ if not result:
1300
+ print_error(self.context, f"{symbol}: 数据服务不可用(API未运行)", "tool")
1301
+ continue
1302
+ if result.get("success") and result.get("data"):
1303
+ output = format_quote_output(result)
1304
+ self.context.console.print(output)
1305
+ else:
1306
+ print_error(self.context, f"Failed: {result.get('error', 'No data')}")
1307
+
1308
+ async def cmd_screen_cn(self, args: str):
1309
+ """A股选股筛选器 (local, akshare)."""
1310
+ params: Dict[str, Any] = {}
1311
+ for tok in args.split():
1312
+ if "=" in tok:
1313
+ k, v = tok.split("=", 1)
1314
+ params[k.strip()] = v.strip()
1315
+ tool_name = "screen_ashare"
1316
+ if tool_name in _get_LOCAL_TOOLS():
1317
+ await self._run_local_tool(tool_name, params, "A股选股筛选")
1318
+ else:
1319
+ await self.terminal.send_message(f"帮我筛选A股股票,条件:{args or '市值>50亿,非ST,流动性好'}")
1320
+
1321
+ async def cmd_limitup(self, args: str):
1322
+ """A股涨停板池. Usage: /limitup [YYYY-MM-DD] [code_filter]"""
1323
+ import re as _re_lu
1324
+ arg = args.strip()
1325
+
1326
+ # Detect if arg looks like a stock code (6 digits) vs a date
1327
+ _is_code = bool(_re_lu.match(r'^[036]\d{5}$', arg))
1328
+ _is_date = bool(_re_lu.match(r'^\d{4}-\d{2}-\d{2}$', arg))
1329
+ _code_filter = arg if _is_code else None
1330
+ _date_arg = arg if _is_date else ""
1331
+ params = {"date": _date_arg} if _date_arg else {}
1332
+
1333
+ tool_name = "get_limit_up_pool"
1334
+ if tool_name in _get_LOCAL_TOOLS():
1335
+ await self._run_local_tool(tool_name, params, "涨停板池")
1336
+ else:
1337
+ # Direct akshare fallback — avoids "A股" keyword triggering market snapshot routing
1338
+ try:
1339
+ import akshare as ak
1340
+ from datetime import date as _dt
1341
+ _date_str = (_date_arg.replace("-", "") if _date_arg
1342
+ else _dt.today().strftime("%Y%m%d"))
1343
+ _df = ak.stock_zt_pool_em(date=_date_str)
1344
+ if _df is not None and not _df.empty:
1345
+ if _code_filter:
1346
+ _col = next((c for c in _df.columns if "代码" in str(c) or c == "code"), None)
1347
+ if _col:
1348
+ _df = _df[_df[_col].astype(str) == _code_filter]
1349
+ _count = len(_df)
1350
+ if self.context.has_rich:
1351
+ from rich.table import Table
1352
+ _date_label = _date_arg or _dt.today().isoformat()
1353
+ tbl = Table(title=f"涨停板池 · {_date_label} · {_count}只", show_header=True, header_style="bold")
1354
+ _col_map = {"代码": "代码", "名称": "名称", "涨停统计": "涨停统计",
1355
+ "连续涨停": "连板", "首次封板时间": "首封", "涨停类型": "类型"}
1356
+ _show_cols = [c for c in _df.columns if c in _col_map]
1357
+ for c in _show_cols:
1358
+ tbl.add_column(_col_map.get(c, c), no_wrap=True)
1359
+ for _, row in _df.head(30).iterrows():
1360
+ tbl.add_row(*[str(row[c]) for c in _show_cols])
1361
+ self.context.console.print(tbl)
1362
+ else:
1363
+ print(f"涨停板池 {_count}只")
1364
+ for _, row in _df.head(20).iterrows():
1365
+ print(f" {row.get('代码','')} {row.get('名称','')}")
1366
+ return
1367
+ except Exception as _e:
1368
+ pass
1369
+ if self.context.has_rich:
1370
+ self.context.console.print("[yellow]akshare 暂不可用,涨停板池无法获取[/yellow]")
1371
+ else:
1372
+ print("akshare unavailable, cannot fetch limit-up pool")
1373
+
1374
+ async def cmd_north(self, args: str):
1375
+ """北向资金净流入."""
1376
+ params = {"days": int(args.strip())} if args.strip().isdigit() else {"days": 10}
1377
+ tool_name = "get_northbound_flow"
1378
+ if tool_name in _get_LOCAL_TOOLS():
1379
+ await self._run_local_tool(tool_name, params, "北向资金")
1380
+ else:
1381
+ await self.terminal.send_message("查询最近10天北向资金(沪深港通)净买入情况")