Giải phương trình
Trong các phương pháp học máy và bài toán tối ưu hóa, thường cần tìm nghiệm của một hệ phương trình tuyến tính. MQL5 cung cấp bốn phương thức cho phép giải các phương trình như vậy tùy thuộc vào loại ma trận.
Solve
: giải phương trình ma trận tuyến tính hoặc hệ phương trình đại số tuyến tínhLstSq
: giải gần đúng hệ phương trình đại số tuyến tính (dành cho ma trận không vuông hoặc suy biến)Inv
: tính ma trận nghịch đảo nhân tính đối với ma trận vuông không suy biến bằng phương pháp Jordan-GaussPInv
: tính ma trận giả nghịch đảo theo phương pháp Moore-Penrose
Dưới đây là các nguyên mẫu của phương thức.
vector<T> matrix<T>::Solve(const vector<T> b)
vector<T> matrix<T>::LstSq(const vector<T> b)
matrix<T> matrix<T>::Inv()
matrix<T> matrix<T>::PInv()
Phương thức Solve
và LstSq
ngụ ý giải hệ phương trình dạng A*X=B
, trong đó A là ma trận, B là vector được truyền qua tham số với các giá trị của hàm (hoặc "biến phụ thuộc").
Hãy thử áp dụng phương thức LstSq
để giải một hệ phương trình, mô hình giao dịch danh mục lý tưởng (trong trường hợp này, chúng ta sẽ phân tích danh mục các đồng tiền chính trên Forex). Để làm điều này, trên một số lượng thanh "lịch sử" đã cho, chúng ta cần tìm kích thước lô cho mỗi đồng tiền sao cho đường số dư có xu hướng là một đường thẳng tăng liên tục.
Gọi cặp tiền thứ i
là S
i
. Giá của nó tại thanh có chỉ số k
là S
i
[k]
. Việc đánh số các thanh sẽ đi từ quá khứ đến tương lai, như trong ma trận và vector được điền bởi phương thức CopyRates. Do đó, điểm bắt đầu của các báo giá thu thập để huấn luyện mô hình tương ứng với thanh được đánh số 0, nhưng trên dòng thời gian, đó sẽ là thanh lịch sử lâu đời nhất (trong số những thanh mà chúng ta xử lý, theo cài đặt thuật toán). Các thanh bên phải (hướng về tương lai) từ đó được đánh số 1, 2, v.v., cho đến tổng số thanh mà người dùng yêu cầu tính toán.
Sự thay đổi giá của một biểu tượng giữa thanh thứ 0 và thanh thứ N xác định lợi nhuận (hoặc lỗ) tại thời điểm thanh thứ N.
Xem xét tập hợp các đồng tiền, ví dụ, chúng ta có phương trình lợi nhuận sau cho thanh thứ 1:
(S1[1] - S1[0]) * X1 + (S2[1] - S2[0]) * X2 + ... + (Sm[1] - Sm[0]) * Xm = B
Ở đây, m
là tổng số biểu tượng, X
i
là kích thước lô của mỗi biểu tượng, và B
là lợi nhuận thả nổi (số dư điều kiện, nếu bạn khóa lợi nhuận).
Để đơn giản, hãy rút gọn ký hiệu. Chuyển từ giá trị tuyệt đối sang mức tăng giá (A
i
[k] = S
i
[k] - S
i
[0]
). Xem xét sự di chuyển qua các thanh, chúng ta sẽ thu được một số biểu thức cho đường cong số dư ảo:
A1[1] * X1 + A2[1] * X2 + ... + Am[1] * Xm = B[1]
A1[2] * X1 + A2[2] * X2 + ... + Am[2] * Xm = B[2]
...
A1[K] * X1 + A2[K] * X2 + ... + Am[K] * Xm = B[K]
2
3
4
Giao dịch thành công được đặc trưng bởi lợi nhuận không đổi trên mỗi thanh, tức là mô hình cho vector bên phải B
là một hàm tăng đơn điệu, lý tưởng là một đường thẳng.
Hãy triển khai mô hình này và chọn các hệ số X
dựa trên báo giá. Vì chúng ta chưa biết API ứng dụng, chúng ta sẽ không lập trình một chiến lược giao dịch đầy đủ. Chỉ cần xây dựng một biểu đồ số dư ảo bằng hàm GraphPlot
từ tệp tiêu đề chuẩn Graphic.mqh
(chúng ta đã sử dụng nó để thể hiện các hàm toán học).
Mã nguồn đầy đủ cho ví dụ mới nằm trong tập lệnh MatrixForexBasket.mq5
.
Trong tham số đầu vào, hãy để người dùng chọn tổng số thanh để lấy mẫu dữ liệu (BarCount
), cũng như số thứ tự thanh trong lựa chọn này (BarOffset
) nơi quá khứ điều kiện kết thúc và tương lai điều kiện bắt đầu.
Mô hình sẽ được xây dựng trên quá khứ điều kiện (hệ phương trình tuyến tính trên sẽ được giải), và một bài kiểm tra tiến sẽ được thực hiện trên tương lai điều kiện.
input int BarCount = 20; // BarCount (lịch sử đã biết và tương lai)
input int BarOffset = 10; // BarOffset (nơi tương lai bắt đầu)
input ENUM_CURVE_TYPE CurveType = CURVE_LINES;
2
3
Để điền vector với số dư lý tưởng, chúng ta viết hàm ConstantGrow
: nó sẽ được sử dụng sau trong quá trình khởi tạo.
void ConstantGrow(vector &v)
{
for(ulong i = 0; i < v.Size(); ++i)
{
v[i] = (double)(i + 1);
}
}
2
3
4
5
6
7
Danh sách các công cụ giao dịch (các cặp Forex chính) được cố định ở đầu hàm OnStart
– chỉnh sửa nó để phù hợp với yêu cầu và môi trường giao dịch của bạn.
void OnStart()
{
const string symbols[] =
{
"EURUSD", "GBPUSD", "USDJPY", "USDCAD",
"USDCHF", "AUDUSD", "NZDUSD"
};
const int size = ArraySize(symbols);
...
2
3
4
5
6
7
8
9
Hãy tạo ma trận rates
để thêm báo giá biểu tượng, vector model
với đường cong số dư mong muốn, và vector phụ close
để yêu cầu giá đóng cửa của thanh theo từng biểu tượng (dữ liệu từ nó sẽ được sao chép vào các cột của ma trận rates
).
matrix rates(BarCount, size);
vector model(BarCount - BarOffset, ConstantGrow);
vector close;
2
3
Trong vòng lặp biểu tượng, chúng ta sao chép giá đóng cửa vào vector close
, tính toán mức tăng giá, và ghi chúng vào cột tương ứng của ma trận rates
.
for(int i = 0; i < size; i++)
{
if(close.CopyRates(symbols[i], _Period, COPY_RATES_CLOSE, 0, BarCount))
{
// tính toán mức tăng (lợi nhuận trên tất cả và trên mỗi thanh trong một dòng)
close -= close[0];
// điều chỉnh lợi nhuận theo giá trị pip
close *= SymbolInfoDouble(symbols[i], SYMBOL_TRADE_TICK_VALUE) /
SymbolInfoDouble(symbols[i], SYMBOL_TRADE_TICK_SIZE);
// đặt vector vào cột ma trận
rates.Col(close, i);
}
else
{
Print("vector.CopyRates(%d, COPY_RATES_CLOSE) failed. Error ",
symbols[i], _LastError);
return;
}
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Chúng ta sẽ xem xét việc tính toán giá trị một điểm giá (bằng tiền gửi) trong Phần 5.
Cũng cần lưu ý rằng các thanh có cùng chỉ số có thể có thời gian khác nhau trên các công cụ tài chính khác nhau, ví dụ, nếu có ngày lễ ở một quốc gia và thị trường đóng cửa (ngoài Forex, các biểu tượng có thể có lịch giao dịch khác nhau về mặt lý thuyết). Để giải quyết vấn đề này, chúng ta cần phân tích sâu hơn về báo giá, xem xét thời gian thanh và đồng bộ hóa chúng trước khi chèn vào ma trận rates
. Chúng ta không làm điều này ở đây để giữ sự đơn giản, và cũng vì thị trường Forex hoạt động theo cùng quy tắc hầu hết thời gian.
Chúng ta chia ma trận thành hai phần: phần đầu sẽ được sử dụng để tìm nghiệm (mô phỏng tối ưu hóa trên lịch sử), và phần tiếp theo sẽ được dùng để kiểm tra tiến (tính toán thay đổi số dư tiếp theo).
matrix split[];
if(BarOffset > 0)
{
// huấn luyện trên BarCount - BarOffset thanh
// kiểm tra trên BarOffset thanh
ulong parts[] = {BarCount - BarOffset, BarOffset};
rates.Split(parts, 0, split);
}
// giải hệ phương trình tuyến tính cho mô hình
vector x = (BarOffset > 0) ? split[0].LstSq(model) : rates.LstSq(model);
Print("Solution (lots per symbol): ");
Print(x);
...
2
3
4
5
6
7
8
9
10
11
12
13
14
Bây giờ, khi chúng ta có nghiệm, hãy xây dựng đường cong số dư cho tất cả các thanh của mẫu (phần "lịch sử" lý tưởng sẽ ở đầu, sau đó phần "tương lai" bắt đầu, phần này không được sử dụng để điều chỉnh mô hình).
vector balance = vector::Zeros(BarCount);
for(int i = 1; i < BarCount; ++i)
{
balance[i] = 0;
for(int j = 0; j < size; ++j)
{
balance[i] += (float)(rates[i][j] * x[j]);
}
}
...
2
3
4
5
6
7
8
9
10
Hãy đánh giá chất lượng nghiệm bằng tiêu chí R2.
if(BarOffset > 0)
{
// tạo bản sao của số dư
vector backtest = balance;
// chỉ chọn các thanh "lịch sử" để kiểm tra ngược
backtest.Resize(BarCount - BarOffset);
// các thanh cho kiểm tra tiến phải được sao chép thủ công
vector forward(BarOffset);
for(int i = 0; i < BarOffset; ++i)
{
forward[i] = balance[BarCount - BarOffset + i];
}
// tính toán chỉ số hồi quy độc lập cho cả hai phần
Print("Backtest R2 = ", backtest.RegressionMetric(REGRESSION_R2));
Print("Forward R2 = ", forward.RegressionMetric(REGRESSION_R2));
}
else
{
Print("R2 = ", balance.RegressionMetric(REGRESSION_R2));
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Để hiển thị đường cong số dư trên biểu đồ, cần chuyển dữ liệu từ vector sang mảng.
double array[];
balance.Swap(array);
// in giá trị của số dư thay đổi với độ chính xác 2 chữ số
Print("Balance: ");
ArrayPrint(array, 2);
// vẽ đường cong số dư trong đối tượng biểu đồ ("kiểm tra ngược" và "tiến")
GraphPlot(array, CurveType);
}
2
3
4
5
6
7
8
9
10
Dưới đây là ví dụ về nhật ký thu được khi chạy tập lệnh trên EURUSD,H1.
Solution (lots per symbol):
[-0.0057809334,-0.0079846876,0.0088985749,-0.0041461736,-0.010710154,-0.0025694175,0.01493552]
Backtest R2 = 0.9896645616246145
Forward R2 = 0.8667852183780984
Balance:
0.00 1.68 3.38 3.90 5.04 5.92 7.09 7.86 9.17 9.88
9.55 10.77 12.06 13.67 15.35 15.89 16.28 15.91 16.85 16.58
2
3
4
5
6
7
Và đây là hình ảnh của đường cong số dư ảo.
Hình ảnh: Số dư ảo của việc giao dịch danh mục tiền tệ theo lô dựa trên quyết định
Phần bên trái có hình dạng đều hơn và R2 cao hơn, điều này không có gì đáng ngạc nhiên vì mô hình (các biến X
) đã được điều chỉnh đặc biệt cho nó.
Chỉ để thử, chúng ta sẽ tăng độ sâu của việc huấn luyện và kiểm tra lên 10 lần, tức là đặt trong tham số BarCount = 200
và BarOffset = 100
. Chúng ta sẽ nhận được một bức tranh mới.
Hình ảnh: Số dư ảo của việc giao dịch danh mục tiền tệ theo lô dựa trên quyết định
Phần "tương lai" trông kém mượt mà hơn, và thậm chí có thể nói chúng ta may mắn vì nó tiếp tục tăng, mặc dù mô hình đơn giản như vậy. Thông thường, trong quá trình kiểm tra tiến, đường cong số dư ảo suy giảm đáng kể và bắt đầu đi xuống.
Điều quan trọng cần lưu ý là để kiểm tra mô hình, chúng ta đã lấy các giá trị X
từ nghiệm "nguyên bản" của hệ phương trình, trong khi thực tế chúng ta cần chuẩn hóa chúng theo lô tối thiểu và bước lô, điều này sẽ ảnh hưởng tiêu cực đến kết quả và đưa chúng gần hơn với thực tế.