Connors RSI(CRSI)는 Larry Connors가 개발한 기술적 지표로, 주가의 상대적 강도를 측정하는 데 사용된다. CRSI는 RSI(Relative Strength Index), 상대 강도(Relative Strength), 그리고 업틱과 다운틱의 비율을 종합적으로 고려하여 계산된다. 이를 통해 단기적인 모멘텀 변화를 포착하고, 매매 신호를 생성할 수 있다.
Connors RSI의 계산 과정은 다음과 같다:
- RSI 계산: 일반적인 RSI 공식을 사용하여 RSI 값을 구한다. 기본 설정은 3일이다.
- 상대 강도(RS) 계산: 전일 종가 대비 상승분을 전일 종가로 나누어 RS 값을 구한다.
- 업틱/다운틱 비율(UpDown Ratio) 계산: 일정 기간(기본 설정은 2일) 동안의 업틱 수를 다운틱 수로 나누어 UpDown Ratio를 구한다.
- CRSI 계산: (RSI + RS + UpDown Ratio) / 3
예를 들어, 3일 RSI가 60, RS가 0.02, UpDown Ratio가 1.5라면, CRSI = (60 + 0.02 + 1.5) / 3 = 20.51이 된다.
CRSI는 0에서 100 사이의 값을 가지며, 일반적으로 70 이상은 과매수 구간, 30 이하는 과매도 구간으로 해석된다.
Connors RSI를 활용한 매매 전략은 다음과 같이 구성할 수 있다:
- 과매수/과매도 구간 활용: CRSI가 70 이상일 때 매도하고, 30 이하일 때 매수한다.
- 비율 기반 매매: CRSI 값의 비율을 활용하여 자금을 배분한다. 예를 들어, CRSI가 50이면 50%의 자금을 투자하고, CRSI가 80이면 20%의 자금만 투자한다.
- 다이버전스 전략: 주가는 신고가를 경신하는데 CRSI는 신고가를 경신하지 못할 때(bearish divergence) 매도하고, 주가는 신저가를 경신하는데 CRSI는 신저가를 경신하지 못할 때(bullish divergence) 매수한다.
파이썬을 활용하여 Connors RSI 기반의 트레이딩 전략을 백테스팅해보자. 필요한 라이브러리는 다음과 같다:
pandas
: 데이터 조작 및 분석numpy
: 수치 계산matplotlib
: 데이터 시각화FinanceDataReader
: 주가 데이터 수집
가상환경을 활성화하고 필요한 라이브러리를 설치한다.
source venv/bin/activate
pip install pandas numpy matplotlib FinanceDataReader
FinanceDataReader를 설치하면 종속 모듈도 함께 설치된다.
이제 Connors RSI를 계산하고 매매 신호를 생성하는 코드를 작성해보자.
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)
# RSI 계산 함수
def calculate_rsi(data, period=3):
delta = data['Close'].diff()
gain = delta.where(delta > 0, 0)
loss = -delta.where(delta < 0, 0)
avg_gain = gain.rolling(window=period).mean()
avg_loss = loss.rolling(window=period).mean()
rs = avg_gain / avg_loss
rsi = 100 - (100 / (1 + rs))
return rsi
# 상대 강도(RS) 계산 함수
def calculate_rs(data):
delta = data['Close'].diff()
rs = delta / data['Close'].shift(1)
return rs
# 업틱/다운틱 비율 계산 함수
def calculate_updown_ratio(data, period=2):
up_tick = np.where(data['Close'].diff() > 0, 1, 0)
down_tick = np.where(data['Close'].diff() < 0, 1, 0)
up_tick_sum = up_tick.rolling(window=period).sum()
down_tick_sum = down_tick.rolling(window=period).sum()
updown_ratio = up_tick_sum / down_tick_sum
return updown_ratio
# Connors RSI 계산 함수
def calculate_crsi(data, rsi_period=3, rs_period=1, updown_period=2):
rsi = calculate_rsi(data, period=rsi_period)
rs = calculate_rs(data)
updown_ratio = calculate_updown_ratio(data, period=updown_period)
crsi = (rsi + rs.rolling(window=rs_period).mean() + updown_ratio) / 3
return crsi
# 매매 신호 생성 함수
def get_signals(crsi, threshold_buy=30, threshold_sell=70):
signals = pd.DataFrame(index=crsi.index)
signals['signal'] = 0
signals['signal'][crsi > threshold_sell] = -1
signals['signal'][crsi < threshold_buy] = 1
signals['positions'] = signals['signal'].diff()
return signals
# Connors RSI 계산
crsi = calculate_crsi(data)
# 매매 신호 생성
signals = get_signals(crsi)
# 백테스팅
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': [2, 1, 1]})
ax1.plot(data['Close'], label='Price')
ax1.set_ylabel('Price')
ax1.set_title('Price Chart')
ax1.legend(loc='upper left')
ax2.plot(crsi, label='Connors RSI')
ax2.axhline(y=70, color='r', linestyle='--', label='Overbought')
ax2.axhline(y=30, color='g', linestyle='--', label='Oversold')
ax2.set_ylabel('Connors RSI')
ax2.set_title('Connors RSI')
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_rsi
, calculate_rs
, calculate_updown_ratio
함수는 각각 RSI, 상대 강도, 업틱/다운틱 비율을 계산한다. 이를 바탕으로 calculate_crsi
함수에서 Connors RSI를 계산한다.
get_signals
함수는 계산된 Connors RSI를 기준으로 매매 신호를 생성한다. Connors RSI가 과매수 임계값(기본값: 70)을 초과할 때 매도 신호(-1)를, 과매도 임계값(기본값: 30)을 하회할 때 매수 신호(1)를 생성한다.
백테스팅을 위해 초기 자본금을 10,000달러로 설정하고, 생성된 매매 신호를 바탕으로 포트폴리오 가치를 계산한다. 최종적으로 총 수익률과 포트폴리오 가치를 출력한다.
matplotlib
을 사용하여 주가, Connors RSI, 포트폴리오 가치를 시각화한다. Connors RSI 차트에는 과매수(70)와 과매도(30) 기준선을 표시하여 매매 신호 발생 구간을 나타낸다.
코드 실행 중 발생할 수 있는 오류를 처리하기 위해 try-except
문을 사용하였다. 데이터를 가져오는 과정에서 오류가 발생하면 에러 메시지를 출력하고 프로그램을 종료한다.
[주의]
제시한 코드에서는 실제 틱(tick) 정보를 직접 받아오지 않고, 일봉(daily) 데이터를 기반으로 업틱과 다운틱을 계산한다. FinanceDataReader에서 제공하는 데이터는 일봉, 주봉, 월봉 등의 시간봉 데이터이며, 실시간 틱 데이터는 제공하지 않는다.
실제 틱 정보를 받아오려면 증권사 API나 전문 금융 데이터 제공업체의 서비스를 이용해야 한다. 예를 들어, 국내 증권사 중에는 KIS Developers, 키움 Open API 등을 통해 실시간 틱 데이터를 제공하는 곳이 있다. 해외 시장의 경우 Interactive Brokers API, TD Ameritrade API 등을 활용할 수 있다.
이러한 API를 사용하면 실시간으로 틱 데이터를 받아올 수 있지만, 대부분 유료 서비스이며 별도의 인증 과정과 계약이 필요하다. 또한 받아온 틱 데이터를 가공하고 처리하는 작업이 추가로 필요할 수 있다.
따라서 제시한 코드는 일봉 데이터를 기반으로 업틱과 다운틱을 근사적으로 계산한 것으로, 실제 틱 데이터를 사용한 것과는 차이가 있다. 보다 정확한 Connors RSI를 계산하고 활용하기 위해서는 실제 틱 데이터를 사용하는 것이 좋겠지만, 데이터 접근성과 비용 등의 제약이 있을 수 있다.