언어 개요¶
QuantiqDSL은 업비트 가상자산 자동매매를 위한 Python 유사 스크립팅 언어입니다. 일반 Python과 다른 점은 세 가지입니다.
- 이벤트 기반 실행 — 가격 변동이나 봉 마감마다 스크립트 전체가 처음부터 다시 실행됩니다.
- 샌드박스 환경 —
import,def,class등을 사용할 수 없습니다. 보안과 예측 가능성을 위한 제약입니다. - 내장 트레이딩 API —
chart(),ta.*,buy(),sell()등 트레이딩 전용 함수가 기본 제공됩니다.
스크립트 구조¶
모든 전략 스크립트는 아래 구조를 따릅니다.
# 1. 메타데이터 (필수)
version("1.0")
description("전략 이름")
# 2. 파라미터 선언 (선택)
param("period", "RSI 계산 기간", 14)
# 3. 청산 규칙 선언 (선택, rule.* 사용 시)
# 주문 게이트는 @trade(order_on=...) 데코레이터를 권장합니다 — 아래 "거래스크립트 데코레이터" 섹션 참조.
rule.stop_loss(pct=3.0)
# 4. 데이터 로딩
c = chart("1D")
# 5. 지표 계산
rsi = ta.rsi(c.close, script_params["period"])
# 6. 매매 결정 (반드시 하나만)
if rsi[0] < 30:
buy(tag="RSI 과매도")
elif rsi[0] > 70:
sell(tag="RSI 과매수")
else:
hold()
타입 시스템¶
TSeries — 시계열 데이터¶
chart() 함수와 ta.* 지표 함수는 모두 TSeries를 반환합니다. 인덱스 [0]이 가장 최신 값, [1]이 한 단계 이전 값입니다.
c = chart("1D")
c.close[0] # 현재 봉 종가
c.close[1] # 이전 봉 종가
c.close[2] # 2봉 전 종가
# 교차 감지
sma5 = ta.sma(c.close, 5)
sma20 = ta.sma(c.close, 20)
sma5.cross_up(sma20) # bool: 이번 봉에서 상향 교차 발생 여부
sma5.cross_down(sma20) # bool: 이번 봉에서 하향 교차 발생 여부
유효성 확인이 필요한 경우:
ScaleChart — 차트 객체¶
chart(timeframe) 함수가 반환하는 객체입니다. OHLCV 데이터와 오버레이 그리기 메서드를 제공합니다.
c = chart("5T") # 5분봉
# OHLCV 데이터
c.open[0] # 시가
c.high[0] # 고가
c.low[0] # 저가
c.close[0] # 종가
c.volume[0] # 거래량
# 차트 오버레이
c.line("이름", series, color="orange")
c.hline("기준선", 50, color="gray")
c.marker("신호", color="green", position="below", shape="triangle_up")
지원 타임프레임: "1T", "3T", "5T", "10T", "15T", "30T", "60T" (분봉), "1D" (일봉)
네임스페이스¶
| 네임스페이스 | 용도 | 예시 |
|---|---|---|
ta.* |
기술 지표 | ta.sma(), ta.rsi(), ta.macd() |
math.* |
수학 함수 | math.mean(), math.abs(), math.max() |
var.* |
실행 간 상태 유지 | var.init(), var.counter |
rule.* |
선언형 청산 규칙 | rule.stop_loss(), rule.take_profit() |
매매 결정 함수¶
스크립트 실행이 끝날 때 마지막으로 호출된 결정 함수가 최종 결정입니다.
| 함수 | 의미 |
|---|---|
buy(tag, qty, price) |
매수 |
sell(tag, qty, price) |
매도 |
hold(tag) |
현재 포지션 유지 |
exit(tag) |
포지션 청산 |
release(tag) |
종목 감시 해제 |
# tag는 사유 문자열, 나중에 로그에서 확인
buy(tag="RSI 반등 진입")
# qty로 수량 지정 (생략 시 전략 설정값 사용)
sell(qty=5, tag="절반 매도")
# price로 지정가 주문 (생략 시 시장가)
buy(qty=10, price=price * 0.995, tag="현재가 -0.5% 지정가")
상태 유지 (var)¶
일반 변수는 이벤트마다 초기화됩니다. 실행 간 값을 유지하려면 var를 사용하세요.
# 잘못된 방법 (매 이벤트마다 0으로 초기화)
count = 0
count += 1 # 항상 1
# 올바른 방법
var.init(count=0, last_price=0.0)
var.count += 1 # 누적됨
var.last_price = price # 이전 가격 저장
저장 제약: JSON 직렬화 가능한 타입만 허용 (int, float, str, bool, list, dict). 총 64KB 한도.
샌드박스 제약¶
보안을 위해 다음 구문은 사용할 수 없습니다.
import numpy as np # ❌ import 금지
def calc(): # ❌ def (함수 정의) 금지
class Model: # ❌ class 금지
lambda x: x * 2 # ❌ lambda 금지
eval("2+2") # ❌ eval 금지
exec("code") # ❌ exec 금지
open("file.txt") # ❌ 파일 접근 금지
대신 내장 함수와 허용된 네임스페이스를 활용합니다.
# 수학 계산
result = math.mean([c.close[i] for i in range(5)])
# 리스트 컴프리헨션 (허용)
prices = [c.close[i] for i in range(10)]
자세한 목록은 샌드박스 제약 문서를 참고하세요.
파라미터 (param)¶
param()으로 스크립트에 파라미터를 선언합니다. 거래 탭에서 실행 전에 값을 조정할 수 있습니다.
param("period", "RSI 계산 기간", 14) # 기본값 14
param("threshold", "과매도 기준", 30.0) # 기본값 30.0
param("name", "전략 이름", "RSI 전략") # 문자열 파라미터
# 값 읽기
rsi = ta.rsi(c.close, script_params["period"])
if rsi[0] < script_params["threshold"]:
buy(tag="과매도 진입")
거래스크립트 데코레이터 — @trade¶
기본 패턴은 데코레이터 없이 top-level 코드를 작성하는 것입니다. 엔진이 그 코드를 암묵적으로 @trade로 wrap해 주문 게이트와 함께 실행합니다.
명시적으로 주문 게이트 봉 스케일을 표현하려면 @trade(order_on=...) 데코레이터를 함수에 부착합니다:
version("1.1")
@trade(order_on="5T")
def main():
if position.qty == 0 and ta.rsi(chart("5T").close, 14).last() < 30:
buy(qty=1, tag="rsi_low")
else:
hold()
데코레이터가 부착된 함수는 평가 시 자동 호출됩니다. 데코레이터 없는 top-level 코드(패턴 1)와 @trade 함수(패턴 2)는 동일한 결과를 냅니다 — 마이그레이션 비용 0.
order_on 우선순위¶
- 외부
strategy_overrides(Studio UI에서 backtest 파라미터 override) @trade(order_on=...)데코레이터 인자 (정본)- 함수 본문 안
strategy.order_on = "..."호출 (런타임 mutate, 호환) - 기본값 — 데코레이터 경로는
"tick"(spec §4.2). 데코레이터 없는 패턴 1은 현재"5T"로 폴백.
주의:
@trade함수 본문 안에서rule.order_on()호출 또는f = rule.order_on같은 참조 자체가 컴파일 에러입니다.@trade함수에서 order_on을 지정하려면 반드시 데코레이터 인자(@trade(order_on="5T"))를 사용하세요. 데코레이터 없는 top-level 코드(패턴 1)에서는rule.order_on()이 여전히 허용됩니다.
허용 / 거부 값¶
order_on 인자는 "tick", "1T", "3T", "5T", "10T", "15T", "30T", "60T", "1H" 만 허용합니다. "1D" 등 지원되지 않는 값은 컴파일 에러로 즉시 반환됩니다 (decision.tag == "compile_error", errors에 안내). 평가가 부분적으로 진행되거나 예외가 호출자로 새지 않습니다.
데코레이터 인자 형식¶
오타나 잘못된 형식이 조용히 기본값("tick")으로 폴백하지 않도록 파싱 단계에서 엄격히 검증합니다. 다음 형식은 모두 컴파일 에러입니다:
- 위치 인자:
@trade("5T")— 반드시@trade(order_on="5T")형식 - 알 수 없는 키워드:
@trade(oder_on="5T")같은 오타 - 비문자열 값:
@trade(order_on=5)— 문자열 리터럴만 허용 - 중복 키워드:
@trade(order_on="5T", order_on="1T")
@trade 함수 시그니처 + 호출 정책¶
엔진이 자동으로 인자 없이 함수를 호출하므로, @trade 함수는 무인자 시그니처여야 합니다. 필수 인자를 가진 함수(def main(x): 등)는 컴파일 에러로 즉시 반환됩니다 — 런타임에서 missing argument로 깨지지 않습니다.
자동 호출이 1회만 일어남을 보장하기 위해, @trade 함수의 모든 직접 참조 / 재바인딩 / 삭제가 컴파일 에러입니다. main() 직접 호출, if 조건: main() 분기 호출, f = main; f() 별칭 후 호출, main = hold 재바인딩, del main 삭제 — 모두 차단됩니다. 엔진의 자동 호출이 원래 함수 본문이 아닌 다른 callable을 실행하거나 본문을 두 번 실행하면 buy/sell·var 갱신·로그가 손상되기 때문입니다.
timescales 자동 등록¶
@trade(order_on="5T")는 함수 안에서 chart("5T")를 호출하지 않더라도 5T 봉을 백테스트/실행 경로에서 자동으로 구독·집계 대상으로 등록합니다. chart("1T")만 사용하더라도 5T가 primary_timescale이 되어 주문 게이트가 정상 동작합니다.
@trade(order_on="5T")와 함수 본문의 런타임 변경(strategy.order_on = "1T")이 공존하는 스크립트는 두 스케일 모두 timescales에 등록됩니다 — 어느 분기로 흘러도 봉이 누락되어 주문 게이트가 우회되지 않습니다.
@trade() (kwarg 미지정 → "tick" 기본값) 스크립트는 primary_timescale이 "tick"으로 명시되며, UI/스케일 추출 경로(extract_scales())에서 tick → 1T로 정규화되어 Studio 차트에서 1T 표시 스케일로 처리됩니다 — 마커 표시·운영 휴리스틱이 잘못된 5T 폴백으로 떨어지지 않습니다.
rule.order_on(...) deprecation¶
rule.order_on(...) 함수 호출은 호환을 위해 동작하지만 deprecated 입니다. 호출 시 ScriptResult.warnings에 마이그레이션 안내가 한 번만 누적됩니다 (중복 호출도 1회). @trade(order_on=...) 데코레이터 인자로 대체하세요.
미구현 예약 데코레이터¶
알 수 없는 데코레이터는 모두 거부됩니다. 본 단계에서 모든 spec 정의 데코레이터(@trade, @screener, @universe.metric)가 구현되어 있습니다.
사용자 정의 universe metric — @universe.metric¶
@universe.metric("key", refresh="...") (#45)로 universe 전체 ticker × refresh 주기로 평가되는 사용자 정의 metric을 등록합니다. 등록된 metric은 sorted rank cache에 저장되어 universe.in_top("key", n) / universe.rank("key") / universe.percentile("key") / universe.metric("key")로 동일하게 참조 가능합니다.
version("1.1")
@universe.metric("rsi_5m_14", refresh="tick")
def _rsi():
return ta.rsi(chart("5T").close, 14)[0]
@trade(order_on="5T")
def main():
if universe.in_top("rsi_5m_14", 5):
buy(1, tag="rsi_top5_entry")
인자¶
| 인자 | 기본값 | 의미 |
|---|---|---|
key |
(필수) | metric 식별자. universe.in_top/rank/percentile에서 동일 key로 참조한다. 위치 인자 또는 key= 키워드 둘 다 가능. |
refresh |
"tick" |
평가 주기. "tick" / "5s" / "30s" / "1m" / "5m" 또는 "candle:<scale>". |
인자 검증¶
다음은 컴파일 에러로 즉시 반환됩니다 — 사용자가 잘못된 데이터로 실행되지 않게 하기 위함:
key누락 또는 빈 문자열:@universe.metric()/@universe.metric("")- 같은
key중복 등록: 한 스크립트에 동일 key의 metric이 둘 이상이면 어느 함수의 결과를 cache에 저장할지 모호 - 비문자열
key:@universe.metric(KEY, ...)같은 변수 참조 — 문자열 리터럴만 허용 - 위치 인자와
key=키워드 동시 사용:@universe.metric("a", key="b") - 알 수 없는 키워드:
@universe.metric("k", refrsh="tick")같은 오타 - 중복 키워드:
@universe.metric("k", refresh="tick", refresh="1m") - 잘못된 refresh:
weekly/candle:(빈 스케일) /candle:(공백만) - built-in ambient metric 이름과 충돌:
last_price,trade_value_24h/_1h/_5m,trade_volume_24h/_1h,change_pct_24h/_1h/_5m,volatility_1h/_24h,spread_bps. 사용자 metric이 같은 이름으로 등록되면 sweep write가 엔진의 WS 기반 rank index를 덮어써 다른 전략의universe.in_top/rank/percentile결과가 오염됩니다 — 등록 단계에서ValueError로 거부됩니다. 다른 key 이름을 선택하세요.
body 안 trading 액션 참조 금지¶
@universe.metric 함수 본문에서 buy / sell / hold / exit / screen / release 이름을 참조하면 컴파일 에러입니다. 직접 호출(buy(1))뿐 아니라 별칭 할당(f = buy; f(1)), 리스트 / 딕셔너리에 담아 호출(actions = [buy, sell]; actions[0](1)), 로컬 변수명 재사용(screen = 42)도 모두 거부됩니다. metric은 read-only 평가만 허용 — engine sweep이 매 ticker × refresh마다 반복 실행하는데 trading 액션이 fire되면 의도치 않은 주문이 폭주합니다. trading은 @trade 함수에서만 작성하세요.
@trade / @screener와 결합 금지¶
같은 함수에 @universe.metric과 @trade(또는 @screener)를 함께 부착하면 컴파일 에러입니다. 두 데코레이터는 호출 모델이 다릅니다 — @trade는 매 evaluate() 호출마다 모듈 끝에서 자동 호출되고, @universe.metric은 engine sweep이 ticker × refresh로 호출합니다. 한 함수가 두 의미로 동작하면 행동이 모호해집니다. 분리 함수로 작성하세요.
함수 시그니처 + 호출 정책¶
@trade/@screener와 동일하게 무인자 시그니처여야 합니다 — engine sweep이 인자 없이 호출합니다. 필수 인자가 있으면 컴파일 에러입니다.
직접 참조 / 재바인딩 / 삭제 모두 컴파일 에러입니다. _rsi() 직접 호출, f = _rsi; f() 별칭 후 호출, _rsi = something 재바인딩, del _rsi 삭제 — 모두 차단됩니다. metric 함수는 engine sweep이 자동 호출하므로 사용자가 추가 호출하면 cache 갱신이 중복되거나 일관성이 깨집니다.
함수 이름 unique¶
같은 스크립트 안의 @trade / @screener / @universe.metric 함수 이름은 모두 unique해야 합니다. 같은 이름의 def를 두 번 작성하면 Python의 last-def-wins 규칙으로 한 함수가 사라지고 sweep이 잘못된 본문을 호출합니다.
refresh 주기와 engine sweep¶
엔진의 metric sweep는 EngineEventLoop이 보유한 UniverseMetricSweeper가 매 step마다 due (strategy, ticker, metric) 작업을 만들어 evaluator의 evaluate_universe_metric()로 흘리고, 결과를 cache에 publish합니다. refresh="tick"은 매 step 발사, "5s"/"30s"/"1m"/"5m"은 wall-clock 경과 후 발사, "candle:<scale>"은 해당 scale 봉이 마감된 step에서 발사됩니다.
refresh="candle:5T"처럼 scale 토큰의 대소문자는 비교 시 정규화됩니다. candle:5t로 적어도 엔진이 5T 봉 마감 신호를 보낼 때 정상적으로 매칭됩니다(상위 호환성).
사용자 정의 metric에서 param("...") 사용¶
@universe.metric 본문의 param("name", default) 호출은 같은 스크립트의 @trade 평가 경로와 동일한 우선순위로 해석됩니다 — 전략 런타임 params.script override > 파서가 추출한 default. 즉 사용자가 Studio UI 또는 strategy 설정에서 파라미터를 바꾸면 metric 값과 trade 의사결정이 같은 파라미터로 평가됩니다.
param() 해석 — 런타임 override 우선¶
@universe.metric 본문의 param("name") 호출은 일반 @trade 경로와 동일한 우선순위로 해석됩니다: 런타임 script_params override가 있으면 그 값을, 없으면 스크립트가 선언한 default를 반환합니다. 따라서 Studio UI나 시뮬레이션이 multiplier=3.0처럼 파라미터를 override하면 그 값이 metric 평가에도 그대로 반영되어, 같은 ticker의 universe.in_top/rank/percentile 결과와 트레이딩 결정이 같은 파라미터 위에서 작동합니다 — metric만 default로 떨어져 사용자 결정과 어긋나는 일은 발생하지 않습니다.
barstate(scale).is_confirmed — closed_scales 반영¶
@universe.metric 본문의 barstate("5T").is_confirmed / is_new 값은 @trade 경로와 동일하게 엔진이 전달한 closed_scales 집합을 기준으로 평가됩니다. 즉 refresh="candle:5T" metric이 5T 봉 마감 시점에 호출되면 closed_scales={"5T"}로 들어와 is_confirmed=True가 되어, 봉 마감 게이트로 작성된 metric(예: 봉 close 시점에만 RSI를 갱신)이 stale 상태로 남지 않습니다. tick refresh metric에서는 봉 마감 외 타이밍이라 False로 평가됩니다.
top-level setup 구문 — strategy.order_on= 등¶
스크립트 상단에 strategy.order_on = "5T" / rule.stop_loss(2.0) / account.risk = 1.0 같은 setup 구문이 있어도 metric 평가가 NameError/AttributeError로 깨지지 않습니다. metric 평가 컨텍스트가 일반 @trade 평가와 동일한 표준 DSL 전역(strategy, position, market, condition, rule, barstate, script_params)을 inert default로 바인딩하고, account는 임의 attribute set/get을 허용하는 inert stub으로 들어갑니다 — metric 본문이 이들을 직접 사용하는 것은 권장되지 않지만, 모듈 본체의 setup 구문이 에러 없이 실행되도록 보장합니다.
script_params dict semantics¶
@universe.metric 본문에서 script_params dict를 직접 검사하는 패턴("p" in script_params / script_params.get("p"))은 일반 @trade 경로와 동일한 의미입니다: dict에는 runtime override만 들어 있고 스크립트가 선언한 default는 포함되지 않습니다. param("p") 호출은 두 경로 모두 runtime override > parser default 우선순위로 해석되지만, dict 멤버십 검사는 "사용자가 명시적으로 override했는가"를 의미합니다. 이렇게 분리하지 않으면 metric 평가에서만 default가 dict에 포함돼 if "p" in script_params: 같은 분기가 trade 평가와 어긋나는 silent divergence가 발생합니다.
Cross-ticker reference — universe.ref¶
universe.ref(symbol)(#44)으로 다른 ticker의 데이터를 read-only로 참조합니다. cross-market 비교(BTC 대비 상대강도, 메이저 코인 vs 알트코인 흐름 비교)를 한 strategy 안에서 작성할 수 있습니다.
version("1.1")
@trade(order_on="5T")
def main():
btc = universe.ref("KRW-BTC")
# ETH의 1h 변화율과 BTC의 1h 변화율을 비교해 outperform이면 매수
eth_change = chart("1h").close[0] / chart("1h").close[2] - 1.0
btc_change = btc.chart("1h").close[0] / btc.chart("1h").close[2] - 1.0
if eth_change - btc_change > 0.05 and position.qty == 0:
buy(qty=1, tag="eth_outperform_btc")
SymbolRef 인터페이스¶
universe.ref(symbol)은 다음 속성을 가진 SymbolRef 객체를 반환합니다:
| 속성/메서드 | 반환 | 의미 |
|---|---|---|
chart(scale) |
read-only _ScaleChart |
다른 ticker의 OHLCV 시계열 + ta.*/tseries.* 연산 가능 |
price |
float | None | cache의 last_price ambient (ticker frame 미수신 시 None) |
volume |
float | None | 가용 scale 중 가장 작은 것의 마지막 candle volume (없으면 None) |
ta |
_TaNamespace |
전역 ta와 동일한 indicator namespace (btc.ta.rsi(btc.chart("5T").close, 14)) |
symbol |
str | 참조 ticker (예: "KRW-BTC") |
Read-only 보장 — overlay 차단¶
SymbolRef.chart(...)이 반환하는 chart는 read-only입니다. line / marker / pane / hline / vline / histogram 같은 overlay 메서드를 호출하면 RefOverlayNotAllowed 런타임 에러로 실패합니다 (marker variant: arrow_up_marker / arrow_down_marker / circle_marker / square_marker / status_marker 모두 동일). 이유: Studio는 active 종목 panel에만 overlay를 그리므로 다른 ticker의 chart에 overlay하는 시도는 의미가 없고 사용자가 의도와 다른 결과를 보게 됩니다. 본인 ticker의 chart(chart("5T").line(...))는 그대로 작동합니다.
자동 scope 추가¶
참조한 ticker가 strategy의 universe scope에 속하지 않더라도 universe.ref()가 자동으로 cache에 추가합니다 — 엔진은 해당 ticker의 ticker frame과 candles를 read-only feed로 유지합니다. strategy 메타에 명시 등록 없이도 cross-ticker 참조가 동작하므로, 한 strategy 안에서 다양한 비교 지표를 작성할 수 있습니다.
candle provider wiring¶
SymbolRef.chart(scale)은 evaluator의 ref_candle_provider: Callable[[ticker, scale], list[candle]] 인자를 통해 다른 ticker의 candles를 받아옵니다. 이 인자가 미주입된 상태에서 chart()를 호출하면 명확한 wiring 에러가 raise됩니다 — empty chart로 silent하게 동작하지 않습니다. 백테스트와 live engine 모두 ticker별 candle store를 만들고 provider로 주입해야 합니다.
스크리너 데코레이터 — @screener¶
@screener(refresh, on_drop, persistent)로 ticker × strategy state machine을 등록합니다. 함수 안에서 screen() / release()로 명시적 in/out 전이를, screen.is_in / screen.state / screen.in_since로 상태 읽기를 합니다.
version("1.1")
@screener(refresh="tick", on_drop="force_exit")
@trade(order_on="5T")
def main():
if not universe.in_top("trade_value_24h", n=50):
return # 자동 release (out)
if ta.rsi(chart("5T").close, 14).last() < 30:
screen() # 명시 in
if position.qty == 0:
buy(qty=1, tag="entry")
인자¶
| 인자 | 기본값 | 의미 |
|---|---|---|
refresh |
"tick" |
평가 주기. "tick" / "5s" / "30s" / "1m" / "5m" 또는 "candle:<scale>". |
on_drop |
"deactivate" |
in → out 전이 시 정책. "deactivate" / "force_exit" / "ignore". |
persistent |
False |
True이면 미호출 시 이전 state 유지. 기본은 매 평가마다 자동 release. |
state 전이 (spec §5.1, last-call-wins)¶
pending (한 번도 평가 안 됨) / in (UI 노출 + trading 활성) / out (UI 비노출 + trading 무시).
screen()호출 →inrelease()호출 →out(in→out 전이 시on_drop발동)- 아무것도 호출 안 함:
persistent=False(기본) → 자동 release(out); pending도 첫 평가 후outpersistent=True→ 이전 state 유지
같은 실행에서 screen()과 release()를 둘 다 호출하면 마지막 호출이 win — 상태 전이뿐 아니라 결과 decision까지 일관됩니다. release(); screen() 이면 state는 in이 되고, release()가 잠시 만들어 둔 RELEASE decision은 자동으로 무효화됩니다. 강등된 자리는 (1) rule.stop_loss/take_profit/... 등 rule 평가 결과 → (2) decision_holder의 가장 최근 비-RELEASE entry → (3) 중립 HOLD 순서로 채워집니다 — 즉 release(); screen()을 거쳐도 risk 안전장치 rule은 그대로 동작합니다.
refresh의 candle:<scale> 형식은 콜론 뒤에 비어있지 않은 스케일 토큰이 필요합니다. candle:만 적거나 candle: 같이 공백만 있는 경우도 컴파일 에러로 차단됩니다.
분리 함수 패턴에서는 @screener 함수와 @trade 함수가 서로 다른 이름이어야 합니다. 같은 이름을 두 번 def로 정의하면 Python의 일반 규칙대로 마지막 정의가 첫 정의를 덮어쓰므로 한 함수 본문이 영영 실행되지 않습니다 — 컴파일 단계에서 거부됩니다.
on_drop 정책¶
in → out 전이 시점에만 발동:
"deactivate"(기본): UI/추적에서 빼지만 포지션 유지. trading 액션은 게이팅 (HOLD로 override)."force_exit": EXIT decision으로 시장가 강제 청산. 추적 해제."ignore": 아무것도 안 함. 다음 refresh에 다시 in 될 수 있음.
trading 액션 게이팅¶
@screener 컨텍스트에서 state == out이면 buy/sell/hold는 HOLD("screener_out")로 override되고 logs에 trade ignored: ticker_out (was BUY) 같은 안내가 쌓입니다. EXIT/RELEASE는 그대로 통과합니다.
@screener 없는 스크립트(패턴 1·2)는 항상 trading 활성. screen()을 @screener 없는 곳에서 호출하면 명확한 RuntimeError.
state 영속 + 재시작 복구¶
ticker × strategy state는 var namespace의 screen__state / screen__in_since_ts / screen__last_eval_ts에 영속됩니다. 재기동 후 이전 state로 복구되어, in 상태로 죽었을 때 force_exit 정책 때문에 의도치 않게 포지션이 청산되는 사고를 막습니다 (spec §5.4).
refresh cadence 동작¶
refresh 인자가 "tick"이 아니면 엔진이 cadence를 강제합니다. 평가 시점에 직전 screen__last_eval_ts로부터 refresh 간격이 elapse되지 않았으면(또는 candle:5T인데 5T 봉이 마감되지 않았으면) 그 평가는 frozen 상태로 떨어집니다.
candle:<scale> 토큰의 scale 비교는 대소문자 비구분입니다. refresh="candle:5t"로 적어도 엔진이 5T 봉 마감 신호를 보낼 때 정상적으로 매칭됩니다 — 사용자가 일관된 표기를 강제하지 않아도 됩니다.
screen()/release()호출은 no-op (state machine에 push되지 않습니다).- screener state는 직전 commit된 값(
prev_state)이 그대로 유지되고,screen__last_eval_ts도 갱신되지 않습니다. - 직전 state가
out이면 trading 액션 게이팅은 freeze 중에도 그대로 적용됩니다 — unified 패턴(같은 함수에@screener+@trade)에서 trade 본문이 매 틱 실행되더라도 state는 cadence 시점에만 바뀌고 그 사이에는 안전하게 잠깁니다. - 직전 state가
in인데 cadence가 elapse되지 않은 채로 missing-call이 발생해도force_exit는 발동하지 않습니다 — 다음 due 평가에서 commit된 transition만 force_exit 결정 권한을 가집니다.
refresh="tick"(기본)인 경우 cadence freeze는 발생하지 않고 매 평가에서 state machine이 평가됩니다.
단일 함수 vs 분리 함수¶
# 패턴 3: 한 함수에 두 데코레이터
@screener(refresh="tick", on_drop="force_exit")
@trade(order_on="5T")
def main():
...
# 패턴 4: screener와 trader가 다른 cadence
@screener(refresh="1m", on_drop="deactivate")
def screen_fn():
if universe.in_top("trade_value_24h", n=50):
screen()
@trade(order_on="5T")
def trader():
if screen.is_in and position.qty == 0:
buy(qty=1, tag="entry")
데코레이터 순서(@screener @trade vs @trade @screener)는 둘 다 허용. @screener 단독(@trade 없이)은 컴파일 에러.
스타터 템플릿¶
Studio에서 템플릿을 열면 바로 실행·수정 가능한 스타터 예제가 제공됩니다. 주요 패턴별 스타터 목록:
| 스타터 | 패턴 | 설명 |
|---|---|---|
듀얼 EMA 크로스 스타터 |
패턴 1 (top-level) | 기존 스타일 EMA 추세추종 |
@trade 데코레이터 EMA 스타터 |
패턴 2 (@trade 단독) |
@trade(order_on="5T") 선언형 작성 예시 |
RSI-거래대금 스크리너+매매 스타터 |
패턴 3 (unified) | @screener+@trade, on_drop="force_exit", screen()/release() 상태 머신 |
거래대금 상위 N 스크리너+매매 스타터 |
패턴 3 (unified) | universe.in_top("trade_value_24h", n=N) 파라미터로 유니버스 크기 조절 |
유니버스 커스텀 지표 스타터 |
패턴 4 (분리) | @universe.metric("rsi_5m_14") 등록 + 분리된 screener_fn / trader |
퍼시스턴트 스크리너 스타터 |
패턴 3 (persistent) | persistent=True — 명시적 release() 전까지 in 상태 유지 |
스타터 기본 리스크 관리: 스타터 템플릿은
rule.stop_loss+rule.trailing_stop조합을 기본으로 사용합니다.rule.take_profit은 여전히 유효한 DSL 함수이지만, trailing stop과 함께 쓸 경우 먼저 도달한 규칙이 청산을 선점하므로 스타터 기본값에서는 제외합니다. 고정 익절이 필요하면 스크립트에 직접 추가하세요.커뮤니티 멀티 타임프레임 예제 주의: 주봉/일봉 분석 스크립트의
order_on은"1H"로 설정되어 있습니다."1D"스케일은 주문 게이트로 지원되지 않으므로 분석 데이터는chart("1D")로 읽고, 주문 실행 게이트는@trade(order_on="1H")또는rule.order_on("1H")를 사용하세요.