|
|
@@ -87,6 +87,10 @@ def sample_config() -> Config:
|
|
|
return Config(api_key="key", api_secret="secret", api_passphrase="passphrase")
|
|
|
|
|
|
|
|
|
+def live_config() -> Config:
|
|
|
+ return Config(api_key="key", api_secret="secret", api_passphrase="passphrase", trading_env="live")
|
|
|
+
|
|
|
+
|
|
|
def candles_response() -> DummyResponse:
|
|
|
return DummyResponse(
|
|
|
{
|
|
|
@@ -174,6 +178,28 @@ def ticker_response(last: str) -> DummyResponse:
|
|
|
return DummyResponse({"code": "0", "msg": "", "data": [{"instId": "BTC-USDT-SWAP", "last": last}]})
|
|
|
|
|
|
|
|
|
+def balance_response() -> DummyResponse:
|
|
|
+ return DummyResponse(
|
|
|
+ {
|
|
|
+ "code": "0",
|
|
|
+ "msg": "",
|
|
|
+ "data": [
|
|
|
+ {
|
|
|
+ "totalEq": "101.5",
|
|
|
+ "details": [
|
|
|
+ {
|
|
|
+ "ccy": "USDT",
|
|
|
+ "eq": "100.25",
|
|
|
+ "availEq": "98.75",
|
|
|
+ "cashBal": "100.0",
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
def account_config_response(pos_mode: str) -> DummyResponse:
|
|
|
return DummyResponse({"code": "0", "msg": "", "data": [{"posMode": pos_mode}]})
|
|
|
|
|
|
@@ -462,6 +488,17 @@ def test_signed_demo_request_attaches_headers():
|
|
|
assert request.timeout > 0
|
|
|
|
|
|
|
|
|
+def test_signed_live_request_attaches_live_header():
|
|
|
+ session = DummySession()
|
|
|
+ client = OkxClient(config=live_config(), session=session)
|
|
|
+
|
|
|
+ client.get_candles(symbol="BTC-USDT-SWAP", bar="1H", limit=20)
|
|
|
+
|
|
|
+ request = session.last_request
|
|
|
+ assert request is not None
|
|
|
+ assert request.headers["x-simulated-trading"] == "0"
|
|
|
+
|
|
|
+
|
|
|
def test_signed_post_request_uses_actual_serialized_body_bytes():
|
|
|
session = DummySession(
|
|
|
[
|
|
|
@@ -473,7 +510,7 @@ def test_signed_post_request_uses_actual_serialized_body_bytes():
|
|
|
)
|
|
|
client = OkxClient(config=sample_config(), session=session)
|
|
|
|
|
|
- client.place_demo_order(symbol="ETH-USDT-SWAP", signal=limit_short_signal(), margin_usdt=100)
|
|
|
+ client.place_order(symbol="ETH-USDT-SWAP", signal=limit_short_signal(), margin_usdt=100)
|
|
|
|
|
|
request = session.last_request
|
|
|
assert request is not None
|
|
|
@@ -534,6 +571,45 @@ def test_get_candles_paginates_when_limit_exceeds_single_page():
|
|
|
assert len(session.request_paths) == 2
|
|
|
|
|
|
|
|
|
+def test_get_account_balance_returns_usdt_equity_fields():
|
|
|
+ session = DummySession([balance_response()])
|
|
|
+ client = OkxClient(config=sample_config(), session=session)
|
|
|
+
|
|
|
+ balance = client.get_account_balance("USDT")
|
|
|
+
|
|
|
+ assert session.request_paths == ["/api/v5/account/balance"]
|
|
|
+ assert balance == {
|
|
|
+ "total_equity_usd": 101.5,
|
|
|
+ "equity": 100.25,
|
|
|
+ "available_equity": 98.75,
|
|
|
+ "cash_balance": 100.0,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def test_get_account_balance_returns_zero_when_currency_detail_is_absent():
|
|
|
+ session = DummySession(
|
|
|
+ [
|
|
|
+ DummyResponse(
|
|
|
+ {
|
|
|
+ "code": "0",
|
|
|
+ "msg": "",
|
|
|
+ "data": [{"totalEq": "0", "details": []}],
|
|
|
+ }
|
|
|
+ )
|
|
|
+ ]
|
|
|
+ )
|
|
|
+ client = OkxClient(config=sample_config(), session=session)
|
|
|
+
|
|
|
+ balance = client.get_account_balance("USDT")
|
|
|
+
|
|
|
+ assert balance == {
|
|
|
+ "total_equity_usd": 0.0,
|
|
|
+ "equity": 0.0,
|
|
|
+ "available_equity": 0.0,
|
|
|
+ "cash_balance": 0.0,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
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
|
|
|
@@ -609,7 +685,7 @@ def test_market_order_fetches_latest_price_before_sizing():
|
|
|
)
|
|
|
client = OkxClient(config=sample_config(), session=session)
|
|
|
|
|
|
- client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
|
|
|
+ client.place_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
|
|
|
|
|
|
assert session.request_paths == [
|
|
|
"/api/v5/account/config",
|
|
|
@@ -641,13 +717,13 @@ def test_fractional_margin_sizing_keeps_decimal_precision():
|
|
|
)
|
|
|
client = OkxClient(config=sample_config(), session=session)
|
|
|
|
|
|
- client.place_demo_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=0.009)
|
|
|
+ client.place_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=0.009)
|
|
|
|
|
|
assert session.last_json_body is not None
|
|
|
assert session.last_json_body["sz"] == "27"
|
|
|
|
|
|
|
|
|
-def test_place_demo_order_fails_when_not_hedge_mode():
|
|
|
+def test_place_order_fails_when_not_hedge_mode():
|
|
|
session = DummySession(
|
|
|
[
|
|
|
account_config_response(pos_mode="net_mode"),
|
|
|
@@ -656,7 +732,7 @@ def test_place_demo_order_fails_when_not_hedge_mode():
|
|
|
client = OkxClient(config=sample_config(), session=session)
|
|
|
|
|
|
with pytest.raises(ValueError):
|
|
|
- client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
|
|
|
+ client.place_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
|
|
|
assert session.request_paths == ["/api/v5/account/config"]
|
|
|
|
|
|
|
|
|
@@ -668,7 +744,7 @@ def test_ensure_hedge_mode_rejects_malformed_config_payload():
|
|
|
client.ensure_hedge_mode()
|
|
|
|
|
|
|
|
|
-def test_place_demo_order_validates_size_before_setting_leverage():
|
|
|
+def test_place_order_validates_size_before_setting_leverage():
|
|
|
session = DummySession(
|
|
|
[
|
|
|
account_config_response(pos_mode="long_short_mode"),
|
|
|
@@ -679,7 +755,7 @@ def test_place_demo_order_validates_size_before_setting_leverage():
|
|
|
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)
|
|
|
+ client.place_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
|
|
|
|
|
|
assert session.request_paths == [
|
|
|
"/api/v5/account/config",
|
|
|
@@ -699,7 +775,7 @@ def test_limit_short_order_uses_sell_and_short_pos_side():
|
|
|
)
|
|
|
client = OkxClient(config=sample_config(), session=session)
|
|
|
|
|
|
- client.place_demo_order(symbol="ETH-USDT-SWAP", signal=limit_short_signal(), margin_usdt=100)
|
|
|
+ client.place_order(symbol="ETH-USDT-SWAP", signal=limit_short_signal(), margin_usdt=100)
|
|
|
|
|
|
order_request = session.last_json_body
|
|
|
assert order_request is not None
|
|
|
@@ -715,13 +791,13 @@ def test_flat_signal_returns_noop_without_order_submission():
|
|
|
session = DummySession([])
|
|
|
client = OkxClient(config=sample_config(), session=session)
|
|
|
|
|
|
- result = client.place_demo_order(symbol="BTC-USDT-SWAP", signal=flat_signal(), margin_usdt=100)
|
|
|
+ result = client.place_order(symbol="BTC-USDT-SWAP", signal=flat_signal(), margin_usdt=100)
|
|
|
|
|
|
assert result.status == "noop"
|
|
|
assert session.request_paths == []
|
|
|
|
|
|
|
|
|
-def test_place_demo_order_sends_computed_sz_and_ignores_tp_sl_fields():
|
|
|
+def test_place_order_sends_computed_sz_and_ignores_tp_sl_fields():
|
|
|
session = DummySession(
|
|
|
[
|
|
|
account_config_response(pos_mode="long_short_mode"),
|
|
|
@@ -733,7 +809,7 @@ def test_place_demo_order_sends_computed_sz_and_ignores_tp_sl_fields():
|
|
|
)
|
|
|
client = OkxClient(config=sample_config(), session=session)
|
|
|
|
|
|
- client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
|
|
|
+ client.place_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
|
|
|
|
|
|
order_request = session.last_json_body
|
|
|
assert order_request is not None
|
|
|
@@ -854,7 +930,7 @@ def test_get_instrument_meta_rejects_non_swap_type():
|
|
|
client.get_instrument_meta(symbol="BTC-USDT-SWAP")
|
|
|
|
|
|
|
|
|
-def test_place_demo_order_raises_when_order_id_is_missing():
|
|
|
+def test_place_order_raises_when_order_id_is_missing():
|
|
|
session = DummySession(
|
|
|
[
|
|
|
account_config_response(pos_mode="long_short_mode"),
|
|
|
@@ -867,11 +943,11 @@ def test_place_demo_order_raises_when_order_id_is_missing():
|
|
|
client = OkxClient(config=sample_config(), session=session)
|
|
|
|
|
|
with pytest.raises(ValueError, match="okx response payload is invalid"):
|
|
|
- client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
|
|
|
+ client.place_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
|
|
|
assert session.request_paths[-1] == "/api/v5/trade/order"
|
|
|
|
|
|
|
|
|
-def test_place_demo_order_rejects_invalid_leverage_before_okx():
|
|
|
+def test_place_order_rejects_invalid_leverage_before_okx():
|
|
|
session = DummySession([])
|
|
|
signal = TradeSignal(
|
|
|
action="long",
|
|
|
@@ -885,11 +961,11 @@ def test_place_demo_order_rejects_invalid_leverage_before_okx():
|
|
|
client = OkxClient(config=sample_config(), session=session)
|
|
|
|
|
|
with pytest.raises(ValueError, match="leverage is invalid"):
|
|
|
- client.place_demo_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=100)
|
|
|
+ client.place_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=100)
|
|
|
assert session.request_paths == []
|
|
|
|
|
|
|
|
|
-def test_place_demo_order_rejects_fractional_leverage_before_okx():
|
|
|
+def test_place_order_rejects_fractional_leverage_before_okx():
|
|
|
session = DummySession([])
|
|
|
signal = TradeSignal(
|
|
|
action="long",
|
|
|
@@ -903,11 +979,11 @@ def test_place_demo_order_rejects_fractional_leverage_before_okx():
|
|
|
client = OkxClient(config=sample_config(), session=session)
|
|
|
|
|
|
with pytest.raises(ValueError, match="leverage is invalid"):
|
|
|
- client.place_demo_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=100)
|
|
|
+ client.place_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=100)
|
|
|
assert session.request_paths == []
|
|
|
|
|
|
|
|
|
-def test_place_demo_order_rejects_boolean_leverage_before_okx():
|
|
|
+def test_place_order_rejects_boolean_leverage_before_okx():
|
|
|
session = DummySession([])
|
|
|
signal = TradeSignal(
|
|
|
action="long",
|
|
|
@@ -921,26 +997,26 @@ def test_place_demo_order_rejects_boolean_leverage_before_okx():
|
|
|
client = OkxClient(config=sample_config(), session=session)
|
|
|
|
|
|
with pytest.raises(ValueError, match="leverage is invalid"):
|
|
|
- client.place_demo_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=100)
|
|
|
+ client.place_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=100)
|
|
|
assert session.request_paths == []
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("margin_usdt", [0, -1, float("nan"), float("inf")])
|
|
|
-def test_place_demo_order_rejects_invalid_margin_before_okx(margin_usdt):
|
|
|
+def test_place_order_rejects_invalid_margin_before_okx(margin_usdt):
|
|
|
session = DummySession([])
|
|
|
client = OkxClient(config=sample_config(), session=session)
|
|
|
|
|
|
with pytest.raises(ValueError, match="margin_usdt is invalid"):
|
|
|
- client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=margin_usdt)
|
|
|
+ client.place_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=margin_usdt)
|
|
|
assert session.request_paths == []
|
|
|
|
|
|
|
|
|
-def test_place_demo_order_rejects_boolean_margin_before_okx():
|
|
|
+def test_place_order_rejects_boolean_margin_before_okx():
|
|
|
session = DummySession([])
|
|
|
client = OkxClient(config=sample_config(), session=session)
|
|
|
|
|
|
with pytest.raises(ValueError, match="margin_usdt is invalid"):
|
|
|
- client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=True)
|
|
|
+ client.place_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=True)
|
|
|
assert session.request_paths == []
|
|
|
|
|
|
|
|
|
@@ -962,7 +1038,7 @@ def test_set_leverage_validates_public_boundary_inputs(symbol, leverage, pos_sid
|
|
|
assert session.request_paths == []
|
|
|
|
|
|
|
|
|
-def test_place_demo_order_rejects_unknown_action_before_okx():
|
|
|
+def test_place_order_rejects_unknown_action_before_okx():
|
|
|
session = DummySession([])
|
|
|
signal = TradeSignal(
|
|
|
action="hold",
|
|
|
@@ -976,7 +1052,7 @@ def test_place_demo_order_rejects_unknown_action_before_okx():
|
|
|
client = OkxClient(config=sample_config(), session=session)
|
|
|
|
|
|
with pytest.raises(ValueError, match="action is invalid"):
|
|
|
- client.place_demo_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=100)
|
|
|
+ client.place_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=100)
|
|
|
assert session.request_paths == []
|
|
|
|
|
|
|