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
brokers/registry.py ADDED
@@ -0,0 +1,181 @@
1
+ """
2
+ brokers/registry.py — 券商注册表 & 连接管理器
3
+ ================================================
4
+ 统一管理所有已连接的券商实例。
5
+ 支持多账户并发持有(如同时连接 IBKR + 富途)。
6
+
7
+ 用法::
8
+
9
+ from brokers.registry import BrokerRegistry
10
+
11
+ reg = BrokerRegistry()
12
+ broker = reg.connect("xt_main") # 从 brokers.json 读取并连接
13
+ acct = broker.account_info()
14
+ pos = broker.positions()
15
+
16
+ # 切换默认账户
17
+ reg.set_active("ibkr_us")
18
+ broker = reg.active()
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+ from typing import Dict, List, Optional, Type
25
+
26
+ from .base import BrokerBase
27
+ from .config import (
28
+ get_broker_config, get_default_broker_config,
29
+ list_broker_configs, set_default_broker,
30
+ )
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ # ── 适配器注册表 ───────────────────────────────────────────────────────────────
35
+
36
+ _BROKER_CLASSES: Dict[str, Type[BrokerBase]] = {}
37
+
38
+
39
+ def _register_all() -> None:
40
+ """延迟注册所有内置适配器(避免 import 循环)。"""
41
+ global _BROKER_CLASSES
42
+ if _BROKER_CLASSES:
43
+ return
44
+ _map: Dict[str, tuple] = {
45
+ "paper": ("brokers.paper_broker", "PaperBroker"),
46
+ "xtquant": ("brokers.cn.xtquant_broker", "XTQuantBroker"),
47
+ "easytrader": ("brokers.cn.easytrader_broker", "EasyTraderBroker"),
48
+ "futu": ("brokers.cn.futu_broker", "FutuBroker"),
49
+ "tiger": ("brokers.cn.tiger_broker", "TigerBroker"),
50
+ "longbridge": ("brokers.cn.longbridge_broker", "LongbridgeBroker"),
51
+ "ibkr": ("brokers.intl.ibkr_broker", "IBKRBroker"),
52
+ "alpaca": ("brokers.intl.alpaca_broker", "AlpacaBroker"),
53
+ "webull": ("brokers.intl.webull_broker", "WebullBroker"),
54
+ }
55
+ for btype, (module_path, class_name) in _map.items():
56
+ try:
57
+ import importlib
58
+ mod = importlib.import_module(module_path)
59
+ _BROKER_CLASSES[btype] = getattr(mod, class_name)
60
+ except Exception as e:
61
+ logger.debug("Broker class load failed for %s: %s", btype, e)
62
+
63
+
64
+ def get_broker_class(broker_type: str) -> Optional[Type[BrokerBase]]:
65
+ _register_all()
66
+ return _BROKER_CLASSES.get(broker_type)
67
+
68
+
69
+ # ── 连接管理器 ────────────────────────────────────────────────────────────────
70
+
71
+ class BrokerRegistry:
72
+ """全局券商连接池,单例使用。"""
73
+
74
+ def __init__(self):
75
+ self._instances: Dict[str, BrokerBase] = {} # broker_id → instance
76
+ self._active_id: Optional[str] = None
77
+
78
+ # ── 连接 ──────────────────────────────────────────────────────────────────
79
+
80
+ def connect(self, broker_id: str) -> BrokerBase:
81
+ """连接指定 id 的券商(如已连接则直接返回)。"""
82
+ if broker_id in self._instances and self._instances[broker_id].is_connected:
83
+ return self._instances[broker_id]
84
+
85
+ cfg = get_broker_config(broker_id)
86
+ if not cfg:
87
+ raise ValueError(f"未找到券商配置: {broker_id!r} (请在 ~/.arthera/brokers.json 添加)")
88
+
89
+ broker_type = cfg.get("type", "")
90
+ cls = get_broker_class(broker_type)
91
+ if not cls:
92
+ raise ValueError(
93
+ f"不支持的券商类型: {broker_type!r}\n"
94
+ f"支持的类型: {', '.join(_BROKER_CLASSES)}"
95
+ )
96
+
97
+ instance = cls(broker_id=broker_id, config=cfg)
98
+ instance.connect()
99
+ self._instances[broker_id] = instance
100
+
101
+ if self._active_id is None:
102
+ self._active_id = broker_id
103
+
104
+ logger.info("✓ 已连接券商: %s (%s)", instance.label, broker_type)
105
+ return instance
106
+
107
+ def connect_default(self) -> Optional[BrokerBase]:
108
+ """连接 brokers.json 中标记为 default 的券商。"""
109
+ cfg = get_default_broker_config()
110
+ if not cfg:
111
+ return None
112
+ return self.connect(cfg["id"])
113
+
114
+ def connect_all(self) -> List[BrokerBase]:
115
+ """尝试连接所有已配置的券商,跳过连接失败的。"""
116
+ connected = []
117
+ for cfg in list_broker_configs():
118
+ try:
119
+ b = self.connect(cfg["id"])
120
+ connected.append(b)
121
+ except Exception as e:
122
+ logger.warning("连接券商 %s 失败: %s", cfg.get("id"), e)
123
+ return connected
124
+
125
+ # ── 查询 ──────────────────────────────────────────────────────────────────
126
+
127
+ def active(self) -> Optional[BrokerBase]:
128
+ """返回当前活跃的券商实例。"""
129
+ if not self._active_id:
130
+ return None
131
+ return self._instances.get(self._active_id)
132
+
133
+ def get(self, broker_id: str) -> Optional[BrokerBase]:
134
+ """按 id 获取已连接的实例。"""
135
+ return self._instances.get(broker_id)
136
+
137
+ def list_connected(self) -> List[BrokerBase]:
138
+ """返回所有已连接的券商。"""
139
+ return [b for b in self._instances.values() if b.is_connected]
140
+
141
+ def set_active(self, broker_id: str) -> bool:
142
+ """设置当前活跃账户。"""
143
+ if broker_id not in self._instances:
144
+ return False
145
+ self._active_id = broker_id
146
+ set_default_broker(broker_id)
147
+ return True
148
+
149
+ # ── 断开 ──────────────────────────────────────────────────────────────────
150
+
151
+ def disconnect(self, broker_id: str) -> None:
152
+ b = self._instances.pop(broker_id, None)
153
+ if b:
154
+ b.disconnect()
155
+ if self._active_id == broker_id:
156
+ remaining = list(self._instances)
157
+ self._active_id = remaining[0] if remaining else None
158
+
159
+ def disconnect_all(self) -> None:
160
+ for b in list(self._instances.values()):
161
+ try:
162
+ b.disconnect()
163
+ except Exception:
164
+ pass
165
+ self._instances.clear()
166
+ self._active_id = None
167
+
168
+ def __repr__(self) -> str:
169
+ ids = list(self._instances)
170
+ return f"<BrokerRegistry active={self._active_id!r} connected={ids}>"
171
+
172
+
173
+ # 全局单例(在 aria_cli.py 中 import 后使用)
174
+ _global_registry: Optional[BrokerRegistry] = None
175
+
176
+
177
+ def get_registry() -> BrokerRegistry:
178
+ global _global_registry
179
+ if _global_registry is None:
180
+ _global_registry = BrokerRegistry()
181
+ return _global_registry
brokers/trading.py ADDED
@@ -0,0 +1,237 @@
1
+ """Trading service layer for paper/live execution.
2
+
3
+ All order execution flows through a preview id. Live trading is denied unless
4
+ the broker config explicitly enables ``allow_live_trade``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import time
11
+ import uuid
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import Any, Dict, Optional
15
+
16
+ from .config import BROKERS_CONFIG_PATH
17
+ from .planning import RiskRuleSet, StrategyIntent, plan_order, snapshot_from_broker
18
+
19
+
20
+ TRADE_PREVIEWS_PATH = BROKERS_CONFIG_PATH.parent / "trade_previews.json"
21
+ TRADE_AUDIT_PATH = BROKERS_CONFIG_PATH.parent / "trade_audit.jsonl"
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class TradingPolicy:
26
+ mode: str = "read_only" # read_only | paper | live
27
+ allow_live_trade: bool = False
28
+ require_confirm: bool = True
29
+ max_single_position_weight: float = 0.20
30
+ min_cash_reserve_weight: float = 0.02
31
+ max_order_value_weight: float = 0.10
32
+ allow_short: bool = False
33
+ allow_fractional: bool = False
34
+
35
+ def rules(self) -> RiskRuleSet:
36
+ return RiskRuleSet(
37
+ max_single_position_weight=self.max_single_position_weight,
38
+ min_cash_reserve_weight=self.min_cash_reserve_weight,
39
+ max_order_value_weight=self.max_order_value_weight,
40
+ allow_short=self.allow_short,
41
+ allow_fractional=self.allow_fractional,
42
+ )
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class OrderIntent:
47
+ symbol: str
48
+ side: str
49
+ quantity: Optional[float] = None
50
+ order_type: str = "limit"
51
+ price: Optional[float] = None
52
+ source: str = "manual"
53
+ target_weight: Optional[float] = None
54
+ user_message: str = ""
55
+ metadata: Dict[str, Any] = field(default_factory=dict)
56
+
57
+
58
+ def resolve_trading_mode(config: Dict[str, Any], broker_type: str = "") -> str:
59
+ explicit = str(config.get("mode", "") or "").lower()
60
+ if explicit in {"read_only", "paper", "live"}:
61
+ return explicit
62
+ if broker_type == "paper" or config.get("paper") is True:
63
+ return "paper"
64
+ return "read_only"
65
+
66
+
67
+ def policy_from_config(config: Dict[str, Any], broker_type: str = "") -> TradingPolicy:
68
+ mode = resolve_trading_mode(config, broker_type=broker_type)
69
+ return TradingPolicy(
70
+ mode=mode,
71
+ allow_live_trade=bool(config.get("allow_live_trade", False)),
72
+ require_confirm=bool(config.get("require_confirm", True)),
73
+ max_single_position_weight=float(config.get("max_single_position_weight", 0.20) or 0.20),
74
+ min_cash_reserve_weight=float(config.get("min_cash_reserve_weight", 0.02) or 0.02),
75
+ max_order_value_weight=float(config.get("max_order_value_weight", 0.10) or 0.10),
76
+ allow_short=bool(config.get("allow_short", False)),
77
+ allow_fractional=bool(config.get("allow_fractional", False)),
78
+ )
79
+
80
+
81
+ def _load_previews() -> Dict[str, Any]:
82
+ if not TRADE_PREVIEWS_PATH.exists():
83
+ return {"previews": {}}
84
+ try:
85
+ data = json.loads(TRADE_PREVIEWS_PATH.read_text(encoding="utf-8"))
86
+ if isinstance(data, dict):
87
+ data.setdefault("previews", {})
88
+ return data
89
+ except Exception:
90
+ pass
91
+ return {"previews": {}}
92
+
93
+
94
+ def _save_previews(data: Dict[str, Any]) -> None:
95
+ TRADE_PREVIEWS_PATH.parent.mkdir(parents=True, exist_ok=True)
96
+ TRADE_PREVIEWS_PATH.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
97
+
98
+
99
+ def _audit(event: Dict[str, Any]) -> None:
100
+ TRADE_AUDIT_PATH.parent.mkdir(parents=True, exist_ok=True)
101
+ row = {"ts": int(time.time()), **event}
102
+ with TRADE_AUDIT_PATH.open("a", encoding="utf-8") as handle:
103
+ handle.write(json.dumps(row, ensure_ascii=False) + "\n")
104
+
105
+
106
+ def _execution_blockers(policy: TradingPolicy, plan: Dict[str, Any]) -> list[str]:
107
+ blockers: list[str] = []
108
+ risk = plan.get("risk") or {}
109
+ blockers.extend(str(item) for item in risk.get("violations") or [])
110
+ if plan.get("action") in {"buy", "sell", "rebalance"} and not plan.get("estimated_order"):
111
+ blockers.append("订单计划没有可执行订单,通常是缺少价格、数量或持仓")
112
+ if policy.mode == "read_only":
113
+ blockers.append("账户处于 read_only 模式,不能执行订单")
114
+ if policy.mode == "live" and not policy.allow_live_trade:
115
+ blockers.append("实盘账户未设置 allow_live_trade=true")
116
+ return blockers
117
+
118
+
119
+ def build_order_preview(broker: Any, intent: OrderIntent) -> Dict[str, Any]:
120
+ policy = policy_from_config(getattr(broker, "config", {}) or {}, getattr(broker, "broker_type", ""))
121
+ snapshot = snapshot_from_broker(broker)
122
+ strategy_intent = StrategyIntent(
123
+ symbol=intent.symbol,
124
+ action=intent.side,
125
+ target_weight=intent.target_weight,
126
+ reason=intent.user_message,
127
+ source=intent.source,
128
+ metadata=dict(intent.metadata),
129
+ )
130
+ planned = plan_order(
131
+ snapshot,
132
+ strategy_intent,
133
+ price=float(intent.price) if intent.price is not None else None,
134
+ quantity=float(intent.quantity) if intent.quantity is not None else None,
135
+ order_type=intent.order_type,
136
+ rules=policy.rules(),
137
+ )
138
+ plan_dict = planned.to_dict()
139
+ preview_id = "tp_" + uuid.uuid4().hex[:12]
140
+ blockers = _execution_blockers(policy, plan_dict)
141
+ preview = {
142
+ "preview_id": preview_id,
143
+ "created_at": int(time.time()),
144
+ "status": "pending",
145
+ "broker_id": getattr(broker, "broker_id", ""),
146
+ "broker_label": getattr(broker, "label", ""),
147
+ "broker_type": getattr(broker, "broker_type", ""),
148
+ "mode": policy.mode,
149
+ "allow_live_trade": policy.allow_live_trade,
150
+ "require_confirm": policy.require_confirm,
151
+ "intent": {
152
+ "symbol": intent.symbol.upper(),
153
+ "side": intent.side.lower(),
154
+ "quantity": float(intent.quantity) if intent.quantity is not None else None,
155
+ "order_type": intent.order_type.lower(),
156
+ "price": intent.price,
157
+ "source": intent.source,
158
+ },
159
+ "order_plan": plan_dict,
160
+ "execution_blockers": blockers,
161
+ "can_execute": not blockers,
162
+ "audit_path": str(TRADE_AUDIT_PATH),
163
+ }
164
+ store = _load_previews()
165
+ store.setdefault("previews", {})[preview_id] = preview
166
+ _save_previews(store)
167
+ _audit({"event": "trade_preview", "preview": preview})
168
+ return preview
169
+
170
+
171
+ def load_order_preview(preview_id: str) -> Dict[str, Any] | None:
172
+ return (_load_previews().get("previews") or {}).get(preview_id)
173
+
174
+
175
+ def list_order_previews(limit: int = 10) -> list[Dict[str, Any]]:
176
+ rows = list((_load_previews().get("previews") or {}).values())
177
+ rows.sort(key=lambda row: int(row.get("created_at", 0)), reverse=True)
178
+ return rows[: max(0, int(limit or 10))]
179
+
180
+
181
+ def mark_preview_status(preview_id: str, status: str, extra: Optional[Dict[str, Any]] = None) -> None:
182
+ store = _load_previews()
183
+ preview = (store.get("previews") or {}).get(preview_id)
184
+ if not preview:
185
+ return
186
+ preview["status"] = status
187
+ preview["updated_at"] = int(time.time())
188
+ if extra:
189
+ preview.update(extra)
190
+ _save_previews(store)
191
+
192
+
193
+ def execute_order_preview(broker: Any, preview_id: str, *, confirmed: bool = False) -> Dict[str, Any]:
194
+ preview = load_order_preview(preview_id)
195
+ if not preview:
196
+ return {"success": False, "error": f"preview not found: {preview_id}"}
197
+ if not confirmed:
198
+ return {"success": False, "confirmation_required": True, "preview": preview}
199
+ if preview.get("broker_id") != getattr(broker, "broker_id", ""):
200
+ return {"success": False, "error": "preview broker does not match active broker", "preview": preview}
201
+
202
+ policy = policy_from_config(getattr(broker, "config", {}) or {}, getattr(broker, "broker_type", ""))
203
+ blockers = _execution_blockers(policy, preview.get("order_plan") or {})
204
+ if blockers:
205
+ mark_preview_status(preview_id, "rejected", {"execution_blockers": blockers})
206
+ _audit({"event": "trade_rejected", "preview_id": preview_id, "blockers": blockers})
207
+ return {"success": False, "risk_rejected": True, "execution_blockers": blockers, "preview": preview}
208
+
209
+ intent = preview.get("intent") or {}
210
+ planned_order = ((preview.get("order_plan") or {}).get("estimated_order") or {})
211
+ if not planned_order:
212
+ return {"success": False, "error": "preview has no executable order", "preview": preview}
213
+ result = broker.place_order(
214
+ symbol=str(planned_order.get("symbol") or intent.get("symbol", "")),
215
+ side=str(planned_order.get("side") or intent.get("side", "")),
216
+ quantity=float(planned_order.get("quantity", intent.get("quantity", 0.0)) or 0.0),
217
+ order_type=str(planned_order.get("order_type") or intent.get("order_type", "limit")),
218
+ price=float(planned_order.get("price", intent.get("price", 0.0)) or 0.0),
219
+ )
220
+ payload = {
221
+ "success": bool(getattr(result, "success", False)),
222
+ "order_id": getattr(result, "order_id", ""),
223
+ "message": getattr(result, "message", ""),
224
+ "broker": getattr(broker, "label", ""),
225
+ "broker_id": getattr(broker, "broker_id", ""),
226
+ "mode": policy.mode,
227
+ "preview_id": preview_id,
228
+ "symbol": str(planned_order.get("symbol") or intent.get("symbol", "")),
229
+ "side": str(planned_order.get("side") or intent.get("side", "")),
230
+ "qty": float(planned_order.get("quantity", intent.get("quantity", 0.0)) or 0.0),
231
+ "order_plan": preview.get("order_plan"),
232
+ }
233
+ mark_preview_status(preview_id, "executed" if payload["success"] else "failed", {
234
+ "result": payload,
235
+ })
236
+ _audit({"event": "trade_execute", "preview_id": preview_id, "result": payload})
237
+ return payload
change_store.py ADDED
@@ -0,0 +1,127 @@
1
+ """Staged file-change store for Aria Code.
2
+
3
+ The CLI still supports direct writes for existing workflows, but every write can
4
+ now be represented as a hash-checked change first. This gives us Codex/Claude
5
+ Code style review/apply/reject primitives without coupling the logic to the REPL.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import difflib
11
+ import hashlib
12
+ import os
13
+ import pathlib
14
+ import time
15
+ import uuid
16
+ from dataclasses import asdict, dataclass
17
+ from typing import Dict, List, Optional
18
+
19
+
20
+ class ChangeConflictError(RuntimeError):
21
+ """Raised when the file changed after a staged change was created."""
22
+
23
+
24
+ def sha256_text(text: str) -> str:
25
+ return hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class StagedChange:
30
+ change_id: str
31
+ path: str
32
+ before_content: str
33
+ after_content: str
34
+ before_hash: str
35
+ after_hash: str
36
+ diff: str
37
+ created_at: float
38
+ source: str = "aria-code"
39
+ applied: bool = False
40
+ rejected: bool = False
41
+
42
+ def to_dict(self) -> dict:
43
+ return asdict(self)
44
+
45
+
46
+ class ChangeStore:
47
+ """In-memory staged change store with conflict-aware apply."""
48
+
49
+ def __init__(self) -> None:
50
+ self._changes: Dict[str, StagedChange] = {}
51
+
52
+ def stage(self, path: str | pathlib.Path, after_content: str, source: str = "aria-code") -> StagedChange:
53
+ target = pathlib.Path(path).expanduser().resolve()
54
+ before = target.read_text(encoding="utf-8", errors="replace") if target.is_file() else ""
55
+ rel = str(target)
56
+ diff = "".join(difflib.unified_diff(
57
+ before.splitlines(keepends=True),
58
+ after_content.splitlines(keepends=True),
59
+ fromfile=f"a/{rel}",
60
+ tofile=f"b/{rel}",
61
+ ))
62
+ change = StagedChange(
63
+ change_id=uuid.uuid4().hex[:12],
64
+ path=rel,
65
+ before_content=before,
66
+ after_content=after_content,
67
+ before_hash=sha256_text(before),
68
+ after_hash=sha256_text(after_content),
69
+ diff=diff,
70
+ created_at=time.time(),
71
+ source=source,
72
+ )
73
+ self._changes[change.change_id] = change
74
+ return change
75
+
76
+ def list(self, include_closed: bool = False) -> List[StagedChange]:
77
+ changes = list(self._changes.values())
78
+ if not include_closed:
79
+ changes = [c for c in changes if not c.applied and not c.rejected]
80
+ return sorted(changes, key=lambda c: c.created_at)
81
+
82
+ def get(self, change_id: str) -> Optional[StagedChange]:
83
+ return self._changes.get(change_id)
84
+
85
+ def apply(self, change_id: str) -> StagedChange:
86
+ change = self._require_open(change_id)
87
+ target = pathlib.Path(change.path)
88
+ current = target.read_text(encoding="utf-8", errors="replace") if target.is_file() else ""
89
+ if sha256_text(current) != change.before_hash:
90
+ raise ChangeConflictError(f"File changed since staging: {change.path}")
91
+ target.parent.mkdir(parents=True, exist_ok=True)
92
+ tmp = target.with_name(f".{target.name}.aria-tmp-{uuid.uuid4().hex[:8]}")
93
+ tmp.write_text(change.after_content, encoding="utf-8")
94
+ os.replace(tmp, target)
95
+ applied = StagedChange(**{**change.to_dict(), "applied": True})
96
+ self._changes[change.change_id] = applied
97
+ return applied
98
+
99
+ def reject(self, change_id: str) -> StagedChange:
100
+ change = self._require_open(change_id)
101
+ rejected = StagedChange(**{**change.to_dict(), "rejected": True})
102
+ self._changes[change.change_id] = rejected
103
+ return rejected
104
+
105
+ def clear_closed(self) -> int:
106
+ closed = [cid for cid, c in self._changes.items() if c.applied or c.rejected]
107
+ for cid in closed:
108
+ del self._changes[cid]
109
+ return len(closed)
110
+
111
+ def _require_open(self, change_id: str) -> StagedChange:
112
+ key = (change_id or "").strip()
113
+ change = self._changes.get(key)
114
+ if change is None:
115
+ matches = [c for cid, c in self._changes.items() if cid.startswith(key)]
116
+ if len(matches) == 1:
117
+ change = matches[0]
118
+ if change is None:
119
+ raise KeyError(f"Unknown change id: {change_id}")
120
+ if change.applied:
121
+ raise ValueError(f"Change already applied: {change.change_id}")
122
+ if change.rejected:
123
+ raise ValueError(f"Change already rejected: {change.change_id}")
124
+ return change
125
+
126
+
127
+ GLOBAL_CHANGE_STORE = ChangeStore()
command_safety.py ADDED
@@ -0,0 +1,19 @@
1
+ """Compatibility wrapper for Aria Code command safety APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from safety.permissions import (
6
+ SAFE_POLICIES,
7
+ PolicyDecision,
8
+ classify_command_risk,
9
+ evaluate_command_policy,
10
+ normalize_command,
11
+ )
12
+
13
+ __all__ = [
14
+ "SAFE_POLICIES",
15
+ "PolicyDecision",
16
+ "classify_command_risk",
17
+ "evaluate_command_policy",
18
+ "normalize_command",
19
+ ]