본문 바로가기
투자일기

배당 고려 안한 모멘텀의 함정 : PR vs TR

by 크바시르 2025. 8. 10.

 

 

나는 연금을 포함해 대부분 계좌에서 월초에 리밸런싱을 한다. 사용하는 동적자산배분 전략은 HAA, LAA, BAA(A) 등이다. 보통 지난달까지의 월별 모멘텀을 구해, 그 결과로 어느 자산에 얼마나 투자할지 결정한다.

 

그런데 이번 달 초, 문제가 생겼다. BAA(A) 계좌에서 내 계산으로는 [방어자산]에 투자하라고 나왔다. 그런데 스노우볼72나 강환국 작가 유튜브를 보면 [공격자산]에 투자하는 걸로 판단했다.

 

BAA(A) 전략은 미국 대표주식(SPY), 신흥시장 주식(EEM), 선진시장 주식(EFA), 미국 종합채(AGG) 이렇게 네 가지 자산을 카나리아 자산으로 본다. 네 자산 모두 모멘텀이 0 이상이면 공격자산에 투자하고, 하나라도 0보다 작으면 방어자산으로 전환한다.

내 계산에서는 미국 종합채(AGG)의 모멘텀이 아주 조금 0보다 작게 나왔다. 참고한 다른 곳에서는 이 값이 0보다 크게 나와 공격자산 투자로 결정된 듯하다.

 

BAA(A)전략 요약. 출처: 스노우볼72

 

원인이 뭘까? 한참 월별 주가 데이터를 조회하는 googlefinance 함수식을 뒤져보고 주가를 직접 비교도 해봤다. 그리고 원인을 찾았다.

내가 조회한 월별 주가 데이터는 배당수익을 포함하지 않은 Price Return(PR)이라는 점을 깜빡했다.

 

잠깐 생각해보자. 만약 실제 주가가 1년간 10% 올랐고, 배당금도 1년간 10% 지급했다면 총수익은 20%다. 그런데 배당을 무시하고 10%만 수익으로 보면 모멘텀은 실제보다 낮게 나온다. 여러 번 백테스트 때는 TR(Total Return) 값을 쓰려고 노력하면서, 실제 포트폴리오 계산할 때는 이걸 놓쳤다니, 참 어이없다.

 

내가 주로 투자하는 종목은 배당금이 크지 않은 곳이라 지금까지 큰 오차를 느끼지 못했다. 그런데 상대적으로 배당 영향이 큰 채권자산에서 차이가 드러난 것이다.

이제 원인은 파악했다. 해결방법은  스프레드시트에서 과거 월별 가격을 PR에서 TR 값으로 바꿔야 한다. 그런데 문제는 googlefinance 함수로는 TR에 해당하는 수정종가(Adj Close)를 불러올 수 없다는 점이다.

 

내가 알기로 무료로 TR 값을 조회할 수 있는 곳은 야후 파이낸스가 유일했다. 그럼 매달 야후 파이낸스에 들어가 수십 종목의 값을 수동 기록하라는 건데, 가능은 해도 몇 번 하다 보면 분명 짜증이 날 것이다. 나는 그런 인간이다.

그래서 결국 내키진 않았지만, 챗GPT 도움을 받아 스크립트를 직접 작성하기 시작했다. 기본 기능부터 시작해 버그 수정과 기능 추가를 반복하며 어느 정도 쓸 만한 수준까지 만들었다. 한 번에 너무 많은 종목을 조회하면 가끔 문제가 생기기도 하지만... 일단 돌아는 간다.


월초 리밸런싱에서 배당을 무시하면 의외의 함정에 빠질 수 있다. 데이터의 정확성은 결국 성과로 돌아온다... 고 믿는다.

 

새로 계산해낸 월별 수정종가

 

function GETADJCLOSE_CROSSTABLE_MODIFIED(tickers, dates) {
  // 1. 입력된 tickers를 1차원 배열로 만들고, 빈 값(null, undefined, "")을 제거합니다.
  //    이것이 'Cannot read properties of undefined' 오류를 해결하는 핵심입니다.
  try {
    tickers = Array.isArray(tickers) ? tickers.flat() : [tickers];
    tickers = tickers.filter(function(t) { return t != null && t !== ''; });
  } catch (e) {
    return [["tickers 인자 처리 중 오류 발생: " + e.message]];
  }

  dates = Array.isArray(dates) ? dates[0] : [dates];

  if (tickers.length === 0) {
    return [["유효한 티커가 없습니다."]];
  }

  var output = [];

  for (var i = 0; i < tickers.length; i++) {
    var rowResult = [];
    for (var j = 0; j < dates.length; j++) {
      // 2. ticker를 명시적으로 문자열로 변환하여 숫자 형식으로 입력되어도 .match를 사용할 수 있게 합니다.
      var ticker = String(tickers[i]);

      // 티커가 숫자 6자리이고, .KS 또는 .KQ가 없는 경우
      if (ticker.match(/^\d{6}$/)) {
        // 코스피/코스닥 여부를 판단하는 정확한 로직은 없으므로, 일단 .KS를 기본으로 가정
        ticker = ticker + ".KS";
      }

      var originalDate = new Date(dates[j]);
      // 날짜가 유효하지 않은 경우를 확인합니다.
      if (isNaN(originalDate.getTime())) {
          rowResult.push("유효하지 않은 날짜");
          continue;
      }
      var date = new Date(originalDate.getTime());

      var adjClose = null;
      var attempts = 0;
      var maxAttempts = 7; // 최대 7일 전까지 탐색

      while (attempts < maxAttempts && adjClose === null) {
        try {
          var start = Math.floor(date.getTime() / 1000);
          var end = start + 86400;

          var url = "https://query1.finance.yahoo.com/v8/finance/chart/" + encodeURIComponent(ticker)
                    + "?period1=" + start
                    + "&period2=" + end
                    + "&interval=1d&events=div%2Csplits";

          var headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0 Safari/537.36",
            "Accept-Language": "en-US,en;q=0.9"
          };

          var resp = UrlFetchApp.fetch(url, {headers: headers, muteHttpExceptions: true});
          var json = JSON.parse(resp.getContentText());

          // 3. API 응답 데이터에 안전하게 접근하여 예기치 않은 오류를 방지합니다.
          var data = json?.chart?.result?.[0]?.indicators?.adjclose?.[0]?.adjclose?.[0];

          if (data !== null && data !== undefined) {
            adjClose = parseFloat(data);
            break; // 데이터 찾았으므로 루프 종료
          }
        } catch (e) {
          // JSON 파싱 실패 등 API 요청 자체에 오류가 발생한 경우 다음 시도(이전 날짜)로 넘어갑니다.
        }

        date.setDate(date.getDate() - 1);
        attempts++;
      }

      if (adjClose === null) {
        rowResult.push("데이터 없음");
      } else {
        rowResult.push(adjClose);
      }
    }
    output.push(rowResult);
  }

  return output;
}

 

 

'투자일기' 카테고리의 다른 글

2025년 8월 결산  (4) 2025.08.29
ISA 계좌에서 Value Rebalancing 전략  (6) 2025.08.17
2025년 7월 결산  (1) 2025.07.30
2025년 6월 결산  (0) 2025.06.28
2025년 5월 결산  (5) 2025.05.31