반응형

PyPDF2 맛보기

가끔 pdf파일에서 텍스트를 추출할 일이 있다.
그때 활용할 수 있도록 Python언어로 pdf파일에서 텍스트를 읽어오는 방법에 대해 예제 코드와 함께 알아본다.

 

1. 필요한 라이브러리

PDF파일을 읽어들이기 위해 PyPDF2 라이브러리를 설치한다.

!pip install PyPDF2

2. 예시 코드

공개된 사이트에서 PDF파일을 갖고 온다. 
이번에는 국토교통부 홈에 있는 보도자료에 바로 올라온 '주택건설사업 인허가 속도 높인다'라는 제목의 파일을 읽어들일 것이다. 쉽게 설명하기 위해 주 디렉토리에 'sample.pdf'라는 파일명으로 다운받아 저장했다.

# 라이브러리를 읽어들인다.
from PyPDF2 import PdfReader

# PDF파일을 읽어서 reader라는 변수에 저장한다.
reader = PdfReader("sample.pdf")

파일을 읽어들인 후에는 전체 페이지가 몇 페이지인지 알아보고 전체 페이지에서 다음과 같이 텍스트를 추출하도록 한다.

반응형
pages = reader.pages

text = ""

for page in pages:
    sub = page.extract_text()
    text += sub
    
# 텍스트를 전부 잘 추출했는지 확인한다.    
print(text)

위 예시 파일에서 텍스트를 잘 읽어들인 것을 확인할 수 있다.

PyPDF의 좀 더 복잡한 기능은 '사용자 가이드'를 참고하면 많은 도움이 될 것이다.

 

반응형
반응형

FRED에서 제공하는 API를 통해 데이터를 받아 사용하면 FRED의 데이터 소스에 변화가 있더라도 바로 바로 대응이 가능하겠지만, 그 정도로 데이터를 보는 사람이라면 이 정보가 필요없을 것이다.

나처럼 쉽게 경기선행 지표를 확인하고자 하는 사람들이 참고했으면 좋겠다.

먼저 관련 라이브러리를 설정하고 읽어들인다.

# matplotlib 설정(그래프 그리기)
import matplotlib.pyplot as plt

plt.rcParams['axes.grid'] = True
plt.rcParams['figure.figsize'] = (12,6)
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['axes.formatter.limits'] = -10000, 10000

import FinanceDataReader as fdr

그럼, 이제부터 나스닥지수의 흐름을 보는 데 도움이 되는 대표적인 선행 지표들을 함께 살펴보도록 한다.

1. 주간 실업수당 청구건수(ICSA)

경기가 나빠지는 경우 후행지표로 동작하지만, 경기가 좋아지는 경우 선행 지표로 활용할 수 있다고 한다. 연속 실업수당 청구건수는 CCSA이다.

df = fdr.DataReader('FRED:NASDAQCOM,ICSA','2010-01-01')
ax = df.plot(secondary_y='NASDAQCOM')
ax = ax.axvspan('2019-12-01', '2023-10-25', color='gray', alpha=0.2)

팬데믹 기간에 폭발적으로 증가하다가 최근에는 과거 평균으로 돌아간 상태이다.

2. 소비자 심리지수(UMCSENT)

UMCSENT는 미시건대 소비자 심리지수 추이로 소비자 설문 결과를 수치화한 값이다. 100을 기준으로 100이상이면 긍정적이고 100이하면 부정적으로 본다.

df = fdr.DataReader('FRED:NASDAQCOM,UMCSENT','2010-01-01')
ax = df.plot(secondary_y='NASDAQCOM')
ax = ax.axvspan('2019-12-01', '2023-10-25', color='gray', alpha=0.2)

팬데믹 때 급락한 후 일시 회복하다가 재차 하락한 후 조금씩 회복하고 있다. 아직 70대 수준으로 부정적인 상태이다.

반응형

3. 주택 판매지수(HSN1F)

주택 판매지수는 주택시장 활성화 정도를 나타내는 지수이다. 주가 하락에 앞서 주택 판매지수가 선행하는 특징을 보인다.

df = fdr.DataReader('FRED:NASDAQCOM,HSN1F','2015-01-01')
ax = df.plot(secondary_y='NASDAQCOM')
ax = ax.axvspan('2019-12-01', '2023-10-25', color='gray', alpha=0.2)

2020년부터 현재까지 나스닥지수와 주택 판매지수 추이를 보면, 2020년에는 주택 판매지수가 더 높게 나오다가 2021년 주택 판매지수가 먼저 하락한 것을 볼 수 있다. 그 후 2022년부터 나스닥지수도 하락하고 있다. 

4. 실업률(UNRATE)

실업률은 불황에는 후행지표, 경기회복에는 동행지표로 동작한다고 한다.

df = fdr.DataReader('FRED:NASDAQCOM,UNRATE','2015-01-01')
ax = df.plot(secondary_y='NASDAQCOM')
ax = ax.axvspan('2019-12-01', '2023-10-25', color='gray', alpha=0.2)

실업률은 팬데믹 때 급격히 증가했다가 지속적으로 낮아져 평년 수준으로 돌아와 있는 상태이다.

5. M2 통화량(M2SL)

M2 통화량은 시중에 돈이 얼마나 많이 풀려있는지 보여주는 유동성 지표이다.

df = fdr.DataReader('FRED:NASDAQCOM,M2SL','2015-01-01')
ax = df.plot(secondary_y='NASDAQCOM')
ax = ax.axvspan('2019-12-01', '2023-10-25', color='gray', alpha=0.2)

6. 하이일드 채권 스프레드(BAMLH0A0HYM2)

하이일드 채권은 낮은 신용등급 회사에 투자하는 고위험, 고수익 채권이다. 여기서 스프레드는 국채 수익률과의 차이를 말한다. 불황일수록 하이일드 채권 스프레드는 더 커진다.

df = fdr.DataReader('FRED:NASDAQCOM,BAMLH0A0HYM2','2015-01-01')
ax = df.plot(secondary_y='NASDAQCOM')
ax = ax.axvspan('2019-12-01', '2023-10-25', color='gray', alpha=0.2)

참고 사이트)

FinanceDataReader 깃헙

 

Jupyter Notebook Viewer

선행지표란 경기가 좋아질 것인지 혹은 나빠질 것인지 앞서 반응하는 지표를 말합니다. 대표적인 선행 지표들이 무엇이 있는지 그리고, 이 선행지표들을 살펴봄으로 향후 주식 시장의 흐름을

