Project 1_Time Series

Time Series Analysis: Visa (V) vs Mastercard (MA) - Q3 and Q4

นำเสนอโดย

  1. นางสาวญาณิศา ฐิติกุล (รหัสนิสิต 6742059626)
  2. นางสาวฐิตาภรณ์ ทิมศรี (รหัสนิสิต 6742026526)
  3. นางสาวพิริยดา ลิ้มวัฒนายิ่งยง (รหัสนิสิต 6742077526)

ข้อ 1-2

Load packages and data

Code
library(quantmod)
Loading required package: xts
Loading required package: zoo

Attaching package: 'zoo'
The following objects are masked from 'package:base':

    as.Date, as.Date.numeric
Loading required package: TTR
Registered S3 method overwritten by 'quantmod':
  method            from
  as.zoo.data.frame zoo 
Code
library(fpp3)   # tsibble, dplyr, tidyr, ggplot2, feasts, fable
── Attaching packages ──────────────────────────────────────────── fpp3 1.0.3 ──
✔ tibble      3.3.1     ✔ tsibble     1.2.0
✔ dplyr       1.1.4     ✔ tsibbledata 0.4.1
✔ tidyr       1.3.2     ✔ ggtime      1.0.0
✔ lubridate   1.9.5     ✔ feasts      0.5.0
✔ ggplot2     4.0.1     ✔ fable       0.5.0
── Conflicts ───────────────────────────────────────────────── fpp3_conflicts ──
✖ lubridate::date()    masks base::date()
✖ dplyr::filter()      masks stats::filter()
✖ dplyr::first()       masks xts::first()
✖ tsibble::index()     masks zoo::index()
✖ tsibble::intersect() masks base::intersect()
✖ tsibble::interval()  masks lubridate::interval()
✖ dplyr::lag()         masks stats::lag()
✖ dplyr::last()        masks xts::last()
✖ tsibble::setdiff()   masks base::setdiff()
✖ tsibble::union()     masks base::union()
Code
getSymbols("V",  src = "yahoo", from = "2026-03-30", to = "2026-10-01")
[1] "V"
Code
getSymbols("MA", src = "yahoo", from = "2026-03-30", to = "2026-10-01")
[1] "MA"
Code
range(zoo::index(V)); range(zoo::index(MA))
[1] "2026-03-30" "2026-09-30"
[1] "2026-03-30" "2026-09-30"
Code
nrow(V); nrow(MA)
[1] 128
[1] 128
Code
identical(zoo::index(V), zoo::index(MA))
[1] TRUE

สร้างตารางข้อมูลและ tsibble โดยใช้ลำดับวันทำการ (day) เป็น index เพราะวันที่จริงมีช่องว่างช่วงเสาร์-อาทิตย์

Code
prices <- inner_join(
  data.frame(Date = zoo::index(V),  V  = as.numeric(Cl(V))),
  data.frame(Date = zoo::index(MA), MA = as.numeric(Cl(MA))),
  by = "Date"
) |> mutate(day = row_number())

write.csv(prices, "close_V_MA.csv", row.names = FALSE)  

X <- prices |>
  pivot_longer(c(V, MA), names_to = "Asset", values_to = "Close") |>
  as_tsibble(index = day, key = Asset)
print(X)
# A tsibble: 256 x 4 [1]
# Key:       Asset [2]
   Date         day Asset Close
   <date>     <int> <chr> <dbl>
 1 2026-03-30     1 MA     494 
 2 2026-03-31     2 MA     500.
 3 2026-04-01     3 MA     492.
 4 2026-04-02     4 MA     493.
 5 2026-04-06     5 MA     502.
 6 2026-04-07     6 MA     498.
 7 2026-04-08     7 MA     507.
 8 2026-04-09     8 MA     504.
 9 2026-04-10     9 MA     499.
10 2026-04-13    10 MA     509.
# ℹ 246 more rows

ข้อ 3: Data Visualization

