چگونه زیگزاگ‌های سریع و بدون ترسیم‌ مجدد بنویسیم

چگونه زیگزاگ‌های سریع و بدون ترسیم

چگونه زیگزاگ‌های سریع و بدون ترسیم‌ مجدد بنویسیم

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

رویکرد کلی

اول از همه، بیایید رویکرد کلی برای نوشتن یک اندیکاتور‌ را فرمول‌بندی کنیم. بنابراین:

  • تابع ()start هر اندیکاتوری (همانند هر اکسپرتی)، فراخوانی بودن یک تابع را نشان می‌دهد؛ به‌عبارت دیگر، تابعی که قرار است برای پردازش اتفاقی خاص فراخوانده شود. برای مثال، جهت پردازش یک تیک.
  • هدف از نوشتن یک اندیکاتور، به‌عنوان یک اصل، محاسبه‌ی یک یا چند مورد از خصوصیات بازار است. همراه با کمیت‌های جانبی مورد نیاز برای محاسبه، یک سری از متغیرهای کلیدی اندیکاتور نیز ایجاد می‌شوند. بیایید وضعیت این اندیکاتور‌ را اینگونه تعریف کنیم: یک سری از مقادیر [مربوط به] آن متغیرهای کلیدی در یک زمان مشخص. بر اساس این تعریف، اینگونه می‌توان گفت که:
    • با محاسبه‌ی مقادیر جدید متغیرها در یک تیک جدید، تابع()start وضعیت جدید اندیکاتور را محاسبه خواهد کرد.
    • بنابراین، در حقیقت، تابع()start یک عملگر است که اندیکاتور را از یک وضعیت به وضعیتی دیگر منتقل می‌کند.
  • در این شرایط، فرآیند نوشتن اندیکاتور تبدیل می‌شود به تعیین یک سری از کمیت‌هایی که وضعیت اندیکاتور را توصیف می‌کنند (متغیرهای وضعیت)، و نوشتن یک عملگر که این اندیکاتور را به وضعیتی جدید، هنگام دریافت تیک جدید، می‌بَرَد. مقداردهی اولیه متغیرهای وضعیت بخش حیاتی الگوریتم اندیکاتور است. در مثالی از نوع مشخصی از زیگزاگ‌ها، به شما نشان می‌دهیم که چگونه تمام این کارها را می‌توان انجام داد.

سوال شامل چه مدل زیگزاگ‌هایی است

همانطور که اشاره کردیم، در این مقاله به زیگزاگ‌هایی علاقه‌مندیم که در شکستن از میان سطح کندشونده، تغییر حالت می‌دهند. اما “سطح کندشونده” چیست؟ فرض کنید می‌خواهیم زیگزاگی بنویسیم که برای آن قله (رأس) ثابت است، و وقتی قیمت از آن قله به‌اندازه‌ی H نقطه جابه‌جا می‌شود، [باز هم] قله ثابت است. ثابت نگه داشتن قله یعنی تغییر جهت یک قسمت [(لِگ)] از زیگزاگ به‌‌سمت جهت مخالف. بیایید فقط حداقل (minimum) را فیکس کرده و اکنون [فرض را بر این بگیریم که] در آن قسمتی از زیگزاگ هستیم که رو به بالاست[لگ رو به بالا]. بیایید یک متغیر برای ماکسیسمم زمان قیمت یک بخش رو به بالای کامل‌نشده، معرفی کنیم، یعنی TempMax. این ماکسیمم را ثابت نگه داشته (و جهت را عوض می‌کنیم)، اگر قیمت از میان این سطح[ها] بشکند:

SwitchLevel = TempMax H *Point (سطح تغییر)

اگر ماکسیمم زمان، قبل از تغییر [جهت] آپدیت شود، آنگاه باید مقدار جدید SwitchLevel را محاسبه کنیم. بنابراین، SwitchLevel دنبال ماکسیمم زمان می‌رود، و H نقطۀ پشت سر آن است.

