برو به محتوای اصلی

تریدینگ ویو

استراتژِی ها

استراتژی‌ها

مقدمه
استراتژی‌های Pine Script™ یه نوع خاص از اسکریپت‌ها هستن که معامله‌ها رو توی کندل‌های گذشته و حتی به‌صورت زنده شبیه‌سازی می‌کنن. این یعنی می‌تونی باهاش سیستم‌های معاملاتی خودت رو هم بک‌تست و هم فوروارد تست کنی.

اسکریپت‌های استراتژی بیشتر قابلیت‌هایی که اندیکاتورها دارن رو هم دارن، ولی علاوه بر اون می‌تونن سفارش‌های فرضی (شبیه‌سازی‌شده) بذارن، تغییرشون بدن یا لغوشون کنن و عملکردشون رو هم تحلیل کنن.

وقتی یه اسکریپت با تابع strategy() شروع بشه، به فضای نام strategy.* دسترسی پیدا می‌کنه. این فضا کلی تابع و متغیر داره برای شبیه‌سازی سفارش‌ها و گرفتن اطلاعات مهم از استراتژی. همچنین اطلاعات مربوط به عملکرد استراتژی رو هم توی تب مخصوص Strategy Tester نشون می‌ده.

یه نمونه استراتژی ساده

اسکریپتی که پایین می‌بینی یه استراتژی خیلی ساده‌ست که ورود به پوزیشن لانگ یا شورت رو بر اساس تقاطع دو میانگین متحرک شبیه‌سازی می‌کنه. وقتی میانگین متحرک سریع (fastMA) از بالا، میانگین متحرک کندتر (slowMA) رو قطع کنه، یه سفارش بازار (market order) برای ورود به پوزیشن خرید (لانگ) ارسال می‌کنه. برعکس، وقتی fastMA از پایین slowMA رو قطع کنه، یه سفارش فروش (شورت) ثبت می‌شه.

//@version=6
strategy("Simple strategy demo", overlay = true, margin_long = 100, margin_short = 100)

//@variable طول fastMA و نصف طول slowMA
int lengthInput = input.int(14, "Base length", 2)

// محاسبه‌ی دو میانگین متحرک با طول‌های متفاوت
float fastMA = ta.sma(close, lengthInput)
float slowMA = ta.sma(close, lengthInput * 2)

// وقتی fastMA از پایین slowMA رو قطع می‌کنه، وارد پوزیشن خرید شو
if ta.crossover(fastMA, slowMA)
    strategy.entry("buy", strategy.long)

// وقتی fastMA از بالا slowMA رو قطع می‌کنه، وارد پوزیشن فروش شو
if ta.crossunder(fastMA, slowMA)
    strategy.entry("sell", strategy.short)

// رسم میانگین‌های متحرک روی چارت
plot(fastMA, "Fast MA", color.aqua)
plot(slowMA, "Slow MA", color.orange)

نکاتی که باید بدونی:

  • تابع strategy() مشخص می‌کنه که این یه اسکریپت استراتژی به اسم “Simple strategy demo” هست که روی چارت اصلی نشون داده می‌شه.
  • پارامترهای margin_long و margin_short توی تابع strategy() نشون می‌دن که استراتژی برای ورود به هر پوزیشن باید ۱۰۰٪ از حجم مورد نیاز اون پوزیشن رو داشته باشه (یعنی اهرم یا لوریج در نظر گرفته نشده).
  • تابع strategy.entry() همون چیزیه که سفارش ورود به معامله رو می‌سازه یا جهت پوزیشن رو برعکس می‌کنه.
    • دستور "buy" اگه پوزیشن شورت فعالی باشه، اول اون رو می‌بنده و بعد وارد پوزیشن لانگ می‌شه.
    • دستور "sell" هم برعکسش رو انجام می‌ده.

اعمال یک استراتژی روی چارت

برای اینکه یه استراتژی رو تست کنی، باید اون رو به چارت اضافه کنی. می‌تونی یکی از استراتژی‌های آماده (Built-in) یا منتشرشده توسط دیگران رو از منوی “Indicators, Metrics & Strategies” انتخاب کنی، یا اینکه یه استراتژی اختصاصی خودت رو توی Pine Editor بنویسی.

بعد از نوشتن کد، کافیه از گوشه بالا سمت راست Pine Editor روی گزینه “Add to chart” کلیک کنی

این اسکریپت، علامت‌های ورود و خروج معاملات (trade markers) رو روی چارت اصلی نشون می‌ده و نتایج عملکرد شبیه‌سازی‌شده رو توی تب Strategy Tester نمایش می‌ده.

توجه کن!

نتایج عملکردی که از اجرای یه استراتژی روی چارت‌های غیر استاندارد (مثل Heikin Ashi، Renko، Line Break، Kagi، Point & Figure و Range) به دست میاد، به‌صورت پیش‌فرض شرایط واقعی بازار رو نشون نمی‌ده. چون استراتژی برای شبیه‌سازی، از قیمت‌های مصنوعی همین چارت‌ها استفاده می‌کنه، که معمولاً با قیمت‌های واقعی بازار فرق دارن و این باعث می‌شه نتایج استراتژی غیرواقعی باشه.
به‌خاطر همین، شدیداً توصیه می‌کنیم موقع تست استراتژی‌ها از چارت‌های استاندارد استفاده کنی. البته، اگه بخوای روی چارت Heikin Ashi تست بزنی، یه راه دیگه هم هست: می‌تونی توی تنظیمات استراتژی، گزینه‌ی “Fill orders on standard OHLC” رو فعال کنی یا توی دستور strategy() از fill_orders_on_standard_ohlc = true استفاده کنی تا سفارش‌ها با قیمت‌های واقعی (OHLC استاندارد) شبیه‌سازی بشن.

Strategy Tester

Strategy Tester عملکرد فرضی یه اسکریپت استراتژی رو به‌صورت کامل نشون می‌ده و مشخصاتش رو نمایش می‌ده. برای استفاده ازش، اول یه اسکریپتی که با تابع strategy() تعریف شده رو به چارت اضافه کن، بعد تب “Strategy Tester” رو باز کن. اگه دو تا یا بیشتر استراتژی روی چارت باشه، باید از گوشه بالا سمت چپ، اسم اون استراتژی‌ای که می‌خوای تحلیلش کنی رو انتخاب کنی.

بعد از اینکه اسکریپت انتخاب‌شده روی دیتای چارت اجرا شد، Strategy Tester اطلاعات مربوط به استراتژی رو توی این چهار تا تب نمایش می‌ده:

  • Overview
  • Performance Summary
  • List of Trades
  • Properties

Overview

تب Overview یه نگاه سریع به عملکرد استراتژی توی یه سری از معامله‌های شبیه‌سازی‌شده می‌ده. این تب یه‌سری معیارهای مهم عملکرد رو نشون می‌ده و یه نمودار داره که شامل سه تا خط مفید می‌شه:

  • نمودار Equity baseline رشد سرمایه استراتژی رو توی معامله‌های بسته‌شده نشون می‌ده.
  • نمودار ستونی Drawdown نشون می‌ده سرمایه استراتژی چقدر از سقف خودش پایین‌تر رفته.
  • نمودار Buy & hold equity رشد سرمایه یه استراتژی رو نشون می‌ده که فقط یه بار وارد پوزیشن خرید شده و اون رو تا آخر بازه تست نگه داشته.

دقت کن که:

نمودار دو تا محور عمودی جداگانه داره. نمودارهای “Equity” و “Buy & hold equity” از مقیاس سمت چپ استفاده می‌کنن، و نمودار “Drawdown” از مقیاس سمت راست. کاربر می‌تونه با استفاده از گزینه‌های پایین نمودار، نمایش این نمودارها رو روشن یا خاموش کنه و بین مقیاس مطلق یا درصدی جابجا بشه.
وقتی کاربر روی یه نقطه از این نمودار کلیک کنه، چارت اصلی به همون کندلی که اون معامله بسته شده اسکرول می‌شه و یه تول‌تیپ نشون می‌ده که زمان بسته شدن معامله رو مشخص می‌کنه.

Performance Summary

تب Performance Summary یه خلاصه‌ی دقیق از معیارهای کلیدی عملکرد استراتژی رو نشون می‌ده که توی ستون‌های جداگانه دسته‌بندی شدن. ستون “All” اطلاعات عملکردی مربوط به همه‌ی معامله‌های شبیه‌سازی‌شده رو نشون می‌ده، و ستون‌های “Long” و “Short” هم معیارهای مربوط به معاملات خرید و فروش رو جداگانه نمایش می‌دن.
این بخش یه دید کامل‌تر و جزئی‌تر از عملکرد کلی استراتژی و همین‌طور عملکردش در جهت‌های مختلف بازار (لانگ یا شورت) بهت می‌ده.

List of Trades

تب List of Trades، معامله‌های شبیه‌سازی‌شده استراتژی رو به‌صورت زمانی پشت سر هم لیست می‌کنه. هر مورد توی این لیست اطلاعات مهمی از یه معامله رو نشون می‌ده، مثل زمان ورود و خروج، اسم سفارش‌ها، قیمت سفارش‌ها و تعداد قراردادها / سهام / لات / واحدها.
علاوه بر این، برای هر معامله میزان سود یا ضرر، سود تجمعی استراتژی، بیشترین سود در طول معامله (run-up) و بیشترین افت سرمایه (drawdown) هم نمایش داده می‌شه.

دقت کن که:

وقتی موس رو روی اطلاعات ورود یا خروج یه معامله توی لیست نگه می‌داری، یه دکمه به اسم “Scroll to bar” ظاهر می‌شه. با کلیک روی اون، چارت اصلی به همون کندلی می‌ره که ورود یا خروج معامله توش انجام شده.
به‌صورت پیش‌فرض، لیست معامله‌ها به‌صورت نزولی نشون داده می‌شه، یعنی جدیدترین معامله بالا قرار می‌گیره. اگه خواستی ترتیب رو عوض کنی، می‌تونی روی دکمه “Trade #” که بالای لیسته کلیک کنی.

Properties
تب “Properties” اطلاعات دقیقی از تنظیمات استراتژی و دیتاستی که روی اون اجرا شده رو نشون می‌ده. این اطلاعات توی چهار بخش قابل جمع‌شدن (collapsible) دسته‌بندی شدن:

  • بخش “Date Range” بازه‌ی تاریخ‌هایی رو نشون می‌ده که توش معاملات شبیه‌سازی شده انجام شدن، به‌علاوه کل بازه‌ای که بک‌تست توش قابل اجرا بوده.
  • بخش “Symbol Info” نماد چارت، تایم‌فریم، نوع چارت، ارزش هر پوینت، واحد پول، اندازه تیک و دقت عددی مشخص‌شده برای چارت رو نمایش می‌ده.
  • بخش “Strategy Inputs” لیست اسم و مقدار همه ورودی‌هایی که توی تب “Settings/Inputs” استراتژی هستن رو نشون می‌ده. این بخش فقط وقتی ظاهر می‌شه که توی اسکریپت از input*() استفاده شده باشه یا پارامتر calc_bars_count توی strategy() مقدار غیر صفر داشته باشه.
  • بخش “Strategy Properties” خلاصه‌ای از تنظیمات استراتژی رو نشون می‌ده، مثل سرمایه اولیه، واحد پول حساب، اندازه سفارش‌ها، مارجین، پیرامیدینگ، کمیسیون، اسلیپیج و بقیه تنظیمات.

Broker emulator

TradingView از یه شبیه‌ساز بروکر (broker emulator) استفاده می‌کنه تا معاملات رو موقع اجرای یه اسکریپت استراتژی شبیه‌سازی کنه. بر خلاف معاملات واقعی، این شبیه‌ساز به‌صورت پیش‌فرض فقط از داده‌های موجود در چارت برای پر کردن سفارش‌ها استفاده می‌کنه. به همین دلیل، سفارش‌ها رو روی کندل‌های گذشته، فقط بعد از بسته شدن اون کندل اجرا می‌کنه. توی کندل‌های زنده (realtime) هم، زودترین زمانی که می‌تونه سفارش رو پر کنه، بعد از اولین تیک قیمتی جدیده. برای اطلاعات بیشتر در مورد این رفتار، می‌تونی صفحه Execution model رو ببینی.

چون شبیه‌ساز فقط از داده‌های قیمتی چارت استفاده می‌کنه، برای پر کردن سفارش‌ها فرض‌هایی درباره‌ی حرکت قیمت داخل کندل (intrabar) در نظر می‌گیره. اینطوری تحلیل می‌کنه:

  • اگه قیمت باز شدن کندل (open) به سقف (high) نزدیک‌تر از کف (low) باشه، شبیه‌ساز فرض می‌کنه ترتیب حرکت قیمت این‌طوری بوده: open → high → low → close
  • اگه قیمت باز شدن کندل به کف نزدیک‌تر باشه، فرض می‌کنه ترتیب حرکت قیمت این‌طوری بوده: open → low → high → close
  • شبیه‌ساز فرض می‌کنه بین قیمت‌های داخل هر کندل (بین high و low) هیچ گپی وجود نداره؛ یعنی هر قیمتی بین این دو مقدار برای اجرای سفارش معتبره.
    موقع پر کردن سفارش‌های مبتنی بر قیمت (یعنی همه سفارش‌ها به‌جز market order)، شبیه‌ساز فرض می‌کنه بین بسته شدن کندل قبلی و باز شدن کندل جدید هیچ دیتایی وجود نداره. بنابراین، اگه قیمت سفارش توی گپ بین دو کندل لمس بشه، شبیه‌ساز اون رو با قیمت باز شدن کندل جدید پر می‌کنه، نه با قیمتی که خودت مشخص کردی.

Bar magnifier

کاربرهایی که پلن Premium یا بالاتر دارن، می‌تونن فرض‌های پیش‌فرض شبیه‌ساز بروکر درباره‌ی قیمت‌های داخل کندل (intrabar) رو با فعال کردن حالت Bar Magnifier توی بک‌تست، تغییر بدن. توی این حالت، شبیه‌ساز از دیتای تایم‌فریم پایین‌تر استفاده می‌کنه تا اطلاعات دقیق‌تری از حرکت قیمت داخل هر کندل به دست بیاره و سفارش‌ها رو دقیق‌تر شبیه‌سازی کنه.

برای فعال کردن حالت Bar Magnifier، می‌تونی توی دستور strategy() این خط رو اضافه کنی:
use_bar_magnifier = true
یا اینکه توی تب “Settings/Properties” استراتژی، داخل بخش “Fill orders” گزینه‌ی “Using bar magnifier” رو فعال کنی.

اسکریپت نمونه زیر نشون می‌ده که Bar Magnifier چطور می‌تونه اجرای سفارش‌ها رو دقیق‌تر کنه. وقتی زمان به مقدار مشخص‌شده (orderTime) برسه، دو تا سفارش لیمیت “Buy” و “Exit” با قیمت‌های محاسبه‌شده (entryPrice و exitPrice) گذاشته می‌شن. برای نشون دادن این اتفاق، اسکریپت پس‌زمینه‌ی چارت رو نارنجی می‌کنه و دو تا خط افقی روی قیمت‌های سفارش رسم می‌کنه.

//@version=6
strategy("Bar Magnifier Demo", overlay = true, use_bar_magnifier = false)

//@variable The UNIX timestamp to place the order at.
int orderTime = timestamp("UTC", 2023, 3, 22, 18)

//@variable Is `color.orange` when `time` crosses the `orderTime`, false otherwise.
color orderColor = na

// Entry and exit prices.
float entryPrice = hl2 - (high - low)
float exitPrice  = entryPrice + (high - low) * 0.25

// Entry and exit lines.
var line entryLine = na
var line exitLine  = na

if ta.cross(time, orderTime)
    // Draw new entry and exit lines.
    entryLine := line.new(bar_index, entryPrice, bar_index + 1, entryPrice, color = color.green, width = 2)
    exitLine  := line.new(bar_index, exitPrice, bar_index + 1, exitPrice, color = color.red, width = 2)

    // Update order highlight color.
    orderColor := color.new(color.orange, 80)

    // Place limit orders at the `entryPrice` and `exitPrice`.
    strategy.entry("Buy", strategy.long, limit = entryPrice)
    strategy.exit("Exit", "Buy", limit = exitPrice)

// Update lines while the position is open.
else if strategy.position_size > 0.0
    entryLine.set_x2(bar_index + 1)
    exitLine.set_x2(bar_index + 1)

bgcolor(orderColor)

چون توی این اسکریپت از آرگومان use_bar_magnifier توی تابع strategy() استفاده نشده، شبیه‌ساز بروکر طبق فرض‌های پیش‌فرض خودش سفارش‌ها رو پر می‌کنه: یعنی قیمت کندل از open به high، بعد به low، و در نهایت به close حرکت کرده. پس وقتی سفارش "Buy" در قیمت مشخص‌شده (خط سبز) پر می‌شه، شبیه‌ساز فرض می‌کنه قیمت دیگه بالا نرفته که به خط قرمز برسه و سفارش "Exit" اجرا بشه. یعنی طبق فرض شبیه‌ساز، ورود و خروج توی همون کندل ممکن نیست.

ولی اگه حالت Bar Magnifier رو فعال کنیم، شبیه‌ساز می‌تونه از دیتای تایم‌فریم پایین‌تر (مثلاً ۱۰ دقیقه‌ای) روی چارت ۱ ساعته استفاده کنه، و به‌جای اینکه به فرض‌هاش تکیه کنه، واقعاً بررسی کنه که قیمت چطور حرکت کرده. توی این بازه دقیق‌تر، قیمت بعد از رسیدن به سفارش "Buy"، دوباره بالا رفته و به سفارش "Exit" هم رسیده.
بنابراین، با Bar Magnifier فعال، هر دو سفارش روی همون کندل ساعتی اجرا می‌شن.

توجه کن!

اسکریپت‌ها حداکثر می‌تونن ۲۰۰,۰۰۰ کندل از یه تایم‌فریم پایین‌تر دریافت کنن. به‌خاطر این محدودیت، بعضی از نمادهایی که تاریخچه‌ی طولانی‌تری دارن ممکنه برای کندل‌های ابتدایی چارتشون، دیتای داخل‌ کندلی (intrabar) در دسترس نداشته باشن.
فعال کردن حالت Bar Magnifier روی معامله‌هایی که کندل‌هاشون دیتای intrabar ندارن، هیچ تأثیری نمی‌ذاره.


Orders and trades

استراتژی‌های Pine Script™ برای انجام معاملات و مدیریت پوزیشن‌ها از سفارش (order) استفاده می‌کنن، دقیقاً مثل دنیای واقعی. توی این فضا، "order" یعنی دستوریه که استراتژی برای اجرای یه عمل بازار به شبیه‌ساز بروکر می‌فرسته، و "trade" هم نتیجه اون سفارشه وقتی که شبیه‌ساز اونو اجرا می‌کنه.

بیایم دقیق‌تر ببینیم سفارش‌های استراتژی چطور کار می‌کنن و چطور به معامله تبدیل می‌شن. توی اسکریپت زیر، هر ۲۰ کندل یه سفارش خرید بازار (market order) با تابع strategy.entry() ایجاد می‌شه و یه لیبل هم روی چارت نشون داده می‌شه. همچنین توی هر کندل، تابع strategy.close_all() صدا زده می‌شه تا اگه پوزیشنی باز بود، با یه سفارش بازار بسته بشه:

//@version=6
strategy("Order execution demo", "My strategy", true, margin_long = 100, margin_short = 100)

//@function نمایش متن مشخص‌شده در یک لیبل روی قیمت high کندل فعلی
debugLabel(string txt) => 
    label.new(
         bar_index, high, text = txt, color=color.lime, style = label.style_label_lower_right, 
         textcolor = color.black, size = size.large
     )

//@variable در هر ۲۰مین کندل true هست، در غیر این صورت false
bool longCondition = bar_index % 20 == 0

// وقتی longCondition برقرار باشه، یه لیبل نشون بده و سفارش خرید بازار ثبت کن
if longCondition
    debugLabel("Long entry order created")
    strategy.entry("My Long Entry Id", strategy.long)

// در هر کندل، اگه پوزیشنی باز بود، با سفارش بازار ببندش
strategy.close_all()

توجه کن که:

با اینکه اسکریپت توی هر کندل تابع strategy.close_all() رو صدا می‌زنه، این تابع فقط زمانی یه سفارش خروج جدید می‌سازه که پوزیشن باز وجود داشته باشه. اگه پوزیشنی باز نباشه، این فراخوانی هیچ تأثیری نداره.

فلش‌های آبی روی چارت نشون می‌دن که استراتژی کجا وارد پوزیشن خرید (لانگ) شده، و فلش‌های بنفش مشخص می‌کنن که پوزیشن در کدوم کندل بسته شده. دقت کن که لیبل‌ها یه کندل قبل از فلش ورود ظاهر می‌شن، و فلش ورود هم یه کندل قبل از فلش خروج دیده می‌شه. این ترتیب نشون می‌ده که فرآیند ایجاد سفارش و اجرای اون چطوری انجام می‌شه.

به‌صورت پیش‌فرض، زودترین زمانی که شبیه‌ساز بروکر می‌تونه یه سفارش رو پر کنه، تیک قیمتی بعدیه. چون ساختن و پر کردن سفارش توی یه تیک کاملاً غیرواقعیه. از اونجایی که استراتژی‌ها به‌طور پیش‌فرض بعد از بسته شدن هر کندل دوباره محاسبه می‌شن، اولین تیک موجود برای پر کردن سفارش، قیمت باز شدن کندل بعدیه.
مثلاً وقتی شرط longCondition توی کندل شماره ۲۰ برقرار می‌شه، اسکریپت یه سفارش ورود ایجاد می‌کنه که باید توی تیک بعدی پر بشه، یعنی قیمت باز شدن کندل ۲۱. بعد که کندل ۲۱ بسته می‌شه و اسکریپت دوباره اجرا می‌شه، یه سفارش خروج ثبت می‌کنه که باید توی تیک بعدی پر بشه، یعنی قیمت باز شدن کندل ۲۲.


Order types

استراتژی‌های Pine Script™ می‌تونن انواع مختلفی از سفارش‌ها رو شبیه‌سازی کنن تا با نیازهای خاص سیستم معاملاتی هماهنگ باشن. مهم‌ترین انواع سفارش‌ها شامل موارد زیر هستن:

  • Market
  • Limit
  • Stop
  • Stop-limit

Market orders

ساده‌ترین نوع سفارش، سفارش بازار (market order) هست که بیشتر دستورات ثبت سفارش به‌صورت پیش‌فرض از همین نوع استفاده می‌کنن.
یه سفارش بازار یعنی دستور خرید یا فروش یه دارایی در سریع‌ترین زمان ممکن، بدون توجه به قیمت. برای همین، شبیه‌ساز بروکر همیشه سفارش‌های بازار رو توی اولین تیک قیمتی بعدی اجرا می‌کنه.

توی مثال پایین، اسکریپت به‌صورت یکی‌درمیون یه سفارش خرید (لانگ) یا فروش (شورت) بازار ثبت می‌کنه، اونم هر چند کندلی یه‌بار که مقدارش با lengthInput مشخص شده.
وقتی bar_index مضرب ۲ برابر lengthInput باشه، یه سفارش لانگ ایجاد می‌شه. در غیر این صورت، وقتی bar_index فقط مضرب lengthInput باشه، یه سفارش شورت ثبت می‌شه:

//@version=6
strategy("Market order demo", overlay = true, margin_long = 100, margin_short = 100)

//@variable تعداد کندل‌هایی که بین ورود لانگ و شورت فاصله هست
int lengthInput = input.int(10, "Cycle length", 1)

//@function نمایش متن مشخص‌شده در یک لیبل روی کندل فعلی
debugLabel(string txt, color lblColor) => label.new(
     bar_index, high, text = txt, color = lblColor, textcolor = color.white, 
     style = label.style_label_lower_right, size = size.large
 )

//@variable هر `2 * lengthInput` کندل، true می‌شه
longCondition = bar_index % (2 * lengthInput) == 0
//@variable هر `lengthInput` کندل، true می‌شه
shortCondition = bar_index % lengthInput == 0

// اگه longCondition برقرار باشه، سفارش لانگ بازار ثبت کن
if longCondition
    debugLabel("Long market order created", color.green)
    strategy.entry("My Long Entry Id", strategy.long)
// اگه نه، اگه shortCondition برقرار باشه، سفارش شورت بازار ثبت کن
else if shortCondition
    debugLabel("Short market order created", color.red)
    strategy.entry("My Short Entry Id", strategy.short)

نکته:

  • لیبل‌ها نشون می‌دن که اسکریپت توی کدوم کندل سفارش بازار رو ایجاد کرده. ولی شبیه‌ساز اون سفارش‌ها رو در قیمت باز شدن کندل بعدی اجرا می‌کنه.
  • دستور strategy.entry() می‌تونه به‌صورت خودکار، یه پوزیشن باز رو ببنده و در جهت مخالف یه پوزیشن جدید باز کنه. (پایین‌تر درباره این موضوع توضیح داده شده.)

Limit orders

سفارش Limit (لیمیت) یعنی دستور خرید یا فروش یه دارایی در یه قیمت مشخص یا بهتر از اون.

  • برای سفارش خرید (لانگ)، یعنی قیمت باید کمتر یا مساوی قیمت مشخص‌شده باشه.
  • برای سفارش فروش (شورت)، یعنی قیمت باید بیشتر یا مساوی قیمت مشخص‌شده باشه.
    مهم نیست این اتفاق چه زمانی بیفته.

برای شبیه‌سازی یه سفارش Limit توی اسکریپت استراتژی، باید مقدار قیمت رو به پارامتر limit توی دستور ثبت سفارش بدی.

وقتی قیمت بازار به قیمت Limit برسه یا از اون در جهت مناسب عبور کنه، شبیه‌ساز بروکر اون سفارش رو با همون قیمت یا بهتر پر می‌کنه. اما اگه قیمت Limit از قیمت فعلی بازار بدتر باشه (برای لانگ: بالاتر، برای شورت: پایین‌تر)، شبیه‌ساز اون سفارش رو بلافاصله پر می‌کنه، بدون اینکه منتظر رسیدن قیمت بمونه.

مثلاً توی مثال زیر، اسکریپت یه سفارش لانگ لیمیت رو ۸۰۰ تیک پایین‌تر از قیمت بسته شدن کندلِ ۱۰۰ کندل قبل از آخرین کندل چارت ایجاد می‌کنه. با strategy.entry() این سفارش ساخته می‌شه. یه لیبل نشون می‌ده سفارش کجا ایجاد شده و یه خط برای نمایش قیمت سفارش کشیده می‌شه:

//@version=6
strategy("Limit order demo", overlay = true, margin_long = 100, margin_short = 100)

//@function نشون دادن یه متن همراه با خط افقی روی قیمت مشخص‌شده
debugLabel(float price, string 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
     )

// ۱۰۰ کندل قبل از آخرین کندل چارت، یه سفارش لیمیت لانگ ایجاد کن
if last_bar_index - bar_index == 100
    limitPrice = close - syminfo.mintick * 800
    debugLabel(limitPrice, "Long Limit order created")
    strategy.entry("Long", strategy.long, limit = limitPrice)

توجه کن که توی چارت بالا، لیبل و شروع خط چندین کندل قبل از علامت ورود “Long” ظاهر شدن. چون تا وقتی قیمت بازار بالاتر از limitPrice بود، شبیه‌ساز بروکر نتونست سفارش رو پر کنه؛ چون این قیمت برای یه معامله لانگ، قیمت بدتری محسوب می‌شه.
بعد از اینکه قیمت افت کرد و به limitPrice رسید، شبیه‌ساز سفارش رو وسط همون کندل (mid-bar) با اون قیمت پر کرد.
حالا اگه limitPrice رو به قیمتی بالاتر از قیمت بسته شدن کندل تنظیم کنیم (به‌جای اینکه پایین‌تر باشه)، شبیه‌ساز سفارش رو در قیمت باز شدن کندل بعدی پر می‌کنه. چون قیمت بسته شدن کندل قبلی قبلاً قیمت بهتری برای معامله لانگ بوده.

توی این حالت، ما توی اسکریپت limitPrice رو ۸۰۰ تیک بالاتر از قیمت بسته شدن کندل قرار دادیم تا این تأثیر رو نشون بدیم.

//@version=6
strategy("Limit order demo", overlay = true, margin_long = 100, margin_short = 100)

//@function نشون دادن متن همراه با یه خط افقی روی قیمت مشخص‌شده
debugLabel(float price, string 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
     )

// ۱۰۰ کندل قبل از آخرین کندل چارت، سفارش لیمیت لانگ ایجاد کن با قیمتی بالاتر از قیمت بسته شدن
if last_bar_index - bar_index == 100
    limitPrice = close + syminfo.mintick * 800
    debugLabel(limitPrice, "Long Limit order created")
    strategy.entry("Long", strategy.long, limit = limitPrice)

Stop و Stop-limit orders

یه سفارش Stop دستوریه که وقتی قیمت بازار به یه سطح مشخص یا قیمتی بعدتر از اون برسه، یه سفارش جدید (بازار یا لیمیت) رو فعال می‌کنه.

  • برای سفارش‌های خرید (لانگ)، این یعنی قیمت بازار باید بالاتر یا مساوی قیمت Stop باشه.
  • برای سفارش‌های فروش (شورت)، باید پایین‌تر یا مساوی باشه.

برای شبیه‌سازی یه سفارش Stop توی Pine Script، باید مقدار قیمت رو به پارامتر stop توی دستور ثبت سفارش بدی.

اگه یه سفارش Stop با قیمتی بهتر از قیمت فعلی بازار ساخته بشه، شبیه‌ساز اون رو بلافاصله فعال می‌کنه، حتی اگه قیمت بازار هنوز به اون سطح نرسیده باشه.

مثال زیر با استفاده از strategy.entry() یه سفارش Stop لانگ رو در قیمتی که ۸۰۰ تیک بالاتر از قیمت بسته‌شدن کندلِ ۱۰۰ کندل قبل از آخرین کندل چارت هست ایجاد می‌کنه.
یه لیبل روی همون کندلی که سفارش ساخته شده نمایش داده می‌شه و یه خط افقی هم برای نشون دادن قیمت Stop رسم می‌شه.
همون‌طور که توی چارت می‌بینی، استراتژی درست بعد از اینکه قیمت از سطح Stop عبور می‌کنه، وارد پوزیشن لانگ می‌شه:

//@version=6
strategy("Stop order demo", overlay = true, margin_long = 100, margin_short = 100)

//@function نمایش متن و قیمت Stop روی چارت
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
     )

// ۱۰۰ کندل قبل از آخرین کندل چارت، سفارش Stop لانگ ایجاد کن
if last_bar_index - bar_index == 100
    stopPrice = close + syminfo.mintick * 800
    debugLabel(stopPrice, "Long Stop order created")
    strategy.entry("Long", strategy.long, stop = stopPrice)

توجه کن:

سفارش Stop از نظر اجرا شدن نسبت به قیمت بازار، برعکس سفارش Limit عمل می‌کنه.
تو همین سناریو، اگه به‌جای Stop از Limit استفاده می‌کردیم، سفارش توی کندل بعدی بلافاصله اجرا می‌شد.
(برای این مورد، مثالش توی بخش قبلی اومده بود.)

وقتی که در strategy.entry() یا strategy.order() هم stop و هم limit مشخص بشن، اون سفارش به یه سفارش Stop-Limit تبدیل می‌شه. برخلاف سفارش Stop ساده که وقتی قیمت به سطح Stop یا بدتر از اون می‌رسه، یه سفارش بازار فعال می‌کنه، سفارش Stop-Limit به‌جاش یه سفارش Limit فعال می‌کنه که فقط در قیمت Limit مشخص‌شده یا بهتر از اون پر می‌شه.

در مثال زیر، اسکریپت قبلی تغییر داده شده تا یه سفارش Stop-Limit لانگ رو شبیه‌سازی و نمایش بده. توی این نسخه، low کندل به‌عنوان قیمت Limit داخل دستور strategy.entry() استفاده شده. همچنین چند تا رسم اضافه شده که نشون می‌ده سفارش Limit کجا فعال شده و قیمت Limit چی بوده.

توی چارت مثال:

می‌بینی که قیمت بازار توی کندل بعد از ایجاد سفارش به سطح Limit رسید، ولی استراتژی هنوز وارد پوزیشن نشد، چون سفارش Limit هنوز فعال نشده بود. بعدتر، وقتی قیمت به سطح Stop رسید، استراتژی سفارش Limit رو فعال کرد. و بعد از اینکه قیمت دوباره اومد پایین و به سطح Limit رسید، شبیه‌ساز بروکر اون رو اجرا کرد.

//@version=6
strategy("Stop-Limit order demo", overlay = true, margin_long = 100, margin_short = 100)

//@function نمایش متن و سطوح قیمتی Stop و Limit روی چارت
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.large
     )
    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

// ۱۰۰ کندل قبل از آخرین کندل چارت، سفارش Stop-Limit لانگ ایجاد کن
if last_bar_index - bar_index == 100
    stopPrice  := close + syminfo.mintick * 800
    limitPrice := low
    debugLabel(limitPrice, "", color.gray)
    debugLabel(stopPrice, "Long Stop-Limit order created", color.teal)
    strategy.entry("Long", strategy.long, stop = stopPrice, limit = limitPrice)

// وقتی قیمت به سطح Stop برسه، سفارش Limit فعال می‌شه، لیبل و خط نمایش بده
if high >= stopPrice
    debugLabel(limitPrice, "Limit order activated", color.green, 2)
    stopPrice := na

ثبت و لغو سفارش‌ها

توی فضای strategy.*، پنج تا تابع هست که شبیه‌سازی ثبت سفارش‌ها رو انجام می‌دن. به این توابع می‌گن دستورات ثبت سفارش (order placement commands). این‌ها عبارت‌ان از:

  • strategy.entry()
  • strategy.order()
  • strategy.exit()
  • strategy.close()
  • strategy.close_all()

علاوه بر این‌ها، دو تا تابع هم برای لغو سفارش‌هایی که هنوز فعال نشده‌ن (pending orders) وجود داره که بهشون می‌گن دستورات لغو سفارش (order cancellation commands). این دو تا هستن:

  • strategy.cancel()
  • strategy.cancel_all()

توی بخش‌های بعدی، هرکدوم از این دستورها، ویژگی‌های خاص‌شون و اینکه چطوری باید ازشون استفاده کنی توضیح داده می‌شن.

# strategy.entry()

دستور `strategy.entry()`برای ایجاد سفارش ورود (entry order) استفاده می‌شه. این دستور یه‌سری ویژگی خاص داره که کار باز کردن و مدیریت پوزیشن رو ساده‌تر می‌کنه.

به‌صورت پیش‌فرض، این دستور یه سفارش بازار (market order) می‌سازه. ولی اگه پارامترهای limit یا stop رو هم براش مشخص کنی، می‌تونه سفارش لیمیت (limit)، استاپ (stop) یا استاپ-لیمیت (stop-limit) هم ایجاد کنه، همون‌طور که توی بخش "Order types" بالا توضیح داده شد.

برگشت دادن پوزیشن (Reversing positions)

یکی از قابلیت‌های خاص دستور strategy.entry() اینه که می‌تونه به‌صورت خودکار یه پوزیشن باز در جهت مخالف رو ببنده و یه پوزیشن جدید در جهت جدید باز کنه.

به‌صورت پیش‌فرض، وقتی یه سفارش strategy.entry() اجرا می‌شه و در همون لحظه یه پوزیشن در جهت مخالف وجود داره، این دستور اندازه‌ی پوزیشن فعلی رو به اندازه سفارش جدید اضافه می‌کنه.
این باعث می‌شه اول پوزیشن قبلی بسته بشه و بعد یه پوزیشن جدید با اندازه مشخص‌شده توی جهت جدید باز بشه.

مثلاً اگه یه استراتژی الان یه پوزیشن لانگ ۱۵ سهمی داشته باشه و strategy.entry() یه سفارش شورت با اندازه ۵ سهم صادر کنه، اندازه‌ی واقعی معامله ۲۰ سهم خواهد بود (۱۵ برای بستن لانگ + ۵ برای باز کردن شورت).

مثال زیر این رفتار رو نشون می‌ده:

  • هر ۱۰۰ کندل، شرط buyCondition برقرار می‌شه و یه سفارش خرید لانگ ۱۵ سهمی ثبت می‌شه.
  • هر ۵۰ کندل، شرط sellCondition برقرار می‌شه و یه سفارش شورت ۵ سهمی ثبت می‌شه.
  • رنگ پس‌زمینه‌ی چارت هم برای نمایش هر دو شرط رنگی می‌شه:چ
//@version=6
strategy("Reversing positions demo", overlay = true)

//@variable هر ۱۰۰ کندل true می‌شه
bool buyCondition = bar_index % 100 == 0
//@variable هر ۵۰ کندل true می‌شه
bool sellCondition = bar_index % 50 == 0
 
if buyCondition
    // سفارش خرید بازار برای بستن پوزیشن شورت و باز کردن لانگ ۱۵ سهمی
    strategy.entry("buy", strategy.long, qty = 15)
else if sellCondition
    // سفارش فروش بازار برای بستن پوزیشن لانگ و باز کردن شورت ۵ سهمی
    strategy.entry("sell", strategy.short, qty = 5)

// پس‌زمینه چارت رو رنگی کن وقتی buyCondition یا sellCondition فعال می‌شن
bgcolor(buyCondition  ? color.new(color.blue, 90) : sellCondition ? color.new(color.red, 90) : na)

نشانگرهای معامله (trade markers) روی چارت اندازه‌ی تراکنش رو نشون می‌دن، نه اندازه‌ی نهایی پوزیشن. توی چارت بالا می‌بینی که اندازه هر تراکنش ۲۰ سهم بوده، نه ۱۵ برای ورود لانگ و ۵ برای ورود شورت.

دلیلش اینه که strategy.entry() به‌صورت پیش‌فرض وقتی یه پوزیشن در جهت مخالف بازه، اونو می‌بنده و یه پوزیشن جدید در جهت جدید با اندازه مشخص‌شده باز می‌کنه.
برای این کار، اندازه پوزیشن باز رو به اندازه سفارش جدید اضافه می‌کنه. مثلاً:

  • اگه یه پوزیشن لانگ ۱۵ سهمی داشته باشی
  • و بخوای یه سفارش شورت ۵ سهمی بدی
  • strategy.entry() یه سفارش ۲۰ سهمی ثبت می‌کنه (۱۵ برای بستن لانگ + ۵ برای باز کردن شورت)

در نتیجه، هر کدوم از این تراکنش‌ها ۲۰ سهمه، ولی پوزیشن نهایی بعدش می‌شه:

  • ۵ سهم شورت بعد از ورود شورت
  • ۱۵ سهم لانگ بعد از ورود لانگ

توجه کن:

تابع strategy.risk.allow_entry_in() می‌تونه جهت مجاز برای ورود سفارش رو کنترل کنه.
اگه توی اسکریپت از این تابع استفاده بشه و جهت خاصی رو مشخص کنه، وقتی strategy.entry() یه سفارش در جهت مخالف بده:

  • فقط پوزیشن فعلی بسته می‌شه
  • و دیگه اجازه باز شدن پوزیشن در جهت مخالف داده نمی‌شه (یعنی ریورسال انجام نمی‌شه)

Pyramiding

یکی دیگه از ویژگی‌های خاص دستور strategy.entry() اینه که با ویژگی pyramiding استراتژی در ارتباطه.
Pyramiding مشخص می‌کنه که یه استراتژی حداکثر چند بار می‌تونه پشت سر هم توی یه جهت وارد پوزیشن بشه.

برای تنظیم این ویژگی، می‌تونی مقدار pyramiding رو توی دستور strategy() بنویسی، یا اینکه از تب Settings > Properties اسکریپت توی قسمت “Pyramiding” مقدارش رو مشخص کنی.

مقدار پیش‌فرضش ۱ هست. یعنی استراتژی فقط می‌تونه یه پوزیشن باز کنه، ولی نمی‌تونه با strategy.entry() به همون پوزیشن چیزی اضافه کنه.

مثال پایین با strategy.entry() هر ۲۵ تا کندل یه سفارش بازار ثبت می‌کنه.
جهت سفارش‌ها هر ۱۰۰ کندل عوض می‌شه. یعنی توی هر چرخه ۱۰۰ کندلی، چهار تا سفارش با یه جهت (همه لانگ یا همه شورت) ثبت می‌شن.

برای اینکه راحت‌تر بتونی بفهمی هر سفارش توی چه جهتی بوده، اسکریپت پس‌زمینه‌ی چارت رو رنگی می‌کنه:
آبی برای لانگ، قرمز برای شورت.

//@version=6
strategy("Pyramiding demo", overlay = true)

// جهت سفارش‌ها: ۱ برای لانگ، -۱ برای شورت
var int direction = 1
// شرط ورود: هر ۲۵ کندل یک بار
bool entryCondition = bar_index % 25 == 0

// هر ۱۰۰ کندل، جهت رو عوض کن
if bar_index % 100 == 0
    direction *= -1

// وقتی شرط ورود برقرار باشه، یه سفارش بازار توی جهت فعلی ثبت کن
if entryCondition
    strategy.entry("Entry", direction == 1 ? strategy.long : strategy.short)

// رنگ پس‌زمینه: آبی برای لانگ، قرمز برای شورت
color bgColor = entryCondition ? (direction == 1 ? color.new(color.blue, 80) : color.new(color.red, 80)) : na
bgcolor(bgColor, title = "Background highlight")

دقت کن که:

با اینکه توی اسکریپت قبلی، دستور strategy.entry() چهار بار توی یه چرخه ۱۰۰ کندلی با یه جهت یکسان اجرا می‌شد، استراتژی بعد از هر بار فراخوانی سفارش اجرا نمی‌کرد. دلیلش اینه که مقدار پیش‌فرض pyramiding برابر با ۱ هست. یعنی استراتژی فقط اجازه داره یه معامله در هر جهت باز کنه و نمی‌تونه چند بار پشت سر هم توی یه جهت وارد بشه.

توی مثال پایین، اسکریپت رو طوری تغییر دادیم که مقدار pyramiding = 4 رو توی دستور strategy() قرار دادیم. این باعث می‌شه استراتژی بتونه تا چهار بار پشت سر هم توی یه جهت معامله کنه.

حالا با این تنظیم، بعد از هر بار فراخوانی strategy.entry()، یه معامله انجام می‌شه:

//@version=6
strategy("Pyramiding demo", overlay = true, pyramiding = 4)

// جهت سفارش‌ها: ۱ برای لانگ، -۱ برای شورت
var int direction = 1
// شرط ورود: هر ۲۵ کندل یک بار
bool entryCondition = bar_index % 25 == 0

// هر ۱۰۰ کندل، جهت رو عوض کن
if bar_index % 100 == 0
    direction *= -1

// وقتی شرط ورود برقرار باشه، یه سفارش بازار توی جهت فعلی ثبت کن
if entryCondition
    strategy.entry("Entry", direction == 1 ? strategy.long : strategy.short)

// رنگ پس‌زمینه: آبی برای لانگ، قرمز برای شورت
color bgColor = entryCondition ? (direction == 1 ? color.new(color.blue, 80) : color.new(color.red, 80)) : na
bgcolor(bgColor, title = "Background highlight")

# strategy.order()

دستور strategy.order() یه سفارش ساده ایجاد می‌کنه. بر خلاف بقیه‌ی دستورات ثبت سفارش که ممکنه بسته به ویژگی‌های استراتژی (مثل pyramiding) یا پوزیشن‌های باز رفتار متفاوتی داشته باشن، این دستور بیشتر این ویژگی‌ها رو نادیده می‌گیره و فقط با همون پارامترهایی که بهش دادی یه سفارش می‌سازه.

به‌صورت پیش‌فرض، این دستور یه سفارش بازار (market order) ایجاد می‌کنه، ولی می‌تونه با پارامترهای limit و stop، سفارش‌های لیمیت، استاپ، یا استاپ-لیمیت هم بسازه.

سفارش‌هایی که با strategy.order() ساخته می‌شن می‌تونن:

  • پوزیشن جدید باز کنن
  • پوزیشن قبلی رو تغییر بدن
  • یا حتی اون رو ببندن

وقتی این دستور اجرا می‌شه، موقعیت نهایی (position) ترکیبی از پوزیشن باز قبلی و مقدار سفارش جدید می‌شه (یعنی جمع یا تفاضل اون‌ها).

مثالی که پایین می‌بینی از strategy.order() برای ورود و خروج استفاده می‌کنه:

  • هر ۱۰۰ تا کندل، یه سفارش خرید بازار ۱۵ واحدی ثبت می‌کنه
  • هر ۲۵ کندل (به‌جز اونایی که خودشون مضرب ۱۰۰ هستن)، یه سفارش فروش ۵ واحدی ثبت می‌کنه
  • پس‌زمینه‌ی چارت هم رنگی می‌شه تا زمان ثبت سفارش‌ها رو مشخص کنه:
//@version=6
strategy("`strategy.order()` demo", overlay = true)

// هر ۱۰۰ کندل true می‌شه
bool buyCondition = bar_index % 100 == 0
// هر ۲۵ کندل true می‌شه
bool sellCondition = bar_index % 25 == 0