3.1 กราฟแท่งเทียนและราคาปิดรายตัว

Code
chartSeries(V)

Code
plot(V$V.Close, main = "V (Close Price in USD)")

Code
chartSeries(MA)

Code
plot(MA$MA.Close, main = "MA (Close Price in USD)")

3.2 กราฟอนุกรมเวลาของทั้งสองสินทรัพย์

Code
autoplot(X, Close) + labs(y = "USD", x = "Trading day",title = "Closing price of V and MA")

Code
plot.ts(prices$V,  xlab = "Trading day", ylab = "USD", main = "V", las = 1)

Code
plot.ts(prices$MA, xlab = "Trading day", ylab = "USD", main = "MA", las = 1)

3.3 กราฟเปรียบเทียบแบบ normalized (วันแรก = 100) และ scatter plot

ราคาของสองตัวอยู่คนละระดับ จึงปรับให้เริ่มที่ 100 เพื่อเทียบการเคลื่อนไหว

Code
plot.ts(prices$V / prices$V[1] * 100, ylab = "Index (day 1 = 100)",
        xlab = "Trading day", las = 1,
        ylim = range(c(prices$V / prices$V[1], prices$MA / prices$MA[1]) * 100))
lines(prices$MA / prices$MA[1] * 100, col = "red")
legend("topleft", legend = c("V", "MA"), col = c("black", "red"), lty = 1)

Code
library(GGally)
prices |> select(V, MA) |> ggpairs()


ข้อ 4: Trend, Seasonality, Cycle, Structural Change

4.1 Trend: Centered Moving Average

ใช้ window 5 และ 21 วันทำการ (ประมาณ 1 สัปดาห์ และ 1 เดือน)

Code
for (nm in c("V", "MA")) {
  s <- ts(prices[[nm]])
  plot.ts(s, main = paste(nm, "- CMA"), xlab = "Trading day", ylab = "USD", las = 1)
  lines(stats::filter(s, rep(1/5, 5),   method = "convolution", sides = 2), lwd = 2, col = "red")
  lines(stats::filter(s, rep(1/21, 21), method = "convolution", sides = 2), lwd = 2, col = "blue")
  legend("topleft", col = c("red", "blue"), legend = c("5-MA", "21-MA"), lty = 1, lwd = 2)
}

4.2 Trend เชิงปริมาณ: linear trend ด้วย TSLM

Code
X |> filter(Asset == "V")  |> model(TSLM(log(Close) ~ trend())) |> report()
Series: Close 
Model: TSLM 
Transformation: log(Close) 

Residuals:
     Min       1Q   Median       3Q      Max 
-0.06650 -0.01774  0.00195  0.01701  0.05604 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) 5.718e+00  4.562e-03 1253.26   <2e-16 ***
trend()     1.821e-03  6.137e-05   29.68   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.02566 on 126 degrees of freedom
Multiple R-squared: 0.8748, Adjusted R-squared: 0.8738
F-statistic: 880.7 on 1 and 126 DF, p-value: < 2.22e-16
Code
X |> filter(Asset == "MA") |> model(TSLM(log(Close) ~ trend())) |> report()
Series: Close 
Model: TSLM 
Transformation: log(Close) 

Residuals:
       Min         1Q     Median         3Q        Max 
-0.0877927 -0.0233758  0.0005826  0.0288005  0.0693619 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) 6.175e+00  6.693e-03  922.64   <2e-16 ***
trend()     1.491e-03  9.004e-05   16.56   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.03764 on 126 degrees of freedom
Multiple R-squared: 0.6853, Adjusted R-squared: 0.6828
F-statistic: 274.3 on 1 and 126 DF, p-value: < 2.22e-16

4.3 Seasonality: STL decomposition

Code
X |> model(STL(log(Close) ~ season(period = 5, window = "periodic"))) |>
  components() |> autoplot()

Code
y_v  <- ts(log(prices$V),  frequency = 5)
y_ma <- ts(log(prices$MA), frequency = 5)
plot(stl(y_v,  s.window = "periodic"), main = "V: STL")

