From 045121a4577cca575942288b48f3e34ffa411d7b Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Mon, 25 May 2026 16:48:19 +0800 Subject: [PATCH] feat: implement scan terminal dashboard with real-time market data services and interactive visual components --- frontend/app/api/scan/terminal/route.ts | 5 + .../dashboard/ScanTerminalDashboard.tsx | 25 +- .../LiveTemperatureThresholdChart.tsx | 263 +++++++++++++++++- .../scan-terminal/scan-terminal-client.ts | 2 +- scratch_test.py | 62 +++++ scratch_test_pagination.py | 42 +++ tests/test_scan_terminal_modules.py | 2 +- web/routers/scan.py | 5 +- web/scan_terminal_filters.py | 4 +- web/services/scan_api.py | 2 + 10 files changed, 391 insertions(+), 21 deletions(-) create mode 100644 scratch_test.py create mode 100644 scratch_test_pagination.py diff --git a/frontend/app/api/scan/terminal/route.ts b/frontend/app/api/scan/terminal/route.ts index 539ca7d4..8910c7be 100644 --- a/frontend/app/api/scan/terminal/route.ts +++ b/frontend/app/api/scan/terminal/route.ts @@ -30,12 +30,17 @@ export async function GET(req: NextRequest) { "time_range", "limit", "force_refresh", + "skip_polymarket", ]) { const value = req.nextUrl.searchParams.get(key); if (value != null && value !== "") { params.set(key, value); } } + const tradingRegion = req.nextUrl.searchParams.get("trading_region"); + if (tradingRegion != null && tradingRegion !== "") { + params.set("region", tradingRegion); + } const cachePolicy = buildForceRefreshProxyCachePolicy(forceRefresh, 10); const url = `${API_BASE}/api/scan/terminal?${params.toString()}`; diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index 187b2192..938c9d40 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -500,6 +500,8 @@ function PolyWeatherTerminal({ searchInputRef, selectedCity, setSelectedCity, + selectedRegionKey, + setSelectedRegionKey, }: { generatedText: string; isEn: boolean; @@ -516,6 +518,8 @@ function PolyWeatherTerminal({ searchInputRef: React.RefObject; selectedCity: string | null; setSelectedCity: (city: string | null) => void; + selectedRegionKey: string; + setSelectedRegionKey: (key: string) => void; }) { useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -540,11 +544,6 @@ function PolyWeatherTerminal({ }, [searchInputRef, setSearchQuery]); const [navExpanded, setNavExpanded] = useState(false); const [activeNavKey, setActiveNavKey] = useState("contracts"); - const [selectedRegionKey, setSelectedRegionKey] = useState("east_asia"); - - useEffect(() => { - setSelectedRegionKey(detectLocalRegion()); - }, []); const NAV_ITEMS = [ { key: "contracts", Icon: Table2, labelEn: "Contracts", labelZh: "天气合约" }, @@ -552,11 +551,13 @@ function PolyWeatherTerminal({ { key: "training", Icon: GraduationCap, labelEn: "Training", labelZh: "训练数据" }, ]; + useEffect(() => { + setSelectedCity(null); + }, [selectedRegionKey, setSelectedCity]); + const filteredRegionRows = useMemo(() => { return rows.filter( - (row) => - resolveTradingRegionKey(row) === selectedRegionKey && - row.is_primary_signal !== false, + (row) => resolveTradingRegionKey(row) === selectedRegionKey, ); }, [rows, selectedRegionKey]); @@ -889,6 +890,7 @@ function ScanTerminalScreen() { hydrated && (proAccess.subscriptionActive || canUseLocalFullAccess); const userLocalTime = useUserLocalClock(); const { themeMode } = useScanTerminalTheme(); + const [selectedRegionKey, setSelectedRegionKey] = useState("east_asia"); useEffect(() => { let cancelled = false; @@ -958,10 +960,15 @@ function ScanTerminalScreen() { }; }, []); + useEffect(() => { + setSelectedRegionKey(detectLocalRegion()); + }, []); + const { refreshScanTerminalManually, scanLoading, terminalData } = useScanTerminalQuery({ isPro, proAccessLoading: !hydrated || (proAccess.loading && !canUseLocalFullAccess), + tradingRegion: selectedRegionKey, }); const rows = useMemo( () => sortRowsByUserTime(terminalData?.rows || []), @@ -1042,6 +1049,8 @@ function ScanTerminalScreen() { searchInputRef={searchInputRef} selectedCity={selectedCity} setSelectedCity={setSelectedCity} + selectedRegionKey={selectedRegionKey} + setSelectedRegionKey={setSelectedRegionKey} /> ); } diff --git a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx index 04b5034f..a8811fc2 100644 --- a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx +++ b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx @@ -6,6 +6,7 @@ import Link from "next/link"; import { ExternalLink } from "lucide-react"; import { CartesianGrid, + Legend, Line, LineChart as ReLineChart, ReferenceLine, @@ -135,13 +136,29 @@ type EvidenceSeries = { dashed?: boolean; featured?: boolean; smooth?: boolean; + curve?: "linear" | "monotone" | "stepAfter"; + connectNulls?: boolean; + showDot?: boolean; values: Array; }; +type RunwayHistorySeries = { + key: string; + label: string; + rwy: string; + isSettlement: boolean; + color: string; + points: Array<{ ts: number; value: number }>; +}; + // Sliding window: keep at most this many observation points (24h at 1-min ≈ 1440) const MAX_OBS_POINTS = 1440; const HOURLY_CACHE_TTL_MS = 30 * 60 * 1000; +const ROLLING_WINDOW_BEFORE_MS = 6 * 60 * 60 * 1000; +const ROLLING_WINDOW_AFTER_LIVE_MS = 45 * 60 * 1000; +const ROLLING_WINDOW_AFTER_FORECAST_MS = 6 * 60 * 60 * 1000; const _hourlyCache = new Map(); +const RUNWAY_LINE_COLORS = ["#00897b", "#d97706", "#7c3aed", "#0891b2", "#ea580c", "#64748b"]; function validNumber(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; @@ -232,17 +249,132 @@ function seriesStats(values: Array) { return { latest, high, delta15 }; } +function isSettlementRunway(row: ScanOpportunityRow | null, rwy: string) { + const cityKey = normalizeCityKey(row?.city); + const settlementPairs = SETTLEMENT_RUNWAY_PAIRS[cityKey] || []; + if (!settlementPairs.length) return false; + const normalized = rwy + .split("/") + .map(normalizeRunwayLabel) + .filter(Boolean) + .sort() + .join("/"); + return settlementPairs.some((pair) => pairKey(pair) === normalized); +} + +function runwayLabelFromPair(rawPair: unknown, index: number) { + if (Array.isArray(rawPair) && rawPair.length >= 2) { + return `${normalizeRunwayLabel(rawPair[0])}/${normalizeRunwayLabel(rawPair[1])}`; + } + return `RWY ${index + 1}`; +} + type HourlyForecast = { forecastTodayHigh?: number | null; localTime?: string | null; times: string[]; temps: Array; modelCurves?: Record>; + runwayPlateHistory?: Record>>; amos?: AmosData | null; airportCurrent?: AirportCurrentConditions | null; airportPrimary?: AirportCurrentConditions | null; } | null; +function parseRunwayHistoryValue(point: Record) { + return validNumber(point.max_temp_c) ?? validNumber(point.temp_c) ?? validNumber(point.temp) ?? validNumber(point.value); +} + +function parseRunwayHistoryTime( + point: Record, + tzOffset: number, + localDateStr: string, +) { + return getCityLocalUtcTimestamp( + (point.timestamp as string | number | null | undefined) ?? + (point.time as string | number | null | undefined) ?? + (point.observed_at as string | number | null | undefined), + tzOffset, + localDateStr, + ); +} + +function buildRunwayHistorySeries( + row: ScanOpportunityRow | null, + hourly: HourlyForecast, + tzOffset: number, + localDateStr: string, +): RunwayHistorySeries[] { + const directHistory = + hourly?.runwayPlateHistory ?? + ((hourly?.amos as any)?.runway_plate_history as Record>> | undefined) ?? + ((row as any)?.runway_plate_history as Record>> | undefined); + + if (directHistory && typeof directHistory === "object") { + return Object.entries(directHistory) + .map(([rwy, rawPoints], index) => { + const normalizedRwy = String(rwy || `RWY ${index + 1}`).trim(); + const points = (Array.isArray(rawPoints) ? rawPoints : []) + .map((point) => { + const ts = parseRunwayHistoryTime(point, tzOffset, localDateStr); + const value = parseRunwayHistoryValue(point); + return ts !== null && value !== null ? { ts, value } : null; + }) + .filter((point): point is { ts: number; value: number } => point !== null) + .sort((a, b) => a.ts - b.ts) + .slice(-MAX_OBS_POINTS); + const isSettlement = isSettlementRunway(row, normalizedRwy); + return { + key: `runway_${index}`, + label: `${normalizedRwy}${isSettlement ? (row ? " 结算跑道" : " Settlement") : ""}`, + rwy: normalizedRwy, + isSettlement, + color: isSettlement ? "#009688" : RUNWAY_LINE_COLORS[index % RUNWAY_LINE_COLORS.length], + points, + }; + }) + .filter((series) => series.points.length > 1); + } + + const amos = hourly?.amos; + const runwayObs = amos?.runway_obs; + const runwayPairs = runwayObs?.runway_pairs || []; + const runwayTemps = runwayObs?.temperatures || []; + const anchor = + getCityLocalUtcTimestamp(amos?.observation_time_local || amos?.observation_time || hourly?.localTime || row?.local_time, tzOffset, localDateStr) ?? + getCityLocalUtcTimestamp(row?.local_time, tzOffset, localDateStr); + + if (!anchor || !Array.isArray(runwayTemps)) return []; + + return runwayTemps + .map((rawTemps, index) => { + if (!Array.isArray(rawTemps) || rawTemps.length <= 2) return null; + const rwy = runwayLabelFromPair(runwayPairs[index], index); + const isSettlement = isSettlementRunway(row, rwy); + const values = rawTemps + .map(validNumber) + .map((value, pointIndex) => { + if (value === null) return null; + const minutesFromEnd = rawTemps.length - 1 - pointIndex; + return { + ts: anchor - minutesFromEnd * 60 * 1000, + value, + }; + }) + .filter((point): point is { ts: number; value: number } => point !== null); + if (values.length <= 1) return null; + return { + key: `runway_${index}`, + label: `${rwy}${isSettlement ? " 结算跑道" : ""}`, + rwy, + isSettlement, + color: isSettlement ? "#009688" : RUNWAY_LINE_COLORS[index % RUNWAY_LINE_COLORS.length], + points: values.slice(-MAX_OBS_POINTS), + }; + }) + .filter((series): series is RunwayHistorySeries => series !== null); +} + // ── Build aligned data rows for the sliding-window chart ──────────────── function buildSlidingChartData( @@ -254,6 +386,7 @@ function buildSlidingChartData( const settlementObs = normObs(row?.settlement_today_obs || row?.metar_context?.settlement_today_obs, tzOffset); const metarObs = normObs(row?.metar_today_obs || row?.metar_context?.today_obs || row?.metar_recent_obs || row?.metar_context?.recent_obs, tzOffset); + const runwayHistorySeries = buildRunwayHistorySeries(row, hourly, tzOffset, localDateStr); // Collect all timestamps from observations + forecasts const allTimes = new Set(); @@ -263,6 +396,9 @@ function buildSlidingChartData( }; pushObs(settlementObs); pushObs(metarObs); + runwayHistorySeries.forEach((item) => { + item.points.forEach((point) => allTimes.add(point.ts)); + }); // Forecast timestamps const forecastTimes: number[] = []; @@ -289,13 +425,35 @@ function buildSlidingChartData( const series: EvidenceSeries[] = []; + runwayHistorySeries.forEach((item) => { + const vals = na(); + item.points.forEach((o) => { + const idx = tsToIdx.get(o.ts); + if (idx !== undefined) vals[idx] = o.value; + }); + if (vals.some((v) => v !== null)) { + series.push({ + key: item.key, + label: item.label, + source: "Runway", + color: item.color, + dashed: !item.isSettlement, + featured: item.isSettlement, + curve: "monotone", + connectNulls: true, + showDot: item.isSettlement, + values: vals, + }); + } + }); + // Settlement const sVals = na(); settlementObs.forEach((o) => { const idx = tsToIdx.get(o.ts); if (idx !== undefined) sVals[idx] = o.value; }); - if (sVals.some((v) => v !== null)) { + if (!runwayHistorySeries.length && sVals.some((v) => v !== null)) { const cityKey = String(row?.city || "").toLowerCase().trim(); const runwaySensorCities = new Set([ 'beijing', 'shanghai', 'guangzhou', 'shenzhen', 'qingdao', @@ -320,6 +478,8 @@ function buildSlidingChartData( source: row?.metar_context?.station || row?.airport || "Settlement", color: "#009688", featured: true, + curve: "monotone", + connectNulls: true, values: sVals, }); } @@ -337,6 +497,9 @@ function buildSlidingChartData( source: row?.airport || "METAR", color: "#0ea5e9", dashed: true, + curve: "stepAfter", + connectNulls: true, + showDot: true, values: mVals, }); } @@ -366,6 +529,8 @@ function buildSlidingChartData( color: "#f97316", featured: true, smooth: true, + curve: "monotone", + connectNulls: true, values: debVals, }); } @@ -376,6 +541,15 @@ function buildSlidingChartData( Object.keys(hourly.modelCurves).forEach((model, idx) => { const modelTemps = hourly.modelCurves![model]; if (!modelTemps?.length) return; + const finiteModelTemps = modelTemps + .map(validNumber) + .filter((v): v is number => v !== null); + if ( + finiteModelTemps.length < 2 || + Math.max(...finiteModelTemps) - Math.min(...finiteModelTemps) < 0.05 + ) { + return; + } const vals = na(); hourly.times.forEach((t, i) => { const ts = getCityLocalUtcTimestamp(t, tzOffset, localDateStr); @@ -390,6 +564,8 @@ function buildSlidingChartData( color: modelColors[idx % modelColors.length], dashed: true, smooth: true, + curve: "monotone", + connectNulls: true, values: vals, }); } @@ -408,6 +584,8 @@ function buildSlidingChartData( source: "Live", color: "#009688", featured: true, + curve: "monotone", + connectNulls: true, values: vals, }); } @@ -426,6 +604,56 @@ function buildSlidingChartData( return { data, series }; } +function hasNumericValue(row: Record, keys: string[]) { + return keys.some((key) => validNumber(row[key]) !== null); +} + +function buildRollingWindowData( + data: Array>, + series: EvidenceSeries[], + row: ScanOpportunityRow | null, + hourly: HourlyForecast, +) { + if (data.length <= 1) return data; + + const liveKeys = series + .filter((item) => item.key !== "hourly_forecast" && !item.key.startsWith("model_curve_")) + .map((item) => item.key); + const forecastKeys = series + .filter((item) => item.key === "hourly_forecast" || item.key.startsWith("model_curve_")) + .map((item) => item.key); + + const timestampRows = data + .filter((point) => typeof point.ts === "number") + .sort((a, b) => Number(a.ts) - Number(b.ts)); + if (!timestampRows.length) return data; + + const latestLiveTs = [...timestampRows] + .reverse() + .find((point) => hasNumericValue(point, liveKeys))?.ts as number | undefined; + + const tzOffset = row?.tz_offset_seconds ?? 0; + const localDateStr = row?.local_date || new Date().toISOString().slice(0, 10); + const currentLocalTs = getCityLocalUtcTimestamp( + hourly?.localTime || row?.local_time, + tzOffset, + localDateStr, + ); + const maxDataTs = Number(timestampRows[timestampRows.length - 1].ts); + const anchor = latestLiveTs ?? currentLocalTs ?? maxDataTs; + const afterMs = latestLiveTs ? ROLLING_WINDOW_AFTER_LIVE_MS : ROLLING_WINDOW_AFTER_FORECAST_MS; + const start = anchor - ROLLING_WINDOW_BEFORE_MS; + const end = anchor + afterMs; + + const visible = timestampRows.filter((point) => { + const ts = Number(point.ts); + if (ts < start || ts > end) return false; + return hasNumericValue(point, liveKeys) || hasNumericValue(point, forecastKeys); + }); + + return visible.length >= 2 ? visible : timestampRows.slice(-120); +} + // ── Model summary cards (daily high point predictions) ───────────────── function buildModelSummaryCards(row: ScanOpportunityRow | null): EvidenceSeries[] { @@ -475,8 +703,11 @@ function buildMarketTemperatureOptions(row: ScanOpportunityRow | null) { function buildChartDomain( ticks: number[] | null, series: EvidenceSeries[], + visibleData?: Array>, ): [number, number] | ["auto", "auto"] { - const vals = series.flatMap((s) => s.values).filter((v): v is number => validNumber(v) !== null); + const vals = visibleData?.length + ? visibleData.flatMap((point) => series.map((s) => point[s.key])).filter((v): v is number => validNumber(v) !== null) + : series.flatMap((s) => s.values).filter((v): v is number => validNumber(v) !== null); const all = [...(ticks || []), ...vals]; if (!all.length) return ["auto", "auto"]; const min = Math.min(...all); @@ -525,6 +756,7 @@ export function LiveTemperatureThresholdChart({ times: hourlySource.times || [], temps: hourlySource.temps || [], modelCurves: (json.models_hourly ?? (json as any)?.timeseries?.models_hourly)?.curves || undefined, + runwayPlateHistory: (json as any)?.runway_plate_history || (json.amos as any)?.runway_plate_history || undefined, amos: json.amos || null, airportCurrent: json.airport_current || null, airportPrimary: json.airport_primary || null, @@ -537,7 +769,10 @@ export function LiveTemperatureThresholdChart({ }, [city]); const { data, series } = useMemo(() => buildSlidingChartData(row, hourly), [row, hourly]); - const threshold = validNumber(row?.target_threshold) ?? validNumber(row?.target_value); + const visibleData = useMemo( + () => buildRollingWindowData(data, series, row, hourly), + [data, series, row, hourly], + ); const tzOffset = row?.tz_offset_seconds ?? 0; const settlementObs = useMemo(() => { @@ -632,7 +867,10 @@ export function LiveTemperatureThresholdChart({ }, [row, allRows]); const marketTicks = useMemo(() => buildMarketTemperatureOptions(row), [row]); - const chartDomain = useMemo(() => buildChartDomain(marketTicks, series), [marketTicks, series]); + const chartDomain = useMemo( + () => buildChartDomain(marketTicks, series, visibleData), + [marketTicks, series, visibleData], + ); return ( @@ -772,14 +1010,14 @@ export function LiveTemperatureThresholdChart({ ) : null} - + `${Number(value).toFixed(2)}°`} /> + 5 ? 56 : 36} + iconType="plainline" + wrapperStyle={{ fontSize: 11 }} + /> {series.map((item) => ( ))} diff --git a/frontend/components/dashboard/scan-terminal/scan-terminal-client.ts b/frontend/components/dashboard/scan-terminal/scan-terminal-client.ts index 13ed88c4..607f727d 100644 --- a/frontend/components/dashboard/scan-terminal/scan-terminal-client.ts +++ b/frontend/components/dashboard/scan-terminal/scan-terminal-client.ts @@ -264,7 +264,7 @@ async function getTerminal({ min_liquidity: "500", market_type: "maxtemp", time_range: "today", - limit: "36", + limit: "180", force_refresh: String(forceRefresh), skip_polymarket: "true", }); diff --git a/scratch_test.py b/scratch_test.py new file mode 100644 index 00000000..69e3217b --- /dev/null +++ b/scratch_test.py @@ -0,0 +1,62 @@ +import sys +import time +import httpx +from loguru import logger + +logger.remove() +logger.add(sys.stdout, format="[{time:HH:mm:ss}] {message}") + +print("1. Testing direct httpx request to Gamma API...") +t0 = time.time() +try: + resp = httpx.get("https://gamma-api.polymarket.com/markets", params={"active": "true", "closed": "false", "limit": 200}, timeout=10.0) + print(f"Gamma response status: {resp.status_code}") + print(f"Gamma response took: {time.time() - t0:.2f}s") + markets = resp.json() + print(f"Markets count: {len(markets)}") +except Exception as e: + print(f"Failed to fetch from Gamma API: {e}") + +print("\n2. Testing loading PolymarketReadOnlyLayer...") +try: + from src.data_collection.polymarket_readonly import PolymarketReadOnlyLayer + layer = PolymarketReadOnlyLayer() + print(f"Polymarket layer enabled: {layer.enabled}") + + t0 = time.time() + print("Loading active markets via layer...") + m = layer._load_markets(active_only=True) + print(f"Layer active markets count: {len(m)} (took {time.time() - t0:.2f}s)") + + t0 = time.time() + print("Loading broad markets via layer...") + m_broad = layer._load_markets(active_only=False) + print(f"Layer broad markets count: {len(m_broad)} (took {time.time() - t0:.2f}s)") +except Exception as e: + print(f"Layer test failed: {e}") + +print("\n3. Testing scan terminal payload for East Asia cities...") +try: + from web.scan_terminal_service import _build_scan_terminal_payload_uncached + filters = { + "scan_mode": "tradable", + "min_price": 0.05, + "max_price": 0.95, + "min_edge_pct": 2.0, + "min_liquidity": 1000.0, + "high_liquidity_only": False, + "market_type": "maxtemp", + "time_range": "today", + "limit": 28, + "trading_region": "east_asia" + } + t0 = time.time() + res = _build_scan_terminal_payload_uncached(filters, force_refresh=True) + print(f"Build scan terminal payload for east_asia took {time.time() - t0:.2f}s") + print(f"Result Status: {res.get('status')}") + print(f"Result Rows count: {len(res.get('rows', []))}") + for row in res.get('rows', []): + print(f"- City: {row.get('city')}, Question: {row.get('market_question')}, Midpoint: {row.get('midpoint')}, IsPrimary: {row.get('is_primary_signal')}") +except Exception as e: + import traceback + traceback.print_exc() diff --git a/scratch_test_pagination.py b/scratch_test_pagination.py new file mode 100644 index 00000000..2b658e28 --- /dev/null +++ b/scratch_test_pagination.py @@ -0,0 +1,42 @@ +import httpx + +gamma_url = "https://gamma-api.polymarket.com" +all_markets = [] +offset = 0 +limit = 100 +pages = 10 + +for page in range(pages): + params = { + "archived": "false", + "limit": limit, + "offset": offset, + "active": "true", + "closed": "false" + } + resp = httpx.get(f"{gamma_url}/markets", params=params) + batch = resp.json() + if not isinstance(batch, list) and isinstance(batch, dict): + batch = batch.get("markets", []) + + if not batch: + print(f"Page {page}: No more markets.") + break + + all_markets.extend(batch) + print(f"Page {page}: Fetched {len(batch)} markets (total: {len(all_markets)})") + if len(batch) < limit: + print(f"Page {page}: Batch size {len(batch)} < limit {limit}. Stopping.") + break + offset += len(batch) + +weather_markets = [] +for m in all_markets: + q = m.get("question", "").lower() + if "temperature" in q or "weather" in q or "highest temperature" in q: + weather_markets.append(m) + +print(f"\nTotal active markets found: {len(all_markets)}") +print(f"Total weather markets found: {len(weather_markets)}") +for wm in weather_markets[:20]: + print(f"- Question: {wm.get('question')} | Slug: {wm.get('slug')}") diff --git a/tests/test_scan_terminal_modules.py b/tests/test_scan_terminal_modules.py index b25f68b0..737a278a 100644 --- a/tests/test_scan_terminal_modules.py +++ b/tests/test_scan_terminal_modules.py @@ -22,7 +22,7 @@ def test_normalize_scan_terminal_filters_clamps_and_swaps_bounds(): assert filters["min_price"] == 0.0 assert filters["max_price"] == 1.0 - assert filters["limit"] == 100 + assert filters["limit"] == 200 assert filters["min_liquidity"] == 5000.0 diff --git a/web/routers/scan.py b/web/routers/scan.py index 58e9c270..15fe8cc8 100644 --- a/web/routers/scan.py +++ b/web/routers/scan.py @@ -27,6 +27,8 @@ async def scan_terminal( limit: int = 25, force_refresh: bool = False, region: str = "", + trading_region: str = "", + skip_polymarket: bool = False, ): return await get_scan_terminal_payload( request, @@ -40,7 +42,8 @@ async def scan_terminal( time_range=time_range, limit=limit, force_refresh=force_refresh, - region=region if region else None, + region=region or trading_region or None, + skip_polymarket=skip_polymarket, ) diff --git a/web/scan_terminal_filters.py b/web/scan_terminal_filters.py index f9fe60be..a51600d7 100644 --- a/web/scan_terminal_filters.py +++ b/web/scan_terminal_filters.py @@ -46,8 +46,10 @@ def normalize_scan_terminal_filters( or "maxtemp", "time_range": str(raw.get("time_range") or "today").strip().lower() or "today", - "limit": max(1, min(safe_int(raw.get("limit"), 25), 100)), + "limit": max(1, min(safe_int(raw.get("limit"), 25), 200)), "max_spread": max(0.0, _safe_float(raw.get("max_spread")) or 0.03), + "skip_polymarket": str(raw.get("skip_polymarket") or "false").lower() + in {"1", "true", "yes", "on"}, } trading_region = str(raw.get("trading_region") or "").strip().lower() if trading_region and trading_region not in ("all", ""): diff --git a/web/services/scan_api.py b/web/services/scan_api.py index 0567b753..917ea8bc 100644 --- a/web/services/scan_api.py +++ b/web/services/scan_api.py @@ -47,6 +47,7 @@ async def get_scan_terminal_payload( limit: int = 25, force_refresh: bool = False, region: str = "", + skip_polymarket: bool = False, ) -> Dict[str, Any]: legacy_routes._assert_entitlement(request) filters: Dict[str, Any] = { @@ -59,6 +60,7 @@ async def get_scan_terminal_payload( "market_type": market_type, "time_range": time_range, "limit": limit, + "skip_polymarket": skip_polymarket, } if region: filters["trading_region"] = region