Cách dự đoán giá cổ phiếu hiệu quả bằng mô hình LSTM

1. Bài toán

Dự đoán giá cổ phiếu sử dụng dữ liệu lịch sử giá, nhằm ước tính giá cổ phiếu của ngày tiếp theo. Mục tiêu là phát triển mô hình học máy hiệu quả, dựa trên chuỗi thời gian về lịch sử giao dịch, để cung cấp dự đoán chính xác, hỗ trợ quyết định đầu tư.

2. Chuẩn bị môi trường và dữ liệu

2.1. Cấu trúc chương trình

Cấu trúc chương trình trong bài:

root@aicandy:/aicandy/projects/AIcandy_LSTM_Stock_iiyiedys# tree
.
├── AIcandy_LSTM_model_hdbhkibl.py
├── AIcandy_LSTM_train_emabpupv.py
├── history_price.csv
└── requirements.txt

0 directories, 4 files
root@aicandy:/aicandy/projects/AIcandy_LSTM_Stock_iiyiedys#

2.2. Cài đặt môi trường

Sử dụng pip để cài đặt các thư viện cần thiết

pip install -r requirements.txt

2.3. Dữ liệu

Dữ liệu sử dụng trong bài viết này là lịch sử giá cổ phiếu của ngân hàng ACB, được lưu trong file CSV với các cột thông tin: Ticker (mã cổ phiếu), DTYYYYMMDD (ngày giao dịch), OpenFixed (giá mở cửa), HighFixed (giá cao nhất), LowFixed (giá thấp nhất), CloseFixed (giá đóng cửa), Volume.

Trong bài, chúng ta sẽ tập trung sử dụng giá đóng cửa (CloseFixed ) làm đặc trưng để dự đoán giá đóng cửa của ngày tiếp theo. Biểu đồ giá theo thời gian như ảnh dưới:

aicandy.vn

Dưới đây là thông tin của 10 phiên giao dịch đầu tiên trong lịch sử giá:

    Ticker  DTYYYYMMDD  OpenFixed  HighFixed  LowFixed  CloseFixed  Volume
0    ACB    20070412    15.3731    15.3731   14.1906     14.2511   57200
1    ACB    20070413    15.3456    15.3456   13.7505     14.6306  134400
2    ACB    20070416    14.8231    15.4006   13.5855     13.5855   74700
3    ACB    20070417    13.7505    13.7505   13.2115     13.4040   85100
4    ACB    20070418    14.0256    14.3006   13.4755     14.2951  117200
5    ACB    20070419    14.5756    14.5756   13.5690     13.6955  118400
6    ACB    20070420    14.2456    14.2456   13.2005     13.3105  135000
7    ACB    20070423    12.4272    12.4558   12.0267     12.0839   40300
8    ACB    20070424    12.1554    12.1697   11.4404     11.9481  136100
9    ACB    20070425    12.1554    13.0278   11.7979     13.0278  165200

3. Xây dựng model

Sử dụng mô hình LSTM kết hợp với một hàm để tạo ra chuỗi dữ liệu đầu vào từ dữ liệu gốc cho mô hình dự đoán theo chuỗi thời gian.

Xây dựng lớp PricePredictionModel kế thừa từ nn.Module của PyTorch, đại diện cho mô hình dự đoán giá dựa trên LSTM.

class PricePredictionModel(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size, dropout_rate=0.2):
        super(PricePredictionModel, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout_rate)
        self.dropout = nn.Dropout(dropout_rate)
        self.fc = nn.Linear(hidden_size, output_size)

Trong đó:

  • __init__(): Đây là hàm khởi tạo để định nghĩa các thành phần của mô hình.
  • input_size: Kích thước đầu vào (số đặc trưng của dữ liệu, ví dụ: giá mở cửa, đóng cửa…).
  • hidden_size: Kích thước của tầng ẩn LSTM (số lượng đơn vị ẩn).
  • num_layers: Số lớp LSTM xếp chồng lên nhau.
  • output_size: Kích thước đầu ra (ví dụ: dự đoán giá đóng cửa tiếp theo).
  • dropout_rate: Tỷ lệ dropout để giảm overfitting.
  • self.lstm: Định nghĩa một mạng LSTM với số tầng, kích thước ẩn và kích thước đầu vào đã cung cấp. batch_first=True nghĩa là đầu vào có dạng (batch_size, sequence_length, input_size).
  • self.dropout: Thêm một tầng dropout để ngăn chặn overfitting.
  • self.fc: Một tầng fully connected (tầng kết nối đầy đủ) để chuyển đổi đầu ra của LSTM thành dự đoán cuối cùng.

