파이썬 변동성 돌파 전략: 래리 윌리엄스 k값 최적화

변동성 돌파 전략이란? 래리 윌리엄스의 핵심 아이디어

변동성 돌파 전략(Volatility Breakout Strategy)은 전설적인 트레이더 래리 윌리엄스(Larry Williams)가 1987년 월드컵 트레이딩 챔피언십에서 연 수익률 11,376%를 달성하며 유명해진 전략입니다. 핵심 아이디어는 간단합니다. 전일 변동폭(고가 – 저가)의 일정 비율(k)만큼 당일 시가에서 상승 돌파하면 매수하고, 당일 종가에 청산하는 것입니다.

이 전략이 퀀트 자동매매 입문자에게 인기 있는 이유는 명확합니다. 규칙이 단순하여 코드 구현이 쉽고, 백테스팅 결과가 다양한 시장에서 유의미한 수익을 보여주며, 파라미터가 k값 하나뿐이라 과최적화(overfitting) 위험이 상대적으로 낮습니다.

변동성 돌파 전략의 매매 규칙 상세

변동성 돌파 전략의 매매 규칙을 수식으로 정리하면 다음과 같습니다.

  • 목표 매수가 = 당일 시가 + (전일 고가 − 전일 저가) × k
  • 매수 조건: 당일 현재가 ≥ 목표 매수가
  • 청산: 다음 날 시가에 전량 매도 (또는 당일 종가 청산)
  • k값: 일반적으로 0.4 ~ 0.6 사이 (기본값 0.5)

k값이 클수록 진입 기준이 엄격해져 거래 횟수가 줄고, 작을수록 민감하게 반응하여 거래 횟수가 늘어납니다. 최적의 k값은 종목과 시장 상황에 따라 달라지며, 이를 백테스팅으로 검증하는 것이 핵심입니다.

파이썬 환경 설정 및 데이터 수집

변동성 돌파 전략을 파이썬으로 구현하기 위해 필요한 라이브러리를 설치합니다.

pip install yfinance pandas numpy matplotlib

먼저 주가 데이터를 수집합니다. 여기서는 yfinance를 활용하여 실제 시장 데이터를 가져옵니다.

import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# 데이터 수집 (예: 삼성전자 또는 SPY ETF)
ticker = "SPY"
df = yf.download(ticker, start="2020-01-01", end="2025-12-31")
df = df[['Open', 'High', 'Low', 'Close', 'Volume']]
df.columns = ['open', 'high', 'low', 'close', 'volume']
print(f"데이터 기간: {df.index[0]} ~ {df.index[-1]}")
print(f"총 거래일: {len(df)}일")
print(df.tail())

변동성 돌파 전략 핵심 구현

전일 변동폭을 계산하고, 목표 매수가를 설정하여 돌파 여부를 판단하는 코드입니다.

def volatility_breakout(df, k=0.5):
    """
    변동성 돌파 전략 백테스팅
    - k: 변동폭 계수 (0.0 ~ 1.0)
    """
    df = df.copy()

    # 전일 변동폭 계산
    df['prev_range'] = (df['high'] - df['low']).shift(1)

    # 목표 매수가 = 당일 시가 + 전일 변동폭 × k
    df['target_price'] = df['open'] + df['prev_range'] * k

    # 돌파 여부: 당일 고가가 목표가 이상이면 매수 성공
    df['signal'] = np.where(df['high'] >= df['target_price'], 1, 0)

    # 수익률 계산: 목표가에 매수 → 다음날 시가에 매도
    df['next_open'] = df['open'].shift(-1)
    df['return'] = np.where(
        df['signal'] == 1,
        (df['next_open'] - df['target_price']) / df['target_price'],
        0
    )

    # 누적 수익률 (복리)
    df['cum_return'] = (1 + df['return']).cumprod()

    return df


# k=0.5로 백테스팅 실행
result = volatility_breakout(df, k=0.5)

# 결과 요약
total_trades = result['signal'].sum()
winning_trades = (result[result['signal'] == 1]['return'] > 0).sum()
win_rate = winning_trades / total_trades * 100
total_return = (result['cum_return'].iloc[-2] - 1) * 100

print(f"총 거래 횟수: {total_trades}회")
print(f"승률: {win_rate:.1f}%")
print(f"누적 수익률: {total_return:.1f}%")

최적 k값 탐색: 파라미터 최적화

