콘텐츠로 이동

실행 모델

전략이 "언제" 실행되고 "어떻게" 주문이 나가는지 이해하면, 전략이 예상과 다르게 동작하는 상황을 방지할 수 있습니다. 예를 들어 "봉이 아직 마감되지 않았는데 매수가 나갔다"거나 "아무 신호가 없는데 매도가 실행됐다"는 문제의 원인이 대부분 실행 모델을 모르기 때문입니다.

처음에는 개념만 파악하면 충분합니다. 나중에 전략이 예상과 다르게 동작할 때 다시 찾아보세요.


이벤트 기반 실행

스크립트는 이벤트가 발생할 때마다 처음부터 끝까지 다시 실행됩니다.

이벤트 종류

대부분의 전략은 event 변수를 직접 체크할 필요가 없습니다. 스크립트는 이벤트마다 실행되므로, 조건을 그냥 작성하면 됩니다. event는 특정 이벤트에서만 동작을 다르게 하고 싶을 때 사용합니다.

price_change — 실시간 가격 변동

예시 (특정 이벤트에서만 처리하고 싶을 때):

if event == "price_change":
    # 가격이 바뀔 때마다 실행
    if price < position.avg_price * 0.97:
        sell(tag="3% 손절")

candle_close — 봉 마감

if event == "candle_close":
    # 봉이 확정될 때 실행 (권장)
    if barstate("1D").is_confirmed:
        # 일봉 마감 기준 판단

일봉 전략에서는 event 체크가 필요 없습니다

일봉(1D) 전략은 event == "candle_close" 체크 없이도 봉 마감 시점에만 실행됩니다. barstate("1D").is_confirmed만으로 충분합니다. 두 조건을 함께 쓰면 봉 마감 이외 이벤트에서도 스크립트가 실행될 때 조건 평가 순서가 달라져 예상치 못한 타이밍에 신호가 생길 수 있습니다.

봉 마감 기준 전략을 권장합니다

price_change 이벤트는 활발한 시간대에 초당 수십~수백 번 발생합니다. 특별한 이유가 없다면 봉 마감(candle_close) 또는 barstate().is_confirmed를 사용하는 것이 더 안정적입니다.

24/7 거래소 — 장 시작/마감 개념 없음

