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
backtest_report.py ADDED
@@ -0,0 +1,472 @@
1
+ """Local real-data strategy backtest and self-contained HTML rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html
6
+ import math
7
+ import re
8
+ from dataclasses import dataclass
9
+ from datetime import date, datetime, timedelta
10
+ from pathlib import Path
11
+ from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
12
+
13
+ from artifacts import create_user_artifact, write_artifact_metadata, write_artifact_raw_data
14
+ from data_service import DataService
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class BacktestConfig:
19
+ symbol: str
20
+ strategy: str = "momentum"
21
+ start_date: Optional[str] = None
22
+ end_date: Optional[str] = None
23
+ initial_capital: float = 100000.0
24
+ fast_period: int = 20
25
+ slow_period: int = 60
26
+ momentum_period: int = 20
27
+
28
+
29
+ def _as_float(value: Any) -> Optional[float]:
30
+ try:
31
+ if value is None:
32
+ return None
33
+ out = float(value)
34
+ return out if math.isfinite(out) else None
35
+ except Exception:
36
+ return None
37
+
38
+
39
+ def _parse_date(value: Any) -> Optional[date]:
40
+ if not value:
41
+ return None
42
+ try:
43
+ return datetime.fromisoformat(str(value)[:10]).date()
44
+ except Exception:
45
+ return None
46
+
47
+
48
+ def _history_days(start_date: Optional[str], end_date: Optional[str]) -> int:
49
+ end = _parse_date(end_date) or date.today()
50
+ start = _parse_date(start_date)
51
+ if not start:
52
+ return 365
53
+ return max((end - start).days + 10, 90)
54
+
55
+
56
+ def _clean_history_rows(rows: Iterable[Dict[str, Any]], start_date: Optional[str], end_date: Optional[str]) -> List[Dict[str, Any]]:
57
+ start = _parse_date(start_date)
58
+ end = _parse_date(end_date)
59
+ cleaned: List[Dict[str, Any]] = []
60
+ for row in rows or []:
61
+ row_date = _parse_date(row.get("date") or row.get("Date"))
62
+ close = _as_float(row.get("close", row.get("Close")))
63
+ if not row_date or close is None or close <= 0:
64
+ continue
65
+ if start and row_date < start:
66
+ continue
67
+ if end and row_date > end:
68
+ continue
69
+ cleaned.append(
70
+ {
71
+ "date": row_date.isoformat(),
72
+ "close": close,
73
+ "open": _as_float(row.get("open", row.get("Open"))),
74
+ "high": _as_float(row.get("high", row.get("High"))),
75
+ "low": _as_float(row.get("low", row.get("Low"))),
76
+ "volume": _as_float(row.get("volume", row.get("Volume"))),
77
+ }
78
+ )
79
+ cleaned.sort(key=lambda x: x["date"])
80
+ return cleaned
81
+
82
+
83
+ def _sma(values: Sequence[float], window: int) -> List[Optional[float]]:
84
+ if window <= 1:
85
+ return [float(v) for v in values]
86
+ out: List[Optional[float]] = []
87
+ rolling = 0.0
88
+ for i, value in enumerate(values):
89
+ rolling += value
90
+ if i >= window:
91
+ rolling -= values[i - window]
92
+ out.append(rolling / window if i + 1 >= window else None)
93
+ return out
94
+
95
+
96
+ def _signals(strategy: str, closes: Sequence[float], fast: int, slow: int, momentum_period: int) -> List[int]:
97
+ strategy = (strategy or "momentum").lower().replace("-", "_").replace(" ", "_")
98
+ n = len(closes)
99
+ if strategy in ("buy_hold", "buyhold", "hold"):
100
+ return [1] * n
101
+ if strategy in ("sma_cross", "ma_cross", "moving_average"):
102
+ fast_ma = _sma(closes, max(2, fast))
103
+ slow_ma = _sma(closes, max(max(3, slow), fast + 1))
104
+ return [1 if f is not None and s is not None and f > s else 0 for f, s in zip(fast_ma, slow_ma)]
105
+ if strategy in ("momentum", "mom"):
106
+ period = max(2, momentum_period)
107
+ return [1 if i >= period and closes[i] > closes[i - period] else 0 for i in range(n)]
108
+ return [1] * n
109
+
110
+
111
+ def _max_drawdown(values: Sequence[float]) -> float:
112
+ if not values:
113
+ return 0.0
114
+ peak = values[0]
115
+ worst = 0.0
116
+ for value in values:
117
+ peak = max(peak, value)
118
+ if peak > 0:
119
+ worst = min(worst, value / peak - 1.0)
120
+ return worst
121
+
122
+
123
+ def _stddev(values: Sequence[float]) -> float:
124
+ if len(values) < 2:
125
+ return 0.0
126
+ mean = sum(values) / len(values)
127
+ variance = sum((v - mean) ** 2 for v in values) / (len(values) - 1)
128
+ return math.sqrt(max(variance, 0.0))
129
+
130
+
131
+ def run_backtest_from_history(history: Sequence[Dict[str, Any]], config: BacktestConfig) -> Dict[str, Any]:
132
+ rows = _clean_history_rows(history, config.start_date, config.end_date)
133
+ min_bars = max(30, min(max(config.slow_period, config.momentum_period) + 2, 80))
134
+ if len(rows) < min_bars:
135
+ return {
136
+ "success": False,
137
+ "symbol": config.symbol,
138
+ "error": f"历史行情不足:需要至少 {min_bars} 根K线,当前 {len(rows)} 根",
139
+ }
140
+
141
+ dates = [str(r["date"]) for r in rows]
142
+ closes = [float(r["close"]) for r in rows]
143
+ volumes = [_as_float(r.get("volume")) for r in rows]
144
+ valid_volumes = [v for v in volumes if v is not None and v >= 0]
145
+ signals = _signals(config.strategy, closes, config.fast_period, config.slow_period, config.momentum_period)
146
+
147
+ initial = float(config.initial_capital or 100000.0)
148
+ equity = [initial]
149
+ benchmark = [initial]
150
+ daily_strategy_returns = [0.0]
151
+ daily_benchmark_returns = [0.0]
152
+ trades = 0
153
+ previous_position = 0
154
+
155
+ for i in range(1, len(closes)):
156
+ day_return = closes[i] / closes[i - 1] - 1.0
157
+ position = signals[i - 1] # shift one day to avoid look-ahead bias
158
+ if position == 1 and previous_position == 0:
159
+ trades += 1
160
+ previous_position = position
161
+ strategy_return = day_return * position
162
+ daily_strategy_returns.append(strategy_return)
163
+ daily_benchmark_returns.append(day_return)
164
+ equity.append(equity[-1] * (1.0 + strategy_return))
165
+ benchmark.append(initial * closes[i] / closes[0])
166
+
167
+ total_return = equity[-1] / initial - 1.0
168
+ benchmark_return = benchmark[-1] / initial - 1.0
169
+ span_days = max((_parse_date(dates[-1]) - _parse_date(dates[0])).days if _parse_date(dates[-1]) and _parse_date(dates[0]) else len(rows), 1)
170
+ years = max(span_days / 365.25, 1 / 252)
171
+ annualized_return = (1.0 + total_return) ** (1.0 / years) - 1.0 if total_return > -1 else -1.0
172
+ volatility = _stddev(daily_strategy_returns[1:]) * math.sqrt(252)
173
+ sharpe = (sum(daily_strategy_returns[1:]) / max(len(daily_strategy_returns) - 1, 1)) / _stddev(daily_strategy_returns[1:]) * math.sqrt(252) if _stddev(daily_strategy_returns[1:]) > 0 else 0.0
174
+ active_returns = [r for r, s in zip(daily_strategy_returns[1:], signals[:-1]) if s == 1]
175
+ win_rate = sum(1 for r in active_returns if r > 0) / len(active_returns) if active_returns else 0.0
176
+
177
+ curve = [
178
+ {
179
+ "date": d,
180
+ "strategy": round(e, 4),
181
+ "benchmark": round(b, 4),
182
+ "close": round(c, 4),
183
+ "position": int(sig),
184
+ }
185
+ for d, e, b, c, sig in zip(dates, equity, benchmark, closes, signals)
186
+ ]
187
+
188
+ return {
189
+ "success": True,
190
+ "symbol": config.symbol.upper(),
191
+ "strategy": config.strategy,
192
+ "start": dates[0],
193
+ "end": dates[-1],
194
+ "bars": len(rows),
195
+ "initial_capital": initial,
196
+ "total_return": round(total_return, 6),
197
+ "annualized_return": round(annualized_return, 6),
198
+ "annual_return": round(annualized_return, 6),
199
+ "benchmark_return": round(benchmark_return, 6),
200
+ "buy_hold_return": round(benchmark_return, 6),
201
+ "alpha": round(total_return - benchmark_return, 6),
202
+ "max_drawdown": round(_max_drawdown(equity), 6),
203
+ "benchmark_max_drawdown": round(_max_drawdown(benchmark), 6),
204
+ "annual_volatility": round(volatility, 6),
205
+ "sharpe_ratio": round(sharpe, 4),
206
+ "win_rate": round(win_rate, 4),
207
+ "total_trades": trades,
208
+ "volume_summary": {
209
+ "last": round(valid_volumes[-1], 2) if valid_volumes else None,
210
+ "average": round(sum(valid_volumes) / len(valid_volumes), 2) if valid_volumes else None,
211
+ "min": round(min(valid_volumes), 2) if valid_volumes else None,
212
+ "max": round(max(valid_volumes), 2) if valid_volumes else None,
213
+ "coverage": round(len(valid_volumes) / len(rows), 4) if rows else 0.0,
214
+ },
215
+ "equity_curve": curve,
216
+ }
217
+
218
+
219
+ def _fmt_pct(value: Any) -> str:
220
+ number = _as_float(value)
221
+ if number is None:
222
+ return "-"
223
+ return f"{number * 100:+.2f}%"
224
+
225
+
226
+ def _fmt_num(value: Any, digits: int = 2) -> str:
227
+ number = _as_float(value)
228
+ if number is None:
229
+ return "-"
230
+ return f"{number:,.{digits}f}"
231
+
232
+
233
+ def _points(values: Sequence[float], width: int, height: int, pad: int) -> str:
234
+ if not values:
235
+ return ""
236
+ lo = min(values)
237
+ hi = max(values)
238
+ if math.isclose(lo, hi):
239
+ lo *= 0.99
240
+ hi *= 1.01
241
+ usable_w = max(width - pad * 2, 1)
242
+ usable_h = max(height - pad * 2, 1)
243
+ pts = []
244
+ for i, value in enumerate(values):
245
+ x = pad + usable_w * (i / max(len(values) - 1, 1))
246
+ y = pad + usable_h * (1 - (value - lo) / (hi - lo))
247
+ pts.append(f"{x:.1f},{y:.1f}")
248
+ return " ".join(pts)
249
+
250
+
251
+ def render_backtest_html(result: Dict[str, Any], output_path: Path) -> Path:
252
+ output_path.parent.mkdir(parents=True, exist_ok=True)
253
+ curve = result.get("equity_curve") or []
254
+ strategy_values = [_as_float(p.get("strategy")) or 0.0 for p in curve if isinstance(p, dict)]
255
+ benchmark_values = [_as_float(p.get("benchmark")) or 0.0 for p in curve if isinstance(p, dict)]
256
+ width, height, pad = 920, 360, 34
257
+ strategy_points = _points(strategy_values, width, height, pad)
258
+ benchmark_points = _points(benchmark_values, width, height, pad)
259
+ start = html.escape(str(result.get("start", "")))
260
+ end = html.escape(str(result.get("end", "")))
261
+ symbol = html.escape(str(result.get("symbol", "")))
262
+ strategy = html.escape(str(result.get("strategy", "")))
263
+ created_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
264
+ bars = int(result.get("bars") or len(curve))
265
+ latest_strategy = strategy_values[-1] if strategy_values else 0.0
266
+ latest_benchmark = benchmark_values[-1] if benchmark_values else 0.0
267
+ provider_chain = [str(p) for p in (result.get("provider_chain") or []) if p]
268
+ missing_fields = [str(p) for p in (result.get("missing_fields") or []) if p]
269
+ data_status = html.escape(str(result.get("data_status") or "unknown"))
270
+ data_provider = html.escape(str(result.get("data_provider") or "history"))
271
+ data_updated_at = html.escape(str(result.get("data_updated_at") or ""))
272
+ source_text = html.escape(" → ".join(provider_chain) if provider_chain else data_provider)
273
+ missing_text = html.escape(", ".join(missing_fields) if missing_fields else "none")
274
+
275
+ metrics = [
276
+ ("策略收益", _fmt_pct(result.get("total_return"))),
277
+ ("买入持有", _fmt_pct(result.get("benchmark_return"))),
278
+ ("超额收益", _fmt_pct(result.get("alpha"))),
279
+ ("年化收益", _fmt_pct(result.get("annualized_return"))),
280
+ ("最大回撤", _fmt_pct(result.get("max_drawdown"))),
281
+ ("夏普比率", _fmt_num(result.get("sharpe_ratio"), 2)),
282
+ ("胜率", _fmt_pct(result.get("win_rate"))),
283
+ ("交易次数", str(result.get("total_trades", 0))),
284
+ ]
285
+ metric_html = "\n".join(
286
+ f"<div class=\"metric\"><span>{html.escape(k)}</span><strong>{html.escape(v)}</strong></div>"
287
+ for k, v in metrics
288
+ )
289
+
290
+ html_doc = f"""<!doctype html>
291
+ <html lang="zh-CN">
292
+ <head>
293
+ <meta charset="utf-8">
294
+ <meta name="viewport" content="width=device-width, initial-scale=1">
295
+ <title>{symbol} {strategy} Backtest</title>
296
+ <style>
297
+ :root {{
298
+ color-scheme: light dark;
299
+ --bg: #f7f7f4;
300
+ --panel: #ffffff;
301
+ --text: #1f1f1f;
302
+ --muted: #787878;
303
+ --line: #d8d4ce;
304
+ --accent: #b8794b;
305
+ --bench: #6d6d6d;
306
+ }}
307
+ @media (prefers-color-scheme: dark) {{
308
+ :root {{
309
+ --bg: #181818;
310
+ --panel: #222222;
311
+ --text: #eeeeee;
312
+ --muted: #9a9a9a;
313
+ --line: #3a3a3a;
314
+ --accent: #d08a52;
315
+ --bench: #aaaaaa;
316
+ }}
317
+ }}
318
+ * {{ box-sizing: border-box; }}
319
+ body {{
320
+ margin: 0;
321
+ background: var(--bg);
322
+ color: var(--text);
323
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
324
+ line-height: 1.45;
325
+ }}
326
+ main {{ max-width: 1080px; margin: 0 auto; padding: 28px 20px 40px; }}
327
+ .top {{ display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 18px; }}
328
+ h1 {{ font-size: 24px; margin: 0; letter-spacing: 0; }}
329
+ .sub {{ color: var(--muted); font-size: 14px; }}
330
+ .panel {{ background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 18px; margin-top: 14px; }}
331
+ .metrics {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }}
332
+ .metric {{ border-top: 1px solid var(--line); padding-top: 10px; min-height: 58px; }}
333
+ .metric span {{ display: block; color: var(--muted); font-size: 12px; margin-bottom: 4px; }}
334
+ .metric strong {{ font-size: 18px; }}
335
+ .legend {{ display: flex; gap: 18px; color: var(--muted); font-size: 13px; margin-top: 12px; }}
336
+ .legend i {{ display: inline-block; width: 18px; height: 3px; vertical-align: middle; margin-right: 6px; }}
337
+ .provenance {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 10px; font-size: 13px; }}
338
+ .provenance div {{ border-top: 1px solid var(--line); padding-top: 8px; }}
339
+ .provenance span {{ display: block; color: var(--muted); font-size: 12px; margin-bottom: 3px; }}
340
+ svg {{ width: 100%; height: auto; display: block; }}
341
+ .foot {{ color: var(--muted); font-size: 12px; margin-top: 14px; }}
342
+ </style>
343
+ </head>
344
+ <body>
345
+ <main>
346
+ <div class="top">
347
+ <div>
348
+ <h1>{symbol} · {strategy} 策略回测</h1>
349
+ <div class="sub">{start} → {end} · {bars} bars · real historical data</div>
350
+ </div>
351
+ <div class="sub">Aria Code · {html.escape(created_at)}</div>
352
+ </div>
353
+ <section class="panel metrics">
354
+ {metric_html}
355
+ </section>
356
+ <section class="panel">
357
+ <svg viewBox="0 0 {width} {height}" role="img" aria-label="equity curve">
358
+ <rect x="0" y="0" width="{width}" height="{height}" fill="transparent"/>
359
+ <line x1="{pad}" y1="{height - pad}" x2="{width - pad}" y2="{height - pad}" stroke="var(--line)"/>
360
+ <line x1="{pad}" y1="{pad}" x2="{pad}" y2="{height - pad}" stroke="var(--line)"/>
361
+ <polyline points="{benchmark_points}" fill="none" stroke="var(--bench)" stroke-width="2" stroke-dasharray="6 6"/>
362
+ <polyline points="{strategy_points}" fill="none" stroke="var(--accent)" stroke-width="3"/>
363
+ <text x="{pad}" y="{pad - 10}" fill="var(--muted)" font-size="12">策略权益 {html.escape(_fmt_num(latest_strategy, 0))}</text>
364
+ <text x="{width - pad - 210}" y="{pad - 10}" fill="var(--muted)" font-size="12">基准权益 {html.escape(_fmt_num(latest_benchmark, 0))}</text>
365
+ </svg>
366
+ <div class="legend">
367
+ <span><i style="background:var(--accent)"></i>Strategy equity</span>
368
+ <span><i style="background:var(--bench)"></i>Buy & hold</span>
369
+ </div>
370
+ </section>
371
+ <section class="panel provenance">
372
+ <div><span>Data status</span>{data_status}</div>
373
+ <div><span>Provider chain</span>{source_text}</div>
374
+ <div><span>Missing fields</span>{missing_text}</div>
375
+ <div><span>Updated at</span>{data_updated_at or html.escape(created_at)}</div>
376
+ </section>
377
+ <div class="foot">本报告为本地生成的历史模拟,不构成投资建议。信号按前一交易日收盘后生效,避免未来函数。</div>
378
+ </main>
379
+ </body>
380
+ </html>
381
+ """
382
+ output_path.write_text(html_doc, encoding="utf-8")
383
+ return output_path
384
+
385
+
386
+ def generate_backtest_report(
387
+ config: BacktestConfig,
388
+ output_dir: Optional[Path] = None,
389
+ market_client: Optional[Any] = None,
390
+ ) -> Dict[str, Any]:
391
+ data_service = DataService(market_client=market_client) if market_client is not None else DataService()
392
+ days = _history_days(config.start_date, config.end_date)
393
+ hist_result = data_service.history(config.symbol, days=days, interval="1d")
394
+ hist = hist_result.data
395
+ if not hist_result.success:
396
+ return {
397
+ "success": False,
398
+ "symbol": config.symbol,
399
+ "strategy": config.strategy,
400
+ "error": hist.get("friendly_error") or hist.get("error") or "历史行情获取失败",
401
+ "provider_chain": hist_result.provider_chain,
402
+ "missing_fields": hist_result.missing_fields,
403
+ "data_status": "data_unavailable",
404
+ "data_warnings": hist_result.warnings,
405
+ }
406
+
407
+ result = run_backtest_from_history(hist.get("data") or [], config)
408
+ result["data_provider"] = hist_result.source or hist.get("provider")
409
+ result["provider_chain"] = hist_result.provider_chain or hist.get("provider_chain") or [hist.get("provider", "history")]
410
+ result["missing_fields"] = hist_result.missing_fields
411
+ result["data_warnings"] = hist_result.warnings
412
+ result["data_status"] = "complete" if hist_result.success and not hist_result.missing_fields else "partial"
413
+ result["data_updated_at"] = hist_result.timestamp
414
+ if not result.get("success"):
415
+ return result
416
+
417
+ safe_symbol = re.sub(r"[^A-Za-z0-9_.-]+", "_", config.symbol.upper()).strip("_") or "SYMBOL"
418
+ safe_strategy = re.sub(r"[^A-Za-z0-9_.-]+", "_", config.strategy.lower()).strip("_") or "strategy"
419
+ ts_dt = datetime.now()
420
+ if output_dir:
421
+ root = Path(output_dir)
422
+ root.mkdir(parents=True, exist_ok=True)
423
+ output_path = root / f"{safe_symbol}_{safe_strategy}_{ts_dt.strftime('%Y%m%d_%H%M%S')}.html"
424
+ artifact = None
425
+ else:
426
+ artifact = create_user_artifact(
427
+ "strategies/backtests",
428
+ config.symbol,
429
+ f"{safe_symbol}_{safe_strategy}_backtest",
430
+ ".html",
431
+ timestamp=ts_dt,
432
+ )
433
+ output_path = artifact.path
434
+ render_backtest_html(result, output_path)
435
+ if artifact:
436
+ write_artifact_metadata(artifact, {
437
+ "kind": "strategy_backtest",
438
+ "status": "complete",
439
+ "symbol": config.symbol,
440
+ "strategy": config.strategy,
441
+ "created_at": ts_dt.isoformat(timespec="seconds"),
442
+ "data": {
443
+ "provider": result.get("data_provider"),
444
+ "provider_chain": result.get("provider_chain"),
445
+ "missing_fields": result.get("missing_fields"),
446
+ "status": result.get("data_status"),
447
+ "updated_at": result.get("data_updated_at"),
448
+ "rows": len(hist.get("data") or []),
449
+ },
450
+ "config": config.__dict__,
451
+ "metrics": {
452
+ key: result.get(key)
453
+ for key in ("total_return", "annual_return", "sharpe", "max_drawdown", "win_rate", "trades")
454
+ },
455
+ })
456
+ write_artifact_raw_data(artifact, {
457
+ "symbol": config.symbol,
458
+ "strategy": config.strategy,
459
+ "history": hist.get("data") or [],
460
+ "data": {
461
+ "provider": result.get("data_provider"),
462
+ "provider_chain": result.get("provider_chain"),
463
+ "missing_fields": result.get("missing_fields"),
464
+ "status": result.get("data_status"),
465
+ "warnings": result.get("data_warnings"),
466
+ "updated_at": result.get("data_updated_at"),
467
+ },
468
+ "result": result,
469
+ })
470
+ result["report_path"] = str(output_path)
471
+ result["provider"] = "local_backtest"
472
+ return result
brokers/__init__.py ADDED
@@ -0,0 +1,72 @@
1
+ """
2
+ brokers/ — Aria Code 券商接入层
3
+ ================================
4
+ 统一接口连接中国和国际主流券商,支持账户查询、持仓管理和下单。
5
+
6
+ 快速使用::
7
+
8
+ from brokers.registry import get_registry
9
+
10
+ reg = get_registry()
11
+ broker = reg.connect("xt_main") # 从 ~/.arthera/brokers.json 读取
12
+ acct = broker.account_info()
13
+ pos = broker.positions()
14
+
15
+ 支持券商
16
+ --------
17
+ 中国:
18
+ xtquant 迅投 XTQuant(中信/华鑫/浙商等QMT平台)
19
+ easytrader EasyTrader(同花顺/通达信/华泰/国君 等客户端)
20
+ futu 富途牛牛 OpenAPI(港/美/A股)
21
+ tiger 老虎证券 OpenAPI(美/港/A股)
22
+ longbridge 长桥证券 OpenAPI(港/美/A股)
23
+
24
+ 国际:
25
+ ibkr Interactive Brokers TWS/Gateway
26
+ alpaca Alpaca Markets(美股 + 模拟盘)
27
+ webull Webull(美股,只读模式)
28
+
29
+ 配置文件:~/.arthera/brokers.json
30
+ """
31
+
32
+ from .base import BrokerBase, AccountInfo, Position, Order, OrderResult, PortfolioSummary
33
+ from .config import (
34
+ load_config, list_broker_configs, get_broker_config,
35
+ add_broker_config, remove_broker_config, set_default_broker,
36
+ validate_broker_config, supported_broker_types, get_config_template,
37
+ BROKERS_CONFIG_PATH,
38
+ )
39
+ from .registry import BrokerRegistry, get_registry
40
+ from .planning import (
41
+ PortfolioSnapshot, StrategyIntent, RiskRuleSet, PlannedOrder, OrderPlan,
42
+ snapshot_from_broker, infer_intent_from_backtest, plan_order,
43
+ evaluate_risk, plans_from_strategy_results,
44
+ )
45
+ from .capabilities import (
46
+ BrokerCapability, broker_connection_plan, broker_dependency_state,
47
+ broker_service_playbook, filter_capabilities, get_broker_capability,
48
+ list_broker_capabilities,
49
+ )
50
+ from .paper_broker import PaperBroker, reset_paper_account
51
+ from .trading import (
52
+ OrderIntent, TradingPolicy, build_order_preview, execute_order_preview,
53
+ list_order_previews, load_order_preview, policy_from_config, resolve_trading_mode,
54
+ )
55
+
56
+ __all__ = [
57
+ "BrokerBase", "AccountInfo", "Position", "Order", "OrderResult", "PortfolioSummary",
58
+ "load_config", "list_broker_configs", "get_broker_config",
59
+ "add_broker_config", "remove_broker_config", "set_default_broker",
60
+ "validate_broker_config", "supported_broker_types", "get_config_template",
61
+ "BROKERS_CONFIG_PATH",
62
+ "BrokerRegistry", "get_registry",
63
+ "PortfolioSnapshot", "StrategyIntent", "RiskRuleSet", "PlannedOrder", "OrderPlan",
64
+ "snapshot_from_broker", "infer_intent_from_backtest", "plan_order",
65
+ "evaluate_risk", "plans_from_strategy_results",
66
+ "BrokerCapability", "broker_connection_plan", "broker_dependency_state",
67
+ "broker_service_playbook", "filter_capabilities", "get_broker_capability",
68
+ "list_broker_capabilities",
69
+ "PaperBroker", "reset_paper_account",
70
+ "OrderIntent", "TradingPolicy", "build_order_preview", "execute_order_preview",
71
+ "list_order_previews", "load_order_preview", "policy_from_config", "resolve_trading_mode",
72
+ ]