Ví dụ Big Expert Advisor
Để tổng quát hóa và củng cố kiến thức về khả năng của bộ kiểm tra, chúng ta sẽ xem xét từng bước một ví dụ lớn về Expert Advisor. Trong ví dụ này, chúng ta sẽ tóm tắt các khía cạnh sau:
- Sử dụng nhiều symbol, bao gồm việc đồng bộ hóa các thanh (bars)
- Sử dụng một indicator từ Expert Advisor
- Sử dụng các sự kiện
- Tính toán độc lập các thống kê giao dịch chính
- Tính toán tiêu chí tối ưu hóa tùy chỉnh R2 điều chỉnh cho các lô biến đổi
- Gửi và xử lý các frame với dữ liệu ứng dụng (báo cáo giao dịch phân tích theo symbol)
Chúng ta sẽ sử dụng MultiMartingale.mq5
làm nền tảng kỹ thuật cho Expert Advisor, nhưng chúng ta sẽ làm cho nó ít rủi ro hơn bằng cách chuyển sang giao dịch tín hiệu quá mua/quá bán đa tiền tệ và chỉ tăng lô như một tùy chọn bổ sung. Trước đây, trong BandOsMA.mq5
, chúng ta đã thấy cách hoạt động dựa trên tín hiệu giao dịch của indicator. Lần này, chúng ta sẽ sử dụng UseUnityPercentPro.mq5
làm indicator tín hiệu. Tuy nhiên, chúng ta cần sửa đổi nó trước. Chúng ta sẽ gọi phiên bản mới là UnityPercentEvent.mq5
.
UnityPercentEvent.mq5
Hãy nhớ lại bản chất của indicator Unity
. Nó tính toán sức mạnh tương đối của các đồng tiền hoặc ticker được bao gồm trong một tập hợp các công cụ đã cho (giả định rằng tất cả các công cụ có một đồng tiền chung để chuyển đổi). Trên mỗi thanh, các giá trị được hình thành cho tất cả các đồng tiền: một số sẽ đắt hơn, một số rẻ hơn, và hai yếu tố cực đoan nằm ở trạng thái biên. Tiếp theo, có thể xem xét hai chiến lược cơ bản đối lập cho chúng:
- Phá vỡ tiếp theo (xác nhận và tiếp tục chuyển động mạnh sang hai bên)
- Hồi giá (đảo chiều chuyển động về trung tâm do quá mua và quá bán)
Để giao dịch bất kỳ tín hiệu nào trong số này, chúng ta phải tạo ra một symbol hoạt động từ hai đồng tiền (hoặc ticker nói chung), nếu có thứ gì đó phù hợp với sự kết hợp này trong Market Watch. Ví dụ, nếu đường trên của indicator thuộc về EUR và đường dưới thuộc về USD, chúng tương ứng với cặp EURUSD, và theo chiến lược phá vỡ, chúng ta nên mua nó, nhưng theo chiến lược hồi giá, chúng ta nên bán nó.
Trong trường hợp tổng quát hơn, chẳng hạn, khi CFD hoặc hàng hóa với một đồng tiền định giá chung được chỉ định trong giỏ công cụ làm việc của indicator, không phải lúc nào cũng có thể tạo ra một công cụ thực tế. Đối với những trường hợp như vậy, cần phải làm phức tạp Expert Advisor bằng cách giới thiệu giao dịch tổng hợp (các vị thế kết hợp), nhưng chúng ta sẽ không làm điều này ở đây và sẽ giới hạn trong thị trường Forex, nơi hầu hết các tỷ giá chéo thường có sẵn.
Do đó, Expert Advisor không chỉ phải đọc tất cả các buffer của indicator mà còn phải tìm ra tên của các đồng tiền tương ứng với giá trị tối đa và tối thiểu. Và tại đây chúng ta gặp một trở ngại nhỏ.
MQL5 không cho phép đọc tên của các buffer indicator bên thứ ba và nói chung, bất kỳ thuộc tính dòng nào ngoài các thuộc tính kiểu số nguyên. Có ba hàm để thiết lập thuộc tính: PlotIndexSetInteger
, PlotIndexSetDouble
, và PlotIndexSetString
, nhưng chỉ có một hàm để đọc chúng: PlotIndexGetInteger
.
Về lý thuyết, khi các chương trình MQL được biên dịch thành một hệ thống giao dịch duy nhất được tạo ra bởi cùng một nhà phát triển, đây không phải là vấn đề lớn. Đặc biệt, chúng ta có thể tách một phần mã nguồn của indicator thành một tệp tiêu đề và bao gồm nó không chỉ trong indicator mà còn trong Expert Advisor. Sau đó, trong Expert Advisor, có thể lặp lại việc phân tích các tham số đầu vào của indicator và khôi phục danh sách các đồng tiền, hoàn toàn tương tự với danh sách được tạo bởi indicator. Việc sao chép các phép tính không đẹp lắm, nhưng nó sẽ hoạt động. Tuy nhiên, cũng cần một giải pháp phổ quát hơn khi indicator có nhà phát triển khác, và họ không muốn tiết lộ thuật toán hoặc dự định thay đổi nó trong tương lai (khi đó các phiên bản biên dịch của indicator và Expert Advisor sẽ không tương thích). Việc "kết nối" các indicator của người khác với của riêng mình, hoặc một Expert Advisor được đặt hàng từ dịch vụ tự do là một thực tiễn rất phổ biến. Do đó, nhà phát triển indicator nên làm cho nó thân thiện với việc tích hợp nhất có thể.
Một trong những giải pháp khả thi là indicator gửi thông điệp với số lượng và tên của các buffer sau khi khởi tạo.
Đây là cách nó được thực hiện trong trình xử lý OnInit
của indicator UnityPercentEvent.mq5
(mã dưới đây được hiển thị ở dạng rút gọn vì hầu như không có gì thay đổi).
int OnInit()
{
// tìm đồng tiền chung cho tất cả các cặp
const string common = InitSymbols();
...
// thiết lập các dòng hiển thị trong chu kỳ tiền tệ
int replaceIndex = -1;
for(int i = 0; i <= SymbolCount; i++)
{
string name;
// thay đổi thứ tự để đồng tiền cơ sở (chung) nằm dưới chỉ số 0,
// phần còn lại phụ thuộc vào thứ tự mà người dùng nhập các cặp
if(i == 0)
{
name = common;
if(name != workCurrencies.getKey(i))
{
replaceIndex = i;
}
}
else
{
if(common == workCurrencies.getKey(i) && replaceIndex > -1)
{
name = workCurrencies.getKey(replaceIndex);
}
else
{
name = workCurrencies.getKey(i);
}
}
// thiết lập hiển thị của các buffer
PlotIndexSetString(i, PLOT_LABEL, name);
...
// gửi chỉ số và tên buffer đến các chương trình cần chúng
EventChartCustom(0, (ushort)BarLimit, i, SymbolCount + 1, name);
}
...
}
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
So với phiên bản gốc, chỉ một dòng đã được thêm vào đây. Nó chứa lời gọi EventChartCustom
. Biến đầu vào BarLimit
được sử dụng làm định danh của bản sao indicator (có thể có nhiều bản sao). Vì indicator sẽ được gọi từ Expert Advisor và không hiển thị cho người dùng, chỉ cần chỉ định một số dương nhỏ, ít nhất là 1, nhưng chúng ta sẽ sử dụng, ví dụ, 10.
Bây giờ indicator đã sẵn sàng và tín hiệu của nó có thể được sử dụng trong các Expert Advisor bên thứ ba. Hãy bắt đầu phát triển Expert Advisor UnityMartingale.mq5
. Để đơn giản hóa việc trình bày, chúng ta sẽ chia nó thành 4 giai đoạn, dần dần thêm các khối mới. Chúng ta sẽ có ba phiên bản sơ bộ và một phiên bản cuối cùng.
UnityMartingaleDraft1.mq5
Ở giai đoạn đầu tiên, đối với phiên bản UnityMartingaleDraft1.mq5
, hãy sử dụng MultiMartingale.mq5
làm cơ sở và sửa đổi nó.
Chúng ta sẽ đổi tên biến đầu vào cũ StartType
, vốn xác định hướng của giao dịch đầu tiên trong chuỗi, thành SignalType
. Nó sẽ được sử dụng để chọn giữa các chiến lược đã xem xét là BREAKOUT và PULLBACK.
enum SIGNAL_TYPE
{
BREAKOUT,
PULLBACK
};
...
input SIGNAL_TYPE StartType = 0; // SignalType
2
3
4
5
6
7
Để thiết lập indicator, chúng ta cần một nhóm biến đầu vào riêng biệt.
input group "U N I T Y S E T T I N G S"
input string UnitySymbols = "EURUSD,GBPUSD,USDCHF,USDJPY,AUDUSD,USDCAD,NZDUSD";
input int UnityBarLimit = 10;
input ENUM_APPLIED_PRICE UnityPriceType = PRICE_CLOSE;
input ENUM_MA_METHOD UnityPriceMethod = MODE_EMA;
input int UnityPricePeriod = 1;
2
3
4
5
6
Lưu ý rằng tham số UnitySymbols
chứa danh sách các công cụ cụm để xây dựng indicator, và thường khác với danh sách các công cụ làm việc mà chúng ta muốn giao dịch. Các công cụ được giao dịch vẫn được thiết lập trong tham số WorkSymbols
.
Ví dụ, mặc định, chúng ta truyền một tập hợp các cặp tiền tệ chính của Forex
cho indicator, và do đó chúng ta có thể chỉ định giao dịch không chỉ các cặp chính mà còn bất kỳ cặp chéo nào. Thường thì có ý nghĩa khi giới hạn tập hợp này ở các công cụ có điều kiện giao dịch tốt nhất (đặc biệt, spread nhỏ hoặc vừa phải). Ngoài ra, nên tránh các biến dạng, tức là giữ một lượng bằng nhau của mỗi đồng tiền trong tất cả các cặp, từ đó thống kê trung hòa các rủi ro tiềm ẩn của việc chọn hướng không thành công cho một trong các đồng tiền.
Tiếp theo, chúng ta bao bọc việc điều khiển indicator trong lớp UnityController
. Ngoài handle
của indicator, các trường của lớp lưu trữ dữ liệu sau:
- Số lượng
buffers
của indicator, sẽ được nhận từ các thông điệp từ indicator sau khi khởi tạo - Số
bar
mà dữ liệu đang được đọc từ đó (thường là thanh hiện tại chưa hoàn thành là 0, hoặc thanh cuối cùng đã hoàn thành là 1) - Mảng
data
với các giá trị được đọc từ các buffer của indicator trên thanh được chỉ định - Thời gian đọc cuối cùng
lastRead
- Cờ hoạt động theo tick hoặc thanh
tickwise
Ngoài ra, lớp sử dụng đối tượng MultiSymbolMonitor
để đồng bộ hóa các thanh của tất cả các symbol liên quan.
class UnityController
{
int handle;
int buffers;
const int bar;
double data[];
datetime lastRead;
const bool tickwise;
MultiSymbolMonitor sync;
...
2
3
4
5
6
7
8
9
10
Trong constructor, vốn chấp nhận tất cả các tham số cho indicator thông qua các đối số, chúng ta tạo indicator và thiết lập đối tượng sync
.
public:
UnityController(const string symbolList, const int offset, const int limit,
const ENUM_APPLIED_PRICE type, const ENUM_MA_METHOD method, const int period):
bar(offset), tickwise(!offset)
{
handle = iCustom(_Symbol, _Period, "MQL5Book/p6/UnityPercentEvent",
symbolList, limit, type, method, period);
lastRead = 0;
string symbols[];
const int n = StringSplit(symbolList, ',', symbols);
for(int i = 0; i < n; ++i)
{
sync.attach(symbols[i]);
}
}
~UnityController()
{
IndicatorRelease(handle);
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Số lượng buffer được thiết lập bởi phương thức attached
. Chúng ta sẽ gọi nó khi nhận được thông điệp từ indicator.
void attached(const int b)
{
buffers = b;
ArrayResize(data, buffers);
}
2
3
4
5
Một phương thức đặc biệt isReady
trả về true
khi các thanh cuối cùng của tất cả các symbol có cùng thời gian. Chỉ trong trạng thái đồng bộ hóa như vậy, chúng ta mới nhận được các giá trị chính xác của indicator. Cần lưu ý rằng ở đây giả định rằng tất cả các công cụ có cùng lịch trình phiên giao dịch. Nếu không phải vậy, cần thay đổi phân tích thời gian.
bool isReady()
{
return sync.check(true) == 0;
}
2
3
4
Chúng ta xác định thời gian hiện tại theo các cách khác nhau tùy thuộc vào chế độ hoạt động của indicator: khi tính toán lại trên mỗi tick (tickwise
bằng true
), chúng ta sử dụng thời gian máy chủ, và khi tính toán lại một lần trên mỗi thanh, chúng ta sử dụng thời gian mở của thanh cuối cùng.
datetime lastTime() const
{
return tickwise ? TimeTradeServer() : iTime(_Symbol, _Period, 0);
}
2
3
4
Sự hiện diện của phương thức này sẽ cho phép chúng ta loại trừ việc đọc indicator nếu thời gian hiện tại không thay đổi và do đó, dữ liệu đọc cuối cùng được lưu trong buffer data
vẫn còn phù hợp. Và đây là cách tổ chức việc đọc các buffer của indicator trong phương thức read
. Chúng ta chỉ cần một giá trị của mỗi buffer cho thanh với chỉ số bar
.
bool read()
{
if(!buffers) return false;
for(int i = 0; i < buffers; ++i)
{
double temp[1];
if(CopyBuffer(handle, i, bar, 1, temp) == 1)
{
data[i] = temp[0];
}
else
{
return false;
}
}
lastRead = lastTime();
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Cuối cùng, chúng ta chỉ lưu thời gian đọc vào biến lastRead
. Nếu nó trống hoặc không bằng thời gian hiện tại mới, việc truy cập dữ liệu của bộ điều khiển trong các phương thức sau sẽ khiến các buffer của indicator được đọc bằng read
.
Các phương thức bên ngoài chính của bộ điều khiển là getOuterIndices
để lấy chỉ số của giá trị tối đa và tối thiểu và toán tử []
để đọc các giá trị.
bool isNewTime() const
{
return lastRead != lastTime();
}
bool getOuterIndices(int &min, int &max)
{
if(isNewTime())
{
if(!read()) return false;
}
max = ArrayMaximum(data);
min = ArrayMinimum(data);
return true;
}
double operator[](const int buffer)
{
if(isNewTime())
{
if(!read())
{
return EMPTY_VALUE;
}
}
return data[buffer];
}
};
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ước đây, Expert Advisor BandOsMA.mq5
đã giới thiệu khái niệm về giao diện TradingSignal
.
interface TradingSignal
{
virtual int signal(void);
};
2
3
4
Dựa trên nó, chúng ta sẽ mô tả việc triển khai tín hiệu bằng indicator UnityPercentEvent
. Đối tượng bộ điều khiển UnityController
được truyền vào constructor. Nó cũng chỉ ra các chỉ số của các đồng tiền (buffer), mà chúng ta muốn theo dõi tín hiệu cho chúng. Chúng ta sẽ có thể tạo ra một tập hợp tùy ý các tín hiệu khác nhau cho các symbol làm việc đã chọn.
class UnitySignal: public TradingSignal
{
UnityController *controller;
const int currency1;
const int currency2;
public:
UnitySignal(UnityController *parent, const int c1, const int c2):
controller(parent), currency1(c1), currency2(c2) { }
virtual int signal(void) override
{
if(!controller.isReady()) return 0; // chờ đồng bộ hóa thanh
if(!controller.isNewTime()) return 0; // chờ thời gian thay đổi
int min, max;
if(!controller.getOuterIndices(min, max)) return 0;
// quá mua
if(currency1 == max && currency2 == min) return +1;
// quá bán
if(currency2 == max && currency1 == min) return -1;
return 0;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Phương thức signal
trả về 0 trong tình huống không chắc chắn và +1 hoặc -1 trong trạng thái quá mua và quá bán của hai đồng tiền cụ thể.
Để chính thức hóa các chiến lược giao dịch, chúng ta đã sử dụng giao diện TradingStrategy
.
interface TradingStrategy
{
virtual bool trade(void);
};
2
3
4
Trong trường hợp này, lớp UnityMartingale
được tạo ra dựa trên nó, phần lớn trùng khớp với SimpleMartingale
từ MultiMartingale.mq5
. Chúng ta sẽ chỉ hiển thị các điểm khác biệt.
class UnityMartingale: public TradingStrategy
{
protected:
...
AutoPtr<TradingSignal> command;
public:
UnityMartingale(const Settings &state, TradingSignal *signal)
{
...
command = signal;
}
virtual bool trade() override
{
...
int s = command[].signal(); // lấy tín hiệu từ bộ điều khiển
if(s != 0)
{
if(settings.startType == PULLBACK) s *= -1; // đảo ngược logic cho hồi giá
}
ulong ticket = 0;
if(position[] == NULL) // bắt đầu sạch - không có (và đang không có) vị thế
{
if(s == +1)
{
ticket = openBuy(settings.lots);
}
else if(s == -1)
{
ticket = openSell(settings.lots);
}
}
else
{
if(position[].refresh()) // vị thế tồn tại
{
if((position[].get(POSITION_TYPE) == POSITION_TYPE_BUY && s == -1)
|| (position[].get(POSITION_TYPE) == POSITION_TYPE_SELL && s == +1))
{
// tín hiệu theo hướng ngược lại - cần đóng
PrintFormat("Opposite signal: %d for position %d %lld",
s, position[].get(POSITION_TYPE), position[].get(POSITION_TICKET));
if(close(position[].get(POSITION_TICKET)))
{
// position = NULL; - lưu vị thế vào bộ nhớ cache
}
else
{
position[].refresh(); // kiểm soát các lỗi đóng có thể xảy ra
}
}
else
{
// tín hiệu giống nhau hoặc không có - "trailing"
position[].update();
if(trailing[]) trailing[].trail();
}
}
else // không có vị thế - mở vị thế mới
{
if(s == 0) // không có tín hiệu
{
// đây là toàn bộ logic của Expert Advisor cũ:
// - đảo ngược cho thua lỗ martingale
// - tiếp tục theo lô ban đầu theo hướng có lợi nhuận
...
}
else // có tín hiệu
{
double lots;
if(position[].get(POSITION_PROFIT) >= 0.0)
{
lots = settings.lots; // lô ban đầu sau lợi nhuận
}
else // tăng lô sau thua lỗ
{
lots = MathFloor((position[].get(POSITION_VOLUME) * settings.factor) / lotsStep) * lotsStep;
if(lotsLimit < lots)
{
lots = settings.lots;
}
}
ticket = (s == +1) ? openBuy(lots) : openSell(lots);
}
}
}
}
...
}
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
Phần giao dịch đã sẵn sàng. Còn lại việc xem xét khởi tạo. Một con trỏ tự động đến đối tượng UnityController
và mảng với tên các đồng tiền được mô tả ở cấp độ toàn cục. Hồ bơi của các hệ thống giao dịch hoàn toàn tương tự với các phát triển trước đó.
AutoPtr<TradingStrategyPool> pool;
AutoPtr<UnityController> controller;
int currenciesCount;
string currencies[];
2
3
4
5
Trong trình xử lý OnInit
, chúng ta tạo đối tượng UnityController
và chờ indicator gửi phân phối các đồng tiền theo chỉ số buffer.
int OnInit()
{
currenciesCount = 0;
ArrayResize(currencies, 0);
if(!StartUp(true)) return INIT_PARAMETERS_INCORRECT;
const bool barwise = UnityPriceType == PRICE_CLOSE && UnityPricePeriod == 1;
controller = new UnityController(UnitySymbols, barwise,
UnityBarLimit, UnityPriceType, UnityPriceMethod, UnityPricePeriod);
// chờ tin nhắn từ indicator về các đồng tiền trong buffer
return INIT_SUCCEEDED;
}
2
3
4
5
6
7
8
9
10
11
12
13
Nếu loại giá PRICE_CLOSE
và chu kỳ đơn được chọn trong tham số đầu vào của indicator, phép tính trong bộ điều khiển sẽ được thực hiện một lần cho mỗi thanh. Trong tất cả các trường hợp khác, tín hiệu sẽ được cập nhật theo tick, nhưng không thường xuyên hơn một lần mỗi giây (hãy nhớ lại việc triển khai phương thức lastTime
trong bộ điều khiển).
Phương thức trợ giúp StartUp
nhìn chung thực hiện cùng một việc như trình xử lý OnInit
cũ trong Expert Advisor MultiMartingale
. Nó điền cấu trúc Settings
với các cài đặt, kiểm tra tính đúng đắn của chúng và tạo một hồ bơi hệ thống giao dịch TradingStrategyPool
, bao gồm các đối tượng của lớp UnityMartingale
cho các symbol giao dịch khác nhau WorkSymbols
. Tuy nhiên, giờ đây quá trình này được chia thành hai giai đoạn do chúng ta cần chờ thông tin về sự phân phối của các đồng tiền giữa các buffer. Do đó, hàm StartUp
có một tham số đầu vào biểu thị lời gọi từ OnInit
và sau đó từ OnChartEvent
.
Khi phân tích mã nguồn của StartUp
, điều quan trọng là phải nhớ rằng việc khởi tạo khác nhau đối với các trường hợp khi chúng ta chỉ giao dịch một công cụ khớp với biểu đồ hiện tại và khi một rổ công cụ được chỉ định. Chế độ đầu tiên hoạt động khi WorkSymbols
là một dòng trống. Điều này thuận tiện để tối ưu hóa Expert Advisor cho một công cụ cụ thể. Sau khi tìm thấy cài đặt cho nhiều công cụ, chúng ta có thể kết hợp chúng trong WorkSymbols
.
bool StartUp(const bool init = false)
{
if(WorkSymbols == "")
{
Settings settings =
{
UseTime, HourStart, HourEnd,
Lots, Factor, Limit,
StopLoss, TakeProfit,
StartType, Magic, SkipTimeOnError, Trailing, _Symbol
};
if(settings.validate())
{
if(init)
{
Print("Input settings:");
settings.print();
}
}
else
{
if(init) Print("Wrong settings, please fix");
return false;
}
if(!init)
{
... // tạo hệ thống giao dịch dựa trên indicator
}
}
else
{
Print("Parsed settings:");
Settings settings[];
if(!Settings::parseAll(WorkSymbols, settings))
{
if(init) Print("Settings are incorrect, can't start up");
return false;
}
if(!init)
{
... // tạo hệ thống giao dịch dựa trên indicator
}
}
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
35
36
37
38
39
40
41
42
43
44
45
46
Hàm StartUp
trong OnInit
được gọi với tham số true
, nghĩa là chỉ kiểm tra tính đúng đắn của cài đặt. Việc tạo đối tượng hệ thống giao dịch bị trì hoãn cho đến khi nhận được tin nhắn từ indicator trong OnChartEvent
.
void OnChartEvent(const int id,
const long &lparam, const double &dparam, const string &sparam)
{
if(id == CHARTEVENT_CUSTOM + UnityBarLimit)
{
PrintFormat("%lld %f '%s'", lparam, dparam, sparam);
if(lparam == 0) ArrayResize(currencies, 0);
currenciesCount = (int)MathRound(dparam);
PUSH(currencies, sparam);
if(ArraySize(currencies) == currenciesCount)
{
if(pool[] == NULL)
{
start up(); // xác nhận sẵn sàng của indicator
}
else
{
Alert("Repeated initialization!");
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Ở đây chúng ta ghi nhớ số lượng đồng tiền trong biến toàn cục currenciesCount
và lưu trữ chúng trong mảng currencies
, sau đó gọi StartUp
với tham số false
(giá trị mặc định, do đó bị bỏ qua). Các tin nhắn đến từ hàng đợi theo thứ tự chúng tồn tại trong buffer của indicator. Do đó, chúng ta có được sự khớp giữa chỉ số và tên của đồng tiền.
Khi StartUp
được gọi lại, một đoạn mã bổ sung được thực thi:
bool StartUp(const bool init = false)
{
if(WorkSymbols == "") // một symbol hiện tại
{
...
if(!init) // khởi tạo cuối cùng sau OnInit
{
controller[].attached(currenciesCount);
// chia _Symbol thành 2 đồng tiền từ mảng currencies []
int first, second;
if(!SplitSymbolToCurrencyIndices(_Symbol, first, second))
{
PrintFormat("Can't find currencies (%s %s) for %s",
(first == -1 ? "base" : ""), (second == -1 ? "profit" : ""), _Symbol);
return false;
}
// tạo hồ bơi từ một chiến lược duy nhất
pool = new TradingStrategyPool(new UnityMartingale(settings,
new UnitySignal(controller[], first, second)));
}
}
else // rổ symbol
{
...
if(!init) // khởi tạo cuối cùng sau OnInit
{
controller[].attached(currenciesCount);
const int n = ArraySize(settings);
pool = new TradingStrategyPool(n);
for(int i = 0; i < n; i++)
{
...
// chia settings[i].symbol thành 2 đồng tiền từ currencies[]
int first, second;
if(!SplitSymbolToCurrencyIndices(settings[i].symbol, first, second))
{
PrintFormat("Can't find currencies (%s %s) for %s",
(first == -1 ? "base" : ""), (second == -1 ? "profit" : ""),
settings[i].symbol);
}
else
{
// thêm chiến lược vào hồ bơi trên symbol giao dịch tiếp theo
pool[].push(new UnityMartingale(settings[i],
new UnitySignal(controller[], first, second)));
}
}
}
}
}
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
Hàm trợ giúp SplitSymbolToCurrencyIndices
chọn đồng tiền cơ bản và đồng tiền lợi nhuận của symbol được truyền vào và tìm chỉ số của chúng trong mảng currencies
. Do đó, chúng ta có được dữ liệu tham chiếu để tạo tín hiệu trong các đối tượng UnitySignal
. Mỗi đối tượng sẽ có một cặp chỉ số đồng tiền riêng.
bool SplitSymbolToCurrencyIndices(const string symbol, int &first, int &second)
{
const string s1 = SymbolInfoString(symbol, SYMBOL_CURRENCY_BASE);
const string s2 = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT);
first = second = -1;
for(int i = 0; i < ArraySize(currencies); ++i)
{
if(currencies[i] == s1) first = i;
else if(currencies[i] == s2) second = i;
}
return first != -1 && second != -1;
}
2
3
4
5
6
7
8
9
10
11
12
13
Nhìn chung, Expert Advisor đã sẵn sàng.
Bạn có thể thấy rằng trong các ví dụ cuối cùng của Expert Advisor, chúng ta có các lớp chiến lược và lớp tín hiệu giao dịch. Chúng ta cố ý làm cho chúng trở thành hậu duệ của các giao diện chung
TradingStrategy
vàTradingSignal
để sau này có thể thu thập các bộ sưu tập của các triển khai tương thích nhưng khác nhau, có thể được kết hợp trong việc phát triển các Expert Advisor trong tương lai. Các lớp cụ thể thống nhất như vậy thường nên được tách ra thành các tệp tiêu đề riêng biệt. Trong các ví dụ của chúng ta, chúng ta không làm điều này để đơn giản hóa việc sửa đổi từng bước.Tuy nhiên, cách tiếp cận được mô tả là tiêu chuẩn cho OOP. Đặc biệt, như chúng ta đã đề cập trong phần về tạo bản nháp Expert Advisor, cùng với MetaTrader 5 là một
framework
của các tệp tiêu đề với các lớp tiêu chuẩn của hoạt động giao dịch, chỉ báo tín hiệu và quản lý tiền, được sử dụng trong MQL Wizard. Các giải pháp tương tự khác được công bố trên trangmql5.com
trong các bài viết và phần Code Base.Bạn có thể sử dụng các hệ thống lớp đã sẵn sàng làm cơ sở cho các dự án của mình, miễn là chúng phù hợp về khả năng và dễ sử dụng.
Để hoàn thiện bức tranh, chúng ta muốn giới thiệu tiêu chí tối ưu hóa dựa trên R2 trong Expert Advisor. Để tránh mâu thuẫn giữa hồi quy tuyến tính trong công thức tính R2 và các lô biến đổi được bao gồm trong chiến lược của chúng ta, chúng ta sẽ tính hệ số không phải cho đường cân bằng thông thường mà cho các gia số tích lũy của nó được chuẩn hóa theo kích thước lô trong mỗi giao dịch.
Để làm điều này, trong trình xử lý OnTester
, chúng ta chọn các giao dịch có loại DEAL_TYPE_BUY
và DEAL_TYPE_SELL
và hướng OUT
. Chúng ta sẽ yêu cầu tất cả thuộc tính giao dịch hình thành kết quả tài chính (lợi nhuận/thua lỗ), tức là DEAL_PROFIT
, DEAL_SWAP
, DEAL_COMMISSION
, DEAL_FEE
, cũng như khối lượng DEAL_VOLUME
của chúng.
#define STAT_PROPS 5 // số lượng thuộc tính giao dịch được yêu cầu
double OnTester()
{
HistorySelect(0, LONG_MAX);
const ENUM_DEAL_PROPERTY_DOUBLE props[STAT_PROPS] =
{
DEAL_PROFIT, DEAL_SWAP, DEAL_COMMISSION, DEAL_FEE, DEAL_VOLUME
};
double expenses[][STAT_PROPS];
ulong tickets[]; // cần thiết vì nguyên mẫu phương thức 'select', nhưng hữu ích để gỡ lỗi
DealFilter filter;
filter.let(DEAL_TYPE, (1 << DEAL_TYPE_BUY) | (1 << DEAL_TYPE_SELL), IS::OR_BITWISE)
.let(DEAL_ENTRY, (1 << DEAL_ENTRY_OUT) | (1 << DEAL_ENTRY_INOUT) | (1 << DEAL_ENTRY_OUT_BY),
IS::OR_BITWISE)
.select(props, tickets, expenses);
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Tiếp theo, trong mảng balance
, chúng ta tích lũy lợi nhuận/thua lỗ được chuẩn hóa theo khối lượng giao dịch và tính toán tiêu chí R2 cho nó.
const int n = ArraySize(tickets);
double balance[];
ArrayResize(balance, n + 1);
balance[0] = TesterStatistics(STAT_INITIAL_DEPOSIT);
for(int i = 0; i < n; ++i)
{
double result = 0;
for(int j = 0; j < STAT_PROPS - 1; ++j)
{
result += expenses[i][j];
}
result /= expenses[i][STAT_PROPS - 1]; // chuẩn hóa theo khối lượng
balance[i + 1] = result + balance[i];
}
const double r2 = RSquaredTest(balance);
return r2 * 100;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Phiên bản đầu tiên của Expert Advisor cơ bản đã sẵn sàng. Chúng ta chưa bao gồm kiểm tra mô hình tick bằng TickModel.mqh
. Giả định rằng Expert Advisor sẽ được kiểm tra khi tạo tick ở chế độ OHLC M1 hoặc tốt hơn. Khi phát hiện mô hình "chỉ giá mở", Expert Advisor sẽ gửi một khung đặc biệt với trạng thái lỗi đến terminal và tự động thoát khỏi tester. Thật không may, điều này chỉ dừng lần chạy này, nhưng quá trình tối ưu hóa sẽ tiếp tục. Do đó, bản sao của Expert Advisor chạy trong terminal sẽ phát ra một "cảnh báo" để người dùng tự tay ngắt quá trình tối ưu hóa.
void OnTesterPass()
{
ulong pass;
string name;
long id;
double value;
uchar data[];
while(FrameNext(pass, name, id, value, data))
{
if(name == "status" && id == 1)
{
Alert("Vui lòng dừng tối ưu hóa!");
Alert("Mô hình tick không đúng: yêu cầu OHLC M1 hoặc tốt hơn");
// logic hợp lý là lệnh tiếp theo sẽ dừng toàn bộ tối ưu hóa,
// nhưng không phải vậy
ExpertRemove();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Bạn có thể tối ưu hóa các tham số SYMBOL SETTINGS cho bất kỳ symbol nào và lặp lại quá trình tối ưu hóa cho các symbol khác nhau. Đồng thời, các nhóm COMMON SETTINGS và UNITY SETTINGS luôn phải chứa cùng cài đặt, vì chúng áp dụng cho tất cả các symbol và các phiên bản của hệ thống giao dịch. Ví dụ, Trailing
phải được bật hoặc tắt cho tất cả các lần tối ưu hóa. Cũng lưu ý rằng các biến đầu vào cho một symbol duy nhất (tức là nhóm SYMBOL SETTINGS) chỉ có hiệu lực khi WorkSymbols
chứa một chuỗi rỗng. Do đó, ở giai đoạn tối ưu hóa, bạn nên giữ nó rỗng.
Ví dụ, để đa dạng hóa rủi ro, bạn có thể liên tục tối ưu hóa Expert Advisor trên các cặp hoàn toàn độc lập: EURUSD, AUDJPY, GBPCHF, NZDCAD, hoặc trong các tổ hợp khác. Ba tệp cài đặt với các ví dụ về cài đặt riêng được liên kết với mã nguồn.
#property tester_set "UnityMartingale-eurusd.set"
#property tester_set "UnityMartingale-gbpchf.set"
#property tester_set "UnityMartingale-audjpy.set"
2
3
Để giao dịch trên ba symbol cùng lúc, các cài đặt này nên được "đóng gói" vào một tham số chung WorkSymbols
:
EURUSD+0.01*1.6^5(200,200)[17,21];GBPCHF+0.01*1.2^8(600,800)[7,20];AUDJPY+0.01*1.2^8(600,800)[7,20]
Cài đặt này cũng được bao gồm trong một tệp riêng.
#property tester_set "UnityMartingale-combo.set"
Một trong những vấn đề của phiên bản hiện tại của Expert Advisor là báo cáo tester sẽ cung cấp số liệu thống kê tổng quát cho tất cả các symbol (chính xác hơn, cho tất cả các chiến lược giao dịch, vì chúng ta có thể bao gồm các lớp khác nhau trong pool), trong khi chúng ta muốn theo dõi và đánh giá từng thành phần của hệ thống một cách riêng biệt.
Để làm điều này, bạn cần học cách tự tính toán các chỉ số tài chính chính của giao dịch, tương tự như cách tester thực hiện cho chúng ta. Chúng ta sẽ xử lý điều này ở giai đoạn thứ hai của việc phát triển Expert Advisor.
UnityMartingaleDraft2.mq5
Việc tính toán thống kê có thể cần khá thường xuyên, vì vậy chúng ta sẽ triển khai nó trong một tệp tiêu đề riêng TradeReport.mqh
, nơi chúng ta tổ chức mã nguồn thành các lớp phù hợp.
Hãy gọi lớp chính là TradeReport
. Nhiều biến giao dịch phụ thuộc vào đường cong số dư và ký quỹ tự do (vốn). Do đó, lớp này chứa các biến để theo dõi số dư hiện tại và lợi nhuận, cũng như một mảng được cập nhật liên tục với lịch sử số dư. Chúng ta sẽ không lưu trữ lịch sử vốn, vì nó có thể thay đổi trên mỗi tick, và tốt hơn là tính toán nó ngay lập tức. Chúng ta sẽ thấy lý do cần đường cong số dư sau một chút.
class TradeReport
{
double balance; // số dư hiện tại
double floating; // lợi nhuận thả nổi hiện tại
double data[]; // đường cong số dư đầy đủ - giá
datetime moments[]; // và ngày/giờ
...
2
3
4
5
6
7
Việc thay đổi và đọc các trường của lớp được thực hiện bằng các phương thức, bao gồm cả hàm khởi tạo, trong đó số dư được khởi tạo bằng thuộc tính ACCOUNT_BALANCE
.
TradeReport()
{
balance = AccountInfoDouble(ACCOUNT_BALANCE);
}
void resetFloatingPL()
{
floating = 0;
}
void addFloatingPL(const double pl)
{
floating += pl;
}
void addBalance(const double pl)
{
balance += pl;
}
double getCurrent() const
{
return balance + floating;
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Các phương thức này sẽ cần thiết để tính toán mức sụt vốn (equity drawdown) một cách lặp đi lặp lại (trên đường đi). Mảng số dư data
sẽ được yêu cầu để tính toán mức sụt số dư một lần (chúng ta sẽ thực hiện điều này vào cuối bài kiểm tra).
Dựa trên sự biến động của đường cong (dù là số dư hay vốn), mức sụt tuyệt đối và tương đối nên được tính toán bằng cùng một thuật toán. Do đó, thuật toán này và các biến nội bộ cần thiết cho nó, lưu trữ các trạng thái trung gian, được triển khai trong cấu trúc lồng nhau DrawDown
. Mã dưới đây cho thấy các phương thức và thuộc tính chính của nó.
struct DrawDown
{
double
series_start,
series_min,
series_dd,
series_dd_percent,
series_dd_relative_percent,
series_dd_relative;
...
void reset();
void calcDrawdown(const double &data[]);
void calcDrawdown(const double amount);
void print() const;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Phương thức calcDrawdown
đầu tiên tính toán mức sụt khi chúng ta biết toàn bộ mảng và điều này sẽ được sử dụng cho số dư. Phương thức calcDrawdown
thứ hai tính toán mức sụt một cách lặp đi lặp lại: mỗi lần nó được gọi, nó được thông báo giá trị tiếp theo của chuỗi, và điều này sẽ được sử dụng cho vốn.
Ngoài mức sụt, như chúng ta biết, có rất nhiều thống kê tiêu chuẩn cho báo cáo, nhưng chúng ta sẽ chỉ hỗ trợ một vài trong số đó để bắt đầu. Để làm điều này, chúng ta mô tả các trường tương ứng trong một cấu trúc lồng nhau khác, GenericStats
. Nó được kế thừa từ DrawDown
vì chúng ta vẫn cần mức sụt trong báo cáo.
struct GenericStats: public DrawDown
{
long deals;
long trades;
long buy_trades;
long wins;
long buy_wins;
long sell_wins;
double profits;
double losses;
double net;
double pf;
double average_trade;
double recovery;
double max_profit;
double max_loss;
double sharpe;
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Dựa trên tên của các biến, dễ dàng đoán được chúng tương ứng với các chỉ số tiêu chuẩn nào. Một số chỉ số là dư thừa và do đó bị bỏ qua. Ví dụ, với tổng số giao dịch (trades
) và số giao dịch mua trong đó (buy_trades
), chúng ta có thể dễ dàng tìm ra số giao dịch bán (trades - sell_trades
). Điều tương tự áp dụng cho số liệu thắng/thua bổ sung. Các chuỗi thắng và thua không được tính. Những ai muốn có thể bổ sung báo cáo của chúng ta với các chỉ số này.
Để thống nhất với thống kê tổng quát của tester, có phương thức fillByTester
điền tất cả các trường thông qua hàm TesterStatistics
. Chúng ta sẽ sử dụng nó sau này.
void fillByTester()
{
deals = (long)TesterStatistics(STAT_DEALS);
trades = (long)TesterStatistics(STAT_TRADES);
buy_trades = (long)TesterStatistics(STAT_LONG_TRADES);
wins = (long)TesterStatistics(STAT_PROFIT_TRADES);
buy_wins = (long)TesterStatistics(STAT_PROFIT_LONGTRADES);
sell_wins = (long)TesterStatistics(STAT_PROFIT_SHORTTRADES);
profits = TesterStatistics(STAT_GROSS_PROFIT);
losses = TesterStatistics(STAT_GROSS_LOSS);
net = TesterStatistics(STAT_PROFIT);
pf = TesterStatistics(STAT_PROFIT_FACTOR);
average_trade = TesterStatistics(STAT_EXPECTED_PAYOFF);
recovery = TesterStatistics(STAT_RECOVERY_FACTOR);
sharpe = TesterStatistics(STAT_SHARPE_RATIO);
max_profit = TesterStatistics(STAT_MAX_PROFITTRADE);
max_loss = TesterStatistics(STAT_MAX_LOSSTRADE);
series_start = TesterStatistics(STAT_INITIAL_DEPOSIT);
series_min = TesterStatistics(STAT_EQUITYMIN);
series_dd = TesterStatistics(STAT_EQUITY_DD);
series_dd_percent = TesterStatistics(STAT_EQUITYDD_PERCENT);
series_dd_relative_percent = TesterStatistics(STAT_EQUITY_DDREL_PERCENT);
series_dd_relative = TesterStatistics(STAT_EQUITY_DD_RELATIVE);
}
};
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
Dĩ nhiên, chúng ta cần tự triển khai tính toán của mình cho các số dư và vốn riêng biệt của các hệ thống giao dịch mà tester không thể tính toán. Các nguyên mẫu của phương thức calcDrawdown
đã được trình bày ở trên. Trong quá trình hoạt động, chúng điền vào nhóm trường cuối cùng với tiền tố series_dd
. Ngoài ra, lớp TradeReport
chứa một phương thức để tính toán tỷ lệ Sharpe. Đầu vào của nó là một chuỗi số và tỷ lệ tài trợ không rủi ro. Mã nguồn đầy đủ có thể được tìm thấy trong tệp đính kèm.
static double calcSharpe(const double &data[], const double riskFreeRate = 0);
Như bạn có thể đoán, khi gọi phương thức này, mảng thành viên liên quan của lớp TradeReport
với số dư sẽ được truyền vào tham số data
. Quá trình điền mảng này và gọi các phương thức trên cho các chỉ số cụ thể diễn ra trong phương thức calcStatistics
(xem bên dưới). Một bộ lọc đối tượng của các giao dịch được truyền vào nó dưới dạng đầu vào (filter
), số tiền gửi ban đầu (start
), và thời gian (origin
). Giả định rằng mã gọi sẽ thiết lập bộ lọc sao cho chỉ các giao dịch của hệ thống giao dịch mà chúng ta quan tâm mới rơi vào đó.
Phương thức trả về một cấu trúc đã điền GenericStats
, và ngoài ra, nó điền hai mảng bên trong đối tượng TradeReport
, data
và moments
, lần lượt với các giá trị số dư và tham chiếu thời gian của các thay đổi. Chúng ta sẽ cần điều này trong phiên bản cuối cùng của Expert Advisor.
GenericStats calcStatistics(DealFilter &filter,
const double start = 0, const datetime origin = 0,
const double riskFreeRate = 0)
{
GenericStats stats;
ArrayResize(data, 0);
ArrayResize(moments, 0);
ulong tickets[];
if(!filter.select(tickets)) return stats;
balance = start;
PUSH(data, balance);
PUSH(moments, origin);
for(int i = 0; i < ArraySize(tickets); ++i)
{
DealMonitor m(tickets[i]);
if(m.get(DEAL_TYPE) == DEAL_TYPE_BALANCE) //deposit/withdrawal
{
balance += m.get(DEAL_PROFIT);
PUSH(data, balance);
PUSH(moments, (datetime)m.get(DEAL_TIME));
}
else if(m.get(DEAL_TYPE) == DEAL_TYPE_BUY
|| m.get(DEAL_TYPE) == DEAL_TYPE_SELL)
{
const double profit = m.get(DEAL_PROFIT) + m.get(DEAL_SWAP)
+ m.get(DEAL_COMMISSION) + m.get(DEAL_FEE);
balance += profit;
stats.deals++;
if(m.get(DEAL_ENTRY) == DEAL_ENTRY_OUT
|| m.get(DEAL_ENTRY) == DEAL_ENTRY_INOUT
|| m.get(DEAL_ENTRY) == DEAL_ENTRY_OUT_BY)
{
PUSH(data, balance);
PUSH(moments, (datetime)m.get(DEAL_TIME));
stats.trades++; // trades are counted by exit deals
if(m.get(DEAL_TYPE) == DEAL_TYPE_SELL)
{
stats.buy_trades++; // closing with a deal in the opposite direction
}
if(profit >= 0)
{
stats.wins++;
if(m.get(DEAL_TYPE) == DEAL_TYPE_BUY)
{
stats.sell_wins++; // closing with a deal in the opposite direction
}
else
{
stats.buy_wins++;
}
}
}
else if(!TU::Equal(profit, 0))
{
PUSH(data, balance); // entry fee (if any)
PUSH(moments, (datetime)m.get(DEAL_TIME));
}
if(profit >= 0)
{
stats.profits += profit;
stats.max_profit = fmax(profit, stats.max_profit);
}
else
{
stats.losses += profit;
stats.max_loss = fmin(profit, stats.max_loss);
}
}
}
if(stats.trades > 0)
{
stats.net = stats.profits + stats.losses;
stats.pf = -stats.losses > DBL_EPSILON ?
stats.profits / -stats.losses : MathExp(10000.0); // NaN(+inf)
stats.average_trade = stats.net / stats.trades;
stats.sharpe = calcSharpe(data, riskFreeRate);
stats.calcDrawdown(data); // fill in all fields of the DrawDown substructure
stats.recovery = stats.series_dd > DBL_EPSILON ?
stats.net / stats.series_dd : MathExp(10000.0);
}
return stats;
}
};
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
Ở đây bạn có thể thấy cách chúng ta gọi calcSharpe
và calcDrawdown
để lấy các chỉ số tương ứng trên mảng data
. Các chỉ số còn lại được tính toán trực tiếp trong vòng lặp bên trong calcStatistics
.
Lớp TradeReport
đã sẵn sàng, và chúng ta có thể mở rộng chức năng của Expert Advisor lên phiên bản UnityMartingaleDraft2.mq5
.
Hãy thêm các thành viên mới vào lớp UnityMartingale
.
class UnityMartingale: public TradingStrategy
{
protected:
...
TradeReport report;
TradeReport::DrawDown equity;
const double deposit;
const datetime epoch;
...
2
3
4
5
6
7
8
9
Chúng ta cần đối tượng report
để gọi calcStatistics
, nơi mà mức sụt số dư sẽ được bao gồm. Đối tượng equity
cần thiết cho việc tính toán độc lập mức sụt vốn. Số dư ban đầu và ngày, cũng như điểm bắt đầu tính toán mức sụt vốn, được thiết lập trong hàm khởi tạo.
public:
UnityMartingale(const Settings &state, TradingSignal *signal):
symbol(state.symbol), deposit(AccountInfoDouble(ACCOUNT_BALANCE)),
epoch(TimeCurrent())
{
...
equity.calcDrawdown(deposit);
...
}
2
3
4
5
6
7
8
9
Việc tiếp tục tính toán mức sụt theo vốn được thực hiện liên tục, với mỗi lần gọi phương thức trade
.
virtual bool trade() override
{
...
if(MQLInfoInteger(MQL_TESTER))
{
if(position[])
{
report.resetFloatingPL();
// after reset, sum all floating profits
// why we call addFloatingPL for each existing position,
// but this strategy has a maximum of 1 position at a time
report.addFloatingPL(position[].get(POSITION_PROFIT)
+ position[].get(POSITION_SWAP));
// after taking into account all the amounts - update the drawdown
equity.calcDrawdown(report.getCurrent());
}
}
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Đây chưa phải là tất cả những gì cần thiết cho một phép tính đúng. Chúng ta nên tính đến lợi nhuận hoặc thua lỗ thả nổi trên số dư. Phần mã trên chỉ hiển thị lời gọi addFloatingPL
, nhưng lớp TradeReport
cũng có một phương thức để thay đổi số dư: addBalance
. Tuy nhiên, số dư chỉ thay đổi khi vị thế được đóng.
Nhờ khái niệm OOP, việc đóng vị thế trong tình huống của chúng ta tương ứng với việc xóa đối tượng position
của lớp PositionState
. Vậy tại sao chúng ta không thể chặn nó lại?
Lớp PositionState
không cung cấp bất kỳ phương tiện nào cho việc này, nhưng chúng ta có thể khai báo một lớp dẫn xuất PositionStateWithEquity
với hàm khởi tạo và hàm hủy đặc biệt.
Khi tạo một đối tượng, không chỉ định danh vị thế được truyền vào hàm khởi tạo, mà còn có một con trỏ tới đối tượng báo cáo mà thông tin sẽ cần được gửi đến.
class PositionStateWithEquity: public PositionState
{
TradeReport *report;
public:
PositionStateWithEquity(const long t, TradeReport *r):
PositionState(t), report(r) { }
...
2
3
4
5
6
7
8
Trong hàm hủy, chúng ta tìm tất cả các giao dịch theo ID vị thế đã đóng, tính toán kết quả tài chính tổng cộng (cùng với hoa hồng và các khoản khấu trừ khác), sau đó gọi addBalance
cho đối tượng report
liên quan.
~PositionStateWithEquity()
{
if(HistorySelectByPosition(get(POSITION_IDENTIFIER)))
{
double result = 0;
DealFilter filter;
int props[] = {DEAL_PROFIT, DEAL_SWAP, DEAL_COMMISSION, DEAL_FEE};
Tuple4<double, double, double, double> overheads[];
if(filter.select(props, overheads))
{
for(int i = 0; i < ArraySize(overheads); ++i)
{
result += NormalizeDouble(overheads[i]._1, 2)
+ NormalizeDouble(overheads[i]._2, 2)
+ NormalizeDouble(overheads[i]._3, 2)
+ NormalizeDouble(overheads[i]._4, 2);
}
}
if(CheckPointer(report) != POINTER_INVALID) report.addBalance(result);
}
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Còn lại phải làm rõ một điểm — làm thế nào để tạo các đối tượng lớp PositionStateWithEquity
cho các vị thế thay vì PositionState
. Để làm điều này, chỉ cần thay đổi toán tử new
ở một vài nơi mà nó được gọi trong lớp TradingStrategy
.
position = MQLInfoInteger(MQL_TESTER) ?
new PositionStateWithEquity(tickets[0], &report) : new PositionState(tickets[0]);
2
Do đó, chúng ta đã triển khai việc thu thập dữ liệu. Bây giờ chúng ta cần trực tiếp tạo báo cáo, tức là gọi calcStatistics
. Ở đây chúng ta cần mở rộng giao diện TradingStrategy
: chúng ta thêm phương thức statement
vào đó.
interface TradingStrategy
{
virtual bool trade(void);
virtual bool statement();
};
2
3
4
5
Sau đó, trong triển khai hiện tại này, dành cho chiến lược của chúng ta, chúng ta sẽ có thể hoàn thiện công việc một cách logic.
class UnityMartingale: public TradingStrategy
{
...
virtual bool statement() override
{
if(MQLInfoInteger(MQL_TESTER))
{
Print("Separate trade report for ", settings.symbol);
// equity drawdown should already be calculated on the fly
Print("Equity DD:");
equity.print();
// balance drawdown is calculated in the resulting report
Print("Trade Statistics (with Balance DD):");
// configure the filter for a specific strategy
DealFilter filter;
filter.let(DEAL_SYMBOL, settings.symbol)
.let(DEAL_MAGIC, settings.magic, IS::EQUAL_OR_ZERO);
// zero "magic" number is needed for the last exit deal
// - it is done by the tester itself
HistorySelect(0, LONG_MAX);
TradeReport::GenericStats stats =
report.calcStatistics(filter, deposit, epoch);
stats.print();
}
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
Phương thức mới sẽ chỉ in ra tất cả các chỉ số đã tính toán trong nhật ký. Bằng cách chuyển tiếp cùng phương thức qua nhóm các hệ thống giao dịch TradingStrategyPool
, hãy yêu cầu báo cáo riêng cho tất cả các biểu tượng từ trình xử lý OnTester
.
double OnTester()
{
...
if(pool[] != NULL)
{
pool[].statement(); // ask all trading systems to display their results
}
...
}
2
3
4
5
6
7
8
9
Hãy kiểm tra tính đúng đắn của báo cáo của chúng ta. Để làm điều này, hãy chạy Expert Advisor trong tester, từng biểu tượng một, và so sánh báo cáo tiêu chuẩn với các tính toán của chúng ta. Ví dụ, để thiết lập UnityMartingale-eurusd.set
, giao dịch trên EURUSD H1, chúng ta sẽ nhận được các chỉ số như vậy cho năm 2021.
Báo cáo Tester cho năm 2021, EURUSD H1
Trong nhật ký, phiên bản của chúng ta được hiển thị dưới dạng hai cấu trúc: DrawDown
với các chỉ số mức sụt vốn và GenericStats
với các chỉ số mức sụt số dư và các thống kê khác.
Separate trade report for EURUSD
Equity DD:
[maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] »
[0] 10022.48 10017.03 10000.00 9998.20 6.23 0.06 »
» [series_dd_relative_percent] [series_dd_relative]
» 0.06 6.23
Trade Statistics (with Balance DD):
[maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] »
[0] 10022.40 10017.63 10000.00 9998.51 5.73 0.06 »
» [series_dd_relative_percent] [series_dd_relative] »
» 0.06 5.73 »
» [deals] [trades] [buy_trades] [wins] [buy_wins] [sell_wins] [profits] [losses] [net] [pf] »
» 194 97 43 42 19 23 57.97 -39.62 18.35 1.46 »
» [average_trade] [recovery] [max_profit] [max_loss] [sharpe]
» 0.19 3.20 2.00 -2.01 0.15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Dễ dàng xác minh rằng các con số này khớp với báo cáo của tester.
Bây giờ hãy bắt đầu giao dịch trên cùng kỳ với ba biểu tượng cùng lúc (thiết lập UnityMartingale-combo.set
).
Ngoài các mục nhập EURUSD, các cấu trúc cho GBPCHF và AUDJPY sẽ xuất hiện trong nhật ký.
Báo cáo giao dịch riêng cho GBPCHF
Equity DD:
[maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] »
[0] 10029.50 10000.19 10000.00 9963.65 62.90 0.63 »
» [series_dd_relative_percent] [series_dd_relative]
» 0.63 62.90
Thống kê giao dịch (với Balance DD):
[maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] »
[0] 10023.68 9964.28 10000.00 9964.28 59.40 0.59 »
» [series_dd_relative_percent] [series_dd_relative] »
» 0.59 59.40 »
» [deals] [trades] [buy_trades] [wins] [buy_wins] [sell_wins] [profits] [losses] [net] [pf] »
» 600 300 154 141 63 78 394.53 -389.33 5.20 1.01 »
» [average_trade] [recovery] [max_profit] [max_loss] [sharpe]
» 0.02 0.09 9.10 -6.73 0.01
Báo cáo giao dịch riêng cho AUDJPY
Equity DD:
[maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] »
[0] 10047.14 10041.53 10000.00 9961.62 48.20 0.48 »
» [series_dd_relative_percent] [series_dd_relative]
» 0.48 48.20
Thống kê giao dịch (với Balance DD):
[maxpeak] [minpeak] [series_start] [series_min] [series_dd] [series_dd_percent] »
[0] 10045.21 10042.75 10000.00 9963.62 44.21 0.44 »
» [series_dd_relative_percent] [series_dd_relative] »
» 0.44 44.21 »
» [deals] [trades] [buy_trades] [wins] [buy_wins] [sell_wins] [profits] [losses] [net] [pf] »
» 332 166 91 89 54 35 214.79 -170.20 44.59 1.26 »
» [average_trade] [recovery] [max_profit] [max_loss] [sharpe]
» 0.27 1.01 7.58 -5.17 0.09
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
Báo cáo tester trong trường hợp này sẽ chứa dữ liệu tổng quát, vì vậy nhờ các lớp của chúng ta, chúng ta đã nhận được các chi tiết trước đây không thể truy cập.
Tuy nhiên, việc xem một báo cáo giả trong nhật ký không thực sự tiện lợi. Hơn nữa, tôi muốn thấy biểu diễn đồ họa của đường số dư, ít nhất là hình dạng của nó thường nói nhiều hơn về tính phù hợp của hệ thống so với các số liệu thống kê khô khan.
Hãy cải thiện Expert Advisor bằng cách cung cấp cho nó khả năng tạo báo cáo trực quan ở định dạng HTML: sau cùng, các báo cáo của tester cũng có thể được xuất sang HTML, lưu lại và so sánh theo thời gian. Ngoài ra, trong tương lai, các báo cáo như vậy có thể được truyền dưới dạng khung tới terminal ngay trong quá trình tối ưu hóa, và người dùng sẽ có thể bắt đầu nghiên cứu báo cáo của các lần chạy cụ thể ngay cả trước khi hoàn thành toàn bộ quá trình.
Đây sẽ là phiên bản gần cuối của ví dụ UnityMartingaleDraft3.mq5
.
UnityMartingaleDraft3.mq5
Việc trực quan hóa báo cáo giao dịch bao gồm đường số dư và bảng với các chỉ số thống kê. Chúng ta sẽ không tạo một báo cáo hoàn chỉnh tương tự như báo cáo của tester mà sẽ giới hạn ở các giá trị quan trọng được chọn. Mục đích của chúng ta là triển khai một cơ chế hoạt động, sau đó có thể được tùy chỉnh theo yêu cầu cá nhân.
Chúng ta sẽ sắp xếp nền tảng của thuật toán dưới dạng lớp TradeReportWriter
(TradeReportWriter.mqh
). Lớp này sẽ có khả năng lưu trữ số lượng báo cáo tùy ý từ các hệ thống giao dịch khác nhau: mỗi báo cáo trong một đối tượng riêng biệt DataHolder
, bao gồm các mảng giá trị số dư và dấu thời gian (data
và when
, tương ứng), cấu trúc stats
với các thống kê, cũng như tiêu đề, màu sắc và độ rộng của đường để hiển thị.
class TradeReportWriter
{
protected:
class DataHolder
{
public:
double data[]; // thay đổi số dư
datetime when[]; // dấu thời gian số dư
string name; // mô tả
color clr; // màu sắc
int width; // độ rộng đường
TradeReport::GenericStats stats; // chỉ số giao dịch
};
...
2
3
4
5
6
7
8
9
10
11
12
13
14
Chúng ta có một mảng các con trỏ tự động curves
được cấp phát cho các đối tượng của lớp DataHolder
. Ngoài ra, chúng ta sẽ cần các giới hạn chung về số tiền và thời hạn để khớp các đường của tất cả các hệ thống giao dịch trong hình ảnh. Điều này sẽ được cung cấp bởi các biến lower
, upper
, start
, và stop
.
AutoPtr<DataHolder> curves[];
double lower, upper;
datetime start, stop;
public:
TradeReportWriter(): lower(DBL_MAX), upper(-DBL_MAX), start(0), stop(0) { }
...
2
3
4
5
6
7
Phương thức addCurve
thêm một đường số dư.
virtual bool addCurve(double &data[], datetime &when[], const string name,
const color clr = clrNONE, const int width = 1)
{
if(ArraySize(data) == 0 || ArraySize(when) == 0) return false;
if(ArraySize(data) != ArraySize(when)) return false;
DataHolder *c = new DataHolder();
if(!ArraySwap(data, c.data) || !ArraySwap(when, c.when))
{
delete c;
return false;
}
const double max = c.data[ArrayMaximum(c.data)];
const double min = c.data[ArrayMinimum(c.data)];
lower = fmin(min, lower);
upper = fmax(max, upper);
if(start == 0) start = c.when[0];
else if(c.when[0] != 0) start = fmin(c.when[0], start);
stop = fmax(c.when[ArraySize(c.when) - 1], stop);
c.name = name;
c.clr = clr;
c.width = width;
ZeroMemory(c.stats); // không có thống kê mặc định
PUSH(curves, c);
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
Phiên bản thứ hai của phương thức addCurve
không chỉ thêm đường số dư mà còn thêm một tập hợp các biến tài chính trong cấu trúc GenericStats
.
virtual bool addCurve(TradeReport::GenericStats &stats,
double &data[], datetime &when[], const string name,
const color clr = clrNONE, const int width = 1)
{
if(addCurve(data, when, name, clr, width))
{
curves[ArraySize(curves) - 1][].stats = stats;
return true;
}
return false;
}
2
3
4
5
6
7
8
9
10
11
Phương thức quan trọng nhất của lớp, trực quan hóa báo cáo, được làm trừu tượng.
virtual void render() = 0;
Điều này cho phép triển khai nhiều cách hiển thị báo cáo, ví dụ, cả với việc ghi vào tệp ở các định dạng khác nhau, và với việc vẽ trực tiếp trên biểu đồ. Hiện tại chúng ta sẽ giới hạn ở việc tạo tệp HTML vì đây là phương pháp tiên tiến và phổ biến nhất.
Lớp mới HTMLReportWriter
có một hàm khởi tạo, các tham số của nó chỉ định tên tệp, cũng như kích thước của hình ảnh với các đường số dư. Chúng ta sẽ tạo chính hình ảnh đó ở định dạng đồ họa vector SVG nổi tiếng: nó lý tưởng trong trường hợp này vì nó là một tập hợp con của ngôn ngữ XML, cũng chính là HTML.
class HTMLReportWriter: public TradeReportWriter
{
int handle;
int width, height;
public:
HTMLReportWriter(const string name, const int w = 600, const int h = 400):
width(w), height(h)
{
handle = FileOpen(name,
FILE_WRITE | FILE_TXT | FILE_ANSI | FILE_REWRITE);
}
~HTMLReportWriter()
{
if(handle != 0) FileClose(handle);
}
void close()
{
if(handle != 0) FileClose(handle);
handle = 0;
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Trước khi chuyển sang phương thức công khai chính render
, cần giới thiệu cho người đọc một công nghệ, sẽ được mô tả chi tiết trong Phần 7 cuối cùng của sách. Chúng ta đang nói về tài nguyên: các tệp và mảng dữ liệu tùy ý được kết nối với chương trình MQL để làm việc với đa phương tiện (âm thanh và hình ảnh), nhúng các chỉ báo đã biên dịch, hoặc đơn giản là kho lưu trữ thông tin ứng dụng. Chính tùy chọn cuối cùng mà chúng ta sẽ sử dụng bây giờ.
Vấn đề là tốt hơn nên tạo trang HTML không hoàn toàn từ mã MQL, mà dựa trên một mẫu (mẫu trang), trong đó mã MQL chỉ chèn giá trị của một số biến. Đây là một kỹ thuật nổi tiếng trong lập trình cho phép tách biệt thuật toán và cách biểu diễn bên ngoài của chương trình (hoặc kết quả công việc của nó). Nhờ đó, chúng ta có thể thử nghiệm riêng với mẫu HTML và mã MQL, làm việc với từng thành phần trong môi trường quen thuộc. Cụ thể, MetaEditor vẫn chưa thực sự phù hợp để chỉnh sửa và xem các trang web, cũng như trình duyệt tiêu chuẩn không biết gì về MQL5 (mặc dù điều này có thể được sửa).
Chúng ta sẽ lưu trữ các mẫu báo cáo HTML trong các tệp văn bản được kết nối với mã nguồn MQL5 dưới dạng tài nguyên. Việc kết nối được thực hiện bằng chỉ thị đặc biệt #resource
. Ví dụ, có dòng sau trong tệp TradeReportWriter.mqh
.
#resource "TradeReportPage.htm" as string ReportPageTemplate
Nó có nghĩa là bên cạnh mã nguồn phải có tệp TradeReportPage.htm
, sẽ trở nên khả dụng trong mã MQL dưới dạng chuỗi ReportPageTemplate
. Qua phần mở rộng, bạn có thể hiểu rằng tệp này là một trang web. Dưới đây là nội dung của tệp này với các phần rút gọn (chúng ta không có nhiệm vụ dạy người đọc về phát triển web, mặc dù rõ ràng kiến thức trong lĩnh vực này cũng có thể hữu ích cho một nhà giao dịch).
<!DOCTYPE html>
<html>
<head>
<title>Báo cáo giao dịch</title>
<style>
*{font: 9pt "Segoe UI";}
.center{width:fit-content;margin:0 auto;}
...
</style>
</head>
<body>
<div class="center">
<h1>Báo cáo giao dịch</h1>
~
</div>
</body>
<script>
...
</script>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Cơ sở của các mẫu được chọn bởi nhà phát triển. Có rất nhiều hệ thống mẫu HTML sẵn có, nhưng chúng cung cấp nhiều tính năng dư thừa và do đó quá phức tạp cho ví dụ của chúng ta. Chúng ta sẽ phát triển khái niệm riêng của mình.
Để bắt đầu, hãy lưu ý rằng hầu hết các trang web có phần đầu (header), phần cuối (footer), và thông tin hữu ích nằm giữa chúng. Bản nháp báo cáo trên không phải là ngoại lệ theo nghĩa này. Nó sử dụng ký tự '~' để chỉ nội dung hữu ích. Thay vào đó, mã MQL sẽ phải chèn hình ảnh số dư và bảng với các chỉ số. Nhưng sự hiện diện của '~' không bắt buộc, vì trang có thể là một khối thống nhất, tức là chính phần giữa hữu ích: sau cùng, mã MQL có thể, nếu cần, chèn kết quả xử lý một mẫu vào mẫu khác.
Để hoàn thành phần nói về mẫu HTML, hãy chú ý thêm một điều nữa. Về lý thuyết, một trang web bao gồm các thẻ thực hiện các chức năng khác nhau cơ bản. Các thẻ HTML tiêu chuẩn cho trình duyệt biết phải hiển thị gì. Ngoài ra, còn có các kiểu xếp tầng (CSS), mô tả cách hiển thị nó. Cuối cùng, trang có thể có thành phần động dưới dạng các kịch bản JavaScript kiểm soát cả hai yếu tố trên một cách tương tác.
Thông thường, ba thành phần này được tạo mẫu độc lập, tức là, ví dụ, một mẫu HTML, nghiêm ngặt mà nói, chỉ nên chứa HTML chứ không phải CSS hay JavaScript. Điều này cho phép
"tách rời nội dung, giao diện và hành vi"
của trang web, giúp việc phát triển dễ dàng hơn (nên áp dụng cách tiếp cận tương tự trong MQL5!).Tuy nhiên, trong ví dụ của chúng ta, chúng ta đã bao gồm tất cả các thành phần trong mẫu. Đặc biệt, trong mẫu trên, chúng ta thấy thẻ
<style>
với các kiểu CSS và thẻ<script>
với một số hàm JavaScript, đã bị lược bỏ. Điều này được thực hiện để đơn giản hóa ví dụ, tập trung vào các tính năng MQL5 hơn là phát triển web.
Có một mẫu trang web trong biến ReportPageTemplate
được kết nối dưới dạng tài nguyên, chúng ta có thể viết phương thức render
.
virtual void render() override
{
string headerAndFooter[2];
StringSplit(ReportPageTemplate, '~', headerAndFooter);
FileWriteString(handle, headerAndFooter[0]);
renderContent();
FileWriteString(handle, headerAndFooter[1]);
}
...
2
3
4
5
6
7
8
9
Nó thực sự chia trang thành hai nửa trên và dưới bằng ký tự '~', hiển thị chúng như nguyên bản, và gọi một phương thức trợ giúp renderContent
ở giữa.
Chúng ta đã mô tả rằng báo cáo sẽ bao gồm một hình ảnh tổng quát với các đường số dư và bảng với các chỉ số của các hệ thống giao dịch, vì vậy việc triển khai renderContent
là tự nhiên.
private:
void renderContent()
{
renderSVG();
renderTables();
}
2
3
4
5
6
Việc tạo hình ảnh bên trong renderSVG
dựa trên một tệp mẫu khác TradeReportSVG.htm
, được liên kết với một biến chuỗi SVGBoxTemplate
:
#resource "TradeReportSVG.htm" as string SVGBoxTemplate
Nội dung của mẫu này là mẫu cuối cùng chúng ta liệt kê ở đây. Những ai muốn có thể tự xem mã nguồn của các mẫu còn lại.
<span id="params" style="display:block;width:%WIDTH%px;text-align:center;"></span>
<a id="main" style="display:block;text-align:center;">
<svg width="%WIDTH%" height="%HEIGHT%" xmlns="http://www.w3.org/2000/svg">
<style>.legend {font: bold 11px Consolas;}</style>
<rect x="0" y="0" width="%WIDTH%" height="%HEIGHT%"
style="fill:none; stroke-width:1; stroke: black;"/>
~
</svg>
</a>
2
3
4
5
6
7
8
9
Trong mã của phương thức renderSVG
, chúng ta sẽ thấy thủ thuật quen thuộc là chia nội dung thành hai khối "trước" và "sau" ký tự '~', nhưng có điều gì đó mới ở đây.
void renderSVG()
{
string headerAndFooter[2];
if(StringSplit(SVGBoxTemplate, '~', headerAndFooter) != 2) return;
StringReplace(headerAndFooter[0], "%WIDTH%", (string)width);
StringReplace(headerAndFooter[0], "%HEIGHT%", (string)height);
FileWriteString(handle, headerAndFooter[0]);
for(int i = 0; i < ArraySize(curves); ++i)
{
renderCurve(i, curves[i][].data, curves[i][].when,
curves[i][].name, curves[i][].clr, curves[i][].width);
}
FileWriteString(handle, headerAndFooter[1]);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Ở đầu trang, trong chuỗi headerAndFooter[0]
, chúng ta tìm kiếm các chuỗi con có dạng đặc biệt "%WIDTH%" và "%HEIGHT%", và thay thế chúng bằng chiều rộng và chiều cao cần thiết của hình ảnh. Đây là nguyên tắc thay thế giá trị hoạt động trong các mẫu của chúng ta. Ví dụ, trong mẫu này, các chuỗi con này thực sự xuất hiện trong thẻ rect
:
<rect x="0" y="0" width="%WIDTH%" height="%HEIGHT%" style="fill:none; stroke-width:1; stroke: black;"/>
Do đó, nếu báo cáo được yêu cầu với kích thước 600 x 400, dòng này sẽ được chuyển đổi thành như sau:
<rect x="0" y="0" width="600" height="400" style="fill:none; stroke-width:1; stroke: black;"/>
Điều này sẽ hiển thị một đường viền màu đen dày 1 pixel với kích thước được chỉ định trong trình duyệt.
Việc tạo các thẻ để vẽ các đường số dư cụ thể được xử lý bởi phương thức renderCurve
, mà chúng ta truyền tất cả các mảng cần thiết và các cài đặt khác (tên, màu sắc và độ dày). Chúng ta sẽ để phương thức này và các phương thức chuyên biệt khác (renderTables
, renderTable
) cho việc tự học.
Hãy quay lại mô-đun chính của Expert Advisor UnityMartingaleDraft3.mq5
. Đặt kích thước của hình ảnh các biểu đồ số dư và kết nối TradeReportWriter.mqh
.
#define MINIWIDTH 400
#define MINIHEIGHT 200
#include <MQL5Book/TradeReportWriter.mqh>
2
3
4
Để "kết nối" các chiến lược với trình tạo báo cáo, bạn sẽ cần sửa đổi phương thức statement
trong giao diện TradingStrategy
: truyền một con trỏ tới đối tượng TradeReportWriter
, mà mã gọi có thể tạo và cấu hình.
interface TradingStrategy
{
virtual bool trade(void);
virtual bool statement(TradeReportWriter *writer = NULL);
};
2
3
4
5
Bây giờ, hãy thêm một số dòng vào triển khai cụ thể của phương thức này trong lớp chiến lược UnityMartingale
của chúng ta.
class UnityMartingale: public TradingStrategy
{
...
TradeReport report;
...
virtual bool statement(TradeReportWriter *writer = NULL) override
{
if(MQLInfoInteger(MQL_TESTER))
{
...
// đã được thực hiện rồi
DealFilter filter;
filter.let(DEAL_SYMBOL, settings.symbol)
.let(DEAL_MAGIC, settings.magic, IS::EQUAL_OR_ZERO);
HistorySelect(0, LONG_MAX);
TradeReport::GenericStats stats =
report.calcStatistics(filter, deposit, epoch);
...
// thêm phần này
if(CheckPointer(writer) != POINTER_INVALID)
{
double data[]; // giá trị số dư
datetime time[]; // thời điểm số dư để đồng bộ hóa các đường cong
report.getCurve(data, time); // điền vào mảng và chuyển để ghi vào tệp
return writer.addCurve(stats, data, time, settings.symbol);
}
return true;
}
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
Tất cả đều xoay quanh việc lấy một mảng số dư và một cấu trúc với các chỉ số từ đối tượng report
(lớp TradeReport
) và truyền sang đối tượng TradeReportWriter
, gọi addCurve
.
Tất nhiên, tập hợp các chiến lược giao dịch đảm bảo việc truyền cùng một đối tượng TradeReportWriter
đến tất cả các chiến lược để tạo một báo cáo tổng hợp.
class TradingStrategyPool: public TradingStrategy
{
...
virtual bool statement(TradeReportWriter *writer = NULL) override
{
bool result = false;
for(int i = 0; i < ArraySize(pool); i++)
{
result = pool[i][].statement(writer) || result;
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
Cuối cùng, trình xử lý OnTester
đã trải qua sự thay đổi lớn nhất. Các dòng sau sẽ đủ để tạo một báo cáo HTML về các chiến lược giao dịch.
double OnTester()
{
...
const static string tempfile = "temp.html";
HTMLReportWriter writer(tempfile, MINIWIDTH, MINIHEIGHT);
if(pool[] != NULL)
{
pool[].statement(&writer); // yêu cầu các chiến lược báo cáo kết quả của chúng
}
writer.render(); // ghi dữ liệu nhận được vào tệp
writer.close();
}
2
3
4
5
6
7
8
9
10
11
12
Tuy nhiên, để rõ ràng và thuận tiện cho người dùng, sẽ tuyệt vời nếu thêm vào báo cáo một đường cong số dư tổng quát, cũng như một bảng với các chỉ số tổng quát. Điều này chỉ có ý nghĩa khi xuất ra chúng khi nhiều biểu tượng được chỉ định trong cài đặt Expert Advisor vì nếu không, báo cáo của một chiến lược sẽ trùng với báo cáo tổng quát trong tệp.
Điều này đòi hỏi thêm một chút mã.
double OnTester()
{
...
// đã có trước đó
DealFilter filter;
// thiết lập bộ lọc và điền vào mảng các giao dịch dựa trên vé của nó
...
const int n = ArraySize(tickets);
// thêm phần này
const bool singleSymbol = WorkSymbols == "";
double curve[]; // đường cong số dư tổng cộng
datetime stamps[]; // ngày và giờ của các điểm số dư tổng cộng
if(!singleSymbol) // số dư tổng cộng chỉ được hiển thị nếu có nhiều biểu tượng/chiến lược
{
ArrayResize(curve, n + 1);
ArrayResize(stamps, n + 1);
curve[0] = TesterStatistics(STAT_INITIAL_DEPOSIT);
// MQL5 không cho phép biết thời gian bắt đầu kiểm tra,
// điều này có thể được tìm ra từ giao dịch đầu tiên,
// nhưng nó nằm ngoài điều kiện lọc của một hệ thống cụ thể,
// vì vậy hãy đồng ý bỏ qua thời gian 0 trong tính toán
stamps[0] = 0;
}
for(int i = 0; i < n; ++i) // chu kỳ giao dịch
{
double result = 0;
for(int j = 0; j < STAT_PROPS - 1; ++j)
{
result += expenses[i][j];
}
if(!singleSymbol)
{
curve[i + 1] = result + curve[i];
stamps[i + 1] = (datetime)HistoryDealGetInteger(tickets[i], DEAL_TIME);
}
...
}
if(!singleSymbol) // gửi thống kê của tester và đường cong tổng thể vào báo cáo
{
TradeReport::GenericStats stats;
stats.fillByTester();
writer.addCurve(stats, curve, stamps, "Overall", clrBlack, 3);
}
...
}
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
Hãy xem chúng ta đã đạt được gì. Nếu chúng ta chạy Expert Advisor với cài đặt UnityMartingale-combo.set
, chúng ta sẽ có tệp temp.html
trong thư mục MQL5/Files
của một trong các tác nhân. Đây là cách nó trông trong trình duyệt.
Báo cáo HTML cho Expert Advisor với nhiều chiến lược giao dịch/biểu tượng
Bây giờ khi chúng ta biết cách tạo báo cáo cho một lần kiểm tra, chúng ta có thể gửi chúng đến terminal trong quá trình tối ưu hóa, chọn những báo cáo tốt nhất ngay lập tức và trình bày chúng cho người dùng trước khi toàn bộ quá trình kết thúc. Tất cả các báo cáo sẽ được đặt trong một thư mục riêng bên trong MQL5/Files
của terminal. Thư mục sẽ nhận được một tên chứa biểu tượng và khung thời gian từ cài đặt của tester, cũng như tên của Expert Advisor.
UnityMartingale.mq5
Như chúng ta biết, để gửi một tệp đến terminal, chỉ cần gọi hàm FrameAdd
. Chúng ta đã tạo tệp trong khuôn khổ của phiên bản trước.
double OnTester()
{
...
if(MQLInfoInteger(MQL_OPTIMIZATION))
{
FrameAdd(tempfile, 0, r2 * 100, tempfile);
}
}
2
3
4
5
6
7
8
Trong phiên bản Expert Advisor nhận được, chúng ta sẽ thực hiện các bước chuẩn bị cần thiết. Hãy mô tả cấu trúc Pass
với các thông số chính của mỗi lần tối ưu hóa.
struct Pass
{
ulong id; // số lần chạy
double value; // giá trị tiêu chí tối ưu hóa
string parameters; // các thông số tối ưu hóa dưới dạng danh sách 'name=value'
string preset; // văn bản để tạo tệp cài đặt (với tất cả các thông số)
};
2
3
4
5
6
7
Trong các chuỗi parameters
, các cặp "name=value" được nối với ký hiệu '&'. Điều này sẽ hữu ích cho việc tương tác của các trang web báo cáo trong tương lai (ký hiệu '&' là tiêu chuẩn để kết hợp các thông số trong địa chỉ web). Chúng ta chưa mô tả định dạng của các tệp cài đặt, nhưng mã nguồn sau đây hình thành chuỗi preset
cho phép bạn nghiên cứu vấn đề này trong thực tế.
Khi các khung đến, chúng ta sẽ ghi lại các cải tiến theo tiêu chí tối ưu hóa vào mảng TopPasses
. Lần chạy tốt nhất hiện tại sẽ luôn là lần chạy cuối cùng trong mảng và cũng có sẵn trong biến BestPass
.
Pass TopPasses[]; // ngăn xếp các lần chạy cải thiện liên tục (lần cuối là tốt nhất)
Pass BestPass; // lần chạy tốt nhất hiện tại
string ReportPath; // thư mục dành riêng cho tất cả các tệp html của lần tối ưu hóa này
2
3
Trong trình xử lý OnTesterInit
, hãy tạo tên thư mục.
void OnTesterInit()
{
BestPass.value = -DBL_MAX;
ReportPath = _Symbol + "-" + PeriodToString(_Period) + "-"
+ MQLInfoString(MQL_PROGRAM_NAME) + "/";
}
2
3
4
5
6
Trong trình xử lý OnTesterPass
, chúng ta sẽ lần lượt chọn chỉ những khung mà chỉ số đã được cải thiện, tìm giá trị của các thông số tối ưu hóa và các thông số khác cho chúng, và thêm tất cả thông tin này vào mảng cấu trúc Pass
.
void OnTesterPass()
{
ulong pass;
string name;
long id;
double value;
uchar data[];
// thông số đầu vào cho lần chạy tương ứng với khung hiện tại
string params[];
uint count;
while(FrameNext(pass, name, id, value, data))
{
// thu thập các lần chạy với số liệu cải thiện
if(value > BestPass.value && FrameInputs(pass, params, count))
{
BestPass.preset = "";
BestPass.parameters = "";
// lấy các thông số tối ưu hóa và các thông số khác để tạo tệp cài đặt
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(StringLen(BestPass.parameters)) BestPass.parameters += "&";
BestPass.parameters += params[i];
}
BestPass.preset += params[i] + "||" + (string)pstart + "||"
+ (string)pstep + "||" + (string)pstop + "||"
+ (enabled ? "Y" : "N") + "<br>\n";
}
else
{
BestPass.preset += params[i] + "<br>\n";
}
}
}
BestPass.value = value;
BestPass.id = pass;
PUSH(TopPasses, BestPass);
// ghi khung với báo cáo vào tệp HTML
const string text = CharArrayToString(data);
int handle = FileOpen(StringFormat(ReportPath + "%06.3f-%lld.htm", value, pass),
FILE_WRITE | FILE_TXT | FILE_ANSI);
FileWriteString(handle, text);
FileClose(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
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
57
Các báo cáo kết quả với các cải tiến được lưu trong các tệp có tên bao gồm giá trị của tiêu chí tối ưu hóa và số lần chạy.
Bây giờ đến phần thú vị nhất. Trong trình xử lý OnTesterDeinit
, chúng ta có thể tạo một tệp HTML chung (overall.htm
), cho phép xem tất cả các báo cáo cùng một lúc (hoặc, giả sử, top 100). Nó sử dụng cùng một sơ đồ với các mẫu mà chúng ta đã đề cập trước đó.
#resource "OptReportPage.htm" as string OptReportPageTemplate
#resource "OptReportElement.htm" as string OptReportElementTemplate
void OnTesterDeinit()
{
int handle = FileOpen(ReportPath + "overall.htm",
FILE_WRITE | FILE_TXT | FILE_ANSI, 0, CP_UTF8);
string headerAndFooter[2];
StringSplit(OptReportPageTemplate, '~', headerAndFooter);
StringReplace(headerAndFooter[0], "%MINIWIDTH%", (string)MINIWIDTH);
StringReplace(headerAndFooter[0], "%MINIHEIGHT%", (string)MINIHEIGHT);
FileWriteString(handle, headerAndFooter[0]);
// đọc không quá 100 bản ghi tốt nhất từ TopPasses
for(int i = ArraySize(TopPasses) - 1, k = 0; i >= 0 && k < 100; --i, ++k)
{
string p = TopPasses[i].parameters;
StringReplace(p, "&", " ");
const string filename = StringFormat("%06.3f-%lld.htm",
TopPasses[i].value, TopPasses[i].id);
string element = OptReportElementTemplate;
StringReplace(element, "%FILENAME%", filename);
StringReplace(element, "%PARAMETERS%", TopPasses[i].parameters);
StringReplace(element, "%PARAMETERS_SPACED%", p);
StringReplace(element, "%PASS%", IntegerToString(TopPasses[i].id));
StringReplace(element, "%PRESET%", TopPasses[i].preset);
StringReplace(element, "%MINIWIDTH%", (string)MINIWIDTH);
StringReplace(element, "%MINIHEIGHT%", (string)MINIHEIGHT);
FileWriteString(handle, element);
}
FileWriteString(handle, headerAndFooter[1]);
FileClose(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
31
32
Hình ảnh sau đây cho thấy trang web tổng quan trông như thế nào sau khi tối ưu hóa UnityMartingale.mq5
theo tham số UnityPricePeriod
trong chế độ đa tiền tệ.
Trang web tổng quan với các báo cáo giao dịch của các lần tối ưu hóa tốt nhất
Đối với mỗi báo cáo, chúng ta chỉ hiển thị phần trên cùng, nơi có biểu đồ số dư. Phần này là thuận tiện nhất để đưa ra đánh giá chỉ bằng cách nhìn vào nó.
Danh sách các tham số tối ưu hóa (name=value&name=value...
) được hiển thị phía trên mỗi biểu đồ. Nhấn vào một dòng sẽ mở ra một khối chứa văn bản cho tệp cài đặt của tất cả các thiết lập của lần chạy này. Nếu bạn nhấp vào trong khối, nội dung của nó sẽ được sao chép vào clipboard. Nó có thể được lưu trong một trình soạn thảo văn bản và do đó nhận được một tệp cài đặt sẵn sàng.
Nhấp vào biểu đồ sẽ đưa bạn đến trang báo cáo cụ thể, cùng với các bảng điểm (được đưa ra ở trên).
Cuối phần này, chúng ta đề cập đến một câu hỏi nữa. Trước đó chúng ta đã hứa sẽ trình bày hiệu quả của hàm TesterHideIndicators
. Expert Advisor UnityMartingale.mq5
hiện đang sử dụng chỉ báo UnityPercentEvent.mq5
. Sau bất kỳ bài kiểm tra nào, chỉ báo được hiển thị trên biểu đồ mở. Giả sử rằng chúng ta muốn giấu người dùng cơ chế hoạt động của Expert Advisor và nơi nó lấy tín hiệu. Khi đó bạn có thể gọi hàm TesterHideIndicators
(với tham số true
) trong trình xử lý OnInit
, trước khi tạo đối tượng UnityController
, trong đó mô tả được nhận qua iCustom
.
int OnInit()
{
...
TesterHideIndicators(true);
...
controller = new UnityController(UnitySymbols, barwise,
UnityBarLimit, UnityPriceType, UnityPriceMethod, UnityPricePeriod);
return INIT_SUCCEEDED;
}
2
3
4
5
6
7
8
9
Phiên bản này của Expert Advisor sẽ không còn hiển thị chỉ báo trên biểu đồ. Tuy nhiên, nó không được giấu quá kỹ. Nếu chúng ta xem xét nhật ký của tester, chúng ta sẽ thấy các dòng về các chương trình đã tải giữa rất nhiều thông tin hữu ích: đầu tiên, một thông báo về việc tải Expert Advisor, và sau đó một chút, về việc tải chỉ báo.
...
expert file added: Experts\MQL5Book\p6\UnityMartingale.ex5.
...
program file added: \Indicators\MQL5Book\p6\UnityPercentEvent.ex5.
...
2
3
4
5
Do đó, một người dùng cẩn thận có thể tìm ra tên của chỉ báo. Khả năng này có thể được loại bỏ bằng cơ chế tài nguyên, mà chúng ta đã đề cập qua loa trong ngữ cảnh của các mẫu trang web. Hóa ra, chỉ báo đã biên dịch cũng có thể được nhúng vào một chương trình MQL (trong Expert Advisor hoặc một chỉ báo khác) như một tài nguyên. Và các chương trình tài nguyên như vậy không còn được đề cập trong nhật ký của tester. Chúng ta sẽ nghiên cứu chi tiết về tài nguyên trong Phần 7 của cuốn sách, và bây giờ chúng ta sẽ hiển thị các dòng liên quan đến chúng trong phiên bản cuối cùng của Expert Advisor của chúng ta.
Trước hết, hãy mô tả tài nguyên với chỉ thị #resource
. Thực tế, nó chỉ chứa đường dẫn đến tệp chỉ báo đã biên dịch (rõ ràng, nó phải được biên dịch trước), và ở đây bắt buộc phải sử dụng dấu gạch chéo ngược đôi làm dấu phân cách vì các dấu gạch chéo đơn trong đường dẫn tài nguyên không được hỗ trợ.
#resource "\\Indicators\\MQL5Book\\p6\\UnityPercentEvent.ex5"
Sau đó, trong các dòng với lệnh gọi iCustom
, chúng ta thay thế toán tử trước đó:
UnityController(const string symbolList, const int offset, const int limit,
const ENUM_APPLIED_PRICE type, const ENUM_MA_METHOD method, const int period):
bar(offset), tickwise(!offset)
{
handle = iCustom(_Symbol, _Period,
"MQL5Book/p6/UnityPercentEvent", // <---
symbolList, limit, type, method, period);
...
2
3
4
5
6
7
8
Bằng cách tương tự, nhưng với một liên kết đến tài nguyên (lưu ý cú pháp với cặp dấu hai chấm ::
ở đầu, điều này là cần thiết để phân biệt giữa các đường dẫn thông thường trong hệ thống tệp và các đường dẫn trong tài nguyên).
UnityController(const string symbolList, const int offset, const int limit,
const ENUM_APPLIED_PRICE type, const ENUM_MA_METHOD method复, const int period):
bar(offset), tickwise(!offset)
{
handle = iCustom(_Symbol, _Period,
"::Indicators\\MQL5Book\\p6\\UnityPercentEvent.ex5", // <---
symbolList, limit, type, method, period);
...
2
3
4
5
6
7
8
Bây giờ phiên bản đã biên dịch của Expert Advisor có thể được cung cấp cho người dùng một cách độc lập, mà không cần chỉ báo riêng biệt, vì nó được ẩn bên trong Expert Advisor. Điều này không ảnh hưởng đến hiệu suất của nó theo bất kỳ cách nào, nhưng với thách thức của TesterHideIndicators
, thiết bị bên trong được giấu đi. Nên nhớ rằng nếu chỉ báo sau đó được cập nhật, Expert Advisor cũng sẽ cần được biên dịch lại.