feat: persist skipped ledger fills
Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
co-authored by
genichin
parent
df0258bc36
commit
76cde2edd0
@@ -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);
|
||||
Reference in New Issue
Block a user