test: cover unmanaged position lifecycle flow
Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
co-authored by
genichin
parent
fe51d211d9
commit
f2b825e720
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user