From bbf2db4da13a9f976b72fc9395792e1b5c400d77 Mon Sep 17 00:00:00 2001
From: "2569718930@qq.com" <2569718930@qq.com>
Date: Mon, 29 Jun 2026 22:27:22 +0800
Subject: [PATCH] Show market buckets with model probability
---
.../scan-terminal/ModelSummaryDashboard.tsx | 37 ++++++-----
.../__tests__/modelSummaryDashboard.test.ts | 34 ++++++++--
frontend/lib/model-summary.ts | 66 ++++++++++++++++++-
3 files changed, 116 insertions(+), 21 deletions(-)
diff --git a/frontend/components/dashboard/scan-terminal/ModelSummaryDashboard.tsx b/frontend/components/dashboard/scan-terminal/ModelSummaryDashboard.tsx
index 80360ffa..c0d992ad 100644
--- a/frontend/components/dashboard/scan-terminal/ModelSummaryDashboard.tsx
+++ b/frontend/components/dashboard/scan-terminal/ModelSummaryDashboard.tsx
@@ -32,7 +32,7 @@ const SUMMARY_TEXT = {
region: { en: "Region", zh: "区域" },
localTime: { en: "Local Time", zh: "当地时间" },
gaussianMu: { en: "Gaussian μ", zh: "高斯 μ" },
- probabilityDistribution: { en: "Probability Distribution", zh: "概率分布" },
+ marketMatch: { en: "Market Match", zh: "市场匹配" },
median: { en: "Median", zh: "模型中位数" },
spread: { en: "Spread", zh: "分歧范围" },
empty: { en: "No model summary rows match the current filters.", zh: "当前筛选下没有模型汇总数据。" },
@@ -100,9 +100,11 @@ function FilterToggle({
function ModelSummaryRowView({
row,
nowMs,
+ isEn,
}: {
row: ModelSummaryRow;
nowMs: number | null;
+ isEn: boolean;
}) {
return (
@@ -136,24 +138,26 @@ function ModelSummaryRowView({
|
- {row.probabilityBuckets.length ? (
+ {row.marketMatches.length ? (
- {row.probabilityBuckets.map((bucket) => {
- const isTopBucket = bucket.key === row.topProbabilityBucketKey;
+ {row.marketMatches.map((match) => {
return (
- = 0.2
+ ? "border-emerald-200 bg-emerald-50 text-emerald-800"
+ : "border-slate-200 bg-slate-50 text-slate-600",
)}
- title={bucket.label}
+ title={`${match.label} model ${formatModelSummaryProbability(match.modelProbability)}`}
>
- {bucket.label}
- {formatModelSummaryProbability(bucket.probability)}
-
+ {match.label}
+ {isEn ? "M" : "模"} {formatModelSummaryProbability(match.modelProbability)}
+
);
})}
@@ -315,8 +319,8 @@ export function ModelSummaryDashboard({
|
{copy("gaussianMu", isEn)}
|
-
- {copy("probabilityDistribution", isEn)}
+ |
+ {copy("marketMatch", isEn)}
|
@@ -326,6 +330,7 @@ export function ModelSummaryDashboard({
key={row.cityKey}
row={row}
nowMs={nowMs}
+ isEn={isEn}
/>
))}
diff --git a/frontend/components/dashboard/scan-terminal/__tests__/modelSummaryDashboard.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/modelSummaryDashboard.test.ts
index 7a3a8670..d2f1f161 100644
--- a/frontend/components/dashboard/scan-terminal/__tests__/modelSummaryDashboard.test.ts
+++ b/frontend/components/dashboard/scan-terminal/__tests__/modelSummaryDashboard.test.ts
@@ -59,6 +59,16 @@ export function runTests() {
{ value: 33, model_probability: 0.31, range: "[32.5~33.5)" },
],
probability_engine: "legacy",
+ all_buckets: [
+ {
+ label: "31.5-32.5°C",
+ model_probability: 0.42,
+ },
+ {
+ label: "33.5-34.5°C",
+ model_probability: 0.08,
+ },
+ ],
},
{
city: "madrid",
@@ -131,6 +141,18 @@ export function runTests() {
parisRow.topProbabilityBucketKey === "31.5-32.5°C",
"model summary should map probability buckets and identify the top bucket",
);
+ assert(parisRow.marketMatches.length === 2, "model summary should keep every Polymarket tradable bucket");
+ assert(
+ parisRow.marketMatches[0].label === "31.5-32.5°C" &&
+ parisRow.marketMatches[0].modelProbability === 0.42 &&
+ parisRow.marketMatches[0].marketUrl === null,
+ "model summary should expose model probability for market-matched buckets without requiring market price",
+ );
+ assert(
+ parisRow.marketMatches[1].label === "33.5-34.5°C" &&
+ parisRow.marketMatches[1].modelProbability === 0.08,
+ "model summary should keep low-probability tradable buckets for manual NO review",
+ );
assert(formatModelSummaryProbability(null) === "—", "missing probability should render as an em dash");
assert(formatModelSummaryProbability(0.424) === "42%", "probability buckets should render as rounded percentages");
assert(formatModelSummaryTemp(null, "°C") === "—", "missing model temperatures should render as an em dash");
@@ -199,10 +221,14 @@ export function runTests() {
modelSummarySource.includes("hasModelSummaryForecastData") &&
modelSummarySource.includes("Gaussian μ") &&
modelSummarySource.includes("高斯 μ") &&
- modelSummarySource.includes("Probability Distribution") &&
- modelSummarySource.includes("概率分布") &&
- modelSummarySource.includes("topProbabilityBucketKey") &&
- modelSummarySource.includes("probabilityBuckets.map") &&
+ modelSummarySource.includes("Market Match") &&
+ modelSummarySource.includes("市场匹配") &&
+ modelSummarySource.includes("marketMatches.map") &&
+ !modelSummarySource.includes("formatModelSummaryEdge") &&
+ !modelSummarySource.includes("marketProbability") &&
+ !modelSummarySource.includes("edgePercent") &&
+ !modelSummarySource.includes("Probability Distribution") &&
+ !modelSummarySource.includes("概率分布") &&
!modelSummarySource.includes("probabilityColumns") &&
modelSummarySource.includes("min-w-[96px]") &&
modelSummarySource.includes("Local Time") &&
diff --git a/frontend/lib/model-summary.ts b/frontend/lib/model-summary.ts
index a958437d..bf367fbe 100644
--- a/frontend/lib/model-summary.ts
+++ b/frontend/lib/model-summary.ts
@@ -29,6 +29,13 @@ export type ModelSummaryProbabilityBucket = {
probability: number;
};
+export type ModelSummaryMarketMatch = {
+ key: string;
+ label: string;
+ modelProbability: number | null;
+ marketUrl: string | null;
+};
+
export type ModelSummaryRow = {
cityKey: string;
cityName: string;
@@ -47,6 +54,7 @@ export type ModelSummaryRow = {
gaussianMu: number | null;
probabilityEngine: string | null;
topProbabilityBucketKey: string | null;
+ marketMatches: ModelSummaryMarketMatch[];
searchText: string;
};
@@ -156,6 +164,54 @@ function weightedProbabilityMu(buckets: ModelSummaryProbabilityBucket[]) {
return roundToOneDecimal(weightedValue / totalProbability);
}
+function normalizeProbability(value: unknown) {
+ const numericValue = finiteNumber(value);
+ if (numericValue == null) return null;
+ return numericValue > 1 ? numericValue / 100 : numericValue;
+}
+
+function marketBucketLabel(bucket: Record, tempSymbol: string) {
+ const textLabel = String(bucket.label || bucket.bucket || bucket.range || "").trim();
+ if (textLabel) return textLabel;
+ const lower = finiteNumber(bucket.lower);
+ const upper = finiteNumber(bucket.upper);
+ if (lower != null && upper != null && upper > lower) {
+ return probabilityBucketLabel(lower, upper, String(bucket.unit || tempSymbol || "°C"));
+ }
+ const value = finiteNumber(bucket.value ?? bucket.temp ?? bucket.temperature);
+ return value == null ? "—" : `${formatBucketBound(value)}${bucket.unit || tempSymbol || "°C"}`;
+}
+
+function buildMarketMatches(row: ScanOpportunityRow): ModelSummaryMarketMatch[] {
+ const marketRow = row as ScanOpportunityRow & {
+ all_buckets?: Array> | null;
+ top_buckets?: Array> | null;
+ };
+ const sourceBuckets = (
+ Array.isArray(marketRow.all_buckets) && marketRow.all_buckets.length
+ ? marketRow.all_buckets
+ : Array.isArray(marketRow.top_buckets)
+ ? marketRow.top_buckets
+ : []
+ ) as Array>;
+ const tempSymbol = row.temp_symbol || "°C";
+
+ return sourceBuckets
+ .map((bucket, index) => {
+ const label = marketBucketLabel(bucket, tempSymbol);
+ const modelProbability = normalizeProbability(bucket.model_probability ?? bucket.probability);
+ return {
+ key: `${label}-${index}`,
+ label,
+ modelProbability,
+ marketUrl: typeof bucket.market_url === "string" ? bucket.market_url : null,
+ };
+ })
+ .sort((a, b) => {
+ return (b.modelProbability ?? -1) - (a.modelProbability ?? -1);
+ });
+}
+
function normalizeCityKey(row: ScanOpportunityRow, index: number) {
const rawKey = row.city || row.city_display_name || row.display_name || `row-${index}`;
return String(rawKey).trim().toLowerCase();
@@ -259,6 +315,13 @@ export function buildModelSummaryRows(
const probabilitySearchText = probabilityBuckets
.map((bucket) => `${bucket.label} ${formatModelSummaryProbability(bucket.probability)}`)
.join(" ");
+ const marketMatches = buildMarketMatches(row);
+ const marketSearchText = marketMatches
+ .map(
+ (match) =>
+ `${match.label} ${formatModelSummaryProbability(match.modelProbability)}`,
+ )
+ .join(" ");
byCity.set(cityKey, {
cityKey,
@@ -278,8 +341,9 @@ export function buildModelSummaryRows(
gaussianMu: weightedProbabilityMu(probabilityBuckets),
probabilityEngine: row.probability_engine || (probabilityBuckets.length ? "legacy" : null),
topProbabilityBucketKey: topProbabilityBucket?.key || null,
+ marketMatches,
searchText:
- `${cityName} ${row.city || ""} ${region.labelEn} ${region.labelZh} ${modelSearchText} ${probabilitySearchText}`.toLowerCase(),
+ `${cityName} ${row.city || ""} ${region.labelEn} ${region.labelZh} ${modelSearchText} ${probabilitySearchText} ${marketSearchText}`.toLowerCase(),
});
});