function loadPage ()
return 'Hello MQL5!';
Làm chủ MQL5 từ người mới bắt đầu đến chuyên gia (Phần IV): Về Mảng, Hàm và Biến Đầu cuối Toàn cục
Category: Indicators
Published:

Làm chủ MQL5 từ người mới bắt đầu đến chuyên gia (Phần IV): Về Mảng, Hàm và Biến Đầu cuối Toàn cục

Làm chủ MQL5 từ người mới bắt đầu đến chuyên gia (Phần IV): Về Mảng, Hàm và Biến Đầu cuối Toàn cục

Giới thiệu

Bài viết này là phần tiếp nối của loạt bài dành cho người mới bắt đầu. Trong các bài viết trước, chúng tôi đã thảo luận chi tiết về các phương pháp mô tả dữ liệu được lưu trữ bên trong chương trình của chúng ta. Đến thời điểm này, người đọc nên biết những điều sau:

  • Dữ liệu có thể được lưu trữ trong các biến hoặc hằng số.
  • Ngôn ngữ MQL5 là một ngôn ngữ có kiểu chặt chẽ, nghĩa là mỗi mảnh dữ liệu trong chương trình có kiểu riêng của nó, mà trình biên dịch sử dụng để phân bổ bộ nhớ một cách chính xác và tránh các lỗi logic.
  • Các kiểu dữ liệu có thể là đơn giản (cơ bản) và phức tạp (do người dùng định nghĩa).
  • Để sử dụng dữ liệu trong chương trình MQL5, bạn cần khai báo ít nhất một hàm.
  • Bất kỳ khối mã nào cũng có thể được chuyển sang các tệp riêng biệt, và sau đó các tệp này có thể được bao gồm trong dự án bằng chỉ thị tiền xử lý #include.

Trong bài viết này, tôi sẽ đề cập đến ba chủ đề chính:

  • Data arrays, hoàn thiện phần chính về dữ liệu bên trong chương trình.
  • Global terminal variables, cho phép trao đổi dữ liệu đơn giản giữa các chương trình MQL5 khác nhau.
  • Một số tính năng của functions và sự tương tác của chúng với các biến.

Thông tin cơ bản về mảng

Một Array là một biến chứa một sequence của dữ liệu same-type.

Để mô tả một mảng, bạn cần mô tả kiểu của nó và tên biến, sau đó viết dấu ngoặc vuông. Số lượng phần tử trong một chuỗi nhất định có thể được chỉ định trong dấu ngoặc vuông.

int myArray[2]; // Mô tả một mảng các số nguyên chứa hai phần tử

Ví dụ 1. Mô tả một mảng tĩnh.

Chúng ta thường xuyên mô tả các chuỗi trong các chương trình MQL5. Chúng bao gồm giá lịch sử, thời gian mở nến, khối lượng giao dịch, và những thứ khác. Nói chung, bất cứ khi nào có datasets, mảng có thể là một lựa chọn tốt.

Việc đánh số các phần tử trong một mảng trong MQL5 luôn bắt đầu từ 0. Do đó, số của phần tử cuối cùng trong mảng sẽ luôn bằng số lượng phần tử của nó trừ đi một (lastElement = size - 1).

Để truy cập một phần tử của mảng, chỉ cần chỉ định chỉ số của phần tử đó trong dấu ngoặc vuông:

// Điền giá trị vào mảng:
myArray[0] = 3;
myArray[1] = 315;

// Xuất phần tử cuối cùng của mảng này ra nhật ký:
Print(myArray[1]); // 315

Ví dụ 2. Sử dụng các phần tử của mảng.

Tất nhiên, bất kỳ mảng nào cũng có thể được khởi tạo khi khai báo, giống như một cấu trúc, bằng cách sử dụng dấu ngoặc nhọn:

double anotherArray[2] = {4.0, 5.2};       // Một mảng hai phần tử được khởi tạo với hai giá trị
Print(DoubleToString(anotherArray[0],2)); // Xuất ra 4.00

Ví dụ 3. Khởi tạo mảng trong quá trình mô tả.

Mảng đa chiều

Một mảng có thể chứa các mảng khác bên trong nó. Những mảng lồng nhau như vậy được gọi là “đa chiều”.

Một ví dụ trực quan đơn giản về mảng đa chiều có thể là các trang sách. Các ký tự được tập hợp thành một dòng, đó là chiều thứ nhất, các dòng được tập hợp thành đoạn văn và tạo thành chiều thứ hai, và một trang là tập hợp các đoạn văn, tức là chiều thứ ba.

Mảng một chiều của các ký tự

Hình 1. Các ký tự được tập hợp thành một dòng - một mảng một chiều.

Mảng hai chiều của các ký tự

Hình 2. Các dòng được tập hợp thành đoạn văn - một mảng hai chiều.

Mảng ba chiều

Hình 3. Các đoạn văn được tập hợp thành trang - một mảng ba chiều.

Để mô tả các mảng như vậy trong MQL5, chỉ cần thêm dấu ngoặc vuông cho mỗi chiều mới. Các dấu ngoặc vuông cho các “thùng chứa bên ngoài” được đặt ở bên trái của các “thùng chứa bên trong”. Ví dụ, các mảng được hiển thị trong Hình 1-3 có thể được mô tả và sử dụng như sau:

char stringArray[21];       // Mảng một chiều
char paragraphArray[2][22]; // Mảng hai chiều
char pageArray[3][2][22];   // Mảng ba chiều

// Điền dữ liệu vào mảng hai chiều
paragraphArray[0][0]='T';
paragraphArray[0][1]='h';
// …
paragraphArray[1][20]='n';
paragraphArray[1][21]='.';

// Truy cập một phần tử bất kỳ của mảng hai chiều
Print(CharToString(paragraphArray[1][3])); // Sẽ in ra "a" (tại sao?)

Ví dụ 4. Mô tả mảng đa chiều cho Hình 1-3.

The total number of dimensions trong một mảng không được vượt quá 4. The maximum number of elements trong bất kỳ chiều nào là 2147483647.

Initialization của mảng đa chiều cũng đơn giản như mảng một chiều. Dấu ngoặc nhọn chỉ cần liệt kê các phần tử của mỗi mảng:

int arrayToInitialize [2][5] = 
  {
    {1,2,3,4,5},
    {6,7,8,9,10}
  }

Ví dụ 5. Khởi tạo mảng đa chiều.

Mảng động

Không phải tất cả các mảng chúng ta đều có thể biết ngay từ đầu sẽ có bao nhiêu phần tử. Ví dụ, các mảng chứa lịch sử terminal hoặc danh sách giao dịch sẽ thay đổi theo thời gian. Do đó, ngoài các mảng tĩnh được mô tả trong các phần trước, MQL5 cho phép bạn tạo các mảng động, tức là những mảng có thể thay đổi số lượng phần tử của chúng trong quá trình hoạt động của chương trình. Các mảng như vậy được mô tả giống hệt như mảng tĩnh, chỉ có số lượng phần tử không được chỉ định trong dấu ngoặc vuông:

int dinamicArray [];

Ví dụ 6. Mô tả mảng động

Một mảng mới được khai báo theo cách này không chứa phần tử nào, độ dài của nó là 0, và do đó không thể truy cập các phần tử của nó. Nếu chương trình cố gắng làm điều này, sẽ xảy ra lỗi nghiêm trọng và chương trình sẽ bị chấm dứt. Do đó, trước khi làm việc với mảng như vậy, cần phải đặt kích thước của nó bằng hàm tích hợp đặc biệt ArrayResize:

ArrayResize(dinamicArray, 1); // Tham số đầu tiên là mảng và tham số thứ hai là kích thước mới

ArrayResize(dinamicArray, 1, 100); // Tham số thứ ba là kích thước dự trữ (dư thừa)

Ví dụ 7. Thay đổi kích thước mảng động

Trong tài liệu ngôn ngữ, bạn có thể thấy rằng hàm này có thể nhận tối đa ba tham số, tuy nhiên tham số thứ ba có giá trị mặc định, nên bạn có thể bỏ qua nó, như tôi đã làm trong dòng đầu tiên của ví dụ.

Tham số đầu tiên của hàm này chắc chắn sẽ là mảng mà chúng ta đang thay đổi. Tham số thứ hai là kích thước mới của mảng. Tôi không nghĩ sẽ có vấn đề gì với điều này. Tham số thứ ba là “kích thước dự trữ”.

Kích thước dự trữ được sử dụng nếu chúng ta biết kích thước ultimate của mảng của mình. Ví dụ, theo điều kiện của bài toán, mảng không thể có quá 100 giá trị, nhưng chính xác bao nhiêu thì không biết. Khi đó bạn có thể sử dụng tham số reserve_size trong hàm này và đặt nó thành 100, như đã làm trong ví dụ 7 ở dòng thứ hai. Trong trường hợp này, hàm sẽ reserve kích thước bộ nhớ dư thừa cho 100 phần tử mặc dù kích thước actual của mảng will remain như được chỉ định trong tham số thứ hai (1 phần tử).

