From 42d4a4e198b1a529fbb5e26ba19abbe77a1f6962 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Fri, 24 Apr 2026 14:26:47 +0800 Subject: [PATCH] Simplify scan opportunity list and speed quotes --- .../components/dashboard/Dashboard.module.css | 67 +---------------- .../components/dashboard/OpportunityTable.tsx | 40 +---------- src/data_collection/polymarket_readonly.py | 72 +++++++++++++++---- tests/test_polymarket_readonly.py | 30 ++++++++ 4 files changed, 92 insertions(+), 117 deletions(-) diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index a0202ee2..9586f9a5 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -9572,7 +9572,7 @@ .root :global(.scan-table-header) { display: grid; - grid-template-columns: 46px minmax(180px, 1.08fr) minmax(132px, 0.72fr) minmax(220px, 1.08fr) minmax(150px, 0.82fr) 92px 82px; + grid-template-columns: minmax(210px, 1.1fr) minmax(132px, 0.72fr) minmax(220px, 1.12fr) minmax(150px, 0.82fr) 96px; gap: 10px; align-items: center; padding: 14px 18px; @@ -9786,7 +9786,7 @@ .root :global(.scan-table-row) { display: grid; - grid-template-columns: 46px minmax(180px, 1.08fr) minmax(132px, 0.72fr) minmax(220px, 1.08fr) minmax(150px, 0.82fr) 92px 82px; + grid-template-columns: minmax(210px, 1.1fr) minmax(132px, 0.72fr) minmax(220px, 1.12fr) minmax(150px, 0.82fr) 96px; gap: 10px; align-items: center; min-height: 132px; @@ -9813,38 +9813,12 @@ box-shadow: inset 0 0 0 1px rgba(23, 217, 139, 0.48); } -.root :global(.scan-rank-cell), -.root :global(.scan-score-cell), .root :global(.scan-edge-cell) { display: flex; align-items: center; justify-content: center; } -.root :global(.scan-rank-circle) { - width: 34px; - height: 34px; - font-size: 16px; - font-weight: 800; - color: #06150d; - background: linear-gradient(180deg, #24e495, #11b86f); -} - -.root :global(.scan-rank-circle.yellow) { - color: #281a00; - background: linear-gradient(180deg, #ffd95f, #ffb61c); -} - -.root :global(.scan-rank-circle.gray) { - color: #08131f; - background: linear-gradient(180deg, #d6e3f7, #94a7c4); -} - -.root :global(.scan-rank-circle.red) { - color: #fff3f3; - background: linear-gradient(180deg, #ff7373, #df4747); -} - .root :global(.scan-city-cell) { display: block; } @@ -9870,11 +9844,6 @@ -webkit-line-clamp: 2; } -.root :global(.scan-city-volume) { - font-size: 14px; - color: #dbe7f8; -} - .root :global(.scan-time-cell) { display: flex; flex-direction: column; @@ -10036,38 +10005,6 @@ color: #ff6868; } -.root :global(.scan-score-ring) { - width: 58px; - height: 58px; - border-radius: 50%; - background: conic-gradient(#20df91 0 85%, rgba(255, 255, 255, 0.06) 85% 100%); -} - -.root :global(.scan-score-ring.tone-yellow) { - background: conic-gradient(#ffbe26 0 72%, rgba(255, 255, 255, 0.06) 72% 100%); -} - -.root :global(.scan-score-ring.tone-red) { - background: conic-gradient(#ff5d5d 0 58%, rgba(255, 255, 255, 0.06) 58% 100%); -} - -.root :global(.scan-score-ring::after) { - content: ""; - position: absolute; - inset: 5px; - border-radius: 50%; - background: #0b1728; - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05); -} - -.root :global(.scan-score-ring span) { - position: relative; - z-index: 1; - font-size: 18px; - font-weight: 800; - color: #ecf4ff; -} - .root :global(.scan-row-fav) { display: none; } diff --git a/frontend/components/dashboard/OpportunityTable.tsx b/frontend/components/dashboard/OpportunityTable.tsx index e7609ce7..f0f8b7c9 100644 --- a/frontend/components/dashboard/OpportunityTable.tsx +++ b/frontend/components/dashboard/OpportunityTable.tsx @@ -24,14 +24,6 @@ function formatPercent(value?: number | null, signed = false) { return `${signed && numeric >= 0 ? "+" : ""}${numeric.toFixed(1)}%`; } -function formatVolume(value?: number | null) { - const numeric = Number(value || 0); - if (!Number.isFinite(numeric) || numeric <= 0) return "--"; - if (numeric >= 1_000_000) return `$${(numeric / 1_000_000).toFixed(1)}M`; - if (numeric >= 1_000) return `$${(numeric / 1_000).toFixed(0)}K`; - return `$${numeric.toFixed(0)}`; -} - function formatWindowMinutes(value: number | null | undefined, locale: string) { if (value == null || !Number.isFinite(Number(value))) return "--"; const minutes = Math.max(0, Math.round(Number(value))); @@ -118,13 +110,6 @@ export function getWindowPhaseMeta( }; } -function scoreTone(score?: number | null) { - const numeric = Number(score || 0); - if (numeric >= 85) return "green"; - if (numeric >= 70) return "yellow"; - return "gray"; -} - function ProbabilityPreview({ row, locale, @@ -197,15 +182,6 @@ function ProbabilityPreview({ ); } -function ScoreRing({ score }: { score?: number | null }) { - const displayScore = Math.max(0, Math.min(100, Number(score || 0))); - return ( -
- {displayScore.toFixed(0)} -
- ); -} - export const OpportunityTable = React.memo(function OpportunityTable({ rows, status, @@ -271,17 +247,15 @@ export const OpportunityTable = React.memo(function OpportunityTable({ ) : null}
- {isEn ? "City / Market" : "城市 / 市场"} {isEn ? "Local Time / Phase" : "当前时间 / 阶段"} {isEn ? "EMOS / Market" : "EMOS / 市场"} {isEn ? "Quote / Model" : "买价 / 模型"} {isEn ? "Edge" : "边际优势"} - {isEn ? "Score" : "综合得分"}
- {rows.map((row, index) => { + {rows.map((row) => { const phaseMeta = getWindowPhaseMeta(row, locale); const tempSymbol = normalizeTemperatureSymbol(row.target_unit || row.temp_symbol); const localizedCityName = getLocalizedCityName( @@ -290,7 +264,6 @@ export const OpportunityTable = React.memo(function OpportunityTable({ locale, ); const selected = selectedRowId === row.id; - const scoreClass = scoreTone(row.final_score); return ( ); })} diff --git a/src/data_collection/polymarket_readonly.py b/src/data_collection/polymarket_readonly.py index d806b838..6ec6370b 100644 --- a/src/data_collection/polymarket_readonly.py +++ b/src/data_collection/polymarket_readonly.py @@ -418,6 +418,8 @@ class PolymarketReadOnlyLayer: _safe_float(os.getenv("POLYMARKET_SIGNAL_MIN_LIQUIDITY")) or 500.0 ) self.edge_threshold = _safe_float(os.getenv("POLYMARKET_SIGNAL_EDGE_PCT")) or 2.0 + fast_price_only = _safe_bool(os.getenv("POLYMARKET_FAST_PRICE_ONLY", "true")) + self.fast_price_only = True if fast_price_only is None else bool(fast_price_only) self._session = httpx.Client( timeout=self.http_timeout, @@ -1747,6 +1749,22 @@ class PolymarketReadOnlyLayer: ask = _extract_price( self._clob_get("/price", {"token_id": token_id, "side": "SELL"}) ) + if self.fast_price_only: + buy, sell = self._resolve_trade_prices(buy=ask, sell=bid, book=None) + midpoint = (buy + sell) / 2.0 if buy is not None and sell is not None else (buy or sell) + spread = max(0.0, float(buy) - float(sell)) if buy is not None and sell is not None else None + return { + "buy": buy, + "sell": sell, + "midpoint": _clamp_probability(midpoint), + "spread": spread, + "last_trade_price": None, + "quote_source": "polymarket_clob_fast_price", + "quote_age_ms": 0, + "book": None, + "book_liquidity": None, + } + midpoint = _extract_price(self._clob_get("/midpoint", {"token_id": token_id})) last_trade = _extract_price( self._clob_get("/last-trade-price", {"token_id": token_id}) @@ -1754,10 +1772,14 @@ class PolymarketReadOnlyLayer: orderbook_raw = self._clob_get("/book", {"token_id": token_id}) book, book_liquidity = self._normalize_orderbook(orderbook_raw) buy, sell = self._resolve_trade_prices(buy=ask, sell=bid, book=book) + if midpoint is None and buy is not None and sell is not None: + midpoint = (buy + sell) / 2.0 + spread = max(0.0, float(buy) - float(sell)) if buy is not None and sell is not None else None return { "buy": buy, "sell": sell, "midpoint": midpoint, + "spread": spread, "last_trade_price": last_trade, "quote_source": "polymarket_clob_rest", "quote_age_ms": 0, @@ -2326,7 +2348,12 @@ class PolymarketReadOnlyLayer: missing.append(token_id) continue cached_data = cached.get("data", {}) or {} - if include_books and not cached_data.get("book") and cached_data.get("book_liquidity") is None: + if ( + include_books + and not self.fast_price_only + and not cached_data.get("book") + and cached_data.get("book_liquidity") is None + ): missing.append(token_id) continue results[token_id] = dict(cached_data) @@ -2350,24 +2377,36 @@ class PolymarketReadOnlyLayer: "/prices", [{"token_id": token_id, "side": "BUY"} for token_id in chunk], ) - midpoint_payload = self._clob_post( - "/midpoints", - [{"token_id": token_id} for token_id in chunk], + midpoint_payload = ( + self._clob_post( + "/midpoints", + [{"token_id": token_id} for token_id in chunk], + ) + if not self.fast_price_only + else None ) - spread_payload = self._clob_post( - "/spreads", - [{"token_id": token_id} for token_id in chunk], + spread_payload = ( + self._clob_post( + "/spreads", + [{"token_id": token_id} for token_id in chunk], + ) + if not self.fast_price_only + else None ) - last_trade_payload = self._clob_post( - "/last-trade-prices", - [{"token_id": token_id} for token_id in chunk], + last_trade_payload = ( + self._clob_post( + "/last-trade-prices", + [{"token_id": token_id} for token_id in chunk], + ) + if not self.fast_price_only + else None ) books_payload = ( self._clob_post( "/books", [{"token_id": token_id} for token_id in chunk], ) - if include_books + if include_books and not self.fast_price_only else None ) ask_map.update(self._extract_batch_price_map(sell_payload, "SELL")) @@ -2419,7 +2458,11 @@ class PolymarketReadOnlyLayer: "midpoint": midpoint, "spread": spread, "last_trade_price": last_trade, - "quote_source": "polymarket_clob_rest_batch", + "quote_source": ( + "polymarket_clob_fast_batch" + if self.fast_price_only + else "polymarket_clob_rest_batch" + ), "quote_age_ms": 0, "book": book, "book_liquidity": book_liquidity, @@ -3087,7 +3130,10 @@ class PolymarketReadOnlyLayer: continue shortlisted_tokens.extend([entry["yes_token_id"], entry["no_token_id"]]) - precise_quotes = self._batch_get_token_market_data(shortlisted_tokens, include_books=True) + precise_quotes = self._batch_get_token_market_data( + shortlisted_tokens, + include_books=not self.fast_price_only, + ) for entry in market_entries: market_slug = str(entry["market"].get("slug") or "").strip() if market_slug not in seen_slugs: diff --git a/tests/test_polymarket_readonly.py b/tests/test_polymarket_readonly.py index 388cf006..95459061 100644 --- a/tests/test_polymarket_readonly.py +++ b/tests/test_polymarket_readonly.py @@ -39,6 +39,7 @@ def test_extract_market_bucket_range_supports_fahrenheit_ranges(): def test_fetch_token_market_data_uses_rest_orderbook_executable_prices(): layer = PolymarketReadOnlyLayer() + layer.fast_price_only = False payloads = { ("/price", "BUY"): {"price": "0.27"}, ("/price", "SELL"): {"price": "0.23"}, @@ -68,8 +69,37 @@ def test_fetch_token_market_data_uses_rest_orderbook_executable_prices(): assert data["quote_source"] == "polymarket_clob_rest" +def test_fetch_token_market_data_fast_price_only_skips_heavy_endpoints(): + layer = PolymarketReadOnlyLayer() + calls = [] + payloads = { + ("/price", "BUY"): {"price": "0.23"}, + ("/price", "SELL"): {"price": "0.27"}, + } + + def _fake_clob_get(path, params): + calls.append((path, params.get("side"))) + if path == "/price": + return payloads[(path, params.get("side"))] + return None + + layer._clob_get = _fake_clob_get + + data = layer._fetch_token_market_data("token-1") + + assert calls == [("/price", "BUY"), ("/price", "SELL")] + assert data["buy"] == 0.27 + assert data["sell"] == 0.23 + assert data["midpoint"] == 0.25 + assert round(data["spread"], 6) == 0.04 + assert data["last_trade_price"] is None + assert data["book"] is None + assert data["quote_source"] == "polymarket_clob_fast_price" + + def test_fetch_token_market_data_keeps_buy_sell_semantics_without_orderbook(): layer = PolymarketReadOnlyLayer() + layer.fast_price_only = False payloads = { ("/price", "BUY"): {"price": "0.23"}, ("/price", "SELL"): {"price": "0.27"},