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,279 @@
1
+ """
2
+ agents/financial/sector.py — 行业轮动分析 Agent
3
+ ================================================
4
+ 分析标的所属行业的相对强弱:行业指数表现、资金流入/流出趋势、
5
+ 板块轮动阶段,以判断标的是否处于顺风还是逆风环境。
6
+ 数据源:yfinance 行业 ETF(XLK/XLF/XLE/…)+ akshare 行业指数(A股)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ from ..base import BaseAgent, AgentResult
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # 美股行业 ETF 映射 (GICS Sectors)
19
+ _US_SECTOR_ETFS: Dict[str, str] = {
20
+ "Technology": "XLK",
21
+ "Health Care": "XLV",
22
+ "Financials": "XLF",
23
+ "Consumer Discretionary": "XLY",
24
+ "Consumer Staples": "XLP",
25
+ "Industrials": "XLI",
26
+ "Energy": "XLE",
27
+ "Materials": "XLB",
28
+ "Real Estate": "XLRE",
29
+ "Utilities": "XLU",
30
+ "Communication Services": "XLC",
31
+ }
32
+
33
+ # 默认参考基准
34
+ _BENCHMARK = "SPY"
35
+
36
+
37
+ class SectorAgent(BaseAgent):
38
+
39
+ name = "sector"
40
+ description = "行业轮动 — 所属板块相对强弱、资金流入/流出趋势"
41
+
42
+ _SYSTEM = (
43
+ "You are a sector rotation analyst. Given a stock and its sector's "
44
+ "recent performance relative to the broader market, assess:\n"
45
+ "1. Is the sector in an uptrend or downtrend vs the S&P 500?\n"
46
+ "2. Is money flowing INTO or OUT OF this sector recently?\n"
47
+ "3. What stage of the sector rotation cycle is this sector in "
48
+ "(early/mid/late recovery, contraction)?\n"
49
+ "4. Does the sector tailwind/headwind support BUY/HOLD/SELL on the stock?\n"
50
+ "Conclude: TAILWIND (sector supports bullish) / NEUTRAL / HEADWIND (sector drags bearish)"
51
+ )
52
+
53
+ async def fetch_data(self, symbol: str) -> Dict[str, Any]:
54
+ data = await super().fetch_data(symbol)
55
+ sector_data: Dict[str, Any] = {}
56
+
57
+ try:
58
+ import yfinance as yf
59
+ ticker = yf.Ticker(symbol)
60
+
61
+ # 1. Identify sector from stock info
62
+ info = ticker.info or {}
63
+ sector = info.get("sector", "")
64
+ industry = info.get("industry", "")
65
+ sector_data["sector"] = sector
66
+ sector_data["industry"] = industry
67
+
68
+ # 2. Get sector ETF for US stocks
69
+ etf_sym = _US_SECTOR_ETFS.get(sector)
70
+ if not etf_sym:
71
+ # Try to guess from ticker pattern for A-shares
72
+ if _is_a_share(symbol):
73
+ sector_data["market"] = "CN"
74
+ sector_data["etf_sym"] = None
75
+ else:
76
+ sector_data["market"] = "US"
77
+ sector_data["etf_sym"] = None
78
+ else:
79
+ sector_data["etf_sym"] = etf_sym
80
+ sector_data["market"] = "US"
81
+
82
+ # 3. Fetch returns for sector ETF and benchmark
83
+ comparisons: Dict[str, Dict] = {}
84
+ for periods in [("1mo", 21), ("3mo", 63), ("6mo", 126)]:
85
+ period_key, _ = periods[0], periods[1]
86
+ comparisons[period_key] = {}
87
+
88
+ syms_to_fetch = [_BENCHMARK]
89
+ if etf_sym:
90
+ syms_to_fetch.append(etf_sym)
91
+
92
+ for fetch_sym in syms_to_fetch:
93
+ try:
94
+ hist = yf.Ticker(fetch_sym).history(period="6mo")
95
+ if hist.empty:
96
+ continue
97
+ close = hist["Close"]
98
+ for period_key, days in [("1mo", 21), ("3mo", 63), ("6mo", len(close))]:
99
+ if len(close) >= days:
100
+ ret = (float(close.iloc[-1]) - float(close.iloc[-days])) / float(close.iloc[-days])
101
+ comparisons[period_key][fetch_sym] = round(ret * 100, 2)
102
+ except Exception as e:
103
+ logger.debug("[sector] fetch %s: %s", fetch_sym, e)
104
+
105
+ sector_data["comparisons"] = comparisons
106
+
107
+ # 4. Stock's own recent performance vs sector
108
+ try:
109
+ hist = ticker.history(period="3mo")
110
+ if not hist.empty and len(hist) >= 21:
111
+ close = hist["Close"]
112
+ stock_1mo = (float(close.iloc[-1]) - float(close.iloc[-21])) / float(close.iloc[-21]) * 100
113
+ sector_data["stock_1mo_return"] = round(stock_1mo, 2)
114
+ except Exception as e:
115
+ logger.debug("[sector] stock return %s: %s", symbol, e)
116
+
117
+ # 5. A-share sector data via akshare
118
+ if _is_a_share(symbol):
119
+ try:
120
+ import akshare as ak
121
+ sector_df = ak.stock_board_industry_name_em()
122
+ if sector_df is not None and not sector_df.empty:
123
+ sector_data["cn_sectors_available"] = True
124
+ except Exception:
125
+ pass
126
+
127
+ except Exception as e:
128
+ logger.debug("[sector] yfinance init %s: %s", symbol, e)
129
+
130
+ data["sector_data"] = sector_data
131
+ return data
132
+
133
+ async def analyze(self, symbol: str, data: Dict[str, Any]) -> AgentResult:
134
+ sd = data.get("sector_data", {})
135
+ quote = data.get("quote", {})
136
+ price = quote.get("price", 0)
137
+
138
+ if not sd:
139
+ return AgentResult(
140
+ agent=self.name, symbol=symbol,
141
+ analysis=f"{symbol}: 未获取到行业数据。",
142
+ confidence=0.3, signal="HOLD",
143
+ key_points=["无行业数据"],
144
+ )
145
+
146
+ sector_block = _format_sector_stats(sd)
147
+ rel_strength = _compute_relative_strength(sd)
148
+
149
+ prompt = (
150
+ f"Stock: {symbol} Sector: {sd.get('sector', 'Unknown')} "
151
+ f"Industry: {sd.get('industry', 'Unknown')}\n\n"
152
+ f"Sector Performance vs Market:\n{sector_block}\n\n"
153
+ f"Relative Strength Assessment: {rel_strength}\n\n"
154
+ "Analyze sector rotation dynamics and conclude:\n"
155
+ "TAILWIND / NEUTRAL / HEADWIND"
156
+ )
157
+
158
+ analysis = await self._call_llm(self._SYSTEM, prompt, max_tokens=450)
159
+ if not analysis:
160
+ analysis = _template_analysis(symbol, sd, rel_strength)
161
+
162
+ signal, confidence = _derive_signal(analysis, sd, rel_strength)
163
+ key_points = _build_key_points(sd, rel_strength, signal)
164
+
165
+ return AgentResult(
166
+ agent=self.name, symbol=symbol,
167
+ analysis=analysis,
168
+ confidence=confidence,
169
+ signal=signal,
170
+ key_points=key_points,
171
+ data_used={
172
+ "sector": sd.get("sector"),
173
+ "etf": sd.get("etf_sym"),
174
+ "rel_strength": rel_strength,
175
+ },
176
+ )
177
+
178
+
179
+ # ── Helpers ───────────────────────────────────────────────────────────────────
180
+
181
+ def _is_a_share(symbol: str) -> bool:
182
+ import re
183
+ return bool(re.match(r"^[036]\d{5}$", symbol))
184
+
185
+
186
+ def _format_sector_stats(sd: Dict) -> str:
187
+ lines = []
188
+ sector = sd.get("sector", "Unknown")
189
+ etf_sym = sd.get("etf_sym", "")
190
+ comps = sd.get("comparisons", {})
191
+ lines.append(f"Sector: {sector}{f' ETF: {etf_sym}' if etf_sym else ''}")
192
+
193
+ if comps:
194
+ lines.append(f"\n{'Period':<8} {'SPY':>8} {etf_sym or 'SECTOR':>8} Rel.Strength")
195
+ for period in ("1mo", "3mo", "6mo"):
196
+ c = comps.get(period, {})
197
+ spy = c.get(_BENCHMARK)
198
+ etf = c.get(etf_sym) if etf_sym else None
199
+ spy_str = f"{spy:+.1f}%" if spy is not None else " N/A"
200
+ etf_str = f"{etf:+.1f}%" if etf is not None else " N/A"
201
+ rel = ""
202
+ if spy is not None and etf is not None:
203
+ diff = etf - spy
204
+ rel = f"{diff:+.1f}% vs SPY"
205
+ lines.append(f"{period:<8} {spy_str:>8} {etf_str:>8} {rel}")
206
+
207
+ stock_1mo = sd.get("stock_1mo_return")
208
+ if stock_1mo is not None:
209
+ lines.append(f"\nStock 1-month: {stock_1mo:+.1f}%")
210
+
211
+ return "\n".join(lines)
212
+
213
+
214
+ def _compute_relative_strength(sd: Dict) -> str:
215
+ etf_sym = sd.get("etf_sym")
216
+ comps = sd.get("comparisons", {})
217
+ if not etf_sym or not comps:
218
+ return "无法计算(无行业 ETF 数据)"
219
+
220
+ scores = []
221
+ for period in ("1mo", "3mo"):
222
+ c = comps.get(period, {})
223
+ spy = c.get(_BENCHMARK)
224
+ etf = c.get(etf_sym)
225
+ if spy is not None and etf is not None:
226
+ scores.append(etf - spy)
227
+
228
+ if not scores:
229
+ return "数据不足"
230
+ avg_rs = sum(scores) / len(scores)
231
+ if avg_rs > 3:
232
+ return f"行业强于大盘 +{avg_rs:.1f}%(顺风)"
233
+ if avg_rs > 0:
234
+ return f"行业小幅跑赢 +{avg_rs:.1f}%(中性偏正)"
235
+ if avg_rs > -3:
236
+ return f"行业小幅落后 {avg_rs:.1f}%(中性偏负)"
237
+ return f"行业明显弱于大盘 {avg_rs:.1f}%(逆风)"
238
+
239
+
240
+ def _derive_signal(analysis: str, sd: Dict, rel_str: str) -> tuple[str, float]:
241
+ text = analysis.upper()
242
+ if "TAILWIND" in text:
243
+ return "BUY", 0.55
244
+ if "HEADWIND" in text:
245
+ return "SELL", 0.55
246
+
247
+ # Fallback: use relative strength
248
+ if "顺风" in rel_str or "跑赢" in rel_str:
249
+ return "BUY", 0.45
250
+ if "逆风" in rel_str or "落后" in rel_str:
251
+ return "SELL", 0.45
252
+ return "HOLD", 0.40
253
+
254
+
255
+ def _build_key_points(sd: Dict, rel_str: str, signal: str) -> List[str]:
256
+ pts = []
257
+ sec = sd.get("sector", "")
258
+ if sec:
259
+ pts.append(f"行业: {sec}")
260
+ etf = sd.get("etf_sym")
261
+ if etf:
262
+ pts.append(f"对应 ETF: {etf}")
263
+ pts.append(f"板块强弱: {rel_str[:40]}")
264
+ stock_1mo = sd.get("stock_1mo_return")
265
+ if stock_1mo is not None:
266
+ pts.append(f"个股1月涨跌: {stock_1mo:+.1f}%")
267
+ pts.append(f"行业信号: {signal}")
268
+ return pts[:5]
269
+
270
+
271
+ def _template_analysis(symbol: str, sd: Dict, rel_str: str) -> str:
272
+ sector = sd.get("sector", "Unknown")
273
+ verdict = "TAILWIND" if "顺风" in rel_str else ("HEADWIND" if "逆风" in rel_str else "NEUTRAL")
274
+ return (
275
+ f"{symbol} 行业分析(模板)\n"
276
+ f"所属行业: {sector}\n"
277
+ f"行业强弱: {rel_str}\n"
278
+ f"结论: {verdict}"
279
+ )
@@ -0,0 +1,274 @@
1
+ """
2
+ agents/financial/synthesis.py — 综合汇总 Agent
3
+ ===============================================
4
+ 汇总所有 agent 结果,输出可操作的投资建议。
5
+ 加权规则:置信度平方作为权重,高置信度信号影响力更大。
6
+ """
7
+ from __future__ import annotations
8
+ from typing import Any, Dict, List
9
+ from ..base import BaseAgent, AgentResult
10
+
11
+ # 每个 agent 的基础重要性权重(反映信息价值,可在 .ariarc 中覆盖)
12
+ _AGENT_WEIGHTS: Dict[str, float] = {
13
+ "technical": 1.2, # 短期动量信息量最高
14
+ "fundamental": 1.1, # 价值锚
15
+ "risk": 1.0,
16
+ "macro": 0.9, # 宏观慢变量,不应过度主导短期判断
17
+ "news": 1.0,
18
+ "catalyst": 1.1, # 催化剂改变时机,提高权重
19
+ "earnings": 1.1,
20
+ "sector": 0.8,
21
+ "debate": 0.5, # 调解结果已融入其他 agent,避免重复计入
22
+ }
23
+
24
+ _SIGNAL_SCORE: Dict[str, float] = {
25
+ "STRONG_BUY": 2.0,
26
+ "BUY": 1.0,
27
+ "HOLD": 0.0,
28
+ "SELL": -1.0,
29
+ "STRONG_SELL": -2.0,
30
+ }
31
+
32
+
33
+ class SynthesisAgent(BaseAgent):
34
+ name = "synthesis"
35
+ description = "综合汇总:整合多 Agent 结论,输出操作建议(置信度加权投票)"
36
+
37
+ _SYSTEM = (
38
+ "You are the chief investment strategist. Synthesize analyses from "
39
+ "macro, fundamental, technical, and risk agents into a clear, "
40
+ "actionable investment recommendation. Be concise and direct. "
41
+ "End with: FINAL: BUY / HOLD / SELL (with target price and stop loss IF real data is available)."
42
+ )
43
+
44
+ async def analyze(self, symbol: str, data: Dict[str, Any]) -> AgentResult:
45
+ agent_results: List[Dict] = data.get("agent_results", [])
46
+ consensus_signal = str(data.get("consensus_signal") or "HOLD").upper()
47
+ consensus_conf = data.get("consensus_confidence")
48
+ consensus_conf_num = (
49
+ _safe_float(consensus_conf)
50
+ if consensus_conf is not None else _calc_weighted_confidence(agent_results)
51
+ )
52
+
53
+ # Check if any agent had real price data
54
+ _quote = data.get("quote", {})
55
+ _snapshot = data.get("market_snapshot", {}) or {}
56
+ _price = _snapshot.get("price") or (_quote.get("price", 0) if _quote else 0)
57
+ _data_available = bool(_safe_float(_price))
58
+ _market_block = data.get("market_data_block") or _format_market_block(data)
59
+ _target = _snapshot.get("analyst_target")
60
+ _stop = _risk_stop(_snapshot, _snapshot.get("currency") or "USD")
61
+
62
+ summary_parts = []
63
+ for r in agent_results:
64
+ if not r.get("error"):
65
+ agent_name = r["agent"].upper()
66
+ signal = r.get("signal", "HOLD")
67
+ conf = r.get("confidence", 0.5)
68
+ points = r.get("key_points", [])
69
+ pts_str = "; ".join(points[:2]) if points else r.get("analysis", "")[:80]
70
+ summary_parts.append(
71
+ f"[{agent_name}] Signal={signal} ({conf:.0%}): {pts_str}"
72
+ )
73
+
74
+ summary = "\n".join(summary_parts) if summary_parts else "No agent data"
75
+
76
+ if _data_available:
77
+ _price_instruction = (
78
+ "3. Entry strategy using ONLY the supplied current price, MA20/MA60, RSI, MACD and analyst_target.\n"
79
+ "4. Target/stop rules: use analyst_target only if supplied; otherwise write Target: N/A (analyst target missing). "
80
+ "Use stated support first; use MA60 only when price is still above it. If price is already below MA60, call MA60 a recovery level, not a stop. Do not invent prices.\n"
81
+ f"FINAL: {consensus_signal} | Target: "
82
+ f"{'$' + str(_target) if _safe_float(_target) else 'N/A (analyst target missing)'} | "
83
+ f"Stop: {_stop}"
84
+ )
85
+ else:
86
+ _price_instruction = (
87
+ "3. Entry strategy qualitative only because current price is missing.\n"
88
+ "4. Do not give target or stop prices.\n"
89
+ f"FINAL: {consensus_signal} | Target: N/A (price missing) | Stop: N/A"
90
+ )
91
+
92
+ prompt = (
93
+ f"Symbol: {symbol}\n"
94
+ f"Consensus signal: {consensus_signal}\n"
95
+ f"Consensus confidence: {consensus_conf_num:.0%}\n"
96
+ f"Real price data available: {'YES — price=' + str(_price) if _data_available else 'NO — do not invent prices'}\n"
97
+ f"Verified market data:\n{_market_block}\n\n"
98
+ f"Agent Analyses:\n{summary}\n\n"
99
+ "Provide final recommendation:\n"
100
+ "1. Overall investment thesis (2-3 concise sentences, grounded in verified market data)\n"
101
+ "2. Key risks to monitor; separate structural risks from missing real-time risk data\n"
102
+ + _price_instruction
103
+ )
104
+
105
+ analysis = await self._call_llm(self._SYSTEM, prompt, max_tokens=600, quote=_quote)
106
+ if not analysis:
107
+ analysis = _template_synthesis(symbol, agent_results, data)
108
+ elif _data_available:
109
+ analysis = (
110
+ analysis
111
+ .replace("N/A (no real data)", "N/A (analyst target missing)")
112
+ .replace("no real data", "analyst target missing")
113
+ .replace("信心 ≤40%", f"置信度 {consensus_conf_num:.0%}")
114
+ )
115
+
116
+ signal = consensus_signal or _extract_final_signal(analysis)
117
+ confidence = consensus_conf_num
118
+ key_points = _extract_key_points(analysis)
119
+
120
+ return AgentResult(
121
+ agent=self.name, symbol=symbol,
122
+ analysis=analysis, confidence=confidence,
123
+ signal=signal, key_points=key_points,
124
+ )
125
+
126
+
127
+ def _extract_final_signal(text: str) -> str:
128
+ import re
129
+ m = re.search(r"FINAL[:\s]+([A-Z_]+)", text.upper())
130
+ if m:
131
+ raw = m.group(1)
132
+ if "STRONG_BUY" in raw: return "STRONG_BUY"
133
+ if "BUY" in raw: return "BUY"
134
+ if "STRONG_SELL" in raw: return "STRONG_SELL"
135
+ if "SELL" in raw: return "SELL"
136
+ return "HOLD"
137
+
138
+
139
+ def _calc_weighted_confidence(results: List[Dict]) -> float:
140
+ """
141
+ Confidence = weighted average where weight = agent_base_weight × confidence².
142
+ High-confidence agents dominate; low-confidence agents contribute minimally.
143
+ """
144
+ valid = [r for r in results if not r.get("error")]
145
+ if not valid:
146
+ return 0.5
147
+ total_w, weighted_sum = 0.0, 0.0
148
+ for r in valid:
149
+ conf = max(0.0, min(1.0, float(r.get("confidence", 0.5))))
150
+ base_w = _AGENT_WEIGHTS.get(r.get("agent", ""), 1.0)
151
+ w = base_w * (conf ** 2) # confidence² amplifies strong signals
152
+ weighted_sum += conf * w
153
+ total_w += w
154
+ return round(weighted_sum / total_w, 2) if total_w else 0.5
155
+
156
+
157
+ def _calc_weighted_signal_score(results: List[Dict]) -> float:
158
+ """Weighted signal score used by the template fallback."""
159
+ valid = [r for r in results if not r.get("error")]
160
+ if not valid:
161
+ return 0.0
162
+ total_w, score_sum = 0.0, 0.0
163
+ for r in valid:
164
+ conf = max(0.0, min(1.0, float(r.get("confidence", 0.5))))
165
+ base_w = _AGENT_WEIGHTS.get(r.get("agent", ""), 1.0)
166
+ w = base_w * (conf ** 2)
167
+ score = _SIGNAL_SCORE.get(r.get("signal", "HOLD"), 0.0)
168
+ score_sum += score * w
169
+ total_w += w
170
+ return score_sum / total_w if total_w else 0.0
171
+
172
+
173
+ def _extract_key_points(text: str) -> List[str]:
174
+ points = []
175
+ for line in text.split("\n"):
176
+ line = line.strip()
177
+ if line.startswith(("1.", "2.", "3.", "4.", "•", "-", "·")) and len(line) > 8:
178
+ points.append(line.lstrip("1234567890.-•· "))
179
+ return points[:4]
180
+
181
+
182
+ def _safe_float(value: Any) -> float | None:
183
+ try:
184
+ if value in (None, ""):
185
+ return None
186
+ return float(value)
187
+ except Exception:
188
+ return None
189
+
190
+
191
+ def _fmt_price(value: Any, currency: str = "USD") -> str:
192
+ number = _safe_float(value)
193
+ return f"{currency} {number:.2f}" if number is not None else "N/A"
194
+
195
+
196
+ def _first_level(value: Any) -> float | None:
197
+ if value in (None, "", [], {}):
198
+ return None
199
+ if isinstance(value, (list, tuple)):
200
+ for item in value:
201
+ level = _first_level(item)
202
+ if level is not None:
203
+ return level
204
+ return None
205
+ if isinstance(value, dict):
206
+ for key in ("price", "level", "value"):
207
+ level = _safe_float(value.get(key))
208
+ if level is not None:
209
+ return level
210
+ return None
211
+ text = str(value).replace("USD", "").replace("$", "").split(",")[0].strip()
212
+ return _safe_float(text)
213
+
214
+
215
+ def _risk_stop(snapshot: Dict[str, Any], currency: str = "USD") -> str:
216
+ support = _first_level(snapshot.get("support") or snapshot.get("supports"))
217
+ if support is not None:
218
+ return f"below support {_fmt_price(support, currency)}"
219
+
220
+ ma60 = _safe_float(snapshot.get("ma60"))
221
+ price = _safe_float(snapshot.get("price"))
222
+ if ma60 is None:
223
+ return "N/A (technical stop missing)"
224
+ if price is not None and price < ma60:
225
+ return f"N/A (already below MA60; recovery level {_fmt_price(ma60, currency)})"
226
+ return f"below MA60 {_fmt_price(ma60, currency)}"
227
+
228
+
229
+ def _format_market_block(data: Dict[str, Any]) -> str:
230
+ snapshot = data.get("market_snapshot", {}) or {}
231
+ quote = data.get("quote", {}) or {}
232
+ fundamentals = data.get("fundamentals", {}) or {}
233
+ technical = data.get("technical", {}) or {}
234
+ quality = data.get("data_quality", {}) or {}
235
+ return "\n".join([
236
+ f"data_status={quality.get('status') or snapshot.get('status') or 'unknown'}",
237
+ f"providers={', '.join(snapshot.get('provider_chain') or quality.get('providers') or []) or 'unknown'}",
238
+ f"missing={', '.join(snapshot.get('missing_fields') or quality.get('missing_fields') or []) or 'none'}",
239
+ f"price={snapshot.get('price') or quote.get('price')}",
240
+ f"market_cap={snapshot.get('market_cap') or quote.get('market_cap') or fundamentals.get('market_cap')}",
241
+ f"pe={snapshot.get('pe_ratio') or fundamentals.get('pe_ratio') or fundamentals.get('pe_ttm')}",
242
+ f"analyst_target={snapshot.get('analyst_target') or fundamentals.get('analyst_target')}",
243
+ f"rsi={snapshot.get('rsi') or technical.get('rsi')}",
244
+ f"macd_hist={snapshot.get('macd_hist') or technical.get('macd_hist')}",
245
+ f"ma20={snapshot.get('ma20') or technical.get('ma20')}",
246
+ f"ma60={snapshot.get('ma60') or technical.get('ma60')}",
247
+ ])
248
+
249
+
250
+ def _template_synthesis(symbol: str, results: List[Dict], data: Dict[str, Any] | None = None) -> str:
251
+ data = data or {}
252
+ avg_s = _calc_weighted_signal_score(results)
253
+ final = str(data.get("consensus_signal") or ("BUY" if avg_s > 0.4 else ("SELL" if avg_s < -0.4 else "HOLD"))).upper()
254
+ conf = float(data.get("consensus_confidence") or _calc_weighted_confidence(results))
255
+ snapshot = data.get("market_snapshot", {}) or {}
256
+ currency = snapshot.get("currency") or "USD"
257
+ price = _fmt_price(snapshot.get("price"), currency)
258
+ target = _fmt_price(snapshot.get("analyst_target"), currency) if _safe_float(snapshot.get("analyst_target")) else "N/A (analyst target missing)"
259
+ stop = _risk_stop(snapshot, currency)
260
+ providers = ", ".join(snapshot.get("provider_chain") or []) or "unknown"
261
+ missing = ", ".join(snapshot.get("missing_fields") or []) or "none"
262
+
263
+ agent_lines = "\n".join(
264
+ f" • {r['agent'].upper()} ({r.get('confidence', 0.5):.0%}): "
265
+ f"{r.get('signal','?')} — " + "; ".join(r.get("key_points", [])[:1])
266
+ for r in results if not r.get("error")
267
+ )
268
+ return (
269
+ f"{symbol} 综合分析(真实数据加权汇总)\n"
270
+ f"当前价: {price} 数据源: {providers} 缺失: {missing}\n"
271
+ f"{agent_lines}\n\n"
272
+ f"加权信号得分: {avg_s:+.2f} 综合置信度: {conf:.0%}\n"
273
+ f"FINAL: {final} | Target: {target} | Stop: {stop}"
274
+ )