Analýza denných výnosov 10 najväčších akcií S&P 500

Author

Livia

Published

21. 10. 2026

Popis zvolenej databázy

Pre ďalšie hodiny som si zvolila denné burzové údaje 10 najväčších firiem indexu S&P 500 (podľa váhy v indexe): NVDA, AAPL, MSFT, AMZN, GOOGL, GOOG, META, AVGO, TSLA a BRK.B. Ide o časový rad za posledné 2 roky (od 21. 10. 2024 do 16. 10. 2026). Pre každý obchodný deň sú k dispozícii cena otvorenia (Open), najvyššia (High), najnižšia (Low) a zatváracia (Close) cena a objem obchodov (Volume).

Údaje som získala v Google Sheets funkciou

=GOOGLEFINANCE("BRK.B"; "all"; TODAY()-730; TODAY(); "DAILY")

a uložila ich do Excelu.

Prečo som si ich vybrala. Ide o časový rad, teda údaje zoradené v čase, s pravidelnou dennou frekvenciou. Údaje sú voľne dostupné a dajú sa kedykoľvek aktualizovať, takže sa na nich dá pracovať aj na ďalších hodinách. Súvisia s mojím štúdiom v odbore Data Analytics in Management a so záujmom o využitie dátovej analytiky v investovaní. Desať najväčších firiem indexu S&P 500 predstavuje významnú časť celého trhu, a preto sa na nich dá dobre ukázať riziko a výnos akcií, vzťahy medzi akciami (korelácie, variančno-kovariančná matica) aj regresné modely.

Code
Ticker <- c("NVDA","AAPL","MSFT","AMZN","GOOGL","GOOG","META","AVGO","TSLA","BRK.B")
Firma  <- c("Nvidia Corp","Apple Inc.","Microsoft Corp","Amazon.com Inc",
            "Alphabet Class A","Alphabet Class C","Meta Platforms",
            "Broadcom Inc.","Tesla Inc.","Berkshire Hathaway Class B")
Vaha   <- c(7.54, 7.11, 5.61, 3.93, 3.08, 2.87, 2.58, 2.31, 2.01, 1.62)

firmy <- data.frame(Ticker, Firma, Vaha)

firmy %>%
  kable(col.names = c("Symbol", "Firma", "Váha v indexe (%)"),
        align = c("l", "l", "r"),
        caption = "Vybrané firmy z indexu S&P 500") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, position = "center")
Vybrané firmy z indexu S&P 500
Symbol Firma Váha v indexe (%)
NVDA Nvidia Corp 7.54
AAPL Apple Inc. 7.11
MSFT Microsoft Corp 5.61
AMZN Amazon.com Inc 3.93
GOOGL Alphabet Class A 3.08
GOOG Alphabet Class C 2.87
META Meta Platforms 2.58
AVGO Broadcom Inc. 2.31
TSLA Tesla Inc. 2.01
BRK.B Berkshire Hathaway Class B 1.62

Import údajov

Údaje sú v jednom hárku Excelu, firmy sú pod sebou (každá má približne 500 riadkov) v poradí ako v tabuľke vyššie. Každá firma začína vlastnou hlavičkou. Nový blok údajov preto rozpoznám podľa toho, že dátum skočí späť na začiatok obdobia.

Code
cesta   <- "C:/Users/livia/Desktop/modregmet/ekonometria/S&P500.xlsx"
tickery <- firmy$Ticker          # poradie firiem v Exceli (zhora nadol)

# všetko načítame ako text, aby opakované hlavičky (Date, Open, ...) neprekážali
raw <- as.data.frame(read_excel(cesta, col_types = "text"))
raw <- raw[, 1:6]
names(raw) <- c("Date", "Open", "High", "Low", "Close", "Volume")

# prevod dátumu (excelovské poradové číslo alebo text RRRR-MM-DD)
na_datum <- function(x) {
  d <- as.Date(suppressWarnings(as.numeric(x)), origin = "1899-12-30")
  chyba <- is.na(d) & !is.na(x)
  d[chyba] <- as.Date(x[chyba], format = "%Y-%m-%d")
  d
}

raw$Date <- na_datum(raw$Date)
raw <- raw[!is.na(raw$Date), ]            # preč opakované hlavičky a prázdne riadky

for (s in c("Open", "High", "Low", "Close", "Volume")) {
  raw[[s]] <- suppressWarnings(as.numeric(raw[[s]]))
}

# nový blok začína tam, kde dátum skočí späť
raw$Blok <- cumsum(c(TRUE, diff(raw$Date) < 0))
stopifnot(max(raw$Blok) == length(tickery))

