diff --git a/brokers/sinopac.py b/brokers/sinopac.py index 302f8dd..cd0b867 100644 --- a/brokers/sinopac.py +++ b/brokers/sinopac.py @@ -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 @@ -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: @@ -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} diff --git a/tests/unit/test_sinopac_realtime_unit.py b/tests/unit/test_sinopac_realtime_unit.py index dd28d62..f49af2f 100644 --- a/tests/unit/test_sinopac_realtime_unit.py +++ b/tests/unit/test_sinopac_realtime_unit.py @@ -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( @@ -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"), @@ -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: