전일 종가보다 3%, 5%, 때로는 10% 넘게 오른 가격으로 장을 시작하면 강한 매수세가 들어온 것처럼 보입니다. 시가부터 급등한 종목을 놓치기 싫어 바로 따라 사기도 합니다. 그러나 갭의 크기만으로는 밤사이 생긴 수요가 장중에도 남아 있는지 알 수 없습니다.
더 먼저 확인할 문제도 있습니다. 일봉에서 갭 상승을 확인한 뒤 그날 시가에 샀다고 계산하면, 이미 결정된 시가를 보고 같은 가격에 체결한 셈입니다. 실제 주문에서는 시가 단일가가 결정된 뒤 그 가격을 다시 살 수 있다는 보장이 없습니다. 갭이 유지됐는지 종가까지 확인하려면 진입은 더 늦어집니다.
이번에는 3% 이상 갭 상승한 국내 주식을 대상으로 당일 갭 메움, 종가 위치, 상대 거래량을 차례로 확인했습니다. 모든 신호는 다음 거래일 시가에 진입하고 비용과 손절을 반영했습니다. 필터를 더할수록 결과가 좋아지는지보다, 무엇을 걸러냈고 그 대가로 어떤 기회를 잃었는지에 초점을 맞췄습니다.
핵심 결론
- 조건을 충족한 1,250일 가운데 320일, 25.6%는 장중 저가가 전일 종가까지 내려와 갭을 메웠습니다.
- 모든 갭을 다음 시가에 산 규칙은 누적수익률 +6.8%, MDD -12.6%였습니다. 당일 갭을 지킨 경우만 남기면 +13.8%, -5.6%로 바뀌었습니다.
- 종가 강도까지 확인하면 승률과 평균 거래수익률은 높아졌지만 누적수익률은 +10.2%로 낮아졌습니다. 거래량 조건까지 붙인 결과는 +5.0%였습니다.
- 세 구간으로 나누면 갭 미메움의 우위가 같은 크기로 반복되지 않았습니다. 갭 미메움은 확인 조건이지 성공 보장이 아닙니다.
- 에스엠 실패 사례에서는 8% 종가 손절 규칙을 사용했지만 다음 시가 급락 때문에 실제 거래손실이 -26.0%까지 벌어졌습니다.
갭 상승과 갭 메움을 먼저 정의합니다
갭 상승률은 당일 시가와 전일 종가의 차이입니다.
갭 상승률 = 당일 시가 ÷ 전일 종가 - 1
예를 들어 전일 종가가 10,000원이고 다음 날 시가가 10,500원이면 갭 상승률은 5%입니다. 이번 검증에서는 3% 이상을 후보로 정했습니다.
갭 메움은 장중 저가가 전일 종가까지 내려왔는지로 구분했습니다.
당일 갭 메움 = 당일 저가 ≤ 전일 종가
당일 갭 미메움 = 당일 저가 > 전일 종가
갭을 메우지 않았다는 말은 종가가 올랐다는 뜻이 아닙니다. 시가보다 종가가 낮아도 저가가 전일 종가 위에 남을 수 있습니다. 그래서 갭 메움 여부와 종가 강도를 따로 계산해야 합니다.
종가 강도는 종가가 시가보다 높고, 하루 고가·저가 범위의 위쪽 30% 안에서 끝난 경우로 정의했습니다.
종가 위치 = (종가 - 저가) ÷ (고가 - 저가)
강한 종가 = 종가 > 시가 그리고 종가 위치 ≥ 0.70
거래량은 당일 거래량을 당일 이전 20거래일 평균으로 나눈 상대 거래량을 사용했습니다. 현재 거래량을 평균에 섞지 않았습니다.
다섯 가지 규칙을 같은 조건으로 비교했습니다
30개 국내 종목의 2014년 1월부터 2025년 12월까지 수정 일봉을 사용했습니다. 거래가 거의 없는 날을 줄이기 위해 신호 이전 20거래일 평균 거래대금이 100억 원 이상인 경우만 포함했습니다.
| 규칙 | 신호 조건 | 체결 시점 | 비교 목적 |
|---|---|---|---|
| 낙관적 당일 시가 | 갭 3% 이상 | 신호일 시가 | 시가 확인과 같은 가격 체결을 가정한 참고값 |
| 모든 갭·다음 시가 | 갭 3% 이상 | 다음 거래일 시가 | 실제로 재현 가능한 기본 비교 |
| 당일 미메움 | 기본 조건 + 저가가 전일 종가 위 | 다음 거래일 시가 | 장중 매도 압력 확인 |
| 종가 강도 | 미메움 + 양봉 + 종가 위치 0.70 이상 | 다음 거래일 시가 | 마감까지 매수 우위가 남았는지 확인 |
| 거래량까지 | 종가 강도 + 상대 거래량 1.5배 이상 | 다음 거래일 시가 | 참여 증가를 추가로 확인 |
한 종목을 보유하는 동안 같은 종목의 새 신호에는 추가 진입하지 않았습니다. 진입가 대비 종가가 8% 이상 하락하면 다음 거래일 시가에 손절하고, 손절이 없으면 5거래일 보유 뒤 다음 시가에 청산했습니다. 매수와 매도에 각각 0.15% 비용을 적용했습니다.
한국거래소의 주식시장 거래 안내에 따르면 시가는 개장 단일가 매매로 결정되고 정규장에서는 연속 매매가 이뤄집니다. 시가가 확정된 뒤 신호를 확인하고도 그 시가에 체결됐다고 소급하는 방식은 실제 주문 순서를 재현하지 못합니다. 여기서 당일 시가 규칙은 수익 전략이 아니라 그 오류의 크기를 보는 참고선입니다.
다음 시가 체결과 비용까지 포함한 파이썬 코드
아래 코드는 yfinance 수정 일봉으로 다섯 규칙을 같은 종목과 기간에 계산합니다. 신호 확인, 다음 시가 체결, 손절, 비용, 자산 곡선과 MDD가 한 흐름에 들어 있습니다. 차트 제작 코드는 본문 예제에서 제외했습니다.
python -m pip install numpy pandas yfinance
#!/usr/bin/env python3
"""yfinance 수정 일봉으로 갭 상승의 지속·메우기 규칙을 비교한다."""
from __future__ import annotations
import numpy as np
import pandas as pd
import yfinance as yf
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 = "2012-01-01"
TEST_START = "2014-01-01"
TEST_END = "2026-01-01"
GAP_MINIMUM_RATE = 0.03
MINIMUM_AVERAGE_TRADING_VALUE = 10_000_000_000
CLOSE_LOCATION_MINIMUM = 0.70
RELATIVE_VOLUME_MINIMUM = 1.50
STOP_LOSS_RATE = 0.08
MAX_HOLDING_DAYS = 5
ONE_WAY_COST_RATE = 0.0015
def add_signals(price_data: pd.DataFrame) -> pd.DataFrame:
signal_data = price_data.copy()
signal_data["previous_close"] = signal_data["close"].shift(1)
signal_data["gap_rate"] = signal_data["open"] / signal_data["previous_close"] - 1.0
price_range = (signal_data["high"] - signal_data["low"]).replace(0.0, np.nan)
signal_data["close_location"] = (signal_data["close"] - signal_data["low"]) / price_range
signal_data["relative_volume"] = signal_data["volume"] / signal_data["volume"].shift(1).rolling(20).mean()
signal_data["average_trading_value"] = (signal_data["close"] * signal_data["volume"]).shift(1).rolling(20).mean()
liquid_market = signal_data["average_trading_value"] >= MINIMUM_AVERAGE_TRADING_VALUE
signal_data["gap_entry"] = (signal_data["gap_rate"] >= GAP_MINIMUM_RATE) & liquid_market
signal_data["gap_held_entry"] = signal_data["gap_entry"] & (signal_data["low"] > signal_data["previous_close"])
signal_data["close_strength_entry"] = signal_data["gap_held_entry"] & (signal_data["close"] > signal_data["open"]) & (signal_data["close_location"] >= CLOSE_LOCATION_MINIMUM)
signal_data["volume_entry"] = signal_data["close_strength_entry"] & (signal_data["relative_volume"] >= RELATIVE_VOLUME_MINIMUM)
return signal_data.loc[TEST_START:TEST_END].copy()
def run_backtest(signal_data: pd.DataFrame, entry_column: str, same_open_entry: bool = False) -> tuple[pd.Series, pd.Series, list[dict]]:
cash = 1.0
position_shares = 0.0
entry_price = 0.0
capital_before_entry = 0.0
entry_index = -1
active_trade: dict = {}
pending_order: tuple | None = None
equity_values: list[float] = []
exposure_values: list[float] = []
trades: list[dict] = []
for row_index, (date, price_row) in enumerate(signal_data.iterrows()):
if pending_order is not None:
action = pending_order[0]
if action == "buy" and position_shares == 0.0:
signal_date = pending_order[1]
capital_before_entry = cash
entry_price = float(price_row["open"])
position_shares = cash * (1.0 - ONE_WAY_COST_RATE) / entry_price
cash = 0.0
entry_index = row_index
active_trade = {**pending_order[2], "signal_date": signal_date, "entry_date": date, "entry_price": entry_price}
elif action == "sell" and position_shares > 0.0:
exit_price = float(price_row["open"])
cash = position_shares * exit_price * (1.0 - ONE_WAY_COST_RATE)
trades.append({**active_trade, "exit_date": date, "exit_price": exit_price, "exit_reason": pending_order[1], "holding_days": row_index - entry_index, "trade_return": cash / capital_before_entry - 1.0})
position_shares = 0.0
entry_price = 0.0
entry_index = -1
active_trade = {}
pending_order = None
if same_open_entry and position_shares == 0.0 and bool(price_row[entry_column]):
capital_before_entry = cash
entry_price = float(price_row["open"])
position_shares = cash * (1.0 - ONE_WAY_COST_RATE) / entry_price
cash = 0.0
entry_index = row_index
active_trade = {**signal_snapshot(price_row), "signal_date": date, "entry_date": date, "entry_price": entry_price}
if position_shares > 0.0:
close_return = float(price_row["close"]) / entry_price - 1.0
holding_days = row_index - entry_index + 1
if close_return <= -STOP_LOSS_RATE:
pending_order = ("sell", "종가 기준 8% 손절")
elif holding_days >= MAX_HOLDING_DAYS:
pending_order = ("sell", "5거래일 시간 청산")
elif not same_open_entry and bool(price_row[entry_column]):
pending_order = ("buy", date, signal_snapshot(price_row))
equity_values.append(cash + position_shares * float(price_row["close"]))
exposure_values.append(float(position_shares > 0.0))
if position_shares > 0.0:
exit_price = float(signal_data.iloc[-1]["close"])
cash = position_shares * exit_price * (1.0 - ONE_WAY_COST_RATE)
trades.append({**active_trade, "exit_date": signal_data.index[-1], "exit_price": exit_price, "exit_reason": "검증 종료", "holding_days": len(signal_data) - entry_index, "trade_return": cash / capital_before_entry - 1.0})
equity_values[-1] = cash
return (
pd.Series(equity_values, index=signal_data.index, name="equity"),
pd.Series(exposure_values, index=signal_data.index, name="exposure"),
trades,
)
def signal_snapshot(price_row: pd.Series) -> dict[str, float | bool]:
return {
"gap_rate": float(price_row["gap_rate"]),
"previous_close": float(price_row["previous_close"]),
"signal_open": float(price_row["open"]),
"signal_high": float(price_row["high"]),
"signal_low": float(price_row["low"]),
"signal_close": float(price_row["close"]),
"gap_filled_same_day": bool(price_row["low"] <= price_row["previous_close"]),
"close_location": float(price_row["close_location"]),
"relative_volume": float(price_row["relative_volume"]),
"average_trading_value": float(price_row["average_trading_value"]),
}
def summarize_strategy(equity_curves: list[pd.Series], exposure_curves: list[pd.Series], trades: list[dict]) -> dict[str, float | int]:
calendar = pd.DatetimeIndex(sorted(set().union(*(curve.index for curve in equity_curves))))
portfolio_equity = pd.concat([curve.reindex(calendar).ffill().fillna(1.0) for curve in equity_curves], axis=1).mean(axis=1)
portfolio_exposure = pd.concat([curve.reindex(calendar).fillna(0.0) for curve in exposure_curves], axis=1).mean(axis=1)
drawdown = portfolio_equity / portfolio_equity.cummax() - 1.0
trade_returns = pd.Series([trade["trade_return"] for trade in trades], dtype=float)
return {
"누적수익률": float(portfolio_equity.iloc[-1] - 1.0),
"MDD": float(drawdown.min()),
"평균노출": float(portfolio_exposure.mean()),
"거래수": len(trades),
"승률": float((trade_returns > 0.0).mean()),
"평균거래수익률": float(trade_returns.mean()),
"중앙거래수익률": float(trade_returns.median()),
}
def download_price_data(ticker: str) -> pd.DataFrame:
price_data = yf.download(ticker, start=DOWNLOAD_START, end=TEST_END, auto_adjust=True, progress=False, multi_level_index=False, threads=False)
price_data = price_data.rename(columns=str.lower).dropna(subset=["open", "high", "low", "close", "volume"]).copy()
if len(price_data) < 500:
raise RuntimeError(f"검증 데이터가 부족합니다: {ticker}")
return price_data
def main() -> None:
strategies = {
"낙관적 당일 시가": ("gap_entry", True),
"모든 갭·다음 시가": ("gap_entry", False),
"당일 미메움": ("gap_held_entry", False),
"종가 강도": ("close_strength_entry", False),
"거래량까지": ("volume_entry", False),
}
equity_by_strategy = {name: [] for name in strategies}
exposure_by_strategy = {name: [] for name in strategies}
trades_by_strategy = {name: [] for name in strategies}
for ticker in TICKERS:
signal_data = add_signals(download_price_data(ticker))
for strategy_name, (entry_column, same_open_entry) in strategies.items():
equity, exposure, trades = run_backtest(signal_data, entry_column, same_open_entry)
equity_by_strategy[strategy_name].append(equity)
exposure_by_strategy[strategy_name].append(exposure)
trades_by_strategy[strategy_name].extend(trades)
summary = pd.DataFrame(
[
{
"전략": name,
**summarize_strategy(
equity_by_strategy[name],
exposure_by_strategy[name],
trades_by_strategy[name],
),
}
for name in strategies
]
)
print(summary.to_string(index=False, formatters={"누적수익률": "{:+.1%}".format, "MDD": "{:.1%}".format, "평균노출": "{:.1%}".format, "승률": "{:.1%}".format, "평균거래수익률": "{:+.2%}".format, "중앙거래수익률": "{:+.2%}".format}))
if __name__ == "__main__":
main()
average_trading_value는 신호일 이전 값만 사용합니다. 반면 gap_rate, close_location, relative_volume은 신호일이 끝나야 모두 확정됩니다. 따라서 이 세 조건으로 고른 거래는 다음 거래일 시가에 진입합니다.
갭을 지킨 종목만 남기자 낙폭은 줄었습니다
30개 종목을 각각 운용한 뒤 동일 비중으로 합친 결과입니다.
| 규칙 | 누적수익률 | MDD | 평균 노출 | 거래 수 | 승률 | 평균 거래수익률 | 중앙값 |
|---|---|---|---|---|---|---|---|
| 낙관적 당일 시가 | +3.0% | -13.4% | 5.3% | 1,009회 | 47.2% | +0.35% | -0.44% |
| 모든 갭·다음 시가 | +6.8% | -12.6% | 5.2% | 965회 | 48.2% | +0.34% | -0.56% |
| 당일 미메움 | +13.8% | -5.6% | 4.2% | 762회 | 50.0% | +0.62% | -0.01% |
| 종가 강도 | +10.2% | -4.0% | 1.5% | 280회 | 53.6% | +1.15% | +0.24% |
| 거래량까지 | +5.0% | -4.6% | 1.0% | 179회 | 49.7% | +0.94% | -0.02% |

