파이썬으로 자동매매 만들기 · 7편

안 죽고 안 터지게 — 자동매매 안전장치와 운영

드라이런·수량 대조·다단계 드로다운 알림·긴급 정지·텔레그램. 자동매매의 진짜 난이도는 운영에 있다.

자동매매의 진짜 난이도는 "돈 버는 로직"이 아니다. 실계좌에서 안 죽고, 안 터지게 운영하는 것이다. 이 마지막 편이 그 안전망을 다룬다.

1) 드라이런 — 항상 여기서 시작

5편에서 봤듯, dry_run=True면 실제 주문 없이 로직이 뭘 하려는지 로그로만 남긴다. 새 전략·새 종목은 무조건 드라이런부터. 며칠 돌려보며 이상한 주문이 없는지 확인한 뒤 실전으로 넘어간다.

2) 수량 대조 (Reconciliation) — 가장 중요한 안전장치

내 DB가 생각하는 보유수량과 브로커의 실제 보유수량이 어긋나면 모든 계산이 무너진다. (내가 손으로 팔았거나, 체결을 놓쳤거나, 부분체결이 꼬였을 때 생긴다.)

그래서 주기적으로 "영웅문 실제 수량 vs 내 DB 수량"을 대조하고, 불일치하면 자동 정지 + 긴급 알림을 보낸다.

if 브로커_실수량 != DB_수량:
    자동매매_정지()
    텔레그램("🚨 수량 불일치 — 수동 확인 필요")

틀린 상태로 계속 주문을 내는 것보다, 멈추고 사람을 부르는 게 항상 낫다.

3) 다단계 드로다운 알림 (L1~L4)

그리드는 하락장에서 계속 사들이며 자금이 물릴 수 있다. 그래서 하락 깊이·자금 소진 정도에 따라 경고를 단계적으로 올린다.

단계의미
L1자금 소진 임박 경고
L2~L3하락 심화 — 주의
L4깊은 하락 — 수동 개입(추가 매수/정지) 판단

돈이 걸린 시스템은 조용히 망가지면 안 된다. 위험이 커질수록 더 크게 알린다.

4) 긴급 정지 (킬 스위치)

무슨 일이 생기면 한 번에 멈출 수단이 반드시 있어야 한다. 메뉴의 "🚨 긴급 정지" 버튼 하나로 모든 자동 주문을 중단한다. 자동화에서 수동 정지 권한은 타협 대상이 아니다.

5) 재시작해도 살아남기

  • 대기 주문·상태를 SQLite에 영속화 → 앱이 꺼졌다 켜져도 그 자리에서 복원.
  • catch-up 조회(4편) → 봇이 밤사이 멈춰 놓친 체결을 재시작 후 따라잡는다.

자동매매는 몇 달을 돌린다. 재부팅·정전·업데이트를 견디게 만드는 게 핵심이다.

6) 알림 — 텔레그램

체결·오류·안전 경고를 텔레그램 봇으로 실시간 전송한다. 네트워크가 잠깐 끊길 수 있으니 재시도 로직을 넣는다. (봇 토큰·chat_id는 2편의 settings.json에서.)

체결됨 → 텔레그램("✅ QLD 매수 4주 @ $594 체결")
오류/정지 → 텔레그램("🚨 ...")

7) 현황을 웹으로 — 대시보드 (선택)

폰에서 언제든 현황을 보고 싶으면, 보유·수익·상태를 정적 HTML로 만들어 웹에 퍼블리시하면 된다. (이 프로젝트는 Cloudflare Worker로 올린다.) 서버를 상시 켜둘 필요 없이 읽기 전용 현황판을 갖는 셈이다.

📦 코드로 — 수량 대조와 드로다운 판정

수량 대조는 놀랍도록 단순하다. 브로커 실수량과 내 체결 합계를 빼기만 하면 된다.

from dataclasses import dataclass

@dataclass
class ReconcileResult:
    broker_quantity: int
    system_quantity: int
    delta: int          # broker - system
    matched: bool

def reconcile(repo, code, broker_quantity):
    system_qty = repo.sum_trade_quantities(code)   # DB에 기록된 체결 합계
    delta = broker_quantity - system_qty
    return ReconcileResult(broker_quantity, system_qty, delta, delta == 0)

# 사용
r = reconcile(repo, "QLD", broker_quantity=controller.get_position_quantity("QLD"))
if not r.matched:
    grid_manager.stop()
    telegram(f"🚨 QLD 수량 불일치: 브로커 {r.broker_quantity} vs DB {r.system_quantity}")

다단계 드로다운 판정 — 가격이 얼마나 깊이 빠졌는지에 따라 레벨을 올린다.

from enum import IntEnum

class AlertLevel(IntEnum):
    NONE = 0; L1_NEAR_EXHAUST = 1; L2_EXHAUSTED = 2
    L3_MANUAL_BUY = 3; L4_DEEP_MANUAL_BUY = 4

