DeepFin / models /deep_portfolio.py
Amós e Souza Fernandes
Upload 120 files
5f10e37 verified
# rnn/agents/deep_portfolio.py (ou onde você tem DeepPortfolioAI)
from typing import List
import tensorflow as tf
from tensorflow.keras.layers import Input, Conv1D, LSTM, Dense, Dropout, MultiHeadAttention, Reshape, Concatenate, TimeDistributed, GlobalAveragePooling1D
from tensorflow.keras.models import Model # Usar a API Funcional do Keras é mais flexível aqui
from transformers import AutoTokenizer, TFAutoModelForSequenceClassification
# Importar do config.py
# from ..config import WINDOW_SIZE, NUM_FEATURES_PER_ASSET, NUM_ASSETS
# (Você precisará adicionar NUM_FEATURES_PER_ASSET e NUM_ASSETS ao config.py)
# VALORES DE EXEMPLO (PEGUE DO CONFIG.PY)
WINDOW_SIZE_CONF = 60
NUM_ASSETS_CONF = 4 # Ex: BTC, ETH, ADA, SOL
NUM_FEATURES_PER_ASSET_CONF = 26 # O número de features que você calcula para UM ativo
# (ex: open, high, ..., sma_10_div_atr, adx_14, etc.)
# Se o input achatado é 104, e são 4 ativos, então 104/4 = 26.
class DeepPortfolioAgentNetwork(tf.keras.Model): # Renomeado para clareza, já que é a rede do agente
def __init__(self, num_assets, sequence_length, num_features_per_asset, lstm_units_asset=64, cnn_filters_asset=32):
super(DeepPortfolioAgentNetwork, self).__init__()
self.num_assets = num_assets
self.sequence_length = sequence_length
self.num_features_per_asset = num_features_per_asset
# --- Camadas para Processamento Individual de Ativos ---
# Usaremos TimeDistributed para aplicar as mesmas camadas CNN/LSTM a cada ativo
# A entrada esperada por TimeDistributed(Layer) é (batch_size, num_time_distributed_items, ...)
# No nosso caso, num_time_distributed_items será num_assets.
# O input para o modelo será (batch_size, sequence_length, num_assets * num_features_per_asset)
# Precisaremos de um Reshape para (batch_size, num_assets, sequence_length, num_features_per_asset)
# e depois talvez um Permute para aplicar TimeDistributed na dimensão de ativos.
# OU, uma abordagem mais simples é criar um "sub-modelo" para processar um ativo
# e depois aplicar esse sub-modelo a cada fatia de ativo.
# Abordagem com sub-modelo para processamento de ativo individual
asset_input_shape = (self.sequence_length, self.num_features_per_asset)
asset_processing_input = Input(shape=asset_input_shape, name="asset_input_features")
# CNN para cada ativo
cnn_layer1 = Conv1D(filters=cnn_filters_asset, kernel_size=3, activation='relu', padding='same', name="asset_cnn1")(asset_processing_input)
# cnn_layer1_dropout = Dropout(0.2)(cnn_layer1) # Opcional
cnn_layer2 = Conv1D(filters=cnn_filters_asset*2, kernel_size=3, activation='relu', padding='same', name="asset_cnn2")(cnn_layer1) # ou cnn_layer1_dropout
# cnn_layer2_pool = MaxPooling1D(pool_size=2)(cnn_layer2) # Opcional
# LSTM para cada ativo
# Se usou pooling na CNN, ajuste a entrada do LSTM
# lstm_output_asset = LSTM(lstm_units_asset, return_sequences=False, name="asset_lstm_final")(cnn_layer2_pool ou cnn_layer2)
lstm_output_asset = LSTM(lstm_units_asset, return_sequences=False, name="asset_lstm_final")(cnn_layer2) # Saída (None, lstm_units_asset)
# Criar o sub-modelo de processamento de ativo
self.asset_processor = Model(inputs=asset_processing_input, outputs=lstm_output_asset, name="single_asset_processor")
self.asset_processor.summary() # Ver a estrutura do processador de ativo
# --- Camadas para Combinação e Decisão ---
# MultiHeadAttention para correlações entre as representações dos ativos
# A entrada para MHA será (batch_size, num_assets, lstm_units_asset)
self.attention = MultiHeadAttention(num_heads=4, key_dim=lstm_units_asset, dropout=0.1, name="multi_asset_attention") # key_dim pode ser lstm_units_asset
# Camadas densas para decisão final
# O tamanho da entrada para self.dense1 dependerá da saída da atenção e do sentimento
# Saída da Atenção pode ser (batch_size, num_assets, lstm_units_asset). Podemos achatá-la ou usar GlobalAveragePooling.
self.global_avg_pool = GlobalAveragePooling1D(name="gap_after_attention") # Reduz a dimensão dos ativos
# Assumindo que o sentimento será um vetor por passo de tempo/batch
# self.sentiment_dense = Dense(32, activation='relu', name="sentiment_processor_dense") # Opcional, para processar embedding de sentimento
self.concat_layer = Concatenate(axis=-1, name="concat_attention_sentiment")
self.dense1 = Dense(128, activation='relu', name="final_dense1")
self.dropout1 = Dropout(0.3, name="final_dropout1")
self.dense2 = Dense(64, activation='relu', name="final_dense2")
self.dropout2 = Dropout(0.3, name="final_dropout2")
self.output_allocation = Dense(self.num_assets, activation='softmax', name="portfolio_allocation_output")
# Inicializar tokenizer e modelo de sentimento (se for usar)
self.use_sentiment = False # Defina como True se for usar
if self.use_sentiment:
try:
self.tokenizer = AutoTokenizer.from_pretrained('ProsusAI/finbert')
self.sentiment_model = TFAutoModelForSequenceClassification.from_pretrained('ProsusAI/finbert', from_pt=True)
print("Modelo FinBERT carregado para análise de sentimento.")
except Exception as e:
print(f"AVISO: Falha ao carregar FinBERT: {e}. Análise de sentimento será desabilitada.")
self.use_sentiment = False
def call(self, inputs, training=False): # Adicionado `training` para Dropout e outras camadas
# `inputs` pode ser um tensor único (market_data_flat) ou uma tupla/dicionário
# se você tiver news_data separado.
# Vamos assumir que a API de RL (Stable-Baselines3) passará um único tensor de observação.
# Se news_data for parte da observação, precisaremos separá-la.
# Por agora, vamos focar no market_data.
market_data_flat = inputs # Shape: (batch_size, sequence_length, num_assets * num_features_per_asset)
# 1. Reshape e Processamento Individual de Ativos
# Reshape para (batch_size, num_assets, sequence_length, num_features_per_asset)
# Isso pode ser complexo. Uma forma é fatiar e processar.
reshaped_market_data = tf.reshape(market_data_flat,
[-1, self.sequence_length, self.num_assets, self.num_features_per_asset])
# Transpor para (batch_size, num_assets, sequence_length, num_features_per_asset)
# (Assume que o input original já está como (batch, seq, assets*features) )
# Se o input é (batch_size, seq_len, total_features), e total_features = num_assets * num_features_asset
# precisamos de (batch_size, num_assets, seq_len, num_features_asset) para TimeDistributed(asset_processor)
# Isso requer cuidado no reshape e transpose.
# Alternativa: Processar cada ativo sequencialmente (mais fácil de implementar, pode ser mais lento)
asset_representations = []
for i in range(self.num_assets):
# Fatiar os dados para o ativo 'i'
# start_idx = i * self.num_features_per_asset
# end_idx = (i + 1) * self.num_features_per_asset
# current_asset_data = market_data_flat[:, :, start_idx:end_idx] # Shape (batch, seq_len, num_feat_per_asset)
# A forma correta de fatiar se market_data_flat é (batch, seq_len, num_assets * num_features_per_asset)
# e as features estão agrupadas por ativo (ex: ativo1_feat1, ativo1_feat2, ..., ativo2_feat1, ...)
slices = []
for asset_idx in range(self.num_assets):
asset_slice_features = market_data_flat[
:, :, asset_idx*self.num_features_per_asset : (asset_idx+1)*self.num_features_per_asset
]
slices.append(asset_slice_features)
# `slices` é uma lista de tensores, cada um (batch, seq_len, num_features_per_asset)
# Agora processar cada um
processed_asset_slices = [self.asset_processor(s, training=training) for s in slices]
# `processed_asset_slices` é uma lista de tensores, cada um (batch, lstm_units_asset)
# Empilhar as representações dos ativos para a camada de Atenção
# Resultado: (batch_size, num_assets, lstm_units_asset)
stacked_asset_representations = tf.stack(processed_asset_slices, axis=1)
# 2. Camada de Atenção entre Ativos
# Input: (batch_size, num_assets, lstm_units_asset)
# Output: (batch_size, num_assets, lstm_units_asset) - o output da MHA geralmente tem a mesma dimensão do value
attention_output = self.attention(
query=stacked_asset_representations,
value=stacked_asset_representations,
key=stacked_asset_representations,
training=training
)
# Reduzir a dimensão dos ativos (ex: com GlobalAveragePooling1D na dimensão num_assets)
# Input para GAP1D: (batch_size, steps, features) -> aqui (batch_size, num_assets, lstm_units_asset)
context_vector_from_attention = self.global_avg_pool(attention_output) # Shape (batch_size, lstm_units_asset)
# 3. Análise de Sentimento (Placeholder - requer `news_data` como input)
# Se você tiver `news_data` como parte do `inputs`
# sentiment_features = tf.zeros_like(context_vector_from_attention[:, :16]) # Placeholder
# if self.use_sentiment and news_data is not None:
# # ... (lógica para _process_news(news_data)) ...
# # sentiment_features = self.sentiment_dense(processed_news_embeddings)
# pass # Implementar
# 4. Combinar e Camadas Densas Finais
# Por agora, apenas usando a saída da atenção
combined_features = context_vector_from_attention
# if self.use_sentiment:
# combined_features = self.concat_layer([context_vector_from_attention, sentiment_features])
x = self.dense1(combined_features)
x = self.dropout1(x, training=training)
x = self.dense2(x)
x = self.dropout2(x, training=training)
portfolio_weights = self.output_allocation(x) # Softmax output
return portfolio_weights
# _process_news como você definiu (com return_tensors="tf" e ajuste para output TF)
def _process_news(self, news_data_batch: List[str]): # Espera um batch de strings
if not self.use_sentiment or not news_data_batch:
# Retornar um tensor de zeros com o shape esperado se não houver notícias ou o modelo de sentimento não for usado
# O shape precisa ser (batch_size, num_sentiment_output_features)
# Se batch_size é inferido, e num_sentiment_output_features é, digamos, 32 (após self.sentiment_dense)
# Isso é complicado de determinar aqui sem o batch_size.
# Uma solução é retornar um placeholder que será tratado no 'call'.
# Alternativamente, se a política RL sempre espera um input combinado,
# o ambiente precisa fornecer um placeholder para news_data.
return tf.zeros((tf.shape(news_data_batch)[0] if isinstance(news_data_batch, tf.Tensor) else len(news_data_batch), 32)) # Exemplo
inputs = self.tokenizer(news_data_batch, return_tensors="tf", padding=True, truncation=True, max_length=128) # max_length menor para eficiência
outputs = self.sentiment_model(inputs, training=False) # `training=False` para inferência do FinBERT
sentiment_logits = outputs.logits # Shape (batch_size, 3) para FinBERT (pos, neg, neu)
# Você pode querer processar esses logits mais (ex: passar por uma Dense)
# ou usar diretamente. Se usar diretamente, a concatenação precisa ser compatível.
# Exemplo: usar uma camada densa para reduzir/expandir para um tamanho fixo de embedding de sentimento.
# processed_sentiment = self.sentiment_dense(sentiment_logits)
# return processed_sentiment
return sentiment_logits # Retornando logits (batch_size, 3) por enquanto