Use model cluster for tail no scan signals

This commit is contained in:
2569718930@qq.com
2026-04-24 15:12:53 +08:00
parent f331673c8d
commit 99ffa717ef
5 changed files with 263 additions and 8 deletions
@@ -342,6 +342,11 @@ export const OpportunityTable = React.memo(function OpportunityTable({
: row.model_event_probability != null
? row.model_event_probability * 100
: null;
const modelLabel = row.cluster_adjusted
? isEn
? "Model"
: "模型"
: "EMOS";
const priceLabel = side === "no" ? "NO" : isEn ? "Market" : "市场";
const edgePositive = Number(row.edge_percent || 0) >= 0;
return (
@@ -360,7 +365,7 @@ export const OpportunityTable = React.memo(function OpportunityTable({
</strong>
</span>
<span className="scan-opportunity-stat">
<small>EMOS</small>
<small>{modelLabel}</small>
<b>{formatPercent(modelProbability)}</b>
</span>
<span className="scan-opportunity-stat">
+11
View File
@@ -470,6 +470,7 @@ export interface ScanOpportunityRow {
model_probability?: number | null;
market_probability?: number | null;
model_event_probability?: number | null;
raw_model_event_probability?: number | null;
market_event_probability?: number | null;
gap?: number | null;
signed_gap?: number | null;
@@ -492,6 +493,7 @@ export interface ScanOpportunityRow {
edge_percent?: number | null;
edge_score?: number | null;
bias_score?: number | null;
consensus_score?: number | null;
distribution_bias?: DistributionBias | null;
distribution_preview?: DistributionPreviewPoint[] | null;
distribution_bias_direction?: string | null;
@@ -502,6 +504,15 @@ export interface ScanOpportunityRow {
peak_distance?: number | null;
peak_alignment_score?: number | null;
is_peak_candidate?: boolean;
is_directional_candidate?: boolean;
cluster_adjusted?: boolean;
cluster_role?: string | null;
cluster_center?: number | null;
cluster_core_low?: number | null;
cluster_core_high?: number | null;
cluster_model_count?: number | null;
cluster_deb_reference?: number | null;
cluster_median?: number | null;
window_phase?: string | null;
window_score?: number | null;
remaining_window_minutes?: number | null;
+135 -7
View File
@@ -2896,24 +2896,125 @@ class PolymarketReadOnlyLayer:
),
)
raw_model_values: List[float] = []
scan_models = (scan_context or {}).get("models")
if isinstance(scan_models, dict):
for raw_value in scan_models.values():
value = _safe_float(raw_value)
if value is not None:
raw_model_values.append(value)
raw_deb_prediction = _safe_float((scan_context or {}).get("deb_prediction"))
current_reference_raw = _safe_float(
(scan_context or {}).get("current_max_so_far")
or (scan_context or {}).get("current_temp")
)
def _median(values: List[float]) -> Optional[float]:
if not values:
return None
sorted_values = sorted(values)
middle = len(sorted_values) // 2
if len(sorted_values) % 2:
return sorted_values[middle]
return (sorted_values[middle - 1] + sorted_values[middle]) / 2.0
def _build_cluster_meta(market_unit: str) -> Dict[str, Any]:
converted_values = [
self._convert_temp_to_market_unit(
value,
source_symbol=temp_symbol,
market_unit=market_unit,
)
for value in raw_model_values
]
model_values = [value for value in converted_values if value is not None]
deb_reference = self._convert_temp_to_market_unit(
raw_deb_prediction,
source_symbol=temp_symbol,
market_unit=market_unit,
)
median_value = _median(model_values)
if deb_reference is not None and median_value is not None:
center = (deb_reference + median_value) / 2.0
elif deb_reference is not None:
center = deb_reference
elif median_value is not None:
center = median_value
elif peak_value is not None:
center = peak_value
else:
center = None
unit_step = 1.8 if str(market_unit or "").upper() == "F" else 1.0
return {
"available": center is not None and bool(model_values),
"center": center,
"core_low": center - 0.75 * unit_step if center is not None else None,
"core_high": center + 1.25 * unit_step if center is not None else None,
"low_tail": center - 0.75 * unit_step if center is not None else None,
"high_tail": center + 1.75 * unit_step if center is not None else None,
"model_count": len(model_values),
"deb_reference": deb_reference,
"median": median_value,
}
def _cluster_role_for_target(
*,
target_value: Optional[float],
cluster_meta: Dict[str, Any],
) -> str:
if not cluster_meta.get("available") or target_value is None:
return "unknown"
low_tail = _safe_float(cluster_meta.get("low_tail"))
high_tail = _safe_float(cluster_meta.get("high_tail"))
core_low = _safe_float(cluster_meta.get("core_low"))
core_high = _safe_float(cluster_meta.get("core_high"))
if low_tail is not None and target_value <= low_tail:
return "low_tail"
if high_tail is not None and target_value >= high_tail:
return "high_tail"
if (
core_low is not None
and core_high is not None
and core_low < target_value <= core_high
):
return "core"
return "shoulder"
def _row_from_entry(
entry: Dict[str, Any],
side: str,
*,
entry_index: int,
) -> Optional[Dict[str, Any]]:
model_event_probability = _clamp_probability(_safe_float(entry.get("model_event_probability")))
raw_model_event_probability = _clamp_probability(_safe_float(entry.get("model_event_probability")))
model_event_probability = raw_model_event_probability
market_event_probability = _clamp_probability(_safe_float(entry.get("market_event_probability")))
ask = _clamp_probability(_safe_float(entry.get("yes_ask") if side == "yes" else entry.get("no_ask")))
bid = _clamp_probability(_safe_float(entry.get("yes_bid") if side == "yes" else entry.get("no_bid")))
if model_event_probability is None or ask is None:
return None
market = entry["market"]
target_threshold = _safe_float(entry.get("target_threshold"))
bucket_range = entry.get("bucket_range")
market_unit = bucket_range[2] if bucket_range else ("F" if self._is_fahrenheit_symbol(temp_symbol) else "C")
cluster_meta = _build_cluster_meta(market_unit)
cluster_target = _safe_float(entry.get("bucket_temp")) or target_threshold
cluster_role = _cluster_role_for_target(
target_value=cluster_target,
cluster_meta=cluster_meta,
)
cluster_adjusted = False
if (
raw_model_event_probability is not None
and str(entry.get("market_direction") or "exact") in {"exact", "range"}
and cluster_role in {"low_tail", "high_tail"}
):
model_event_probability = _clamp_probability(raw_model_event_probability * 0.45)
cluster_adjusted = True
model_probability = (
model_event_probability
if side == "yes"
@@ -2927,10 +3028,6 @@ class PolymarketReadOnlyLayer:
if model_probability is None:
return None
market = entry["market"]
target_threshold = _safe_float(entry.get("target_threshold"))
bucket_range = entry.get("bucket_range")
market_unit = bucket_range[2] if bucket_range else ("F" if self._is_fahrenheit_symbol(temp_symbol) else "C")
current_reference = self._convert_temp_to_market_unit(
current_reference_raw,
source_symbol=temp_symbol,
@@ -2947,6 +3044,23 @@ class PolymarketReadOnlyLayer:
if entry_order is not None and peak_entry_order is not None:
peak_distance = abs(entry_order - peak_entry_order)
is_peak_candidate = peak_distance <= 1
market_structure = str(entry.get("market_direction") or "exact")
is_consensus_tail_no = (
side == "no"
and market_structure in {"exact", "range"}
and cluster_role in {"low_tail", "high_tail"}
)
is_consensus_core_yes = (
side == "yes"
and market_structure in {"exact", "range"}
and cluster_role in {"core", "shoulder", "unknown"}
and (is_peak_candidate or cluster_role == "core")
)
is_directional_candidate = (
is_consensus_tail_no
or is_consensus_core_yes
or (market_structure not in {"exact", "range"} and is_peak_candidate)
)
peak_alignment_score = 0.0
if peak_distance is None:
peak_alignment_score = 0.35
@@ -2999,13 +3113,15 @@ class PolymarketReadOnlyLayer:
min(((spread or 0.0) - 0.01) / 0.02, 1.0),
) * 15.0
edge_score = max(0.0, min(edge_percent / 12.0, 1.0))
consensus_score = 1.0 if is_directional_candidate else 0.0
final_score = 100.0 * (
0.35 * edge_score
0.32 * edge_score
+ 0.25 * bias_score
+ 0.20 * float(window_meta.get("score") or 0.0)
+ 0.10 * liquidity_score
+ 0.10 * price_usefulness_score
+ 0.08 * peak_alignment_score
+ 0.12 * consensus_score
) - spread_penalty
market_slug = str(market.get("slug") or "").strip()
target_label = str(entry.get("target_label") or "").strip()
@@ -3032,6 +3148,7 @@ class PolymarketReadOnlyLayer:
"model_probability": model_probability,
"market_probability": market_probability,
"model_event_probability": model_event_probability,
"raw_model_event_probability": raw_model_event_probability,
"market_event_probability": market_event_probability,
"gap": (
model_event_probability - market_event_probability
@@ -3070,6 +3187,7 @@ class PolymarketReadOnlyLayer:
"edge_percent": edge_percent,
"edge_score": edge_score,
"bias_score": bias_score,
"consensus_score": consensus_score,
"window_phase": window_meta.get("phase"),
"window_score": window_meta.get("score"),
"remaining_window_minutes": window_meta.get("remaining_minutes"),
@@ -3086,6 +3204,15 @@ class PolymarketReadOnlyLayer:
"peak_distance": peak_distance,
"peak_alignment_score": peak_alignment_score,
"is_peak_candidate": is_peak_candidate,
"is_directional_candidate": is_directional_candidate,
"cluster_adjusted": cluster_adjusted,
"cluster_role": cluster_role,
"cluster_center": cluster_meta.get("center"),
"cluster_core_low": cluster_meta.get("core_low"),
"cluster_core_high": cluster_meta.get("core_high"),
"cluster_model_count": cluster_meta.get("model_count"),
"cluster_deb_reference": cluster_meta.get("deb_reference"),
"cluster_median": cluster_meta.get("median"),
"current_reference": current_reference,
"gap_to_target": gap_to_target,
"touch_distance": abs(gap_to_target) if gap_to_target is not None else None,
@@ -3191,7 +3318,7 @@ class PolymarketReadOnlyLayer:
if scan_mode == "tradable":
return (
float(row.get("window_score") or 0.0) >= 0.65
and bool(row.get("is_peak_candidate"))
and bool(row.get("is_directional_candidate"))
)
if scan_mode == "early":
return str(row.get("window_phase") or "") in {"tomorrow", "week_ahead", "early_today"}
@@ -3213,6 +3340,7 @@ class PolymarketReadOnlyLayer:
]
filtered_rows.sort(
key=lambda row: (
1.0 if bool(row.get("is_directional_candidate")) else 0.0,
1.0 if bool(row.get("is_peak_candidate")) else 0.0,
float(row.get("final_score") or 0.0),
float(row.get("edge_percent") or 0.0),
+107
View File
@@ -738,6 +738,113 @@ def test_distribution_scan_tradable_prefers_peak_bucket_and_adjacent_only():
assert all((row.get("peak_distance") or 0) <= 1 for row in scan["rows"])
def test_distribution_scan_uses_model_cluster_to_prefer_tail_no_over_yes():
layer = PolymarketReadOnlyLayer()
markets = [
{
"id": "m-21",
"slug": "highest-temperature-in-paris-on-april-24-2026-21c",
"question": "Will the highest temperature in Paris be 21C on April 24?",
"active": True,
"closed": False,
"acceptingOrders": True,
"enableOrderBook": True,
"liquidityNum": 6000,
"volumeNum": 5000,
"_model_prob": 0.205,
},
{
"id": "m-22",
"slug": "highest-temperature-in-paris-on-april-24-2026-22c",
"question": "Will the highest temperature in Paris be 22C on April 24?",
"active": True,
"closed": False,
"acceptingOrders": True,
"enableOrderBook": True,
"liquidityNum": 6000,
"volumeNum": 5000,
"_model_prob": 0.34,
},
{
"id": "m-24",
"slug": "highest-temperature-in-paris-on-april-24-2026-24c",
"question": "Will the highest temperature in Paris be 24C on April 24?",
"active": True,
"closed": False,
"acceptingOrders": True,
"enableOrderBook": True,
"liquidityNum": 6000,
"volumeNum": 5000,
"_model_prob": 0.06,
},
]
token_map = {
"m-21": {"yes": "yes-21", "no": "no-21"},
"m-22": {"yes": "yes-22", "no": "no-22"},
"m-24": {"yes": "yes-24", "no": "no-24"},
}
quote_map = {
"yes-21": {"buy": 0.16, "sell": 0.14, "midpoint": 0.15, "spread": 0.02, "book_liquidity": 6000},
"no-21": {"buy": 0.85, "sell": 0.83, "midpoint": 0.84, "spread": 0.02, "book_liquidity": 6000},
"yes-22": {"buy": 0.34, "sell": 0.32, "midpoint": 0.33, "spread": 0.02, "book_liquidity": 6000},
"no-22": {"buy": 0.67, "sell": 0.65, "midpoint": 0.66, "spread": 0.02, "book_liquidity": 6000},
"yes-24": {"buy": 0.06, "sell": 0.05, "midpoint": 0.055, "spread": 0.01, "book_liquidity": 6000},
"no-24": {"buy": 0.948, "sell": 0.93, "midpoint": 0.94, "spread": 0.018, "book_liquidity": 6000},
}
layer._collect_related_temperature_markets = lambda **_kwargs: markets
layer._aggregate_distribution_probability_for_market = (
lambda market, **_kwargs: market.get("_model_prob")
)
layer._extract_market_tokens = lambda market: [
{"outcome": "Yes", "token_id": token_map[market["id"]]["yes"]},
{"outcome": "No", "token_id": token_map[market["id"]]["no"]},
]
layer._batch_get_token_market_data = (
lambda token_ids, include_books=False: {
token_id: dict(quote_map[token_id])
for token_id in token_ids
if token_id in quote_map
}
)
scan = layer._build_distribution_scan_pack(
city_key="paris",
target_date="2026-04-24",
primary_market=markets[1],
probability_distribution=[],
temp_symbol="°C",
scan_context={
"local_date": "2026-04-24",
"local_time": "08:54",
"peak": {"first_h": 14, "last_h": 16},
"current_max_so_far": 20.0,
"current_temp": 20.0,
"trend": {"recent": []},
"network_lead_signal": {},
"deb_prediction": 22.0,
"models": {
"Open-Meteo": 22.4,
"ICON": 22.4,
"GEM": 22.2,
"GDPS": 22.2,
"ECMWF": 21.2,
"JMA": 20.9,
"GFS": 20.6,
"AIFS": 22.9,
},
},
scan_filters={"limit": 10, "scan_mode": "tradable", "min_edge_pct": 2},
)
recommendations = {(row["target_value"], row["side"]) for row in scan["rows"]}
assert (21.0, "no") in recommendations
assert (24.0, "no") in recommendations
assert (21.0, "yes") not in recommendations
assert all(row.get("is_directional_candidate") for row in scan["rows"])
assert all(row.get("cluster_adjusted") for row in scan["rows"])
def test_batch_token_market_data_falls_back_to_single_fetch_when_batch_fails():
layer = PolymarketReadOnlyLayer()
layer._clob_post = lambda *_args, **_kwargs: None
+4
View File
@@ -2880,6 +2880,8 @@ def _build_city_market_scan_payload(
if isinstance(p, dict)
]
current = data.get("current") or {}
selected_deb = selected_daily.get("deb") if isinstance(selected_daily.get("deb"), dict) else {}
current_deb = data.get("deb") if isinstance(data.get("deb"), dict) else {}
scan_context = {
"local_date": data.get("local_date"),
"local_time": data.get("local_time"),
@@ -2888,6 +2890,8 @@ def _build_city_market_scan_payload(
"current_temp": current.get("temp"),
"trend": data.get("trend") or {},
"network_lead_signal": data.get("network_lead_signal") or {},
"models": model_map,
"deb_prediction": selected_deb.get("prediction") or current_deb.get("prediction"),
}
market_scan = _market_layer.build_market_scan(
city=data.get("name"),