k값에 따라 전략 성과가 크게 달라집니다. 0.1부터 0.9까지 다양한 k값을 테스트하여 최적 파라미터를 찾아보겠습니다.

def optimize_k(df, k_range=np.arange(0.1, 1.0, 0.1)):
    """다양한 k값에 대해 성과 지표를 비교"""
    results = []
    for k in k_range:
        bt = volatility_breakout(df, k=k)
        trades = bt['signal'].sum()
        if trades == 0:
            continue

        trade_returns = bt[bt['signal'] == 1]['return']
        win_rate = (trade_returns > 0).mean() * 100
        cum_ret = (bt['cum_return'].iloc[-2] - 1) * 100
        sharpe = trade_returns.mean() / trade_returns.std() * np.sqrt(252) if trade_returns.std() > 0 else 0

        results.append({
            'k': round(k, 1),
            'trades': trades,
            'win_rate': round(win_rate, 1),
            'cum_return': round(cum_ret, 1),
            'sharpe': round(sharpe, 2)
        })

    return pd.DataFrame(results)


opt_df = optimize_k(df)
print(opt_df.to_string(index=False))

# 최적 k값 시각화
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
axes[0].bar(opt_df['k'], opt_df['cum_return'], color='steelblue')
axes[0].set_title('누적 수익률 by k')
axes[0].set_xlabel('k값')
axes[0].set_ylabel('수익률 (%)')

axes[1].bar(opt_df['k'], opt_df['win_rate'], color='coral')
axes[1].set_title('승률 by k')
axes[1].set_xlabel('k값')
axes[1].set_ylabel('승률 (%)')

axes[2].bar(opt_df['k'], opt_df['sharpe'], color='seagreen')
axes[2].set_title('샤프 비율 by k')
axes[2].set_xlabel('k값')
axes[2].set_ylabel('Sharpe Ratio')

plt.tight_layout()
plt.savefig('k_optimization.png', dpi=150)
plt.show()

리스크 관리: 손절매와 노이즈 비율 필터

변동성 돌파 전략의 약점은 횡보장에서의 잦은 손실입니다. 이를 보완하기 위해 두 가지 리스크 관리 기법을 추가합니다.

1. 손절매(Stop-Loss) 적용

def volatility_breakout_with_stoploss(df, k=0.5, stop_pct=0.02):
    """손절매가 포함된 변동성 돌파 전략"""
    df = df.copy()
    df['prev_range'] = (df['high'] - df['low']).shift(1)
    df['target_price'] = df['open'] + df['prev_range'] * k
    df['signal'] = np.where(df['high'] >= df['target_price'], 1, 0)
    df['next_open'] = df['open'].shift(-1)

    # 손절가 = 매수가 × (1 - stop_pct)
    df['stop_price'] = df['target_price'] * (1 - stop_pct)

    # 손절 발동 여부: 매수 후 당일 저가가 손절가 이하
    df['stopped'] = np.where(
        (df['signal'] == 1) & (df['low'] <= df['stop_price']),
        1, 0
    )

    # 수익률: 손절 시 -stop_pct, 아니면 다음날 시가 매도
    df['return'] = np.where(
        df['signal'] == 0, 0,
        np.where(df['stopped'] == 1, -stop_pct,
                 (df['next_open'] - df['target_price']) / df['target_price'])
    )
    df['cum_return'] = (1 + df['return']).cumprod()
    return df

2. 노이즈 비율(Noise Ratio) 필터

노이즈 비율은 시장의 추세 강도를 측정하는 지표입니다. 값이 낮을수록 추세가 강하고, 높을수록 횡보(노이즈)가 심합니다. 이 필터를 적용하면 추세가 강한 날에만 진입하여 불필요한 거래를 줄일 수 있습니다.

def add_noise_filter(df, lookback=20):
    """
    노이즈 비율 필터
    noise = 1 - |종가 - N일전 종가| / 일별 변동폭 합
    낮을수록 추세 강함 → 0.5 이하일 때만 진입
    """
    df = df.copy()

    # N일간 절대 변동폭 합
    df['abs_range'] = abs(df['close'] - df['open'])
    df['sum_abs_range'] = df['abs_range'].rolling(lookback).sum()

    # N일간 순 변동폭
    df['net_change'] = abs(df['close'] - df['close'].shift(lookback))

    # 노이즈 비율
    df['noise'] = 1 - (df['net_change'] / df['sum_abs_range'])

    return df


