Any backtest, indicator, or model needs history. For the Saudi market there are a few real ways to get it, and the right one depends on what you're doing. US-market tooling doesn't map cleanly here — Tadawul-listed names use a 4-digit code, most of the free ecosystem built around yfinance/pandas-datareader was written with NYSE/NASDAQ tickers in mind, and licensed Saudi market-data vendors are a much smaller field than their US counterparts. Here's the honest version of what's actually available — no single source wins for every job.
Your options
| Source | Best for | Watch out for |
|---|---|---|
| Licensed data API (e.g. SAHMK) | Production apps needing raw, redistributable, comprehensive TASI data + fundamentals | Paid tiers; you build the storage/backtest layer yourself |
| yfinance | Quick, free exploration in a notebook (2222.SR) | Delayed, coverage/quality gaps on some TASI names, unofficial |
| Kaggle datasets | One-off study on a fixed window | Static snapshots that go stale; no live updates |
R tasi package | R users wanting prices + financial statements | R ecosystem, not Python |
| Tasilab | Backtesting & paper trading in one sandbox — bars + orders + experiments together | 15-min delayed on free tier; a sandbox, not a raw-data reseller |
If you need raw data to redistribute inside your own product, a licensed provider is the right tool. If you want to test strategies on that market — bars, simulated orders, and logged results in one place — that's what Tasilab is for.
Licensed data APIs
If you're building something that redistributes Saudi market data — a research product, an internal dashboard for a fund, a public data feed — a licensed vendor (SAHMK is the one most Saudi-market developers run into first) is the only option that's actually meant for that. You get comprehensive coverage, fundamentals alongside prices, and a commercial license that covers redistribution. What you don't get is a backtest engine or an order book — a licensed feed gives you rows of data; you still have to build the layer that turns those rows into a strategy result.
yfinance in practice
For quick, free exploration, yfinance is genuinely useful — Tadawul symbols are addressable by appending .SR to the 4-digit code (Saudi Aramco is 2222.SR, SABIC is 2010.SR, Al Rajhi Bank is 1120.SR):
import yfinance as yf
df = yf.download("2222.SR", start="2024-01-01", end="2025-12-31")
print(df[["Open", "High", "Low", "Close", "Volume"]].tail())
Three things to know before you trust the output. First, it's unofficial — Yahoo Finance doesn't publish an SLA for Tadawul coverage, so depth and history length vary by symbol, and thinly-traded names are more likely to have gaps than the large caps. Second, quotes are delayed, which is fine for a backtest on daily bars but wrong for anything that needs a live price. Third, corporate actions (splits, stock dividends) are applied inconsistently across smaller listings — always sanity-check a few known price levels against a second source before trusting a multi-year series for anything that matters.
Static datasets (Kaggle and similar)
A downloaded CSV is the fastest way to get started on a one-off study, and there are Tadawul snapshots on Kaggle and similar sites. The tradeoff is exactly what you'd expect from a static file: it's frozen at whatever date it was uploaded, it won't pick up new listings or delistings, and — the subtler problem — a dataset assembled from today's constituent list has survivorship bias baked in. Companies that were delisted, merged, or suspended during the window aren't in the file, which quietly inflates any backtest that treats "today's TASI 30" as if it were the actual investable universe on every past date.
Historical bars with Tasilab
Get a free key, then pull a date range and drop it straight into pandas:
import os
import pandas as pd
from tasilab import Tasilab
tasi = Tasilab(api_key=os.environ["TASILAB_API_KEY"])
data = tasi.get_historical("2222", "2024-01-01", "2025-12-31") # Saudi Aramco
df = pd.DataFrame(data["bars"])
df["date"] = pd.to_datetime(df["date"])
df = df.set_index("date")
print(df[["open", "high", "low", "close", "volume"]].tail())
Each bar carries symbol, date, open, high, low, close, adj_close, and volume — standard OHLCV, ready for indicators, resampling, or a chart. The same data is one GET away over HTTP:
curl "https://api.tasilab.com/v1/historical/2222?start=2024-01-01&end=2025-12-31" \
-H "X-API-Key: $TASILAB_API_KEY"
Under the hood, Tasilab pulls from yfinance and caches the result in its own database, so the first request for a symbol/range combination you haven't asked for before can take a few seconds; every request after that is served from cache. History depth is tier-gated — free accounts get 1 year, Starter and Growth get 3 years, and Pro is unlimited up to a 50-year absolute ceiling (see pricing). Asking for more than your plan allows returns a normal plan_limit_exceeded error rather than silently truncating the range, so a backtest never runs on a shorter window than you think it did.
adj_close when you need returns that account for splits and dividends; use close for the raw traded price. Mixing the two is a classic source of wrong backtest numbers.What "clean" OHLCV actually means
"Clean" isn't just "no missing rows." Two things matter more, and both are easy to get wrong on TASI specifically.
Adjustment. Several TASI blue chips — Al Rajhi Bank, STC, SABIC among them — pay meaningful regular dividends. On the ex-dividend date, the raw traded price drops by roughly the dividend amount even though nothing happened to the company's value; a return series built from unadjusted close records that drop as a loss. Run enough of those through a Sharpe ratio calculation and the strategy looks worse than it is — sometimes badly enough to flip a real edge into an apparent loser. adj_close backs that drop out, so period-over-period returns reflect what a holder actually earned, dividends included.
Gaps. Any pipeline that resamples through yfinance — Tasilab's included — occasionally hits a holiday-adjacent or thinly-traded day where the upstream feed returns a non-numeric OHLC value instead of a normal bar. Tasilab's historical endpoint filters those rows out before they ever reach your client, specifically because an unfiltered NaN silently breaks any pipeline that assumes every bar is a real number — a moving average or RSI calculation will propagate a single NaN through every value downstream of it. If you're pulling from yfinance directly instead, you don't get that filtering for free — check for non-finite values before you feed a series into an indicator.
Data quality gotchas specific to TASI
- Trading week and holidays. Tadawul trades Sunday–Thursday, not the Monday–Friday week most US-market tooling assumes by default. Ramadan brings shortened trading hours for the whole exchange, and Saudi public holidays close the market entirely — any calendar logic ported from a US-market library needs the week and holiday set corrected, or your bar-counting and "days since last trade" math will be quietly off.
- Symbol format. Tadawul-listed companies use a 4-digit numeric code (
2222for Saudi Aramco), not a ticker of letters. Most non-Saudi-specific tooling expects letters, so double-check any library's symbol validation doesn't reject a purely numeric code before you conclude the data itself is missing. - Corporate actions. Stock splits and bonus-share issuances happen on TASI the same as any exchange. A price series that isn't split-adjusted will show a sudden, spurious drop on the effective date — if a chart shows a stock "losing half its value" overnight with no news to match, a stock split is the first thing to check, not a data bug.
- Survivorship bias. Covered above for static datasets, but it applies to any workflow that starts from "today's constituent list" and pulls history for those symbols only — delisted and merged companies are invisible by construction, which flatters any backtest that implicitly assumes the investable universe never changes.
Cross-checking a source before you trust it
Whichever source you pick, the fastest way to catch a coverage or adjustment problem is to compare a handful of known prices against a second source before running a real backtest. If you're using Tasilab and want a second opinion, pulling the same symbol from yfinance alongside it takes a few lines:
import pandas as pd
import yfinance as yf
from tasilab import Tasilab
tasi = Tasilab(api_key="YOUR_API_KEY")
data = tasi.get_historical("2222", "2025-01-01", "2025-06-30")
tasilab_df = pd.DataFrame(data["bars"]).set_index("date")["close"]
yf_df = yf.download("2222.SR", start="2025-01-01", end="2025-06-30")["Close"]
yf_df.index = yf_df.index.strftime("%Y-%m-%d")
compare = pd.DataFrame({"tasilab": tasilab_df, "yfinance": yf_df}).dropna()
compare["diff_pct"] = (compare["tasilab"] - compare["yfinance"]) / compare["yfinance"] * 100
print(compare[compare["diff_pct"].abs() > 0.5]) # rows that disagree by more than 0.5%
An empty result means the two sources agree closely enough to trust either one for that window. A handful of disagreeing rows around a known dividend or split date is expected — that's the adjustment difference described above, not a bug. Disagreement scattered across ordinary trading days with no corporate action nearby is the signal to dig further before building anything on top of the data.
FAQ
Is TASI historical data free?
yfinance and Kaggle snapshots are free with the tradeoffs above. Tasilab's free tier includes 1 year of historical bars at no cost; paid tiers extend that to 3 years or unlimited. Fully licensed, redistributable data from a vendor like SAHMK is paid.
What's the difference between close and adj_close?
close is the actual traded price on that date. adj_close retroactively adjusts every prior price for splits and dividends so that period-over-period percentage changes reflect real investment return. Use adj_close for any return, drawdown, or Sharpe calculation; use close only when you specifically need the price someone actually paid on that day.
Why does my TASI backtest look different from a US-market tutorial's numbers?
Usually one of the gotchas above: a Sunday–Thursday week instead of Monday–Friday, an unadjusted close on a dividend-heavy name, or a static dataset with survivorship bias baked in. Rule those out first before assuming the strategy logic itself is wrong.
