feat: implement dashboard types, Polymarket data collection, and decision utilities for AI-driven city weather analysis
This commit is contained in:
@@ -35,6 +35,12 @@ export function normalizeMarketProbability(value: unknown) {
|
|||||||
return numeric;
|
return numeric;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeQuotePrice(value: unknown) {
|
||||||
|
const normalized = normalizeMarketProbability(value);
|
||||||
|
if (normalized == null || normalized <= 0) return null;
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
export function formatMarketPercent(value: number | null, digits = 1) {
|
export function formatMarketPercent(value: number | null, digits = 1) {
|
||||||
if (value == null || !Number.isFinite(value)) return "--";
|
if (value == null || !Number.isFinite(value)) return "--";
|
||||||
return `${(value * 100).toFixed(digits)}%`;
|
return `${(value * 100).toFixed(digits)}%`;
|
||||||
@@ -42,6 +48,7 @@ export function formatMarketPercent(value: number | null, digits = 1) {
|
|||||||
|
|
||||||
export function formatMarketCents(value: number | null) {
|
export function formatMarketCents(value: number | null) {
|
||||||
if (value == null || !Number.isFinite(value)) return "--";
|
if (value == null || !Number.isFinite(value)) return "--";
|
||||||
|
if (value > 0 && value < 0.01) return "<1¢";
|
||||||
return `${Math.round(value * 100)}¢`;
|
return `${Math.round(value * 100)}¢`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +73,9 @@ export function normalizeMarketComparableTemp(
|
|||||||
|
|
||||||
export function getMarketBucketLabel(bucket?: MarketTopBucket | null, tempSymbol = "°C") {
|
export function getMarketBucketLabel(bucket?: MarketTopBucket | null, tempSymbol = "°C") {
|
||||||
const direct = String(bucket?.label || "").trim();
|
const direct = String(bucket?.label || "").trim();
|
||||||
if (direct) return direct;
|
if (direct && /[°]?[CF]\b|\d+\s*[+-]?$/i.test(direct) && !/[�。紊]/.test(direct)) {
|
||||||
|
return direct.replace(/\bC\b/g, "°C").replace(/\bF\b/g, "°F");
|
||||||
|
}
|
||||||
const value = bucket?.temp ?? bucket?.value ?? bucket?.lower ?? null;
|
const value = bucket?.temp ?? bucket?.value ?? bucket?.lower ?? null;
|
||||||
const numeric = Number(value);
|
const numeric = Number(value);
|
||||||
if (Number.isFinite(numeric)) {
|
if (Number.isFinite(numeric)) {
|
||||||
@@ -78,6 +87,53 @@ export function getMarketBucketLabel(bucket?: MarketTopBucket | null, tempSymbol
|
|||||||
return "--";
|
return "--";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBucketAnchor(bucket: MarketTopBucket) {
|
||||||
|
const anchor = Number(bucket.temp ?? bucket.value ?? bucket.lower);
|
||||||
|
return Number.isFinite(anchor) ? anchor : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBucketModelProbability(bucket?: MarketTopBucket | null) {
|
||||||
|
const model = normalizeMarketProbability(bucket?.model_probability);
|
||||||
|
const probability = normalizeMarketProbability(bucket?.probability);
|
||||||
|
const market = normalizeMarketProbability(bucket?.market_price);
|
||||||
|
// Some persisted market_scan payloads from older builds overwrote bucket
|
||||||
|
// probability with the market price. Treat an exact price clone as missing
|
||||||
|
// model probability so the caller can fall back to scan.model_probability.
|
||||||
|
if (
|
||||||
|
model != null &&
|
||||||
|
market != null &&
|
||||||
|
Math.abs(model - market) <= 0.000_001
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
probability != null &&
|
||||||
|
market != null &&
|
||||||
|
Math.abs(probability - market) <= 0.000_001
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return model ?? probability;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMarketSelectedBucket(scan: MarketScan | null | undefined): MarketTopBucket | null {
|
||||||
|
const selected = scan?.temperature_bucket;
|
||||||
|
if (!selected) return null;
|
||||||
|
const value = Number(selected.value);
|
||||||
|
return {
|
||||||
|
label: selected.label || selected.bucket || selected.range || null,
|
||||||
|
value: Number.isFinite(value) ? value : null,
|
||||||
|
temp: Number.isFinite(value) ? value : null,
|
||||||
|
unit: selected.unit || null,
|
||||||
|
probability: selected.probability ?? scan?.model_probability ?? null,
|
||||||
|
model_probability: selected.probability ?? scan?.model_probability ?? null,
|
||||||
|
market_price: scan?.market_price ?? null,
|
||||||
|
yes_buy: scan?.yes_buy ?? null,
|
||||||
|
yes_sell: scan?.yes_sell ?? null,
|
||||||
|
slug: scan?.selected_slug ?? scan?.primary_market?.slug ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function pickMarketBucketForWeatherCenter(
|
export function pickMarketBucketForWeatherCenter(
|
||||||
scan: MarketScan | null | undefined,
|
scan: MarketScan | null | undefined,
|
||||||
expectedHigh: number | null,
|
expectedHigh: number | null,
|
||||||
@@ -90,8 +146,18 @@ export function pickMarketBucketForWeatherCenter(
|
|||||||
? scan?.top_buckets
|
? scan?.top_buckets
|
||||||
: []
|
: []
|
||||||
) as MarketTopBucket[];
|
) as MarketTopBucket[];
|
||||||
|
const selectedBucket = getMarketSelectedBucket(scan);
|
||||||
|
const isReasonableFallback = (bucket: MarketTopBucket | null) => {
|
||||||
|
if (!bucket) return false;
|
||||||
|
const comparable = normalizeMarketComparableTemp(expectedHigh, tempSymbol, bucket);
|
||||||
|
const anchor = getBucketAnchor(bucket);
|
||||||
|
if (comparable == null || anchor == null) return false;
|
||||||
|
const unit = String(bucket.unit || "").toUpperCase();
|
||||||
|
const maxReasonableDelta = unit === "F" ? 16 : 8;
|
||||||
|
return Math.abs(anchor - comparable) <= maxReasonableDelta;
|
||||||
|
};
|
||||||
if (!buckets.length || expectedHigh == null || !Number.isFinite(expectedHigh)) {
|
if (!buckets.length || expectedHigh == null || !Number.isFinite(expectedHigh)) {
|
||||||
return null;
|
return selectedBucket;
|
||||||
}
|
}
|
||||||
|
|
||||||
let nearest: MarketTopBucket | null = null;
|
let nearest: MarketTopBucket | null = null;
|
||||||
@@ -111,15 +177,26 @@ export function pickMarketBucketForWeatherCenter(
|
|||||||
) {
|
) {
|
||||||
return bucket;
|
return bucket;
|
||||||
}
|
}
|
||||||
const anchor = Number(bucket.temp ?? bucket.value ?? bucket.lower);
|
const anchor = getBucketAnchor(bucket);
|
||||||
if (!Number.isFinite(anchor)) continue;
|
if (anchor == null) continue;
|
||||||
const delta = Math.abs(anchor - comparable);
|
const delta = Math.abs(anchor - comparable);
|
||||||
if (delta < nearestDelta) {
|
if (delta < nearestDelta) {
|
||||||
nearest = bucket;
|
nearest = bucket;
|
||||||
nearestDelta = delta;
|
nearestDelta = delta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nearest;
|
if (!nearest) return isReasonableFallback(selectedBucket) ? selectedBucket : null;
|
||||||
|
const comparable = normalizeMarketComparableTemp(expectedHigh, tempSymbol, nearest);
|
||||||
|
const unit = String(nearest.unit || "").toUpperCase();
|
||||||
|
const maxReasonableDelta = unit === "F" ? 16 : 8;
|
||||||
|
if (
|
||||||
|
comparable != null &&
|
||||||
|
Number.isFinite(nearestDelta) &&
|
||||||
|
nearestDelta <= maxReasonableDelta
|
||||||
|
) {
|
||||||
|
return nearest;
|
||||||
|
}
|
||||||
|
return isReasonableFallback(selectedBucket) ? selectedBucket : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildMarketDecisionView({
|
export function buildMarketDecisionView({
|
||||||
@@ -171,18 +248,42 @@ export function buildMarketDecisionView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bucket = pickMarketBucketForWeatherCenter(marketScan, expectedHigh, tempSymbol);
|
const bucket = pickMarketBucketForWeatherCenter(marketScan, expectedHigh, tempSymbol);
|
||||||
const bucketProbability = normalizeMarketProbability(bucket?.probability);
|
if (!bucket) {
|
||||||
|
return {
|
||||||
|
bucketLabel: "--",
|
||||||
|
confidence: marketScan.confidence || "--",
|
||||||
|
edgeText: "--",
|
||||||
|
impliedText: formatMarketPercent(
|
||||||
|
normalizeMarketProbability(marketScan.market_price) ??
|
||||||
|
normalizeMarketProbability(marketScan.midpoint) ??
|
||||||
|
normalizeMarketProbability(marketScan.yes_midpoint),
|
||||||
|
),
|
||||||
|
marketUrl: marketScan.market_url || marketScan.primary_market_url || null,
|
||||||
|
modelText: formatMarketPercent(normalizeMarketProbability(marketScan.model_probability)),
|
||||||
|
priceText: formatMarketCents(normalizeQuotePrice(marketScan.yes_buy)),
|
||||||
|
reason: isEn
|
||||||
|
? "A market was found, but its temperature bucket does not match today’s expected high closely enough, so edge is withheld."
|
||||||
|
: "已找到市场,但温度桶与今日预计高点不够匹配,暂不计算概率差。",
|
||||||
|
status: "ready",
|
||||||
|
title: isEn ? "Market bucket needs rematch" : "市场温度桶需重新匹配",
|
||||||
|
tone: "watch",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const bucketProbability = getBucketModelProbability(bucket);
|
||||||
const scanProbability = normalizeMarketProbability(marketScan.model_probability);
|
const scanProbability = normalizeMarketProbability(marketScan.model_probability);
|
||||||
const modelProbability = bucketProbability ?? scanProbability;
|
const modelProbability = bucketProbability ?? scanProbability;
|
||||||
const yesBuy =
|
const yesBuy =
|
||||||
normalizeMarketProbability(bucket?.yes_buy) ??
|
normalizeQuotePrice(bucket?.yes_buy) ??
|
||||||
normalizeMarketProbability(bucket?.market_price) ??
|
normalizeQuotePrice(marketScan.yes_buy);
|
||||||
normalizeMarketProbability(marketScan.yes_buy) ??
|
|
||||||
normalizeMarketProbability(marketScan.market_price);
|
|
||||||
const yesSell =
|
const yesSell =
|
||||||
normalizeMarketProbability(bucket?.yes_sell) ??
|
normalizeQuotePrice(bucket?.yes_sell) ??
|
||||||
normalizeMarketProbability(marketScan.yes_sell);
|
normalizeQuotePrice(marketScan.yes_sell);
|
||||||
const implied = yesBuy ?? yesSell ?? null;
|
const marketMid =
|
||||||
|
normalizeMarketProbability(bucket?.market_price) ??
|
||||||
|
normalizeMarketProbability(marketScan.market_price) ??
|
||||||
|
normalizeMarketProbability(marketScan.midpoint) ??
|
||||||
|
normalizeMarketProbability(marketScan.yes_midpoint);
|
||||||
|
const implied = marketMid ?? yesBuy ?? yesSell ?? null;
|
||||||
const edge =
|
const edge =
|
||||||
modelProbability != null && implied != null ? modelProbability - implied : null;
|
modelProbability != null && implied != null ? modelProbability - implied : null;
|
||||||
const tone =
|
const tone =
|
||||||
@@ -216,9 +317,10 @@ export function buildMarketDecisionView({
|
|||||||
edgeText: formatSignedMarketPercent(edge),
|
edgeText: formatSignedMarketPercent(edge),
|
||||||
impliedText: formatMarketPercent(implied),
|
impliedText: formatMarketPercent(implied),
|
||||||
marketUrl:
|
marketUrl:
|
||||||
bucket?.slug
|
bucket?.market_url ||
|
||||||
|
(bucket?.slug
|
||||||
? `https://polymarket.com/market/${bucket.slug}`
|
? `https://polymarket.com/market/${bucket.slug}`
|
||||||
: marketScan.market_url || marketScan.primary_market_url || null,
|
: marketScan.market_url || marketScan.primary_market_url || null),
|
||||||
modelText: formatMarketPercent(modelProbability),
|
modelText: formatMarketPercent(modelProbability),
|
||||||
priceText: formatMarketCents(yesBuy),
|
priceText: formatMarketCents(yesBuy),
|
||||||
reason:
|
reason:
|
||||||
|
|||||||
@@ -195,6 +195,50 @@ export function normalizeCityKey(value?: string | null) {
|
|||||||
.replace(/[\s_-]+/g, "");
|
.replace(/[\s_-]+/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOpportunityCardKey(row: ScanOpportunityRow) {
|
||||||
|
const city =
|
||||||
|
normalizeCityKey(row.city) ||
|
||||||
|
normalizeCityKey(row.city_display_name) ||
|
||||||
|
normalizeCityKey(row.display_name);
|
||||||
|
const date = String(row.selected_date || row.local_date || "").trim();
|
||||||
|
if (city || date) {
|
||||||
|
return `${city || row.id}:${date || "date-unknown"}`;
|
||||||
|
}
|
||||||
|
return row.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOpportunitySortScore(row: ScanOpportunityRow) {
|
||||||
|
return Number(row.final_score || 0) * 1000 + Number(row.edge_percent || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedupeOpportunityCards(rows: ScanOpportunityRow[]) {
|
||||||
|
const bestByCard = new Map<string, ScanOpportunityRow>();
|
||||||
|
for (const row of rows) {
|
||||||
|
const key = getOpportunityCardKey(row);
|
||||||
|
const current = bestByCard.get(key);
|
||||||
|
if (!current || getOpportunitySortScore(row) > getOpportunitySortScore(current)) {
|
||||||
|
bestByCard.set(key, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...bestByCard.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeUniqueOpportunityRows(
|
||||||
|
candidates: ScanOpportunityRow[],
|
||||||
|
usedCardKeys: Set<string>,
|
||||||
|
limit: number,
|
||||||
|
) {
|
||||||
|
const picked: ScanOpportunityRow[] = [];
|
||||||
|
for (const row of candidates) {
|
||||||
|
const key = getOpportunityCardKey(row);
|
||||||
|
if (usedCardKeys.has(key)) continue;
|
||||||
|
usedCardKeys.add(key);
|
||||||
|
picked.push(row);
|
||||||
|
if (picked.length >= limit) break;
|
||||||
|
}
|
||||||
|
return picked;
|
||||||
|
}
|
||||||
|
|
||||||
export function prettifyCityName(value?: string | null) {
|
export function prettifyCityName(value?: string | null) {
|
||||||
return String(value || "")
|
return String(value || "")
|
||||||
.trim()
|
.trim()
|
||||||
@@ -323,25 +367,32 @@ export function getRowDecisionMeta(row: ScanOpportunityRow, locale = "zh-CN") {
|
|||||||
|
|
||||||
export function pickOpportunitySections(rows: ScanOpportunityRow[], locale = "zh-CN") {
|
export function pickOpportunitySections(rows: ScanOpportunityRow[], locale = "zh-CN") {
|
||||||
const isEn = locale === "en-US";
|
const isEn = locale === "en-US";
|
||||||
const top = [...rows]
|
const usedCardKeys = new Set<string>();
|
||||||
|
const uniqueRows = dedupeOpportunityCards(rows);
|
||||||
|
const top = takeUniqueOpportunityRows([...uniqueRows]
|
||||||
.sort((left, right) => {
|
.sort((left, right) => {
|
||||||
const scoreDelta = Number(right.final_score || 0) - Number(left.final_score || 0);
|
const scoreDelta = Number(right.final_score || 0) - Number(left.final_score || 0);
|
||||||
if (scoreDelta !== 0) return scoreDelta;
|
if (scoreDelta !== 0) return scoreDelta;
|
||||||
return Number(right.edge_percent || 0) - Number(left.edge_percent || 0);
|
return Number(right.edge_percent || 0) - Number(left.edge_percent || 0);
|
||||||
})
|
}), usedCardKeys, 4);
|
||||||
.slice(0, 4);
|
const peak = takeUniqueOpportunityRows(
|
||||||
const peak = rows
|
uniqueRows.filter((row) => {
|
||||||
.filter((row) => {
|
|
||||||
const meta = getPeakCountdownMeta(row, locale);
|
const meta = getPeakCountdownMeta(row, locale);
|
||||||
return meta.key === "active" || meta.key === "next";
|
return meta.key === "active" || meta.key === "next";
|
||||||
})
|
}),
|
||||||
.slice(0, 4);
|
usedCardKeys,
|
||||||
const model = rows
|
4,
|
||||||
.filter((row) => Number(row.cluster_model_count || 0) >= 4 || Number(row.consensus_score || 0) >= 0.65)
|
);
|
||||||
.slice(0, 4);
|
const model = takeUniqueOpportunityRows(
|
||||||
const risk = rows
|
uniqueRows.filter((row) => Number(row.cluster_model_count || 0) >= 4 || Number(row.consensus_score || 0) >= 0.65),
|
||||||
.filter((row) => row.risk_level === "high" || ["veto", "downgrade"].includes(String(row.v4_metar_decision || row.ai_decision || "").toLowerCase()))
|
usedCardKeys,
|
||||||
.slice(0, 4);
|
4,
|
||||||
|
);
|
||||||
|
const risk = takeUniqueOpportunityRows(
|
||||||
|
uniqueRows.filter((row) => row.risk_level === "high" || ["veto", "downgrade"].includes(String(row.v4_metar_decision || row.ai_decision || "").toLowerCase())),
|
||||||
|
usedCardKeys,
|
||||||
|
4,
|
||||||
|
);
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: "top",
|
key: "top",
|
||||||
|
|||||||
@@ -99,6 +99,13 @@ export function useAiCityForecast({
|
|||||||
let buffer = "";
|
let buffer = "";
|
||||||
let rawStream = "";
|
let rawStream = "";
|
||||||
let finalPayload: AiCityForecastPayload | null = null;
|
let finalPayload: AiCityForecastPayload | null = null;
|
||||||
|
let latestReadableText = "";
|
||||||
|
const rememberReadableText = (value?: string | null) => {
|
||||||
|
const text = String(value || "").trim();
|
||||||
|
if (text) {
|
||||||
|
latestReadableText = text;
|
||||||
|
}
|
||||||
|
};
|
||||||
const handleBlock = (block: string) => {
|
const handleBlock = (block: string) => {
|
||||||
const message = parseSseBlock(block);
|
const message = parseSseBlock(block);
|
||||||
if (!message || !message.data || typeof message.data !== "object") {
|
if (!message || !message.data || typeof message.data !== "object") {
|
||||||
@@ -111,6 +118,7 @@ export function useAiCityForecast({
|
|||||||
locale === "en-US" ? data.message_en || "" : data.message_zh || "",
|
locale === "en-US" ? data.message_en || "" : data.message_zh || "",
|
||||||
).trim() || String(data.message || "").trim();
|
).trim() || String(data.message || "").trim();
|
||||||
if (progressText && !cancelled) {
|
if (progressText && !cancelled) {
|
||||||
|
rememberReadableText(progressText);
|
||||||
setAiForecast((current) =>
|
setAiForecast((current) =>
|
||||||
current.status === "loading"
|
current.status === "loading"
|
||||||
? { ...current, streamText: current.streamText || progressText }
|
? { ...current, streamText: current.streamText || progressText }
|
||||||
@@ -132,6 +140,7 @@ export function useAiCityForecast({
|
|||||||
).trim() ||
|
).trim() ||
|
||||||
String(data.final_judgment_zh || data.final_judgment_en || "").trim();
|
String(data.final_judgment_zh || data.final_judgment_en || "").trim();
|
||||||
if (previewText && !cancelled) {
|
if (previewText && !cancelled) {
|
||||||
|
rememberReadableText(previewText);
|
||||||
setAiForecast((current) =>
|
setAiForecast((current) =>
|
||||||
current.status === "loading"
|
current.status === "loading"
|
||||||
? {
|
? {
|
||||||
@@ -153,6 +162,7 @@ export function useAiCityForecast({
|
|||||||
? "AI has started streaming; parsing the METAR read field…"
|
? "AI has started streaming; parsing the METAR read field…"
|
||||||
: "AI 已开始流式输出,正在解析机场报文字段…"
|
: "AI 已开始流式输出,正在解析机场报文字段…"
|
||||||
: "");
|
: "");
|
||||||
|
rememberReadableText(airportRead || streamingText);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setAiForecast((current) =>
|
setAiForecast((current) =>
|
||||||
current.status === "loading"
|
current.status === "loading"
|
||||||
@@ -184,7 +194,45 @@ export function useAiCityForecast({
|
|||||||
handleBlock(buffer);
|
handleBlock(buffer);
|
||||||
}
|
}
|
||||||
if (!finalPayload) {
|
if (!finalPayload) {
|
||||||
throw new Error("AI stream ended before final payload");
|
const fallbackText =
|
||||||
|
extractStreamingAirportRead(rawStream, locale) ||
|
||||||
|
latestReadableText ||
|
||||||
|
(isEn
|
||||||
|
? "The AI airport read stream was interrupted after partial output."
|
||||||
|
: "AI 机场报文解读已输出部分内容,但最终载荷未返回。");
|
||||||
|
const retryHint = isEn
|
||||||
|
? "The streaming connection ended before the final structured payload. The partial airport read above is preserved; refresh once if you need the full JSON-backed conclusion."
|
||||||
|
: "流式连接在最终结构化载荷返回前结束。上方已保留已输出的机场报文解读;如需完整 JSON 结论可刷新一次。";
|
||||||
|
return {
|
||||||
|
city_forecast: {
|
||||||
|
confidence: "low",
|
||||||
|
final_judgment_en: isEn
|
||||||
|
? fallbackText
|
||||||
|
: "Partial AI airport read was preserved after the stream ended early.",
|
||||||
|
final_judgment_zh: isEn
|
||||||
|
? "AI 机场报文解读已保留部分输出,但流式连接提前结束。"
|
||||||
|
: fallbackText,
|
||||||
|
metar_read_en: isEn ? fallbackText : "",
|
||||||
|
metar_read_zh: isEn ? "" : fallbackText,
|
||||||
|
model_cluster_note_en: "",
|
||||||
|
model_cluster_note_zh: "",
|
||||||
|
predicted_max: null,
|
||||||
|
range_high: null,
|
||||||
|
range_low: null,
|
||||||
|
reasoning_en: retryHint,
|
||||||
|
reasoning_zh: retryHint,
|
||||||
|
risks_en: isEn ? [retryHint] : [],
|
||||||
|
risks_zh: isEn ? [] : [retryHint],
|
||||||
|
unit: detail?.temp_symbol || "°C",
|
||||||
|
},
|
||||||
|
raw_reason: "AI stream ended before final payload",
|
||||||
|
reason: retryHint,
|
||||||
|
reason_en: isEn
|
||||||
|
? retryHint
|
||||||
|
: "AI stream ended before the final payload; partial text was preserved.",
|
||||||
|
reason_zh: isEn ? "AI 流在最终载荷前结束;已保留部分文本。" : retryHint,
|
||||||
|
status: "partial_stream",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return finalPayload;
|
return finalPayload;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -29,19 +29,25 @@ export function useChart<TType extends ChartType>(
|
|||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
|
|
||||||
const config = createConfig();
|
const config = createConfig();
|
||||||
if (chartRef.current) {
|
const nextType = (config as { type?: ChartType }).type;
|
||||||
chartRef.current.destroy();
|
const currentType = chartRef.current
|
||||||
chartRef.current = null;
|
? (chartRef.current.config as { type?: ChartType }).type
|
||||||
|
: null;
|
||||||
|
if (chartRef.current && currentType === nextType) {
|
||||||
|
chartRef.current.data = config.data as ChartInstance<TType>["data"];
|
||||||
|
chartRef.current.options =
|
||||||
|
(config.options || {}) as ChartInstance<TType>["options"];
|
||||||
|
chartRef.current.update("none");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chartRef.current?.destroy();
|
||||||
chartRef.current = new Chart(canvas, config);
|
chartRef.current = new Chart(canvas, config);
|
||||||
};
|
};
|
||||||
|
|
||||||
void setupChart();
|
void setupChart();
|
||||||
return () => {
|
return () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
chartRef.current?.destroy();
|
|
||||||
chartRef.current = null;
|
|
||||||
};
|
};
|
||||||
}, dependencies);
|
}, dependencies);
|
||||||
|
|
||||||
|
|||||||
@@ -315,7 +315,9 @@ export interface MarketTopBucket {
|
|||||||
upper?: number | null;
|
upper?: number | null;
|
||||||
unit?: string | null;
|
unit?: string | null;
|
||||||
probability?: number | null;
|
probability?: number | null;
|
||||||
|
model_probability?: number | null;
|
||||||
market_price?: number | null;
|
market_price?: number | null;
|
||||||
|
edge_percent?: number | null;
|
||||||
yes_buy?: number | null;
|
yes_buy?: number | null;
|
||||||
yes_sell?: number | null;
|
yes_sell?: number | null;
|
||||||
no_buy?: number | null;
|
no_buy?: number | null;
|
||||||
@@ -325,6 +327,7 @@ export interface MarketTopBucket {
|
|||||||
quote_source?: string | null;
|
quote_source?: string | null;
|
||||||
quote_age_ms?: number | null;
|
quote_age_ms?: number | null;
|
||||||
slug?: string | null;
|
slug?: string | null;
|
||||||
|
market_url?: string | null;
|
||||||
question?: string | null;
|
question?: string | null;
|
||||||
is_primary?: boolean;
|
is_primary?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -901,7 +901,11 @@ class PolymarketReadOnlyLayer:
|
|||||||
if reference_price is not None:
|
if reference_price is not None:
|
||||||
reference_price = max(0.0, min(1.0, float(reference_price)))
|
reference_price = max(0.0, min(1.0, float(reference_price)))
|
||||||
bucket["market_price"] = reference_price
|
bucket["market_price"] = reference_price
|
||||||
bucket["probability"] = reference_price
|
# Keep model probability separate from market-implied price.
|
||||||
|
# Older code overwrote ``probability`` with the quote, which made
|
||||||
|
# downstream UI compare a market price against itself or display
|
||||||
|
# stale bucket probabilities as weather probabilities.
|
||||||
|
bucket.setdefault("model_probability", bucket.get("probability"))
|
||||||
if yes_prices.get("quote_source"):
|
if yes_prices.get("quote_source"):
|
||||||
bucket["quote_source"] = yes_prices.get("quote_source")
|
bucket["quote_source"] = yes_prices.get("quote_source")
|
||||||
if yes_prices.get("quote_age_ms") is not None:
|
if yes_prices.get("quote_age_ms") is not None:
|
||||||
|
|||||||
Reference in New Issue
Block a user