본문 바로가기

퀀트투자 A to Z

데이트레이딩과 볼린저밴드: 확률적 투자 전략의 매력적인 세계

반응형

솔직히 말하자면 이 글은 오래전에 작성해 둔 글인데 창고에 썩혀두다가(사실 창고에 넣어둔 글이 몇 개 있다) 약간의 수정을 더하여 공개하고자 한다. 데이트레이딩을 몇 번 진행하면서, 데이트레이딩 매매전략에 대해 관심을 가지게 되었다. 몇 가지 매매전략을 테스트할 생각인데 그중 볼린저밴드를 활용한 매매전략을 소개하려고 한다. 개인적으로 확률적인 관점에서 볼 때 볼린저밴드를 활용한 매매방법이 굉장히 매력적으로 느껴졌고 볼린저밴드를 계산하기 위한 데이터와 함수도 만들어 놓은 것이 있었기에 빠르게 전략을 테스트해 볼 수 있다고 생각했다. 이글에서는 주로 볼린저밴드에 대해서 설명하겠지만 볼린저밴드만 보는 것이 아니라 다른 지표들과 함께 보는 것을 강력하게 추천한다. 나의 경우는 현재 진행하고 있는 포워드테스팅에서 볼린저밴드 %B (현재가가 볼린저밴드의 어느 위치에 있는지를 수치화 한 값) 매수와 매도에 기록하여 분석하고 있다.

 

볼린저밴드의 통계적 의미

볼린저밴드는 이동평균과 표준편차를 이용하여 주가의 변동폭을 보여준다. 이동평균은 주가의 과거 흐름을 보여주는 지표이며, 표준편차는 주가의 변동성 정도를 보여주는 지표이다. 볼린저밴드는 주가가 이동평균을 중심으로 일정한 범위 내에서 움직이는 경향을 이용한다. 주가가 상단밴드를 상향 돌파하면 주가가 평균 이상으로 상승했다는 것이고, 하단밴드를 하향 돌파하면 주가가 평균 이하로 하락했다는 것이다.

따라서, 볼린저밴드는 주가의 상승 추세와 하락 추세를 판단하는 데 유용한 지표가 될 수 있다.

볼린저밴드 활용 투자 전략

볼린저밴드는 주가의 변동폭을 보여주는 지표이기 때문에, 주가의 상승 추세와 하락 추세를 판단하는 데 유용하게 사용할 수 있다.

  • 상단밴드 상향 돌파: 주가가 상단밴드를 상향 돌파하면 추가 상승 가능성이 높다고 판단할 수 있다. 이때 매수에 대한 신호로 볼 수 있다.
  • 하단밴드 하향 돌파: 주가가 하단밴드를 하향 돌파하면 추가 하락 가능성이 높다고 판단할 수 있다. 이때 매도에 대한 신호로 볼 수 있다.

또한, 볼린저밴드는 주가의 과열과 과매도 상태를 판단하는 데에도 유용하게 사용할 수 있다.

  • 상단밴드 근접: 주가가 상단밴드에 근접하면 주가가 과열 상태에 있다고 판단할 수 있다. 이때 매도에 대한 신호로 볼 수 있다.
  • 하단밴드 근접: 주가가 하단밴드에 근접하면 주가가 과매도 상태에 있다고 판단할 수 있다. 이때 매수에 대한 신호로 볼 수 있다.

물론, 볼린저밴드는 단기적인 주가의 움직임을 보여주는 지표이기 때문에, 장기적인 투자에는 적합하지 않을 수 있다. 또한, 볼린저밴드는 주가의 변동폭을 보여주는 지표이기 때문에, 주가의 변동성이 큰 경우에는 신뢰도가 떨어질 수 있다.

따라서, 볼린저밴드를 활용한 투자 전략을 사용할 때에는 다른 지표와 함께 종합적으로 분석하는 것이 바람직하다.

원하는 output

telegram봇을 통해 볼린저밴드 하단 N%지점까지 내려갔다가 N% 지점을 상향 돌파한 기업과 현재가에 대한 실시간으로 정보를 제공받는다.(실제로는 다른 지표들의 조건들이 만족할 경우에 발동한다)

볼린저밴드활용-텔레그램알림
볼린저밴드 하한선 상향돌파 이벤트 발생 텔레그램 봇 notification

