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