파이썬으로 급등주 포착 알리미 만들기 : Part 3
토이 프로젝트/급등주 포착 알리미 만들기

파이썬으로 급등주 포착 알리미 만들기 : Part 3

by TechNyang 2024. 9. 22.
반응형

이전 포스팅에는 종목정보를 가져와서 저장하는 것에 대하여 작성했습니다.

 

 

파이썬으로 급등주 포착 알리미 만들기 : Part 2

이전 포스팅에는 아나콘다 및 파이썬을 설치한 후 코스닥 종목코드와 현재가를 가져오는 방법에 대해 서술했습니다.  파이썬으로 급등주 포착 알라미 만들기 : Part 1급등주 포착을 위해서는 우

technyang.tistory.com

 

이번에는 기술적 분석을 이용하여 추천주를 생성하는 내용을 소개해 드리려고 합니다.

 

기술적 분석기법을 이용한 추천주 생성하기

기술적 분석에는 여러 가지 방법들이 많습니다. 

눌림목 매매기법, 골든크로스 & 데드크로스, 변동성 돌파전략, 지지선 & 저항선을 활용한 기법 등뿐만 아니라 여러분들 만의 기법들도 존재할 것입니다. 

 

이동평균선 구현하기

저는 골든크로스 기법을 사용해보겠습니다.
골든크로스란, 단기 이동평균선이 장기 이동평균선을 돌파할 때를 말합니다. 
여기서 단기 이동평균선과 장기 이동 평균선의 정의는 사람마다 다를 수 있습니다.

5일선이 20일선을 돌파할 때를 단기 골든크로스, 20일선이 60일선을 돌파할 때를 장기 골든크로스라고 말하는 사람도 있습니다.

 

네이버 금융에서도 골든크로스 종목을 알려주는데요. 

이때에는 20일선이 60일선을 돌파하는 경우를 말합니다. 

여기서는 5일선이 20일선을 돌파하는 단기 골든크로스 종목과 20일선이 60일선을 돌파하는 장기 골든크로스 종목을 선별해 보겠습니다.

def getGoldenCrossStock(df): 
    goldenDf = df.sort_values(by=['날짜'], axis=0) 
    goldenDf.reset_index(drop=True, inplace=True)

 

이전 포스팅에서 결측값을 제거한 DataFrame을 df라는 매개변수로 받도록 하겠습니다.

그리고 현재 DataFrame은 날짜기준으로 내림차순 정렬이 되어 있습니다.

 

이동평균값을 계산하기 위해 날짜기준 오름차순 정렬로 변경합니다.

DataFrame을 정렬시키기 위해서는 sort_value를 사용하며 구조는 아래와 같습니다.

sort_value(by=[컬럼명], axis , ascending) 
axis = 0 : 행 기준 정렬 , axis = 1 : 열 기준 정렬 , default = 0 
ascending = True : 오름차순 , ascending = False : 내림차순 , default = True

 

이제 20일 이동평균값과 5일 이동평균값을 구해보겠습니다.

ma20 = goldenDf['종가'].rolling(20).mean() #20일 이동평균 
ma5 = goldenDf['종가'].rolling(5).mean() #5일 이동평균

 

rolling 함수의 구조는 아래와 같습니다.

rolling(window, min_periods=None, center=False, win_type=None, on=None, axis=0, 
closed=None)

 

주로 쓰이는 옵션은 window와 min_periods입니다. 

window는 묶음으로 생각하시면 되고, min_periods가 1이면 데이터가 1개라도 존재하면 연산을 수행하게 됩니다.

rolling(20).mean()는 20일씩 묶어서 평균을 계산하고 나머지 옵션들은 모두 Default로 설정한다.라는 뜻입니다. rolling(2).mean을 수행하면 아래와 같은 결과를 확인할 수 있습니다. 

 

rolling-함수를-설명하는-그림
rolling함수 설명

 

rolling함수를 사용하게 되면 어쩔 수 없이 결측값이 발생하게 됩니다.

그래서 dropna를 사용하여 결측값을 제거해야 합니다.

ma20.dropna(axis=0, inplace=True) 
ma5.dropna(axis=0, inplace=True)

 

