fix: bind shadow book to its own pipeline's scan, not wall-clock freshness
A 6-hour freshness window proves only that some scan ran recently, which a manual mid-day scan satisfies. Scenario: a manual scan succeeds at 13:00; the 15:30 near-close pipeline's scan step is disabled or fails; at 15:30 the 13:00 completion is still 'fresh', so the shadow step trades that earlier batch despite no successful scan in the current pipeline. _run_pipeline now records its start in a per-task contextvar, visible to the steps it awaits. run_shadow_book reads it and requires the scan completion marker to be at/after the pipeline start, so a scan that failed or was disabled in this pass (marker left at a prior run, before the pipeline began) cannot be substituted by an earlier manual scan. A direct Admin trigger has no pipeline context and falls back to the freshness window -- an explicit operator action, not an automated one. Tests pin the reported case: a fresh manual scan predating the pipeline start is refused; the pipeline's own post-start scan is accepted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -283,6 +283,44 @@ class TestScanFreshness:
|
||||
assert summary["opened"] == 0
|
||||
|
||||
|
||||
class TestPipelineScanBinding:
|
||||
@pytest.mark.asyncio
|
||||
async def test_fresh_manual_scan_before_pipeline_is_refused(self, session):
|
||||
"""A manual scan at 13:00 is still 'fresh' at 15:30, but the 15:30
|
||||
pipeline's own scan failed. Binding to the pipeline start rejects the
|
||||
13:00 batch — no successful scan happened in *this* pipeline pass."""
|
||||
ids = await _seed(session, ["AAA"])
|
||||
pipeline_start = datetime.now(timezone.utc)
|
||||
manual_scan = pipeline_start - timedelta(hours=2, minutes=30)
|
||||
session.add(_setup(ids["AAA"], rank=0.9, detected=manual_scan))
|
||||
await session.commit()
|
||||
await _mark_scan(session, started=manual_scan - timedelta(minutes=5),
|
||||
completed=manual_scan)
|
||||
|
||||
summary = await shadow_book_service.open_shadow_positions(
|
||||
session, activation_config=_CONFIG, require_scan_after=pipeline_start
|
||||
)
|
||||
|
||||
assert summary["opened"] == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pipeline_scan_after_start_is_accepted(self, session):
|
||||
"""The pipeline's own scan completes just after the pipeline began."""
|
||||
ids = await _seed(session, ["AAA"])
|
||||
pipeline_start = datetime.now(timezone.utc)
|
||||
scan_completed = pipeline_start + timedelta(minutes=1)
|
||||
session.add(_setup(ids["AAA"], rank=0.9, detected=scan_completed))
|
||||
await session.commit()
|
||||
await _mark_scan(session, started=pipeline_start + timedelta(seconds=1),
|
||||
completed=scan_completed)
|
||||
|
||||
summary = await shadow_book_service.open_shadow_positions(
|
||||
session, activation_config=_CONFIG, require_scan_after=pipeline_start
|
||||
)
|
||||
|
||||
assert summary["opened"] == 1
|
||||
|
||||
|
||||
class TestLongOnly:
|
||||
@pytest.mark.asyncio
|
||||
async def test_shorts_are_never_taken_even_with_gate_disabled(self, session):
|
||||
|
||||
Reference in New Issue
Block a user