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/__init__.py ADDED
@@ -0,0 +1,32 @@
1
+ """
2
+ agents/ — Aria Code 可组合多智能体系统
3
+ =======================================
4
+ 用户可在项目根放置 aria_agents/ 目录,其中的 Agent 类自动被发现。
5
+
6
+ 内置 Agent:
7
+ macro → 宏观环境、利率、行业周期
8
+ fundamental → 财务指标、估值、竞争壁垒
9
+ technical → 图形形态、动量、关键价位
10
+ risk → 风险评分、仓位建议
11
+ synthesis → 汇总以上,输出可操作建议
12
+
13
+ 自定义 Agent 示例 (aria_agents/northbound_agent.py):
14
+ from agents.base import BaseAgent, AgentResult
15
+
16
+ class NorthboundAgent(BaseAgent):
17
+ name = "northbound"
18
+ description = "北向资金分析专家"
19
+
20
+ async def analyze(self, symbol: str, data: dict) -> AgentResult:
21
+ ...
22
+ """
23
+
24
+ from .base import BaseAgent, AgentResult
25
+ from .registry import AgentRegistry, get_registry
26
+ from .team import AgentTeam, run_team
27
+
28
+ __all__ = [
29
+ "BaseAgent", "AgentResult",
30
+ "AgentRegistry", "get_registry",
31
+ "AgentTeam", "run_team",
32
+ ]
agents/base.py ADDED
@@ -0,0 +1,190 @@
1
+ """
2
+ agents/base.py — Agent 统一抽象基类
3
+ =====================================
4
+ 所有 agent 继承 BaseAgent,实现 analyze() 方法。
5
+ LLM provider 和数据源从外部注入,agent 本身不关心底层实现。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from abc import ABC, abstractmethod
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Callable, Dict, List, Optional
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class AgentResult:
20
+ """Agent 分析结果,统一输出格式"""
21
+ agent: str # agent 名称
22
+ symbol: str # 分析标的
23
+ analysis: str # 核心分析文本
24
+ confidence: float # 置信度 0.0-1.0
25
+ signal: str = "HOLD" # BUY / HOLD / SELL / STRONG_BUY / STRONG_SELL
26
+ key_points: List[str] = field(default_factory=list) # 关键结论(用于 synthesis)
27
+ data_used: Dict[str, Any] = field(default_factory=dict) # 使用的原始数据
28
+ error: Optional[str] = None # 失败时的错误信息
29
+
30
+ @property
31
+ def success(self) -> bool:
32
+ return self.error is None
33
+
34
+ def to_dict(self) -> Dict:
35
+ return {
36
+ "agent": self.agent,
37
+ "symbol": self.symbol,
38
+ "analysis": self.analysis,
39
+ "confidence": self.confidence,
40
+ "signal": self.signal,
41
+ "key_points": self.key_points,
42
+ "error": self.error,
43
+ }
44
+
45
+
46
+ class BaseAgent(ABC):
47
+ """
48
+ 所有 Agent 的抽象基类。
49
+
50
+ 子类必须声明:
51
+ name: str — 唯一标识(用于 /team --agents macro,fundamental)
52
+ description: str — 简短描述(显示在 /help 中)
53
+
54
+ 子类必须实现:
55
+ analyze(symbol, data) → AgentResult
56
+
57
+ 可选覆盖:
58
+ fetch_data(symbol) → dict — 自定义数据获取逻辑
59
+ """
60
+
61
+ name: str = "base"
62
+ description: str = "基础 Agent"
63
+
64
+ # Post-training IPO facts injected into every agent's system prompt
65
+ _POST_TRAINING_FACTS = (
66
+ "\n\n## ⚡ Critical post-training facts (override training memory)\n"
67
+ "- **SPCX** = SpaceX (Space Exploration Technologies Corp.) — IPO'd June 12, 2026 "
68
+ "on NASDAQ. It is NOT an ETF. ⛔ Never say 'SPCX is a China ETF' or 'SPCX is private'.\n"
69
+ "- **2026 FIFA World Cup** opened June 11, 2026 in USA/Canada/Mexico.\n"
70
+ )
71
+
72
+ # Language rule injected per-call based on detected user language
73
+ _LANG_RULES = {
74
+ "zh": "\n\n## Language rule\nRespond in Chinese (中文). Technical terms (RSI, MACD, P/E, EPS) may stay in English.\n",
75
+ "en": "\n\n## Language rule\nRespond in English.\n",
76
+ }
77
+
78
+ def __init__(
79
+ self,
80
+ llm_provider=None, # BaseLLMProvider 实例(可选,None 则用模板生成)
81
+ data_router=None, # DataRouter 实例(可选)
82
+ on_token: Optional[Callable[[str], None]] = None, # 流式 token 回调
83
+ config: Optional[Dict] = None,
84
+ lang: str = "zh", # user language: "zh" | "en"
85
+ ):
86
+ self.llm = llm_provider
87
+ self.data = data_router
88
+ self.on_token = on_token
89
+ self.config = config or {}
90
+ self.lang = lang
91
+
92
+ async def fetch_data(self, symbol: str) -> Dict[str, Any]:
93
+ """
94
+ 从数据路由器获取分析所需数据。
95
+ 子类可覆盖此方法以自定义数据获取逻辑。
96
+ """
97
+ if not self.data:
98
+ return {}
99
+ result = {}
100
+ try:
101
+ q = self.data.quote(symbol)
102
+ if q:
103
+ result["quote"] = q.to_dict()
104
+ except Exception as e:
105
+ logger.debug(f"[{self.name}] fetch quote {symbol}: {e}")
106
+ return result
107
+
108
+ def _data_guard(self, quote: Dict[str, Any]) -> str:
109
+ """Return a warning string if real data is unavailable; empty string if data is present."""
110
+ price = quote.get("price") if quote else None
111
+ if not price or float(price) == 0:
112
+ return (
113
+ "\n\n## ⛔ DATA UNAVAILABLE — STRICT RULES\n"
114
+ "Real market data could not be fetched (price=0 or missing).\n"
115
+ "You MUST:\n"
116
+ "1. State clearly that no real data is available.\n"
117
+ "2. NEVER invent specific prices, P/E ratios, EPS, revenue, RSI, MACD, or any numbers.\n"
118
+ "3. NEVER give specific price targets, stop-loss levels, or entry prices.\n"
119
+ "4. Give only qualitative analysis based on publicly known company characteristics.\n"
120
+ "5. End with the signal word (BUY/HOLD/SELL) but with low confidence (≤40%).\n"
121
+ )
122
+ return ""
123
+
124
+ async def _call_llm(
125
+ self,
126
+ system: str,
127
+ user: str,
128
+ max_tokens: int = 800,
129
+ quote: Optional[Dict[str, Any]] = None,
130
+ ) -> str:
131
+ """调用 LLM 生成分析文本(无 LLM 时返回空字符串)"""
132
+ if not self.llm:
133
+ return ""
134
+ # Inject language rule + post-training facts + data guard into system prompt
135
+ _lang_rule = self._LANG_RULES.get(self.lang, self._LANG_RULES["zh"])
136
+ _data_warn = self._data_guard(quote or {})
137
+ system = system + self._POST_TRAINING_FACTS + _lang_rule + _data_warn
138
+ from providers.llm.base import Message
139
+ messages = [
140
+ Message(role="system", content=system),
141
+ Message(role="user", content=user),
142
+ ]
143
+ full_text = ""
144
+ try:
145
+ async for event in self.llm.stream(
146
+ messages, max_tokens=max_tokens
147
+ ):
148
+ t = event.get("type")
149
+ if t == "token":
150
+ tok = event.get("text", "")
151
+ full_text += tok
152
+ if self.on_token:
153
+ self.on_token(tok)
154
+ elif t == "error":
155
+ logger.warning(f"[{self.name}] LLM 错误: {event.get('message')}")
156
+ break
157
+ except Exception as e:
158
+ logger.warning(f"[{self.name}] LLM 调用失败: {e}")
159
+ return full_text.strip()
160
+
161
+ @abstractmethod
162
+ async def analyze(self, symbol: str, data: Dict[str, Any]) -> AgentResult:
163
+ """
164
+ 核心分析方法。
165
+
166
+ Args:
167
+ symbol: 股票/资产代码
168
+ data: 由 fetch_data() 预取的数据字典
169
+
170
+ Returns:
171
+ AgentResult
172
+ """
173
+ ...
174
+
175
+ async def run(self, symbol: str) -> AgentResult:
176
+ """完整执行:fetch_data → analyze,异常自动捕获。"""
177
+ try:
178
+ data = await self.fetch_data(symbol)
179
+ result = await self.analyze(symbol, data)
180
+ return result
181
+ except Exception as e:
182
+ logger.error(f"[{self.name}] run({symbol}) 失败: {e}", exc_info=True)
183
+ return AgentResult(
184
+ agent=self.name, symbol=symbol,
185
+ analysis="", confidence=0.0,
186
+ error=str(e),
187
+ )
188
+
189
+ def __repr__(self) -> str:
190
+ return f"{self.__class__.__name__}(name={self.name!r})"
@@ -0,0 +1,37 @@
1
+ """Deep analysis pipeline — Claude-Code-style layered research on top of AgentTeam.
2
+
3
+ Adds the four "deep" layers the flat team pipeline was missing:
4
+
5
+ P0 deepen — tool-augmented evidence gathering for material/uncertain findings
6
+ P1 hierarchical + critic — theme-grouped sub-synthesis, then a self-check pass
7
+ P2 quant fusion — fuse ML/backtest/risk signals as ground truth + calibrate confidence
8
+ P3 tiered output + provenance — brief / standard / deep reports with data lineage
9
+
10
+ Everything degrades gracefully: any layer whose dependency (LLM, quant tools, data
11
+ cleaner) is unavailable is skipped, never raised. The deterministic parts (theme
12
+ grouping, calibration math, critic rules, tier rendering) run with no LLM/network,
13
+ which keeps the pipeline testable.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from .models import (
19
+ Critique,
20
+ CritiqueIssue,
21
+ DeepAnalysisResult,
22
+ Provenance,
23
+ QuantEvidence,
24
+ ThemeGroup,
25
+ )
26
+ from .pipeline import DeepAnalysisPipeline, run_deep_analysis
27
+
28
+ __all__ = [
29
+ "Critique",
30
+ "CritiqueIssue",
31
+ "DeepAnalysisResult",
32
+ "Provenance",
33
+ "QuantEvidence",
34
+ "ThemeGroup",
35
+ "DeepAnalysisPipeline",
36
+ "run_deep_analysis",
37
+ ]
@@ -0,0 +1,144 @@
1
+ """P2 closed loop — log predictions, later score them against realised price.
2
+
3
+ Calibration only improves if confidence is checked against what actually happened.
4
+ This module:
5
+
6
+ 1. logs every deep verdict (symbol, signal, confidence, reference price, time),
7
+ 2. once a prediction's horizon has elapsed, fetches the realised return and
8
+ marks it correct/incorrect,
9
+ 3. feeds the outcome into CalibrationStore so the reliability factor — and thus
10
+ future calibrated confidence — drifts toward the true hit-rate.
11
+
12
+ Everything is injectable (price function, clock) so it tests without network.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import time
20
+ import uuid
21
+ from pathlib import Path
22
+ from typing import Callable, Dict, List, Optional
23
+
24
+ from .quant_fusion import CalibrationStore
25
+
26
+ _BULL = ("STRONG_BUY", "BUY")
27
+ _BEAR = ("STRONG_SELL", "SELL")
28
+
29
+
30
+ def correctness(signal: str, ret: float, threshold: float = 0.02) -> bool:
31
+ """Was the call right given the realised return?"""
32
+ if signal in _BULL:
33
+ return ret > threshold
34
+ if signal in _BEAR:
35
+ return ret < -threshold
36
+ return abs(ret) <= threshold # HOLD: right when it stayed flat
37
+
38
+
39
+ class PredictionLog:
40
+ """Append-only log of deep verdicts, JSON-list backed (low volume)."""
41
+
42
+ def __init__(self, path: Optional[Path] = None):
43
+ self.path = Path(path or os.path.expanduser("~/.arthera/deep_predictions.json"))
44
+ self._items: List[Dict] = []
45
+ try:
46
+ if self.path.exists():
47
+ self._items = json.loads(self.path.read_text())
48
+ except Exception:
49
+ self._items = []
50
+
51
+ def _save(self) -> None:
52
+ try:
53
+ self.path.parent.mkdir(parents=True, exist_ok=True)
54
+ self.path.write_text(json.dumps(self._items, ensure_ascii=False, indent=2))
55
+ except Exception:
56
+ pass
57
+
58
+ def log(self, symbol: str, signal: str, confidence: float,
59
+ ref_price: float, ts: Optional[float] = None) -> str:
60
+ pid = uuid.uuid4().hex[:12]
61
+ self._items.append({
62
+ "id": pid, "symbol": symbol, "signal": signal,
63
+ "confidence": float(confidence), "ref_price": float(ref_price),
64
+ "ts": ts if ts is not None else time.time(), "evaluated": False,
65
+ })
66
+ self._save()
67
+ return pid
68
+
69
+ def pending(self, horizon_days: float, now: Optional[float] = None) -> List[Dict]:
70
+ now = now if now is not None else time.time()
71
+ cutoff = now - horizon_days * 86400
72
+ return [p for p in self._items if not p.get("evaluated") and p["ts"] <= cutoff]
73
+
74
+ def mark_evaluated(self, pid: str, ret: float, correct: bool,
75
+ source: str = "price") -> None:
76
+ for p in self._items:
77
+ if p["id"] == pid:
78
+ p["evaluated"] = True
79
+ p["realised_return"] = ret
80
+ p["correct"] = correct
81
+ p["source"] = source
82
+ break
83
+ self._save()
84
+
85
+
86
+ def evaluate_due(
87
+ store: CalibrationStore,
88
+ log: PredictionLog,
89
+ price_fn: Callable[[str], Optional[float]],
90
+ horizon_days: float = 5.0,
91
+ threshold: float = 0.02,
92
+ now: Optional[float] = None,
93
+ ) -> Dict[str, int]:
94
+ """Score every prediction past its horizon and update calibration. Returns counts."""
95
+ evaluated = hits = 0
96
+ for p in log.pending(horizon_days, now=now):
97
+ try:
98
+ cur = price_fn(p["symbol"])
99
+ except Exception:
100
+ cur = None
101
+ if not cur or not p.get("ref_price"):
102
+ continue
103
+ ret = (cur - p["ref_price"]) / p["ref_price"]
104
+ ok = correctness(p["signal"], ret, threshold)
105
+ store.record_outcome(p["signal"], p["confidence"], ok)
106
+ log.mark_evaluated(p["id"], round(ret, 5), ok)
107
+ evaluated += 1
108
+ hits += 1 if ok else 0
109
+ return {"evaluated": evaluated, "hits": hits,
110
+ "hit_rate": round(hits / evaluated, 3) if evaluated else 0.0}
111
+
112
+
113
+ def evaluate_from_ledger(
114
+ store: CalibrationStore,
115
+ log: PredictionLog,
116
+ ledger,
117
+ ) -> Dict[str, int]:
118
+ """Score predictions against the portfolio ledger's REALISED P&L (actual closed
119
+ trades) — a stronger ground truth than market price for symbols you traded.
120
+
121
+ BUY is right when the symbol's realised P&L is positive, SELL when negative.
122
+ HOLD is skipped (no clean threshold without a cost basis). ``ledger`` is any
123
+ object exposing ``get_realized_pnl() -> [{symbol, realized_pnl, ...}]``.
124
+ """
125
+ try:
126
+ realized = ledger.get_realized_pnl()
127
+ except Exception:
128
+ return {"evaluated": 0, "hits": 0, "hit_rate": 0.0}
129
+ pnl_map = {str(r.get("symbol", "")).upper(): r.get("realized_pnl", 0.0)
130
+ for r in (realized or []) if r.get("realized_pnl")}
131
+
132
+ evaluated = hits = 0
133
+ for p in log.pending(0.0): # any un-evaluated prediction
134
+ sym = str(p["symbol"]).upper()
135
+ if sym not in pnl_map or p["signal"] not in (_BULL + _BEAR):
136
+ continue
137
+ pnl = pnl_map[sym]
138
+ ok = (pnl > 0) if p["signal"] in _BULL else (pnl < 0)
139
+ store.record_outcome(p["signal"], p["confidence"], ok)
140
+ log.mark_evaluated(p["id"], round(pnl, 2), ok, source="ledger")
141
+ evaluated += 1
142
+ hits += 1 if ok else 0
143
+ return {"evaluated": evaluated, "hits": hits,
144
+ "hit_rate": round(hits / evaluated, 3) if evaluated else 0.0}
agents/deep/critic.py ADDED
@@ -0,0 +1,125 @@
1
+ """P1b — a self-check pass over the assembled analysis.
2
+
3
+ Claude Code verifies its own work; the flat pipeline never did. The critic applies
4
+ deterministic rules (no LLM, fully testable) to flag the failure modes that quietly
5
+ ruin a research note: thin agent coverage, no risk angle, a strong call on weak
6
+ confidence, or the quant signal contradicting the qualitative verdict. An optional
7
+ LLM pass can add free-text findings on top.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import List, Optional
13
+
14
+ from ..base import AgentResult
15
+ from .models import Critique, CritiqueIssue, Provenance, QuantEvidence
16
+ from .themes import theme_of
17
+
18
+ _STRONG = ("STRONG_BUY", "STRONG_SELL")
19
+
20
+
21
+ def critique(
22
+ agent_results: List[AgentResult],
23
+ final_signal: str,
24
+ calibrated_confidence: float,
25
+ quant: Optional[QuantEvidence] = None,
26
+ agreement: str = "neutral",
27
+ provenance: Optional[List[Provenance]] = None,
28
+ key_point_count: int = 0,
29
+ ) -> Critique:
30
+ """Deterministic self-check. Returns a Critique; ``passed`` is False on any
31
+ high-severity issue so the caller can soften an over-confident conclusion."""
32
+ issues: List[CritiqueIssue] = []
33
+
34
+ total = len(agent_results)
35
+ ok = sum(1 for r in agent_results if r.success)
36
+
37
+ # 1) coverage
38
+ if ok < 2:
39
+ issues.append(CritiqueIssue("high", "thin_coverage",
40
+ f"只有 {ok} 个 agent 成功,结论证据不足,不应作为决策依据。"))
41
+ elif total and ok / total < 0.5:
42
+ issues.append(CritiqueIssue("medium", "thin_coverage",
43
+ f"{total - ok}/{total} 个 agent 失败,覆盖偏薄。"))
44
+
45
+ # 2) risk angle present?
46
+ has_risk = any(r.success and theme_of(r.agent) == "risk" for r in agent_results)
47
+ if not has_risk:
48
+ issues.append(CritiqueIssue("medium", "missing_risk",
49
+ "缺少有效的风险维度分析,下行风险可能被低估。"))
50
+
51
+ # 3) quant contradicts qualitative verdict
52
+ if quant and quant.available and agreement == "disagree":
53
+ issues.append(CritiqueIssue("high", "conflict",
54
+ f"量化信号({quant.verdict()})与定性结论({final_signal})相反,置信度已下调;建议人工复核。"))
55
+
56
+ # 4) strong call on weak confidence
57
+ if final_signal in _STRONG and calibrated_confidence < 0.5:
58
+ issues.append(CritiqueIssue("medium", "unsupported",
59
+ f"给出强信号 {final_signal} 但校准后置信度仅 {calibrated_confidence:.0%},"
60
+ "强度与把握不匹配。"))
61
+
62
+ # 5) strong call with little supporting evidence
63
+ if final_signal in _STRONG and key_point_count < 3:
64
+ issues.append(CritiqueIssue("medium", "unsupported",
65
+ "强信号但关键论据少于 3 条,论证偏薄。"))
66
+
67
+ # 6) stale data
68
+ for p in (provenance or []):
69
+ if p.freshness.endswith("d old"):
70
+ issues.append(CritiqueIssue("low", "stale_data",
71
+ f"{p.field} 数据为 {p.freshness}(来源 {p.source}),注意时效。"))
72
+
73
+ passed = not any(i.severity == "high" for i in issues)
74
+ return Critique(issues=issues, passed=passed)
75
+
76
+
77
+ def soften_signal(final_signal: str) -> str:
78
+ """Step a verdict one notch toward HOLD (used when the critic fails)."""
79
+ ladder = {"STRONG_BUY": "BUY", "BUY": "HOLD", "HOLD": "HOLD",
80
+ "SELL": "HOLD", "STRONG_SELL": "SELL"}
81
+ return ladder.get(final_signal, "HOLD")
82
+
83
+
84
+ _SEV = {"高": "high", "中": "medium", "低": "low",
85
+ "high": "high", "medium": "medium", "low": "low"}
86
+
87
+
88
+ def parse_llm_issues(text: str, max_issues: int = 3) -> List[CritiqueIssue]:
89
+ """Parse the LLM reviewer's reply (``高|问题`` per line) into issues. Pure/testable."""
90
+ import re
91
+ issues: List[CritiqueIssue] = []
92
+ for line in (text or "").splitlines():
93
+ line = line.strip().lstrip("-•*0123456789. ").strip()
94
+ if not line or line.upper().startswith("OK") or line in ("无", "无问题"):
95
+ continue
96
+ parts = re.split(r"[||::]", line, maxsplit=1)
97
+ if len(parts) == 2 and _SEV.get(parts[0].strip().lower()):
98
+ sev, msg = _SEV[parts[0].strip().lower()], parts[1].strip()
99
+ else:
100
+ sev, msg = "medium", line
101
+ if msg:
102
+ issues.append(CritiqueIssue(sev, "llm_review", msg))
103
+ if len(issues) >= max_issues:
104
+ break
105
+ return issues
106
+
107
+
108
+ async def llm_critique(
109
+ symbol: str,
110
+ synthesis: str,
111
+ theme_summaries: str,
112
+ llm,
113
+ max_issues: int = 3,
114
+ ) -> List[CritiqueIssue]:
115
+ """Optional LLM reviewer — flags unsupported claims / missing risk / contradiction
116
+ / overconfidence in the synthesis. Returns extra issues (empty if no LLM)."""
117
+ if llm is None or not (synthesis or "").strip():
118
+ return []
119
+ from .deepen import _collect_llm
120
+ system = ("你是严格的研究审稿人。只挑【确凿的】问题:无数据支撑的论断、漏掉的下行风险、"
121
+ "自相矛盾、过度自信。每行一个,格式 `高|问题` / `中|问题` / `低|问题`,"
122
+ f"最多 {max_issues} 条。没有问题就只输出 OK。不要多余的话。")
123
+ user = f"标的: {symbol}\n各维度小结: {theme_summaries}\n\n综合结论:\n{synthesis}\n\n审查:"
124
+ resp = await _collect_llm(llm, system, user, max_tokens=300)
125
+ return parse_llm_issues(resp, max_issues)