<?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: Dozor</title>
    <description>The latest articles on DEV Community by Dozor (dozor).</description>
    <link>https://dev.to/dozor</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%2Forganization%2Fprofile_image%2F15127%2F5c00ff19-e89e-4b39-aa9a-bb0b0056f55f.png</url>
      <title>DEV Community: Dozor</title>
      <link>https://dev.to/dozor</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dozor"/>
    <language>en</language>
    <item>
      <title>Dota 2 and League of Legends Arbitrage: Why Slower Games Give Longer Windows</title>
      <dc:creator>Dozor | Esports Surebet Scanner</dc:creator>
      <pubDate>Mon, 26 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/dozor/dota-2-and-league-of-legends-arbitrage-why-slower-games-give-longer-windows-4719</link>
      <guid>https://dev.to/dozor/dota-2-and-league-of-legends-arbitrage-why-slower-games-give-longer-windows-4719</guid>
      <description>&lt;p&gt;Do slower games give longer arbitrage windows? Partly. In 30 days of scanner history (Sep 6 – Oct 6, 2026), &lt;strong&gt;59.1% of League of Legends arbs and 43.1% of Dota 2 arbs lasted 5 seconds or more&lt;/strong&gt;, against 27.0% in CS2 and 29.0% in Valorant. But much of that gap is mix, not tempo: LoL and Dota 2 have far more prematch arbs than CS2, and prematch arbs last longer. At CS2's live/prematch mix, LoL falls to 38.0% and Dota 2 to 34.1%. The windows are still longer, by 7 to 11 points rather than 16 to 32. Below: the numbers by game, market and exchange leg, and the hypotheses we think explain them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dataset and method
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Window:&lt;/strong&gt; 30 days, Sep 6 – Oct 6, 2026. Esports only, 27 books: 25 sportsbooks and crypto sportsbooks plus the exchanges Polymarket and Kalshi. About 4.87 million arbs in total; this article uses six games.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lifetime&lt;/strong&gt; of an arb is last sighting minus first sighting, with a step of about 2 seconds. A median of 0 s means most arbs were seen in a single snapshot. We call 5 seconds or more a "working window". It is our threshold, not a promise a bet can be placed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ROI&lt;/strong&gt; is the best ROI the arb reached. Exchange commissions are already taken off it, per market.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exchange leg&lt;/strong&gt; means Polymarket or Kalshi is one of the books in the arb.&lt;/li&gt;
&lt;li&gt;Each line of a market counts as its own arb, so counts show how often books disagree, not how many independent chances there were.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Finding 1: esports arbitrage windows by game
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Game&lt;/th&gt;
&lt;th&gt;Arbs&lt;/th&gt;
&lt;th&gt;Median life&lt;/th&gt;
&lt;th&gt;≥5 s&lt;/th&gt;
&lt;th&gt;≥10 s&lt;/th&gt;
&lt;th&gt;≥60 s&lt;/th&gt;
&lt;th&gt;Median ROI&lt;/th&gt;
&lt;th&gt;Live share&lt;/th&gt;
&lt;th&gt;With exchange leg&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CS2&lt;/td&gt;
&lt;td&gt;3,617,804&lt;/td&gt;
&lt;td&gt;2.0 s&lt;/td&gt;
&lt;td&gt;27.0%&lt;/td&gt;
&lt;td&gt;18.5%&lt;/td&gt;
&lt;td&gt;2.9%&lt;/td&gt;
&lt;td&gt;3.22%&lt;/td&gt;
&lt;td&gt;90.7%&lt;/td&gt;
&lt;td&gt;12.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Valorant&lt;/td&gt;
&lt;td&gt;228,529&lt;/td&gt;
&lt;td&gt;2.0 s&lt;/td&gt;
&lt;td&gt;29.0%&lt;/td&gt;
&lt;td&gt;21.4%&lt;/td&gt;
&lt;td&gt;5.4%&lt;/td&gt;
&lt;td&gt;2.69%&lt;/td&gt;
&lt;td&gt;87.0%&lt;/td&gt;
&lt;td&gt;12.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dota 2&lt;/td&gt;
&lt;td&gt;548,410&lt;/td&gt;
&lt;td&gt;4.1 s&lt;/td&gt;
&lt;td&gt;43.1%&lt;/td&gt;
&lt;td&gt;30.3%&lt;/td&gt;
&lt;td&gt;6.1%&lt;/td&gt;
&lt;td&gt;2.16%&lt;/td&gt;
&lt;td&gt;49.6%&lt;/td&gt;
&lt;td&gt;25.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;League of Legends&lt;/td&gt;
&lt;td&gt;359,789&lt;/td&gt;
&lt;td&gt;8.4 s&lt;/td&gt;
&lt;td&gt;59.1%&lt;/td&gt;
&lt;td&gt;49.9%&lt;/td&gt;
&lt;td&gt;15.4%&lt;/td&gt;
&lt;td&gt;1.82%&lt;/td&gt;
&lt;td&gt;37.3%&lt;/td&gt;
&lt;td&gt;42.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile Legends&lt;/td&gt;
&lt;td&gt;25,099&lt;/td&gt;
&lt;td&gt;10.1 s&lt;/td&gt;
&lt;td&gt;61.5%&lt;/td&gt;
&lt;td&gt;50.1%&lt;/td&gt;
&lt;td&gt;9.5%&lt;/td&gt;
&lt;td&gt;1.74%&lt;/td&gt;
&lt;td&gt;37.4%&lt;/td&gt;
&lt;td&gt;6.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rainbow Six&lt;/td&gt;
&lt;td&gt;66,937&lt;/td&gt;
&lt;td&gt;14.2 s&lt;/td&gt;
&lt;td&gt;60.7%&lt;/td&gt;
&lt;td&gt;54.0%&lt;/td&gt;
&lt;td&gt;11.1%&lt;/td&gt;
&lt;td&gt;1.99%&lt;/td&gt;
&lt;td&gt;58.4%&lt;/td&gt;
&lt;td&gt;13.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flarnbxb3d6uzb4gx3v7c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flarnbxb3d6uzb4gx3v7c.png" alt="Share of arbs lasting 5 and 60 seconds by game: CS2 27% and 2.9%, Dota 2 43% and 6.1%, LoL 59% and 15%" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The ranking by median life roughly matches the ranking by tail. It also roughly matches the ranking by margin, in reverse: the median ROI is 3.22% in CS2, 2.16% in Dota 2 and 1.82% in LoL. Longer windows come with thinner edges. More on the titles: &lt;a href="https://dozor.bet/en/esports" rel="noopener noreferrer"&gt;esports arbitrage&lt;/a&gt; and the &lt;a href="https://dozor.bet/en/blog/dota-2-surebets" rel="noopener noreferrer"&gt;Dota 2 surebets guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 2: a large part of the gap is live vs prematch mix
&lt;/h2&gt;

&lt;p&gt;Live and prematch arbs behave differently, and the games have very different mixes. 90.7% of CS2 arbs are live; for Dota 2 it is 49.6% and for LoL 37.3%. So we split by status.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Game&lt;/th&gt;
&lt;th&gt;Live share&lt;/th&gt;
&lt;th&gt;≥5 s, live&lt;/th&gt;
&lt;th&gt;≥5 s, prematch&lt;/th&gt;
&lt;th&gt;≥5 s, all&lt;/th&gt;
&lt;th&gt;≥5 s at CS2's mix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CS2&lt;/td&gt;
&lt;td&gt;90.7%&lt;/td&gt;
&lt;td&gt;23.0%&lt;/td&gt;
&lt;td&gt;66.3%&lt;/td&gt;
&lt;td&gt;27.0%&lt;/td&gt;
&lt;td&gt;27.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Valorant&lt;/td&gt;
&lt;td&gt;87.0%&lt;/td&gt;
&lt;td&gt;22.9%&lt;/td&gt;
&lt;td&gt;69.3%&lt;/td&gt;
&lt;td&gt;29.0%&lt;/td&gt;
&lt;td&gt;27.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dota 2&lt;/td&gt;
&lt;td&gt;49.6%&lt;/td&gt;
&lt;td&gt;32.0%&lt;/td&gt;
&lt;td&gt;54.0%&lt;/td&gt;
&lt;td&gt;43.1%&lt;/td&gt;
&lt;td&gt;34.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;League of Legends&lt;/td&gt;
&lt;td&gt;37.3%&lt;/td&gt;
&lt;td&gt;34.3%&lt;/td&gt;
&lt;td&gt;73.9%&lt;/td&gt;
&lt;td&gt;59.1%&lt;/td&gt;
&lt;td&gt;38.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last column re-weights each game's live and prematch numbers to CS2's 90.7% / 9.3% split. On this basis 66% of LoL's gap to CS2 and 56% of Dota 2's is mix. What remains is real but modest: in live play, 32.0% (Dota 2) and 34.3% (LoL) of arbs last 5 seconds or more, against 23.0% in CS2. Minute-long live arbs are rare everywhere: 1.2% in CS2, 1.8% in Dota 2, 2.8% in LoL. Live medians are 2.0 s for CS2, Dota 2 and LoL.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ehks97geztts6hi8o9g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ehks97geztts6hi8o9g.png" alt="Median arb life, live versus prematch, by game: live is 0 to 4 seconds everywhere, prematch from 6 to 43 seconds" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One caution on Dota 2. Two tournaments, BLAST Slam VIII and PGL Wallachia Season 9, produced 375,324 of its 548,410 arbs (68%), and BLAST Slam VIII alone was 83% prematch. Without them, Dota 2 is 90% live, with 34.0% of live arbs lasting 5 seconds or more and a prematch median of 18.7 s. The live conclusion holds; the prematch comparison with CS2 depends on which events were running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 3: which Dota 2 and LoL markets last
&lt;/h2&gt;