S_P500 <- raw %>%
  mutate(Ticker = tickery[Blok]) %>%
  select(Ticker, Date, Open, High, Low, Close, Volume) %>%
  arrange(Ticker, Date)

head(S_P500)
  Ticker       Date   Open   High    Low  Close   Volume
1   AAPL 2024-10-21 234.45 236.85 234.45 236.48 36254470
2   AAPL 2024-10-22 233.89 236.22 232.60 235.86 38846578
3   AAPL 2024-10-23 234.08 235.14 227.76 230.76 52286979
4   AAPL 2024-10-24 229.98 230.82 228.41 230.57 31109503
5   AAPL 2024-10-25 229.74 233.22 229.57 231.41 38802304
6   AAPL 2024-10-28 233.32 234.73 232.55 233.40 36087134
Code
dim(S_P500)
[1] 4990    7

Výsledkom je jedna tabuľka s 4990 riadkami, v ktorej jeden riadok predstavuje jednu firmu a jeden obchodný deň.

Code
S_P500 %>%
  group_by(Ticker) %>%
  summarise(od = min(Date), do = max(Date), pocet_dni = n(), .groups = "drop") %>%
  kable(caption = "Rozsah údajov podľa firmy") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE)
Rozsah údajov podľa firmy
Ticker od do pocet_dni
AAPL 2024-10-21 2026-10-16 499
AMZN 2024-10-21 2026-10-16 499
AVGO 2024-10-21 2026-10-16 499
BRK.B 2024-10-21 2026-10-16 499
GOOG 2024-10-21 2026-10-16 499
GOOGL 2024-10-21 2026-10-16 499
META 2024-10-21 2026-10-16 499
MSFT 2024-10-21 2026-10-16 499
NVDA 2024-10-21 2026-10-16 499
TSLA 2024-10-21 2026-10-16 499

Čistenie údajov

Názvy stĺpcov (Ticker, Date, Open, High, Low, Close, Volume) neobsahujú medzery ani zátvorky, takže ich netreba upravovať. Skontrolujem ešte chýbajúce hodnoty:

Code
colSums(is.na(S_P500))
Ticker   Date   Open   High    Low  Close Volume 
     0      0      0      0      0      0      0 

V údajoch sa nenachádzajú žiadne chýbajúce hodnoty, preto netreba nič doplňovať (imputovať).

Denné výnosy

Denný logaritmický výnos je \(r_t = \ln(P_t / P_{t-1})\). Pridám ho do tabuľky spolu s cenou prepočítanou na bázu 100 (cena prvého dňa = 100), aby sa dali porovnať firmy s rôznou cenou akcie.

Code
S_P500 <- S_P500 %>%
  group_by(Ticker) %>%
  arrange(Date, .by_group = TRUE) %>%
  mutate(
    Vynos = log(Close / lag(Close)),
    Index = 100 * Close / first(Close)
  ) %>%
  ungroup()

head(S_P500 %>% filter(Ticker == "AAPL"))
# A tibble: 6 × 9
  Ticker Date        Open  High   Low Close   Volume     Vynos Index
  <chr>  <date>     <dbl> <dbl> <dbl> <dbl>    <dbl>     <dbl> <dbl>
1 AAPL   2024-10-21  234.  237.  234.  236. 36254470 NA        100  
2 AAPL   2024-10-22  234.  236.  233.  236. 38846578 -0.00263   99.7
3 AAPL   2024-10-23  234.  235.  228.  231. 52286979 -0.0219    97.6
4 AAPL   2024-10-24  230.  231.  228.  231. 31109503 -0.000824  97.5
5 AAPL   2024-10-25  230.  233.  230.  231. 38802304  0.00364   97.9
6 AAPL   2024-10-28  233.  235.  233.  233. 36087134  0.00856   98.7

Prvý obchodný deň každej firmy nemá výnos (NA), pretože chýba predchádzajúca cena.

Grafy

Vývoj cien (báza = 100)

Code
ggplot(S_P500, aes(x = Date, y = Index, colour = Ticker)) +
  geom_line(linewidth = 0.6) +
  theme_minimal() +
  labs(title = "Vývoj zatváracích cien (začiatok = 100)",
       x = "Dátum", y = "Index ceny", colour = "Firma")

Graf ukazuje, ako sa vyvíjala hodnota 100 € investovaných do jednotlivých akcií v prvý deň sledovaného obdobia. Firmy sa výrazne líšia v tom, ako rýchlo rástli a ako veľmi ich cena kolísala.

Boxplot denných výnosov

Code
S_P500 %>%
  filter(!is.na(Vynos)) %>%
  ggplot(aes(x = Ticker, y = Vynos)) +
  geom_boxplot(fill = "lightblue", color = "darkblue", outlier.size = 0.8) +
  theme_minimal() +
  labs(title = "Rozdelenie denných výnosov", x = "Firma", y = "Log. výnos")

Výnosy všetkých firiem sa sústreďujú okolo nuly. Najužšiu škatuľu (najmenšie kolísanie) má BRK.B, najširšiu TSLA a AVGO. U všetkých firiem vidno extrémne dni (body mimo škatule), čo je pri akciách typické.

Scatter plot

Pre ďalšie výpočty potrebujem výnosy v „širokom“ tvare, teda jeden stĺpec na firmu a jeden riadok na deň:

Code
vynosy_wide <- S_P500 %>%
  select(Date, Ticker, Vynos) %>%
  pivot_wider(names_from = Ticker, values_from = Vynos) %>%
  drop_na()

dim(vynosy_wide)
[1] 496  11
Code
ggplot(vynosy_wide, aes(x = MSFT, y = AAPL)) +
  geom_point(alpha = 0.5) +
  geom_smooth(method = "lm", se = FALSE, colour = "darkorange2") +
  theme_minimal() +
  labs(title = "Denné výnosy: MSFT vs AAPL", x = "MSFT", y = "AAPL")

Medzi dennými výnosmi MSFT a AAPL je kladný vzťah: keď rastie jedna akcia, spravidla rastie aj druhá. Vzťah však nie je veľmi tesný, body sú okolo priamky dosť rozptýlené.

Základné štatistiky

Code
statistiky <- S_P500 %>%
  filter(!is.na(Vynos)) %>%
  group_by(Ticker) %>%
  summarise(
    n         = n(),
    mean      = mean(Vynos),
    sd        = sd(Vynos),
    min       = min(Vynos),
    q25       = quantile(Vynos, 0.25),
    median    = median(Vynos),
    q75       = quantile(Vynos, 0.75),
    max       = max(Vynos),
    .groups   = "drop"
  )

statistiky %>%
  kable(digits = 4, caption = "Základné štatistiky denných výnosov") %>%
  kable_styling(full_width = FALSE,
                bootstrap_options = c("striped", "hover", "condensed")) %>%
  column_spec(1, bold = TRUE) %>%
  row_spec(0, bold = TRUE, background = "#f2f2f2") %>%
  add_header_above(c(" " = 2, "Štatistiky denných výnosov" = 7))
Základné štatistiky denných výnosov
Štatistiky denných výnosov
Ticker n mean sd min q25 median q75 max
AAPL 498 0.0007 0.0180 -0.0970 -0.0070 0.0011 0.0093 0.1426
AMZN 498 0.0006 0.0213 -0.0941 -0.0111 0.0003 0.0130 0.1425
AVGO 498 0.0012 0.0327 -0.1911 -0.0148 0.0008 0.0180 0.2186
BRK.B 498 0.0003 0.0108 -0.0716 -0.0053 0.0005 0.0065 0.0568
GOOG 498 0.0014 0.0198 -0.0780 -0.0097 0.0005 0.0124 0.0950
GOOGL 498 0.0015 0.0201 -0.0757 -0.0100 0.0010 0.0125 0.0950
META 498 0.0004 0.0248 -0.1203 -0.0118 0.0008 0.0120 0.1376
MSFT 498 0.0005 0.0182 -0.1053 -0.0080 0.0005 0.0091 0.1442
NVDA 498 0.0008 0.0276 -0.1859 -0.0138 0.0016 0.0171 0.1716
TSLA 498 0.0010 0.0372 -0.1675 -0.0193 0.0001 0.0225 0.2045

Priemerný denný výnos je u všetkých firiem veľmi malý (od 0,03 % u BRK.B po 0,15 % u GOOGL). Rozdiely vo volatilite (smerodajná odchýlka) sú oveľa výraznejšie. Najstabilnejšia je BRK.B (0,011), najrizikovejšia TSLA (0,037), čo je približne trojnásobok. Vyššie riziko však v tomto období neprinieslo úmerne vyšší priemerný výnos.

Testovanie hypotéz

t-test: porovnanie priemerného výnosu NVDA a MSFT

Code
t.test(vynosy_wide$NVDA, vynosy_wide$MSFT)

    Welch Two Sample t-test

data:  vynosy_wide$NVDA and vynosy_wide$MSFT
t = 0.27391, df = 856.79, p-value = 0.7842
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
 -0.002513385  0.003328689
sample estimates:
   mean of x    mean of y 
0.0008472028 0.0004395507 

