ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • (파이썬 증권데이터 분석) 삼중창 매매, 듀얼 모멘텀 투자
    computer_IT 2022. 7. 24. 09:16

     

    반응형

    2022.07.23 - [computer_IT] - (파이썬 증권데이터 분석) 볼린저 밴드 지표

     

    (파이썬 증권데이터 분석) 볼린저 밴드 지표

    2022.07.22 - [computer_IT] - (파이썬 증권데이터 분석) 일별시세 DB구축 및 시세 조회 API 개발 (파이썬 증권데이터 분석) 일별시세 DB구축 및 시세 조회 API 개발 2022.07.17 - [computer_IT] - (파이썬 증권데..

    lifenlight.tistory.com

    소스코드는 아래 링크

    https://github.com/Investar/StockAnalysisInPython

     

    GitHub - INVESTAR/StockAnalysisInPython

    Contribute to INVESTAR/StockAnalysisInPython development by creating an account on GitHub.

    github.com

     

    주식시장의 딜레마는 시간의 관점에 따라 주가가 오를 수도 있고 내릴 수도 있다는 점이다. 예를 들어 일봉 차트에서 상승 추세라 할지라도 주봉 차트에서는 하락일 수도 있고 그 반대일 수도 있다. 삼중창은 서로 다른 시간 단위에서 신호를 비교함으로써 정확한 매매 시점을 파악하도록 개발되었다.

    첫 번째 창(시장조류)

    트레이더가 일간 차트를 기준으로 매매한다면 이보다 긴 주간 차트로 추세를 분석하는 기법이다. 

    import sys
    sys.path.insert(0, '/home/05_Stock_Price_API/')  # Investar폴더의 상위폴더까지 지정
    import pandas as pd
    import matplotlib.pyplot as plt
    import datetime
    from mpl_finance import candlestick_ohlc  # pip install mpl_finance 로 설치 후 실행
    #from mplfinance.original_flavor import candlestick_ohlc
    import matplotlib.dates as mdates
    from Investar import Analyzer
    
    mk = Analyzer.MarketDB()
    df = mk.get_daily_price('엔씨소프트', '2017-01-01')
    
    
    ema60 = df.close.ewm(span=60).mean()   # ① 종가의 12주 지수 이동평균
    ema130 = df.close.ewm(span=130).mean() # ② 종가의 12주 지수 이동평균
    macd = ema60 - ema130                  # ③ MACD선
    signal = macd.ewm(span=45).mean()      # ④ 신호선(MACD의 9주 지수 이동평균)
    macdhist = macd - signal               # ⑤ MACD 히스토그램
    
    df = df.assign(ema130=ema130, ema60=ema60, macd=macd, signal=signal,
        macdhist=macdhist).dropna() 
    df['number'] = df.index.map(mdates.date2num)  # ⑥
    ohlc = df[['number','open','high','low','close']]
    
    plt.figure(figsize=(9, 7))
    p1 = plt.subplot(2, 1, 1)
    plt.title('Triple Screen Trading - First Screen (NCSOFT)')
    plt.grid(True)
    candlestick_ohlc(p1, ohlc.values, width=.6, colorup='red', 
        colordown='blue')  # ⑦
    p1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
    plt.plot(df.number, df['ema130'], color='c', label='EMA130')
    plt.legend(loc='best')
    
    p2 = plt.subplot(2, 1, 2)
    plt.grid(True)
    p2.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
    plt.bar(df.number, df['macdhist'], color='m', label='MACD-Hist')
    plt.plot(df.number, df['macd'], color='b', label='MACD')
    plt.plot(df.number, df['signal'], 'g--', label='MACD-Signal')
    plt.legend(loc='best')
    plt.savefig('1stScreen.png')  # plt.show()

    결과 그래프

    삼중창 매매 시스템 첫 번째 창

    책의 저자는 EMA130(26주 지수 이동평균)을 주간 추세추종 지표로 사용하고 있다고 한다. 결론적으로 EMA130선이 오르고 있을 때만 시장에 참여하면 된다. 현재 시점으로 볼 때는 관망이 적절할 듯 싶다.

    두 번째 창(시장 파도)

    130일 지수 이동평균과 스토캐스틱을 사용하여 시장이 하락할 때 매수기회, 상승할 때 매도 기회를 제공하는 기법이다.

    import sys
    sys.path.insert(0, '/home/05_Stock_Price_API/')  # Investar폴더의 상위폴더까지 지정
    import pandas as pd
    import matplotlib.pyplot as plt
    import datetime
    from mpl_finance import candlestick_ohlc
    #from mplfinance.original_flavor import candlestick_ohlc
    import matplotlib.dates as mdates
    from Investar import Analyzer
    
    mk = Analyzer.MarketDB()
    df = mk.get_daily_price('엔씨소프트', '2017-01-01')
    
    ema60 = df.close.ewm(span=60).mean()
    ema130 = df.close.ewm(span=130).mean() 
    macd = ema60 - ema130
    signal = macd.ewm(span=45).mean() 
    macdhist = macd - signal
    
    df = df.assign(ema130=ema130, ema60=ema60, macd=macd, signal=signal,
        macdhist=macdhist).dropna()
    df['number'] = df.index.map(mdates.date2num)
    ohlc = df[['number','open','high','low','close']]
    
    ndays_high = df.high.rolling(window=14, min_periods=1).max()      # ① 14일 동안의 최대값
    ndays_low = df.low.rolling(window=14, min_periods=1).min()        # ② 14일 동안의 최소값
    fast_k = (df.close - ndays_low) / (ndays_high - ndays_low) * 100  # ③ 빠른 스토캐스틱
    slow_d= fast_k.rolling(window=3).mean()                           # ④ 느린 스토캐스틱(3일 평균선)
    df = df.assign(fast_k=fast_k, slow_d=slow_d).dropna()             # ⑤ 결측치 제거
    
    plt.figure(figsize=(9, 7))
    p1 = plt.subplot(2, 1, 1)
    plt.title('Triple Screen Trading - Second Screen (NCSOFT)')
    plt.grid(True)
    candlestick_ohlc(p1, ohlc.values, width=.6, colorup='red', colordown='blue')
    p1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
    plt.plot(df.number, df['ema130'], color='c', label='EMA130')
    plt.legend(loc='best')
    p1 = plt.subplot(2, 1, 2)
    plt.grid(True)
    p1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
    plt.plot(df.number, df['fast_k'], color='c', label='%K')
    plt.plot(df.number, df['slow_d'], color='k', label='%D')
    plt.yticks([0, 20, 80, 100]) # ⑥ Y축 눈금(스토캐스틱 기준선)
    plt.legend(loc='best')
    plt.savefig('2ndScreen.png')  #plt.show()

    결과 그래프

    삼중창 매매 시스템 두 번째 창

    책에서는 EMA130이 상승하고 있고 스토캐스틱이 30 아래로 내려가면 매수, EMA130이 하락하고 있고 스토캐스틱이 70 위로 올라가면 매도 기회라고 한다. 그러면 기준선을 20과 80이 아닌 30과 70으로 했어야 하는데 왜 그런지 모르겠다. 느린 스토캐스틱 %D를 사용하면 좀 더 확실한 신호를 잡을 수 있다고 한다.

    세 번째 창(진입 기술)

    세 번째 창은 차트나 지표를 필요로 하지 않고 단지 첫 번째 창과 두 번째 창이 동시에 매매 신호를 내면 진입 시점을 찾아내는 기법이다. (근데 왜 삼중창 매매 기술인지? 이중창 매매라고 하지...)

    이를 바탕으로 매수 매도 시점을 그려보자.

    import sys
    sys.path.insert(0, '/home/05_Stock_Price_API/')  # Investar폴더의 상위폴더까지 지정
    import pandas as pd
    import matplotlib.pyplot as plt
    import datetime
    from mpl_finance import candlestick_ohlc
    #from mplfinance.original_flavor import candlestick_ohlc
    import matplotlib.dates as mdates
    from Investar import Analyzer
    
    mk = Analyzer.MarketDB()
    df = mk.get_daily_price('엔씨소프트', '2017-01-01')
    
    ema60 = df.close.ewm(span=60).mean()
    ema130 = df.close.ewm(span=130).mean()
    macd = ema60 - ema130
    signal = macd.ewm(span=45).mean()
    macdhist = macd - signal
    df = df.assign(ema130=ema130, ema60=ema60, macd=macd, signal=signal, macdhist=macdhist).dropna()
    
    df['number'] = df.index.map(mdates.date2num)
    ohlc = df[['number','open','high','low','close']]
    
    ndays_high = df.high.rolling(window=14, min_periods=1).max()
    ndays_low = df.low.rolling(window=14, min_periods=1).min()
    
    fast_k = (df.close - ndays_low) / (ndays_high - ndays_low) * 100
    slow_d = fast_k.rolling(window=3).mean()
    df = df.assign(fast_k=fast_k, slow_d=slow_d).dropna()
    
    plt.figure(figsize=(9, 9))
    p1 = plt.subplot(3, 1, 1)
    plt.title('Triple Screen Trading (NCSOFT)')
    plt.grid(True)
    candlestick_ohlc(p1, ohlc.values, width=.6, colorup='red', colordown='blue')
    p1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
    plt.plot(df.number, df['ema130'], color='c', label='EMA130')
    for i in range(1, len(df.close)):
        if df.ema130.values[i-1] < df.ema130.values[i] and \
            df.slow_d.values[i-1] >= 20 and df.slow_d.values[i] < 20:
            plt.plot(df.number.values[i], 250000, 'r^') 
        elif df.ema130.values[i-1] > df.ema130.values[i] and \
            df.slow_d.values[i-1] <= 80 and df.slow_d.values[i] > 80:
            plt.plot(df.number.values[i], 250000, 'bv') 
    plt.legend(loc='best')
    
    p2 = plt.subplot(3, 1, 2)
    plt.grid(True)
    p2.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
    plt.bar(df.number, df['macdhist'], color='m', label='MACD-Hist')
    plt.plot(df.number, df['macd'], color='b', label='MACD')
    plt.plot(df.number, df['signal'], 'g--', label='MACD-Signal')
    plt.legend(loc='best')
    
    p3 = plt.subplot(3, 1, 3)
    plt.grid(True)
    p3.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
    plt.plot(df.number, df['fast_k'], color='c', label='%K')
    plt.plot(df.number, df['slow_d'], color='k', label='%D')
    plt.yticks([0, 20, 80, 100])
    plt.legend(loc='best')
    plt.savefig('3rdScreen.png')  # plt.show()

    결과 그래프

     

    삼중창 시스템 엔씨소프트 매매 결과

    그래프를 보면 EMA130이 상승하고 있을 때 매수 포인트에서 매수하면 수익이 발생할 수 있다. 다만 EMA130이 상승이 둔화되다 하락하는 2021년 3~4월경의 매수 신호는 손실이 발생하는 포인트다. 오히려 매도 포인트가 좋을 것이다.

    듀얼 모멘텀 투자

    뉴턴 운동 제1법칙인 '관성의 법칙'은 외부 힘이 작용하지 않는 한 물체의 질량 중심은 일정한 속도로 움직인다는 것이다. 주식 시장에서도 모멘텀은 비슷한 의미로 쓰이는데, 한 번 움직이기 시작한 주식 가격이 계속 그 방향으로 나아가려는 성질을 가르킨다. 로버트 래비는 1968년 출간된 "주식 가격 예상을 위한 상대 강도 개념"이라는 책에서 상대 강도가 높은 주식들을 매수했을 때 26주 이후부터 시장 초과 수익이 발생한다고 했다. 상대 강도가 높은 주식이란 26주 이동평균선을 기준으로 더 많이 오른 종목을 말한다. 게리 안토나치의 듀얼 모멘텀 투자는 상대 강도가 센 주식 종목들에 투자하는 상대적 모멘텀 전략과 과거 6~12개월의 수익이 단기 국채 수익률을 능가하는 강세장에서만 투자하는 절대적 모멘텀 전략을 하나로 합친 전략이다. 

    import sys
    sys.path.insert(0, '/home/05_Stock_Price_API/')  # Investar폴더의 상위폴더까지 지정
    import pandas as pd
    import pymysql
    from datetime import datetime
    from datetime import timedelta
    from Investar import Analyzer
    
    class DualMomentum:
        def __init__(self):
            """생성자: KRX 종목코드(codes)를 구하기 위한 MarkgetDB 객체 생성"""
            self.mk = Analyzer.MarketDB()
        
        def get_rltv_momentum(self, start_date, end_date, stock_count):
            """특정 기간 동안 수익률이 제일 높았던 stock_count 개의 종목들 (상대 모멘텀)
                - start_date  : 상대 모멘텀을 구할 시작일자 ('2020-01-01')   
                - end_date    : 상대 모멘텀을 구할 종료일자 ('2020-12-31')
                - stock_count : 상대 모멘텀을 구할 종목수
            """       
            connection = pymysql.connect(host='localhost', port=3306, 
                db='Investar', user='root', passwd='*****',charset='utf8', autocommit=True)
            cursor = connection.cursor()
            
            # 사용자가 입력한 시작일자를 DB에서 조회되는 일자로 보정 
            sql = f"select max(date) from daily_price where date <= '{start_date}'"
            cursor.execute(sql)
            result = cursor.fetchone()
            if (result[0] is None):
                print ("start_date : {} -> returned None".format(sql))
                return
            start_date = result[0].strftime('%Y-%m-%d')
    
    
            # 사용자가 입력한 종료일자를 DB에서 조회되는 일자로 보정
            sql = f"select max(date) from daily_price where date <= '{end_date}'"
            cursor.execute(sql)
            result = cursor.fetchone()
            if (result[0] is None):
                print ("end_date : {} -> returned None".format(sql))
                return
            end_date = result[0].strftime('%Y-%m-%d')
    
    
            # KRX 종목별 수익률을 구해서 2차원 리스트 형태로 추가
            rows = []
            columns = ['code', 'company', 'old_price', 'new_price', 'returns']
            for _, code in enumerate(self.mk.codes):            
                sql = f"select close from daily_price "\
                    f"where code='{code}' and date='{start_date}'"
                cursor.execute(sql)
                result = cursor.fetchone()
                if (result is None):
                    continue
                old_price = int(result[0])
                sql = f"select close from daily_price "\
                    f"where code='{code}' and date='{end_date}'"
                cursor.execute(sql)
                result = cursor.fetchone()
                if (result is None):
                    continue
                new_price = int(result[0])
                returns = (new_price / old_price - 1) * 100
                rows.append([code, self.mk.codes[code], old_price, new_price, 
                    returns])
    
    
            # 상대 모멘텀 데이터프레임을 생성한 후 수익률순으로 출력
            df = pd.DataFrame(rows, columns=columns)
            df = df[['code', 'company', 'old_price', 'new_price', 'returns']]
            df = df.sort_values(by='returns', ascending=False)
            df = df.head(stock_count)
            df.index = pd.Index(range(stock_count))
            connection.close()
            print(df)
            print(f"\nRelative momentum ({start_date} ~ {end_date}) : "\
                f"{df['returns'].mean():.2f}% \n")
            return df
        
        def get_abs_momentum(self, rltv_momentum, start_date, end_date):
            """특정 기간 동안 상대 모멘텀에 투자했을 때의 평균 수익률 (절대 모멘텀)
                - rltv_momentum : get_rltv_momentum() 함수의 리턴값 (상대 모멘텀)
                - start_date    : 절대 모멘텀을 구할 매수일 ('2020-01-01')   
                - end_date      : 절대 모멘텀을 구할 매도일 ('2020-12-31')
            """
            stockList = list(rltv_momentum['code'])        
            connection = pymysql.connect(host='localhost', port=3306, 
                db='Investar', user='root', passwd='*****',charset='utf8', autocommit=True)
            cursor = connection.cursor()
    
    
            # 사용자가 입력한 매수일을 DB에서 조회되는 일자로 변경 
            sql = f"select max(date) from daily_price "\
                f"where date <= '{start_date}'"
            cursor.execute(sql)
            result = cursor.fetchone()
            if (result[0] is None):
                print ("{} -> returned None".format(sql))
                return
            start_date = result[0].strftime('%Y-%m-%d')
    
    
            # 사용자가 입력한 매도일을 DB에서 조회되는 일자로 변경 
            sql = f"select max(date) from daily_price "\
                f"where date <= '{end_date}'"
            cursor.execute(sql)
            result = cursor.fetchone()
            if (result[0] is None):
                print ("{} -> returned None".format(sql))
                return
            end_date = result[0].strftime('%Y-%m-%d')
    
    
            # 상대 모멘텀의 종목별 수익률을 구해서 2차원 리스트 형태로 추가
            rows = []
            columns = ['code', 'company', 'old_price', 'new_price', 'returns']
            for _, code in enumerate(stockList):            
                sql = f"select close from daily_price "\
                    f"where code='{code}' and date='{start_date}'"
                cursor.execute(sql)
                result = cursor.fetchone()
                if (result is None):
                    continue
                old_price = int(result[0])
                sql = f"select close from daily_price "\
                    f"where code='{code}' and date='{end_date}'"
                cursor.execute(sql)
                result = cursor.fetchone()
                if (result is None):
                    continue
                new_price = int(result[0])
                returns = (new_price / old_price - 1) * 100
                rows.append([code, self.mk.codes[code], old_price, new_price,
                    returns])
    
    
            # 절대 모멘텀 데이터프레임을 생성한 후 수익률순으로 출력
            df = pd.DataFrame(rows, columns=columns)
            df = df[['code', 'company', 'old_price', 'new_price', 'returns']]
            df = df.sort_values(by='returns', ascending=False)
            connection.close()
            print(df)
            print(f"\nAbasolute momentum ({start_date} ~ {end_date}) : "\
                f"{df['returns'].mean():.2f}%")
            return

    2021년 7월부터 12월까지 6개월간 상대모멘텀으로 구한 강세주 300종목을 매수했다면, 6개월 뒤에는 -25% 손실이 발생한다.

    듀얼모멘텀 전략

    2022년 상반기 장은 대세 하락장이었기 때문에 위와 같은 손실을 면하기 어려웠을 거라고 본다. 강환국 저 "듀얼 모멘텀 투자 전략" 책에서는 한국 시장에서는 3개월 전략이 수익이 좋았다고 한다. 한국 자산만으로 운영하는 전략에는 3개월 듀얼 모멘텀을 적용하고 해외 자산과 혼합하는 경우 12개월 듀얼 모멘텀을 적용하는 한국형 전략도 고려해볼 만하다고 한다. 미국 국채를 직접 매수하는 대신 'iShares 20+ Year Treasury Bond ETF(TLT)'같은 ETF에 투자하는 경우, 미국 국채 수익률을 추종하면서 환 헤지까지 할 수 있는 장점이 있다고 한다.

    2022.07.24 - [computer_IT] - (파이썬 증권데이터 분석) 장고 웹서버 구축 및 자동화

     

    (파이썬 증권데이터 분석) 장고 웹서버 구축 및 자동화

    2022.07.24 - [computer_IT] - (파이썬 증권데이터 분석) 삼중창 매매, 듀얼 모멘텀 투자 (파이썬 증권데이터 분석) 삼중창 매매, 듀얼 모멘텀 투자 2022.07.23 - [computer_IT] - (파이썬 증권데이터 분석) 볼린..

    lifenlight.tistory.com

     

    반응형

    댓글

Designed by Tistory.