inplace라는 옵션은 dropna함수를 사용하게 되면 처리가 된 DataFrame을 리턴하게 되는데, inplace 옵션을 True로 주게 되면 기존 DataFrame을 대체하게 되는 것입니다.

    if ma20.iloc[-1] >= ma20.iloc[-2] >= ma20.iloc[-3]:
        if ma5.iloc[-1] > ma5.iloc[-2] > ma5.iloc[-3]:
            if ma5.iloc[-2] < ma20.iloc[-2] and ma5.iloc[-1] >= ma20.iloc[-1]:
                return True
    if ma20.iloc[-1] > ma20.iloc[-2] > ma20.iloc[-3]:  # 20일선 우상향 
        if ma20.iloc[-2] < ma60.iloc[-2] and ma20.iloc[-1] >= ma60.iloc[-1]:
            return True
        else:
            return False
    else:
        return False

 

위 코드는 최근 3일 동안 우상향 혹은 횡보하는 20일 선을 5일선이 20일선 아래에 있다가 막 돌파하려는지를 체크하는 부분과 20일선이 60일선을 돌파하는지 체크하는 부분입니다.

이 부분은 자신의 전략에 맞게 수정하셔도 됩니다. 

 

그리고 또 중요한 것은 DataFrame의 값을 인덱싱하는 방법입니다. 
인덱싱하는 방법에는 loc와 iloc가 있습니다. 

loc는 열의 레이블을 기준으로 값을 참조하며, iloc는 인덱스를 기준으로 값을 참조합니다. 

 

iloc는 순서를 기준으로 값을 참조하기 때문에 ignore_index를 True로 설정한 것입니다.

위 코드를 보면 iloc[음수]로 되어 있습니다.

인덱스 번호를 음수로 주면 DataFrame의 가장 마지막부터 시작하게 되며 시작은 -1입니다.

 

최근 3일 동안 20일선과 5일선이 우상향인 상태에서 5일선이 어제는 20일선 아래에 있다가 오늘 20일선을 돌파했을 경우와 20일 선이 60일선을 돌파했을 경우 True값을 리턴합니다.

아래는 전체 코드입니다.

def getGoldenCrossStock(df):     
    goldenDf = df.sort_values(by=['날짜'], axis=0)  
    goldenDf.reset_index(drop=True, inplace=True) 
    ma60 = goldenDf['종가'].rolling(60).mean() 
    ma20 = goldenDf['종가'].rolling(20).mean() 
    ma5 = goldenDf['종가'].rolling(5).mean() 
    ma20.dropna(axis=0, inplace=True) 
    ma5.dropna(axis=0, inplace=True) 
    if ma20.iloc[-1] >= ma20.iloc[-2] >= ma20.iloc[-3]: 
        if ma5.iloc[-1] > ma5.iloc[-2] > ma5.iloc[-3]:  
            if ma5.iloc[-2] < ma20.iloc[-2] and ma5.iloc[-1] >= ma20.iloc[-1]: 
                return True 
    if ma20.iloc[-1] > ma20.iloc[-2] > ma20.iloc[-3]:
        if ma20.iloc[-2] < ma60.iloc[-2] and ma20.iloc[-1] >= ma60.iloc[-1]: 
            return True 
        else: 
            return False        
    else: 
        return False

 

getGoldenCrossStock함수는 makeDataFrame함수에서 호출하고, getGoldenCrossStock함수에서 True로 리턴 받은 종목은 별도의 리스트에 저장할 것입니다.

그러므로 makeDataFrmae 함수 안에 다음과 같은 코드를 stockDf = stockDf.dropna()밑에 추가합니다.