df_filtered = add_noise_filter(df)
# 노이즈 비율이 0.5 이하일 때만 매매
df_filtered['trend_ok'] = np.where(df_filtered['noise'] < 0.5, 1, 0)
print(f"추세 구간 비율: {df_filtered['trend_ok'].mean()*100:.1f}%")

고급 전략: 종목 분산과 자금 관리

단일 종목에 전액 투자하는 것은 위험합니다. 실전에서는 여러 종목에 분산 투자하고 포지션 크기를 조절해야 합니다.

def multi_asset_breakout(tickers, k=0.5, start="2020-01-01", end="2025-12-31"):
    """
    다중 종목 변동성 돌파 전략
    - 균등 배분 방식으로 포트폴리오 구성
    """
    all_returns = pd.DataFrame()

    for ticker in tickers:
        data = yf.download(ticker, start=start, end=end)
        data.columns = ['open', 'high', 'low', 'close', 'volume']
        bt = volatility_breakout(data, k=k)
        all_returns[ticker] = bt['return']

    # 균등 배분 포트폴리오 수익률
    all_returns['portfolio'] = all_returns.mean(axis=1)
    all_returns['cum_portfolio'] = (1 + all_returns['portfolio']).cumprod()

    return all_returns


# ETF 포트폴리오 예시
tickers = ['SPY', 'QQQ', 'IWM', 'EFA', 'GLD']
portfolio = multi_asset_breakout(tickers, k=0.5)

final_return = (portfolio['cum_portfolio'].iloc[-1] - 1) * 100
print(f"포트폴리오 누적 수익률: {final_return:.1f}%")

성과 분석: MDD와 샤프 비율 계산

전략의 실전 적용 가능성을 평가하기 위해 최대 낙폭(MDD)샤프 비율을 계산합니다. MDD는 전략이 겪을 수 있는 최악의 손실폭을 보여주는 중요한 리스크 지표입니다.

def calculate_metrics(cum_returns, daily_returns):
    """전략 성과 지표 계산"""
    # 최대 낙폭 (MDD)
    peak = cum_returns.cummax()
    drawdown = (cum_returns - peak) / peak
    mdd = drawdown.min() * 100

    # 연율화 수익률
    total_days = len(daily_returns)
    total_return = cum_returns.iloc[-1] / cum_returns.iloc[0]
    annual_return = (total_return ** (252 / total_days) - 1) * 100

    # 샤프 비율 (무위험 수익률 4% 가정)
    excess_return = daily_returns.mean() * 252 - 0.04
    annual_vol = daily_returns.std() * np.sqrt(252)
    sharpe = excess_return / annual_vol if annual_vol > 0 else 0

    return {
        '연율화 수익률': f"{annual_return:.1f}%",
        '최대 낙폭(MDD)': f"{mdd:.1f}%",
        '샤프 비율': f"{sharpe:.2f}",
        '연율화 변동성': f"{annual_vol*100:.1f}%"
    }


# 성과 분석 실행
result = volatility_breakout(df, k=0.5)
metrics = calculate_metrics(
    result['cum_return'].dropna(),
    result['return'].dropna()
)
for key, val in metrics.items():
    print(f"{key}: {val}")

실전 자동매매 적용 시 고려사항

백테스팅에서 좋은 결과가 나왔더라도 실전 적용에는 추가 고려가 필요합니다.

  • 슬리피지(Slippage): 실제 체결가는 목표가와 차이가 있습니다. 호가 단위와 유동성을 고려하여 0.1~0.3%의 슬리피지를 백테스트에 반영해야 합니다.
  • 수수료: 국내 주식 약 0.015%, 해외 주식은 브로커마다 다릅니다. 잦은 거래 전략은 수수료 영향이 큽니다.
  • 데이터 지연: 실시간 데이터 피드의 지연 시간을 고려해야 합니다. API 호출 주기는 최소 1초 이상 권장합니다.
  • 시장 영향: 소형주에서 큰 금액을 매매하면 가격에 영향을 줄 수 있습니다.
  • 과최적화 방지: 훈련 기간과 검증 기간을 분리(Walk-Forward Analysis)하여 전략의 견고성을 확인하세요.

변동성 돌파 전략 변형: 적응형 k값

고정 k값 대신 최근 N일 성과를 기반으로 k값을 동적으로 조정하는 적응형(Adaptive) 방식을 구현할 수 있습니다.