모든 갭을 다음 시가에 산 기본 규칙과 비교하면 당일 미메움 규칙은 누적수익률이 높고 MDD는 절반 이하였습니다. 적어도 이 표본에서는 장중 저가가 전일 종가까지 되밀린 날을 제외하는 것이 손실 구간을 줄이는 데 도움이 됐습니다.
그러나 종가 강도와 거래량을 더할 때 수익률이 계속 높아지지는 않았습니다. 종가 강도 규칙은 승률 53.6%, 평균 거래수익률 +1.15%로 거래 단위의 분포가 좋아졌지만 거래는 280회, 평균 노출은 1.5%까지 줄었습니다. 현금으로 기다린 시간이 늘면서 전체 누적수익률은 당일 미메움 규칙보다 낮았습니다.
상대 거래량 1.5배를 더한 규칙은 거래를 179회로 줄였지만 승률과 누적수익률이 모두 낮아졌습니다. 거래량 급증이 관심 증가를 보여 줄 수는 있어도, 그 관심이 다음 날 이후의 매수 우위라는 뜻은 아닙니다.
당일 메운 갭의 거래 분포는 더 약했습니다
조건을 충족한 1,250일 중 320일은 당일 전일 종가까지 하락했습니다. 전체 후보의 25.6%입니다. 같은 종목을 보유 중인 중복 신호를 제외하고 실제 기본 규칙이 진입한 965회를 나누면 다음과 같습니다.
| 신호일 상태 | 거래 수 | 승률 | 평균 거래수익률 | 중앙 거래수익률 |
|---|---|---|---|---|
| 당일 갭 메움 | 229회 | 44.5% | -0.30% | -0.98% |
| 당일 갭 미메움 | 736회 | 49.3% | +0.53% | -0.08% |

