<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: manja316</title>
    <description>The latest articles on DEV Community by manja316 (@manja316).</description>
    <link>https://dev.to/manja316</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3807032%2F019bc956-974c-46d5-880d-00fcfa5fabf7.jpeg</url>
      <title>DEV Community: manja316</title>
      <link>https://dev.to/manja316</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/manja316"/>
    <language>en</language>
    <item>
      <title>94.6% of ended prediction markets converge to near-certainty — and why I still can't tell you if they were right</title>
      <dc:creator>manja316</dc:creator>
      <pubDate>Mon, 29 Jun 2026 01:08:25 +0000</pubDate>
      <link>https://dev.to/manja316/946-of-ended-prediction-markets-converge-to-near-certainty-and-why-i-still-cant-tell-you-if-3ahj</link>
      <guid>https://dev.to/manja316/946-of-ended-prediction-markets-converge-to-near-certainty-and-why-i-still-cant-tell-you-if-3ahj</guid>
      <description>&lt;p&gt;I run a collector that snapshots every active Polymarket market's price every 15 minutes. After 92 days it's 18.6 million price points across 22,410 markets. People keep asking the same question, and the honest answer is more interesting than the marketing one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Is Polymarket well-calibrated? When it says 70%, does the thing happen 70% of the time?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I can't answer that from this data. But I can show you something adjacent and real — and, more usefully, I can show you exactly where the wall is, because most "prediction markets are smart" posts walk straight through it without noticing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I CAN measure: convergence
&lt;/h2&gt;

&lt;p&gt;Take every market that has ended (&lt;code&gt;end_date&lt;/code&gt; in the past) and look at its last traded YES price. On the frozen export (2026-03-28 → 2026-06-28):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ended&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;last_trade_price&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;95&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;last_trade_price&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;05&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;pct_decisive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;last_trade_price&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;last_trade_price&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;
        &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;pct_coinflip&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;markets&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;end_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'now'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;end_date&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;last_trade_price&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ended    pct_decisive   pct_coinflip
19402    94.6           0.9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;94.6% of ended markets had their price collapse to ≥0.95 or ≤0.05 by close.&lt;/strong&gt; Only 0.9% were still a coin-flip (0.40–0.60). The crowd makes up its mind almost every time before the window shuts.&lt;/p&gt;

&lt;p&gt;That's a genuinely useful fact if you're building anything time-aware: a market sitting at 0.55 two days before close is in the rare 4.5% that &lt;em&gt;hasn't&lt;/em&gt; resolved its uncertainty yet — that's where the trades live.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I CANNOT measure: was it right?
&lt;/h2&gt;

&lt;p&gt;Here's where the wall is. &lt;strong&gt;Convergence is not correctness.&lt;/strong&gt; A market going to 0.97 tells you traders agreed. It does not tell you the YES outcome happened. To know that, you need the resolved outcome — and this dataset doesn't have it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;markets&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- 0 | 22410   (every single row)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;resolved_outcome&lt;/code&gt; is empty for all 22,410 markets. The collector reads live prices from Polymarket's Gamma/CLOB APIs every 15 minutes; it never joins the on-chain resolution feed. So calibration curves, favorite-longshot bias, Brier scores — anything that needs "which side won" — are &lt;strong&gt;not computable from this file alone.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You could approximate by &lt;em&gt;assuming&lt;/em&gt; the final price equals the truth (price ≥ 0.95 → "YES happened"). But that's circular: you'd be grading the market against its own last guess, then announcing it's well-calibrated. It's the single most common mistake in prediction-market blog posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ship a dataset and tell you what it can't do?
&lt;/h2&gt;

&lt;p&gt;Because the alternative is a refund and a 1-star review. The price series is the dense, reliable layer — 15-minute resolution, 92 days, no gaps, no scraping (straight from the public Gamma + CLOB APIs). That's worth paying for if you're backtesting price-based strategies, training a short-horizon predictor, or studying microstructure. It is &lt;em&gt;not&lt;/em&gt; worth paying for if you wanted a calibration study, and I'd rather you know that before you click.&lt;/p&gt;