if buyCondition
    // سفارش خرید بازار ۱۵ واحدی ثبت کن
    strategy.order("buy", strategy.long, qty = 15)
else if sellCondition
    // سفارش فروش بازار ۵ واحدی ثبت کن
    strategy.order("sell", strategy.short, qty = 5)

// پس‌زمینه چارت رو رنگی کن بر اساس نوع سفارش
bgcolor(buyCondition ? color.new(color.blue, 90) : sellCondition ? color.new(color.red, 90) : na)

توی این استراتژی، هیچ‌وقت پوزیشن شورت باز نمی‌شه.
بر خلاف strategy.entry() که به‌صورت خودکار پوزیشن رو برعکس می‌کنه (ریورسال)، دستور strategy.order() این کار رو انجام نمی‌ده.

مثلاً:

  • بعد از اجرای یه سفارش خرید، پوزیشن لانگ ۱۵ واحدی باز می‌شه
  • بعدش سه تا سفارش فروش ۵ واحدی اجرا می‌شن
  • در مجموع ۱۵ - (۵×۳) = ۰، یعنی پوزیشن به صفر می‌رسه و بسته می‌شه

اگه توی همین مثال از strategy.entry() استفاده می‌کردیم، استراتژی بین پوزیشن‌های لانگ ۱۵ واحدی و شورت ۵ واحدی جابه‌جا می‌شد.

# strategy.exit()

دستور strategy.exit() برای ساختن سفارش خروج (exit order) استفاده می‌شه. این دستور رفتارهای خاصی داره که به پوزیشن‌های باز وصل می‌شن و باعث می‌شن بستن پوزیشن‌ها یا ساختن خروج‌های چندمرحله‌ای (مثل حد سود، حد ضرر یا تریلینگ استاپ) راحت‌تر انجام بشه.

بر خلاف بقیه دستورات ثبت سفارش که معمولاً فقط یه سفارش تولید می‌کنن، یه فراخوانی strategy.exit() می‌تونه بیش از یک نوع سفارش خروج بسازه، بسته به اینکه چه پارامترهایی براش مشخص شده باشه.

علاوه بر این، یه دستور strategy.exit() می‌تونه برای چند پوزیشن ورودی مختلف سفارش خروج ایجاد کنه، البته اگه توی پارامتر from_entry مشخص شده باشه و استراتژی چند پوزیشن باز داشته باشه.

Take-profit و Stop-loss

ساده‌ترین استفاده‌ای که از دستور strategy.exit() می‌شه، اینه که باهاش سفارش‌هایی بسازی برای خروج از پوزیشن:
یا وقتی به اندازه کافی سود کردی (take-profit)،
یا وقتی زیادی ضرر کردی (stop-loss)،
یا هر دو مورد با هم (که بهش می‌گن bracket).

چهار تا پارامتر هست که قیمت سفارش‌های حد سود و حد ضرر رو تعیین می‌کنه:

  1. پارامترهای profit و loss مقدار نسبی دارن؛ یعنی تعداد تیکی که قیمت باید از قیمت ورود فاصله بگیره تا سفارش فعال بشه.
  2. پارامترهای limit و stop مقدار مطلق دارن؛ یعنی دقیقاً یه قیمت خاص که اگه بازار بهش برسه، خروج انجام می‌شه.

اگه توی strategy.exit() هم مقدار نسبی بدی (مثلاً profit) و هم مقدار مطلق (مثلاً limit) برای یه سطح مشخص (مثلاً حد سود)، فقط اون سفارشی ساخته می‌شه که احتمال بیشتری داره زودتر فعال بشه.

مثال:

  • اگه profit برابر ۱۹ تیک باشه و limit برابر ۲۰ تیک بالاتر از قیمت ورود، چون قیمت اول به ۱۹ تیک می‌رسه، سفارش با profit ساخته می‌شه.
  • ولی اگه برعکس باشه (یعنی profit برابر ۲۰ تیک و limit فقط ۱۹ تیک)، اون‌وقت سفارش با limit ساخته می‌شه.

دقت کن:

پارامترهای limit و stop توی دستور strategy.exit() دقیقاً مثل همون پارامترها توی strategy.entry() یا strategy.order() عمل نمی‌کنن.

  • توی strategy.entry() یا strategy.order()، وقتی هم limit و هم stop رو با هم بدی، فقط یه سفارش stop-limit ساخته می‌شه.
  • ولی توی strategy.exit() اگه هر دو رو بدی، دو تا سفارش جدا ساخته می‌شه:
    • یکی سفارش حد سود (limit)
    • یکی سفارش حد ضرر (stop)

مثال زیر با استفاده از دستور strategy.exit()، یه سفارش bracket می‌سازه که هم حد سود (take-profit) داره و هم حد ضرر (stop-loss).

وقتی شرط buyCondition برقرار بشه، اسکریپت:

  • با strategy.entry() یه سفارش بازار برای خرید ثبت می‌کنه
  • با strategy.exit() دو تا سفارش خروج می‌سازه:
    • یکی روی قیمت takeProfit (برای بستن با سود)
    • یکی روی قیمت stopLoss (برای بستن با ضرر)

مقادیر takeProfit و stopLoss روی چارت با دایره نشون داده می‌شن تا قیمت‌های خروج رو به‌صورت کامل ببینی:

//@version=6
strategy("Take-profit and stop-loss demo", overlay = true)

// شرط ورود: هر ۱۰۰ کندل
bool buyCondition = bar_index % 100 == 0

// متغیر برای ذخیره قیمت‌های حد سود و حد ضرر
var float takeProfit = na
var float stopLoss = na

if buyCondition
    // فقط اگه هنوز پوزیشنی باز نیست، قیمت‌های حد سود و ضرر رو محاسبه کن
    if strategy.opentrades == 0
        takeProfit := close * 1.01  // حد سود: ۱ درصد بالاتر از قیمت بسته‌شدن
        stopLoss   := close * 0.99  // حد ضرر: ۱ درصد پایین‌تر از قیمت بسته‌شدن
    // ثبت سفارش خرید بازار
    strategy.entry("buy", strategy.long)
    // ثبت سفارش حد سود و حد ضرر
    strategy.exit("exit", "buy", limit = takeProfit, stop = stopLoss)

// وقتی پوزیشن بسته شد، مقادیر حد سود و ضرر رو پاک کن
if ta.change(strategy.closedtrades) > 0
    takeProfit := na
    stopLoss   := na

// رسم کردن قیمت‌های حد سود و حد ضرر روی چارت
plot(takeProfit, "TP", color.green, style = plot.style_circles)
plot(stopLoss, "SL", color.red, style = plot.style_circles)

دقت کن:

  • توی دستور strategy.exit() هیچ پارامتر qty یا qty_percent مشخص نکردیم، یعنی سفارش‌های خروجی که ساخته می‌شن قراره ۱۰۰٪ حجم سفارش "buy" رو ببندن.
  • سفارش‌های خروجی که با strategy.exit() ساخته می‌شن الزاماً دقیقاً روی همون قیمتی که مشخص کردی اجرا نمی‌شن.
    ممکنه سفارش‌های لیمیت با قیمت بهتر پر بشن
    و سفارش‌های استاپ با قیمت بدتر،
    بستگی داره که توی اون لحظه چه محدوده قیمتی‌ای برای شبیه‌ساز بروکر در دسترس بوده.

وقتی توی strategy.exit() از آرگومان from_entry استفاده کنی، سفارش‌های خروج فقط روی اون معامله‌هایی اعمال می‌شن که IDشون با مقدار from_entry یکی باشه.
اگه مقدار from_entry با هیچ‌کدوم از IDهای ورود فعلی پوزیشن هم‌خوانی نداشته باشه، هیچ سفارشی ساخته نمی‌شه.

مثلاً توی اسکریپت پایین، مقدار from_entry توی strategy.exit() برابر با "buy2" هست.
ولی هیچ سفارش ورودی با ID "buy2" وجود نداره، چون ما توی strategy.entry() فقط "buy" گذاشتیم.

پس نتیجه این می‌شه که هیچ سفارشی برای خروج ساخته نمی‌شه:

//@version=6
strategy("Invalid `from_entry` ID demo", overlay = true)

// شرط ورود: هر ۱۰۰ کندل
bool buyCondition = bar_index % 100 == 0

// قیمت‌های حد سود و ضرر
var float takeProfit = na
var float stopLoss = na

if buyCondition
    // قبل از ورود، مقادیر حد سود و ضرر رو محاسبه کن
    if strategy.opentrades == 0
        takeProfit := close * 1.01
        stopLoss   := close * 0.99
    // سفارش خرید با ID "buy"
    strategy.entry("buy", strategy.long)
    // تلاش برای ثبت سفارش خروج برای ID اشتباه "buy2"
    strategy.exit("exit", "buy2", limit = takeProfit, stop = stopLoss)

// وقتی پوزیشن بسته شد، مقادیر رو پاک کن
if ta.change(strategy.closedtrades) > 0
    takeProfit := na
    stopLoss   := na

// رسم قیمت‌های حد سود و ضرر
plot(takeProfit, "TP", color.green, style = plot.style_circles)
plot(stopLoss, "SL", color.red, style = plot.style_circles)

نکته مهم:

اگه توی strategy.exit() هیچ آرگومان from_entry مشخص نکنی، اون‌وقت سفارش خروج برای همه‌ی پوزیشن‌های باز فعلی ساخته می‌شه، فارغ از اینکه ID ورودشون چی بوده.
(در ادامه توی بخش «خروج برای چند ورود» بیشتر درباره‌ش توضیح داده می‌شه.)

خروج پله‌ای و جزئی (Partial and multi-level exits)

توی استراتژی‌ها می‌تونی چند بار از دستور strategy.exit() برای یه پوزیشن استفاده کنی تا خروج پله‌ای یا چندمرحله‌ای بسازی. این کار کمک می‌کنه یه استراتژی خروج دقیق‌تر و حرفه‌ای‌تر طراحی کنی.

برای این کار، باید توی هر فراخوانی strategy.exit() از پارامتر qty یا qty_percent استفاده کنی تا مشخص بشه چه مقدار از پوزیشن باید بسته بشه.
اگه مجموع مقدار سفارش‌های خروج بیشتر از حجم پوزیشن باز باشه، استراتژی به‌صورت خودکار حجم سفارش‌ها رو کم می‌کنه تا با حجم پوزیشن تطابق پیدا کنن.

نکته مهم:

اگه توی یه strategy.exit() هم qty بدی هم qty_percent، فقط مقدار qty در نظر گرفته می‌شه و qty_percent نادیده گرفته می‌شه.

توی مثال زیر:

  • وقتی شرط buyCondition برقرار بشه (هر ۱۰۰ کندل)، یه سفارش خرید ۲ واحدی ثبت می‌شه.
  • دو تا خروج با strategy.exit() ساخته می‌شن:
    • exit1 که برای یه واحده
    • exit2 که گفته ۳ واحد، ولی چون پوزیشن فقط ۲ واحده، حجمش به‌صورت خودکار کاهش پیدا می‌کنه
//@version=6
strategy("Multi-level exit demo", "test", overlay = true)

// شرط ورود: هر ۱۰۰ کندل
bool buyCondition = bar_index % 100 == 0

// قیمت‌های حد سود و ضرر برای دو مرحله خروج
var float takeProfit1 = na
var float takeProfit2 = na
var float stopLoss1 = na
var float stopLoss2 = na

if buyCondition
    // فقط اگه پوزیشنی باز نیست، قیمت‌های خروج رو محاسبه کن
    if strategy.opentrades == 0
        takeProfit1 := close * 1.01  // ۱٪ سود
        takeProfit2 := close * 1.02  // ۲٪ سود
        stopLoss1   := close * 0.99  // ۱٪ ضرر
        stopLoss2   := close * 0.98  // ۲٪ ضرر
    // سفارش خرید ۲ واحدی
    strategy.entry("buy", strategy.long, qty = 2)
    // خروج اول: ۱ واحد با حد سود و ضرر اول
    strategy.exit("exit1", "buy", limit = takeProfit1, stop = stopLoss1, qty = 1)
    // خروج دوم: گفته ۳ واحد، ولی چون بیشتر از حجم پوزیشن هست، خودکار تنظیم می‌شه
    strategy.exit("exit2", "buy", limit = takeProfit2, stop = stopLoss2, qty = 3)

// اگه قیمت به حد سود یا ضرر رسید، مقدارها رو پاک کن تا دیگه چیزی رسم نشه
if high >= takeProfit1 or low <= stopLoss1
    takeProfit1 := na
    stopLoss1 := na

if high >= takeProfit2 or low <= stopLoss2
    takeProfit2 := na
    stopLoss2 := na

// نمایش حد سود و ضررها روی چارت
plot(takeProfit1, "TP1", color.green, style = plot.style_circles)
plot(takeProfit2, "TP2", color.green, style = plot.style_circles)
plot(stopLoss1, "SL1", color.red, style = plot.style_circles)
plot(stopLoss2, "SL2", color.red, style = plot.style_circles)

همون‌طور که از علامت‌های معامله روی چارت مشخصه، استراتژی اول یکی از سفارش‌های exit1 (یا حد سود یا حد ضرر) رو اجرا می‌کنه و با این کار یه سهم از پوزیشن کم می‌شه. بعدش فقط یه سهم توی پوزیشن باقی می‌مونه.

اما توی سفارش exit2 گفتیم که قراره ۳ سهم خارج بشه، که بیشتر از پوزیشن باقی‌مونده‌ست.
استراتژی نمیاد همون عدد ۳ رو استفاده کنه، بلکه خودش به‌صورت خودکار مقدارش رو کم می‌کنه و می‌ذاره روی ۱، تا بتونه پوزیشن رو کامل ببنده.

نکته:

  • از سفارش exit1 فقط یکی‌شون اجرا می‌شه (یا حد سود یا حد ضرر، هر کدوم که اول فعال بشه).
    وقتی strategy.exit() چند نوع سفارش خروج (مثل TP و SL) برای یه معامله می‌سازه، فقط اولین سفارشی که فعال می‌شه اجرا می‌شه، بقیه هم خودکار کنسل می‌شن.
  • اینکه سفارش exit2 خودش کاهش پیدا کرد، به‌خاطر اینه که تمام سفارش‌هایی که با strategy.exit() ساخته می‌شن، به‌صورت پیش‌فرض عضو یه گروه به اسم strategy.oca.reduce هستن.
    توی این گروه، اگه حجم پوزیشن کافی نباشه، خود Pine Script اندازه سفارش‌ها رو کم می‌کنه تا با پوزیشن باقی‌مونده هماهنگ بشن.

در ادامه، درباره‌ی گروه‌های OCA بیشتر توضیح داده می‌شه.

وقتی می‌خوای با چندتا strategy.exit() مختلف، سفارش خروج بسازی، یه نکته خیلی مهم اینه که هر کدوم از این دستورها یه بخشی از پوزیشن باز رو برای خودش رزرو می‌کنه.
یعنی یه strategy.exit() نمی‌تونه اون قسمتی از پوزیشن رو ببنده که یه strategy.exit() دیگه قبلاً گرفته.

مثلاً توی این اسکریپت:

  • یه سفارش خرید "buy" برای ۲۰ سهم ثبت می‌کنیم.
  • بعد، دو تا دستور strategy.exit() داریم:
    • اولی (limit) یه سفارش حد سود برای ۱۹ سهم با قیمت limitPrice می‌سازه.
    • دومی (stop) یه سفارش حد ضرر برای ۲۰ سهم با قیمت stopPrice می‌سازه.

ولی چون strategy.exit("limit") اول اجرا شده و ۱۹ سهم از پوزیشن رو برای خودش رزرو کرده،
اون یکی دستور (stop) فقط می‌تونه ۱ سهم باقی‌مونده رو ببنده —
حتی اگه قیمت اول به حد ضرر برسه.

کدی که این رفتار رو نشون می‌ده:

//@version=6
strategy("Reserved exit demo", "test", overlay = true)

// قیمت‌های خروج
var float limitPrice = na
var float stopPrice = na

// شرط اجرا: ۱۰۰ کندل قبل از آخر چارت
bool longCondition = last_bar_index - bar_index == 100

if longCondition
    limitPrice := close * 1.01
    stopPrice  := close * 0.99
    // سفارش خرید ۲۰ سهم
    strategy.entry("buy", strategy.long, 20)
    // سفارش حد سود برای ۱۹ سهم
    strategy.exit("limit", limit = limitPrice, qty = 19)
    // سفارش حد ضرر برای ۲۰ سهم (ولی فقط ۱ سهم از پوزیشن مونده که می‌تونه بگیره)
    strategy.exit("stop", stop = stopPrice, qty = 20)

// فقط وقتی پوزیشن بازه، قیمت‌ها رو نشون بده
bool showPlot = strategy.opentrades == 1

plot(showPlot ? limitPrice : na, "Limit (take-profit) price", color.green, 2, plot.style_linebr)
plot(showPlot ? stopPrice : na, "Stop (stop-loss) price", color.red, 2, plot.style_linebr)

کسایی که با رفتار خاص strategy.exit() آشنا نباشن، ممکنه فکر کنن اگه قیمت به stopPrice برسه، کل پوزیشن بسته می‌شه.
ولی واقعیت اینه که چون exit("limit") زودتر توی کد اجرا شده و ۱۹ سهم رو رزرو کرده، فقط ۱ سهم برای exit("stop") باقی مونده —
فرقی هم نمی‌کنه قیمت اول به کدوم سطح برسه.

Trailing stop (حد ضرر متحرک)

یکی از قابلیت‌های مهم دستور strategy.exit() اینه که می‌تونه تریلینگ استاپ بسازه؛ یعنی یه نوع حد ضرر که وقتی قیمت در جهت دلخواه حرکت می‌کنه (مثلاً بالا رفتن در پوزیشن لانگ)، خودش رو با قیمت هماهنگ می‌کنه و عقب‌تر از اون حرکت می‌کنه.

این نوع سفارش خروج دو قسمت داره:

  1. سطح فعال‌سازی (activation level): وقتی قیمت بازار از این سطح عبور کنه، حد ضرر متحرک فعال می‌شه.
  2. فاصله دنباله‌رو (trail offset): فاصله‌ای که بعد از فعال شدن، حد ضرر پشت سر قیمت حرکت می‌کنه (یعنی با بالا رفتن قیمت، حد ضرر هم میاد بالا، ولی با همون فاصله مشخص).

سه تا پارامتر هستن که این رفتار رو مشخص می‌کنن:

  • trail_price: یه قیمت مطلقه. اگه قیمت بازار به این سطح برسه، trailing stop فعال می‌شه.
  • trail_points: یه مقدار نسبی به صورت تعداد تیکه (tick). مشخص می‌کنه چند تیک از قیمت ورود فاصله بگیره تا trailing stop فعال بشه.
  • trail_offset: فاصله‌ای که بعد از فعال شدن، trailing stop باهاش پشت سر قیمت حرکت می‌کنه.

برای اینکه یه trailing stop ساخته و فعال بشه، حتماً باید trail_offset رو مشخص کنی، و حداقل یکی از trail_price یا trail_points رو هم بدی.

اگه هر دو رو مشخص کرده باشی (trail_price و trail_points) اون سطحی در نظر گرفته می‌شه که زودتر بهش می‌رسی.

مثال:

  • اگه trail_points = 50 تیک باشه
  • و trail_price معادل ۵۱ تیک بالاتر از قیمت ورود باشه
    اون وقت strategy.exit() از trail_points استفاده می‌کنه، چون بازار زودتر به اون می‌رسه.

توی مثال پایین، دقیقاً نشون داده شده که trailing stop چطور کار می‌کنه:

  • ۱۰۰ کندل قبل از آخرین کندل چارت، یه سفارش خرید (لانگ) با strategy.entry() ثبت می‌شه.
  • یه کندل بعد، با strategy.exit() یه trailing stop ساخته می‌شه که trail_price و trail_offset داره.
  • با لیبل، خط و نمودار، نحوه‌ی حرکت trailing stop روی چارت نشون داده می‌شه.

توی این مثال، رفتار تریلینگ استاپ (Trailing Stop) به‌صورت دقیق روی چارت نشون داده می‌شه:

  • خط سبز روی چارت نشون می‌ده که قیمت بازار باید به اون سطح برسه تا تریلینگ استاپ فعال بشه.
  • وقتی قیمت از پایین به اون سطح می‌رسه، حد ضرر متحرک فعال می‌شه و با یه خط آبی روی چارت نمایش داده می‌شه.
  • از اون لحظه به بعد، هر بار که قیمت یه سقف جدید بزنه، قیمت تریلینگ استاپ هم بالا می‌ره و فاصله خودش رو با قیمت جدید (به اندازه trailOffsetInput) حفظ می‌کنه.
  • ولی اگه قیمت پایین بیاد یا به سقف جدیدی نرسه، حد ضرر تغییری نمی‌کنه.
  • در نهایت، اگه قیمت از تریلینگ استاپ پایین‌تر بیاد، سفارش خروج فعال می‌شه و پوزیشن بسته می‌شه.

//@version=6
strategy("Trailing stop order demo", overlay = true, margin_long = 100, margin_short = 100)

// فاصله فعال‌سازی تریلینگ استاپ از قیمت ورود (بر حسب تیک)
int activationOffsetInput = input.int(1000, "Activation level offset (in ticks)", 0)
// فاصله‌ای که حد ضرر پشت سقف قیمت حرکت می‌کنه
int trailOffsetInput = input.int(2000, "Trailing stop offset (in ticks)", 0)

// رسم لیبل و خط روی چارت
debugDrawings(float price, string txt, color drawingColor, bool drawLine = false) =>
    label.new(
         bar_index, price, text = txt, color = drawingColor, textcolor = color.white,
         style = label.style_label_lower_right, size = size.large
     )
    line.new(
         bar_index, price, bar_index + 1, price, color = drawingColor, extend = extend.right,
         style = line.style_dashed
     )

// سطح فعال‌سازی و مقدار تریلینگ استاپ
var float activationLevel = na
var float trailingStop = na
// محاسبه حد ضرر متحرک فعلی بر اساس سقف قیمت
float theoreticalStopPrice = high - trailOffsetInput * syminfo.mintick

// ثبت سفارش خرید لانگ ۱۰۰ کندل قبل از آخر چارت
if last_bar_index - bar_index == 100
    strategy.entry("Long", strategy.long)

// یه کندل بعد، تریلینگ استاپ رو ثبت کن
if last_bar_index - bar_index == 99
    activationLevel := open + syminfo.mintick * activationOffsetInput
    strategy.exit(
         "Trailing Stop", from_entry = "Long", trail_price = activationLevel, 
         trail_offset = trailOffsetInput
     )
    debugDrawings(activationLevel, "Trailing Stop Activation Level", color.green, true)

// زمانی که پوزیشن بازه، رفتار تریلینگ استاپ رو نمایش بده
if strategy.opentrades == 1
    // وقتی برای اولین بار قیمت به سطح فعال‌سازی می‌رسه
    if na(trailingStop) and high >= activationLevel
        debugDrawings(activationLevel, "Activation level crossed", color.green)
        trailingStop := theoreticalStopPrice
        debugDrawings(trailingStop, "Trailing Stop Activated", color.blue)
    // هر بار که سقف جدیدی ثبت شد، حد ضرر رو بالا ببر
    else if theoreticalStopPrice > trailingStop
        trailingStop := theoreticalStopPrice

// نمایش حد ضرر متحرک روی چارت
plot(trailingStop, "Trailing Stop")

خروج برای چند ورود

با یه بار صدا زدن strategy.exit() می‌شه برای بیش از یه ورود داخل یه پوزیشن باز، سفارش خروج ساخت، بستگی داره به مقداری که توی پارامتر from_entry مشخص شده.

اگه پوزیشن باز شامل دو یا چند ورود با یه ID یکسان باشه، یه فراخوانی strategy.exit() با همون ID توی from_entry، برای هر کدوم از اون ورودها که قبل یا روی همون کندلی که strategy.exit() اجرا شده ساخته شدن، یه سفارش خروج جدا می‌سازه.

مثلاً توی این اسکریپت، strategy.entry() به‌صورت دوره‌ای روی دو کندل پشت سر هم اجرا می‌شه تا یه پوزیشن لانگ باز کنه و بهش اضافه کنه. هر دو بار با ID "buy" ثبت می‌شن. بعد از دومین ورود، اسکریپت یه بار strategy.exit() رو با from_entry = "buy" صدا می‌زنه تا برای هر دو ورود با این ID، سفارش خروج جدا بسازه. وقتی قیمت بازار به مقدار takeProfit یا stopLoss برسه، شبیه‌ساز بروکر هر دو سفارش خروج رو پر می‌کنه و پوزیشن بسته می‌شه:

//@version=6
strategy("Exits for entries with the same ID demo", overlay = true, pyramiding = 2)

//@variable Take-profit price for exit commands.
var float takeProfit = na
//@variable Stop-loss price for exit commands.
var float stopLoss   = na

//@variable Is `true` on two consecutive bars in 100-bar cycles. 
bool buyCondition = math.min(bar_index % 100, math.max(bar_index - 1, 0) % 100) == 0

if buyCondition
    // Place a "buy" market order to enter a trade. 
    strategy.entry("buy", strategy.long)
    // Calculate exits on the second order.
    if strategy.opentrades == 1
        // Update the `takeProfit` and `stopLoss`.
        takeProfit := close * 1.01
        stopLoss   := close * 0.99
        // Place exit orders for both "buy" entries.
        strategy.exit("exit", "buy", limit = takeProfit, stop = stopLoss)

// Set `takeProfit` and `stopLoss` to `na` when both trades close.
if ta.change(strategy.closedtrades) == 2
    takeProfit := na
    stopLoss   := na

// Plot the `takeProfit` and `stopLoss` values.
plot(takeProfit, "TP", color.green, style = plot.style_circles)
plot(stopLoss, "SL", color.red, style = plot.style_circles)

یک فراخوانی strategy.exit() می‌تونه بدون توجه به ID ورود، برای همه‌ی ورودهای پوزیشن باز سفارش خروج بسازه، وقتی که پارامتر from_entry رو نداشته باشه.

توی این مثال، دستور strategy.entry() طوری تغییر داده شده که هر بار یه سفارش ورود با یه ID منحصربه‌فرد بسازه، و پارامتر from_entry هم از strategy.exit() حذف شده. چون این نسخه مشخص نمی‌کنه سفارش‌های خروج برای کدوم ورودها باشن، strategy.exit() برای همه‌ی ورودهای پوزیشن سفارش خروج می‌سازه:

//@version=6
strategy("Exits for entries with different IDs demo", overlay = true, pyramiding = 2)

//@variable Take-profit price for exit commands.
var float takeProfit = na
//@variable Stop-loss price for exit commands.
var float stopLoss   = na

//@variable Is `true` on two consecutive bars in 100-bar cycles. 
bool buyCondition = math.min(bar_index % 100, math.max(bar_index - 1, 0) % 100) == 0

if buyCondition
    // Place a long market order with a unique ID. 
    strategy.entry("buy" + str.tostring(strategy.opentrades + strategy.closedtrades), strategy.long)
    // Calculate exits on the second order.
    if strategy.opentrades == 1
        // Update the `takeProfit` and `stopLoss`.
        takeProfit := close * 1.01
        stopLoss   := close * 0.99
        // Place exit orders for ALL entries in the position, irrespective of ID.
        strategy.exit("exit", limit = takeProfit, stop = stopLoss)

// Set `takeProfit` and `stopLoss` to `na` when both trades close.
if ta.change(strategy.closedtrades) == 2
    takeProfit := na
    stopLoss   := na

// Plot the `takeProfit` and `stopLoss` values.
plot(takeProfit, "TP", color.green, style = plot.style_circles)
plot(stopLoss, "SL", color.red, style = plot.style_circles)

یه نکته خیلی مهم اینه که وقتی strategy.exit() رو بدون پارامتر from_entry صدا بزنی، اون دستور به‌صورت مداوم فعال می‌مونه و برای همه‌ی ورودهای پوزیشن باز سفارش خروج می‌سازه، فرقی نداره اون ورودها کی اتفاق افتاده باشن. این رفتار ممکنه روی استراتژی‌هایی که چندتا ورود و خروج دارن تأثیر بذاره.

اگه یه استراتژی پوزیشن باز داشته باشه و توی هر کندلی strategy.exit() رو (بدون مشخص کردن from_entry) صدا بزنه، اون دستور:

  • برای هر ورودی که قبل یا روی همون کندل ایجاد شده، سفارش خروج می‌سازه
  • و همین‌طور برای ورودهایی که بعدش هم ایجاد می‌شن، تا وقتی که پوزیشن بسته بشه.

توی این اسکریپت، این رفتار نشون داده شده:

  • توی یه بازه زمانی که کاربر مشخص می‌کنه، روی هر کندل یه سفارش خرید لانگ با strategy.entry() ثبت می‌شه.
  • توی یه زمان مشخص داخل اون بازه، فقط یه بار strategy.exit() صدا زده می‌شه بدون پارامتر from_entry.
  • اون یه دستور، برای همه‌ی ورودها توی پوزیشن، سفارش خروج با loss = 0 می‌سازه.
    یعنی اگه قیمت بازار برابر یا پایین‌تر از قیمت ورود بشه، خروج انجام می‌شه.

قبل از اجرای اسکریپت، کاربر باید سه تا زمان انتخاب کنه:

  1. زمان شروع ثبت سفارش‌ها
  2. زمان اجرای strategy.exit()
  3. زمان پایان ثبت سفارش‌ها
//@version=6
strategy("Exit persist demo", overlay = true, margin_long = 100, margin_short = 100, pyramiding = 100)

int entryStartTime = input.time(0, "Start time for entries", confirm = true)
int exitCallTime   = input.time(0, "Exit call time", confirm = true)
int entryEndTime   = input.time(0, "End time for entries", confirm = true)

if exitCallTime <= entryStartTime or entryEndTime <= exitCallTime or entryEndTime <= entryStartTime
    runtime.error("The input timestamps must follow this condition: entryStartTime < exitCallTime < entryEndTime.")

bool entriesStart = time == entryStartTime
bool callExit     = time == exitCallTime
bool entriesEnd   = time == entryEndTime
bool callEntry    = time >= entryStartTime and time < entryEndTime

if callEntry
    strategy.entry("Entry", strategy.long)

if callExit
    strategy.exit("Exit", loss = 0)

switch 
    entriesStart => label.new(bar_index, high, "Start placing entry orders.", color = color.green, textcolor = color.white, style = label.style_label_lower_right, size = size.large)
    callExit     => label.new(bar_index, high, "Call `strategy.exit()` once.", color = color.blue, textcolor = color.white, style = label.style_label_lower_right, size = size.large)
    entriesEnd   => label.new(bar_index, high, "Stop placing orders.", color = color.red, textcolor = color.white, style = label.style_label_lower_left, size = size.large)

var line lowestLine = line.new(entryStartTime + 1000, na, entryEndTime, na, xloc.bar_time, extend.right, color.orange, width = 2)
var label lowestLabel = label.new(entryStartTime + 1000, na, "Lowest entry price", color = color.orange, style = label.style_label_upper_right, xloc = xloc.bar_time)

if callEntry[1]
    var float lowestPrice = strategy.opentrades.entry_price(0)
    float entryPrice = strategy.opentrades.entry_price(strategy.opentrades - 1)
    if not na(entryPrice)
        lowestPrice := math.min(lowestPrice, entryPrice)
        lowestLine.set_y1(lowestPrice)
        lowestLine.set_y2(lowestPrice)
        lowestLabel.set_y(lowestPrice)

bgcolor(entriesStart ? color.new(color.green, 80) : na, title = "Entries start highlight")
bgcolor(callExit     ? color.new(color.blue, 80)  : na, title = "Exit call highlight")
bgcolor(entriesEnd   ? color.new(color.red, 80)   : na, title = "Entries end highlight")

نکته:

توی strategy() مقدار pyramiding = 100 گذاشتیم تا استراتژی بتونه حداکثر ۱۰۰ ورودی هم‌زمان داشته باشه با استفاده از strategy.entry().

اسکریپت با استفاده از label و bgcolor() نشون می‌ده چه زمانی ثبت سفارش‌ها شروع و تموم می‌شه و چه زمانی دستور strategy.exit() اجرا می‌شه.

یه خط و یه لیبل روی پایین‌ترین قیمت ورود کشیده شده تا مشخص بشه قیمت بازار باید به کجا برسه تا پوزیشن کامل بسته بشه.

رفتار خاص strategy.exit() رو می‌تونیم با مقایسه‌ی کد با نمودار خروجی بهتر درک کنیم. توی کد، strategy.exit() فقط یک‌بار روی کندلی که لیبل آبی داره اجرا می‌شه. اما همین یک‌بار باعث شده برای همه‌ی ورودهایی که قبل یا روی اون کندل انجام شدن، سفارش خروج ساخته بشه و حتی برای ورودهایی که بعد از اون هم بودن، باز هم خروج ساخته شده. این رفتار به این دلیله که چون strategy.exit() به یه ID مشخص وصل نیست، نمی‌دونه کی باید متوقف بشه. فقط وقتی پوزیشن کامل بسته بشه، اونم متوقف می‌شه.

اگه توی همین اسکریپت به strategy.exit() پارامتر from_entry = "Entry" رو اضافه کنیم، رفتارش فرق می‌کنه. وقتی from_entry مشخص بشه، فقط برای ورودهایی با همون ID که قبل یا روی اون کندل انجام شدن، سفارش خروج ساخته می‌شه. حتی اگه بعداً با همون ID هم ورود داشته باشیم، اون‌ها رو شامل نمی‌شه.

توی این نسخه‌ی اسکریپت، فقط ۱۷ خروج اتفاق می‌افته، که مربوط به ۱۷ تا ورودیه که قبل یا روی کندلی بودن که strategy.exit() توش اجرا شده. ورودهایی که بعد از اون کندل انجام شدن، اصلاً تحت تأثیر اون strategy.exit() نیستن:

//@version=6
strategy("Exit persist demo", overlay = true, margin_long = 100, margin_short = 100, pyramiding = 100)

//@variable The time when order creation starts. 
int entryStartTime = input.time(0, "Start time for entries", confirm = true)
//@variable The time when the `strategy.exit()` call occurs.
int exitCallTime = input.time(0, "Exit call time", confirm = true)
//@variable The time when order creation stops.
int entryEndTime = input.time(0, "End time for entries", confirm = true)

if exitCallTime <= entryStartTime or entryEndTime <= exitCallTime or entryEndTime <= entryStartTime
    runtime.error("The input timestamps must follow this condition: entryStartTime < exitCallTime < entryEndTime.")

bool entriesStart = time == entryStartTime
bool callExit     = time == exitCallTime
bool entriesEnd   = time == entryEndTime
bool callEntry    = time >= entryStartTime and time < entryEndTime

if callEntry
    strategy.entry("Entry", strategy.long)

if callExit
    strategy.exit("Exit", from_entry = "Entry", loss = 0)

switch 
    entriesStart => label.new(
         bar_index, high, "Start placing entry orders.", color = color.green, textcolor = color.white, 
         style = label.style_label_lower_right, size = size.large
     )
    callExit => label.new(
         bar_index, high, "Call `strategy.exit()` once.", color = color.blue, textcolor = color.white, 
         style = label.style_label_lower_right, size = size.large
     )
    entriesEnd => label.new(
         bar_index, high, "Stop placing orders.", color = color.red, textcolor = color.white, 
         style = label.style_label_lower_left, size = size.large
     )

var line lowestLine = line.new(
     entryStartTime + 1000, na, entryEndTime, na, xloc.bar_time, extend.right, color.orange, width = 2
 )
var label lowestLabel = label.new(
     entryStartTime + 1000, na, "Lowest entry price", color = color.orange, 
     style = label.style_label_upper_right, xloc = xloc.bar_time
 )

if callEntry[1]
    var float lowestPrice = strategy.opentrades.entry_price(0)
    float entryPrice = strategy.opentrades.entry_price(strategy.opentrades - 1)
    if not na(entryPrice)
        lowestPrice := math.min(lowestPrice, entryPrice)
        lowestLine.set_y1(lowestPrice)
        lowestLine.set_y2(lowestPrice)
        lowestLabel.set_y(lowestPrice)

bgcolor(entriesStart ? color.new(color.green, 80) : na, title = "Entries start highlight")
bgcolor(callExit ? color.new(color.blue, 80) : na, title = "Exit call highlight")
bgcolor(entriesEnd ? color.new(color.red, 80) : na, title = "Entries end highlight")