당일 갭을 메운 묶음은 평균과 중앙값이 모두 음수였습니다. 미메움 묶음은 평균이 양수였지만 중앙값은 여전히 -0.08%였습니다. 절반에 가까운 거래가 손실이라는 뜻이며, 소수의 큰 수익 거래가 평균을 끌어올렸습니다.
두 묶음은 무작위로 나뉜 실험군이 아닙니다. 종목, 시장 국면과 사건의 차이가 함께 들어 있습니다. 따라서 이 결과를 갭 미메움이 수익을 원인적으로 만든다고 해석할 수는 없습니다. 다음 단계의 후보를 줄이는 조건으로만 사용해야 합니다.
기간을 나누면 우위는 일정하지 않았습니다
전체 기간의 +13.8%만 보면 당일 미메움 규칙이 명확한 정답처럼 보입니다. 같은 조건을 세 구간으로 나누면 결론이 약해집니다.
| 규칙 | 2014~2017 | 2018~2021 | 2022~2025 | 전체 기간 |
|---|---|---|---|---|
| 모든 갭·다음 시가 | -1.8% | +11.2% | -0.6% | +6.8% |
| 당일 미메움 | -1.4% | +10.5% | +5.1% | +13.8% |
| 종가 강도 | -0.2% | +8.6% | +0.8% | +10.2% |
| 거래량까지 | +0.0% | +5.2% | -1.2% | +5.0% |