Code
plot(stl(y_ma, s.window = "periodic"), main = "MA: STL")

4.4 Classical Decomposition

Code
X |> model(classical_decomposition(Close ~ season(5), type = "multiplicative")) |>
  components() |> autoplot()
Warning: Removed 16 rows containing missing values or values outside the scale range
(`geom_line()`).

4.5 ตรวจ seasonality ด้วย ACF

Code
X |> ACF(log(Close), lag_max = 30) |> autoplot() + labs(title = "ACF of log(Close)")

Code
X |> ACF(difference(log(Close)), lag_max = 30) |> autoplot() +
  labs(title = "ACF of differenced log(Close)")

ทดสอบด้วย Ljung-Box ว่าการเปลี่ยนแปลงรายวัน (differenced log price) ยังมีสหสัมพันธ์เหลืออยู่หรือไม่

Code
for (nm in c("V", "MA")) {
  d <- diff(log(prices[[nm]]))
  cat("\n", nm, ": Ljung-Box (lag 10) ของ diff(log(Close))\n")
  print(Box.test(d, lag = 10, type = "Ljung-Box"))
}

 V : Ljung-Box (lag 10) ของ diff(log(Close))

    Box-Ljung test

data:  d
X-squared = 7.1107, df = 10, p-value = 0.715


 MA : Ljung-Box (lag 10) ของ diff(log(Close))

    Box-Ljung test

data:  d
X-squared = 5.0808, df = 10, p-value = 0.8857

ข้อ 5:

Import Library

Code
library(quantmod)
Code
library(tseries)

ข้อ 5.1 Descriptive analysis

Mastercard:

Code
# Define
x <- Cl(MA)
plot(x)

Code
plot.ts(MA$MA.Close, las=1, ylab="Mastercard Close")

Code
min(MA$MA.Close)
[1] 471.55
Code
max(MA$MA.Close)
[1] 599.86
Code
sd(MA$MA.Close)
[1] 35.68933
Code
mean(MA$MA.Close) # sample mean
[1] 530.4012
Code
var(MA$MA.Close) # sample var
         MA.Close
MA.Close 1273.728
Code
summary(MA$MA.Close)
     Index               MA.Close    
 Min.   :2026-03-30   Min.   :471.5  
 1st Qu.:2026-05-13   1st Qu.:498.0  
 Median :2026-06-30   Median :521.9  
 Mean   :2026-06-30   Mean   :530.4  
 3rd Qu.:2026-08-14   3rd Qu.:565.8  
 Max.   :2026-09-30   Max.   :599.9  

Visa:

Code
plot.ts(V$V.Close, las=1, ylab="Visa Close")

Code
min(V$V.Close)
[1] 298.51
Code
max(V$V.Close)
[1] 384.14
Code
sd(V$V.Close)
[1] 24.62729
Code
mean(V$V.Close) # sample mean
[1] 342.9748
Code
var(V$V.Close) # sample var
         V.Close
V.Close 606.5032
Code
summary(V$V.Close)
     Index               V.Close     
 Min.   :2026-03-30   Min.   :298.5  
 1st Qu.:2026-05-13   1st Qu.:322.5  
 Median :2026-06-30   Median :345.3  
 Mean   :2026-06-30   Mean   :343.0  
 3rd Qu.:2026-08-14   3rd Qu.:365.8  
 Max.   :2026-09-30   Max.   :384.1  

ข้อ 5.2 Stationary Test and White noise

Code
# Mastercard
# ADF Test
adf.test(MA$MA.Close)

    Augmented Dickey-Fuller Test

data:  MA$MA.Close
Dickey-Fuller = -1.4452, Lag order = 5, p-value = 0.8071
alternative hypothesis: stationary

p-value = 0.7289 > 0.05 we fail to reject H0 -> non-stationary