Tại sao lại phức tạp như vậy? Tại sao không chỉ thêm một phần tử tại một thời điểm khi cần?

Câu trả lời đơn giản là để tăng tốc chương trình của chúng ta.

Câu trả lời chi tiết hơn có thể mất nhiều thời gian để viết. Nhưng, nói ngắn gọn, vấn đề là mỗi khi chúng ta sử dụng hàm ArrayResize mà không có tham số thứ ba, chương trình của chúng ta yêu cầu hệ điều hành cấp thêm bộ nhớ. Việc phân bổ bộ nhớ này là một hoạt động khá dài (từ góc độ của bộ xử lý), và không quan trọng liệu bộ nhớ đó cần cho chỉ một phần tử hay cho nhiều phần tử cùng lúc. Chương trình càng ít phải làm điều này, càng tốt. Tức là, tốt hơn là dự trữ nhiều không gian ngay lập tức và sau đó điền vào nó, hơn là phân bổ một ít không gian rồi mở rộng nó. Tuy nhiên, ở đây cũng cần lưu ý rằng RAM là một tài nguyên có hạn, và do đó bạn sẽ luôn phải tìm sự cân bằng giữa tốc độ hoạt động và kích thước dữ liệu.

Nếu bạn biết giới hạn của mảng của mình, tốt hơn là nên chỉ rõ điều này một cách rõ ràng cho chương trình bằng cách khai báo mảng tĩnh hoặc dự trữ bộ nhớ bằng tham số thứ ba trong hàm ArrayResize. Nếu bạn không biết, thì mảng chắc chắn là động, và tham số thứ ba trong hàm ArrayResize không nhất thiết phải được chỉ định, mặc dù có thể, vì nếu kích thước thực tế của mảng lớn hơn kích thước dự trữ, MQL5 sẽ chỉ đơn giản phân bổ bộ nhớ thực tế cần thiết.

Khi mảng đã được thay đổi kích thước, như trong ví dụ 7, bạn có thể thay đổi dữ liệu bên trong nó:

dinamicArray[0] = 3; // Bây giờ mảng của chúng ta chứa chính xác một phần tử (xem ví dụ 7), chỉ số của nó là 0

Ví dụ 8. Sử dụng mảng đã được sửa đổi

Khi chúng ta làm việc với mảng động, thường thì nhiệm vụ là thêm dữ liệu vào cuối mảng này, chứ không phải thay đổi điều gì đó ở giữa (mặc dù điều này cũng xảy ra). Vì tại bất kỳ thời điểm nào, chương trình không biết mảng chứa bao nhiêu phần tử tại thời điểm đó, nên cần một hàm đặc biệt để tìm hiểu. Nó được gọi là ArraySize. Hàm này nhận một tham số - một mảng - và trả về giá trị nguyên cho số lượng phần tử trong mảng này. Và khi chúng ta biết kích thước chính xác của mảng động (mà hàm này sẽ trả về), việc thêm một phần tử trở nên khá đơn giản:

int size,          // Số lượng phần tử trong mảng
    lastElemet;    // Chỉ số của phần tử cuối cùng
char stringArray[]; // Mảng động ví dụ của chúng ta. 
                    // Ngay lập tức mô tả kích thước của nó là 0 (mảng không thể chứa phần tử)

// Thêm một phần tử vào cuối.
size = ArraySize(stringArray);     // Tìm kích thước hiện tại của mảng
size++;                            // Kích thước mảng nên tăng thêm 1
ArrayResize(stringArray, size, 2); // Thay đổi kích thước mảng. Trong ví dụ của chúng ta, mảng sẽ không có quá hai phần tử.
lastElemet = size - 1;             // Đánh số bắt đầu từ 0, nên số của phần tử cuối cùng ít hơn kích thước mảng 1

stringArray[lastElement] = 'H';    // Ghi giá trị

// Bây giờ thêm một phần tử nữa. Trình tự hành động hoàn toàn giống nhau.
size = ArraySize(stringArray);     // Tìm kích thước hiện tại của mảng
size++;                            // Kích thước mảng nên tăng thêm 1
ArrayResize(stringArray, size, 2); // Thay đổi kích thước mảng. Trong ví dụ của chúng ta, mảng sẽ không có quá hai phần tử.
lastElemet = size - 1;             // Đánh số bắt đầu từ 0, nên số của phần tử cuối cùng ít hơn kích thước mảng 1

stringArray[lastElement] = 'i';    // Ghi giá trị

// Lưu ý rằng khi thêm phần tử thứ hai theo cách này, chỉ có một dòng thay đổi: 
//   dòng ghi giá trị thực tế vào một ô cụ thể.
//
// Điều này có nghĩa là giải pháp có thể được viết dưới dạng ngắn hơn. Ví dụ, bằng cách tạo một hàm tùy chỉnh riêng cho nó.

Ví dụ 9. Thêm một phần tử vào cuối mảng động.

Các hàm ArraySize và ArrayResize được sử dụng liên tục khi làm việc với mảng động, thường kết hợp như trong ví dụ 9. Đối với các hàm khác ít được sử dụng hơn nhưng không kém phần hữu ích, vui lòng xem tài liệu.

Và để kết thúc phần này, tôi muốn lưu ý rằng ngôn ngữ MQL5 cũng hỗ trợ mảng động multidimensional, tuy nhiên, chỉ chỉ số đầu tiên có thể không xác định trong đó.

int a [][12]; // Không sao cả

// int b [][]; // Lỗi biên dịch: chỉ chỉ số đầu tiên có thể là động

Ví dụ 10. Mảng động đa chiều.

Nếu bạn thực sự cần làm cho nhiều chỉ số trở thành động, bạn có thể tạo một cấu trúc mà trường duy nhất sẽ là một mảng động, và sau đó tạo một mảng của các cấu trúc như vậy.

struct DinArray                         // Cấu trúc chứa một mảng động
     {
      int a          [];
     };

DinArray dinamicTwoDimensions [];       // Mảng động của các cấu trúc

ArrayResize(dinamicTwoDimensions, 1);   // Đặt kích thước của chiều ngoài
ArrayResize(dinamicTwoDimensions[0].a, 1); // Đặt kích thước của chiều trong

dinamicTwoDimensions[0].a[0] = 12; // Sử dụng ô để ghi dữ liệu

Ví dụ 11. Mảng với hai chỉ số động.

Có những phương pháp khác để giải quyết vấn đề này. Ví dụ, bạn có thể tạo lớp của riêng mình hoặc sử dụng một lớp đã tồn tại trong thư viện tiêu chuẩn. Tuy nhiên, tôi sẽ để chủ đề làm việc với các lớp cho các bài viết sau.

Mảng chuỗi thời gian

Giá Open, Close, High và Low, khối lượng tick và khối lượng thực, spread, thời gian bắt đầu nến và giá trị chỉ báo trên mỗi nến trong MQL5 được gọi là series hoặc time series.

Một lập trình viên MQL5 có no direct access đến các chuỗi thời gian này, nhưng ngôn ngữ cung cấp khả năng sao chép dữ liệu này vào bất kỳ biến nào bên trong chương trình của chúng ta bằng một tập hợp các hàm định sẵn đặc biệt (được liệt kê trong Bảng 1).

Nếu chúng ta cần, ví dụ, giá đóng cửa, bạn cần first create your own array, nơi các giá này sẽ được lưu trữ, và sau đó gọi hàm CopyClose function và truyền mảng đã tạo vào nó làm tham số cuối cùng. Hàm sẽ sao chép chuỗi thời gian tiêu chuẩn vào biến của chúng ta, và sau đó dữ liệu này có thể được sử dụng theo cách thông thường: sử dụng chỉ số trong dấu ngoặc vuông.

Tuy nhiên, các thao tác với chuỗi thời gian có phần khác biệt so với các mảng khác. Đây hiện là một hình thức đã được thiết lập theo truyền thống.

Trong bộ nhớ, chuỗi thời gian được lưu trữ giống như tất cả các dữ liệu khác: từ cũ nhất đến mới nhất. Nhưng các hàm thao tác dữ liệu chuỗi từ bảng 1 number các phần tử trong chuỗi thời gian theo reverse order, từ phải sang trái. Đối với tất cả các hàm này, nến số 0 sẽ là nến bên phải nhất, nến hiện tại, nến chưa hoàn thành. Nhưng các mảng “thông thường” không biết về điều này, và do đó đối với chúng, nến này sẽ là the last one. Điều này hơi gây nhầm lẫn…

Hãy cố gắng hiểu điều này bằng các hình ảnh sau.

Đánh chỉ số các phần tử trong mảng