Upbit는 24시간 거래되므로 장 시작·마감 시각, 장중/장마감 같은 KRX(국내주식) 시장 시간 개념이 적용되지 않습니다. 엔진은 항상 동작하며, 사용자 결정 BUY/SELL을 시간으로 차단하는 자동 게이트는 없습니다. rule.close_before(...)market.minutes_until_close / market.minutes_since_open은 제거되었습니다 (#29). 시간 기반 정책이 필요하면 market.time (KST "HH:MM")을 직접 비교해 사용자가 정의하세요.

Upbit Public/Private WebSocket으로 시세·체결 분리

실시간 시세는 Upbit Public WS의 ticker 채널에서, 주문 체결 통보는 Upbit Private WS의 myOrder 채널(키 저장 후 활성화)에서 각각 받습니다. KIS 시절의 단일 시세 WebSocket(시장시간 가드 + 체결통보 결합)은 Upbit 피벗에서 제거되었습니다(#32 PR-2b'-γ). 따라서 (1) 차트/probe/백테스트는 Upbit Quotation REST API로 비인증 호출되고, (2) 실주문(매수/매도)이 발생하는 시점에만 Private WS가 키를 사용해 체결을 푸시합니다. 키를 저장하지 않은 상태에서도 Studio 차트와 백테스트는 정상 동작하며, 키 저장 후 다음 활성화 사이클에 Private WS가 자동 연결됩니다.

실행 순서

매 이벤트마다 아래 순서로 처리됩니다.

flowchart TD
    A["이벤트 수신\nprice_change / candle_close"] --> B["스크립트 전체 실행\n(처음부터 끝까지)"]
    B --> C{"마지막 결정 함수"}
    C -->|"hold() / buy() / sell() / exit()"| D["리스크 검사\n손절 · 익절 · 계좌 한도"]
    C -->|"release()"| F["즉시 해제 처리\n(게이트 무시)"]
    D -->|"rule.* / 리스크 강제청산"| G["시장가 주문 전송\n(order_on 우회)"]
    D -->|"미발동 + hold()"| H["주문 없음"]
    D -->|"일반 BUY/SELL 통과"| E{"order_on 게이트"}
    E -->|"게이트 열림\n(봉 마감 일치)"| I["주문 전송\n내 PC → 업비트 거래소"]
    E -->|"게이트 닫힘"| H
    D -->|"차단"| H

의사결정 규칙

한 번의 실행에서 buy, sell, hold 중 하나만 최종 결정이 됩니다. 여러 번 호출하면 마지막 호출만 유효합니다.

buy(tag="첫 번째 시도")
sell(tag="두 번째 시도")
# 최종 결정: SELL (마지막 호출)

모든 분기에서 반드시 하나의 결정이 내려지도록 if/elif/else를 완성하세요.

# 올바른 패턴
if 조건_A:
    buy(tag="A 조건")
elif 조건_B:
    sell(tag="B 조건")
else:
    hold()      # 나머지 경우에도 반드시 결정

상태 유지 (var)

일반 변수는 이벤트마다 초기화됩니다. 이전 실행의 값을 기억하려면 var를 사용하세요.

# 잘못된 방법 — 항상 1
count = 0
count += 1

# 올바른 방법 — 실행 간 누적
var.init(count=0)
var.count += 1
log("총 실행 횟수:", var.count)

자세한 내용: var 네임스페이스


주문 타이밍 (order_on)

order_on은 주문이 실행될 봉 마감 조건을 설정합니다. 패턴에 따라 선언 방식이 다릅니다.

패턴 1 — 데코레이터 없는 top-level 코드: rule.order_on()을 사용합니다.

rule.order_on("5T")   # 5분봉 마감 시에만 주문

패턴 2 — @trade 함수: @trade(order_on=...) 데코레이터 인자를 사용합니다. @trade 함수 본문 안에서 rule.order_on()을 호출하면 컴파일 에러입니다.

@trade(order_on="5T")
def main():
    ...  # rule.order_on() 호출 금지 — 데코레이터 인자를 사용할 것

order_on 게이트가 열리지 않은 이벤트에서는 일반 buy(), sell() 주문이 전송되지 않습니다.
다만 아래 두 경우는 order_on을 우회해 즉시 처리됩니다.

  • rule.* 태그로 생성된 주문 (예: rule.stop_loss, rule.take_profit)
  • 리스크 엔진 강제청산 (risk_liquidate)

release()는 게이트 상태와 무관하게 즉시 해제 처리됩니다. hold()는 주문을 전송하지 않으므로 게이트의 영향을 받지 않습니다.

지원 값: "tick", "1T", "3T", "5T", "10T", "15T", "30T", "60T", "1H" — 일봉(1D) 초과는 지원하지 않습니다.


백테스트

스튜디오에서 백테스트를 실행하면 과거 데이터로 전략의 동작을 검증할 수 있습니다.

  • 실거래와 동일한 주문 판단 로직을 사용합니다
  • 봉 마감 타이밍이 실거래와 동일하게 적용됩니다
  • 거래량이 0인 봉(무체결 구간)에서는 시장가 주문이 체결되지 않습니다
  • @screener + @trade 스크립트는 백테스트에서도 동일하게 동작합니다 — screener 상태(screen__state)가 봉 간 유지되고 on_drop 정책도 재현됩니다

백테스트 알려진 제한: universe.in_top() / universe.rank() 등 universe 기반 스크리너는 백테스트에서 지원되지 않습니다. 과거 시점의 universe 순위를 재구성하는 기능이 별도로 필요합니다. - 리스크 트리거는 종가만이 아니라 시가→고가→저가→종가 경로에서 평가됩니다 - 리스크 트리거 가격(expected_price)과 실제 체결가(fill_price)는 다를 수 있습니다 (예: next_open 체결 모델) - 마지막 캔들에서 리스크가 트리거되면 다음 봉이 없으므로 next_open 설정과 무관하게 트리거 가격으로 체결됩니다 (체결 모델이 close로 자동 전환)

백테스트 결과는 미래를 보장하지 않습니다

백테스트는 전략의 논리적 타당성을 확인하는 도구입니다. 과거 성과가 미래 수익을 보장하지 않으며, 실제 시장 상황은 백테스트와 다를 수 있습니다.


자체 스크리너 (업비트 피벗 이후)

업비트 피벗 이후 quantiq는 외부 HTS 조건검색식에 의존하지 않고 자체 스크리너로 시장 조건에 따른 감시 대상을 DSL 내부에서 선정합니다. 유니버스(개인 취향) → 스크리너(시장 조건) → 거래스크립트(진입·청산) 3계층 구조입니다.

  • 유니버스: UI에서 고정. 마켓 선택(KRW/BTC/USDT), 수동 코인 리스트, 제외 리스트
  • 스크리너: DSL @screener 메서드에서 시장 데이터 기반 필터·랭킹 (거래대금 상위 N, 변동성 돌파, BTC 상대 강도 등)
  • 거래스크립트: DSL @strategy 메서드에서 진입/리스크/청산 제어, 스크리너가 설정한 맥락 변수(var.*) 참조
유니버스 → 스크리너 → 거래스크립트 → 진입/청산

자세한 개념은 ssot/spec/upbit-screener.md 를 참고하세요. 국내주식 버전의 HTS 조건검색 연동은 업비트 버전에서 제공하지 않습니다.

@universe.metric 활성화와 캔들 워밍업 (구현 진행 중)

@universe.metric을 사용하는 전략 활성화 시 캔들 이력 프리워밍 기능이 구현 중입니다. 완성 시 DSL이 참조하는 봉 스케일에 대해 전체 스코프 종목의 이력을 REST로 미리 로드하며, 로드 완료 전까지 metric 평가를 일시 정지합니다.

자세한 내용과 현재 구현 상태는 워밍업 모드를 참고하세요.


실행 흐름 전체 요약

전략 하나의 실행 흐름을 한 줄로 요약하면:

이벤트 수신 → 전략 코드 실행 → buy/sell/hold 결정 → 리스크 검사 → 주문 전송

각 단계의 상세 내용이 궁금하다면 아래 다음 단계 문서를 참고하세요.


다음 단계