액면분할 날 주가가 -80%로 찍혔다 — 수정주가 처리
2026-02-28 · 3 min read
$ SELECT close, adj_factor FROM prices
5000 | 0.2 — 1:5 분할
폭락 신호가 잔뜩 나왔다
역추세 전략을 돌렸더니 특정 날짜에 -50%, -80% 하락이 여러 종목에서 잡혔다. 전부 액면분할이었다. 5,000원짜리를 1:5로 분할하면 다음 날 주가는 1,000원이다. 가격은 80% 떨어졌지만 내 돈은 그대로다. 주식 수가 5배가 됐으니까.
이걸 처리 안 하면 수익률 계산이 통째로 틀어진다.
분할 전 종가 5000, 분할 후 시가 1000
→ 순진한 수익률: -80%
→ 실제 수익률: 0%
세 종류를 구분한다
| 이벤트 | 가격에 미치는 영향 | 처리 |
|---|---|---|
| 액면분할·병합 | 비율만큼 기계적 변동 | 반드시 조정 |
| 현금배당 | 배당락일에 배당금만큼 하락 | 총수익 기준이면 조정 |
| 유상·무상증자 | 발행가에 따라 희석 | 반드시 조정 |
액면분할과 증자는 선택의 여지가 없다. 안 하면 틀린 값이다. 배당은 뭘 재느냐에 달렸다.
- 가격 수익률만 볼 거면 조정 안 함
- 실제로 손에 쥐는 총수익(total return)을 볼 거면 조정
전략 성과를 평가할 거면 후자가 맞다. 배당수익률 3%를 무시하면 장기 백테스트에서 누적 차이가 크다. 다만 세금은 별도라 총수익 기준도 실제보다 낙관적이라는 걸 알고 봐야 한다.
수정계수를 곱한다
이벤트 이전 가격에 계수를 곱해서 현재 기준으로 끌어내린다.
1:5 분할 → factor = 1/5 = 0.2
분할 이전 모든 가격 × 0.2, 거래량 ÷ 0.2
배당은 배당락 직전 종가 기준으로 계산한다.
factor = (close - dividend) / close
이벤트가 여러 번이면 누적 곱이다. 각각 따로 적용하면 안 되고, 해당 시점 이후의 모든 계수를 곱한 값을 써야 한다.
def adjust(df: pd.DataFrame, events: list[Event]) -> pd.DataFrame:
df = df.copy()
df["factor"] = 1.0
for e in events: # 이벤트 시점 이전 구간에 계수 반영
df.loc[df.index < e.ex_date, "factor"] *= e.factor
for c in ["open", "high", "low", "close"]:
df[c] = df[c] * df["factor"]
df["volume"] = df["volume"] / df["factor"]
return df거래량을 나누는 걸 자주 빼먹는다. 가격만 조정하면 거래대금(가격 × 거래량)이 안 맞는다. 거래대금 필터를 쓰는 전략이면 여기서 틀어진다.
원본을 덮어쓰지 않는다
제일 중요한 규칙이다. 수정주가는 오늘 기준으로 계산된 값이라 새 이벤트가 생기면 과거 전체가 다시 바뀐다.
-- 원본은 그대로 두고
close BIGINT NOT NULL, -- 그날 실제로 찍힌 가격
adj_factor NUMERIC NOT NULL DEFAULT 1,adj_close를 저장하는 대신 계수를 저장하고 조회 때 곱한다.
새 분할이 생기면 계수 컬럼만 갱신하면 되고, 원본 가격은 건드릴 일이 없다.
원본이 필요한 이유도 분명하다. 2020년에 실제로 얼마에 거래됐는지는 호가 단위·상하한가 검증에 필요하고, 수정주가로는 복원이 안 된다.
룩어헤드가 여기서도 나온다
수정주가는 미래 정보다. 2024년에 일어난 분할이 2020년 가격에 반영돼 있으니까.
가격 수익률을 계산할 때는 계수가 분자·분모에서 상쇄되어 문제가 없다. 그런데 절대 가격 기준 조건을 쓰면 그 순간 미래를 보게 된다.
# 위험: 수정주가로 "1만원 이하 종목" 필터
universe = df[df["adj_close"] < 10000]2020년에 5만원이던 종목이 분할 때문에 수정주가 1만원으로 보인다. 당시엔 저가주가 아니었다. 이런 조건은 원본 가격으로 걸어야 한다.
검증
def test_split_no_fake_crash():
"""분할일 수익률이 0 근처여야 한다"""
r = adjusted_returns(sym, around=split_date)
assert abs(r) < 0.15 # 기계적 -80%가 사라졌는지
def test_factor_is_cumulative():
"""분할이 두 번이면 계수가 곱해져야 한다"""
df = adjust(prices, [Event(d1, 0.2), Event(d2, 0.5)])
assert df.loc[before_d1, "factor"] == pytest.approx(0.1)첫 테스트를 전 종목에 돌려서 일간 수익률 ±40%를 넘는 날을 전부 뽑아보면, 남은 것들이 진짜 이벤트인지 미처리 코퍼레이트 액션인지 금방 드러난다. 상한가는 30%가 한도라 그걸 넘는 값은 대부분 데이터 문제다.
한 줄 요약
원본 가격 + 계수 컬럼으로 저장하고, 수익률에만 수정주가를 쓰고, 절대 가격 조건에는 원본을 쓴다.