library(quantmod)
library(ggplot2)
library(dplyr)
# Daftar 50 saham Indonesia kapitalisasi terbesar (suffix .JK untuk Yahoo Finance)
tickers <- c(
"BBCA.JK", "BBRI.JK", "BMRI.JK", "BYAN.JK", "TLKM.JK",
"ASII.JK", "BBNI.JK", "GOTO.JK", "UNVR.JK", "ICBP.JK",
"KLBF.JK", "HMSP.JK", "EXCL.JK", "PGAS.JK", "SMGR.JK",
"INDF.JK", "GGRM.JK", "AALI.JK", "BSDE.JK", "LPKR.JK",
"PTBA.JK", "ADRO.JK", "INCO.JK", "ANTM.JK", "MNCN.JK",
"ITMG.JK", "BUMI.JK", "HRUM.JK", "MEDC.JK", "BRPT.JK",
"INKP.JK", "TPIA.JK", "AKRA.JK", "JSMR.JK", "BBTN.JK",
"SCMA.JK", "TBIG.JK", "MAPI.JK", "ERAA.JK", "SIDO.JK",
"DSNG.JK", "PALM.JK", "LPPF.JK", "UNTR.JK", "MDKA.JK",
"ESSA.JK", "SRTG.JK", "DNET.JK", "BRIS.JK", "ACES.JK"
)
# Ambil data dari Yahoo Finance (Jan 2024 - Mar 2026)
start_date <- "2024-01-01"
end_date <- "2026-03-31"
# Simpan semua data ke dalam satu dataframe
all_stocks <- data.frame()
for (ticker in tickers) {
tryCatch({
data <- getSymbols(ticker, src = "yahoo",
from = start_date, to = end_date,
auto.assign = FALSE)
df <- data.frame(
Date = index(data),
Ticker = gsub(".JK", "", ticker),
Price = as.numeric(Ad(data)) # Adjusted Close Price
)
all_stocks <- rbind(all_stocks, df)
}, error = function(e) {
cat("Gagal ambil data:", ticker, "\n")
})
}
# Cek hasil
cat("Jumlah baris data:", nrow(all_stocks), "\n")
## Jumlah baris data: 26350
cat("Jumlah saham:", length(unique(all_stocks$Ticker)), "\n")
## Jumlah saham: 50
cat("Rentang tanggal:", as.character(min(all_stocks$Date)),
"sampai", as.character(max(all_stocks$Date)), "\n")
## Rentang tanggal: 2024-01-02 sampai 2026-03-30
summary(all_stocks)
## Date Ticker Price
## Min. :2024-01-02 Length:26350 Min. : 50.0
## 1st Qu.:2024-07-30 Class :character 1st Qu.: 932.5
## Median :2025-02-12 Mode :character Median : 1936.7
## Mean :2025-02-14 Mean : 4057.3
## 3rd Qu.:2025-09-10 3rd Qu.: 4760.6
## Max. :2026-03-30 Max. :31975.0
# Jumlah missing value per kolom
cat("Missing value per kolom:\n")
## Missing value per kolom:
colSums(is.na(all_stocks))
## Date Ticker Price
## 0 0 0
cat("\nTotal missing value:", sum(is.na(all_stocks)), "\n")
##
## Total missing value: 0
# Hapus baris yang mengandung missing value
stocks_clean <- all_stocks[complete.cases(all_stocks), ]
cat("Jumlah baris sebelum:", nrow(all_stocks), "\n")
## Jumlah baris sebelum: 26350
cat("Jumlah baris sesudah:", nrow(stocks_clean), "\n")
## Jumlah baris sesudah: 26350
cat("Missing value setelah dibersihkan:", sum(is.na(stocks_clean)), "\n")
## Missing value setelah dibersihkan: 0
summary(stocks_clean)
## Date Ticker Price
## Min. :2024-01-02 Length:26350 Min. : 50.0
## 1st Qu.:2024-07-30 Class :character 1st Qu.: 932.5
## Median :2025-02-12 Mode :character Median : 1936.7
## Mean :2025-02-14 Mean : 4057.3
## 3rd Qu.:2025-09-10 3rd Qu.: 4760.6
## Max. :2026-03-30 Max. :31975.0
# Ambil daftar ticker
ticker_list <- unique(stocks_clean$Ticker)
# Plot per batch 10 saham
for (i in seq(1, length(ticker_list), by = 10)) {
batch <- ticker_list[i:min(i+9, length(ticker_list))]
data_batch <- stocks_clean[stocks_clean$Ticker %in% batch, ]
p <- ggplot(data_batch, aes(x = Date, y = Price, color = Ticker)) +
geom_line() +
facet_wrap(~ Ticker, scales = "free_y") +
labs(
title = "Harga Saham Individual",
x = "Tanggal",
y = "Harga (IDR)"
) +
theme_minimal() +
theme(legend.position = "none",
axis.text.x = element_text(angle = 45, hjust = 1))
print(p)
}
Saham dikelompokkan berdasarkan accumulative return dan volatilitas menggunakan K-Means clustering.
# Hitung return harian
stocks_clean <- stocks_clean %>%
arrange(Ticker, Date) %>%
group_by(Ticker) %>%
mutate(Daily_Return = (Price / lag(Price)) - 1) %>%
ungroup()
stocks_clean <- stocks_clean[!is.na(stocks_clean$Daily_Return), ]
# Buat ringkasan per saham
summary_saham <- stocks_clean %>%
group_by(Ticker) %>%
summarise(
Accum_Return = prod(1 + Daily_Return) - 1,
Volatility = sd(Daily_Return)
)
# Normalisasi data untuk clustering
data_cluster <- scale(summary_saham[, c("Accum_Return", "Volatility")])
# K-Means dengan 4 kelompok
set.seed(123)
hasil_cluster <- kmeans(data_cluster, centers = 4, nstart = 20)
summary_saham$Cluster <- as.factor(hasil_cluster$cluster)
# Tampilkan hasil
print(summary_saham)
## # A tibble: 50 × 4
## Ticker Accum_Return Volatility Cluster
## <chr> <dbl> <dbl> <fct>
## 1 AALI 0.158 0.0158 2
## 2 ACES -0.414 0.0243 3
## 3 ADRO 1.21 0.0288 4
## 4 AKRA 0.0952 0.0256 3
## 5 ANTM 1.30 0.0308 4
## 6 ASII 0.345 0.0204 2
## 7 BBCA -0.238 0.0164 3
## 8 BBNI -0.112 0.0209 3
## 9 BBRI -0.303 0.0204 3
## 10 BBTN 0.103 0.0254 3
## # ℹ 40 more rows
# Visualisasi cluster
ggplot(summary_saham, aes(x = Volatility, y = Accum_Return,
color = Cluster, label = Ticker)) +
geom_point(size = 3) +
geom_text(vjust = -0.8, size = 3) +
labs(
title = "Pengelompokan Saham Berdasarkan Return dan Volatilitas",
x = "Volatilitas (Std Dev Return Harian)",
y = "Accumulative Return",
color = "Kelompok"
) +
theme_minimal()
# Urutkan dari return tertinggi
top2 <- summary_saham %>%
arrange(desc(Accum_Return)) %>%
slice(1:2)
cat("2 saham dengan accumulative return tertinggi:\n")
## 2 saham dengan accumulative return tertinggi:
print(top2)
## # A tibble: 2 × 4
## Ticker Accum_Return Volatility Cluster
## <chr> <dbl> <dbl> <fct>
## 1 DSNG 2.38 0.0339 4
## 2 BUMI 1.38 0.0463 4
top2_tickers <- top2$Ticker
# Filter data hanya untuk 2 saham terpilih
data_top2 <- stocks_clean %>%
filter(Ticker %in% top2_tickers) %>%
select(Date, Ticker, Daily_Return)
# Pivot ke wide format
data_wide <- data_top2 %>%
tidyr::pivot_wider(names_from = Ticker, values_from = Daily_Return) %>%
tidyr::drop_na()
# Hitung return portofolio (rata-rata dari 2 saham = equal weight)
data_wide$Portfolio_Return <- rowMeans(data_wide[, top2_tickers])
# Hitung cumulative return portofolio
data_wide$Portfolio_Cumulative <- cumprod(1 + data_wide$Portfolio_Return) - 1
head(data_wide)
## # A tibble: 6 × 5
## Date BUMI DSNG Portfolio_Return Portfolio_Cumulative
## <date> <dbl> <dbl> <dbl> <dbl>
## 1 2024-01-03 0.0316 -0.0275 0.00203 0.00203
## 2 2024-01-04 0.0102 0.0283 0.0193 0.0213
## 3 2024-01-05 0 -0.00917 -0.00459 0.0166
## 4 2024-01-08 -0.0202 0 -0.0101 0.00637
## 5 2024-01-09 -0.0103 -0.0185 -0.0144 -0.00814
## 6 2024-01-10 0 0.00943 0.00472 -0.00346
accum_return_portf <- tail(data_wide$Portfolio_Cumulative, 1)
mean_return_portf <- mean(data_wide$Portfolio_Return)
sd_return_portf <- sd(data_wide$Portfolio_Return)
cat("=== Statistik Portofolio ===\n")
## === Statistik Portofolio ===
cat("Accumulative Return :", round(accum_return_portf * 100, 2), "%\n")
## Accumulative Return : 240.81 %
cat("Mean Return Harian :", round(mean_return_portf * 100, 4), "%\n")
## Mean Return Harian : 0.2783 %
cat("Standard Deviation :", round(sd_return_portf * 100, 4), "%\n")
## Standard Deviation : 3.0186 %
ihsg_data <- getSymbols("^JKSE", src = "yahoo",
from = start_date, to = end_date,
auto.assign = FALSE)
ihsg_df <- data.frame(
Date = index(ihsg_data),
IHSG = as.numeric(Ad(ihsg_data))
)
ihsg_df <- ihsg_df[!is.na(ihsg_df$IHSG), ]
# Hitung return harian IHSG
ihsg_df$IHSG_Return <- c(NA, diff(ihsg_df$IHSG) / head(ihsg_df$IHSG, -1))
ihsg_df <- ihsg_df[!is.na(ihsg_df$IHSG_Return), ]
ihsg_df$IHSG_Cumulative <- cumprod(1 + ihsg_df$IHSG_Return) - 1
accum_return_ihsg <- tail(ihsg_df$IHSG_Cumulative, 1)
mean_return_ihsg <- mean(ihsg_df$IHSG_Return)
sd_return_ihsg <- sd(ihsg_df$IHSG_Return)
cat("=== Statistik IHSG ===\n")
## === Statistik IHSG ===
cat("Accumulative Return :", round(accum_return_ihsg * 100, 2), "%\n")
## Accumulative Return : -3.17 %
cat("Mean Return Harian :", round(mean_return_ihsg * 100, 4), "%\n")
## Mean Return Harian : 7e-04 %
cat("Standard Deviation :", round(sd_return_ihsg * 100, 4), "%\n")
## Standard Deviation : 1.1589 %
tabel_perbandingan <- data.frame(
Metrik = c("Accumulative Return", "Mean Return Harian", "Standard Deviation"),
Portofolio = c(
paste0(round(accum_return_portf * 100, 2), "%"),
paste0(round(mean_return_portf * 100, 4), "%"),
paste0(round(sd_return_portf * 100, 4), "%")
),
IHSG = c(
paste0(round(accum_return_ihsg * 100, 2), "%"),
paste0(round(mean_return_ihsg * 100, 4), "%"),
paste0(round(sd_return_ihsg * 100, 4), "%")
)
)
print(tabel_perbandingan)
## Metrik Portofolio IHSG
## 1 Accumulative Return 240.81% -3.17%
## 2 Mean Return Harian 0.2783% 7e-04%
## 3 Standard Deviation 3.0186% 1.1589%
# Gabungkan data portofolio dan IHSG berdasarkan tanggal
data_portf_plot <- data_wide[, c("Date", "Portfolio_Cumulative")]
data_ihsg_plot <- ihsg_df[, c("Date", "IHSG_Cumulative")]
combined <- merge(data_portf_plot, data_ihsg_plot, by = "Date")
# Faktor skala untuk double axis
scale_factor <- max(combined$Portfolio_Cumulative) / max(combined$IHSG_Cumulative)
ggplot(combined, aes(x = Date)) +
geom_line(aes(y = Portfolio_Cumulative, color = "Portofolio"), linewidth = 1) +
geom_line(aes(y = IHSG_Cumulative * scale_factor, color = "IHSG"),
linewidth = 1, linetype = "dashed") +
scale_y_continuous(
name = "Cumulative Return - Portofolio",
labels = scales::percent,
sec.axis = sec_axis(
transform = ~ . / scale_factor,
name = "Cumulative Return - IHSG",
labels = scales::percent
)
) +
scale_color_manual(values = c("Portofolio" = "steelblue", "IHSG" = "tomato")) +
labs(
title = "Pergerakan Portofolio vs IHSG",
x = "Tanggal",
color = "Keterangan"
) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
Return tinggi secara historis. Portofolio dipilih berdasarkan 2 saham dengan return tertinggi, sehingga secara historis menunjukkan performa yang baik dibandingkan IHSG.
Mudah dikelola. Hanya 2 saham dengan bobot sama (50:50), sehingga sangat sederhana untuk dipantau dan di-rebalancing.
Saham likuid. Kedua saham berasal dari daftar 50 saham terbesar di IDX, artinya mudah dibeli dan dijual di pasar.
Risiko konsentrasi tinggi. Hanya 2 saham berarti jika salah satu saham turun drastis, portofolio langsung terdampak besar. Tidak ada diversifikasi yang memadai.
Bias historis. Saham dipilih karena return masa lalu tertinggi. Return masa lalu tidak menjamin return masa depan yang sama.
Volatilitas lebih tinggi. Portofolio dengan sedikit saham cenderung lebih fluktuatif dibandingkan indeks seperti IHSG yang terdiri dari ratusan saham.
Tidak mempertimbangkan korelasi antar saham. Portofolio yang baik seharusnya memilih saham yang tidak bergerak bersamaan (korelasi rendah) agar risiko lebih tersebar.
Portofolio yang dibangun menunjukkan performa yang baik secara historis, namun memiliki risiko yang lebih tinggi karena hanya terdiri dari 2 saham. Jika dibandingkan dengan IHSG, portofolio ini bisa menghasilkan return lebih tinggi, tetapi juga lebih berisiko. Untuk investasi jangka panjang yang lebih aman, disarankan untuk menambah jumlah saham dan mempertimbangkan korelasi antar saham.
Data bersumber dari Yahoo Finance menggunakan package
quantmod di R.