Thursday, July 16, 2026
Python Saham

Belajar Backtesting Strategi Moving Average Menggunakan Python

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:

PositionArti
1Buy Signal
-1Sell Signal
0Tidak 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:

HariCloseDaily Return
Senin9.000
Selasa9.1802.00%
Rabu9.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 DateSell DateProfit (%)
2023-03-122023-05-088.23
2023-08-152023-10-20-3.12
2024-01-092024-04-1811.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.

MetrikNilai
Total Return31.45%
Buy & Hold22.18%
Jumlah Transaksi14
Win Rate68.42%
Profit Rata-rata4.82%
Best Trade17.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 AwalReturnModal Akhir
Rp100.000.00031%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 CepatMA LambatReturn
105024.5%
205031.4%
2010028.2%
3010019.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:

TickerReturn
BBCA31%
BBRI27%
BMRI34%
TLKM16%

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=True pada yfinance agar 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

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *