이동평균선 교차 전략을 돌리면 횡보장에서 짧은 손실이 반복됩니다. 단기선이 장기선을 넘어서 매수했는데 얼마 지나지 않아 다시 아래로 내려오는 휩쏘입니다.
이 문제를 줄이는 익숙한 방법이 이동평균선을 하나 더 넣는 것입니다. 20일 EMA가 100일 EMA를 넘었다는 사실만 보지 않고, 중간의 50일 EMA까지 20일 > 50일 > 100일 순서로 정렬될 때 진입합니다.
국내 주식 30종목에 같은 규칙을 적용하자 전체 거래는 571건에서 438건으로 줄었습니다. 진입 후 20거래일 이내 손실 청산으로 정의한 휩쏘도 159건에서 69건으로 90건 감소했습니다.
하지만 확인을 기다리는 동안 진입은 중앙값 5거래일 늦어졌고 누적수익률도 368.83%에서 346.47%로 22.36%p 낮아졌습니다. 필터가 나쁜 신호만 제거하지는 않았다는 뜻입니다.
SYSTEM LAB 06 핵심 결과
- 2중 EMA 거래 571건 → 3중 EMA 확인 438건
- 20거래일 이내 손실 159건 → 69건
- 확인 지연 중앙값 5거래일
- MDD -25.28% → -22.04%
- 누적수익률 +368.83% → +346.47%
1. 이동평균선 세 개가 추가로 알려주는 것
이동평균선이 두 개면 배열은 단순합니다.
- 20일 EMA가 100일 EMA 위에 있으면 상승 추세로 봅니다.
- 20일 EMA가 100일 EMA 아래에 있으면 하락 추세로 봅니다.
50일 EMA를 가운데 추가하면 애매한 배열을 구분할 수 있습니다.
| 배열 | 해석 |
|---|---|
| 20일 > 50일 > 100일 | 단기·중기·장기 추세가 모두 상승 방향 |
| 20일 > 100일 > 50일 | 단기 반등은 강하지만 중기 추세 확인 전 |
| 50일 > 20일 > 100일 | 상승 추세 안에서 단기 힘이 약해진 상태 |
| 20일 < 50일 < 100일 | 단기·중기·장기 추세가 모두 하락 방향 |
3중 이동평균선의 목적은 복잡한 신호를 만드는 것이 아닙니다. 두 선만으로 상승이라고 판단했던 구간을 확인된 상승과 아직 애매한 상승으로 나누는 것입니다.
EMA 교차를 종목과 기간별로 나눠 검증한 5편에서는 20일·100일 규칙이 종목과 구간에 따라 다른 결과를 만든다는 점을 확인했습니다. 이번에는 같은 종목군에서 50일 EMA 하나만 추가합니다.
2. 공정한 비교를 위해 진입 조건만 바꾼다
세 번째 선의 효과만 확인하려면 다른 조건을 함께 바꾸면 안 됩니다. 두 전략의 청산, 체결, 비용은 동일하게 유지했습니다.
| 항목 | 2중 EMA | 3중 EMA 확인 |
|---|---|---|
| 빠른 선 | 20일 EMA | 20일 EMA |
| 확인선 | 없음 | 50일 EMA |
| 느린 선 | 100일 EMA | 100일 EMA |
| 진입 | 20일 EMA가 100일 EMA 상향 돌파 | 20일 > 50일 > 100일 정배열 최초 완성 |
| 청산 | 20일 EMA가 100일 EMA 하향 돌파 | 동일 |
| 신호 확정 | 종가 | 동일 |
| 체결 | 다음 거래일 시가 | 동일 |
| 비용 | 매수·매도 각각 0.05% | 동일 |
| 추가 진입 | 보유 중 금지 | 동일 |
3중 정배열이 깨졌다고 바로 청산하지 않습니다. 청산까지 바꾸면 확인선의 진입 효과와 빠른 청산 효과가 섞이기 때문입니다. 이번 실험에서 50일 EMA는 진입 확인선으로만 사용합니다.
신호일과 체결일을 분리하는 방법은 종가 신호와 다음 시가 체결의 차이와 같습니다.
3. 휩쏘를 숫자로 먼저 정의한다
차트를 보고 짧아 보이는 손실을 임의로 고르면 두 전략을 공정하게 비교할 수 없습니다. 이번 글에서는 휩쏘를 다음처럼 고정합니다.
휩쏘 = 진입 후 20거래일 이내에 청산했고 거래비용 반영 후 손실인 거래
20거래일은 약 한 달에 해당하는 관찰 기준입니다. 절대적인 정의는 아닙니다. 10일이나 30일로 바꾸면 휩쏘 수는 달라집니다.
중요한 것은 두 전략에 같은 정의를 적용하는 것입니다. 전체 거래 수와 휩쏘 수를 함께 계산해 단순히 거래가 줄어서 휩쏘 수도 줄어든 것인지, 거래 중 휩쏘 비율까지 개선됐는지 확인합니다.
4. 전체 Python 코드
필요한 패키지는 터미널에서 먼저 설치합니다.
python -m pip install yfinance pandas numpy matplotlib
아래 코드 하나가 30종목의 2중·3중 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"]
DOWNLOAD_START = "2013-01-01"
TEST_START = "2014-01-01"
TEST_END = "2026-01-01"
FAST_WINDOW = 20
CONFIRM_WINDOW = 50
SLOW_WINDOW = 100
COST_RATE = 0.0005
WHIPSAW_DAYS = 20
MIN_ROWS = 252
CHART_FILE = Path("triple_ema_chart.png")
RESULT_FILE = Path("triple_ema_results.csv")
SIGNAL_FILE = Path("triple_ema_signals.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_confirm"] = data["close"].ewm(span=CONFIRM_WINDOW, adjust=False, min_periods=CONFIRM_WINDOW).mean()
data["ema_slow"] = data["close"].ewm(span=SLOW_WINDOW, adjust=False, min_periods=SLOW_WINDOW).mean()
data["two_regime"] = data["ema_fast"] > data["ema_slow"]
data["three_regime"] = (data["ema_fast"] > data["ema_confirm"]) & (data["ema_confirm"] > data["ema_slow"])
data["two_entry_signal"] = data["two_regime"] & ~data["two_regime"].shift(1, fill_value=False)
data["three_entry_signal"] = data["three_regime"] & ~data["three_regime"].shift(1, fill_value=False)
data["exit_signal"] = ~data["two_regime"] & data["two_regime"].shift(1, fill_value=False)
return data.loc[(data.index >= TEST_START) & (data.index < TEST_END)].copy()
def run_backtest(data: pd.DataFrame, entry_column: str) -> tuple[pd.DataFrame, pd.DataFrame]:
cash = 1.0
shares = 0.0
pending_order = None
current_trade = None
trades = []
daily_records = []
for index, (date, row) in enumerate(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_index": index, "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
trade_return = cash / current_trade["capital_before"] - 1.0
holding_days = index - current_trade["entry_index"]
trades.append({**current_trade, "exit_signal_date": pending_order["signal_date"], "exit_date": date, "exit_price": exit_price, "trade_return": trade_return, "holding_days": holding_days, "whipsaw": trade_return < 0.0 and holding_days <= WHIPSAW_DAYS, "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_column]):
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_index = len(data) - 1
last_date = data.index[-1]
last_close = float(data.iloc[-1]["close"])
cash = shares * last_close * (1.0 - COST_RATE)
trade_return = cash / current_trade["capital_before"] - 1.0
holding_days = last_index - current_trade["entry_index"]
trades.append({**current_trade, "exit_signal_date": last_date, "exit_date": last_date, "exit_price": last_close, "trade_return": trade_return, "holding_days": holding_days, "whipsaw": trade_return < 0.0 and holding_days <= WHIPSAW_DAYS, "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 analyze_confirmations(data: pd.DataFrame, two_trades: pd.DataFrame, ticker: str) -> pd.DataFrame:
rows = []
positions = pd.Series(np.arange(len(data)), index=data.index)
for trade in two_trades.itertuples():
episode = data.loc[trade.entry_signal_date:trade.exit_signal_date]
confirmations = episode.index[episode["three_entry_signal"]]
confirmed = len(confirmations) > 0
confirmation_date = confirmations[0] if confirmed else pd.NaT
delay_days = int(positions.loc[confirmation_date] - positions.loc[trade.entry_signal_date]) if confirmed else np.nan
rows.append({"ticker": ticker, "two_entry_signal_date": trade.entry_signal_date, "two_exit_signal_date": trade.exit_signal_date, "two_trade_return": trade.trade_return, "two_holding_days": trade.holding_days, "two_whipsaw": trade.whipsaw, "confirmed": confirmed, "confirmation_date": confirmation_date, "delay_days": delay_days})
return pd.DataFrame(rows)
def summarize(trades: pd.DataFrame, portfolio: pd.DataFrame) -> dict[str, float]:
drawdown = portfolio["equity"] / portfolio["equity"].cummax() - 1.0
winners = trades.loc[trades["trade_return"] > 0.0, "trade_return"]
losers = trades.loc[trades["trade_return"] < 0.0, "trade_return"]
profit_factor = float(winners.sum() / losers.abs().sum())
return {"누적수익률": float(portfolio["equity"].iloc[-1] - 1.0), "MDD": float(drawdown.min()), "Profit Factor": profit_factor, "승률": float((trades["trade_return"] > 0.0).mean()), "거래수": len(trades), "휩쏘수": int(trades["whipsaw"].sum()), "휩쏘비율": float(trades["whipsaw"].mean()), "평균보유일": float(trades["holding_days"].mean()), "노출비중": float(portfolio["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(summary: pd.DataFrame, portfolios: dict[str, pd.DataFrame], signals: pd.DataFrame) -> None:
colors = {"2중 EMA": "#F59E0B", "3중 EMA 확인": "#0F766E"}
fig = plt.figure(figsize=(13, 9), facecolor="white")
grid = fig.add_gridspec(2, 2, height_ratios=[1.8, 1.2], hspace=0.35, wspace=0.24)
equity_axis = fig.add_subplot(grid[0, :])
count_axis = fig.add_subplot(grid[1, 0])
delay_axis = fig.add_subplot(grid[1, 1])
for label, portfolio in portfolios.items():
equity_axis.plot(portfolio.index, portfolio["equity"], color=colors[label], linewidth=2.3, label=f"{label} {summary.loc[label, '누적수익률']:+.1%}")
equity_axis.set_title("50일 EMA 확인선은 휩쏘와 함께 일부 추세도 건너뛴다", fontsize=18, fontweight="bold", pad=16)
equity_axis.set_ylabel("30종목 동일 비중 누적 자산")
equity_axis.legend(loc="upper left", frameon=False)
labels = list(summary.index)
x = np.arange(len(labels))
width = 0.34
count_axis.bar(x - width / 2, summary["거래수"], width, color="#94A3B8", label="전체 거래")
count_axis.bar(x + width / 2, summary["휩쏘수"], width, color=[colors[label] for label in labels], label=f"{WHIPSAW_DAYS}일 이내 손실")
count_axis.set_xticks(x, labels)
count_axis.set_title("거래 수와 단기 손실 거래", fontsize=14, fontweight="bold")
count_axis.set_ylabel("거래 수")
count_axis.legend(frameon=False)
for container in count_axis.containers:
count_axis.bar_label(container, padding=3)
confirmed_delays = signals.loc[signals["confirmed"], "delay_days"].dropna()
delay_axis.hist(confirmed_delays, bins=np.arange(-0.5, max(confirmed_delays.max() + 2.0, 12.0), 3.0), color="#0F766E", edgecolor="white", alpha=0.88)
delay_axis.axvline(confirmed_delays.median(), color="#F59E0B", linewidth=2.2, linestyle="--", label=f"중앙값 {confirmed_delays.median():.0f}거래일")
delay_axis.set_title("3중 정배열 확인까지 걸린 시간", fontsize=14, fontweight="bold")
delay_axis.set_xlabel("2중 EMA 신호 이후 거래일")
delay_axis.set_ylabel("신호 수")
delay_axis.legend(frameon=False)
for axis in (equity_axis, count_axis, delay_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=TEST_END, auto_adjust=True, progress=False, group_by="ticker", threads=False)
if raw.empty:
raise RuntimeError("가격 데이터를 받지 못했습니다.")
trade_frames = {"2중 EMA": [], "3중 EMA 확인": []}
equity_series = {"2중 EMA": [], "3중 EMA 확인": []}
exposure_series = {"2중 EMA": [], "3중 EMA 확인": []}
signal_frames = []
calendar = raw.loc[(raw.index >= TEST_START) & (raw.index < TEST_END)].index
for ticker in TICKERS:
data = prepare_price(raw, ticker)
if len(data) < MIN_ROWS or data.dropna(subset=["ema_fast", "ema_confirm", "ema_slow"]).empty:
continue
two_trades, two_daily = run_backtest(data, "two_entry_signal")
three_trades, three_daily = run_backtest(data, "three_entry_signal")
for label, trades, daily in (("2중 EMA", two_trades, two_daily), ("3중 EMA 확인", three_trades, three_daily)):
trades.insert(0, "ticker", ticker)
trade_frames[label].append(trades)
equity_series[label].append(daily["equity"].reindex(calendar).ffill().fillna(1.0).rename(ticker))
exposure_series[label].append(daily["exposure"].reindex(calendar).ffill().fillna(0.0).rename(ticker))
signal_frames.append(analyze_confirmations(data, two_trades, ticker))
portfolios = {}
all_trades = {}
summary_rows = {}
for label in trade_frames:
all_trades[label] = pd.concat(trade_frames[label], ignore_index=True)
portfolios[label] = pd.DataFrame({"equity": pd.concat(equity_series[label], axis=1).mean(axis=1), "exposure": pd.concat(exposure_series[label], axis=1).mean(axis=1)})
summary_rows[label] = summarize(all_trades[label], portfolios[label])
summary = pd.DataFrame(summary_rows).T
summary[["거래수", "휩쏘수"]] = summary[["거래수", "휩쏘수"]].astype(int)
signals = pd.concat(signal_frames, ignore_index=True)
skipped = signals.loc[~signals["confirmed"]]
confirmed = signals.loc[signals["confirmed"]]
summary.to_csv(RESULT_FILE, encoding="utf-8-sig")
signals.to_csv(SIGNAL_FILE, index=False, encoding="utf-8-sig")
draw_chart(summary, portfolios, signals)
print(summary.to_string(formatters={"누적수익률": "{:+.2%}".format, "MDD": "{:.2%}".format, "Profit Factor": "{:.2f}".format, "승률": "{:.2%}".format, "휩쏘비율": "{:.2%}".format, "평균보유일": "{:.1f}".format, "노출비중": "{:.2%}".format}))
print(f"2중 EMA 신호 중 3중 정배열 확인: {len(confirmed)}/{len(signals)} ({len(confirmed) / len(signals):.2%})")
print(f"3중 정배열 확인 지연 중앙값: {confirmed['delay_days'].median():.0f}거래일")
print(f"건너뛴 2중 EMA 거래: {len(skipped)}건, 이 중 손실 {int((skipped['two_trade_return'] < 0.0).sum())}건, 수익 {int((skipped['two_trade_return'] > 0.0).sum())}건")
print(f"결과 저장: {RESULT_FILE}")
print(f"신호 저장: {SIGNAL_FILE}")
print(f"차트 저장: {CHART_FILE}")
two_entry_signal은 20일 EMA가 100일 EMA 위로 처음 올라가는 시점입니다. three_entry_signal은 세 선이 처음으로 완전한 정배열을 만드는 시점입니다.
청산은 두 전략 모두 exit_signal 하나를 사용합니다. 신호가 발생하면 pending_order에 주문을 저장하고 다음 거래일 시가에서 체결합니다.
analyze_confirmations()는 각 2중 EMA 상승 구간 안에서 3중 정배열이 실제로 완성됐는지 찾습니다. 완성됐다면 2중 신호일부터 몇 거래일을 더 기다렸는지 기록하고, 끝까지 완성되지 않았다면 건너뛴 거래로 분류합니다.
5. 거래 133건, 휩쏘 90건이 줄었다
작성 환경에서 실행한 결과입니다.
| 지표 | 2중 EMA | 3중 EMA 확인 | 변화 |
|---|---|---|---|
| 누적수익률 | +368.83% | +346.47% | -22.36%p |
| MDD | -25.28% | -22.04% | +3.24%p 개선 |
| Profit Factor | 3.34 | 3.59 | +0.25 |
| 승률 | 30.47% | 35.16% | +4.69%p |
| 거래 수 | 571건 | 438건 | -133건 |
| 휩쏘 수 | 159건 | 69건 | -90건 |
| 휩쏘 비율 | 27.85% | 15.75% | -12.10%p |
| 평균 보유일 | 77.7일 | 92.9일 | +15.2일 |
| 노출 비중 | 50.26% | 46.11% | -4.15%p |

3중 EMA 확인은 전체 거래를 23.29% 줄였습니다. 휩쏘 수는 56.60% 줄어 전체 거래보다 더 빠르게 감소했습니다. 단순히 거래를 덜 해서 휩쏘가 줄어든 것만은 아닙니다. 거래 중 휩쏘 비율도 27.85%에서 15.75%로 낮아졌습니다.
승률은 4.69%p 높아졌고 Profit Factor도 3.34에서 3.59로 개선됐습니다. 확인선을 추가한 목적은 실제 데이터에서 어느 정도 작동했습니다.
6. 건너뛴 137건 가운데 손실은 136건이었다
2중 EMA 진입 571건 가운데 같은 상승 구간에서 3중 정배열까지 완성된 신호는 434건, 76.01%였습니다. 나머지 137건은 50일 EMA가 100일 EMA 위로 올라오기 전에 상승 구간이 끝났습니다.
| 3중 EMA가 건너뛴 2중 EMA 거래 | 건수 |
|---|---|
| 전체 | 137건 |
| 손실 거래 | 136건 |
| 수익 거래 | 1건 |
| 20거래일 이내 손실 | 90건 |
이 결과가 휩쏘 감소를 설명합니다. 짧은 반등에서는 20일 EMA만 빠르게 올라가고 50일 EMA는 100일 EMA 아래에 머무르는 경우가 많았습니다. 3중 정배열을 기다리자 이런 짧은 상승 구간 대부분을 거래하지 않았습니다.
그렇다고 50일 EMA를 추가하면 손실만 완벽하게 제거된다고 일반화할 수는 없습니다. 이 숫자는 고정한 30종목과 기간, 20·50·100일 조합에서 나온 결과입니다. 다른 시장과 기간에서는 건너뛴 수익 거래가 더 많아질 수 있습니다.
7. 휩쏘를 줄인 대가는 5거래일의 지연이었다
3중 정배열이 완성된 434개 신호는 2중 EMA 신호보다 중앙값 5거래일 늦었습니다. 일부 신호는 20거래일 이상 기다린 뒤에야 확인됐습니다.
이 지연은 두 가지 결과를 만듭니다.
- 짧게 끝나는 반등에는 진입하지 않습니다.
- 길게 이어지는 추세에도 더 높은 가격에서 늦게 진입합니다.
그래서 휩쏘와 MDD는 줄었지만 누적수익률도 22.36%p 낮아졌습니다. 3중 EMA의 자산곡선은 2중 EMA보다 매끄러운 구간이 많았지만 강한 추세의 초반 수익 일부를 놓쳤습니다.
필터는 정보를 공짜로 더해주지 않습니다. 확인이 늘어날수록 오신호는 줄지만 진입 속도도 느려집니다.
8. 수익률이 낮아졌는데 전략은 개선된 것인가
한 숫자로는 답할 수 없습니다.
누적수익률이 가장 중요하다면 이번 표본에서는 2중 EMA가 앞섰습니다. 반면 짧은 손실 거래, MDD, 승률, Profit Factor와 시장 노출 시간을 함께 보면 3중 EMA 확인이 더 안정적이었습니다.
| 목표 | 더 유리했던 규칙 |
|---|---|
| 높은 최종 누적수익률 | 2중 EMA |
| 낮은 MDD | 3중 EMA 확인 |
| 적은 휩쏘와 거래 횟수 | 3중 EMA 확인 |
| 빠른 추세 진입 | 2중 EMA |
| 높은 승률·Profit Factor | 3중 EMA 확인 |
이 선택은 누적수익률 옆에 MDD와 노출 비중을 놓아야 하는 이유와 연결됩니다. 수익률만 낮아졌다고 실패로 보거나 거래가 줄었다고 무조건 개선됐다고 판단하면 안 됩니다.
9. 이번 실험의 한계
첫째, 휩쏘를 20거래일 이내 손실로 정의했습니다. 관찰 기간을 바꾸면 휩쏘 수와 감소율도 달라집니다.
둘째, 50일 EMA는 진입 확인에만 사용했습니다. 3중 정배열이 깨져도 바로 청산하지 않았으며, ATR 추적 청산이나 재진입 규칙도 넣지 않았습니다. 여러 요소를 한꺼번에 바꾸지 않기 위한 선택입니다.
셋째, 현재 확인 가능한 대형·유동성 종목 30개를 사용해 상장폐지 종목이 빠진 생존 편향이 남아 있습니다.
넷째, yfinance 수정주가 일봉과 가정 비용을 사용했습니다. 세금, 호가 스프레드, 시장 충격과 주문 실패를 모두 재현한 결과는 아닙니다.
다섯째, 20·50·100일은 최적값이 아닙니다. 이 결과를 보고 50일 확인선만 고정하기 전에 다른 인접 기간에서도 휩쏘 감소가 유지되는지 확인해야 합니다.
10. 정리
이번 3중 EMA 실험에서 확인한 사실은 세 가지입니다.
- 50일 EMA 확인선을 추가하자 거래 수는 133건, 20거래일 이내 손실은 90건 줄었습니다.
- MDD, 승률과 Profit Factor는 개선됐지만 진입은 중앙값 5거래일 늦고 누적수익률은 22.36%p 낮았습니다.
- 확인선은 짧은 손실 신호를 효과적으로 걸렀지만 강한 추세의 초반 수익까지 일부 포기하게 했습니다.
이동평균선을 늘리는 목적은 차트를 복잡하게 만드는 것이 아닙니다. 어떤 신호를 건너뛰고 그 대가로 무엇을 잃는지 숫자로 확인하는 데 있습니다.
다음 글에서는 가격 변화율 ROC 신호에 ADX 추세 강도 필터를 추가합니다. 가짜 신호가 줄어드는지뿐 아니라 좋은 추세 초입을 얼마나 놓치는지도 같은 방식으로 비교합니다.
이 글은 과거 데이터를 이용한 백테스트 예제이며 특정 종목이나 전략의 미래 수익을 보장하지 않습니다. 실제 투자에서는 세금, 수수료, 슬리피지, 체결 가능성과 계좌 위험을 별도로 검토해야 합니다.