def makeDataFrame(codeList):
    for code in codeList:
        stockDf = pd.DataFrame()
        for page in range(1, 21):
            try:
               headers = {'User-Agent' : 'Mozilla/5.0'}
               priceUrl = 'https://finance.naver.com/item/sise_day.naver?code='+str(code)+'&page='+str(page)
               response = requests.get(priceUrl, headers = headers)
               html = BeautifulSoup(response.text, 'html.parser')
               table = html.select('table')

               if table:
                   # stockDf가 초기화되지 않았다면 빈 DataFrame으로 초기화
                   if 'stockDf' not in locals():
                       stockDf = pd.DataFrame()

                   # 테이블을 DataFrame에 추가
                   html_string = str(table[0])
                   new_df = pd.read_html(StringIO(html_string))[0]
                   stockDf = pd.concat([stockDf, new_df], ignore_index=True)

               time.sleep(0.1)
            except Exception as e:
                print(e)
                continue
        stockDf = stockDf.dropna()

        if getGoldenCrossStock(stockDf):
            goldenCrossStockList.append(code)

 

goldenCrossStockList는 makeDataFrame함수가 호출되어도 리스트 안에 저장되어 있는 값이 삭제가 되면 안 되기 때문에, 함수 바깥에 모듈들을 import 시키는 부분 밑에 추가합니다.

 

실시간 주가정보 조회 및 알리미 만들기

지금까지 골든크로스에 근접하는 종목들을 발굴하는 법을 알아봤습니다.

이제 남은 것은 장중에 실시간으로 주가정보를 확인하여 각자가 세운 기준을 충족할 때 텔레그램으로 알려주는 것입니다.

그럼 코드를 작성해 보겠습니다.

def run(): 
    isStockSend = {} 
    isGetStock = False     
    is_ready = False

 

run이라는 이름의 함수를 만들었습니다.

그리고 isSockSend라는 이름의 딕셔너리를 만들었는데요.

 

이것의 용도는 메시지가 발송된 종목에 대해서는 재차 보내지 않도록 하기 위함입니다.

isGetStock은 False로 지정했습니다. 이 것은 추천주 즉, 골든크로스 종목을 발굴했는지 여부를 나타내는 변수입니다. is_ready변수는 실시간으로 주가정보 조회를 시작할 준비가 되었는지 나타냅니다. 기본은 False로 두었습니다.

    while True:
        now = datetime.datetime.now()
        open_market = now.replace(hour=9, minute=0, second=0,
                                  microsecond=0)
        close_market = now.replace(hour=15, minute=30, second=0,
                                   microsecond=0)
        get_stock_time = now.replace(hour=16, minute=0, second=0,
                                     microsecond=0)

 

while반복문을 사용했습니다.

반복 조건이 True일 때까지입니다.

 

즉, 명시적으로 반복문을 나가지 않는 한 계속 반복된다는 것입니다.

다음은 현재날짜 및 시간을 가져오는 부분입니다.

 

이것을 위해 datetime 모듈을 import 시켜주셔야 합니다.

정석대로라면 시간대(timezone)를 ‘Asia/Seoul’로 설정한 후에 현재시각을 구해야 하지만, 해당 프로그램을 외국에서 실행시키지는 않을 것이므로 간단하게 datetime.now메소드를 사용하겠습니다.

 

현재날짜 및 시간을 now변수에 저장했습니다. 그리고 replace라는 메소드가 보이실 것입니다.

replace메소드는 괄호 안에 있는 시간으로 변경을 해주는 역할을 합니다.

 

예를 들어 now변수에 2020-10-01 06:50:00.000000라는 시간이 저장되었다고 할 때, now.replace(hour = 9, minute = 30, second= 45, microsecond = 999)로 변경할 경우 2020-10-01 09:30:45.000999로 바뀌게 됩니다.

 

즉, now 변수에 어떤 시간이 저장되어 있든지 간에, open_market변수에는 장 시작시간인 09시, close_market변수에는 장 마감시간인 15시 30분이 저장됩니다.

 

get_stock_time은 제가 임의로 정한 시간으로서 16시에 오늘 장을 기준으로 골든 크로스 종목을 찾기 시작한다는 뜻입니다.

위 코드를 보시면 while문이 계속 돌면서 현재 시간을 now변수에 저장을 하고 있습니다.

그러다가 현재시간이 get_stock_time변수에 지정된 16시보다 크거나 같고, isGetStock이 False. 즉, 아직 추천주를 발굴하지 않았으면, 분기문 안에 있는 로직을 수행합니다.

        if now >= get_stock_time and not isGetStock:
            open("recommend_list.txt", "w").close()
            getStockData()
            send_golden_stock()
            isGetStock = True
            is_ready = False

 

open메소드를 모시면 파일 입출력 모드는 ‘w’즉 쓰기 모드인데, 쓸 내용이 없습니다.

이렇게 될 경우 파일에 있는 내용이 삭제가 됩니다.

 

새로운 종목을 발굴하기 전 이전에 저장된 종목들을 삭제하기 위해 방금과 같은 코드를 사용했습니다.

그다음 getStockData함수를 호출합니다.

이렇게 되면 getStockData함수를 통해 종목들을 수집하고, makeDataFrame이 호출되면서 종목별 과거 주가정보를 DataFrame형태로 만듭니다.

 

그 후 getGoldenCrossStock함수를 호출하여 골든크로스에 해당하는 종목을 찾고 조건에 부합하면 goldenCrossStockList리스트에 종목코드를 담습니다.

 

종목명 구하기

다음 send_golden_stock()이라는 함수를 호출하는데요. 이 함수를 알아보겠습니다.

def send_golden_stock():
    if len(goldenCrossStockList) > 0:
        with open('recommend_list.txt', 'at') as f:
            for goldenItem in goldenCrossStockList:
                f.write(goldenItem)
                f.write("\n")

        recomStockStr = ''
        bot.send_message(chat_id='각자의 chat id를 입력해주세요.', text='추천주가 생성되었습니다.')
        for goldenCd in goldenCrossStockList:
            stockName = getStockName(goldenCd)
            recomStockStr += stockName+'('+goldenCd+')' + ' ,'

        bot.send_message(chat_id='각자의 chat id를 입력해주세요.', text=recomStockStr)
        print("금일의 추천주" + recomStockStr)
    else:
        bot.send_message(chat_id='각자의 chat id를 입력해주세요.', text='금일은 추천주가 없습니다.')
        print("금일은 추천주가 없습니다.")

 

함수가 호출되면 goldenCrossStockList안에 데이터가 있는지 확인을 합니다. 
len()이라는 메소드는 길이를 구하는 메소드인데요. 

 

리스트에 하나라도 데이터가 존재하면 길이는 1 이상이고, 없으면 0입니다. 

리스트의 길이가 0보다 크면, 즉 데이터가 있으면 recommend_list.txt파일을 열어서 작업을 하는데, 모드가 
‘at’입니다. 

 

앞에 a는 파일에 데이터를 추가한다는 뜻이고, 뒤 t는 텍스트 파일임을 나타내는 것입니다. 

‘a’ 모드를 쓰는 이유는 리스트에 있는 종목코드를 텍스트파일에 쓸 건데요. 

 

‘w’를 사용하게 되면 이전 코드는 덮어씌워지기 때문입니다. 

f.write("\n")는 줄 바꿈을 하기 위해서입니다. 

이 코드가 없으면 이전종목 바로 옆에 종목코드가 추가되기 때문에, 파일을 열어보았을 때 
알아보기가 힘들어집니다. 


다음 종목명과 종목코드를 받아줄 변수 recomStockStr를 선언합니다. 
goldenCrossStockList 리스트에 종목이 존재하기 때문에 텔레그램으로 추천주가 생성되었다는 메시지를 보내고, goldenCrossStockList에 있는 종목코드를 텔레그램으로 메시지를 전송합니다. 
종목코드를 이용하여 해당 종목의 종목명을 알아내는 함수의 코드는 다음과 같습니다.

def getStockName(stockCd):
    url = 'https://finance.naver.com/item/main.nhn?code=' + stockCd
    reqStockName = urlopen(url)
    bsStockName = BeautifulSoup(reqStockName, 'html.parser')
    findList = bsStockName.find_all('dl', {'class': 'blind'})
    for item in findList:
        stockName = item.find('strong')
    return stockName.get_text()

 

추천주가 생성되면 아래와 같이 텔레그램으로 메시지를 받게 됩니다.

 

텔레그램으로 추천주 메세지 받은 모습

 

