Đóng vị thế: Toàn bộ và từng phần
Về mặt kỹ thuật, đóng một vị thế có thể được xem như một hoạt động giao dịch ngược lại với hoạt động được sử dụng để mở nó. Ví dụ, để thoát khỏi vị thế mua, bạn cần thực hiện một hoạt động bán (ORDER_TYPE_SELL
trong trường type
) và để thoát khỏi vị thế bán, bạn cần mua (ORDER_TYPE_BUY
trong trường type
).
Loại hoạt động giao dịch trong trường action
của cấu trúc MqlTradeTransaction
vẫn giữ nguyên: TRADE_ACTION_DEAL
.
Trên tài khoản phòng ngừa rủi ro (hedging), vị thế cần đóng phải được chỉ định bằng vé trong trường position
. Đối với tài khoản netting, bạn chỉ có thể chỉ định tên của biểu tượng trong trường symbol
vì chỉ có thể có một vị thế cho mỗi biểu tượng trên các tài khoản này. Tuy nhiên, bạn cũng có thể đóng vị thế bằng vé ở đây.
Để thống nhất mã, việc điền cả hai trường position
và symbol
bất kể loại tài khoản là điều hợp lý.
Ngoài ra, hãy đảm bảo thiết lập khối lượng trong trường volume
. Nếu nó bằng với khối lượng của vị thế, vị thế sẽ được đóng hoàn toàn. Tuy nhiên, bằng cách chỉ định một giá trị thấp hơn, có thể đóng chỉ một phần của vị thế.
Trong bảng sau, tất cả các trường bắt buộc của cấu trúc được đánh dấu bằng dấu hoa thị và các trường tùy chọn được đánh dấu bằng dấu cộng.
Trường | Netting | Hedging |
---|---|---|
action | * | * |
symbol | * | + |
position | + | * |
type | * | * |
type_filling | * | * |
volume | * | * |
price | *' | *' |
deviation | ± | ± |
magic | + | + |
comment | + | + |
Trường price
được đánh dấu bằng dấu hoa thị kèm dấu nháy vì nó chỉ bắt buộc đối với các biểu tượng có chế độ thực thi Request
và Instant
, trong khi đối với chế độ thực thi Exchange
và Market
, giá trong cấu trúc không được tính đến.
Vì lý do tương tự, trường deviation
được đánh dấu bằng '±'. Nó chỉ có hiệu lực đối với các chế độ Instant
và Request
.
Để đơn giản hóa việc triển khai lập trình đóng vị thế, hãy quay lại cấu trúc mở rộng của chúng ta MqlTradeRequestSync
trong tệp MqlTradeSync.mqh
. Phương thức để đóng vị thế bằng vé có mã sau:
struct MqlTradeRequestSync: public MqlTradeRequest
{
double partial; // khối lượng sau khi đóng từng phần
...
bool close(const ulong ticket, const double lot = 0)
{
if(!PositionSelectByTicket(ticket)) return false;
position = ticket;
symbol = PositionGetString(POSITION_SYMBOL);
type = (ENUM_ORDER_TYPE)(PositionGetInteger(POSITION_TYPE) ^ 1);
price = 0;
...
2
3
4
5
6
7
8
9
10
11
12
13
Ở đây, chúng ta đầu tiên kiểm tra sự tồn tại của vị thế bằng cách gọi hàm PositionSelectByTicket
. Ngoài ra, lệnh gọi này khiến vị thế được chọn trong môi trường giao dịch của terminal, điều này cho phép bạn đọc các thuộc tính của nó bằng các hàm tiếp theo. Đặc biệt, chúng ta tìm hiểu biểu tượng của vị thế từ thuộc tính POSITION_SYMBOL
và "đảo ngược" loại của nó từ POSITION_TYPE
sang loại ngược lại để lấy loại lệnh cần thiết.
Các loại vị thế trong enum ENUM_POSITION_TYPE
là POSITION_TYPE_BUY
(giá trị 0) và POSITION_TYPE_SELL
(giá trị 1). Trong enum của các loại lệnh ENUM_ORDER_TYPE
, chính các giá trị này được chiếm bởi các hoạt động thị trường: ORDER_TYPE_BUY
và ORDER_TYPE_SELL
. Đó là lý do tại sao chúng ta có thể chuyển enum đầu tiên sang enum thứ hai, và để lấy hướng giao dịch ngược lại, chỉ cần chuyển đổi bit zero bằng phép toán OR độc quyền ('^'): chúng ta nhận được 1 từ 0, và 0 từ 1.
Việc đặt trường price
về 0 có nghĩa là tự động chọn giá hiện tại chính xác (Ask
hoặc Bid
) trước khi gửi yêu cầu: điều này được thực hiện sau một chút, bên trong phương thức trợ giúp setVolumePrices
, được gọi tiếp theo trong thuật toán, từ phương thức market
.
Lệnh gọi phương thức _market
xảy ra vài dòng sau đó. Phương thức _market
tạo ra một lệnh thị trường cho toàn bộ khối lượng hoặc một phần, tính đến tất cả các trường đã điền của cấu trúc.
const double total = lot == 0 ? PositionGetDouble(POSITION_VOLUME) : lot;
partial = PositionGetDouble(POSITION_VOLUME) - total;
return _market(symbol, total);
}
2
3
4
Đoạn mã này được đơn giản hóa một chút so với mã nguồn hiện tại. Mã đầy đủ bao gồm việc xử lý tình huống hiếm gặp nhưng có thể xảy ra khi khối lượng vị thế vượt quá khối lượng tối đa cho phép trong một lệnh cho mỗi biểu tượng (thuộc tính SYMBOL_VOLUME_MAX
). Trong trường hợp này, vị thế phải được đóng từng phần, qua nhiều lệnh.
Cũng lưu ý rằng vì vị thế có thể được đóng từng phần, chúng ta đã phải thêm một trường vào cấu trúc partial
, nơi đặt số dư khối lượng dự kiến sau hoạt động. Tất nhiên, đối với việc đóng hoàn toàn, giá trị này sẽ là 0. Thông tin này sẽ cần thiết để tiếp tục xác minh việc hoàn thành hoạt động.
Đối với tài khoản netting, có một phiên bản của phương thức close
xác định vị thế bằng tên biểu tượng. Nó chọn một vị thế theo biểu tượng, lấy vé của nó, rồi tham chiếu đến phiên bản trước của close
.
bool close(const string name, const double lot = 0)
{
if(!PositionSelect(name)) return false;
return close(PositionGetInteger(POSITION_TICKET), lot);
}
2
3
4
5
Trong cấu trúc MqlTradeRequestSync
, chúng ta có phương thức completed
cung cấp việc chờ đồng bộ cho việc hoàn thành hoạt động, nếu cần thiết. Bây giờ chúng ta cần bổ sung nó để đóng vị thế, trong nhánh mà action
bằng TRADE_ACTION_DEAL
. Chúng ta sẽ phân biệt giữa việc mở vị thế và đóng vị thế bằng giá trị zero trong trường position
: nó không có vé khi mở vị thế và có vé khi đóng.
bool completed()
{
if(action == TRADE_ACTION_DEAL)
{
if(position == 0)
{
const bool success = result.opened(timeout);
if(success) position = result.position;
return success;
}
else
{
result.position = position;
result.partial = partial;
return result.closed(timeout);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Để kiểm tra việc đóng thực sự của một vị thế, chúng ta đã thêm phương thức closed
vào cấu trúc MqlTradeResultSync
. Trước khi gọi nó, chúng ta ghi vé vị thế vào trường result.position
để cấu trúc kết quả có thể theo dõi thời điểm vé tương ứng biến mất khỏi môi trường giao dịch của terminal, hoặc khi khối lượng bằng result.partial
trong trường hợp đóng từng phần.
Đây là phương thức closed
. Nó được xây dựng dựa trên nguyên tắc quen thuộc: đầu tiên kiểm tra sự thành công của mã trả về từ máy chủ, sau đó chờ với phương thức wait
để một điều kiện nào đó được thỏa mãn.
struct MqlTradeResultSync: public MqlTradeResult
{
...
bool closed(const ulong msc = 1000)
{
if(retcode != TRADE_RETCODE_DONE)
{
return false;
}
if(!wait(positionRemoved, msc))
{
Print("Position removal timeout: P=" + (string)position);
}
return true;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Trong trường hợp này, để kiểm tra điều kiện vị thế biến mất, chúng ta đã phải triển khai một hàm mới positionRemoved
.
static bool positionRemoved(MqlTradeResultSync &ref)
{
if(ref.partial)
{
return PositionSelectByTicket(ref.position)
&& TU::Equal(PositionGetDouble(POSITION_VOLUME), ref.partial);
}
return !PositionSelectByTicket(ref.position);
}
2
3
4
5
6
7
8
9
Chúng ta sẽ kiểm tra hoạt động đóng vị thế bằng Expert Advisor TradeClose.mq5
, triển khai một chiến lược giao dịch đơn giản: vào thị trường nếu có hai thanh liên tiếp cùng hướng, và ngay khi thanh tiếp theo đóng ngược lại với xu hướng trước đó, chúng ta thoát khỏi thị trường. Các tín hiệu lặp lại trong suốt xu hướng liên tục sẽ bị bỏ qua, nghĩa là sẽ có tối đa một vị thế (khối lượng tối thiểu) hoặc không có vị thế nào trên thị trường.
Expert Advisor sẽ không có bất kỳ tham số nào có thể điều chỉnh: chỉ có (Deviation
) và một số duy nhất (Magic
). Các tham số ngầm là khung thời gian và biểu tượng hoạt động của biểu đồ.
Để theo dõi sự hiện diện của một vị thế đã mở, chúng ta sử dụng hàm GetMyPosition
từ ví dụ trước TradeTrailing.mq5
: nó tìm kiếm trong số các vị thế theo biểu tượng và số Expert Advisor và trả về giá trị logic true
nếu tìm thấy một vị thế phù hợp.
Chúng ta cũng lấy hàm gần như không thay đổi OpenPosition
: nó mở một vị thế theo loại lệnh thị trường được truyền trong tham số duy nhất. Ở đây, tham số này sẽ đến từ thuật toán phát hiện xu hướng, và trước đó (trong TrailingStop.mq5
) loại lệnh được người dùng thiết lập thông qua biến đầu vào.
Hàm mới triển khai việc đóng vị thế là ClosePosition
. Vì tệp tiêu đề MqlTradeSync.mqh
đã đảm nhận toàn bộ quy trình, chúng ta chỉ cần gọi phương thức request.close(ticket)
cho vé vị thế được gửi và chờ việc xóa hoàn tất bằng request.completed()
.
Về lý thuyết, điều này có thể tránh được nếu Expert Advisor phân tích tình hình tại mỗi tick. Trong trường hợp này, vấn đề tiềm ẩn với việc xóa vị thế sẽ nhanh chóng lộ ra vào tick tiếp theo, và Expert Advisor có thể thử xóa lại. Tuy nhiên, Expert Advisor này có logic giao dịch dựa trên thanh, và do đó không có ý nghĩa khi phân tích mỗi tick. Tiếp theo, chúng ta triển khai một cơ chế đặc biệt để làm việc theo thanh, và liên quan đến điều này, chúng ta kiểm soát đồng bộ việc xóa, nếu không vị thế sẽ vẫn "treo" suốt một thanh.
ulong LastErrorCode = 0;
ulong ClosePosition(const ulong ticket)
{
MqlTradeRequestSync request; // cấu trúc rỗng
// 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;
ResetLastError();
// thực hiện đóng và chờ xác nhận
if(request.close(ticket) && request.completed())
{
Print("OK Close Order/Deal/Position");
}
else // in chẩn đoán trong trường hợp có vấn đề
{
Print(TU::StringOf(request));
Print(TU::StringOf(request.result));
LastErrorCode = request.result.retcode;
return 0; // lỗi, mã để phân tích trong LastErrorCode
}
return request.position; // giá trị khác 0 - thành công
}
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
Chúng ta có thể buộc các hàm ClosePosition
trả về 0 trong trường hợp xóa vị thế thành công và mã lỗi trong trường hợp ngược lại. Cách tiếp cận tưởng chừng hiệu quả này sẽ làm cho hành vi của hai hàm OpenPosition
và ClosePosition
khác nhau: trong mã gọi, sẽ cần lồng các lệnh gọi của các hàm này trong các biểu thức logic có ý nghĩa trái ngược nhau, và điều này sẽ gây nhầm lẫn. Ngoài ra, chúng ta sẽ cần biến toàn cục LastErrorCode
trong bất kỳ trường hợp nào, để thêm thông tin về lỗi bên trong hàm OpenPosition
. Hơn nữa, kiểm tra if(condition)
được diễn giải tự nhiên hơn là thành công so với if(!condition)
.
Hàm tạo tín hiệu giao dịch theo chiến lược trên được gọi là GetTradeDirection
.
ENUM_ORDER_TYPE GetTradeDirection()
{
if(iClose(_Symbol, _Period, 1) > iClose(_Symbol, _Period, 2)
&& iClose(_Symbol, _Period, 2) > iClose(_Symbol, _Period, 3))
{
return ORDER_TYPE_BUY; // mở vị thế mua
}
if(iClose(_Symbol, _Period, 1) < iClose(_Symbol, _Period, 2)
&& iClose(_Symbol, _Period, 2) < iClose(_Symbol, _Period, 3))
{
return ORDER_TYPE_SELL; // mở vị thế bán
}
return (ENUM_ORDER_TYPE)-1; // đóng
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Hàm trả về giá trị của loại ENUM_ORDER_TYPE
với hai phần tử tiêu chuẩn (ORDER_TYPE_BUY
và ORDER_TYPE_SELL
) kích hoạt việc mua và bán, tương ứng. Giá trị đặc biệt -1 (không có trong enum) sẽ được sử dụng làm tín hiệu đóng.
Để kích hoạt Expert Advisor dựa trên thuật toán giao dịch, chúng ta sử dụng trình xử lý OnTick
. Như chúng ta nhớ, các tùy chọn khác phù hợp cho các chiến lược khác, ví dụ, bộ đếm thời gian cho giao dịch dựa trên tin tức hoặc sự kiện Độ sâu Thị trường cho giao dịch khối lượng.
Đầu tiên, hãy phân tích hàm ở dạng đơn giản hóa, không xử lý các lỗi tiềm ẩn. Ngay từ đầu, có một khối đảm bảo rằng thuật toán tiếp theo chỉ được kích hoạt khi một thanh mới mở.
void OnTick()
{
static datetime lastBar = 0;
if(iTime(_Symbol, _Period, 0) == lastBar) return;
lastBar = iTime(_Symbol, _Period, 0);
...
2
3
4
5
6
Tiếp theo, chúng ta lấy tín hiệu hiện tại từ hàm GetTradeDirection
.
const ENUM_ORDER_TYPE type = GetTradeDirection();
Nếu có vị thế, chúng ta kiểm tra xem có nhận được tín hiệu đóng nó hay không và gọi ClosePosition
nếu cần thiết. Nếu chưa có vị thế và có tín hiệu vào thị trường, chúng ta gọi OpenPosition
.
if(GetMyPosition(_Symbol, Magic))
{
if(type != ORDER_TYPE_BUY && type != ORDER_TYPE_SELL)
{
ClosePosition(PositionGetInteger(POSITION_TICKET));
}
}
else if(type == ORDER_TYPE_BUY || type == ORDER_TYPE_SELL)
{
OpenPosition(type);
}
}
2
3
4
5
6
7
8
9
10
11
12
Để phân tích lỗi, bạn sẽ cần bao bọc các lệnh gọi OpenPosition
và ClosePosition
vào các câu lệnh điều kiện và thực hiện một số hành động để khôi phục trạng thái làm việc của chương trình. Trong trường hợp đơn giản nhất, chỉ cần lặp lại yêu cầu tại tick tiếp theo, nhưng nên làm điều này với số lần giới hạn. Do đó, chúng ta sẽ tạo các biến tĩnh với bộ đếm và giới hạn lỗi.
void OnTick()
{
static int errors = 0;
static const int maxtrials = 10; // không quá 10 lần thử mỗi thanh
// mong đợi một thanh mới xuất hiện nếu không có lỗi
static datetime lastBar = 0;
if(iTime(_Symbol, _Period, 0) == lastBar && errors == 0) return;
lastBar = iTime(_Symbol, _Period, 0);
...
2
3
4
5
6
7
8
9
10
Cơ chế theo thanh tạm thời bị vô hiệu hóa nếu xuất hiện lỗi vì cần khắc phục chúng càng sớm càng tốt.
Lỗi được đếm trong các câu lệnh điều kiện xung quanh ClosePosition
và OpenPosition
.
const ENUM_ORDER_TYPE type = GetTradeDirection();
if(GetMyPosition(_Symbol, Magic))
{
if(type != ORDER_TYPE_BUY && type != ORDER_TYPE_SELL)
{
if(!ClosePosition(PositionGetInteger(POSITION_TICKET)))
{
++errors;
}
else
{
errors = 0;
}
}
}
else if(type == ORDER_TYPE_BUY || type == ORDER_TYPE_SELL)
{
if(!OpenPosition(type))
{
++errors;
}
else
{
errors = 0;
}
}
// quá nhiều lỗi mỗi thanh
if(errors >= maxtrials) errors = 0;
// lỗi đủ nghiêm trọng để tạm dừng
if(IS_TANGIBLE(LastErrorCode)) errors = 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
26
27
28
29
30
31
32
Việc đặt biến errors
về 0 sẽ bật lại cơ chế theo thanh và dừng các nỗ lực lặp lại yêu cầu cho đến thanh tiếp theo.
Macro IS_TANGIBLE
được định nghĩa trong TradeRetcode.mqh
như sau:
#define IS_TANGIBLE(T) ((T) >= TRADE_RETCODE_ERROR)
Các lỗi với mã nhỏ hơn là lỗi hoạt động, tức là bình thường theo một nghĩa nào đó. Các mã lớn yêu cầu phân tích và các hành động khác nhau, tùy thuộc vào nguyên nhân của vấn đề: tham số yêu cầu không chính xác, lệnh cấm vĩnh viễn hoặc tạm thời trong môi trường giao dịch, thiếu vốn, v.v. Chúng ta sẽ trình bày một bộ phân loại lỗi cải tiến trong phần Sửa đổi lệnh chờ.
Hãy chạy Expert Advisor trong tester trên XAUUSD, H1 từ đầu năm 2022, mô phỏng các tick thực. Ảnh ghép tiếp theo cho thấy một đoạn biểu đồ với các giao dịch, cũng như đường cong số dư.
Kết quả kiểm tra TradeClose trên XAUUSD, H1
Dựa trên báo cáo và nhật ký, chúng ta có thể thấy rằng sự kết hợp giữa logic giao dịch đơn giản của chúng ta và hai hoạt động mở và đóng vị thế đang hoạt động đúng cách.
Ngoài việc chỉ đơn giản đóng một vị thế, nền tảng còn hỗ trợ khả năng đóng lẫn nhau của hai vị thế ngược chiều trên các tài khoản hedging.