Development/Indicator Lab

Williams %R

MildChoco 2026. 3. 19. 11:21

 

모멘텀/오실레이터 섹션의 마지막 지표입니다. Williams %R은 스토캐스틱과 수학적으로 거의 같지만, 스케일이 반전되어 있습니다. Larry Williams가 1973년에 개발했습니다.


지표 소개

Williams %R은 현재 가격이 최근 N일 범위에서 얼마나 높은/낮은 위치에 있는지를 보여줍니다.

%R = (N일 최고가 - 현재 종가) / (N일 최고가 - N일 최저가) × -100

 

결과값은 0 ~ -100 범위입니다. 스토캐스틱이 0~100인 것과 반대 방향이죠.

  • 0에 가까울수록 최근 최고가 근처 (과매수)
  • -100에 가까울수록 최근 최저가 근처 (과매도)

일반적으로 -20 이상을 과매수, -80 이하를 과매도로 봅니다.

스토캐스틱과의 관계

스토캐스틱 Fast %K의 공식을 다시 보면:

%K = (현재 종가 - N일 최저가) / (N일 최고가 - N일 최저가) × 100
%R = (N일 최고가 - 현재 종가) / (N일 최고가 - N일 최저가) × -100

 

수학적으로 %R = %K - 100입니다. 스토캐스틱 %K가 80이면 %R은 -20, %K가 20이면 %R은 -80. 완전히 같은 정보를 거꾸로 표현한 것입니다.

 

차이점은 평활화가 없다는 것입니다. 스토캐스틱은 Slow %K(3일 평균)와 %D(다시 3일 평균)로 두 번 평활화하지만, Williams %R은 원시 값을 그대로 사용합니다. 따라서 스토캐스틱보다 더 민감하게 반응합니다.

 

Y축이 0~-100으로 되어 있고, 위쪽이 과매수(-20), 아래쪽이 과매도(-80)입니다. 상승장에서 %R이 -20 위(0에 가까운 쪽)에 자주 올라가는 것을 볼 수 있습니다.

 

스토캐스틱과 형태가 거의 같지만, 평활화가 없어서 움직임이 더 날카롭습니다.


가설 설정

다른 오실레이터들과 같은 구조입니다. 방향만 주의하면 됩니다 — %R은 값이 올라갈수록(0에 가까울수록) 과매수입니다.

  • 진입 — %R이 -80 아래에서 위로 크로스 (과매도 탈출)
  • 청산 — %R이 -50 / -40 / -20 위에서 아래로 크로스

%R -50은 "중립 회복", -40은 "중립 약간 위", -20은 "과매수 진입"에 해당합니다. 스토캐스틱의 50/60/80 청산과 대응됩니다.


백테스트 조건

항목 설정
종목 BTC/USDT (Binance Spot)
기간 2018.01.01 ~ 2025.12.31
타임프레임 일봉 (1D)
Williams %R 기간 14일
진입 %R이 -80 위로 크로스 (고정)
청산 %R이 -50 / -40 / -20 아래로 크로스 (비교)
초기 자본 $10,000
수수료 / 슬리피지 미적용

코드 구현

import pandas as pd
import numpy as np
import mplfinance as mpf
import matplotlib.pyplot as plt

# ===== 설정 =====
DATA_FILE = "BTCUSDT_1d.csv"
WR_PERIOD = 14
OVERBOUGHT = -20
OVERSOLD = -80
EXIT_LEVELS = [-50, -40, -20]     # 비교할 청산 기준
INITIAL_CAPITAL = 10000
ZOOM_DAYS = 180
# ================

# 데이터 로드
df = pd.read_csv(DATA_FILE, index_col="timestamp", parse_dates=True)

# Williams %R 계산
highest_high = df["high"].rolling(window=WR_PERIOD).max()
lowest_low = df["low"].rolling(window=WR_PERIOD).min()
df["williams_r"] = (highest_high - df["close"]) / (highest_high - lowest_low) * -100

