Last month I forked my US trading platform (Dawul Trader) to build a Saudi equivalent for the Tadawul exchange. I assumed it'd be a branding-and-data-source swap. A weekend, maybe.
It took longer than that. Here's what the "just change the data feed" plan actually looked like in practice, and the five things I didn't see coming.
The stack
Same as the US version, because I wanted the ports to evolve independently without a shared-package dance:
- Backend: Python / FastAPI, Anthropic SDK for AI analysis
- Frontend: React 19, Vite, Tailwind
-
Indicators:
tafor RSI/MACD/EMA/VWAP - Data: Twelve Data REST + a Laravel MySQL DB (existing Saudi equity data)
Gotcha #1 — Your data vendor probably doesn't cover Saudi
My US version used Alpha Vantage. I was ready to flip an env var and call it done. Alpha Vantage has zero Tadawul coverage. Neither does yfinance in any reliable way.
Twelve Data does, but you have to pass exchange=SAU explicitly — otherwise their symbol resolver happily gives you an unrelated ticker from another market. Stock 2222 is Aramco on Tadawul and something completely different on a dozen other exchanges.
def _td_macd(symbol, interval="1day"):
d = _td_get("macd", {
"symbol": symbol,
"exchange": "SAU", # <-- critical
"interval": interval,
"fast_period": macd_fast,
"slow_period": macd_slow,
"signal_period": macd_signal,
"outputsize": 2,
"format": "JSON"
})
...
If you're building on Twelve Data for Saudi, bake exchange=SAU into every single endpoint call. I lost an afternoon debugging "correct numbers for the wrong company."
Gotcha #2 — No options market
Tadawul has no equity options. The entire Options Chain page, all the options strategies, the expiration logic — dead code in the Saudi version. I'd built a lot of UI around a feature that simply doesn't exist for these users.
Lesson: when porting, audit your feature list against what the target market actually trades before you migrate the code. I could have skipped two days of ripping out dependencies if I'd mapped this first.
Gotcha #3 — SELL signals in a long-only market
My US screener outputs BUY, SELL, and NO TRADE. Easy call on NYSE — retail users can short with a click.
Tadawul is effectively long-only for retail. Naked shorting is heavily restricted. So when I initially ported the screener, I considered stripping SELL entirely and labeling bearish setups as NO TRADE.
I pushed back on that when a user asked: "why no sells?" The answer is nuance — a SELL signal isn't useless in a long-only market; it just means something different:
- On NYSE: "Consider a short entry."
- On Tadawul: "Exit longs / don't enter here."
So I kept the detection logic but reframed the UI copy and made sure the signal was visible:
// Strategy2.jsx — SELL badge coexists with BUY
if (signal === "BUY") return <Badge color="green">BUY</Badge>;
if (signal === "SELL") return <Badge color="red">SELL</Badge>;
return <Badge color="gray">NO TRADE</Badge>;
The backend mirrors bullish filters into bearish ones:
buy_ok = True
sell_ok = True
if use_macd:
if macd_line > 0:
sell_ok = False # bullish -> block SELL
reasons.append("MACD bullish")
elif macd_line < 0:
buy_ok = False # bearish -> block BUY
reasons.append("MACD bearish")
else:
buy_ok = sell_ok = False # near-zero -> no trade
# Priority: BUY > SELL > NO TRADE
signal = "BUY" if buy_ok else "SELL" if sell_ok else "NO TRADE"
Same code, different semantics at the UX layer. Worth the extra 30 lines.
Gotcha #4 — Numeric tickers + Arabic names + RTL
US tickers are letters (AAPL, MSFT). Saudi tickers are numbers (2222, 1120, 7010). That sounds trivial — it breaks a surprising amount:
- Any regex you wrote assuming
[A-Z]{1,5}on ticker input. - URL params that look nothing like tickers (
/stock/2222reads like a product ID). - Users recognizing companies. Nobody in Riyadh thinks "2222," they think "أرامكو السعودية."
I wired Arabic names in alongside the numeric code, and let React handle direction:
<div className="flex flex-col">
<span>{r.symbol}</span>
{stockNames[r.symbol]?.name_ar && (
<span style={{ direction: "rtl", fontWeight: 600 }}>
{stockNames[r.symbol].name_ar}
</span>
)}
</div>
The direction: "rtl" on just the Arabic span — not the whole page — is the move. Full-page RTL breaks every Tailwind utility I had.
Gotcha #5 — The market calendar
Saudi trades Sunday through Thursday, 10:00–15:00 AST. Every "is market open?" helper I'd written assumed weekday = Mon–Fri. Every "next trading day" calculation was wrong. Every cron that ran at US market open fired on Saudi lunch.
Don't hard-code weekends. Read them from the exchange config:
SAUDI_TRADING_DAYS = {6, 0, 1, 2, 3} # Sun=6, Mon=0, ..., Thu=3 (Python weekday)
SAUDI_HOURS = (time(10, 0), time(15, 0))
One small UX win
After the SELL work shipped, I got asked a second time: "can I just copy all the BUYs to paste into my broker?"
Two-line change. The count pill becomes a button; click it, symbols hit the clipboard comma-separated:
async function copySignalSymbols(sig) {
const syms = (results || []).filter(r => r.signal === sig).map(r => r.symbol);
await navigator.clipboard.writeText(syms.join(","));
setCopied(sig);
setTimeout(() => setCopied(null), 1500);
}
Takes 30 seconds to write and users immediately noticed. Most satisfying kind of feature.
What I'd tell past me
- Pick your vendor before you write any code. Data source constraints cascade everywhere.
- Audit the market's feature surface, not your app's. Options, shorting, ETFs, fractional shares — all vary.
- Don't strip signals, reframe them. A bearish signal is useful in every market; only the recommended action changes.
- Localization is not translation. Numeric tickers, RTL text, calendar, currency, decimal format — each is its own ticket.
- Ship the tiny UX requests. Clicking a count to copy symbols is the kind of thing users remember.
If you're thinking about porting a trading tool to a new market, start with the calendar and the vendor. Everything else is downstream.
Top comments (0)