&lt;p&gt;In live play, the main markets look alike. Live arbs lasting 5 seconds or more, by market:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Market&lt;/th&gt;
&lt;th&gt;Dota 2 live arbs&lt;/th&gt;
&lt;th&gt;≥5 s&lt;/th&gt;
&lt;th&gt;LoL live arbs&lt;/th&gt;
&lt;th&gt;≥5 s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Winner (match and map)&lt;/td&gt;
&lt;td&gt;153,535&lt;/td&gt;
&lt;td&gt;33.6%&lt;/td&gt;
&lt;td&gt;82,482&lt;/td&gt;
&lt;td&gt;33.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handicap (maps)&lt;/td&gt;
&lt;td&gt;42,610&lt;/td&gt;
&lt;td&gt;29.9%&lt;/td&gt;
&lt;td&gt;15,379&lt;/td&gt;
&lt;td&gt;32.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total (maps)&lt;/td&gt;
&lt;td&gt;30,938&lt;/td&gt;
&lt;td&gt;29.4%&lt;/td&gt;
&lt;td&gt;12,671&lt;/td&gt;
&lt;td&gt;28.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total kills&lt;/td&gt;
&lt;td&gt;31,241&lt;/td&gt;
&lt;td&gt;27.5%&lt;/td&gt;
&lt;td&gt;12,814&lt;/td&gt;
&lt;td&gt;41.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kill handicap&lt;/td&gt;
&lt;td&gt;7,487&lt;/td&gt;
&lt;td&gt;32.2%&lt;/td&gt;
&lt;td&gt;4,405&lt;/td&gt;
&lt;td&gt;37.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Map duration total&lt;/td&gt;
&lt;td&gt;2,248&lt;/td&gt;
&lt;td&gt;41.0%&lt;/td&gt;
&lt;td&gt;2,154&lt;/td&gt;
&lt;td&gt;45.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Kills markets are where the games differ. In LoL, live total-kills and kill-handicap arbs outlast the winner markets (41.0% and 37.2% against 33.7%); in Dota 2 total kills is shorter (27.5% against 33.6%). Map-duration totals last longest of the live markets in both games. See &lt;a href="https://dozor.bet/en/glossary/total-kills" rel="noopener noreferrer"&gt;total kills&lt;/a&gt; for how the market works.&lt;/p&gt;

&lt;p&gt;The longest windows in the data are not live at all. Object props, such as dragons, barons, towers and inhibitors, have medians of 39 to 52 seconds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LoL:&lt;/strong&gt; total dragons 21,557 arbs, median 42.6 s; total barons 13,353, 39.4 s; total towers 12,207, 42.6 s; total inhibitors 9,143, 49.8 s (44.5% lasted a minute or more).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dota 2:&lt;/strong&gt; total towers 11,927 arbs, median 51.5 s (47.3% lasted a minute or more). First barracks is shorter: 18,788 arbs, median 6.2 s.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only 0.2% to 2.4% of these arbs were live, and none had an exchange leg. They are prematch prop lines, and in our reading they sit untouched between updates. Among the six largest LoL pairs, the one with the longest windows, &lt;a href="https://dozor.bet/en/bookmakers/gamdom" rel="noopener noreferrer"&gt;Gamdom&lt;/a&gt; with &lt;a href="https://dozor.bet/en/bookmakers/leon" rel="noopener noreferrer"&gt;Leon&lt;/a&gt;, trades almost only these: dragons and map duration were 97% of its 15,930 arbs, with 97.6% lasting 5 seconds or more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 4: the exchange leg
&lt;/h2&gt;

&lt;p&gt;Exchanges are far more present in the slow games. The share of arbs with a Polymarket or Kalshi leg is 42.5% in LoL (Polymarket 32.7%, Kalshi 10.1%), 25.7% in Dota 2 and 12.1% in CS2. In live LoL, Polymarket is in 45.5% of arbs. For the LoL match winner, 75.8% of arbs have an exchange leg.&lt;/p&gt;

&lt;p&gt;Does an exchange leg lengthen the window? It depends on the game. Share of arbs lasting 5 seconds or more, live only, without and with an exchange leg:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CS2: 21.9% and 33.6%.&lt;/li&gt;
&lt;li&gt;Dota 2: 30.2% and 36.0%.&lt;/li&gt;
&lt;li&gt;LoL: 34.9% and 33.8%. No lift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In LoL prematch the order is even reversed (80.2% without an exchange, 62.3% with one), likely because books-only arbs include the long-lived object props above. The largest pairs with an exchange: &lt;a href="https://dozor.bet/en/pairs/olimp-polymarket" rel="noopener noreferrer"&gt;Olimp – Polymarket&lt;/a&gt; in LoL (27,348 arbs, 53.3% lasting 5 s or more, median 6.1 s) and in Dota 2 (34,618, 39.5%); &lt;a href="https://dozor.bet/en/pairs/onexbet-polymarket" rel="noopener noreferrer"&gt;1xBet – Polymarket&lt;/a&gt; in LoL (19,497, 52.9%); Kalshi – Olimp in LoL (11,246, 49.0%). More on the exchange side: &lt;a href="https://dozor.bet/en/prediction-markets" rel="noopener noreferrer"&gt;prediction market arbitrage&lt;/a&gt; and the &lt;a href="https://dozor.bet/en/bookmakers/polymarket" rel="noopener noreferrer"&gt;Polymarket&lt;/a&gt; and &lt;a href="https://dozor.bet/en/bookmakers/kalshi" rel="noopener noreferrer"&gt;Kalshi&lt;/a&gt; pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why slower games last longer: three hypotheses
&lt;/h2&gt;

&lt;p&gt;These are our readings. We did not measure causes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tempo of repricing.&lt;/strong&gt; A CS2 line can change after every round; a LoL or Dota 2 line moves mostly on kills, towers and other objects. The map-duration totals in our arbs have a median line of 32 minutes in LoL and 40 minutes in Dota 2, so there is more time between events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prop lines nobody refreshes.&lt;/strong&gt; Object props are prematch and likely quoted by few books. The 40 to 50 second windows come from there, and they inflate the numbers for the slow games.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exchange pricing.&lt;/strong&gt; Exchange prices move when someone trades, so a stale order waits. The lift from an exchange leg in live CS2 and Dota 2 fits that. Live LoL shows no lift, so at best it is part of the story.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rainbow Six is a round-based shooter like CS2 but has live windows closer to the MOBAs (39.2% of live arbs lasting 5 s or more). With 66,937 arbs it is a small sample, and a pure tempo story does not explain it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do with it
&lt;/h2&gt;

&lt;p&gt;A longer window only matters if the quote is fresh. Check &lt;a href="https://dozor.bet/en/glossary/price-age" rel="noopener noreferrer"&gt;price age&lt;/a&gt; on every leg: a 6% LoL arb that is 30 seconds old is more likely a stale line than an opportunity. Dozor shows bookmakers and the two exchanges on one screen, with ROI and quote age per leg, and a rule for alerts can be limited to one game, minimum ROI, bookmakers and live or prematch. Split stakes with the &lt;a href="https://dozor.bet/en/calculator" rel="noopener noreferrer"&gt;surebet calculator&lt;/a&gt;; see the screens for &lt;a href="https://dozor.bet/en/live" rel="noopener noreferrer"&gt;live&lt;/a&gt; and &lt;a href="https://dozor.bet/en/prematch" rel="noopener noreferrer"&gt;prematch&lt;/a&gt; arbs.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Dota 2 or LoL better for arbitrage than CS2?&lt;/strong&gt; Windows are longer and margins thinner. 59.1% of LoL arbs and 43.1% of Dota 2 arbs lasted 5 seconds or more against 27.0% in CS2, with median ROI of 1.82% and 2.16% against 3.22%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long do LoL betting arbitrage opportunities last?&lt;/strong&gt; The median is 8.4 s overall: 2.0 s live and 22.8 s prematch. In live play 34.3% last 5 seconds or more and 2.8% last a minute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do Polymarket and Kalshi have arbs on Dota 2 and LoL?&lt;/strong&gt; Yes, and more than on CS2: 42.5% of LoL arbs and 25.7% of Dota 2 arbs had a Polymarket or Kalshi leg, against 12.1% in CS2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which markets have the longest windows?&lt;/strong&gt; Prematch object props: total towers, dragons, barons and inhibitors, with medians of 39 to 52 seconds. Among live markets with at least 2,000 arbs, map-duration totals (LoL 45.0% lasting 5 s or more, Dota 2 41.0%) and LoL total kills (41.0%) last longest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on live data
&lt;/h2&gt;

&lt;p&gt;Dozor puts 25 sportsbooks and the Polymarket and Kalshi exchanges on one screen, with ROI, quote age and a direct link to the event for every arb. The Free plan needs no card and has no time limit: all arbs with ROI up to 1% are shown in full with no delay, and above that you see the count and the best ROI. Start at &lt;a href="https://dozor.bet/en" rel="noopener noreferrer"&gt;dozor.bet/en&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Data from &lt;a href="https://dozor.bet/en" rel="noopener noreferrer"&gt;Dozor&lt;/a&gt; scanner history, Sep 6 – Oct 6, 2026. Dozor is an information tool, not a bookmaker. 18+. Betting involves risk of loss; sportsbooks may restrict arbitrage accounts.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>esports</category>
      <category>analytics</category>
      <category>gaming</category>
    </item>
    <item>
      <title>Why Sportsbooks Limit Winning Bettors: Research vs 30 Days of Esports Data</title>
      <dc:creator>Dozor | Esports Surebet Scanner</dc:creator>
      <pubDate>Thu, 22 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/dozor/why-sportsbooks-limit-winning-bettors-research-vs-30-days-of-esports-data-1jop</link>
      <guid>https://dev.to/dozor/why-sportsbooks-limit-winning-bettors-research-vs-30-days-of-esports-data-1jop</guid>
      <description>&lt;p&gt;Sportsbooks limit winning bettors because a customer who keeps winning is, from the book's side, adverse selection: someone who knows more than the price. A lower maximum stake is the cheapest response. Wikipedia's summary of the research says bookmakers usually detect arbitrage bettors quickly and that their maximum wagers are often cut heavily, sometimes until betting is no longer viable.&lt;/p&gt;

&lt;p&gt;We compared that picture with 30 days of our own esports data (Sep 6 – Oct 6, 2026): about 4.87 million arbitrage opportunities across &lt;a href="https://dozor.bet/en/bookmakers" rel="noopener noreferrer"&gt;27 books&lt;/a&gt;: 25 sportsbooks and crypto sportsbooks plus the Polymarket and Kalshi exchanges. Three results stand out. Esports arbs are much larger than in the football study that is usually quoted: the median best ROI was 2.89% and only 13.75% of arbs were below 1%. A rule of thumb we have used ourselves, that a 10%+ arb is a stale line, is not supported by the lifetimes. And arbs with an exchange leg live longer, which is where limits matter least.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bookmakers limit accounts
&lt;/h2&gt;

&lt;p&gt;A sportsbook sets a price from a model, a feed and the bets it receives, and charges a margin on top. Third-party guides to Pinnacle commonly put a standard sportsbook margin at 5–7% and Pinnacle's own at 2–3%. A bettor who beats that margin repeatedly is telling the book its price is wrong. Cutting the maximum stake costs the book nothing and stops the leak.&lt;/p&gt;

