Files
PolyWeather/tests/test_ops_market_opportunities.py
2026-07-04 02:32:22 +08:00

516 lines
17 KiB
Python

import pytest
from fastapi import HTTPException
import web.services.ops.market_opportunities as market_opportunities
from web.services.ops.market_opportunities import (
build_market_opportunity_rows,
get_ops_market_opportunities,
parse_market_option_from_question,
)
def _row(city="shanghai", symbol="°C"):
return {
"city": city,
"city_display_name": city.title(),
"local_date": "2026-07-03",
"local_time": "18:10",
"temp_symbol": symbol,
"current_max_so_far": 31.0,
"deb_prediction": 32.2,
"model_cluster_sources": {"ECMWF": 32.0, "GFS": 33.0, "ICON": 31.0},
"distribution_full": [
{"value": 31, "probability": 0.10},
{"value": 32, "probability": 0.41},
{"value": 33, "probability": 0.46},
{"value": 34, "probability": 0.03},
],
"trading_region_label_zh": "东亚",
}
def test_parse_market_option_supports_celsius_single_temperature():
option = parse_market_option_from_question(
"Will the highest temperature in Shanghai be 33°C on July 3?",
"°C",
)
assert option["label"] == "33°C"
assert option["lower"] == 33
assert option["upper"] == 33
def test_parse_market_option_supports_fahrenheit_two_degree_range():
option = parse_market_option_from_question(
"Will the highest temperature in Houston be between 94-95°F on July 3?",
"°F",
)
assert option["label"] == "94-95°F"
assert option["lower"] == 94
assert option["upper"] == 95
def test_build_market_opportunities_scans_yes_and_no_low_price_edges():
event = {
"slug": "highest-temperature-in-shanghai-on-july-3-2026",
"title": "Highest temperature in Shanghai on July 3?",
"markets": [
{
"question": "Will the highest temperature in Shanghai be 33°C on July 3?",
"slug": "highest-temperature-in-shanghai-on-july-3-2026-33c",
"active": True,
"closed": False,
"enableOrderBook": True,
"liquidity": "47000",
"volume": "13000",
"outcomes": '["Yes", "No"]',
"clobTokenIds": '["yes-33", "no-33"]',
},
{
"question": "Will the highest temperature in Shanghai be 31°C on July 3?",
"slug": "highest-temperature-in-shanghai-on-july-3-2026-31c",
"active": True,
"closed": False,
"enableOrderBook": True,
"liquidity": "27000",
"volume": "6000",
"outcomes": '["Yes", "No"]',
"clobTokenIds": '["yes-31", "no-31"]',
},
],
}
rows = build_market_opportunity_rows(
[_row()],
{"shanghai": event},
{
"yes-33": 0.18,
"no-33": 0.62,
"yes-31": 0.12,
"no-31": 0.18,
},
max_price=0.20,
side="both",
positive_edge_only=True,
min_edge=0.0,
limit=20,
)
yes = next(row for row in rows if row["bucket_label"] == "33°C" and row["side"] == "yes")
no = next(row for row in rows if row["bucket_label"] == "31°C" and row["side"] == "no")
assert yes["model_probability"] == 0.46
assert yes["yes_probability"] == 0.46
assert yes["side_probability"] == 0.46
assert yes["model_cluster_sources"] == {"ECMWF": 32.0, "GFS": 33.0, "ICON": 31.0}
assert yes["model_min"] == 31.0
assert yes["model_max"] == 33.0
assert yes["models_below_bucket"] == 2
assert yes["models_in_bucket"] == 1
assert yes["models_above_bucket"] == 0
assert yes["models_above_deb"] == 1
assert yes["ask_price"] == 0.18
assert round(yes["edge"], 2) == 0.28
assert no["model_probability"] == 0.10
assert no["yes_probability"] == 0.10
assert no["side_probability"] == 0.90
assert no["ask_price"] == 0.18
assert round(no["edge"], 2) == 0.72
assert all(row["ask_price"] < 0.20 for row in rows)
assert rows[0]["edge"] >= rows[-1]["edge"]
def test_build_market_opportunities_includes_ask_equal_to_max_price():
event = {
"slug": "highest-temperature-in-shanghai-on-july-3-2026",
"markets": [
{
"question": "Will the highest temperature in Shanghai be 33°C on July 3?",
"slug": "highest-temperature-in-shanghai-on-july-3-2026-33c",
"active": True,
"closed": False,
"enableOrderBook": True,
"outcomes": '["Yes", "No"]',
"clobTokenIds": '["yes-33", "no-33"]',
}
],
}
rows = build_market_opportunity_rows(
[_row()],
{"shanghai": event},
{"yes-33": 0.18, "no-33": 0.82},
max_price=0.18,
side="yes",
positive_edge_only=True,
min_edge=0.0,
limit=20,
)
assert len(rows) == 1
assert rows[0]["ask_price"] == 0.18
def test_build_market_opportunities_aggregates_fahrenheit_distribution():
event = {
"slug": "highest-temperature-in-houston-on-july-3-2026",
"markets": [
{
"question": "Will the highest temperature in Houston be between 94-95°F on July 3?",
"slug": "highest-temperature-in-houston-on-july-3-2026-94-95f",
"active": True,
"closed": False,
"enableOrderBook": True,
"liquidity": "10000",
"volume": "2000",
"outcomes": '["Yes", "No"]',
"clobTokenIds": '["yes-94", "no-94"]',
}
],
}
row = _row("houston", "°F")
row["distribution_full"] = [
{"value": 94, "probability": 0.40},
{"value": 95, "probability": 0.25},
{"value": 96, "probability": 0.10},
]
rows = build_market_opportunity_rows(
[row],
{"houston": event},
{"yes-94": 0.19, "no-94": 0.81},
max_price=0.20,
side="yes",
positive_edge_only=True,
min_edge=0.0,
limit=20,
)
assert rows[0]["bucket_label"] == "94-95°F"
assert rows[0]["model_probability"] == 0.65
assert round(rows[0]["edge"], 2) == 0.46
def test_build_market_opportunities_filters_late_priced_no_when_models_are_in_bucket():
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.8
row["model_cluster_sources"] = {"ECMWF": 35.7, "GFS": 36.1, "ICON": 36.2}
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_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",
"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_build_market_opportunities_keeps_late_no_when_models_are_above_bucket():
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": 37.0, "GFS": 38.5, "ICON": 36.8}
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 len(rows) == 1
assert rows[0]["models_above_bucket"] == 3
assert rows[0]["models_in_bucket"] == 0
assert rows[0]["side"] == "no"
def test_ops_market_opportunities_requires_ops_admin(monkeypatch):
def deny(_request):
raise HTTPException(status_code=403, detail="ops only")
monkeypatch.setattr(market_opportunities, "_require_ops", deny)
with pytest.raises(HTTPException) as exc:
get_ops_market_opportunities(object())
assert exc.value.status_code == 403
def test_ops_market_opportunities_degrades_when_quotes_fail(monkeypatch):
monkeypatch.setattr(market_opportunities, "_require_ops", lambda _request: {"email": "ops@example.com"})
monkeypatch.setattr(
market_opportunities,
"build_scan_terminal_payload",
lambda *_args, **_kwargs: {"rows": [_row()]},
)
def fail_quotes(*_args, **_kwargs):
raise RuntimeError("polymarket down")
monkeypatch.setattr(market_opportunities, "_collect_events_and_prices", fail_quotes)
payload = get_ops_market_opportunities(object())
assert payload["summary"]["quote_status"] == "unavailable"
assert payload["summary"]["error"] == "polymarket down"
assert payload["rows"] == []
def test_ops_market_opportunities_reuses_prewarmed_terminal_snapshot(monkeypatch):
captured = {}
monkeypatch.setattr(market_opportunities, "_require_ops", lambda _request: {"email": "ops@example.com"})
def fake_scan(filters, *, force_refresh=False):
captured["filters"] = filters
captured["force_refresh"] = force_refresh
return {"rows": [_row()]}
monkeypatch.setattr(market_opportunities, "build_scan_terminal_payload", fake_scan)
monkeypatch.setattr(
market_opportunities,
"_collect_events_and_prices",
lambda rows, _scanner: ({row["city"]: None for row in rows}, {}, "ready"),
)
payload = get_ops_market_opportunities(object(), max_price=0.90, min_edge=0.0)
assert payload["summary"]["scanned_city_count"] == 1
assert captured["force_refresh"] is False
assert captured["filters"]["limit"] == 180
assert captured["filters"]["min_edge_pct"] == 2
assert captured["filters"]["min_liquidity"] == 500
def test_ops_market_opportunities_force_refreshes_empty_terminal_snapshot(monkeypatch):
calls = []
monkeypatch.setattr(market_opportunities, "_require_ops", lambda _request: {"email": "ops@example.com"})
def fake_scan(_filters, *, force_refresh=False):
calls.append(force_refresh)
return {"rows": [_row()]} if force_refresh else {"rows": [], "status": "ready"}
monkeypatch.setattr(market_opportunities, "build_scan_terminal_payload", fake_scan)
monkeypatch.setattr(
market_opportunities,
"_collect_events_and_prices",
lambda rows, _scanner: ({row["city"]: None for row in rows}, {}, "ready"),
)
payload = get_ops_market_opportunities(object(), max_price=0.90, min_edge=0.0)
assert calls == [False, True]
assert payload["summary"]["scanned_city_count"] == 1
assert payload["summary"]["quote_status"] == "ready"
assert payload["summary"]["scan_source"] == "force_refresh"
def test_ops_market_opportunities_reports_scan_empty_when_refresh_is_empty(monkeypatch):
monkeypatch.setattr(market_opportunities, "_require_ops", lambda _request: {"email": "ops@example.com"})
monkeypatch.setattr(
market_opportunities,
"build_scan_terminal_payload",
lambda *_args, **_kwargs: {"rows": [], "status": "ready"},
)
payload = get_ops_market_opportunities(object(), max_price=0.90, min_edge=0.0)
assert payload["summary"]["scanned_city_count"] == 0
assert payload["summary"]["quote_status"] == "scan_empty"
assert payload["summary"]["matched_event_count"] == 0
assert payload["rows"] == []
def test_collect_events_and_prices_uses_gamma_hint_prices_without_clob_fanout():
class FakeScanner:
def fetch_event(self, _slug):
return {
"slug": "highest-temperature-in-shanghai-on-july-3-2026",
"markets": [
{
"question": "Will the highest temperature in Shanghai be 33°C on July 3?",
"outcomes": '["Yes", "No"]',
"clobTokenIds": '["yes-33", "no-33"]',
"outcomePrices": '["0.12", "0.88"]',
}
],
}
def fetch_ask_price(self, _token_id):
raise AssertionError("CLOB should not be called when Gamma outcomePrices are present")
events, prices, status = market_opportunities._collect_events_and_prices(
[_row()],
FakeScanner(),
)
assert status == "ready"
assert "shanghai" in events
assert prices == {"yes-33": 0.12, "no-33": 0.88}
def test_collect_events_and_prices_stops_on_time_budget():
class SlowScanner:
def fetch_event(self, _slug):
raise AssertionError("time budget should stop external event fetching")
def fetch_ask_price(self, _token_id):
raise AssertionError("time budget should stop external price fetching")
events, prices, status = market_opportunities._collect_events_and_prices(
[_row()],
SlowScanner(),
time_budget_sec=0,
)
assert status == "partial"
assert events == {}
assert prices == {}