Pārlūkot izejas kodu

fix: tighten order and position handling

lxy 1 mēnesi atpakaļ
vecāks
revīzija
9f8da67423
2 mainītis faili ar 99 papildinājumiem un 3 dzēšanām
  1. 5 3
      okx_codex_trader/okx_client.py
  2. 94 0
      tests/test_okx_client.py

+ 5 - 3
okx_codex_trader/okx_client.py

@@ -92,7 +92,7 @@ class OkxClient:
             params={"instId": symbol, "bar": bar, "limit": limit},
         )
         try:
-            return [
+            candles = [
                 Candle(
                     symbol=symbol,
                     ts=int(entry[0]),
@@ -104,6 +104,7 @@ class OkxClient:
                 )
                 for entry in data
             ]
+            return sorted(candles, key=lambda candle: candle.ts)
         except (IndexError, TypeError, ValueError):
             raise self._invalid_payload() from None
 
@@ -169,8 +170,8 @@ class OkxClient:
         side = "buy" if signal.action == "long" else "sell"
         pos_side = "long" if signal.action == "long" else "short"
         self.ensure_hedge_mode()
-        self.set_leverage(symbol, signal.leverage, pos_side)
         size = build_contract_size(margin_usdt * signal.leverage, price, metadata)
+        self.set_leverage(symbol, signal.leverage, pos_side)
         order_type = "market" if signal.entry_price is None else "limit"
         request_body = {
             "instId": symbol,
@@ -202,7 +203,7 @@ class OkxClient:
         if not data:
             return []
         try:
-            return [
+            positions = [
                 Position(
                     symbol=str(entry["instId"]),
                     pos_side=str(entry["posSide"]),
@@ -211,5 +212,6 @@ class OkxClient:
                 )
                 for entry in data
             ]
+            return [position for position in positions if position.size != 0.0]
         except (KeyError, TypeError, ValueError):
             raise self._invalid_payload() from None

+ 94 - 0
tests/test_okx_client.py

@@ -76,6 +76,19 @@ def candles_response() -> DummyResponse:
     )
 
 
+def descending_candles_response() -> DummyResponse:
+    return DummyResponse(
+        {
+            "code": "0",
+            "msg": "",
+            "data": [
+                ["1710000001000", "25100", "25200", "25000", "25150", "110", "1100", "1100", "1"],
+                ["1710000000000", "25000", "25100", "24900", "25050", "100", "1000", "1000", "1"],
+            ],
+        }
+    )
+
+
 def instrument_response() -> DummyResponse:
     return DummyResponse(
         {
@@ -94,6 +107,24 @@ def instrument_response() -> DummyResponse:
     )
 
 
+def large_min_size_instrument_response() -> DummyResponse:
+    return DummyResponse(
+        {
+            "code": "0",
+            "msg": "",
+            "data": [
+                {
+                    "instId": "BTC-USDT-SWAP",
+                    "instType": "SWAP",
+                    "ctVal": "0.01",
+                    "lotSz": "1",
+                    "minSz": "100",
+                }
+            ],
+        }
+    )
+
+
 def ticker_response(last: str) -> DummyResponse:
     return DummyResponse({"code": "0", "msg": "", "data": [{"instId": "BTC-USDT-SWAP", "last": last}]})
 
@@ -135,6 +166,29 @@ def positions_response() -> DummyResponse:
     )
 
 
+def positions_with_zero_size_response() -> DummyResponse:
+    return DummyResponse(
+        {
+            "code": "0",
+            "msg": "",
+            "data": [
+                {
+                    "instId": "BTC-USDT-SWAP",
+                    "posSide": "long",
+                    "pos": "0",
+                    "avgPx": "25000",
+                },
+                {
+                    "instId": "BTC-USDT-SWAP",
+                    "posSide": "short",
+                    "pos": "3",
+                    "avgPx": "24900",
+                },
+            ],
+        }
+    )
+
+
 def market_long_signal() -> TradeSignal:
     return TradeSignal(
         action="long",
@@ -186,6 +240,15 @@ def test_signed_demo_request_attaches_headers():
     assert request.headers["OK-ACCESS-PASSPHRASE"] == "passphrase"
 
 
+def test_get_candles_returns_chronological_ascending_order():
+    session = DummySession([descending_candles_response()])
+    client = OkxClient(config=sample_config(), session=session)
+
+    candles = client.get_candles(symbol="BTC-USDT-SWAP", bar="1H", limit=20)
+
+    assert [candle.ts for candle in candles] == [1710000000000, 1710000001000]
+
+
 def test_build_contract_size_rounds_down_to_lot_size():
     metadata = InstrumentMeta(ct_val=0.01, lot_sz=0.1, min_sz=0.1)
     assert build_contract_size(notional=251, price=25_000, metadata=metadata) == 1.0
@@ -234,6 +297,26 @@ def test_place_demo_order_fails_when_not_hedge_mode():
         client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
 
 
+def test_place_demo_order_validates_size_before_setting_leverage():
+    session = DummySession(
+        [
+            large_min_size_instrument_response(),
+            ticker_response(last="25000"),
+            account_config_response(pos_mode="long_short_mode"),
+        ]
+    )
+    client = OkxClient(config=sample_config(), session=session)
+
+    with pytest.raises(ValueError, match="contract size below minimum"):
+        client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
+
+    assert session.request_paths == [
+        "/api/v5/public/instruments",
+        "/api/v5/market/ticker",
+        "/api/v5/account/config",
+    ]
+
+
 def test_limit_short_order_uses_sell_and_short_pos_side():
     session = DummySession(
         [
@@ -380,3 +463,14 @@ def test_get_positions_returns_normalized_positions():
     assert positions[0].pos_side == "long"
     assert positions[0].size == 8.0
     assert positions[0].avg_price == 25000.0
+
+
+def test_get_positions_filters_zero_size_rows():
+    session = DummySession([positions_with_zero_size_response()])
+    client = OkxClient(config=sample_config(), session=session)
+
+    positions = client.get_positions(symbol="BTC-USDT-SWAP")
+
+    assert len(positions) == 1
+    assert positions[0].pos_side == "short"
+    assert positions[0].size == 3.0