-
Notifications
You must be signed in to change notification settings - Fork 33
Expand file tree
/
Copy pathtrading.py
More file actions
416 lines (342 loc) · 16.5 KB
/
Copy pathtrading.py
File metadata and controls
416 lines (342 loc) · 16.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
import logging
import os
from typing import TYPE_CHECKING, List
import shioaji as sj
from shioaji.contracts import Contract
from shioaji.error import (
TokenError,
SystemMaintenance,
TimeoutError as SjTimeoutError,
AccountNotSignError,
AccountNotProvideError,
TargetContractNotExistError,
)
logger = logging.getLogger(__name__)
# Supported futures products (configurable via ENV)
# Default: MXF (小台), TXF (大台)
# Available options: MXF, TXF, EXF, FXF, etc.
SUPPORTED_FUTURES = os.getenv("SUPPORTED_FUTURES", "MXF,TXF").split(",")
SUPPORTED_FUTURES = [f.strip().upper() for f in SUPPORTED_FUTURES if f.strip()]
logger.info(f"Supported futures: {SUPPORTED_FUTURES}")
class ShioajiError(Exception):
"""Base exception for Shioaji operations."""
pass
class LoginError(ShioajiError):
"""Raised when login fails."""
pass
class OrderError(ShioajiError):
"""Raised when order placement fails."""
pass
def get_api_client(simulation: bool = True):
logger.debug(f"Creating API client with simulation={simulation}")
api_key = os.getenv("API_KEY")
secret_key = os.getenv("SECRET_KEY")
if not api_key or not secret_key:
logger.error("API_KEY or SECRET_KEY environment variable not set")
raise LoginError("API_KEY or SECRET_KEY environment variable not set")
try:
api = sj.Shioaji(simulation=simulation)
api.login(api_key=api_key, secret_key=secret_key)
logger.debug("API client logged in successfully")
# Activate CA certificate for real trading
if not simulation:
ca_path = os.getenv("CA_PATH")
ca_password = os.getenv("CA_PASSWORD")
if not ca_path or not ca_password:
logger.error("CA_PATH or CA_PASSWORD not set for real trading")
raise LoginError(
"Real trading requires CA certificate. "
"Please set CA_PATH and CA_PASSWORD environment variables."
)
# Get person_id from account (Taiwan National ID / 身分證字號)
# It's automatically available after login
accounts = api.list_accounts()
if not accounts:
raise LoginError("No accounts found after login")
person_id = accounts[0].person_id
logger.info(f"Activating CA certificate from {ca_path} for person_id={person_id}")
result = api.activate_ca(
ca_path=ca_path,
ca_passwd=ca_password,
person_id=person_id,
)
logger.info(f"CA activation result: {result}")
return api
except TokenError as e:
logger.error(f"Authentication failed: {e}")
raise LoginError(f"Authentication failed: {e}") from e
except SystemMaintenance as e:
logger.error(f"System is under maintenance: {e}")
raise LoginError(f"System is under maintenance: {e}") from e
except SjTimeoutError as e:
logger.error(f"Login timeout: {e}")
raise LoginError(f"Login timeout: {e}") from e
except Exception as e:
logger.error(f"Unexpected error during login: {e}")
raise LoginError(f"Unexpected error during login: {e}") from e
def _get_futures_contracts(api: sj.Shioaji) -> List[Contract]:
"""Get all contracts from supported futures products."""
contracts = []
for product in SUPPORTED_FUTURES:
product_contracts = getattr(api.Contracts.Futures, product, None)
if product_contracts:
contracts.extend([c for c in product_contracts if c.symbol.startswith(product)])
else:
logger.warning(f"Futures product '{product}' not found in api.Contracts.Futures")
return contracts
def get_valid_symbols(api: sj.Shioaji) -> List[str]:
"""Get all valid trading symbols from supported futures."""
return [contract.symbol for contract in _get_futures_contracts(api)]
def get_valid_symbols_with_info(api: sj.Shioaji) -> List[dict]:
"""
Get all valid trading symbols with their codes from supported futures.
Returns list of dicts with:
- symbol: MXF202601 (YYYYMM format) - use this for trading
- code: MXFA6 (month letter + year digit format)
- name: Contract name (e.g., 小型臺指01)
"""
return [
{
"symbol": contract.symbol,
"code": contract.code,
"name": contract.name,
}
for contract in _get_futures_contracts(api)
]
def get_valid_contract_codes(api: sj.Shioaji) -> List[str]:
"""Get all valid contract codes from supported futures."""
return [contract.code for contract in _get_futures_contracts(api)]
def get_contract_from_symbol(api: sj.Shioaji, symbol: str) -> Contract:
"""Find a contract by its symbol."""
for contract in _get_futures_contracts(api):
if contract.symbol == symbol:
return contract
raise ValueError(f"Contract {symbol} not found in supported futures: {SUPPORTED_FUTURES}")
def get_contract_from_contract_code(api: sj.Shioaji, contract_code: str) -> Contract:
"""Find a contract by its contract code."""
for contract in _get_futures_contracts(api):
if contract.code == contract_code:
return contract
raise ValueError(f"Contract {contract_code} not found in supported futures: {SUPPORTED_FUTURES}")
def resolve_actual_contract_code(api: sj.Shioaji, contract: Contract) -> str:
"""
Resolve a contract to its actual trading code.
Rolling contracts (like MXFR1, TXFR1) have code == symbol (e.g., "MXFR1"),
but positions are stored with the actual contract code (e.g., "MXFA6").
This function resolves rolling contracts to their actual code by matching
category + delivery_month with actual contracts.
Args:
api: Shioaji API instance
contract: The contract to resolve
Returns:
The actual contract code (e.g., "MXFA6" for MXFR1)
"""
# If code != symbol, it's already an actual contract (e.g., MXF202601 -> MXFA6)
if contract.code != contract.symbol:
return contract.code
# For rolling contracts (code == symbol, e.g., MXFR1 -> MXFR1),
# find the actual contract by matching category + delivery_month
for c in _get_futures_contracts(api):
if (c.category == contract.category and
c.delivery_month == contract.delivery_month and
c.code != c.symbol): # Actual contract has different code from symbol
logger.debug(f"Resolved rolling contract {contract.code} to actual code {c.code}")
return c.code
# Fallback: return original code if no match found
logger.warning(f"Could not resolve rolling contract {contract.code}, using original code")
return contract.code
def get_current_position(api: sj.Shioaji, contract: Contract):
# Resolve rolling contracts (MXFR1, TXFR1) to actual contract codes (MXFA6, TXFA6)
actual_code = resolve_actual_contract_code(api, contract)
logger.debug(f"Getting current position for contract: {contract.code} (resolved to {actual_code})")
for position in api.list_positions(api.futopt_account):
if actual_code == position.code:
# FuturePosition uses 'direction' not 'side'
direction = position.direction
if direction == sj.constant.Action.Buy:
logger.debug(f"Found long position: {position.quantity}")
return position.quantity
elif direction == sj.constant.Action.Sell:
logger.debug(f"Found short position: {-position.quantity}")
return -position.quantity
else:
raise ValueError(f"Position {position.code} has invalid direction: {direction}")
logger.debug("No position found")
return None
def place_entry_order(
api: sj.Shioaji, symbol: str, quantity: int, action: sj.constant.Action
):
logger.debug(f"Placing entry order: symbol={symbol}, quantity={quantity}, action={action}")
try:
contract = get_contract_from_symbol(api, symbol)
except ValueError as e:
logger.error(f"Contract not found: {e}")
raise OrderError(f"Contract not found: {e}") from e
try:
current_position = get_current_position(api, contract) or 0
logger.debug(f"Current position: {current_position}")
except (AccountNotSignError, AccountNotProvideError) as e:
logger.error(f"Account error when getting position: {e}")
raise OrderError(f"Account error: {e}") from e
original_quantity = quantity
if action == sj.constant.Action.Buy and current_position < 0:
quantity = quantity - current_position
logger.debug(f"Adjusting quantity for short reversal: {original_quantity} -> {quantity}")
elif action == sj.constant.Action.Sell and current_position > 0:
quantity = quantity + current_position
logger.debug(f"Adjusting quantity for long reversal: {original_quantity} -> {quantity}")
order = api.Order(
action=action,
price=0.0,
quantity=quantity,
price_type=sj.constant.FuturesPriceType.MKT,
order_type=sj.constant.OrderType.IOC,
octype=sj.constant.FuturesOCType.Auto,
account=api.futopt_account,
)
try:
logger.debug(f"Submitting order: action={action}, quantity={quantity}")
result = api.place_order(contract, order)
logger.debug(f"Order result: {result}")
return result
except TargetContractNotExistError as e:
logger.error(f"Target contract not exist: {e}")
raise OrderError(f"Target contract not exist: {e}") from e
except SjTimeoutError as e:
logger.error(f"Order timeout: {e}")
raise OrderError(f"Order timeout: {e}") from e
except (AccountNotSignError, AccountNotProvideError) as e:
logger.error(f"Account error when placing order: {e}")
raise OrderError(f"Account error: {e}") from e
except Exception as e:
logger.error(f"Unexpected error when placing order: {e}")
raise OrderError(f"Unexpected error when placing order: {e}") from e
def place_exit_order(api: sj.Shioaji, symbol: str, position_direction: sj.constant.Action):
logger.debug(f"Placing exit order: symbol={symbol}, position_direction={position_direction}")
try:
contract = get_contract_from_symbol(api, symbol)
except ValueError as e:
logger.error(f"Contract not found: {e}")
raise OrderError(f"Contract not found: {e}") from e
try:
current_position = get_current_position(api, contract) or 0
logger.debug(f"Current position: {current_position}")
except (AccountNotSignError, AccountNotProvideError) as e:
logger.error(f"Account error when getting position: {e}")
raise OrderError(f"Account error: {e}") from e
# close long
if position_direction == sj.constant.Action.Buy and current_position > 0:
logger.debug(f"Closing long position: selling {current_position}")
order = api.Order(
action=sj.constant.Action.Sell,
price=0.0,
quantity=current_position,
price_type=sj.constant.FuturesPriceType.MKT,
order_type=sj.constant.OrderType.IOC,
octype=sj.constant.FuturesOCType.Auto,
account=api.futopt_account,
)
# close short
elif position_direction == sj.constant.Action.Sell and current_position < 0:
logger.debug(f"Closing short position: buying {-current_position}")
order = api.Order(
action=sj.constant.Action.Buy,
price=0.0,
quantity=-current_position,
price_type=sj.constant.FuturesPriceType.MKT,
order_type=sj.constant.OrderType.IOC,
octype=sj.constant.FuturesOCType.Auto,
account=api.futopt_account,
)
else:
logger.debug("No position to exit")
return None
try:
result = api.place_order(contract, order)
logger.debug(f"Order result: {result}")
return result
except TargetContractNotExistError as e:
logger.error(f"Target contract not exist: {e}")
raise OrderError(f"Target contract not exist: {e}") from e
except SjTimeoutError as e:
logger.error(f"Order timeout: {e}")
raise OrderError(f"Order timeout: {e}") from e
except (AccountNotSignError, AccountNotProvideError) as e:
logger.error(f"Account error when placing order: {e}")
raise OrderError(f"Account error: {e}") from e
except Exception as e:
logger.error(f"Unexpected error when placing order: {e}")
raise OrderError(f"Unexpected error when placing order: {e}") from e
def check_order_status(api: sj.Shioaji, trade) -> dict:
"""
Check the actual fill status of an order by calling update_status.
According to Shioaji source code:
- update_status() updates the trade object in-place (doesn't return anything)
- OrderStatus has: status, deal_quantity, cancel_quantity, deals, order_quantity
- Status enum: PendingSubmit, PreSubmitted, Submitted, PartFilled, Filled, Cancelled, Failed, Inactive
Ref: https://sinotrade.github.io/zh/tutor/order/FutureOption/#_2
Returns a dict with:
- status: str (PendingSubmit, Submitted, Filled, PartFilled, Cancelled, Failed, Inactive)
- order_quantity: int
- deal_quantity: int (filled quantity from OrderStatus)
- cancel_quantity: int
- deals: list of deal info (price, quantity, timestamp)
- fill_avg_price: float (average fill price calculated from deals)
"""
if trade is None:
logger.warning("check_order_status called with trade=None")
return {"status": "no_trade", "error": "No trade object provided"}
order_id = getattr(trade.order, 'id', 'unknown')
seqno = getattr(trade.order, 'seqno', 'unknown')
try:
logger.debug(f"Calling api.update_status(trade=...) for order_id={order_id}, seqno={seqno}")
# update_status() updates trade object in-place, passing trade= for specific trade update
api.update_status(trade=trade)
# Extract status info from updated trade object
status_obj = trade.status
order_obj = trade.order
# Get status value - Status is an Enum
status_value = status_obj.status.value if hasattr(status_obj.status, 'value') else str(status_obj.status)
logger.debug(
f"Raw status from exchange: status={status_value}, "
f"status_code={getattr(status_obj, 'status_code', '')}, "
f"msg={getattr(status_obj, 'msg', '')}"
)
# Get deals list for calculating average price
deals = status_obj.deals if status_obj.deals else []
# Use deal_quantity from OrderStatus (this is the official filled quantity)
deal_quantity = status_obj.deal_quantity if hasattr(status_obj, 'deal_quantity') else 0
# Calculate average fill price from deals
total_value = sum(d.price * d.quantity for d in deals) if deals else 0
total_qty = sum(d.quantity for d in deals) if deals else 0
fill_avg_price = total_value / total_qty if total_qty > 0 else 0.0
# Log deal details if any
if deals:
logger.debug(f"Found {len(deals)} deal(s) for order_id={order_id}:")
for i, d in enumerate(deals):
logger.debug(f" Deal[{i}]: seq={getattr(d, 'seq', '')}, qty={d.quantity}, price={d.price}, ts={getattr(d, 'ts', 0)}")
result = {
"status": status_value,
"status_code": getattr(status_obj, 'status_code', ''),
"msg": getattr(status_obj, 'msg', ''),
"order_id": getattr(order_obj, 'id', ''),
"seqno": getattr(order_obj, 'seqno', ''),
"ordno": getattr(order_obj, 'ordno', ''),
"order_quantity": getattr(status_obj, 'order_quantity', 0) or order_obj.quantity,
"deal_quantity": deal_quantity,
"cancel_quantity": getattr(status_obj, 'cancel_quantity', 0),
"fill_avg_price": fill_avg_price,
"deals": [
{
"seq": getattr(d, 'seq', ''),
"price": d.price,
"quantity": d.quantity,
"ts": getattr(d, 'ts', 0),
}
for d in deals
],
}
return result
except Exception as e:
logger.exception(f"Error checking order status for order_id={order_id}: {e}")
return {"status": "error", "error": str(e)}