추천종목을 가져왔기 때문에, isGetStock은 True로 바꿔주고, is_ready변수는 장은 이미 끝났으므로 False로 지정해 줍니다.

       if now >= open_market and now < close_market:
            if not is_ready:
                get_stock_from_txt()
                for stockCd in stock_list:
                    isStockSend[stockCd] = False

                if isGetStock:
                    isGetStock = False

                is_ready = True

 

위 코드는 현재시각이 장중일 때의 코드입니다.

is_ready변수가 False일 때, 수행이 되고 get_stock_from_txt라는 함수를 호출하는데요.

이 함수는 실시간으로 조회하고자 하는 종목이 들어있는 txt파일을 불러옵니다.

def get_stock_from_txt():
    file = open('stock_list.txt', 'r')
    while (1):
        line = file.readline()
        try:
            escape = line.index('\n')
        except:
            escape = len(line)

        if line:
            stock_list.append(line[0:escape])
        else:
            break
    file.close
    file = open('recommend_list.txt', 'r')
    while (1):
        line = file.readline()
        try:
            escape = line.index('\n')
        except:
            escape = len(line)

        if line:
            stock_list.append(line[0:escape])
        else:
            break
    file.close

 

stock_list.txt 파일은 개인적으로 관심이 있는 종목을 넣어놓은 파일입니다.

while 반복문인데 괄호 안에 1이라고 되어있지요? 컴퓨터에서는 1은 True, 0은 False를 뜻합니다.

 

즉, while (1)은 반복문을 빠져나오는 코드를 만나기 전까지는 계속 수행된다고 보면 됩니다.

readline 메소드를 사용하게 되면 텍스트 파일 안에 있는 내용을 한 줄씩 읽어옵니다.

 

관심종목 코드를 별도로 지정한 모습

 

저는 stock_list 파일에 위와 같은 종목코드를 넣어 놓았습니다.

그래서 첫 번째 반복문 수행 시 line이라는 변수에는 293490이라는 값이 저장이 됩니다.

 

line.index('\n')는 개행문자(‘\n’)의 위치를 찾는데요.

위치는 0부터 시작하므로, escape 변수에는 6이 저장됩니다.

 

이와 같은 코드가 필요한 이유는 line변수를 그대로 stock_list에 추가하게 되면 stock_list에는 293490\n이라는 값이 들어가게 됩니다.

 

우리는 개행문자를 리스트에 담을 필요는 없으므로 line[0: escape]코드를 추가하게 된 것입니다.

escape변수는 6이므로 line[0:6]은 293490 을 뜻합니다.

 

반복문이 계속 수행되다가 마지막 종목코드인 028300을 리스트에 담고, 다시 반복문을 수행합니다.

이때 readline메소드를 호출하지만 더 이상 읽어드릴 것이 없습니다.

 

그럼에도 불구하고 이 부분에서는 에러가 발생하지 않고, 다음 개행문자를 찾는 부분에서 에러가 발생합니다.

에러가 발생해도 프로그램이 종료가 되면 안 되므로 Try & Except로 예외처리를 해준 것입니다.

 

오류가 발생하여 line변수의 길이를 구하는데요. 읽어드린 데이터가 없으므로 길이는 0 이 됩니다.

파이썬은 빈 문자열의 경우 False로 간주합니다. 그래서 else문으로 가서 break를 만나 반복문을 빠져나오게 됩니다.

아래 'recommend_list.txt 파일도 동일한 방법으로 stock_list리스트에 종목코드를 담습니다.

 

다시 돌아와 get_stock_from_txt 함수 호출 이후를 보시면, 실시간으로 조회할 종목들이 들어있는 stock_list리스트에서 종목코드를 하나씩 꺼내와, isStockSend라는 딕셔너리에 담고 값을 False로 지정해 줍니다.

추천주도 얻어온 상태이기 때문에 isGetStock의 값도 다시 False로 바꿔줍니다.

 

이렇게 되면 실시간 조회할 준비가 끝났으므로 is_ready값을 True로 지정해줍니다.

 

실시간으로 주가정보 가져오기 및 변동성 돌파전략 구현

다음은 실시간으로 주가정보를 가져오는 부분을 살펴보겠습니다. 실시간 주가정보는 한국거래서에서 제공하는 API를 이용하겠습니다.

