Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 12 additions & 17 deletions brokers/sinopac.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,16 +364,20 @@ def _resolve_exchange(self, stock_ids: list[str]) -> dict[str, SJExchange]:
SJStock(
security_type=SJSecurityType.Stock,
code=sid,
exchange=SJExchange.TSE,
exchange=exchange,
)
for sid in unknown
for exchange in (SJExchange.TSE, SJExchange.OTC)
]
try:
snapshots = self.api.snapshots(contracts)
for snap in snapshots:
exchange_str = getattr(snap, "exchange", "TSE")
self._exchange_cache[snap.code] = (
SJExchange.OTC if str(exchange_str) == "OTC" else SJExchange.TSE
SJExchange.OTC
if exchange_str == SJExchange.OTC
or str(exchange_str).upper().endswith("OTC")
else SJExchange.TSE
)
except Exception:
pass
Expand Down Expand Up @@ -505,11 +509,8 @@ def create_order(
order_cond: OrderCondition = OrderCondition.CASH,
) -> str:

contract = SJStock(
security_type=SJSecurityType.Stock,
code=stock_id,
exchange=SJExchange.TSE,
)
exchanges = self._resolve_exchange([stock_id])
contract = self._make_contract(stock_id, exchanges)
pinfo = self.get_price_info()

if stock_id not in pinfo:
Expand Down Expand Up @@ -698,20 +699,14 @@ def get_orders(self) -> dict[str, Order]:
return {t.status.id: trade_to_order(t) for name, t in self.trades.items()}

def get_stocks(self, stock_ids: list[str]) -> dict[str, Stock]:
contracts = [
SJStock(security_type=SJSecurityType.Stock, code=s, exchange=SJExchange.TSE)
for s in stock_ids
]
exchanges = self._resolve_exchange(stock_ids)
contracts = [self._make_contract(s, exchanges) for s in stock_ids]
try:
snapshots = self.api.snapshots(list(contracts))
except Exception:
time.sleep(10)
contracts = [
SJStock(
security_type=SJSecurityType.Stock, code=s, exchange=SJExchange.TSE
)
for s in stock_ids
]
exchanges = self._resolve_exchange(stock_ids)
contracts = [self._make_contract(s, exchanges) for s in stock_ids]
snapshots = self.api.snapshots(list(contracts))

return {s.code: snapshot_to_stock(s) for s in snapshots}
Expand Down
77 changes: 72 additions & 5 deletions tests/unit/test_sinopac_realtime_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,42 @@ def __init__(self) -> None:
self.order_cb = None
self.quote = _FakeQuote()
self.stock_account = types.SimpleNamespace(account_id="9809789")
self.snapshot_calls: list[list[tuple[str | None, str | None]]] = []
self.snapshot_exchange_by_code = {"2330": "TSE", "8042": "OTC"}
self.placed_orders: list[tuple[object, object]] = []

def snapshots(self, contracts: list[object]) -> list[types.SimpleNamespace]:
self.snapshot_calls.append([(c.code, c.exchange) for c in contracts])
snapshots = []
for contract in contracts:
expected_exchange = self.snapshot_exchange_by_code.get(contract.code, "TSE")
if contract.exchange != expected_exchange:
continue
snapshots.append(
types.SimpleNamespace(
code=contract.code,
exchange=expected_exchange,
open=100.0,
high=105.0,
low=99.0,
close=102.0,
buy_price=101.5,
buy_volume=12,
sell_price=102.5,
sell_volume=15,
change_rate=1.2,
)
)
return snapshots

def Order(self, **kwargs: object) -> types.SimpleNamespace:
return types.SimpleNamespace(**kwargs)

def place_order(
self, contract: object, order: object
) -> types.SimpleNamespace:
self.placed_orders.append((contract, order))
return types.SimpleNamespace(status=types.SimpleNamespace(id="order-1"))

def ticks(self, contract: object, date: str | None = None) -> types.SimpleNamespace:
return types.SimpleNamespace(
Expand Down Expand Up @@ -71,12 +107,14 @@ def _import_sinopac_module_with_fake_sdk(
)

constant_module = types.ModuleType("shioaji.constant")
constant_module.StockPriceType = types.SimpleNamespace()
constant_module.StockOrderLot = types.SimpleNamespace()
constant_module.Action = types.SimpleNamespace()
constant_module.StockPriceType = types.SimpleNamespace(LMT="LMT")
constant_module.StockOrderLot = types.SimpleNamespace(
Common="Common", IntradayOdd="IntradayOdd", Odd="Odd", Fixing="Fixing"
)
constant_module.Action = types.SimpleNamespace(Buy="Buy", Sell="Sell")
constant_module.SecurityType = types.SimpleNamespace(Stock="Stock")
constant_module.Exchange = types.SimpleNamespace(TSE="TSE")
constant_module.OrderType = types.SimpleNamespace()
constant_module.Exchange = types.SimpleNamespace(TSE="TSE", OTC="OTC")
constant_module.OrderType = types.SimpleNamespace(ROD="ROD")
constant_module.Unit = types.SimpleNamespace()
constant_module.OrderState = types.SimpleNamespace(
StockDeal=types.SimpleNamespace(value="SDEAL"),
Expand Down Expand Up @@ -235,6 +273,35 @@ def test_sinopac_subscribe_ticks_and_bidask(monkeypatch: pytest.MonkeyPatch) ->
assert ("2330", "BidAsk") in account.api.quote.unsubscriptions


def test_sinopac_resolves_otc_exchange_for_stocks_and_orders(
monkeypatch: pytest.MonkeyPatch,
) -> None:
sinopac_module = _import_sinopac_module_with_fake_sdk(monkeypatch)
SinopacAccount = sinopac_module.SinopacAccount

account = SinopacAccount.__new__(SinopacAccount)
account.api = _FakeShioaji()
account._exchange_cache = {}
account.trades = {}

stocks = account.get_stocks(["2330", "8042"])

assert sorted(stocks) == ["2330", "8042"]
assert ("8042", "OTC") in account.api.snapshot_calls[-1]

account.get_price_info = lambda: {"8042": {"漲停價": 110, "跌停價": 90}}
order_id = account.create_order(
sinopac_module.Action.BUY,
stock_id="8042",
quantity=1,
price=100,
)

assert order_id == "order-1"
assert account.api.placed_orders[0][0].code == "8042"
assert account.api.placed_orders[0][0].exchange == "OTC"


def test_sinopac_backfill_ticks_uses_historical_tick_query(
monkeypatch: pytest.MonkeyPatch,
) -> None:
Expand Down