Tiếp theo, tạo hàm forward để định nghĩa cách dữ liệu đi qua mô hình

def forward(self, x):
    h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device).to(x.dtype)
    c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device).to(x.dtype)
        
    out, _ = self.lstm(x, (h0, c0))
    out = self.dropout(out[:, -1, :])
    out = self.fc(out)
    return out

Trong đó:

  • h0, c0: Các trạng thái ẩn (hidden states) ban đầu của LSTM. Đây là các ma trận zero với kích thước phù hợp dựa trên num_layers và hidden_size.
  • out, _ = self.lstm(x, (h0, c0)): Đưa dữ liệu đầu vào qua LSTM. out chứa các đầu ra của LSTM tại mỗi thời điểm của chuỗi.
  • out = self.dropout(out[:, -1, :]): Sử dụng giá trị của chuỗi tại thời điểm cuối cùng (out[:, -1, :]) và áp dụng dropout.
  • out = self.fc(out): Đưa giá trị này qua tầng fully connected để nhận kết quả dự đoán cuối cùng.

Bước tiếp theo là tạo hàm xử lý dữ liệu đầu vào để tạo các chuỗi có độ dài cố định từ dữ liệu đầu vào:

def create_sequences(data, seq_length):
    sequences = []
    targets = []
    for i in range(len(data) - seq_length):
        seq = data[i:i+seq_length]
        target = data[i+seq_length]
        sequences.append(seq)
        targets.append(target)
    return np.array(sequences), np.array(targets)

Trong đó

  • data: Dữ liệu gốc, ví dụ là một chuỗi giá cổ phiếu.
  • seq_length: Độ dài của mỗi chuỗi đầu vào mà mô hình sẽ sử dụng.
  • Hàm duyệt qua dữ liệu, lấy các đoạn liên tiếp có độ dài seq_length làm chuỗi đầu vào (seq), và giá trị tiếp theo ngay sau đó làm nhãn (target). Sau đó lưu các chuỗi và nhãn vào sequences và targets, cuối cùng chuyển chúng thành mảng NumPy và trả về.

4. Chương trình huấn luyện

Bước 1: Load dữ liệu từ file

df = pd.read_csv(data_file)
prices = df['CloseFixed'].values.reshape(-1, 1)
scaler = StandardScaler()
prices_scaled = scaler.fit_transform(prices).flatten()

Trong đó:

  • prices = df[‘CloseFixed’].values.reshape(-1, 1): Trích xuất cột ‘CloseFixed’ (cột giá đóng cửa đã được điều chỉnh) từ DataFrame và chuyển đổi nó thành mảng NumPy.
  • scaler = StandardScaler(): Dùng để khởi tạo bộ chuẩn hóa dữ liệu StandardScaler. Nó chuẩn hóa dữ liệu sao cho dữ liệu có giá trị trung bình là 0 và độ lệch chuẩn là 1.
  • prices_scaled = scaler.fit_transform(prices).flatten(): Chuẩn hóa dữ liệu giá đóng cửa và chuyển nó thành mảng 1 chiều.

Sau khi chuẩn hóa và đưa về mảng 1 chiều, 10 giá trị của CloseFixed như sau:

[0.71767401 0.77455819 0.61790561 0.59070014 0.72426928 0.63439378
 0.5766852  0.39282714 0.37247175 0.53431061]

Bước 2: Tạo mỗi chuỗi dữ liệu với kích thước là 20 giá trị lịch sử và chia dữ liệu thành bộ train và test để đánh giá

sequences, targets = create_sequences(prices_scaled, SEQUENCE_LENGTH)
X_train, X_test, y_train, y_test = train_test_split(sequences, targets, test_size=0.2, random_state=42)