def adaptive_volatility_breakout(df, lookback=20):
    """
    적응형 변동성 돌파: 최근 N일 최적 k값을 매일 재계산
    """
    df = df.copy()
    df['prev_range'] = (df['high'] - df['low']).shift(1)
    df['next_open'] = df['open'].shift(-1)
    df['adaptive_k'] = 0.5  # 초기값
    df['signal'] = 0
    df['return'] = 0.0

    for i in range(lookback, len(df) - 1):
        # 최근 N일 데이터로 최적 k 찾기
        window = df.iloc[i-lookback:i]
        best_k, best_return = 0.5, -999

        for k in np.arange(0.1, 1.0, 0.1):
            target = window['open'] + window['prev_range'] * k
            signals = window['high'] >= target
            returns = np.where(
                signals,
                (window['next_open'] - target) / target,
                0
            )
            cum = (1 + returns).prod() - 1
            if cum > best_return:
                best_k, best_return = k, cum

        # 오늘 적용
        df.iloc[i, df.columns.get_loc('adaptive_k')] = best_k
        target_price = df.iloc[i]['open'] + df.iloc[i]['prev_range'] * best_k

        if df.iloc[i]['high'] >= target_price:
            df.iloc[i, df.columns.get_loc('signal')] = 1
            df.iloc[i, df.columns.get_loc('return')] = (
                df.iloc[i]['next_open'] - target_price
            ) / target_price

    df['cum_return'] = (1 + df['return']).cumprod()
    return df

시각화: 전략 수익 곡선과 Buy & Hold 비교

# 전략 vs Buy & Hold 비교 차트
result = volatility_breakout(df, k=0.5)
result['buy_hold'] = result['close'] / result['close'].iloc[0]

plt.figure(figsize=(14, 7))
plt.plot(result.index, result['cum_return'], label='변동성 돌파 전략', linewidth=2)
plt.plot(result.index, result['buy_hold'], label='Buy & Hold', linewidth=2, alpha=0.7)
plt.fill_between(result.index, result['cum_return'], alpha=0.1)
plt.title('변동성 돌파 전략 vs Buy & Hold 수익 곡선', fontsize=14)
plt.xlabel('날짜')
plt.ylabel('누적 수익 배수')
plt.legend(fontsize=12)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('strategy_comparison.png', dpi=150)
plt.show()

자주 묻는 질문 (FAQ)

변동성 돌파 전략은 어떤 시장에서 잘 작동하나요?

추세가 강한 시장에서 가장 효과적입니다. 강한 상승 추세나 하락 후 반등 구간에서 높은 승률을 보입니다. 반면 좁은 박스권 횡보장에서는 잦은 손절이 발생할 수 있어 노이즈 비율 필터와 함께 사용하는 것이 좋습니다.

k값은 어떻게 설정해야 하나요?

일반적으로 0.4~0.6 범위가 가장 안정적입니다. k=0.5가 가장 널리 사용되는 기본값이며, 변동성이 큰 종목은 k를 높이고 안정적인 종목은 낮추는 것이 좋습니다. 반드시 백테스팅으로 검증한 후 적용하세요.

암호화폐에도 적용할 수 있나요?

네, 변동성 돌파 전략은 비트코인, 이더리움 등 암호화폐에서도 활용 가능합니다. 다만 24시간 운영되는 암호화폐 시장의 특성상 '시가'와 '종가'를 UTC 00:00 기준으로 정의하는 등 기준 시간 설정이 중요합니다.

결론: 변동성 돌파 전략의 실전 활용 로드맵

변동성 돌파 전략은 퀀트 자동매매의 가장 검증된 출발점입니다. 단순한 규칙 기반이면서도 실전에서 유의미한 성과를 보여주는 전략으로, 본 가이드에서 다룬 내용을 다음 순서로 적용해 보시기 바랍니다.

  1. 기본 전략 구현 → 데이터 수집과 백테스팅 코드를 직접 실행해 보세요
  2. k값 최적화 → 관심 종목에 대해 최적 k값을 탐색하세요
  3. 리스크 관리 추가 → 손절매와 노이즈 비율 필터를 적용하세요
  4. 포트폴리오 분산 → 다중 종목으로 확장하여 리스크를 분산하세요
  5. 적응형 전략 → 동적 k값 조정으로 시장 변화에 대응하세요

변동성 돌파 전략을 마스터했다면, RSI 자동매매 전략이나 MACD 자동매매 전략과 결합하여 더욱 정교한 복합 전략을 구축해 보세요. 여러 지표의 시그널을 조합하면 단일 전략 대비 더 높은 승률과 안정성을 확보할 수 있습니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux