test: cover unmanaged position lifecycle flow

Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-09-12 13:27:12 +00:00
committed by GitHub
co-authored by genichin
parent fe51d211d9
commit f2b825e720
6 changed files with 45 additions and 42 deletions
@@ -32,7 +32,7 @@ class FakeBroker:
class RaisingBroker(FakeBroker): class RaisingBroker(FakeBroker):
def get_balance(self): def get_balance(self):
raise BrokerBalanceError("broker_unavailable") raise BrokerBalanceError("transport_failed")
def _wire(): def _wire():
+1 -2
View File
@@ -96,7 +96,6 @@ def pg_clean(pg_dsn: str):
from trex_core.adapters.persistence.migrate import apply_migrations from trex_core.adapters.persistence.migrate import apply_migrations
apply_migrations(pg_dsn) apply_migrations(pg_dsn)
with psycopg.connect(pg_dsn, autocommit=True) as conn: with psycopg.connect(pg_dsn, autocommit=True) as conn, conn.cursor() as cur:
with conn.cursor() as cur:
cur.execute(f"TRUNCATE {', '.join(_PG_TABLES)}") cur.execute(f"TRUNCATE {', '.join(_PG_TABLES)}")
return pg_dsn return pg_dsn
+10 -7
View File
@@ -383,8 +383,11 @@ def test_sync_requires_yes_when_not_dry_run(capsys, monkeypatch):
"""sync without --yes (and not dry-run) should abort.""" """sync without --yes (and not dry-run) should abort."""
def _fake_sync(args, cfg, confirm): def _fake_sync(args, cfg, confirm):
# Simulate the confirmation prompt returning False (no --yes, interactive aborts) # Simulate the confirmation prompt returning False (no --yes, interactive aborts)
if not getattr(args, "dry_run", False) and not getattr(args, "yes", False): if (
if not confirm("Sync?"): not getattr(args, "dry_run", False)
and not getattr(args, "yes", False)
and not confirm("Sync?")
):
print("aborted") print("aborted")
return 1 return 1
return 0 return 0
@@ -398,7 +401,7 @@ def test_sync_live_requires_live_flag(capsys, monkeypatch):
monkeypatch.setattr(cli, "_broker_mode", lambda cfg: ("kis", True)) monkeypatch.setattr(cli, "_broker_mode", lambda cfg: ("kis", True))
def _fake_sync(args, cfg, confirm): def _fake_sync(args, cfg, confirm):
broker, is_live = cli._broker_mode(cfg) _broker, is_live = cli._broker_mode(cfg)
if is_live and not getattr(args, "live", False): if is_live and not getattr(args, "live", False):
cli._fail("live account requires --live (with --yes) to confirm a real sync", args.json) cli._fail("live account requires --live (with --yes) to confirm a real sync", args.json)
return 2 return 2
@@ -415,7 +418,7 @@ def test_sync_fail_closed_skipped_error_exits_nonzero_without_writes(
"""REQ-123: a fail-closed skipped live sync is a CLI failure.""" """REQ-123: a fail-closed skipped live sync is a CLI failure."""
class _Provider: class _Provider:
def get_broker_for_read(self): def get_broker_for_read(self):
return SimpleNamespace(get_balance=lambda: []), None return SimpleNamespace(get_balance=list), None
class _WarningStore: class _WarningStore:
def clear_warned(self, *_args): def clear_warned(self, *_args):
@@ -452,7 +455,7 @@ def test_sync_fail_closed_skipped_error_exits_nonzero_without_writes(
} }
psycopg = types.ModuleType("psycopg") psycopg = types.ModuleType("psycopg")
setattr(psycopg, "connect", lambda *_args, **_kwargs: _Connection()) psycopg.connect = lambda *_args, **_kwargs: _Connection()
monkeypatch.setitem(sys.modules, "psycopg", psycopg) monkeypatch.setitem(sys.modules, "psycopg", psycopg)
monkeypatch.setattr("trex_core.runtime.broker_provider.build_broker_provider", lambda _cfg: _Provider()) monkeypatch.setattr("trex_core.runtime.broker_provider.build_broker_provider", lambda _cfg: _Provider())
monkeypatch.setattr("trex_core.adapters.portfolio.reconcile_warning_store.PostgreSQLReconcileWarningStore", lambda *_args: _WarningStore()) monkeypatch.setattr("trex_core.adapters.portfolio.reconcile_warning_store.PostgreSQLReconcileWarningStore", lambda *_args: _WarningStore())
@@ -707,10 +710,10 @@ def test_verify_cmd_broker_import_does_not_raise(monkeypatch):
# Patch at module level — _cmd_verify uses local `from ... import` so we # Patch at module level — _cmd_verify uses local `from ... import` so we
# must patch the source module, not the cli namespace. # must patch the source module, not the cli namespace.
import trex_core.adapters.persistence.postgresql as pg_mod
import trex_core.adapters.portfolio.reconcile_warning_store as rws_mod
from trex_core.application.portfolio import InMemoryReconcileWarningStore from trex_core.application.portfolio import InMemoryReconcileWarningStore
from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository
import trex_core.adapters.portfolio.reconcile_warning_store as rws_mod
import trex_core.adapters.persistence.postgresql as pg_mod
monkeypatch.setattr(rws_mod, "PostgreSQLReconcileWarningStore", monkeypatch.setattr(rws_mod, "PostgreSQLReconcileWarningStore",
lambda dsn: InMemoryReconcileWarningStore()) lambda dsn: InMemoryReconcileWarningStore())
+20 -18
View File
@@ -12,8 +12,8 @@ from __future__ import annotations
import logging import logging
import os import os
import threading import threading
from datetime import date, datetime, time, timedelta, timezone from collections.abc import Callable
from typing import Callable, Optional from datetime import UTC, date, datetime, time, timedelta, timezone
from trex_core.application.alarm_emitter import AlarmEmitter from trex_core.application.alarm_emitter import AlarmEmitter
from trex_core.application.broker_port import BrokerPort from trex_core.application.broker_port import BrokerPort
@@ -23,10 +23,10 @@ from trex_core.application.portfolio import (
PortfolioHubReadPort, PortfolioHubReadPort,
PortfolioHubUnavailableError, PortfolioHubUnavailableError,
ReconcileNotificationStore, ReconcileNotificationStore,
UnmanagedPositionStore,
ReconcileWarningStore, ReconcileWarningStore,
SnapshotStore, SnapshotStore,
TradingContext, TradingContext,
UnmanagedPositionStore,
) )
from trex_core.application.reconciliation import reconcile_positions from trex_core.application.reconciliation import reconcile_positions
@@ -45,32 +45,32 @@ class SnapshotScheduler:
store: SnapshotStore, store: SnapshotStore,
broker: BrokerPort, broker: BrokerPort,
schedule_kst: time = time(hour=7, minute=30), schedule_kst: time = time(hour=7, minute=30),
clock: Optional[Callable[[], datetime]] = None, clock: Callable[[], datetime] | None = None,
slot_repo=None, slot_repo=None,
slot_service=None, slot_service=None,
instance_key: str = "default", instance_key: str = "default",
alarm_emitter: Optional[AlarmEmitter] = None, alarm_emitter: AlarmEmitter | None = None,
warn_store: Optional[ReconcileWarningStore] = None, warn_store: ReconcileWarningStore | None = None,
notification_store: Optional[ReconcileNotificationStore] = None, notification_store: ReconcileNotificationStore | None = None,
unmanaged_position_store: Optional[UnmanagedPositionStore] = None, unmanaged_position_store: UnmanagedPositionStore | None = None,
credential_fetch_enabled: bool = False, credential_fetch_enabled: bool = False,
kis_reinit_callback: Optional[Callable[[BrokerCredentialPayload], None]] = None, kis_reinit_callback: Callable[[BrokerCredentialPayload], None] | None = None,
credential_refresh: Optional[Callable[[], object]] = None, credential_refresh: Callable[[], object] | None = None,
) -> None: ) -> None:
self._read = read_port self._read = read_port
self._store = store self._store = store
self._broker = broker self._broker = broker
self._schedule_kst = schedule_kst self._schedule_kst = schedule_kst
self._clock = clock or (lambda: datetime.now(tz=timezone.utc)) self._clock = clock or (lambda: datetime.now(tz=UTC))
self._daemon_id: Optional[str] = None self._daemon_id: str | None = None
self._last_fired_date: Optional[datetime] = None self._last_fired_date: datetime | None = None
# dcy-020 / REQ-058 (BAT-030): asset-centric slot reconcile after each # dcy-020 / REQ-058 (BAT-030): asset-centric slot reconcile after each
# universe fetch — always on when slot_repo is wired (the reconcile is # universe fetch — always on when slot_repo is wired (the reconcile is
# what creates/retires the permanent asset slots). # what creates/retires the permanent asset slots).
self._slot_repo = slot_repo self._slot_repo = slot_repo
self._slot_service = slot_service self._slot_service = slot_service
self._instance_key = instance_key self._instance_key = instance_key
self._last_universe_size: Optional[int] = None self._last_universe_size: int | None = None
# dcy-033 / REQ-100: best-effort alarm emitter. # dcy-033 / REQ-100: best-effort alarm emitter.
self._alarm_emitter = alarm_emitter self._alarm_emitter = alarm_emitter
# BAT-050 / REQ-103: durable reconcile-warning store (Optional). # BAT-050 / REQ-103: durable reconcile-warning store (Optional).
@@ -84,7 +84,9 @@ class SnapshotScheduler:
# dcy-036 / REQ-113 AC-2 / REQ-115 AC-1: optional callback to reinitialize # dcy-036 / REQ-113 AC-2 / REQ-115 AC-1: optional callback to reinitialize
# KISBroker with the fetched day-start credential. None = no reinit (paper # KISBroker with the fetched day-start credential. None = no reinit (paper
# broker or credential_fetch_enabled=False). Set by daemon.py assembly. # broker or credential_fetch_enabled=False). Set by daemon.py assembly.
self._kis_reinit_callback: Optional[Callable[[BrokerCredentialPayload], None]] = kis_reinit_callback self._kis_reinit_callback: Callable[[BrokerCredentialPayload], None] | None = (
kis_reinit_callback
)
# REQ-119: provider-owned refresh supersedes the legacy raw fetch hook. # REQ-119: provider-owned refresh supersedes the legacy raw fetch hook.
self._credential_refresh = credential_refresh self._credential_refresh = credential_refresh
# REQ-113: detect break-glass at init time (env var set by operator). # REQ-113: detect break-glass at init time (env var set by operator).
@@ -93,8 +95,8 @@ class SnapshotScheduler:
in ("1", "true", "yes") in ("1", "true", "yes")
) )
# REQ-115: day-scoped credential cache. None until first successful fetch. # REQ-115: day-scoped credential cache. None until first successful fetch.
self._cached_credential: Optional[BrokerCredentialPayload] = None self._cached_credential: BrokerCredentialPayload | None = None
self._credential_date: Optional[date] = None self._credential_date: date | None = None
# REQ-114: entry gate. True = new entries allowed (default). # REQ-114: entry gate. True = new entries allowed (default).
# credential_fetch_enabled=False keeps this always True (no gate). # credential_fetch_enabled=False keeps this always True (no gate).
self._entry_allowed: bool = True self._entry_allowed: bool = True
@@ -266,7 +268,7 @@ class SnapshotScheduler:
try: try:
self._credential_refresh() self._credential_refresh()
self._entry_allowed = True self._entry_allowed = True
except Exception as exc: # provider exposes only sanitized failure except Exception as exc: # noqa: BLE001 - provider exposes only sanitized failure
_log.error("DAY_START_LOAD_FAILED: broker credential refresh failed: %s (instance=%s)", _log.error("DAY_START_LOAD_FAILED: broker credential refresh failed: %s (instance=%s)",
exc.__class__.__name__, self._instance_key) exc.__class__.__name__, self._instance_key)
self._entry_allowed = False self._entry_allowed = False
+6 -6
View File
@@ -737,12 +737,12 @@ def _build_parser() -> argparse.ArgumentParser:
def _cmd_sync(args, cfg, confirm) -> int: def _cmd_sync(args, cfg, confirm) -> int:
"""REQ-105 (dcy-034, BAT-051): sync slot qty to broker ground truth.""" """REQ-105 (dcy-034, BAT-051): sync slot qty to broker ground truth."""
from trex_core.adapters.portfolio.reconcile_warning_store import (
PostgreSQLReconcileWarningStore,
)
from trex_core.adapters.portfolio.reconcile_notification_store import ( from trex_core.adapters.portfolio.reconcile_notification_store import (
PostgreSQLReconcileNotificationStore, PostgreSQLReconcileNotificationStore,
) )
from trex_core.adapters.portfolio.reconcile_warning_store import (
PostgreSQLReconcileWarningStore,
)
from trex_core.application.reconciliation import sync_slot_to_broker from trex_core.application.reconciliation import sync_slot_to_broker
if not args.portfolio_id and not args.all: if not args.portfolio_id and not args.all:
@@ -891,12 +891,12 @@ def _cmd_sync(args, cfg, confirm) -> int:
def _cmd_verify(args, cfg) -> int: def _cmd_verify(args, cfg) -> int:
"""REQ-106 (dcy-034, BAT-051): re-check C-case reconcile-warning (read-centric).""" """REQ-106 (dcy-034, BAT-051): re-check C-case reconcile-warning (read-centric)."""
from trex_core.adapters.persistence.postgresql import PostgreSQLSlotRepository from trex_core.adapters.persistence.postgresql import PostgreSQLSlotRepository
from trex_core.adapters.portfolio.reconcile_warning_store import (
PostgreSQLReconcileWarningStore,
)
from trex_core.adapters.portfolio.reconcile_notification_store import ( from trex_core.adapters.portfolio.reconcile_notification_store import (
PostgreSQLReconcileNotificationStore, PostgreSQLReconcileNotificationStore,
) )
from trex_core.adapters.portfolio.reconcile_warning_store import (
PostgreSQLReconcileWarningStore,
)
from trex_core.application.portfolio import SnapshotStore from trex_core.application.portfolio import SnapshotStore
from trex_core.application.reconciliation import verify_slot_reconcile from trex_core.application.reconciliation import verify_slot_reconcile
+4 -5
View File
@@ -31,11 +31,11 @@ from trex_core.adapters.messaging import (
RabbitMQTransport, RabbitMQTransport,
topology, topology,
) )
from trex_core.adapters.persistence.migrate import apply_migrations
from trex_core.adapters.persistence.account_snapshot import ( from trex_core.adapters.persistence.account_snapshot import (
PostgreSQLAccountSnapshotStore, PostgreSQLAccountSnapshotStore,
) )
from trex_core.adapters.persistence.ledger_skip_store import PostgreSQLLedgerSkipStore from trex_core.adapters.persistence.ledger_skip_store import PostgreSQLLedgerSkipStore
from trex_core.adapters.persistence.migrate import apply_migrations
from trex_core.adapters.persistence.postgresql import ( from trex_core.adapters.persistence.postgresql import (
PgStrategyStateStore, PgStrategyStateStore,
PostgreSQLIntentStore, PostgreSQLIntentStore,
@@ -55,16 +55,15 @@ from trex_core.adapters.portfolio.reconcile_notification_store import (
from trex_core.adapters.portfolio.unmanaged_position_store import ( from trex_core.adapters.portfolio.unmanaged_position_store import (
PostgreSQLUnmanagedPositionStore, PostgreSQLUnmanagedPositionStore,
) )
from trex_core.application.alarm_emitter import AlarmEmitter
from trex_core.application.account_snapshot import AccountSnapshotService from trex_core.application.account_snapshot import AccountSnapshotService
from trex_core.application.alarm_emitter import AlarmEmitter
from trex_core.application.broker_port import BrokerPort from trex_core.application.broker_port import BrokerPort
from trex_core.application.execution import ExecutionService from trex_core.application.execution import ExecutionService
from trex_core.application.intents import IntentService from trex_core.application.intents import BuyIntent, IntentService
from trex_core.application.intents import BuyIntent from trex_core.application.killswitch import KillSwitchService
from trex_core.application.ledger_outbox import LedgerWriter from trex_core.application.ledger_outbox import LedgerWriter
from trex_core.application.ledger_port import LedgerPort from trex_core.application.ledger_port import LedgerPort
from trex_core.application.messaging import MessagingTransport from trex_core.application.messaging import MessagingTransport
from trex_core.application.killswitch import KillSwitchService
from trex_core.application.messaging_handlers import ( from trex_core.application.messaging_handlers import (
make_control_rpc_handler, make_control_rpc_handler,
make_intent_rpc_handler, make_intent_rpc_handler,