## tibble [5,307 × 10] (S3: tbl_df/tbl/data.frame)
## $ Index Code : chr [1:5307] "J113" "J113" "J113" "J113" ...
## $ Statistic Date : POSIXct[1:5307], format: "2015-10-05" "2015-10-06" ...
## $ Constituents : num [1:5307] 63 63 63 63 63 63 63 63 63 63 ...
## $ Capital Index : num [1:5307] 10076 10064 10168 10192 10302 ...
## $ Total Return Index: num [1:5307] 10526 10512 10621 10646 10761 ...
## $ XD Adjustment : num [1:5307] 25.7 0 0 0 0 ...
## $ Dividend Yield : num [1:5307] 0.0307 0.0307 0.0304 0.0303 0.03 0.0303 0.0303 0.0303 0.0303 0.0302 ...
## $ Earnings Yield : num [1:5307] 5.07 5.08 5.03 5.01 4.96 5.01 5 5.01 5 5 ...
## $ Closing MCAP : num [1:5307] 4.00e+12 4.00e+12 4.04e+12 4.05e+12 4.09e+12 ...
## $ Divisor : num [1:5307] 0.00 3.97e+08 3.97e+08 3.97e+08 3.97e+08 ...
This step prepares that raw data set for time series analysis by fixing the date format and ensuring only valid return data remains.
This creates two separate data frames, each containing data for each specific index.
This is the core step for financial time series analysis. Log returns are in volatility modelling because they are more normally distributed than simple returns and they also approximate percentage changes for small moves.
calculated_returns <- function(df, name) {
df %>%
mutate(Return = log(`Total Return Index`/lag(`Total Return Index`))) %>%
na.omit() %>%
select(Date, Return) %>%
rename(!!name := Return)
}
returns_j113 <- calculated_returns(j113, "J113")
returns_j203 <- calculated_returns(j203, "J203")This creates a clean, aligned wide format data set containing daily log returns for both J113 and J203 indices
library(tseries)
library(moments)
results <- data.frame(
Series = character(),
skewness = numeric(),
kurtosis = numeric(),
JB_Statistic = numeric(),
JB_pvalue = numeric(),
Interpretation = character(),
stringsAsFactors = FALSE
)
for (col in c("J113", "J203")){
returns <- na.omit(returns_wide[[col]])
skewness_value <- skewness(returns)
kurtosis_value <- kurtosis(returns)
Jarque_Bera_test <- jarque.bera.test(returns)
results <- rbind(results, data.frame(
Series = col,
skewness = round(skewness_value, 6),
Kurtosis = round(kurtosis_value, 6),
JB_Statistic = round(Jarque_Bera_test$statistic, 4),
JB_pvalue = ifelse(Jarque_Bera_test$p.value < 0.05,
"Reject Normality (Good for GARCH)",
"Fail to Reject Normality")
))
}
print("===Skewness, Kurtosis & Jarque-Bera Test Results===")## [1] "===Skewness, Kurtosis & Jarque-Bera Test Results==="
## Series skewness Kurtosis JB_Statistic
## X-squared J113 -0.366364 287.59578 8632748.32
## X-squared1 J203 -0.597621 10.76261 6574.77
## JB_pvalue
## X-squared Reject Normality (Good for GARCH)
## X-squared1 Reject Normality (Good for GARCH)
Both series(J113 and J203) show negative skewness. This actually means the return distribution has longer left tails. There is higher probabilities of negative returns than positive returns. J203 has a stronger negative skew (-0.60) compared to J113(-0.37). J113 has Kurtosis = 287.6 which extremely leptokurtic (extremely fat tailed). This is extremely high and suggest many extreme outliers or very heavy tails. J203 has 10.76 Kurtosis which is also significantly leptokurtic though much less extreme than J113. Both p_values for the Jarque-Bera test are zero and for that we strongly reject the hypothesis that the returns are normally distributed. Financial returns almost never follow a normal distribution. These results show the classic characteristics that makes GARCH models appropriate.
## === Augumented Dickey-Fuller Test Results ===
for (col in c("J113", "J203")){
series <- na.omit(returns_wide[[col]])
adf_result <- adf.test(series, alternative = "stationary")
cat("series:", col, "\n")
print(adf_result)
cat("\n")
}## series: J113
##
## Augmented Dickey-Fuller Test
##
## data: series
## Dickey-Fuller = -15.156, Lag order = 13, p-value = 0.01
## alternative hypothesis: stationary
## series: J203
##
## Augmented Dickey-Fuller Test
##
## data: series
## Dickey-Fuller = -14.511, Lag order = 13, p-value = 0.01
## alternative hypothesis: stationary
Both series have a very large negative Dickey-Fuller statistics(-15.156 and -14.511). The p-values for both are 0.01 which is less than 0.05 and therefore we reject the null hypothesis at the 1% significant level. Both J113 and J203 return series are stationary.
This separate the data from a wide format to a long format. It separate columns for J113 and J203
library(tidyr)
returns_long <- returns_wide %>%
pivot_longer(cols = c(J113, J203), names_to = "Index",
values_to = "Return")
print(summary(returns_wide))## Date J113 J203
## Min. :2015-10-05 Min. :-0.4002265 Min. :-0.1022681
## 1st Qu.:2018-04-29 1st Qu.:-0.0056744 1st Qu.:-0.0051358
## Median :2020-11-15 Median : 0.0005834 Median : 0.0007187
## Mean :2020-11-15 Mean : 0.0004128 Mean : 0.0004473
## 3rd Qu.:2023-06-07 3rd Qu.: 0.0069964 3rd Qu.: 0.0064667
## Max. :2025-12-30 Max. : 0.3974412 Max. : 0.0726150
Both Indices have positive average daily returns (~0.04% - 0.044% per day). J311 is more volatile than J203 (much larger daily gain, +39.0% vs +7.3% and much larger daily loss of -40.0% vs -10.2%. This suggest that J113 is a higher risk index than J203.
This is essential to visually inspect volatility patterns and to see the differences in behavior between the two indices.
ggplot(returns_long, aes(x = Date, y = Return, color = Index))+
geom_line()+theme_minimal()+
labs(title = "Daily Log Returns: J113 vs J203")From approximately mid-2015 to the end of 2025, both return series fluctuates around zero, which is normal for daily log returns. There is clear evidence of volatility clustering (Periods of calm markets are followed by periods of high turbulence especially in 2020). J113 shows much larger swings while J203 is smoother and more stable. This confirms that J113 is slightly more volatile that J203.
This step annualized the the daily statistics and computes the Sharpe Ratio for easy comparison between the two indices.
annual_stats <- function(returns){
data.frame(
mean = mean(returns)*252,
Sd = sd(returns)*sqrt(252),
Sharpe = (mean(returns)*252)/(sd(returns)*sqrt(252))
)
}
stats_j113 <- annual_stats(returns_wide$J113)
stats_j203 <- annual_stats(returns_wide$J203)
print(rbind(J113 = stats_j113, J203 = stats_j203))## mean Sd Sharpe
## J113 0.1040284 0.2577083 0.4036671
## J203 0.1127249 0.1751163 0.6437145
J203 shows slightly higher annualized returns(11.27% vs 10.40%). J113 is much riskier(annualized volatility is 25.77%, significantly higher than J203’s 17.51%). J203 performs better on a risk-adjusted bases. This means J203 gives more return per unit of risk taken. Overall, the results suggest that J203 would generally be preferred by most investors over J113, assuming similar correlation with their portfolio.
fit_garch <- function(returns_vec, index_name)
{
returns_vec <- na.omit(returns_vec)
spec <- ugarchspec(
variance.model = list(model = "sGARCH", garchOrder = c(1,1)),
mean.model = list(armaOrder = c(0,0), include.mean = TRUE),
distribution.model = "std"
)
fit <- ugarchfit(spec = spec, data = returns_vec, solver = "hybrid")
cat("\n=== GARCH(1,1) for", index_name, "===\n")
print(coef(fit))
print(persistence(fit))
return(fit)
}
returns_wide <- xts(
returns_wide[, c("J113", "J203")],
order.by = as.Date(returns_wide$Date)
)
fit_j113_garch <- fit_garch(returns_wide$J113, "J113")##
## === GARCH(1,1) for J113 ===
## mu omega alpha1 beta1 shape
## 7.333934e-04 7.728034e-06 1.139509e-01 8.360086e-01 5.820769e+00
## [1] 0.9499595
##
## === GARCH(1,1) for J203 ===
## mu omega alpha1 beta1 shape
## 7.220343e-04 5.412017e-06 1.042941e-01 8.479138e-01 7.456516e+00
## [1] 0.9522079
J113 has a very high persistence of 0.94996. This means that shocks to volatility die slowly. a value close to 1 (but < 1) is typical for financial modelling. J203 has a persistence value of 0.95221 which is also very high. This result show that Volatility clustering is strong since both models have alpha + beta around 0.95. High Volatility periods are also followed by more high volatility. Since alpha + beta < 1 in both J113 and J203, the models are good.
library(rugarch)
y1 <- returns_wide$J113
y2 <- returns_wide$J203
y_stack <- c(y1, y2)
dummy <- c(rep(0, length(y1)), rep(1, length(y2)))
spec_unrest <- ugarchspec(
variance.model = list(
model = "sGARCH",
garchOrder = c(1,1),
external.regressors = matrix(dummy, ncol = 1)
),
mean.model = list(armaOrder = c(0,0), include.mean = T),
distribution.model = "std"
)
fit_unrest <- ugarchfit(spec_unrest, data = y_stack)
spec_rest <- ugarchspec(
variance.model = list(
model = "sGARCH",
garchOrder = c(1,1),
external.regressors = matrix(dummy, ncol = 1)
),
mean.model = list(armaOrder = c(0,0), include.mean = T),
distribution.model = "std"
)
fit_rest <- ugarchfit(spec_rest, data = y_stack)
LR_stat <- 2*(likelihood(fit_unrest)-likelihood(fit_rest))
p_val <- pchisq(LR_stat, df=1, lower.tail = FALSE)
cat("LR statistic:", round(LR_stat, 4), "\n") ## LR statistic: 0
## p-value: 1
if(p_val < 0.05){
cat("Result: Persistence differs significantly between J113 and J203\n")
}else{
cat("Result: No Significant difference in persistence\n")
}## Result: No Significant difference in persistence
sigma_j113 <- sigma(fit_j113_garch)
sigma_j203 <- sigma(fit_j203_garch)
dates <- index(sigma_j113)
Cond_var <- data.frame(
Date = dates,
J113_Cond_Vol = as.numeric(sigma_j113),
J203_Cond_Vol = as.numeric(sigma_j203)
)
var_j113 <- sigma_j113^2
var_j203 <- sigma_j203^2
omega_j113 <- coef(fit_j113_garch)["omega"]
omega_j203 <- coef(fit_j203_garch)["omega"]
pers_j113 <- persistence(fit_j113_garch)
pers_j203 <- persistence(fit_j203_garch)
longrun_j113 <- omega_j113 / (1 - pers_j113)
longrun_j203 <- omega_j203 / (1 - pers_j203)
summary_stats <- data.frame(
metric = c("Mean Cond. Volatility",
"Median Cond. Volatility",
"Max Cond. Volatility",
"Long_run variance"),
J113 = c(mean(sigma_j113),
median(sigma_j113),
max(sigma_j113),
longrun_j113),
J203 = c(mean(sigma_j203),
median(sigma_j203),
max(sigma_j203),
longrun_j203)
)
print("=== Conditional Volatility and Variance summary ===")## [1] "=== Conditional Volatility and Variance summary ==="
## metric J113 J203
## 1 Mean Cond. Volatility 0.0114865693 0.0103044921
## 2 Median Cond. Volatility 0.0105621810 0.0094944448
## 3 Max Cond. Volatility 0.1356862711 0.0460379407
## 4 Long_run variance 0.0001544357 0.0001132409
plot(Cond_var$Date, Cond_var$J113_Cond_Vol, type ="l", col ="blue", lwd=2,
ylab = "Conditional Volatility",
xlab = "Date",
main = "Conditional Volatility from GARCH(1,1)")
lines(Cond_var$Date, Cond_var$J203_Cond_Vol, col = "red")
legend("topright", legend = c("J113", "J203"), col = c("blue", "red"))t_test_var <- t.test(sigma_j113, sigma_j203, alternative = "two.sided",
var.equal = F)
print(t_test_var)##
## Welch Two Sample t-test
##
## data: sigma_j113 and sigma_j203
## t = 10.487, df = 4918.7, p-value < 2.2e-16
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
## 0.0009611075 0.0014030469
## sample estimates:
## mean of x mean of y
## 0.01148657 0.01030449
This code models how positive and negative shocks differently affect future volatility
fit_egarch <- function(returns_vec, index_name)
{
returns_vec <- na.omit(returns_vec)
spec <- ugarchspec(
variance.model = list(model = "eGARCH", garchOrder = c(1,1)),
mean.model = list(armaOrder = c(0,0), include.mean = TRUE),
distribution.model = "std"
)
fit <- ugarchfit(
spec = spec,
data = returns_vec,
solver = "hybrid")
mat <- fit@fit$matcoef
cat("\n=== EGARCH(1,1) for", index_name, "===\n")
print(mat)
if("gamma" %in% rownames(mat)){
gamma1 <- mat["gamma", "Estimate"]
se_gamma1 <- mat["gamma1", "Std. Error"]
t_stat <- mat["gamma1", "t value"]
p_val <- mat["gamma1", "Pr(>|t|)"]
cat("\nLeverage Effect Test for", index_name, ":\n")
cat("gamma1=", round(gamma1, 4), "\n")
cat("Std. Error=", round(se_gamma1, 4), "\n")
cat("t_stat=", round(t_stat, 4), "\n")
cat("p-value=", round(p_val, 6), "\n")
if(p_val < 0.05){
if(gamma1 < 0){
cat("Result: Significant Negative leverage Effect. Negative shocks increses volatility more than positive shocks. \n")
} else{
cat("Result: Significant Positive leverage Effect. Positive shocks increses volatility more. \n")
}
} else{
cat("Result: No Significant leverage effect. Symmeytric volatility response. \n")
}
}else{
cat("\ngamma1 not found. Check if model converge.\n")
}
return(fit)
}##
## === EGARCH(1,1) for J113 ===
## Estimate Std. Error t value Pr(>|t|)
## mu 0.0004669715 0.0001772207 2.634972 8.414428e-03
## omega -0.3341029844 0.0111018238 -30.094423 0.000000e+00
## alpha1 -0.1095032063 0.0135718690 -8.068395 6.661338e-16
## beta1 0.9632126465 0.0013103036 735.106437 0.000000e+00
## gamma1 0.1270433228 0.0205199004 6.191225 5.969838e-10
## shape 6.3562389153 0.6829343448 9.307247 0.000000e+00
##
## gamma1 not found. Check if model converge.
The positive gamma of 0.127 and the p_value of 5.97e-10 (which is far below 0.05) suggest that leverage effect parameter is highly statistically significant. It actually means that positive shocks increase log volatility more that negative shocks at the same magnitude. Volatility in this index responds asymmetrically , but in the opposite direction to typical equity markets.
##
## === EGARCH(1,1) for J203 ===
## Estimate Std. Error t value Pr(>|t|)
## mu 0.0004389154 0.0001600553 2.742274 6.101543e-03
## omega -0.3335575033 0.0106220755 -31.402291 0.000000e+00
## alpha1 -0.1207575117 0.0139020073 -8.686336 0.000000e+00
## beta1 0.9641176524 0.0012690756 759.700751 0.000000e+00
## gamma1 0.1346232829 0.0180094949 7.475128 7.704948e-14
## shape 8.5453015734 1.2737444082 6.708804 1.962253e-11
##
## gamma1 not found. Check if model converge.
The positive gamma of 0.135 and the p_value of 7.7004948e-14 (which is far below 0.05) also suggest that leverage effect parameter is highly statistically significant for this index. It actually means that positive shocks increase log volatility more that negative shocks at the same magnitude. Volatility in this index also responds asymmetrically , but in the opposite direction to typical equity markets.
Both J113 and J203 have positive gamma(0.127 and 0.135) which means that increase future volatility more that negative shocks of the same size.
Risk-adjusted performance of J113 and J203 using proper financial metrics. First ensuring clean, aligned dates and removing missing values.
returns_wide <- data.frame(Date = index(returns_wide),
coredata(returns_wide))
returns_wide <- returns_wide %>%
mutate(Date = ymd(Date)) %>%
filter(!is.na(J113) & !is.na(J203)) %>%
arrange(Date)
returns_wide <- na.omit(returns_wide)return_xts <- xts(returns_wide[, c("J113", "J203")],
order.by = returns_wide$Date)
rf_daily <- 0.07 / 252
risk_metrics <- table.AnnualizedReturns(return_xts,
Rf = rf_daily)
print("=== Annualized Perfomance Metrics ===")## [1] "=== Annualized Perfomance Metrics ==="
## J113 J203
## Annualized Return 0.0717 0.1022
## Annualized Std Dev 0.2577 0.1751
## Annualized Sharpe (Rf=7%) -0.0027 0.1582
## J113 J203
## Sortino Ratio (MAR = 0.028%) 0.01156574 0.02118394
J203 outperformed J113 with 10.22% annualized return vs 7.17%. J113 is significantly more volatile (25.77% vs 17.51%).J203 has a positive Sharpe Ratio of 0.158 while J113 has a negative Sharpe Ratio of -0.0027(and the 7% risk-free rate). J113 has a Sortino ratio(Non parameter test for downside Risk) of 0.0116. This means that after adjusting for downside risk, the excess return is minimal.J203 has 0.0212 which almost twice as J113. This indicates better risk-adjusted performance down significantly. The fact that Sortino ratios are positive while Sharpe for J113 is negative suggests that a large portion of the volatility in these series comes from positive returns.
excess_j113 <- returns_wide$J113 - rf_daily
excess_j203 <- returns_wide$J203 - rf_daily
t_test_result <- t.test(excess_j113, excess_j203, paired = TRUE, alternative = "two.sided")
cat("\n=== Paired t-test for Difference in Excess Returns ===\n")##
## === Paired t-test for Difference in Excess Returns ===
## Mean excess return J113: 0.000135
## Mean excess return J203: 0.00017
## Mean difference (J113 - J203): -3.5e-05
## t-statistic: -0.1503
## p-value: 0.880563
if(t_test_result$p.value < 0.05){
cat("Result: Significant difference in risk-adjusted performance at 5% level\n")
} else {
cat("Result: No significant difference in risk-adjusted performance\n")
}## Result: No significant difference in risk-adjusted performance
The results show a paired t-test comparing the daily excess returns of J113 and J203. There is the mean difference of -0.000035(0.000135 - 0.00017) per day. J203 looks better on Sharpe ratio but you can not say it outperforms J113 in a statistically reliable way. The pared t-test finds no significant difference in risk-adjusted performance over this sample period.
This visualize how performance and risk-adjusted returns change over time, helping identify regime shifts.
charts.RollingPerformance(return_xts,
width = 252,
Rf = rf_daily,
main = "Rolling 1-year Sharpe Ratio")The top plot(Rolling Annualized Return) shows how the 1-year return has evolved. It shows that both indices performed poorly around 2020 (sharp drop into negative territory). It shows a strong recovery after 2020. J203 generally shows higher returns than J113 in most periods, especially after 2021. Recent period(2024-2025) shows strong positive returns for both(~0.3-0.45 annualized)
The middle plot(Rolling Annualized Standard Deviation) shows volatility over rolling 1-year windows. J113 (top line) is consistently more volatile than J203. Volatility spiked more noticeably around 2020 for both and more dramatically for J113. Volatility has been relatively stable since 2021.
Bottom plot(Rolling 1-year Sharpe Ratio) is the key plot for regime changes. The Sharpe ratio fluctuates significantly over time which is evidence of different market regimes. It shows a very poor performance in 2020. Strong improvement post-2020, especially in 2021. J203 has higher Sharpe Ratio than J113 in most periods. Recent years(2024-2025) show improving Sharpe Ratios for both, reaching around 0.8-1.0 in the lates window.
This provides a comprehensive visual summary of the performance of both indices in a sing chart with three panels: Cumulative Return (growth of R1 over time), Daily return(Return series), Drawdown(peak-to-trough losses). It also adjust for the risk-free rate (7% annual) where relevant.
charts.PerformanceSummary(return_xts,
Rf = rf_daily,
main = "J113 vs J203 Risk Adjusted Perfomance")The Cumulative return show that both indices delivers a strong long-term growth(around 1.3x 1.6x). J203(pink/red line) clearly outperformed J113(black line) over the full period. J203 pulled ahead especially after 2020 recovery and maintained the lead. major dip in early 2020 is visible for both.
The Daily return shows mostly small fluctuations around zero. a few large spikes are visible(especially the extreme negative return in 2020 for J113). This graph also shows that returns are generally noisy but centered around zero.
The Drawdown shows that both indices had a severe drawdown in 2020(around -35% - -40%). J113 generally experiences slightly deeper drawdowns than J203. Recovery from drawdown took time, but both indices recovered well.
Overall, J203 is the better performer.