Hình 4. Hướng đánh chỉ số trong mảng thông thường (mũi tên xanh lá) và trong chuỗi thời gian (mũi tên xanh dương).

Sao chép dữ liệu chuỗi thời gian vào mảng thông thường

Hình 5. Sao chép chuỗi vào mảng thông thường.

Hình 4 cho thấy sự khác biệt trong hướng đánh chỉ số của chuỗi thời gian và mảng thông thường.

Hình 5 trực quan hóa cách dữ liệu chuỗi thời gian có thể được sao chép vào mảng thông thường, ví dụ, sử dụng hàm CopyRates hoặc hàm tương tự (xem Bảng 1). Physical order của các phần tử trong bộ nhớ là giống nhau cho mảng thông thường và chuỗi thời gian, nhưng indexing changes, và phần tử đầu tiên trong chuỗi thời gian sau khi sao chép sẽ trở thành phần tử cuối cùng trong mảng thông thường.

Đôi khi việc lập trình trong khi luôn ghi nhớ những sắc thái này có thể bất tiện. Có hai cách để khắc phục sự bất tiện này:

  1. Hàm tiêu chuẩn ArraySetAsSeries cho phép bạn thay đổi hướng đánh chỉ số cho bất kỳ mảng dynamic nào. Nó nhận hai tham số: chính mảng và chỉ báo liệu nó có phải là chuỗi thời gian hay không (true/false). Nếu thuật toán của bạn liên quan đến việc sao chép dữ liệu luôn starts from the last candlestick, thường thì bạn có thể gán mảng đích làm chuỗi, và sau đó các hàm tiêu chuẩn và thuật toán của bạn sẽ sử dụng cùng chỉ số để làm việc.
  2. Nếu thuật toán của bạn liên quan đến việc sao chép small data fragments từ một arbitrary place on the chart, đặc biệt nếu số lượng của chúng được biết chính xác tại mỗi bước của thuật toán (ví dụ, chúng ta lấy giá đóng cửa của ba nến: nến đã đóng đầu tiên - tức là với chỉ số chuỗi 1 - và hai nến tiếp theo, với chỉ số chuỗi 2 và 3), thì tốt nhất là chấp nhận như vậy. Tốt hơn là chấp nhận rằng việc đánh chỉ số sẽ theo các hướng khác nhau và chỉ cần cẩn thận hơn khi lập trình. Một giải pháp khả thi là tạo một hàm riêng để kiểm tra các giá trị cần thiết và cố gắng sử dụng nó trong bất kỳ biểu thức nào.

Trong ví dụ sau, tôi đã cố gắng minh họa tất cả những điều trên bằng mã:

datetime lastBarTime;     // Chúng ta sẽ cố gắng ghi thời gian của nến cuối cùng vào biến này
datetime lastTimeValues[]; // Mảng sẽ lưu trữ thời gian của hai nến cuối cùng. 
                           // Nó là động để có thể biến thành chuỗi thời gian nhằm kiểm tra chỉ số

// Lấy thời gian bắt đầu của nến hiện tại bằng hàm iTime
lastBarTime = iTime
              (
                  Symbol(),      // Sử dụng biểu tượng hiện tại
                  PERIOD_CURRENT, // Cho khung thời gian hiện tại
                  0              // Nến hiện tại
              );
Print("Start time of the 0 bar is ", lastBarTime);

// Lấy thời gian bắt đầu của hai nến cuối cùng bằng hàm CopyTime
CopyTime
(
   Symbol(),      // Sử dụng biểu tượng hiện tại
   PERIOD_CURRENT, // Cho khung thời gian hiện tại
   0,             // Bắt đầu từ vị trí 0
   2,             // Lấy hai giá trị
   lastTimeValues  // Ghi chúng vào mảng lastTimeValues (mảng "thông thường")
);

Print("No series");
ArrayPrint(lastTimeValues,_Digits,"; "); // In toàn bộ mảng ra nhật ký. Dấu phân cách giữa các phần tử là dấu chấm phẩy

ArraySetAsSeries(lastTimeValues,true);   // Chuyển mảng thành chuỗi thời gian

Print("Series");
ArrayPrint(lastTimeValues,_Digits,"; "); // In lại toàn bộ mảng. Lưu ý thứ tự của dữ liệu

/* Đầu ra của script:

2024.08.01 09:43:27.000 PrintArraySeries (EURUSD,H4) Start time of the 0 bar is 2024.08.01 08:00:00
2024.08.01 09:43:27.051 PrintArraySeries (EURUSD,H4) No series
2024.08.01 09:43:27.061 PrintArraySeries (EURUSD,H4) 2024.08.01 04:00:00; 2024.08.01 08:00:00
2024.08.01 09:43:27.061 PrintArraySeries (EURUSD,H4) Series
2024.08.01 09:43:27.061 PrintArraySeries (EURUSD,H4) 2024.08.01 08:00:00; 2024.08.01 04:00:00

*/

Ví dụ 12. Kiểm tra các hàm làm việc với chuỗi thời gian.

Bảng 1. Danh sách các hàm để truy cập chuỗi thời gian. Đối với all các hàm này, việc đánh chỉ số các phần tử bắt đầu on the right, từ nến cuối cùng (chưa hoàn thành).

HàmHành động
CopyBufferLấy dữ liệu của bộ đệm indicator được chỉ định vào một mảng
CopyRatesLấy dữ liệu lịch sử cho biểu tượng và khung thời gian được chỉ định vào một mảng cấu trúc MqlRates
CopySeriesLấy nhiều chuỗi thời gian đồng bộ cho biểu tượng/khung thời gian được chỉ định với số lượng được chỉ định. Danh sách tất cả các mảng cần điền được truyền vào cuối, và thứ tự của chúng phải tương ứng với các trường của cấu trúc MqlRates.
CopyTimeLấy dữ liệu lịch sử về thời gian mở nến cho biểu tượng và khung thời gian tương ứng vào một mảng
CopyOpenLấy dữ liệu lịch sử về giá mở nến cho biểu tượng và khung thời gian tương ứng vào một mảng
CopyHighLấy dữ liệu lịch sử về giá High của nến cho biểu tượng và khung thời gian tương ứng vào một mảng
CopyLowLấy dữ liệu lịch sử về giá Low của nến cho biểu tượng và khung thời gian tương ứng vào một mảng
CopyCloseLấy dữ liệu lịch sử về giá đóng nến cho biểu tượng và khung thời gian tương ứng vào một mảng
CopyTickVolumeLấy dữ liệu lịch sử về khối lượng tick cho biểu tượng và khung thời gian tương ứng vào một mảng
CopyRealVolumeLấy dữ liệu lịch sử về khối lượng giao dịch cho biểu tượng và khung thời gian tương ứng vào một mảng
CopySpreadLấy dữ liệu lịch sử về spread cho biểu tượng và khung thời gian tương ứng vào một mảng
CopyTicksLấy các tick ở định dạng MqlTick vào một mảng
CopyTicksRangeLấy một mảng các tick trong phạm vi ngày được chỉ định
iBarShiftTrả về chỉ số của nến trong chuỗi chứa thời gian được chỉ định
iCloseTrả về giá Close của nến (được chỉ định bởi tham số ‘shift’) trên biểu đồ tương ứng
iHighTrả về giá High của nến (được chỉ định bởi tham số ‘shift’) trên biểu đồ tương ứng
iHighestTrả về chỉ số của giá trị cao nhất được tìm thấy trên biểu đồ tương ứng (dịch chuyển so với nến hiện tại)
iLowTrả về giá Low của nến (được chỉ định bởi tham số ‘shift’) trên biểu đồ tương ứng
iLowestTrả về chỉ số của giá trị nhỏ nhất được tìm thấy trên biểu đồ tương ứng (dịch chuyển so với nến hiện tại)
iOpenTrả về giá Open của nến (được chỉ định bởi tham số ‘shift’) trên biểu đồ tương ứng
iTimeTrả về thời gian mở của nến (được chỉ định bởi tham số ‘shift’) trên biểu đồ tương ứng
iTickVolumeiVolumeTrả về khối lượng tick của nến (được chỉ định bởi tham số ‘shift’) trên biểu đồ tương ứng
iRealVolumeTrả về khối lượng thực của nến (được chỉ định bởi tham số ‘shift’) trên biểu đồ tương ứng
iSpreadTrả về giá trị spread cho nến được chỉ định bởi tham số ‘shift’ trên biểu đồ tương ứng

Tạo hàm (chi tiết)

Bất kỳ hàm nào trong chương trình MQL5 được tạo bằng cùng một mẫu, mà chúng ta đã thảo luận ngắn gọn trong bài viết đầu tiên của loạt bài:

ResultType Function_Name(TypeOfParameter1 nameOfParameter1, TypeOfParameter2 nameOfParameter2)
  {
   // Mô tả biến kết quả và các biến cục bộ khác
   ResultType result;

   // …

   //---
      // Các hành động chính được thực hiện ở đây
   //---
   return result;
  }

Ví dụ 13. Mẫu mô tả hàm.

ResultType (và TypeOfParameter) đại diện cho any kiểu dữ liệu permissible. Đây có thể là int, double, tên lớp hoặc liệt kê, hoặc bất cứ thứ gì bạn biết.

Cũng có thể no parameters hoặc no explicit result của hoạt động hàm. Khi đó, thay vì kiểu kết quả hoặc thay vì danh sách tham số trong dấu ngoặc, từ ‘void’ được chèn vào. Nó đại diện cho kết quả rỗng.

Rõ ràng, nếu ResultType là void, thì không cần trả về dữ liệu, và do đó, dòng cuối cùng trong dấu ngoặc nhọn (return result) không cần phải được chỉ định, và cũng không cần thiết phải mô tả biến kết quả.

Dưới đây là một vài quy tắc đơn giản khác:

  • Function_Name và tên tham số phải tuân theo quy ước định danh.
  • Toán tử return chỉ trả về only one value. Nó không thể trả về nhiều hơn. Nhưng có những cách giải quyết, chúng ta sẽ thảo luận sau.
  • Một hàm không thể được mô tả bên trong một hàm khác, chỉ bên ngoài tất cả các hàm.
  • Bạn can mô tả nhiều hàm with the same name, nhưng với số lượng (hoặc kiểu) tham số khác nhau và/hoặc kiểu giá trị trả về khác nhau. Chỉ cần đảm bảo bạn có thể hiểu chính xác hàm nào nên được sử dụng trong một trường hợp cụ thể. Nếu bạn có thể phân biệt và giải thích những khác biệt này cho người không quen với mã của bạn, thì trình biên dịch cũng có thể.

Dưới đây là một số ví dụ minh họa cách các hàm có thể được mô tả:

//+------------------------------------------------------------------+
//| Ví dụ 1                                                        |
//| Các bình luận thường được sử dụng để mô tả hàm làm gì,         |
//|   dữ liệu nào nó cần và tại sao. Ví dụ, như thế này:           |
//|                                                                |           
//|                                                                |
//| Hàm trả về hiệu giữa hai số nguyên.                           |
//|                                                                |
//| Tham số:                                                      |
//|   int a là số bị trừ                                          |
//|   int b là số trừ                                             |
//| Giá trị trả về:                                               |
//|   hiệu giữa a và b                                            |
//+------------------------------------------------------------------+
int diff(int a, int b)
 {
  // Hành động rất đơn giản, chúng ta không tạo biến cho kết quả.
  return (a-b);
 }

//+------------------------------------------------------------------+ 
//| Ví dụ 1a                                                       | 
//| Hàm trả về hiệu giữa hai số thực.                             | 
//|                                                                |
//| Tên hàm giống như trong ví dụ trước, nhưng kiểu tham số       |
//| khác                                                          |
//|                                                                | 
//| Tham số:                                                      | 
//|   double a là số bị trừ                                       | 
//|   double b là số trừ                                          | 
//| Giá trị trả về:                                               | 
//|   hiệu giữa a và b                                            | 
//+------------------------------------------------------------------+ 
double diff(double a, double b)
  {
   return (a-b);
  }

//+------------------------------------------------------------------+
//| Ví dụ 2                                                        |
//| Minh họa việc sử dụng "void".                                  |
//| Gọi (sử dụng) hàm diff                                        |
//+------------------------------------------------------------------+
void test()
 {
  // Bạn có thể làm bất cứ điều gì bạn muốn.

  // Ví dụ, sử dụng hàm từ Ví dụ 1.
  Print(diff(3,4)); // kết quả là -1

  // Vì khi gọi hàm diff, các tham số nguyên được truyền trong dấu ngoặc, kết quả cũng là int.

  // Bây giờ hãy thử gọi cùng hàm với tham số độ chính xác kép
  Print(diff(3.0,4.0)); // kết quả là -1.0

  // Vì hàm được khai báo là "void", câu lệnh "return" không cần thiết
 }

//+------------------------------------------------------------------+
//| Ví dụ 3                                                        |
//| Hàm không có tham số để xử lý. Chúng ta có thể sử dụng        |
//|   dấu ngoặc trống như trong ví dụ trước hoặc sử dụng rõ ràng  |
//|   từ "void"                                                  |
//| Giá trị trả về:                                              |
//|   string nameForReturn là một tên nào đó, luôn giống nhau    |
//+------------------------------------------------------------------+
string name(void)
 {
  string nameForReturn="Needed Name";
  return nameForReturn;
 }

Ví dụ 14. Ví dụ về mô tả hàm theo mẫu.

Điều quan trọng là hiểu rằng làm việc với một hàm bao gồm hai giai đoạn: descriptionuse. Khi chúng ta describe một hàm, hàm này chưa làm gì cả. Nó chỉ là một thuật toán formal của các hành động. Ví dụ, hàm diff từ Ví dụ 14 có thể được mô tả bằng lời như sau:

  1. Lấy hai số nguyên (bất kỳ, chưa biết trước).
  2. Inside the algorithm, ký hiệu một trong số chúng là a và cái còn lại là b.
  3. Trừ b từ a.
  4. Đưa kết quả của phép tính (bất kỳ, chưa biết trước) cho người gọi (return to call point).

Nơi chúng ta nói về giá trị “bất kỳ, chưa biết trước”, biểu thức này có thể được thay thế bằng “tham số hình thức”. Các hàm được tạo ra để thực hiện một cách hình thức các hành động nhất định với dữ liệu any.

Các tham số được sử dụng when describing các hàm được gọi là “formal”.

Trong ví dụ 14, hàm diff chứa hai tham số hình thức, các hàm khác không có. Nói chung, một hàm có thể có nhiều tham số hình thức (tối đa 63).

Nhưng để có được một specific result, hàm phải được call (tức là sử dụng). Xem hàm test trong ví dụ 14, nơi nó gọi các hàm Print và diff. Ở đây, hàm sử dụng các giá trị rất cụ thể là actual tại thời điểm gọi: nội dung của biến hoặc hằng số, giá trị chữ (như trong ví dụ của tôi), kết quả của các hàm khác.

Các tham số chúng ta truyền cho hàm at the call time được gọi là “actual”.

Để call bất kỳ function nào, bạn cần chỉ định tên của nó và liệt kê các tham số thực tế trong dấu ngoặc. Các tham số thực tế phải correspond với các tham số hình thức về typequantity.

Trong ví dụ 14, hàm ‘test’ sử dụng chính xác hai số nguyên hoặc chính xác hai số thực để gọi hàm ‘diff’. Nếu tôi mắc lỗi và cố gắng viết một hoặc ba tham số, tôi sẽ nhận được lỗi biên dịch.

Phạm vi biến

Khi khai báo biến, bạn cần xem xét chúng được khai báo ở đâu.

  • Nếu biến được khai báo inside một hàm (bao gồm các tham số hình thức của hàm này), other functions will not see this variable (và do đó không thể sử dụng nó). Thông thường, biến như vậy “ra đời” tại thời điểm hàm được gọi và “chết” khi hàm hoàn thành công việc của nó. Biến như vậy được gọi là local.

    Nói chung, có thể nói rằng phạm vi của một biến được xác định bởi toàn bộ “thực thể” trong mã của chúng ta. Ví dụ, nếu một biến được khai báo trong dấu ngoặc nhọn, nó sẽ chỉ hiển thị bên trong những dấu ngoặc nhọn tạo thành khối đó, nhưng không hiển thị bên ngoài khối đó. Các tham số hình thức của một hàm thuộc về “thực thể” của hàm, nên chúng sẽ chỉ hiển thị trong hàm đó. Và cứ thế. Thời gian sống của biến như vậy bằng với thời gian sống của “thực thể” mà nó thuộc về. Ví dụ, một biến được khai báo trong một hàm được tạo ra khi hàm đó is called và bị hủy khi hàm kết thúc.

void OnStart()
  {
   //--- Biến cục bộ bên trong hàm hiển thị cho tất cả các khối của hàm đó, nhưng không vượt ra ngoài
   int myString = "This is local string";
   // Dấu ngoặc nhọn mô tả một khối bên trong hàm
    {
      int k=4;  // Biến cục bộ khối - chỉ hiển thị bên trong dấu ngoặc nhọn
      Print(k);  // Không sao
      Print(myString); 
    }
   // Print(k);    // Lỗi biên dịch. Biến k không tồn tại ngoài khối dấu ngoặc nhọn.
  }

Ví dụ 15. Biến cục bộ bên trong khối dấu ngoặc nhọn

  • Nếu một biến được khai báo outside mô tả của bất kỳ hàm nào, nó can be used by all functions của ứng dụng của chúng ta. Trong trường hợp này, thời gian sống của biến như vậy bằng với thời gian sống của chương trình. Biến như vậy được gọi là global.