# 백테스트 함수
def run_wr_backtest(df, oversold, exit_level, initial_capital):
    capital = initial_capital
    holdings = 0
    buy_price = 0
    in_position = False
    trades = []
    equity_curve = []

    for i in range(WR_PERIOD + 1, len(df)):
        wr = df["williams_r"].iloc[i]
        wr_prev = df["williams_r"].iloc[i - 1]
        close = df["close"].iloc[i]

        if not in_position:
            # %R이 과매도(-80) 위로 크로스 → 매수
            if wr > oversold and wr_prev <= oversold:
                buy_price = close
                holdings = capital / buy_price
                capital = 0
                in_position = True
        else:
            # %R이 청산 기준 아래로 크로스 → 매도
            if wr < exit_level and wr_prev >= exit_level:
                sell_price = close
                capital = holdings * sell_price
                pnl = (sell_price - buy_price) / buy_price * 100
                trades.append({"pnl_pct": round(pnl, 2)})
                holdings = 0
                in_position = False

        equity = capital + holdings * close
        equity_curve.append(equity)

    if in_position:
        last_price = df["close"].iloc[-1]
        capital = holdings * last_price
        pnl = (last_price - buy_price) / buy_price * 100
        trades.append({"pnl_pct": round(pnl, 2)})
        holdings = 0

    eq = pd.Series(equity_curve)
    peak = eq.cummax()
    mdd = ((eq - peak) / peak * 100).min()

    return capital, trades, mdd

# 바이앤홀드
first_price = df["close"].dropna().iloc[0]
last_price = df["close"].dropna().iloc[-1]
bnh_return = (last_price - first_price) / first_price * 100
bnh_capital = INITIAL_CAPITAL * (1 + bnh_return / 100)

# 실행 및 비교
print("=" * 65)
print(f"Williams %R ({WR_PERIOD}) 과매도 크로스 백테스트 — 청산 기준 비교")
print(f"진입: %R {OVERSOLD} 상향 크로스")
print(f"Data: {df.index[0].strftime('%Y-%m-%d')} ~ {df.index[-1].strftime('%Y-%m-%d')}")
print("=" * 65)
print(f"  {'Exit':<10} {'Capital':>10} {'Return':>10} {'MDD':>8} {'Trades':>7} {'Win':>5} {'Lose':>5} {'WinRate':>8}")
print("-" * 65)

