지난 글에서는 유튜브에서 본 데이트레이딩 setup(첫 캔들 기준 ORB 돌파 + FVG 되돌림)이 어디서 왔는지를 다뤘다. 이번 글은 그 다음 질문이다. “차트를 눈으로 보며 판단하던 규칙을, 어떻게 컴퓨터가 매일 똑같이 실행하는 코드로 옮길 것인가.” 미장봇(한국에서 KIS API로 미국 ETF를 자동매매하는 개인 프로젝트)의 상태머신과 시그널 엔진을 실제 코드로 따라간다.
🛠️ 이 글의 성격 — 투자 권유가 아니라, 자동매매 시스템을 만들며 남긴 엔지니어링 기록입니다. 모든 수치·코드는 실제 구현과 실거래 로그에 근거합니다.
신호는 어디서 보고, 주문은 어디에 넣는가
코드로 옮기기 전에 정리해야 할 원칙이 하나 있다. 신호를 읽는 차트와 실제 주문을 넣는 종목이 다르다는 점이다. 리테일은 지수 선물(NQ)을 직접 매매하기 어렵기 때문에, 지수의 방향을 QQQ에서 읽고 레버리지 ETF로 실행하는 구조를 택했다.
| 차트 | 용도 | 실행 |
|---|---|---|
| TQQQ 5분봉 | 상승 setup의 진입가·손절·목표 계산 | ✅ TQQQ 매수 |
| QQQ 5분봉 | 하락 setup 신호 추출(인버스 매핑) | ✅ SQQQ 매수 |
| QQQ 일봉 · ^VIX 일봉 | 그날 매매 여부(Daily Bias·변동성 필터) | — 판단용 |
QQQ가 하락 방향으로 신호를 주면, 그것을 인버스 ETF인 SQQQ의 상승 매수로 뒤집어 실행한다. 덕분에 “롱만 매수하는 봇”이 사실상 양방향 매매를 하게 된다.
봇의 뼈대 — 하루를 6개 상태로 쪼개기
자동매매 봇의 핵심은 “지금이 하루 중 어느 국면인가”를 명확히 아는 것이다. 미장봇은 하루를 6개 상태로 나눈 상태머신으로 돌아간다. 메인 루프는 시간을 확인하며 상태를 한 방향으로만 전이시킨다.

