<?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: Muhamed Ashraf</title>
    <description>The latest articles on DEV Community by Muhamed Ashraf (@7ashraf).</description>
    <link>https://dev.to/7ashraf</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3951608%2Fbb267df7-962d-494c-ab6a-709e3b8a4da5.jpeg</url>
      <title>DEV Community: Muhamed Ashraf</title>
      <link>https://dev.to/7ashraf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/7ashraf"/>
    <language>en</language>
    <item>
      <title>Building an Open-Source API for Egyptian Stock Market Data</title>
      <dc:creator>Muhamed Ashraf</dc:creator>
      <pubDate>Tue, 26 May 2026 03:33:22 +0000</pubDate>
      <link>https://dev.to/7ashraf/building-an-open-source-api-for-egyptian-stock-market-data-56f3</link>
      <guid>https://dev.to/7ashraf/building-an-open-source-api-for-egyptian-stock-market-data-56f3</guid>
      <description>&lt;p&gt;A few months ago I was working on an investments page inside an app. Standard stuff — a portfolio view, some live prices, a few charts, the ability to track stocks the user cared about.&lt;/p&gt;

&lt;p&gt;The Egyptian Exchange (EGX) data piece was the last thing on my list. I figured it would be the easiest. Just &lt;code&gt;pip install&lt;/code&gt; whichever library people use, plug in an API key, ship the feature.&lt;/p&gt;

&lt;p&gt;That's not how Egyptian market data works.&lt;/p&gt;

&lt;p&gt;Two days of dead ends later I was writing my own data layer — a small Python service that stitched together multiple providers, fell back between them when any of them rate-limited, and gave the app one clean interface to call. It worked. The investments page launched. I kept using the script internally.&lt;/p&gt;

&lt;p&gt;After a while it became increasingly obvious that I wasn't the only Egyptian developer who'd hit this wall. So I cleaned it up, packaged it properly, and open sourced it. That's &lt;strong&gt;Borsa&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post is about why I built it, how it works, and the piece of the implementation I'm most proud of.&lt;/p&gt;

&lt;h4&gt;
  
  
  The problem: programmatic EGX access barely exists
&lt;/h4&gt;

&lt;p&gt;If you trade or build on US equities you're spoiled for choice. yfinance, Polygon, Tiingo, Alpaca, IEX Cloud — pick your poison. The data is clean, the libraries are mature, and there's an entire ecosystem of tooling that "just works."&lt;/p&gt;

&lt;p&gt;For EGX, the landscape is brutal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bloomberg Terminal&lt;/strong&gt; covers EGX well. It also costs around $20,000+/year per seat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reuters Eikon / Refinitiv Workspace&lt;/strong&gt; has it too. Institutional pricing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mubasher&lt;/strong&gt; is what most retail traders in Egypt actually use. There's no public API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct EGX data feeds&lt;/strong&gt; are sold to brokerages, not to individuals.&lt;/li&gt;
&lt;li&gt;A handful of consumer providers (Alpha Vantage, Finnhub, Yahoo Finance) include &lt;em&gt;some&lt;/em&gt; EGX coverage — but each covers a different partial slice, with different intraday support, different rate limits, and different terms of service.
For one of the largest stock exchanges in MENA, the message to individual developers is essentially: pay $20k+/year or stitch it together yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I stitched it together myself.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why existing tools didn't fit
&lt;/h4&gt;

&lt;p&gt;I looked, hard, before resigning myself to writing my own. There are scrapers people have published as gists. There's &lt;code&gt;pandas_datareader&lt;/code&gt;, which is mostly unmaintained for international markets. There are wrappers for individual providers that don't talk to each other. There are SaaS startups in the broader MENA fintech space, but most are closed and product-focused, not infrastructure.&lt;/p&gt;

&lt;p&gt;What I couldn't find was anything that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Abstracted over the consumer providers that &lt;em&gt;do&lt;/em&gt; cover EGX,&lt;/li&gt;
&lt;li&gt;Fell back gracefully when one rate-limited me, and&lt;/li&gt;
&lt;li&gt;Ran on infrastructure I controlled.
So the script I wrote for the app did all three. It was rough at first — hard-coded mappings, single file, no tests — but it worked. Once I'd been using it for a few weeks and the rough edges had smoothed out, turning it into a proper open-source project felt like the next natural step.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  The architecture decision: self-host + BYOK
&lt;/h4&gt;

&lt;p&gt;Two choices shaped what Borsa became when I cleaned it up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-hostable.&lt;/strong&gt; Borsa runs as a FastAPI service in a single Docker container. No SaaS, no signup, no usage tracking. You clone the repo, set env vars for whichever providers you want, and &lt;code&gt;docker compose up&lt;/code&gt;. The whole thing fits on a $5 VPS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bring your own keys (BYOK).&lt;/strong&gt; I don't proxy requests through my own keys. You sign up directly with Alpha Vantage, Finnhub, or Yahoo Finance, paste the keys into &lt;code&gt;.env&lt;/code&gt;, and Borsa uses them. This dodges two problems: I don't manage a shared rate-limit pool (your quota is yours), and I don't have to handle billing if you outgrow free tiers.&lt;/p&gt;

&lt;p&gt;The combination — self-host + BYOK — is also what makes it legally clean. Borsa is MIT, providers have their own ToS, and the relationship is just "you brought their key to my code." No reselling, no aggregation business, no risk of getting yanked offline.&lt;/p&gt;

&lt;h4&gt;
  
  
  Technical walkthrough
&lt;/h4&gt;

&lt;p&gt;The request flow is unsurprising: FastAPI receives a request, checks the cache, falls through to a provider chain if cold, and returns a unified response shape regardless of which provider answered.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/routes.py
&lt;/span&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/quote/{symbol}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;QuoteResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;QuoteResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;canonical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize_symbol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbol&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;cached&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canonical&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;cached&lt;/span&gt;
    &lt;span class="n"&gt;quote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;provider_chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For batch requests — say, fetching quotes for 30 symbols at once — &lt;code&gt;asyncio.gather&lt;/code&gt; parallelizes the work. Each symbol runs through its own provider chain independently, so a rate-limit on one symbol doesn't block the others.&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_quotes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbols&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;QuoteResult&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&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="nf"&gt;get_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&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;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;symbols&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;return_exceptions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cache is deliberately boring: &lt;code&gt;cachetools.TTLCache&lt;/code&gt; with a 60-second TTL by default, configurable per-endpoint. In-process, no Redis, no extra moving parts. For a self-hosted API that one developer is hitting from a notebook or a backtest harness, this is plenty. If you outgrow it, swap in Redis — the cache is a single dependency-injected protocol.&lt;/p&gt;

&lt;h4&gt;
  
  
  The fallback chain (the interesting bit)
&lt;/h4&gt;

&lt;p&gt;This is the piece that mattered most in the original app, and still matters in Borsa.&lt;/p&gt;

&lt;p&gt;Free-tier providers are unreliable. Alpha Vantage caps you at 25 calls/day. Finnhub at 60/minute. Yahoo isn't even rate-limited cleanly — it just starts returning empty payloads or 429s when it decides you've had enough. When my investments page depended on any &lt;em&gt;one&lt;/em&gt; of these, I had a bad day on a regular basis.&lt;/p&gt;

&lt;p&gt;The fallback chain treats providers as interchangeable for any given symbol. Try the primary; if it fails or rate-limits, transparently try the next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/providers/chain.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProviderChain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;providers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;providers&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CanonicalSymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Quote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;supports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RateLimitError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ProviderUnavailable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;NoDataAvailable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What makes this work is that every provider exposes the same shape. &lt;code&gt;Quote&lt;/code&gt; is a single dataclass, every provider implementation returns one, and the route handler doesn't know or care who answered. Adding a new provider is implementing one interface:&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CanonicalSymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Quote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_history&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CanonicalSymbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;History&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;supports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CanonicalSymbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;supports()&lt;/code&gt; matters because providers don't have identical coverage — the chain skips providers that can't serve a given symbol rather than wasting a call. Coverage and provider-specific symbol mapping live in a single YAML registry loaded at startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# symbols/egx.yaml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;canonical&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CIB&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Commercial International Bank&lt;/span&gt;
  &lt;span class="na"&gt;sector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Banking&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;alpha_vantage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CIBEA.CAI&lt;/span&gt;
    &lt;span class="na"&gt;finnhub&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;COMI.CA&lt;/span&gt;
    &lt;span class="na"&gt;yahoo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;COMI.CA&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Borsa ships with 200+ EGX symbols pre-mapped this way. Adding more is a PR against this one file — anyone can do it.&lt;/p&gt;

&lt;h4&gt;
  
  
  What I learned
&lt;/h4&gt;

&lt;p&gt;A few things stuck.&lt;/p&gt;

&lt;p&gt;Building infrastructure for a market that already has good infrastructure is a slog — everything has been done. Building for a market that &lt;em&gt;doesn't&lt;/em&gt; is invigorating, because every small improvement is the new state of the art. The first time someone other than me used Borsa to pull EGX data without manual fiddling, I understood why people enjoy infra work.&lt;/p&gt;

&lt;p&gt;Open sourcing internal tools you've already battle-tested is a much better path than building open source from scratch with no concrete user. By the time I was packaging Borsa, the design choices had been validated by months of an actual product depending on them. The hard questions were already answered.&lt;/p&gt;

&lt;p&gt;Also: rate limits will humble you. Designing for failure (the fallback chain) rather than the happy path made the whole system more honest about what it actually is — a thin, careful layer over services I don't control.&lt;/p&gt;

&lt;h4&gt;
  
  
  How to contribute
&lt;/h4&gt;

&lt;p&gt;Borsa lives at &lt;strong&gt;&lt;a href="https://github.com/7ashraf/borsa" rel="noopener noreferrer"&gt;https://github.com/7ashraf/borsa&lt;/a&gt;&lt;/strong&gt; (MIT).&lt;/p&gt;

&lt;p&gt;The live demo is at &lt;strong&gt;borsa.ashh.me&lt;/strong&gt; — please be gentle, it's a single small instance.&lt;/p&gt;

&lt;p&gt;The most useful contributions right now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;More EGX symbols&lt;/strong&gt; in &lt;code&gt;symbols/egx.yaml&lt;/code&gt;. 200+ are pre-mapped, but EGX has more and coverage maps drift over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Mubasher provider&lt;/strong&gt; for users who already have access. The &lt;code&gt;Provider&lt;/code&gt; interface is maybe 40 lines to implement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arabic translations&lt;/strong&gt; for the README and API error messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backtest examples&lt;/strong&gt; showing how to wire Borsa into &lt;code&gt;vectorbt&lt;/code&gt; or &lt;code&gt;backtrader&lt;/code&gt;.
If you're in the MENA dev or quant scene, this tool is for you as much as for me. Open an issue, send a PR, or just tell me what's missing. The point is to build the layer the ecosystem doesn't yet have.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>opensource</category>
      <category>python</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