این وضعیت کاملاً برای یک لِگ رو به پایین (down-segment) متقارن خواهد بود: SwitchLevel اکنون دنبال مینیمم زمان (TempMin) می‌رود، و همانقدر، به‌اندازه‌ی H نقطه پشت سر آن است، اما این‌ دفعه خواهیم داشت:

SwitchLevel = TempMin + H *Point

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

مدل زیگزاگ

اکنون بیایید متغیرهای وضعیت زیگزاگ را تعیین کنیم.

اول از همه، جهت قسمت کنونی زیگزاگ. متغیر مربوطه را UpZ  می‌نامیم و مقادیرtrue  را برای قسمت‌های (لِگ‌های) رو به بالا وfalse  را برای قسمت‌های رو به پایین، اختصاص می‌دهیم.

مشخص است که، باید به لیست خودTempMax  وTempMin  را، که پیش‌تر معرفی شدند، اضافه کنیم. همچنین باید مختصات زمانی آنها را هم اضافه کنیم. هرچند، اینجا مقداری در تعریف واحدهای اندازه‌گیری آزاد هستیم. به‌عنوان یک مشخصه‌ی زمانی، از شماره‌ی کندل که از آغاز نمودار شروع می‌شود استفاده می‌کنیم؛ به‌عبارت دیگر، از سیستم شماره‌گذاری‌ایی‌ استفاده می‌کنیم که معکوس سیستم پذیرفته‌شده‌ در متاتریدر ۴ است. این کار هم کد را ساده می‌کند و هم سرعت اجرا را بالا می‌برد. بنابراین، لیست با متغیرهای TempMaxBar  و TempMinBar دوباره پُر خواهد شد.

قصد داریم هم زیگزاگ را روی یک نمودار رسم کنیم، و هم به‌نحوی از آن استفاده کنیم. بنابراین؛ ما به لیست خود خصوصیات آخرین قله‌های فیکس‌شده‌ی زیگزاگ را اضافه می‌کنیم: CurMax، CurMaxBar، CurMin، CurMinBar.

و این هم از لیست! فردی که نویسنده‌ی‌ نوعی خاصی از زیگزاگ است، می‌تواند آزادانه لیست را با کارهایی که می‌‌خواهد با زیگزاگ انجام دهد، دوباره پُر کند. برای مثال، شاید منطقی باشد که خصوصیات قله‌های پیشین را اضافه کنیم: PreMax، PreMaxBar، PreMin، PreMinBar. یا شاید نیاز داشته باشید، در چنین مواردی، خصوصیات تعدادی از قله‌های پیشین از پیش ‌تعریف‌شده را، با استفاده از آرایه‌ها، اضافه کنید.

اُپراتور انتقال

در رویکرد پیشنهادی، نوشتن یک اُپراتور انتقال برای زیگزاگ کاری نسبتاً ساده خواهد بود. فقط باید تعریف کلاس زیگزاگی که به آن علاقه‌مند هستیم را، به MQL4 ترجمه کنیم. نتیجه اینگونه خواهد بود:

// First, process the case of an up-segment
    if (UpZ) {
// Check whether the current maximum has changed
      if (High[pos]>TempMax) {
// If yes, then correct the corresponding variables
        TempMax = High[pos];
        TempMaxBar = Bars-pos;  // Here switching to direct numbering
      } else {
// If not, then check whether the slowing level has been broken through
        if (Low[pos]<SwitchLevel()) {
// If yes, then fix the maximum
          CurMax = TempMax;
          CurMaxBar = TempMaxBar;
// And draw a peak
          ZZ[Bars-CurMaxBar]=CurMax;  // Here switching to reverse numbering
// Correct the corresponding variables
          UpZ = false;
          TempMin = Low[pos];
          TempMinBar = Bars-pos;  // Here switching to direct numbering
        }
      }
    }  else {
// Now processing the case of down-segment
// Check whether the current minimum has changed
      if (Low[pos]<TempMin) {
// If yes, then correct the corresponding variables
        TempMin = Low[pos];
        TempMinBar = Bars-pos;  // Here switching to direct numbering
      } else {
// If not, then check whether the slowing level has been broken through
        if (High[pos]>SwitchLevel()) {
// If yes, then fix the minimum
          CurMin = TempMin;
          CurMinBar = TempMinBar;
// And draw a peak
          ZZ[Bars-CurMinBar]=CurMin;  // Here switching to reverse numbering
// Correct the corresponding variables
          UpZ = true;
          TempMax = High[pos];
          TempMaxBar = Bars-pos;  // Here switching to direct numbering
       }
      }
    }

