Skip to content

블로그

기술적 분석 10장: 같은 신호가 산업마다 다르게 작동하는 이유

산업별 기술적 신호 분석

9장에서는 여러 조건을 합친 복합 신호의 평균수익률이 +7.03%였지만, 단일 거래량 돌파의 +9.75%를 앞서지 못했습니다. 이제 질문을 한 단계 바꿉니다. 복합 신호의 평균을 전체 종목에 적용하기보다, 어떤 산업과 시장 국면에서 결과가 달라지는가를 확인합니다. 기술적 신호는 모든 산업에서 같은 의미를 갖지 않으며, 반도체처럼 추세가 빠르게 이어지는 산업과 바이오처럼 급등락이 큰 산업에 같은 조건을 적용하면 결과가 달라질 수 있습니다.

selected = df[
(df["composite_entry"] == 1) &
(df["date"] >= "20260101")
].copy()
industry_result = (
selected.groupby("industry")
.agg(
events=("code", "size"),
positive_rate=("forward_return_20d", lambda x: (x > 0).mean()),
mean_return=("forward_return_20d", "mean"),
median_return=("forward_return_20d", "median"),
)
.query("events >= 5")
.sort_values("mean_return", ascending=False)
)

검증 구간에서 복합 신호가 5회 이상 발생한 산업 중 일부는 다음과 같습니다.

산업신호 수양수 비율평균 수익률중앙값
전자부품 제조업12962.8%+14.55%+10.53%
반도체 제조업4759.6%+14.07%+11.65%
기타 금융업16261.7%+9.53%+3.53%
기타 화학제품 제조업7048.6%+1.02%-1.67%
의약품 제조업4346.5%+6.46%-3.08%
소프트웨어 개발 및 공급업4240.5%+4.42%-3.94%

전자부품과 반도체는 양수 비율이 59.6~62.8%, 중앙값이 +10%를 넘었습니다. 반면 소프트웨어와 의약품은 평균은 플러스였지만 중앙값이 각각 -3.94%, -3.08%였습니다. 같은 복합 신호라도 산업에 따라 전형적인 결과가 달라지고, 평균만 보면 이 차이를 놓치게 됩니다.

시장 국면도 함께 봐야 합니다. 이번 분석에서는 코스피의 20일 수익률을 양수·음수로 나누고, 각 신호의 이후 수익률을 다시 계산합니다. 개별 종목 신호만으로는 시장 전체의 위험 선호 변화를 설명할 수 없기 때문입니다.

코스피 20일 모멘텀신호 수20일 양수 비율평균 수익률중앙값
양수3,39051.1%+5.57%+1.28%
음수1,41547.5%+3.78%0.00%

이번 결과에서 “이 지표의 평균 수익률은 얼마인가?”보다 더 좋은 질문은 “어떤 산업과 시장 상태에서 이 지표가 작동했는가?”입니다. 전자부품·반도체처럼 평균과 중앙값이 함께 높은 산업은 우선 검토 대상이 될 수 있지만, 소프트웨어·의약품처럼 평균과 중앙값이 엇갈리는 산업은 개별 종목과 손실 관리가 더 중요합니다.

코스피 20일 모멘텀이 양수인 국면에서는 양수 비율 51.1%, 평균 +5.57%였고, 음수 국면에서는 각각 47.5%, +3.78%로 낮아졌습니다. 시장 국면이 우호적일 때 신호의 결과도 개선됐지만, 양수 국면에서도 양수 비율은 절반을 조금 넘는 수준입니다. 시장 필터는 확률을 보완할 뿐, 결과를 보장하지 않습니다.

표본 수가 적은 산업의 높은 수익률은 과대해석하지 않아야 하며, 여러 산업을 동시에 비교할 때 우연히 좋은 그룹을 고르는 문제도 주의해야 합니다. 다음 장에서는 이 조건을 기준일 종목 목록으로 바꿀 때 실제 후보가 어떻게 만들어지는지 확인합니다.

산업별 원자료는 industry_summary.csv, 시장 국면 결과는 regime_summary.csv에서 확인할 수 있습니다.

기술적 분석 11장: 실제 데이터로 오늘의 후보 종목 찾기

기술적 분석 종목 스크리닝

앞 장에서는 복합 신호가 산업과 시장 국면에 따라 다른 결과를 보인다는 점을 확인했습니다. 이번에는 그 신호를 2026년 9월 4일 기준 종목 전체에 적용해 실제 후보군을 추출합니다. 여기서 중요한 질문은 “점수가 높은 종목이 곧 매수 종목인가?”가 아니라 “어떤 조건을 통과한 후보를 다음 분석 대상으로 좁힐 수 있는가?”입니다. 이 결과는 매수 추천 목록이 아니라, 동일한 규칙으로 다시 확인할 수 있는 분석 후보 목록입니다.

다섯 조건 중 네 개 이상을 만족하는 종목을 후보로 분류했습니다.

