Skip to content

블로그

기술적 분석 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 추세 전략이 코스피를 앞섰습니다. 반면 복합 신호 전략은 조건이 더 많았음에도 누적수익률이 +5.11%에 그쳤고 최대낙폭은 -46.21%였습니다.

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

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

따라서 기술적 분석은 미래를 맞히는 주문이 아니라, 가격·거래량에서 반복적으로 관찰되는 조건을 정의하고 그 조건의 성과와 실패 범위를 측정하는 방법으로 보는 편이 정확합니다.

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

기술적 분석 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
서산KOSDAQ6,73066.21.155

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

스크리닝의 역할은 분석 대상을 줄이는 것입니다. 점수 5점이라고 해서 바로 매수할 수 있는 것은 아닙니다. 다음 단계에서는 산업, 실적, 현금흐름, 수급, 밸류에이션을 확인해야 합니다.

오히려 중요한 것은 후보가 어떤 조건으로 통과했는지입니다. RSI만 통과한 종목과 MA·MACD·거래량을 동시에 통과한 종목은 이후 검토 순서가 달라집니다.

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

산업별 기술적 신호 분석

기술적 신호는 모든 산업에서 같은 의미를 갖지 않습니다. 반도체처럼 추세가 빠르게 이어지는 산업과 바이오처럼 급등락이 큰 산업에 같은 돌파 조건을 적용하면 결과가 달라질 수 있습니다.

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회 이상 발생한 산업 중 일부는 다음과 같습니다.

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

전자부품과 반도체는 양수 비율과 중앙값이 함께 높았지만, 소프트웨어와 의약품은 평균과 중앙값의 차이가 컸습니다. 일부 급등 사례가 평균을 만들었을 가능성이 있다는 뜻입니다.

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

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

“이 지표의 평균 수익률은 얼마인가?”보다 더 좋은 질문은 “어떤 산업과 시장 상태에서 이 지표가 작동했는가?”입니다. 산업 분류와 시장 모멘텀을 추가하면 기술적 신호를 종목의 고유한 패턴과 시장의 공통 움직임으로 나눠 볼 수 있습니다.

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

기술적 분석 9장: 여러 지표를 합치면 성과가 좋아지는가?

기술적 지표 복합 신호

이동평균, RSI, MACD, 거래량이 모두 좋아 보이면 더 강한 매수 신호일까요? 여러 지표를 합치는 방식은 직관적이지만, 서로 비슷한 가격 정보를 반복해서 사용하거나 신호가 너무 늦어지는 문제가 생길 수 있습니다.

이번 시리즈에서는 다섯 조건 중 네 개 이상을 만족하면 복합 신호로 분류했습니다.

conditions = pd.concat(
[
df["close"] > df["ma20"],
df["ma20"] > df["ma60"],
df["rsi14"].between(40, 70),
df["macd"] > df["macd_signal"],
df["volume_ratio20"] > 1,
],
axis=1,
)
df["technical_score"] = conditions.sum(axis=1)
df["composite_state"] = df["technical_score"] >= 4
previous_state = df.groupby("code")["composite_state"].shift(1).fillna(False)
df["composite_entry"] = (
df["composite_state"] &
~previous_state
)

검증 구간에서 복합 신호 진입은 1,601회 발생했습니다.

신호발생 횟수20일 양수 비율평균 수익률중앙값
복합 신호 진입1,601회53.3%+7.03%+2.10%

거래량 돌파의 평균 수익률 +9.75%보다는 낮았습니다. 여러 조건을 요구했기 때문에 신호의 질이 자동으로 높아진 것이 아니라, 진입 시점이 늦어지고 상승 초기를 놓쳤을 가능성이 있습니다.

복합 신호의 장점은 신호의 설명력입니다. “왜 이 종목이 후보인가?”에 대해 추세, 모멘텀, 거래량으로 나눠 설명할 수 있습니다. 반면 조건을 추가할수록 표본 수가 줄고, 과거 기간에 맞춘 과최적화 위험이 커집니다.

따라서 복합 전략은 점수 자체보다 다음을 관리해야 합니다.

  • 조건을 추가하기 전후의 표본 수
  • 학습 구간과 검증 구간의 성과 차이
  • 평균과 중앙값의 차이
  • 산업별 편중
  • 신호 발생 후 최대 손실

복합 신호는 최종 매수 버튼이 아니라 다음 분석으로 넘어갈 종목을 줄이는 스크리닝 언어로 사용하는 것이 적절합니다.

기술적 분석 8장: 거래량을 동반한 돌파는 얼마나 강한가?

거래량 돌파 분석