اُپراتور انتقال آماده است. هر زمان که بخواهیم، می‌توانیم به متغیرهای وضعیت اندیکاتور مراجعه کنیم.

هرچند، چنین اُپراتوری ویژگی خاصی دارد که ممکن است هنگام رسم زیگزاگ، به‌مثابه یک خطا برداشت شود. بیایید این قسمتی که در پایین آورده شده‌است را با جزئیات بیشتری بررسی کنیم:

      if (High[pos]>TempMax) {
// If yes, then correct the corresponding variables
        TempMax = High[pos];
        TempMaxBar = Bars-pos;  // Here switching to direct numbering
      } else {
// If not, then check whether the slowing level has been broken through
        if (Low[pos]<SwitchLevel()) {
// If yes, then fix the maximum
          CurMax = TempMax;
          CurMaxBar = TempMaxBar;

استفاده از جفت ifelse یعنی Low کندلی که حاوی TempMax است، لحاظ نخواهد شد. وضعیت‌هایی که در آنها این قیمت کمتر از مینیمم فیکس‌شده‌ی بعدی باشد، ممکن است هنگام رسم زیگزاگ، به‌عنوان خطا (error) درنظر گرفته شوند. به‌ هر حال، آیا واقعاً خطاست؟

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

یک قسمت دیگر نیاز به برخی توضیحات دارد:

// Correct the corresponding variables
          UpZ = false;
          TempMin = Low[pos];
          TempMinBar = Bars-pos;  // Here switching to direct numbering
        }
      }

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

   // Correct the corresponding variables
          UpZ = false;
          TempMinBar = CurMaxBar+1;
          TempExtPos = Bars - TempMinBar;  // Here switching to reverse numbering
          TempMin = Low[TempExtPos];
          for (i=TempExtPos-1;i>=pos;i--) {
            if (Low[i]<TempMin) {
              TempMin = Low[i];
              TempMinBar = Bars-i;  // Here switching to direct numbering
            }
          }

در اینجا، Low کندلی که مینیمم آن فیکس‌شده است، بار دیگر، لحاظ نمی‌شود.

این دو یادداشت نیز مربوط به پردازش بخش‌های پایین می‌شوند.

اندیکاتور

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

مسئله‌ی مهم دیگری را هم باید دراین‌باره مدنظر داشت. کار جدی روی یک سیستم ترید، جمع‌آوری آمار از بخش تاریخچه را می‌طلبد. این آمار فقط در صورتی معتبر (صحیح) هستند که خصوصیات بدست‌آمده در زمان واقعی (real time) کاملاً با گزارش‌هایی که [از کار روی] بخش تاریخچه بدست آمده‌اند، مطابقت داشته باشند. ما تاریخچه‌ی تیک‌های واقعی را نداریم، بنابراین، فقط می‌توانیم انطباق کامل کاری را در زمان واقعی، با کندل‌های بسته‌شده (کامل‌شده) بدست آوریم.‌ حداکثر کاری که می‌توانیم برای کاهش تأخیر انجام دهیم این است که به تایم‌فریم‌های پایین‌تر، تا M1، برویم.

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

بنابراین، باید یک تابع استاندارد دیگر، یعنی ()Reset را اضافه کنیم. نتیجتاً اینکه آرزوی استفاده از ()IndicatorCounted خیلی کمک نخواهد کرد زیرا مانع انجام بررسی محاسبات مجدد می‌شود درحالیکه این محاسبات مجدد برای اندیکاتوری از این نوع، ضروری هستند. بررسی مذکور، اینگونه محقق می‌شود:

int start() {
//  Work with completed bars only
  if (Bars == PreBars) return(0);  
//  Check whether there are enough bars on the chart
  if (Bars < MinBars) {
    Alert(": Not enough bars on the chart");
    return(0);
  }  
//  If the history was not pumped, make calculations for the bar just completed
  if (Bars-PreBars == 1 && BarTime==Time[1]) StartPos = 1;
//  Otherwise, count the number of bars specified in function Reset() 
  else StartPos = Reset();
// Modify check variables
  PreBars = Bars;  
  BarTime=Time[0];
// Cycle on history
  for (pos=StartPos;pos>0;pos--) {

تابع ()Reset اینگونه خواهد بود:

int Reset() {
  if (MinBars == 0) MinBars = Bars-1;
  StartPos = MinBars;
  PreBars = 0;
  BarTime = 0;
  dH = H*Point;
  UpZ = true;
  TempMaxBar = Bars-StartPos;
  TempMinBar = Bars-StartPos;
  TempMax = High[StartPos];
  TempMin = Low[StartPos];
  StartPos++;
  return(StartPos);
}

در اینجا باید توجه مضاعف به متغیر اضافی dH داشته باشیم، که ما یکبار برای همیشه به آن مقدار آستانه‌ی تغییر زیگزاگ (H) را اختصاص می‌دهیم که به مقیاس قیمت تبدیل شده‌است. یک سوال ممکن است اینجا به‌وجود آید: چرا UpZ = true و نه false؟ جواب ساده است: بعد از تعداد کمی از بخش‌ها، اندیکاتور، به‌طور مستقل روی مقدار اولیه‌ی UpZ، همان نمودار را تحویل خواهد داد.

در نهایت، محاسبات سطح کندشونده را داریم:

double SwitchLevel() {
  double SwLvl;
  if (UpZ) SwLvl = TempMax - dH;
  else SwLvl = TempMin + dH;
  return(SwLvl);
}

دیگر همه‌چیز باید روشن شده باشد.

نتیجهگیری

یک قالب برای نوشتن اندیکاتورها، ZZTemplate، به این مقاله پیوست شده‌است. تمام کاری که باید انجام دهید این است که کد مورد نیاز را به تابع ()SwitchLevel اضافه کنید. برای تبدیل قالب به زیگزاگی که از آن در اینجا به‌عنوان یک مثال یاد شده‌است، تنها کافیست خط‌های زیر را پیدا کرده و روی آنها توضیحاتی اضافه کنید:

//extern int H = 33;
//double dH;
//  dH = H*Point;
//  if (UpZ) SwLvl = TempMax - dH;
//  else SwLvl = TempMin + dH;

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

توصیه‌های عمومی: در جایی که امکانش هست، عملکردها را درون عملگرهای If قرار دهید. برای مثالی (اما نه به‌عنوان مدل ایده‌آل) از بهینه‌سازی. لطفاً اندیکاتور پیوست‌شده‌ی HZZ را پیدا کنید، که یک مفهوم جایگزین برای زیگزاگ استفاده‌شده در این مقاله است. ساده‌بودن مسئله به ما اجازه می‌دهد که هم تابع() SwitchLevel و هم برخی از متغیرهای وضعیت را رها کنیم. به‌عنوان یک جایزه‌ی کوچک، من به HZZ نوشتن قله‌های زیگزاگ به‌شکل فایل و “در حین کارکردن [برنامه]” بررسی بعضی از خصوصیات آماری زیگزاگ را اضافه کرده‌ام.

این مقاله دارای فایل پیوست است.

فایل پیوست مقاله را از اینجا میتوانید دانلود کنید .

 

مقالات پیشنهادی :

M23admin

→ خواندن مطلب قبلی

اکسپرتی که برای معامله ساخته شده‌است. دستورالعمل یک تریدر…

خواندن مطلب بعدی ←

رویداد‌ها در متاتریدر ۴

۲۴ مورد نظر

نوشتن نظر شما

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *