본문 바로가기
Algorithm Trading/Pine Script v5 기본

Pine Script v5 기본 개념 - 2 (Strategies)

by 컴돈AI 2024. 1. 5.

목차

    Introduction

    • Pine Script의 strategy는 과거나 실시간 데이터에 대한 거래 실행을 시뮬레이션 하여 트레이딩 시스템의 백테스팅과 포워드 테스팅을 지원합니다.
    • strategy는 indicator와 거의 동일한 기능을 포함하면서 가상 주문을 넣고, 수정하고, 취소하며 결과를 분석할 수 있는 기능을 제공합니다.
    • strategy 선언
      • 스크립트에서 strategy() 함수를 사용하면 strategy.* 에 접근할 수 있고, 주문 시뮬레이션 및 필수 strategy 정보에 대한 함수와 변수를 호출할 수 있습니다.
      • 전략테스터 탭에서 시뮬레이션 결과를 표시합니다.
    • 모든 주문은 bar가 마감이 되고 접수가 됩니다. 만약 다음봉 시가에서 바로 체결될 수 있는 주문이라면 바로 체결되는 형태입니다. 접수가 되고 해당 가격이 오기를 기다리는 주문은 bar가 마감되고 해당 봉에서 스크립트가 실행되기 전에 해당가격에 도달했었는 체크를 하고 도달했다면 해당 봉에 곧바로 체결내역을 표시해줍니다.
      • 즉, 스크립트가 실행되는 것은 봉이 마감되고 실행되는 것이기때문에, 바로 체결될수 있는 주문은 바로 다음 봉 시가에서 체결표시가 나타나지만, 스크립트가 실행되고 나중에 체결되는 주문은 가격이 도달한 시점의 봉에 바로 체결표시가 나타납니다.

    간단한 strategy 예시

    • 두개의 이동 평균선의 교차 시 롱 또는 숏 포지션에 진입하는 간단한 전략 코드를 작성해 보겠습니다. 
    • 코드
      • //@version=5
        strategy("test", overlay = true)
        
        // Calculate two moving averages with different lengths.
        float fastMA = ta.sma(close, 14)
        float slowMA = ta.sma(close, 28)
        
        // Enter a long position when `fastMA` crosses over `slowMA`.
        if ta.crossover(fastMA, slowMA)
            strategy.entry("buy", strategy.long)
        
        // Enter a short position when `fastMA` crosses under `slowMA`.
        if ta.crossunder(fastMA, slowMA)
            strategy.entry("sell", strategy.short)
        
        // Plot the moving averages.
        plot(fastMA, "Fast MA", color.aqua)
        plot(slowMA, "Slow MA", color.orange)
        • fastMA와 slowMA라는 두 개의 다른 길이의 이동 평균을 계산합니다.
        • fastMA가 slowMA를 상향 교차할 때 롱 포지션에 진입합니다. ( "strategy.entry("buy", strategy.long)" )
        • fastMA가 slowMA를 하향 교차할 때 숏 포지션에 진입합니다. ( "strategy.entry("sell", strateg.short)" )
        • 두 이동 평균을 다른 색으로 차트에 플롯 합니다.
      • strategy("test", overlay=true) : 스크립트가 전략임을 선언하며, 메인 차트 패널에 시각적 출력이 겹쳐집니다.
      • strategy.entry() : "buy" 및 "sell" 주문을 시뮬레이션하기 위해 사용되며, 주문을 플롯하고 방향을 나타내는 화살표를 차트에 표시합니다.
    •  

    차트에 strategy 적용하기

    • strategy 코드를 Pine Editor에 작성한 뒤 add to chart를 클릭하여 스크립트를 차트에 적용합니다.
    • strategy 코드가 컴파일되고 차트에 적용되면, order mark가 차트창에 표시되고, 시뮬레이션 성과 결과가 Stragey Tester 탭에 표시됩니다.

    Strategy tester(전략 테스터)

    • Strategy tester(전략 테스터)는 strategy() 함수로 선언된 모든 스크립트에서 사용할 수 있습니다.
    • 사용자는 자신의 전략을 시각화하고 가상 성능 결과를 분석할 수 있습니다.

    Overview

      • Net Profit(순익) : 순이익은 전략이나 투자로 인해 얻은 총수익에서 총손실을 뺀 금액입니다. 이는 투자 전략의 총적인 수익성을 평가하는데 중요한 지표입니다.
      • Total Closed Trades(청산된 트레이드 전체) : 포지션이 종료된 개수입니다.
      • Percent Profitable(승률) : 수익성 비율은 전체 거래 중 수익이 발생한 거래의 비율을 나타냅니다. 즉 전체 거래 중 얼마나 많은 거래가 이익을 가져다주었는지를 백분율로 표시합니다.
      • Profit Factor(수익 계수) : 수익 계수는 총수익을 총손실로 나눈 값입니다. 이 값이 1보다 크면 수익이 손실보다 많다는 것을 의미합니다.
      • Max Drawdown(최대 손실폭) : 최대 하락률은 투자 포트폴리오가 경험한 최대 하락 비율을 말합니다. 이는 전략의 위험도를 평가하는 데 사용됩니다.
      • Avg Trade(평균 거래) : 평균 거래는 모든 거래의 평균 수익을 나타냅니다. 이는 거래당 평균적으로 얼마나 이익이나 손실이 발생했는지 보여줍니다.
      • Avg # Bars in Trades(거래시 평균봉수) : 평균 거래 바 수는 거래가 개시되어 종료될 때까지 걸린 평균 바(시간 단위) 수를 나타냅니다. 이는 거래가 보통 얼마나 오래 지속되는지를 보여줍니다.

    Performance Summary(성과 요약)

      • Net Profit(순익) : 총 수익에서 총손실을 뺀 금액. 전체적인 수익성을 나타내는 중요한 지표.
      • Gross Profit(총수익) : 발생한 총 수익
      • Gross Loss(총손실) : 발생한 총 손실
      • Max Run up(최대 상승률) : 투자에서 경험한 최대 상승 비율(전략의 최적 성능을 나타내는 지표)
      • Max Drawdown(최대 하락률) : 포트폴리오가 경험한 최대 하락 비율(전략의 위험도를 나타내는 지표)
      • Buy & Hold Return(매수 후 보유 수익률) : 매수 후 보유 수익률은 초기 투자 금액 대비 최종 가치의 증가 비율을 나타냅니다. 이는 전략을 사용하지 않고 장기간 보유하는 경우의 수익률을 의미합니다.
      • Sharpe Ratio(샤프 비율): 샤프 비율은 초과 수익률(포트폴리오 수익률에서 무위험 수익률을 뺀 값)을 표준편차(위험)로 나눈 값입니다. 이는 위험 대비 수익률을 평가하는 데 사용됩니다.
      • Sortino Ratio(소르티노 비율) : 소르티노 비율은 초과 수익률을 하방 표준 편차(하락 위험)로 나눈 값입니다. 이는 하방 위험 대비 수익성을 측정하는 지표로, 샤프 비율과 비슷하지만 하락 위험에 더 중점을 둡니다.

    List of Trades

      • 각 거래에 대한 상세 정보를 제공합니다. 
      • 해당 거래 시점으로 차트를 이동하고 싶다면 해당 거래항목에서 바까지 스크롤을 클릭하면 해당 위치로 이동합니다.

    Properties(속성)


      • Date Range : 시뮬레이션된 거래가 포함된 날짜 범위와 사용가능한 총 백테스팅 범위
      • Symbol info : Symbol 이름과 차트의 기간 등 Symbol에 대한 정보
      • Strategy Inputs : 스크립트 설정의 "입력" 탭에서 사용할 수 있는 전략 스크립트에서 사용되는 다양한 매개변수와 변수
      • Strategy Properties : 초기 자본금, 주문 사이즈, 피라미딩, 수수료, 슬리피지 등 세부 정보

    Broker emulator

    • TradingView에서는 broker emulator를 활용하여 트레이딩 전략의 성과를 시뮬레이션합니다.
    • emulator는 차트 데이터만을 이용가능하기 때문에 하나의 막대의 가격 움직임에 대해 추정해야 합니다. 다음 로직을 따라 작동합니다.
    • 기본 작동 과정
      • 고가가 저가보다 시가에 가까울 경우, 하나의 bar에서 다음과 같은 순서로 움직인 것으로 가정합니다.
        • 시가 -> 고가 -> 저가 -> 종가
      • 저가가 고가보다 시가에 가까울 경우, 하나의 bar에서 다음과 같은 순서로 움직인 것으로 가정합니다.
        • 시가 -> 저가 -> 고가 -> 종가

    Bar magnifier(막대 돋보기)

    • Trading View의 프리미엄 계정은 strategy() 함수의 use_bar_magnifier 매개변수 또는 스크립트 설정의 속성 탭에 있는 "바 돋보기 사용"을 통해 broker emulator의 가정을 무시할 수 있습니다. (위에서 설명한 기본 작동 과정을 말합니다.)
      •  
      • strategy("bar_magnifier_demo", overlay = true, use_bar_magnifier = true)
    • 즉, Bar magnifier는 차트보다 짧은 기간의 데이터를 검사하여 막대 내의 가격 움직임에 대해 보다 세부적인 정보를 얻음으로써 시뮬레이션 중에 보다 정확한 주문 체결을 가능하게 합니다. (조금 더 실시간 매매에 가까운 백테스팅 진행 가능)
    • 예시를 통한 이해
      • entryPrice에 오면 Buy를 하고 exitPrice에 오면 Exit 하는 방식으로 백테스팅을 진행한다고 가정하겠습니다. 
      • Bar magnifier을 사용하지 않은 경우
        • buy 하는 시점의 막대를 살펴보면 고가가 저가보다 시가에 가깝기 때문에 시가 -> 고가 -> 저가 -> 종가 순서로 막대가 진행했다고 가정하고 백테스팅을 진행하게 됩니다.
        • 따라서 가격이 해당 막대에서 가격이 하락만 한 것으로 보이기 때문에 entryPrice에 도달했을 때 Buy만 진행하게 됩니다.
      • Bar magnifier을 사용한 경우
        • Bar magnifier을 사용하지 않았을 때와 달리 Buy 한 시점에 Exit도 같이 진행된 것을 확인할 수 있습니다. 이는 이제 Bar magnifier 기능을 이용하여 차트보다 짧은 기간의 데이터를 검사하여 막대 내의 가격 움직임을 더 정확하게 살펴봤기 때문에 가능한 것입니다. 즉, 하나의 막대 안에서의 움직임을 조금 더 짧은 기간의 데이터를 살펴봄으로써, Broker emulator의 기본 작동 과정이 아닌 실제 거래데이터에 보다 근접하게 백테스팅을 진행하게 됩니다.
    • 하나의 막대에 대해서 테스트를 진행하는 세부적인 timeframe의 기준은 다음과 같습니다.
        • 하나의 일봉에 대해서 60분 봉을 움직임을 살펴보고 보다 정확하게 백테스팅을 진행하는 것입니다.
        • 그렇지만 이제 intrabar에 대한 가격 움직임의 예측은 처음 설명했던 기본 가정을 따르게 됩니다.(고가가 저가보다 시가에 가까울 경우 시가->고가->저가-> 종가 / 저가가 고가보다 시가에 가까울 경우 시가->저가->고가->종가)

    Orders and entries

    • 기본 예시 코드
      •  
        • 20번째 바(bar)마다 true가 되는 longCondition을 정의한 뒤, longCondition일 경우 strategy.entry()로 진입 주문을 합니다. 사용자 정의 debugLabel() 함수로 진입 가격에 라벨을 그립니다.
        • 스크립트는 strategy.close_all()을 호출하여 열린 모든 포지션을 청산하는 시장 주문을 시뮬레이션합니다.
      • //@version=5 strategy("My strategy", overlay = true, margin_long = 100, margin_short = 100) //@function Displays text passed to `txt` when called. debugLabel(txt) => label.new( bar_index, high, text = txt, color=color.lime, style = label.style_label_lower_right, textcolor = color.black, size = size.large ) longCondition = bar_index % 20 == 0 // true on every 20th bar if (longCondition) debugLabel("Long entry order created") strategy.entry("My Long Entry Id", strategy.long) strategy.close_all()
        • 차트에서 파란 화살표는 진입 위치를, 보라색 화살표는 전략이 포지션을 청산한 지점을 나타냅니다.
        • 라벨은 실제 진입 지점보다 앞서 발생하며, 이는 주문이 작동하는 방식을 보여줍니다.
          • Pine Script에서 Strategy는 현실적인 거래를 모방하기 위해서 실시간으로 즉시 체결되지 않고, 다음 차트 바의 시작에서 체결됩니다.
        • 스크립트는 전역 범위에서 strategy.close_all()을 호출하여 모든 열린 포지션을 청산합니다. 하지만 열린 포지션이 없으면 아무런 작업도 수행하지 않습니다.
          • 따라서 진입 주문이 체결된 시점의 다음 바의 시작에서 포지션을 청산하는 주문이 체결되게 됩니다.(Pine Script에서는 실시간으로 즉시 체결되지 않고, 다음 바의 시작에서 체결되기 때문입니다.)

    Order types

    • Pine Script의 Strategy에서는 다양한 주문 유형을 시뮬레이션할 수 있습니다.
    • 주요 주문 유형
      • Market orders(시장가 주문)
      • Limit orders(지정가 주문)
      • Stop orders(스탑 주문)
      • Stop-limit orders(스탑-리밋 주문)

    Market orders(시장가 주문)

    • 설명
      • Market orders는 가장 기본적인 주문 유형입니다.
      • 가능한 한 빨리, 가격에 관계없이 증권을 매수하거나 매도하도록 지시합니다.
      • Market orders는 주문이 생성되면 다음 가격 틱에 실행되는 가장 기본적인 주문 유형입니다.
      • 모든 strategy.*() 함수는 기본적으로 Market orders 주문을 생성합니다.
    • 예시 코드
      • //@version=5
        strategy("Market order demo", overlay = true, margin_long = 100, margin_short = 100)
        
        //@variable Number of bars between long and short entries.
        cycleLength = input.int(10, "Cycle length")
        
        //@function Displays text passed to `txt` when called.
        debugLabel(txt, lblColor) => label.new(
             bar_index, high, text = txt, color = lblColor, textcolor = color.white,
             style = label.style_label_lower_right, size = size.large
         )
        
        //@variable Returns `true` every `2 * cycleLength` bars.
        longCondition = bar_index % (2 * cycleLength) == 0
        //@variable Returns `true` every `cycleLength` bars.
        shortCondition = bar_index % cycleLength == 0
        
        // Generate a long market order with a `color.green` label on `longCondition`.
        if longCondition
            debugLabel("Long market order created", color.green)
            strategy.entry("My Long Entry Id", strategy.long)
        // Otherwise, generate a short market order with a `color.red` label on `shortCondition`.
        else if shortCondition
            debugLabel("Short market order created", color.red)
            strategy.entry("My Short Entry Id", strategy.short)
        • bar_index 가 2*cycleLength로 나누어 떨이질 때마다 롱 주문을, cycleLength로 나누어 떨어질 때마다 숏주문을 생성합니다.
          • if longCondition이 True라면 else if shortCondition 구문이 실행되지 않기 때문에, longCondition은 2*cycleLength 주기만큼 shortCondition은 cycleLength주기만큼 진행하였습니다.
          • debugLabel이 생성된 이후 다음 바(bar)의 시작에서 주문이 체결되는 것을 확인할 수 있습니다.
          • strategy.entry 진입 주문밖에 없지만 기존의 포지션을 자동으로 종료하고 반대 포지션 주문이 들어가는 것을 확인할 수 있습니다.
        • 참고
          • 만약 strategy.entry에서 long의 경우 qty=2, short의 경우 qty=5으로 설정하면 다음과 같이 7개의 주문(2+5)이 계속해서 들어가게 됩니다.(long은 2개의 주문을 유지하기 위해 short 5개 주문을 종료하고 2개 주문을 추가로 넣기 위해 총 7개의 주문이 들어가고, short의 경우 5개의 주문을 유지하기 위해 long 2개 주문을 종료하고 5개 주문을 추가로 넣어주기 위해 7개의 주문이 들어가는 것입니다.)
            • 주문 시작에는 long에는 2개만큼만 진입했지만, 다음 거래부터는 모두 7개씩 진행되는 것을 확인할 수 있습니다.

    Limit orders(지정가 주문, 유리한 가격(눌림목매수용))

    • 설명
      • Limit orders는 특정 가격 또는 그보다 좋은 가격에서 포지션을 열도록 지시하는 주문입니다.(간단하게 말하면, 포지션 오픈하는 사람 입장에서 현재가보다 좋은 가격에 포지션 오픈하기 위한 장치) 
        • 즉, limit의 값은 롱(매수) 주문의 경우 현재가보다 낮아야 하고, 숏(매도) 주문의 경우에는 현재가보다 높아야 합니다.
          • 그래야 포지션 오픈하는 사람 입장에서는 이득인 가격이기 때문입니다.
        • 현재 시장 가격이 주문의 지정가 파라미터보다 좋을 때, 시장 가격이 지정가에 도달하기를 기다리지 않고 주문이 체결됩니다.(이때는 바로 주문이 체결됩니다. 즉, 시장가 주문)
          • 내가 생각한 이득가격을 limit으로 지정했는데 현재 시장가격이 그 가격보다 더 좋은 가격이라면 바로 체결하면 됩니다.
      • Limit orders도 마찬가지로 주문이 들어간 시점 봉에는 체결이 되지 않고 다음봉부터 해당 가격에 도달했는지 체크한 뒤에 체결이 이루어집니다.
    • 예시 코드
      • //@version=5
        strategy("Limit order demo", overlay = true, margin_long = 100, margin_short = 100)
        
        //@function Displays text passed to `txt` and a horizontal line at `price` when called.
        debugLabel(price, txt) =>
            label.new(
                 bar_index, price, text = txt, color = color.teal, textcolor = color.white,
                 style = label.style_label_lower_right, size = size.large
             )
            line.new(
                 bar_index, price, bar_index + 1, price, color = color.teal, extend = extend.right,
                 style = line.style_dashed
             )
        
        // Generate a long limit order with a label and line 100 bars before the `last_bar_index`.
        if last_bar_index - bar_index == 15
            limitPrice = close - syminfo.mintick * 800
            debugLabel(limitPrice, "Long Limit order created")
            strategy.entry("Long", strategy.long, limit = limitPrice)
        • 백테스팅 기간의 마지막 bar보다 10개 앞에 있는 bar에서 지정가 주문을 넣습니다

          • 주문이 들어가는 15개 전 봉의 종가는 43950.0이고 롱 진입시점의 가격은 43870.0입니다. 이는 이제 최소틱기준*800이기 때문에 80원 밑에 가격에 지정가 주문을 걸어놨는데 해당 가격에 도달한 봉에서 주문이 체결된 것입니다.
    • 예시 코드 2
      • //@version=5
        strategy("Limit order demo", overlay = true, margin_long = 100, margin_short = 100)
        
        //@function Displays text passed to `txt` and a horizontal line at `price` when called.
        debugLabel(price, txt) =>
            label.new(
                 bar_index, price, text = txt, color = color.teal, textcolor = color.white,
                 style = label.style_label_lower_right, size = size.large
             )
            line.new(
                 bar_index, price, bar_index + 1, price, color = color.teal, extend = extend.right,
                 style = line.style_dashed
             )
        
        // Generate a long limit order with a label and line 100 bars before the `last_bar_index`.
        if last_bar_index - bar_index == 15
            limitPrice = close - syminfo.mintick * 8000
            debugLabel(limitPrice, "Long Limit order created")
            strategy.entry("Long", strategy.long, limit = limitPrice)
        • entry() 주문은 해당 스크립트가 실행되고 다음 봉 시가부터 체결이 진행된다고 했습니다. 여기서 헷갈리면 안 될 것이 limit 주문이 해당하는 지점에 도달하면 그 도달한 시점 다음 봉의 시가부터 체결되는 것이 아닌, limit 주문이 해당하는 시점의 봉에서 바로 체결이 진행되게 됩니다. 
        • 스크립트는 해당봉이 완성된 후 실행되는 것이기 때문에 entry()나 exit()나 모든 주문은 다음 봉 시가부터 체결이 시작되는 것입니다.(스크립트가 실행될 때, 시가, 고가, 저가, 종가가 모두 형성되어 있어야 하기 때문에 다음 봉의 시가에서 체결됩니다.) 하지만 다음봉 시가부터는 이미 주문이 들어가서 대기 중인 entry()나 exit() 주문이라면 해당 가격이 도달한 봉에서 바로 체결이 진행되게 됩니다.

    Stop orders(지정가 주문, 불리한 가격(추격매수, 추격매도), 돌파매매 시)

    • 설명
      • Stop orders는 특정 가격에 도달하거나 그보다 나쁜 가격에서 포지션을 열도록 지시하는 주문입니다.(간단하게 말하면, 포지션 오픈하는 사람 입장에서는 추격으로 포지션 오픈하는 것이라고 생각하면 됩니다. ( 롱의 경우 가격이 내가 설정한 가격까지 상승하면, 계속해서 상승할 것이라고 예측하여 추격 매수를 위한 장치, 반대로 숏의 경우 가격이 내가 설정한 가격까지 하락하면, 계속해서 하락할 것이라고 예측하여 추격 매도를 위한 장치)
        • 이들은 본질적으로 지정가 주문의 반대입니다.
          • 즉, stop의 값은 롱(매수) 주문의 경우 현재가보다 높아야 하고, 숏(매도) 주문의 경우에는 현재가보다 낮아야 합니다.
            • 그래야 포지션 오픈하는 사람 입장에서는 추격으로 포지션을 오픈하는 가격이기 때문입니다.
        • 현재 시장 가격이 스탑 파라미터보다 나쁠 때, 전략은 현재 가격이 스탑 레벨에 도달하기를 기다리지 않고 주문이 체결됩니다. (이때는 바로 주문이 체결됩니다. 즉, 시장가 주문)
          • 내가 생각한 추격으로 포지션 오픈하는 시점(추격매수 or 추격매도)을 stop으로 지정했는데 현재 시장가격이 그거보다 더 나쁜 가격(나쁜 가격은 롱의 입장에서는 더 높은 가격을 의미, 숏의 입장에서는 더 낮은 가격을 의미)이라면 바로 체결하면 됩니다.
      • 정리하면 stop은 추격매수(숏의 경우 추격매도)를 위한 장치라고 생각하면 됩니다.
        • 반대로 limit은 눌림목매수(숏의 경우 눌림목매도)를 위한 장치라고 생각하면 됩니다.
          • (롱) 가격이 현재가보다 하락하면 매수하기 위한 장치
          • (숏) 가격이 현재가보다 상승하면 매도하기 위한 장치
      • 이상적인 stop과 limit 진입 시점 그림
        • 참고 : https://www.tradingview.com/chart/EURUSD/SR8NLZOI-STOP-AND-LIMIT-ORDERS-EXPLAINED/
        • stop의 경우, 계속 상승할 것을(숏의 경우 계속 하락할 것을) 예상하고 롱의 경우 추격매수, 숏의 경우 추격매도를 한 것입니다.
          • 하지만 stop의 경우, (롱의 경우) 해당 가격에서 가격이 하락한다면 높은 가격에 사기만 해서 망하는 꼴이 된 것입니다. (반대로 숏의 경우도 해당 가격에서 상승한다면 낮은 가격에서 사기만 해서 망하는 꼴입니다.)
        • limit의 경우, 해당 가격에서 다시 가격이 상승할 것으로(숏의 경우 해당 가격에서 다시 가격이 하락할 것으로) 예상하여 롱의 경우 해당가격에서 매수를 진행한 것이고, 숏의 경우는 매도를 진행한 것입니다.
          • 하지만 limit의 경우, (롱의 경우) 해당 가격에서 가격이 상승하지 않고 계속해서 하락한다면 꼼짝없이 손해를 보게 되는 것입니다. (반대로 숏의 경우 해당 가격에서 가격이 하락하지 않고 계속해서 상승한다면 꼼짝없이 손해를 보게 됩니다.)
    • stop 예시 코드
      • //@version=5
        strategy("Stop order demo", overlay = true, margin_long = 100, margin_short = 100)
        
        //@function Displays text passed to `txt` when called and shows the `price` level on the chart.
        debugLabel(price, txt) =>
            label.new(
                 bar_index, high, text = txt, color = color.teal, textcolor = color.white,
                 style = label.style_label_lower_right, size = size.large
             )
            line.new(bar_index, high, bar_index, price, style = line.style_dotted, color = color.teal)
            line.new(
                 bar_index, price, bar_index + 1, price, color = color.teal, extend = extend.right,
                 style = line.style_dashed
             )
        
        // Generate a long stop order with a label and lines 100 bars before the last bar.
        if last_bar_index - bar_index == 100
            stopPrice = close + syminfo.mintick * 8000
            debugLabel(stopPrice, "Long Stop order created")
            strategy.entry("Long", strategy.long, stop = stopPrice)
         

        • stop 주문이 들어간 시점의 종가는 42423.3입니다. 따라서 stopPrice는 42423.3 + 0.1*8000 = 43223.3입니다.
        • 해당 가격이 도달한 시점에 실제로 long이 진입되었습니다.(추격매수)

    Stop-Limit orders(지정가 매수, 돌파 후 눌림목 매수)

    • 설명
      • stop-limit orders는 시장 가격이 지정된 stop 가격을 넘어선 후에 지정된 limit 가격에서 주문을 배치하는 주문 유형입니다.
        • 이 주문은 시장 가격이 스톱 가격을 넘어서면 활성화되는데, 이때 거래는 리밋 가격에 도달했을 때만 실행됩니다.
        • 즉, 돌파후 눌림목 지점에서 포지션을 오픈하는 것입니다.
    • 예시 코드
      • //@version=5
        strategy("Stop-Limit order demo", overlay = true, margin_long = 100, margin_short = 100)
        
        //@function Displays text passed to `txt` when called and shows the `price` level on the chart.
        debugLabel(price, txt, lblColor, lineWidth = 1) =>
            label.new(
                 bar_index, high, text = txt, color = lblColor, textcolor = color.white,
                 style = label.style_label_lower_right, size = size.small
             )
            line.new(bar_index, close, bar_index, price, style = line.style_dotted, color = lblColor, width = lineWidth)
            line.new(
                 bar_index, price, bar_index + 1, price, color = lblColor, extend = extend.right,
                 style = line.style_dashed, width = lineWidth
             )
        
        var float stopPrice  = na
        var float limitPrice = na
        
        // Generate a long stop-limit order with a label and lines 100 bars before the last bar.
        if last_bar_index - bar_index == 100
            stopPrice  := close + syminfo.mintick * 10000
            limitPrice := close + syminfo.mintick * 8000
            debugLabel(limitPrice, "", color.gray)
            debugLabel(stopPrice, "Long Stop-Limit order created", color.teal)
            strategy.entry("Long", strategy.long, stop = stopPrice, limit = limitPrice)
        
        // Draw a line and label once the strategy activates the limit order.
        if high >= stopPrice
            debugLabel(limitPrice, "Limit order activated", color.green, 2)
            stopPrice := na
        • 100개 봉 전의 종가가 26359.6이기 때문에 stop 가격은 27359.6이 되고 limit 가격은 27159.6이 됩니다.
        • 따라서 27359.6이 넘은 시점에 limit 주문이 활성화가 되게 됩니다. 그 후에 가격이 하락하여 27159.6이 됐을 때 long 진입을 하게 되었습니다.

    Order placement commands

    strategy.entry()

    • 설명
      • 포지션 진입 주문을 시뮬레이션합니다.
      • 기본적으로 이 함수를 호출할 때 시장 주문으로 적용되지만, stop과 limit 파라미터를 사용하면 stop orders, limit orders, stop-limit orders 주문도 생성이 가능합니다.
      • strategy.entry()는 기본적으로 이전에 열린 반대 포지션을 청산하고 주문한 양을 맞춰 주문이 진행됩니다.
        • 만약 기존에 15개의 숏포지션을 가지고 있을 경우, strategy.entry()를 통해 롱포지션 진입을 할 시에 기존 롱포지션 진입 수량에 15개를 더하여 주문이 진행되게 됩니다.
        • 예를 들어 롱 포지션 5개를 진행하고 싶은데, 기존에 15개의 숏포지션이 있다면 20개의 롱포지션으로 체결이 되게 됩니다.
        • 따라서 단순히 이분적인 매매에서 효율적인 방법입니다. 
          • 예를 들어 현재 가격이 60 이평선보다 위에 있다면 롱, 아래 있다면 숏 이런 식으로 바로 포지션 스위칭이 필요한 경우에 유용합니다.
      • 동일한 코드로 예약 주문을 넣어놨는데, 만약 다음 봉에 가서 그 예약 주문이 체결되지 않았다면 그 동일한 코드는 다시 덮어져서 다시 예약 주문이 걸리게 됩니다.
        • 아래 코드에서 주문 내역이 0개라면 strategy.entry("Long", strategy.long, stop=high) 코드가 계속해서 실행됩니다. 하지만 새롭게 주문이 추가 되는 것이 아닌 기존의 코드를 취소하고 새로운 주문이 들어가는 것입니다. 만약 계속 주문이 추가되는 것이였다면, 10342.7 고점이 되어서 체결되었을때, 앞에서 high에 주문을 넣었던 최소 2개의 주문이 동시에 체결되었어야 할것입니다.
          • order의 경우도 마찬가지입니다. 
        • //@version=5
          strategy("OCA Cancel Demo", overlay=true,pyramiding = 2)
          
          if strategy.position_size == 0
              strategy.order("Long",  strategy.long, stop = high)
          else
              strategy.close_all()
    • 예시 코드
      • //@version=5
        strategy("Entry demo", "test", overlay = true)
        
        //@variable Is `true` on every 100th bar.
        buyCondition = bar_index % 100 == 0
        //@variable Is `true` on every 25th bar except for those that are divisible by 100.
        sellCondition = bar_index % 25 == 0 and not buyCondition
        
        if buyCondition
            strategy.entry("buy", strategy.long, qty = 15)
        if sellCondition
            strategy.entry("sell", strategy.short, qty = 5)
        
        bgcolor(buyCondition  ? color.new(color.blue, 90) : na)
        bgcolor(sellCondition ? color.new(color.red, 90) : na)
        • 제일 처음 주문에는 short 포지션이 없기 때문에 15개의 수량만 진입합니다. 하지만 다음 short 포지션 5개 주문을 위해서는 long 포지션 15개를 종료해야 하기 때문에 총 20개의 수량만큼 short 포지션 진입이 됩니다.
        • 또한 코드대로라면 sellCondition이 buyCondition 전에 3번 발생해야 하지만, 실제 백테스팅 결과 한 번만 sell이 진행된 것을 확인할 수 있습니다.
          • 이는 strategy의 pyramiding이 기본값인 1로 설정되어 있기 때문입니다. 피라미딩은 전략이 같은 방향으로 연속적으로 체결할 수 있는 주문의 수를 지정합니다. 
          • pyramiding이 1이면 전략이 어느 방향이든 연속적으로 한 번의 주문만을 체결할 수 있습니다.
          • 만약 strategy에서 pyramiding=3을 추가하면 전략은 같은 방향으로 최대 3번의 연속 거래를 허용하므로 원래 코드 의도대로 숏을 3번 오픈하게 됩니다.
      • strategy("Entry demo", "test", overlay = true, pyramiding = 3)
        • pyramiding = 3을 추가
        • 코드 의도대로 정상 작동하는 것을 확인할 수 있습니다.

    strategy.order()

    • 설명
      • strategy.order()은 지정된 파라미터 그대로 사용하는 주문입니다. pyramiding이나 반대포지션 개수 등을 고려하지 않아도 됩니다. 말 그대로 order안의 파라미터가 우선적으로 중요한 주문입니다.
    • 예시 코드
      • //@version=5
        strategy("Order demo", "test", overlay = true)
        
        buyCond = bar_index % 100 == 0
        sellCond = bar_index % 25 == 0 and not buyCond
        
        if buyCond
            strategy.order("buy", strategy.long, qty = 15) // 롱 포지션 15개 단위 진입.
        if sellCond
            strategy.order("sell", strategy.short, qty = 5) // 롱 포지션에서 5개 단위 청산.
        
        bgcolor(buyCond  ? color.new(color.blue, 90) : na)
        bgcolor(sellCond ? color.new(color.red, 90) : na)
        • pyramding을 3으로 설정하지 않았지만 숏포지션 3개가 모두 오픈된 것을 확인할 수 있습니다.
        • 또한 short 포지션으로 바꾸기 위해 20개(15+5)를 주문을 넣는 것이 아닌, 그냥 말 그대로 단순히 qty양 5개 만을 short주문을 넣게 됩니다. 

    strategy.exit()

    • 설명
      • strategy.exit()은 포지션을 청산하는 주문을 시뮬레이션합니다. 
      • strategy.exit()는 손절매(stop-loss), 이익실현(take-profit), 트레일링 스탑(trailing stop) 주문과 같이 여러 형태의 청산 명령을 loss, profit, stop, limit, trail_* 파라미터를 통해 설정이 가능합니다.
      • strategy.exit()의 가장 기본적인 사용법은 전략이 너무 많은 손실을 보거나 충분한 이익을 얻었을 때 포지션을 청산하는 것입니다.
        • 손절매와 이익실현 기능은 (loss/profit 또는 stop/limit) 파라미터로 가능합니다.
        • loss와 profit 파라미터는 진입 주문 가격으로부터 정해진 틱 수만큼 떨어진 가치로 손절매와 이익실현 값을 지정합니다. (틱 수 기준)
        • stop과 limit 파라미터는 구체적인 손절매와 이익실현 가격을 제공합니다. (가격 기준)
        • 만약 가격 기준 파라미터(stop/limit)와 틱수 기준 파라미터(loss/profit)이 같이 strategy.exit() 호출에 같이 포함되어 있다면 명령은 가격 기준 파라미터(stop/limit)을 우선하고 틱수 기준 파라미터(loss/profit)를 무시합니다.
      • strategy.exit()의 limit, stop 파라미터는 strategy.entry() 및 strategy.order()의 limit, stop 파라미터와 이름은 같지만, 다르게 작동합니다.
        • strategy.entry()와 strategy.order()의 경우 위의 설명 참고(limit / stop / stop-limit)
        • strategy.exit()의 경우 열린 포지션에서 청산하기 위한 손절가(stop)와 익절가(limit)를 의미합니다.
      • strategy.exit()에 첫 번째 입력인자로 id 입력인자가 있는데 이는 exit() 오더 아이디를 의미합니다. 이 아이디를 통해 exit() 주문을 취소하거나 수정할 수 있습니다.
      • strategy.exit()에는 두 번째 입력인자로 from_entry가 있습니다. 이는 exit()하고자 하는 특정 엔트리 오더 아이디를 의미합니다. 만약 모든 엔트리를 엑시트 하고 싶으면 빈 string을 사용하면 됩니다. (디폴트 값은 빈 string입니다.)
      • strategy.exit()를 사용하면 같은 진입 ID에 대해 한 번 이상 청산할 수 있어 다중 레벨 청산 전략을 구성할 수 있습니다. 여러 청산 명령을 수행할 때, 각 주문의 수량은 거래된 수량의 일부여야 하며, 그 합은 열린 포지션을 초과해서는 안 됩니다. 함수의 qty가 현재 시장 포지션 크기보다 작으면 전략은 부분적인 청산을 시뮬레이션합니다. qty 값이 열린 포지션 수량을 초과하면 주문을 줄입니다.
      • strategy.exit()는 strategy.entry() 코드와 같이 다음봉부터 적용됩니다. 다음 봉 시가에서 entry 주문이 들어갈 것이고, 그다음에 exit 주문을 체크하게 됩니다. 따라서 같은 봉에서 entry와 exit가 동시에 진행될 수 있습니다.
      • exit() 주문이나 entry()/order() 주문 모두 해당 스크립트가 실행되고 다음 봉 시가부터 체결이 진행됩니다. 단, 중요한 점은 이미 주문이 들어가서 대기중인 상태에서 해당 가격에 도달한 경우라면 그 도달한 시점기준 다음 봉 시가에서 체결이 진행되는 것이 아닌 해당 가격의 봉에서 해당 가격으로 바로 체결이 진행되어 백테스팅이 진행되게 됩니다.
      • strategy.exit() 명령어를 사용하여 생성된 주문은 오픈된 포지션 일부를 종료하기 위해 예약됩니다. 즉, 한번 exit()로 지정된 수량은 다른 exit 명령어에 의해서 포지션 종료 예약을 할 수 없습니다.
        • 예를 들어, 20주에 대한 buy 시장 주문을 체결한 뒤, 각각 19주와 20주에 대한 limit과 stop exit 주문을 하였습니다. 이때 stop 가격에 먼저 도달하여서 체결된 수량을 확인하니 1주밖에 거래되지 않았습니다. 이는 스크립트가 limit 주문을 먼저 배치했기 때문에 먼저 19주 수량을 limit exit를 예약한 것이고, stop 주문은 20주를 exit 예약했지만, 실질적으로 남은 양은 1주였기 때문에 1주만 stop exit 예약이 된 것입니다. (정확한 코드는 예시 코드 4 참고)
      • strategy.exit()의 또 다른 주요 기능은 트레일링 스탑(trailing stops)을 생성할 수 있습니다. 
        • 트레일링 스탑은 시장 가격이 유리한 방향으로 움직일 때 고점 대비 특정 금액만큼 하락할 때 판매하는 스탑로스 주문입니다. 이는 계속해서 상승하는 장에서 유리한 방법입니다. 
        • activation level과 trail offset이 중요한 구성 요소입니다.
          • activation level은 시장 가격이 트레일링 스탑 계산을 활성화하기 위해 넘어야 하는 값으로, trail_points를 통해 틱 수로 표현하거나 train_price를 통해 가격 값으로 표현됩니다. (exit 호출에 두 인자가 모두 있다면 trail_price 인자가 우선됩니다.)
          • trail offset은 고점대비 스탑로스로, trail_offset을 통해 틱으로 표현됩니다. 
        • 예시 코드 5를 참고하면 조금 더 쉽게 이해할 수 있습니다.
    • 예시 코드 1 (exit() id 지정)
      • //@version=5
        strategy("Exit demo", "test", overlay = true)
        
        //@variable Is `true` on every 100th bar.
        buyCondition = bar_index % 100 == 0
        
        //@variable Stop-loss price for exit commands.
        var float stopLoss   = na
        //@variable Take-profit price for exit commands.
        var float takeProfit = na
        
        // Place orders upon `buyCondition`.
        if buyCondition
            if strategy.position_size == 0.0
                stopLoss   := close * 0.99
                takeProfit := close * 1.01
            strategy.entry("buy", strategy.long)
            strategy.exit("exit1", "buy1", stop = stopLoss, limit = takeProfit) // Does nothing. "buy1" order doesn't exist.
            strategy.exit("exit2", "buy", stop = stopLoss, limit = takeProfit)
        
        // Set `stopLoss` and `takeProfit` to `na` when price touches either, i.e., when the strategy simulates an exit.
        if low <= stopLoss or high >= takeProfit
            stopLoss   := na
            takeProfit := na
        
        plot(stopLoss, "SL", color.red, style = plot.style_circles)
        plot(takeProfit, "TP", color.green, style = plot.style_circles)
        • 100개의 바마다 strategy.entry()를 통해 buy 진입 주문을 배치하고, strategy.exit() 명령을 통해 stop-loss 또는 take-profit 주문을 배치합니다. 
        • 여기서 strategy.exit()가 두 번 호출되는데 exit1 명령은 buy1 진입 주문을 참조하고, exit2 명령은 buy 주문을 참조합니다. exit1은 존재하지 않는 주문 id인 buy1을 참조하기 때문에 exit2에서만 청산 주문을 시뮬레이션합니다.
    • 예시 코드 2 (from_entry가 없는 경우)
      • //@version=5
        strategy("Exit all demo", "test", overlay = true, pyramiding = 2)
        
        //@variable Is `true` on every 100th bar.
        buyCondition = bar_index % 100 == 0
        
        //@variable Stop-loss price for exit commands.
        var float stopLoss   = na
        //@variable Take-profit price for exit commands.
        var float takeProfit = na
        
        // Place orders upon `buyCondition`.
        if buyCondition
            if strategy.position_size == 0.0
                stopLoss   := close * 0.99
                takeProfit := close * 1.01
            strategy.entry("buy1", strategy.long)
            strategy.entry("buy2", strategy.long)
            strategy.exit("exit", stop = stopLoss, limit = takeProfit) // Places orders to exit all open entries.
        
        // Set `stopLoss` and `takeProfit` to `na` when price touches either, i.e., when the strategy simulates an exit.
        if low <= stopLoss or high >= takeProfit
            stopLoss   := na
            takeProfit := na
        
        plot(stopLoss, "SL", color.red, style = plot.style_circles)
        plot(takeProfit, "TP", color.green, style = plot.style_circles)
        • exit에서 from_entry 인자에 아무것도 제공하지 않으면 열린 모든 진입에 대해 청산 주문을 생성합니다.
        • 따라서 stopLoss 나 takeProfit 값에 도달하면 전략이 buy1 및 buy2 진입 모두에 대해 청산 주문을 체결합니다.
    • 예시 코드 3 (분할 exit())
      • //@version=5
        strategy("Multiple exit demo", "test", overlay = true)
        
        //@variable Is `true` on every 100th bar.
        buyCondition = bar_index % 100 == 0
        
        //@variable Stop-loss price for "exit1" commands.
        var float stopLoss1 = na
        //@variable Stop-loss price for "exit2" commands.
        var float stopLoss2 = na
        //@variable Take-profit price for "exit1" commands.
        var float takeProfit1 = na
        //@variable Take-profit price for "exit2" commands.
        var float takeProfit2 = na
        
        // Place orders upon `buyCondition`.
        if buyCondition
            if strategy.position_size == 0.0
                stopLoss1   := close * 0.99
                stopLoss2   := close * 0.98
                takeProfit1 := close * 1.01
                takeProfit2 := close * 1.02
            strategy.entry("buy", strategy.long, qty = 2)
            strategy.exit("exit1", "buy", stop = stopLoss1, limit = takeProfit1, qty = 1)
            strategy.exit("exit2", "buy", stop = stopLoss2, limit = takeProfit2, qty = 3)
        
        // Set `stopLoss1` and `takeProfit1` to `na` when price touches either.
        if low <= stopLoss1 or high >= takeProfit1
            stopLoss1   := na
            takeProfit1 := na
        // Set `stopLoss2` and `takeProfit2` to `na` when price touches either.
        if low <= stopLoss2 or high >= takeProfit2
            stopLoss2   := na
            takeProfit2 := na
        
        plot(stopLoss1, "SL1", color.red, style = plot.style_circles)
        plot(stopLoss2, "SL2", color.red, style = plot.style_circles)
        plot(takeProfit1, "TP1", color.green, style = plot.style_circles)
        plot(takeProfit2, "TP2", color.green, style = plot.style_circles)
        • exit 주문을 exit1, exit2 두 개로 다른 stop, limit 가격을 넣어주어서 분할 매도를 진행하게 됩니다.
        • 총 buy 수량은 2개이지만 exit1은 1개 종료, exit2는 3개 종료 이기 때문에 1+3은 2보다 초과하게 되어서 exit2는 3개 종료 중에서 1개 종료만 체결되게 됩니다.(qty 값이 열린 포지션 수량을 초과하면 주문을 줄입니다.)
    • 예시 코드 4 (exit() 할당 수량)
      • //@version=5
        strategy("Reserved exit demo", "test", overlay = true)
        
        //@variable "stop" exit order price.
        var float stop   = na
        //@variable "limit" exit order price
        var float limit  = na
        //@variable Is `true` 100 bars before the `last_bar_index`.
        longCondition = last_bar_index - bar_index == 100
        
        if longCondition
            stop  := close * 0.99
            limit := close * 1.01
            strategy.entry("buy", strategy.long, 20)
            strategy.exit("limit", limit = limit,  qty = 19)
            strategy.exit("stop", stop = stop, qty = 20)
        
        bool showPlot = strategy.position_size != 0
        plot(showPlot ? stop : na, "Stop", color.red, 2, plot.style_linebr)
        plot(showPlot ? limit : na, "Limit 1", color.green, 2, plot.style_linebr)
      • 스크립트에서 limit exit가 먼저 entry 한 20주 중에서 19주를 차지하였기 때문에 stop exit는 20주를 exit 주문을 걸었지만 남은 1주에 대해서만 stop exit를 진행할 수밖에 없습니다. (즉, exit로 먼저 할당한 순서가 중요합니다.)
    • 예시 코드 5 (트레일링 스탑(trailing stop))
      • //@version=5
        strategy("Trailing stop order demo", overlay = true, margin_long = 100, margin_short = 100)
        
        //@variable Offset used to determine how far above the entry price (in ticks) the activation level will be located.
        activationLevelOffset = input(10000, "Activation Level Offset (in ticks)")
        //@variable Offset used to determine how far below the high price (in ticks) the trailing stop will trail the chart.
        trailingStopOffset = input(5000, "Trailing Stop Offset (in ticks)")
        
        //@function Displays text passed to `txt` when called and shows the `price` level on the chart.
        debugLabel(price, txt, lblColor, hasLine = false) =>
            label.new(
                 bar_index, price, text = txt, color = lblColor, textcolor = color.white,
                 style = label.style_label_lower_right, size = size.small
             )
            if hasLine
                line.new(
                     bar_index, price, bar_index + 1, price, color = lblColor, extend = extend.right,
                     style = line.style_dashed
                 )
        
        //@variable The price at which the trailing stop activation level is located.
        var float trailPriceActivationLevel = na
        //@variable The price at which the trailing stop itself is located.
        var float trailingStop = na
        //@variable Caclulates the value that Trailing Stop would have if it were active at the moment.
        theoreticalStopPrice = high - trailingStopOffset * syminfo.mintick
        
        // Generate a long market order to enter 100 bars before the last bar.
        if last_bar_index - bar_index == 400
            strategy.entry("Long", strategy.long)
        
        // Generate a trailing stop 99 bars before the last bar.
        if last_bar_index - bar_index == 400
            trailPriceActivationLevel := open + syminfo.mintick * activationLevelOffset
            strategy.exit(
                 "Trailing Stop", from_entry = "Long", trail_price = trailPriceActivationLevel,
                 trail_offset = trailingStopOffset
             )
            debugLabel(trailPriceActivationLevel, "Trailing Stop Activation Level", color.green, true)
        
        // Visualize the trailing stop mechanic in action.
        // If there is an open trade, check whether the Activation Level has been achieved.
        // If it has been achieved, track the trailing stop by assigning its value to a variable.
        if na(trailingStop) and high > trailPriceActivationLevel
            debugLabel(trailPriceActivationLevel, "Activation level crossed", color.green)
            trailingStop := theoreticalStopPrice
            debugLabel(trailingStop, "Trailing Stop Activated", color.blue)
        
        else if theoreticalStopPrice > trailingStop
            trailingStop := theoreticalStopPrice
        
        // Visualize the movement of the trailing stop.
        plot(trailingStop, "Trailing Stop")
         
        • long을 진입한 시점(42742.6)부터 10000 틱 위 인 43742.6 가격에 도달하면 트레일링 스탑 주문이 트리거 됩니다.(녹색선을 넘은 시점부터 트레일링 스탑 주문이 트리거 되었습니다. 파란색은 고점대비 5000 틱 아래 가격을 나타냅니다.)
        • 쭉 따라가다가 고점보다 5000 틱 낮은 파란색선에 도달한 봉에서 트레일링스탑이 체결되게 됩니다. 여기서 체결된 지점을 살펴보면 고점인 45485.7보다 5000틱 낮은 44985.7에 도달하게 되어 트레일링 스탑이 진행되었습니다.
        • 근데 여기서 한 가지 살펴봐야 할 점이 파란 색선에 도달한 적이 있던 적이 해당 트레일링 스탑이 체결된 시점보다 앞서 한번 존재하였습니다. 근데 왜 여기서는 트레일링 스탑이 진행되지 않았을까요? 
          • 이것은 Broker emulator의 기본 작동 과정 때문입니다. 
          • 장대 양봉의 경우 기본 작동 과정을 생각해 보면 저점이 고점보다 시가에 가깝기 때문에 시가 -> 저가 -> 고가 -> 종가 순서대로 가격이 이동했다고 가정합니다. 따라서 이 경우 저가에 먼저 도달한 다음에 고가에 도달한 것입니다. 고가가 상승하면서 트레일링스탑 가격도 같이 상승하게 되었고, 자연스럽게 먼저 도달했던 저가보다 트레일링 스탑 가격이 높이지게 된 것입니다. 그렇기 때문에 이경우는 체결이 됐다고 생각하지 않는 것입니다.
          • 즉, Broker emulator의 기본 작동 과정을 따르면 장대양봉의 경우 해당 바의 저점이 trailing stop 가격에 도달했어도 체결되지 않는 경우가 대부분이지만, 장대음봉의 경우 해당 바의 저점이 trailing stop 가격에 도달한 경우라면 무조건 체결됩니다.
            • 따라서 보다 정확한 백테스팅을 위해서는 Bar magnifier가 필요한 것입니다. 단순히 기본 작동 과정을 따르면 이러한 오류가 발생할 수 있습니다.

    strategy.close() and strategy.close_all()

    • 설명
      • strategy.close()와 strategy.close_all() 명령은 Market order(시장가 주문)을 사용하여 포지션을 청산하는 것을 시뮬레이션합니다.
      • 이 함수들은 특정 가격에서가 아니라 호출될 때 즉시 거래를 청산합니다.
      • exit()와 마찬가지로 id 파라미터가 필요합니다. 동일한 ID를 가진 여러 개의 entry가 존재할 경우 해당 id를 가지는 entry들을 한 번에 종료시켜줍니다.
      • 다양한 id를 가진 entry들이 있을 경우 strategy.close_all() 명령어를 사용하면 Id에 관계없이 모든 entry를 종료시켜 줍니다.
    • 예시 코드 1
      • //@version=5
        strategy("Close demo", "test", overlay = true)
        
        //@variable Is `true` on every 50th bar.
        buyCond = bar_index % 50 == 0
        //@variable Is `true` on every 25th bar except for those that are divisible by 50.
        sellCond = bar_index % 25 == 0 and not buyCond
        
        if buyCond
            strategy.entry("buy", strategy.long)
        if sellCond
            strategy.close("buy")
        
        bgcolor(buyCond  ? color.new(color.blue, 90) : na)
        bgcolor(sellCond ? color.new(color.red, 90) : na)
    • 예시 코드 2
      • //@version=5
        strategy("Multiple close demo", "test", overlay = true, pyramiding = 3)
        
        sellCond = bar_index % 100 == 0
        buyCond = bar_index % 25 == 0 and not sellCond
        
        if buyCond
            strategy.entry("buy", strategy.long)
        if sellCond
            strategy.close("buy")
        
        bgcolor(buyCond  ? color.new(color.blue, 90) : na)
        bgcolor(sellCond ? color.new(color.red, 90) : na)
        • 같은 ID를 가지는 entry 주문이 한 번에 종료되는 것을 확인할 수 있습니다.
    • 예시 코드 3
      • //@version=5
        strategy("Close multiple ID demo", "test", overlay = true, pyramiding = 3)
        
        switch strategy.opentrades
            0 => strategy.entry("A", strategy.long)
            1 => strategy.entry("B", strategy.long)
            2 => strategy.entry("C", strategy.long)
            3 => strategy.close_all()

    strategy.cancel() and strategy.cancel_all()

    • 설명
      • strategy.cancel()과 strategy.cancel_all() 명령은 전략에서 대기 중인 주문들, 즉 strategy.exit()이나 limit/stop 인자를 사용하는 strategy.order()/strategy.entry()에 의해 생성된 주문들을 취소하는 데 사용됩니다.
      • strategy.cancel()의 경우도 ID를 작성해주어야 합니다. 지정된 ID가 존재하지 않으면 명령은 주문을 실행하지 않습니다.
      • 동일한 ID로 여러 대기 주문이 있는 경우, 이 명령은 한 번에 모든 주문을 취소합니다.
      • strategy.cancel_all의 경우, ID를 지정하지 않아도 모든 주문들이 취소되게 됩니다.
    • 예시 코드 1
      • //@version=5
        strategy("Multiple cancel demo", "test", overlay = true, pyramiding = 3)
        
        var line limitLine = na
        color bgColor = na
        
        if last_bar_index - bar_index <= 100 and last_bar_index - bar_index >= 98
            float limitPrice = close - syminfo.mintick * 500
            strategy.entry("buy", strategy.long, limit = limitPrice)
            limitLine := line.new(bar_index, limitPrice, bar_index + 1, limitPrice, extend = extend.right)
            bgColor := color.new(color.green, 50)
        
        if last_bar_index - bar_index == 97
            strategy.cancel("buy")
            bgColor := color.new(color.orange, 50)
        
        bgcolor(bgColor)
         

        • 3개의 limit entry 주문이 하나도 진행되지 않았습니다. 모두 buy라는 ID를 가지기 때문에 한 번의 cancel로 모든 주문이 취소되게 되었습니다.

    Position sizing

    • Pine Script Strategy 에서는 거래의 크기를 제어하는 방법에는 두가지가 존재합니다.
      • strategy() 함수의 "default_qty_type" 및 "default_qty_value" 인자를 사용하여 모든 주문에 대한 기본 고정 수량 유형 및 값을 설정합니다. 이는 스크립트 설정의 Properties(속성) 탭에서도 설정 가능합니다.
      • strategy.entry()를 호출할 때, "qty" 인자를 지정합니다. 이 인자를 함수에 제공하면 스크립트는 전략의 기본 수량 값 및 유형을 무시합니다.(즉, 위에서 설정한 default_qty_value를 무시하게 됩니다.)
    • 예시 코드 1
      • //@version=5
        strategy("Buy low, sell high", overlay = true, default_qty_type = strategy.cash, default_qty_value = 5000)
        
        int   length      = input.int(20, "Length")
        float longAmount  = input.float(4.0, "Long Amount")
        float shortAmount = input.float(2.0, "Short Amount")
        
        float highest = ta.highest(length)
        float lowest  = ta.lowest(length)
        
        switch
            low == lowest   => strategy.entry("Buy", strategy.long, longAmount)
            high == highest => strategy.entry("Sell", strategy.short, shortAmount)
        • 이 예시에서는 strategy.entry 에서 qty 값을 지정해주었기 때문에 제일 처음 strategy() 에서 지정한 default_qty_type 및 default_qty_value 값이 무시되었습니다.
    • 예시 코드 2
      • //@version=5
        strategy("Buy low, sell high", overlay = true, default_qty_type = strategy.cash, default_qty_value = 5000)
        
        int length = input.int(20, "Length")
        
        float highest = ta.highest(length)
        float lowest  = ta.lowest(length)
        
        switch
            low == lowest   => strategy.entry("Buy", strategy.long)
            high == highest => strategy.entry("Sell", strategy.short)

        • 이 예시에서는 strategy.entry에서 qty값이 지정되지 않았기 때문에 strategy()에서 지정한 5000usdt씩 진입이 되고 있습니다.
        • 43711.0 * (0.227585/2) = 약 5000usdt 입니다.

    Closing a market position

    • 설명
      • 특정 진입 주문의 ID를 지정하면 해당 포지션을 종료하는 것이 가능합니다. 하지만 기본적으로 list of trades(거래목록)탭에 표시된 모든 주문은 FIFO(선입선출) 규칙에 따라 연결됩니다. 따라서 ID 기본적으로 먼저 entry 된 주문을 종료하게 되어 있습니다.
      • 즉, 만약에 Buy1 entry -> Buy2 entry 순서로 entry 되었을때, Buy2 entry를 종료하기 위해 close 나 exit를 사용할 경우, Buy2 entry 수량만큼 포지션이 종료되는데 먼저 들어간 Buy1 entry가 다 종료되어서야 Buy2 entry가 종료되게 됩니다. 
        • 예를 들어 Buy1 entry가 30개, Buy2 entry가 20개가 있다고 할때, Buy2 entry 종료 코드를 작성하면 Buy2 entry 수량인 20개 만큼 Buy1 entry가 먼저 종료되게 됩니다. Buy2 entry는 하나도 종료되지 않게 됩니다.(이때 종료되는 가격은, Buy2 entry 종료 조건 가격에서 Buy1 entry 가 종료되게 됩니다. 따라서 정확한 백테스팅이 진행되지 않을 수가 있습니다.)  다음으로 또 Buy2 entry가 종료되면 그때서야 Buy1 entry 남은 10개 수량과 Buy2 entry 10개가 종료되게 됩니다( 10+10=20 )
      • 이러한 것을 방지하기 위해서는 strategy에 close_entries_rule = "ANY"를 넣어주면 올바른 방법으로 포지션이 종료되게 됩니다.
        • 이 방법을 적용해야지 분할 매수, 분할 매도 방법을 적용할때 올바르게 작동할 수 있습니다.
    • 예시 코드 1(strategy.close())
      • //@version=5
        strategy("Exit Demo", pyramiding = 2)
        
        float positionSize = strategy.position_size
        
        if positionSize == 0 and last_bar_index - bar_index <= 100
            strategy.entry("Buy1", strategy.long, 5)
        else if positionSize == 5
            strategy.entry("Buy2", strategy.long, 10)
        else if positionSize == 15
            strategy.close("Buy2")
            strategy.exit("bracket", "Buy1", loss = 10000, profit = 10000)

        • 코드상으로 close 주문은 Buy2에게 적용되어서 Buy2 주문 entry 수량인 10개가 close 되어야합니다. 하지만 결과를 보게되면 19:00에 Buy1 5개와 Buy2 5개가 close 되었습니다. 이는 이제 Broker Emulator가 FIFO 규칙을 따르기 때문에 Buy1의 수량을 먼저 청산하고 Buy2 포지션의 절반을 청산합니다. 사용자는 strategy() 함수에서 close_entries_rule="ANY" 를 지정하여 이 행동을 변경할 수 있습니다.
        • exit의 경우는 loss와 profit 가격에 도달하지 않았기 때문에 계속 주문 대기 상태입니다.
      • Buy2에 대해서 close 주문을 내렸지만 Buy2에 대해서 하나도 체결이 되지 않을 수도 있습니다.
        • //@version=5
          strategy("Exit Demo", pyramiding = 2)
          
          float positionSize = strategy.position_size
          
          if positionSize == 0 and last_bar_index - bar_index <= 100
              strategy.entry("Buy1", strategy.long, 15)
          else if positionSize == 15
              strategy.entry("Buy2", strategy.long, 10)
          else if positionSize == 25
              strategy.close("Buy2")

          • Buy2에 대해서 종료하라고 했는데 먼저 entry 된 Buy1의 수량이 모두 종료되지 않았기 때문에 Buy1의 주문을 Buy2의 주문만큼 먼저 종료를 시킵니다.
            • 즉 Buy1의 15개의 주문중 Buy2 가 entry 했던 수량이 10개이기때문에 10개가 먼저 종료됩니다.
            • 그 다음 종료 코드에서 Buy1의 5개가 종료되고 같이 Buy2의 5개가 종료되게 됩니다.
          • 이처럼 모든 주문은 FIFO 규칙을 따라 연결되기때문에 close나 exit할때 주의가 필요합니다. 원하는대로 구현하기 위해서는 strategy에 close_entries_rule = "ANY"를 붙여주는것이 좋습니다.
    • 예시 코드 2 (strategy.close() / close_entries_rule="ANY"로 설정한 경우)
      • //@version=5
        strategy("Exit Demo", pyramiding = 2, close_entries_rule = "ANY")
        
        float positionSize = strategy.position_size
        
        if positionSize == 0 and last_bar_index - bar_index <= 100
            strategy.entry("Buy1", strategy.long, 5)
        else if positionSize == 5
            strategy.entry("Buy2", strategy.long, 10)
        else if positionSize == 15
            strategy.close("Buy2")
            strategy.exit("bracket", "Buy1", loss = 10000, profit = 10000)
        • 코드가 정상적으로 Buy2 entry에 대해서 10개의 주문이 종료되는 것을 확인할 수 있습니다.
    • 예시 코드 3 (strategy.exit())
      • //@version=5
        strategy("Exit Demo", pyramiding = 2)
        
        float positionSize = strategy.position_size
        
        if positionSize == 0 and last_bar_index - bar_index <= 100
            strategy.entry("Buy1", strategy.long, 5)
        else if positionSize == 5
            strategy.entry("Buy2", strategy.long, 10)
        else if positionSize == 15
            strategy.exit('exit',"Buy2",stop=close)
        • exit의 경우도 Buy2를 지정했지만, Buy1부터 종료되는 것을 확인할 수 있습니다.
        • 마찬가지로 close_entries_rule="ANY"를 지정하면 Buy1은 청산되지 않고 Buy2 10개가 정상적으로 종료되게 됩니다.
    • 예시 코드 4 (전체 exit())
      • //@version=5
        strategy("Exit Demo", pyramiding = 2)
        
        float positionSize = strategy.position_size
        
        if positionSize == 0 and last_bar_index - bar_index <= 100
            strategy.entry("Buy1", strategy.long, 5)
        else if positionSize == 5
            strategy.entry("Buy2", strategy.long, 10)
        else if positionSize == 15
            strategy.exit("bracket", loss = 10000, profit = 10000)
        • 10000틱 밑인 가격이나 10000틱 위인 가격이 오면 모든 포지션을 종료하게 exit 주문을 넣었습니다. 하지만 Buy1의 경우 10000틱 밑은 44495.6에 도달하지 않은 44816.8에 종료되었습니다. 이는 Buy2가 45816.8에 진입하였는데 10000틱 밑인 44816.8가격에 도달하여 종료 주문이 들어간것입니다. 이때 위에서 설명했듯이 FIFO 정책으로 인해 먼저 entry된 Buy1 5개와 Buy2 5개가 종료되게 된것입니다.
        • 이를 해결하기 위해서는 마찬가지로 close_entries_rule="ANY"를 지정하면 됩니다.
        • 아래 결과는 close_entries_rule="ANY"를 지정했을때입니다. 정상적으로 Buy2 먼저 10000틱 밑 가격에 전체가 종료되고, Buy1도 10000틱 밑 가격에 도달했을때 종료되게 되었습니다.
    • 예시 코드 5 (진입시에 종료 주문을 같이 넣어준 경우)
      • //@version=5
        strategy("Exit Demo", pyramiding = 2)
        
        float positionSize = strategy.position_size
        
        if positionSize == 0 and last_bar_index - bar_index <= 100
            strategy.entry("Buy1", strategy.long, 15)
            strategy.exit("buy1 exit", "Buy1", loss = 10000, profit = 10000)
        else if positionSize == 15
            strategy.entry("Buy2", strategy.long, 10)
            strategy.exit("buy2 exit", "Buy2", loss = 10000, profit = 10000)
        • exit 주문을 진입시에 작성했는데도 불구하고, buy2 exit가 먼저 체결되어 buy1 주문이 먼저 종료되는 것을 확인할 수 있습니다. (종료되는 가격또한 buy2 exit 기준 가격입니다. 수량도 buy2 수량만큼 buy1이 종료되었습니다.)
        • 이 경우에도 close_entries_rule="ANY" 를 넣어줄 경우 정상적으로 원하는 구현대로 작동하는 것을 확인할 수 있습니다.
          • strategy("Exit Demo", pyramiding = 2, close_entries_rule="ANY")

    OCA groups

    • One-Cancels-All (OCA) 그룹은 전략이 하나의 주문 배치 명력이 실행될 때 동일한 oca_name을 가진 다른 주문들을 전부 또는 부분적으로 취소할 수 있게 합니다. 이는 strategy.entry() 및 strategy.order() 명령에 포함되며, 사용자가 함수 호출 시 제공하는 oca_type에 따라 달라집니다.
    • 예시 : strategy.order("Short", strategy.short, stop = low, oca_name = "Entry", oca_type = strategy.oca.cancel)
      • oca_type에는 strategy.oca.cancel / strategy.oca.reduce / strategy.oca.none 이 들어갈 수 있습니다.
      • 기본값은 strategy.oca.none 입니다.
    • 참고로 oca_name은 같지만 oca_type이 다르다면 이 두개는 별도의 그룹으로 간주합니다. 즉, strategy.oca.cancel, strategy.oca.reduce, strategy.oca.non 유형을 결합할 수 없습니다.

    strategy.oca.cancel

    • strategy.oca.cancel 유형은 그룹에서 하나의 주문이 전부 또는 부분적으로 체결될 때 동일한 oca_name을 가진 모든 주문을 취소합니다.
      • 어떤 주문이 체결되면 다른 주문을 취소해야하는 경우에 유용합니다.
      • 예를 들어, Long 과 Short 진입 기준이 있을때 둘 중 하나가 체결되면 반드시 다른 하나는 생각하지 말아야합니다. 이럴경우, 같은 oca_name과 oca_type을 strategy.oca.cancel 로 지정해두면 다른 하나가 체결되면 나머지 주문은 자동으로 취소됩니다.
    • 예시 코드
      • 기존 코드
        • //@version=5
          strategy("OCA Cancel Demo", overlay=true)
          
          float ma1 = ta.sma(close, 5)
          float ma2 = ta.sma(close, 9)
          
          if ta.cross(ma1, ma2)
              if strategy.position_size == 0
                  strategy.order("Long",  strategy.long, stop = high)
                  strategy.order("Short", strategy.short, stop = low)
              else
                  strategy.close_all()
          
          plot(ma1, "Fast MA", color.aqua)
          plot(ma2, "Slow MA", color.orange)
          • ta.cross(ma1,ma2)가 일어난 시점인 12월 12일에 저점인 40711과 고점인 42125를 12월 13일에 둘다 도달하여, Long과 Short 진입이 동시에 진행되었습니다. 따라서 포지션이 자동으로 종료되게 되었습니다.
          • 하지만 원하는 것은 하나의 포지션이 들어간다음에 그 다음 ma1과 ma2가 교차할때 포지션을 종료하는 것을 원합니다. 이럴 경우 하나의 주문이 체결되면 다른 주문들을 취소할수 있는 strategy.oca.cancel을 사용하면 Long과 Short 둘중에 먼저 도달한 주문을 체결하면 나머지 하나는 종료되게 됩니다.
      • 변경된 코드 (oca_type = strategy.oca.cancel 적용)
        • //@version=5
          strategy("OCA Cancel Demo", overlay=true)
          
          float ma1 = ta.sma(close, 5)
          float ma2 = ta.sma(close, 9)
          
          if ta.cross(ma1, ma2)
              if strategy.position_size == 0
                  strategy.order("Long",  strategy.long, stop = high, oca_name = "Entry", oca_type = strategy.oca.cancel)
                  strategy.order("Short", strategy.short, stop = low, oca_name = "Entry", oca_type = strategy.oca.cancel)
              else
                  strategy.close_all()
          
          plot(ma1, "Fast MA", color.aqua)
          plot(ma2, "Slow MA", color.orange)
          • ma1과 ma2가 cross 할때 저점과 고점에 대해서 order 주문이 들어갑니다. 그 후에 이제 봉을 진행하다가 그 저점 가격에 먼저 도달하게 되면 short 포지션을 열고 (고점가격에 먼저 도달했을 경우 long 포지션을 엽니다.) 반대 포지션 주문을 취소하게 됩니다. (동일한 oca_name을 가지는 주문들을 모두 취소가 됩니다.)
          • 백테스팅 결과들을 쭉 살펴보면 long과 short 둘다 체결된 경우는 존재하지 않습니다. 

    strategy.oca.reduce

    • strategy.oca.reduce는 주문을 취소하지 않고, 대신 동일한 oca_name을 가진 주문들의 크기를 각 주문이 체결될 때마다 closed된 계약/주식/단위 수만큼 줄입니다.
      • 이는 특히 exit 전략에 유용합니다.
      • 예를 들어, entry를 한뒤에 손절가1과 익절가 1,2를 지정해두었습니다. entry는 총 6개를 진입했을때, 손절은 한번에 손절하기 때문에 6개를 한번에 종료합니다. 익절의 경우는 2번에 나누어서 익절할것이기때문에 3개씩 종료 코드를 작성하였습니다. 이럴 경우 만약에 익절가 1에 도달한 뒤에 익절가 2에 도달하지 못하고 손절가 1에 도달한 경우 3개를 종료하고 6개를 종료하기때문에 반대 포지션으로 3개가 진입하게 됩니다. 이러한 사건을 방지하게 위해서 필요한 것이 strategy.oca.reduce입니다.
      • strategy.oca.reduce를 사용하면 같은 oca_name을 가진 주문들에 대해서 체결도니 수량만큼 주문 수량을 줄여주게 됩니다.
    • 예시 코드
      • 기존 코드 
        • //@version=5
          strategy("Multiple TP Demo", overlay = true)
          
          var float stop   = na
          var float limit1 = na
          var float limit2 = na
          
          bool longCondition = ta.crossover(ta.sma(close, 5), ta.sma(close, 9))
          if longCondition and strategy.position_size == 0
              stop   := close * 0.9
              limit1 := close * 1.1
              limit2 := close * 1.2
              strategy.entry("Long",  strategy.long, 6)
              strategy.order("Stop",  strategy.short, stop = stop, qty = 6)
              strategy.order("Limit 1", strategy.short, limit = limit1, qty = 3)
              strategy.order("Limit 2", strategy.short, limit = limit2, qty = 3)
          
          bool showPlot = strategy.position_size != 0
          plot(showPlot ? stop   : na, "Stop",    color.red,   style = plot.style_linebr)
          plot(showPlot ? limit1 : na, "Limit 1", color.green, style = plot.style_linebr)
          plot(showPlot ? limit2 : na, "Limit 2", color.green, style = plot.style_linebr)
          • Stop 가격에 먼저 도달해서 6개의 포지션을 종료했지만, 추가적으로 limit 1 과 limit 2 주문까지 체결된것을 확인할 수 있습니다. (정확한 백테스팅 불가)
      • 변경된 코드 (oca_type = strategy.oca.reduce 적용)
        • //@version=5
          strategy("Multiple TP Demo", overlay = true)
          
          var float stop   = na
          var float limit1 = na
          var float limit2 = na
          
          bool longCondition = ta.crossover(ta.sma(close, 5), ta.sma(close, 9))
          if longCondition and strategy.position_size == 0
              stop   := close * 0.9
              limit1 := close * 1.1
              limit2 := close * 1.2
              strategy.entry("Long",  strategy.long, 6)
              strategy.order("Stop",  strategy.short, stop = stop, qty = 6, oca_name = "Bracket", oca_type = strategy.oca.reduce)
              strategy.order("Limit 1", strategy.short, limit = limit1, qty = 3, oca_name = "Bracket", oca_type = strategy.oca.reduce)
              strategy.order("Limit 2", strategy.short, limit = limit2, qty = 6, oca_name = "Bracket", oca_type = strategy.oca.reduce)
          
          bool showPlot = strategy.position_size != 0
          plot(showPlot ? stop   : na, "Stop",    color.red,   style = plot.style_linebr)
          plot(showPlot ? limit1 : na, "Limit 1", color.green, style = plot.style_linebr)
          plot(showPlot ? limit2 : na, "Limit 2", color.green, style = plot.style_linebr)
          • 정상적으로 6개 진입하면 6개의 주문이 모두 체결되면 종료되는 것을 확인할 수 있습니다. 
          • 여기서 중요한점은 limit 2 부분의 order를 6으로 해야합니다. 같은 oca_name이기때문에 limit 1이 체결되면 같은 oca_name을 가진 order에 대해서 3씩 주문 수량이 줄어들기때문에 만약 3으로 해놓으면 0이 되어서 체결이 되지 않을 것입니다.
          • 즉, 최종 종료 지점의 qty는 진입 수량과 동일해야합니다.
            • 예를 들어 익절가1, 익절가2, 익절가3, 손절가1, 손절가2, 손절가3 이 있을 경우 각각 2 4 6 이런식으로 설정을 해주어야합니다.
            • 하지만 이렇게 할경우 익절가 1이 체결되면 손절가1에 대해서도 수량이 0으로 바뀌므로 손절가 1에서는 아무런 주문이 일어나지 않게 됩니다.
    • 예시 코드 2
      • //@version=5
        strategy("Multiple TP Demo", overlay = true)
        
        var float stop1   = na
        var float stop2   = na
        var float stop3   = na
        
        var float limit1 = na
        var float limit2 = na
        var float limit3 = na
        
        bool longCondition = ta.crossover(ta.sma(close, 5), ta.sma(close, 9))
        if longCondition and strategy.position_size == 0
            stop1   := close * 0.9
            stop2   := close * 0.8
            stop3   := close * 0.7
            limit1 := close * 1.1
            limit2 := close * 1.2
            limit3 := close * 1.3
            strategy.entry("Long",  strategy.long, 6)
            strategy.order("Stop 1",  strategy.short, stop = stop1, qty = 2, oca_name = "Bracket", oca_type = strategy.oca.reduce)
            strategy.order("Stop 2",  strategy.short, stop = stop2, qty = 4, oca_name = "Bracket", oca_type = strategy.oca.reduce)
            strategy.order("Stop 3",  strategy.short, stop = stop3, qty = 6, oca_name = "Bracket", oca_type = strategy.oca.reduce)
            strategy.order("Limit 1", strategy.short, limit = limit1, qty = 2, oca_name = "Bracket", oca_type = strategy.oca.reduce)
            strategy.order("Limit 2", strategy.short, limit = limit2, qty = 4, oca_name = "Bracket", oca_type = strategy.oca.reduce)
            strategy.order("Limit 3", strategy.short, limit = limit3, qty = 6, oca_name = "Bracket", oca_type = strategy.oca.reduce)
        
        bool showPlot = strategy.position_size != 0
        plot(showPlot ? stop1   : na, "Stop 1",    color.red,   style = plot.style_linebr)
        plot(showPlot ? stop2   : na, "Stop 2",    color.red,   style = plot.style_linebr)
        plot(showPlot ? stop3   : na, "Stop 3",    color.red,   style = plot.style_linebr)
        plot(showPlot ? limit1 : na, "Limit 1", color.green, style = plot.style_linebr)
        plot(showPlot ? limit2 : na, "Limit 2", color.green, style = plot.style_linebr)
        plot(showPlot ? limit3 : na, "Limit 3", color.green, style = plot.style_linebr)

        • limit1이 체결되면 stop1이 체결되지 않고 바로 다음 stop2로 넘어가는 것을 확인할 수 있습니다.

    strategy.oca.none

    • strategy.oca.none의 경우 strategy.order()와 strategy.entry() 명령에 대한 기본 OCA 유형입니다. 즉, 이 유형을 사용할 경우 주문은 다른 주문과 연관되지 않고 각각 개별적으로 처리됩니다.
    • 예를 들어 여러 주문이 배치될 때 특정 주문이 체결되거나 취소되어도 다른 주문에 영향을 주지 않으려면 strategy.oca.none을 사용하면 됩니다.

    Currency

    • strategy에서 계산에 사용하는 기본 통화를 지정해줄 수 있습니다.
    • strategy("Currency Test", currency = currency.KRW) 와 같이 변경하면 strategy.account_currency 값이 변경되는 것입니다. 
      • 전략의 기본 통화 값은 currency.NONE 이며 이는 스크립트가 차트의 기본 통화를 따라가는 것입니다.
    • 통화를 변경하게 되면 전 거래일의 환율 변환율로 곱해지게 됩니다.

    Altering calculation behavior

    • Strategy는 차트에서 과거 데이터에 대해서 백테스팅을 진행한 뒤에 실시간 데이터에 대해서도 계속해서 시그널이 나오는지 체크를 진행합니다.
    • 백테스팅의 경우 항상 하나의 봉이 종료되면 주문내역들중에서 가격에 도달한것들이 있는지 체크를 하고, 있다면 해당봉에 체결내역을 표시한뒤에, 해당 스크립트 코드가 실행되었습니다.
    • 하지만 이제 실시간 매매의 경우 하나의 하나의 봉이 종료되고나서 해당 가격에 도달했을경우 그 봉에서 체결되었다고 표시하게 되면 무조건적으로 해당 봉의 종가에서 체결되게 됩니다. (백테스팅처럼 그 순간 원하는 가격에 체결되지 않습니다.)
    • 이러한 것들을 방지하기 위해서 다음 사항들이 있습니다.

    calc_on_every_tick

    • calc_on_every_tick은 실시간 데이터에서 매 틱마다 신호를 발생하기 위해 사용합니다. (즉, 백테스팅 용이 아닌 실시간 데이터 용입니다.)
    • strategy에서 calc_on_every_tick=true로 설정하게 되면 실시간 데이터에서 가격이 변화할때마다 스크립트를 실행하게 됩니다. 
    • calc_one_every_tick=true로 설정할때는 pyramiding 설정이 1로 되어 있어야지 안정적입니다. 만약 2 이상일 경우, 매 틱마다 신호가 발생하면 스크립트가 실행되기때문에 그 잠깐사이에 수많은 거래가 발생하면 pyramiding 설정한만큼 한번에 주문이 들어가버릴수도 있습니다.
      • 만약 pyramiding을 아래 코드처럼 1000으로 설정하면 돌파한 순간 틱이 변화할때마다 한번에 수많은 alert_message가 발송될 수 있습니다.
    • 예시 코드
      • //@version=5
        //@strategy_alert_message {{strategy.order.alert_message}}
        strategy("돌파 체크", overlay = true, calc_on_every_tick = true, pyramiding = 1000)
        
        int length = input.int(15, "Length")
        
        float highest = ta.highest(high, length)[1]
        float lowest  = ta.lowest(low, length)[1]
        
        if close[0] >= highest
            strategy.entry("Buy", strategy.long,alert_message = "현재가 : "+ str.tostring(close[0])+ "hieghest : "+str.tostring(highest))
        if close[0] <= lowest
            strategy.entry("Sell", strategy.short,alert_message = "현재가 : "+ str.tostring(close[0])+ "lowest : "+str.tostring(lowest))
        
        var realTimeStart = timenow
        
        bgcolor(time_close >= realTimeStart ? color.new(color.orange, 80) : na)
        
        plot(highest, "Highest", color = color.lime)
        plot(lowest, "Lowest", color = color.red)
        • 저가를 돌파한 가격에서 가격이 조금이라도 변화할때마다 alert message가 전송되는 것을 확인할 수 있습니다. 이를 통해 매 틱 변화마다 스크립트가 실행된다는 것을 확인할 수 있습니다.
        • 하지만 따로 sell 표시가 alert message를 보낸것처럼 엄청나게 표시되지는 않습니다. 이로 인해 차트에서 표시된 부분만 본다면 매틱마다 작동되는거 맞아? 라고 생각할 수 있습니다. 하지만 alert message를 보내서 확인해보면 정상적으로 매 틱마다 코드가 실행된다는 것을 확인할 수 있습니다. (차트에서 buy 표시나 sell표시는 많은 주문으로 인해 정상적으로 표시된거같지 않습니다.)

    calc_on_orders_fills

    • calc_on_order_fills는 주문이 체결된 직후 스크립트를 바로 해당 봉에서 다시 실행하여 추가 주문을 배치할 수 있도록 해줍니다.
    • 예시 코드
      • //@version=5
        strategy("Intrabar exit", overlay = true, calc_on_order_fills = true)
        
        float stopSize   = input.float(10.0, "SL %", minval = 0.0) / 100.0
        float profitSize = input.float(10.0, "TP %", minval = 0.0) / 100.0
        
        if strategy.position_size == 0.0
            strategy.entry("Buy", strategy.long)
        
        float stopLoss   = strategy.position_avg_price * (1.0 - stopSize)
        float takeProfit = strategy.position_avg_price * (1.0 + profitSize)
        
        strategy.exit("Exit", stop = stopLoss, limit = takeProfit)
        • calc_on_order_fills가 true이기 때문에 exit가 된 봉에서 스크립트를 다시 실행해서 바로 exit된 봉에서 진입을 다시 할 수 있습니다. (이때 entry 가격은 exit 이후 다음 틱에서 주문을 체결합니다. 이는 이제 Emulator의 순서에 따라 움직입니다. 시가가 고점에 가까우면 시->고->저->종 , 시가가 저가에 가까우면 시->저->고->종 순서인데 이 판단하에 움직이게 됩니다. 바 돋보기를 킨다면 보다 정확하게 실행될것입니다.)
        • calc_on_order_fills가 꺼져있으면, 동일한 전략은 exit 주문을 트리거한 후 한 바가 지난 후에만 진입 주문을 합니다. 먼저 바 중간에 exit가 발생하고, 그 후 바가 닫힌 후에 스크립트가 실행된뒤 다음 봉 시작에서 진입 주문이 들어갑니다.
        • calc_on_order_fills를 활성화 하면 실제 거래에서는 불가능한 주문 가격을 Broker Emulator가 가정할 수 있기때문에 전략 결과가 비현실적일수 있습니다.
    • 예시 코드 2
      • //@version=5
        strategy("buy on every fill", overlay = true, calc_on_order_fills = true, pyramiding = 100)
        
        if last_bar_index - bar_index <= 25
            strategy.entry("Buy", strategy.long)
        • 이 예제는 entry가 체결될때마다 스크립트가 다시실행되고 해당 바에서 또 체결이 될경우 다시 또 스크립트가 실행되게 됩니다. 이런식으로 네번의 틱(시가,고가,저가,종가)에서 buy가 진행되게 된것입니다. (이는 Emulator가 한 바를 네번의 틱으로 간주하기 때문입니다.)
        • 이는 비현실적인 행동이며, 실제로 바의 정확한 고가 또는 저가에서 주문이 체결되는 것은 일반적으로 불가능합니다.
    • 예시 코드 3
      • //@version=5
        strategy("buy on every fill", overlay = true, calc_on_order_fills = true, pyramiding = 100, use_bar_magnifier = true)
        
        if last_bar_index - bar_index <= 25
            strategy.entry("Buy", strategy.long)
        • 위 코드는 예시 코드 2에서 use_bar_magnifier=true 부분만 추가시켜준것입니다.
        • 바 돋보기를 키게된다면, 1일봉의 경우 백테스팅을 진행할때 60분봉을 기준으로 한번더 테스트를 진행해주게 됩니다. 따라서 1일봉의 경우 24개의 60분봉을 기준으로 테스트를 진행하는것입니다. 따라서 각 60분봉마다 시가,고가,저가,종가가 있기때문에 총 24x4 = 96개가 일봉 한개에서 체결된것입니다.  나머지 4개는 다음 봉에서 체결되었습니다.
        • 이처럼 calc_on_orders_fills를 사용할때는 주의가 필요합니다. 각 봉의 시가, 고가, 저가, 종가에서 체결된다고 가정하기때문에 말도 안되는 백테스팅 결과가 나올 수 있습니다.(시가, 종가에 체결은 가능해도, 고가, 저가에 딱 체결하기는 힘들기때문입니다.)

    process_orders_on_close

    • 기본적으로 strategy는 각 바의 종가에서 주문을 시뮬레이션 합니다. 그러므로 가장 빨리 주문 체결과 전략 계산 및 alert를 실행이 이루어지는 때는 다음바가 시작될때 입니다. 이것을 변경시켜주고 싶다면 process_orders_on_close 설정을 활성화함으로 써 변경할 수 있습니다.
    • 이것은 주식시장과 같이 전날에 마감하면 전날봉 마감에 체결이 되어야하는데 다음날봉 처음봉에 체결된걸로 백테스팅 되는 것을 방지할 수 있습니다.
      • 즉, 스크립트가 실행되고 다음봉의 시가에서 체결되는 것이 아닌, 스크립트가 실행된 봉의 종가에서 체결되어 있도록 백테스팅할 수 있게 해줍니다.
      • 예를 들면 기존의 경우 수요일 3시30분에 장이 마감한다면 3시 30분 봉에서 시그널이 발생해서 3시 30분에 마감할때 딱 매수가 되어야하는데, 목요일 9시00분 장이 시작할때 매수한것처럼 나올 수 있게 됩니다. 이러한것을 방지해주는 것이 process_orders_on_close 입니다.

    Simulating trading costs

    • 백테스팅의 결과가 유의미하기위해서는 실제 세계의 비용을 고려하는 것이 중요합니다.
      • 수수료, 슬리피지 등

    Commission(수수료)

    • 수수료는 실제 거래소에서 거래가 체결될때마다 지불되는 금액입니다. 
      • 거래소마다 다르기때문에 거래소 기준을 참고해야합니다.
    • 자동매매의 경우, 거래 진행횟수가 많은 전략이 많기 때문에 수수료가 많이 발생합니다. 따라서 수수료를 고려하지 않고 백테스팅을 진행했을때는 수익이 발생하지만, 수수료를 고려했을 경우 손실이 발생하는 경우가 자주 발생합니다.
    • strategy() 함수에서 commision_type 과 commision_value 인자를 통해 수수료를 설정할 수 있습니다.
      • commision_type = strategy.commision.percent / commision_value = 1 인 경우 수수료는 1%를 의미하게 됩니다. 
      • 또는 설정에서 Commision 입력값을 설정해서도 설정이 가능합니다.
    • 예시 코드
      • //@version=5
        strategy(
             "Commission Demo", overlay=true, default_qty_value = 2, default_qty_type = strategy.percent_of_equity,
             commission_type = strategy.commission.percent, commission_value = 1
         )
        
        length = input.int(10, "Length")
        
        float highest = ta.highest(close, length)
        float lowest  = ta.lowest(close, length)
        
        switch close
            highest => strategy.entry("Long", strategy.long)
            lowest  => strategy.close("Long")
        
        plot(highest, color = color.new(color.lime, 50))
        plot(lowest, color = color.new(color.red, 50))

    Slippage(슬리피지)

    • 실제 거래에서는 변동성, 유동성, 주문 크기 및 기타 시장 요인으로 인해 거래자가 의도한 가격과 다소 다른 가격으로 주문이 체결될 수 있습니다. 이는 백테스팅과 다른 결과를 유도하게 될것입니다. 
      • 이와 같이 기대 가격과 실제 체결 가격 간의 차이를 슬리피지(Slippage)라고 합니다.
      • 슬리피지는 정확하게 예측하기는 힘들지만, 어느정도 거래가 될때마다 수수료처럼 소량의 슬리피지를 고려하면 어느정도 실제 거래와 백테스팅이 비슷한 결과를 낼 수 있을것입니다.
    • strategy() 함수에서 slippage를 통해 설정해줄 수 있습니다.
      • slippage = 20 인 경우 각 시뮬레이션의 주문의 가격이 거래 방향에서 현재 가격보다 안좋은 가격으로 20틱 이동한 가격으로 체결되는것으로 설정됩니다.
    • 예시 코드
      • //@version=5
        strategy(
             "Slippage Demo", overlay = true, slippage = 1000,
             default_qty_value = 2, default_qty_type = strategy.percent_of_equity
         )
        
        int length = input.int(5, "Length")
        
        //@variable Exponential moving average with an input `length`.
        float ma = ta.ema(close, length)
        
        //@variable Returns `true` when `ma` has increased and `close` is greater than it, `false` otherwise.
        bool longCondition = close > ma and ma > ma[1]
        //@variable Returns `true` when `ma` has decreased and `close` is less than it, `false` otherwise.
        bool shortCondition = close < ma and ma < ma[1]
        
        // Enter a long market position on `longCondition`, close the position on `shortCondition`.
        if longCondition
            strategy.entry("Buy", strategy.long)
        if shortCondition
            strategy.close("Buy")
        
        //@variable The `bar_index` of the position's entry order fill.
        int entryIndex = strategy.opentrades.entry_bar_index(0)
        //@variable The `bar_index` of the position's close order fill.
        int exitIndex  = strategy.closedtrades.exit_bar_index(strategy.closedtrades - 1)
        
        //@variable The fill price simulated by the strategy.
        float fillPrice = switch bar_index
            entryIndex => strategy.opentrades.entry_price(0)
            exitIndex  => strategy.closedtrades.exit_price(strategy.closedtrades - 1)
        
        //@variable The expected fill price of the open market position.
        float expectedPrice = na(fillPrice) ? open : na
        
        color expectedColor = na
        color filledColor   = na
        
        if bar_index == entryIndex
            expectedColor := color.green
            filledColor   := color.blue
        else if bar_index == exitIndex
            expectedColor := color.red
            filledColor   := color.fuchsia
        
        plot(ma, color = color.new(color.orange, 50))
        
        plotchar(na(fillPrice) ? open : na, "Expected fill price", "—", location.absolute, expectedColor)
        plotchar(fillPrice, "Fill price after slippage", "—", location.absolute, filledColor)
        • 슬리피지로 인해 실제 체결 가격은 슬리피지 틱만큼 안좋은 방향에서 백테스팅 진행된것을 확인할 수 있습니다.

    unfilled limits (미체결 limit 주문)

    • limit(지정가) 주문을 하면은 해당가격에 체결되는 것이기때문에 슬리피지와 같은것은 고려하지 않아도 됩니다. 하지만 여기서 중요한 점은 실제 시장에서는 해당 가격에 도달하더라도 해당 limit(지정가) 주문이 체결되지 않을 수가 있습니다.
      • limit(지정가) 주문의 경우 해당 가치 주변의 가격 움직임과 충분한 유동성이 있을때만 체결될 수 있습니다. 예를들어 만약 해당 가격에 도달하더라도 해당 가격에서 조금만 체결될경우 내가 한 주문은 체결되지 않고 진행될수가 있습니다. 
    • 따라서 백테스팅에서 미체결 주문 가능성을 고려하기 위해 가격이 주문 가격을 지정된 틱 수만큼 넘어간 후에만 리밋 주문을 체결하도록 지시할 수 있습니다.
      • strategy에 backtest_fill_limits_assuption 값을 지정하여 설정 가능합니다.
      • 또는 전략 설정에서 Properties 탭에서 Verify price for limit orders 입력값을 사용하여 설정 가능합니다.
      • 해당 틱수만큼 가격을 넘어간 후에 체결됩니다.
      • 하지만 여기서 중요한 점은 해당 틱수만큼 넘어갔을때의 가격이 아닌 limit 가격에서 지정한 가격으로 체결됩니다.
    • 예시 코드
      • //@version=5
        
        strategy(
             "Verify price for limits example", overlay = true,
             default_qty_type = strategy.percent_of_equity, default_qty_value = 2,
             backtest_fill_limits_assumption = 10000
         )
        
        int length = input.int(25, title = "Length")
        
        //@variable Draws a line at the limit price of the most recent entry order.
        var line limitLine = na
        
        // Highest high and lowest low
        highest = ta.highest(length)
        lowest  = ta.lowest(length)
        
        // Place an entry order and draw a new line when the the `high` equals the `highest` value and `limitLine` is `na`.
        if high == highest and na(limitLine)
            float limitPrice = close
            strategy.entry("Long", strategy.long, limit = limitPrice)
            limitLine := line.new(bar_index, limitPrice, bar_index + 1, limitPrice)
        
        // Close the open market position, cancel orders, and set `limitLine` to `na` when the `low` equals the `lowest` value.
        if low == lowest
            strategy.cancel_all()
            limitLine := na
            strategy.close_all()
        
        // Update the `x2` value of `limitLine` if it isn't `na`.
        if not na(limitLine)
            limitLine.set_x2(bar_index + 1)
        
        plot(highest, "Highest High", color = color.new(color.green, 50))
        plot(lowest, "Lowest Low", color = color.new(color.red, 50))
        • 이해를 위해 backtest_fill_limits_assumption을 과도하게 10000으로 설정했습니다. 이는 가격이 10000틱만큼 더 멀어져야지 체결됨을 의미합니다.
        • 여기서 10월 1일에 종가(27981.4)로 Long 주문이 들어갔습니다. 하지만 backtest_fill_limits_assumption을 10000으로 설정했기때문에 해당 가격보다 1000원 더 떨어진 26981.4가격에 도달해야지만 long 주문이 체결되게 됩니다. 
        • 그러므로 이제 10월11일(저가 26525.0)이 되어서야 26981.4보다 가격이 더 떨어졌기 때문에 그때서야 앞서 주문한 long 주문이 10월 1일 종가 가격으로체결됩니다.
          • backtest_fill_limits_assumption 틱만큼 떨어진 가격이 아닌 기존에 설정한 주문 가격으로 체결된다는 것을 알아야합니다.
          • 즉, 주문 체결 시간만을 변경할 뿐, 주문이 체결된 가격은 동일하게 유지합니다. 
          • 따라서 10월 11일에 27981.4라는 가격이 존재하지는 않았지만 10월 11일에 체결이 되게 됩니다. 이는 이제 말도안되는 백테스팅 결과가 될수 있습니다.
        • 이와 같은 이유로 이제 bakctest_fill_limits_assumption을 사용할때 주의가 필요합니다.

    Risk management

    • 성능이 좋은 전략을 설계하기 위해서는 위험 관리는 반드시 필요합니다.
    • strategy.risk 접두사가 붙은 특수 명령어를 사용하여 전략 스크립트에 위험 관리 기준을 설정할 수 있습니다.
    • 모든 위험 관리 명령은 전략의 계산 행동에 대한 변경과 관계없이 모든 틱과 주문 실행 이벤트에서 실행됩니다.
      • 스크립트 실행 중에 이 명령을 비활성화 하는 방법은 없습니다.
      • 사용자가 코드에서의 호출을 제거하지 않는 한 항상 전략에 적용됩니다.

    strategy.risk.allow_entry_in()

    • 이 명령은 strategy.entry() 명령에 허용된 시장 방향을 재정의합니다. 사용자가 이 함수를 사용하여 거래 방향을 지정하면 (예: strategy.direction.long) 전략은 해당 방향으로만 거래를 진행합니다. 그러나 만약 열린 시장 포지션이 있는 상태에서 반대 방향으로 진입 명령을 호출하는 경우, 전략은 해당 포지션을 종료하는 시장 주문을 시뮬레이션 합니다.
      • Possible values: strategy.direction.long, strategy.direction.short, strategy.direction.all
    • 예시 코드
      • //@version=5
        strategy("strategy.risk.allow_entry_in")
        
        strategy.risk.allow_entry_in(strategy.direction.long)
        if open > close
            strategy.entry("Long", strategy.long)
        // Instead of opening a short position with 10 contracts, this command will close long entries.
        if open < close
            strategy.entry("Short", strategy.short, qty = 10)
        • 기존대로라면 short 방향으로 11개 진입이 들어가야합니다. (long 진입을 종료하는것 1개 포함 총 11개)
        • 하지만 short 방향으로 1개만 체결되어 기존의 long 진입이 종료되는 것을 확인할 수 있습니다.
        • 이를 통해 포지션의 방향이 strategy.risk.allow_entry_in에서 설정한 방향으로만 진행된다는 것을 확인할 수 있습니다.

    strategy.risk.max_cons_loss_days()

    • 이 명령은 전략이 연속적인 손실을 보이는 일정한 거래일 수가 나타나면 모든 대기 중인 주문을 취소하고, 열린 시장 포지션을 종료하며 추가 거래 행동을 중지합니다.
    • 예시 코드
      • //@version=5
        strategy("risk.max_cons_loss_days Demo 1")
        strategy.risk.max_cons_loss_days(3) // No orders will be placed after 3 days, if each day is with loss.
        plot(strategy.position_size)

    strategy.risk.max_drawdown()

    • 이 명령은 전략의 손실이 함수 호출에서 지정된 금액에 도달하면 모든 대기 중인 주문을 취소하고, 열린 시장 포지션을 종료하며, 추가 거래 행동을 중지합니다.
    • 예시 코드
      • strategy("risk.max_drawdown Demo 1")
        strategy.risk.max_drawdown(50, strategy.percent_of_equity) // set maximum drawdown to 50% of maximum equity
        plot(strategy.position_size)

    strategy.risk.max_intraday_filled_orders()

    • 이 명령은 하루 거래일(또는 차트 타임프레임이 일간보다 높은 경우 차트 바 당)당 채워진 주문의 최대 수를 지정합니다. 전략이 하루 동안 최대 주문 수를 실행하면 모든 대기 중인 주문을 취소하고, 열린 시장 포지션을 종료하며, 현재 세션의 끝까지 거래 활동을 중단합니다.
    • 예시 코드
      • //@version=5
        strategy("risk.max_intraday_filled_orders Demo")
        strategy.risk.max_intraday_filled_orders(10) // After 10 orders are filled, no more strategy orders will be placed (except for a market order to exit current open market position, if there is any).
        if open > close
            strategy.entry("buy", strategy.long)
        if open < close
            strategy.entry("sell", strategy.short)

    strategy.risk.max_intraday_loss()

    • 이 명령은 전략이 하루 거래일당(또는 차트 타임프레임이 일간보다 높은 경우 차트 바 당) 허용하는 최대 손실을 제어합니다. 전략의 손실이 이 임계값에 도달하면 모든 대기 중인 주문을 취소하고, 열린 시장 포지션을 종료하며, 현재 세션의 끝까지 모든 거래 활동을 중단합니다.
    • 예시 코드
      • // Sets the maximum intraday loss using the strategy's equity value.
        //@version=5
        strategy("strategy.risk.max_intraday_loss Example 1", overlay = false, default_qty_type = strategy.percent_of_equity, default_qty_value = 100)
        
        // Input for maximum intraday loss %. 
        lossPct = input.float(10)
        
        // Set maximum intraday loss to our lossPct input
        strategy.risk.max_intraday_loss(lossPct, strategy.percent_of_equity)
        
        // Enter Short at bar_index zero.
        if bar_index == 0
            strategy.entry("Short", strategy.short)
        
        // Store equity value from the beginning of the day
        eqFromDayStart = ta.valuewhen(ta.change(dayofweek) > 0, strategy.equity, 0)
        
        // Calculate change of the current equity from the beginning of the current day.
        eqChgPct = 100 * ((strategy.equity - eqFromDayStart) / strategy.equity)
        
        // Plot it
        plot(eqChgPct) 
        hline(-lossPct)

    strategy.risk.max_position_size()

      • 이 명령은 strategy.entry() 명령을 사용할 때 가능한 최대 포지션 크기를 지정합니다. 진입 명령의 수량이 이 한계를 초과하는 시장 포지션을 초래하는 경우, 전략은 주문 수량을 줄여 결과적인 포지션이 제한을 초과하지 않도록 합니다.
      • 예시 코드
        • //@version=5
          strategy("risk.max_position_size Demo", default_qty_value = 100)
          strategy.risk.max_position_size(10)
          if open > close
              strategy.entry("buy", strategy.long)
          plot(strategy.position_size)  // max plot value will be 10