🔁 흐름 요약: 대기 → 장 시작 전 필터(VIX·방향) → 첫 15분 ORB 박스 형성 → 25분간 신호 스캔 → 진입 시 손절·익절 감시 → 청산. 스캔 중 신호가 없으면 그날은 매매 0건으로 마감한다.
| 상태 | 핵심 동작 |
|---|---|
| PRE_MARKET | VIX 12~30 범위 확인, QQQ 일봉으로 Daily Bias 점수 계산, 공휴일 체크 |
| ORB_FORMING | 개장 첫 15분(9:30~9:44 ET) 3개 봉으로 ORB 고가·저가 확정 |
| SCANNING | 돌파+FVG 감지, 되돌림 진입 시그널 탐색(하루 최대 1건) |
| POSITION_OPEN | 손절·익절 모니터링, 11:00 ET 손익분기 이동(BE move) |
1단계 — ORB, 첫 15분을 두 개의 선으로
ORB(Opening Range)는 개장 직후 첫 3개 5분봉(9:30·9:35·9:40 ET)의 최고가·최저가다. 이 두 값이 그날 모든 판단의 기준선이 된다. 코드는 놀랄 만큼 단순하다.
def calculate_orb(bars_5m):
# 09:30~09:44 ET, 첫 3개 5분봉만 사용
orb_bars = bars_5m.between_time("09:30", "09:44")
if len(orb_bars) < 3:
return None
orb_high = orb_bars["High"].max()
orb_low = orb_bars["Low"].min()
return OpeningRange(high=orb_high, low=orb_low,
range_size=orb_high - orb_low)
단순한 만큼 위험도 있다. 개장 초 변동성이 폭발하면 ORB 폭이 비정상적으로 넓어져, 손절 폭이 커지고 R:R이 무너진다. 그래서 20일 평균 일중 변동폭(ADR) 대비 ORB가 1.5배를 넘으면 그 종목은 그날 제외한다.
def is_orb_too_wide(orb, avg_daily_range, max_ratio=1.5):
ratio = orb.range_size / avg_daily_range
return ratio > max_ratio # 너무 넓으면 그 leg 스킵
2단계 — FVG, “가격이 건너뛴 빈 공간” 찾기
FVG(Fair Value Gap)는 3개 연속 캔들에서, 1번 봉과 3번 봉 사이에 겹치지 않는 가격 공간이 생긴 자리다. 상승 FVG는 1번 봉 고가가 3번 봉 저가보다 낮을 때(가격이 위로 점프) 성립한다. 이 조건 하나가 함수의 전부다.
def detect_bullish_fvg(candles): # 정확히 3개 봉
c1, c3 = candles.iloc[0], candles.iloc[2]
if c1["High"] < c3["Low"]: # 겹치지 않는 빈 공간
return FairValueGap(top=c3["Low"], bottom=c1["High"],
size=c3["Low"] - c1["High"])
return None
하락 FVG는 정확히 거울상이다(1번 봉 저가 > 3번 봉 고가). 이렇게 감지한 FVG box로 가격이 나중에 되돌아오면 그때가 진입 후보가 된다.
3단계 — Strict FVG, 가짜 신호를 걷어내는 핵심 조건
여기가 이 전략의 진짜 엔지니어링 포인트다. 단순히 “돌파 후 어딘가에 FVG가 생겼다”로 진입하면 신호가 너무 많고 대부분 가짜다. 원본 규칙의 핵심은 FVG가 ORB 라인을 관통해야 한다는 것. 이를 두 조건으로 코드화했다.
(S1) 몸통이 ORB 라인을 가로지름 — 돌파 봉의 시가는 ORB 라인 아래, 종가는 위. 즉 Open ≤ ORB high ≤ Close
(S2) FVG 영역이 ORB 라인을 포함 — fvg.bottom ≤ ORB high ≤ fvg.top
def check_breakout_with_fvg(bars, orb_high, i, strict=False):
candle = bars.iloc[i]
# 돌파: ORB high 위 종가 + 양봉
if not (candle["Close"] > orb_high and candle["Close"] > candle["Open"]):
return None
# (S1) 몸통이 ORB 라인을 가로지름
if strict and not (candle["Open"] <= orb_high <= candle["Close"]):
return None
fvg = detect_bullish_fvg(bars.iloc[i-1 : i+2])
if fvg is None:
return None
# (S2) FVG 영역이 ORB 라인을 포함
if strict and not (fvg.bottom <= orb_high <= fvg.top):
return None
return fvg
이 strict 한 줄의 효과가 컸다. 백테스트에서 가짜 시그널을 약 70% 제거했다. 신호 수가 줄어드는 대신, 남은 신호의 질이 올라간다 — 이 맞바꿈이 시리즈 뒤편에서 다시 중요한 주제가 된다.
4단계 — 시그널 엔진, 조각을 하나의 신호로
ORB와 FVG 조각을 실제 매매 신호로 합치는 곳이 시그널 엔진이다. 스캔 윈도우(9:45~10:55 ET)의 5분봉을 돌면서, 돌파+FVG가 확인되면 진입가·손절·목표를 하나의 신호 객체로 만든다.
def scan_for_signal(bars_5m, orb, symbol, rr_ratio=2.0,
strict=True, allowed_killzones=["AM_MACRO"],
require_displacement=False, ...):
# 1. 각 봉이 ORB 돌파 + FVG(strict=ORB 관통) 형성?
# 2. 진입 = FVG 중간값, 손절 = 직전 봉 저가
# 3. 목표 = 진입 + (진입-손절) × R:R
# 4. killzone·displacement·sweep 필터 통과?
return TradeSignal(entry_price=..., stop_loss=...,
take_profit=..., rr_ratio=rr_ratio) # 없으면 None
신호 객체(TradeSignal)에는 진입가·손절·목표뿐 아니라, 부분 익절가(1.5R)와 R:R 비율까지 담긴다. 진입·청산 규칙은 이렇게 정해진다.
• 진입 — 되돌림이 FVG 중간값에 닿는 순간 시장가 매수
• 손절(SL) — 직전 봉 저가
• 목표(TP) — 킬존별 R:R (AM_MACRO 1:3, 늦은 진입 1:2)
• 부분 익절 — 1.5R 도달 시 일부 청산
• 손익분기 이동 — 11:00 ET에 미청산 포지션 SL을 진입가로
• 강제 청산 — 15:50 ET 시장가 종료(오버나잇 리스크 0)
여기에 킬존 필터(개장 후 첫 40분에 나온 setup만 허용), 변위 봉(displacement) 검증, sweep+전환(CHoCH) 시퀀스 같은 상위 필터가 옵션으로 얹힌다. 함수 인자만 20개가 넘는데, 각 필터는 켜고 끌 수 있어서 백테스트로 효과를 검증하며 단계적으로 추가했다.
실제로 한 번 걸린 신호 — 2026-04-07 SQQQ
이론만으로는 감이 안 오니, 실거래 로그에서 가장 깔끔했던 사례 하나를 그대로 옮긴다.
• ORB 형성: SQQQ 고가 $77.57 / 저가 $76.52 (폭 $1.05)
• 강한 양봉이 ORB 고가를 몸통으로 관통하며 마감 → FVG [$77.98 ~ $78.13] 형성
• 되돌림이 FVG 중간값 $78.05에 닿음 → 매수 진입
• 약 8분 뒤 목표가 $79.24 도달 → 익절 ✅ +1.78R
돌파 → 변위 → FVG → 되돌림 → 진입 → 목표. 앞에서 코드로 쪼갠 조각들이 실거래에서 정확히 이 순서로 맞물린다.
정직한 트레이드오프 — 필터를 다 켜면 매매가 거의 없다
여기까지만 보면 “필터를 촘촘히 쌓을수록 좋은 봇”처럼 들린다. 하지만 실제로 상위 ICT 필터를 전부 켜자, 신호가 거의 잡히지 않았다. 60일 구간에서 조건이 다 맞아떨어지는 날이 손에 꼽을 정도였다. 흔한 패턴은 이랬다.
• ORB 돌파 후 되돌림 없이 추세 일변도 → 진입 자리 없음
• ORB 안에서 종일 횡보 → 돌파 자체가 안 나옴
• 좋은 setup이 킬존(첫 40분) 이후에 발생 → 필터로 차단
• Daily Bias가 중립이라 그날 자체를 스킵
“quality > quantity”라는 철학은 근사하지만, 매매 빈도가 낮다는 건 그 자체로 검증할 표본이 적다는 뜻이기도 하다. 그리고 표본이 적을수록, 몇 번 안 되는 거래에 붙는 수수료의 무게가 결과를 좌우하기 시작한다. 이 지점이 다음 두 글의 주제다 — 내가 만든 전략을 어떻게 의심했고(리스크 점검), 결국 왜 이 인트라데이 전략을 접었는지(수수료 이야기).
구현은 끝났지만, 엔지니어링에서 진짜 어려운 부분은 “돌아가게 만드는 것”이 아니라 “이게 정말 돈을 버는지 정직하게 확인하는 것”이었다. 코드는 시킨 대로 완벽히 돌아갔다. 문제는 언제나 그 코드가 무엇을 하도록 시켰는가에 있었다.
본 글은 공개된 데이터와 출처를 바탕으로 작성했습니다. 최종 업데이트: 2026-07-20