Code
# KPSS Test
kpss.test(MA$MA.Close)
Warning in kpss.test(MA$MA.Close): p-value smaller than printed p-value

    KPSS Test for Level Stationarity

data:  MA$MA.Close
KPSS Level = 2.1106, Truncation lag parameter = 4, p-value = 0.01

p-value = 0.01 < 0.05 we reject H0 -> non-stationary

Code
# Visa
# ADF Test
adf.test(V$V.Close)

    Augmented Dickey-Fuller Test

data:  V$V.Close
Dickey-Fuller = -1.5309, Lag order = 5, p-value = 0.7715
alternative hypothesis: stationary

p-value = 0.6346 > 0.05 we fail to reject H0 -> non-stationary

Code
# KPSS Test
kpss.test(V$V.Close)
Warning in kpss.test(V$V.Close): p-value smaller than printed p-value

    KPSS Test for Level Stationarity

data:  V$V.Close
KPSS Level = 2.469, Truncation lag parameter = 4, p-value = 0.01

p-value = 0.01 < 0.05 we reject the H0 -> non-stationary

ดังนั้นทั้งในข้อมูลของ MA และ V ทั้งสอง test (ADF และ KPSS) ให้ผลลัพธ์ออกมาตรงกันว่าเป็น non-stationary -> ต้องทำการ difference จนกว่าจะ stationary

ข้อ 5.3 1st order Differencing

Code
MA_dif1 <- na.omit(diff(log(Cl(MA))))
V_dif1 <- na.omit(diff(log(Cl(V))))
Code
# MA 1st order differencing
# ADF test
adf.test(MA_dif1)
Warning in adf.test(MA_dif1): p-value smaller than printed p-value

    Augmented Dickey-Fuller Test

data:  MA_dif1
Dickey-Fuller = -4.9857, Lag order = 5, p-value = 0.01
alternative hypothesis: stationary
Code
# KPSS Test
kpss.test(MA_dif1)
Warning in kpss.test(MA_dif1): p-value greater than printed p-value

    KPSS Test for Level Stationarity

data:  MA_dif1
KPSS Level = 0.11114, Truncation lag parameter = 4, p-value = 0.1

in ADF Test: p-value < 0.05 we reject H0 -> stationary

in KPSS Test: p-value > 0.05 we fail to reject H0 -> stationary

Code
# V 1st order differencing
# ADF test
adf.test(V_dif1)
Warning in adf.test(V_dif1): p-value smaller than printed p-value

    Augmented Dickey-Fuller Test

data:  V_dif1
Dickey-Fuller = -5.7914, Lag order = 5, p-value = 0.01
alternative hypothesis: stationary
Code
# KPSS Test
kpss.test(V_dif1)
Warning in kpss.test(V_dif1): p-value greater than printed p-value

    KPSS Test for Level Stationarity

data:  V_dif1
KPSS Level = 0.14679, Truncation lag parameter = 4, p-value = 0.1

in ADF Test: p-value < 0.05 we reject H0 -> stationary

in KPSS Test: p-value > 0.05 we fail to reject H0 -> stationary

ดังนั้นการทำ 1st order differencing ก็เพียงพอให้สามารถ stationary ได้ทั้งสำหรับ MA และ V

Descriptive stats ของ log return เพื่อดูค่าตอบแทน

Code
mean(MA_dif1) * 100
[1] 0.08665502
Code
var(MA_dif1)
             MA.Close
MA.Close 0.0001919961
Code
sd(MA_dif1) * 100
[1] 1.385627
Code
min(MA_dif1)
[1] -0.04340509
Code
max(MA_dif1)
[1] 0.0341031
Code
mean(V_dif1) * 100
[1] 0.1433017
Code
var(V_dif1)
            V.Close
V.Close 0.000191638
Code
sd(V_dif1) * 100
[1] 1.384334
Code
min(V_dif1)
[1] -0.021748
Code
max(V_dif1)
[1] 0.07940085
Code
# Sample autocovariance function
acf(MA_dif1, lag.max = 30, type = "covariance", plot = TRUE, las =  1)