nbviewer.org

FRED : https://fred.stlouisfed.org/

 

Federal Reserve Economic Data | FRED | St. Louis Fed

Welcome to FRED, your trusted source for economic data since 1991. Download, graph, and track 823,000 US and international time series from 114 sources. Welcome to FRED, your trusted source for economic data since 1991. Download, graph, and track 823,000 U

fred.stlouisfed.org

 

반응형
반응형

절대 모멘텀 전략은 단일 종목에도 백테스트를 할 수 있다. 그러나 상대 모멘텀 전략은 다수 종목으로 투자 대상군을 형성해야 이용할 수 있다. 

상대 모멘텀 전략에서는 모멘텀 지수를 계산하기 위해 과거 1개월 종가의 수익률을 계산한다. 지난달 마지막 일자를 기준으로 전체 투자 대상 종목에서 상대적으로 높은 수익률을 보인 상위 종목에 매수 신호를 발생시킨다. 

먼저 사용할 라이브러리들을 읽어들인다.

import pandas as pd 
import numpy as np
import FinanceDataReader as fdr

그 다음 파이낸스데이타리더 라이브러리를 사용해 주식 데이터를 읽어들인다.

# 주식을 먼저 선정한다. 여기서는 10개의 종목을 선정한다. 
# 애플 AAPL, 아마존 AMZN, 메타 META, 월마트 WMT, 넷플릭스 NFLX, 제너럴 모터스 GM, 마이크로소프트MSFT, 테슬라 TSLA, 엔비디아 NVDA, 코카콜라 KO
# 주식 기간은 2015.1.1~2023.08.31
ticker = ['AAPL', 'AMZN', 'META', 'WMT', 'NFLX', 'GM', 'MSFT', 'TSLA', 'NVDA', 'KO']
all_df = pd.DataFrame()
for f in ticker: 
    temp = fdr.DataReader(f, '2015-01-01', '2023-08-31')
    temp['CODE'] = f
    temp.reset_index(inplace = True)
    all_df = pd.concat([all_df, temp], axis = 0, ignore_index = True) #pandas 2.0이상에서는 append 함수가 없어지고, concat함수를 사용해야 함
all_df.head()

읽어드린 주식 데이터에서 필요한 컬럼만 추출해 사용할 수 있도록 데이터를 전처리하는 함수를 아래처럼 만든다.

def data_preprocessing(sample, t_code, base_date):
    sample = sample[sample['Date'] >= base_date][['Date','CODE','Adj Close']].copy()
    sample.reset_index(inplace = True, drop = True)
    sample['STD_YM'] = sample['Date'].dt.to_period(freq='M') 
    sample['1M_RET'] = 0.0
    ym_keys = list(sample['STD_YM'].unique())
    return sample, ym_keys

data_preprocessing함수로 데이터를 전처리해서 price_df, ym_keys 변수에 저장한다. 
이때 기준 날짜를 정할 수 있다. 이번에는 2019-01-01로 기준 날짜를 정했다.

price_df, ym_keys = data_preprocessing(all_df, ticker, base_date ='2019-01-01')
price_df.head()

월별 데이터를 저장할 month_last_df라는 데이터프레임을 만들고, 각 주식별 1개월간 수익률을 계산하여 '1M_RET'이라는 컬럼에 넣도록 한다.

month_last_df = pd.DataFrame(columns = ['Date', 'CODE', '1M_RET'])

for f in ticker: 
    temp_df = price_df[price_df['CODE'] == f][['Date','CODE','STD_YM','Adj Close','1M_RET']].copy()
    for ym in ym_keys:
        m_ret = temp_df.loc[temp_df[temp_df['STD_YM'] == ym].index[-1],'Adj Close'] \
        / temp_df.loc[temp_df[temp_df['STD_YM'] == ym].index[0],'Adj Close']
        temp_df.loc[temp_df['STD_YM'] == ym, ['1M_RET']] = m_ret
        month_last_df = month_last_df._append(temp_df.loc[temp_df[temp_df['STD_YM'] == ym].index[-1], ['Date', 'CODE', '1M_RET']])

그 다음으로 종목별 종가를 저장할 stock_df라는 데이터프레임도 만들도록 한다.

stock_df = pd.DataFrame(columns = ['Date', 'CODE', 'Adj Close'])
stock_df = pd.concat([stock_df, price_df.loc[:,['Date', 'CODE', 'Adj Close']]], sort=False)
stock_df.sort_values(by = 'Date', ascending = True).head(10)

월별 수익률이 행으로 쌓여있는 데이터를 피벗해 일자별로 종목별 수익률 데이터로 만든다. 종목 코드를 컬럼으로 올리고, DataFrame의 rank()함수를 사용해 월말 수익률의 순위를 퍼센트 순위로 계산하고 상위 40% 종목만 선별한 다음 나머지 값에는 np.nan값을 채운다.

month_ret_df = month_last_df.pivot_table(index='Date', columns='CODE', values='1M_RET').copy()
month_ret_df = month_ret_df.rank(axis=1, ascending=False, method='max', pct=True)
month_ret_df = month_ret_df.where(month_ret_df < 0.4, np.nan)
month_ret_df.fillna(0, inplace=True)
month_ret_df[month_ret_df !=0] = 1
stock_codes = list(stock_df['CODE'].unique())

month_ret_df.head()

월말일자로 1로 표시된 종목코드와 0으로 표시된 종목코드를 확인할 수 있다. 다음 날짜로 넘어갈 때 0에서 1로 되면 제로 포지션에서 매수 포지션으로 변경되고 1에서 0으로 변경되면 청산한다. 1에서 1로 변함없는 종목은 매수후 보유상태를 유지한다.

반응형

다음에 진행할 순서는 월말에 거래 신호가 나타난 종목을 대상으로 포지셔닝을 처리하는 것이다.

sig_dict = dict()
# 신호가 포착된 종목 코드만 읽어오기
for date in month_ret_df.index: 
    ticker_list = list(month_ret_df.loc[date, month_ret_df.loc[date,:] >= 1.0].index)
    sig_dict[date] = ticker_list
stock_c_matrix = stock_df.pivot_table(index='Date', columns='CODE', values='Adj Close').copy()
book = create_trade_book(stock_c_matrix, list(stock_df['CODE'].unique()))