&lt;p&gt;Arbitrage bettors are the clearest case, because they win by definition: the implied probabilities of the best prices add up to less than 1 (&lt;a href="https://dozor.bet/en/glossary/surebet" rel="noopener noreferrer"&gt;surebet&lt;/a&gt;). According to the Wikipedia article on arbitrage betting, which cites a 2016 Yale senior essay by Avery Schwartz on the European soccer market, the practice is usually detected quickly. In the past it could mean half of an arbitrage bet being cancelled or the account being closed. Our own explainers cover the mechanics: &lt;a href="https://dozor.bet/en/glossary/betting-limits" rel="noopener noreferrer"&gt;betting limits&lt;/a&gt; and &lt;a href="https://dozor.bet/en/blog/why-bookmakers-limit-accounts" rel="noopener noreferrer"&gt;why bookmakers limit accounts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There is an opposite model. A guide to Pinnacle sports betting (by Pinnacle Odds Dropper) describes a "winners welcome" policy: sharp action is used to turn opening lines into accurate closing lines. As that guide states it, limits open at about 25% of the maximum and scale up towards the start of the event. That is a claim of the source, not something we measured. Our reading: a book that earns from a low margin and from price accuracy has a reason to keep winners. A book that earns from a high margin on recreational flow has a reason to remove them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the research says about arbitrage size
&lt;/h2&gt;

&lt;p&gt;Franck, Verbeek and Nüesch studied arbitrage in football betting (working paper 2009, published in &lt;em&gt;Economica&lt;/em&gt;, vol. 80, issue 318, 2013). They looked at 5,478 matches in the top five European leagues over three seasons, with prices from eight bookmakers and one betting exchange. They found only 10 arbitrage opportunities between bookmakers, but 1,450 cases where a bookmaker bet combined with an exchange bet gave a guaranteed profit. The authors link this to the two kinds of market differing in informational efficiency.&lt;/p&gt;

&lt;p&gt;Wikipedia cites the same study for a second number: 98% of arbitrage opportunities return less than 1.2%. Two things follow. Arbitrage in that setting is a thin-margin activity, and the opportunities that exist are mostly between a bookmaker and an exchange rather than between two bookmakers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our distribution: esports arbs are bigger
&lt;/h2&gt;

&lt;p&gt;Our median ROI over the 30 days was 2.89%, where ROI is the best value an arb reached during its life. The history holds arbs with a best ROI from 0.5% to 15.0%. Only 13.75% were below 1%, and 35.78% below 2%, so the share below 1.2% is somewhere between those two numbers, nowhere near 98%.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ROI bucket&lt;/th&gt;
&lt;th&gt;Arbs&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;th&gt;Median life&lt;/th&gt;
&lt;th&gt;One snapshot&lt;/th&gt;
&lt;th&gt;≥10 s&lt;/th&gt;
&lt;th&gt;≥60 s&lt;/th&gt;
&lt;th&gt;Live share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;1%&lt;/td&gt;
&lt;td&gt;669,966&lt;/td&gt;
&lt;td&gt;13.75%&lt;/td&gt;
&lt;td&gt;2.11 s&lt;/td&gt;
&lt;td&gt;34.9%&lt;/td&gt;
&lt;td&gt;29.6%&lt;/td&gt;
&lt;td&gt;7.2%&lt;/td&gt;
&lt;td&gt;64.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1–2%&lt;/td&gt;
&lt;td&gt;1,073,400&lt;/td&gt;
&lt;td&gt;22.03%&lt;/td&gt;
&lt;td&gt;2.06 s&lt;/td&gt;
&lt;td&gt;37.3%&lt;/td&gt;
&lt;td&gt;26.7%&lt;/td&gt;
&lt;td&gt;5.9%&lt;/td&gt;
&lt;td&gt;72.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2–3%&lt;/td&gt;
&lt;td&gt;757,546&lt;/td&gt;
&lt;td&gt;15.55%&lt;/td&gt;
&lt;td&gt;2.04 s&lt;/td&gt;
&lt;td&gt;39.5%&lt;/td&gt;
&lt;td&gt;22.5%&lt;/td&gt;
&lt;td&gt;4.6%&lt;/td&gt;
&lt;td&gt;79.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3–5%&lt;/td&gt;
&lt;td&gt;940,016&lt;/td&gt;
&lt;td&gt;19.30%&lt;/td&gt;
&lt;td&gt;2.02 s&lt;/td&gt;
&lt;td&gt;42.4%&lt;/td&gt;
&lt;td&gt;20.0%&lt;/td&gt;
&lt;td&gt;3.4%&lt;/td&gt;
&lt;td&gt;88.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5–10%&lt;/td&gt;
&lt;td&gt;1,058,490&lt;/td&gt;
&lt;td&gt;21.73%&lt;/td&gt;
&lt;td&gt;2.02 s&lt;/td&gt;
&lt;td&gt;42.8%&lt;/td&gt;
&lt;td&gt;19.2%&lt;/td&gt;
&lt;td&gt;2.8%&lt;/td&gt;
&lt;td&gt;91.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;≥10%&lt;/td&gt;
&lt;td&gt;372,057&lt;/td&gt;
&lt;td&gt;7.64%&lt;/td&gt;
&lt;td&gt;2.03 s&lt;/td&gt;
&lt;td&gt;41.8%&lt;/td&gt;
&lt;td&gt;19.8%&lt;/td&gt;
&lt;td&gt;2.8%&lt;/td&gt;
&lt;td&gt;92.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"One snapshot" means the arb was seen once and was gone by the next scan, which runs about every 2 seconds. This is not a like-for-like comparison with the football study. It is a different sport and a later period, 27 books instead of eight bookmakers and one exchange, and our ROI is the best value reached during an arb's life, which is never lower than a single snapshot would show. Esports lines move fast and books price many small markets (handicaps, totals, rounds). The median esports arb here, at 2.89%, is more than twice the 1.2% line the football study drew.&lt;/p&gt;

&lt;h2&gt;
  
  
  Are 10%+ arbs a gift or a stale line?
&lt;/h2&gt;

&lt;p&gt;Our own hypothesis going in was that a very high ROI is more often a stale quote than a bookmaker's mistake. If so, 10%+ arbs should be shorter-lived than small ones. The all-arbs table seems to agree: arbs lasting 60 s or more fall from 7.2% in the lowest bucket to 2.8% in the top two, and the share seen in a single snapshot rises from 34.9% to 41.8%.&lt;/p&gt;

&lt;p&gt;But look at the last column. The live share grows from 64.8% to 92.9% as ROI rises, and live arbs are short in every bucket: across the whole window 1.4% of live arbs lasted 60 s or more, against 17.7% of prematch arbs. So the first comparison mixes two things. Splitting out live arbs removes the mix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ROI bucket&lt;/th&gt;
&lt;th&gt;Live arbs&lt;/th&gt;
&lt;th&gt;Median life&lt;/th&gt;
&lt;th&gt;One snapshot&lt;/th&gt;
&lt;th&gt;≥60 s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt;1%&lt;/td&gt;
&lt;td&gt;433,104&lt;/td&gt;
&lt;td&gt;2.00 s&lt;/td&gt;
&lt;td&gt;49.6%&lt;/td&gt;
&lt;td&gt;1.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1–2%&lt;/td&gt;
&lt;td&gt;772,447&lt;/td&gt;
&lt;td&gt;2.00 s&lt;/td&gt;
&lt;td&gt;49.1%&lt;/td&gt;
&lt;td&gt;1.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2–3%&lt;/td&gt;
&lt;td&gt;602,083&lt;/td&gt;
&lt;td&gt;2.01 s&lt;/td&gt;
&lt;td&gt;47.8%&lt;/td&gt;
&lt;td&gt;1.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3–5%&lt;/td&gt;
&lt;td&gt;825,370&lt;/td&gt;
&lt;td&gt;2.01 s&lt;/td&gt;
&lt;td&gt;47.1%&lt;/td&gt;
&lt;td&gt;1.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5–10%&lt;/td&gt;
&lt;td&gt;963,998&lt;/td&gt;
&lt;td&gt;2.01 s&lt;/td&gt;
&lt;td&gt;46.2%&lt;/td&gt;
&lt;td&gt;1.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;≥10%&lt;/td&gt;
&lt;td&gt;344,845&lt;/td&gt;
&lt;td&gt;2.02 s&lt;/td&gt;
&lt;td&gt;44.4%&lt;/td&gt;
&lt;td&gt;1.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fppxh9pm8f9sn0oqe6ggg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fppxh9pm8f9sn0oqe6ggg.png" alt="Share of esports arbs by ROI bucket, and share lasting 60 s or more for all arbs and for live arbs only, Sep 6 – Oct 6, 2026" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Inside live play the buckets look the same. The median life is 2.00–2.02 s everywhere, and the 10%+ bucket is, if anything, slightly better: 44.4% single-snapshot arbs against 49.6% in the lowest bucket, and 1.9% lasting a minute against 1.2%. Arbs with an exchange leg are 14.1–17.8% of every bucket, so exchanges do not explain the pattern either.&lt;/p&gt;

&lt;p&gt;We have not re-weighted the all-arbs table to an equal live share, and we did not break out prematch by bucket. So we cannot say that high ROI is worse, and we cannot say it is better. What we can say is that the stale-line rule is not supported by arb lifetimes: in live play, big arbs live as long as small ones. Lifetime is only a proxy; the direct check for a stale quote is its age. That is why Dozor shows the &lt;a href="https://dozor.bet/en/glossary/price-age" rel="noopener noreferrer"&gt;age of every quote&lt;/a&gt; next to the &lt;a href="https://dozor.bet/en/glossary/roi" rel="noopener noreferrer"&gt;ROI&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where limits matter less: exchanges
&lt;/h2&gt;

