Chuyển cơ sở dữ liệu lịch sang bộ kiểm tra
Lịch chỉ khả dụng cho các chương trình MQL trực tuyến, và do đó việc kiểm tra các chiến lược giao dịch tin tức gặp một số khó khăn. Một trong những giải pháp là tự tạo một hình ảnh nhất định của lịch, tức là bộ nhớ đệm, và sau đó sử dụng nó trong bộ kiểm tra. Công nghệ lưu trữ bộ nhớ đệm có thể khác nhau, chẳng hạn như tệp hoặc cơ sở dữ liệu SQLite nhúng. Trong phần này, chúng ta sẽ trình bày một triển khai sử dụng tệp.
Dù trong trường hợp nào, khi sử dụng bộ nhớ đệm lịch, hãy nhớ rằng nó tương ứng với một thời điểm cụ thể X. Trong tất cả các sự kiện "cũ" (báo cáo tài chính) xảy ra trước X, các giá trị thực tế đã được thiết lập, còn trong các sự kiện sau đó (trong "tương lai", so với X) thì không có giá trị thực tế, và sẽ không có cho đến khi một bản sao mới, cập nhật hơn của bộ nhớ đệm xuất hiện. Nói cách khác, không có ý nghĩa gì khi kiểm tra các chỉ báo và Expert Advisors ở bên phải của X. Đối với những cái ở bên trái của X, bạn nên tránh nhìn trước, tức là không đọc các chỉ báo hiện tại cho đến thời điểm công bố của mỗi tin tức cụ thể.
Chú ý! Khi yêu cầu dữ liệu lịch trong terminal, thời gian của tất cả các sự kiện được báo cáo dựa trên múi giờ hiện tại của máy chủ, bao gồm cả khả năng điều chỉnh cho thời gian "tiết kiệm ánh sáng ban ngày" (thường thì điều này có nghĩa là tăng dấu thời gian lên 1 giờ). Điều này đồng bộ hóa việc phát hành tin tức với thời gian báo giá trực tuyến. Tuy nhiên, các thay đổi đồng hồ trong quá khứ (nửa năm, một năm trước, hoặc hơn) chỉ được hiển thị trong báo giá, nhưng không có trong các sự kiện lịch. Toàn bộ cơ sở dữ liệu lịch được đọc qua MQL5 theo múi giờ hiện tại của máy chủ. Do đó, bất kỳ kho lưu trữ lịch nào được tạo sẽ chứa các dấu thời gian chính xác cho những sự kiện xảy ra cùng chế độ DST (bật hoặc tắt) đang hoạt động tại thời điểm lưu trữ. Đối với các sự kiện trong "nửa năm đối lập", cần tự điều chỉnh một giờ sau khi đọc kho lưu trữ. Trong các ví dụ dưới đây, tình huống này được bỏ qua.
Chúng ta sẽ gọi lớp bộ nhớ đệm là CalendarCache
và đặt nó trong một tệp có tên CalendarCache.mqh
. Chúng ta sẽ cần lưu tất cả 3 bảng của cơ sở lịch vào tệp (MqlCalendarCountry
, MqlCalendarEvent
, MqlCalendarValue
). MQL5 cung cấp các hàm FileWriteArray
và FileReadArray
(xem Viết và đọc mảng) có thể trực tiếp ghi và đọc các mảng của cấu trúc đơn giản vào tệp. Tuy nhiên, 2 trong số 3 cấu trúc trong trường hợp của chúng ta không đơn giản, vì chúng có các trường chuỗi. Do đó, chúng ta cần một cơ chế để lưu trữ chuỗi riêng biệt, tương tự như cơ chế chúng ta đã sử dụng trong lớp CalendarFilter
(có một mảng chuỗi stringCache
, và chỉ số của chuỗi mong muốn từ mảng này được chỉ định trong các bộ lọc).
Để tránh lẫn lộn các chuỗi từ các cấu trúc "lịch" khác nhau trong một "từ điển", chúng ta sẽ chuẩn bị một lớp mẫu StringRef
: tham số loại T sẽ là bất kỳ cấu trúc nào của MqlCalendar
. Điều này sẽ cho chúng ta một bộ nhớ đệm chuỗi riêng cho các quốc gia, và một bộ nhớ đệm chuỗi riêng cho các loại sự kiện.
template<typename T>
struct StringRef
{
static string cache[];
int index;
StringRef(): index(-1) { }
void operator=(const string s)
{
if(index == -1)
{
PUSH(cache, s);
index = ArraySize(cache) - 1;
}
else
{
cache[index] = s;
}
}
string operator[](int x = 0) const
{
if(index != -1)
{
return cache[index];
}
return NULL;
}
static bool save(const int handle)
{
FileWriteInteger(handle, ArraySize(cache));
for(int i = 0; i < ArraySize(cache); ++i)
{
FileWriteInteger(handle, StringLen(cache[i]));
FileWriteString(handle, cache[i]);
}
return true;
}
static bool load(const int handle)
{
const int n = FileReadInteger(handle);
for(int i = 0; i < n; ++i)
{
PUSH(cache, FileReadString(handle, FileReadInteger(handle)));
}
return true;
}
};
template<typename T>
static string StringRef::cache[];
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
Các chuỗi được lưu trữ trong mảng cache
bằng cách sử dụng operator=
, và được trích xuất từ nó bằng operator[]
(với một chỉ số giả luôn bị bỏ qua). Mỗi đối tượng chỉ lưu trữ chỉ số của chuỗi trong mảng. Mảng cache
được khai báo tĩnh, vì vậy nó sẽ tích lũy tất cả các trường chuỗi của một cấu trúc T. Những ai muốn có thể thay đổi phương pháp lưu trữ bộ nhớ đệm sao cho mỗi trường của cấu trúc có mảng riêng, nhưng điều này không quan trọng đối với chúng ta.
Việc ghi mảng vào tệp và đọc từ tệp được thực hiện bởi một cặp phương thức tĩnh save
và load
: cả hai đều nhận một xử lý tệp làm tham số.
Dựa trên lớp StringRef
, chúng ta hãy mô tả các cấu trúc sao chép các cấu trúc lịch tiêu chuẩn sử dụng các đối tượng StringRef
thay cho các trường chuỗi. Ví dụ, đối với MqlCalendarCountry
, chúng ta nhận được MqlCalendarCountryRef
. Các cấu trúc tiêu chuẩn và sửa đổi được sao chép vào nhau theo cách tương tự bởi các toán tử nạp chồng '=' và '[]'.
struct MqlCalendarCountryRef
{
ulong id;
StringRef<MqlCalendarCountry> name;
StringRef<MqlCalendarCountry> code;
StringRef<MqlCalendarCountry> currency;
StringRef<MqlCalendarCountry> currency_symbol;
StringRef<MqlCalendarCountry> url_name;
void operator=(const MqlCalendarCountry &c)
{
id = c.id;
name = c.name;
code = c.code;
currency = c.currency;
currency_symbol = c.currency_symbol;
url_name = c.url_name;
}
MqlCalendarCountry operator[](int x = 0) const
{
MqlCalendarCountry r;
r.id = id;
r.name = name[];
r.code = code[];
r.currency = currency[];
r.currency_symbol = currency_symbol[];
r.url_name = url_name[];
return r;
}
};
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
Lưu ý rằng các toán tử gán của phương thức đầu tiên có nạp chồng '=' từ StringRef
, nhờ đó tất cả các chuỗi rơi vào mảng StringRef<MqlCalendarCountry>::cache
. Trong phương thức thứ hai, toán tử '[]' gọi ngầm để lấy địa chỉ của chuỗi và trả về từ StringRef
trực tiếp chuỗi được lưu trữ tại địa chỉ đó trong mảng cache
.
Cấu trúc MqlCalendarEventRef
được định nghĩa theo cách tương tự, nhưng chỉ có 3 trường trong đó (source_url
, event_code
, name
) yêu cầu thay thế loại string
bằng StringRef<MqlCalendarEvent>
. Cấu trúc MqlCalendarValue
không yêu cầu các biến đổi như vậy, vì không có trường chuỗi trong đó.
Điều này kết thúc các giai đoạn chuẩn bị, và bạn có thể chuyển sang lớp bộ nhớ đệm chính CalendarCache
.
Từ các cân nhắc chung, cũng như để tương thích với lớp CalendarFilter
đã phát triển, chúng ta hãy mô tả các trường trong bộ nhớ đệm xác định ngữ cảnh (quốc gia hoặc tiền tệ), phạm vi ngày cho các sự kiện được lưu trữ, và thời điểm tạo bộ nhớ đệm (thời gian X, biến t).
class CalendarCache
{
string context;
datetime from, to;
datetime t;
...
public:
CalendarCache(const string _context = NULL,
const datetime _from = 0, const datetime _to = 0):
context(_context), from(_from), to(_to), t(0)
{
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Thực tế, không có nhiều ý nghĩa trong việc đặt giới hạn khi tạo bộ nhớ đệm từ lịch. Một bộ nhớ đệm đầy đủ có lẽ thực tế hơn vì kích thước của nó không quan trọng, khoảng hai chục megabyte tính đến giữa năm 2022 (bao gồm dữ liệu lịch sử từ năm 2007 với các sự kiện được lên kế hoạch đến năm 2024). Tuy nhiên, các giới hạn có thể hữu ích cho các chương trình demo với chức năng giảm thiểu nhân tạo.
Rõ ràng là các mảng của cấu trúc lịch nên được cung cấp trong bộ nhớ đệm để lưu trữ tất cả dữ liệu.
MqlCalendarValue values[];
MqlCalendarEvent events[];
MqlCalendarCountry countries[];
...
2
3
4
Ban đầu, chúng được điền từ cơ sở dữ liệu lịch bởi phương thức update
.
bool update()
{
string country = NULL, currency = NULL;
if(StringLen(context) == 3)
{
currency = context;
}
else if(StringLen(context) == 2)
{
country = context;
}
Print("Reading online calendar base...");
if(!PRTF(CalendarValueHistory(values, from, to, country, currency))
|| (currency != NULL ?
!PRTF(CalendarEventByCurrency(currency, events)) :
!PRTF(CalendarEventByCountry(country, events)))
|| !PRTF(CalendarCountries(countries)))
{
// object is not ready, t = 0
}
else
{
t = TimeTradeServer();
}
return (bool)t;
}
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
Trường t
là dấu hiệu của sức khỏe bộ nhớ đệm, với thời gian điền các mảng.
Đối tượng bộ nhớ đệm đã điền có thể được ghi vào tệp bằng phương thức save
. Ở đầu tệp, có một tiêu đề CALENDAR_CACHE_HEADER
— đây là chuỗi "MQL5 Calendar Cache\r\nv.1.0\r\n", cho phép đảm bảo rằng định dạng là chính xác khi đọc. Tiếp theo, phương thức lưu các biến context
, from
, to
, và t
, cũng như mảng values
, "nguyên trạng". Trước chính mảng, chúng ta ghi lại kích thước của nó để khôi phục khi đọc.
bool save(string filename = NULL)
{
if(!t) return false;
MqlDateTime mdt;
TimeToStruct(t, mdt);
if(filename == NULL) filename = "calendar-" +
StringFormat("%04d-%02d-%02d-%02d-%02d.cal",
mdt.year, mdt.mon, mdt.day, mdt.hour, mdt.min);
int handle = PRTF(FileOpen(filename, FILE_WRITE | FILE_BIN));
if(handle == INVALID_HANDLE) return false;
FileWriteString(handle, CALENDAR_CACHE_HEADER);
FileWriteString(handle, context, 4);
FileWriteLong(handle, from);
FileWriteLong(handle, to);
FileWriteLong(handle, t);
FileWriteInteger(handle, ArraySize(values));
FileWriteArray(handle, values);
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Với các mảng events
và countries
, chúng ta sử dụng các cấu trúc bao bọc với hậu tố "Ref". Phương thức trợ giúp store
chuyển đổi mảng events
thành mảng của các cấu trúc đơn giản erefs
, trong đó các chuỗi được thay thế bằng số trong từ điển chuỗi StringRef<MqlCalendarEvent>
. Các cấu trúc đơn giản như vậy đã có thể được ghi vào tệp theo cách thông thường, nhưng để đọc tiếp theo, cũng cần lưu tất cả các dòng của từ điển (gọi StringRef<MqlCalendarEvent>::save(handle)
). Các cấu trúc quốc gia được chuyển đổi và lưu vào tệp theo cách tương tự.
MqlCalendarEventRef erefs[];
store(erefs, events);
FileWriteInteger(handle, ArraySize(erefs));
FileWriteArray(handle, erefs);
StringRef<MqlCalendarEvent>::save(handle);
MqlCalendarCountryRef crefs[];
store(crefs, countries);
FileWriteInteger(handle, ArraySize(crefs));
FileWriteArray(handle, crefs);
StringRef<MqlCalendarCountry>::save(handle);
FileClose(handle);
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Phương thức store
đã đề cập khá đơn giản: trong đó, trong một vòng lặp qua các phần tử, một toán tử gán nạp chồng được thực thi trong các cấu trúc MqlCalendarEventRef
hoặc MqlCalendarCountryRef
.
template<typename T1,typename T2>
void static store(T1 &array[], T2 &origin[])
{
ArrayResize(array, ArraySize(origin));
for(int i = 0; i < ArraySize(origin); ++i)
{
array[i] = origin[i];
}
}
2
3
4
5
6
7
8
9
Để tải tệp nhận được vào đối tượng bộ nhớ đệm, một phương thức gương load
được viết. Nó đọc dữ liệu từ tệp vào các biến và mảng theo cùng thứ tự, đồng thời thực hiện các biến đổi ngược của các trường chuỗi cho các loại sự kiện và quốc gia.
bool load(const string filename)
{
Print("Loading calendar cache ", filename);
t = 0;
int handle = PRTF(FileOpen(filename, FILE_READ | FILE_BIN));
if(handle == INVALID_HANDLE) return false;
const string header = FileReadString(handle, StringLen(CALENDAR_CACHE_HEADER));
if(header != CALENDAR_CACHE_HEADER) return false; // not our format
context = FileReadString(handle, 4);
if(!StringLen(context)) context = NULL;
from = (datetime)FileReadLong(handle);
to = (datetime)FileReadLong(handle);
t = (datetime)FileReadLong(handle);
Print("Calendar cache interval: ", from, "-", to);
Print("Calendar cache saved at: ", t);
int n = FileReadInteger(handle);
FileReadArray(handle, values, 0, n);
MqlCalendarEventRef erefs[];
n = FileReadInteger(handle);
FileReadArray(handle, erefs, 0, n);
StringRef<MqlCalendarEvent>::load(handle);
restore(events, erefs);
MqlCalendarCountryRef crefs[];
n = FileReadInteger(handle);
FileReadArray(handle, crefs, 0, n);
StringRef<MqlCalendarCountry>::load(handle);
restore(countries, crefs);
FileClose(handle);
... // something else will be here
}
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
Phương thức trợ giúp restore
sử dụng nạp chồng của toán tử '[]' trong một vòng lặp qua các phần tử trong các cấu trúc MqlCalendarEventRef
hoặc MqlCalendarCountryRef
để lấy chính chuỗi theo số dòng và gán nó cho một cấu trúc tiêu chuẩn MqlCalendarEvent
hoặc MqlCalendarCountry
.
template<typename T1,typename T2>
void static restore(T1 &array[], T2 &origin[])
{
ArrayResize(array, ArraySize(origin));
for(int i = 0; i < ArraySize(origin); ++i)
{
array[i] = origin[i][];
}
}
2
3
4
5
6
7
8
9
Ở giai đoạn này, chúng ta đã có thể viết một chỉ báo thử nghiệm đơn giản dựa trên lớp CalendarCache
, chạy nó trên biểu đồ trực tuyến và lưu nó vào một tệp với bộ nhớ đệm lịch. Sau đó, tệp này có thể được tải từ bản sao của chỉ báo trong bộ kiểm tra, và toàn bộ tập hợp các sự kiện có thể được nhận. Tuy nhiên, điều này chưa đủ cho các phát triển thực tế.
Thực tế là để truy cập dữ liệu nhanh chóng, cần cung cấp indexing
, một khái niệm quen thuộc trong lập trình, mà chúng ta sẽ đề cập sau, trong chương về cơ sở dữ liệu. Về lý thuyết, chúng ta có thể sử dụng động cơ SQLite tích hợp để lưu trữ bộ nhớ đệm, và sau đó chúng ta sẽ nhận được các chỉ số "miễn phí", nhưng chi tiết hơn về điều này sẽ được nói sau.
Ý nghĩa của việc lập chỉ mục dễ hiểu nếu chúng ta tưởng tượng cách triển khai hiệu quả các hàm lịch tiêu chuẩn tương tự trong bộ nhớ đệm của chúng ta. Ví dụ, ID sự kiện được truyền trong hàm CalendarValueById
. Việc liệt kê trực tiếp các bản ghi trong mảng values
sẽ rất tốn thời gian. Do đó, cần bổ sung mảng bằng một "cấu trúc dữ liệu" nào đó để tối ưu hóa việc tìm kiếm. "Cấu trúc dữ liệu" được đặt trong dấu ngoặc kép, vì nó không liên quan đến ý nghĩa của ngôn ngữ lập trình (struct
), mà nói chung là về kiến trúc xây dựng dữ liệu. Nó có thể bao gồm các phần khác nhau và dựa trên các nguyên tắc tổ chức khác nhau. Tất nhiên, dữ liệu bổ sung sẽ yêu cầu bộ nhớ, nhưng việc đổi bộ nhớ lấy tốc độ là một cách tiếp cận phổ biến trong lập trình.
Giải pháp đơn giản nhất cho việc lập chỉ mục là một mảng hai chiều riêng biệt, được sắp xếp theo thứ tự tăng dần để có thể tìm kiếm nhanh bằng hàm ArrayBsearch
. Hai phần tử là đủ cho chiều thứ hai: các giá trị với chỉ số [i][0]
, được dùng để sắp xếp, chứa các định danh, và các giá trị [i][1]
chứa vị trí thứ tự trong mảng cấu trúc.
Một khái niệm thường được sử dụng khác là hashing
, tức là biến đổi các giá trị ban đầu thành một số khóa (hash, số nguyên) sao cho cung cấp số lượng va chạm tối thiểu (khớp khóa cho các dữ liệu ban đầu khác nhau). Đặc tính cơ bản của khóa là phân phối ngẫu nhiên gần đồng đều các giá trị của chúng, nhờ đó chúng có thể được dùng làm chỉ số trong các mảng đã phân bổ trước. Việc tính toán hàm hash cho một phần tử duy nhất của dữ liệu gốc là một quá trình nhanh chóng, thực tế mang lại địa chỉ của chính phần tử đó. Ví dụ, các cấu trúc dữ liệu hash map nổi tiếng tuân theo nguyên tắc này.
Nếu hai giá trị ban đầu nhận cùng một hash (dù điều này hiếm), chúng được xếp thành danh sách cho khóa của chúng, và tìm kiếm tuần tự sẽ được thực hiện trong danh sách. Tuy nhiên, vì các hàm hash được chọn sao cho số lượng khớp là nhỏ, việc tìm kiếm thường đạt mục tiêu ngay khi hash được tính toán.
Để minh họa, chúng ta sẽ sử dụng cả hai phương pháp trong lớp CalendarCache
: hashing và tìm kiếm nhị phân.
Gói MetaTrader 5 bao gồm một tập hợp các lớp để tạo hash map (MQL5/Include/Generic/HashMap.mqh), nhưng chúng ta sẽ tự quản lý với một triển khai đơn giản hơn, trong đó chỉ giữ lại nguyên tắc sử dụng hàm hash.
Sơ đồ lập chỉ mục dữ liệu bằng hashing
Trong trường hợp của chúng ta, chỉ cần hash các định danh của các đối tượng lịch. Hàm hash mà chúng ta chọn sẽ phải chuyển đổi định danh thành một chỉ số bên trong một mảng đặc biệt: vị trí của định danh trong mảng cấu trúc "lịch" sẽ được lưu trong ô có chỉ số này. Đối với các quốc gia, loại sự kiện và tin tức cụ thể, nó được phân bổ theo mảng riêng của mình.
int id4country[];
int id4event[];
int id4value[];
2
3
Các phần tử của chúng sẽ lưu số thứ tự của bản ghi trong mảng tương ứng (countries
, events
, values
).
Đối với mỗi mảng "chuyển hướng", nên phân bổ ít nhất gấp đôi số phần tử so với số cấu trúc tương ứng trong cơ sở dữ liệu (và trong bộ nhớ đệm) của lịch. Nhờ sự dư thừa này, chúng ta giảm thiểu số lượng va chạm hash. Người ta tin rằng hiệu quả lớn nhất đạt được khi chọn kích thước bằng một số nguyên tố. Do đó, lớp có một phương thức tĩnh size2prime
trả về kích thước khuyến nghị của mảng "giỏ" hash (một trong các mảng id4
) theo số lượng phần tử trong dữ liệu nguồn.
static int size2prime(const int size)
{
static int primes[] =
{
17, 53, 97, 193, 389,
769, 1543, 3079, 6151,
12289, 24593, 49157, 98317,
196613, 393241, 786433, 1572869,
3145739, 6291469, 12582917, 25165843,
50331653, 100663319, 201326611, 402653189,
805306457, 1610612741
};
const int pmax = ArraySize(primes);
for(int p = 0; p < pmax; ++p)
{
if(primes[p] >= 2 * size)
{
return primes[p];
}
}
return size;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Toàn bộ quá trình hashing lịch được mô tả trong phương thức hash
. Hãy xem phần đầu của nó bằng ví dụ về mảng cấu trúc countries
, và hai mảng còn lại được xử lý tương tự.
Vậy chúng ta lấy kích thước chỉ số "thuần" khuyến nghị id4country
từ kích thước của mảng countries
bằng cách gọi size2prime
. Ban đầu, mảng chỉ số được điền bằng giá trị -1, tức là tất cả các phần tử của nó đều trống. Tiếp theo trong vòng lặp qua các quốc gia, cần tính hash cho mỗi định danh quốc gia tiếp theo và sử dụng nó để tìm một chỉ số trống trong mảng id4country
. Đây là công việc của phương thức trợ giúp place
.
bool hash()
{
Print("Hashing calendar...");
...
const int c = PRTF(ArraySize(countries));
PRTF(ArrayResize(id4country, size2prime(c)));
ArrayInitialize(id4country, -1);
for(int i = 0; i < c; ++i)
{
if(place(countries[i].id, i, id4country) == -1)
{
return false; // thất bại
}
}
...
return true; // thành công
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Hàm hash bên trong place
là biểu thức (MathSwap(id) ^ 0xEFCDAB8967452301) % n
, trong đó id
là định danh của chúng ta, và n
là kích thước của mảng chỉ số. Do đó, kết quả tính toán luôn được giảm xuống một chỉ số hợp lệ bên trong array[]
. Nguyên tắc chọn hàm hash là một chủ đề riêng biệt vượt ra ngoài phạm vi của cuốn sách.
int place(const ulong id, const int index, int &array[])
{
const int n = ArraySize(array);
int p = (int)((MathSwap(id) ^ 0xEFCDAB8967452301) % n); // hàm hash
int attempt = 0;
while(array[p] != -1)
{
if(++attempt > n / 10) // số va chạm - không quá 1/10 số lượng
{
return -1; // lỗi ghi vào mảng chỉ số
}
p = (p + attempt) % n;
}
array[p] = index;
return p;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Nếu ô tại vị trí p
trong mảng chỉ số chưa được chiếm (bằng -1), chúng ta ngay lập tức ghi địa chỉ vị trí của cấu trúc lịch vào phần tử [p]
. Nếu ô đã bị chiếm, chúng ta cố gắng chọn ô tiếp theo bằng công thức p = (p + attempt) % n
, trong đó attempt
là bộ đếm số lần thử (đây là phiên bản ngụy trang của danh sách các phần tử có hash khớp). Nếu số lần thử thất bại đạt một phần mười dữ liệu ban đầu, việc lập chỉ mục sẽ thất bại, nhưng điều này hầu như không thể xảy ra với kích thước mảng chỉ số quá lớn của chúng ta và bản chất đã biết của dữ liệu được hash (các định danh duy nhất).
Kết quả của việc hash mảng cấu trúc, chúng ta nhận được một mảng chỉ số đã điền (có các khoảng trống trong đó, nhưng điều này là cố ý), qua đó chúng ta có thể tìm vị trí của cấu trúc tương ứng trong mảng cấu trúc bằng định danh của phần tử lịch. Điều này được thực hiện bởi phương thức find
, ngược nghĩa với place
.
template<typename S>
int find(const ulong id, const int &array[], const S &structs[])
{
const int n = ArraySize(array);
if(!n) return false;
int p = (int)((MathSwap(id) ^ 0xEFCDAB8967452301) % n); // hàm hash
int attempt = 0;
while(structs[array[p]].id != id)
{
if(++attempt > n / 10)
{
return -1; // lỗi trích xuất từ mảng chỉ số
}
p = (p + attempt) % n;
}
return array[p];
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Hãy chỉ ra cách nó được sử dụng trong thực tế. Các hàm lịch tiêu chuẩn bao gồm CalendarCountryById
và CalendarEventById
. Khi bạn cần kiểm tra một chương trình MQL trong bộ kiểm tra, nó sẽ không thể truy cập trực tiếp chúng, nhưng nó sẽ có thể tải bộ nhớ đệm lịch vào đối tượng CalendarCache
và do đó nó nên có các phương thức tương tự.
bool calendarCountryById(ulong country_id, MqlCalendarCountry &cnt)
{
const int index = find(country_id, id4country, countries);
if(index == -1) return false;
cnt = countries[index];
return true;
}
bool calendarEventById(ulong event_id, MqlCalendarEvent &event)
{
const int index = find(event_id, id4event, events);
if(index == -1) return false;
event = events[index];
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Chúng sử dụng phương thức find
và các mảng chỉ số id4country
và id4event
.
Nhưng đây không phải là các tính năng mong muốn nhất của lịch. Thường xuyên hơn, một chương trình MQL với chiến lược tin tức cần các hàm CalendarValueHistory
, CalendarValueHistoryByEvent
, CalendarValueLast
, hoặc CalendarValueLastByEvent
. Chúng cung cấp truy cập nhanh vào các mục lịch theo thời gian, quốc gia hoặc tiền tệ.
Vì vậy, lớp CalendarCache
nên cung cấp các phương thức tương tự. Ở đây chúng ta sẽ sử dụng phương pháp thứ hai của "lập chỉ mục" — thông qua tìm kiếm nhị phân trong một mảng đã sắp xếp.
Để triển khai các phương thức trên, hãy thêm 4 mảng hai chiều nữa vào lớp để thiết lập mối tương ứng giữa tin tức và loại sự kiện, tin tức và quốc gia, tin tức và tiền tệ, cũng như tin tức và thời gian công bố của nó.
ulong value2event[][2]; // [0] - event_id, [1] - value_id
ulong value2country[][2]; // [0] - country_id, [1] - value_id
ulong value2currency[][2]; // [0] - currency ushort[4]<->long, [1] - value_id
ulong value2time[][2]; // [0] - time, [1] - value_id
2
3
4
Trong phần tử đầu tiên của mỗi hàng, tức là dưới các chỉ số [i][0]
, sẽ ghi lại ID sự kiện, quốc gia, tiền tệ hoặc thời gian tương ứng. Trong phần tử thứ hai của hàng, dưới các chỉ số [i][1]
, sẽ đặt các ID của tin tức cụ thể. Sau khi điền tất cả các mảng một lần, chúng được sắp xếp bằng ArraySort
theo các giá trị [i][0]
. Sau đó, chúng ta có thể tìm kiếm theo ID, ví dụ, theo event_id
, cho tất cả tin tức như vậy trong mảng value2event
: hàm ArrayBsearch
sẽ trả về số của phần tử khớp đầu tiên, tiếp theo là các phần tử khác có cùng event_id
cho đến khi gặp một định danh khác. Thứ tự trong "cột" thứ hai không được xác định (có thể là bất kỳ).
Tìm kiếm nhanh các cấu trúc liên quan dựa trên sắp xếp
Thao tác liên kết lẫn nhau các cấu trúc của các loại khác nhau được thực hiện trong phương thức bind
. Kích thước của mỗi mảng "liên kết" bằng kích thước của mảng tin tức. Duyệt qua tất cả tin tức trong một vòng lặp, chúng ta sử dụng các mảng chỉ số đã sẵn sàng và phương thức find
để định địa chỉ nhanh.
bool bind()
{
Print("Binding calendar tables...");
const int n = ArraySize(values);
ArrayResize(value2event, n);
ArrayResize(value2country, n);
ArrayResize(value2currency, n);
ArrayResize(value2time, n);
for(int i = 0; i < n; ++i)
{
value2event[i][0] = values[i].event_id;
value2event[i][1] = values[i].id;
const int e = find(values[i].event_id, id4event, events);
if(e == -1) return false;
value2country[i][0] = events[e].country_id;
value2country[i][1] = values[i].id;
const int c = find(events[e].country_id, id4country, countries);
if(c == -1) return false;
value2currency[i][0] = currencyId(countries[c].currency);
value2currency[i][1] = values[i].id;
value2time[i][0] = values[i].time;
value2time[i][1] = values[i].id;
}
ArraySort(value2event);
ArraySort(value2country);
ArraySort(value2currency);
ArraySort(value2time);
return true;
}
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
Trong trường hợp tiền tệ, một số đặc biệt thu được từ chuỗi bằng hàm currencyId
được lấy làm định danh.
static ulong currencyId(const string s)
{
union CRNC4
{
ushort word[4];
ulong ul;
} v;
StringToShortArray(s, v.word);
return v.ul;
}
2
3
4
5
6
7
8
9
10
Bây giờ chúng ta cuối cùng có thể trình bày toàn bộ hàm tạo của lớp CalendarCache
.
CalendarCache(const string _context = NULL,
const datetime _from = 0, const datetime _to = 0):
context(_context), from(_from), to(_to), t(0), eventId(0)
{
if(from > to) // nhãn rằng context là tên tệp
{
load(_context);
}
else
{
if(!update() || !hash() || !bind())
{
t = 0;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Khi chạy trên biểu đồ trực tuyến, đối tượng được tạo với các tham số mặc định sẽ thu thập tất cả thông tin lịch (update
), lập chỉ mục nó (hash
), và liên kết các bảng (bind
). Nếu có gì đó sai sót ở bất kỳ giai đoạn nào, dấu hiệu lỗi sẽ là 0 trong biến t
. Nếu thành công, giá trị từ hàm TimeTradeServer
sẽ được giữ lại đó (nhớ rằng, nó được đặt bên trong update
). Một đối tượng sẵn sàng sử dụng như vậy có thể được xuất ra tệp bằng phương thức save
đã mô tả ở trên.
Khi chạy trong bộ kiểm tra, đối tượng nên được tạo với một tổ hợp tham số đặc biệt from
và to
(from > to
) — trong trường hợp này, chương trình sẽ coi chuỗi context
là tên tệp và sẽ tải trạng thái lịch từ đó. Cách dễ nhất để làm điều này là:
CalendarCache calca("filename.cal", true);
Bên trong phương thức load
, chúng ta cũng sẽ gọi hash
và bind
để đưa đối tượng vào trạng thái hoạt động.
bool load(const string filename)
{
... // đọc tệp đã được hiển thị trước đó
const bool result = hash() && bind();
if(!result) t = 0;
return result;
}
2
3
4
5
6
7
Dùng hàm CalendarValueLast
làm ví dụ, chúng ta trình bày một triển khai tương đương của phương thức calendarValueLast
(với nguyên mẫu hoàn toàn giống nhau). Bộ nhớ đệm sẽ sử dụng thời gian "máy chủ" hiện tại làm định danh thay đổi, trong trường hợp không có API phần mềm mở để đọc bảng thay đổi lịch trực tuyến. Giả thuyết, chúng ta có thể sử dụng thông tin về các ID thay đổi được lưu bởi dịch vụ CalendarChangeSaver.mq5
, nhưng cách tiếp cận này đòi hỏi thu thập thống kê dài hạn trước khi có thể bắt đầu kiểm tra. Do đó, thời gian "máy chủ" do bộ kiểm tra tạo ra được chấp nhận là một sự thay thế khá phù hợp.
Khi chương trình MQL yêu cầu thay đổi lần đầu tiên với định danh null, chúng ta chỉ đơn giản trả về giá trị từ TimeTradeServer
.
int calendarValueLast(ulong &change, MqlCalendarValue &result[],
const string code = NULL, const string currency = NULL)
{
if(!change)
{
change = TimeTradeServer();
return 0;
}
...
2
3
4
5
6
7
8
9
Nếu định danh thay đổi đã khác không, chúng ta tiếp tục nhánh chính của thuật toán.
Tùy thuộc vào nội dung của các tham số code
và currency
, chúng ta tìm các định danh của quốc gia và tiền tệ. Mặc định là 0, nghĩa là tìm kiếm tất cả các thay đổi.
ulong country_id = 0;
ulong currency_id = currency != NULL ? currencyId(currency) : 0;
if(code != NULL)
{
for(int i = 0; i < ArraySize(countries); ++i)
{
if(countries[i].code == code)
{
country_id = countries[i].id;
break;
}
}
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Tiếp theo, sử dụng số thời gian truyền vào change
làm điểm bắt đầu tìm kiếm, chúng ta tìm tất cả các tin tức trong value2time
cho đến giá trị hiện tại mới TimeTradeServer
. Bên trong vòng lặp, chúng ta sử dụng phương thức find
để tìm chỉ số của cấu trúc MqlCalendarValue
tương ứng trong mảng values
và, nếu cần, so sánh quốc gia và tiền tệ của loại sự kiện liên quan với các giá trị mong muốn. Tất cả các mục tin tức đáp ứng tiêu chí được ghi vào mảng đầu ra result
.
const ulong past = change;
const int index = ArrayBsearch(value2time, past);
if(index < 0 || index >= ArrayRange(value2time, 0)) return 0;
int i = index;
while(value2time[i][0] <= (ulong)past && i < ArrayRange(value2time, 0)) ++i;
if(i >= ArrayRange(value2time, 0)) return 0;
for(int j = i; j < ArrayRange(value2time, 0)
&& value2time[j][0] <= (ulong)TimeTradeServer(); ++j)
{
const int p = find(value2time[j][1], id4value, values);
if(p != -1)
{
change = TimeTradeServer();
if(country_id != 0 || currency_id != 0)
{
const int q = find(values[p].event_id, id4event, events);
if(country_id != 0 && country_id != events[q].country_id) continue;
if(currency_id != 0)
{
const int m = find(events[q].country_id, id4country, countries);
if(countries[m].currency != currency) continue;
}
}
PUSH(result, values[p]);
}
}
return ArraySize(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
Các phương thức calendarValueHistory
, calendarValueHistoryByEvent
, và calendarValueLastByEvent
được triển khai theo nguyên tắc tương tự (phương thức cuối thực tế giao toàn bộ công việc cho phương thức calendarValueLast
đã thảo luận trước đó). Mã nguồn hoàn chỉnh có thể được tìm thấy trong tệp đính kèm CalendarCache.mqh
.
Dựa trên lớp bộ nhớ đệm, việc tạo một lớp dẫn xuất CalendarFilter
là hợp lý, lớp này khi xử lý các yêu cầu sẽ truy cập vào bộ nhớ đệm thay vì lịch.
Giải pháp hoàn chỉnh nằm trong tệp CalendarFilterCached.mqh
. Do API bộ nhớ đệm được thiết kế dựa trên API tiêu chuẩn, việc tích hợp chỉ đơn giản là chuyển tiếp các cuộc gọi bộ lọc đến đối tượng bộ nhớ đệm (con trỏ tự động cache
).
class CalendarFilterCached: public CalendarFilter
{
protected:
AutoPtr<CalendarCache> cache;
virtual bool calendarCountryById(ulong country_id, MqlCalendarCountry &cnt) override
{
return cache[].calendarCountryById(country_id, cnt);
}
virtual bool calendarEventById(ulong event_id, MqlCalendarEvent &event) override
{
return cache[].calendarEventById(event_id, event);
}
virtual int calendarValueHistoryByEvent(ulong event_id, MqlCalendarValue &temp[],
datetime _from, datetime _to = 0) override
{
return cache[].calendarValueHistoryByEvent(event_id, temp, _from, _to);
}
virtual int calendarValueHistory(MqlCalendarValue &temp[],
datetime _from, datetime _to = 0,
const string _code = NULL, const string _coin = NULL) override
{
return cache[].calendarValueHistory(temp, _from, _to, _code, _coin);
}
virtual int calendarValueLast(ulong &_change, MqlCalendarValue &result[],
const string _code = NULL, const string _coin = NULL) override
{
return cache[].calendarValueLast(_change, result, _code, _coin);
}
virtual int calendarValueLastByEvent(ulong event_id, ulong &_change,
MqlCalendarValue &result[]) override
{
return cache[].calendarValueLastByEvent(event_id, _change, result);
}
public:
CalendarFilterCached(CalendarCache *_cache): cache(_cache),
CalendarFilter(_cache.getContext(), _cache.getFrom(), _cache.getTo())
{
}
virtual bool isLoaded() const override
{
// sự sẵn sàng được xác định bởi bộ nhớ đệm
return cache[].isLoaded();
}
};
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
Để kiểm tra lịch trong bộ kiểm tra, hãy tạo một phiên bản mới của chỉ báo CalendarMonitor.mq5
– CalendarMonitorCached.mq5
.
Những khác biệt chính bao gồm như sau.
Chúng ta giả định rằng một tệp bộ nhớ đệm nào đó sẽ được tạo hoặc đã được tạo dưới tên "xyz.cal" (trong thư mục MQL5/Files
) và do đó kết nối nó với chương trình MQL bằng chỉ thị tester_file
.
#property tester_file "xyz.cal"
Chỉ thị này đảm bảo việc chuyển bộ nhớ đệm đến bất kỳ tác nhân nào, bao gồm cả các tác nhân phân tán (tuy nhiên, điều này phù hợp hơn với Expert Advisors hơn là một chỉ báo). Một tệp bộ nhớ đệm với tên này (hoặc tên khác) có thể được tạo bằng biến đầu vào mới CalendarCacheFile
. Nếu người dùng thay đổi tên mặc định thành một tên khác, để hoạt động trong bộ kiểm tra, bạn sẽ cần sửa chỉ thị (yêu cầu biên dịch lại!), hoặc chuyển tệp đến thư mục chung của các terminal (tính năng này được hỗ trợ trong lớp bộ nhớ đệm, nhưng "để lại phía sau"), tuy nhiên, tệp như vậy không còn khả dụng cho các tác nhân từ xa.
input string CalendarCacheFile = "xyz.cal";
Đối tượng CalendarFilter
giờ được mô tả như một con trỏ tự động, vì tùy thuộc vào nơi chỉ báo được chạy, nó có thể sử dụng lớp gốc CalendarFilter
cũng như lớp dẫn xuất CalendarFilterCached
.
AutoPtr<CalendarFilter> fptr;
AutoPtr<CalendarCache> cache;
2
Ở đầu OnInit
, có một đoạn mã mới chịu trách nhiệm tạo và đọc bộ nhớ đệm.
int OnInit()
{
cache = new CalendarCache(CalendarCacheFile, true);
if(cache[].isLoaded())
{
fptr = new CalendarFilterCached(cache[]);
}
else
{
if(MQLInfoInteger(MQL_TESTER))
{
Print("Can't run in the tester without calendar cache file");
return INIT_FAILED;
}
else
if(StringLen(CalendarCacheFile))
{
Alert("Calendar cache not found, trying to create '" + CalendarCacheFile + "'");
cache = new CalendarCache();
if(cache[].save(CalendarCacheFile))
{
Alert("File saved. Re-run indicator in online chart or in the tester");
}
else
{
Alert("Error: ", _LastError);
}
ChartIndicatorDelete(0, 0, MQLInfoString(MQL_PROGRAM_NAME));
return INIT_PARAMETERS_INCORRECT;
}
Alert("Currently working in online mode (no cache)");
fptr = new CalendarFilter(Context);
}
CalendarFilter *f = fptr[];
... // tiếp tục mà không thay đổi
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
Nếu tệp bộ nhớ đệm đã được đọc, chúng ta sẽ nhận được đối tượng hoàn chỉnh CalendarCache
, được truyền vào hàm tạo CalendarFilterCached
. Nếu không, chương trình kiểm tra xem nó đang chạy trong bộ kiểm tra hay trực tuyến. Việc không có bộ nhớ đệm trong bộ kiểm tra là một trường hợp nghiêm trọng. Trên biểu đồ thông thường, chương trình tạo một đối tượng mới dựa trên dữ liệu lịch tích hợp và lưu nó vào bộ nhớ đệm dưới tên đã chỉ định. Nhưng nếu tên tệp được để trống, chỉ báo sẽ hoạt động giống như phiên bản gốc — trực tiếp với lịch.
Hãy chạy chỉ báo trên biểu đồ EURUSD. Người dùng sẽ được cảnh báo rằng tệp đã chỉ định không được tìm thấy và đã cố gắng lưu nó. Với điều kiện lịch được bật trong cài đặt terminal, chúng ta sẽ nhận được khoảng các dòng sau trong nhật ký. Dưới đây là phiên bản với thông tin chẩn đoán chi tiết. Chi tiết có thể bị tắt bằng cách bình luận chỉ thị trong mã nguồn #define LOGGING
.
Loading calendar cache xyz.cal
FileOpen(filename,FILE_READ|FILE_BIN|flags)=-1 / CANNOT_OPEN_FILE(5004)
Alert: Calendar cache not found, trying to create 'xyz.cal'
Reading online calendar base...
CalendarValueHistory(values,from,to,country,currency)=157173 / ok
CalendarEventByCountry(country,events)=1493 / ok
CalendarCountries(countries)=23 / ok
Hashing calendar...
ArraySize(countries)=23 / ok
ArrayResize(id4country,size2prime(c))=53 / ok
Total collisions: 9, worse:3, average: 2.25 in 4
ArraySize(events)=1493 / ok
ArrayResize(id4event,size2prime(e))=3079 / ok
Total collisions: 495, worse:7, average: 1.43478 in 345
ArraySize(values)=157173 / ok
ArrayResize(id4value,size2prime(v))=393241 / ok
Total collisions: 3511, worse:1, average: 1.0 in 3511
Binding calendar tables...
FileOpen(filename,FILE_WRITE|FILE_BIN|flags)=1 / ok
Alert: File saved. Re-run indicator in online chart or in the tester
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Giờ chúng ta có thể chọn chỉ báo CalendarMonitorCached.mq5
trong bộ kiểm tra và xem theo động lực, dựa trên lịch sử, cách bảng tin tức thay đổi.
Chỉ báo tin tức với bộ nhớ đệm lịch trong bộ kiểm tra
Sự hiện diện của bộ nhớ đệm lịch cho phép bạn kiểm tra các chiến lược giao dịch dựa trên tin tức. Chúng ta sẽ trình bày điều này trong phần tiếp theo.