단기 이동평균선이 장기 이동평균선을 상향 돌파하면 사고, 다시 하향 돌파하면 판다. 골든크로스와 데드크로스만 알면 만들 수 있는 가장 단순한 추세추종 전략입니다.
이 규칙을 한 종목의 좋은 구간에 적용하면 꽤 그럴듯한 자산곡선을 쉽게 찾을 수 있습니다. 문제는 종목이나 검증 기간을 바꿨을 때도 같은 결과가 나오는가입니다.
20일 EMA와 100일 EMA 교차를 국내 주식 30종목에 적용하고 네 해씩 세 구간으로 나눠 검증했습니다. 동일 비중 포트폴리오는 세 구간 모두 수익이었고 한 구간의 누적수익률은 116.48%였습니다. 그러나 세 구간에서 모두 수익이었던 공통 종목은 27개 중 10개뿐이었습니다.
이번 글에서는 한 구간에서 수익이 났다와 종목과 기간이 바뀌어도 살아남았다가 왜 다른 질문인지 확인합니다.
SYSTEM LAB 05 핵심 결과
- 국내 주식 30종목, 세 구간에서 총 87개 종목·구간 표본 검증
- 전체 표본 중 EMA 교차 전략이 수익인 비율 71.26%
- 같은 구간의 매수보유 수익률을 이긴 비율 25.29%
- 세 구간 데이터가 모두 있는 27종목 중 매번 수익인 종목 10개
- 나머지 17종목은 구간에 따라 수익과 손실이 바뀜
1. 이동평균 교차가 단순해서 나쁜 것은 아니다
이동평균선은 가격의 잦은 움직임을 완화해 추세를 보기 쉽게 만듭니다. 기간이 짧으면 가격 변화에 빠르게 반응하지만 횡보장에서 교차가 자주 발생합니다. 기간이 길면 작은 흔들림은 줄어들지만 신호가 늦어집니다.
두 이동평균선의 교차 전략은 이 차이를 이용합니다.
- 단기 EMA가 장기 EMA 위로 올라가면 상승 추세가 시작됐다고 판단합니다.
- 단기 EMA가 장기 EMA 아래로 내려가면 상승 추세가 끝났다고 판단합니다.
- 작은 손실이 반복되더라도 한 번의 긴 추세를 따라가려는 구조입니다.
단순한 규칙 자체는 문제가 아닙니다. 오히려 신호가 명확해 재현과 검증이 쉽습니다. 문제는 특정 종목과 특정 기간에서 나온 좋은 결과를 규칙의 보편적인 성능으로 착각하는 데 있습니다.
EMA의 계산 방식과 SMA와의 차이는 지수 이동평균선 골든크로스와 파이썬 수식에서 먼저 확인할 수 있습니다.
2. 20일·100일 EMA 규칙을 고정한다
원래 장중 데이터에 사용하던 장·단기 EMA 규칙을 일봉 현물 매수 전략에 맞게 바꿨습니다. 기간을 그대로 복사하지 않고 한 달과 약 다섯 달의 추세를 비교하는 20일·100일 조합을 사용합니다.
| 항목 | 이번 검증의 고정 규칙 |
|---|---|
| 입력 데이터 | yfinance 수정주가 일봉 시가·종가 |
| 종목 | 국내 대형·유동성 종목 30개 고정 목록 |
| 진입 신호 | 20일 EMA가 100일 EMA를 상향 돌파 |
| 청산 신호 | 20일 EMA가 100일 EMA를 하향 돌파 |
| 신호 확정 | 해당 거래일 종가 |
| 주문 체결 | 다음 거래일 시가 |
| 방향 | 현물 매수만 허용 |
| 비용 | 매수·매도 각각 0.05% |
| 중복 진입 | 보유 중 추가 매수 금지 |
| 구간 종료 | 마지막 종가로 강제 청산 |
EMA 기간을 여러 조합으로 돌려 가장 좋은 값을 고르지 않았습니다. 20일과 100일을 먼저 고정한 뒤 종목과 구간을 바꿉니다. 이번 질문은 최적 EMA를 찾는 것이 아니라 하나의 단순한 규칙이 표본 변화에 얼마나 민감한지 확인하는 것이기 때문입니다.
신호가 나온 종가로 즉시 체결하지 않고 다음 시가로 주문합니다. 종가 신호를 바로 샀더니 백테스트가 틀어진 이유에서 사용한 체결 상태와 동일합니다.
3. 한 번의 긴 백테스트 대신 세 구간으로 나눈 이유
전체 데이터를 한 번에 연결하면 마지막 누적수익률 하나만 남습니다. 어느 시장 환경에서 벌었고 어느 구간에서 잃었는지 가려질 수 있습니다.
그래서 약 12년을 네 해씩 세 구간으로 나눴습니다.
| 검증 구간 | 유효 종목 | 역할 |
|---|---|---|
| 2014~2017 | 27개 | 초기 상승·조정 구간에서의 반응 |
| 2018~2021 | 30개 | 급락과 강한 유동성 장세가 함께 있던 구간 |
| 2022~2025 | 30개 | 긴축·업종 순환과 종목별 차이가 커진 구간 |
첫 구간은 상장 이력이 1년 미만인 종목을 제외해 27개가 남았습니다. 같은 종목도 각 구간을 시작할 때 현금 상태로 초기화하고 새로운 교차 신호가 나올 때까지 기다립니다.
종목별 결과 외에 각 종목을 같은 비중으로 합친 포트폴리오도 계산합니다. 한두 종목의 큰 수익이 전체 평균을 끌어올리는지 확인하려면 포트폴리오 수익률과 수익 종목 비율을 함께 봐야 합니다.
4. 전체 Python 코드
필요한 패키지는 터미널에서 먼저 설치합니다.
python -m pip install yfinance pandas numpy matplotlib
아래 코드 하나가 30종목을 내려받고 EMA 신호, 다음 시가 체결, 종목·구간별 결과, 동일 비중 포트폴리오와 비교 차트를 모두 만듭니다.
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import yfinance as yf
from matplotlib import font_manager
TICKERS = ["005930.KS", "000660.KS", "005380.KS", "000270.KS", "035420.KS", "035720.KS", "068270.KS", "207940.KS", "105560.KS", "055550.KS", "066570.KS", "012330.KS", "028260.KS", "017670.KS", "009150.KS", "003670.KS", "042700.KS", "000810.KS", "196170.KQ", "247540.KQ", "086520.KQ", "058470.KQ", "035900.KQ", "041510.KQ", "067160.KQ", "263750.KQ", "293490.KQ", "112040.KQ", "036930.KQ", "240810.KQ"]
PERIODS = {"2014~2017": ("2014-01-01", "2018-01-01"), "2018~2021": ("2018-01-01", "2022-01-01"), "2022~2025": ("2022-01-01", "2026-01-01")}
DOWNLOAD_START = "2013-01-01"
DOWNLOAD_END = "2026-01-01"
FAST_WINDOW = 20
SLOW_WINDOW = 100
COST_RATE = 0.0005
MIN_ROWS = 252
CHART_FILE = Path("ema_robustness_chart.png")
RESULT_FILE = Path("ema_robustness_results.csv")
SUMMARY_FILE = Path("ema_robustness_summary.csv")
def prepare_price(raw: pd.DataFrame, ticker: str) -> pd.DataFrame:
data = raw[ticker].rename(columns=str.lower).dropna(subset=["open", "close"]).copy()
data["ema_fast"] = data["close"].ewm(span=FAST_WINDOW, adjust=False, min_periods=FAST_WINDOW).mean()
data["ema_slow"] = data["close"].ewm(span=SLOW_WINDOW, adjust=False, min_periods=SLOW_WINDOW).mean()
data["entry_signal"] = (data["ema_fast"] > data["ema_slow"]) & (data["ema_fast"].shift(1) <= data["ema_slow"].shift(1))
data["exit_signal"] = (data["ema_fast"] < data["ema_slow"]) & (data["ema_fast"].shift(1) >= data["ema_slow"].shift(1))
return data
def run_backtest(data: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
cash = 1.0
shares = 0.0
pending_order = None
current_trade = None
trades = []
daily_records = []
for date, row in data.iterrows():
if pending_order is not None:
if pending_order["side"] == "buy" and shares == 0.0:
capital_before = cash
entry_price = float(row["open"])
shares = cash * (1.0 - COST_RATE) / entry_price
cash = 0.0
current_trade = {"entry_signal_date": pending_order["signal_date"], "entry_date": date, "entry_price": entry_price, "capital_before": capital_before}
elif pending_order["side"] == "sell" and shares > 0.0:
exit_price = float(row["open"])
cash = shares * exit_price * (1.0 - COST_RATE)
shares = 0.0
trades.append({**current_trade, "exit_signal_date": pending_order["signal_date"], "exit_date": date, "exit_price": exit_price, "trade_return": cash / current_trade["capital_before"] - 1.0, "forced_exit": False})
current_trade = None
pending_order = None
if shares > 0.0 and bool(row["exit_signal"]):
pending_order = {"side": "sell", "signal_date": date}
elif shares == 0.0 and bool(row["entry_signal"]):
pending_order = {"side": "buy", "signal_date": date}
daily_records.append({"date": date, "equity": cash + shares * float(row["close"]), "exposure": float(shares > 0.0)})
if shares > 0.0:
last_date = data.index[-1]
last_close = float(data.iloc[-1]["close"])
cash = shares * last_close * (1.0 - COST_RATE)
trades.append({**current_trade, "exit_signal_date": last_date, "exit_date": last_date, "exit_price": last_close, "trade_return": cash / current_trade["capital_before"] - 1.0, "forced_exit": True})
daily_records[-1]["equity"] = cash
daily_records[-1]["exposure"] = 0.0
return pd.DataFrame(trades), pd.DataFrame(daily_records).set_index("date")
def summarize_sample(ticker: str, period: str, data: pd.DataFrame, trades: pd.DataFrame, daily: pd.DataFrame) -> dict[str, float | str]:
drawdown = daily["equity"] / daily["equity"].cummax() - 1.0
buy_hold = float(data["close"].iloc[-1] * (1.0 - COST_RATE) / (data["open"].iloc[0] / (1.0 - COST_RATE)) - 1.0)
return {"ticker": ticker, "period": period, "rows": len(data), "strategy_return": float(daily["equity"].iloc[-1] - 1.0), "buy_hold_return": buy_hold, "excess_return": float(daily["equity"].iloc[-1] - 1.0 - buy_hold), "mdd": float(drawdown.min()), "trade_count": len(trades), "win_rate": float((trades["trade_return"] > 0.0).mean()) if not trades.empty else np.nan, "exposure": float(daily["exposure"].mean())}
def configure_font() -> None:
available_fonts = {item.name for item in font_manager.fontManager.ttflist}
for font_name in ("Malgun Gothic", "AppleGothic", "UnDotum", "DejaVu Sans"):
if font_name in available_fonts:
plt.rcParams["font.family"] = font_name
break
plt.rcParams["axes.unicode_minus"] = False
def draw_chart(results: pd.DataFrame, summaries: pd.DataFrame, portfolios: dict[str, pd.DataFrame]) -> None:
colors = {"2014~2017": "#64748B", "2018~2021": "#F59E0B", "2022~2025": "#0F766E"}
fig = plt.figure(figsize=(13, 10), facecolor="white")
grid = fig.add_gridspec(2, 2, height_ratios=[1.8, 1.3], hspace=0.34, wspace=0.24)
equity_axis = fig.add_subplot(grid[0, :])
scatter_axis = fig.add_subplot(grid[1, 0])
ratio_axis = fig.add_subplot(grid[1, 1])
for period, portfolio in portfolios.items():
elapsed = np.arange(len(portfolio)) / 252.0
equity_axis.plot(elapsed, portfolio["equity"], color=colors[period], linewidth=2.4, label=f"{period} {portfolio['equity'].iloc[-1] - 1.0:+.1%}")
equity_axis.axhline(1.0, color="#9CA3AF", linewidth=1.0)
equity_axis.set_title("같은 EMA 규칙도 검증 구간이 바뀌면 다른 경로를 만든다", fontsize=18, fontweight="bold", pad=16)
equity_axis.set_xlabel("구간 시작 후 경과 연수")
equity_axis.set_ylabel("유효 종목 동일 비중 누적 자산")
equity_axis.legend(loc="upper left", frameon=False, ncol=3)
for period, group in results.groupby("period"):
scatter_axis.scatter(group["buy_hold_return"] * 100.0, group["strategy_return"] * 100.0, s=48, alpha=0.72, color=colors[period], label=period)
bounds = results[["buy_hold_return", "strategy_return"]].to_numpy() * 100.0
low = float(np.nanmin(bounds)) - 5.0
high = float(np.nanmax(bounds)) + 5.0
scatter_axis.plot([low, high], [low, high], color="#9CA3AF", linewidth=1.0, linestyle="--")
scatter_axis.axhline(0.0, color="#D1D5DB", linewidth=0.8)
scatter_axis.axvline(0.0, color="#D1D5DB", linewidth=0.8)
scatter_axis.set_xlim(low, high)
scatter_axis.set_ylim(low, high)
scatter_axis.set_title("종목·구간별 EMA 전략과 매수보유", fontsize=14, fontweight="bold")
scatter_axis.set_xlabel("매수보유 수익률(%)")
scatter_axis.set_ylabel("EMA 교차 수익률(%)")
scatter_axis.legend(loc="upper left", frameon=False, fontsize=8)
x = np.arange(len(summaries))
width = 0.34
ratio_axis.bar(x - width / 2, summaries["profitable_ratio"] * 100.0, width, color="#0F766E", label="수익 종목")
ratio_axis.bar(x + width / 2, summaries["beat_buy_hold_ratio"] * 100.0, width, color="#F59E0B", label="매수보유 초과")
ratio_axis.set_xticks(x, summaries.index)
ratio_axis.set_ylim(0, 100)
ratio_axis.set_ylabel("종목 비율(%)")
ratio_axis.set_title("수익 종목과 매수보유 초과 종목은 달랐다", fontsize=14, fontweight="bold")
ratio_axis.legend(frameon=False)
for container in ratio_axis.containers:
ratio_axis.bar_label(container, labels=[f"{value.get_height():.0f}%" for value in container], padding=3, fontsize=9)
for axis in (equity_axis, scatter_axis, ratio_axis):
axis.grid(axis="y", color="#E5E7EB", linewidth=0.8)
axis.spines["top"].set_visible(False)
axis.spines["right"].set_visible(False)
CHART_FILE.parent.mkdir(parents=True, exist_ok=True)
fig.savefig(CHART_FILE, dpi=160, bbox_inches="tight", facecolor="white")
plt.close(fig)
configure_font()
raw = yf.download(TICKERS, start=DOWNLOAD_START, end=DOWNLOAD_END, auto_adjust=True, progress=False, group_by="ticker", threads=False)
if raw.empty:
raise RuntimeError("가격 데이터를 받지 못했습니다.")
price_by_ticker = {ticker: prepare_price(raw, ticker) for ticker in TICKERS}
result_rows = []
portfolio_results = {}
for period, (start, end) in PERIODS.items():
equity_series = []
for ticker, full_data in price_by_ticker.items():
data = full_data.loc[(full_data.index >= start) & (full_data.index < end)].copy()
if len(data) < MIN_ROWS or data.dropna(subset=["ema_fast", "ema_slow"]).empty:
continue
trades, daily = run_backtest(data)
result_rows.append(summarize_sample(ticker, period, data, trades, daily))
equity_series.append(daily["equity"].rename(ticker))
aligned = pd.concat(equity_series, axis=1).ffill().fillna(1.0)
portfolio = pd.DataFrame({"equity": aligned.mean(axis=1)})
portfolio["drawdown"] = portfolio["equity"] / portfolio["equity"].cummax() - 1.0
portfolio_results[period] = portfolio
results = pd.DataFrame(result_rows)
summary_rows = []
for period, group in results.groupby("period", sort=False):
portfolio = portfolio_results[period]
summary_rows.append({"period": period, "ticker_count": len(group), "portfolio_return": float(portfolio["equity"].iloc[-1] - 1.0), "portfolio_mdd": float(portfolio["drawdown"].min()), "median_return": float(group["strategy_return"].median()), "profitable_ratio": float((group["strategy_return"] > 0.0).mean()), "beat_buy_hold_ratio": float((group["excess_return"] > 0.0).mean()), "average_trades": float(group["trade_count"].mean()), "average_exposure": float(group["exposure"].mean())})
summaries = pd.DataFrame(summary_rows).set_index("period")
results.to_csv(RESULT_FILE, index=False, encoding="utf-8-sig")
summaries.to_csv(SUMMARY_FILE, encoding="utf-8-sig")
draw_chart(results, summaries, portfolio_results)
common = results.pivot(index="ticker", columns="period", values="strategy_return").dropna()
all_positive_ratio = float((common > 0.0).all(axis=1).mean())
print(summaries.to_string(formatters={"portfolio_return": "{:+.2%}".format, "portfolio_mdd": "{:.2%}".format, "median_return": "{:+.2%}".format, "profitable_ratio": "{:.2%}".format, "beat_buy_hold_ratio": "{:.2%}".format, "average_trades": "{:.1f}".format, "average_exposure": "{:.2%}".format}))
print(f"전체 표본: {len(results)}개")
print(f"세 구간 모두 수익인 공통 종목 비율: {all_positive_ratio:.2%} ({int((common > 0.0).all(axis=1).sum())}/{len(common)})")
print(f"결과 저장: {RESULT_FILE}")
print(f"요약 저장: {SUMMARY_FILE}")
print(f"차트 저장: {CHART_FILE}")
entry_signal과 exit_signal은 종가로 확정됩니다. 주문은 pending_order에 저장되고 반복문이 다음 거래일로 넘어간 뒤 시가로 체결됩니다. 같은 봉의 종가를 보고 그 종가로 체결하는 미래 참조를 피한 구조입니다.
각 종목은 초기 자산 1로 독립적으로 백테스트합니다. 포트폴리오는 종목별 일간 자산을 같은 비중으로 평균합니다. 상장 전이거나 데이터가 부족한 구간을 억지로 채우지 않고 최소 252거래일을 확보한 종목만 사용합니다.
5. 포트폴리오는 세 구간 모두 수익이었다
작성 환경에서 실행한 결과입니다.
| 구간 | 종목 수 | 포트폴리오 수익률 | MDD | 종목 수익률 중앙값 | 평균 거래 수 | 평균 노출 |
|---|---|---|---|---|---|---|
| 2014~2017 | 27개 | +45.56% | -12.29% | +22.16% | 6.3회 | 48.41% |
| 2018~2021 | 30개 | +116.48% | -18.39% | +51.06% | 6.5회 | 53.70% |
| 2022~2025 | 30개 | +83.26% | -21.18% | +27.92% | 6.6회 | 48.64% |

이 표만 보면 EMA 교차 전략은 세 구간에서 모두 성공한 것처럼 보입니다. 특히 두 번째 구간의 동일 비중 수익률은 116.48%입니다.
하지만 평균은 일부 큰 수익의 영향을 받습니다. 두 번째 구간에는 위메이드 +626.48%, 에코프로비엠 +575.72%, 알테오젠 +447.25%처럼 큰 추세를 포착한 종목이 포함돼 있습니다. 이 종목들이 포트폴리오 평균을 끌어올렸습니다.
포트폴리오가 수익이라는 사실은 30종목 대부분에서 비슷하게 작동했다는 뜻이 아닙니다. 다음으로 종목별 손익 분포를 확인해야 합니다.
6. 세 구간 모두 수익인 종목은 10개였다
세 구간의 데이터가 모두 있는 공통 종목은 27개입니다. 이 가운데 매번 수익을 기록한 종목은 10개, 37.04%였습니다.
| 일관성 구분 | 종목 수 | 비율 |
|---|---|---|
| 세 구간 모두 수익 | 10개 | 37.04% |
| 구간에 따라 수익·손실 전환 | 17개 | 62.96% |
| 세 구간 모두 손실 | 0개 | 0.00% |
세 구간 모두 수익인 종목에는 삼성전자, SK하이닉스, 포스코퓨처엠, 삼성전기, 한미반도체, 신한지주, 에코프로 등이 포함됐습니다.
반면 셀트리온은 첫 구간에 +228.79%였지만 다음 두 구간에는 -26.40%, -25.96%였습니다. 동일한 종목과 동일한 20일·100일 EMA 규칙도 시장 구조와 추세의 길이가 바뀌면 손익이 반대로 바뀌었습니다.
이 결과는 EMA 교차가 무효라는 뜻이 아닙니다. 단일 종목 한 구간의 성공만으로 규칙의 일관성을 판단할 수 없다는 뜻입니다.
7. 전체 71%가 수익이었지만 매수보유를 이긴 비율은 25%였다
87개 종목·구간 표본 중 EMA 전략이 플러스 수익을 기록한 표본은 62개로 71.26%였습니다. 그러나 같은 종목을 구간 시작에 사서 끝까지 보유한 결과를 이긴 표본은 22개, 25.29%에 그쳤습니다.
| 구간 | 수익 종목 비율 | 매수보유 초과 비율 |
|---|---|---|
| 2014~2017 | 59.26% | 14.81% |
| 2018~2021 | 83.33% | 16.67% |
| 2022~2025 | 70.00% | 43.33% |
| 전체 | 71.26% | 25.29% |
수익이 났다는 사실과 단순 보유보다 나았다는 사실은 다릅니다. 강한 상승장이 길게 이어질 때 EMA 교차는 상승 초반을 기다리고 중간 조정에서 청산하기 때문에 매수보유보다 낮은 수익을 기록할 수 있습니다.
위메이드는 두 번째 구간에 EMA 전략으로 +626.48%를 기록했습니다. 큰 수익이지만 같은 구간의 매수보유 +652.22%에는 미치지 못했습니다. 다음 구간에는 EMA 전략 -74.58%, 매수보유 -85.86%였습니다. 하락을 일부 줄였지만 자산의 대부분을 잃었다는 점은 달라지지 않습니다.
따라서 벤치마크를 이겼다는 이유만으로 전략을 성공이라고 평가해서도 안 됩니다. 절대수익, MDD, 초과수익을 함께 봐야 합니다.
8. EMA 교차가 작동한 구간과 실패한 구간
긴 추세가 이어질 때
20일 EMA가 100일 EMA 위에 머무르는 시간이 길수록 전략은 큰 추세를 따라갑니다. 매매 횟수가 적고 평균 노출 비중이 약 50%인 이유입니다. 강한 추세 종목에서는 한 번의 거래가 이전의 작은 손실을 모두 만회할 수 있습니다.
횡보와 빠른 반전이 반복될 때
가격이 방향 없이 움직이면 단기 EMA가 장기 EMA 위아래를 반복해서 통과합니다. 진입 직후 데드크로스가 발생해 손실 거래가 쌓이고, 다음 시가 체결의 갭과 비용도 반복됩니다.
이미 많이 오른 뒤 교차할 때
100일 EMA는 느립니다. 상승 초반이 아니라 가격이 상당히 오른 뒤 골든크로스가 발생할 수 있습니다. 추세가 짧으면 진입 직후 반전하고, 추세가 길어도 매수보유보다 늦게 참여합니다.
급락 뒤 회복이 빠를 때
데드크로스는 손실 확대를 막는 역할을 하지만 신호가 늦습니다. 급락 직후 빠르게 반등하면 청산 후 더 높은 가격에서 다시 진입하는 상황도 생깁니다. 이동평균선의 후행성을 없앨 수는 없습니다.
9. 이 결과를 실전 전략으로 바로 사용하면 안 되는 이유
첫째, 종목 목록은 현재 확인 가능한 대형·유동성 종목을 고정한 것입니다. 과거에 상장폐지된 종목이 빠져 있어 생존 편향이 남아 있습니다.
둘째, yfinance 수정주가 일봉을 사용했습니다. 실제 증권사 체결 데이터, 호가 스프레드와 주문 충격은 포함하지 않았습니다.
셋째, 비용은 매수·매도 각각 0.05%만 적용했습니다. 계좌와 시장에 따라 달라지는 세금, 슬리피지와 주문 실패를 모두 재현한 값은 아닙니다.
넷째, 20일·100일 EMA는 최적값이 아닙니다. 반대로 이번 데이터에서 가장 높은 수익을 내는 조합을 찾지도 않았습니다. 다음 단계에서 기간을 바꿀 때는 최고점 하나가 아니라 주변 값에서도 성과가 유지되는지 확인해야 합니다.
다섯째, 종목별 초기 자산을 같은 비중으로 합쳤습니다. 실제 계좌에서는 동시에 발생한 신호, 보유 한도, 현금 부족과 종목별 주문 금액을 처리하는 포트폴리오 엔진이 필요합니다.
누적수익률이 비슷한데 낙폭은 달랐던 성과표 실험처럼 최종 수익률만으로 전략을 선택하지 않는 이유입니다.
10. 정리
이번 EMA 교차 실험에서 확인한 사실은 세 가지입니다.
- 동일 비중 포트폴리오는 세 구간 모두 수익이었지만, 세 구간에서 매번 수익인 공통 종목은 27개 중 10개였습니다.
- 전체 표본의 71.26%가 수익이었지만 같은 구간의 매수보유를 이긴 비율은 25.29%였습니다.
- 단순한 EMA 교차는 긴 추세를 포착할 수 있지만 종목과 시장 구간이 바뀌면 손익 부호가 달라졌습니다.
단순한 전략은 출발점으로 충분합니다. 다만 한 종목의 우상향 차트를 찾는 데서 멈추면 안 됩니다. 종목을 바꾸고, 기간을 자르고, 매수보유와 비교한 뒤에도 남는 특성이 있는지를 확인해야 시스템의 규칙으로 사용할 근거가 생깁니다.
다음 글에서는 빠른 선과 느린 선 사이에 확인용 이동평균선을 하나 더 넣습니다. 세 개의 이동평균선이 횡보장의 휩쏘를 줄이는 대신 추세 진입을 얼마나 늦추는지 같은 종목과 비용 조건에서 비교합니다.
이 글은 과거 데이터를 이용한 백테스트 예제이며 특정 종목이나 전략의 미래 수익을 보장하지 않습니다. 실제 투자에서는 세금, 수수료, 슬리피지, 체결 가능성과 계좌 위험을 별도로 검토해야 합니다.