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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
brokers/intl/alpaca_broker.py — Alpaca Markets 适配器
|
|
3
|
+
======================================================
|
|
4
|
+
支持市场:美股(实盘 + 模拟盘)
|
|
5
|
+
|
|
6
|
+
安装:pip install alpaca-py
|
|
7
|
+
文档:https://docs.alpaca.markets/
|
|
8
|
+
|
|
9
|
+
配置示例::
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
"id": "alpaca_paper",
|
|
13
|
+
"type": "alpaca",
|
|
14
|
+
"label": "Alpaca 模拟盘",
|
|
15
|
+
"api_key": "PKxxx",
|
|
16
|
+
"api_secret": "xxx",
|
|
17
|
+
"paper": true
|
|
18
|
+
}
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Any, Dict, List
|
|
24
|
+
|
|
25
|
+
from ..base import BrokerBase, AccountInfo, Position, Order, OrderResult
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AlpacaBroker(BrokerBase):
|
|
29
|
+
broker_type = "alpaca"
|
|
30
|
+
broker_name = "Alpaca Markets"
|
|
31
|
+
market = "US"
|
|
32
|
+
|
|
33
|
+
def __init__(self, broker_id: str, config: Dict[str, Any]):
|
|
34
|
+
super().__init__(broker_id, config)
|
|
35
|
+
self._api_key = config.get("api_key", "")
|
|
36
|
+
self._api_secret = config.get("api_secret", "")
|
|
37
|
+
self._paper = bool(config.get("paper", False))
|
|
38
|
+
self._trading = None
|
|
39
|
+
|
|
40
|
+
def connect(self) -> bool:
|
|
41
|
+
try:
|
|
42
|
+
from alpaca.trading.client import TradingClient
|
|
43
|
+
except ImportError:
|
|
44
|
+
raise ImportError("alpaca-py 未安装。请运行: pip install alpaca-py")
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
client = TradingClient(self._api_key, self._api_secret, paper=self._paper)
|
|
48
|
+
client.get_account() # 测试连通性
|
|
49
|
+
self._trading = client
|
|
50
|
+
self._connected = True
|
|
51
|
+
return True
|
|
52
|
+
except Exception as e:
|
|
53
|
+
raise RuntimeError(f"Alpaca 连接失败: {e}") from e
|
|
54
|
+
|
|
55
|
+
def account_info(self) -> AccountInfo:
|
|
56
|
+
self._require_connected()
|
|
57
|
+
try:
|
|
58
|
+
a = self._trading.get_account()
|
|
59
|
+
except Exception as e:
|
|
60
|
+
raise RuntimeError(f"Alpaca 账户查询失败: {e}") from e
|
|
61
|
+
|
|
62
|
+
equity = float(getattr(a, "equity", 0))
|
|
63
|
+
cash = float(getattr(a, "cash", 0))
|
|
64
|
+
mv = float(getattr(a, "long_market_value", 0)) - float(getattr(a, "short_market_value", 0))
|
|
65
|
+
pnl = float(getattr(a, "unrealized_pl", 0))
|
|
66
|
+
return AccountInfo(
|
|
67
|
+
broker_id=self.broker_id,
|
|
68
|
+
broker_type=self.broker_type,
|
|
69
|
+
label=self.label,
|
|
70
|
+
account_id=str(getattr(a, "account_number", getattr(a, "id", ""))),
|
|
71
|
+
currency="USD",
|
|
72
|
+
total_assets=equity,
|
|
73
|
+
cash=cash,
|
|
74
|
+
market_value=mv,
|
|
75
|
+
pnl_today=float(getattr(a, "unrealized_pl", 0)),
|
|
76
|
+
pnl_total=pnl,
|
|
77
|
+
extra={"buying_power": float(getattr(a, "buying_power", 0)),
|
|
78
|
+
"paper": self._paper},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def positions(self) -> List[Position]:
|
|
82
|
+
self._require_connected()
|
|
83
|
+
try:
|
|
84
|
+
raw = self._trading.get_all_positions()
|
|
85
|
+
except Exception as e:
|
|
86
|
+
raise RuntimeError(f"Alpaca 持仓查询失败: {e}") from e
|
|
87
|
+
|
|
88
|
+
result = []
|
|
89
|
+
for p in (raw or []):
|
|
90
|
+
sym = str(getattr(p, "symbol", ""))
|
|
91
|
+
qty = float(getattr(p, "qty", 0))
|
|
92
|
+
avail = float(getattr(p, "qty_available", qty))
|
|
93
|
+
cost = float(getattr(p, "avg_entry_price", 0))
|
|
94
|
+
price = float(getattr(p, "current_price", 0))
|
|
95
|
+
mv = float(getattr(p, "market_value", 0))
|
|
96
|
+
pnl = float(getattr(p, "unrealized_pl", 0))
|
|
97
|
+
pnl_pct = float(getattr(p, "unrealized_plpc", 0)) * 100
|
|
98
|
+
result.append(Position(
|
|
99
|
+
symbol=sym, name=sym,
|
|
100
|
+
quantity=qty, available_qty=avail,
|
|
101
|
+
cost_price=cost, current_price=price,
|
|
102
|
+
market_value=mv, pnl=pnl, pnl_pct=pnl_pct,
|
|
103
|
+
currency="USD", market="us",
|
|
104
|
+
))
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
def orders(self, status: str = "all", limit: int = 50) -> List[Order]:
|
|
108
|
+
self._require_connected()
|
|
109
|
+
try:
|
|
110
|
+
from alpaca.trading.requests import GetOrdersRequest
|
|
111
|
+
from alpaca.trading.enums import QueryOrderStatus
|
|
112
|
+
qstatus = {
|
|
113
|
+
"open": QueryOrderStatus.OPEN,
|
|
114
|
+
"filled": QueryOrderStatus.CLOSED,
|
|
115
|
+
"cancelled": QueryOrderStatus.CLOSED,
|
|
116
|
+
}.get(status, QueryOrderStatus.ALL)
|
|
117
|
+
raw = self._trading.get_orders(GetOrdersRequest(status=qstatus, limit=limit))
|
|
118
|
+
except Exception as e:
|
|
119
|
+
raise RuntimeError(f"Alpaca 订单查询失败: {e}") from e
|
|
120
|
+
|
|
121
|
+
result = []
|
|
122
|
+
for o in (raw or []):
|
|
123
|
+
side = "buy" if str(getattr(o, "side", "")).lower() == "buy" else "sell"
|
|
124
|
+
mapped = _alpaca_order_status(str(getattr(o, "status", "")))
|
|
125
|
+
if status not in ("all",) and mapped != status:
|
|
126
|
+
continue
|
|
127
|
+
result.append(Order(
|
|
128
|
+
order_id=str(getattr(o, "id", "")),
|
|
129
|
+
symbol=str(getattr(o, "symbol", "")),
|
|
130
|
+
name=str(getattr(o, "symbol", "")),
|
|
131
|
+
side=side,
|
|
132
|
+
order_type=str(getattr(o, "order_type", "limit")).lower(),
|
|
133
|
+
quantity=float(getattr(o, "qty", 0) or 0),
|
|
134
|
+
filled_qty=float(getattr(o, "filled_qty", 0) or 0),
|
|
135
|
+
price=float(getattr(o, "limit_price", 0) or 0),
|
|
136
|
+
avg_price=float(getattr(o, "filled_avg_price", 0) or 0),
|
|
137
|
+
status=mapped,
|
|
138
|
+
created_at=str(getattr(o, "created_at", "")),
|
|
139
|
+
currency="USD",
|
|
140
|
+
))
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
def place_order(self, symbol: str, side: str, quantity: float,
|
|
144
|
+
order_type: str = "limit", price: float = 0.0, **kwargs) -> OrderResult:
|
|
145
|
+
self._require_connected()
|
|
146
|
+
try:
|
|
147
|
+
from alpaca.trading.requests import MarketOrderRequest, LimitOrderRequest
|
|
148
|
+
from alpaca.trading.enums import OrderSide, TimeInForce
|
|
149
|
+
alpaca_side = OrderSide.BUY if side == "buy" else OrderSide.SELL
|
|
150
|
+
if order_type == "market":
|
|
151
|
+
req = MarketOrderRequest(
|
|
152
|
+
symbol=symbol, qty=quantity, side=alpaca_side, time_in_force=TimeInForce.DAY)
|
|
153
|
+
else:
|
|
154
|
+
req = LimitOrderRequest(
|
|
155
|
+
symbol=symbol, qty=quantity, side=alpaca_side,
|
|
156
|
+
limit_price=price, time_in_force=TimeInForce.DAY)
|
|
157
|
+
o = self._trading.submit_order(req)
|
|
158
|
+
return OrderResult(success=True, order_id=str(o.id), broker_id=self.broker_id)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
return OrderResult(success=False, message=str(e), broker_id=self.broker_id)
|
|
161
|
+
|
|
162
|
+
def cancel_order(self, order_id: str) -> bool:
|
|
163
|
+
self._require_connected()
|
|
164
|
+
try:
|
|
165
|
+
self._trading.cancel_order_by_id(order_id)
|
|
166
|
+
return True
|
|
167
|
+
except Exception:
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
def _require_connected(self):
|
|
171
|
+
if not self._connected or not self._trading:
|
|
172
|
+
raise RuntimeError("Alpaca 未连接,请先调用 connect()")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _alpaca_order_status(raw: str) -> str:
|
|
176
|
+
raw = raw.lower()
|
|
177
|
+
if raw == "filled":
|
|
178
|
+
return "filled"
|
|
179
|
+
if raw == "partially_filled":
|
|
180
|
+
return "partial"
|
|
181
|
+
if raw in ("canceled", "cancelled", "expired", "rejected"):
|
|
182
|
+
return "cancelled"
|
|
183
|
+
return "open"
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
brokers/intl/ibkr_broker.py — Interactive Brokers 适配器
|
|
3
|
+
==========================================================
|
|
4
|
+
通过 ib_insync 连接 TWS 或 IB Gateway。
|
|
5
|
+
|
|
6
|
+
安装:pip install ib_insync
|
|
7
|
+
文档:https://ib-insync.readthedocs.io/
|
|
8
|
+
|
|
9
|
+
配置示例::
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
"id": "ibkr_us",
|
|
13
|
+
"type": "ibkr",
|
|
14
|
+
"label": "盈透美股",
|
|
15
|
+
"host": "127.0.0.1",
|
|
16
|
+
"port": 7496,
|
|
17
|
+
"client_id": 1,
|
|
18
|
+
"readonly": false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
端口说明:
|
|
22
|
+
TWS 实盘: 7496
|
|
23
|
+
TWS 模拟: 7497
|
|
24
|
+
Gateway 实盘: 4001
|
|
25
|
+
Gateway 模拟: 4002
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from typing import Any, Dict, List
|
|
31
|
+
|
|
32
|
+
from ..base import BrokerBase, AccountInfo, Position, Order, OrderResult
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class IBKRBroker(BrokerBase):
|
|
36
|
+
broker_type = "ibkr"
|
|
37
|
+
broker_name = "Interactive Brokers"
|
|
38
|
+
market = "US"
|
|
39
|
+
|
|
40
|
+
def __init__(self, broker_id: str, config: Dict[str, Any]):
|
|
41
|
+
super().__init__(broker_id, config)
|
|
42
|
+
self._host = config.get("host", "127.0.0.1")
|
|
43
|
+
self._port = int(config.get("port", 7496))
|
|
44
|
+
self._client_id = int(config.get("client_id", 1))
|
|
45
|
+
self._readonly = bool(config.get("readonly", False))
|
|
46
|
+
self._ib = None
|
|
47
|
+
|
|
48
|
+
def connect(self) -> bool:
|
|
49
|
+
try:
|
|
50
|
+
from ib_insync import IB
|
|
51
|
+
except ImportError:
|
|
52
|
+
raise ImportError("ib_insync 未安装。请运行: pip install ib_insync 并启动 TWS / IB Gateway")
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
ib = IB()
|
|
56
|
+
ib.connect(self._host, self._port, clientId=self._client_id, readonly=self._readonly)
|
|
57
|
+
self._ib = ib
|
|
58
|
+
self._connected = True
|
|
59
|
+
return True
|
|
60
|
+
except Exception as e:
|
|
61
|
+
raise RuntimeError(f"IBKR 连接失败: {e}") from e
|
|
62
|
+
|
|
63
|
+
def disconnect(self) -> None:
|
|
64
|
+
if self._ib:
|
|
65
|
+
try:
|
|
66
|
+
self._ib.disconnect()
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
self._connected = False
|
|
70
|
+
|
|
71
|
+
def account_info(self) -> AccountInfo:
|
|
72
|
+
self._require_connected()
|
|
73
|
+
try:
|
|
74
|
+
summary = {v.tag: v.value for v in self._ib.accountSummary()}
|
|
75
|
+
account_id = summary.get("AccountCode", self._ib.managedAccounts()[0] if self._ib.managedAccounts() else "")
|
|
76
|
+
currency = summary.get("BaseCurrency", "USD")
|
|
77
|
+
netliq = float(summary.get("NetLiquidation", 0))
|
|
78
|
+
cash = float(summary.get("TotalCashValue", 0))
|
|
79
|
+
mv = float(summary.get("GrossPositionValue", 0))
|
|
80
|
+
pnl = float(summary.get("UnrealizedPnL", 0))
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise RuntimeError(f"IBKR 账户查询失败: {e}") from e
|
|
83
|
+
|
|
84
|
+
return AccountInfo(
|
|
85
|
+
broker_id=self.broker_id,
|
|
86
|
+
broker_type=self.broker_type,
|
|
87
|
+
label=self.label,
|
|
88
|
+
account_id=account_id,
|
|
89
|
+
currency=currency,
|
|
90
|
+
total_assets=netliq,
|
|
91
|
+
cash=cash,
|
|
92
|
+
market_value=mv,
|
|
93
|
+
pnl_total=pnl,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def positions(self) -> List[Position]:
|
|
97
|
+
self._require_connected()
|
|
98
|
+
try:
|
|
99
|
+
raw = self._ib.positions()
|
|
100
|
+
except Exception as e:
|
|
101
|
+
raise RuntimeError(f"IBKR 持仓查询失败: {e}") from e
|
|
102
|
+
|
|
103
|
+
# Request market data for all contracts in batch to get current prices
|
|
104
|
+
_tick_map: dict = {}
|
|
105
|
+
try:
|
|
106
|
+
contracts = [p.contract for p in (raw or [])]
|
|
107
|
+
if contracts:
|
|
108
|
+
tickers = self._ib.reqTickers(*contracts)
|
|
109
|
+
for tk in tickers:
|
|
110
|
+
_sym = tk.contract.symbol
|
|
111
|
+
# Use last price, fall back to close, then mark price
|
|
112
|
+
_px = tk.last or tk.close or tk.marketPrice() or 0.0
|
|
113
|
+
if _px and _px > 0:
|
|
114
|
+
_tick_map[_sym] = float(_px)
|
|
115
|
+
except Exception:
|
|
116
|
+
pass # tick fetch best-effort; fall back to cost-based estimates below
|
|
117
|
+
|
|
118
|
+
result = []
|
|
119
|
+
for p in (raw or []):
|
|
120
|
+
contract = p.contract
|
|
121
|
+
sym = contract.symbol
|
|
122
|
+
qty = float(p.position)
|
|
123
|
+
cost = float(p.avgCost)
|
|
124
|
+
price = _tick_map.get(sym, 0.0)
|
|
125
|
+
# If tick fetch failed, estimate from unrealizedPNL + cost basis
|
|
126
|
+
if price == 0.0 and qty != 0 and cost > 0:
|
|
127
|
+
upnl = float(getattr(p, "unrealizedPNL", 0))
|
|
128
|
+
price = cost + upnl / qty
|
|
129
|
+
price = max(price, 0.0)
|
|
130
|
+
mv = qty * price
|
|
131
|
+
pnl = float(getattr(p, "unrealizedPNL", 0))
|
|
132
|
+
currency = contract.currency or "USD"
|
|
133
|
+
result.append(Position(
|
|
134
|
+
symbol=sym, name=contract.localSymbol or sym,
|
|
135
|
+
quantity=qty, available_qty=qty,
|
|
136
|
+
cost_price=cost, current_price=price,
|
|
137
|
+
market_value=mv, pnl=pnl,
|
|
138
|
+
pnl_pct=round(pnl / (cost * qty) * 100, 2) if cost and qty else 0.0,
|
|
139
|
+
currency=currency, market="us",
|
|
140
|
+
))
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
def orders(self, status: str = "all", limit: int = 50) -> List[Order]:
|
|
144
|
+
self._require_connected()
|
|
145
|
+
try:
|
|
146
|
+
raw = self._ib.openOrders() + self._ib.reqCompletedOrders(apiOnly=False)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
raise RuntimeError(f"IBKR 订单查询失败: {e}") from e
|
|
149
|
+
|
|
150
|
+
result = []
|
|
151
|
+
for trade in list(raw or [])[:limit]:
|
|
152
|
+
order = trade.order
|
|
153
|
+
contract = trade.contract
|
|
154
|
+
side = "buy" if order.action.upper() == "BUY" else "sell"
|
|
155
|
+
mapped = _ibkr_order_status(trade.orderStatus.status)
|
|
156
|
+
if status != "all" and mapped != status:
|
|
157
|
+
continue
|
|
158
|
+
result.append(Order(
|
|
159
|
+
order_id=str(order.orderId),
|
|
160
|
+
symbol=contract.symbol,
|
|
161
|
+
name=contract.localSymbol or contract.symbol,
|
|
162
|
+
side=side,
|
|
163
|
+
order_type=order.orderType.lower(),
|
|
164
|
+
quantity=float(order.totalQuantity),
|
|
165
|
+
filled_qty=float(trade.orderStatus.filled),
|
|
166
|
+
price=float(getattr(order, "lmtPrice", 0)),
|
|
167
|
+
avg_price=float(trade.orderStatus.avgFillPrice),
|
|
168
|
+
status=mapped,
|
|
169
|
+
currency=contract.currency or "USD",
|
|
170
|
+
))
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
def place_order(self, symbol: str, side: str, quantity: float,
|
|
174
|
+
order_type: str = "limit", price: float = 0.0, **kwargs) -> OrderResult:
|
|
175
|
+
if self._readonly:
|
|
176
|
+
return OrderResult(success=False, message="IBKR 账户配置为只读模式", broker_id=self.broker_id)
|
|
177
|
+
self._require_connected()
|
|
178
|
+
try:
|
|
179
|
+
from ib_insync import Stock, MarketOrder, LimitOrder
|
|
180
|
+
contract = Stock(symbol, "SMART", "USD")
|
|
181
|
+
if order_type == "market":
|
|
182
|
+
order = MarketOrder(action=side.upper(), totalQuantity=int(quantity))
|
|
183
|
+
else:
|
|
184
|
+
order = LimitOrder(action=side.upper(), totalQuantity=int(quantity), lmtPrice=price)
|
|
185
|
+
trade = self._ib.placeOrder(contract, order)
|
|
186
|
+
return OrderResult(success=True, order_id=str(trade.order.orderId), broker_id=self.broker_id)
|
|
187
|
+
except Exception as e:
|
|
188
|
+
return OrderResult(success=False, message=str(e), broker_id=self.broker_id)
|
|
189
|
+
|
|
190
|
+
def cancel_order(self, order_id: str) -> bool:
|
|
191
|
+
self._require_connected()
|
|
192
|
+
try:
|
|
193
|
+
open_trades = self._ib.openTrades()
|
|
194
|
+
for t in open_trades:
|
|
195
|
+
if str(t.order.orderId) == order_id:
|
|
196
|
+
self._ib.cancelOrder(t.order)
|
|
197
|
+
return True
|
|
198
|
+
return False
|
|
199
|
+
except Exception:
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
def _require_connected(self):
|
|
203
|
+
if not self._connected or not self._ib:
|
|
204
|
+
raise RuntimeError("IBKR 未连接,请先调用 connect()")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _ibkr_order_status(raw: str) -> str:
|
|
208
|
+
raw = raw.lower()
|
|
209
|
+
if raw == "filled":
|
|
210
|
+
return "filled"
|
|
211
|
+
if "partial" in raw:
|
|
212
|
+
return "partial"
|
|
213
|
+
if raw in ("cancelled", "inactive"):
|
|
214
|
+
return "cancelled"
|
|
215
|
+
return "open"
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
brokers/intl/webull_broker.py — Webull 适配器
|
|
3
|
+
==============================================
|
|
4
|
+
支持市场:美股、港股(非官方 API,以查询为主)
|
|
5
|
+
|
|
6
|
+
安装:pip install webull
|
|
7
|
+
文档:https://github.com/tedchou12/webull
|
|
8
|
+
|
|
9
|
+
配置示例::
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
"id": "webull_us",
|
|
13
|
+
"type": "webull",
|
|
14
|
+
"label": "Webull 美股",
|
|
15
|
+
"username": "your@email.com",
|
|
16
|
+
"password": "your_password",
|
|
17
|
+
"device_id": "",
|
|
18
|
+
"mfa": ""
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
注意:Webull 为非官方 API,下单功能可能不稳定。
|
|
22
|
+
建议仅用于查询(持仓/账户),下单在 App 内操作。
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from typing import Any, Dict, List
|
|
28
|
+
|
|
29
|
+
from ..base import BrokerBase, AccountInfo, Position, Order, OrderResult
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WebullBroker(BrokerBase):
|
|
33
|
+
broker_type = "webull"
|
|
34
|
+
broker_name = "Webull"
|
|
35
|
+
market = "US"
|
|
36
|
+
read_only = True # 默认只读,避免非官方 API 下单风险
|
|
37
|
+
|
|
38
|
+
def __init__(self, broker_id: str, config: Dict[str, Any]):
|
|
39
|
+
super().__init__(broker_id, config)
|
|
40
|
+
self._username = config.get("username", "")
|
|
41
|
+
self._password = config.get("password", "")
|
|
42
|
+
self._device_id = config.get("device_id", "")
|
|
43
|
+
self._mfa = config.get("mfa", "")
|
|
44
|
+
self._wb = None
|
|
45
|
+
|
|
46
|
+
def connect(self) -> bool:
|
|
47
|
+
try:
|
|
48
|
+
from webull import webull as wb_cls
|
|
49
|
+
except ImportError:
|
|
50
|
+
raise ImportError("webull 未安装。请运行: pip install webull")
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
wb = wb_cls()
|
|
54
|
+
if self._device_id:
|
|
55
|
+
wb._set_did(self._device_id)
|
|
56
|
+
wb.login(self._username, self._password, mfa_code=self._mfa or None)
|
|
57
|
+
self._wb = wb
|
|
58
|
+
self._connected = True
|
|
59
|
+
return True
|
|
60
|
+
except Exception as e:
|
|
61
|
+
raise RuntimeError(f"Webull 登录失败: {e}") from e
|
|
62
|
+
|
|
63
|
+
def account_info(self) -> AccountInfo:
|
|
64
|
+
self._require_connected()
|
|
65
|
+
try:
|
|
66
|
+
data = self._wb.get_account()
|
|
67
|
+
except Exception as e:
|
|
68
|
+
raise RuntimeError(f"Webull 账户查询失败: {e}") from e
|
|
69
|
+
|
|
70
|
+
total = float(data.get("netLiquidation", data.get("totalMarketValue", 0)))
|
|
71
|
+
cash = float(data.get("cashBalance", data.get("availableBalance", 0)))
|
|
72
|
+
mv = float(data.get("totalMarketValue", 0))
|
|
73
|
+
pnl_day = float(data.get("unrealizedProfitLoss", 0))
|
|
74
|
+
return AccountInfo(
|
|
75
|
+
broker_id=self.broker_id,
|
|
76
|
+
broker_type=self.broker_type,
|
|
77
|
+
label=self.label,
|
|
78
|
+
account_id=str(data.get("accountId", "")),
|
|
79
|
+
currency="USD",
|
|
80
|
+
total_assets=total,
|
|
81
|
+
cash=cash,
|
|
82
|
+
market_value=mv,
|
|
83
|
+
pnl_today=pnl_day,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def positions(self) -> List[Position]:
|
|
87
|
+
self._require_connected()
|
|
88
|
+
try:
|
|
89
|
+
raw = self._wb.get_positions()
|
|
90
|
+
except Exception as e:
|
|
91
|
+
raise RuntimeError(f"Webull 持仓查询失败: {e}") from e
|
|
92
|
+
|
|
93
|
+
result = []
|
|
94
|
+
for p in (raw or []):
|
|
95
|
+
ticker = p.get("ticker", {})
|
|
96
|
+
sym = str(ticker.get("symbol", p.get("symbol", "")))
|
|
97
|
+
name = str(ticker.get("name", ""))
|
|
98
|
+
qty = float(p.get("position", p.get("quantity", 0)))
|
|
99
|
+
cost = float(p.get("costPrice", p.get("avgCost", 0)))
|
|
100
|
+
price = float(p.get("lastPrice", p.get("price", 0)))
|
|
101
|
+
mv = float(p.get("marketValue", qty * price))
|
|
102
|
+
pnl = float(p.get("unrealizedProfitLoss", 0))
|
|
103
|
+
pnl_pct= float(p.get("unrealizedProfitLossRate", 0)) * 100
|
|
104
|
+
result.append(Position(
|
|
105
|
+
symbol=sym, name=name,
|
|
106
|
+
quantity=qty, available_qty=qty,
|
|
107
|
+
cost_price=cost, current_price=price,
|
|
108
|
+
market_value=mv, pnl=pnl, pnl_pct=pnl_pct,
|
|
109
|
+
currency="USD", market="us",
|
|
110
|
+
))
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
def orders(self, status: str = "all", limit: int = 50) -> List[Order]:
|
|
114
|
+
self._require_connected()
|
|
115
|
+
try:
|
|
116
|
+
raw = self._wb.get_history_orders(status="All")
|
|
117
|
+
except Exception as e:
|
|
118
|
+
raise RuntimeError(f"Webull 订单查询失败: {e}") from e
|
|
119
|
+
|
|
120
|
+
result = []
|
|
121
|
+
for o in list(raw or [])[:limit]:
|
|
122
|
+
side = "buy" if str(o.get("action", "")).upper() == "BUY" else "sell"
|
|
123
|
+
mapped = _wb_order_status(str(o.get("status", "")))
|
|
124
|
+
if status != "all" and mapped != status:
|
|
125
|
+
continue
|
|
126
|
+
ticker = o.get("ticker", {})
|
|
127
|
+
result.append(Order(
|
|
128
|
+
order_id=str(o.get("orderId", "")),
|
|
129
|
+
symbol=str(ticker.get("symbol", o.get("symbol", ""))),
|
|
130
|
+
name=str(ticker.get("name", "")),
|
|
131
|
+
side=side,
|
|
132
|
+
order_type=str(o.get("orderType", "limit")).lower(),
|
|
133
|
+
quantity=float(o.get("totalQuantity", 0)),
|
|
134
|
+
filled_qty=float(o.get("filledQuantity", 0)),
|
|
135
|
+
price=float(o.get("lmtPrice", 0) or 0),
|
|
136
|
+
avg_price=float(o.get("avgFilledPrice", 0) or 0),
|
|
137
|
+
status=mapped,
|
|
138
|
+
created_at=str(o.get("createTime", "")),
|
|
139
|
+
currency="USD",
|
|
140
|
+
))
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
def _require_connected(self):
|
|
144
|
+
if not self._connected or not self._wb:
|
|
145
|
+
raise RuntimeError("Webull 未连接,请先调用 connect()")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _wb_order_status(raw: str) -> str:
|
|
149
|
+
raw = raw.upper()
|
|
150
|
+
if raw == "FILLED":
|
|
151
|
+
return "filled"
|
|
152
|
+
if raw == "PARTIALLY_FILLED":
|
|
153
|
+
return "partial"
|
|
154
|
+
if raw in ("CANCELLED", "EXPIRED", "REJECTED"):
|
|
155
|
+
return "cancelled"
|
|
156
|
+
return "open"
|