&lt;p&gt;Polymarket and Kalshi are exchanges. Prices come from other traders, not from a bookmaker's model, so there is no book to protect from winners. That is our reading; check each venue's terms and your jurisdiction. The data show how often they matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frequency.&lt;/strong&gt; 15.9% of arbs had a Polymarket or Kalshi leg: 642,253 with &lt;a href="https://dozor.bet/en/bookmakers/polymarket" rel="noopener noreferrer"&gt;Polymarket&lt;/a&gt; and 138,017 with &lt;a href="https://dozor.bet/en/bookmakers/kalshi" rel="noopener noreferrer"&gt;Kalshi&lt;/a&gt;, over the same 30 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lifetime.&lt;/strong&gt; In live play, 23.9% of arbs with an exchange leg lasted 10 s or more, against 14.4% for sportsbook-only arbs. For 60 s or more it was 2.9% against 1.2%, so exchange arbs are 1.7× and 2.4× as likely to survive (more in &lt;a href="https://dozor.bet/en/blog/how-long-do-surebets-last" rel="noopener noreferrer"&gt;how long surebets last&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;By venue.&lt;/strong&gt; The median lifetime was 4.1 s for Polymarket and 4.0 s for Kalshi, against 2.0 s for all arbs. Arbs lasting 60 s or more were 8.3% of Polymarket arbs, 6.9% of Kalshi arbs and 4.5% overall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ROI.&lt;/strong&gt; In prematch the median ROI of arbs with an exchange leg was 2.30%, against 1.54% for sportsbook-only arbs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fol1pnzumewe57pjcvfx0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fol1pnzumewe57pjcvfx0.png" alt="Live esports arbs lasting 10 s and 60 s: 23.9% and 2.9% with an exchange leg against 14.4% and 1.2% for sportsbooks only" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This fits the football study above: the more reliable gaps are between a bookmaker and an exchange. A caveat applies. An exchange leg is limited by the depth of its order book at the quoted price, and fees are part of the ROI. See &lt;a href="https://dozor.bet/en/prediction-markets" rel="noopener noreferrer"&gt;prediction markets vs sportsbooks&lt;/a&gt; and &lt;a href="https://dozor.bet/en/blog/how-to-arbitrage-polymarket-kalshi" rel="noopener noreferrer"&gt;how to arbitrage Polymarket and Kalshi&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means in practice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Do not rank arbs by ROI alone.&lt;/strong&gt; In our data a 10%+ arb in live play lived as long as a 1% one, which does not make it safe either. Check the age of both quotes and whether the match is live before acting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan for limits as a cost.&lt;/strong&gt; A sportsbook leg can shrink or disappear. Use more than one book, and read each book's terms. Multiple accounts and other ways around a book's rules get accounts closed, and we do not suggest them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the legs that stay open longer.&lt;/strong&gt; Exchange legs lived longer in our data. Filter by book and minimum ROI so the screen shows only what you can act on: Dozor's alerts take a minimum ROI, disciplines, books and live or prematch.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why do bookmakers limit accounts?&lt;/strong&gt;&lt;br&gt;
Because a consistently winning account signals that the price is wrong, which is adverse selection. Cutting the maximum stake is the cheapest fix. Pinnacle is described as following the opposite policy, "winners welcome", by third-party guides.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do sportsbooks limit arbitrage bettors?&lt;/strong&gt;&lt;br&gt;
Per Wikipedia's article on arbitrage betting, yes: maximum wagers are often reduced heavily when the book detects the pattern. Exchanges such as Polymarket and Kalshi are a different model, since there is no bookmaker taking the other side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How big are arbitrage opportunities?&lt;/strong&gt;&lt;br&gt;
A football study cited by Wikipedia found 98% below 1.2% ROI. In our esports data (Sep 6 – Oct 6, 2026) the median best ROI was 2.89%: 13.75% of arbs were below 1%, 22.03% between 1% and 2%, and 7.64% at 10% or more. Different sport, period and method, so it is not a like-for-like comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are high-ROI arbs stale lines?&lt;/strong&gt;&lt;br&gt;
Not by lifetime. In live play the 10%+ bucket had a median life of 2.02 s against 2.00 s for the lowest bucket, and 1.9% of its arbs lasted 60 s or more against 1.2%. Exchange legs are the ones that last: 23.9% of live arbs with one lasted 10 s or more, against 14.4% for sportsbook-only arbs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Dozor scans 25 sportsbooks plus Polymarket and Kalshi for esports arbs, with the ROI, the age of every quote and a direct link per leg, so you can filter by book and ROI and see exchange legs on the same screen. Free plan: no card and no time limit, every arb with ROI up to 1% in full and without delay; above that you see the count and the best ROI. &lt;a href="https://dozor.bet/en" rel="noopener noreferrer"&gt;Open Dozor&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Data from Dozor (Sep 6 – Oct 6, 2026), an esports arbitrage scanner covering 25 sportsbooks plus Polymarket and Kalshi. Dozor is an information tool, not a bookmaker. 18+. Betting involves risk of loss; sportsbooks may restrict arbitrage accounts.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>gambling</category>
      <category>esports</category>
      <category>research</category>
    </item>
    <item>
      <title>The Kelly Criterion for Bettors: Math, Code and When Arbers Don't Need It</title>
      <dc:creator>Dozor | Esports Surebet Scanner</dc:creator>
      <pubDate>Sun, 18 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/dozor/the-kelly-criterion-for-bettors-math-code-and-when-arbers-dont-need-it-28dm</link>
      <guid>https://dev.to/dozor/the-kelly-criterion-for-bettors-math-code-and-when-arbers-dont-need-it-28dm</guid>
      <description>&lt;p&gt;The Kelly criterion says to stake the fraction &lt;strong&gt;f = (p·D − 1) / (D − 1)&lt;/strong&gt; of your bankroll on a bet with win probability &lt;em&gt;p&lt;/em&gt; and decimal odds &lt;em&gt;D&lt;/em&gt;. It maximises long-run growth, but only if &lt;em&gt;p&lt;/em&gt; is right, and it is rough even then: in the simulation below, full Kelly with a true 20% edge had a median worst drawdown of 90%. Arbitrage bettors do not need it, because a surebet has no outcome variance to size against.&lt;/p&gt;

&lt;p&gt;Below: the derivation, a textbook example I recomputed, &lt;code&gt;kelly()&lt;/code&gt; in TypeScript and Python, a Monte Carlo of half, full and double Kelly (fractional Kelly), and 30 days of esports arbitrage data (Sep 6 – Oct 6, 2026). If you only need the numbers for one bet, use the &lt;a href="https://dozor.bet/en/kelly-calculator" rel="noopener noreferrer"&gt;Kelly criterion calculator&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Kelly formula in three lines
&lt;/h2&gt;

&lt;p&gt;Stake a fraction &lt;em&gt;f&lt;/em&gt; of your bankroll at net odds &lt;em&gt;b = D − 1&lt;/em&gt;. A win multiplies the bankroll by &lt;em&gt;1 + f·b&lt;/em&gt;, a loss by &lt;em&gt;1 − f&lt;/em&gt;. Kelly maximises the expected log of the bankroll, which is the long-run growth rate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;g(f)  = p·ln(1 + f·b) + (1 − p)·ln(1 − f)
g'(f) = p·b / (1 + f·b) − (1 − p) / (1 − f) = 0     →     f = p − (1 − p)/b
f*    = (p·D − 1) / (D − 1)                         (edge divided by net odds)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The numerator is the expected profit per unit staked. If &lt;em&gt;p·D ≤ 1&lt;/em&gt; there is no edge and the answer is zero: do not bet. According to Wikipedia's article on the &lt;a href="https://en.wikipedia.org/wiki/Kelly_criterion" rel="noopener noreferrer"&gt;Kelly criterion&lt;/a&gt;, the rule was described by John L. Kelly Jr. at Bell Labs in 1956 and was applied to blackjack and the stock market by Edward Thorp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recomputing the Haghani &amp;amp; Dewey coin example
&lt;/h2&gt;

&lt;p&gt;Wikipedia summarises a 2016 study by Haghani and Dewey (&lt;a href="https://arxiv.org/abs/1701.01427" rel="noopener noreferrer"&gt;arXiv:1701.01427&lt;/a&gt;). 61 participants got $25 and could bet even money on a coin that lands heads 60% of the time. They had 30 minutes, about 300 bets, and prizes were capped at $250. According to that summary, 28% went bust and the average payout was $91.&lt;/p&gt;

&lt;p&gt;The Kelly bet is (0.6·2 − 1) / (2 − 1) = &lt;strong&gt;20%&lt;/strong&gt;. Wikipedia gives the growth as 2.034% per round. I checked it: g(0.2) = 0.6·ln 1.2 + 0.4·ln 0.8 = 0.020136, which is &lt;strong&gt;2.01%&lt;/strong&gt; in log terms. And e^0.020136 − 1 = 2.034%. It is the same number in two units: log growth and multiplicative growth. The arithmetic expected gain per bet is 4% (0.2 × 0.2), but compounding makes the long-run rate about half of that. Uncapped, $25 × 1.02034^300 is about $10,500.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbl95ilygrl7dmt26z07t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbl95ilygrl7dmt26z07t.png" alt="Expected log-growth per bet versus stake size for a 60% even-odds bet: peak at 20%, zero at 38.9%, negative beyond" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same curve gives the other two points we need. Half Kelly (10% stake) grows the bankroll 1.50% per bet, &lt;strong&gt;75% of the full-Kelly rate&lt;/strong&gt; for half the stake. Double Kelly (40%) is already negative at −0.24% per bet, and growth crosses zero at a 38.9% stake, about 1.95× Kelly.&lt;/p&gt;

&lt;h2&gt;
  
  
  kelly() in TypeScript and Python
&lt;/h2&gt;

&lt;p&gt;Both versions take decimal odds. For a prediction-market price (for example 48.8¢), the odds are 1 / price, see the &lt;a href="https://dozor.bet/en/odds-converter" rel="noopener noreferrer"&gt;odds converter&lt;/a&gt;. If a venue charges a fee, pass the after-fee odds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** Full-Kelly stake fraction for a two-outcome bet: win probability p, decimal odds d. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;kelly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;edge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;d&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="c1"&gt;// expected profit per unit staked&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;edge&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="nx"&gt;edge&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&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="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="c1"&gt;// no edge, no bet&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/** Expected log-growth of the bankroll per bet when staking fraction f. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;growth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&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="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="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;);&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 python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;kelly&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="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Full-Kelly stake fraction for a two-outcome bet: win probability p, decimal odds d.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;edge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;# expected profit per unit staked
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;edge&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&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="n"&gt;edge&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="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;  &lt;span class="c1"&gt;# no edge, no bet
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;growth&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="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Expected log-growth of the bankroll per bet when staking fraction f.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;log&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="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&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="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="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;log&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="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;kelly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&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="s"&gt;Kelly &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&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;  g(K) &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;growth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  g(2K) &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;growth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;%&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;span class="c1"&gt;# Kelly 0.200  g(K) 2.0136%  g(2K) -0.2447%
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran both: they agree to four decimals (0.200, 2.0136%, −0.2447%).&lt;/p&gt;

&lt;h2&gt;
  
  
  Fractional Kelly in a simulation: half, full and double
&lt;/h2&gt;

&lt;p&gt;Expected growth is an average, but you live through one path. This stdlib-only script plays the same bet 300 times, 20,000 paths per stake size, and reports the final bankroll (start = 1), the share of paths that end below the start, and the median worst drawdown.&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;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;kelly&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="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;edge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;edge&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&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="n"&gt;edge&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="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;simulate&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="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bets&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&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="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;finals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;drawdowns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;bank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;peak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
        &lt;span class="n"&gt;worst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bets&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;stake&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;bank&lt;/span&gt;
            &lt;span class="n"&gt;bank&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;stake&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&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="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&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;p&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;stake&lt;/span&gt;
            &lt;span class="n"&gt;peak&lt;/span&gt; &lt;span class="o"&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;peak&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;worst&lt;/span&gt; &lt;span class="o"&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;worst&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="n"&gt;bank&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;peak&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;finals&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;bank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;drawdowns&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;worst&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finals&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;drawdowns&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="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;
&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;kelly&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="n"&gt;d&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;stake&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;median&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5th pct&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;95th pct&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt; start&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;worst dd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;9&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;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mult&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.5K&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&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;1K&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&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;2K&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;finals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;simulate&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="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mult&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;k&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finals&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;name&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mult&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;finals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;2&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; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;finals&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="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;2&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; &lt;/span&gt;&lt;span class="sh"&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;finals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;0&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; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;finals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;8.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; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dd&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;9.0&lt;/span&gt;&lt;span class="o"&gt;%&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;Output of &lt;code&gt;python3 -I&lt;/code&gt;, run locally (about 2 seconds):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    stake   median  5th pct  95th pct  &amp;lt; start  worst dd
0.5K  10%    91.16     5.49     1,513     0.5%       62%
  1K  20%   420.17     1.44   122,659     4.6%       90%
  2K  40%     0.48     0.00    68,057    51.9%      100%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fujlpa9ayndmr6s2elu68.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fujlpa9ayndmr6s2elu68.png" alt="Final bankroll after 300 bets at half, full and double Kelly: medians 91x, 420x and 0.48x, 52% of double-Kelly paths end below start" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Full Kelly's median is more than four times higher than half Kelly's (420× against 91×). It also ends below the start in 4.6% of paths against 0.5%, and its median worst drawdown is 90% against 62%. Double Kelly has a median of 0.48×: half the bankroll is gone in the typical run, and 51.9% of paths end below the start. Yet the 95th percentile is still 68,057×, which is why the average is a poor summary and the median is the number to read. An exact binomial calculation agrees: 0.4%, 4.4% and 52.2% of paths end below the start. The final bankroll depends only on the number of wins, and at the median (180 wins in 300) 1.2^180 × 0.8^120 = 420.17.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bettors use fractional Kelly: model risk
&lt;/h2&gt;

&lt;p&gt;The probability &lt;em&gt;p&lt;/em&gt; is your own estimate. Say the true chance is 55% but you believe 60% at even odds. The true Kelly stake is 10%; you stake 20%, which is exactly double Kelly for the real edge. Growth per bet falls from +0.501% to −0.014%, so a five-point error removes essentially all of the growth. This is the practical case for staking a fraction, and the &lt;a href="https://dozor.bet/en/kelly-calculator" rel="noopener noreferrer"&gt;Kelly calculator&lt;/a&gt; has ½ and ¼ options for it.&lt;/p&gt;

&lt;p&gt;It is not a free fix. According to Wikipedia, Paul Samuelson argued in 1963 and 1979 that maximising expected log wealth is only the right goal for someone with log utility, and that the losses along the way can be larger than people will tolerate. Karl Whelan, in &lt;a href="https://www.karlwhelan.com/Teaching/BettingMarkets/17.TheKellyCriterion.pdf" rel="noopener noreferrer"&gt;lecture 17 of his UCD betting-markets course&lt;/a&gt; (Spring 2026), goes further. For utility functions other than log (CRRA with risk aversion γ ≠ 1) the optimal stake is &lt;em&gt;not&lt;/em&gt; a fixed fraction of Kelly: the ratio changes with the odds and with your belief. He calls "bet half Kelly if you are twice as risk-averse" an urban legend. Treat ½ Kelly as a rule of thumb, not as an optimum.&lt;/p&gt;

&lt;p&gt;With several mutually exclusive outcomes the single-bet formula stops being optimal; in Whelan's simulation 17% of the optimal bets had negative expected value and served as hedges. His blunt conclusion: most bettors have no real edge, and then the best stake is zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an arbitrageur does not need Kelly
&lt;/h2&gt;

&lt;p&gt;An arb is a set of bets on every outcome where the implied probabilities sum to less than one: S = Σ 1/Di &amp;lt; 1. Stake c/Di of the bankroll on leg &lt;em&gt;i&lt;/em&gt;. Whatever happens, one leg wins and returns &lt;em&gt;c&lt;/em&gt;, so the bankroll becomes 1 + c·(1 − S) in every outcome. There is no &lt;em&gt;p&lt;/em&gt; in that formula, so g(c) = ln(1 + c·(1 − S)) always rises with &lt;em&gt;c&lt;/em&gt;, and Kelly's answer is: stake as much as you are allowed. Wikipedia's &lt;a href="https://en.wikipedia.org/wiki/Arbitrage_betting" rel="noopener noreferrer"&gt;arbitrage betting&lt;/a&gt; entry makes the same point: you do not need to estimate outcome probabilities. Here is a two-leg example, best price per outcome from two different books:&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="n"&gt;odds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;2.10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.04&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                      &lt;span class="c1"&gt;# best price per outcome, two different books
&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&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="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;odds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;# &amp;lt; 1 means an arbitrage exists
&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;
&lt;span class="n"&gt;stakes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;total&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="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;odds&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;payouts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stakes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;odds&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="s"&gt;sum(1/D) = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&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;  ROI = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&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;1&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;%&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;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stakes &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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stakes&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payouts&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payouts&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# same in every outcome
&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;sum(1/D) = 0.9664  ROI = 3.48%
stakes  [49.28, 50.72]
payouts [103.48, 103.48]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://dozor.bet/en/calculator" rel="noopener noreferrer"&gt;surebet calculator&lt;/a&gt; does this split for up to 10 outcomes, with exchange commission and a fixed leg; the method is in &lt;a href="https://dozor.bet/en/blog/how-to-calculate-a-surebet" rel="noopener noreferrer"&gt;how to calculate a surebet&lt;/a&gt;. The question for an arber is therefore not "what fraction", but "what limits my size". In our data (Sep 6 – Oct 6, 2026, about 4.87 million esports arbs across 27 books) the limits are these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time.&lt;/strong&gt; The median arb lived 2.0 s. 32% were seen for 5 s or more and 4.5% for 60 s or more. Size is bounded by how fast you can place both legs. See &lt;a href="https://dozor.bet/en/blog/how-long-do-surebets-last" rel="noopener noreferrer"&gt;how long surebets last&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Book limits.&lt;/strong&gt; Sportsbooks can cap stakes and restrict accounts that win through arbs (&lt;a href="https://dozor.bet/en/glossary/betting-limits" rel="noopener noreferrer"&gt;betting limits&lt;/a&gt;, &lt;a href="https://dozor.bet/en/blog/why-bookmakers-limit-accounts" rel="noopener noreferrer"&gt;why bookmakers limit accounts&lt;/a&gt;). Check the maximum stake before you size up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exchange depth.&lt;/strong&gt; 15.9% of arbs had a Polymarket or Kalshi leg. In live play, 23.9% of those survived 10 s or more, against 14.4% for sportsbook-only arbs. A longer window helps, but you can only fill what the order book of &lt;a href="https://dozor.bet/en/bookmakers/polymarket" rel="noopener noreferrer"&gt;Polymarket&lt;/a&gt; or &lt;a href="https://dozor.bet/en/bookmakers/kalshi" rel="noopener noreferrer"&gt;Kalshi&lt;/a&gt; holds at the quoted price. More on this side: &lt;a href="https://dozor.bet/en/prediction-markets" rel="noopener noreferrer"&gt;prediction markets&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ROI.&lt;/strong&gt; The median ROI (the best value reached during an arb's life) was 2.89%. Fully compounding that gives about 2.85% log growth per cycle with zero variance, compared with 2.01% and a 90% drawdown for full Kelly on a 20% edge. This holds only if the whole bankroll fits on every arb, and in practice the three limits above bind much earlier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One caveat. An arb is riskless only if both legs fill at the quoted prices. If a leg is rejected or its price moves, you hold an open one-sided bet, and that remainder is where sizing discipline matters. Settlement rules also differ between books (a cancelled match, a forfeit), so read them. This is why we show the &lt;a href="https://dozor.bet/en/glossary/price-age" rel="noopener noreferrer"&gt;age of every quote&lt;/a&gt; next to the &lt;a href="https://dozor.bet/en/glossary/roi" rel="noopener noreferrer"&gt;ROI&lt;/a&gt;: our reading is that an old quote is the one most likely to have moved already.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Kelly still applies: value bets
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://dozor.bet/en/glossary/value-bet" rel="noopener noreferrer"&gt;value bet&lt;/a&gt; is a single bet where you believe the price is wrong. It has outcome variance and an estimated probability, which is exactly the setting Kelly was built for. Use &lt;code&gt;kelly()&lt;/code&gt; with a conservative &lt;em&gt;p&lt;/em&gt;, a fraction (¼ to ½), and a fixed review of whether your estimates were right. Our reading: if you cannot show that your probabilities beat the closing price over a few hundred bets, the right Kelly stake is zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the Kelly criterion formula for betting?&lt;/strong&gt;&lt;br&gt;
f = (p·D − 1) / (D − 1), where &lt;em&gt;p&lt;/em&gt; is your win probability and &lt;em&gt;D&lt;/em&gt; the decimal odds. At p = 60% and D = 2.0 it gives 20% of the bankroll.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is fractional Kelly and which fraction should I use?&lt;/strong&gt;&lt;br&gt;
You stake a fixed share of the Kelly amount, usually ½ or ¼. In the simulation above, half Kelly kept 75% of the growth (+1.50% against +2.01% per bet) and cut the share of paths ending below the start from 4.6% to 0.5%. Whelan's lecture shows the fraction is not an optimum for non-log utility, so choose it by drawdown you can accept.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Kelly betting safe?&lt;/strong&gt;&lt;br&gt;
No. Even with a correct probability the median worst drawdown over 300 bets was 90% at full Kelly, and a 5-point error in &lt;em&gt;p&lt;/em&gt; at even odds turns full Kelly into double Kelly, with growth near zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do arbitrage bettors use the Kelly criterion?&lt;/strong&gt;&lt;br&gt;
Not for outcome risk. Stakes are split in proportion to 1/D so every outcome pays the same, and the size is set by book limits, exchange depth and time. In Sep 6 – Oct 6, 2026, 32% of esports arbs stayed open 5 s or more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Dozor scans 25 sportsbooks plus Polymarket and Kalshi for esports arbs, with the ROI, the age of every quote and a direct link per leg. Free plan: no card and no time limit, every arb with ROI up to 1% in full and without delay; above that you see the count and the best ROI. &lt;a href="https://dozor.bet/en" rel="noopener noreferrer"&gt;Open Dozor&lt;/a&gt; or start with the &lt;a href="https://dozor.bet/en/kelly-calculator" rel="noopener noreferrer"&gt;Kelly calculator&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Data from Dozor (Sep 6 – Oct 6, 2026), an esports arbitrage scanner covering 25 sportsbooks plus Polymarket and Kalshi. Dozor is an information tool, not a bookmaker. 18+. Betting involves risk of loss; sportsbooks may restrict arbitrage accounts.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>typescript</category>
      <category>math</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Matching the Same Esports Match Across 27 Bookmakers: Entity Resolution in Go</title>
      <dc:creator>Dozor | Esports Surebet Scanner</dc:creator>
      <pubDate>Wed, 14 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/dozor/matching-the-same-esports-match-across-27-bookmakers-entity-resolution-in-go-2dg5</link>
      <guid>https://dev.to/dozor/matching-the-same-esports-match-across-27-bookmakers-entity-resolution-in-go-2dg5</guid>
      <description>&lt;p&gt;An arbitrage scanner is only as good as its answer to a boring question: are these two listings the same match? Our scanner reads esports odds from 27 books — sportsbooks, crypto sportsbooks, Polymarket and Kalshi. The same CS2 match shows up with different team spellings, different IDs, different tournament names and start times that disagree by minutes. Get the match wrong in one direction and you miss arbs. Get it wrong in the other and you show an "arb" between two different matches — and someone bets real money on it. &lt;strong&gt;A false merge is worse than a split&lt;/strong&gt;, and most of the design follows from that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline is a pure function
&lt;/h2&gt;

&lt;p&gt;Every tick, the resolver hands the matcher the full current snapshot of all books and gets back a world of clusters plus proposals for a human. The matcher does no I/O, reads no database, and does not look at the clock. The same input always produces the same world.&lt;/p&gt;

&lt;p&gt;That was not always true. The previous core kept a long-lived index in Postgres, mutated it during the cycle and minted IDs into it, so the result depended on event order and on what had already been written. Recomputing the world from scratch every tick turned out to be cheaper to reason about and fast enough: the whole working set is in memory anyway.&lt;/p&gt;

&lt;p&gt;The stages: normalize names → resolve discipline → apply identity rules → group into time slots → find splits by anchor → decide → merge for this tick only.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Normalize names, but not too much
&lt;/h2&gt;

&lt;p&gt;Team names go through Unicode NFKD, combining marks are stripped, full case-folding is applied, punctuation becomes spaces, and whitespace is collapsed. Then a small set of organisation tokens that do not identify a roster is removed — &lt;code&gt;esports&lt;/code&gt;, &lt;code&gt;esport&lt;/code&gt;, &lt;code&gt;gaming&lt;/code&gt;, &lt;code&gt;team&lt;/code&gt;, &lt;code&gt;club&lt;/code&gt; — so "PaiN" and "PaiN Gaming" meet. Duo-team connectors ("A x B" vs "A &amp;amp; B") are dropped, and different conventions for women's rosters are mapped to one marker.&lt;/p&gt;

&lt;p&gt;Just as important is what is &lt;strong&gt;not&lt;/strong&gt; removed: &lt;code&gt;academy&lt;/code&gt;, &lt;code&gt;junior&lt;/code&gt; and digits stay, because they mean a different team. Normalization is cached, with the cache reset when it overflows — keys come from third-party feeds, and one broken feed can stream unique strings forever. One small performance lesson: creating a Unicode case-folder per call cost 1.47 s in a resolver profile; pooling them removed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Time slots anchored to the first event
&lt;/h2&gt;

&lt;p&gt;Within a pair of normalized names, events are cut into time slots with a 90-minute window. The slot boundary is measured from the slot's &lt;strong&gt;anchor&lt;/strong&gt;, not chained from event to event; otherwise events an hour apart would chain into one slot of unbounded width.&lt;/p&gt;

&lt;p&gt;Live events get a directional tolerance of six hours backwards, because some books report a live match without an honest start time. That tolerance created a subtle bug: with "first slot that fits", the live leg of game two landed in game one's slot, and the cluster combined prematch odds from one game with live odds from another. The fix is to choose the &lt;strong&gt;nearest&lt;/strong&gt; compatible slot, which also means the normal window always beats the tolerance.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Find splits by a shared opponent, not by similar names
&lt;/h2&gt;

&lt;p&gt;String similarity fails in both directions. "NAVI" vs "Natus Vincere" is 18% similar and obviously the same team. "Evil Geniuses" vs "Evil Geniuses GC" is 81% similar and a different roster.&lt;/p&gt;

&lt;p&gt;So the matcher first looks for evidence that does not depend on spelling: two groups with a &lt;strong&gt;common opponent&lt;/strong&gt;, the same discipline and close start times, but different names on the other side. That is almost certainly one match listed two ways. Each group is registered under both of its canonical keys in an inverted index, so finding candidates is two hash lookups — a team plays one or two matches at a time. The previous implementation scanned the whole index on every miss, every tick.&lt;/p&gt;

&lt;p&gt;Two rules keep the anchor honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Placeholders are never anchors.&lt;/strong&gt; "TBD" and "Bye" appear in every unfilled bracket slot; letting them in made the candidate check quadratic and proved nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If one book shows both sides, they are two events&lt;/strong&gt; (games of a series), not a split.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Similarity only decides how fast to merge
&lt;/h2&gt;

&lt;p&gt;Once an anchor says "probably the same match", similarity decides whether to merge now or ask a human. It is deliberately conservative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an initialism matches only if one name's token equals the initials of &lt;strong&gt;all&lt;/strong&gt; tokens of the other ("ic" does not match "inner circle academy");&lt;/li&gt;
&lt;li&gt;both names must be at least 5 characters;&lt;/li&gt;
&lt;li&gt;a differing token from a block list — &lt;code&gt;academy&lt;/code&gt;, &lt;code&gt;junior&lt;/code&gt;, &lt;code&gt;youth&lt;/code&gt;, &lt;code&gt;women&lt;/code&gt;, colour markers like &lt;code&gt;blue&lt;/code&gt;/&lt;code&gt;white&lt;/code&gt; — means a different team;&lt;/li&gt;
&lt;li&gt;a one-sided addition (all tokens of one name contained in the other, like "gc") is vetoed;&lt;/li&gt;
&lt;li&gt;a diff made only of organisation suffixes is fine;&lt;/li&gt;
&lt;li&gt;otherwise, normalized Levenshtein similarity of at least 0.7.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Let automation write rules — with guardrails
&lt;/h2&gt;

&lt;p&gt;Merges from a single tick are ephemeral. The durable truth about team identity is one rules file with exactly one writer, the resolver. Humans add rules through bot buttons; automation may also add one, but only with strong evidence that does not depend on spelling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;starts within &lt;strong&gt;5 minutes&lt;/strong&gt; — on a sample of 185 multi-book clusters, 90% of real matches started within five minutes of each other across books and 53% in the same minute, while repeated games of the same pairing are about a quarter of an hour apart;&lt;/li&gt;
&lt;li&gt;the books agree on a meaningful tournament token;&lt;/li&gt;
&lt;li&gt;at least &lt;strong&gt;two independent feed groups&lt;/strong&gt; on each side (mirrors of one vendor's line do not count twice);&lt;/li&gt;
&lt;li&gt;at most 20 automatic writes per hour, and a human "never" is an absolute veto.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one-sided-addition veto exists because of an incident: two names, one being the other plus a regional suffix, were merged and split back and forth, rewriting the file 86 times per second. With the veto, that pair is never auto-merged; and structurally, after a real merge both sides normalize to the same key, so there is nothing left to flip.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Decide on the outcome, not on the intention
&lt;/h2&gt;

&lt;p&gt;One last lesson cost us a silent failure. The matcher used to suppress the "please confirm this merge" question whenever it decided an automatic merge would happen. But between that decision and the file on disk sit the hourly limit, the human veto and the write itself — any of them can refuse. When one did, nobody got a question or a summary, and the split stayed invisible. Now the proposal is always created, and it is suppressed only after the write is confirmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it powers
&lt;/h2&gt;

&lt;p&gt;The matched world feeds a server-side arb engine that recomputes every market each cycle, with exchange fees applied per market. Per-book and per-pair statistics from the result are public and update daily: &lt;a href="https://dozor.bet/en/bookmakers" rel="noopener noreferrer"&gt;bookmakers&lt;/a&gt;, &lt;a href="https://dozor.bet/en/pairs" rel="noopener noreferrer"&gt;pairs&lt;/a&gt;. Why mirrors of one vendor are kept apart: &lt;a href="https://dozor.bet/en/glossary/feed-group" rel="noopener noreferrer"&gt;feed groups&lt;/a&gt;. The exchange side of the pipeline is in our earlier post on &lt;a href="https://dev.to/dozor/streaming-polymarket-and-kalshi-order-books-over-websockets-7-lessons-from-production-3khf"&gt;streaming Polymarket and Kalshi order books&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Dozor is an esports arbitrage scanner covering 25 sportsbooks plus Polymarket and Kalshi. It is an information tool, not a bookmaker. 18+.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>algorithms</category>
      <category>datascience</category>
      <category>esports</category>
    </item>
    <item>
      <title>Streaming Polymarket and Kalshi Order Books over WebSockets: 7 Lessons from Production</title>
      <dc:creator>Dozor | Esports Surebet Scanner</dc:creator>
      <pubDate>Sat, 10 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/dozor/streaming-polymarket-and-kalshi-order-books-over-websockets-7-lessons-from-production-3khf</link>
      <guid>https://dev.to/dozor/streaming-polymarket-and-kalshi-order-books-over-websockets-7-lessons-from-production-3khf</guid>
      <description>&lt;p&gt;We run a live arbitrage scanner that prices esports markets on &lt;a href="https://dozor.bet/en/bookmakers/polymarket" rel="noopener noreferrer"&gt;Polymarket&lt;/a&gt; and &lt;a href="https://dozor.bet/en/bookmakers/kalshi" rel="noopener noreferrer"&gt;Kalshi&lt;/a&gt; against 25 sportsbooks. Both exchanges are read through WebSocket order-book channels in Go (&lt;code&gt;gorilla/websocket&lt;/code&gt;). A wrong top-of-book here is not a cosmetic bug: the price becomes decimal odds, the odds become an arb, and the arb is someone's real money. These are the lessons that cost us the most to learn.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. On Polymarket, trust the server's best bid/ask, not the book you rebuilt
&lt;/h2&gt;

&lt;p&gt;Polymarket's public &lt;code&gt;/ws/market&lt;/code&gt; channel sends a &lt;code&gt;book&lt;/code&gt; snapshot and then &lt;code&gt;price_change&lt;/code&gt; deltas that also carry the server's &lt;code&gt;best_bid&lt;/code&gt;/&lt;code&gt;best_ask&lt;/code&gt;. The obvious design is to apply deltas to a local book and read the top of it. We measured that on a live channel — 1,200 tokens, 5 minutes, 24.6k deltas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;About &lt;strong&gt;1% of deltas&lt;/strong&gt; left our locally rebuilt book with a different top-of-book than the server reported, and the divergence was sticky — it did not converge by itself.&lt;/li&gt;
&lt;li&gt;We checked who was right by forcing a fresh snapshot: of 106 divergences, the new &lt;code&gt;book&lt;/code&gt; confirmed the &lt;strong&gt;server's best in 87 cases&lt;/strong&gt; and our local book in one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the price comes from the server's &lt;code&gt;best_bid&lt;/code&gt;/&lt;code&gt;best_ask&lt;/code&gt;, and the divergence itself became a &lt;strong&gt;desync detector&lt;/strong&gt;: if levels disagree with the server, depth from that book is not trusted and the book is rebuilt.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Resync by unsubscribe + subscribe — a plain re-subscribe does nothing
&lt;/h2&gt;

&lt;p&gt;To rebuild one token's book we unsubscribe and subscribe it again; the server sends a fresh &lt;code&gt;book&lt;/code&gt; in &lt;strong&gt;89 ms on average (119 ms max)&lt;/strong&gt;. Re-subscribing without unsubscribing first does not work: the channel answers with the text &lt;code&gt;NO NEW ASSETS&lt;/code&gt; and sends no snapshot.&lt;/p&gt;

&lt;p&gt;Resyncs are batched (every 500 ms, at most 200 tokens per tick) with a per-token cooldown, so a burst of divergences cannot turn into a storm of subscription frames. One more limit we hit: a subscribe/unsubscribe message carries at most &lt;strong&gt;500 asset IDs&lt;/strong&gt;; anything past that is dropped without an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Know who answers your heartbeat
&lt;/h2&gt;

&lt;p&gt;On Polymarket, protocol-level ping frames are answered — but by the Cloudflare edge, not by the origin. A dead stream behind a live edge keeps answering control pings, so a read deadline extended by those pongs would keep a frozen book "alive". We use the channel's application-level heartbeat instead: a text &lt;code&gt;PING&lt;/code&gt; every 10 s, a text &lt;code&gt;PONG&lt;/code&gt; back from the channel itself, and a 30 s read deadline. Writes get their own 5 s deadline — without it, a full send buffer hangs the book forever and silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. On Kalshi, server pings must extend your read deadline
&lt;/h2&gt;

&lt;p&gt;Kalshi does the opposite: the server sends control pings every 10 seconds, and those pings are the sign of a live channel. Our read loop extended the 30 s deadline only on data frames. &lt;code&gt;gorilla/websocket&lt;/code&gt;'s default ping handler replies with a pong but does not touch the read deadline.&lt;/p&gt;

&lt;p&gt;Most of the week that did not matter — order books change often enough. Then, every Thursday during Kalshi's maintenance window (07:00–09:00 UTC, when trading pauses), books stop changing, and the connection dropped every &lt;strong&gt;30–80 seconds&lt;/strong&gt; with &lt;code&gt;i/o timeout&lt;/code&gt;. Each reconnect cleared the cache and re-fetched about a thousand snapshots. It was not a regression; it had happened every Thursday since we moved Kalshi to WebSockets. The fix is four lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;pong&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PingHandler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetPingHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;appData&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetReadDeadline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readWait&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;pong&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;appData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A downstream guard that holds the last good snapshot when a book suddenly empties kept the drops off users' screens — which is also why it took weeks to notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Kalshi's &lt;code&gt;seq&lt;/code&gt; is per subscription, not per market
&lt;/h2&gt;

&lt;p&gt;We subscribed &lt;code&gt;orderbook_delta&lt;/code&gt; to 30 tickers at once. The 30 &lt;code&gt;orderbook_snapshot&lt;/code&gt; messages arrived with &lt;code&gt;seq&lt;/code&gt; 1 to 30, and every delta after that, across all tickers, continued one shared sequence (31, 32, 33…). A gap in &lt;code&gt;seq&lt;/code&gt; therefore means the whole subscription is suspect: we rebuild the subscription, not one market. Partial recovery would be a guess, not documented behaviour.&lt;/p&gt;

&lt;p&gt;The WebSocket handshake is signed: an RSA-PSS signature of &lt;code&gt;timestamp_ms + "GET" + "/trade-api/ws/v2"&lt;/code&gt; in the &lt;code&gt;KALSHI-ACCESS-*&lt;/code&gt; headers. An unsigned request gets &lt;code&gt;401&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Use WebSockets only where they pay off
&lt;/h2&gt;

&lt;p&gt;Measured traffic on Kalshi: 30 tickers produced 158 deltas in 25 s — 6.3 messages/s, about 0.2 per ticker. For the ~640 open esports markets that day, that extrapolates to roughly &lt;strong&gt;135 messages/s&lt;/strong&gt; in one subscription on one connection, far from needing sharding.&lt;/p&gt;

&lt;p&gt;Market structure is a different story. &lt;code&gt;market_lifecycle_v2&lt;/code&gt; cannot be filtered by ticker, so it streams the lifecycle of the whole exchange — weather, politics, economics — and its messages carry no tag that says "esports". We kept structure on REST (&lt;code&gt;/series?category=Sports&lt;/code&gt;, then &lt;code&gt;/events&lt;/code&gt; per series, refreshed every 15 s) and moved only the order books to WebSockets, which is where the REST budget was going.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Decimals, stale books and one reconnect loop
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decimals, not floats.&lt;/strong&gt; Odds are computed as &lt;code&gt;1/best&lt;/code&gt;, so a rounding error becomes a phantom arb. Prices are compared as decimals, not strings: the server does not guarantee the same text for one price in a snapshot and in a later delta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A stale book is worse than none.&lt;/strong&gt; On disconnect we drop the books instead of keeping them. For Polymarket the scanner then falls back to the slower Gamma listing price until fresh snapshots arrive; a frozen book would quote prices that no longer exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One reconnect loop for everyone.&lt;/strong&gt; Polymarket, Kalshi and a third WebSocket book each had their own copy of the lifecycle and backoff code (1 s, doubling to 30 s). A fix in one copy did not reach the others, so they now share one package.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this feeds
&lt;/h2&gt;

&lt;p&gt;These caches feed a canonical market model shared with the sportsbook adapters; a matcher maps the same match across 27 books, and a server-side engine recomputes arbs on every cycle, net of each market's exchange fee. The data side of the project is in our previous post, &lt;a href="https://dev.to/dozor/polymarket-kalshi-arbitrage-vs-sportsbooks-30-days-of-esports-data-1dc2"&gt;Polymarket &amp;amp; Kalshi Arbitrage vs Sportsbooks: 30 Days of Esports Data&lt;/a&gt;. The scanner itself: &lt;a href="https://dozor.bet/en/prediction-markets" rel="noopener noreferrer"&gt;prediction market arbitrage&lt;/a&gt;; the price conversion it relies on: &lt;a href="https://dozor.bet/en/odds-converter" rel="noopener noreferrer"&gt;odds converter&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Dozor is an esports arbitrage scanner covering 25 sportsbooks plus Polymarket and Kalshi. It is an information tool, not a bookmaker. 18+.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>websocket</category>
      <category>polymarket</category>
      <category>kalshi</category>
    </item>
    <item>
      <title>Polymarket &amp; Kalshi Arbitrage vs Sportsbooks: 30 Days of Esports Data</title>
      <dc:creator>Dozor | Esports Surebet Scanner</dc:creator>
      <pubDate>Tue, 06 Oct 2026 13:51:39 +0000</pubDate>
      <link>https://dev.to/dozor/polymarket-kalshi-arbitrage-vs-sportsbooks-30-days-of-esports-data-1dc2</link>
      <guid>https://dev.to/dozor/polymarket-kalshi-arbitrage-vs-sportsbooks-30-days-of-esports-data-1dc2</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; Over 30 days (Sep 6 – Oct 6, 2026) our scanner logged about &lt;strong&gt;4.87 million&lt;/strong&gt; arbitrage opportunities in esports across 27 books: sportsbooks, crypto sportsbooks and two prediction markets, Polymarket and Kalshi.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;About 1 in 6 arbs (15.9%)&lt;/strong&gt; had a Polymarket or Kalshi leg. Polymarket alone appeared in 642,253, Kalshi in 138,017.&lt;/li&gt;
&lt;li&gt;Arbs &lt;strong&gt;between Polymarket and Kalshi themselves are rare&lt;/strong&gt;: 4,435 in a month, 0.09% of all arbs. The other side of a prediction-market arb is almost always a sportsbook.&lt;/li&gt;
&lt;li&gt;Arbs with an exchange leg &lt;strong&gt;last longer&lt;/strong&gt;. In live play, 23.9% of them survive 10 seconds or more, versus 14.4% for sportsbook-only arbs. The median lifetime of a Polymarket arb is 4.1 s, twice the overall 2.0 s.&lt;/li&gt;
&lt;li&gt;In prematch, exchange arbs pay more: median ROI &lt;strong&gt;2.30%&lt;/strong&gt; versus 1.54% for sportsbook-only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below: the dataset, how we measured it, the tables, and a short look at the pipeline that produces the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What counts as an arb
&lt;/h2&gt;

&lt;p&gt;An arbitrage (a "surebet") is a set of bets on every outcome of the same market, placed at different books, where the implied probabilities add up to less than 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1/k1 + 1/k2 &amp;lt; 1          ROI = 1 / (1/k1 + 1/k2) - 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example: Team A to win the match at 2.10 on a sportsbook, Team B to win at a price that converts to 2.05 decimal on Polymarket (a 48.8¢ share). &lt;code&gt;1/2.10 + 1/2.05 = 0.964&lt;/code&gt;, so the arb returns about 3.7% whatever the result. Prediction-market prices are probabilities, so they need converting first: &lt;a href="https://dozor.bet/en/odds-converter" rel="noopener noreferrer"&gt;odds converter&lt;/a&gt;, &lt;a href="https://dozor.bet/en/calculator" rel="noopener noreferrer"&gt;surebet calculator&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dataset and method
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Window:&lt;/strong&gt; 30 days, Sep 6 – Oct 6, 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage:&lt;/strong&gt; esports only — CS2, Dota 2, League of Legends, Valorant, Rainbow Six and smaller titles. 25 sportsbooks and crypto sportsbooks plus Polymarket and Kalshi. Full list with per-book stats: &lt;a href="https://dozor.bet/en/bookmakers" rel="noopener noreferrer"&gt;bookmakers&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lifetime&lt;/strong&gt; of an arb = last time it was seen minus first time it was seen. All books are read at the same time and every arb is recomputed on each cycle, so lifetime has a resolution of about 2 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ROI&lt;/strong&gt; = the best ROI the arb reached during its life.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caveats.&lt;/strong&gt; The set of books changed during the window, so daily volumes are not a market trend. A slower scanner sees fewer short-lived arbs and a longer median. An arb seen in a single snapshot is often a timing difference between two quotes rather than a real mispricing — which is why we show &lt;a href="https://dozor.bet/en/glossary/price-age" rel="noopener noreferrer"&gt;price age&lt;/a&gt; on every leg.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Finding 1: prediction markets are in 1 of 6 arbs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Arbs, 30 days&lt;/th&gt;
&lt;th&gt;Share of all arbs&lt;/th&gt;
&lt;th&gt;Live&lt;/th&gt;
&lt;th&gt;Prematch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;All books&lt;/td&gt;
&lt;td&gt;~4.87M&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;81%&lt;/td&gt;
&lt;td&gt;19%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With Polymarket&lt;/td&gt;
&lt;td&gt;642,253&lt;/td&gt;
&lt;td&gt;13.2%&lt;/td&gt;
&lt;td&gt;64%&lt;/td&gt;
&lt;td&gt;36%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With Kalshi&lt;/td&gt;
&lt;td&gt;138,017&lt;/td&gt;
&lt;td&gt;2.8%&lt;/td&gt;
&lt;td&gt;65%&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polymarket + Kalshi together&lt;/td&gt;
&lt;td&gt;4,435&lt;/td&gt;
&lt;td&gt;0.09%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things stand out. Exchanges are relatively stronger in prematch: a third of their arbs happen before the match starts, versus a fifth across all books. And Polymarket and Kalshi rarely disagree with each other on esports — when they are mispriced, they are mispriced against sportsbooks. If you are looking for "Polymarket vs Kalshi arbitrage" in esports, the volume is on the &lt;a href="https://dozor.bet/en/prediction-markets" rel="noopener noreferrer"&gt;prediction market vs sportsbook&lt;/a&gt; side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 2: exchange arbs live longer
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Arb type&lt;/th&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Median life&lt;/th&gt;
&lt;th&gt;90th percentile&lt;/th&gt;
&lt;th&gt;Lasting ≥10 s&lt;/th&gt;
&lt;th&gt;Lasting ≥60 s&lt;/th&gt;
&lt;th&gt;Median ROI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sportsbooks only&lt;/td&gt;
&lt;td&gt;Live&lt;/td&gt;
&lt;td&gt;2.0 s&lt;/td&gt;
&lt;td&gt;14.2 s&lt;/td&gt;
&lt;td&gt;14.4%&lt;/td&gt;
&lt;td&gt;1.2%&lt;/td&gt;
&lt;td&gt;3.37%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With an exchange leg&lt;/td&gt;
&lt;td&gt;Live&lt;/td&gt;
&lt;td&gt;2.0 s&lt;/td&gt;
&lt;td&gt;24.4 s&lt;/td&gt;
&lt;td&gt;23.9%&lt;/td&gt;
&lt;td&gt;2.9%&lt;/td&gt;
&lt;td&gt;3.09%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sportsbooks only&lt;/td&gt;
&lt;td&gt;Prematch&lt;/td&gt;
&lt;td&gt;14.1 s&lt;/td&gt;
&lt;td&gt;93.5 s&lt;/td&gt;
&lt;td&gt;55.9%&lt;/td&gt;
&lt;td&gt;17.8%&lt;/td&gt;
&lt;td&gt;1.54%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With an exchange leg&lt;/td&gt;
&lt;td&gt;Prematch&lt;/td&gt;
&lt;td&gt;10.3 s&lt;/td&gt;
&lt;td&gt;177.4 s&lt;/td&gt;
&lt;td&gt;51.7%&lt;/td&gt;
&lt;td&gt;17.3%&lt;/td&gt;
&lt;td&gt;2.30%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In live play the median is the same, but the tail is much fatter: an arb with a Polymarket or Kalshi leg is &lt;strong&gt;1.7x more likely&lt;/strong&gt; to survive 10 seconds and &lt;strong&gt;2.4x more likely&lt;/strong&gt; to survive a minute. Across both modes, 44.6% of exchange arbs live 5 seconds or more, versus 29.7% for sportsbook-only arbs.&lt;/p&gt;

&lt;p&gt;By book, the median lifetime is 4.1 s for Polymarket and 4.0 s for Kalshi, against 2.0 s for all arbs. Arbs lasting a minute or more make up 8.3% of Polymarket arbs and 6.9% of Kalshi arbs, against 4.5% overall. Per-book pages: &lt;a href="https://dozor.bet/en/bookmakers/polymarket" rel="noopener noreferrer"&gt;Polymarket&lt;/a&gt;, &lt;a href="https://dozor.bet/en/bookmakers/kalshi" rel="noopener noreferrer"&gt;Kalshi&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Our reading (a hypothesis, not a measured cause): sportsbooks reprice esports lines with trading teams and feeds, while exchange prices move only when someone trades. A stale order sits in the book until it is taken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 3: the pairs that produce them
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pair&lt;/th&gt;
&lt;th&gt;Arbs&lt;/th&gt;
&lt;th&gt;Lasting ≥60 s&lt;/th&gt;
&lt;th&gt;Median life&lt;/th&gt;
&lt;th&gt;Median ROI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://dozor.bet/en/pairs/olimp-polymarket" rel="noopener noreferrer"&gt;Olimp – Polymarket&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;135,476&lt;/td&gt;
&lt;td&gt;7,738&lt;/td&gt;
&lt;td&gt;6.1 s&lt;/td&gt;
&lt;td&gt;2.81%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://dozor.bet/en/pairs/duel-polymarket" rel="noopener noreferrer"&gt;Duel – Polymarket&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;80,260&lt;/td&gt;
&lt;td&gt;11,062&lt;/td&gt;
&lt;td&gt;4.2 s&lt;/td&gt;
&lt;td&gt;1.96%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://dozor.bet/en/pairs/onexbet-polymarket" rel="noopener noreferrer"&gt;1xBet – Polymarket&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;69,719&lt;/td&gt;
&lt;td&gt;5,437&lt;/td&gt;
&lt;td&gt;4.0 s&lt;/td&gt;
&lt;td&gt;2.96%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BetBoom – Polymarket&lt;/td&gt;
&lt;td&gt;39,014&lt;/td&gt;
&lt;td&gt;3,849&lt;/td&gt;
&lt;td&gt;6.4 s&lt;/td&gt;
&lt;td&gt;3.91%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marathon – Polymarket&lt;/td&gt;
&lt;td&gt;37,755&lt;/td&gt;
&lt;td&gt;2,575&lt;/td&gt;
&lt;td&gt;6.1 s&lt;/td&gt;
&lt;td&gt;3.47%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kalshi – Olimp&lt;/td&gt;
&lt;td&gt;25,469&lt;/td&gt;
&lt;td&gt;1,413&lt;/td&gt;
&lt;td&gt;4.2 s&lt;/td&gt;
&lt;td&gt;2.82%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All pairs with daily-updated stats: &lt;a href="https://dozor.bet/en/pairs" rel="noopener noreferrer"&gt;pairs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding 4: which esports titles
&lt;/h2&gt;

&lt;p&gt;CS2 dominates everywhere, but exchanges lean towards slower titles. League of Legends is 18% of Polymarket arbs and 26% of Kalshi arbs, against about 7% across all books. Dota 2 is 20% of Polymarket arbs. Slower games mean fewer price jumps per minute, which fits the longer lifetimes above. More on titles: &lt;a href="https://dozor.bet/en/esports" rel="noopener noreferrer"&gt;esports arbitrage&lt;/a&gt;, &lt;a href="https://dozor.bet/en/blog/live-vs-prematch-surebets" rel="noopener noreferrer"&gt;live vs prematch&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the pipeline works
&lt;/h2&gt;

&lt;p&gt;For the developers reading this, a short version of what produces these numbers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One adapter per book.&lt;/strong&gt; Each sportsbook is read by its own process (Python) and normalized into a canonical market model: &lt;code&gt;(kind, scope, line)&lt;/code&gt; — match winner, map winner, handicap, total, rounds, kills.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prediction markets in Go.&lt;/strong&gt; Polymarket markets are discovered through the Gamma API and priced from the CLOB WebSocket order book; Kalshi comes in through its REST API and the v2 WebSocket feed. Exchange prices are converted from probabilities to decimal odds before they meet sportsbook lines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event matching.&lt;/strong&gt; The same match has different names, IDs and start times on 27 books. A matcher (Go) resolves them into one event using team-name normalization, tournament rules and a rules file that humans and strong automatic evidence can update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-side arb engine.&lt;/strong&gt; Arbs are computed in one place, on the server, for every matched market on every cycle. When an arb disappears, its first/last seen time, max ROI, market kind and books go into a history table — the source of this article.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery.&lt;/strong&gt; The browser gets a compact WebSocket stream with ROI, the age of every quote and a direct link to the event, because in live play the question is not "is there an arb" but "is it still there".&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Practical notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Liquidity is the limit on exchanges.&lt;/strong&gt; Polymarket and Kalshi do not limit winning accounts, but an arb is only as large as the depth at the quoted price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fees and withdrawals differ.&lt;/strong&gt; See &lt;a href="https://dozor.bet/en/blog/polymarket-fees-and-withdrawal" rel="noopener noreferrer"&gt;Polymarket fees and withdrawal&lt;/a&gt; and &lt;a href="https://dozor.bet/en/blog/polymarket-or-a-bookmaker" rel="noopener noreferrer"&gt;Polymarket or a bookmaker&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sportsbooks limit arbers.&lt;/strong&gt; The exchange is often the leg that stays open longest; the sportsbook leg is the one that gets restricted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check quote age before you click.&lt;/strong&gt; A 10% "arb" is more often a stale line than a gift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Step-by-step guide: &lt;a href="https://dozor.bet/en/blog/how-to-arbitrage-polymarket-kalshi" rel="noopener noreferrer"&gt;how to arbitrage Polymarket and Kalshi&lt;/a&gt;. Lifetime study across all books: &lt;a href="https://dozor.bet/en/blog/how-long-do-surebets-last" rel="noopener noreferrer"&gt;how long do surebets last&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Data from &lt;a href="https://dozor.bet/en" rel="noopener noreferrer"&gt;Dozor&lt;/a&gt;, an esports arbitrage scanner covering 25 sportsbooks plus Polymarket and Kalshi; free plan without a card. Dozor is an information tool, not a bookmaker. 18+. Betting involves risk of loss, and sportsbooks may restrict arbitrage accounts.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>showdev</category>
      <category>polymarket</category>
      <category>go</category>
    </item>
  </channel>
</rss>
