import torch
import torch.nn.functional as F
from torch import nn
from einops import rearrange
from finol.data_layer.scaler_selector import ScalerSelector
from finol.utils import load_config
class SREM(nn.Module):
"""
This class implements the Sequence Representations Extraction Module (SREM).
For more details, please refer to the papers `AlphaPortfolio: Direct Construction through Reinforcement Learning
and Interpretable AI <https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3698800>`__ and `Attention is all you need
<https://proceedings.neurips.cc/paper_files/paper/2017/hash/3f5ee243547dee91fbd053c1c4a845aa-Abstract.html>`__
"""
def __init__(self, model_args, model_params):
super().__init__()
self.token_emb = nn.Linear(model_args["num_features_original"], model_params["DIM_EMBEDDING"])
self.pos_emb = nn.Embedding(model_args["window_size"], model_params["DIM_EMBEDDING"])
self.transformer_encoder = nn.TransformerEncoder(
nn.TransformerEncoderLayer(
d_model=model_params["DIM_EMBEDDING"],
nhead=model_params["NUM_HEADS"],
dim_feedforward=model_params["DIM_FEEDFORWARD"],
dropout=model_params["DROPOUT"],
batch_first=True,
),
num_layers=model_params["NUM_LAYERS"],
)
def forward(self, x):
"""
Args:
x (Tensor): the sequence to the encoder (required).
shape (batch_size * num_assets, window_size, num_features_original)
"""
_, n, d, device = x.shape[0], x.shape[1], x.shape[2], x.device # n: window size; d: number of features
x = self.token_emb(x) # [batch_size * num_assets, window_size, num_features_original] -> [batch_size * num_assets, window_size, DIM_EMBEDDING]
pos_emb = self.pos_emb(torch.arange(n, device=device))
pos_emb = rearrange(pos_emb, "n d -> () n d")
x = x + pos_emb
x = self.transformer_encoder(x) # [batch_size * num_assets, window_size, DIM_EMBEDDING] -> [batch_size * num_assets, window_size, DIM_EMBEDDING]
return torch.mean(x, dim=1) # [batch_size * num_assets, window_size, DIM_EMBEDDING] -> [batch_size * num_assets, DIM_EMBEDDING]
class CAAN(nn.Module):
"""
This class implements the Cross Asset Attention Network (CAAN) module
"""
def __init__(self, model_params):
super().__init__()
self.linear_query = torch.nn.Linear(model_params["DIM_EMBEDDING"], model_params["DIM_EMBEDDING"])
self.linear_key = torch.nn.Linear(model_params["DIM_EMBEDDING"], model_params["DIM_EMBEDDING"])
self.linear_value = torch.nn.Linear(model_params["DIM_EMBEDDING"], model_params["DIM_EMBEDDING"])
self.linear_winner = torch.nn.Linear(model_params["DIM_EMBEDDING"], 1)
def forward(self, x):
query = self.linear_query(x) # [batch_size, num_assets, DIM_EMBEDDING]
key = self.linear_key(x) # [batch_size, num_assets, DIM_EMBEDDING]
value = self.linear_value(x) # [batch_size, num_assets, DIM_EMBEDDING]
beta = torch.matmul(query, key.transpose(1, 2)) / torch.sqrt(torch.tensor(float(query.shape[-1]))) # [batch_size, num_assets, DIM_EMBEDDING]
beta = F.softmax(beta, dim=-1).unsqueeze(-1)
x = torch.sum(value.unsqueeze(1) * beta, dim=2) # [batch_size, num_assets, DIM_EMBEDDING]
final_scores = self.linear_winner(x).squeeze(-1) # [batch_size, num_assets]
return final_scores
[文件]class AlphaPortfolio(nn.Module):
"""
Class to generate predicted scores for the input assets based on the AlphaPortfolio model.
The AlphaPortfolio model is a Transformer-based model for asset scoring and portfolio selection. It consists of two
main components:
1. Sequence Representations Extraction Module (SREM): This module takes the input features for each asset over a
time window and generates a fixed-size embedding vector to represent the asset.
2. Cross Asset Attention Network (CAAN): This module takes the sequence representations generated by the SREM and
applies cross-asset attention to produce the final asset scores.
The AlphaPortfolio model takes an input tensor ``x`` of shape ``(batch_size, num_assets, num_features_augmented)``,
where ``num_features_augmented`` represents the number of features (including any preprocessed or augmented
features) for each asset. The final output of the AlphaPortfolio model is a tensor of shape ``(batch_size, num_assets)``,
where each element represents the predicted score for the corresponding asset.
For more details, please refer to the paper `AlphaPortfolio: Direct Construction through Reinforcement Learning
and Interpretable AI <https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3698800>`__.
.. table:: Table C.1: Hyperparameters of TE-CAAN-Based AP.
:class: ghost
+----------------------+--------+-----------------+--------+
| Hyper-parameter | Choice | Hyper-parameter | Choice |
+======================+========+=================+========+
| Embedding dimension | 256 | Optimizer | SGD |
+----------------------+--------+-----------------+--------+
| Feed-forward network | 1021 | Learning rate | 0.0001 |
+----------------------+--------+-----------------+--------+
| Number of multi-head | 4 | Dropout ratio | 0.2 |
+----------------------+--------+-----------------+--------+
| Number of TE layer | 1 | Training epochs | 30 |
+----------------------+--------+-----------------+--------+
:param model_args: Dictionary containing model arguments, such as the number of features.
:param model_params: Dictionary containing model hyperparameters, such as the number of layers, the hidden size, and the dropout rate.
Example:
.. code:: python
>>> from finol.data_layer.dataset_loader import DatasetLoader
>>> from finol.model_layer.model_instantiator import ModelInstantiator
>>> from finol.utils import load_config, update_config, portfolio_selection
>>>
>>> # Configuration
>>> config = load_config()
>>> config["MODEL_NAME"] = "AlphaPortfolio"
>>> config["MODEL_PARAMS"]["AlphaPortfolio"]["NUM_LAYERS"] = 1
>>> config["MODEL_PARAMS"]["AlphaPortfolio"]["DIM_EMBEDDING"] = 64
>>> config["MODEL_PARAMS"]["AlphaPortfolio"]["DIM_FEEDFORWARD"] = 64
>>> ...
>>> update_config(config)
>>>
>>> # Data Layer
>>> load_dataset_output = DatasetLoader().load_dataset()
>>>
>>> # Model Layer & Optimization Layer
>>> ...
>>> model = ModelInstantiator(load_dataset_output).instantiate_model()
>>> print(f"model: {model}")
>>> ...
>>> train_loader = load_dataset_output["train_loader"]
>>> for i, data in enumerate(train_loader, 1):
... x_data, label = data
... final_scores = model(x_data.float())
... portfolio = portfolio_selection(final_scores)
... print(f"batch {i} input shape: {x_data.shape}")
... print(f"batch {i} label shape: {label.shape}")
... print(f"batch {i} output shape: {portfolio.shape}")
... print("-"*50)
\\
"""
def __init__(self, model_args, model_params):
super().__init__()
self.config = load_config()
self.model_args = model_args
self.model_params = model_params
self.srem = SREM(model_args, model_params)
self.caan = CAAN(model_params)
[文件] def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Forward pass of the model.
:param x: Input tensor of shape ``(batch_size, num_assets, num_features_augmented)``.
:return: Output tensor of shape ``(batch_size, num_assets)`` containing the predicted scores for each asset.
"""
batch_size, num_assets, num_features_augmented = x.shape
"""Input Transformation"""
x = x.view(batch_size, num_assets, self.model_args["window_size"], self.model_args["num_features_original"])
x = rearrange(x, "b m n d -> (b m) n d")
if self.config["SCALER"].startswith("Window"):
x = ScalerSelector().window_normalize(x)
"""Sequence Representations Extraction (SREM)"""
stock_rep = self.srem(x) # [batch_size * num_assets, window_size, DIM_EMBEDDING] -> [batch_size * num_assets, DIM_EMBEDDING]
x = stock_rep.view(batch_size, num_assets, self.model_params["DIM_EMBEDDING"]) # [batch_size * num_assets, DIM_EMBEDDING] -> [batch_size, num_assets, DIM_EMBEDDING]
"""Cross Asset Attention Network (CAAN)"""
final_scores = self.caan(x)
return final_scores