Priemerný denný výnos NVDA (0,085 %) je vyšší ako MSFT (0,044 %), ale rozdiel nie je štatisticky významný (p = 0,78), interval spoľahlivosti obsahuje nulu. Na hladine 5 % teda nezamietame, že sú priemerné výnosy rovnaké.

ANOVA: líšia sa priemerné denné výnosy medzi firmami?

Code
vynosy_long <- S_P500 %>% filter(!is.na(Vynos))
anova.result <- aov(Vynos ~ Ticker, data = vynosy_long)
summary(anova.result)
              Df Sum Sq   Mean Sq F value Pr(>F)
Ticker         9 0.0008 0.0000889   0.152  0.998
Residuals   4970 2.9079 0.0005851               

Ani medzi všetkými desiatimi firmami nie je štatisticky významný rozdiel v priemerných denných výnosoch (p = 0,998). Treba však dodať, že výnosy firiem v ten istý deň spolu súvisia, takže predpoklad nezávislosti skupín nie je celkom splnený a výsledok je skôr orientačný.

Lineárna regresia: výnos AAPL vysvetlený výnosmi MSFT a NVDA

Code
model <- lm(AAPL ~ MSFT + NVDA, data = vynosy_wide)
summary(model)

Call:
lm(formula = AAPL ~ MSFT + NVDA, data = vynosy_wide)

Residuals:
      Min        1Q    Median        3Q       Max 
-0.087122 -0.008264  0.000190  0.008299  0.098735 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) 0.0005263  0.0007613   0.691     0.49    
MSFT        0.2162356  0.0460445   4.696 3.44e-06 ***
NVDA        0.1310085  0.0303476   4.317 1.91e-05 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.01695 on 493 degrees of freedom
Multiple R-squared:  0.1247,    Adjusted R-squared:  0.1212 
F-statistic: 35.12 on 2 and 493 DF,  p-value: 5.509e-15
Code
tidy(model, conf.int = TRUE) %>%
  mutate(
    stars = case_when(
      p.value < 0.001 ~ "***",
      p.value < 0.01  ~ "**",
      p.value < 0.05  ~ "*",
      p.value < 0.1   ~ "·",
      TRUE            ~ ""
    )
  ) %>%
  transmute(
    Term = term,
    Estimate = estimate,
    `Std. Error` = std.error,
    `t value` = statistic,
    `p value` = p.value,
    `95% CI` = str_c("[", round(conf.low, 3), ", ", round(conf.high, 3), "]"),
    Sig = stars
  ) %>%
  kable(digits = 3, caption = "OLS Regression Coefficients (AAPL ~ MSFT + NVDA)") %>%
  kable_styling(full_width = FALSE,
                bootstrap_options = c("striped", "hover", "condensed")) %>%
  column_spec(1, bold = TRUE) %>%
  row_spec(0, bold = TRUE, background = "#f2f2f2") %>%
  footnote(general = "Signif. codes: *** p<0.001, ** p<0.01, * p<0.05, · p<0.1.",
           threeparttable = TRUE)
OLS Regression Coefficients (AAPL ~ MSFT + NVDA)
Term Estimate Std. Error t value p value 95% CI Sig
(Intercept) 0.001 0.001 0.691 0.49 [-0.001, 0.002]
MSFT 0.216 0.046 4.696 0.00 [0.126, 0.307] ***
NVDA 0.131 0.030 4.317 0.00 [0.071, 0.191] ***
Note:
Signif. codes: *** p<0.001, ** p<0.01, * p<0.05, · p<0.1.
Code
glance(model) %>%
  transmute(`R-squared` = r.squared,
            `Adj. R-squared` = adj.r.squared,
            `F-statistic` = statistic,
            `F p-value` = p.value,
            AIC = AIC, BIC = BIC,
            `Num. obs.` = nobs) %>%
  kable(digits = 3, caption = "Model Fit Statistics") %>%
  kable_styling(full_width = FALSE, bootstrap_options = "condensed")
Model Fit Statistics
R-squared Adj. R-squared F-statistic F p-value AIC BIC Num. obs.
0.125 0.121 35.119 0 -2632.495 -2615.669 496

Výnosy MSFT aj NVDA významne (p < 0,001) vysvetľujú výnos AAPL. Ak výnos MSFT vzrastie o 1 percentuálny bod, výnos AAPL vzrastie v priemere o 0,22 p. b. (pri nezmenenom NVDA). Pri NVDA je to 0,13 p. b. Konštanta nie je významná (p = 0,49). Model však vysvetľuje len okolo 12 % variability výnosu AAPL (R² = 0,125), výnos Apple teda závisí najmä od iných faktorov než od týchto dvoch akcií.