Tiếp theo là chuyển các chuỗi dữ liệu sang tensor để phù hợp với pytorch.

X_train = torch.FloatTensor(X_train).to(device)
y_train = torch.FloatTensor(y_train).to(device)
X_test = torch.FloatTensor(X_test).to(device)
y_test = torch.FloatTensor(y_test).to(device)

Bước 3: Khởi tạo mô hình và khởi tạo hàm loss và optimizer

model = PricePredictionModel(input_size=1, hidden_size=HIDDEN_SIZE, num_layers=NUM_LAYERS, output_size=1).to(device)
print(model)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE, weight_decay=1e-5)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=10, factor=0.5)

Khi đó, thông tin model sẽ như sau:

PricePredictionModel(
  (lstm): LSTM(1, 64, num_layers=2, batch_first=True, dropout=0.2)
  (dropout): Dropout(p=0.2, inplace=False)
  (fc): Linear(in_features=64, out_features=1, bias=True)
)

Bước 4: Kiểm tra checkpoint, nếu đã có checkpoint thì sẽ tiếp tục train từ checkpoint này để tăng hiệu suất và tiết kiệm thời gian train.

if os.path.exists(checkpoint_path):
    checkpoint = torch.load(checkpoint_path, map_location=device)
    model.load_state_dict(checkpoint['model_state_dict'])
    optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
    start_epoch = checkpoint['epoch'] + 1
    best_val_loss = checkpoint['best_val_loss']
    print(f"Resuming training from epoch {start_epoch}")
else:
    best_val_loss = float('inf')

Bước 5: Trong mỗi epoch, sẽ đưa dữ liệu vào huấn luyện và tính toán sai số của dữ liệu train và dữ liệu val

for batch_x, batch_y in train_loader:
    optimizer.zero_grad()
    outputs = model(batch_x.unsqueeze(-1))
    loss = criterion(outputs, batch_y.unsqueeze(-1))
    loss.backward()
    optimizer.step()
    train_loss += loss.item()
train_loss /= len(train_loader)
train_losses.append(train_loss)

Bước 6: Một kỹ thuật hay là lưu lại model khi có sai số val_loss được cải thiện, và kỹ thuật dừng khi sau nhiều epoch mà chất lượng mô hình không được cải thiện (earlystop).

if val_loss < best_val_loss: 
    best_val_loss = val_loss 
    patience_counter = 0 
    torch.save({ 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'best_val_loss': best_val_loss, 'train_loss': train_loss, 'val_loss': val_loss, }, checkpoint_path)
    print("Checkpoint saved.") 
else: patience_counter += 1 
    if patience_counter >= PATIENCE:
        print(f'Early stopping triggered after {epoch+1} epochs')
        break

Kết quả train
Huấn luyện chương trình từ đầu:

root@aicandy:/aicandy/projects/AIcandy_LSTM_Stock_iiyiedys# python AIcandy_LSTM_train_emabpupv.py --data_file history_price.csv --num_epochs 10 --batch_size 32 --checkpoint_path aicandy_lstm_checkpoint_xpisdedn.pth
Using device: cuda
Epoch [1/10], Train Loss: 0.2023, Val Loss: 0.0090
Checkpoint saved.
Epoch [2/10], Train Loss: 0.0155, Val Loss: 0.0059
Checkpoint saved.
Epoch [3/10], Train Loss: 0.0147, Val Loss: 0.0079
Epoch [4/10], Train Loss: 0.0118, Val Loss: 0.0083
Epoch [5/10], Train Loss: 0.0134, Val Loss: 0.0063
Epoch [6/10], Train Loss: 0.0116, Val Loss: 0.0068
Epoch [7/10], Train Loss: 0.0113, Val Loss: 0.0049
Checkpoint saved.
Epoch [8/10], Train Loss: 0.0101, Val Loss: 0.0047
Checkpoint saved.
Epoch [9/10], Train Loss: 0.0116, Val Loss: 0.0045
Checkpoint saved.
Epoch [10/10], Train Loss: 0.0102, Val Loss: 0.0036
Checkpoint saved.
Predicted price for the next day: 25.63
root@aicandy:/aicandy/projects/AIcandy_LSTM_Stock_iiyiedys#

Trường hợp huấn luyện từ checkpoint đã có:

root@aicandy:/aicandy/projects/AIcandy_LSTM_Stock_iiyiedys# python AIcandy_LSTM_train_emabpupv.py --data_file history_price.csv --num_epochs 20 --batch_size 32 --checkpoint_path aicandy_lstm_checkpoint_xpisdedn.pth
Using device: cuda
Resuming training from epoch 10
Epoch [11/20], Train Loss: 0.0110, Val Loss: 0.0035
Checkpoint saved.
Epoch [12/20], Train Loss: 0.0098, Val Loss: 0.0043
Epoch [13/20], Train Loss: 0.0089, Val Loss: 0.0036
Epoch [14/20], Train Loss: 0.0101, Val Loss: 0.0037
Epoch [15/20], Train Loss: 0.0095, Val Loss: 0.0032
Checkpoint saved.
Epoch [16/20], Train Loss: 0.0103, Val Loss: 0.0036
Epoch [17/20], Train Loss: 0.0098, Val Loss: 0.0034
Epoch [18/20], Train Loss: 0.0091, Val Loss: 0.0044
Epoch [19/20], Train Loss: 0.0091, Val Loss: 0.0029
Checkpoint saved.
Epoch [20/20], Train Loss: 0.0091, Val Loss: 0.0034
Predicted price for the next day: 25.76
root@aicandy:/aicandy/projects/AIcandy_LSTM_Stock_iiyiedys#

Kết quả:

Chương trình dự đoán kết quả ngày tiếp theo là 25.76. 

aicandy.vn

5. Kết luận

Với sự phát triển của các mô hình học sâu như LSTM (Long Short-Term Memory), việc phân tích dữ liệu chuỗi thời gian đã trở nên khả thi và hiệu quả hơn. Mô hình LSTM, với khả năng ghi nhớ các mối liên hệ dài hạn trong chuỗi dữ liệu, giúp cải thiện đáng kể độ chính xác trong việc dự đoán xu hướng giá cổ phiếu.

Tuy nhiên cũng đối diện với nhiều thách thức và nguy cơ do đặc thù của dữ liệu giá cổ phiếu như:

Tính biến động cao của thị trường

Thị trường chứng khoán là một môi trường phức tạp và biến động mạnh mẽ. Giá cổ phiếu có thể thay đổi đột ngột do các yếu tố không thể lường trước như chính trị, khủng hoảng kinh tế, các sự kiện bất ngờ hoặc thay đổi chính sách. Mô hình học máy, dù mạnh mẽ, vẫn gặp khó khăn trong việc phản ứng kịp thời với những sự kiện bất khả kháng này.

Dữ liệu quá khứ không phản ánh hoàn toàn tương lai

Hầu hết các mô hình dự đoán, bao gồm LSTM, dựa trên dữ liệu lịch sử để dự đoán xu hướng tương lai. Tuy nhiên, sự biến đổi của thị trường tài chính có thể phụ thuộc vào nhiều yếu tố khác mà dữ liệu quá khứ không thể hiện rõ ràng, làm giảm độ chính xác của dự đoán.

Sự phức tạp của các yếu tố tác động

Giá cổ phiếu không chỉ phụ thuộc vào dữ liệu tài chính mà còn ảnh hưởng bởi nhiều yếu tố khác như tâm lý nhà đầu tư, tình hình chính trị, khủng hoảng kinh tế, và các tin tức xã hội. Mặc dù các mô hình học máy như LSTM có thể nắm bắt mối quan hệ giữa các yếu tố thời gian, nhưng việc tích hợp toàn diện các yếu tố phi tài chính là rất khó khăn.

Do đó, để đạt được hiệu quả cao, người sử dụng cần thận trọng trong việc đánh giá mô hình, kết hợp với các chiến lược quản lý rủi ro và không phụ thuộc hoàn toàn vào các dự đoán tự động.

6. Source code

Toàn bộ source code được public miễn phí tại đây