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
data_analysis_tools.py ADDED
@@ -0,0 +1,808 @@
1
+ """
2
+ data_analysis_tools.py — 数据分析增强层
3
+ ==========================================
4
+ 提供:
5
+ - Excel 导出 (多 Sheet + 内嵌图表)
6
+ - DuckDB SQL 接口(本地内存数据仓库)
7
+ - 价格预警系统 (~/.arthera/alerts.json)
8
+ - 多资产组合回测 (相关性矩阵 + 权重收益)
9
+ - 相关性热力图数据
10
+ - 自定义因子表达式 DSL
11
+
12
+ 依赖(可选):
13
+ pip install openpyxl duckdb pandas numpy yfinance
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import logging
20
+ import os
21
+ from datetime import datetime, timedelta
22
+ from pathlib import Path
23
+ from typing import Any, Dict, List, Optional, Tuple
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # ── Optional imports ──────────────────────────────────────────────────────────
28
+
29
+ try:
30
+ import pandas as pd
31
+ _HAS_PD = True
32
+ except ImportError:
33
+ _HAS_PD = False
34
+
35
+ try:
36
+ import numpy as np
37
+ _HAS_NP = True
38
+ except ImportError:
39
+ _HAS_NP = False
40
+
41
+ try:
42
+ import yfinance as yf
43
+ _HAS_YF = True
44
+ except ImportError:
45
+ _HAS_YF = False
46
+
47
+ try:
48
+ import duckdb
49
+ _HAS_DUCK = True
50
+ except ImportError:
51
+ _HAS_DUCK = False
52
+
53
+ try:
54
+ import openpyxl
55
+ from openpyxl.styles import PatternFill, Font, Alignment, Border, Side
56
+ from openpyxl.chart import LineChart, Reference
57
+ from openpyxl.utils import get_column_letter
58
+ _HAS_OPENPYXL = True
59
+ except ImportError:
60
+ _HAS_OPENPYXL = False
61
+
62
+ ALERTS_PATH = Path.home() / ".arthera" / "alerts.json"
63
+ EXPORT_DIR = Path.home() / ".arthera" / "exports"
64
+
65
+
66
+ # ── Excel 导出 ────────────────────────────────────────────────────────────────
67
+
68
+ def export_to_excel(params: dict) -> dict:
69
+ """
70
+ 将数据导出到格式化 Excel 文件(多 Sheet)。
71
+
72
+ 参数:
73
+ data: dict,键=Sheet名,值=list[dict] 行数据
74
+ filename: 输出文件名(默认 aria_export_<timestamp>.xlsx)
75
+ add_chart: 是否为含 '价格'/'close'/'收盘' 列的 Sheet 添加折线图
76
+ """
77
+ if not _HAS_OPENPYXL:
78
+ return {"success": False, "error": "openpyxl 未安装,请运行: pip install openpyxl"}
79
+ if not _HAS_PD:
80
+ return {"success": False, "error": "pandas 未安装"}
81
+
82
+ data = params.get("data", {})
83
+ if not data:
84
+ return {"success": False, "error": "data 参数不能为空"}
85
+
86
+ EXPORT_DIR.mkdir(parents=True, exist_ok=True)
87
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
88
+ filename = params.get("filename") or f"aria_export_{ts}.xlsx"
89
+ if not filename.endswith(".xlsx"):
90
+ filename += ".xlsx"
91
+ out_path = EXPORT_DIR / filename
92
+ add_chart = bool(params.get("add_chart", True))
93
+
94
+ wb = openpyxl.Workbook()
95
+ wb.remove(wb.active) # remove default empty sheet
96
+
97
+ # Style constants
98
+ header_fill = PatternFill("solid", fgColor="1F4E79")
99
+ header_font = Font(color="FFFFFF", bold=True, size=10)
100
+ alt_fill = PatternFill("solid", fgColor="EBF5FB")
101
+ center_align = Alignment(horizontal="center", vertical="center")
102
+ thin_border = Border(
103
+ bottom=Side(style="thin", color="BDC3C7"),
104
+ )
105
+
106
+ for sheet_name, rows in data.items():
107
+ if not rows:
108
+ continue
109
+ ws = wb.create_sheet(title=str(sheet_name)[:31]) # Excel max 31 chars
110
+ df = pd.DataFrame(rows)
111
+ cols = list(df.columns)
112
+
113
+ # Header row
114
+ for ci, col in enumerate(cols, 1):
115
+ cell = ws.cell(row=1, column=ci, value=str(col))
116
+ cell.fill = header_fill
117
+ cell.font = header_font
118
+ cell.alignment = center_align
119
+
120
+ # Data rows
121
+ price_col_idx = None
122
+ for ri, row_dict in enumerate(rows, 2):
123
+ for ci, col in enumerate(cols, 1):
124
+ val = row_dict.get(col)
125
+ cell = ws.cell(row=ri, column=ci, value=val)
126
+ if ri % 2 == 0:
127
+ cell.fill = alt_fill
128
+ cell.border = thin_border
129
+ # Auto-detect price column
130
+ col_lower = col.lower()
131
+ if price_col_idx is None and any(k in col_lower for k in
132
+ ["价格","close","收盘","price","last","最新"]):
133
+ price_col_idx = ci
134
+
135
+ # Auto-column width
136
+ for ci, col in enumerate(cols, 1):
137
+ max_len = max(len(str(col)),
138
+ *(len(str(r.get(col,""))) for r in rows[:50]))
139
+ ws.column_dimensions[get_column_letter(ci)].width = min(max_len + 3, 40)
140
+
141
+ # Freeze header
142
+ ws.freeze_panes = "A2"
143
+
144
+ # Add line chart if price column detected
145
+ if add_chart and price_col_idx and len(rows) >= 3:
146
+ chart = LineChart()
147
+ chart.title = f"{sheet_name} 价格走势"
148
+ chart.style = 10
149
+ chart.y_axis.title = "价格"
150
+ chart.x_axis.title = "时间"
151
+ data_ref = Reference(ws, min_col=price_col_idx,
152
+ min_row=1, max_row=len(rows) + 1)
153
+ chart.add_data(data_ref, titles_from_data=True)
154
+ chart.width = 20
155
+ chart.height = 12
156
+ ws.add_chart(chart, f"A{len(rows) + 4}")
157
+
158
+ # Summary sheet
159
+ ws_sum = wb.create_sheet(title="📊 汇总", index=0)
160
+ ws_sum["A1"] = "Aria Code 数据导出报告"
161
+ ws_sum["A1"].font = Font(bold=True, size=14, color="1F4E79")
162
+ ws_sum["A3"] = "导出时间:"
163
+ ws_sum["B3"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
164
+ ws_sum["A4"] = "Sheet 数量:"
165
+ ws_sum["B4"] = len(data)
166
+ ws_sum["A5"] = "总行数:"
167
+ ws_sum["B5"] = sum(len(v) for v in data.values())
168
+ ws_sum["A7"] = "包含 Sheet:"
169
+ for i, sname in enumerate(data.keys(), 8):
170
+ ws_sum[f"A{i}"] = f" • {sname}"
171
+ ws_sum[f"B{i}"] = f"{len(data[sname])} 行"
172
+
173
+ wb.save(str(out_path))
174
+ return {
175
+ "success": True,
176
+ "path": str(out_path),
177
+ "sheets": list(data.keys()),
178
+ "total_rows": sum(len(v) for v in data.values()),
179
+ "filename": filename,
180
+ }
181
+
182
+
183
+ # ── DuckDB SQL 接口 ───────────────────────────────────────────────────────────
184
+
185
+ # Persistent in-memory DuckDB connection for the session
186
+ _duck_conn: Optional[Any] = None
187
+ _duck_tables: Dict[str, bool] = {}
188
+
189
+
190
+ def _get_duck_conn():
191
+ global _duck_conn
192
+ if _duck_conn is None:
193
+ _duck_conn = duckdb.connect(":memory:")
194
+ return _duck_conn
195
+
196
+
197
+ def sql_query(params: dict) -> dict:
198
+ """
199
+ 在内存 DuckDB 中执行 SQL 查询。
200
+
201
+ 参数:
202
+ query: SQL 语句
203
+ load: 可选,dict {table_name: list[dict]} — 在查询前加载数据
204
+ limit: 结果行数限制(默认 500)
205
+ """
206
+ if not _HAS_DUCK:
207
+ return {"success": False,
208
+ "error": "duckdb 未安装,请运行: pip install duckdb"}
209
+ if not _HAS_PD:
210
+ return {"success": False, "error": "pandas 未安装"}
211
+
212
+ query = str(params.get("query", "")).strip()
213
+ if not query:
214
+ return {"success": False, "error": "query 不能为空"}
215
+
216
+ conn = _get_duck_conn()
217
+ limit = int(params.get("limit", 500))
218
+ load = params.get("load", {})
219
+
220
+ # Load tables
221
+ for tname, rows in (load or {}).items():
222
+ if rows:
223
+ df = pd.DataFrame(rows)
224
+ conn.register(tname, df)
225
+ _duck_tables[tname] = True
226
+
227
+ # Safety check: block destructive operations
228
+ q_upper = query.upper().lstrip()
229
+ if any(q_upper.startswith(kw) for kw in ("DROP ", "DELETE ", "TRUNCATE ")):
230
+ return {"success": False,
231
+ "error": "安全限制:不允许 DROP/DELETE/TRUNCATE 操作"}
232
+
233
+ # Auto-add LIMIT if SELECT without one
234
+ if q_upper.startswith("SELECT") and "LIMIT" not in q_upper:
235
+ query = f"{query.rstrip(';')} LIMIT {limit}"
236
+
237
+ try:
238
+ result = conn.execute(query).fetchdf()
239
+ records = result.to_dict("records")
240
+ cols = list(result.columns)
241
+ return {
242
+ "success": True,
243
+ "columns": cols,
244
+ "rows": records,
245
+ "row_count": len(records),
246
+ "tables_loaded": list(_duck_tables.keys()),
247
+ }
248
+ except Exception as e:
249
+ return {"success": False, "error": str(e), "query": query}
250
+
251
+
252
+ def sql_list_tables(params: dict = None) -> dict:
253
+ """列出内存 DuckDB 中已加载的表。"""
254
+ if not _HAS_DUCK:
255
+ return {"success": False, "error": "duckdb 未安装"}
256
+ conn = _get_duck_conn()
257
+ try:
258
+ df = conn.execute("SHOW TABLES").fetchdf()
259
+ tables = df["name"].tolist() if "name" in df.columns else []
260
+ return {"success": True, "tables": tables}
261
+ except Exception as e:
262
+ return {"success": False, "error": str(e)}
263
+
264
+
265
+ # ── 价格预警系统 ──────────────────────────────────────────────────────────────
266
+
267
+ def _load_alerts() -> List[dict]:
268
+ if ALERTS_PATH.exists():
269
+ try:
270
+ return json.loads(ALERTS_PATH.read_text(encoding="utf-8"))
271
+ except Exception:
272
+ pass
273
+ return []
274
+
275
+
276
+ def _save_alerts(alerts: list) -> None:
277
+ ALERTS_PATH.parent.mkdir(parents=True, exist_ok=True)
278
+ ALERTS_PATH.write_text(json.dumps(alerts, ensure_ascii=False, indent=2),
279
+ encoding="utf-8")
280
+
281
+
282
+ def add_price_alert(params: dict) -> dict:
283
+ """
284
+ 添加价格预警。
285
+
286
+ 参数:
287
+ symbol: 标的代码(如 "AAPL" / "600519")
288
+ condition: 触发条件 ("gt" / "lt" / "cross_up" / "cross_down")
289
+ price: 触发价格
290
+ note: 备注(可选)
291
+ """
292
+ symbol = str(params.get("symbol", "")).upper().strip()
293
+ condition = str(params.get("condition", "gt")).lower()
294
+ price = float(params.get("price", 0))
295
+ note = str(params.get("note", ""))
296
+
297
+ if not symbol:
298
+ return {"success": False, "error": "symbol 不能为空"}
299
+ if condition not in ("gt", "lt", "cross_up", "cross_down"):
300
+ return {"success": False,
301
+ "error": "condition 必须是 gt/lt/cross_up/cross_down"}
302
+ if price <= 0:
303
+ return {"success": False, "error": "price 必须 > 0"}
304
+
305
+ alerts = _load_alerts()
306
+ alert_id = f"{symbol}_{condition}_{price}_{int(datetime.now().timestamp())}"
307
+ new_alert = {
308
+ "id": alert_id,
309
+ "symbol": symbol,
310
+ "condition": condition,
311
+ "price": price,
312
+ "note": note,
313
+ "created_at": datetime.now().isoformat(),
314
+ "triggered": False,
315
+ }
316
+ alerts.append(new_alert)
317
+ _save_alerts(alerts)
318
+
319
+ cond_label = {"gt": ">", "lt": "<",
320
+ "cross_up": "向上突破", "cross_down": "向下跌破"}[condition]
321
+ return {
322
+ "success": True,
323
+ "alert_id": alert_id,
324
+ "message": f"已设置预警:{symbol} 价格 {cond_label} {price}",
325
+ "total_alerts": len(alerts),
326
+ }
327
+
328
+
329
+ def list_price_alerts(params: dict = None) -> dict:
330
+ """列出所有价格预警。"""
331
+ alerts = _load_alerts()
332
+ active = [a for a in alerts if not a.get("triggered")]
333
+ done = [a for a in alerts if a.get("triggered")]
334
+ return {
335
+ "success": True,
336
+ "active_alerts": active,
337
+ "triggered_alerts": done,
338
+ "total": len(alerts),
339
+ }
340
+
341
+
342
+ def delete_price_alert(params: dict) -> dict:
343
+ """删除指定 ID 的价格预警。"""
344
+ alert_id = str(params.get("alert_id", ""))
345
+ if not alert_id:
346
+ return {"success": False, "error": "alert_id 不能为空"}
347
+ alerts = _load_alerts()
348
+ before = len(alerts)
349
+ alerts = [a for a in alerts if a.get("id") != alert_id]
350
+ if len(alerts) == before:
351
+ return {"success": False, "error": f"未找到预警 {alert_id}"}
352
+ _save_alerts(alerts)
353
+ return {"success": True, "deleted_id": alert_id, "remaining": len(alerts)}
354
+
355
+
356
+ def check_alerts(params: dict = None) -> dict:
357
+ """
358
+ 检查当前所有未触发预警的状态(需联网获取实时价格)。
359
+ 返回已触发的预警列表。
360
+ """
361
+ if not _HAS_YF:
362
+ return {"success": False, "error": "yfinance 未安装,无法获取实时价格"}
363
+
364
+ alerts = _load_alerts()
365
+ active = [a for a in alerts if not a.get("triggered")]
366
+ if not active:
367
+ return {"success": True, "triggered": [], "message": "无活跃预警"}
368
+
369
+ # Batch price fetch
370
+ symbols = list(set(a.get("symbol", "") for a in active if a.get("symbol")))
371
+ prices: Dict[str, float] = {}
372
+ try:
373
+ tickers = yf.Tickers(" ".join(symbols))
374
+ for sym in symbols:
375
+ try:
376
+ info = tickers.tickers[sym].fast_info
377
+ px = getattr(info, "last_price", None) or \
378
+ getattr(info, "regularMarketPrice", None)
379
+ if px:
380
+ prices[sym] = float(px)
381
+ except Exception:
382
+ pass
383
+ except Exception as e:
384
+ logger.debug("Alert price fetch failed: %s", e)
385
+
386
+ triggered_now = []
387
+ for alert in active:
388
+ sym = alert.get("symbol")
389
+ cond = alert.get("condition")
390
+ tgt = alert.get("price")
391
+ if not sym or not cond or tgt is None:
392
+ continue
393
+ cur = prices.get(sym)
394
+ if cur is None:
395
+ continue
396
+ hit = False
397
+ if cond == "gt" and cur > tgt:
398
+ hit = True
399
+ elif cond == "lt" and cur < tgt:
400
+ hit = True
401
+ elif cond == "cross_up" and cur >= tgt:
402
+ hit = True
403
+ elif cond == "cross_down" and cur <= tgt:
404
+ hit = True
405
+ if hit:
406
+ alert["triggered"] = True
407
+ alert["triggered_at"] = datetime.now().isoformat()
408
+ alert["triggered_price"] = cur
409
+ triggered_now.append(alert)
410
+
411
+ _save_alerts(alerts)
412
+
413
+ # Push notifications for newly triggered alerts
414
+ if triggered_now:
415
+ try:
416
+ from notification_tools import send_alert_notification
417
+ for _alrt in triggered_now:
418
+ send_alert_notification(_alrt)
419
+ except Exception as _ne:
420
+ logger.debug("Notification dispatch failed: %s", _ne)
421
+
422
+ return {
423
+ "success": True,
424
+ "triggered": triggered_now,
425
+ "checked": len(active),
426
+ "prices": prices,
427
+ }
428
+
429
+
430
+ # ── 相关性矩阵 ────────────────────────────────────────────────────────────────
431
+
432
+ def calc_correlation_matrix(params: dict) -> dict:
433
+ """
434
+ 计算多资产收益率相关性矩阵。
435
+
436
+ 参数:
437
+ symbols: list[str],如 ["AAPL","MSFT","TSLA","SPY"]
438
+ period: 历史区间 ("1y" / "2y" / "6mo",默认 "1y")
439
+ interval: 频率 ("1d" / "1wk",默认 "1d")
440
+ """
441
+ if not _HAS_YF:
442
+ return {"success": False, "error": "yfinance 未安装"}
443
+ if not _HAS_PD or not _HAS_NP:
444
+ return {"success": False, "error": "pandas / numpy 未安装"}
445
+
446
+ symbols = params.get("symbols", [])
447
+ period = str(params.get("period", "1y"))
448
+ interval = str(params.get("interval", "1d"))
449
+
450
+ if len(symbols) < 2:
451
+ return {"success": False, "error": "至少需要 2 个标的"}
452
+ if len(symbols) > 20:
453
+ symbols = symbols[:20]
454
+
455
+ try:
456
+ raw = yf.download(symbols, period=period, interval=interval,
457
+ progress=False, auto_adjust=True)
458
+ if raw.empty:
459
+ return {"success": False, "error": "下载数据为空"}
460
+
461
+ if isinstance(raw.columns, pd.MultiIndex):
462
+ closes = raw["Close"]
463
+ else:
464
+ closes = raw
465
+
466
+ closes = closes.dropna(how="all", axis=1)
467
+ rets = closes.pct_change().dropna()
468
+ corr = rets.corr()
469
+
470
+ # Convert to serializable format
471
+ corr_dict = {}
472
+ for sym in corr.columns:
473
+ corr_dict[sym] = {
474
+ other: round(float(corr.loc[sym, other]), 4)
475
+ for other in corr.columns
476
+ }
477
+
478
+ # Summary stats
479
+ stats = {}
480
+ for sym in closes.columns:
481
+ s = closes[sym].dropna()
482
+ r = rets[sym].dropna() if sym in rets.columns else pd.Series(dtype=float)
483
+ if s.empty:
484
+ continue
485
+ stats[sym] = {
486
+ "return_total": round((s.iloc[-1]/s.iloc[0] - 1)*100, 2),
487
+ "volatility": round(float(r.std() * np.sqrt(252) * 100), 2) if len(r) > 1 else None,
488
+ "sharpe": _calc_sharpe(r),
489
+ "max_drawdown": round(_max_drawdown(s) * 100, 2),
490
+ }
491
+
492
+ return {
493
+ "success": True,
494
+ "symbols": list(closes.columns),
495
+ "period": period,
496
+ "interval": interval,
497
+ "corr_matrix": corr_dict,
498
+ "stats": stats,
499
+ "data_points": len(rets),
500
+ }
501
+ except Exception as e:
502
+ return {"success": False, "error": str(e)}
503
+
504
+
505
+ # ── 多资产组合回测 ─────────────────────────────────────────────────────────────
506
+
507
+ def portfolio_backtest(params: dict) -> dict:
508
+ """
509
+ 多资产组合历史回测。
510
+
511
+ 参数:
512
+ symbols: list[str],如 ["AAPL","MSFT","GOOG"]
513
+ weights: list[float],权重(自动归一化);为空则等权
514
+ period: "1y" / "2y" / "3y" / "5y"(默认 "2y")
515
+ benchmark: 基准标的(默认 "SPY")
516
+ rebalance: 再平衡频率 ("monthly" / "quarterly" / "none",默认 "monthly")
517
+ """
518
+ if not _HAS_YF or not _HAS_PD or not _HAS_NP:
519
+ return {"success": False, "error": "需要 yfinance + pandas + numpy"}
520
+
521
+ symbols = params.get("symbols", [])
522
+ weights_r = params.get("weights", [])
523
+ period = str(params.get("period", "2y"))
524
+ benchmark = str(params.get("benchmark", "SPY"))
525
+ rebalance = str(params.get("rebalance", "monthly"))
526
+
527
+ if not symbols:
528
+ return {"success": False, "error": "symbols 不能为空"}
529
+ if len(symbols) > 15:
530
+ symbols = symbols[:15]
531
+
532
+ # Normalize weights
533
+ if weights_r and len(weights_r) == len(symbols):
534
+ ws = [float(w) for w in weights_r]
535
+ total = sum(ws)
536
+ weights = [w / total for w in ws]
537
+ else:
538
+ weights = [1 / len(symbols)] * len(symbols)
539
+
540
+ all_syms = list(dict.fromkeys(symbols + [benchmark]))
541
+ try:
542
+ raw = yf.download(all_syms, period=period, progress=False, auto_adjust=True)
543
+ if raw.empty:
544
+ return {"success": False, "error": "数据下载失败"}
545
+
546
+ closes = raw["Close"] if isinstance(raw.columns, pd.MultiIndex) else raw
547
+ closes = closes.dropna(how="all", axis=1)
548
+
549
+ # Align symbols with available data
550
+ avail = [s for s in symbols if s in closes.columns]
551
+ if not avail:
552
+ return {"success": False, "error": "所有标的数据均不可用"}
553
+ if len(avail) < len(symbols):
554
+ missing = [s for s in symbols if s not in closes.columns]
555
+ weights = [weights[i] for i, s in enumerate(symbols) if s in closes.columns]
556
+ wt_total = sum(weights)
557
+ weights = [w / wt_total for w in weights]
558
+ symbols = avail
559
+
560
+ port_data = closes[symbols].ffill().dropna()
561
+ if benchmark in closes.columns:
562
+ bench_data = closes[benchmark].ffill().dropna()
563
+ else:
564
+ bench_data = None
565
+
566
+ # Rebalance
567
+ if rebalance == "monthly":
568
+ freq = "MS"
569
+ elif rebalance == "quarterly":
570
+ freq = "QS"
571
+ else:
572
+ freq = None
573
+
574
+ # Compute portfolio returns
575
+ if freq and len(port_data) > 0:
576
+ port_rets = port_data.pct_change().dropna()
577
+ # Monthly rebalancing: compound weighted returns per period
578
+ port_monthly_rets = (port_rets + 1).resample(freq).prod() - 1
579
+ port_rets_w = (port_monthly_rets[symbols] * weights).sum(axis=1)
580
+ # Expand back to daily for metrics
581
+ port_cum = (1 + port_rets_w).cumprod()
582
+ else:
583
+ port_rets = port_data.pct_change().dropna()
584
+ port_rets_w = (port_rets[symbols] * weights).sum(axis=1)
585
+ port_cum = (1 + port_rets_w).cumprod()
586
+
587
+ total_return = float(port_cum.iloc[-1] - 1) * 100
588
+ annual_vol = float(port_rets_w.std() * np.sqrt(252 if freq else 252) * 100)
589
+ sharpe = _calc_sharpe(port_rets_w)
590
+ max_dd = round(_max_drawdown(port_cum) * 100, 2)
591
+
592
+ # Benchmark
593
+ bench_stats = None
594
+ if bench_data is not None:
595
+ bench_rets = bench_data.pct_change().dropna()
596
+ b_cum = (1 + bench_rets).cumprod()
597
+ bench_stats = {
598
+ "symbol": benchmark,
599
+ "total_return": round((float(b_cum.iloc[-1]) - 1)*100, 2),
600
+ "volatility": round(float(bench_rets.std() * np.sqrt(252) * 100), 2),
601
+ "sharpe": _calc_sharpe(bench_rets),
602
+ "max_drawdown": round(_max_drawdown(b_cum) * 100, 2),
603
+ }
604
+
605
+ # Allocation breakdown
606
+ allocation = [{"symbol": s, "weight_pct": round(w*100, 1),
607
+ "weight_abs": w} for s, w in zip(symbols, weights)]
608
+
609
+ # Performance curve (monthly points)
610
+ if freq:
611
+ curve_pts = [{"date": str(d.date()), "value": round(float(v), 4)}
612
+ for d, v in port_cum.items()]
613
+ else:
614
+ curve_pts = [{"date": str(d.date()), "value": round(float(v), 4)}
615
+ for d, v in port_cum.iloc[::5].items()] # every 5 days
616
+
617
+ return {
618
+ "success": True,
619
+ "symbols": symbols,
620
+ "weights": weights,
621
+ "allocation": allocation,
622
+ "period": period,
623
+ "rebalance": rebalance,
624
+ "portfolio": {
625
+ "total_return_pct": round(total_return, 2),
626
+ "annual_vol_pct": round(annual_vol, 2),
627
+ "sharpe_ratio": sharpe,
628
+ "max_drawdown_pct": max_dd,
629
+ "calmar_ratio": round(total_return / abs(max_dd), 2) if max_dd != 0 else None,
630
+ },
631
+ "benchmark": bench_stats,
632
+ "curve": curve_pts[-60:], # last 60 points for chart
633
+ "data_points": len(port_rets_w),
634
+ }
635
+ except Exception as e:
636
+ logger.exception("Portfolio backtest error")
637
+ return {"success": False, "error": str(e)}
638
+
639
+
640
+ # ── 自定义因子 DSL ─────────────────────────────────────────────────────────────
641
+
642
+ def eval_custom_factor(params: dict) -> dict:
643
+ """
644
+ 计算自定义因子表达式。
645
+
646
+ 参数:
647
+ symbol: 标的代码
648
+ period: 历史区间(默认 "1y")
649
+ expr: 因子表达式字符串(Python 数学语法,可使用变量)
650
+ 可用变量:close, open, high, low, volume, returns
651
+ 可用函数:sma(n), ema(n), std(n), rsi(n), atr(n)
652
+ 示例:
653
+ "(close - sma(20)) / std(20)" → Z-Score
654
+ "sma(5) / sma(20) - 1" → 短期动量
655
+ "volume / sma(20, volume)" → 量比
656
+ """
657
+ if not _HAS_YF or not _HAS_PD or not _HAS_NP:
658
+ return {"success": False, "error": "需要 yfinance + pandas + numpy"}
659
+
660
+ symbol = str(params.get("symbol", "")).upper()
661
+ period = str(params.get("period", "1y"))
662
+ expr = str(params.get("expr", "")).strip()
663
+
664
+ if not symbol or not expr:
665
+ return {"success": False, "error": "symbol 和 expr 均为必填"}
666
+
667
+ # Security: block imports and exec
668
+ forbidden = ["import", "__", "exec(", "eval(", "open(", "os.", "sys."]
669
+ for tok in forbidden:
670
+ if tok in expr:
671
+ return {"success": False, "error": f"表达式包含禁止关键字: {tok}"}
672
+
673
+ try:
674
+ tkr = yf.Ticker(symbol)
675
+ hist = tkr.history(period=period, auto_adjust=True)
676
+ if hist.empty:
677
+ return {"success": False, "error": f"无法获取 {symbol} 历史数据"}
678
+
679
+ close = hist["Close"].astype(float)
680
+ open_ = hist["Open"].astype(float)
681
+ high = hist["High"].astype(float)
682
+ low = hist["Low"].astype(float)
683
+ volume = hist["Volume"].astype(float)
684
+ returns = close.pct_change()
685
+
686
+ def sma(n, series=None):
687
+ s = series if series is not None else close
688
+ return s.rolling(n).mean()
689
+
690
+ def ema(n, series=None):
691
+ s = series if series is not None else close
692
+ return s.ewm(span=n, adjust=False).mean()
693
+
694
+ def std(n, series=None):
695
+ s = series if series is not None else close
696
+ return s.rolling(n).std()
697
+
698
+ def rsi(n=14):
699
+ delta = close.diff()
700
+ gain = delta.clip(lower=0).rolling(n).mean()
701
+ loss = (-delta.clip(upper=0)).rolling(n).mean()
702
+ rs = gain / loss
703
+ return 100 - (100 / (1 + rs))
704
+
705
+ def atr(n=14):
706
+ tr = pd.concat([
707
+ high - low,
708
+ (high - close.shift()).abs(),
709
+ (low - close.shift()).abs(),
710
+ ], axis=1).max(axis=1)
711
+ return tr.rolling(n).mean()
712
+
713
+ ns = dict(
714
+ close=close, open=open_, high=high, low=low,
715
+ volume=volume, returns=returns,
716
+ sma=sma, ema=ema, std=std, rsi=rsi, atr=atr,
717
+ pd=pd, np=np, abs=abs, min=min, max=max,
718
+ )
719
+
720
+ result_series = eval(expr, {"__builtins__": {}}, ns) # noqa: S307
721
+ if hasattr(result_series, "dropna"):
722
+ result_series = result_series.dropna()
723
+
724
+ latest_val = float(result_series.iloc[-1]) if len(result_series) > 0 else None
725
+
726
+ # Last 20 values for chart
727
+ tail = result_series.tail(20)
728
+ series_pts = [{"date": str(d.date()), "value": round(float(v), 6)}
729
+ for d, v in tail.items()]
730
+
731
+ return {
732
+ "success": True,
733
+ "symbol": symbol,
734
+ "expr": expr,
735
+ "latest": latest_val,
736
+ "series": series_pts,
737
+ "data_len": len(result_series),
738
+ }
739
+ except Exception as e:
740
+ return {"success": False, "error": f"表达式计算失败: {e}"}
741
+
742
+
743
+ # ── 通用数据导入 ───────────────────────────────────────────────────────────────
744
+
745
+ def load_csv_data(params: dict) -> dict:
746
+ """
747
+ 从 CSV 文件加载数据到内存 DuckDB 表。
748
+
749
+ 参数:
750
+ path: CSV 文件路径
751
+ table_name: 目标表名(默认由文件名推导)
752
+ encoding: 编码(默认 utf-8)
753
+ """
754
+ if not _HAS_PD:
755
+ return {"success": False, "error": "pandas 未安装"}
756
+
757
+ path = Path(str(params.get("path", ""))).expanduser()
758
+ if not path.exists():
759
+ return {"success": False, "error": f"文件不存在: {path}"}
760
+ if path.suffix.lower() not in (".csv", ".tsv", ".txt"):
761
+ return {"success": False, "error": "仅支持 CSV/TSV 文件"}
762
+
763
+ enc = params.get("encoding", "utf-8")
764
+ tname = str(params.get("table_name", path.stem))
765
+
766
+ try:
767
+ df = pd.read_csv(str(path), encoding=enc)
768
+ if _HAS_DUCK:
769
+ conn = _get_duck_conn()
770
+ conn.register(tname, df)
771
+ _duck_tables[tname] = True
772
+
773
+ return {
774
+ "success": True,
775
+ "table_name": tname,
776
+ "rows": len(df),
777
+ "columns": list(df.columns),
778
+ "sample": df.head(3).to_dict("records"),
779
+ "path": str(path),
780
+ }
781
+ except Exception as e:
782
+ return {"success": False, "error": str(e)}
783
+
784
+
785
+ # ── Private Helpers ───────────────────────────────────────────────────────────
786
+
787
+ def _calc_sharpe(rets, rf_daily: float = 0.0) -> Optional[float]:
788
+ if not _HAS_NP or not hasattr(rets, "__len__") or len(rets) < 2:
789
+ return None
790
+ try:
791
+ excess = rets - rf_daily
792
+ if float(excess.std()) == 0:
793
+ return None
794
+ return round(float(excess.mean() / excess.std() * np.sqrt(252)), 3)
795
+ except Exception:
796
+ return None
797
+
798
+
799
+ def _max_drawdown(cum_series) -> float:
800
+ if not _HAS_NP or not _HAS_PD:
801
+ return 0.0
802
+ try:
803
+ s = pd.Series(cum_series)
804
+ roll_max = s.cummax()
805
+ drawdown = (s - roll_max) / roll_max
806
+ return float(drawdown.min())
807
+ except Exception:
808
+ return 0.0