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,962 @@
1
+ """
2
+ PortfolioCommandsMixin — Portfolio commands: journal, report, portfolio, apply_plan, team.
3
+
4
+ Extracted from aria_cli.py. Methods' __globals__ are rebound to aria_cli's namespace
5
+ by _rebind_mixin_globals() called at module load time.
6
+ """
7
+ from __future__ import annotations
8
+
9
+
10
+ def _detect_lang_for_team(text: str) -> str:
11
+ if not text:
12
+ return "zh"
13
+ zh_chars = sum(1 for c in text if '一' <= c <= '鿿')
14
+ return "zh" if zh_chars / max(len(text), 1) > 0.15 else "en"
15
+
16
+
17
+ class PortfolioCommandsMixin:
18
+ """Mixin: Portfolio commands: journal, report, portfolio, apply_plan, team."""
19
+
20
+ async def cmd_journal(self, args: str):
21
+ """
22
+ 本地持仓账本(SQLite,~/.arthera/portfolio.db)
23
+ Usage:
24
+ /journal → 当前持仓
25
+ /journal add buy AAPL 100 185.50 [理由]
26
+ /journal add sell AAPL 50 200.00 [理由]
27
+ /journal trades [SYMBOL] → 交易记录
28
+ /journal pnl → 含实时报价的未实现盈亏
29
+ /journal realized → 已实现盈亏(FIFO)
30
+ /journal export → 导出 CSV 到桌面
31
+ /journal delete <id> → 删除指定记录
32
+ """
33
+ try:
34
+ from portfolio_ledger import PortfolioLedger as _PL
35
+ except ImportError:
36
+ msg = "portfolio_ledger 模块未找到"
37
+ console.print(f"[red]{msg}[/red]") if HAS_RICH else print(msg)
38
+ return
39
+
40
+ ledger = _PL()
41
+ parts = args.strip().split() if args.strip() else []
42
+ sub = parts[0].lower() if parts else "positions"
43
+
44
+ # ── add buy/sell ─────────────────────────────────────────────────────
45
+ if sub == "add":
46
+ # /journal add buy AAPL 100 185.50 [reason...]
47
+ if len(parts) < 5:
48
+ usage = "用法: /journal add <buy|sell> <symbol> <qty> <price> [理由]"
49
+ console.print(f"[yellow]{usage}[/yellow]") if HAS_RICH else print(usage)
50
+ return
51
+ try:
52
+ side = parts[1].upper()
53
+ symbol = parts[2].upper()
54
+ qty = float(parts[3])
55
+ price = float(parts[4])
56
+ reason = " ".join(parts[5:]) if len(parts) > 5 else ""
57
+ tid = ledger.add_trade(symbol, side, qty, price, reason=reason)
58
+ amount = round(qty * price, 2)
59
+ msg = (f"✓ 已记录: #{tid} {side} {symbol} × {qty} @ {price}"
60
+ f" 总额 {amount:,.2f} {reason}")
61
+ console.print(f"[green]{msg}[/green]") if HAS_RICH else print(msg)
62
+ except Exception as e:
63
+ console.print(f"[red]记录失败: {e}[/red]") if HAS_RICH else print(f"记录失败: {e}")
64
+ return
65
+
66
+ # ── delete ───────────────────────────────────────────────────────────
67
+ if sub == "delete" and len(parts) >= 2:
68
+ try:
69
+ tid = int(parts[1])
70
+ ok = ledger.delete_trade(tid)
71
+ msg = f"✓ 已删除记录 #{tid}" if ok else f"未找到记录 #{tid}"
72
+ console.print(f"[{'green' if ok else 'yellow'}]{msg}[/{'green' if ok else 'yellow'}]") if HAS_RICH else print(msg)
73
+ except Exception as e:
74
+ console.print(f"[red]删除失败: {e}[/red]") if HAS_RICH else print(f"删除失败: {e}")
75
+ return
76
+
77
+ # ── trades history ───────────────────────────────────────────────────
78
+ if sub == "trades":
79
+ sym = parts[1].upper() if len(parts) > 1 else None
80
+ trades = ledger.get_trades(symbol=sym, limit=30)
81
+ title = f"交易记录{f' — {sym}' if sym else ''} (最近 {len(trades)} 条)"
82
+ if HAS_RICH:
83
+ from rich.table import Table
84
+ tbl = Table(title=title, box=None, show_header=True, header_style="bold")
85
+ tbl.add_column("#", style="dim", width=4)
86
+ tbl.add_column("日期", width=10)
87
+ tbl.add_column("方向", width=5)
88
+ tbl.add_column("标的", width=8)
89
+ tbl.add_column("数量", justify="right", width=10)
90
+ tbl.add_column("价格", justify="right", width=10)
91
+ tbl.add_column("总额", justify="right", width=12)
92
+ tbl.add_column("理由", width=20)
93
+ for t in trades:
94
+ side_color = "green" if t["side"] == "BUY" else "red"
95
+ tbl.add_row(
96
+ str(t["id"]),
97
+ t["date"],
98
+ f"[{side_color}]{t['side']}[/{side_color}]",
99
+ t["symbol"],
100
+ f"{t['qty']:,.4g}",
101
+ f"{t['price']:,.4f}",
102
+ f"{t['amount']:,.2f}",
103
+ (t["reason"] or "")[:18],
104
+ )
105
+ console.print(tbl)
106
+ if not trades:
107
+ console.print("[dim]无交易记录[/dim]")
108
+ else:
109
+ print(title)
110
+ for t in trades:
111
+ print(f" #{t['id']} {t['date']} {t['side']} {t['symbol']} "
112
+ f"× {t['qty']} @ {t['price']} {t['reason']}")
113
+ return
114
+
115
+ # ── export ───────────────────────────────────────────────────────────
116
+ if sub == "export":
117
+ try:
118
+ out = ledger.export_csv()
119
+ msg = f"✓ 已导出 {ledger.trade_count()} 条记录 → {out}"
120
+ console.print(f"[green]{msg}[/green]") if HAS_RICH else print(msg)
121
+ except Exception as e:
122
+ console.print(f"[red]导出失败: {e}[/red]") if HAS_RICH else print(f"导出失败: {e}")
123
+ return
124
+
125
+ # ── realized P&L ─────────────────────────────────────────────────────
126
+ if sub == "realized":
127
+ rows = ledger.get_realized_pnl()
128
+ if HAS_RICH:
129
+ from rich.table import Table
130
+ tbl = Table(title="已实现盈亏(FIFO)", box=None, header_style="bold")
131
+ tbl.add_column("标的", width=8)
132
+ tbl.add_column("已实现盈亏", justify="right", width=14)
133
+ tbl.add_column("剩余持仓", justify="right", width=10)
134
+ for r in rows:
135
+ pnl = r["realized_pnl"]
136
+ color = "green" if pnl >= 0 else "red"
137
+ tbl.add_row(
138
+ r["symbol"],
139
+ f"[{color}]{pnl:+,.2f}[/{color}]",
140
+ f"{r['open_lots']:,.4g}" if r["has_open"] else "已平仓",
141
+ )
142
+ console.print(tbl)
143
+ total = sum(r["realized_pnl"] for r in rows)
144
+ tc = "green" if total >= 0 else "red"
145
+ console.print(f" [bold]合计已实现盈亏: [{tc}]{total:+,.2f}[/{tc}][/bold]")
146
+ else:
147
+ for r in rows:
148
+ print(f" {r['symbol']}: {r['realized_pnl']:+,.2f}")
149
+ return
150
+
151
+ # ── pnl with live prices ──────────────────────────────────────────────
152
+ if sub == "pnl":
153
+ positions = ledger.get_positions()
154
+ if not positions:
155
+ console.print("[dim]暂无持仓记录。用 /journal add buy … 添加。[/dim]") if HAS_RICH else print("暂无持仓")
156
+ return
157
+ # fetch live prices via yfinance
158
+ live_prices: dict = {}
159
+ syms = [p["symbol"] for p in positions]
160
+ if HAS_RICH:
161
+ console.print(f" [dim]获取 {len(syms)} 只股票实时报价…[/dim]")
162
+ try:
163
+ import yfinance as yf
164
+ for sym in syms:
165
+ try:
166
+ h = yf.Ticker(sym).history(period="1d")
167
+ if not h.empty:
168
+ live_prices[sym] = float(h["Close"].iloc[-1])
169
+ except Exception:
170
+ pass
171
+ except ImportError:
172
+ pass
173
+ rows = ledger.get_pnl_with_prices(live_prices)
174
+ if HAS_RICH:
175
+ from rich.table import Table
176
+ tbl = Table(title="持仓盈亏", box=None, header_style="bold")
177
+ tbl.add_column("标的", width=8)
178
+ tbl.add_column("数量", justify="right", width=10)
179
+ tbl.add_column("均价", justify="right", width=10)
180
+ tbl.add_column("现价", justify="right", width=10)
181
+ tbl.add_column("市值", justify="right", width=12)
182
+ tbl.add_column("未实现盈亏", justify="right", width=14)
183
+ tbl.add_column("涨跌%", justify="right", width=8)
184
+ for r in rows:
185
+ has_price = "current_price" in r
186
+ pnl = r.get("unrealized_pnl", "")
187
+ pct = r.get("unrealized_pct", "")
188
+ color = ("green" if isinstance(pnl, (int, float)) and pnl >= 0 else "red") if has_price else "dim"
189
+ tbl.add_row(
190
+ r["symbol"],
191
+ f"{r['net_qty']:,.4g}",
192
+ f"{r['avg_cost']:,.4f}",
193
+ f"{r.get('current_price', 'N/A'):,.4f}" if has_price else "N/A",
194
+ f"{r.get('market_value', ''):,.2f}" if has_price else "N/A",
195
+ f"[{color}]{pnl:+,.2f}[/{color}]" if has_price else "—",
196
+ f"[{color}]{pct:+.2f}%[/{color}]" if has_price else "—",
197
+ )
198
+ console.print(tbl)
199
+ total_pnl = sum(r.get("unrealized_pnl", 0) for r in rows if "unrealized_pnl" in r)
200
+ total_mv = sum(r.get("market_value", 0) for r in rows if "market_value" in r)
201
+ total_cost = sum(r["cost_basis"] for r in rows)
202
+ tc = "green" if total_pnl >= 0 else "red"
203
+ console.print(
204
+ f" [bold]总持仓成本 {total_cost:,.2f} "
205
+ f"总市值 {total_mv:,.2f} "
206
+ f"未实现盈亏 [{tc}]{total_pnl:+,.2f}[/{tc}][/bold]"
207
+ )
208
+ # Portfolio status banner
209
+ _pnl_pct = (total_pnl / total_cost * 100) if total_cost else 0
210
+ _pnl_verdict = "HEALTHY" if _pnl_pct >= 0 else ("NEEDS_ATTENTION" if _pnl_pct >= -10 else "HIGH_RISK")
211
+ _pnl_sub = f"总盈亏 {total_pnl:+,.2f} ({_pnl_pct:+.1f}%)"
212
+ _print_verdict_banner(_pnl_verdict, subtitle=_pnl_sub)
213
+ else:
214
+ for r in rows:
215
+ pnl = r.get("unrealized_pnl", "N/A")
216
+ print(f" {r['symbol']}: {r['net_qty']} × avg {r['avg_cost']} pnl {pnl}")
217
+ return
218
+
219
+ # ── default: positions ────────────────────────────────────────────────
220
+ positions = ledger.get_positions()
221
+ if not positions:
222
+ hint = "暂无持仓记录。\n 添加示例: /journal add buy AAPL 100 185.50 首次建仓"
223
+ console.print(f"[dim]{hint}[/dim]") if HAS_RICH else print(hint)
224
+ return
225
+ if HAS_RICH:
226
+ from rich.table import Table
227
+ tbl = Table(
228
+ title=f"当前持仓({len(positions)} 只,共 {ledger.trade_count()} 条交易)",
229
+ box=None, header_style="bold",
230
+ )
231
+ tbl.add_column("标的", width=8)
232
+ tbl.add_column("持仓量", justify="right", width=12)
233
+ tbl.add_column("均价成本", justify="right", width=12)
234
+ tbl.add_column("持仓成本", justify="right", width=14)
235
+ tbl.add_column("首次建仓", width=12)
236
+ for pos in positions:
237
+ tbl.add_row(
238
+ pos["symbol"],
239
+ f"{pos['net_qty']:,.4g}",
240
+ f"{pos['avg_cost']:,.4f}",
241
+ f"{pos['cost_basis']:,.2f}",
242
+ pos.get("first_trade", ""),
243
+ )
244
+ console.print(tbl)
245
+ console.print(
246
+ f" [dim]更多命令: /journal pnl | /journal trades | "
247
+ f"/journal realized | /journal export[/dim]"
248
+ )
249
+ else:
250
+ print(f"当前持仓 ({len(positions)} 只):")
251
+ for pos in positions:
252
+ print(f" {pos['symbol']}: {pos['net_qty']} 股 均价 {pos['avg_cost']}")
253
+
254
+ async def cmd_report(self, args: str):
255
+ """生成综合投资报告(图表 + 多 Agent 分析 → HTML / Markdown 文件)。
256
+
257
+ Usage:
258
+ /report AAPL
259
+ /report 000333
260
+ /report AAPL --format md # Markdown 投研报告(离线可用)
261
+ /report AAPL --type deep # 深度研报(8页)
262
+ /report AAPL --type brief # 简评(1页)
263
+ /report AAPL --pdf # 同时导出 PDF(需 weasyprint 或 wkhtmltopdf)
264
+ """
265
+ from datetime import datetime as _dt
266
+
267
+ report_args = parse_report_args(args)
268
+ symbol = report_args.symbol
269
+ fmt = report_args.fmt
270
+ report_type = report_args.report_type
271
+ export_pdf_flag = report_args.export_pdf
272
+ out_dir = report_args.output_dir
273
+ if out_dir:
274
+ out_dir.mkdir(parents=True, exist_ok=True)
275
+ ts = _dt.now().strftime("%Y%m%d_%H%M")
276
+
277
+ # ── Markdown report mode (works fully offline) ────────────────────────
278
+ if fmt in ("md", "markdown"):
279
+ console.print(f"\n 📄 生成 [bold]{symbol}[/bold] Markdown 投研报告 ({report_type})...") if HAS_RICH else print(f"\n Generating {symbol} Markdown report...")
280
+
281
+ # Fetch real data through the service boundary so provenance and
282
+ # quality metadata travel with the report prompt and artifact.
283
+ mdc_data = {}
284
+ data_bundle = None
285
+ data_quality = {}
286
+ try:
287
+ from packages.aria_services.data import DataService as _ReportDataService
288
+ data_bundle = await asyncio.get_event_loop().run_in_executor(
289
+ None,
290
+ lambda: _ReportDataService().bundle(symbol, history_days=370, technical_days=120),
291
+ )
292
+ quote = data_bundle.quote or {}
293
+ technical = data_bundle.technical or {}
294
+ mdc_data = {**quote, **technical}
295
+ data_quality = data_bundle.quality or {}
296
+ except Exception as _ds_exc:
297
+ logger.debug("report markdown data service failed: %s", _ds_exc)
298
+ if _HAS_MDC:
299
+ try:
300
+ mdc = _get_mdc()
301
+ q = mdc.quote(symbol)
302
+ ti = mdc.technical_indicators(symbol, days=120)
303
+ mdc_data = {**q, **ti}
304
+ data_quality = {
305
+ "status": "partial",
306
+ "stale": False,
307
+ "providers": mdc_data.get("provider_chain") or list(dict.fromkeys(
308
+ str(v) for v in [mdc_data.get("provider"), mdc_data.get("source")] if v
309
+ )),
310
+ "warnings": [f"data service unavailable: {_ds_exc}"],
311
+ }
312
+ except Exception:
313
+ data_quality = {"status": "data_unavailable", "warnings": [str(_ds_exc)]}
314
+
315
+ ai_prompt = build_markdown_report_prompt(
316
+ symbol=symbol,
317
+ report_type=report_type,
318
+ market_data=mdc_data,
319
+ data_quality=data_quality,
320
+ data_bundle=data_bundle,
321
+ now=_dt.now(),
322
+ )
323
+
324
+ await self.terminal.send_message(ai_prompt)
325
+
326
+ # Extract last AI response and save as markdown
327
+ last_ai = next(
328
+ (m["content"] for m in reversed(self.terminal.conversation)
329
+ if m.get("role") == "assistant"), ""
330
+ )
331
+ if last_ai:
332
+ saved = save_markdown_report(
333
+ symbol=symbol,
334
+ report_type=report_type,
335
+ markdown_text=last_ai,
336
+ timestamp=ts,
337
+ output_dir=out_dir,
338
+ market_data=mdc_data,
339
+ data_quality=data_quality,
340
+ data_bundle=data_bundle,
341
+ created_at=_dt.now(),
342
+ )
343
+ out_f = saved.path
344
+ if HAS_RICH:
345
+ console.print(f"\n [green]✅ 报告已保存: {out_f}[/green]")
346
+ console.print(f" [dim]预览: open {out_f}[/dim]\n")
347
+ else:
348
+ print(f"\n Saved: {out_f}")
349
+ return
350
+
351
+ # ── HTML 研报(Bloomberg 暗色主题)────────────────────────────────
352
+ if HAS_RICH:
353
+ console.print(f"\n [dim]正在生成 [bold]{symbol}[/bold] 专业研报(数据清洗 + 图表 + Agent 分析)…[/dim]")
354
+ else:
355
+ print(f"\n 正在生成 {symbol} 研报…")
356
+
357
+ _agent_names_for_report = report_agent_names(report_type)
358
+ try:
359
+ if HAS_RICH:
360
+ with console.status(
361
+ f"[dim]{len(_agent_names_for_report)} agents 并行分析…[/dim]",
362
+ spinner="dots",
363
+ ):
364
+ _html_report = await generate_html_report(
365
+ symbol=symbol,
366
+ report_type=report_type,
367
+ output_dir=out_dir,
368
+ config=self.terminal.config,
369
+ )
370
+ else:
371
+ _html_report = await generate_html_report(
372
+ symbol=symbol,
373
+ report_type=report_type,
374
+ output_dir=out_dir,
375
+ config=self.terminal.config,
376
+ )
377
+ out_f = _html_report.path
378
+ _team_result = _html_report.team_result
379
+ except Exception as e:
380
+ if HAS_RICH:
381
+ console.print(f" [red]研报生成失败: {e}[/red]")
382
+ else:
383
+ print(f" 研报生成失败: {e}")
384
+ return
385
+
386
+ if not out_f:
387
+ console.print(" [red]研报生成失败(无输出文件)[/red]") if HAS_RICH else print(" 研报生成失败")
388
+ return
389
+
390
+ path = str(out_f)
391
+ from ui.render.output import display_path as _display_path
392
+ path_label = _display_path(out_f, fallback="report")
393
+ _file_kb = report_file_size_kb(out_f)
394
+ # Check if all agents failed — show warning instead of false success
395
+ _all_agents_failed = all_agents_failed(_team_result)
396
+ if HAS_RICH:
397
+ if _all_agents_failed:
398
+ console.print(
399
+ f"\n [yellow]⚠ 研报已保存(所有 Agent 分析失败,内容仅含基础数据)[/yellow]"
400
+ f" [dim]{out_f.name} ({_file_kb}KB)[/dim]"
401
+ )
402
+ else:
403
+ console.print(
404
+ f"\n [green]✅ 研报已保存[/green]"
405
+ f" [link={path}]{path_label}[/link]"
406
+ f" [dim]({_file_kb}KB)[/dim]"
407
+ )
408
+ console.print(f" [dim]文件: {path_label}[/dim]")
409
+ if _team_result:
410
+ _print_verdict_banner(
411
+ _team_result.final_signal,
412
+ subtitle=f"耗时 {_team_result.elapsed_sec:.1f}s · {len(_agent_names_for_report)} agents",
413
+ confidence=_team_result.confidence,
414
+ )
415
+ else:
416
+ _pfx = "⚠ 研报已保存(Agent 全部失败)" if _all_agents_failed else "✅ 研报已保存"
417
+ print(f"\n {_pfx}: {path_label} ({_file_kb}KB)")
418
+
419
+ # ── PDF 导出 ──────────────────────────────────────────────────────────
420
+ if export_pdf_flag:
421
+ try:
422
+ if HAS_RICH:
423
+ with console.status("[dim]导出 PDF…[/dim]", spinner="dots"):
424
+ _pdf_path = await export_report_pdf(out_f)
425
+ else:
426
+ _pdf_path = await export_report_pdf(out_f)
427
+ if _pdf_path:
428
+ _pdf_kb = report_file_size_kb(_pdf_path)
429
+ if HAS_RICH:
430
+ console.print(
431
+ f" [green]PDF 导出成功[/green]"
432
+ f" [link={_pdf_path}]{_pdf_path.name}[/link]"
433
+ f" [dim]({_pdf_kb}KB)[/dim]"
434
+ )
435
+ else:
436
+ print(f" PDF: {_pdf_path} ({_pdf_kb}KB)")
437
+ import subprocess as _subp2
438
+ try:
439
+ _subp2.Popen(["open", str(_pdf_path)])
440
+ except Exception:
441
+ pass
442
+ else:
443
+ _hint = "pip install weasyprint 或 brew install wkhtmltopdf"
444
+ if HAS_RICH:
445
+ console.print(
446
+ f" [yellow]PDF 导出失败[/yellow] "
447
+ f"[dim]请安装: {_hint} 或在浏览器按 Cmd+P → 存储为 PDF[/dim]"
448
+ )
449
+ else:
450
+ print(f" PDF 导出失败,请安装: {_hint}")
451
+ except Exception as _e:
452
+ logger.debug("[report] pdf export error: %s", _e)
453
+
454
+ # ── 更新研报索引 ──────────────────────────────────────────────────────
455
+ try:
456
+ _idx = await update_report_index(out_f.parent)
457
+ if _idx and HAS_RICH:
458
+ console.print(
459
+ f" [dim]索引已更新: [link={_idx}]{_idx.name}[/link][/dim]"
460
+ )
461
+ except Exception as _e:
462
+ logger.debug("[report] index update error: %s", _e)
463
+
464
+ import subprocess as _subp
465
+ try:
466
+ _subp.Popen(["open", path])
467
+ except Exception:
468
+ pass
469
+
470
+ async def cmd_portfolio(self, args: str):
471
+ """
472
+ 组合级跨标的分析(相关性/分散度/风险)
473
+ Usage:
474
+ /portfolio → 分析 watchlist(最多 10 只)
475
+ /portfolio analyze → 同上
476
+ /portfolio analyze AAPL TSLA MSFT
477
+ /portfolio rebalance → 生成再平衡建议(同 analyze,着重操作)
478
+ """
479
+ import sys as _sys
480
+ parts = args.strip().split()
481
+ sub = parts[0].lower() if parts else "analyze"
482
+ sym_parts = parts[1:] if parts else []
483
+ rebalance = (sub == "rebalance")
484
+
485
+ # 解析标的:命令行 > watchlist
486
+ if sym_parts:
487
+ symbols = [s.strip(",").upper() for s in sym_parts if s.strip(",")]
488
+ else:
489
+ symbols = self.terminal.config.get("watchlist", ["AAPL", "MSFT", "GOOGL", "NVDA", "TSLA"])[:10]
490
+
491
+ if not symbols:
492
+ msg = "请先设置 watchlist 或指定标的:/portfolio analyze AAPL TSLA MSFT"
493
+ console.print(f"[yellow]{msg}[/yellow]") if HAS_RICH else print(msg)
494
+ return
495
+
496
+ # 尝试使用新 PortfolioAgent
497
+ _use_new = False
498
+ try:
499
+ from agents.portfolio_agent import PortfolioAgent as _PA
500
+ from providers.llm.registry import get_provider as _get_prov, list_available_providers as _laps
501
+ _use_new = True
502
+ except ImportError:
503
+ pass
504
+
505
+ if _use_new:
506
+ hdr = "分析 watchlist 组合" if not sym_parts else f"分析组合:{' '.join(symbols)}"
507
+ if rebalance:
508
+ hdr = "再平衡方案:" + hdr
509
+ if HAS_RICH:
510
+ console.print()
511
+ console.print(f" [bold cyan]━━━ /portfolio {hdr} ━━━[/bold cyan]")
512
+ console.print(f" [dim]标的 ({len(symbols)}): {', '.join(symbols)}[/dim]")
513
+ console.print()
514
+ else:
515
+ print(f"\n ━━━ /portfolio ━━━\n 标的: {', '.join(symbols)}\n")
516
+
517
+ _llm = None
518
+ try:
519
+ all_avail = [p for p in _laps() if p["available"]]
520
+ chosen = [p for p in all_avail if p.get("local")] or all_avail
521
+ if chosen:
522
+ _llm = _get_prov(chosen[0]["name"])
523
+ except Exception as _e:
524
+ logger.debug("portfolio LLM provider init failed: %s", _e)
525
+
526
+ tokens: list = []
527
+ def _on_tok(t):
528
+ tokens.append(t)
529
+ _sys.stdout.write(t); _sys.stdout.flush()
530
+
531
+ try:
532
+ agent = _PA(llm_provider=_llm, on_token=_on_tok)
533
+ result = await agent.run_portfolio(symbols)
534
+ print() # 换行(流式输出后)
535
+
536
+ if not result:
537
+ if HAS_RICH:
538
+ console.print("[yellow] ⚠ 组合分析返回空结果[/yellow]")
539
+ return
540
+
541
+ if HAS_RICH:
542
+ console.print()
543
+ for pt in (result.key_points or []):
544
+ console.print(f" [dim]• {pt}[/dim]")
545
+ console.print()
546
+ # Derive portfolio verdict from signal for the banner
547
+ _port_verdict = {
548
+ "BUY": "HEALTHY",
549
+ "HOLD": "NEEDS_ATTENTION",
550
+ "SELL": "HIGH_RISK",
551
+ "STRONG_BUY": "HEALTHY",
552
+ "STRONG_SELL":"HIGH_RISK",
553
+ }.get(result.signal.upper() if result.signal else "HOLD", "NEEDS_ATTENTION")
554
+ _subtitle = " · ".join(result.key_points[:2]) if result.key_points else ""
555
+ _print_verdict_banner(_port_verdict, subtitle=_subtitle,
556
+ confidence=result.confidence)
557
+ else:
558
+ for pt in result.key_points:
559
+ print(f" • {pt}")
560
+ print(f"\n 置信度: {result.confidence:.0%} 信号: {result.signal}")
561
+
562
+ if rebalance and HAS_RICH:
563
+ console.print("\n [dim]提示: 再平衡建议已包含在上方分析中。"
564
+ "如需详细方案,可追问 Aria 具体操作步骤。[/dim]")
565
+
566
+ except Exception as e:
567
+ msg = f"组合分析失败: {e}"
568
+ console.print(f" [red]{msg}[/red]") if HAS_RICH else print(f" {msg}")
569
+ return
570
+
571
+ # 旧路径回退(无新 agents 包时)
572
+ if HAS_RICH:
573
+ console.print("[dim]Assessing portfolio risk...[/dim]")
574
+ else:
575
+ print("Assessing portfolio risk...")
576
+ result = await execute_aria_tool(self.terminal.api_url, "assess_portfolio_risk", {
577
+ "symbols": symbols[:10],
578
+ })
579
+ if result.get("success") and result.get("data"):
580
+ if HAS_RICH:
581
+ console.print(f"\n [bold]Portfolio Risk[/bold]\n")
582
+ console.print(f"[dim]{json.dumps(result['data'], indent=2, ensure_ascii=False)[:1000]}[/dim]")
583
+ else:
584
+ print(json.dumps(result.get("data", {}), indent=2, ensure_ascii=False))
585
+ else:
586
+ console.print(f"[dim]No data: {result.get('error', '')}[/dim]" if HAS_RICH
587
+ else f"No data: {result.get('error', '')}")
588
+
589
+ def cmd_apply_plan(self, args: str):
590
+ """Execute the pending command plan sequentially."""
591
+ plan = list(getattr(self.terminal, "pending_plan", []) or [])
592
+ arg_tokens = args.split()
593
+ start_idx = 0
594
+ if "--from" in arg_tokens:
595
+ idx = arg_tokens.index("--from")
596
+ if idx + 1 >= len(arg_tokens):
597
+ msg = "Usage: /apply-plan --from <step_number>"
598
+ console.print(f"[dim]{msg}[/dim]" if HAS_RICH else msg)
599
+ return
600
+ try:
601
+ start_idx = max(0, int(arg_tokens[idx + 1]) - 1)
602
+ except ValueError:
603
+ msg = "Invalid step number for --from"
604
+ console.print(f"[red]{msg}[/red]" if HAS_RICH else msg)
605
+ return
606
+
607
+ if not plan:
608
+ console.print("[dim]No pending plan. Use /plan first.[/dim]" if HAS_RICH
609
+ else "No pending plan. Use /plan first.")
610
+ return
611
+ if start_idx > 0:
612
+ if start_idx >= len(plan):
613
+ msg = f"--from {start_idx + 1} exceeds available steps ({len(plan)})"
614
+ console.print(f"[red]{msg}[/red]" if HAS_RICH else msg)
615
+ return
616
+ plan = plan[start_idx:]
617
+ if "--resume" in arg_tokens and HAS_RICH:
618
+ console.print(f"[dim]Resuming execution from step 1 of remaining {len(plan)} step(s).[/dim]")
619
+
620
+ policy = self.terminal.config.get("command_policy", "safe")
621
+ results = []
622
+ failed = None
623
+ for i, step in enumerate(plan, 1):
624
+ started_at = time.time()
625
+ if HAS_RICH:
626
+ console.print(f"[dim]Step {i}/{len(plan)}:[/dim] [bold]{step}[/bold]")
627
+ else:
628
+ print(f"Step {i}/{len(plan)}: {step}")
629
+
630
+ step_decision = evaluate_command_policy(step, policy)
631
+ if step_decision.risk == "high":
632
+ if not self._confirm_high_risk_command(step_decision.normalized_command, step_decision.risk, policy):
633
+ failed = (i, step, "Cancelled by user at high-risk step confirmation")
634
+ results.append({
635
+ "step": step,
636
+ "status": "blocked",
637
+ "duration": round(time.time() - started_at, 3),
638
+ "exit_code": None,
639
+ "error": failed[2],
640
+ })
641
+ break
642
+
643
+ res = _tool_run_command({"command": step, "policy": policy})
644
+ duration = time.time() - started_at
645
+ exit_code = res.get("data", {}).get("exit_code", None) if res.get("success") else None
646
+ status = "completed" if res.get("success") and exit_code == 0 else "failed"
647
+ results.append({
648
+ "step": step,
649
+ "status": status,
650
+ "duration": round(duration, 3),
651
+ "exit_code": exit_code,
652
+ "error": None if status == "completed" else (res.get("error") or f"Command exited {exit_code}"),
653
+ })
654
+ if not res.get("success"):
655
+ failed = (i, step, res.get("error", "Unknown error"))
656
+ break
657
+ exit_code = res.get("data", {}).get("exit_code", 0)
658
+ if exit_code != 0:
659
+ failed = (i, step, f"Command exited {exit_code}")
660
+ break
661
+
662
+ self.terminal.last_plan_results = results
663
+
664
+ if failed:
665
+ idx, step, err = failed
666
+ self.terminal.pending_plan = plan[idx - 1:]
667
+ if HAS_RICH:
668
+ console.print(f"[red]Plan failed at step {idx}[/red]: [bold]{step}[/bold]")
669
+ console.print(f"[red]{err}[/red]")
670
+ console.print("[dim]Recovery hints:[/dim]")
671
+ if "blocked by policy" in (err or "").lower():
672
+ console.print(" [dim]> /run --dry-run <command> to inspect risk[/dim]")
673
+ console.print(" [dim]> /config set command_policy=balanced (or full) if needed[/dim]")
674
+ else:
675
+ console.print(" [dim]> Fix code/config, then rerun /apply-plan[/dim]")
676
+ console.print(" [dim]> Use /git diff to inspect changes[/dim]")
677
+ else:
678
+ print(f"Plan failed at step {idx}: {step}\n{err}")
679
+ if "blocked by policy" in (err or "").lower():
680
+ print("Recovery: /run --dry-run <command> and /config set command_policy=balanced")
681
+ else:
682
+ print("Recovery: fix issue, then rerun /apply-plan")
683
+ else:
684
+ if HAS_RICH:
685
+ console.print(f"[green]Plan completed ({len(plan)} steps)[/green]")
686
+ for i, row in enumerate(results, 1):
687
+ console.print(f" [dim]{i}. {row['step']} ({row['duration']}s)[/dim]")
688
+ else:
689
+ print(f"Plan completed ({len(plan)} steps)")
690
+ self.terminal.pending_plan = []
691
+
692
+ async def cmd_deep(self, args: str):
693
+ """
694
+ 深度多层研究(Claude-Code 架构 P0–P3):
695
+ 团队并行 → 主题分组 → 工具深挖 → 量化融合+置信度校准 → Critic 自检 → 分级报告
696
+ Usage: /deep NVDA ← 标准档
697
+ /deep AAPL --deep ← 深度档(含量化地面真值/自检/数据血缘)
698
+ /deep 000333 --brief ← 简报档
699
+ /deep TSLA --agents technical,risk,macro
700
+ /deep calibrate ← 用真实价回评历史预测,更新置信度校准
701
+ """
702
+ def _latest_close(symbol: str):
703
+ try:
704
+ import data_cleaner
705
+ df, _ = data_cleaner.get_clean_prices(symbol, period="5d")
706
+ if df is not None and len(df):
707
+ for col in ("close", "Close", "adj_close", "收盘"):
708
+ if col in df.columns:
709
+ return float(df[col].iloc[-1])
710
+ except Exception:
711
+ pass
712
+ return None
713
+
714
+ # /deep calibrate — score logged predictions against realised price (P2 loop)
715
+ if args.strip().lower().startswith(("calibrate", "校准")):
716
+ from agents.deep.calibration_loop import (
717
+ PredictionLog, evaluate_due, evaluate_from_ledger)
718
+ from agents.deep.quant_fusion import CalibrationStore
719
+ store, log = CalibrationStore(), PredictionLog()
720
+ led_res = {"evaluated": 0, "hits": 0}
721
+ try: # actual realised P&L first — the strongest ground truth
722
+ from portfolio_ledger import PortfolioLedger
723
+ led_res = evaluate_from_ledger(store, log, PortfolioLedger())
724
+ except Exception:
725
+ pass
726
+ px_res = evaluate_due(store, log, _latest_close) # market price for the rest
727
+ total = led_res["evaluated"] + px_res["evaluated"]
728
+ hits = led_res["hits"] + px_res["hits"]
729
+ if total:
730
+ msg = (f"校准完成:评估 {total} 条(实盘 {led_res['evaluated']} + "
731
+ f"市价 {px_res['evaluated']}),命中 {hits},"
732
+ f"命中率 {hits / total:.0%}(置信度校准已更新)")
733
+ else:
734
+ msg = "暂无到期预测可校准(先用 /deep 跑几次分析积累预测)。"
735
+ console.print(f"[green]✓[/green] {msg}") if HAS_RICH else print(msg)
736
+ return
737
+
738
+ team_args = parse_team_args(args)
739
+ symbols = resolve_team_symbols(team_args, self.terminal.config)
740
+ agent_names = team_agent_names(team_args)
741
+ _low = args.lower()
742
+ tier = ("deep" if ("--deep" in _low or "--full" in _low)
743
+ else "brief" if "--brief" in _low else "standard")
744
+ _zh = sum(1 for c in args if '一' <= c <= '鿿')
745
+ _lang = "zh" if _zh / max(len(args), 1) > 0.15 else "en"
746
+
747
+ from agents.deep.tiers import render_tier
748
+ from ui.render.team import render_agent_tree_root, render_agent_node
749
+
750
+ for sym in symbols:
751
+ def _on_agent_done(name, result):
752
+ _kps = getattr(result, "key_points", None)
753
+ _kp = (_kps[0] if isinstance(_kps, (list, tuple)) and _kps else "")
754
+ if HAS_RICH:
755
+ render_agent_node(
756
+ console, name, getattr(result, "signal", None), _kp,
757
+ success=bool(getattr(result, "success", True)),
758
+ error=getattr(result, "error", None),
759
+ )
760
+ else:
761
+ print(f" ⎿ {name} {getattr(result, 'signal', '')} {_kp[:50]}")
762
+
763
+ if HAS_RICH:
764
+ render_agent_tree_root(console, sym, len(agent_names), lang=_lang)
765
+ else:
766
+ print(f"\n ⏺ 深度分析 {sym} {len(agent_names)} 个分析师")
767
+
768
+ try:
769
+ result = await run_deep_cli(
770
+ symbol=sym, args=team_args, config=self.terminal.config,
771
+ lang=_lang, on_agent_done=_on_agent_done,
772
+ )
773
+ except Exception as e:
774
+ _print_error(str(e), "deep")
775
+ continue
776
+
777
+ md = render_tier(result, tier)
778
+ if HAS_RICH:
779
+ from rich import box as _box
780
+ from rich.markdown import Markdown
781
+ from rich.panel import Panel
782
+ console.print(Panel(
783
+ Markdown(md), border_style="dim", box=_box.ROUNDED,
784
+ title=f"[bold]深度研究 · {sym}[/bold] [dim]({tier})[/dim]",
785
+ title_align="left", padding=(1, 2),
786
+ ))
787
+ else:
788
+ print("\n" + md)
789
+
790
+ # P2 closed loop: log the verdict so /deep calibrate can score it later
791
+ try:
792
+ from agents.deep.calibration_loop import PredictionLog
793
+ _p = _latest_close(sym)
794
+ if _p and result.final_signal:
795
+ PredictionLog().log(sym, result.final_signal,
796
+ result.calibrated_confidence, _p)
797
+ except Exception:
798
+ pass
799
+
800
+ async def cmd_team(self, args: str):
801
+ """
802
+ 多 Agent 金融研究团队:宏观 + 基本面 + 技术 + 风控 → 综合报告
803
+ Usage: /team NVDA
804
+ /team 000333 --agents technical,risk
805
+ /team watchlist
806
+ /team AAPL --full ← 7-agent 完整模式(+新闻/催化剂/行业)
807
+ """
808
+ import sys as _sys
809
+ team_args = parse_team_args(args)
810
+ symbols = resolve_team_symbols(team_args, self.terminal.config)
811
+ agent_names = team_agent_names(team_args)
812
+ _zh = sum(1 for c in args if '一' <= c <= '鿿')
813
+ _lang = "zh" if _zh / max(len(args), 1) > 0.15 else "en"
814
+
815
+ for sym in symbols:
816
+ _agent_count = len(agent_names)
817
+
818
+ # ── Streaming nested agent tree (Claude Code-style) ──────────────
819
+ from ui.render.team import (
820
+ render_agent_tree_root, render_agent_node,
821
+ render_agent_synthesis_leaf,
822
+ )
823
+
824
+ def _on_agent_done(name, result):
825
+ # Fires as each analyst finishes — render its leaf live.
826
+ _kp = ""
827
+ _kps = getattr(result, "key_points", None)
828
+ if _kps:
829
+ _kp = _kps[0] if isinstance(_kps, (list, tuple)) else str(_kps)
830
+ if HAS_RICH:
831
+ render_agent_node(
832
+ console, name,
833
+ getattr(result, "signal", None), _kp,
834
+ success=bool(getattr(result, "success", True)),
835
+ error=getattr(result, "error", None),
836
+ )
837
+ else:
838
+ print(f" ⎿ {name} {getattr(result, 'signal', '')} {_kp[:50]}")
839
+
840
+ if HAS_RICH:
841
+ render_agent_tree_root(console, sym, _agent_count, lang=_lang)
842
+ else:
843
+ print(f"\n ⏺ 多代理分析 {sym} {_agent_count} 个分析师并行")
844
+
845
+ try:
846
+ # ── 新 Agent 系统(无 Ollama 依赖)────────────────────────
847
+ _analysis = await run_team_analysis(
848
+ symbol=sym,
849
+ args=team_args,
850
+ config=self.terminal.config,
851
+ sanitize_result=_sanitize_team_result_with_market_data,
852
+ lang=_lang,
853
+ on_agent_done=_on_agent_done,
854
+ )
855
+
856
+ team_result = _analysis.team_result
857
+ _data_bundle = _analysis.data_bundle
858
+ _quality_notes = _analysis.quality_notes or []
859
+
860
+ if HAS_RICH:
861
+ # Synthesis leaf closes the tree, then the detailed Panel
862
+ render_agent_synthesis_leaf(
863
+ console,
864
+ team_result.final_signal,
865
+ team_result.confidence,
866
+ team_result.elapsed_sec,
867
+ lang=_lang,
868
+ )
869
+ if _quality_notes:
870
+ console.print(
871
+ " [yellow]数据质量警告:[/yellow] "
872
+ + "; ".join(_quality_notes[:3])
873
+ )
874
+
875
+ # Signal divergence notice — only when DebateAgent ran
876
+ _has_debate = any(
877
+ getattr(r, "agent", "") == "debate"
878
+ for r in (team_result.results or [])
879
+ )
880
+ if _has_debate:
881
+ console.print(
882
+ " [#C08050]🔥 信号分歧已触发 DebateAgent 调解[/#C08050]"
883
+ )
884
+
885
+ # Synthesis in a Panel for visual separation
886
+ from rich import box as _rbox_team
887
+ from ui.render.team import SIGNAL_COLORS as _SC, VERDICT_STYLE as _VS
888
+ from apps.cli.commands.team import (
889
+ build_team_terminal_summary as _team_terminal_summary,
890
+ clean_team_synthesis_text as _clean_team_synthesis,
891
+ )
892
+ _syn = _clean_team_synthesis(team_result.synthesis or "*(无综合结论)*")
893
+ _market_summary = _team_terminal_summary(_data_bundle)
894
+ _elapsed = f" [dim]耗时 {team_result.elapsed_sec:.1f}s[/dim]"
895
+ _sig_str = team_result.final_signal or ""
896
+ _conf_str = (f" [dim]置信度 {team_result.confidence:.0%}[/dim]"
897
+ if team_result.confidence else "")
898
+ _sig_color = _SC.get(_sig_str.upper(), "dim")
899
+ _sig_icon = _VS.get(_sig_str.upper(), ("dim", "●"))[1]
900
+ _footer = (f"[{_sig_color}]{_sig_icon} {_sig_str}[/{_sig_color}]"
901
+ f"{_conf_str}{_elapsed}")
902
+ console.print(Panel(
903
+ f"{_market_summary}\n\n{_syn}\n\n{_footer}",
904
+ title="[bold]综合结论[/bold]",
905
+ box=_rbox_team.ROUNDED,
906
+ border_style="#C08050",
907
+ padding=(0, 1),
908
+ ))
909
+ else:
910
+ # agents already streamed via _on_agent_done (plain print)
911
+ if _quality_notes:
912
+ print(" 数据质量警告: " + "; ".join(_quality_notes[:3]))
913
+ print("\n ── 综合结论 ──")
914
+ from apps.cli.commands.team import (
915
+ build_team_terminal_summary as _team_terminal_summary,
916
+ clean_team_synthesis_text as _clean_team_synthesis,
917
+ )
918
+ print(_team_terminal_summary(_data_bundle))
919
+ print()
920
+ print(_clean_team_synthesis(team_result.synthesis or "*(无综合结论)*"))
921
+ print(f"\n 耗时 {team_result.elapsed_sec:.1f}s "
922
+ f"Signal: {team_result.final_signal} "
923
+ f"置信度: {team_result.confidence:.0%}")
924
+
925
+ # 保存报告
926
+ await self._save_team_report(sym, team_result, _data_bundle, _quality_notes)
927
+
928
+ # Record the directional call for outcome verification (DPO loop).
929
+ # synthesis + final_signal → detect_direction; entry price fetched
930
+ # by _record_prediction. Best-effort, never blocks.
931
+ try:
932
+ _call_text = f"{team_result.synthesis or ''} {team_result.final_signal or ''}"
933
+ self.terminal._record_prediction(sym, _call_text)
934
+ except Exception:
935
+ pass
936
+
937
+ except ImportError as _imp_err:
938
+ # agents 包不可用 — 不再回退到已废弃的 financial_agents
939
+ _m = (f"多代理分析模块加载失败:{_imp_err}。"
940
+ "请确认 agents 包完整(/install 或 pip install -e .)。")
941
+ console.print(f"\n [red]{_m}[/red]") if HAS_RICH else print(f"\n {_m}")
942
+ continue
943
+ except Exception as e:
944
+ msg = f"团队分析失败: {e}"
945
+ console.print(f"\n [red]{msg}[/red]") if HAS_RICH else print(f"\n {msg}")
946
+ continue
947
+
948
+ async def _save_team_report(self, symbol: str, team_result, data_bundle=None, quality_notes: Optional[list] = None) -> None:
949
+ """将 /team 分析结果保存为 Markdown 报告"""
950
+ saved = save_team_report(
951
+ symbol=symbol,
952
+ team_result=team_result,
953
+ data_bundle=data_bundle,
954
+ quality_notes=quality_notes,
955
+ )
956
+ try:
957
+ parts = saved.path.parts
958
+ short_path = "/".join(parts[-5:]) if len(parts) > 5 else str(saved.path)
959
+ except Exception:
960
+ short_path = str(saved.path)
961
+ msg = f" 报告已保存: .../{short_path}"
962
+ console.print(f" [dim]{msg}[/dim]") if HAS_RICH else print(msg)