feat: implement comprehensive Polymarket weather analysis service with frontend dashboard and market scanning capabilities
This commit is contained in:
@@ -33,6 +33,11 @@ export async function GET(
|
||||
params.set("market_slug", marketSlug);
|
||||
}
|
||||
|
||||
const lite = req.nextUrl.searchParams.get("lite");
|
||||
if (lite) {
|
||||
params.set("lite", lite);
|
||||
}
|
||||
|
||||
const url = `${API_BASE}/api/city/${encodeURIComponent(name)}/market-scan?${params.toString()}`;
|
||||
|
||||
try {
|
||||
|
||||
@@ -746,6 +746,7 @@ export function FutureForecastModal() {
|
||||
dashboardClient
|
||||
.getCityMarketScan(cityName, {
|
||||
force: false,
|
||||
lite: false,
|
||||
marketSlug: detail.market_scan?.primary_market?.slug || null,
|
||||
targetDate: dateStr,
|
||||
})
|
||||
@@ -766,7 +767,7 @@ export function FutureForecastModal() {
|
||||
return;
|
||||
}
|
||||
refreshMarketScan();
|
||||
}, 3000);
|
||||
}, 30_000);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
|
||||
@@ -346,6 +346,7 @@ type AssistantMessage = {
|
||||
};
|
||||
|
||||
type OpportunityScanState = "pending" | "complete" | "empty" | "error";
|
||||
const HOME_OPPORTUNITY_REFRESH_MS = 30_000;
|
||||
|
||||
type AssistantDockPosition = {
|
||||
right: number;
|
||||
@@ -2395,6 +2396,7 @@ function DashboardScreen() {
|
||||
const { t } = useI18n();
|
||||
const didAutoFocusRef = useRef(false);
|
||||
const marketScanInflightRef = useRef<Set<string>>(new Set());
|
||||
const marketScanPollingRef = useRef(false);
|
||||
const [marketScanStatusByCity, setMarketScanStatusByCity] = useState<
|
||||
Record<string, OpportunityScanState>
|
||||
>({});
|
||||
@@ -2517,7 +2519,7 @@ function DashboardScreen() {
|
||||
const existingDetail = store.cityDetailsByName[cityName];
|
||||
const marketScan =
|
||||
existingDetail?.market_scan ||
|
||||
(await store.ensureCityMarketScan(cityName, false));
|
||||
(await store.ensureCityMarketScan(cityName, false, { lite: true }));
|
||||
if (cancelled) return;
|
||||
setMarketScanStatusByCity((current) => ({
|
||||
...current,
|
||||
@@ -2552,6 +2554,57 @@ function DashboardScreen() {
|
||||
store.proAccess.loading,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showHomepageChrome) return;
|
||||
if (store.proAccess.loading || !store.proAccess.authenticated) return;
|
||||
if (!marketScanTargetNames.length) return;
|
||||
|
||||
let cancelled = false;
|
||||
let intervalId: number | null = null;
|
||||
|
||||
const refreshAllMarketScans = async () => {
|
||||
if (cancelled || marketScanPollingRef.current) return;
|
||||
marketScanPollingRef.current = true;
|
||||
const queue = [...marketScanTargetNames];
|
||||
try {
|
||||
const runWorker = async () => {
|
||||
while (!cancelled) {
|
||||
const cityName = queue.shift();
|
||||
if (!cityName) return;
|
||||
await store.ensureCityMarketScan(cityName, false, { lite: true });
|
||||
}
|
||||
};
|
||||
await Promise.allSettled(
|
||||
Array.from({ length: Math.min(3, queue.length) }, () => runWorker()),
|
||||
);
|
||||
} finally {
|
||||
marketScanPollingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
void refreshAllMarketScans();
|
||||
intervalId = window.setInterval(() => {
|
||||
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
||||
return;
|
||||
}
|
||||
void refreshAllMarketScans();
|
||||
}, HOME_OPPORTUNITY_REFRESH_MS);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
marketScanPollingRef.current = false;
|
||||
if (intervalId != null) {
|
||||
window.clearInterval(intervalId);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
marketScanTargetNames,
|
||||
showHomepageChrome,
|
||||
store.ensureCityMarketScan,
|
||||
store.proAccess.authenticated,
|
||||
store.proAccess.loading,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
|
||||
@@ -45,6 +45,11 @@ interface DashboardStoreValue extends DashboardState {
|
||||
ensureCityMarketScan: (
|
||||
cityName: string,
|
||||
force?: boolean,
|
||||
options?: {
|
||||
lite?: boolean;
|
||||
marketSlug?: string | null;
|
||||
targetDate?: string | null;
|
||||
},
|
||||
) => Promise<CityDetail["market_scan"] | null>;
|
||||
focusCity: (cityName: string) => Promise<void>;
|
||||
forecastModalMode: ForecastModalMode | null;
|
||||
@@ -298,6 +303,39 @@ function mergeCityDetail(
|
||||
};
|
||||
}
|
||||
|
||||
function mergeMarketScan(
|
||||
current: CityDetail["market_scan"] | undefined,
|
||||
incoming: CityDetail["market_scan"] | null | undefined,
|
||||
): CityDetail["market_scan"] | undefined {
|
||||
if (!incoming) return current;
|
||||
if (!current) return incoming || undefined;
|
||||
|
||||
const preserveHeavySlices = incoming.scan_scope === "lite";
|
||||
const nextTopBuckets =
|
||||
preserveHeavySlices &&
|
||||
(!Array.isArray(incoming.top_buckets) || incoming.top_buckets.length === 0)
|
||||
? current.top_buckets
|
||||
: incoming.top_buckets;
|
||||
const nextAllBuckets =
|
||||
preserveHeavySlices &&
|
||||
(!Array.isArray(incoming.all_buckets) || incoming.all_buckets.length === 0)
|
||||
? current.all_buckets
|
||||
: incoming.all_buckets;
|
||||
const nextRecentTrades =
|
||||
preserveHeavySlices &&
|
||||
(!Array.isArray(incoming.recent_trades) || incoming.recent_trades.length === 0)
|
||||
? current.recent_trades
|
||||
: incoming.recent_trades;
|
||||
|
||||
return {
|
||||
...current,
|
||||
...incoming,
|
||||
top_buckets: nextTopBuckets,
|
||||
all_buckets: nextAllBuckets,
|
||||
recent_trades: nextRecentTrades,
|
||||
};
|
||||
}
|
||||
|
||||
function toHistoryMeta(payload: HistoryPayload): HistoryPayloadMeta {
|
||||
const history = Array.isArray(payload.history) ? payload.history : [];
|
||||
const previewCount = Number(payload.preview_count || history.length || 0);
|
||||
@@ -569,7 +607,15 @@ export function DashboardStoreProvider({
|
||||
return detail;
|
||||
};
|
||||
|
||||
const ensureCityMarketScan = async (cityName: string, force = false) => {
|
||||
const ensureCityMarketScan = async (
|
||||
cityName: string,
|
||||
force = false,
|
||||
options?: {
|
||||
lite?: boolean;
|
||||
marketSlug?: string | null;
|
||||
targetDate?: string | null;
|
||||
},
|
||||
) => {
|
||||
let cached = cityDetailsByName[cityName];
|
||||
try {
|
||||
if (!cached) {
|
||||
@@ -577,7 +623,10 @@ export function DashboardStoreProvider({
|
||||
}
|
||||
const payload = await dashboardClient.getCityMarketScan(cityName, {
|
||||
force,
|
||||
targetDate: cached?.local_date || selectedForecastDate || null,
|
||||
lite: options?.lite === true,
|
||||
marketSlug: options?.marketSlug || null,
|
||||
targetDate:
|
||||
options?.targetDate || cached?.local_date || selectedForecastDate || null,
|
||||
});
|
||||
if (!payload.market_scan) return null;
|
||||
setCityDetailsByName((current) => {
|
||||
@@ -587,7 +636,7 @@ export function DashboardStoreProvider({
|
||||
...current,
|
||||
[cityName]: {
|
||||
...detail,
|
||||
market_scan: payload.market_scan || undefined,
|
||||
market_scan: mergeMarketScan(detail.market_scan, payload.market_scan),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -303,6 +303,7 @@ export const dashboardClient = {
|
||||
cityName: string,
|
||||
options?: {
|
||||
force?: boolean;
|
||||
lite?: boolean;
|
||||
marketSlug?: string | null;
|
||||
targetDate?: string | null;
|
||||
},
|
||||
@@ -318,9 +319,13 @@ export const dashboardClient = {
|
||||
if (options?.marketSlug) {
|
||||
params.set("market_slug", options.marketSlug);
|
||||
}
|
||||
if (options?.lite) {
|
||||
params.set("lite", "true");
|
||||
}
|
||||
const requestKey = [
|
||||
cityName,
|
||||
force ? "force" : "cached",
|
||||
options?.lite ? "lite" : "full",
|
||||
options?.targetDate || "",
|
||||
options?.marketSlug || "",
|
||||
].join("::");
|
||||
|
||||
@@ -369,23 +369,34 @@ export interface MarketScan {
|
||||
temperature_bucket?: ProbabilityBucket | null;
|
||||
model_probability?: number | null;
|
||||
market_price?: number | null;
|
||||
midpoint?: number | null;
|
||||
spread?: number | null;
|
||||
edge_percent?: number | null;
|
||||
signal_label?: string | null;
|
||||
confidence?: string | null;
|
||||
probability_engine?: string | null;
|
||||
probability_calibration_mode?: string | null;
|
||||
yes_token?: MarketToken | null;
|
||||
no_token?: MarketToken | null;
|
||||
yes_buy?: number | null;
|
||||
yes_sell?: number | null;
|
||||
yes_midpoint?: number | null;
|
||||
yes_spread?: number | null;
|
||||
no_buy?: number | null;
|
||||
no_sell?: number | null;
|
||||
no_midpoint?: number | null;
|
||||
no_spread?: number | null;
|
||||
last_trade_price?: number | null;
|
||||
liquidity?: number | null;
|
||||
volume?: number | null;
|
||||
quote_source?: string | null;
|
||||
quote_age_ms?: number | null;
|
||||
price_analysis?: MarketPriceAnalysis | null;
|
||||
sparkline?: number[];
|
||||
top_buckets?: MarketTopBucket[] | null;
|
||||
all_buckets?: MarketTopBucket[] | null;
|
||||
recent_trades?: unknown[];
|
||||
scan_scope?: "lite" | "full" | string | null;
|
||||
websocket?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
|
||||
@@ -283,18 +283,28 @@ export interface MarketScan {
|
||||
temperature_bucket: any | null;
|
||||
model_probability: number | null;
|
||||
market_price: number | null;
|
||||
midpoint?: number | null;
|
||||
spread?: number | null;
|
||||
edge_percent: number | null;
|
||||
signal_label: "BUY YES" | "BUY NO" | "MONITOR";
|
||||
confidence: "low" | "medium" | "high";
|
||||
probability_engine?: string | null;
|
||||
probability_calibration_mode?: string | null;
|
||||
yes_token: MarketToken | null;
|
||||
no_token: MarketToken | null;
|
||||
yes_buy: number | null;
|
||||
yes_sell: number | null;
|
||||
yes_midpoint?: number | null;
|
||||
yes_spread?: number | null;
|
||||
no_buy: number | null;
|
||||
no_sell: number | null;
|
||||
no_midpoint?: number | null;
|
||||
no_spread?: number | null;
|
||||
last_trade_price: number | null;
|
||||
liquidity: number | null;
|
||||
volume: number | null;
|
||||
quote_source?: string | null;
|
||||
quote_age_ms?: number | null;
|
||||
sparkline: number[];
|
||||
top_buckets?: Array<{
|
||||
label?: string | null;
|
||||
@@ -311,6 +321,7 @@ export interface MarketScan {
|
||||
is_primary?: boolean;
|
||||
}>;
|
||||
recent_trades: Trade[];
|
||||
scan_scope?: "lite" | "full" | string | null;
|
||||
websocket: any;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user