mirror of
https://github.com/OpenByteInc/QuantDinger.git
synced 2026-09-28 23:32:55 +08:00
151 lines
4.8 KiB
Python
151 lines
4.8 KiB
Python
"""Regression coverage for exchange step edges and generated numeric literals."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import re
|
|
import time
|
|
from decimal import Decimal
|
|
|
|
import pytest
|
|
|
|
from app.services.live_trading.binance import BinanceFuturesClient
|
|
from app.services.live_trading.binance_spot import BinanceSpotClient
|
|
from app.services.live_trading.bitget import BitgetMixClient
|
|
from app.services.live_trading.bitget_spot import BitgetSpotClient
|
|
from app.services.live_trading.bybit import BybitClient
|
|
from app.services.live_trading.gate import GateUsdtFuturesClient
|
|
from app.services.live_trading.htx import HtxClient
|
|
from app.services.live_trading.okx import OkxClient
|
|
from app.services.pending_order_worker import PendingOrderWorker
|
|
from app.services.strategy_runtime.robot_v2 import build_robot_v2_source
|
|
from app.utils.numeric_precision import (
|
|
clean_generated_number,
|
|
floor_decimal_to_step,
|
|
format_decimal,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"normalizer",
|
|
[
|
|
BinanceFuturesClient._floor_to_step,
|
|
BinanceSpotClient._floor_to_step,
|
|
BybitClient._floor_to_step,
|
|
OkxClient._floor_to_step,
|
|
BitgetMixClient._floor_to_step,
|
|
BitgetSpotClient._floor_to_step,
|
|
],
|
|
)
|
|
def test_exchange_step_normalizers_snap_float_noise_at_exact_boundary(normalizer):
|
|
noisy_minimum = Decimal("0.00009999999999999994")
|
|
|
|
assert normalizer(noisy_minimum, Decimal("0.0001")) == Decimal("0.0001")
|
|
assert normalizer(Decimal("0.0000999"), Decimal("0.0001")) == Decimal("0")
|
|
|
|
|
|
def test_integer_contract_normalizers_snap_float_noise_at_exact_boundary():
|
|
noisy_contract = Decimal("0.9999999999999994")
|
|
|
|
assert GateUsdtFuturesClient._floor(noisy_contract) == Decimal("1")
|
|
assert HtxClient._floor_to_int(noisy_contract) == 1
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"normalizer",
|
|
[
|
|
BinanceFuturesClient._floor_to_precision,
|
|
BinanceSpotClient._floor_to_precision,
|
|
],
|
|
)
|
|
def test_binance_precision_fallback_snaps_float_noise_at_boundary(normalizer):
|
|
assert normalizer(Decimal("0.009999999999999994"), 2) == Decimal("0.01")
|
|
assert normalizer(Decimal("0.00999"), 2) == Decimal("0")
|
|
|
|
|
|
def test_okx_minimum_swap_close_survives_base_to_contract_conversion():
|
|
client = OkxClient(api_key="k", secret_key="s", passphrase="p")
|
|
client._inst_cache["SWAP:BTC-USDT-SWAP"] = (
|
|
time.time(),
|
|
{
|
|
"instId": "BTC-USDT-SWAP",
|
|
"ctVal": "0.01",
|
|
"lotSz": "0.01",
|
|
"minSz": "0.01",
|
|
},
|
|
)
|
|
|
|
normalized, precision = client._normalize_order_size(
|
|
inst_id="BTC-USDT-SWAP",
|
|
market_type="swap",
|
|
size=9.999999999999994e-05,
|
|
)
|
|
|
|
assert normalized == Decimal("0.01")
|
|
assert precision == 2
|
|
assert client._dec_str(normalized, strict_precision=precision) == "0.01"
|
|
|
|
|
|
def test_precision_helpers_keep_user_facing_values_short():
|
|
assert floor_decimal_to_step("9.999999999999994e-05", "0.0001") == Decimal("0.0001")
|
|
assert clean_generated_number(1 / 3) == 0.333333333333
|
|
assert format_decimal(9.999999999999994e-05) == "0.0001"
|
|
assert format_decimal(1.0) == "1"
|
|
|
|
|
|
def test_close_size_error_does_not_recommend_increasing_leverage():
|
|
worker = object.__new__(PendingOrderWorker)
|
|
message = worker._friendly_order_error(
|
|
"Invalid size (below lot/min size): requested=0.0001",
|
|
client=object(),
|
|
exchange_id="okx",
|
|
symbol="BTC/USDT",
|
|
signal_type="close_long",
|
|
amount=0.0001,
|
|
price=63_500,
|
|
payload={
|
|
"sizing": {
|
|
"initial_capital": 1_000,
|
|
"entry_pct": 0.635,
|
|
"leverage": 1,
|
|
"source": "strategy_v2",
|
|
}
|
|
},
|
|
)
|
|
|
|
assert "remaining strategy position" in message
|
|
assert "reconcile the residual position" in message
|
|
assert "Increase capital" not in message
|
|
|
|
|
|
def test_grid_template_limits_fractional_literals_and_preserves_budget():
|
|
source = build_robot_v2_source(
|
|
"grid",
|
|
{
|
|
"side": "long",
|
|
"dynamic_anchor": True,
|
|
"start_price": 0.98,
|
|
"end_price": 1.02,
|
|
"grid_count": 7,
|
|
"grid_mode": "arithmetic",
|
|
"initial_position_pct": 1 / 3,
|
|
"max_open_orders": 7,
|
|
},
|
|
{"levels": []},
|
|
symbol="BTC/USDT",
|
|
market_type="swap",
|
|
timeframe="1m",
|
|
)
|
|
constants = source.split("def initialize", 1)[0]
|
|
|
|
assert "0.3333333333333333" not in constants
|
|
assert all(
|
|
len(fraction) <= 12
|
|
for fraction in re.findall(r"(?<![\w.])\d+\.(\d+)", constants)
|
|
)
|
|
budget_line = next(
|
|
line for line in constants.splitlines() if line.startswith("CELL_BUDGET_PCTS =")
|
|
)
|
|
budget_values = ast.literal_eval(budget_line.split("=", 1)[1].strip())
|
|
assert sum(budget_values) == pytest.approx(1.0, abs=1e-12)
|