fix(pipelines): persist anomalies without embeddings, chunk earnings-calendar fetch, unbreak AQR cutoff #87
No reviewers
Labels
No labels
bug
documentation
duplicate
enhancement
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
gertjan/bodega!87
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/silent-pipeline-failures"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fix A — anomaly persistence discarded on embedding failure
Root cause:
src/scheduler/tasks/anomaly_detection.py:embed_and_deduplicatesetembeddings = []whenperplexity.embed(texts)raised. The subsequentfor anomaly, embedding in zip(anomalies, embeddings)then iterated zero times, soupsert_anomalywas never called for any detected anomaly.Evidence: a live run detected 169 anomalies above the severity threshold; the
anomaliesMongoDB collection had 0 documents.Fix: when embeddings are unavailable, or the returned count doesn't match the anomaly count (a partial/malformed response can't be trusted positionally either), every anomaly is now persisted with
embedding=Noneandcheck_dedup=False(dedup needs an embedding to compare against). When embeddings are available and line up 1:1, behavior is unchanged (check_dedup=True). A warning logs how many anomalies were persisted without embeddings. Downstream stages (assign_clusters,search_context,interpret_anomalies) already tolerateembedding=None/missing-embedding docs —assign_clustersqueries{"embedding": {"$exists": True}}andAnomaly.model_dump(exclude_none=True)omits the field entirely, so no further guard changes were needed.Fix B — earnings calendar silently near-empty on the default 365-day window
Root cause:
src/sources/fmp.py:get_earnings_calendarpassesfrom/tostraight through to FMP's/api/v3/earning_calendar. For wide ranges, FMP silently truncates the result from the front of the range instead of erroring — near-term events (the ones the job actually needs) get dropped as the window widens.Empirical findings (live GETs against FMP, 2026-07-14, all anchored at "today" unless noted):
refresh_earningsdefault)populate_earnings_calendardefault)Non-zero-start 60-day chunks (e.g. offset 60→120, 120→180 days out) returned full, correct coverage for their span, confirming the corruption is driven by total requested width, not by anchor position.
Fix: added
FMPSource.get_earnings_calendar_chunked(from_date, to_date, chunk_days=45)— 45 days is half of the largest window (90d) that still returned full, correct coverage, leaving a comfortable safety margin. Chunks are fetched sequentially and aggregated with dedup on(symbol, date); a failing chunk is logged and skipped rather than aborting the whole run.populate_earnings_calendarnow calls this instead of the raw single-shot fetch, and its completion log always reports total events/symbols written (and logs atwarninglevel, notinfo, when nothing was fetched) so a fast no-op run can't pass for a healthy one in the logs again.Fix C — AQR adapter: pinned featured article aborted every page
Root cause:
src/adapters/aqr_adapter.py:get_articlesiterates each page's parsed articles in order and breaks the whole pagination loop the first time it sees an article older thansince._parse_listing_pagealways puts the site's pinned "featured" article first, regardless of its actual publish date — so whenever that featured article is older thansince(which is most of the time, since it's rarely refreshed), the cutoff fires on the very first iteration and every regular article on the page — however recent — is discarded.Live verification (read-only GETs against aqr.com, 2026-07-14): the real listing page's featured article is dated 2026-03-18, while the newest regular article is 2026-06-15 — 3 months newer than the featured pin.
since=now-14d(2026-06-30) correctly returned 0 articles (nothing is that recent right now);since=now-60d(2026-05-15) returned 2 articles (2026-06-15 and 2026-05-19), confirming the fixed cutoff logic no longer aborts on the stale featured article and correctly walks past it to the real, recent articles.Fix:
_parse_listing_pagenow tags each article withis_featured. Inget_articles, a featured article is only included if it independently passes the date filter — it's never allowed to setreached_cutoff, so the scan continues into the regular articles regardless of the featured article's age.Tests
tests/unit/test_scheduler_anomaly_detection.py(new): embed failure → all anomalies persisted withembedding=None/check_dedup=False; embed count mismatch → same fallback; embed success → embeddings attached andcheck_dedup=True.tests/unit/test_sources_fmp.py:get_earnings_calendar_chunkedsplits a 365-day range into 9 contiguous 45-day windows; aggregates + dedupes across chunks; one chunk raising doesn't stop the others; events missing symbol/date are skipped; a narrow range collapses to a single chunk.tests/unit/test_adapters_aqr.py: fixture HTML (trimmed from the real site) with an old featured article + newer regular items — cutoff doesn't fire on the featured article,sincebefore the featured date includes it, andsince=Nonereturns everything.ruff check src/,mypy src/, andpytest -m "not integration"are all green (1075 passed, 27 deselected).Follow-ups (not in scope for this PR)
pplx-embed-v1-0.6b) still exists and is reachable — the pipeline now degrades gracefully and persists anomalies without embeddings when it fails, but dedup/clustering silently stop working until that's fixed.task_runscleanly while doing nothing useful — a cheap output-count check on completion would have caught this much earlier).Co-Authored-By: Claude Fable 5 noreply@anthropic.com
b4820b3c581a61f0ad05