fix: match shadow book to its pipeline's scan by run id, not timestamp
A manually triggered rr_scanner and the scheduled near-close pipeline are separate APScheduler jobs; max_instances=1 serialises a job only against itself, so they can overlap. A manual scan starting just before the pipeline can finish just after it began and overwrite the scan markers. Its completion timestamp is then later than the pipeline start, so the previous 'completed >= pipeline_start' check accepted its batch as though it were the pipeline's own -- exactly when the pipeline's scan may have failed. Replace the timestamp comparison with an exact run-id match. A new pipeline_run module holds a per-task run-id contextvar (separate module so the scanner and scheduler import it without a cycle). _run_pipeline binds a fresh id per invocation; scan_all_tickers stamps that id -- or a fresh one when run standalone -- into the scan markers, written with started/completed in a single commit. The shadow step requires the stored run id to equal its pipeline's id exactly, so a concurrent manual scan (its own id) or a failed pipeline scan (a prior run's id) can never be mistaken for it. Direct Admin triggers have no pipeline context and keep the freshness fallback. Known residual: the id match governs whether shadow proceeds; setup selection remains detected_at >= scan start, so a fully per-run setup isolation would need a run_id column on trade_setups (not required here). Tests cover the reported race (manual scan finishing last is refused), a failed pipeline scan, the id-match accept path, and contextvar propagation and non-leakage across tasks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -47,12 +47,15 @@ from app.services.recommendation_service import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Boundary of the most recent *successful* scan. Written only when
|
||||
# scan_all_tickers completes, so a consumer can tell a scan actually ran this
|
||||
# pipeline pass (freshness of COMPLETED) and which setups belong to it
|
||||
# (detected_at >= STARTED). The shadow book relies on both.
|
||||
# Boundary of the most recent *successful* scan. Written together, only when
|
||||
# scan_all_tickers completes: STARTED bounds which setups belong to the run
|
||||
# (detected_at >= STARTED), COMPLETED gives its freshness, and RUN_ID identifies
|
||||
# the pipeline invocation that produced it (or a fresh id for a manual scan).
|
||||
# The shadow book matches RUN_ID exactly rather than trusting timestamps, so a
|
||||
# concurrent manual scan cannot be mistaken for the pipeline's own.
|
||||
KEY_LAST_SCAN_STARTED = "last_scan_run_started_at"
|
||||
KEY_LAST_SCAN_COMPLETED = "last_scan_run_completed_at"
|
||||
KEY_LAST_SCAN_RUN_ID = "last_scan_run_id"
|
||||
|
||||
STRATEGY_VERSION = "residual_highvol_80_20_atr_trail3_v1"
|
||||
|
||||
@@ -820,17 +823,22 @@ async def scan_all_tickers(
|
||||
if progress_callback is not None and total:
|
||||
progress_callback(total, total, "")
|
||||
|
||||
# Record the run boundary only now that the scan has completed. The shadow
|
||||
# book refuses to trade unless COMPLETED is fresh (proving a scan ran in this
|
||||
# pipeline pass, not a prior session) and selects only setups from this run
|
||||
# (detected_at >= STARTED). Written after the loop so a hard failure above
|
||||
# leaves the previous, now-stale, marker in place.
|
||||
# Record the run boundary only now that the scan has completed, stamped with
|
||||
# the run id of the pipeline this scan ran inside (or a fresh id when run
|
||||
# standalone — a manual scan then can never match a pipeline's expected id).
|
||||
# All three markers are written in one commit so a reader sees a consistent
|
||||
# (started, completed, run_id) triple, and a hard failure above leaves the
|
||||
# previous, now-superseded, markers in place.
|
||||
from app.services import pipeline_run
|
||||
|
||||
run_id = pipeline_run.current() or pipeline_run.new_run_id()
|
||||
await settings_store.upsert_setting(
|
||||
db, KEY_LAST_SCAN_STARTED, gate_observation_started_at.isoformat()
|
||||
)
|
||||
await settings_store.upsert_setting(
|
||||
db, KEY_LAST_SCAN_COMPLETED, datetime.now(timezone.utc).isoformat()
|
||||
)
|
||||
await settings_store.upsert_setting(db, KEY_LAST_SCAN_RUN_ID, run_id)
|
||||
await db.commit()
|
||||
|
||||
return all_setups
|
||||
|
||||
Reference in New Issue
Block a user