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
ui/render/finance.py
ADDED
|
@@ -0,0 +1,1480 @@
|
|
|
1
|
+
"""
|
|
2
|
+
apps/cli/commands/finance_render.py — Finance tool result renderers
|
|
3
|
+
===================================================================
|
|
4
|
+
All public functions accept ``console`` and ``has_rich`` as keyword-only
|
|
5
|
+
arguments following the same contract as team_render.py:
|
|
6
|
+
|
|
7
|
+
render_finance_result(tool_name, result, console=console, has_rich=HAS_RICH)
|
|
8
|
+
|
|
9
|
+
``aria_cli.py`` keeps thin wrappers that supply its module-level globals.
|
|
10
|
+
No imports from aria_cli.py — dependency flows one way only.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from rich.text import Text
|
|
22
|
+
HAS_RICH = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
HAS_RICH = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _clean_error_msg(error: object) -> str:
|
|
28
|
+
"""Convert provider/runtime errors into short user-facing messages."""
|
|
29
|
+
raw = str(error or "failed").strip()
|
|
30
|
+
low = raw.lower()
|
|
31
|
+
if not raw:
|
|
32
|
+
return "操作失败"
|
|
33
|
+
if "curl: (28)" in low or "timed out" in low or "timeout" in low:
|
|
34
|
+
return "请求超时,数据源暂时不可用。请稍后重试或运行 /health 检查服务。"
|
|
35
|
+
if "connection refused" in low:
|
|
36
|
+
return "连接被拒绝,服务暂时不可用。请检查本地服务或网络。"
|
|
37
|
+
if "connection aborted" in low or "remotedisconnected" in low:
|
|
38
|
+
return "网络连接中断,数据源未完成响应。请稍后重试。"
|
|
39
|
+
if "rate" in low or "429" in low or "too many requests" in low:
|
|
40
|
+
return "数据源请求频率受限,请稍后重试。"
|
|
41
|
+
if "traceback" in low:
|
|
42
|
+
return raw.splitlines()[-1][:160] if raw.splitlines() else "运行失败"
|
|
43
|
+
return raw[:240]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def render_finance_result(tool_name: str, result: dict, *, console=None, has_rich: bool = True, bot_mode: bool = False) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Rich-formatted display for all finance tool results.
|
|
49
|
+
Shows structured tables instead of raw dicts.
|
|
50
|
+
"""
|
|
51
|
+
if bot_mode:
|
|
52
|
+
return
|
|
53
|
+
if not result or not isinstance(result, dict):
|
|
54
|
+
return
|
|
55
|
+
if not result.get("success"):
|
|
56
|
+
err = _clean_error_msg(
|
|
57
|
+
result.get("error") or result.get("message") or "数据暂不可用(服务离线或无数据)"
|
|
58
|
+
)
|
|
59
|
+
chain = result.get("provider_chain") or []
|
|
60
|
+
chain_text = f"\n[dim]已尝试: {' -> '.join(chain)}[/dim]" if chain else ""
|
|
61
|
+
if has_rich:
|
|
62
|
+
from rich.panel import Panel
|
|
63
|
+
from rich import box as rich_box
|
|
64
|
+
console.print(Panel(
|
|
65
|
+
f"[yellow]⚠ {err}[/yellow]{chain_text}",
|
|
66
|
+
border_style="yellow",
|
|
67
|
+
box=rich_box.ROUNDED,
|
|
68
|
+
padding=(0, 1),
|
|
69
|
+
))
|
|
70
|
+
else:
|
|
71
|
+
print(f" ⚠ {err}")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
provider = result.get("provider", "")
|
|
75
|
+
prov_tag = f" [dim][{provider}][/dim]" if provider else ""
|
|
76
|
+
|
|
77
|
+
# ── Market data / quote ────────────────────────────────────────────
|
|
78
|
+
if tool_name in ("get_market_data", "get_crypto_data", "get_forex_data"):
|
|
79
|
+
sym = result.get("symbol", "")
|
|
80
|
+
px = result.get("latest_close", result.get("price", 0))
|
|
81
|
+
chg = result.get("change_pct", result.get("change_pct_24h", 0)) or 0
|
|
82
|
+
vol = result.get("volume", 0)
|
|
83
|
+
name = result.get("name", "")
|
|
84
|
+
curr = result.get("currency", "")
|
|
85
|
+
color = "green" if chg >= 0 else "red"
|
|
86
|
+
arrow = "▲" if chg >= 0 else "▼"
|
|
87
|
+
if has_rich:
|
|
88
|
+
from rich.table import Table
|
|
89
|
+
t = Table(show_header=False, box=None, padding=(0, 1))
|
|
90
|
+
t.add_column(style="dim", width=20)
|
|
91
|
+
t.add_column()
|
|
92
|
+
title_str = f"[bold]{sym}[/bold]" + (f" {name}" if name else "")
|
|
93
|
+
t.add_row("标的", title_str)
|
|
94
|
+
px_disp = f"{curr} {px:,.4g}" if curr else f"{px:,.4g}"
|
|
95
|
+
t.add_row("最新价", f"[bold]{px_disp}[/bold]")
|
|
96
|
+
t.add_row("涨跌幅", f"[{color}]{arrow} {chg:+.2f}%[/{color}]")
|
|
97
|
+
_hi = result.get("high"); _lo = result.get("low")
|
|
98
|
+
if _hi and _lo:
|
|
99
|
+
t.add_row("日内区间", f"{_lo:,.4g} — {_hi:,.4g}")
|
|
100
|
+
if vol:
|
|
101
|
+
t.add_row("成交量", f"{int(vol):,}")
|
|
102
|
+
# Technical indicators from local tool
|
|
103
|
+
_rsi = result.get("rsi")
|
|
104
|
+
if _rsi is not None:
|
|
105
|
+
_rsi_color = "red" if _rsi >= 70 else ("cyan" if _rsi <= 30 else "white")
|
|
106
|
+
t.add_row("RSI(14)", f"[{_rsi_color}]{_rsi:.1f}[/{_rsi_color}]")
|
|
107
|
+
_mh = result.get("macd_hist")
|
|
108
|
+
if _mh is not None:
|
|
109
|
+
_mh_color = "green" if _mh > 0 else "red"
|
|
110
|
+
t.add_row("MACD hist", f"[{_mh_color}]{_mh:+.4f}[/{_mh_color}]")
|
|
111
|
+
_ma20 = result.get("ma20"); _ma60 = result.get("ma60")
|
|
112
|
+
if _ma20:
|
|
113
|
+
t.add_row("MA20", f"{_ma20:,.4g}")
|
|
114
|
+
if _ma60:
|
|
115
|
+
t.add_row("MA60", f"{_ma60:,.4g}")
|
|
116
|
+
# Legacy cloud fields
|
|
117
|
+
for k in ("high_52w", "low_52w", "bid", "ask"):
|
|
118
|
+
v = result.get(k)
|
|
119
|
+
if v is not None:
|
|
120
|
+
t.add_row(k.replace("_", " ").title(), f"{v:,.4g}")
|
|
121
|
+
console.print(t)
|
|
122
|
+
if prov_tag:
|
|
123
|
+
console.print(f" {prov_tag}")
|
|
124
|
+
else:
|
|
125
|
+
print(f" {sym}: {px} ({chg:+.2f}%)")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
# ── Commodity data ─────────────────────────────────────────────────
|
|
129
|
+
if tool_name == "get_commodities_data":
|
|
130
|
+
sym = result.get("symbol", "")
|
|
131
|
+
px = result.get("latest_close", 0)
|
|
132
|
+
chg = result.get("change_pct", 0) or 0
|
|
133
|
+
unit = result.get("unit", "")
|
|
134
|
+
color = "green" if chg >= 0 else "red"
|
|
135
|
+
arrow = "▲" if chg >= 0 else "▼"
|
|
136
|
+
if has_rich:
|
|
137
|
+
console.print(
|
|
138
|
+
f" [bold]{sym}[/bold] {px:,.3g} {unit} "
|
|
139
|
+
f"[{color}]{arrow} {chg:+.3f}%[/{color}]{prov_tag}"
|
|
140
|
+
)
|
|
141
|
+
for k in ("pct_from_52w_high", "pct_from_52w_low", "year_return"):
|
|
142
|
+
v = result.get(k)
|
|
143
|
+
if v is not None:
|
|
144
|
+
console.print(f" [dim]{k:<25s}[/dim] {v:+.3%}")
|
|
145
|
+
else:
|
|
146
|
+
print(f" {sym}: {px} ({chg:+.3f}%)")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
# ── AI signal ──────────────────────────────────────────────────────
|
|
150
|
+
if tool_name == "get_ai_signal":
|
|
151
|
+
action = result.get("action", "HOLD")
|
|
152
|
+
conf = result.get("confidence", 0)
|
|
153
|
+
reason = result.get("reasoning", "")
|
|
154
|
+
sl = result.get("stop_loss")
|
|
155
|
+
tp = result.get("take_profit")
|
|
156
|
+
color = {"BUY": "green", "SELL": "red", "HOLD": "yellow"}.get(action, "white")
|
|
157
|
+
if has_rich:
|
|
158
|
+
console.print(f" Signal: [{color}][bold]{action}[/bold][/{color}] "
|
|
159
|
+
f"Confidence: [bold]{conf:.1%}[/bold]{prov_tag}")
|
|
160
|
+
if reason:
|
|
161
|
+
console.print(f" [dim]{reason[:120]}[/dim]")
|
|
162
|
+
if sl is not None:
|
|
163
|
+
console.print(f" [dim]Stop-loss: {sl} Take-profit: {tp}[/dim]")
|
|
164
|
+
else:
|
|
165
|
+
print(f" {action} ({conf:.1%}) — {reason[:80]}")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
# ── Factors ────────────────────────────────────────────────────────
|
|
169
|
+
if tool_name == "calculate_factors":
|
|
170
|
+
sym = result.get("symbol", "")
|
|
171
|
+
if has_rich:
|
|
172
|
+
from rich.table import Table
|
|
173
|
+
t = Table(title=f"Factors — {sym}", show_header=True, box=None, padding=(0, 1))
|
|
174
|
+
t.add_column("Factor", style="dim", width=24)
|
|
175
|
+
t.add_column("Value", justify="right")
|
|
176
|
+
t.add_column("Signal", width=6)
|
|
177
|
+
def _sig(v, neutral_lo=-0.1, neutral_hi=0.1):
|
|
178
|
+
if v is None: return ""
|
|
179
|
+
return "[green]▲[/green]" if v > neutral_hi else "[red]▼[/red]" if v < neutral_lo else "[yellow]─[/yellow]"
|
|
180
|
+
FACTOR_ROWS = [
|
|
181
|
+
("rsi_14", "RSI(14)", lambda v: "[red]OB[/red]" if v and v > 70 else "[green]OS[/green]" if v and v < 30 else "[dim]─[/dim]"),
|
|
182
|
+
("macd_hist", "MACD Hist", lambda v: "[green]▲[/green]" if v and v > 0 else "[red]▼[/red]"),
|
|
183
|
+
("trend_score", "Trend Score", lambda v: _sig(v, -0.2, 0.2)),
|
|
184
|
+
("bb_position", "BB Position", lambda v: "[red]OB[/red]" if v and v > 0.9 else "[green]OS[/green]" if v and v < 0.1 else "[dim]─[/dim]"),
|
|
185
|
+
("ma_20_gap", "vs MA20", lambda v: "[green]▲[/green]" if v and v > 0 else "[red]▼[/red]"),
|
|
186
|
+
("ma_60_gap", "vs MA60", lambda v: "[green]▲[/green]" if v and v > 0 else "[red]▼[/red]"),
|
|
187
|
+
("volatility_20d", "Vol(20d)", lambda v: ""),
|
|
188
|
+
("volume_ratio_20d","Vol Ratio", lambda v: "[green]⬆[/green]" if v and v > 1.5 else ""),
|
|
189
|
+
("return_5d", "Return 5d", lambda v: "[green]▲[/green]" if v and v > 0 else "[red]▼[/red]"),
|
|
190
|
+
("return_20d", "Return 20d", lambda v: "[green]▲[/green]" if v and v > 0 else "[red]▼[/red]"),
|
|
191
|
+
]
|
|
192
|
+
for key, label, sig_fn in FACTOR_ROWS:
|
|
193
|
+
v = result.get(key)
|
|
194
|
+
if v is not None:
|
|
195
|
+
val_str = f"{v:+.4f}" if isinstance(v, float) else str(v)
|
|
196
|
+
t.add_row(label, val_str, sig_fn(v))
|
|
197
|
+
console.print(t)
|
|
198
|
+
console.print(f" {prov_tag}")
|
|
199
|
+
else:
|
|
200
|
+
for k, v in result.items():
|
|
201
|
+
if k not in ("success", "symbol", "provider") and isinstance(v, (int, float)):
|
|
202
|
+
print(f" {k:<25s} {v:.5g}")
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
# ── Backtest ───────────────────────────────────────────────────────
|
|
206
|
+
if tool_name in ("backtest_strategy", "cloud_backtest"):
|
|
207
|
+
sym = result.get("symbol", result.get("symbols", ""))
|
|
208
|
+
strat = result.get("strategy", result.get("model_type", ""))
|
|
209
|
+
if has_rich:
|
|
210
|
+
from rich.table import Table
|
|
211
|
+
t = Table(title=f"Backtest — {sym} [{strat}]", show_header=True, box=None)
|
|
212
|
+
t.add_column("Metric", style="dim", width=24)
|
|
213
|
+
t.add_column("Value", justify="right")
|
|
214
|
+
PERF_ROWS = [
|
|
215
|
+
("total_return", "Total Return", lambda v: f"[{'green' if v >= 0 else 'red'}]{v:+.2%}[/]"),
|
|
216
|
+
("annual_return", "Annual Return", lambda v: f"[{'green' if v >= 0 else 'red'}]{v:+.2%}[/]"),
|
|
217
|
+
("sharpe_ratio", "Sharpe Ratio", lambda v: f"[{'green' if v >= 1 else 'yellow' if v >= 0.5 else 'red'}]{v:.3f}[/]"),
|
|
218
|
+
("sortino_ratio", "Sortino Ratio", lambda v: f"{v:.3f}"),
|
|
219
|
+
("max_drawdown", "Max Drawdown", lambda v: f"[red]{v:.2%}[/red]"),
|
|
220
|
+
("win_rate", "Win Rate", lambda v: f"{v:.1%}"),
|
|
221
|
+
("total_trades", "Trades", lambda v: str(int(v))),
|
|
222
|
+
("benchmark_return","Benchmark (B&H)", lambda v: f"{v:+.2%}"),
|
|
223
|
+
("alpha", "Alpha", lambda v: f"[{'green' if v >= 0 else 'red'}]{v:+.2%}[/]"),
|
|
224
|
+
]
|
|
225
|
+
for key, label, fmt_fn in PERF_ROWS:
|
|
226
|
+
v = result.get(key)
|
|
227
|
+
if v is not None:
|
|
228
|
+
t.add_row(label, fmt_fn(v))
|
|
229
|
+
console.print(t)
|
|
230
|
+
console.print(f" {result.get('start', '')} → {result.get('end', '')} "
|
|
231
|
+
f"[dim]{result.get('bars', '')} bars[/dim]{prov_tag}")
|
|
232
|
+
else:
|
|
233
|
+
for k in ("total_return", "sharpe_ratio", "max_drawdown", "win_rate"):
|
|
234
|
+
v = result.get(k)
|
|
235
|
+
if v is not None:
|
|
236
|
+
print(f" {k:<20s} {v:.4g}")
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
# ── Predictions ────────────────────────────────────────────────────
|
|
240
|
+
if tool_name == "get_predictions":
|
|
241
|
+
preds = result.get("predictions", [])
|
|
242
|
+
days = result.get("prediction_days", 5)
|
|
243
|
+
if has_rich and preds:
|
|
244
|
+
from rich.table import Table
|
|
245
|
+
t = Table(title=f"ML Predictions ({days}d)", show_header=True, box=None)
|
|
246
|
+
t.add_column("Symbol", style="bold", width=12)
|
|
247
|
+
t.add_column("Predicted Return", justify="right")
|
|
248
|
+
t.add_column("Confidence", justify="right")
|
|
249
|
+
for p in preds:
|
|
250
|
+
ret = p.get("predicted_return", 0)
|
|
251
|
+
conf = p.get("confidence", 0)
|
|
252
|
+
color = "green" if ret >= 0 else "red"
|
|
253
|
+
t.add_row(p["symbol"], f"[{color}]{ret:+.2%}[/{color}]", f"{conf:.0%}")
|
|
254
|
+
console.print(t)
|
|
255
|
+
console.print(f" {prov_tag}")
|
|
256
|
+
else:
|
|
257
|
+
for p in preds:
|
|
258
|
+
print(f" {p.get('symbol')}: {p.get('predicted_return',0):+.2%}")
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
# ── Northbound flow ────────────────────────────────────────────────
|
|
262
|
+
if tool_name == "get_northbound_flow":
|
|
263
|
+
latest = result.get("latest_net_buy_yi", 0)
|
|
264
|
+
total = result.get("total_net_buy_yi", 0)
|
|
265
|
+
trend = result.get("trend", "")
|
|
266
|
+
color = "green" if latest >= 0 else "red"
|
|
267
|
+
if has_rich:
|
|
268
|
+
console.print(f" 北向资金 Today: [{color}][bold]{latest:+.2f}亿[/bold][/{color}] "
|
|
269
|
+
f"Period Total: {total:+.2f}亿 [{trend}]{prov_tag}")
|
|
270
|
+
else:
|
|
271
|
+
print(f" 北向 Today: {latest:+.2f}亿 Period: {total:+.2f}亿")
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
# ── Market indices ────────────────────────────────────────────────
|
|
275
|
+
if tool_name == "get_market_indices":
|
|
276
|
+
indices = result.get("indices", result)
|
|
277
|
+
if has_rich:
|
|
278
|
+
from rich.table import Table
|
|
279
|
+
t = Table(title="全球市场指数", show_header=True, box=None, padding=(0, 1))
|
|
280
|
+
t.add_column("指数", style="bold", width=16)
|
|
281
|
+
t.add_column("最新价", justify="right")
|
|
282
|
+
t.add_column("涨跌", justify="right")
|
|
283
|
+
# Handles both list-of-dicts (yfinance) and dict-of-dicts (legacy) formats
|
|
284
|
+
rows = (
|
|
285
|
+
[(d.get("name",""), d) for d in indices]
|
|
286
|
+
if isinstance(indices, list)
|
|
287
|
+
else [(k, v) for k, v in indices.items() if isinstance(v, dict)]
|
|
288
|
+
)
|
|
289
|
+
for name, d in rows:
|
|
290
|
+
px = d.get("price", d.get("latest_close", d.get("close", 0))) or 0
|
|
291
|
+
chg = d.get("change_pct", d.get("changePercent", 0)) or 0
|
|
292
|
+
color = "green" if chg >= 0 else "red"
|
|
293
|
+
t.add_row(name or d.get("ticker",""), f"{px:,.2f}",
|
|
294
|
+
f"[{color}]{chg:+.2f}%[/{color}]")
|
|
295
|
+
console.print(t)
|
|
296
|
+
console.print(f" [dim]{result.get('date','')} {prov_tag}[/dim]")
|
|
297
|
+
else:
|
|
298
|
+
rows = indices if isinstance(indices, list) else list(indices.values())
|
|
299
|
+
for d in rows[:10]:
|
|
300
|
+
nm = d.get("name", d.get("ticker", ""))
|
|
301
|
+
print(f" {nm:<16} {d.get('price',0):>10,.2f} {d.get('change_pct',0):+.2f}%")
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
# ── Risk metrics ──────────────────────────────────────────────────
|
|
305
|
+
if tool_name == "get_risk_metrics":
|
|
306
|
+
sym = result.get("symbol", "")
|
|
307
|
+
if has_rich:
|
|
308
|
+
from rich.table import Table
|
|
309
|
+
t = Table(title=f"[bold]{sym}[/bold] 风险指标", show_header=False, box=None, padding=(0,1))
|
|
310
|
+
t.add_column(style="dim", width=22)
|
|
311
|
+
t.add_column(justify="right")
|
|
312
|
+
conf = result.get("confidence_level", 0.95)
|
|
313
|
+
rows_r = [
|
|
314
|
+
("年化波动率", f"{result.get('annual_volatility',0):.2%}"),
|
|
315
|
+
("年化收益率", f"{result.get('annual_return',0):+.2%}"),
|
|
316
|
+
(f"VaR({conf:.0%}) 日", f"[red]{result.get('var_daily',0):.2%}[/red]"),
|
|
317
|
+
(f"VaR({conf:.0%}) 月", f"[red]{result.get('var_monthly',0):.2%}[/red]"),
|
|
318
|
+
("CVaR 日", f"[red]{result.get('cvar_daily',0):.2%}[/red]"),
|
|
319
|
+
("最大回撤", f"[red]{result.get('max_drawdown',0):.2%}[/red]"),
|
|
320
|
+
("Sharpe Ratio", f"{result.get('sharpe_ratio',0):.3f}"),
|
|
321
|
+
("Calmar Ratio", f"{result.get('calmar_ratio',0):.3f}"),
|
|
322
|
+
("偏度", f"{result.get('skewness',0):.3f}"),
|
|
323
|
+
("峰度", f"{result.get('kurtosis',0):.3f}"),
|
|
324
|
+
]
|
|
325
|
+
for label, val in rows_r:
|
|
326
|
+
t.add_row(label, val)
|
|
327
|
+
console.print(t)
|
|
328
|
+
console.print(f" [dim]{prov_tag}[/dim]")
|
|
329
|
+
else:
|
|
330
|
+
for k in ("annual_volatility","var_daily","max_drawdown","sharpe_ratio"):
|
|
331
|
+
print(f" {k:<25} {result.get(k,0):.4g}")
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
# ── optimize_positions ────────────────────────────────────────────
|
|
335
|
+
if tool_name == "optimize_positions":
|
|
336
|
+
weights = result.get("weights", {})
|
|
337
|
+
method = result.get("method", "max_sharpe")
|
|
338
|
+
if has_rich:
|
|
339
|
+
from rich.table import Table
|
|
340
|
+
t = Table(title=f"组合优化 [{method}]", show_header=True, box=None, padding=(0,1))
|
|
341
|
+
t.add_column("代码", style="bold")
|
|
342
|
+
t.add_column("权重", justify="right")
|
|
343
|
+
t.add_column("占比", justify="right")
|
|
344
|
+
for sym_k, w in sorted(weights.items(), key=lambda x: -x[1]):
|
|
345
|
+
bar = "█" * int(w * 20)
|
|
346
|
+
t.add_row(sym_k, f"{w:.2%}", f"[cyan]{bar}[/cyan]")
|
|
347
|
+
console.print(t)
|
|
348
|
+
p_ret = result.get("portfolio_return", 0)
|
|
349
|
+
p_vol = result.get("portfolio_vol", 0)
|
|
350
|
+
sr = result.get("sharpe_ratio", 0)
|
|
351
|
+
console.print(
|
|
352
|
+
f" 期望收益 [bold]{p_ret:+.2%}[/bold] "
|
|
353
|
+
f"波动率 {p_vol:.2%} "
|
|
354
|
+
f"Sharpe [bold]{sr:.3f}[/bold] [dim]{prov_tag}[/dim]"
|
|
355
|
+
)
|
|
356
|
+
else:
|
|
357
|
+
for sym_k, w in weights.items():
|
|
358
|
+
print(f" {sym_k:<10} {w:.2%}")
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
# ── get_sector_performance ────────────────────────────────────────
|
|
362
|
+
if tool_name == "get_sector_performance":
|
|
363
|
+
top = result.get("top_sectors", [])
|
|
364
|
+
bottom = result.get("bottom_sectors", [])
|
|
365
|
+
all_s = result.get("sectors", top + bottom)
|
|
366
|
+
mkt = result.get("market", "")
|
|
367
|
+
if has_rich:
|
|
368
|
+
from rich.table import Table
|
|
369
|
+
t = Table(title=f"板块表现 [{mkt.upper()}]", show_header=True, box=None, padding=(0,1))
|
|
370
|
+
t.add_column("板块", style="bold", min_width=14)
|
|
371
|
+
t.add_column("涨跌", justify="right")
|
|
372
|
+
for row in sorted(all_s, key=lambda x: -(x.get("change_pct") or 0)):
|
|
373
|
+
chg = row.get("change_pct") or 0
|
|
374
|
+
color = "green" if chg >= 0 else "red"
|
|
375
|
+
t.add_row(row.get("sector",""), f"[{color}]{chg:+.2f}%[/{color}]")
|
|
376
|
+
console.print(t)
|
|
377
|
+
console.print(f" [dim]{result.get('date','')} {prov_tag}[/dim]")
|
|
378
|
+
else:
|
|
379
|
+
for row in sorted(all_s, key=lambda x: -(x.get("change_pct") or 0))[:10]:
|
|
380
|
+
print(f" {row.get('sector',''):<16} {row.get('change_pct',0):+.2f}%")
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
# ── screen_ashare ─────────────────────────────────────────────────
|
|
384
|
+
if tool_name == "screen_ashare":
|
|
385
|
+
stocks = result.get("stocks", [])
|
|
386
|
+
count = result.get("count", len(stocks))
|
|
387
|
+
if has_rich:
|
|
388
|
+
from rich.table import Table
|
|
389
|
+
t = Table(title=f"A股筛选 共 {count} 只", show_header=True, box=None, padding=(0,1))
|
|
390
|
+
t.add_column("代码", style="bold", width=8)
|
|
391
|
+
t.add_column("名称", width=10)
|
|
392
|
+
t.add_column("价格", justify="right")
|
|
393
|
+
t.add_column("涨跌%", justify="right")
|
|
394
|
+
t.add_column("PE", justify="right", style="dim")
|
|
395
|
+
t.add_column("市值(亿)",justify="right", style="dim")
|
|
396
|
+
for s in stocks[:30]:
|
|
397
|
+
chg = s.get("change_pct") or 0
|
|
398
|
+
color = "green" if chg >= 0 else "red"
|
|
399
|
+
pe = f"{s.get('pe_dynamic',0):.1f}" if s.get("pe_dynamic") else "—"
|
|
400
|
+
mc = f"{s.get('market_cap_yi', (s.get('market_cap') or 0)/1e8):.0f}"
|
|
401
|
+
t.add_row(
|
|
402
|
+
str(s.get("code","")), str(s.get("name",""))[:10],
|
|
403
|
+
f"{s.get('price',0):.2f}",
|
|
404
|
+
f"[{color}]{chg:+.2f}%[/{color}]",
|
|
405
|
+
pe, mc,
|
|
406
|
+
)
|
|
407
|
+
console.print(t)
|
|
408
|
+
console.print(f" [dim]{prov_tag}[/dim]")
|
|
409
|
+
else:
|
|
410
|
+
for s in stocks[:20]:
|
|
411
|
+
print(f" {s.get('code','')} {s.get('name','')} {s.get('change_pct',0):+.2f}%")
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
# ── get_limit_up_pool ─────────────────────────────────────────────
|
|
415
|
+
if tool_name == "get_limit_up_pool":
|
|
416
|
+
stocks = result.get("stocks", [])
|
|
417
|
+
count = result.get("count", len(stocks))
|
|
418
|
+
date_s = result.get("date", "")
|
|
419
|
+
if has_rich:
|
|
420
|
+
from rich.table import Table
|
|
421
|
+
t = Table(title=f"涨停板池 {date_s} 共 {count} 只",
|
|
422
|
+
show_header=True, box=None, padding=(0,1))
|
|
423
|
+
t.add_column("代码", style="bold", width=8)
|
|
424
|
+
t.add_column("名称", width=10)
|
|
425
|
+
t.add_column("连板", justify="right")
|
|
426
|
+
t.add_column("首封时间", style="dim")
|
|
427
|
+
t.add_column("类型", style="dim")
|
|
428
|
+
for s in stocks[:30]:
|
|
429
|
+
consec = s.get("consecutive") or s.get("limit_streak") or ""
|
|
430
|
+
t.add_row(
|
|
431
|
+
str(s.get("code","")), str(s.get("name",""))[:10],
|
|
432
|
+
str(consec), str(s.get("first_lock_time",""))[:8],
|
|
433
|
+
str(s.get("limit_type",""))[:6],
|
|
434
|
+
)
|
|
435
|
+
console.print(t)
|
|
436
|
+
console.print(f" [dim]{prov_tag}[/dim]")
|
|
437
|
+
else:
|
|
438
|
+
for s in stocks[:20]:
|
|
439
|
+
print(f" {s.get('code','')} {s.get('name','')} 连板:{s.get('consecutive','')}")
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
# ── get_futures_data / get_bonds_data ─────────────────────────────
|
|
443
|
+
if tool_name == "get_futures_data":
|
|
444
|
+
sym = result.get("symbol", result.get("ticker", ""))
|
|
445
|
+
price = result.get("price", result.get("current_price", 0)) or 0
|
|
446
|
+
chg = result.get("change_pct", result.get("changePercent", 0)) or 0
|
|
447
|
+
vol = result.get("volume", 0)
|
|
448
|
+
if has_rich:
|
|
449
|
+
color = "green" if chg >= 0 else "red"
|
|
450
|
+
console.print(
|
|
451
|
+
f" [bold]{sym}[/bold] {price:,.2f} "
|
|
452
|
+
f"[{color}]{chg:+.2f}%[/{color}]"
|
|
453
|
+
+ (f" vol {vol:,.0f}" if vol else "")
|
|
454
|
+
+ f" [dim]{prov_tag}[/dim]"
|
|
455
|
+
)
|
|
456
|
+
else:
|
|
457
|
+
print(f" {sym} {price} {chg:+.2f}%")
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
if tool_name == "get_bonds_data":
|
|
461
|
+
yields = result.get("yields", {})
|
|
462
|
+
spread = yields.get("10Y_2Y_spread")
|
|
463
|
+
curve = yields.get("curve_shape", "")
|
|
464
|
+
if has_rich:
|
|
465
|
+
from rich.table import Table
|
|
466
|
+
t = Table(title="美国国债收益率", show_header=False, box=None, padding=(0,1))
|
|
467
|
+
t.add_column(style="dim", width=6)
|
|
468
|
+
t.add_column(justify="right")
|
|
469
|
+
for tenor in ("2Y", "5Y", "10Y", "30Y"):
|
|
470
|
+
y = yields.get(tenor)
|
|
471
|
+
if y is not None:
|
|
472
|
+
t.add_row(tenor, f"[bold]{y:.3f}%[/bold]")
|
|
473
|
+
console.print(t)
|
|
474
|
+
if spread is not None:
|
|
475
|
+
color = "green" if spread >= 0 else "red"
|
|
476
|
+
console.print(
|
|
477
|
+
f" 10Y-2Y spread: [{color}]{spread:+.3f}%[/{color}] "
|
|
478
|
+
f"[dim]{curve} {prov_tag}[/dim]"
|
|
479
|
+
)
|
|
480
|
+
else:
|
|
481
|
+
for tenor, y in yields.items():
|
|
482
|
+
if isinstance(y, float):
|
|
483
|
+
print(f" {tenor}: {y:.3f}%")
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
# ── get_market_insights ───────────────────────────────────────────
|
|
487
|
+
if tool_name == "get_market_insights":
|
|
488
|
+
summaries = result.get("summaries", [])
|
|
489
|
+
note = result.get("note", "")
|
|
490
|
+
if has_rich:
|
|
491
|
+
from rich.table import Table
|
|
492
|
+
t = Table(title="市场洞察", show_header=True, box=None, padding=(0,1))
|
|
493
|
+
t.add_column("代码", style="bold", width=10)
|
|
494
|
+
t.add_column("RSI", justify="right")
|
|
495
|
+
t.add_column("MACD Hist", justify="right")
|
|
496
|
+
t.add_column("量比", justify="right")
|
|
497
|
+
t.add_column("趋势", justify="right")
|
|
498
|
+
for s in summaries:
|
|
499
|
+
rsi_v = s.get("rsi_14") or 0
|
|
500
|
+
mh = s.get("macd_hist") or 0
|
|
501
|
+
tr = s.get("trend_score") or 0
|
|
502
|
+
vr = s.get("vol_ratio") or 1.0
|
|
503
|
+
rsi_c = "red" if rsi_v > 70 else "green" if rsi_v < 30 else ""
|
|
504
|
+
mh_c = "green" if mh > 0 else "red"
|
|
505
|
+
t.add_row(
|
|
506
|
+
s.get("symbol",""),
|
|
507
|
+
f"[{rsi_c}]{rsi_v:.1f}[/{rsi_c}]" if rsi_c else f"{rsi_v:.1f}",
|
|
508
|
+
f"[{mh_c}]{mh:+.4f}[/{mh_c}]",
|
|
509
|
+
f"{vr:.2f}x",
|
|
510
|
+
f"{'↑' if tr > 0 else '↓' if tr < 0 else '→'} {tr:.2f}" if tr else "—",
|
|
511
|
+
)
|
|
512
|
+
console.print(t)
|
|
513
|
+
if note:
|
|
514
|
+
console.print(f" [dim]{note}[/dim]")
|
|
515
|
+
console.print(f" [dim]{prov_tag}[/dim]")
|
|
516
|
+
else:
|
|
517
|
+
for s in summaries:
|
|
518
|
+
print(f" {s.get('symbol','')} RSI:{s.get('rsi_14','')} MACD:{s.get('macd_hist','')}")
|
|
519
|
+
return
|
|
520
|
+
|
|
521
|
+
# ── calculate_factors ─────────────────────────────────────────────
|
|
522
|
+
if tool_name == "calculate_factors":
|
|
523
|
+
sym = result.get("symbol", "")
|
|
524
|
+
if has_rich:
|
|
525
|
+
from rich.table import Table
|
|
526
|
+
t = Table(title=f"[bold]{sym}[/bold] 因子分析", show_header=False, box=None, padding=(0,1))
|
|
527
|
+
t.add_column(style="dim", width=22)
|
|
528
|
+
t.add_column(justify="right")
|
|
529
|
+
_FACTOR_LABELS = {
|
|
530
|
+
"rsi_14": "RSI(14)",
|
|
531
|
+
"macd_hist": "MACD Hist",
|
|
532
|
+
"trend_score": "趋势评分",
|
|
533
|
+
"volume_ratio_20d": "量比(20d)",
|
|
534
|
+
"volatility_20d": "波动率(20d)",
|
|
535
|
+
"bb_position": "布林带位置",
|
|
536
|
+
"return_5d": "5日收益",
|
|
537
|
+
"return_20d": "20日收益",
|
|
538
|
+
"return_60d": "60日收益",
|
|
539
|
+
}
|
|
540
|
+
for key, label in _FACTOR_LABELS.items():
|
|
541
|
+
val = result.get(key)
|
|
542
|
+
if val is None:
|
|
543
|
+
continue
|
|
544
|
+
if key in ("return_5d","return_20d","return_60d","volatility_20d"):
|
|
545
|
+
color = "green" if val > 0 else "red"
|
|
546
|
+
t.add_row(label, f"[{color}]{val:+.2%}[/{color}]")
|
|
547
|
+
elif key == "rsi_14":
|
|
548
|
+
color = "red" if val > 70 else "green" if val < 30 else ""
|
|
549
|
+
t.add_row(label, f"[{color}]{val:.1f}[/{color}]" if color else f"{val:.1f}")
|
|
550
|
+
elif key == "macd_hist":
|
|
551
|
+
color = "green" if val > 0 else "red"
|
|
552
|
+
t.add_row(label, f"[{color}]{val:+.4f}[/{color}]")
|
|
553
|
+
elif key == "bb_position":
|
|
554
|
+
t.add_row(label, f"{val:.1%} {'超买区' if val > 0.8 else '超卖区' if val < 0.2 else '中间带'}")
|
|
555
|
+
else:
|
|
556
|
+
t.add_row(label, f"{val:.4g}")
|
|
557
|
+
console.print(t)
|
|
558
|
+
console.print(f" [dim]{prov_tag}[/dim]")
|
|
559
|
+
else:
|
|
560
|
+
for k in ("rsi_14","macd_hist","trend_score","return_20d"):
|
|
561
|
+
v = result.get(k)
|
|
562
|
+
if v is not None:
|
|
563
|
+
print(f" {k:<22} {v:.4g}")
|
|
564
|
+
return
|
|
565
|
+
|
|
566
|
+
# ── Generic fallback ──────────────────────────────────────────────
|
|
567
|
+
if has_rich:
|
|
568
|
+
# Show key=value pairs, skip large nested objects
|
|
569
|
+
out = Text()
|
|
570
|
+
for k, v in result.items():
|
|
571
|
+
if k in ("success", "provider", "history_tail", "equity_curve", "trades"):
|
|
572
|
+
continue
|
|
573
|
+
if isinstance(v, (int, float)):
|
|
574
|
+
color = "green" if v > 0 else "red" if v < 0 else ""
|
|
575
|
+
out.append(f" {k.replace('_',' ').title():<24s}", style="dim")
|
|
576
|
+
out.append(f"{v:,.5g}\n", style=color)
|
|
577
|
+
elif isinstance(v, str) and len(v) < 80:
|
|
578
|
+
out.append(f" {k.replace('_',' ').title():<24s}", style="dim")
|
|
579
|
+
out.append(f"{v}\n")
|
|
580
|
+
if str(out):
|
|
581
|
+
console.print(out)
|
|
582
|
+
if provider:
|
|
583
|
+
console.print(f" {prov_tag}")
|
|
584
|
+
else:
|
|
585
|
+
print(json.dumps({k: v for k, v in result.items()
|
|
586
|
+
if k not in ("success",) and not isinstance(v, list)},
|
|
587
|
+
indent=2, ensure_ascii=False, default=str)[:400])
|
|
588
|
+
|
|
589
|
+
# ── Broker query results ───────────────────────────────────────────
|
|
590
|
+
if tool_name in ("broker_query", "broker_order"):
|
|
591
|
+
query = result.get("query", "")
|
|
592
|
+
broker = result.get("broker", "券商")
|
|
593
|
+
|
|
594
|
+
if query == "account":
|
|
595
|
+
currency = result.get("currency", "CNY")
|
|
596
|
+
total = result.get("total_assets", 0)
|
|
597
|
+
cash = result.get("cash", 0)
|
|
598
|
+
mv = result.get("market_value", 0)
|
|
599
|
+
pnl_day = result.get("pnl_today", 0)
|
|
600
|
+
pnl_tot = result.get("pnl_total", 0)
|
|
601
|
+
if has_rich:
|
|
602
|
+
pday_c = "green" if pnl_day >= 0 else "red"
|
|
603
|
+
ptot_c = "green" if pnl_tot >= 0 else "red"
|
|
604
|
+
from rich.table import Table
|
|
605
|
+
t = Table(show_header=False, box=None, padding=(0, 1))
|
|
606
|
+
t.add_column(style="dim", width=16)
|
|
607
|
+
t.add_column()
|
|
608
|
+
t.add_row("账户", f"[bold]{broker}[/bold] [dim]{result.get('account_id','****')}[/dim]")
|
|
609
|
+
t.add_row("总资产", f"[bold]{currency} {total:,.2f}[/bold]")
|
|
610
|
+
t.add_row("持仓市值", f"{mv:,.2f}")
|
|
611
|
+
t.add_row("可用现金", f"{cash:,.2f}")
|
|
612
|
+
if pnl_day:
|
|
613
|
+
t.add_row("当日盈亏", f"[{pday_c}]{pnl_day:+,.2f}[/{pday_c}]")
|
|
614
|
+
if pnl_tot:
|
|
615
|
+
t.add_row("累计盈亏", f"[{ptot_c}]{pnl_tot:+,.2f}[/{ptot_c}]")
|
|
616
|
+
console.print(t)
|
|
617
|
+
else:
|
|
618
|
+
print(f"{broker}: 总资产 {total:,.2f} 可用 {cash:,.2f}")
|
|
619
|
+
return
|
|
620
|
+
|
|
621
|
+
if query == "positions":
|
|
622
|
+
positions = result.get("positions", [])
|
|
623
|
+
if not positions:
|
|
624
|
+
if has_rich:
|
|
625
|
+
console.print(f"[dim]{broker} — 当前无持仓[/dim]")
|
|
626
|
+
return
|
|
627
|
+
if has_rich:
|
|
628
|
+
from rich.table import Table
|
|
629
|
+
t = Table(title=f"[bold]{broker}[/bold] 持仓", show_header=True, header_style="bold")
|
|
630
|
+
t.add_column("代码", style="bold", no_wrap=True)
|
|
631
|
+
t.add_column("名称", max_width=10)
|
|
632
|
+
t.add_column("持仓", justify="right")
|
|
633
|
+
t.add_column("成本", justify="right", style="dim")
|
|
634
|
+
t.add_column("现价", justify="right")
|
|
635
|
+
t.add_column("市值", justify="right")
|
|
636
|
+
t.add_column("盈亏", justify="right")
|
|
637
|
+
t.add_column("盈亏%", justify="right")
|
|
638
|
+
for p in sorted(positions, key=lambda x: -abs(x.get("market_value", 0))):
|
|
639
|
+
pnl = p.get("pnl", 0)
|
|
640
|
+
pct = p.get("pnl_pct", 0)
|
|
641
|
+
c = "green" if pnl >= 0 else "red"
|
|
642
|
+
t.add_row(
|
|
643
|
+
p.get("symbol",""), p.get("name","—")[:10],
|
|
644
|
+
f"{p.get('quantity',0):,.0f}",
|
|
645
|
+
f"{p.get('cost',0):.3f}", f"{p.get('price',0):.3f}",
|
|
646
|
+
f"{p.get('market_value',0):,.2f}",
|
|
647
|
+
f"[{c}]{pnl:+,.2f}[/{c}]",
|
|
648
|
+
f"[{c}]{pct:+.2f}%[/{c}]",
|
|
649
|
+
)
|
|
650
|
+
console.print(t)
|
|
651
|
+
total_mv = sum(p.get("market_value",0) for p in positions)
|
|
652
|
+
total_pnl = sum(p.get("pnl",0) for p in positions)
|
|
653
|
+
tc = "green" if total_pnl >= 0 else "red"
|
|
654
|
+
console.print(f" [dim]{len(positions)} 只 市值 {total_mv:,.2f} 总盈亏 [{tc}]{total_pnl:+,.2f}[/{tc}][/dim]")
|
|
655
|
+
else:
|
|
656
|
+
for p in positions:
|
|
657
|
+
print(f" {p.get('symbol',''):<8} {p.get('name',''):<10} 持仓:{p.get('quantity',0):.0f} 盈亏:{p.get('pnl',0):+,.2f}")
|
|
658
|
+
return
|
|
659
|
+
|
|
660
|
+
if query == "orders":
|
|
661
|
+
orders = result.get("orders", [])
|
|
662
|
+
if not orders:
|
|
663
|
+
if has_rich:
|
|
664
|
+
console.print(f"[dim]{broker} — 无订单记录[/dim]")
|
|
665
|
+
return
|
|
666
|
+
if has_rich:
|
|
667
|
+
from rich.table import Table
|
|
668
|
+
t = Table(title=f"[bold]{broker}[/bold] 订单", show_header=True, header_style="bold")
|
|
669
|
+
t.add_column("代码", style="bold")
|
|
670
|
+
t.add_column("方向", justify="center")
|
|
671
|
+
t.add_column("委托量", justify="right")
|
|
672
|
+
t.add_column("成交量", justify="right")
|
|
673
|
+
t.add_column("委托价", justify="right", style="dim")
|
|
674
|
+
t.add_column("均价", justify="right")
|
|
675
|
+
t.add_column("状态")
|
|
676
|
+
t.add_column("时间", style="dim", max_width=14)
|
|
677
|
+
_ss = {"filled":"[green]成交[/green]","partial":"[yellow]部成[/yellow]",
|
|
678
|
+
"open":"[cyan]委托中[/cyan]","cancelled":"[dim]已撤[/dim]"}
|
|
679
|
+
_sd = {"buy":"[green]买入[/green]","sell":"[red]卖出[/red]"}
|
|
680
|
+
for o in orders:
|
|
681
|
+
t.add_row(
|
|
682
|
+
o.get("symbol",""),
|
|
683
|
+
_sd.get(o.get("side",""), o.get("side","")),
|
|
684
|
+
f"{o.get('quantity',0):,.0f}", f"{o.get('filled',0):,.0f}",
|
|
685
|
+
f"{o.get('price',0):.3f}",
|
|
686
|
+
f"{o.get('avg_price',0):.3f}" if o.get("avg_price") else "—",
|
|
687
|
+
_ss.get(o.get("status",""), o.get("status","")),
|
|
688
|
+
str(o.get("time",""))[:14],
|
|
689
|
+
)
|
|
690
|
+
console.print(t)
|
|
691
|
+
else:
|
|
692
|
+
for o in orders:
|
|
693
|
+
print(f" {o.get('symbol','')} {o.get('side','')} {o.get('quantity',0):.0f} @ {o.get('price',0):.3f} [{o.get('status','')}]")
|
|
694
|
+
return
|
|
695
|
+
|
|
696
|
+
# ── broker_order: confirmation required ────────────────────────
|
|
697
|
+
if tool_name == "broker_order" and result.get("confirmation_required"):
|
|
698
|
+
preview = result.get("order_preview", {})
|
|
699
|
+
if has_rich:
|
|
700
|
+
from rich.panel import Panel
|
|
701
|
+
from rich import box as _rbox
|
|
702
|
+
_side_cn = preview.get("side_cn", preview.get("side", ""))
|
|
703
|
+
_side_color = "green" if preview.get("side") == "buy" else "red"
|
|
704
|
+
_preview_id = preview.get("preview_id") or result.get("preview_id") or ""
|
|
705
|
+
_mode = preview.get("mode") or (result.get("trade_preview") or {}).get("mode") or ""
|
|
706
|
+
_broker = preview.get("broker") or (result.get("trade_preview") or {}).get("broker_label") or ""
|
|
707
|
+
_blockers = (result.get("trade_preview") or {}).get("execution_blockers") or []
|
|
708
|
+
_body = (
|
|
709
|
+
f"preview_id: [bold]{_preview_id}[/bold]\n"
|
|
710
|
+
f"模式: [bold]{_mode or '—'}[/bold] 券商: [bold]{_broker or '—'}[/bold]\n\n"
|
|
711
|
+
f"[bold]{_side_cn}[/bold] "
|
|
712
|
+
f"[bold]{preview.get('symbol','')}[/bold] "
|
|
713
|
+
f"数量: [bold]{preview.get('qty', 0):,}[/bold] "
|
|
714
|
+
f"价格: [bold]{preview.get('price_display','市价')}[/bold]\n\n"
|
|
715
|
+
"[yellow]确认执行时必须携带 preview_id · 其他任何回复取消[/yellow]"
|
|
716
|
+
)
|
|
717
|
+
if _blockers:
|
|
718
|
+
_body += "\n\n[red]执行限制:[/red]\n" + "\n".join(f" - {b}" for b in _blockers)
|
|
719
|
+
console.print(Panel(
|
|
720
|
+
_body,
|
|
721
|
+
title=f"[yellow]⚠ 订单确认[/yellow]",
|
|
722
|
+
border_style="yellow",
|
|
723
|
+
box=_rbox.ROUNDED,
|
|
724
|
+
padding=(0, 1),
|
|
725
|
+
))
|
|
726
|
+
else:
|
|
727
|
+
msg = result.get("message", "请确认订单")
|
|
728
|
+
print(f"\n⚠ 订单确认\n{msg}\n")
|
|
729
|
+
return
|
|
730
|
+
|
|
731
|
+
# ── broker_order: placed successfully ──────────────────────────
|
|
732
|
+
if tool_name == "broker_order" and result.get("success"):
|
|
733
|
+
if has_rich:
|
|
734
|
+
_side_cn = "买入" if result.get("side") == "buy" else "卖出"
|
|
735
|
+
console.print(
|
|
736
|
+
f"[green]✓ 订单已提交[/green] {result.get('broker','')} — "
|
|
737
|
+
f"{_side_cn} [bold]{result.get('symbol','')}[/bold] "
|
|
738
|
+
f"× {result.get('qty',0):,} "
|
|
739
|
+
f"[dim]#{result.get('order_id','—')} {result.get('status','')}[/dim]"
|
|
740
|
+
)
|
|
741
|
+
else:
|
|
742
|
+
print(f"✓ 订单已提交: {result}")
|
|
743
|
+
return
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def render_macro_result(r: dict, title: str, *, console=None, has_rich: bool = True) -> None:
|
|
747
|
+
"""Render US or CN macro result dict."""
|
|
748
|
+
if not r.get("success"):
|
|
749
|
+
if has_rich: console.print(f" [red]{r.get('error','数据获取失败')}[/red]")
|
|
750
|
+
else: print(f" {r.get('error','failed')}")
|
|
751
|
+
return
|
|
752
|
+
data = r.get("data", {})
|
|
753
|
+
if has_rich:
|
|
754
|
+
from rich.table import Table
|
|
755
|
+
from rich.rule import Rule
|
|
756
|
+
console.print(Rule(f"[bold]{title}[/bold]", style="dim"))
|
|
757
|
+
t = Table(show_header=True, box=None, padding=(0, 1))
|
|
758
|
+
t.add_column("指标", style="dim", min_width=20)
|
|
759
|
+
t.add_column("最新值", justify="right", min_width=10)
|
|
760
|
+
t.add_column("环比变化", justify="right")
|
|
761
|
+
t.add_column("时间", style="dim")
|
|
762
|
+
for key, item in data.items():
|
|
763
|
+
if key.startswith("_"): continue
|
|
764
|
+
if not isinstance(item, dict): continue
|
|
765
|
+
latest = item.get("latest", {}) or {}
|
|
766
|
+
val = latest.get("value")
|
|
767
|
+
date = latest.get("date", "")
|
|
768
|
+
change = item.get("change")
|
|
769
|
+
unit = item.get("unit", "")
|
|
770
|
+
label = item.get("label", key)
|
|
771
|
+
if val is None: continue
|
|
772
|
+
val_str = f"{val:.2f}{unit}"
|
|
773
|
+
if change is not None:
|
|
774
|
+
color = "green" if change > 0 else "red" if change < 0 else ""
|
|
775
|
+
chg_str = f"[{color}]{change:+.3f}[/{color}]" if color else f"{change:+.3f}"
|
|
776
|
+
else:
|
|
777
|
+
chg_str = "—"
|
|
778
|
+
t.add_row(label, val_str, chg_str, str(date)[:10])
|
|
779
|
+
console.print(t)
|
|
780
|
+
yc = data.get("_yield_curve", {})
|
|
781
|
+
if yc:
|
|
782
|
+
sp = yc.get("spread_10y_2y", 0)
|
|
783
|
+
shape = yc.get("shape", "")
|
|
784
|
+
color = "green" if sp > 0 else "red"
|
|
785
|
+
console.print(f" 收益率曲线: [{color}]{shape}[/{color}] 10Y-2Y利差: [{color}]{sp:+.3f}%[/{color}]")
|
|
786
|
+
else:
|
|
787
|
+
print(f"\n{title}")
|
|
788
|
+
for key, item in data.items():
|
|
789
|
+
if not isinstance(item, dict) or key.startswith("_"): continue
|
|
790
|
+
v = (item.get("latest") or {}).get("value")
|
|
791
|
+
if v is not None:
|
|
792
|
+
print(f" {item.get('label',key):<28} {v:.3g}")
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def render_cb_rates(r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
796
|
+
"""Render central bank rates."""
|
|
797
|
+
if not r.get("success"):
|
|
798
|
+
if has_rich: console.print(f" [red]{r.get('error')}[/red]")
|
|
799
|
+
return
|
|
800
|
+
rates = r.get("rates", {})
|
|
801
|
+
if has_rich:
|
|
802
|
+
from rich.rule import Rule
|
|
803
|
+
from rich.table import Table
|
|
804
|
+
console.print(Rule("[bold]🏦 主要央行政策利率[/bold]", style="dim"))
|
|
805
|
+
t = Table(show_header=False, box=None, padding=(0,1))
|
|
806
|
+
t.add_column(style="dim", min_width=28)
|
|
807
|
+
t.add_column(justify="right")
|
|
808
|
+
for name, val in rates.items():
|
|
809
|
+
if val is not None:
|
|
810
|
+
t.add_row(name, f"[bold]{val:.2f}%[/bold]")
|
|
811
|
+
console.print(t)
|
|
812
|
+
else:
|
|
813
|
+
print("\n央行利率")
|
|
814
|
+
for name, val in rates.items():
|
|
815
|
+
if val is not None:
|
|
816
|
+
print(f" {name:<30} {val:.2f}%")
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def render_econ_calendar(r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
820
|
+
"""Render economic calendar."""
|
|
821
|
+
events = r.get("events", [])
|
|
822
|
+
if has_rich:
|
|
823
|
+
from rich.rule import Rule
|
|
824
|
+
from rich.table import Table
|
|
825
|
+
console.print(Rule("[bold]📅 经济事件日历[/bold]", style="dim"))
|
|
826
|
+
t = Table(show_header=True, box=None, padding=(0,1))
|
|
827
|
+
t.add_column("时间", style="dim", width=12)
|
|
828
|
+
t.add_column("事件", min_width=30)
|
|
829
|
+
t.add_column("重要性", justify="center")
|
|
830
|
+
for ev in events[:15]:
|
|
831
|
+
imp = ev.get("importance", ev.get("importance_level",""))
|
|
832
|
+
imp_colored = (
|
|
833
|
+
"[red]HIGH[/red]" if str(imp).upper() in ("HIGH","3","★★★")
|
|
834
|
+
else "[yellow]MED[/yellow]" if str(imp).upper() in ("MEDIUM","2","★★")
|
|
835
|
+
else f"[dim]{imp}[/dim]"
|
|
836
|
+
)
|
|
837
|
+
console.print
|
|
838
|
+
t.add_row(
|
|
839
|
+
str(ev.get("time","") or ev.get("date",""))[:12],
|
|
840
|
+
str(ev.get("event","") or ev.get("title",""))[:45],
|
|
841
|
+
imp_colored,
|
|
842
|
+
)
|
|
843
|
+
console.print(t)
|
|
844
|
+
else:
|
|
845
|
+
for ev in events[:10]:
|
|
846
|
+
print(f" {ev.get('event','')} [{ev.get('importance','')}]")
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def render_options_chain(r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
850
|
+
"""Render options chain."""
|
|
851
|
+
if not r.get("success"):
|
|
852
|
+
if has_rich: console.print(f" [red]{r.get('error')}[/red]")
|
|
853
|
+
return
|
|
854
|
+
symbol = r.get("symbol","")
|
|
855
|
+
price = r.get("price", 0)
|
|
856
|
+
expiry = r.get("expiry","")
|
|
857
|
+
all_exp = r.get("all_expiries", [])
|
|
858
|
+
if has_rich:
|
|
859
|
+
from rich.table import Table
|
|
860
|
+
from rich.rule import Rule
|
|
861
|
+
console.print(Rule(f"[bold]{symbol}[/bold] 期权链 到期: [cyan]{expiry}[/cyan] 现价: [bold]{price:.2f}[/bold]", style="dim"))
|
|
862
|
+
if all_exp:
|
|
863
|
+
console.print(f" [dim]可用到期日: {', '.join(all_exp)}[/dim]")
|
|
864
|
+
for side in ("calls", "puts"):
|
|
865
|
+
rows = r.get(side, [])
|
|
866
|
+
if not rows: continue
|
|
867
|
+
label = "认购期权 (Calls)" if side == "calls" else "认沽期权 (Puts)"
|
|
868
|
+
t = Table(title=f"[bold]{label}[/bold]", show_header=True, box=None, padding=(0,1))
|
|
869
|
+
t.add_column("行权价", justify="right", style="bold")
|
|
870
|
+
t.add_column("最新价", justify="right")
|
|
871
|
+
t.add_column("买/卖", justify="right", style="dim")
|
|
872
|
+
t.add_column("IV%", justify="right")
|
|
873
|
+
t.add_column("OI", justify="right")
|
|
874
|
+
t.add_column("价内?", justify="center")
|
|
875
|
+
for row in rows:
|
|
876
|
+
itm = row.get("inTheMoney", False)
|
|
877
|
+
itm_s = "[green]✓[/green]" if itm else "[dim]—[/dim]"
|
|
878
|
+
bid = row.get("bid", 0) or 0
|
|
879
|
+
ask = row.get("ask", 0) or 0
|
|
880
|
+
t.add_row(
|
|
881
|
+
f"{row.get('strike',0):.2f}",
|
|
882
|
+
f"{row.get('lastPrice',0):.2f}",
|
|
883
|
+
f"{bid:.2f}/{ask:.2f}",
|
|
884
|
+
f"{row.get('iv_pct',0):.1f}%",
|
|
885
|
+
f"{int(row.get('openInterest',0)):,}",
|
|
886
|
+
itm_s,
|
|
887
|
+
)
|
|
888
|
+
console.print(t)
|
|
889
|
+
else:
|
|
890
|
+
for side in ("calls","puts"):
|
|
891
|
+
print(f"\n{side.upper()}")
|
|
892
|
+
for row in r.get(side, []):
|
|
893
|
+
print(f" K={row.get('strike')} last={row.get('lastPrice')} IV={row.get('iv_pct')}%")
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def render_quality_scores(symbol: str, f_r: dict, z_r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
897
|
+
"""Render Piotroski F-Score + Altman Z-Score side by side."""
|
|
898
|
+
if has_rich:
|
|
899
|
+
from rich.table import Table
|
|
900
|
+
from rich.rule import Rule
|
|
901
|
+
from rich.columns import Columns
|
|
902
|
+
from rich.panel import Panel
|
|
903
|
+
from rich import box as _rbox
|
|
904
|
+
|
|
905
|
+
console.print(Rule(f"[bold]{symbol}[/bold] 财务质量双维评估", style="dim"))
|
|
906
|
+
|
|
907
|
+
# F-Score panel
|
|
908
|
+
if f_r.get("success"):
|
|
909
|
+
fs = f_r["f_score"]
|
|
910
|
+
sig = f_r.get("signal","")
|
|
911
|
+
color = "green" if sig == "bullish" else "red" if sig == "bearish" else "yellow"
|
|
912
|
+
bars = "█" * fs + "░" * (9 - fs)
|
|
913
|
+
f_body = (
|
|
914
|
+
f"[bold {color}]{fs}/9[/bold {color}] [{color}]{f_r.get('verdict','')}[/{color}]\n"
|
|
915
|
+
f"[{color}]{bars}[/{color}]\n\n"
|
|
916
|
+
)
|
|
917
|
+
scores = f_r.get("scores", {})
|
|
918
|
+
categories = [
|
|
919
|
+
("盈利能力", ["F1_ROA_positive","F2_CFO_positive","F3_ROA_increasing","F4_CFO_gt_ROA"]),
|
|
920
|
+
("杠杆/流动性", ["F5_Leverage_lower","F6_CurrentRatio_up","F7_NoDilution"]),
|
|
921
|
+
("运营效率", ["F8_GrossMargin_up","F9_AssetTurnover_up"]),
|
|
922
|
+
]
|
|
923
|
+
for cat, keys in categories:
|
|
924
|
+
f_body += f"[dim]{cat}[/dim]\n"
|
|
925
|
+
for k in keys:
|
|
926
|
+
v = scores.get(k, 0)
|
|
927
|
+
check = "[green]✓[/green]" if v else "[dim]✗[/dim]"
|
|
928
|
+
f_body += f" {check} {k[3:].replace('_',' ')}\n"
|
|
929
|
+
f_panel = Panel(f_body.strip(), title="[bold]Piotroski F-Score[/bold]",
|
|
930
|
+
border_style=color, box=_rbox.ROUNDED, padding=(0,1))
|
|
931
|
+
else:
|
|
932
|
+
f_panel = Panel(f"[red]{f_r.get('error','失败')}[/red]",
|
|
933
|
+
title="Piotroski F-Score", border_style="red")
|
|
934
|
+
|
|
935
|
+
# Z-Score panel
|
|
936
|
+
if z_r.get("success"):
|
|
937
|
+
zs = z_r["z_score"]
|
|
938
|
+
risk = z_r.get("risk","medium")
|
|
939
|
+
zone = z_r.get("zone","")
|
|
940
|
+
zcolor = "green" if risk == "low" else "red" if risk == "high" else "yellow"
|
|
941
|
+
z_body = (
|
|
942
|
+
f"[bold {zcolor}]Z'' = {zs:.3f}[/bold {zcolor}]\n"
|
|
943
|
+
f"[{zcolor}]{zone}[/{zcolor}]\n\n"
|
|
944
|
+
f"[dim]安全区 >2.6 | 灰色区 1.1-2.6 | 风险区 <1.1[/dim]\n\n"
|
|
945
|
+
)
|
|
946
|
+
comp = z_r.get("components", {})
|
|
947
|
+
labels = {
|
|
948
|
+
"X1_working_capital_ratio": "X1 营运资本/总资产",
|
|
949
|
+
"X2_retained_earnings_ratio": "X2 留存收益/总资产",
|
|
950
|
+
"X3_ebit_ratio": "X3 EBIT/总资产",
|
|
951
|
+
"X4_equity_to_debt": "X4 权益/负债",
|
|
952
|
+
}
|
|
953
|
+
weights = {"X1": 6.56, "X2": 3.26, "X3": 6.72, "X4": 1.05}
|
|
954
|
+
for k, label in labels.items():
|
|
955
|
+
v = comp.get(k, 0)
|
|
956
|
+
prefix = k[:2]
|
|
957
|
+
contrib = round(v * weights.get(prefix, 1), 3)
|
|
958
|
+
c = "green" if v >= 0 else "red"
|
|
959
|
+
z_body += f" [dim]{label}[/dim]: [{c}]{v:.4f}[/{c}] (贡献 {contrib:+.3f})\n"
|
|
960
|
+
z_body += f"\n[dim]{z_r.get('formula','')}[/dim]"
|
|
961
|
+
z_panel = Panel(z_body.strip(), title="[bold]Altman Z''-Score[/bold]",
|
|
962
|
+
border_style=zcolor, box=_rbox.ROUNDED, padding=(0,1))
|
|
963
|
+
else:
|
|
964
|
+
z_panel = Panel(f"[red]{z_r.get('error','失败')}[/red]",
|
|
965
|
+
title="Altman Z-Score", border_style="red")
|
|
966
|
+
|
|
967
|
+
console.print(Columns([f_panel, z_panel]))
|
|
968
|
+
else:
|
|
969
|
+
if f_r.get("success"):
|
|
970
|
+
print(f"\nPiotroski F-Score: {f_r['f_score']}/9 {f_r.get('verdict','')}")
|
|
971
|
+
if z_r.get("success"):
|
|
972
|
+
print(f"Altman Z''-Score: {z_r['z_score']} {z_r.get('zone','')}")
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def render_ichimoku(r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
976
|
+
"""Render Ichimoku Cloud analysis."""
|
|
977
|
+
if not r.get("success"):
|
|
978
|
+
if has_rich: console.print(f" [red]{r.get('error')}[/red]")
|
|
979
|
+
return
|
|
980
|
+
if has_rich:
|
|
981
|
+
from rich.table import Table
|
|
982
|
+
from rich.rule import Rule
|
|
983
|
+
sym = r.get("symbol","")
|
|
984
|
+
sig = r.get("signal","")
|
|
985
|
+
price = r.get("price", 0)
|
|
986
|
+
is_bull = r.get("above_cloud", False)
|
|
987
|
+
sig_color = "green" if is_bull else "red" if r.get("below_cloud") else "yellow"
|
|
988
|
+
console.print(Rule(f"[bold]{sym}[/bold] 一目均衡表 [{sig_color}]{sig}[/{sig_color}]", style="dim"))
|
|
989
|
+
t = Table(show_header=False, box=None, padding=(0,1))
|
|
990
|
+
t.add_column(style="dim", width=18)
|
|
991
|
+
t.add_column(justify="right")
|
|
992
|
+
t.add_column(style="dim")
|
|
993
|
+
t.add_row("现价", f"[bold]{price:.3f}[/bold]", "")
|
|
994
|
+
t.add_row("转换线 (9)", f"{r.get('tenkan',0):.3f}",
|
|
995
|
+
"[green]↑ 多头[/green]" if r.get('tenkan',0) > r.get('kijun',0) else "[red]↓ 空头[/red]")
|
|
996
|
+
t.add_row("基准线 (26)",f"{r.get('kijun',0):.3f}", "")
|
|
997
|
+
if r.get("senkou_a"):
|
|
998
|
+
t.add_row("先行带A", f"{r.get('senkou_a',0):.3f}", "")
|
|
999
|
+
if r.get("senkou_b"):
|
|
1000
|
+
t.add_row("先行带B", f"{r.get('senkou_b',0):.3f}", "")
|
|
1001
|
+
t.add_row("云层厚度", f"{r.get('cloud_thickness',0):.3f}", r.get("cloud_color",""))
|
|
1002
|
+
t.add_row("TK交叉", r.get("tk_cross",""), "")
|
|
1003
|
+
console.print(t)
|
|
1004
|
+
console.print(f" [bold {sig_color}]结论: {sig}[/bold {sig_color}] [dim]先行带偏移 26期,迟行线(Chikou)={r.get('chikou',0):.3f}[/dim]")
|
|
1005
|
+
else:
|
|
1006
|
+
print(f"\n{r.get('symbol','')} 一目均衡表")
|
|
1007
|
+
for k in ("tenkan","kijun","senkou_a","senkou_b","signal"):
|
|
1008
|
+
print(f" {k}: {r.get(k,'')}")
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def render_fear_greed(r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
1012
|
+
"""Render Fear & Greed Index with ASCII gauge."""
|
|
1013
|
+
if not r.get("success"):
|
|
1014
|
+
if has_rich: console.print(f" [red]{r.get('error')}[/red]")
|
|
1015
|
+
return
|
|
1016
|
+
val = r.get("value", 50)
|
|
1017
|
+
label = r.get("label", "")
|
|
1018
|
+
sig = r.get("signal", "中性")
|
|
1019
|
+
if has_rich:
|
|
1020
|
+
from rich.rule import Rule
|
|
1021
|
+
color = "green" if val <= 25 else "red" if val >= 75 else "yellow"
|
|
1022
|
+
# ASCII gauge
|
|
1023
|
+
filled = int(val / 5)
|
|
1024
|
+
bar = "█" * filled + "░" * (20 - filled)
|
|
1025
|
+
console.print(Rule("[bold]₿ 加密恐惧贪婪指数[/bold]", style="dim"))
|
|
1026
|
+
console.print(f"\n [{color}]{bar}[/{color}] [bold {color}]{val}/100[/bold {color}] [{color}]{label}[/{color}]\n")
|
|
1027
|
+
console.print(f" 操作信号: [bold]{sig}[/bold] [dim]>75 极度贪婪(卖出信号) <25 极度恐惧(买入信号)[/dim]\n")
|
|
1028
|
+
hist = r.get("history", [])
|
|
1029
|
+
if len(hist) > 1:
|
|
1030
|
+
hist_str = " [dim]近7天: "
|
|
1031
|
+
for h in hist[:7]:
|
|
1032
|
+
v = h.get("value", 0)
|
|
1033
|
+
c = "green" if v <= 25 else "red" if v >= 75 else "yellow"
|
|
1034
|
+
hist_str += f"[{c}]{v}[/{c}] "
|
|
1035
|
+
console.print(hist_str + "[/dim]")
|
|
1036
|
+
else:
|
|
1037
|
+
print(f"\n恐惧贪婪指数: {val}/100 ({label}) 信号: {sig}")
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def render_funding_rates(r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
1041
|
+
"""Render perpetual funding rates."""
|
|
1042
|
+
if not r.get("success"):
|
|
1043
|
+
if has_rich: console.print(f" [red]{r.get('error')}[/red]")
|
|
1044
|
+
return
|
|
1045
|
+
rates = r.get("rates", [])
|
|
1046
|
+
if has_rich:
|
|
1047
|
+
from rich.table import Table
|
|
1048
|
+
from rich.rule import Rule
|
|
1049
|
+
exchange = r.get("exchange","")
|
|
1050
|
+
bias = r.get("market_bias","")
|
|
1051
|
+
console.print(Rule(f"[bold]{exchange.upper()}[/bold] 永续合约资金费率", style="dim"))
|
|
1052
|
+
t = Table(show_header=True, box=None, padding=(0,1))
|
|
1053
|
+
t.add_column("合约", style="bold", width=12)
|
|
1054
|
+
t.add_column("费率", justify="right")
|
|
1055
|
+
t.add_column("年化", justify="right")
|
|
1056
|
+
t.add_column("下次结算", style="dim")
|
|
1057
|
+
t.add_column("信号", justify="center")
|
|
1058
|
+
for rt in rates:
|
|
1059
|
+
rate = rt.get("rate", 0)
|
|
1060
|
+
color = "red" if rate > 0.05 else "green" if rate < -0.01 else "dim"
|
|
1061
|
+
sig_s = rt.get("signal","中性")
|
|
1062
|
+
sig_c = "red" if sig_s == "空" else "green" if sig_s == "多" else "dim"
|
|
1063
|
+
t.add_row(
|
|
1064
|
+
rt.get("symbol",""),
|
|
1065
|
+
f"[{color}]{rt.get('rate_pct','')}[/{color}]",
|
|
1066
|
+
rt.get("annualized",""),
|
|
1067
|
+
rt.get("next_funding","")[:12],
|
|
1068
|
+
f"[{sig_c}]{sig_s}[/{sig_c}]",
|
|
1069
|
+
)
|
|
1070
|
+
console.print(t)
|
|
1071
|
+
console.print(f" [dim]市场偏向: [bold]{bias}[/bold] 正费率=多头付费给空头,负费率=空头付费给多头[/dim]")
|
|
1072
|
+
else:
|
|
1073
|
+
for rt in rates:
|
|
1074
|
+
print(f" {rt.get('symbol','')} {rt.get('rate_pct','')} (年化{rt.get('annualized','')})")
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def render_peer_comparison(r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
1078
|
+
"""Render peer comparison table."""
|
|
1079
|
+
if not r.get("success"):
|
|
1080
|
+
if has_rich: console.print(f" [red]{r.get('error')}[/red]")
|
|
1081
|
+
return
|
|
1082
|
+
rows = r.get("table", [])
|
|
1083
|
+
if has_rich:
|
|
1084
|
+
from rich.table import Table
|
|
1085
|
+
from rich.rule import Rule
|
|
1086
|
+
symbol = r.get("symbol","")
|
|
1087
|
+
console.print(Rule(f"[bold]{symbol}[/bold] 同行估值对比", style="dim"))
|
|
1088
|
+
t = Table(show_header=True, box=None, padding=(0,1))
|
|
1089
|
+
t.add_column("代码", style="bold", width=8)
|
|
1090
|
+
t.add_column("名称", width=14)
|
|
1091
|
+
t.add_column("PE", justify="right")
|
|
1092
|
+
t.add_column("PB", justify="right")
|
|
1093
|
+
t.add_column("ROE%", justify="right")
|
|
1094
|
+
t.add_column("股息%", justify="right")
|
|
1095
|
+
t.add_column("市值(B)", justify="right", style="dim")
|
|
1096
|
+
for row in rows:
|
|
1097
|
+
is_t = row.get("is_target", False)
|
|
1098
|
+
pe = f"{row['pe']:.1f}" if row.get("pe") else "—"
|
|
1099
|
+
pb = f"{row['pb']:.2f}" if row.get("pb") else "—"
|
|
1100
|
+
roe = f"{row['roe_pct']:.1f}" if row.get("roe_pct") else "—"
|
|
1101
|
+
dy = f"{row['div_yield']:.2f}" if row.get("div_yield") else "—"
|
|
1102
|
+
mc = f"{row['market_cap_b']:.0f}" if row.get("market_cap_b") else "—"
|
|
1103
|
+
# Highlight target row
|
|
1104
|
+
style = "bold cyan" if is_t else ""
|
|
1105
|
+
t.add_row(
|
|
1106
|
+
f"[{style}]{row['symbol']}[/{style}]" if style else row["symbol"],
|
|
1107
|
+
(row.get("name","") or "")[:14],
|
|
1108
|
+
pe, pb, roe, dy, mc,
|
|
1109
|
+
)
|
|
1110
|
+
console.print(t)
|
|
1111
|
+
analysis = r.get("analysis", [])
|
|
1112
|
+
for line in analysis:
|
|
1113
|
+
console.print(f" [dim]▸ {line}[/dim]")
|
|
1114
|
+
else:
|
|
1115
|
+
print(f"\n{r.get('symbol','')} 同行对比")
|
|
1116
|
+
for row in rows:
|
|
1117
|
+
print(f" {row['symbol']:<8} PE:{row.get('pe','—')} PB:{row.get('pb','—')}")
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
# ── 不动产渲染函数 ─────────────────────────────────────────────────────────────
|
|
1121
|
+
|
|
1122
|
+
def render_house_price(r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
1123
|
+
if not has_rich:
|
|
1124
|
+
print(json.dumps(r, ensure_ascii=False, indent=2)); return
|
|
1125
|
+
if not r.get("success"):
|
|
1126
|
+
console.print(f"[red]{r.get('error','获取失败')}[/red]"); return
|
|
1127
|
+
from rich.table import Table as _T
|
|
1128
|
+
from rich import box as _box
|
|
1129
|
+
c1 = str(r.get("city1") or "城市1")
|
|
1130
|
+
c2 = str(r.get("city2") or "城市2")
|
|
1131
|
+
lc1, lc2 = r.get("latest_city1") or {}, r.get("latest_city2") or {}
|
|
1132
|
+
|
|
1133
|
+
def _fmt(v):
|
|
1134
|
+
if v is None: return "[dim]—[/dim]"
|
|
1135
|
+
fv = float(v) if not isinstance(v, float) else v
|
|
1136
|
+
color = "green" if fv > 0 else "red" if fv < 0 else "dim"
|
|
1137
|
+
return f"[{color}]{fv:+.2f}%[/{color}]"
|
|
1138
|
+
|
|
1139
|
+
tb = _T(title=f"[bold]🏠 房价指数对比[/bold]", box=_box.ROUNDED, show_header=True)
|
|
1140
|
+
tb.add_column("指标", style="dim")
|
|
1141
|
+
tb.add_column(c1, justify="right")
|
|
1142
|
+
tb.add_column(c2, justify="right")
|
|
1143
|
+
tb.add_row("新建商品房同比", _fmt(lc1.get("new_yoy")), _fmt(lc2.get("new_yoy")))
|
|
1144
|
+
tb.add_row("新建商品房环比", _fmt(lc1.get("new_mom")), _fmt(lc2.get("new_mom")))
|
|
1145
|
+
tb.add_row("二手房价同比", _fmt(lc1.get("second_yoy")), _fmt(lc2.get("second_yoy")))
|
|
1146
|
+
tb.add_row("二手房价环比", _fmt(lc1.get("second_mom")), _fmt(lc2.get("second_mom")))
|
|
1147
|
+
tb.add_row("[dim]数据期[/dim]", str(lc1.get("date") or "—"), str(lc2.get("date") or "—"))
|
|
1148
|
+
console.print(tb)
|
|
1149
|
+
console.print("[dim]数据来源:国家统计局 via akshare[/dim]")
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
def render_reits_list(r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
1153
|
+
if not has_rich:
|
|
1154
|
+
for row in r.get("reits", [])[:10]: print(row); return
|
|
1155
|
+
if not r.get("success"):
|
|
1156
|
+
console.print(f"[red]{r.get('error','获取失败')}[/red]"); return
|
|
1157
|
+
from rich.table import Table as _T
|
|
1158
|
+
from rich import box as _box
|
|
1159
|
+
tb = _T(title=f"[bold]🏗 中国 REITs 实时行情[/bold]", box=_box.ROUNDED)
|
|
1160
|
+
tb.add_column("代码", style="cyan")
|
|
1161
|
+
tb.add_column("名称")
|
|
1162
|
+
tb.add_column("最新价", justify="right")
|
|
1163
|
+
tb.add_column("涨跌幅", justify="right")
|
|
1164
|
+
tb.add_column("昨收", justify="right", style="dim")
|
|
1165
|
+
tb.add_column("成交额(万)", justify="right", style="dim")
|
|
1166
|
+
for row in r.get("reits", [])[:20]:
|
|
1167
|
+
chg = row.get("涨跌幅") or 0
|
|
1168
|
+
try: chg_f = float(chg)
|
|
1169
|
+
except Exception: chg_f = 0
|
|
1170
|
+
color = "green" if chg_f > 0 else "red" if chg_f < 0 else "dim"
|
|
1171
|
+
vol_wan = ""
|
|
1172
|
+
try: vol_wan = f"{float(row.get('成交额',0))/10000:.0f}"
|
|
1173
|
+
except Exception as _e: logger.debug("vol_wan parse error: %s", _e)
|
|
1174
|
+
tb.add_row(
|
|
1175
|
+
str(row.get("代码","")),
|
|
1176
|
+
str(row.get("名称",""))[:12],
|
|
1177
|
+
str(row.get("最新价","")),
|
|
1178
|
+
f"[{color}]{chg}%[/{color}]",
|
|
1179
|
+
str(row.get("昨收","")),
|
|
1180
|
+
vol_wan,
|
|
1181
|
+
)
|
|
1182
|
+
console.print(tb)
|
|
1183
|
+
console.print(f"[dim]共 {r.get('count',0)} 只 REITs · 数据来源:东方财富[/dim]")
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def render_rental_yield(r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
1187
|
+
if not has_rich:
|
|
1188
|
+
print(json.dumps(r, ensure_ascii=False, indent=2)); return
|
|
1189
|
+
if not r.get("success"):
|
|
1190
|
+
console.print(f"[red]{r.get('error','计算失败')}[/red]"); return
|
|
1191
|
+
from rich.panel import Panel as _P
|
|
1192
|
+
from rich.columns import Columns as _C
|
|
1193
|
+
from rich import box as _box
|
|
1194
|
+
assess_color = "green" if "优质" in str(r.get("assessment","")) else \
|
|
1195
|
+
"yellow" if "合理" in str(r.get("assessment","")) else "red"
|
|
1196
|
+
lines = [
|
|
1197
|
+
f"[bold cyan]购入价格[/bold cyan] {r['purchase_price_wan']:.1f} 万元",
|
|
1198
|
+
f"[bold cyan]月租金[/bold cyan] {r['monthly_rent']:.0f} 元/月",
|
|
1199
|
+
f"[dim]───────────────────────────────[/dim]",
|
|
1200
|
+
f"[bold]毛租金收益率[/bold] [{assess_color}]{r['gross_yield_pct']:.2f}%[/{assess_color}]",
|
|
1201
|
+
f"[bold]净收益率[/bold] {r['net_yield_pct']:.2f}%",
|
|
1202
|
+
f"[bold]资本化率[/bold] {r['cap_rate_pct']:.2f}%",
|
|
1203
|
+
f"[bold]回本年限[/bold] {r['payback_years']:.1f} 年",
|
|
1204
|
+
]
|
|
1205
|
+
if r.get("leveraged_yield_pct") is not None:
|
|
1206
|
+
lines.append(f"[bold]杠杆收益率[/bold] {r['leveraged_yield_pct']:.2f}% [dim](含贷款)[/dim]")
|
|
1207
|
+
lines += [
|
|
1208
|
+
f"[dim]───────────────────────────────[/dim]",
|
|
1209
|
+
f"[{assess_color}]综合评级:{r.get('assessment','')}[/{assess_color}]",
|
|
1210
|
+
f"[dim]{r.get('benchmark','')}[/dim]",
|
|
1211
|
+
]
|
|
1212
|
+
console.print(_P("\n".join(lines), title="[bold]💰 租金收益率分析[/bold]",
|
|
1213
|
+
border_style="cyan"))
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
def render_property_val(r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
1217
|
+
if not has_rich:
|
|
1218
|
+
print(json.dumps(r, ensure_ascii=False, indent=2)); return
|
|
1219
|
+
if not r.get("success"):
|
|
1220
|
+
console.print(f"[red]{r.get('error','估值失败')}[/red]"); return
|
|
1221
|
+
from rich.table import Table as _T
|
|
1222
|
+
from rich import box as _box
|
|
1223
|
+
from rich.panel import Panel as _P
|
|
1224
|
+
verd = r.get("verdict","")
|
|
1225
|
+
verd_color = "green" if "低估" in verd else "red" if "高估" in verd else "yellow"
|
|
1226
|
+
tb = _T(title="[bold]🏢 物业三合一估值[/bold]", box=_box.ROUNDED)
|
|
1227
|
+
tb.add_column("方法", style="dim")
|
|
1228
|
+
tb.add_column("估值(万元)", justify="right")
|
|
1229
|
+
tb.add_column("说明", style="dim")
|
|
1230
|
+
tb.add_row("收益法 (Cap Rate)", f"{r['income_approach']:.1f}", f"资本化率 {r['cap_rate_used']:.1f}%")
|
|
1231
|
+
tb.add_row("DCF 折现法", f"{r['dcf_approach']:.1f}", f"折现率 {r['discount_rate_used']:.1f}%")
|
|
1232
|
+
tb.add_row("市场比较法", f"{r['market_approach']:.1f}", "基于租金倍数推算")
|
|
1233
|
+
tb.add_row("[bold]综合估值[/bold]", f"[bold cyan]{r['blended_value_wan']:.1f}[/bold cyan]", "权重 4:4:2")
|
|
1234
|
+
console.print(tb)
|
|
1235
|
+
lo, hi = r.get("market_range_wan", [0, 0])
|
|
1236
|
+
console.print(f" 区位参考区间: [dim]{lo:.0f} — {hi:.0f} 万元[/dim]")
|
|
1237
|
+
console.print(f" 单价参考: [bold]{r.get('price_per_sqm',0):,.0f}[/bold] 元/㎡")
|
|
1238
|
+
console.print(f" [{verd_color}]{verd}[/{verd_color}]")
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
def render_multi_city(r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
1242
|
+
if not has_rich:
|
|
1243
|
+
for c in r.get("cities", []): print(c); return
|
|
1244
|
+
if not r.get("success"):
|
|
1245
|
+
console.print(f"[red]{r.get('error','获取失败')}[/red]"); return
|
|
1246
|
+
from rich.table import Table as _T
|
|
1247
|
+
from rich import box as _box
|
|
1248
|
+
tb = _T(title="[bold]🗺 多城市房价对比[/bold]", box=_box.ROUNDED)
|
|
1249
|
+
tb.add_column("城市", style="bold")
|
|
1250
|
+
tb.add_column("等级", style="dim")
|
|
1251
|
+
tb.add_column("数据期", style="dim")
|
|
1252
|
+
tb.add_column("新房同比", justify="right")
|
|
1253
|
+
tb.add_column("新房环比", justify="right")
|
|
1254
|
+
tb.add_column("二手同比", justify="right")
|
|
1255
|
+
for city in r.get("cities", []):
|
|
1256
|
+
def _fc(v):
|
|
1257
|
+
if v is None: return "[dim]—[/dim]"
|
|
1258
|
+
c = "green" if v > 0 else "red" if v < 0 else "dim"
|
|
1259
|
+
return f"[{c}]{v:+.2f}%[/{c}]"
|
|
1260
|
+
tb.add_row(
|
|
1261
|
+
city["city"], city.get("tier",""),
|
|
1262
|
+
city.get("date",""),
|
|
1263
|
+
_fc(city.get("new_yoy")), _fc(city.get("new_mom")),
|
|
1264
|
+
_fc(city.get("second_yoy")),
|
|
1265
|
+
)
|
|
1266
|
+
console.print(tb)
|
|
1267
|
+
console.print(f" 涨幅最高: [green]{r.get('top_riser','')}[/green] "
|
|
1268
|
+
f"涨幅最低/下跌: [red]{r.get('top_faller','')}[/red]")
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
def render_asset_score(r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
1272
|
+
if not has_rich:
|
|
1273
|
+
print(json.dumps(r, ensure_ascii=False, indent=2)); return
|
|
1274
|
+
if not r.get("success"):
|
|
1275
|
+
console.print(f"[red]{r.get('error','评分失败')}[/red]"); return
|
|
1276
|
+
from rich.panel import Panel as _P
|
|
1277
|
+
score = r.get("score", 0)
|
|
1278
|
+
rating = r.get("rating", "")
|
|
1279
|
+
color = "green" if score >= 75 else "yellow" if score >= 60 else "red"
|
|
1280
|
+
bar_len = int(score / 5)
|
|
1281
|
+
bar = "█" * bar_len + "░" * (20 - bar_len)
|
|
1282
|
+
lines = [
|
|
1283
|
+
f"综合评分: [{color}]{score}[/{color}] / 100",
|
|
1284
|
+
f"评级: [{color}]{rating}[/{color}]",
|
|
1285
|
+
f"[{color}]{bar}[/{color}]",
|
|
1286
|
+
"[dim]─────────────────────────────[/dim]",
|
|
1287
|
+
]
|
|
1288
|
+
for k, v in r.get("breakdown", {}).items():
|
|
1289
|
+
lines.append(f" {k:<12} [dim]{v}[/dim]")
|
|
1290
|
+
if r.get("suitable_businesses"):
|
|
1291
|
+
lines.append("[dim]─────────────────────────────[/dim]")
|
|
1292
|
+
lines.append(f"推荐业态: [cyan]{' / '.join(r['suitable_businesses'])}[/cyan]")
|
|
1293
|
+
console.print(_P("\n".join(lines), title="[bold]📍 资产区位评分[/bold]",
|
|
1294
|
+
border_style=color))
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
# ── 数据分析渲染函数 ────────────────────────────────────────────────────────────
|
|
1298
|
+
|
|
1299
|
+
def render_corr_matrix(r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
1300
|
+
if not has_rich:
|
|
1301
|
+
print(json.dumps(r.get("corr_matrix", {}), ensure_ascii=False, indent=2)); return
|
|
1302
|
+
if not r.get("success"):
|
|
1303
|
+
console.print(f"[red]{r.get('error','计算失败')}[/red]"); return
|
|
1304
|
+
from rich.table import Table as _T
|
|
1305
|
+
from rich import box as _box
|
|
1306
|
+
syms = r.get("symbols", [])
|
|
1307
|
+
corr = r.get("corr_matrix", {})
|
|
1308
|
+
tb = _T(title=f"[bold]📊 相关性矩阵 ({r.get('period','')}/{r.get('interval','1d')})[/bold]",
|
|
1309
|
+
box=_box.SIMPLE_HEAVY)
|
|
1310
|
+
tb.add_column("", style="bold")
|
|
1311
|
+
for s in syms:
|
|
1312
|
+
tb.add_column(s, justify="right")
|
|
1313
|
+
for s1 in syms:
|
|
1314
|
+
row_vals = []
|
|
1315
|
+
for s2 in syms:
|
|
1316
|
+
v = corr.get(s1, {}).get(s2)
|
|
1317
|
+
if v is None:
|
|
1318
|
+
row_vals.append("[dim]—[/dim]")
|
|
1319
|
+
elif s1 == s2:
|
|
1320
|
+
row_vals.append("[dim]1.00[/dim]")
|
|
1321
|
+
else:
|
|
1322
|
+
abs_v = abs(v)
|
|
1323
|
+
color = "red" if abs_v > 0.8 else "yellow" if abs_v > 0.5 else "green"
|
|
1324
|
+
row_vals.append(f"[{color}]{v:+.3f}[/{color}]")
|
|
1325
|
+
tb.add_row(f"[bold]{s1}[/bold]", *row_vals)
|
|
1326
|
+
console.print(tb)
|
|
1327
|
+
console.print("[dim]红 > 0.8 高相关 | 黄 0.5-0.8 中度 | 绿 < 0.5 低相关[/dim]")
|
|
1328
|
+
# Stats table
|
|
1329
|
+
stats = r.get("stats", {})
|
|
1330
|
+
if stats:
|
|
1331
|
+
st = _T(title="[dim]个股统计[/dim]", box=_box.MINIMAL)
|
|
1332
|
+
st.add_column("标的"); st.add_column("总收益%", justify="right")
|
|
1333
|
+
st.add_column("年化波动%", justify="right"); st.add_column("夏普", justify="right")
|
|
1334
|
+
st.add_column("最大回撤%", justify="right")
|
|
1335
|
+
for sym, sv in stats.items():
|
|
1336
|
+
ret = sv.get("return_total")
|
|
1337
|
+
rcolor = "green" if (ret or 0) > 0 else "red"
|
|
1338
|
+
st.add_row(sym,
|
|
1339
|
+
f"[{rcolor}]{ret:+.1f}[/{rcolor}]" if ret is not None else "—",
|
|
1340
|
+
f"{sv.get('volatility',0):.1f}" if sv.get("volatility") else "—",
|
|
1341
|
+
f"{sv.get('sharpe',0):.2f}" if sv.get("sharpe") else "—",
|
|
1342
|
+
f"[red]{sv.get('max_drawdown',0):.1f}[/red]")
|
|
1343
|
+
console.print(st)
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
def render_portfolio_bt(r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
1347
|
+
if not has_rich:
|
|
1348
|
+
print(json.dumps(r, ensure_ascii=False, indent=2)); return
|
|
1349
|
+
if not r.get("success"):
|
|
1350
|
+
console.print(f"[red]{r.get('error','回测失败')}[/red]"); return
|
|
1351
|
+
from rich.table import Table as _T
|
|
1352
|
+
from rich import box as _box
|
|
1353
|
+
from rich.panel import Panel as _P
|
|
1354
|
+
pf = r.get("portfolio", {})
|
|
1355
|
+
bm = r.get("benchmark", {})
|
|
1356
|
+
ret = pf.get("total_return_pct", 0)
|
|
1357
|
+
rcolor = "green" if ret > 0 else "red"
|
|
1358
|
+
lines = [
|
|
1359
|
+
f" [bold]总收益率[/bold] [{rcolor}]{ret:+.2f}%[/{rcolor}]",
|
|
1360
|
+
f" [bold]年化波动率[/bold] {pf.get('annual_vol_pct',0):.2f}%",
|
|
1361
|
+
f" [bold]夏普比率[/bold] {pf.get('sharpe_ratio','—')}",
|
|
1362
|
+
f" [bold]最大回撤[/bold] [red]{pf.get('max_drawdown_pct',0):.2f}%[/red]",
|
|
1363
|
+
f" [bold]卡玛比率[/bold] {pf.get('calmar_ratio','—')}",
|
|
1364
|
+
]
|
|
1365
|
+
if bm:
|
|
1366
|
+
br = bm.get("total_return_pct", 0)
|
|
1367
|
+
bc = "green" if br > 0 else "red"
|
|
1368
|
+
alpha = round(ret - br, 2)
|
|
1369
|
+
ac = "green" if alpha > 0 else "red"
|
|
1370
|
+
lines += [
|
|
1371
|
+
f" [dim]─────────────────────────────────[/dim]",
|
|
1372
|
+
f" [dim]基准 {bm['symbol']}[/dim] [{bc}]{br:+.2f}%[/{bc}]",
|
|
1373
|
+
f" [bold]超额收益[/bold] [{ac}]{alpha:+.2f}%[/{ac}]",
|
|
1374
|
+
]
|
|
1375
|
+
console.print(_P("\n".join(lines), title="[bold]📈 组合回测结果[/bold]",
|
|
1376
|
+
border_style=rcolor))
|
|
1377
|
+
# Allocation table
|
|
1378
|
+
alloc = r.get("allocation", [])
|
|
1379
|
+
if alloc:
|
|
1380
|
+
ta = _T(title=f"[dim]持仓分配 · 回测区间 {r.get('period','N/A')} · 再平衡 {r.get('rebalance','—')}[/dim]",
|
|
1381
|
+
box=_box.MINIMAL)
|
|
1382
|
+
ta.add_column("标的"); ta.add_column("权重%", justify="right")
|
|
1383
|
+
for a in alloc:
|
|
1384
|
+
ta.add_row(a["symbol"], f"{a['weight_pct']:.1f}%")
|
|
1385
|
+
console.print(ta)
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
def render_sql_result(r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
1389
|
+
if not has_rich:
|
|
1390
|
+
print(json.dumps(r, ensure_ascii=False, indent=2)); return
|
|
1391
|
+
if not r.get("success"):
|
|
1392
|
+
console.print(f"[red]{r.get('error','查询失败')}[/red]"); return
|
|
1393
|
+
from rich.table import Table as _T
|
|
1394
|
+
from rich import box as _box
|
|
1395
|
+
rows = r.get("rows", [])
|
|
1396
|
+
cols = r.get("columns", [])
|
|
1397
|
+
if not rows:
|
|
1398
|
+
console.print(f"[dim]查询返回 0 行[/dim]"); return
|
|
1399
|
+
tb = _T(title=f"[bold]🦆 DuckDB 查询结果[/bold] [dim]({r.get('row_count',0)} 行)[/dim]",
|
|
1400
|
+
box=_box.ROUNDED)
|
|
1401
|
+
for c in cols:
|
|
1402
|
+
tb.add_column(str(c))
|
|
1403
|
+
for row in rows[:100]:
|
|
1404
|
+
tb.add_row(*[str(row.get(c, "")) for c in cols])
|
|
1405
|
+
console.print(tb)
|
|
1406
|
+
if r.get("tables_loaded"):
|
|
1407
|
+
console.print(f"[dim]已加载表: {', '.join(r['tables_loaded'])}[/dim]")
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
def render_alerts(r: dict, *, console=None, has_rich: bool = True) -> None:
|
|
1411
|
+
if not has_rich:
|
|
1412
|
+
print(json.dumps(r, ensure_ascii=False, indent=2)); return
|
|
1413
|
+
from rich.table import Table as _T
|
|
1414
|
+
from rich import box as _box
|
|
1415
|
+
active = r.get("active_alerts", [])
|
|
1416
|
+
triggered = r.get("triggered_alerts", [])
|
|
1417
|
+
cond_lbl = {"gt": "高于", "lt": "低于", "cross_up": "向上突破", "cross_down": "向下跌破"}
|
|
1418
|
+
if active:
|
|
1419
|
+
ta = _T(title="[bold]🔔 活跃预警[/bold]", box=_box.ROUNDED)
|
|
1420
|
+
ta.add_column("标的", style="cyan"); ta.add_column("条件")
|
|
1421
|
+
ta.add_column("触发价", justify="right"); ta.add_column("备注", style="dim")
|
|
1422
|
+
ta.add_column("ID", style="dim")
|
|
1423
|
+
for a in active:
|
|
1424
|
+
ta.add_row(a["symbol"],
|
|
1425
|
+
cond_lbl.get(a["condition"], a["condition"]),
|
|
1426
|
+
str(a["price"]), a.get("note",""),
|
|
1427
|
+
a["id"][:16]+"…")
|
|
1428
|
+
console.print(ta)
|
|
1429
|
+
if triggered:
|
|
1430
|
+
tt = _T(title="[dim]已触发预警[/dim]", box=_box.MINIMAL)
|
|
1431
|
+
tt.add_column("标的"); tt.add_column("触发价"); tt.add_column("触发时间", style="dim")
|
|
1432
|
+
for a in triggered:
|
|
1433
|
+
tt.add_row(a["symbol"], str(a.get("triggered_price","")),
|
|
1434
|
+
str(a.get("triggered_at",""))[:16])
|
|
1435
|
+
console.print(tt)
|
|
1436
|
+
if not active and not triggered:
|
|
1437
|
+
console.print("[dim]暂无预警记录。使用 /alert add AAPL gt 200 设置预警[/dim]")
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
def _prompt_float(label: str, default: float) -> float:
|
|
1441
|
+
"""交互式数字输入,失败时返回 default。"""
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
# Moved from aria_cli.py
|
|
1445
|
+
def format_backtest_output(data: dict):
|
|
1446
|
+
"""Format backtest results as clean rows."""
|
|
1447
|
+
if not HAS_RICH:
|
|
1448
|
+
return json.dumps(data, indent=2, ensure_ascii=False)
|
|
1449
|
+
|
|
1450
|
+
d = data.get("data", data.get("backtest", data))
|
|
1451
|
+
total_ret = d.get("total_return", 0)
|
|
1452
|
+
ann_ret = d.get("annualized_return", 0)
|
|
1453
|
+
sharpe = d.get("sharpe_ratio", 0)
|
|
1454
|
+
max_dd = d.get("max_drawdown", 0)
|
|
1455
|
+
win_rate = d.get("win_rate", 0)
|
|
1456
|
+
trades = d.get("num_trades", 0)
|
|
1457
|
+
bh_ret = d.get("buy_hold_return", 0)
|
|
1458
|
+
outperf = d.get("outperformance", 0)
|
|
1459
|
+
|
|
1460
|
+
def _c(v): return "green" if v >= 0 else "red"
|
|
1461
|
+
|
|
1462
|
+
out = Text()
|
|
1463
|
+
out.append(" Backtest Results\n", style="bold")
|
|
1464
|
+
out.append(f" {'Total Return':<18s}", style="dim")
|
|
1465
|
+
out.append(f"{total_ret*100:+.2f}%", style=_c(total_ret))
|
|
1466
|
+
out.append(f" vs B&H ", style="dim")
|
|
1467
|
+
out.append(f"{bh_ret*100:+.2f}%\n", style=_c(bh_ret))
|
|
1468
|
+
out.append(f" {'Annualized':<18s}", style="dim")
|
|
1469
|
+
out.append(f"{ann_ret*100:+.2f}%\n")
|
|
1470
|
+
out.append(f" {'Sharpe Ratio':<18s}", style="dim")
|
|
1471
|
+
out.append(f"{sharpe:.2f}\n")
|
|
1472
|
+
out.append(f" {'Max Drawdown':<18s}", style="dim")
|
|
1473
|
+
out.append(f"{max_dd*100:.2f}%\n", style="red")
|
|
1474
|
+
out.append(f" {'Win Rate':<18s}", style="dim")
|
|
1475
|
+
out.append(f"{win_rate*100:.1f}%\n")
|
|
1476
|
+
out.append(f" {'Trades':<18s}", style="dim")
|
|
1477
|
+
out.append(f"{trades}\n")
|
|
1478
|
+
out.append(f" {'Outperformance':<18s}", style="dim")
|
|
1479
|
+
out.append(f"{outperf*100:+.2f}%\n", style=_c(outperf))
|
|
1480
|
+
return out
|