Strategies
SOL 15-Minute Polymarket Up/Down Fair-Value Lag: Price the Binary From the Binance SOL Perp's Move Since the Window Open and Its Realized Vol, Buy the UP Token Only When Expected Value Net of the Polymarket Taker Fee and Spread Clears a Threshold, Hold to Settlement (POLYMARKET sol-updown-15m + SOLUSDT.BINANCE 1-MINUTE signal leg)
Outcome: Backtesting
PolymarketSol15mFairValueLagUp
SOL 15-Minute Polymarket Up/Down Fair-Value Lag: Price the Binary From the Binance SOL Perp's Move Since the Window Open and Its Realized Vol, Buy the UP Token Only When Expected Value Net of the Polymarket Taker Fee…
Hypothesis
Quota gaps addressed: cross-venue (9.9% vs a target of at least 15%) and short horizon, on POLYMARKET, the most under-used supported venue (2 experiments, both data-blocked). The catalog now holds about 6,000 resolved SOL 15m markets, so the earlier data_unavailable blocker should no longer apply. It is an event-driven design with a fixed clock (resolution), flat most of the time and without supplementary data, which are the tag families with the best survival rates (L224, L189). It avoids every crowded family: no BTC, no momentum confluence, no NR7, no Hyperliquid panel, no funding carry. Long-only is structural on a CASH binary venue.
Implementation
Buys the SOL 15-minute Polymarket UP token when its fair probability, net of the 0.07*(1-p) taker fee and a 0.5% half-spread, beats the token price by at least min_net_edge. The fair probability is a driftless random-walk price, Phi(ln(S_t/S_0)/(sigma_1m*sqrt(tau))), taken from the Binance SOL perp. Entries are allowed from entry_start_min until entry_cutoff_frac of the 15-minute window has passed, and every position is held to settlement. Orders are market orders filled at the close of the decision bar, which on a 24/7 market stands in for the next bar's open.
Iteration 3 fixes the critical alignment defect QA found. Token bars are stamped at irregular seconds within each minute, so the old rule (token timestamp must equal a Binance timestamp exactly) fired on only about 0.14% of decision points. _fair_up now matches one-sided: it uses the latest Binance 1-minute close stamped at or before the token bar's ts_init and less than 60 seconds older (the bar closing at the start of the token bar's minute). This uses no future data and has a match on every token bar. The second decision path in on_extra_bar, and the schedule index it needed, is removed, so decisions are made only on token bars. S_0, the EWMA volatility, tau and minutes_in are computed as before. Setting _pm_entry_cutoff stays in market_signal because strategies may not override on_start, where the base class sets it from the shared entry_cutoff_frac parameter name; setting it again on each call is harmless. Config is unchanged.
Verification Results
Acceptable as is. Ideally the template would read a separately named listing cutoff parameter, so strategies would not need to patch base state.
market_signal sets self._pm_entry_cutoff = 1.0 as a side effect, to neutralise the base class's listing-life cutoff. It works because _pm_decide calls market_signal before checking the cutoff, and the hypothesis's cutoff is applied to the 15-minute window in should_enter_market. But it depends on the base's call order. I verified from the market metadata that start_ns is the listing time, about 24h before the window: for sol-updown-15m-1792278000, start_ts_ns is 1792192132e9 and end_ts_ns is the epoch + 15 min. So the neutralisation is needed and correct.
None required.
S_0 fallback: if the Binance bar that closes at the window open is missing, the code takes S_0 as the open of the first in-window minute. That is a reasonable approximation and does not look ahead, since the bar is already closed when it is used. The fallback cannot recover if the first in-window bar is missing too, in which case that window is skipped. This is harmless.
The fair value comes from the Binance SOLUSDT perp, but Polymarket resolves crypto up/down markets on a Chainlink SOL/USD reference. Near the money, a few basis points of perp-vs-oracle basis can flip outcomes. The p input is the token's last trade, not the mid or ask; in a thin 15-minute book a stale last print can make the model gap look larger than the price actually available. The engine charges PolymarketFeeModel (0.07 x p x (1-p) per share) but models no spread beyond the fill model, so the 0.5% spread_cost in the gate is only an assumption. A driftless-Gaussian q with an EWMA sigma floor of 0.0005 may also be overconfident on fat-tailed 1-minute SOL moves. First check: calibration of q against realized outcomes, and whether the edge survives using entry prices one tick worse.
Iteration History
Verification failed (Layer 3 — sandbox backtest): NotImplementedError Bar type used: SOL-UPDOWN-15M-1792226700-UP.POLYMARKET-1-MINUTE-LAST-EXTERNAL, Bars processed: 0 Ensure your strategy produces trades with the given data and parameters.
Match one-sided instead of by equality. On each token bar at now_ns, use the latest Binance close with _u_ts <= now_ns and now_ns - _u_ts < 60s (i.e. the Binance bar closing at floor(now_ns to the minute)). That is point-in-time correct and always available. Keep the S_0 = close at the window-open boundary (ts % win == 0) as it is. The on_extra_bar re-decision path can then be dropped, or changed to use the latest token bar with ts_init <= the Binance ts within the same minute (never a later token bar). Compute minutes_in and tau from the token bar's now_ns (as now).
The point-in-time alignment requires the token bar's ts_init to EXACTLY equal a Binance 1-minute close stamp (_fair_up: `self._u_ts != now_ns` -> None; on_extra_bar: `int(token_bar.ts_init) == ts`). Polymarket token bars in the catalog are NOT on the minute grid: they are stamped at irregular times (e.g. for SOL-UPDOWN-15M-1786564800-UP the in-window stamps are +15s, +72s, +132s, +192s, +252s, +313s, +369s ... after the epoch). I measured this on the last 300 sol-updown-15m UP markets: only 6 of 4,274 in-window token bars (0.14%) fall exactly on a minute boundary. So q is computed on about 0.14% of the decision points the hypothesis defines (every minute from entry_start_min to the cutoff), roughly 2 per 100 markets before the edge filter. That explains the 2 sandbox trades. The full backtest would test an accidental subsample of timestamp coincidences, not the hypothesis.
S_0 fallback: when the boundary Binance bar is missing, the first in-window bar's open is used. That is acceptable (open ~= the previous close on a continuous perp). Note that if both are missing, _k_ts never matches and that window is skipped silently, which is fine.
Set self._pm_entry_cutoff = 1.0 once in on_start (after super().on_start()).
market_signal overwrites the base _pm_entry_cutoff with 1.0 on every call so the base's listing-life cutoff (start_ns is the ~24h-earlier listing) does not block entries; the 15-minute window gate is enforced in should_enter_market instead. That is correct, but it relies on a side effect inside a signal hook.
Backtest Review
- Sharpe
- -5.46
- Total return
- -45.17%
- Max drawdown
- 57.39%
- Trades
- 2955
- Win rate
- 35.8%
- Profit factor
- 0.92
Backtest and paper results are hypothetical. Trading involves risk of loss.