diff --git a/tests/test_ops_market_opportunities.py b/tests/test_ops_market_opportunities.py index 139ff580..4cc3a687 100644 --- a/tests/test_ops_market_opportunities.py +++ b/tests/test_ops_market_opportunities.py @@ -233,6 +233,57 @@ def test_build_market_opportunities_filters_late_priced_no_when_models_are_in_bu assert rows == [] +def test_build_market_opportunities_filters_priced_no_when_models_support_market_bucket(): + event = { + "slug": "highest-temperature-in-buenos-aires-on-july-3-2026", + "markets": [ + { + "question": "Will the highest temperature in Buenos Aires be 10°C on July 3?", + "slug": "highest-temperature-in-buenos-aires-on-july-3-2026-10c", + "active": True, + "closed": False, + "enableOrderBook": True, + "liquidity": "141", + "volume": "7749", + "outcomes": '["Yes", "No"]', + "clobTokenIds": '["yes-10", "no-10"]', + } + ], + } + row = _row("buenos aires") + row["local_time"] = "15:21" + row["current_max_so_far"] = None + row["deb_prediction"] = 8.7 + row["model_cluster_sources"] = { + "AI-GFS": 8.4, + "ECMWF": 8.3, + "ECMWF AIFS": 9.2, + "GFS": 9.5, + "ICON": 9.1, + "GEM": 9.0, + "GDPS": 8.9, + "JMA": 8.8, + "AROME HD": 9.5, + } + row["distribution_full"] = [ + {"value": 9, "probability": 0.27}, + {"value": 10, "probability": 0.73}, + ] + + rows = build_market_opportunity_rows( + [row], + {"buenos aires": event}, + {"yes-10": 0.94, "no-10": 0.08}, + 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", diff --git a/web/services/ops/market_opportunities.py b/web/services/ops/market_opportunities.py index d438153b..e9b7b73b 100644 --- a/web/services/ops/market_opportunities.py +++ b/web/services/ops/market_opportunities.py @@ -299,7 +299,17 @@ def _option_near_value(option: Mapping[str, Any], value: Any) -> bool: return False -def _is_late_priced_no_noise( +def _option_effective_bounds(option: Mapping[str, Any]) -> Tuple[Optional[float], Optional[float]]: + lower = _finite_number(option.get("lower")) + upper = _finite_number(option.get("upper")) + unit = str(option.get("unit") or "").upper() + half_width = 1.0 if "F" in unit and lower != upper else 0.5 + effective_lower = lower - half_width if lower is not None else None + effective_upper = upper + half_width if upper is not None else None + return effective_lower, effective_upper + + +def _is_priced_no_noise( row: Mapping[str, Any], option: Mapping[str, Any], ask_prices_by_token: Mapping[str, Optional[float]], @@ -309,26 +319,30 @@ def _is_late_priced_no_noise( model_median: Optional[float], model_relation: Optional[Mapping[str, Any]] = None, ) -> bool: - if no_ask > 0.05: - return False - hour = _local_hour(row) - if hour is None or hour < 17: + if no_ask > 0.20: 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 relation = model_relation or _model_option_relation(row, option) - outside = int(relation.get("models_above_bucket") or 0) + int(relation.get("models_below_bucket") or 0) + above = int(relation.get("models_above_bucket") or 0) + below = int(relation.get("models_below_bucket") or 0) inside = int(relation.get("models_in_bucket") or 0) - if outside > inside: + if above > max(inside, below): 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) + if any(_option_near_value(option, anchor) for anchor in anchors): + return True + effective_lower, _effective_upper = _option_effective_bounds(option) + model_max = _finite_number(relation.get("model_max")) + if above == 0 and inside > 0 and effective_lower is not None and model_max is not None: + return model_max >= effective_lower + return False def _market_tokens(market: Mapping[str, Any]) -> Dict[str, Optional[str]]: @@ -418,7 +432,7 @@ 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( + if option_side == "no" and _is_priced_no_noise( scan_row, option, ask_prices_by_token,