From f9284d1de8bf62ac8b05181f7917fb5a8f87eba3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 12 Sep 2026 13:33:05 +0000 Subject: [PATCH] fix: preserve unmanaged first-seen timestamps Co-authored-by: genichin <4282227+genichin@users.noreply.github.com> --- .../test_unmanaged_position_store.py | 27 +++++++++++++++++++ .../test_unmanaged_position_lifecycle.py | 16 +++++++++++ .../portfolio/unmanaged_position_store.py | 6 ++--- trex_core/application/portfolio.py | 4 ++- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/tests/adapters/portfolio/test_unmanaged_position_store.py b/tests/adapters/portfolio/test_unmanaged_position_store.py index b882f76..4639bf2 100644 --- a/tests/adapters/portfolio/test_unmanaged_position_store.py +++ b/tests/adapters/portfolio/test_unmanaged_position_store.py @@ -99,3 +99,30 @@ class TestCRUDRoundTrip: rows_b = pg_unmanaged_store.list_states(instance_key="inst-B", lifecycle_state="all") assert len(rows_a) == 1 and rows_a[0]["lifecycle_state"] == "resolved" assert len(rows_b) == 1 and rows_b[0]["quantity"] == 7 + + def test_reactivated_state_preserves_original_first_seen(self, pg_unmanaged_store): + t1 = datetime(2026, 9, 7, 0, 0, tzinfo=UTC) + t2 = t1 + timedelta(minutes=1) + t3 = t2 + timedelta(minutes=1) + + pg_unmanaged_store.observe( + instance_key="inst-A", + symbol="008110", + quantity=3, + observed_at=t1, + ) + pg_unmanaged_store.resolve_absent( + instance_key="inst-A", + observed_symbols=set(), + observed_at=t2, + ) + reopened = pg_unmanaged_store.observe( + instance_key="inst-A", + symbol="008110", + quantity=5, + observed_at=t3, + ) + + assert reopened["transition"] == "detected" + assert reopened["state"]["first_seen_at"] == t1 + assert reopened["state"]["last_seen_at"] == t3 diff --git a/tests/application/test_unmanaged_position_lifecycle.py b/tests/application/test_unmanaged_position_lifecycle.py index bd91c6b..b906e06 100644 --- a/tests/application/test_unmanaged_position_lifecycle.py +++ b/tests/application/test_unmanaged_position_lifecycle.py @@ -109,6 +109,22 @@ def test_in_memory_store_isolates_same_symbol_by_instance(): assert state_b["quantity"] == 7 +def test_in_memory_store_preserves_first_seen_when_resolved_symbol_reappears(): + store = InMemoryUnmanagedPositionStore() + t1 = datetime(2026, 9, 7, 0, 0, tzinfo=UTC) + t2 = t1 + timedelta(minutes=1) + t3 = t2 + timedelta(minutes=1) + + store.observe(instance_key=INSTANCE, symbol="008110", quantity=3, observed_at=t1) + store.resolve_absent(instance_key=INSTANCE, observed_symbols=set(), observed_at=t2) + reopened = store.observe(instance_key=INSTANCE, symbol="008110", quantity=5, observed_at=t3) + + assert reopened["transition"] == "detected" + assert reopened["state"]["first_seen_at"] == t1 + assert reopened["state"]["last_seen_at"] == t3 + assert reopened["state"]["quantity_changed_at"] == t3 + + def test_reconcile_core_holdings_suppresses_repeated_unmanaged_alarm_spam(): repo, svc = _wire() emitter, transport = _make_emitter() diff --git a/trex_core/adapters/portfolio/unmanaged_position_store.py b/trex_core/adapters/portfolio/unmanaged_position_store.py index 6e75469..0d15adf 100644 --- a/trex_core/adapters/portfolio/unmanaged_position_store.py +++ b/trex_core/adapters/portfolio/unmanaged_position_store.py @@ -58,11 +58,11 @@ class PostgreSQLUnmanagedPositionStore: cur.execute( "UPDATE unmanaged_position_state " "SET quantity = %s, lifecycle_state = 'active', " - " first_seen_at = %s, last_seen_at = %s, quantity_changed_at = %s, " - " resolved_at = NULL, updated_at = %s " + " last_seen_at = %s, quantity_changed_at = %s, resolved_at = NULL, " + " updated_at = %s " "WHERE instance_key = %s AND symbol = %s " f"RETURNING {_STATE_COLUMNS}", - (quantity, now, now, now, now, instance_key, symbol), + (quantity, now, now, now, instance_key, symbol), ) elif int(previous["quantity"]) != int(quantity): transition = "quantity_changed" diff --git a/trex_core/application/portfolio.py b/trex_core/application/portfolio.py index cf2870e..ced6b87 100644 --- a/trex_core/application/portfolio.py +++ b/trex_core/application/portfolio.py @@ -629,7 +629,9 @@ class InMemoryUnmanagedPositionStore: "symbol": symbol, "quantity": quantity, "lifecycle_state": "active", - "first_seen_at": now, + "first_seen_at": ( + existing.get("first_seen_at", now) if existing is not None else now + ), "last_seen_at": now, "quantity_changed_at": now, "resolved_at": None,