Làm việc với mẫu biểu đồ tpl
MQL5 API cung cấp hai hàm để làm việc với các mẫu. Các mẫu là các tệp có phần mở rộng .tpl
, lưu trữ nội dung của các biểu đồ, tức là tất cả các cài đặt của chúng, cùng với các đối tượng được vẽ, chỉ báo và EA (nếu có).
bool ChartSaveTemplate(long chartId, const string filename)
Hàm lưu cài đặt biểu đồ hiện tại vào một mẫu .tpl
với tên được chỉ định.
Biểu đồ được đặt bởi chartId
, 0 có nghĩa là biểu đồ hiện tại.
Tên tệp để lưu mẫu (filename
) có thể được chỉ định mà không cần phần mở rộng .tpl
: nó sẽ được thêm tự động. Mẫu mặc định được lưu vào thư mục terminal_dir/Profiles/Templates/
và sau đó có thể được sử dụng để áp dụng thủ công trong terminal. Tuy nhiên, có thể chỉ định không chỉ tên mà còn đường dẫn tương đối so với thư mục MQL5, đặc biệt bắt đầu bằng /Files/
. Do đó, có thể mở mẫu đã lưu bằng các hàm thao tác tệp, phân tích và nếu cần, chỉnh sửa chúng (xem ví dụ ChartTemplate.mq5
ở phần sau).
Nếu một tệp có cùng tên đã tồn tại tại đường dẫn được chỉ định, nội dung của nó sẽ bị ghi đè.
Chúng ta sẽ xem xét một ví dụ kết hợp để lưu và áp dụng mẫu sau một chút.
bool ChartApplyTemplate(long chartId, const string filename)
Hàm áp dụng mẫu từ tệp được chỉ định vào biểu đồ chartId
.
Tệp mẫu được tìm kiếm theo các quy tắc sau:
- Nếu
filename
chứa đường dẫn (bắt đầu bằng dấu gạch chéo ngược\
hoặc dấu gạch chéo/
), thì mẫu được khớp tương đối với đường dẫnterminal_data_directory/MQL5
. - Nếu không có đường dẫn trong tên, mẫu sẽ được tìm kiếm ở cùng vị trí với tệp thực thi EX5 nơi hàm được gọi.
- Nếu mẫu không được tìm thấy ở hai nơi đầu tiên, nó sẽ được tìm kiếm trong thư mục mẫu tiêu chuẩn
terminal_dir/Profiles/Templates/
.
Lưu ý rằng terminal_data_directory
đề cập đến thư mục nơi các tệp đã sửa đổi được lưu trữ, và vị trí của nó có thể thay đổi tùy thuộc vào loại hệ điều hành, tên người dùng và cài đặt bảo mật máy tính. Thông thường, nó khác với thư mục terminal_dir
, mặc dù trong một số trường hợp (ví dụ, khi làm việc dưới tài khoản thuộc nhóm Administrators), chúng có thể giống nhau. Vị trí của các thư mục terminal_data_directory
và terminal_directory
có thể được tìm thấy bằng hàm TerminalInfoString (xem các hằng số TERMINAL_DATA_PATH
và TERMINAL_PATH
, tương ứng).
Gọi ChartApplyTemplate
tạo một lệnh được thêm vào hàng đợi tin nhắn của biểu đồ và chỉ được thực thi sau khi tất cả các lệnh trước đó đã được xử lý.
Việc tải một mẫu sẽ dừng tất cả các chương trình MQL đang chạy trên biểu đồ, bao gồm cả chương trình đã khởi tạo việc tải. Nếu mẫu chứa các chỉ báo và một Expert Advisor, các phiên bản mới của chúng sẽ được khởi chạy.
Vì mục đích bảo mật, khi áp dụng mẫu có Expert Advisor vào biểu đồ, quyền giao dịch có thể bị giới hạn. Nếu chương trình MQL gọi hàm
ChartApplyTemplate
không có quyền giao dịch, thì Expert Advisor được tải bằng mẫu sẽ không có quyền giao dịch, bất kể cài đặt của mẫu. Nếu chương trình MQL gọiChartApplyTemplate
được phép giao dịch nhưng giao dịch không được phép trong cài đặt mẫu, thì Expert Advisor được tải bằng mẫu sẽ không được phép giao dịch.
Ví dụ về script ChartDuplicate.mq5
cho phép bạn tạo một bản sao của biểu đồ hiện tại.
void OnStart()
{
const string temp = "/Files/ChartTemp";
if(ChartSaveTemplate(0, temp))
{
const long id = ChartOpen(NULL, 0);
if(!ChartApplyTemplate(id, temp))
{
Print("Apply Error: ", _LastError);
}
}
else
{
Print("Save Error: ", _LastError);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Đầu tiên, một tệp .tpl
tạm thời được tạo bằng ChartSaveTemplate
, sau đó một biểu đồ mới được mở (gọi ChartOpen
), và cuối cùng, hàm ChartApplyTemplate
áp dụng mẫu này vào biểu đồ mới.
Tuy nhiên, trong nhiều trường hợp, lập trình viên phải đối mặt với một nhiệm vụ khó khăn hơn: không chỉ áp dụng mẫu mà còn chỉnh sửa nó trước.
Sử dụng các mẫu, bạn có thể thay đổi nhiều thuộc tính biểu đồ không khả dụng bằng các hàm API MQL5 khác, ví dụ: khả năng hiển thị của các chỉ báo trong bối cảnh các khung thời gian, thứ tự của các cửa sổ con chỉ báo cùng với các đối tượng được áp dụng cho chúng, v.v.
Định dạng tệp .tpl
giống hệt với các tệp .chr
được terminal sử dụng để lưu trữ biểu đồ giữa các phiên (trong thư mục terminal_directory/Profiles/Charts/profile_name
).
Tệp .tpl
là một tệp văn bản có cú pháp đặc biệt. Các thuộc tính trong đó có thể là cặp key=value
được viết trên một dòng hoặc một loại nhóm chứa nhiều thuộc tính key=value
. Những nhóm này sẽ được gọi là container dưới đây vì ngoài các thuộc tính riêng lẻ, chúng cũng có thể chứa các container khác lồng nhau.
Container bắt đầu bằng một dòng trông như <tag>
, trong đó tag
là một trong những loại container được xác định trước (xem bên dưới), và kết thúc bằng một cặp dòng như </tag>
(tên thẻ phải khớp nhau). Nói cách khác, định dạng này tương tự như XML (không có tiêu đề), trong đó tất cả các đơn vị từ vựng phải được viết trên các dòng riêng biệt và các thuộc tính của thẻ không được chỉ định bởi các thuộc tính của chúng (như trong XML bên trong phần mở <tag attribute1=value1...>
), mà trong văn bản bên trong của thẻ.
Danh sách các thẻ được hỗ trợ:
chart
— container gốc với các thuộc tính chính của biểu đồ và tất cả các container phụ;expert
— container với các thuộc tính chung của Expert Advisor, ví dụ: quyền giao dịch (bên trong biểu đồ);window
— container với các thuộc tính của cửa sổ/cửa sổ con và các container phụ của nó (bên trong biểu đồ);object
— container với các thuộc tính của đối tượng đồ họa (bên trong cửa sổ);indicator
— container với các thuộc tính của chỉ báo (bên trong cửa sổ);graph
— container với các thuộc tính biểu đồ của chỉ báo (bên trong chỉ báo);level
— container với các thuộc tính mức của chỉ báo (bên trong chỉ báo);period
— container với các thuộc tính hiển thị của một đối tượng hoặc chỉ báo trên một khung thời gian cụ thể (bên trong đối tượng hoặc chỉ báo);inputs
— container với các cài đặt (biến đầu vào) của các chỉ báo tùy chỉnh và Expert Advisors.
Danh sách các thuộc tính có thể trong cặp key=value
khá rộng và không có tài liệu chính thức. Nếu cần, bạn có thể tự tìm hiểu các tính năng này của nền tảng.
Dưới đây là các đoạn từ một tệp .tpl
(các thụt đầu dòng trong định dạng được thực hiện để hiển thị rõ ràng sự lồng nhau của các container).
<chart>
id=0
symbol=EURUSD
description=Euro vs US Dollar
period_type=1
period_size=1
digits=5
...
<window>
height=117.133747
objects=0
<indicator>
name=Main
path=
apply=1
show_data=1
...
fixed_height=-1
</indicator>
</window>
<window>
<indicator>
name=Momentum
path=
apply=6
show_data=1
...
fixed_height=-1
period=14
<graph>
name=
draw=1
style=0
width=1
color=16748574
</graph>
</indicator>
...
</window>
</chart>
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
32
33
34
35
36
37
38
39
40
Chúng ta có tệp tiêu đề TplFile.mqh
để làm việc với các tệp .tpl
, với đó bạn có thể phân tích và sửa đổi các mẫu. Nó có hai lớp:
Container
— để đọc và lưu trữ các phần tử tệp, tính đến cấu trúc phân cấp (lồng nhau), cũng như để ghi vào tệp sau khi có thể sửa đổi;Selector
— để duyệt qua các phần tử của cấu trúc phân cấp (các đối tượngContainer
) theo thứ tự nhằm tìm kiếm sự khớp với một truy vấn nhất định được viết dưới dạng chuỗi tương tự như bộ chọn xpath (/path/element[attribute=value]
).
Các đối tượng của lớp Container
được tạo bằng một hàm khởi tạo nhận mô tả tệp .tpl
để đọc làm tham số đầu tiên và tên thẻ làm tham số thứ hai. Theo mặc định, tên thẻ là NULL
, có nghĩa là container gốc (toàn bộ tệp). Do đó, chính container sẽ tự điền nội dung trong quá trình đọc tệp (xem phương thức read
).
Các thuộc tính của phần tử hiện tại, tức là các cặp key=value
nằm trực tiếp bên trong container này, được cho là sẽ được thêm vào bản đồ MapArray<string,string> properties
. Các container lồng nhau được thêm vào mảng Container *children[]
.
#include <MQL5Book/MapArray.mqh>
#define PUSH(A,V) (A[ArrayResize(A, ArraySize(A) + 1) - 1] = V)
class Container
{
MapArray<string,string> properties;
Container *children[];
const string tag;
const int handle;
public:
Container(const int h, const string t = NULL): handle(h), tag(t) { }
~Container()
{
for(int i = 0; i < ArraySize(children); ++i)
{
if(CheckPointer(children[i]) == POINTER_DYNAMIC) delete children[i];
}
}
bool read(const bool verbose = false)
{
while(!FileIsEnding(handle))
{
string text = FileReadString(handle);
const int len = StringLen(text);
if(len > 0)
{
if(text[0] == '<' && text[len - 1] == '>')
{
const string subtag = StringSubstr(text, 1, len - 2);
if(subtag[0] == '/' && StringFind(subtag, tag) == 1)
{
if(verbose)
{
print();
}
return true; // phần tử đã sẵn sàng
}
PUSH(children, new Container(handle, subtag)).read(verbose);
}
else
{
string pair[];
if(StringSplit(text, '=', pair) == 2)
{
properties.put(pair[0], pair[1]);
}
}
}
}
return false;
}
...
};
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
Trong phương thức read
, chúng ta đọc và phân tích tệp từng dòng. Nếu thẻ mở có dạng <tag>
, chúng ta tạo một đối tượng container mới và tiếp tục đọc trong đó. Nếu thẻ đóng có dạng </tag>
với cùng tên, chúng ta trả về một cờ thành công (true
), có nghĩa là container đã được tạo. Trong các dòng còn lại, chúng ta đọc các cặp key=value
và thêm chúng vào mảng properties
.
Chúng ta đã chuẩn bị Selector
để tìm kiếm các phần tử trong mẫu. Một chuỗi với cấu trúc phân cấp của các thẻ được tìm kiếm được truyền vào hàm khởi tạo của nó. Ví dụ, chuỗi /chart/window/indicator
tương ứng với một biểu đồ có cửa sổ/cửa sổ con, mà trong đó, lần lượt, chứa bất kỳ chỉ báo nào. Kết quả tìm kiếm sẽ là sự khớp đầu tiên. Truy vấn này, theo quy tắc, sẽ tìm thấy biểu đồ báo giá, vì nó được lưu trong mẫu dưới dạng chỉ báo có tên Main
và nằm ở đầu tệp, trước các cửa sổ con khác.
Các truy vấn thực tế hơn chỉ định tên và giá trị của một thuộc tính cụ thể. Đặc biệt, chuỗi đã sửa đổi /chart/window/indicator[name=Momentum]
sẽ chỉ tìm kiếm chỉ báo Momentum
. Tìm kiếm này khác với việc gọi ChartWindowFind
, vì ở đây tên được chỉ định mà không có tham số, trong khi ChartWindowFind
sử dụng tên ngắn của chỉ báo, thường bao gồm các giá trị tham số, nhưng chúng có thể thay đổi.
Đối với các chỉ báo tích hợp sẵn, thuộc tính name
chứa chính tên đó, và đối với các chỉ báo tùy chỉnh, nó sẽ hiển thị Custom Indicator
. Liên kết đến chỉ báo tùy chỉnh được cung cấp trong thuộc tính path
dưới dạng đường dẫn đến tệp thực thi, ví dụ: Indicators\MQL5Book\IndTripleEMA.ex5
.
bool save(int h)
{
if(tag != NULL)
{
if(FileWriteString(h, "<" + tag + ">\n") <= 0)
return false;
}
for(int i = 0; i < properties.getSize(); ++i)
{
if(FileWriteString(h, properties.getKey(i) + "=" + properties[i] + "\n") <= 0)
return false;
}
for(int i = 0; i < ArraySize(children); ++i)
{
children[i].save(h);
}
if(tag != NULL)
{
if(FileWriteString(h, "</" + tag + ">\n") <= 0)
return false;
}
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Phương thức cấp cao để ghi toàn bộ mẫu vào tệp được gọi là write
. Tham số đầu vào của nó (mô tả tệp) có thể bằng 0, nghĩa là ghi vào cùng tệp mà nó được đọc. Tuy nhiên, tệp phải được mở với quyền ghi.
Cần lưu ý rằng khi ghi đè một tệp văn bản Unicode, MQL5 không ghi dấu UTF ban đầu (cái gọi là BOM, Byte Order Mark), và do đó chúng ta phải tự làm điều đó. Nếu không có dấu này, terminal sẽ không đọc và áp dụng mẫu của chúng ta.
Nếu mã gọi truyền vào tham số h
một tệp khác được mở chỉ để ghi ở định dạng Unicode, MQL5 sẽ tự động ghi BOM.
bool write(int h = 0)
{
bool rewriting = false;
if(h == 0)
{
h = handle;
rewriting = true;
}
if(!FileGetInteger(h, FILE_IS_WRITABLE))
{
Print("File is not writable");
return false;
}
if(rewriting)
{
// NB! Chúng ta ghi BOM thủ công vì MQL5 không làm điều này khi ghi đè
ushort u[1] = {0xFEFF};
FileSeek(h, SEEK_SET, 0);
FileWriteString(h, ShortArrayToString(u));
}
bool result = save(h);
if(rewriting)
{
// NB! MQL5 không cho phép giảm kích thước tệp,
// vì vậy chúng ta điền phần cuối thừa bằng khoảng trắng
while(FileTell(h) < FileSize(h) && !IsStopped())
{
FileWriteString(h, " ");
}
}
return result;
}
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
32
33
34
35
Để thể hiện khả năng của các lớp mới, hãy xem xét vấn đề ẩn cửa sổ của một chỉ báo cụ thể. Như bạn đã biết, người dùng có thể đạt được điều này bằng cách đặt lại các cờ hiển thị cho các khung thời gian trong hộp thoại thuộc tính chỉ báo (tab Display
). Theo chương trình, điều này không thể thực hiện trực tiếp. Đây là nơi khả năng chỉnh sửa mẫu trở nên hữu ích.
Trong mẫu, khả năng hiển thị của chỉ báo cho các khung thời gian được chỉ định trong container <indicator>
, bên trong đó một container riêng được ghi cho mỗi khung thời gian hiển thị <period>
. Ví dụ, khả năng hiển thị trên khung thời gian M15 trông như sau:
<period>
period_type=0
period_size=15
</period>
2
3
4
Bên trong container <period>
, các thuộc tính period_type
và period_size
được sử dụng. period_type
là đơn vị đo lường, một trong những giá trị sau:
- 0 cho phút
- 1 cho giờ
- 2 cho tuần
- 3 cho tháng
period_size
là số đơn vị đo lường trong khung thời gian. Cần lưu ý rằng khung thời gian hàng ngày được chỉ định là 24 giờ.
Khi không có container lồng <period>
trong container <indicator>
, chỉ báo được hiển thị trên tất cả các khung thời gian.
Cuốn sách đi kèm với script ChartTemplate.mq5
, script này thêm chỉ báo Momentum vào biểu đồ (nếu nó chưa có) và làm cho nó chỉ hiển thị trên khung thời gian hàng tháng.
void OnStart()
{
// nếu Momentum(14) chưa có trên biểu đồ, thêm nó
const int w = ChartWindowFind(0, "Momentum(14)");
if(w == -1)
{
const int momentum = iMomentum(NULL, 0, 14, PRICE_TYPICAL);
ChartIndicatorAdd(0, (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL), momentum);
// không cần thiết ở đây vì script sẽ sớm thoát,
// tuy nhiên tuyên bố rõ ràng rằng handle sẽ không còn cần trong mã
IndicatorRelease(momentum);
}
2
3
4
5
6
7
8
9
10
11
12
Tiếp theo, chúng ta lưu mẫu biểu đồ hiện tại vào một tệp, sau đó mở nó để ghi và đọc. Có thể phân bổ một tệp riêng để ghi.
const string filename = _Symbol + "-" + PeriodToString(_Period) + "-momentum-rw";
if(PRTF(ChartSaveTemplate(0, "/Files/" + filename)))
{
int handle = PRTF(FileOpen(filename + ".tpl",
FILE_READ | FILE_WRITE | FILE_TXT | FILE_SHARE_READ | FILE_SHARE_WRITE));
// thay thế - một tệp khác mở chỉ để ghi
// int writer = PRTF(FileOpen(filename + "w.tpl",
// FILE_WRITE | FILE_TXT | FILE_SHARE_READ | FILE_SHARE_WRITE));
2
3
4
5
6
7
8
Sau khi nhận được mô tả tệp, chúng ta tạo một container gốc main
và đọc toàn bộ tệp vào đó (các container lồng nhau và tất cả thuộc tính của chúng sẽ được đọc tự động).
Container main(handle);
main.read();
2
Sau đó, chúng ta xác định một bộ chọn để tìm kiếm chỉ báo Momentum
. Theo lý thuyết, một cách tiếp cận nghiêm ngặt hơn cũng sẽ yêu cầu kiểm tra khoảng thời gian đã chỉ định (14), nhưng các lớp của chúng ta không hỗ trợ truy vấn nhiều thuộc tính cùng lúc (khả năng này được để lại cho việc tự nghiên cứu).
Sử dụng bộ chọn, chúng ta tìm kiếm, in đối tượng được tìm thấy (chỉ để tham khảo) và thêm container lồng <period>
của nó với các cài đặt để hiển thị khung thời gian hàng tháng.
Container *found = main.find("/chart/window/indicator[name=Momentum]");
if(found)
{
found.print();
Container *period = found.add("period");
period.assign("period_type", "3");
period.assign("period_size", "1");
}
2
3
4
5
6
7
8
Cuối cùng, chúng ta ghi mẫu đã sửa đổi vào cùng tệp, đóng nó và áp dụng nó trên biểu đồ.
main.write(); // hoặc main.write(writer);
FileClose(handle);
PRTF(ChartApplyTemplate(0, "/Files/" + filename));
}
}
2
3
4
5
6
Khi chạy script trên một biểu đồ sạch, chúng ta sẽ thấy các mục như vậy trong nhật ký.
ChartSaveTemplate(0,/Files/+filename)=true / ok
FileOpen(filename+.tpl,FILE_READ|FILE_WRITE|FILE_TXT| »
» FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_UNICODE)=1 / ok
<> 1 accepted
<chart> 2 accepted
<window> 1 accepted
<indicator> 0
<window> 1 accepted
<indicator> 1 accepted
Tag: indicator
[key] [value]
[ 0] "name" "Momentum"
[ 1] "path" ""
[ 2] "apply" "6"
[ 3] "show_data" "1"
[ 4] "scale_inherit" "0"
[ 5] "scale_line" "0"
[ 6] "scale_line_percent" "50"
[ 7] "scale_line_value" "0.000000"
[ 8] "scale_fix_min" "0"
[ 9] "scale_fix_min_val" "0.000000"
[10] "scale_fix_max" "0"
[11] "scale_fix_max_val" "0.000000"
[12] "expertmode" "0"
[13] "fixed_height" "-1"
[14] "period" "14"
ChartApplyTemplate(0,/Files/+filename)=true / ok
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
Có thể thấy ở đây rằng trước khi tìm thấy chỉ báo cần thiết (được đánh dấu "accepted"), thuật toán đã tìm thấy chỉ báo trong cửa sổ chính trước đó, nhưng nó không phù hợp, vì tên của nó không bằng với Momentum
mong muốn.
Bây giờ, nếu bạn mở danh sách các chỉ báo trên biểu đồ, sẽ có momentum
, và trong hộp thoại thuộc tính của nó, trên tab Display
, khung thời gian duy nhất được kích hoạt là Month
.
Cuốn sách đi kèm với phiên bản mở rộng của tệp TplFileFull.mqh
, hỗ trợ các thao tác so sánh khác nhau trong các điều kiện để chọn thẻ và lựa chọn nhiều thẻ vào mảng. Một ví dụ về việc sử dụng nó có thể được tìm thấy trong script ChartUnfix.mq5
, script này bỏ cố định kích thước của tất cả các cửa sổ con biểu đồ.