<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>life00</title>
<link>https://life00.github.io/</link>
<atom:link href="https://life00.github.io/index.xml" rel="self" type="application/rss+xml"/>
<description>Personal website</description>
<generator>quarto-1.9.38</generator>
<lastBuildDate>Fri, 05 Jun 2026 00:00:00 GMT</lastBuildDate>
<item>
  <title>Arbitrage Inspector: Why Real Arbitrage Is Harder Than It Looks</title>
  <dc:creator>Ivan </dc:creator>
  <link>https://life00.github.io/posts/arbitrage-inspector/</link>
  <description><![CDATA[ 




<div id="fig-network" class="preview-image quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-network-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://life00.github.io/posts/arbitrage-inspector/network.png" class="preview-image img-fluid figure-img">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-network-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure&nbsp;1: A large currency network of cryptocurrencies and markets across ten crypto exchanges
</figcaption>
</figure>
</div>
<p><span style="color:#888;font-size:0.85em;">Nodes (currencies) are colored based on type (stablecoin, major, meme, altcoin). Edges (markets) inside an exchange take that exchange’s color. Gray edges connect the same currency across different exchanges.</span></p>
<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>It was a hot day in May 2025, right after I finished all my semester exams. I knew I would have plenty of time during the summer, so I had to think of something interesting, creative, and useful for me to work on during the summer. This was the perfect project to select (<em>it took more time than just summer</em> 😆).</p>
<section id="motivation" class="level2">
<h2 class="anchored" data-anchor-id="motivation">Motivation</h2>
<p>Originally, I brainstormed many different project ideas across finance. Some were more interesting than others, and some were not really useful or interesting at all. Eventually, I came to the conclusion that only <strong>market research</strong> related ideas could be both interesting and practical as a personal project.</p>
<p>By slowly narrowing the list of project ideas, I came across the topic of <strong>triangular arbitrage</strong>. It was (and probably still is) quite a popular <em>“trading strategy”</em> across the retail crypto communities. I was skeptical initially, thinking that there were already many open source projects covering this topic. However, after some research on GitHub, I realized that there were not that many <em>good</em> projects for this.</p>
<p>Despite that, my financial intuition still suggested that these markets are efficient and the arbitrage opportunities are marginal. Either way, I went for it because this idea had the most potential. I also knew it would improve my programming skills, let me properly learn the Go programming language, and most importantly allow me to practice with real-world trading exchange APIs and better understand market microstructure. The cryptocurrency ecosystem was also the best for this project because it is one of the most open financial currency markets. This project was definitely <strong>the right choice</strong>.</p>
</section>
<section id="research" class="level2">
<h2 class="anchored" data-anchor-id="research">Research</h2>
<p>I initially started researching this topic by looking at existing open source GitHub projects. There were many that implemented basic proof-of-concept modeling and optimization, however most of them did not even account for fees. There were a couple that accounted for fees, however they either did not account for liquidity or they only supported a single exchange. Therefore, I realized that none of these projects properly implemented a multi-exchange solution that accounts for fees and liquidity to identify <em>real</em> arbitrage opportunities, not merely as a proof-of-concept algorithm.</p>
<p>Existing projects primarily used two kinds of approaches to model and identify arbitrage opportunities.</p>
<ol type="1">
<li><strong>Graph approaches:</strong> use graph theory to model the currency system as nodes (currencies) connected with directed weighted edges (markets) representing exchange rates. Graph theory algorithms are used to efficiently traverse the graph and identify arbitrage opportunities.</li>
<li><strong>Mathematical programming approaches:</strong> use a mathematical programming method to model the problem as a profit maximization objective function under path-dependent constraints, typically using mixed-integer programming.</li>
</ol>
<p>Most projects used graph-based approaches because they are more intuitive, simpler to implement, and very efficient. Mathematical programming is less commonly used, likely due to higher complexity and potentially lower efficiency, even though it allows for much more versatile model formulation. There were also a few advanced approaches using machine learning methods, however the formulation and models are quite different.</p>
<p>After thorough research, I decided to select the <strong>graph approach</strong> to implement this project since it is easier to implement, much more efficient, and I already knew how to properly incorporate fees and liquidity into the model.</p>
</section>
</section>
<section id="implementation" class="level1">
<h1>Implementation</h1>
<p>I called the project <strong>Arbitrage Inspector</strong>. To implement it I selected the Go programming language. I already wanted to learn Go by using it in a project and I knew that it is quite fast and suitable for this application. Additionally, while researching available libraries for exchange APIs, I was happy to find that <a href="https://github.com/ccxt/ccxt">CCXT</a>, one of the most popular cryptocurrency exchange API libraries, was releasing its initial version for Go. I would really like to thank the developers of CCXT for developing such an amazing library, without it I probably wouldn’t have been able to implement this project.</p>
<p>After selecting <strong>Go with the CCXT library</strong>, I still had to design a good currency system model that accounts for fees and liquidity, and select a reliable arbitrage detection algorithm.</p>
<section id="model-architecture" class="level2">
<h2 class="anchored" data-anchor-id="model-architecture">Model architecture</h2>
<p>In the graph approach, the currency system is modeled as a <strong>weighted directed graph</strong>. Each node represents a specific currency. Nodes are connected with weighted directed edges which correspond to exchange rates.</p>
<p>As previously mentioned, most existing implementations were limited to a single exchange, however I wanted to make it <strong>multi-exchange</strong>. Luckily, there is a natural way to add multiple exchanges to a graph structure. This is done by having each node represent a specific currency on a specific exchange, and connecting each currency with its equivalents across all exchanges by matching a currency ticker and selecting a shared crypto network.</p>
<p>With this implementation model there are two kinds of transactions:</p>
<ol type="1">
<li><strong>Intra-exchange:</strong> buying or selling currencies on markets within an exchange</li>
<li><strong>Inter-exchange:</strong> transferring a currency from one exchange to another over a crypto network</li>
</ol>
<p>Different treatment is applied to each type of transaction. This will be further explained in the following sections.</p>
<p>To understand the model better, I created two example illustrations. At the top of the page you can see a part of a very large currency network consisting of ten exchanges, more than a thousand nodes, and thousands of edges. It was built using data obtained with Arbitrage Inspector. It demonstrates the full scale of the arbitrage detection problem.</p>
<p>To make it easier to grasp, I also created a much simpler example of the graph model with two exchanges and several currencies:</p>
<div id="cell-fig-graph-model" class="cell" data-execution_count="1">
<div id="fig-graph-model" class="cell-output cell-output-display quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-graph-model-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<div style="height:525px; width:100%;">            <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS-MML_SVG"></script><script>if (window.MathJax && window.MathJax.Hub && window.MathJax.Hub.Config) {window.MathJax.Hub.Config({SVG: {font: "STIX-Web"}});}</script>                <script>window.PlotlyConfig = {MathJaxConfig: 'local'};</script>
        <script charset="utf-8" src="https://cdn.plot.ly/plotly-3.6.0.min.js" integrity="sha256-QaOVwtVY0T02VaHrr6pnoHLCwayMJp4O5n4YyaE3rJk=" crossorigin="anonymous"></script>                <div id="05429a68-6012-44f7-819a-5e59055fb2a9" class="plotly-graph-div" style="height:100%; width:100%;"></div>            <script>                window.PLOTLYENV=window.PLOTLYENV || {};                                if (document.getElementById("05429a68-6012-44f7-819a-5e59055fb2a9")) {                    Plotly.newPlot(                        "05429a68-6012-44f7-819a-5e59055fb2a9",                        [{"hoverinfo":"text","hoverlabel":{"bgcolor":"#222","font":{"color":"white","size":12}},"hovertext":["Binance | BTC","Binance | ETH","Binance | SOL","Binance | XRP","Binance | USDT","Kraken | BTC","Kraken | ETH","Kraken | SOL","Kraken | XRP","Kraken | USDT"],"marker":{"color":["#F7931A","#627EEA","#AA42F7","#3466AA","#26A17B","#F7931A","#627EEA","#AA42F7","#3466AA","#26A17B"],"line":{"color":"white","width":1.5},"size":18},"mode":"markers+text","showlegend":false,"text":["BTC","ETH","SOL","XRP","USDT","BTC","ETH","SOL","XRP","USDT"],"textfont":{"color":"white","size":11,"weight":"bold"},"textposition":"middle center","x":[3.346243162989211,43.35705771557175,109.41230962491629,138.7151332665137,188.63686327241274,1.0785955344772002,57.42633096026784,104.75534547971745,160.97311373296276,190.32119304400314],"y":[-11.399741874655994,45.675309139936296,-49.913468016894015,33.24731139528648,-7.227896383520436,-6.709425071023279,28.155970232273464,-43.8339876035682,36.078269082703045,8.339864792339036],"z":[-2.2497068163088074,1.7669948742291126,-0.7807818031472955,0.053552881033623656,1.4988443777952316,80.89265683875908,83.05819251832808,76.55479499811781,75.92745843380148,81.03726031366891],"type":"scatter3d"},{"hoverinfo":"text","hoverlabel":{"bgcolor":"#222","font":{"color":"white","size":12}},"hovertext":["Binance | BTC\u002fUSDT \u2248 67,000.00","Binance | BTC\u002fUSDT \u2248 67,000.00",null,"Binance | ETH\u002fUSDT \u2248 3,500.00","Binance | ETH\u002fUSDT \u2248 3,500.00",null,"Binance | SOL\u002fUSDT \u2248 160.00","Binance | SOL\u002fUSDT \u2248 160.00",null,"Binance | XRP\u002fUSDT \u2248 0.5500","Binance | XRP\u002fUSDT \u2248 0.5500",null,"Binance | ETH\u002fBTC \u2248 0.0522","Binance | ETH\u002fBTC \u2248 0.0522",null,"Binance | SOL\u002fBTC \u2248 0.0024","Binance | SOL\u002fBTC \u2248 0.0024",null,"Binance | XRP\u002fBTC \u2248 0.0000","Binance | XRP\u002fBTC \u2248 0.0000",null,"Binance | SOL\u002fETH \u2248 0.0457","Binance | SOL\u002fETH \u2248 0.0457",null,"Binance | XRP\u002fETH \u2248 0.0002","Binance | XRP\u002fETH \u2248 0.0002",null],"line":{"color":"#FFD700","width":2.5},"mode":"lines","name":"Binance (intra-exchange)","x":[3.346243162989211,188.63686327241274,null,43.35705771557175,188.63686327241274,null,109.41230962491629,188.63686327241274,null,138.7151332665137,188.63686327241274,null,43.35705771557175,3.346243162989211,null,109.41230962491629,3.346243162989211,null,138.7151332665137,3.346243162989211,null,109.41230962491629,43.35705771557175,null,138.7151332665137,43.35705771557175,null],"y":[-11.399741874655994,-7.227896383520436,null,45.675309139936296,-7.227896383520436,null,-49.913468016894015,-7.227896383520436,null,33.24731139528648,-7.227896383520436,null,45.675309139936296,-11.399741874655994,null,-49.913468016894015,-11.399741874655994,null,33.24731139528648,-11.399741874655994,null,-49.913468016894015,45.675309139936296,null,33.24731139528648,45.675309139936296,null],"z":[-2.2497068163088074,1.4988443777952316,null,1.7669948742291126,1.4988443777952316,null,-0.7807818031472955,1.4988443777952316,null,0.053552881033623656,1.4988443777952316,null,1.7669948742291126,-2.2497068163088074,null,-0.7807818031472955,-2.2497068163088074,null,0.053552881033623656,-2.2497068163088074,null,-0.7807818031472955,1.7669948742291126,null,0.053552881033623656,1.7669948742291126,null],"type":"scatter3d"},{"hoverinfo":"text","hoverlabel":{"bgcolor":"#222","font":{"color":"white","size":12}},"hovertext":["Kraken | BTC\u002fUSDT \u2248 67,000.00","Kraken | BTC\u002fUSDT \u2248 67,000.00",null,"Kraken | ETH\u002fUSDT \u2248 3,500.00","Kraken | ETH\u002fUSDT \u2248 3,500.00",null,"Kraken | SOL\u002fUSDT \u2248 160.00","Kraken | SOL\u002fUSDT \u2248 160.00",null,"Kraken | XRP\u002fUSDT \u2248 0.5500","Kraken | XRP\u002fUSDT \u2248 0.5500",null,"Kraken | ETH\u002fBTC \u2248 0.0522","Kraken | ETH\u002fBTC \u2248 0.0522",null,"Kraken | SOL\u002fBTC \u2248 0.0024","Kraken | SOL\u002fBTC \u2248 0.0024",null,"Kraken | XRP\u002fBTC \u2248 0.0000","Kraken | XRP\u002fBTC \u2248 0.0000",null,"Kraken | SOL\u002fETH \u2248 0.0457","Kraken | SOL\u002fETH \u2248 0.0457",null,"Kraken | XRP\u002fETH \u2248 0.0002","Kraken | XRP\u002fETH \u2248 0.0002",null],"line":{"color":"#A855F7","width":2.5},"mode":"lines","name":"Kraken (intra-exchange)","x":[1.0785955344772002,190.32119304400314,null,57.42633096026784,190.32119304400314,null,104.75534547971745,190.32119304400314,null,160.97311373296276,190.32119304400314,null,57.42633096026784,1.0785955344772002,null,104.75534547971745,1.0785955344772002,null,160.97311373296276,1.0785955344772002,null,104.75534547971745,57.42633096026784,null,160.97311373296276,57.42633096026784,null],"y":[-6.709425071023279,8.339864792339036,null,28.155970232273464,8.339864792339036,null,-43.8339876035682,8.339864792339036,null,36.078269082703045,8.339864792339036,null,28.155970232273464,-6.709425071023279,null,-43.8339876035682,-6.709425071023279,null,36.078269082703045,-6.709425071023279,null,-43.8339876035682,28.155970232273464,null,36.078269082703045,28.155970232273464,null],"z":[80.89265683875908,81.03726031366891,null,83.05819251832808,81.03726031366891,null,76.55479499811781,81.03726031366891,null,75.92745843380148,81.03726031366891,null,83.05819251832808,80.89265683875908,null,76.55479499811781,80.89265683875908,null,75.92745843380148,80.89265683875908,null,76.55479499811781,83.05819251832808,null,75.92745843380148,83.05819251832808,null],"type":"scatter3d"},{"hoverinfo":"text","hoverlabel":{"bgcolor":"#222","font":{"color":"white","size":12}},"hovertext":["1 BTC (Binance) \u2192 0.99933 BTC (Kraken)","1 BTC (Binance) \u2192 0.99933 BTC (Kraken)",null,"1 ETH (Binance) \u2192 0.99938 ETH (Kraken)","1 ETH (Binance) \u2192 0.99938 ETH (Kraken)",null,"1 SOL (Binance) \u2192 0.99952 SOL (Kraken)","1 SOL (Binance) \u2192 0.99952 SOL (Kraken)",null,"1 XRP (Binance) \u2192 0.99922 XRP (Kraken)","1 XRP (Binance) \u2192 0.99922 XRP (Kraken)",null,"1 USDT (Binance) \u2192 0.99962 USDT (Kraken)","1 USDT (Binance) \u2192 0.99962 USDT (Kraken)",null],"line":{"color":"#00CCCC","width":2.5},"mode":"lines","name":"inter-exchange","x":[3.346243162989211,1.0785955344772002,null,43.35705771557175,57.42633096026784,null,109.41230962491629,104.75534547971745,null,138.7151332665137,160.97311373296276,null,188.63686327241274,190.32119304400314,null],"y":[-11.399741874655994,-6.709425071023279,null,45.675309139936296,28.155970232273464,null,-49.913468016894015,-43.8339876035682,null,33.24731139528648,36.078269082703045,null,-7.227896383520436,8.339864792339036,null],"z":[-2.2497068163088074,80.89265683875908,null,1.7669948742291126,83.05819251832808,null,-0.7807818031472955,76.55479499811781,null,0.053552881033623656,75.92745843380148,null,1.4988443777952316,81.03726031366891,null],"type":"scatter3d"},{"hoverinfo":"skip","mode":"text","showlegend":false,"text":["Binance","Kraken"],"textfont":{"color":["#FFD700","#A855F7"],"size":16,"weight":"bold"},"x":[-35,-35],"y":[0,0],"z":[0,80],"type":"scatter3d"}],                        {"template":{"data":{"histogram2dcontour":[{"type":"histogram2dcontour","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"choropleth":[{"type":"choropleth","colorbar":{"outlinewidth":0,"ticks":""}}],"histogram2d":[{"type":"histogram2d","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"heatmap":[{"type":"heatmap","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"contourcarpet":[{"type":"contourcarpet","colorbar":{"outlinewidth":0,"ticks":""}}],"contour":[{"type":"contour","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"surface":[{"type":"surface","colorbar":{"outlinewidth":0,"ticks":""},"colorscale":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]]}],"mesh3d":[{"type":"mesh3d","colorbar":{"outlinewidth":0,"ticks":""}}],"scatter":[{"fillpattern":{"fillmode":"overlay","size":10,"solidity":0.2},"type":"scatter"}],"parcoords":[{"type":"parcoords","line":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scatterpolargl":[{"type":"scatterpolargl","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"bar":[{"error_x":{"color":"#2a3f5f"},"error_y":{"color":"#2a3f5f"},"marker":{"line":{"color":"#E5ECF6","width":0.5},"pattern":{"fillmode":"overlay","size":10,"solidity":0.2}},"type":"bar"}],"scattergeo":[{"type":"scattergeo","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scatterpolar":[{"type":"scatterpolar","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"histogram":[{"marker":{"pattern":{"fillmode":"overlay","size":10,"solidity":0.2}},"type":"histogram"}],"scattergl":[{"type":"scattergl","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scatter3d":[{"type":"scatter3d","line":{"colorbar":{"outlinewidth":0,"ticks":""}},"marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scattermap":[{"type":"scattermap","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scattermapbox":[{"type":"scattermapbox","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scatterternary":[{"type":"scatterternary","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"scattercarpet":[{"type":"scattercarpet","marker":{"colorbar":{"outlinewidth":0,"ticks":""}}}],"carpet":[{"aaxis":{"endlinecolor":"#2a3f5f","gridcolor":"white","linecolor":"white","minorgridcolor":"white","startlinecolor":"#2a3f5f"},"baxis":{"endlinecolor":"#2a3f5f","gridcolor":"white","linecolor":"white","minorgridcolor":"white","startlinecolor":"#2a3f5f"},"type":"carpet"}],"table":[{"cells":{"fill":{"color":"#EBF0F8"},"line":{"color":"white"}},"header":{"fill":{"color":"#C8D4E3"},"line":{"color":"white"}},"type":"table"}],"barpolar":[{"marker":{"line":{"color":"#E5ECF6","width":0.5},"pattern":{"fillmode":"overlay","size":10,"solidity":0.2}},"type":"barpolar"}],"pie":[{"automargin":true,"type":"pie"}]},"layout":{"autotypenumbers":"strict","colorway":["#636efa","#EF553B","#00cc96","#ab63fa","#FFA15A","#19d3f3","#FF6692","#B6E880","#FF97FF","#FECB52"],"font":{"color":"#2a3f5f"},"hovermode":"closest","hoverlabel":{"align":"left"},"paper_bgcolor":"white","plot_bgcolor":"#E5ECF6","polar":{"bgcolor":"#E5ECF6","angularaxis":{"gridcolor":"white","linecolor":"white","ticks":""},"radialaxis":{"gridcolor":"white","linecolor":"white","ticks":""}},"ternary":{"bgcolor":"#E5ECF6","aaxis":{"gridcolor":"white","linecolor":"white","ticks":""},"baxis":{"gridcolor":"white","linecolor":"white","ticks":""},"caxis":{"gridcolor":"white","linecolor":"white","ticks":""}},"coloraxis":{"colorbar":{"outlinewidth":0,"ticks":""}},"colorscale":{"sequential":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]],"sequentialminus":[[0.0,"#0d0887"],[0.1111111111111111,"#46039f"],[0.2222222222222222,"#7201a8"],[0.3333333333333333,"#9c179e"],[0.4444444444444444,"#bd3786"],[0.5555555555555556,"#d8576b"],[0.6666666666666666,"#ed7953"],[0.7777777777777778,"#fb9f3a"],[0.8888888888888888,"#fdca26"],[1.0,"#f0f921"]],"diverging":[[0,"#8e0152"],[0.1,"#c51b7d"],[0.2,"#de77ae"],[0.3,"#f1b6da"],[0.4,"#fde0ef"],[0.5,"#f7f7f7"],[0.6,"#e6f5d0"],[0.7,"#b8e186"],[0.8,"#7fbc41"],[0.9,"#4d9221"],[1,"#276419"]]},"xaxis":{"gridcolor":"white","linecolor":"white","ticks":"","title":{"standoff":15},"zerolinecolor":"white","automargin":true,"zerolinewidth":2},"yaxis":{"gridcolor":"white","linecolor":"white","ticks":"","title":{"standoff":15},"zerolinecolor":"white","automargin":true,"zerolinewidth":2},"scene":{"xaxis":{"backgroundcolor":"#E5ECF6","gridcolor":"white","linecolor":"white","showbackground":true,"ticks":"","zerolinecolor":"white","gridwidth":2},"yaxis":{"backgroundcolor":"#E5ECF6","gridcolor":"white","linecolor":"white","showbackground":true,"ticks":"","zerolinecolor":"white","gridwidth":2},"zaxis":{"backgroundcolor":"#E5ECF6","gridcolor":"white","linecolor":"white","showbackground":true,"ticks":"","zerolinecolor":"white","gridwidth":2}},"shapedefaults":{"line":{"color":"#2a3f5f"}},"annotationdefaults":{"arrowcolor":"#2a3f5f","arrowhead":0,"arrowwidth":1},"geo":{"bgcolor":"white","landcolor":"#E5ECF6","subunitcolor":"white","showland":true,"showlakes":true,"lakecolor":"white"},"title":{"x":0.05},"mapbox":{"style":"light"},"margin":{"b":0,"l":0,"r":0,"t":30}}},"margin":{"l":0,"r":0,"t":0,"b":0},"scene":{"xaxis":{"visible":false},"yaxis":{"visible":false},"zaxis":{"visible":false},"camera":{"eye":{"x":1.3,"y":1.5,"z":0.8},"center":{"x":0,"y":0,"z":0}},"bgcolor":"rgba(0,0,0,0)","aspectmode":"data"},"legend":{"font":{"color":"#ccc","size":11},"x":0.02,"y":0.98,"xanchor":"left","yanchor":"top","bgcolor":"rgba(0,0,0,0.4)"},"paper_bgcolor":"rgba(0,0,0,0)","plot_bgcolor":"rgba(0,0,0,0)"},                        {"responsive": true}                    ).then(function(){
                            
var gd = document.getElementById('05429a68-6012-44f7-819a-5e59055fb2a9');
var x = new MutationObserver(function (mutations, observer) {{
        var display = window.getComputedStyle(gd).display;
        if (!display || display === 'none') {{
            console.log([gd, 'removed!']);
            Plotly.purge(gd);
            observer.disconnect();
        }}
}});

// Listen for the removal of the full notebook cells
var notebookContainer = gd.closest('#notebook-container');
if (notebookContainer) {{
    x.observe(notebookContainer, {childList: true});
}}

// Listen for the clearing of the current output cell
var outputEl = gd.closest('.output');
if (outputEl) {{
    x.observe(outputEl, {childList: true});
}}

                        })                };            </script>        </div>
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-graph-model-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure&nbsp;2: Multi-exchange currency system represented as a graph
</figcaption>
</figure>
</div>
</div>
<p>Once the graph is constructed, arbitrage detection becomes a graph algorithm problem. If the product of weights along a cycle exceeds 1, there is an arbitrage opportunity. By taking the negative natural logarithm of each weight, the multiplicative problem becomes additive. Therefore, a profitable cycle corresponds to a cycle with a negative sum of transformed weights, also known as a <strong>negative weight cycle</strong>. I used the <strong>Bellman-Ford algorithm</strong> to find these because it handles negative weights natively and runs in <img src="https://latex.codecogs.com/png.latex?O(V%5Ctimes%20E)">. This is a rather standard implementation for arbitrage detection in a graph structure.</p>
<p>However, there is one more detail: investment capital should be stored somewhere. It can be stored in a single source node, but that is not ideal. Instead, I changed the model to allow for multiple source assets by adding a virtual <strong>super-source</strong> node at index 0 connected to each source asset with weight 1. This way, Bellman-Ford finds cycles reachable from any of the capital sources in a single pass. This extension allows storing capital in many exchanges at the same time, potentially reducing transaction costs and increasing exposure to arbitrage opportunities.</p>
<p>Of course, the model above assumes no transaction costs and <strong>ignores real complexities</strong>: variable fees, fixed fees, and order book liquidity. The next three sections explain how I dealt with each of these issues.</p>
</section>
<section id="accounting-for-fees" class="level2">
<h2 class="anchored" data-anchor-id="accounting-for-fees">Accounting for fees</h2>
<p>Depending on the size of the transaction, fees can significantly impact the actual cost of the transaction, and thus the arbitrage opportunity. Therefore, it is crucial to account for all kinds of fees. Accounting for them is rather straightforward. The fee can be incorporated into the exchange rate before a graph is constructed, making all weights represent <strong>net exchange rate</strong>.</p>
<p>For intra-exchange transactions it is pretty simple because taker/maker fees are proportional to the transacted amount; just apply the formula <img src="https://latex.codecogs.com/png.latex?w%5E%7B%5Ctext%7Bintra%7D%7D_%7Bij%7D=e_%7Bij%7D%5Ctimes(1-f_%7B%5Ctext%7Bmarket%7D%7D)"> to each of the weights. However, for inter-exchange transactions it is much more complicated because network transaction fees are denominated in fixed amount of the transacted currency (e.g., you pay a fixed 0.00001 SOL to transfer any amount of SOL across exchanges).</p>
<p>The fixed nature of network transaction fees makes them incompatible with the weighted directed graph model because the transacted quantity itself determines the relative proportion of the fee, and therefore of the weight as well. Given that I was already too invested in this modeling approach, I decided to find a workaround which would allow me to at least approximate the weights of inter-exchange edges.</p>
<p>After some brainstorming, I figured out that it is still possible to convert the fixed fees into percentage fees, but only if I know the nominal value of the transaction relative to the fee. In order to do that, I had to first obtain the nominal value of the investment capital in all possible currencies, which I did by computing shortest paths to all currencies from a reference currency (e.g.&nbsp;USDT) and then calculating the nominal value of the investment in all currencies using those shortest path values. Note that this assumes a <strong>fixed amount of investment capital</strong>. Afterward, when computing the inter-exchange fees, I had to calculate the amount of <strong>network fee relative to the total capital</strong> to find the correct inter-exchange weights: <img src="https://latex.codecogs.com/png.latex?w%5E%7B%5Ctext%7Binter%7D%7D_%7Bij%7D=1%5Ctimes%20(1-%5Cfrac%7Bf_%7B%5Ctext%7Bnetwork%7D_%7Bij%7D%7D%7D%7BI_%7Bi%7D%7D)">.</p>
</section>
<section id="accounting-for-liquidity" class="level2">
<h2 class="anchored" data-anchor-id="accounting-for-liquidity">Accounting for liquidity</h2>
<p>Bid and ask prices may <strong>not</strong> be a good representation of <strong>actual market prices</strong>. Market orderbooks have varying amounts of liquidity on both sides, and it is crucial to properly account for it. However, accounting for liquidity is not very straightforward.</p>
<p>Similar to how I handled the fixed network fees, I also had to assume a fixed amount of investment capital. Then I could reuse the previously calculated nominal value of investment in all currencies to compute volume-weighted average price (VWAP) for both bid and ask sides. This approach allows me to properly account for liquidity, assuming fixed investment capital.</p>
<p><img src="https://latex.codecogs.com/png.latex?%5Cdisplaystyle%20VWAP%20=%20%5Cfrac%7B%5Cdisplaystyle%5Csum_%7Bi=1%7D%5E%7Bn%7D%20(P_i%20%5Ctimes%20Q_i)%7D%7B%5Cdisplaystyle%5Csum_%7Bi=1%7D%5E%7Bn%7D%20Q_i%7D"></p>
<p>However, I missed one crucial implementation detail. It is rather difficult to obtain orderbooks for <em>thousands</em> of markets in real time… 😅</p>
<p>CCXT does support a REST API method for obtaining orderbooks for all markets in an exchange in one API call, however very few exchanges support this endpoint. There is also a similar method for WebSocket (WS) connections to listen to orderbooks of multiple markets at the same time (with a max market limit). Therefore, I designed a dynamic <strong>watcher</strong> system which spawns individual exchange watchers, which spawn individual workers to listen to dedicated WS connections. The watcher runs asynchronously along the main logic and continuously collects new orderbook updates. Once data refresh is needed, the watcher synchronizes all cached orderbook data and updates the main market orderbook data structure.</p>
<p>In the end, it became a sophisticated dynamic asynchronous system which unfortunately didn’t really work due to hidden WS API differences and rate limits. I even tried fine-tuning the frequency of WS connection requests, number of markets per WS connection, and other parameters to avoid rate limits, however only a few exchanges provide consistent results. Therefore, due to technical limitations, it is not really possible to account for liquidity by precalculating all VWAP prices.</p>
<p>Ideally, this networking restriction could have been solved by increasing API limits or using proxies to watch WS connections, however that probably would have been overly complicated.</p>
<p>As a temporary workaround, the main branch of the project includes the <em>fetcher</em> version of the algorithm which ignores liquidity when constructing a graph and checks it only after it finds a potential arbitrage opportunity. The watcher version is also available in a dedicated branch, however it only supports a few tested exchanges.</p>
</section>
<section id="development" class="level2">
<h2 class="anchored" data-anchor-id="development">Development</h2>
<p>The actual code implementation was a bit tricky because of many interdependent processes in the model. It required well-designed data structures, robust functions, and coordinated architecture to minimize the time from updating market data to identifying a concrete arbitrage opportunity.</p>
<p>The code is structured following separation of concerns based on functionality. The client (CLI, TUI, etc.) orchestrates the main processes using six internal packages/modules:</p>
<ol type="1">
<li><strong>Engine:</strong> responsible for optimization algorithms</li>
<li><strong>Fetch:</strong> responsible for fetching data using CCXT via REST APIs</li>
<li><strong>Models:</strong> includes all shared data structures for config, exchange market data, graph structures, etc.</li>
<li><strong>Trade:</strong> responsible for verifying and executing trades (wasn’t fully implemented)</li>
<li><strong>Transform:</strong> includes all main data transformation functionality</li>
<li><strong>Watch:</strong> includes the watcher implemented using CCXT via WebSocket APIs</li>
</ol>
<p>For more implementation details you may check the <a href="https://github.com/life00/arbitrage-inspector/tree/master/docs/packages">project package documentation</a> for each of the internal packages used in the execution process.</p>
<p>The main control flow process is split into four key stages: initialization, periodic update, continuous update, and trade. Each stage handles a different kind of processing. You may find the process control flow illustration below.</p>
<details>
<summary>
Process control flow (click to expand)
</summary>
<div id="fig-process" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-process-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://life00.github.io/posts/arbitrage-inspector/process.png" class="img-fluid figure-img">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-process-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure&nbsp;3: Process control flow
</figcaption>
</figure>
</div>
</details>
<p>Notice that continuous update is only responsible for updating the market data and finding the arbitrage. Other, less frequently updated data is handled by periodic update, such as inter-exchange weights and investment capital denominations in all currencies. This ensures that only the most essential data is updated on each arbitrage detection refresh.</p>
</section>
</section>
<section id="evaluation" class="level1">
<h1>Evaluation</h1>
<p>Overall, I think the <strong>project is a success</strong> despite the implementation issues and limitations because it let me learn and work with quant finance in practice. Given that I am now aware of the technical and algorithmic limitations, this project could potentially be redesigned to follow a more robust and realistic architecture.</p>
<section id="results" class="level2">
<h2 class="anchored" data-anchor-id="results">Results</h2>
<p>Using the developed algorithm, I was able to see for myself that <strong>arbitrage opportunities slowly diminish</strong> when accounting for fees and liquidity. Without fees or liquidity, it can easily identify major arbitrage opportunities with <strong>+3% return</strong>. With fees but without liquidity, it can identify marginal arbitrage opportunities with <strong>1% return</strong>. Once liquidity is also considered, it is rarely able to reliably identify any arbitrage opportunities.</p>
<p>I also realized that investment quantity is another crucial factor to account for. If it is too large, then arbitrage opportunities disappear due to insufficient liquidity, while if it is too small, the opportunity is diminished by fixed fees. This creates an additional <strong>quantity optimization</strong> problem where the optimal quantity must balance the costs of liquidity against fixed fees.</p>
<p>The last release (v0.7) marked the first version that could identify potentially real arbitrage in live market data. With around 2500 assets and 6000 pairs across multiple exchanges, the full detection cycle, from fetching fresh data to identifying a cycle, completes in under 400 milliseconds, which is not really ideal. <strong>Real opportunities were most reliably found with capital below $500</strong>, especially during periods of volatility. However, I did not track formal profit-and-loss over time, so these findings are observational rather than rigorously measured.</p>
<p>The algorithm also has <strong>structural gaps</strong> that likely caused it to miss opportunities. Bellman-Ford finds the first negative cycle, not the best or shortest one, and the path from source assets back to themselves after a cycle is not properly implemented for all cases.</p>
</section>
<section id="limitations-and-next-steps" class="level2">
<h2 class="anchored" data-anchor-id="limitations-and-next-steps">Limitations and Next Steps</h2>
<p>As I already mentioned, there are many limitations. The primary one is related to obtaining large quantities of orderbooks in real time. The watcher system I built to solve this became a sophisticated dynamic asynchronous system that didn’t really work due to hidden WS API differences and rate limits.</p>
<p>Beyond the data access problem, the <strong>graph model</strong> itself was the <strong>root cause of many limitations</strong>. Everything (fees, liquidity, network costs) must be compressed into a single edge weight, which forces simplifying assumptions like fixed capital. Bellman-Ford finds the first negative cycle, not the most profitable one. The path from source assets back to themselves is not properly implemented. Network transaction speeds, which can void an arbitrage opportunity before a transfer completes, cannot be expressed in a static graph at all. There are also other issues: non-optimal performance, overcomplicated model, no trade execution (the project currently detects but does not trade). You may find the full list <a href="https://github.com/life00/arbitrage-inspector/blob/master/docs/other/problems.md">here</a>.</p>
<p>Given these fundamental limitations, many of which stem from the graph modeling approach itself, a redesign from scratch would probably be more effective than patching the current one. I’ve recently been studying mathematical optimization more, and I can see how a <strong>hybrid approach</strong> could work better: scan for opportunities with graph algorithms, then optimize the quantity with mathematical programming. That would capture more of the real-world complexity that the current model has to ignore.</p>


</section>
</section>

 ]]></description>
  <category>arbitrage</category>
  <category>cryptocurrency</category>
  <category>graph theory</category>
  <category>optimization</category>
  <guid>https://life00.github.io/posts/arbitrage-inspector/</guid>
  <pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate>
  <media:content url="https://life00.github.io/posts/arbitrage-inspector/network.png" medium="image" type="image/png" height="101" width="144"/>
</item>
<item>
  <title>Fountain and Snowballs</title>
  <dc:creator>Ivan </dc:creator>
  <link>https://life00.github.io/posts/fountain-and-snowballs/</link>
  <description><![CDATA[ 




<section id="introduction" class="level1">
<h1>Introduction</h1>
<p>It was the end of exam period during winter holidays. There was a thin layer of snow everywhere. While walking through the city center I noticed a fountain covered with a thin layer of snow. I casually started throwing snowballs into the fountain. The snowballs were passing through the snow layer and creating holes. Then I thought of a game: what if I throw multiple snowballs to clear out holes, and then take one final throw to score into those previously made holes? Afterwards, I thought to myself: <em>What is the probability to score into a hole with that final snowball?</em>, and I realized this can be calculated using basic statistics!</p>
</section>
<section id="information" class="level1">
<h1>Information</h1>
<ul>
<li>area of the fountain
<ul>
<li><img src="https://latex.codecogs.com/png.latex?S_%7Bf%7D=1,000%5C%20%5Ctext%7Bcm%7D%5E2"></li>
</ul></li>
<li>average area of the hole created from a snowball
<ul>
<li><img src="https://latex.codecogs.com/png.latex?S_%7BE(s)%7D=10%5C%20%5Ctext%7Bcm%7D%5E2"></li>
</ul></li>
<li>total number of snowballs thrown (<img src="https://latex.codecogs.com/png.latex?x-1"> setup throws + <img src="https://latex.codecogs.com/png.latex?1"> final target throw)
<ul>
<li><img src="https://latex.codecogs.com/png.latex?X"></li>
</ul></li>
<li>probability of success on the final throw
<ul>
<li><img src="https://latex.codecogs.com/png.latex?P(S_x)"></li>
</ul></li>
<li>probability of failure on the final throw
<ul>
<li><img src="https://latex.codecogs.com/png.latex?P(F_x)"></li>
</ul></li>
</ul>
</section>
<section id="assumptions" class="level1">
<h1>Assumptions</h1>
<ul>
<li>The person who throws the snowballs is throwing them completely randomly</li>
<li>All throws will always be scored somewhere within the area of the fountain</li>
<li>The first <img src="https://latex.codecogs.com/png.latex?x-1"> setup throws each create a brand new, unique hole in the snow layer that does not overlap with any previously made holes</li>
<li>The final (<img src="https://latex.codecogs.com/png.latex?x">-th) throw scores a success if it lands inside an existing hole, and a failure if it lands on remaining snow</li>
</ul>
</section>
<section id="solution" class="level1">
<h1>Solution</h1>
<p>The first <img src="https://latex.codecogs.com/png.latex?x-1"> setup throws clear a total area equal to <img src="https://latex.codecogs.com/png.latex?S_%7BE(s)%7D%20%5Ccdot%20(x-1)">. Because the final throw lands completely randomly, the probability of it landing in a hole scales linearly with the total area cleared by the setup throws. Considering that the area of the fountain (<img src="https://latex.codecogs.com/png.latex?S_f">) is limited, after a certain number of setup throws the probability of success on the final throw will be <img src="https://latex.codecogs.com/png.latex?100%5C%25">.</p>
<ul>
<li>number of attempts to achieve maximum probability
<ul>
<li><img src="https://latex.codecogs.com/png.latex?%5Cdisplaystyle%20X_%7B%5Ctext%7Bmax%7D%7D=%5Cfrac%7BS_%7Bf%20%7D%7D%7BS_%7BE(s)%7D%7D+1=%5Cfrac%7B1,000%20%7D%7B10%20%7D+1=101"></li>
<li><img src="https://latex.codecogs.com/png.latex?+1"> accounts for the final target throw</li>
</ul></li>
<li>probability function
<ul>
<li><img src="https://latex.codecogs.com/png.latex?%5Cdisplaystyle%5Cfrac%7BS_%7BE(x)%7D%5Ccdot(x-1)%7D%7BS_f%7D=%5Cfrac%7B10(x-1)%7D%7B1,000%7D=%5Cfrac%7Bx%20%7D%7B100%20%7D-%5Cfrac%7B1%20%7D%7B100%7D=0.01x-0.01"></li>
<li><img src="https://latex.codecogs.com/png.latex?p(x)=%5Cbegin%7Bcases%7D0.01x-0.01%20&amp;x%20%5Cin%20%5C%7B1,2,3,%5Cdots,100%5C%7D%5C%5C%201&amp;x%5Cge%20101%5C%5C0&amp;%5Ctext%7Botherwise%7D%5Cend%7Bcases%7D"></li>
</ul></li>
<li>example
<ul>
<li>What is the probability of scoring on the final throw if you throw <img src="https://latex.codecogs.com/png.latex?20"> snowballs in total (<img src="https://latex.codecogs.com/png.latex?19"> setup throws <img src="https://latex.codecogs.com/png.latex?+"> <img src="https://latex.codecogs.com/png.latex?1"> target throw)?</li>
<li><img src="https://latex.codecogs.com/png.latex?P(S_%7B20%7D)=p(20)=0.19"></li>
<li><img src="https://latex.codecogs.com/png.latex?P(F_%7B20%7D)=1-p(20)=0.81"></li>
</ul></li>
</ul>
<p>Of course, these assumptions are quite unrealistic for a real-world game, but they allow to explore this elegant probability puzzle!</p>


</section>

 ]]></description>
  <category>probability theory</category>
  <category>probability puzzle</category>
  <guid>https://life00.github.io/posts/fountain-and-snowballs/</guid>
  <pubDate>Wed, 02 Oct 2024 00:00:00 GMT</pubDate>
</item>
</channel>
</rss>