int globalVariable = 345;

void OnStart()
  {
   //---
    Print(globalVariable); // Không sao
  }

Ví dụ 16. Một biến toàn cục hiển thị cho tất cả các hàm trong chương trình của chúng ta.

  • Một biến cục bộ có thể có cùng tên với một biến toàn cục, nhưng trong trường hợp này, biến cục bộ che giấu biến toàn cục đối với hàm đó.
int globalVariable=5;

void OnStart()
  {
    int globalVariable=10;  // Biến được mô tả theo tất cả các quy tắc, bao gồm kiểu. 
                            //   Nếu kiểu không được khai báo, biểu thức này sẽ thay đổi biến toàn cục
   //---
   
    Print(globalVariable);  // Kết quả là 10 - tức là giá trị của biến cục bộ

    Print(::globalVariable); // Kết quả là 5. Để in giá trị của biến toàn cục, không phải biến cục bộ,
                             //   chúng ta sử dụng hai dấu hai chấm trước tên
  }

Ví dụ 17. Tên trùng nhau của biến cục bộ và toàn cục. Biến cục bộ che giấu biến toàn cục.

Biến tĩnh

Có một trường hợp đặc biệt trong mô tả của biến local.

Như đã đề cập ở trên, các biến cục bộ mất giá trị của chúng sau khi hàm hoàn thành. Đây thường chính là hành vi được mong đợi. Tuy nhiên, có những tình huống khi bạn cần lưu giá trị của một biến cục bộ ngay cả sau khi hàm đã hoàn thành thực thi.

Ví dụ, đôi khi cần giữ một bộ đếm số lần gọi hàm. Một nhiệm vụ phổ biến hơn đối với các nhà giao dịch là tổ chức một hàm để kiểm tra sự bắt đầu của một nến mới. Điều này yêu cầu lấy giá trị thời gian hiện tại tại mỗi tick và so sánh nó với giá trị đã biết trước đó. Bạn có thể, tất nhiên, tạo một biến toàn cục cho mỗi bộ đếm này. Nhưng mỗi biến toàn cục làm tăng xác suất lỗi, vì các bộ đếm này được sử dụng bởi một hàm, trong khi các hàm khác không nên thay đổi và thậm chí không nên thấy chúng.

Trong những trường hợp như vậy, khi một biến cục bộ nên sống lâu như một biến toàn cục, các biến static được sử dụng. Chúng được mô tả giống như các biến thông thường, chỉ có từ static được thêm vào trước mô tả. Việc sử dụng này được thể hiện trong hàm HowManyCalls trong ví dụ sau:

//+------------------------------------------------------------------+
//| Hàm bắt đầu chương trình script                                  |
//+------------------------------------------------------------------+
void OnStart()
 {
  //---
  HowManyCalls();  
  HowManyCalls();  
  HowManyCalls();  
 }

//+------------------------------------------------------------------+
//| Hàm đếm số lần yêu cầu                                          |
//+------------------------------------------------------------------+
void   HowManyCalls()
 {
  //--- Mô tả biến. Biến là cục bộ, nhưng thời gian sống của nó dài.
  static int counter=0;  // Vì từ khóa 'static' được sử dụng, biến chỉ được khởi tạo
                         //   trước lần gọi hàm đầu tiên 
                         //   (chính xác hơn, trước khi gọi hàm OnInit)
  //--- Hành động chính
  counter++;             // Trong quá trình thực thi chương trình, giá trị sẽ được lưu giữ đến cuối

  //--- Kết quả hoạt động
  Print(IntegerToString(counter)+" calls");
 }

// Đầu ra của script:
// 1 calls
// 2 calls
// 3 calls

Ví dụ 18. Sử dụng biến tĩnh.

Ví dụ chứa hai hàm: HowManyCalls, sử dụng một biến tĩnh để đếm số lần gọi đến nó và in kết quả ra nhật ký, và OnStart, gọi HowManyCalls ba lần liên tiếp.

Truyền tham số hàm theo giá trị và theo tham chiếu

Theo mặc định, một hàm chỉ sử dụng data copies, được truyền cho nó làm tham số (các lập trình viên nói rằng trong trường hợp này dữ liệu được truyền “by value”). Do đó, ngay cả khi có gì đó được ghi vào một biến-tham số bên trong hàm, không có gì sẽ xảy ra với dữ liệu source.

Nếu chúng ta muốn dữ liệu source được thay đổi inside hàm, tham số hình thức có thể thay đổi phải được chỉ định bằng biểu tượng đặc biệt & (dấu và). Phương pháp mô tả tham số này được gọi là truyền by reference.

Để minh họa cách một hàm có thể sửa đổi dữ liệu bên ngoài, hãy tạo một tệp script mới chứa mã sau:

//+------------------------------------------------------------------+
//| Hàm bắt đầu chương trình script                                  |
//+------------------------------------------------------------------+
void OnStart(void)
  {
   //--- Khai báo và khởi tạo hai biến cục bộ
   int first = 3;
   int second = 77;
   //--- In giá trị của chúng TRƯỚC tất cả các thay đổi
   Print("Before swap: first = " + first + " second = " + second);
   //--- Sử dụng hàm Swap, nhận dữ liệu theo tham chiếu
   Swap(first,second);
   //--- Xem điều gì đã xảy ra
   Print("After swap: first = " + first + " second = " + second);
   //---

   //--- Áp dụng hàm CheckLocal cho dữ liệu đã nhận
   //--- Hàm này nhận tham số theo giá trị
   CheckLocal(first,second);
   //--- In kết quả lần nữa
   Print("After CheckLocal: first = " + first + " second = " + second);
  }

//+------------------------------------------------------------------+
//| Hoán đổi giá trị của hai biến số nguyên                         |
//|   Dữ liệu được truyền theo tham chiếu, nên bản gốc sẽ bị sửa đổi|
//+------------------------------------------------------------------+
void Swap(int &a, int& b) // Có thể thực hiện theo bất kỳ cách nào, cả hai vị trí đều đúng
  {
   int temp;
   //---
   temp = a;
   a = b;
   b = temp;
  }

//+------------------------------------------------------------------+
//| Nhận tham số theo giá trị, đó là lý do tại sao thay đổi chỉ xảy ra cục bộ|
//+------------------------------------------------------------------+
void CheckLocal(int a, int b)
  {
   a = 5;
   b = 10;
  }

// Đầu ra của script:
// Before swap: first = 3 second = 77
// After swap: first = 77 second = 3
// After CheckLocal: first = 77 second = 3

Ví dụ 19. Truyền tham số theo tham chiếu.

Mã này định nghĩa ba hàm ngắn: OnStart, Swap và CheckLocal.

CheckLocal nhận dữ liệu theo giá trị và do đó làm việc với các bản sao, Swap nhận hai tham số by reference, do đó, làm việc với dữ liệu nguồn. Hàm OnStart khai báo hai biến cục bộ, sau đó in giá trị của các biến đó, gọi các hàm Swap và CheckLocal, và hiển thị kết quả của sự tương tác bằng cách in giá trị của các biến cục bộ của nó ra nhật ký sau mỗi lần tương tác. Một lần nữa, tôi lưu ý rằng hàm Swap đã changed dữ liệu được truyền cho nó, nhưng CheckLocal không thể làm điều này.

Điều quan trọng cần lưu ý là tất cả các biến của kiểu complex types (như liệt kê, cấu trúc, đối tượng, v.v., cũng như bất kỳ mảng nào) phải always được truyền by reference.

Khi cố gắng truyền các biến như vậy theo giá trị, trình biên dịch sẽ tạo ra lỗi.

Và một lần nữa tôi sẽ liệt kê ngắn gọn các quy tắc cơ bản của sự tương tác giữa biến và hàm:

  • Các biến toàn cục trong ngôn ngữ MQL5 có thể được sử dụng trực tiếp từ trong bất kỳ hàm nào, bao gồm cả việc thay đổi giá trị của chúng.
  • Các biến cục bộ chỉ có thể truy cập trong khối nơi chúng được khai báo.
  • Nếu một tham số hình thức mô tả việc truyền dữ liệu “theo giá trị”, hàm không thể thay đổi dữ liệu gốc, ngay cả khi nó thay đổi giá trị của biến tham số bên trong. Nhưng dữ liệu được truyền “theo tham chiếu” có thể thay đổi tại vị trí gốc của nó.
  • Nếu một biến toàn cục và một biến cục bộ có cùng tên, biến cục bộ được ưu tiên (nói cách khác, biến cục bộ overrides biến toàn cục).
  • Thời gian sống của các biến toàn cục bằng với thời gian sống của chương trình, và các biến cục bộ bằng với thời gian sống của khối nơi chúng được mô tả.

Giá trị mặc định cho tham số hình thức của hàm

