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
agents/deep/deepen.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""P0 — tool-augmented deepening for material/uncertain findings.
|
|
2
|
+
|
|
3
|
+
The flat agents summarise pre-fetched data in a single pass. This layer runs a
|
|
4
|
+
small *gap-driven tool loop*: it looks at where the analysis is thin (no risk
|
|
5
|
+
angle, momentum undecided, no catalyst coverage) and calls finance tools to pull
|
|
6
|
+
the missing evidence — the same move Claude Code's agent loop makes when it needs
|
|
7
|
+
more before concluding.
|
|
8
|
+
|
|
9
|
+
v1 selects tools deterministically from the gaps (testable, no LLM). The selector
|
|
10
|
+
is isolated in ``_plan_steps`` so an LLM-driven planner can drop in later without
|
|
11
|
+
touching the execution loop. The tool runner is injectable for tests.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Callable, Dict, List, Optional, Tuple
|
|
17
|
+
|
|
18
|
+
from .models import Provenance, QuantEvidence, ThemeGroup
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _default_runner(tool: str, params: Dict) -> Optional[Dict]:
|
|
22
|
+
try:
|
|
23
|
+
import local_finance_tools as lft
|
|
24
|
+
except Exception:
|
|
25
|
+
return None
|
|
26
|
+
fn = getattr(lft, tool, None)
|
|
27
|
+
if not fn:
|
|
28
|
+
return None
|
|
29
|
+
try:
|
|
30
|
+
return fn(params)
|
|
31
|
+
except Exception:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _plan_steps(themes: List[ThemeGroup], quant: Optional[QuantEvidence]) -> List[Tuple[str, str]]:
|
|
36
|
+
"""Decide which tools to call based on coverage gaps. Returns [(tool, label)]."""
|
|
37
|
+
by_theme = {t.theme: t for t in themes}
|
|
38
|
+
plan: List[Tuple[str, str]] = []
|
|
39
|
+
|
|
40
|
+
risk = next((t for k, t in by_theme.items() if k.startswith("风险")), None)
|
|
41
|
+
if risk is None or risk.confidence < 0.5:
|
|
42
|
+
plan.append(("_get_risk_metrics", "下行风险"))
|
|
43
|
+
|
|
44
|
+
mom = next((t for k, t in by_theme.items() if k.startswith("动量")), None)
|
|
45
|
+
if mom is not None and mom.signal == "HOLD":
|
|
46
|
+
plan.append(("_calculate_factors", "动量/因子"))
|
|
47
|
+
|
|
48
|
+
cat = next((t for k, t in by_theme.items() if k.startswith("催化")), None)
|
|
49
|
+
if cat is None:
|
|
50
|
+
plan.append(("_analyze_news", "催化/消息"))
|
|
51
|
+
|
|
52
|
+
# quant undecided → a backtest gives an independent, data-grounded read
|
|
53
|
+
if quant is None or not quant.available or quant.verdict() == "NEUTRAL":
|
|
54
|
+
plan.append(("_backtest_strategy", "策略回测"))
|
|
55
|
+
|
|
56
|
+
return plan
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _summarize(tool: str, res: Dict) -> str:
|
|
60
|
+
if tool == "_get_risk_metrics":
|
|
61
|
+
bits = []
|
|
62
|
+
if "var_daily" in res: bits.append(f"日VaR {res['var_daily']:.1%}")
|
|
63
|
+
for k in ("sharpe", "sharpe_ratio"):
|
|
64
|
+
if isinstance(res.get(k), (int, float)): bits.append(f"Sharpe {res[k]:.2f}"); break
|
|
65
|
+
for k in ("max_drawdown", "max_dd"):
|
|
66
|
+
if isinstance(res.get(k), (int, float)): bits.append(f"MaxDD {res[k]:.1%}"); break
|
|
67
|
+
return ",".join(bits)
|
|
68
|
+
if tool == "_calculate_factors":
|
|
69
|
+
bits = [f"{k}={v:.3f}" for k, v in res.items()
|
|
70
|
+
if isinstance(v, (int, float)) and k in
|
|
71
|
+
("momentum", "volatility", "beta", "rsi", "ic")][:4]
|
|
72
|
+
return ",".join(bits)
|
|
73
|
+
if tool == "_analyze_news":
|
|
74
|
+
s = res.get("sentiment") or res.get("score")
|
|
75
|
+
return f"新闻情绪 {s}" if s is not None else (res.get("summary", "")[:80])
|
|
76
|
+
if tool == "_backtest_strategy":
|
|
77
|
+
bits = []
|
|
78
|
+
for k in ("total_return", "return", "cagr"):
|
|
79
|
+
if isinstance(res.get(k), (int, float)): bits.append(f"收益 {res[k]:+.1%}"); break
|
|
80
|
+
for k in ("sharpe", "sharpe_ratio"):
|
|
81
|
+
if isinstance(res.get(k), (int, float)): bits.append(f"Sharpe {res[k]:.2f}"); break
|
|
82
|
+
return ",".join(bits)
|
|
83
|
+
return ""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def deepen(
|
|
87
|
+
symbol: str,
|
|
88
|
+
themes: List[ThemeGroup],
|
|
89
|
+
quant: Optional[QuantEvidence] = None,
|
|
90
|
+
tool_runner: Optional[Callable[[str, Dict], Optional[Dict]]] = None,
|
|
91
|
+
max_steps: int = 3,
|
|
92
|
+
) -> Tuple[List[str], List[Provenance]]:
|
|
93
|
+
"""Deterministic gap-driven tool loop. Returns (deepening_notes, provenance)."""
|
|
94
|
+
runner = tool_runner or _default_runner
|
|
95
|
+
notes: List[str] = []
|
|
96
|
+
prov: List[Provenance] = []
|
|
97
|
+
for tool, label in _plan_steps(themes, quant)[:max_steps]:
|
|
98
|
+
res = None
|
|
99
|
+
try:
|
|
100
|
+
res = runner(tool, {"symbol": symbol})
|
|
101
|
+
except Exception:
|
|
102
|
+
res = None
|
|
103
|
+
if isinstance(res, dict) and res.get("success"):
|
|
104
|
+
line = _summarize(tool, res)
|
|
105
|
+
if line:
|
|
106
|
+
notes.append(f"[{label}] {line}")
|
|
107
|
+
prov.append(Provenance(label, f"deepen:{tool}"))
|
|
108
|
+
return notes, prov
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ── P0 agentic: LLM-driven tool loop (plan → act → observe → re-plan) ──────────
|
|
112
|
+
_TOOL_MENU = {
|
|
113
|
+
"_get_risk_metrics": "下行风险:VaR / Sharpe / 最大回撤",
|
|
114
|
+
"_calculate_factors": "量化因子:动量 / 波动 / Beta / IC",
|
|
115
|
+
"_analyze_news": "新闻情绪与催化事件",
|
|
116
|
+
"_backtest_strategy": "对该标的做策略回测(收益/Sharpe)",
|
|
117
|
+
"_get_sector_performance":"所属板块近期表现",
|
|
118
|
+
"_get_northbound_flow": "北向资金流向(A股)",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def _collect_llm(llm, system: str, user: str, max_tokens: int = 120) -> str:
|
|
123
|
+
try:
|
|
124
|
+
from providers.llm.base import Message
|
|
125
|
+
except Exception:
|
|
126
|
+
return ""
|
|
127
|
+
msgs = [Message(role="system", content=system), Message(role="user", content=user)]
|
|
128
|
+
out = ""
|
|
129
|
+
try:
|
|
130
|
+
async for ev in llm.stream(msgs, max_tokens=max_tokens):
|
|
131
|
+
if ev.get("type") == "token":
|
|
132
|
+
out += ev.get("text", "")
|
|
133
|
+
elif ev.get("type") == "error":
|
|
134
|
+
break
|
|
135
|
+
except Exception:
|
|
136
|
+
return ""
|
|
137
|
+
return out.strip()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def _llm_pick_tool(llm, symbol: str, context: str, used: set) -> str:
|
|
141
|
+
menu = "\n".join(f"- {t}: {d}" for t, d in _TOOL_MENU.items() if t not in used)
|
|
142
|
+
if not menu:
|
|
143
|
+
return "DONE"
|
|
144
|
+
system = ("你是量化研究的工具调度器。看已知信息的缺口,从工具清单里挑【最该补的一个】"
|
|
145
|
+
"来补证据。只输出工具名(如 _get_risk_metrics);证据已够就只输出 DONE。不要解释。")
|
|
146
|
+
user = f"标的: {symbol}\n已知:\n{context}\n\n可用工具:\n{menu}\n\n选一个工具名或 DONE:"
|
|
147
|
+
resp = (await _collect_llm(llm, system, user)).strip()
|
|
148
|
+
for t in _TOOL_MENU:
|
|
149
|
+
if t in resp and t not in used:
|
|
150
|
+
return t
|
|
151
|
+
return "DONE"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _gap_summary(themes: List[ThemeGroup], quant: Optional[QuantEvidence]) -> str:
|
|
155
|
+
bits = [t.summary for t in themes]
|
|
156
|
+
if quant and quant.available:
|
|
157
|
+
bits.append(f"量化: {quant.verdict()}")
|
|
158
|
+
return ";".join(bits) or "(无)"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def deepen_agentic(
|
|
162
|
+
symbol: str,
|
|
163
|
+
themes: List[ThemeGroup],
|
|
164
|
+
quant: Optional[QuantEvidence] = None,
|
|
165
|
+
llm=None,
|
|
166
|
+
tool_runner: Optional[Callable[[str, Dict], Optional[Dict]]] = None,
|
|
167
|
+
max_steps: int = 3,
|
|
168
|
+
) -> Tuple[List[str], List[Provenance]]:
|
|
169
|
+
"""LLM-driven deepening loop. Falls back to the deterministic planner if no LLM."""
|
|
170
|
+
if llm is None:
|
|
171
|
+
return deepen(symbol, themes, quant, tool_runner, max_steps)
|
|
172
|
+
runner = tool_runner or _default_runner
|
|
173
|
+
notes: List[str] = []
|
|
174
|
+
prov: List[Provenance] = []
|
|
175
|
+
used: set = set()
|
|
176
|
+
context = _gap_summary(themes, quant)
|
|
177
|
+
for _ in range(max_steps):
|
|
178
|
+
tool = await _llm_pick_tool(llm, symbol, context, used)
|
|
179
|
+
if not tool or tool == "DONE":
|
|
180
|
+
break
|
|
181
|
+
used.add(tool)
|
|
182
|
+
res = None
|
|
183
|
+
try:
|
|
184
|
+
res = runner(tool, {"symbol": symbol})
|
|
185
|
+
except Exception:
|
|
186
|
+
res = None
|
|
187
|
+
if isinstance(res, dict) and res.get("success"):
|
|
188
|
+
line = _summarize(tool, res) or "已查询"
|
|
189
|
+
label = _TOOL_MENU.get(tool, tool).split(":")[0][:8]
|
|
190
|
+
notes.append(f"[{label}] {line}")
|
|
191
|
+
prov.append(Provenance(label, f"deepen:{tool}"))
|
|
192
|
+
context += f"\n已补({label}): {line}"
|
|
193
|
+
return notes, prov
|
agents/deep/models.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Structured data model for the deep analysis pipeline.
|
|
2
|
+
|
|
3
|
+
Everything the pipeline produces is captured here as plain dataclasses with
|
|
4
|
+
``to_dict()`` so the whole analysis is machine-readable (downstream tools, audit,
|
|
5
|
+
training data) — not just a blob of synthesis text.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ── P3: data provenance / lineage ─────────────────────────────────────────────
|
|
16
|
+
@dataclass
|
|
17
|
+
class Provenance:
|
|
18
|
+
"""Where a datum came from and how fresh it is."""
|
|
19
|
+
field: str # e.g. "price", "fundamentals", "ai_signal"
|
|
20
|
+
source: str # e.g. "yfinance", "akshare", "ml_pipeline"
|
|
21
|
+
fetched_at: float = field(default_factory=time.time)
|
|
22
|
+
age_sec: Optional[float] = None # data age (not fetch age) when known
|
|
23
|
+
note: str = ""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def freshness(self) -> str:
|
|
27
|
+
age = self.age_sec if self.age_sec is not None else (time.time() - self.fetched_at)
|
|
28
|
+
if age < 90:
|
|
29
|
+
return "live"
|
|
30
|
+
if age < 3600:
|
|
31
|
+
return f"{int(age // 60)}m old"
|
|
32
|
+
if age < 86400:
|
|
33
|
+
return f"{int(age // 3600)}h old"
|
|
34
|
+
return f"{int(age // 86400)}d old"
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
37
|
+
return {"field": self.field, "source": self.source,
|
|
38
|
+
"freshness": self.freshness, "note": self.note}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── P2: quantitative ground truth ─────────────────────────────────────────────
|
|
42
|
+
@dataclass
|
|
43
|
+
class QuantEvidence:
|
|
44
|
+
"""Quantitative signals used to anchor and calibrate the qualitative verdict."""
|
|
45
|
+
ai_signal: Optional[str] = None # BUY/HOLD/SELL from the quant model
|
|
46
|
+
ai_score: Optional[float] = None # -1..1 expected-return-ish score
|
|
47
|
+
ic: Optional[float] = None # information coefficient (model skill)
|
|
48
|
+
sharpe: Optional[float] = None
|
|
49
|
+
max_drawdown: Optional[float] = None
|
|
50
|
+
backtest_return: Optional[float] = None
|
|
51
|
+
factors: Dict[str, Any] = field(default_factory=dict)
|
|
52
|
+
available: bool = False
|
|
53
|
+
note: str = ""
|
|
54
|
+
|
|
55
|
+
def verdict(self) -> str:
|
|
56
|
+
"""Collapse the quant signals into BULLISH / BEARISH / NEUTRAL."""
|
|
57
|
+
if not self.available:
|
|
58
|
+
return "NEUTRAL"
|
|
59
|
+
if self.ai_signal in ("STRONG_BUY", "BUY"):
|
|
60
|
+
return "BULLISH"
|
|
61
|
+
if self.ai_signal in ("STRONG_SELL", "SELL"):
|
|
62
|
+
return "BEARISH"
|
|
63
|
+
if self.ai_score is not None:
|
|
64
|
+
if self.ai_score >= 0.15:
|
|
65
|
+
return "BULLISH"
|
|
66
|
+
if self.ai_score <= -0.15:
|
|
67
|
+
return "BEARISH"
|
|
68
|
+
return "NEUTRAL"
|
|
69
|
+
|
|
70
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
71
|
+
return {
|
|
72
|
+
"ai_signal": self.ai_signal, "ai_score": self.ai_score,
|
|
73
|
+
"ic": self.ic, "sharpe": self.sharpe,
|
|
74
|
+
"max_drawdown": self.max_drawdown, "backtest_return": self.backtest_return,
|
|
75
|
+
"verdict": self.verdict(), "available": self.available, "note": self.note,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ── P1: hierarchical synthesis ────────────────────────────────────────────────
|
|
80
|
+
@dataclass
|
|
81
|
+
class ThemeGroup:
|
|
82
|
+
"""A cluster of agents that speak to the same theme (valuation, momentum, …)."""
|
|
83
|
+
theme: str
|
|
84
|
+
agents: List[str] = field(default_factory=list)
|
|
85
|
+
signal: str = "HOLD"
|
|
86
|
+
confidence: float = 0.0
|
|
87
|
+
summary: str = ""
|
|
88
|
+
key_points: List[str] = field(default_factory=list)
|
|
89
|
+
|
|
90
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
91
|
+
return {"theme": self.theme, "agents": self.agents, "signal": self.signal,
|
|
92
|
+
"confidence": round(self.confidence, 3), "summary": self.summary,
|
|
93
|
+
"key_points": self.key_points}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ── P1: critic / self-check ───────────────────────────────────────────────────
|
|
97
|
+
@dataclass
|
|
98
|
+
class CritiqueIssue:
|
|
99
|
+
severity: str # "high" | "medium" | "low"
|
|
100
|
+
kind: str # "unsupported" | "missing_risk" | "stale_data" | "thin_coverage" | "conflict"
|
|
101
|
+
message: str
|
|
102
|
+
|
|
103
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
104
|
+
return {"severity": self.severity, "kind": self.kind, "message": self.message}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class Critique:
|
|
109
|
+
issues: List[CritiqueIssue] = field(default_factory=list)
|
|
110
|
+
passed: bool = True
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def high(self) -> List[CritiqueIssue]:
|
|
114
|
+
return [i for i in self.issues if i.severity == "high"]
|
|
115
|
+
|
|
116
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
117
|
+
return {"passed": self.passed, "issues": [i.to_dict() for i in self.issues]}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ── Top-level result ──────────────────────────────────────────────────────────
|
|
121
|
+
@dataclass
|
|
122
|
+
class DeepAnalysisResult:
|
|
123
|
+
symbol: str
|
|
124
|
+
final_signal: str = "HOLD"
|
|
125
|
+
raw_confidence: float = 0.0 # team vote, uncalibrated
|
|
126
|
+
calibrated_confidence: float = 0.0 # after quant fusion + reliability
|
|
127
|
+
themes: List[ThemeGroup] = field(default_factory=list)
|
|
128
|
+
quant: Optional[QuantEvidence] = None
|
|
129
|
+
critique: Optional[Critique] = None
|
|
130
|
+
provenance: List[Provenance] = field(default_factory=list)
|
|
131
|
+
synthesis: str = "" # top-level narrative (post-critic)
|
|
132
|
+
agent_results: List[Dict[str, Any]] = field(default_factory=list)
|
|
133
|
+
elapsed_sec: float = 0.0
|
|
134
|
+
error: Optional[str] = None
|
|
135
|
+
|
|
136
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
137
|
+
return {
|
|
138
|
+
"symbol": self.symbol,
|
|
139
|
+
"final_signal": self.final_signal,
|
|
140
|
+
"raw_confidence": round(self.raw_confidence, 3),
|
|
141
|
+
"calibrated_confidence": round(self.calibrated_confidence, 3),
|
|
142
|
+
"themes": [t.to_dict() for t in self.themes],
|
|
143
|
+
"quant": self.quant.to_dict() if self.quant else None,
|
|
144
|
+
"critique": self.critique.to_dict() if self.critique else None,
|
|
145
|
+
"provenance": [p.to_dict() for p in self.provenance],
|
|
146
|
+
"synthesis": self.synthesis,
|
|
147
|
+
"elapsed_sec": self.elapsed_sec,
|
|
148
|
+
"error": self.error,
|
|
149
|
+
}
|
agents/deep/pipeline.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Deep analysis orchestrator — ties P0–P3 into one layered pass.
|
|
2
|
+
|
|
3
|
+
team (parallel agents) ─▶ group by theme (P1a) ─▶ deepen gaps (P0)
|
|
4
|
+
─▶ quant fusion (P2) ─▶ vote ─▶ calibrate (P2) ─▶ critic (P1b)
|
|
5
|
+
─▶ synthesis ─▶ tiered result (P3)
|
|
6
|
+
|
|
7
|
+
``analyze()`` is the deterministic core (assembles a DeepAnalysisResult from given
|
|
8
|
+
agent results); it needs no LLM or network and is fully unit-tested. ``run()`` is
|
|
9
|
+
the async convenience that first runs the AgentTeam, then calls ``analyze()``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from typing import Callable, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from ..base import AgentResult
|
|
18
|
+
from .critic import critique, soften_signal
|
|
19
|
+
from .deepen import deepen
|
|
20
|
+
from .models import DeepAnalysisResult
|
|
21
|
+
from .quant_fusion import CalibrationStore, calibrate_confidence, gather_quant_evidence
|
|
22
|
+
from .themes import group_by_theme
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _vote_all(results: List[AgentResult]):
|
|
26
|
+
"""Confidence-weighted majority over every successful result."""
|
|
27
|
+
try:
|
|
28
|
+
from ..team import _vote_signal
|
|
29
|
+
return _vote_signal(results)
|
|
30
|
+
except Exception:
|
|
31
|
+
return "HOLD", 0.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _build_synthesis(themes, notes, team_synthesis, agree, quant) -> str:
|
|
35
|
+
parts: List[str] = []
|
|
36
|
+
if team_synthesis and team_synthesis.strip():
|
|
37
|
+
parts.append(team_synthesis.strip())
|
|
38
|
+
elif themes:
|
|
39
|
+
parts.append(";".join(t.summary for t in themes))
|
|
40
|
+
if notes:
|
|
41
|
+
parts.append("补充证据(深挖):" + ";".join(notes))
|
|
42
|
+
if quant and quant.available:
|
|
43
|
+
if agree == "disagree":
|
|
44
|
+
parts.append(f"⚠️ 量化信号为 {quant.verdict()},与定性方向相反,已下调置信度,建议人工复核。")
|
|
45
|
+
elif agree == "agree":
|
|
46
|
+
parts.append(f"量化信号({quant.verdict()})与定性方向一致,置信度已上调。")
|
|
47
|
+
return "\n\n".join(p for p in parts if p)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DeepAnalysisPipeline:
|
|
51
|
+
def __init__(self, llm_provider=None, data_router=None,
|
|
52
|
+
store: Optional[CalibrationStore] = None, lang: str = "zh"):
|
|
53
|
+
self.llm = llm_provider
|
|
54
|
+
self.data = data_router
|
|
55
|
+
self.store = store if store is not None else CalibrationStore()
|
|
56
|
+
self.lang = lang
|
|
57
|
+
|
|
58
|
+
def analyze(
|
|
59
|
+
self,
|
|
60
|
+
symbol: str,
|
|
61
|
+
agent_results: List[AgentResult],
|
|
62
|
+
*,
|
|
63
|
+
team_synthesis: str = "",
|
|
64
|
+
quant_provider: Optional[Callable[[str], Dict[str, Dict]]] = None,
|
|
65
|
+
tool_runner: Optional[Callable[[str, Dict], Optional[Dict]]] = None,
|
|
66
|
+
deepen_result: Optional[tuple] = None,
|
|
67
|
+
) -> DeepAnalysisResult:
|
|
68
|
+
"""Deterministic assembly of the deep result (no LLM/network required).
|
|
69
|
+
|
|
70
|
+
``deepen_result`` lets ``run()`` inject the LLM-driven (agentic) deepening
|
|
71
|
+
output; when absent the deterministic gap planner runs instead.
|
|
72
|
+
"""
|
|
73
|
+
if not agent_results:
|
|
74
|
+
return DeepAnalysisResult(symbol=symbol, error="no_agent_results")
|
|
75
|
+
|
|
76
|
+
themes = group_by_theme(agent_results) # P1a
|
|
77
|
+
quant, qprov = gather_quant_evidence(symbol, quant_provider) # P2
|
|
78
|
+
if deepen_result is not None: # P0 (agentic, injected)
|
|
79
|
+
notes, dprov = deepen_result
|
|
80
|
+
else:
|
|
81
|
+
notes, dprov = deepen(symbol, themes, quant, tool_runner) # P0 (deterministic)
|
|
82
|
+
|
|
83
|
+
raw_signal, raw_conf = _vote_all(agent_results)
|
|
84
|
+
cal_conf, agree = calibrate_confidence(raw_conf, raw_signal, quant, self.store) # P2
|
|
85
|
+
|
|
86
|
+
kp_count = sum(len(t.key_points) for t in themes)
|
|
87
|
+
crit = critique(agent_results, raw_signal, cal_conf, quant, agree, # P1b
|
|
88
|
+
qprov + dprov, kp_count)
|
|
89
|
+
|
|
90
|
+
final_signal = raw_signal if crit.passed else soften_signal(raw_signal)
|
|
91
|
+
synthesis = _build_synthesis(themes, notes, team_synthesis, agree, quant)
|
|
92
|
+
|
|
93
|
+
return DeepAnalysisResult(
|
|
94
|
+
symbol=symbol,
|
|
95
|
+
final_signal=final_signal,
|
|
96
|
+
raw_confidence=raw_conf,
|
|
97
|
+
calibrated_confidence=cal_conf,
|
|
98
|
+
themes=themes,
|
|
99
|
+
quant=quant,
|
|
100
|
+
critique=crit,
|
|
101
|
+
provenance=qprov + dprov,
|
|
102
|
+
synthesis=synthesis,
|
|
103
|
+
agent_results=[r.to_dict() for r in agent_results],
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
async def run(
|
|
107
|
+
self,
|
|
108
|
+
symbol: str,
|
|
109
|
+
agents: Optional[List[str]] = None,
|
|
110
|
+
quant_provider: Optional[Callable] = None,
|
|
111
|
+
tool_runner: Optional[Callable] = None,
|
|
112
|
+
on_agent_done: Optional[Callable] = None,
|
|
113
|
+
) -> DeepAnalysisResult:
|
|
114
|
+
t0 = time.time()
|
|
115
|
+
from ..team import AgentTeam
|
|
116
|
+
team = AgentTeam(llm_provider=self.llm, data_router=self.data,
|
|
117
|
+
on_agent_done=on_agent_done, lang=self.lang)
|
|
118
|
+
tr = await team.run(symbol, agents=agents)
|
|
119
|
+
|
|
120
|
+
# P0 agentic deepening (LLM-driven tool loop) when an LLM is available.
|
|
121
|
+
deepen_result = None
|
|
122
|
+
if self.llm and tr.results:
|
|
123
|
+
try:
|
|
124
|
+
from .deepen import deepen_agentic
|
|
125
|
+
from .themes import group_by_theme as _grp
|
|
126
|
+
deepen_result = await deepen_agentic(
|
|
127
|
+
symbol, _grp(tr.results), None, self.llm, tool_runner)
|
|
128
|
+
except Exception:
|
|
129
|
+
deepen_result = None
|
|
130
|
+
|
|
131
|
+
res = self.analyze(symbol, tr.results, team_synthesis=tr.synthesis,
|
|
132
|
+
quant_provider=quant_provider, tool_runner=tool_runner,
|
|
133
|
+
deepen_result=deepen_result)
|
|
134
|
+
|
|
135
|
+
# P1b LLM self-check — augments the deterministic critic when an LLM is present.
|
|
136
|
+
if self.llm and res.critique is not None and res.synthesis:
|
|
137
|
+
try:
|
|
138
|
+
from .critic import llm_critique, soften_signal
|
|
139
|
+
theme_sum = ";".join(t.summary for t in res.themes)
|
|
140
|
+
extra = await llm_critique(symbol, res.synthesis, theme_sum, self.llm)
|
|
141
|
+
if extra:
|
|
142
|
+
had_high = bool(res.critique.high)
|
|
143
|
+
res.critique.issues.extend(extra)
|
|
144
|
+
new_high = any(i.severity == "high" for i in extra)
|
|
145
|
+
if new_high:
|
|
146
|
+
res.critique.passed = False
|
|
147
|
+
# soften once if the deterministic pass had cleared it
|
|
148
|
+
if not had_high:
|
|
149
|
+
res.final_signal = soften_signal(res.final_signal)
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
res.elapsed_sec = round(time.time() - t0, 1)
|
|
154
|
+
if tr.error and not tr.results:
|
|
155
|
+
res.error = tr.error
|
|
156
|
+
return res
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def run_deep_analysis(symbol: str, llm_provider=None, data_router=None,
|
|
160
|
+
agents: Optional[List[str]] = None, lang: str = "zh",
|
|
161
|
+
**kw) -> DeepAnalysisResult:
|
|
162
|
+
"""Convenience: run the full team + deep pipeline for ``symbol``."""
|
|
163
|
+
pipe = DeepAnalysisPipeline(llm_provider=llm_provider, data_router=data_router, lang=lang)
|
|
164
|
+
return await pipe.run(symbol, agents=agents, **kw)
|