파인스크립트 손절·익절·트레일링 스탑 만들기 (strategy.exit 예제)
진입 신호만큼 중요한 것이 청산입니다. 손절이 없는 전략은 한 번의 큰 손실로 그동안의 수익을 모두 잃을 수 있습니다.
이 글에서는strategy.exit()로 퍼센트 손절·익절, ATR 손절, 트레일링 스탑을 만들고, 각각 TVExtBot 주문메시지로 자동매매하는 방법을 설명합니다.
이 글에서 배우는 것
strategy.exit()의 주요 인수- 퍼센트 기준 / ATR 기준 손절·익절
- 트레일링 스탑
- 익절·손절마다 다른 얼러트 메시지 보내기
※ 예제 코드는 파인스크립트 v6 기준입니다. 전략이 처음이라면 지표를 전략으로 바꾸는 방법을 먼저 읽어 주세요.
1. strategy.exit() 기본
strategy.exit(id, from_entry, limit, stop, profit, loss, trail_price, trail_points, trail_offset, ...)| 인수 | 의미 | 단위 |
|---|---|---|
from_entry | 청산할 진입 주문 이름 (예: "Long") | |
limit | 익절 가격 | 가격 |
stop | 손절 가격 | 가격 |
profit / loss | 진입가 기준 익절·손절 거리 | 틱 |
trail_price / trail_points | 트레일링 시작 가격 / 시작 거리 | 가격 / 틱 |
trail_offset | 최고가에서 얼마나 내려오면 청산할지 | 틱 |
alert_message | 청산 시 얼러트 메시지 | |
alert_profit / alert_loss / alert_trailing | 익절 / 손절 / 트레일링으로 청산될 때 각각의 메시지 |
틱은 종목의 최소 가격 단위(syminfo.mintick)입니다. 퍼센트나 가격으로 계산하는 편이 여러 종목에 쓰기 쉬워, 이 글에서는 limit, stop 가격을 직접 계산합니다.
strategy.exit() 는 호출한 봉에서 끝나지 않고, 포지션이 청산될 때까지 계속 유효한 주문으로 남습니다. 봉마다 호출하면 가격이 갱신됩니다.
2. 퍼센트 손절·익절
tpPct = input.float(3.0, "익절 (%)", minval = 0.1, step = 0.1)
slPct = input.float(1.5, "손절 (%)", minval = 0.1, step = 0.1)
if strategy.position_size > 0
entry = strategy.position_avg_price
strategy.exit("Exit", "Long",
limit = entry * (1 + tpPct / 100),
stop = entry * (1 - slPct / 100))strategy.position_avg_price는 평균 진입가입니다. 진입 직후 봉부터 값이 있습니다.- 숏 포지션이라면
limit은 진입가 아래,stop은 진입가 위로 계산합니다.
3. ATR 기준 손절·익절
변동성이 큰 시기에는 손절 폭을 넓게, 작은 시기에는 좁게 잡는 방법입니다. 진입할 때의 ATR 값을 저장해 두고 사용해야 손절가가 봉마다 흔들리지 않습니다.
atrLen = input.int(14, "ATR 기간")
slMult = input.float(2.0, "손절 ATR 배수")
tpMult = input.float(3.0, "익절 ATR 배수")
atr = ta.atr(atrLen)
var float entryAtr = na
if buySignal and strategy.position_size == 0
entryAtr := atr // 진입 시점의 ATR 저장
strategy.entry("Long", strategy.long)
if strategy.position_size > 0
entry = strategy.position_avg_price
strategy.exit("Exit", "Long",
limit = entry + entryAtr * tpMult,
stop = entry - entryAtr * slMult)4. 트레일링 스탑
가격이 목표만큼 오르면 추적을 시작하고, 최고가에서 일정 비율 내려오면 청산합니다. 추세를 길게 따라가면서 수익을 지키는 방법입니다.
trailStartPct = input.float(2.0, "트레일링 시작 (%)")
trailOffsetPct = input.float(1.0, "최고가 대비 하락 (%)")
if strategy.position_size > 0
entry = strategy.position_avg_price
strategy.exit("Trail", "Long",
stop = entry * (1 - slPct / 100), // 기본 손절
trail_price = entry * (1 + trailStartPct / 100), // 이 가격에 도달하면 추적 시작
trail_offset = math.max(1, math.round(entry * trailOffsetPct / 100 / syminfo.mintick))) // 틱 단위
※ trail_offset 은 틱 단위입니다. 퍼센트를 가격으로 바꾼 뒤 syminfo.mintick 으로 나눠 틱 수로 변환합니다.
5. 전체 코드: 슈퍼트렌드 + 손절·익절·트레일링
지표를 전략으로 바꾸는 방법에서 만든 슈퍼트렌드 전략에 청산 규칙을 더했습니다. 익절, 손절, 트레일링, 추세 반전 청산마다 다른 주문메시지를 보낼 수 있습니다.
전략 스크립트 (Pine Script v6)
1//@version=6
2strategy("슈퍼트렌드 + 손절·익절", overlay = true,
3 initial_capital = 10000,
4 default_qty_type = strategy.percent_of_equity,
5 default_qty_value = 10,
6 commission_type = strategy.commission.percent,
7 commission_value = 0.05,
8 slippage = 1)
9
10// ── 설정 ─────────────────────────────
11atrPeriod = input.int(10, "ATR 기간", minval = 1, group = "슈퍼트렌드")
12factor = input.float(3.0, "ATR 배수", minval = 0.1, step = 0.1, group = "슈퍼트렌드")
13
14tpPct = input.float(4.0, "익절 (%)", minval = 0.1, step = 0.1, group = "청산")
15slPct = input.float(2.0, "손절 (%)", minval = 0.1, step = 0.1, group = "청산")
16useTrail = input.bool(true, "트레일링 스탑 사용", group = "청산")
17trailStartPct = input.float(2.0, "트레일링 시작 (%)", minval = 0.1, step = 0.1, group = "청산")
18trailOffsetPct = input.float(1.0, "최고가 대비 하락 (%)", minval = 0.1, step = 0.1, group = "청산")
19
20entryMsg = input.text_area("", "롱 진입 주문메시지", group = "TVExtBot")
21tpMsg = input.text_area("", "익절 주문메시지", group = "TVExtBot")
22slMsg = input.text_area("", "손절 주문메시지", group = "TVExtBot")
23trailMsg = input.text_area("", "트레일링 청산 주문메시지", group = "TVExtBot")
24exitMsg = input.text_area("", "추세 반전 청산 주문메시지", group = "TVExtBot")
25
26// ── 신호 ─────────────────────────────
27[st, dir] = ta.supertrend(factor, atrPeriod)
28plot(dir < 0 ? st : na, "상승 추세", color.green, style = plot.style_linebr)
29plot(dir > 0 ? st : na, "하락 추세", color.red, style = plot.style_linebr)
30
31buySignal = ta.change(dir) < 0
32sellSignal = ta.change(dir) > 0
33
34// ── 진입 ─────────────────────────────
35if buySignal and strategy.position_size == 0
36 strategy.entry("Long", strategy.long, alert_message = entryMsg)
37
38// ── 청산 ─────────────────────────────
39if strategy.position_size > 0
40 entry = strategy.position_avg_price
41 tpPrice = entry * (1 + tpPct / 100)
42 slPrice = entry * (1 - slPct / 100)
43 trailTick = math.max(1, math.round(entry * trailOffsetPct / 100 / syminfo.mintick))
44
45 strategy.exit("Exit", "Long",
46 limit = useTrail ? na : tpPrice,
47 stop = slPrice,
48 trail_price = useTrail ? entry * (1 + trailStartPct / 100) : na,
49 trail_offset = useTrail ? trailTick : na,
50 alert_profit = tpMsg,
51 alert_loss = slMsg,
52 alert_trailing = trailMsg)
53
54 if sellSignal
55 strategy.close("Long", alert_message = exitMsg)
56
57// ── 차트에 손절·익절 가격 표시 ─────────────
58plot(strategy.position_size > 0 ? strategy.position_avg_price * (1 - slPct / 100) : na, "손절가", color.red, style = plot.style_linebr)
59plot(strategy.position_size > 0 and not useTrail ? strategy.position_avg_price * (1 + tpPct / 100) : na, "익절가", color.green, style = plot.style_linebr)얼러트 메시지 칸에는 {{strategy.order.alert_message}} 한 줄만 입력합니다. 청산 이유에 따라 해당 주문메시지가 전송됩니다. (얼러트 메시지 정리 참고)
6. 전략 손절과 거래소 손절 주문의 차이
| 구분 | 전략의 손절 (이 글의 방법) | 거래소에 미리 거는 손절 주문 |
|---|---|---|
| 동작 | 손절가에 닿으면 얼러트 → 시장가 주문 | 거래소가 가격을 보고 직접 체결 |
| 장점 | 백테스트와 같은 규칙으로 동작, 전략 안에서 관리 | 트레이딩뷰·네트워크에 문제가 있어도 동작 |
| 주의 | 얼러트 전송이 늦거나 급변동이면 손절가보다 불리하게 체결될 수 있음 | 전략 백테스트와 따로 관리해야 함 |
※ 전략의 손절은 얼러트가 전송되어야 주문이 나갑니다. 레버리지를 크게 쓴다면 청산가보다 충분히 여유 있게 손절을 잡아 주세요.
7. 자주 하는 실수
from_entry이름이 진입 이름과 다름:strategy.entry("Long", ...)이라면strategy.exit(..., "Long", ...)처럼 같은 이름을 써야 합니다.profit,loss에 퍼센트나 가격을 넣음: 두 인수는 틱 단위입니다. 가격을 쓰려면limit,stop을 사용합니다.- ATR을 매 봉 새로 계산해 손절가가 움직임: 진입 시점의 값을
var변수에 저장해서 쓰세요. - 같은 봉 안에서 손절과 익절에 모두 닿음: 과거 봉에서는 봉 안의 가격 순서를 알 수 없어 트레이딩뷰가 추정해 체결합니다. 손절·익절 폭이 봉 크기보다 충분히 커야 결과를 믿을 수 있습니다.
