Backtesting is how you ask history what a strategy would have done, before you risk a single riyal. Most Python backtesting tutorials assume US stocks and a data feed you have to wire up yourself. This guide does it on the Saudi Exchange (Tadawul, index TASI) with Tasilab, so the commission, VAT, and price bars are the real Saudi-market ones — not a generic approximation.
We'll run the classic RSI mean-reversion strategy — buy when RSI drops below 30 (oversold), sell when it rises above 70 (overbought) — on Saudi Aramco (2222), two ways:
- Method 1 — the hosted backtest. One HTTP call. Tasilab runs the strategy over cached historical bars and hands back the result, including a buy-and-hold comparison.
- Method 2 — roll your own. Pull the bars, compute the signal yourself in Python, simulate the fills with real fees, and log the whole run as a tracked experiment.
Before you start
You need a free Tasilab account and an API key. Sign up — it takes a minute and comes with a paper portfolio of SAR 100,000 in simulated cash. Copy your API key from the dashboard, then install the SDK and export the key:
pip install tasilab
export TASILAB_API_KEY="your-api-key"
Method 1 — the hosted backtest (one call)
The fastest path is POST /v1/backtest/run. You describe the strategy as data — indicator, thresholds, symbol, window — and Tasilab does the rest. Here is the RSI mean-reversion run on Aramco over the last year (1Y ≈ 250 trading days):
curl -X POST https://api.tasilab.com/v1/backtest/run \
-H "X-API-Key: $TASILAB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"symbol": "2222",
"period": "1Y",
"strategy_type": "threshold",
"indicator": "RSI",
"params": {"period": 14},
"buy_op": "<", "buy_threshold": 30,
"sell_op": ">", "sell_threshold": 70
}'
The same request from Python, reading the two numbers that actually matter:
import os, requests
resp = requests.post(
"https://api.tasilab.com/v1/backtest/run",
headers={"X-API-Key": os.environ["TASILAB_API_KEY"]},
json={
"symbol": "2222", # Saudi Aramco
"period": "1Y", # ~250 trading days
"strategy_type": "threshold",
"indicator": "RSI",
"params": {"period": 14},
"buy_op": "<", "buy_threshold": 30, # oversold -> buy
"sell_op": ">", "sell_threshold": 70, # overbought -> sell
},
timeout=60,
)
resp.raise_for_status()
r = resp.json()
print(f"Strategy : {r['return_pct']:+.2f}% ({r['num_trades']} trades)")
print(f"Buy & hold: {r['buy_hold_return_pct']:+.2f}%")
print(f"Final : {r['final_value']} SAR vs {r['buy_hold_final_value']} SAR held")
The response is a full record of the run — not just a number. The fields you'll use most:
return_pct/return_sar— how the strategy did.buy_hold_return_pct/buy_hold_final_value— how simply holding the stock over the same window did. This is the benchmark.num_trades,num_bars— how active the strategy was, over how many days.trades— every BUY/SELL leg with date, price, quantity, and value.equity_curve— daily portfolio value, ready to plot.run_id— a handle to the saved run.
Reading history's verdict
The single most useful line in that output is the comparison between return_pct and buy_hold_return_pct. A strategy is only interesting if it beats the boring alternative of buying the stock and doing nothing.
Here is the uncomfortable part, and the reason we show you both numbers by default: a clever-sounding rule often loses to buy-and-hold. Classic RSI mean-reversion on a strong trending name frequently sells its winners too early and underperforms. Tasilab reports that plainly — it does not sugar-coat history, and neither should your own analysis. An honest "this edge is weak" is worth more than a backtest tuned until it looks good.
return_pct comes in below buy-and-hold, that is a real result, not a failure of the tool. The discipline of comparing against the benchmark — every time — is most of what separates a tradeable edge from a story.Method 2 — roll your own, and track it
The hosted backtest covers the common indicator strategies. When you want full control — custom signals, custom position sizing, your own metrics — pull the bars and run the loop yourself, then log the run as a Tasilab experiment so it is reproducible and comparable later. Here is the shape, using a simple moving-average crossover:
import os
from decimal import Decimal
from tasilab import Tasilab
COMMISSION, VAT = Decimal("0.00155"), Decimal("0.15") # real TASI fees
def fees(price, qty):
commission = price * qty * COMMISSION
return commission + commission * VAT
tasi = Tasilab(api_key=os.environ["TASILAB_API_KEY"])
# 1) real historical bars for Saudi Aramco
bars = tasi.get_historical("2222", "2024-01-01", "2025-12-31")["bars"]
closes = [float(b["close"]) for b in bars]
# 2) your own signal — a 10/30 SMA crossover here
def sma(v, p): return [None]*(p-1) + [sum(v[i-p+1:i+1])/p for i in range(p-1, len(v))]
fast, slow = sma(closes, 10), sma(closes, 30)
# 3) simulate fills with real fees, collect trades ... (loop omitted)
# 4) log the run as a tracked experiment
with tasi.create_experiment(
name="SMA 10/30 on Saudi Aramco",
symbol="2222", start_date="2024-01-01", end_date="2025-12-31",
parameters={"fast": 10, "slow": 30, "starting_capital_sar": 100000},
) as exp:
exp.log_trades(trades)
exp.log_metrics({"total_return_pct": total_return_pct, "num_trades": len(trades)})
print("Logged experiment:", exp.id)
Every run you log shows up under Experiments in your dashboard with its parameters, metrics, trade list, and equity curve — so you can line up "SMA 10/30" against "RSI 30/70" against "MACD 12/26/9" on the same symbol and window, and let the numbers decide. That is the difference between running a backtest and building a track record.
Why doing it on TASI specifically matters
You could approximate all of this with a generic backtesting library and a free data source. What you would miss is exactly what determines whether an edge survives contact with the real Saudi market:
- Real fees. Commission of
0.155%plus15%VAT on the commission, applied to every fill — the same math the exchange uses. A high-frequency rule that looks profitable before fees often is not after them. - Real bars and tick rules. Actual TASI historical prices, not a proxy index.
- Real trading hours. Sunday–Thursday, 10:00–15:00 AST — the calendar your live strategy would actually trade on.
Model the market you're going to trade, not a convenient stand-in for it.
Next: read the full API reference, or explore the other hosted strategies — ma_crossover, price_ma_cross, and macd_signal_cross — by swapping strategy_type in the request above.