df["score"] = (
(df["close"] > df["ma20"]).astype(int)
+ (df["ma20"] > df["ma60"]).astype(int)
+ df["rsi14"].between(40, 70).astype(int)
+ (df["macd"] > df["macd_signal"]).astype(int)
+ (df["volume_ratio20"] > 1).astype(int)
)
latest = df[df["date"] == "20260904"]
screen = latest[latest["score"] >= 4].sort_values(
["score", "volume_ratio20"], ascending=False
)
종목시장종가RSI거래량 비율점수
카카오페이KOSPI47,30055.41.335
현대약품KOSPI7,95069.02.885
KCCKOSPI482,00054.71.295
디아이KOSPI31,25067.81.835
한화투자증권KOSPI5,10057.23.285
써니전자KOSPI2,18565.01.665
파인엠텍KOSDAQ9,35055.12.175
신스틸KOSDAQ2,10569.13.205

전체 30개 후보는 latest_screen.csv에서 확인할 수 있습니다. 기준일이 바뀌면 후보도 바뀌므로, 이 목록의 종목명을 고정해 추천처럼 사용해서는 안 됩니다.

이번 기준일 후보는 총 30개였고, 모두 5개 조건을 통과했습니다. 시장별로는 KOSPI 15개, KOSDAQ 15개였습니다. 따라서 이 결과에서 점수 5점과 4점의 우열을 비교할 수는 없고, 거래량 비율이나 산업·시장 구성을 추가 기준으로 사용해야 합니다.

스크리닝의 역할은 분석 대상을 줄이는 것입니다. 점수 5점이라고 해서 바로 매수할 수 있는 것은 아닙니다. 후보가 어떤 산업에 몰렸는지, 실적·현금흐름·수급·밸류에이션이 뒷받침되는지, 그리고 기준일 이후에도 조건이 유지되는지를 확인해야 합니다.

다음 장에서는 후보를 고르는 규칙을 실제 포트폴리오 전략으로 바꿀 때 성과와 낙폭이 어떻게 달라지는지 비교합니다.

기술적 분석 12장: 백테스트에서 살아남는 전략은 무엇인가?

기술적 분석 전략 백테스트

앞 장의 후보 목록은 분석 대상을 줄여줄 뿐, 실제 투자 성과를 보여주지 않습니다. 기술적 분석의 마지막 질문은 “어떤 지표가 가장 유명한가?”가 아니라 “같은 조건과 비용을 적용했을 때 어떤 전략이 남는가?”입니다. 이번 장에서는 300개 종목에 같은 기간과 같은 비용 가정을 적용해 이동평균, MACD, 복합 신호를 비교합니다.

state = df["close"] > df["ma20"]
state &= df["ma20"] > df["ma60"]
# 당일 신호로 다음 거래일부터 보유
position = state.groupby(df["code"]).shift(1).fillna(False)
gross_return = position * df["return_1d"]
turnover = position.groupby(df["code"]).diff().abs().fillna(position)
net_return = gross_return - turnover * 0.0015

실제 포트폴리오 계산에서는 해당 날짜에 보유 중인 종목의 수익률을 동일 비중으로 평균냈습니다. 거래비용은 포지션 변경 시 0.15%를 차감했습니다.

검증 구간은 2026년 1월 1일부터 9월 4일까지입니다.

전략누적수익률연환산수익률변동성최대낙폭Sharpe 근사
MA 추세+63.55%+111.03%52.43%-31.05%2.12
MACD 추세+58.15%+100.55%50.36%-33.52%2.00
복합 신호+5.11%+7.86%63.69%-46.21%0.12
코스피+33.62%+55.69%61.14%-38.63%0.91

기술적 분석 전략 누적자산

이번 검증 구간에서는 MA와 MACD 추세 전략이 코스피를 앞섰습니다. MA 추세는 코스피보다 누적수익률이 높고 최대낙폭도 작았지만, -31.05%의 하락을 감수해야 했습니다. 반면 복합 신호 전략은 조건이 더 많았음에도 누적수익률이 +5.11%에 그쳤고 최대낙폭은 -46.21%였습니다.

이 표는 9장의 사건별 결과와 12장의 포트폴리오 결과가 다를 수 있다는 점도 보여줍니다. 복합 신호는 사건별 20일 평균수익률이 +7.03%였지만, 실제 진입·보유·거래비용을 적용한 전략의 누적수익률은 +5.11%였습니다. 신호가 발생한 사례의 평균과 투자자가 보유한 포트폴리오의 성과는 진입 시점, 신호 중복, 보유 종목 수, 거래비용에 의해 달라집니다.

이번 데이터에서 확인한 핵심은 다음과 같습니다.

  1. 거래량 돌파와 MACD 골든크로스는 20일 선행수익률에서 상대적으로 강했습니다.
  2. RSI 회복과 볼린저 하단 반등은 평균보다 중앙값이 낮아 일부 급등 사례의 영향을 크게 받았습니다.
  3. 높은 변동성 종목은 복합 신호가 나와도 양수 비율과 중앙값이 낮았습니다.
  4. 같은 기술 신호라도 산업별 결과가 달랐습니다.
  5. 조건을 많이 결합한다고 전략 성과가 자동으로 좋아지지는 않았습니다.
  6. 높은 수익률에는 항상 최대낙폭과 표본 편향을 함께 표시해야 합니다.

