Thêm, thay thế và xóa báo giá
Một ký hiệu tùy chỉnh được điền báo giá bởi hai hàm tích hợp sẵn: CustomRatesUpdate
và CustomRatesReplace
. Ở đầu vào, ngoài tên của ký hiệu, cả hai đều mong đợi một mảng cấu trúc MqlRates
cho khung thời gian M1 (các khung thời gian cao hơn được hoàn thành tự động từ M1). CustomRatesReplace
có thêm một cặp tham số (from
và to
) xác định phạm vi thời gian mà việc chỉnh sửa lịch sử bị giới hạn.
int CustomRatesUpdate(const string symbol, const MqlRates &rates[], uint count = WHOLE_ARRAY)
int CustomRatesReplace(const string symbol, datetime from, datetime to, const MqlRates &rates[], uint count = WHOLE_ARRAY)
CustomRatesUpdate
thêm các thanh còn thiếu vào lịch sử và thay thế các thanh hiện có khớp với dữ liệu từ mảng.
CustomRatesReplace
thay thế hoàn toàn lịch sử trong khoảng thời gian được chỉ định bằng dữ liệu từ mảng.
Sự khác biệt giữa các hàm là do các kịch bản ứng dụng dự kiến khác nhau. Các khác biệt được liệt kê chi tiết hơn trong bảng sau.
CustomRatesUpdate | CustomRatesReplace |
---|---|
Áp dụng các phần tử của mảng MqlRates được truyền vào lịch sử, bất kể dấu thời gian của chúng | Chỉ áp dụng những phần tử của mảng MqlRates được truyền vào nằm trong phạm vi được chỉ định |
Để lại nguyên trong lịch sử những thanh M1 đã có trước khi gọi hàm và không trùng thời gian với các thanh trong mảng | Để lại nguyên tất cả lịch sử ngoài phạm vi |
Thay thế các thanh lịch sử hiện có bằng các thanh từ mảng khi dấu thời gian khớp | Xóa hoàn toàn các thanh lịch sử hiện có trong phạm vi được chỉ định |
Chèn các phần tử từ mảng dưới dạng các thanh "mới" nếu không có sự khớp với các thanh cũ | Chèn các thanh từ mảng rơi vào phạm vi liên quan vào phạm vi lịch sử được chỉ định |
Dữ liệu trong mảng rates
phải được biểu diễn bằng giá OHLC hợp lệ, và thời gian mở thanh không được chứa giây.
Một khoảng trong from
và to
được đặt bao gồm: from
bằng với thời gian của thanh đầu tiên được xử lý và to
bằng với thời gian của thanh cuối cùng.
Sơ đồ sau minh họa các quy tắc này rõ ràng hơn. Mỗi dấu thời gian duy nhất cho một thanh được chỉ định bằng chữ cái Latinh riêng của nó. Các thanh có sẵn trong lịch sử được hiển thị bằng chữ cái in hoa, trong khi các thanh trong mảng được hiển thị bằng chữ cái thường. Ký tự '-' là khoảng trống trong lịch sử hoặc trong mảng cho thời gian tương ứng.
History ABC-EFGHIJKLMN-PQRST------ B
Array -------hijk--nopqrstuvwxyz A
Result of CustomRatesUpdate ABC-EFGhijkLMnopqrstuvwxyz R
Result of CustomRatesReplace ABC-E--hijk--nopqrstuvw--- S
^ ^
|from to| TIME
2
3
4
5
6
Tham số tùy chọn count
thiết lập số lượng phần tử trong mảng rates
nên được sử dụng (các phần tử khác sẽ bị bỏ qua). Điều này cho phép bạn xử lý một phần mảng được truyền vào. Giá trị mặc định WHOLE_ARRAY có nghĩa là toàn bộ mảng.
Lịch sử báo giá của một ký hiệu tùy chỉnh có thể được xóa hoàn toàn hoặc một phần bằng hàm CustomRatesDelete
.
int CustomRatesDelete(const string symbol, datetime from, datetime to)
Ở đây, các tham số from
và to
cũng thiết lập phạm vi thời gian của các thanh bị xóa. Để bao phủ toàn bộ lịch sử, chỉ định 0 và LONG_MAX.
Cả ba hàm trả về số lượng thanh được xử lý: cập nhật hoặc xóa. Trong trường hợp lỗi, kết quả là -1.
Cần lưu ý rằng báo giá của một ký hiệu tùy chỉnh không chỉ có thể được hình thành bằng cách thêm các thanh đã sẵn sàng mà còn bằng các mảng tick hoặc thậm chí một chuỗi các tick riêng lẻ. Các hàm liên quan sẽ được trình bày trong phần tiếp theo. Khi thêm tick, terminal sẽ tự động tính toán các thanh dựa trên chúng. Sự khác biệt giữa các phương pháp này là lịch sử tick tùy chỉnh cho phép bạn kiểm tra các chương trình MQL trong chế độ tick "thực", trong khi lịch sử chỉ có thanh sẽ buộc bạn phải giới hạn ở OHLC M1 hoặc chế độ giá mở hoặc dựa vào mô phỏng tick được triển khai bởi trình kiểm tra.
Ngoài ra, việc thêm tick từng cái một cho phép bạn mô phỏng các sự kiện tiêu chuẩn OnTick
và OnCalculate
trên biểu đồ của một ký hiệu tùy chỉnh, điều này "làm sống động" biểu đồ tương tự như các công cụ có sẵn trực tuyến, và khởi chạy các hàm xử lý tương ứng trong các chương trình MQL nếu chúng được vẽ trên biểu đồ. Nhưng chúng ta sẽ nói về điều này trong phần tiếp theo.
Ví dụ về việc sử dụng các hàm mới, hãy xem xét tập lệnh CustomSymbolRandomRates.mq5
. Nó được thiết kế để tạo báo giá ngẫu nhiên theo nguyên tắc "random walk" hoặc nhiễu báo giá hiện có. Cách sau có thể được sử dụng để đánh giá độ ổn định của một Expert Advisor.
Để kiểm tra tính đúng đắn của việc hình thành báo giá, chúng ta cũng sẽ hỗ trợ chế độ trong đó một bản sao hoàn chỉnh của công cụ ban đầu được tạo, trên biểu đồ mà tập lệnh được khởi chạy.
Tất cả các chế độ được thu thập trong liệt kê RANDOMIZATION.
enum RANDOMIZATION
{
ORIGINAL,
RANDOM_WALK,
FUZZY_WEAK,
FUZZY_STRONG,
};
2
3
4
5
6
7
Chúng ta triển khai nhiễu báo giá với hai mức độ mạnh: yếu và mạnh.
Trong các tham số đầu vào, bạn có thể chọn, ngoài chế độ, một thư mục trong phân cấp ký hiệu, một khoảng ngày, và một số để khởi tạo bộ tạo ngẫu nhiên (để có thể tái tạo kết quả).
input string CustomPath = "MQL5Book\\Part7"; // Thư mục Ký hiệu Tùy chỉnh
input RANDOMIZATION RandomFactor = RANDOM_WALK;
input datetime _From; // Từ (mặc định: 120 ngày trước)
input datetime _To; // Đến (mặc định: thời gian hiện tại)
input uint RandomSeed = 0;
2
3
4
5
Theo mặc định, khi không có ngày nào được chỉ định, tập lệnh tạo báo giá cho 120 ngày qua. Giá trị 0 trong tham số RandomSeed
có nghĩa là khởi tạo ngẫu nhiên.
Tên của ký hiệu được tạo dựa trên ký hiệu của biểu đồ hiện tại và các cài đặt đã chọn.
const string CustomSymbol = _Symbol + "." + EnumToString(RandomFactor)
+ (RandomSeed ? "_" + (string)RandomSeed : "");
2
Tại đầu của OnStart
, chúng ta sẽ chuẩn bị và kiểm tra dữ liệu.
datetime From;
datetime To;
void OnStart()
{
From = _From == 0 ? TimeCurrent() - 60 * 60 * 24 * 120 : _From;
To = _To == 0 ? TimeCurrent() / 60 * 60 : _To;
if(From > To)
{
Alert("Date range must include From <= To");
return;
}
if(RandomSeed != 0) MathSrand(RandomSeed);
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Vì tập lệnh có thể sẽ cần chạy nhiều lần, chúng ta sẽ cung cấp khả năng xóa ký hiệu tùy chỉnh đã tạo trước đó, với yêu cầu xác nhận trước từ người dùng.
bool custom = false;
if(PRTF(SymbolExist(CustomSymbol, custom)) && custom)
{
if(IDYES == MessageBox(StringFormat("Delete custom symbol '%s'?", CustomSymbol),
"Please, confirm", MB_YESNO))
{
if(CloseChartsForSymbol(CustomSymbol))
{
Sleep(500); // chờ các thay đổi có hiệu lực (theo cơ hội)
PRTF(CustomRatesDelete(CustomSymbol, 0, LONG_MAX));
PRTF(SymbolSelect(CustomSymbol, false));
PRTF(CustomSymbolDelete(CustomSymbol));
}
}
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Hàm trợ giúp CloseChartsForSymbol
không được hiển thị ở đây (những ai muốn có thể xem mã nguồn đính kèm): mục đích của nó là xem danh sách các biểu đồ đang mở và đóng những biểu đồ nơi ký hiệu đang hoạt động là ký hiệu tùy chỉnh đang bị xóa (nếu không có điều này, việc xóa sẽ không hoạt động).
Quan trọng hơn là cần chú ý đến việc gọi CustomRatesDelete
với toàn bộ phạm vi ngày. Nếu không thực hiện điều này, dữ liệu của ký hiệu người dùng trước đó sẽ vẫn còn trên đĩa một thời gian trong cơ sở dữ liệu lịch sử (thư mục bases/Custom/history/<symbol-name>
). Nói cách khác, việc gọi CustomSymbolDelete
, được hiển thị ở dòng cuối cùng phía trên, là không đủ để thực sự xóa ký hiệu tùy chỉnh khỏi terminal.
Nếu người dùng quyết định ngay lập tức tạo lại một ký hiệu với cùng tên (và chúng ta cung cấp khả năng này trong mã dưới đây), thì các báo giá cũ có thể bị trộn lẫn vào các báo giá mới.
Tiếp theo, khi có xác nhận của người dùng, quá trình tạo báo giá được khởi chạy. Điều này được thực hiện bởi hàm GenerateQuotes
(xem thêm).
if(IDYES == MessageBox(StringFormat("Create new custom symbol '%s'?", CustomSymbol),
"Please, confirm", MB_YESNO))
{
if(PRTF(CustomSymbolCreate(CustomSymbol, CustomPath, _Symbol)))
{
if(RandomFactor == RANDOM_WALK)
{
CustomSymbolSetInteger(CustomSymbol, SYMBOL_DIGITS, 8);
}
CustomSymbolSetString(CustomSymbol, SYMBOL_DESCRIPTION, "Randomized quotes");
const int n = GenerateQuotes();
Print("Bars M1 generated: ", n);
if(n > 0)
{
SymbolSelect(CustomSymbol, true);
ChartOpen(CustomSymbol, PERIOD_M1);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Nếu thành công, ký hiệu mới được tạo sẽ được chọn trong Market Watch
và một biểu đồ sẽ mở ra cho nó. Đồng thời, việc thiết lập một cặp thuộc tính được thể hiện ở đây: SYMBOL_DIGITS và SYMBOL_DESCRIPTION.
Trong hàm GenerateQuotes
, cần yêu cầu báo giá của ký hiệu gốc cho tất cả các chế độ ngoại trừ RANDOM_WALK.
int GenerateQuotes()
{
MqlRates rates[];
MqlRates zero = {};
datetime start; // thời gian của thanh hiện tại
double price; // giá đóng cửa cuối cùng
if(RandomFactor != RANDOM_WALK)
{
if(PRTF(CopyRates(_Symbol, PERIOD_M1, From, To, rates)) <= 0)
{
return 0; // lỗi
}
if(RandomFactor == ORIGINAL)
{
return PRTF(CustomRatesReplace(CustomSymbol, From, To, rates));
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Điều quan trọng cần nhớ là CopyRates
bị ảnh hưởng bởi giới hạn số lượng thanh trên biểu đồ, được thiết lập trong cài đặt terminal.
Trong trường hợp chế độ ORIGINAL, chúng ta chỉ cần chuyển mảng kết quả rates
vào hàm CustomRatesReplace
. Đối với các chế độ nhiễu, chúng ta thiết lập các biến được chọn đặc biệt price
và start
thành các giá trị ban đầu của giá và thời gian từ thanh đầu tiên.
price = rates[0].open;
start = rates[0].time;
}
...
2
3
4
Trong chế độ random walk, báo giá không cần thiết, vì vậy chúng ta chỉ phân bổ mảng rates
cho các thanh M1 ngẫu nhiên trong tương lai.
else
{
ArrayResize(rates, (int)((To - From) / 60) + 1);
price = 1.0;
start = From;
}
...
2
3
4
5
6
7
Tiếp theo trong vòng lặp qua mảng rates
, các giá trị ngẫu nhiên được thêm vào hoặc là các giá nhiễu của ký hiệu gốc hoặc "nguyên trạng". Trong chế độ RANDOM_WALK, chúng ta tự chịu trách nhiệm tăng thời gian trong biến start
. Ở các chế độ khác, thời gian đã có trong báo giá ban đầu.
const int size = ArraySize(rates);
double hlc[3]; // High Low Close tương lai (theo thứ tự không xác định)
for(int i = 0; i < size; ++i)
{
if(RandomFactor == RANDOM_WALK)
{
rates[i] = zero; // đặt lại cấu trúc về 0
rates[i].time = start += 60; // cộng một phút vào thanh cuối cùng
rates[i].open = price; // bắt đầu từ giá cuối cùng
hlc[0] = RandomWalk(price);
hlc[1] = RandomWalk(price);
hlc[2] = RandomWalk(price);
}
else
{
double delta = 0;
if(i > 0)
{
delta = rates[i].open - price; // hiệu chỉnh tích lũy
}
rates[i].open = price;
hlc[0] = RandomWalk(rates[i].high - delta);
hlc[1] = RandomWalk(rates[i].low - delta);
hlc[2] = RandomWalk(rates[i].close - delta);
}
ArraySort(hlc);
rates[i].high = fmax(hlc[2], rates[i].open);
rates[i].low = fmin(hlc[0], rates[i].open);
rates[i].close = price = hlc[1];
rates[i].tick_volume = 4;
}
...
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
Dựa trên giá đóng cửa của thanh cuối cùng, 3 giá trị ngẫu nhiên được tạo ra (sử dụng hàm RandomWalk
). Giá trị tối đa và tối thiểu của chúng lần lượt trở thành giá High
và Low
của một thanh mới. Giá trị trung bình là giá Close
.
Ở cuối vòng lặp, chúng ta truyền mảng vào CustomRatesReplace
.
return PRTF(CustomRatesReplace(CustomSymbol, From, To, rates));
}
2
Trong hàm RandomWalk
, đã có một nỗ lực mô phỏng phân phối với đuôi rộng, điều này điển hình cho báo giá thực tế.
double RandomWalk(const double p)
{
const static double factor[] = {0.0, 0.1, 0.01, 0.05};
const static double f = factor[RandomFactor] / 100;
const double r = (rand() - 16383.0) / 16384.0; // [-1,+1]
const int sign = r >= 0 ? +1 : -1;
if(r != 0)
{
return p + p * sign * f * sqrt(-log(sqrt(fabs(r))));
}
return p;
}
2
3
4
5
6
7
8
9
10
11
12
Hệ số phân tán của các biến ngẫu nhiên phụ thuộc vào chế độ. Ví dụ, nhiễu yếu thêm (hoặc trừ) tối đa một phần trăm của phần trăm, và nhiễu mạnh thêm 5 phần trăm của phần trăm giá.
Trong khi chạy, tập lệnh xuất ra một nhật ký chi tiết như thế này:
Create new custom symbol 'GBPUSD.RANDOM_WALK'?
CustomSymbolCreate(CustomSymbol,CustomPath,_Symbol)=true / ok
CustomRatesReplace(CustomSymbol,From,To,rates)=171416 / ok
Bars M1 generated: 171416
2
3
4
Hãy xem kết quả chúng ta nhận được.
Hình ảnh sau cho thấy một số triển khai của random walk (lớp phủ trực quan được thực hiện trong một trình chỉnh sửa đồ họa, trong thực tế, mỗi ký hiệu tùy chỉnh mở trong một cửa sổ riêng như thông thường).
Các tùy chọn báo giá cho ký hiệu tùy chỉnh với random walk
Và đây là cách báo giá GBPUSD nhiễu trông như thế nào (gốc màu đen, màu có nhiễu). Đầu tiên, trong phiên bản yếu.
Báo giá GBPUSD với nhiễu yếu
Và sau đó với nhiễu mạnh.
Báo giá GBPUSD với nhiễu mạnh
Sự khác biệt lớn hơn là rõ ràng, mặc dù vẫn giữ được các đặc điểm cục bộ.