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,112 @@
|
|
|
1
|
+
"""Stateless file / code search tools — thin wrappers around WorkspaceFiles.
|
|
2
|
+
|
|
3
|
+
These functions are pure: they take a params dict, call WorkspaceFiles(), and
|
|
4
|
+
return a result dict. No console/HAS_RICH/write-policy state involved.
|
|
5
|
+
|
|
6
|
+
They are registered in aria_cli.py's execute_aria_tool dispatch table via:
|
|
7
|
+
|
|
8
|
+
from apps.cli.tools.file_tools import tool_read_file, tool_list_files, ...
|
|
9
|
+
|
|
10
|
+
"read_file": tool_read_file,
|
|
11
|
+
"list_files": tool_list_files,
|
|
12
|
+
"search_code": tool_search_code,
|
|
13
|
+
"glob": tool_glob,
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import pathlib
|
|
19
|
+
import sys
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
# WorkspaceFiles lives at the aria-code root; insert it when running tests
|
|
23
|
+
_ROOT = pathlib.Path(__file__).parent.parent.parent.parent # aria-code/
|
|
24
|
+
if str(_ROOT) not in sys.path:
|
|
25
|
+
sys.path.insert(0, str(_ROOT))
|
|
26
|
+
|
|
27
|
+
from workspace import WorkspaceFiles # noqa: E402
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def tool_read_file(params: dict) -> dict:
|
|
31
|
+
"""Read file contents with optional line range."""
|
|
32
|
+
path = params.get("path", "")
|
|
33
|
+
if not path:
|
|
34
|
+
return {"success": False, "error": "Missing 'path' parameter"}
|
|
35
|
+
try:
|
|
36
|
+
offset = int(params.get("offset", 0) or 0)
|
|
37
|
+
limit = int(params.get("limit", 0) or 0)
|
|
38
|
+
if not offset and not limit:
|
|
39
|
+
limit = 160
|
|
40
|
+
result = WorkspaceFiles().read_file(path, offset=offset, limit=limit)
|
|
41
|
+
content = result.content
|
|
42
|
+
if limit and result.lines >= limit and "use offset/limit to read more" not in content:
|
|
43
|
+
content += "\n... [default read limit applied — use offset/limit to read more]"
|
|
44
|
+
return {"success": True, "data": {
|
|
45
|
+
"path": result.path,
|
|
46
|
+
"lines": result.lines,
|
|
47
|
+
"content": content,
|
|
48
|
+
}}
|
|
49
|
+
except Exception as e:
|
|
50
|
+
return {"success": False, "error": str(e)}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def tool_list_files(params: dict) -> dict:
|
|
54
|
+
"""List files in a directory, optionally matching a glob pattern."""
|
|
55
|
+
path = params.get("path", ".")
|
|
56
|
+
pattern = params.get("pattern", "*")
|
|
57
|
+
try:
|
|
58
|
+
data = WorkspaceFiles().list_files(path, pattern)
|
|
59
|
+
return {"success": True, "data": {
|
|
60
|
+
"path": data["path"],
|
|
61
|
+
"pattern": data["pattern"],
|
|
62
|
+
"count": data["count"],
|
|
63
|
+
"items": data["items"],
|
|
64
|
+
}}
|
|
65
|
+
except Exception as e:
|
|
66
|
+
return {"success": False, "error": str(e)}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def tool_search_code(params: dict) -> dict:
|
|
70
|
+
"""Search for a pattern in files (like grep)."""
|
|
71
|
+
pattern = params.get("pattern", "")
|
|
72
|
+
path = params.get("path", ".")
|
|
73
|
+
file_glob = params.get("glob", "**/*.py")
|
|
74
|
+
if not pattern:
|
|
75
|
+
return {"success": False, "error": "Missing 'pattern' parameter"}
|
|
76
|
+
try:
|
|
77
|
+
data = WorkspaceFiles().search_code(pattern, path, file_glob)
|
|
78
|
+
return {"success": True, "data": {
|
|
79
|
+
"pattern": data["pattern"],
|
|
80
|
+
"path": data["path"],
|
|
81
|
+
"count": data["count"],
|
|
82
|
+
"matches": data["matches"],
|
|
83
|
+
}}
|
|
84
|
+
except Exception as e:
|
|
85
|
+
return {"success": False, "error": str(e)}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def tool_glob(params: dict) -> dict:
|
|
89
|
+
"""Fast file-pattern search (supports ** recursive globs).
|
|
90
|
+
|
|
91
|
+
Returns a flat sorted list of matching file paths up to *limit* entries.
|
|
92
|
+
"""
|
|
93
|
+
pattern = params.get("pattern", "**/*")
|
|
94
|
+
root = (params.get("path", ".") or ".").strip()
|
|
95
|
+
limit = min(int(params.get("limit", 200)), 1000)
|
|
96
|
+
try:
|
|
97
|
+
p = pathlib.Path(root).expanduser().resolve()
|
|
98
|
+
if not p.is_dir():
|
|
99
|
+
return {"success": False, "error": f"Directory not found: {p}"}
|
|
100
|
+
results = sorted(
|
|
101
|
+
str(fp.relative_to(p) if fp.is_relative_to(p) else fp)
|
|
102
|
+
for fp in p.glob(pattern)
|
|
103
|
+
if fp.is_file()
|
|
104
|
+
)[:limit]
|
|
105
|
+
return {"success": True, "data": {
|
|
106
|
+
"pattern": pattern,
|
|
107
|
+
"root": str(p),
|
|
108
|
+
"count": len(results),
|
|
109
|
+
"files": results,
|
|
110
|
+
}}
|
|
111
|
+
except Exception as e:
|
|
112
|
+
return {"success": False, "error": str(e)}
|
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
"""Market data and broker tools extracted from aria_cli.py.
|
|
2
|
+
|
|
3
|
+
Lazy-imports ``market_data_client`` and ``brokers`` so the module loads
|
|
4
|
+
even when those optional packages are absent. The import guards mirror
|
|
5
|
+
aria_cli.py lines 134-189 so the same failure modes apply.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
_ROOT = Path(__file__).parent.parent.parent.parent # aria-code/
|
|
16
|
+
if str(_ROOT) not in sys.path:
|
|
17
|
+
sys.path.insert(0, str(_ROOT))
|
|
18
|
+
|
|
19
|
+
from apps.cli.market_metadata import enrich_market_quote
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# ── Optional: market data client ────────────────────────────────────────────
|
|
24
|
+
try:
|
|
25
|
+
from market_data_client import get_mdc as _get_mdc # noqa: E402
|
|
26
|
+
_HAS_MDC = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
_HAS_MDC = False
|
|
29
|
+
_get_mdc = None # type: ignore[assignment]
|
|
30
|
+
|
|
31
|
+
# ── Optional: broker integration ────────────────────────────────────────────
|
|
32
|
+
try:
|
|
33
|
+
from brokers import ( # noqa: E402
|
|
34
|
+
get_registry as _get_broker_registry,
|
|
35
|
+
list_broker_configs as _list_broker_configs,
|
|
36
|
+
BROKERS_CONFIG_PATH as _BROKERS_CONFIG_PATH,
|
|
37
|
+
)
|
|
38
|
+
_HAS_BROKERS = True
|
|
39
|
+
except ImportError:
|
|
40
|
+
_HAS_BROKERS = False
|
|
41
|
+
def _get_broker_registry(): return None # type: ignore[return-value]
|
|
42
|
+
def _list_broker_configs(): return []
|
|
43
|
+
_BROKERS_CONFIG_PATH = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get_finnhub_key() -> str:
|
|
47
|
+
"""Read Finnhub API key from env or ~/.arthera/providers.json."""
|
|
48
|
+
val = os.getenv("FINNHUB_API_KEY", "")
|
|
49
|
+
if val:
|
|
50
|
+
return val
|
|
51
|
+
providers_file = Path.home() / ".arthera" / "providers.json"
|
|
52
|
+
try:
|
|
53
|
+
if providers_file.exists():
|
|
54
|
+
raw = json.loads(providers_file.read_text(encoding="utf-8"))
|
|
55
|
+
for section in ("llm", "data"):
|
|
56
|
+
entry = raw.get(section, {}).get("finnhub", {})
|
|
57
|
+
if entry.get("api_key"):
|
|
58
|
+
return entry["api_key"]
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
return ""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def tool_get_market_data(params: dict) -> dict:
|
|
65
|
+
"""Fetch real-time quote + technical indicators for any stock/ETF/crypto.
|
|
66
|
+
|
|
67
|
+
Supports A-shares (6-digit code), HK (.HK), US tickers, crypto.
|
|
68
|
+
Primary source: MarketDataClient. Fallback: Finnhub (US/global only).
|
|
69
|
+
"""
|
|
70
|
+
symbol = str(params.get("symbol", "")).strip().upper()
|
|
71
|
+
if not symbol:
|
|
72
|
+
return {"success": False, "error": "symbol is required"}
|
|
73
|
+
symbol_base = symbol.rsplit(".", 1)[0] if symbol.endswith((".SZ", ".SS", ".SH")) else symbol
|
|
74
|
+
is_ashare_symbol = (
|
|
75
|
+
symbol_base.isdigit() and len(symbol_base) == 6
|
|
76
|
+
) or (
|
|
77
|
+
symbol_base.startswith(("SH", "SZ"))
|
|
78
|
+
and symbol_base[2:].isdigit()
|
|
79
|
+
and len(symbol_base[2:]) == 6
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# ── 1. Quote ─────────────────────────────────────────────────────────────
|
|
83
|
+
quote: dict = {"success": False, "error": "market data client unavailable"}
|
|
84
|
+
if _HAS_MDC and _get_mdc is not None:
|
|
85
|
+
import time as _t
|
|
86
|
+
mdc = _get_mdc()
|
|
87
|
+
for _att in range(3):
|
|
88
|
+
try:
|
|
89
|
+
quote = mdc.quote(symbol)
|
|
90
|
+
if quote.get("success"):
|
|
91
|
+
break
|
|
92
|
+
_e = str(quote.get("error", "")).lower()
|
|
93
|
+
if ("rate" in _e or "429" in _e) and _att < 2:
|
|
94
|
+
_t.sleep(2 ** _att)
|
|
95
|
+
continue
|
|
96
|
+
break
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
_es = str(exc).lower()
|
|
99
|
+
if ("rate" in _es or "429" in _es) and _att < 2:
|
|
100
|
+
_t.sleep(2 ** _att)
|
|
101
|
+
continue
|
|
102
|
+
_raw = str(exc)
|
|
103
|
+
if "Connection aborted" in _raw or "RemoteDisconnected" in _raw:
|
|
104
|
+
quote = {"success": False, "error": "网络连接中断,请稍后重试"}
|
|
105
|
+
elif "Connection refused" in _raw:
|
|
106
|
+
quote = {"success": False, "error": "连接被拒绝,数据服务暂时不可用"}
|
|
107
|
+
elif "timeout" in _raw.lower():
|
|
108
|
+
quote = {"success": False, "error": "连接超时,请稍后重试"}
|
|
109
|
+
else:
|
|
110
|
+
quote = {"success": False, "error": _raw}
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
# Finnhub fallback for US/global symbols
|
|
114
|
+
if not quote.get("success"):
|
|
115
|
+
_fh_key = _get_finnhub_key()
|
|
116
|
+
if _fh_key:
|
|
117
|
+
try:
|
|
118
|
+
import requests as _rq
|
|
119
|
+
_r = _rq.get(
|
|
120
|
+
f"https://finnhub.io/api/v1/quote?symbol={symbol}&token={_fh_key}",
|
|
121
|
+
timeout=6,
|
|
122
|
+
)
|
|
123
|
+
if _r.status_code == 200:
|
|
124
|
+
_fh = _r.json()
|
|
125
|
+
if _fh.get("c"):
|
|
126
|
+
quote = {
|
|
127
|
+
"success": True, "symbol": symbol,
|
|
128
|
+
"price": round(_fh["c"], 4),
|
|
129
|
+
"change_pct": round(float(_fh.get("dp") or 0), 4),
|
|
130
|
+
"high": round(_fh.get("h", 0), 4),
|
|
131
|
+
"low": round(_fh.get("l", 0), 4),
|
|
132
|
+
"currency": "USD", "provider": "finnhub",
|
|
133
|
+
}
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
if not quote.get("success"):
|
|
138
|
+
return {
|
|
139
|
+
"success": False,
|
|
140
|
+
"symbol": symbol,
|
|
141
|
+
"market": "CN" if is_ashare_symbol else "GLOBAL",
|
|
142
|
+
"provider_chain": quote.get("provider_chain") or (
|
|
143
|
+
["eastmoney", "akshare", "yfinance"] if is_ashare_symbol
|
|
144
|
+
else ["yfinance", "finnhub"]
|
|
145
|
+
),
|
|
146
|
+
"error": quote.get("error") or "行情数据源暂时不可用,请稍后重试。",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
quote = enrich_market_quote(symbol, quote)
|
|
150
|
+
result = {
|
|
151
|
+
"success": True,
|
|
152
|
+
"symbol": symbol,
|
|
153
|
+
"name": quote.get("name") or symbol,
|
|
154
|
+
"price": quote.get("price"),
|
|
155
|
+
"change_pct": quote.get("change_pct"),
|
|
156
|
+
"high": quote.get("high"),
|
|
157
|
+
"low": quote.get("low"),
|
|
158
|
+
"volume": quote.get("volume"),
|
|
159
|
+
"market_cap": quote.get("market_cap"),
|
|
160
|
+
"currency": quote.get("currency") or "USD",
|
|
161
|
+
"provider": quote.get("provider") or "market_data_client",
|
|
162
|
+
"provider_chain": quote.get("provider_chain") or [
|
|
163
|
+
quote.get("provider") or "market_data_client"
|
|
164
|
+
],
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# ── 2. Technical indicators ───────────────────────────────────────────────
|
|
168
|
+
ti: dict = {}
|
|
169
|
+
if _HAS_MDC and _get_mdc is not None:
|
|
170
|
+
try:
|
|
171
|
+
ti = mdc.technical_indicators(symbol, days=120) or {} # type: ignore[name-defined]
|
|
172
|
+
except Exception:
|
|
173
|
+
ti = {}
|
|
174
|
+
|
|
175
|
+
if (not ti.get("success") or ti.get("rsi") is None) and not is_ashare_symbol:
|
|
176
|
+
import yfinance as _yf
|
|
177
|
+
import numpy as _np
|
|
178
|
+
from datetime import date as _date, timedelta as _td
|
|
179
|
+
_yf_sym = symbol
|
|
180
|
+
if symbol.isdigit() and len(symbol) == 6:
|
|
181
|
+
_yf_sym = symbol + (".SS" if symbol.startswith("6") else ".SZ")
|
|
182
|
+
|
|
183
|
+
def _compute_ta(df) -> dict:
|
|
184
|
+
_c = df["Close"] if "Close" in df.columns else df.iloc[:, 0]
|
|
185
|
+
_v = df["Volume"] if "Volume" in df.columns else None
|
|
186
|
+
_d = _c.diff()
|
|
187
|
+
_g = _d.clip(lower=0).rolling(14).mean()
|
|
188
|
+
_l = (-_d.clip(upper=0)).rolling(14).mean()
|
|
189
|
+
_rsi_val = float((100 - 100 / (1 + _g / _l.replace(0, _np.nan))).iloc[-1])
|
|
190
|
+
_ema12 = _c.ewm(span=12).mean()
|
|
191
|
+
_ema26 = _c.ewm(span=26).mean()
|
|
192
|
+
_macd = _ema12 - _ema26
|
|
193
|
+
_mhist = float((_macd - _macd.ewm(span=9).mean()).iloc[-1])
|
|
194
|
+
_ma20 = _c.rolling(20).mean()
|
|
195
|
+
_std20 = _c.rolling(20).std()
|
|
196
|
+
_ma60 = _c.rolling(60).mean() if len(_c) >= 60 else _ma20
|
|
197
|
+
r = {
|
|
198
|
+
"success": True,
|
|
199
|
+
"rsi": round(_rsi_val, 2) if not _np.isnan(_rsi_val) else None,
|
|
200
|
+
"macd_hist": round(_mhist, 4),
|
|
201
|
+
"ma20": round(float(_ma20.iloc[-1]), 2),
|
|
202
|
+
"ma60": round(float(_ma60.iloc[-1]), 2),
|
|
203
|
+
"bb_upper": round(float((_ma20 + 2 * _std20).iloc[-1]), 2),
|
|
204
|
+
"bb_lower": round(float((_ma20 - 2 * _std20).iloc[-1]), 2),
|
|
205
|
+
}
|
|
206
|
+
if _v is not None and result.get("volume") is None:
|
|
207
|
+
_rv = _v.iloc[-1]
|
|
208
|
+
if not _np.isnan(_rv):
|
|
209
|
+
result["volume"] = int(_rv)
|
|
210
|
+
return r
|
|
211
|
+
|
|
212
|
+
_df_ta = None
|
|
213
|
+
try:
|
|
214
|
+
_df_ta = _yf.Ticker(_yf_sym).history(period="6mo", auto_adjust=True)
|
|
215
|
+
if _df_ta.empty:
|
|
216
|
+
_df_ta = None
|
|
217
|
+
except Exception:
|
|
218
|
+
_df_ta = None
|
|
219
|
+
|
|
220
|
+
if _df_ta is None or len(_df_ta) < 20:
|
|
221
|
+
try:
|
|
222
|
+
_start = (_date.today() - _td(days=185)).isoformat()
|
|
223
|
+
_df_ta = _yf.download(
|
|
224
|
+
_yf_sym, start=_start, auto_adjust=True, progress=False, timeout=15
|
|
225
|
+
)
|
|
226
|
+
if hasattr(_df_ta.columns, "levels") and len(_df_ta.columns.levels) > 1:
|
|
227
|
+
_df_ta.columns = _df_ta.columns.droplevel(1)
|
|
228
|
+
if _df_ta.empty:
|
|
229
|
+
_df_ta = None
|
|
230
|
+
except Exception:
|
|
231
|
+
_df_ta = None
|
|
232
|
+
|
|
233
|
+
if _df_ta is not None and len(_df_ta) >= 20:
|
|
234
|
+
try:
|
|
235
|
+
ti = _compute_ta(_df_ta)
|
|
236
|
+
except Exception:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
if ti.get("success"):
|
|
240
|
+
for _k in ("rsi", "macd_hist", "ma20", "ma60", "bb_upper", "bb_lower"):
|
|
241
|
+
if ti.get(_k) is not None:
|
|
242
|
+
result[_k] = ti[_k]
|
|
243
|
+
|
|
244
|
+
return result
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def tool_get_market_history(params: dict) -> dict:
|
|
248
|
+
"""Fetch OHLC price history for any stock/ETF/index/crypto.
|
|
249
|
+
|
|
250
|
+
Returns a *compact* summary (period stats + the most recent candles), NOT
|
|
251
|
+
the full series, so the model can reason about trends without flooding the
|
|
252
|
+
context window. Routes through MarketDataClient, which uses the user's
|
|
253
|
+
configured Tushare for A-shares (if a token is set), then Eastmoney/Sina/
|
|
254
|
+
AKShare, and yfinance for HK/US/global.
|
|
255
|
+
|
|
256
|
+
Use this instead of writing ad-hoc akshare/tushare scripts: it handles
|
|
257
|
+
symbol normalisation, source fallback, and is environment-independent.
|
|
258
|
+
"""
|
|
259
|
+
symbol = str(params.get("symbol", "")).strip().upper()
|
|
260
|
+
if not symbol:
|
|
261
|
+
return {"success": False, "error": "symbol is required"}
|
|
262
|
+
try:
|
|
263
|
+
days = int(params.get("days", 120) or 120)
|
|
264
|
+
except (TypeError, ValueError):
|
|
265
|
+
days = 120
|
|
266
|
+
days = max(5, min(days, 1000))
|
|
267
|
+
interval = str(params.get("interval", "1d") or "1d").lower()
|
|
268
|
+
|
|
269
|
+
if not (_HAS_MDC and _get_mdc is not None):
|
|
270
|
+
return {"success": False, "symbol": symbol,
|
|
271
|
+
"error": "market data client unavailable"}
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
hist = _get_mdc().history(symbol, days=days, interval=interval)
|
|
275
|
+
except Exception as exc:
|
|
276
|
+
return {"success": False, "symbol": symbol, "error": str(exc)}
|
|
277
|
+
|
|
278
|
+
if not hist.get("success") or not hist.get("data"):
|
|
279
|
+
return {
|
|
280
|
+
"success": False,
|
|
281
|
+
"symbol": symbol,
|
|
282
|
+
"provider_chain": hist.get("provider_chain") or ["eastmoney", "sina", "akshare", "yfinance"],
|
|
283
|
+
"error": hist.get("error") or "历史行情数据源暂时不可用,请稍后重试。",
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
rows = hist["data"] # ascending by date: [{date,open,high,low,close,volume},...]
|
|
287
|
+
closes = [r["close"] for r in rows if r.get("close")]
|
|
288
|
+
if not closes:
|
|
289
|
+
return {"success": False, "symbol": symbol, "error": "history contained no usable closes"}
|
|
290
|
+
|
|
291
|
+
def _ma(n: int):
|
|
292
|
+
return round(sum(closes[-n:]) / n, 4) if len(closes) >= n else None
|
|
293
|
+
|
|
294
|
+
start_close = closes[0]
|
|
295
|
+
end_close = closes[-1]
|
|
296
|
+
period_high = max(r["high"] for r in rows if r.get("high"))
|
|
297
|
+
period_low = min(r["low"] for r in rows if r.get("low"))
|
|
298
|
+
vols = [r["volume"] for r in rows if r.get("volume")]
|
|
299
|
+
|
|
300
|
+
# Only the most recent candles travel back to the model — the rest is
|
|
301
|
+
# summarised. Keeps the payload small regardless of `days`.
|
|
302
|
+
recent_n = min(30, len(rows))
|
|
303
|
+
return {
|
|
304
|
+
"success": True,
|
|
305
|
+
"symbol": hist.get("symbol") or symbol,
|
|
306
|
+
"name": hist.get("name") or symbol,
|
|
307
|
+
"provider": hist.get("provider"),
|
|
308
|
+
"interval": interval,
|
|
309
|
+
"total_points": len(rows),
|
|
310
|
+
"summary": {
|
|
311
|
+
"start_date": rows[0].get("date"),
|
|
312
|
+
"end_date": rows[-1].get("date"),
|
|
313
|
+
"start_close": round(start_close, 4),
|
|
314
|
+
"end_close": round(end_close, 4),
|
|
315
|
+
"change_pct": round((end_close - start_close) / start_close * 100, 2) if start_close else None,
|
|
316
|
+
"period_high": round(period_high, 4),
|
|
317
|
+
"period_low": round(period_low, 4),
|
|
318
|
+
"avg_volume": int(sum(vols) / len(vols)) if vols else None,
|
|
319
|
+
"ma5": _ma(5),
|
|
320
|
+
"ma20": _ma(20),
|
|
321
|
+
"ma60": _ma(60),
|
|
322
|
+
},
|
|
323
|
+
"recent_candles": rows[-recent_n:],
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def tool_broker_query(params: dict) -> dict:
|
|
328
|
+
"""Query a connected broker account (read-only): balance, positions, orders."""
|
|
329
|
+
if not _HAS_BROKERS:
|
|
330
|
+
return {"success": False, "error": "brokers 模块未加载,请确认 brokers/ 目录存在"}
|
|
331
|
+
|
|
332
|
+
query = str(params.get("query", "positions")).lower()
|
|
333
|
+
bid = params.get("broker_id", "")
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
reg = _get_broker_registry()
|
|
337
|
+
if bid:
|
|
338
|
+
broker = reg.get(bid)
|
|
339
|
+
if not broker:
|
|
340
|
+
broker = reg.connect(bid)
|
|
341
|
+
else:
|
|
342
|
+
broker = reg.active()
|
|
343
|
+
if not broker:
|
|
344
|
+
broker = reg.connect_default()
|
|
345
|
+
if not broker:
|
|
346
|
+
cfgs = _list_broker_configs()
|
|
347
|
+
if not cfgs:
|
|
348
|
+
return {
|
|
349
|
+
"success": False,
|
|
350
|
+
"error": (
|
|
351
|
+
"尚未配置任何券商。\n"
|
|
352
|
+
f"请编辑 {_BROKERS_CONFIG_PATH} 或使用 /broker add <type> 命令。"
|
|
353
|
+
),
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
"success": False,
|
|
357
|
+
"error": "没有已连接的券商。请先运行 /broker connect <id>。",
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if "account" in query or "balance" in query or "资金" in query or "余额" in query:
|
|
361
|
+
acct = broker.account_info()
|
|
362
|
+
return {
|
|
363
|
+
"success": True, "query": "account",
|
|
364
|
+
"broker": broker.label, "broker_type": broker.broker_type,
|
|
365
|
+
"account_id": acct.masked_account, "currency": acct.currency,
|
|
366
|
+
"total_assets": acct.total_assets, "cash": acct.cash,
|
|
367
|
+
"market_value": acct.market_value, "frozen": acct.frozen,
|
|
368
|
+
"pnl_today": acct.pnl_today, "pnl_total": acct.pnl_total,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if "position" in query or "持仓" in query or "portfolio" in query:
|
|
372
|
+
positions = broker.positions()
|
|
373
|
+
return {
|
|
374
|
+
"success": True, "query": "positions",
|
|
375
|
+
"broker": broker.label, "count": len(positions),
|
|
376
|
+
"positions": [
|
|
377
|
+
{
|
|
378
|
+
"symbol": p.symbol, "name": p.name,
|
|
379
|
+
"quantity": p.quantity, "available": p.available_qty,
|
|
380
|
+
"cost": p.cost_price, "price": p.current_price,
|
|
381
|
+
"market_value": p.market_value,
|
|
382
|
+
"pnl": p.pnl, "pnl_pct": round(p.pnl_pct, 2),
|
|
383
|
+
"currency": p.currency,
|
|
384
|
+
}
|
|
385
|
+
for p in positions
|
|
386
|
+
],
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if "order" in query or "订单" in query or "委托" in query:
|
|
390
|
+
status = params.get("status", "all")
|
|
391
|
+
orders = broker.orders(status=status, limit=int(params.get("limit", 20)))
|
|
392
|
+
return {
|
|
393
|
+
"success": True, "query": "orders",
|
|
394
|
+
"broker": broker.label, "status_filter": status,
|
|
395
|
+
"count": len(orders),
|
|
396
|
+
"orders": [
|
|
397
|
+
{
|
|
398
|
+
"order_id": o.order_id, "symbol": o.symbol, "name": o.name,
|
|
399
|
+
"side": o.side, "type": o.order_type,
|
|
400
|
+
"quantity": o.quantity, "filled": o.filled_qty,
|
|
401
|
+
"price": o.price, "avg_price": o.avg_price,
|
|
402
|
+
"status": o.status, "time": o.created_at,
|
|
403
|
+
"currency": o.currency,
|
|
404
|
+
}
|
|
405
|
+
for o in orders
|
|
406
|
+
],
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
# Default: positions
|
|
410
|
+
positions = broker.positions()
|
|
411
|
+
return {
|
|
412
|
+
"success": True, "query": "positions",
|
|
413
|
+
"broker": broker.label, "count": len(positions),
|
|
414
|
+
"positions": [
|
|
415
|
+
{
|
|
416
|
+
"symbol": p.symbol, "name": p.name,
|
|
417
|
+
"quantity": p.quantity, "market_value": p.market_value,
|
|
418
|
+
"pnl": p.pnl, "pnl_pct": round(p.pnl_pct, 2),
|
|
419
|
+
}
|
|
420
|
+
for p in positions
|
|
421
|
+
],
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
except Exception as e:
|
|
425
|
+
return {"success": False, "error": str(e)}
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def tool_broker_order(params: dict) -> dict:
|
|
429
|
+
"""Propose or execute a previewed order through the trading service layer."""
|
|
430
|
+
if not _HAS_BROKERS:
|
|
431
|
+
return {"success": False, "error": "brokers 模块未加载"}
|
|
432
|
+
|
|
433
|
+
symbol = str(params.get("symbol", "")).strip().upper()
|
|
434
|
+
side = str(params.get("side", "")).lower()
|
|
435
|
+
qty = params.get("quantity") or params.get("qty")
|
|
436
|
+
price = params.get("price")
|
|
437
|
+
order_type = str(params.get("order_type", "limit")).lower()
|
|
438
|
+
confirmed = bool(params.get("confirmed", False))
|
|
439
|
+
preview_id = str(params.get("preview_id", "") or params.get("order_preview_id", "")).strip()
|
|
440
|
+
target_weight = params.get("target_weight")
|
|
441
|
+
|
|
442
|
+
try:
|
|
443
|
+
reg = _get_broker_registry()
|
|
444
|
+
broker = reg.active()
|
|
445
|
+
if not broker:
|
|
446
|
+
broker = reg.connect_default()
|
|
447
|
+
except Exception as _e:
|
|
448
|
+
logger.debug("broker lookup failed: %s", _e)
|
|
449
|
+
broker = None
|
|
450
|
+
|
|
451
|
+
if not broker:
|
|
452
|
+
return {"success": False, "error": "无已连接账户,请先 /broker connect"}
|
|
453
|
+
|
|
454
|
+
if confirmed:
|
|
455
|
+
if not preview_id:
|
|
456
|
+
return {
|
|
457
|
+
"success": False,
|
|
458
|
+
"confirmation_required": True,
|
|
459
|
+
"error": "confirmed=true 需要 preview_id;请先生成订单预览。",
|
|
460
|
+
}
|
|
461
|
+
try:
|
|
462
|
+
from brokers import execute_order_preview
|
|
463
|
+
return execute_order_preview(broker, preview_id, confirmed=True)
|
|
464
|
+
except Exception as e:
|
|
465
|
+
return {"success": False, "error": str(e)}
|
|
466
|
+
|
|
467
|
+
if not symbol:
|
|
468
|
+
return {"success": False, "error": "symbol 是必填项"}
|
|
469
|
+
if side not in ("buy", "sell"):
|
|
470
|
+
return {"success": False, "error": "side 必须是 'buy' 或 'sell'"}
|
|
471
|
+
if not qty:
|
|
472
|
+
return {"success": False, "error": "quantity 是必填项"}
|
|
473
|
+
try:
|
|
474
|
+
qty = float(qty)
|
|
475
|
+
if qty <= 0:
|
|
476
|
+
raise ValueError
|
|
477
|
+
except (ValueError, TypeError):
|
|
478
|
+
return {"success": False, "error": "quantity 必须是正数"}
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
from brokers import OrderIntent, build_order_preview
|
|
482
|
+
preview = build_order_preview(
|
|
483
|
+
broker,
|
|
484
|
+
OrderIntent(
|
|
485
|
+
symbol=symbol,
|
|
486
|
+
side=side,
|
|
487
|
+
quantity=qty,
|
|
488
|
+
price=float(price) if price is not None else None,
|
|
489
|
+
order_type=order_type,
|
|
490
|
+
target_weight=float(target_weight) if target_weight is not None else None,
|
|
491
|
+
source=str(params.get("source", "order_preview") or "order_preview"),
|
|
492
|
+
user_message=str(params.get("user_message", "") or ""),
|
|
493
|
+
metadata={"tool": "broker_order"},
|
|
494
|
+
),
|
|
495
|
+
)
|
|
496
|
+
except Exception as e:
|
|
497
|
+
return {"success": False, "error": str(e)}
|
|
498
|
+
|
|
499
|
+
plan_data = preview.get("order_plan")
|
|
500
|
+
plan_message = ""
|
|
501
|
+
risk = (plan_data or {}).get("risk", {})
|
|
502
|
+
if risk.get("violations"):
|
|
503
|
+
plan_message = "\n".join(f" - {v}" for v in risk.get("violations", []))
|
|
504
|
+
elif risk.get("warnings"):
|
|
505
|
+
plan_message = "\n".join(f" - {w}" for w in risk.get("warnings", []))
|
|
506
|
+
blockers = preview.get("execution_blockers") or []
|
|
507
|
+
if blockers:
|
|
508
|
+
plan_message = "\n".join(f" - {v}" for v in blockers)
|
|
509
|
+
|
|
510
|
+
if plan_data and plan_data.get("risk", {}).get("violations"):
|
|
511
|
+
return {
|
|
512
|
+
"success": False, "risk_rejected": True,
|
|
513
|
+
"order_plan": plan_data,
|
|
514
|
+
"preview_id": preview.get("preview_id"),
|
|
515
|
+
"trade_preview": preview,
|
|
516
|
+
"message": "订单计划未通过风控:\n" + plan_message,
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
_price_str = f"{float(price):.2f}" if price is not None else "市价"
|
|
520
|
+
_side_cn = "买入" if side == "buy" else "卖出"
|
|
521
|
+
_risk_note = ""
|
|
522
|
+
if plan_data:
|
|
523
|
+
if risk.get("warnings"):
|
|
524
|
+
_risk_note = "\n\n风控提示:\n" + plan_message
|
|
525
|
+
elif blockers:
|
|
526
|
+
_risk_note = "\n\n执行限制:\n" + plan_message
|
|
527
|
+
return {
|
|
528
|
+
"success": False,
|
|
529
|
+
"confirmation_required": True,
|
|
530
|
+
"preview_id": preview.get("preview_id"),
|
|
531
|
+
"trade_preview": preview,
|
|
532
|
+
"order_plan": plan_data,
|
|
533
|
+
"order_preview": {
|
|
534
|
+
"preview_id": preview.get("preview_id"),
|
|
535
|
+
"mode": preview.get("mode"),
|
|
536
|
+
"broker": preview.get("broker_label"),
|
|
537
|
+
"symbol": symbol, "side": side, "side_cn": _side_cn,
|
|
538
|
+
"qty": qty, "price": price, "price_display": _price_str,
|
|
539
|
+
"order_type": order_type,
|
|
540
|
+
"can_execute": preview.get("can_execute"),
|
|
541
|
+
},
|
|
542
|
+
"message": (
|
|
543
|
+
f"⚠️ 请确认以下订单预览 `{preview.get('preview_id')}`:\n"
|
|
544
|
+
f" 模式: {preview.get('mode')} 券商: {preview.get('broker_label')}\n"
|
|
545
|
+
f" {_side_cn} **{symbol}** 数量: {qty:,.4g} 价格: {_price_str}\n\n"
|
|
546
|
+
"确认时必须携带 preview_id;其他任何回复取消。"
|
|
547
|
+
f"{_risk_note}"
|
|
548
|
+
),
|
|
549
|
+
}
|