저번 포스트에서 OpenDartReader를 이용해 공시 재무정보를 읽어보았습니다.
이번 포스트에서는 펙터데이터를 계산해보고, 필터조건을 만들어보기 시작합니다.
팩터데이터 계산하기
앞선 포스트에서는 기본적인 재무정보를 불러오는 작업을 진행했었습니다.
def extract_finance_data(self, finance_years, df):
#디버깅을 위한 설정
pd.set_option('display.max_columns', None)
pd.options.display.float_format = '{:.2f}'.format
data = []
count = 1
for row in df.itertuples():
#터미널 상의 추출상황 로깅을 위한 프린트문 현재갯수/전체종목갯수, 종목명
print(f"extracting {count}/{len(df)} {row[2]}...")
count += 1
for year in finance_years:
dt = self.__find_financial_indicator(row[1], year)
data += dt
#각 종목별 호출속도를 조절하기 위한 sleep
time.sleep(0.3)
# 각 종목별 데이터가 들어있는 2차원 배열의 데이터프레임화
# extract.py의 클래스의 클래스변수 로 설정했던
df_financial = pd.DataFrame(data, columns=self.financial_column_header)
#팩터데이터 계산
df_financial = self.__calculate_indicator(df_financial)
맨 마지막줄의 호출정보처럼, 팩터데이터를 계산할 메소드를 구현해줍니다.
사실 모든 데이터를 정확하게 계산하기에는 지금 조회방법으로 공시데이터에서 얻는 방법으로는 조금 부족합니다만, 그래도 스크리닝에는 큰 도움이 될 것입니다.
계산 준비
def __calculate_indicator(self, df):
df.sort_values(by=['종목코드', '연도'], inplace=True)
#디버깅용 프린트문
print(df)
# 분기별 PER
df['PER_quarterly'] = np.nan
# 분기별 PBR
df['PBR_quarterly'] = np.nan
df['PSR'] = np.nan
df['GP/A'] = np.nan
df['POR'] = np.nan
df['PCR'] = np.nan
df['PFCR'] = np.nan
df['NCAV/MC'] = np.nan
status = ['영업이익 상태', '매출액 상태', '당기순이익 상태']
three_indicators = ['영업이익', '매출액', '당기순이익']
일단 종목코드 와 연도 기준으로 오름차순 정렬을 해줍니다.
그 후에 새롭게 계산할 팩트 데이터들을 위한 컬럼을 생성하고, nan
값으로 채워줍니다.
그 다음으로는 별도의 컬럼 정보들을 리스트로 생성해줍니다. 별도의 리스트로 생성한 것은 반복문에서 사용하기 편하기 위해서입니다.
df_temp = pd.DataFrame(columns=df.columns)
corp_ticker = df.loc[:, ["종목코드"]].drop_duplicates().values.tolist()
이후에 계산용으로 쓰일 임시 데이터프레임 df_temp
라는 녀석을 생성하고, 기존에 재무정보를 추출했던 데이터프레임 변수의 컬럼과 똑같은 컬럼을 생성하도록 합니다.corp_ticker
도 배열로 편하게 쓰기 위해, 기존에 재무정보를 추출했던 데이터프레임에서 종목코드만 추출합니다. 이때, 재무정보를 최근 3개년의 분기별 데이터를 모두 추출했으므로, 중복된 종목코드가 데이터상에 남아있는데, 이것들의 중복된 값들을 지워주고, 해당 데이터셀의 값만 리스트로 변환해줍니다.
for row in corp_ticker:
if row is None:
continue
# 진행상황 확인용 프린트문
print(f"Calculating {row[0]} factor indicators")
df_finance = df[df["종목코드"] == row[0]].reset_index()
for i in range(3, len(df_finance)):
일단 종목들을 순차적으로 도는데, 기존에 재무정보를 추출했던 데이터프레임에서, 혹시나 비어있는 데이터행이 있을 수 있어서, 일단 로우가 None값일때는 다음 종목을 검색하고, 그렇지 않고 데이터가 잘 들어있는 경우에는 계산을 시작합니다.
계산하기 위한 종목의 3개년 데이터을 전부 가져와 df_finance 라는 데이터프레임으로 다시 저장했습니다.
데이터를 계산하는 기준은 네이버증권등을 비롯한 정보조회 사이트나, 계산하는 사람마다 조금의 차이가 있으나 많은 경우의 최근 4개분기의 데이터를 더한값으로 분기데이터를 생성하고 있습니다. 즉, 가장 과거 데이터 3개분기의 팩터데이터는 이 코드에서는 생성이 안됩니다. (for문이 3부터 시작하는 이유입니다. 생성할 수 있는 기반이 되어줄 과거 데이터가 없음)
아래에서부터 설명하는 각각의 팩터데이터에 대한 자세한 설명은 생략합니다.. 저도 어렴풋하게만 알고있는 정도라서..
PER, Price Earning Ratio
주가수익비율입니다. 주가를 주당순이익으로 나는 지표입니다.
이미 pykrx 라이브러리를 이용해서 조회일의 종가기준 주가를 이용한 PER를 계산하지만, 각 분기별 공시자료 데이터를 이용하여 다시 분기별 데이터를 계산합니다.
# PER : 시가총액 / 당기 순이익
df_finance.loc[i, "PER_quarterly"] = df_finance.iloc[i]['시가총액'] / (
df_finance.iloc[i - 3]['당기순이익'] + df_finance.iloc[i - 2]['당기순이익'] +
df_finance.iloc[i - 1]['당기순이익'] + df_finance.iloc[i]['당기순이익'])
시가총액을 해당분기 순이익으로 나누어, 분기별 지표를 계산합니다.
단, 당기 순이익의 경우에는 최근4개분기 순이익을 다 더한값으로 나누고 있습니다.
PSR, Price Selling Ratio
주가 매출비율입니다.
# PSR : 시가총액 / 매출액
df_finance.loc[i, "PSR"] = df_finance.iloc[i]['시가총액'] / (
df_finance.iloc[i - 3]['매출액'] + df_finance.iloc[i - 2]['매출액'] +
df_finance.iloc[i - 1]['매출액'] + df_finance.iloc[i]['매출액'])
POR, Price Operating Earnings
시가총액/영업이익입니다.
# POR : 시가총액 / 영업이익
df_finance.loc[i, "POR"] = df_finance.iloc[i]['시가총액'] / (
df_finance.iloc[i - 3]['영업이익'] + df_finance.iloc[i - 2]['영업이익'] +
df_finance.iloc[i - 1]['영업이익'] + df_finance.iloc[i]['영업이익'])
PCR, Price per Cashflow Ratio
주당 현금흐름입니다. 영업활동 현금흐름은, 법인세를 제한 기업의 영업매출을 의미합니다.
# PCR : 시가총액 / 영업활동 현금흐름
df_finance.loc[i, "PCR"] = df_finance.iloc[i]['시가총액'] / (
df_finance.iloc[i - 3]['영업활동현금흐름'] + df_finance.iloc[i - 2]['영업활동현금흐름'] +
df_finance.iloc[i - 1]['영업활동현금흐름'] + df_finance.iloc[i]['영업활동현금흐름'])
PFCR, Price per Free Cashflow Raito
주당 잉여현금흐름입니다. 잉여현금흐름은 현금흐름에서 감가상각비, 각종 영업비용을 제하고 순수하게 남은 현금을 뜻합니다.
# PFCR : 시가총액 / 잉여현금 흐름
df_finance.loc[i, "PFCR"] = df_finance.iloc[i]['시가총액'] / (
df_finance.iloc[i - 3]['잉여현금흐름'] + df_finance.iloc[i - 2]['잉여현금흐름'] +
df_finance.iloc[i - 1]['잉여현금흐름'] + df_finance.iloc[i]['잉여현금흐름'])
다음은 직전 3개분기 데이터가 필요하지 않은, 각 분기별 데이터의 계산입니다.
반복문의 밖으로 나와 작성합니다.
PBR, Price Book value Ratio
주당 순자산비율입니다.
# PBR : 시가총액 / 자본총계
df_finance["PBR_quarterly"] = df_finance['시가총액'] / df_finance['자본총계']
GP/A, Gross Profit per Asset
매출총이익을 자산총계로 나눈 값입니다.
# GP/A : 최근 분기 매출총이익 / 자산총계
df_finance["GP/A"] = df_finance['매출총이익'] / df_finance['자산총계']
NCAV/MC, Net Current Asset Value per MarketCap
청산가치 / 시가총액입니다. 청산가치란, 유동자산에서 총 부채를 뺀 금액으로, 만약 회사가 파산한다면, 회사의 자산에서 부채로 갚아야할 것들을 정리하고 남은 금액을 말합니다. 이것을 다시 시가총액으로 나눈다면, 즉 회사가 파산할 경우 주주가 주당 돌려받을 수 있는 청산가치를 뜻합니다 (아마도...?)
# NCAV/MK : 청산가치(유동자산 - 부채총계) / 시가총액
# 퍼센트로 계산하기 위해 100을 곱했음.
df_finance["NCAV/MC"] = (df_finance['유동자산'] - df_finance['부채총계']) / \
df_finance['시가총액'] * 100
부채비율
총 부채를 총 자본으로 나눈 후에 100을 곱해서, 부채 비율이 어느정도 되는지를 계산합니다.
## 부채 비율
df_finance['부채비율'] = (df_finance['부채총계'] / df_finance['자본총계']) * 100
영업이익, 매출액, 당기순이익의 증가율 계산
각 종목의 재무정보를 읽어들일 때 영업이익, 매출액, 당기순이익을 불러왔습니다. 이전 분기에서 현재 분기로 얼마나 증가를 했는지 계산합니다.
###영업이익 / 매출액 / 당기순이익 증가율
df_finance['영업이익 증가율'] = (df_finance['영업이익'].diff() / df_finance['영업이익'].shift(1)).fillna(
0) * 100
df_finance['매출액 증가율'] = (df_finance['매출액'].diff() / df_finance['매출액'].shift(1)).fillna(0) * 100
df_finance['당기순이익 증가율'] = (df_finance['당기순이익'].diff() / df_finance['당기순이익'].shift(1)).fillna(
0) * 100
현재 row를 기준으로, 그 이전값과 diff를 이용하여 뺄셈을 하고, 분모로는 이전 row값을 shift(1)
로 조회합니다. 단 n/a거나 값이 없을땐 0으로 채우고, 퍼센테이지 계산을 위해 100을 곱합니다.
이렇게 하면 직전분기대비 얼마나 증가했는지 알 수 있습니다.
이 경우에도 직전분기 데이터가 필요하므로, 즉 가장 예전의 데이터는 비교할 데이터군이 없으므로 0이 출력될 것입니다.
각종 지표를 계산한 다음, 다시 연도를 정렬합니다. 이번에는 내림차순으로 정렬하여 가장 최근 분기가 해당 종목의 제일 상단에 올라오게끔 합니다. 이렇게 하는 이유는 어차피 가장 최근분기의 데이터가 각 종목별로 최상단에 위치하게끔 하고자함도 있었고, 영업이익/매출액/당기순이익의 상태를 확인하기 위한 계산이 이렇게 먼저 정렬하고 계산하는 것이 더 알기 쉬웠기 때문입니다.
df_finance.sort_values(by=['연도'], inplace=True, ascending=False)
영업이익, 매출액, 당기순이익의 전환지표 확인하기
우리는 재무정보를 불러오는 과정에서 영업이익, 매출액, 당기순이익을 추출하였는데, 전분기 대비하여 각각의 상태들이 계속 지속중인지, 아니면 상태가 전환되었는지 알 수 있으면 더욱 좋을 것입니다.
요 계산도 다른 어느분의 블로그에서 참조했습니다. 복 받으세요 ㅜㅜ
## 영업이익, 매출액, 당기순이익 확인 지표
for i in range(len(status)):
df_finance[status[i]] = np.nan
df_finance.loc[
(df_finance[three_indicators[i]] > 0) & (df_finance[three_indicators[i]].shift(-1) <= 0),
status[i]
] = "흑자 전환"
df_finance.loc[
(df_finance[three_indicators[i]] <= 0) & (df_finance[three_indicators[i]].shift(-1) > 0),
status[i]
] = "적자 전환"
df_finance.loc[
(df_finance[three_indicators[i]] > 0) & (df_finance[three_indicators[i]].shift(-1) > 0),
status[i]
] = "흑자 지속"
df_finance.loc[
(df_finance[three_indicators[i]] <= 0) & (df_finance[three_indicators[i]].shift(-1) <= 0),
status[i]
] = "적자 지속"
이 상태값을 통해서 과거 분기들의 데이터와 비교해가며 어느정도 흑자지속중인지, 적자지속중인지, 그 폭은 감소하고 있는지 등을 증가율과 더불어 확인할 수 있을 것입니다.
데이터 합친 후 반환하기
이렇게 각 종목별로, 최근 3개년의 각 분기데이터들의 팩터데이터들을 계산한 다음, concat을 이용해 종목들의 데이터를 차곡차곡 붙여나갑니다.
이후 좀 더 보기쉬운 순서로 컬럼의 순서를 변환환 하여 원래 호출되었던 메소드로 리턴합니다.
## 기존 데이터프레임 하단에 종목별로 정제데이터들을 붙이기.
df_temp = pd.concat([df_finance, df_temp])
### reindexing columns and return
return df_temp.reindex(
columns=['종목코드', '연도', '시가총액', 'PER_quarterly', 'PBR_quarterly', 'PSR', 'GP/A', 'POR', 'PCR', 'PFCR',
'NCAV/MC']
+ self.indicators
+ ['부채비율', '영업이익 증가율', status[0], '매출액 증가율', status[1], '당기순이익 증가율', status[2]]
)
다시 맨 처음에 팩터데이터 계산 메소드를 호출했던, extract_finacne_data
메소드로 돌아간다면, 아래와 같은 형태가 됩니다.
def extract_finance_data(self, finance_years, df):
#디버깅을 위한 설정
pd.set_option('display.max_columns', None)
pd.options.display.float_format = '{:.2f}'.format
data = []
count = 1
for row in df.itertuples():
#터미널 상의 추출상황 로깅을 위한 프린트문 현재갯수/전체종목갯수, 종목명
print(f"extracting {count}/{len(df)} {row[2]}...")
count += 1
for year in finance_years:
dt = self.__find_financial_indicator(row[1], year)
data += dt
#각 종목별 호출속도를 조절하기 위한 sleep
time.sleep(0.3)
# 각 종목별 데이터가 들어있는 2차원 배열의 데이터프레임화
# extract.py의 클래스의 클래스변수 로 설정했던
df_financial = pd.DataFrame(data, columns=self.financial_column_header)
#팩터데이터 계산
df_financial = self.__calculate_indicator(df_financial)
# 진행상황 확인용 프린트문
print("Join Data------------")
return pd.merge(df, df_financial, left_on="종목코드", right_on="종목코드", how="outer")
이렇게 하나의 메소드가 완성됩니다. __calculate_indicator
를 이용해 추출해낸 팩터데이터의 데이터프레임과, pykrx에서 얻어온 데이터를 하나로 합쳐줍니다.
pd.merge(df, df_financial, left_on="종목코드", right_on="종목코드", how="outer")
이 때, how="outer"
로 지정했는데, pykrx에서 얻어온 데이터 (df
) 에는 각 종목별로 한 행의 데이터밖에 없지만, 최근 3개년의 분기별 재무정보와 팩터데이터가 담긴 데이터 (df_financial
) 에는 하나의 종목정보가 여러행이 존재합니다. 이때 outer 조인을 하게 되면, df
데이터에서는 종목코드 기준으로 부족한 행은 자동으로 기존 데이터가 채워져 하나로 합쳐지게 됩니다. (하나의 행을 부족한 행수만큼 복사해서 채워짐)
이제 추출을 시도해봅니다.
이 모든 흐름을 관리하는 main.py
는 아래와 같은 상황이 될 것 입니다.
from extract_data.extract import Extract
from export_data import ExportToData
extractor = Extract()
exporter = ExportToData()
# Pykrx와 FinanceDataReader를 이용해 기본종목정보 불러오기
kospi_kosdaq_data = extractor.get_data()
#최근 3개년 데이터의 재무정보와 팩터데이터를 계산하기
extracted_data = extractor.extract_finance_data(
[2020, 2021, 2022],
kospi_kosdaq_data.copy()
)
#엑셀로 내보내기
exporter.export_to_excel("/{path}/test.xlsx", extracted_data)
#최근 3개년 데이터의 재무정보와 팩터데이터를 계산하기 extracted_data = extractor.extract_finance_data( [2020, 2021, 2022], kospi_kosdaq_data.copy() )
위와 같이 기본 종목을 추출한 kospi_kosdaq_data
변수를 copy 하는 이유는, 추후 액셀 파일로 작성할때 추출한 원본데이터를 참조용으로 보존하고자하는데, 이때 각종 연산으로 인해서 추출데이터가 변형되는 것을 막기 위해, 재무정보 및 팩터데이터를 추출할때는 카피본으로 넘겨줍니다.
여기서, 이전 포스트의 말미에서 말씀드린 것의 문제 가 발생합니다.
코스피, 코스닥의 종목수가 많기 때문에, 데이터베이스를 쓰지 않는한 일일 호출횟수 제한에 걸려서 모든 종목을 확인하기가 어렵다는 것입니다.
때문이 일정한 조건으로 필터링을 한 다음에, 필터링된 데이터들을 기반으로 재무정보를 불러와야합니다.
기본 필터 만들기
저는 아래와 같이 파이썬 파일을 만들고, 필터를 정의했습니다.
/filter_data/filter_by_condition.py
특정 조건의 주식들 솎아내기
일단 몇가지 주식들을 걸러내는 조건을 만들었습니다.
def drop_column(df: pd.DataFrame):
# 스팩 주식 드랍
df.drop(
df[df["종목명"].str.contains("스팩")].index,
inplace=True
)
# 우선주 드랍
df.drop(
df[df["종목명"].str.endswith(("우", "우B", "우C"))].index,
inplace=True
)
# 지주사 드랍
df.drop(
df[df["종목명"].str.endswith(("홀딩스", "지주", "지주회사"))].index,
inplace=True
)
# 직전 거래일의 거래량이 0인 경우는 어떠한 이유에서 거래정지가 되어있을 확률이 높음
df = df[df["거래량"] > 0]
return df
다른 표현들과 거래량 조건식이 다른 이유는,
df.drop(
df[df["거래량"] == 0].index,
inplace=True
)
으로 했을 때 디버깅 결과 몇몇 종목이 포함이 안되는 것을 확인하였기 때문에, 동작내용은 조금 다르지만 얻고하는 결과값은 같은 다른 표현으로 작성해봤습니다.
왜 결과가 다른지는 잘 모르겠네요 ㅜㅜ
시가총액 하위 30퍼센트 이하의 종목으로 필터링하기
저의 전략으로는 일단 시가총액 하위 30퍼센트의 종목을 걸러내어서, (약 500~600여개 필터링) 거기서 종목을 발굴해보자는 전략으로 정했습니다.
(소형주 전략)
def filtering_data_that_market_cap_under_thirty_percent(data: pd.DataFrame):
"""
코스피, 코스닥의 종목에서 시가총액 30%이하의 종목으로 필터함.
이 때, 기업소재지가 외국, 스팩주, 우선주, 최신거래일에 거래량이 0인 종목은 제거함.
:param data:
:return: DataFrame
"""
data = drop_column(data)
return data[data["시가총액"] <= data["시가총액"].quantile(q=0.3)].sort_values(by=["시가총액"], ascending=True)
즉, pykrx와 FinaceDataReader로 읽어들인 기본적인 종목정보들에 포함된, 시가총액을 기준으로 30퍼센트 이하의 종목들만 추려내는 것입니다.
이 필터를 이용해서 main.py
를 정의하면 아래와 같이 됩니다.
import filter_data
from extract_data.extract import Extract
from export_data import ExportToData
extractor = Extract()
exporter = ExportToData()
# Pykrx와 FinanceDataReader를 이용해 기본종목정보 불러오기
kospi_kosdaq_data = extractor.get_data()
#최근 3개년 데이터의 재무정보와 팩터데이터를 계산하기
extracted_data = extractor.extract_finance_data(
[2020, 2021, 2022],
filtering_data_that_market_cap_under_thirty_percent(kospi_kosdaq_data.copy())
)
#엑셀로 내보내기
exporter.export_to_excel("/{path}/test.xlsx", extracted_data)
이렇게 하면 기본 종목데이터를 기반으로 시가총액 하위 30퍼센트의 기업만(약 500여개)을 대상으로 재무정보와 팩터데이터를 계산할 수 있게 되어, OpenDart의 일일 호출제한에 아슬아슬하게 세이프할 수 있게 되었습니다.
추출 결과
이 후에 main.py
를 실행시키면, 프린트문을 통해 진행상황을 알 수 있고, 약 20여분 후에 이렇게 빈 셀이 숭숭숭(?) 있는 데이터가 완성되었습니다.
다음 포스트에서는 왜 빈 셀이 있는지와, 특정 종목을 대상으로 데이터 검증작업을 해봅니다.
ref
- https://wikidocs.net/156786
- https://wikidocs.net/153307
- https://wikidocs.net/158146
- https://gils-lab.tistory.com/24?category=504500
- https://gils-lab.tistory.com/38?category=504501
- https://sjblog1.tistory.com/41
- https://dragon1-honey1-wayfarer.tistory.com/entry/OpenDartReader-로-종목을-분류해보자-2?category=892039
- https://dragon1-honey1-wayfarer.tistory.com/entry/OpenDartReader-로-종목을-분류해보자-3
- https://dragon1-honey1-wayfarer.tistory.com/entry/OpenDartReader-로-종목을-분류해보자-4-근데-pykrx를-곁들인?category=892039
- https://blog.naver.com/mnman2/222075646712
- https://blog.naver.com/rkdwlsgursla/222560944332
- https://blog.naver.com/piersn/222618921017?isInf=true
- https://blog.naver.com/yeojh1/222080532433
- https://steadiness-193.tistory.com/215
- https://www.dinolabs.ai/97
- https://kongdols-room.tistory.com/123
- https://2030bigdata.tistory.com/183
'파이썬으로 종목 스크리너 만들기' 카테고리의 다른 글
뇌동매매 금지 - 6. 스크리닝 결과를 엑셀로 저장할 메소드 만들기 (0) | 2022.09.12 |
---|---|
뇌동매매 금지 - 5. 추출하고 계산한 분기별 데이터 검증해보기 (0) | 2022.09.12 |
뇌동매매 금지 - 3. 분기별 재무정보 조회해보기 (0) | 2022.09.12 |
뇌동매매 금지 - 2. 종목정보 가져오기 (2) | 2022.09.12 |
뇌동매매 금지 - 1. 파이썬 종목 스크리닝을 위한 준비 (0) | 2022.09.12 |
댓글