aria-code 4.1.3__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 (284) hide show
  1. agents/__init__.py +32 -0
  2. agents/base.py +190 -0
  3. agents/deep/__init__.py +37 -0
  4. agents/deep/calibration_loop.py +144 -0
  5. agents/deep/critic.py +125 -0
  6. agents/deep/deepen.py +193 -0
  7. agents/deep/models.py +149 -0
  8. agents/deep/pipeline.py +164 -0
  9. agents/deep/quant_fusion.py +192 -0
  10. agents/deep/themes.py +95 -0
  11. agents/deep/tiers.py +106 -0
  12. agents/financial/__init__.py +10 -0
  13. agents/financial/catalyst.py +279 -0
  14. agents/financial/debate.py +145 -0
  15. agents/financial/earnings.py +303 -0
  16. agents/financial/fundamental.py +159 -0
  17. agents/financial/macro.py +99 -0
  18. agents/financial/news.py +207 -0
  19. agents/financial/risk.py +132 -0
  20. agents/financial/sector.py +279 -0
  21. agents/financial/synthesis.py +274 -0
  22. agents/financial/technical.py +258 -0
  23. agents/portfolio_agent.py +333 -0
  24. agents/realty/__init__.py +62 -0
  25. agents/realty/asset_diagnosis.py +150 -0
  26. agents/realty/business_match.py +165 -0
  27. agents/realty/cashflow_verify.py +208 -0
  28. agents/realty/contract_rules.py +209 -0
  29. agents/realty/energy_anomaly.py +188 -0
  30. agents/realty/exit_settlement.py +207 -0
  31. agents/realty/fulfillment_risk.py +205 -0
  32. agents/realty/ops_optimize.py +159 -0
  33. agents/realty/revenue_share.py +214 -0
  34. agents/registry.py +144 -0
  35. agents/sports/__init__.py +0 -0
  36. agents/sports/football_agent.py +169 -0
  37. agents/team.py +289 -0
  38. aliyun_data_client.py +660 -0
  39. apps/README.md +12 -0
  40. apps/__init__.py +2 -0
  41. apps/channels/README.md +15 -0
  42. apps/cli/README.md +13 -0
  43. apps/cli/__init__.py +2 -0
  44. apps/cli/bootstrap.py +99 -0
  45. apps/cli/codegen_paths.py +29 -0
  46. apps/cli/commands/__init__.py +16 -0
  47. apps/cli/commands/analysis_cmds.py +288 -0
  48. apps/cli/commands/backtest_cmds.py +1887 -0
  49. apps/cli/commands/broker_cmds.py +1154 -0
  50. apps/cli/commands/business_workflow_cmds.py +289 -0
  51. apps/cli/commands/catalog.py +84 -0
  52. apps/cli/commands/data_cmds.py +405 -0
  53. apps/cli/commands/diagnostic_cmds.py +179 -0
  54. apps/cli/commands/diagnostic_ops_cmds.py +696 -0
  55. apps/cli/commands/finance_render.py +12 -0
  56. apps/cli/commands/market.py +399 -0
  57. apps/cli/commands/market_cmds.py +1276 -0
  58. apps/cli/commands/market_context.py +425 -0
  59. apps/cli/commands/market_render.py +7 -0
  60. apps/cli/commands/model_cmds.py +1579 -0
  61. apps/cli/commands/ops_cmds.py +668 -0
  62. apps/cli/commands/portfolio_cmds.py +962 -0
  63. apps/cli/commands/report.py +377 -0
  64. apps/cli/commands/scaffold_templates.py +617 -0
  65. apps/cli/commands/session_cmds.py +179 -0
  66. apps/cli/commands/session_ux_cmds.py +280 -0
  67. apps/cli/commands/team.py +588 -0
  68. apps/cli/commands/team_render.py +8 -0
  69. apps/cli/commands/ui_cmds.py +358 -0
  70. apps/cli/commands/workflow_cmds.py +279 -0
  71. apps/cli/commands/workspace_cmds.py +1414 -0
  72. apps/cli/config_paths.py +70 -0
  73. apps/cli/config_store.py +61 -0
  74. apps/cli/deterministic.py +122 -0
  75. apps/cli/direct.py +48 -0
  76. apps/cli/github_app_auth.py +135 -0
  77. apps/cli/handlers/__init__.py +11 -0
  78. apps/cli/handlers/broker_handlers.py +122 -0
  79. apps/cli/handlers/chart_handlers.py +1309 -0
  80. apps/cli/handlers/market_handlers.py +2509 -0
  81. apps/cli/handlers/realty_handlers.py +114 -0
  82. apps/cli/handlers/strategy_advice.py +82 -0
  83. apps/cli/hooks.py +180 -0
  84. apps/cli/i18n.py +284 -0
  85. apps/cli/intent.py +136 -0
  86. apps/cli/intent_router.py +217 -0
  87. apps/cli/lifecycle_hooks.py +48 -0
  88. apps/cli/main.py +29 -0
  89. apps/cli/market_metadata.py +135 -0
  90. apps/cli/market_universe.py +265 -0
  91. apps/cli/message_processing.py +257 -0
  92. apps/cli/plan_mode.py +139 -0
  93. apps/cli/plotly_html.py +15 -0
  94. apps/cli/prediction_feedback.py +202 -0
  95. apps/cli/preflight.py +497 -0
  96. apps/cli/project_aria.py +60 -0
  97. apps/cli/prompts/__init__.py +0 -0
  98. apps/cli/prompts/coding.py +658 -0
  99. apps/cli/prompts/system_prompts.py +531 -0
  100. apps/cli/prompts/ui.py +434 -0
  101. apps/cli/providers/__init__.py +1 -0
  102. apps/cli/providers/base.py +271 -0
  103. apps/cli/providers/chat_routing.py +80 -0
  104. apps/cli/providers/llm/__init__.py +1 -0
  105. apps/cli/providers/llm/ollama_stream.py +1170 -0
  106. apps/cli/providers/llm/sse_stream.py +216 -0
  107. apps/cli/providers/runtime_bridge.py +185 -0
  108. apps/cli/runtime_consumer.py +489 -0
  109. apps/cli/session_export.py +87 -0
  110. apps/cli/session_jsonl.py +207 -0
  111. apps/cli/session_store.py +112 -0
  112. apps/cli/todo_tracker.py +190 -0
  113. apps/cli/tools/__init__.py +40 -0
  114. apps/cli/tools/context.py +46 -0
  115. apps/cli/tools/file_tools.py +112 -0
  116. apps/cli/tools/market_tools.py +549 -0
  117. apps/cli/tools/notebook_tools.py +111 -0
  118. apps/cli/tools/system_tools.py +669 -0
  119. apps/cli/tools/write_tools.py +715 -0
  120. apps/cli/tradingview_bridge.py +434 -0
  121. apps/cli/update_check.py +152 -0
  122. apps/cli/utils/__init__.py +0 -0
  123. apps/cli/utils/market_detect.py +1578 -0
  124. apps/daemon/README.md +14 -0
  125. apps/vscode/README.md +115 -0
  126. apps/vscode/package.json +70 -0
  127. aria_cli.py +11636 -0
  128. aria_code-4.1.3.dist-info/METADATA +952 -0
  129. aria_code-4.1.3.dist-info/RECORD +284 -0
  130. aria_code-4.1.3.dist-info/WHEEL +5 -0
  131. aria_code-4.1.3.dist-info/entry_points.txt +2 -0
  132. aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
  133. aria_code-4.1.3.dist-info/top_level.txt +50 -0
  134. aria_daemon.py +1295 -0
  135. aria_feishu_bot.py +1359 -0
  136. aria_relay_client.py +182 -0
  137. aria_relay_server.py +405 -0
  138. aria_telegram_bot.py +202 -0
  139. ariarc.py +328 -0
  140. artifacts.py +491 -0
  141. backtest_report.py +472 -0
  142. brokers/__init__.py +72 -0
  143. brokers/base.py +207 -0
  144. brokers/capabilities.py +264 -0
  145. brokers/cn/__init__.py +10 -0
  146. brokers/cn/easytrader_broker.py +193 -0
  147. brokers/cn/futu_broker.py +194 -0
  148. brokers/cn/longbridge_broker.py +190 -0
  149. brokers/cn/tiger_broker.py +196 -0
  150. brokers/cn/xtquant_broker.py +175 -0
  151. brokers/config.py +364 -0
  152. brokers/intl/__init__.py +5 -0
  153. brokers/intl/alpaca_broker.py +183 -0
  154. brokers/intl/ibkr_broker.py +215 -0
  155. brokers/intl/webull_broker.py +156 -0
  156. brokers/paper_broker.py +259 -0
  157. brokers/planning.py +296 -0
  158. brokers/registry.py +181 -0
  159. brokers/trading.py +237 -0
  160. change_store.py +127 -0
  161. command_safety.py +19 -0
  162. computer_use_tools.py +504 -0
  163. dashboard_generator.py +578 -0
  164. data_analysis_tools.py +808 -0
  165. data_cleaner.py +483 -0
  166. data_service.py +481 -0
  167. datasources/__init__.py +23 -0
  168. datasources/base.py +166 -0
  169. datasources/router.py +221 -0
  170. datasources/sources/__init__.py +15 -0
  171. datasources/sources/akshare_source.py +269 -0
  172. datasources/sources/alpha_vantage_source.py +202 -0
  173. datasources/sources/edgar_source.py +218 -0
  174. datasources/sources/finnhub_source.py +197 -0
  175. datasources/sources/fred_source.py +219 -0
  176. datasources/sources/tushare_source.py +141 -0
  177. datasources/sources/web_scraper_source.py +278 -0
  178. datasources/sources/world_bank_source.py +205 -0
  179. datasources/sources/yfinance_source.py +152 -0
  180. demo_player.py +204 -0
  181. doctor.py +508 -0
  182. file_analysis_tools.py +734 -0
  183. finance_formulas.py +389 -0
  184. football_data_client.py +1670 -0
  185. intent_classifier.py +358 -0
  186. local_finance_tools.py +3221 -0
  187. local_llm_provider.py +552 -0
  188. macro_tools.py +368 -0
  189. market_data_client.py +1899 -0
  190. mcp_client.py +506 -0
  191. memory_manager.py +245 -0
  192. model_capability.py +416 -0
  193. notification_tools.py +248 -0
  194. packages/__init__.py +23 -0
  195. packages/aria_agents/__init__.py +5 -0
  196. packages/aria_agents/manifest.py +69 -0
  197. packages/aria_core/__init__.py +34 -0
  198. packages/aria_core/architecture.py +192 -0
  199. packages/aria_core/export.py +124 -0
  200. packages/aria_core/manifest.py +65 -0
  201. packages/aria_infra/__init__.py +15 -0
  202. packages/aria_infra/arthera.py +52 -0
  203. packages/aria_infra/doctor.py +246 -0
  204. packages/aria_infra/product.py +37 -0
  205. packages/aria_mcp/__init__.py +25 -0
  206. packages/aria_mcp/bridge.py +38 -0
  207. packages/aria_mcp/config.py +97 -0
  208. packages/aria_mcp/tools.py +61 -0
  209. packages/aria_sdk/__init__.py +19 -0
  210. packages/aria_sdk/client.py +396 -0
  211. packages/aria_sdk/providers.py +70 -0
  212. packages/aria_sdk/streaming.py +73 -0
  213. packages/aria_sdk/types.py +86 -0
  214. packages/aria_services/__init__.py +55 -0
  215. packages/aria_services/context.py +258 -0
  216. packages/aria_services/data.py +11 -0
  217. packages/aria_services/provider_health.py +189 -0
  218. packages/aria_services/registry.py +213 -0
  219. packages/aria_services/usage.py +138 -0
  220. packages/aria_skills/__init__.py +5 -0
  221. packages/aria_skills/registry.py +59 -0
  222. packages/aria_tools/__init__.py +5 -0
  223. packages/aria_tools/registry.py +128 -0
  224. packages/quant_engine/__init__.py +6 -0
  225. packages/quant_engine/sports/__init__.py +72 -0
  226. packages/quant_engine/sports/calibrator.py +353 -0
  227. packages/quant_engine/sports/dixon_coles.py +234 -0
  228. packages/quant_engine/sports/elo.py +299 -0
  229. packages/quant_engine/sports/form.py +188 -0
  230. packages/quant_engine/sports/h2h.py +195 -0
  231. packages/quant_engine/sports/ml_model.py +354 -0
  232. packages/quant_engine/sports/predictor.py +311 -0
  233. packages/quant_engine/sports/tracker.py +664 -0
  234. packages/quant_engine/stochastic/__init__.py +27 -0
  235. packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  236. packages/quant_engine/stochastic/ito_calculus.py +477 -0
  237. packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  238. packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  239. packages/quant_engine/stochastic/options_pricing.py +573 -0
  240. packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  241. plan_utils.py +194 -0
  242. plugin_loader.py +328 -0
  243. portfolio_ledger.py +262 -0
  244. privacy/__init__.py +5 -0
  245. privacy/feedback.py +123 -0
  246. project_tools.py +525 -0
  247. providers/__init__.py +30 -0
  248. providers/llm/__init__.py +19 -0
  249. providers/llm/anthropic.py +184 -0
  250. providers/llm/base.py +139 -0
  251. providers/llm/ollama.py +128 -0
  252. providers/llm/openai_compat.py +282 -0
  253. providers/llm/registry.py +358 -0
  254. realty_data_tools.py +659 -0
  255. report_generator.py +1314 -0
  256. runtime/__init__.py +103 -0
  257. runtime/agent_loop.py +1183 -0
  258. runtime/approval.py +51 -0
  259. runtime/events.py +102 -0
  260. runtime/gateway.py +128 -0
  261. runtime/lsp.py +346 -0
  262. runtime/subagent.py +258 -0
  263. runtime/tool_executor.py +104 -0
  264. runtime/tool_policy.py +106 -0
  265. safety/__init__.py +21 -0
  266. safety/permissions.py +275 -0
  267. setup_wizard.py +653 -0
  268. strategy_vault.py +420 -0
  269. ui/__init__.py +100 -0
  270. ui/banner.py +310 -0
  271. ui/completer.py +391 -0
  272. ui/console.py +271 -0
  273. ui/image_render.py +243 -0
  274. ui/input_box.py +376 -0
  275. ui/picker.py +195 -0
  276. ui/render/__init__.py +11 -0
  277. ui/render/finance.py +1480 -0
  278. ui/render/market.py +225 -0
  279. ui/render/output.py +681 -0
  280. ui/render/team.py +346 -0
  281. ui/robot.py +235 -0
  282. workspace/__init__.py +6 -0
  283. workspace/files.py +170 -0
  284. workspace/verify.py +113 -0
