|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import os |
| 4 | +import unittest |
| 5 | +from decimal import Decimal |
| 6 | + |
| 7 | + |
| 8 | +def _api_probe_enabled() -> bool: |
| 9 | + return str(os.getenv("LONGBRIDGE_API_PROBE", "")).strip().lower() in {"1", "true", "yes", "on"} |
| 10 | + |
| 11 | + |
| 12 | +@unittest.skipUnless( |
| 13 | + _api_probe_enabled(), |
| 14 | + "Set LONGBRIDGE_API_PROBE=1 with HK simulated LongPort credentials to run live API probes.", |
| 15 | +) |
| 16 | +class LongBridgeFractionalOrderApiProbeTests(unittest.TestCase): |
| 17 | + """Manual probe for LongBridge API quantity validation against a simulated account. |
| 18 | +
|
| 19 | + These tests intentionally call the broker API. Keep them skipped in normal CI. |
| 20 | + """ |
| 21 | + |
| 22 | + symbol = os.getenv("LONGBRIDGE_API_PROBE_SYMBOL", "SOXX.US") |
| 23 | + limit_price = Decimal(os.getenv("LONGBRIDGE_API_PROBE_LIMIT_PRICE", "0.01")) |
| 24 | + |
| 25 | + def setUp(self) -> None: |
| 26 | + try: |
| 27 | + from longport.openapi import Config, TradeContext |
| 28 | + except ImportError as exc: # pragma: no cover - only relevant outside probe env |
| 29 | + raise unittest.SkipTest("longport is required for API probes") from exc |
| 30 | + |
| 31 | + missing = [ |
| 32 | + name |
| 33 | + for name in ("LONGPORT_APP_KEY", "LONGPORT_APP_SECRET", "LONGPORT_ACCESS_TOKEN") |
| 34 | + if not os.getenv(name) |
| 35 | + ] |
| 36 | + if missing: |
| 37 | + raise unittest.SkipTest(f"Missing LongPort credentials: {', '.join(missing)}") |
| 38 | + |
| 39 | + config = Config( |
| 40 | + app_key=os.environ["LONGPORT_APP_KEY"], |
| 41 | + app_secret=os.environ["LONGPORT_APP_SECRET"], |
| 42 | + access_token=os.environ["LONGPORT_ACCESS_TOKEN"], |
| 43 | + ) |
| 44 | + self.trade_context = TradeContext(config) |
| 45 | + |
| 46 | + def _submit_limit_buy(self, quantity: Decimal): |
| 47 | + from longport.openapi import OrderSide, OrderType, TimeInForceType |
| 48 | + |
| 49 | + return self.trade_context.submit_order( |
| 50 | + self.symbol, |
| 51 | + OrderType.LO, |
| 52 | + OrderSide.Buy, |
| 53 | + quantity, |
| 54 | + TimeInForceType.Day, |
| 55 | + submitted_price=self.limit_price, |
| 56 | + remark="qpk-fractional-api-probe", |
| 57 | + ) |
| 58 | + |
| 59 | + def test_sub_one_fractional_order_is_rejected_by_openapi_quantity_validation(self) -> None: |
| 60 | + from longport.openapi import OpenApiException |
| 61 | + |
| 62 | + with self.assertRaises(OpenApiException) as raised: |
| 63 | + self._submit_limit_buy(Decimal("0.4326")) |
| 64 | + |
| 65 | + message = str(raised.exception) |
| 66 | + self.assertIn("SubmittedQuantity", message) |
| 67 | + self.assertIn("^([1-9]", message) |
| 68 | + |
| 69 | + def test_fractional_order_at_or_above_one_share_can_be_submitted_then_cancelled(self) -> None: |
| 70 | + response = self._submit_limit_buy(Decimal("1.5")) |
| 71 | + order_id = str(getattr(response, "order_id", "") or "").strip() |
| 72 | + self.assertTrue(order_id, "LongBridge accepted 1.5 quantity but did not return an order_id") |
| 73 | + |
| 74 | + try: |
| 75 | + self.trade_context.cancel_order(order_id) |
| 76 | + except Exception as exc: # pragma: no cover - preserves the original acceptance assertion |
| 77 | + self.fail(f"LongBridge accepted 1.5 quantity but cancel failed for order_id={order_id}: {exc}") |
| 78 | + |
| 79 | + |
| 80 | +if __name__ == "__main__": |
| 81 | + unittest.main() |
0 commit comments