repo hygiene: track the PINK launcher import closure
67 production .py modules that the running PINK service imports but which were never committed: prod/bingx/ (HTTP client, market/user streams, journal, config), prod/clean_arch/ adapters/persistence/runtime/dita/dita_v2 production modules and their co-located tests. Rule going forward: every module imported by launch_dolphin_pink.py / pink_direct.py must appear in git ls-files. Excludes _backup dirs, __pycache__, and non-code files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
226
prod/bingx/friction.py
Normal file
226
prod/bingx/friction.py
Normal file
@@ -0,0 +1,226 @@
|
||||
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),
|
||||
}
|
||||
Reference in New Issue
Block a user