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.
- agents/__init__.py +32 -0
- agents/base.py +190 -0
- agents/deep/__init__.py +37 -0
- agents/deep/calibration_loop.py +144 -0
- agents/deep/critic.py +125 -0
- agents/deep/deepen.py +193 -0
- agents/deep/models.py +149 -0
- agents/deep/pipeline.py +164 -0
- agents/deep/quant_fusion.py +192 -0
- agents/deep/themes.py +95 -0
- agents/deep/tiers.py +106 -0
- agents/financial/__init__.py +10 -0
- agents/financial/catalyst.py +279 -0
- agents/financial/debate.py +145 -0
- agents/financial/earnings.py +303 -0
- agents/financial/fundamental.py +159 -0
- agents/financial/macro.py +99 -0
- agents/financial/news.py +207 -0
- agents/financial/risk.py +132 -0
- agents/financial/sector.py +279 -0
- agents/financial/synthesis.py +274 -0
- agents/financial/technical.py +258 -0
- agents/portfolio_agent.py +333 -0
- agents/realty/__init__.py +62 -0
- agents/realty/asset_diagnosis.py +150 -0
- agents/realty/business_match.py +165 -0
- agents/realty/cashflow_verify.py +208 -0
- agents/realty/contract_rules.py +209 -0
- agents/realty/energy_anomaly.py +188 -0
- agents/realty/exit_settlement.py +207 -0
- agents/realty/fulfillment_risk.py +205 -0
- agents/realty/ops_optimize.py +159 -0
- agents/realty/revenue_share.py +214 -0
- agents/registry.py +144 -0
- agents/sports/__init__.py +0 -0
- agents/sports/football_agent.py +169 -0
- agents/team.py +289 -0
- aliyun_data_client.py +660 -0
- apps/README.md +12 -0
- apps/__init__.py +2 -0
- apps/channels/README.md +15 -0
- apps/cli/README.md +13 -0
- apps/cli/__init__.py +2 -0
- apps/cli/bootstrap.py +99 -0
- apps/cli/codegen_paths.py +29 -0
- apps/cli/commands/__init__.py +16 -0
- apps/cli/commands/analysis_cmds.py +288 -0
- apps/cli/commands/backtest_cmds.py +1887 -0
- apps/cli/commands/broker_cmds.py +1154 -0
- apps/cli/commands/business_workflow_cmds.py +289 -0
- apps/cli/commands/catalog.py +84 -0
- apps/cli/commands/data_cmds.py +405 -0
- apps/cli/commands/diagnostic_cmds.py +179 -0
- apps/cli/commands/diagnostic_ops_cmds.py +696 -0
- apps/cli/commands/finance_render.py +12 -0
- apps/cli/commands/market.py +399 -0
- apps/cli/commands/market_cmds.py +1276 -0
- apps/cli/commands/market_context.py +425 -0
- apps/cli/commands/market_render.py +7 -0
- apps/cli/commands/model_cmds.py +1579 -0
- apps/cli/commands/ops_cmds.py +668 -0
- apps/cli/commands/portfolio_cmds.py +962 -0
- apps/cli/commands/report.py +377 -0
- apps/cli/commands/scaffold_templates.py +617 -0
- apps/cli/commands/session_cmds.py +179 -0
- apps/cli/commands/session_ux_cmds.py +280 -0
- apps/cli/commands/team.py +588 -0
- apps/cli/commands/team_render.py +8 -0
- apps/cli/commands/ui_cmds.py +358 -0
- apps/cli/commands/workflow_cmds.py +279 -0
- apps/cli/commands/workspace_cmds.py +1414 -0
- apps/cli/config_paths.py +70 -0
- apps/cli/config_store.py +61 -0
- apps/cli/deterministic.py +122 -0
- apps/cli/direct.py +48 -0
- apps/cli/github_app_auth.py +135 -0
- apps/cli/handlers/__init__.py +11 -0
- apps/cli/handlers/broker_handlers.py +122 -0
- apps/cli/handlers/chart_handlers.py +1309 -0
- apps/cli/handlers/market_handlers.py +2509 -0
- apps/cli/handlers/realty_handlers.py +114 -0
- apps/cli/handlers/strategy_advice.py +82 -0
- apps/cli/hooks.py +180 -0
- apps/cli/i18n.py +284 -0
- apps/cli/intent.py +136 -0
- apps/cli/intent_router.py +217 -0
- apps/cli/lifecycle_hooks.py +48 -0
- apps/cli/main.py +29 -0
- apps/cli/market_metadata.py +135 -0
- apps/cli/market_universe.py +265 -0
- apps/cli/message_processing.py +257 -0
- apps/cli/plan_mode.py +139 -0
- apps/cli/plotly_html.py +15 -0
- apps/cli/prediction_feedback.py +202 -0
- apps/cli/preflight.py +497 -0
- apps/cli/project_aria.py +60 -0
- apps/cli/prompts/__init__.py +0 -0
- apps/cli/prompts/coding.py +658 -0
- apps/cli/prompts/system_prompts.py +531 -0
- apps/cli/prompts/ui.py +434 -0
- apps/cli/providers/__init__.py +1 -0
- apps/cli/providers/base.py +271 -0
- apps/cli/providers/chat_routing.py +80 -0
- apps/cli/providers/llm/__init__.py +1 -0
- apps/cli/providers/llm/ollama_stream.py +1170 -0
- apps/cli/providers/llm/sse_stream.py +216 -0
- apps/cli/providers/runtime_bridge.py +185 -0
- apps/cli/runtime_consumer.py +489 -0
- apps/cli/session_export.py +87 -0
- apps/cli/session_jsonl.py +207 -0
- apps/cli/session_store.py +112 -0
- apps/cli/todo_tracker.py +190 -0
- apps/cli/tools/__init__.py +40 -0
- apps/cli/tools/context.py +46 -0
- apps/cli/tools/file_tools.py +112 -0
- apps/cli/tools/market_tools.py +549 -0
- apps/cli/tools/notebook_tools.py +111 -0
- apps/cli/tools/system_tools.py +669 -0
- apps/cli/tools/write_tools.py +715 -0
- apps/cli/tradingview_bridge.py +434 -0
- apps/cli/update_check.py +152 -0
- apps/cli/utils/__init__.py +0 -0
- apps/cli/utils/market_detect.py +1578 -0
- apps/daemon/README.md +14 -0
- apps/vscode/README.md +115 -0
- apps/vscode/package.json +70 -0
- aria_cli.py +11636 -0
- aria_code-4.1.3.dist-info/METADATA +952 -0
- aria_code-4.1.3.dist-info/RECORD +284 -0
- aria_code-4.1.3.dist-info/WHEEL +5 -0
- aria_code-4.1.3.dist-info/entry_points.txt +2 -0
- aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
- aria_code-4.1.3.dist-info/top_level.txt +50 -0
- aria_daemon.py +1295 -0
- aria_feishu_bot.py +1359 -0
- aria_relay_client.py +182 -0
- aria_relay_server.py +405 -0
- aria_telegram_bot.py +202 -0
- ariarc.py +328 -0
- artifacts.py +491 -0
- backtest_report.py +472 -0
- brokers/__init__.py +72 -0
- brokers/base.py +207 -0
- brokers/capabilities.py +264 -0
- brokers/cn/__init__.py +10 -0
- brokers/cn/easytrader_broker.py +193 -0
- brokers/cn/futu_broker.py +194 -0
- brokers/cn/longbridge_broker.py +190 -0
- brokers/cn/tiger_broker.py +196 -0
- brokers/cn/xtquant_broker.py +175 -0
- brokers/config.py +364 -0
- brokers/intl/__init__.py +5 -0
- brokers/intl/alpaca_broker.py +183 -0
- brokers/intl/ibkr_broker.py +215 -0
- brokers/intl/webull_broker.py +156 -0
- brokers/paper_broker.py +259 -0
- brokers/planning.py +296 -0
- brokers/registry.py +181 -0
- brokers/trading.py +237 -0
- change_store.py +127 -0
- command_safety.py +19 -0
- computer_use_tools.py +504 -0
- dashboard_generator.py +578 -0
- data_analysis_tools.py +808 -0
- data_cleaner.py +483 -0
- data_service.py +481 -0
- datasources/__init__.py +23 -0
- datasources/base.py +166 -0
- datasources/router.py +221 -0
- datasources/sources/__init__.py +15 -0
- datasources/sources/akshare_source.py +269 -0
- datasources/sources/alpha_vantage_source.py +202 -0
- datasources/sources/edgar_source.py +218 -0
- datasources/sources/finnhub_source.py +197 -0
- datasources/sources/fred_source.py +219 -0
- datasources/sources/tushare_source.py +141 -0
- datasources/sources/web_scraper_source.py +278 -0
- datasources/sources/world_bank_source.py +205 -0
- datasources/sources/yfinance_source.py +152 -0
- demo_player.py +204 -0
- doctor.py +508 -0
- file_analysis_tools.py +734 -0
- finance_formulas.py +389 -0
- football_data_client.py +1670 -0
- intent_classifier.py +358 -0
- local_finance_tools.py +3221 -0
- local_llm_provider.py +552 -0
- macro_tools.py +368 -0
- market_data_client.py +1899 -0
- mcp_client.py +506 -0
- memory_manager.py +245 -0
- model_capability.py +416 -0
- notification_tools.py +248 -0
- packages/__init__.py +23 -0
- packages/aria_agents/__init__.py +5 -0
- packages/aria_agents/manifest.py +69 -0
- packages/aria_core/__init__.py +34 -0
- packages/aria_core/architecture.py +192 -0
- packages/aria_core/export.py +124 -0
- packages/aria_core/manifest.py +65 -0
- packages/aria_infra/__init__.py +15 -0
- packages/aria_infra/arthera.py +52 -0
- packages/aria_infra/doctor.py +246 -0
- packages/aria_infra/product.py +37 -0
- packages/aria_mcp/__init__.py +25 -0
- packages/aria_mcp/bridge.py +38 -0
- packages/aria_mcp/config.py +97 -0
- packages/aria_mcp/tools.py +61 -0
- packages/aria_sdk/__init__.py +19 -0
- packages/aria_sdk/client.py +396 -0
- packages/aria_sdk/providers.py +70 -0
- packages/aria_sdk/streaming.py +73 -0
- packages/aria_sdk/types.py +86 -0
- packages/aria_services/__init__.py +55 -0
- packages/aria_services/context.py +258 -0
- packages/aria_services/data.py +11 -0
- packages/aria_services/provider_health.py +189 -0
- packages/aria_services/registry.py +213 -0
- packages/aria_services/usage.py +138 -0
- packages/aria_skills/__init__.py +5 -0
- packages/aria_skills/registry.py +59 -0
- packages/aria_tools/__init__.py +5 -0
- packages/aria_tools/registry.py +128 -0
- packages/quant_engine/__init__.py +6 -0
- packages/quant_engine/sports/__init__.py +72 -0
- packages/quant_engine/sports/calibrator.py +353 -0
- packages/quant_engine/sports/dixon_coles.py +234 -0
- packages/quant_engine/sports/elo.py +299 -0
- packages/quant_engine/sports/form.py +188 -0
- packages/quant_engine/sports/h2h.py +195 -0
- packages/quant_engine/sports/ml_model.py +354 -0
- packages/quant_engine/sports/predictor.py +311 -0
- packages/quant_engine/sports/tracker.py +664 -0
- packages/quant_engine/stochastic/__init__.py +27 -0
- packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- packages/quant_engine/stochastic/ito_calculus.py +477 -0
- packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- packages/quant_engine/stochastic/options_pricing.py +573 -0
- packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- plan_utils.py +194 -0
- plugin_loader.py +328 -0
- portfolio_ledger.py +262 -0
- privacy/__init__.py +5 -0
- privacy/feedback.py +123 -0
- project_tools.py +525 -0
- providers/__init__.py +30 -0
- providers/llm/__init__.py +19 -0
- providers/llm/anthropic.py +184 -0
- providers/llm/base.py +139 -0
- providers/llm/ollama.py +128 -0
- providers/llm/openai_compat.py +282 -0
- providers/llm/registry.py +358 -0
- realty_data_tools.py +659 -0
- report_generator.py +1314 -0
- runtime/__init__.py +103 -0
- runtime/agent_loop.py +1183 -0
- runtime/approval.py +51 -0
- runtime/events.py +102 -0
- runtime/gateway.py +128 -0
- runtime/lsp.py +346 -0
- runtime/subagent.py +258 -0
- runtime/tool_executor.py +104 -0
- runtime/tool_policy.py +106 -0
- safety/__init__.py +21 -0
- safety/permissions.py +275 -0
- setup_wizard.py +653 -0
- strategy_vault.py +420 -0
- ui/__init__.py +100 -0
- ui/banner.py +310 -0
- ui/completer.py +391 -0
- ui/console.py +271 -0
- ui/image_render.py +243 -0
- ui/input_box.py +376 -0
- ui/picker.py +195 -0
- ui/render/__init__.py +11 -0
- ui/render/finance.py +1480 -0
- ui/render/market.py +225 -0
- ui/render/output.py +681 -0
- ui/render/team.py +346 -0
- ui/robot.py +235 -0
- workspace/__init__.py +6 -0
- workspace/files.py +170 -0
- 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
|
+
]
|