Code
# Sample autocovariance at lag 0
acf(MA_dif1, type = "covariance", plot = FALSE)[0]

Autocovariances of series 'MA_dif1', by lag

      0 
0.00019 

ข้อ 5.4 White noise

Code
# Sample autocorrelations
acf(MA_dif1, lag.max = 30, plot = FALSE)

Autocorrelations of series 'MA_dif1', by lag

     0      1      2      3      4      5      6      7      8      9     10 
 1.000 -0.067 -0.076  0.063 -0.143 -0.033  0.024 -0.004  0.006  0.043  0.001 
    11     12     13     14     15     16     17     18     19     20     21 
-0.036 -0.008  0.120 -0.114  0.086 -0.040  0.069  0.031  0.012  0.087 -0.047 
    22     23     24     25     26     27     28     29     30 
 0.083  0.000 -0.084  0.006 -0.131 -0.014  0.082 -0.050 -0.035 
Code
# MA - Box-Pierce Test (ไม่ใช้)
Box.test(MA_dif1, lag = 10, type = "Box-Pierce")

    Box-Pierce test

data:  MA_dif1
X-squared = 4.8615, df = 10, p-value = 0.9002
Code
# MA - Ljung-Box test
Box.test(MA_dif1, lag = 10, type = "Ljung-Box")

    Box-Ljung test

data:  MA_dif1
X-squared = 5.0808, df = 10, p-value = 0.8857

from Box-Pierce, p-value > 0.05 we fail to reject H0 -> เป็น white noise

from Ljung-Box, p-value > 0.05 we fail to reject H0 -> เป็น white noise

Code
# V - Box-Pierce Test (ไม่ใช้)
Box.test(V_dif1, lag = 10, type = "Box-Pierce")

    Box-Pierce test

data:  V_dif1
X-squared = 6.7853, df = 10, p-value = 0.7455
Code
# V - Ljung-Box test
Box.test(V_dif1, lag = 10, type = "Ljung-Box")

    Box-Ljung test

data:  V_dif1
X-squared = 7.1107, df = 10, p-value = 0.715

from Box-Pierce, p-value > 0.05 we fail to reject H0 -> เป็น white noise

from Ljung-Box, p-value > 0.05 we fail to reject H0 -> เป็น white noise


ข้อ 6 ความสัมพันธ์เชิงเวลาระหว่างสินทรัพย์

ข้อ 6.1 sample correlation

Code
# sample correlation
asset <- na.omit(merge(MA_dif1, V_dif1))
cor(asset[,1], asset[, 2])
           V.Close
MA.Close 0.8435566

มี sample correlation อยู่ที่ 0.84 โดยประมาณ

ข้อ 6.2 CCF

Code
ccf(as.numeric(asset[,1]), as.numeric(asset[,2]), lag.max = 10,
    main = "CCF of MA and V Log Returns")

Code
# ดูค่า lag0 จาก CCF
cc <- ccf(as.numeric(asset[,1]), as.numeric(asset[,2]),
          lag.max = 10, plot = FALSE)

cc$acf[cc$lag == 0]
[1] 0.8435566
Code
1.96 / sqrt(nrow(asset)) # Bound
[1] 0.1739219
Code
# ดูค่า lag เป็นตาราง
data.frame(lag = cc$lag, ccf = round(cc$acf, 4))
   lag     ccf
1  -10 -0.0299
2   -9  0.0150
3   -8  0.0001
4   -7 -0.0020
5   -6 -0.0007
6   -5 -0.0285
7   -4 -0.2012
8   -3  0.0685
9   -2 -0.0627
10  -1 -0.0026
11   0  0.8436
12   1 -0.1162
13   2 -0.0982
14   3  0.0677
15   4 -0.1675
16   5 -0.0362
17   6  0.0384
18   7 -0.0004
19   8  0.0464
20   9 -0.0112
21  10 -0.0066