2014~2017년에는 네 규칙 모두 의미 있는 수익을 만들지 못했습니다. 2018~2021년에는 모든 갭을 산 규칙의 누적수익률이 당일 미메움 규칙보다 오히려 조금 높았습니다. 2022~2025년에만 기본 규칙이 손실인 가운데 미메움 규칙이 +5.1%를 기록했습니다.
MDD는 종가 강도 조건에서 세 구간 모두 상대적으로 작았지만, 낮은 낙폭의 상당 부분은 평균 노출이 1.5%에 불과한 데서 나왔습니다. 필터의 질과 현금 비중의 효과를 구분하지 않으면 안전성이 좋아졌다고 과장할 수 있습니다.
성공 사례: 갭을 지키고 종가까지 강했던 셀트리온
2017년 12월 27일 셀트리온은 수정주가 기준 전일 종가 141,524원에서 155,459원으로 출발했습니다. 갭 상승률은 9.85%였습니다. 장중 저가 151,017원이 전일 종가 위에 남았고, 종가는 고가 범위의 98.2% 위치에서 끝났습니다. 상대 거래량도 2.78배였습니다.

주황색 원은 신호일 시가, 초록색 삼각형은 다음 거래일 진입가, 빨간색 역삼각형은 청산가입니다. 다음 거래일 수정 시가 164,268원에 진입해 5거래일 규칙이 끝난 뒤 206,168원에 청산한 거래수익률은 비용 차감 후 +25.13%였습니다.
이 사례에서 중요한 것은 큰 갭 자체가 아닙니다. 갭이 장중에 무너지지 않았고, 종가가 고가에 가깝게 끝났으며, 다음 날에도 더 높은 가격을 지불할 매수세가 이어졌습니다. 다만 이 한 거래가 전체 수익의 일부를 크게 끌어올렸으므로 같은 형태가 반복된다고 가정하면 안 됩니다.
실패 사례: 모든 확인 조건을 통과하고도 급락한 에스엠
2023년 3월 7일 에스엠은 수정주가 기준 전일 종가 126,043원에서 142,610원으로 출발해 13.14% 갭 상승했습니다. 저가는 141,932원으로 전일 종가를 메우지 않았고 종가는 당일 고가에서 끝났습니다. 종가 위치 100%, 상대 거래량 6.41배로 완성 규칙의 모든 조건을 충족했습니다.

