iBetter Books
수정

Ch 02. 수익률과 변동성 분석

주가 자체를 분석하는 것보다 수익률을 분석하는 것이 훨씬 유용합니다. 삼성전자가 7만 원이고 애플이 200달러라면 어느 쪽이 더 좋은 투자인지 직접 비교하기 어렵습니다. 하지만 둘 다 수익률로 바꾸면 같은 척도로 비교할 수 있습니다.

이 챕터에서는 일일 수익률과 누적 수익률을 계산하고, 수익률의 변동성을 측정합니다. 마지막으로 롤링 통계를 이용해 시간에 따라 변하는 변동성을 시각화합니다.

일일 수익률 계산

일일 수익률은 어제 대비 오늘 얼마나 변했는지를 비율로 나타냅니다.

rt=PtPt1Pt1r_t = \frac{P_t - P_{t-1}}{P_{t-1}}

R에서는 lag() 함수로 전날 가격을 만들어 직접 계산하거나, tidyquant의 tq_transmute()를 사용합니다.

# 새 파일: analysis/stock_returns.R

library(tidyquant)
library(tidyverse)
library(lubridate)
library(scales)

# 데이터 수집 (Ch 01에서 이어짐)
stocks_tbl <- tq_get(
  c("AAPL", "MSFT", "GOOGL"),
  get  = "stock.prices",
  from = "2022-01-01",
  to   = "2024-12-31"
)

# 방법 1: 직접 계산 (dplyr)
aapl_ret <- stocks_tbl |>
  filter(symbol == "AAPL") |>
  arrange(date) |>
  mutate(
    daily_return = (adjusted - lag(adjusted)) / lag(adjusted)
  ) |>
  drop_na(daily_return)

head(aapl_ret |> select(date, adjusted, daily_return))

lag() 첫 행은 NA가 됩니다. drop_na()로 제거합니다.

# 방법 2: tidyquant tq_transmute() 사용 (더 간결)
aapl_daily <- stocks_tbl |>
  filter(symbol == "AAPL") |>
  tq_transmute(
    select     = adjusted,
    mutate_fun = periodReturn,
    period     = "daily",
    col_rename = "daily_return"
  )

head(aapl_daily)

periodReturn()은 quantmod 함수로, period 인자에 "daily", "weekly", "monthly"를 지정합니다. tq_transmute()가 내부적으로 호출해줍니다.

전체 종목 수익률 한 번에

# 3개 종목 모두 일일 수익률 계산
all_returns <- stocks_tbl |>
  group_by(symbol) |>
  tq_transmute(
    select     = adjusted,
    mutate_fun = periodReturn,
    period     = "daily",
    col_rename = "daily_return"
  )

# 기술 통계량
all_returns |>
  group_by(symbol) |>
  summarise(
    mean_return = mean(daily_return) * 100,
    sd_return   = sd(daily_return) * 100,
    min_return  = min(daily_return) * 100,
    max_return  = max(daily_return) * 100
  ) |>
  mutate(across(where(is.numeric), \(x) round(x, 3)))

수익률 분포 시각화

수익률은 보통 정규분포에 가깝게 분포하지만, 주식 수익률은 꼬리가 두꺼운 경우가 많습니다. 히스토그램과 밀도 곡선으로 확인합니다.

# 수익률 히스토그램 + 밀도 곡선
all_returns |>
  ggplot(aes(x = daily_return, fill = symbol)) +
  geom_histogram(
    aes(y = after_stat(density)),
    bins  = 60,
    alpha = 0.5,
    color = "white"
  ) +
  geom_density(alpha = 0, linewidth = 0.8, color = "gray30") +
  facet_wrap(~symbol, ncol = 1) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "red") +
  scale_x_continuous(labels = percent) +
  labs(
    title = "일일 수익률 분포",
    x     = "일일 수익률",
    y     = "밀도"
  ) +
  theme_minimal(base_family = "AppleGothic") +
  theme(legend.position = "none")

누적 수익률 계산

누적 수익률은 투자 시작일부터 오늘까지 얼마나 성장했는지를 보여줍니다. 복리 수익률 공식을 사용합니다.

Rcum=t=1T(1+rt)1R_{cum} = \prod_{t=1}^{T}(1 + r_t) - 1

# 누적 수익률 계산
cumulative_returns <- all_returns |>
  group_by(symbol) |>
  arrange(date) |>
  mutate(
    cum_return = cumprod(1 + daily_return) - 1
  )