신고가 돌파는 강한 매수세의 결과일 수 있지만, 이미 많은 투자자가 같은 가격을 보고 있을 가능성도 있습니다. 이번에는 현재 종가가 직전 20거래일 최고가를 넘고, 거래량이 20일 평균의 1.5배를 넘은 경우를 돌파 신호로 정의했습니다.

previous_high20 = df.groupby("code")["close"].transform(
lambda x: x.rolling(20).max().shift(1)
)
volume_average20 = df.groupby("code")["volume"].transform(
lambda x: x.rolling(20).mean().shift(1)
)
df["volume_breakout"] = (
(df["close"] > previous_high20) &
(df["volume"] > volume_average20 * 1.5)
)

기준선을 당일을 제외한 직전 20일로 만들었습니다. 당일 종가를 최고가 계산에 포함하면 돌파 여부를 판단할 때 미래 값을 일부 재사용하게 되기 때문입니다.

검증 구간의 거래량 돌파 신호는 2,726회 발생했습니다.

신호발생 횟수20일 양수 비율평균 수익률중앙값
20일 고점·거래량 돌파2,726회53.4%+9.75%+2.51%

이번 표본에서 평균 수익률은 지표 중 가장 높았습니다. 그러나 중앙값은 +2.51%이고 양수 비율은 53.4%였습니다. 소수의 큰 추세 상승이 평균을 끌어올렸을 가능성이 있으므로, 돌파 신호를 사용할 때는 평균만 보고 판단하면 안 됩니다.

돌파 매매는 손절 규칙이 전략의 일부입니다. 돌파가 실패해 가격이 다시 20일 고점 아래로 내려오면 신호가 무효화됐다고 판단할 수 있습니다. ATR을 이용해 돌파 종목마다 다른 손절 폭을 정하면 6장의 변동성 분석과도 연결됩니다.

거래량 돌파는 유망한 후보를 찾는 필터로는 유용하지만, 그 자체가 매수 추천은 아닙니다. 특히 장중 돌파와 종가 확정 돌파를 구분해야 하며, 다음 거래일 체결 가격을 사용해 재검증해야 합니다.

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

OBV와 거래량 분석

가격이 오르는데 거래량이 줄어드는 경우와 가격 상승에 거래량이 함께 증가하는 경우는 같은 상승으로 보기 어렵습니다. 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%

OBV 신호는 RSI 회복보다 평균과 중앙값이 모두 높았습니다. 하지만 양수 비율은 53.8%로 압도적이지 않았습니다. 거래량이 증가했다는 사실만으로 향후 상승을 확정할 수는 없습니다.

OBV의 역할은 단독 매수 신호보다 가격 신호의 신뢰도 확인에 가깝습니다.

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

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

기술적 분석 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%

평균 수익률만 보면 중간 변동성이 가장 높습니다. 하지만 높은 변동성 그룹은 양수 비율이 41.9%이고 중앙값도 음수입니다.

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

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

변동성이 높은 종목을 무조건 제외할 필요는 없습니다. 대신 같은 금액을 매수하지 않고, 손실 금액이 일정하도록 수량을 조절해야 합니다. 이번 결과는 변동성 높은 신호를 그대로 추격하기보다 ATR을 이용해 위험을 먼저 제한해야 한다는 점을 보여줍니다.

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

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

볼린저 밴드 분석

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

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%였습니다. 일부 큰 반등 사례가 평균을 높였지만, 절반 이상의 전형적인 사례가 안정적인 수익으로 이어졌다고 보기는 어렵습니다.

볼린저 밴드는 가격의 절대적인 고평가·저평가를 알려주는 지표가 아닙니다. 최근 20일 가격 분포에서 현재 위치가 어디인지 보여주는 변동성 도구입니다.

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

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

하단 밴드에 닿았다는 이유만으로 매수하면 “싼 가격”이 아니라 “하락 중인 가격”을 매수하게 될 수 있습니다. 결과는 신호별 결과 CSV에 공개했습니다.

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

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

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

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

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

RSI 평균회귀 분석

RSI가 30 이하이면 과매도라고 설명하는 경우가 많습니다. 그러나 강한 하락 추세에서는 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%였습니다. 일부 큰 반등이 평균을 끌어올렸고, 전형적인 신호의 결과는 평균보다 약했습니다.

RSI는 단독 매수 신호라기보다 시장 상태를 설명하는 지표로 쓰는 편이 낫습니다. RSI 회복 신호가 다음 조건과 함께 나타나는지 확인할 필요가 있습니다.

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

RSI 30이라는 숫자만 보고 매수하면 하락 추세 중간의 일시적 반등을 잡을 가능성이 큽니다. 평균회귀 전략은 특히 손절 기준과 보유 기간을 함께 정해야 합니다.

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