0.1 Runing Libraries

library(dplyr)
library(lubridate)
library(rugarch)
library(PerformanceAnalytics)
library(ggplot2)
library(tidyr)

1 Loading data

library(readxl)
Copy_of_Index_Data_20152020 <- read_excel("Copy of Index Data 20152020.xlsx")
Copy_of_Index_Data_20212025 <- read_excel("Copy of Index Data 20212025.xlsx")

2 Combining the data

Index_data <- bind_rows(Copy_of_Index_Data_20152020, Copy_of_Index_Data_20212025)

3 Data Examination

str(Index_data)
## 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 ...

4 Converting Statistic Date to date format

This step prepares that raw data set for time series analysis by fixing the date format and ensuring only valid return data remains.

Index_data <- Index_data %>% 
  mutate(Date = as.Date(`Statistic Date` - 25569, origin = "1899-12-30")) %>% 
  arrange(Date) %>% 
  filter(!is.na(`Total Return Index`))

5 Split by index

This creates two separate data frames, each containing data for each specific index.

j113 <- Index_data %>% filter(`Index Code`=="J113")
j203 <- Index_data %>% filter(`Index Code`=="J203")

6 Calculating log Returns

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")

6.1 Merging returns

This creates a clean, aligned wide format data set containing daily log returns for both J113 and J203 indices

returns_wide <- returns_j113 %>% 
  full_join(returns_j203, by = "Date") %>% 
  mutate(Date = ymd(Date)) %>% 
  filter(!is.na(Date)) %>% 
  distinct(Date, .keep_all = TRUE) %>% 
  arrange(Date) %>% 
  filter(if_all(c(J113, J203), ~!is.na(.x)))

View(returns_wide)

6.2 Normality Tests (skewness & kurtosis, Jarque-Bera)

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==="
print(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.

6.3 Stationarity testing: Augumented Dickey Fuller (ADF)

cat("=== Augumented Dickey-Fuller Test Results ===\n\n")
## === 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.

6.4 Long format for plotting

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.

7 Exploratory Plots and Basic Stats

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.

7.1 Annualized Stats

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.

8 GARCH(1,1) for Volatility Dynamics and Persistent

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
fit_j203_garch <- fit_garch(returns_wide$J203, "J203")
## 
## === 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.

8.1 test for signicancy of difference in Persistence for the two indices Using Likelihood Ratio Test (LR-tes)

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
cat("p-value:", round(p_val, 6), "\n")
## 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

8.2 Extracting Conditional Volatility and Variance

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 ==="
print(summary_stats)
##                    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

8.3 Ploting Conditional variance/Volatility

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"))

8.4 Assessing the difference in variance using t-test

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

9 Assymetric Volatility

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)
}

9.1 Fitting eGARCH model for J113 to test significance of leverage effect

fit_j113_egarch <- fit_egarch(returns_wide$J113, "J113")
## 
## === 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.

9.2 Fitting eGARCH model for J203 to test significance of leverage effect

fit_j203_egarch <- fit_egarch(returns_wide$J203, "J203")
## 
## === 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.

10 Comparing Risk-Adjusted Perfomance

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)

10.1 Perfomance Analytics

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 ==="
print(risk_metrics)
##                              J113   J203
## Annualized Return          0.0717 0.1022
## Annualized Std Dev         0.2577 0.1751
## Annualized Sharpe (Rf=7%) -0.0027 0.1582
Sortino_metrics <- SortinoRatio(return_xts,
                                MAR = rf_daily,
                                FUN = "StdDev")

print(Sortino_metrics)
##                                    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.

10.2 Testing the difference in Sharpe Ratios using the mean of the difference in excess return

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 ===
cat("Mean excess return J113:", round(mean(excess_j113), 6), "\n")
## Mean excess return J113: 0.000135
cat("Mean excess return J203:", round(mean(excess_j203), 6), "\n")
## Mean excess return J203: 0.00017
cat("Mean difference (J113 - J203):", round(mean(excess_j113 - excess_j203), 6), "\n")
## Mean difference (J113 - J203): -3.5e-05
cat("t-statistic:", round(t_test_result$statistic, 4), "\n")
## t-statistic: -0.1503
cat("p-value:", round(t_test_result$p.value, 6), "\n")
## 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.

10.3 Rolling 1-year Sharpe to see regime changes

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.

10.4 Full perfomamce summary plot

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.