&lt;p&gt;(Same reason I published &lt;a href="https://dev.to/manja316/88-of-the-order-book-rows-in-my-dataset-were-fake-heres-how-i-caught-it-4hn8"&gt;the audit showing ~94% of the order-book rows are thin-market placeholders&lt;/a&gt;. Price = the product. Order book and resolution labels = honest caveats.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it in 30 seconds
&lt;/h2&gt;

&lt;p&gt;Free 1-day sample (no signup): &lt;a href="https://huggingface.co/datasets/manja316/polymarket-historical-prices" rel="noopener noreferrer"&gt;Hugging Face&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="n"&gt;con&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;polymarket.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT last_trade_price p FROM markets &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHERE end_date &amp;lt; date(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;now&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;) AND last_trade_price IS NOT NULL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;decisive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;decisive&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; of ended markets converged&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full dataset, $19 one-time: &lt;a href="https://manja8.gumroad.com/l/polymarket-quant-toolkit?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=polymarket-data-2026-06-29" rel="noopener noreferrer"&gt;Gumroad&lt;/a&gt;. Live auto-refreshing API: &lt;a href="https://api.protodex.io" rel="noopener noreferrer"&gt;api.protodex.io&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open question for the comments:&lt;/strong&gt; if you had the resolution labels joined in, what's the first thing you'd test — calibration, or fading the longshots? That's the next build and I'll prioritize by what people actually want.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>dataset</category>
      <category>python</category>
      <category>trading</category>
    </item>
    <item>
      <title>What 91 days of Polymarket price data can — and can't — tell you</title>
      <dc:creator>manja316</dc:creator>
      <pubDate>Sat, 27 Jun 2026 23:06:08 +0000</pubDate>
      <link>https://dev.to/manja316/what-91-days-of-polymarket-price-data-can-and-cant-tell-you-19p7</link>
      <guid>https://dev.to/manja316/what-91-days-of-polymarket-price-data-can-and-cant-tell-you-19p7</guid>
      <description>&lt;p&gt;Most "dataset" posts oversell. This one tells you the limits first.&lt;/p&gt;

&lt;p&gt;For 91 straight days (28 Mar → 27 Jun 2026) I collected Polymarket prices on one Mac, no cloud bill. The frozen export is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;18,591,646&lt;/strong&gt; price snapshots&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;22,362&lt;/strong&gt; markets (19,819 of them with an actual price series — avg &lt;strong&gt;938 snapshots per market&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1,854,788&lt;/strong&gt; orderbook snapshots&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Category mix: sports 12,258 · &lt;code&gt;other&lt;/code&gt; 4,689 · crypto 2,514 · politics 1,441 · geopolitics 685 · science/tech 241 · economics 224 · entertainment 169 · weather 137.&lt;/p&gt;

&lt;p&gt;Here's what you can actually do with that — and three things you can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it's good for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Price-path / convergence studies.&lt;/strong&gt; Of the 6,776 markets that &lt;em&gt;ended&lt;/em&gt; inside the window, &lt;strong&gt;6,551 (96.7%) closed decisively&lt;/strong&gt; — last Yes-price above 0.95 or below 0.05. Only 3.3% were still mushy in the middle at the end. So the data is clean enough to study &lt;em&gt;how&lt;/em&gt; and &lt;em&gt;when&lt;/em&gt; a market makes up its mind: most of the information arrives well before close, and you can measure the shape of that arrival per category. (Crypto markets snap late; politics drifts.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Mean-reversion / momentum backtests on the spread.&lt;/strong&gt; With ~938 snapshots/market you have enough intraday resolution to test "does a 10-point move in 6 hours revert or continue?" across thousands of contracts and eight categories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Behavioral / microstructure features.&lt;/strong&gt; Volume, 24h volume, liquidity, best bid/ask, spread, and last-trade are all captured per snapshot, so you can build features without re-hitting the API 17 million times.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it can NOT do (the part most listings hide)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. True calibration. The dataset has no resolution labels.&lt;/strong&gt; Every market in the export carries price series — not the final settled outcome. So you can study &lt;em&gt;price&lt;/em&gt; convergence, but you cannot compute real calibration (predicted probability vs realized result) without joining external resolution data yourself. If a vendor shows you a "calibration curve" derived purely from prices, they're measuring the market against itself, not against reality. I'm not going to pretend otherwise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Deep order-book research.&lt;/strong&gt; The orderbook table is large (1.85M rows) but only a single-digit % of those rows are genuine two-sided quotes — the rest are placeholder/one-sided. &lt;strong&gt;The price series is the real product here, not the book.&lt;/strong&gt; Buy it for price history; don't buy it expecting an L2 reconstruction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Sub-minute tick data.&lt;/strong&gt; This is snapshot cadence, not a trade-by-trade feed. Great for hourly/daily strategy research, wrong tool for HFT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why collect it at all
&lt;/h2&gt;

&lt;p&gt;Polymarket's API is free but rate-limited and ephemeral — query it today and last month's prices are gone. The value of a frozen 91-day export is that the history &lt;em&gt;exists&lt;/em&gt; and is queryable in one SQLite file (&lt;code&gt;prices&lt;/code&gt;, &lt;code&gt;markets&lt;/code&gt;, &lt;code&gt;orderbooks&lt;/code&gt;, &lt;code&gt;market_features&lt;/code&gt;, …), indexed on &lt;code&gt;(market_id, ts)&lt;/code&gt;. Pull a market's full path in one query instead of paginating a live endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;market_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Yes'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole pitch: clean, indexed, honestly-scoped prediction-market price history you can backtest against tonight.&lt;/p&gt;

&lt;p&gt;If that's the shape of data you need, the full export (and a free sample tier) is here:&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://manja8.gumroad.com/l/agyjd?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=polymarket-data-2026-06-29" rel="noopener noreferrer"&gt;Polymarket Historical Price Dataset&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Questions about schema or coverage before you buy — ask in the comments and I'll answer with a real query against the file.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Counts above are computed directly from the delivered export (the exact file you download), not the live collector, so what you read is what you get.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>datasets</category>
      <category>data</category>
      <category>trading</category>
      <category>opensource</category>
    </item>
    <item>
      <title>88% of the order-book rows in my dataset were fake. Here's how I caught it.</title>
      <dc:creator>manja316</dc:creator>
      <pubDate>Fri, 26 Jun 2026 02:23:29 +0000</pubDate>
      <link>https://dev.to/manja316/88-of-the-order-book-rows-in-my-dataset-were-fake-heres-how-i-caught-it-4hn8</link>
      <guid>https://dev.to/manja316/88-of-the-order-book-rows-in-my-dataset-were-fake-heres-how-i-caught-it-4hn8</guid>
      <description>&lt;p&gt;I've been collecting Polymarket prediction-market data on a single Mac for 89 days. As of this morning the SQLite file holds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;18,207,844&lt;/strong&gt; price snapshots (15-minute OHLC)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;21,871&lt;/strong&gt; markets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1,816,392&lt;/strong&gt; order-book rows&lt;/li&gt;
&lt;li&gt;span: &lt;strong&gt;2026-03-28 → 2026-06-25&lt;/strong&gt; (89 days)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For weeks I described it as "18M price snapshots &lt;strong&gt;+ 1.8M order-book records&lt;/strong&gt;." That second number was a lie I was telling by accident. This post is the autopsy, because if you buy or build on market data, the failure mode I hit is one you will hit too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smell test that should have run on day one
&lt;/h2&gt;

&lt;p&gt;I went to compute realized spreads — &lt;code&gt;best_ask - best_bid&lt;/code&gt; over time — expecting a tight distribution for liquid markets. Instead almost every row looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;best_bid | best_ask | spread | mid
   0.001 |    0.999 |  0.998 | 0.5
   0.001 |    0.999 |  0.998 | 0.5
   0.001 |    0.999 |  0.998 | 0.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 99.8-cent spread on a market that resolves between $0 and $1 is not a quote. It's the placeholder my collector wrote when the CLOB returned an empty book — and I had been counting every one of those as a "record."&lt;/p&gt;

&lt;p&gt;One query settled it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;best_ask&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;best_bid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;effectively_empty&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orderbooks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- total            = 1,816,392&lt;/span&gt;
&lt;span class="c1"&gt;-- effectively_empty = 1,602,616   (88.2%)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;88.2% of my order-book rows carry the 0.001 / 0.999 placeholder.&lt;/strong&gt; Only ~11.8% have a spread tight enough to even be a candidate for a real two-sided quote, and the genuinely tradeable subset is smaller still. The "1.8M order-book records" headline was real rows in a table and almost entirely empty as &lt;em&gt;information&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this happens (and why it's not Polymarket's fault)
&lt;/h2&gt;

&lt;p&gt;Prediction markets are thin. Most of the 21,871 markets are long-tail contracts — a niche election, a sports prop, a "will X tweet by Friday." At any given 15-minute poll, most of those books are genuinely empty. The collector dutifully recorded "no bid, no ask" as a row. Nothing was broken. The bug was in how I &lt;em&gt;summarized&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;The price series, by contrast, is dense and reliable: a market's last-trade / OHLC ticks along even when the book is empty, because it reflects executed prices, not resting orders. &lt;strong&gt;18.2M price rows are 18.2M real observations. 1.8M book rows are ~200K real observations wearing a costume.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dropped "order-book records" as a headline feature&lt;/strong&gt; everywhere I controlled — README, the dataset article, the listing copy. Selling an 88%-empty column as a feature is how you earn refund requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-led with the price series&lt;/strong&gt;, which is what the dataset is actually good for: backtesting, calibration studies, favorite-longshot analysis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shipped the empty book anyway, labeled honestly&lt;/strong&gt; — as a sparse bonus with its real coverage stated, not as a selling point. A few hundred thousand real top-of-book snapshots still have uses; pretending it's 1.8M does not.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The transferable lesson
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;COUNT(*)&lt;/code&gt; is not coverage. Before you trust — or sell, or backtest on — any market dataset:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run the &lt;strong&gt;spread/sanity distribution&lt;/strong&gt;, not just the row count. A column can be 100% populated and ~0% informative.&lt;/li&gt;
&lt;li&gt;Separate &lt;strong&gt;observations&lt;/strong&gt; from &lt;strong&gt;rows&lt;/strong&gt;. Placeholders are rows. Only non-degenerate values are observations.&lt;/li&gt;
&lt;li&gt;State coverage &lt;strong&gt;as a percentage of plausible values&lt;/strong&gt;, not as a raw total. "1.8M rows, 12% non-empty" is honest; "1.8M records" is marketing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd rather a buyer learn this from my article than discover it after paying. The dense, verified layer — 18.2M price snapshots across 89 days — is the thing worth having, and it's the thing I now lead with.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The full dataset (price series + the honestly-labeled sparse book) is on Gumroad: &lt;a href="https://manja8.gumroad.com/l/polymarket-quant-toolkit?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=polymarket-data-2026-06-26" rel="noopener noreferrer"&gt;Polymarket Quant Toolkit&lt;/a&gt;. There's a free sample so you can run your own sanity queries before paying — which, after reading this, you absolutely should.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>dataset</category>
      <category>python</category>
      <category>trading</category>
    </item>
    <item>
      <title>What 86 Days of Clean Prediction-Market History Actually Shows</title>
      <dc:creator>manja316</dc:creator>
      <pubDate>Wed, 24 Jun 2026 22:59:02 +0000</pubDate>
      <link>https://dev.to/manja316/what-86-days-of-clean-prediction-market-history-actually-shows-1122</link>
      <guid>https://dev.to/manja316/what-86-days-of-clean-prediction-market-history-actually-shows-1122</guid>
      <description>&lt;p&gt;Everyone has an opinion about whether prediction markets are "smart." Almost nobody has the data to check.&lt;/p&gt;

&lt;p&gt;I do. For 86 days straight, a single Mac mini has been polling Polymarket's public API and writing every price tick to a local SQLite file. As of this morning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;20,585 markets&lt;/strong&gt; tracked&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;17,256,332 price snapshots&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1,721,218 order-book snapshots&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;85 continuous days&lt;/strong&gt; (2026-03-28 → 2026-06-21)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$0/month&lt;/strong&gt; in infrastructure — no cloud, no cluster, one always-on machine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;(Those are the exact counts in the downloadable export. The live recorder keeps running — the local feed is already past 18.0M snapshots across 19,084 markets — but a buyer should know precisely what's in the file, so these are the file's numbers, not the live feed's.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's not a toy sample. It's enough to ask the only question that matters about a prediction market: &lt;strong&gt;when the crowd says 70%, does the thing happen 70% of the time?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is hard to get anywhere else
&lt;/h2&gt;

&lt;p&gt;Polymarket's API serves you the &lt;em&gt;current&lt;/em&gt; price. It does not hand you history. If you want to know what a market was trading at three weeks ago — the thing you need for any backtest — you had to have been recording it then. There's no rewind button.&lt;/p&gt;

&lt;p&gt;So the dataset isn't valuable because the data is secret. It's valuable because it's &lt;strong&gt;time-stamped and continuous&lt;/strong&gt;. You can't reconstruct it after the fact at any price. You either started the recorder in March or you didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The calibration question, concretely
&lt;/h2&gt;

&lt;p&gt;Take every market that resolved. Bucket the price history into deciles — the 0–10% bucket, the 10–20% bucket, and so on. For each bucket, compute the fraction of those markets that actually resolved YES.&lt;/p&gt;

&lt;p&gt;A perfectly calibrated market draws a straight 45° line: the 30% bucket resolves YES ~30% of the time, the 90% bucket ~90%, etc.&lt;/p&gt;

&lt;p&gt;When I ran exactly this earlier in June, the broad finding held up better than the cynics expect — the crowd is roughly honest in the middle of the distribution, and the interesting distortions live in the &lt;strong&gt;tails&lt;/strong&gt; (very cheap longshots and very expensive favorites), which is precisely where the &lt;a href="https://en.wikipedia.org/wiki/Favourite-longshot_bias" rel="noopener noreferrer"&gt;favorite-longshot bias&lt;/a&gt; lives in every betting market ever studied. I wrote that up with the bucket-by-bucket table here: &lt;strong&gt;&lt;a href="https://dev.to/manja316/when-polymarket-says-70-does-it-happen-70-of-the-time-i-checked-against-194m-price-snapshots-3enj"&gt;when Polymarket says 70%, does it happen 70% of the time?&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The point of &lt;em&gt;this&lt;/em&gt; post isn't to re-paste that table. It's to show you the query is trivial once you have the history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;con&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market_universe.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Latest price per resolved market, joined to its outcome
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
  SELECT p.market_id, p.outcome, p.price, mo.outcome_label
  FROM prices p
  JOIN market_outcomes mo
    ON p.market_id = mo.market_id
  WHERE p.outcome = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Yes&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# join your resolution labels, then:
&lt;/span&gt;&lt;span class="n"&gt;calib&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;mean_price&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;calib&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five lines of pandas. The hard part — &lt;em&gt;having 17 million rows of honest, timestamped history to run it against&lt;/em&gt; — is the part that took 86 days.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can build on top of it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Calibration audits&lt;/strong&gt; — is a specific market category (politics? sports? crypto?) better calibrated than another?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Longshot fade backtests&lt;/strong&gt; — systematically short the sub-10% bucket and measure the edge after fees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mean-reversion / momentum&lt;/strong&gt; studies on the minute-level price path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event-study windows&lt;/strong&gt; — how fast does a market re-price around a news shock?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of it needs the same thing: a continuous price record you didn't have to be there to capture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get the data
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free sample + schema&lt;/strong&gt; to kick the tires: comment and I'll point you at it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full archive (17.2M snapshots, one-time download):&lt;/strong&gt; &lt;a href="https://manja8.gumroad.com/l/agyjd?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=polymarket-data-2026-06-29" rel="noopener noreferrer"&gt;on Gumroad&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly refresh&lt;/strong&gt; (the recorder keeps running; you get the new days): ask in the comments — I'm pricing it for the people who actually backtest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The crowd is mostly calibrated. The edge is in knowing exactly &lt;em&gt;where&lt;/em&gt; it isn't — and that only shows up if someone was recording. Someone was.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>trading</category>
      <category>python</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>17 million Polymarket price snapshots, collected on one Mac for $0/month</title>
      <dc:creator>manja316</dc:creator>
      <pubDate>Mon, 22 Jun 2026 10:54:09 +0000</pubDate>
      <link>https://dev.to/manja316/17-million-polymarket-price-snapshots-collected-on-one-mac-for-0month-2cen</link>
      <guid>https://dev.to/manja316/17-million-polymarket-price-snapshots-collected-on-one-mac-for-0month-2cen</guid>
      <description>&lt;p&gt;Most prediction-market datasets you find online are a one-time dump someone scraped, posted, and abandoned. They go stale the day after they're published. I wanted a &lt;em&gt;living&lt;/em&gt; archive of Polymarket — every market, sampled every 15 minutes, running continuously — and I wanted it to cost nothing to operate.&lt;/p&gt;

&lt;p&gt;Here's what the archive holds today, counted straight from the database:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;18,611,636&lt;/strong&gt; price snapshots&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;22,410&lt;/strong&gt; markets tracked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~92 days&lt;/strong&gt; of continuous 15-minute history (and counting)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of it runs on a single Mac, with a recurring infrastructure bill of &lt;strong&gt;$0/month&lt;/strong&gt;. No cloud database, no managed queue, no Kubernetes. This post is how.&lt;/p&gt;

&lt;h3&gt;
  
  
  The architecture is boring on purpose
&lt;/h3&gt;

&lt;p&gt;The whole thing is three moving parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A collector&lt;/strong&gt; — a Python process on a 15-minute timer (launchd, the macOS-native scheduler — no cron daemon, no external trigger). Each tick pulls the live market list from Polymarket's public API, then for each active market records the current YES/NO prices and, where one exists, the top of the order book.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One SQLite file&lt;/strong&gt; — not Postgres, not a warehouse. SQLite handles tens of millions of rows on a laptop without complaint as long as you index the access path you actually use. The entire archive is a single &lt;code&gt;.db&lt;/code&gt; file you can &lt;code&gt;scp&lt;/code&gt; anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A daily exporter&lt;/strong&gt; — dumps the SQLite tables to Parquet so the dataset is portable and loads in one line of pandas/Polars.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. The "$0/month" isn't a trick — a laptop you already own, the OS scheduler you already have, and a file format that doesn't bill you.&lt;/p&gt;

&lt;h3&gt;
  
  
  One honest caveat: the price series is the real product, not the book
&lt;/h3&gt;

&lt;p&gt;The collector also samples top-of-book (best bid / best ask) every tick — but be clear-eyed about it: most Polymarket markets are thin, so a live two-sided book simply doesn't exist at most sample times. Counted straight from the DB, only about &lt;strong&gt;6%&lt;/strong&gt; of the 1.86M book rows captured a real two-sided quote; the rest are markets with no live book at that moment. So treat the dense, reliable layer as the &lt;strong&gt;price time series&lt;/strong&gt; (18.6M real snapshots) — that's what you backtest on. The book samples are a sparse bonus for the handful of liquid markets, not a full reconstructable order book for everything. I'd rather tell you that up front than have you discover it after download.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Index the query, not the table.&lt;/strong&gt; The naive mistake is to over-index and watch your write throughput collapse every 15 minutes. The prices table has exactly one composite index — &lt;code&gt;(market_id, ts)&lt;/code&gt; — because the only read pattern that matters is "give me the price history of &lt;em&gt;this&lt;/em&gt; market over &lt;em&gt;this&lt;/em&gt; window." One index, sized to the actual query, keeps both the 15-minute writes and the backtest reads fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Append, never mutate.&lt;/strong&gt; Every snapshot is an immutable row stamped with an ISO-8601 UTC timestamp. Nothing is ever updated in place. That means the archive is a true time series — you can reconstruct the YES/NO price as it stood at any 15-minute mark in the last 92 days, not just "latest state." Mutable rows would have quietly destroyed the history I was trying to capture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this is hard to copy (and why that matters if you trade)
&lt;/h3&gt;

&lt;p&gt;The dataset isn't valuable because the &lt;em&gt;code&lt;/em&gt; is clever — it's three boring parts. It's valuable because of the one thing you cannot backfill: &lt;strong&gt;time.&lt;/strong&gt; You cannot retroactively collect the price as it stood on April 3rd at 14:15 UTC. Either a process was running and recording it, or that moment is gone forever.&lt;/p&gt;

&lt;p&gt;So if you want 92 days of 15-minute Polymarket history to backtest a mean-reversion or calibration strategy, you have two options: stand up a collector today and wait three months, or start from the archive that's already been running since March 28th.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get the data
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free sample + schema + loader:&lt;/strong&gt; &lt;a href="https://github.com/LuciferForge/polymarket-historical-data" rel="noopener noreferrer"&gt;github.com/LuciferForge/polymarket-historical-data&lt;/a&gt; — grab the sample, check the schema, run the example query before you commit to anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full dataset (one-time download):&lt;/strong&gt; &lt;a href="https://manja8.gumroad.com/l/polymarket-quant-toolkit?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=polymarket-data-2026-06-29" rel="noopener noreferrer"&gt;on Gumroad&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd want this kept fresh automatically — a recurring refresh so your backtests never run on a frozen file — that's the thing I'm deciding whether to build next. There's an open roadmap thread on the repo; tell me what cadence and format you'd actually use.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All figures in this post were counted from the database export built on 2026-06-28 and are not estimates.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>sqlite</category>
      <category>opensource</category>
    </item>
    <item>
      <title>What 295,732 IPL balls reveal: chase to win, and the death overs are a different game</title>
      <dc:creator>manja316</dc:creator>
      <pubDate>Mon, 22 Jun 2026 02:01:25 +0000</pubDate>
      <link>https://dev.to/manja316/what-295732-ipl-balls-reveal-chase-to-win-and-the-death-overs-are-a-different-game-3nf2</link>
      <guid>https://dev.to/manja316/what-295732-ipl-balls-reveal-chase-to-win-and-the-death-overs-are-a-different-game-3nf2</guid>
      <description>&lt;p&gt;I cleaned every IPL delivery from 2008 to 2026 into one tidy ball-by-ball table — 295,732 rows — and added the columns I always end up recomputing by hand (match phase, running run-rate, per-batter tallies, and fantasy box-scores). Then I asked the data three simple questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Chasing wins — but the toss barely matters
&lt;/h2&gt;

&lt;p&gt;Across 1,218 completed matches, the toss-winner won just &lt;strong&gt;51.6%&lt;/strong&gt; of the time. Close to a coin flip — &lt;em&gt;until you look at what they do with it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Captains who won the toss and chose to &lt;strong&gt;field (chase)&lt;/strong&gt; won &lt;strong&gt;54.7%&lt;/strong&gt; of their matches. Those who chose to &lt;strong&gt;bat first&lt;/strong&gt; won only &lt;strong&gt;45.3%&lt;/strong&gt;. A ~9-point swing from one decision — chasing is a real, persistent edge in the IPL.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The death overs are a different sport
&lt;/h2&gt;

&lt;p&gt;Run rate by phase, across every IPL ball:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Run rate&lt;/th&gt;
&lt;th&gt;Wickets / over&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Powerplay (1–6)&lt;/td&gt;
&lt;td&gt;7.78&lt;/td&gt;
&lt;td&gt;0.23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Middle (7–15)&lt;/td&gt;
&lt;td&gt;7.81&lt;/td&gt;
&lt;td&gt;0.26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Death (16–20)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.78&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.52&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Scoring barely moves from powerplay to middle, then explodes at the death — but the wicket rate &lt;strong&gt;more than doubles&lt;/strong&gt;. If you're modeling fantasy points or win probability, treating all overs the same leaves signal on the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Do it yourself
&lt;/h2&gt;

&lt;p&gt;The dataset is one flat CSV, so this is a three-liner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ipl_ballbyball.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phase&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runs_total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phase&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ball_in_innings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The data
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free sample on Kaggle:&lt;/strong&gt; &lt;a href="https://www.kaggle.com/datasets/luciferforge/ipl-cricket-ballbyball-enriched" rel="noopener noreferrer"&gt;IPL ball-by-ball + fantasy box-scores&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full 19-season history (295,732 deliveries + 27,374 player-match fantasy box-scores), auto-refreshed weekly:&lt;/strong&gt; &lt;a href="https://manja8.gumroad.com/l/ipl-cricket" rel="noopener noreferrer"&gt;$12 on Gumroad&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Internationals too: &lt;a href="https://www.kaggle.com/datasets/luciferforge/t20i-cricket-ballbyball-enriched" rel="noopener noreferrer"&gt;765k+ T20I deliveries on Kaggle&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built from &lt;a href="https://cricsheet.org" rel="noopener noreferrer"&gt;Cricsheet&lt;/a&gt; (ODC-BY). Columns include match phase, live run rate, running score, per-batter tallies, and per-player fantasy box-scores — so you can model straight away instead of parsing raw JSON.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>python</category>
      <category>cricket</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Supervising 13 always-on processes without Kubernetes: the log-freshness health check that ended my 3 a.m. babysitting</title>
      <dc:creator>manja316</dc:creator>
      <pubDate>Sat, 20 Jun 2026 22:54:36 +0000</pubDate>
      <link>https://dev.to/manja316/supervising-13-always-on-processes-without-kubernetes-the-log-freshness-health-check-that-ended-my-4g4l</link>
      <guid>https://dev.to/manja316/supervising-13-always-on-processes-without-kubernetes-the-log-freshness-health-check-that-ended-my-4g4l</guid>
      <description>&lt;p&gt;I'm a solo builder. At any given moment I have a handful of always-on processes earning, scraping, or thinking on my behalf: a Polymarket crash-recovery trading bot, a weekly site-scraper that regenerates a ~14k-entry directory, a couple of API servers, a secret-scanner, a DB-retention job, and a few scheduled agents that call an LLM to review strategy or hunt vulnerabilities. Thirteen in total.&lt;/p&gt;

&lt;p&gt;For months these lived as a mess of &lt;code&gt;launchd&lt;/code&gt; plists, loose &lt;code&gt;crontab&lt;/code&gt; lines, and &lt;code&gt;nohup … &amp;amp;&lt;/code&gt; processes I'd forget about until one silently died — and I'd notice &lt;em&gt;days&lt;/em&gt; later, usually because a number on a dashboard stopped moving. That's the classic agent-ops failure mode: &lt;strong&gt;lots of independent autonomous processes, no unified supervisor.&lt;/strong&gt; Each one is individually simple. Collectively they're un-auditable.&lt;/p&gt;

&lt;p&gt;I didn't want Kubernetes. For one solo dev's always-on jobs, k8s is a sledgehammer — a control plane heavier than everything it would supervise. I wanted the &lt;em&gt;smallest&lt;/em&gt; thing that would give me one honest answer to one question: &lt;strong&gt;is everything that's supposed to be running actually running, and actually working?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I built ForgeOS: a tiny kernel (~1,900 LOC of Python, zero heavy deps — just &lt;code&gt;pyyaml&lt;/code&gt;) that treats every process as an &lt;strong&gt;engine&lt;/strong&gt; defined by a single YAML file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three engine types, one primitive
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;daemon        long-running        the trading bot, API servers, collectors
cron          scheduled           weekly export, daily scan
intelligence  scheduled + LLM     a Claude-CLI agent run on a schedule, output piped back in
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third type is the one people raise an eyebrow at, so let me defend it early: an &lt;strong&gt;intelligence engine is just a cron job that happens to call an LLM.&lt;/strong&gt; Same scheduling, same health checks, same logging as any other engine. The moment I stopped treating "AI agents" as special snowflakes and modeled them as the same primitive as a backup job, the whole system got simpler. A scheduled strategy-review agent and a nightly DB-prune are the same shape; only the &lt;code&gt;command&lt;/code&gt; differs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually changed my life: health + self-heal
&lt;/h2&gt;

&lt;p&gt;Each engine declares what "healthy" means &lt;em&gt;right in its config&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;crash-bot&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;daemon&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Polymarket crash-recovery bot&lt;/span&gt;
&lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python3"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pm_crash_monitor.py"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/.../agents/trader&lt;/span&gt;
&lt;span class="na"&gt;health&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;process&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pm_crash_monitor&lt;/span&gt;      &lt;span class="c1"&gt;# must appear in the process table&lt;/span&gt;
  &lt;span class="na"&gt;log_max_age_min&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;            &lt;span class="c1"&gt;# log must have been written in the last 20 min&lt;/span&gt;
  &lt;span class="na"&gt;log_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/pm_crash_monitor.log&lt;/span&gt;
&lt;span class="na"&gt;kill_condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily_loss&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$10&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;OR&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cash&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$20"&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;LIVE_TRADING&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The kernel runs a loop: for each enabled engine, check the declared health signals — is the process alive, is the log fresh? If a daemon is dead &lt;em&gt;or its log has gone stale&lt;/em&gt;, restart it and record the event. One command, &lt;code&gt;forge health&lt;/code&gt;, tells me the true state of all 13 engines instead of me SSH-ing around running &lt;code&gt;ps aux | grep&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;forge init       &lt;span class="c"&gt;# creates ~/.forgeos/&lt;/span&gt;
forge start      &lt;span class="c"&gt;# start all daemon engines&lt;/span&gt;
forge health     &lt;span class="c"&gt;# the one honest answer&lt;/span&gt;
forge daemon     &lt;span class="c"&gt;# run the kernel: health + scheduling + self-heal&lt;/span&gt;
forge brief      &lt;span class="c"&gt;# one-screen status of everything&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Three lessons that generalize beyond my setup
&lt;/h2&gt;

&lt;p&gt;If you run autonomous agents, these cost me real downtime to learn:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. "Is the process running?" is a near-useless health check on its own.&lt;/strong&gt; A &lt;em&gt;hung&lt;/em&gt; process is "running." The PID is right there. My collectors and AI agents fail far more often by &lt;em&gt;getting stuck&lt;/em&gt; than by &lt;em&gt;crashing&lt;/em&gt; — and a stuck process keeps its PID forever. Adding &lt;code&gt;log_max_age_min&lt;/code&gt; (the log must have been written recently) caught dramatically more real failures than PID checks ever did. Liveness ≠ progress. Health-check progress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Declare kill conditions in config, not buried in code.&lt;/strong&gt; My trading engine carries &lt;code&gt;kill_condition: daily_loss &amp;gt; $10 OR cash &amp;lt; $20&lt;/code&gt; next to its definition — not 400 lines deep in the bot's source. Keeping the safety limit &lt;em&gt;visible&lt;/em&gt;, beside the engine it governs, is what made me actually willing to let a money-moving process run unattended. A safety limit you can't see at a glance is a safety limit you don't trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Model AI agents as ordinary engines.&lt;/strong&gt; The temptation is to build a special "agent runtime." Resist it. An LLM call on a schedule is a cron job with an expensive &lt;code&gt;command&lt;/code&gt;. Give it the same health loop (did it run? is its output log fresh?) and the same restart policy as everything else. The non-determinism lives &lt;em&gt;inside&lt;/em&gt; the &lt;code&gt;command&lt;/code&gt;; the supervision around it should be boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part: where this still falls short
&lt;/h2&gt;

&lt;p&gt;Log-freshness answers "did it run." It does &lt;strong&gt;not&lt;/strong&gt; answer "did it run &lt;em&gt;well&lt;/em&gt;" — which for an intelligence engine (an LLM whose output is non-deterministic text) is the question that actually matters. Right now that's still manual review for me: the agent reliably &lt;em&gt;runs&lt;/em&gt;, but judging whether its strategy review was &lt;em&gt;good&lt;/em&gt; is a human gate. I haven't automated the quality check, and I'm not convinced a naive "LLM grades the LLM" loop is trustworthy enough to remove me from it yet. If you've solved non-deterministic-output health-checking for scheduled agents, I genuinely want to hear how — that's the open edge of this design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status
&lt;/h2&gt;

&lt;p&gt;ForgeOS is &lt;strong&gt;pre-release (v0.1.0)&lt;/strong&gt; and built in public — it's not on PyPI or a public repo &lt;em&gt;yet&lt;/em&gt;; today it installs from a source checkout (&lt;code&gt;pip install -e .&lt;/code&gt; from the project root). I'm writing this up now because the architecture (declarative engines + log-freshness health + self-heal, no k8s) is the reusable part, and it stands on its own whether or not you ever run my code.&lt;/p&gt;

&lt;p&gt;If you're supervising long-running or scheduled agents today — systemd? a hosted orchestrator? homegrown? — I'd love to compare notes in the comments, especially on that last open problem: how do you health-check an agent whose output is non-deterministic text?&lt;/p&gt;

</description>
      <category>python</category>
      <category>devops</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>MCP's real attack surface isn't prompt injection — it's the trust boundary (21 patterns, 5 languages)</title>
      <dc:creator>manja316</dc:creator>
      <pubDate>Fri, 19 Jun 2026 22:53:24 +0000</pubDate>
      <link>https://dev.to/manja316/mcps-real-attack-surface-isnt-prompt-injection-its-the-trust-boundary-21-patterns-5-554</link>
      <guid>https://dev.to/manja316/mcps-real-attack-surface-isnt-prompt-injection-its-the-trust-boundary-21-patterns-5-554</guid>
      <description>&lt;p&gt;We keep talking about prompt injection like it's the endgame. It isn't. Prompt injection is step one. The actual damage happens one step later, at a place most MCP threat models barely mention: the &lt;strong&gt;trust boundary&lt;/strong&gt; — the moment an injected instruction turns into a real tool call that runs with &lt;em&gt;your&lt;/em&gt; machine's privileges.&lt;/p&gt;

&lt;p&gt;I build and run a handful of MCP servers locally, and the more server source I read, the more I became convinced we're auditing the wrong layer. So I built a small MCP server whose only job is to audit &lt;em&gt;other&lt;/em&gt; MCP servers. This is the write-up of what it looks for and why — the threat model is the reusable part whether or not you ever run my code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The chain that should keep you up at night
&lt;/h2&gt;

&lt;p&gt;An MCP server is, by design, code that an LLM is &lt;em&gt;allowed&lt;/em&gt; to invoke. That's the whole point. Which means the dangerous chain is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;untrusted input → model is convinced → tool call fires → server-side code executes on your box&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The injection was never the vulnerability. The injection is just the &lt;em&gt;delivery mechanism&lt;/em&gt;. The vulnerability is what the tool &lt;strong&gt;does&lt;/strong&gt; once it's called. If the server-side handler does anything unsafe with the arguments it receives, the model has just become a remote-code-execution courier — and it did it while behaving exactly as designed.&lt;/p&gt;

&lt;p&gt;This reframes the problem in a useful way: &lt;strong&gt;most MCP security is just appsec, newly reachable through a model.&lt;/strong&gt; The bugs that matter are 20-year-old classics. What changed is who can reach them. Pre-MCP, a lot of this code was only callable by a trusted caller. Now a sentence in a webpage your agent reads can reach it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The patterns that keep showing up
&lt;/h2&gt;

&lt;p&gt;Reading through MCP server source, the same handful of mistakes appear over and over. These are the OWASP greatest hits, scored by how badly they bite when they sit one tool-call away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eval() / exec() / os.system() on tool arguments     CRITICAL  direct code injection
subprocess(..., shell=True), Runtime.exec(concat)    HIGH      command injection
pickle.load / torch.load / ObjectInputStream         HIGH      deserialization RCE
yaml.load() without SafeLoader                        HIGH      object instantiation
f-string / string-concat SQL                          HIGH      SQL injection
URL built by concatenation (fetch tools)              MEDIUM    SSRF / network pivot
hardcoded API keys / tokens in server source          MEDIUM    credential leak
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of these are exotic. That's the point. The scanner I built detects &lt;strong&gt;21 vulnerability patterns across Python, Java, Go, C++, and Rust&lt;/strong&gt; — and almost every one predates LLMs entirely. The novelty isn't the bug class; it's the reachability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design decision that mattered most: purpose-aware severity
&lt;/h2&gt;

&lt;p&gt;The naive version of a scanner like this is a glorified &lt;code&gt;grep eval&lt;/code&gt;. It floods you with false positives and you stop reading the output by day two. An &lt;code&gt;eval()&lt;/code&gt; inside a sandboxed test harness is not the same finding as an &lt;code&gt;eval()&lt;/code&gt; on a tool's input argument — and a scanner that scores them identically is noise.&lt;/p&gt;

&lt;p&gt;So the core of the tool is &lt;strong&gt;purpose-aware scoring&lt;/strong&gt;: it weighs a pattern by where it sits and what reaches it. A &lt;code&gt;subprocess(shell=True)&lt;/code&gt; in a CI helper that never touches model input is a low-priority note. The same call wired to a tool argument is a CRITICAL. Getting this right is the difference between a report someone acts on and a report someone closes.&lt;/p&gt;

&lt;p&gt;The scanner ships as an MCP server itself, with three tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;audit_repo&lt;/code&gt; — point it at a GitHub URL, get a scored report&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;audit_code&lt;/code&gt; — paste a snippet, get findings inline&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;list_patterns&lt;/code&gt; — see every pattern and its severity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because it runs locally as an MCP server in Claude Desktop, your agent can audit a repo &lt;em&gt;before you wire it in&lt;/em&gt; — which is the right time to find out a server you're about to trust shells out on its arguments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;mcp-security-audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Two lessons that generalize beyond MCP
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The injection is not the vuln — the handler is.&lt;/strong&gt; Spend your defensive budget on what tool code does with arguments, not only on filtering what reaches the model. Input filtering is a sieve; a safe handler is a wall. You want the wall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Severity without context is noise that trains people to ignore you.&lt;/strong&gt; Any scanner that can't tell a sandboxed &lt;code&gt;eval&lt;/code&gt; from a reachable one will get muted. Context-awareness isn't a nice-to-have on a security tool — it's the feature that decides whether anyone reads the second report.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part: where this still falls short
&lt;/h2&gt;

&lt;p&gt;Static patterns catch reachable-by-shape, not reachable-in-fact. A taint analysis that proves an argument actually flows into a dangerous sink would cut false positives further — that's the open edge of this design, and it's not built yet. And the two pattern families I'm least confident I've covered well are &lt;strong&gt;path traversal&lt;/strong&gt; in file-serving tools and &lt;strong&gt;SSRF&lt;/strong&gt; in fetch-style tools that are &lt;em&gt;built&lt;/em&gt; to make outbound requests, where "is this call malicious" is genuinely ambiguous. If you've solved the fetch-tool SSRF problem cleanly, I want to hear how.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to find it
&lt;/h2&gt;

&lt;p&gt;The scanner is open-source (MIT) and free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo + star:&lt;/strong&gt; &lt;a href="https://github.com/LuciferForge/mcp-security-audit" rel="noopener noreferrer"&gt;https://github.com/LuciferForge/mcp-security-audit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install:&lt;/strong&gt; &lt;code&gt;pip install mcp-security-audit&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd rather not run it yourself — if you're shipping an MCP server and want a scored, human-reviewed audit report with a 3-day turnaround instead of a raw scan dump — I offer that as a paid service here: &lt;a href="https://manja8.gumroad.com/l/mcp-security-audit?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mcp-security-audit-2026-06-22" rel="noopener noreferrer"&gt;MCP Security Audit Report — $29&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But I'm more interested in the threat-model discussion than either. Two open questions for the comments:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Where should the boundary live?&lt;/strong&gt; Sandbox every tool call by default (perf + DX cost) or scan-and-trust at install time? I lean install-time scanning plus runtime allow-lists, and I'd like to be talked out of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What pattern am I missing?&lt;/strong&gt; I've got 21. Tell me the 22nd.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>python</category>
      <category>mcp</category>
    </item>
    <item>
      <title>How much edge is left in the final 48h of a Polymarket? I measured price convergence on 18.6M snapshots</title>
      <dc:creator>manja316</dc:creator>
      <pubDate>Thu, 18 Jun 2026 22:54:17 +0000</pubDate>
      <link>https://dev.to/manja316/how-much-edge-is-left-in-the-final-48h-of-a-polymarket-i-measured-price-convergence-on-186m-51bi</link>
      <guid>https://dev.to/manja316/how-much-edge-is-left-in-the-final-48h-of-a-polymarket-i-measured-price-convergence-on-186m-51bi</guid>
      <description>&lt;p&gt;A question I kept circling when I started treating prediction markets as a trading venue: by the time a market is &lt;em&gt;obviously&lt;/em&gt; heading one way, is the move already priced in — or is there still a systematic gap between price and outcome you can lean on?&lt;/p&gt;

&lt;p&gt;"Buy the favorite at 0.85, collect the 0.15" sounds like free money until you account for the markets where the favorite loses. So I stopped eyeballing it and computed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data
&lt;/h2&gt;

&lt;p&gt;I've been archiving Polymarket order books every 15 minutes since late March 2026. As of mid-June that's &lt;strong&gt;~18.6M price snapshots across ~18.6k markets&lt;/strong&gt; (~77 days, most of them resolved). Prediction markets are one of the few venues where you get the probability estimate, the &lt;em&gt;full path&lt;/em&gt; it took, &lt;strong&gt;and&lt;/strong&gt; the realized 0/1 outcome — so convergence is actually measurable instead of hand-waved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement: how far is price from truth, as a function of time-to-resolution?
&lt;/h2&gt;

&lt;p&gt;For every resolved market, line up each snapshot by hours-to-resolution, then measure the mean absolute error between price and the eventual outcome in each time bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="c1"&gt;# df: rows of (market_id, ts, price_yes, resolved_outcome[0/1], resolve_ts)
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hrs_to_res&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolve_ts&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abs_err&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price_yes&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolved_outcome&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;bins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;168&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e9&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;window&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hrs_to_res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bins&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;conv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;window&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;mae&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abs_err&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;brier&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abs_err&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;  &lt;span class="c1"&gt;# abs_err == |p - outcome|
&lt;/span&gt;    &lt;span class="n"&gt;n_snaps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abs_err&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing. No model, no backtest — just a descriptive stat that most people assume the shape of instead of computing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the curve actually shows
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error shrinks fast in the last 24h, but it is &lt;em&gt;not&lt;/em&gt; zero at the buzzer.&lt;/strong&gt; There's a non-trivial residual right up to resolution — and that residual is concentrated in exactly the set of markets where "the obvious favorite" flips. Those tail markets are where a naive favorite-buying strategy quietly bleeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The 24–48h bucket is the interesting one for a trader.&lt;/strong&gt; Most of the information has arrived, spreads have tightened — yet realized error is still meaningfully above the last-hour bucket. That gap is the thing to interrogate: is it edge, or is it just the variance of unresolved coin-flips that &lt;em&gt;look&lt;/em&gt; settled?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The caveats people skip (and why I'm posting the method, not a strategy)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Survivorship / resolution-time leakage.&lt;/strong&gt; &lt;code&gt;resolve_ts&lt;/code&gt; is known only ex-post. If you bucket by hours-to-resolution you're implicitly conditioning on a market that &lt;em&gt;did&lt;/em&gt; resolve when it did — fine for a descriptive convergence curve, fatal if you turn it into a live signal without a causal "what did I know at time &lt;em&gt;t&lt;/em&gt;" cut.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Liquidity ≠ midprice.&lt;/strong&gt; The curve above uses last-trade / mid. The order book (I store depth too) tells a different story near the buzzer: getting size off at the "converged" price is often impossible, and that illiquidity is precisely where the residual error lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Resolution risk is not modeled.&lt;/strong&gt; Some of that terminal error is genuine ambiguity or dispute, not mispricing. You can't arb a market whose &lt;em&gt;resolution criteria themselves&lt;/em&gt; are the uncertain part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;I'm not claiming an edge. I'm claiming the &lt;strong&gt;measurement is cheap and most people eyeball it instead of computing it.&lt;/strong&gt; If you trade these, the convergence curve plus an honest point-in-time cut is the first thing worth building — before any strategy logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;The data behind this is open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free API, no signup:&lt;/strong&gt; &lt;a href="https://api.protodex.io" rel="noopener noreferrer"&gt;api.protodex.io&lt;/a&gt; — &lt;code&gt;/stats&lt;/code&gt;, &lt;code&gt;/markets&lt;/code&gt;, &lt;code&gt;/market/{id}&lt;/code&gt;, &lt;code&gt;/prices&lt;/code&gt;, &lt;code&gt;/orderbook&lt;/code&gt;, &lt;code&gt;/categories&lt;/code&gt;. Enough to rebuild the convergence curve yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full historical archive&lt;/strong&gt; (18.7M+ snapshots, depth-level order books, resolved outcomes — the version I run this analysis on): &lt;a href="https://manja8.gumroad.com/l/polymarket-quant-toolkit?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=polymarket-data-week25" rel="noopener noreferrer"&gt;Polymarket Quant Toolkit dataset&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What's your experience with the last-48h window — real residual edge, or just liquidity-trapped variance? I'd genuinely like to compare notes in the comments.&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>trading</category>
      <category>statistics</category>
    </item>
    <item>
      <title>When Polymarket says 70%, does it happen 70%? Why price-only data can't answer that — and what it can</title>
      <dc:creator>manja316</dc:creator>
      <pubDate>Tue, 16 Jun 2026 10:51:24 +0000</pubDate>
      <link>https://dev.to/manja316/when-polymarket-says-70-does-it-happen-70-of-the-time-i-checked-against-194m-price-snapshots-3enj</link>
      <guid>https://dev.to/manja316/when-polymarket-says-70-does-it-happen-70-of-the-time-i-checked-against-194m-price-snapshots-3enj</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Correction (2026-06-27):&lt;/strong&gt; An earlier version of this post implied the dataset ships with per-market 0/1 resolution labels and that I had measured calibration directly. That was wrong, and I'd rather fix it loudly than quietly. &lt;strong&gt;This is a &lt;em&gt;price&lt;/em&gt; dataset — it does not contain settled outcome labels.&lt;/strong&gt; True calibration needs realized outcomes you have to join in from an external source. Below is the honest version: what price-only data &lt;em&gt;can&lt;/em&gt; tell you, the legit proxy it gives you, and exactly what you'd have to add to score real calibration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you trade, model, or just read prediction markets, there's one question that decides whether the price means anything: &lt;strong&gt;when the market says 70%, does the thing actually happen about 70% of the time?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's &lt;em&gt;calibration&lt;/em&gt;, and it's the single most decision-relevant property of any probabilistic forecaster. A market can be liquid, popular, and heavily traded and still be systematically wrong in a way that's invisible until you score it against what actually resolved.&lt;/p&gt;

&lt;p&gt;Here's the honest catch I have to lead with: &lt;strong&gt;measuring calibration needs two things — a dense price history AND the realized outcome of each market.&lt;/strong&gt; I have the first cleanly. The second is &lt;em&gt;not&lt;/em&gt; in this dataset, and any vendor (me included) who shows you a "calibration curve" derived from prices alone is measuring the market against itself, not against reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dataset (what's actually in it)
&lt;/h2&gt;

&lt;p&gt;Since late March I've logged Polymarket every 15 minutes. The frozen export holds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;22,410&lt;/strong&gt; markets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;18,611,636&lt;/strong&gt; price snapshots (≈831 per market with a series)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1,856,388&lt;/strong&gt; order-book snapshots&lt;/li&gt;
&lt;li&gt;15-minute cadence, &lt;strong&gt;92 continuous days&lt;/strong&gt; (2026-03-28 → 2026-06-28)&lt;/li&gt;
&lt;li&gt;Each row: market id, timestamp, yes-price, plus volume / liquidity / best bid-ask features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it does &lt;strong&gt;not&lt;/strong&gt; carry: a per-market settled &lt;code&gt;0/1&lt;/code&gt; outcome. &lt;code&gt;SELECT COUNT(*) FROM markets WHERE resolved=1&lt;/code&gt; returns &lt;strong&gt;0&lt;/strong&gt; for all 22,410 markets in the export — these are price paths, not graded results. So you cannot, from this file alone, compute "of the times the price sat at 70%, how often did the event happen." You need the resolution labels, and those live outside the price feed (Polymarket's resolution / the on-chain settlement).&lt;/p&gt;

&lt;h2&gt;
  
  
  What price-only data CAN give you: the convergence proxy
&lt;/h2&gt;

&lt;p&gt;You don't get true calibration, but you &lt;em&gt;do&lt;/em&gt; get a useful, honest proxy. Of the &lt;strong&gt;7,101&lt;/strong&gt; markets that ended inside the window &lt;em&gt;and&lt;/em&gt; still carry a terminal price series, &lt;strong&gt;6,836 (96.3%)&lt;/strong&gt; closed decisively — last yes-price ≥ 0.95 or ≤ 0.05. Only &lt;strong&gt;265 (3.7%)&lt;/strong&gt; were still mushy in the middle at the end. &lt;em&gt;(Denominator matters: measured across **all 19,584&lt;/em&gt;* markets whose end-date fell in the window — many with no live terminal quote — the decisive share drops sharply, so always state which denominator you mean.)*&lt;/p&gt;

&lt;p&gt;That terminal price is a &lt;em&gt;noisy&lt;/em&gt; stand-in for the outcome: ~96.3% of the time the market made up its mind hard enough that "did it close near 1?" is a defensible label. It's not ground truth (the 3.7% ambiguous tail and any post-close revision are exactly where it breaks), but it lets you study the &lt;em&gt;shape&lt;/em&gt; of price convergence — how and when a market sharpens — which is genuinely informative on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real calibration measurement (what you'd add)
&lt;/h2&gt;

&lt;p&gt;The classic check is a &lt;strong&gt;reliability diagram&lt;/strong&gt;, and the method is fine — it just needs labels you join in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get realized outcomes for resolved markets &lt;strong&gt;from an external resolution source&lt;/strong&gt; (the price feed won't give them to you).&lt;/li&gt;
&lt;li&gt;Bin every historical price into deciles (0–10%, 10–20%, … 90–100%).&lt;/li&gt;
&lt;li&gt;For each bin, compute the empirical resolution rate — of all the times the price sat in that bin, how often did the event actually happen?&lt;/li&gt;
&lt;li&gt;Plot empirical rate vs. stated price. Perfect calibration is the 45° diagonal.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.protodex.io&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# free price API, no signup
&lt;/span&gt;
&lt;span class="c1"&gt;# Prices come from the dataset/API. LABELS DO NOT — you must supply them.
# `resolutions` here is an external {market_id: 0/1} you join in yourself
# (Polymarket resolution / on-chain settlement). It is NOT in this price feed.
&lt;/span&gt;&lt;span class="n"&gt;resolutions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_external_resolution_labels&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# &amp;lt;-- the part you provide
&lt;/span&gt;
&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;market_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resolutions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;prices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/prices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;market_id&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yes_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outcome&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;reliability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outcome&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# empirical rate per decile
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reliability&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# compare each row to its bin midpoint -&amp;gt; the diagonal
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only have the price feed and substitute the convergence proxy for &lt;code&gt;label&lt;/code&gt;, be explicit that you're scoring the market against its own terminal price — a different, weaker claim than calibration against reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to look for (and the trap)
&lt;/h2&gt;

&lt;p&gt;The literature on real-money markets has a well-documented signature: the &lt;strong&gt;favorite–longshot bias&lt;/strong&gt; — longshots tend to be overpriced, heavy favorites slightly underpriced. &lt;em&gt;But you cannot confirm it from prices alone.&lt;/em&gt; It's a statement about realized outcomes vs. price, so it lives or dies on the labels you join in. Same for any Brier-score-over-time trajectory: great question, needs ground truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No resolution labels in the dataset.&lt;/strong&gt; The single biggest limit — restated because it's the one most listings hide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convergence ≠ truth.&lt;/strong&gt; The 96.3% terminal-decisiveness proxy mislabels the ambiguous tail and ignores any post-close correction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Survivorship / selection.&lt;/strong&gt; Ended markets ≠ all markets; conditioning on resolution biases some analyses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mid ≠ executable price.&lt;/strong&gt; Fees and spread are real the moment you make a trading claim.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;86 days is one window, not multiple regimes.&lt;/strong&gt; Treat any "markets are calibrated" conclusion as provisional.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Get the data
&lt;/h2&gt;

&lt;p&gt;The free read-only API reproduces the price side with no signup:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://api.protodex.io&lt;/code&gt; — endpoints &lt;code&gt;/stats&lt;/code&gt;, &lt;code&gt;/markets&lt;/code&gt;, &lt;code&gt;/market/{id}&lt;/code&gt;, &lt;code&gt;/prices&lt;/code&gt;, &lt;code&gt;/orderbook&lt;/code&gt;, &lt;code&gt;/categories&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you'd rather not page the API market-by-market and want the full 18.6M-snapshot, 92-day price history as one indexed SQLite file for offline work, the one-time archive is here: &lt;strong&gt;&lt;a href="https://manja8.gumroad.com/l/agyjd?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=polymarket-data-2026-06-29" rel="noopener noreferrer"&gt;Polymarket Historical Price Dataset&lt;/a&gt;&lt;/strong&gt;. It's price history, honestly scoped — bring your own resolution labels if calibration is the goal.&lt;/p&gt;

&lt;p&gt;I'd genuinely like to know how others source clean resolution labels for prediction-market calibration work — that's the open edge here. If you've benchmarked a market's calibration, where did your ground truth come from?&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>python</category>
      <category>statistics</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>How to backtest a Polymarket strategy with free 15-minute historical data</title>
      <dc:creator>manja316</dc:creator>
      <pubDate>Wed, 10 Jun 2026 22:51:26 +0000</pubDate>
      <link>https://dev.to/manja316/how-to-backtest-a-polymarket-strategy-with-free-15-minute-historical-data-3hm0</link>
      <guid>https://dev.to/manja316/how-to-backtest-a-polymarket-strategy-with-free-15-minute-historical-data-3hm0</guid>
      <description>&lt;p&gt;Most people backtest prediction-market strategies wrong, and it's not their fault — the data to do it right is annoying to assemble. You need &lt;em&gt;time series&lt;/em&gt; per contract (not just the final resolution), aligned to a clock, with the resolution label attached so you know who won. Polymarket's API gives you the live order book, but the moment a market resolves, that history is gone from where most people look.&lt;/p&gt;

&lt;p&gt;So here's a clean recipe. Free data, ~40 lines of pandas, and the caveats that separate a backtest you can trust from one that lies to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  0. The data
&lt;/h2&gt;

&lt;p&gt;You need a price history per market at a fixed interval. I've been archiving Polymarket at a 15-minute cadence since late March — &lt;strong&gt;18.6M+ price snapshots across 22,410 markets, 92 days of history&lt;/strong&gt; as of this writing. The live market index is free to browse at &lt;a href="https://protodex.io?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=polymarket-data-2026-06-30" rel="noopener noreferrer"&gt;protodex.io&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One honest caveat before you write a line of code:&lt;/strong&gt; this is &lt;em&gt;price&lt;/em&gt; history, not officially-labeled outcomes. Polymarket's public API does not hand you a clean post-hoc &lt;code&gt;resolved_yes&lt;/code&gt; flag, so you derive the outcome from where the price ends up — and roughly 5% of ended markets never converge decisively, so they stay ambiguous and must be dropped, not guessed. That limitation is the single most important thing to understand before you trust any backtest built on this (or any price-only prediction-market dataset). I measured it: 94.6% of ended markets close at ≥0.95 or ≤0.05; the rest are coin-flips you cannot score.&lt;/p&gt;

&lt;p&gt;You can roll your own collector against the Polymarket CLOB + Gamma APIs (the snapshot loop is maybe 60 lines), or skip the three-month wait and grab the parquet bundle — link at the end. Either way, the analysis below is identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Load it
&lt;/h2&gt;

&lt;p&gt;Assume a parquet with columns &lt;code&gt;market_id&lt;/code&gt;, &lt;code&gt;timestamp&lt;/code&gt;, and &lt;code&gt;price&lt;/code&gt; (the YES probability, 0–1). There is &lt;strong&gt;no&lt;/strong&gt; ground-truth &lt;code&gt;resolved_yes&lt;/code&gt; column — you derive a proxy outcome from each market's terminal price, and you throw away anything that didn't converge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;polymarket_history.parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Derive a PROXY outcome from the terminal price — there is no official label.
# End &amp;gt;= 0.95 -&amp;gt; treat YES, &amp;lt;= 0.05 -&amp;gt; treat NO; anything in between never
# converged, so it's unscoreable and gets dropped (NOT guessed at 0.5).
&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;final_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolved_yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NA&lt;/span&gt;
&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;final_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolved_yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;final_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolved_yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolved_yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;how&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Keep only markets that converged decisively — you can't score a coin-flip
&lt;/span&gt;&lt;span class="n"&gt;resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dropna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolved_yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;nunique&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decisively-converged markets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be honest with yourself about what this proxy is: a circularity check is worth doing here — you're using the &lt;em&gt;late&lt;/em&gt; price to label the outcome and the &lt;em&gt;early&lt;/em&gt; price as the signal, so as long as your entry snapshot is well before convergence (see §2) the label isn't leaking your signal. But it is still a convergence proxy, not an official settlement, and it silently excludes the ~5% messy markets. State that in any result you publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The strategy: fade the longshots
&lt;/h2&gt;

&lt;p&gt;The favorite-longshot bias is the most-documented inefficiency in betting markets: longshots (low-probability contracts) are systematically &lt;em&gt;over&lt;/em&gt;-priced, favorites slightly &lt;em&gt;under&lt;/em&gt;-priced. Translated to Polymarket: a contract trading at 8¢ resolves YES less than 8% of the time on average. So a naive edge is &lt;strong&gt;short the longshots&lt;/strong&gt; (buy NO when YES is cheap).&lt;/p&gt;

&lt;p&gt;Let's test whether that holds in the snapshots. Take each market's price in a chosen band at a fixed point before resolution, and compare to the realized outcome.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# For each market, grab the last snapshot at least 24h before its final timestamp
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;snapshot_24h_out&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iloc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="n"&gt;picks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;group_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot_24h_out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dropna&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;# Longshot band: YES priced 2–15¢
&lt;/span&gt;&lt;span class="n"&gt;band&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;picks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;implied&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;band&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;# what the market said
&lt;/span&gt;&lt;span class="n"&gt;realized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;band&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolved_yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# what actually happened
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;implied YES &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;implied&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; vs realized YES &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;realized&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;realized &amp;lt; implied&lt;/code&gt;, the longshots were overpriced — the bias is present and shorting them has positive expectancy &lt;em&gt;before costs&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The part most backtests skip — costs and survivorship
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A backtest without these three corrections is marketing, not research.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;1. Spread + fees.&lt;/strong&gt; You don't trade at the mid. On thin Polymarket longshots the bid/ask can be 2–4¢ wide. A 1¢ edge on a 6¢ contract evaporates the moment you cross a 3¢ spread. Always subtract a realistic fill cost from the implied price before scoring P&amp;amp;L.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Survivorship / resolution timing.&lt;/strong&gt; If your archive only kept markets that resolved cleanly, you've dropped the messy ones (extended, disputed, voided) — and those aren't random. Score against &lt;em&gt;every&lt;/em&gt; market that hit your entry filter, not just the tidy winners.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Liquidity ceiling.&lt;/strong&gt; A 20¢ edge on a market with $300 of depth is a $60 edge, not a strategy. Weight every backtested position by the order-book depth at entry, or you'll "discover" an edge you can't actually fill. (This is exactly why snapshot data needs the &lt;em&gt;order book&lt;/em&gt;, not just last-price — depth is the difference between a paper edge and a real one.)&lt;/p&gt;

&lt;p&gt;Do these three and the favorite-longshot edge usually survives — but smaller than the raw number, and only in the deeper markets. That gap &lt;em&gt;is&lt;/em&gt; the finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Why 15-minute snapshots specifically
&lt;/h2&gt;

&lt;p&gt;Tick data is overkill for this and a nightmare to store; daily closes are too coarse to catch the late convergence where most of the price action lives (markets snap toward 0/1 in the final hours). 15 minutes is the sweet spot: dense enough to study convergence and intraday moves, sparse enough that three months fits in a few hundred MB of parquet.&lt;/p&gt;




&lt;p&gt;If you'd rather not run a collector for three months before you can test a single idea, I've packaged the full archive — parquet + CSV with resolution labels included, plus a ready-to-run Python notebook that runs the exact favorite-longshot backtest above, as a one-time purchase:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://manja8.gumroad.com/l/agyjd?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=polymarket-data-2026-06-30" rel="noopener noreferrer"&gt;Polymarket Quant Toolkit — 18.6M-snapshot dataset + analysis notebook ($49)&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's price history with the convergence-proxy approach above already wired into the notebook — no over-promised resolution labels, just the honest version of the backtest. And the live market index is free to poke at first: &lt;a href="https://protodex.io?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=polymarket-data-2026-06-30" rel="noopener noreferrer"&gt;protodex.io&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What strategy would you test first — favorite-longshot, late-convergence momentum, or cross-market arbitrage? Drop it in the comments and I'll point you at the columns you'd need.&lt;/p&gt;

</description>
      <category>trading</category>
      <category>python</category>
      <category>datascience</category>
      <category>data</category>
    </item>
    <item>
      <title>I archived 19.1M Polymarket price snapshots. Three things the data shows that the order book hides.</title>
      <dc:creator>manja316</dc:creator>
      <pubDate>Tue, 09 Jun 2026 10:51:30 +0000</pubDate>
      <link>https://dev.to/manja316/i-archived-179m-polymarket-price-snapshots-three-things-the-data-shows-that-the-order-book-hides-do4</link>
      <guid>https://dev.to/manja316/i-archived-179m-polymarket-price-snapshots-three-things-the-data-shows-that-the-order-book-hides-do4</guid>
      <description>&lt;p&gt;If you only ever look at a prediction market through its live order book, you see one number: the current price. That number is the market's best guess at a probability — "73¢ = ~73% YES." Useful, but it throws away everything about &lt;em&gt;how the market got there&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I've been archiving Polymarket order-book snapshots every 15 minutes for about eleven weeks. The dataset is now &lt;strong&gt;19.1M+ price points across ~19,000 markets&lt;/strong&gt;, most of them resolved (so we know the ground-truth outcome). Looking at prediction markets in bulk, instead of one at a time, surfaces structure that's invisible tick-by-tick. Three findings worth your time.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Favorite-longshot bias is real here too
&lt;/h2&gt;

&lt;p&gt;The oldest result in betting-market research is the &lt;strong&gt;favorite-longshot bias&lt;/strong&gt;: longshots are systematically overpriced and heavy favorites are systematically underpriced. People overpay for the lottery-ticket thrill of a 5¢ "it could happen" and underpay for the boring 95¢ near-certainty.&lt;/p&gt;

&lt;p&gt;It shows up cleanly when you bucket thousands of resolved markets by their pre-resolution price and compare each bucket's &lt;em&gt;implied&lt;/em&gt; probability to its &lt;em&gt;actual&lt;/em&gt; hit rate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contracts trading in the &lt;strong&gt;2–10¢&lt;/strong&gt; band resolve YES &lt;strong&gt;less often&lt;/strong&gt; than their price implies — the longshot tax.&lt;/li&gt;
&lt;li&gt;Contracts in the &lt;strong&gt;90–98¢&lt;/strong&gt; band resolve YES &lt;strong&gt;slightly more often&lt;/strong&gt; than their price implies — favorites are a hair cheap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You cannot see this in one market. You can only see it across a thousand resolved ones — which is exactly what a bulk archive is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Convergence is late and lumpy, not smooth
&lt;/h2&gt;

&lt;p&gt;Intuition says a market should drift smoothly toward 0 or 1 as resolution nears. The data says otherwise: most markets sit in a noisy band for the majority of their life and then &lt;strong&gt;convergence happens in a short burst&lt;/strong&gt; near the resolving event — a debate, an earnings print, an election night.&lt;/p&gt;

&lt;p&gt;The practical implication: time-to-resolution matters more than price level when you're reasoning about how much a contract can still move. A 60¢ market with three weeks left and a 60¢ market with three hours left are completely different objects, and a single live quote can't tell them apart. A timestamped history can.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Volume ≠ movement
&lt;/h2&gt;

&lt;p&gt;The markets with the biggest 24h &lt;strong&gt;price moves&lt;/strong&gt; are usually not the ones with the biggest &lt;strong&gt;volume&lt;/strong&gt;. High-volume markets are liquid and efficient — lots of participants, tight spreads, slow to move. The violent re-pricings happen in &lt;strong&gt;thinner&lt;/strong&gt; markets where a single piece of news has nobody on the other side to absorb it.&lt;/p&gt;

&lt;p&gt;If you screen for "interesting" markets by volume, you'll mostly find markets that have already finished being interesting. Screening by &lt;em&gt;realized movement relative to liquidity&lt;/em&gt; finds the ones still in motion. (I built a free screener around exactly this idea — separate post.)&lt;/p&gt;

&lt;h2&gt;
  
  
  How to reproduce this yourself
&lt;/h2&gt;

&lt;p&gt;Every claim above is just &lt;code&gt;pandas&lt;/code&gt; over a long-format table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;polymarket_snapshots.parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# columns: market_id, timestamp, price_yes, volume, resolved_outcome
&lt;/span&gt;
&lt;span class="c1"&gt;# Favorite-longshot calibration: take each market's last pre-resolution price,
# bucket it, and compare implied probability to the actual YES rate.
&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dropna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolved_outcome&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price_yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;calib&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;implied&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price_yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolved_outcome&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;market_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;calib&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# actual &amp;lt; implied for low buckets = longshot bias
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hard part isn't the analysis — it's getting clean, timestamped, resolution-labeled history out of an API that's built for live trading, not bulk export. That collection problem is the whole reason this dataset exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dataset
&lt;/h2&gt;

&lt;p&gt;If you want the raw archive instead of building the collector yourself, it's here:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://manja8.gumroad.com/l/polymarket-quant-toolkit?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=polymarket-data-2026-07" rel="noopener noreferrer"&gt;Polymarket Full Dataset — 18M+ price snapshots, 22,000+ markets&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Parquet + CSV, 15-minute snapshot cadence across the full market history, one-time purchase. If you just want to poke at the live numbers first, the market index is free at &lt;a href="https://protodex.io?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=polymarket-data-2026-07" rel="noopener noreferrer"&gt;protodex.io&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What would you check first with 19.1M+ labeled prediction-market snapshots? I'll run reader requests against the archive and post the results.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>trading</category>
      <category>python</category>
      <category>data</category>
    </item>
  </channel>
</rss>