for date, values in sig_dict.items(): # 포지셔닝
    for stock in values:
        book.loc[date,'p '+ stock] = 'ready ' + stock
        
book = tradings(book, stock_codes) # 트레이딩

신호가 발생한 종목 리스트를 만들고 stock_df 변수를 피벗해 만든 stock_c_matrix 변수를 전달해 새로운 거래 장부 역할 변수를 만든다. 

거래 장부 역할 변수는 다음의 코드로 작성된 create_trade_book() 함수로 만들어진다. 

그 다음 for 문을 사용해 월초 일자별 신호가 발생한 종목에 포지션을 기록한다. 이렇게 포지셔닝을 해놓고 두번째로 만든 tradings() 함수를 통해 트레이딩을 진행하게 된다.

- create_trade_book() 함수

def create_trade_book(sample, sample_codes):
    book = pd.DataFrame()
    book = sample[sample_codes].copy()
    book['STD_YM'] = book.index.to_period(freq='M')
    
    for c in sample_codes:
        book['p '+c] = ''
        book['r '+c] = ''
    return book

- tradings() 함수

def tradings(book, s_codes):
    std_ym = ''
    buy_phase = False
    for s in s_codes:
        print(s)
        
        for i in book.index:
            if book.loc[i, 'p '+ s] == '' and book.shift(1).loc[i, 'p '+s] == 'ready ' +s:
                std_ym = book.loc[i, 'STD_YM']
                buy_phase = True
                
            if book.loc[i, 'p '+ s] == '' and book.loc[i, 'STD_YM'] == std_ym and buy_phase == True:
                book.loc[i, 'p '+ s] = 'buy ' + s
            if book.loc[i, 'p '+ s] == '':
                std_ym = None
                buy_phase = False
    return book

마지막으로 거래 장부 book에 있는 거래를 가지고 상대 모멘텀 전략의 거래 수익률을 계산할 multi_returns() 함수를 아래와 같이 만든다.

def multi_returns(book, s_codes):
    # 손익 계산
    rtn = 1.0
    buy_dict = {}
    num = len(s_codes)
    sell_dict = {}
    
    for i in book.index:
        for s in s_codes:
            if book.loc[i, 'p ' + s] == 'buy '+ s and \
            book.shift(1).loc[i, 'p '+s] == 'ready '+s and \
            book.shift(2).loc[i, 'p '+s] == '' :     # long 진입
                buy_dict[s] = book.loc[i, s]
#                 print('진입일 : ',i, '종목코드 : ',s ,' long 진입가격 : ', buy_dict[s])
            elif book.loc[i, 'p '+ s] == '' and book.shift(1).loc[i, 'p '+s] == 'buy '+ s:     # long 청산
                sell_dict[s] = book.loc[i, s]
                # 손익 계산
                rtn = (sell_dict[s] / buy_dict[s]) -1
                book.loc[i, 'r '+s] = rtn
                print('개별 청산일 : ',i,' 종목코드 : ', s , 'long 진입가격 : ', buy_dict[s], ' |  long 청산가격 : ',\
                      sell_dict[s],' | return:', round(rtn * 100, 2),'%') # 수익률 계산.
            if book.loc[i, 'p '+ s] == '':     # zero position || long 청산.
                buy_dict[s] = 0.0
                sell_dict[s] = 0.0


    acc_rtn = 1.0        
    for i in book.index:
        rtn  = 0.0
        count = 0
        for s in s_codes:
            if book.loc[i, 'p '+ s] == '' and book.shift(1).loc[i,'p '+ s] == 'buy '+ s: 
                # 청산 수익률계산.
                count += 1
                rtn += book.loc[i, 'r '+s]
        if (rtn != 0.0) & (count != 0) :
            acc_rtn *= (rtn /count )  + 1
            print('누적 청산일 : ',i,'청산 종목수 : ',count, \
                  '청산 수익률 : ',round((rtn /count),4),'누적 수익률 : ' ,round(acc_rtn, 4)) # 수익률 계산.
        book.loc[i,'acc_rtn'] = acc_rtn
    print ('누적 수익률 :', round(acc_rtn, 4))

상대 모멘텀 전략의 수익률은 다수 종목을 가지고 계산하므로 단일 종목으로 계산한 절대 모멘텀 전략의 수익률 계산과는 다른 것을 알 수 있다. 하지만, 절대 모멘텀 전략이나 상대 모멘텀 전략 모두 전에 보유하고 있던 포지션 여부를 검사한 후 그에 따라 진입과 청산, 유지를 결정하는 것이라는 기본적인 개념은 동일하다. 

마지막 수익률 계산은 다음 코드로 실행된다.

multi_returns(book, stock_codes) # 수익률 계산

 

<참고 서적> 퀀트 전략을 위한 인공지능 트레이딩

반응형
반응형

듀얼 모멘텀은 투자 자산 가운데 상대적으로 상승 추세가 강한 종목에 투자하는 상대 모멘텀 전략과거 시점 대비 현재 시점의 절대적 상승세를 평가한 절대 모멘텀 전략을 결합해 위험을 관리하는 투자 전략이다. 

  • 모멘텀(momentum)은 물질의 운동량이나 가속도를 의미하는 물리학 용어로, 투자 분야에서는 주가가 한 방향성을 유지하려는 힘을 의미한다.

듀얼 모멘텀 전략은 국내에서도 많은 미디어에 소개되며 대중에게 익숙한 퀀트 투자 전략이기도 하다. 

듀얼 모멘텀 전략을 구현하기 위해서는 먼저 절대 모멘텀과 상대 모멘텀을 이해하고 구현할 수 있어야 한다. 

최근 N개월간 수익률이 양수이면 매수하고 음수이면 공매도하는 전략을 절대 모멘텀 전략이라고 한다.
반면에 상대 모멘텀 전략은 투자 종목군이 10개 종목이라 할 때 10개 종목의 최근 N개월 모멘텀을 계산해 상대적으로 모멘텀이 높은 종목을 매수하고 상대적으로 낮은 종목은 공매도하는 전략이다. 

이번에는 절대 모멘텀 전략을 실행해 보도록 한다.

절대 모멘텀 전략

먼저 필요한 라이브러리들을 가져오고, 분석할 데이터셋을 읽어온다.

import pandas as pd
import numpy as np
import FinanceDataReader as fdr

read_df = fdr.DataReader('SPY', '1993-01-29', '2023-09-12') # S&P 500 ETF 데이터를 가져옴.
read_df.head()