`strategy.close()` and `strategy.close_all()

دستورات strategy.close() و strategy.close_all() باعث می‌شن یه سفارش برای بستن پوزیشن باز صادر بشه. بر خلاف strategy.exit() که سفارش خروج بر اساس قیمت (مثل استاپ لاس) ایجاد می‌کنه، این دوتا دستور یه سفارش مارکت (بازاری) صادر می‌کنن که شبیه‌ساز بروکر توی اولین تیک بعدی (یعنی وقتی قیمت آپدیت بشه) اون رو پر می‌کنه، بدون توجه به قیمت.

مثالی که پایین زده شده، یه استراتژی ساده رو نشون می‌ده که با استفاده از strategy.entry() هر ۵۰ کندل یه بار یه سفارش خرید (buy) می‌فرسته، و بعد از ۲۵ کندل، با استفاده از strategy.close() اون پوزیشن خرید رو با یه سفارش مارکت می‌بنده:

//@version=6
strategy("Close demo", "test", overlay = true)

// این متغیر توی هر کندل پنجاهم مقدارش true می‌شه
buyCond = bar_index % 50 == 0

// این یکی متغیر توی هر کندل بیست‌وپنجم true می‌شه، به جز اونایی که خودشون کندل پنجاهم هستن
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)

توجه کن که توی این اسکریپت، تابع strategy.close() از "buy" به عنوان آرگومان id استفاده کرده. بر خلاف strategy.exit()، پارامتر id توی این دستور، شناسه‌ی ورود (entry ID) یه معامله‌ی باز رو مشخص می‌کنه. این شناسه مربوط به خود سفارش خروج نیست، بلکه به معامله‌ای که قبلاً با اون ID وارد شده‌ای مربوط می‌شه.

اگه چند معامله‌ی باز با یه شناسه‌ی ورود (entry ID) یکسان وجود داشته باشه، یه بار صدا زدن strategy.close() با اون ID باعث می‌شه یه سفارش مارکت صادر بشه که همه‌ی اون پوزیشن‌ها رو با هم ببنده.

اسکریپتی که پایین نشون داده، هر ۲۵ کندل یه بار با strategy.entry() یه سفارش خرید با شناسه‌ی "buy" صادر می‌کنه، و هر ۱۰۰ کندل یه بار با strategy.close() همه‌ی معاملات باز با اون شناسه‌ی "buy" رو می‌بنده. چون همه‌ی پوزیشن‌های باز یه شناسه دارن، سفارش مارکتی که با strategy.close() صادر می‌شه، کل اون موقعیت (position) رو می‌بنده:

//@version=6
strategy("Multiple close demo", "test", overlay = true, pyramiding = 3)

// این شرط هر ۱۰۰ کندل یه بار true می‌شه
sellCond = bar_index % 100 == 0

// این شرط هر ۲۵ کندل یه بار true می‌شه، به جز اونایی که خودشون کندل ۱۰۰‌ام هستن
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)

دقت کن که:

  • ما توی دستور strategy() پارامتر pyramiding = 3 رو اضافه کردیم. این یعنی این اسکریپت اجازه داره تا سقف ۳ بار وارد یه پوزیشن بشه (یعنی سه تا سفارش ورود جداگونه برای یه پوزیشن باز بفرسته) با استفاده از strategy.entry().
  • دستور strategy.close_all() یه سفارش مارکت صادر می‌کنه که کل پوزیشن باز رو می‌بنده، بدون اینکه به یه شناسه‌ی خاص (entry ID) وابسته باشه. این دستور وقتی به کار میاد که استراتژی بخواد هر چی پوزیشن باز داره رو، حتی اگه با IDهای مختلف باشن، سریع ببنده.

توی اسکریپت پایین، به ترتیب و با توجه به تعداد معاملات باز، سفارش‌های ورودی با شناسه‌های "A"، "B" و "C" ارسال می‌شن. وقتی تعداد معاملات باز به ۳ تا رسید، strategy.close_all() صدا زده می‌شه و یه سفارش مارکت صادر می‌کنه که توی کندل بعدی کل پوزیشن رو می‌بنده:

//@version=6
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() تعریف شدن، یا سفارش‌هایی که با strategy.entry() و strategy.order() فرستاده می‌شن و از پارامتر limit یا stop استفاده می‌کنن.

دستور strategy.cancel() یه پارامتر اجباری به اسم id داره که مشخص می‌کنه کدوم سفارش (ورودی یا خروجی) باید لغو بشه. ولی strategy.cancel_all() همچین پارامتری نداره، چون بدون توجه به ID، همه‌ی سفارش‌های پرنشده رو لغو می‌کنه.

توی اسکریپت زیر، یه سفارش خرید (limit) با strategy.entry() ثبت می‌شه که قیمتش ۵۰۰ تیک پایین‌تر از قیمت بسته شدن هست، و این سفارش توی کندلی که ۱۰۰ بار قبل از آخرین کندل چارت قرار داره، گذاشته می‌شه. توی کندل بعدش، با استفاده از strategy.cancel() اون سفارش لغو می‌شه.

این اسکریپت پس‌زمینه‌ی چارت رو با رنگ‌های مختلف نشون می‌ده: وقتی سفارش خرید ارسال می‌شه رنگ پس‌زمینه سبز می‌شه، و وقتی سفارش لغو می‌شه، رنگ نارنجی می‌شه. همچنین یه خط افقی هم روی قیمت سفارش کشیده می‌شه. همون‌طور که توی چارت می‌بینی، وقتی قیمت به اون خط می‌رسه، هیچ سفارش ورودی انجام نمی‌شه، چون استراتژی قبلش اون سفارش رو لغو کرده بوده (یعنی وقتی پس‌زمینه نارنجی شده):

//@version=6
strategy("Cancel demo", "test", overlay = true)

// یه خط افقی روی قیمت limit سفارش "buy" می‌کشه
var line limitLine = na

// رنگ پس‌زمینه: سبز یعنی سفارش ثبت شده، نارنجی یعنی لغو شده
color bgColor = na

if last_bar_index - bar_index == 100
    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 == 99
    strategy.cancel("buy")
    bgColor := color.new(color.orange, 50)

bgcolor(bgColor)

دستور strategy.cancel() روی همه‌ی سفارش‌های پرنشده‌ای که یه ID مشخص دارن تأثیر می‌ذاره. اگه IDی که بهش دادی اصلاً وجود نداشته باشه، هیچ کاری انجام نمی‌ده. ولی اگه چند تا سفارش باز (پرنشده) با اون ID وجود داشته باشن، همشون رو یه‌جا لغو می‌کنه.

تو اسکریپت پایین، نسخه‌ی قبلی یه مقدار تغییر داده شده تا این بار سه تا سفارش خرید (limit) با شناسه‌ی "buy" توی سه کندل پشت سر هم ثبت کنه—از ۱۰۰ کندل قبل از آخرین کندل چارت شروع می‌شه. بعد از اینکه این سه تا سفارش ثبت شدن، استراتژی با استفاده از strategy.cancel("buy") همشون رو با هم لغو می‌کنه.

نتیجه این می‌شه که وقتی قیمت بازار به اون سطوحی که سفارش‌ها بودن می‌رسه (که با خط افقی نشون داده شدن)، هیچ معامله‌ای انجام نمی‌شه، چون همه‌ی اون سفارش‌ها از قبل لغو شدن (یعنی موقعی که پس‌زمینه نارنجی شده):

//@version=6
strategy("Multiple cancel demo", "test", overlay = true, pyramiding = 3)

// یه خط افقی روی قیمت سفارش limit رسم می‌کنه
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)

توجه کن که:

  • ما توی دستور strategy() پارامتر pyramiding = 3 رو اضافه کردیم، که اجازه می‌ده تا سه بار پشت سر هم از طریق strategy.entry() وارد یه پوزیشن بشیم. البته حتی اگه این تنظیم رو نمی‌ذاشتیم هم، اگه به جای strategy.entry() از strategy.order() استفاده می‌کردیم، باز هم همین نتیجه حاصل می‌شد، چون pyramiding روی سفارش‌هایی که با strategy.order() فرستاده می‌شن، تأثیری نداره.
  • دستورات strategy.cancel() و strategy.cancel_all() می‌تونن هر نوع سفارشی رو لغو کنن، حتی سفارش‌های مارکت (بازاری). اما یه نکته‌ی مهم وجود داره: این دستورها فقط در صورتی می‌تونن یه سفارش مارکت رو لغو کنن که توی همون اجرای اسکریپتی که اون سفارش ثبت شده، صدا زده بشن. یعنی اگه بخوای توی اجرای بعدی اسکریپت اون سفارش مارکت رو لغو کنی، دیگه فایده‌ای نداره، چون شبیه‌ساز بروکر سفارش مارکت رو توی اولین تیک کندل بعدی پر می‌کنه.

توی این مثال، یه سفارش خرید مارکت با strategy.entry() توی کندلی که ۱۰۰ کندل قبل از آخرین کندل چارت هست، ثبت می‌شه. توی کندل بعدی، اسکریپت سعی می‌کنه با strategy.cancel_all() اون سفارش رو لغو کنه. اما این دستور اثری نداره، چون سفارش مارکت قبل از اینکه strategy.cancel_all() اجرا بشه، توسط شبیه‌ساز بروکر پر شده. (یعنی توی اولین تیک کندل جدید پر شده، قبل از اینکه Pine Script اجرا بشه.)

//@version=6
strategy("Cancel market demo", "test", overlay = true)

// رنگ پس‌زمینه: سبز یعنی سفارش مارکت ارسال شده، نارنجی یعنی تلاش برای لغوش
color bgColor = na

if last_bar_index - bar_index == 100
    strategy.entry("buy", strategy.long)
    bgColor := color.new(color.green, 50)

if last_bar_index - bar_index == 99
    strategy.cancel_all()
    bgColor := color.new(color.orange, 50)

bgcolor(bgColor)

Position sizing

توی استراتژی‌های Pine Script™ دو روش وجود داره برای اینکه تعیین کنیم اندازه‌ی سفارش‌ها (Position Sizing) چقدر باشه:

  1. روش اول: مشخص کردن یه مقدار ثابت به عنوان مقدار پیش‌فرض برای اندازه سفارش. این کار با استفاده از دو تا پارامتر default_qty_type و default_qty_value توی دستور strategy() انجام می‌شه. کاربر اسکریپت هم بعداً می‌تونه این مقدار رو از قسمت "Settings → Properties" تغییر بده.
    مثلا اگه بگی default_qty_type = strategy.cash و default_qty_value = 5000 یعنی هر معامله با ۵۰۰۰ دلار انجام بشه.
  2. روش دوم: مشخص کردن مستقیم مقدار سفارش داخل خود strategy.entry() یا strategy.order() با استفاده از پارامتر qty.
    اگه این پارامتر qty رو به یه مقدار غیر na بدی، اون دستور دیگه به مقدار پیش‌فرض اهمیتی نمی‌ده و فقط همون مقدار رو اجرا می‌کنه. یعنی مستقیماً تعیین می‌کنی چند تا قرارداد/سهم/لات/واحد خریده یا فروخته بشه.

تو مثال زیر، از strategy.entry() استفاده شده و برای پوزیشن‌های خرید (long) و فروش (short) اندازه سفارش متفاوت در نظر گرفته شده. منطقش اینه:

  • اگه کمترین قیمت کندل جاری (low) برابر با کمترین قیمت در ۲۰ کندل اخیر باشه، یه سفارش خرید با مقدار longAmount فرستاده می‌شه.
  • در غیر این صورت، اگه بیشترین قیمت کندل جاری (high) برابر با بیشترین قیمت در ۲۰ کندل اخیر باشه، یه سفارش فروش با مقدار shortAmount فرستاده می‌شه.
//@version=6
strategy("Buy low, sell high", overlay = true, default_qty_type = strategy.cash, default_qty_value = 5000)

int   length      = input.int(20, "Length", 1)
float longAmount  = input.float(4.0, "Long Amount", 0.0)
float shortAmount = input.float(2.0, "Short Amount", 0.0)

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)

توجه کن که با وجود اینکه ما پارامترهای default_qty_type و default_qty_value رو توی دستور strategy() مشخص کردیم، اما چون توی دستورات strategy.entry() مقدار qty رو مستقیماً تعیین کردیم، اون تنظیمات پیش‌فرض استفاده نمی‌شن. یعنی وقتی توی خود دستور entry یه مقدار برای qty بدی، اون مقدار اولویت داره و دیگه مقدار پیش‌فرض نادیده گرفته می‌شه.

اگه بخوای استراتژی از مقدار پیش‌فرض استفاده کنه، باید یا پارامتر qty رو از strategy.entry() حذف کنی یا مقدارش رو na بذاری.

توی این نسخه‌ی ویرایش‌شده از اسکریپت قبلی، ما از عبارت‌های شرطی (ternary) برای پارامتر qty استفاده کردیم. یعنی اگه مقدار ورودی longAmount یا shortAmount صفر باشه (که ما به عنوان مقدار پیش‌فرض جدید تنظیم کردیم)، اون سفارش‌ها از اندازه‌ی پیش‌فرض استفاده می‌کنن.

پس در این حالت، اگه کاربر مقدار "Long Amount" یا "Short Amount" رو صفر بذاره، دستور entry از مقدار پیش‌فرض ۵۰۰۰ دلاری استفاده می‌کنه. ولی اگه عدد دیگه‌ای وارد کنه، همون عدد رو به عنوان اندازه‌ی سفارش استفاده می‌کنه.

//@version=6
strategy("Buy low, sell high", overlay = true, default_qty_type = strategy.cash, default_qty_value = 5000)

int   length      = input.int(20, "Length", 1)
float longAmount  = input.float(0.0, "Long Amount", 0.0)
float shortAmount = input.float(0.0, "Short Amount", 0.0)

float highest = ta.highest(length)
float lowest  = ta.lowest(length)

switch
    low == lowest   => strategy.entry("Buy", strategy.long, longAmount == 0.0 ? na : longAmount)
    high == highest => strategy.entry("Sell", strategy.short, shortAmount == 0.0 ? na : shortAmount)

Closing a market position

به‌طور پیش‌فرض، وقتی استراتژی می‌خواد یه پوزیشن بازار (Market Position) رو ببنده، از روش FIFO (First In, First Out) استفاده می‌کنه. یعنی هر سفارشی که برای خروج صادر می‌شه، از اولین معامله‌ی باز شروع می‌کنه به بستن یا کم‌کردن حجم، حتی اگه توی اون دستور خروج، اسم یه معامله‌ی دیگه مشخص شده باشه.

اگه بخوای این رفتار پیش‌فرض رو تغییر بدی و به استراتژی بگی می‌تونه هر معامله‌ی بازی رو ببنده (نه لزوماً اولینش)، باید توی دستور strategy() این گزینه رو اضافه کنی:

close_entries_rule = "ANY"

توی مثالی که پایین هست، اسکریپت دو تا سفارش خرید پشت سر هم ثبت می‌کنه با اسم‌های "Buy1" و "Buy2":

  1. وقتی اندازه‌ی پوزیشن صفره (یعنی هیچ معامله‌ای باز نیست) و ما ۱۰۰ کندل تا آخر چارت فاصله داریم، یه سفارش "Buy1" به اندازه ۵ واحد ثبت می‌کنه.
  2. وقتی اندازه‌ی پوزیشن شد ۵ (یعنی همون سفارش اول پر شده)، یه سفارش "Buy2" به اندازه ۱۰ واحد ثبت می‌کنه.
  3. وقتی اندازه‌ی پوزیشن رسید به ۱۵ (یعنی هر دو سفارش پر شدن)، یه دستور خروج (exit) به اسم "bracket" صادر می‌کنه که هم حد سود (profit = 10) داره و هم حد ضرر (loss = 10).

نکته مهم اینه که چون توی strategy.exit() نگفته از کدوم سفارش ورود (from_entry) این خروج انجام بشه، پس Pine خودش با روش FIFO شروع می‌کنه از اولین ورود، یعنی "Buy1".

در ضمن، برای اینکه بهتر ببینی چه اتفاقی افتاده، توی یه پنجره جدا (pane جداگونه)، مقدار strategy.position_size (یعنی اندازه‌ی پوزیشن) رو با یه نمودار ستونی (histogram) رسم کرده:

//@version=6
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 = 10, profit = 10)

plot(positionSize == 0 ? na : positionSize, "Position Size", color.lime, 4, plot.style_histogram)

توجه کن که:

  • توی دستور strategy() مقدار pyramiding = 2 رو گذاشتیم، یعنی این استراتژی اجازه داره تا دو بار پشت سر هم وارد پوزیشن بشه (دو تا معامله هم‌زمان باز کنه) با استفاده از strategy.entry().
  • هر وقت قیمت بازار به حد سود یا حد ضرری که تعریف کردیم برسه، سفارش خروج اجرا می‌شه. اما این خروج طبق قانون FIFO انجام می‌شه؛ یعنی از اولین معامله‌ی بازی که وارد شدیم شروع می‌کنه به بستن، حتی اگه توی کد به‌صورت واضح گفته باشیم که مثلاً اول "Buy2" رو ببند، ولی باز Pine Script اول "Buy1" رو می‌بنده چون اون زودتر وارد شده.

حالا توی نسخه‌ی جدیدی از اسکریپت که پایین هست، ما اومدیم دو کار کردیم:

  1. با استفاده از strategy.close("Buy2") گفتیم که پوزیشن مربوط به "Buy2" بسته بشه. این دستور یه سفارش مارکت صادر می‌کنه که توی اولین تیک بعدی (اولین آپدیت قیمت بعدی) اجرا می‌شه و چون مارکت اوردره، خیلی سریع پر می‌شه.
  2. بعدش با strategy.exit("bracket", "Buy1", ...) یه سفارش خروج برای "Buy1" تعریف کردیم که شامل حد سود (profit = 10) و حد ضرر (loss = 10) هست. چون این سفارش از نوع قیمت‌محوره (limit و stop)، بعد از سفارش مارکت اجرا می‌شه. یعنی اول "Buy2" بسته می‌شه، بعد اگر شرایطش رسید، "Buy1" هم از طریق bracket بسته می‌شه.
//@version=6
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")  // سفارش مارکت برای بستن Buy2
    strategy.exit("bracket", "Buy1", loss = 10, profit = 10)  // سفارش حد سود/ضرر برای Buy1

plot(positionSize == 0 ? na : positionSize, "Position Size", color.lime, 4, plot.style_histogram)

سفارش مارکت که با strategy.close() توی اسکریپت صادر شده، مربوط به ۱۰ واحده، چون به معامله‌ای وصله که شناسه‌ی ورودش "Buy2" هست. ممکنه کاربر انتظار داشته باشه که این استراتژی، اون معامله رو کامل ببنده وقتی سفارش اجرا می‌شه.
ولی توی تب "List of Trades" نشون داده می‌شه که پنج واحد از سفارش برای بستن معامله‌ی "Buy1" استفاده شده چون اون قدیمی‌تره، و پنج واحد باقی‌مونده، نصف معامله‌ی "Buy2" رو می‌بنده. بعد از اون، سفارش‌های "bracket" از strategy.exit() بقیه‌ی پوزیشن رو می‌بندن.

توجه کن که:

اگه close_entries_rule = "ANY" رو توی دستور strategy() می‌ذاشتیم، سفارش مارکت از strategy.close() اول معامله‌ی باز با شناسه‌ی "Buy2" رو می‌بست، و بعدش سفارش‌های "bracket" از strategy.exit() معامله‌ی "Buy1" رو می‌بستن.


OCA groups

گروه‌های OCA یا "One-Cancels-All" به استراتژی اجازه می‌دن که وقتی یه سفارش از یه گروه اجرا می‌شه، بعضی از سفارش‌های دیگه‌ی اون گروه به‌طور کامل یا جزئی لغو بشن.
برای اینکه یه سفارش رو به یه گروه OCA اختصاص بدیم، باید از آرگومان oca_name داخل دستور ثبت سفارش استفاده کنیم.
دستورهای strategy.entry() و strategy.order() همچنین اجازه می‌دن که نوع OCA رو هم مشخص کنیم، که نشون می‌ده استراتژی بعد از اجرای یه سفارش دیگه تو همون گروه، سفارش فعلی رو لغو کنه، مقدارش رو کم کنه یا اصلاً کاری نکنه.

توجه:
همه‌ی دستوراتی که برای یه گروه OCA سفارش صادر می‌کنن، باید هم اسم گروه (oca_name) و هم نوع OCA (oca_type) یکسانی داشته باشن.
اگه دو تا دستور، oca_name یکسان ولی oca_type متفاوت داشته باشن، استراتژی اون‌ها رو دو گروه متفاوت در نظر می‌گیره.
به عبارتی، یه گروه OCA نمی‌تونه ترکیبی از strategy.oca.cancel، strategy.oca.reduce و strategy.oca.none باشه.

strategy.oca.cancel

وقتی یه دستور ثبت سفارش از strategy.oca.cancel به عنوان مقدار oca_type استفاده کنه، اگه یکی از سفارش‌های هم‌گروه (با همون oca_name) زودتر اجرا بشه، سفارش فعلی به‌طور کامل لغو می‌شه.

برای اینکه نشون بدیم این نوع OCA چطوری روی سفارش‌ها تأثیر می‌ذاره، اسکریپت زیر رو در نظر بگیر. این اسکریپت وقتی مقدار ma1 از ma2 عبور می‌کنه، سفارش ثبت می‌کنه.

  • اگه توی اون لحظه اندازه‌ی پوزیشن (strategy.position_size) صفر باشه، استراتژی دوتا سفارش توقفی (stop order) ثبت می‌کنه با strategy.order():
    • یکی سفارش خرید در قیمت سقف کندل (high)
    • یکی سفارش فروش در قیمت کف کندل (low)
  • اگه موقع تقاطع، استراتژی پوزیشن باز داشته باشه، با strategy.close_all() همه‌ی پوزیشن رو با یه سفارش مارکت می‌بنده.
//@version=6
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)

بسته به حرکت قیمت، ممکنه استراتژی هر دو سفارش استاپ رو پر کنه قبل از اینکه سفارش مارکت مربوط به strategy.close_all() ساخته بشه.
در این حالت، استراتژی از پوزیشن خارج می‌شه بدون اینکه اصلاً دستور strategy.close_all() رو اجرا کنه، چون هر دو سفارش اندازه‌ی یکسانی داشتن.

ما این رفتار رو توی نمودار پایین می‌بینیم، جایی که استراتژی چند بار پشت سر هم بین اجرای سفارش‌های "Long" و "Short" جابه‌جا شده، بدون اینکه هیچ سفارشی از طرف strategy.close_all() اجرا بشه.

برای اینکه جلوی حالتی رو بگیریم که استراتژی هر دو سفارش "Long" و "Short" رو پر کنه قبل از اینکه دستور strategy.close_all() اجرا بشه، می‌تونیم به استراتژی بگیم وقتی یکی از سفارش‌ها اجرا شد، اون یکی رو لغو کنه.

توی کد پایین، ما "Entry" رو به عنوان مقدار oca_name و strategy.oca.cancel رو به عنوان مقدار oca_type توی هر دو دستور strategy.order() گذاشتیم.
حالا وقتی استراتژی یکی از سفارش‌های "Long" یا "Short" رو اجرا کنه، اون یکی رو خودکار لغو می‌کنه و بعد صبر می‌کنه تا strategy.close_all() بیاد و پوزیشن رو ببنده.

//@version=6
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)

strategy.oca.reduce

وقتی یه دستور ثبت سفارش از strategy.oca.reduce به عنوان نوع OCA استفاده می‌کنه، استراتژی اون سفارش رو به‌طور کامل لغو نمی‌کنه اگه یه سفارش دیگه با همون oca_name اول اجرا بشه.
در عوض، اندازه‌ی اون سفارش رو به اندازه‌ی تعداد قرارداد/سهم/لات/واحدی که توی سفارش اول پر شده، کم می‌کنه. این ویژگی مخصوصاً برای ساخت استراتژی‌های خروج سفارشی خیلی کاربردیه.

مثال زیر یه استراتژی فقط خرید (long-only) رو نشون می‌ده که برای هر ورود جدید، یه سفارش استاپ‌لاس و دو تا سفارش حد سود (take-profit) می‌سازه.

وقتی میانگین متحرک سریع‌تر از روی میانگین کندتر رد می‌شه، اسکریپت یه سفارش ورود با qty = 6 ایجاد می‌کنه با استفاده از strategy.entry(). بعد با سه تا strategy.order() سفارش استاپ و دو تا سفارش لیمیت می‌فرسته:

  • سفارش "Stop" با قیمت stop و qty = 6 فرستاده می‌شه.
  • سفارش‌های "Limit 1" و "Limit 2" به ترتیب با قیمت‌های limit1 و limit2 و هر کدوم با qty = 3 فرستاده می‌شن.
//@version=6
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.99
    limit1 := close * 1.01
    limit2 := close * 1.02
    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)

وقتی این استراتژی رو به چارت اضافه می‌کنیم، می‌بینیم که اون‌جوری که انتظار داشتیم کار نمی‌کنه.
مشکل این اسکریپت اینه که سفارش‌هایی که با strategy.order() فرستاده می‌شن، به‌صورت پیش‌فرض عضو هیچ گروه OCA نیستن (بر خلاف strategy.exit() که سفارش‌هاش به‌طور خودکار عضو یه گروه OCA از نوع strategy.oca.reduce هستن).

چون توی این اسکریپت، سفارش‌های strategy.order() به هیچ گروه OCA اختصاص داده نشده‌ن، استراتژی بعد از اجرای یه سفارش، هیچ‌کدوم از سفارش‌های استاپ یا لیمیت باقی‌مونده رو کاهش نمی‌ده.

در نتیجه، اگه شبیه‌ساز بروکر سفارش استاپ رو پر کنه و حداقل یکی از سفارش‌های لیمیت رو هم پر کنه، مقدار کل معامله‌شده از اندازه پوزیشن خرید (long) بیشتر می‌شه، و باعث می‌شه یه پوزیشن فروش (short) باز بمونه.

برای اینکه استراتژی فقط خرید (long-only) ما طبق انتظار کار کنه، باید بهش بگیم که وقتی یکی از سفارش‌های استاپ یا لیمیت اجرا شد، اندازه‌ی سفارش‌های پرنشده‌ی دیگه رو کاهش بده. این کار باعث می‌شه بیشتر از مقدار پوزیشن خرید فعلی فروش انجام نشه.

توی کدی که پایین هست، ما "Bracket" رو به عنوان مقدار oca_name و strategy.oca.reduce رو به عنوان مقدار oca_type توی همه‌ی دستورهای strategy.order() مشخص کردیم.
این تغییرات به استراتژی می‌گن که هر بار یکی از سفارش‌های گروه "Bracket" پر شد، بقیه‌ی سفارش‌های اون گروه رو به اندازه‌ی مناسب کاهش بده.

توی این نسخه از استراتژی، دیگه هیچ‌وقت پوزیشن فروش (short) شبیه‌سازی نمی‌شه، چون مجموع سفارش‌های پرشده‌ی استاپ و لیمیت، هیچ‌وقت از اندازه‌ی پوزیشن خرید بیشتر نمی‌شن:

//@version=6
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.99
    limit1 := close * 1.01
    limit2 := close * 1.02
    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)

توجه کن که:

ما مقدار qty سفارش "Limit 2" رو از ۳ به ۶ تغییر دادیم، چون وقتی استراتژی سفارش "Limit 1" رو اجرا می‌کنه، اندازه‌ی سفارش "Limit 2" رو سه واحد کاهش می‌ده.
اگه مقدار qty برای "Limit 2" رو همون ۳ نگه می‌داشتیم، بعد از اجرای "Limit 1"، اندازه‌ی سفارش دوم به صفر می‌رسید و دیگه هیچ‌وقت اجرا نمی‌شد.

strategy.oca.none

وقتی یه دستور ثبت سفارش از strategy.oca.none به عنوان مقدار oca_type استفاده کنه، همه‌ی سفارش‌هایی که با اون دستور فرستاده می‌شن، کاملاً مستقل از هر گروه OCA اجرا می‌شن.
این مقدار (strategy.oca.none) مقدار پیش‌فرض برای oca_type توی دستورهای strategy.order() و strategy.entry() هست.


Currency

استراتژی‌های Pine Script™ می‌تونن توی محاسباتشون از ارزی متفاوت با ارز نمادی که روش معامله شبیه‌سازی می‌کنن استفاده کنن. برنامه‌نویس می‌تونه با استفاده از یه متغیر مثل currency.* و قرار دادن اون به عنوان آرگومان currency توی دستور strategy()، ارز حساب استراتژی رو مشخص کنه.

مقدار پیش‌فرض این پارامتر currency.NONE هست، یعنی استراتژی از همون ارزی استفاده می‌کنه که نمودار فعلی داره (یعنی syminfo.currency).
کاربر اسکریپت هم می‌تونه ارز پایه حساب رو از طریق تنظیمات اسکریپت، توی تب "Settings → Properties"، قسمت "Base currency" تغییر بده.

وقتی استراتژی از ارزی استفاده کنه که با ارز نمودار فرق داره، برای تبدیل نرخ ارز، از مقدار روز قبل یه جفت‌ارز مرتبط توی محبوب‌ترین صرافی استفاده می‌کنه.
اگه هیچ صرافی به‌طور مستقیم این نرخ رو ارائه نده، استراتژی از یه نماد واسطه (spread symbol) برای به‌دست آوردن نرخ تبدیل استفاده می‌کنه.

استراتژی همه‌ی مقادیر پولی، مثل سود و ضرر شبیه‌سازی‌شده رو در این نرخ ضرب می‌کنه تا اون‌ها رو بر حسب ارز حساب نشون بده.

برای اینکه نرخ تبدیلی که استراتژی ازش استفاده کرده رو به‌دست بیاری، می‌تونی تابع request.currency_rate() رو صدا بزنی و از syminfo.currency به عنوان from و از strategy.account_currency به عنوان to استفاده کنی.

توجه کن که:
برنامه‌نویس می‌تونه مستقیماً با استفاده از توابع strategy.convert_to_symbol() و strategy.convert_to_account()، مقدارها رو از ارز حساب به ارز نماد چارت و برعکس تبدیل کنه.

مثالی که پایین هست نشون می‌ده که چطوری تبدیل ارز روی مقادیر مالی استراتژی تأثیر می‌ذاره، و چطوری محاسبات نرخ تبدیل استراتژی دقیقاً با خروجی تابع request.currency_rate() یکی درمیاد.

توی ۵۰۰ کندل آخر، استراتژی توی هر کندل یه سفارش ورود با strategy.entry() ثبت می‌کنه، و با strategy.exit() یه سفارش حد سود و حد ضرر می‌ذاره که فقط یک تیک با قیمت ورود فاصله دارن.
اندازه‌ی هر سفارش ورودی برابر با 1.0 / syminfo.mintick هست (گرد شده به نزدیک‌ترین تیک)، یعنی سود یا ضرر هر معامله دقیقاً برابر با یه پوینت از ارز مظنه‌ی چارت هست.

ما currency.EUR رو به عنوان ارز حساب توی دستور strategy() تعیین کردیم، یعنی استراتژی همه‌ی مقادیر مالی رو در نرخ تبدیل ضرب می‌کنه تا بر حسب یورو محاسبه بشن.

این اسکریپت تغییر مطلق (absolute change) نسبت سود خالص استراتژی (strategy.netprofit) به ارزش هر پوینت نماد (syminfo.pointvalue) رو حساب می‌کنه تا مشخص کنه یک واحد از ارز نمودار، چند یورو می‌ارزه.

این مقدار رو در کنار خروجی تابع request.currency_rate() رسم می‌کنه. این تابع برای گرفتن نرخ تبدیل، از syminfo.currency (یعنی ارز چارت) به عنوان مبدأ (from) و از strategy.account_currency (یعنی ارز حساب) به عنوان مقصد (to) استفاده می‌کنه.

همون‌طور که توی تصویر نشون داده شده، دو تا نموداری که رسم شدن با هم هماهنگ هستن، که این موضوع نشون می‌ده هم استراتژی و هم توابع request.*() برای محاسبه نرخ تبدیل روزانه، از یه فرمول و منبع داده استفاده می‌کنن.

//@version=6
strategy("Currency Test", currency = currency.EUR)

if last_bar_index - bar_index < 500
    // ثبت سفارش ورود با حجمی که سود/ضررش برابر با 1 پوینت در ارز چارت باشه
    strategy.entry("LE", strategy.long, math.round_to_mintick(1.0 / syminfo.mintick))
    
    // سفارش خروج با حد سود و حد ضرر یک تیکی (یعنی هر معامله با 1 پوینت سود یا ضرر بسته می‌شه)
    strategy.exit("LX", "LE", profit = 1, loss = 1)

// رسم تغییر مطلق نسبت سود خالص به ارزش هر پوینت — نشون‌دهنده‌ی ارزش 1 واحد از ارز چارت به یورو
plot(
     math.abs(ta.change(strategy.netprofit / syminfo.pointvalue)), 
     "1 chart unit of profit/loss in EUR", 
     color = color.fuchsia, 
     linewidth = 4
 )

// رسم نرخ تبدیل بین ارز چارت و ارز حساب
plot(
     request.currency_rate(syminfo.currency, strategy.account_currency), 
     "Requested conversion rate", 
     color = color.lime
 )

توجه کن که:

وقتی استراتژی روی چارت‌هایی با تایم‌فریم بالاتر از “1D” (مثلاً هفتگی) اجرا می‌شه، برای محاسبه نرخ تبدیل از اطلاعات یه روز قبل از زمان بسته شدن کندل استفاده می‌کنه.
مثلاً روی چارت “1W”، نرخ تبدیل از قیمت بسته شدن روز پنج‌شنبه هفته‌ی قبل گرفته می‌شه.

اما روی کندل‌های لحظه‌ای (real-time)، همچنان از آخرین نرخ روزانه‌ی تأییدشده استفاده می‌کنه.


Altering calculation behavior

اسکریپت‌های استراتژی روی همه‌ی کندل‌های تاریخی موجود اجرا می‌شن و وقتی داده‌های جدید برسن، روی کندل‌های زنده (realtime) هم ادامه پیدا می‌کنن.
اما به‌صورت پیش‌فرض، استراتژی‌ها فقط بعد از بسته شدن هر کندل دوباره محاسبه می‌شن — حتی روی کندل‌های زنده.
همچنین، زودترین زمانی که شبیه‌ساز بروکر (broker emulator) می‌تونه سفارش‌هایی که استراتژی روی قیمت بسته‌شدن کندل ارسال کرده رو پر کنه، ابتدای کندل بعدی هست (یعنی توی باز شدن کندل بعد).

کاربر می‌تونه این رفتارها رو با استفاده از پارامترهای calc_on_every_tick، calc_on_order_fills، و process_orders_on_close در دستور strategy() یا از طریق تنظیمات اسکریپت در تب "Settings → Properties" (بخش‌های "Recalculate" و "Fill orders") تغییر بده.

توی بخش‌های بعدی، توضیح داده می‌شه که این تنظیمات دقیقاً چه تأثیری روی محاسبات استراتژی دارن.

# calc_on_every_tick

پارامتر calc_on_every_tick توی تابع strategy() مشخص می‌کنه که استراتژی توی کندل‌های زنده (realtime) هر چند وقت یه بار محاسبه بشه.
وقتی مقدارش true باشه، اسکریپت با هر تیک جدیدی که توی دیتای زنده می‌رسه، دوباره محاسبه می‌شه.
مقدار پیش‌فرض این پارامتر false هست، یعنی اسکریپت فقط وقتی اجرا می‌شه که یه کندل زنده کامل بسته بشه.

کاربر می‌تونه این رفتار رو از طریق گزینه‌ی “On every tick” توی تب "Settings → Properties" اسکریپت هم تغییر بده.

فعال‌ کردن این گزینه می‌تونه برای تست زنده (forward testing) مفید باشه، چون باعث می‌شه استراتژی از قیمت‌های لحظه‌ای توی محاسباتش استفاده کنه.
ولی این تنظیم روی کندل‌های تاریخی اثری نداره، چون دیتای تاریخی شامل تمام تیک‌ها نیست — شبیه‌ساز بروکر هر کندل تاریخی رو فقط با چهار تیک در نظر می‌گیره: open، high، low و close.

بنابراین کاربر باید با احتیاط از این گزینه استفاده کنه و محدودیتش رو بدونه.
اگه فعال‌ کردن محاسبه روی هر تیک باعث بشه استراتژی توی کندل‌های تاریخی و زنده رفتار متفاوتی داشته باشه، بعد از اینکه کاربر اسکریپت رو ری‌لود کنه، استراتژی دوباره رسم می‌شه (repaint می‌کنه).

این مثال نشون می‌ده که چطور فعال بودن محاسبه در هر تیک (با calc_on_every_tick = true) می‌تونه باعث repaint شدن استراتژی بشه.

توی این اسکریپت، هر وقت قیمت بسته شدن (close) به بالاترین مقدارش برسه، با strategy.entry() یه سفارش خرید (long) فرستاده می‌شه، و هر وقت close به پایین‌ترین مقدارش برسه، یه سفارش فروش (short) ثبت می‌شه.

توی خط اول strategy() گفتیم calc_on_every_tick = true، یعنی روی کندل‌های زنده (realtime)، استراتژی با هر آپدیت قیمتی (تیک جدید) دوباره محاسبه می‌شه و ممکنه سفارش ارسال کنه قبل از اینکه کندل بسته بشه.

//@version=6
strategy("Donchian Channel Break", overlay = true, calc_on_every_tick = true, pyramiding = 20)

int length = input.int(15, "Length")

float highest = ta.highest(close, length)
float lowest  = ta.lowest(close, length)

if close == highest
    strategy.entry("Buy", strategy.long)
if close == lowest
    strategy.entry("Sell", strategy.short)

// اگه کندل realtime باشه، پس‌زمینه نارنجی بشه
bgcolor(barstate.isrealtime ? color.new(color.orange, 80) : na)

plot(highest, "Highest", color = color.lime)
plot(lowest, "Lowest", color = color.red)

توی این اسکریپت مقدار pyramiding روی ۲۰ تنظیم شده، یعنی استراتژی می‌تونه تا سقف ۲۰ بار وارد پوزیشن بشه، با استفاده از دستور strategy.entry().

اسکریپت وقتی که barstate.isrealtime برابر با true باشه (یعنی کندل realtime باشه)، پس‌زمینه‌ی چارت رو نارنجی می‌کنه تا نشون بده داریم روی کندل زنده کار می‌کنیم.

وقتی این اسکریپت رو روی چارت اعمال می‌کنیم و اجازه می‌دیم چند تا کندل realtime اجرا بشن، خروجی‌ای که می‌بینیم به این شکله:

اسکریپت روی هر تیکی که close برابر با بالاترین مقدار بود، یه سفارش "Buy" (خرید) ثبت کرد — که این اتفاق توی هر کندل realtime چند بار افتاد.

همچنین، شبیه‌ساز بروکر (broker emulator) هر سفارش مارکت رو با قیمت لحظه‌ای همون لحظه پر کرد، نه الزاماً با قیمت باز شدن کندل بعدی.

اما وقتی چارت رو ری‌لود می‌کنیم، می‌بینیم که رفتار استراتژی تغییر کرده و نتایجش رو روی اون کندل‌ها دوباره رسم کرده (repaint کرده).
این بار، استراتژی فقط یه سفارش "Buy" برای هر کندلی که شرط برقرار بوده ثبت کرده، و شبیه‌ساز هم سفارش رو با قیمت باز شدن کندل بعدی پر کرده.

استراتژی دیگه چند ورود (entry) برای هر کندل انجام نمی‌ده، چون اون کندل‌هایی که قبلاً realtime بودن، الان historical شدن — و داده‌ی تاریخی اطلاعات کامل تیک‌ها رو نداره.

# calc_on_order_fills

پارامتر calc_on_order_fills توی تابع strategy() باعث می‌شه که استراتژی بلافاصله بعد از پر شدن یه سفارش دوباره محاسبه بشه. اینطوری می‌تونه از اطلاعات دقیق‌تری استفاده کنه و اگه لازم باشه، سفارش‌های بیشتری ثبت کنه بدون اینکه منتظر بسته شدن کندل بمونه.

مقدار پیش‌فرض این پارامتر false هست، یعنی به‌صورت عادی استراتژی بعد از پر شدن هر سفارش، خودش رو دوباره اجرا نمی‌کنه.
کاربر می‌تونه این رفتار رو با گزینه‌ی "After order is filled" توی تب "Settings → Properties" اسکریپت هم تغییر بده.

فعال‌ کردن این تنظیم می‌تونه به اسکریپت استراتژی اطلاعات بیشتری بده که در حالت عادی تا بعد از بسته شدن کندل در دسترس نیست — مثل میانگین قیمت پوزیشن شبیه‌سازی‌شده در یه کندل باز.

مثالی که پایین هست یه استراتژی ساده رو نشون می‌ده که هر وقت اندازه‌ی پوزیشن برابر با صفر باشه (strategy.position_size == 0)، با strategy.entry() یه سفارش "Buy" (خرید) ثبت می‌کنه.
اسکریپت از strategy.position_avg_price استفاده می‌کنه تا سطوح قیمتی استاپ‌لاس و تیک‌پرافیت رو برای دستور strategy.exit() حساب کنه و پوزیشن رو ببنده.

ما توی دستور strategy() مقدار calc_on_order_fills = true رو اضافه کردیم، یعنی استراتژی هر بار که یه سفارش "Buy" یا "Exit" پر می‌شه، دوباره محاسبه می‌شه.

هر وقت یه سفارش "Exit" پر می‌شه، مقدار strategy.position_size برمی‌گرده به صفر، که باعث می‌شه یه سفارش جدید "Buy" فعال بشه.
شبیه‌ساز بروکر سفارش "Buy" رو توی اولین تیک بعدی با یکی از قیمت‌های OHLC اون کندل پر می‌کنه.

بعد از اینکه سفارش خرید پر شد، استراتژی از مقدار به‌روزشده‌ی strategy.position_avg_price استفاده می‌کنه تا قیمت‌های جدید برای سفارش خروج (strategy.exit) رو حساب کنه (یعنی حد سود و حد ضرر رو طبق قیمت خرید جدید تنظیم می‌کنه).

//@version=6
strategy("Intrabar exit", overlay = true, calc_on_order_fills = true)

float stopSize   = input.float(5.0, "SL %", minval = 0.0) / 100.0
float profitSize = input.float(5.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) فعال نباشه، این استراتژی تا وقتی کندل بسته نشه، سفارش جدیدی ثبت نمی‌کنه.

بعد از اینکه یه سفارش خروج انجام بشه، استراتژی منتظر می‌مونه تا کندل تموم بشه، بعد یه سفارش "Buy" جدید ثبت می‌کنه،
و شبیه‌ساز بروکر اون سفارش رو توی تیک اول کندل بعدی (یعنی قیمت باز شدن کندل بعد) پر می‌کنه.

مهمه که بدونی فعال کردن calc_on_order_fills ممکنه تو بعضی شرایط باعث نتایج غیرواقعی توی استراتژی بشه، چون شبیه‌ساز بروکر ممکنه فرض کنه سفارش‌ها با قیمت‌هایی پر شدن که توی دنیای واقعی قابل دستیابی نیستن.

به‌همین خاطر، کاربر باید با احتیاط از این تنظیم استفاده کنه و منطق استراتژی خودش رو دقیق بررسی کنه.

مثلاً اسکریپت زیر، بعد از هر پر شدن سفارش و بسته شدن کندل، یه سفارش خرید "Buy" ثبت می‌کنه — این کار رو توی ۲۵ کندل آخر انجام می‌ده.

توی این حالت، استراتژی توی هر کندل تاریخی ۴ بار وارد پوزیشن می‌شه، چون شبیه‌ساز بروکر هر کندل تاریخی رو متشکل از ۴ تیک در نظر می‌گیره: open، high، low و close.

اما این رفتار غیرواقعیه، چون توی معامله واقعی معمولاً نمی‌تونی دقیقاً توی بالاترین یا پایین‌ترین قیمت کندل سفارشت رو پر کنی.

//@version=6
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)

# process_orders_on_close

به‌صورت پیش‌فرض، استراتژی‌ها سفارش‌ها رو روی قیمت بسته‌شدن (close) هر کندل شبیه‌سازی می‌کنن.
یعنی زودترین زمانی که اون سفارش‌ها می‌تونن پر بشن، و همین‌طور محاسبات استراتژی یا هشدارها (alerts) اجرا بشن، ابتدای کندل بعدی هست.

برنامه‌نویس‌ها می‌تونن این رفتار رو تغییر بدن و بگن که سفارش‌ها همون لحظه‌ای که کندل داره بسته می‌شه (یعنی تیک بسته‌شدن) پردازش بشن؛
برای این کار باید توی دستور strategy() پارامتر process_orders_on_close = true رو تنظیم کنن.
کاربرها هم می‌تونن این گزینه رو از بخش “Settings → Properties” و قسمت “Fill Orders / On Bar Close” فعال کنن.

این رفتار مخصوصاً وقتی به درد می‌خوره که:

  • داری یه استراتژی دستی رو بک‌تست می‌کنی که توش تریدر قبل از بسته شدن کندل از پوزیشن خارج می‌شه،
  • یا توی بازارهایی که ۲۴ ساعته نیستن (مثل بورس‌های سنتی)، الگوریتم‌ها توی زمان بسته بودن بازار (after hours) تنظیم شدن که اگه هشدار بعد از بسته شدن کندل ارسال شد، شاید هنوز شانس پر شدن داشته باشن قبل از روز بعد.

توجه کن که:

اگه از استراتژی‌هایی استفاده کنی که process_orders_on_close توشون فعاله و بخوای با اون‌ها هشدار (alert) به یه سرویس شخص ثالث بفرستی، ممکنه نتیجه‌های ناخواسته‌ای ایجاد بشه.

چرا؟ چون هشدارهایی که روی بسته‌شدن کندل فعال می‌شن، باز هم بعد از بسته شدن بازار ارسال می‌شن،
و سفارش‌هایی که توی دنیای واقعی بر اساس این هشدارها ارسال بشن، ممکنه تا وقتی بازار دوباره باز بشه، پر نشن.

دستورهای strategy.close() و strategy.close_all() یه پارامتر به اسم immediately دارن.
اگه مقدار این پارامتر true باشه، باعث می‌شه سفارش مارکتِ خروجی که ساخته می‌شه، توی همون تیک که ایجاد شده پر بشه.

این پارامتر یه راه جایگزین به برنامه‌نویس می‌ده تا فقط برای سفارش‌های مارکتِ بستن پوزیشن، رفتار process_orders_on_close رو اعمال کنه
بدون اینکه رو بقیه‌ی سفارش‌ها (مثل سفارش‌های ورود یا لیمیت) تأثیر بذاره.


Simulating trading costs

شبیه‌سازی هزینه‌های معاملاتی

گزارش عملکرد استراتژی‌ها وقتی کاربردی‌تر و واقعی‌تر می‌شن که هزینه‌های احتمالی معاملات دنیای واقعی رو هم در نظر بگیرن.
اگه هزینه‌های مربوط به معاملات توی شبیه‌سازی حساب نشن، ممکنه تریدر به اشتباه سودآوری تاریخی استراتژی رو بیشتر از واقعیت تصور کنه،
و این باعث بشه توی معاملات واقعی تصمیم‌هایی بگیره که بهینه نیستن.

توی Pine Script™، استراتژی‌ها ورودی‌ها (inputs) و پارامترهایی دارن که اجازه می‌دن هزینه‌های معاملاتی رو توی نتایج عملکرد شبیه‌سازی کنن.

Commission
کمیسیون همون کارمزدی‌ـه که یه بروکر یا صرافی موقع انجام معامله دریافت می‌کنه.
کمیسیون می‌تونه به‌صورت یه مبلغ ثابت برای هر معامله یا برای هر قرارداد/سهم/لات/واحد باشه، یا یه درصدی از کل ارزش معامله.
کاربرها می‌تونن مشخصات کمیسیون استراتژی رو با استفاده از آرگومان‌های commission_type و commission_value داخل تابع strategy() تعیین کنن، یا از طریق تنظیمات اسکریپت، توی تب "Properties" قسمت "Commission" تنظیمش کنن.

اسکریپت زیر یه استراتژی ساده‌ست که وقتی قیمت بسته شدن (close) برابر با بالاترین مقدار توی یه بازه باشه، یه پوزیشن خرید ("Long") به اندازه ۲٪ از موجودی حساب باز می‌کنه.
وقتی قیمت برابر با کمترین مقدار اون بازه بشه، اون پوزیشن رو می‌بنده:

//@version=6
strategy("Commission Demo", overlay=true, default_qty_value = 2, default_qty_type = strategy.percent_of_equity)

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))

نتایج توی Strategy Tester نشون می‌ده که استراتژی رشد مثبت سرمایه (equity growth) به اندازه ۱۷.۶۱٪ داشته توی بازه‌ی تست.
اما این نتایج بک‌تست هزینه‌هایی که بروکر یا صرافی ممکنه برای هر معامله بگیره رو حساب نکرده.
بیاید ببینیم چه اتفاقی می‌افته اگه یه کمیسیون کوچیک برای هر معامله توی شبیه‌سازی استراتژی در نظر بگیریم.

توی این مثال، ما این دو تا پارامتر رو به دستور strategy() اضافه کردیم:

commission_type = strategy.commission.percent,
commission_value = 1

یعنی استراتژی، روی هر معامله‌ای که انجام می‌ده، ۱٪ کارمزد در نظر می‌گیره.

//@version=6
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))

همون‌طور که توی مثال بالا می‌بینیم، بعد از اینکه ۱٪ کمیسیون به بک‌تست اضافه شد، سود خالص شبیه‌سازی‌شده‌ی استراتژی به‌طور چشم‌گیری کاهش پیدا کرد و فقط ۱.۴۲٪ شد.
همچنین منحنی سرمایه (equity curve) نوسانی‌تر شد و مقدار افت سرمایه (max drawdown) هم بیشتر شد.
این نتایج نشون می‌ده که کمیسیون چه تأثیر مهمی می‌تونه روی عملکرد فرضی یه استراتژی داشته باشه.

#Slippage and unfilled limits

لغزش قیمت و پُر نشدن سفارش‌های لیمیت

توی معاملات واقعی، ممکنه سفارش‌هامون با قیمتی که انتظار داشتیم انجام بشن، فرق داشته باشن. این اتفاق به‌خاطر نوسان‌های شدید بازار، نقدشوندگی پایین، اندازه‌ی سفارش، یا یه سری عوامل دیگه‌ی بازاریه. این اختلاف بین قیمتی که انتظار داشتیم و قیمتی که در واقع بروکر یا صرافی معامله رو انجام می‌ده، همون چیزیه که بهش می‌گیم slippage یا «لغزش قیمت».

لغزش قیمت یه چیز ثابت و قابل پیش‌بینی نیست. همیشه در حال تغییره و نمی‌شه دقیق شبیه‌سازی‌ش کرد. ولی اگه موقع بک‌تست یا تست زنده‌ی استراتژیمون، یه مقدار کوچیکی لغزش رو برای هر معامله در نظر بگیریم، نتایجی که می‌گیریم به واقعیت نزدیک‌تر می‌شن.

برای اینکه توی نتایج استراتژی‌مون لغزش رو مدل‌سازی کنیم، می‌تونیم به‌صورت تعداد تیک (tick) ثابت، یه مقدار مشخص کنیم. این کار یا از طریق اضافه کردن آرگومان slippage توی تابع strategy() انجام می‌شه یا از بخش “Settings/Properties” با وارد کردن مقدار “Slippage” امکان‌پذیره.

تو مثال پایین، نشون داده که وقتی لغزش قیمت رو شبیه‌سازی می‌کنیم، چطوری روی قیمتی که سفارش‌های مارکت باهاش پُر می‌شن تأثیر می‌ذاره. تو این اسکریپت، وقتی قیمت از یه EMA صعودی بالاتر می‌ره، یه سفارش خرید (Buy) به‌اندازه ۲٪ از سرمایه ثبت می‌کنه. وقتی قیمت می‌افته و می‌ره زیر EMA نزولی، پوزیشن رو می‌بنده. ما توی تابع strategy() مقدار slippage = 20 رو گذاشتیم، یعنی قیمت هر معامله‌ای که توی این شبیه‌سازی انجام می‌شه، به‌اندازه ۲۰ تیک در جهت معامله از قیمت اصلی فاصله می‌گیره.

اسکریپت از strategy.opentrades.entry_bar_index() و strategy.closedtrades.exit_bar_index() استفاده می‌کنه تا مقدارهای entryIndex و exitIndex رو به‌دست بیاره، که از اون‌ها برای مشخص کردن fillPrice سفارش استفاده می‌شه.

وقتی bar_index برابر با entryIndex باشه، fillPrice برابر با اولین مقدار strategy.opentrades.entry_price() هست.
و وقتی bar_index برابر با exitIndex باشه، fillPrice برابر با مقدار strategy.closedtrades.exit_price() توی آخرین معامله‌ی بسته‌شده هست.

اسکریپت قیمت پر شدن مورد انتظار (expected fill price) و قیمت پر شدن واقعی بعد از اعمال اسلیپیج (simulated fill price after slippage) رو با هم رسم می‌کنه تا تفاوت بینشون به‌صورت کامل مقایسه بشه:

//@version=6
strategy(
     "Slippage Demo", overlay = true, slippage = 20,
     default_qty_value = 2, default_qty_type = strategy.percent_of_equity
 )

int length = input.int(5, "Length")

float ma = ta.ema(close, length)

bool longCondition = close > ma and ma > ma[1]
bool shortCondition = close < ma and ma < ma[1]

if longCondition    
    strategy.entry("Buy", strategy.long)
if shortCondition
    strategy.close("Buy")

int entryIndex = strategy.opentrades.entry_bar_index(0)
int exitIndex  = strategy.closedtrades.exit_bar_index(strategy.closedtrades - 1)

float fillPrice = switch bar_index
    entryIndex => strategy.opentrades.entry_price(0)
    exitIndex  => strategy.closedtrades.exit_price(strategy.closedtrades - 1)

float expectedPrice = not 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(not na(fillPrice) ? open : na, "Expected fill price", "—", location.absolute, expectedColor)
plotchar(fillPrice, "Fill price after slippage", "—", location.absolute, filledColor)

توجه کن که:
از اونجایی که استراتژی روی همه‌ی سفارش‌ها یه مقدار ثابت اسلیپیج اعمال می‌کنه، ممکنه بعضی از سفارش‌ها توی شبیه‌سازی با قیمتی خارج از محدوده کندل پر بشن.
واسه همین، باید با احتیاط از این تنظیم استفاده کنی، چون اگه مقدار اسلیپیج خیلی زیاد باشه، می‌تونه نتایج تست رو غیرواقعی و خیلی بدتر از واقعیت نشون بده.

بعضی تریدرها ممکنه فکر کنن که با استفاده از سفارش‌های limit می‌تونن از اثرات منفی اسلیپیج جلوگیری کنن، چون برخلاف سفارش‌های مارکت، سفارش limit نمی‌تونه با قیمتی بدتر از مقدار مشخص‌شده اجرا بشه.
اما حتی اگه قیمت بازار به قیمت سفارش برسه، باز هم ممکنه سفارش limit پر نشه.
چون پر شدن یه سفارش limit به وضعیت واقعی بازار بستگی داره — باید نقدشوندگی کافی وجود داشته باشه و حرکت قیمت هم واقعاً از روی اون قیمت رد بشه تا سفارش پر بشه.

برای اینکه احتمال پر نشدن سفارش‌های limit توی بک‌تست در نظر گرفته بشه، کاربر می‌تونه مقدار backtest_fill_limits_assumption رو توی دستور strategy() مشخص کنه،
یا از گزینه‌ی “Verify price for limit orders” توی تب “Settings → Properties” استفاده کنه.

این تنظیم به استراتژی می‌گه که سفارش‌های limit فقط وقتی پر بشن که قیمت بازار به اندازه‌ی مشخصی از سفارش عبور کنه (مثلاً چند تیک بالاتر یا پایین‌تر بره).

اسکریپت زیر یه سفارش limit به اندازه ۲٪ از موجودی حساب می‌ذاره روی قیمت hlcc4 همون کندل،
وقتی که high برابر با بالاترین مقدار توی length کندل قبلی باشه و هیچ سفارش بازی (pending) وجود نداشته باشه.

استراتژی وقتی که low برابر با پایین‌ترین مقدار بشه، پوزیشن باز رو می‌بنده و همه‌ی سفارش‌ها رو لغو می‌کنه.
هر بار که سفارش فعال می‌شه، یه خط افقی روی قیمت limit رسم می‌شه و تا وقتی که پوزیشن بسته بشه یا سفارش کنسل بشه، هر کندل این خط آپدیت می‌شه.

//@version=6
strategy(
     "Verify price for limits example", overlay = true,
     default_qty_type = strategy.percent_of_equity, default_qty_value = 2
 )

int length = input.int(25, title = "Length")

//@variable رسم خط روی قیمت limit آخرین سفارش ورود
var line limitLine = na

// بالاترین high و پایین‌ترین low
highest = ta.highest(length)
lowest  = ta.lowest(length)

// وقتی high برابر با بالاترین مقدار باشه و خط limit قبلاً رسم نشده باشه:
if high == highest and na(limitLine)
    float limitPrice = hlcc4
    strategy.entry("Long", strategy.long, limit = limitPrice)
    limitLine := line.new(bar_index, limitPrice, bar_index + 1, limitPrice)

// وقتی low برابر با پایین‌ترین مقدار باشه، پوزیشن بسته می‌شه، سفارش‌ها لغو می‌شن و خط پاک می‌شه
if low == lowest
    strategy.cancel_all()
    limitLine := na
    strategy.close_all()

// آپدیت کردن انتهای خط روی کندل‌های جدید
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))

به‌صورت پیش‌فرض، اسکریپت فرض می‌کنه که هر وقت قیمت بازار به قیمت سفارش limit برسه، اون سفارش حتماً پر می‌شه.
اما توی دنیای واقعی معمولاً این‌طور نیست و همیشه تضمینی برای پر شدن سفارش‌های limit وجود نداره.

بیایم تأیید قیمت رو به سفارش‌های limit اضافه کنیم تا احتمال پر نشدن‌شون هم توی بک‌تست حساب بشه.
توی این مثال، ما backtest_fill_limits_assumption = 3 رو توی تابع strategy() گذاشتیم.

یعنی حالا سفارش‌های ورود فقط وقتی می‌تونن پر بشن که قیمت بازار سه تیک از قیمت limit رد شده باشه.

همون‌طور که می‌بینیم، فعال کردن این تنظیم باعث می‌شه بعضی از سفارش‌هایی که قبلاً توی شبیه‌سازی پر می‌شدن، دیگه پر نشن،
و زمان پر شدن بعضی سفارش‌های دیگه هم تغییر کنه.

توجه کن! تأیید قیمت سفارش limit می‌تونه زمان پر شدن بعضی سفارش‌ها رو تغییر بده.
اما استراتژی همچنان همون قیمت limit رو برای اجرای سفارش حفظ می‌کنه.

این حالت که بهش می‌شه گفت "جا‌به‌جایی زمانی" (time-warping)، یه نوع مصالحه‌ست که باعث می‌شه قیمت سفارش‌های limit تغییر نکنه،
ولی ممکنه باعث بشه سفارش توی زمانی پر بشه که در دنیای واقعی لزوماً امکان‌پذیر نیست.

برای همین، کاربر باید با احتیاط از این تنظیم استفاده کنه و موقع تحلیل نتایج استراتژی، محدودیت‌هاش رو در نظر بگیره.


Risk management

مدیریت ریسک

طراحی یه استراتژی که عملکرد خوبی داشته باشه، مخصوصاً توی دسته‌ای از بازارهای مختلف، کار آسونی نیست.
بیشتر استراتژی‌ها برای شرایط یا الگوهای خاصی از بازار طراحی می‌شن و اگه روی داده‌های دیگه اجرا بشن، ممکنه ضررهای کنترل‌نشده‌ای بدن.
برای همین، رفتار مدیریت ریسک توی یه استراتژی می‌تونه نقش خیلی مهمی توی عملکرد اون داشته باشه.

برنامه‌نویس‌ها می‌تونن با استفاده از دستورهای strategy.risk.*() توی کدشون شرایط مدیریت ریسک رو تعریف کنن.

استراتژی‌ها می‌تونن هر تعداد معیار مدیریت ریسک رو با هر ترکیبی توی خودشون داشته باشن.
همه‌ی این دستورهای مدیریت ریسک روی هر تیک و هر بار پر شدن سفارش اجرا می‌شن،
و هیچ راهی نیست که روی بعضی از اجراهای اسکریپت، این دستورها غیرفعال بشن.
فرقی نداره این دستورات کجای کد نوشته بشن، تا وقتی توی کد هستن، استراتژی ازشون پیروی می‌کنه،
مگر اینکه برنامه‌نویس خودش اون دستور رو از کد حذف کنه.

strategy.risk.allow_entry_in()

این دستور تعیین می‌کنه که استراتژی فقط در یه جهت خاص (مثلاً فقط خرید یا فقط فروش) اجازه ورود به معامله داشته باشه.
مثلاً اگه بنویسی فقط long، استراتژی فقط می‌تونه پوزیشن خرید باز کنه.
اگه یه دستور ورود برخلاف اون جهت صادر بشه، استراتژی اون رو تبدیل به یه سفارش خروج مارکت می‌کنه تا پوزیشن فعلی رو ببنده.

strategy.risk.max_cons_loss_days()

اگه استراتژی یه تعداد مشخص روز پشت سر هم ضرر بده،
این دستور همه‌ی سفارش‌های باز رو لغو می‌کنه، پوزیشن رو می‌بنده و جلوی ادامه‌ی معاملات رو می‌گیره.

strategy.risk.max_drawdown()

اگه مقدار افت سرمایه (drawdown) استراتژی به یه حد مشخص برسه،
این دستور همه‌ی سفارش‌های باز رو لغو می‌کنه، پوزیشن رو می‌بنده و اجازه نمی‌ده معامله‌ای ادامه پیدا کنه.

strategy.risk.max_intraday_filled_orders()

تعداد حداکثر سفارش‌هایی که می‌تونن توی یه روز معاملاتی (یا توی هر کندل اگه تایم‌فریم بیشتر از روزانه باشه) پر بشن رو مشخص می‌کنه.
اگه استراتژی بیشتر از اون تعداد سفارش پر کنه، همه‌ی سفارش‌های باز لغو می‌شن، پوزیشن بسته می‌شه و تا آخر اون جلسه معاملاتی دیگه معامله‌ای انجام نمی‌شه.

strategy.risk.max_intraday_loss()

حداکثر مقدار ضرری که استراتژی توی یه روز معاملاتی (یا هر کندل اگه تایم‌فریم بالاتر از روزانه باشه) می‌تونه تحمل کنه رو کنترل می‌کنه.
اگه ضرر استراتژی به اون حد برسه، همه‌ی سفارش‌ها لغو می‌شن، پوزیشن بسته می‌شه و تا پایان اون جلسه دیگه معامله‌ای انجام نمی‌شه.

strategy.risk.max_position_size()

بیشترین اندازه‌ی مجاز پوزیشن رو تعیین می‌کنه.
اگه یه دستور ورود باعث بشه اندازه‌ی پوزیشن از این مقدار بیشتر بشه، استراتژی خودش مقدار سفارش رو کم می‌کنه تا پوزیشن از اون حد بیشتر نشه.


Margin

مارجین

مارجین اون حداقل درصدی از یه پوزیشن بازه که تریدر باید توی حسابش نگه داره تا بتونه از بروکرش وام بگیره و بتونه لوریج دلخواهش رو داشته باشه یا نگه‌داره.

پارامترهای margin_long و margin_short توی تابع strategy() و همین‌طور تنظیمات "Margin for long/short positions" توی تب "Properties" اسکریپت، درصد مارجین برای پوزیشن‌های خرید (long) و فروش (short) رو مشخص می‌کنن.

مثلاً اگه یه تریدر مارجین پوزیشن‌های خرید رو روی ۲۵٪ بذاره، یعنی باید به اندازه ۲۵٪ از اندازه‌ی پوزیشن بازش پول توی حساب داشته باشه.
این یعنی می‌تونه تا سقف ۴۰۰٪ از سرمایش معامله بزنه (چون ۱۰۰ تقسیم بر ۲۵ می‌شه ۴، یعنی ۴ برابر پول موجود توی حسابش رو وارد معامله کنه).

اگه سرمایه‌ی شبیه‌سازی‌شده‌ی استراتژی نتونه ضرر یه معامله مارجینی رو پوشش بده، شبیه‌ساز بروکر یه margin call می‌زنه.
یعنی یا کل پوزیشن، یا بخشی از اون رو به اجبار می‌بنده (لیکویید می‌کنه).

مقداری که شبیه‌ساز می‌فروشه، چهار برابر مقداریه که لازمه تا ضرر جبران بشه. این کار باعث می‌شه توی کندل‌های بعدی دوباره margin call نخوره و پشت‌سرهم لیکویید نشه.

مراحلی که شبیه‌ساز برای حساب کردن مقدار لیکویید طی می‌کنه ایناست:

  1. مقدار سرمایه‌ای که برای گرفتن پوزیشن استفاده شده:
    Money Spent = تعداد × قیمت ورود
  2. ارزش فعلی پوزیشن (Market Value of Security - MVS):
    MVS = اندازه پوزیشن × قیمت فعلی
  3. سود یا ضرر باز:
    Open Profit = MVS - Money Spent
    اگه پوزیشن short باشه، این مقدار در منفی ۱ ضرب می‌شه.
  4. مقدار equity (ارزش حساب استراتژی):
    Equity = سرمایه اولیه + سود/ضرر خالص + سود/ضرر باز
  5. نسبت مارجین:
    Margin Ratio = درصد مارجین / 100
  6. مقدار مارجین مورد نیاز:
    Margin = MVS × Margin Ratio
  7. موجودی قابل استفاده:
    Available Funds = Equity - Margin
  8. مقدار ضرر:
    Loss = Available Funds / Margin Ratio
  9. مقدار پوزیشنی که باید فروخته بشه تا ضرر پوشش داده بشه (گرد شده به اندازه مجاز نماد):
    Cover Amount = TRUNCATE(Loss / قیمت فعلی)
  10. مقدار نهایی لیکویید (margin call):
    Margin Call Size = Cover Amount × 4

برای اینکه این محاسبه رو دقیق‌تر ببینی، کافیه استراتژی آماده‌ی Supertrend رو روی نماد NASDAQ:TSLA توی تایم‌فریم ۱روزه (1D) بندازی،
بعد توی تب "Properties" تنظیمات استراتژی، مقدار "Order size" رو روی ۳۰۰٪ از equity و "Margin for long positions" رو روی ۲۵٪ بذاری.

اولین ورود به پوزیشن توی قیمت باز شدن کندل مربوط به ۱۶ سپتامبر ۲۰۱۰ انجام شد.
استراتژی توی این نقطه ۶۸۲,۴۳۸ سهم خرید (اندازه پوزیشن) با قیمت ۴.۴۳ دلار (قیمت ورود).

بعد، توی ۲۳ سپتامبر ۲۰۱۰، وقتی قیمت به ۳.۹ دلار (قیمت فعلی) رسید،
شبیه‌ساز بروکر با صدور margin call به‌صورت اجباری ۱۱۱,۰۵۲ سهم رو لیکویید کرد.

محاسبات زیر نشون می‌دن که چطور شبیه‌ساز این مقدار رو برای margin call محاسبه کرده:

پول خرج‌شده:
682438 * 4.43 = 3023200.34

ارزش فعلی بازار پوزیشن (MVS):
682438 * 3.9 = 2661508.2

سود/ضرر باز:
2661508.2 - 3023200.34 = −361692.14

ارزش حساب (Equity):
1000000 + 0 − 361692.14 = 638307.86

نسبت مارجین:
25 / 100 = 0.25

مقدار مارجین مورد نیاز:
2661508.2 * 0.25 = 665377.05

موجودی قابل استفاده:
638307.86 - 665377.05 = -27069.19

مقدار ضرر:
-27069.19 / 0.25 = -108276.76

مقدار پوزیشنی که باید فروخته بشه:
TRUNCATE(-108276.76 / 3.9) = TRUNCATE(-27763.27) = -27763

سایز margin call:
-27763 * 4 = -111052

توجه کن که:
مقدار متغیر strategy.margin_liquidation_price نشون می‌ده که قیمت بازار اگه به چه سطحی برسه، margin call اتفاق می‌افته.
برای اطلاعات بیشتر در مورد نحوه کارکرد مارجین و فرمول محاسبه قیمت margin call یه پوزیشن، می‌تونی به صفحه‌ی مربوطه توی Help Center مراجعه کنی.


Using strategy information in scripts

استفاده از اطلاعات استراتژی داخل اسکریپت

توی فضای strategy.* و زیرمجموعه‌های اون، تعداد زیادی متغیر آماده وجود داره که به برنامه‌نویس‌ها اجازه می‌ده اطلاعات مربوط به معاملات و عملکرد استراتژی رو — مثل چیزهایی که توی Strategy Tester نشون داده می‌شن — مستقیماً توی منطق و محاسبات کدشون استفاده کنن.

چندتا از این متغیرها اطلاعات پایه‌ای درباره‌ی خود استراتژی نگه می‌دارن، مثل سرمایه اولیه، ارزش حساب، سود و ضررها، بیشترین رشد، بیشترین افت، و وضعیت پوزیشن باز:

  • strategy.account_currency
  • strategy.initial_capital
  • strategy.equity
  • strategy.netprofit و strategy.netprofit_percent
  • strategy.grossprofit و strategy.grossprofit_percent
  • strategy.grossloss و strategy.grossloss_percent
  • strategy.openprofit و strategy.openprofit_percent
  • strategy.max_runup و strategy.max_runup_percent
  • strategy.max_drawdown و strategy.max_drawdown_percent
  • strategy.position_size
  • strategy.position_avg_price
  • strategy.position_entry_name

همچنین این فضای نام (strategy.*) یه‌سری متغیر دیگه هم داره که اطلاعات کلی درباره معاملات رو نگه می‌دارن؛ مثل تعداد معاملات باز و بسته، تعداد معاملات سودده و ضررده، میانگین سود معاملات، و بیشترین مقدار پوزیشنی که استراتژی تا حالا نگه داشته:

  • strategy.opentrades
  • strategy.closedtrades
  • strategy.wintrades
  • strategy.losstrades
  • strategy.eventrades
  • strategy.avg_trade و strategy.avg_trade_percent
  • strategy.avg_winning_trade و strategy.avg_winning_trade_percent
  • strategy.avg_losing_trade و strategy.avg_losing_trade_percent
  • strategy.max_contracts_held_all
  • strategy.max_contracts_held_long
  • strategy.max_contracts_held_short

برنامه‌نویس‌ها می‌تونن از این متغیرها برای نشون دادن اطلاعات مربوط به عملکرد استراتژی روی چارت، ساختن منطق‌های معاملاتی اختصاصی، محاسبه‌ی معیارهای عملکرد سفارشی، و کارهای دیگه استفاده کنن.

این مثال چند تا کاربرد ساده از متغیرهای strategy.* رو نشون می‌ده. اسکریپت از این متغیرها هم برای شرط‌گذاری معاملات استفاده می‌کنه، هم برای نمایش اطلاعات روی چارت.

وقتی مقدار محاسبه‌شده‌ی rank از عدد ۱۰ عبور می‌کنه و هیچ پوزیشن بازی وجود نداره (strategy.opentrades == 0
اسکریپت با استفاده از strategy.entry() یه سفارش مارکت خرید به اسم "Buy" باز می‌کنه.

توی کندل بعدی (که سفارش توش پر می‌شه)، با strategy.exit() یه سفارش استاپ‌لاس می‌ذاره،
که قیمتش درصدی پایین‌تر از strategy.position_avg_price هست، این درصد رو کاربر به‌صورت ورودی مشخص می‌کنه.

اگه توی زمانی که پوزیشن بازه مقدار rank از ۸۰ عبور کنه، اسکریپت با strategy.close() پوزیشن رو می‌بنده.

علاوه بر این، اسکریپت یه جدول روی چارت رسم می‌کنه که اطلاعات مختلف استراتژی رو نشون می‌ده، از جمله:

  • سود خالص (strategy.netprofit) و درصدی اون (strategy.netprofit_percent)
  • ارز حساب (strategy.account_currency)
  • تعداد معاملات سودده (strategy.wintrades) و نسبت اون به کل معاملات بسته‌شده
  • نسبت میانگین سود به میانگین ضرر
  • فاکتور سود (نسبت سود ناخالص به ضرر ناخالص)

همچنین مقدار کل equity (سرمایه‌ی فعلی) رو توی یه پنجره جدا رسم می‌کنه
و پس‌زمینه‌ی این پنجره رو بسته به اینکه پوزیشن باز سودده هست یا ضررده، رنگی می‌کنه.

//@version=6
strategy(
     "Using strategy information demo", default_qty_type = strategy.percent_of_equity, default_qty_value = 5, 
     margin_long = 100, margin_short = 100
 )

int lengthInput = input.int(50, "Length", 1)
float slPercentInput = input.float(4.0, "SL %", 0.0, 100.0) / 100.0

float rank = ta.percentrank(close, lengthInput)
bool entrySignal = ta.crossover(rank, 10) and strategy.opentrades == 0
bool exitSignal  = ta.crossover(rank, 80) and strategy.opentrades == 1

switch
    entrySignal    => strategy.entry("Buy", strategy.long)
    entrySignal[1] => strategy.exit("SL", "Buy", stop = strategy.position_avg_price * (1.0 - slPercentInput))
    exitSignal     => strategy.close("Buy")

if barstate.islastconfirmedhistory or barstate.isrealtime
    var table dashboard = table.new(position.top_right, 2, 10, border_color = chart.fg_color, border_width = 1, force_overlay = true)
    string currency = strategy.account_currency

    dashboard.cell(0, 1, "Net P/L")
    dashboard.cell(
         1, 1, str.format("{0, number, 0.00} {1} ({2}%)", strategy.netprofit, currency, strategy.netprofit_percent), 
         text_color = chart.fg_color, bgcolor = strategy.netprofit > 0 ? color.lime : color.red
     )

    dashboard.cell(0, 2, "Winning trades")
    dashboard.cell(
         1, 2, str.format("{0} ({1, number, #.##%})", strategy.wintrades, strategy.wintrades / strategy.closedtrades), 
         text_color = chart.fg_color, bgcolor = strategy.wintrades > strategy.losstrades ? color.lime : color.red
     )

    dashboard.cell(0, 3, "Avg. win / Avg. loss")
    dashboard.cell(
         1, 3, str.format("{0, number, #.###}", strategy.avg_winning_trade / strategy.avg_losing_trade), 
         text_color = chart.fg_color, 
         bgcolor = strategy.avg_winning_trade > strategy.avg_losing_trade ? color.lime : color.red
     )

    dashboard.cell(0, 4, "Profit factor")
    dashboard.cell(
         1, 4, str.format("{0, number, #.###}", strategy.grossprofit / strategy.grossloss), text_color = chart.fg_color, 
         bgcolor = strategy.grossprofit > strategy.grossloss ? color.lime : color.red
     )

plot(strategy.equity, "Total equity", strategy.equity > strategy.initial_capital ? color.teal : color.maroon, 3)
bgcolor(
     strategy.openprofit > 0 ? color.new(color.teal, 80) : strategy.openprofit < 0 ? color.new(color.maroon, 80) : na, 
     title = "Open position highlight"
 )

توجه کن که:

  • این اسکریپت سفارش استاپ‌لاس رو یه کندل بعد از سفارش ورود ایجاد می‌کنه، چون برای تعیین قیمت استاپ‌لاس از strategy.position_avg_price استفاده می‌کنه. این متغیر فقط وقتی مقدار داره (یعنی na نیست) که پوزیشن باز باشه.
  • جدول فقط روی آخرین کندل دیتای تاریخی و روی همه کندل‌های ریل‌تایم نمایش داده می‌شه، چون توی Pine Script، جدول‌ها حالت تاریخی ندارن و نمی‌تونن توی گذشته دیده بشن. برای اطلاعات بیشتر می‌تونی بخش Reducing drawing updates توی صفحه‌ی Profiling and optimization رو ببینی.
  • ما توی تابع table.new() مقدار force_overlay = true رو گذاشتیم تا جدول روی خود پنجره‌ی اصلی چارت نمایش داده بشه، نه توی یه پنجره جدا.

#Individual trade information

اطلاعات تکیِ معامله

توی فضای strategy.* دو تا زیرمجموعه وجود داره که بهت اجازه می‌دن به اطلاعات جداگانه‌ی هر معامله دسترسی داشته باشی:

  • strategy.opentrades.*: اطلاعات مربوط به معامله‌هایی که هنوز بازن (یعنی بسته نشدن)
  • strategy.closedtrades.*: اطلاعات مربوط به معامله‌هایی که بسته شدن

با استفاده از این توابع آماده (built-in)، برنامه‌نویس‌ها می‌تونن به داده‌های دقیق و جزئی از هر معامله توی اسکریپتشون دسترسی پیدا کنن،
و ازشون برای تحلیل‌های عمیق‌تر یا محاسبات پیشرفته‌تر استفاده کنن.

هر دوی این زیرمجموعه‌ها توابع مشابهی دارن که اطلاعات مربوط به سفارش‌ها، هزینه‌های شبیه‌سازی‌شده، و سود و ضرر رو برمی‌گردونن.

لیست این توابع اینه:

ورود به معامله (Entry)

  • strategy.opentrades.entry_id() / strategy.closedtrades.entry_id()
    شناسه‌ی ورود به معامله
  • strategy.opentrades.entry_price() / strategy.closedtrades.entry_price()
    قیمت ورود به معامله
  • strategy.opentrades.entry_bar_index() / strategy.closedtrades.entry_bar_index()
    شماره‌ی کندلی که معامله توش باز شده
  • strategy.opentrades.entry_time() / strategy.closedtrades.entry_time()
    زمان دقیق باز شدن معامله
  • strategy.opentrades.entry_comment() / strategy.closedtrades.entry_comment()
    کامنت یا توضیحاتی که موقع ورود به معامله ثبت شده (در صورت وجود)

اندازه، سود و ضرر، کارمزد

  • strategy.opentrades.size() / strategy.closedtrades.size()
    اندازه پوزیشن (مثلاً چند سهم یا چند قرارداد)
  • strategy.opentrades.profit() / strategy.closedtrades.profit()
    سود یا ضرر فعلی یا نهایی معامله
  • strategy.opentrades.profit_percent() / strategy.closedtrades.profit_percent()
    سود یا ضرر به درصد
  • strategy.opentrades.commission() / strategy.closedtrades.commission()
    مقدار کارمزدی که برای این معامله در نظر گرفته شده

بیشترین سود باز و بیشترین ضرر باز

  • strategy.opentrades.max_runup() / strategy.closedtrades.max_runup()
    بیشترین سودی که در طول معامله اتفاق افتاده (قبل از بسته شدن یا الان)
  • strategy.opentrades.max_runup_percent() / strategy.closedtrades.max_runup_percent()
    همون مقدار بالا ولی به درصد
  • strategy.opentrades.max_drawdown() / strategy.closedtrades.max_drawdown()
    بیشترین ضرری که در طول معامله اتفاق افتاده
  • strategy.opentrades.max_drawdown_percent() / strategy.closedtrades.max_drawdown_percent()
    همون مقدار بالا ولی به درصد

خروج از معامله (فقط برای معاملات بسته‌شده)

  • strategy.closedtrades.exit_id()
    شناسه‌ی سفارش خروج
  • strategy.closedtrades.exit_price()
    قیمت خروج
  • strategy.closedtrades.exit_time()
    زمان دقیق خروج
  • strategy.closedtrades.exit_bar_index()
    شماره‌ی کندلی که توش معامله بسته شده
  • strategy.closedtrades.exit_comment()
    کامنت یا توضیحاتی که موقع خروج ثبت شده (در صورت وجود)

توجه کن که:

بیشتر توابع داخل این زیرمجموعه‌ها، تابع هستن.
اما زیرمجموعه‌ی strategy.opentrades.* یه متغیر خاص هم داره به اسم strategy.opentrades.capital_held.
مقدار این متغیر نشون می‌ده چقدر سرمایه توسط معامله‌های باز رزرو شده.

فقط زیرمجموعه‌ی strategy.closedtrades.* توابع .exit_*() داره که اطلاعات سفارش‌های خروج از معامله رو برمی‌گردونه.

همه‌ی توابع strategy.opentrades.*() و strategy.closedtrades.*() یه پارامتر به اسم trade_num دارن
که مقدار عدد صحیح (int) می‌گیره و مشخص می‌کنه به اطلاعات کدوم معامله‌ی باز یا بسته دسترسی داشته باشه.

اولین معامله‌ای که باز یا بسته شده، اندیسش صفره (0)
و آخرین معامله، اندیسش می‌شه یک عدد کمتر از مقدار strategy.opentrades یا strategy.closedtrades
(یعنی مثلاً اگه ۳ معامله بسته شده باشه، اندیس‌ها می‌شن ۰ و ۱ و ۲)

توی مثال زیر، استراتژی می‌تونه حداکثر پنج سفارش خرید (long) برای هر پوزیشن باز کنه
که هر کدوم یه شناسه‌ی مخصوص به خودش داره.

ورود به معامله زمانی انجام می‌شه که قیمت بسته‌شدن (close) از میانه (median) عبور کنه
ولی هنوز به بالاترین مقدار (high) نرسیده باشه،
و فقط در صورتی که تعداد معاملات باز کمتر از ۵ تا باشه.

خروج از هر معامله یا با دستور strategy.exit() (برای استاپ‌لاس) انجام می‌شه،
یا با strategy.close_all() که یه سفارش مارکت برای بستن همه‌ی پوزیشن‌ها می‌فرسته.

شناسه‌ی هر سفارش ورود، بر اساس تعداد معاملات باز ساخته می‌شه:
اولین سفارش اسمش "Buy0" هست، و آخرین حالت ممکن "Buy4" هست.

این اسکریپت توابع strategy.closedtrades.*() رو داخل یه حلقه for صدا می‌زنه تا به شناسه‌های ورود معاملات بسته‌شده، سود/ضررها، شماره کندل‌های ورود و خروج دسترسی پیدا کنه.
با استفاده از این اطلاعات، تعداد کل معاملات بسته‌شده با یه شناسه ورود مشخص، تعداد معاملات سودده، میانگین تعداد کندل‌ها در هر معامله و مجموع سود تمام معاملات رو محاسبه می‌کنه.
بعد اسکریپت این اطلاعات رو به‌صورت یه رشته فرمت‌شده می‌چینه و داخل یه جدول تک‌سلولی نشون می‌ده.

//@version=6
strategy(
     "Individual trade information demo", pyramiding = 5, default_qty_type = strategy.percent_of_equity, 
     default_qty_value = 1, margin_long = 100, margin_short = 100
 )

int lengthInput = input.int(50, "Length", 1)
string idInput = input.string("Buy0", "Entry ID to analyze", ["Buy0", "Buy1", "Buy2", "Buy3", "Buy4"])

float highest = ta.highest(close, lengthInput)
float lowest  = ta.lowest(close, lengthInput)
float median  = 0.5 * (highest + lowest)

if ta.crossover(close, median) and close != highest and strategy.opentrades < 5
    strategy.entry("Buy" + str.tostring(strategy.opentrades), strategy.long) 
    if strategy.opentrades == 0
        strategy.exit("SL", stop = lowest)

if close == lowest
    strategy.close_all()

int   trades  = 0
int   wins    = 0
float avgBars = 0
float totalPL = 0.0

if barstate.islastconfirmedhistory or barstate.isrealtime
    var table infoTable = table.new(position.middle_center, 1, 1, color.purple)
    for tradeNum = 0 to strategy.closedtrades - 1
        if strategy.closedtrades.entry_id(tradeNum) != idInput
            continue
        float profit = strategy.closedtrades.profit(tradeNum)
        trades  += 1
        wins    += profit > 0 ? 1 : 0
        avgBars += strategy.closedtrades.exit_bar_index(tradeNum) - strategy.closedtrades.entry_bar_index(tradeNum) + 1
        totalPL += profit
    avgBars /= trades

    string displayText = str.format(
         "ID: {0}\n\nTotal trades: {1}\nWin trades: {2}\nAvg. bars: {3}\nTotal P/L: {4} {5}",
         idInput, trades, wins, avgBars, totalPL, strategy.account_currency
     )
    infoTable.cell(0, 0, displayText, text_color = color.white, text_halign = text.align_left, text_size = size.large)

plot(highest, "Highest close", force_overlay = true)
plot(median, "Median close", force_overlay = true)
plot(lowest, "Lowest close", force_overlay = true)

توجه کن که:

این استراتژی می‌تونه تا پنج معامله خرید برای هر پوزیشن باز کنه، چون توی دستور strategy() مقدار pyramiding = 5 قرار داده شده. برای اطلاعات بیشتر بخش pyramiding رو ببین.

تابع strategy.exit() که توی این اسکریپت استفاده شده، برای همه‌ی ورودهای موجود توی پوزیشن باز، سفارش خروج می‌سازه چون هیچ from_entry مشخص نشده. برای فهم بهتر این رفتار، بخش Exits for multiple entries رو ببین.


Strategy alerts

هشدارهای استراتژی

توی Pine Script، اندیکاتورها (نه استراتژی‌ها) دو روش مختلف برای ساخت هشدار سفارشی دارن:
یکی alertcondition() که هر بار فقط یه شرط رو دنبال می‌کنه،
و یکی alert() که می‌تونه همزمان چندتا فراخوانی رو مدیریت کنه و انعطاف‌پذیری بیشتری داره از نظر تعداد هشدار، پیام هشدار و غیره.

اما استراتژی‌ها نمی‌تونن از alertcondition() استفاده کنن.
اونا فقط می‌تونن از تابع alert() استفاده کنن.
علاوه بر اون، هر دستور ثبت سفارش توی استراتژی‌ها (مثل strategy.entry() یا strategy.exit()) خودش به‌صورت پیش‌فرض سیستم هشدار داخلی داره
و لازم نیست برای هشدار دادن، کدی جدا بنویسی.
جزئیات دقیق‌تر این هشدارهای داخلی توی بخش Order Fill events توی صفحه‌ی Alerts توضیح داده شده.

وقتی یه استراتژی هم alert() رو داشته باشه و هم از توابع سفارش استفاده کنه،
توی پنجره‌ی “Create Alert” می‌تونی انتخاب کنی هشدار با چی فعال بشه:
رویدادهای alert()، پر شدن سفارش‌ها (order fill)، یا هر دو.

برای خیلی از استراتژی‌های معاملاتی، تاخیر بین فعال شدن هشدار و اجرای معامله زنده خیلی مهمه.
به‌طور پیش‌فرض، alert() توی استراتژی‌ها فقط می‌تونه روی بسته شدن کندل ریل‌تایم اجرا بشه،
درست مثل اینکه از alert.freq_once_per_bar_close استفاده شده،
حتی اگه توی فراخوانی تابع، یه چیز دیگه براش تعیین کرده باشی.

اگه بخوای این رفتار رو تغییر بدی، باید calc_on_every_tick = true رو توی strategy() بذاری
یا توی تنظیمات استراتژی (تب Settings/Properties)، گزینه‌ی “Recalculate/On every tick” رو فعال کنی قبل از ساختن هشدار.
ولی بسته به کدی که نوشتی، این تنظیم می‌تونه باعث تغییر رفتار استراتژی بشه.
برای اطلاعات بیشتر، بخش calc_on_every_tick رو ببین.

هشدارهای مربوط به پر شدن سفارش‌ها (order fill alerts) این محدودیت رو ندارن.
اونا بلافاصله و بدون توجه به تنظیم calc_on_every_tick اجرا می‌شن
و به همین خاطر گزینه‌ی بهتری برای فرستادن هشدار به سیستم‌های خودکار (مثل ربات‌ها) هستن.

می‌تونی پیام پیش‌فرض هشدار سفارش رو با استفاده از //@strategy_alert_message مشخص کنی.
متنی که اینجا وارد می‌کنی، توی فیلد “Message” توی پنجره‌ی “Create Alert” قرار می‌گیره.

اسکریپت زیر یه مثال ساده از پیام هشدار پیش‌فرض برای پر شدن سفارش رو نشون می‌ده.
بالای دستور strategy()، از @strategy_alert_message استفاده شده و داخل متن پیام، از چند تا جای‌نگهدار (placeholder) استفاده می‌کنه:

  • {{strategy.order.action}} برای نوع سفارش (خرید یا فروش)
  • {{strategy.position_size}} برای اندازه‌ی پوزیشن
  • {{ticker}} برای نماد فعلی
  • {{strategy.order.price}} برای قیمت پر شدن سفارش
//@version=6
//@strategy_alert_message {{strategy.order.action}} {{strategy.position_size}} {{ticker}} @ {{strategy.order.price}}
strategy("Alert Message Demo", overlay = true)

float fastMa = ta.sma(close, 5)
float slowMa = ta.sma(close, 10)

if ta.crossover(fastMa, slowMa)
    strategy.entry("buy", strategy.long)

if ta.crossunder(fastMa, slowMa)
    strategy.entry("sell", strategy.short)

plot(fastMa, "Fast MA", color.aqua)
plot(slowMa, "Slow MA", color.orange)

این اسکریپت وقتی کاربر اسمش رو از بخش “Condition” توی پنجره‌ی “Create Alert” انتخاب می‌کنه،
پیام پیش‌فرضی که با @strategy_alert_message تعریف شده رو توی فیلد “Message” به‌صورت خودکار قرار می‌ده.

وقتی هشدار فعال می‌شه، استراتژی جای‌نگهدارها (placeholders) رو با مقدار واقعیشون پر می‌کنه.


Notes on testing strategies

نکاتی درباره تست استراتژی‌ها

تست و بهینه‌سازی استراتژی‌ها در شرایط بازار چه گذشته چه زنده، می‌تونه دید خوبی نسبت به ویژگی‌ها، ضعف‌ها و حتی پتانسیل آینده‌ی یه استراتژی بهت بده.
ولی تریدرها همیشه باید حواسشون به محدودیت‌ها و خطاهای نتایج شبیه‌سازی‌شده باشه، مخصوصاً وقتی می‌خوان از اون نتایج برای تصمیم‌گیری در معاملات واقعی استفاده کنن.

توجه کن:
تست گرفتن روی داده‌های گذشته ممکنه اطلاعات مفیدی درباره‌ی کیفیت یه استراتژی بهت بده،
اما مهمه بدونی که نه گذشته و نه حال، تضمینی برای آینده نیستن.
بازارهای مالی می‌تونن خیلی سریع و غیرقابل‌پیش‌بینی تغییر کنن،
و این تغییرات ممکنه باعث بشه یه استراتژی دچار ضررهای سنگین و غیرقابل‌کنترل بشه.

از طرفی، نتایج شبیه‌سازی‌شده (چه بک‌تست چه فوروارد تست) معمولاً نمی‌تونن تمام فاکتورهای دنیای واقعی رو دقیق در نظر بگیرن—مثل لغزش قیمت، نقدشوندگی، خطاهای اجرایی و غیره.

پس پیشنهاد می‌شه تریدرها حتماً محدودیت‌ها و ریسک‌های بک‌تست و فوروارد تست رو به‌درستی درک کنن
و نتایج تست رو فقط یه بخش از فرآیند ارزیابی بدونن،
نه اینکه تصمیم‌گیری نهایی رو فقط بر اساس همون نتایج انجام بدن.

بک‌تست و فوروارد تست

بک‌تست یه روش برای بررسی عملکرد گذشته‌ی یه استراتژی یا مدل معاملاتی هست که با شبیه‌سازی و تحلیل نتایج اون روی داده‌های تاریخی بازار انجام می‌شه.
فرض این روش اینه که نتایج یه استراتژی در گذشته می‌تونه دیدی درباره‌ی نقاط قوت و ضعف اون به ما بده.
خیلی از تریدرها موقع بک‌تست، پارامترهای استراتژی رو تغییر می‌دن تا نتایج رو بهینه کنن.
این تحلیل و بهینه‌سازی روی داده‌های قبلی می‌تونه باعث بشه تریدر شناخت دقیق‌تری نسبت به رفتار استراتژی پیدا کنه.
با این حال، تریدر باید همیشه ریسک‌ها و محدودیت‌های تصمیم‌گیری بر اساس نتایج بهینه‌شده‌ی بک‌تست رو در نظر بگیره.

علاوه بر اون، بهتره از تحلیل زنده (Realtime / Forward Testing) هم استفاده بشه تا عملکرد استراتژی در شرایط واقعی بازار بررسی بشه.
فوروارد تست به‌دنبال اینه که ببینه استراتژی در بازار زنده چطور عمل می‌کنه؛
جایی که عواملی مثل هزینه‌های معامله، لغزش قیمت (slippage) و نقدشوندگی واقعاً روی عملکرد تاثیر می‌ذارن.

از مزیت‌های فوروارد تست اینه که دچار بعضی خطاهای رایج بک‌تست نمی‌شه،
مثل bias نگاه به آینده (lookahead bias) یا نشت اطلاعات از آینده (future data leakage).
ولی از طرف دیگه، محدودیت اصلیش اینه که فقط روی داده‌های محدودی قابل اجراست و طول می‌کشه تا اطلاعات کافی جمع بشه.

خطای نگاه به آینده

یکی از مشکلات رایج موقع بک‌تست گرفتن از استراتژی‌ها اینه که وقتی از تایم‌فریم دیگه‌ای داده می‌گیری، یا از متغیرهایی استفاده می‌کنی که مقدارشون ممکنه توی گذشته تغییر کنه (مثل timenow)، یا وقتی رفتار محاسباتی استراتژی رو طوری تنظیم می‌کنی که سفارش‌ها داخل یه کندل پر بشن، ممکنه اطلاعات آینده وارد گذشته بشه. به این اتفاق می‌گن lookahead bias.

این نوع خطا باعث می‌شه نتایج استراتژی غیرواقعی و فریب‌دهنده بشن، چون توی واقعیت هیچ‌کس نمی‌تونه از آینده خبر داشته باشه.
این خطا یکی از دلایل رایج ری‌پینت شدن استراتژی هم هست.

معمولاً می‌شه فهمید که یه استراتژی lookahead bias داره یا نه، با تست زنده (فوروارد تست).
چون توی چارت زنده، هیچ اطلاعاتی از کندل‌های آینده وجود نداره،
اگه استراتژی روی چارت‌های گذشته یه جور عمل کنه و روی چارت زنده یه جور دیگه،
نشون می‌ده که به احتمال زیاد دچار lookahead bias هست.

برای اینکه توی استراتژی دچار lookahead bias نشی:

  • از متغیرهایی که اطلاعات آینده رو وارد گذشته می‌کنن توی منطق ثبت یا کنسل کردن سفارش استفاده نکن.
  • توی توابع request.*() از barmerge.lookahead_on استفاده نکن، مگر اینکه طبق توضیحاتی که توی بخش Repainting هست، سری داده‌ها رو آفست داده باشی.
  • از رفتار محاسباتی واقع‌گرایانه برای استراتژی استفاده کن.

خطای انتخابی

خطای انتخابی زمانی اتفاق می‌افته که تریدر فقط نتایج استراتژی رو روی یه سری نماد یا تایم‌فریم خاص بررسی می‌کنه و بقیه رو نادیده می‌گیره.
این کار می‌تونه باعث بشه برداشت اشتباهی از قدرت یا پایداری استراتژی پیدا کنی،
و در نتیجه تصمیم‌گیری یا بهینه‌سازی معاملات تحت تأثیر قرار بگیره.

برای اینکه اثر این نوع خطا کمتر بشه، بهتره استراتژی‌هات رو روی چند نماد مختلف و ترجیحاً متنوع،
و همین‌طور تایم‌فریم‌های مختلف تست کنی،
و مهم‌تر از اون، نتایج ضعیف رو نادیده نگیری یا فقط دوره‌هایی که عملکرد خوب داشتن رو جدا نکنی.

بیش‌برازش (Overfitting)

یکی از مشکلات رایج موقع بهینه‌سازی استراتژی بر اساس نتایج بک‌تست، بیش‌برازش یا همون curve fitting هست.
یعنی استراتژی رو اون‌قدر برای داده‌های خاص تنظیم می‌کنی که فقط روی همون داده‌ها خوب جواب می‌ده.
اما وقتی داده‌ی جدیدی وارد بشه که استراتژی تا حالا ندیده، عملکردش ضعیف می‌شه و نمی‌تونه تعمیم پیدا کنه.

برای اینکه احتمال بیش‌برازش کمتر بشه و استراتژی قدرت تعمیم بیشتری داشته باشه،
یکی از روش‌های متداول اینه که داده‌های یه نماد رو به دو یا چند بخش تقسیم کنی:
یه بخش برای آموزش و بهینه‌سازی که بهش می‌گن "in-sample" (IS)،
و یه بخش جدا برای تست نهایی که بهش می‌گن "out-of-sample" (OOS).

توی این روش، تریدر اول پارامترهای استراتژی رو روی داده‌ی IS تنظیم می‌کنه،
بعد بدون هیچ دستکاری یا ریزتنظیم دیگه، همون استراتژی رو روی داده‌ی OOS تست می‌کنه.
این کار می‌تونه یه دید نسبی بده از اینکه استراتژی بعد از بهینه‌سازی چقدر ممکنه خوب عمل کنه.

اما حتی با این روش‌ها هم باید با احتیاط جلو رفت،
چون هیچ استراتژی‌ای نمی‌تونه عملکرد آینده رو تضمین کنه،
فرقی نمی‌کنه از چه داده‌ای برای تست یا بهینه‌سازی استفاده شده،
چون آینده ذاتاً قابل پیش‌بینی نیست.

محدودیت تعداد سفارش‌ها

خارج از حالت Deep Backtesting، یه استراتژی می‌تونه حداکثر تا ۹۰۰۰ تا سفارش رو دنبال کنه.
اگه تعداد سفارش‌هایی که استراتژی می‌سازه از ۹۰۰۰ تا بیشتر بشه، سفارش‌های قدیمی‌تر حذف می‌شن
و فقط اطلاعات آخرین سفارش‌ها نگه داشته می‌شه.

سفارش‌هایی که حذف می‌شن، دیگه توی Strategy Tester نشون داده نمی‌شن.
اگه با توابع strategy.closedtrades.* بری سراغ شناسه‌ی سفارش‌هایی که حذف شدن، نتیجه‌ش na برمی‌گرده.

متغیر strategy.closedtrades.first_index شماره‌ی اولین معامله‌ای رو نگه می‌داره که حذف نشده.
یعنی همون معامله‌ای که اول توی لیست معاملات (List of Trades) دیده می‌شه.
اگه تعداد سفارش‌های ساخته‌شده کمتر از ۹۰۰۰ تا باشه،
هیچ سفارشی حذف نمی‌شه و مقدار این متغیر صفره.