Các tham số hình thức có thể được gán giá trị mặc định.

Ví dụ, nếu chúng ta tạo một hàm ghi nhật ký, chúng ta có thể cần phân biệt các thông điệp của hàm này với tất cả các thông điệp terminal khác. Cách dễ nhất để làm điều này là thêm một tiền tố vào đầu thông điệp gốc và một hậu tố vào cuối. Chuỗi chính nó phải luôn được chỉ định, nếu không ý nghĩa của hàm sẽ bị mất. Các “phần bổ sung” có thể là tiêu chuẩn hoặc thay đổi.

Mã đơn giản nhất để minh họa ý tưởng này được đưa ra dưới đây:

//+------------------------------------------------------------------+
//| Thêm tiền tố và hậu tố vào chuỗi                                |
//+------------------------------------------------------------------+
string MakeMessage(
  string mainString,
  string prefix="=== ",
  string suffix=" ==="
)
 {
  return (prefix + mainString + suffix);
 }

Ví dụ 20. Mô tả hàm với các tham số hình thức mặc định

Khi gọi hàm này, một hoặc cả hai tham số có giá trị mặc định có thể được bỏ qua. Nếu các tham số này không được chỉ định rõ ràng, hàm sẽ sử dụng các giá trị được chỉ định trong mô tả. Ví dụ:

Print(MakeMessage("My first string"));               // Tiền tố và hậu tố mặc định
Print(MakeMessage("My second string", "~~ "));       // Tiền tố thay đổi, hậu tố không đổi
Print(MakeMessage("My third string", "~~ ", " ~~")); // Cả hai tham số thực tế đã thay đổi

// Đầu ra của script:
// === My first string ===
// ~~ My first string ===
// ~~ My first string ~~

Ví dụ 21. Các tham số thực tế có giá trị mặc định có thể được bỏ qua

Các tham số có giá trị mặc định chỉ có thể là liên tục và phải được mô tả after all các tham số khác không có giá trị như vậy.

Làm thế nào để hàm trả về nhiều kết quả

Như đã đề cập ở trên, toán tử return chỉ có thể trả về một kết quả. Hơn nữa, hàm không thể trả về mảng. Nhưng nếu bạn thực sự cần nhiều giá trị trả về hơn thì sao? Ví dụ, nếu bạn cần sử dụng một hàm để tính toán cả thời gian đóng nến và giá hoặc lấy danh sách các công cụ có sẵn? Trước tiên, hãy thử tự tìm giải pháp, sau đó so sánh với những gì được viết dưới đây.

Để giải quyết vấn đề được nêu trong tiêu đề, bạn có thể sử dụng một trong các phương pháp sau:

  • Tạo một kiểu dữ liệu complex data type (ví dụ, một cấu trúc) và trả về một biến của kiểu đó.
  • Sử dụng truyền tham số by reference. Điều này, tất nhiên, sẽ không giúp bạn trả về dữ liệu này bằng câu lệnh ‘return’, nhưng nó sẽ cho phép bạn ghi lại bất kỳ giá trị nào và sau đó sử dụng chúng.
  • Sử dụng global variables (not recommended). Phương pháp này tương tự như phương pháp trước, nhưng tiềm ẩn nguy cơ lớn hơn cho mã. Tốt hơn là sử dụng biến toàn cục ở mức tối thiểu, chỉ nơi thực sự không thể làm mà không có chúng. Nhưng nếu bạn thực sự cần, bạn có thể thử làm điều này.

Các sửa đổi biến toàn cục: input và extern

Cũng có “các trường hợp đặc biệt” khi sử dụng biến toàn cục. Chúng bao gồm:

  • Mô tả các tham số đầu vào của chương trình bằng sửa đổi ‘input’
  • Sử dụng sửa đổi ‘extern’

Tham số đầu vào

Mỗi input parameter của một chương trình viết bằng MQL5 được mô tả như một global variable (bên ngoài tất cả các hàm) và được chỉ định bằng từ khóa input, được đặt ở đầu mô tả.

input string smart = "The smartest decision"; // Cửa sổ sẽ chứa mô tả này

Ví dụ 22. Mô tả các tham số đầu vào

Thông thường, cột bên trái của cửa sổ thuộc tính hiển thị tên biến. Tuy nhiên, nếu the same line, nơi biến này được mô tả, chứa một comment, như trong ví dụ 22, bình luận này sẽ được hiển thị thay vì tên biến.

Trong các chương trình viết bằng MQL5, các biến được đánh dấu là input, chỉ có thể truy cập như read only, trong khi bạn không thể ghi bất cứ thứ gì vào chúng. Giá trị của các biến này chỉ có thể được đặt trong mô tả (trong mã) hoặc từ hộp thoại thuộc tính chương trình.

Nếu bạn đang tạo một Expert Advisor hoặc chỉ báo, bạn thường có thể tối ưu hóa giá trị của các biến như vậy bằng trình kiểm tra chiến lược. Tuy nhiên, nếu bạn muốn exclude một số tham số khỏi tối ưu hóa, ở đầu từ input bạn cần thêm chữ s hoặc sửa đổi ‘static’:

input double price =1.0456;               // tối ưu hóa
sinput int points =15;                    // KHÔNG tối ưu hóa
static input int unoptimizedVariable =100; // KHÔNG tối ưu hóa

Ví dụ 23. Sử dụng sửa đổi ‘sinput’ để loại trừ một biến khỏi tối ưu hóa trong trình kiểm tra

Nếu bạn muốn người dùng chọn values from a list trong trường đầu vào, bạn cần thêm một enumeration cho mỗi trường như vậy. Các bình luận nội dòng cho các phần tử liệt kê cũng hoạt động, vì vậy thay vì tên như POINT_PRICE_CLOSE, bạn có thể hiển thị “Point Close Price” bằng bất kỳ ngôn ngữ nào của con người. Thật không may, không có cách dễ dàng để chọn ngôn ngữ văn bản cho tên trường (bình luận). Đối với mỗi ngôn ngữ bạn sử dụng, bạn sẽ phải biên dịch một tệp riêng, đó là lý do tại sao hầu hết các lập trình viên giàu kinh nghiệm thích sử dụng ngôn ngữ phổ quát (tiếng Anh).

Các tham số có thể được nhóm trực quan để dễ sử dụng hơn. Để chỉ định tên nhóm, một mô tả đặc biệt được sử dụng:

input group "Group Name"

Ví dụ 24. Tiêu đề nhóm tham số

Dưới đây là một ví dụ hoàn chỉnh minh họa tất cả các khả năng này:

#property script_show_inputs

// Liệt kê. Cho phép tạo hộp danh sách.
enum ENUM_DIRECTION
  {
   // Tất cả bình luận nội dòng bên cạnh các dòng mô tả tham số, 
   //   sẽ được hiển thị thay vì tên trong cửa sổ tham số
   DIRECTION_UP =  1, // Up
   DIRECTION_DN = -1, // Down
   DIRECTION_FL =  0  // Unknown
  };

input group "Will be optimized"
input  int            onlyExampleName              = 10;
input  ENUM_DIRECTION direction                    = DIRECTION_FL;      // Danh sách các hướng có thể
input group "Will not be optimized"
sinput string         something                    = "Something good";
static input double   doNotOptimizedMagickVariable = 1.618;             // Một số phép thuật
//+------------------------------------------------------------------+
//| Hàm bắt đầu chương trình script                                  |
//+------------------------------------------------------------------+
void OnStart()
  {
   //---
  }
//+------------------------------------------------------------------+

Ví dụ 25. Các tùy chọn khác nhau để mô tả tham số đầu vào

Hộp thoại tham số

Hình 6. Hộp thoại tham số. Các nhóm (input group) được đánh dấu màu. Mũi tên xanh lá chỉ ra các giá trị của bình luận được thay thế thay vì tên biến.

Hình 6 hiển thị hộp thoại tùy chọn được tạo từ mã trong Ví dụ 25. Nó có thể trông hơi khác trên các máy tính khác nhau, nhưng trong bất kỳ trường hợp nào, tiêu đề nhóm sẽ được đánh dấu (tôi đã đánh dấu chúng bằng màu xanh trong hình). Bạn cũng có thể thấy rằng các tham số không có bình luận nội dòng sẽ sử dụng tên biến. Nếu có bình luận, trình biên dịch sử dụng chúng thay vì tên biến, như trong các ô của tôi được chỉ định bởi mũi tên xanh. So sánh mã từ Ví dụ 25 với hình ảnh; tôi hy vọng nó giúp bạn hiểu mọi thứ.

Và một điều nữa. Không phải tất cả người mới bắt đầu đều nhận thấy các biểu tượng ở phía bên trái của kiểu dữ liệu của mỗi tham số. Ví dụ, tham số có tên “Possible directions list” trong hình có kiểu dữ liệu là enumeration, và biểu tượng của nó ( Biểu tượng liệt kê ) gợi ý rằng đó là một danh sách. Dữ liệu cho trường này chỉ có thể được chọn từ một limited enumeration. Các biểu tượng còn lại cũng tự giải thích.