그 다음 조정 종가만 선택해 price_df 변수에 별도의 데이터프레임으로 저장한다.

price_df = read_df.loc[:,['Adj Close']].copy()
price_df.head()

이 전략에서는 월말 데이터를 추가해서 사용한다.

price_df['STD_YM'] = price_df.index.to_period('M')
price_df.head()

이번에는 월말 종가에 접근하는 데이터프레임을 만들어 보자.

month_list = price_df['STD_YM'].unique()
month_last_df = pd.DataFrame()
for m in month_list:
    # 기준 연월에 맞는 인덱스의 마지막 날짜 row를 데이터프레임에 추가
    month_last_df = month_last_df._append(price_df.loc[price_df[price_df['STD_YM'] == m].index[-1], :])

month_last_df.head()

현재 데이터프레임에 있는 중복을 제거한 모든 '연-월' 데이터를 리스트에 저장해 월별 말일자에 해당하는 종가에 쉽게 접근할 수 있다.

월말 종가 데이터를 적재하는 별도의 데이터프레임을 만든다. 월말 종가 데이터 리스트를 순회하면서 index[-1]을 통해 월말에 쉽게 접근하고, 데이터를 추출해 새로 만든 데이터프레임에 적재한다.
(append가 pandas 2.0부터 제거됨. _append로 사용가능)

모멘텀 지수를 계산하기 위한 이전 시점의 데이터를 어떻게 가공하는지 살펴보자.

month_last_df['BF_1M_Adj Close'] = month_last_df.shift(1)['Adj Close']
month_last_df['BF_12M_Adj Close'] = month_last_df.shift(12)['Adj Close']
month_last_df.fillna(0, inplace=True)
month_last_df.head(15)

판다스 DataFrame에서 제공하는 shift()함수를 이용하면 손쉽게 데이터를 가공할 수 있다. shift() 함수는 기본값이 period = 1, axis = 0으로 설정되지만, 입력 매개변수를 원하는 값으로 대체해 사용할 수 있다. 
우리가 만든 데이터프레임에서 1을 지정하면 1개월 전 말일자 종가를 가져오고, 12를 넣으면 12개월 전 말일자 종가 데이터를 가져오게 할 수 있다. 데이터를 뒤로 밀어내는 것인데 초기 n개의 행에는 값이 없으므로 np.nan 값이 출력된다. 

이제 모멘텀 지수를 계산해서 거래가 생길 때 포지션을 기록할 DataFrame을 만든다.

book = price_df.copy()
book['trade'] = ''
book.head()

반응형

최종적으로 기록된 포지션을 바탕으로 최종 수익률을 계산하는 데 사용된다. 
처음에 만든 일별 종가가 저장된 DataFrame을 복사해 날짜 Date 컬럼을 index로 설정하고 거래가 일어난 내역을 저장할 Trade 컬럼을 만든다.

# trading 부분
ticker = 'SPY'
for x in month_last_df.index:
    signal = ''
    # 절대 모멘텀을 계산
    momentum_index = month_last_df.loc[x, 'BF_1M_Adj Close'] / month_last_df.loc[x,'BF_12M_Adj Close']-1
    # 절대 모멘텀 지표 True/False를 판단
    flag = True if ((momentum_index > 0.0) and (momentum_index != np.inf) and (momentum_index != -np.inf)) else False and True
    if flag :
        signal = 'buy ' + ticker # 절대 모멘텀 지표가 Positive이면 매수 후 보유
    print('날짜 : ', x, '모멘텀 인덱스 : ', momentum_index, 'flag : ', flag, 'signal : ', signal)
    book.loc[x:, 'trade'] = signal

월별 인덱스를 순회하면서 12개월 전 종가 대비 1개월 전 종가 수익률이 얼마인지 계산한다. 계산된 수익률은 momentum_index 변수에 저장해 0 이상인지 확인하고, 0 이상이면 모멘텀 현상이 나타난 것으로 판단해 매수 신호가 발생하도록 한다. 이 부분을 flag 변수로 처리했다. 이어서 signal 변수에 저장된 매수 신호를 book 데이터프레임에 저장한다.

월말 종가를 기준으로 매수/매도 신호를 계산하므로 최소 1개월 이상 해당 포지션을 유지한다. 포지션을 유지하는 기간은 개인마다 다르지만 보통 1개월을 유지하기도 한다. 이를 전문 용어로 리밸런스 주기라고 한다.

그 다음으로 전략 수익률을 확인해 보자. 

def returns(book, ticker):
    # 손익 계산
    rtn = 1.0
    book['return'] = 1
    buy = 0.0
    sell = 0.0
    for i in book.index:
        if book.loc[i, 'trade'] == 'buy ' + ticker and book.shift(1).loc[i, 'trade'] == '' :
            # long 진입
            buy = book.loc[i, 'Adj Close']
            print('진입일 : ', i, 'long 진입가격 : ', buy)
        elif book.loc[i, 'trade'] == 'buy' + ticker and book.shift(1).loc[i, 'trade'] == 'buy ' + ticker:
            # 보유중
            current = book.loc[i, 'Adj Close']
            rtn = (current - buy) / buy + 1
            book.loc[i, 'return'] = rtn

        elif book.loc[i, 'trade'] == '' and book.shift(1).loc[i, 'trade'] == 'buy ' + ticker:
            # long 청산
            sell = book.loc[i, 'Adj Close']
            rtn = (sell - buy) / buy + 1 # 손익 계산
            book.loc[i, 'return'] = rtn
            print('청산일 : ', i, 'long 진입가격 : ', buy, ' | long 청산가격 : ', sell, ' | return: ', round(rtn, 4))

        if book.loc[i, 'trade'] == '': # 제로 포지션
            buy = 0.0
            sell = 0.0
            current = 0.0

    acc_rtn = 1.0
    for i in book.index :
        if book.loc[i, 'trade'] == '' and book.shift(1).loc[i, 'trade'] == 'buy ' + ticker:
            # long 청산시
            rtn = book.loc[i, 'return']
            acc_rtn = acc_rtn * rtn # 누적 수익률 계산
            book.loc[i:, 'acc return'] = acc_rtn
    print ('Accumulated return : ', round(acc_rtn, 4))
    return (round(acc_rtn, 4))

SPY ETF의 경우 12개월 전 대비 가격이 올랐을 때 매수하는 절대 모멘텀 전략의 수익률은 다음과 같다.
1에서 시작해 16.4076으로 종료되었으므로 1994년부터 2023년까지 1600%의 수익률을 낸 것이다. 