투자전략 아이디어

  • 전일 종가 기준으로 볼린저밴드 하단 N%지점 이하로 내려간 리스트를 필터링하고 해당 리스트에 대하여 현재가가 하단 N%지점을 상향 돌파 하는지 실시간 감시한다. 상향돌파가 이루어진 경우 텔레그램을 통하여 실시간으로 notify 한다.
  • 투자는 미래에셋증권 MTS를 이용하고 해당 이벤트 발생 메시지를 받고 주관적인 의견을 더하여 투자를 결정한다. 투자가 실패했다고 판단할 지점을 미리 설정하여 자동매도 현재가 N% 이하에 매도를 걸고 상승할 경우 급하락 시에도 손익보존을 위하여 Trailing Stop(추적손절매)를 사용하여 자동 매도하도록 설정한다.
  • 만든 로직은 기존 퀀트 백테스팅 결과가 좋은 기업들의 매수 타이밍에 적용가능한지 테스트한다.
  • 며칠 이동평균선을 사용할지와 하단 몇% 지점을 기준으로 할지에대허서는 테스트할 생각이며, 여러 정보와 실제 하루평균 이벤트가 발생하는 기업의 수가 얼마나 되는지를 고려하여 결정할 생각이다.

실제 개발코드 샘플

// 볼린저밴드를 셋팅하기위해 실제 트리거되는 함수
function setBollingerLowerBand20(){
  const startTime = new Date().getTime(); // 함수 시작 시간
  setBollingerLowerBand('ma20', 20)
  const currentTime = new Date().getTime();
  const elapsedTime = (currentTime - startTime)/1000;
  updateWorkflowDate('setBollingerLowerBand20',elapsedTime)
}
// 볼린저밴드 실시간으로 처리하기위해 트리거 되는 함수
function setRealTimeBLBAll(){
  const startTime = new Date().getTime(); // 함수 시작 시간
  setRealTimeBollingerLowerBand('ma20',20)
  const currentTime = new Date().getTime();
  const elapsedTime = (currentTime - startTime)/1000;
  updateWorkflowDate('setRealTimeBLBAll',elapsedTime)
}
// 각 기업리스트를 loop를 돌면서 볼린저밴드계산함수를 이용하여 얻은 볼린저밴드를 볼린저밴드 시트에 셋팅하는 함수
function setBollingerLowerBand(maSheetName,period){
  var price_file = SpreadsheetApp.openByUrl(PRICE_FILE);
  const dailyPriceSheet = price_file.getSheetByName('주가');
  var maFile = SpreadsheetApp.openByUrl(MA_FILE);
  const maSheet = maFile.getSheetByName(maSheetName);
  dailyPriceSheet.getActiveRange().getValues()
  const priceAllData = dailyPriceSheet.getSheetValues(1, 1, dailyPriceSheet.getLastRow(), dailyPriceSheet.getLastColumn());
  const maAllData = maSheet.getSheetValues(1, 1, dailyPriceSheet.getLastRow(), dailyPriceSheet.getLastColumn());
  const dates = [priceAllData[0]]
  var blbFile = SpreadsheetApp.openByUrl(BB_FILE);

  const blbSheet = blbFile.getSheetByName("blb"+period);
  const bubSheet = blbFile.getSheetByName("bub"+period);
  const bandwidthSheet = blbFile.getSheetByName("bandwidth"+period);

  blbSheet.getRange(1,1,1, dailyPriceSheet.getLastColumn()).setValues(dates)
  bubSheet.getRange(1,1,1, dailyPriceSheet.getLastColumn()).setValues(dates)
  bandwidthSheet.getRange(1,1,1, dailyPriceSheet.getLastColumn()).setValues(dates)

  // return
  const blbList = []
  const bubList = []
  const bandwidthList = []
  for (var i = 1; i < priceAllData.length; i++) {
      const basicInfo = priceAllData[i].slice(0, 2); 
      const priceList = priceAllData[i].slice(2);
      const maList = maAllData[i].slice(2);   
      const bollingerBand = calculateBollingerBandV4(priceList, maList, period, 2);
      blbList.push(basicInfo.concat(bollingerBand.blb))
      bubList.push(basicInfo.concat(bollingerBand.bub))
      bandwidthList.push(basicInfo.concat(bollingerBand.bandwidth))
  }
  // console.log(blbSheet.getRange(2, 1, 1, dailyPriceSheet.getLastColumn()).getValues())
  // 사실 lowerband랑 upperband는 필요가 없다.
  blbSheet.getRange(2, 1, blbList.length, dailyPriceSheet.getLastColumn()).setValues(blbList)
  bubSheet.getRange(2, 1, bubList.length, dailyPriceSheet.getLastColumn()).setValues(bubList)
  bandwidthSheet.getRange(2, 1, bandwidthList.length, dailyPriceSheet.getLastColumn()).setValues(bandwidthList)

}
// 실제 볼린저밴드를 계산하는 함수
// 파라메터로  주식가격리스트, N이동평균리스트, 볼린저밴드에 사용하는 이동평균일수, 그리고 표준편차승수이다.  
function calculateBollingerBandV4(stockPriceList, maList, period, stdMultiplier) {
  // 주가 데이터는 missingValue를 허용하지만 최소 Period기간만큼은 있어야한다.
  // maList는 missingValue를 허용하지만 period기간만큼은 있어야한다.
  // getMa 에서 missingValue가 있는 일에는 null을 강제로 채워주고있다.

  // 볼린저 밴드 하한선 = N일 이동평균선 값 - (N일 동안의 주가 표준편차 값) * 승수
  for (var i = 0; i < stockPriceList.length; i++) {
    if (stockPriceList[i] === null) {
      console.log("배열에 null 값이 포함되어 있음");
      return [];
    }
  }
  if (stockPriceList.length != maList.length) {
    console.log("배열이 일치하지 않음.");
    return []; // 배열이 일치하지 않음.
  }

  var lowerBands = [];
  var upperBands = [];
  var bandwidthList = [];
  for (var i = maList.length-1; i >=0 ; i--) {
    if (maList[i] == "" || maList[i] == null) {
      lowerBands.push(null);
      upperBands.push(null);
      bandwidthList.push(null);
      // console.log(i)
      continue
    }
    var sumSquaredDeviation = 0;
    for (var j = i; j > i-period; j--) {
      if (j < 0) break; // 인덱스가 음수가 나온다면  그것은 종료신호다 .20D 면 20개 다 계산한거임.
      sumSquaredDeviation += Math.pow(stockPriceList[j] - maList[i], 2);
    }
    var stdDeviation = Math.sqrt(sumSquaredDeviation / period);
    var lowerBand = maList[i] - (stdDeviation * stdMultiplier);
    var upperBand = maList[i] + (stdDeviation * stdMultiplier);
    var bandwidth =(stockPriceList[i] - lowerBand) / (upperBand-lowerBand)
//    %b = (최종 가격 - LB) / (UB - LB).
    lowerBands.push(lowerBand);
    upperBands.push(upperBand);
    bandwidthList.push(bandwidth);


  }
  var result = {blb: lowerBands.reverse(), bub: upperBands.reverse(), bandwidth: bandwidthList.reverse()};
return result
}

 

 

