Fix exchange minimum sizing for buys

This commit is contained in:
Codex
2026-06-27 15:41:08 +03:00
parent baf307041a
commit 07d132632d
3 changed files with 83 additions and 11 deletions
+53 -10
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from collections import deque
from datetime import timedelta
from decimal import Decimal, ROUND_DOWN
from decimal import Decimal, ROUND_DOWN, ROUND_UP
from typing import Iterable
from uuid import uuid4
@@ -25,6 +25,15 @@ def _round_step(value: float, step: float) -> float:
return float(rounded * step_decimal)
def _round_step_up(value: float, step: float) -> float:
if step <= 0:
return value
value_decimal = Decimal(str(value))
step_decimal = Decimal(str(step))
rounded = (value_decimal / step_decimal).to_integral_value(rounding=ROUND_UP)
return float(rounded * step_decimal)
class PaperBroker:
def __init__(self, settings: Settings, storage: Storage):
self.settings = settings
@@ -133,13 +142,15 @@ class PaperBroker:
) -> Position | None:
fill_price = self._buy_price(ticker)
minimum_budget = self._minimum_entry_budget(instrument)
notional = self._entry_budget(signal, ticker, minimum_notional=minimum_budget)
if notional < max(self.settings.min_position_usdt, minimum_budget):
minimum_budget = self._minimum_entry_budget(instrument, fill_price)
budget = self._entry_budget(signal, ticker, minimum_notional=minimum_budget)
if budget < max(self.settings.min_position_usdt, minimum_budget):
self.storage.event(f"{ticker.symbol}: покупка пропущена, adaptive-лимит экспозиции исчерпан", "WARN")
return None
notional = notional / (1 + self.settings.taker_fee_rate)
notional = budget / (1 + self.settings.taker_fee_rate)
qty = _round_step(notional / fill_price, instrument.qty_step if instrument else 0)
if instrument:
qty = self._raise_qty_to_exchange_minimum(qty, fill_price, instrument, budget)
if instrument and qty < instrument.min_order_qty:
self.storage.event(f"{ticker.symbol}: количество ниже minOrderQty Bybit", "WARN")
return None
@@ -267,13 +278,44 @@ class PaperBroker:
value = default
return max(low, min(high, value))
def _minimum_entry_budget(self, instrument: Instrument | None) -> float:
def _minimum_entry_budget(self, instrument: Instrument | None, fill_price: float | None = None) -> float:
minimum = max(0.0, self.settings.min_position_usdt)
if instrument and instrument.min_notional_value > 0:
exchange_minimum = instrument.min_notional_value * (1 + self.settings.taker_fee_rate) * 1.002 + 0.01
minimum = max(minimum, exchange_minimum)
if instrument:
exchange_notional = max(0.0, instrument.min_notional_value)
if fill_price and fill_price > 0:
minimum_qty = max(0.0, instrument.min_order_qty)
if exchange_notional > 0:
minimum_qty = max(
minimum_qty,
_round_step_up(exchange_notional / fill_price, instrument.qty_step),
)
if minimum_qty > 0:
exchange_notional = max(exchange_notional, minimum_qty * fill_price)
if exchange_notional > 0:
exchange_minimum = exchange_notional * (1 + self.settings.taker_fee_rate) * 1.002 + 0.01
minimum = max(minimum, exchange_minimum)
return minimum
def _raise_qty_to_exchange_minimum(
self,
qty: float,
fill_price: float,
instrument: Instrument,
budget: float,
) -> float:
minimum_qty = max(0.0, instrument.min_order_qty)
if instrument.min_notional_value > 0 and fill_price > 0:
minimum_qty = max(
minimum_qty,
_round_step_up(instrument.min_notional_value / fill_price, instrument.qty_step),
)
if minimum_qty <= qty:
return qty
minimum_cost = minimum_qty * fill_price * (1 + self.settings.taker_fee_rate)
if minimum_cost <= budget + 1e-9:
return minimum_qty
return qty
def _entry_budget(
self,
signal: Signal,
@@ -335,7 +377,8 @@ class LiveBroker(PaperBroker):
instrument: Instrument | None,
prices: dict[str, float],
) -> Position | None:
minimum_budget = self._minimum_entry_budget(instrument)
fill_price = self._buy_price(ticker)
minimum_budget = self._minimum_entry_budget(instrument, fill_price)
requested_notional = min(
max(self._signal_notional(signal), minimum_budget),
self.settings.live_order_max_usdt,