for exit_level in EXIT_LEVELS:
    capital, trades, mdd = run_wr_backtest(df, OVERSOLD, exit_level, INITIAL_CAPITAL)
    total_return = (capital - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
    wins = len([t for t in trades if t["pnl_pct"] > 0])
    losses = len([t for t in trades if t["pnl_pct"] <= 0])
    win_rate = wins / len(trades) * 100 if trades else 0

    print(f"  %R {exit_level:<5} ${capital:>9,.0f} {total_return:>+9.2f}% {mdd:>+7.02f}% {len(trades):>6}  {wins:>5} {losses:>5} {win_rate:>7.1f}%")

print("-" * 65)
print(f"  {'B&H':<10} ${bnh_capital:>9,.0f} {bnh_return:>+9.2f}%")
print("=" * 65)

# 차트 헬퍼
def plot_wr(df_plot, title):
    fig, axes = mpf.plot(df_plot, type="candle", style="charles",
        title=title, volume=False, figsize=(14, 8), returnfig=True)

    ax_wr = fig.add_axes([0.12, 0.08, 0.76, 0.25])

    wr_data = df_plot["williams_r"].dropna()
    x = range(len(wr_data))

    ax_wr.plot(x, wr_data.values, color="#1E88E5", linewidth=1.2)
    ax_wr.axhline(y=OVERBOUGHT, color="#E53935", linewidth=0.8,
        linestyle="--", label=f"{OVERBOUGHT}")
    ax_wr.axhline(y=OVERSOLD, color="#43A047", linewidth=0.8,
        linestyle="--", label=f"{OVERSOLD}")
    ax_wr.axhline(y=-50, color="gray", linewidth=0.5, linestyle=":")
    ax_wr.fill_between(x, -100, OVERSOLD, alpha=0.1, color="#43A047")
    ax_wr.fill_between(x, OVERBOUGHT, 0, alpha=0.1, color="#E53935")
    ax_wr.set_ylim(-100, 0)
    ax_wr.set_ylabel("Williams %R")
    ax_wr.set_xlim(0, len(wr_data))
    ax_wr.legend(loc="upper left", fontsize=8)

    axes[0].set_position([0.12, 0.38, 0.76, 0.55])
    return fig

# 차트 1 — 상승장
df_bull = df.loc["2020-10-01":"2021-04-30"].copy()
plot_wr(df_bull,
    f"BTC/USDT  Williams %R ({WR_PERIOD}) (2020.10 ~ 2021.04, Bull)")

# 차트 2 — 최근
df_zoom = df.tail(ZOOM_DAYS).copy()
plot_wr(df_zoom,
    f"BTC/USDT  Williams %R ({WR_PERIOD}) (Last {ZOOM_DAYS}D)")

plt.show()

사용법

EXIT_LEVELS로 청산 기준을, OVERSOLD로 진입 기준을 변경할 수 있습니다. 값이 음수라는 점만 주의하면 됩니다.


결과 분석

 

예상치 못한 결과입니다. %R -40 청산이 +584%로 바이앤홀드(+555%)를 넘었습니다. 이 섹션에서 다룬 모든 오실레이터 단독 역추세 전략 중 유일하게 B&H를 이긴 결과입니다.

전체 오실레이터 비교표입니다.

지표 최고 청산 수익률 MDD 거래 승률
RSI 60 60 +229% -46% 17 64.7%
Stochastic 50 50 +168% -73% 63 54.0%
CCI 100 100 +205% -66% 52 59.6%
Williams %R -40 -40 +584% -58% 82 58.5%

 

%R이 좋은 성과를 낸 이유를 생각해보면, 역설적으로 평활화가 없다는 것이 도움이 되었습니다. 스토캐스틱은 Slow %K, %D로 두 번 평활화하면서 신호가 늦어지고, 진입/청산 타이밍이 지연됩니다. %R은 원시 값을 그대로 쓰기 때문에, 과매도 탈출과 청산 기준 도달을 더 빠르게 감지합니다. 민감함이 추세추종에서는 노이즈지만, 역추세 단타에서는 빠른 반응이라는 장점이 된 것입니다.

 

82회 거래, 승률 58.5%도 균형잡힌 수치입니다. RSI는 거래가 너무 적었고(17회), 스토캐스틱은 MDD가 너무 컸고(-73%), CCI는 수익률이 부족했습니다. %R -40이 거래 빈도, 승률, 수익률, MDD에서 전반적으로 가장 나은 균형을 보여줍니다.

 

다만 MDD -58%는 여전히 큰 수치이고, 이 결과도 과최적화의 가능성이 있습니다. 하나의 파라미터(-40)가 유독 좋다는 것은 해당 값이 이 데이터셋에 맞는 것일 수 있습니다. 항상 주의할 점입니다.


정리

Williams %R은 스토캐스틱 Fast %K의 반전 버전입니다. 수학적으로 %R = %K - 100이며, 같은 정보를 0~-100 스케일로 보여줍니다. 평활화가 없어서 스토캐스틱보다 더 민감합니다.

 

이것으로 모멘텀/오실레이터 섹션의 개별 지표 소개를 마칩니다.


본 포스팅은 지표의 원리와 백테스트 과정을 정리한 기술 블로그이며, 특정 자산의 매매를 권유하지 않습니다.
백테스트 결과는 과거 데이터 기반이며 실제 수익을 보장하지 않습니다. 모든 투자의 책임은 본인에게 있습니다.

'Development > Indicator Lab' 카테고리의 다른 글

볼린저 밴드 (Bollinger Bands)  (0) 2026.03.28
멀티타임프레임 전략  (0) 2026.03.26
CCI (Commodity Channel Index)  (0) 2026.03.18
스토캐스틱 (Stochastic Oscillator)  (0) 2026.03.17
RSI (Relative Strength Index)  (3) 2026.03.16