Fix unmanaged reconciliation review blockers
Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
co-authored by
genichin
parent
f9284d1de8
commit
591db7adb7
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from datetime import UTC, date, datetime, timedelta, timezone
|
||||
from typing import Literal, Protocol
|
||||
|
||||
from trex_core.adapters.broker.paper import PaperBroker
|
||||
@@ -33,6 +33,7 @@ class BrokerCredentialUnavailable(RuntimeError):
|
||||
|
||||
class BrokerProvider(Protocol):
|
||||
def get_broker_for_read(self) -> tuple[BrokerPort, BrokerSource]: ...
|
||||
def get_broker_for_reconciliation(self) -> tuple[BrokerPort, BrokerSource]: ...
|
||||
def refresh_day_start_credential(self) -> BrokerSource: ...
|
||||
|
||||
|
||||
@@ -80,7 +81,7 @@ class ConfigBrokerProvider:
|
||||
def __init__(self, config: DaemonConfig, *, read_port=None, clock=None) -> None:
|
||||
self._config = config
|
||||
self._read = read_port
|
||||
self._clock = clock or (lambda: datetime.now(tz=timezone.utc))
|
||||
self._clock = clock or (lambda: datetime.now(tz=UTC))
|
||||
self._broker: BrokerPort | None = None
|
||||
self._source: BrokerSource | None = None
|
||||
self._credential_date: date | None = None
|
||||
@@ -101,7 +102,7 @@ class ConfigBrokerProvider:
|
||||
raise BrokerCredentialUnavailable("portfolio_hub_unavailable") from None
|
||||
except BrokerCredentialUnavailable:
|
||||
raise
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 - sanitize arbitrary read adapter failures
|
||||
raise BrokerCredentialUnavailable("portfolio_hub_unavailable") from None
|
||||
if ctx is None or getattr(ctx, "broker", None) is None:
|
||||
raise BrokerCredentialUnavailable("broker_absent")
|
||||
@@ -121,7 +122,7 @@ class ConfigBrokerProvider:
|
||||
self._broker = broker
|
||||
else:
|
||||
self._broker.reinitialize(credential) # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 - sanitize broker construction/reload failures
|
||||
raise BrokerCredentialUnavailable("broker_initialization_failed") from None
|
||||
self._credential_date = self._clock().astimezone(_KST).date()
|
||||
# Version is designated non-secret by the portfolio contract; no account/key material.
|
||||
@@ -147,6 +148,13 @@ class ConfigBrokerProvider:
|
||||
self._source = source
|
||||
return self._broker, source # type: ignore[return-value]
|
||||
|
||||
def get_broker_for_reconciliation(self) -> tuple[BrokerPort, BrokerSource]:
|
||||
broker, source = self.get_broker_for_read()
|
||||
authoritative_reader = getattr(broker, "authoritative_balance_reader", None)
|
||||
if callable(authoritative_reader):
|
||||
return authoritative_reader(), source
|
||||
return broker, source
|
||||
|
||||
def refresh_day_start_credential(self) -> BrokerSource:
|
||||
# Paper/env/break-glass paths deliberately do no portfolio-hub call.
|
||||
if self._config.broker != "kis" or not self._config.credential_fetch_enabled or _break_glass_enabled():
|
||||
|
||||
@@ -12,6 +12,7 @@ Subcommands:
|
||||
sync — sync slot qty to broker ground truth (direct-DB; REQ-105)
|
||||
verify — re-check C-case reconcile-warning (direct-DB; REQ-106)
|
||||
unmanaged list — query durable unmanaged-position lifecycle state
|
||||
unmanaged audit — inspect lifecycle/operator audit trail for one symbol
|
||||
unmanaged record — record explicit operator disposition + evidence reference
|
||||
ledger backfill — evidence-backed historical ledger recovery
|
||||
ledger skips list — query durable skipped-ledger records
|
||||
@@ -38,6 +39,7 @@ LedgerWriter path, and resolves matching durable skipped-ledger records.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import json as _json
|
||||
import logging
|
||||
import re
|
||||
@@ -78,6 +80,14 @@ def _resolve_instance(args) -> str:
|
||||
return topology.DEFAULT_INSTANCE_KEY
|
||||
|
||||
|
||||
def _operator_actor() -> str:
|
||||
try:
|
||||
username = getpass.getuser().strip()
|
||||
except Exception: # noqa: BLE001
|
||||
username = ""
|
||||
return f"cli:{username or 'unknown'}"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# default transport-backed RPC (overridable in tests)
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -659,6 +669,13 @@ def _build_parser() -> argparse.ArgumentParser:
|
||||
p_unmanaged_list.add_argument("--symbol", default=None)
|
||||
_add_common(p_unmanaged_list)
|
||||
|
||||
p_unmanaged_audit = p_unmanaged_sub.add_parser(
|
||||
"audit",
|
||||
help="Show lifecycle/operator audit trail for one unmanaged position",
|
||||
)
|
||||
p_unmanaged_audit.add_argument("symbol")
|
||||
_add_common(p_unmanaged_audit)
|
||||
|
||||
p_unmanaged_record = p_unmanaged_sub.add_parser(
|
||||
"record",
|
||||
help="Record an explicit operator disposition for an unmanaged position",
|
||||
@@ -670,7 +687,6 @@ def _build_parser() -> argparse.ArgumentParser:
|
||||
)
|
||||
p_unmanaged_record.add_argument("--evidence-ref", required=True)
|
||||
p_unmanaged_record.add_argument("--note", default="")
|
||||
p_unmanaged_record.add_argument("--actor", default="operator")
|
||||
_add_common(p_unmanaged_record)
|
||||
|
||||
p_ledger = sub.add_parser(
|
||||
@@ -1040,6 +1056,44 @@ def _cmd_unmanaged_list(args, cfg) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_unmanaged_audit(args, cfg) -> int:
|
||||
from trex_core.adapters.portfolio.unmanaged_position_store import (
|
||||
PostgreSQLUnmanagedPositionStore,
|
||||
)
|
||||
|
||||
instance = _resolve_instance(args)
|
||||
store = PostgreSQLUnmanagedPositionStore(cfg.dsn)
|
||||
state = store.get_state(instance_key=instance, symbol=args.symbol)
|
||||
if state is None:
|
||||
_fail(
|
||||
f"unmanaged position not found: instance={instance} symbol={args.symbol}",
|
||||
args.json,
|
||||
)
|
||||
return 1
|
||||
audit = store.list_audit(instance_key=instance, symbol=args.symbol)
|
||||
payload = {
|
||||
"instance_key": instance,
|
||||
"symbol": args.symbol,
|
||||
"state": state,
|
||||
"audit": audit,
|
||||
}
|
||||
if args.json:
|
||||
_emit(payload, as_json=True)
|
||||
else:
|
||||
print(
|
||||
f"instance={instance} symbol={args.symbol} lifecycle={state['lifecycle_state']} "
|
||||
f"entries={len(audit)}"
|
||||
)
|
||||
for row in audit:
|
||||
print(
|
||||
f" {row['event_type']}\tat={row['recorded_at']}\t"
|
||||
f"quantity={row.get('quantity')}\taction={row.get('action') or '-'}\t"
|
||||
f"actor={row.get('actor') or '-'}\tevidence_ref={row.get('evidence_ref') or '-'}\t"
|
||||
f"note={row.get('note') or '-'}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_unmanaged_record(args, cfg) -> int:
|
||||
from trex_core.adapters.portfolio.unmanaged_position_store import (
|
||||
PostgreSQLUnmanagedPositionStore,
|
||||
@@ -1051,7 +1105,7 @@ def _cmd_unmanaged_record(args, cfg) -> int:
|
||||
symbol=args.symbol,
|
||||
action=args.action,
|
||||
evidence_ref=args.evidence_ref,
|
||||
actor=args.actor,
|
||||
actor=_operator_actor(),
|
||||
note=args.note,
|
||||
)
|
||||
if state is None:
|
||||
@@ -1536,6 +1590,8 @@ def main(argv: list[str] | None = None, *, rpc=None, confirm=None) -> int:
|
||||
return _cmd_verify(args, cfg)
|
||||
if args.cmd == "unmanaged" and getattr(args, "unmanaged_cmd", None) == "list":
|
||||
return _cmd_unmanaged_list(args, cfg)
|
||||
if args.cmd == "unmanaged" and getattr(args, "unmanaged_cmd", None) == "audit":
|
||||
return _cmd_unmanaged_audit(args, cfg)
|
||||
if args.cmd == "unmanaged" and getattr(args, "unmanaged_cmd", None) == "record":
|
||||
return _cmd_unmanaged_record(args, cfg)
|
||||
|
||||
|
||||
@@ -213,6 +213,9 @@ class Daemon:
|
||||
from trex_core.runtime.broker_provider import build_broker_provider
|
||||
self.broker_provider = build_broker_provider(config, read_port=self.portfolio_read)
|
||||
self.broker, self.broker_source = self.broker_provider.get_broker_for_read()
|
||||
self.reconciliation_broker, self.reconciliation_broker_source = (
|
||||
self.broker_provider.get_broker_for_reconciliation()
|
||||
)
|
||||
self.account_snapshot_service: AccountSnapshotService | None = None
|
||||
if config.account_snapshot.enabled:
|
||||
if config.broker != "kis":
|
||||
@@ -271,6 +274,7 @@ class Daemon:
|
||||
read_port=self.portfolio_read,
|
||||
store=self.snapshot_store,
|
||||
broker=self.broker,
|
||||
core_holdings_broker=self.reconciliation_broker,
|
||||
schedule_kst=_parse_kst_time(config.snapshot_schedule.kst_time),
|
||||
# dcy-020 / REQ-058·062 (BAT-030): universe slot reconcile (creates/
|
||||
# retires the permanent asset slots) + core-holding divergence
|
||||
@@ -425,7 +429,7 @@ class Daemon:
|
||||
while not self.stop.wait(self.config.ledger_interval_sec):
|
||||
try:
|
||||
self.ledger_writer.run_once()
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception: # noqa: BLE001,S112
|
||||
continue
|
||||
|
||||
def _intent_rpc_loop(self) -> None:
|
||||
@@ -433,7 +437,7 @@ class Daemon:
|
||||
self._transports.append(transport)
|
||||
try:
|
||||
transport.declare_topology(self.config.instance_key)
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception: # noqa: BLE001,S110
|
||||
pass
|
||||
handler = make_intent_rpc_handler(
|
||||
self.intent_service,
|
||||
@@ -448,7 +452,7 @@ class Daemon:
|
||||
self._transports.append(transport)
|
||||
try:
|
||||
transport.declare_topology(self.config.instance_key)
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception: # noqa: BLE001,S110
|
||||
pass
|
||||
handler = make_slots_rpc_handler(
|
||||
self.slot_repo,
|
||||
@@ -472,7 +476,7 @@ class Daemon:
|
||||
self._transports.append(transport)
|
||||
try:
|
||||
transport.declare_topology(self.config.instance_key)
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception: # noqa: BLE001,S110
|
||||
pass
|
||||
handler = make_strategy_state_set_handler(
|
||||
self.slot_repo, self.strategy_state_store,
|
||||
@@ -486,7 +490,7 @@ class Daemon:
|
||||
self._transports.append(transport)
|
||||
try:
|
||||
transport.declare_topology(self.config.instance_key)
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception: # noqa: BLE001,S110
|
||||
pass
|
||||
handler = make_control_rpc_handler(
|
||||
self.pause_store,
|
||||
@@ -501,7 +505,7 @@ class Daemon:
|
||||
while not self.stop.wait(self.safety_sell.base_tick_sec):
|
||||
try:
|
||||
self.safety_sell.run_once()
|
||||
except Exception: # noqa: BLE001 - loop must survive transient errors
|
||||
except Exception: # noqa: BLE001,S112 - loop must survive transient errors
|
||||
continue
|
||||
|
||||
def _snapshot_scheduler_loop(self) -> None:
|
||||
@@ -566,7 +570,7 @@ class Daemon:
|
||||
for tr in self._transports:
|
||||
try:
|
||||
tr.close()
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception: # noqa: BLE001,S110
|
||||
pass
|
||||
|
||||
def run(self) -> None: # pragma: no cover - process entrypoint
|
||||
|
||||
Reference in New Issue
Block a user