Name của bất kỳ parameter nào không được dài quá 63 ký tự (rất nhiều, vì tên thật thường ngắn hơn nhiều).

Tôi đang nói về tên của một parameter (tức là dòng hiển thị trong hộp thoại), không phải biến. Còn về bình luận, nó có thể rất dài. Nếu kích thước 63 ký tự bị vượt quá, trình biên dịch sẽ chỉ đơn giản cắt bỏ phần thừa.

String parameter length không được vượt quá 254 ký tự. Ngoài ra, the longer the name của tham số, the shorter the content, vì chúng được lưu trong bộ nhớ dưới dạng một dòng liên tục.

Điều quan trọng là phải nhớ giới hạn này, đặc biệt nếu bạn đang chỉ định địa chỉ của một số trang web quan trọng cho chương trình. Đôi khi địa chỉ thực sự dài, và nếu đây là trường hợp của bạn, hãy thử truyền địa chỉ theo cách khác, ví dụ, mã hóa cứng nó như một biến toàn cục, nhưng không phải là tham số. Tất nhiên, có những giải pháp tốt hơn, chẳng hạn như sử dụng tệp hoặc “dán” một địa chỉ từ nhiều đoạn, chỉ cần nhớ giới hạn 254 ký tự cho giá trị values của tham số.

Biến bên ngoài (‘extern’)

Trường hợp đặc biệt thứ hai là biến ‘extern’.

Khi các nhà phát triển viết một chương trình lớn được chia thành nhiều tệp, có những trường hợp một biến toàn cục được described in one tệp, và chương trình cần accessfrom other tệp. Và chúng ta không muốn bao gồm các tệp bằng chỉ thị #include. MetaEditor coi mỗi tệp riêng biệt và, do đó, trong trường hợp này, không thể giúp đỡ.

Thường thì tình huống này phát sinh khi sử dụng các tham số đầu vào (được mô tả trong tiểu mục trước).

Đây là nơi từ khóa ‘extern’ có thể được sử dụng.

extern bool testComplete;

Ví dụ 26. Mô tả biến bên ngoài

Các biến như vậy có thể not được initialized trong tệp này, và khi biên dịch, địa chỉ bộ nhớ của biến này rất có thể sẽ được thay thế bằng một biến toàn cục “thực” cùng tên, nếu trình biên dịch có thể tìm thấy một biến như vậy. Tuy nhiên, các hàm có thể tự do truy cập dữ liệu “hình thức” này, bao gồm cả việc sửa đổi nó, và IDE sẽ không gặp khó khăn với việc tự động thay thế.

Biến toàn cục terminal

Cả biến cục bộ và toàn cục được mô tả trong các phần trước chỉ có thể truy cập trong chương trình hiện tại. Tất cả các chương trình khác không thể sử dụng dữ liệu này. Nhưng có những tình huống khi các chương trình cần trao đổi dữ liệu với nhau, hoặc cần đảm bảo rằng giá trị của các biến được lưu lại ngay cả sau khi terminal bị tắt.

Một ví dụ về trường hợp trao đổi dữ liệu có thể là một indicator rất đơn giản, trong đó bạn cần xuất số tiền bằng đơn vị tiền tệ của khoản tiền gửi cần thiết để mở một vị thế. Có vẻ mọi thứ đều đơn giản. Sau khi tìm kiếm qua mục lục trợ giúp, chúng ta phát hiện ra rằng MQL5 có một hàm đặc biệt OrderCalcMargin, tính toán số tiền cần thiết. Chúng ta cố gắng áp dụng nó, và chúng ta… thất vọng. Điều này là do bạn không thể sử dụng các hàm giao dịch trong các chỉ báo. Điều này bị cấm về mặt vật lý, ở cấp độ trình biên dịch. OrderCalcMargin là một hàm giao dịch.

Do đó, chúng ta sẽ phải tìm cách giải quyết. Một lựa chọn là viết một script hoặc dịch vụ sẽ tính toán các số tiền cần thiết và sau đó ghi các số tiền này vào các biến terminal. Và sau đó chỉ báo của chúng ta sẽ đọc dữ liệu này, không tính toán nó. Thủ thuật này có thể thực hiện được vì, không giống như chỉ báo, script và dịch vụ được phép giao dịch (xem Bảng trong bài viết đầu tiên trong loạt bài).

Hãy xem cách trao đổi dữ liệu như vậy có thể được triển khai. Đầu tiên, hãy tạo một tệp script bằng trình hướng dẫn. Hãy đặt tên tệp này là “CalculateMargin.mq5”.

Có một tập hợp các hàm định sẵn để truy cập các biến terminal, tên của chúng bắt đầu bằng tiền tố GlobalVariable. Sử dụng các hàm này và hàm OrderCalcMargin để làm cho dữ liệu cần thiết có sẵn cho các chỉ báo, chúng ta sẽ tạo một script mới:

//+------------------------------------------------------------------+
//|                                              CalculateMargin.mq5 |
//|                                       Oleg Fedorov (aka certain) |
//|                                   mailto:[email protected] |
//+------------------------------------------------------------------+
#property copyright "Oleg Fedorov (aka certain)"
#property link      "mailto:[email protected]"
#property version   "1.00"

#property script_show_inputs

//--- Tham số đầu vào của script
input double requiredAmount = 1; // Số lượng lot

//+------------------------------------------------------------------+
//| Hàm bắt đầu chương trình script                                  |
//+------------------------------------------------------------------+
void OnStart()
 {
  //--- Mô tả các biến cục bộ
  string symbolName = Symbol();  // Tên của biểu tượng hiện tại
  string terminalVariableName;   // Tên biến terminal

  double marginBuy, marginSell;  // Giá trị margin (mua và bán)
  double currentPrice = iClose(symbolName,PERIOD_CURRENT,0);  // Giá hiện tại để tính margin

  bool okCalcBuy, okCalcSell;    // Chỉ báo thành công khi tính margin lên hoặc xuống

  //--- Các thao tác chính

  // Tính margin mua
  okCalcBuy = OrderCalcMargin(
                ORDER_TYPE_BUY, // Loại lệnh
                symbolName,     // Tên biểu tượng
                requiredAmount, // Khối lượng cần thiết tính bằng lot
                currentPrice,   // Giá mở lệnh
                marginBuy       // Kết quả (theo tham chiếu)
              );
  // Tính margin bán
  okCalcSell = OrderCalcMargin(
                 ORDER_TYPE_SELL, // Đôi khi cần số tiền khác nhau để mở lên và xuống
                 symbolName,
                 requiredAmount,
                 currentPrice,
                 marginSell
               );

  //--- Kết quả hoạt động
  // Tạo tên biến terminal cho chi tiết mua
  terminalVariableName = symbolName + "BuyAmount";

  // Ghi dữ liệu. Nếu biến toàn cục terminal không tồn tại, nó sẽ được tạo.
  GlobalVariableSet
    (
      terminalVariableName, // Nơi ghi
      marginBuy             // Ghi gì
    );

  // Bây giờ chúng ta tạo một tên khác - cho chi tiết bán
  terminalVariableName = symbolName + "SellAmount";

  // Ghi dữ liệu cho bán. Nếu không có biến với tên được lưu trong terminalVariableName, 
  //   tạo một biến
  GlobalVariableSet(terminalVariableName,marginSell);
 }
//+------------------------------------------------------------------+

Ví dụ 31. Script để tính toán số tiền bằng đơn vị tiền tệ của khoản tiền gửi cần thiết để mua hoặc bán 1 lot và lưu dữ liệu này vào biến toàn cục của terminal

Ở đây chúng tôi đã sử dụng hàm tiêu chuẩn GlobalVariableSet để ghi dữ liệu vào biến terminal. Tôi nghĩ trong ví dụ đã cho, việc sử dụng các hàm này là rõ ràng. Ghi chú bổ sung: name length cho biến toàn cục terminal không được vượt quá 63 ký tự.

Nếu bạn chạy script này trên bất kỳ biểu đồ nào, bạn sẽ không thấy kết quả rõ ràng ngay lập tức. Tuy nhiên, bạn có thể thấy điều gì đã xảy ra bằng cách sử dụng phím F3 hoặc bằng cách chọn “Tools -> Global Variables” từ menu terminal.

Menu biến terminal

Hình 7. Menu biến terminal.

Sau khi chọn mục menu này, một cửa sổ sẽ xuất hiện với danh sách tất cả các biến terminal:

Cửa sổ với danh sách biến toàn cục terminal

Hình 8. Cửa sổ với danh sách biến toàn cục terminal

