Ví dụ về tìm kiếm chiến lược giao dịch bằng SQLite
Hãy thử sử dụng SQLite để giải quyết các vấn đề thực tế. Chúng ta sẽ nhập các cấu trúc vào cơ sở dữ liệu MqlRates
với lịch sử báo giá và phân tích chúng để xác định các mẫu hình và tìm kiếm các chiến lược giao dịch tiềm năng. Tất nhiên, bất kỳ logic nào được chọn cũng có thể được triển khai trong MQL5, nhưng SQL cho phép thực hiện theo cách khác, trong nhiều trường hợp hiệu quả hơn và sử dụng nhiều hàm SQL tích hợp thú vị. Chủ đề của cuốn sách, nhằm học MQL5, không cho phép đi sâu vào công nghệ này, nhưng chúng ta đề cập đến nó như một điều đáng chú ý đối với một nhà giao dịch thuật toán.
Kịch bản để chuyển đổi lịch sử báo giá thành định dạng cơ sở dữ liệu được gọi là DBquotesImport.mq5
. Trong các tham số đầu vào, bạn có thể đặt tiền tố của tên cơ sở dữ liệu và kích thước giao dịch (số lượng bản ghi trong một giao dịch).
input string Database = "MQL5Book/DB/Quotes";
input int TransactionSize = 1000;
2
Để thêm các cấu trúc MqlRates
vào cơ sở dữ liệu bằng lớp ORM của chúng ta, kịch bản định nghĩa một cấu trúc phụ trợ MqlRatesDB
cung cấp các quy tắc liên kết các trường cấu trúc với các cột cơ sở. Vì kịch bản của chúng ta chỉ ghi dữ liệu vào cơ sở dữ liệu và không đọc từ đó, nó không cần được liên kết bằng hàm DatabaseReadBind
, điều này sẽ áp đặt một hạn chế về "độ đơn giản" của cấu trúc. Việc không có ràng buộc cho phép dẫn xuất cấu trúc MqlRatesDB
từ MqlRates
(và không lặp lại mô tả của các trường).
struct MqlRatesDB: public MqlRates
{
/* tham khảo:
datetime time;
double open;
double high;
double low;
double close;
long tick_volume;
int spread;
long real_volume;
*/
bool bindAll(DBQuery &q) const
{
return q.bind(0, time)
&& q.bind(1, open)
&& q.bind(2, high)
&& q.bind(3, low)
&& q.bind(4, close)
&& q.bind(5, tick_volume)
&& q.bind(6, spread)
&& q.bind(7, real_volume);
}
long rowid(const long setter = 0)
{
// rowid được chúng ta đặt theo thời gian thanh
return time;
}
};
DB_FIELD_C1(MqlRatesDB, datetime, time, DB_CONSTRAINT::PRIMARY_KEY);
DB_FIELD(MqlRatesDB, double, open);
DB_FIELD(MqlRatesDB, double, high);
DB_FIELD(MqlRatesDB, double, low);
DB_FIELD(MqlRatesDB, double, close);
DB_FIELD(MqlRatesDB, long, tick_volume);
DB_FIELD(MqlRatesDB, int, spread);
DB_FIELD(MqlRatesDB, long, real_volume);
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
Tên cơ sở dữ liệu được hình thành từ tiền tố Database
, tên và khung thời gian của biểu đồ hiện tại mà kịch bản đang chạy. Một bảng duy nhất "MqlRatesDB" được tạo trong cơ sở dữ liệu với cấu hình trường được chỉ định bởi các macro DB_FIELD. Lưu ý rằng khóa chính sẽ không được tạo bởi cơ sở dữ liệu, mà được lấy trực tiếp từ các thanh, từ trường time
(thời gian mở thanh).
void OnStart()
{
Print("");
DBSQLite db(Database + _Symbol + PeriodToString());
if(!PRTF(db.isOpen())) return;
PRTF(db.deleteTable(typename(MqlRatesDB)));
if(!PRTF(db.createTable<MqlRatesDB>(true))) return;
...
}
2
3
4
5
6
7
8
9
10
11
Tiếp theo, sử dụng các gói gồm TransactionSize
thanh, chúng ta yêu cầu các thanh từ lịch sử và thêm chúng vào bảng. Đây là công việc của hàm hỗ trợ ReadChunk
, được gọi trong một vòng lặp miễn là còn dữ liệu (hàm trả về true
) hoặc người dùng không dừng kịch bản thủ công. Mã hàm được hiển thị dưới đây.
int offset = 0;
while(ReadChunk(db, offset, TransactionSize) && !IsStopped())
{
offset += TransactionSize;
}
2
3
4
5
Sau khi hoàn tất quá trình, chúng ta yêu cầu cơ sở dữ liệu số lượng bản ghi được tạo trong bảng và xuất nó ra nhật ký.
DBRow *rows[];
if(db.prepare(StringFormat("SELECT COUNT(*) FROM %s",
typename(MqlRatesDB))).readAll(rows))
{
Print("Records added: ", rows[0][0].integer_value);
}
}
2
3
4
5
6
7
Hàm ReadChunk
trông như sau.
bool ReadChunk(DBSQLite &db, const int offset, const int size)
{
MqlRates rates[];
MqlRatesDB ratesDB[];
const int n = CopyRates(_Symbol, PERIOD_CURRENT, offset, size, rates);
if(n > 0)
{
DBTransaction tr(db, true);
Print(rates[0].time);
ArrayResize(ratesDB, n);
for(int i = 0; i < n; ++i)
{
ratesDB[i] = rates[i];
}
return db.insert(ratesDB);
}
else
{
Print("CopyRates failed: ", _LastError, " ", E2S(_LastError));
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Nó gọi hàm tích hợp CopyRates
để điền mảng thanh rates
. Sau đó, các thanh được chuyển sang mảng ratesDB
để chỉ với một câu lệnh db.insert(ratesDB)
chúng ta có thể ghi thông tin vào cơ sở dữ liệu (chúng ta đã chuẩn hóa trong MqlRatesDB
cách thực hiện đúng).
Sự hiện diện của đối tượng DBTransaction
(với tùy chọn "commit" tự động được bật) bên trong khối có nghĩa là tất cả các thao tác với mảng được "phủ" bởi một giao dịch. Để chỉ ra tiến trình, trong quá trình xử lý mỗi khối thanh, nhãn của thanh đầu tiên được hiển thị trong nhật ký.
Trong khi hàm CopyRates
trả về dữ liệu và việc chèn chúng vào cơ sở dữ liệu thành công, vòng lặp trong OnStart
tiếp tục với việc dịch chuyển số lượng các thanh được sao chép sâu vào lịch sử. Khi đến cuối lịch sử có sẵn hoặc giới hạn thanh được đặt trong cài đặt terminal, CopyRates
sẽ trả về lỗi 4401 (HISTORY_NOT_FOUND) và kịch bản sẽ thoát.
Hãy chạy kịch bản trên biểu đồ EURUSD, H1. Nhật ký sẽ hiển thị như sau.
db.isOpen()=true / ok
db.deleteTable(typename(MqlRatesDB))=true / ok
db.createTable<MqlRatesDB>(true)=true / ok
2022.06.29 20:00:00
2022.05.03 04:00:00
2022.03.04 10:00:00
...
CopyRates failed: 4401 HISTORY_NOT_FOUND
Records added: 100000
2
3
4
5
6
7
8
9
Bây giờ chúng ta có cơ sở QuotesEURUSDH1.sqlite
, nơi bạn có thể thử nghiệm để kiểm tra các giả thuyết giao dịch khác nhau. Bạn có thể mở nó trong MetaEditor để đảm bảo rằng dữ liệu được chuyển đúng.
Hãy kiểm tra một trong những chiến lược đơn giản nhất dựa trên các quy luật trong lịch sử. Chúng ta sẽ tìm thống kê của hai thanh liên tiếp cùng hướng, phân chia theo thời gian trong ngày và ngày trong tuần. Nếu có lợi thế rõ rệt cho một số kết hợp của thời gian và ngày trong tuần, nó có thể được xem xét trong tương lai như một tín hiệu để vào thị trường theo hướng của thanh đầu tiên.
Đầu tiên, hãy thiết kế một truy vấn SQL yêu cầu báo giá cho một khoảng thời gian nhất định và tính toán chuyển động giá trên mỗi thanh, tức là sự khác biệt giữa các giá mở cửa liền kề.
Vì thời gian cho các thanh được lưu trữ dưới dạng số giây (theo tiêu chuẩn của datetime
trong MQL5 và đồng thời là "Unix epoch" của SQL), nên chuyển đổi hiển thị của chúng thành chuỗi để dễ đọc, vì vậy hãy bắt đầu truy vấn SELECT từ trường datetime
dựa trên hàm DATETIME:
SELECT
DATETIME(time, 'unixepoch') as datetime, open, ...
2
Trường này sẽ không tham gia vào phân tích và được đưa ra đây chỉ cho người dùng. Sau đó, giá được hiển thị để tham khảo, để chúng ta có thể kiểm tra việc tính toán gia tăng giá bằng cách in gỡ lỗi.
Vì chúng ta sẽ chọn, nếu cần, một khoảng thời gian nhất định từ toàn bộ tệp, điều kiện sẽ yêu cầu trường time
ở "dạng thuần", và nó cũng nên được thêm vào yêu cầu. Ngoài ra, theo phân tích báo giá đã lên kế hoạch, chúng ta sẽ cần tách từ nhãn thanh thời gian trong ngày của nó, cũng như ngày trong tuần (số thứ tự của chúng tương ứng với cách đánh số trong MQL5, 0 là Chủ nhật). Hãy gọi hai cột cuối cùng của truy vấn là intraday
và day
, tương ứng, và các hàm TIME và STRFTIME được sử dụng để lấy chúng.
SELECT
DATETIME(time, 'unixepoch') as datetime, open,
time,
TIME(time, 'unixepoch') AS intraday,
STRFTIME('%w', time, 'unixepoch') AS day, ...
2
3
4
5
Để tính toán gia tăng giá trong SQL, bạn có thể sử dụng hàm LAG. Nó trả về giá trị của cột được chỉ định với độ lệch của số hàng được chỉ định. Ví dụ, LAG(X, 1)
có nghĩa là lấy giá trị X
trong bản ghi trước đó, với tham số thứ hai 1 là độ lệch mặc định là 1, tức là có thể bỏ qua để có mục tương đương LAG(X)
. Để lấy giá trị của bản ghi tiếp theo, gọi LAG(X,-1)
. Trong mọi trường hợp, khi sử dụng LAG, cần có một cấu trúc cú pháp bổ sung chỉ định thứ tự sắp xếp của các bản ghi, trong trường hợp đơn giản nhất, dưới dạng OVER(ORDER BY column)
.
Do đó, để lấy gia tăng giá giữa các giá mở của hai thanh lân cận, chúng ta viết:
...
(LAG(open,-1) OVER (ORDER BY time) - open) AS delta, ...
2
Cột này mang tính dự đoán vì nó nhìn vào tương lai.
Chúng ta có thể phát hiện rằng hai thanh hình thành cùng hướng bằng cách nhân các gia tăng với chúng: các giá trị dương cho thấy sự tăng hoặc giảm nhất quán:
...
(LAG(open,-1) OVER (ORDER BY time) - open) * (open - LAG(open) OVER (ORDER BY time))
AS product, ...
2
3
Chỉ số này được chọn vì đơn giản nhất để sử dụng trong tính toán: cho các hệ thống giao dịch thực tế, bạn có thể chọn một tiêu chí phức tạp hơn.
Để đánh giá lợi nhuận do hệ thống tạo ra trên backtest, bạn cần nhân hướng của thanh trước (đóng vai trò là chỉ báo cho chuyển động tương lai) với gia tăng giá trên thanh tiếp theo. Hướng được tính toán trong cột direction
(sử dụng hàm SIGN), chỉ để tham khảo. Ước tính lợi nhuận trong cột estimate
là tích của chuyển động trước đó direction
và gia tăng của thanh tiếp theo (delta
): nếu hướng được giữ nguyên, chúng ta nhận được kết quả dương (tính bằng điểm).
...
SIGN(open - LAG(open) OVER (ORDER BY time)) AS direction,
(LAG(open,-1) OVER (ORDER BY time) - open) * SIGN(open - LAG(open) OVER (ORDER BY time))
AS estimate ...
2
3
4
Trong các biểu thức trong lệnh SQL, bạn không thể sử dụng bí danh AS được định nghĩa trong cùng lệnh đó. Đó là lý do tại sao chúng ta không thể xác định
estimate
làdelta * direction
, và chúng ta phải lặp lại việc tính toán tích một cách rõ ràng. Tuy nhiên, chúng ta nhớ rằng các cộtdelta
vàdirection
không cần thiết cho phân tích lập trình và được thêm vào đây chỉ để trực quan hóa bảng trước mặt người dùng.
Ở cuối lệnh SQL, chúng ta chỉ định bảng mà từ đó thực hiện lựa chọn và các điều kiện lọc cho khoảng thời gian backtest: hai tham số "từ" và "đến".
...
FROM MqlRatesDB
WHERE (time >= ?1 AND time < ?2)
2
3
Tùy chọn, chúng ta có thể thêm ràng buộc LIMIT?3
(và nhập một giá trị nhỏ, ví dụ, 10) để việc xác minh trực quan kết quả truy vấn ban đầu không buộc bạn phải xem qua hàng chục nghìn bản ghi.
Bạn có thể kiểm tra hoạt động của lệnh SQL bằng hàm DatabasePrint
, tuy nhiên, hàm này, thật không may, không cho phép làm việc với các truy vấn đã chuẩn bị có tham số. Do đó, chúng ta sẽ phải thay thế việc chuẩn bị tham số SQL '?n' bằng định dạng chuỗi truy vấn sử dụng StringFormat
và thay thế giá trị tham số ở đó. Ngoài ra, có thể hoàn toàn tránh DatabasePrint
và tự xuất kết quả ra nhật ký, từng dòng một (qua mảng DBRow
).
Do đó, đoạn cuối cùng của yêu cầu sẽ biến thành:
...
WHERE (time >= %ld AND time < %ld)
ORDER BY time LIMIT %d;
2
3
Cần lưu ý rằng các giá trị datetime
trong truy vấn này sẽ đến từ MQL5 ở định dạng "máy", tức là số giây kể từ đầu năm 1970. Nếu chúng ta muốn gỡ lỗi cùng truy vấn SQL trong MetaEditor, thì việc viết điều kiện khoảng ngày bằng các chuỗi ký tự ngày (literals) sẽ tiện hơn, như sau:
WHERE (time >= STRFTIME('%s', '2015-01-01') AND time < STRFTIME('%s', '2021-01-01'))
Một lần nữa, chúng ta cần sử dụng hàm STRFTIME ở đây (trình sửa đổi '%s' trong SQL đặt việc chuyển chuỗi ngày được chỉ định sang nhãn "Unix epoch"; việc '%s' giống với chuỗi định dạng MQL5 chỉ là trùng hợp ngẫu nhiên).
Lưu truy vấn SQL đã thiết kế vào một tệp văn bản riêng DBQuotesIntradayLag.sql
và kết nối nó như một tài nguyên vào kịch bản thử nghiệm cùng tên, DBQuotesIntradayLag.mq5
.
#resource "DBQuotesIntradayLag.sql" as string sql1
Tham số đầu tiên của kịch bản cho phép bạn đặt tiền tố trong tên cơ sở dữ liệu, cơ sở này phải đã tồn tại sau khi chạy DBquotesImport.mq5
trên biểu đồ với cùng biểu tượng và khung thời gian. Các đầu vào tiếp theo là cho khoảng ngày và giới hạn độ dài của bản in gỡ lỗi ra nhật ký.
input string Database = "MQL5Book/DB/Quotes";
input datetime SubsetStart = D'2022.01.01';
input datetime SubsetStop = D'2023.01.01';
input int Limit = 10;
2
3
4
Bảng chứa các báo giá đã được biết trước từ kịch bản trước đó.
const string Table = "MqlRatesDB";
Trong hàm OnStart
, chúng ta mở cơ sở dữ liệu và đảm bảo rằng bảng báo giá đã có sẵn.
void OnStart()
{
Print("");
DBSQLite db(Database + _Symbol + PeriodToString());
if(!PRTF(db.isOpen())) return;
if(!PRTF(db.hasTable(Table))) return;
...
}
2
3
4
5
6
7
8
Tiếp theo, chúng ta thay thế các tham số trong chuỗi truy vấn SQL. Chúng ta không chỉ chú ý đến việc thay thế các tham số SQL ?n
bằng các chuỗi định dạng mà còn nhân đôi ký hiệu phần trăm %
trước, vì nếu không hàm StringFormat
sẽ coi chúng là lệnh của riêng nó và sẽ không bỏ qua chúng trong SQL.
string sqlrep = sql1;
StringReplace(sqlrep, "%", "%%");
StringReplace(sqlrep, "?1", "%ld");
StringReplace(sqlrep, "?2", "%ld");
StringReplace(sqlrep, "?3", "%d");
const string sqlfmt = StringFormat(sqlrep, SubsetStart, SubsetStop, Limit);
Print(sqlfmt);
2
3
4
5
6
7
8
Tất cả các thao tác này chỉ cần thiết để thực thi yêu cầu trong ngữ cảnh của hàm DatabasePrint
. Trong phiên bản hoạt động của kịch bản phân tích, chúng ta sẽ đọc kết quả của truy vấn và phân tích chúng theo chương trình, bỏ qua việc định dạng và gọi DatabasePrint
.
Cuối cùng, hãy thực thi truy vấn SQL và xuất bảng kết quả ra nhật ký.
DatabasePrint(db.getHandle(), sqlfmt, 0);
}
2
Đây là những gì chúng ta sẽ thấy cho 10 thanh EURUSD, H1 vào đầu năm 2022.
db.isOpen()=true / ok
db.hasTable(Table)=true / ok
SELECT
DATETIME(time, 'unixepoch') as datetime,
open,
time,
TIME(time, 'unixepoch') AS intraday,
STRFTIME('%w', time, 'unixepoch') AS day,
(LAG(open,-1) OVER (ORDER BY time) - open) AS delta,
SIGN(open - LAG(open) OVER (ORDER BY time)) AS direction,
(LAG(open,-1) OVER (ORDER BY time) - open) * (open - LAG(open) OVER (ORDER BY time))
AS product,
(LAG(open,-1) OVER (ORDER BY time) - open) * SIGN(open - LAG(open) OVER (ORDER BY time))
AS estimate
FROM MqlRatesDB
WHERE (time >= 1640995200 AND time < 1672531200)
ORDER BY time LIMIT 10;
#| datetime open time intraday day delta dir product estimate
--+------------------------------------------------------------------------------------------------
1| 2022-01-03 00:00:00 1.13693 1641168000 00:00:00 1 0.0003200098
2| 2022-01-03 01:00:00 1.13725 1641171600 01:00:00 1 2.999999e-05 1 9.5999478e-09 2.999999e-05
3| 2022-01-03 02:00:00 1.13728 1641175200 02:00:00 1 -0.001060006 1 -3.1799748e-08 -0.001060006
4| 2022-01-03 03:00:00 1.13622 1641178800 03:00:00 1 -0.0003400007 -1 3.6040028e-07 0.0003400007
5| 2022-01-03 04:00:00 1.13588 1641182400 04:00:00 1 -0.001579991 -1 5.3719982e-07 0.001579991
6| 2022-01-03 05:00:00 1.1343 1641186000 05:00:00 1 0.0005299919 -1 -8.3739827e-07 -0.0005299919
7| 2022-01-03 06:00:00 1.13483 1641189600 06:00:00 1 -0.0007699937 1 -4.0809905e-07 -0.0007699937
8| 2022-01-03 07:00:00 1.13406 1641193200 07:00:00 1 -0.0002600149 -1 2.0020098e-07 0.0002600149
9| 2022-01-03 08:00:00 1.1338 1641196800 08:00:00 1 0.000510001 -1 -1.3260079e-07 -0.000510001
10| 2022-01-03 09:00:00 1.13431 1641200400 09:00:00 1 0.0004800036 1 2.4480023e-07 0.0004800036
...
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
Dễ dàng kiểm tra rằng thời gian trong ngày của thanh được phân bổ chính xác, cũng như ngày trong tuần - 1, tương ứng với thứ Hai. Bạn cũng có thể kiểm tra gia tăng delta. Giá trị product
và estimate
trống ở hàng đầu tiên vì chúng yêu cầu hàng trước đó, vốn không tồn tại, để tính toán.
Hãy làm phức tạp truy vấn SQL của chúng ta bằng cách nhóm các bản ghi có cùng kết hợp thời gian trong ngày (intraday
) và ngày trong tuần (day
), đồng thời tính toán một chỉ số mục tiêu nhất định đặc trưng cho sự thành công của giao dịch cho từng kết hợp này. Hãy lấy chỉ số đó là kích thước trung bình của ô product
chia cho độ lệch chuẩn của cùng các sản phẩm đó. Giá trị trung bình của tích gia tăng giá của các thanh lân cận càng lớn, lợi nhuận kỳ vọng càng cao, và độ phân tán của các sản phẩm này càng nhỏ, dự báo càng ổn định. Tên của chỉ số trong truy vấn SQL là objective
.
Ngoài chỉ số mục tiêu, chúng ta cũng sẽ tính toán ước tính lợi nhuận (backtest_profit
) và hệ số lợi nhuận (backtest_PF
). Chúng ta sẽ ước tính lợi nhuận là tổng các gia tăng giá (estimate
) cho tất cả các thanh trong ngữ cảnh thời gian trong ngày và ngày trong tuần (kích thước của thanh mở như một gia tăng giá là tương tự với lợi nhuận tương lai tính bằng điểm cho mỗi thanh). Hệ số lợi nhuận theo truyền thống là thương số của các gia tăng dương và âm.
SELECT
AVG(product) / STDDEV(product) AS objective,
SUM(estimate) AS backtest_profit,
SUM(CASE WHEN estimate >= 0 THEN estimate ELSE 0 END) /
SUM(CASE WHEN estimate < 0 THEN -estimate ELSE 0 END) AS backtest_PF,
intraday, day
FROM
(
SELECT
time,
TIME(time, 'unixepoch') AS intraday,
STRFTIME('%w', time, 'unixepoch') AS day,
(LAG(open,-1) OVER (ORDER BY time) - open) AS delta,
SIGN(open - LAG(open) OVER (ORDER BY time)) AS direction,
(LAG(open,-1) OVER (ORDER BY time) - open) * (open - LAG(open) OVER (ORDER BY time))
AS product,
(LAG(open,-1) OVER (ORDER BY time) - open) * SIGN(open - LAG(open) OVER (ORDER BY time))
AS estimate
FROM MqlRatesDB
WHERE (time >= STRFTIME('%s', '2015-01-01') AND time < STRFTIME('%s', '2021-01-01'))
)
GROUP BY intraday, day
ORDER BY objective DESC
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Truy vấn SQL đầu tiên đã trở thành truy vấn lồng nhau, từ đó chúng ta giờ tích lũy dữ liệu bằng một truy vấn SQL bên ngoài. Việc nhóm theo tất cả các kết hợp của thời gian và ngày trong tuần mang lại "thêm" từ GROUP BY intraday, day
. Ngoài ra, chúng ta đã thêm sắp xếp theo chỉ số mục tiêu (ORDER BY objective DESC
) để các lựa chọn tốt nhất nằm ở đầu bảng.
Trong truy vấn lồng nhau, chúng ta đã loại bỏ tham số LIMIT, vì số lượng nhóm trở nên chấp nhận được, ít hơn nhiều so với số lượng thanh được phân tích. Vì vậy, đối với H1, chúng ta nhận được 120 lựa chọn (24 * 5).
Truy vấn mở rộng được đặt trong tệp văn bản DBQuotesIntradayLagGroup.sql
, tệp này lần lượt được kết nối như một tài nguyên vào kịch bản thử nghiệm cùng tên, DBQuotesIntradayLagGroup.mq5
. Mã nguồn của nó ít khác biệt so với mã trước đó, vì vậy chúng ta sẽ hiển thị ngay kết quả của việc chạy nó cho khoảng ngày mặc định: từ đầu năm 2015 đến đầu năm 2021 (không bao gồm 2021 và 2022).
db.isOpen()=true / ok
db.hasTable(Table)=true / ok
SELECT
AVG(product) / STDDEV(product) AS objective,
SUM(estimate) AS backtest_profit,
SUM(CASE WHEN estimate >= 0 THEN estimate ELSE 0 END) /
SUM(CASE WHEN estimate < 0 THEN -estimate ELSE 0 END) AS backtest_PF,
intraday, day
FROM
(
SELECT
...
FROM MqlRatesDB
WHERE (time >= 1420070400 AND time < 1609459200)
)
GROUP BY intraday, day
ORDER BY objective DESC
#| objective backtest_profit backtest_PF intraday day
---+---------------------------------------------------------------------------
1| 0.16713214428916 0.073200000000001 1.46040631486258 16:00:00 5
2| 0.118128291843983 0.0433099999999995 1.33678071539657 20:00:00 3
3| 0.103701251751617 0.00929999999999853 1.14148790506616 05:00:00 2
4| 0.102930330078208 0.0164399999999973 1.1932071923845 08:00:00 4
5| 0.089531492651001 0.0064300000000006 1.10167615433271 07:00:00 2
6| 0.0827628326995007 -8.99999999970369e-05 0.999601152226913 17:00:00 4
7| 0.0823433025146974 0.0159700000000012 1.21665988332657 21:00:00 1
8| 0.0767938336191962 0.00522999999999874 1.04226945769012 13:00:00 1
9| 0.0657741522256548 0.0162299999999986 1.09699976093712 15:00:00 2
10| 0.0635243373432768 0.00932000000000044 1.08294766820933 22:00:00 3
...
110| -0.0814131025461459 -0.0189100000000015 0.820605255668329 21:00:00 5
111| -0.0899571263478305 -0.0321900000000028 0.721250432975386 22:00:00 4
112| -0.0909772560603298 -0.0226100000000016 0.851161872161138 19:00:00 4
113| -0.0961794181717023 -0.00846999999999931 0.936377976414036 12:00:00 5
114| -0.108868074018582 -0.0246099999999998 0.634920634920637 00:00:00 5
115| -0.109368419185336 -0.0250700000000013 0.744496534855268 08:00:00 2
116| -0.121893581607986 -0.0234599999999998 0.610945273631843 00:00:00 3
117| -0.135416609546408 -0.0898899999999971 0.343437294573087 00:00:00 1
118| -0.142128458003631 -0.0255200000000018 0.681835182645536 06:00:00 4
119| -0.142196924506816 -0.0205700000000004 0.629769618430515 00:00:00 2
120| -0.15200009633513 -0.0301499999999988 0.708864426419475 02:00:00 1
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
Do đó, phân tích cho chúng ta biết rằng thanh H1 lúc 16 giờ vào thứ Sáu là ứng cử viên tốt nhất để tiếp tục xu hướng dựa trên thanh trước đó. Tiếp theo trong ưu tiên là thanh lúc 20 giờ thứ Tư. Và cứ như vậy.
Tuy nhiên, nên kiểm tra các cài đặt đã tìm thấy trên khoảng thời gian tương lai.
Để làm điều này, chúng ta có thể thực thi truy vấn SQL hiện tại không chỉ trên khoảng ngày "quá khứ" (trong bài kiểm tra của chúng ta đến năm 2021) mà còn một lần nữa trong "tương lai" (từ đầu năm 2021). Kết quả của cả hai truy vấn nên được nối (JOIN) theo các nhóm của chúng ta (intraday
, day
). Sau đó, trong khi giữ nguyên sắp xếp theo chỉ số mục tiêu, chúng ta sẽ thấy trong các cột liền kề lợi nhuận và hệ số lợi nhuận cho cùng các kết hợp của thời gian và ngày trong tuần, và mức độ chúng giảm xuống.
Dưới đây là truy vấn SQL cuối cùng (được rút gọn):
SELECT * FROM
(
SELECT
AVG(product) / STDDEV(product) AS objective,
SUM(estimate) AS backtest_profit,
SUM(CASE WHEN estimate >= 0 THEN estimate ELSE 0 END) /
SUM(CASE WHEN estimate < 0 THEN -estimate ELSE 0 END) AS backtest_PF,
intraday, day
FROM
(
SELECT ...
FROM MqlRatesDB
WHERE (time >= STRFTIME('%s', '2015-01-01') AND time < STRFTIME('%s', '2021-01-01'))
)
GROUP BY intraday, day
) backtest
JOIN
(
SELECT
SUM(estimate) AS forward_profit,
SUM(CASE WHEN estimate >= 0 THEN estimate ELSE 0 END) /
SUM(CASE WHEN estimate < 0 THEN -estimate ELSE 0 END) AS forward_PF,
intraday, day
FROM
(
SELECT ...
FROM MqlRatesDB
WHERE (time >= STRFTIME('%s', '2021-01-01'))
)
GROUP BY intraday, day
) forward
USING(intraday, day)
ORDER BY objective DESC
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
Văn bản đầy đủ của yêu cầu được cung cấp trong tệp DBQuotesIntradayBackAndForward.sql
. Nó được kết nối như một tài nguyên trong kịch bản DBQuotesIntradayBackAndForward.mq5
.
Bằng cách chạy kịch bản với cài đặt mặc định, chúng ta nhận được các chỉ số sau (với các từ viết tắt):
#| objective backtest_profit backtest_PF intraday day forward_profit forward_PF
--+------------------------------------------------------------------------------------------------
1| 0.16713214428916 0.073200000001 1.46040631486 16:00:00 5 0.004920000048 1.12852664576
2| 0.118128291843983 0.0433099999995 1.33678071539 20:00:00 3 0.007880000055 1.277856135
3| 0.103701251751617 0.00929999999853 1.14148790506 05:00:00 2 0.002210000082 1.12149532710
4| 0.102930330078208 0.0164399999973 1.1932071923 08:00:00 4 0.001409999969 1.07253086419
5| 0.089531492651001 0.0064300000006 1.10167615433 07:00:00 2 -0.009119999869 0.561749159058
6| 0.0827628326995007 -8.99999999970e-05 0.999601152226 17:00:00 4 0.009070000091 1.18809622563
7| 0.0823433025146974 0.0159700000012 1.21665988332 21:00:00 1 0.00250999999 1.12131464475
8| 0.0767938336191962 0.00522999999874 1.04226945769 13:00:00 1 -0.008490000055 0.753913043478
9| 0.0657741522256548 0.0162299999986 1.09699976093 15:00:00 2 0.01423999997 1.34979120609
10| 0.0635243373432768 0.00932000000044 1.08294766820 22:00:00 3 -0.00456999993 0.828967065868
...
2
3
4
5
6
7
8
9
10
11
12
13
Vì vậy, hệ thống giao dịch với lịch giao dịch tốt nhất được tìm thấy tiếp tục cho thấy lợi nhuận trong khoảng thời gian "tương lai", mặc dù không lớn như trên backtest.
Tất nhiên, ví dụ được xem xét chỉ là một trường hợp cụ thể của hệ thống giao dịch. Chúng ta có thể, ví dụ, tìm các kết hợp của thời gian và ngày trong tuần khi chiến lược đảo chiều hoạt động trên các thanh lân cận, hoặc dựa trên các nguyên tắc hoàn toàn khác (phân tích tick, lịch, danh mục tín hiệu giao dịch, v.v.).
Điều cốt lõi là công cụ SQLite cung cấp nhiều công cụ tiện lợi mà bạn sẽ cần tự triển khai trong MQL5. Thành thật mà nói, việc học SQL đòi hỏi thời gian. Nền tảng này cho phép bạn chọn sự kết hợp tối ưu của hai công nghệ để lập trình hiệu quả.