NVI 지표의 개념과 유래
NVI(Negative Volume Index)는 거래량이 감소한 날의 주가 변동을 누적하여 계산하는 지표이다. 이 지표는 Paul Dysart가 1930년대에 개발하였으며, 거래량이 감소한 날에는 투자자들의 관심이 낮아지므로 주가 움직임이 더 의미 있다는 가정에 기반한다. NVI는 시장에서 소외된 투자자들의 행동을 추적하는 데 사용된다.
NVI는 다음과 같은 가정을 전제로 한다:
- 거래량이 감소한 날의 주가 상승은 잘 알려지지 않은 정보에 의한 것일 수 있다.
- 거래량이 감소한 날의 주가 하락은 일반적으로 무시할 수 있다.
따라서 NVI는 거래량이 감소한 날 중에서도 주가가 상승한 날에 주목하여 지표를 계산한다.
NVI 지표 계산 방법
NVI는 다음과 같은 단계를 통해 계산된다:
- 기준일의 NVI 값을 100으로 설정한다.
- 거래량이 전일 대비 감소한 날의 주가 변동률을 계산한다.
- 거래량이 감소한 날의 NVI 값을 다음과 같이 계산한다:
- 주가가 상승한 경우: NVI = 전일 NVI × (1 + 주가 변동률)
- 주가가 하락한 경우: NVI = 전일 NVI
- 거래량이 증가한 날의 NVI 값은 전일 NVI 값과 동일하게 유지한다.
이렇게 계산된 NVI 값은 거래량이 감소한 날 중 주가가 상승한 날의 영향을 누적하여 반영한다. NVI가 상승한다는 것은 거래량이 감소한 날에 주가가 상승하는 경향이 있다는 것을 의미한다.
NVI 지표를 활용한 매매 전략
NVI 지표를 활용한 매매 전략은 다음과 같이 구성할 수 있다:
- NVI 지표의 추세 전환 포착:
- NVI 지표가 상승 추세에서 하락 추세로 전환될 때 매도 신호로 해석한다.
- NVI 지표가 하락 추세에서 상승 추세로 전환될 때 매수 신호로 해석한다.
- NVI 지표와 주가 추세의 비교:
- NVI 지표가 상승하는데 주가는 횡보하거나 하락할 때, 주가 상승에 대한 잠재력이 있다고 판단하여 매수 기회로 활용한다.
- NVI 지표가 하락하는데 주가는 횡보하거나 상승할 때, 주가 하락에 대한 위험이 있다고 판단하여 매도 기회로 활용한다.
- NVI 지표의 변화율을 활용한 매매:
- NVI 지표의 변화율이 일정 기준(예: 10%) 이상 상승할 때 매수하고, 일정 기준 이상 하락할 때 매도한다.
- 이는 NVI 지표의 급격한 변화를 포착하여 매매 타이밍을 결정하는 전략이다.
NVI 지표는 거래량 감소 시 주가 움직임에 주목하여 시장 참여자들의 행동을 분석하는 데 유용하지만, 단독으로 사용하기보다는 다른 기술적 지표나 시장 상황과 함께 종합적으로 판단하는 것이 좋다.
파이썬을 활용한 NVI 지표 백테스팅
파이썬을 사용하여 NVI 지표 기반의 트레이딩 전략을 백테스팅해보자. 다음 라이브러리를 활용할 것이다:
pandas
: 데이터 조작 및 분석numpy
: 수치 계산matplotlib
: 데이터 시각화FinanceDataReader
: 주가 데이터 수집
먼저 가상환경을 활성화하고 필요한 라이브러리를 설치한다.
source venv/bin/activate
pip install pandas numpy matplotlib FinanceDataReader
FinanceDataReader를 설치하면 종속 모듈도 함께 설치된다.
다음으로 NVI 지표를 계산하고 매매 신호를 생성하는 코드를 작성해보자.
import FinanceDataReader as fdr
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# 데이터 가져오기
symbol = "VOO"
start_date = "1900-01-01"
end_date = "2023-05-01"
try:
data = fdr.DataReader(symbol, start_date, end_date)
except Exception as e:
print(f"Error occurred while fetching data: {e}")
exit(1)
# NVI 지표 계산 함수
def calculate_nvi(data, start_value=100):
nvi = pd.Series(np.nan, index=data.index)
nvi.iloc[0] = start_value
for i in range(1, len(data)):
if data['Volume'][i] < data['Volume'][i-1]:
if data['Close'][i] > data['Close'][i-1]:
nvi.iloc[i] = nvi.iloc[i-1] * (1 + (data['Close'][i] - data['Close'][i-1]) / data['Close'][i-1])
else:
nvi.iloc[i] = nvi.iloc[i-1]
else:
nvi.iloc[i] = nvi.iloc[i-1]
return nvi
# 매매 신호 생성 함수
def get_signals(nvi, window=20):
signals = pd.DataFrame(index=nvi.index)
signals['nvi'] = nvi
signals['nvi_ma'] = nvi.rolling(window=window).mean()
signals['signal'] = 0
signals['signal'][window:] = np.where(signals['nvi'][window:] > signals['nvi_ma'][window:], 1, 0)
signals['positions'] = signals['signal'].diff()
return signals
# NVI 지표 계산
nvi = calculate_nvi(data)
# 매매 신호 생성
signals = get_signals(nvi)
# 백테스팅
initial_capital = 10000
positions = pd.DataFrame(index=signals.index).fillna(0.0)
positions[symbol] = 100 * signals['signal']
portfolio = positions.multiply(data['Adj Close'], axis=0)
pos_diff = positions.diff()
portfolio['holdings'] = (positions.multiply(data['Adj Close'], axis=0)).sum(axis=1)
portfolio['cash'] = initial_capital - (pos_diff.multiply(data['Adj Close'], axis=0)).sum(axis=1).cumsum()
portfolio['total'] = portfolio['cash'] + portfolio['holdings']
portfolio['returns'] = portfolio['total'].pct_change()
# 결과 출력
print(f"Final portfolio value: ${portfolio['total'][-1]:,.2f}")
print(f"Total returns: {100 * (portfolio['total'][-1] / initial_capital - 1):.2f}%")
# 시각화
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(15, 15), gridspec_kw={'height_ratios': [3, 1, 1]})
ax1.plot(data['Close'], label='Price')
ax1.set_ylabel('Price')
ax1.set_title('Price Chart')
ax1.legend(loc='upper left')
ax2.plot(nvi, label='NVI')
ax2.plot(signals['nvi_ma'], label='NVI MA')
ax2.set_ylabel('NVI')
ax2.set_title('Negative Volume Index (NVI)')
ax2.legend(loc='upper left')
ax3.plot(portfolio['total'], label='Portfolio Value')
ax3.set_ylabel('Portfolio Value')
ax3.set_title('Portfolio Performance')
ax3.legend(loc='upper left')
plt.tight_layout()
plt.show()
위 코드에서는 FinanceDataReader를 사용하여 VOO ETF의 전체 기간 주가 데이터를 가져온다. start_date
를 가능한 한 오래전 날짜로 설정하여 모든 데이터를 가져올 수 있도록 한다.
calculate_nvi
함수는 주어진 데이터를 바탕으로 NVI 지표를 계산한다. 초기 NVI 값을 100으로 설정하고, 거래량이 감소한 날 중 주가가 상승한 경우에만 NVI 값을 업데이트한다.
get_signals
함수는 계산된 NVI 지표를 기준으로 매매 신호를 생성한다. NVI 값이 이동평균(기본값: 20일)을 상향 돌파할 때 매수 신호(1)를 생성한다.
백테스팅을 위해 초기 자본금을 10,000달러로 설정하고, 생성된 매매 신호를 바탕으로 포트폴리오 가치를 계산한다. 최종적으로 총 수익률과 포트폴리오 가치를 출력한다.
matplotlib
을 사용하여 주가, NVI 지표, 포트폴리오 가치를 시각화한다. NVI 지표 차트에는 NVI 값과 이동평균을 함께 표시하여 매매 신호 생성 기준을 나타낸다.
코드 실행 중 발생할 수 있는 오류를 처리하기 위해 try-except
문을 사용하였다. 데이터를 가져오는 과정에서 오류가 발생하면 에러 메시지를 출력하고 프로그램을 종료한다.