เกินมาไม่เยอะมาก อาจเกิดจากข้อผิดพลาด


ข้อ 7 การวิเคราะห์แนวโน้มโดยใช้ตัวชี้วัดทางเทคนิค

SMA ของแต่ละสินทรัพย์

Code
#Plot SMA on V Close
plot.ts(V$V.Close, main = "SMA of V at Close Price", xlab = "Monthly", ylab = "Stock price",
        xaxt = "n", las = 1) 

dates <- seq(as.Date("2026-03-30"), as.Date("2026-10-01"), by = "day")
dates <- dates[!weekdays(dates) %in% c("Saturday", "Sunday")]

month_starts <- tapply(seq_along(dates), format(dates, "%Y-%m"), min)
month_starts <- month_starts[names(month_starts) != "2026-03"]   # drop March

axis(1,
     at     = as.numeric(month_starts),
     labels = format(dates[as.numeric(month_starts)], "%b"))

ma_5 <- stats::filter(V$V.Close, filter = rep(1/5,5), method = "convolution", sides = 1)
ma_21 <- stats::filter(V$V.Close, filter = rep(1/21,21), method = "convolution", sides = 1)
lines(ma_5, lwd = 2, col = "red") # 5-MA
lines(ma_21, lwd = 2, col = "blue") # 21-MA
legend("topleft", col = c("red", "blue"), legend = c("5-MA", "21-MA"),
       lty = 1, lwd = 2)

Code
#Plot SMA on MA Close
plot.ts(MA$MA.Close, main = "SMA of MA at Close Price", xlab = "Monthly", ylab = "Stock price",
        xaxt = "n", las = 1) 

dates <- seq(as.Date("2026-03-30"), as.Date("2026-10-01"), by = "day")
dates <- dates[!weekdays(dates) %in% c("Saturday", "Sunday")]  

month_starts <- tapply(seq_along(dates), format(dates, "%Y-%m"), min)
month_starts <- month_starts[names(month_starts) != "2026-03"]   # drop March

axis(1,
     at     = as.numeric(month_starts),
     labels = format(dates[as.numeric(month_starts)], "%b"))

ma_5 <- stats::filter(MA$MA.Close, filter = rep(1/5,5), method = "convolution", sides = 1)
ma_21 <- stats::filter(MA$MA.Close, filter = rep(1/21,21), method = "convolution", sides = 1)
lines(ma_5, lwd = 2, col = "red") # 5-MA
lines(ma_21, lwd = 2, col = "blue") # 21-MA
legend("topleft", col = c("red", "blue"), legend = c("5-MA", "21-MA"),
       lty = 1, lwd = 2)

EMA ของแต่ละสินทรัพย์

Code
# EMA with fixed alpha (n=21) vs optimized alpha (min SSE)

n_fixed  <- 21
alpha_fx <- 2 / (n_fixed + 1)     
cat(sprintf("Fixed alpha for n=%d: %.4f\n\n", n_fixed, alpha_fx))
Fixed alpha for n=21: 0.0909
Code
# 1. EMA function (recursive definition)
#    EMA_t = alpha * price_t + (1 - alpha) * EMA_{t-1}
#    First value seeded with first price (common convention).
ema_recursive <- function(x, alpha) {
  out <- numeric(length(x))
  out[1] <- x[1]
  for (i in 2:length(x)) {
    out[i] <- alpha * x[i] + (1 - alpha) * out[i - 1]
  }
  return(out)
}
# 2. Optimize alpha by minimizing SSE (training set)
optimize_alpha <- function(train, alpha_grid = seq(0.01, 0.99, by = 0.005)) {
  sse <- sapply(alpha_grid, function(a) {
    ema <- ema_recursive(train, a)
    sum((train - ema)^2, na.rm = TRUE)
  })
  best_alpha <- alpha_grid[which.min(sse)]
  return(list(alpha = best_alpha, sse = min(sse), grid = alpha_grid, sse_all = sse))
}

