Skip to content

Commit d37df79

Browse files
committed
Add LongBridge fractional order API probe
1 parent 4a98277 commit d37df79

3 files changed

Lines changed: 117 additions & 0 deletions

File tree

src/quant_platform_kit/longbridge/execution.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def estimate_max_purchase_quantity(
1212
*,
1313
order_kind: str,
1414
ref_price: float,
15+
fractional_shares: bool = False,
1516
) -> float:
1617
from longport.openapi import OrderSide, OrderType
1718

@@ -21,6 +22,7 @@ def estimate_max_purchase_quantity(
2122
order_type=order_type,
2223
side=OrderSide.Buy,
2324
price=Decimal(str(ref_price)),
25+
fractional_shares=bool(fractional_shares),
2426
)
2527
cash_max_qty = getattr(response, "cash_max_qty", 0)
2628
return max(0.0, float(Decimal(str(cash_max_qty or "0"))))

tests/test_longbridge_execution.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,26 @@ def test_estimate_max_purchase_quantity(self) -> None:
5353
quantity = estimate_max_purchase_quantity(ctx, "SOXL.US", order_kind="limit", ref_price=100.5)
5454

5555
self.assertEqual(quantity, 12)
56+
self.assertIs(ctx.estimate_kwargs["fractional_shares"], False)
57+
58+
def test_estimate_max_purchase_quantity_can_request_fractional_buying_power(self) -> None:
59+
longport_module = types.ModuleType("longport")
60+
openapi_module = types.ModuleType("longport.openapi")
61+
openapi_module.OrderSide = types.SimpleNamespace(Buy="Buy")
62+
openapi_module.OrderType = types.SimpleNamespace(LO="LO", MO="MO")
63+
64+
ctx = FakeTradeContext()
65+
with patch.dict(sys.modules, {"longport": longport_module, "longport.openapi": openapi_module}):
66+
quantity = estimate_max_purchase_quantity(
67+
ctx,
68+
"SOXX.US",
69+
order_kind="limit",
70+
ref_price=495.91,
71+
fractional_shares=True,
72+
)
73+
74+
self.assertEqual(quantity, 12)
75+
self.assertIs(ctx.estimate_kwargs["fractional_shares"], True)
5676

5777
def test_submit_order(self) -> None:
5878
longport_module = types.ModuleType("longport")
@@ -68,6 +88,20 @@ def test_submit_order(self) -> None:
6888
self.assertEqual(report.status, "submitted")
6989
self.assertEqual(report.broker_order_id, "OID-1")
7090

91+
def test_submit_order_allows_decimal_quantity_at_or_above_one_share(self) -> None:
92+
longport_module = types.ModuleType("longport")
93+
openapi_module = types.ModuleType("longport.openapi")
94+
openapi_module.OrderSide = types.SimpleNamespace(Buy="Buy", Sell="Sell")
95+
openapi_module.OrderType = types.SimpleNamespace(LO="LO", MO="MO")
96+
openapi_module.TimeInForceType = types.SimpleNamespace(Day="Day")
97+
98+
ctx = FakeTradeContext()
99+
with patch.dict(sys.modules, {"longport": longport_module, "longport.openapi": openapi_module}):
100+
report = submit_order(ctx, "SOXL.US", order_kind="limit", side="buy", quantity=1.5, submitted_price=100.25)
101+
102+
self.assertEqual(report.status, "submitted")
103+
self.assertEqual(str(ctx.submit_args[3]), "1.5")
104+
71105
def test_submit_order_rejects_quantity_below_one_before_api_call(self) -> None:
72106
longport_module = types.ModuleType("longport")
73107
openapi_module = types.ModuleType("longport.openapi")
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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

Comments
 (0)