Files
siloqy/prod/bingx/friction.py

227 lines
8.3 KiB
Python
Raw Normal View History

from __future__ import annotations
from decimal import Decimal
from typing import Any
from nautilus_trader.model.enums import LiquiditySide
from nautilus_trader.model.enums import OrderType
from nautilus_trader.model.enums import TimeInForce
from nautilus_trader.model.objects import Price
from nautilus_trader.model.objects import Quantity
from nautilus_trader.model.orders import Order
def _decimal(value: Any) -> Decimal | None:
if value in (None, "", "null"):
return None
try:
return Decimal(str(value))
except Exception:
return None
def _decimal_text(value: Decimal | None) -> str | None:
if value is None:
return None
text = format(value.normalize(), "f")
if "." in text:
text = text.rstrip("0").rstrip(".")
return text or "0"
def _fill_quality_score(
*,
slippage_bps: Decimal | None,
fee_bps: Decimal | None,
liquidity_side: LiquiditySide,
) -> tuple[Decimal, str]:
"""Return a compact fill-quality score and label.
The score is intentionally conservative: 100 is ideal, and lower scores
reflect adverse slippage plus fee drag. Maker fills are allowed a small
bonus because they are typically less toxic than taker fills.
"""
score = Decimal("100")
if slippage_bps is not None:
score -= abs(slippage_bps)
if fee_bps is not None:
score -= abs(fee_bps)
if liquidity_side == LiquiditySide.MAKER:
score += Decimal("1")
score = max(Decimal("0"), min(Decimal("100"), score))
if score >= Decimal("99"):
label = "excellent"
elif score >= Decimal("95"):
label = "good"
elif score >= Decimal("85"):
label = "fair"
else:
label = "poor"
return score, label
def _price_from_order(order: Order) -> Decimal | None:
if getattr(order, "has_price", False):
price = getattr(order, "price", None)
if price is not None:
try:
return price.as_decimal()
except Exception:
return _decimal(price)
return None
def infer_liquidity_side(order: Order, row: dict[str, Any] | None = None) -> LiquiditySide:
row = row or {}
explicit = row.get("liquiditySide") or row.get("liquidity_side")
if isinstance(explicit, str):
upper = explicit.upper()
if "MAKER" in upper:
return LiquiditySide.MAKER
if "TAKER" in upper:
return LiquiditySide.TAKER
for key in ("isMaker", "maker", "m"):
value = row.get(key)
if isinstance(value, bool):
return LiquiditySide.MAKER if value else LiquiditySide.TAKER
if isinstance(value, str):
lower = value.strip().lower()
if lower in {"true", "1", "yes", "maker"}:
return LiquiditySide.MAKER
if lower in {"false", "0", "no", "taker"}:
return LiquiditySide.TAKER
if bool(getattr(order, "is_post_only", False)):
return LiquiditySide.MAKER
tif = getattr(order, "time_in_force", None)
if tif in {TimeInForce.IOC, TimeInForce.FOK}:
return LiquiditySide.TAKER
if order.order_type in {
OrderType.MARKET,
OrderType.STOP_MARKET,
OrderType.MARKET_IF_TOUCHED,
OrderType.TRAILING_STOP_MARKET,
}:
return LiquiditySide.TAKER
if order.order_type in {
OrderType.LIMIT,
OrderType.STOP_LIMIT,
OrderType.LIMIT_IF_TOUCHED,
OrderType.TRAILING_STOP_LIMIT,
}:
return LiquiditySide.MAKER
return LiquiditySide.NO_LIQUIDITY_SIDE
def reference_price(order: Order, row: dict[str, Any] | None = None) -> tuple[Decimal | None, str]:
row = row or {}
for key in ("referencePrice", "referencePx", "expectedPrice", "expectedPx", "bestPrice"):
value = _decimal(row.get(key))
if value is not None and value > 0:
return value, key
if getattr(order, "has_price", False):
price = _price_from_order(order)
if price is not None and price > 0:
return price, "order_price"
if getattr(order, "has_trigger_price", False) and order.order_type in {
OrderType.STOP_MARKET,
OrderType.MARKET_IF_TOUCHED,
OrderType.TRAILING_STOP_MARKET,
}:
trigger = getattr(order, "trigger_price", None)
if trigger is not None:
try:
value = trigger.as_decimal()
except Exception:
value = _decimal(trigger)
if value is not None and value > 0:
return value, "trigger_price"
return None, "unavailable"
def _commission_quote(
*,
commission_amount: Decimal,
commission_asset: str,
quote_currency: str,
base_currency: str,
last_px: Decimal | None,
estimate_quote: Decimal,
) -> tuple[Decimal, str]:
if commission_amount == 0:
return estimate_quote, "estimated"
if commission_asset == quote_currency:
return commission_amount, "quote"
if commission_asset == base_currency and last_px is not None:
return commission_amount * last_px, "base_converted"
return estimate_quote, "estimated"
def estimate_friction(
order: Order,
row: dict[str, Any],
*,
last_qty: Quantity | None = None,
last_px: Price | None = None,
quote_currency: str,
base_currency: str,
maker_fee: Decimal,
taker_fee: Decimal,
feed_source: str = "unknown",
) -> dict[str, Any]:
qty = last_qty.as_decimal() if last_qty is not None else _decimal(row.get("lastFilledQty") or row.get("executedQty")) or Decimal("0")
px = last_px.as_decimal() if last_px is not None else _decimal(row.get("lastFillPrice") or row.get("avgPrice") or row.get("price"))
notional = qty * px if px is not None else Decimal("0")
liquidity_side = infer_liquidity_side(order, row)
fee_rate = maker_fee if liquidity_side == LiquiditySide.MAKER else taker_fee if liquidity_side == LiquiditySide.TAKER else taker_fee
estimated_fee_quote = notional * fee_rate if notional > 0 else Decimal("0")
commission_amount = _decimal(row.get("commission")) or Decimal("0")
commission_asset = str(row.get("commissionAsset") or quote_currency)
actual_fee_quote, commission_source = _commission_quote(
commission_amount=commission_amount,
commission_asset=commission_asset,
quote_currency=quote_currency,
base_currency=base_currency,
last_px=px,
estimate_quote=estimated_fee_quote,
)
reference_px, reference_source = reference_price(order, row)
slippage_quote = None
slippage_bps = None
if reference_px is not None and px is not None and qty > 0:
side = getattr(order.side, "name", str(order.side)).upper()
delta = px - reference_px if side == "BUY" else reference_px - px
slippage_quote = delta * qty
if reference_px > 0:
slippage_bps = (delta / reference_px) * Decimal("10000")
net_friction_quote = actual_fee_quote + (slippage_quote or Decimal("0"))
gross_friction_quote = abs(actual_fee_quote) + abs(slippage_quote or Decimal("0"))
fee_bps = None
if notional > 0:
fee_bps = (actual_fee_quote / notional) * Decimal("10000")
fill_quality_score, fill_quality_class = _fill_quality_score(
slippage_bps=slippage_bps,
fee_bps=fee_bps,
liquidity_side=liquidity_side,
)
return {
"liquidity_side": getattr(liquidity_side, "name", str(liquidity_side)),
"feed_source": str(feed_source or "unknown"),
"commission_asset": commission_asset,
"commission_source": commission_source,
"commission_quote": _decimal_text(actual_fee_quote),
"estimated_fee_quote": _decimal_text(estimated_fee_quote),
"fee_rate": _decimal_text(fee_rate),
"fee_bps": _decimal_text(fee_bps),
"reference_px": _decimal_text(reference_px),
"reference_source": reference_source,
"slippage_quote": _decimal_text(slippage_quote),
"slippage_bps": _decimal_text(slippage_bps),
"net_friction_quote": _decimal_text(net_friction_quote),
"gross_friction_quote": _decimal_text(gross_friction_quote),
"fill_quality_score": _decimal_text(fill_quality_score),
"fill_quality_class": fill_quality_class,
"notional_quote": _decimal_text(notional),
"last_qty": _decimal_text(qty),
"last_px": _decimal_text(px),
}