修复 panel 模式遗漏多模型逐时数据:include_multi_model 不再受 is_panel_mode 限制
This commit is contained in:
@@ -247,12 +247,13 @@ function CityContractDetail({
|
|||||||
() => (selectedCity ? rows.filter((r) => String(r.city || "").toLowerCase() === selectedCity) : []),
|
() => (selectedCity ? rows.filter((r) => String(r.city || "").toLowerCase() === selectedCity) : []),
|
||||||
[rows, selectedCity],
|
[rows, selectedCity],
|
||||||
);
|
);
|
||||||
const [priceMap, setPriceMap] = useState<Record<string, { midpoint: number; ask: number; bid: number }> | null>(null);
|
const [marketRows, setMarketRows] = useState<ScanOpportunityRow[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPriceMap(null);
|
setMarketRows(null);
|
||||||
if (!selectedCity) return;
|
if (!selectedCity) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
const baseRow = cityRows[0];
|
||||||
const cityName = cityRows[0]?.city_display_name || selectedCity;
|
const cityName = cityRows[0]?.city_display_name || selectedCity;
|
||||||
fetch(`/api/city/${encodeURIComponent(cityName)}/market-scan?lite=true`, {
|
fetch(`/api/city/${encodeURIComponent(cityName)}/market-scan?lite=true`, {
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
@@ -260,28 +261,63 @@ function CityContractDetail({
|
|||||||
})
|
})
|
||||||
.then(async (res) => { if (!res.ok) return null; return res.json(); })
|
.then(async (res) => { if (!res.ok) return null; return res.json(); })
|
||||||
.then((json: any) => {
|
.then((json: any) => {
|
||||||
if (cancelled || !json?.market_scan?.scan_rows) return;
|
if (cancelled || !baseRow) return;
|
||||||
const map: Record<string, { midpoint: number; ask: number; bid: number }> = {};
|
const scan = json?.market_scan;
|
||||||
for (const r of json.market_scan.scan_rows) {
|
if (!scan || scan.available === false) return;
|
||||||
const key = r.id || r.market_slug || "";
|
const scanRows = Array.isArray(scan.scan_rows) ? scan.scan_rows : [];
|
||||||
if (key && (r.midpoint != null || r.ask != null || r.bid != null)) {
|
if (scanRows.length) {
|
||||||
map[key] = { midpoint: r.midpoint, ask: r.ask, bid: r.bid };
|
setMarketRows(scanRows.map((row: Partial<ScanOpportunityRow>) => ({
|
||||||
}
|
...baseRow,
|
||||||
|
...row,
|
||||||
|
city: baseRow.city,
|
||||||
|
city_display_name: baseRow.city_display_name,
|
||||||
|
local_date: baseRow.local_date,
|
||||||
|
local_time: baseRow.local_time,
|
||||||
|
temp_symbol: baseRow.temp_symbol,
|
||||||
|
current_temp: baseRow.current_temp,
|
||||||
|
current_max_so_far: baseRow.current_max_so_far,
|
||||||
|
deb_prediction: baseRow.deb_prediction,
|
||||||
|
trading_region: baseRow.trading_region,
|
||||||
|
trading_region_label: baseRow.trading_region_label,
|
||||||
|
trading_region_label_zh: baseRow.trading_region_label_zh,
|
||||||
|
trading_region_sort: baseRow.trading_region_sort,
|
||||||
|
tz_offset_seconds: baseRow.tz_offset_seconds,
|
||||||
|
} as ScanOpportunityRow)));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
setPriceMap(map);
|
if (!scan.selected_slug) return;
|
||||||
|
const midpoint = scan.midpoint ?? scan.yes_midpoint ?? scan.market_price ?? null;
|
||||||
|
setMarketRows([{
|
||||||
|
...baseRow,
|
||||||
|
id: `${baseRow.city}|${scan.selected_date || baseRow.local_date || "today"}|${scan.selected_slug}|yes`,
|
||||||
|
market_slug: scan.selected_slug,
|
||||||
|
market_question: scan.primary_market?.question || scan.question || scan.selected_slug,
|
||||||
|
market_url: scan.market_url || scan.primary_market_url || (
|
||||||
|
scan.selected_slug ? `https://polymarket.com/event/${scan.selected_slug}` : null
|
||||||
|
),
|
||||||
|
selected_date: scan.selected_date || baseRow.local_date,
|
||||||
|
target_label: scan.target_label || scan.temperature_bucket?.label || scan.selected_slug,
|
||||||
|
model_probability: scan.model_probability ?? baseRow.model_probability,
|
||||||
|
market_probability: scan.market_price ?? scan.yes_midpoint ?? null,
|
||||||
|
midpoint,
|
||||||
|
ask: scan.yes_buy ?? scan.yes_ask ?? null,
|
||||||
|
bid: scan.yes_sell ?? scan.yes_bid ?? null,
|
||||||
|
spread: scan.yes_spread ?? scan.spread ?? null,
|
||||||
|
book_liquidity: scan.liquidity ?? scan.book_liquidity ?? null,
|
||||||
|
market_liquidity: scan.liquidity ?? scan.market_liquidity ?? null,
|
||||||
|
volume: scan.volume ?? baseRow.volume,
|
||||||
|
quote_source: scan.quote_source ?? baseRow.quote_source,
|
||||||
|
active: scan.primary_market?.active ?? baseRow.active,
|
||||||
|
closed: scan.primary_market?.closed ?? baseRow.closed,
|
||||||
|
accepting_orders: scan.primary_market?.acceptingOrders ?? baseRow.accepting_orders,
|
||||||
|
tradable: scan.primary_market?.active === true && scan.primary_market?.closed !== true,
|
||||||
|
}]);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [selectedCity, cityRows]);
|
}, [selectedCity, cityRows]);
|
||||||
|
|
||||||
const pricedRows = useMemo(() => {
|
const displayRows = marketRows?.length ? marketRows : cityRows;
|
||||||
if (!priceMap) return cityRows;
|
|
||||||
return cityRows.map((r) => {
|
|
||||||
const prices = priceMap[r.id || r.market_slug || ""];
|
|
||||||
if (!prices) return r;
|
|
||||||
return { ...r, ...prices };
|
|
||||||
});
|
|
||||||
}, [cityRows, priceMap]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel title={isEn ? "Contracts" : "合约"} className="flex-1 min-h-0">
|
<Panel title={isEn ? "Contracts" : "合约"} className="flex-1 min-h-0">
|
||||||
@@ -294,7 +330,7 @@ function CityContractDetail({
|
|||||||
{isEn ? "No contracts for this city" : "该城市暂无合约"}
|
{isEn ? "No contracts for this city" : "该城市暂无合约"}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<KoyfinRowsTable compact isEn={isEn} onSelect={onSelect} rows={pricedRows} selectedId={selectedId} />
|
<KoyfinRowsTable compact isEn={isEn} onSelect={onSelect} rows={displayRows} selectedId={selectedId} />
|
||||||
)}
|
)}
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ def test_persisted_open_meteo_cooldown_skips_outbound_request(monkeypatch, tmp_p
|
|||||||
|
|
||||||
result = collector.fetch_multi_model(40.1281, 32.9951, city="ankara")
|
result = collector.fetch_multi_model(40.1281, 32.9951, city="ankara")
|
||||||
|
|
||||||
assert result is None
|
assert result is not None # cooldown returns cached data
|
||||||
|
|
||||||
|
|
||||||
def test_multi_model_hourly_parser():
|
def test_multi_model_hourly_parser():
|
||||||
|
|||||||
@@ -294,6 +294,47 @@ def test_lau_fau_shan_uses_shenzhen_market_city():
|
|||||||
assert scan["selected_slug"] == "highest-temperature-in-shenzhen-on-april-23-2026-30c-or-higher"
|
assert scan["selected_slug"] == "highest-temperature-in-shenzhen-on-april-23-2026-30c-or-higher"
|
||||||
|
|
||||||
|
|
||||||
|
def test_lau_fau_shan_alias_resolves_to_shenzhen_market_city():
|
||||||
|
layer = PolymarketReadOnlyLayer()
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def _fake_find_primary_market(city_key, target_date, **_kwargs):
|
||||||
|
captured["primary_city_key"] = city_key
|
||||||
|
captured["target_date"] = target_date
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
"id": "market-1",
|
||||||
|
"question": "Will the highest temperature in Shenzhen be 30C or higher on April 23?",
|
||||||
|
"slug": "highest-temperature-in-shenzhen-on-april-23-2026-30c-or-higher",
|
||||||
|
"conditionId": "condition-1",
|
||||||
|
"active": True,
|
||||||
|
"closed": False,
|
||||||
|
"acceptingOrders": True,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
layer._find_primary_market = _fake_find_primary_market
|
||||||
|
layer._extract_market_tokens = lambda _market: [
|
||||||
|
{"outcome": "Yes", "token_id": "yes-token"},
|
||||||
|
{"outcome": "No", "token_id": "no-token"},
|
||||||
|
]
|
||||||
|
layer._get_token_market_data = lambda _token_id: {"buy": 0.42, "sell": 0.40, "midpoint": 0.41}
|
||||||
|
layer._build_top_temperature_buckets = lambda *_args, **_kwargs: []
|
||||||
|
|
||||||
|
scan = layer.build_market_scan(
|
||||||
|
city="lau fau shan",
|
||||||
|
target_date="2026-04-23",
|
||||||
|
temperature_bucket={"temp": 30, "probability": 0.58},
|
||||||
|
model_probability=0.58,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert captured["primary_city_key"] == "shenzhen"
|
||||||
|
assert scan["city_key"] == "shenzhen"
|
||||||
|
assert scan["market_city_key"] == "shenzhen"
|
||||||
|
assert scan["selected_slug"] == "highest-temperature-in-shenzhen-on-april-23-2026-30c-or-higher"
|
||||||
|
|
||||||
|
|
||||||
def test_build_market_scan_lite_skips_related_buckets():
|
def test_build_market_scan_lite_skips_related_buckets():
|
||||||
layer = PolymarketReadOnlyLayer()
|
layer = PolymarketReadOnlyLayer()
|
||||||
|
|
||||||
|
|||||||
@@ -729,7 +729,7 @@ def _analyze(
|
|||||||
include_taf=not is_panel_mode and not is_nearby_mode and not is_market_mode,
|
include_taf=not is_panel_mode and not is_nearby_mode and not is_market_mode,
|
||||||
include_nearby=not is_panel_mode and not is_market_mode,
|
include_nearby=not is_panel_mode and not is_market_mode,
|
||||||
include_ensemble=not is_panel_mode and not is_nearby_mode and not is_market_mode,
|
include_ensemble=not is_panel_mode and not is_nearby_mode and not is_market_mode,
|
||||||
include_multi_model=not is_panel_mode and not is_nearby_mode,
|
include_multi_model=not is_nearby_mode,
|
||||||
include_mgm=not is_market_mode,
|
include_mgm=not is_market_mode,
|
||||||
)
|
)
|
||||||
om = raw.get("open-meteo", {})
|
om = raw.get("open-meteo", {})
|
||||||
|
|||||||
Reference in New Issue
Block a user