Files
eezd 285b3952d7 Support selectable demo exchanges and configurable analysis limits
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.
2026-10-20 18:45:00 +08:00

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