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
@@ -1,10 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from types import SimpleNamespace
|
||||
from uuid import UUID
|
||||
|
||||
from trex_core.application.ledger_outbox import OutboxOutcome
|
||||
from trex_core.application.ledger_port import LedgerTrade
|
||||
from trex_core.application.ledger_skip import LedgerSkipRecord
|
||||
from trex_core.runtime import cli
|
||||
|
||||
|
||||
@@ -63,6 +66,33 @@ def _evidence():
|
||||
}
|
||||
|
||||
|
||||
def _skip_record():
|
||||
return LedgerSkipRecord(
|
||||
skip_key="ledger-skip:inst-1:slot-1:sell:12345678:0001234567:kis-daily-ccld:20260824:12345678:0001234567:10:missing_execution_id",
|
||||
instance_key="inst-1",
|
||||
slot_id=UUID(_SLOT_ID),
|
||||
portfolio_id="PF-1",
|
||||
symbol="005930",
|
||||
order_id="12345678:0001234567",
|
||||
execution_id="kis-daily-ccld:20260824:12345678:0001234567:10",
|
||||
side="sell",
|
||||
qty=10.0,
|
||||
price=70000.0,
|
||||
executed_at=datetime.fromisoformat("2026-08-24T00:01:02+00:00"),
|
||||
order_venue="SOR",
|
||||
execution_venue="unknown",
|
||||
skip_reason="missing_execution_id",
|
||||
status="resolved",
|
||||
resolution_reason="manual_ledger_backfill",
|
||||
resolved_at=datetime.fromisoformat("2026-08-24T00:03:02+00:00"),
|
||||
created_at=datetime.fromisoformat("2026-08-24T00:01:02+00:00"),
|
||||
updated_at=datetime.fromisoformat("2026-08-24T00:03:02+00:00"),
|
||||
)
|
||||
|
||||
|
||||
_SLOT_ID = "00000000-0000-0000-0000-000000000001"
|
||||
|
||||
|
||||
def test_ledger_backfill_parser_accepts_required_flags():
|
||||
parser = cli._build_parser()
|
||||
args = parser.parse_args([
|
||||
@@ -80,7 +110,7 @@ def test_ledger_backfill_dry_run_emits_trade_and_writes_audit(monkeypatch, tmp_p
|
||||
monkeypatch.setattr(cli, "_lookup_ledger_backfill_evidence",
|
||||
lambda cfg, **kwargs: (_evidence(), SimpleNamespace(mode="env", identity_label="kis-config")))
|
||||
monkeypatch.setattr(cli, "_resolve_ledger_backfill_slot",
|
||||
lambda dsn, **kwargs: {"slot_id": "slot-1", "portfolio_id": "PF-1", "status": "inactive", "symbol": "005930"})
|
||||
lambda dsn, **kwargs: {"slot_id": _SLOT_ID, "portfolio_id": "PF-1", "status": "inactive", "symbol": "005930"})
|
||||
monkeypatch.setattr(cli, "_lookup_outbox_by_execution_id", lambda dsn, **kwargs: [])
|
||||
|
||||
rc = cli._cmd_ledger_backfill(_args(tmp_path, dry_run=True), _cfg(), lambda _: True)
|
||||
@@ -99,7 +129,7 @@ def test_ledger_backfill_live_requires_live_flag(monkeypatch, tmp_path, capsys):
|
||||
monkeypatch.setattr(cli, "_lookup_ledger_backfill_evidence",
|
||||
lambda cfg, **kwargs: (_evidence(), SimpleNamespace(mode="env", identity_label="kis-config")))
|
||||
monkeypatch.setattr(cli, "_resolve_ledger_backfill_slot",
|
||||
lambda dsn, **kwargs: {"slot_id": "slot-1", "portfolio_id": "PF-1", "status": "inactive", "symbol": "005930"})
|
||||
lambda dsn, **kwargs: {"slot_id": _SLOT_ID, "portfolio_id": "PF-1", "status": "inactive", "symbol": "005930"})
|
||||
monkeypatch.setattr(cli, "_lookup_outbox_by_execution_id", lambda dsn, **kwargs: [])
|
||||
|
||||
rc = cli._cmd_ledger_backfill(_args(tmp_path, live=False), _cfg(live=True), lambda _: True)
|
||||
@@ -115,7 +145,7 @@ def test_ledger_backfill_existing_completed_row_is_idempotent(monkeypatch, tmp_p
|
||||
monkeypatch.setattr(cli, "_lookup_ledger_backfill_evidence",
|
||||
lambda cfg, **kwargs: (_evidence(), SimpleNamespace(mode="env", identity_label="kis-config")))
|
||||
monkeypatch.setattr(cli, "_resolve_ledger_backfill_slot",
|
||||
lambda dsn, **kwargs: {"slot_id": "slot-1", "portfolio_id": "PF-1", "status": "inactive", "symbol": "005930"})
|
||||
lambda dsn, **kwargs: {"slot_id": _SLOT_ID, "portfolio_id": "PF-1", "status": "inactive", "symbol": "005930"})
|
||||
existing = [{
|
||||
"correlation_id": "PF-1:12345678:0001234567:kis-daily-ccld:20260824:12345678:0001234567:10",
|
||||
"asset_id": "PF-1",
|
||||
@@ -147,12 +177,23 @@ def test_ledger_backfill_existing_completed_row_is_idempotent(monkeypatch, tmp_p
|
||||
def process_correlation_id(self, correlation_id):
|
||||
raise AssertionError("completed execution must not be re-processed")
|
||||
|
||||
class _Store:
|
||||
def __init__(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
def resolve_for_fill(self, **_kwargs):
|
||||
return []
|
||||
|
||||
import trex_core.adapters.ledger.portfolio_hub as hub_mod
|
||||
import trex_core.adapters.persistence.postgresql as pg_mod
|
||||
|
||||
monkeypatch.setattr(pg_mod, "PostgreSQLLedgerOutbox", lambda dsn: _Outbox())
|
||||
monkeypatch.setattr(hub_mod, "PortfolioHubLedger", lambda *args, **kwargs: object())
|
||||
monkeypatch.setattr(cli, "LedgerWriter", _Writer)
|
||||
monkeypatch.setattr(
|
||||
"trex_core.adapters.persistence.ledger_skip_store.PostgreSQLLedgerSkipStore",
|
||||
_Store,
|
||||
)
|
||||
|
||||
rc = cli._cmd_ledger_backfill(_args(tmp_path), _cfg(), lambda _: True)
|
||||
|
||||
@@ -165,7 +206,7 @@ def test_ledger_backfill_live_enqueue_and_processes_exact_trade(monkeypatch, tmp
|
||||
monkeypatch.setattr(cli, "_lookup_ledger_backfill_evidence",
|
||||
lambda cfg, **kwargs: (_evidence(), SimpleNamespace(mode="env", identity_label="kis-config")))
|
||||
monkeypatch.setattr(cli, "_resolve_ledger_backfill_slot",
|
||||
lambda dsn, **kwargs: {"slot_id": "slot-1", "portfolio_id": "PF-1", "status": "inactive", "symbol": "005930"})
|
||||
lambda dsn, **kwargs: {"slot_id": _SLOT_ID, "portfolio_id": "PF-1", "status": "inactive", "symbol": "005930"})
|
||||
monkeypatch.setattr(cli, "_lookup_outbox_by_execution_id", lambda dsn, **kwargs: [])
|
||||
|
||||
class _Outbox:
|
||||
@@ -195,12 +236,23 @@ def test_ledger_backfill_live_enqueue_and_processes_exact_trade(monkeypatch, tmp
|
||||
assert correlation_id == self.seen_outbox.trade.correlation_id
|
||||
return OutboxOutcome.COMPLETED
|
||||
|
||||
class _Store:
|
||||
def __init__(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
def resolve_for_fill(self, **_kwargs):
|
||||
return []
|
||||
|
||||
import trex_core.adapters.ledger.portfolio_hub as hub_mod
|
||||
import trex_core.adapters.persistence.postgresql as pg_mod
|
||||
|
||||
monkeypatch.setattr(pg_mod, "PostgreSQLLedgerOutbox", lambda dsn: outbox)
|
||||
monkeypatch.setattr(hub_mod, "PortfolioHubLedger", lambda *args, **kwargs: object())
|
||||
monkeypatch.setattr(cli, "LedgerWriter", _Writer)
|
||||
monkeypatch.setattr(
|
||||
"trex_core.adapters.persistence.ledger_skip_store.PostgreSQLLedgerSkipStore",
|
||||
_Store,
|
||||
)
|
||||
|
||||
rc = cli._cmd_ledger_backfill(_args(tmp_path), _cfg(), lambda _: True)
|
||||
|
||||
@@ -210,3 +262,67 @@ def test_ledger_backfill_live_enqueue_and_processes_exact_trade(monkeypatch, tmp
|
||||
assert outbox.trade is not None
|
||||
assert outbox.trade.order_venue == "SOR"
|
||||
assert outbox.trade.venue is None
|
||||
|
||||
|
||||
def test_ledger_backfill_resolves_matching_skip(monkeypatch, tmp_path, capsys):
|
||||
monkeypatch.setattr(cli, "_lookup_ledger_backfill_evidence",
|
||||
lambda cfg, **kwargs: (_evidence(), SimpleNamespace(mode="env", identity_label="kis-config")))
|
||||
monkeypatch.setattr(cli, "_resolve_ledger_backfill_slot",
|
||||
lambda dsn, **kwargs: {"slot_id": _SLOT_ID, "portfolio_id": "PF-1", "status": "inactive", "symbol": "005930"})
|
||||
monkeypatch.setattr(cli, "_lookup_outbox_by_execution_id", lambda dsn, **kwargs: [])
|
||||
|
||||
class _Outbox:
|
||||
def enqueue(self, trade: LedgerTrade):
|
||||
self.trade = trade
|
||||
|
||||
def get(self, correlation_id):
|
||||
return SimpleNamespace(
|
||||
status=SimpleNamespace(value="completed"),
|
||||
attempts=1,
|
||||
last_outcome=SimpleNamespace(value="completed"),
|
||||
last_error=None,
|
||||
)
|
||||
|
||||
class _Writer:
|
||||
def __init__(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
def process_correlation_id(self, _correlation_id):
|
||||
return OutboxOutcome.COMPLETED
|
||||
|
||||
class _Store:
|
||||
def __init__(self, *_args, **_kwargs):
|
||||
pass
|
||||
|
||||
def resolve_for_fill(self, **_kwargs):
|
||||
return [_skip_record()]
|
||||
|
||||
published = []
|
||||
|
||||
class _Emitter:
|
||||
def publish_alarm(self, code, title, *, fields=None, severity="critical"):
|
||||
published.append((code, severity, fields))
|
||||
|
||||
class _Transport:
|
||||
def close(self):
|
||||
return None
|
||||
|
||||
import trex_core.adapters.ledger.portfolio_hub as hub_mod
|
||||
import trex_core.adapters.persistence.postgresql as pg_mod
|
||||
|
||||
monkeypatch.setattr(pg_mod, "PostgreSQLLedgerOutbox", lambda dsn: _Outbox())
|
||||
monkeypatch.setattr(hub_mod, "PortfolioHubLedger", lambda *args, **kwargs: object())
|
||||
monkeypatch.setattr(cli, "LedgerWriter", _Writer)
|
||||
monkeypatch.setattr(
|
||||
"trex_core.adapters.persistence.ledger_skip_store.PostgreSQLLedgerSkipStore",
|
||||
_Store,
|
||||
)
|
||||
monkeypatch.setattr(cli, "_default_alarm_emitter", lambda cfg: (_Emitter(), _Transport()))
|
||||
|
||||
rc = cli._cmd_ledger_backfill(_args(tmp_path), _cfg(), lambda _: True)
|
||||
|
||||
assert rc == 0
|
||||
out = json.loads(capsys.readouterr().out)
|
||||
assert len(out["resolved_skips"]) == 1
|
||||
assert out["resolved_skips"][0]["resolution_reason"] == "manual_ledger_backfill"
|
||||
assert published[0][0] == "ledger_skip_resolved"
|
||||
|
||||
Reference in New Issue
Block a user