# 3. Error metrics
metrics <- function(actual, pred) {
  err <- actual - pred
  c(
    MAE  = mean(abs(err),  na.rm = TRUE),
    RMSE = sqrt(mean(err^2, na.rm = TRUE)),
    MAPE = mean(abs(err / actual), na.rm = TRUE) * 100
  )
}

# 4. Run the analysis for one series
analyse_series <- function(price, label = "Series") {
  price <- as.numeric(price)
  n     <- length(price)
  cut   <- floor(0.8 * n)
  train <- price[1:cut]
  test  <- price[(cut + 1):n]
  
  cat("=============================================\n")
  cat(" Dataset:", label, " | n =", n, "| train =", cut, "| test =", n - cut, "\n")
  cat("=============================================\n")
  
  # -- (A) Fixed alpha = 2/(n+1) with n=21 --
  ema_fx_full <- ema_recursive(price, alpha_fx)
  pred_fx     <- ema_fx_full[(cut + 1):n]
  m_fx        <- metrics(test, pred_fx)
  
  # -- (B) Optimized alpha (min SSE on train) --
  opt         <- optimize_alpha(train)
  ema_op_full <- ema_recursive(price, opt$alpha)
  pred_op     <- ema_op_full[(cut + 1):n]
  m_op        <- metrics(test, pred_op)
  
  # -- (C) Baseline: simple trailing SMA(21) for reference --
  sma21   <- stats::filter(price, rep(1 / 21, 21), sides = 1)
  pred_sm <- sma21[(cut + 1):n]
  m_sm    <- metrics(test, pred_sm)
  
  # -- Report --
  cat(sprintf("Fixed alpha  (a = %.4f, n = %d): MAE=%.4f  RMSE=%.4f  MAPE=%.2f%%\n",
              alpha_fx, n_fixed, m_fx["MAE"], m_fx["RMSE"], m_fx["MAPE"]))
  cat(sprintf("Optimized    (a = %.4f, n_eq = %.2f): MAE=%.4f  RMSE=%.4f  MAPE=%.2f%%\n",
              opt$alpha, 2 / opt$alpha - 1,
              m_op["MAE"], m_op["RMSE"], m_op["MAPE"]))
  cat(sprintf("SMA(21) reference:                    MAE=%.4f  RMSE=%.4f  MAPE=%.2f%%\n\n",
              m_sm["MAE"], m_sm["RMSE"], m_sm["MAPE"]))
  
  # -- Return everything for later use --
  invisible(list(
    fixed     = list(alpha = alpha_fx, n = n_fixed, ema = ema_fx_full, metrics = m_fx),
    optimized = list(alpha = opt$alpha, n_eq = 2 / opt$alpha - 1,
                     ema = ema_op_full, metrics = m_op,
                     grid = opt$grid, sse = opt$sse_all),
    sma21     = list(ema = as.numeric(sma21), metrics = m_sm),
    price     = price,
    train_cut = cut
  ))
}

# 5. Run on both datasets
res1 <- analyse_series(V$V.Close)
=============================================
 Dataset: Series  | n = 128 | train = 102 | test = 26 
=============================================
Fixed alpha  (a = 0.0909, n = 21): MAE=5.4426  RMSE=7.0330  MAPE=1.45%
Optimized    (a = 0.9900, n_eq = 1.02): MAE=0.0295  RMSE=0.0382  MAPE=0.01%
SMA(21) reference:                    MAE=6.8103  RMSE=8.0542  MAPE=1.83%
Code
res2 <- analyse_series(MA$MA.Close)
=============================================
 Dataset: Series  | n = 128 | train = 102 | test = 26 
=============================================
Fixed alpha  (a = 0.0909, n = 21): MAE=9.3590  RMSE=11.8905  MAPE=1.62%
Optimized    (a = 0.9900, n_eq = 1.02): MAE=0.0451  RMSE=0.0553  MAPE=0.01%
SMA(21) reference:                    MAE=12.3102  RMSE=13.9111  MAPE=2.14%

