투자 방식은 크게 두 가지 유형으로 나눌 수 있습니다. 펀더멘털 투자자는 기업의 비즈니스 모델, 수익, 현금 흐름, 현재 가치 평가, 리스크, 성장 전망과 같은 근본 지표를 바탕으로 투자 여부를 결정합니다. 워런 버핏(Warren Buffett)이 펀더멘털 분석을 통해 저평가된 기업을 발굴해 세계 최고 부자 중 한 명이 된 것은 유명한 이야기입니다.
반면 기술적 투자자는 기업의 펀더멘털에는 거의 관심을 두지 않고, 수백 가지 기술적 지표에서 매수·매도·관망 신호를 도출하는 데 집중합니다. 이들은 일 단위, 주 단위, 또는 월 단위로 거래를 실행합니다.
여기서 어느 투자 스타일이 옳고 그른지를 논하려는 것은 아닙니다. 중요한 점은 펀더멘털 투자자든 기술적 투자자든, 수백~수천 개에 달하는 데이터 소스를 연구하고 분석하며 그 위에서 의사결정을 내린다는 사실입니다.
펀더멘털 투자자는 전 세계 수천 개 기업과 수백 개 산업에 걸친 대차대조표, 손익계산서, 현금흐름표, 인구 통계 추세, 테마별·사회적 트렌드를 연구합니다. 그리고 그 데이터 위에 재무 모델이나 분석 체계를 구축해 기업의 궤적과 성장 가능성을 더 깊이 이해합니다.
기술적 투자자는 시간당 또는 일 단위로 수백 개의 기술적 지표를 살피며 투자 신호를 도출합니다. 기술적 지표에 의존하는 투자자는 여러 시간 프레임에 걸친 방대한 데이터를 다루며 자신만의 트레이딩 전략을 세워야 합니다. 수많은 지표를 조회하면서도 사실상 즉각적인 답을 얻어야 하고, 빠르게 변하는 시장에 맞춰 알고리즘과 트레이딩 전략을 신속하게 조정해야 합니다. Redis Enterprise는 이러한 모든 과제를 해결할 수 있습니다.
최근 필진은 금융 업계에서 Redis Enterprise가 왜 필수적인 도구인지 다룬 적이 있습니다. 이번 글에서는 RedisTimeSeries를 활용해 주가와 기술적 지표를 저장하고, 집계하고, 조회하는 방법을 소개합니다. 여기서 설명하는 원리는 기업 재무 정보나 펀더멘털 투자자가 사용하는 기타 시계열 데이터의 저장 및 조회에도 그대로 적용할 수 있습니다.
RedisTimeSeries 모듈은 초당 수백만 건의 이벤트, 샘플, 가격 데이터를 수집하고 조회할 수 있습니다. IoT부터 헬스케어, 금융까지 다양한 분야에서 시간과 값의 쌍(time-value pair)을 저장해 추세를 파악하는 용도에 특히 적합합니다. 또한 평균, 합계, 최솟값, 최댓값, 표준편차, 범위(range) 등의 집계 기능을 제공해 데이터 분석과 의사결정을 손쉽게 할 수 있습니다.
RedisTimeSeries로 주가 추적하기
이번 포스트에서는 RedisTimeSeries를 사용해 주가와 기술적 지표를 저장하는 모델을 소개합니다. 가격과 지표를 위한 다양한 시계열 생성 방법, 원본 시계열 위에 집계를 구성하는 방법, 그리고 다양한 RedisTimeSeries 명령으로 대량의 시계열을 손쉽게 수집하고 조회하는 방법을 다룹니다. 독자 여러분의 유즈 케이스 출발점으로 삼을 수 있는 Python 샘플 코드도 함께 제공합니다.
옳든 그르든 많은 사람들이 다우존스 산업평균지수(DJIA)를 미국 경제의 선도 지표로 여깁니다. 필진은 DJIA를 구성하는 30개 종목 전체의 기술적 지표와 함께 주가 및 거래량을 추적하고자 합니다. 우선 다우 구성종목인 골드만삭스(Goldman Sachs)의 주가와 해당 종목의 기술적 지표 하나부터 추적을 시작해 보겠습니다.
필진이 즐겨 쓰는 기술적 지표 중 하나는 RSI(Relative Strength Index, 상대강도지수)입니다. RSI는 종목이 과매수 또는 과매도 상태에 있는지를 측정하는 모멘텀 지표입니다. RSI가 30 근처 또는 그 이하로 떨어지면 해당 종목은 과매도 상태로 간주되며 매수 기회가 될 수 있습니다. 반대로 RSI가 70을 넘어서면 과매수 구간에 진입했음을 의미하며 매도 타이밍의 신호로 볼 수 있습니다.
골드만삭스그룹의 상대강도지수(RSI)
장중에는 다른 기술적 지표와 마찬가지로, 골드만삭스 주식의 수요와 공급에 따라 거래가 체결되면서 RSI 값이 계속 변동합니다. RedisTimeSeries 모듈을 활용하면 다음과 같은 핵심 질문들에 답할 수 있습니다.
- 지정한 시간 구간별로 기술적 지표의 최솟값과 최댓값은 무엇인가?
- 이를 통해 거래의 진입·청산 시점을 빠르게 포착할 수 있습니다.
- 지정한 시간 구간 동안 주가의 범위와 표준편차는 얼마인가?
- 범위와 표준편차는 해당 주식 가격의 변동성을 나타내는 지표입니다.
- 이 종목 거래에서 수익성 있는 진입가와 청산가는 얼마일까?
- 지정한 시간 구간 동안 전체 종목에 걸친 기술적 지표 값은 어떠한가?
RedisTimeSeries 쿼리를 사용하면 지정된 기간 동안의 RSI 최솟값·최댓값과 주가를 프로그래밍 방식으로 식별할 수 있습니다. 예를 들어, 장중 각 15분 구간마다 RSI의 최소·최대 값을 찾고 싶다고 가정해 보겠습니다. RSI가 30 근처에 도달하면 알림을 생성하거나, 매매를 실행하거나, 매수·매도 전에 다른 기술적 지표를 분석하는 복잡한 트레이딩 워크플로를 시작할 수 있습니다. 원하는 어떤 작업이든 RedisTimeSeries 데이터베이스에서 손쉽게 모델링할 수 있습니다.
주가와 기술적 지표를 위한 데이터 모델
RedisTimeSeries를 직접 경험하는 가장 쉬운 방법은 Docker 이미지를 실행하는 것입니다.
다음 명령으로 Docker 이미지를 받아 실행합니다:
docker run -p 6379:6379 -it --rm redis/redistimeseries
Redis 팀은 RedisTimeSeries 모듈 설계 시 시계열 하나당 단일 메트릭(single metric per time series)을 담도록 의도적인 결정을 내렸습니다. 이 단순한 데이터 모델 덕분에 데이터 삽입과 검색이 매우 빠릅니다. 필요할 때마다 새로운 시계열을 추가해도 기존 애플리케이션이 깨질 걱정이 없습니다. 즉, 데이터베이스 스키마나 애플리케이션에 영향을 주지 않으면서 새로운 데이터 소스를 손쉽게 추가할 수 있습니다.
RedisTimeSeries 컨테이너가 실행되면 Python으로 서버(올바른 IP 주소 또는 호스트명 확인 필요)에 다음과 같이 연결할 수 있습니다:
from redistimeseries.client import Client rts = Client(host='127.0.0.1', port=6379)
시계열 하나당 단일 메트릭이라는 설계 원칙에 따라, DJIA 각 종목의 장중 주가와 RSI를 각각의 시계열에 저장할 수 있습니다. 아래는 골드만삭스그룹(NYSE: GS)용 시계열 생성 예시입니다. 골드만삭스의 장중 RSI는 'DAILYRSI:GS'로 명명했으며, 각 시계열에는 여러 라벨(label)을 붙였습니다. 시계열에 라벨을 지정하면 라벨 기준으로 전체 키를 대상으로 조회할 수 있습니다.
rts.create('DAILYRSI:GS',
labels={ 'SYMBOL': 'GS'
, 'DESC':'RELATIVE_STRENGTH_INDEX'
, 'INDEX' :'DJIA'
, 'TIMEFRAME': '1_DAY'
, 'INDICATOR':'RSI'
, 'COMPANYNAME': 'GOLDMAN_SACHS_GROUP'})
다음은 골드만삭스의 장중 주가를 위한 'INTRADAYPRICES:GS'라는 시계열입니다:
rts.create('INTRADAYPRICES:GS',
labels={ 'SYMBOL': 'GS'
, 'DESC':'SHARE_PRICE'
, 'INDEX' :'DJIA'
, 'PRICETYPE':'INTRADAY'
, 'COMPANYNAME': 'GOLDMAN_SACHS_GROUP'})
다음으로, RSI 데이터에 대해 15분 단위 시간 프레임의 집계를 생성했습니다. (물론 트레이딩 요건에 맞춰 더 길거나 짧은 시간 프레임의 집계를 만들 수도 있습니다.) 이러한 집계를 통해 RSI의 첫 값, 마지막 값, 최솟값, 최댓값, 범위를 살펴볼 수 있습니다. 집계를 생성하려면 먼저 집계 값을 저장할 시계열을 만들고, 그 시계열을 집계 값으로 채우는 규칙(rule)을 생성합니다. 여기서는 15분 구간 내 RSI의 범위를 저장하기 위해 'DAILYRSI15MINRNG:GS'라는 시계열을 만들었습니다. 'createrule'은 원본 데이터('DAILYRSI:GS')에 대해 각 15분 시간 프레임마다 range 함수를 적용하고 그 결과를 'DAILYRSI15MINRNG:GS'로 집계합니다.
rts.create('DAILYRSI15MINRNG:GS',
labels={ 'SYMBOL': 'GS'
, 'DESC':'RELATIVE_STRENGTH_INDEX'
, 'INDEX' :'DJIA'
, 'TIMEFRAME': '15_MINUTES'
, 'AGGREGATION': 'RANGE'
, 'INDICATOR':'RSI'
, 'COMPANYNAME': 'GOLDMAN_SACHS_GROUP'})
rts.createrule('DAILYRSI:GS', 'DAILYRSI15MINRNG:GS', 'range', 900)
아래에서는 장중 각 15분 구간 동안 골드만삭스 주가의 범위와 표준편차를 계산하는 두 가지 규칙을 생성했습니다:
rts.create('INTRADAYPRICES15MINRNG:GS'
, labels={ 'SYMBOL': 'GS'
, 'DESC':'SHARE_PRICE'
, 'INDEX' :'DJIA'
, 'PRICETYPE':'RANGE'
, 'AGGREGATION': 'RANGE'
, 'DURATION':'15_MINUTES'
, 'COMPANYNAME': 'GOLDMAN_SACHS_GROUP'})
rts.createrule('INTRADAYPRICES:GS'
,'INTRADAYPRICES15MINRNG:GS'
,'range', 900)
rts.create('INTRADAYPRICES15MINSTDP:GS'
, labels={ 'SYMBOL': 'GS'
, 'DESC':'SHARE_PRICE'
, 'INDEX' :'DJIA'
, 'PRICETYPE':'STDDEV'
, 'AGGREGATION': 'STDDEV'
, 'DURATION':'15_MINUTES'
, 'COMPANYNAME': 'GOLDMAN_SACHS_GROUP'})
rts.createrule( 'INTRADAYPRICES:GS'
, 'INTRADAYPRICES15MINSTDP:GS'
, 'std.p', 900)
같은 방식으로 다우존스 산업평균지수(DJIA)를 구성하는 30개 종목 전체에 대한 시계열을 만들 수 있습니다.
데이터 수집 방법
RedisTimeSeries에 데이터를 넣는 방법은 두 가지입니다. TS.ADD 명령은 개별 주가나 기술적 지표 값을 하나씩 시계열에 추가합니다. 하지만 금융 시장 데이터는 거의 끊임없이 생성되므로 여러 샘플을 한 번에 넣어야 하는 경우가 많습니다. 이럴 때는 TS.MADD 메서드를 사용하는 것이 좋습니다. MADD 함수는 튜플 리스트를 인수로 받으며, 각 튜플은 시계열 키 이름, 타임스탬프, 값으로 구성됩니다:
다음 Python 명령으로 시계열 키에 데이터를 삽입할 수 있습니다:
rts.madd(<RSIIndicatorList>)
RedisTimeSeries 조회하기
RedisTimeSeries에서는 대부분의 작업이 그렇듯 조회 역시 간단합니다. 주가나 기술적 지표의 범위와 표준편차는 변동성의 척도입니다. 다음 쿼리를 실행하면 15분 구간별로 골드만삭스 주가의 가격 범위와 표준편차를 확인할 수 있습니다:
rts.range( 'INTRADAYPRICES15MINRNG:GS'
, from_time = 1603704600
, to_time = 1603713600)
이 범위 집계 쿼리는 다음과 같은 결과 집합을 반환합니다:
[(1603704600, 1.75999999999999), (1603705500, 0.775000000000006), (1603706400, 0.730000000000018), (1603707300, 0.449999999999989), (1603708200, 0.370000000000005), (1603709100, 1.01000000000002), (1603710000, 0.490000000000009), (1603710900, 0.89500000000001), (1603711800, 0.629999999999995), (1603712700, 0.490000000000009), (1603713600, 0.27000000000001)]
쿼리 결과를 보면, 2020년 10월 26일 오전 9시 30분(동부시간, 정수 타임스탬프 = 1603704600) 장 시작 직후 골드만삭스 주식은 $1.75의 범위로 거래될 만큼 변동성이 컸습니다. 이를 10월 26일 오전 9시 45분(타임스탬프 = 1603705500)부터 시작한 다음 15분 구간과 비교해 보면, 변동성이 낮아져 범위가 $0.77로 줄었습니다. 이후 15분 구간들에서도 골드만삭스 주식의 가격 범위는 계속 축소되었고, 장 초반 15분 구간에서 보였던 변동성 수준으로 되돌아오지 않았습니다.
RedisTimeSeries에서 얻은 이런 데이터는 대시보드와 차트로 손쉽게 시각화할 수 있습니다. 아래 차트는 15분 구간별 골드만삭스의 가격 범위를 보여줍니다:
다음 차트는 같은 데이터를 선 그래프 형태로 나타낸 것으로, 하루가 진행될수록 골드만삭스가 거래되는 가격 범위가 훨씬 좁아짐을 보여줍니다(x축은 정수 타임스탬프):
변동성의 또 다른 척도인 표준편차를 집계 함수로 사용해 비슷한 쿼리를 실행할 수도 있습니다:
rts.range( 'INTRADAYPRICES15MINSTDP:GS'
, from_time = 1603704600
, to_time = 1603713600)
[(1603704600, 0.54657783830434), (1603705500, 0.23201149395202), (1603706400, 0.196072381345986), (1603707300, 0.160267138157647), (1603708200, 0.116990621700049), (1603709100, 0.28043101744222), (1603710000, 0.144379900126934), (1603710900, 0.327611558618967), (1603711800, 0.163118915546951), (1603712700, 0.151417199675549), (1603713600, 0.0963889432843203)]
여기서 흥미로운 가능성이 열립니다. 예컨대 장중 RSI 값이 30에서 40 사이일 때 골드만삭스 주가가 얼마였는지 알고 싶다고 해봅시다. RSI 시계열과 주가 시계열을 함께 조회하면 수익성 있는 매수 진입점을 찾아낼 수 있습니다.
아래에서는 1605260100(2020년 11월 13일 오전 9시 35분 ET)부터 1605260940(같은 날 오전 9시 49분 ET)까지의 구간에서 골드만삭스의 RSI 값을 조회합니다.
dailyGSRSIValue = rts.range( 'DAILYRSI:GS'
, from_time = 1605260100
, to_time = 1605260940)
쿼리 결과, 1605260820(오전 9시 47분 ET) 시점에 RSI 값이 34.2996427544861이었음을 확인했습니다.
[(1605260100, 75.0305441024708), (1605260160, 81.6673948350152), (1605260220, 83.8852225932517), (1605260280, 85.9469082344746), (1605260340, 94.3803586011592), (1605260400, 92.2412262502652), (1605260460, 85.6867329371465), (1605260520, 87.9557361513823), (1605260580, 89.9407873066781), (1605260640, 57.1512452602676), (1605260700, 50.5638232111769), (1605260760, 35.2804436894564), (1605260820, 34.2996427544861), (1605260880, 54.5486275202972), (1605260940, 64.7593307385219)]
이제 RSI 쿼리에 사용한 것과 동일한 구간으로 골드만삭스 장중 주가를 조회하거나, RSI 값이 34.29였던 시점에 맞춰 쿼리를 프로그래밍 방식으로 조정할 수 있습니다. 아래는 RSI 쿼리와 같은 시간 프레임을 사용한 예시입니다.
dailyGSPrice = rts.range( 'INTRADAYPRICES:GS'
, from_time = 1605260100
, to_time = 1605260940)
쿼리는 지정된 구간의 골드만삭스 주가를 반환합니다. 1605260820(2020년 11월 13일 오전 9시 47분 ET) 시점의 가격은 $217.18이었습니다.
[(1605260100, 216.57), (1605260160, 216.73), (1605260220, 217.08), (1605260280, 217.17), (1605260340, 217.87), (1605260400, 218.05), (1605260460, 217.91), (1605260520, 218.0), (1605260580, 218.11), (1605260640, 218.02), (1605260700, 217.72), (1605260760, 217.22), (1605260820, 217.18), (1605260880, 217.46), (1605260940, 217.61)]
RedisTimeSeries는 여러 시계열을 한 번에 조회할 수 있는 강력한 기능을 제공합니다. TS.MGET 명령을 사용하면 필터를 적용해 여러 시계열을 동시에 조회할 수 있습니다. 앞서 이미 여러 시계열을 만들고 라벨을 붙여 두었으므로, 이제 그 라벨들을 필터로 활용해 시계열 전반을 조회할 수 있습니다.
다음 Python 코드는 "DESC"와 "TIMEFRAME"이라는 두 라벨을 기반으로 두 개의 필터를 적용합니다. "with_labels=False" 파라미터를 사용하면 결과 집합이 각 값의 라벨 없이 반환됩니다:
allRSIValues = rts.mget(filters=['DESC=RELATIVE_STRENGTH_INDEX','TIMEFRAME=1_DAY'], with_labels=False)
이 쿼리는 여러 종목에 걸친 최신 RSI 값을 아래와 유사한 형태로 반환합니다:
[{'DAILYRSI:BA': [{}, 1605261060, 62.2048922111768]}, {'DAILYRSI:CAT': [{}, 1605261060, 68.3834400302296]}, {'DAILYRSI:CRM': [{}, 1605261060, 59.2107333830133]}, {'DAILYRSI:CSCO': [{}, 1605261060, 52.7011052724688]}, {'DAILYRSI:CVX': [{}, 1605261060, 62.9890368832232]}, {'DAILYRSI:DOW': [{}, 1605261060, 73.597680480764]}, {'DAILYRSI:GS': [{}, 1605283140, 41.182852552541]}, {'DAILYRSI:IBM': [{}, 1605261060, 65.3742140862697]}, {'DAILYRSI:JPM': [{}, 1605261060, 77.7760292843745]}, {'DAILYRSI:KO': [{}, 1605261060, 26.8638381005608]}, {'DAILYRSI:MMM': [{}, 1605261060, 65.7852833683174]}, {'DAILYRSI:MRK': [{}, 1605261060, 38.9991886598036]}, {'DAILYRSI:UNH': [{}, 1605261060, 74.2672428885775]}, {'DAILYRSI:VZ': [{}, 1605261060, 33.177554436462]}, {'DAILYRSI:WBA': [{}, 1605261060, 47.3877762365391]}]
만약 "with_labels=True"로 설정했다면, 결과에는 아래와 같이 각 시계열에 붙은 모든 라벨이 함께 포함됩니다:
[{'DAILYRSI:BA': [{'SYMBOL': 'BA', 'DESC': 'RELATIVE_STRENGTH_INDEX', 'INDEX': 'DJIA', 'TIMEFRAME': '1_DAY', 'INDICATOR': 'RSI', 'COMPANYNAME': 'BOEING'}, 1605261060, 62.2048922111768]}, {'DAILYRSI:CAT': [{'SYMBOL': 'CAT', 'DESC': 'RELATIVE_STRENGTH_INDEX', 'INDEX': 'DJIA', 'TIMEFRAME': '1_DAY', 'INDICATOR': 'RSI', 'COMPANYNAME': 'CATERPILLAR'}, 1605261060, 68.3834400302296]}, {'DAILYRSI:CRM': [{'SYMBOL': 'CRM', 'DESC': 'RELATIVE_STRENGTH_INDEX', 'INDEX': 'DJIA', 'TIMEFRAME': '1_DAY', 'INDICATOR': 'RSI', 'COMPANYNAME': 'SALESFORCE'}, 1605261060, 59.2107333830133]}]
마무리
이번 포스트에서는 시계열 데이터에 의존하는 금융 애플리케이션을 유연하게 구축할 수 있도록 해주는 RedisTimeSeries의 다양한 명령 몇 가지를 살펴보았습니다. 예컨대 주식 트레이더는 수십 가지 변수를 기반으로 실시간으로 고위험 의사결정을 내려야 합니다. RedisTimeSeries는 스키마리스(schemaless) 방식이므로 스키마를 미리 정의하지 않고도 데이터를 로드할 수 있고, 즉석에서 새로운 필드를 추가하거나 비즈니스 환경이 변할 때 데이터 모델을 변경할 수도 있습니다. 뛰어난 실시간 성능과 개발자 친화적인 경험 덕분에 시계열 데이터 작업이 즐거워질 것입니다!
이 포스트의 샘플 코드는 GitHub에서 확인할 수 있으며, RedisTimeSeries에 대해 더 자세히 알아보려면 공식 문서를 참고하세요.