지금까지 단일 종목을 절대 모멘텀 전략으로 구현해 보았다.
절대 모멘텀 전략은 종목별 자신의 과거 수익률을 기반으로 매수/매도 신호를 포착하므로 단일 종목으로도 구성할 수 있다.

 

<참고 서적> 퀀트 전략을 위한 인공지능 트레이딩

반응형
반응형

퀀트Quant 투자를 달리 표현하면 데이터 기반 data-driven 전략이라고 할 수 있다. 

퀀트는 정량적 방법론을 기반으로 투자 의사를 결정하는 것이며, 정량적 방법론이란 모든 것을 수치화하는 것을 의미한다. 

사용하는 데이터에 따라 주가를 사용해 기술 지표를 만들고 이를 투자에 활용하는 '기술 지표 투자 전략'과 기업 재무제표를 사용하는 '가치 투자 전략', 이렇게 두 가지로 크게 나눌 수 있다. 

주식 시장을 바라보는 트레이더의 시각에 따라 사용되는 지표는 다르다.

기술 지표를 활용한 퀀트 투자 전략 구현에서의 관건은 주가 데이터를 활용해 기술 지표를 만드는 것이다. 
많은 파이썬 라이브러리에서 해당 기능을 제공하지만, 전략 확장성을 위해 수식을 기반으로 직접 만들어보는 것이 더 의미가 있다. 
해당 지표들로 신호를 발생시켜 종목을 매매하고 전략의 수익률이나 승률 등을 살펴보는 방법도 알아본다. 

기술 지표를 활용한 퀀트 투자 방법으로 모멘텀 전략과 평균 회귀 전략을 알아볼 것이다. 평균 회귀와 모멘텀은 서로 상반된 개념이다. 두 전략의 상관관계가 낮고 전략이 초점을 맞추는 시간대도 다르기 때문에 양극에 있는 두 방법을 살펴볼 가치는 충분하다고 볼 수 있다.

가치 투자 전략은 당기순이익, 영업이익, 영업이익률, 매출액, 부채비율, PER, PBR, PSR, PCR, ROE, ROA 등 기업의 가치 판단에 기준이 되는 재무제표 데이터를 기초로 한다. 

가치 투자 전략에서는 기준이 되는 데이터에서 순위를 매겨 분위별로 자르는 작업이 훨씬 더 중요하다. 

하지만 전략 평가는 기술 지표를 활용한 퀀트 투자 방법과 유사하다.

평균 회귀 전략

평균 회귀 (regression to mean)란 결과를 예측할 때 평균에 가까워지려는 경향성을 말한다. 한번 평균보다 큰 값이 나오면 다음번에는 평균보다 작은 값이 나와 전체적으로는 평균 수준을 유지한다는 의미다. 

주식시장에서도 평균 회귀 현상이 통용되는지 검증하려는 많은 시도가 있었다.
이 현상에 의하면, 가격이 평균보다 높아지면 다음번에는 평균보다 낮아질 확률이 높다. 

평균 회귀 속성을 이용한 전략을 들여다보면, 주가의 평균가격을 계산해 일정 범위를 유지하느냐 이탈하느냐에 따라 매매를 결정한다. 현재 주가가 평균가격보다 낮으면 앞으로 주가가 상승할 것으로 기대해 주식을 매수하고, 현재 주가가 평균가격보다 높으면 앞으로 주가가 하락할 것으로 예상해 주식을 매도하는 규칙을 설정할 수 있다. 

볼린저 밴드 Bollinger band

볼린저 밴드는 현재 주가가 상대적으로 높은지 낮은지를 판단할 때 사용하는 보조지표다. 볼린저 밴드는 3개 선으로 구성되는데, 중심선은 이동평균 moving average 선, 상단선과 하단성을 구성하는 표준편차 standard deviation 밴드다.

볼린저 밴드를 사용하는 이유는 이동 평균선으로 추세를 관찰하고 상단선과 하단선으로 밴드 내에서 현재 가격의 상승과 하락폭을 정량적으로 계산할 수 있기 때문이다. 볼린저 밴드는 '추세'와 '변동성'을 분석해주어 기술 분석에서 활용도가 높으며 평균 회귀 현상을 관찰하는 데도 매우 유용하다. 
보통 중심선을 이루는 이동 평균선을 계산할 땐 20일을 사용하고 상하위 밴드는 20일 이동 평균선 ± 2 * 20일 이동 표준 편차(σ)를 사용한다.

그럼 판다스를 사용해 볼린저 밴드 공식을 구현해 보도록 한다.

import pandas as pd
import FinanceDataReader as fdr

df = fdr.DataReader('AAPL', '1997-01-01', '2023-09-12') #애플 주식데이터를 가져온다.
df.head()

가장 먼저 해야할 작업은 데이터를 불러와 탐색해 보는 것이다.
애플('AAPL') 데이터를 실시간으로 불러와 df 변수에 저장한 후 head()함수를 통해 상위 5개 데이터를 확인한다. 
인덱스는 날짜(Date)로 되어 있고, 각 컬럼은 시가(Open), 고가(High), 저가(Low), 종가(Close), 수정 종가(Adj Close), 거래량(Volume) 데이터로 구성된다.

데이터프레임에 있는 describe() 함수를 사용해 데이터셋의 분포, 경향 분산 및 형태를 요약하는 정보를 확인할 수 있다.

df.describe()

위 데이터셋에서 볼린저 밴드 분석에 필요한 컬럼만 선택하도록 한다.

