teji

tede

AI 에이전트, 트레이딩 봇 개발 중인 남편의 일기 🤖
화장품에 진심인 간호사 아내의 코덕로그 💄

전체보기

600만원을 1,000억으로, 워뇨띠(Wonyotti) 따라잡기 (2) 구현편: 공개 기록에서 뽑은 규칙 4개를 자동매매 봇 코드로 옮긴 과정, 포스트온리 실행·노출 하드캡·출금 알림·역추세 전략 wony_v1 백테스트

최종 업데이트: 2026-09-25

이 글은 투자 조언이 아니에요. 공개된 거래 기록에서 뽑은 규칙을 제 봇에 넣어 실험하는 과정이고, 이 글 시점에 봇은 켜져 있지 않아요.

워뇨띠 따라잡기 2편 구현: 규칙 4개를 자동매매 봇 코드로 옮긴 과정 썸네일

1편에서 워뇨띠(Wonyotti)의 BitMEX 기록 144만 건을 분석해 “규칙으로 옮길 수 있는 것” 다섯 줄과 “재량이라 옮길 수 없는 것” 세 줄로 나눴어요. 2편은 그 다섯 줄을 클로드코드(Claude Code)로 제 자동매매 봇에 실제로 넣은 기록이에요. 결과는 코드 네 덩어리예요. 시장가 주문을 없애고 포스트온리(Post-Only) 지정가만 쓰는 실행 규칙, 총노출이 자본의 3배를 넘으면 주문을 거부하는 하드캡, 자본이 고점 대비 10% 오르면 출금을 권하는 알림, 그리고 볼린저 밴드와 RSI 로 과열·과매도를 근사한 역추세 전략 wony_v1 이에요. 24개 조합 스윕에서 채택한 설정의 2년 백테스트는 BTC 37건 승률 75.7% 프로핏팩터 2.24, ETH 27건 승률 74.1% 프로핏팩터 3.00 이었어요. 네 가지 모두 기본값 OFF 로 넣었고 봇은 아직 멈춰 있어요.

1편의 다섯 줄이 어떻게 코드 네 덩어리가 됐나요?

워뇨띠 기록의 규칙 4개와 봇 코드, 환경변수 스위치 매핑 카드

# 워뇨띠 기록 (1편) 봇에 넣은 것 스위치
① 3년간 시장가 25건, 2021년 수수료로 22 BTC 수취 포스트온리(ALO) 지정가만. 90초 안에 체결 안 되면 취소하고 다음 사이클에 다시 호가. 시장가 폴백 없음 ORDER_POST_ONLY
② 3배 초과 레버리지 구간은 3년 누적 손익 0 근처 총노출이 자본의 3배를 넘으면 그 사이클 주문 전부 거부 + 알림. 기존 0.95배 소프트캡은 그대로 GROSS_EXPOSURE_HARD_X
③ 잔고의 10% 씩 56회 정기 출금 자본이 직전 고점보다 10% 높아지면 “잔고 10% 출금 검토” 텔레그램. 알림만, 자동 출금 없음 WITHDRAW_ALERT_ENABLED
④ 과열에 숏, 과매도에 롱. 승률 76% / 손익비 0.56 wony_v1: 4시간봉이 볼린저 밴드를 건드리고 RSI 극단 + 1시간봉이 돌아섰을 때 진입, 중심선 복귀에 청산, 손절 4%, 레버리지 1 WONY_V1_ENABLED

1편의 “리베이트 구조 활용” 은 ① 에 합쳐졌어요. 포스트온리로 체결되면 자동으로 메이커라서 따로 규칙이 필요 없었어요. 넣지 않은 것도 분명히 해둘게요. 물타기(1편: 2020년 물타기형 137건 합계 −411 BTC), AI 재량 청산(6편: 24건 평균 0), 2018년식 10배 스캘핑. 셋 다 기록이 “하지 마라” 고 말한 것들이에요.

규칙 ①: 시장가를 없애고 포스트온리만 쓰면 어떻게 되나요?

포스트온리 실행 코드: ALO 지정가 주문, 90초 폴링, 미체결 시 취소 후 다음 사이클 재호가

# 규칙 ① 포스트온리 실행 : exchange_hl.py (실제 코드 요약)
def _submit_post_only(self, symbol, resolved, side, notional, leverage):
    """포스트온리 지정가 1회 → 90초 동안 5초마다 체결 확인 →
    미체결이면 취소하고 빈 dict 반환 (호출측이 트랜치 롤백, 다음 사이클 재호가). 시장가 폴백 없음."""
    result = self.client.place_order(resolved, side, notional, leverage=leverage, post_only=True)
    order_id = result.get("id") or result.get("orderId")
    if not order_id:
        logger.warning(f"Post-only rejected (would take liquidity?): {symbol} {side}")
        return {}
    deadline = time.time() + POST_ONLY_WAIT_SEC          # 기본 90초
    while time.time() < deadline:
        time.sleep(5)
        st = str(self.client.get_order(order_id).get("status", "")).lower()
        if st in {"closed", "filled"}:
            return updated                                # 메이커 체결
        if st == "canceled":
            return {}
    self.client.cancel_order(resolved, order_id)          # 시간 초과 → 취소
    logger.warning(f"Post-only unfilled after {POST_ONLY_WAIT_SEC}s, canceled - retry next cycle")
    return {}

# hyperliquid.py : 최우선 호가에 ALO(Add-Liquidity-Only) 주문. 테이커가 되면 거래소가 거부
bid, ask = self.get_best_bid_ask(symbol)                  # info.l2_snapshot
limit_px = self._round_perp_price(bid if is_buy else ask, coin)
self.exchange.order(coin, is_buy, size, limit_px, {"limit": {"tif": "Alo"}})

제 봇은 지금까지 IoC(Immediate-Or-Cancel) 주문이었어요. 현재가 근처에서 바로 체결되고 안 되면 취소되는 방식이라 사실상 시장가이고 매 체결이 테이커였어요. 워뇨띠 기록의 핵심이 “시장가 0건, 큰 지정가를 깔아두고 시장이 오게 한다” 였으니 여기부터 바꿨어요.

  1. 호가창 최우선 매수·매도 호가를 읽어 그 가격에 ALO(Add Liquidity Only) 주문을 내요. 테이커로 체결될 상황이면 거래소가 주문을 거부해요.
  2. 5초마다 체결 여부를 확인하면서 90초 기다려요.
  3. 안 되면 취소하고 빈손으로 돌아가요. 봇의 기존 로직이 “주문 실패” 로 보고 그 트랜치를 장부에서 롤백하고, 5분 뒤 다음 사이클에 신호가 살아 있으면 새 호가로 다시 시도해요.

시장가 폴백은 일부러 넣지 않았어요. “90초 안에 안 되면 시장가로” 를 넣는 순간 워뇨띠 규칙이 아니게 돼요. 4시간봉 전략이라 5분 늦게 들어가는 것은 큰 문제가 아니라고 봤어요. 대가는 급하게 움직이는 장에서 미체결이 반복돼 신호를 놓칠 수 있다는 거예요. 라이브에서 미체결 롤백 알림 빈도로 판단할 거예요. ALO 주문 타입은 Hyperliquid 주문 유형 문서를 참고하세요.

규칙 ②③: 하드캡과 출금 알림은 어떻게 짜였나요?

총노출 하드캡과 출금 알림 코드: 주문 직전 3배 검사, 자본 고점 갱신과 텔레그램 알림

# 규칙 ②③ 하드캡 + 출금 알림 : run_live.py (실제 코드 요약)
def rebalance(broker, targets, dry_run, equity=0.0):
    # 규칙 ② 총노출 하드캡. 장부 cap(0.95x) 을 뚫고 여기까지 오면 주문 전부 거부
    gross = sum(abs(v) for v in targets.values())
    if equity > 0 and gross > GROSS_EXPOSURE_HARD_X * equity:        # 기본 3.0
        logger.error(f"HARD CAP: gross target {gross:.2f} > {GROSS_EXPOSURE_HARD_X}x equity")
        notify_error("총노출 하드캡 초과, 주문 거부")
        return set(targets.keys())                                    # 전부 실패 처리 → 롤백
    ...

# 규칙 ③ 출금 알림 (알림만, 자동 출금 없음). 고점은 operation_state.json 에 저장
if state.hwm_equity <= 0:
    state.hwm_equity = equity
elif WITHDRAW_ALERT_ENABLED and equity >= state.hwm_equity * (1 + WITHDRAW_ALERT_PCT):   # +10%
    notify_send(f"💸 [출금 검토] equity {equity:.0f} = 직전 고점 {state.hwm_equity:.0f} 대비 +10% 이상. "
                f"워뇨띠 규칙: 잔고의 10%({equity*0.10:.0f}) 출금 검토")
    state.hwm_equity = equity
elif equity > state.hwm_equity:
    state.hwm_equity = equity

하드캡은 주문을 내기 직전에 한 번 더 세는 거예요. 장부 쪽에는 이미 자본의 95% 소프트캡이 있어 정상 경로로는 3배에 닿을 수 없어요. 그래도 넣은 이유는 5편의 사고 때문이에요. 장부가 꼬였을 때 마지막에 막아주는 선이 필요했어요. 워뇨띠 기록에서 3배 이상 구간의 누적 손익이 0 근처였다는 것이 숫자 3의 근거예요.

출금 알림은 자본 고점을 상태 파일에 저장해두고, 고점보다 10% 높아지면 텔레그램으로 “잔고의 10% 출금 검토” 를 보내요. 자동으로 빼지는 않아요. 출금은 거래소에서 사람이 하고 봇은 잊지 않게 알려주는 역할만 해요. 워뇨띠가 출금 1회에 잔고의 10% 씩 56번 뺐고 그 덕에 2021년 4분기 −43% 를 견뎠다는 1편 내용이 그대로 규칙이 됐어요.

규칙 ④: 역추세 판단을 지표로 어떻게 근사했나요?

wony_v1 신호 코드: 볼린저 밴드 터치, RSI 40/60, 1시간봉 EMA20 확인, 중심선 복귀 청산

# 규칙 ④ wony_v1 신호 : wony_v1.py (실제 코드)
def wony_v1_signal(symbol, df, df_1h=None, rsi_lo=40.0, rsi_hi=60.0):
    """4h 마감 봉 기준. exit_only=True 는 '중심선 복귀 → 청산만'."""
    out = indicators(df)                     # 볼린저(20, 2σ) + RSI14
    row, prev = out.iloc[-1], out.iloc[-2]
    price = float(row["close"])
    h1_up = h1_down = True
    if df_1h is not None and len(df_1h) >= 25:
        e = df_1h["close"].ewm(span=20, adjust=False).mean()
        h1_up   = bool(df_1h["close"].iloc[-1] > e.iloc[-1])    # 1h 가 이미 돌아섰나
        h1_down = bool(df_1h["close"].iloc[-1] < e.iloc[-1])

    # 과매도 : 봉 저가가 하단 밴드를 건드리고 RSI < 40, 1h 는 반등 시작
    if float(row["low"]) < row["bb_dn"] and row["rsi"] < rsi_lo and h1_up:
        return Signal("wony_v1", symbol, "LONG", row["time"], 0.6, "과매도", entry_price=price)
    # 과열 : 봉 고가가 상단 밴드를 건드리고 RSI > 60, 1h 는 꺾임
    if float(row["high"]) > row["bb_up"] and row["rsi"] > rsi_hi and h1_down:
        return Signal("wony_v1", symbol, "SHORT", row["time"], 0.6, "과열", entry_price=price)
    # 평균회귀 목표 : 종가가 중심선을 가로지르면 보유 포지션 청산
    if prev["close"] < prev["bb_mid"] <= price:
        return Signal("wony_v1", symbol, "SHORT", row["time"], 0.6, "중심선 복귀", entry_price=price, exit_only=True)
    if prev["close"] > prev["bb_mid"] >= price:
        return Signal("wony_v1", symbol, "LONG", row["time"], 0.6, "중심선 복귀", entry_price=price, exit_only=True)
    return None

1편에서 “과열·과매도 판별 자체는 재량이라 재현 불가” 라고 했어요. 그래서 목표를 낮췄어요. 워뇨띠의 판단을 흉내 내는 게 아니라, 같은 통계 꼴(승률 3/4, 손익비 1 미만, 프로핏팩터 1.5 이상)이 나오는 가장 단순한 규칙을 찾는 거예요.

항목 규칙
진입 (롱) 4시간봉 저가가 볼린저 밴드(20, 2σ) 하단을 건드리고, RSI14 가 40 미만이고, 1시간봉 종가가 1시간 EMA20 위에 있을 때 (반등 시작 확인)
진입 (숏) 대칭. 고가가 상단 밴드 터치, RSI 60 초과, 1시간봉이 EMA20 아래
청산 종가가 밴드 중심선(SMA20)을 가로지르면 전량. 평균회귀 목표
손절 4% (레짐 반대일 때 3%)
사이즈 레버리지 1, 심볼당 최대 2트랜치. 물타기 없음

볼린저 밴드와 RSI 의 정의는 Investopedia Bollinger Bands와 Investopedia RSI를 참고하세요.

