Tạo chỉ báo linh hoạt với IndicatorCreate
Sau khi làm quen với cách tạo chỉ báo mới, chúng ta hãy chuyển sang một nhiệm vụ thực tế hơn. IndicatorCreate
thường được sử dụng trong các trường hợp mà chỉ báo được gọi không được biết trước. Nhu cầu như vậy, chẳng hạn, phát sinh khi viết các Expert Advisor phổ quát có khả năng giao dịch dựa trên các tín hiệu tùy ý do người dùng cấu hình. Thậm chí tên của các chỉ báo cũng có thể được người dùng đặt.
Chúng ta chưa sẵn sàng để phát triển Expert Advisor, do đó chúng ta sẽ nghiên cứu công nghệ này thông qua ví dụ về một chỉ báo bao bọc UseDemoAll.mq5
, có khả năng hiển thị dữ liệu của bất kỳ chỉ báo nào khác.
Quy trình sẽ diễn ra như sau. Khi chúng ta chạy UseDemoAll
trên biểu đồ, một danh sách sẽ xuất hiện trong hộp thoại thuộc tính, nơi chúng ta nên chọn một trong những chỉ báo tích hợp sẵn hoặc một chỉ báo tùy chỉnh, và trong trường hợp sau, chúng ta sẽ cần thêm tên của nó vào trường nhập liệu. Trong một tham số chuỗi khác, chúng ta có thể nhập danh sách các tham số phân tách bằng dấu phẩy. Kiểu của các tham số sẽ được xác định tự động dựa trên cách viết của chúng. Ví dụ, một số có dấu chấm thập phân (10.0) sẽ được coi là double
, một số không có dấu chấm (15) là integer
, và một thứ gì đó được bao trong dấu nháy kép ("text") là string
.
Đây chỉ là các cài đặt cơ bản của UseDemoAll
, nhưng không phải tất cả các cài đặt có thể. Chúng ta sẽ xem xét các cài đặt khác sau.
Hãy lấy kiểu liệt kê ENUM_INDICATOR
làm cơ sở cho giải pháp: nó đã có các phần tử cho tất cả các loại chỉ báo, bao gồm cả chỉ báo tùy chỉnh (IND_CUSTOM
). Thành thật mà nói, ở dạng nguyên bản, nó không phù hợp vì một số lý do. Thứ nhất, không thể lấy siêu dữ liệu về một chỉ báo cụ thể từ nó, chẳng hạn như số lượng và kiểu của các đối số, số lượng bộ đệm, và chỉ báo được hiển thị ở cửa sổ nào (cửa sổ chính hay cửa sổ phụ). Thông tin này rất quan trọng để tạo và hiển thị chỉ báo một cách chính xác. Thứ hai, nếu chúng ta định nghĩa một biến đầu vào kiểu ENUM_INDICATOR
để người dùng có thể chọn chỉ báo mong muốn, trong hộp thoại thuộc tính, điều này sẽ được biểu diễn bằng một danh sách thả xuống, nơi các tùy chọn chỉ chứa tên của phần tử. Thực tế, sẽ rất hữu ích nếu cung cấp gợi ý cho người dùng trong danh sách này (ít nhất là về các tham số). Do đó, chúng ta sẽ mô tả kiểu liệt kê riêng của mình là IndicatorType
. Hãy nhớ rằng MQL5 cho phép chỉ định một chú thích bên phải cho mỗi phần tử, và chú thích này được hiển thị trong giao diện.
Trong mỗi phần tử của kiểu liệt kê IndicatorType
, chúng ta sẽ mã hóa không chỉ định danh tương ứng (ID) từ ENUM_INDICATOR
, mà còn cả số lượng tham số (P), số lượng bộ đệm (B) và số của cửa sổ làm việc (W). Các macro sau đã được phát triển cho mục đích này.
#define MAKE_IND(P,B,W,ID) (int)((W << 24) | ((B & 0xFF) << 16) | ((P & 0xFF) << 8) | (ID & 0xFF))
#define IND_PARAMS(X) ((X >> 8) & 0xFF)
#define IND_BUFFERS(X) ((X >> 16) & 0xFF)
#define IND_WINDOW(X) ((uchar)(X >> 24))
#define IND_ID(X) ((ENUM_INDICATOR)(X & 0xFF))
2
3
4
5
Macro MAKE_IND
nhận tất cả các đặc tính trên làm tham số và đóng gói chúng vào các byte khác nhau của một số nguyên 4 byte duy nhất, từ đó tạo thành một mã duy nhất cho phần tử của kiểu liệt kê mới. 4 macro còn lại cho phép thực hiện thao tác ngược lại, tức là tính toán tất cả các đặc tính của chỉ báo dựa trên mã.
Chúng ta sẽ không cung cấp toàn bộ kiểu liệt kê IndicatorType
tại đây, mà chỉ một phần của nó. Mã nguồn đầy đủ có thể được tìm thấy trong tệp AutoIndicator.mqh
.
enum IndicatorType
{
iCustom_ = MAKE_IND(0, 0, 0, IND_CUSTOM), // {iCustom}(...)[?]
iAC_ = MAKE_IND(0, 1, 1, IND_AC), // iAC( )[1]*
iAD_volume = MAKE_IND(1, 1, 1, IND_AD), // iAD(volume)[1]*
iADX_period = MAKE_IND(1, 3, 1, IND_ADX), // iADX(period)[3]*
iADXWilder_period = MAKE_IND(1, 3, 1, IND_ADXW), // iADXWilder(period)[3]*
...
iMomentum_period_price = MAKE_IND(2, 1, 1, IND_MOMENTUM), // iMomentum(period,price)[1]*
iMFI_period_volume = MAKE_IND(2, 1, 1, IND_MFI), // iMFI(period,volume)[1]*
iMA_period_shift_method_price = MAKE_IND(4, 1, 0, IND_MA), // iMA(period,shift,method,price)[1]
iMACD_fast_slow_signal_price = MAKE_IND(4, 2, 1, IND_MACD), // iMACD(fast,slow,signal,price)[2]*
...
iTEMA_period_shift_price = MAKE_IND(3, 1, 0, IND_TEMA), // iTEMA(period,shift,price)[1]
iVolumes_volume = MAKE_IND(1, 1, 1, IND_VOLUMES), // iVolumes(volume)[1]*
iWPR_period = MAKE_IND(1, 1, 1, IND_WPR) // iWPR(period)[1]*
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Các chú thích, sẽ trở thành các phần tử của danh sách thả xuống hiển thị cho người dùng, chỉ ra nguyên mẫu với các tham số được đặt tên, số lượng bộ đệm trong dấu ngoặc vuông, và dấu sao đánh dấu các chỉ báo được hiển thị trong cửa sổ riêng của chúng. Bản thân các định danh cũng được đặt một cách rõ ràng, vì chúng là những thứ được chuyển đổi thành văn bản bởi hàm EnumToString
được sử dụng để xuất thông báo ra nhật ký.
Danh sách tham số đặc biệt quan trọng, vì người dùng sẽ cần nhập các giá trị phù hợp được phân tách bằng dấu phẩy vào biến đầu vào được dành sẵn cho mục đích này. Chúng ta cũng có thể hiển thị kiểu của các tham số, nhưng để đơn giản, đã quyết định chỉ để lại các tên có ý nghĩa, từ đó cũng có thể suy ra kiểu. Ví dụ, period
, fast
, slow
là các số nguyên với chu kỳ (số lượng thanh), method
là phương pháp trung bình ENUM_MA_METHOD
, price
là loại giá ENUM_APPLIED_PRICE
, volume
là loại khối lượng ENUM_APPLIED_VOLUME
.
Để thuận tiện cho người dùng (để không phải nhớ các giá trị của các phần tử liệt kê), chương trình sẽ hỗ trợ tên của tất cả các kiểu liệt kê. Cụ thể, định danh sma
biểu thị MODE_SMA
, ema
biểu thị MODE_EMA
, v.v. Giá close
sẽ biến thành PRICE_CLOSE
, open
sẽ biến thành PRICE_OPEN
, và các loại giá khác sẽ hoạt động tương tự, theo từ cuối cùng (sau dấu gạch dưới) trong định danh phần tử liệt kê. Ví dụ, đối với danh sách tham số của chỉ báo iMA (iMA_period_shift_method_price
), bạn có thể viết dòng sau: 11,0,sma,close
. Các định danh không cần được đặt trong dấu nháy. Tuy nhiên, nếu cần, bạn có thể truyền một chuỗi với cùng văn bản, ví dụ, danh sách 1.5,"close"
chứa số thực 1.5 và chuỗi "close".
Kiểu chỉ báo, cũng như các chuỗi với danh sách tham số và, tùy chọn, một tên (nếu chỉ báo là tùy chỉnh) là dữ liệu chính cho hàm tạo của lớp AutoIndicator
.
class AutoIndicator
{
protected:
IndicatorType type; // kiểu chỉ báo được chọn
string symbols; // ký hiệu làm việc (tùy chọn)
ENUM_TIMEFRAMES tf; // khung thời gian làm việc (tùy chọn)
MqlParamBuilder builder; // "builder" của mảng tham số
int handle; // handle của chỉ báo
string name; // tên chỉ báo tùy chỉnh
...
public:
AutoIndicator(const IndicatorType t, const string custom, const string parameters,
const string s = NULL, const ENUM_TIMEFRAMES p = 0):
type(t), name(custom), symbol(s), tf(p), handle(INVALID_HANDLE)
{
PrintFormat("Initializing %s(%s) %s, %s",
(type == iCustom_ ? name : EnumToString(type)), parameters,
(symbol == NULL ? _Symbol : symbol), EnumToString(tf == 0 ? _Period : tf));
// phân tách chuỗi thành mảng các tham số (được hình thành bên trong builder)
parseParameters(parameters);
// tạo và lưu trữ handle
handle = create();
}
int getHandle() const
{
return handle;
}
};
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
Tại đây và bên dưới, một số đoạn liên quan đến việc kiểm tra dữ liệu đầu vào cho tính chính xác bị lược bỏ. Mã nguồn đầy đủ được bao gồm cùng với sách.
Quá trình phân tích chuỗi với các tham số được giao cho phương thức parseParameters
. Nó thực hiện sơ đồ được mô tả ở trên với việc nhận diện kiểu giá trị và truyền chúng đến đối tượng MqlParamBuilder
, mà chúng ta đã gặp trong ví dụ trước.
int parseParameters(const string &list)
{
string sparams[];
const int n = StringSplit(list, ',', sparams);
for(int i = 0; i < n; i++)
{
// chuẩn hóa chuỗi (loại bỏ khoảng trắng, chuyển thành chữ thường)
StringTrimLeft(sparams[i]);
StringTrimRight(sparams[i]);
StringToLower(sparams[i]);
if(StringGetCharacter(sparams[i], 0) == '"'
&& StringGetCharacter(sparams[i], StringLen(sparams[i]) - 1) == '"')
{
// mọi thứ trong dấu nháy được coi là chuỗi
builder << StringSubstr(sparams[i], ...(bị cắt bớt 29683 ký tự)...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Phương thức trả về handle nhận được.
Điều đặc biệt đáng chú ý là cách một tham số chuỗi bổ sung với tên của chỉ báo tùy chỉnh được chèn vào đầu mảng. Đầu tiên, mảng được gán thứ tự lập chỉ mục "như trong chuỗi thời gian" (xem ArraySetAsSeries
), kết quả là chỉ số của phần tử cuối cùng (về mặt vật lý, theo vị trí trong bộ nhớ) trở thành 0, và các phần tử được đếm từ phải sang trái. Sau đó, mảng được tăng kích thước và tên chỉ báo được ghi vào phần tử được thêm vào. Do lập chỉ mục ngược, việc bổ sung này không xảy ra bên phải các phần tử hiện có, mà ở bên trái. Cuối cùng, chúng ta đưa mảng trở lại thứ tự lập chỉ mục thông thường, và tại chỉ số 0 là phần tử mới với chuỗi vừa là phần tử cuối cùng.
Tùy chọn, lớp AutoIndicator
có thể tạo một tên viết tắt của chỉ báo tích hợp sẵn từ tên của một phần tử liệt kê.
...
string getName() const
{
if(type != iCustom_)
{
const string s = EnumToString(type);
const int p = StringFind(s, "_");
if(p > 0) return StringSubstr(s, 0, p);
return s;
}
return name;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
Bây giờ mọi thứ đã sẵn sàng để chuyển trực tiếp đến mã nguồn UseDemoAll.mq5
. Nhưng hãy bắt đầu với một phiên bản đơn giản hóa một chút UseDemoAllSimple.mq5
.
Trước hết, hãy xác định số lượng bộ đệm của chỉ báo. Vì số lượng bộ đệm tối đa trong số các chỉ báo tích hợp sẵn là năm (cho Ichimoku
), chúng ta lấy nó làm giới hạn. Chúng ta sẽ giao việc đăng ký số lượng mảng này làm bộ đệm cho lớp đã quen thuộc với chúng ta, BufferArray
(xem phần Chỉ báo đa tiền tệ và đa khung thời gian, ví dụ IndUnityPercent
).
#define BUF_NUM 5
#property indicator_chart_window
#property indicator_buffers BUF_NUM
#property indicator_plots BUF_NUM
#include <MQL5Book/IndBufArray.mqh>
BufferArray buffers(5);
2
3
4
5
6
7
8
9
Điều quan trọng cần nhớ là một chỉ báo có thể được thiết kế để hiển thị trong cửa sổ chính hoặc trong một cửa sổ riêng. MQL5 không cho phép kết hợp hai chế độ. Tuy nhiên, chúng ta không biết trước người dùng sẽ chọn chỉ báo nào, và do đó chúng ta cần nghĩ ra một số cách "lách luật". Hiện tại, hãy đặt chỉ báo của chúng ta trong cửa sổ chính, và chúng ta sẽ giải quyết vấn đề về cửa sổ riêng sau.
Về mặt kỹ thuật thuần túy, không có trở ngại nào để sao chép dữ liệu từ các bộ đệm chỉ báo với thuộc tính indicator_separate_window
vào các bộ đệm của chúng được hiển thị trong cửa sổ chính. Tuy nhiên, cần lưu ý rằng phạm vi giá trị của các chỉ báo như vậy thường không trùng với thang giá, và do đó rất khó có thể nhìn thấy chúng trên biểu đồ (các đường sẽ nằm đâu đó xa ngoài vùng hiển thị, ở trên cùng hoặc dưới cùng), mặc dù các giá trị vẫn sẽ được xuất ra cửa sổ Data window
.
Với sự trợ giúp của các biến đầu vào, chúng ta sẽ chọn kiểu chỉ báo, tên của chỉ báo tùy chỉnh, và danh sách các tham số. Chúng ta cũng sẽ thêm các biến cho kiểu hiển thị và độ rộng đường kẻ. Vì các bộ đệm sẽ được kết nối để hoạt động động, tùy thuộc vào số lượng bộ đệm của chỉ báo nguồn, chúng ta không mô tả kiểu bộ đệm một cách tĩnh bằng các chỉ thị và sẽ thực hiện điều này trong OnInit
thông qua các lời gọi của các hàm Plot
tích hợp sẵn.
input IndicatorType IndicatorSelector = iMA_period_shift_method_price; // Bộ chọn chỉ báo tích hợp sẵn
input string IndicatorCustom = ""; // Tên chỉ báo tùy chỉnh
input string IndicatorParameters = "11,0,sma,close"; // Tham số chỉ báo (danh sách phân tách bằng dấu phẩy)
input ENUM_DRAW_TYPE DrawType = DRAW_LINE; // Kiểu vẽ
input int DrawLineWidth = 1; // Độ rộng đường vẽ
2
3
4
5
Hãy định nghĩa một biến toàn cục để lưu trữ mô tả chỉ báo.
int Handle;
Trong trình xử lý OnInit
, chúng ta sử dụng lớp AutoIndicator
đã trình bày trước đó, để phân tích dữ liệu đầu vào, chuẩn bị mảng MqlParam
và lấy handle dựa trên nó.
#include <MQL5Book/AutoIndicator.mqh>
int OnInit()
{
AutoIndicator indicator(IndicatorSelector, IndicatorCustom, IndicatorParameters);
Handle = indicator.getHandle();
if(Handle == INVALID_HANDLE)
{
Alert(StringFormat("Không thể tạo chỉ báo: %s",
_LastError ? E2S(_LastError) : "Tên hoặc số lượng tham số không đúng"));
return INIT_FAILED;
}
...
2
3
4
5
6
7
8
9
10
11
12
13
Để tùy chỉnh các biểu đồ, chúng ta mô tả một tập hợp màu sắc và lấy tên ngắn của chỉ báo từ đối tượng AutoIndicator
. Chúng ta cũng tính toán số lượng bộ đệm n
được sử dụng của chỉ báo tích hợp sẵn bằng macro IND_BUFFERS
, và đối với bất kỳ chỉ báo tùy chỉnh nào (không được biết trước), vì không có giải pháp tốt hơn, chúng ta sẽ bao gồm tất cả các bộ đệm. Tiếp theo, trong quá trình sao chép dữ liệu, các lời gọi CopyBuffer
không cần thiết sẽ chỉ đơn giản trả về lỗi, và các mảng như vậy có thể được điền bằng các giá trị trống.
...
static color defColors[BUF_NUM] = {clrBlue, clrGreen, clrRed, clrCyan, clrMagenta};
const string s = indicator.getName();
const int n = (IndicatorSelector != iCustom_) ? IND_BUFFERS(IndicatorSelector) : BUF_NUM;
...
2
3
4
5
Trong vòng lặp, chúng ta sẽ thiết lập các thuộc tính của các biểu đồ, tính đến giới hạn n
: các bộ đệm vượt quá giới hạn này sẽ bị ẩn.
for(int i = 0; i < BUF_NUM; ++i)
{
PlotIndexSetString(i, PLOT_LABEL, s + "[" + (string)i + "]");
PlotIndexSetInteger(i, PLOT_DRAW_TYPE, i < n ? DrawType : DRAW_NONE);
PlotIndexSetInteger(i, PLOT_LINE_WIDTH, DrawLineWidth);
PlotIndexSetInteger(i, PLOT_LINE_COLOR, defColors[i]);
PlotIndexSetInteger(i, PLOT_SHOW_DATA, i < n);
}
Comment("DemoAll: ", (IndicatorSelector == iCustom_ ? IndicatorCustom : s),
"(", IndicatorParameters, ")");
return INIT_SUCCEEDED;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Ở góc trên bên trái của biểu đồ, chú thích sẽ hiển thị tên của chỉ báo cùng với các tham số.
Trong trình xử lý OnCalculate
, khi dữ liệu handle đã sẵn sàng, chúng ta đọc chúng vào các mảng của mình.
int OnCalculate(ON_CALCULATE_STD_SHORT_PARAM_LIST)
{
if(BarsCalculated(Handle) != rates_total)
{
return prev_calculated;
}
const int m = (IndicatorSelector != iCustom_) ? IND_BUFFERS(IndicatorSelector) : BUF_NUM;
for(int k = 0; k < m; ++k)
{
// điền các bộ đệm của chúng ta bằng dữ liệu từ chỉ báo với handle 'Handle'
const int n = buffers[k].copy(Handle,
k, 0, rates_total - prev_calculated + 1);
// trong trường hợp lỗi, làm sạch bộ đệm
if(n < 0)
{
buffers[k].empty(EMPTY_VALUE, prev_calculated, rates_total - prev_calculated);
}
}
return rates_total;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Việc triển khai trên là đơn giản hóa và khớp với tệp gốc UseDemoAllSimple.mq5
. Chúng ta sẽ xử lý phần mở rộng của nó sau, nhưng hiện tại chúng ta sẽ kiểm tra hành vi của phiên bản hiện tại. Hình ảnh sau cho thấy 2 phiên bản của chỉ báo: đường màu xanh với cài đặt mặc định (iMA_period_shift_method_price
, tùy chọn 11,0,sma,close
), và đường đỏ iRSI_period_price
với tham số 11 close
.
Hai phiên bản của chỉ báo UseDemoAllSimple với giá trị iMA và iRSI
Biểu đồ USDRUB được chọn có chủ ý để trình diễn, vì giá trị của các báo giá ở đây ít nhiều trùng với phạm vi của chỉ báo RSI (đáng lẽ nên được hiển thị trong một cửa sổ riêng). Trên hầu hết các biểu đồ của các ký hiệu khác, chúng ta sẽ không nhận thấy RSI. Nếu bạn chỉ quan tâm đến việc truy cập chương trình vào các giá trị, thì điều này không phải là vấn đề lớn, nhưng nếu bạn có yêu cầu về hình ảnh hóa, đây là một vấn đề cần được giải quyết.
Vì vậy, bạn nên cung cấp một cách nào đó để hiển thị riêng các chỉ báo dành cho cửa sổ phụ. Về cơ bản, có một yêu cầu phổ biến từ cộng đồng nhà phát triển MQL để kích hoạt hiển thị đồ họa cả trong cửa sổ chính và trong cửa sổ phụ cùng một lúc. Chúng ta sẽ trình bày một trong những giải pháp, nhưng để làm điều này, bạn cần làm quen với một số tính năng mới trước.