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,259 @@
1
+ """Local paper-trading broker.
2
+
3
+ The paper broker implements the same BrokerBase contract as live adapters, but
4
+ all orders are filled into a local JSON ledger. It is meant for simulation,
5
+ strategy rehearsal, and TradingView alert dry-runs.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import time
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List
14
+
15
+ from .base import AccountInfo, BrokerBase, Order, OrderResult, Position
16
+ from .config import BROKERS_CONFIG_PATH
17
+
18
+
19
+ PAPER_LEDGER_PATH = BROKERS_CONFIG_PATH.parent / "paper_ledger.json"
20
+
21
+
22
+ def _now_id(prefix: str) -> str:
23
+ return f"{prefix}_{int(time.time() * 1000)}"
24
+
25
+
26
+ def _load_ledger() -> Dict[str, Any]:
27
+ if not PAPER_LEDGER_PATH.exists():
28
+ return {"accounts": {}}
29
+ try:
30
+ data = json.loads(PAPER_LEDGER_PATH.read_text(encoding="utf-8"))
31
+ if isinstance(data, dict):
32
+ data.setdefault("accounts", {})
33
+ return data
34
+ except Exception:
35
+ pass
36
+ return {"accounts": {}}
37
+
38
+
39
+ def _save_ledger(data: Dict[str, Any]) -> None:
40
+ PAPER_LEDGER_PATH.parent.mkdir(parents=True, exist_ok=True)
41
+ PAPER_LEDGER_PATH.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
42
+
43
+
44
+ class PaperBroker(BrokerBase):
45
+ broker_type = "paper"
46
+ broker_name = "Aria Paper Broker"
47
+ market = "GLOBAL"
48
+
49
+ def __init__(self, broker_id: str, config: Dict[str, Any]):
50
+ super().__init__(broker_id, config)
51
+ self.currency = str(config.get("currency", "USD") or "USD").upper()
52
+ self.starting_cash = float(config.get("starting_cash", 100000.0) or 100000.0)
53
+
54
+ def connect(self) -> bool:
55
+ ledger = _load_ledger()
56
+ accounts = ledger.setdefault("accounts", {})
57
+ if self.broker_id not in accounts:
58
+ accounts[self.broker_id] = {
59
+ "broker_id": self.broker_id,
60
+ "label": self.label,
61
+ "currency": self.currency,
62
+ "starting_cash": self.starting_cash,
63
+ "cash": self.starting_cash,
64
+ "positions": {},
65
+ "orders": [],
66
+ "created_at": int(time.time()),
67
+ }
68
+ _save_ledger(ledger)
69
+ self._connected = True
70
+ return True
71
+
72
+ def reset(self, starting_cash: float | None = None, currency: str | None = None) -> None:
73
+ ledger = _load_ledger()
74
+ accounts = ledger.setdefault("accounts", {})
75
+ cash = float(starting_cash if starting_cash is not None else self.starting_cash)
76
+ curr = str(currency or self.currency).upper()
77
+ accounts[self.broker_id] = {
78
+ "broker_id": self.broker_id,
79
+ "label": self.label,
80
+ "currency": curr,
81
+ "starting_cash": cash,
82
+ "cash": cash,
83
+ "positions": {},
84
+ "orders": [],
85
+ "created_at": int(time.time()),
86
+ }
87
+ _save_ledger(ledger)
88
+ self.currency = curr
89
+ self.starting_cash = cash
90
+ self._connected = True
91
+
92
+ def _account(self) -> Dict[str, Any]:
93
+ if not self._connected:
94
+ self.connect()
95
+ ledger = _load_ledger()
96
+ return ledger.setdefault("accounts", {}).setdefault(self.broker_id, {
97
+ "broker_id": self.broker_id,
98
+ "label": self.label,
99
+ "currency": self.currency,
100
+ "starting_cash": self.starting_cash,
101
+ "cash": self.starting_cash,
102
+ "positions": {},
103
+ "orders": [],
104
+ })
105
+
106
+ def _save_account(self, account: Dict[str, Any]) -> None:
107
+ ledger = _load_ledger()
108
+ ledger.setdefault("accounts", {})[self.broker_id] = account
109
+ _save_ledger(ledger)
110
+
111
+ def account_info(self) -> AccountInfo:
112
+ account = self._account()
113
+ positions = self._positions_from_account(account)
114
+ market_value = sum(p.market_value for p in positions)
115
+ cost_basis = sum(p.cost_price * p.quantity for p in positions)
116
+ pnl_total = market_value - cost_basis
117
+ cash = float(account.get("cash", 0.0) or 0.0)
118
+ return AccountInfo(
119
+ broker_id=self.broker_id,
120
+ broker_type=self.broker_type,
121
+ label=self.label,
122
+ account_id=f"PAPER-{self.broker_id}",
123
+ currency=str(account.get("currency", self.currency)),
124
+ total_assets=cash + market_value,
125
+ cash=cash,
126
+ market_value=market_value,
127
+ pnl_total=pnl_total,
128
+ pnl_pct=(pnl_total / cost_basis * 100) if cost_basis > 0 else 0.0,
129
+ extra={"mode": "paper", "ledger_path": str(PAPER_LEDGER_PATH)},
130
+ )
131
+
132
+ def positions(self) -> List[Position]:
133
+ return self._positions_from_account(self._account())
134
+
135
+ def _positions_from_account(self, account: Dict[str, Any]) -> List[Position]:
136
+ out: List[Position] = []
137
+ for symbol, raw in sorted((account.get("positions") or {}).items()):
138
+ qty = float(raw.get("quantity", 0.0) or 0.0)
139
+ if qty <= 0:
140
+ continue
141
+ price = float(raw.get("current_price", raw.get("cost_price", 0.0)) or 0.0)
142
+ cost = float(raw.get("cost_price", 0.0) or 0.0)
143
+ market_value = qty * price
144
+ pnl = (price - cost) * qty
145
+ out.append(Position(
146
+ symbol=symbol,
147
+ name=symbol,
148
+ quantity=qty,
149
+ available_qty=qty,
150
+ cost_price=cost,
151
+ current_price=price,
152
+ market_value=market_value,
153
+ pnl=pnl,
154
+ pnl_pct=(pnl / (cost * qty) * 100) if cost > 0 and qty > 0 else 0.0,
155
+ currency=str(account.get("currency", self.currency)),
156
+ market="paper",
157
+ ))
158
+ return out
159
+
160
+ def orders(self, status: str = "all", limit: int = 50) -> List[Order]:
161
+ raw_orders = list(self._account().get("orders") or [])
162
+ raw_orders = list(reversed(raw_orders))[: max(0, int(limit or 50))]
163
+ out: List[Order] = []
164
+ for raw in raw_orders:
165
+ mapped = str(raw.get("status", "filled"))
166
+ if status != "all" and mapped != status:
167
+ continue
168
+ out.append(Order(
169
+ order_id=str(raw.get("order_id", "")),
170
+ symbol=str(raw.get("symbol", "")),
171
+ name=str(raw.get("symbol", "")),
172
+ side=str(raw.get("side", "")),
173
+ order_type=str(raw.get("order_type", "")),
174
+ quantity=float(raw.get("quantity", 0.0) or 0.0),
175
+ filled_qty=float(raw.get("filled_qty", raw.get("quantity", 0.0)) or 0.0),
176
+ price=float(raw.get("price", 0.0) or 0.0),
177
+ avg_price=float(raw.get("avg_price", raw.get("price", 0.0)) or 0.0),
178
+ status=mapped,
179
+ created_at=str(raw.get("created_at", "")),
180
+ currency=str(raw.get("currency", self.currency)),
181
+ ))
182
+ return out
183
+
184
+ def place_order(
185
+ self,
186
+ symbol: str,
187
+ side: str,
188
+ quantity: float,
189
+ order_type: str = "limit",
190
+ price: float = 0.0,
191
+ **kwargs: Any,
192
+ ) -> OrderResult:
193
+ symbol = str(symbol or "").strip().upper()
194
+ side = str(side or "").lower()
195
+ qty = float(quantity or 0.0)
196
+ if not symbol:
197
+ return OrderResult(False, message="symbol is required", broker_id=self.broker_id)
198
+ if side not in ("buy", "sell"):
199
+ return OrderResult(False, message="side must be buy or sell", broker_id=self.broker_id)
200
+ if qty <= 0:
201
+ return OrderResult(False, message="quantity must be positive", broker_id=self.broker_id)
202
+
203
+ account = self._account()
204
+ positions = account.setdefault("positions", {})
205
+ pos = dict(positions.get(symbol) or {})
206
+ current_price = float(price or pos.get("current_price", pos.get("cost_price", 0.0)) or 0.0)
207
+ if current_price <= 0:
208
+ return OrderResult(False, message="paper order requires a positive price", broker_id=self.broker_id)
209
+
210
+ cash = float(account.get("cash", 0.0) or 0.0)
211
+ notional = qty * current_price
212
+ existing_qty = float(pos.get("quantity", 0.0) or 0.0)
213
+ existing_cost = float(pos.get("cost_price", current_price) or current_price)
214
+
215
+ if side == "buy":
216
+ if notional > cash:
217
+ return OrderResult(False, message="paper cash insufficient", broker_id=self.broker_id)
218
+ new_qty = existing_qty + qty
219
+ new_cost = ((existing_qty * existing_cost) + notional) / new_qty if new_qty > 0 else current_price
220
+ pos.update({"quantity": new_qty, "cost_price": new_cost, "current_price": current_price})
221
+ account["cash"] = cash - notional
222
+ positions[symbol] = pos
223
+ else:
224
+ if qty > existing_qty:
225
+ return OrderResult(False, message="paper position insufficient", broker_id=self.broker_id)
226
+ new_qty = existing_qty - qty
227
+ account["cash"] = cash + notional
228
+ if new_qty <= 0:
229
+ positions.pop(symbol, None)
230
+ else:
231
+ pos.update({"quantity": new_qty, "cost_price": existing_cost, "current_price": current_price})
232
+ positions[symbol] = pos
233
+
234
+ order_id = _now_id("paper")
235
+ account.setdefault("orders", []).append({
236
+ "order_id": order_id,
237
+ "symbol": symbol,
238
+ "side": side,
239
+ "order_type": order_type,
240
+ "quantity": qty,
241
+ "filled_qty": qty,
242
+ "price": current_price,
243
+ "avg_price": current_price,
244
+ "status": "filled",
245
+ "created_at": int(time.time()),
246
+ "currency": str(account.get("currency", self.currency)),
247
+ })
248
+ self._save_account(account)
249
+ return OrderResult(True, order_id=order_id, message="paper order filled", broker_id=self.broker_id)
250
+
251
+
252
+ def reset_paper_account(broker_id: str = "paper_main", starting_cash: float = 100000.0, currency: str = "USD") -> None:
253
+ PaperBroker(broker_id, {
254
+ "id": broker_id,
255
+ "type": "paper",
256
+ "label": "Aria 仿盘账户",
257
+ "starting_cash": starting_cash,
258
+ "currency": currency,
259
+ }).reset(starting_cash=starting_cash, currency=currency)
brokers/planning.py ADDED
@@ -0,0 +1,296 @@
1
+ """Broker-aware portfolio snapshot, order planning, and risk gates.
2
+
3
+ This module intentionally does not place orders. It converts strategy output or
4
+ user order intent into an auditable plan that a human must approve first.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Dict, Iterable, List, Optional
12
+
13
+ from .base import AccountInfo, Position
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class PortfolioSnapshot:
18
+ broker_id: str
19
+ broker_label: str
20
+ currency: str
21
+ total_assets: float
22
+ cash: float
23
+ market_value: float
24
+ positions: List[Position] = field(default_factory=list)
25
+
26
+ def position_for(self, symbol: str) -> Optional[Position]:
27
+ sym = (symbol or "").upper()
28
+ for pos in self.positions:
29
+ if (pos.symbol or "").upper() == sym:
30
+ return pos
31
+ return None
32
+
33
+ def current_weight(self, symbol: str) -> float:
34
+ if self.total_assets <= 0:
35
+ return 0.0
36
+ pos = self.position_for(symbol)
37
+ return float(pos.market_value / self.total_assets) if pos else 0.0
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class StrategyIntent:
42
+ symbol: str
43
+ action: str = "hold" # buy | sell | hold | rebalance
44
+ target_weight: Optional[float] = None
45
+ confidence: Optional[float] = None
46
+ reason: str = ""
47
+ source: str = "manual"
48
+ metadata: Dict[str, Any] = field(default_factory=dict)
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class RiskRuleSet:
53
+ max_single_position_weight: float = 0.20
54
+ min_cash_reserve_weight: float = 0.02
55
+ max_order_value_weight: float = 0.10
56
+ allow_short: bool = False
57
+ allow_fractional: bool = False
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class PlannedOrder:
62
+ symbol: str
63
+ side: str
64
+ quantity: float
65
+ order_type: str
66
+ price: float
67
+ estimated_value: float
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class OrderPlan:
72
+ symbol: str
73
+ action: str
74
+ current_weight: float
75
+ target_weight: float
76
+ current_quantity: float
77
+ estimated_price: float
78
+ estimated_order: Optional[PlannedOrder]
79
+ cash_before: float
80
+ cash_after: float
81
+ requires_approval: bool
82
+ reason: str = ""
83
+ source: str = "manual"
84
+ risk: Dict[str, Any] = field(default_factory=dict)
85
+
86
+ def to_dict(self) -> Dict[str, Any]:
87
+ return {
88
+ "symbol": self.symbol,
89
+ "action": self.action,
90
+ "current_weight": round(self.current_weight, 6),
91
+ "target_weight": round(self.target_weight, 6),
92
+ "current_quantity": self.current_quantity,
93
+ "estimated_price": self.estimated_price,
94
+ "estimated_order": self.estimated_order.__dict__ if self.estimated_order else None,
95
+ "cash_before": round(self.cash_before, 4),
96
+ "cash_after": round(self.cash_after, 4),
97
+ "requires_approval": self.requires_approval,
98
+ "reason": self.reason,
99
+ "source": self.source,
100
+ "risk": self.risk,
101
+ }
102
+
103
+
104
+ def _as_float(value: Any, default: float = 0.0) -> float:
105
+ try:
106
+ out = float(value)
107
+ return out if math.isfinite(out) else default
108
+ except Exception:
109
+ return default
110
+
111
+
112
+ def snapshot_from_broker(broker: Any) -> PortfolioSnapshot:
113
+ account: AccountInfo = broker.account_info()
114
+ positions = list(broker.positions() or [])
115
+ return PortfolioSnapshot(
116
+ broker_id=getattr(broker, "broker_id", ""),
117
+ broker_label=getattr(broker, "label", getattr(broker, "broker_id", "")),
118
+ currency=account.currency,
119
+ total_assets=_as_float(account.total_assets),
120
+ cash=_as_float(account.cash),
121
+ market_value=_as_float(account.market_value),
122
+ positions=positions,
123
+ )
124
+
125
+
126
+ def infer_intent_from_backtest(result: Dict[str, Any], target_weight: Optional[float] = None) -> StrategyIntent:
127
+ total_return = _as_float(result.get("total_return"))
128
+ alpha = _as_float(result.get("alpha"))
129
+ max_dd = _as_float(result.get("max_drawdown"))
130
+ symbol = str(result.get("symbol", "")).upper()
131
+ if target_weight is None:
132
+ if total_return > 0 and alpha >= 0:
133
+ target_weight = 0.10
134
+ elif total_return > 0:
135
+ target_weight = 0.05
136
+ else:
137
+ target_weight = 0.0
138
+ action = "rebalance" if target_weight > 0 else "sell"
139
+ confidence = max(0.0, min(1.0, 0.5 + alpha - abs(max_dd) * 0.25))
140
+ return StrategyIntent(
141
+ symbol=symbol,
142
+ action=action,
143
+ target_weight=target_weight,
144
+ confidence=round(confidence, 4),
145
+ reason=f"backtest total={total_return:.2%}, alpha={alpha:.2%}, max_dd={max_dd:.2%}",
146
+ source="backtest",
147
+ metadata={
148
+ "strategy": result.get("strategy"),
149
+ "report_path": result.get("report_path"),
150
+ "total_return": result.get("total_return"),
151
+ "alpha": result.get("alpha"),
152
+ "max_drawdown": result.get("max_drawdown"),
153
+ },
154
+ )
155
+
156
+
157
+ def _resolve_price(symbol: str, explicit_price: Optional[float], snapshot: PortfolioSnapshot) -> float:
158
+ price = _as_float(explicit_price)
159
+ if price > 0:
160
+ return price
161
+ pos = snapshot.position_for(symbol)
162
+ return _as_float(pos.current_price if pos else 0.0)
163
+
164
+
165
+ def plan_order(
166
+ snapshot: PortfolioSnapshot,
167
+ intent: StrategyIntent,
168
+ price: Optional[float] = None,
169
+ quantity: Optional[float] = None,
170
+ order_type: str = "limit",
171
+ rules: Optional[RiskRuleSet] = None,
172
+ ) -> OrderPlan:
173
+ rules = rules or RiskRuleSet()
174
+ symbol = intent.symbol.upper()
175
+ current_pos = snapshot.position_for(symbol)
176
+ current_qty = _as_float(current_pos.quantity if current_pos else 0.0)
177
+ resolved_price = _resolve_price(symbol, price, snapshot)
178
+ current_weight = snapshot.current_weight(symbol)
179
+ target_weight = current_weight
180
+ if intent.target_weight is not None:
181
+ target_weight = max(0.0 if not rules.allow_short else -1.0, _as_float(intent.target_weight))
182
+
183
+ if quantity is not None:
184
+ qty = _as_float(quantity)
185
+ side = "buy" if intent.action in ("buy", "rebalance") else "sell"
186
+ else:
187
+ if resolved_price <= 0 or snapshot.total_assets <= 0:
188
+ qty = 0.0
189
+ side = "hold"
190
+ else:
191
+ target_value = snapshot.total_assets * target_weight
192
+ current_value = snapshot.total_assets * current_weight
193
+ delta_value = target_value - current_value
194
+ side = "buy" if delta_value > 0 else "sell" if delta_value < 0 else "hold"
195
+ qty = abs(delta_value) / resolved_price
196
+
197
+ if not rules.allow_fractional:
198
+ qty = math.floor(qty)
199
+ if side == "sell":
200
+ qty = min(qty, max(current_qty, 0.0))
201
+
202
+ estimated_value = max(qty, 0.0) * max(resolved_price, 0.0)
203
+ order = None
204
+ cash_after = snapshot.cash
205
+ if side in ("buy", "sell") and qty > 0 and resolved_price > 0:
206
+ order = PlannedOrder(
207
+ symbol=symbol,
208
+ side=side,
209
+ quantity=qty,
210
+ order_type=order_type,
211
+ price=resolved_price,
212
+ estimated_value=estimated_value,
213
+ )
214
+ cash_after = snapshot.cash - estimated_value if side == "buy" else snapshot.cash + estimated_value
215
+
216
+ plan = OrderPlan(
217
+ symbol=symbol,
218
+ action=intent.action,
219
+ current_weight=current_weight,
220
+ target_weight=target_weight,
221
+ current_quantity=current_qty,
222
+ estimated_price=resolved_price,
223
+ estimated_order=order,
224
+ cash_before=snapshot.cash,
225
+ cash_after=cash_after,
226
+ requires_approval=order is not None,
227
+ reason=intent.reason,
228
+ source=intent.source,
229
+ )
230
+ risk = evaluate_risk(plan, snapshot, rules)
231
+ return OrderPlan(**{**plan.__dict__, "risk": risk})
232
+
233
+
234
+ def evaluate_risk(plan: OrderPlan, snapshot: PortfolioSnapshot, rules: Optional[RiskRuleSet] = None) -> Dict[str, Any]:
235
+ rules = rules or RiskRuleSet()
236
+ violations: List[str] = []
237
+ warnings: List[str] = []
238
+ order = plan.estimated_order
239
+ total_assets = max(snapshot.total_assets, 0.0)
240
+ if total_assets <= 0:
241
+ violations.append("账户总资产不可用,无法评估仓位")
242
+ if plan.target_weight > rules.max_single_position_weight:
243
+ violations.append(f"目标单票仓位 {plan.target_weight:.1%} 超过上限 {rules.max_single_position_weight:.1%}")
244
+ if order and order.side == "buy":
245
+ if order.estimated_value > snapshot.cash:
246
+ violations.append("可用现金不足")
247
+ reserve = total_assets * rules.min_cash_reserve_weight
248
+ if plan.cash_after < reserve:
249
+ warnings.append(f"交易后现金低于保留比例 {rules.min_cash_reserve_weight:.1%}")
250
+ projected_position_value = 0.0
251
+ projected_position_weight = 0.0
252
+ if order and total_assets > 0:
253
+ current_value = total_assets * plan.current_weight
254
+ if order.side == "buy":
255
+ projected_position_value = current_value + order.estimated_value
256
+ else:
257
+ projected_position_value = max(0.0, current_value - order.estimated_value)
258
+ projected_position_weight = projected_position_value / total_assets
259
+ if projected_position_weight > rules.max_single_position_weight:
260
+ violations.append(
261
+ f"成交后单票仓位 {projected_position_weight:.1%} 超过上限 {rules.max_single_position_weight:.1%}"
262
+ )
263
+ if order and total_assets > 0 and order.estimated_value / total_assets > rules.max_order_value_weight:
264
+ warnings.append(f"单笔订单金额超过账户 {rules.max_order_value_weight:.1%}")
265
+ if order and order.side == "sell" and order.quantity > plan.current_quantity and not rules.allow_short:
266
+ violations.append("卖出数量超过当前持仓,且未允许做空")
267
+ return {
268
+ "passed": not violations,
269
+ "requires_manual_review": bool(warnings or violations),
270
+ "violations": violations,
271
+ "warnings": warnings,
272
+ "rules": {
273
+ "max_single_position_weight": rules.max_single_position_weight,
274
+ "min_cash_reserve_weight": rules.min_cash_reserve_weight,
275
+ "max_order_value_weight": rules.max_order_value_weight,
276
+ "allow_short": rules.allow_short,
277
+ "allow_fractional": rules.allow_fractional,
278
+ },
279
+ "projected_position_weight": round(projected_position_weight, 6),
280
+ }
281
+
282
+
283
+ def plans_from_strategy_results(
284
+ snapshot: PortfolioSnapshot,
285
+ results: Iterable[Dict[str, Any]],
286
+ rules: Optional[RiskRuleSet] = None,
287
+ ) -> List[OrderPlan]:
288
+ plans: List[OrderPlan] = []
289
+ for result in results:
290
+ intent = infer_intent_from_backtest(result)
291
+ last_price = None
292
+ curve = result.get("equity_curve") or []
293
+ if curve and isinstance(curve[-1], dict):
294
+ last_price = curve[-1].get("close")
295
+ plans.append(plan_order(snapshot, intent, price=last_price, rules=rules))
296
+ return plans