API는 국문과 영문버전이 있는데요. 국문버전의 경우 종목정보를 가져오지 못하는 경우가 있습니다.

그래서 영문을 사용하도록 하겠습니다.

            for stockCd in stock_list:
                try: #국문 URL은 한국거래소에서 종목을 못가져올 때가 있다.
                    stockUrl = 'http://asp1.krx.co.kr/servlet/krx.asp.XMLSiseEng?code='+stockCd
                    reqStock = urlopen(stockUrl)

                    bsObjStock = BeautifulSoup(reqStock, 'lxml-xml')
                    currStockInfo = bsObjStock.find('TBL_StockInfo')
                    prevStockInfo = bsObjStock.find_all('DailyStock')

                    stockName = currStockInfo['JongName']
                    currPrice = float(currStockInfo['CurJuka'].replace(',',''))
                    prevHighPrice = float(prevStockInfo[1]['day_High'].replace(',',''))
                    prevLowPrice = float(prevStockInfo[1]['day_Low'].replace(',',''))
                    startPrice = float(currStockInfo['StartJuka'].replace(',',''))
                    prevRange = (prevHighPrice - prevLowPrice) * 0.5
                    tagetPrice = startPrice + prevRange

                    if currPrice > tagetPrice and not isStockSend[stockCd]:
                        bot.send_message(chat_id='각자의 chat id를 입력해주세요.', text=stockName+'('+stockCd+') : 변동성 돌파! 현재가 : '\
                                                                   + str(currPrice) + '타겟가 : ' + str(tagetPrice))
                        isStockSend[stockCd] = True
                    time.sleep(0.1)
                except:
                    continue

 

한국 거래소 API를 통해 관심종목의 종목명, 현재가, 전일고가, 전일저가, 시가를 가져왔습니다.

그리고 현재가와 고가, 전일고가, 시가는 데이터 타입이 문자열입니다.

 

그래서 수치비교를 위해서는 정수 혹은 실수로의 전환이 필요합니다. 또한, 가격은 천 단위로 콤마가 있기 때문에 타입 변환 전에 replace 메소드를 이용하여 콤마를 제거해 줍니다.

 

그다음 코드는 래리 윌리엄스의 변동성 돌파전략을 구현한 것입니다.

변동성 돌파전략은 변동성이 큰 시장에서 일정 수준(타겟가격)을 넘어서는 상승세가 발생되면 매수하고 익일 시가에 매도하는 전략입니다.

공식은 아래와 같습니다.

 

타켓가격 = 당일시가 + (전일고가 – 전일저가) * K

 

여기서 K는 0 ~ 1까지의 상수이며, 보통 0.5 혹은 0.6으로 설정합니다. 이 부분은 여러분의 전략대로 구현하셔도 좋습니다.

텔레그램 알림을 보냈으면 메시지 재전송을 막기 위해 isStockSend딕셔너리에서 해당하는 종목의 값을 True로 지정합니다.

코드를 try와 except로 둘러싼 이유는 API를 통해 종목정보를 가져오지 못하더라도 프로그램이 중지되지 않도록 하기 위함입니다.

그리고 단시간 내에 너무 많은 호출은 자칫 차단될 수 있으므로 time.sleep을 이용하여 0.1초 정도 멈춰주도록 합니다.

아래는 코드를 실행한 결과입니다.

 

텔레그램으로-받은-변동성-돌파한-종목
텔레그램으로 받은 변동성 돌파한 종목

 

본 글에서는 텔레그램과 연동하는 법에 대해서는 별도로 다루지 않았습니다.

왜냐하면 텔레그램과 파이썬 연동하는 법은 검색해 보시면 어렵지 않게 찾으실 수 있기 때문입니다.

 

지금까지 파이썬을 활용한 급증주 포착 알리미 만들기에 대하여 설명드렸습니다.

다음 글에는 전체 소스를 제공해 드릴 예정이오니, 내용이 어려우시면 전체 소스를 참고하셔도 좋습니다.

 

긴 글 읽어주셔서 감사합니다.

반응형

TOP

Designed by 티스토리

loading