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
@@ -0,0 +1,145 @@
1
+ """
2
+ agents/financial/debate.py — 信号争议调解 Agent
3
+ ================================================
4
+ 当多个 Agent 出现真实分歧(看涨 vs 看跌)时,
5
+ DebateAgent 作为"裁判"对冲突进行分析,输出综合判断。
6
+ 不独立运行,由 AgentTeam 在检测到分歧时自动触发。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import Any, Dict, List
13
+
14
+ from ..base import BaseAgent, AgentResult
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class DebateAgent(BaseAgent):
20
+
21
+ name = "debate"
22
+ description = "信号争议调解 — 当多 Agent 信号冲突时自动触发,输出裁判视角"
23
+
24
+ _SYSTEM = (
25
+ "You are a senior investment committee chair mediating a dispute between "
26
+ "analysts who have conflicting views on a stock. Your role is to:\n"
27
+ "1. Identify the core disagreement\n"
28
+ "2. Evaluate which side has stronger evidence\n"
29
+ "3. Determine the dominant factor (macro vs technical vs fundamental)\n"
30
+ "4. Provide a nuanced resolution that acknowledges both sides\n"
31
+ "5. Conclude with a clear signal: BUY / HOLD / SELL and the primary reason\n"
32
+ "Be direct. Avoid empty hedging. Make a call."
33
+ )
34
+
35
+ async def fetch_data(self, symbol: str) -> Dict[str, Any]:
36
+ return await super().fetch_data(symbol)
37
+
38
+ async def analyze(self, symbol: str, data: Dict[str, Any]) -> AgentResult:
39
+ conflicting: List[Dict] = data.get("conflicting", [])
40
+
41
+ if not conflicting:
42
+ return AgentResult(
43
+ agent=self.name, symbol=symbol,
44
+ analysis="无冲突结果可调解。",
45
+ confidence=0.3, signal="HOLD",
46
+ key_points=["无需调解"],
47
+ )
48
+
49
+ debate_block = _format_conflict(conflicting)
50
+
51
+ prompt = (
52
+ f"Stock: {symbol}\n\n"
53
+ f"Conflicting Analyst Views:\n{debate_block}\n\n"
54
+ "Mediate this dispute. Which view is more compelling and why? "
55
+ "What is the dominant factor driving the stock right now? "
56
+ "End with: Signal: BUY / HOLD / SELL — [primary reason in one line]"
57
+ )
58
+
59
+ analysis = await self._call_llm(self._SYSTEM, prompt, max_tokens=600)
60
+ if not analysis:
61
+ analysis = _template_resolution(symbol, conflicting)
62
+
63
+ signal = _extract_signal(analysis)
64
+ confidence = _estimate_confidence(conflicting)
65
+ key_points = _build_key_points(conflicting, analysis)
66
+
67
+ return AgentResult(
68
+ agent=self.name, symbol=symbol,
69
+ analysis=analysis,
70
+ confidence=confidence,
71
+ signal=signal,
72
+ key_points=key_points,
73
+ data_used={"conflict_count": len(conflicting)},
74
+ )
75
+
76
+
77
+ # ── Helpers ───────────────────────────────────────────────────────────────────
78
+
79
+ def _format_conflict(results: List[Dict]) -> str:
80
+ lines = []
81
+ for r in results:
82
+ agent = r.get("agent", "unknown")
83
+ signal = r.get("signal", "HOLD")
84
+ conf = r.get("confidence", 0)
85
+ pts = r.get("key_points", [])
86
+ summary = "; ".join(pts[:3]) if pts else r.get("analysis", "")[:150]
87
+ lines.append(
88
+ f" [{agent.upper()}] Signal: {signal} (conf {conf:.0%})\n"
89
+ f" Key points: {summary}"
90
+ )
91
+ return "\n".join(lines)
92
+
93
+
94
+ def _extract_signal(analysis: str) -> str:
95
+ text = analysis.upper()
96
+ for marker in ("SIGNAL: ", "SIGNAL:", "CONCLUSION:", "CONCLUSION: "):
97
+ idx = text.find(marker)
98
+ if idx != -1:
99
+ remainder = text[idx + len(marker):].strip()
100
+ if remainder.startswith("STRONG_BUY"): return "STRONG_BUY"
101
+ if remainder.startswith("STRONG_SELL"): return "STRONG_SELL"
102
+ if remainder.startswith("BUY"): return "BUY"
103
+ if remainder.startswith("SELL"): return "SELL"
104
+ if "BUY" in text and "SELL" not in text: return "BUY"
105
+ if "SELL" in text and "BUY" not in text: return "SELL"
106
+ return "HOLD"
107
+
108
+
109
+ def _estimate_confidence(results: List[Dict]) -> float:
110
+ if not results:
111
+ return 0.4
112
+ confs = [r.get("confidence", 0.5) for r in results if r.get("confidence")]
113
+ avg = sum(confs) / len(confs) if confs else 0.5
114
+ return round(min(avg * 0.9, 0.75), 2)
115
+
116
+
117
+ def _build_key_points(results: List[Dict], analysis: str) -> List[str]:
118
+ bullish = [r["agent"] for r in results if r.get("signal") in ("BUY", "STRONG_BUY")]
119
+ bearish = [r["agent"] for r in results if r.get("signal") in ("SELL", "STRONG_SELL")]
120
+ points = []
121
+ if bullish:
122
+ points.append(f"看涨方: {', '.join(bullish)}")
123
+ if bearish:
124
+ points.append(f"看跌方: {', '.join(bearish)}")
125
+ points.append("DebateAgent 已介入调解")
126
+ sig = _extract_signal(analysis)
127
+ points.append(f"裁判结论: {sig}")
128
+ return points
129
+
130
+
131
+ def _template_resolution(symbol: str, results: List[Dict]) -> str:
132
+ bullish = [r for r in results if r.get("signal") in ("BUY", "STRONG_BUY")]
133
+ bearish = [r for r in results if r.get("signal") in ("SELL", "STRONG_SELL")]
134
+ if len(bullish) > len(bearish):
135
+ resolution = "BUY — 多数看涨信号占优"
136
+ elif len(bearish) > len(bullish):
137
+ resolution = "SELL — 多数看跌信号占优"
138
+ else:
139
+ resolution = "HOLD — 多空力量均衡,建议观望"
140
+ return (
141
+ f"{symbol} 信号冲突调解报告\n"
142
+ f"看涨方: {', '.join(r['agent'] for r in bullish) or '无'}\n"
143
+ f"看跌方: {', '.join(r['agent'] for r in bearish) or '无'}\n"
144
+ f"裁判结论: Signal: {resolution}"
145
+ )
@@ -0,0 +1,303 @@
1
+ """
2
+ agents/financial/earnings.py — 财报解读 Agent
3
+ =============================================
4
+ 分析最近一期财报:EPS/营收 beat or miss、同比/环比变化、
5
+ 指引调整及市场反应。在财报发布后 5 天内尤为有效。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from datetime import datetime, timedelta
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ from ..base import BaseAgent, AgentResult
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class EarningsAgent(BaseAgent):
20
+
21
+ name = "earnings"
22
+ description = "财报解读 — EPS/营收 beat or miss、同比增速、指引变化"
23
+
24
+ _SYSTEM = (
25
+ "You are a financial analyst specializing in earnings analysis. "
26
+ "Evaluate the most recent quarterly earnings report:\n"
27
+ "1. Was it a beat or miss on EPS and revenue?\n"
28
+ "2. How does this compare to prior quarters (trend)?\n"
29
+ "3. Was guidance raised, lowered, or maintained?\n"
30
+ "4. What is the market likely to do with this information?\n"
31
+ "5. Conclude: STRONG_BEAT / BEAT / IN_LINE / MISS / STRONG_MISS\n"
32
+ "Map to signal: BEAT/STRONG_BEAT→BUY, IN_LINE→HOLD, MISS/STRONG_MISS→SELL"
33
+ )
34
+
35
+ async def fetch_data(self, symbol: str) -> Dict[str, Any]:
36
+ data = await super().fetch_data(symbol)
37
+ earnings: Dict[str, Any] = {}
38
+
39
+ try:
40
+ import yfinance as yf
41
+ ticker = yf.Ticker(symbol)
42
+
43
+ # 1. Quarterly earnings history
44
+ try:
45
+ qe = ticker.quarterly_earnings
46
+ if qe is not None and not qe.empty:
47
+ rows = []
48
+ for ts, row in qe.sort_index(ascending=False).head(4).iterrows():
49
+ rows.append({
50
+ "period": str(ts)[:10],
51
+ "actual_eps": _safe_float(row.get("Earnings")),
52
+ "est_eps": _safe_float(row.get("Estimate", row.get("EPS Estimate"))),
53
+ "revenue": _safe_float(row.get("Revenue")),
54
+ "rev_estimate": _safe_float(row.get("Revenue Estimate")),
55
+ })
56
+ earnings["quarterly_eps"] = rows
57
+ except Exception as e:
58
+ logger.debug("[earnings] quarterly_earnings %s: %s", symbol, e)
59
+
60
+ # 2. Earnings dates
61
+ try:
62
+ ed = ticker.earnings_dates
63
+ if ed is not None and not ed.empty:
64
+ recent = ed.sort_index(ascending=False).head(2)
65
+ dates = []
66
+ for ts, row in recent.iterrows():
67
+ reported_eps = _safe_float(row.get("Reported EPS", row.get("EPS")))
68
+ est_eps = _safe_float(row.get("EPS Estimate"))
69
+ if reported_eps is not None:
70
+ beat = reported_eps - est_eps if est_eps is not None else None
71
+ dates.append({
72
+ "date": str(ts)[:10],
73
+ "reported_eps": reported_eps,
74
+ "estimated_eps": est_eps,
75
+ "beat_by": round(beat, 4) if beat is not None else None,
76
+ "pct_surprise": round(beat / abs(est_eps) * 100, 1)
77
+ if (beat is not None and est_eps and est_eps != 0) else None,
78
+ })
79
+ earnings["recent_reports"] = dates
80
+ except Exception as e:
81
+ logger.debug("[earnings] earnings_dates %s: %s", symbol, e)
82
+
83
+ # 3. Revenue trend (quarterly income)
84
+ try:
85
+ qs = ticker.quarterly_financials
86
+ if qs is not None and not qs.empty:
87
+ rev_row = None
88
+ for label in ("Total Revenue", "Revenue", "Net Revenue"):
89
+ if label in qs.index:
90
+ rev_row = qs.loc[label]
91
+ break
92
+ if rev_row is not None:
93
+ revs = []
94
+ for ts, val in rev_row.sort_index(ascending=False).head(4).items():
95
+ revs.append({"period": str(ts)[:10], "revenue": _safe_float(val)})
96
+ earnings["revenue_trend"] = revs
97
+ except Exception as e:
98
+ logger.debug("[earnings] quarterly_financials %s: %s", symbol, e)
99
+
100
+ # 4. Stock price reaction (compare price before/after earnings)
101
+ try:
102
+ recent = earnings.get("recent_reports", [])
103
+ if recent:
104
+ from datetime import date
105
+ report_date_str = recent[0].get("date", "")
106
+ if report_date_str:
107
+ hist = ticker.history(start=report_date_str, period="5d")
108
+ if not hist.empty and len(hist) >= 2:
109
+ open_price = float(hist["Open"].iloc[0])
110
+ close_price = float(hist["Close"].iloc[-1])
111
+ earnings["price_reaction_pct"] = round(
112
+ (close_price - open_price) / open_price * 100, 2
113
+ )
114
+ except Exception as e:
115
+ logger.debug("[earnings] price_reaction %s: %s", symbol, e)
116
+
117
+ except Exception as e:
118
+ logger.debug("[earnings] yfinance init %s: %s", symbol, e)
119
+
120
+ data["earnings"] = earnings
121
+ return data
122
+
123
+ async def analyze(self, symbol: str, data: Dict[str, Any]) -> AgentResult:
124
+ earnings = data.get("earnings", {})
125
+ quote = data.get("quote", {})
126
+ price = quote.get("price", 0)
127
+
128
+ if not earnings:
129
+ return AgentResult(
130
+ agent=self.name, symbol=symbol,
131
+ analysis=f"{symbol}: 未获取到财报数据。",
132
+ confidence=0.3, signal="HOLD",
133
+ key_points=["无财报数据"],
134
+ )
135
+
136
+ earnings_block = _format_earnings(earnings)
137
+ most_recent = earnings.get("recent_reports", [{}])[0] if earnings.get("recent_reports") else {}
138
+ beat_miss_summary = _assess_beat_miss(most_recent, earnings)
139
+
140
+ prompt = (
141
+ f"Stock: {symbol} Price: {price}\n\n"
142
+ f"Earnings Data:\n{earnings_block}\n\n"
143
+ f"Initial Assessment: {beat_miss_summary}\n\n"
144
+ "Provide detailed earnings analysis:\n"
145
+ "1. Most recent quarter: beat or miss? By how much?\n"
146
+ "2. Revenue & EPS trend direction (improving / deteriorating / stable)\n"
147
+ "3. Likely market interpretation\n"
148
+ "4. Conclude: STRONG_BEAT / BEAT / IN_LINE / MISS / STRONG_MISS"
149
+ )
150
+
151
+ analysis = await self._call_llm(self._SYSTEM, prompt, max_tokens=500)
152
+ if not analysis:
153
+ analysis = _template_analysis(symbol, earnings, most_recent, beat_miss_summary)
154
+
155
+ signal, confidence = _derive_signal(analysis, most_recent, earnings)
156
+ key_points = _build_key_points(earnings, most_recent, signal)
157
+
158
+ return AgentResult(
159
+ agent=self.name, symbol=symbol,
160
+ analysis=analysis,
161
+ confidence=confidence,
162
+ signal=signal,
163
+ key_points=key_points,
164
+ data_used={
165
+ "beat_miss": beat_miss_summary,
166
+ "pct_surprise": most_recent.get("pct_surprise"),
167
+ "price_reaction_pct": earnings.get("price_reaction_pct"),
168
+ },
169
+ )
170
+
171
+
172
+ # ── Helpers ───────────────────────────────────────────────────────────────────
173
+
174
+ def _safe_float(val) -> Optional[float]:
175
+ try:
176
+ v = float(val)
177
+ return None if (v != v) else v # NaN check
178
+ except (TypeError, ValueError):
179
+ return None
180
+
181
+
182
+ def _format_earnings(e: Dict) -> str:
183
+ lines = []
184
+ recent = e.get("recent_reports", [])
185
+ if recent:
186
+ lines.append("Recent Reports:")
187
+ for r in recent[:2]:
188
+ act = r.get("reported_eps")
189
+ est = r.get("estimated_eps")
190
+ sup = r.get("pct_surprise")
191
+ lines.append(
192
+ f" {r.get('date','')} EPS: actual {act} est {est} "
193
+ f"surprise {sup:+.1f}%" if sup else
194
+ f" {r.get('date','')} EPS: actual {act} est {est}"
195
+ )
196
+
197
+ rx = e.get("price_reaction_pct")
198
+ if rx is not None:
199
+ lines.append(f"Post-earnings price reaction: {rx:+.1f}%")
200
+
201
+ qt = e.get("quarterly_eps", [])
202
+ if qt:
203
+ lines.append("Quarterly EPS (last 4):")
204
+ for q in qt[:4]:
205
+ ep = q.get("actual_eps")
206
+ lines.append(f" {q.get('period','')} EPS: {ep}")
207
+
208
+ rev = e.get("revenue_trend", [])
209
+ if rev:
210
+ lines.append("Revenue Trend (last 4 quarters):")
211
+ prev = None
212
+ for r in rev[:4]:
213
+ rv = r.get("revenue")
214
+ growth = ""
215
+ if rv and prev:
216
+ g = (rv - prev) / abs(prev) * 100
217
+ growth = f" {g:+.1f}% QoQ"
218
+ lines.append(f" {r.get('period','')} {_fmt_num(rv)}{growth}")
219
+ prev = rv
220
+
221
+ return "\n".join(lines) or "无财报数据"
222
+
223
+
224
+ def _fmt_num(v) -> str:
225
+ if v is None: return "N/A"
226
+ if abs(v) >= 1e9: return f"${v/1e9:.2f}B"
227
+ if abs(v) >= 1e6: return f"${v/1e6:.1f}M"
228
+ return f"{v:.4f}"
229
+
230
+
231
+ def _assess_beat_miss(most_recent: Dict, earnings: Dict) -> str:
232
+ sup = most_recent.get("pct_surprise")
233
+ if sup is None:
234
+ return "无充分数据判断"
235
+ if sup >= 10: return f"大幅超预期 +{sup:.1f}%"
236
+ if sup >= 3: return f"超预期 +{sup:.1f}%"
237
+ if sup >= -3: return f"基本符合预期 {sup:+.1f}%"
238
+ if sup >= -10: return f"略低预期 {sup:.1f}%"
239
+ return f"大幅低于预期 {sup:.1f}%"
240
+
241
+
242
+ def _derive_signal(analysis: str, most_recent: Dict, earnings: Dict) -> tuple[str, float]:
243
+ text = analysis.upper()
244
+ sup = most_recent.get("pct_surprise")
245
+ rx = earnings.get("price_reaction_pct")
246
+
247
+ if "STRONG_BEAT" in text:
248
+ return "BUY", 0.75
249
+ if "STRONG_MISS" in text:
250
+ return "SELL", 0.75
251
+ if "BEAT" in text and "MISS" not in text:
252
+ conf = 0.65 if (rx and rx > 2) else 0.55
253
+ return "BUY", conf
254
+ if "MISS" in text and "BEAT" not in text:
255
+ conf = 0.65 if (rx and rx < -2) else 0.55
256
+ return "SELL", conf
257
+
258
+ # Fallback to raw surprise
259
+ if sup is not None:
260
+ if sup >= 10: return "BUY", 0.70
261
+ if sup >= 3: return "BUY", 0.55
262
+ if sup <= -10: return "SELL", 0.70
263
+ if sup <= -3: return "SELL", 0.55
264
+
265
+ return "HOLD", 0.40
266
+
267
+
268
+ def _build_key_points(earnings: Dict, most_recent: Dict, signal: str) -> List[str]:
269
+ pts = []
270
+ sup = most_recent.get("pct_surprise")
271
+ if sup is not None:
272
+ pts.append(f"EPS 超预期 {sup:+.1f}%" if sup >= 0 else f"EPS 低于预期 {sup:.1f}%")
273
+ rx = earnings.get("price_reaction_pct")
274
+ if rx is not None:
275
+ pts.append(f"财报后股价反应: {rx:+.1f}%")
276
+ rev = earnings.get("revenue_trend", [])
277
+ if len(rev) >= 2:
278
+ v0 = rev[0].get("revenue")
279
+ v1 = rev[1].get("revenue")
280
+ if v0 and v1:
281
+ qoq = (v0 - v1) / abs(v1) * 100
282
+ pts.append(f"营收环比 {qoq:+.1f}%")
283
+ pts.append(f"财报信号: {signal}")
284
+ return pts[:5]
285
+
286
+
287
+ def _template_analysis(symbol: str, earnings: Dict, most_recent: Dict, summary: str) -> str:
288
+ sup = most_recent.get("pct_surprise")
289
+ if sup is not None:
290
+ if sup >= 10: verdict = "STRONG_BEAT"
291
+ elif sup >= 3: verdict = "BEAT"
292
+ elif sup >= -3: verdict = "IN_LINE"
293
+ elif sup >= -10:verdict = "MISS"
294
+ else: verdict = "STRONG_MISS"
295
+ else:
296
+ verdict = "IN_LINE"
297
+
298
+ return (
299
+ f"{symbol} 财报解读(模板):\n"
300
+ f"初步判断:{summary}\n"
301
+ f"财报评级:{verdict}\n"
302
+ f"建议信号:{'BUY' if 'BEAT' in verdict else ('SELL' if 'MISS' in verdict else 'HOLD')}"
303
+ )
@@ -0,0 +1,159 @@
1
+ """
2
+ agents/financial/fundamental.py — 基本面分析 Agent
3
+ ====================================================
4
+ 分析:PE/PB/ROE、营收增速、竞争壁垒、估值水位。
5
+ """
6
+ from __future__ import annotations
7
+ from typing import Any, Dict, List, Optional
8
+ from ..base import BaseAgent, AgentResult
9
+
10
+
11
+ class FundamentalAgent(BaseAgent):
12
+ name = "fundamental"
13
+ description = "基本面分析:估值/ROE/竞争优势/财务健康"
14
+
15
+ _SYSTEM = (
16
+ "You are a fundamental equity analyst. Evaluate: PE/PB valuation levels "
17
+ "(vs historical range and sector peers), ROE quality, revenue growth, "
18
+ "balance sheet health, and competitive moat. "
19
+ "Be data-driven. End with: UNDERVALUED / FAIRLY_VALUED / OVERVALUED."
20
+ )
21
+
22
+ async def fetch_data(self, symbol: str) -> Dict[str, Any]:
23
+ data = await super().fetch_data(symbol)
24
+ if self.data:
25
+ try:
26
+ f = self.data.fundamentals(symbol)
27
+ if f:
28
+ data["fundamentals"] = {
29
+ "pe_ttm": f.pe_ttm,
30
+ "pb": f.pb,
31
+ "roe": f.roe,
32
+ "revenue_growth": f.revenue_growth,
33
+ "dividend_yield": f.dividend_yield,
34
+ "source": f.source,
35
+ }
36
+ except Exception:
37
+ pass
38
+ return data
39
+
40
+ async def analyze(self, symbol: str, data: Dict[str, Any]) -> AgentResult:
41
+ quote = data.get("quote", {})
42
+ fund = data.get("fundamentals", {})
43
+ price = _num_or_none(quote.get("price"))
44
+ # Accept both schemas: the agent's own fetch_data (pe_ttm/pb) AND the
45
+ # shared team data bundle (pe_ratio/pb_ratio from finnhub/yahoo).
46
+ pe = _num_or_none(
47
+ fund.get("pe_ttm") or fund.get("pe_ratio") or fund.get("pe")
48
+ or quote.get("pe_ttm") or quote.get("pe_ratio")
49
+ )
50
+ pb = _num_or_none(
51
+ fund.get("pb") or fund.get("pb_ratio")
52
+ or quote.get("pb") or quote.get("pb_ratio")
53
+ )
54
+ roe = _num_or_none(fund.get("roe"))
55
+ rev_g = _num_or_none(fund.get("revenue_growth") or fund.get("rev_growth"))
56
+ div_y = _num_or_none(fund.get("dividend_yield"))
57
+
58
+ fund_str = (
59
+ f" PE(TTM): {_fmt_num(pe, 1, 'x')}\n"
60
+ f" PB: {_fmt_num(pb, 2, 'x')}\n"
61
+ f" ROE: {_fmt_num(roe, 1, '%')}\n"
62
+ f" Revenue growth: {_fmt_num(rev_g, 1, '%')}\n"
63
+ f" Dividend yield: {_fmt_num(div_y, 2, '%')}"
64
+ ) if pe or pb else " (fundamental data unavailable)"
65
+
66
+ prompt = (
67
+ f"Stock: {symbol} Price: {price}\n"
68
+ f"Fundamentals:\n{fund_str}\n\n"
69
+ "Evaluate:\n"
70
+ "1. Valuation (PE/PB vs historical/sector)\n"
71
+ "2. Profitability quality (ROE trend)\n"
72
+ "3. Growth outlook (revenue/earnings)\n"
73
+ "4. Balance sheet and dividend\n"
74
+ "5. Competitive moat\n"
75
+ "Conclusion: UNDERVALUED / FAIRLY_VALUED / OVERVALUED"
76
+ )
77
+
78
+ analysis = await self._call_llm(self._SYSTEM, prompt, max_tokens=500, quote=quote)
79
+ if not analysis:
80
+ analysis = _template_fundamental(symbol, pe, pb, roe, rev_g)
81
+
82
+ signal = _extract_signal(analysis, pe or 0)
83
+ confidence = _calc_confidence(pe or 0, pb or 0, roe or 0)
84
+ key_points = _extract_key_points(analysis)
85
+
86
+ return AgentResult(
87
+ agent=self.name, symbol=symbol,
88
+ analysis=analysis, confidence=confidence,
89
+ signal=signal, key_points=key_points,
90
+ data_used={"pe": pe, "pb": pb, "roe": roe},
91
+ )
92
+
93
+
94
+ def _num_or_none(value: Any) -> Optional[float]:
95
+ try:
96
+ if value is None:
97
+ return None
98
+ out = float(value)
99
+ return out if out != 0 else None
100
+ except (TypeError, ValueError):
101
+ return None
102
+
103
+
104
+ def _fmt_num(value: Optional[float], digits: int = 1, suffix: str = "") -> str:
105
+ if value is None:
106
+ return "数据不足"
107
+ return f"{value:.{digits}f}{suffix}"
108
+
109
+
110
+ def _extract_signal(text: str, pe: float = 0) -> str:
111
+ t = text.upper()
112
+ if "UNDERVALUED" in t or "低估" in t: return "BUY"
113
+ if "OVERVALUED" in t or "高估" in t: return "SELL"
114
+ # PE 辅助判断
115
+ if pe > 0:
116
+ if pe < 15: return "BUY"
117
+ if pe > 50: return "SELL"
118
+ return "HOLD"
119
+
120
+
121
+ def _calc_confidence(pe: float, pb: float, roe: float) -> float:
122
+ if pe <= 0 and pb <= 0 and roe == 0:
123
+ return 0.4 # 无数据,低置信度
124
+ score = 0.5
125
+ if pe > 0:
126
+ if pe < 15: score += 0.15
127
+ elif pe < 25: score += 0.05
128
+ elif pe > 50: score -= 0.15
129
+ if roe > 20: score += 0.15
130
+ elif roe > 10: score += 0.05
131
+ return round(min(0.95, max(0.2, score)), 2)
132
+
133
+
134
+ def _extract_key_points(text: str) -> List[str]:
135
+ points = []
136
+ for line in text.split("\n"):
137
+ line = line.strip()
138
+ if line.startswith(("1.", "2.", "3.", "4.", "5.", "•", "-", "·")) and len(line) > 5:
139
+ points.append(line.lstrip("1234567890.-•· "))
140
+ return points[:4]
141
+
142
+
143
+ def _template_fundamental(symbol: str, pe: Optional[float], pb: Optional[float],
144
+ roe: Optional[float], rev_g: Optional[float]) -> str:
145
+ if not pe or pe <= 0:
146
+ valuation, conclusion = "数据不足", "DATA_LIMITED"
147
+ elif pe < 15:
148
+ valuation, conclusion = "低估", "UNDERVALUED"
149
+ elif pe > 40:
150
+ valuation, conclusion = "高估", "OVERVALUED"
151
+ else:
152
+ valuation, conclusion = "合理", "FAIRLY_VALUED"
153
+ return (
154
+ f"{symbol} 基本面分析(模板):\n"
155
+ f"• 估值:PE={_fmt_num(pe, 1, 'x')} PB={_fmt_num(pb, 2, 'x')} → {valuation}\n"
156
+ f"• 盈利能力:ROE={_fmt_num(roe, 1, '%')} 营收增速={_fmt_num(rev_g, 1, '%')}\n"
157
+ "• 基本面数据不足时,不应把缺失值当作 0;建议结合财报和行业对比复核\n"
158
+ f"• 结论: {conclusion}"
159
+ )