Simplify scan opportunity list and speed quotes
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className={`scan-score-ring tone-${scoreTone(displayScore)}`}>
|
||||
<span>{displayScore.toFixed(0)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const OpportunityTable = React.memo(function OpportunityTable({
|
||||
rows,
|
||||
status,
|
||||
@@ -271,17 +247,15 @@ export const OpportunityTable = React.memo(function OpportunityTable({
|
||||
</div>
|
||||
) : null}
|
||||
<div className="scan-table-header">
|
||||
<span />
|
||||
<span>{isEn ? "City / Market" : "城市 / 市场"}</span>
|
||||
<span>{isEn ? "Local Time / Phase" : "当前时间 / 阶段"}</span>
|
||||
<span>{isEn ? "EMOS / Market" : "EMOS / 市场"}</span>
|
||||
<span>{isEn ? "Quote / Model" : "买价 / 模型"}</span>
|
||||
<span>{isEn ? "Edge" : "边际优势"}</span>
|
||||
<span>{isEn ? "Score" : "综合得分"}</span>
|
||||
</div>
|
||||
|
||||
<div className="scan-table-body">
|
||||
{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 (
|
||||
<button
|
||||
key={row.id}
|
||||
@@ -298,19 +271,12 @@ export const OpportunityTable = React.memo(function OpportunityTable({
|
||||
className={`scan-table-row ${selected ? "selected" : ""}`}
|
||||
onClick={() => onSelectRow?.(row)}
|
||||
>
|
||||
<div className="scan-rank-cell">
|
||||
<div className={`scan-rank-circle ${scoreClass}`}>
|
||||
{index + 1}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="scan-city-cell">
|
||||
<div className="scan-city-copy">
|
||||
<div className="scan-city-name">{localizedCityName}</div>
|
||||
<div className="scan-city-sub">
|
||||
{row.market_question || row.target_label || "--"}
|
||||
</div>
|
||||
<div className="scan-city-volume">{formatVolume(row.volume)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -344,10 +310,6 @@ export const OpportunityTable = React.memo(function OpportunityTable({
|
||||
<div className={`scan-edge-cell ${Number(row.edge_percent || 0) >= 0 ? "positive" : "negative"}`}>
|
||||
{formatPercent(row.edge_percent, true)}
|
||||
</div>
|
||||
|
||||
<div className="scan-score-cell">
|
||||
<ScoreRing score={row.final_score} />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"},
|
||||
|
||||
Reference in New Issue
Block a user