백테스트에서 24개 조합을 어떻게 고르고 무엇을 배웠나요?

wony_v1 백테스트 결과 차트: BTC와 ETH 의 6개월 창 3개와 2년 전체 순수익률

4편에서 배운 대로 했어요. 한 조합만 돌려서 좋다고 하지 않고, RSI 기준 3가지 × 1시간 확인 유무 × 밴드 기준(종가 vs 저가·고가 터치) × 손절(7% vs 4%) = 24조합을 BTC·ETH 에 전부 돌려 표로 남겼어요. 파라미터를 창별로 최적화하지 않았고, 6개월 창 3개로 나눠 봤어요. 조건은 2024-03~2026-03 4시간봉·1시간봉, 메이커 수수료 2bp, 슬리피지 0 이에요.

심볼 거래 승률 순수익률 (2년) PF 최대 낙폭 평균 익절 / 손절 창별 (W1 / W2 / W3)
BTCUSDT 37 75.7% +47.7% 2.24 −11.6% +2.65% / −3.67% +4.8 / +23.0 / −5.4
ETHUSDT 27 74.1% +40.9% 3.00 −5.6% +2.67% / −2.54% +2.1 / +2.4 / +20.9

채택한 것은 RSI 40/60, 1시간 확인 켬, 밴드 터치, 손절 4% 예요. 통계 꼴이 워뇨띠와 같아요. 승률 3/4, 손익비 0.7, 프로핏팩터 2~3. 6개 창 중 5개가 플러스이고 유일한 마이너스는 BTC 의 마지막 창이에요.

  1. RSI 30/70 은 4시간봉에서 너무 드물어요. 1시간 확인까지 걸면 2년에 BTC 5건, ETH 0건이에요. 과열·과매도를 4시간봉에서 근사하려면 40/60 이 맞았어요.
  2. 1시간 확인이 필수예요. 끄면 거래는 5~6배로 늘지만 최대 낙폭이 −30~−55% 로 커지고, ETH 는 손절 7% 조합이 전부 마이너스예요. 확인은 “떨어지는 칼” 을 거르는 역할이에요.
  3. 손절 4% 가 7% 보다 나아요. 두 심볼 다요. 평균회귀 목표(중심선)가 2~3% 라 손절이 7% 면 손익비가 무너져요.

한계는 표본이에요. 2년에 27~37건, 월 1.3건이에요. 워뇨띠는 2021년에 하루 0.9건이었으니 빈도에서는 근처도 못 갔어요. 창 하나가 마이너스라는 것은 추세장에서 손절이 연달아 나올 수 있다는 뜻이에요.

왜 전부 기본 OFF 로 넣었나요?

config.py 플래그 코드: 네 규칙 모두 환경변수로 켜고 기본값은 OFF, 검증 순서

# 전부 기본 OFF : config.py 플래그 (켜는 건 사람)
# 워뇨띠 규칙 (2026-09-24, Trading/워뇨띠/report.md 의 "재현 가능" 항목). 전부 기본 OFF.
ORDER_POST_ONLY        = _env_bool("ORDER_POST_ONLY")           # ① 시장가 금지
POST_ONLY_WAIT_SEC     = int(os.getenv("POST_ONLY_WAIT_SEC", "90"))
GROSS_EXPOSURE_HARD_X  = float(os.getenv("GROSS_EXPOSURE_HARD_X", "3.0"))   # ② 항상 ON, 값만 조정
WITHDRAW_ALERT_ENABLED = _env_bool("WITHDRAW_ALERT_ENABLED")    # ③ 출금 알림
WITHDRAW_ALERT_PCT     = float(os.getenv("WITHDRAW_ALERT_PCT", "0.10"))
WONY_V1_ENABLED        = _env_bool("WONY_V1_ENABLED")           # ④ 역추세 전략
STRATEGIES["wony_v1"]  = StrategyConfig(name="wony_v1",
                                        bucket_weight=float(os.getenv("WONY_V1_BUCKET", "0.0")),
                                        tranche_weight=0.25, max_tranches=2,
                                        leverage=1,          # 잔고 대비 1~2배. 레버리지로 안 키움
                                        timeframe="4h")
STOP_LOSS_PCT["wony_v1"] = 0.04        # 스윕: 7% 보다 4% 가 두 심볼 모두 우위

# 검증 순서 (5편 원칙) : import → --once --dry-run → 자가 테스트 3개 → 봇은 아직 정지
> python -m operation.run_live --once --dry-run   → State and live positions are in sync.
> python -m operation.test_wony_v1               → ok ×3

