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,425 @@
|
|
|
1
|
+
"""Context builders for market-analysis commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any, Callable, Optional
|
|
9
|
+
|
|
10
|
+
from apps.cli.prompts.system_prompts import build_response_style_rule
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
TA_SESSION_CACHE: dict[str, dict[str, Any]] = {}
|
|
14
|
+
TA_SESSION_CACHE_TTL = 600
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _cached_ta(symbol: str) -> dict[str, Any]:
|
|
18
|
+
cached = TA_SESSION_CACHE.get(symbol)
|
|
19
|
+
if cached and (time.time() - float(cached.get("ts", 0))) < TA_SESSION_CACHE_TTL:
|
|
20
|
+
return {**(cached.get("data") or {}), "_cached": True}
|
|
21
|
+
return {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _store_ta(symbol: str, data: dict[str, Any]) -> None:
|
|
25
|
+
if data.get("success"):
|
|
26
|
+
TA_SESSION_CACHE[symbol] = {"data": data, "ts": time.time()}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_analyze_prompt(symbol: str, context: str, is_cn: bool, response_lang: str | None = None) -> str:
|
|
30
|
+
"""Build the LLM prompt for /analyze from a prepared market context."""
|
|
31
|
+
|
|
32
|
+
symbol = symbol.upper()
|
|
33
|
+
lang = response_lang if response_lang in ("zh", "en") else ("zh" if is_cn else "en")
|
|
34
|
+
_no_explain = (
|
|
35
|
+
"注意:如某字段数据缺失,请直接省略该项,不要写'数据缺失'或'未提供'的说明文字。"
|
|
36
|
+
"对于无数据的章节整体跳过即可。\n"
|
|
37
|
+
if lang == "zh" else
|
|
38
|
+
"Note: If any data field is missing, skip that item entirely. Do not write 'data unavailable' or explain why it is missing.\n"
|
|
39
|
+
)
|
|
40
|
+
if lang == "zh":
|
|
41
|
+
return (
|
|
42
|
+
f"{context}\n\n"
|
|
43
|
+
f"{build_response_style_rule('zh')}"
|
|
44
|
+
f"请对以上 {symbol} 进行综合分析,包括:\n"
|
|
45
|
+
f"1. 技术面分析(趋势判断、支撑/阻力、指标信号)\n"
|
|
46
|
+
f"2. 基本面评估(估值合理性、盈利能力)\n"
|
|
47
|
+
f"3. 风险提示(简要 2-3 条)\n"
|
|
48
|
+
f"4. 综合建议(操作方向 + 关键价位参考)\n"
|
|
49
|
+
f"\n{_no_explain}"
|
|
50
|
+
)
|
|
51
|
+
return (
|
|
52
|
+
f"{context}\n\n"
|
|
53
|
+
f"{build_response_style_rule('en')}"
|
|
54
|
+
f"Please provide a comprehensive analysis of {symbol}, covering:\n"
|
|
55
|
+
f"1. Technical analysis (trend, support/resistance, indicator signals)\n"
|
|
56
|
+
f"2. Fundamentals (valuation, profitability — only if data is available)\n"
|
|
57
|
+
f"3. Risk assessment (2-3 key points)\n"
|
|
58
|
+
f"4. Summary outlook with key price levels\n"
|
|
59
|
+
f"\n{_no_explain}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def build_analyze_context(
|
|
64
|
+
symbol: str,
|
|
65
|
+
is_cn: bool,
|
|
66
|
+
*,
|
|
67
|
+
has_mdc: bool = False,
|
|
68
|
+
get_mdc: Callable[[], Any] | None = None,
|
|
69
|
+
ashare_name_lookup: Callable[[str], str | None] | None = None,
|
|
70
|
+
has_brokers: bool = False,
|
|
71
|
+
get_broker_registry: Callable[[], Any] | None = None,
|
|
72
|
+
logger: logging.Logger | None = None,
|
|
73
|
+
) -> str:
|
|
74
|
+
"""Fetch real market data and build a structured LLM context."""
|
|
75
|
+
|
|
76
|
+
log = logger or logging.getLogger(__name__)
|
|
77
|
+
loop = asyncio.get_event_loop()
|
|
78
|
+
ctx_lines: list[str] = [f"## {symbol} 市场数据" if is_cn else f"## {symbol} Market Data"]
|
|
79
|
+
|
|
80
|
+
quote: dict[str, Any] = {}
|
|
81
|
+
technical: dict[str, Any] = {}
|
|
82
|
+
quality: dict[str, Any] = {}
|
|
83
|
+
|
|
84
|
+
# ── Primary: cloud DataService bundle ────────────────────────────────────
|
|
85
|
+
try:
|
|
86
|
+
from packages.aria_services import data as service_data
|
|
87
|
+
try:
|
|
88
|
+
from datasources.router import get_router as get_data_router
|
|
89
|
+
router = get_data_router()
|
|
90
|
+
except Exception:
|
|
91
|
+
router = None
|
|
92
|
+
bundle = await loop.run_in_executor(
|
|
93
|
+
None,
|
|
94
|
+
lambda: service_data.DataService(router=router).bundle(
|
|
95
|
+
symbol, history_days=370, technical_days=120,
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
quote = bundle.quote or {}
|
|
99
|
+
technical = bundle.technical or {}
|
|
100
|
+
quality = dict(bundle.quality or {})
|
|
101
|
+
if not quality.get("providers") and getattr(bundle, "provider_chain", None):
|
|
102
|
+
quality["providers"] = list(getattr(bundle, "provider_chain", []) or [])
|
|
103
|
+
if not quality.get("missing_fields") and getattr(bundle, "missing_fields", None):
|
|
104
|
+
quality["missing_fields"] = list(getattr(bundle, "missing_fields", []) or [])
|
|
105
|
+
_store_ta(symbol, technical)
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
log.debug("analyze data service failed for %s: %s", symbol, exc)
|
|
108
|
+
|
|
109
|
+
# ── Fallback 1: market data client ────────────────────────────────────────
|
|
110
|
+
if not quote and has_mdc and get_mdc:
|
|
111
|
+
try:
|
|
112
|
+
mdc = get_mdc()
|
|
113
|
+
raw_q = await loop.run_in_executor(None, mdc.quote, symbol)
|
|
114
|
+
if raw_q:
|
|
115
|
+
quote = raw_q if isinstance(raw_q, dict) else (raw_q.to_dict() if hasattr(raw_q, "to_dict") else vars(raw_q))
|
|
116
|
+
if quote.get("price"):
|
|
117
|
+
raw_ta = await loop.run_in_executor(None, mdc.technical_indicators, symbol, 120)
|
|
118
|
+
if isinstance(raw_ta, dict) and raw_ta.get("success"):
|
|
119
|
+
technical = raw_ta
|
|
120
|
+
_store_ta(symbol, technical)
|
|
121
|
+
else:
|
|
122
|
+
technical = _cached_ta(symbol)
|
|
123
|
+
except Exception:
|
|
124
|
+
technical = _cached_ta(symbol)
|
|
125
|
+
|
|
126
|
+
# ── Fallback 2: DataRouter (yfinance direct) ─────────────────────────────
|
|
127
|
+
if not quote or not quote.get("price"):
|
|
128
|
+
try:
|
|
129
|
+
from datasources.router import DataRouter
|
|
130
|
+
router_direct = DataRouter()
|
|
131
|
+
raw_q = await loop.run_in_executor(None, router_direct.quote, symbol)
|
|
132
|
+
if raw_q:
|
|
133
|
+
quote = raw_q.to_dict() if hasattr(raw_q, "to_dict") else vars(raw_q)
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
log.debug("DataRouter.quote fallback failed for %s: %s", symbol, exc)
|
|
136
|
+
|
|
137
|
+
# If no technical from above, try cached
|
|
138
|
+
if not technical:
|
|
139
|
+
technical = _cached_ta(symbol)
|
|
140
|
+
|
|
141
|
+
if quality:
|
|
142
|
+
ctx_lines.append(f"\n### {'数据质量' if is_cn else 'Data Quality'}")
|
|
143
|
+
status = quality.get("status")
|
|
144
|
+
if status:
|
|
145
|
+
ctx_lines.append(f"- {'状态' if is_cn else 'Status'}: {status}")
|
|
146
|
+
providers = quality.get("providers") or []
|
|
147
|
+
if providers:
|
|
148
|
+
ctx_lines.append(f"- {'数据源' if is_cn else 'Providers'}: {', '.join(map(str, providers))}")
|
|
149
|
+
missing = quality.get("missing_fields") or []
|
|
150
|
+
if missing:
|
|
151
|
+
ctx_lines.append(f"- {'缺失字段' if is_cn else 'Missing fields'}: {', '.join(map(str, missing))}")
|
|
152
|
+
warnings = quality.get("warnings") or []
|
|
153
|
+
if warnings:
|
|
154
|
+
ctx_lines.append(f"- {'警告' if is_cn else 'Warnings'}: {'; '.join(map(str, warnings[:3]))}")
|
|
155
|
+
|
|
156
|
+
# ── Price / header line ───────────────────────────────────────────────────
|
|
157
|
+
price = quote.get("price") if quote else None
|
|
158
|
+
change_pct = quote.get("change_pct") if quote else None
|
|
159
|
+
name = (quote.get("name") or symbol) if quote else symbol
|
|
160
|
+
|
|
161
|
+
if price and float(price) == 0.0:
|
|
162
|
+
price = None # treat 0 as missing
|
|
163
|
+
|
|
164
|
+
if is_cn and (not name or name == symbol or str(name).isascii()):
|
|
165
|
+
try:
|
|
166
|
+
cn_name = ashare_name_lookup(symbol) if ashare_name_lookup else None
|
|
167
|
+
if cn_name:
|
|
168
|
+
name = cn_name
|
|
169
|
+
except Exception as exc:
|
|
170
|
+
log.debug("ashare name lookup failed for %s: %s", symbol, exc)
|
|
171
|
+
|
|
172
|
+
if price:
|
|
173
|
+
chg_str = f"{float(change_pct):+.2f}%" if change_pct is not None else ""
|
|
174
|
+
header = f"- 价格: {float(price):.2f}" if is_cn else f"- Price: {float(price):.2f}"
|
|
175
|
+
if chg_str:
|
|
176
|
+
header += f" ({chg_str})"
|
|
177
|
+
if name and name != symbol:
|
|
178
|
+
header += f" [{name}]"
|
|
179
|
+
ctx_lines.append(header)
|
|
180
|
+
# 52-week range
|
|
181
|
+
h52 = quote.get("high_52w", 0)
|
|
182
|
+
l52 = quote.get("low_52w", 0)
|
|
183
|
+
if h52 and l52 and float(h52) > 0:
|
|
184
|
+
ctx_lines.append(
|
|
185
|
+
f"- 52周区间: {float(l52):.2f} — {float(h52):.2f}"
|
|
186
|
+
if is_cn else
|
|
187
|
+
f"- 52-week range: {float(l52):.2f} — {float(h52):.2f}"
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
ctx_lines.append(
|
|
191
|
+
"- 价格: 获取失败(稍后重试或配置数据服务 key)"
|
|
192
|
+
if is_cn else
|
|
193
|
+
"- Price: unavailable (configure a data service key via /apikey)"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# ── Technical indicators ──────────────────────────────────────────────────
|
|
197
|
+
rsi = technical.get("rsi")
|
|
198
|
+
macd_hist= technical.get("macd_hist")
|
|
199
|
+
ma20 = technical.get("ma20")
|
|
200
|
+
ma60 = technical.get("ma60")
|
|
201
|
+
bb_upper = technical.get("bb_upper")
|
|
202
|
+
bb_lower = technical.get("bb_lower")
|
|
203
|
+
|
|
204
|
+
has_tech = any(v is not None for v in (rsi, macd_hist, ma20))
|
|
205
|
+
if has_tech:
|
|
206
|
+
ctx_lines.append(f"\n### {'技术指标' if is_cn else 'Technical Indicators'}")
|
|
207
|
+
if rsi is not None:
|
|
208
|
+
rsi_desc = ("超买" if rsi > 70 else "超卖" if rsi < 30 else "中性") if is_cn else (
|
|
209
|
+
"Overbought" if rsi > 70 else "Oversold" if rsi < 30 else "Neutral")
|
|
210
|
+
ctx_lines.append(f"- RSI (14): {rsi:.1f} [{rsi_desc}]")
|
|
211
|
+
if macd_hist is not None:
|
|
212
|
+
trend = ("多头 Bullish" if macd_hist > 0 else "空头 Bearish") if is_cn else (
|
|
213
|
+
"Bullish" if macd_hist > 0 else "Bearish")
|
|
214
|
+
ctx_lines.append(f"- MACD 柱状图: {macd_hist:.4f} [{trend}]" if is_cn else
|
|
215
|
+
f"- MACD histogram: {macd_hist:.4f} [{trend}]")
|
|
216
|
+
if ma20 and price:
|
|
217
|
+
rel = ("上方 ↑" if float(price) > ma20 else "下方 ↓") if is_cn else (
|
|
218
|
+
"above ↑" if float(price) > ma20 else "below ↓")
|
|
219
|
+
ctx_lines.append(f"- MA20: {ma20:.2f} [价格在{rel}]" if is_cn else
|
|
220
|
+
f"- MA20: {ma20:.2f} [Price {rel}]")
|
|
221
|
+
if ma60 and price:
|
|
222
|
+
rel = ("上方 ↑" if float(price) > ma60 else "下方 ↓") if is_cn else (
|
|
223
|
+
"above ↑" if float(price) > ma60 else "below ↓")
|
|
224
|
+
ctx_lines.append(f"- MA60: {ma60:.2f} [价格在{rel}]" if is_cn else
|
|
225
|
+
f"- MA60: {ma60:.2f} [Price {rel}]")
|
|
226
|
+
if bb_upper and bb_lower:
|
|
227
|
+
ctx_lines.append(f"- 布林带: {bb_lower:.2f} — {bb_upper:.2f}" if is_cn else
|
|
228
|
+
f"- Bollinger Bands: {bb_lower:.2f} — {bb_upper:.2f}")
|
|
229
|
+
if technical.get("_cached"):
|
|
230
|
+
ctx_lines.append(" [以上技术指标来自缓存]" if is_cn else " [Technical data from session cache]")
|
|
231
|
+
|
|
232
|
+
# ── Support / resistance (computed from history) ──────────────────────────
|
|
233
|
+
await _append_support_resistance(ctx_lines, symbol, is_cn, price, ma20, ma60, bb_upper, bb_lower, loop, log)
|
|
234
|
+
|
|
235
|
+
# ── Fundamentals ──────────────────────────────────────────────────────────
|
|
236
|
+
await _append_fundamentals(ctx_lines, symbol, is_cn, loop, log, quote)
|
|
237
|
+
|
|
238
|
+
# ── Broker position ───────────────────────────────────────────────────────
|
|
239
|
+
_append_broker_position(
|
|
240
|
+
ctx_lines, symbol, is_cn,
|
|
241
|
+
has_brokers=has_brokers, get_broker_registry=get_broker_registry, logger=log,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return "\n".join(ctx_lines)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
async def _append_support_resistance(
|
|
248
|
+
ctx_lines: list[str],
|
|
249
|
+
symbol: str,
|
|
250
|
+
is_cn: bool,
|
|
251
|
+
price: Optional[float],
|
|
252
|
+
ma20: Optional[float],
|
|
253
|
+
ma60: Optional[float],
|
|
254
|
+
bb_upper: Optional[float],
|
|
255
|
+
bb_lower: Optional[float],
|
|
256
|
+
loop,
|
|
257
|
+
log: logging.Logger,
|
|
258
|
+
) -> None:
|
|
259
|
+
"""Compute support/resistance from price history and key MAs."""
|
|
260
|
+
levels: dict[str, list[float]] = {"support": [], "resistance": []}
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
from datasources.router import DataRouter
|
|
264
|
+
hist_result = await loop.run_in_executor(None, DataRouter().history, symbol, 90)
|
|
265
|
+
if hist_result and hist_result.data is not None and not hist_result.data.empty:
|
|
266
|
+
df = hist_result.data
|
|
267
|
+
close_col = next((c for c in df.columns if "close" in c.lower()), None)
|
|
268
|
+
high_col = next((c for c in df.columns if "high" in c.lower()), None)
|
|
269
|
+
low_col = next((c for c in df.columns if "low" in c.lower()), None)
|
|
270
|
+
|
|
271
|
+
if close_col and len(df) >= 10:
|
|
272
|
+
closes = df[close_col].dropna()
|
|
273
|
+
highs = df[high_col].dropna() if high_col else closes
|
|
274
|
+
lows = df[low_col].dropna() if low_col else closes
|
|
275
|
+
|
|
276
|
+
# Rolling 10-day swing highs (local maxima)
|
|
277
|
+
for i in range(5, len(highs) - 5):
|
|
278
|
+
window = highs.iloc[i-5:i+5]
|
|
279
|
+
if float(highs.iloc[i]) == float(window.max()):
|
|
280
|
+
levels["resistance"].append(float(highs.iloc[i]))
|
|
281
|
+
# Rolling 10-day swing lows (local minima)
|
|
282
|
+
for i in range(5, len(lows) - 5):
|
|
283
|
+
window = lows.iloc[i-5:i+5]
|
|
284
|
+
if float(lows.iloc[i]) == float(window.min()):
|
|
285
|
+
levels["support"].append(float(lows.iloc[i]))
|
|
286
|
+
except Exception as exc:
|
|
287
|
+
log.debug("support/resistance history failed for %s: %s", symbol, exc)
|
|
288
|
+
|
|
289
|
+
# Add MA lines as dynamic support/resistance
|
|
290
|
+
if price:
|
|
291
|
+
p = float(price)
|
|
292
|
+
if ma20:
|
|
293
|
+
(levels["support"] if p > ma20 else levels["resistance"]).append(ma20)
|
|
294
|
+
if ma60:
|
|
295
|
+
(levels["support"] if p > ma60 else levels["resistance"]).append(ma60)
|
|
296
|
+
# Bollinger bands
|
|
297
|
+
if bb_lower:
|
|
298
|
+
levels["support"].append(bb_lower)
|
|
299
|
+
if bb_upper:
|
|
300
|
+
levels["resistance"].append(bb_upper)
|
|
301
|
+
|
|
302
|
+
if levels["support"] or levels["resistance"]:
|
|
303
|
+
ctx_lines.append(f"\n### {'关键价位' if is_cn else 'Key Price Levels'}")
|
|
304
|
+
|
|
305
|
+
# Pick the 3 nearest support levels below price
|
|
306
|
+
sup = sorted(set(round(v, 2) for v in levels["support"] if v < p), reverse=True)[:3]
|
|
307
|
+
# Pick the 3 nearest resistance levels above price
|
|
308
|
+
res = sorted(set(round(v, 2) for v in levels["resistance"] if v > p))[:3]
|
|
309
|
+
|
|
310
|
+
if sup:
|
|
311
|
+
sup_str = " / ".join(f"{v:.2f}" for v in sup)
|
|
312
|
+
ctx_lines.append(f"- 支撑位: {sup_str}" if is_cn else f"- Support: {sup_str}")
|
|
313
|
+
if res:
|
|
314
|
+
res_str = " / ".join(f"{v:.2f}" for v in res)
|
|
315
|
+
ctx_lines.append(f"- 阻力位: {res_str}" if is_cn else f"- Resistance: {res_str}")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
async def _append_fundamentals(
|
|
319
|
+
ctx_lines: list[str],
|
|
320
|
+
symbol: str,
|
|
321
|
+
is_cn: bool,
|
|
322
|
+
loop,
|
|
323
|
+
log: logging.Logger,
|
|
324
|
+
quote: dict[str, Any],
|
|
325
|
+
) -> None:
|
|
326
|
+
fund_lines: list[str] = []
|
|
327
|
+
|
|
328
|
+
# Try DataRouter (yfinance / alpha_vantage / edgar chain)
|
|
329
|
+
try:
|
|
330
|
+
from datasources.router import DataRouter
|
|
331
|
+
fund = await loop.run_in_executor(None, DataRouter().fundamentals, symbol)
|
|
332
|
+
if fund:
|
|
333
|
+
def _row(label_cn: str, label_en: str, val: Optional[float], fmt: str = ".2f") -> None:
|
|
334
|
+
if val is not None and val != 0.0:
|
|
335
|
+
formatted = f"{val:{fmt}}"
|
|
336
|
+
fund_lines.append(f"- {label_cn}: {formatted}" if is_cn else f"- {label_en}: {formatted}")
|
|
337
|
+
|
|
338
|
+
_row("市盈率 (TTM)", "P/E ratio (TTM)", fund.pe_ttm)
|
|
339
|
+
_row("市净率", "Price-to-Book", fund.pb)
|
|
340
|
+
_row("ROE", "Return on Equity", fund.roe, ".2f")
|
|
341
|
+
_row("营收增速", "Revenue Growth (YoY)", fund.revenue_growth, ".2f")
|
|
342
|
+
_row("净利增速", "Earnings Growth (YoY)", fund.net_profit_growth, ".2f")
|
|
343
|
+
_row("股息率", "Dividend Yield", fund.dividend_yield, ".2f")
|
|
344
|
+
|
|
345
|
+
# Market cap: format nicely (USD or CNY)
|
|
346
|
+
if fund.total_mv and fund.total_mv > 0:
|
|
347
|
+
mv = fund.total_mv
|
|
348
|
+
if mv >= 1e12:
|
|
349
|
+
mv_str = f"{mv/1e12:.2f}T"
|
|
350
|
+
elif mv >= 1e9:
|
|
351
|
+
mv_str = f"{mv/1e9:.2f}B"
|
|
352
|
+
elif mv >= 1e8:
|
|
353
|
+
mv_str = f"{mv/1e8:.2f}亿" if is_cn else f"{mv/1e9:.2f}B"
|
|
354
|
+
else:
|
|
355
|
+
mv_str = f"{mv:,.0f}"
|
|
356
|
+
fund_lines.append(f"- 总市值: {mv_str}" if is_cn else f"- Market Cap: {mv_str}")
|
|
357
|
+
|
|
358
|
+
if fund.source:
|
|
359
|
+
fund_lines.append(f" [数据源: {fund.source}]" if is_cn else f" [source: {fund.source}]")
|
|
360
|
+
except Exception as exc:
|
|
361
|
+
log.debug("fundamentals fetch failed for %s: %s", symbol, exc)
|
|
362
|
+
|
|
363
|
+
# Also try quote-level PE/PB if fundamentals didn't yield anything
|
|
364
|
+
if not fund_lines and quote:
|
|
365
|
+
pe = quote.get("pe_ttm", 0)
|
|
366
|
+
pb = quote.get("pb", 0)
|
|
367
|
+
if pe and float(pe) > 0:
|
|
368
|
+
fund_lines.append(f"- 市盈率 (TTM): {float(pe):.2f}" if is_cn else f"- P/E ratio (TTM): {float(pe):.2f}")
|
|
369
|
+
if pb and float(pb) > 0:
|
|
370
|
+
fund_lines.append(f"- 市净率: {float(pb):.2f}" if is_cn else f"- Price-to-Book: {float(pb):.2f}")
|
|
371
|
+
|
|
372
|
+
if fund_lines:
|
|
373
|
+
ctx_lines.append(f"\n### {'基本面' if is_cn else 'Fundamentals'}")
|
|
374
|
+
ctx_lines.extend(fund_lines)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _append_broker_position(
|
|
378
|
+
ctx_lines: list[str],
|
|
379
|
+
symbol: str,
|
|
380
|
+
is_cn: bool,
|
|
381
|
+
*,
|
|
382
|
+
has_brokers: bool,
|
|
383
|
+
get_broker_registry: Callable[[], Any] | None,
|
|
384
|
+
logger: logging.Logger,
|
|
385
|
+
) -> None:
|
|
386
|
+
if not has_brokers or not get_broker_registry:
|
|
387
|
+
return
|
|
388
|
+
try:
|
|
389
|
+
registry = get_broker_registry()
|
|
390
|
+
broker = registry.active()
|
|
391
|
+
if not broker or not broker.is_connected:
|
|
392
|
+
return
|
|
393
|
+
positions = broker.positions()
|
|
394
|
+
symbol_norm = symbol.lstrip("0").upper()
|
|
395
|
+
match = None
|
|
396
|
+
for position in positions:
|
|
397
|
+
pos_symbol = str(position.symbol or "").lstrip("0").upper()
|
|
398
|
+
if pos_symbol == symbol_norm or pos_symbol.startswith(symbol_norm) or symbol_norm.startswith(pos_symbol):
|
|
399
|
+
match = position
|
|
400
|
+
break
|
|
401
|
+
ctx_lines.append(f"\n### {'我的持仓' if is_cn else 'Your Position'}")
|
|
402
|
+
if not match:
|
|
403
|
+
ctx_lines.append(f"- 当前未持有此股 [{broker.label}]" if is_cn else f"- Not currently held [{broker.label}]")
|
|
404
|
+
return
|
|
405
|
+
qty = getattr(match, "quantity", None) or getattr(match, "qty", None)
|
|
406
|
+
cost = getattr(match, "cost_price", None) or getattr(match, "avg_cost", None)
|
|
407
|
+
pnl = getattr(match, "pnl", None)
|
|
408
|
+
pnl_pct = getattr(match, "pnl_pct", None)
|
|
409
|
+
market_value = getattr(match, "market_value", None)
|
|
410
|
+
ctx_lines.append(f"- 持有: 是 [{broker.label}]" if is_cn else f"- Held: Yes [{broker.label}]")
|
|
411
|
+
if qty is not None:
|
|
412
|
+
ctx_lines.append(f"- 持仓量: {qty:,}" if is_cn else f"- Quantity: {qty:,}")
|
|
413
|
+
if cost is not None:
|
|
414
|
+
ctx_lines.append(f"- 成本价: {cost:.3f}" if is_cn else f"- Avg Cost: {cost:.3f}")
|
|
415
|
+
if market_value is not None:
|
|
416
|
+
ctx_lines.append(f"- 市值: {market_value:,.2f}" if is_cn else f"- Market Value: {market_value:,.2f}")
|
|
417
|
+
if pnl is not None and pnl_pct is not None:
|
|
418
|
+
sign = "+" if pnl >= 0 else ""
|
|
419
|
+
ctx_lines.append(
|
|
420
|
+
f"- 浮动盈亏: {sign}{pnl:,.2f} ({sign}{pnl_pct:.2f}%)"
|
|
421
|
+
if is_cn else
|
|
422
|
+
f"- Unrealized P&L: {sign}{pnl:,.2f} ({sign}{pnl_pct:.2f}%)"
|
|
423
|
+
)
|
|
424
|
+
except Exception as exc:
|
|
425
|
+
logger.debug("broker position lookup failed for %s: %s", symbol, exc)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Backwards-compatibility shim — canonical code lives in ui/render/market.py."""
|
|
2
|
+
from ui.render.market import * # noqa: F401, F403
|
|
3
|
+
from ui.render.market import (
|
|
4
|
+
print_quote_result, print_ta_result,
|
|
5
|
+
render_quote_plain, render_ta_plain,
|
|
6
|
+
compact_quote_market_cap,
|
|
7
|
+
)
|