Trailing Stop
Một trong những nhiệm vụ phổ biến nhất sử dụng khả năng thay đổi mức giá bảo vệ là dịch chuyển tuần tự Stop Loss
đến một mức giá tốt hơn khi xu hướng thuận lợi tiếp tục. Đây chính là trailing stop. Chúng ta triển khai nó bằng cách sử dụng các cấu trúc mới MqlTradeRequestSync
và MqlTradeResultSync
từ các phần trước.
Để có thể kết nối cơ chế này với bất kỳ Expert Advisor nào, hãy khai báo nó dưới dạng lớp Trailing Stop
(xem tệp TrailingStop.mqh
). Chúng ta sẽ lưu trữ số vé của vị thế được kiểm soát, biểu tượng của nó, kích thước điểm giá, cũng như khoảng cách cần thiết của mức stop loss từ giá hiện tại, và bước thay đổi mức trong các biến riêng của lớp.
#include <MQL5Book/MqlTradeSync.mqh>
class TrailingStop
{
const ulong ticket; // vé của vị thế được kiểm soát
const string symbol; // biểu tượng vị thế
const double point; // kích thước điểm giá của biểu tượng
const uint distance; // khoảng cách đến stop tính bằng điểm
const uint step; // bước di chuyển (độ nhạy) tính bằng điểm
...
};
2
3
4
5
6
7
8
9
10
11
Khoảng cách chỉ cần thiết cho thuật toán theo dõi vị thế tiêu chuẩn được cung cấp bởi lớp cơ sở. Các lớp dẫn xuất sẽ có thể di chuyển mức bảo vệ theo các nguyên tắc khác, chẳng hạn như đường trung bình động, kênh giá, chỉ báo SAR, và các nguyên tắc khác. Sau khi làm quen với lớp cơ sở, chúng ta sẽ đưa ra một ví dụ về lớp dẫn xuất với đường trung bình động.
Hãy tạo biến level
cho mức giá dừng hiện tại. Trong biến ok
, chúng ta sẽ duy trì trạng thái hiện tại của vị thế: true
nếu vị thế vẫn tồn tại và false
nếu xảy ra lỗi và vị thế đã bị đóng.
protected:
double level;
bool ok;
virtual double detectLevel()
{
return DBL_MAX;
}
2
3
4
5
6
7
Phương thức ảo detectLevel
được thiết kế để ghi đè trong các lớp con, nơi mức giá dừng nên được tính toán theo một thuật toán tùy ý. Trong triển khai này, một giá trị đặc biệt DBL_MAX
được trả về, cho biết hoạt động theo thuật toán tiêu chuẩn (xem bên dưới).
Trong hàm khởi tạo, điền tất cả các trường với các giá trị của các tham số tương ứng. Hàm PositionSelectByTicket
kiểm tra sự tồn tại của một vị thế với vé đã cho và phân bổ nó trong môi trường chương trình để cuộc gọi tiếp theo của PositionGetString
trả về thuộc tính chuỗi của nó với tên biểu tượng.
public:
TrailingStop(const ulong t, const uint d, const uint s = 1) :
ticket(t), distance(d), step(s),
symbol(PositionSelectByTicket(t) ? PositionGetString(POSITION_SYMBOL) : NULL),
point(SymbolInfoDouble(symbol, SYMBOL_POINT))
{
if(symbol == NULL)
{
Print("Position not found: " + (string)t);
ok = false;
}
else
{
ok = true;
}
}
bool isOK() const
{
return ok;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Bây giờ, hãy xem xét phương thức công khai chính của lớp trail
. Chương trình MQL sẽ cần gọi nó trên mỗi tick hoặc theo bộ đếm thời gian để theo dõi vị thế. Phương thức trả về true
trong khi vị thế còn tồn tại.
virtual bool trail()
{
if(!PositionSelectByTicket(ticket))
{
ok = false;
return false; // vị thế đã đóng
}
// tìm hiểu giá để tính toán: báo giá hiện tại và mức dừng
const double current = PositionGetDouble(POSITION_PRICE_CURRENT);
const double sl = PositionGetDouble(POSITION_SL);
...
2
3
4
5
6
7
8
9
10
11
12
Ở đây và dưới đây, chúng ta sử dụng các hàm đọc thuộc tính vị thế. Chúng sẽ được thảo luận chi tiết trong một phần riêng. Đặc biệt, chúng ta cần tìm hiểu hướng giao dịch — mua và bán — để biết mức dừng nên được đặt theo hướng nào.
// POSITION_TYPE_BUY = 0 (false)
// POSITION_TYPE_SELL = 1 (true)
const bool sell = (bool)PositionGetInteger(POSITION_TYPE);
TU::TradeDirection dir(sell);
...
2
3
4
5
Để tính toán và kiểm tra, chúng ta sẽ sử dụng lớp trợ giúp TU::TradeDirection
và đối tượng của nó dir
. Ví dụ, phương thức negative
của nó cho phép tính toán giá nằm ở khoảng cách đã chỉ định từ giá hiện tại theo hướng thua lỗ, bất kể loại giao dịch. Điều này đơn giản hóa mã vì nếu không, bạn sẽ phải thực hiện các tính toán "đối xứng" cho mua và bán.
level = detectLevel();
// không thể trailing mà không có mức: việc xóa mức dừng phải được thực hiện bởi mã gọi
if(level == 0) return true;
// nếu có giá trị mặc định, thực hiện offset tiêu chuẩn từ giá hiện tại
if(level == DBL_MAX) level = dir.negative(current, point * distance);
level = TU::NormalizePrice(level, symbol);
if(!dir.better(current, level))
{
return true; // không thể đặt mức dừng ở phía có lợi nhuận
}
...
2
3
4
5
6
7
8
9
10
11
12
Phương thức better
của lớp TU::TradeDirection
kiểm tra rằng mức dừng nhận được nằm ở phía đúng của giá. Nếu không có phương thức này, chúng ta sẽ cần viết lại kiểm tra hai lần (cho mua và bán).
Chúng ta có thể nhận được giá trị mức dừng không chính xác vì phương thức detectLevel
có thể được ghi đè trong các lớp dẫn xuất. Với tính toán tiêu chuẩn, vấn đề này được loại bỏ vì mức được tính toán bởi đối tượng dir
.
Cuối cùng, khi mức được tính toán, cần áp dụng nó vào vị thế. Nếu vị thế chưa có stop loss, bất kỳ mức hợp lệ nào cũng được. Nếu stop loss đã được đặt, thì giá trị mới nên tốt hơn giá trị trước đó và khác biệt lớn hơn bước đã chỉ định.
if(sl == 0)
{
PrintFormat("Initial SL: %f", level);
move(level);
}
else
{
if(dir.better(level, sl) && fabs(level - sl) >= point * step)
{
PrintFormat("SL: %f -> %f", sl, level);
move(level);
}
}
return true; // thành công
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Việc gửi yêu cầu sửa đổi vị thế được triển khai trong phương thức move
, sử dụng phương thức quen thuộc adjust
của cấu trúc MqlTradeRequestSync
(xem phần Sửa đổi mức Stop Loss và/hoặc Take Profit).
bool move(const double sl)
{
MqlTradeRequestSync request;
request.position = ticket;
if(request.adjust(sl, 0) && request.completed())
{
Print("OK Trailing: ", TU::StringOf(sl));
return true;
}
return false;
}
};
2
3
4
5
6
7
8
9
10
11
12
Bây giờ mọi thứ đã sẵn sàng để thêm trailing vào Expert Advisor thử nghiệm TrailingStop.mq5
. Trong các tham số đầu vào, bạn có thể chỉ định hướng giao dịch, khoảng cách đến mức dừng tính bằng điểm và bước tính bằng điểm. Tham số TrailingDistance
mặc định bằng 0, nghĩa là tự động tính toán phạm vi hàng ngày của báo giá và sử dụng một nửa của nó làm khoảng cách.
#include <MQL5Book/MqlTradeSync.mqh>
#include <MQL5Book/TrailingStop.mqh>
enum ENUM_ORDER_TYPE_MARKET
{
MARKET_BUY = ORDER_TYPE_BUY, // ORDER_TYPE_BUY
MARKET_SELL = ORDER_TYPE_SELL // ORDER_TYPE_SELL
};
input int TrailingDistance = 0; // Khoảng cách đến Stop Loss tính bằng điểm (0 = tự động phát hiện)
input int TrailingStep = 10; // Bước Trailing tính bằng điểm
input ENUM_ORDER_TYPE_MARKET Type;
input string Comment;
input ulong Deviation;
input ulong Magic = 1234567890;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Khi khởi chạy, Expert Advisor sẽ tìm xem có vị thế nào trên biểu tượng hiện tại với số Magic
đã chỉ định hay không và sẽ tạo nó nếu không tồn tại.
Trailing sẽ được thực hiện bởi một đối tượng của lớp TrailingStop
được bọc trong một con trỏ thông minh AutoPtr
. Nhờ cái sau, chúng ta không cần xóa thủ công đối tượng cũ khi cần một đối tượng theo dõi mới để thay thế cho vị thế mới được tạo. Khi một đối tượng mới được gán cho con trỏ thông minh, đối tượng cũ sẽ tự động bị xóa. Hãy nhớ rằng việc giải tham chiếu một con trỏ thông minh, tức là truy cập vào đối tượng công việc được lưu trữ bên trong, được thực hiện bằng cách sử dụng toán tử [] được nạp chồng.
#include <MQL5Book/AutoPtr.mqh>
AutoPtr<TrailingStop> tr;
2
3
Trong trình xử lý OnTick
, chúng ta kiểm tra xem có đối tượng nào không. Nếu có, kiểm tra xem vị thế có tồn tại không (thuộc tính được trả về từ phương thức trail
). Ngay sau khi chương trình khởi động, đối tượng không có và con trỏ là NULL
. Trong trường hợp này, bạn nên tạo một vị thế mới hoặc tìm một vị thế đã mở và tạo một đối tượng Trailing Stop
cho nó. Điều này được thực hiện bởi hàm Setup
. Trong các lần gọi tiếp theo của OnTick
, đối tượng bắt đầu và tiếp tục theo dõi, ngăn chương trình đi vào khối if
trong khi vị thế còn "sống".
void OnTick()
{
if(tr[] == NULL || !tr[].trail())
{
// nếu chưa có trailing, tạo hoặc tìm một vị thế phù hợp
Setup();
}
}
2
3
4
5
6
7
8
Và đây là hàm Setup
.
void Setup()
{
int distance = 0;
const double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(TrailingDistance == 0) // tự động phát hiện phạm vi giá hàng ngày
{
distance = (int)((iHigh(_Symbol, PERIOD_D1, 1) - iLow(_Symbol, PERIOD_D1, 1))
/ point / 2);
Print("Autodetected daily distance (points): ", distance);
}
else
{
distance = TrailingDistance;
}
// chỉ xử lý vị thế của biểu tượng hiện tại và Magic của chúng ta
if(GetMyPosition(_Symbol, Magic))
{
const ulong ticket = PositionGetInteger(POSITION_TICKET);
Print("The next position found: ", ticket);
tr = new TrailingStop(ticket, distance, TrailingStep);
}
else // không có vị thế của chúng ta
{
Print("No positions found, lets open it...");
const ulong ticket = OpenPosition();
if(ticket)
{
tr = new TrailingStop(ticket, distance, TrailingStep);
}
}
if(tr[] != NULL)
{
// Thực hiện trailing lần đầu tiên ngay sau khi tạo hoặc tìm vị thế
tr[].trail();
}
}
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
Việc tìm kiếm một vị thế mở phù hợp được triển khai trong hàm GetMyPosition
, và việc mở một vị thế mới được thực hiện bởi hàm OpenPosition
. Cả hai đều được trình bày dưới đây. Trong bất kỳ trường hợp nào, chúng ta nhận được vé vị thế và tạo một đối tượng trailing cho nó.
bool GetMyPosition(const string s, const ulong m)
{
for(int i = 0; i < PositionsTotal(); ++i)
{
if(PositionGetSymbol(i) == s && PositionGetInteger(POSITION_MAGIC) == m)
{
return true;
}
}
return false;
}
2
3
4
5
6
7
8
9
10
11
Mục đích và ý nghĩa chung của thuật toán nên rõ ràng từ tên của các hàm tích hợp. Trong vòng lặp qua tất cả các vị thế mở (PositionsTotal
), chúng ta lần lượt chọn từng vị thế bằng PositionGetSymbol
và lấy biểu tượng của nó. Nếu biểu tượng khớp với yêu cầu, chúng ta đọc và so sánh thuộc tính vị thế POSITION_MAGIC
với "magic" đã truyền. Tất cả các hàm làm việc với vị thế sẽ được thảo luận trong một phần riêng.
Hàm sẽ trả về true
ngay khi tìm thấy vị thế phù hợp đầu tiên. Đồng thời, vị thế sẽ vẫn được chọn trong môi trường giao dịch của terminal, điều này cho phép phần còn lại của mã đọc các thuộc tính khác của nó nếu cần.
Chúng ta đã biết thuật toán để mở một vị thế.
ulong OpenPosition()
{
MqlTradeRequestSync request;
// giá trị mặc định
const bool wantToBuy = Type == MARKET_BUY;
const double volume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
// các trường tùy chọn được điền trực tiếp vào cấu trúc
request.magic = Magic;
request.deviation = Deviation;
request.comment = Comment;
ResetLastError();
// thực hiện thao tác giao dịch đã chọn và chờ xác nhận
if((bool)(wantToBuy ? request.buy(volume) : request.sell(volume))
&& request.completed())
{
Print("OK Order/Deal/Position");
}
return request.position; // giá trị khác 0 - dấu hiệu thành công
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Để rõ ràng, hãy xem cách chương trình này hoạt động trong tester, ở chế độ trực quan.
Sau khi biên dịch, hãy mở bảng kiểm tra chiến lược trong terminal, trên tab Review
, và chọn tùy chọn đầu tiên: Single test
.
Trong tab Settings
, chọn như sau:
- Trong danh sách thả xuống
Expert Advisor
: MQL5Book\p6\TralingStop Symbol
: EURUSDTimeframe
: H1Interval
: năm ngoái, tháng, hoặc tùy chỉnhForward
: KhôngDelays
: tắtModeling
: dựa trên tick thực hoặc được tạoOptimization
: tắtVisual mode
: bật
Khi bạn nhấn Start
, bạn sẽ thấy điều gì đó như thế này trong cửa sổ tester riêng:
Trailing stop tiêu chuẩn trong tester
Nhật ký sẽ hiển thị các mục như thế này:
2022.01.10 00:02:00 Autodetected daily distance (points): 373
2022.01.10 00:02:00 No positions found, let's open it...
2022.01.10 00:02:00 instant buy 0.01 EURUSD at 1.13612 (1.13550 / 1.13612 / 1.13550)
2022.01.10 00:02:00 deal #2 buy 0.01 EURUSD at 1.13612 done (based on order #2)
2022.01.10 00:02:00 deal performed [#2 buy 0.01 EURUSD at 1.13612]
2022.01.10 00:02:00 order performed buy 0.01 at 1.13612 [#2 buy 0.01 EURUSD at 1.13612]
2022.01.10 00:02:00 Waiting for position for deal D=2
2022.01.10 00:02:00 OK Order/Deal/Position
2022.01.10 00:02:00 Initial SL: 1.131770
2022.01.10 00:02:00 position modified [#2 buy 0.01 EURUSD 1.13612 sl: 1.13177]
2022.01.10 00:02:00 OK Trailing: 1.13177
2022.01.10 00:06:13 SL: 1.131770 -> 1.131880
2022.01.10 00:06:13 position modified [#2 buy 0.01 EURUSD 1.13612 sl: 1.13188]
2022.01.10 00:06:13 OK Trailing: 1.13188
2022.01.10 00:09:17 SL: 1.131880 -> 1.131990
2022.01.10 00:09:17 position modified [#2 buy 0.01 EURUSD 1.13612 sl: 1.13199]
2022.01.10 00:09:17 OK Trailing: 1.13199
2022.01.10 00:09:26 SL: 1.131990 -> 1.132110
2022.01.10 00:09:26 position modified [#2 buy 0.01 EURUSD 1.13612 sl: 1.13211]
2022.01.10 00:09:26 OK Trailing: 1.13211
2022.01.10 00:09:35 SL: 1.132110 -> 1.132240
2022.01.10 00:09:35 position modified [#2 buy 0.01 EURUSD 1.13612 sl: 1.13224]
2022.01.10 00:09:35 OK Trailing: 1.13224
2022.01.10 10:06:38 stop loss triggered #2 buy 0.01 EURUSD 1.13612 sl: 1.13224 [#3 sell 0.01 EURUSD at 1.13224]
2022.01.10 10:06:38 deal #3 sell 0.01 EURUSD at 1.13221 done (based on order #3)
2022.01.10 10:06:38 deal performed [#3 sell 0.01 EURUSD at 1.13221]
2022.01.10 10:06:38 order performed sell 0.01 at 1.13221 [#3 sell 0.01 EURUSD at 1.13224]
2022.01.10 10:06:38 Autodetected daily distance (points): 373
2022.01.10 10:06:38 No positions found, let's open it...
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
Hãy xem cách thuật toán dịch chuyển mức SL lên với sự di chuyển giá thuận lợi, cho đến khi vị thế bị đóng bởi stop loss. Ngay sau khi thanh lý một vị thế, chương trình mở một vị thế mới.
Để kiểm tra khả năng sử dụng các cơ chế theo dõi không tiêu chuẩn, chúng ta triển khai một ví dụ về thuật toán trên đường trung bình động. Để làm điều này, hãy quay lại tệp TrailingStop.mqh
và mô tả lớp dẫn xuất TrailingStopByMA
.
class TrailingStopByMA: public TrailingStop
{
int handle;
public:
TrailingStopByMA(const ulong t, const int period,
const int offset = 1,
const ENUM_MA_METHOD method = MODE_SMA,
const ENUM_APPLIED_PRICE type = PRICE_CLOSE): TrailingStop(t, 0, 1)
{
handle = iMA(_Symbol, PERIOD_CURRENT, period, offset, method, type);
}
virtual double detectLevel() override
{
double array[1];
ResetLastError();
if(CopyBuffer(handle, 0, 0, 1, array) != 1)
{
Print("CopyBuffer error: ", _LastError);
return 0;
}
return array[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
Nó tạo ra một thể hiện của chỉ báo iMA
trong hàm khởi tạo: chu kỳ, phương pháp trung bình và loại giá được truyền qua các tham số.
Trong phương thức detectLevel
được ghi đè, chúng ta đọc giá trị từ bộ đệm chỉ báo, và mặc định, điều này được thực hiện với độ lệch 1 thanh, tức là thanh đã đóng và các giá trị của nó không thay đổi khi tick đến. Những ai muốn có thể lấy giá trị từ thanh số 0, nhưng các tín hiệu như vậy không ổn định cho tất cả các loại giá, ngoại trừ PRICE_OPEN
.
Để sử dụng lớp mới trong cùng Expert Advisor thử nghiệm TrailingStop.mq5
, hãy thêm một tham số đầu vào khác MATrailingPeriod
với chu kỳ trung bình động (chúng ta sẽ giữ nguyên các tham số khác của chỉ báo).
input int MATrailingPeriod = 0; // Chu kỳ cho Trailing bằng MA (0 = tắt)
Giá trị 0 trong tham số này tắt trailing trung bình động. Nếu nó được bật, các cài đặt khoảng cách trong tham số TrailingDistance
sẽ bị bỏ qua.
Tùy thuộc vào tham số này, chúng ta sẽ tạo либо một đối tượng trailing tiêu chuẩn TrailingStop
hoặc một đối tượng dẫn xuất từ iMA
— TrailingStopByMA
.
...
tr = MATrailingPeriod > 0 ?
new TrailingStopByMA(ticket, MATrailingPeriod) :
new TrailingStop(ticket, distance, TrailingStep);
...
2
3
4
5
Hãy xem cách chương trình cập nhật hoạt động trong tester. Trong cài đặt Expert Advisor, đặt một chu kỳ khác 0 cho MA, ví dụ, 10.
Trailing stop trên đường trung bình động trong tester
Xin lưu ý rằng vào những khoảnh khắc khi trung bình tiến gần đến giá, có hiệu ứng kích hoạt stop-loss thường xuyên và đóng vị thế. Khi trung bình nằm trên báo giá, mức bảo vệ không được đặt chút nào, vì điều này không đúng cho việc mua. Đây là hậu quả của việc Expert Advisor của chúng ta không có bất kỳ chiến lược nào và luôn mở các vị thế cùng loại, bất kể tình hình trên thị trường. Đối với bán, tình huống nghịch lý tương tự sẽ thỉnh thoảng xảy ra khi trung bình đi dưới giá, nghĩa là thị trường đang tăng, và robot "cứng đầu" vào vị thế ngắn.
Trong các chiến lược hoạt động, theo quy tắc, hướng của vị thế được chọn có tính đến chuyển động của thị trường, và đường trung bình động nằm ở phía đúng của giá hiện tại, nơi việc đặt stop loss là được phép.