Final Window Size Plot

SMA

Code
#SMA On V Close
plot.ts(V$V.Close, main = "SMA of V at Close Price", xlab = "Monthly", ylab = "Stock price",
        xaxt = "n", las = 1) 

dates <- seq(as.Date("2026-03-30"), as.Date("2026-10-01"), by = "day")
dates <- dates[!weekdays(dates) %in% c("Saturday", "Sunday")]  

month_starts <- tapply(seq_along(dates), format(dates, "%Y-%m"), min)
month_starts <- month_starts[names(month_starts) != "2025-03"]   # drop March

axis(1,
     at     = as.numeric(month_starts),
     labels = format(dates[as.numeric(month_starts)], "%b"))

ma_21 <- stats::filter(V$V.Close, filter = rep(1/21,21), method = "convolution", sides = 1)
lines(ma_21, lwd = 2, col = "blue") # 21-MA
legend("topleft", col = "blue", legend = "21-MA", lwd = 2)

Code
#SMA On MA Close
plot.ts(MA$MA.Close, main = "SMA of MA at Close Price", xlab = "Monthly", ylab = "Stock price",
        xaxt = "n", las = 1) 

dates <- seq(as.Date("2025-03-30"), as.Date("2025-09-30"), by = "day")
dates <- dates[!weekdays(dates) %in% c("Saturday", "Sunday")]  

month_starts <- tapply(seq_along(dates), format(dates, "%Y-%m"), min)
month_starts <- month_starts[names(month_starts) != "2025-03"]   # drop March

axis(1,
     at     = as.numeric(month_starts),
     labels = format(dates[as.numeric(month_starts)], "%b"))

ma_5 <- stats::filter(MA$MA.Close, filter = rep(1/5,5), method = "convolution", sides = 1)
ma_21 <- stats::filter(MA$MA.Close, filter = rep(1/21,21), method = "convolution", sides = 1)
lines(ma_21, lwd = 2, col = "blue") # 21-MA
legend("topleft", col = "blue", legend = "21-MA", lwd = 2)

EMA

Code
#EMA
price_V  <- as.numeric(V$V.Close)
price_MA <- as.numeric(MA$MA.Close)

# Same length, same calendar (verify this!)
stopifnot(length(price_V) == length(price_MA))
n <- length(price_V)

# Numeric index (avoids the date-length problem)
idx <- 1:n

alpha_opt <- 0.99
ema_V_opt  <- ema_recursive(price_V,  alpha_opt)
ema_MA_opt <- ema_recursive(price_MA, alpha_opt)
Code
#Plot EMA comparing both stocks
plot(idx, ema_V_opt, type = "l", col = "blue", lwd = 2,
     main = expression(paste("EMA (", alpha, " = 0.99) — Stock V vs Stock MA")),
     xlab = "Trading day", ylab = "Price", las = 1,
     ylim = range(c(ema_V_opt, ema_MA_opt), na.rm = TRUE))
lines(idx, ema_MA_opt, col = "red", lwd = 2)
legend("topleft", legend = c("Stock V", "Stock MA"),
       col = c("blue","red"), lty = 1, lwd = 2, bty = "n")

Code
norm_V  <- ema_V_opt  / ema_V_opt[1]  * 100
norm_MA <- ema_MA_opt / ema_MA_opt[1] * 100
Code
#Normalized plot of EMA (starts at 100)
plot(idx, norm_V, type = "l", col = "blue", lwd = 2,
     main = expression(paste("Normalized EMA (", alpha, " = 0.99)")),
     xlab = "Trading day", ylab = "Index (Start = 100)", las = 1,
     ylim = range(c(norm_V, norm_MA), na.rm = TRUE))
lines(idx, norm_MA, col = "red", lwd = 2)
abline(h = 100, lty = 3, col = "grey40")
legend("topleft", legend = c("Stock V", "Stock MA"),
       col = c("blue","red"), lty = 1, lwd = 2, bty = "n")