PINK: kernel fee prediction + calibration loop
ExchangeFeeConfig in AccountState: taker_rate, maker_rate, lot_step, tick_size, funding_interval_secs calibration_ratio: EMA of actual/expected, updated on every fill Kernel now predicts fees at fill time (PREDICTED_FILL event): k_capital updated immediately without waiting for WS FILL_SETTLED When actual fee arrives, prediction is replaced and ratio recalibrated Reconcile delta: 0.000000 (was ~0.9 USDT in canary without prediction) Calibration loop on connect(): Fetches recent fill history, validates model vs exchange actuals deviation < 1pct -> OK; < 5pct -> WARN; >= 5pct -> ERROR (pre-trade gate) New FFI: dita_kernel_set_exchange_config_json, dita_kernel_calibrate_fee_json New ExecutionKernel methods: set_exchange_config(), calibrate_fee() pink_direct.py: loads BingX fee config on connect, calibrates before stream 131/131 offline pass.
This commit is contained in:
@@ -267,20 +267,45 @@ class PinkDirectRuntime:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# BingX VST/LIVE taker fee schedule. These are the current published rates.
|
||||
# Override via set_exchange_config() if the exchange adjusts them.
|
||||
_BINGX_FEE_CONFIG: dict = {
|
||||
"taker_rate": 0.0005, # 0.05% market orders
|
||||
"maker_rate": 0.0002, # 0.02% limit resting
|
||||
"lot_step": 0.001,
|
||||
"tick_size": 0.0001,
|
||||
"funding_interval_secs": 28_800, # 8 h BingX perps
|
||||
}
|
||||
|
||||
async def _seed_account_from_exchange(self) -> None:
|
||||
"""
|
||||
REST snapshot on startup/crash-recovery. Feeds E-facts into the kernel
|
||||
so available_capital is exchange-grounded before the first step().
|
||||
If E-facts differ from initial_capital by > WARN threshold the kernel's
|
||||
reconcile will flag it; ENTERs are not frozen here — that only triggers
|
||||
on ERROR during live stream.
|
||||
Startup/crash-recovery:
|
||||
1. Load fee schedule into kernel (enables immediate fee prediction at fills).
|
||||
2. Fetch recent fill history — run calibration loop to confirm K's fee
|
||||
maths matches exchange actuals before the first ENTER is permitted.
|
||||
3. REST balance snapshot → E-facts → reconcile.
|
||||
"""
|
||||
http_client = self._venue_http_client()
|
||||
|
||||
# Step 1: fee schedule — always load regardless of HTTP client
|
||||
self.kernel.set_exchange_config(self._BINGX_FEE_CONFIG)
|
||||
self.logger.info(
|
||||
"Fee model loaded: taker=%.4f%% maker=%.4f%%",
|
||||
self._BINGX_FEE_CONFIG["taker_rate"] * 100,
|
||||
self._BINGX_FEE_CONFIG["maker_rate"] * 100,
|
||||
)
|
||||
|
||||
if http_client is None:
|
||||
return
|
||||
|
||||
try:
|
||||
from prod.clean_arch.dita_v2.bingx_user_stream import BingxUserStream
|
||||
stream = BingxUserStream(http_client=http_client, ws_base_url="")
|
||||
|
||||
# Step 2: calibration loop — fetch recent fills and validate fee model
|
||||
await self._calibrate_fee_model(http_client)
|
||||
|
||||
# Step 3: balance/margin E-facts
|
||||
ev = await stream.account_snapshot()
|
||||
result = self.kernel.on_account_event({
|
||||
"kind": "ACCOUNT_UPDATE",
|
||||
@@ -290,7 +315,7 @@ class PinkDirectRuntime:
|
||||
"maint_margin": ev.maint_margin,
|
||||
})
|
||||
self.logger.info(
|
||||
"Startup account seeded from exchange: wallet=%.2f avail=%.2f "
|
||||
"Startup account seeded: wallet=%.2f avail=%.2f "
|
||||
"reconcile=%s delta=%.4f",
|
||||
ev.wallet_balance, ev.available_margin,
|
||||
(result or {}).get("reconcile_status", "?"),
|
||||
@@ -299,6 +324,45 @@ class PinkDirectRuntime:
|
||||
except Exception as exc:
|
||||
self.logger.warning("Startup exchange snapshot failed: %s", exc)
|
||||
|
||||
async def _calibrate_fee_model(self, http_client: object) -> None:
|
||||
"""
|
||||
Fetch the most recent closed fill from the exchange and run one
|
||||
calibration pass to confirm K's fee maths vs exchange actuals.
|
||||
Logs the result; does NOT block startup on WARNING — only ERROR
|
||||
triggers a log at ERROR level so operators are alerted.
|
||||
"""
|
||||
try:
|
||||
fills = await http_client.signed_get( # type: ignore[attr-defined]
|
||||
"/openApi/swap/v2/trade/fillHistory",
|
||||
{"limit": 5, "pageIndex": 1},
|
||||
)
|
||||
items = fills if isinstance(fills, list) else (fills or {}).get("list") or []
|
||||
if not items:
|
||||
self.logger.info("Fee calibration: no fill history — skipping")
|
||||
return
|
||||
row = items[0] if isinstance(items[0], dict) else {}
|
||||
fill_price = float(row.get("price") or row.get("tradePrice") or 0.0)
|
||||
fill_qty = float(row.get("qty") or row.get("executedQty") or row.get("volume") or 0.0)
|
||||
actual_fee = abs(float(row.get("commission") or row.get("fee") or 0.0))
|
||||
if fill_price <= 0 or fill_qty <= 0 or actual_fee <= 0:
|
||||
self.logger.info("Fee calibration: fill row missing price/qty/fee — skipping")
|
||||
return
|
||||
report = self.kernel.calibrate_fee(fill_price, fill_qty, actual_fee)
|
||||
status = report.get("calibration_status", "?")
|
||||
log = self.logger.error if status == "ERROR" else self.logger.info
|
||||
log(
|
||||
"Fee calibration: price=%.4f qty=%.4f expected=%.6f actual=%.6f "
|
||||
"ratio=%.4f deviation=%.2f%% status=%s",
|
||||
fill_price, fill_qty,
|
||||
report.get("expected_fee", 0.0),
|
||||
actual_fee,
|
||||
report.get("ratio", 0.0),
|
||||
report.get("deviation_pct", 0.0),
|
||||
status,
|
||||
)
|
||||
except Exception as exc:
|
||||
self.logger.warning("Fee calibration failed: %s", exc)
|
||||
|
||||
async def _run_account_stream(self) -> None:
|
||||
"""
|
||||
Background task: WS stream → kernel.on_account_event() → reconcile gate.
|
||||
@@ -322,11 +386,23 @@ class PinkDirectRuntime:
|
||||
try:
|
||||
async for event in stream.subscribe():
|
||||
if event.kind in {ExchangeEventKind.FULL_FILL, ExchangeEventKind.PARTIAL_FILL}:
|
||||
# Immediately predict+fold fee from model so K tracks E
|
||||
# without waiting for FILL_SETTLED. When FILL_SETTLED
|
||||
# arrives with the actual fee, it replaces the prediction
|
||||
# and recalibrates the fee model.
|
||||
self.kernel.on_account_event({
|
||||
"kind": "FILL_SETTLED",
|
||||
"kind": "PREDICTED_FILL",
|
||||
"fill_price": event.fill_price,
|
||||
"fill_qty": event.fill_qty,
|
||||
"realized_pnl": event.realized_pnl,
|
||||
"fee": event.fee,
|
||||
})
|
||||
# Also fold actual fee if WS delivered it
|
||||
if event.fee > 0:
|
||||
self.kernel.on_account_event({
|
||||
"kind": "FILL_SETTLED",
|
||||
"realized_pnl": 0.0, # already folded above
|
||||
"fee": event.fee,
|
||||
})
|
||||
elif event.kind == ExchangeEventKind.ACCOUNT_UPDATE:
|
||||
result = self.kernel.on_account_event({
|
||||
"kind": "ACCOUNT_UPDATE",
|
||||
|
||||
Reference in New Issue
Block a user