Đóng các vị thế ngược chiều: Toàn bộ và từng phần (hedging)
Trên tài khoản phòng ngừa rủi ro (hedging), bạn được phép mở nhiều vị thế cùng lúc, và trong hầu hết các trường hợp, các vị thế này có thể ở hai hướng ngược nhau. Ở một số khu vực pháp lý, tài khoản hedging bị hạn chế: bạn chỉ có thể giữ vị thế theo một hướng tại một thời điểm. Trong trường hợp này, bạn sẽ nhận được mã lỗi TRADE_RETCODE_HEDGE_PROHIBITED
khi cố gắng thực hiện một hoạt động giao dịch ngược hướng. Ngoài ra, hạn chế này thường liên quan đến việc cài đặt thuộc tính tài khoản ACCOUNT_FIFO_CLOSE
thành true
.
Khi hai vị thế ngược chiều được mở cùng lúc, nền tảng hỗ trợ cơ chế đóng lẫn nhau đồng thời bằng hoạt động TRADE_ACTION_CLOSE_BY
. Để thực hiện hành động này, bạn cần điền thêm hai trường trong cấu trúc MqlTradeTransaction
ngoài trường action
: position
và position_by
phải chứa vé (ticket) của các vị thế cần đóng.
Tính khả dụng của tính năng này phụ thuộc vào thuộc tính SYMBOL_ORDER_MODE của công cụ tài chính: SYMBOL_ORDER_CLOSEBY
(64) phải có trong bitmask các cờ được phép.
Hoạt động này không chỉ đơn giản hóa việc đóng (một thao tác thay vì hai) mà còn tiết kiệm được một spread.
Như bạn đã biết, bất kỳ vị thế mới nào bắt đầu giao dịch với khoản lỗ bằng spread. Ví dụ, khi mua một công cụ tài chính, giao dịch được thực hiện ở giá Ask
, nhưng để thoát giao dịch, tức là bán, giá thực tế là Bid
. Đối với vị thế bán, tình huống ngược lại: ngay sau khi vào lệnh ở giá Bid
, chúng ta bắt đầu theo dõi giá Ask
cho một lần thoát tiềm năng.
Nếu bạn đóng các vị thế đồng thời theo cách thông thường, giá thoát của chúng sẽ cách nhau một khoảng bằng spread hiện tại. Tuy nhiên, nếu bạn sử dụng hoạt động TRADE_ACTION_CLOSE_BY
, cả hai vị thế sẽ được đóng mà không tính đến giá hiện tại. Giá mà tại đó các vị thế được bù trừ bằng giá mở của vị thế position_by
(trong cấu trúc yêu cầu). Nó được chỉ định trong lệnh ORDER_TYPE_CLOSE_BY được tạo ra bởi yêu cầu TRADE_ACTION_CLOSE_BY
.
Thật không may, trong các báo cáo theo ngữ cảnh của giao dịch và vị thế, giá đóng và mở của các vị thế/giao dịch ngược chiều được hiển thị dưới dạng các cặp giá trị giống nhau, theo hướng đối xứng, tạo ấn tượng về lợi nhuận hoặc lỗ gấp đôi. Trên thực tế, kết quả tài chính của hoạt động (chênh lệch giữa các giá đã điều chỉnh theo khối lượng) chỉ được ghi nhận cho giao dịch thoát vị thế đầu tiên (trường position
trong cấu trúc yêu cầu). Kết quả của giao dịch thoát thứ hai luôn là 0, bất kể chênh lệch giá.
Một hệ quả khác của sự bất đối xứng này là việc thay đổi vị trí của các vé trong các trường position
và position_by
sẽ làm thay đổi thống kê lợi nhuận và lỗ trong báo cáo giao dịch theo ngữ cảnh của các giao dịch mua và bán, ví dụ, các giao dịch mua có lợi nhuận có thể tăng lên đúng bằng số lượng giao dịch bán có lợi nhuận giảm đi. Nhưng về lý thuyết, điều này không nên ảnh hưởng đến kết quả tổng thể, nếu chúng ta giả định rằng độ trễ trong việc thực hiện lệnh không phụ thuộc vào thứ tự truyền vé.
Biểu đồ sau đây cho thấy giải thích bằng hình ảnh về quy trình (spread được phóng đại cố ý).
Kế toán spread khi đóng các vị thế có lợi nhuận
Đây là trường hợp của một cặp vị thế có lợi nhuận. Nếu các vị thế có hướng ngược nhau và đang lỗ, khi đóng riêng lẻ, spread sẽ được tính hai lần (trong mỗi vị thế). Đóng ngược chiều cho phép giảm lỗ đi một spread.
Kế toán spread khi đóng các vị thế không có lợi nhuận
Các vị thế ngược chiều không nhất thiết phải có kích thước bằng nhau. Hoạt động đóng ngược chiều sẽ hoạt động dựa trên khối lượng tối thiểu của hai vị thế.
Trong tệp MqlTradeSync.mqh
, hoạt động đóng ngược chiều được triển khai bằng phương thức closeby
với hai tham số cho vé vị thế.
struct MqlTradeRequestSync: public MqlTradeRequest
{
...
bool closeby(const ulong ticket1, const ulong ticket2)
{
if(!PositionSelectByTicket(ticket1)) return false;
double volume1 = PositionGetDouble(POSITION_VOLUME);
if(!PositionSelectByTicket(ticket2)) return false;
double volume2 = PositionGetDouble(POSITION_VOLUME);
action = TRADE_ACTION_CLOSE_BY;
position = ticket1;
position_by = ticket2;
ZeroMemory(result);
if(volume1 != volume2)
{
// remember which position should disappear
if(volume1 < volume2)
result.position = ticket1;
else
result.position = ticket2;
}
return OrderSend(this, result);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Để kiểm soát kết quả của việc đóng, chúng ta lưu vé của vị thế nhỏ hơn trong biến result.position
. Mọi thứ trong phương thức completed
và trong cấu trúc MqlTradeResultSync
đã sẵn sàng để theo dõi đồng bộ việc đóng vị thế: cùng một thuật toán đã hoạt động cho việc đóng vị thế thông thường.
struct MqlTradeRequestSync: public MqlTradeRequest
{
...
bool completed()
{
...
else if(action == TRADE_ACTION_CLOSE_BY)
{
return result.closed(timeout);
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
Các vị thế ngược chiều thường được sử dụng để thay thế cho lệnh dừng hoặc cố gắng chốt lời trên một đợt điều chỉnh ngắn hạn trong khi vẫn ở lại thị trường và theo xu hướng chính. Tùy chọn sử dụng lệnh dừng giả cho phép hoãn quyết định thực sự đóng vị thế trong một thời gian, tiếp tục phân tích biến động thị trường với kỳ vọng giá sẽ đảo chiều theo hướng mong muốn. Tuy nhiên, cần lưu ý rằng các vị thế "bị khóa" đòi hỏi ký quỹ cao hơn và phải chịu phí swap. Đó là lý do tại sao rất khó tưởng tượng một chiến lược giao dịch được xây dựng hoàn toàn dựa trên các vị thế ngược chiều, có thể dùng làm ví dụ cho phần này.
Hãy phát triển ý tưởng của chiến lược dựa trên thanh giá được nêu trong ví dụ trước. Expert Advisor mới là TradeCloseBy.mq5
.
Chúng ta sẽ sử dụng tín hiệu trước đó để vào thị trường khi phát hiện hai cây nến liên tiếp đóng cùng hướng. Một hàm chịu trách nhiệm tạo tín hiệu này là GetTradeDirection
. Tuy nhiên, hãy cho phép tái nhập nếu xu hướng tiếp tục. Tổng số vị thế tối đa được phép sẽ được đặt trong biến đầu vào PositionLimit
, mặc định là 5.
Hàm GetMyPositions
sẽ có một số thay đổi: nó sẽ có hai tham số, là các tham chiếu đến mảng chấp nhận vé vị thế: mua và bán riêng biệt.
#define PUSH(A,V) (A[ArrayResize(A, ArraySize(A) + 1, ArraySize(A) * 2) - 1] = V)
int GetMyPositions(const string s, const ulong m,
ulong &ticketsLong[], ulong &ticketsShort[])
{
for(int i = 0; i < PositionsTotal(); ++i)
{
if(PositionGetSymbol(i) == s && PositionGetInteger(POSITION_MAGIC) == m)
{
if((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
PUSH(ticketsLong, PositionGetInteger(POSITION_TICKET));
else
PUSH(ticketsShort, PositionGetInteger(POSITION_TICKET));
}
}
const int min = fmin(ArraySize(ticketsLong), ArraySize(ticketsShort));
if(min == 0) return -fmax(ArraySize(ticketsLong), ArraySize(ticketsShort));
return min;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Hàm trả về kích thước của mảng nhỏ nhất trong hai mảng. Khi nó lớn hơn 0, chúng ta có cơ hội đóng các vị thế ngược chiều.
Nếu mảng tối thiểu có kích thước bằng 0, hàm sẽ trả về kích thước của mảng còn lại, nhưng với dấu âm, chỉ để thông báo cho mã gọi rằng tất cả các vị thế đều cùng hướng.
Nếu không có vị thế nào ở cả hai hướng, hàm sẽ trả về 0.
Việc mở vị thế sẽ vẫn nằm dưới sự kiểm soát của hàm OpenPosition
- không có thay đổi ở đây.
Việc đóng sẽ chỉ được thực hiện ở chế độ hai vị thế ngược chiều trong hàm mới CloseByPosition
. Nói cách khác, Expert Advisor này không có khả năng đóng từng vị thế một theo cách thông thường. Tất nhiên, trong một robot thực sự, nguyên tắc như vậy khó có thể xảy ra, nhưng như một ví dụ về việc đóng ngược chiều, nó rất phù hợp. Nếu cần đóng một vị thế đơn lẻ, chỉ cần mở một vị thế ngược chiều cho nó (tại thời điểm này, lợi nhuận hoặc lỗ thả nổi được cố định) và gọi CloseByPosition
cho cả hai.
bool CloseByPosition(const ulong ticket1, const ulong ticket2)
{
MqlTradeRequestSync request;
request.magic = Magic;
ResetLastError();
// gửi yêu cầu và chờ hoàn thành
if(request.closeby(ticket1, ticket2))
{
Print("Positions collapse initiated");
if(request.completed())
{
Print("OK CloseBy Order/Deal/Position");
return true; // thành công
}
}
Print(TU::StringOf(request));
Print(TU::StringOf(request.result));
return false; // lỗi
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Mã sử dụng phương thức request.closeby
được mô tả ở trên. Các trường position
và position_by
được điền và OrderSend
được gọi.
Logic giao dịch được mô tả trong trình xử lý OnTick
, phân tích cấu hình giá chỉ tại thời điểm hình thành thanh mới và nhận tín hiệu từ hàm GetTradeDirection
.
void OnTick()
{
static bool error = false;
// chờ thanh mới hình thành, nếu không có lỗi
static datetime lastBar = 0;
if(iTime(_Symbol, _Period, 0) == lastBar && !error) return;
lastBar = iTime(_Symbol, _Period, 0);
const ENUM_ORDER_TYPE type = GetTradeDirection();
...
2
3
4
5
6
7
8
9
10
Tiếp theo, chúng ta điền các mảng ticketsLong
và ticketsShort
với vé vị thế của biểu tượng đang hoạt động và với số Magic
đã cho. Nếu hàm GetMyPositions
trả về giá trị lớn hơn 0, nó cho biết số lượng cặp vị thế ngược chiều được hình thành. Chúng có thể được đóng trong một vòng lặp bằng hàm CloseByPosition
. Việc kết hợp các cặp trong trường hợp này được chọn ngẫu nhiên (theo thứ tự vị thế trong môi trường terminal), tuy nhiên, trong thực tế, có thể quan trọng khi chọn cặp theo khối lượng hoặc sao cho các vị thế có lợi nhuận nhất được đóng trước.
ulong ticketsLong[], ticketsShort[];
const int n = GetMyPositions(_Symbol, Magic, ticketsLong, ticketsShort);
if(n > 0)
{
for(int i = 0; i < n; ++i)
{
error = !CloseByPosition(ticketsShort[i], ticketsLong[i]) && error;
}
}
...
2
3
4
5
6
7
8
9
10
Đối với bất kỳ giá trị nào khác của n
, bạn nên kiểm tra xem có tín hiệu (có thể lặp lại) để vào thị trường và thực hiện nó bằng cách gọi OpenPosition
.
else if(type == ORDER_TYPE_BUY || type == ORDER_TYPE_SELL)
{
error = !OpenPosition(type);
}
...
2
3
4
5
Cuối cùng, nếu vẫn còn các vị thế mở, nhưng chúng cùng hướng, chúng ta kiểm tra xem số lượng của chúng có đạt đến giới hạn hay không, trong trường hợp đó chúng ta tạo một vị thế ngược chiều để "sụp đổ" hai vị thế trong thanh tiếp theo (do đó đóng một trong bất kỳ vị thế cũ nào).
else if(n < 0)
{
if(-n >= (int)PositionLimit)
{
if(ArraySize(ticketsLong) > 0)
{
error = !OpenPosition(ORDER_TYPE_SELL);
}
else // (ArraySize(ticketsShort) > 0)
{
error = !OpenPosition(ORDER_TYPE_BUY);
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Hãy chạy Expert Advisor trong tester trên XAUUSD, H1 từ đầu năm 2022, với cài đặt mặc định. Dưới đây là biểu đồ với các vị thế trong quá trình hoạt động của chương trình, cũng như đường cong số dư.
Kết quả kiểm tra TradeCloseBy trên XAUUSD, H1
Dễ dàng tìm thấy trong nhật ký các thời điểm xu hướng một chiều kết thúc (mua với vé từ #2 đến #4), và các giao dịch bắt đầu được tạo ra theo hướng ngược lại (bán #5), sau đó một lần đóng ngược chiều được kích hoạt.
2022.01.03 01:05:00 instant buy 0.01 XAUUSD at 1831.13 (1830.63 / 1831.13 / 1830.63)
2022.01.03 01:05:00 deal #2 buy 0.01 XAUUSD at 1831.13 done (based on order #2)
2022.01.03 01:05:00 deal performed [#2 buy 0.01 XAUUSD at 1831.13]
2022.01.03 01:05:00 order performed buy 0.01 at 1831.13 [#2 buy 0.01 XAUUSD at 1831.13]
2022.01.03 01:05:00 Waiting for position for deal D=2
2022.01.03 01:05:00 OK New Order/Deal/Position
2022.01.03 02:00:00 instant buy 0.01 XAUUSD at 1828.77 (1828.47 / 1828.77 / 1828.47)
2022.01.03 02:00:00 deal #3 buy 0.01 XAUUSD at 1828.77 done (based on order #3)
2022.01.03 02:00:00 deal performed [#3 buy 0.01 XAUUSD at 1828.77]
2022.01.03 02:00:00 order performed buy 0.01 at 1828.77 [#3 buy 0.01 XAUUSD at 1828.77]
2022.01.03 02:00:00 Waiting for position for deal D=3
2022.01.03 02:00:00 OK New Order/Deal/Position
2022.01.03 03:00:00 instant buy 0.01 XAUUSD at 1830.40 (1830.16 / 1830.40 / 1830.16)
2022.01.03 03:00:00 deal #4 buy 0.01 XAUUSD at 1830.40 done (based on order #4)
2022.01.03 03:00:00 deal performed [#4 buy 0.01 XAUUSD at 1830.40]
2022.01.03 03:00:00 order performed buy 0.01 at 1830.40 [#4 buy 0.01 XAUUSD at 1830.40]
2022.01.03 03:00:00 Waiting for position for deal D=4
2022.01.03 03:00:00 OK New Order/Deal/Position
2022.01.03 05:00:00 instant sell 0.01 XAUUSD at 1826.22 (1826.22 / 1826.45 / 1826.22)
2022.01.03 05:00:00 deal #5 sell 0.01 XAUUSD at 1826.22 done (based on order #5)
2022.01.03 05:00:00 deal performed [#5 sell 0.01 XAUUSD at 1826.22]
2022.01.03 05:00:00 order performed sell 0.01 at 1826.22 [#5 sell 0.01 XAUUSD at 1826.22]
2022.01.03 05:00:00 Waiting for position for deal D=5
2022.01.03 05:00:00 OK New Order/Deal/Position
2022.01.03 06:00:00 close position #5 sell 0.01 XAUUSD by position #2 buy 0.01 XAUUSD (1825.64 / 1825.86 / 1825.64)
2022.01.03 06:00:00 deal #6 buy 0.01 XAUUSD at 1831.13 done (based on order #6)
2022.01.03 06:00:00 deal #7 sell 0.01 XAUUSD at 1826.22 done (based on order #6)
2022.01.03 06:00:00 Positions collapse initiated
2022.01.03 06:00:00 OK CloseBy Order/Deal/Position
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
Giao dịch #3 là một hiện tượng thú vị. Người đọc cẩn thận sẽ nhận thấy rằng nó mở thấp hơn so với giao dịch trước, dường như vi phạm chiến lược của chúng ta. Thực tế, không có lỗi ở đây, và đây là hệ quả của việc các điều kiện tín hiệu được viết đơn giản nhất có thể: chỉ dựa trên giá đóng của các thanh. Do đó, một cây nến đảo chiều giảm (D), mở với khoảng cách tăng và đóng trên mức kết thúc của cây nến tăng trước đó (C), đã tạo ra tín hiệu mua. Tình huống này được minh họa trong ảnh chụp màn hình sau.
Giao dịch trong xu hướng tăng tại giá đóng
Tất cả các cây nến theo thứ tự A, B, C, D và E đóng cao hơn cây trước đó và khuyến khích tiếp tục mua. Để loại bỏ các hiện tượng như vậy, cần phân tích thêm hướng của chính các thanh.
Điều cuối cùng cần chú ý trong ví dụ này là hàm OnInit
. Vì Expert Advisor sử dụng hoạt động TRADE_ACTION_CLOSE_BY
, các kiểm tra được thực hiện ở đây đối với cài đặt tài khoản và biểu tượng làm việc liên quan.
int OnInit()
{
...
if(AccountInfoInteger(ACCOUNT_MARGIN_MODE) != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
{
Alert("An account with hedging is required for this EA!");
return INIT_FAILED;
}
if((SymbolInfoInteger(_Symbol, SYMBOL_ORDER_MODE) & SYMBOL_ORDER_CLOSEBY) == 0)
{
Alert("'Close By' mode is not supported for ", _Symbol);
return INIT_FAILED;
}
return INIT_SUCCEEDED;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Nếu một trong các thuộc tính không hỗ trợ đóng chéo, Expert Advisor sẽ không thể tiếp tục hoạt động. Khi tạo robot hoạt động, các kiểm tra này thường được thực hiện bên trong thuật toán giao dịch và chuyển chương trình sang các chế độ thay thế, đặc biệt là đóng vị thế đơn lẻ và duy trì vị thế tổng hợp trong trường hợp netting.