aria-code 4.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. agents/__init__.py +32 -0
  2. agents/base.py +190 -0
  3. agents/deep/__init__.py +37 -0
  4. agents/deep/calibration_loop.py +144 -0
  5. agents/deep/critic.py +125 -0
  6. agents/deep/deepen.py +193 -0
  7. agents/deep/models.py +149 -0
  8. agents/deep/pipeline.py +164 -0
  9. agents/deep/quant_fusion.py +192 -0
  10. agents/deep/themes.py +95 -0
  11. agents/deep/tiers.py +106 -0
  12. agents/financial/__init__.py +10 -0
  13. agents/financial/catalyst.py +279 -0
  14. agents/financial/debate.py +145 -0
  15. agents/financial/earnings.py +303 -0
  16. agents/financial/fundamental.py +159 -0
  17. agents/financial/macro.py +99 -0
  18. agents/financial/news.py +207 -0
  19. agents/financial/risk.py +132 -0
  20. agents/financial/sector.py +279 -0
  21. agents/financial/synthesis.py +274 -0
  22. agents/financial/technical.py +258 -0
  23. agents/portfolio_agent.py +333 -0
  24. agents/realty/__init__.py +62 -0
  25. agents/realty/asset_diagnosis.py +150 -0
  26. agents/realty/business_match.py +165 -0
  27. agents/realty/cashflow_verify.py +208 -0
  28. agents/realty/contract_rules.py +209 -0
  29. agents/realty/energy_anomaly.py +188 -0
  30. agents/realty/exit_settlement.py +207 -0
  31. agents/realty/fulfillment_risk.py +205 -0
  32. agents/realty/ops_optimize.py +159 -0
  33. agents/realty/revenue_share.py +214 -0
  34. agents/registry.py +144 -0
  35. agents/sports/__init__.py +0 -0
  36. agents/sports/football_agent.py +169 -0
  37. agents/team.py +289 -0
  38. aliyun_data_client.py +660 -0
  39. apps/README.md +12 -0
  40. apps/__init__.py +2 -0
  41. apps/channels/README.md +15 -0
  42. apps/cli/README.md +13 -0
  43. apps/cli/__init__.py +2 -0
  44. apps/cli/bootstrap.py +99 -0
  45. apps/cli/codegen_paths.py +29 -0
  46. apps/cli/commands/__init__.py +16 -0
  47. apps/cli/commands/analysis_cmds.py +288 -0
  48. apps/cli/commands/backtest_cmds.py +1887 -0
  49. apps/cli/commands/broker_cmds.py +1154 -0
  50. apps/cli/commands/business_workflow_cmds.py +289 -0
  51. apps/cli/commands/catalog.py +84 -0
  52. apps/cli/commands/data_cmds.py +405 -0
  53. apps/cli/commands/diagnostic_cmds.py +179 -0
  54. apps/cli/commands/diagnostic_ops_cmds.py +696 -0
  55. apps/cli/commands/finance_render.py +12 -0
  56. apps/cli/commands/market.py +399 -0
  57. apps/cli/commands/market_cmds.py +1276 -0
  58. apps/cli/commands/market_context.py +425 -0
  59. apps/cli/commands/market_render.py +7 -0
  60. apps/cli/commands/model_cmds.py +1579 -0
  61. apps/cli/commands/ops_cmds.py +668 -0
  62. apps/cli/commands/portfolio_cmds.py +962 -0
  63. apps/cli/commands/report.py +377 -0
  64. apps/cli/commands/scaffold_templates.py +617 -0
  65. apps/cli/commands/session_cmds.py +179 -0
  66. apps/cli/commands/session_ux_cmds.py +280 -0
  67. apps/cli/commands/team.py +588 -0
  68. apps/cli/commands/team_render.py +8 -0
  69. apps/cli/commands/ui_cmds.py +358 -0
  70. apps/cli/commands/workflow_cmds.py +279 -0
  71. apps/cli/commands/workspace_cmds.py +1414 -0
  72. apps/cli/config_paths.py +70 -0
  73. apps/cli/config_store.py +61 -0
  74. apps/cli/deterministic.py +122 -0
  75. apps/cli/direct.py +48 -0
  76. apps/cli/github_app_auth.py +135 -0
  77. apps/cli/handlers/__init__.py +11 -0
  78. apps/cli/handlers/broker_handlers.py +122 -0
  79. apps/cli/handlers/chart_handlers.py +1309 -0
  80. apps/cli/handlers/market_handlers.py +2509 -0
  81. apps/cli/handlers/realty_handlers.py +114 -0
  82. apps/cli/handlers/strategy_advice.py +82 -0
  83. apps/cli/hooks.py +180 -0
  84. apps/cli/i18n.py +284 -0
  85. apps/cli/intent.py +136 -0
  86. apps/cli/intent_router.py +217 -0
  87. apps/cli/lifecycle_hooks.py +48 -0
  88. apps/cli/main.py +29 -0
  89. apps/cli/market_metadata.py +135 -0
  90. apps/cli/market_universe.py +265 -0
  91. apps/cli/message_processing.py +257 -0
  92. apps/cli/plan_mode.py +139 -0
  93. apps/cli/plotly_html.py +15 -0
  94. apps/cli/prediction_feedback.py +202 -0
  95. apps/cli/preflight.py +497 -0
  96. apps/cli/project_aria.py +60 -0
  97. apps/cli/prompts/__init__.py +0 -0
  98. apps/cli/prompts/coding.py +658 -0
  99. apps/cli/prompts/system_prompts.py +531 -0
  100. apps/cli/prompts/ui.py +434 -0
  101. apps/cli/providers/__init__.py +1 -0
  102. apps/cli/providers/base.py +271 -0
  103. apps/cli/providers/chat_routing.py +80 -0
  104. apps/cli/providers/llm/__init__.py +1 -0
  105. apps/cli/providers/llm/ollama_stream.py +1170 -0
  106. apps/cli/providers/llm/sse_stream.py +216 -0
  107. apps/cli/providers/runtime_bridge.py +185 -0
  108. apps/cli/runtime_consumer.py +489 -0
  109. apps/cli/session_export.py +87 -0
  110. apps/cli/session_jsonl.py +207 -0
  111. apps/cli/session_store.py +112 -0
  112. apps/cli/todo_tracker.py +190 -0
  113. apps/cli/tools/__init__.py +40 -0
  114. apps/cli/tools/context.py +46 -0
  115. apps/cli/tools/file_tools.py +112 -0
  116. apps/cli/tools/market_tools.py +549 -0
  117. apps/cli/tools/notebook_tools.py +111 -0
  118. apps/cli/tools/system_tools.py +669 -0
  119. apps/cli/tools/write_tools.py +715 -0
  120. apps/cli/tradingview_bridge.py +434 -0
  121. apps/cli/update_check.py +152 -0
  122. apps/cli/utils/__init__.py +0 -0
  123. apps/cli/utils/market_detect.py +1578 -0
  124. apps/daemon/README.md +14 -0
  125. apps/vscode/README.md +115 -0
  126. apps/vscode/package.json +70 -0
  127. aria_cli.py +11636 -0
  128. aria_code-4.1.3.dist-info/METADATA +952 -0
  129. aria_code-4.1.3.dist-info/RECORD +284 -0
  130. aria_code-4.1.3.dist-info/WHEEL +5 -0
  131. aria_code-4.1.3.dist-info/entry_points.txt +2 -0
  132. aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
  133. aria_code-4.1.3.dist-info/top_level.txt +50 -0
  134. aria_daemon.py +1295 -0
  135. aria_feishu_bot.py +1359 -0
  136. aria_relay_client.py +182 -0
  137. aria_relay_server.py +405 -0
  138. aria_telegram_bot.py +202 -0
  139. ariarc.py +328 -0
  140. artifacts.py +491 -0
  141. backtest_report.py +472 -0
  142. brokers/__init__.py +72 -0
  143. brokers/base.py +207 -0
  144. brokers/capabilities.py +264 -0
  145. brokers/cn/__init__.py +10 -0
  146. brokers/cn/easytrader_broker.py +193 -0
  147. brokers/cn/futu_broker.py +194 -0
  148. brokers/cn/longbridge_broker.py +190 -0
  149. brokers/cn/tiger_broker.py +196 -0
  150. brokers/cn/xtquant_broker.py +175 -0
  151. brokers/config.py +364 -0
  152. brokers/intl/__init__.py +5 -0
  153. brokers/intl/alpaca_broker.py +183 -0
  154. brokers/intl/ibkr_broker.py +215 -0
  155. brokers/intl/webull_broker.py +156 -0
  156. brokers/paper_broker.py +259 -0
  157. brokers/planning.py +296 -0
  158. brokers/registry.py +181 -0
  159. brokers/trading.py +237 -0
  160. change_store.py +127 -0
  161. command_safety.py +19 -0
  162. computer_use_tools.py +504 -0
  163. dashboard_generator.py +578 -0
  164. data_analysis_tools.py +808 -0
  165. data_cleaner.py +483 -0
  166. data_service.py +481 -0
  167. datasources/__init__.py +23 -0
  168. datasources/base.py +166 -0
  169. datasources/router.py +221 -0
  170. datasources/sources/__init__.py +15 -0
  171. datasources/sources/akshare_source.py +269 -0
  172. datasources/sources/alpha_vantage_source.py +202 -0
  173. datasources/sources/edgar_source.py +218 -0
  174. datasources/sources/finnhub_source.py +197 -0
  175. datasources/sources/fred_source.py +219 -0
  176. datasources/sources/tushare_source.py +141 -0
  177. datasources/sources/web_scraper_source.py +278 -0
  178. datasources/sources/world_bank_source.py +205 -0
  179. datasources/sources/yfinance_source.py +152 -0
  180. demo_player.py +204 -0
  181. doctor.py +508 -0
  182. file_analysis_tools.py +734 -0
  183. finance_formulas.py +389 -0
  184. football_data_client.py +1670 -0
  185. intent_classifier.py +358 -0
  186. local_finance_tools.py +3221 -0
  187. local_llm_provider.py +552 -0
  188. macro_tools.py +368 -0
  189. market_data_client.py +1899 -0
  190. mcp_client.py +506 -0
  191. memory_manager.py +245 -0
  192. model_capability.py +416 -0
  193. notification_tools.py +248 -0
  194. packages/__init__.py +23 -0
  195. packages/aria_agents/__init__.py +5 -0
  196. packages/aria_agents/manifest.py +69 -0
  197. packages/aria_core/__init__.py +34 -0
  198. packages/aria_core/architecture.py +192 -0
  199. packages/aria_core/export.py +124 -0
  200. packages/aria_core/manifest.py +65 -0
  201. packages/aria_infra/__init__.py +15 -0
  202. packages/aria_infra/arthera.py +52 -0
  203. packages/aria_infra/doctor.py +246 -0
  204. packages/aria_infra/product.py +37 -0
  205. packages/aria_mcp/__init__.py +25 -0
  206. packages/aria_mcp/bridge.py +38 -0
  207. packages/aria_mcp/config.py +97 -0
  208. packages/aria_mcp/tools.py +61 -0
  209. packages/aria_sdk/__init__.py +19 -0
  210. packages/aria_sdk/client.py +396 -0
  211. packages/aria_sdk/providers.py +70 -0
  212. packages/aria_sdk/streaming.py +73 -0
  213. packages/aria_sdk/types.py +86 -0
  214. packages/aria_services/__init__.py +55 -0
  215. packages/aria_services/context.py +258 -0
  216. packages/aria_services/data.py +11 -0
  217. packages/aria_services/provider_health.py +189 -0
  218. packages/aria_services/registry.py +213 -0
  219. packages/aria_services/usage.py +138 -0
  220. packages/aria_skills/__init__.py +5 -0
  221. packages/aria_skills/registry.py +59 -0
  222. packages/aria_tools/__init__.py +5 -0
  223. packages/aria_tools/registry.py +128 -0
  224. packages/quant_engine/__init__.py +6 -0
  225. packages/quant_engine/sports/__init__.py +72 -0
  226. packages/quant_engine/sports/calibrator.py +353 -0
  227. packages/quant_engine/sports/dixon_coles.py +234 -0
  228. packages/quant_engine/sports/elo.py +299 -0
  229. packages/quant_engine/sports/form.py +188 -0
  230. packages/quant_engine/sports/h2h.py +195 -0
  231. packages/quant_engine/sports/ml_model.py +354 -0
  232. packages/quant_engine/sports/predictor.py +311 -0
  233. packages/quant_engine/sports/tracker.py +664 -0
  234. packages/quant_engine/stochastic/__init__.py +27 -0
  235. packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  236. packages/quant_engine/stochastic/ito_calculus.py +477 -0
  237. packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  238. packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  239. packages/quant_engine/stochastic/options_pricing.py +573 -0
  240. packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  241. plan_utils.py +194 -0
  242. plugin_loader.py +328 -0
  243. portfolio_ledger.py +262 -0
  244. privacy/__init__.py +5 -0
  245. privacy/feedback.py +123 -0
  246. project_tools.py +525 -0
  247. providers/__init__.py +30 -0
  248. providers/llm/__init__.py +19 -0
  249. providers/llm/anthropic.py +184 -0
  250. providers/llm/base.py +139 -0
  251. providers/llm/ollama.py +128 -0
  252. providers/llm/openai_compat.py +282 -0
  253. providers/llm/registry.py +358 -0
  254. realty_data_tools.py +659 -0
  255. report_generator.py +1314 -0
  256. runtime/__init__.py +103 -0
  257. runtime/agent_loop.py +1183 -0
  258. runtime/approval.py +51 -0
  259. runtime/events.py +102 -0
  260. runtime/gateway.py +128 -0
  261. runtime/lsp.py +346 -0
  262. runtime/subagent.py +258 -0
  263. runtime/tool_executor.py +104 -0
  264. runtime/tool_policy.py +106 -0
  265. safety/__init__.py +21 -0
  266. safety/permissions.py +275 -0
  267. setup_wizard.py +653 -0
  268. strategy_vault.py +420 -0
  269. ui/__init__.py +100 -0
  270. ui/banner.py +310 -0
  271. ui/completer.py +391 -0
  272. ui/console.py +271 -0
  273. ui/image_render.py +243 -0
  274. ui/input_box.py +376 -0
  275. ui/picker.py +195 -0
  276. ui/render/__init__.py +11 -0
  277. ui/render/finance.py +1480 -0
  278. ui/render/market.py +225 -0
  279. ui/render/output.py +681 -0
  280. ui/render/team.py +346 -0
  281. ui/robot.py +235 -0
  282. workspace/__init__.py +6 -0
  283. workspace/files.py +170 -0
  284. workspace/verify.py +113 -0
@@ -0,0 +1,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"