Các thao tác mua và bán
Trong phần này, chúng ta cuối cùng cũng bắt đầu nghiên cứu việc áp dụng các hàm MQL5 cho các nhiệm vụ giao dịch cụ thể. Mục đích của các hàm này là điền vào cấu trúc MqlTradeRequest
theo cách đặc biệt và gọi hàm OrderSend
hoặc OrderSendAsync
.
Hành động đầu tiên chúng ta sẽ tìm hiểu là mua hoặc bán một công cụ tài chính tại giá thị trường hiện tại. Quy trình thực hiện hành động này bao gồm:
- Tạo một lệnh thị trường dựa trên lệnh đã gửi
- Thực hiện một giao dịch (hoặc nhiều giao dịch) theo lệnh
- Kết quả phải là một vị thế được mở
Như chúng ta đã thấy trong phần về các loại hoạt động giao dịch, việc mua/bán tức thì tương ứng với phần tử TRADE_ACTION_DEAL
trong liệt kê ENUM_TRADE_REQUEST_ACTIONS
. Do đó, khi điền cấu trúc MqlTradeRequest
, hãy ghi TRADE_ACTION_DEAL
vào trường action
.
Hướng giao dịch được thiết lập bằng trường type
, trường này phải chứa một trong các loại lệnh: ORDER_TYPE_BUY
hoặc ORDER_TYPE_SELL
.
Tất nhiên, để mua hoặc bán, bạn cần chỉ định tên của biểu tượng trong trường symbol
và khối lượng mong muốn trong trường volume
.
Trường type_filling
phải được điền bằng một trong những chính sách điền từ liệt kê ENUM_ORDER_TYPE_FILLING, được chọn dựa trên thuộc tính ký tự SYMBOL_FILLING_MODE với các chính sách được phép.
Tùy chọn, chương trình có thể điền vào các trường với mức giá bảo vệ (sl
và tp
), một bình luận (comment
), và một ID của Expert Advisor (magic
).
Nội dung của các trường khác được thiết lập khác nhau tùy thuộc vào chế độ thực hiện giá cho biểu tượng đã chọn. Trong một số chế độ, một số trường không có hiệu lực. Ví dụ, trong chế độ Request Execution và Instant Execution, trường price
phải được điền với một mức giá phù hợp (giá Ask
cuối cùng đã biết để mua và Bid
để bán), và trường deviation
có thể chứa độ lệch tối đa cho phép của giá so với giá đã đặt để thực hiện giao dịch thành công. Trong Exchange Execution và Market Execution, các trường này bị bỏ qua. Để đơn giản hóa mã nguồn, bạn có thể điền giá và độ trượt giá đồng nhất trong tất cả các chế độ, nhưng trong hai tùy chọn cuối, giá vẫn sẽ được máy chủ giao dịch chọn và thay thế theo quy tắc của các chế độ.
Các trường khác của cấu trúc MqlTradeRequest
không được đề cập ở đây không được sử dụng cho các hoạt động giao dịch này.
Bảng sau tóm tắt các quy tắc điền các trường cho các chế độ thực hiện khác nhau. Các trường bắt buộc được đánh dấu bằng dấu sao (*), trong khi các trường tùy chọn được đánh dấu bằng dấu cộng (+).
Trường | Request | Instant | Exchange | Market |
---|---|---|---|---|
action | * | * | * | * |
symbol | * | * | * | * |
volume | * | * | * | * |
type | * | * | * | * |
type_filling | * | * | * | * |
price | * | * | ||
sl | + | + | + | + |
tp | + | + | + | + |
deviation | + | + | ||
magic | + | + | + | + |
comment | + | + | + | + |
Tùy thuộc vào cài đặt máy chủ, có thể bị cấm điền vào các trường với mức giá bảo vệ sl
và tp
tại thời điểm mở vị thế. Điều này thường xảy ra đối với chế độ thực hiện giao dịch hoặc thị trường, nhưng API MQL5 không cung cấp các thuộc tính để làm rõ tình huống này trước. Trong những trường hợp như vậy, Stop Loss
và Take Profit
nên được thiết lập bằng cách sửa đổi một vị thế đã mở. Nhân tiện, phương pháp này có thể được khuyến nghị cho tất cả các chế độ thực hiện, vì nó là cách duy nhất cho phép bạn trì hoãn chính xác các mức bảo vệ từ giá mở vị thế thực tế. Mặt khác, việc tạo và thiết lập một vị thế trong hai bước có thể dẫn đến tình huống vị thế được mở, nhưng yêu cầu thứ hai để đặt mức bảo vệ thất bại vì một lý do nào đó.
Bất kể hướng giao dịch (mua/bán), lệnh Stop Loss
luôn được đặt dưới dạng lệnh dừng (ORDER_TYPE_BUY_STOP
hoặc ORDER_TYPE_SELL_STOP
), và lệnh Take Profit
được đặt dưới dạng lệnh giới hạn (ORDER_TYPE_BUY_LIMIT
hoặc ORDER_TYPE_SELL_LIMIT
). Hơn nữa, các lệnh dừng luôn được máy chủ MetaTrader 5 kiểm soát và chỉ khi giá đạt đến mức đã chỉ định, chúng mới được gửi đến hệ thống giao dịch bên ngoài. Ngược lại, các lệnh giới hạn có thể được xuất trực tiếp ra hệ thống giao dịch bên ngoài. Cụ thể, điều này thường áp dụng cho các công cụ giao dịch trên sàn.
Để đơn giản hóa việc lập mã cho các hoạt động giao dịch, không chỉ mua và bán mà còn tất cả các hoạt động khác, từ phần này chúng ta sẽ bắt đầu phát triển các lớp, hay chính xác hơn là các cấu trúc cung cấp việc điền tự động và chính xác các trường cho các yêu cầu giao dịch, cũng như chờ đợi kết quả thực sự đồng bộ. Điều cuối cùng đặc biệt quan trọng, vì các hàm OrderSend
và OrderSendAsync
trả lại quyền điều khiển cho mã gọi trước khi hành động giao dịch được hoàn tất đầy đủ. Đặc biệt, đối với mua và bán trên thị trường, thuật toán thường cần biết không phải số vé của lệnh được tạo trên máy chủ, mà liệu vị thế đã được mở hay chưa. Tùy thuộc vào điều này, nó có thể, ví dụ, sửa đổi vị thế bằng cách đặt Stop Loss
và Take Profit
nếu nó đã mở hoặc lặp lại các nỗ lực mở nếu lệnh bị từ chối.
Một chút sau, chúng ta sẽ tìm hiểu về các sự kiện giao dịch OnTrade
và OnTradeTransaction
, thông báo cho chương trình về các thay đổi trong trạng thái tài khoản, bao gồm trạng thái của lệnh, giao dịch và vị thế. Tuy nhiên, việc chia thuật toán thành hai phần — tạo lệnh riêng theo một số tín hiệu hoặc quy tắc nhất định, và phân tích tình huống riêng trong các trình xử lý sự kiện — làm cho mã khó hiểu và khó bảo trì hơn.
Về lý thuyết, mô hình lập trình không đồng bộ không thua kém mô hình đồng bộ cả về tốc độ lẫn sự dễ dàng trong việc lập mã. Tuy nhiên, các cách triển khai của nó có thể khác nhau, ví dụ, dựa trên các con trỏ trực tiếp đến các hàm gọi lại (một kỹ thuật cơ bản trong Java, JavaScript và nhiều ngôn ngữ khác) hoặc sự kiện (như trong MQL5), điều này định hình một số đặc điểm, sẽ được thảo luận trong phần OnTradeTransaction
. Chế độ không đồng bộ cho phép tăng tốc độ gửi yêu cầu do việc trì hoãn kiểm soát việc thực hiện chúng. Nhưng việc kiểm soát này vẫn sẽ cần được thực hiện sớm hay muộn trong cùng một luồng, vì vậy hiệu suất trung bình của các mạch là như nhau.
Tất cả các cấu trúc mới sẽ được đặt trong tệp MqlTradeSync.mqh
. Để không "phát minh lại bánh xe", hãy lấy các cấu trúc tích hợp sẵn của MQL5 làm điểm xuất phát và mô tả các cấu trúc của chúng ta dưới dạng cấu trúc con. Ví dụ, để nhận kết quả truy vấn, hãy định nghĩa MqlTradeResultSync
, được dẫn xuất từ MqlTradeResult
. Ở đây chúng ta sẽ thêm các trường và phương thức hữu ích, đặc biệt là trường position
để lưu trữ vé của vị thế mở do kết quả của hoạt động mua hoặc bán trên thị trường.
struct MqlTradeResultSync : public MqlTradeResult
{
ulong position;
...
};
2
3
4
5
Cải tiến thứ hai quan trọng sẽ là một hàm tạo đặt lại tất cả các trường (điều này giúp chúng ta không phải chỉ định khởi tạo rõ ràng khi mô tả các biến thuộc loại cấu trúc).
MqlTradeResultSync()
{
ZeroMemory(this);
}
2
3
4
Tiếp theo, chúng ta sẽ giới thiệu một cơ chế đồng bộ hóa phổ quát, tức là chờ đợi kết quả của một yêu cầu (mỗi loại yêu cầu sẽ có quy tắc riêng để kiểm tra sự sẵn sàng).
Hãy định nghĩa loại của hàm gọi lại condition
. Một hàm thuộc loại này phải nhận tham số cấu trúc MqlTradeResultSync
và trả về true
nếu thành công: kết quả của hoạt động đã được nhận.
typedef bool (*condition)(MqlTradeResultSync &ref);
Các hàm như thế này được thiết kế để truyền vào phương thức wait
, phương thức này thực hiện kiểm tra định kỳ sự sẵn sàng của kết quả trong một khoảng thời gian chờ được xác định trước tính bằng mili giây.
bool wait(condition p, const ulong msc = 1000)
{
const ulong start = GetTickCount64();
bool success;
while(!(success = p(this)) && GetTickCount64() - start < msc);
return success;
}
2
3
4
5
6
7
Hãy làm rõ ngay rằng thời gian chờ là thời gian chờ tối đa: ngay cả khi nó được đặt thành một giá trị rất lớn, vòng lặp sẽ kết thúc ngay lập tức khi kết quả được nhận, điều này có thể xảy ra ngay tức thì. Tất nhiên, một thời gian chờ có ý nghĩa không nên kéo dài quá vài giây.
Hãy xem một ví dụ về phương thức sẽ được sử dụng để chờ đợi đồng bộ sự xuất hiện của một lệnh trên máy chủ (không quan trọng trạng thái của nó là gì: phân tích trạng thái là nhiệm vụ của mã gọi).
static bool orderExist(MqlTradeResultSync &ref)
{
return OrderSelect(ref.order) || HistoryOrderSelect(ref.order);
}
2
3
4
Hai hàm API MQL5 tích hợp được áp dụng ở đây, OrderSelect
và HistoryOrderSelect
: chúng tìm kiếm và chọn logic một lệnh theo vé của nó trong môi trường giao dịch nội bộ của terminal. Đầu tiên, điều này xác nhận sự tồn tại của một lệnh (nếu một trong các hàm trả về true
), và thứ hai, nó cho phép đọc các thuộc tính của nó bằng các hàm khác, điều này chưa quan trọng với chúng ta. Chúng ta sẽ đề cập đến tất cả các tính năng này trong các phần riêng biệt. Hai hàm được viết kết hợp vì một lệnh thị trường có thể được điền nhanh đến mức giai đoạn hoạt động của nó (rơi vào OrderSelect
) sẽ ngay lập tức chuyển sang lịch sử (HistoryOrderSelect
).
Lưu ý rằng phương thức được khai báo là tĩnh. Điều này là do MQL5 không hỗ trợ con trỏ đến các phương thức của đối tượng. Nếu điều này khả thi, chúng ta có thể khai báo phương thức không tĩnh trong khi sử dụng nguyên mẫu của con trỏ đến các hàm gọi lại condition
mà không cần tham số tham chiếu đến MqlTradeResultSync
(vì tất cả các trường đều có mặt bên trong đối tượng this
).
Cơ chế chờ có thể được bắt đầu như sau:
if(wait(orderExist))
{
// có một lệnh
}
else
{
// hết thời gian
}
2
3
4
5
6
7
8
Tất nhiên, đoạn mã này phải được thực thi sau khi chúng ta nhận được kết quả từ máy chủ với trạng thái TRADE_RETCODE_DONE
hoặc TRADE_RETCODE_DONE_PARTIAL
, và trường order
trong cấu trúc MqlTradeResultSync
được đảm bảo chứa một vé lệnh. Vui lòng lưu ý rằng do tính chất phân tán của hệ thống, một lệnh từ máy chủ có thể không hiển thị ngay lập tức trong môi trường terminal. Đó là lý do tại sao cần thời gian chờ.
Miễn là hàm orderExist
trả về false
vào phương thức wait
, vòng lặp chờ bên trong chạy cho đến khi hết thời gian chờ. Trong điều kiện bình thường, chúng ta sẽ gần như ngay lập tức tìm thấy một lệnh trong môi trường terminal, và vòng lặp sẽ kết thúc với dấu hiệu thành công (true
).
Hàm positionExist
kiểm tra sự hiện diện của một vị thế mở theo cách tương tự nhưng phức tạp hơn một chút. Vì hàm orderExist
trước đó đã hoàn thành việc kiểm tra lệnh, vé của nó chứa trong trường ref.order
của cấu trúc được xác nhận là hoạt động.
static bool positionExist(MqlTradeResultSync &ref)
{
ulong posid, ticket;
if(HistoryOrderGetInteger(ref.order, ORDER_POSITION_ID, posid))
{
// trong hầu hết các trường hợp, ID vị thế bằng với vé,
// nhưng không phải lúc nào cũng vậy: mã đầy đủ thực hiện việc lấy vé theo ID,
// điều mà không có công cụ MQL5 tích hợp sẵn
ticket = posid;
if(HistorySelectByPosition(posid))
{
ref.position = ticket;
...
if(HistoryDealGetInteger(deal, DEAL_POSITION_ID, position))
{
return true;
}
Print("Waiting for position for deal D=" + (string)deal);
}
}
if(!wait(positionExist, msc))
{
Print("Timeout");
return false;
}
position = result.position;
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
Tất nhiên, việc chờ đợi một lệnh và một vị thế xuất hiện chỉ có ý nghĩa nếu trạng thái của retcode
cho biết thành công. Các trạng thái khác liên quan đến lỗi hoặc hủy bỏ hoạt động, hoặc các mã trung gian cụ thể (TRADE_RETCODE_PLACED
, TRADE_RETCODE_TIMEOUT
) không đi kèm với thông tin hữu ích trong các trường khác. Trong cả hai trường hợp, điều này ngăn chặn việc xử lý tiếp theo trong khuôn khổ "đồng bộ" này.
Điều quan trọng cần lưu ý là chúng ta đang sử dụng OrderSync
và do đó dựa vào sự hiện diện bắt buộc của vé lệnh trong cấu trúc nhận được từ máy chủ.
Trong một số trường hợp, hệ thống gửi không chỉ vé lệnh mà còn vé giao dịch cùng lúc. Sau đó, từ giao dịch, bạn có thể tìm thấy vị thế nhanh hơn. Nhưng ngay cả khi có thông tin về giao dịch, môi trường giao dịch của terminal có thể tạm thời không có thông tin về vị thế mới. Đó là lý do tại sao bạn nên đợi nó với wait(positionExist)
.
Hãy tổng kết kết quả trung gian. Các cấu trúc được tạo ra cho phép bạn viết mã sau để mua 1 lô của biểu tượng hiện tại:
MqlTradeRequestSync request;
if(request.buy(1.0) && request.completed())
{
Print("OK Position: P=", request.result.position);
}
2
3
4
5
Chúng ta chỉ vào trong khối của toán tử điều kiện với một vị thế được mở đảm bảo, và chúng ta biết vé của nó. Nếu chúng ta chỉ sử dụng các phương thức buy/sell
, chúng sẽ nhận được vé lệnh tại đầu ra của chúng và phải tự kiểm tra việc thực hiện. Trong trường hợp xảy ra lỗi, chúng ta sẽ không vào trong khối if
, và mã máy chủ sẽ được chứa trong request.result.retcode
.
Khi chúng ta triển khai các phương thức cho các giao dịch khác trong các phần sau, chúng có thể được thực thi trong chế độ "chặn" tương tự, ví dụ, để sửa đổi mức dừng:
if(request.adjust(SL, TP) && request.completed())
{
Print("OK Adjust")
}
2
3
4
Tất nhiên, bạn không bắt buộc phải gọi completed
nếu bạn không muốn kiểm tra kết quả của hoạt động trong chế độ chặn. Thay vào đó, bạn có thể tuân theo mô hình không đồng bộ và phân tích môi trường trong các trình xử lý sự kiện giao dịch. Nhưng ngay cả trong trường hợp này, cấu trúc MqlTradeRequestAsync
có thể hữu ích để kiểm tra và chuẩn hóa các tham số hoạt động.
Hãy viết một Expert Advisor thử nghiệm MarketOrderSend.mq5
để kết hợp tất cả điều này. Các tham số đầu vào sẽ cung cấp việc nhập giá trị cho các trường chính và một số trường tùy chọn của yêu cầu giao dịch.
enum ENUM_ORDER_TYPE_MARKET
{
MARKET_BUY = ORDER_TYPE_BUY, // ORDER_TYPE_BUY
MARKET_SELL = ORDER_TYPE_SELL // ORDER_TYPE_SELL
};
input string Symbol; // Symbol (empty = current _Symbol)
input double Volume; // Volume (0 = minimal lot)
input double Price; // Price (0 = current Ask)
input ENUM_ORDER_TYPE_MARKET Type;
input string Comment;
input ulong Magic;
input ulong Deviation;
2
3
4
5
6
7
8
9
10
11
12
13
Liệt kê ENUM_ORDER_TYPE_MARKET
là một tập hợp con của ENUM_ORDER_TYPE
tiêu chuẩn và được giới thiệu để giới hạn các loại hoạt động có sẵn chỉ còn hai: mua và bán trên thị trường.
Hành động sẽ chạy một lần trên bộ đếm thời gian, theo cách tương tự như trong các ví dụ trước.
void OnInit()
{
// lập lịch khởi động trễ
EventSetTimer(1);
}
2
3
4
5
Trong trình xử lý bộ đếm thời gian, chúng ta tắt bộ đếm thời gian để yêu cầu chỉ được thực thi một lần. Đối với lần khởi động tiếp theo, bạn sẽ cần thay đổi các tham số Expert Advisor.
void OnTimer()
{
EventKillTimer();
...
2
3
4
Hãy mô tả một biến thuộc loại MqlTradeRequestSync
và chuẩn bị các giá trị cho các trường chính.
const bool wantToBuy = Type == MARKET_BUY;
const string symbol = StringLen(Symbol) == 0 ? _Symbol : Symbol;
const double volume = Volume == 0 ?
SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN) : Volume;
MqlTradeRequestSync request(symbol);
...
2
3
4
5
6
7
Các trường tùy chọn sẽ được điền trực tiếp.
request.magic = Magic;
request.deviation = Deviation;
request.comment = Comment;
...
2
3
4
Trong số các trường tùy chọn, bạn có thể chọn chế độ điền (type_filling
). Theo mặc định, MqlTradeRequestSync
tự động ghi vào trường này chế độ đầu tiên trong số các chế độ được phép ENUM_ORDER_TYPE_FILLING. Nhớ lại rằng cấu trúc có một phương thức đặc biệt setFilling
cho việc này.
Tiếp theo, chúng ta gọi phương thức buy
hoặc sell
với các tham số, và nếu nó trả về vé lệnh, chúng ta đợi một vị thế mở xuất hiện.
ResetLastError();
const ulong order = (wantToBuy ?
request.buy(volume, Price) :
request.sell(volume, Price));
if(order != 0)
{
Print("OK Order: #=", order);
if(request.completed()) // đợi một vị thế mở
{
Print("OK Position: P=", request.result.position);
}
}
Print(TU::StringOf(request));
Print(TU::StringOf(request.result));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Cuối hàm, các cấu trúc yêu cầu và kết quả được ghi lại để tham khảo.
Nếu chúng ta chạy Expert Advisor với các tham số mặc định (mua biểu tượng hiện tại với lô tối thiểu), chúng ta có thể nhận được kết quả sau cho "XTIUSD".
OK Order: #=218966930
Waiting for position for deal D=215494463
OK Position: P=218966930
TRADE_ACTION_DEAL, XTIUSD, ORDER_TYPE_BUY, V=0.01, ORDER_FILLING_FOK, @ 109.340, P=218966930
DONE, D=215494463, #=218966930, V=0.01, @ 109.35, Request executed, Req=8
2
3
4
5
Chú ý đến cảnh báo về sự vắng mặt tạm thời của vị thế: nó sẽ luôn xuất hiện do xử lý phân tán các yêu cầu (các cảnh báo này có thể bị tắt bằng cách xóa macro SHOW_WARNINGS
trong mã Expert Advisor, nhưng tình huống vẫn sẽ tồn tại). Nhưng nhờ các cấu trúc mới được phát triển, mã áp dụng không bị phân tâm bởi những phức tạp nội bộ này và được viết dưới dạng một chuỗi các bước đơn giản, trong đó mỗi bước tiếp theo "tự tin" vào sự thành công của các bước trước đó.
Trên tài khoản netting, chúng ta có thể đạt được hiệu ứng đảo ngược vị thế thú vị bằng cách bán tiếp theo với lô tối thiểu gấp đôi (0.02 trong trường hợp này).
OK Order: #=218966932
Waiting for position for deal D=215494468
Position ticket <> id: 218966932, 218966930
OK Position: P=218966932
TRADE_ACTION_DEAL, XTIUSD, ORDER_TYPE_SELL, V=0.02, ORDER_FILLING_FOK, @ 109.390, P=218966932
DONE, D=215494468, #=218966932, V=0.02, @ 109.39, Request executed, Req=9
2
3
4
5
6
Điều quan trọng cần lưu ý là sau khi đảo ngược, vé vị thế không còn bằng với định danh vị thế: định danh vẫn giữ từ lệnh đầu tiên, và vé vẫn giữ từ lệnh thứ hai. Chúng ta cố tình bỏ qua nhiệm vụ tìm vé vị thế theo định danh của nó để đơn giản hóa việc trình bày. Trong hầu hết các trường hợp, vé và ID là như nhau, nhưng để kiểm soát chính xác, hãy sử dụng hàm TU::PositionSelectById
. Những người quan tâm có thể nghiên cứu mã nguồn đính kèm.
Các định danh là cố định miễn là vị thế tồn tại (cho đến khi nó đóng về 0 về mặt khối lượng) và hữu ích để phân tích lịch sử tài khoản. Vé mô tả các vị thế trong khi chúng đang mở (không có khái niệm vé vị thế trong lịch sử) và được sử dụng trong một số loại yêu cầu, đặc biệt là để sửa đổi mức bảo vệ hoặc đóng với một vị thế ngược lại. Nhưng có những sắc thái liên quan đến việc đổ từng phần. Chúng ta sẽ nói thêm về các thuộc tính vị thế trong một phần riêng biệt.
Khi thực hiện hoạt động mua hoặc bán, các phương thức buy/sell
của chúng ta cho phép bạn ngay lập tức đặt các mức Stop Loss
và/hoặc Take Profit
. Để làm điều này, chỉ cần truyền chúng dưới dạng các tham số bổ sung lấy từ các biến đầu vào hoặc được tính toán bằng một số công thức nào đó. Ví dụ,
input double SL;
input double TP;
...
void OnTimer()
{
...
const ulong order = (wantToBuy ?
request.buy(symbol, volume, Price, SL, TP) :
request.sell(symbol, volume, Price, SL, TP));
...
2
3
4
5
6
7
8
9
10
Tất cả các phương thức của các cấu trúc mới cung cấp chuẩn hóa tự động các tham số được truyền, vì vậy không cần sử dụng NormalizeDouble
hay bất cứ thứ gì khác.
Đã được ghi nhận ở trên rằng một số cài đặt máy chủ có thể cấm việc đặt mức bảo vệ tại thời điểm mở vị thế. Trong trường hợp này, bạn nên đặt các trường sl
và tp
thông qua một yêu cầu riêng. Yêu cầu tương tự cũng được sử dụng trong những trường hợp cần sửa đổi các mức đã đặt, đặc biệt là để triển khai trailing stop hoặc trailing profit.
Trong phần tiếp theo, chúng ta sẽ hoàn thành ví dụ hiện tại với việc đặt trễ sl
và tp
bằng yêu cầu thứ hai sau khi mở vị thế thành công.