Variančno-kovariančná matica a vlastné čísla

Code
S <- cov(vynosy_wide %>% select(-Date))
round(S, 6)
          AAPL     AMZN      AVGO     BRK.B     GOOG    GOOGL     META     MSFT
AAPL  0.000327 0.000139  0.000150  0.000064 0.000121 0.000122 0.000148 0.000100
AMZN  0.000139 0.000456  0.000260  0.000046 0.000243 0.000246 0.000289 0.000198
AVGO  0.000150 0.000260  0.001072 -0.000006 0.000245 0.000245 0.000270 0.000191
BRK.B 0.000064 0.000046 -0.000006  0.000117 0.000021 0.000022 0.000032 0.000028
GOOG  0.000121 0.000243  0.000245  0.000021 0.000393 0.000397 0.000195 0.000112
GOOGL 0.000122 0.000246  0.000245  0.000022 0.000397 0.000405 0.000197 0.000113
META  0.000148 0.000289  0.000270  0.000032 0.000195 0.000197 0.000616 0.000168
MSFT  0.000100 0.000198  0.000191  0.000028 0.000112 0.000113 0.000168 0.000333
NVDA  0.000146 0.000258  0.000544  0.000003 0.000205 0.000206 0.000294 0.000213
TSLA  0.000245 0.000340  0.000473  0.000079 0.000337 0.000339 0.000367 0.000220
          NVDA     TSLA
AAPL  0.000146 0.000245
AMZN  0.000258 0.000340
AVGO  0.000544 0.000473
BRK.B 0.000003 0.000079
GOOG  0.000205 0.000337
GOOGL 0.000206 0.000339
META  0.000294 0.000367
MSFT  0.000213 0.000220
NVDA  0.000766 0.000452
TSLA  0.000452 0.001388
Code
eig <- eigen(S, symmetric = TRUE)
podiel <- eig$values / sum(eig$values)

data.frame(
  zlozka    = paste0("PC", seq_along(podiel)),
  lambda    = eig$values,
  podiel    = round(podiel, 3),
  kumulativ = round(cumsum(podiel), 3)
)
   zlozka       lambda podiel kumulativ
1     PC1 2.899325e-03  0.494     0.494
2     PC2 8.071200e-04  0.137     0.631
3     PC3 6.156105e-04  0.105     0.736
4     PC4 4.524472e-04  0.077     0.813
5     PC5 3.217945e-04  0.055     0.868
6     PC6 2.806329e-04  0.048     0.916
7     PC7 2.401313e-04  0.041     0.957
8     PC8 1.607684e-04  0.027     0.984
9     PC9 9.186351e-05  0.016     1.000
10   PC10 1.492612e-06  0.000     1.000
Code
korelacie <- cor(vynosy_wide %>% select(-Date))

korelacie %>%
  as.data.frame() %>%
  rownames_to_column("A") %>%
  pivot_longer(-A, names_to = "B", values_to = "r") %>%
  ggplot(aes(A, B, fill = r)) +
  geom_tile() +
  geom_text(aes(label = round(r, 2)), size = 3) +
  scale_fill_gradient2(low = "steelblue4", mid = "white",
                       high = "darkorange2", midpoint = 0) +
  theme_minimal() +
  labs(title = "Korelácie denných výnosov", x = NULL, y = NULL)

Prvá vlastná hodnota vysvetľuje približne 49 % celkového rozptylu výnosov a prvé tri spolu približne 74 %. Prvá hlavná zložka teda zodpovedá spoločnému pohybu celého trhu, ktorý ovplyvňuje všetky akcie naraz. Najmenšia vlastná hodnota je takmer nulová (\(1{,}5 \cdot 10^{-6}\)), pretože GOOG a GOOGL sú dve triedy akcií tej istej firmy a ich výnosy sú takmer totožné (korelácia približne 0,99). Matica je preto takmer singulárna, čo treba mať na pamäti pri ďalších výpočtoch (napríklad pri inverzii matice).

Záver

V dokumente som načítala denné údaje 10 najväčších akcií indexu S&P 500, overila ich kvalitu (bez chýbajúcich hodnôt), spočítala denné výnosy a ukázala ich základné štatistiky, vzťahy medzi akciami a jednoduchý regresný model. Na ďalších hodinách chcem údaje využiť na modelovanie rizika a výnosu portfólia z týchto akcií.

Zdroje

  • Cenové údaje: Google Finance (funkcia GOOGLEFINANCE v Google Sheets).
  • Váhy firiem v indexe S&P 500: verejne dostupný zoznam najväčších zložiek indexu.