Filter late priced market noise
This commit is contained in:
@@ -154,6 +154,91 @@ def test_build_market_opportunities_aggregates_fahrenheit_distribution():
|
||||
assert round(rows[0]["edge"], 2) == 0.46
|
||||
|
||||
|
||||
def test_build_market_opportunities_filters_late_priced_no_noise():
|
||||
event = {
|
||||
"slug": "highest-temperature-in-jeddah-on-july-3-2026",
|
||||
"markets": [
|
||||
{
|
||||
"question": "Will the highest temperature in Jeddah be 36°C on July 3?",
|
||||
"slug": "highest-temperature-in-jeddah-on-july-3-2026-36c",
|
||||
"active": True,
|
||||
"closed": False,
|
||||
"enableOrderBook": True,
|
||||
"liquidity": "154",
|
||||
"volume": "6833",
|
||||
"outcomes": '["Yes", "No"]',
|
||||
"clobTokenIds": '["yes-36", "no-36"]',
|
||||
}
|
||||
],
|
||||
}
|
||||
row = _row("jeddah")
|
||||
row["local_time"] = "19:22"
|
||||
row["current_max_so_far"] = None
|
||||
row["deb_prediction"] = 35.3
|
||||
row["model_cluster_sources"] = {"ECMWF": 34.5, "GFS": 44.0, "ICON": 35.0}
|
||||
row["distribution_full"] = [
|
||||
{"value": 36, "probability": 0.10},
|
||||
{"value": 37, "probability": 0.90},
|
||||
]
|
||||
|
||||
rows = build_market_opportunity_rows(
|
||||
[row],
|
||||
{"jeddah": event},
|
||||
{"yes-36": 0.99, "no-36": 0.01},
|
||||
max_price=0.20,
|
||||
side="both",
|
||||
positive_edge_only=True,
|
||||
min_edge=0.0,
|
||||
limit=20,
|
||||
)
|
||||
|
||||
assert rows == []
|
||||
|
||||
|
||||
def test_build_market_opportunities_keeps_late_no_when_bucket_conflicts_with_anchors():
|
||||
event = {
|
||||
"slug": "highest-temperature-in-jeddah-on-july-3-2026",
|
||||
"markets": [
|
||||
{
|
||||
"question": "Will the highest temperature in Jeddah be 36°C on July 3?",
|
||||
"slug": "highest-temperature-in-jeddah-on-july-3-2026-36c",
|
||||
"active": True,
|
||||
"closed": False,
|
||||
"enableOrderBook": True,
|
||||
"liquidity": "154",
|
||||
"volume": "6833",
|
||||
"outcomes": '["Yes", "No"]',
|
||||
"clobTokenIds": '["yes-36", "no-36"]',
|
||||
}
|
||||
],
|
||||
}
|
||||
row = _row("jeddah")
|
||||
row["local_time"] = "19:22"
|
||||
row["current_max_so_far"] = None
|
||||
row["deb_prediction"] = 31.0
|
||||
row["model_cluster_sources"] = {"ECMWF": 30.5, "GFS": 31.0, "ICON": 31.5}
|
||||
row["distribution_full"] = [
|
||||
{"value": 36, "probability": 0.10},
|
||||
{"value": 31, "probability": 0.90},
|
||||
]
|
||||
|
||||
rows = build_market_opportunity_rows(
|
||||
[row],
|
||||
{"jeddah": event},
|
||||
{"yes-36": 0.99, "no-36": 0.01},
|
||||
max_price=0.20,
|
||||
side="both",
|
||||
positive_edge_only=True,
|
||||
min_edge=0.0,
|
||||
limit=20,
|
||||
)
|
||||
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["bucket_label"] == "36°C"
|
||||
assert rows[0]["side"] == "no"
|
||||
assert rows[0]["side_probability"] == 0.90
|
||||
|
||||
|
||||
def test_ops_market_opportunities_requires_ops_admin(monkeypatch):
|
||||
def deny(_request):
|
||||
raise HTTPException(status_code=403, detail="ops only")
|
||||
|
||||
@@ -229,6 +229,60 @@ def _model_stats(row: Mapping[str, Any]) -> Tuple[Optional[float], Optional[floa
|
||||
return round(median, 1), round(max(values) - min(values), 1)
|
||||
|
||||
|
||||
def _local_hour(row: Mapping[str, Any]) -> Optional[int]:
|
||||
text = str(row.get("local_time") or "").strip()
|
||||
if not text:
|
||||
return None
|
||||
try:
|
||||
hour = int(text.split(":", 1)[0])
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return hour if 0 <= hour <= 23 else None
|
||||
|
||||
|
||||
def _option_near_value(option: Mapping[str, Any], value: Any) -> bool:
|
||||
number = _finite_number(value)
|
||||
if number is None:
|
||||
return False
|
||||
unit = str(option.get("unit") or "").upper()
|
||||
tolerance = 2.0 if "F" in unit else 1.0
|
||||
lower = _finite_number(option.get("lower"))
|
||||
upper = _finite_number(option.get("upper"))
|
||||
if lower is not None and upper is not None:
|
||||
return lower - tolerance <= number <= upper + tolerance
|
||||
if lower is not None:
|
||||
return number >= lower - tolerance
|
||||
if upper is not None:
|
||||
return number <= upper + tolerance
|
||||
return False
|
||||
|
||||
|
||||
def _is_late_priced_no_noise(
|
||||
row: Mapping[str, Any],
|
||||
option: Mapping[str, Any],
|
||||
ask_prices_by_token: Mapping[str, Optional[float]],
|
||||
tokens: Mapping[str, Optional[str]],
|
||||
*,
|
||||
no_ask: float,
|
||||
model_median: Optional[float],
|
||||
) -> bool:
|
||||
if no_ask > 0.05:
|
||||
return False
|
||||
hour = _local_hour(row)
|
||||
if hour is None or hour < 17:
|
||||
return False
|
||||
yes_token = tokens.get("yes")
|
||||
yes_ask = _finite_number(ask_prices_by_token.get(yes_token)) if yes_token else None
|
||||
if yes_ask is not None and yes_ask < 0.80:
|
||||
return False
|
||||
anchors = (
|
||||
row.get("current_max_so_far"),
|
||||
row.get("deb_prediction"),
|
||||
model_median,
|
||||
)
|
||||
return any(_option_near_value(option, anchor) for anchor in anchors)
|
||||
|
||||
|
||||
def _market_tokens(market: Mapping[str, Any]) -> Dict[str, Optional[str]]:
|
||||
outcomes = [str(item).strip().lower() for item in _json_list(market.get("outcomes"))]
|
||||
token_ids = [str(item).strip() for item in _json_list(market.get("clobTokenIds"))]
|
||||
@@ -315,6 +369,15 @@ def build_market_opportunity_rows(
|
||||
ask_number = _finite_number(ask)
|
||||
if ask_number is None or ask_number <= 0 or ask_number >= float(max_price):
|
||||
continue
|
||||
if option_side == "no" and _is_late_priced_no_noise(
|
||||
scan_row,
|
||||
option,
|
||||
ask_prices_by_token,
|
||||
tokens,
|
||||
no_ask=ask_number,
|
||||
model_median=model_median,
|
||||
):
|
||||
continue
|
||||
target_probability = (
|
||||
model_probability
|
||||
if option_side == "yes"
|
||||
|
||||
Reference in New Issue
Block a user