Cách nâng cao để tạo chỉ báo: IndicatorCreate
Việc tạo một chỉ báo bằng hàm iCustom
hoặc một trong những hàm thuộc tập hợp các chỉ báo tích hợp sẵn yêu cầu kiến thức về danh sách các tham số ngay từ giai đoạn lập trình. Tuy nhiên, trong thực tế, thường cần viết các chương trình đủ linh hoạt để thay thế một chỉ báo này bằng một chỉ báo khác.
Ví dụ, khi tối ưu hóa một Expert Advisor trong trình kiểm tra, việc chọn không chỉ chu kỳ của đường trung bình động mà còn cả thuật toán tính toán của nó là điều hợp lý. Tất nhiên, nếu chúng ta xây dựng thuật toán dựa trên một chỉ báo duy nhất như iMA
, bạn có thể cung cấp khả năng chỉ định ENUM_MA_METHOD
trong cài đặt phương pháp của nó. Nhưng một số người có thể muốn mở rộng lựa chọn bằng cách chuyển đổi giữa đường trung bình động hàm mũ kép, hàm mũ ba và đường trung bình động phân dạng. Thoạt nhìn, điều này có thể được thực hiện bằng cách sử dụng switch
với việc gọi DEMA
, iTEMA
, và iFrAMA
tương ứng. Tuy nhiên, làm thế nào để bao gồm các chỉ báo tùy chỉnh vào danh sách này?
Mặc dù tên của chỉ báo có thể dễ dàng được thay thế trong lời gọi iCustom
, nhưng danh sách các tham số có thể khác biệt đáng kể. Trong trường hợp tổng quát, một Expert Advisor có thể cần tạo tín hiệu dựa trên sự kết hợp của bất kỳ chỉ báo nào không được biết trước, không chỉ là các đường trung bình động.
Đối với những trường hợp như vậy, MQL5 cung cấp một phương pháp phổ quát để tạo ra một chỉ báo kỹ thuật bất kỳ bằng cách sử dụng hàm IndicatorCreate
.
int IndicatorCreate(const string symbol, ENUM_TIMEFRAMES timeframe, ENUM_INDICATOR indicator, int count = 0, const MqlParam ¶meters[] = NULL)
Hàm này tạo một phiên bản chỉ báo cho ký hiệu và khung thời gian được chỉ định. Loại chỉ báo được thiết lập bằng tham số indicator
. Loại của nó là kiểu liệt kê ENUM_INDICATOR
(xem thêm bên dưới) chứa các định danh cho tất cả các chỉ báo tích hợp sẵn, cũng như một tùy chọn cho iCustom
. Số lượng tham số của chỉ báo và mô tả của chúng được truyền tương ứng trong đối số count
và trong mảng cấu trúc MqlParam
(xem bên dưới).
Mỗi phần tử của mảng này mô tả tham số đầu vào tương ứng của chỉ báo đang được tạo, vì vậy nội dung và thứ tự của các phần tử phải tương ứng với nguyên mẫu của hàm chỉ báo tích hợp sẵn hoặc, trong trường hợp chỉ báo tùy chỉnh, với mô tả của các biến đầu vào trong mã nguồn của nó.
Việc vi phạm quy tắc này có thể dẫn đến lỗi ở giai đoạn thực thi chương trình (xem ví dụ bên dưới) và không thể tạo được handle. Trong trường hợp xấu nhất, các tham số được truyền sẽ bị diễn giải sai và chỉ báo sẽ không hoạt động như kỳ vọng, nhưng do không có lỗi rõ ràng, điều này không dễ nhận ra. Ngoại lệ là việc truyền một mảng rỗng hoặc không truyền gì cả (vì các đối số count
và parameters
là tùy chọn): trong trường hợp này, chỉ báo sẽ được tạo với cài đặt mặc định. Ngoài ra, đối với các chỉ báo tùy chỉnh, bạn có thể bỏ qua một số lượng tham số tùy ý từ cuối danh sách.
Cấu trúc MqlParam
được thiết kế đặc biệt để truyền các tham số đầu vào khi tạo chỉ báo bằng IndicatorCreate
hoặc để lấy thông tin về các tham số của chỉ báo bên thứ ba (được thực hiện trên biểu đồ) bằng cách sử dụng IndicatorParameters
.
struct MqlParam
{
ENUM_DATATYPE type; // loại tham số đầu vào
long integer_value; // trường để lưu trữ giá trị nguyên
double double_value; // trường để lưu trữ giá trị double hoặc float
string string_value; // trường để lưu trữ giá trị kiểu chuỗi
};
2
3
4
5
6
7
Giá trị thực tế của tham số phải được đặt trong một trong các trường integer_value
, double_value
, string_value
, tùy theo giá trị của trường type
đầu tiên. Đến lượt nó, trường type
được mô tả bằng cách sử dụng kiểu liệt kê ENUM_DATATYPE
chứa các định danh cho tất cả các kiểu tích hợp sẵn của MQL5.
Định danh | Kiểu dữ liệu |
---|---|
TYPE_BOOL | bool |
TYPE_CHAR | char |
TYPE_UCHAR | uchar |
TYPE_SHORT | short |
TYPE_USHORT | ushort |
TYPE_COLOR | color |
TYPE_INT | int |
TYPE_UINT | uint |
TYPE_DATETIME | datetime |
TYPE_LONG | long |
TYPE_ULONG | ulong |
TYPE_FLOAT | float |
TYPE_DOUBLE | double |
TYPE_STRING | string |
Nếu bất kỳ tham số nào của chỉ báo có kiểu liệt kê, bạn nên sử dụng giá trị TYPE_INT
trong trường type
để mô tả nó.
Kiểu liệt kê ENUM_INDICATOR
được sử dụng trong tham số thứ ba của IndicatorCreate
để chỉ định loại chỉ báo chứa các hằng số sau:
Định danh | Chỉ báo |
---|---|
IND_AC | Accelerator Oscillator |
IND_AD | Accumulation/Distribution |
IND_ADX | Average Directional Index |
IND_ADXW | ADX by Welles Wilder |
IND_ALLIGATOR | Alligator |
IND_AMA | Adaptive Moving Average |
IND_AO | Awesome Oscillator |
IND_ATR | Average True Range |
IND_BANDS | Bollinger Bands® |
IND_BEARS | Bears Power |
IND_BULLS | Bulls Power |
IND_BWMFI | Market Facilitation Index |
IND_CCI | Commodity Channel Index |
IND_CHAIKIN | Chaikin Oscillator |
IND_CUSTOM | Custom indicator |
IND_DEMA | Double Exponential Moving Average |
IND_DEMARKER | DeMarker |
IND_ENVELOPES | Envelopes |
IND_FORCE | Force Index |
IND_FRACTALS | Fractals |
IND_FRAMA | Fractal Adaptive Moving Average |
IND_GATOR | Gator Oscillator |
IND_ICHIMOKU | Ichimoku Kinko Hyo |
IND_MA | Moving Average |
IND_MACD | MACD |
IND_MFI | Money Flow Index |
IND_MOMENTUM | Momentum |
IND_OBV | On Balance Volume |
IND_OSMA | OsMA |
IND_RSI | Relative Strength Index |
IND_RVI | Relative Vigor Index |
IND_SAR | Parabolic SAR |
IND_STDDEV | Standard Deviation |
IND_STOCHASTIC | Stochastic Oscillator |
IND_TEMA | Triple Exponential Moving Average |
IND_TRIX | Triple Exponential Moving Averages Oscillator |
IND_VIDYA | Variable Index Dynamic Average |
IND_VOLUMES | Volumes |
IND_WPR | Williams Percent Range |
Điều quan trọng cần lưu ý là nếu giá trị IND_CUSTOM
được truyền làm loại chỉ báo, thì phần tử đầu tiên của mảng tham số phải có trường type
với giá trị TYPE_STRING
, và trường string_value
phải chứa tên (đường dẫn) của chỉ báo tùy chỉnh.
Nếu thành công, hàm IndicatorCreate
trả về một handle của chỉ báo đã tạo, và trong trường hợp thất bại, nó trả về INVALID_HANDLE
. Mã lỗi sẽ được cung cấp trong _LastError
.
Hãy nhớ rằng để kiểm tra các chương trình MQL tạo các chỉ báo tùy chỉnh mà tên của chúng không được biết ở giai đoạn biên dịch (điều này cũng áp dụng khi sử dụng IndicatorCreate
), bạn phải ràng buộc chúng một cách rõ ràng bằng chỉ thị:
#property tester_indicator "indicator_name.ex5"
Điều này cho phép trình kiểm tra gửi các chỉ báo phụ trợ cần thiết đến các tác nhân kiểm tra nhưng giới hạn quy trình chỉ với các chỉ báo đã biết trước.
Hãy xem xét một vài ví dụ. Chúng ta sẽ bắt đầu với một ứng dụng đơn giản của IndicatorCreate
như một thay thế cho các hàm đã biết, sau đó, để chứng minh tính linh hoạt của cách tiếp cận mới, chúng ta sẽ tạo một chỉ báo bao bọc phổ quát để hiển thị các chỉ báo tích hợp sẵn hoặc tùy chỉnh bất kỳ.
Ví dụ đầu tiên của UseEnvelopesParams1.mq5
tạo một bản sao nhúng của chỉ báo Envelopes
. Để làm điều này, chúng ta mô tả hai bộ đệm, hai biểu đồ, mảng cho chúng, và các tham số đầu vào lặp lại các tham số của iEnvelopes
.
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots 2
// cài đặt vẽ
#property indicator_type1 DRAW_LINE
#property indicator_color1 clrBlue
#property indicator_width1 1
#property indicator_label1 "Upper"
#property indicator_style1 STYLE_DOT
#property indicator_type2 DRAW_LINE
#property indicator_color2 clrRed
#property indicator_width2 1
#property indicator_label2 "Lower"
#property indicator_style2 STYLE_DOT
input int WorkPeriod = 14;
input int Shift = 0;
input ENUM_MA_METHOD Method = MODE_EMA;
input ENUM_APPLIED_PRICE Price = PRICE_TYPICAL;
input double Deviation = 0.1; // Độ lệch, %
double UpBuffer[];
double DownBuffer[];
int Handle; // handle của chỉ báo phụ
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Trình xử lý OnInit
có thể trông như thế này nếu bạn sử dụng hàm iEnvelopes
.
int OnInit()
{
SetIndexBuffer(0, UpBuffer);
SetIndexBuffer(1, DownBuffer);
Handle = iEnvelopes(WorkPeriod, Shift, Method, Price, Deviation);
return Handle == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED;
}
2
3
4
5
6
7
8
Việc ràng buộc bộ đệm vẫn giữ nguyên, nhưng để tạo handle, chúng ta sẽ đi theo cách khác. Hãy mô tả mảng MqlParam
, điền vào nó và gọi hàm IndicatorCreate
.
int OnInit()
{
...
MqlParam params[5] = {};
params[0].type = TYPE_INT;
params[0].integer_value = WorkPeriod;
params[1].type = TYPE_INT;
params[1].integer_value = Shift;
params[2].type = TYPE_INT;
params[2].integer_value = Method;
params[3].type = TYPE_INT;
params[3].integer_value = Price;
params[4].type = TYPE_DOUBLE;
params[4].double_value = Deviation;
Handle = IndicatorCreate(_Symbol, _Period, IND_ENVELOPES,
ArraySize(params), params);
return Handle == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Sau khi nhận được handle, chúng ta sử dụng nó trong OnCalculate
để điền vào hai bộ đệm của nó.
int OnCalculate(const int rates_total,
const int prev_calculated,
const int begin,
const double &data[])
{
if(BarsCalculated(Handle) != rates_total)
{
return prev_calculated;
}
const int n = CopyBuffer(Handle, 0, 0, rates_total - prev_calculated + 1, UpBuffer);
const int m = CopyBuffer(Handle, 1, 0, rates_total - prev_calculated + 1, DownBuffer);
return n > -1 && m > -1 ? rates_total : 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Hãy kiểm tra xem chỉ báo đã tạo UseEnvelopesParams1
trông như thế nào trên biểu đồ.
Chỉ báo UseEnvelopesParams1
Phía trên là một cách tiêu chuẩn nhưng không quá thanh lịch để điền các thuộc tính. Vì lời gọi IndicatorCreate
có thể cần thiết trong nhiều dự án, việc đơn giản hóa quy trình cho mã gọi là hợp lý. Để đạt được mục đích này, chúng ta sẽ phát triển một lớp có tên MqlParamBuilder
(xem tệp MqlParamBuilder.mqh
). Nhiệm vụ của nó sẽ là chấp nhận các giá trị tham số bằng một số phương thức, xác định loại của chúng, và thêm các phần tử phù hợp (các cấu trúc được điền chính xác) vào mảng.
MQL5 không hỗ trợ đầy đủ khái niệm Thông tin Kiểu Thời gian Chạy (RTTI). Với RTTI, các chương trình có thể yêu cầu môi trường thời gian chạy cung cấp siêu dữ liệu mô tả về các thành phần của chúng, bao gồm biến, cấu trúc, lớp, hàm, v.v. Một vài tính năng tích hợp sẵn của MQL5 có thể được phân loại là RTTI là các toán tử typename
và offsetof
. Vì typename
trả về tên của kiểu dưới dạng chuỗi, hãy xây dựng bộ phát hiện kiểu tự động của chúng ta dựa trên chuỗi (xem tệp RTTI.mqh
).
template<typename T>
ENUM_DATATYPE rtti(T v = (T)NULL)
{
static string types[] =
{
"null", // (0)
"bool", // 0 TYPE_BOOL=1 (1)
"char", // 1 TYPE_CHAR=2 (2)
"uchar", // 2 TYPE_UCHAR=3 (3)
"short", // 3 TYPE_SHORT=4 (4)
"ushort", // 4 TYPE_USHORT=5 (5)
"color", // 5 TYPE_COLOR=6 (6)
"int", // 6 TYPE_INT=7 (7)
"uint", // 7 TYPE_UINT=8 (8)
"datetime", // 8 TYPE_DATETIME=9 (9)
"long", // 9 TYPE_LONG=10 (A)
"ulong", // 10 TYPE_ULONG=11 (B)
"float", // 11 TYPE_FLOAT=12 (C)
"double", // 12 TYPE_DOUBLE=13 (D)
"string", // 13 TYPE_STRING=14 (E)
};
const string t = typename(T);
for(int i = 0; i < ArraySize(types); ++i)
{
if(types[i] == t)
{
return (ENUM_DATATYPE)i;
}
}
return (ENUM_DATATYPE)0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Hàm mẫu rtti
sử dụng typename
để nhận một chuỗi với tên của tham số kiểu mẫu và so sánh nó với các phần tử của một mảng chứa tất cả các kiểu tích hợp sẵn từ kiểu liệt kê ENUM_DATATYPE
. Thứ tự liệt kê tên trong mảng tương ứng với giá trị của phần tử liệt kê, vì vậy khi tìm thấy một chuỗi phù hợp, chỉ cần ép kiểu chỉ số sang kiểu (ENUM_DATATYPE)
và trả nó về mã gọi. Ví dụ, lời gọi rtti(1.0)
hoặc rtti<double>()
sẽ cho giá trị TYPE_DOUBLE
.
Với công cụ này, chúng ta có thể quay lại làm việc với MqlParamBuilder
. Trong lớp, chúng ta mô tả mảng cấu trúc MqlParam
và biến n
sẽ chứa chỉ số của phần tử cuối cùng được điền.
class MqlParamBuilder
{
protected:
MqlParam array[];
int n;
...
2
3
4
5
6
Chúng ta sẽ tạo phương thức công khai để thêm giá trị tiếp theo vào danh sách tham số dưới dạng một phương thức mẫu. Hơn nữa, chúng ta triển khai nó dưới dạng quá tải của toán tử <<
, trả về một con trỏ đến chính đối tượng "builder". Điều này sẽ cho phép ghi nhiều giá trị vào mảng trong một dòng, ví dụ như: builder << WorkPeriod << PriceType << SmoothingMode
.
Chính trong phương thức này, chúng ta tăng kích thước của mảng, lấy chỉ số làm việc n
để điền, và ngay lập tức đặt lại cấu trúc thứ n
này.
...
public:
template<typename T>
MqlParamBuilder *operator<<(T v)
{
// mở rộng mảng
n = ArraySize(array);
ArrayResize(array, n + 1);
ZeroMemory(array[n]);
...
return &this;
}
2
3
4
5
6
7
8
9
10
11
12
Nơi có dấu chấm lửng, phần làm việc chính sẽ tiếp theo, tức là điền vào các trường của cấu trúc. Có thể giả định rằng chúng ta sẽ trực tiếp xác định loại của tham số bằng rtti
tự tạo. Nhưng cần chú ý đến một sắc thái. Nếu chúng ta viết lệnh array[n].type = rtti(v)
, nó sẽ không hoạt động chính xác đối với các kiểu liệt kê. Mỗi kiểu liệt kê là một loại độc lập với tên riêng của nó, mặc dù nó được lưu trữ giống như các số nguyên. Đối với các kiểu liệt kê, hàm rtti
sẽ trả về 0, và do đó, cần thay thế nó một cách rõ ràng bằng TYPE_INT
.
...
// xác định loại giá trị
array[n].type = rtti(v);
if(array[n].type == 0) array[n].type = TYPE_INT; // ngụ ý enum
...
2
3
4
5
Bây giờ chúng ta chỉ cần đặt giá trị v
vào một trong ba trường của cấu trúc: integer_value
kiểu long
(lưu ý, long
là số nguyên dài, do đó tên của trường), double_value
kiểu double
hoặc string_value
kiểu string
. Trong khi đó, số lượng kiểu tích hợp sẵn lớn hơn nhiều, vì vậy假定 rằng tất cả các kiểu nguyên (bao gồm int
, short
, char
, color
, datetime
, và các kiểu liệt kê) phải rơi vào trường integer_value
, các giá trị float
phải rơi vào trường double_value
, và chỉ trường string_value
có cách diễn giải rõ ràng: nó luôn là string
.
Để hoàn thành nhiệm vụ này, chúng ta triển khai một số phương thức assign
quá tải: ba phương thức với các kiểu cụ thể là float
, double
, và string
, và một phương thức mẫu cho mọi thứ khác.
class MqlParamBuilder
{
protected:
...
void assign(const float v)
{
array[n].double_value = v;
}
void assign(const double v)
{
array[n].double_value = v;
}
void assign(const string v)
{
array[n].string_value = v;
}
// ở đây chúng ta xử lý int, enum, color, datetime, v.v. tương thích với long
template<typename T>
void assign(const T v)
{
array[n].integer_value = v;
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Điều này hoàn tất quá trình điền cấu trúc, và câu hỏi còn lại là truyền mảng đã tạo cho mã gọi. Hành động này được giao cho một phương thức công khai với quá tải của toán tử >>
, có một đối số duy nhất: tham chiếu đến mảng nhận MqlParam
.
// xuất mảng bên trong ra ngoài
void operator>>(MqlParam ¶ms[])
{
ArraySwap(array, params);
}
2
3
4
5
Bây giờ mọi thứ đã sẵn sàng, chúng ta có thể làm việc với mã nguồn của chỉ báo đã sửa đổi UseEnvelopesParams2.mq5
. Các thay đổi so với phiên bản đầu tiên chỉ liên quan đến việc điền mảng MqlParam
trong trình xử lý OnInit
. Trong đó, chúng ta mô tả đối tượng "builder", gửi tất cả các tham số đến nó qua <<
và trả về mảng đã hoàn thành qua >>
. Tất cả được thực hiện trong một dòng.
int OnInit()
{
...
MqlParam params[];
MqlParamBuilder builder;
builder << WorkPeriod << Shift << Method << Price << Deviation >> params;
ArrayPrint(params);
/*
[type] [integer_value] [double_value] [string_value]
[0] 7 14 0.00000 null <- "INT" period
[1] 7 0 0.00000 null <- "INT" shift
[2] 7 1 0.00000 null <- "INT" EMA
[3] 7 6 0.00000 null <- "INT" TYPICAL
[4] 13 0 0.10000 null <- "DOUBLE" deviation
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Để kiểm soát, chúng ta xuất mảng ra nhật ký (kết quả cho các giá trị mặc định được hiển thị ở trên).
Nếu mảng không được điền đầy đủ, lời gọi IndicatorCreate
sẽ kết thúc bằng lỗi. Ví dụ, nếu bạn chỉ truyền 3 tham số trong số 5 tham số cần thiết cho Envelopes
, bạn sẽ nhận được lỗi 4002 và một handle không hợp lệ.
Handle = PRTF(IndicatorCreate(_Symbol, _Period, IND_ENVELOPES, 3, params));
// Ví dụ lỗi:
// indicator Envelopes cannot load [4002]
// IndicatorCreate(_Symbol,_Period,IND_ENVELOPES,3,params)=
-1 / WRONG_INTERNAL_PARAMETER(4002)
2
3
4
5
Tuy nhiên, một mảng dài hơn so với thông số kỹ thuật của chỉ báo không được coi là lỗi: các giá trị thừa chỉ đơn giản là không được tính đến.
Lưu ý rằng khi các loại giá trị khác với các loại tham số mong đợi, hệ thống thực hiện ép kiểu ngầm, và điều này không gây ra lỗi rõ ràng, mặc dù chỉ báo được tạo có thể không hoạt động như kỳ vọng. Ví dụ, nếu thay vì Deviation
, chúng ta gửi một chuỗi đến chỉ báo, nó sẽ được diễn giải là số 0, dẫn đến "bao bì" sẽ sụp đổ: cả hai đường sẽ thẳng hàng trên đường giữa, mà khoảng cách được tạo ra bởi kích thước của Deviation
(tính bằng phần trăm). Tương tự, việc truyền một số thực có phần thập phân trong một tham số mà mong đợi một số nguyên sẽ khiến nó bị làm tròn.
Nhưng tất nhiên, chúng ta giữ lại phiên bản đúng của lời gọi IndicatorCreate
và nhận được một chỉ báo hoạt động, giống như trong phiên bản đầu tiên.
...
Handle = PRTF(IndicatorCreate(_Symbol, _Period, IND_ENVELOPES,
ArraySize(params), params));
// thành công:
// IndicatorCreate(_Symbol,_Period,IND_ENVELOPES,ArraySize(params),params)=10 / ok
return Handle == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED;
}
2
3
4
5
6
7
Về mặt hình thức, chỉ báo mới không khác gì so với phiên bản trước.