Persist manual requests until exchange confirmation, preserve stop protection, and reconcile fills before settling plans. Tighten model stream, crawler, skill, research accounting, and frontend race handling, with regression coverage. Keep queued analyses pending until a fresh account snapshot is available.
92 lines
4.4 KiB
Python
92 lines
4.4 KiB
Python
import asyncio
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
from decimal import Decimal
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from alphapulse.trading.bybit import BybitDemoClient, normalize_order, normalize_trade
|
|
from alphapulse.trading.client import ExchangeError
|
|
from alphapulse.trading.plan import OrderSpec
|
|
|
|
|
|
@pytest.mark.parametrize("method,params", [("GET", {"symbol": "BTCUSDT", "cursor": "a:b+="}), ("POST", {"reduceOnly": True, "qty": "0.001"})])
|
|
def test_signature_matches_exact_transmitted_payload(method, params):
|
|
def handle(request):
|
|
payload = request.url.query.decode() if method == "GET" else request.content.decode()
|
|
headers = request.headers
|
|
signed = headers["X-BAPI-TIMESTAMP"] + "test-key" + headers["X-BAPI-RECV-WINDOW"] + payload
|
|
assert headers["X-BAPI-SIGN"] == hmac.new(b"test-secret", signed.encode(), hashlib.sha256).hexdigest()
|
|
assert request.url.host == "api-demo.bybit.com"
|
|
if method == "POST":
|
|
assert json.loads(payload) == params
|
|
return httpx.Response(200, json={"retCode": 0, "result": {"confirmed": True}})
|
|
|
|
async def check():
|
|
async with BybitDemoClient("test-key", "test-secret") as client:
|
|
await client.http.aclose()
|
|
client.http = httpx.AsyncClient(base_url="https://api-demo.bybit.com", transport=httpx.MockTransport(handle))
|
|
assert await client.request(method, "/v5/test", params) == {"confirmed": True}
|
|
asyncio.run(check())
|
|
|
|
|
|
def test_conditional_stop_ack_keeps_execution_unknown():
|
|
def handle(request):
|
|
params = json.loads(request.content)
|
|
assert (params["orderType"], params["triggerDirection"], params["triggerBy"]) == ("Market", 2, "MarkPrice")
|
|
assert params["reduceOnly"] and params["closeOnTrigger"]
|
|
assert params["positionIdx"] == 0
|
|
assert params["orderLinkId"] == "ap42"
|
|
return httpx.Response(200, json={"retCode": 0, "result": {"orderId": "7b6a537e-6f74-43d9-b775-35d0521c20de"}})
|
|
|
|
async def check():
|
|
async with BybitDemoClient("test-key", "test-secret") as client:
|
|
await client.http.aclose()
|
|
client.http = httpx.AsyncClient(base_url="https://api-demo.bybit.com", transport=httpx.MockTransport(handle))
|
|
result = await client.place_order("BTCUSDT", "ap42", OrderSpec("stop", None, "SELL", "STOP_MARKET", None, Decimal("70000"), Decimal("0.001")), True)
|
|
assert result["status"] == "SUBMITTING"
|
|
asyncio.run(check())
|
|
|
|
|
|
def test_filled_conditional_order_preserves_execution_and_uuid():
|
|
order_id = "7b6a537e-6f74-43d9-b775-35d0521c20de"
|
|
result = normalize_order({
|
|
"symbol": "BTCUSDT", "orderId": order_id, "orderLinkId": "ap42", "side": "Sell", "orderType": "Market",
|
|
"orderStatus": "Filled", "price": "", "qty": "0.001", "cumExecQty": "0.001", "avgPrice": "70000",
|
|
"reduceOnly": True, "createdTime": "1792400000000", "triggerPrice": "70100",
|
|
"stopOrderType": "Stop", "triggerDirection": 2, "closeOnTrigger": True,
|
|
}, True)
|
|
assert (result["status"], result["executedQty"], result["avgPrice"]) == ("FILLED", "0.001", "70000")
|
|
assert result["actualOrderId"] == result["algoId"] == order_id
|
|
|
|
|
|
def test_trade_uses_realized_cashflow_without_subtracting_fee_twice():
|
|
result = normalize_trade({"symbol": "BTCUSDT", "tradeId": "trade-uuid", "orderId": "order-uuid", "side": "Sell",
|
|
"tradePrice": "81000", "qty": "0.001", "cashFlow": "1", "fee": "0.04", "currency": "USDT",
|
|
"transactionTime": "1792400000000"})
|
|
assert (result["realizedPnl"], result["commission"], result["id"]) == ("1", "0.04", "trade-uuid")
|
|
|
|
|
|
def test_timeout_response_is_unknown_and_not_resent():
|
|
calls = []
|
|
|
|
def handle(request):
|
|
calls.append(request)
|
|
return httpx.Response(200, json={"retCode": 10000, "retMsg": "Server Timeout", "result": {}})
|
|
|
|
async def check():
|
|
async with BybitDemoClient("test-key", "test-secret") as client:
|
|
await client.http.aclose()
|
|
client.http = httpx.AsyncClient(base_url="https://api-demo.bybit.com", transport=httpx.MockTransport(handle))
|
|
with pytest.raises(ExchangeError) as error:
|
|
await client.request("POST", "/v5/order/create", {"orderLinkId": "ap42"})
|
|
assert error.value.retryable
|
|
assert len(calls) == 1
|
|
asyncio.run(check())
|
|
|
|
|
|
def test_binance_backend_timeout_is_not_definitive_rejection():
|
|
assert ExchangeError(408, -1007, "Timeout").retryable
|