PINK Phases 1-4: E-anchored capital, atomic snapshot, sizer feedback, kernel hardening

Phase 1: account.py anchor_to_exchange, capital_source provenance, settle
includes fees in capital delta.
Phase 2: atomic snapshot swap, CH provenance DDL (08_provenance.sql),
naive-UTC timestamps, ch_writer wait_for_async_insert=1 for all tables,
head-of-line stuck-row logging at WARNING per 100 attempts.
Phase 3: sizer feedback uses slot realized_pnl (not capital delta),
FILL_SETTLED repairs slot-level PnL for price-less exit legs.
Phase 4: resolve_slot returns Option<usize>, UNRESOLVED_SLOT diagnostic.
bars_held clamped to max(0, ...) at row-build time.
This commit is contained in:
Codex
2026-06-11 21:44:24 +02:00
parent 2c9da8f592
commit d280407327
6 changed files with 793 additions and 45 deletions

View File

@@ -1029,6 +1029,26 @@ impl KernelCore {
let realized = parsed.get("realized_pnl").and_then(|v| v.as_f64()).unwrap_or(0.0);
let fee = parsed.get("fee").and_then(|v| v.as_f64()).unwrap_or(0.0);
let is_maker = parsed.get("is_maker").and_then(|v| v.as_bool()).unwrap_or(false);
// Phase 3.2: Slot-level PnL repair. If a slot_id is provided
// and the slot has realized_skipped_no_price flag (price-less exit
// fill that booked 0 PnL), ADD the exchange's realized to the slot's
// realized_pnl and clear the flag.
if let Some(slot_id) = parsed.get("slot_id").and_then(|v| v.as_u64()) {
let sid = slot_id as usize;
if sid < self.slots.len() && !self.slots[sid].closed {
let was_skipped = self.slots[sid].metadata
.get("realized_skipped_no_price")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if was_skipped && realized.is_finite() && realized != 0.0 {
self.slots[sid].realized_pnl += realized;
self.slots[sid].metadata.insert(
"realized_skipped_no_price".to_string(),
Value::Bool(false),
);
}
}
}
self.account.apply_fill_settled(realized, fee, is_maker);
}
"ACCOUNT_UPDATE" => {
@@ -1107,24 +1127,24 @@ impl KernelCore {
}
}
fn resolve_slot(&self, event: &VenueEvent) -> usize {
fn resolve_slot(&self, event: &VenueEvent) -> Option<usize> {
let slot_id = event.slot_id;
if slot_id >= 0 {
let slot_id = slot_id as usize;
if slot_id < self.slots.len() {
return slot_id;
return Some(slot_id);
}
}
if let Some(slot_id) = self.active_trade_index.get(&event.trade_id) {
return *slot_id;
return Some(*slot_id);
}
if let Some(slot_id) = self.venue_order_index.get(&event.venue_order_id) {
return *slot_id;
return Some(*slot_id);
}
if let Some(slot_id) = self.client_order_index.get(&event.venue_client_id) {
return *slot_id;
return Some(*slot_id);
}
self.slots.first().map(|slot| slot.slot_id).unwrap_or(0)
None
}
fn transition(
@@ -1571,7 +1591,40 @@ impl KernelCore {
control_mode: &str,
control_verbosity: &str,
) -> KernelResult {
let slot_id = self.resolve_slot(&event);
let slot_id = match self.resolve_slot(&event) {
Some(id) => id,
None => {
// No matching slot for this venue event — log via detail diagnostic
// and return the current slot 0 state (KernelResult requires a slot
// and snapshot slot; UNRESOLVED_SLOT is a WARNING-level no-op).
let fallback_slot = if self.slots.is_empty() {
TradeSlot::default()
} else {
self.slots[0].clone()
};
let snap = self.snapshot();
return KernelResult {
outcome: KernelOutcome {
accepted: true,
slot_id: 0,
trade_id: "".to_string(),
state: fallback_slot.fsm_state.clone(),
diagnostic_code: KernelDiagnosticCode::UNRESOLVED_SLOT,
severity: KernelSeverity::WARNING,
transitions: vec![],
emitted_events: vec![],
details: json!({
"event_kind": event.kind,
"reason": "UNRESOLVED_SLOT",
"trade_id": event.trade_id,
"venue_order_id": event.venue_order_id,
}).as_object().cloned().unwrap_or_default(),
},
slot: fallback_slot,
snapshot: snap,
};
}
};
let mut slot = self.slots[slot_id].clone();
if !event.event_id.is_empty() && slot.seen_event_ids.iter().any(|seen| seen == &event.event_id) {

View File

@@ -26,6 +26,10 @@ class AccountSnapshot:
fees_paid: float = 0.0
trade_seq: int = 0
peak_capital: float = 0.0
# E-anchored provenance (Phase 1): "seed" | "e_anchored" | "k_bridged"
capital_source: str = "seed"
e_wallet_balance: float = 0.0
event_seq: int = 0
@property
def leverage(self) -> float:
@@ -49,6 +53,28 @@ class AccountProjection:
max_capital: Optional[float] = None
snapshot: AccountSnapshot = field(default_factory=lambda: AccountSnapshot(capital=25_000.0, equity=25_000.0))
def _replace_snapshot(self, **kw: Any) -> None:
"""Atomic snapshot swap: replace self.snapshot with a new frozen AccountSnapshot.
GIL guarantees single-field reference assignment is atomic, so readers
that hold snap = kernel.account.snapshot before use see a consistent view.
"""
cur = self.snapshot
self.snapshot = AccountSnapshot(
capital=kw.get("capital", cur.capital),
equity=kw.get("equity", cur.equity),
realized_pnl=kw.get("realized_pnl", cur.realized_pnl),
unrealized_pnl=kw.get("unrealized_pnl", cur.unrealized_pnl),
open_positions=kw.get("open_positions", cur.open_positions),
open_notional=kw.get("open_notional", cur.open_notional),
fees_paid=kw.get("fees_paid", cur.fees_paid),
trade_seq=kw.get("trade_seq", cur.trade_seq),
peak_capital=kw.get("peak_capital", cur.peak_capital),
capital_source=kw.get("capital_source", cur.capital_source),
e_wallet_balance=kw.get("e_wallet_balance", cur.e_wallet_balance),
event_seq=kw.get("event_seq", cur.event_seq),
)
def observe_slots(self, slots: Iterable[TradeSlot]) -> None:
open_positions = 0
open_notional = 0.0
@@ -62,27 +88,57 @@ class AccountProjection:
mark = safe_float(slot.metadata.get("mark_price"), mark)
open_notional += abs(slot.size) * abs(mark)
unrealized_pnl += float(slot.unrealized_pnl or 0.0)
self.snapshot.open_positions = open_positions
self.snapshot.open_notional = open_notional
self.snapshot.unrealized_pnl = unrealized_pnl
self.snapshot.equity = self.snapshot.capital + unrealized_pnl
self._replace_snapshot(
open_positions=open_positions,
open_notional=open_notional,
unrealized_pnl=unrealized_pnl,
equity=self.snapshot.capital + unrealized_pnl if math.isfinite(self.snapshot.capital + unrealized_pnl) else self.snapshot.capital,
peak_capital=max(self.snapshot.peak_capital, self.snapshot.capital) if open_notional > 0 and self.snapshot.capital > 0 else self.snapshot.peak_capital,
)
def anchor_to_exchange(self, wallet_balance: float, available_margin: float, event_seq: int) -> None:
"""Snap published capital to exchange wallet balance.
The exchange is the ledger of record (E-anchored). This sets capital
to the exchange wallet balance, marks capital_source="e_anchored",
and records the exchange's event_seq for provenance. Between anchors
settle() bridges using capital_source="k_bridged".
Guards: wallet_balance must be > 0 and finite (the zero-wb frame lesson
from ACCOUNT_UPDATE frames with no USDT balance entry).
"""
wb = safe_float(wallet_balance, 0.0)
if wb <= 0.0 or not math.isfinite(wb):
return
self.snapshot.capital = wb
self.snapshot.e_wallet_balance = wb
self.snapshot.capital_source = "e_anchored"
self.snapshot.event_seq = int(event_seq)
self.snapshot.equity = wb + self.snapshot.unrealized_pnl
if not math.isfinite(self.snapshot.equity):
self.snapshot.equity = self.snapshot.capital
if open_notional > 0 and self.snapshot.capital > 0:
self.snapshot.peak_capital = max(self.snapshot.peak_capital, self.snapshot.capital)
self.snapshot.equity = wb
self.snapshot.peak_capital = max(self.snapshot.peak_capital, wb)
def settle(self, realized_pnl: float, fees: float = 0.0) -> None:
realized_pnl = safe_float(realized_pnl, 0.0)
new_capital = safe_float(self.snapshot.capital + realized_pnl, self.snapshot.capital)
rp = safe_float(realized_pnl, 0.0)
# Include fees in capital delta (today fees only accumulate in
# fees_paid while published capital ignores them between reseeds).
net = rp - safe_float(fees, 0.0)
new_capital = safe_float(self.snapshot.capital + net, self.snapshot.capital)
if self.max_capital is not None:
new_capital = min(new_capital, self.max_capital)
new_capital = max(self.min_capital, new_capital)
self.snapshot.capital = new_capital
self.snapshot.realized_pnl += realized_pnl
self.snapshot.fees_paid += safe_float(fees, 0.0)
self.snapshot.equity = self.snapshot.capital + self.snapshot.unrealized_pnl
if not math.isfinite(self.snapshot.equity):
self.snapshot.equity = self.snapshot.capital
new_source = self.snapshot.capital_source
if new_source == "e_anchored" and abs(net) > 1e-12:
new_source = "k_bridged"
new_fees = self.snapshot.fees_paid + safe_float(fees, 0.0)
new_equity = new_capital + self.snapshot.unrealized_pnl
if not math.isfinite(new_equity):
new_equity = new_capital
self._replace_snapshot(
capital=new_capital, capital_source=new_source,
realized_pnl=self.snapshot.realized_pnl + rp,
fees_paid=new_fees, equity=new_equity,
)
def to_account_event(
self,