따라서 이번 분석에서 얻은 실전적인 순서는 다음과 같습니다.

  1. MA·MACD로 상승 추세에 있는 종목을 먼저 구분합니다.
  2. 거래량 돌파와 OBV로 추세에 거래량 확인이 붙었는지 봅니다.
  3. RSI·볼린저 반등은 단독 매수 신호가 아니라 시장·산업 조건과 함께 해석합니다.
  4. ATR로 종목별 포지션 크기와 손실 폭을 조정합니다.
  5. 산업·시장 국면으로 후보를 다시 나눈 뒤, 백테스트에서 거래비용과 최대낙폭을 확인합니다.

기술적 분석은 미래를 맞히는 주문이 아니라, 가격·거래량에서 반복적으로 관찰되는 조건을 정의하고 그 조건의 성과와 실패 범위를 측정하는 방법입니다. 이번 결과는 이 표본과 검증 기간에 대한 증거이지, 어떤 지표도 항상 작동한다는 보장은 아닙니다.

전체 원자료는 OHLCV, 신호 결과, 전략 결과, Python 코드에서 확인할 수 있습니다. 과거 검증 결과는 미래 수익을 보장하지 않으며, 실제 투자 전에는 체결 가능성·세금·슬리피지·생존자 편향을 추가로 검토해야 합니다.

기술적 분석 1장: 지표는 미래 수익률을 구분하는가?

차트와 기술적 분석 데이터

기술적 분석에서 먼저 답해야 할 질문은 “이 지표가 유명한가?”가 아니라 “신호가 발생한 뒤의 수익률이 신호가 없을 때와 달라지는가?”입니다. 한 종목의 차트에서 그럴듯해 보이는 패턴만으로는 이 질문에 답할 수 없습니다.

이번 분석은 같은 표본과 같은 규칙으로 이동평균, RSI, MACD, 볼린저 밴드, 거래량, OBV 신호를 만들고, 신호 발생 뒤 5·20·60거래일 수익률을 비교합니다. 이 장에서는 분석에 사용할 데이터와 계산 규칙을 고정한 뒤, 전체 지표 결과를 먼저 훑어봅니다. 이후 각 장에서 지표를 하나씩 정의하고, 이 결과가 어떤 조건에서 유지되거나 무너지는지 확인합니다.

따라서 이 글의 결론은 “데이터를 공개했다”가 아닙니다. 어떤 신호가 이후 수익률과 더 강하게 연결됐고, 어떤 신호는 평균수익률만 좋아 보였는지를 구분하는 것이 이 장의 목적입니다.

분석 기준일은 2026년 9월 4일입니다. 기준일 거래대금이 큰 일반 주식 300개를 표본으로 선정하고, 2024년 1월 1일부터 2026년 9월 4일까지의 일봉을 사용했습니다. ETF, ETN, 인버스, 레버리지, 채권형 상품은 제외했습니다.

항목
표본 종목 수300개
OHLCV 원자료 행 수185,309개
120일 워밍업 이후 지표 행 수150,419개
가격 데이터 기간2024-01-01 ~ 2026-09-04
학습 구간2024-01-01 ~ 2025-12-31
검증 구간2026-01-01 ~ 2026-09-04

표본은 모든 상장 종목을 대표하지 않습니다. 거래대금 상위 종목을 사용했기 때문에 유동성이 낮은 종목의 체결 문제를 줄이는 대신, 대형주와 관심 종목에 유리한 표본 편향이 생깁니다. 이 한계를 숨기지 않고 원자료와 조건을 함께 공개합니다.

import pandas as pd
df = pd.read_csv(
"ohlcv.csv",
dtype={"code": str},
parse_dates=["date"],
)
df = df.sort_values(["code", "date"])
df["return_1d"] = df.groupby("code")["close"].pct_change()
df["return_20d"] = df.groupby("code")["close"].pct_change(20)
print(df.shape)
print(df[["code", "date", "close", "return_20d"]].tail())

이 코드의 핵심은 종목별로 날짜를 정렬한 뒤 수익률을 계산하는 것입니다. 전체 데이터를 날짜만 기준으로 정렬하면 서로 다른 회사의 가격이 이어지는 오류가 생깁니다. 실제 지표 계산과 신호 정의는 공개한 Python 파일에서 재현할 수 있고, 이 장에서는 그 결과 파일을 같은 기준으로 읽어 비교합니다.

지표 신호와 20일 수익률의 관계

Section titled “지표 신호와 20일 수익률의 관계”

먼저 중간 기간인 20거래일을 기준으로 비교합니다. 검증 구간은 2026년 1월 1일부터 9월 4일까지이며, 신호가 발생한 날의 종가부터 20거래일 뒤 종가까지의 수익률을 계산했습니다. 신호 당일의 수익률을 사용하지 않았기 때문에 신호 이후의 결과만 측정합니다.

import pandas as pd
results = pd.read_csv(
"event_results.csv",
encoding="utf-8-sig",
)
validation_20d = (
results.query(
"sample == '검증' and horizon_days == 20 "
"and signal != '복합 신호 진입'"
)
.assign(
positive_rate=lambda x: x["positive_rate"] * 100,
mean_return=lambda x: x["mean_return"] * 100,
median_return=lambda x: x["median_return"] * 100,
)
.loc[:, [
"signal", "events", "positive_rate",
"mean_return", "median_return",
]]
.sort_values("mean_return", ascending=False)
)
print(validation_20d.round(2).to_string(index=False))

실행 결과는 다음과 같습니다.

