Files
QuantDinger/backend_api_python/tests/test_htx_v5.py
T
Dinger 57b54c598f v3.0.17
Signed-off-by: Dinger <quantdinger@gmail.com>
2026-05-28 22:40:02 +08:00

289 lines
9.8 KiB
Python

"""Unit tests for HTX swap API V5 helpers and client."""
from __future__ import annotations
from unittest.mock import patch
import pytest
from app.services.live_trading import htx_v5
from app.services.live_trading.htx import HtxClient
def test_v5_ok_accepts_status_and_code():
assert htx_v5.v5_ok({"status": "ok", "data": {}})
assert htx_v5.v5_ok({"code": 200, "data": {}})
assert not htx_v5.v5_ok({"code": 400, "message": "fail"})
def test_normalize_balance_from_v5_data_list():
raw = {
"code": 200,
"data": [
{"currency": "USDT", "available": "42", "equity": "42"},
],
}
out = htx_v5.normalize_balance(raw)
assert len(out["data"]) == 1
assert out["data"][0]["margin_available"] == 42.0
def test_normalize_balance_from_v5_details():
raw = {
"code": 200,
"data": {
"equity": "1000",
"available_margin": "800",
"details": [
{
"currency": "USDT",
"equity": "1000",
"available": "750",
"withdraw_available": "750",
}
],
},
}
out = htx_v5.normalize_balance(raw)
assert out["status"] == "ok"
assert len(out["data"]) == 1
row = out["data"][0]
assert row["margin_asset"] == "USDT"
assert row["margin_available"] == 750.0
assert row["margin_balance"] == 1000.0
def test_normalize_positions_list():
raw = {
"code": 200,
"data": [
{"contract_code": "BTC-USDT", "volume": 10, "direction": "buy"},
],
}
out = htx_v5.normalize_positions(raw)
assert len(out["data"]) == 1
assert out["data"][0]["contract_code"] == "BTC-USDT"
assert out["data"][0]["volume"] == 10
def test_normalize_order_place():
raw = {"status": "ok", "data": {"order_id": "12345", "order_id_str": "12345"}}
out = htx_v5.normalize_order_place(raw)
assert out["data"]["order_id_str"] == "12345"
def test_swap_balance_uses_v5_only():
c = HtxClient(api_key="k", secret_key="s", market_type="swap")
v5_raw = {
"code": 200,
"data": {
"details": [{"currency": "USDT", "available": "100", "equity": "100"}],
},
}
with patch.object(c, "_swap_v5_request", return_value=v5_raw) as mock_v5:
out = c.get_balance()
mock_v5.assert_called_once_with("GET", "/v5/account/balance")
assert out["data"][0]["margin_available"] == 100.0
def test_map_v1_order_price_type_to_v5_type():
assert htx_v5.map_v1_order_price_type_to_v5_type("opponent") == "market"
assert htx_v5.map_v1_order_price_type_to_v5_type("limit", has_limit_price=True) == "limit"
assert htx_v5.map_v1_order_price_type_to_v5_type("ioc") == "ioc"
def test_parse_position_mode_hedged():
assert htx_v5.parse_position_mode_hedged({"position_mode": "dual_side"}) is True
assert htx_v5.parse_position_mode_hedged({"position_mode": "single_side"}) is False
def test_build_v1_cross_order_body_market():
body = htx_v5.build_v1_cross_order_body(
contract_code="DOGE-USDT",
volume=2,
side="buy",
lever_rate=5,
order_price_type="opponent",
hedge_mode=False,
)
assert body.get("direction") == "buy"
assert body.get("order_price_type") == "opponent"
assert body.get("offset") == "both"
assert "position_mode" not in body
assert "side" not in body
def test_build_swap_order_body_one_way_uses_position_side_both():
body = htx_v5.build_swap_order_body(
contract_code="DOGE-USDT",
volume=1,
side="buy",
order_price_type="opponent",
position_side="both",
)
assert body.get("position_side") == "both"
assert "offset" not in body
assert "trade_type" not in body
assert "lever_rate" not in body
assert "position_mode" not in body
assert "reduce_only" not in body
def test_build_swap_order_body_one_way_close_uses_reduce_only():
body = htx_v5.build_swap_order_body(
contract_code="DOGE-USDT",
volume=1,
side="sell",
order_price_type="opponent",
reduce_only=True,
position_side="both",
)
assert body.get("reduce_only") == 1
assert body.get("position_side") == "both"
def test_build_swap_order_body_hedge_uses_position_side_long():
body = htx_v5.build_swap_order_body(
contract_code="DOGE-USDT",
volume=1,
side="buy",
order_price_type="opponent",
position_side="long",
)
assert body.get("position_side") == "long"
assert "offset" not in body
assert "trade_type" not in body
assert "lever_rate" not in body
def test_resolve_v5_position_side():
# dual_side, infer from side/reduce_only
assert htx_v5.resolve_v5_position_side(
side="buy", reduce_only=False, hedge_mode=True
) == "long"
assert htx_v5.resolve_v5_position_side(
side="sell", reduce_only=False, hedge_mode=True
) == "short"
assert htx_v5.resolve_v5_position_side(
side="sell", reduce_only=True, hedge_mode=True
) == "long"
assert htx_v5.resolve_v5_position_side(
side="buy", reduce_only=True, hedge_mode=True
) == "short"
# explicit pos_side wins
assert htx_v5.resolve_v5_position_side(
side="buy", reduce_only=False, hedge_mode=True, pos_side="short"
) == "short"
# one-way always both
assert htx_v5.resolve_v5_position_side(
side="buy", reduce_only=False, hedge_mode=False
) == "both"
def test_swap_order_body_variants_uses_position_side():
variants = htx_v5.swap_order_body_variants(
contract_code="DOGE-USDT",
volume=1,
side="buy",
order_price_type="opponent",
preferred_hedge=True,
)
assert all("offset" not in v and "trade_type" not in v for v in variants)
assert any(v.get("position_side") == "long" for v in variants)
assert any(v.get("position_side") == "both" for v in variants)
assert all(v.get("type") == "market" for v in variants)
hedge_only = htx_v5.swap_order_body_variants(
contract_code="DOGE-USDT",
volume=1,
side="sell",
order_price_type="opponent",
reduce_only=True,
hedge_only=True,
)
assert hedge_only[0].get("position_side") == "long"
assert hedge_only[0].get("reduce_only") == 1
def test_is_multi_asset_v1_unavailable():
assert htx_v5.is_multi_asset_v1_unavailable(
"The Multi-Assets Collateral mode is temporarily unavailable."
)
def test_place_swap_order_v1():
c = HtxClient(api_key="k", secret_key="s", market_type="swap")
body = htx_v5.build_v1_cross_order_body(
contract_code="BTC-USDT",
volume=1,
side="buy",
lever_rate=5,
order_price_type="opponent",
)
v1_resp = {"status": "ok", "data": {"order_id_str": "888"}}
with patch.object(c, "_swap_private_request_raw", return_value=v1_resp) as mock_v1:
res = c._place_swap_order_v1(body)
assert res.exchange_order_id == "888"
sent = mock_v1.call_args.kwargs.get("json_body") or mock_v1.call_args[1].get("json_body")
assert sent.get("direction") == "buy"
assert sent.get("order_price_type") == "opponent"
def test_place_swap_order_v5_only():
c = HtxClient(api_key="k", secret_key="s", market_type="swap")
body = htx_v5.build_swap_order_body(
contract_code="BTC-USDT",
volume=1,
side="buy",
order_price_type="opponent",
position_side="long",
)
assert body.get("side") == "buy"
assert body.get("type") == "market"
assert body.get("position_side") == "long"
assert "direction" not in body
assert "order_price_type" not in body
assert "offset" not in body
assert "lever_rate" not in body
v5_resp = {"status": "ok", "data": {"order_id_str": "999"}}
with patch.object(c, "_swap_v5_request", return_value=v5_resp) as mock_v5:
res = c._place_swap_order(body)
assert res.exchange_order_id == "999"
sent = mock_v5.call_args.kwargs.get("json_body") or mock_v5.call_args[1].get("json_body")
assert sent.get("side") == "buy"
assert sent.get("type") == "market"
def test_htx_swap_market_order_uses_mode_fallback():
c = HtxClient(api_key="k", secret_key="s", market_type="swap")
with patch.object(c, "get_swap_hedge_mode", return_value=False):
with patch.object(c, "_base_to_contracts", return_value=2):
with patch.object(c, "_place_swap_order_with_mode_fallback") as mock_place:
mock_place.return_value = type("R", (), {"exchange_order_id": "1"})()
c.place_market_order(symbol="DOGE/USDT", side="buy", qty=100.0)
mock_place.assert_called_once()
def test_is_single_asset_mode_unavailable():
assert htx_v5.is_single_asset_mode_unavailable(
"The Single-Asset Collateral mode is temporarily unavailable."
)
assert not htx_v5.is_single_asset_mode_unavailable("insufficient margin")
def test_v5_request_retries_after_mode_switch():
c = HtxClient(api_key="k", secret_key="s", market_type="swap")
fail_raw = {"code": 400, "message": "The Single-Asset Collateral mode is temporarily unavailable."}
ok_raw = {"code": 200, "data": {"details": [{"currency": "USDT", "available": "1", "equity": "1"}]}}
with patch.object(c, "_swap_private_request_raw", side_effect=[fail_raw, ok_raw]):
with patch.object(c, "_try_upgrade_to_multi_asset_mode", return_value=True):
out = c._swap_v5_request("GET", "/v5/account/balance")
assert htx_v5.v5_ok(out)
def test_cancel_order_swap_v5():
c = HtxClient(api_key="k", secret_key="s", market_type="swap")
with patch.object(c, "_swap_v5_request", return_value={"code": 200}) as mock_v5:
c.cancel_order(symbol="BTC/USDT", order_id="123")
mock_v5.assert_called_once()
assert mock_v5.call_args[0][1] == "/v5/trade/cancel_order"