# 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