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.
Files changed (284) hide show
  1. agents/__init__.py +32 -0
  2. agents/base.py +190 -0
  3. agents/deep/__init__.py +37 -0
  4. agents/deep/calibration_loop.py +144 -0
  5. agents/deep/critic.py +125 -0
  6. agents/deep/deepen.py +193 -0
  7. agents/deep/models.py +149 -0
  8. agents/deep/pipeline.py +164 -0
  9. agents/deep/quant_fusion.py +192 -0
  10. agents/deep/themes.py +95 -0
  11. agents/deep/tiers.py +106 -0
  12. agents/financial/__init__.py +10 -0
  13. agents/financial/catalyst.py +279 -0
  14. agents/financial/debate.py +145 -0
  15. agents/financial/earnings.py +303 -0
  16. agents/financial/fundamental.py +159 -0
  17. agents/financial/macro.py +99 -0
  18. agents/financial/news.py +207 -0
  19. agents/financial/risk.py +132 -0
  20. agents/financial/sector.py +279 -0
  21. agents/financial/synthesis.py +274 -0
  22. agents/financial/technical.py +258 -0
  23. agents/portfolio_agent.py +333 -0
  24. agents/realty/__init__.py +62 -0
  25. agents/realty/asset_diagnosis.py +150 -0
  26. agents/realty/business_match.py +165 -0
  27. agents/realty/cashflow_verify.py +208 -0
  28. agents/realty/contract_rules.py +209 -0
  29. agents/realty/energy_anomaly.py +188 -0
  30. agents/realty/exit_settlement.py +207 -0
  31. agents/realty/fulfillment_risk.py +205 -0
  32. agents/realty/ops_optimize.py +159 -0
  33. agents/realty/revenue_share.py +214 -0
  34. agents/registry.py +144 -0
  35. agents/sports/__init__.py +0 -0
  36. agents/sports/football_agent.py +169 -0
  37. agents/team.py +289 -0
  38. aliyun_data_client.py +660 -0
  39. apps/README.md +12 -0
  40. apps/__init__.py +2 -0
  41. apps/channels/README.md +15 -0
  42. apps/cli/README.md +13 -0
  43. apps/cli/__init__.py +2 -0
  44. apps/cli/bootstrap.py +99 -0
  45. apps/cli/codegen_paths.py +29 -0
  46. apps/cli/commands/__init__.py +16 -0
  47. apps/cli/commands/analysis_cmds.py +288 -0
  48. apps/cli/commands/backtest_cmds.py +1887 -0
  49. apps/cli/commands/broker_cmds.py +1154 -0
  50. apps/cli/commands/business_workflow_cmds.py +289 -0
  51. apps/cli/commands/catalog.py +84 -0
  52. apps/cli/commands/data_cmds.py +405 -0
  53. apps/cli/commands/diagnostic_cmds.py +179 -0
  54. apps/cli/commands/diagnostic_ops_cmds.py +696 -0
  55. apps/cli/commands/finance_render.py +12 -0
  56. apps/cli/commands/market.py +399 -0
  57. apps/cli/commands/market_cmds.py +1276 -0
  58. apps/cli/commands/market_context.py +425 -0
  59. apps/cli/commands/market_render.py +7 -0
  60. apps/cli/commands/model_cmds.py +1579 -0
  61. apps/cli/commands/ops_cmds.py +668 -0
  62. apps/cli/commands/portfolio_cmds.py +962 -0
  63. apps/cli/commands/report.py +377 -0
  64. apps/cli/commands/scaffold_templates.py +617 -0
  65. apps/cli/commands/session_cmds.py +179 -0
  66. apps/cli/commands/session_ux_cmds.py +280 -0
  67. apps/cli/commands/team.py +588 -0
  68. apps/cli/commands/team_render.py +8 -0
  69. apps/cli/commands/ui_cmds.py +358 -0
  70. apps/cli/commands/workflow_cmds.py +279 -0
  71. apps/cli/commands/workspace_cmds.py +1414 -0
  72. apps/cli/config_paths.py +70 -0
  73. apps/cli/config_store.py +61 -0
  74. apps/cli/deterministic.py +122 -0
  75. apps/cli/direct.py +48 -0
  76. apps/cli/github_app_auth.py +135 -0
  77. apps/cli/handlers/__init__.py +11 -0
  78. apps/cli/handlers/broker_handlers.py +122 -0
  79. apps/cli/handlers/chart_handlers.py +1309 -0
  80. apps/cli/handlers/market_handlers.py +2509 -0
  81. apps/cli/handlers/realty_handlers.py +114 -0
  82. apps/cli/handlers/strategy_advice.py +82 -0
  83. apps/cli/hooks.py +180 -0
  84. apps/cli/i18n.py +284 -0
  85. apps/cli/intent.py +136 -0
  86. apps/cli/intent_router.py +217 -0
  87. apps/cli/lifecycle_hooks.py +48 -0
  88. apps/cli/main.py +29 -0
  89. apps/cli/market_metadata.py +135 -0
  90. apps/cli/market_universe.py +265 -0
  91. apps/cli/message_processing.py +257 -0
  92. apps/cli/plan_mode.py +139 -0
  93. apps/cli/plotly_html.py +15 -0
  94. apps/cli/prediction_feedback.py +202 -0
  95. apps/cli/preflight.py +497 -0
  96. apps/cli/project_aria.py +60 -0
  97. apps/cli/prompts/__init__.py +0 -0
  98. apps/cli/prompts/coding.py +658 -0
  99. apps/cli/prompts/system_prompts.py +531 -0
  100. apps/cli/prompts/ui.py +434 -0
  101. apps/cli/providers/__init__.py +1 -0
  102. apps/cli/providers/base.py +271 -0
  103. apps/cli/providers/chat_routing.py +80 -0
  104. apps/cli/providers/llm/__init__.py +1 -0
  105. apps/cli/providers/llm/ollama_stream.py +1170 -0
  106. apps/cli/providers/llm/sse_stream.py +216 -0
  107. apps/cli/providers/runtime_bridge.py +185 -0
  108. apps/cli/runtime_consumer.py +489 -0
  109. apps/cli/session_export.py +87 -0
  110. apps/cli/session_jsonl.py +207 -0
  111. apps/cli/session_store.py +112 -0
  112. apps/cli/todo_tracker.py +190 -0
  113. apps/cli/tools/__init__.py +40 -0
  114. apps/cli/tools/context.py +46 -0
  115. apps/cli/tools/file_tools.py +112 -0
  116. apps/cli/tools/market_tools.py +549 -0
  117. apps/cli/tools/notebook_tools.py +111 -0
  118. apps/cli/tools/system_tools.py +669 -0
  119. apps/cli/tools/write_tools.py +715 -0
  120. apps/cli/tradingview_bridge.py +434 -0
  121. apps/cli/update_check.py +152 -0
  122. apps/cli/utils/__init__.py +0 -0
  123. apps/cli/utils/market_detect.py +1578 -0
  124. apps/daemon/README.md +14 -0
  125. apps/vscode/README.md +115 -0
  126. apps/vscode/package.json +70 -0
  127. aria_cli.py +11636 -0
  128. aria_code-4.1.3.dist-info/METADATA +952 -0
  129. aria_code-4.1.3.dist-info/RECORD +284 -0
  130. aria_code-4.1.3.dist-info/WHEEL +5 -0
  131. aria_code-4.1.3.dist-info/entry_points.txt +2 -0
  132. aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
  133. aria_code-4.1.3.dist-info/top_level.txt +50 -0
  134. aria_daemon.py +1295 -0
  135. aria_feishu_bot.py +1359 -0
  136. aria_relay_client.py +182 -0
  137. aria_relay_server.py +405 -0
  138. aria_telegram_bot.py +202 -0
  139. ariarc.py +328 -0
  140. artifacts.py +491 -0
  141. backtest_report.py +472 -0
  142. brokers/__init__.py +72 -0
  143. brokers/base.py +207 -0
  144. brokers/capabilities.py +264 -0
  145. brokers/cn/__init__.py +10 -0
  146. brokers/cn/easytrader_broker.py +193 -0
  147. brokers/cn/futu_broker.py +194 -0
  148. brokers/cn/longbridge_broker.py +190 -0
  149. brokers/cn/tiger_broker.py +196 -0
  150. brokers/cn/xtquant_broker.py +175 -0
  151. brokers/config.py +364 -0
  152. brokers/intl/__init__.py +5 -0
  153. brokers/intl/alpaca_broker.py +183 -0
  154. brokers/intl/ibkr_broker.py +215 -0
  155. brokers/intl/webull_broker.py +156 -0
  156. brokers/paper_broker.py +259 -0
  157. brokers/planning.py +296 -0
  158. brokers/registry.py +181 -0
  159. brokers/trading.py +237 -0
  160. change_store.py +127 -0
  161. command_safety.py +19 -0
  162. computer_use_tools.py +504 -0
  163. dashboard_generator.py +578 -0
  164. data_analysis_tools.py +808 -0
  165. data_cleaner.py +483 -0
  166. data_service.py +481 -0
  167. datasources/__init__.py +23 -0
  168. datasources/base.py +166 -0
  169. datasources/router.py +221 -0
  170. datasources/sources/__init__.py +15 -0
  171. datasources/sources/akshare_source.py +269 -0
  172. datasources/sources/alpha_vantage_source.py +202 -0
  173. datasources/sources/edgar_source.py +218 -0
  174. datasources/sources/finnhub_source.py +197 -0
  175. datasources/sources/fred_source.py +219 -0
  176. datasources/sources/tushare_source.py +141 -0
  177. datasources/sources/web_scraper_source.py +278 -0
  178. datasources/sources/world_bank_source.py +205 -0
  179. datasources/sources/yfinance_source.py +152 -0
  180. demo_player.py +204 -0
  181. doctor.py +508 -0
  182. file_analysis_tools.py +734 -0
  183. finance_formulas.py +389 -0
  184. football_data_client.py +1670 -0
  185. intent_classifier.py +358 -0
  186. local_finance_tools.py +3221 -0
  187. local_llm_provider.py +552 -0
  188. macro_tools.py +368 -0
  189. market_data_client.py +1899 -0
  190. mcp_client.py +506 -0
  191. memory_manager.py +245 -0
  192. model_capability.py +416 -0
  193. notification_tools.py +248 -0
  194. packages/__init__.py +23 -0
  195. packages/aria_agents/__init__.py +5 -0
  196. packages/aria_agents/manifest.py +69 -0
  197. packages/aria_core/__init__.py +34 -0
  198. packages/aria_core/architecture.py +192 -0
  199. packages/aria_core/export.py +124 -0
  200. packages/aria_core/manifest.py +65 -0
  201. packages/aria_infra/__init__.py +15 -0
  202. packages/aria_infra/arthera.py +52 -0
  203. packages/aria_infra/doctor.py +246 -0
  204. packages/aria_infra/product.py +37 -0
  205. packages/aria_mcp/__init__.py +25 -0
  206. packages/aria_mcp/bridge.py +38 -0
  207. packages/aria_mcp/config.py +97 -0
  208. packages/aria_mcp/tools.py +61 -0
  209. packages/aria_sdk/__init__.py +19 -0
  210. packages/aria_sdk/client.py +396 -0
  211. packages/aria_sdk/providers.py +70 -0
  212. packages/aria_sdk/streaming.py +73 -0
  213. packages/aria_sdk/types.py +86 -0
  214. packages/aria_services/__init__.py +55 -0
  215. packages/aria_services/context.py +258 -0
  216. packages/aria_services/data.py +11 -0
  217. packages/aria_services/provider_health.py +189 -0
  218. packages/aria_services/registry.py +213 -0
  219. packages/aria_services/usage.py +138 -0
  220. packages/aria_skills/__init__.py +5 -0
  221. packages/aria_skills/registry.py +59 -0
  222. packages/aria_tools/__init__.py +5 -0
  223. packages/aria_tools/registry.py +128 -0
  224. packages/quant_engine/__init__.py +6 -0
  225. packages/quant_engine/sports/__init__.py +72 -0
  226. packages/quant_engine/sports/calibrator.py +353 -0
  227. packages/quant_engine/sports/dixon_coles.py +234 -0
  228. packages/quant_engine/sports/elo.py +299 -0
  229. packages/quant_engine/sports/form.py +188 -0
  230. packages/quant_engine/sports/h2h.py +195 -0
  231. packages/quant_engine/sports/ml_model.py +354 -0
  232. packages/quant_engine/sports/predictor.py +311 -0
  233. packages/quant_engine/sports/tracker.py +664 -0
  234. packages/quant_engine/stochastic/__init__.py +27 -0
  235. packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  236. packages/quant_engine/stochastic/ito_calculus.py +477 -0
  237. packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  238. packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  239. packages/quant_engine/stochastic/options_pricing.py +573 -0
  240. packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  241. plan_utils.py +194 -0
  242. plugin_loader.py +328 -0
  243. portfolio_ledger.py +262 -0
  244. privacy/__init__.py +5 -0
  245. privacy/feedback.py +123 -0
  246. project_tools.py +525 -0
  247. providers/__init__.py +30 -0
  248. providers/llm/__init__.py +19 -0
  249. providers/llm/anthropic.py +184 -0
  250. providers/llm/base.py +139 -0
  251. providers/llm/ollama.py +128 -0
  252. providers/llm/openai_compat.py +282 -0
  253. providers/llm/registry.py +358 -0
  254. realty_data_tools.py +659 -0
  255. report_generator.py +1314 -0
  256. runtime/__init__.py +103 -0
  257. runtime/agent_loop.py +1183 -0
  258. runtime/approval.py +51 -0
  259. runtime/events.py +102 -0
  260. runtime/gateway.py +128 -0
  261. runtime/lsp.py +346 -0
  262. runtime/subagent.py +258 -0
  263. runtime/tool_executor.py +104 -0
  264. runtime/tool_policy.py +106 -0
  265. safety/__init__.py +21 -0
  266. safety/permissions.py +275 -0
  267. setup_wizard.py +653 -0
  268. strategy_vault.py +420 -0
  269. ui/__init__.py +100 -0
  270. ui/banner.py +310 -0
  271. ui/completer.py +391 -0
  272. ui/console.py +271 -0
  273. ui/image_render.py +243 -0
  274. ui/input_box.py +376 -0
  275. ui/picker.py +195 -0
  276. ui/render/__init__.py +11 -0
  277. ui/render/finance.py +1480 -0
  278. ui/render/market.py +225 -0
  279. ui/render/output.py +681 -0
  280. ui/render/team.py +346 -0
  281. ui/robot.py +235 -0
  282. workspace/__init__.py +6 -0
  283. workspace/files.py +170 -0
  284. 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
+ }
@@ -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)