신호발생 횟수양수 비율평균 수익률중앙값 수익률
거래량 돌파2,72653.4%+9.75%+2.51%
MACD 골든크로스1,86855.1%+6.98%+2.72%
OBV 확인2,65353.8%+6.02%+2.03%
볼린저 하단 반등1,01549.4%+4.94%-0.27%
RSI 회복1,25847.1%+2.14%-0.91%

기술적 신호의 20일 선행수익률

이 표에서 가장 먼저 보이는 것은 평균수익률만으로 신호의 질을 판단하면 안 된다는 점입니다.

  • 거래량 돌파는 평균 수익률이 +9.75%로 가장 높고 중앙값도 +2.51%였습니다. 일부 대박 사례만으로 평균이 올라간 것이 아니라, 전형적인 신호에서도 수익률이 양수 쪽에 놓였다는 뜻입니다.
  • MACD 골든크로스는 양수 비율이 55.1%로 가장 높고 중앙값도 +2.72%였습니다. 이번 검증 구간에서는 추세 전환 신호 중 결과의 방향성이 가장 안정적이었습니다.
  • OBV 확인도 평균 +6.02%, 중앙값 +2.03%를 기록했습니다. 거래량 돌파와 함께 거래량을 확인하는 신호가 가격 반등만 보는 신호보다 일관된 결과를 보였다는 해석이 가능합니다.
  • RSI 회복과 볼린저 하단 반등은 평균수익률은 플러스였지만 중앙값이 각각 -0.91%, -0.27%였습니다. 소수의 큰 상승 사례가 평균을 끌어올렸을 가능성이 있어, “과매도 후 반등”을 반복적으로 기대하기에는 근거가 약합니다.

다만 이 결과가 신호가 발생하면 무조건 매수해야 한다는 뜻은 아닙니다. 가장 높은 양수 비율도 55.1%에 그쳤고, 모든 신호의 하위 25% 수익률은 마이너스였습니다. 즉 기술적 지표는 방향을 맞히는 단독 예언 도구라기보다, 어떤 상황에서 상대적으로 유리한 후보를 좁히는 필터로 해석하는 편이 적절합니다.

또한 이 표는 신호별 사건을 독립적으로 집계한 결과입니다. 신호가 겹치는 종목을 하나의 포트폴리오로 운용했을 때의 거래비용, 보유 비중, 최대낙폭은 아직 반영하지 않았습니다. 그래서 다음 장부터는 개별 지표의 신호 정의를 확인하고, 마지막 장에서 실제 전략 성과와 리스크를 별도로 비교합니다.

이번 결과는 다음 순서를 정하는 기준이 됩니다.

  • 이동평균과 MACD처럼 추세를 따라가는 신호는 왜 상대적으로 안정적이었는가
  • RSI와 볼린저 밴드의 반등 신호는 어떤 시장 국면에서 실패했는가
  • 거래량 돌파와 OBV 확인은 같은 정보를 반복해서 보는 것인가, 서로 보완하는가
  • 개별 신호의 관계가 실제 종목 스크리닝과 포트폴리오 백테스트에서도 유지되는가

다음 장에서는 이동평균선 신호를 전략으로 바꿔, 단순히 수익률이 높았다는 사실을 넘어 보유 시점과 최대낙폭까지 확인합니다.

원본은 OHLCV CSV, 종목 유니버스, 전체 데이터 설명에서 확인할 수 있습니다. 전체 재현 코드는 technical_analysis.py로 제공합니다.

기술적 분석 2장: 이동평균 추세 조건은 수익을 만드는가?

이동평균 기반 추세 분석

1장에서는 MACD와 거래량 신호가 RSI·볼린저 반등보다 20일 수익률에서 상대적으로 나은 결과를 보였습니다. 이번 장에서는 그보다 단순한 추세 조건, 즉 종가 > MA20 > MA60을 계속 보유하는 전략으로 바꿔도 결과가 유지되는지 확인합니다. 사건별 수익률과 실제 포트폴리오 성과는 다를 수 있으므로, 다음 거래일 진입·거래비용·최대낙폭까지 함께 계산합니다.

df = df.sort_values(["code", "date"])
df["ma20"] = df.groupby("code")["close"].transform(
lambda x: x.rolling(20).mean()
)
df["ma60"] = df.groupby("code")["close"].transform(
lambda x: x.rolling(60).mean()
)
df["ma_trend"] = (
(df["close"] > df["ma20"]) &
(df["ma20"] > df["ma60"])
)
# 신호가 만들어진 다음 거래일부터 보유
df["position"] = df.groupby("code")["ma_trend"].shift(1).fillna(False)
df["strategy_return"] = df["position"] * df["return_1d"]

신호 당일의 종가 수익률을 사용하지 않고 다음 거래일부터 포지션을 적용했습니다. 이것만으로도 백테스트의 과도한 낙관을 상당히 줄일 수 있습니다.

2026년 1월 1일부터 9월 4일까지의 결과는 다음과 같습니다. 거래비용은 포지션이 바뀔 때 0.15%를 차감했습니다.

전략누적수익률연환산수익률최대낙폭Sharpe 근사
MA 추세+63.55%+111.03%-31.05%2.12
코스피+33.62%+55.69%-38.63%0.91

추세 전략과 코스피의 누적자산

