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>
227 lines
8.3 KiB
Python
227 lines
8.3 KiB
Python
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),
|
|
}
|