
모멘텀/오실레이터 섹션의 마지막 지표입니다. 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 |