이 차트는 검증 구간인 2026년 1월 1일부터 9월 4일까지, 시작 자산을 1로 놓고 매일의 수익률을 누적한 결과입니다. 가로축은 날짜이고 세로축은 누적자산입니다. 예를 들어 선이 1.64에서 끝나면 시작 자산 1이 1.64가 되었다는 뜻이므로 누적수익률은 약 +64%입니다. 하루 수익률이나 특정 종목의 주가가 아니라 포트폴리오 전체의 복리 성과를 그린 선입니다.

차트의 선은 다음과 같이 구성됩니다. 여기서 범례의 Composite는 composition(구성)이라는 별도 지표가 아니라, 여러 조건을 합친 복합 신호를 뜻합니다.

범례의미실제 보유 구성
MA trend종가 > MA20 > MA60인 상승 추세조건을 만족한 종목을 동일 비중으로 보유. 검증 구간 평균 90.7개
MACD trendMACD 골든크로스가 발생한 뒤 20거래일 동안 활성화활성 신호 종목을 동일 비중으로 보유. 평균 187.1개
Composite추세·RSI·MACD·거래량 조건을 모두 만족한 복합 신호종가 > MA20, MA20 > MA60, RSI 40~70, MACD > 시그널, 거래량 비율 > 1인 종목을 동일 비중으로 보유. 평균 15.4개
KOSPI비교 기준인 코스피코스피 일별 수익률을 그대로 누적한 벤치마크

세 전략 모두 기준일의 신호를 다음 거래일부터 적용하고, 포지션이 바뀔 때 거래비용 0.15%를 반영했습니다. 따라서 ‘구성’은 단순히 어떤 지표를 계산했는지가 아니라, 어떤 종목을 언제 포트폴리오에 넣고, 보유 종목 사이에 자금을 어떻게 나눴는지를 의미합니다. 조건을 만족하는 종목이 여러 개면 각 종목의 수익률을 동일 비중으로 평균내며, 조건을 만족하는 종목이 없으면 해당 날짜의 전략 수익률은 0으로 처리합니다.

이제 선의 높이보다 선 사이의 간격과 중간 하락을 봐야 합니다. MA 추세선의 검증 구간 끝값은 약 1.64로 코스피의 약 1.34보다 높지만, 중간에 크게 꺾인 구간도 있습니다. 이 중간 하락이 최대낙폭 -31.05%로 이어졌습니다. 즉 끝점은 “얼마나 벌었는가”, 선이 가장 깊게 내려간 폭은 “그 결과를 얻는 동안 얼마나 잃을 수 있었는가”를 보여줍니다. Composite 선이 거의 1 부근에 머무는 것은 조건이 더 엄격해 보인다는 이유만으로 성과가 좋아지는 것은 아니며, 적은 보유 종목 수와 신호 공백의 영향까지 함께 봐야 한다는 의미입니다.

MA 추세 전략은 검증 구간에서 코스피보다 29.93%포인트 높은 누적수익률을 기록했고, 최대낙폭도 코스피보다 7.58%포인트 작았습니다. 다만 평균 보유 종목 수가 약 90.7개인 분산 포트폴리오였고, 그 상태에서도 최대낙폭은 -31.05%였습니다. “수익률이 높았다”는 문장만으로는 부족하며, 투자자는 이 하락을 실제로 유지할 수 있는지도 판단해야 합니다.

이번 결과에서 얻는 구체적인 인사이트는 이동평균선이 “돌파 순간을 맞히는 신호”라기보다 상승 상태에 있는 종목을 보유하고 하락 상태의 종목을 비우는 필터로 작동했다는 점입니다. 1장의 신호별 평균수익률을 전략으로 옮겼을 때도 성과가 이어졌지만, -31.05%의 낙폭이 남았으므로 추세 필터가 위험을 제거한 것은 아닙니다. 또한 2026년 검증 구간의 시장 방향성과 표본 구성에 영향을 받은 결과이므로 다른 기간에도 같은 우위가 반복되는지는 별도 검증이 필요합니다.

다음 장에서는 RSI 30 이하를 매수 신호로 사용하는 평균회귀 발상이 실제로 통하는지 확인합니다.

결과 원본은 전략 결과 CSV, 누적자산 CSV에서 확인할 수 있습니다.

기술적 분석 3장: RSI 30 이하면 정말 매수 기회인가?

RSI 평균회귀 분석

1장에서는 RSI 회복 신호의 20일 평균수익률이 +2.14%였지만 중앙값은 -0.91%, 양수 비율은 47.1%였습니다. 이번 장에서는 왜 “RSI 30 이하 = 매수 기회”라는 설명이 반복해서 실패할 수 있는지, 과매도 수준이 아니라 회복 신호를 기준으로 다시 확인합니다. 강한 하락 추세에서는 RSI가 30 아래에 오래 머물 수 있으므로, 30 아래에서 다시 30을 상향 돌파한 날을 신호로 정의했습니다.