def evaluate_drawdown(current_tier, total_tiers,
                      lowest_tier_price, current_price,
                      l3_pct=10.0, l4_pct=30.0):
    # 높은 레벨 우선
    if current_price <= lowest_tier_price * (1 - l4_pct/100):
        return AlertLevel.L4_DEEP_MANUAL_BUY      # 최저가 -30% 이하
    if current_price <= lowest_tier_price * (1 - l3_pct/100):
        return AlertLevel.L3_MANUAL_BUY           # 최저가 -10% 이하
    if current_tier >= total_tiers and current_price <= lowest_tier_price:
        return AlertLevel.L2_EXHAUSTED            # 그리드 탄약 소진
    if current_tier >= total_tiers - 1:
        return AlertLevel.L1_NEAR_EXHAUST         # 소진 임박
    return AlertLevel.NONE

같은 레벨 알림이 반복되지 않게 한 번 보낸 레벨은 기록(중복 방지) 한다. 위험이 커질수록(L1→L4) 더 크게, 하지만 딱 한 번씩 알린다.

✅ 확인

  • reconcile을 주기적으로 돌려 matched=False면 즉시 정지 + 알림이 오는지(가짜 불일치를 넣어) 테스트.
  • evaluate_drawdown에 여러 가격을 넣어 L1~L4 경계가 의도대로 나오는지 확인.
  • 이 둘이 동작하면, "조용히 망가지는" 최악은 막은 것이다.

🔧 부품을 하나로 — 최소 봇

지금까지 만든 부품(시세·주문·체결·알림)을 묶으면, 실제로 도는 봇이 된다. 핵심은 놀랍도록 짧다 — 주기적으로 보고 → 판단 → 주문 → 체결 알림의 반복.

# bot.py — 부품을 묶은 최소 자동 봇
import time, json, pathlib
from hero.kiwoom_rest import KiwoomRestClient

cfg = json.loads(pathlib.Path("config/settings.json").read_text("utf-8"))["kiwoom_rest"]
client = KiwoomRestClient(cfg["appkey"], cfg["secretkey"], mock=cfg.get("mock", True))
TICKER, EXCH = "QLD", "NY"
seen = set()

while True:
    price = client.us_current_price(TICKER, EXCH)
    if price is not None:
        # 1) 규칙 판단 (6편의 그리드가 여기 들어간다)
        #    if 매수조건(price): client.us_order("buy", TICKER, EXCH, qty, price=...)
        #    if 매도조건(price): client.us_order("sell", TICKER, EXCH, qty, price=...)

        # 2) 새 체결이면 알림
        for f in client.us_executions():
            oid = f.get("ord_no")
            if oid and oid not in seen:
                seen.add(oid)
                print(f"✅ 체결: {f['stk_cd']} {f['slby_tp_nm']} "
                      f"{f['cntr_qty']}주 @ {f['cntr_uv']}")   # → 텔레그램 발송

    time.sleep(10)   # rate limit 여유 + CPU 절약

이게 자동매매 봇의 심장이다. 여기 # 1) 규칙 판단에 6편 그리드를, # 2) 알림에 텔레그램을, 그리고 앞서 본 수량 대조·드로다운·긴급 정지를 두르면 — 처음부터 끝까지 당신이 만든 봇이 완성된다.

⚠️ 실전에서는 while True 안에 장 시간 체크(미국장 열렸을 때만), 예외 처리(조회 실패해도 안 멈춤), 긴급 정지 플래그를 반드시 넣어야 한다. 위는 뼈대다.

마무리 — 여기까지가 '작동하는 기초'

7편에 걸쳐, 우리는:

  1. 왜 REST로 만드는가 → 2. 키 발급 → 3. 토큰·인증 → 4. 데이터 조회 → 5. 주문 → 6. 그리드 로직 → 7. 안전장치

까지 자동매매의 뼈대 전체를 훑었다. 이건 작동하는 기초형이다. 실전에서 오래 굴리려면 여기에 더 많은 것이 필요하다 — 백테스트, 엣지케이스(휴장·조기폐장·섬머타임), 세금·수수료 정밀 반영, 장기 하락 대응, 수백 개의 테스트.

가장 중요한 원칙 하나: 실전 투입 전 모의·소액·드라이런으로 충분히 검증하라. 자동매매는 잘 만들면 강력하지만, 잘못 만들면 잘못을 빠르게 반복한다. 잃어도 되는 여윳돈으로만.

이 강좌가 "나도 만들 수 있다"의 출발점이 되면 좋겠다. 막히는 부분은 각 편 댓글로 편하게 물어봐 주세요 — 함께 만들어가요.


※ 이 강좌는 프로그래밍 교육입니다. 투자 권유나 특정 상품·종목·시점 추천이 아니며, 투자에는 원금 손실 위험이 있습니다. 증권사 API 약관과 관련 규정을 반드시 확인하세요.

핵심 주장 확인 자료 · 검토 기준 2026-08-27

아래 자료에서 본인에게 적용되는 최신 조건을 다시 확인하세요. 링크는 특정 상품 가입을 권하는 제휴 링크가 아닙니다.

이 글은 교육·정보 제공 목적이며 개인별 투자자문이나 특정 금융상품의 매수·매도 권유가 아닙니다.

함께 생각해요

이 글을 읽고 생긴 질문이나 직접 실행한 경험을 나눠주세요.

댓글을 불러오는 중입니다…