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,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
brokers/cn/futu_broker.py — 富途牛牛 OpenAPI 适配器
|
|
3
|
+
=====================================================
|
|
4
|
+
支持市场:港股、美股、A股(通过 OpenD 网关)
|
|
5
|
+
|
|
6
|
+
安装:pip install futu-api
|
|
7
|
+
文档:https://openapi.futunn.com/futu-api-doc/
|
|
8
|
+
|
|
9
|
+
配置示例::
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
"id": "futu_hk",
|
|
13
|
+
"type": "futu",
|
|
14
|
+
"label": "富途港股",
|
|
15
|
+
"host": "127.0.0.1",
|
|
16
|
+
"port": 11111,
|
|
17
|
+
"market": "HK",
|
|
18
|
+
"trd_env": "REAL"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
注意:需在本机启动 Futu OpenD 进程。
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any, Dict, List
|
|
27
|
+
|
|
28
|
+
from ..base import BrokerBase, AccountInfo, Position, Order, OrderResult
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FutuBroker(BrokerBase):
|
|
32
|
+
broker_type = "futu"
|
|
33
|
+
broker_name = "富途牛牛"
|
|
34
|
+
market = "HK"
|
|
35
|
+
|
|
36
|
+
def __init__(self, broker_id: str, config: Dict[str, Any]):
|
|
37
|
+
super().__init__(broker_id, config)
|
|
38
|
+
self._host = config.get("host", "127.0.0.1")
|
|
39
|
+
self._port = int(config.get("port", 11111))
|
|
40
|
+
self._market = config.get("market", "HK").upper()
|
|
41
|
+
self._trd_env = config.get("trd_env", "REAL").upper()
|
|
42
|
+
self._quote_ctx = None
|
|
43
|
+
self._trd_ctx = None
|
|
44
|
+
self.market = self._market
|
|
45
|
+
|
|
46
|
+
def connect(self) -> bool:
|
|
47
|
+
try:
|
|
48
|
+
import futu as ft
|
|
49
|
+
except ImportError:
|
|
50
|
+
raise ImportError("futu-api 未安装。请运行: pip install futu-api 并启动 Futu OpenD")
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
self._quote_ctx = ft.OpenQuoteContext(host=self._host, port=self._port)
|
|
54
|
+
trd_market = {
|
|
55
|
+
"HK": ft.TrdMarket.HK,
|
|
56
|
+
"US": ft.TrdMarket.US,
|
|
57
|
+
"CN": ft.TrdMarket.CN,
|
|
58
|
+
"A": ft.TrdMarket.CN,
|
|
59
|
+
}.get(self._market, ft.TrdMarket.HK)
|
|
60
|
+
trd_env = ft.TrdEnv.REAL if self._trd_env == "REAL" else ft.TrdEnv.SIMULATE
|
|
61
|
+
self._trd_ctx = ft.OpenSecTradeContext(
|
|
62
|
+
filter_trdmarket=trd_market,
|
|
63
|
+
host=self._host, port=self._port,
|
|
64
|
+
security_firm=ft.SecurityFirm.FUTUINC,
|
|
65
|
+
)
|
|
66
|
+
self._ft = ft
|
|
67
|
+
self._trd_env_val = trd_env
|
|
68
|
+
self._connected = True
|
|
69
|
+
return True
|
|
70
|
+
except Exception as e:
|
|
71
|
+
raise RuntimeError(f"富途 OpenAPI 连接失败: {e}") from e
|
|
72
|
+
|
|
73
|
+
def disconnect(self) -> None:
|
|
74
|
+
for ctx in (self._quote_ctx, self._trd_ctx):
|
|
75
|
+
try:
|
|
76
|
+
if ctx:
|
|
77
|
+
ctx.close()
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
self._connected = False
|
|
81
|
+
|
|
82
|
+
def account_info(self) -> AccountInfo:
|
|
83
|
+
self._require_connected()
|
|
84
|
+
ret, data = self._trd_ctx.accinfo_query(trd_env=self._trd_env_val)
|
|
85
|
+
if ret != self._ft.RET_OK:
|
|
86
|
+
raise RuntimeError(f"富途账户查询失败: {data}")
|
|
87
|
+
row = data.iloc[0]
|
|
88
|
+
currency = "HKD" if self._market == "HK" else "USD" if self._market == "US" else "CNY"
|
|
89
|
+
return AccountInfo(
|
|
90
|
+
broker_id=self.broker_id,
|
|
91
|
+
broker_type=self.broker_type,
|
|
92
|
+
label=self.label,
|
|
93
|
+
account_id=str(row.get("acc_id", "")),
|
|
94
|
+
currency=currency,
|
|
95
|
+
total_assets=float(row.get("total_assets", 0)),
|
|
96
|
+
cash=float(row.get("cash", 0)),
|
|
97
|
+
market_value=float(row.get("market_val", 0)),
|
|
98
|
+
frozen=float(row.get("frozen_cash", 0)),
|
|
99
|
+
pnl_today=float(row.get("today_profit", 0)),
|
|
100
|
+
pnl_pct=float(row.get("today_profit_ratio", 0)) * 100,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def positions(self) -> List[Position]:
|
|
104
|
+
self._require_connected()
|
|
105
|
+
ret, data = self._trd_ctx.position_list_query(trd_env=self._trd_env_val)
|
|
106
|
+
if ret != self._ft.RET_OK:
|
|
107
|
+
raise RuntimeError(f"富途持仓查询失败: {data}")
|
|
108
|
+
result = []
|
|
109
|
+
currency = "HKD" if self._market == "HK" else "USD" if self._market == "US" else "CNY"
|
|
110
|
+
for _, row in data.iterrows():
|
|
111
|
+
sym = str(row.get("code", ""))
|
|
112
|
+
name = str(row.get("stock_name", ""))
|
|
113
|
+
qty = float(row.get("qty", 0))
|
|
114
|
+
avail= float(row.get("can_sell_qty", 0))
|
|
115
|
+
cost = float(row.get("cost_price", 0))
|
|
116
|
+
price= float(row.get("price", 0))
|
|
117
|
+
mv = float(row.get("market_val", 0))
|
|
118
|
+
pnl = float(row.get("unrealized_pl", 0))
|
|
119
|
+
pnl_pct = float(row.get("unrealized_pl_ratio", 0)) * 100
|
|
120
|
+
result.append(Position(
|
|
121
|
+
symbol=sym, name=name,
|
|
122
|
+
quantity=qty, available_qty=avail,
|
|
123
|
+
cost_price=cost, current_price=price,
|
|
124
|
+
market_value=mv, pnl=pnl, pnl_pct=pnl_pct,
|
|
125
|
+
currency=currency, market=self._market.lower(),
|
|
126
|
+
))
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
def orders(self, status: str = "all", limit: int = 50) -> List[Order]:
|
|
130
|
+
self._require_connected()
|
|
131
|
+
ret, data = self._trd_ctx.order_list_query(trd_env=self._trd_env_val)
|
|
132
|
+
if ret != self._ft.RET_OK:
|
|
133
|
+
raise RuntimeError(f"富途订单查询失败: {data}")
|
|
134
|
+
result = []
|
|
135
|
+
for _, row in data.head(limit).iterrows():
|
|
136
|
+
side = "buy" if "BUY" in str(row.get("trd_side", "")).upper() else "sell"
|
|
137
|
+
raw_st = str(row.get("order_status", ""))
|
|
138
|
+
mapped = _futu_order_status(raw_st)
|
|
139
|
+
if status != "all" and mapped != status:
|
|
140
|
+
continue
|
|
141
|
+
result.append(Order(
|
|
142
|
+
order_id=str(row.get("order_id", "")),
|
|
143
|
+
symbol=str(row.get("code", "")),
|
|
144
|
+
name=str(row.get("stock_name", "")),
|
|
145
|
+
side=side,
|
|
146
|
+
order_type=str(row.get("order_type", "")).lower(),
|
|
147
|
+
quantity=float(row.get("qty", 0)),
|
|
148
|
+
filled_qty=float(row.get("dealt_qty", 0)),
|
|
149
|
+
price=float(row.get("price", 0)),
|
|
150
|
+
avg_price=float(row.get("dealt_avg_price", 0)),
|
|
151
|
+
status=mapped,
|
|
152
|
+
created_at=str(row.get("create_time", "")),
|
|
153
|
+
))
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
def place_order(self, symbol: str, side: str, quantity: float,
|
|
157
|
+
order_type: str = "limit", price: float = 0.0, **kwargs) -> OrderResult:
|
|
158
|
+
self._require_connected()
|
|
159
|
+
ft = self._ft
|
|
160
|
+
trd_side = ft.TrdSide.BUY if side == "buy" else ft.TrdSide.SELL
|
|
161
|
+
ot = ft.OrderType.NORMAL if order_type == "limit" else ft.OrderType.MARKET
|
|
162
|
+
ret, data = self._trd_ctx.place_order(
|
|
163
|
+
price=price, qty=quantity, code=symbol,
|
|
164
|
+
trd_side=trd_side, order_type=ot,
|
|
165
|
+
trd_env=self._trd_env_val,
|
|
166
|
+
)
|
|
167
|
+
if ret != ft.RET_OK:
|
|
168
|
+
return OrderResult(success=False, message=str(data), broker_id=self.broker_id)
|
|
169
|
+
oid = str(data.iloc[0].get("order_id", ""))
|
|
170
|
+
return OrderResult(success=True, order_id=oid, broker_id=self.broker_id)
|
|
171
|
+
|
|
172
|
+
def cancel_order(self, order_id: str) -> bool:
|
|
173
|
+
self._require_connected()
|
|
174
|
+
ret, _ = self._trd_ctx.modify_order(
|
|
175
|
+
modify_order_op=self._ft.ModifyOrderOp.CANCEL,
|
|
176
|
+
order_id=int(order_id), qty=0, price=0,
|
|
177
|
+
trd_env=self._trd_env_val,
|
|
178
|
+
)
|
|
179
|
+
return ret == self._ft.RET_OK
|
|
180
|
+
|
|
181
|
+
def _require_connected(self):
|
|
182
|
+
if not self._connected or not self._trd_ctx:
|
|
183
|
+
raise RuntimeError("富途 OpenAPI 未连接,请先调用 connect()")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _futu_order_status(raw: str) -> str:
|
|
187
|
+
raw = raw.upper()
|
|
188
|
+
if "FILLED_ALL" in raw or "已全部成交" in raw:
|
|
189
|
+
return "filled"
|
|
190
|
+
if "FILLED_PART" in raw or "部分" in raw:
|
|
191
|
+
return "partial"
|
|
192
|
+
if "CANCELLED" in raw or "已撤" in raw:
|
|
193
|
+
return "cancelled"
|
|
194
|
+
return "open"
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
brokers/cn/longbridge_broker.py — 长桥证券 OpenAPI 适配器
|
|
3
|
+
===========================================================
|
|
4
|
+
支持市场:港股、美股、A股
|
|
5
|
+
|
|
6
|
+
安装:pip install longbridge
|
|
7
|
+
文档:https://open.longportapp.com/
|
|
8
|
+
|
|
9
|
+
配置示例::
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
"id": "lb_main",
|
|
13
|
+
"type": "longbridge",
|
|
14
|
+
"label": "长桥主账户",
|
|
15
|
+
"app_key": "YOUR_APP_KEY",
|
|
16
|
+
"app_secret": "YOUR_APP_SECRET",
|
|
17
|
+
"access_token": "YOUR_ACCESS_TOKEN"
|
|
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 LongbridgeBroker(BrokerBase):
|
|
29
|
+
broker_type = "longbridge"
|
|
30
|
+
broker_name = "长桥证券"
|
|
31
|
+
market = "GLOBAL"
|
|
32
|
+
|
|
33
|
+
def __init__(self, broker_id: str, config: Dict[str, Any]):
|
|
34
|
+
super().__init__(broker_id, config)
|
|
35
|
+
self._app_key = config.get("app_key", "")
|
|
36
|
+
self._app_secret = config.get("app_secret", "")
|
|
37
|
+
self._access_token = config.get("access_token", "")
|
|
38
|
+
self._trade_ctx = None
|
|
39
|
+
self._quote_ctx = None
|
|
40
|
+
|
|
41
|
+
def connect(self) -> bool:
|
|
42
|
+
try:
|
|
43
|
+
from longbridge.openapi import TradeContext, QuoteContext, Config
|
|
44
|
+
except ImportError:
|
|
45
|
+
raise ImportError("longbridge 未安装。请运行: pip install longbridge")
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
cfg = Config(
|
|
49
|
+
app_key=self._app_key,
|
|
50
|
+
app_secret=self._app_secret,
|
|
51
|
+
access_token=self._access_token,
|
|
52
|
+
)
|
|
53
|
+
self._trade_ctx = TradeContext(cfg)
|
|
54
|
+
self._quote_ctx = QuoteContext(cfg)
|
|
55
|
+
# 测试连接
|
|
56
|
+
self._trade_ctx.account_balance()
|
|
57
|
+
self._connected = True
|
|
58
|
+
return True
|
|
59
|
+
except Exception as e:
|
|
60
|
+
raise RuntimeError(f"长桥证券连接失败: {e}") from e
|
|
61
|
+
|
|
62
|
+
def account_info(self) -> AccountInfo:
|
|
63
|
+
self._require_connected()
|
|
64
|
+
try:
|
|
65
|
+
resp = self._trade_ctx.account_balance()
|
|
66
|
+
bal = resp[0] if resp else None
|
|
67
|
+
if not bal:
|
|
68
|
+
raise RuntimeError("长桥账户余额为空")
|
|
69
|
+
currency = str(getattr(bal, "currency", "HKD"))
|
|
70
|
+
total = float(getattr(bal, "total_cash", 0))
|
|
71
|
+
cash = float(getattr(bal, "available_cash", 0))
|
|
72
|
+
mv = float(getattr(bal, "market_value", 0))
|
|
73
|
+
pnl = float(getattr(bal, "unrealized_pnl", 0))
|
|
74
|
+
except Exception as e:
|
|
75
|
+
raise RuntimeError(f"长桥账户查询失败: {e}") from e
|
|
76
|
+
|
|
77
|
+
return AccountInfo(
|
|
78
|
+
broker_id=self.broker_id,
|
|
79
|
+
broker_type=self.broker_type,
|
|
80
|
+
label=self.label,
|
|
81
|
+
account_id=self.config.get("account_id", self._app_key[-6:]),
|
|
82
|
+
currency=currency,
|
|
83
|
+
total_assets=total + mv,
|
|
84
|
+
cash=cash,
|
|
85
|
+
market_value=mv,
|
|
86
|
+
pnl_total=pnl,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def positions(self) -> List[Position]:
|
|
90
|
+
self._require_connected()
|
|
91
|
+
try:
|
|
92
|
+
raw = self._trade_ctx.stock_positions()
|
|
93
|
+
except Exception as e:
|
|
94
|
+
raise RuntimeError(f"长桥持仓查询失败: {e}") from e
|
|
95
|
+
|
|
96
|
+
result = []
|
|
97
|
+
for ch in getattr(raw, "channels", []):
|
|
98
|
+
for p in getattr(ch, "positions", []):
|
|
99
|
+
sym = str(getattr(p, "symbol", ""))
|
|
100
|
+
name = str(getattr(p, "symbol_name", ""))
|
|
101
|
+
qty = float(getattr(p, "quantity", 0))
|
|
102
|
+
avail = float(getattr(p, "available_quantity", qty))
|
|
103
|
+
cost = float(getattr(p, "cost_price", 0))
|
|
104
|
+
price = float(getattr(p, "current_price", 0))
|
|
105
|
+
mv = float(getattr(p, "market_value", 0))
|
|
106
|
+
pnl = float(getattr(p, "unrealized_pnl", 0))
|
|
107
|
+
pnl_pct = float(getattr(p, "unrealized_pnl_ratio", 0))
|
|
108
|
+
currency = str(getattr(p, "currency", "HKD"))
|
|
109
|
+
result.append(Position(
|
|
110
|
+
symbol=sym, name=name,
|
|
111
|
+
quantity=qty, available_qty=avail,
|
|
112
|
+
cost_price=cost, current_price=price,
|
|
113
|
+
market_value=mv, pnl=pnl, pnl_pct=pnl_pct * 100,
|
|
114
|
+
currency=currency,
|
|
115
|
+
))
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
def orders(self, status: str = "all", limit: int = 50) -> List[Order]:
|
|
119
|
+
self._require_connected()
|
|
120
|
+
try:
|
|
121
|
+
from longbridge.openapi import OrderStatus as LBOrderStatus
|
|
122
|
+
raw = self._trade_ctx.today_orders()
|
|
123
|
+
except Exception as e:
|
|
124
|
+
raise RuntimeError(f"长桥订单查询失败: {e}") from e
|
|
125
|
+
|
|
126
|
+
result = []
|
|
127
|
+
for o in list(raw or [])[:limit]:
|
|
128
|
+
side = "buy" if "BUY" in str(getattr(o, "side", "")).upper() else "sell"
|
|
129
|
+
mapped = _lb_order_status(str(getattr(o, "status", "")))
|
|
130
|
+
if status != "all" and mapped != status:
|
|
131
|
+
continue
|
|
132
|
+
result.append(Order(
|
|
133
|
+
order_id=str(getattr(o, "order_id", "")),
|
|
134
|
+
symbol=str(getattr(o, "symbol", "")),
|
|
135
|
+
name=str(getattr(o, "stock_name", "")),
|
|
136
|
+
side=side,
|
|
137
|
+
order_type=str(getattr(o, "order_type", "")).lower(),
|
|
138
|
+
quantity=float(getattr(o, "quantity", 0)),
|
|
139
|
+
filled_qty=float(getattr(o, "executed_quantity", 0)),
|
|
140
|
+
price=float(getattr(o, "price", 0)),
|
|
141
|
+
avg_price=float(getattr(o, "executed_price", 0)),
|
|
142
|
+
status=mapped,
|
|
143
|
+
created_at=str(getattr(o, "submitted_at", "")),
|
|
144
|
+
currency=str(getattr(o, "currency", "HKD")),
|
|
145
|
+
))
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
def place_order(self, symbol: str, side: str, quantity: float,
|
|
149
|
+
order_type: str = "limit", price: float = 0.0, **kwargs) -> OrderResult:
|
|
150
|
+
self._require_connected()
|
|
151
|
+
try:
|
|
152
|
+
from longbridge.openapi import OrderSide, OrderType, TimeInForceType
|
|
153
|
+
from decimal import Decimal
|
|
154
|
+
lb_side = OrderSide.Buy if side == "buy" else OrderSide.Sell
|
|
155
|
+
lb_type = OrderType.MO if order_type == "market" else OrderType.LO
|
|
156
|
+
resp = self._trade_ctx.submit_order(
|
|
157
|
+
symbol=symbol,
|
|
158
|
+
order_type=lb_type,
|
|
159
|
+
side=lb_side,
|
|
160
|
+
submitted_quantity=int(quantity),
|
|
161
|
+
time_in_force=TimeInForceType.Day,
|
|
162
|
+
submitted_price=Decimal(str(price)) if price else None,
|
|
163
|
+
)
|
|
164
|
+
oid = str(getattr(resp, "order_id", ""))
|
|
165
|
+
return OrderResult(success=True, order_id=oid, broker_id=self.broker_id)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
return OrderResult(success=False, message=str(e), broker_id=self.broker_id)
|
|
168
|
+
|
|
169
|
+
def cancel_order(self, order_id: str) -> bool:
|
|
170
|
+
self._require_connected()
|
|
171
|
+
try:
|
|
172
|
+
self._trade_ctx.cancel_order(order_id)
|
|
173
|
+
return True
|
|
174
|
+
except Exception:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
def _require_connected(self):
|
|
178
|
+
if not self._connected:
|
|
179
|
+
raise RuntimeError("长桥证券未连接,请先调用 connect()")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _lb_order_status(raw: str) -> str:
|
|
183
|
+
raw = raw.upper()
|
|
184
|
+
if "FILLED" in raw and "PARTIAL" not in raw:
|
|
185
|
+
return "filled"
|
|
186
|
+
if "PARTIAL" in raw:
|
|
187
|
+
return "partial"
|
|
188
|
+
if any(x in raw for x in ("CANCELLED", "EXPIRED", "REJECTED")):
|
|
189
|
+
return "cancelled"
|
|
190
|
+
return "open"
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
brokers/cn/tiger_broker.py — 老虎证券 OpenAPI 适配器
|
|
3
|
+
======================================================
|
|
4
|
+
支持市场:美股、港股、A股
|
|
5
|
+
|
|
6
|
+
安装:pip install tigeropen
|
|
7
|
+
文档:https://quant.tigerfintech.com/
|
|
8
|
+
|
|
9
|
+
配置示例::
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
"id": "tiger_us",
|
|
13
|
+
"type": "tiger",
|
|
14
|
+
"label": "老虎美股",
|
|
15
|
+
"tiger_id": "YOUR_TIGER_ID",
|
|
16
|
+
"private_key_path": "~/.arthera/tiger_rsa.pem",
|
|
17
|
+
"account": "YOUR_ACCOUNT_ID",
|
|
18
|
+
"sandbox": false
|
|
19
|
+
}
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any, Dict, List
|
|
25
|
+
|
|
26
|
+
from ..base import BrokerBase, AccountInfo, Position, Order, OrderResult
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TigerBroker(BrokerBase):
|
|
30
|
+
broker_type = "tiger"
|
|
31
|
+
broker_name = "老虎证券"
|
|
32
|
+
market = "US"
|
|
33
|
+
|
|
34
|
+
def __init__(self, broker_id: str, config: Dict[str, Any]):
|
|
35
|
+
super().__init__(broker_id, config)
|
|
36
|
+
self._tiger_id = config.get("tiger_id", "")
|
|
37
|
+
self._private_key_path = config.get("private_key_path", "")
|
|
38
|
+
self._account = config.get("account", "")
|
|
39
|
+
self._sandbox = bool(config.get("sandbox", False))
|
|
40
|
+
self._client_config = None
|
|
41
|
+
self._trade_client = None
|
|
42
|
+
self._quote_client = None
|
|
43
|
+
|
|
44
|
+
def connect(self) -> bool:
|
|
45
|
+
try:
|
|
46
|
+
from tigeropen.tiger_open_config import TigerOpenClientConfig
|
|
47
|
+
from tigeropen.common.consts import TigerApiConstants
|
|
48
|
+
from tigeropen.trade.trade_client import TradeClient
|
|
49
|
+
from tigeropen.quote.quote_client import QuoteClient
|
|
50
|
+
except ImportError:
|
|
51
|
+
raise ImportError("tigeropen 未安装。请运行: pip install tigeropen")
|
|
52
|
+
|
|
53
|
+
import os
|
|
54
|
+
pk_path = os.path.expanduser(self._private_key_path)
|
|
55
|
+
if not os.path.exists(pk_path):
|
|
56
|
+
raise FileNotFoundError(f"老虎证券私钥文件不存在: {pk_path}")
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
cfg = TigerOpenClientConfig(sandbox_debug=self._sandbox)
|
|
60
|
+
cfg.tiger_id = self._tiger_id
|
|
61
|
+
cfg.private_key = open(pk_path).read()
|
|
62
|
+
cfg.account = self._account
|
|
63
|
+
self._client_config = cfg
|
|
64
|
+
self._trade_client = TradeClient(cfg)
|
|
65
|
+
self._quote_client = QuoteClient(cfg)
|
|
66
|
+
# 测试连接
|
|
67
|
+
self._trade_client.get_managed_accounts()
|
|
68
|
+
self._connected = True
|
|
69
|
+
return True
|
|
70
|
+
except Exception as e:
|
|
71
|
+
raise RuntimeError(f"老虎证券连接失败: {e}") from e
|
|
72
|
+
|
|
73
|
+
def account_info(self) -> AccountInfo:
|
|
74
|
+
self._require_connected()
|
|
75
|
+
try:
|
|
76
|
+
assets = self._trade_client.get_assets(account=self._account)
|
|
77
|
+
a = assets[0] if assets else None
|
|
78
|
+
if not a:
|
|
79
|
+
raise RuntimeError("老虎证券返回空账户数据")
|
|
80
|
+
return AccountInfo(
|
|
81
|
+
broker_id=self.broker_id,
|
|
82
|
+
broker_type=self.broker_type,
|
|
83
|
+
label=self.label,
|
|
84
|
+
account_id=self._account,
|
|
85
|
+
currency=str(getattr(a, "currency", "USD")),
|
|
86
|
+
total_assets=float(getattr(a, "net_liquidation", 0)),
|
|
87
|
+
cash=float(getattr(a, "cash_balance", 0)),
|
|
88
|
+
market_value=float(getattr(a, "gross_position_value", 0)),
|
|
89
|
+
pnl_today=float(getattr(a, "realized_pnl", 0)),
|
|
90
|
+
pnl_total=float(getattr(a, "unrealized_pnl", 0)),
|
|
91
|
+
)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise RuntimeError(f"老虎证券账户查询失败: {e}") from e
|
|
94
|
+
|
|
95
|
+
def positions(self) -> List[Position]:
|
|
96
|
+
self._require_connected()
|
|
97
|
+
try:
|
|
98
|
+
raw = self._trade_client.get_positions(account=self._account)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
raise RuntimeError(f"老虎证券持仓查询失败: {e}") from e
|
|
101
|
+
|
|
102
|
+
result = []
|
|
103
|
+
for p in (raw or []):
|
|
104
|
+
sym = str(getattr(p, "contract", {}).symbol if hasattr(p, "contract") else getattr(p, "symbol", ""))
|
|
105
|
+
qty = float(getattr(p, "quantity", 0))
|
|
106
|
+
avail = float(getattr(p, "available_qty", qty))
|
|
107
|
+
cost = float(getattr(p, "average_cost", 0))
|
|
108
|
+
price = float(getattr(p, "market_price", 0))
|
|
109
|
+
mv = float(getattr(p, "position_value", 0))
|
|
110
|
+
pnl = float(getattr(p, "unrealized_pnl", 0))
|
|
111
|
+
pnl_pct = float(getattr(p, "unrealized_pnl_ratio", 0)) * 100
|
|
112
|
+
currency = str(getattr(p, "currency", "USD"))
|
|
113
|
+
result.append(Position(
|
|
114
|
+
symbol=sym, name=sym,
|
|
115
|
+
quantity=qty, available_qty=avail,
|
|
116
|
+
cost_price=cost, current_price=price,
|
|
117
|
+
market_value=mv, pnl=pnl, pnl_pct=pnl_pct,
|
|
118
|
+
currency=currency, market="us",
|
|
119
|
+
))
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
def orders(self, status: str = "all", limit: int = 50) -> List[Order]:
|
|
123
|
+
self._require_connected()
|
|
124
|
+
try:
|
|
125
|
+
from tigeropen.common.consts import OrderStatus
|
|
126
|
+
raw = self._trade_client.get_orders(account=self._account, limit=limit)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
raise RuntimeError(f"老虎证券订单查询失败: {e}") from e
|
|
129
|
+
|
|
130
|
+
result = []
|
|
131
|
+
for o in (raw or []):
|
|
132
|
+
side = "buy" if str(getattr(o, "action", "")).upper() == "BUY" else "sell"
|
|
133
|
+
mapped = _tiger_order_status(str(getattr(o, "status", "")))
|
|
134
|
+
if status != "all" and mapped != status:
|
|
135
|
+
continue
|
|
136
|
+
contract = getattr(o, "contract", None)
|
|
137
|
+
sym = str(contract.symbol if contract else getattr(o, "symbol", ""))
|
|
138
|
+
result.append(Order(
|
|
139
|
+
order_id=str(getattr(o, "id", getattr(o, "order_id", ""))),
|
|
140
|
+
symbol=sym, name=sym,
|
|
141
|
+
side=side,
|
|
142
|
+
order_type=str(getattr(o, "order_type", "limit")).lower(),
|
|
143
|
+
quantity=float(getattr(o, "quantity", 0)),
|
|
144
|
+
filled_qty=float(getattr(o, "filled", 0)),
|
|
145
|
+
price=float(getattr(o, "limit_price", 0)),
|
|
146
|
+
avg_price=float(getattr(o, "avg_fill_price", 0)),
|
|
147
|
+
status=mapped,
|
|
148
|
+
created_at=str(getattr(o, "order_time", "")),
|
|
149
|
+
currency=str(getattr(o, "currency", "USD")),
|
|
150
|
+
))
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
def place_order(self, symbol: str, side: str, quantity: float,
|
|
154
|
+
order_type: str = "limit", price: float = 0.0, **kwargs) -> OrderResult:
|
|
155
|
+
self._require_connected()
|
|
156
|
+
try:
|
|
157
|
+
from tigeropen.trade.domain.order import market_order, limit_order
|
|
158
|
+
from tigeropen.common.consts import Currency, Market
|
|
159
|
+
|
|
160
|
+
if order_type == "market":
|
|
161
|
+
order = market_order(
|
|
162
|
+
account=self._account, symbol=symbol,
|
|
163
|
+
action=side.upper(), quantity=int(quantity),
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
order = limit_order(
|
|
167
|
+
account=self._account, symbol=symbol,
|
|
168
|
+
action=side.upper(), quantity=int(quantity), limit_price=price,
|
|
169
|
+
)
|
|
170
|
+
ret = self._trade_client.place_order(order)
|
|
171
|
+
return OrderResult(success=True, order_id=str(ret), broker_id=self.broker_id)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
return OrderResult(success=False, message=str(e), broker_id=self.broker_id)
|
|
174
|
+
|
|
175
|
+
def cancel_order(self, order_id: str) -> bool:
|
|
176
|
+
self._require_connected()
|
|
177
|
+
try:
|
|
178
|
+
self._trade_client.cancel_order(account=self._account, id=int(order_id))
|
|
179
|
+
return True
|
|
180
|
+
except Exception:
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
def _require_connected(self):
|
|
184
|
+
if not self._connected:
|
|
185
|
+
raise RuntimeError("老虎证券未连接,请先调用 connect()")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _tiger_order_status(raw: str) -> str:
|
|
189
|
+
raw = raw.upper()
|
|
190
|
+
if raw in ("FILLED",):
|
|
191
|
+
return "filled"
|
|
192
|
+
if raw in ("PARTIALLY_FILLED",):
|
|
193
|
+
return "partial"
|
|
194
|
+
if raw in ("CANCELLED", "EXPIRED", "REJECTED"):
|
|
195
|
+
return "cancelled"
|
|
196
|
+
return "open"
|