price_df = df.loc[:, [''Adj Close']].copy()
price_df.head()

price_df 데이터셋으로 볼린저 밴드를 만들어 본자.

  • 상단 밴드 = 중간 밴드 + 2*20일 이동 표준 편차
  • 중간 밴드 = 20일 이동 평균선
  • 하단 밴드 = 중간 밴드 - 2*20일 이동 표준 편차

먼저 중간 밴드를 만들도록 한다. 중간 밴드의 20일 이동 평균선은 판다스의 rolling() 함수를 이용하여 만든다.

price_df['center'] = price_df['Adj Close'].rolling(window = 20).mean()
price_df.iloc[18:25]

iloc 인덱서를 사용해 18행부터 24행까지 어떤 값이 들어있는지 확인해 본다. 새롭게 만들어진 center 컬럼은 rolling()함수와 mean()함수로 계산된 20일 이동 평균선을 의미한다.
rolling() 함수 특성상 window 입력값으로 20일을 주었기 때문에 데이터가 20개 미만인 부분은 결측치를 의미하는 NaN으로 표시된다. 

일반적으로 결측치는 어쩔 수 없이 생성되기 때문에 볼린저 밴드에 필요한 기간보다 앞뒤로 더 많은 데이터(보통 쿠션 데이터 cushion data라고 부르는 여분의 데이터)를 추가하여 분석을 해야 한다. 실무에서는 생성하려는 자료를 고려해서 작성하는 것이 중요하지만, 여기서는 결측치가 발생한 행을 삭제한다.

중간 밴드를 만들었으니 이번에는 상단 밴드와 하단 밴드를 만든다.

price_df['ub'] = price_df['center] + 2 * price_df['Adj Close'].rolling(window=20).std()
price_df.iloc[18:25]

상단 밴드는 'ub'라는 컬럼으로 만들고, 표준 편차를 계산하는 std() 함수로 이동 표준 편차를 구한다.

price_df['db'] = price_df['center'] - 2 * price_df['Adj Close'].rolling(window=20).std()
price_df.iloc[18:25]

볼린저 밴드는 다음에 재활용할 수 있도록 파이썬 함수로 만들어 저장하면 편리하게 사용할 수 있다.

n = 20
sigma = 2
def bollinger_band(price_df, n, sigma):
    bb = price_df.copy()
    bb['center'] = price_df['Adj Close'].rolling(n).mean() # 중앙 이동 평균선
    bb['ub'] = bb['center'] + sigma * price_df['Adj Close'].rolling(n).std() # 상단밴드
    bb['db'] = bb['center'] - sigma * price_df['Adj Close'].rolling(n).std() # 하단밴드
    return bb
    
bollinger = bollinger_band(price_df, n, sigma)

그 다음 기간을 나눠 볼린저 밴드를 활용한 전략의 성과를 확인해 보자.

base_date = '2008-01-02'
sample = bollinger.loc[base_date:]
sample.head()

base_date 변수에 새로운 날짜를 할당하고 설정된 날짜 이후 데이터로 전략 성과를 확인한다. 

평균 회귀 전략에서 진입/청산 신호가 발생할 대 우리가 취하는 행동을 기록할 데이터프레임을 만들어보자. 앞으로 이러한 데이터프레임을 trading book이라고 명명한다. 

다음 book 변수를 만들어 거래내역 컬럼을 만든다.

book = sample[['Adj Close']].copy()
book['trade'] = '' # 거래내역 컬럼
book.head()

위 코드를 함수화 하면 다음과 같다.

def create_trade_book(sample):
    book = sample[['Adj Close']].copy()
    book['trade'] = ''
    return(book)

이번에는 볼린저 밴드를 활용한 전략 알고리즘을 만들어 본다.

반응형

트레이딩 전략 알고리즘에 대한 전체 코드는 다음과 같다.

def tradings(sample, book):
    for i in sample.index:
        if sample.loc[i, 'Adj Close'] > sample.loc[i, 'ub']: # 상단밴드 이탈시 동작 안함
            book.loc[i, 'trade'] = ''
        elif sample.loc[i, 'db'] > sample.loc[i, 'Adj Close']: # 하단밴드 이탈시 매수
            if book.shift(1).loc[i, 'trade'] == 'buy': # 이미 매수 상태라면
                book.loc[i, 'trade'] = 'buy' # 매수상태 유지
            else:
                book.loc[i, 'trade'] = 'buy'
        elif sample.loc[i, 'ub'] >= sample.loc[i, 'Adj Close'] and sample.loc[i, 'Adj Close'] >= sample.loc[i, 'db']: # 볼린저밴드 안에 있을 시
            if book.shift(1).loc[i, 'trade'] == 'buy':
                book.loc[i, 'trade'] = 'buy' # 매수상태 유지
            else:
                book.loc[i, 'trade'] = '' # 동작 안함
    return (book)

이 전략의 특징은 장기 보유보다는 과매도 / 과매수 구간을 포착하는 단기 매매에 더 효과적이라고 해석한다. 

완성된 거래 전략을 수행하면 다음과 같이 거래내역이 기록된 것을 확인할 수 있다. 

book = tradings(sample, book)
book.tail(10)

다음은 트레이딩 book에 적혀있는 거래내역대로 진입/청산 일자에 따른 매수/매도 금액을 바탕으로 수익률을 계산해 보자.

def returns(book): # 손익계산
    rtn = 1.000
    book['return'] = 1
    buy = 0.0
    sell = 0.0
    for i in book.index:
        # long 진입
        if book.loc[i, 'trade'] == 'buy' and book.shift(1).loc[i, 'trade'] == '':
            buy = book.loc[i, 'Adj Close']
            print('진입일 : ', i, 'long 진입가격 : ', buy)
        #long 청산
        elif book.loc[i, 'trade'] == '' and book.shift(1).loc[i, 'trade'] == 'buy':
            sell = book.loc[i, 'Adj Close']
            rtn = (sell-buy) / buy + 1 # 손익 계산
            book.loc[i, 'return'] = rtn
            print('청산일 : ', i, 'long 진입가격 : ', buy, '| long 청산가격 : ', sell, '| return:', round(rtn,4))

        if book.loc[i, 'trade'] == '' : # 제로 포지션
            buy = 0.0
            sell = 0.0

    acc_rtn = 1.0
    for i in book.index:
        rtn = book.loc[i, 'return']
        acc_rtn = acc_rtn * rtn # 누적 수익률 계산
        book.loc[i, 'acc return'] = acc_rtn

    print('Accumulated return : ', round(acc_rtn, 4))
    return (round(acc_rtn, 4))

수익률을 저장한 변수와 book변수에 수익률 컬럼을 만든다.
for-loop문을 돌면서 포지션 여부에 따라 수익률을 계산해 데이터프레임에 저장하고, 최종적으로 누적 수익률을 계산한다.

print(returns(book))

변화 추이를 한눈에 보고 싶다면 누적 수익률을 그래프로 그려보자.

import matplotlib.pylab as plt
book['acc return'].plot()

백테스팅이라는 불리는 과정을 통해 실제 데이터를 가지고 확인을 해봐야 자신이 만든 전략의 신뢰도를 높일 수 있다. 여기서는 간단하게 과거 데이터를 통해 평균 회귀 전략에 대한 기본 개념을 정리해 본 것이다. 따라서, 이 전략을 바로 활용하기는 어렵다고 생각한다. 또한 평균 회귀 전략이 모든 종목에 적합한 것도 아니다.
그렇기 때문에 본인의 투자 철학과 해당 주식에 대한 면밀한 파악을 통해 전략을 수립하고 실행하도록 해야 한다.

반응형
반응형

보통 ML이 가능한 서버와 원격으로 접속하는 클라이언트 환경에서 주피터 노트북을 사용한다. 그리고, 서버는 리눅스(또는 우분투) 환경에서 사용하는 것이 일반적이라고 생각하기에 그런 환경을 염두에 두고 정리한다.

서버 주소를 알고 있다는 가정하에, SSH로 서버에 접속한다.

SSH 아이디@192.168.0.123
아이디@192.168.0.123's password:

패스워드를 입력하고, 접속하면 원격 서버에 접속하게 된다.

아이디@sss:~$ jupyter notebook --generate-config

콘솔 창에서 위 jupyter notebook --generate-config를 입력하면, jupyter_notebook_config.py 파일이 생성된다.
우분투의 ls -al 명령을 실행하면 현재 위치에 있는 폴더 목록(숨김 폴더까지 보여줌)을 보여준다. 
* 아래는 주피터 노트북 사이트에 나온 내용을 정리함

1. 단일 노트북 서버 실행

주피터 노트북 웹 애플리케이션은 서버-클라이언트 구조를 기반으로 한다. 노트북 서버는 HTTP 요청을 처리하기 위해 ZeroMQ와 Tornado를 기반으로 하는 2-프로세스 커널 아키텍처를 사용한다. 
※ 기본적으로 노트북 서버는 127.0.0.1:8888에서 로컬로 실행되며 localhost에서만 액세스할 수 있다. http://127.0.0.1:8888을 사용하여 브라우저에서 노트북 서버에 액세스할 수 있다.

2. 공개 노트북 서버 실행

웹 브라우저를 통해 원격으로 노트북 서버에 액세스하려면 공용 노트북 서버를 실행하면 된다. 공용 노트북 서버를 실행할 때 최적의 보안을 위해서는 먼저 노트북 서버 보안을 해야 한다.

비밀번호와 SSL/HTTPS로 서버를 보호해야 한다.

먼저 비밀번호를 사용해서 노트북 보안을 하려면 다음과 같은 명령어를 입력하면 된다.

jupyter notebook password

노트북 서버 보안을 위해 인정서 파일과 해시된 비밀번호 생성하는 것부터 시작한다.

아직 구성 파일이 없으면 다음 명령줄을 사용해 노트북용 구성 파일을 만든다.

jupyter notebook --generate-config

~/.jupyter 디렉토리에서, jupyter_notebook_config.py 구성 파일을 편집한다. 기본적으로 노트북용 구성파일은 모든 필드가 주석처리되어 있다. 사용하려는 명령어 옵션만 사용가능하게 주석 처리를 해제하고, 편집해야 한다. 최소한의 구성 옵션 세트는 다음과 같다.

# 인증서 파일, IP, 비밀번호에 대한 옵션을 설정하고 브라우저 자동 열기를 끕니다.
c.NotebookApp.certfile = u'/absolute/path/to/your/certificate/mycert.pem'
c.NotebooApp.keyfile = u'/absolute/path/to/your/certificate/mykey.key'
# 공용 서버의 모든 인터페이스(ips)에 바인딩하려면 ip를 '*'로 설정합니다.
c.NotebookApp.ip = '*'
c.NotebookApp.password = u'sha1:bcd259ccf...<your hashed password here>'
c.NotebookApp.open_brower = False

# 서버 액세스를 위해 알려진 고정 포트를 설정하는 것이 좋습니다.
c.NotebookApp.port = 9999

그런 다음 jupyter notebook 명령어를 사용하여 노트북을 시작할 수 있다.

  • 'https를 사용하세요. SSL 지원을 활성화했을 땐,  일반 http://가 아닌 https://를 사용해 노트북 서버에 접속해야 한다는 것을 기억하세요. 콘솔의 서버 시작 메시지에 이 내용을 미리 알려주만 세부 사항을 간과하고 서버가 다른 이유로 응답하지 않는다고 생각하기 쉽습니다. 
  • SSL을 사용하는 경우 항상 'https://'로 노트북 서버에 접속!

이제 브라우저에서 공개 서버의 도메인 'https://your.host.com:9999'를 입력하여 당신의 공개 서버에 액세스할 수 있다.

반응형

3. 방화벽 설정

올바르게 작동하게 하려면, 클라이언트에서 연결할 수 있도록 주피터 노트북 서버가 돌아가는 컴퓨터 방화벽(공유기가 있는 경우 공유기 방화벽 설정도 필요)의 주피터 노트북 구성 파일(jupyter_notebook_config.py)의 액세스 포트(c.NotebooApp.port) 설정에 구성되어 있어야 한다. 방화벽은 49152에서 65535까지의 포트에서 127.0.0.1(localhost)의 연결을 허용해야 한다. 이러한 포트는 서버가 노트북 커널과 통신하는 데 사용된다. 커널 통신 포트는 ZeroMQ에 의해 무작위로 선택되며 커널당 여러 연결이 필요할 수 있으므로 광범위한 포트에 액세스할 수 있어야 한다.

좀 더 많은 명령어를 알아보려면, 주피터 노트북에서 다음과 같은 명령어를 사용하면 된다.

jupyter notebook --help

반응형
반응형

WMS가 필요한 이유

데이터를 옮기고 변경시켜서 어떤 작업을 수행하는데 있어 각종 작업을 자동화하여 프로그램을 만든 다음 cron(일정시간이 되면 특정 명령어를 실행하는 도구)같은 거에 실어서 주기적으로 실행하게 합니다.
그러다 이걸로도 충분하지 않은 상황이 된다면(복잡해지거나, 테스크가 변경되거나 등) 이런 시점에 WMS가 필요해집니다.
 

WMS도구 중 가장 유명한 2개 비교 ( Luigi & Airflow)

 
  • Luigi 는…
    • pipeline에 기반하였고, 각 task의 input/output은 정보를 공유하고 서로 연결된다.
    • Target기반 접근(Target은 Task의 결과물-통상 파일-)
    • UI는 꼭 필요한 기능만 단순하게 구현되었고, 프로세스를 실행하거나 하는 기능은 없다.
    • 내장된 Trigger가 없다(crontab 파일을 편집해서 일정주기마다 실행되게 하는 식으로 구성해야 한단다)
    • 분산 실행은 지원하지 않음(역주 : 병렬실행은 지원하지만…)
  • Airflow 는 …
    • DAG 표현에 기반
    • Task간 정보공유가 없으므로, 최대한 병렬화를 할 수 있단다(위상정렬/순서만 잘 정하면 된다)
    • Task간 통신 수단이 마땅치 않다.
    • 설정하면 분산 실행할 수 있는 Executor가 있다
    • Scheduler가 있으므로, set it and forget it 할 수 있다(스스로 trigger한다)
    • 실행상태를 확인하고, 작업중인 Task를 조작할 수 있는 강력한 UI가 있다


비교표 Source)
https://towardsdatascience.com/why-quizlet-chose-apache-airflow-for-executing-data-workflows-3f97d40e9571

간단히 정리

Luigi는 좀 더 단순하기 때문에 처음에 시작하기 쉽기 때문에 작은 조직에서 사용하기 좋고, Airflow는 더 효율적인 시각화와 지원기능이 있어 체계적으로 사용하려는 조직에서 쓰는 것이 더 합당하다고 할 수 있습니다. 

반응형
반응형

요즘 부업 또는 본업으로 온라인 유통업을 하는 분들이 많이 늘고 있다.

나도 몇개 제품을 가지고 판매를 시작했는데, 처음에는 광고를 달지 않고 판매를 하려고 했다.

그런데, 정말 노출이 안되는 걸 알게 되었다.

그래서, 네이버 광고에 가입해서 광고를 시작하게 되었다.

네이버 광고 시스템이 첫눈에 확 들어오는 구조가 아니라서, 좀 복잡했다.

일단 만들어서 올려보는게 상책인 거 같아서, 만들어 올려보았다.

 

첫 주문이 발생했다. 일주일에 약 3~5개 주문이 들어왔다. 기분이 좋았다. 

그런데, 2주 정도 지나자 주문이 안들어오는 것이다. 광고 노출도 안되고 있었다. 

원인이 뭘까 찾아보니, 유료로 네이버 광고를 도와주는 시스템들이 눈에 들어왔다.

광고소재별 약 5~10분 주기로 그 소재(키워드) 광고 순위를 확인한 후 광고금액을 자동 증감하는 구조로 되어 있었다.

아, 이래서 내 광고는 노출이 안되었던 거구나.

난 목표순위와 상관없이 언제나 최소 고정금액이었으니, 당연히 노출이 안되었던 것이다.

많은 사람들이 네이버 광고를 도와주는 유료 시스템을 사용하고 있다면, 그 사람들께 노출될 수 밖에 없겠구나...

그런데, 이 유료 시스템이 네이버에서 제공하고 있는 네이버 광고 API를 사용하고 있는 것이다.

그렇다면, 파이썬으로 직접 필요한 부분을 만들어서 사용하면 어떨까 하는 생각이 들었다.

네이버 검색 광고 API

http://naver.github.io/searchad-apidoc/#/guides

 

searchad-apidoc

 

naver.github.io

구성은 AdExtension(확장소재), Adgroup(광고그룹), Ad(광고), Campaign(캠페인), BusinessChannel(비즈니스채널), AdKeyword(광고키워드), LabelRef(라벨 참조), Label(라벨), ManagedKeyword(관리하고 있는 키워드), Target(목표), IpExclusion(차단된 IP), Bizmoney(비즈머니), ManagedCustomerLink(관리하고 있는 고객링크), StatReport(상태보고서), Stat(상태), MasterReport(마스터보고서), RelKwdStat(연관 키워드 정보보기), Estimate(평가)

한 눈에 안들어오는 구성이다. 

전체 구성을 대략적으로 파악하기 위해서 '네이버 검색 광고 API'를 실제 네이버 광고 시스템과 매칭해 보면서 익혀 보는 것이 좋다.

일단, 가장 쉬운 것부터 하기로 했으니, 소스 분석은 나중에 심도있게 쪼개보기로 하자.

지금은 파이썬에서 쉽게 사용할 수 있도록 네이버 검색 광고 API에 접근할 수 있는 라이브러리를 활용해 보도록 하겠다.

깃헙에 올라와 있는 powernad로 쉽게 접근해 보도록 하자.

https://github.com/devkingsejong/python-PowerNad

 

devkingsejong/python-PowerNad

Naver search Ad Lib for Python. Contribute to devkingsejong/python-PowerNad development by creating an account on GitHub.

github.com

  • 먼저 라이브러리를 인스톨한다.

>> pip install powernad

또는 git clone https://github.com/devkingsejong/python-PowerNad.git

  • 그 다음에 라이브러리를 읽어들인다.

>>> from powernad.API.Campaign import *

  • 네이버 광고에서 제공하는 관련 API 정보를 세팅한다.

BASE_URL = 'https://api.naver.com'
API_KEY = '000000000000000000000000000000000000000000000000000000000000000000'
SECRET_KEY = '00000000000000000000000000000000000000000000000000'
CUSTOMER_ID = '0000000'   

  • 캠페인 초기화

>>> c = Campaign(BASE_URL, API_KEY, SECRET_KEY, CUSTOMER_ID)

  • 캠페인 리스트 불러오기

>>> c.get_campaign_list()

  • 연관 키워드 정보 보기

>>> from powernad.API.RelKwdStat import *

  • RelKwdStat 초기화

>>> rel = RelKwdStat(BASE_URL, API_KEY, SECRET_KEY, CUSTOMER_ID)

  • '샴푸' 키워드로 검색해 보기

# 위 결과값은 키워드, 월간검색수(PC, 모바일), 월평균클릭수(PC, 모바일), 월평균클릭률(PC, 모바일), 경쟁정도를 말한다. [0]을 다른 값으로 변경해서 연간키워드에 대한 정보도 습득할 수 있다.

오늘은 여기까지 해보자. 

5월 중에 위 알고리즘에 따라 광고키워드 순위에 따른 광고금액 변경을 할 수 있는 방법을 올려보도록 하겠다.

각자 위 라이브러리를 활용해서, 네이버 광고 API를 잘 이용할 수 있는 노하우를 갖게 되었으면 좋겠다.

반응형

+ Recent posts