feat: persist skipped ledger fills

Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-08-24 13:23:06 +00:00
committed by GitHub
co-authored by genichin
parent df0258bc36
commit 76cde2edd0
19 changed files with 1246 additions and 151 deletions
@@ -0,0 +1,247 @@
from __future__ import annotations
from datetime import UTC, date, datetime, time, timedelta
from typing import Any
from uuid import UUID
from trex_core.adapters.persistence.postgresql import _require_psycopg
from trex_core.application.ledger_skip import LedgerSkipRecord, make_ledger_skip_key
class PostgreSQLLedgerSkipStore:
def __init__(self, dsn: str, *, psycopg_module: Any = None) -> None:
self._psycopg = _require_psycopg(psycopg_module)
self._dsn = dsn
def record_skip(
self,
*,
instance_key: str,
slot_id: UUID,
portfolio_id: str | None,
symbol: str | None,
order_id: str | None,
execution_id: str | None,
side: str,
qty: float,
price: float,
executed_at: datetime,
order_venue: str | None,
execution_venue: str | None,
skip_reason: str,
) -> tuple[LedgerSkipRecord, bool]:
skip_key = make_ledger_skip_key(
instance_key=instance_key,
slot_id=slot_id,
side=side,
order_id=order_id,
execution_id=execution_id,
skip_reason=skip_reason,
)
now = datetime.now(tz=UTC)
notify = False
with self._psycopg.connect(self._dsn, autocommit=False) as conn, conn.cursor() as cur:
cur.execute(
"SELECT status FROM ledger_skip WHERE skip_key = %s FOR UPDATE",
(skip_key,),
)
row = cur.fetchone()
if row is None:
notify = True
cur.execute(
"INSERT INTO ledger_skip "
"(skip_key, instance_key, slot_id, portfolio_id, symbol, order_id, "
" execution_id, side, qty, price, executed_at, order_venue, "
" execution_venue, skip_reason, status, created_at, updated_at) "
"VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, "
" 'unresolved', %s, %s)",
(
skip_key,
instance_key,
slot_id,
portfolio_id,
symbol,
order_id,
execution_id,
side,
qty,
price,
executed_at,
order_venue or "unknown",
execution_venue or "unknown",
skip_reason,
now,
now,
),
)
else:
notify = row[0] == "resolved"
cur.execute(
"UPDATE ledger_skip SET portfolio_id=%s, symbol=%s, order_id=%s, "
"execution_id=%s, qty=%s, price=%s, executed_at=%s, "
"order_venue=%s, execution_venue=%s, status='unresolved', "
"resolution_reason=NULL, resolved_at=NULL, updated_at=%s "
"WHERE skip_key=%s",
(
portfolio_id,
symbol,
order_id,
execution_id,
qty,
price,
executed_at,
order_venue or "unknown",
execution_venue or "unknown",
now,
skip_key,
),
)
conn.commit()
record = self.get(skip_key)
assert record is not None
return record, notify
def get(self, skip_key: str) -> LedgerSkipRecord | None:
with self._psycopg.connect(self._dsn) as conn, conn.cursor() as cur:
cur.execute(
"SELECT skip_key, instance_key, slot_id, portfolio_id, symbol, order_id, "
"execution_id, side, qty, price, executed_at, order_venue, execution_venue, "
"skip_reason, status, resolution_reason, resolved_at, created_at, updated_at "
"FROM ledger_skip WHERE skip_key = %s",
(skip_key,),
)
row = cur.fetchone()
return self._row_to_record(row) if row else None
def list_records(
self,
*,
instance_key: str | None = None,
portfolio_id: str | None = None,
on_date: date | None = None,
status: str = "unresolved",
limit: int = 100,
) -> list[LedgerSkipRecord]:
clauses: list[str] = []
params: list[object] = []
if status != "all":
clauses.append("status = %s")
params.append(status)
if instance_key is not None:
clauses.append("instance_key = %s")
params.append(instance_key)
if portfolio_id is not None:
clauses.append("portfolio_id = %s")
params.append(portfolio_id)
if on_date is not None:
start = datetime.combine(on_date, time.min, tzinfo=UTC)
end = start + timedelta(days=1)
clauses.append("created_at >= %s AND created_at < %s")
params.extend([start, end])
where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
with self._psycopg.connect(self._dsn) as conn, conn.cursor() as cur:
cur.execute(
"SELECT skip_key, instance_key, slot_id, portfolio_id, symbol, order_id, "
"execution_id, side, qty, price, executed_at, order_venue, execution_venue, "
"skip_reason, status, resolution_reason, resolved_at, created_at, updated_at "
f"FROM ledger_skip {where} ORDER BY created_at DESC LIMIT %s",
(*params, limit),
)
rows = cur.fetchall()
return [self._row_to_record(row) for row in rows]
def resolve_for_fill(
self,
*,
instance_key: str,
slot_id: UUID,
side: str,
order_id: str | None,
execution_id: str | None,
resolution_reason: str,
) -> list[LedgerSkipRecord]:
now = datetime.now(tz=UTC)
with self._psycopg.connect(self._dsn, autocommit=False) as conn, conn.cursor() as cur:
if execution_id:
cur.execute(
"UPDATE ledger_skip SET status='resolved', resolution_reason=%s, "
"resolved_at=%s, updated_at=%s "
"WHERE status='unresolved' AND instance_key=%s AND slot_id=%s "
"AND side=%s AND execution_id=%s "
"RETURNING skip_key, instance_key, slot_id, portfolio_id, symbol, "
"order_id, execution_id, side, qty, price, executed_at, order_venue, "
"execution_venue, skip_reason, status, resolution_reason, resolved_at, "
"created_at, updated_at",
(
resolution_reason,
now,
now,
instance_key,
slot_id,
side,
execution_id,
),
)
else:
cur.execute(
"UPDATE ledger_skip SET status='resolved', resolution_reason=%s, "
"resolved_at=%s, updated_at=%s "
"WHERE status='unresolved' AND instance_key=%s AND slot_id=%s "
"AND side=%s AND order_id=%s "
"RETURNING skip_key, instance_key, slot_id, portfolio_id, symbol, "
"order_id, execution_id, side, qty, price, executed_at, order_venue, "
"execution_venue, skip_reason, status, resolution_reason, resolved_at, "
"created_at, updated_at",
(
resolution_reason,
now,
now,
instance_key,
slot_id,
side,
order_id,
),
)
rows = cur.fetchall()
conn.commit()
return [self._row_to_record(row) for row in rows]
def resolve_by_key(self, skip_key: str, *, resolution_reason: str) -> LedgerSkipRecord | None:
now = datetime.now(tz=UTC)
with self._psycopg.connect(self._dsn, autocommit=False) as conn, conn.cursor() as cur:
cur.execute(
"UPDATE ledger_skip SET status='resolved', resolution_reason=%s, "
"resolved_at=%s, updated_at=%s "
"WHERE skip_key=%s AND status='unresolved' "
"RETURNING skip_key, instance_key, slot_id, portfolio_id, symbol, order_id, "
"execution_id, side, qty, price, executed_at, order_venue, execution_venue, "
"skip_reason, status, resolution_reason, resolved_at, created_at, updated_at",
(resolution_reason, now, now, skip_key),
)
row = cur.fetchone()
conn.commit()
return self._row_to_record(row) if row else None
@staticmethod
def _row_to_record(row: tuple) -> LedgerSkipRecord:
return LedgerSkipRecord(
skip_key=row[0],
instance_key=row[1],
slot_id=row[2],
portfolio_id=row[3],
symbol=row[4],
order_id=row[5],
execution_id=row[6],
side=row[7],
qty=float(row[8]),
price=float(row[9]),
executed_at=row[10],
order_venue=row[11],
execution_venue=row[12],
skip_reason=row[13],
status=row[14],
resolution_reason=row[15],
resolved_at=row[16],
created_at=row[17],
updated_at=row[18],
)
@@ -0,0 +1,30 @@
CREATE TABLE IF NOT EXISTS ledger_skip (
skip_key TEXT PRIMARY KEY,
instance_key TEXT NOT NULL,
slot_id UUID NOT NULL,
portfolio_id TEXT NULL,
symbol TEXT NULL,
order_id TEXT NULL,
execution_id TEXT NULL,
side TEXT NOT NULL CHECK (side IN ('buy', 'sell')),
qty DOUBLE PRECISION NOT NULL,
price DOUBLE PRECISION NOT NULL,
executed_at TIMESTAMPTZ NOT NULL,
order_venue TEXT NOT NULL,
execution_venue TEXT NOT NULL,
skip_reason TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('unresolved', 'resolved')),
resolution_reason TEXT NULL,
resolved_at TIMESTAMPTZ NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_ledger_skip_status_created
ON ledger_skip (status, created_at);
CREATE INDEX IF NOT EXISTS idx_ledger_skip_instance_portfolio_created
ON ledger_skip (instance_key, portfolio_id, created_at);
CREATE INDEX IF NOT EXISTS idx_ledger_skip_execution_id
ON ledger_skip (execution_id);