From 6d50357d111574d84b77e4cb03867b3f061a5afc Mon Sep 17 00:00:00 2001 From: k Date: Fri, 29 May 2026 15:15:32 +0800 Subject: [PATCH] fix: /posts source filter, /performance closed_at basis, funding venue field - /posts gains an optional ?source= filter. The Macro page was pulling the latest-500 posts globally and filtering client-side; rare scanner signals (btc_bottom_reversal ~2-4/cycle, funding_reversal hourly) got pushed off the page by frequent Trump posts, so Macro falsely showed "no signals". - /performance now filters and orders by closed_at (realized-PnL-in-window) instead of opened_at, so it shares ONE basis with the frontend Analytics page (which filters every window by closed_at). Boundary trades no longer land in one basis but not the other. - funding snapshot returns the actual `venue` (provider.name) so the frontend label follows the real data source instead of hardcoding "Binance". 72 tests pass. Co-Authored-By: Claude Opus 4.7 --- app/api/performance.py | 11 +++++++++-- app/api/posts.py | 14 +++++++++++--- app/services/scanners/funding_reversal.py | 4 ++++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/api/performance.py b/app/api/performance.py index e5cc81b..058094e 100644 --- a/app/api/performance.py +++ b/app/api/performance.py @@ -36,12 +36,19 @@ async def get_performance( ) since = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(days=PERIOD_DAYS) + # Period basis is closed_at (realized-PnL-in-window), NOT opened_at. This + # matches how the frontend Analytics page filters every time window, so the + # 30d numbers shown there (and on the dashboard tile) use one consistent + # basis. Ordering by closed_at also makes the drawdown equity curve reflect + # realization order. A trade opened before the window but closed inside it + # correctly counts; one opened inside but still open does not (closed_at IS + # NOT NULL already excludes it). result = await db.execute( select(BotTrade) .where(BotTrade.wallet_address == wallet) .where(BotTrade.closed_at.is_not(None)) - .where(BotTrade.opened_at >= since) - .order_by(BotTrade.opened_at.asc()) + .where(BotTrade.closed_at >= since) + .order_by(BotTrade.closed_at.asc()) ) trades = result.scalars().all() diff --git a/app/api/posts.py b/app/api/posts.py index 0d4be94..9bf3515 100644 --- a/app/api/posts.py +++ b/app/api/posts.py @@ -74,13 +74,21 @@ async def get_posts( request: Request, limit: int = Query(default=20, ge=1, le=500), page: int = Query(default=1, ge=1), + source: Optional[str] = Query( + default=None, + description="Filter to a single source (e.g. 'btc_bottom_reversal', " + "'funding_reversal', 'truth'). Without it, rare-source " + "signals can be pushed off the latest-N page by Trump posts.", + ), db: AsyncSession = Depends(get_db), response: Response = None, ): offset = (page - 1) * limit - result = await db.execute( - select(Post).order_by(Post.published_at.desc()).offset(offset).limit(limit) - ) + stmt = select(Post) + if source: + stmt = stmt.where(Post.source == source) + stmt = stmt.order_by(Post.published_at.desc()).offset(offset).limit(limit) + result = await db.execute(stmt) posts = result.scalars().all() # Posts are scraped every 5s but rarely change once written — allow CDN/browser # to cache for 30s. stale-while-revalidate=60 means stale content is served diff --git a/app/services/scanners/funding_reversal.py b/app/services/scanners/funding_reversal.py index 59b39e5..4f26fbd 100644 --- a/app/services/scanners/funding_reversal.py +++ b/app/services/scanners/funding_reversal.py @@ -367,6 +367,10 @@ async def get_current_snapshot() -> dict: return { "ok": True, "asset": ASSET, + # Report the ACTUAL funding venue so the frontend label follows the + # data source instead of hardcoding "Binance" (which silently lies if + # for_asset() routes to HL, or ASSET becomes an HL-native perp). + "venue": getattr(for_asset(ASSET), "name", None), "cadence_hours": round(cadence_h, 2), "coverage_days": round(span_days, 1), "latest_rate_pct": round(latest, 5),