Pendahuluan
Salah satu kesalahan yang sering dilakukan investor pemula adalah langsung menggunakan sebuah strategi trading tanpa mengetahui apakah strategi tersebut benar-benar menghasilkan keuntungan.
Misalnya, seseorang mengatakan bahwa strategi Moving Average Cross selalu memberikan profit. Jika kita langsung mempercayainya tanpa melakukan pengujian, risiko kerugian tentu akan lebih besar.
Di sinilah backtesting menjadi sangat penting.
Backtesting adalah proses menguji suatu strategi menggunakan data historis untuk mengetahui bagaimana performa strategi tersebut jika diterapkan pada masa lalu.
Dengan melakukan backtesting, kita dapat menjawab pertanyaan seperti:
- Apakah strategi menghasilkan keuntungan?
- Berapa tingkat kemenangan (win rate)?
- Berapa keuntungan rata-rata?
- Berapa kerugian terbesar (maximum drawdown)?
- Berapa jumlah transaksi yang terjadi?
Pada artikel ini kita akan membangun sistem backtesting sederhana menggunakan Python dan data saham Indonesia dari Yahoo Finance.
Apa Itu Backtesting?
Backtesting adalah proses menjalankan aturan trading pada data historis.
Sebagai contoh.
Aturan:
- Buy ketika MA20 memotong MA50 dari bawah.
- Sell ketika MA20 memotong MA50 dari atas.
Kemudian aturan tersebut dijalankan pada data historis BBCA selama lima tahun.
Hasil akhirnya dapat berupa:
- Total keuntungan
- Jumlah transaksi
- Win Rate
- Drawdown
- Return tahunan
Backtesting membantu kita mengevaluasi strategi sebelum digunakan dengan uang sungguhan.
Mengapa Backtesting Penting?
Bayangkan Anda menemukan strategi trading di media sosial.
Tanpa backtesting:
❌ Langsung membeli saham.
Dengan backtesting:
✅ Menguji strategi selama lima tahun.
Jika hasilnya buruk, Anda dapat mencari strategi lain tanpa kehilangan modal.
Manfaat utama backtesting antara lain:
- Mengurangi keputusan berdasarkan emosi.
- Mengetahui performa strategi secara objektif.
- Membandingkan beberapa strategi.
- Mengoptimalkan parameter strategi.
- Mengurangi risiko sebelum investasi.
Persiapan
Install library.
pip install yfinance pandas matplotlib numpy
Import library.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import yfinance as yf
Mengambil Data Saham
Kita akan menggunakan saham BBCA.
ticker = "BBCA.JK"
df = yf.download(
ticker,
period="5y",
progress=False,
auto_adjust=True
)
Lihat data.
print(df.head())
Menghitung Moving Average
Strategi pertama menggunakan:
- MA20
- MA50
Hitung indikator.
df["MA20"] = (
df["Close"]
.rolling(20)
.mean()
)
df["MA50"] = (
df["Close"]
.rolling(50)
.mean()
)
Sekarang DataFrame memiliki dua indikator tambahan.
Membuat Grafik Moving Average
Visualisasi membantu memahami strategi sebelum diuji.
plt.figure(figsize=(14,6))
plt.plot(
df["Close"],
label="Close"
)
plt.plot(
df["MA20"],
label="MA20"
)
plt.plot(
df["MA50"],
label="MA50"
)
plt.legend()
plt.show()
Grafik akan memperlihatkan bagaimana garis MA20 bergerak lebih cepat dibandingkan MA50.
Apa Itu Golden Cross?
Golden Cross terjadi ketika:
MA20
Naik
Memotong
MA50
dari bawah ke atas.
Hal ini sering dianggap sebagai sinyal awal tren naik.
Sebaliknya:
MA20
Turun
Memotong
MA50
dari atas ke bawah disebut Death Cross dan sering dianggap sebagai sinyal pelemahan tren.
Membuat Kolom Signal
Sekarang kita buat sinyal.
df["Signal"] = 0
Jika MA20 lebih besar daripada MA50.
df.loc[
df["MA20"] > df["MA50"],
"Signal"
] = 1
Artinya.
1 = Buy
0 = Sell
Lihat hasilnya.
print(
df[
[
"Close",
"MA20",
"MA50",
"Signal"
]
].tail()
)
Mengidentifikasi Perubahan Sinyal
Yang sebenarnya kita butuhkan bukan kondisi MA20 > MA50, tetapi perubahan dari kondisi sebelumnya.
Gunakan diff().
df["Position"] = (
df["Signal"]
.diff()
)
Interpretasi:
| Position | Arti |
|---|---|
| 1 | Buy Signal |
| -1 | Sell Signal |
| 0 | Tidak ada perubahan |
Kolom Position inilah yang akan menjadi dasar simulasi transaksi pada bagian berikutnya.
Menampilkan Titik Buy dan Sell
Mari kita lihat kapan strategi memberikan sinyal.
buy = df[
df["Position"] == 1
]
sell = df[
df["Position"] == -1
]
Jumlah transaksi:
print("Buy :", len(buy))
print("Sell:", len(sell))
Informasi ini membantu mengetahui seberapa aktif strategi tersebut selama periode pengujian.
Konsep Simulasi Backtesting
Dalam artikel ini kita menggunakan aturan sederhana:
- Buy ketika MA20 memotong MA50 dari bawah ke atas (Golden Cross).
- Sell ketika MA20 memotong MA50 dari atas ke bawah (Death Cross).
- Selama belum ada sinyal jual, posisi tetap dipertahankan.
Untuk menyederhanakan simulasi:
- Tidak memperhitungkan biaya broker.
- Tidak memperhitungkan pajak.
- Tidak menggunakan margin.
- Tidak melakukan pembelian bertahap (scaling in).
Pendekatan ini cukup untuk memahami konsep dasar backtesting.
Membuat Kolom Posisi
Kita akan menggunakan kolom Signal yang dibuat pada bagian sebelumnya.
df["Position"] = df["Signal"].shift(1)
Mengapa menggunakan shift(1)?
Karena keputusan transaksi dilakukan setelah sinyal muncul. Dengan demikian kita menghindari look-ahead bias, yaitu menggunakan informasi yang sebenarnya belum tersedia pada saat keputusan dibuat.
Menghitung Return Harian
Hitung perubahan harga harian.
df["Daily Return"] = df["Close"].pct_change()
Misalnya:
| Hari | Close | Daily Return |
|---|---|---|
| Senin | 9.000 | – |
| Selasa | 9.180 | 2.00% |
| Rabu | 9.090 | -0.98% |
Kolom ini akan menjadi dasar perhitungan keuntungan strategi.
Menghitung Return Strategi
Strategi hanya memperoleh keuntungan ketika sedang berada dalam posisi beli.
df["Strategy Return"] = (
df["Daily Return"]
*
df["Position"]
)
Artinya:
- Jika
Position = 1, strategi mengikuti perubahan harga. - Jika
Position = 0, strategi tidak memiliki posisi sehingga return dianggap nol.
Menghitung Cumulative Return
Selanjutnya kita hitung pertumbuhan modal.
df["Cumulative Strategy"] = (
1 + df["Strategy Return"]
).cumprod()
Sedangkan untuk Buy & Hold:
df["Buy Hold"] = (
1 + df["Daily Return"]
).cumprod()
Kedua kurva ini nantinya akan dibandingkan.
Visualisasi Performa
import matplotlib.pyplot as plt
plt.figure(figsize=(14,6))
plt.plot(
df["Cumulative Strategy"],
label="Moving Average Strategy"
)
plt.plot(
df["Buy Hold"],
label="Buy & Hold"
)
plt.legend()
plt.title("Perbandingan Strategi")
plt.show()
Interpretasi:
- Jika garis strategi berada di atas Buy & Hold, berarti strategi memberikan hasil lebih baik pada periode tersebut.
- Jika berada di bawah, strategi kurang efektif.
Menghitung Total Return
Nilai akhir strategi dapat dihitung dengan mudah.
strategy_return = (
df["Cumulative Strategy"].iloc[-1] - 1
) * 100
buyhold_return = (
df["Buy Hold"].iloc[-1] - 1
) * 100
print(f"Strategy : {strategy_return:.2f}%")
print(f"Buy Hold : {buyhold_return:.2f}%")
Contoh hasil:
Strategy : 31.45%
Buy Hold : 22.18%
Hasil ini menunjukkan bahwa pada periode pengujian, strategi Moving Average menghasilkan return yang lebih tinggi.
Menghitung Jumlah Transaksi
buy_signal = (
df["Position"]
.diff()
== 1
).sum()
sell_signal = (
df["Position"]
.diff()
== -1
).sum()
print("Buy :", buy_signal)
print("Sell:", sell_signal)
Jumlah transaksi penting karena strategi dengan transaksi terlalu banyak biasanya akan terkena biaya broker yang lebih besar.
Membuat Riwayat Transaksi
Kita dapat menyimpan setiap transaksi dalam sebuah daftar.
trades = []
buy_price = None
for index, row in df.iterrows():
if row["Position"] == 1 and buy_price is None:
buy_price = row["Close"]
buy_date = index
elif row["Position"] == 0 and buy_price is not None:
sell_price = row["Close"]
profit = (
sell_price - buy_price
) / buy_price * 100
trades.append({
"Buy Date": buy_date,
"Sell Date": index,
"Profit (%)": round(profit,2)
})
buy_price = None
Menampilkan Riwayat Trading
import pandas as pd
trade_df = pd.DataFrame(trades)
print(trade_df)
Contoh:
| Buy Date | Sell Date | Profit (%) |
|---|---|---|
| 2023-03-12 | 2023-05-08 | 8.23 |
| 2023-08-15 | 2023-10-20 | -3.12 |
| 2024-01-09 | 2024-04-18 | 11.45 |
Riwayat ini membantu mengevaluasi setiap transaksi secara individual.
Menghitung Win Rate
Win Rate menunjukkan persentase transaksi yang menghasilkan keuntungan.
win = (
trade_df["Profit (%)"] > 0
).sum()
total = len(trade_df)
win_rate = (
win / total
) * 100
print(
f"Win Rate : {win_rate:.2f}%"
)
Contoh:
Win Rate : 68.42%
Perlu diingat bahwa Win Rate tinggi tidak selalu berarti strategi lebih baik. Strategi dengan Win Rate rendah tetapi keuntungan rata-rata jauh lebih besar juga bisa menghasilkan performa yang sangat baik.
Menghitung Profit Rata-rata
average_profit = (
trade_df["Profit (%)"]
.mean()
)
print(
average_profit
)
Misalnya:
4.82%
Angka ini menunjukkan keuntungan rata-rata per transaksi.
Menghitung Transaksi Terbaik dan Terburuk
best_trade = (
trade_df["Profit (%)"]
.max()
)
worst_trade = (
trade_df["Profit (%)"]
.min()
)
print(best_trade)
print(worst_trade)
Informasi ini membantu memahami seberapa besar potensi keuntungan maupun risiko dari strategi yang digunakan.
Menghitung Maximum Drawdown
Maximum Drawdown (MDD) adalah salah satu metrik risiko yang paling penting dalam backtesting.
rolling_max = (
df["Cumulative Strategy"]
.cummax()
)
drawdown = (
df["Cumulative Strategy"]
-
rolling_max
) / rolling_max
max_drawdown = (
drawdown.min()
) * 100
print(
f"MDD : {max_drawdown:.2f}%"
)
Semakin kecil nilai drawdown, semakin stabil strategi tersebut.
Ringkasan Hasil Backtesting
Agar mudah dibaca, tampilkan hasil dalam bentuk tabel.
| Metrik | Nilai |
|---|---|
| Total Return | 31.45% |
| Buy & Hold | 22.18% |
| Jumlah Transaksi | 14 |
| Win Rate | 68.42% |
| Profit Rata-rata | 4.82% |
| Best Trade | 17.90% |
| Worst Trade | -6.75% |
| Maximum Drawdown | -8.42% |
Tabel seperti ini umum digunakan ketika membandingkan beberapa strategi.
Menampilkan Titik Buy dan Sell pada Grafik
Grafik akan lebih mudah dipahami jika dilengkapi dengan penanda kapan strategi melakukan pembelian dan penjualan.
buy = df[df["Position"].diff() == 1]
sell = df[df["Position"].diff() == -1]
plt.figure(figsize=(16,7))
plt.plot(df.index, df["Close"], label="Close Price")
plt.scatter(
buy.index,
buy["Close"],
marker="^",
s=120,
label="BUY"
)
plt.scatter(
sell.index,
sell["Close"],
marker="v",
s=120,
label="SELL"
)
plt.legend()
plt.show()
Grafik tersebut membantu memverifikasi apakah sinyal yang dihasilkan sudah sesuai dengan aturan strategi.
Simulasi Modal Awal
Agar hasil lebih realistis, tentukan modal awal.
initial_capital = 100_000_000
Misalnya strategi menghasilkan return 31%.
final_capital = initial_capital * (
1 + strategy_return / 100
)
print(final_capital)
Hasil:
| Modal Awal | Return | Modal Akhir |
|---|---|---|
| Rp100.000.000 | 31% | Rp131.000.000 |
Pendekatan ini memudahkan pembaca membayangkan dampak strategi terhadap nilai investasi.
Memasukkan Biaya Broker
Backtesting yang baik tidak boleh mengabaikan biaya transaksi.
Misalkan:
- Fee beli: 0,15%
- Fee jual: 0,25%
buy_fee = 0.0015
sell_fee = 0.0025
Saat menghitung keuntungan:
profit = (
sell_price * (1 - sell_fee)
-
buy_price * (1 + buy_fee)
)
Dengan memasukkan biaya broker, hasil backtesting menjadi lebih mendekati kondisi nyata.
Menghitung Compound Growth
Alih-alih menghitung keuntungan per transaksi secara terpisah, kita dapat melihat pertumbuhan modal dari waktu ke waktu.
capital = initial_capital
for trade in trades:
capital *= (
1 +
trade["Profit (%)"] / 100
)
print(capital)
Konsep ini menunjukkan efek compound return, yaitu keuntungan yang terus diinvestasikan kembali.
Menguji Berbagai Kombinasi Moving Average
Belum tentu kombinasi MA20 dan MA50 adalah yang terbaik.
Kita dapat mencoba beberapa kombinasi.
fast_ma = [10,20,30]
slow_ma = [50,100,200]
Loop sederhana:
results = []
for fast in fast_ma:
for slow in slow_ma:
print(
fast,
slow
)
Selanjutnya lakukan backtesting untuk setiap kombinasi dan simpan hasilnya.
Contoh hasil:
| MA Cepat | MA Lambat | Return |
|---|---|---|
| 10 | 50 | 24.5% |
| 20 | 50 | 31.4% |
| 20 | 100 | 28.2% |
| 30 | 100 | 19.8% |
Cara ini sering disebut parameter optimization.
Catatan: Saat melakukan optimasi, berhati-hatilah terhadap overfitting. Strategi yang sangat bagus pada data historis belum tentu memberikan hasil yang sama pada data di masa depan.
Menguji Banyak Saham
Daripada hanya BBCA, kita dapat menguji beberapa emiten sekaligus.
stocks = [
"BBCA.JK",
"BBRI.JK",
"BMRI.JK",
"BBNI.JK",
"TLKM.JK"
]
Loop:
summary = []
for ticker in stocks:
df = yf.download(
ticker,
period="5y",
progress=False
)
# Jalankan fungsi backtest
summary.append({
"Ticker": ticker,
"Return": strategy_return
})
Kemudian ubah menjadi DataFrame.
result = pd.DataFrame(summary)
print(result)
Hasil:
| Ticker | Return |
|---|---|
| BBCA | 31% |
| BBRI | 27% |
| BMRI | 34% |
| TLKM | 16% |
Dengan cara ini, kita dapat membandingkan performa strategi pada berbagai saham.
Menyimpan Laporan Backtesting
Simpan hasil ke CSV.
result.to_csv(
"backtest_result.csv",
index=False
)
Laporan tersebut dapat dianalisis lebih lanjut menggunakan Excel, LibreOffice Calc, atau Power BI.
Menampilkan Dashboard Menggunakan Streamlit
Jika Anda mengikuti artikel sebelumnya tentang Streamlit, hasil backtesting dapat ditampilkan dalam dashboard interaktif.
Contoh sederhana:
import streamlit as st
st.title(
"Backtesting Moving Average"
)
st.dataframe(result)
Tambahkan grafik:
st.line_chart(
df["Cumulative Strategy"]
)
Dashboard seperti ini memudahkan eksplorasi hasil tanpa perlu membaca output di terminal.
Ide Pengembangan Selanjutnya
Setelah sistem dasar selesai, Anda dapat menambahkan berbagai fitur lanjutan, seperti:
- Strategi berdasarkan RSI.
- Strategi MACD.
- Bollinger Bands.
- Ichimoku Cloud.
- Donchian Channel.
- ATR untuk Stop Loss.
- Trailing Stop.
- Position Sizing berdasarkan risiko.
- Multi-Timeframe Analysis.
- Portfolio Backtesting untuk beberapa saham sekaligus.
Semua fitur tersebut dapat dibangun di atas fondasi yang sudah kita buat.
Best Practice
Beberapa hal yang sebaiknya diperhatikan saat membuat sistem backtesting:
- Pisahkan logika pengambilan data, perhitungan indikator, dan simulasi transaksi ke dalam fungsi yang berbeda.
- Gunakan
auto_adjust=Truepadayfinanceagar harga sudah disesuaikan dengan aksi korporasi seperti stock split dan dividen. - Simpan hasil pengujian ke database jika melakukan banyak eksperimen.
- Gunakan virtual environment agar dependensi tetap konsisten.
- Dokumentasikan setiap perubahan parameter agar hasil dapat direproduksi.
Keterbatasan Backtesting
Walaupun sangat berguna, backtesting memiliki beberapa keterbatasan yang perlu dipahami:
- Performa masa lalu tidak menjamin hasil di masa depan.
- Data historis bisa mengandung kekurangan atau perubahan akibat aksi korporasi.
- Likuiditas saham dan slippage belum tentu tercermin dalam simulasi sederhana.
- Kondisi pasar dapat berubah sehingga strategi yang efektif pada satu periode belum tentu efektif pada periode lain.
Karena itu, hasil backtesting sebaiknya dijadikan salah satu bahan pertimbangan, bukan satu-satunya dasar pengambilan keputusan investasi.
Kesimpulan
Backtesting merupakan langkah penting sebelum menerapkan strategi investasi atau trading dengan uang sungguhan. Dengan Python, kita dapat menguji sebuah strategi secara objektif menggunakan data historis.
Dalam seri artikel ini kita telah mempelajari:
- Mengambil data historis saham menggunakan
yfinance. - Menghitung indikator Moving Average.
- Membuat sinyal Buy dan Sell.
- Melakukan simulasi transaksi.
- Menghitung return, Win Rate, dan Maximum Drawdown.
- Membandingkan strategi dengan Buy & Hold.
- Mengoptimalkan parameter strategi.
- Menguji beberapa saham sekaligus.
- Menyimpan hasil pengujian dan menampilkannya dalam dashboard.
Fondasi ini dapat dikembangkan menjadi sistem analisis kuantitatif yang lebih kompleks sesuai kebutuhan Anda.
FAQ
Apakah strategi Moving Average selalu menghasilkan keuntungan?
Tidak. Performa strategi bergantung pada karakteristik saham dan kondisi pasar. Oleh karena itu, backtesting diperlukan untuk mengevaluasi strategi pada berbagai periode dan emiten.
Mengapa hasil backtesting saya berbeda dengan aplikasi trading?
Perbedaan dapat disebabkan oleh data historis yang digunakan, biaya transaksi, penyesuaian dividen atau stock split, serta aturan eksekusi order yang berbeda.
Apakah saya bisa menggunakan data selain Yahoo Finance?
Bisa. Anda dapat menggunakan API lain seperti Alpha Vantage, Finnhub, Polygon.io, atau data dari penyedia resmi Bursa Efek Indonesia jika tersedia.
Berapa periode Moving Average yang paling baik?
Tidak ada jawaban yang berlaku untuk semua kondisi. Kombinasi seperti MA20–MA50, MA20–MA100, atau MA50–MA200 sering digunakan, tetapi sebaiknya diuji terlebih dahulu melalui backtesting