change = df.groupby("code")["close"].diff()
gain = change.clip(lower=0)
loss = -change.clip(upper=0)
avg_gain = gain.groupby(df["code"]).transform(lambda x: x.rolling(14).mean())
avg_loss = loss.groupby(df["code"]).transform(lambda x: x.rolling(14).mean())
df["rsi14"] = avg_gain / (avg_gain + avg_loss) * 100
df["rsi_recovery"] = (
(df.groupby("code")["rsi14"].shift(1) <= 30) &
(df["rsi14"] > 30)
)
df["forward_return_20d"] = (
df.groupby("code")["close"].shift(-20) / df["close"] - 1
)

검증 구간에서 RSI 회복 신호는 1,258회 발생했습니다.

신호발생 횟수20일 양수 비율평균 수익률중앙값
RSI 30 상향 회복1,258회47.1%+2.14%-0.91%

평균 수익률은 양수였지만 중앙값은 -0.91%였고, 양수 비율도 47.1%로 절반에 미치지 못했습니다. 25% 분위수는 -14.56%였습니다. 일부 큰 반등이 평균을 끌어올렸을 뿐, 전형적인 신호가 손실을 피했다고 보기는 어렵습니다. 같은 검증 구간에서 MACD 골든크로스는 양수 비율 55.1%, 중앙값 +2.72%였기 때문에 RSI 회복의 약점이 더 분명해집니다.

이번 결과에서 RSI는 단독 매수 신호라기보다 하락 압력이 완화됐는지 확인하는 보조 지표로 쓰는 편이 낫습니다. RSI 회복만으로는 부족하므로 다음 조건을 추가했을 때 결과가 개선되는지 별도 검증해야 합니다.

  • 종가가 MA20 위에 있는가
  • 20일 수익률이 양수인가
  • 거래량이 평소보다 증가했는가
  • 산업 전체가 같은 방향으로 움직이는가

RSI 30이라는 숫자만 보고 매수하면 하락 추세 중간의 일시적 반등을 잡을 가능성이 큽니다. 따라서 평균수익률이 플러스라는 이유만으로 평균회귀 전략을 채택하기보다, 손절 기준·보유 기간·시장 국면을 함께 정해야 합니다. 다음 장에서는 같은 검증 구간에서 더 높은 양수 비율과 중앙값을 보인 MACD 골든크로스가 실제 추세 전략으로도 이어지는지 확인합니다.

전체 결과는 신호별 결과 CSV, 계산 코드는 technical_analysis.py에서 확인할 수 있습니다.

기술적 분석 4장: MACD 골든크로스는 언제 유효한가?

MACD 골든크로스 분석

2장에서는 MA20과 MA60의 추세 상태를 보유 전략으로 바꿨을 때 누적수익률 +63.55%를 기록했습니다. 1장의 개별 신호 비교에서도 MACD 골든크로스는 RSI 회복보다 양수 비율과 중앙값이 높았습니다. 이번 장에서는 이 관계가 단순한 사건별 통계에 그치지 않고, MACD 상태를 계속 보유하는 전략에서도 이어지는지 확인합니다.

MACD 골든크로스는 MACD 선이 Signal 선을 아래에서 위로 통과하는 현상입니다. 상승 전환을 보여주는 것처럼 보이지만, 이동평균에서 파생된 지표이므로 신호가 발생할 때 이미 주가가 상당히 움직였을 수 있습니다.

g = df.sort_values(["code", "date"]).copy()
g["ema12"] = g.groupby("code")["close"].transform(
lambda x: x.ewm(span=12).mean()
)
g["ema26"] = g.groupby("code")["close"].transform(
lambda x: x.ewm(span=26).mean()
)
g["macd"] = g["ema12"] - g["ema26"]
g["macd_signal"] = g.groupby("code")["macd"].transform(
lambda x: x.ewm(span=9).mean()
)
g["macd_golden_cross"] = (
(g.groupby("code")["macd"].shift(1) <= g.groupby("code")["macd_signal"].shift(1))
& (g["macd"] > g["macd_signal"])
)

검증 구간에서 MACD 골든크로스는 1,868회 발생했습니다.

신호발생 횟수20일 양수 비율평균 수익률중앙값
MACD 골든크로스1,868회55.1%+6.98%+2.72%

RSI 회복보다 양수 비율과 중앙값이 모두 높았습니다. 평균수익률뿐 아니라 양수 비율과 중앙값도 함께 개선됐기 때문에, 최소한 이번 표본과 기간에서는 MACD 골든크로스가 단순 과매도 회복보다 안정적인 결과를 보였습니다.

MACD 상태를 20일 동안 보유하는 추세 전략으로 바꾸면 검증 구간 누적수익률은 +58.15%, 최대낙폭은 -33.52%였습니다. 코스피의 누적수익률 +33.62%보다 높았지만, 손실 구간은 여전히 컸습니다.

MACD 골든크로스의 사건별 결과는 평균 +6.98%, 중앙값 +2.72%, 양수 비율 55.1%였습니다. 이 신호를 상태 기반 전략으로 바꾸자 누적수익률 +58.15%와 Sharpe 근사 2.00이 나왔습니다. 즉 신호가 발생한 순간의 통계적 우위가 포트폴리오 수준에서도 어느 정도 이어졌지만, 최대낙폭 -33.52%가 남아 “수익률이 높으면 위험도 낮다”는 결론으로 확장할 수는 없습니다.

MACD는 “오늘 사면 내일 오른다”는 단기 예측 도구가 아니라 방향이 바뀌었는지 확인하는 추세 신호에 가깝습니다. 따라서 다음 조건을 함께 기록해야 합니다.

  • 신호 이전 20일 수익률
  • 신호일의 거래량 비율
  • MA20과 MA60의 방향
  • 신호 후 최대 하락폭

평균 수익률이 높아도 특정 몇 개 종목의 급등 때문일 수 있습니다. 그래서 평균과 중앙값, 양수 비율을 동시에 공개했습니다.

다음 장에서는 평균회귀 성격이 강한 볼린저 밴드 하단 반등이 RSI와 같은 약점을 보이는지 비교합니다.

결과 원본은 신호별 결과 CSV, 전략 결과 CSV에서 확인할 수 있습니다.

기술적 분석 5장: 볼린저 밴드 하단 이탈은 반등 신호인가?

볼린저 밴드 분석

3장의 RSI 회복과 이번 볼린저 하단 반등은 모두 “많이 하락했으니 되돌아오지 않을까?”라는 평균회귀 가설에서 출발합니다. RSI 회복은 20일 중앙값 -0.91%였고, 이번 장에서는 가격이 밴드 안으로 돌아온 사건도 같은 약점을 보이는지 확인합니다.

볼린저 밴드 하단에 닿으면 주가가 싸졌다고 생각하기 쉽습니다. 그러나 하단 밴드를 따라 내려가는 강한 하락 추세에서는 하단 접촉만으로 반등을 예측할 수 없습니다. 이번 글에서는 전일 종가가 하단 밴드 아래였고 당일 종가가 다시 밴드 안으로 들어온 경우를 반등 신호로 정의했습니다.

g = df.sort_values(["code", "date"]).copy()
g["bb_middle"] = g.groupby("code")["close"].transform(
lambda x: x.rolling(20).mean()
)
std20 = g.groupby("code")["close"].transform(lambda x: x.rolling(20).std())
g["bb_upper"] = g["bb_middle"] + 2 * std20
g["bb_lower"] = g["bb_middle"] - 2 * std20
previous_close = g.groupby("code")["close"].shift(1)
previous_lower = g.groupby("code")["bb_lower"].shift(1)
g["bb_rebound"] = (previous_close <= previous_lower) & (
g["close"] > g["bb_lower"]
)

검증 구간에서 볼린저 하단 반등은 1,015회 발생했습니다.

신호발생 횟수20일 양수 비율평균 수익률중앙값
볼린저 하단 반등1,015회49.4%+4.94%-0.27%

평균은 +4.94%였지만 중앙값은 -0.27%, 양수 비율은 49.4%였습니다. 25% 분위수도 -13.11%였습니다. 일부 큰 반등 사례가 평균을 높였지만, 절반 이상의 전형적인 사례가 안정적인 수익으로 이어졌다고 보기는 어렵습니다. RSI 회복의 평균 +2.14%보다 높아 보이지만, 중앙값과 양수 비율은 모두 약해 평균 차이만으로 우위를 주장할 수 없습니다.

이번 결과에서 볼린저 밴드는 가격의 절대적인 고평가·저평가를 알려주는 지표가 아니라, 최근 20일 가격 분포에서 현재 위치가 어디인지 보여주는 변동성 도구로 해석해야 합니다. 밴드 안으로 복귀했다는 사건만으로는 추세가 끝났다는 증거가 부족합니다.

따라서 하단 반등 신호를 사용할 때는 다음을 추가해야 합니다.

  • 밴드 폭이 줄어들고 있는가
  • 거래량이 동반되었는가
  • 종가가 MA20을 회복했는가
  • 시장과 산업 지수가 상승 중인가

하단 밴드에 닿았다는 이유만으로 매수하면 “싼 가격”이 아니라 “하락 중인 가격”을 매수하게 될 수 있습니다. 위 조건들은 개선 가능성을 제시할 뿐, 이 장에서 추가 검증한 결과는 아닙니다. 다음 장에서는 평균회귀 신호의 손실 폭을 변동성에 맞춰 관리하는 ATR을 확인합니다.

결과는 신호별 결과 CSV에 공개했습니다.

기술적 분석 6장: ATR로 종목마다 다른 손절선을 계산하기

ATR과 변동성 분석

저변동성 대형주와 변동성이 큰 바이오주에 똑같이 -7% 손절선을 적용하면 어떤 종목에는 너무 좁고, 다른 종목에는 너무 넓을 수 있습니다. ATR은 최근 가격 범위를 이용해 종목별 변동성을 비교하는 방법입니다.

previous_close = df.groupby("code")["close"].shift(1)
true_range = pd.concat(
[
df["high"] - df["low"],
(df["high"] - previous_close).abs(),
(df["low"] - previous_close).abs(),
],
axis=1,
).max(axis=1)
df["true_range"] = true_range
df["atr14_pct"] = df.groupby("code")["true_range"].transform(
lambda x: x.rolling(14).mean()
) / df["close"]

실제 공개 코드에서는 종목별 그룹 안에서 True Range를 계산합니다. atr14_pct는 ATR을 현재 가격으로 나눈 값이므로 종목 가격이 달라도 변동성을 비교할 수 있습니다.

복합 신호를 변동성별로 나누기

Section titled “복합 신호를 변동성별로 나누기”

