-
신고가 매매는 얼마나 통할까?재테크 2021. 8. 23. 23:49
이 글은 잘못된 백테스트 방법을 사용하고 있기 때문에 사용한 데이터 부분을 제외한 분석 방법만 확인하고 다음 글의 결과만을 참고하세요.
주식 매매 기법에 대한 자료를 찾다보면 신고가 매매 혹은 신고가 돌파 매매가 강력하다 라는 이야기가 가끔씩 보인다. 이 매매 기법의 논리는 전 고점을 뚫었기 때문에 그 위에 매물이 없어서 위쪽 저항이 적다는 점을 이용하는 것이다. 실제로 가만히 생각해보면 2020년 네이버와 카카오도 전 고점을 뚫고 한참을 위로 올라갔고 신풍제약과 같이 말도 안되게 올라갔던 사례도 있었다. 그래서 실제로 신고가 매매를 했을 때 코스피를 상회 할 수 있는지를 코스피 상위 100개의 종목에 대하여 분석해보았다.
데이터 수집
먼저, 상위 코스피 상위 100개의 종목을 네이버 증권 사이트의 시가총액 순위를 보면서 종목들을 기록했다. 너무 옛날 데이터는 제외하고 2018년부터 3년간의 주가 데이터를 기준으로 보기로 했기 때문에 비교적 최근에 상장한 종목인 카카오뱅크, SK바이오사이언스, 크래프톤, SK아이테크놀로지, 하이브, SK바이오팜, 에스디바이오센서, F&F 그리고 개별 종목이 아닌 KODEX200을 제외하였다. 이렇게 92개의 종목 데이터를 수집한 후에 csv 파일로 다운로드 받아서 계속 다뤘던 gspread를 이용한 방식을 통해 일 단위 주가 데이터를 수집하였다.
백테스팅 방법
기본적으로 2018년 1월 1일 데이터부터 각 종목마다 거래일의 종가를 탐색한다. 전 고점을 기준으로 신고가를 세운 날을 찾는데, 전 고점과의 거래일이 최소 30일 차이가 나야 전 고점을 돌파한 지점이라고 지정하였다 (연속적으로 고점을 돌파하는 상황을 제외). 이전의 실험과 마찬가지로 그날 종가에 주식을 샀다고 가정했을 때 N일 동안 보유하고 있다가 팔아야 이익이 가장 큰지 계산을 해보기로 했다. 또한 보유하고 있는 기간 동안의 코스피 변화율과도 비교하여 코스피 지수의 성적을 상회하는지 승률과 수익률은 어떻게 되는지 테스트를 해보았다.
결과
아래에 차트는 보유 기간에 따른 평균 수익률 및 코스피의 수익률 그래프이다. 또한 승리는 코스피의 수익률을 이겼을 때 승리라고 가정한다.
보유 시간에 따라 코스피 지수의 수익률을 이긴 비율 보유 시간이 길어질 수록 벌어지는 코스피 수익률과 돌파매매 수익률의 차이 먼저, 보유하고 있는 기간이 길거나 짧거나 코스피 수익률을 이기는 종목의 비율은 50% 근처에서 변하지 않음을 확인 할 수 있었다. 하지만 평균 수익률은 오래 보유하고 있을 수록 코스피 수익률을 크게 상회하는 것을 확인 할 수 있었다. 즉, 코스피를 이기는 종목들이 시간이 흘러갈 수록 크게 이기는 것을 확인 할 수 있었다. 특히 신풍제약과 같은 종목은 30일 신고가 돌파 후에도 기준 164%의 주가 상승으로 통계에 영향을 많이 주는? 주범이 되었다. 다만 보유기간에 대한 피크 값이 존재하는 것이 아니라 길면 길어질 수록 차이가 계속 벌어지는 것을 확인 했을 때 두 가지 인사이트가 있다.
- 신고가 돌파 매매는 생각보다 단기에 이익을 주는 기법이 아니다.
- 신고가를 돌파한 대형주들 중 코스피의 수익률을 상회하는 주식들은 오래 보유할 수록 상회하는 폭이 커진다.
개별 종목을 살펴보면 30일 보유 기간 후에 큰 수익률을 내주는 종목 16 종목 중에 10일 차에서는 5개의 종목이 코스피 수익률과 유의미한 차이가 나지 않는다. 하지만 코스피 수익률에 크게 하회한 종목 7가지는 처음 10일차 때부터 엄청난 차이로 코스피 수익률에 비해 떨어졌다. 즉, 신고가를 경신 했을 때는 장기적으로 좋은 모멘텀을 가지고 있다고 생각하고 기본적으로 오랜 기간 보유를 하되 코스피에 비해 수익률이 유의미하게 떨어지면 빠르게 손절하는 전략을 선택 할 수 있다.
나는 이 백테스트를 처음 하기 전에 단기적으로 치고 빠지는 전략이 나오지 않을까 생각했었는데, 인사이트를 많이 빗나간 결론이 나왔다. 하지만 코스피 수익률을 전혀 이기지 못 할 수도 있다고 생각했었는데 생존자편향 (신고가를 경신하면서 과거에 코스피 상위 100개의 종목이 아닌데 상위 100개의 종목이 된 경우가 많다, 즉 지금 상위 100개의 종목으로 하면 이 실험 결과보다 성능이 떨어질 수 있음)에도 불구하고 꽤 괜찮은 성적을 냈다고 생각한다.
구현
import csv import os import gspread import json import time import datetime from statistics import mean def save_stock_records(sh): with open('large_cap.csv', 'r') as f: lines = csv.reader(f) for line in lines: name, ticker = line save_price_data(sh, ticker, name, 2018, 1, 1) # Ticker 정보를 통하여 특정 기간동안의 날짜 데이터를 가져와서 json 파일로 저장 def save_price_data(sh, ticker, name, year, month, day, _type='list'): print(f'Save data: {name}: {ticker}') time.sleep(2) sh.clear() # KOSPI 데이터 로드 values = load_price_values(sh, ticker, year, month, day) # KOSPI에 없는 종목은 KOSDAQ에서 로드 if len(values) < 2: values = load_price_values(sh, ticker, year, month, day, platform='KOSDAQ') ret = [] if _type == 'list' else dict() for value in values[1:]: year, month, day, _, __ = value[0].split(' ') price = value[1] if _type == 'list': ret.append({ 'year': int(year[:-1]), 'month': int(month[:-1]), 'day': int(day), 'price': float(price) }) else: ret[f'{year[:-1]}.{month[:-1]}.{day}'] = price if len(ret) < 100: print('Skip this stock due to the lack of records') return with open(f'stocks/{ticker}_{name}.json', 'w') as df: json.dump(ret, df, indent=2) # googlefinance 함수를 통하여 주가 데이터를 시작 날짜를 기준으로 400일간 받아서 값을 가져옴 def load_price_values(sh, ticker, year, month, day, platform='KRX'): start_date = datetime.datetime(year, month, day) end_date = start_date + datetime.timedelta(days=1300) query = ( f'=googlefinance("{platform}:{ticker}", "price", "{year}. {month}. {day}", ' f'"{end_date.year}. {end_date.month}. {end_date.day}", "DAILY")' ) time.sleep(2) sh.update_cell(1, 1, query) time.sleep(2) return sh.get_all_values() # 코스피 데이터를 기준으로 offset 일 뒤의 가격을 불러옴 def get_offset_price(records, year, month, day, offset): end_price = None while end_price is None: delta = datetime.timedelta(days=offset) date = datetime.datetime(year, month, day) + delta date_key = f'{date.year}.{date.month}.{date.day}' if date_key in records: end_price = float(records[date_key]) else: offset += 1 return end_price # 주가 데이터를 기준으로 신고가 돌파 후 duration 일 만큼 보유했을 때 성적을 기록 def backtest(name, ticker, kospi_records, duration): with open(f'stocks/{ticker}_{name}.json', 'r') as price_file: records = json.load(price_file) win_count = 0 lose_count = 0 kospi_return_rates = [] return_rates = [] best_index = 0 best_price = records[0]['price'] for i, record in enumerate(records[1:-(duration + 2)]): price = record['price'] year = record['year'] month = record['month'] day = record['day'] try: if price > best_price: best_price = price if i >= best_index + 30: return_rate = (records[i + duration]['price'] - price) / price kospi = float(kospi_records[f'{year}.{month}.{day}']) kospi_offset = get_offset_price(kospi_records, year, month, day, duration) kospi_return_rate = (kospi_offset - kospi) / kospi if return_rate > kospi_return_rate: win_count += 1 else: lose_count += 1 return_rates.append(return_rate) kospi_return_rates.append(kospi_return_rate) best_index = i except Exception: continue print(f'종목: {name}, 코스피 대비 승리: {win_count}, 패배: {lose_count}') if len(return_rates) == 0: return 0, 0, [], [] print(f'평균 수익률: {mean(return_rates) * 100:.1f}%') print(f'코스피 평균 수익률: {mean(kospi_return_rates) * 100:.1f}%') return win_count, lose_count, return_rates, kospi_return_rates if __name__ == '__main__': gc = gspread.service_account(filename='../service_account.json') sh = gc.open('gspread-test').sheet1 save_stock_records(sh) save_price_data(sh, 'KOSPI', 'KOSPI', 2018, 1, 1, _type='dict') with open('stocks/KOSPI_KOSPI.json', 'r') as kospi_file: kospi_records = json.load(kospi_file) duration = 10 total_win_count = 0 total_lose_count = 0 total_return_rates = [] total_kospi_return_rates = [] for filename in os.listdir('stocks/'): ticker, name = os.path.splitext(filename)[0].split('_') if ticker == 'KOSPI': continue win_count, lose_count, return_rates, kospi_return_rates = \ backtest(name, ticker, kospi_records, duration) total_win_count += win_count total_lose_count += lose_count total_return_rates += return_rates total_kospi_return_rates += kospi_return_rates print(f'총 승리 횟수: {total_win_count}') print(f'총 패배 횟수: {total_lose_count}') print(f'총 평균 수익률: {mean(total_return_rates) * 100:.1f}%') print(f'총 평균 코스피 수익률: {mean(total_kospi_return_rates) * 100:.1f}%') print(f'수익률 차이: {(mean(total_return_rates) - mean(total_kospi_return_rates)) * 100:.1f}%')
'재테크' 카테고리의 다른 글
신고가 매매는 얼마나 통할까? (2) (2) 2021.10.09 MSCI 편입/편출 종목 매매는 효과적일까? (0) 2021.08.14 하나금융투자 Weekly Talk의 추천주 믿어도 될까? (0) 2021.06.27 주가 데이터 자동으로 가져오기 (gspread 이용) (1) 2021.06.22 삼성전자 주식 조금이라도 싸게 모을 수 있을까? (2) - RSI 보조지표의 활용 (0) 2021.03.01