Trong Hình 8, bạn có thể thấy rằng tôi chỉ chạy script trên cặp EURUSD, nên chỉ có hai biến hiển thị: số tiền để mua và bán, trong trường hợp này là như nhau.

Bây giờ hãy tạo một indicator, sẽ use dữ liệu này, và đồng thời chúng ta sẽ thấy cách các hàm tiêu chuẩn sử dụng các nguyên tắc làm việc với biến đã thảo luận ở trên.

Hãy đặt tên tệp này là “GlobalVars.mq5”. Các thao tác chính trong chỉ báo này sẽ được thực hiện bên trong hàm OnInit, được thực thi một lần ngay sau khi chương trình khởi động. Chúng tôi cũng thêm hàm OnDeinit, xóa bình luận khi chúng tôi gỡ chỉ báo khỏi biểu đồ. Hàm OnCalculate, bắt buộc cho mỗi chỉ báo và được thực thi trên mỗi tick, cũng có trong chỉ báo này nhưng không được sử dụng.

//+------------------------------------------------------------------+
//|                                                   GlobalVars.mq5 |
//|                                       Oleg Fedorov (aka certain) |
//|                                   mailto:[email protected] |
//+------------------------------------------------------------------+
#property copyright "Oleg Fedorov (aka certain)"
#property link      "mailto:[email protected]"
#property version   "1.00"
#property indicator_chart_window
//+------------------------------------------------------------------+
//| Hàm khởi tạo chỉ báo tùy chỉnh                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Mô tả các biến cục bộ
   string symbolName = Symbol();            // Tên biểu tượng
   string terminalVariableName;             // Tên của giá trị toàn cục terminal

   double buyMarginValue, sellMarginValue;  // Giá trị mua và bán

   bool okCalcBuy; // Chỉ báo mọi thứ đều ổn khi gọi một trong các biến thể của hàm GlobalVariableGet

   //--- Các thao tác chính
   // Tạo tên biến terminal cho chi tiết mua
   terminalVariableName = symbolName + "BuyAmount";

   // Sử dụng phương pháp đầu tiên để lấy giá trị của biến toàn cục. 
   //   Để lấy kết quả, tham số được truyền theo tham chiếu
   okCalcBuy = GlobalVariableGet(terminalVariableName, buyMarginValue);

   // Thay đổi tên của biến terminal - cho chi tiết bán
   terminalVariableName = symbolName + "SellAmount";

   // Cách thứ hai để lấy kết quả: giá trị trả về
   sellMarginValue = GlobalVariableGet(terminalVariableName);
   //--- Xuất kết quả dưới dạng bình luận trên biểu đồ
   Comment(
      "Buy margin is " + DoubleToString(buyMarginValue)       // Giá trị margin mua, tham số thứ hai 
                                                              //   của hàm DoubleToString bị bỏ qua
      +"\n"                                                   // Ngắt dòng
      +"Sell margin is " + DoubleToString(sellMarginValue,2)  // Giá trị margin bán, chỉ định số 
                                                              //   chữ số thập phân
   );
   //---
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Hàm sẽ được gọi khi chương trình kết thúc.                      |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   // Xóa bình luận
   Comment("");
  }

//+------------------------------------------------------------------+
//| Hàm lặp chỉ báo tùy chỉnh (không được sử dụng ở đây)            |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   //---

   //--- trả về giá trị của prev_calculated cho lần gọi tiếp theo
   return(rates_total);
  }
//+------------------------------------------------------------------+

Ví dụ 32. Sử dụng biến toàn cục terminal trong chỉ báo.

Trong chỉ báo demo này, chúng ta cần đọc các biến terminal một lần và kết thúc hoạt động. Do đó, mã chính được đặt trong hàm OnInit. Ví dụ có thể trông lớn và đáng sợ, nhưng thực tế nó rất dễ đọc, đặc biệt vì phần lớn là bình luận. Hãy để tôi mô tả lại bằng lời những gì xảy ra trong hàm OnInit:

  • Trong khối đầu tiên của hàm này, chúng ta declare all variables, mà chúng ta sẽ cần sau này.
  • Sau đó, chúng ta create a name cho biến toàn cục terminal.
  • Chúng ta read the value của biến toàn cục terminal cần thiết vào biến cục bộ tương ứng.
  • Sau đó, chúng ta generate the name of the second variableread its value trong dòng tiếp theo.
  • Và hành động cuối cùng là output a message cho người dùng dưới dạng bình luận ở góc trên bên trái (xem Hình 9).

Vui lòng lưu ý rằng hàm GlobalVariableGet có hai tùy chọn gọi: sử dụng giá trị trả về hoặc tham số được truyền theo tham chiếu, và hàm DoubleToString có một tham số với giá trị mặc định. Nếu bạn nhập văn bản ví dụ trong trình chỉnh sửa để kiểm tra chức năng mã, thay vì sao chép qua clipboard, MetaEditor sẽ nhắc bạn về những sắc thái này.

Kết quả của chỉ báo

Hình 9. Kết quả hoạt động của chỉ báo

Vì tôi đã sử dụng các cách khác nhau để gọi hàm DoubleToString để tạo đầu ra, các bình luận ở dòng trên và dưới trông hơi khác nhau. Khi định dạng thông điệp for the top line, tôi đã bỏ qua tham số thứ hai của hàm DoubleToString. Tham số này phải chỉ định số ký tự sau dấu chấm thập phân, và theo mặc định nó là 8. Đối với dòng dưới, tôi đã chỉ định giá trị này rõ ràng và hướng dẫn chương trình xuất hai ký tự.

Vui lòng lưu ý rằng chỉ báo phải được khởi chạy trên biểu đồ nơi script đã được áp dụng, và chỉ sau script - để đảm bảo rằng các biến toàn cục của terminal tồn tại khi nó chạy. Nếu không, sẽ xảy ra lỗi khi chỉ báo chạy và các bình luận sẽ không xuất hiện.

Do đó, hàm GlobalVariableSet được sử dụng để ghi biến terminal, và GlobalVariableGet được sử dụng để đọc chúng. Các hàm này được các lập trình viên sử dụng thường xuyên nhất, nhưng các hàm khác cũng hữu ích, vì vậy tôi khuyên bạn nên đọc ít nhất danh sách của chúng trong tài liệu ngôn ngữ (liên kết ở đầu phần).

Kết luận

Hãy xem lại danh sách các chủ đề đã đề cập hôm nay một lần nữa. Nếu bất kỳ điểm nào trong danh sách vẫn chưa rõ với bạn, vui lòng quay lại chỗ tương ứng trong bài viết và đọc lại, vì tài liệu này là nền tảng cho phần còn lại của công việc (có lẽ ngoại trừ biến toàn cục terminal, bạn thường có thể làm mà không cần chúng - nhưng việc hiểu chúng thường không gây khó khăn). Vì vậy, trong bài viết này chúng ta đã nói về:

  1. Mảng:
    • Có thể là tĩnh và động
    • Có thể là một chiều và đa chiều
    • Mảng tĩnh có thể được khởi tạo bằng giá trị chữ (trong dấu ngoặc nhọn)
    • Để làm việc với mảng động, bạn cần sử dụng các hàm tiêu chuẩn để thay đổi kích thước và tìm ra kích thước hiện tại
  2. Các biến liên quan đến các hàm có thể là
    • Địa phương (tồn tại trong thời gian ngắn ngoại trừ tĩnh); bộ điều chỉnh tĩnh có thể được thêm vào chúng
    • Toàn cầu (tồn tại lâu dài); bạn có thể thêm các trình sửa đổi bên ngoài và đầu vào vào chúng
  3. Các tham số hàm có thể được truyền
    • Bằng cách tham khảo
    • Theo giá trị
  4. Có các biến terminal toàn cục. Không giống như các biến chương trình toàn cục, chúng có thể được sử dụng để trao đổi dữ liệu giữa các chương trình khác nhau. Có một tập hợp các hàm đặc biệt để sử dụng chúng.

Nếu bạn nhớ tất cả những điều này và không có mục nào trong danh sách làm bạn bối rối, thì bạn không còn được coi là người mới nữa: bạn đã có một nền tảng tốt để làm việc. Tất cả những gì còn lại là tìm ra cách sử dụng các toán tử cơ bản, cũng như hiểu những tính năng có trong ngôn ngữ MQL5 khi viết các chỉ báo và chuyên gia liên quan đến các ngôn ngữ khác - và bạn có thể bắt đầu viết các chương trình hữu ích. Tuy nhiên, để đạt đến trình độ “chuyên nghiệp”, bạn sẽ cần hiểu thêm hàng chục chủ đề nữa, bao gồm cả lập trình hướng đối tượng, nhưng tất cả các chủ đề này vẫn sẽ, theo cách này hay cách khác, dựa trên nền tảng, vốn đã sẵn sàng một nửa.

Và mong tài liệu ngôn ngữ sẽ luôn bên bạn…