PINK: fix fee calibration poisoning + fee_settled trade_id linkage
Two related accounting fixes: 1. _calibrate_fee_model startup guard: before calling calibrate_fee, compute raw deviation from the published taker/maker rate (ignoring any stale calibration_ratio). If >15%, skip and log WARNING rather than letting a bad REST fill set calibration_ratio to ~0.8 and cause ESTIMATED fees to understate actuals by 20% for the entire session. 2. fee_settled_events trade_id: BingX WS does not echo back our clientOrderId in fill events (field "c" is empty). Was falling back to BingX's internal orderId (p-e-mq5.../p-x-mq5...) which can't be joined to trade_events. Now reads trade_id from kernel slot 0 (which retains the trade_id until the next ENTER) so fee_settled_events.trade_id = BTCUSDT-T-N. Added venue_order_id field to persist_fee_settled for bidirectional reconciliation. 128/128 tests green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -624,6 +624,7 @@ class PinkClickHousePersistence:
|
|||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
trade_id: str,
|
trade_id: str,
|
||||||
|
venue_order_id: str = "",
|
||||||
fee: float,
|
fee: float,
|
||||||
fee_asset: str = "USDT",
|
fee_asset: str = "USDT",
|
||||||
is_maker: bool = False,
|
is_maker: bool = False,
|
||||||
@@ -637,6 +638,9 @@ class PinkClickHousePersistence:
|
|||||||
When the WS ORDER_TRADE_UPDATE frame arrives with field "n" (actual
|
When the WS ORDER_TRADE_UPDATE frame arrives with field "n" (actual
|
||||||
commission), call this method to log the settled truth.
|
commission), call this method to log the settled truth.
|
||||||
|
|
||||||
|
trade_id should be our BTCUSDT-T-N format (from kernel slot).
|
||||||
|
venue_order_id is BingX's own orderId for bidirectional lookup.
|
||||||
|
|
||||||
The CH spool stores both the original estimated row AND this settled row.
|
The CH spool stores both the original estimated row AND this settled row.
|
||||||
Downstream queries can reconcile using:
|
Downstream queries can reconcile using:
|
||||||
SELECT trade_id, MAX(fee) FILTER(WHERE fee_source='WS_SETTLED') AS settled_fee,
|
SELECT trade_id, MAX(fee) FILTER(WHERE fee_source='WS_SETTLED') AS settled_fee,
|
||||||
@@ -650,6 +654,7 @@ class PinkClickHousePersistence:
|
|||||||
self._sink("fee_settled_events", {
|
self._sink("fee_settled_events", {
|
||||||
"ts": ts_val.isoformat() if hasattr(ts_val, "isoformat") else str(ts_val),
|
"ts": ts_val.isoformat() if hasattr(ts_val, "isoformat") else str(ts_val),
|
||||||
"trade_id": trade_id,
|
"trade_id": trade_id,
|
||||||
|
"venue_order_id": venue_order_id,
|
||||||
"fee": float(fee),
|
"fee": float(fee),
|
||||||
"fee_asset": fee_asset,
|
"fee_asset": fee_asset,
|
||||||
"fee_source": "WS_SETTLED",
|
"fee_source": "WS_SETTLED",
|
||||||
|
|||||||
@@ -473,6 +473,23 @@ class PinkDirectRuntime:
|
|||||||
return
|
return
|
||||||
order_type = str(row.get("orderType") or row.get("type") or "MARKET").upper()
|
order_type = str(row.get("orderType") or row.get("type") or "MARKET").upper()
|
||||||
is_maker = order_type == "LIMIT"
|
is_maker = order_type == "LIMIT"
|
||||||
|
# Guard: check raw deviation (ignoring any stale calibration_ratio) before
|
||||||
|
# mutating kernel state. A REST fill with >15% deviation from published rate
|
||||||
|
# poisons calibration_ratio and causes ESTIMATED fees to drift from actuals.
|
||||||
|
raw_rate = (
|
||||||
|
self._BINGX_FEE_CONFIG.get("maker_rate", 0.0002) if is_maker
|
||||||
|
else self._BINGX_FEE_CONFIG.get("taker_rate", 0.0005)
|
||||||
|
)
|
||||||
|
raw_expected = fill_price * fill_qty * raw_rate
|
||||||
|
raw_deviation_pct = abs(actual_fee - raw_expected) / raw_expected * 100 if raw_expected > 0 else 0.0
|
||||||
|
if raw_deviation_pct > 15.0:
|
||||||
|
self.logger.warning(
|
||||||
|
"Fee calibration SKIPPED: REST fill shows %.2f%% deviation from "
|
||||||
|
"published %.4f%% rate (expected=%.6f actual=%.6f). "
|
||||||
|
"Holding calibration_ratio=1.0 to avoid poisoning kernel fee model.",
|
||||||
|
raw_deviation_pct, raw_rate * 100, raw_expected, actual_fee,
|
||||||
|
)
|
||||||
|
return
|
||||||
report = self.kernel.calibrate_fee(fill_price, fill_qty, actual_fee, is_maker=is_maker)
|
report = self.kernel.calibrate_fee(fill_price, fill_qty, actual_fee, is_maker=is_maker)
|
||||||
status = report.get("calibration_status", "?")
|
status = report.get("calibration_status", "?")
|
||||||
log = self.logger.error if status == "ERROR" else self.logger.info
|
log = self.logger.error if status == "ERROR" else self.logger.info
|
||||||
@@ -536,11 +553,25 @@ class PinkDirectRuntime:
|
|||||||
# downstream can reconcile against the ESTIMATED_TAKER row.
|
# downstream can reconcile against the ESTIMATED_TAKER row.
|
||||||
if self.persistence is not None:
|
if self.persistence is not None:
|
||||||
try:
|
try:
|
||||||
# trade_id: best-effort from the client_order_id field ("c")
|
# BingX WS does not echo back our clientOrderId ("c" field
|
||||||
# or order_id ("i") — WS may not carry our trade_id directly.
|
# is empty). Read trade_id from the kernel slot instead —
|
||||||
ws_trade_id = str(event.client_order_id or event.order_id or "")
|
# the slot retains its trade_id until the next ENTER.
|
||||||
|
# Store BingX's own orderId as venue_order_id for
|
||||||
|
# bidirectional reconciliation.
|
||||||
|
_venue_order_id = str(event.order_id or "")
|
||||||
|
try:
|
||||||
|
_slot_dict = self.kernel.slot(0).to_dict() if self.kernel.max_slots > 0 else {}
|
||||||
|
_our_trade_id = str(_slot_dict.get("trade_id") or "")
|
||||||
|
except Exception:
|
||||||
|
_our_trade_id = ""
|
||||||
|
# Fall back: if clientOrderId was echoed (future BingX change)
|
||||||
|
# parse our trade_id prefix from "BTCUSDT-T-N:intent_id"
|
||||||
|
if not _our_trade_id:
|
||||||
|
_c = str(event.client_order_id or "")
|
||||||
|
_our_trade_id = _c.split(":")[0] if ":" in _c else (_c or _venue_order_id)
|
||||||
self.persistence.persist_fee_settled(
|
self.persistence.persist_fee_settled(
|
||||||
trade_id=ws_trade_id,
|
trade_id=_our_trade_id,
|
||||||
|
venue_order_id=_venue_order_id,
|
||||||
fee=event.fee,
|
fee=event.fee,
|
||||||
fee_asset=event.fee_asset or "USDT",
|
fee_asset=event.fee_asset or "USDT",
|
||||||
is_maker=event.is_maker,
|
is_maker=event.is_maker,
|
||||||
|
|||||||
Reference in New Issue
Block a user