# 누적 수익률 시계열 그래프
cumulative_returns |>
  ggplot(aes(x = date, y = cum_return, color = symbol)) +
  geom_line(linewidth = 0.8) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "gray60") +
  scale_y_continuous(labels = percent) +
  scale_color_manual(
    values = c("AAPL" = "#1976D2", "MSFT" = "#388E3C", "GOOGL" = "#F57C00")
  ) +
  labs(
    title    = "누적 수익률 비교 (2022-2024)",
    x        = "날짜",
    y        = "누적 수익률",
    color    = "종목"
  ) +
  theme_minimal(base_family = "AppleGothic")

변동성 계산

변동성은 수익률의 표준편차로 측정합니다. 수익률이 평균에서 얼마나 퍼져 있는지를 나타내며, 값이 클수록 위험이 큽니다. 보통 연간 단위로 환산합니다(거래일 기준 252일).

σann=σdaily×252\sigma_{ann} = \sigma_{daily} \times \sqrt{252}

# 연간화 변동성 (Annualized Volatility)
all_returns |>
  group_by(symbol) |>
  summarise(
    daily_vol = sd(daily_return),
    annual_vol = sd(daily_return) * sqrt(252)
  ) |>
  mutate(across(where(is.numeric), \(x) round(x * 100, 2))) |>
  rename(
    "일별 변동성(%)" = daily_vol,
    "연간화 변동성(%)" = annual_vol
  )

롤링 변동성

변동성은 고정된 값이 아닙니다. 시장 상황에 따라 오르내립니다. 특정 기간(예: 최근 30거래일)의 이동 표준편차를 계산하는 롤링(rolling) 변동성으로 이 변화를 포착합니다.

library(zoo)   # rollsd() 함수 제공

# 30일 롤링 변동성 계산
rolling_vol <- all_returns |>
  group_by(symbol) |>
  arrange(date) |>
  mutate(
    rolling_vol_30 = rollapply(
      daily_return,
      width   = 30,
      FUN     = sd,
      fill    = NA,
      align   = "right"
    ) * sqrt(252)   # 연간화
  ) |>
  drop_na(rolling_vol_30)

# 롤링 변동성 시각화
rolling_vol |>
  ggplot(aes(x = date, y = rolling_vol_30, color = symbol)) +
  geom_line(linewidth = 0.7, alpha = 0.9) +
  scale_y_continuous(labels = percent) +
  scale_color_manual(
    values = c("AAPL" = "#1976D2", "MSFT" = "#388E3C", "GOOGL" = "#F57C00")
  ) +
  labs(
    title    = "30일 롤링 변동성 (연간화)",
    subtitle = "높을수록 가격 변화가 크고 위험함",
    x        = "날짜",
    y        = "변동성 (연간화)",
    color    = "종목"
  ) +
  theme_minimal(base_family = "AppleGothic")

수익률과 변동성 비교 차트

수익률과 변동성을 동시에 보여주는 산점도는 투자 효율성을 한눈에 비교할 때 유용합니다. 오른쪽 위(고수익·고위험)가 아니라 왼쪽 위(고수익·저위험)에 가까울수록 좋은 투자 대상입니다.

# 종목별 평균 수익률 vs 변동성 산점도
risk_return <- all_returns |>
  group_by(symbol) |>
  summarise(
    mean_return = mean(daily_return) * 252 * 100,        # 연간화 수익률(%)
    annual_vol  = sd(daily_return) * sqrt(252) * 100     # 연간화 변동성(%)
  )

risk_return |>
  ggplot(aes(x = annual_vol, y = mean_return, label = symbol)) +
  geom_point(size = 4, color = "#1976D2") +
  ggrepel::geom_text_repel(size = 4, fontface = "bold") +
  geom_hline(yintercept = 0, linetype = "dashed", color = "gray50") +
  labs(
    title = "수익률 vs 변동성 (Risk-Return)",
    x     = "연간화 변동성 (%)",
    y     = "연간화 평균 수익률 (%)"
  ) +
  theme_minimal(base_family = "AppleGothic")

ggrepel 패키지가 없으면 install.packages("ggrepel")로 먼저 설치합니다. 레이블이 겹치지 않게 자동으로 배치해줍니다.

이 챕터를 마치며

일일 수익률, 누적 수익률, 변동성, 롤링 변동성까지 금융 분석의 핵심 지표를 계산했습니다. 수익률은 서로 다른 종목을 같은 척도로 비교하게 해주고, 변동성은 그 뒤에 숨은 위험을 드러냅니다.

다음 챕터에서는 이동평균을 계산해 주가 차트 위에 오버레이합니다. 트레이더들이 매일 보는 그 차트를 R로 직접 만들어봅니다.