@@ -0,0 +1,1154 @@
1
+ """BrokerCommandsMixin — /broker /account /positions /orders commands.
2
+
3
+ Extracted from aria_cli.py. Module globals (HAS_RICH, console, etc.) are
4
+ imported lazily inside each method body to avoid circular imports at load time.
5
+ """
6
+ from __future__ import annotations
7
+
8
+
9
+ class BrokerCommandsMixin:
10
+ """Mixin providing broker/account/positions/orders commands."""
11
+
12
+ async def cmd_broker(self, args: str):
13
+ """券商账户管理: /broker list | guide | doctor | services | connect <id> | add <type>"""
14
+ from aria_cli import HAS_RICH, console, Panel, rich_box, _HAS_BROKERS, _print_error
15
+ if not _HAS_BROKERS:
16
+ _print_error("brokers 模块未加载", "请确认 brokers/ 目录存在")
17
+ return
18
+
19
+ parts = args.strip().split(maxsplit=1)
20
+ sub = parts[0].lower() if parts else "list"
21
+ rest = parts[1].strip() if len(parts) > 1 else ""
22
+
23
+ if sub == "list":
24
+ await self._cmd_broker_list()
25
+ elif sub == "status":
26
+ await self._cmd_broker_status()
27
+ elif sub in ("guide", "matrix", "capabilities", "capability", "help"):
28
+ await self._cmd_broker_guide(rest)
29
+ elif sub in ("doctor", "check", "preflight"):
30
+ await self._cmd_broker_doctor(rest)
31
+ elif sub in ("services", "service", "usage"):
32
+ await self._cmd_broker_services()
33
+ elif sub == "connect":
34
+ await self._cmd_broker_connect(rest)
35
+ elif sub == "disconnect":
36
+ await self._cmd_broker_disconnect(rest)
37
+ elif sub in ("add", "new"):
38
+ await self._cmd_broker_add(rest)
39
+ elif sub == "remove":
40
+ await self._cmd_broker_remove(rest)
41
+ elif sub in ("default", "use"):
42
+ await self._cmd_broker_default(rest)
43
+ elif sub == "init":
44
+ await self._cmd_broker_init()
45
+ else:
46
+ if HAS_RICH:
47
+ console.print(Panel(
48
+ "[dim]用法:[/dim]\n"
49
+ " [bold]/broker list[/bold] — 显示所有已配置券商\n"
50
+ " [bold]/broker guide[/bold] [type] — 查看券商能力矩阵和连接步骤\n"
51
+ " [bold]/broker doctor[/bold] — 检查配置字段、SDK 和连接状态\n"
52
+ " [bold]/broker services[/bold] — 查看券商数据如何进入分析/报告/交易计划\n"
53
+ " [bold]/broker connect[/bold] [id] — 连接券商\n"
54
+ " [bold]/broker disconnect[/bold] [id] — 断开连接\n"
55
+ " [bold]/broker status[/bold] — 查看连接状态\n"
56
+ " [bold]/broker add[/bold] <type> — 添加新券商配置\n"
57
+ " [bold]/broker remove[/bold] <id> — 删除券商配置\n"
58
+ " [bold]/broker default[/bold] <id> — 设置默认账户\n"
59
+ " [bold]/broker init[/bold] — 输出所有类型的配置模板",
60
+ title="[bold]/broker[/bold]",
61
+ border_style="dim", box=rich_box.ROUNDED, padding=(0, 1),
62
+ ))
63
+
64
+ async def _cmd_broker_list(self):
65
+ from aria_cli import (HAS_RICH, console, Panel, rich_box,
66
+ _list_broker_configs, _BROKERS_CONFIG_PATH, _get_broker_registry)
67
+ cfgs = _list_broker_configs()
68
+ if not cfgs:
69
+ await self._prompt_no_broker_action()
70
+ return
71
+
72
+ reg = _get_broker_registry()
73
+ if HAS_RICH:
74
+ from rich.table import Table
75
+ tbl = Table(title="[bold]已配置券商[/bold]", show_header=True, header_style="bold")
76
+ tbl.add_column("ID", style="bold")
77
+ tbl.add_column("类型", style="dim")
78
+ tbl.add_column("名称")
79
+ tbl.add_column("市场", style="dim")
80
+ tbl.add_column("状态")
81
+ tbl.add_column("默认", justify="center")
82
+ for c in cfgs:
83
+ bid = c.get("id", "")
84
+ btype = c.get("type", "")
85
+ label = c.get("label", bid)
86
+ b = reg.get(bid) if reg else None
87
+ is_active = reg and reg.active() and reg.active().broker_id == bid
88
+ if b and b.is_connected:
89
+ status = "[green]● 已连接[/green]"
90
+ else:
91
+ status = "[dim]○ 未连接[/dim]"
92
+ market_map = {
93
+ "xtquant":"A股","easytrader":"A股","futu":"港/美/A",
94
+ "tiger":"美/港/A","longbridge":"港/美/A",
95
+ "ibkr":"美股","alpaca":"美股","webull":"美股",
96
+ }
97
+ mkt = c.get("market", market_map.get(btype, "—"))
98
+ default_mark = "[green]✓[/green]" if c.get("default") or is_active else ""
99
+ tbl.add_row(bid, btype, label, mkt, status, default_mark)
100
+ console.print(tbl)
101
+ else:
102
+ for c in cfgs:
103
+ print(f" {c.get('id',''):<20} {c.get('type',''):<12} {c.get('label','')}")
104
+
105
+ async def _cmd_broker_status(self):
106
+ from aria_cli import HAS_RICH, console, _get_broker_registry
107
+ reg = _get_broker_registry()
108
+ connected = reg.list_connected() if reg else []
109
+ if not connected:
110
+ if HAS_RICH:
111
+ console.print("[dim]当前无已连接券商。运行 [bold]/broker connect[/bold] 建立连接。[/dim]")
112
+ else:
113
+ print("无已连接券商")
114
+ return
115
+ for b in connected:
116
+ try:
117
+ acct = b.account_info()
118
+ line = (
119
+ f"[green]●[/green] [bold]{b.label}[/bold] ({b.broker_type})"
120
+ f" 账户: {acct.masked_account}"
121
+ f" 总资产: [bold]{acct.currency} {acct.total_assets:,.2f}[/bold]"
122
+ f" 可用: {acct.cash:,.2f}"
123
+ )
124
+ except Exception as e:
125
+ line = f"[yellow]●[/yellow] [bold]{b.label}[/bold] ({b.broker_type}) [dim]查询失败: {e}[/dim]"
126
+ if HAS_RICH:
127
+ console.print(line)
128
+ else:
129
+ print(line)
130
+
131
+ async def _cmd_broker_guide(self, broker_type: str = ""):
132
+ """Show broker capability matrix or a single broker setup plan."""
133
+ from aria_cli import HAS_RICH, console, Panel, rich_box, _print_error
134
+ from brokers.capabilities import (
135
+ broker_connection_plan, broker_dependency_state,
136
+ get_broker_capability, list_broker_capabilities,
137
+ )
138
+ from brokers.config import get_broker_config
139
+
140
+ query = (broker_type or "").strip().split(maxsplit=1)[0].lower()
141
+ spec = get_broker_capability(query) if query else None
142
+ if query and not spec:
143
+ cfg = get_broker_config(query)
144
+ if cfg:
145
+ spec = get_broker_capability(str(cfg.get("type", "")))
146
+ if query and not spec:
147
+ _print_error(f"未知券商类型或配置 id: {query}", "运行 /broker guide 查看支持列表")
148
+ return
149
+
150
+ if spec:
151
+ dep = broker_dependency_state(spec)
152
+ lines = [
153
+ f"类型: {spec.broker_type}",
154
+ f"市场: {', '.join(spec.markets)}",
155
+ f"读取能力: {', '.join(spec.read_capabilities)}",
156
+ f"交易能力: {spec.trade_capability}",
157
+ f"SDK: {spec.sdk_module} / {spec.pip_package} "
158
+ f"({'已安装' if dep['installed'] else '未安装'})",
159
+ f"本地运行时: {spec.local_runtime}",
160
+ f"配置字段: {', '.join(spec.credential_fields)}",
161
+ "",
162
+ "接入步骤:",
163
+ *[f" {idx}. {step}" for idx, step in enumerate(broker_connection_plan(spec.broker_type), 1)],
164
+ "",
165
+ "安全边界:",
166
+ *[f" - {note}" for note in spec.safety_notes],
167
+ ]
168
+ if HAS_RICH:
169
+ console.print(Panel(
170
+ "\n".join(lines),
171
+ title=f"[bold]{spec.display_name}[/bold] 连接指南",
172
+ border_style="blue",
173
+ box=rich_box.ROUNDED,
174
+ padding=(0, 1),
175
+ ))
176
+ else:
177
+ print("\n".join(lines))
178
+ return
179
+
180
+ specs = list_broker_capabilities()
181
+ if HAS_RICH:
182
+ from rich.table import Table
183
+ tbl = Table(
184
+ title="[bold]券商能力矩阵[/bold]",
185
+ show_header=True,
186
+ header_style="bold dim",
187
+ box=rich_box.ROUNDED,
188
+ border_style="dim",
189
+ )
190
+ tbl.add_column("Type", style="bold")
191
+ tbl.add_column("券商")
192
+ tbl.add_column("市场")
193
+ tbl.add_column("SDK")
194
+ tbl.add_column("交易")
195
+ tbl.add_column("运行时 / 凭证")
196
+ tbl.add_column("下一步")
197
+ for item in specs:
198
+ dep = broker_dependency_state(item)
199
+ sdk = f"{item.sdk_module} " + ("[green]✓[/green]" if dep["installed"] else "[yellow]未装[/yellow]")
200
+ trade = "[green]可交易[/green]" if item.can_trade else "[dim]只读[/dim]"
201
+ runtime = item.local_runtime.split(";", 1)[0]
202
+ tbl.add_row(
203
+ item.broker_type,
204
+ item.display_name,
205
+ "/".join(item.markets),
206
+ sdk,
207
+ trade,
208
+ runtime,
209
+ f"/broker guide {item.broker_type}",
210
+ )
211
+ console.print(tbl)
212
+ console.print("[dim]配置路径: brokers.json;添加: /broker add <type>;体检: /broker doctor[/dim]")
213
+ else:
214
+ for item in specs:
215
+ print(
216
+ f"{item.broker_type:<12} {item.display_name:<28} "
217
+ f"{'/'.join(item.markets):<12} {item.pip_package:<14} {item.trade_capability}"
218
+ )
219
+
220
+ async def _cmd_broker_doctor(self, args: str = ""):
221
+ """Check configured broker fields, SDK availability, and connection state."""
222
+ from aria_cli import HAS_RICH, console, Panel, rich_box
223
+ from brokers.capabilities import broker_dependency_state, get_broker_capability
224
+ from brokers.config import BROKERS_CONFIG_PATH, list_broker_configs, validate_broker_config
225
+ from aria_cli import _get_broker_registry
226
+
227
+ cfgs = list_broker_configs()
228
+ reg = _get_broker_registry()
229
+ if not cfgs:
230
+ if HAS_RICH:
231
+ console.print(Panel(
232
+ "尚未配置券商。\n\n"
233
+ "先运行 /broker guide 选择券商类型,再运行 /broker add <type>。\n"
234
+ f"配置文件: {BROKERS_CONFIG_PATH}",
235
+ title="[bold]Broker Doctor[/bold]",
236
+ border_style="yellow",
237
+ box=rich_box.ROUNDED,
238
+ padding=(0, 1),
239
+ ))
240
+ else:
241
+ print(f"尚未配置券商。配置文件: {BROKERS_CONFIG_PATH}")
242
+ return
243
+
244
+ rows = []
245
+ for cfg in cfgs:
246
+ broker_id = str(cfg.get("id", ""))
247
+ btype = str(cfg.get("type", ""))
248
+ spec = get_broker_capability(btype)
249
+ errors = validate_broker_config(cfg)
250
+ fatal = [err for err in errors if not str(err).startswith("⚠")]
251
+ warnings = [err for err in errors if str(err).startswith("⚠")]
252
+ dep = broker_dependency_state(spec) if spec else {
253
+ "installed": False,
254
+ "install_hint": "",
255
+ "module": "",
256
+ "package": "",
257
+ }
258
+ broker = reg.get(broker_id) if reg and broker_id else None
259
+ connected = bool(broker and broker.is_connected)
260
+ if fatal:
261
+ status = "fail"
262
+ next_step = fatal[0]
263
+ elif not dep["installed"]:
264
+ status = "warn"
265
+ next_step = str(dep["install_hint"] or f"安装 {dep['package']}")
266
+ elif warnings:
267
+ status = "warn"
268
+ next_step = warnings[0]
269
+ elif not connected:
270
+ status = "ready"
271
+ next_step = f"/broker connect {broker_id}"
272
+ else:
273
+ status = "ok"
274
+ next_step = "/account /positions"
275
+ rows.append({
276
+ "id": broker_id,
277
+ "type": btype,
278
+ "label": str(cfg.get("label", broker_id)),
279
+ "sdk": str(dep.get("module") or "—"),
280
+ "sdk_ok": bool(dep.get("installed")),
281
+ "connected": connected,
282
+ "status": status,
283
+ "next": next_step,
284
+ })
285
+
286
+ if HAS_RICH:
287
+ from rich.table import Table
288
+ tbl = Table(
289
+ title="[bold]Broker Doctor[/bold]",
290
+ show_header=True,
291
+ header_style="bold dim",
292
+ box=rich_box.ROUNDED,
293
+ border_style="dim",
294
+ )
295
+ tbl.add_column("ID", style="bold")
296
+ tbl.add_column("Type")
297
+ tbl.add_column("SDK")
298
+ tbl.add_column("连接")
299
+ tbl.add_column("状态")
300
+ tbl.add_column("下一步")
301
+ colors = {"ok": "green", "ready": "cyan", "warn": "yellow", "fail": "red"}
302
+ for row in rows:
303
+ color = colors.get(row["status"], "dim")
304
+ tbl.add_row(
305
+ row["id"],
306
+ row["type"],
307
+ f"{row['sdk']} {'✓' if row['sdk_ok'] else '未装'}",
308
+ "已连接" if row["connected"] else "未连接",
309
+ f"[{color}]{row['status']}[/{color}]",
310
+ row["next"],
311
+ )
312
+ console.print(tbl)
313
+ console.print(f"[dim]配置文件: {BROKERS_CONFIG_PATH}[/dim]")
314
+ else:
315
+ for row in rows:
316
+ print(f"{row['id']} {row['type']} {row['status']} next={row['next']}")
317
+
318
+ async def _cmd_broker_services(self):
319
+ """Show how broker data flows into Aria services."""
320
+ from aria_cli import HAS_RICH, console, rich_box
321
+ from brokers.capabilities import broker_service_playbook
322
+
323
+ rows = broker_service_playbook()
324
+ if HAS_RICH:
325
+ from rich.table import Table
326
+ tbl = Table(
327
+ title="[bold]券商数据与项目服务联动[/bold]",
328
+ show_header=True,
329
+ header_style="bold dim",
330
+ box=rich_box.ROUNDED,
331
+ border_style="dim",
332
+ )
333
+ tbl.add_column("服务", width=18)
334
+ tbl.add_column("命令 / 流程", width=42)
335
+ tbl.add_column("被哪些能力使用", width=34)
336
+ tbl.add_column("安全边界")
337
+ for row in rows:
338
+ tbl.add_row(row["service"], row["commands"], row["used_by"], row["guardrail"])
339
+ console.print(tbl)
340
+ else:
341
+ for row in rows:
342
+ print(f"{row['service']}: {row['commands']} | {row['guardrail']}")
343
+
344
+ async def _cmd_broker_connect(self, broker_id: str):
345
+ from aria_cli import (HAS_RICH, console, _list_broker_configs,
346
+ _BROKERS_CONFIG_PATH, _get_broker_registry, _print_error)
347
+ cfgs = _list_broker_configs()
348
+ if not cfgs:
349
+ _print_error("尚未配置任何券商", f"请先编辑 {_BROKERS_CONFIG_PATH}")
350
+ return
351
+ if not broker_id:
352
+ from brokers.config import get_default_broker_config
353
+ cfg = get_default_broker_config()
354
+ if not cfg:
355
+ _print_error("未设置默认券商", "请用 /broker connect <id> 指定")
356
+ return
357
+ broker_id = cfg["id"]
358
+
359
+ reg = _get_broker_registry()
360
+ label = broker_id
361
+ try:
362
+ if HAS_RICH:
363
+ with console.status(f"[dim]正在连接 {broker_id}...[/dim]", spinner="dots"):
364
+ import asyncio as _aio
365
+ loop = _aio.get_event_loop()
366
+ broker = await loop.run_in_executor(None, reg.connect, broker_id)
367
+ else:
368
+ broker = reg.connect(broker_id)
369
+ label = broker.label
370
+ msg = f"[green]✓[/green] 已连接 [bold]{label}[/bold] ({broker.broker_type})"
371
+ if HAS_RICH:
372
+ console.print(msg)
373
+ else:
374
+ print(f"已连接 {label}")
375
+ except Exception as e:
376
+ # If the failure is a missing broker SDK, give actionable guidance
377
+ # that ties into the dependency system (/install <pkg>).
378
+ _err = str(e)
379
+ _btype = ""
380
+ try:
381
+ _btype = next((c.get("type", "") for c in cfgs
382
+ if c.get("id") == broker_id), "")
383
+ except Exception:
384
+ pass
385
+ _SDK_PKG = {
386
+ "longbridge": "longbridge", "ibkr": "ib_insync", "futu": "futu-api",
387
+ "tiger": "tigeropen", "alpaca": "alpaca-py", "webull": "webull",
388
+ "easytrader": "easytrader",
389
+ }
390
+ _pkg = _SDK_PKG.get(_btype, "")
391
+ # Primary signal: can we import the SDK module? If not, it's a
392
+ # missing-dependency failure regardless of the error message
393
+ # language (the SDK raises a localized "未安装" ImportError).
394
+ _is_missing_sdk = False
395
+ if _pkg:
396
+ _mod = {"longbridge": "longbridge", "ibkr": "ib_insync",
397
+ "futu": "futu", "tiger": "tigeropen", "alpaca": "alpaca",
398
+ "webull": "webull", "easytrader": "easytrader"}.get(_btype, _pkg)
399
+ try:
400
+ import importlib as _il
401
+ _il.import_module(_mod)
402
+ except Exception:
403
+ _is_missing_sdk = True
404
+ if _is_missing_sdk:
405
+ # Put the actionable guidance in msg — _print_error's `context`
406
+ # arg is a category keyword, not free-text, so it won't render.
407
+ _print_error(
408
+ f"连接失败: {label} — {_btype} SDK 未安装。"
409
+ f"运行 /install {_pkg} 安装后重试(会先确认)。",
410
+ )
411
+ else:
412
+ _print_error(f"连接失败: {label}", _err or "未知错误(检查账户配置与网络)")
413
+
414
+ async def _cmd_broker_disconnect(self, broker_id: str):
415
+ from aria_cli import HAS_RICH, console, _get_broker_registry, _print_error
416
+ reg = _get_broker_registry()
417
+ if not broker_id:
418
+ b = reg.active() if reg else None
419
+ if not b:
420
+ _print_error("无活跃券商", "请指定 id:/broker disconnect <id>")
421
+ return
422
+ broker_id = b.broker_id
423
+ if reg:
424
+ reg.disconnect(broker_id)
425
+ msg = f"[dim]已断开连接: {broker_id}[/dim]"
426
+ if HAS_RICH:
427
+ console.print(msg)
428
+ else:
429
+ print(f"已断开: {broker_id}")
430
+
431
+ async def _cmd_broker_add(self, broker_type: str):
432
+ from aria_cli import (HAS_RICH, console, Panel, rich_box, _print_error,
433
+ _supported_broker_types, _get_broker_template,
434
+ _add_broker_cfg, _BROKERS_CONFIG_PATH)
435
+ from ui.picker import arrow_select
436
+
437
+ supported = _supported_broker_types()
438
+
439
+ # ── 按市场分组、固定宽度对齐 ─────────────────────────────────────────
440
+ # 每组: (分组标签, [broker_key, ...])
441
+ _GROUPS = [
442
+ ("仿盘", ["paper"]),
443
+ ("A 股", ["xtquant", "easytrader"]),
444
+ ("港股 / 美股", ["futu", "tiger", "longbridge"]),
445
+ ("美股 / 国际", ["ibkr", "alpaca", "webull"]),
446
+ ]
447
+ _CAT = {
448
+ "paper": "仿盘",
449
+ "xtquant": "A股", "easytrader": "A股",
450
+ "futu": "港/美/A", "tiger": "港/美/A", "longbridge": "港/美/A",
451
+ "ibkr": "全球", "alpaca": "美股", "webull": "美股",
452
+ }
453
+
454
+ # ── 选择券商类型 ─────────────────────────────────────────────────────
455
+ if not broker_type or broker_type not in supported:
456
+ # Build ordered list with separators for display grouping.
457
+ # Separator entries have key=None and won't be assigned.
458
+ all_items = [] # (display_label, desc_str, key_or_None)
459
+ for g_label, g_keys in _GROUPS:
460
+ all_items.append((f"─── {g_label} ", "", None))
461
+ for k in g_keys:
462
+ if k in supported:
463
+ all_items.append((f" {k:<12}", supported[k], k))
464
+
465
+ picker_options = [(label, desc) for label, desc, _ in all_items]
466
+ sep_indices = {i for i, (_, _, key) in enumerate(all_items) if key is None}
467
+ key_at = {i: key for i, (_, _, key) in enumerate(all_items) if key}
468
+
469
+ # Start cursor on first real entry (skip leading separator)
470
+ first_real = next((i for i in range(len(all_items)) if i not in sep_indices), 0)
471
+
472
+ if HAS_RICH:
473
+ console.print(
474
+ "[bold]选择要添加的券商[/bold] "
475
+ "[dim]↑↓ 移动 Enter 确认 q 取消[/dim]"
476
+ )
477
+
478
+ # If user lands on a separator, nudge to the next real entry.
479
+ while True:
480
+ idx = _arrow_select(picker_options, selected=first_real,
481
+ title="", max_visible=12)
482
+ if idx < 0:
483
+ return
484
+ if idx in sep_indices:
485
+ # Find next real entry below; wrap to first if none
486
+ nxt = next((i for i in range(idx + 1, len(all_items))
487
+ if i not in sep_indices), first_real)
488
+ first_real = nxt
489
+ continue
490
+ broker_type = key_at[idx]
491
+ break
492
+
493
+ tmpl = _get_broker_template(broker_type)
494
+ if not tmpl:
495
+ _print_error(f"无法获取 {broker_type} 模板", "")
496
+ return
497
+
498
+ # ── 开户 & 凭证获取指南 ───────────────────────────────────────────
499
+ _GUIDE: dict[str, str] = {
500
+ "paper": (
501
+ "Aria 本地仿盘账户 — 无需真实券商\n\n"
502
+ "使用步骤:\n"
503
+ " 1. 运行 /paper start 100000 USD 创建本地仿盘账户\n"
504
+ " 2. 运行 /trade preview AAPL buy 10 190 生成订单预览\n"
505
+ " 3. 运行 /trade confirm <preview_id> 执行仿盘成交\n\n"
506
+ "数据位置:~/.arthera/paper_ledger.json"
507
+ ),
508
+ "alpaca": (
509
+ "Alpaca Markets — 免费美股/加密货币 API(支持模拟盘)\n\n"
510
+ "获取 API Key 步骤:\n"
511
+ " 1. 注册账号: https://app.alpaca.markets/signup\n"
512
+ " 2. 登录后进入 Paper Trading → API Keys\n"
513
+ " 3. 点击 [Generate New Key],复制 Key 和 Secret\n"
514
+ " 4. 模拟盘(paper=true)无需入金即可使用\n"
515
+ " 5. 实盘:修改 paper=false 并完成入金认证\n\n"
516
+ "依赖:pip install alpaca-py"
517
+ ),
518
+ "tiger": (
519
+ "老虎证券 OpenAPI — 港股 / 美股\n\n"
520
+ "获取凭证步骤:\n"
521
+ " 1. 在老虎证券 App 开户并完成实名认证\n"
522
+ " 2. 访问开发者平台: https://quant.tigeropen.com\n"
523
+ " 3. 创建应用,获取 Tiger ID 和 RSA 密钥对\n"
524
+ " 4. 将私钥文件保存到 ~/.arthera/tiger_rsa.pem\n\n"
525
+ "依赖:pip install tigeropen"
526
+ ),
527
+ "longbridge": (
528
+ "长桥证券 OpenAPI — 港股 / 美股 / A股\n\n"
529
+ "获取凭证步骤:\n"
530
+ " 1. 在长桥 App 开户并完成入金\n"
531
+ " 2. 开发者中心: https://open.longportapp.com\n"
532
+ " 3. 创建应用获取 App Key、App Secret、Access Token\n\n"
533
+ "依赖:pip install longbridge"
534
+ ),
535
+ "ibkr": (
536
+ "Interactive Brokers TWS/Gateway — 全球市场\n\n"
537
+ "连接步骤:\n"
538
+ " 1. 开户: https://www.interactivebrokers.com\n"
539
+ " 2. 下载并启动 TWS 或 IB Gateway(保持后台运行)\n"
540
+ " 3. TWS → 配置 → API → 启用 Socket Client\n"
541
+ " 实盘端口 7496,模拟端口 7497\n"
542
+ " 4. Gateway 端口:实盘 4001,模拟 4002\n\n"
543
+ "依赖:pip install ib_insync"
544
+ ),
545
+ "futu": (
546
+ "富途牛牛 OpenAPI — 港股 / 美股\n\n"
547
+ "连接步骤:\n"
548
+ " 1. 在富途牛牛 App 开户\n"
549
+ " 2. 下载并启动 FutuOpenD\n"
550
+ " (牛牛客户端 → 更多 → OpenD)\n"
551
+ " 3. OpenD 默认监听 127.0.0.1:11111,保持运行\n"
552
+ " 4. 开发者文档: https://openapi.futunn.com\n\n"
553
+ "依赖:pip install futu-api"
554
+ ),
555
+ "webull": (
556
+ "Webull — 美股(非官方 API,行情查询为主)\n\n"
557
+ "获取凭证步骤:\n"
558
+ " 1. 注册: https://www.webull.com\n"
559
+ " 2. 使用注册邮箱/手机号 + 密码即可\n"
560
+ " 3. device_id 首次留空,登录后自动填充\n"
561
+ " 4. 建议仅用于行情查询,下单功能稳定性有限\n\n"
562
+ "依赖:pip install webull"
563
+ ),
564
+ "xtquant": (
565
+ "迅投 XTQuant — A股(中信/华鑫/浙商等券商)\n\n"
566
+ "获取凭证步骤:\n"
567
+ " 1. 在支持的券商(中信/华鑫/浙商等)开户\n"
568
+ " 2. 从券商获取并安装 QMT 量化交易终端\n"
569
+ " 3. 登录 QMT 后保持运行\n"
570
+ " 4. account_id 即你的券商账号\n"
571
+ " 5. 仅支持 Windows / Linux (Wine)\n\n"
572
+ "依赖:pip install xtquant (安装包需从券商获取)"
573
+ ),
574
+ "easytrader": (
575
+ "EasyTrader — A股(同花顺/通达信/华泰/国君等)\n\n"
576
+ "配置步骤:\n"
577
+ " 1. 安装对应券商的交易客户端\n"
578
+ " 2. broker_name 可选值:\n"
579
+ " huatai / guojun / ths / tdx / yh / zszq / xq\n"
580
+ " 3. exe_path 填写客户端完整路径\n"
581
+ " 4. 使用时需保持客户端登录运行\n"
582
+ " 5. 仅支持 Windows\n\n"
583
+ "依赖:pip install easytrader"
584
+ ),
585
+ }
586
+
587
+ guide = _GUIDE.get(broker_type, "")
588
+ cat = _CAT.get(broker_type, "")
589
+ if guide and HAS_RICH:
590
+ console.print(Panel(
591
+ guide,
592
+ title=f"[bold]{supported[broker_type]}[/bold]"
593
+ + (f" [dim]{cat}[/dim]" if cat else "")
594
+ + " — 开户 & 凭证获取指南",
595
+ border_style="blue", box=rich_box.ROUNDED, padding=(0, 2),
596
+ ))
597
+ try:
598
+ input("\n [Enter] 继续填写配置 Ctrl+C 取消 › ")
599
+ except (EOFError, KeyboardInterrupt):
600
+ if HAS_RICH:
601
+ console.print("[dim]已取消[/dim]")
602
+ return
603
+
604
+ # ── 对话式配置向导 ─────────────────────────────────────────────────
605
+ # 字段元组: (key, 说明, 默认值, 是否隐藏输入, 是否可选)
606
+ _WIZARD: dict[str, list[tuple]] = {
607
+ "alpaca": [
608
+ ("id", "配置 ID (用于 /broker connect)", "alpaca_paper", False, False),
609
+ ("label", "显示名称", "Alpaca 模拟盘", False, False),
610
+ ("api_key", "API Key", "", False, False),
611
+ ("api_secret", "API Secret", "", True, False),
612
+ ("paper", "模拟盘 (true=模拟 / false=实盘)", "true", False, False),
613
+ ],
614
+ "paper": [
615
+ ("id", "配置 ID", "paper_main", False, False),
616
+ ("label", "显示名称", "Aria 仿盘账户", False, False),
617
+ ("starting_cash", "初始资金", "100000", False, False),
618
+ ("currency", "币种", "USD", False, False),
619
+ ],
620
+ "tiger": [
621
+ ("id", "配置 ID", "tiger_us", False, False),
622
+ ("label", "显示名称", "老虎", False, False),
623
+ ("tiger_id", "Tiger ID", "", False, False),
624
+ ("account", "账户号", "", False, False),
625
+ ("private_key_path", "RSA 私钥路径", "~/.arthera/tiger_rsa.pem", False, True),
626
+ ],
627
+ "longbridge": [
628
+ ("id", "配置 ID", "lb_main", False, False),
629
+ ("label", "显示名称", "长桥", False, False),
630
+ ("app_key", "App Key", "", False, False),
631
+ ("app_secret", "App Secret", "", True, False),
632
+ ("access_token", "Access Token", "", True, False),
633
+ ],
634
+ "ibkr": [
635
+ ("id", "配置 ID", "ibkr_main", False, False),
636
+ ("label", "显示名称", "盈透", False, False),
637
+ ("host", "TWS/Gateway 主机", "127.0.0.1", False, False),
638
+ ("port", "端口 (TWS实盘=7496 模拟=7497 Gateway=4001)", "7496", False, False),
639
+ ("client_id", "Client ID (每个连接唯一,整数)", "1", False, True),
640
+ ],
641
+ "futu": [
642
+ ("id", "配置 ID", "futu_main", False, False),
643
+ ("label", "显示名称", "富途", False, False),
644
+ ("host", "OpenD 主机", "127.0.0.1", False, False),
645
+ ("port", "OpenD 端口", "11111", False, False),
646
+ ("market", "市场 (HK / US / CN)", "HK", False, True),
647
+ ],
648
+ "webull": [
649
+ ("id", "配置 ID", "webull_main", False, False),
650
+ ("label", "显示名称", "Webull", False, False),
651
+ ("username", "邮箱或手机号", "", False, False),
652
+ ("password", "密码", "", True, False),
653
+ ("device_id", "设备 ID", "", False, True),
654
+ ],
655
+ "xtquant": [
656
+ ("id", "配置 ID", "xt_main", False, False),
657
+ ("label", "显示名称", "XTQuant", False, False),
658
+ ("account_id", "账户号", "", False, False),
659
+ ],
660
+ "easytrader": [
661
+ ("id", "配置 ID", "et_main", False, False),
662
+ ("label", "显示名称", "EasyTrader", False, False),
663
+ ("broker_name", "券商名", "huatai", False, False),
664
+ ("exe_path", "客户端路径", "C:\\华泰证券\\xiadan.exe", False, True),
665
+ ],
666
+ }
667
+
668
+ fields = _WIZARD.get(broker_type, [])
669
+ total = len(fields)
670
+
671
+ import getpass as _getpass
672
+
673
+ if HAS_RICH:
674
+ console.print(Panel(
675
+ f"[bold]{supported[broker_type]}[/bold] 配置向导 "
676
+ f"[dim]共 {total} 项[/dim]\n"
677
+ f"[dim]Enter = 使用括号内默认值 / 标注 (可选) 的字段可直接跳过[/dim]",
678
+ border_style="dim", box=rich_box.ROUNDED, padding=(0, 1),
679
+ ))
680
+ console.print()
681
+
682
+ filled = dict(tmpl)
683
+ for step, (key, label, default, secret, optional) in enumerate(fields, 1):
684
+ # Progress prefix: [1/5]
685
+ progress = f"[{step}/{total}]"
686
+ opt_tag = " (可选)" if optional else ""
687
+
688
+ if default:
689
+ prompt_str = f" {progress} {label}{opt_tag} [{default}]: "
690
+ else:
691
+ prompt_str = f" {progress} {label}{opt_tag}: "
692
+
693
+ try:
694
+ if secret:
695
+ val = _getpass.getpass(prompt_str) or default
696
+ else:
697
+ val = input(prompt_str).strip() or default
698
+ except (EOFError, KeyboardInterrupt):
699
+ if HAS_RICH:
700
+ console.print("\n[dim]已取消[/dim]")
701
+ else:
702
+ print("\n已取消")
703
+ return
704
+
705
+ # Type coercion
706
+ if key == "paper":
707
+ filled[key] = val.lower() not in ("false", "0", "no", "f")
708
+ elif key == "starting_cash" and val:
709
+ try:
710
+ filled[key] = float(val)
711
+ except ValueError:
712
+ filled[key] = val
713
+ elif key in ("port", "client_id") and val:
714
+ try:
715
+ filled[key] = int(val)
716
+ except ValueError:
717
+ filled[key] = val
718
+ elif val:
719
+ filled[key] = val
720
+
721
+ filled.pop("_comment", None)
722
+
723
+ # ── 保存配置 ──────────────────────────────────────────────────────
724
+ try:
725
+ _add_broker_cfg(filled)
726
+ broker_id = filled.get("id", broker_type)
727
+ if HAS_RICH:
728
+ console.print()
729
+ console.print(Panel(
730
+ f"[green]✓ 已保存[/green] {broker_id} [dim]→ {_BROKERS_CONFIG_PATH}[/dim]",
731
+ border_style="green", box=rich_box.ROUNDED, padding=(0, 1),
732
+ ))
733
+ else:
734
+ print(f"✓ 已保存 {broker_id}")
735
+ except Exception as exc:
736
+ _print_error(f"保存失败: {exc}", f"请手动编辑 {_BROKERS_CONFIG_PATH}")
737
+ return
738
+
739
+ # ── 保存后即刻连接 ────────────────────────────────────────────────
740
+ try:
741
+ ans = input(f"\n 是否立即尝试连接 {broker_id}? (y/N) › ").strip().lower()
742
+ except (EOFError, KeyboardInterrupt):
743
+ ans = ""
744
+ if ans in ("y", "yes", "是"):
745
+ await self._cmd_broker_connect(broker_id)
746
+ else:
747
+ if HAS_RICH:
748
+ console.print(
749
+ f"[dim]稍后可运行 [bold]/broker connect {broker_id}[/bold] 建立连接[/dim]"
750
+ )
751
+
752
+ async def _cmd_broker_remove(self, broker_id: str):
753
+ from aria_cli import HAS_RICH, console, _print_error, _remove_broker_cfg
754
+ if not broker_id:
755
+ _print_error("请指定要删除的券商 id", "/broker remove <id>")
756
+ return
757
+ removed = _remove_broker_cfg(broker_id)
758
+ if removed:
759
+ console.print(f"[dim]已删除券商配置: {broker_id}[/dim]") if HAS_RICH else print(f"已删除: {broker_id}")
760
+ else:
761
+ _print_error(f"未找到券商: {broker_id}", "")
762
+
763
+ async def _cmd_broker_default(self, broker_id: str):
764
+ from aria_cli import HAS_RICH, console, _print_error, _set_default_broker, _get_broker_registry
765
+ if not broker_id:
766
+ _print_error("请指定 id", "/broker default <id>")
767
+ return
768
+ ok = _set_default_broker(broker_id)
769
+ if ok:
770
+ reg = _get_broker_registry()
771
+ if reg:
772
+ reg.set_active(broker_id)
773
+ msg = f"[green]✓[/green] 默认账户已设为: [bold]{broker_id}[/bold]"
774
+ if HAS_RICH:
775
+ console.print(msg)
776
+ else:
777
+ print(f"默认账户: {broker_id}")
778
+ else:
779
+ _print_error(f"未找到券商: {broker_id}", "请先用 /broker add 添加")
780
+
781
+ async def _cmd_broker_init(self):
782
+ from aria_cli import HAS_RICH, console, Panel, rich_box, _BROKERS_CONFIG_PATH
783
+ from brokers.config import print_all_templates
784
+ if HAS_RICH:
785
+ console.print(Panel(
786
+ f"[dim]将以下内容保存到[/dim] [bold]{_BROKERS_CONFIG_PATH}[/bold] [dim],填写实际凭证后运行 /broker connect 连接。[/dim]\n\n"
787
+ f"[dim](仅保留你需要的券商,删除不用的)[/dim]",
788
+ title="[bold]所有券商配置模板[/bold]",
789
+ border_style="dim", box=rich_box.ROUNDED, padding=(0, 1),
790
+ ))
791
+ from rich.syntax import Syntax
792
+ console.print(Syntax(print_all_templates(), "json", theme="monokai", line_numbers=False))
793
+ else:
794
+ print(print_all_templates())
795
+
796
+ async def cmd_account(self, args: str):
797
+ """显示账户资金汇总。"""
798
+ from aria_cli import _HAS_BROKERS, _print_error, _get_broker_registry, _print_broker_account
799
+ if not _HAS_BROKERS:
800
+ _print_error("brokers 模块未加载", "")
801
+ return
802
+ broker_id = args.strip()
803
+ reg = _get_broker_registry()
804
+ try:
805
+ broker = reg.get(broker_id) if broker_id else reg.active()
806
+ if not broker:
807
+ broker = await self._auto_connect_broker(broker_id)
808
+ if not broker:
809
+ return
810
+ import asyncio as _aio
811
+ acct = await _aio.get_event_loop().run_in_executor(None, broker.account_info)
812
+ _print_broker_account(acct)
813
+ except Exception as e:
814
+ _print_error(f"账户查询失败: {e}", "请检查券商连接状态 (/broker status)")
815
+
816
+ async def cmd_positions(self, args: str):
817
+ """显示当前持仓。"""
818
+ from aria_cli import (HAS_RICH, console, _null_ctx, _HAS_BROKERS,
819
+ _print_error, _get_broker_registry, _print_broker_positions)
820
+ if not _HAS_BROKERS:
821
+ _print_error("brokers 模块未加载", "")
822
+ return
823
+ broker_id = args.strip()
824
+ reg = _get_broker_registry()
825
+ try:
826
+ broker = reg.get(broker_id) if broker_id else reg.active()
827
+ if not broker:
828
+ broker = await self._auto_connect_broker(broker_id)
829
+ if not broker:
830
+ return
831
+ import asyncio as _aio
832
+ with console.status("[dim]获取持仓...[/dim]", spinner="dots") if HAS_RICH else _null_ctx():
833
+ pos = await _aio.get_event_loop().run_in_executor(None, broker.positions)
834
+ _print_broker_positions(pos, broker.label, broker.config.get("currency","CNY"))
835
+ except Exception as e:
836
+ _print_error(f"持仓查询失败: {e}", "请检查券商连接状态 (/broker status)")
837
+
838
+ async def cmd_orders(self, args: str):
839
+ """显示订单记录。"""
840
+ from aria_cli import (HAS_RICH, console, _null_ctx, _HAS_BROKERS,
841
+ _print_error, _get_broker_registry, _print_broker_orders)
842
+ if not _HAS_BROKERS:
843
+ _print_error("brokers 模块未加载", "")
844
+ return
845
+ parts = args.strip().split()
846
+ status = "all"
847
+ broker_id = ""
848
+ for p in parts:
849
+ if p in ("open", "filled", "cancelled", "all"):
850
+ status = p
851
+ else:
852
+ broker_id = p
853
+ reg = _get_broker_registry()
854
+ try:
855
+ broker = reg.get(broker_id) if broker_id else reg.active()
856
+ if not broker:
857
+ broker = await self._auto_connect_broker(broker_id)
858
+ if not broker:
859
+ return
860
+ import asyncio as _aio
861
+ with console.status("[dim]获取订单...[/dim]", spinner="dots") if HAS_RICH else _null_ctx():
862
+ orders = await _aio.get_event_loop().run_in_executor(
863
+ None, lambda: broker.orders(status=status, limit=30)
864
+ )
865
+ _print_broker_orders(orders, broker.label, status)
866
+ except Exception as e:
867
+ _print_error(f"订单查询失败: {e}", "请检查券商连接状态 (/broker status)")
868
+
869
+ async def cmd_paper(self, args: str):
870
+ """本地仿盘账户: /paper start [cash] [currency] | account | positions | orders | reset."""
871
+ from aria_cli import HAS_RICH, console, _print_error
872
+ from brokers.config import add_broker_config, get_broker_config, set_default_broker
873
+ from brokers.paper_broker import PAPER_LEDGER_PATH, PaperBroker
874
+
875
+ parts = args.strip().split()
876
+ sub = parts[0].lower() if parts else "account"
877
+ rest = parts[1:]
878
+ broker_id = "paper_main"
879
+ cash = 100000.0
880
+ currency = "USD"
881
+ if rest:
882
+ try:
883
+ cash = float(rest[0])
884
+ except Exception:
885
+ pass
886
+ if len(rest) > 1:
887
+ currency = rest[1].upper()
888
+
889
+ if sub in ("start", "init", "reset"):
890
+ cfg = {
891
+ "id": broker_id,
892
+ "type": "paper",
893
+ "label": "Aria 仿盘账户",
894
+ "mode": "paper",
895
+ "starting_cash": cash,
896
+ "currency": currency,
897
+ "default": True,
898
+ }
899
+ add_broker_config(cfg)
900
+ set_default_broker(broker_id)
901
+ broker = PaperBroker(broker_id, cfg)
902
+ broker.reset(starting_cash=cash, currency=currency)
903
+ if HAS_RICH:
904
+ console.print(
905
+ f"[green]✓[/green] 仿盘账户已初始化: [bold]{currency} {cash:,.2f}[/bold] "
906
+ f"[dim]{PAPER_LEDGER_PATH}[/dim]"
907
+ )
908
+ else:
909
+ print(f"仿盘账户已初始化: {currency} {cash:,.2f} {PAPER_LEDGER_PATH}")
910
+ return
911
+
912
+ cfg = get_broker_config(broker_id)
913
+ if not cfg:
914
+ _print_error("尚未创建仿盘账户", "运行 /paper start 100000 USD")
915
+ return
916
+ broker = PaperBroker(broker_id, cfg)
917
+ broker.connect()
918
+ if sub in ("account", "status"):
919
+ from aria_cli import _print_broker_account
920
+ _print_broker_account(broker.account_info())
921
+ elif sub in ("positions", "pos"):
922
+ from aria_cli import _print_broker_positions
923
+ _print_broker_positions(broker.positions(), broker.label, broker.currency)
924
+ elif sub in ("orders", "order"):
925
+ from aria_cli import _print_broker_orders
926
+ _print_broker_orders(broker.orders(limit=30), broker.label, "all")
927
+ else:
928
+ if HAS_RICH:
929
+ console.print("[dim]用法: /paper start [cash] [currency] | account | positions | orders | reset[/dim]")
930
+ else:
931
+ print("Usage: /paper start [cash] [currency] | account | positions | orders | reset")
932
+
933
+ async def cmd_trade(self, args: str):
934
+ """两阶段交易: /trade mode | preview SYMBOL buy|sell QTY PRICE | confirm PREVIEW_ID | previews."""
935
+ from aria_cli import HAS_RICH, console, _print_error, _get_broker_registry
936
+ from brokers import (
937
+ OrderIntent, build_order_preview, execute_order_preview,
938
+ list_order_previews, policy_from_config,
939
+ )
940
+
941
+ parts = args.strip().split()
942
+ sub = parts[0].lower() if parts else "mode"
943
+ reg = _get_broker_registry()
944
+ broker = reg.active() if reg else None
945
+ if not broker:
946
+ try:
947
+ broker = reg.connect_default() if reg else None
948
+ except Exception as exc:
949
+ _print_error(f"无法连接默认账户: {exc}", "先运行 /paper start 或 /broker connect <id>")
950
+ return
951
+
952
+ if sub == "mode":
953
+ if not broker:
954
+ _print_error("无活跃账户", "先运行 /paper start 或 /broker connect <id>")
955
+ return
956
+ policy = policy_from_config(getattr(broker, "config", {}) or {}, getattr(broker, "broker_type", ""))
957
+ msg = (
958
+ f"账户: {broker.label} ({broker.broker_type})\n"
959
+ f"模式: {policy.mode}\n"
960
+ f"实盘允许: {policy.allow_live_trade}\n"
961
+ f"确认要求: {policy.require_confirm}\n"
962
+ f"单笔上限: {policy.max_order_value_weight:.1%} 单票仓位上限: {policy.max_single_position_weight:.1%}"
963
+ )
964
+ if HAS_RICH:
965
+ from aria_cli import Panel, rich_box
966
+ color = "red" if policy.mode == "live" else "green" if policy.mode == "paper" else "yellow"
967
+ console.print(Panel(msg, title="[bold]Trade Mode[/bold]", border_style=color, box=rich_box.ROUNDED))
968
+ else:
969
+ print(msg)
970
+ return
971
+
972
+ if sub in ("previews", "list"):
973
+ rows = list_order_previews(limit=10)
974
+ if HAS_RICH:
975
+ from rich.table import Table
976
+ from aria_cli import rich_box
977
+ tbl = Table(title="[bold]Trade Previews[/bold]", box=rich_box.ROUNDED, border_style="dim")
978
+ tbl.add_column("ID")
979
+ tbl.add_column("Mode")
980
+ tbl.add_column("Broker")
981
+ tbl.add_column("Order")
982
+ tbl.add_column("Status")
983
+ tbl.add_column("Can Exec")
984
+ for row in rows:
985
+ intent = row.get("intent") or {}
986
+ tbl.add_row(
987
+ row.get("preview_id", ""),
988
+ row.get("mode", ""),
989
+ row.get("broker_label", ""),
990
+ f"{intent.get('side','')} {intent.get('symbol','')} x {intent.get('quantity','')}",
991
+ row.get("status", ""),
992
+ "yes" if row.get("can_execute") else "no",
993
+ )
994
+ console.print(tbl)
995
+ else:
996
+ for row in rows:
997
+ print(row)
998
+ return
999
+
1000
+ if sub == "confirm":
1001
+ if not broker:
1002
+ _print_error("无活跃账户", "先运行 /paper start 或 /broker connect <id>")
1003
+ return
1004
+ preview_id = parts[1] if len(parts) > 1 else ""
1005
+ if not preview_id:
1006
+ _print_error("请提供 preview_id", "/trade confirm <preview_id>")
1007
+ return
1008
+ result = execute_order_preview(broker, preview_id, confirmed=True)
1009
+ if result.get("success"):
1010
+ if HAS_RICH:
1011
+ console.print(
1012
+ f"[green]✓[/green] 订单已执行 [{result.get('mode')}] "
1013
+ f"{result.get('side')} {result.get('symbol')} x {result.get('qty')} "
1014
+ f"[dim]#{result.get('order_id')}[/dim]"
1015
+ )
1016
+ else:
1017
+ print(result)
1018
+ else:
1019
+ _print_error(
1020
+ "订单未执行",
1021
+ "; ".join(result.get("execution_blockers") or [result.get("error", "unknown")]),
1022
+ )
1023
+ return
1024
+
1025
+ if sub == "preview":
1026
+ order_parts = parts[1:]
1027
+ else:
1028
+ order_parts = parts
1029
+ if len(order_parts) < 4:
1030
+ _print_error("用法: /trade preview SYMBOL buy|sell QTY PRICE", "/trade preview AAPL buy 10 190")
1031
+ return
1032
+ if not broker:
1033
+ _print_error("无活跃账户", "先运行 /paper start 或 /broker connect <id>")
1034
+ return
1035
+ symbol, side, qty_raw, price_raw = order_parts[:4]
1036
+ try:
1037
+ qty = float(qty_raw)
1038
+ price = float(price_raw)
1039
+ except Exception:
1040
+ _print_error("数量和价格必须是数字", "/trade preview AAPL buy 10 190")
1041
+ return
1042
+ if side.lower() not in ("buy", "sell"):
1043
+ _print_error("side 必须是 buy 或 sell", "/trade preview AAPL buy 10 190")
1044
+ return
1045
+ preview = build_order_preview(
1046
+ broker,
1047
+ OrderIntent(
1048
+ symbol=symbol,
1049
+ side=side,
1050
+ quantity=qty,
1051
+ price=price,
1052
+ order_type="limit",
1053
+ source="slash_trade",
1054
+ ),
1055
+ )
1056
+ blockers = preview.get("execution_blockers") or []
1057
+ if HAS_RICH:
1058
+ from aria_cli import Panel, rich_box
1059
+ status = "可执行" if preview.get("can_execute") else "不可执行"
1060
+ body = (
1061
+ f"preview_id: [bold]{preview.get('preview_id')}[/bold]\n"
1062
+ f"模式: {preview.get('mode')} 账户: {preview.get('broker_label')}\n"
1063
+ f"订单: {side.upper()} {symbol.upper()} x {qty:g} @ {price:g}\n"
1064
+ f"状态: {status}\n"
1065
+ )
1066
+ if blockers:
1067
+ body += "\n执行限制:\n" + "\n".join(f" - {b}" for b in blockers)
1068
+ body += "\n\n确认执行: /trade confirm " + str(preview.get("preview_id"))
1069
+ console.print(Panel(body, title="[bold]Trade Preview[/bold]", border_style="yellow", box=rich_box.ROUNDED))
1070
+ else:
1071
+ print(preview)
1072
+
1073
+ async def _prompt_no_broker_action(self) -> None:
1074
+ """未配置券商时显示可导航的操作菜单,选择后直接路由到对应功能。"""
1075
+ from aria_cli import (HAS_RICH, console, Panel, rich_box, _BROKERS_CONFIG_PATH)
1076
+ from ui.picker import arrow_select
1077
+ import subprocess, sys as _sys
1078
+
1079
+ if HAS_RICH:
1080
+ console.print(Panel(
1081
+ "[yellow]尚未配置任何券商[/yellow] — 请选择下一步操作:",
1082
+ border_style="yellow", box=rich_box.ROUNDED, padding=(0, 1),
1083
+ ))
1084
+
1085
+ actions = [
1086
+ (" 添加新券商", "交互式向导:选择券商 → 开户指引 → 填写凭证 → 一键连接"),
1087
+ (" 手动编辑配置文件", f"用系统编辑器打开 {_BROKERS_CONFIG_PATH}"),
1088
+ (" 查看所有配置模板", "输出全部券商的 JSON 模板供参考"),
1089
+ (" 暂时跳过", "关闭此菜单,稍后再配置"),
1090
+ ]
1091
+ idx = _arrow_select(actions, selected=0, title="", max_visible=6)
1092
+
1093
+ if idx == 0:
1094
+ await self._cmd_broker_add("")
1095
+ elif idx == 1:
1096
+ try:
1097
+ import pathlib as _pl, json as _json
1098
+ from brokers.config import print_all_templates
1099
+ path = _pl.Path(str(_BROKERS_CONFIG_PATH or
1100
+ _pl.Path.home() / ".arthera" / "brokers.json"))
1101
+ path.parent.mkdir(parents=True, exist_ok=True)
1102
+
1103
+ # Pre-populate with full commented template if file is empty/missing
1104
+ needs_template = (
1105
+ not path.exists()
1106
+ or path.stat().st_size < 20
1107
+ or path.read_text(encoding="utf-8").strip() in ('', '{"brokers": []}')
1108
+ )
1109
+ if needs_template:
1110
+ path.write_text(print_all_templates(), encoding="utf-8")
1111
+
1112
+ subprocess.Popen(["open", str(path)])
1113
+
1114
+ if HAS_RICH:
1115
+ from rich.syntax import Syntax
1116
+ from ui.render.output import display_path as _display_path
1117
+ console.print()
1118
+ console.print(Panel(
1119
+ f"[bold]已在编辑器中打开:[/bold] {_display_path(path, fallback='config')}\n\n"
1120
+ f"[dim]文件已预填所有券商模板。\n"
1121
+ f"删除不需要的券商块,填写你的实际凭证后保存。[/dim]\n\n"
1122
+ f"[dim]保存后回到此终端,运行:[/dim]\n"
1123
+ f" [bold]/broker connect <id>[/bold] 建立连接\n"
1124
+ f" [bold]/broker list[/bold] 查看配置状态",
1125
+ title="[bold]手动编辑配置[/bold]",
1126
+ border_style="dim", box=rich_box.ROUNDED, padding=(0, 1),
1127
+ ))
1128
+ except Exception as exc:
1129
+ if HAS_RICH:
1130
+ console.print(f"[dim]配置文件路径: {_BROKERS_CONFIG_PATH}[/dim]")
1131
+ console.print(f"[red]打开失败: {exc}[/red]")
1132
+ elif idx == 2:
1133
+ await self._cmd_broker_init()
1134
+ # idx == 3 or -1 → do nothing (skip)
1135
+
1136
+ async def _auto_connect_broker(self, broker_id: str):
1137
+ """尝试自动连接;无配置时弹出操作菜单。"""
1138
+ from aria_cli import (HAS_RICH, _print_error,
1139
+ _get_broker_registry, _list_broker_configs)
1140
+ reg = _get_broker_registry()
1141
+ cfgs = _list_broker_configs()
1142
+ if not cfgs:
1143
+ await self._prompt_no_broker_action()
1144
+ return None
1145
+ target_id = broker_id or (cfgs[0].get("id", "") if cfgs else "")
1146
+ if not target_id:
1147
+ return None
1148
+ try:
1149
+ import asyncio as _aio
1150
+ return await _aio.get_event_loop().run_in_executor(None, reg.connect, target_id)
1151
+ except Exception as e:
1152
+ _print_error(f"自动连接 {target_id} 失败: {e}",
1153
+ "请运行 /broker connect <id> 手动连接")
1154
+ return None