Nhận khung dữ liệu trong terminal
Các khung được gửi từ các agent kiểm tra bằng hàm FrameAdd
được chuyển đến terminal và ghi theo thứ tự nhận được vào một tệp mqd có tên của Expert Advisor trong thư mục terminal_directory/MQL5/Files/Tester
. Việc một hoặc nhiều khung đến cùng lúc sẽ tạo ra sự kiện OnTesterPass
.
API MQL5 cung cấp 4 hàm để phân tích và đọc các khung: FrameFirst
, FrameFilter
, FrameNext
, và FrameInputs
. Tất cả các hàm đều trả về giá trị boolean với chỉ báo thành công (true
) hoặc lỗi (false
).
Để truy cập các khung hiện có, nhân tố duy trì một con trỏ nội bộ đến khung hiện tại. Con trỏ tự động di chuyển về phía trước khi khung tiếp theo được đọc bằng hàm FrameNext
, nhưng nó có thể được đưa về đầu tất cả các khung bằng FrameFirst
hoặc FrameFilter
. Do đó, một chương trình MQL có thể tổ chức việc lặp qua các khung trong một vòng lặp cho đến khi xem qua hết tất cả các khung. Quá trình này có thể được lặp lại nếu cần, ví dụ, bằng cách áp dụng các bộ lọc khác nhau trong OnTesterDeinit
.
bool FrameFirst()
Hàm FrameFirst
đặt con trỏ đọc khung nội bộ về đầu và đặt lại bộ lọc (nếu trước đó đã được thiết lập bằng hàm FrameFilter
).
Về lý thuyết, để nhận và xử lý một lần tất cả các khung, không cần gọi FrameFirst
, vì con trỏ đã ở vị trí đầu khi tối ưu hóa bắt đầu.
bool FrameFilter(const string name, ulong id)
Nó thiết lập bộ lọc đọc khung và đặt con trỏ khung nội bộ về đầu. Bộ lọc sẽ ảnh hưởng đến các khung nào được bao gồm trong các lần gọi tiếp theo của FrameNext
.
Nếu một chuỗi rỗng được truyền làm tham số đầu tiên, bộ lọc sẽ chỉ hoạt động theo tham số số, tức là tất cả các khung với id
được chỉ định. Nếu giá trị của tham số thứ hai bằng ULONG_MAX, thì chỉ bộ lọc văn bản hoạt động.
Gọi FrameFilter("", ULONG_MAX)
tương đương với gọi FrameFirst()
, tức là không có bộ lọc.
Nếu bạn gọi
FrameFirst
hoặcFrameFilter
trongOnTesterPass
, hãy đảm bảo đây thực sự là điều bạn cần: mã có thể chứa lỗi logic vì có thể lặp lại, đọc cùng một khung, hoặc tăng tải tính toán theo cấp số nhân.
bool FrameNext(ulong &pass, string &name, ulong &id, double &value)
bool FrameNext(ulong &pass, string &name, ulong &id, double &value, void &data[])
Hàm FrameNext
đọc một khung và di chuyển con trỏ đến khung tiếp theo. Tham số pass
sẽ được ghi số lần chạy tối ưu hóa. Các tham số name
, id
, và value
sẽ nhận các giá trị được truyền trong các tham số tương ứng của hàm FrameAdd
.
Điều quan trọng cần lưu ý là hàm có thể trả về false
trong khi hoạt động bình thường khi không còn khung nào để đọc. Trong trường hợp này, biến tích hợp _LastError
chứa giá trị 4000 (nó không có ký hiệu tích hợp).
Dù dạng nào của hàm FrameAdd
được sử dụng để gửi dữ liệu, nội dung của tệp hoặc mảng sẽ được đặt trong mảng nhận data
. Kiểu của mảng nhận phải khớp với kiểu của mảng được gửi, và có một số sắc thái trong trường hợp gửi tệp.
Tệp nhị phân (FILE_BIN) nên được chấp nhận trong một mảng byte uchar
để đảm bảo tương thích với bất kỳ kích thước nào (vì các kiểu lớn hơn khác có thể không phải là bội số của kích thước tệp). Nếu kích thước tệp (thực tế là kích thước của khối dữ liệu trong khung nhận được) không phải là bội số của kích thước kiểu mảng nhận, hàm FrameNext
sẽ không đọc dữ liệu và sẽ trả về lỗi INVALID_ARRAY (4006).
Tệp văn bản Unicode
(FILE_TXT hoặc FILE_CSV không có bộ修饰符 FILE_ANSI) nên được chấp nhận vào một mảng kiểu ushort
và sau đó chuyển đổi thành chuỗi bằng cách gọi ShortArrayToString
. Tệp văn bản ANSI nên được nhận trong mảng uchar
và chuyển đổi bằng CharArrayToString
.
bool FrameInputs(ulong pass, string ¶meters[], uint &count)
Hàm FrameInputs
cho phép lấy mô tả và giá trị của các tham số input
của Expert Advisor mà lần chạy với số lần chạy được chỉ định được hình thành. Mảng chuỗi parameters
sẽ được điền với các dòng như "ParameterNameN=ValueParameterN". Tham số count
sẽ được điền với số phần tử trong mảng parameters
.
Việc gọi bốn hàm này chỉ được phép bên trong các trình xử lý OnTesterPass
và OnTesterDeinit
.
Các khung có thể đến terminal theo lô, trong trường hợp đó cần thời gian để chuyển chúng. Vì vậy, không nhất thiết tất cả chúng sẽ kịp tạo ra sự kiện OnTesterPass
và được xử lý cho đến khi kết thúc tối ưu hóa. Về vấn đề này, để đảm bảo nhận được tất cả các khung đến muộn, cần đặt một khối mã xử lý chúng (sử dụng hàm FrameNext
) trong OnTesterDeinit
.
Hãy xem xét một ví dụ đơn giản FrameTransfer.mq5
.
Expert Advisor có bốn tham số kiểm tra. Tất cả, ngoại trừ chuỗi cuối cùng, có thể được đưa vào tối ưu hóa.
input bool Parameter0;
input long Parameter1;
input double Parameter2;
input string Parameter3;
2
3
4
Tuy nhiên, để đơn giản hóa ví dụ, số bước cho các tham số Parameter1
và Parameter2
được giới hạn ở 10 (cho mỗi tham số). Do đó, nếu không sử dụng Parameter0
, số lần chạy tối đa là 121. Parameter3
là ví dụ về tham số không thể đưa vào tối ưu hóa.
Expert Advisor không giao dịch mà tạo dữ liệu ngẫu nhiên mô phỏng dữ liệu ứng dụng tùy ý. Không sử dụng việc ngẫu nhiên hóa như thế này trong các dự án thực tế: nó chỉ phù hợp cho mục đích trình diễn.
ulong startup; // theo dõi thời gian của một lần chạy (chỉ như dữ liệu demo)
int OnInit()
{
startup = GetMicrosecondCount();
MathSrand((int)startup);
return INIT_SUCCEEDED;
}
2
3
4
5
6
7
8
Dữ liệu được gửi trong hai loại khung: từ tệp và từ mảng. Mỗi loại có định danh riêng.
#define MY_FILE_ID 100
#define MY_TIME_ID 101
double OnTester()
{
// gửi tệp trong một khung
const static string filename = "binfile";
int h = FileOpen(filename, FILE_WRITE | FILE_BIN | FILE_ANSI);
FileWriteString(h, StringFormat("Random: %d", MathRand()));
FileClose(h);
FrameAdd(filename, MY_FILE_ID, MathRand(), filename);
// gửi mảng trong một khung khác
ulong dummy[1];
dummy[0] = GetMicrosecondCount() - startup;
FrameAdd("timing", MY_TIME_ID, 0, dummy);
return (Parameter2 + 1) * (Parameter1 + 2);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Tệp được ghi dưới dạng nhị phân, với các chuỗi đơn giản. Kết quả (tiêu chí) của OnTester
là một biểu thức số học đơn giản liên quan đến Parameter1
và Parameter2
.
Ở phía nhận, trong bản sao Expert Advisor chạy ở chế độ dịch vụ trên biểu đồ terminal, chúng ta thu thập dữ liệu từ tất cả các khung với tệp và đưa chúng vào một tệp CSV chung. Tệp được mở trong trình xử lý OnTesterInit
.
int handle; // tệp để thu thập kết quả ứng dụng
void OnTesterInit()
{
handle = FileOpen("output.csv", FILE_WRITE | FILE_CSV | FILE_ANSI, ",");
}
2
3
4
5
Như đã đề cập trước đó, tất cả các khung có thể không kịp vào trình xử lý OnTesterPass
, và chúng cần được kiểm tra thêm trong OnTesterDeinit
. Do đó, chúng ta đã triển khai một hàm trợ giúp ProcessFileFrames
, mà chúng ta sẽ gọi từ OnTesterPass
và từ OnTesterDeinit
.
Bên trong ProcessFileFrames
, chúng ta giữ một bộ đếm nội bộ của các khung đã xử lý, framecount
. Sử dụng nó làm ví dụ, chúng ta sẽ đảm bảo rằng thứ tự đến của các khung và số thứ tự của các lần kiểm tra thường không khớp.
void ProcessFileFrames()
{
static ulong framecount = 0;
...
2
3
4
Để nhận các khung trong hàm, các biến cần thiết theo nguyên mẫu FrameNext
được mô tả. Mảng dữ liệu nhận được mô tả ở đây dưới dạng uchar
. Nếu chúng ta ghi một số cấu trúc vào tệp nhị phân của mình, chúng ta có thể nhận chúng trực tiếp vào một mảng cấu trúc cùng loại.
ulong pass;
string name;
long id;
double value;
uchar data[];
...
2
3
4
5
6
Tiếp theo mô tả các biến để lấy các đầu vào Expert Advisor cho lần chạy hiện tại mà khung thuộc về.
string params[];
uint count;
...
2
3
Sau đó, chúng ta đọc các khung trong một vòng lặp với FrameNext
. Hãy nhớ rằng một số khung có thể vào trình xử lý cùng lúc, vì vậy cần một vòng lặp. Đối với mỗi khung, chúng ta xuất ra nhật ký terminal số lần chạy, tên của khung, và giá trị double
kết quả. Chúng ta bỏ qua các khung có ID khác MY_FILE_ID và sẽ xử lý chúng sau.
ResetLastError();
while(FrameNext(pass, name, id, value, data))
{
PrintFormat("Pass: %lld Frame: %s Value:%f", pass, name, value);
if(id != MY_FILE_ID) continue;
...
}
if(_LastError != 4000 && _LastError != 0)
{
Print("Error: ", E2S(_LastError));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Đối với các khung với MY_FILE_ID, chúng ta thực hiện như sau: truy vấn các biến đầu vào, tìm hiểu xem những biến nào được đưa vào tối ưu hóa, và lưu giá trị của chúng vào tệp CSV chung cùng với thông tin từ khung. Khi số lượng khung là 0, chúng ta tạo tiêu đề của tệp CSV trong biến header
. Trong tất cả các khung, bản ghi hiện tại (mới) cho tệp CSV được hình thành trong biến record
.
void ProcessFileFrames()
{
...
if(FrameInputs(pass, params, count))
{
string header, record;
if(framecount == 0) // chuẩn bị tiêu đề CSV
{
header = "Counter,Pass ID,";
}
record = (string)framecount + "," + (string)pass + ",";
// thu thập các tham số tối ưu hóa và giá trị của chúng
for(uint i = 0; i < count; i++)
{
string name2value[];
int n = StringSplit(params[i], '=', name2value);
if(n == 2)
{
long pvalue, pstart, pstep, pstop;
bool enabled = false;
if(ParameterGetRange(name2value[0],
enabled, pvalue, pstart, pstep, pstop))
{
if(enabled)
{
if(framecount == 0) // chuẩn bị tiêu đề CSV
{
header += name2value[0] + ",";
}
record += name2value[1] + ","; // trường dữ liệu
}
}
}
}
if(framecount == 0) // chuẩn bị tiêu đề CSV
{
FileWriteString(handle, header + "Value,File Content\n");
}
// ghi dữ liệu vào CSV
FileWriteString(handle, record + DoubleToString(value) + ","
+ CharArrayToString(data) + "\n");
}
framecount++;
...
}
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
Việc gọi ParameterGetRange
cũng có thể được thực hiện hiệu quả hơn, chỉ với giá trị bằng 0 của framecount
. Bạn có thể thử làm điều đó.
Trong trình xử lý OnTesterPass
, chúng ta chỉ gọi ProcessFileFrames
.
void OnTesterPass()
{
ProcessFileFrames(); // xử lý tiêu chuẩn các khung trong quá trình thực hiện
}
2
3
4
Ngoài ra, chúng ta gọi cùng hàm từ OnTesterDeinit
và đóng tệp CSV.
void OnTesterDeinit()
{
ProcessFileFrames(); // thu thập các khung đến muộn
FileClose(handle); // đóng tệp CSV
..
}
2
3
4
5
6
Trong OnTesterDeinit
, chúng ta xử lý các khung với MY_TIME_ID. Thời gian của các lần kiểm tra được chuyển trong các khung này, và thời gian trung bình của một lần chạy được tính toán ở đây. Về lý thuyết, điều này chỉ có ý nghĩa để phân tích trong chương trình của bạn, vì đối với người dùng, thời gian của các lần chạy đã được tester hiển thị trong nhật ký.
void OnTesterDeinit()
{
...
ulong pass;
string name;
long id;
double value;
ulong data[]; // cùng loại mảng như đã gửi
FrameFilter("timing", MY_TIME_ID); // quay lại khung đầu tiên
ulong count = 0;
ulong total = 0;
// lặp qua các khung 'timing' chỉ
while(FrameNext(pass, name, id, value, data))
{
if(ArraySize(data) == 1)
{
total += data[0];
}
else
{
total += (ulong)value;
}
++count;
}
if(count > 0)
{
PrintFormat("Average timing: %lld", total / count);
}
}
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
Expert Advisor đã sẵn sàng. Hãy bật tối ưu hóa hoàn toàn cho nó (vì tổng số tùy chọn được giới hạn nhân tạo và quá nhỏ cho thuật toán di truyền). Chúng ta có thể chọn chỉ giá mở vì Expert Advisor không giao dịch. Do đó, bạn nên chọn một tiêu chí tùy chỉnh (tất cả các tiêu chí khác sẽ cho 0). Ví dụ, hãy đặt phạm vi Parameter1
từ 1 đến 10 với bước đơn, và Parameter2
được đặt từ -0.5 đến +0.5 với bước 0.1.
Hãy chạy tối ưu hóa. Trong nhật ký chuyên gia trong terminal, chúng ta sẽ thấy các mục về các khung nhận được dưới dạng:
Pass: 0 Frame: binfile Value:5105.000000
Pass: 0 Frame: timing Value:0.000000
Pass: 1 Frame: binfile Value:28170.000000
Pass: 1 Frame: timing Value:0.000000
Pass: 2 Frame: binfile Value:17422.000000
Pass: 2 Frame: timing Value:0.000000
...
Average timing: 1811
2
3
4
5
6
7
8
Các dòng tương ứng với số lần chạy, giá trị tham số và nội dung khung sẽ xuất hiện trong tệp output.csv:
Counter,Pass ID,Parameter1,Parameter2,Value,File Content
0,0,0,-0.5,5105.00000000,Random: 87
1,1,1,-0.5,28170.00000000,Random: 64
2,2,2,-0.5,17422.00000000,Random: 61
...
37,35,2,-0.2,6151.00000000,Random: 68
38,62,7,0.0,17422.00000000,Random: 61
39,36,3,-0.2,16899.00000000,Random: 71
40,63,8,0.0,17422.00000000,Random: 61
...
117,116,6,0.5,27648.00000000,Random: 74
118,117,7,0.5,16899.00000000,Random: 71
119,118,8,0.5,17422.00000000,Random: 61
120,119,9,0.5,28170.00000000,Random: 64
2
3
4
5
6
7
8
9
10
11
12
13
14
Rõ ràng, số thứ tự nội bộ của chúng ta (cột Count
) đi theo thứ tự, và số lần chạy Pass ID
có thể bị lẫn lộn (điều này phụ thuộc vào nhiều yếu tố của việc xử lý song song các lô công việc bởi các agent). Đặc biệt, lô công việc có thể là lô đầu tiên hoàn thành agent mà các công việc với số thứ tự cao hơn được giao: trong trường hợp này, số thứ tự trong tệp sẽ bắt đầu từ các lần chạy cao hơn.
Trong nhật ký của tester, bạn có thể kiểm tra số liệu thống kê dịch vụ theo khung.
242 frames (42.78 Kb total, 181 bytes per frame) received
local 121 tasks (100%), remote 0 tasks (0%), cloud 0 tasks (0%)
121 new records saved to cache file 'tester\cache\FrameTransfer.EURUSD.H1. »
» 20220101.20220201.20.9E2DE099D4744A064644F6BB39711DE8.opt'
2
3
4
Điều quan trọng cần lưu ý là trong quá trình tối ưu hóa di truyền, số lần chạy được trình bày trong báo cáo tối ưu hóa dưới dạng cặp (generation number, copy number)
, trong khi số lần chạy thu được trong hàm FrameNext
là ulong
. Thực tế, nó là số lần chạy trong các công việc lô trong bối cảnh của lần chạy tối ưu hóa hiện tại. MQL5 không cung cấp phương tiện để khớp số lần chạy với báo cáo di truyền. Để làm điều này, cần tính toán tổng kiểm tra của các tham số đầu vào của mỗi lần chạy. Các tệp Opt với bộ nhớ cache tối ưu hóa đã chứa trường như vậy với hàm băm MD5.