MA20·MA60·RSI·MACD·거래량 조건을 동시에 만족한 진입 신호 중 20일 이후 수익률을 계산할 수 있는 1,601건을 ATR 기준 세 그룹으로 나누었습니다. 각 그룹의 실제 20일 결과는 다음과 같습니다.

변동성 그룹신호 수평균 ATR14/가격양수 비율평균 수익률중앙값
낮은 변동성5344.08%59.4%+7.73%+3.76%
중간 변동성5336.41%53.7%+9.00%+2.24%
높은 변동성53410.01%47.0%+4.36%-1.83%

평균 수익률만 보면 중간 변동성이 가장 높습니다. 그러나 낮은 변동성 그룹의 양수 비율은 59.4%, 중앙값은 +3.76%로 가장 안정적이었습니다. 높은 변동성 그룹은 평균이 +4.36%였지만 양수 비율은 47.0%, 중앙값은 -1.83%였습니다.

ATR은 매수 신호가 아니라 포지션 크기와 손절 폭을 정하는 도구입니다.

target_risk = 100_000
stop_distance = df["atr14_pct"] * df["close"] * 2
df["shares"] = (target_risk / stop_distance).fillna(0).astype(int)

이번 결과에서 중요한 것은 평균수익률 1위인 중간 변동성 그룹을 그대로 고르는 것이 아닙니다. 낮은 변동성은 양수 비율과 중앙값이 좋았고, 중간 변동성은 평균만 높았으며, 높은 변동성은 전형적인 결과가 손실이었습니다. 변동성이 높은 종목을 무조건 제외할 필요는 없지만, 같은 금액을 매수하지 않고 손실 금액이 일정하도록 수량을 조절해야 합니다. ATR은 매수 신호를 강화하는 지표가 아니라, 신호를 실행할 때 위험을 먼저 제한하는 도구입니다.

다음 장에서는 가격이 MA20 위에 있다는 조건에 OBV를 추가했을 때 거래량 확인이 실제로 수익률의 일관성을 높이는지 살펴봅니다.

원자료는 변동성 그룹 결과에서 확인할 수 있습니다.

기술적 분석 7장: 거래량과 OBV는 주가보다 먼저 움직이는가?

OBV와 거래량 분석

8장에서 거래량 돌파의 성과를 보기 전에, 거래량을 단순히 급증한 하루의 크기로만 볼 필요가 있는지 확인합니다. 가격이 오르는데 거래량이 줄어드는 경우와 가격 상승에 거래량이 함께 증가하는 경우는 같은 상승으로 보기 어렵습니다. OBV는 상승한 날의 거래량을 더하고 하락한 날의 거래량을 빼서 가격과 거래량의 방향을 하나의 시계열로 만듭니다.

direction = df.groupby("code")["close"].diff().apply(np.sign)
df["obv"] = (direction * df["volume"]).groupby(df["code"]).cumsum()
df["obv_ma20"] = df.groupby("code")["obv"].transform(
lambda x: x.rolling(20).mean()
)
df["obv_confirmation"] = (
(df["close"] > df["ma20"]) &
(df["obv"] > df["obv_ma20"])
)

이번 분석에서는 가격이 MA20 위에 있고 OBV도 OBV20 위로 올라온 첫날을 확인 신호로 정의했습니다.

검증 구간에서 OBV 확인 신호는 2,653회 발생했습니다.

신호발생 횟수20일 양수 비율평균 수익률중앙값
OBV 확인2,653회53.8%+6.02%+2.03%
거래량 돌파2,726회53.4%+9.75%+2.51%

OBV 확인은 거래량 돌파보다 평균수익률과 중앙값은 낮았지만, 양수 비율은 53.8%로 거래량 돌파의 53.4%보다 소폭 높았습니다. RSI 회복과 비교하면 평균과 중앙값이 모두 높았습니다. 즉 OBV가 거래량 돌파를 대체하는 더 강한 신호라기보다, 가격 추세가 거래량의 누적 방향과도 맞는지 확인하는 역할에 가깝습니다. 그래도 양수 비율이 53.8%에 그쳤으므로 거래량이 증가했다는 사실만으로 향후 상승을 확정할 수는 없습니다.

이번 결과에서 OBV의 역할은 단독 매수 신호보다 가격 신호의 신뢰도 확인에 가깝습니다. 급격한 거래량 돌파는 큰 평균수익률을 만들었고, OBV 확인은 비슷한 양수 비율을 조금 더 안정적으로 보완했습니다. 둘을 함께 사용하면 정보가 추가되는지, 같은 거래량 정보를 중복해서 쓰는지는 복합 신호에서 다시 확인해야 합니다.

  • 가격이 MA20 위인데 OBV가 하락하면 상승의 내부가 약할 수 있습니다.
  • 가격이 횡보하는 동안 OBV가 상승하면 매집 가능성을 살펴볼 수 있습니다.
  • 급등일 거래량만으로 OBV가 높아졌는지, 여러 날 누적되어 높아졌는지 구분해야 합니다.

다음 장에서는 평균수익률이 가장 높았던 거래량 돌파를 20일 고점 돌파라는 명확한 조건으로 분해해, 높은 평균이 얼마나 넓은 사례에서 나왔는지 확인합니다.

결과는 신호별 결과 CSVOBV 확인 행에서 확인할 수 있습니다.