setBollingerLowerBand20와 setRealTimeBLBAll 두 함수의 차이는 전자는 전일 종가 기준으로 하루에 한 번 트리거 되고(전체 기업리스트의 2년 치가 한 번에 계산된다) 후자의 경우에는 실시간 처리하기 위해 작성하였으며 현재 볼린저%B를 실시간으로 구하기 위해 최적화된 함수를 호출하여 처리한다. 볼린저밴드를 갱신하기위해서는 주가데이터와 이동평균선 데이터가 필요하며 주가 갱신 -> 이동평균선 갱신 -> 볼린저밴드 갱신 순으로 작동한다.

 

실제 전략 테스트 후기

여러 증권사를 활용하면 여러가지 이벤트혜택을 받을수있는데 가끔씩 주식 매수 쿠폰을 주기도 한다. 이런 쿠폰이 생기면 보통 쿠폰 가격대와 비슷한 주식을 매수 / 매도한 후에 현금화 하는 편이다. 이번에는 하나증권에서 6만원 매수 쿠폰이 생겼는데 마침 볼린저밴드 관련 글을 작성중이어서 볼린저밴드를 이용한 투자를 해보았다. 볼린저밴드가 하단 N%지점에 있는 주식중에서 N%지점을 상승돌파하는 주식을 필터링하고 추가적으로 ROE PER 장기 이동평균선 등으로 필터링 하였다.(아직 국내주식의 경우 많은 종류의 퀀트데이터를 축적하고 있지 않은 상태여서 간단한 지표들만 사용하였다.) 그결과 이수페타시스가 필터링되었고 2023년 9월 27일 매수하였다. 운이 좋게도 당일의 등락률은 6.89%를 기록했다. 물론 단 1번의 테스트이기에 통계적으로 의미를 부여하진 않는다. 실제 게시하는 이 블로그글의 전략대로 매수를 해보았다는것을 공유해보는데 의미를 가진다. 참고로 8월 31일에도 마찬가지로 하나증권 쿠폰이 있었는데 그때는 아무주식이나 랜덤하게 골랐다. 선택한게 삼성전자였는데 바로 다음날 2023년 9월 1일 6.12% 상승하였다. 이정도면 쿠폰 사용할때는 운이 좋은편이라고 말해도 되려나 싶다.

이수페타시스-볼린저밴드
이수페타시스의 2023년 9월 27일 전일종가기준 볼린저밴드 %b는 0.1
이수페타시스등락률
2023년 9월 27일 이수페타시스 등락률

 

하나증권국내주식쿠폰사용
하나증권 쿠폰 사용기록