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.
- agents/__init__.py +32 -0
- agents/base.py +190 -0
- agents/deep/__init__.py +37 -0
- agents/deep/calibration_loop.py +144 -0
- agents/deep/critic.py +125 -0
- agents/deep/deepen.py +193 -0
- agents/deep/models.py +149 -0
- agents/deep/pipeline.py +164 -0
- agents/deep/quant_fusion.py +192 -0
- agents/deep/themes.py +95 -0
- agents/deep/tiers.py +106 -0
- agents/financial/__init__.py +10 -0
- agents/financial/catalyst.py +279 -0
- agents/financial/debate.py +145 -0
- agents/financial/earnings.py +303 -0
- agents/financial/fundamental.py +159 -0
- agents/financial/macro.py +99 -0
- agents/financial/news.py +207 -0
- agents/financial/risk.py +132 -0
- agents/financial/sector.py +279 -0
- agents/financial/synthesis.py +274 -0
- agents/financial/technical.py +258 -0
- agents/portfolio_agent.py +333 -0
- agents/realty/__init__.py +62 -0
- agents/realty/asset_diagnosis.py +150 -0
- agents/realty/business_match.py +165 -0
- agents/realty/cashflow_verify.py +208 -0
- agents/realty/contract_rules.py +209 -0
- agents/realty/energy_anomaly.py +188 -0
- agents/realty/exit_settlement.py +207 -0
- agents/realty/fulfillment_risk.py +205 -0
- agents/realty/ops_optimize.py +159 -0
- agents/realty/revenue_share.py +214 -0
- agents/registry.py +144 -0
- agents/sports/__init__.py +0 -0
- agents/sports/football_agent.py +169 -0
- agents/team.py +289 -0
- aliyun_data_client.py +660 -0
- apps/README.md +12 -0
- apps/__init__.py +2 -0
- apps/channels/README.md +15 -0
- apps/cli/README.md +13 -0
- apps/cli/__init__.py +2 -0
- apps/cli/bootstrap.py +99 -0
- apps/cli/codegen_paths.py +29 -0
- apps/cli/commands/__init__.py +16 -0
- apps/cli/commands/analysis_cmds.py +288 -0
- apps/cli/commands/backtest_cmds.py +1887 -0
- apps/cli/commands/broker_cmds.py +1154 -0
- apps/cli/commands/business_workflow_cmds.py +289 -0
- apps/cli/commands/catalog.py +84 -0
- apps/cli/commands/data_cmds.py +405 -0
- apps/cli/commands/diagnostic_cmds.py +179 -0
- apps/cli/commands/diagnostic_ops_cmds.py +696 -0
- apps/cli/commands/finance_render.py +12 -0
- apps/cli/commands/market.py +399 -0
- apps/cli/commands/market_cmds.py +1276 -0
- apps/cli/commands/market_context.py +425 -0
- apps/cli/commands/market_render.py +7 -0
- apps/cli/commands/model_cmds.py +1579 -0
- apps/cli/commands/ops_cmds.py +668 -0
- apps/cli/commands/portfolio_cmds.py +962 -0
- apps/cli/commands/report.py +377 -0
- apps/cli/commands/scaffold_templates.py +617 -0
- apps/cli/commands/session_cmds.py +179 -0
- apps/cli/commands/session_ux_cmds.py +280 -0
- apps/cli/commands/team.py +588 -0
- apps/cli/commands/team_render.py +8 -0
- apps/cli/commands/ui_cmds.py +358 -0
- apps/cli/commands/workflow_cmds.py +279 -0
- apps/cli/commands/workspace_cmds.py +1414 -0
- apps/cli/config_paths.py +70 -0
- apps/cli/config_store.py +61 -0
- apps/cli/deterministic.py +122 -0
- apps/cli/direct.py +48 -0
- apps/cli/github_app_auth.py +135 -0
- apps/cli/handlers/__init__.py +11 -0
- apps/cli/handlers/broker_handlers.py +122 -0
- apps/cli/handlers/chart_handlers.py +1309 -0
- apps/cli/handlers/market_handlers.py +2509 -0
- apps/cli/handlers/realty_handlers.py +114 -0
- apps/cli/handlers/strategy_advice.py +82 -0
- apps/cli/hooks.py +180 -0
- apps/cli/i18n.py +284 -0
- apps/cli/intent.py +136 -0
- apps/cli/intent_router.py +217 -0
- apps/cli/lifecycle_hooks.py +48 -0
- apps/cli/main.py +29 -0
- apps/cli/market_metadata.py +135 -0
- apps/cli/market_universe.py +265 -0
- apps/cli/message_processing.py +257 -0
- apps/cli/plan_mode.py +139 -0
- apps/cli/plotly_html.py +15 -0
- apps/cli/prediction_feedback.py +202 -0
- apps/cli/preflight.py +497 -0
- apps/cli/project_aria.py +60 -0
- apps/cli/prompts/__init__.py +0 -0
- apps/cli/prompts/coding.py +658 -0
- apps/cli/prompts/system_prompts.py +531 -0
- apps/cli/prompts/ui.py +434 -0
- apps/cli/providers/__init__.py +1 -0
- apps/cli/providers/base.py +271 -0
- apps/cli/providers/chat_routing.py +80 -0
- apps/cli/providers/llm/__init__.py +1 -0
- apps/cli/providers/llm/ollama_stream.py +1170 -0
- apps/cli/providers/llm/sse_stream.py +216 -0
- apps/cli/providers/runtime_bridge.py +185 -0
- apps/cli/runtime_consumer.py +489 -0
- apps/cli/session_export.py +87 -0
- apps/cli/session_jsonl.py +207 -0
- apps/cli/session_store.py +112 -0
- apps/cli/todo_tracker.py +190 -0
- apps/cli/tools/__init__.py +40 -0
- apps/cli/tools/context.py +46 -0
- apps/cli/tools/file_tools.py +112 -0
- apps/cli/tools/market_tools.py +549 -0
- apps/cli/tools/notebook_tools.py +111 -0
- apps/cli/tools/system_tools.py +669 -0
- apps/cli/tools/write_tools.py +715 -0
- apps/cli/tradingview_bridge.py +434 -0
- apps/cli/update_check.py +152 -0
- apps/cli/utils/__init__.py +0 -0
- apps/cli/utils/market_detect.py +1578 -0
- apps/daemon/README.md +14 -0
- apps/vscode/README.md +115 -0
- apps/vscode/package.json +70 -0
- aria_cli.py +11636 -0
- aria_code-4.1.3.dist-info/METADATA +952 -0
- aria_code-4.1.3.dist-info/RECORD +284 -0
- aria_code-4.1.3.dist-info/WHEEL +5 -0
- aria_code-4.1.3.dist-info/entry_points.txt +2 -0
- aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
- aria_code-4.1.3.dist-info/top_level.txt +50 -0
- aria_daemon.py +1295 -0
- aria_feishu_bot.py +1359 -0
- aria_relay_client.py +182 -0
- aria_relay_server.py +405 -0
- aria_telegram_bot.py +202 -0
- ariarc.py +328 -0
- artifacts.py +491 -0
- backtest_report.py +472 -0
- brokers/__init__.py +72 -0
- brokers/base.py +207 -0
- brokers/capabilities.py +264 -0
- brokers/cn/__init__.py +10 -0
- brokers/cn/easytrader_broker.py +193 -0
- brokers/cn/futu_broker.py +194 -0
- brokers/cn/longbridge_broker.py +190 -0
- brokers/cn/tiger_broker.py +196 -0
- brokers/cn/xtquant_broker.py +175 -0
- brokers/config.py +364 -0
- brokers/intl/__init__.py +5 -0
- brokers/intl/alpaca_broker.py +183 -0
- brokers/intl/ibkr_broker.py +215 -0
- brokers/intl/webull_broker.py +156 -0
- brokers/paper_broker.py +259 -0
- brokers/planning.py +296 -0
- brokers/registry.py +181 -0
- brokers/trading.py +237 -0
- change_store.py +127 -0
- command_safety.py +19 -0
- computer_use_tools.py +504 -0
- dashboard_generator.py +578 -0
- data_analysis_tools.py +808 -0
- data_cleaner.py +483 -0
- data_service.py +481 -0
- datasources/__init__.py +23 -0
- datasources/base.py +166 -0
- datasources/router.py +221 -0
- datasources/sources/__init__.py +15 -0
- datasources/sources/akshare_source.py +269 -0
- datasources/sources/alpha_vantage_source.py +202 -0
- datasources/sources/edgar_source.py +218 -0
- datasources/sources/finnhub_source.py +197 -0
- datasources/sources/fred_source.py +219 -0
- datasources/sources/tushare_source.py +141 -0
- datasources/sources/web_scraper_source.py +278 -0
- datasources/sources/world_bank_source.py +205 -0
- datasources/sources/yfinance_source.py +152 -0
- demo_player.py +204 -0
- doctor.py +508 -0
- file_analysis_tools.py +734 -0
- finance_formulas.py +389 -0
- football_data_client.py +1670 -0
- intent_classifier.py +358 -0
- local_finance_tools.py +3221 -0
- local_llm_provider.py +552 -0
- macro_tools.py +368 -0
- market_data_client.py +1899 -0
- mcp_client.py +506 -0
- memory_manager.py +245 -0
- model_capability.py +416 -0
- notification_tools.py +248 -0
- packages/__init__.py +23 -0
- packages/aria_agents/__init__.py +5 -0
- packages/aria_agents/manifest.py +69 -0
- packages/aria_core/__init__.py +34 -0
- packages/aria_core/architecture.py +192 -0
- packages/aria_core/export.py +124 -0
- packages/aria_core/manifest.py +65 -0
- packages/aria_infra/__init__.py +15 -0
- packages/aria_infra/arthera.py +52 -0
- packages/aria_infra/doctor.py +246 -0
- packages/aria_infra/product.py +37 -0
- packages/aria_mcp/__init__.py +25 -0
- packages/aria_mcp/bridge.py +38 -0
- packages/aria_mcp/config.py +97 -0
- packages/aria_mcp/tools.py +61 -0
- packages/aria_sdk/__init__.py +19 -0
- packages/aria_sdk/client.py +396 -0
- packages/aria_sdk/providers.py +70 -0
- packages/aria_sdk/streaming.py +73 -0
- packages/aria_sdk/types.py +86 -0
- packages/aria_services/__init__.py +55 -0
- packages/aria_services/context.py +258 -0
- packages/aria_services/data.py +11 -0
- packages/aria_services/provider_health.py +189 -0
- packages/aria_services/registry.py +213 -0
- packages/aria_services/usage.py +138 -0
- packages/aria_skills/__init__.py +5 -0
- packages/aria_skills/registry.py +59 -0
- packages/aria_tools/__init__.py +5 -0
- packages/aria_tools/registry.py +128 -0
- packages/quant_engine/__init__.py +6 -0
- packages/quant_engine/sports/__init__.py +72 -0
- packages/quant_engine/sports/calibrator.py +353 -0
- packages/quant_engine/sports/dixon_coles.py +234 -0
- packages/quant_engine/sports/elo.py +299 -0
- packages/quant_engine/sports/form.py +188 -0
- packages/quant_engine/sports/h2h.py +195 -0
- packages/quant_engine/sports/ml_model.py +354 -0
- packages/quant_engine/sports/predictor.py +311 -0
- packages/quant_engine/sports/tracker.py +664 -0
- packages/quant_engine/stochastic/__init__.py +27 -0
- packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- packages/quant_engine/stochastic/ito_calculus.py +477 -0
- packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- packages/quant_engine/stochastic/options_pricing.py +573 -0
- packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- plan_utils.py +194 -0
- plugin_loader.py +328 -0
- portfolio_ledger.py +262 -0
- privacy/__init__.py +5 -0
- privacy/feedback.py +123 -0
- project_tools.py +525 -0
- providers/__init__.py +30 -0
- providers/llm/__init__.py +19 -0
- providers/llm/anthropic.py +184 -0
- providers/llm/base.py +139 -0
- providers/llm/ollama.py +128 -0
- providers/llm/openai_compat.py +282 -0
- providers/llm/registry.py +358 -0
- realty_data_tools.py +659 -0
- report_generator.py +1314 -0
- runtime/__init__.py +103 -0
- runtime/agent_loop.py +1183 -0
- runtime/approval.py +51 -0
- runtime/events.py +102 -0
- runtime/gateway.py +128 -0
- runtime/lsp.py +346 -0
- runtime/subagent.py +258 -0
- runtime/tool_executor.py +104 -0
- runtime/tool_policy.py +106 -0
- safety/__init__.py +21 -0
- safety/permissions.py +275 -0
- setup_wizard.py +653 -0
- strategy_vault.py +420 -0
- ui/__init__.py +100 -0
- ui/banner.py +310 -0
- ui/completer.py +391 -0
- ui/console.py +271 -0
- ui/image_render.py +243 -0
- ui/input_box.py +376 -0
- ui/picker.py +195 -0
- ui/render/__init__.py +11 -0
- ui/render/finance.py +1480 -0
- ui/render/market.py +225 -0
- ui/render/output.py +681 -0
- ui/render/team.py +346 -0
- ui/robot.py +235 -0
- workspace/__init__.py +6 -0
- workspace/files.py +170 -0
- 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)
|