5편에서 정한 원칙이 그대로 쓰였어요. 스위치 네 개는 환경변수이고 기본값은 전부 OFF 예요. 파일을 고쳐도 봇 동작은 바뀌지 않고, 켜는 것도 켠 뒤 재시작하는 것도 사람이 해요. 검증은 세 단계로 했어요. import 체인 확인, 봇을 한 사이클만 돌리는 dry-run 에서 “장부와 실포지션 일치” 확인, 그리고 wony_v1 신호에 대한 자가 테스트 3개(과매도 롱 / 1시간 확인 없으면 진입 안 함 / 중심선 복귀 청산)예요.

왜 아직 켜지 않았나요?

  1. 포스트온리는 dry-run 으로 검증이 안 돼요. dry-run 은 주문 자체를 안 내요. 첫 라이브 사이클에서 체결 또는 미체결 취소 로그를 직접 봐야 하고, 그것이 3편의 일이에요.
  2. 버킷 배분을 정해야 해요. wony_v1 을 기존 스퀴즈 모멘텀 봇과 같은 계좌에서 돌리면 자본을 나눠야 해요. 같은 심볼에 두 전략이 반대 포지션을 잡으면 서로 상계되는 구조라 배분을 먼저 정해야 해요.
  3. 표본 27~37건으로 켜기엔 신중해야 해요. 소액으로 시작해서 미체결 비율과 첫 10건의 통계 꼴을 보는 것이 순서예요.

3편은 “켜고 나서 실제로 어땠나” 예요. 켜는 조합은 시장가 금지와 출금 알림을 먼저, wony_v1 은 소액 버킷으로 병행하는 쪽으로 생각하고 있어요.

배운 것 3가지

  1. 실행 규칙이 전략보다 먼저예요. 워뇨띠 기록에서 가장 확실한 엣지가 “시장가 0건” 이었고, 그것은 전략과 무관하게 지금 봇에 바로 넣을 수 있었어요.
  2. 재량은 흉내 내지 말고 통계 꼴을 맞춰요. 워뇨띠의 판단을 재현하려는 대신 같은 승률·손익비 프로필이 나오는 가장 단순한 규칙을 찾았더니 스윕에서 그 꼴이 실제로 나왔어요.
  3. 기본값 OFF 가 실험을 가능하게 해요. 4편의 기능 플래그, 5편의 “켜는 건 사람” 원칙 덕에 코드를 다 넣고도 그날 아무것도 바뀌지 않았어요.

자주 묻는 질문

Q. 포스트온리 주문이 무엇인가요?
호가창에 깔아두는 주문만 허용하는 옵션이에요. 내 주문이 바로 체결될 상황(테이커)이면 거래소가 주문을 거부해요. 그래서 항상 메이커로만 체결되고, 거래소에 따라 수수료 대신 리베이트를 받아요.

Q. wony_v1 은 워뇨띠 전략인가요?
아니에요. 워뇨띠 기록과 같은 통계 꼴이 나오도록 만든 단순한 역추세 규칙이에요. 워뇨띠는 고정 규칙 없이 재량으로 매매했고 본인도 봇으로는 안 된다고 했어요. 이름은 출발점을 기억하려고 붙였어요.

Q. 백테스트 프로핏팩터 2~3 이면 좋은 것 아닌가요?
숫자만 보면요. 2년에 30건 안팎이라 표본이 작고, 6개 창 중 하나는 마이너스였어요. 백테스트는 규칙이 틀리지 않았는지 확인하는 도구이지 수익 보증이 아니에요.

Q. 코드는 클로드코드가 전부 작성했나요?
네. 저는 1편의 규칙 목록을 주고, 스윕 결과를 보고 파라미터를 고르고, 켤지 말지를 정했어요. 코드 변경, 백테스트, 자가 테스트, 문서는 클로드코드가 했어요.

Q. 언제 켜나요?
버킷 배분과 소액 시작 규모를 정하면요. 3편에서 첫 체결 로그와 함께 적을게요.

함께 보면 좋은 글

출처: 제 봇 코드(config.py, exchange_hl.py, run_live.py, wony_v1.py, 2026년 9월 24일 변경분), 스윕 결과 research/wony_v1/sweep_results.csv, 1편의 워뇨띠 BitMEX 기록 분석. 투자 조언이 아니에요. 3편은 켜고 나서, 첫 포스트온리 체결이 될지 미체결 취소가 될지부터예요.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *