moomoo-api-mcp 0.1.6__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.
- moomoo_api_mcp-0.1.6.dist-info/METADATA +457 -0
- moomoo_api_mcp-0.1.6.dist-info/RECORD +17 -0
- moomoo_api_mcp-0.1.6.dist-info/WHEEL +4 -0
- moomoo_api_mcp-0.1.6.dist-info/entry_points.txt +2 -0
- moomoo_api_mcp-0.1.6.dist-info/licenses/LICENSE +201 -0
- moomoo_mcp/__init__.py +0 -0
- moomoo_mcp/resources/__init__.py +0 -0
- moomoo_mcp/server.py +117 -0
- moomoo_mcp/services/__init__.py +0 -0
- moomoo_mcp/services/base_service.py +39 -0
- moomoo_mcp/services/market_data_service.py +219 -0
- moomoo_mcp/services/trade_service.py +691 -0
- moomoo_mcp/tools/__init__.py +0 -0
- moomoo_mcp/tools/account.py +296 -0
- moomoo_mcp/tools/market_data.py +212 -0
- moomoo_mcp/tools/system.py +20 -0
- moomoo_mcp/tools/trading.py +328 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
"""Trade service for managing Moomoo trading context and account operations."""
|
|
2
|
+
|
|
3
|
+
from moomoo import OpenSecTradeContext, OrderStatus, RET_OK, SecurityFirm, TrdMarket
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TradeService:
|
|
7
|
+
"""Service to manage Moomoo Trade API connections and account operations."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
host: str = "127.0.0.1",
|
|
12
|
+
port: int = 11111,
|
|
13
|
+
security_firm: str | None = None,
|
|
14
|
+
):
|
|
15
|
+
"""Initialize TradeService.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
host: Host address of OpenD gateway.
|
|
19
|
+
port: Port number of OpenD gateway.
|
|
20
|
+
security_firm: Securities firm identifier (e.g., 'FUTUSG' for Singapore,
|
|
21
|
+
'FUTUSECURITIES' for HK). If None, no filter is applied.
|
|
22
|
+
"""
|
|
23
|
+
self.host = host
|
|
24
|
+
self.port = port
|
|
25
|
+
self.security_firm = security_firm
|
|
26
|
+
self.trade_ctx: OpenSecTradeContext | None = None
|
|
27
|
+
|
|
28
|
+
def _convert_status_filter(
|
|
29
|
+
self, status_filter_list: list[str] | None
|
|
30
|
+
) -> list[OrderStatus]:
|
|
31
|
+
"""Convert string status values to OrderStatus enum values.
|
|
32
|
+
|
|
33
|
+
The Moomoo SDK expects OrderStatus enum values, not strings.
|
|
34
|
+
This method converts user-provided string values to the proper enum format.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
status_filter_list: List of status strings like ['SUBMITTED', 'FILLED_ALL'].
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
List of OrderStatus enum values. Returns an empty list if input is None.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
ValueError: If an invalid status string is provided.
|
|
44
|
+
"""
|
|
45
|
+
if status_filter_list is None:
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
converted = []
|
|
49
|
+
for status_str in status_filter_list:
|
|
50
|
+
# OrderStatus has the attribute matching the string (e.g., OrderStatus.SUBMITTED)
|
|
51
|
+
status_enum = getattr(OrderStatus, status_str.upper(), None)
|
|
52
|
+
if status_enum is None:
|
|
53
|
+
valid_statuses = [
|
|
54
|
+
"UNSUBMITTED", "WAITING_SUBMIT", "SUBMITTING", "SUBMIT_FAILED",
|
|
55
|
+
"SUBMITTED", "FILLED_PART", "FILLED_ALL",
|
|
56
|
+
"CANCELLING_PART", "CANCELLING_ALL", "CANCELLED_PART", "CANCELLED_ALL",
|
|
57
|
+
"REJECTED", "DISABLED", "DELETED", "FAILED", "NONE"
|
|
58
|
+
]
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Invalid order status: '{status_str}'. "
|
|
61
|
+
f"Valid options: {valid_statuses}"
|
|
62
|
+
)
|
|
63
|
+
converted.append(status_enum)
|
|
64
|
+
return converted
|
|
65
|
+
|
|
66
|
+
def _get_market_from_code(self, code: str) -> str | None:
|
|
67
|
+
"""Extract market from stock code (e.g., 'JP' from 'JP.8058')."""
|
|
68
|
+
if "." in code:
|
|
69
|
+
return code.split(".")[0].upper()
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def _find_best_account(self, trd_env: str, market: str) -> int:
|
|
73
|
+
"""Find the best account for the given environment and market.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
trd_env: Trading environment ('REAL' or 'SIMULATE').
|
|
77
|
+
market: Target market (e.g., 'JP', 'US', 'HK').
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Account ID if found, otherwise 0 (default).
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ValueError: If no suitable account is found.
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
accounts = self.get_accounts()
|
|
87
|
+
except Exception as e:
|
|
88
|
+
# Re-raise as a ValueError to ensure the caller knows account finding failed.
|
|
89
|
+
raise ValueError("Failed to retrieve account list from the API.") from e
|
|
90
|
+
|
|
91
|
+
# Filter by environment
|
|
92
|
+
env_accounts = [acc for acc in accounts if acc.get("trd_env") == trd_env]
|
|
93
|
+
|
|
94
|
+
if not env_accounts:
|
|
95
|
+
# Raise an error if no accounts are found for the environment.
|
|
96
|
+
raise ValueError(f"No accounts found for the '{trd_env}' environment.")
|
|
97
|
+
|
|
98
|
+
# Moomoo market codes mapping to market_auth strings
|
|
99
|
+
# Adjust as needed based on actual API values
|
|
100
|
+
target_market = market.upper()
|
|
101
|
+
|
|
102
|
+
supported_markets = []
|
|
103
|
+
|
|
104
|
+
for acc in env_accounts:
|
|
105
|
+
# Check market_auth which is a list like ['HK', 'US']
|
|
106
|
+
# Note: The field name might be 'trdmarket_auth' based on debug output
|
|
107
|
+
market_auth = acc.get("market_auth") or acc.get("trdmarket_auth") or []
|
|
108
|
+
supported_markets.extend(market_auth)
|
|
109
|
+
|
|
110
|
+
if target_market in market_auth:
|
|
111
|
+
return acc["acc_id"]
|
|
112
|
+
|
|
113
|
+
# If we are here, we found accounts for the env, but none support the market
|
|
114
|
+
unique_supported = sorted(list(set(supported_markets)))
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"No account found in {trd_env} environment that supports trading in {market}. "
|
|
117
|
+
f"Available accounts support: {unique_supported}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def connect(self) -> None:
|
|
121
|
+
"""Initialize connection to OpenD trade context."""
|
|
122
|
+
# Build kwargs for OpenSecTradeContext
|
|
123
|
+
kwargs = {"host": self.host, "port": self.port}
|
|
124
|
+
|
|
125
|
+
# Add security_firm if specified
|
|
126
|
+
if self.security_firm:
|
|
127
|
+
# Convert string to SecurityFirm enum
|
|
128
|
+
firm_enum = getattr(SecurityFirm, self.security_firm, None)
|
|
129
|
+
if firm_enum:
|
|
130
|
+
kwargs["security_firm"] = firm_enum
|
|
131
|
+
|
|
132
|
+
self.trade_ctx = OpenSecTradeContext(**kwargs)
|
|
133
|
+
|
|
134
|
+
def close(self) -> None:
|
|
135
|
+
"""Close trade context connection."""
|
|
136
|
+
if self.trade_ctx:
|
|
137
|
+
self.trade_ctx.close()
|
|
138
|
+
self.trade_ctx = None
|
|
139
|
+
|
|
140
|
+
def get_accounts(self) -> list[dict]:
|
|
141
|
+
"""Get list of trading accounts.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
List of account dictionaries with acc_id, trd_env, etc.
|
|
145
|
+
"""
|
|
146
|
+
if not self.trade_ctx:
|
|
147
|
+
raise RuntimeError("Trade context not connected")
|
|
148
|
+
|
|
149
|
+
ret, data = self.trade_ctx.get_acc_list()
|
|
150
|
+
if ret != RET_OK:
|
|
151
|
+
raise RuntimeError(f"get_acc_list failed: {data}")
|
|
152
|
+
|
|
153
|
+
return data.to_dict("records")
|
|
154
|
+
|
|
155
|
+
def get_assets(
|
|
156
|
+
self,
|
|
157
|
+
trd_env: str = "SIMULATE",
|
|
158
|
+
acc_id: int = 0,
|
|
159
|
+
refresh_cache: bool = False,
|
|
160
|
+
currency: str | None = None,
|
|
161
|
+
) -> dict:
|
|
162
|
+
"""Get account assets (cash, market value, etc.).
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
trd_env: Trading environment, 'REAL' or 'SIMULATE'.
|
|
166
|
+
acc_id: Account ID. Must be obtained from get_accounts().
|
|
167
|
+
refresh_cache: Whether to refresh the cache.
|
|
168
|
+
currency: Filter by currency (e.g., 'HKD', 'USD'). Leave None for default.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Dictionary with asset information.
|
|
172
|
+
"""
|
|
173
|
+
if not self.trade_ctx:
|
|
174
|
+
raise RuntimeError("Trade context not connected")
|
|
175
|
+
|
|
176
|
+
kwargs = {
|
|
177
|
+
"trd_env": trd_env,
|
|
178
|
+
"acc_id": acc_id,
|
|
179
|
+
"refresh_cache": refresh_cache,
|
|
180
|
+
}
|
|
181
|
+
if currency is not None:
|
|
182
|
+
normalized_currency = currency.strip().upper()
|
|
183
|
+
if normalized_currency:
|
|
184
|
+
kwargs["currency"] = normalized_currency
|
|
185
|
+
|
|
186
|
+
ret, data = self.trade_ctx.accinfo_query(**kwargs)
|
|
187
|
+
if ret != RET_OK:
|
|
188
|
+
raise RuntimeError(f"accinfo_query failed: {data}")
|
|
189
|
+
|
|
190
|
+
records = data.to_dict("records")
|
|
191
|
+
return records[0] if records else {}
|
|
192
|
+
|
|
193
|
+
def get_positions(
|
|
194
|
+
self,
|
|
195
|
+
code: str = "",
|
|
196
|
+
market: str = "",
|
|
197
|
+
pl_ratio_min: float | None = None,
|
|
198
|
+
pl_ratio_max: float | None = None,
|
|
199
|
+
trd_env: str = "SIMULATE",
|
|
200
|
+
acc_id: int = 0,
|
|
201
|
+
refresh_cache: bool = False,
|
|
202
|
+
) -> list[dict]:
|
|
203
|
+
"""Get current positions.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
code: Filter by stock code.
|
|
207
|
+
market: Filter by market (e.g., 'US', 'HK', 'CN', 'SG', 'JP').
|
|
208
|
+
pl_ratio_min: Minimum profit/loss ratio filter.
|
|
209
|
+
pl_ratio_max: Maximum profit/loss ratio filter.
|
|
210
|
+
trd_env: Trading environment.
|
|
211
|
+
acc_id: Account ID. Must be obtained from get_accounts().
|
|
212
|
+
refresh_cache: Whether to refresh cache.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
List of position dictionaries.
|
|
216
|
+
"""
|
|
217
|
+
if not self.trade_ctx:
|
|
218
|
+
raise RuntimeError("Trade context not connected")
|
|
219
|
+
|
|
220
|
+
# Map market string to TrdMarket enum
|
|
221
|
+
# Note: CN is for A-share simulation only; HKCC for Stock Connect (live only)
|
|
222
|
+
market_map = {
|
|
223
|
+
"US": TrdMarket.US,
|
|
224
|
+
"HK": TrdMarket.HK,
|
|
225
|
+
"CN": TrdMarket.CN,
|
|
226
|
+
"HKCC": TrdMarket.HKCC,
|
|
227
|
+
"SG": TrdMarket.SG,
|
|
228
|
+
"JP": TrdMarket.JP,
|
|
229
|
+
}
|
|
230
|
+
position_market = TrdMarket.NONE
|
|
231
|
+
if market:
|
|
232
|
+
try:
|
|
233
|
+
# Try direct map first
|
|
234
|
+
position_market = market_map.get(market.upper())
|
|
235
|
+
if position_market is None:
|
|
236
|
+
# Try to use getattr for other potential values
|
|
237
|
+
position_market = getattr(TrdMarket, market.upper())
|
|
238
|
+
except AttributeError:
|
|
239
|
+
position_market = TrdMarket.NONE
|
|
240
|
+
|
|
241
|
+
ret, data = self.trade_ctx.position_list_query(
|
|
242
|
+
code=code,
|
|
243
|
+
position_market=position_market,
|
|
244
|
+
pl_ratio_min=pl_ratio_min,
|
|
245
|
+
pl_ratio_max=pl_ratio_max,
|
|
246
|
+
trd_env=trd_env,
|
|
247
|
+
acc_id=acc_id,
|
|
248
|
+
refresh_cache=refresh_cache,
|
|
249
|
+
)
|
|
250
|
+
if ret != RET_OK:
|
|
251
|
+
raise RuntimeError(f"position_list_query failed: {data}")
|
|
252
|
+
|
|
253
|
+
return data.to_dict("records")
|
|
254
|
+
|
|
255
|
+
def get_max_tradable(
|
|
256
|
+
self,
|
|
257
|
+
order_type: str,
|
|
258
|
+
code: str,
|
|
259
|
+
price: float,
|
|
260
|
+
order_id: str = "",
|
|
261
|
+
adjust_limit: float = 0,
|
|
262
|
+
trd_env: str = "SIMULATE",
|
|
263
|
+
acc_id: int = 0,
|
|
264
|
+
) -> dict:
|
|
265
|
+
"""Get maximum tradable quantity for a stock.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
order_type: Order type string (e.g., 'NORMAL').
|
|
269
|
+
code: Stock code.
|
|
270
|
+
price: Target price.
|
|
271
|
+
order_id: Optional order ID for modification.
|
|
272
|
+
adjust_limit: Adjust limit percentage.
|
|
273
|
+
trd_env: Trading environment.
|
|
274
|
+
acc_id: Account ID. Must be obtained from get_accounts().
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Dictionary with max quantities for buy/sell.
|
|
278
|
+
"""
|
|
279
|
+
if not self.trade_ctx:
|
|
280
|
+
raise RuntimeError("Trade context not connected")
|
|
281
|
+
|
|
282
|
+
ret, data = self.trade_ctx.acctradinginfo_query(
|
|
283
|
+
order_type=order_type,
|
|
284
|
+
code=code,
|
|
285
|
+
price=price,
|
|
286
|
+
order_id=order_id,
|
|
287
|
+
adjust_limit=adjust_limit,
|
|
288
|
+
trd_env=trd_env,
|
|
289
|
+
acc_id=acc_id,
|
|
290
|
+
)
|
|
291
|
+
if ret != RET_OK:
|
|
292
|
+
raise RuntimeError(f"acctradinginfo_query failed: {data}")
|
|
293
|
+
|
|
294
|
+
records = data.to_dict("records")
|
|
295
|
+
return records[0] if records else {}
|
|
296
|
+
|
|
297
|
+
def get_margin_ratio(self, code_list: list[str]) -> list[dict]:
|
|
298
|
+
"""Get margin ratio for stocks.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
code_list: List of stock codes.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
List of margin ratio dictionaries.
|
|
305
|
+
"""
|
|
306
|
+
if not self.trade_ctx:
|
|
307
|
+
raise RuntimeError("Trade context not connected")
|
|
308
|
+
|
|
309
|
+
ret, data = self.trade_ctx.get_margin_ratio(code_list=code_list)
|
|
310
|
+
if ret != RET_OK:
|
|
311
|
+
raise RuntimeError(f"get_margin_ratio failed: {data}")
|
|
312
|
+
|
|
313
|
+
return data.to_dict("records")
|
|
314
|
+
|
|
315
|
+
def get_cash_flow(
|
|
316
|
+
self,
|
|
317
|
+
clearing_date: str = "",
|
|
318
|
+
trd_env: str = "SIMULATE",
|
|
319
|
+
acc_id: int = 0,
|
|
320
|
+
) -> list[dict]:
|
|
321
|
+
"""Get account cash flow history.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
clearing_date: Filter by clearing date (YYYY-MM-DD).
|
|
325
|
+
trd_env: Trading environment.
|
|
326
|
+
acc_id: Account ID. Must be obtained from get_accounts().
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
List of cash flow record dictionaries.
|
|
330
|
+
"""
|
|
331
|
+
if not self.trade_ctx:
|
|
332
|
+
raise RuntimeError("Trade context not connected")
|
|
333
|
+
|
|
334
|
+
ret, data = self.trade_ctx.get_acc_cash_flow(
|
|
335
|
+
clearing_date=clearing_date,
|
|
336
|
+
trd_env=trd_env,
|
|
337
|
+
acc_id=acc_id,
|
|
338
|
+
)
|
|
339
|
+
if ret != RET_OK:
|
|
340
|
+
raise RuntimeError(f"get_acc_cash_flow failed: {data}")
|
|
341
|
+
|
|
342
|
+
return data.to_dict("records")
|
|
343
|
+
|
|
344
|
+
def unlock_trade(
|
|
345
|
+
self, password: str | None = None, password_md5: str | None = None
|
|
346
|
+
) -> None:
|
|
347
|
+
"""Unlock trade for trading operations.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
password: Plain text trade password.
|
|
351
|
+
password_md5: MD5 hash of trade password (alternative to password).
|
|
352
|
+
|
|
353
|
+
Raises:
|
|
354
|
+
RuntimeError: If unlock fails.
|
|
355
|
+
"""
|
|
356
|
+
if not self.trade_ctx:
|
|
357
|
+
raise RuntimeError("Trade context not connected")
|
|
358
|
+
|
|
359
|
+
ret, data = self.trade_ctx.unlock_trade(
|
|
360
|
+
password=password,
|
|
361
|
+
password_md5=password_md5,
|
|
362
|
+
is_unlock=True,
|
|
363
|
+
)
|
|
364
|
+
if ret != RET_OK:
|
|
365
|
+
raise RuntimeError(f"unlock_trade failed: {data}")
|
|
366
|
+
|
|
367
|
+
def place_order(
|
|
368
|
+
self,
|
|
369
|
+
code: str,
|
|
370
|
+
price: float,
|
|
371
|
+
qty: int,
|
|
372
|
+
trd_side: str,
|
|
373
|
+
order_type: str = "NORMAL",
|
|
374
|
+
time_in_force: str = "DAY",
|
|
375
|
+
adjust_limit: float = 0,
|
|
376
|
+
aux_price: float | None = None,
|
|
377
|
+
trail_type: str | None = None,
|
|
378
|
+
trail_value: float | None = None,
|
|
379
|
+
trail_spread: float | None = None,
|
|
380
|
+
trd_env: str = "SIMULATE",
|
|
381
|
+
acc_id: int = 0,
|
|
382
|
+
remark: str = "",
|
|
383
|
+
) -> dict:
|
|
384
|
+
"""Place a new trading order.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
code: Stock code (e.g., 'US.AAPL').
|
|
388
|
+
price: Order price.
|
|
389
|
+
qty: Order quantity.
|
|
390
|
+
trd_side: Trade side ('BUY' or 'SELL').
|
|
391
|
+
order_type: Order type ('NORMAL', 'MARKET', etc.).
|
|
392
|
+
time_in_force: Time in force ('DAY' or 'GTC'). Defaults to 'DAY'.
|
|
393
|
+
adjust_limit: Adjust limit percentage.
|
|
394
|
+
aux_price: Trigger price for stop/if-touched order types.
|
|
395
|
+
trail_type: Trailing type ('RATIO' or 'AMOUNT') for trailing stop types.
|
|
396
|
+
trail_value: Trailing value (ratio or amount) for trailing stop types.
|
|
397
|
+
trail_spread: Optional trailing spread for trailing stop limit types.
|
|
398
|
+
trd_env: Trading environment ('REAL' or 'SIMULATE').
|
|
399
|
+
acc_id: Account ID. Must be obtained from get_accounts().
|
|
400
|
+
remark: Order remark/note.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Dictionary with order details including order_id.
|
|
404
|
+
"""
|
|
405
|
+
if not self.trade_ctx:
|
|
406
|
+
raise RuntimeError("Trade context not connected")
|
|
407
|
+
|
|
408
|
+
# Smart account selection if acc_id is default (0)
|
|
409
|
+
if acc_id == 0:
|
|
410
|
+
market = self._get_market_from_code(code)
|
|
411
|
+
if market:
|
|
412
|
+
# Try to find a specific account for this market
|
|
413
|
+
# If valid account found, use it.
|
|
414
|
+
# If none found that support the market, it will raise ValueError
|
|
415
|
+
acc_id = self._find_best_account(trd_env, market)
|
|
416
|
+
|
|
417
|
+
stop_order_types = {
|
|
418
|
+
"STOP",
|
|
419
|
+
"STOP_LIMIT",
|
|
420
|
+
"MARKET_IF_TOUCHED",
|
|
421
|
+
"LIMIT_IF_TOUCHED",
|
|
422
|
+
}
|
|
423
|
+
trailing_order_types = {"TRAILING_STOP", "TRAILING_STOP_LIMIT"}
|
|
424
|
+
if order_type in stop_order_types and aux_price is None:
|
|
425
|
+
raise ValueError("aux_price is required for stop/if-touched order types")
|
|
426
|
+
if order_type in trailing_order_types and (
|
|
427
|
+
trail_type is None or trail_value is None
|
|
428
|
+
):
|
|
429
|
+
raise ValueError(
|
|
430
|
+
"trail_type and trail_value are required for trailing stop order types"
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
ret, data = self.trade_ctx.place_order(
|
|
434
|
+
price=price,
|
|
435
|
+
qty=qty,
|
|
436
|
+
code=code,
|
|
437
|
+
trd_side=trd_side,
|
|
438
|
+
order_type=order_type,
|
|
439
|
+
time_in_force=time_in_force,
|
|
440
|
+
adjust_limit=adjust_limit,
|
|
441
|
+
aux_price=aux_price,
|
|
442
|
+
trail_type=trail_type,
|
|
443
|
+
trail_value=trail_value,
|
|
444
|
+
trail_spread=trail_spread,
|
|
445
|
+
trd_env=trd_env,
|
|
446
|
+
acc_id=acc_id,
|
|
447
|
+
remark=remark,
|
|
448
|
+
)
|
|
449
|
+
if ret != RET_OK:
|
|
450
|
+
raise RuntimeError(f"place_order failed: {data}")
|
|
451
|
+
|
|
452
|
+
records = data.to_dict("records")
|
|
453
|
+
return records[0] if records else {}
|
|
454
|
+
|
|
455
|
+
def modify_order(
|
|
456
|
+
self,
|
|
457
|
+
order_id: str,
|
|
458
|
+
modify_order_op: str,
|
|
459
|
+
qty: int | None = None,
|
|
460
|
+
price: float | None = None,
|
|
461
|
+
adjust_limit: float = 0,
|
|
462
|
+
trd_env: str = "SIMULATE",
|
|
463
|
+
acc_id: int = 0,
|
|
464
|
+
) -> dict:
|
|
465
|
+
"""Modify an existing order.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
order_id: Order ID to modify.
|
|
469
|
+
modify_order_op: Modification operation ('NORMAL', 'CANCEL', 'DISABLE', 'ENABLE', 'DELETE').
|
|
470
|
+
qty: New quantity (optional).
|
|
471
|
+
price: New price (optional).
|
|
472
|
+
adjust_limit: Adjust limit percentage.
|
|
473
|
+
trd_env: Trading environment.
|
|
474
|
+
acc_id: Account ID.
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
Dictionary with modified order details.
|
|
478
|
+
"""
|
|
479
|
+
if not self.trade_ctx:
|
|
480
|
+
raise RuntimeError("Trade context not connected")
|
|
481
|
+
|
|
482
|
+
ret, data = self.trade_ctx.modify_order(
|
|
483
|
+
modify_order_op=modify_order_op,
|
|
484
|
+
order_id=order_id,
|
|
485
|
+
qty=qty,
|
|
486
|
+
price=price,
|
|
487
|
+
adjust_limit=adjust_limit,
|
|
488
|
+
trd_env=trd_env,
|
|
489
|
+
acc_id=acc_id,
|
|
490
|
+
)
|
|
491
|
+
if ret != RET_OK:
|
|
492
|
+
raise RuntimeError(f"modify_order failed: {data}")
|
|
493
|
+
|
|
494
|
+
records = data.to_dict("records")
|
|
495
|
+
return records[0] if records else {}
|
|
496
|
+
|
|
497
|
+
def cancel_order(
|
|
498
|
+
self,
|
|
499
|
+
order_id: str,
|
|
500
|
+
trd_env: str = "SIMULATE",
|
|
501
|
+
acc_id: int = 0,
|
|
502
|
+
) -> dict:
|
|
503
|
+
"""Cancel an existing order.
|
|
504
|
+
|
|
505
|
+
Convenience wrapper around modify_order with CANCEL operation.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
order_id: Order ID to cancel.
|
|
509
|
+
trd_env: Trading environment.
|
|
510
|
+
acc_id: Account ID.
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
Dictionary with cancelled order details.
|
|
514
|
+
"""
|
|
515
|
+
if not self.trade_ctx:
|
|
516
|
+
raise RuntimeError("Trade context not connected")
|
|
517
|
+
|
|
518
|
+
ret, data = self.trade_ctx.modify_order(
|
|
519
|
+
modify_order_op="CANCEL",
|
|
520
|
+
order_id=order_id,
|
|
521
|
+
qty=0,
|
|
522
|
+
price=0,
|
|
523
|
+
adjust_limit=0,
|
|
524
|
+
trd_env=trd_env,
|
|
525
|
+
acc_id=acc_id,
|
|
526
|
+
)
|
|
527
|
+
if ret != RET_OK:
|
|
528
|
+
raise RuntimeError(f"cancel_order failed: {data}")
|
|
529
|
+
|
|
530
|
+
records = data.to_dict("records")
|
|
531
|
+
return records[0] if records else {}
|
|
532
|
+
|
|
533
|
+
def get_orders(
|
|
534
|
+
self,
|
|
535
|
+
code: str = "",
|
|
536
|
+
status_filter_list: list[str] | None = None,
|
|
537
|
+
trd_env: str = "SIMULATE",
|
|
538
|
+
acc_id: int = 0,
|
|
539
|
+
refresh_cache: bool = False,
|
|
540
|
+
) -> list[dict]:
|
|
541
|
+
"""Get list of today's orders.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
code: Filter by stock code.
|
|
545
|
+
status_filter_list: Filter by order statuses (as strings).
|
|
546
|
+
Valid options: UNSUBMITTED, WAITING_SUBMIT, SUBMITTING, SUBMIT_FAILED,
|
|
547
|
+
SUBMITTED, FILLED_PART, FILLED_ALL, CANCELLING_PART, CANCELLING_ALL,
|
|
548
|
+
CANCELLED_PART, CANCELLED_ALL, REJECTED, DISABLED, DELETED, FAILED, NONE.
|
|
549
|
+
trd_env: Trading environment.
|
|
550
|
+
acc_id: Account ID.
|
|
551
|
+
refresh_cache: Whether to refresh cache.
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
List of order dictionaries. Returns empty list if no orders found.
|
|
555
|
+
"""
|
|
556
|
+
if not self.trade_ctx:
|
|
557
|
+
raise RuntimeError("Trade context not connected")
|
|
558
|
+
|
|
559
|
+
# Convert string status values to OrderStatus enum values
|
|
560
|
+
converted_status_filter = self._convert_status_filter(status_filter_list)
|
|
561
|
+
|
|
562
|
+
ret, data = self.trade_ctx.order_list_query(
|
|
563
|
+
code=code,
|
|
564
|
+
status_filter_list=converted_status_filter,
|
|
565
|
+
trd_env=trd_env,
|
|
566
|
+
acc_id=acc_id,
|
|
567
|
+
refresh_cache=refresh_cache,
|
|
568
|
+
)
|
|
569
|
+
if ret != RET_OK:
|
|
570
|
+
raise RuntimeError(f"order_list_query failed: {data}")
|
|
571
|
+
|
|
572
|
+
# Handle None or empty DataFrame gracefully
|
|
573
|
+
if data is None or data.empty:
|
|
574
|
+
return []
|
|
575
|
+
|
|
576
|
+
return data.to_dict("records")
|
|
577
|
+
|
|
578
|
+
def get_deals(
|
|
579
|
+
self,
|
|
580
|
+
code: str = "",
|
|
581
|
+
trd_env: str = "SIMULATE",
|
|
582
|
+
acc_id: int = 0,
|
|
583
|
+
refresh_cache: bool = False,
|
|
584
|
+
) -> list[dict]:
|
|
585
|
+
"""Get list of today's deals (executed trades).
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
code: Filter by stock code.
|
|
589
|
+
trd_env: Trading environment.
|
|
590
|
+
acc_id: Account ID.
|
|
591
|
+
refresh_cache: Whether to refresh cache.
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
List of deal dictionaries.
|
|
595
|
+
"""
|
|
596
|
+
if not self.trade_ctx:
|
|
597
|
+
raise RuntimeError("Trade context not connected")
|
|
598
|
+
|
|
599
|
+
ret, data = self.trade_ctx.deal_list_query(
|
|
600
|
+
code=code,
|
|
601
|
+
trd_env=trd_env,
|
|
602
|
+
acc_id=acc_id,
|
|
603
|
+
refresh_cache=refresh_cache,
|
|
604
|
+
)
|
|
605
|
+
if ret != RET_OK:
|
|
606
|
+
raise RuntimeError(f"deal_list_query failed: {data}")
|
|
607
|
+
|
|
608
|
+
return data.to_dict("records")
|
|
609
|
+
|
|
610
|
+
def get_history_orders(
|
|
611
|
+
self,
|
|
612
|
+
code: str = "",
|
|
613
|
+
status_filter_list: list[str] | None = None,
|
|
614
|
+
start: str = "",
|
|
615
|
+
end: str = "",
|
|
616
|
+
trd_env: str = "SIMULATE",
|
|
617
|
+
acc_id: int = 0,
|
|
618
|
+
) -> list[dict]:
|
|
619
|
+
"""Get historical orders.
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
code: Filter by stock code.
|
|
623
|
+
status_filter_list: Filter by order statuses (as strings).
|
|
624
|
+
Valid options: UNSUBMITTED, WAITING_SUBMIT, SUBMITTING, SUBMIT_FAILED,
|
|
625
|
+
SUBMITTED, FILLED_PART, FILLED_ALL, CANCELLING_PART, CANCELLING_ALL,
|
|
626
|
+
CANCELLED_PART, CANCELLED_ALL, REJECTED, DISABLED, DELETED, FAILED, NONE.
|
|
627
|
+
start: Start date (YYYY-MM-DD).
|
|
628
|
+
end: End date (YYYY-MM-DD).
|
|
629
|
+
trd_env: Trading environment.
|
|
630
|
+
acc_id: Account ID.
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
List of historical order dictionaries. Returns empty list if no orders found.
|
|
634
|
+
"""
|
|
635
|
+
if not self.trade_ctx:
|
|
636
|
+
raise RuntimeError("Trade context not connected")
|
|
637
|
+
|
|
638
|
+
# Convert string status values to OrderStatus enum values
|
|
639
|
+
converted_status_filter = self._convert_status_filter(status_filter_list)
|
|
640
|
+
|
|
641
|
+
ret, data = self.trade_ctx.history_order_list_query(
|
|
642
|
+
code=code,
|
|
643
|
+
status_filter_list=converted_status_filter,
|
|
644
|
+
start=start,
|
|
645
|
+
end=end,
|
|
646
|
+
trd_env=trd_env,
|
|
647
|
+
acc_id=acc_id,
|
|
648
|
+
)
|
|
649
|
+
if ret != RET_OK:
|
|
650
|
+
raise RuntimeError(f"history_order_list_query failed: {data}")
|
|
651
|
+
|
|
652
|
+
# Handle None or empty DataFrame gracefully
|
|
653
|
+
if data is None or data.empty:
|
|
654
|
+
return []
|
|
655
|
+
|
|
656
|
+
return data.to_dict("records")
|
|
657
|
+
|
|
658
|
+
def get_history_deals(
|
|
659
|
+
self,
|
|
660
|
+
code: str = "",
|
|
661
|
+
start: str = "",
|
|
662
|
+
end: str = "",
|
|
663
|
+
trd_env: str = "SIMULATE",
|
|
664
|
+
acc_id: int = 0,
|
|
665
|
+
) -> list[dict]:
|
|
666
|
+
"""Get historical deals (executed trades).
|
|
667
|
+
|
|
668
|
+
Args:
|
|
669
|
+
code: Filter by stock code.
|
|
670
|
+
start: Start date (YYYY-MM-DD).
|
|
671
|
+
end: End date (YYYY-MM-DD).
|
|
672
|
+
trd_env: Trading environment.
|
|
673
|
+
acc_id: Account ID.
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
List of historical deal dictionaries.
|
|
677
|
+
"""
|
|
678
|
+
if not self.trade_ctx:
|
|
679
|
+
raise RuntimeError("Trade context not connected")
|
|
680
|
+
|
|
681
|
+
ret, data = self.trade_ctx.history_deal_list_query(
|
|
682
|
+
code=code,
|
|
683
|
+
start=start,
|
|
684
|
+
end=end,
|
|
685
|
+
trd_env=trd_env,
|
|
686
|
+
acc_id=acc_id,
|
|
687
|
+
)
|
|
688
|
+
if ret != RET_OK:
|
|
689
|
+
raise RuntimeError(f"history_deal_list_query failed: {data}")
|
|
690
|
+
|
|
691
|
+
return data.to_dict("records")
|
|
File without changes
|