diff --git a/mock_data/polymarket/up_down_15min_BTC.json b/mock_data/polymarket/up_down_15min_BTC.json
new file mode 100644
index 0000000..c1ffc9c
--- /dev/null
+++ b/mock_data/polymarket/up_down_15min_BTC.json
@@ -0,0 +1,20 @@
+{
+ "slug": "btc-updown-15m-1772204400",
+ "start_price": 68176.05,
+ "current_time": "2026-02-27T15:04:21+00:00",
+ "current_price": 67936.60,
+ "current_outcome": "Down",
+ "synth_probability_up": 0.0969,
+ "synth_outcome": "Down",
+ "polymarket_probability_up": 0.475,
+ "polymarket_outcome": "Down",
+ "event_start_time": "2026-02-27T15:00:00+00:00",
+ "event_end_time": "2026-02-27T15:15:00+00:00",
+ "best_bid_price": 0.47,
+ "best_ask_price": 0.48,
+ "best_bid_size": 218.0,
+ "best_ask_size": 445.47,
+ "polymarket_last_trade_time": "2026-02-27T14:59:57+00:00",
+ "polymarket_last_trade_price": 0.4582,
+ "polymarket_last_trade_outcome": "Down"
+}
diff --git a/mock_data/polymarket/up_down_15min_ETH.json b/mock_data/polymarket/up_down_15min_ETH.json
new file mode 100644
index 0000000..e7aefca
--- /dev/null
+++ b/mock_data/polymarket/up_down_15min_ETH.json
@@ -0,0 +1,20 @@
+{
+ "slug": "eth-updown-15m-1772204400",
+ "start_price": 3245.12,
+ "current_time": "2026-02-27T15:04:21+00:00",
+ "current_price": 3238.45,
+ "current_outcome": "Down",
+ "synth_probability_up": 0.3521,
+ "synth_outcome": "Down",
+ "polymarket_probability_up": 0.42,
+ "polymarket_outcome": "Down",
+ "event_start_time": "2026-02-27T15:00:00+00:00",
+ "event_end_time": "2026-02-27T15:15:00+00:00",
+ "best_bid_price": 0.41,
+ "best_ask_price": 0.43,
+ "best_bid_size": 156.0,
+ "best_ask_size": 312.5,
+ "polymarket_last_trade_time": "2026-02-27T14:58:32+00:00",
+ "polymarket_last_trade_price": 0.42,
+ "polymarket_last_trade_outcome": "Down"
+}
diff --git a/mock_data/polymarket/up_down_15min_SOL.json b/mock_data/polymarket/up_down_15min_SOL.json
new file mode 100644
index 0000000..1b6eda6
--- /dev/null
+++ b/mock_data/polymarket/up_down_15min_SOL.json
@@ -0,0 +1,20 @@
+{
+ "slug": "sol-updown-15m-1772204400",
+ "start_price": 142.85,
+ "current_time": "2026-02-27T15:04:21+00:00",
+ "current_price": 141.92,
+ "current_outcome": "Down",
+ "synth_probability_up": 0.2845,
+ "synth_outcome": "Down",
+ "polymarket_probability_up": 0.38,
+ "polymarket_outcome": "Down",
+ "event_start_time": "2026-02-27T15:00:00+00:00",
+ "event_end_time": "2026-02-27T15:15:00+00:00",
+ "best_bid_price": 0.37,
+ "best_ask_price": 0.39,
+ "best_bid_size": 89.0,
+ "best_ask_size": 201.3,
+ "polymarket_last_trade_time": "2026-02-27T14:57:18+00:00",
+ "polymarket_last_trade_price": 0.38,
+ "polymarket_last_trade_outcome": "Down"
+}
diff --git a/mock_data/polymarket/up_down_5min_BTC.json b/mock_data/polymarket/up_down_5min_BTC.json
new file mode 100644
index 0000000..abc6dec
--- /dev/null
+++ b/mock_data/polymarket/up_down_5min_BTC.json
@@ -0,0 +1,20 @@
+{
+ "slug": "btc-updown-5m-1772205000",
+ "start_price": 67980.25,
+ "current_time": "2026-02-27T15:16:21+00:00",
+ "current_price": 67945.80,
+ "current_outcome": "Down",
+ "synth_probability_up": 0.4215,
+ "synth_outcome": "Down",
+ "polymarket_probability_up": 0.485,
+ "polymarket_outcome": "Down",
+ "event_start_time": "2026-02-27T15:15:00+00:00",
+ "event_end_time": "2026-02-27T15:20:00+00:00",
+ "best_bid_price": 0.48,
+ "best_ask_price": 0.49,
+ "best_bid_size": 125.0,
+ "best_ask_size": 267.3,
+ "polymarket_last_trade_time": "2026-02-27T15:15:42+00:00",
+ "polymarket_last_trade_price": 0.485,
+ "polymarket_last_trade_outcome": "Down"
+}
diff --git a/mock_data/polymarket/up_down_5min_ETH.json b/mock_data/polymarket/up_down_5min_ETH.json
new file mode 100644
index 0000000..78a2d0c
--- /dev/null
+++ b/mock_data/polymarket/up_down_5min_ETH.json
@@ -0,0 +1,20 @@
+{
+ "slug": "eth-updown-5m-1772205000",
+ "start_price": 3240.50,
+ "current_time": "2026-02-27T15:16:21+00:00",
+ "current_price": 3242.15,
+ "current_outcome": "Up",
+ "synth_probability_up": 0.5832,
+ "synth_outcome": "Up",
+ "polymarket_probability_up": 0.52,
+ "polymarket_outcome": "Up",
+ "event_start_time": "2026-02-27T15:15:00+00:00",
+ "event_end_time": "2026-02-27T15:20:00+00:00",
+ "best_bid_price": 0.51,
+ "best_ask_price": 0.53,
+ "best_bid_size": 98.0,
+ "best_ask_size": 184.2,
+ "polymarket_last_trade_time": "2026-02-27T15:15:38+00:00",
+ "polymarket_last_trade_price": 0.52,
+ "polymarket_last_trade_outcome": "Up"
+}
diff --git a/mock_data/polymarket/up_down_5min_SOL.json b/mock_data/polymarket/up_down_5min_SOL.json
new file mode 100644
index 0000000..99493f5
--- /dev/null
+++ b/mock_data/polymarket/up_down_5min_SOL.json
@@ -0,0 +1,20 @@
+{
+ "slug": "sol-updown-5m-1772205000",
+ "start_price": 141.95,
+ "current_time": "2026-02-27T15:16:21+00:00",
+ "current_price": 142.08,
+ "current_outcome": "Up",
+ "synth_probability_up": 0.5124,
+ "synth_outcome": "Up",
+ "polymarket_probability_up": 0.48,
+ "polymarket_outcome": "Down",
+ "event_start_time": "2026-02-27T15:15:00+00:00",
+ "event_end_time": "2026-02-27T15:20:00+00:00",
+ "best_bid_price": 0.47,
+ "best_ask_price": 0.49,
+ "best_bid_size": 72.0,
+ "best_ask_size": 145.8,
+ "polymarket_last_trade_time": "2026-02-27T15:15:29+00:00",
+ "polymarket_last_trade_price": 0.48,
+ "polymarket_last_trade_outcome": "Down"
+}
diff --git a/mock_data/polymarket/up_down_daily.json b/mock_data/polymarket/up_down_daily_BTC.json
similarity index 100%
rename from mock_data/polymarket/up_down_daily.json
rename to mock_data/polymarket/up_down_daily_BTC.json
diff --git a/mock_data/polymarket/up_down_daily_ETH.json b/mock_data/polymarket/up_down_daily_ETH.json
new file mode 100644
index 0000000..40ee07d
--- /dev/null
+++ b/mock_data/polymarket/up_down_daily_ETH.json
@@ -0,0 +1,21 @@
+{
+ "slug": "ethereum-up-or-down-on-february-26",
+ "start_price": 3280.45,
+ "current_time": "2026-02-25T23:45:17.526062+00:00",
+ "current_price": 3245.82,
+ "current_outcome": "Down",
+ "synth_probability_up": 0.3825,
+ "synth_outcome": "Down",
+ "polymarket_probability_up": 0.415,
+ "polymarket_outcome": "Down",
+ "event_start_time": "2026-02-25T17:00:00+00:00",
+ "event_end_time": "2026-02-26T17:00:00+00:00",
+ "event_creation_time": "2026-02-24T17:02:40.731726+00:00",
+ "best_bid_price": 0.41,
+ "best_ask_price": 0.42,
+ "best_bid_size": 890.0,
+ "best_ask_size": 1250.0,
+ "polymarket_last_trade_time": "2026-02-25T23:42:15+00:00",
+ "polymarket_last_trade_price": 0.415,
+ "polymarket_last_trade_outcome": "Down"
+}
diff --git a/mock_data/polymarket/up_down_daily_SOL.json b/mock_data/polymarket/up_down_daily_SOL.json
new file mode 100644
index 0000000..851bc2f
--- /dev/null
+++ b/mock_data/polymarket/up_down_daily_SOL.json
@@ -0,0 +1,21 @@
+{
+ "slug": "solana-up-or-down-on-february-26",
+ "start_price": 145.62,
+ "current_time": "2026-02-25T23:45:17.526062+00:00",
+ "current_price": 142.18,
+ "current_outcome": "Down",
+ "synth_probability_up": 0.3142,
+ "synth_outcome": "Down",
+ "polymarket_probability_up": 0.38,
+ "polymarket_outcome": "Down",
+ "event_start_time": "2026-02-25T17:00:00+00:00",
+ "event_end_time": "2026-02-26T17:00:00+00:00",
+ "event_creation_time": "2026-02-24T17:02:40.731726+00:00",
+ "best_bid_price": 0.37,
+ "best_ask_price": 0.39,
+ "best_bid_size": 520.0,
+ "best_ask_size": 780.0,
+ "polymarket_last_trade_time": "2026-02-25T23:40:32+00:00",
+ "polymarket_last_trade_price": 0.38,
+ "polymarket_last_trade_outcome": "Down"
+}
diff --git a/mock_data/polymarket/up_down_hourly.json b/mock_data/polymarket/up_down_hourly_BTC.json
similarity index 100%
rename from mock_data/polymarket/up_down_hourly.json
rename to mock_data/polymarket/up_down_hourly_BTC.json
diff --git a/mock_data/polymarket/up_down_hourly_ETH.json b/mock_data/polymarket/up_down_hourly_ETH.json
new file mode 100644
index 0000000..dc7f442
--- /dev/null
+++ b/mock_data/polymarket/up_down_hourly_ETH.json
@@ -0,0 +1,21 @@
+{
+ "slug": "ethereum-up-or-down-february-25-6pm-et",
+ "start_price": 3252.80,
+ "current_time": "2026-02-25T23:45:23.586996+00:00",
+ "current_price": 3245.82,
+ "current_outcome": "Down",
+ "synth_probability_up": 0.2845,
+ "synth_outcome": "Down",
+ "polymarket_probability_up": 0.32,
+ "polymarket_outcome": "Down",
+ "event_start_time": "2026-02-25T23:00:00+00:00",
+ "event_end_time": "2026-02-26T00:00:00+00:00",
+ "event_creation_time": "2026-02-23T23:02:47.889891+00:00",
+ "best_bid_price": 0.31,
+ "best_ask_price": 0.33,
+ "best_bid_size": 145.0,
+ "best_ask_size": 210.5,
+ "polymarket_last_trade_time": "2026-02-25T23:43:18+00:00",
+ "polymarket_last_trade_price": 0.32,
+ "polymarket_last_trade_outcome": "Down"
+}
diff --git a/mock_data/polymarket/up_down_hourly_SOL.json b/mock_data/polymarket/up_down_hourly_SOL.json
new file mode 100644
index 0000000..d659e35
--- /dev/null
+++ b/mock_data/polymarket/up_down_hourly_SOL.json
@@ -0,0 +1,21 @@
+{
+ "slug": "solana-up-or-down-february-25-6pm-et",
+ "start_price": 143.25,
+ "current_time": "2026-02-25T23:45:23.586996+00:00",
+ "current_price": 142.18,
+ "current_outcome": "Down",
+ "synth_probability_up": 0.3521,
+ "synth_outcome": "Down",
+ "polymarket_probability_up": 0.40,
+ "polymarket_outcome": "Down",
+ "event_start_time": "2026-02-25T23:00:00+00:00",
+ "event_end_time": "2026-02-26T00:00:00+00:00",
+ "event_creation_time": "2026-02-23T23:02:47.889891+00:00",
+ "best_bid_price": 0.39,
+ "best_ask_price": 0.41,
+ "best_bid_size": 98.0,
+ "best_ask_size": 156.2,
+ "polymarket_last_trade_time": "2026-02-25T23:41:45+00:00",
+ "polymarket_last_trade_price": 0.40,
+ "polymarket_last_trade_outcome": "Down"
+}
diff --git a/synth_client/client.py b/synth_client/client.py
index ab68980..7954178 100644
--- a/synth_client/client.py
+++ b/synth_client/client.py
@@ -219,28 +219,68 @@ def get_lp_probabilities(self, asset: str) -> dict:
# ─── Polymarket ──────────────────────────────────────────────────
- def get_polymarket_daily(self) -> dict:
+ def get_polymarket_daily(self, asset: str = "BTC") -> dict:
"""
Get daily up/down comparison between Synth forecasts and Polymarket prices.
+ Args:
+ asset: Asset symbol (BTC, ETH, SOL). Default: BTC
+
Returns:
- Dict with Synth vs Polymarket probability comparison (BTC)
+ Dict with Synth vs Polymarket probability comparison
"""
return self._get(
"/insights/polymarket/up-down/daily",
- ["polymarket", "up_down_daily.json"],
+ ["polymarket", f"up_down_daily_{asset}.json"],
+ params={"asset": asset},
)
- def get_polymarket_hourly(self) -> dict:
+ def get_polymarket_hourly(self, asset: str = "BTC") -> dict:
"""
Get hourly up/down comparison between Synth forecasts and Polymarket prices.
+ Args:
+ asset: Asset symbol (BTC, ETH, SOL). Default: BTC
+
Returns:
- Dict with Synth vs Polymarket probability comparison (BTC)
+ Dict with Synth vs Polymarket probability comparison
"""
return self._get(
"/insights/polymarket/up-down/hourly",
- ["polymarket", "up_down_hourly.json"],
+ ["polymarket", f"up_down_hourly_{asset}.json"],
+ params={"asset": asset},
+ )
+
+ def get_polymarket_15min(self, asset: str = "BTC") -> dict:
+ """
+ Get 15-minute up/down comparison between Synth forecasts and Polymarket prices.
+
+ Args:
+ asset: Asset symbol (BTC, ETH, SOL). Default: BTC
+
+ Returns:
+ Dict with Synth vs Polymarket probability comparison
+ """
+ return self._get(
+ "/insights/polymarket/up-down/15min",
+ ["polymarket", f"up_down_15min_{asset}.json"],
+ params={"asset": asset},
+ )
+
+ def get_polymarket_5min(self, asset: str = "BTC") -> dict:
+ """
+ Get 5-minute up/down comparison between Synth forecasts and Polymarket prices.
+
+ Args:
+ asset: Asset symbol (BTC, ETH, SOL). Default: BTC
+
+ Returns:
+ Dict with Synth vs Polymarket probability comparison
+ """
+ return self._get(
+ "/insights/polymarket/up-down/5min",
+ ["polymarket", f"up_down_5min_{asset}.json"],
+ params={"asset": asset},
)
def get_polymarket_range(self) -> list:
diff --git a/tools/synth-overlay/README.md b/tools/synth-overlay/README.md
index 00f71ff..f1e015e 100644
--- a/tools/synth-overlay/README.md
+++ b/tools/synth-overlay/README.md
@@ -20,10 +20,12 @@ Chrome extension that uses Chrome's **native Side Panel** to show Synth market c
## Synth API usage
-- `get_polymarket_daily()` — daily up/down (24h) Synth vs Polymarket.
-- `get_polymarket_hourly()` — hourly up/down (1h).
+- `get_polymarket_daily(asset)` — daily up/down (24h) Synth vs Polymarket.
+- `get_polymarket_hourly(asset)` — hourly up/down (1h).
+- `get_polymarket_15min(asset)` — 15-minute up/down (15m).
+- `get_polymarket_5min(asset)` — 5-minute up/down (5m).
- `get_polymarket_range()` — range brackets with synth vs polymarket probability per bracket.
-- `get_prediction_percentiles(asset, horizon)` — used for confidence scoring (forecast spread) and optional bias in explanations; wired for both up/down and range.
+- `get_prediction_percentiles(asset, horizon)` — used for confidence scoring (forecast spread) and optional bias in explanations.
## Run locally
@@ -41,8 +43,9 @@ Chrome extension that uses Chrome's **native Side Panel** to show Synth market c
You should see JSON with `"signal"`, `"edge_pct"`, etc. If you see `"error"` or 404, the slug is not supported for the current mock/API.
2. **Open the exact URL** in Chrome (with the extension loaded from `extension/`):
- - Daily (mock): `https://polymarket.com/event/bitcoin-up-or-down-on-february-26`
- - Hourly (mock): `https://polymarket.com/event/bitcoin-up-or-down-february-25-6pm-et`
+ - Daily (BTC): `https://polymarket.com/event/bitcoin-up-or-down-on-february-26`
+ - Hourly (ETH): `https://polymarket.com/event/ethereum-up-or-down-february-25-6pm-et`
+ - 15-Min (SOL): `https://polymarket.com/event/sol-updown-15m-1772204400`
- The side panel requests the slug from the page and fetches Synth data from the local API. If API returns 200, panel fields populate.
3. **Interaction:**
diff --git a/tools/synth-overlay/analyzer.py b/tools/synth-overlay/analyzer.py
index d4134e6..2e897b4 100644
--- a/tools/synth-overlay/analyzer.py
+++ b/tools/synth-overlay/analyzer.py
@@ -247,6 +247,91 @@ def _build_range_invalidation(self, bracket: dict, signal: str) -> str:
)
return f"No meaningful edge on {title} — bracket is fairly priced."
+ def analyze_single_horizon(self, data: dict, horizon: str = "24h") -> AnalysisResult:
+ """Analyze a single up/down market with optional reference-horizon context."""
+ edge = self._extract_edge(data, horizon)
+ strength = strength_from_edge(edge.edge_pct)
+
+ ref_edge = None
+ if self._hourly:
+ try:
+ ref_edge = self._extract_edge(self._hourly, "ref")
+ except (KeyError, ValueError):
+ pass
+
+ if ref_edge and signals_conflict(edge.signal, ref_edge.signal):
+ strength = "none"
+
+ spread_1h = self._percentile_spread(self._pct_1h)
+ spread_24h = self._percentile_spread(self._pct_24h)
+ confidence = self.compute_confidence(spread_1h, spread_24h)
+
+ high_uncertainty = any(
+ s is not None and s > 0.05 for s in (spread_1h, spread_24h)
+ )
+ conflict = ref_edge is not None and signals_conflict(edge.signal, ref_edge.signal)
+ no_trade = conflict or strength == "none" or high_uncertainty
+
+ bias = self._directional_bias(self._pct_24h) or self._directional_bias(self._pct_1h)
+ explanation = self._build_single_explanation(edge, ref_edge, confidence, horizon)
+ invalidation = self._build_short_invalidation(edge, bias, horizon)
+
+ return AnalysisResult(
+ primary=edge,
+ secondary=ref_edge,
+ strength=strength,
+ confidence_score=confidence,
+ no_trade=no_trade,
+ explanation=explanation,
+ invalidation=invalidation,
+ )
+
+ def _build_single_explanation(
+ self, edge: HorizonEdge, ref_edge: HorizonEdge | None, confidence: float, horizon: str
+ ) -> str:
+ """Build explanation for single-horizon analysis with optional reference context."""
+ parts = []
+ if edge.signal == "fair":
+ parts.append(f"Synth and Polymarket agree closely on this {horizon} market.")
+ else:
+ direction = "higher" if edge.edge_pct > 0 else "lower"
+ parts.append(
+ f"Synth forecasts {direction} probability than Polymarket "
+ f"by {abs(edge.edge_pct):.1f}pp on the {horizon} horizon."
+ )
+ if ref_edge and ref_edge.signal != "fair":
+ if signals_conflict(edge.signal, ref_edge.signal):
+ parts.append("Reference horizon disagrees — signals conflict.")
+ else:
+ parts.append(
+ f"Reference horizon confirms with {abs(ref_edge.edge_pct):.1f}pp edge."
+ )
+ if confidence >= 0.7:
+ parts.append("Forecast distribution is narrow — high confidence.")
+ elif confidence <= 0.3:
+ parts.append("Forecast distribution is wide — low confidence, treat with caution.")
+ return " ".join(parts)
+
+ def _build_short_invalidation(self, edge: HorizonEdge, bias: float | None, horizon: str) -> str:
+ """Build invalidation text appropriate for short-term markets."""
+ parts = []
+ if edge.signal == "underpriced":
+ parts.append(
+ f"This {horizon} edge invalidates if price reverses, "
+ f"pushing Synth probability below market."
+ )
+ elif edge.signal == "overpriced":
+ parts.append(
+ f"This {horizon} edge invalidates if price moves up, "
+ f"pushing Synth probability above market."
+ )
+ else:
+ parts.append("No meaningful edge to invalidate — market is fairly priced.")
+ if bias is not None and abs(bias) > 0.02:
+ direction = "upward" if bias > 0 else "downward"
+ parts.append(f"Synth median shows a {direction} bias of {abs(bias)*100:.1f}%.")
+ return " ".join(parts)
+
def analyze(self, primary_horizon: str = "24h") -> AnalysisResult:
if not self._daily or not self._hourly:
raise ValueError("Both daily and hourly data required for analysis")
diff --git a/tools/synth-overlay/extension/sidepanel.html b/tools/synth-overlay/extension/sidepanel.html
index 561b3c0..eb2dc2b 100644
--- a/tools/synth-overlay/extension/sidepanel.html
+++ b/tools/synth-overlay/extension/sidepanel.html
@@ -25,11 +25,16 @@
Synth
Signal
- 1 h—
- 24 h—
+ Primary—
+ Reference—
Strength—
+
+
Confidence
diff --git a/tools/synth-overlay/extension/sidepanel.js b/tools/synth-overlay/extension/sidepanel.js
index fae0a45..37682a5 100644
--- a/tools/synth-overlay/extension/sidepanel.js
+++ b/tools/synth-overlay/extension/sidepanel.js
@@ -7,9 +7,14 @@ const els = {
synthUp: document.getElementById("synthUp"),
synthDown: document.getElementById("synthDown"),
edgeValue: document.getElementById("edgeValue"),
- signal1h: document.getElementById("signal1h"),
- signal24h: document.getElementById("signal24h"),
+ horizonLabel: document.getElementById("horizonLabel"),
+ signalPrimary: document.getElementById("signalPrimary"),
+ refRow: document.getElementById("refRow"),
+ refLabel: document.getElementById("refLabel"),
+ signalRef: document.getElementById("signalRef"),
strength: document.getElementById("strength"),
+ assetName: document.getElementById("assetName"),
+ marketType: document.getElementById("marketType"),
confFill: document.getElementById("confFill"),
confText: document.getElementById("confText"),
analysisText: document.getElementById("analysisText"),
@@ -69,9 +74,14 @@ function render(state) {
els.synthUp.textContent = state.synthUp;
els.synthDown.textContent = state.synthDown;
els.edgeValue.textContent = state.edge;
- els.signal1h.textContent = state.signal1h;
- els.signal24h.textContent = state.signal24h;
+ els.horizonLabel.textContent = state.horizonLabel || "Primary";
+ els.signalPrimary.textContent = state.signalPrimary;
+ els.refLabel.textContent = state.refLabel || "Reference";
+ els.signalRef.textContent = state.signalRef;
+ els.refRow.style.display = state.signalRef === "—" ? "none" : "";
els.strength.textContent = state.strength;
+ els.assetName.textContent = state.asset || "—";
+ els.marketType.textContent = state.marketType || "—";
els.analysisText.textContent = state.analysis;
els.noTrade.classList.toggle("hidden", !state.noTrade);
els.invalidationText.textContent = state.invalidation;
@@ -83,7 +93,9 @@ function render(state) {
const EMPTY = {
synthUp: "—", synthDown: "—", edge: "—",
- signal1h: "—", signal24h: "—", strength: "—",
+ horizonLabel: "Primary", signalPrimary: "—",
+ refLabel: "Reference", signalRef: "—",
+ strength: "—", asset: "—", marketType: "—",
analysis: "—", noTrade: false, invalidation: "—",
confPct: 0, confColor: "#9ca3af", confText: "—",
lastUpdate: "—",
@@ -119,22 +131,46 @@ async function refresh() {
return;
}
- const synthProbUp = edge.synth_probability_up != null ? edge.synth_probability_up : edge.synth_probability;
- const conf = edge.confidence_score != null ? edge.confidence_score : 0.5;
- const confPct = Math.round(conf * 100);
+ var synthProbUp = edge.synth_probability_up != null ? edge.synth_probability_up : edge.synth_probability;
+ var conf = edge.confidence_score != null ? edge.confidence_score : 0.5;
+ var confPct = Math.round(conf * 100);
+ var horizon = edge.horizon || "24h";
+ var mtype = edge.market_type || "daily";
+
+ // Build signal labels based on response shape
+ var horizonLabel = horizon;
+ var signalPrimary = (edge.signal || "—") + " " + fmtEdge(edge.edge_pct);
+ var refLabel = "Reference";
+ var signalRef = "—";
+
+ // Dual-horizon (daily/hourly): use 1h/24h fields
+ if (edge.signal_1h && edge.signal_24h) {
+ horizonLabel = "1 h";
+ signalPrimary = edge.signal_1h + " " + fmtEdge(edge.edge_1h_pct);
+ refLabel = "24 h";
+ signalRef = edge.signal_24h + " " + fmtEdge(edge.edge_24h_pct);
+ } else if (edge.ref_signal) {
+ // Short-horizon with reference context
+ refLabel = edge.ref_horizon || "Ref";
+ signalRef = edge.ref_signal + " " + fmtEdge(edge.ref_edge_pct);
+ }
render({
- status: "Synced — showing Synth forecast data.",
+ status: "Synced — " + (edge.asset || "BTC") + " " + horizon + " forecast.",
synthUp: fmtCentsFromProb(synthProbUp),
synthDown: synthProbUp == null ? "—" : fmtCentsFromProb(1 - synthProbUp),
edge: fmtEdge(edge.edge_pct),
- signal1h: edge.signal_1h ? edge.signal_1h + " " + fmtEdge(edge.edge_1h_pct) : (edge.signal || "—"),
- signal24h: edge.signal_24h ? edge.signal_24h + " " + fmtEdge(edge.edge_24h_pct) : "—",
+ horizonLabel: horizonLabel,
+ signalPrimary: signalPrimary,
+ refLabel: refLabel,
+ signalRef: signalRef,
strength: edge.strength || "—",
+ asset: edge.asset || "BTC",
+ marketType: mtype,
analysis: edge.explanation || "No explanation available.",
invalidation: edge.invalidation || "—",
noTrade: !!edge.no_trade_warning,
- confPct,
+ confPct: confPct,
confColor: confidenceColor(conf),
confText: (conf >= 0.7 ? "High" : conf >= 0.4 ? "Medium" : "Low") + " (" + confPct + "%)",
lastUpdate: fmtApiTime(edge.current_time),
diff --git a/tools/synth-overlay/matcher.py b/tools/synth-overlay/matcher.py
index f3c9dae..7f5c596 100644
--- a/tools/synth-overlay/matcher.py
+++ b/tools/synth-overlay/matcher.py
@@ -5,15 +5,22 @@
MARKET_DAILY = "daily"
MARKET_HOURLY = "hourly"
+MARKET_15MIN = "15min"
+MARKET_5MIN = "5min"
MARKET_RANGE = "range"
_HOURLY_TIME_PATTERN = re.compile(r"\d{1,2}(am|pm)")
+_15MIN_PATTERN = re.compile(r"(updown|up-down)-15m-|(? str | None:
return None
-def get_market_type(slug: str) -> Literal["daily", "hourly", "range"] | None:
+def get_market_type(slug: str) -> Literal["daily", "hourly", "15min", "5min", "range"] | None:
"""Infer Synth market type from slug. Returns None if not recognizable."""
if not slug:
return None
slug_lower = slug.lower()
+ if _5MIN_PATTERN.search(slug_lower):
+ return MARKET_5MIN
+ if _15MIN_PATTERN.search(slug_lower):
+ return MARKET_15MIN
if "up-or-down" in slug_lower and _HOURLY_TIME_PATTERN.search(slug_lower):
return MARKET_HOURLY
if "up-or-down" in slug_lower and "on-" in slug_lower:
diff --git a/tools/synth-overlay/server.py b/tools/synth-overlay/server.py
index c155791..7629f06 100644
--- a/tools/synth-overlay/server.py
+++ b/tools/synth-overlay/server.py
@@ -45,6 +45,98 @@ def health():
return jsonify({"status": "ok", "mock": get_client().mock_mode})
+_HORIZON_MAP = {"5min": "5min", "15min": "15min", "hourly": "1h", "daily": "24h"}
+
+
+def _fetch_updown_pair(client: SynthClient, asset: str, market_type: str) -> tuple[dict, dict]:
+ """Fetch primary + reference up/down data for cross-horizon context."""
+ fetchers = {
+ "5min": (client.get_polymarket_5min, client.get_polymarket_15min),
+ "15min": (client.get_polymarket_15min, client.get_polymarket_hourly),
+ "hourly": (client.get_polymarket_hourly, client.get_polymarket_daily),
+ "daily": (client.get_polymarket_daily, client.get_polymarket_hourly),
+ }
+ primary_fn, ref_fn = fetchers[market_type]
+ primary = primary_fn(asset)
+ try:
+ reference = ref_fn(asset)
+ except Exception:
+ reference = None
+ return primary, reference
+
+
+def _handle_updown_market(client: SynthClient, slug: str, asset: str, market_type: str):
+ """Handle up/down markets for any supported asset and horizon."""
+ primary_data, reference_data = _fetch_updown_pair(client, asset, market_type)
+
+ pct_1h = None
+ pct_24h = None
+ try:
+ pct_1h = client.get_prediction_percentiles(asset, horizon="1h")
+ pct_24h = client.get_prediction_percentiles(asset, horizon="24h")
+ except Exception:
+ pass
+
+ primary_horizon = _HORIZON_MAP[market_type]
+
+ # Daily/hourly: preserve dual-horizon analysis (1h vs 24h cross-comparison)
+ if market_type in ("daily", "hourly") and reference_data:
+ daily = primary_data if market_type == "daily" else reference_data
+ hourly = reference_data if market_type == "daily" else primary_data
+ analyzer = EdgeAnalyzer(daily, hourly, pct_1h, pct_24h)
+ result = analyzer.analyze(primary_horizon=primary_horizon)
+ primary_src = daily if market_type == "daily" else hourly
+ return jsonify({
+ "slug": slug,
+ "asset": asset,
+ "horizon": primary_horizon,
+ "market_type": market_type,
+ "edge_pct": result.primary.edge_pct,
+ "signal": result.primary.signal,
+ "strength": result.strength,
+ "confidence_score": result.confidence_score,
+ "edge_1h_pct": result.secondary.edge_pct if primary_horizon == "24h" else result.primary.edge_pct,
+ "signal_1h": result.secondary.signal if primary_horizon == "24h" else result.primary.signal,
+ "edge_24h_pct": result.primary.edge_pct if primary_horizon == "24h" else result.secondary.edge_pct,
+ "signal_24h": result.primary.signal if primary_horizon == "24h" else result.secondary.signal,
+ "no_trade_warning": result.no_trade,
+ "explanation": result.explanation,
+ "invalidation": result.invalidation,
+ "synth_probability_up": primary_src.get("synth_probability_up"),
+ "polymarket_probability_up": primary_src.get("polymarket_probability_up"),
+ "current_time": primary_src.get("current_time"),
+ })
+
+ # 5min/15min: single-horizon analysis with optional reference context
+ analyzer = EdgeAnalyzer(primary_data, reference_data, pct_1h, pct_24h)
+ result = analyzer.analyze_single_horizon(primary_data, horizon=primary_horizon)
+
+ resp = {
+ "slug": slug,
+ "asset": asset,
+ "horizon": primary_horizon,
+ "market_type": market_type,
+ "edge_pct": result.primary.edge_pct,
+ "signal": result.primary.signal,
+ "strength": result.strength,
+ "confidence_score": result.confidence_score,
+ "no_trade_warning": result.no_trade,
+ "explanation": result.explanation,
+ "invalidation": result.invalidation,
+ "synth_probability_up": primary_data.get("synth_probability_up"),
+ "polymarket_probability_up": primary_data.get("polymarket_probability_up"),
+ "current_time": primary_data.get("current_time"),
+ }
+ # Include reference horizon context when available
+ if reference_data and result.secondary:
+ resp["ref_horizon"] = _HORIZON_MAP.get(
+ {"5min": "15min", "15min": "hourly"}.get(market_type, ""), ""
+ )
+ resp["ref_edge_pct"] = result.secondary.edge_pct
+ resp["ref_signal"] = result.secondary.signal
+ return jsonify(resp)
+
+
@app.route("/api/edge", methods=["GET", "OPTIONS"])
def edge():
if request.method == "OPTIONS":
@@ -59,47 +151,8 @@ def edge():
asset = asset_from_slug(slug) or "BTC"
try:
client = get_client()
- if market_type in ("daily", "hourly"):
- if asset != "BTC":
- return jsonify({
- "error": f"Only BTC up-or-down markets are currently supported. "
- f"This market appears to be {asset}.",
- "slug": slug,
- "asset": asset,
- }), 404
- daily_data = client.get_polymarket_daily()
- hourly_data = client.get_polymarket_hourly()
- pct_1h = None
- pct_24h = None
- try:
- pct_1h = client.get_prediction_percentiles(asset, horizon="1h")
- pct_24h = client.get_prediction_percentiles(asset, horizon="24h")
- except Exception:
- pass
- primary_horizon = "24h" if market_type == "daily" else "1h"
- analyzer = EdgeAnalyzer(daily_data, hourly_data, pct_1h, pct_24h)
- result = analyzer.analyze(primary_horizon=primary_horizon)
- primary_data = daily_data if market_type == "daily" else hourly_data
- return jsonify({
- # Echo requested slug so extension can run on active matching markets,
- # even when mock payload carries a different representative slug.
- "slug": slug,
- "horizon": primary_horizon,
- "edge_pct": result.primary.edge_pct,
- "signal": result.primary.signal,
- "strength": result.strength,
- "confidence_score": result.confidence_score,
- "edge_1h_pct": result.secondary.edge_pct if primary_horizon == "24h" else result.primary.edge_pct,
- "signal_1h": result.secondary.signal if primary_horizon == "24h" else result.primary.signal,
- "edge_24h_pct": result.primary.edge_pct if primary_horizon == "24h" else result.secondary.edge_pct,
- "signal_24h": result.primary.signal if primary_horizon == "24h" else result.secondary.signal,
- "no_trade_warning": result.no_trade,
- "explanation": result.explanation,
- "invalidation": result.invalidation,
- "synth_probability_up": primary_data.get("synth_probability_up"),
- "polymarket_probability_up": primary_data.get("polymarket_probability_up"),
- "current_time": primary_data.get("current_time"),
- })
+ if market_type in ("daily", "hourly", "15min", "5min"):
+ return _handle_updown_market(client, slug, asset, market_type)
# range
data = client.get_polymarket_range()
if not isinstance(data, list):
diff --git a/tools/synth-overlay/tests/test_analyzer.py b/tools/synth-overlay/tests/test_analyzer.py
index 4727efb..5752adf 100644
--- a/tools/synth-overlay/tests/test_analyzer.py
+++ b/tools/synth-overlay/tests/test_analyzer.py
@@ -185,3 +185,46 @@ def test_range_no_trade_on_weak_edge(self):
sel = _bracket("[66000, 68000]", 0.40, 0.40)
r = EdgeAnalyzer().analyze_range(sel, [sel])
assert r.no_trade is True
+
+
+class TestSingleHorizonAnalysis:
+ def test_basic_single_horizon(self):
+ data = _daily(0.55, 0.50)
+ r = EdgeAnalyzer(data, None).analyze_single_horizon(data, horizon="15min")
+ assert isinstance(r, AnalysisResult)
+ assert r.primary.horizon == "15min"
+ assert r.primary.edge_pct == 5.0
+ assert r.primary.signal == "underpriced"
+
+ def test_single_horizon_with_reference(self):
+ primary = _daily(0.55, 0.50)
+ ref = _daily(0.54, 0.50)
+ r = EdgeAnalyzer(primary, ref).analyze_single_horizon(primary, horizon="5min")
+ assert r.secondary is not None
+ assert "confirms" in r.explanation.lower() or "higher" in r.explanation.lower()
+
+ def test_single_horizon_conflict_with_reference(self):
+ primary = _daily(0.55, 0.50)
+ ref = _daily(0.44, 0.50)
+ r = EdgeAnalyzer(primary, ref).analyze_single_horizon(primary, horizon="15min")
+ assert r.no_trade is True
+ assert r.strength == "none"
+ assert "conflict" in r.explanation.lower()
+
+ def test_single_horizon_no_reference(self):
+ data = _daily(0.50, 0.50)
+ r = EdgeAnalyzer(data, None).analyze_single_horizon(data, horizon="5min")
+ assert r.secondary is None
+ assert r.primary.signal == "fair"
+ assert "agree" in r.explanation.lower()
+
+ def test_single_horizon_invalidation_uses_horizon(self):
+ data = _daily(0.55, 0.50)
+ r = EdgeAnalyzer(data, None).analyze_single_horizon(data, horizon="5min")
+ assert "5min" in r.invalidation
+
+ def test_single_horizon_confidence_with_percentiles(self):
+ data = _daily(0.55, 0.50)
+ pct_narrow = _pct(100, 99.5, 100, 100.5)
+ r = EdgeAnalyzer(data, None, pct_narrow, pct_narrow).analyze_single_horizon(data)
+ assert r.confidence_score >= 0.7
diff --git a/tools/synth-overlay/tests/test_matcher.py b/tools/synth-overlay/tests/test_matcher.py
index a11dac6..a8f74dc 100644
--- a/tools/synth-overlay/tests/test_matcher.py
+++ b/tools/synth-overlay/tests/test_matcher.py
@@ -33,6 +33,20 @@ def test_get_market_type_hourly():
assert get_market_type("btc-up-or-down-march-1-3pm-et") == "hourly"
+def test_get_market_type_15min():
+ assert get_market_type("btc-updown-15m-1772204400") == "15min"
+ assert get_market_type("eth-updown-15m-1772204400") == "15min"
+ assert get_market_type("sol-up-down-15m-1772204400") == "15min"
+ assert get_market_type("bitcoin-15min-market") == "15min"
+
+
+def test_get_market_type_5min():
+ assert get_market_type("btc-updown-5m-1772205000") == "5min"
+ assert get_market_type("eth-updown-5m-1772205000") == "5min"
+ assert get_market_type("sol-up-down-5m-1772205000") == "5min"
+ assert get_market_type("bitcoin-5min-market") == "5min"
+
+
def test_get_market_type_range():
assert get_market_type("bitcoin-price-on-february-26") == "range"
@@ -44,6 +58,8 @@ def test_get_market_type_unsupported():
def test_is_supported():
assert is_supported("bitcoin-up-or-down-on-february-26") is True
assert is_supported("bitcoin-price-on-february-26") is True
+ assert is_supported("btc-updown-15m-1772204400") is True
+ assert is_supported("eth-updown-5m-1772205000") is True
assert is_supported("unknown-market") is False
@@ -54,8 +70,13 @@ def test_asset_from_slug():
assert asset_from_slug("xrp-up-or-down-on-march-1") == "XRP"
+def test_asset_from_slug_short_prefixes():
+ assert asset_from_slug("btc-up-or-down-on-march-1") == "BTC"
+ assert asset_from_slug("eth-updown-15m-1772204400") == "ETH"
+ assert asset_from_slug("sol-updown-5m-1772205000") == "SOL"
+
+
def test_asset_from_slug_unknown():
- assert asset_from_slug("btc-up-or-down-on-march-1") is None
assert asset_from_slug("random-slug") is None
assert asset_from_slug("") is None
assert asset_from_slug(None) is None
diff --git a/tools/synth-overlay/tests/test_server.py b/tools/synth-overlay/tests/test_server.py
index 11c4117..b2cb182 100644
--- a/tools/synth-overlay/tests/test_server.py
+++ b/tools/synth-overlay/tests/test_server.py
@@ -37,6 +37,9 @@ def test_edge_daily(client):
assert data["signal"] in ("underpriced", "overpriced", "fair")
assert data["strength"] in ("strong", "moderate", "none")
assert data["horizon"] == "24h"
+ assert data["market_type"] == "daily"
+ assert data["asset"] == "BTC"
+ # Dual-horizon fields preserved for daily/hourly
assert "edge_1h_pct" in data
assert "edge_24h_pct" in data
assert "signal_1h" in data
@@ -55,9 +58,10 @@ def test_edge_hourly_uses_hourly_primary_fields(client):
assert resp.status_code == 200
data = resp.get_json()
assert data["horizon"] == "1h"
+ assert data["market_type"] == "hourly"
assert data["slug"] == "bitcoin-up-or-down-february-25-6pm-et"
- assert data["synth_probability_up"] == 0.0004
- assert data["polymarket_probability_up"] == 0.006500000000000001
+ assert "synth_probability_up" in data
+ assert "polymarket_probability_up" in data
def test_edge_missing_slug(client):
@@ -109,17 +113,65 @@ def test_edge_range_unknown_slug_404(client):
assert resp.status_code == 404
-def test_edge_non_btc_daily_rejected(client):
- resp = client.get("/api/edge?slug=ethereum-up-or-down-on-february-28")
- assert resp.status_code == 404
+def test_edge_eth_daily_supported(client):
+ resp = client.get("/api/edge?slug=ethereum-up-or-down-on-february-26")
+ assert resp.status_code == 200
data = resp.get_json()
- assert "Only BTC" in data["error"]
assert data["asset"] == "ETH"
+ assert data["market_type"] == "daily"
+ assert "edge_pct" in data
-def test_edge_non_btc_hourly_rejected(client):
- resp = client.get("/api/edge?slug=solana-up-or-down-february-28-3pm-et")
- assert resp.status_code == 404
+def test_edge_sol_hourly_supported(client):
+ resp = client.get("/api/edge?slug=solana-up-or-down-february-25-6pm-et")
+ assert resp.status_code == 200
+ data = resp.get_json()
+ assert data["asset"] == "SOL"
+ assert data["market_type"] == "hourly"
+ assert "edge_pct" in data
+
+
+def test_edge_15min_btc(client):
+ resp = client.get("/api/edge?slug=btc-updown-15m-1772204400")
+ assert resp.status_code == 200
+ data = resp.get_json()
+ assert data["horizon"] == "15min"
+ assert data["market_type"] == "15min"
+ assert data["asset"] == "BTC"
+ assert "edge_pct" in data
+ assert "confidence_score" in data
+ assert "explanation" in data
+ assert len(data["explanation"]) > 10
+ assert "invalidation" in data
+ assert "15min" in data["invalidation"]
+ # Should NOT have dual-horizon 1h/24h fields
+ assert "edge_1h_pct" not in data
+ assert "signal_24h" not in data
+
+
+def test_edge_15min_eth(client):
+ resp = client.get("/api/edge?slug=eth-updown-15m-1772204400")
+ assert resp.status_code == 200
+ data = resp.get_json()
+ assert data["asset"] == "ETH"
+ assert data["market_type"] == "15min"
+
+
+def test_edge_5min_btc(client):
+ resp = client.get("/api/edge?slug=btc-updown-5m-1772205000")
+ assert resp.status_code == 200
+ data = resp.get_json()
+ assert data["horizon"] == "5min"
+ assert data["market_type"] == "5min"
+ assert data["asset"] == "BTC"
+ assert "edge_pct" in data
+ assert "5min" in data["invalidation"]
+ assert "edge_1h_pct" not in data
+
+
+def test_edge_5min_sol(client):
+ resp = client.get("/api/edge?slug=sol-updown-5m-1772205000")
+ assert resp.status_code == 200
data = resp.get_json()
- assert "Only BTC" in data["error"]
assert data["asset"] == "SOL"
+ assert data["market_type"] == "5min"