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,258 @@
1
+ """
2
+ agents/financial/technical.py — 技术分析 Agent
3
+ ===============================================
4
+ 分析 K线形态、均线结构、MACD/RSI、布林带、关键价位。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from typing import Any, Dict, List
11
+
12
+ from ..base import BaseAgent, AgentResult
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class TechnicalAgent(BaseAgent):
18
+
19
+ name = "technical"
20
+ description = "技术分析:图形形态、动量指标、关键价位"
21
+
22
+ _SYSTEM = (
23
+ "You are a quantitative technical analyst. Analyze price action, "
24
+ "chart patterns, momentum indicators (RSI, MACD), moving averages, "
25
+ "and Bollinger Bands. Provide concise, data-driven insights. "
26
+ "Conclude with a clear directional bias: BULLISH / NEUTRAL / BEARISH."
27
+ )
28
+
29
+ async def fetch_data(self, symbol: str) -> Dict[str, Any]:
30
+ data = await super().fetch_data(symbol)
31
+ if self.data:
32
+ try:
33
+ h = self.data.history(symbol, days=120)
34
+ if h and h.data is not None:
35
+ df = h.data
36
+ data["history"] = _compute_indicators(df)
37
+ except Exception as e:
38
+ logger.debug(f"[technical] fetch history {symbol}: {e}")
39
+ return data
40
+
41
+ async def analyze(self, symbol: str, data: Dict[str, Any]) -> AgentResult:
42
+ quote = data.get("quote", {})
43
+ history = data.get("history", {})
44
+ price = quote.get("price", 0)
45
+
46
+ # 提取指标用于 prompt
47
+ indicators = _format_indicators(history)
48
+ pattern = history.get("pattern", "无特殊形态")
49
+
50
+ prompt = (
51
+ f"Stock: {symbol} Current Price: {price}\n"
52
+ f"Technical Indicators:\n{indicators}\n"
53
+ f"Pattern: {pattern}\n\n"
54
+ "Provide: 1) Trend assessment 2) Key support/resistance levels "
55
+ "3) Signal strength 4) Short-term outlook (3-10 days) "
56
+ "5) Conclusion: BULLISH / NEUTRAL / BEARISH"
57
+ )
58
+
59
+ analysis = await self._call_llm(self._SYSTEM, prompt, max_tokens=600, quote=quote)
60
+ if not analysis:
61
+ analysis = _template_analysis(symbol, price, history)
62
+
63
+ signal = _extract_signal(analysis, history)
64
+ confidence = history.get("signal_strength", 0.5)
65
+ key_points = _extract_key_points(history, price)
66
+
67
+ return AgentResult(
68
+ agent=self.name, symbol=symbol,
69
+ analysis=analysis, confidence=confidence,
70
+ signal=signal, key_points=key_points,
71
+ data_used={"price": price, "indicators": history},
72
+ )
73
+
74
+
75
+ # ── 技术指标计算 ──────────────────────────────────────────────────────────────
76
+
77
+ def _compute_indicators(df) -> Dict[str, Any]:
78
+ """从 OHLCV DataFrame 计算常用指标"""
79
+ try:
80
+ import numpy as np
81
+ close = df["close"].values if "close" in df.columns else df.iloc[:, -1].values
82
+
83
+ # MA
84
+ ma5 = float(np.mean(close[-5:])) if len(close) >= 5 else 0
85
+ ma20 = float(np.mean(close[-20:])) if len(close) >= 20 else 0
86
+ ma60 = float(np.mean(close[-60:])) if len(close) >= 60 else 0
87
+
88
+ # RSI(14)
89
+ delta = np.diff(close[-15:])
90
+ gains = np.where(delta > 0, delta, 0)
91
+ losses= np.where(delta < 0, -delta, 0)
92
+ rs = np.mean(gains[-14:]) / (np.mean(losses[-14:]) + 1e-9)
93
+ rsi = round(100 - 100 / (1 + rs), 1)
94
+
95
+ # MACD
96
+ ema12 = _ema(close, 12)
97
+ ema26 = _ema(close, 26)
98
+ macd = ema12 - ema26
99
+ signal= _ema_arr(macd[-50:] if len(macd) >= 50 else macd, 9)
100
+ macd_val = round(float(macd[-1]), 4)
101
+ signal_val= round(float(signal[-1]), 4)
102
+ hist_val = round(macd_val - signal_val, 4)
103
+
104
+ # 布林带
105
+ std20 = float(np.std(close[-20:])) if len(close) >= 20 else 0
106
+ bb_up = round(ma20 + 2 * std20, 2)
107
+ bb_lo = round(ma20 - 2 * std20, 2)
108
+
109
+ # 信号强度
110
+ price = float(close[-1])
111
+ ma_bull = price > ma5 > ma20
112
+ strength = 0.5
113
+ if ma_bull and macd_val > signal_val:
114
+ strength = 0.75
115
+ elif not ma_bull and macd_val < signal_val:
116
+ strength = 0.25
117
+
118
+ # 形态检测(简单)
119
+ pattern = _detect_simple_pattern(close)
120
+
121
+ return {
122
+ "price": round(price, 2),
123
+ "ma5": round(ma5, 2),
124
+ "ma20": round(ma20, 2),
125
+ "ma60": round(ma60, 2),
126
+ "rsi": rsi,
127
+ "macd": macd_val,
128
+ "macd_signal": signal_val,
129
+ "macd_hist": hist_val,
130
+ "bb_upper": bb_up,
131
+ "bb_lower": bb_lo,
132
+ "signal_strength": strength,
133
+ "pattern": pattern,
134
+ }
135
+ except Exception as e:
136
+ logger.debug(f"compute_indicators 失败: {e}")
137
+ return {}
138
+
139
+
140
+ def _ema(arr, period):
141
+ import numpy as np
142
+ k = 2 / (period + 1)
143
+ ema = np.zeros(len(arr))
144
+ ema[0] = arr[0]
145
+ for i in range(1, len(arr)):
146
+ ema[i] = arr[i] * k + ema[i-1] * (1 - k)
147
+ return ema
148
+
149
+ def _ema_arr(arr, period):
150
+ return _ema(arr, period)
151
+
152
+ def _detect_simple_pattern(close) -> str:
153
+ if len(close) < 3:
154
+ return "数据不足"
155
+ o, h, c = close[-3], close[-2], close[-1]
156
+ if c > o and c > h and (c - h) > abs(c - o) * 2:
157
+ return "锤子线"
158
+ if c > o and o < close[-4] and c > close[-4]:
159
+ return "阳线吞噬"
160
+ if abs(c - o) / (max(close[-3:]) - min(close[-3:]) + 1e-9) < 0.1:
161
+ return "十字星"
162
+ return "无特殊形态"
163
+
164
+
165
+ def _format_indicators(history: Dict) -> str:
166
+ if not history:
167
+ return " (无指标数据)"
168
+ return (
169
+ f" MA5={history.get('ma5',0):.2f} MA20={history.get('ma20',0):.2f} "
170
+ f"MA60={history.get('ma60',0):.2f}\n"
171
+ f" RSI={history.get('rsi',50):.1f} "
172
+ f"MACD={history.get('macd',0):.4f} Signal={history.get('macd_signal',0):.4f}\n"
173
+ f" BB Upper={history.get('bb_upper',0):.2f} BB Lower={history.get('bb_lower',0):.2f}"
174
+ )
175
+
176
+
177
+ def _extract_signal(analysis: str, history: Dict) -> str:
178
+ import re as _re
179
+ text = analysis.upper()
180
+
181
+ # Unambiguous strong signals always win
182
+ if "STRONG_BUY" in text or "强烈买入" in text:
183
+ return "STRONG_BUY"
184
+ if "STRONG_SELL" in text or "强烈卖出" in text:
185
+ return "STRONG_SELL"
186
+
187
+ # The LLM prompt asks the model to conclude with BULLISH/NEUTRAL/BEARISH.
188
+ # Use the LAST occurrence — it's the conclusion, not mid-text context like
189
+ # "a potential BULLISH reversal" appearing before "currently BEARISH".
190
+ matches = _re.findall(r'\b(BULLISH|BEARISH|NEUTRAL)\b', text)
191
+ if matches:
192
+ last = matches[-1]
193
+ if last == "BULLISH":
194
+ return "BUY"
195
+ if last == "BEARISH":
196
+ return "SELL"
197
+ # NEUTRAL → fall through to quantitative check below
198
+
199
+ # Explicit Chinese directional signals
200
+ if "看多" in analysis:
201
+ return "BUY"
202
+ if "看空" in analysis:
203
+ return "SELL"
204
+
205
+ # Quantitative fallback: score RSI + MACD crossover
206
+ rsi = history.get("rsi", 50)
207
+ macd = history.get("macd", 0)
208
+ msig = history.get("macd_signal", 0)
209
+ score = 0
210
+ if rsi < 30: score += 2 # oversold — strong mean-reversion signal
211
+ elif rsi < 45: score -= 1
212
+ elif rsi > 70: score -= 2 # overbought
213
+ elif rsi > 55: score += 1
214
+ if macd > msig: score += 1
215
+ elif macd < msig: score -= 1
216
+ if score >= 2: return "BUY"
217
+ if score <= -2: return "SELL"
218
+ return "HOLD"
219
+
220
+
221
+ def _extract_key_points(history: Dict, price: float) -> List[str]:
222
+ points = []
223
+ rsi = history.get("rsi", 50)
224
+ ma20 = history.get("ma20", 0)
225
+ ma60 = history.get("ma60", 0)
226
+ if rsi < 35:
227
+ points.append(f"RSI超卖({rsi:.0f}),有反弹机会")
228
+ elif rsi > 70:
229
+ points.append(f"RSI超买({rsi:.0f}),注意回调风险")
230
+ if ma20 > 0:
231
+ diff_pct = (price - ma20) / ma20 * 100
232
+ points.append(f"距MA20 {diff_pct:+.1f}%")
233
+ macd = history.get("macd", 0)
234
+ sig = history.get("macd_signal", 0)
235
+ if macd > sig and history.get("macd_hist", 0) > 0:
236
+ points.append("MACD金叉,多头动能")
237
+ elif macd < sig:
238
+ points.append("MACD死叉,空头压力")
239
+ pattern = history.get("pattern", "")
240
+ if pattern and pattern != "无特殊形态":
241
+ points.append(f"K线形态: {pattern}")
242
+ return points
243
+
244
+
245
+ def _template_analysis(symbol: str, price: float, history: Dict) -> str:
246
+ rsi = history.get("rsi", 50)
247
+ ma20 = history.get("ma20", 0)
248
+ macd = history.get("macd", 0)
249
+ sig = history.get("macd_signal", 0)
250
+ trend = "上升趋势" if price > ma20 else "下降趋势"
251
+ momentum = "偏多" if macd > sig else "偏空"
252
+ return (
253
+ f"{symbol} 技术面分析(模板):\n"
254
+ f"• 当前价格 {price},处于 {trend}(MA20={ma20:.2f})\n"
255
+ f"• RSI={rsi:.0f},{'超卖' if rsi<35 else '超买' if rsi>70 else '正常区间'}\n"
256
+ f"• MACD 动能{momentum}\n"
257
+ f"• 整体技术面{'BULLISH' if momentum=='偏多' and trend=='上升趋势' else 'NEUTRAL'}"
258
+ )
@@ -0,0 +1,333 @@
1
+ """
2
+ agents/portfolio_agent.py — 组合级分析 Agent
3
+ =============================================
4
+ 跨标的分析:相关性矩阵、波动率、集中度风险、再平衡建议。
5
+ 数据源:yfinance 1年日线(免费,无需 key)
6
+ 触发:/portfolio analyze [symbols…]
7
+ /portfolio rebalance [symbols…]
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from datetime import datetime
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ from .base import BaseAgent, AgentResult
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class PortfolioAgent(BaseAgent):
22
+
23
+ name = "portfolio"
24
+ description = "组合级分析 — 相关性、分散度、整体风险、再平衡建议"
25
+
26
+ _SYSTEM = (
27
+ "You are a portfolio risk analyst. Given price history and statistics for "
28
+ "a portfolio of stocks, your job is to:\n"
29
+ "1. Evaluate overall portfolio risk (Low / Medium / High)\n"
30
+ "2. Identify diversification gaps — highly correlated pairs, sector crowding\n"
31
+ "3. Flag concentration risk if any single position dominates\n"
32
+ "4. Give 2-3 actionable rebalancing suggestions (be specific: which stocks to "
33
+ "trim / add / replace and why)\n"
34
+ "5. End with a one-line verdict: HEALTHY / NEEDS_ATTENTION / HIGH_RISK\n"
35
+ "Be concrete. Avoid boilerplate."
36
+ )
37
+
38
+ # ── BaseAgent compatibility ───────────────────────────────────────────────
39
+
40
+ async def fetch_data(self, symbol: str) -> Dict[str, Any]:
41
+ symbols = [s.strip().upper() for s in symbol.split(",") if s.strip()]
42
+ return await self.fetch_portfolio_data(symbols)
43
+
44
+ async def analyze(self, symbol: str, data: Dict[str, Any]) -> AgentResult:
45
+ symbols = data.get("symbols") or [s.strip().upper() for s in symbol.split(",") if s.strip()]
46
+ return await self.analyze_portfolio(symbols, data)
47
+
48
+ # ── Multi-symbol interface ────────────────────────────────────────────────
49
+
50
+ async def run_portfolio(self, symbols: List[str]) -> AgentResult:
51
+ """Primary entry point for multi-symbol analysis."""
52
+ data = await self.fetch_portfolio_data(symbols)
53
+ return await self.analyze_portfolio(symbols, data)
54
+
55
+ async def fetch_portfolio_data(self, symbols: List[str]) -> Dict[str, Any]:
56
+ data: Dict[str, Any] = {"symbols": symbols}
57
+ if len(symbols) < 2:
58
+ return data
59
+
60
+ price_series: Dict[str, Any] = {}
61
+ latest_prices: Dict[str, float] = {}
62
+ sector_map: Dict[str, str] = {}
63
+
64
+ for sym in symbols:
65
+ try:
66
+ import yfinance as yf
67
+ ticker = yf.Ticker(sym)
68
+ hist = ticker.history(period="1y")
69
+ if not hist.empty and len(hist) > 20:
70
+ price_series[sym] = hist["Close"]
71
+ latest_prices[sym] = float(hist["Close"].iloc[-1])
72
+ # Try to get sector info
73
+ try:
74
+ info = ticker.info or {}
75
+ sector = info.get("sector") or info.get("industry", "")
76
+ if sector:
77
+ sector_map[sym] = sector
78
+ except Exception:
79
+ pass
80
+ except Exception as e:
81
+ logger.debug("[portfolio] yfinance %s: %s", sym, e)
82
+
83
+ if len(price_series) < 2:
84
+ data["error"] = "insufficient_data"
85
+ return data
86
+
87
+ try:
88
+ import pandas as pd
89
+ import numpy as np
90
+
91
+ df = pd.DataFrame(price_series).dropna()
92
+ returns = df.pct_change().dropna()
93
+
94
+ corr = returns.corr()
95
+ ann_returns = (returns.mean() * 252).round(4)
96
+ ann_vols = (returns.std() * np.sqrt(252)).round(4)
97
+ cov_matrix = returns.cov() * 252
98
+ valid_syms = list(df.columns)
99
+ n = len(valid_syms)
100
+ weights = np.ones(n) / n
101
+ port_vol = float(np.sqrt(weights @ cov_matrix.values @ weights))
102
+
103
+ # High correlation pairs (|r| > 0.70)
104
+ high_corr: List[Dict] = []
105
+ for i in range(len(valid_syms)):
106
+ for j in range(i + 1, len(valid_syms)):
107
+ c = float(corr.iloc[i, j])
108
+ if abs(c) > 0.70:
109
+ high_corr.append({
110
+ "sym1": valid_syms[i],
111
+ "sym2": valid_syms[j],
112
+ "corr": round(c, 3),
113
+ })
114
+
115
+ # Diversification ratio: weighted avg individual vol / portfolio vol
116
+ avg_vol = float(np.mean([float(ann_vols.get(s, 0)) for s in valid_syms]))
117
+ div_ratio = round(avg_vol / port_vol, 2) if port_vol > 0 else 1.0
118
+
119
+ # 52-week return per symbol
120
+ returns_1y: Dict[str, float] = {}
121
+ for sym in valid_syms:
122
+ first = float(df[sym].iloc[0])
123
+ last = float(df[sym].iloc[-1])
124
+ if first > 0:
125
+ returns_1y[sym] = round((last - first) / first, 4)
126
+
127
+ # Best/worst performer
128
+ sorted_ret = sorted(returns_1y.items(), key=lambda x: x[1], reverse=True)
129
+
130
+ data.update({
131
+ "valid_symbols": valid_syms,
132
+ "latest_prices": latest_prices,
133
+ "ann_returns": {s: float(v) for s, v in ann_returns.items()},
134
+ "ann_vols": {s: float(v) for s, v in ann_vols.items()},
135
+ "port_vol_ann": round(port_vol, 4),
136
+ "div_ratio": div_ratio,
137
+ "corr_matrix": corr.round(3).to_dict(),
138
+ "high_corr": high_corr,
139
+ "returns_1y": returns_1y,
140
+ "best_performer": sorted_ret[0] if sorted_ret else None,
141
+ "worst_performer": sorted_ret[-1] if sorted_ret else None,
142
+ "sector_map": sector_map,
143
+ })
144
+
145
+ except ImportError:
146
+ data["error"] = "pandas/numpy not available"
147
+ except Exception as e:
148
+ logger.warning("[portfolio] stats calculation: %s", e)
149
+ data["error"] = str(e)
150
+
151
+ return data
152
+
153
+ async def analyze_portfolio(
154
+ self, symbols: List[str], data: Dict[str, Any]
155
+ ) -> AgentResult:
156
+ if len(symbols) < 2:
157
+ return AgentResult(
158
+ agent=self.name, symbol=",".join(symbols),
159
+ analysis="组合分析需要至少 2 个标的。",
160
+ confidence=0.0, signal="HOLD",
161
+ key_points=["标的数量不足"],
162
+ )
163
+
164
+ if data.get("error") == "insufficient_data":
165
+ return AgentResult(
166
+ agent=self.name, symbol=",".join(symbols),
167
+ analysis="无法获取足够的历史价格数据进行组合分析。",
168
+ confidence=0.3, signal="HOLD",
169
+ key_points=["历史数据不足(需要 >20 个交易日)"],
170
+ )
171
+
172
+ valid_syms = data.get("valid_symbols", symbols)
173
+ port_block = _format_portfolio_stats(data)
174
+
175
+ prompt = (
176
+ f"Portfolio: {', '.join(valid_syms)} ({len(valid_syms)} positions, equal weight)\n\n"
177
+ f"{port_block}\n\n"
178
+ "Analyze this portfolio:\n"
179
+ "1. Overall risk level (Low / Medium / High)\n"
180
+ "2. Diversification quality — any dangerous correlations or sector crowding?\n"
181
+ "3. Top 2-3 concerns (be specific about which symbols)\n"
182
+ "4. Concrete rebalancing suggestions\n"
183
+ "5. End with: HEALTHY / NEEDS_ATTENTION / HIGH_RISK"
184
+ )
185
+
186
+ analysis = await self._call_llm(self._SYSTEM, prompt, max_tokens=650)
187
+ if not analysis:
188
+ analysis = _template_analysis(valid_syms, data)
189
+
190
+ verdict = _extract_verdict(analysis)
191
+ signal = _verdict_to_signal(verdict)
192
+ confidence = _estimate_confidence(data)
193
+ key_points = _build_key_points(data, verdict)
194
+
195
+ return AgentResult(
196
+ agent=self.name,
197
+ symbol=",".join(valid_syms),
198
+ analysis=analysis,
199
+ confidence=confidence,
200
+ signal=signal,
201
+ key_points=key_points,
202
+ data_used={
203
+ "n": len(valid_syms),
204
+ "port_vol_ann": data.get("port_vol_ann"),
205
+ "div_ratio": data.get("div_ratio"),
206
+ "high_corr_count": len(data.get("high_corr", [])),
207
+ },
208
+ )
209
+
210
+
211
+ # ── Helpers ───────────────────────────────────────────────────────────────────
212
+
213
+ def _format_portfolio_stats(d: Dict) -> str:
214
+ lines = []
215
+
216
+ # Returns & volatility table
217
+ vols = d.get("ann_vols", {})
218
+ rets = d.get("ann_returns", {})
219
+ rets_1y = d.get("returns_1y", {})
220
+ if vols:
221
+ lines.append("Symbol │ Ann.Vol │ Ann.Ret │ 1Y Return")
222
+ lines.append("─" * 42)
223
+ for sym in d.get("valid_symbols", list(vols)):
224
+ v = vols.get(sym, 0)
225
+ r = rets.get(sym, 0)
226
+ y = rets_1y.get(sym, 0)
227
+ lines.append(
228
+ f"{sym:<6} │ {v*100:>6.1f}% │ {r*100:>6.1f}% │ {y*100:>+7.1f}%"
229
+ )
230
+
231
+ port_vol = d.get("port_vol_ann", 0)
232
+ div_r = d.get("div_ratio", 1)
233
+ if port_vol:
234
+ lines.append(f"\nPortfolio Ann.Vol: {port_vol*100:.1f}%")
235
+ lines.append(f"Diversification Ratio: {div_r:.2f}x "
236
+ f"({'good' if div_r >= 1.3 else 'poor'})")
237
+
238
+ high_corr = d.get("high_corr", [])
239
+ if high_corr:
240
+ lines.append(f"\nHigh-Correlation Pairs (|r|>0.70):")
241
+ for p in high_corr[:5]:
242
+ lines.append(f" {p['sym1']} ↔ {p['sym2']}: {p['corr']:+.2f}")
243
+
244
+ sectors = d.get("sector_map", {})
245
+ if sectors:
246
+ from collections import Counter
247
+ sector_counts = Counter(sectors.values())
248
+ dominant = [(s, c) for s, c in sector_counts.items() if c >= 2]
249
+ if dominant:
250
+ lines.append("\nSector Concentration:")
251
+ for sec, cnt in dominant:
252
+ lines.append(f" {sec}: {cnt} positions")
253
+
254
+ best = d.get("best_performer")
255
+ worst = d.get("worst_performer")
256
+ if best and worst:
257
+ lines.append(f"\nBest 1Y: {best[0]} ({best[1]*100:+.1f}%)")
258
+ lines.append(f"Worst 1Y: {worst[0]} ({worst[1]*100:+.1f}%)")
259
+
260
+ return "\n".join(lines)
261
+
262
+
263
+ def _extract_verdict(analysis: str) -> str:
264
+ text = analysis.upper()
265
+ if "HIGH_RISK" in text or "HIGH RISK" in text:
266
+ return "HIGH_RISK"
267
+ if "NEEDS_ATTENTION" in text or "NEEDS ATTENTION" in text:
268
+ return "NEEDS_ATTENTION"
269
+ if "HEALTHY" in text:
270
+ return "HEALTHY"
271
+ return "NEEDS_ATTENTION"
272
+
273
+
274
+ def _verdict_to_signal(verdict: str) -> str:
275
+ return {"HEALTHY": "HOLD", "NEEDS_ATTENTION": "SELL", "HIGH_RISK": "SELL"}.get(verdict, "HOLD")
276
+
277
+
278
+ def _estimate_confidence(d: Dict) -> float:
279
+ base = 0.60
280
+ n = len(d.get("valid_symbols", []))
281
+ if n >= 5:
282
+ base += 0.05
283
+ if d.get("div_ratio", 1) < 1.1:
284
+ base += 0.05
285
+ return min(round(base, 2), 0.75)
286
+
287
+
288
+ def _build_key_points(d: Dict, verdict: str) -> List[str]:
289
+ pts = []
290
+ n = len(d.get("valid_symbols", []))
291
+ pts.append(f"{n} 个标的,等权配置")
292
+
293
+ port_vol = d.get("port_vol_ann", 0)
294
+ if port_vol:
295
+ risk_lv = "高风险" if port_vol > 0.30 else ("中等" if port_vol > 0.18 else "低风险")
296
+ pts.append(f"组合年化波动 {port_vol*100:.1f}%({risk_lv})")
297
+
298
+ div_r = d.get("div_ratio", 1)
299
+ if div_r < 1.1:
300
+ pts.append(f"分散度不足(ratio {div_r:.2f}x,标的相关性高)")
301
+ else:
302
+ pts.append(f"分散度合理(ratio {div_r:.2f}x)")
303
+
304
+ high_corr = d.get("high_corr", [])
305
+ if high_corr:
306
+ top = high_corr[0]
307
+ pts.append(f"最高相关对: {top['sym1']}↔{top['sym2']} ({top['corr']:+.2f})")
308
+
309
+ pts.append(f"整体评级: {verdict}")
310
+ return pts[:6]
311
+
312
+
313
+ def _template_analysis(symbols: List[str], d: Dict) -> str:
314
+ high_corr = d.get("high_corr", [])
315
+ port_vol = d.get("port_vol_ann", 0)
316
+ div_r = d.get("div_ratio", 1)
317
+
318
+ verdict = "NEEDS_ATTENTION"
319
+ if not high_corr and port_vol < 0.20 and div_r >= 1.2:
320
+ verdict = "HEALTHY"
321
+ elif len(high_corr) >= 3 or port_vol > 0.30:
322
+ verdict = "HIGH_RISK"
323
+
324
+ lines = [f"{','.join(symbols)} 组合分析报告(模板)"]
325
+ lines.append(f"年化波动率: {port_vol*100:.1f}%")
326
+ lines.append(f"分散度系数: {div_r:.2f}x")
327
+ if high_corr:
328
+ lines.append(f"高相关对 {len(high_corr)} 组,最高: "
329
+ f"{high_corr[0]['sym1']}↔{high_corr[0]['sym2']} {high_corr[0]['corr']:+.2f}")
330
+ else:
331
+ lines.append("无显著高相关对,分散良好。")
332
+ lines.append(f"\n{verdict}")
333
+ return "\n".join(lines)
@@ -0,0 +1,62 @@
1
+ """
2
+ agents/realty — 经营权共创平台 AI Agent 模块
3
+ =============================================
4
+ 9 个领域专属 Agent,继承 BaseAgent,用于:
5
+ - 资产诊断与处置建议
6
+ - 业态匹配与经营方推荐
7
+ - 合同条款结构化生成
8
+ - 分账规则配置建议
9
+ - 流水核验与异常检测
10
+ - 能耗异常分析
11
+ - 合同履约风控
12
+ - 运营优化建议
13
+ - 退出清算方案生成
14
+
15
+ 用法:
16
+ from agents.realty import AssetDiagnosisAgent, RevenueShareAgent
17
+ from agents.realty import REALTY_TEAM # 默认 team 名称列表
18
+ """
19
+
20
+ from .asset_diagnosis import AssetDiagnosisAgent
21
+ from .business_match import BusinessMatchAgent
22
+ from .contract_rules import ContractRulesAgent
23
+ from .revenue_share import RevenueShareAgent
24
+ from .cashflow_verify import CashFlowVerifyAgent
25
+ from .energy_anomaly import EnergyAnomalyAgent
26
+ from .fulfillment_risk import FulfillmentRiskAgent
27
+ from .ops_optimize import OpsOptimizeAgent
28
+ from .exit_settlement import ExitSettlementAgent
29
+
30
+ # 默认完整 team
31
+ REALTY_TEAM = [
32
+ "asset_diagnosis",
33
+ "business_match",
34
+ "contract_rules",
35
+ "revenue_share",
36
+ "cashflow_verify",
37
+ "energy_anomaly",
38
+ "fulfillment_risk",
39
+ "ops_optimize",
40
+ "exit_settlement",
41
+ ]
42
+
43
+ # 风控专项 team(快速预警)
44
+ RISK_TEAM = [
45
+ "cashflow_verify",
46
+ "energy_anomaly",
47
+ "fulfillment_risk",
48
+ ]
49
+
50
+ __all__ = [
51
+ "AssetDiagnosisAgent",
52
+ "BusinessMatchAgent",
53
+ "ContractRulesAgent",
54
+ "RevenueShareAgent",
55
+ "CashFlowVerifyAgent",
56
+ "EnergyAnomalyAgent",
57
+ "FulfillmentRiskAgent",
58
+ "OpsOptimizeAgent",
59
+ "ExitSettlementAgent",
60
+ "REALTY_TEAM",
61
+ "RISK_TEAM",
62
+ ]