From d087e6607740fb405c663b0183e285473027c832 Mon Sep 17 00:00:00 2001 From: Savage <139500071+svg8bit@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:57:35 +0000 Subject: [PATCH] fix: cover all eligible trade alert markets --- .../preview/activity-rail.tsx | 79 ++++++++++++------- lib/polymarket-activity-query.ts | 27 ++++++- tests/global-conflict-map-preview.spec.ts | 64 +++++++++++++++ tests/polymarket-activity-query.spec.ts | 10 ++- 4 files changed, 148 insertions(+), 32 deletions(-) diff --git a/features/global-conflict-map/preview/activity-rail.tsx b/features/global-conflict-map/preview/activity-rail.tsx index 66babb3..61959d3 100644 --- a/features/global-conflict-map/preview/activity-rail.tsx +++ b/features/global-conflict-map/preview/activity-rail.tsx @@ -25,6 +25,7 @@ import { import { buildRollingActivitySignals } from "@/lib/conflict-activity-signals"; import { formatMarketTitle } from "@/lib/market-title"; import { + batchPolymarketActivityMarketIds, isPolymarketActivityEventCurrent, POLYMARKET_LARGE_TRADE_USD, selectPolymarketActivityMarketIds, @@ -339,9 +340,9 @@ export function ActivityRail({ const currentActivityEvents = useMemo( () => feed.events.filter((event) => - isPolymarketActivityEventCurrent(event, eligibilityClock), + isPolymarketActivityEventCurrent(event, feedClock), ), - [eligibilityClock, feed.events], + [feed.events, feedClock], ); const currentActivityEventIds = useMemo( () => new Set(currentActivityEvents.map((event) => event.id)), @@ -397,8 +398,11 @@ export function ActivityRail({ currentActivityMarketConditionIds, rollingNotices, ]); - const marketIdQuery = useMemo( - () => selectPolymarketActivityMarketIds(feed.events).join(","), + const marketIdQueries = useMemo( + () => + batchPolymarketActivityMarketIds( + selectPolymarketActivityMarketIds(feed.events), + ).map((marketIds) => marketIds.join(",")), [feed.events], ); @@ -407,39 +411,58 @@ export function ActivityRail({ fixtureMode || !liveRefreshEnabled || feed.dataMode !== "live" || - !marketIdQuery + marketIdQueries.length === 0 ) { return; } let cancelled = false; const refresh = async () => { - try { - const response = await fetch( - `/api/global-conflict-activity?marketIds=${encodeURIComponent(marketIdQuery)}`, - { headers: { Accept: "application/json" } }, + const results = await Promise.allSettled( + marketIdQueries.map(async (marketIdQuery) => { + const response = await fetch( + `/api/global-conflict-activity?marketIds=${encodeURIComponent(marketIdQuery)}`, + { headers: { Accept: "application/json" } }, + ); + if (!response.ok) { + throw new Error(`Activity batch returned ${response.status}`); + } + const payload: unknown = await response.json(); + return isActivityFeed(payload) && payload.dataMode === "live" + ? payload + : null; + }), + ); + if (cancelled) return; + const failedBatchCount = results.filter( + (result) => result.status === "rejected", + ).length; + if (failedBatchCount > 0) { + console.warn( + `Polymarket activity refresh kept partial coverage; ${failedBatchCount}/${marketIdQueries.length} batches unavailable.`, ); - if (!response.ok) return; - const payload: unknown = await response.json(); - if (cancelled || !isActivityFeed(payload) || payload.dataMode !== "live") { - return; - } - const tradeNotices = payload.items + } + const payloads = results + .filter( + ( + result, + ): result is PromiseFulfilledResult => + result.status === "fulfilled", + ) + .map((result) => result.value) + .filter((payload): payload is ConflictActivityFeed => payload !== null); + const tradeNotices = payloads.flatMap((payload) => + payload.items .map((item) => tradeNotice(item, payload.expiresAfterSeconds, eventsByUrl), ) - .filter((notice): notice is ActivityNotice => Boolean(notice)); - addNotices( - tradeNotices, - currentActivityEventIds, - currentActivityMarketConditionIds, - ); - } catch (error) { - console.warn( - "Polymarket activity refresh failed; keeping verified alerts.", - error instanceof Error ? error.message : "Unknown error", - ); - } + .filter((notice): notice is ActivityNotice => Boolean(notice)), + ); + addNotices( + tradeNotices, + currentActivityEventIds, + currentActivityMarketConditionIds, + ); }; let timer: number | null = null; @@ -470,7 +493,7 @@ export function ActivityRail({ feed.dataMode, fixtureMode, liveRefreshEnabled, - marketIdQuery, + marketIdQueries, ]); useEffect(() => { diff --git a/lib/polymarket-activity-query.ts b/lib/polymarket-activity-query.ts index 763d164..556e140 100644 --- a/lib/polymarket-activity-query.ts +++ b/lib/polymarket-activity-query.ts @@ -58,11 +58,36 @@ export function selectPolymarketActivityMarketIds( ([leftId, leftVolume], [rightId, rightVolume]) => rightVolume - leftVolume || leftId.localeCompare(rightId), ) - .slice(0, POLYMARKET_ACTIVITY_MAX_MARKET_IDS) .map(([marketId]) => marketId) .toSorted((left, right) => left.localeCompare(right)); } +export function batchPolymarketActivityMarketIds( + marketIds: readonly string[], +): string[][] { + const normalizedMarketIds = Array.from( + new Set( + marketIds + .filter((marketId) => POLYMARKET_CONDITION_ID_PATTERN.test(marketId)) + .map((marketId) => marketId.toLowerCase()), + ), + ).toSorted((left, right) => left.localeCompare(right)); + const batches: string[][] = []; + for ( + let index = 0; + index < normalizedMarketIds.length; + index += POLYMARKET_ACTIVITY_MAX_MARKET_IDS + ) { + batches.push( + normalizedMarketIds.slice( + index, + index + POLYMARKET_ACTIVITY_MAX_MARKET_IDS, + ), + ); + } + return batches; +} + export function buildPolymarketActivityUrl( marketIds: readonly string[], nowSeconds: number, diff --git a/tests/global-conflict-map-preview.spec.ts b/tests/global-conflict-map-preview.spec.ts index fdaa9d7..27d1140 100644 --- a/tests/global-conflict-map-preview.spec.ts +++ b/tests/global-conflict-map-preview.spec.ts @@ -1032,6 +1032,70 @@ test("fills the rail with 5% daily and 20% weekly moves in both directions", asy await expect(rail.locator('[data-activity-window="24h"]')).toHaveCount(1); }); +test("batches trade-watch coverage across every eligible $100K market", async ({ + page, +}) => { + const fixture = getConflictPreviewFixtureFeed(); + const updatedAt = new Date().toISOString(); + const liveFeed = { + ...fixture, + dataMode: "live" as const, + updatedAt, + sourceLabel: "Polymarket Gamma API", + events: Array.from({ length: 136 }, (_, index) => ({ + ...fixture.events[index % fixture.events.length]!, + id: `polymarket-${900_000 + index}`, + dataOrigin: "polymarket" as const, + evidenceStatus: "country-anchor" as const, + marketUrl: `https://polymarket.com/event/batched-activity-${index}`, + updatedAt, + endDate: new Date(Date.now() + 7 * 24 * 60 * 60_000).toISOString(), + marketConditionId: mockConditionId(900_000 + index), + volume: 100_000 + index, + volume24h: 1, + priceChange1h: null, + priceChange24h: 0.01, + priceChange7d: 0.01, + })), + }; + const activityMarketIdQueries: string[][] = []; + + await page.route("**/api/global-conflict-events", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(liveFeed), + }); + }); + await page.route("**/api/global-conflict-activity?**", async (route) => { + activityMarketIdQueries.push( + (new URL(route.request().url()).searchParams.get("marketIds") ?? "") + .split(",") + .filter(Boolean), + ); + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + dataMode: "live", + updatedAt, + expiresAfterSeconds: 900, + sourceLabel: "Polymarket Data API", + items: [], + }), + }); + }); + + await openActivityRailPage(page); + await expect.poll(() => activityMarketIdQueries.length).toBe(2); + expect( + activityMarketIdQueries.map((marketIds) => marketIds.length).sort((a, b) => a - b), + ).toEqual([36, 100]); + expect([...new Set(activityMarketIdQueries.flat())].sort()).toEqual( + liveFeed.events.map((event) => event.marketConditionId).sort(), + ); +}); + test("does not reanimate the same rolling signal after a feed refresh", async ({ page, }) => { diff --git a/tests/polymarket-activity-query.spec.ts b/tests/polymarket-activity-query.spec.ts index 3ce5bd8..80a46e0 100644 --- a/tests/polymarket-activity-query.spec.ts +++ b/tests/polymarket-activity-query.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from "@playwright/test"; import { + batchPolymarketActivityMarketIds, buildPolymarketActivityUrl, selectPolymarketActivityMarketIds, } from "../lib/polymarket-activity-query"; @@ -54,7 +55,7 @@ test("selects future markets from $100K volume and rejects ineligible markets", expect(marketIds).not.toContain(conditionId(1_000)); }); -test("caps exact-market coverage at the highest-volume one hundred", () => { +test("keeps every eligible market and batches upstream requests at one hundred", () => { const now = Date.parse("2026-08-11T13:00:00Z"); const events = Array.from({ length: 130 }, (_, index) => ({ id: `polymarket-${710_000 + index}`, @@ -64,10 +65,13 @@ test("caps exact-market coverage at the highest-volume one hundred", () => { })); const marketIds = selectPolymarketActivityMarketIds(events, now); + const batches = batchPolymarketActivityMarketIds(marketIds); - expect(marketIds).toHaveLength(100); + expect(marketIds).toHaveLength(130); + expect(marketIds).toContain(conditionId(1)); expect(marketIds).toContain(conditionId(130)); - expect(marketIds).not.toContain(conditionId(1)); + expect(batches.map((batch) => batch.length)).toEqual([100, 30]); + expect(batches.flat()).toEqual(marketIds); }); test("bounds the Polymarket trade query to a fresh fifteen-minute window", () => {