다음 거래일 수정 시가 149,295원에 진입했지만 가격은 빠르게 하락했습니다. 종가 기준 손실이 8%를 넘은 뒤 다음 거래일 시가 110,833원에 청산됐고 비용 차감 거래수익률은 -25.99%였습니다.
이 사례는 두 가지 한계를 동시에 보여 줍니다. 첫째, 갭 미메움·강한 종가·거래량 급증을 모두 확인해도 이후 가격을 보장하지 못합니다. 둘째, 종가 손절은 손실을 8%에 고정하는 주문이 아닙니다. 종가에 손절 조건이 확정된 뒤 다음 날 더 낮게 출발하면 실제 손실은 기준보다 크게 벌어집니다.
초기·추적·시간 손절 규칙 비교에서도 손절선과 실제 체결 손실을 구분해야 하는 이유를 확인할 수 있습니다.
필터를 추가하기 전에 역할부터 정해야 합니다
이번 결과에서 각 조건의 역할은 서로 달랐습니다.
- 갭 3%는 밤사이 가격 재평가가 큰 종목을 찾습니다.
- 당일 미메움은 전일 종가까지 되돌린 신호를 제외합니다.
- 종가 강도는 장 마감까지 매수 우위가 남았는지 확인합니다.
- 상대 거래량은 평소보다 참여가 늘었는지 측정합니다.
- 평균 거래대금은 수익 방향이 아니라 주문 가능한 규모를 점검합니다.
서로 다른 질문에 답하는 조건을 모두 붙인다고 하나의 강력한 신호가 되지는 않습니다. 필터가 거래 수를 줄이면 누적수익률, MDD, 승률과 평균 노출이 함께 변합니다. 수익률 하나만 비교하면 필터가 손실 신호를 제거한 것인지, 단순히 시장에 덜 머문 것인지 구분할 수 없습니다.
실전에 적용하기 전에는 다음 순서로 확인하는 편이 낫습니다.
- 갭을 확인한 뒤 어느 가격에 실제 주문할 수 있는지 정합니다.
- 장중 저가가 전일 종가를 메웠는지와 종가 위치를 분리해 기록합니다.
- 필터를 한 번에 하나씩 추가하고 거래 수와 평균 노출을 함께 비교합니다.
- 손절 기준과 실제 다음 시가 청산 손실의 차이를 따로 측정합니다.
- 전체 기간뿐 아니라 상승장·하락장과 여러 구간에서 같은 방향이 남는지 확인합니다.
일봉 검증이 놓치는 것도 있습니다
이번 결과에는 다음 한계가 있습니다.
- 현재 확인 가능한 30종목을 고정해 과거로 계산했으므로 상장폐지 종목과 과거 유니버스 변화가 빠진 생존자 편향이 있습니다.
- yfinance 수정 일봉은 배당과 주식분할을 반영하지만 실제 당시 호가, 주문 잔량과 체결 순서를 재현하지 않습니다.
- 일봉의 고가·저가만으로는 갭을 메운 시각과 이후 회복 순서를 알 수 없습니다.
- 상한가, 변동성완화장치, 뉴스 직후의 미체결과 가격 충격을 0.15% 비용만으로 설명할 수 없습니다.
- 종목별 자산을 동일 비중으로 합쳤으며 동시에 나온 신호의 포트폴리오 보유 한도는 적용하지 않았습니다.
- 3%, 종가 위치 0.70, 상대 거래량 1.5배와 5거래일은 비교를 위해 고정한 값이지 최적 변수나 미래 성과 예측값이 아닙니다.
- 데이터 제공자가 과거 수정값을 정정하면 코드를 다시 실행한 세부 수치는 소폭 달라질 수 있습니다.
백테스트 결과를 믿기 전 확인할 3가지에서 생존자 편향과 과최적화 문제를 함께 볼 수 있습니다.
갭 상승을 매수 신호 하나로 보지 않습니다
갭 상승은 밤사이 가격이 크게 이동했다는 사실을 보여 줍니다. 그 자체로 다음 날 상승을 보장하지 않습니다. 이번 표본에서는 당일 갭을 메운 신호를 제외했을 때 전체 성과와 낙폭이 좋아졌지만, 구간별 우위는 일정하지 않았습니다. 종가와 거래량 확인을 더해도 수익률은 순서대로 높아지지 않았습니다.
따라서 갭 상승 전략의 핵심은 큰 갭을 찾는 일이 아닙니다. 확인 가능한 시점 뒤에 살 수 있는 가격을 사용하고, 갭이 무너진 날을 분리하며, 다음 날 손절 가격까지 벌어질 수 있다는 점을 규칙에 넣는 일입니다.
이 글은 공개 가격 데이터를 이용한 과거 검증 예시입니다. 특정 종목의 매수·매도 추천이나 미래 수익을 의미하지 않으며, 실제 투자 판단과 책임은 투자자 본인에게 있습니다.