diff --git a/AutomatedLendingPool.sol b/AutomatedLendingPool.sol new file mode 100644 index 0000000000000000000000000000000000000000..148748c0e9a61f8fd024345d099e9061172d2b85 --- /dev/null +++ b/AutomatedLendingPool.sol @@ -0,0 +1,35 @@ +pragma solidity ^0.8.0; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./CreditOracle.sol"; + +contract AutomatedLendingPool { + IERC20 public immutable token; + CreditOracle public immutable creditOracle; + + struct Loan { + uint256 amount; + uint256 interest; + uint256 dueDate; + } + + mapping(address => Loan) public loans; + + constructor(address _token, address _creditOracle) { + token = IERC20(_token); + creditOracle = CreditOracle(_creditOracle); + } + + function requestLoan(uint256 amount) external { + require(loans[msg.sender].amount == 0, "Existing loan must be repaid"); + bytes32 requestId = creditOracle.requestCreditScore(msg.sender); + // Lógica para processar o empréstimo com base no score de crédito + } + + function repayLoan() external { + Loan storage loan = loans[msg.sender]; + require(loan.amount > 0, "No active loan"); + uint256 totalDue = loan.amount + loan.interest; + require(token.transferFrom(msg.sender, address(this), totalDue), "Transfer failed"); + delete loans[msg.sender]; + } +} \ No newline at end of file diff --git a/CreditOracle.sol b/CreditOracle.sol new file mode 100644 index 0000000000000000000000000000000000000000..35d7c373b49389425391a0536745fa6612484a1e --- /dev/null +++ b/CreditOracle.sol @@ -0,0 +1,27 @@ +pragma solidity ^0.8.0; +import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol"; + +contract CreditOracle is ChainlinkClient { + using Chainlink for Chainlink.Request; + + address private oracle; + bytes32 private jobId; + uint256 private fee; + + constructor() { + setPublicChainlinkToken(); + oracle = 0x...; // Endereço do nó Chainlink + jobId = "..."; // ID do job Chainlink + fee = 0.1 * 10 ** 18; // 0.1 LINK + } + + function requestCreditScore(address user) public returns (bytes32 requestId) { + Chainlink.Request memory request = buildChainlinkRequest(jobId, address(this), this.fulfill.selector); + request.add("userId", uint256(uint160(user))); + return sendChainlinkRequestTo(oracle, request, fee); + } + + function fulfill(bytes32 _requestId, uint256 _creditScore) public recordChainlinkFulfillment(_requestId) { + // Lógica para atualizar o score de crédito do usuário + } +} \ No newline at end of file diff --git a/FuzzedDataProvider.js b/FuzzedDataProvider.js new file mode 100644 index 0000000000000000000000000000000000000000..eb599d11bedaed6ab732ec0e3a818a7c80e49387 --- /dev/null +++ b/FuzzedDataProvider.js @@ -0,0 +1,15 @@ +const { FuzzedDataProvider } = require('fuzzing-tools'); + +function fuzzTest(data) { + const fuzz = new FuzzedDataProvider(data); + const amount = fuzz.consumeNumber(); + const recipient = fuzz.consumeAddress(); + + try { + token.transfer(recipient, amount); + } catch (error) { + // Registrar e analisar erros + } +} + +module.exports = { fuzzTest }; \ No newline at end of file diff --git a/ImeAtcoinGovernance.sol b/ImeAtcoinGovernance.sol new file mode 100644 index 0000000000000000000000000000000000000000..49a896e7e0fc1c5d97ae194b5d25251101fdb466 --- /dev/null +++ b/ImeAtcoinGovernance.sol @@ -0,0 +1,59 @@ +pragma solidity ^0.8.0; +import "@openzeppelin/contracts/governance/Governor.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol"; + +contract ImeAtcoinGovernance is Governor, GovernorSettings, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl { + constructor(IVotes _token, TimelockController _timelock) + Governor("ImeAtcoinGovernance") + GovernorSettings(1 /* 1 block */, 45818 /* 1 week */, 0) + GovernorVotes(_token) + GovernorVotesQuorumFraction(4) + GovernorTimelockControl(_timelock) + {} + + function votingDelay() public view override(IGovernor, GovernorSettings) returns (uint256) { + return super.votingDelay(); + } + + function votingPeriod() public view override(IGovernor, GovernorSettings) returns (uint256) { + return super.votingPeriod(); + } + + function quorum(uint256 blockNumber) public view override(IGovernor, GovernorVotesQuorumFraction) returns (uint256) { + return super.quorum(blockNumber); + } + + function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) { + return super.state(proposalId); + } + + function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) + public override(Governor, IGovernor) returns (uint256) + { + return super.propose(targets, values, calldatas, description); + } + + function _execute(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) + internal override(Governor, GovernorTimelockControl) + { + super._execute(proposalId, targets, values, calldatas, descriptionHash); + } + + function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) + internal override(Governor, GovernorTimelockControl) returns (uint256) + { + return super._cancel(targets, values, calldatas, descriptionHash); + } + + function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) { + return super._executor(); + } + + function supportsInterface(bytes4 interfaceId) public view override(Governor, GovernorTimelockControl) returns (bool) { + return super.supportsInterface(interfaceId); + } +} \ No newline at end of file diff --git a/ImeAtcoinPayment.js b/ImeAtcoinPayment.js new file mode 100644 index 0000000000000000000000000000000000000000..5d8451a816799debeedd64f7d37dc394183cc0aa --- /dev/null +++ b/ImeAtcoinPayment.js @@ -0,0 +1,15 @@ +class ImeAtcoinPayment { + constructor(apiKey) { + this.apiKey = apiKey; + } + + async createPayment(amount, currency) { + // Lógica para criar um pagamento + } + + async verifyPayment(paymentId) { + // Lógica para verificar um pagamento + } +} + +module.exports = ImeAtcoinPayment; \ No newline at end of file diff --git a/MarketMaker.py b/MarketMaker.py new file mode 100644 index 0000000000000000000000000000000000000000..148e73d80f02c31ee328be392b8f4291e64f573d --- /dev/null +++ b/MarketMaker.py @@ -0,0 +1,30 @@ +import ccxt + +class MarketMaker: + def __init__(self, exchange, symbol, spread=0.01): + self.exchange = ccxt.exchange({'apiKey': '...', 'secret': '...'}) + self.symbol = symbol + self.spread = spread + + def place_orders(self): + ticker = self.exchange.fetch_ticker(self.symbol) + mid_price = (ticker['bid'] + ticker['ask']) / 2 + + bid_price = mid_price * (1 - self.spread / 2) + ask_price = mid_price * (1 + self.spread / 2) + + self.exchange.create_limit_buy_order(self.symbol, 1, bid_price) + self.exchange.create_limit_sell_order(self.symbol, 1, ask_price) + + def run(self): + while True: + try: + self.place_orders() + time.sleep(60) # Atualiza ordens a cada minuto + except Exception as e: + print(f"Error: {e}") + time.sleep(60) + +if __name__ == "__main__": + market_maker = MarketMaker("binance", "I*****ME/USDT") + market_maker.run() \ No newline at end of file diff --git a/PrivateTransactions.sol b/PrivateTransactions.sol new file mode 100644 index 0000000000000000000000000000000000000000..ab80d3c1e4f85ad1c2b2ba5a9a9542d50000683e --- /dev/null +++ b/PrivateTransactions.sol @@ -0,0 +1,23 @@ +pragma solidity ^0.8.0; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./ZkSnarkVerifier.sol"; + +contract PrivateTransactions { + IERC20 public immutable token; + ZkSnarkVerifier public immutable verifier; + + constructor(address _token, address _verifier) { + token = IERC20(_token); + verifier = ZkSnarkVerifier(_verifier); + } + + function privateTransfer( + uint256[2] memory a, + uint256[2][2] memory b, + uint256[2] memory c, + uint256[3] memory input + ) external { + require(verifier.verifyProof(a, b, c, input), "Invalid zk-SNARK proof"); + // Executar a transferência privada + } +} \ No newline at end of file diff --git a/README.md b/README.md index 7be5fc7f47d5db027d120b8024982df93db95b74..8e5f6824513a6cf6d1d95222c7d3ac19969933c3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ --- -license: mit +title: Aibank Token +emoji: 🐠 +colorFrom: yellow +colorTo: pink +sdk: static +pinned: false +license: other +short_description: Token aibank --- + +Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference diff --git a/RewardSystem.sol b/RewardSystem.sol new file mode 100644 index 0000000000000000000000000000000000000000..669bbcf9ffd5d7a1483c7d45f759f826cbe389f9 --- /dev/null +++ b/RewardSystem.sol @@ -0,0 +1,22 @@ +pragma solidity ^0.8.0; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract RewardSystem { + IERC20 public immutable token; + mapping(address => uint256) public rewards; + + constructor(address _token) { + token = IERC20(_token); + } + + function addReward(address user, uint256 amount) external { + rewards[user] += amount; + } + + function claimReward() external { + uint256 amount = rewards[msg.sender]; + require(amount > 0, "No rewards to claim"); + rewards[msg.sender] = 0; + require(token.transfer(msg.sender, amount), "Transfer failed"); + } +} \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8c8e9cc85ff39e11638fc1640787a383f191a0c8 Binary files /dev/null and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/__pycache__/app.cpython-312.pyc b/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cb242cd1e53d078155c7ff237d34e07dab08d4ba Binary files /dev/null and b/__pycache__/app.cpython-312.pyc differ diff --git a/agents/DeepPortfolioAgent.py b/agents/DeepPortfolioAgent.py new file mode 100644 index 0000000000000000000000000000000000000000..5c555163ef1b701f467edb6748eec9d0cff38521 --- /dev/null +++ b/agents/DeepPortfolioAgent.py @@ -0,0 +1,361 @@ + +# rnn/agents/deep_portfolio.py (ou onde você tem DeepPortfolioAI / DeepPortfolioAgentNetwork) +import numpy as np +from tensorflow.keras import regularizers +import tensorflow as tf +from tensorflow.keras.layers import ( + Input, Conv1D, LSTM, Dense, Dropout, + MultiHeadAttention, Reshape, Concatenate, + TimeDistributed, GlobalAveragePooling1D, LayerNormalization +) +from tensorflow.keras.models import Model +from transformers import AutoTokenizer, TFAutoModelForSequenceClassification +# Comente as importações do transformers se não for testar o sentimento agora para simplificar +# from transformers import AutoTokenizer, TFAutoModelForSequenceClassification + +# --- DEFINIÇÕES DE CONFIGURAÇÃO (COPIE OU IMPORTE DO SEU CONFIG.PY) --- +# Se você não importar do config.py, defina-as aqui para o teste +WINDOW_SIZE_CONF = 60 +NUM_ASSETS_CONF = 4 # Ex: ETH, BTC, ADA, SOL +NUM_FEATURES_PER_ASSET_CONF = 26 # Número de features calculadas para CADA ativo + # (open_div_atr, ..., buy_condition_v1, etc.) +L2_REG = 0.0001 # Exemplo, use o valor do seu config + +# (Cole as classes AssetProcessor e DeepPortfolioAgentNetwork aqui se estiver em um novo script) +# Ou, se estiver no mesmo arquivo, elas já estarão definidas. + +# ... (Definição das classes AssetProcessor e DeepPortfolioAgentNetwork como na resposta anterior) ... +# Certifique-se que a classe DeepPortfolioAgentNetwork está usando estas constantes: +# num_assets=NUM_ASSETS_CONF, +# sequence_length=WINDOW_SIZE_CONF, +# num_features_per_asset=NUM_FEATURES_PER_ASSET_CONF + +class AssetProcessor(tf.keras.Model): + def __init__(self, sequence_length, num_features, cnn_filters1=32, cnn_filters2=64, lstm_units1=64, lstm_units2=32, dropout_rate=0.2, name="single_asset_processor_module", **kwargs): # Adicionado **kwargs + super(AssetProcessor, self).__init__(name=name, **kwargs) # Adicionado **kwargs + self.sequence_length = sequence_length + self.num_features = num_features + self.cnn_filters1 = cnn_filters1 # Salvar para get_config + self.cnn_filters2 = cnn_filters2 + self.lstm_units1 = lstm_units1 + self.lstm_units2 = lstm_units2 + self.dropout_rate = dropout_rate + + self.conv1 = Conv1D(filters=cnn_filters1, kernel_size=3, activation='relu', padding='same', name="asset_cnn1") + self.dropout_cnn1 = Dropout(dropout_rate, name="asset_cnn1_dropout") + self.conv2 = Conv1D(filters=cnn_filters2, kernel_size=3, activation='relu', padding='same', name="asset_cnn2") + self.dropout_cnn2 = Dropout(dropout_rate, name="asset_cnn2_dropout") + self.lstm1 = LSTM(lstm_units1, return_sequences=True, name="asset_lstm1") + self.dropout_lstm1 = Dropout(dropout_rate, name="asset_lstm1_dropout") + self.lstm2 = LSTM(lstm_units2, return_sequences=False, name="asset_lstm2_final") + self.dropout_lstm2 = Dropout(dropout_rate, name="asset_lstm2_dropout") + + def call(self, inputs, training=False): + x = self.conv1(inputs) + x = self.dropout_cnn1(x, training=training) + x = self.conv2(x) + x = self.dropout_cnn2(x, training=training) + x = self.lstm1(x, training=training) + x = self.dropout_lstm1(x, training=training) + x = self.lstm2(x, training=training) + x_processed_asset = self.dropout_lstm2(x, training=training) + return x_processed_asset + + def get_config(self): + config = super().get_config() + config.update({ + "sequence_length": self.sequence_length, + "num_features": self.num_features, + "cnn_filters1": self.cnn_filters1, + "cnn_filters2": self.cnn_filters2, + "lstm_units1": self.lstm_units1, + "lstm_units2": self.lstm_units2, + "dropout_rate": self.dropout_rate, + }) + return config + +class DeepPortfolioAgentNetwork(tf.keras.Model): + def __init__(self, + num_assets=int(NUM_ASSETS_CONF), + sequence_length=int(WINDOW_SIZE_CONF), + num_features_per_asset=int(NUM_FEATURES_PER_ASSET_CONF), + asset_cnn_filters1=32, asset_cnn_filters2=64, + asset_lstm_units1=64, asset_lstm_units2=32, asset_dropout=0.2, + mha_num_heads=4, mha_key_dim_divisor=2, # key_dim será asset_lstm_units2 // mha_key_dim_divisor + final_dense_units1=128, final_dense_units2=64, final_dropout=0.3, + use_sentiment_analysis=True, + output_latent_features=False, **kwargs): # Adicionado **kwargs + super(DeepPortfolioAgentNetwork, self).__init__(name="deep_portfolio_agent_network", **kwargs) # Adicionado **kwargs + + + print(f"DPN __init__ > num_assets ENTRADA: {num_assets}, tipo: {type(num_assets)}") + + # Tentar extrair o valor escalar se for um tensor/variável ou TrackedDict + def get_int_value(param_name, val): + if isinstance(val, (tf.Tensor, tf.Variable)): + if val.shape == tf.TensorShape([]): # Escalar + print(f"DPN __init__: Convertendo {param_name} (Tensor/Variable escalar) para int.") + return int(val.numpy()) + else: + raise ValueError(f"{param_name} é um Tensor/Variable mas não é escalar. Shape: {val.shape}") + elif isinstance(val, dict): # Pode ser um TrackedDict + # TrackedDict pode se comportar como um dict. Se o valor real está "escondido", + # precisamos descobrir como acessá-lo. + # Por agora, vamos tentar a conversão direta, e se falhar, o erro será mais claro. + # Se for um dict simples com uma chave específica, você precisaria dessa chave. + # O erro 'KeyError: value' sugere que ['value'] não é a forma correta. + # Geralmente, para hiperparâmetros, o TrackedDict deve conter o valor diretamente + # se o SB3 o passou corretamente. + print(f"DPN __init__: Tentando converter {param_name} (dict-like) para int.") + try: + return int(val) # Tentar conversão direta + except TypeError: + # Se TrackedDict se comporta como um tensor quando usado em ops TF, + # tf.get_static_value pode funcionar, ou apenas o uso direto + # em operações TF (mas range() não é uma op TF). + # Se for um tensor TF "disfarçado", .numpy() pode funcionar. + # Se for um dict com uma chave específica, essa chave seria necessária. + # O erro mostra que ['value'] não funcionou. + print(f"DPN __init__: Conversão direta de {param_name} (dict-like) para int falhou. Investigar TrackedDict.") + # Para depuração, você pode tentar imprimir os itens do dict: + # if isinstance(val, collections.abc.Mapping): # Checa se é um dict-like + # for k, v_item in val.items(): + # print(f" {param_name} item: {k} -> {v_item}") + raise TypeError(f"{param_name} é {type(val)} e não pôde ser convertido para int diretamente. Valor: {val}") + else: # Tenta conversão direta para outros tipos + return int(val) + + try: + self.num_assets = get_int_value("num_assets", num_assets) + self.sequence_length = get_int_value("sequence_length", sequence_length) + self.num_features_per_asset = get_int_value("num_features_per_asset", num_features_per_asset) + self.asset_lstm_output_dim = get_int_value("asset_lstm_units2", asset_lstm_units2) # Do kwargs + + # Faça o mesmo para TODOS os outros parâmetros que devem ser inteiros e são passados + # para construtores de camadas Keras (cnn_filters, lstm_units, mha_num_heads, etc.) + # Exemplo: + # self.asset_cnn_filters1_val = get_int_value("asset_cnn_filters1", kwargs.get("asset_cnn_filters1")) + + except Exception as e_conv: + print(f"ERRO CRÍTICO DE CONVERSÃO DE TIPO no __init__ da DeepPortfolioAgentNetwork: {e_conv}") + raise + + print(f"DPN __init__ > self.num_assets APÓS conversão: {self.num_assets}, tipo: {type(self.num_assets)}") + + + + + + + self.num_assets = num_assets + self.sequence_length = sequence_length + self.num_features_per_asset = num_features_per_asset + self.asset_lstm_output_dim = asset_lstm_units2 + + self.asset_processor = AssetProcessor( + sequence_length=self.sequence_length, num_features=self.num_features_per_asset, + cnn_filters1=asset_cnn_filters1, cnn_filters2=asset_cnn_filters2, + lstm_units1=asset_lstm_units1, lstm_units2=asset_lstm_units2, + dropout_rate=asset_dropout + ) + + # Ajustar key_dim para ser compatível com a dimensão de entrada e num_heads + # key_dim * num_heads deve ser idealmente igual a asset_lstm_output_dim se for auto-atenção direta, + # ou o MHA projeta internamente. Para simplificar, vamos fazer key_dim ser divisível. + # Se asset_lstm_output_dim não for divisível por num_heads, key_dim pode ser diferente. + # Vamos definir key_dim explicitamente. Se asset_lstm_output_dim = 32 e num_heads = 4, key_dim pode ser 8. + # Ou deixar o MHA lidar com a projeção se key_dim for diferente. + # Para maior clareza, calculamos uma key_dim sensata. + calculated_key_dim = self.asset_lstm_output_dim // mha_key_dim_divisor + if calculated_key_dim == 0: # Evitar key_dim zero + calculated_key_dim = self.asset_lstm_output_dim # Fallback se for muito pequeno + print(f"AVISO: asset_lstm_output_dim ({self.asset_lstm_output_dim}) muito pequeno para mha_key_dim_divisor ({mha_key_dim_divisor}). Usando key_dim = {calculated_key_dim}") + + self.attention = MultiHeadAttention(num_heads=mha_num_heads, key_dim=calculated_key_dim, dropout=0.1, name="multi_asset_attention") + self.attention_norm = LayerNormalization(epsilon=1e-6, name="attention_layernorm") + self.global_avg_pool_attention = GlobalAveragePooling1D(name="gap_after_attention") + + self.use_sentiment = use_sentiment_analysis # Desabilitado por padrão para este teste + self.sentiment_embedding_size = 3 + 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 + + + dense_input_dim = self.use_sentiment + #if self.use_sentiment: dense_input_dim += self.sentiment_embedding_size + + self.dense1 = Dense(final_dense_units1, activation='relu', kernel_regularizer=regularizers.l2(L2_REG), name="final_dense1") + self.dropout1 = Dropout(final_dropout, name="final_dropout1") + self.dense2 = Dense(final_dense_units2, activation='relu', kernel_regularizer=regularizers.l2(L2_REG), name="final_dense2") + self.dropout2 = Dropout(final_dropout, name="final_dropout2") + self.output_allocation = Dense(self.num_assets, activation='softmax', name="portfolio_allocation_output") + + def call(self, inputs, training=False): + market_data_flat = inputs + + print(type(self.num_assets)) + asset_representations_list = [] + for i in range(self.num_assets): + 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] + processed_asset_representation = self.asset_processor(current_asset_data, training=training) + asset_representations_list.append(processed_asset_representation) + + stacked_asset_features = tf.stack(asset_representations_list, axis=1) + + # Para MHA, query, value, key são (batch_size, Tq, dim), (batch_size, Tv, dim) + # Aqui, T = num_assets, dim = asset_lstm_output_dim + attention_output = self.attention( + query=stacked_asset_features, value=stacked_asset_features, key=stacked_asset_features, + training=training + ) + attention_output = self.attention_norm(stacked_asset_features + attention_output) + + context_vector_from_attention = self.global_avg_pool_attention(attention_output) + + current_features_for_dense = context_vector_from_attention + # if self.use_sentiment: ... (lógica de concatenação) + + x = self.dense1(current_features_for_dense) + x = self.dropout1(x, training=training) + x = self.dense2(x) + x = self.dropout2(x, training=training) + + portfolio_weights = self.output_allocation(x) + return portfolio_weights + + def get_config(self): # Necessário se você quiser salvar/carregar o modelo que usa este sub-modelo + config = super().get_config() + config.update({ + "num_assets": self.num_assets, + "sequence_length": self.sequence_length, + "num_features_per_asset": self.num_features_per_asset, + # Adicione outros args do __init__ aqui para todas as camadas e sub-modelos + "asset_lstm_output_dim": self.asset_lstm_output_dim, + # ... e os parâmetros passados para AssetProcessor e MHA, etc. + }) + return config + + # @classmethod + # def from_config(cls, config): # Necessário para carregar com sub-modelo customizado + # # Extrair config do AssetProcessor se necessário + # return cls(**config) + + +if __name__ == '__main__': + print("Testando o Forward Pass do DeepPortfolioAgentNetwork...") + + # 1. Definir Parâmetros para o Teste (devem corresponder ao config.py) + batch_size_test = 2 # Um batch pequeno para teste + seq_len_test = WINDOW_SIZE_CONF + num_assets_test = NUM_ASSETS_CONF + num_features_per_asset_test = NUM_FEATURES_PER_ASSET_CONF + total_features_flat = num_assets_test * num_features_per_asset_test + + print(f"Configuração do Teste:") + print(f" Batch Size: {batch_size_test}") + print(f" Sequence Length (Window): {seq_len_test}") + print(f" Number of Assets: {num_assets_test}") + print(f" Features per Asset: {num_features_per_asset_test}") + print(f" Total Flat Features per Timestep: {total_features_flat}") + + # 2. Criar Tensor de Input Mockado + # Shape: (batch_size, sequence_length, num_assets * num_features_per_asset) + mock_market_data_flat = tf.random.normal( + shape=(batch_size_test, seq_len_test, total_features_flat) + ) + print(f"Shape do Input Mockado (market_data_flat): {mock_market_data_flat.shape}") + + # 3. Instanciar o Modelo + # Use os mesmos hiperparâmetros que você definiria no config.py para a rede + print("\nInstanciando DeepPortfolioAgentNetwork...") + agent_network = DeepPortfolioAgentNetwork( + num_assets=num_assets_test, + sequence_length=seq_len_test, + num_features_per_asset=num_features_per_asset_test, + # Você pode variar os próximos parâmetros para testar diferentes configs + asset_cnn_filters1=32, asset_cnn_filters2=64, + asset_lstm_units1=64, asset_lstm_units2=32, # asset_lstm_units2 define asset_lstm_output_dim + asset_dropout=0.1, + mha_num_heads=4, mha_key_dim_divisor=4, # Ex: 32 // 4 = 8 para key_dim + final_dense_units1=64, final_dense_units2=32, final_dropout=0.2, + use_sentiment_analysis=False # Testar sem sentimento primeiro + ) + + # Para construir o modelo e ver o summary, você pode chamar com o input mockado + # ou explicitamente chamar model.build() se souber o input shape completo + # Chamar com input mockado é mais fácil para construir. + print("\nConstruindo o modelo com input mockado (primeira chamada)...") + try: + # É uma boa prática fazer a primeira chamada dentro de um tf.function para otimizar + # ou apenas chamar diretamente para teste. + _ = agent_network(mock_market_data_flat) # Chamada para construir as camadas + print("\n--- Summary da Rede Principal (DeepPortfolioAgentNetwork) ---") + agent_network.summary() + + # O summary do asset_processor já foi impresso no __init__ do DeepPortfolioAgentNetwork + # se você descomentar as linhas de build/summary lá. + # Ou você pode imprimir aqui: + print("\n--- Summary do AssetProcessor (Sub-Modelo) ---") + agent_network.asset_processor.summary() + + + except Exception as e: + print(f"Erro ao construir a rede principal: {e}", exc_info=True) + exit() + + # 4. Chamar model(mock_input) para o Forward Pass + print("\nExecutando Forward Pass...") + try: + predictions = agent_network(mock_market_data_flat, training=False) # Passar training=False para inferência + print("Forward Pass concluído com sucesso!") + except Exception as e: + print(f"Erro durante o Forward Pass: {e}", exc_info=True) + exit() + + # 5. Verificar o Shape da Saída + print(f"\nShape da Saída (predictions): {predictions.shape}") + expected_output_shape = (batch_size_test, num_assets_test) + if predictions.shape == expected_output_shape: + print(f"Shape da Saída está CORRETO! Esperado: {expected_output_shape}") + else: + print(f"ERRO: Shape da Saída INCORRETO. Esperado: {expected_output_shape}, Obtido: {predictions.shape}") + + # Verificar se a saída é uma distribuição de probabilidade (softmax) + if hasattr(predictions, 'numpy'): # Se for um EagerTensor + output_sum = tf.reduce_sum(predictions, axis=-1).numpy() + print(f"Soma das probabilidades de saída por amostra no batch (deve ser próximo de 1): {output_sum}") + if np.allclose(output_sum, 1.0): + print("Saída Softmax parece CORRETA (soma 1).") + else: + print("AVISO: Saída Softmax pode NÃO estar correta (soma diferente de 1).") + + print("\nExemplo das primeiras predições (pesos do portfólio):") + print(predictions.numpy()[:min(5, batch_size_test)]) # Imprime até 5 predições do batch + + # Teste com sentimento (se implementado e FinBERT carregado) + # agent_network.use_sentiment = True # Ativar para teste + # if agent_network.use_sentiment and hasattr(agent_network, 'tokenizer'): + # print("\nTestando Forward Pass COM SENTIMENTO...") + # mock_news_batch = ["positive news for asset 1", "market is very volatile today"] # Exemplo + # # A forma como você passa 'news' para o call() precisa ser definida. + # # Se for um dicionário: + # # mock_inputs_with_news = {"market_data": mock_market_data_flat, "news_data": mock_news_batch} + # # predictions_with_sentiment = agent_network(mock_inputs_with_news, training=False) + # # print(f"Shape da Saída com Sentimento: {predictions_with_sentiment.shape}") + # else: + # print("\nTeste com sentimento pulado (use_sentiment=False ou FinBERT não carregado).") + + print("\nTeste do Forward Pass Concluído!") + + + + + diff --git a/agents/__init__.py b/agents/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/agents/__pycache__/DeepPortfolioAgent.cpython-312.pyc b/agents/__pycache__/DeepPortfolioAgent.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04228403751e3ca5716e634250c8c3af720bad64 Binary files /dev/null and b/agents/__pycache__/DeepPortfolioAgent.cpython-312.pyc differ diff --git a/agents/__pycache__/DeepPortfolioAgentNetwork.cpython-312.pyc b/agents/__pycache__/DeepPortfolioAgentNetwork.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1463d0291472dc8cc11955113b1c0300606f59d Binary files /dev/null and b/agents/__pycache__/DeepPortfolioAgentNetwork.cpython-312.pyc differ diff --git a/agents/__pycache__/__init__.cpython-312.pyc b/agents/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..992e8518a035d65ef3b9c65b5b9cc0a909631d0f Binary files /dev/null and b/agents/__pycache__/__init__.cpython-312.pyc differ diff --git a/agents/__pycache__/config.cpython-312.pyc b/agents/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94a8c07b24eb97520dd3d191e0d17ac97919449a Binary files /dev/null and b/agents/__pycache__/config.cpython-312.pyc differ diff --git a/agents/__pycache__/custom_policies.cpython-312.pyc b/agents/__pycache__/custom_policies.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b54e0bc1dd96ec51762fce535310cb14fc0eb68 Binary files /dev/null and b/agents/__pycache__/custom_policies.cpython-312.pyc differ diff --git a/agents/__pycache__/data_handler_multi_asset.cpython-312.pyc b/agents/__pycache__/data_handler_multi_asset.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2fa5a2118a5e92f361a55a041f79dbad505d0fdc Binary files /dev/null and b/agents/__pycache__/data_handler_multi_asset.cpython-312.pyc differ diff --git a/agents/__pycache__/dataset_update_agent.cpython-312.pyc b/agents/__pycache__/dataset_update_agent.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a85989b6ef87e95cfd5694f22b88703471a9239b Binary files /dev/null and b/agents/__pycache__/dataset_update_agent.cpython-312.pyc differ diff --git a/agents/__pycache__/deep_portfolio.cpython-312.pyc b/agents/__pycache__/deep_portfolio.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e060dad971887fac02bcec028fa9a3ff375bb6be Binary files /dev/null and b/agents/__pycache__/deep_portfolio.cpython-312.pyc differ diff --git a/agents/__pycache__/deep_portfolio_torch.cpython-312.pyc b/agents/__pycache__/deep_portfolio_torch.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99880bb4e5a43cb7e6839b5d424036a01bf656e3 Binary files /dev/null and b/agents/__pycache__/deep_portfolio_torch.cpython-312.pyc differ diff --git a/agents/__pycache__/portfolio_environment.cpython-312.pyc b/agents/__pycache__/portfolio_environment.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce4e175968366af99448179f516cda248c5b757e Binary files /dev/null and b/agents/__pycache__/portfolio_environment.cpython-312.pyc differ diff --git a/agents/__pycache__/portfolio_features_extractor_torch.cpython-312.pyc b/agents/__pycache__/portfolio_features_extractor_torch.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..875d6df990c4782026a0db3127403c4c19c4e2a3 Binary files /dev/null and b/agents/__pycache__/portfolio_features_extractor_torch.cpython-312.pyc differ diff --git a/agents/__pycache__/train_rl_portfolio_agent.cpython-312.pyc b/agents/__pycache__/train_rl_portfolio_agent.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..46c3a5f5bbed1abe61c7cf432f222e15640bf656 Binary files /dev/null and b/agents/__pycache__/train_rl_portfolio_agent.cpython-312.pyc differ diff --git a/agents/config.md b/agents/config.md new file mode 100644 index 0000000000000000000000000000000000000000..0d91747d9bb970a491e0385602c43c01a49be7c7 --- /dev/null +++ b/agents/config.md @@ -0,0 +1,13 @@ +# EXPECTED_FEATURES_ORDER define como as colunas DEVEM ESTAR no DataFrame +# que entra na função create_sequences, APÓS o escalonamento e ANTES do sufixo _scaled. +# E também como o rnn_predictor.py espera as features ANTES de aplicar os scalers. +# Se rnn_predictor.py vai aplicar scalers separados, ele precisa dos nomes originais (ou _div_atr). +# O script de treino vai criar colunas com sufixo _scaled para alimentar o modelo. +# A lista EXPECTED_FEATURES_ORDER no config.py não é diretamente usada no train_rnn_model.py +# da forma como está agora, mas é CRUCIAL para alinhar com rnn_predictor.py. +# Fazer o train_rnn_model.py funcionar e depois alinhar o rnn_predictor.py. +# Importante que as NUM_FEATURES e o input_shape do modelo estejam corretos. + +# Esta variável será usada para nomear as colunas escaladas que vão para o modelo +# e deve corresponder ao que o rnn_predictor.py espera encontrar como features escaladas. +# Os nomes aqui devem ser as colunas de BASE_FEATURE_COLS com "_scaled" no final. \ No newline at end of file diff --git a/agents/config.py b/agents/config.py new file mode 100644 index 0000000000000000000000000000000000000000..f866a07183760bd2d207c8c1aa6fbc737c2c0e8f --- /dev/null +++ b/agents/config.py @@ -0,0 +1,126 @@ +# config.py + +# --- Parâmetros de Dados --- +SYMBOL = 'ETH/USDT' # Ativo principal para o modelo de classificação (se ainda usar) +MULTI_ASSET_SYMBOLS = { # Para o agente de portfólio RL + 'eth': 'ETH-USD', # Chave amigável: ticker_yfinance + 'btc': 'BTC-USD', + 'ada': 'ADA-USD', + 'sol': 'SOL-USD' +} +NUM_ASSETS_PORTFOLIO = len(MULTI_ASSET_SYMBOLS) # Número de ativos no portfólio +NUM_ASSETS=4 +TIMEFRAME = '1h' # Usado tanto para yfinance quanto para ccxt (se adaptar) +DAYS_OF_DATA_TO_FETCH = 365 * 2 +LIMIT_PER_FETCH = 1000 # Para ccxt + +# --- Parâmetros de Features e Janela --- +WINDOW_SIZE = 60 + +# BASE_FEATURE_COLS: Colunas calculadas para CADA ativo ANTES do escalonamento final para o modelo. +# Estas são as features que seu data_handler_multi_asset.py DEVE produzir para cada ativo. +# O rnn_predictor.py (ou a parte de features da DeepPortfolioAgentNetwork) também as calculará. +BASE_FEATURES_PER_ASSET_INPUT = [ # Renomeado para clareza + 'open_div_atr', 'high_div_atr', 'low_div_atr', 'close_div_atr', 'volume_div_atr', + 'log_return', 'rsi_14', 'atr', 'bbp', 'cci_37', 'mfi_37', + 'body_size_norm_atr', 'body_vs_avg_body', 'macd', 'sma_10_div_atr', + 'adx_14', 'volume_zscore', 'buy_condition_v1' + # Se 'cond_compra_v1' for diferente de 'buy_condition_v1', adicione aqui. + # Se for igual, remova a redundância. Assumindo que 'buy_condition_v1' é a correta. +] +# Nomes das colunas de preço/volume (normalizadas por ATR) que usarão o price_vol_scaler +API_PRICE_VOL_COLS = ['open_div_atr', 'high_div_atr', 'low_div_atr', 'close_div_atr', 'volume_div_atr', 'body_size_norm_atr'] +# Nomes das colunas de indicadores (e outras) que usarão o indicator_scaler +API_INDICATOR_COLS = [col for col in BASE_FEATURES_PER_ASSET_INPUT if col not in API_PRICE_VOL_COLS] + +# Número de features que CADA ativo terá após todos os cálculos e ANTES do escalonamento final. +NUM_FEATURES_PER_ASSET = len(BASE_FEATURES_PER_ASSET_INPUT) + +# Nomes das colunas escaladas que o modelo RNN/RL efetivamente verá como entrada. +# Esta é a ordem que deve ser mantida após o escalonamento no data_handler e no rnn_predictor. +# E também o que o create_sequences espera. +EXPECTED_SCALED_FEATURES_FOR_MODEL = [f"{col}_scaled" for col in BASE_FEATURES_PER_ASSET_INPUT] +# NUM_FEATURES_MODEL_INPUT será len(EXPECTED_SCALED_FEATURES_FOR_MODEL), que é igual a NUM_FEATURES_PER_ASSET + +# --- Parâmetros do Alvo da Predição (Para o Modelo de Classificação Supervisionado, se ainda usar) --- +PREDICTION_HORIZON = 5 +PRICE_CHANGE_THRESHOLD = 0.0075 + +# --- Parâmetros da Rede Neural (DeepPortfolioAgentNetwork e seu AssetProcessor) --- +# Para AssetProcessor (processamento individual de ativo) +ASSET_CNN_FILTERS1 = 32 +ASSET_CNN_FILTERS2 = 64 +ASSET_LSTM_UNITS1 = 64 +ASSET_LSTM_UNITS2 = 32 # Saída do AssetProcessor, se torna a dimensão da feature latente por ativo +ASSET_DROPOUT = 0.2 + +# Para DeepPortfolioAgentNetwork (camadas após processamento individual) +MHA_NUM_HEADS = 4 +# key_dim da MHA será ASSET_LSTM_UNITS2 // MHA_KEY_DIM_DIVISOR +MHA_KEY_DIM_DIVISOR = 2 # Ex: 32 // 2 = 16. Garanta que ASSET_LSTM_UNITS2 seja divisível. Se não, ajuste. + +# Camadas densas FINAIS DENTRO da DeepPortfolioAgentNetwork, ANTES da saída de features latentes +# ou da camada de alocação softmax (se não estiver retornando features latentes). +# `FINAL_DENSE_UNITS2_EXTRACTOR` será a dimensão das features que o extrator cospe para o SB3. +DPN_FINAL_DENSE1_UNITS = 64 # "DPN" para DeepPortfolioNetwork +DPN_LATENT_FEATURE_DIM = 32 # Saída da DPN quando output_latent_features=True. IGUAL A ASSET_LSTM_UNITS2 se não houver mais camadas após GAP. + # Se você adicionou Dense(final_dense_units1) e Dense(final_dense_units2) APÓS a atenção + # no DeepPortfolioAgentNetwork, então DPN_LATENT_FEATURE_DIM seria final_dense_units2. + # No nosso último design, era a saída do global_avg_pool_attention, então ASSET_LSTM_UNITS2. + # Vamos assumir que a saída do GAP é usada como feature latente por enquanto. + # DPN_LATENT_FEATURE_DIM = ASSET_LSTM_UNITS2 + +# Ajustando com base no seu código de `deep_portfolio.py` onde você tinha `final_dense_units1` e `final_dense_units2` +# após a atenção e antes do output de alocação. +# Estas são as camadas que produzem as features latentes para SB3. +DPN_SHARED_HEAD_DENSE1_UNITS = 128 # Corresponde a final_dense_units1 na sua DeepPortfolioAgentNetwork +DPN_SHARED_HEAD_LATENT_DIM = 64 # Corresponde a final_dense_units2, que será o self.features_dim do extrator +DPN_SHARED_HEAD_DROPOUT = 0.3 + +DEFAULT_EXTRACTOR_KWARGS=DPN_SHARED_HEAD_DENSE1_UNITS + +# Para as cabeças de Política (Ator) e Valor (Crítico) no Stable-Baselines3 (APÓS o extrator) +# Se vazias, a saída do extrator é usada diretamente para as camadas finais de ação/valor. +POLICY_HEAD_NET_ARCH = [64] # Ex: [64, 32] ou [] se não quiser camadas extras +VALUE_HEAD_NET_ARCH = [64] # Ex: [64, 32] ou [] + +# --- Parâmetros Gerais do Modelo (se aplicável a ambos os tipos de modelo) --- +MODEL_DROPOUT_RATE = 0.3 # Você usou 0.3 na última rodada de classificação bem-sucedida +MODEL_L2_REG = 0.0001 # Você usou 0.0001 ou 0.0005 + +L2_REG = 0.0001 + +# --- Parâmetros de Treinamento --- +# Para o modelo de classificação supervisionado (se ainda usar) +SUPERVISED_LEARNING_RATE = 0.0005 +SUPERVISED_BATCH_SIZE = 128 +SUPERVISED_EPOCHS = 100 +LEARNING_RATE=0.0005 + +# Para o agente RL (PPO) +PPO_LEARNING_RATE = 0.0003 # Padrão do SB3 PPO, pode ajustar +PPO_N_STEPS = 2048 +PPO_BATCH_SIZE_RL = 64 # Mini-batch size do PPO +PPO_ENT_COEF = 0.01 +PPO_TOTAL_TIMESTEPS = 1000000 # Comece com menos para teste (ex: 50k-100k) + +# --- Parâmetros do Ambiente RL --- +# RISK_FREE_RATE_ANNUAL = 0.02 # Taxa livre de risco anual (ex: 2%) +# REWARD_WINDOW_SHARPE = 252 * 1 # Ex: Janela de 1 ano de dados horários para Sharpe (252 dias * 24h) + # Ou uma janela menor como 60 ou 120 passos. +INITIAL_BALANCE = 100000 +TRANSACTION_COST_PCT = 0.001 # 0.1% + +# --- Caminhos para Salvar --- +MODEL_ROOT_DIR = "app/model" # Diretório raiz para todos os modelos e scalers +# Para modelo de classificação supervisionado (se mantiver) +SUPERVISED_MODEL_NAME = "classification_model.h5" +SUPERVISED_PV_SCALER_NAME = "supervisor_pv_scaler.joblib" +SUPERVISED_IND_SCALER_NAME = "supervisor_ind_scaler.joblib" +# Para modelo RL (agente PPO salvo pelo SB3) +RL_AGENT_MODEL_NAME = "ppo_deep_portfolio_agent" # SB3 adiciona .zip +# Scalers usados para preparar dados para o DeepPortfolioAgentNetwork (que é o extrator do RL) +RL_PV_SCALER_NAME = "rl_price_volume_atr_norm_scaler.joblib" # Seus nomes descritivos +RL_INDICATOR_SCALER_NAME = "rl_other_indicators_scaler.joblib" +FINAL_DENSE_UNITS1_EXTRACTOR=DEFAULT_EXTRACTOR_KWARGS +USE_SENTIMENT_CONFIG=True \ No newline at end of file diff --git a/agents/custom_policies.py b/agents/custom_policies.py new file mode 100644 index 0000000000000000000000000000000000000000..1d12dba096c8023b9b83d4b1830d3c5673ac77fb --- /dev/null +++ b/agents/custom_policies.py @@ -0,0 +1,99 @@ +# rnn/agents/custom_policies.py (NOVO ARQUIVO, ou adicione ao deep_portfolio.py) + +import gymnasium as gym # Usar gymnasium +import tensorflow as tf +from stable_baselines3.common.torch_layers import BaseFeaturesExtractor as PyTorchBaseFeaturesExtractor + +from stable_baselines3.common.torch_layers import MlpExtractor +import torch.nn as nn +import torch + +class CustomTFMlpExtractor(tf.keras.layers.Layer): + def __init__(self, feature_dim, net_arch, activation_fn=tf.nn.relu): + super().__init__() + self.net = tf.keras.Sequential() + for units in net_arch: + self.net.add(tf.keras.layers.Dense(units)) + self.net.add(tf.keras.layers.Activation(activation_fn)) + + def call(self, inputs, training=False): + return self.net(inputs, training=training) + + + +class CustomMlpExtractor(MlpExtractor): + def __init__(self, input_dim, net_arch, activation_fn, device): + super().__init__(input_dim, net_arch, activation_fn, device) + + def forward(self, features): + for layer in self.policy_net: + if isinstance(layer, nn.ReLU): + features = layer(features) # Passando 'features' como argumento + else: + features = layer(features) + return features +# Para TensorFlow, precisamos de um extrator de features compatível ou construir a política de forma diferente. +# Stable Baselines3 tem melhor suporte nativo para PyTorch. Para TF, é um pouco mais manual. +# VAMOS USAR A ABORDAGEM DE POLÍTICA CUSTOMIZADA COM TF DIRETAMENTE. +from stable_baselines3.common.policies import ActorCriticPolicy +from typing import List, Dict, Any, Optional, Union, Type +# Importar sua rede e configs +#import agents.DeepPortfolioAgent as DeepPortfolioAgent +from portfolio_features_extractor_torch import PortfolioFeaturesExtractorTorch +# from ..config import (NUM_ASSETS, WINDOW_SIZE, NUM_FEATURES_PER_ASSET, ...) # Importe do seu config real +# VALORES DE EXEMPLO (PEGUE DO SEU CONFIG.PY REAL) +NUM_ASSETS_POLICY = 4 +WINDOW_SIZE_POLICY = 60 +NUM_FEATURES_PER_ASSET_POLICY = 26 +# Hiperparâmetros para DeepPortfolioAgentNetwork quando usada como extrator +ASSET_CNN_FILTERS1_POLICY = 32 +ASSET_CNN_FILTERS2_POLICY = 64 +ASSET_LSTM_UNITS1_POLICY = 64 +ASSET_LSTM_UNITS2_POLICY = 32 # Esta será a dimensão das features latentes para ator/crítico +ASSET_DROPOUT_POLICY = 0.2 +MHA_NUM_HEADS_POLICY = 4 +MHA_KEY_DIM_DIVISOR_POLICY = 2 # Para key_dim = 32 // 2 = 16 +FINAL_DENSE_UNITS1_POLICY = 128 +FINAL_DENSE_UNITS2_POLICY = ASSET_LSTM_UNITS2_POLICY # A saída da dense2 SÃO as features latentes +FINAL_DROPOUT_POLICY = 0.3 + + + + +class CustomPolicy(ActorCriticPolicy): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.mlp_extractor = CustomTFMlpExtractor( + input_dim=self.observation_space.shape[0], + net_arch=[64, 64], # Exemplo de arquitetura + activation_fn=nn.ReLU, + device=self.device + ) + +from torch import nn + +class CustomPortfolioPolicySB3(ActorCriticPolicy): + def __init__( + self, + observation_space: gym.spaces.Space, + action_space: gym.spaces.Space, + lr_schedule, + net_arch: Optional[List[Union[int, Dict[str, List[int]]]]] = None, + activation_fn: Type[nn.Module] = nn.ReLU, # Use PyTorch ReLU + features_extractor_kwargs: Optional[Dict[str, Any]] = None, + **kwargs, + ): + if features_extractor_kwargs is None: + features_extractor_kwargs = {} + features_extractor_kwargs.setdefault("features_dim", ASSET_LSTM_UNITS2_POLICY) + + super().__init__( + observation_space, + action_space, + lr_schedule, + net_arch=net_arch, + activation_fn=activation_fn, # Passando nn.ReLU + features_extractor_class=PortfolioFeaturesExtractorTorch, + features_extractor_kwargs=features_extractor_kwargs, + **kwargs, + ) \ No newline at end of file diff --git a/agents/data_handler_multi_asset.py b/agents/data_handler_multi_asset.py new file mode 100644 index 0000000000000000000000000000000000000000..781238c72e5ef24778b626004f0674b0f32599b6 --- /dev/null +++ b/agents/data_handler_multi_asset.py @@ -0,0 +1,448 @@ +# rnn/data_handler_multi_asset.py (NOVO ARQUIVO) + + +import pandas as pd +import numpy as np +import yfinance as yf # Ou ccxt, dependendo da sua preferência de fonte de dados +import pandas_ta as ta +from datetime import datetime, timedelta, timezone +from typing import List, Dict, Optional + +WINDOW_SIZE = 60 + +# Importar do seu config.py +# Assumindo que config.py está em ../config.py ou rnn/config.py +# Ajuste o import conforme sua estrutura. +# Se train_rnn_model.py e este data_handler estiverem na mesma pasta 'scripts', +# e config.py estiver um nível acima: +# from ..app/config.py import ( +# MULTI_ASSET_LIST, TIMEFRAME, DAYS_OF_DATA_TO_FETCH, +# # etc. para features a serem calculadas +# ) +# Por agora, vamos definir aqui para exemplo: + +# EXEMPLO DE CONFIGURAÇÃO (Mova para config.py depois) +MULTI_ASSET_SYMBOLS = { + 'eth': 'ETH-USD', + 'btc': 'BTC-USD', + 'ada': 'ADA-USD', + 'sol': 'SOL-USD' +} # Use os tickers corretos para yfinance ou ccxt +TIMEFRAME_YFINANCE = '1h' # yfinance suporta '1m', '2m', '5m', '15m', '30m', '60m', '90m', '1h', '1d', '5d', '1wk', '1mo', '3mo' +# Para '1h', yfinance só retorna os últimos 730 dias. Para mais dados, use '1d'. +# Se usar ccxt, TIMEFRAME = '1h' como antes. +DAYS_TO_FETCH = 365 * 2 # 2 anos + +# Lista das features base que você quer calcular para CADA ativo +# (as 19 que definimos antes) +INDIVIDUAL_ASSET_BASE_FEATURES = [ + 'open', 'high', 'low', 'close', 'volume', # OHLCV originais são necessários para os cálculos + 'sma_10', 'rsi_14', 'macd', 'macds', 'atr', 'bbp', 'cci_37', 'mfi_37', 'adx_14', + 'volume_zscore', 'body_size', 'body_size_norm_atr', 'body_vs_avg_body', + 'log_return', 'buy_condition_v1', + 'open_div_atr', 'high_div_atr', + 'low_div_atr', + 'close_div_atr', + 'volume_div_atr', + 'sma_10_div_atr' # 'sma_50' é calculada dentro de buy_condition_v1 + # As colunas _div_atr serão criadas a partir destas +] + +# Features que serão normalizadas pelo ATR +COLS_TO_NORM_BY_ATR = ['open', 'high', 'low', 'close', 'volume', 'sma_10', 'macd', 'body_size'] +#---------------------------------- +# -- New get multi asset data for rl +# ... (imports e outras funções como antes) ... + +def get_multi_asset_data_for_rl( + asset_symbols_map: Dict[str, str], + timeframe_yf: str, + days_to_fetch: int, + logger_instance # Adicionar logger como parâmetro +) -> Optional[pd.DataFrame]: # Adicionar logger como parâmetro + + all_asset_features_list: List[pd.DataFrame] = [] # Tipagem para clareza + min_data_length = float('inf') + print(all_asset_features_list) + + #logger_instance.info(f"Iniciando get_multi_asset_data_for_rl para: {list(asset_symbols_map.keys())}") + + for asset_key, yf_ticker in asset_symbols_map.items(): + #logger_instance.info(f"\n--- Processando {asset_key} ({yf_ticker}) ---") + # ... (lógica de fetch_single_asset_ohlcv_yf como antes) ... + single_asset_ohlcv = fetch_single_asset_ohlcv_yf(yf_ticker, period=f"{days_to_fetch}d", interval=timeframe_yf) # Passar logger se a função aceitar + + # if single_asset_ohlcv.empty: + # logger_instance.warning(f"Sem dados OHLCV para {yf_ticker}, pulando.") + # continue + + single_asset_features = calculate_all_features_for_single_asset(single_asset_ohlcv)#, logger_instance) + + if single_asset_features is None or single_asset_features.empty: + logger_instance.warning(f"Sem features calculadas para {yf_ticker}, pulando.") + continue + + #logger_instance.info(f"Features para {asset_key} shape: {single_asset_features.shape}, Index Min: {single_asset_features.index.min()}, Index Max: {single_asset_features.index.max()}") + + # Garantir que o índice é DatetimeIndex e UTC para todos antes de adicionar prefixo e à lista + if not isinstance(single_asset_features.index, pd.DatetimeIndex): + single_asset_features.index = pd.to_datetime(single_asset_features.index) + if single_asset_features.index.tz is None: + single_asset_features.index = single_asset_features.index.tz_localize('UTC') + else: + single_asset_features.index = single_asset_features.index.tz_convert('UTC') + + single_asset_features = single_asset_features.add_prefix(f"{asset_key}_") + all_asset_features_list.append(single_asset_features) + min_data_length = min(min_data_length, len(single_asset_features)) + + if not all_asset_features_list: + logger_instance.error("Nenhum DataFrame de feature de ativo foi adicionado à lista.") + return None + + if min_data_length == float('inf') or min_data_length < WINDOW_SIZE: # Adicionada checagem de WINDOW_SIZE + logger_instance.error(f"min_data_length inválido ({min_data_length}) ou menor que WINDOW_SIZE ({WINDOW_SIZE}). Não é possível truncar/usar.") + return None + + #logger_instance.info(f"Menor número de linhas de dados encontrado (min_data_length): {min_data_length}") + + # Truncar para garantir que todos os DFs tenham o mesmo comprimento ANTES do concat + # E que tenham pelo menos min_data_length. + # É importante que os ÍNDICES de data/hora se sobreponham para o join='inner' funcionar. + # Apenas pegar o .tail() pode não alinhar os timestamps se os DFs tiverem começos diferentes. + + # Melhor abordagem: encontrar o índice comum mais recente e o mais antigo. + if not all_asset_features_list: # Checagem se a lista não ficou vazia por algum motivo + logger_instance.error("all_asset_features_list está vazia antes do alinhamento de índice.") + return None + print(all_asset_features_list) + # Alinhar DataFrames por um índice comum antes de concatenar + # 1. Encontrar o primeiro timestamp comum a todos + # 2. Encontrar o último timestamp comum a todos + # Ou, mais simples, confiar no join='inner' do concat, mas garantir que os DFs são válidos. + + # Vamos simplificar e manter o truncamento pelo tail, mas com mais logs + # e garantir que são DataFrames. + + truncated_asset_features_list = [] + for i, df_asset in enumerate(all_asset_features_list): + if isinstance(df_asset, pd.DataFrame) and len(df_asset) >= min_data_length: + truncated_df = df_asset.tail(min_data_length) + #logger_instance.info(f" DF truncado {i} ({df_asset.columns[0].split('_')[0]}): shape {truncated_df.shape}, ") + #f"Index Min: {truncated_df.index.min()}, Max: {truncated_df.index.max()}") + truncated_asset_features_list.append(truncated_df) + else: + asset_name_debug = df_asset.columns[0].split('_')[0] if isinstance(df_asset, pd.DataFrame) and not df_asset.empty else f"DF_{i}" + logger_instance.warning(f" DF {asset_name_debug} inválido ou muito curto (len: {len(df_asset) if isinstance(df_asset, pd.DataFrame) else 'N/A'}) para truncamento. Pulando.") + + if not truncated_asset_features_list: + #ogger_instance.error("Nenhum DataFrame válido restou após truncamento para concatenar.") + return None + + # Se houver apenas UM DataFrame na lista, não precisa concatenar, apenas retorna ele. + if len(truncated_asset_features_list) == 1: + #logger_instance.info("Apenas um DataFrame de ativo processado, retornando-o diretamente.") + combined_df = truncated_asset_features_list[0] + else: + #logger_instance.info(f"Concatenando {len(truncated_asset_features_list)} DataFrames de ativos com join='inner'...") + try: + combined_df = pd.concat(truncated_asset_features_list, axis=1, join='outer') + print(combined_df) + except Exception as e_concat: + logger_instance.error(f"ERRO CRÍTICO durante pd.concat: {e_concat}", exc_info=True) + return None + + + combined_df.fillna(method='ffill', inplace=True) + # Depois do ffill, ainda pode haver NaNs no início se algum ativo começar depois dos outros. + if not combined_df.empty: + #logger_instance.info(f"Shape após ffill: {combined_df.shape}. Buscando primeiro/último índice válido...") + first_valid_index = combined_df.first_valid_index() + last_valid_index = combined_df.last_valid_index() + + if pd.isna(first_valid_index) or pd.isna(last_valid_index): + print("Não foi possível determinar first/last_valid_index após ffill.") + return None + + print(f"Primeiro índice válido: {first_valid_index}, Último índice válido: {last_valid_index}") + combined_df = combined_df.loc[first_valid_index:last_valid_index] + print(f"Shape após fatiar por first/last valid index: {combined_df.shape}") + + # Um dropna final pode ser necessário se o ffill não pegou tudo (improvável, mas seguro) + combined_df.dropna(inplace=True) + print(f"Shape após dropna final (pós-fatiamento): {combined_df.shape}") + print("Imprimindo DF_COMBINED com index ") + print(combined_df) + + #logger_instance.info(f"Tipo de combined_df após concat: {type(combined_df)}") + if not isinstance(combined_df, pd.DataFrame): + logger_instance.error(f"combined_df NÃO é um DataFrame após concat. Tipo: {type(combined_df)}") + return None + + + return combined_df + # if combined_df.empty: + + # logger_instance.error("DataFrame combinado está VAZIO após concatenação e join='inner'. " + # "Isso geralmente significa que não há timestamps comuns entre TODOS os ativos processados.") + # # Adicionar mais depuração aqui se isso acontecer: + # # for i, df_trunc_debug in enumerate(truncated_asset_features_list): + # # logger_instance.info(f"Debug DF {i} - Head:\n{df_trunc_debug.head(3)}") + # # logger_instance.info(f"Debug DF {i} - Tail:\n{df_trunc_debug.tail(3)}") + # # logger_instance.info(f"DataFrame combinado ANTES do dropna final, shape: {combined_df.shape}") + # print(combined_df.head()) # Descomente para depuração pesada + # return None + + + + +# ... +#---------------------------------- + +# def get_multi_asset_data_for_rl( +# asset_symbols_map: Dict[str, str], +# timeframe_yf: str, +# days_to_fetch: int +# ) -> Optional[pd.DataFrame]: +# """ +# Busca, processa e combina dados de múltiplos ativos em um DataFrame achatado. +# """ +# all_asset_features_list = [] +# # min_data_length = float('inf') # Inicializar com um valor alto +# # Vamos inicializar com 0 e pegar o len do primeiro DF válido +# min_data_length = 0 +# first_valid_df_processed = False + +# for asset_key, yf_ticker in asset_symbols_map.items(): +# print(f"\n--- Processando {asset_key} ({yf_ticker}) ---") +# period_yf = f"{days_to_fetch}d" +# if timeframe_yf == '1h' and days_to_fetch > 730: +# print(f"AVISO: Para {timeframe_yf}, buscando no máximo 730 dias com yfinance para {yf_ticker}.") +# period_yf = "730d" + +# single_asset_ohlcv = fetch_single_asset_ohlcv_yf(yf_ticker, period=period_yf, interval=timeframe_yf) +# if single_asset_ohlcv.empty: +# print(f"AVISO: Sem dados OHLCV para {yf_ticker}, pulando este ativo.") +# continue + +# single_asset_features = calculate_all_features_for_single_asset(single_asset_ohlcv) # Passar logger se tiver +# if single_asset_features is None or single_asset_features.empty: +# print(f"AVISO: Sem features calculadas para {yf_ticker}, pulando este ativo.") +# continue + +# single_asset_features = single_asset_features.add_prefix(f"{asset_key}_") +# all_asset_features_list.append(single_asset_features) + +# # Atualizar min_data_length de forma mais segura +# if not first_valid_df_processed: +# min_data_length = len(single_asset_features) +# first_valid_df_processed = True +# else: +# min_data_length = min(min_data_length, len(single_asset_features)) + +# if not all_asset_features_list: # Se nenhum ativo foi processado com sucesso +# print("ERRO: Nenhum dado de feature de ativo foi processado com sucesso para a lista `all_asset_features_list`.") +# return None # Retorna None, que não tem '.empty' mas será checado por 'is None' + +# if min_data_length == 0 : # Checagem adicional se algo deu muito errado +# print("ERRO: min_data_length é zero após processar ativos, não é possível truncar DataFrames.") +# return None + +# print(f"Menor número de linhas de dados encontrado entre os ativos (min_data_length): {min_data_length}") +# truncated_asset_features_list = [df.tail(min_data_length) for df in all_asset_features_list if not df.empty and len(df) >= min_data_length] + +# # Verificar se a lista de DFs truncados não está vazia ANTES de concatenar +# if not truncated_asset_features_list: +# print("ERRO: Nenhum DataFrame válido restou após o truncamento. Não é possível concatenar.") +# return None + +# print(f"Concatenando {len(truncated_asset_features_list)} DataFrames de ativos...") +# try: +# combined_df = pd.concat(truncated_asset_features_list, axis=1, join='inner') +# except Exception as e_concat: +# print(f"ERRO durante pd.concat: {e_concat}") +# return None # Retorna None em caso de erro na concatenação + +# # Agora, combined_df DEVE ser um DataFrame (mesmo que vazio se o join falhar) +# if not isinstance(combined_df, pd.DataFrame): +# print(f"ERRO: pd.concat não retornou um DataFrame. Tipo retornado: {type(combined_df)}") +# return None + +# if combined_df.empty: # Esta checagem agora deve funcionar ou ser desnecessária se o anterior já retornou None +# print("ERRO: DataFrame combinado está vazio após concatenação e join. Verifique os dados dos ativos e o alinhamento de datas.") +# return None + +# combined_df.dropna(inplace=True) + +# if combined_df.empty: # Checagem final após dropna +# print("ERRO: DataFrame combinado está vazio após dropna final.") +# return None + +# print(f"\nDataFrame multi-ativo final gerado com shape: {combined_df.shape}") +# return combined_df + + + + +def fetch_single_asset_ohlcv_yf(ticker_symbol: str, period: str = "2y", interval: str = "1h") -> pd.DataFrame: + """ Adaptação da sua função fetch_historical_ohlcv de financial_data_agent.py """ + print(f"Buscando dados para {ticker_symbol} com yfinance (period: {period}, interval: {interval})...") + try: + ticker = yf.Ticker(ticker_symbol) + # Para dados horários, o período máximo é geralmente 730 dias com yfinance + # Se precisar de mais, considere '1d' e depois reamostre, ou use ccxt para cripto. + if interval == '1h' and period.endswith('y') and int(period[:-1]) * 365 > 730: + print(f"AVISO: yfinance pode limitar dados horários a 730 dias. Buscando 'max' para {interval} e depois fatiando.") + data = ticker.history(interval=interval, period="730d") # Pega o máximo possível + elif interval == '1d' and period.endswith('y'): + data = ticker.history(period=period, interval=interval) + else: # Para períodos menores ou outros intervalos + data = ticker.history(period=period, interval=interval) + + if data.empty: + print(f"Nenhum dado encontrado para {ticker_symbol}.") + return pd.DataFrame() + + data.rename(columns={ + "Open": "open", "High": "high", "Low": "low", + "Close": "close", "Volume": "volume", "Adj Close": "adj_close" + }, inplace=True) + + # Selecionar apenas as colunas OHLCV e garantir que o índice é DatetimeIndex UTC + data = data[['open', 'high', 'low', 'close', 'volume']] + if data.index.tz is None: + data.index = data.index.tz_localize('UTC') + else: + data.index = data.index.tz_convert('UTC') + + # Para dados horários, yfinance pode retornar dados do fim de semana (sem volume) + # e o último candle pode estar incompleto. + # if interval == '1h': + # data = data[data['volume'] > 0] # Remover candles sem volume + # data = data[:-1] # Remover o último candle que pode estar incompleto + + print(f"Dados coletados para {ticker_symbol}: {len(data)} linhas.") + return data + except Exception as e: + print(f"Erro ao buscar dados para {ticker_symbol} com yfinance: {e}") + return pd.DataFrame() + + +def calculate_all_features_for_single_asset(ohlcv_df: pd.DataFrame) -> Optional[pd.DataFrame]: + """Calcula todas as features base para um único ativo.""" + if ohlcv_df.empty: return None + df = ohlcv_df.copy() + print(f"Calculando features para ativo (shape inicial: {df.shape})...") + + # GARANTIR que a coluna 'close' original será preservada no DataFrame final + if 'close' in df.columns: + df['close'] = df['close'] # redundante, mas deixa explícito que não será sobrescrita + + if ta: + df.ta.sma(length=10, close='close', append=True, col_names=('sma_10',)) + df.ta.rsi(length=14, close='close', append=True, col_names=('rsi_14',)) + macd_out = df.ta.macd(close='close', append=False) + if macd_out is not None and not macd_out.empty: + df['macd'] = macd_out.iloc[:,0] + df['macds'] = macd_out.iloc[:,2] # Linha de sinal para buy_condition + df.ta.atr(length=14, append=True, col_names=('atr',)) + df.ta.bbands(length=20, close='close', append=True, col_names=('bbl', 'bbm', 'bbu', 'bbb', 'bbp')) + df.ta.cci(length=37, append=True, col_names=('cci_37',)) + df['volume'] = df['volume'].astype(float) + df.ta.mfi(length=37, close='close', high='high', low='low', volume='volume', append=True, col_names=('mfi_37',)) + df.ta.mfi(length=37, append=True, col_names=('mfi_37',)) + adx_out = df.ta.adx(length=14, append=False) + if adx_out is not None and not adx_out.empty: + df['adx_14'] = adx_out.iloc[:,0] + + rolling_vol_mean = df['volume'].rolling(window=20).mean() + rolling_vol_std = df['volume'].rolling(window=20).std() + df['volume_zscore'] = (df['volume'] - rolling_vol_mean) / (rolling_vol_std + 1e-9) + + df['body_size'] = abs(df['close'] - df['open']) + + # ATR precisa existir para as próximas. Drop NaNs do ATR primeiro. + df.dropna(subset=['atr'], inplace=True) + df_atr_valid = df[df['atr'] > 1e-9].copy() + if df_atr_valid.empty: + print("AVISO: ATR inválido para todas as linhas restantes, features _div_atr e body_size_norm_atr podem ser todas NaN ou vazias.") + # Criar colunas com NaN para manter a estrutura + df['body_size_norm_atr'] = np.nan + for col in COLS_TO_NORM_BY_ATR: + df[f'{col}_div_atr'] = np.nan + else: + df['body_size_norm_atr'] = df['body_size'] / df['atr'] # ATR já filtrado para > 1e-9 + for col in COLS_TO_NORM_BY_ATR: + if col in df.columns: + df[f'{col}_div_atr'] = df[col] / (df['atr'] + 1e-9) # Adicionar 1e-9 aqui também por segurança + else: + df[f'{col}_div_atr'] = np.nan + + + df['body_vs_avg_body'] = df['body_size'] / (df['body_size'].rolling(window=20).mean() + 1e-9) + df['log_return'] = np.log(df['close'] / df['close'].shift(1)) + + sma_50_series = df.ta.sma(length=50, close='close', append=False) + if sma_50_series is not None: df['sma_50'] = sma_50_series + else: df['sma_50'] = np.nan + + if all(col in df.columns for col in ['macd', 'macds', 'rsi_14', 'close', 'sma_50']): + df['buy_condition_v1'] = ((df['macd'] > df['macds']) & (df['rsi_14'] > 50) & (df['close'] > df['sma_50'])).astype(int) + else: + df['buy_condition_v1'] = 0 + + # Selecionar apenas as colunas que realmente usaremos como features base para o modelo + # (incluindo as _div_atr e as originais que não foram normalizadas por ATR) + # Esta lista de features é a que será passada para os scalers no script de treino. + # E também as colunas que o rnn_predictor.py precisará ter antes de aplicar seus scalers. + # Esta lista deve vir do config.py (BASE_FEATURE_COLS) + + # Exemplo: + # final_feature_columns = [ + # 'open_div_atr', 'high_div_atr', 'low_div_atr', 'close_div_atr', 'volume_div_atr', + # 'log_return', 'rsi_14', 'atr', 'bbp', 'cci_37', 'mfi_37', + # 'body_size_norm_atr', 'body_vs_avg_body', 'macd', 'sma_10_div_atr', + # 'adx_14', 'volume_zscore', 'buy_condition_v1' + # ] # Esta é a BASE_FEATURE_COLS do seu config.py + + # Verificar se todas as colunas em INDIVIDUAL_ASSET_BASE_FEATURES existem + # (INDIVIDUAL_ASSET_BASE_FEATURES deve ser igual a config.BASE_FEATURE_COLS) + current_feature_cols = [col for col in INDIVIDUAL_ASSET_BASE_FEATURES if col in df.columns] + missing_cols = [col for col in INDIVIDUAL_ASSET_BASE_FEATURES if col not in df.columns] + if missing_cols: + print(f"AVISO: Colunas de features ausentes após cálculo: {missing_cols}. Usando apenas as disponíveis: {current_feature_cols}") + # GARANTIR que 'close' (preço original) está presente nas features finais + if 'close' not in current_feature_cols and 'close' in df.columns: + current_feature_cols.append('close') + + df_final_features = df[current_feature_cols].copy() + df_final_features.dropna(inplace=True) + print(f"Features calculadas. Shape após dropna: {df_final_features.shape}. Colunas: {df_final_features.columns.tolist()}") + return df_final_features + else: + print("pandas_ta não está disponível.") + return None + + +if __name__ == '__main__': + print("Testando data_handler_multi_asset.py...") + # Substitua pelos tickers yfinance reais que você quer usar + test_assets = { + 'eth': 'ETH-USD', + 'btc': 'BTC-USD', + # 'aapl': 'AAPL' # Exemplo de ação + } + multi_asset_data = get_multi_asset_data_for_rl( + test_assets, + timeframe_yf='1h', # Para teste rápido, período menor + days_to_fetch=90 # Para teste rápido, período menor + ) + + if multi_asset_data is not None and not multi_asset_data.empty: + print("\n--- Exemplo do DataFrame Multi-Ativo Gerado ---") + print(multi_asset_data.head()) + print(f"\nShape: {multi_asset_data.shape}") + print(f"\nInfo:") + multi_asset_data.info() + else: + print("\nFalha ao gerar DataFrame multi-ativo.") \ No newline at end of file diff --git a/agents/dataset_update_agent.py b/agents/dataset_update_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce506c7a500cdb62a85d8d9b869e6d9f3145769 --- /dev/null +++ b/agents/dataset_update_agent.py @@ -0,0 +1,9 @@ +# agents/dataset_update_agent.py +import pandas as pd +from datetime import datetime + +def update_dataset(path="data/sp500_news.csv", new_data=pd.DataFrame()): + df = pd.read_csv(path) + combined = pd.concat([df, new_data]).drop_duplicates().reset_index(drop=True) + combined.to_csv(path, index=False) + print(f"Dataset atualizado: {path}") diff --git a/agents/deep_portfolio.py b/agents/deep_portfolio.py new file mode 100644 index 0000000000000000000000000000000000000000..2e13a9c60abc9ce554adbaca3b7ed39e7eb9240e --- /dev/null +++ b/agents/deep_portfolio.py @@ -0,0 +1,104 @@ +import numpy as np +import tensorflow as tf +from tensorflow.keras.layers import LSTM, Dense, Conv1D, MultiHeadAttention +from transformers import AutoTokenizer, TFAutoModelForSequenceClassification # Changed here +import gymnasium as gym +from gymnasium import spaces + +class DeepPortfolioAI(tf.keras.Model): + def __init__(self, num_assets, sequence_length=60): + super(DeepPortfolioAI, self).__init__() + + # Parâmetros do modelo + self.num_assets = num_assets + self.sequence_length = sequence_length + + # CNN para análise de padrões técnicos + self.conv1 = Conv1D(64, 3, activation='relu') + self.conv2 = Conv1D(128, 3, activation='relu') + + # LSTM para análise temporal + self.lstm1 = LSTM(128, return_sequences=True) + self.lstm2 = LSTM(64) + + # Attention para correlações entre ativos + self.attention = MultiHeadAttention(num_heads=8, key_dim=64) + + # Camadas densas para decisão final + self.dense1 = Dense(256, activation='relu') + self.dense2 = Dense(128, activation='relu') + self.output_layer = Dense(num_assets, activation='softmax') + + # Inicializar tokenizer e modelo de sentimento + self.tokenizer = AutoTokenizer.from_pretrained('ProsusAI/finbert') + self.sentiment_model = TFAutoModelForSequenceClassification.from_pretrained('ProsusAI/finbert') # Changed here + + def call(self, inputs): + market_data, news_data = inputs + + # Análise técnica com CNN + x_technical = self.conv1(market_data) + x_technical = self.conv2(x_technical) + + # Análise temporal com LSTM + x_temporal = self.lstm1(x_technical) + x_temporal = self.lstm2(x_temporal) + + # Attention para correlações + x_attention = self.attention(x_temporal, x_temporal, x_temporal) + + # Combinar com análise de sentimento + sentiment_embeddings = self._process_news(news_data) + x_combined = tf.concat([x_attention, sentiment_embeddings], axis=-1) + + # Camadas densas finais + x = self.dense1(x_combined) + x = self.dense2(x) + return self.output_layer(x) + + def _process_news(self, news_data): + inputs = self.tokenizer(news_data, return_tensors="pt", padding=True, truncation=True) + sentiment_scores = self.sentiment_model(**inputs).logits + return tf.convert_to_tensor(sentiment_scores.detach().numpy()) + +class PortfolioEnvironment(gym.Env): + def __init__(self, data, initial_balance=100000): + super(PortfolioEnvironment, self).__init__() + + self.data = data + self.initial_balance = initial_balance + self.current_step = 0 + + # Define espaços de ação e observação + self.action_space = spaces.Box( + low=0, high=1, shape=(len(data.columns),), dtype=np.float32) + self.observation_space = spaces.Box( + low=-np.inf, high=np.inf, shape=(60, len(data.columns)), dtype=np.float32) + + def reset(self): + self.current_step = 0 + self.balance = self.initial_balance + self.portfolio = np.zeros(len(self.data.columns)) + return self._get_observation() + + def step(self, action): + # Implementar lógica de negociação + current_prices = self.data.iloc[self.current_step] + next_prices = self.data.iloc[self.current_step + 1] + + # Calcular retorno + returns = (next_prices - current_prices) / current_prices + reward = np.sum(action * returns) + + # Atualizar portfolio + self.portfolio = action + self.balance *= (1 + reward) + + # Incrementar step + self.current_step += 1 + done = self.current_step >= len(self.data) - 1 + + return self._get_observation(), reward, done, {} + + def _get_observation(self): + return self.data.iloc[self.current_step-60:self.current_step].values \ No newline at end of file diff --git a/agents/deep_portfolio_torch.py b/agents/deep_portfolio_torch.py new file mode 100644 index 0000000000000000000000000000000000000000..f957a1fafc6f81023efd3b2de883068b5be62580 --- /dev/null +++ b/agents/deep_portfolio_torch.py @@ -0,0 +1,80 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +class SingleAssetProcessor(nn.Module): + def __init__(self, sequence_length, num_features_per_asset, cnn_filters1, cnn_filters2, lstm_units): + super().__init__() + self.conv1 = nn.Conv1d(num_features_per_asset, cnn_filters1, kernel_size=3, padding=1) + self.conv2 = nn.Conv1d(cnn_filters1, cnn_filters2, kernel_size=3, padding=1) + self.lstm = nn.LSTM(input_size=cnn_filters2, hidden_size=lstm_units, batch_first=True) + + def forward(self, x): + # x: (batch, seq_len, num_features_per_asset) + x = x.transpose(1, 2) # (batch, num_features_per_asset, seq_len) + x = F.relu(self.conv1(x)) + x = F.relu(self.conv2(x)) + x = x.transpose(1, 2) # (batch, seq_len, cnn_filters2) + _, (h_n, _) = self.lstm(x) # h_n: (1, batch, lstm_units) + return h_n.squeeze(0) # (batch, lstm_units) + +class DeepPortfolioAgentNetworkTorch(nn.Module): + def __init__(self, num_assets, sequence_length, num_features_per_asset, + asset_cnn_filters1=32, asset_cnn_filters2=64, + asset_lstm_units1=64, asset_lstm_units2=32, + final_dense_units1=128, final_dense_units2=32, + final_dropout=0.3, mha_num_heads=4, mha_key_dim_divisor=2, + output_latent_features=True, use_sentiment_analysis=False): + super().__init__() + self.num_assets = num_assets + self.sequence_length = sequence_length + self.num_features_per_asset = num_features_per_asset + self.output_latent_features = output_latent_features + self.use_sentiment_analysis = use_sentiment_analysis + + # Processador individual de ativos + self.asset_processor = SingleAssetProcessor( + sequence_length, num_features_per_asset, + asset_cnn_filters1, asset_cnn_filters2, asset_lstm_units1 + ) + # Atenção multi-cabeça + self.attention = nn.MultiheadAttention( + embed_dim=asset_lstm_units1, + num_heads=mha_num_heads, + batch_first=True + ) + # Pooling global + self.global_avg_pool = nn.AdaptiveAvgPool1d(1) + # Camadas densas finais + self.dense1 = nn.Linear(asset_lstm_units1, final_dense_units1) + self.dropout1 = nn.Dropout(final_dropout) + self.dense2 = nn.Linear(final_dense_units1, final_dense_units2) + self.dropout2 = nn.Dropout(final_dropout) + self.output_allocation = nn.Linear(final_dense_units2, num_assets) + + def forward(self, x): + # x: (batch, seq_len, num_assets * num_features_per_asset) + batch_size = x.size(0) + # Separar cada ativo + asset_representations = [] + for i in range(self.num_assets): + start = i * self.num_features_per_asset + end = (i + 1) * self.num_features_per_asset + asset_data = x[:, :, start:end] # (batch, seq_len, num_features_per_asset) + asset_repr = self.asset_processor(asset_data) # (batch, lstm_units1) + asset_representations.append(asset_repr) + # Empilhar ativos: (batch, num_assets, lstm_units1) + stacked = torch.stack(asset_representations, dim=1) + # Atenção multi-cabeça + attn_output, _ = self.attention(stacked, stacked, stacked) + # Pooling global sobre ativos (num_assets) + pooled = self.global_avg_pool(attn_output.transpose(1,2)).squeeze(-1) # (batch, lstm_units1) + # Camadas densas finais + x = F.relu(self.dense1(pooled)) + x = self.dropout1(x) + x = F.relu(self.dense2(x)) + x = self.dropout2(x) + if self.output_latent_features: + return x # (batch, final_dense_units2) + else: + return F.softmax(self.output_allocation(x), dim=-1) # (batch, num_assets) diff --git a/agents/financial_data_agent.py b/agents/financial_data_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..aaeba673bdad88f495c79c411e8e0136cac4cb85 --- /dev/null +++ b/agents/financial_data_agent.py @@ -0,0 +1,76 @@ +# agents/financial_data_agent.py +import yfinance as yf +import pandas as pd +from agno.agent import Agent # Commented out +from agno.models.anthropic import Claude # Commented out +from agno.tools.yfinance import YFinanceTools # Commented out + +def get_stock_report(ticker="NVDA"): # Commented out + agent = Agent( + model=Claude(id="claude-3-7-sonnet-latest"), + tools=[ + YFinanceTools( + stock_price=True, + analyst_recommendations=True, + company_info=True, + company_news=True, + ) + ], + instructions=[ + "Use tables to display data", + "Only output the report, no other text", + ], + markdown=True, + ) + return agent.get_response(f"Write a financial report on {ticker}") + +def fetch_historical_ohlcv(ticker_symbol: str, period: str = "1y", interval: str = "1d") -> pd.DataFrame: + """ + Fetches historical OHLCV data for a given ticker symbol. + + Args: + ticker_symbol (str): The stock ticker symbol (e.g., "AAPL" for Apple on NASDAQ, + "PETR4.SA" for Petrobras on B3, "000001.SS" for SSE Composite Index). + period (str): The period for which to download data (e.g., "1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "ytd", "max"). + interval (str): The interval of data points (e.g., "1m", "2m", "5m", "15m", "30m", "60m", "90m", "1h", "1d", "5d", "1wk", "1mo", "3mo"). + + Returns: + pd.DataFrame: A pandas DataFrame containing the OHLCV data, or an empty DataFrame if an error occurs. + """ + try: + ticker = yf.Ticker(ticker_symbol) + data = ticker.history(period=period, interval=interval) + if data.empty: + print(f"No data found for {ticker_symbol} for the given period/interval.") + return pd.DataFrame() + # Ensure column names are consistent (Yahoo Finance sometimes uses 'Adj Close') + data.rename(columns={"Adj Close": "Adj_Close"}, inplace=True) + return data + except Exception as e: + print(f"Error fetching data for {ticker_symbol}: {e}") + return pd.DataFrame() + +if __name__ == '__main__': + # Example usage: + # NASDAQ + aapl_data = fetch_historical_ohlcv("AAPL", period="1mo", interval="1d") + if not aapl_data.empty: + print("\nAAPL Data (NASDAQ):") + print(aapl_data.head()) + + # B3 (Brazilian Stock Exchange) - Example: Petrobras + petr4_data = fetch_historical_ohlcv("PETR4.SA", period="1mo", interval="1d") + if not petr4_data.empty: + print("\nPETR4.SA Data (B3):") + print(petr4_data.head()) + + # Asian Market - Example: Samsung Electronics (Korea Exchange) + samsung_data = fetch_historical_ohlcv("005930.KS", period="1mo", interval="1d") + if not samsung_data.empty: + print("\n005930.KS Data (Samsung - KRX):") + print(samsung_data.head()) + + # Example for a non-existent ticker or error + error_data = fetch_historical_ohlcv("NONEXISTENTTICKER", period="1d") + if error_data.empty: + print("\nSuccessfully handled non-existent ticker.") diff --git a/agents/investment_agent.py b/agents/investment_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..a9c988c317e97114cfc3200d2affa2314bddd16b --- /dev/null +++ b/agents/investment_agent.py @@ -0,0 +1,7 @@ +# agents/investment_agent.py +def execute_investment(prediction, threshold=0.8): + if prediction > threshold: + print("Comprar ativos com probabilidade:", prediction) + # chamada API para execução real + else: + print("Não investir. Probabilidade baixa:", prediction) diff --git a/agents/portfolio_environment.py b/agents/portfolio_environment.py new file mode 100644 index 0000000000000000000000000000000000000000..a94fcbf8d0ba7fc1010e729a37cd8882f5fc0819 --- /dev/null +++ b/agents/portfolio_environment.py @@ -0,0 +1,246 @@ +# agents/portfolio_environment.py (ou atcoin_env.py) + +from typing import List +import numpy as np +import pandas as pd +import gymnasium as gym +from gymnasium import spaces +from collections import deque + +# Importar do config.py +# from ..config import WINDOW_SIZE # Ajuste o import +WINDOW_SIZE_ENV = 60 # Exemplo, pegue do config +NUM_ASSETS=4 +WINDOW_SIZE=60 + +NUM_FEATURES_PER_ASSET=26 + + + + + + +class PortfolioEnv(gym.Env): # Renomeado para seguir convenção de Gymnasium (Opcional) + metadata = {'render_modes': ['human'], 'render_fps': 30} + + def __init__(self, df_multi_asset_features: pd.DataFrame, + asset_symbols_list: List[str], # Lista de chaves dos ativos ex: ['crypto_eth', 'stock_aapl'] + initial_balance=100000, + window_size=WINDOW_SIZE_ENV, + transaction_cost_pct=0.001, + reward_window_size=60, # Janela para cálculo do Sharpe Ratio (ex: 60 passos/horas) + risk_free_rate_per_step=None): # Custo de transação de 0.1% + super(PortfolioEnv, self).__init__() + + self.df = df_multi_asset_features.copy() # DataFrame ACHATADO com todas as features de todos os ativos + self.asset_keys = asset_symbols_list # Usado para identificar colunas de preço de fechamento + self.num_assets = len(asset_symbols_list) + self.initial_balance = initial_balance + self.window_size = window_size + self.transaction_cost_pct = transaction_cost_pct + self.reward_window_size = reward_window_size + # Cálculo automático da taxa livre de risco por passo se não for passada + RISK_FREE_RATE_ANNUAL = 0.02 # 2% ao ano + TRADING_DAYS_PER_YEAR = 252 + HOURS_PER_DAY_TRADING = 24 + if risk_free_rate_per_step is None: + self.risk_free_rate_per_step = RISK_FREE_RATE_ANNUAL / (TRADING_DAYS_PER_YEAR * HOURS_PER_DAY_TRADING) + else: + self.risk_free_rate_per_step = risk_free_rate_per_step + self.portfolio_returns_history = deque(maxlen=self.reward_window_size) # Armazena retorenos do portifólio por passos + + self.current_step = 0 + self.balance = self.initial_balance + self.portfolio_weights = np.full(self.num_assets, 1.0 / self.num_assets if self.num_assets > 0 else 0) # Pesos iniciais iguais + self.portfolio_value = self.initial_balance + self.total_steps = len(self.df) - self.window_size -2 # -1 para ter um next_prices + + # Espaço de Ação: pesos do portfólio para cada ativo (devem somar 1, via Softmax da rede) + # A rede neural vai outputar pesos que somam 1 (softmax). + self.action_space = spaces.Box(low=0, high=1, shape=(NUM_ASSETS,), dtype=np.float32) + + # Espaço de Observação: janela de N features para M ativos (achatado) + # O número de colunas no df é num_assets * num_features_per_asset + num_total_features = self.df.shape[1] + self.observation_space = spaces.Box( + low=-np.inf, high=np.inf, + shape=(WINDOW_SIZE, NUM_ASSETS * NUM_FEATURES_PER_ASSET), + dtype=np.float32 + ) + + + + + self.current_prices_cols = [f"{key}_close" for key in self.asset_keys] # Assumindo que 'close' é uma das features base + # Se você usa 'close_div_atr', então seria f"{key}_close_div_atr" + # É importante ter uma coluna de preço de fechamento *original* (não escalada, não normalizada por ATR) + # para calcular os retornos reais do portfólio. Se não estiver no df, precisará ser adicionada/mantida. + # Por agora, vamos assumir que o df passado já tem as colunas de preço de fechamento originais, + # ou você precisará de um df separado só com os preços para o cálculo de retorno. + # VOU ASSUMIR QUE VOCÊ ADICIONA COLUNAS DE PREÇO DE FECHAMENTO ORIGINAIS AO `df_multi_asset_features` + # com nomes como `eth_orig_close`, `ada_orig_close` etc. + self.orig_close_price_cols = [f"{asset_prefix}_close" for asset_prefix in self.asset_keys] # Ex: 'crypto_eth_close' + + # Verificar se as colunas de preço de fechamento original existem + missing_price_cols = [col for col in self.orig_close_price_cols if col not in self.df.columns] + if missing_price_cols: + raise ValueError(f"Colunas de preço de fechamento original ausentes no DataFrame do ambiente: {missing_price_cols}. " + "Adicione-as ao DataFrame com prefixo do ativo (ex: 'crypto_eth_close').") + + + def _get_observation(self): + # Pega as features da janela atual + # O DataFrame self.df já deve estar achatado e conter TODAS as features de TODOS os ativos + start = self.current_step + end = start + self.window_size + obs = self.df.iloc[start:end].values + return obs.astype(np.float32) + + def _get_current_prices(self): + # Pega os preços de fechamento originais do passo atual para cálculo de retorno + # O índice é window_size - 1 dentro da observação atual, que corresponde a self.current_step + self.window_size -1 no df original. + # Mas para o cálculo de PnL, precisamos do preço no início do step e no final do step. + # Preço no início do step (t) + return self.df[self.orig_close_price_cols].iloc[self.current_step + self.window_size -1].values + + def _get_next_prices(self): + # Preço no final do step (t+1) + return self.df[self.orig_close_price_cols].iloc[self.current_step + self.window_size].values + + def reset(self, seed=None, options=None): # Assinatura atualizada do Gymnasium + super().reset(seed=seed) # Importante para Gymnasium + self.current_step = 0 # Inicia do primeiro ponto onde uma janela completa pode ser formada + self.balance = self.initial_balance + self.portfolio_value = self.initial_balance + self.portfolio_weights = np.full(self.num_assets, 1.0 / self.num_assets if self.num_assets > 0 else 0) + self.portfolio_returns_history.clear() + + observation = self._get_observation() + info = self._get_info() # Informações adicionais (opcional) + return observation, info + + def _calculate_sharpe_ratio(self) -> float: + """Calcula o Sharpe Ratio anualizado a partir do histórico de retornos por passo.""" + if len(self.portfolio_returns_history) < self.reward_window_size / 2: # Precisa de um mínimo de dados + return 0.0 # Ou uma pequena penalidade por não ter histórico suficiente + + returns_array = np.array(self.portfolio_returns_history) + + # Média dos retornos por passo + mean_return_per_step = np.mean(returns_array) + # Desvio padrão dos retornos por passo + std_return_per_step = np.std(returns_array) + + print(f" DEBUG Sharpe: mean_ret_step={mean_return_per_step:.6f}, std_ret_step={std_return_per_step:.6f}, risk_free_step={self.risk_free_rate_per_step:.8f}") + + if std_return_per_step < 1e-9: # Evitar divisão por zero se não houver volatilidade + print(" DEBUG Sharpe: Std dev muito baixo, retornando 0.") + return 0.0 + + # Sharpe Ratio por passo + sharpe_per_step = (mean_return_per_step - self.risk_free_rate_per_step) / std_return_per_step + + # Anualizar o Sharpe Ratio (assumindo passos horários e ~252 dias de negociação * 24 horas) + annualization_factor = np.sqrt(252 * 24) # Ajuste se seu timeframe for diferente + + annualized_sharpe = sharpe_per_step * annualization_factor + print(f" DEBUG Sharpe: sharpe_per_step={sharpe_per_step:.4f}, annualized_sharpe={annualized_sharpe:.4f}") + return annualized_sharpe + + + def step(self, action_weights: np.ndarray): # Ação são os pesos do portfólio + current_portfolio_value_before_rebalance = self.portfolio_value + + # Normalizar pesos da ação se não somarem 1 (saída softmax da rede já deve fazer isso) + + if not np.isclose(np.sum(action_weights), 1.0): + action_weights = action_weights / (np.sum(action_weights) + 1e-9) + action_weights = np.clip(action_weights, 0, 1) # Garantir que os pesos estão entre 0 e 1 # Normaliza + + # Calcular custo de transação para rebalancear + # Valor de cada ativo ANTES do rebalanceamento + current_asset_values = self.portfolio_weights * current_portfolio_value_before_rebalance + # Valor de cada ativo DEPOIS do rebalanceamento (com base nos novos pesos) + target_asset_values = action_weights * current_portfolio_value_before_rebalance # Valor do portfólio ainda não mudou por preço + + # Volume negociado (absoluto) para cada ativo + trade_volume_per_asset = np.abs(target_asset_values - current_asset_values) + total_trade_volume = np.sum(trade_volume_per_asset) + transaction_costs = total_trade_volume * self.transaction_cost_pct + + # Deduzir custos do valor do portfólio + current_portfolio_value_after_costs = current_portfolio_value_before_rebalance - transaction_costs + + # Atualizar os pesos do portfólio + self.portfolio_weights = action_weights + + # Pegar preços atuais (t) e próximos (t+1) + prices_t = self._get_current_prices() + self.current_step += 1 # Avançar para o próximo estado + prices_t_plus_1 = self._get_next_prices() + + # Calcular retornos dos ativos + asset_returns_on_step = (prices_t_plus_1 - prices_t) / (prices_t + 1e-9) + + # Calcular retorno do portfólio neste passo, APÓS custos e com os NOVOS pesos + portfolio_return_on_step = np.sum(self.portfolio_weights * asset_returns_on_step) + + # Atualizar valor do portfólio + self.portfolio_value = current_portfolio_value_after_costs * (1 + portfolio_return_on_step) + + # Adicionar retorno do passo ao histórico + self.portfolio_returns_history.append(portfolio_return_on_step) + + # Calcular Recompensa (Sharpe Ratio) + # Pode ser o Sharpe Ratio incremental ou o Sharpe Ratio da janela inteira + # Para RL, uma recompensa mais frequente é geralmente melhor. + # Usar o retorno do passo como recompensa imediata pode ser mais estável para PPO. + # Ou, podemos dar o Sharpe Ratio da janela como recompensa a cada N passos, ou no final. + # Por agora, vamos usar o retorno do passo como recompensa principal, e o Sharpe pode ser parte do 'info'. + # Se quisermos o Sharpe Ratio *como* recompensa, ele seria calculado aqui. + + # Opção A: Recompensa = Retorno do Passo (mais simples e denso) + # ultima iteração -- reward = portfolio_return_on_step + + #Opção B: Recompensa = Sharpe Ratio da Janela (mais complexo, pode ser esparso se calculado raramente) + # Em PortfolioEnv.step() +# reward = portfolio_return_on_step # Recompensa atual + +# NOVA RECOMPENSA (Opção B da nossa discussão anterior): + REWARD_SCALE_FACTOR_SHARPE = 0.1 # Ou 0.01, experimente + if len(self.portfolio_returns_history) >= self.reward_window_size: + current_sharpe = self._calculate_sharpe_ratio() + reward = np.clip(current_sharpe, -5, 5) * REWARD_SCALE_FACTOR_SHARPE + print(f" Sharpe Ratio Calculado: {current_sharpe:.4f}, Recompensa (Sharpe escalado): {reward:.6f}") + elif len(self.portfolio_returns_history) > 1: + reward = portfolio_return_on_step * 0.1 + print(f" Retorno Simples como Recompensa (escalado): {reward:.6f}") + else: + reward = 0.0 + + terminated = self.current_step >= self.total_steps + truncated = False + + observation = self._get_observation() + info = self._get_info() # Adicionar Sharpe Ratio ao info + + return observation, reward, terminated, truncated, info + + + def _get_info(self): # Opcional, para retornar métricas + current_sharpe = self._calculate_sharpe_ratio() if len(self.portfolio_returns_history) > 1 else 0.0 + return { + "current_step": self.current_step, + "portfolio_value": self.portfolio_value, + "balance": self.balance, # Se você rastrear cash separadamente + "portfolio_weights": self.portfolio_weights.tolist(), + "last_step_return": self.portfolio_returns_history[-1] if self.portfolio_returns_history else 0.0, + "sharpe_ratio_window": current_sharpe + } + + def render(self, mode='human'): + if mode == 'human': + print(f"Step: {self.current_step}, Portfolio Value: {self.portfolio_value:.2f}, Weights: {self.portfolio_weights}") + + def close(self): + pass # Limpar recursos se necessário \ No newline at end of file diff --git a/agents/portfolio_features_extractor_torch.py b/agents/portfolio_features_extractor_torch.py new file mode 100644 index 0000000000000000000000000000000000000000..39c4dd3a51562f6f46ffbb481a7ef3f7ff2b288a --- /dev/null +++ b/agents/portfolio_features_extractor_torch.py @@ -0,0 +1,35 @@ +import torch +import torch.nn as nn +from stable_baselines3.common.torch_layers import BaseFeaturesExtractor +from deep_portfolio_torch import DeepPortfolioAgentNetworkTorch + +class PortfolioFeaturesExtractorTorch(BaseFeaturesExtractor): + def __init__(self, observation_space, features_dim=32, + num_assets=4, sequence_length=60, num_features_per_asset=26, + asset_cnn_filters1=32, asset_cnn_filters2=64, + asset_lstm_units1=64, asset_lstm_units2=32, + final_dense_units1=128, final_dense_units2=32, + final_dropout=0.3, mha_num_heads=4, mha_key_dim_divisor=2, + output_latent_features=True, use_sentiment_analysis=False): + super().__init__(observation_space, features_dim) + self.network = DeepPortfolioAgentNetworkTorch( + num_assets=num_assets, + sequence_length=sequence_length, + num_features_per_asset=num_features_per_asset, + asset_cnn_filters1=asset_cnn_filters1, + asset_cnn_filters2=asset_cnn_filters2, + asset_lstm_units1=asset_lstm_units1, + asset_lstm_units2=asset_lstm_units2, + final_dense_units1=final_dense_units1, + final_dense_units2=final_dense_units2, + final_dropout=final_dropout, + mha_num_heads=mha_num_heads, + mha_key_dim_divisor=mha_key_dim_divisor, + output_latent_features=output_latent_features, + use_sentiment_analysis=use_sentiment_analysis + ) + self._features_dim = features_dim + + def forward(self, observations): + # observations: (batch, seq_len, num_assets * num_features_per_asset) + return self.network(observations) diff --git a/agents/ppo_deep_portfolio_tensorboard/PPO_1/events.out.tfevents.1750287361.verticalagent-X555LPB.89910.0 b/agents/ppo_deep_portfolio_tensorboard/PPO_1/events.out.tfevents.1750287361.verticalagent-X555LPB.89910.0 new file mode 100644 index 0000000000000000000000000000000000000000..31a766e3c1767abe6ca2e6d3e307571e9c4bfd2d --- /dev/null +++ b/agents/ppo_deep_portfolio_tensorboard/PPO_1/events.out.tfevents.1750287361.verticalagent-X555LPB.89910.0 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:416a6f624b3dae44c64d6ad216c0d9c90927c65aa2fffc56dde39d387a66b0d2 +size 83375 diff --git a/agents/ppo_deep_portfolio_tensorboard/PPO_2/events.out.tfevents.1750321811.verticalagent-X555LPB.160180.0 b/agents/ppo_deep_portfolio_tensorboard/PPO_2/events.out.tfevents.1750321811.verticalagent-X555LPB.160180.0 new file mode 100644 index 0000000000000000000000000000000000000000..1601c01aa50a607cc2045a702136e043e773aa4f --- /dev/null +++ b/agents/ppo_deep_portfolio_tensorboard/PPO_2/events.out.tfevents.1750321811.verticalagent-X555LPB.160180.0 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:554a8d430bd4a7a956b2166b588b07e2345511df984019982768a8bb199c83c8 +size 42785 diff --git a/agents/ppo_deep_portfolio_tensorboard/PPO_3/events.out.tfevents.1750336135.verticalagent-X555LPB.200649.0 b/agents/ppo_deep_portfolio_tensorboard/PPO_3/events.out.tfevents.1750336135.verticalagent-X555LPB.200649.0 new file mode 100644 index 0000000000000000000000000000000000000000..7ae3b763a33253d89bed6978ae3520cd80e098d5 --- /dev/null +++ b/agents/ppo_deep_portfolio_tensorboard/PPO_3/events.out.tfevents.1750336135.verticalagent-X555LPB.200649.0 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ee8dfec483892359b26ff8390f513acca25fc47bc447f47f82b2fc5eda2e014 +size 30977 diff --git a/agents/rl_agent.py b/agents/rl_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..6284464b2a6bcc466500b1a6f92879cf774ae702 --- /dev/null +++ b/agents/rl_agent.py @@ -0,0 +1,9 @@ +# agents/rl_agent.py +from stable_baselines3 import PPO +from atcoin_env import TradingEnv # custom env + +def train_rl_model(): + env = TradingEnv() + model = PPO("MlpPolicy", env, verbose=1) + model.learn(total_timesteps=10000) + model.save("models/ppo_trading") diff --git a/agents/train_agent_tfagents.py b/agents/train_agent_tfagents.py new file mode 100644 index 0000000000000000000000000000000000000000..d054af5d63e08d06cee4bf778ef1f6c532c16283 --- /dev/null +++ b/agents/train_agent_tfagents.py @@ -0,0 +1,127 @@ +import tensorflow as tf +import numpy as np +from tf_agents.environments import tf_py_environment, wrappers, gym_wrapper +from tf_agents.agents.ppo import ppo_agent +from tf_agents.networks import actor_distribution_network, value_network +from tf_agents.trajectories import trajectory +from tf_agents.drivers.dynamic_step_driver import DynamicStepDriver +from tf_agents.replay_buffers.tf_uniform_replay_buffer import TFUniformReplayBuffer +from tf_agents.utils import common +from tf_agents.metrics import tf_metrics +from tensorflow.keras.optimizers import Adam + +# Seus módulos +from portfolio_environment import PortfolioEnv # Adaptado do seu código +from DeepPortfolioAgent import DeepPortfolioAgentNetwork + +# --- CONFIGS --- +NUM_ASSETS = 4 +WINDOW_SIZE = 60 +FEATURES_PER_ASSET = 26 +TOTAL_FEATURES = NUM_ASSETS * FEATURES_PER_ASSET +BATCH_SIZE = 64 +REPLAY_BUFFER_CAPACITY = 1000 +LEARNING_RATE = 3e-4 +NUM_ITERATIONS = 200 + +import pandas as pd + +# Mock: gera dados aleatórios para teste +num_steps = 500 +symbols = ["ETH-USD","BTC-USD","ADA-USD","SOL-USD"] +features = ["open", "high", "low", "close", "volume"] +df_data = {} + +for symbol in symbols: + for feat in features: + df_data[f"{symbol}_{feat}"] = np.random.rand(num_steps) + +df_mock = pd.DataFrame(df_data) + + +# --- ENV SETUP --- +py_env = PortfolioEnv(df_mock, asset_symbols_list=symbols) +train_env = tf_py_environment.TFPyEnvironment(gym_wrapper.GymWrapper(py_env)) + +# --- FEATURE EXTRACTOR WRAPPER --- +class FeatureProjector(tf.keras.Model): + def __init__(self): + super().__init__() + self.extractor = DeepPortfolioAgentNetwork( + num_assets=NUM_ASSETS, + sequence_length=WINDOW_SIZE, + num_features_per_asset=FEATURES_PER_ASSET, + asset_cnn_filters1=32, + asset_cnn_filters2=64, + asset_lstm_units1=64, + asset_lstm_units2=32, + mha_num_heads=4, + mha_key_dim_divisor=4, + final_dense_units1=64, + final_dense_units2=32, + final_dropout=0.2, + use_sentiment_analysis=False + ) + + def call(self, observation, training=False): + return self.extractor(observation, training=training) + +# --- NETWORKS --- +feature_combiner = FeatureProjector() + +actor_net = actor_distribution_network.ActorDistributionNetwork( + input_tensor_spec=train_env.observation_spec(), + output_tensor_spec=train_env.action_spec(), + preprocessing_combiner=feature_combiner, + fc_layer_params=() +) + +value_net = value_network.ValueNetwork( + input_tensor_spec=train_env.observation_spec(), + preprocessing_combiner=feature_combiner, + fc_layer_params=() +) + +# --- AGENT --- +optimizer = Adam(learning_rate=LEARNING_RATE) +train_step = tf.Variable(0) +agent = ppo_agent.PPOAgent( + train_env.time_step_spec(), + train_env.action_spec(), + actor_net=actor_net, + value_net=value_net, + optimizer=optimizer, + num_epochs=5, + train_step_counter=train_step +) +agent.initialize() + +# --- REPLAY BUFFER --- +replay_buffer = TFUniformReplayBuffer( + data_spec=agent.collect_data_spec, + batch_size=train_env.batch_size, + max_length=REPLAY_BUFFER_CAPACITY +) + +# --- DRIVER --- +observer = replay_buffer.add_batch +driver = DynamicStepDriver( + env=train_env, + policy=agent.collect_policy, + observers=[observer], + num_steps=1 +) + +# --- METRICS (opcional) --- +avg_return = tf_metrics.AverageReturnMetric() + +# --- TREINAMENTO --- +print("🚦 Iniciando ciclo de treino PPO com TFAgents e DeepPortfolioAgentNetwork...") +for i in range(NUM_ITERATIONS): + driver.run() + experience = replay_buffer.gather_all() + train_loss = agent.train(experience) + replay_buffer.clear() + print(f"Iteração {i+1}/{NUM_ITERATIONS} - Loss: {train_loss.loss:.5f}") + +print("✅ Treinamento finalizado com sucesso!") diff --git a/agents/train_rl_portfolio_agent.py b/agents/train_rl_portfolio_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..973d20bfa58c59d9bf55a1f789e1e6c40db13557 --- /dev/null +++ b/agents/train_rl_portfolio_agent.py @@ -0,0 +1,755 @@ +# train_rl_portfolio_agent.py +from stable_baselines3 import PPO +from stable_baselines3.common.env_checker import check_env +#from transformers import logger + +# rnn/agents/custom_policies.py (NOVO ARQUIVO, ou adicione ao deep_portfolio.py) + +import gymnasium as gym # Usar gymnasium +import tensorflow as tf +from stable_baselines3.common.torch_layers import BaseFeaturesExtractor as PyTorchBaseFeaturesExtractor + +from stable_baselines3.common.torch_layers import MlpExtractor +import torch.nn as nn + +class CustomMlpExtractor(MlpExtractor): + def __init__(self, input_dim, net_arch, activation_fn, device): + super().__init__(input_dim, net_arch, activation_fn, device) + + def forward(self, features): + for layer in self.policy_net: + if isinstance(layer, nn.ReLU): + features = layer(features) # Passando 'features' como argumento + else: + features = layer(features) + return features +# Para TensorFlow, precisamos de um extrator de features compatível ou construir a política de forma diferente. +# Stable Baselines3 tem melhor suporte nativo para PyTorch. Para TF, é um pouco mais manual. +# VAMOS USAR A ABORDAGEM DE POLÍTICA CUSTOMIZADA COM TF DIRETAMENTE. +from stable_baselines3.common.policies import ActorCriticPolicy +from typing import List, Dict, Any, Optional, Union, Type +# Importar sua rede e configs +#import agents.DeepPortfolioAgent as DeepPortfolioAgent +from DeepPortfolioAgent import DeepPortfolioAgentNetwork +# from ..config import (NUM_ASSETS, WINDOW_SIZE, NUM_FEATURES_PER_ASSET, ...) # Importe do seu config real +# VALORES DE EXEMPLO (PEGUE DO SEU CONFIG.PY REAL) +NUM_ASSETS_POLICY = 4 +WINDOW_SIZE_POLICY = 60 +NUM_FEATURES_PER_ASSET_POLICY = 26 +# Hiperparâmetros para DeepPortfolioAgentNetwork quando usada como extrator +ASSET_CNN_FILTERS1_POLICY = 32 +ASSET_CNN_FILTERS2_POLICY = 64 +ASSET_LSTM_UNITS1_POLICY = 64 +ASSET_LSTM_UNITS2_POLICY = 32 # Esta será a dimensão das features latentes para ator/crítico +ASSET_DROPOUT_POLICY = 0.2 +MHA_NUM_HEADS_POLICY = 4 +MHA_KEY_DIM_DIVISOR_POLICY = 2 # Para key_dim = 32 // 2 = 16 +FINAL_DENSE_UNITS1_POLICY = 128 +FINAL_DENSE_UNITS2_POLICY = ASSET_LSTM_UNITS2_POLICY # A saída da dense2 SÃO as features latentes +FINAL_DROPOUT_POLICY = 0.3 + + +class TFPortfolioFeaturesExtractor(tf.keras.layers.Layer): # Herda de tf.keras.layers.Layer + """ + Extrator de features customizado para SB3 que usa DeepPortfolioAgentNetwork. + A observação do ambiente é (batch, window, num_assets * num_features_per_asset). + A saída são as features latentes (batch, latent_dim). + """ + def __init__(self, observation_space: gym.spaces.Box, features_dim: int = ASSET_LSTM_UNITS2_POLICY): + super(TFPortfolioFeaturesExtractor, self).__init__() + self.features_dim = features_dim # SB3 usa isso para saber o tamanho da saída + + # Instanciar a rede base para extrair features + # Ela deve retornar as ativações ANTES da camada softmax de alocação. + self.network = DeepPortfolioAgentNetwork( + num_assets=NUM_ASSETS_POLICY, + sequence_length=WINDOW_SIZE_POLICY, + num_features_per_asset=NUM_FEATURES_PER_ASSET_POLICY, + asset_cnn_filters1=ASSET_CNN_FILTERS1_POLICY, + asset_cnn_filters2=ASSET_CNN_FILTERS2_POLICY, + asset_lstm_units1=ASSET_LSTM_UNITS1_POLICY, + asset_lstm_units2=ASSET_LSTM_UNITS2_POLICY, # Define a saída do asset_processor + asset_dropout=ASSET_DROPOUT_POLICY, + mha_num_heads=MHA_NUM_HEADS_POLICY, + mha_key_dim_divisor=MHA_KEY_DIM_DIVISOR_POLICY, + final_dense_units1=FINAL_DENSE_UNITS1_POLICY, + final_dense_units2=self.features_dim, # A saída da dense2 é a nossa feature latente + final_dropout=FINAL_DROPOUT_POLICY, + output_latent_features=True, + use_sentiment_analysis=True # MUITO IMPORTANTE! + ) + print("TFPortfolioFeaturesExtractor inicializado e usando DeepPortfolioAgentNetwork (output_latent_features=True).") + + def call(self, observations: tf.Tensor, training: bool = False) -> tf.Tensor: + # A DeepPortfolioAgentNetwork já lida com o fatiamento e processamento. + # Ela foi configurada para retornar features latentes. + return self.network(observations, training=training) + + +class CustomPolicy(ActorCriticPolicy): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.mlp_extractor = CustomMlpExtractor( + input_dim=self.observation_space.shape[0], + net_arch=[64, 64], # Exemplo de arquitetura + activation_fn=nn.ReLU, + device=self.device + ) + + + +class CustomPortfolioPolicySB3(ActorCriticPolicy): + def __init__( + self, + observation_space: gym.spaces.Space, + action_space: gym.spaces.Space, + lr_schedule, # Função que retorna a taxa de aprendizado + net_arch: Optional[List[Union[int, Dict[str, List[int]]]]] = None, # Arquitetura para MLPs pós-extrator + activation_fn: Type[tf.Module] = tf.nn.relu, # Usar tf.nn.relu para TF + # Adicionar quaisquer outros parâmetros específicos que o extrator precise + features_extractor_kwargs: Optional[Dict[str, Any]] = None, + **kwargs, + ): + if features_extractor_kwargs is None: + features_extractor_kwargs = {} + + # A dimensão das features que o nosso extrator PortfolioFeatureExtractor vai cuspir. + # Deve ser igual a ASSET_LSTM_UNITS2_POLICY (ou final_dense_units2 do extrator) + # Se não for passado, o construtor do ActorCriticPolicy pode tentar inferir. + # Vamos passar explicitamente para garantir. + features_extractor_kwargs.setdefault("features_dim", ASSET_LSTM_UNITS2_POLICY) # Ou o valor que você definiu + + super().__init__( + observation_space, + action_space, + lr_schedule, + net_arch=net_arch, # Para camadas Dense APÓS o extrator de features + activation_fn=activation_fn, + features_extractor_class=TFPortfolioFeaturesExtractor, + features_extractor_kwargs=features_extractor_kwargs, + **kwargs, + ) + # Otimizador é criado na classe base. + # As redes de ator e crítico são construídas no método _build da classe base, + # usando o self.features_extractor e depois o self.mlp_extractor (que é + # construído com base no net_arch). + + # Não precisamos sobrescrever _build_mlp_extractor se o features_extractor + # já fizer o trabalho pesado e o net_arch padrão para as cabeças for suficiente. + # Se quisermos MLPs customizados para ator e crítico APÓS o extrator: + # def _build_mlp_extractor(self) -> None: + # # self.mlp_extractor é uma instância de MlpExtractor (ou similar) + # # A entrada para ele é self.features_extractor.features_dim + # # Aqui, net_arch definiria a estrutura do mlp_extractor + # self.mlp_extractor = MlpExtractor( + # feature_dim=self.features_extractor.features_dim, + # net_arch=self.net_arch, # net_arch é uma lista de ints para camadas da política e valor + # activation_fn=self.activation_fn, + # device=self.device, + # ) + # As redes de ação e valor (action_net, value_net) são então criadas + # no _build da classe ActorCriticPolicy, no topo do mlp_extractor.# rnn/agents/custom_policies.py (NOVO ARQUIVO, ou adicione ao deep_portfolio.py) + +import gymnasium as gym # Usar gymnasium +import tensorflow as tf +from stable_baselines3.common.torch_layers import BaseFeaturesExtractor as PyTorchBaseFeaturesExtractor + +from stable_baselines3.common.torch_layers import MlpExtractor +import torch.nn as nn + +class CustomMlpExtractor(MlpExtractor): + def __init__(self, input_dim, net_arch, activation_fn, device): + super().__init__(input_dim, net_arch, activation_fn, device) + + def forward(self, features): + for layer in self.policy_net: + if isinstance(layer, nn.ReLU): + features = layer(features) # Passando 'features' como argumento + else: + features = layer(features) + return features +# Para TensorFlow, precisamos de um extrator de features compatível ou construir a política de forma diferente. +# Stable Baselines3 tem melhor suporte nativo para PyTorch. Para TF, é um pouco mais manual. +# VAMOS USAR A ABORDAGEM DE POLÍTICA CUSTOMIZADA COM TF DIRETAMENTE. +from stable_baselines3.common.policies import ActorCriticPolicy +from typing import List, Dict, Any, Optional, Union, Type +# Importar sua rede e configs +#import agents.DeepPortfolioAgent as DeepPortfolioAgent +from DeepPortfolioAgent import DeepPortfolioAgentNetwork +# from ..config import (NUM_ASSETS, WINDOW_SIZE, NUM_FEATURES_PER_ASSET, ...) # Importe do seu config real +# VALORES DE EXEMPLO (PEGUE DO SEU CONFIG.PY REAL) +NUM_ASSETS_POLICY = 4 +WINDOW_SIZE_POLICY = 60 +NUM_FEATURES_PER_ASSET_POLICY = 26 +# Hiperparâmetros para DeepPortfolioAgentNetwork quando usada como extrator +ASSET_CNN_FILTERS1_POLICY = 32 +ASSET_CNN_FILTERS2_POLICY = 64 +ASSET_LSTM_UNITS1_POLICY = 64 +ASSET_LSTM_UNITS2_POLICY = 32 # Esta será a dimensão das features latentes para ator/crítico +ASSET_DROPOUT_POLICY = 0.2 +MHA_NUM_HEADS_POLICY = 4 +MHA_KEY_DIM_DIVISOR_POLICY = 2 # Para key_dim = 32 // 2 = 16 +FINAL_DENSE_UNITS1_POLICY = 128 +FINAL_DENSE_UNITS2_POLICY = ASSET_LSTM_UNITS2_POLICY # A saída da dense2 SÃO as features latentes +FINAL_DROPOUT_POLICY = 0.3 + + +class TFPortfolioFeaturesExtractor(tf.keras.layers.Layer): # Herda de tf.keras.layers.Layer + """ + Extrator de features customizado para SB3 que usa DeepPortfolioAgentNetwork. + A observação do ambiente é (batch, window, num_assets * num_features_per_asset). + A saída são as features latentes (batch, latent_dim). + """ + def __init__(self, observation_space: gym.spaces.Box, features_dim: int = ASSET_LSTM_UNITS2_POLICY): + super(TFPortfolioFeaturesExtractor, self).__init__() + self.features_dim = features_dim # SB3 usa isso para saber o tamanho da saída + + # Instanciar a rede base para extrair features + # Ela deve retornar as ativações ANTES da camada softmax de alocação. + self.network = DeepPortfolioAgentNetwork( + num_assets=NUM_ASSETS_POLICY, + sequence_length=WINDOW_SIZE_POLICY, + num_features_per_asset=NUM_FEATURES_PER_ASSET_POLICY, + asset_cnn_filters1=ASSET_CNN_FILTERS1_POLICY, + asset_cnn_filters2=ASSET_CNN_FILTERS2_POLICY, + asset_lstm_units1=ASSET_LSTM_UNITS1_POLICY, + asset_lstm_units2=ASSET_LSTM_UNITS2_POLICY, # Define a saída do asset_processor + asset_dropout=ASSET_DROPOUT_POLICY, + mha_num_heads=MHA_NUM_HEADS_POLICY, + mha_key_dim_divisor=MHA_KEY_DIM_DIVISOR_POLICY, + final_dense_units1=FINAL_DENSE_UNITS1_POLICY, + final_dense_units2=self.features_dim, # A saída da dense2 é a nossa feature latente + final_dropout=FINAL_DROPOUT_POLICY, + output_latent_features=True, + use_sentiment_analysis=True # MUITO IMPORTANTE! + ) + print("TFPortfolioFeaturesExtractor inicializado e usando DeepPortfolioAgentNetwork (output_latent_features=True).") + + def call(self, observations: tf.Tensor, training: bool = False) -> tf.Tensor: + # A DeepPortfolioAgentNetwork já lida com o fatiamento e processamento. + # Ela foi configurada para retornar features latentes. + return self.network(observations, training=training) + + +class CustomPolicy(ActorCriticPolicy): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.mlp_extractor = CustomMlpExtractor( + input_dim=self.observation_space.shape[0], + net_arch=[64, 64], # Exemplo de arquitetura + activation_fn=nn.ReLU, + device=self.device + ) + + + +class CustomPortfolioPolicySB3(ActorCriticPolicy): + def __init__( + self, + observation_space: gym.spaces.Space, + action_space: gym.spaces.Space, + lr_schedule, # Função que retorna a taxa de aprendizado + net_arch: Optional[List[Union[int, Dict[str, List[int]]]]] = None, # Arquitetura para MLPs pós-extrator + activation_fn: Type[tf.Module] = tf.nn.relu, # Usar tf.nn.relu para TF + # Adicionar quaisquer outros parâmetros específicos que o extrator precise + features_extractor_kwargs: Optional[Dict[str, Any]] = None, + **kwargs, + ): + if features_extractor_kwargs is None: + features_extractor_kwargs = {} + + # A dimensão das features que o nosso extrator PortfolioFeatureExtractor vai cuspir. + # Deve ser igual a ASSET_LSTM_UNITS2_POLICY (ou final_dense_units2 do extrator) + # Se não for passado, o construtor do ActorCriticPolicy pode tentar inferir. + # Vamos passar explicitamente para garantir. + features_extractor_kwargs.setdefault("features_dim", ASSET_LSTM_UNITS2_POLICY) # Ou o valor que você definiu + + super().__init__( + observation_space, + action_space, + lr_schedule, + net_arch=net_arch, # Para camadas Dense APÓS o extrator de features + activation_fn=activation_fn, + features_extractor_class=TFPortfolioFeaturesExtractor, + features_extractor_kwargs=features_extractor_kwargs, + **kwargs, + ) + # Otimizador é criado na classe base. + # As redes de ator e crítico são construídas no método _build da classe base, + # usando o self.features_extractor e depois o self.mlp_extractor (que é + # construído com base no net_arch). + + # Não precisamos sobrescrever _build_mlp_extractor se o features_extractor + # já fizer o trabalho pesado e o net_arch padrão para as cabeças for suficiente. + # Se quisermos MLPs customizados para ator e crítico APÓS o extrator: + # def _build_mlp_extractor(self) -> None: + # # self.mlp_extractor é uma instância de MlpExtractor (ou similar) + # # A entrada para ele é self.features_extractor.features_dim + # # Aqui, net_arch definiria a estrutura do mlp_extractor + # self.mlp_extractor = MlpExtractor( + # feature_dim=self.features_extractor.features_dim, + # net_arch=self.net_arch, # net_arch é uma lista de ints para camadas da política e valor + # activation_fn=self.activation_fn, + # device=self.device, + # ) + # As redes de ação e valor (action_net, value_net) são então criadas + # no _build da classe ActorCriticPolicy, no topo do mlp_extractor.# rnn/agents/custom_policies.py (NOVO ARQUIVO, ou adicione ao deep_portfolio.py) + +import gymnasium as gym # Usar gymnasium +import tensorflow as tf +from stable_baselines3.common.torch_layers import BaseFeaturesExtractor as PyTorchBaseFeaturesExtractor + +from stable_baselines3.common.torch_layers import MlpExtractor +import torch.nn as nn + +class CustomMlpExtractor(MlpExtractor): + def __init__(self, input_dim, net_arch, activation_fn, device): + super().__init__(input_dim, net_arch, activation_fn, device) + + def forward(self, features): + for layer in self.policy_net: + if isinstance(layer, nn.ReLU): + features = layer(features) # Passando 'features' como argumento + else: + features = layer(features) + return features +# Para TensorFlow, precisamos de um extrator de features compatível ou construir a política de forma diferente. +# Stable Baselines3 tem melhor suporte nativo para PyTorch. Para TF, é um pouco mais manual. +# VAMOS USAR A ABORDAGEM DE POLÍTICA CUSTOMIZADA COM TF DIRETAMENTE. +from stable_baselines3.common.policies import ActorCriticPolicy +from typing import List, Dict, Any, Optional, Union, Type +# Importar sua rede e configs +#import agents.DeepPortfolioAgent as DeepPortfolioAgent +from DeepPortfolioAgent import DeepPortfolioAgentNetwork +# from ..config import (NUM_ASSETS, WINDOW_SIZE, NUM_FEATURES_PER_ASSET, ...) # Importe do seu config real +# VALORES DE EXEMPLO (PEGUE DO SEU CONFIG.PY REAL) +NUM_ASSETS_POLICY = 4 +WINDOW_SIZE_POLICY = 60 +NUM_FEATURES_PER_ASSET_POLICY = 26 +# Hiperparâmetros para DeepPortfolioAgentNetwork quando usada como extrator +ASSET_CNN_FILTERS1_POLICY = 32 +ASSET_CNN_FILTERS2_POLICY = 64 +ASSET_LSTM_UNITS1_POLICY = 64 +ASSET_LSTM_UNITS2_POLICY = 32 # Esta será a dimensão das features latentes para ator/crítico +ASSET_DROPOUT_POLICY = 0.2 +MHA_NUM_HEADS_POLICY = 4 +MHA_KEY_DIM_DIVISOR_POLICY = 2 # Para key_dim = 32 // 2 = 16 +FINAL_DENSE_UNITS1_POLICY = 128 +FINAL_DENSE_UNITS2_POLICY = ASSET_LSTM_UNITS2_POLICY # A saída da dense2 SÃO as features latentes +FINAL_DROPOUT_POLICY = 0.3 + + +class TFPortfolioFeaturesExtractor(tf.keras.layers.Layer): # Herda de tf.keras.layers.Layer + """ + Extrator de features customizado para SB3 que usa DeepPortfolioAgentNetwork. + A observação do ambiente é (batch, window, num_assets * num_features_per_asset). + A saída são as features latentes (batch, latent_dim). + """ + def __init__(self, observation_space: gym.spaces.Box, features_dim: int = ASSET_LSTM_UNITS2_POLICY): + super(TFPortfolioFeaturesExtractor, self).__init__() + self.features_dim = features_dim # SB3 usa isso para saber o tamanho da saída + + # Instanciar a rede base para extrair features + # Ela deve retornar as ativações ANTES da camada softmax de alocação. + self.network = DeepPortfolioAgentNetwork( + num_assets=NUM_ASSETS_POLICY, + sequence_length=WINDOW_SIZE_POLICY, + num_features_per_asset=NUM_FEATURES_PER_ASSET_POLICY, + asset_cnn_filters1=ASSET_CNN_FILTERS1_POLICY, + asset_cnn_filters2=ASSET_CNN_FILTERS2_POLICY, + asset_lstm_units1=ASSET_LSTM_UNITS1_POLICY, + asset_lstm_units2=ASSET_LSTM_UNITS2_POLICY, # Define a saída do asset_processor + asset_dropout=ASSET_DROPOUT_POLICY, + mha_num_heads=MHA_NUM_HEADS_POLICY, + mha_key_dim_divisor=MHA_KEY_DIM_DIVISOR_POLICY, + final_dense_units1=FINAL_DENSE_UNITS1_POLICY, + final_dense_units2=self.features_dim, # A saída da dense2 é a nossa feature latente + final_dropout=FINAL_DROPOUT_POLICY, + output_latent_features=True, + use_sentiment_analysis=True # MUITO IMPORTANTE! + ) + print("TFPortfolioFeaturesExtractor inicializado e usando DeepPortfolioAgentNetwork (output_latent_features=True).") + + def call(self, observations: tf.Tensor, training: bool = False) -> tf.Tensor: + # A DeepPortfolioAgentNetwork já lida com o fatiamento e processamento. + # Ela foi configurada para retornar features latentes. + return self.network(observations, training=training) + + +class CustomPolicy(ActorCriticPolicy): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.mlp_extractor = CustomMlpExtractor( + input_dim=self.observation_space.shape[0], + net_arch=[64, 64], # Exemplo de arquitetura + activation_fn=nn.ReLU, + device=self.device + ) + + + +class CustomPortfolioPolicySB3(ActorCriticPolicy): + def __init__( + self, + observation_space: gym.spaces.Space, + action_space: gym.spaces.Space, + lr_schedule, # Função que retorna a taxa de aprendizado + net_arch: Optional[List[Union[int, Dict[str, List[int]]]]] = None, # Arquitetura para MLPs pós-extrator + activation_fn: Type[tf.Module] = tf.nn.relu, # Usar tf.nn.relu para TF + # Adicionar quaisquer outros parâmetros específicos que o extrator precise + features_extractor_kwargs: Optional[Dict[str, Any]] = None, + **kwargs, + ): + if features_extractor_kwargs is None: + features_extractor_kwargs = {} + + # A dimensão das features que o nosso extrator PortfolioFeatureExtractor vai cuspir. + # Deve ser igual a ASSET_LSTM_UNITS2_POLICY (ou final_dense_units2 do extrator) + # Se não for passado, o construtor do ActorCriticPolicy pode tentar inferir. + # Vamos passar explicitamente para garantir. + features_extractor_kwargs.setdefault("features_dim", ASSET_LSTM_UNITS2_POLICY) # Ou o valor que você definiu + + super().__init__( + observation_space, + action_space, + lr_schedule, + net_arch=net_arch, # Para camadas Dense APÓS o extrator de features + activation_fn=activation_fn, + features_extractor_class=TFPortfolioFeaturesExtractor, + features_extractor_kwargs=features_extractor_kwargs, + **kwargs, + ) + # Otimizador é criado na classe base. + # As redes de ator e crítico são construídas no método _build da classe base, + # usando o self.features_extractor e depois o self.mlp_extractor (que é + # construído com base no net_arch). + + # Não precisamos sobrescrever _build_mlp_extractor se o features_extractor + # já fizer o trabalho pesado e o net_arch padrão para as cabeças for suficiente. + # Se quisermos MLPs customizados para ator e crítico APÓS o extrator: + # def _build_mlp_extractor(self) -> None: + # # self.mlp_extractor é uma instância de MlpExtractor (ou similar) + # # A entrada para ele é self.features_extractor.features_dim + # # Aqui, net_arch definiria a estrutura do mlp_extractor + # self.mlp_extractor = MlpExtractor( + # feature_dim=self.features_extractor.features_dim, + # net_arch=self.net_arch, # net_arch é uma lista de ints para camadas da política e valor + # activation_fn=self.activation_fn, + # device=self.device, + # ) + # As redes de ação e valor (action_net, value_net) são então criadas + # no _build da classe ActorCriticPolicy, no topo do mlp_extractor.# rnn/agents/custom_policies.py (NOVO ARQUIVO, ou adicione ao deep_portfolio.py) + +import gymnasium as gym # Usar gymnasium +import tensorflow as tf +from stable_baselines3.common.torch_layers import BaseFeaturesExtractor as PyTorchBaseFeaturesExtractor + +from stable_baselines3.common.torch_layers import MlpExtractor +import torch.nn as nn + +class CustomMlpExtractor(MlpExtractor): + def __init__(self, input_dim, net_arch, activation_fn, device): + super().__init__(input_dim, net_arch, activation_fn, device) + + def forward(self, features): + for layer in self.policy_net: + if isinstance(layer, nn.ReLU): + features = layer(features) # Passando 'features' como argumento + else: + features = layer(features) + return features +# Para TensorFlow, precisamos de um extrator de features compatível ou construir a política de forma diferente. +# Stable Baselines3 tem melhor suporte nativo para PyTorch. Para TF, é um pouco mais manual. +# VAMOS USAR A ABORDAGEM DE POLÍTICA CUSTOMIZADA COM TF DIRETAMENTE. +from stable_baselines3.common.policies import ActorCriticPolicy +from typing import List, Dict, Any, Optional, Union, Type +# Importar sua rede e configs +#import agents.DeepPortfolioAgent as DeepPortfolioAgent +from DeepPortfolioAgent import DeepPortfolioAgentNetwork +# from ..config import (NUM_ASSETS, WINDOW_SIZE, NUM_FEATURES_PER_ASSET, ...) # Importe do seu config real +# VALORES DE EXEMPLO (PEGUE DO SEU CONFIG.PY REAL) +NUM_ASSETS_POLICY = 4 +WINDOW_SIZE_POLICY = 60 +NUM_FEATURES_PER_ASSET_POLICY = 26 +# Hiperparâmetros para DeepPortfolioAgentNetwork quando usada como extrator +ASSET_CNN_FILTERS1_POLICY = 32 +ASSET_CNN_FILTERS2_POLICY = 64 +ASSET_LSTM_UNITS1_POLICY = 64 +ASSET_LSTM_UNITS2_POLICY = 32 # Esta será a dimensão das features latentes para ator/crítico +ASSET_DROPOUT_POLICY = 0.2 +MHA_NUM_HEADS_POLICY = 4 +MHA_KEY_DIM_DIVISOR_POLICY = 2 # Para key_dim = 32 // 2 = 16 +FINAL_DENSE_UNITS1_POLICY = 128 +FINAL_DENSE_UNITS2_POLICY = ASSET_LSTM_UNITS2_POLICY # A saída da dense2 SÃO as features latentes +FINAL_DROPOUT_POLICY = 0.3 + + +class TFPortfolioFeaturesExtractor(tf.keras.layers.Layer): # Herda de tf.keras.layers.Layer + """ + Extrator de features customizado para SB3 que usa DeepPortfolioAgentNetwork. + A observação do ambiente é (batch, window, num_assets * num_features_per_asset). + A saída são as features latentes (batch, latent_dim). + """ + def __init__(self, observation_space: gym.spaces.Box, features_dim: int = ASSET_LSTM_UNITS2_POLICY): + super(TFPortfolioFeaturesExtractor, self).__init__() + self.features_dim = features_dim # SB3 usa isso para saber o tamanho da saída + + # Instanciar a rede base para extrair features + # Ela deve retornar as ativações ANTES da camada softmax de alocação. + self.network = DeepPortfolioAgentNetwork( + num_assets=NUM_ASSETS_POLICY, + sequence_length=WINDOW_SIZE_POLICY, + num_features_per_asset=NUM_FEATURES_PER_ASSET_POLICY, + asset_cnn_filters1=ASSET_CNN_FILTERS1_POLICY, + asset_cnn_filters2=ASSET_CNN_FILTERS2_POLICY, + asset_lstm_units1=ASSET_LSTM_UNITS1_POLICY, + asset_lstm_units2=ASSET_LSTM_UNITS2_POLICY, # Define a saída do asset_processor + asset_dropout=ASSET_DROPOUT_POLICY, + mha_num_heads=MHA_NUM_HEADS_POLICY, + mha_key_dim_divisor=MHA_KEY_DIM_DIVISOR_POLICY, + final_dense_units1=FINAL_DENSE_UNITS1_POLICY, + final_dense_units2=self.features_dim, # A saída da dense2 é a nossa feature latente + final_dropout=FINAL_DROPOUT_POLICY, + output_latent_features=True, + use_sentiment_analysis=True # MUITO IMPORTANTE! + ) + print("TFPortfolioFeaturesExtractor inicializado e usando DeepPortfolioAgentNetwork (output_latent_features=True).") + + def call(self, observations: tf.Tensor, training: bool = False) -> tf.Tensor: + # A DeepPortfolioAgentNetwork já lida com o fatiamento e processamento. + # Ela foi configurada para retornar features latentes. + return self.network(observations, training=training) + + +class CustomPolicy(ActorCriticPolicy): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.mlp_extractor = CustomMlpExtractor( + input_dim=self.observation_space.shape[0], + net_arch=[64, 64], # Exemplo de arquitetura + activation_fn=nn.ReLU, + device=self.device + ) + + + +class CustomPortfolioPolicySB3(ActorCriticPolicy): + def __init__( + self, + observation_space: gym.spaces.Space, + action_space: gym.spaces.Space, + lr_schedule, # Função que retorna a taxa de aprendizado + net_arch: Optional[List[Union[int, Dict[str, List[int]]]]] = None, # Arquitetura para MLPs pós-extrator + activation_fn: Type[tf.Module] = tf.nn.relu, # Usar tf.nn.relu para TF + # Adicionar quaisquer outros parâmetros específicos que o extrator precise + features_extractor_kwargs: Optional[Dict[str, Any]] = None, + **kwargs, + ): + if features_extractor_kwargs is None: + features_extractor_kwargs = {} + + # A dimensão das features que o nosso extrator PortfolioFeatureExtractor vai cuspir. + # Deve ser igual a ASSET_LSTM_UNITS2_POLICY (ou final_dense_units2 do extrator) + # Se não for passado, o construtor do ActorCriticPolicy pode tentar inferir. + # Vamos passar explicitamente para garantir. + features_extractor_kwargs.setdefault("features_dim", ASSET_LSTM_UNITS2_POLICY) # Ou o valor que você definiu + + super().__init__( + observation_space, + action_space, + lr_schedule, + net_arch=net_arch, # Para camadas Dense APÓS o extrator de features + activation_fn=activation_fn, + features_extractor_class=TFPortfolioFeaturesExtractor, + features_extractor_kwargs=features_extractor_kwargs, + **kwargs, + ) + # Otimizador é criado na classe base. + # As redes de ator e crítico são construídas no método _build da classe base, + # usando o self.features_extractor e depois o self.mlp_extractor (que é + # construído com base no net_arch). + + # Não precisamos sobrescrever _build_mlp_extractor se o features_extractor + # já fizer o trabalho pesado e o net_arch padrão para as cabeças for suficiente. + # Se quisermos MLPs customizados para ator e crítico APÓS o extrator: + # def _build_mlp_extractor(self) -> None: + # # self.mlp_extractor é uma instância de MlpExtractor (ou similar) + # # A entrada para ele é self.features_extractor.features_dim + # # Aqui, net_arch definiria a estrutura do mlp_extractor + # self.mlp_extractor = MlpExtractor( + # feature_dim=self.features_extractor.features_dim, + # net_arch=self.net_arch, # net_arch é uma lista de ints para camadas da política e valor + # activation_fn=self.activation_fn, + # device=self.device, + # ) + # As redes de ação e valor (action_net, value_net) são então criadas + # no _build da classe ActorCriticPolicy, no topo do mlp_extractor. + +from custom_policies import CustomPortfolioPolicySB3 # Importar a política customizada +#from .deep_portfolio import NUM_ASSETS_CONF, WINDOW_SIZE_CONF, NUM_FEATURES_PER_ASSET_CONF # Se precisar para policy_kwargs +# (Importe as configs do config.py) +#from ..config import LEARNING_RATE as PPO_LEARNING_RATE +PPO_LEARNING_RATE = 0.0005 + + +from data_handler_multi_asset import get_multi_asset_data_for_rl, MULTI_ASSET_SYMBOLS # Do seu config/data_handler +from portfolio_environment import PortfolioEnv +from deep_portfolio import DeepPortfolioAI # Seu modelo (usado como policy) +# from config import ... # Outras configs + +RISK_FREE_RATE_ANNUAL = 0.2 +REWARD_WINDOW = 252 +frisk_free_per_step = 0.0 + +# Janela de recompensa para Sharpe (ex: últimos 60 passos/horas) +# Deve ser menor ou igual ao ep_len_mean ou um valor razoável +reward_calc_window = 60 + +# 1. Carregar e preparar dados multi-ativos +# (MULTI_ASSET_SYMBOLS viria do config.py) +asset_keys_list = list(MULTI_ASSET_SYMBOLS.keys()) # ['crypto_eth', 'crypto_ada', ...] + +multi_asset_df = get_multi_asset_data_for_rl( + MULTI_ASSET_SYMBOLS, + timeframe_yf='1h', # Ou TIMEFRAME_YFINANCE do config + days_to_fetch=365*2, + logger_instance=any + # Ou DAYS_TO_FETCH do config +) + +print("Imprimindo retorno para df_combined passado para train_rl_portifolio") +print(multi_asset_df) + + +#------------------- + +if multi_asset_df is None or multi_asset_df.empty: + print("Falha ao carregar dados multi-ativos. Encerrando treinamento RL.") + exit() + +env = PortfolioEnv(df_multi_asset_features=multi_asset_df, asset_symbols_list=asset_keys_list) +print("Ambiente de Portfólio Criado.") + +# --- Usar a Política Customizada --- +# Hiperparâmetros para o PPO +learning_rate_ppo = PPO_LEARNING_RATE # Ex: 3e-4 ou 1e-4 (do config.py) +n_steps_ppo = 2048 +batch_size_ppo = 64 +ent_coef_ppo = 0.01 + +# policy_kwargs para passar para o __init__ da CustomPortfolioPolicySB3, se necessário +# (além dos que já são passados para o features_extractor_kwargs) +# Exemplo: Se você adicionou mais args ao __init__ de CustomPortfolioPolicySB3 +# policy_custom_kwargs = dict( +# meu_parametro_customizado=valor, +# # features_extractor_kwargs já é tratado pela classe base se você passar features_extractor_class +# ) + +print("Instanciando PPO com Política Customizada (DeepPortfolioAgentNetwork)...") +model_ppo = PPO( + CustomPortfolioPolicySB3, + env, + verbose=1, + learning_rate=learning_rate_ppo, # Pode ser uma função lr_schedule + n_steps=n_steps_ppo, + batch_size=batch_size_ppo, + ent_coef=ent_coef_ppo, + # policy_kwargs=policy_custom_kwargs, # Se tiver kwargs específicos para a política + tensorboard_log="./ppo_deep_portfolio_tensorboard/" +) + +print("Iniciando treinamento do agente PPO com rede customizada...") +model_ppo.learn(total_timesteps=1000000, progress_bar=True) # Comece com menos timesteps para teste (ex: 50k) + +model_ppo.save("rl_models/ppo_custom_deep_portfolio_agent") +print("Modelo RL com política customizada treinado e salvo.") + +#---------- + + + + +# if multi_asset_df is None or multi_asset_df.empty: +# print("Falha ao carregar dados multi-ativos. Encerrando treinamento RL.") +# exit() + +# # 2. Criar o Ambiente +# # O multi_asset_df já deve ter as features para observação E as colunas de preço de close original +# env = PortfolioEnv(df_multi_asset_features=multi_asset_df, asset_symbols_list=asset_keys_list) + + +# risk_free_per_step = 0.0 + +# # Janela de recompensa para Sharpe (ex: últimos 60 passos/horas) +# # Deve ser menor ou igual ao ep_len_mean ou um valor razoável +# reward_calc_window = 60 + +# env = PortfolioEnv( +# df_multi_asset_features=multi_asset_df, +# asset_symbols_list=asset_keys_list, +# initial_balance=100000, # Do config +# window_size=60, # Do config +# transaction_cost_pct=0.001, # Do config ou defina aqui +# reward_window_size=reward_calc_window, +# risk_free_rate_per_step=risk_free_per_step +# ) + +# # Opcional: Verificar se o ambiente está em conformidade com a API do Gymnasium +# # check_env(env) # Pode dar avisos/erros se algo estiver errado +# print("Ambiente de Portfólio Criado.") +# print(f"Observation Space: {env.observation_space.shape}") +# print(f"Action Space: {env.action_space.shape}") + + + + +# # 3. Definir a Política de Rede Neural +# # Stable-Baselines3 permite que você defina uma arquitetura customizada. +# # Precisamos de uma forma de passar sua arquitetura DeepPortfolioAI para o PPO. +# # Uma maneira é criar uma classe de política customizada. +# # Por agora, vamos usar a política padrão "MlpPolicy" e depois vemos como integrar a sua. +# # Ou, se DeepPortfolioAI for uma tf.keras.Model, podemos tentar usá-la em policy_kwargs. + +# # Para usar sua DeepPortfolioAI, você precisaria de uma FeatureExtractor customizada +# # ou uma política que a incorpore, o que é mais avançado com Stable-Baselines3. +# # Vamos começar com MlpPolicy para testar o ambiente. + +# # policy_kwargs = dict( +# # features_extractor_class=YourCustomFeatureExtractor, # Se a entrada precisar de tratamento especial +# # features_extractor_kwargs=dict(features_dim=128), +# # net_arch=[dict(pi=[256, 128], vf=[256, 128])] # Exemplo de arquitetura para policy e value networks +# # ) +# # Ou, se o DeepPortfolioAI puder ser adaptado para ser a policy_network: +# policy_kwargs = dict( +# net_arch=dict( +# pi=[{'model': DeepPortfolioAI(num_assets=env.num_assets)}], # Não é direto assim +# vf=[] # Value function pode ser separada ou compartilhada +# ) +# ) + +# # Para começar e testar o ambiente, use a MlpPolicy padrão. +# # O input da MlpPolicy será a observação achatada (WINDOW_SIZE * num_total_features). +# # Isso pode não ser ideal para dados sequenciais. "MlpLstmPolicy" é melhor. + +# model_ppo = PPO("MlpPolicy", env, verbose=1, ent_coef=0.01, tensorboard_log="./ppo_portfolio_tensorboard/") +# # Se "MlpLstmPolicy" não funcionar bem com o shape da observação (janela, features_totais), +# # você pode precisar de um FeatureExtractor que achate a janela, ou uma política customizada. + +# # 4. Treinar o Agente +# print("Iniciando treinamento do agente PPO...") +# model_ppo.learn(total_timesteps=int("1000000"), progress_bar=True) # Aumente timesteps para treino real + +# # 5. Salvar o Modelo Treinado +# model_ppo.save("rl_models/ppo_deep_portfolio_agent") +# print("Modelo RL treinado salvo.") + + +# # (Opcional) Testar o agente treinado +# obs, _ = env.reset() +# for _ in range(200): +# action, _states = model_ppo.predict(obs, deterministic=True) +# obs, rewards, terminated, truncated, info = env.step(action) +# env.render() +# if terminated or truncated: +# obs, _ = env.reset() +# env.close() \ No newline at end of file diff --git a/api/main.py b/api/main.py new file mode 100644 index 0000000000000000000000000000000000000000..408bf38bd12aede8e3947f1d7428448c0f4ac57e --- /dev/null +++ b/api/main.py @@ -0,0 +1,61 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from endpoints.predict import router as predict_router +from agents.dataset_update_agent import router as dataset_router +from agents.financial_data_agent import router as finance_router +from agents.investment_agent import router as invest_router +from agents.rl_agent import router as rl_router +from utils.logger import get_logger +from fastapi import FastAPI, Request +from fastapi.responses import HTMLResponse +from fastapi.staticfiles import StaticFiles +from jinja2 import Environment, FileSystemLoader +import os + + + + +logger = get_logger() + +app = FastAPI(title="ATCoin Neural Agents") + +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:3000"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Registrar rotas +app.include_router(predict_router) +app.include_router(dataset_router, prefix="/dataset") +app.include_router(finance_router, prefix="/finance") +app.include_router(invest_router, prefix="/invest") +app.include_router(rl_router, prefix="/rl") + + + + +@app.get("/", response_class=HTMLResponse) +async def index(): + agentes = [ + { + "nome": "Captura de Dados", + "status": "Ativo", + "ultima_acao": "08/05/2025 10:00", + "proxima_execucao": "09/05/2025 10:00", + "resultado": "12 ativos atualizados" + }, + { + "nome": "PPO Trainer", + "status": "Treinando", + "ultima_acao": "08/05/2025 12:00", + "proxima_execucao": "09/05/2025 12:00", + "resultado": "Reward: 0.89" + }, + # outros agentes... + ] + + template = templates.get_template("index.html") + return HTMLResponse(template.render(agentes=agentes)) \ No newline at end of file diff --git a/app-modularizado.py b/app-modularizado.py new file mode 100644 index 0000000000000000000000000000000000000000..25196e0ed58a8d1300ae3200ef0c1aaa452de1ba --- /dev/null +++ b/app-modularizado.py @@ -0,0 +1,606 @@ +# rnn/app.py + +import os +import uuid +import time +import hmac +import hashlib +import json +from datetime import datetime, timedelta +from typing import Dict, Any +from rnn.app.ccxt_utils import get_ccxt_exchange, fetch_crypto_data + +import httpx # Para fazer chamadas HTTP assíncronas (para o callback) +from fastapi import FastAPI, Request, HTTPException, Depends, Header, BackgroundTasks +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from jinja2 import Environment, FileSystemLoader +from pydantic import BaseModel, Field + +from rnn.app.utils.logger import get_logger + + + +logger = get_logger() + +# --- Configuração Inicial e Variáveis de Ambiente (Secrets do Hugging Face) --- +AIBANK_API_KEY = os.environ.get("AIBANK_API_KEY") # Chave que o aibank usa para chamar esta API RNN +AIBANK_CALLBACK_URL = os.environ.get("AIBANK_CALLBACK_URL") # URL no aibank para onde esta API RNN enviará o resultado +CALLBACK_SHARED_SECRET = os.environ.get("CALLBACK_SHARED_SECRET") # Segredo para assinar/verificar o payload do callback + +# Chaves para serviços externos +MARKET_DATA_API_KEY = os.environ.get("MARKET_DATA_API_KEY") +EXCHANGE_API_KEY = os.environ.get("EXCHANGE_API_KEY") +EXCHANGE_API_SECRET = os.environ.get("EXCHANGE_API_SECRET") + +if not AIBANK_API_KEY: + logger.warning("AIBANK_API_KEY não configurada. A autenticação para /api/invest falhou.") +if not AIBANK_CALLBACK_URL: + logger.warning("AIBANK_CALLBACK_URL não configurada. O callback para o aibank falhou.") +if not CALLBACK_SHARED_SECRET: + logger.warning("CALLBACK_SHARED_SECRET não configurado. A segurança do callback está comprometida.") + + + + +app = FastAPI(title="ATCoin Neural Agents - Investment API") + +# --- Middlewares --- +app.add_middleware( + CORSMiddleware, + allow_origins=[ + "http://localhost:3000", # URL desenvolvimento local + "http://aibank.app.br", # URL de produção + "https://*.aibank.app.br", # subdomínios + "https://*.hf.space" # HF Space + ], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# --- Simulação de Banco de Dados de Transações DEV --- +# Em produção MongoDB +transactions_db: Dict[str, Dict[str, Any]] = {} + +# --- Modelos Pydantic --- +class InvestmentRequest(BaseModel): + client_id: str + amount: float = Field(..., gt=0) # Garante que o montante seja positivo + aibank_transaction_token: str # Token único gerado pelo aibank para rastreamento + +class InvestmentResponse(BaseModel): + status: str + message: str + rnn_transaction_id: str # ID da transação this.API + +class InvestmentResultPayload(BaseModel): # Payload para o callback para o aibank + rnn_transaction_id: str + aibank_transaction_token: str + client_id: str + initial_amount: float + final_amount: float + profit_loss: float + status: str # "completed", "failed" + timestamp: datetime + details: str = "" + + +# --- Dependência de Autenticação --- +async def verify_aibank_key(authorization: str = Header(None)): + if not AIBANK_API_KEY: # Checagem se a chave do servidor está configurada + logger.error("CRITICAL: AIBANK_API_KEY (server-side) não está configurada nos Secrets.") + raise HTTPException(status_code=500, detail="Internal Server Configuration Error: Missing server API Key.") + + if authorization is None: + logger.warning("Authorization header ausente na chamada do AIBank.") + raise HTTPException(status_code=401, detail="Authorization header is missing") + + parts = authorization.split() + if len(parts) != 2 or parts[0].lower() != 'bearer': + logger.warning(f"Formato inválido do Authorization header: {authorization}") + raise HTTPException(status_code=401, detail="Authorization header must be 'Bearer '") + + token_from_aibank = parts[1] + if not hmac.compare_digest(token_from_aibank, AIBANK_API_KEY): + logger.warning(f"Chave de API inválida fornecida pelo AIBank. Token: {token_from_aibank[:10]}...") + raise HTTPException(status_code=403, detail="Invalid API Key provided by AIBank.") + logger.info("API Key do AIBank verificada com sucesso.") + return True + + +# --- Lógica de Negócio Principal (Simulada e em Background) --- + + +async def execute_investment_strategy_background( + rnn_tx_id: str, + client_id: str, + amount: float, + aibank_tx_token: str +): + logger.info(f"BG TASK [{rnn_tx_id}]: Iniciando estratégia de investimento para cliente {client_id}, valor {amount}.") + transactions_db[rnn_tx_id]["status"] = "processing" + transactions_db[rnn_tx_id]["status_details"] = "Initializing investment cycle" + + final_status = "completed" + error_details = "" # Acumula mensagens de erro de várias etapas + calculated_final_amount = amount + + # Inicializa a exchange ccxt usando o utilitário + # O logger do app.py é passado para ccxt_utils para que os logs apareçam no mesmo stream + exchange = await get_ccxt_exchange(logger_instance=logger) # MODIFICADO + + if not exchange: + # get_ccxt_exchange já loga o erro. Se a exchange é crucial, podemos falhar aqui. + logger.warning(f"BG TASK [{rnn_tx_id}]: Falha ao inicializar a exchange. A estratégia pode não funcionar como esperado para cripto.") + # Se as chaves CCXT foram fornecidas no ambiente mas a exchange falhou, considere isso um erro de config. + if os.environ.get("CCXT_API_KEY") and os.environ.get("CCXT_API_SECRET"): + error_details += "Failed to initialize CCXT exchange despite API keys being present; " + final_status = "failed_config" + # (PULAR PARA CALLBACK - veja a seção de tratamento de erro crítico abaixo) + + # ========================================================================= + # 1. COLETAR DADOS DE MERCADO + # ========================================================================= + logger.info(f"BG TASK [{rnn_tx_id}]: Coletando dados de mercado...") + transactions_db[rnn_tx_id]["status_details"] = "Fetching market data" + market_data_results = {"crypto": {}, "stocks": {}, "other": {}} + critical_data_fetch_failed = False # Flag para falha crítica na coleta de dados + + # --- Coleta de dados de Cripto via ccxt_utils --- + if exchange: + crypto_pairs_to_fetch = ["BTC/USDT", "ETH/USDT", "SOL/USDT"] # Mantenha configurável + + crypto_data, crypto_fetch_ok, crypto_err_msg = await fetch_crypto_data( + exchange, + crypto_pairs_to_fetch, + logger_instance=logger + ) + market_data_results["crypto"] = crypto_data + if not crypto_fetch_ok: + error_details += f"Crypto data fetch issues: {crypto_err_msg}; " + # Decida se a falha na coleta de cripto é crítica + # Se for, defina critical_data_fetch_failed = True + if os.environ.get("CCXT_API_KEY"): # Se esperávamos dados de cripto + critical_data_fetch_failed = True + logger.error(f"BG TASK [{rnn_tx_id}]: Falha crítica na coleta de dados de cripto.") + else: + logger.info(f"BG TASK [{rnn_tx_id}]: Instância da exchange ccxt não disponível. Pulando coleta de dados de cripto.") + if os.environ.get("CCXT_API_KEY"): # Se esperávamos dados de cripto mas a exchange não inicializou + error_details += "CCXT exchange not initialized, crypto data skipped; " + critical_data_fetch_failed = True + + + # --- Coleta de dados para outros tipos de ativos (ex: Ações com yfinance) --- + # (Sua lógica yfinance aqui, se aplicável, similarmente atualizando market_data_results["stocks"]) + # try: + # import yfinance as yf # Mova para o topo do app.py se for usar + # # ... lógica yfinance ... + # except Exception as e_yf: + # logger.warning(f"BG TASK [{rnn_tx_id}]: Falha ao buscar dados de ações com yfinance: {e_yf}") + # error_details += f"YFinance data fetch failed: {str(e_yf)}; " + # # Decida se isso é crítico: critical_data_fetch_failed = True + + market_data_results["other"]['simulated_index_level'] = random.uniform(10000, 15000) # Mantém simulação + + transactions_db[rnn_tx_id]["market_data_collected"] = market_data_results + + # --- PONTO DE CHECAGEM PARA FALHA CRÍTICA NA COLETA DE DADOS --- + if critical_data_fetch_failed: + final_status = "failed_market_data" + logger.error(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado falhou criticamente. {error_details}") + # Pular para a seção de callback + # (A lógica de envio do callback precisa ser alcançada) + else: + logger.info(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado concluída.") + transactions_db[rnn_tx_id]["status_details"] = "Processing RNN analysis" + + + # ========================================================================= + # 2. ANÁLISE PELA RNN E TOMADA DE DECISÃO (Só executa se a coleta de dados não falhou criticamente) + # ========================================================================= + investment_decisions: List[Dict[str, Any]] = [] + total_usd_allocated_by_rnn = 0.0 + + if final_status == "completed": # Prossiga apenas se não houve erro crítico anterior + logger.info(f"BG TASK [{rnn_tx_id}]: Executando análise RNN...") + transactions_db[rnn_tx_id]["status_details"] = "Running RNN model" + rnn_analysis_success = True + try: + # Placeholder para a LÓGICA REAL DA RNN (RNN_LOGIC_PLACEHOLDER) + # Esta seção precisa ser preenchida com: + # 1. Carregamento do seu modelo RNN treinado (ex: TensorFlow, PyTorch, scikit-learn). + # O modelo pode ser carregado uma vez quando a API inicia, ou sob demanda. + # Se for grande, carregue na inicialização da API. + # Ex: from rnn.models.my_rnn_predictor import MyRNNModel + # if 'rnn_model_instance' not in app.state: # Carregar uma vez + # app.state.rnn_model_instance = MyRNNModel(path_to_model_weights) + # predictor = app.state.rnn_model_instance + # + # 2. Pré-processamento dos `market_data_results` para o formato que sua RNN espera. + # Ex: features = preprocess_market_data_for_rnn(market_data_results) + # + # 3. Inferência/Predição com a RNN. + # Ex: raw_rnn_output = await predictor.predict_async(features, client_profile_if_any) + # + # 4. Pós-processamento do output da RNN para gerar `investment_decisions` no formato: + # [{ "asset_id": "BTC/USDT", "type": "CRYPTO", "action": "BUY", + # "target_usd_amount": 5000.00, "confidence_score": 0.85, + # "stop_loss_price": 60000, "take_profit_price": 75000, + # "reasoning": "RNN output details..." }, ...] + + # Simulação atual: + await asyncio.sleep(random.uniform(5, 10)) # Simula processamento da RNN + if market_data_results.get("crypto"): + if random.random() > 0.2: # 80% chance + num_crypto_to_invest = min(random.randint(1, 2), len(market_data_results["crypto"])) + available_crypto_keys = [k for k,v in market_data_results["crypto"].items() if not v.get("error")] + if available_crypto_keys: + chosen_pairs_keys = random.sample(available_crypto_keys, k=min(num_crypto_to_invest, len(available_crypto_keys))) + + base_allocation_per_asset = amount / len(chosen_pairs_keys) if chosen_pairs_keys else 0 + + for crypto_key in chosen_pairs_keys: + asset_symbol = crypto_key.replace("_", "/") + # Alocação mais realista: não necessariamente usar todo o 'amount' + # E a RNN pode dar pesos diferentes + asset_specific_allocation_factor = random.uniform(0.3, 0.7) / len(chosen_pairs_keys) + allocated_amount = amount * asset_specific_allocation_factor + + # Evitar alocar mais do que o 'amount' total disponível + if total_usd_allocated_by_rnn + allocated_amount > amount * 0.95: # Deixa uma margem + allocated_amount = max(0, (amount * 0.95) - total_usd_allocated_by_rnn) + + if allocated_amount > 10: # Investimento mínimo de $10 (exemplo) + investment_decisions.append({ + "asset_id": asset_symbol, "type": "CRYPTO", "action": "BUY", + "target_usd_amount": round(allocated_amount, 2), + "reasoning": f"Simulated RNN signal for {asset_symbol}" + }) + total_usd_allocated_by_rnn += round(allocated_amount, 2) + + if not investment_decisions: + logger.info(f"BG TASK [{rnn_tx_id}]: RNN não gerou decisões de COMPRA.") + else: + logger.info(f"BG TASK [{rnn_tx_id}]: RNN gerou {len(investment_decisions)} decisões de compra: {investment_decisions}") + + except Exception as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante análise RNN: {str(e)}", exc_info=True) + rnn_analysis_success = False + error_details += f"RNN analysis failed: {str(e)}; " + + if not rnn_analysis_success: + final_status = "failed_rnn_analysis" + + transactions_db[rnn_tx_id]["rnn_decisions"] = investment_decisions + + # Atualiza o total_usd_allocated_by_rnn com base nas decisões finais + total_usd_allocated_by_rnn = sum(d['target_usd_amount'] for d in investment_decisions if d['action'] == 'BUY') + transactions_db[rnn_tx_id]["status_details"] = "Preparing to execute orders" + + + # ========================================================================= + # 3. EXECUÇÃO DE ORDENS (Só executa se a RNN não falhou e gerou ordens) + # ========================================================================= + executed_trades_info: List[Dict[str, Any]] = [] + current_portfolio_value = 0.0 # Valor dos ativos comprados, baseado no custo + cash_remaining_after_execution = amount # Começa com todo o montante + + if final_status == "completed" and investment_decisions and exchange: + logger.info(f"BG TASK [{rnn_tx_id}]: Executando {len(investment_decisions)} ordens...") + transactions_db[rnn_tx_id]["status_details"] = "Executing investment orders" + order_execution_overall_success = True + + # Placeholder para LÓGICA REAL DE EXECUÇÃO DE ORDENS (CREATE_ORDER_PLACEHOLDER) + # Esta seção precisa ser preenchida com: + # 1. Iterar sobre `investment_decisions`. + # 2. Para cada decisão de "BUY": + # a. Determinar o símbolo correto na exchange (ex: "BTC/USDT"). + # b. Obter o preço atual (ticker) para calcular a quantidade de ativo a comprar. + # `amount_of_asset = target_usd_amount / current_price_of_asset` + # c. Considerar saldo disponível na exchange (se estiver gerenciando isso). + # d. Criar a ordem via `await exchange.create_market_buy_order(symbol, amount_of_asset)` + # ou `create_limit_buy_order(symbol, amount_of_asset, limit_price)`. + # Para ordens limite, a RNN precisaria fornecer o `limit_price`. + # e. Tratar respostas da exchange (sucesso, falha, ID da ordem). + # `ccxt.InsufficientFunds`, `ccxt.InvalidOrder`, etc. + # f. Armazenar detalhes da ordem em `executed_trades_info`: + # { "asset_id": ..., "order_id_exchange": ..., "type": "market/limit", "side": "buy", + # "requested_usd_amount": ..., "asset_quantity_ordered": ..., + # "status_from_exchange": ..., "filled_quantity": ..., "average_fill_price": ..., + # "cost_in_usd": ..., "fees_paid": ..., "timestamp": ... } + # g. Atualizar `current_portfolio_value` com o `cost_in_usd` da ordem preenchida. + # h. Deduzir `cost_in_usd` de `cash_remaining_after_execution`. + # 3. Para decisões de "SELL" (se sua RNN gerar): + # a. Verificar se você possui o ativo (requer gerenciamento de portfólio). + # b. Criar ordem de venda. + # c. Atualizar `current_portfolio_value` e `cash_remaining_after_execution`. + + # Simulação atual: + for decision in investment_decisions: + if decision.get("action") == "BUY" and decision.get("type") == "CRYPTO": + asset_symbol = decision["asset_id"] + usd_to_spend = decision["target_usd_amount"] + + # Simular pequena chance de falha na ordem + if random.random() < 0.05: + logger.warning(f"BG TASK [{rnn_tx_id}]: Falha simulada ao executar ordem para {asset_symbol}.") + executed_trades_info.append({ + "asset_id": asset_symbol, "status": "failed_simulated", + "requested_usd_amount": usd_to_spend, "error": "Simulated exchange rejection" + }) + order_execution_overall_success = False # Marca que pelo menos uma falhou + continue # Pula para a próxima decisão + + # Simular slippage e custo + simulated_cost = usd_to_spend * random.uniform(0.995, 1.005) # +/- 0.5% slippage + + # Garantir que não estamos gastando mais do que o caixa restante + if simulated_cost > cash_remaining_after_execution: + simulated_cost = cash_remaining_after_execution # Gasta apenas o que tem + if simulated_cost < 1: # Se não há quase nada, não faz a ordem + logger.info(f"BG TASK [{rnn_tx_id}]: Saldo insuficiente ({cash_remaining_after_execution:.2f}) para ordem de {asset_symbol}, pulando.") + continue + + + if simulated_cost > 0: + current_portfolio_value += simulated_cost + cash_remaining_after_execution -= simulated_cost + executed_trades_info.append({ + "asset_id": asset_symbol, "order_id_exchange": f"sim_ord_{uuid.uuid4()}", + "type": "market", "side": "buy", + "requested_usd_amount": usd_to_spend, + "status_from_exchange": "filled", "cost_in_usd": round(simulated_cost, 2), + "timestamp": datetime.utcnow().isoformat() + }) + logger.info(f"BG TASK [{rnn_tx_id}]: Ordem simulada para {asset_symbol} (custo: {simulated_cost:.2f} USD) preenchida.") + + await asyncio.sleep(random.uniform(1, 2) * len(investment_decisions) if investment_decisions else 1) + + if not order_execution_overall_success: + error_details += "One or more orders failed during execution; " + # Decida se isso torna o status final 'failed_order_execution' ou se 'completed_with_partial_failure' + # final_status = "completed_with_partial_failure" # Exemplo de um novo status + + elif not exchange and investment_decisions: + logger.warning(f"BG TASK [{rnn_tx_id}]: Decisões de investimento geradas, mas a exchange não está disponível para execução.") + error_details += "Exchange not available for order execution; " + final_status = "failed_order_execution" # Se a execução é crítica + cash_remaining_after_execution = amount # Nada foi gasto + + transactions_db[rnn_tx_id]["executed_trades"] = executed_trades_info + transactions_db[rnn_tx_id]["cash_after_execution"] = round(cash_remaining_after_execution, 2) + transactions_db[rnn_tx_id]["portfolio_value_after_execution"] = round(current_portfolio_value, 2) + + + # ========================================================================= + # 4. SIMULAÇÃO DO PERÍODO DE INVESTIMENTO E CÁLCULO DE LUCRO/PERDA (Só se não houve falha crítica antes) + # ========================================================================= + value_of_investments_at_eod = current_portfolio_value # Começa com o valor de custo + + if final_status == "completed": # Ou "completed_with_partial_failure" + transactions_db[rnn_tx_id]["status_details"] = "Simulating EOD valuation" + logger.info(f"BG TASK [{rnn_tx_id}]: Simulando valorização do portfólio no final do dia...") + await asyncio.sleep(random.uniform(3, 7)) + + if current_portfolio_value > 0: + # Simular mudança de valor do portfólio. A meta de 4.2% é sobre o capital INVESTIDO. + # O lucro/perda é aplicado ao `current_portfolio_value` (o que foi efetivamente comprado). + daily_return_factor = 0.042 # A meta + simulated_performance_factor = random.uniform(0.7, 1.3) # Variação em torno da meta (pode ser prejuízo) + # Para ser mais realista, o fator de performance deveria ser algo como: + # random.uniform(-0.05, 0.08) -> -5% a +8% de retorno diário sobre o investido (ainda alto) + # E não diretamente ligado à meta de 4.2% + + # Ajuste para uma simulação de retorno mais plausível (ainda agressiva) + # Suponha que o retorno diário real possa variar de -3% a +5% sobre o investido + actual_daily_return_on_portfolio = random.uniform(-0.03, 0.05) + + profit_or_loss_on_portfolio = current_portfolio_value * actual_daily_return_on_portfolio + value_of_investments_at_eod = current_portfolio_value + profit_or_loss_on_portfolio + logger.info(f"BG TASK [{rnn_tx_id}]: Portfólio inicial: {current_portfolio_value:.2f}, Retorno simulado: {actual_daily_return_on_portfolio*100:.2f}%, " + f"Lucro/Prejuízo no portfólio: {profit_or_loss_on_portfolio:.2f}, Valor EOD do portfólio: {value_of_investments_at_eod:.2f}") + else: + logger.info(f"BG TASK [{rnn_tx_id}]: Nenhum portfólio para valorizar no EOD (nada foi comprado).") + value_of_investments_at_eod = 0.0 + + # O calculated_final_amount é o valor dos investimentos liquidados + o caixa que não foi usado + calculated_final_amount = value_of_investments_at_eod + cash_remaining_after_execution + + else: # Se houve falha antes, o valor final é o que sobrou após a falha + calculated_final_amount = cash_remaining_after_execution + current_portfolio_value # current_portfolio_value pode ser 0 ou parcial + logger.warning(f"BG TASK [{rnn_tx_id}]: Ciclo de investimento não concluído normalmente ({final_status}). Valor final baseado no estado atual.") + + transactions_db[rnn_tx_id]["eod_portfolio_value_simulated"] = round(value_of_investments_at_eod, 2) + transactions_db[rnn_tx_id]["final_calculated_amount"] = round(calculated_final_amount, 2) + + + # ========================================================================= + # 5. TOKENIZAÇÃO / REGISTRO DA OPERAÇÃO (Só se não houve falha crítica antes) + # ========================================================================= + if final_status not in ["failed_config", "failed_market_data", "failed_rnn_analysis"]: # Prossegue se ao menos tentou executar + transactions_db[rnn_tx_id]["status_details"] = "Finalizing transaction log (tokenization)" + logger.info(f"BG TASK [{rnn_tx_id}]: Registrando (tokenizando) operação detalhadamente...") + # Placeholder para LÓGICA REAL DE TOKENIZAÇÃO (TOKENIZATION_PLACEHOLDER) + # 1. Coletar todos os dados relevantes da transação de `transactions_db[rnn_tx_id]` + # (market_data_collected, rnn_decisions, executed_trades, eod_portfolio_value_simulated, etc.) + # 2. Se for usar blockchain: + # a. Preparar os dados para um contrato inteligente. + # b. Interagir com o contrato (ex: web3.py para Ethereum). + # c. Armazenar o hash da transação da blockchain. + # 3. Se for um registro interno avançado: + # a. Assinar digitalmente os dados da transação. + # b. Armazenar em um sistema de log imutável ou banco de dados com auditoria. + + # Simulação atual (hash dos dados da transação): + transaction_data_for_hash = { + "rnn_tx_id": rnn_tx_id, "client_id": client_id, "initial_amount": amount, + "final_amount_calculated": calculated_final_amount, + # Incluir resumos ou hashes dos dados coletados para não tornar o hash gigante + "market_data_summary_keys": list(transactions_db[rnn_tx_id].get("market_data_collected", {}).keys()), + "rnn_decisions_count": len(transactions_db[rnn_tx_id].get("rnn_decisions", [])), + "executed_trades_count": len(transactions_db[rnn_tx_id].get("executed_trades", [])), + "eod_portfolio_value": transactions_db[rnn_tx_id].get("eod_portfolio_value_simulated"), + "timestamp": datetime.utcnow().isoformat() + } + ordered_tx_data_str = json.dumps(transaction_data_for_hash, sort_keys=True) + proof_token_hash = hashlib.sha256(ordered_tx_data_str.encode('utf-8')).hexdigest() + + transactions_db[rnn_tx_id]["proof_of_operation_token"] = proof_token_hash + transactions_db[rnn_tx_id]["tokenization_method"] = "internal_summary_hash_proof" + await asyncio.sleep(0.5) # Simula tempo de escrita/hash + logger.info(f"BG TASK [{rnn_tx_id}]: Operação registrada. Prova (hash): {proof_token_hash[:10]}...") + + + # ========================================================================= + # 6. PREPARAR E ENVIAR CALLBACK PARA AIBANK + # ========================================================================= + if exchange and hasattr(exchange, 'close'): + try: + await exchange.close() + logger.info(f"BG TASK [{rnn_tx_id}]: Conexão ccxt fechada.") + except Exception as e_close: # Especificar o tipo de exceção se souber + logger.warning(f"BG TASK [{rnn_tx_id}]: Erro ao fechar conexão ccxt: {str(e_close)}") + + if not AIBANK_CALLBACK_URL or not CALLBACK_SHARED_SECRET: + logger.error(f"BG TASK [{rnn_tx_id}]: Configuração de callback ausente. Não é possível notificar o AIBank.") + transactions_db[rnn_tx_id]["callback_status"] = "config_missing_critical" + return + + # Certifique-se que `final_status` reflete o estado real da operação + # Se `error_details` não estiver vazio e `final_status` ainda for "completed", ajuste-o + if error_details and final_status == "completed": + final_status = "completed_with_warnings" # Ou um status mais apropriado + + callback_payload_data = InvestmentResultPayload( + rnn_transaction_id=rnn_tx_id, aibank_transaction_token=aibank_tx_token, client_id=client_id, + initial_amount=amount, final_amount=round(calculated_final_amount, 2), # Arredonda para 2 casas decimais + profit_loss=round(calculated_final_amount - amount, 2), + status=final_status, timestamp=datetime.utcnow(), + details=error_details if error_details else "Investment cycle processed." + ) + payload_json_str = callback_payload_data.model_dump_json() # Garante que está usando a string serializada + + signature = hmac.new(CALLBACK_SHARED_SECRET.encode('utf-8'), payload_json_str.encode('utf-8'), hashlib.sha256).hexdigest() + headers = {'Content-Type': 'application/json', 'X-RNN-Signature': signature} + + logger.info(f"BG TASK [{rnn_tx_id}]: Enviando callback para AIBank ({AIBANK_CALLBACK_URL}) com status final '{final_status}'. Payload: {payload_json_str}") + transactions_db[rnn_tx_id]["callback_status"] = "sending" + try: + async with httpx.AsyncClient(timeout=30.0) as client: # Timeout global para o cliente + response = await client.post(AIBANK_CALLBACK_URL, content=payload_json_str, headers=headers) + response.raise_for_status() + logger.info(f"BG TASK [{rnn_tx_id}]: Callback para AIBank enviado com sucesso. Resposta: {response.status_code}") + transactions_db[rnn_tx_id]["callback_status"] = f"sent_success_{response.status_code}" + except httpx.RequestError as e_req: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro de REDE ao enviar callback para AIBank: {e_req}") + transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_network_error" + except httpx.HTTPStatusError as e_http: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro HTTP do AIBank ao receber callback: {e_http.response.status_code} - {e_http.response.text[:200]}") + transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_http_error_{e_http.response.status_code}" + except Exception as e_cb_final: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro INESPERADO ao enviar callback: {e_cb_final}", exc_info=True) + transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_unknown_error" + + +import asyncio +import random + + + +# --- Endpoints da API --- +@app.post("/api/invest", + response_model=InvestmentResponse, + dependencies=[Depends(verify_aibank_key)]) +async def initiate_investment( + request_data: InvestmentRequest, + background_tasks: BackgroundTasks +): + """ + Endpoint para o AIBank iniciar um ciclo de investimento. + Responde rapidamente e executa a lógica pesada em background. + """ + logger.info(f"Requisição de investimento recebida para client_id: {request_data.client_id}, " + f"amount: {request_data.amount}, aibank_tx_token: {request_data.aibank_transaction_token}") + + rnn_tx_id = str(uuid.uuid4()) + + # Armazena informações iniciais da transação DB real para ser mais robusto + transactions_db[rnn_tx_id] = { + "rnn_transaction_id": rnn_tx_id, + "aibank_transaction_token": request_data.aibank_transaction_token, + "client_id": request_data.client_id, + "initial_amount": request_data.amount, + "status": "pending_background_processing", + "received_at": datetime.utcnow().isoformat(), + "callback_status": "not_sent_yet" + } + + # Adiciona a tarefa de longa duração ao background + background_tasks.add_task( + execute_investment_strategy_background, + rnn_tx_id, + request_data.client_id, + request_data.amount, + request_data.aibank_transaction_token + ) + + logger.info(f"Estratégia de investimento para rnn_tx_id: {rnn_tx_id} agendada para execução em background.") + return InvestmentResponse( + status="pending", + message="Investment request received and is being processed in the background. Await callback for results.", + rnn_transaction_id=rnn_tx_id + ) + +@app.get("/api/transaction_status/{rnn_tx_id}", response_class=JSONResponse) +async def get_transaction_status(rnn_tx_id: str): + """ Endpoint para verificar o status de uma transação (para debug/admin) """ + transaction = transactions_db.get(rnn_tx_id) + if not transaction: + raise HTTPException(status_code=404, detail="Transaction not found") + return transaction + + +# --- Dashboard (Existente, adaptado) --- +# Setup para arquivos estáticos e templates + +try: + app.mount("/static", StaticFiles(directory="rnn/static"), name="static") + templates = Environment(loader=FileSystemLoader("rnn/templates")) +except RuntimeError as e: + logger.warning(f"Não foi possível montar /static ou carregar templates: {e}. O dashboard pode não funcionar.") + templates = None # Para evitar erros se o loader falhar + +@app.get("/", response_class=HTMLResponse) +async def index(request: Request): + if not templates: + return HTMLResponse("

Dashboard indisponível

Configuração de templates/estáticos falhou.

") + + agora = datetime.now() + agentes_simulados = [ + # dados de agentes ... + ] + template = templates.get_template("index.html") + # Adicionar transações recentes ao contexto do template + recent_txs = list(transactions_db.values())[-5:] # Últimas 5 transações + return HTMLResponse(template.render(request=request, agentes=agentes_simulados, transactions=recent_txs)) + +# --- Imports para Background Task --- +import asyncio +import random + +# Função de logger dummy s +# class DummyLogger: +# def info(self, msg, *args, **kwargs): print(f"INFO: {msg}") +# def warning(self, msg, *args, **kwargs): print(f"WARNING: {msg}") +# def error(self, msg, *args, **kwargs): print(f"ERROR: {msg}", kwargs.get('exc_info')) + +# if __name__ == "__main__": # Para teste local +# # logger = DummyLogger() # se não tiver get_logger() +# # Configuração das variáveis de ambiente para teste local +# os.environ["AIBANK_API_KEY"] = "test_aibank_key_from_rnn_server" +# os.environ["AIBANK_CALLBACK_URL"] = "http://localhost:8001/api/rnn_investment_result_callback" # URL do aibank simulado +# os.environ["CALLBACK_SHARED_SECRET"] = "super_secret_for_callback_signing" +# # import uvicorn +# # uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/app-old-no-module.py b/app-old-no-module.py new file mode 100644 index 0000000000000000000000000000000000000000..a222bc66ef4df320e294bebc890281cc92a0b1f3 --- /dev/null +++ b/app-old-no-module.py @@ -0,0 +1,901 @@ + + +import os +import uuid +import time +import hmac +import hashlib +import json +from datetime import datetime, timedelta +from typing import Dict, Any +import ccxt.async_support as ccxt + +import httpx # Para fazer chamadas HTTP assíncronas (para o callback) +from fastapi import FastAPI, Request, HTTPException, Depends, Header, BackgroundTasks +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from jinja2 import Environment, FileSystemLoader +from pydantic import BaseModel, Field + +from app. utils.logger import get_logger + +logger = get_logger() + +# --- Configuração Inicial e Variáveis de Ambiente (Secrets do Hugging Face) --- +AIBANK_API_KEY = os.environ.get("AIBANK_API_KEY") # Chave que o aibank usa para chamar esta API RNN +AIBANK_CALLBACK_URL = os.environ.get("AIBANK_CALLBACK_URL") # URL no aibank para onde esta API RNN enviará o resultado +CALLBACK_SHARED_SECRET = os.environ.get("CALLBACK_SHARED_SECRET") # Segredo para assinar/verificar o payload do callback + +# Chaves para serviços externos +MARKET_DATA_API_KEY = os.environ.get("MARKET_DATA_API_KEY") +EXCHANGE_API_KEY = os.environ.get("EXCHANGE_API_KEY") +EXCHANGE_API_SECRET = os.environ.get("EXCHANGE_API_SECRET") + +if not AIBANK_API_KEY: + logger.warning("AIBANK_API_KEY não configurada. A autenticação para /api/invest falhou.") +if not AIBANK_CALLBACK_URL: + logger.warning("AIBANK_CALLBACK_URL não configurada. O callback para o aibank falhou.") +if not CALLBACK_SHARED_SECRET: + logger.warning("CALLBACK_SHARED_SECRET não configurado. A segurança do callback está comprometida.") + + + +#CHAVES PARA CCXT (para Binance) +# configuradas como vriaveis no Hugging Face Space + +CCXT_EXCHANGE_ID = os.environ.get("CCXT_EXCHANGE_ID", "binance") +CCXT_API_KEY = os.environ.get("CCXT_API_KEY") +CCXT_API_SECRET = os.environ.get("CCXT_API_SECRET") +CCXT_API_PASSWORD = os.environ.get("CCXT_API_PASSWORD") # Opcional + +# Binance Testnet +CCXT_SANDBOX_MODE = os.environ.get("CCXT_SANDBOX_MODE", "false").lower() == "true" + + + + + +app = FastAPI(title="ATCoin Neural Agents - Investment API") + +# --- Middlewares --- +app.add_middleware( + CORSMiddleware, + allow_origins=[ + "http://localhost:3000", # URL desenvolvimento local + "http://aibank.app.br", # URL de produção + "https://*.aibank.app.br", # subdomínios + "https://*.hf.space" # HF Space + ], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# --- Simulação de Banco de Dados de Transações DEV --- +# Em produção MongoDB +transactions_db: Dict[str, Dict[str, Any]] = {} + +# --- Modelos Pydantic --- +class InvestmentRequest(BaseModel): + client_id: str + amount: float = Field(..., gt=0) # Garante que o montante seja positivo + aibank_transaction_token: str # Token único gerado pelo aibank para rastreamento + +class InvestmentResponse(BaseModel): + status: str + message: str + rnn_transaction_id: str # ID da transação this.API + +class InvestmentResultPayload(BaseModel): # Payload para o callback para o aibank + rnn_transaction_id: str + aibank_transaction_token: str + client_id: str + initial_amount: float + final_amount: float + profit_loss: float + status: str # "completed", "failed" + timestamp: datetime + details: str = "" + + +# --- Dependência de Autenticação --- +async def verify_aibank_key(authorization: str = Header(None)): + if not AIBANK_API_KEY: # Checagem se a chave do servidor está configurada + logger.error("CRITICAL: AIBANK_API_KEY (server-side) não está configurada nos Secrets.") + raise HTTPException(status_code=500, detail="Internal Server Configuration Error: Missing server API Key.") + + if authorization is None: + logger.warning("Authorization header ausente na chamada do AIBank.") + raise HTTPException(status_code=401, detail="Authorization header is missing") + + parts = authorization.split() + if len(parts) != 2 or parts[0].lower() != 'bearer': + logger.warning(f"Formato inválido do Authorization header: {authorization}") + raise HTTPException(status_code=401, detail="Authorization header must be 'Bearer '") + + token_from_aibank = parts[1] + if not hmac.compare_digest(token_from_aibank, AIBANK_API_KEY): + logger.warning(f"Chave de API inválida fornecida pelo AIBank. Token: {token_from_aibank[:10]}...") + raise HTTPException(status_code=403, detail="Invalid API Key provided by AIBank.") + logger.info("API Key do AIBank verificada com sucesso.") + return True + + +# --- Lógica de Negócio Principal (Simulada e em Background) --- + +async def execute_investment_strategy_background( + rnn_tx_id: str, + client_id: str, + amount: float, + aibank_tx_token: str +): + """ + Esta função roda em background. Simula a coleta de dados, RNN, execução e tokenização. + No final, chama o callback para o aibank. + """ + logger.info(f"BG TASK [{rnn_tx_id}]: Iniciando estratégia de investimento para cliente {client_id}, valor {amount}.") + transactions_db[rnn_tx_id]["status"] = "processing" # Status geral inicial + transactions_db[rnn_tx_id]["status_details"] = "Initializing investment cycle" + + final_status = "completed" # Status final presumido + error_details = "" + calculated_final_amount = amount # Valor inicial, será modificado + + # Inicializa o objeto da exchange ccxt + exchange = None + if CCXT_API_KEY and CCXT_API_SECRET: #nicializa se as chaves básicas estiverem presentes + try: + exchange_class = getattr(ccxt, CCXT_EXCHANGE_ID) + config = { + 'apiKey': CCXT_API_KEY, + 'secret': CCXT_API_SECRET, + 'enableRateLimit': True, # evitar bans da API + # 'verbose': True, # Para debug detalhado das chamadas ccxt + } + if CCXT_API_PASSWORD: + config['password'] = CCXT_API_PASSWORD + + exchange = exchange_class(config) + + if CCXT_SANDBOX_MODE: + if hasattr(exchange, 'set_sandbox_mode'): # Nem todas as exchanges suportam isso diretamente em ccxt + exchange.set_sandbox_mode(True) + logger.info(f"BG TASK [{rnn_tx_id}]: CCXT configurado para modo SANDBOX para {CCXT_EXCHANGE_ID}.") + elif 'test' in exchange.urls: # usar testnet another.way + exchange.urls['api'] = exchange.urls['test'] + logger.info(f"BG TASK [{rnn_tx_id}]: CCXT URLs alteradas para TESTNET para {CCXT_EXCHANGE_ID}.") + else: + logger.warning(f"BG TASK [{rnn_tx_id}]: Modo SANDBOX solicitado mas não explicitamente suportado por ccxt para {CCXT_EXCHANGE_ID} ou URL de teste não encontrada.") + + # Carregar mercados para validar símbolos, demorado. + # await exchange.load_markets() + # logger.info(f"BG TASK [{rnn_tx_id}]: Mercados carregados para {CCXT_EXCHANGE_ID}.") + + except AttributeError: + logger.error(f"BG TASK [{rnn_tx_id}]: Exchange ID '{CCXT_EXCHANGE_ID}' inválida ou não suportada pelo ccxt.") + error_details += f"Invalid CCXT_EXCHANGE_ID: {CCXT_EXCHANGE_ID}; " + final_status = "failed_config" # Um novo status para falha de configuração + # (Pular para o callback aqui) + except Exception as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao inicializar ccxt para {CCXT_EXCHANGE_ID}: {str(e)}", exc_info=True) + error_details += f"CCXT initialization error: {str(e)}; " + final_status = "failed_config" + # (Pular para o callback aqui) + else: + logger.warning(f"BG TASK [{rnn_tx_id}]: CCXT_API_KEY ou CCXT_API_SECRET não configurados. A coleta de dados de cripto e execução de ordens via ccxt serão puladas.") + #Decidir se é uma falha fatal ou se a estratégia pode prosseguir sem dados de cripto. + # Precisa lidar com dados ausentes. + + + # ========================================================================= + # 1. COLETAR DADOS DE MERCADO (com ccxt integrado) + # ========================================================================= + logger.info(f"BG TASK [{rnn_tx_id}]: Coletando dados de mercado...") + transactions_db[rnn_tx_id]["status_details"] = "Fetching market data" + market_data_results = { + "crypto": {}, # Dados específicos de cripto + "stocks": {}, # Dados de ações (integrar yfinance ou outro) + "other": {} # Outros dados + } + market_data_fetch_success = True + + # --- Defina os pares de cripto --- + # Estes poderiam vir de uma configuração, ou serem dinâmicos. + crypto_pairs_to_fetch = ["BTC/USDT", "ETH/USDT", "SOL/USDT"] + + if exchange: # Buscar se a exchange foi inicializada corretamente + try: + for pair in crypto_pairs_to_fetch: + pair_data = {} + if exchange.has['fetchTicker']: + ticker = await exchange.fetch_ticker(pair) + pair_data['ticker'] = { + 'last': ticker.get('last'), + 'bid': ticker.get('bid'), + 'ask': ticker.get('ask'), + 'volume': ticker.get('baseVolume'), # Ou 'quoteVolume' + 'timestamp': ticker.get('timestamp') + } + logger.info(f"BG TASK [{rnn_tx_id}]: Ticker {pair}: Preço {ticker.get('last')}") + + if exchange.has['fetchOHLCV']: + # timeframe: '1m', '5m', '15m', '1h', '4h', '1d', '1w', '1M' + # limit: número de candles + ohlcv = await exchange.fetch_ohlcv(pair, timeframe='1h', limit=72) # Últimas 72 horas + # Formato OHLCV: [timestamp, open, high, low, close, volume] + pair_data['ohlcv_1h'] = ohlcv + logger.info(f"BG TASK [{rnn_tx_id}]: Coletado {len(ohlcv)} candles OHLCV para {pair} (1h).") + + # Outros dados que ccxt: + # if exchange.has['fetchOrderBook']: + # order_book = await exchange.fetch_order_book(pair, limit=5) # Top 5 bids/asks + # pair_data['order_book_L1'] = { # Exemplo de Level 1 + # 'bids': order_book['bids'][0] if order_book['bids'] else None, + # 'asks': order_book['asks'][0] if order_book['asks'] else None, + # } + # if exchange.has['fetchTrades']: # Últimas negociações públicas + # trades = await exchange.fetch_trades(pair, limit=10) + # pair_data['recent_trades'] = trades + + market_data_results["crypto"][pair.replace("/", "_")] = pair_data # Usar _ para chaves de dict seguras + + logger.info(f"BG TASK [{rnn_tx_id}]: Dados de cripto via ccxt coletados.") + + except ccxt.NetworkError as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro de rede ccxt ao coletar dados de mercado: {str(e)}", exc_info=True) + market_data_fetch_success = False + error_details += f"CCXT NetworkError: {str(e)}; " + except ccxt.ExchangeError as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro da exchange ccxt ao coletar dados de mercado: {str(e)}", exc_info=True) + market_data_fetch_success = False + error_details += f"CCXT ExchangeError: {str(e)}; " + except Exception as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro geral ao coletar dados de cripto via ccxt: {str(e)}", exc_info=True) + market_data_fetch_success = False + error_details += f"General CCXT data collection error: {str(e)}; " + else: + logger.info(f"BG TASK [{rnn_tx_id}]: Instância da exchange ccxt não disponível. Pulando coleta de dados de cripto.") + # Se não há 'exchange', a coleta de dados de cripto não pode ocorrerá. + # Erro, dependendo se as chaves Not.ok. + if CCXT_API_KEY and CCXT_API_SECRET: # Chaves dadas mas a 'exchange' falhou na init + market_data_fetch_success = False # Falha se a config estava lá + error_details += "CCXT exchange object not initialized despite API keys being present; " + + + # --- Coleta de dados para outros tipos de ativos (Ações com yfinance) --- + # try: + # import yfinance as yf + # aapl = yf.Ticker("AAPL") + # hist_aapl = aapl.history(period="5d", interval="1h") # Últimos 5 dias, candles de 1h + # if not hist_aapl.empty: + # market_data_results["stocks"]["AAPL"] = { + # "ohlcv_1h": [[ts.timestamp() * 1000] + list(row) for ts, row in hist_aapl[['Open', 'High', 'Low', 'Close', 'Volume']].iterrows()] + # } + # logger.info(f"BG TASK [{rnn_tx_id}]: Coletado {len(hist_aapl)} candles para AAPL via yfinance.") + # except Exception as e: + # logger.warning(f"BG TASK [{rnn_tx_id}]: Falha ao buscar dados de ações com yfinance: {e}") + # + + + # --- Simulação para outros dados, se necessário --- + market_data_results["other"]['simulated_index_level'] = random.uniform(10000, 15000) + market_data_results["other"]['simulated_crypto_sentiment'] = random.uniform(-1, 1) + + if not market_data_fetch_success and (CCXT_API_KEY and CCXT_API_SECRET): # Se as chaves de cripto foram dadas e a coleta falhou + final_status = "failed_market_data" + logger.error(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado falhou criticamente. {error_details}") + # (Pular para o callback aqui, pois a RNN pode não ter dados suficientes) + # 'return' ou lógica de pular precisa ser implementada se a falha for fatal + # Registrar e permitir que a lógica da RNN tente lidar com dados ausentes/parciais + pass + + transactions_db[rnn_tx_id]["market_data_collected"] = market_data_results # Armazena para auditoria/debug + transactions_db[rnn_tx_id]["status_details"] = "Processing RNN analysis" + logger.info(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado concluída (sucesso: {market_data_fetch_success}).") + + # ========================================================================= + # 2. ANÁLISE PELA RNN E TOMADA DE DECISÃO + # ========================================================================= + # (alimentada por market_data_results) + # ... + logger.info(f"BG TASK [{rnn_tx_id}]: Executando análise RNN com os dados coletados...") + transactions_db[rnn_tx_id]["status_details"] = "Running RNN model" + investment_decisions = [] + rnn_analysis_success = True + + try: + + # A RNN precisará ser capaz de lidar com a estrutura de market_data_results, + # incluindo a possibilidade de alguns dados estarem ausentes se a coleta falhar. + + # investment_decisions = await rnn_model.predict_async(market_data_results, amount_to_invest=amount) + + # Simulação para prosseguir: + await asyncio.sleep(random.uniform(8, 15)) + if market_data_results["crypto"]: # Com dados de cripto + if random.random() > 0.1: + num_crypto_assets_to_invest = random.randint(1, len(crypto_pairs_to_fetch)) + chosen_pairs = random.sample(list(market_data_results["crypto"].keys()), k=num_crypto_assets_to_invest) + + for crypto_key in chosen_pairs: # crypto_key "BTC_USDT" + asset_symbol = crypto_key.replace("_", "/") # Converte de volta para "BTC/USDT" + allocated_amount = (amount / num_crypto_assets_to_invest) * random.uniform(0.7, 0.9) # Aloca uma parte do total + investment_decisions.append({ + "asset_id": asset_symbol, # Usa o símbolo da exchange + "type": "CRYPTO", + "action": "BUY", + "target_usd_amount": round(allocated_amount, 2), + "reasoning": f"RNN signal for {asset_symbol} based on simulated data and ticker {market_data_results['crypto'][crypto_key].get('ticker', {}).get('last', 'N/A')}" + }) + else: + logger.info(f"BG TASK [{rnn_tx_id}]: Sem dados de cripto para a RNN processar.") + + if not investment_decisions: + logger.info(f"BG TASK [{rnn_tx_id}]: RNN não gerou nenhuma decisão de investimento.") + else: + logger.info(f"BG TASK [{rnn_tx_id}]: RNN gerou {len(investment_decisions)} decisões: {investment_decisions}") + + except Exception as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante análise RNN: {str(e)}", exc_info=True) + rnn_analysis_success = False + error_details += f"RNN analysis failed: {str(e)}; " + + if not rnn_analysis_success: + final_status = "failed_rnn_analysis" + # (Pular para o callback) + pass + + transactions_db[rnn_tx_id]["rnn_decisions"] = investment_decisions + transactions_db[rnn_tx_id]["status_details"] = "Preparing to execute orders" + total_usd_allocated_by_rnn = sum(d['target_usd_amount'] for d in investment_decisions if d['action'] == 'BUY') + + + # ========================================================================= + # 3. EXECUÇÃO DE ORDENS (Será detalhado no próximo passo) + # ========================================================================= + # ... (execução de ordens aqui, usando `exchange` ccxt e `investment_decisions`) + # ... (Incluindo cálculo de `current_portfolio_value` e `cash_remaining_after_allocation`) + logger.info(f"BG TASK [{rnn_tx_id}]: Executando ordens baseadas nas decisões da RNN...") + transactions_db[rnn_tx_id]["status_details"] = "Executing investment orders" + executed_trades_info = [] + order_execution_success = True + cash_remaining_after_allocation = amount - total_usd_allocated_by_rnn + current_portfolio_value = 0 # Valor dos ativos comprados + + # Placeholder para execução + if investment_decisions: + for decision in investment_decisions: + if decision.get("action") == "BUY": + usd_amount_to_spend = decision.get("target_usd_amount") + simulated_cost = usd_amount_to_spend * random.uniform(0.99, 1.0) + executed_trades_info.append({ + "asset": decision.get("asset_id"), "order_id": f"sim_ord_{uuid.uuid4()}", + "status": "filled", "cost_usd": simulated_cost + }) + current_portfolio_value += simulated_cost + await asyncio.sleep(random.uniform(1,3) * len(investment_decisions)) # Simula tempo de execução + else: + cash_remaining_after_allocation = amount + + transactions_db[rnn_tx_id]["executed_trades"] = executed_trades_info + transactions_db[rnn_tx_id]["status_details"] = "Simulating holding period and profit/loss" + + + # ========================================================================= + # 4. SIMULAÇÃO DO PERÍODO DE INVESTIMENTO E CÁLCULO DE LUCRO/PERDA + # ========================================================================= + # ... (Inserir lógia ) + logger.info(f"BG TASK [{rnn_tx_id}]: Simulando período de investimento e fechamento de posições...") + await asyncio.sleep(random.uniform(5, 10)) + + if current_portfolio_value > 0: + profit_on_invested_part = current_portfolio_value * (0.042 * random.uniform(0.8, 1.2)) + value_of_investments_at_eod = current_portfolio_value + profit_on_invested_part + else: + value_of_investments_at_eod = 0 + profit_on_invested_part = 0 + + calculated_final_amount = value_of_investments_at_eod + cash_remaining_after_allocation + + logger.info(f"BG TASK [{rnn_tx_id}]: Valor inicial total: {amount:.2f}. " + f"Valor alocado para investimento: {total_usd_allocated_by_rnn:.2f}. " + f"Valor dos investimentos no EOD: {value_of_investments_at_eod:.2f}. " + f"Caixa não alocado: {cash_remaining_after_allocation:.2f}. " + f"Valor final total: {calculated_final_amount:.2f}") + + transactions_db[rnn_tx_id]["eod_portfolio_value_simulated"] = value_of_investments_at_eod + + # ========================================================================= + # 5. TOKENIZAÇÃO / REGISTRO DA OPERAÇÃO + # ========================================================================= + # ... (Inserir) + logger.info(f"BG TASK [{rnn_tx_id}]: Registrando (tokenizando) operação detalhadamente...") + # ... (código do hash proof) + await asyncio.sleep(1) + + # ========================================================================= + # 6. PREPARAR E ENVIAR CALLBACK PARA AIBANK + # ========================================================================= + # (Fechamento da conexão ccxt antes do callback, se a conexão foi aberta e bem sucedida) + if exchange and hasattr(exchange, 'close'): + try: + await exchange.close() + logger.info(f"BG TASK [{rnn_tx_id}]: Conexão ccxt com {CCXT_EXCHANGE_ID} fechada.") + except Exception as e: + logger.warning(f"BG TASK [{rnn_tx_id}]: Erro ao fechar conexão ccxt: {str(e)}") + + # (callback , usando `final_status`, `error_details`, `calculated_final_amount`) + # ... + if not AIBANK_CALLBACK_URL or not CALLBACK_SHARED_SECRET: + logger.error(f"BG TASK [{rnn_tx_id}]: AIBANK_CALLBACK_URL ou CALLBACK_SHARED_SECRET não configurado. Não é possível enviar callback.") + transactions_db[rnn_tx_id]["callback_status"] = "config_missing" + return # Não pode prosseguir sem config de callback + + callback_payload_data = InvestmentResultPayload( + rnn_transaction_id=rnn_tx_id, + aibank_transaction_token=aibank_tx_token, + client_id=client_id, + initial_amount=amount, + final_amount=calculated_final_amount, + profit_loss=calculated_final_amount - amount, + status=final_status, # O status final da operação + timestamp=datetime.utcnow(), + details=error_details if final_status != "completed" else "Investment cycle completed successfully." + ) + payload_json = callback_payload_data.model_dump_json() + signature = hmac.new(CALLBACK_SHARED_SECRET.encode('utf-8'), payload_json.encode('utf-8'), hashlib.sha256).hexdigest() + headers = {'Content-Type': 'application/json', 'X-RNN-Signature': signature} + + logger.info(f"BG TASK [{rnn_tx_id}]: Enviando callback para AIBank: {AIBANK_CALLBACK_URL} com status final '{final_status}'") + transactions_db[rnn_tx_id]["callback_status"] = "sending" + try: + async with httpx.AsyncClient() as client: + response = await client.post(AIBANK_CALLBACK_URL, content=payload_json, headers=headers, timeout=30.0) + response.raise_for_status() + logger.info(f"BG TASK [{rnn_tx_id}]: Callback enviado com sucesso para AIBank. Status da resposta: {response.status_code}") + transactions_db[rnn_tx_id]["callback_status"] = f"sent_success_{response.status_code}" + except Exception as e: # Captura mais genérica para erros de callback + logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao enviar callback para AIBank: {str(e)}", exc_info=True) + # Classificar o erro para o log + if isinstance(e, httpx.RequestError): + transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_request_error" + elif isinstance(e, httpx.HTTPStatusError): + transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_http_error_{e.response.status_code}" + else: + transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_unknown_error" + + + +# --- +""" async def execute_investment_strategy_background( + rnn_tx_id: str, + client_id: str, + amount: float, + aibank_tx_token: str +): + + logger.info(f"BG TASK [{rnn_tx_id}]: Iniciando estratégia de investimento para cliente {client_id}, valor {amount}.") + transactions_db[rnn_tx_id]["status"] = "processing_market_data" + + final_status = "completed" + error_details = "" + calculated_final_amount = amount # Valor inicial + + try: + # 1. COLETAR DADOS DE MERCADO (Placeholder) + + logger.info(f"BG TASK [{rnn_tx_id}]: Coletando dados de mercado...") + transactions_db[rnn_tx_id]["status_details"] = "Fetching market data" + market_data_results = {} + market_data_fetch_success = True + + # Exemplo para Cripto com ccxt (requer 'pip install ccxt') + # import ccxt.async_support as ccxt # Coloque no topo do app.py + # exchange_id = 'binance' # Exemplo + # exchange_class = getattr(ccxt, exchange_id) + # exchange = exchange_class({ + # 'apiKey': EXCHANGE_API_KEY, # Do os.environ + # 'secret': EXCHANGE_API_SECRET, # Do os.environ + # 'enableRateLimit': True, # Importante + # }) + + try: + # --- Exemplo para buscar dados de BTC/USDT --- + # if exchange.has['fetchTicker']: + # ticker_btc = await exchange.fetch_ticker('BTC/USDT') + # market_data_results['BTC_USDT_ticker'] = ticker_btc + # logger.info(f"BG TASK [{rnn_tx_id}]: Ticker BTC/USDT: {ticker_btc['last']}") + # if exchange.has['fetchOHLCV']: + # ohlcv_btc = await exchange.fetch_ohlcv('BTC/USDT', timeframe='1h', limit=100) # Últimas 100 horas + # market_data_results['BTC_USDT_ohlcv_1h'] = ohlcv_btc + # logger.info(f"BG TASK [{rnn_tx_id}]: Coletado {len(ohlcv_btc)} candles OHLCV para BTC/USDT 1h.") + + # --- Exemplo para Ações com yfinance (requer 'pip install yfinance') --- + # import yfinance as yf # Coloque no topo do app.py + # aapl = yf.Ticker("AAPL") + # hist_aapl = aapl.history(period="1mo") # Dados do último mês + # market_data_results['AAPL_history_1mo'] = hist_aapl.to_dict() # Pode ser grande, serialize com cuidado + # current_price_aapl = hist_aapl['Close'].iloc[-1] if not hist_aapl.empty else None + # market_data_results['AAPL_current_price'] = current_price_aapl + # logger.info(f"BG TASK [{rnn_tx_id}]: Preço atual AAPL (yfinance): {current_price_aapl}") + + # --- Placeholder para sua lógica real de coleta --- + # Você precisará definir QUAIS ativos e QUAIS dados são necessários para sua RNN. + # Este é um ponto crucial para sua estratégia. + # Simulação para prosseguir: + await asyncio.sleep(random.uniform(3, 7)) # Simula demora da coleta real + market_data_results['simulated_index_level'] = random.uniform(10000, 15000) + market_data_results['simulated_crypto_sentiment'] = random.uniform(-1, 1) + logger.info(f"BG TASK [{rnn_tx_id}]: Dados de mercado (simulados/reais) coletados.") + + except Exception as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao coletar dados de mercado: {str(e)}", exc_info=True) + market_data_fetch_success = False + error_details += f"Market data collection failed: {str(e)}; " + # Decida se a falha aqui é crítica e impede de continuar. + # Se sim, atualize final_status e pule para o callback. + + # finally: # Importante para fechar conexões de exchange em ccxt + # if 'exchange' in locals() and hasattr(exchange, 'close'): + # await exchange.close() + + if not market_data_fetch_success: + # Lógica para lidar com falha na coleta de dados (ex: não prosseguir) + final_status = "failed_market_data" + # ... (atualize transactions_db e pule para a seção de callback) ... + # (Este 'return' ou lógica de pular precisa ser implementada se a falha for fatal) + pass # Por ora, deixamos prosseguir com dados possivelmente incompletos ou apenas simulados + + transactions_db[rnn_tx_id]["market_data_collected"] = market_data_results # Armazene para auditoria/debug + transactions_db[rnn_tx_id]["status_details"] = "Processing RNN analysis" + + + # 2. ANÁLISE PELA RNN E TOMADA DE DECISÃO (Placeholder) + l + + logger.info(f"BG TASK [{rnn_tx_id}]: Executando análise RNN com os dados coletados...") + transactions_db[rnn_tx_id]["status_details"] = "Running RNN model" + investment_decisions = [] # Lista de decisões: {'asset': 'BTC/USDT', 'action': 'BUY', 'amount_usd': 5000, 'price_target': 70000} + rnn_analysis_success = True + + try: + # --- SUBSTITUA PELA CHAMADA AO SEU MODELO RNN --- + # Exemplo: Supondo que você tenha uma classe ou função rnn_predictor + # from rnn.models.predictor import rnn_predictor # Exemplo de import + + # Supondo que seu predictor precise dos dados de mercado e do montante a investir + # investment_decisions = await rnn_predictor.generate_signals_async( + # market_data_results, + # amount_to_invest=amount # O montante total disponível para este ciclo + # ) + + # Simulação para prosseguir: + await asyncio.sleep(random.uniform(8, 15)) # Simula processamento da RNN + if random.random() > 0.1: # 90% de chance de "decidir" investir + num_assets_to_invest = random.randint(1, 3) + for i in range(num_assets_to_invest): + asset_name = random.choice(["SIMULATED_CRYPTO_X", "SIMULATED_STOCK_Y", "SIMULATED_BOND_Z"]) + action = random.choice(["BUY", "HOLD"]) # Simplificado, sem SELL por enquanto + if action == "BUY": + # Alocar uma porção do 'amount' total para este ativo + allocated_amount = (amount / num_assets_to_invest) * random.uniform(0.8, 1.0) + investment_decisions.append({ + "asset_id": f"{asset_name}_{i}", + "type": "CRYPTO" if "CRYPTO" in asset_name else "STOCK", # Exemplo + "action": action, + "target_usd_amount": round(allocated_amount, 2), + "reasoning": "RNN signal strong based on simulated data" # Adicione o output real da RNN + }) + + if not investment_decisions: + logger.info(f"BG TASK [{rnn_tx_id}]: RNN não gerou nenhuma decisão de investimento (ou decidiu não investir).") + # Isso pode ser um resultado válido. + else: + logger.info(f"BG TASK [{rnn_tx_id}]: RNN gerou {len(investment_decisions)} decisões: {investment_decisions}") + + except Exception as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante análise RNN: {str(e)}", exc_info=True) + rnn_analysis_success = False + error_details += f"RNN analysis failed: {str(e)}; " + + if not rnn_analysis_success: + final_status = "failed_rnn_analysis" + # ... (atualize transactions_db e pule para a seção de callback) ... + pass + + transactions_db[rnn_tx_id]["rnn_decisions"] = investment_decisions + transactions_db[rnn_tx_id]["status_details"] = "Preparing to execute orders" + + # Antes de executar ordens, vamos calcular o valor que REALMENTE foi investido + # e o que pode ter sobrado, já que a RNN pode não usar todo o 'amount'. + total_usd_allocated_by_rnn = sum(d['target_usd_amount'] for d in investment_decisions if d['action'] == 'BUY') + # calculated_final_amount = amount # Inicializa com o montante original + # Esta variável será atualizada após a execução das ordens e cálculo do lucro/perda + + + # 3. EXECUÇÃO DE ORDENS (Placeholder) + + # Dentro de execute_investment_strategy_background, substituindo a seção de execução de ordens: + + logger.info(f"BG TASK [{rnn_tx_id}]: Executando ordens baseadas nas decisões da RNN...") + transactions_db[rnn_tx_id]["status_details"] = "Executing investment orders" + executed_trades_info = [] + order_execution_success = True + + # O calculated_final_amount começa como o 'amount' inicial. + # Vamos deduzir o que foi efetivamente usado para compras + # e depois adicionar os lucros/perdas. + # Por enquanto, vamos assumir que o investimento visa usar o 'total_usd_allocated_by_rnn'. + # E o restante do 'amount' não alocado fica como "cash". + cash_remaining_after_allocation = amount - total_usd_allocated_by_rnn + current_portfolio_value = 0 # Valor dos ativos comprados + + # --- Exemplo de lógica de execução para decisões de COMPRA (BUY) --- + if investment_decisions: # Apenas se houver decisões + # exchange_exec = ccxt.binance({'apiKey': EXCHANGE_API_KEY, 'secret': EXCHANGE_API_SECRET}) # Exemplo + try: + for decision in investment_decisions: + if decision.get("action") == "BUY": + asset_id_to_buy = decision.get("asset_id") # Ex: "BTC/USDT" ou um ID interno que mapeia para um símbolo + usd_amount_to_spend = decision.get("target_usd_amount") + + logger.info(f"BG TASK [{rnn_tx_id}]: Tentando comprar {usd_amount_to_spend} USD de {asset_id_to_buy}") + + # --- SUBSTITUA PELA LÓGICA REAL DE EXECUÇÃO NA EXCHANGE --- + # Exemplo com ccxt (precisa de mais detalhes como símbolo de mercado correto): + # symbol_on_exchange = convert_asset_id_to_exchange_symbol(asset_id_to_buy, exchange_exec.id) + # current_price = (await exchange_exec.fetch_ticker(symbol_on_exchange))['last'] + # amount_of_asset_to_buy = usd_amount_to_spend / current_price + + # order = await exchange_exec.create_market_buy_order(symbol_on_exchange, amount_of_asset_to_buy) + # logger.info(f"BG TASK [{rnn_tx_id}]: Ordem de compra para {asset_id_to_buy} enviada: {order['id']}") + # executed_trades_info.append({ + # "asset": asset_id_to_buy, + # "order_id": order['id'], + # "status": order.get('status', 'unknown'), + # "amount_filled": order.get('filled', 0), + # "avg_price": order.get('average', current_price), + # "cost_usd": order.get('cost', usd_amount_to_spend), # Custo real da ordem + # "fees": order.get('fee', {}), + # }) + # current_portfolio_value += order.get('cost', usd_amount_to_spend) # Adiciona o valor do ativo comprado + + # Simulação para prosseguir: + await asyncio.sleep(random.uniform(1, 3)) # Simula envio de ordem + simulated_order_id = f"sim_ord_{uuid.uuid4()}" + simulated_cost = usd_amount_to_spend * random.uniform(0.99, 1.0) # Slippage simulado + executed_trades_info.append({ + "asset": asset_id_to_buy, + "order_id": simulated_order_id, + "status": "filled", + "amount_filled": simulated_cost / random.uniform(100, 200), # Qtd de ativo simulada + "avg_price": random.uniform(100, 200), # Preço simulado + "cost_usd": simulated_cost, + "fees": {"currency": "USD", "cost": simulated_cost * 0.001} # Taxa simulada + }) + current_portfolio_value += simulated_cost + logger.info(f"BG TASK [{rnn_tx_id}]: Ordem simulada {simulated_order_id} para {asset_id_to_buy} preenchida, custo {simulated_cost:.2f} USD.") + else: + logger.info(f"BG TASK [{rnn_tx_id}]: Decisão '{decision.get('action')}' para {decision.get('asset_id')} não é uma compra, pulando execução por enquanto.") + + except Exception as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante execução de ordens: {str(e)}", exc_info=True) + order_execution_success = False + error_details += f"Order execution failed: {str(e)}; " + # finally: + # if 'exchange_exec' in locals() and hasattr(exchange_exec, 'close'): + # await exchange_exec.close() + else: + logger.info(f"BG TASK [{rnn_tx_id}]: Nenhuma decisão de investimento para executar.") + # Se não há decisões, o current_portfolio_value é 0 e o cash_remaining é todo o 'amount' + cash_remaining_after_allocation = amount + + if not order_execution_success: + final_status = "failed_order_execution" + # ... (atualize transactions_db e pule para a seção de callback) ... + # O valor do portfólio aqui pode ser parcial se algumas ordens falharam + pass + + transactions_db[rnn_tx_id]["executed_trades"] = executed_trades_info + transactions_db[rnn_tx_id]["status_details"] = "Simulating holding period and profit/loss" + + # --- SIMULAÇÃO DO PERÍODO DE INVESTIMENTO E CÁLCULO DE LUCRO/PERDA DIÁRIO --- + # Em um sistema real, você monitoraria as posições e as fecharia no final do dia. + # Ou, se for um investimento de mais longo prazo, apenas calcularia o valor atual do portfólio. + # Para o objetivo de 4.2% ao dia, é implícito que as posições são fechadas diariamente. + + logger.info(f"BG TASK [{rnn_tx_id}]: Simulando período de investimento e fechamento de posições...") + await asyncio.sleep(random.uniform(5, 10)) # Simula o dia passando + + # Supondo que todas as posições são vendidas no final do "dia" + # E o current_portfolio_value muda com base no mercado. + # Para simular o objetivo de 4.2% sobre o VALOR INVESTIDO (current_portfolio_value no momento da compra): + if current_portfolio_value > 0: # Se algo foi investido + profit_on_invested_part = current_portfolio_value * (0.042 * random.uniform(0.8, 1.2)) # Simula variação no lucro + value_of_investments_at_eod = current_portfolio_value + profit_on_invested_part + else: # Nada foi investido + value_of_investments_at_eod = 0 + profit_on_invested_part = 0 + + # O calculated_final_amount é o valor dos investimentos no fim do dia + o caixa que não foi alocado + calculated_final_amount = value_of_investments_at_eod + cash_remaining_after_allocation + + logger.info(f"BG TASK [{rnn_tx_id}]: Valor inicial total: {amount:.2f}. " + f"Valor alocado para investimento: {total_usd_allocated_by_rnn:.2f}. " + f"Valor dos investimentos no EOD: {value_of_investments_at_eod:.2f}. " + f"Caixa não alocado: {cash_remaining_after_allocation:.2f}. " + f"Valor final total: {calculated_final_amount:.2f}") + + transactions_db[rnn_tx_id]["eod_portfolio_value_simulated"] = value_of_investments_at_eod + + + + # 4. TOKENIZAÇÃO / REGISTRO DA OPERAÇÃO (Placeholder) + logger.info(f"BG TASK [{rnn_tx_id}]: Registrando (tokenizando) operação...") + # Aqui você implementaria sua lógica de tokenização. + # Poderia ser salvar em uma blockchain, ou um registro detalhado e imutável no seu DB. + # Ex: await tokenize_operation_async(rnn_tx_id, client_id, investment_decisions, execution_results) + await asyncio.sleep(2) + transactions_db[rnn_tx_id]["tokenization_status"] = "completed" + logger.info(f"BG TASK [{rnn_tx_id}]: Operação registrada/tokenizada.") + + transactions_db[rnn_tx_id]["status"] = "completed" + transactions_db[rnn_tx_id]["final_amount"] = calculated_final_amount + transactions_db[rnn_tx_id]["profit_loss"] = calculated_final_amount - amount + + except Exception as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante execução da estratégia: {str(e)}", exc_info=True) + final_status = "failed" + error_details = str(e) + transactions_db[rnn_tx_id]["status"] = "failed" + transactions_db[rnn_tx_id]["error"] = error_details + # Em caso de falha, o final_amount pode ser o inicial ou o que foi possível recuperar + calculated_final_amount = amount # Ou o valor parcial se algumas ordens falharam + + # 5. PREPARAR E ENVIAR CALLBACK PARA AIBANK + if not AIBANK_CALLBACK_URL or not CALLBACK_SHARED_SECRET: + logger.error(f"BG TASK [{rnn_tx_id}]: AIBANK_CALLBACK_URL ou CALLBACK_SHARED_SECRET não configurado. Não é possível enviar callback.") + transactions_db[rnn_tx_id]["callback_status"] = "config_missing" + return + + callback_payload = InvestmentResultPayload( + rnn_transaction_id=rnn_tx_id, + aibank_transaction_token=aibank_tx_token, + client_id=client_id, + initial_amount=amount, + final_amount=calculated_final_amount, + profit_loss=calculated_final_amount - amount, + status=final_status, + timestamp=datetime.utcnow(), + details=error_details if final_status == "failed" else "Investment cycle completed." + ) + + payload_json = callback_payload.model_dump_json() + + # Criar assinatura HMAC para segurança do callback + signature = hmac.new( + CALLBACK_SHARED_SECRET.encode('utf-8'), + payload_json.encode('utf-8'), + hashlib.sha256 + ).hexdigest() + + headers = { + 'Content-Type': 'application/json', + 'X-RNN-Signature': signature # Assinatura para o aibank verificar + } + + logger.info(f"BG TASK [{rnn_tx_id}]: Enviando callback para AIBank: {AIBANK_CALLBACK_URL}") + transactions_db[rnn_tx_id]["callback_status"] = "sending" + try: + async with httpx.AsyncClient() as client: + response = await client.post(AIBANK_CALLBACK_URL, content=payload_json, headers=headers, timeout=30.0) + response.raise_for_status() # Lança exceção para erros HTTP 4xx/5xx + logger.info(f"BG TASK [{rnn_tx_id}]: Callback enviado com sucesso para AIBank. Status: {response.status_code}") + transactions_db[rnn_tx_id]["callback_status"] = f"sent_success_{response.status_code}" + except httpx.RequestError as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao enviar callback para AIBank (RequestError): {str(e)}") + transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_request_error" + except httpx.HTTPStatusError as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro HTTP ao enviar callback para AIBank (HTTPStatusError): {e.response.status_code} - {e.response.text}") + transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_http_error_{e.response.status_code}" + except Exception as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro inesperado ao enviar callback: {str(e)}", exc_info=True) + transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_unknown_error" + + """ +# --- Endpoints da API --- +@app.post("/api/invest", + response_model=InvestmentResponse, + dependencies=[Depends(verify_aibank_key)]) +async def initiate_investment( + request_data: InvestmentRequest, + background_tasks: BackgroundTasks +): + """ + Endpoint para o AIBank iniciar um ciclo de investimento. + Responde rapidamente e executa a lógica pesada em background. + """ + logger.info(f"Requisição de investimento recebida para client_id: {request_data.client_id}, " + f"amount: {request_data.amount}, aibank_tx_token: {request_data.aibank_transaction_token}") + + rnn_tx_id = str(uuid.uuid4()) + + # Armazena informações iniciais da transação DB real para ser mais robusto + transactions_db[rnn_tx_id] = { + "rnn_transaction_id": rnn_tx_id, + "aibank_transaction_token": request_data.aibank_transaction_token, + "client_id": request_data.client_id, + "initial_amount": request_data.amount, + "status": "pending_background_processing", + "received_at": datetime.utcnow().isoformat(), + "callback_status": "not_sent_yet" + } + + # Adiciona a tarefa de longa duração ao background + background_tasks.add_task( + execute_investment_strategy_background, + rnn_tx_id, + request_data.client_id, + request_data.amount, + request_data.aibank_transaction_token + ) + + logger.info(f"Estratégia de investimento para rnn_tx_id: {rnn_tx_id} agendada para execução em background.") + return InvestmentResponse( + status="pending", + message="Investment request received and is being processed in the background. Await callback for results.", + rnn_transaction_id=rnn_tx_id + ) + +@app.get("/api/transaction_status/{rnn_tx_id}", response_class=JSONResponse) +async def get_transaction_status(rnn_tx_id: str): + """ Endpoint para verificar o status de uma transação (para debug/admin) """ + transaction = transactions_db.get(rnn_tx_id) + if not transaction: + raise HTTPException(status_code=404, detail="Transaction not found") + return transaction + + +# --- Dashboard (Existente, adaptado) --- +# Setup para arquivos estáticos e templates + +try: + app.mount("/static", StaticFiles(directory="rnn/static"), name="static") + templates = Environment(loader=FileSystemLoader("rnn/templates")) +except RuntimeError as e: + logger.warning(f"Não foi possível montar /static ou carregar templates: {e}. O dashboard pode não funcionar.") + templates = None # Para evitar erros se o loader falhar + +@app.get("/", response_class=HTMLResponse) +async def index(request: Request): + if not templates: + return HTMLResponse("

Dashboard indisponível

Configuração de templates/estáticos falhou.

") + + agora = datetime.now() + agentes_simulados = [ + # dados de agentes ... + ] + template = templates.get_template("index.html") + # Adicionar transações recentes ao contexto do template + recent_txs = list(transactions_db.values())[-5:] # Últimas 5 transações + return HTMLResponse(template.render(request=request, agentes=agentes_simulados, transactions=recent_txs)) + +# --- Imports para Background Task --- +import asyncio +import random + +# Função de logger dummy s +# class DummyLogger: +# def info(self, msg, *args, **kwargs): print(f"INFO: {msg}") +# def warning(self, msg, *args, **kwargs): print(f"WARNING: {msg}") +# def error(self, msg, *args, **kwargs): print(f"ERROR: {msg}", kwargs.get('exc_info')) + +# if __name__ == "__main__": # Para teste local +# # logger = DummyLogger() # se não tiver get_logger() +# # Configuração das variáveis de ambiente para teste local +# os.environ["AIBANK_API_KEY"] = "test_aibank_key_from_rnn_server" +# os.environ["AIBANK_CALLBACK_URL"] = "http://localhost:8001/api/rnn_investment_result_callback" # URL do aibank simulado +# os.environ["CALLBACK_SHARED_SECRET"] = "super_secret_for_callback_signing" +# # import uvicorn +# # uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/app-old-v1.py b/app-old-v1.py new file mode 100644 index 0000000000000000000000000000000000000000..a222bc66ef4df320e294bebc890281cc92a0b1f3 --- /dev/null +++ b/app-old-v1.py @@ -0,0 +1,901 @@ + + +import os +import uuid +import time +import hmac +import hashlib +import json +from datetime import datetime, timedelta +from typing import Dict, Any +import ccxt.async_support as ccxt + +import httpx # Para fazer chamadas HTTP assíncronas (para o callback) +from fastapi import FastAPI, Request, HTTPException, Depends, Header, BackgroundTasks +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from jinja2 import Environment, FileSystemLoader +from pydantic import BaseModel, Field + +from app. utils.logger import get_logger + +logger = get_logger() + +# --- Configuração Inicial e Variáveis de Ambiente (Secrets do Hugging Face) --- +AIBANK_API_KEY = os.environ.get("AIBANK_API_KEY") # Chave que o aibank usa para chamar esta API RNN +AIBANK_CALLBACK_URL = os.environ.get("AIBANK_CALLBACK_URL") # URL no aibank para onde esta API RNN enviará o resultado +CALLBACK_SHARED_SECRET = os.environ.get("CALLBACK_SHARED_SECRET") # Segredo para assinar/verificar o payload do callback + +# Chaves para serviços externos +MARKET_DATA_API_KEY = os.environ.get("MARKET_DATA_API_KEY") +EXCHANGE_API_KEY = os.environ.get("EXCHANGE_API_KEY") +EXCHANGE_API_SECRET = os.environ.get("EXCHANGE_API_SECRET") + +if not AIBANK_API_KEY: + logger.warning("AIBANK_API_KEY não configurada. A autenticação para /api/invest falhou.") +if not AIBANK_CALLBACK_URL: + logger.warning("AIBANK_CALLBACK_URL não configurada. O callback para o aibank falhou.") +if not CALLBACK_SHARED_SECRET: + logger.warning("CALLBACK_SHARED_SECRET não configurado. A segurança do callback está comprometida.") + + + +#CHAVES PARA CCXT (para Binance) +# configuradas como vriaveis no Hugging Face Space + +CCXT_EXCHANGE_ID = os.environ.get("CCXT_EXCHANGE_ID", "binance") +CCXT_API_KEY = os.environ.get("CCXT_API_KEY") +CCXT_API_SECRET = os.environ.get("CCXT_API_SECRET") +CCXT_API_PASSWORD = os.environ.get("CCXT_API_PASSWORD") # Opcional + +# Binance Testnet +CCXT_SANDBOX_MODE = os.environ.get("CCXT_SANDBOX_MODE", "false").lower() == "true" + + + + + +app = FastAPI(title="ATCoin Neural Agents - Investment API") + +# --- Middlewares --- +app.add_middleware( + CORSMiddleware, + allow_origins=[ + "http://localhost:3000", # URL desenvolvimento local + "http://aibank.app.br", # URL de produção + "https://*.aibank.app.br", # subdomínios + "https://*.hf.space" # HF Space + ], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# --- Simulação de Banco de Dados de Transações DEV --- +# Em produção MongoDB +transactions_db: Dict[str, Dict[str, Any]] = {} + +# --- Modelos Pydantic --- +class InvestmentRequest(BaseModel): + client_id: str + amount: float = Field(..., gt=0) # Garante que o montante seja positivo + aibank_transaction_token: str # Token único gerado pelo aibank para rastreamento + +class InvestmentResponse(BaseModel): + status: str + message: str + rnn_transaction_id: str # ID da transação this.API + +class InvestmentResultPayload(BaseModel): # Payload para o callback para o aibank + rnn_transaction_id: str + aibank_transaction_token: str + client_id: str + initial_amount: float + final_amount: float + profit_loss: float + status: str # "completed", "failed" + timestamp: datetime + details: str = "" + + +# --- Dependência de Autenticação --- +async def verify_aibank_key(authorization: str = Header(None)): + if not AIBANK_API_KEY: # Checagem se a chave do servidor está configurada + logger.error("CRITICAL: AIBANK_API_KEY (server-side) não está configurada nos Secrets.") + raise HTTPException(status_code=500, detail="Internal Server Configuration Error: Missing server API Key.") + + if authorization is None: + logger.warning("Authorization header ausente na chamada do AIBank.") + raise HTTPException(status_code=401, detail="Authorization header is missing") + + parts = authorization.split() + if len(parts) != 2 or parts[0].lower() != 'bearer': + logger.warning(f"Formato inválido do Authorization header: {authorization}") + raise HTTPException(status_code=401, detail="Authorization header must be 'Bearer '") + + token_from_aibank = parts[1] + if not hmac.compare_digest(token_from_aibank, AIBANK_API_KEY): + logger.warning(f"Chave de API inválida fornecida pelo AIBank. Token: {token_from_aibank[:10]}...") + raise HTTPException(status_code=403, detail="Invalid API Key provided by AIBank.") + logger.info("API Key do AIBank verificada com sucesso.") + return True + + +# --- Lógica de Negócio Principal (Simulada e em Background) --- + +async def execute_investment_strategy_background( + rnn_tx_id: str, + client_id: str, + amount: float, + aibank_tx_token: str +): + """ + Esta função roda em background. Simula a coleta de dados, RNN, execução e tokenização. + No final, chama o callback para o aibank. + """ + logger.info(f"BG TASK [{rnn_tx_id}]: Iniciando estratégia de investimento para cliente {client_id}, valor {amount}.") + transactions_db[rnn_tx_id]["status"] = "processing" # Status geral inicial + transactions_db[rnn_tx_id]["status_details"] = "Initializing investment cycle" + + final_status = "completed" # Status final presumido + error_details = "" + calculated_final_amount = amount # Valor inicial, será modificado + + # Inicializa o objeto da exchange ccxt + exchange = None + if CCXT_API_KEY and CCXT_API_SECRET: #nicializa se as chaves básicas estiverem presentes + try: + exchange_class = getattr(ccxt, CCXT_EXCHANGE_ID) + config = { + 'apiKey': CCXT_API_KEY, + 'secret': CCXT_API_SECRET, + 'enableRateLimit': True, # evitar bans da API + # 'verbose': True, # Para debug detalhado das chamadas ccxt + } + if CCXT_API_PASSWORD: + config['password'] = CCXT_API_PASSWORD + + exchange = exchange_class(config) + + if CCXT_SANDBOX_MODE: + if hasattr(exchange, 'set_sandbox_mode'): # Nem todas as exchanges suportam isso diretamente em ccxt + exchange.set_sandbox_mode(True) + logger.info(f"BG TASK [{rnn_tx_id}]: CCXT configurado para modo SANDBOX para {CCXT_EXCHANGE_ID}.") + elif 'test' in exchange.urls: # usar testnet another.way + exchange.urls['api'] = exchange.urls['test'] + logger.info(f"BG TASK [{rnn_tx_id}]: CCXT URLs alteradas para TESTNET para {CCXT_EXCHANGE_ID}.") + else: + logger.warning(f"BG TASK [{rnn_tx_id}]: Modo SANDBOX solicitado mas não explicitamente suportado por ccxt para {CCXT_EXCHANGE_ID} ou URL de teste não encontrada.") + + # Carregar mercados para validar símbolos, demorado. + # await exchange.load_markets() + # logger.info(f"BG TASK [{rnn_tx_id}]: Mercados carregados para {CCXT_EXCHANGE_ID}.") + + except AttributeError: + logger.error(f"BG TASK [{rnn_tx_id}]: Exchange ID '{CCXT_EXCHANGE_ID}' inválida ou não suportada pelo ccxt.") + error_details += f"Invalid CCXT_EXCHANGE_ID: {CCXT_EXCHANGE_ID}; " + final_status = "failed_config" # Um novo status para falha de configuração + # (Pular para o callback aqui) + except Exception as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao inicializar ccxt para {CCXT_EXCHANGE_ID}: {str(e)}", exc_info=True) + error_details += f"CCXT initialization error: {str(e)}; " + final_status = "failed_config" + # (Pular para o callback aqui) + else: + logger.warning(f"BG TASK [{rnn_tx_id}]: CCXT_API_KEY ou CCXT_API_SECRET não configurados. A coleta de dados de cripto e execução de ordens via ccxt serão puladas.") + #Decidir se é uma falha fatal ou se a estratégia pode prosseguir sem dados de cripto. + # Precisa lidar com dados ausentes. + + + # ========================================================================= + # 1. COLETAR DADOS DE MERCADO (com ccxt integrado) + # ========================================================================= + logger.info(f"BG TASK [{rnn_tx_id}]: Coletando dados de mercado...") + transactions_db[rnn_tx_id]["status_details"] = "Fetching market data" + market_data_results = { + "crypto": {}, # Dados específicos de cripto + "stocks": {}, # Dados de ações (integrar yfinance ou outro) + "other": {} # Outros dados + } + market_data_fetch_success = True + + # --- Defina os pares de cripto --- + # Estes poderiam vir de uma configuração, ou serem dinâmicos. + crypto_pairs_to_fetch = ["BTC/USDT", "ETH/USDT", "SOL/USDT"] + + if exchange: # Buscar se a exchange foi inicializada corretamente + try: + for pair in crypto_pairs_to_fetch: + pair_data = {} + if exchange.has['fetchTicker']: + ticker = await exchange.fetch_ticker(pair) + pair_data['ticker'] = { + 'last': ticker.get('last'), + 'bid': ticker.get('bid'), + 'ask': ticker.get('ask'), + 'volume': ticker.get('baseVolume'), # Ou 'quoteVolume' + 'timestamp': ticker.get('timestamp') + } + logger.info(f"BG TASK [{rnn_tx_id}]: Ticker {pair}: Preço {ticker.get('last')}") + + if exchange.has['fetchOHLCV']: + # timeframe: '1m', '5m', '15m', '1h', '4h', '1d', '1w', '1M' + # limit: número de candles + ohlcv = await exchange.fetch_ohlcv(pair, timeframe='1h', limit=72) # Últimas 72 horas + # Formato OHLCV: [timestamp, open, high, low, close, volume] + pair_data['ohlcv_1h'] = ohlcv + logger.info(f"BG TASK [{rnn_tx_id}]: Coletado {len(ohlcv)} candles OHLCV para {pair} (1h).") + + # Outros dados que ccxt: + # if exchange.has['fetchOrderBook']: + # order_book = await exchange.fetch_order_book(pair, limit=5) # Top 5 bids/asks + # pair_data['order_book_L1'] = { # Exemplo de Level 1 + # 'bids': order_book['bids'][0] if order_book['bids'] else None, + # 'asks': order_book['asks'][0] if order_book['asks'] else None, + # } + # if exchange.has['fetchTrades']: # Últimas negociações públicas + # trades = await exchange.fetch_trades(pair, limit=10) + # pair_data['recent_trades'] = trades + + market_data_results["crypto"][pair.replace("/", "_")] = pair_data # Usar _ para chaves de dict seguras + + logger.info(f"BG TASK [{rnn_tx_id}]: Dados de cripto via ccxt coletados.") + + except ccxt.NetworkError as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro de rede ccxt ao coletar dados de mercado: {str(e)}", exc_info=True) + market_data_fetch_success = False + error_details += f"CCXT NetworkError: {str(e)}; " + except ccxt.ExchangeError as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro da exchange ccxt ao coletar dados de mercado: {str(e)}", exc_info=True) + market_data_fetch_success = False + error_details += f"CCXT ExchangeError: {str(e)}; " + except Exception as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro geral ao coletar dados de cripto via ccxt: {str(e)}", exc_info=True) + market_data_fetch_success = False + error_details += f"General CCXT data collection error: {str(e)}; " + else: + logger.info(f"BG TASK [{rnn_tx_id}]: Instância da exchange ccxt não disponível. Pulando coleta de dados de cripto.") + # Se não há 'exchange', a coleta de dados de cripto não pode ocorrerá. + # Erro, dependendo se as chaves Not.ok. + if CCXT_API_KEY and CCXT_API_SECRET: # Chaves dadas mas a 'exchange' falhou na init + market_data_fetch_success = False # Falha se a config estava lá + error_details += "CCXT exchange object not initialized despite API keys being present; " + + + # --- Coleta de dados para outros tipos de ativos (Ações com yfinance) --- + # try: + # import yfinance as yf + # aapl = yf.Ticker("AAPL") + # hist_aapl = aapl.history(period="5d", interval="1h") # Últimos 5 dias, candles de 1h + # if not hist_aapl.empty: + # market_data_results["stocks"]["AAPL"] = { + # "ohlcv_1h": [[ts.timestamp() * 1000] + list(row) for ts, row in hist_aapl[['Open', 'High', 'Low', 'Close', 'Volume']].iterrows()] + # } + # logger.info(f"BG TASK [{rnn_tx_id}]: Coletado {len(hist_aapl)} candles para AAPL via yfinance.") + # except Exception as e: + # logger.warning(f"BG TASK [{rnn_tx_id}]: Falha ao buscar dados de ações com yfinance: {e}") + # + + + # --- Simulação para outros dados, se necessário --- + market_data_results["other"]['simulated_index_level'] = random.uniform(10000, 15000) + market_data_results["other"]['simulated_crypto_sentiment'] = random.uniform(-1, 1) + + if not market_data_fetch_success and (CCXT_API_KEY and CCXT_API_SECRET): # Se as chaves de cripto foram dadas e a coleta falhou + final_status = "failed_market_data" + logger.error(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado falhou criticamente. {error_details}") + # (Pular para o callback aqui, pois a RNN pode não ter dados suficientes) + # 'return' ou lógica de pular precisa ser implementada se a falha for fatal + # Registrar e permitir que a lógica da RNN tente lidar com dados ausentes/parciais + pass + + transactions_db[rnn_tx_id]["market_data_collected"] = market_data_results # Armazena para auditoria/debug + transactions_db[rnn_tx_id]["status_details"] = "Processing RNN analysis" + logger.info(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado concluída (sucesso: {market_data_fetch_success}).") + + # ========================================================================= + # 2. ANÁLISE PELA RNN E TOMADA DE DECISÃO + # ========================================================================= + # (alimentada por market_data_results) + # ... + logger.info(f"BG TASK [{rnn_tx_id}]: Executando análise RNN com os dados coletados...") + transactions_db[rnn_tx_id]["status_details"] = "Running RNN model" + investment_decisions = [] + rnn_analysis_success = True + + try: + + # A RNN precisará ser capaz de lidar com a estrutura de market_data_results, + # incluindo a possibilidade de alguns dados estarem ausentes se a coleta falhar. + + # investment_decisions = await rnn_model.predict_async(market_data_results, amount_to_invest=amount) + + # Simulação para prosseguir: + await asyncio.sleep(random.uniform(8, 15)) + if market_data_results["crypto"]: # Com dados de cripto + if random.random() > 0.1: + num_crypto_assets_to_invest = random.randint(1, len(crypto_pairs_to_fetch)) + chosen_pairs = random.sample(list(market_data_results["crypto"].keys()), k=num_crypto_assets_to_invest) + + for crypto_key in chosen_pairs: # crypto_key "BTC_USDT" + asset_symbol = crypto_key.replace("_", "/") # Converte de volta para "BTC/USDT" + allocated_amount = (amount / num_crypto_assets_to_invest) * random.uniform(0.7, 0.9) # Aloca uma parte do total + investment_decisions.append({ + "asset_id": asset_symbol, # Usa o símbolo da exchange + "type": "CRYPTO", + "action": "BUY", + "target_usd_amount": round(allocated_amount, 2), + "reasoning": f"RNN signal for {asset_symbol} based on simulated data and ticker {market_data_results['crypto'][crypto_key].get('ticker', {}).get('last', 'N/A')}" + }) + else: + logger.info(f"BG TASK [{rnn_tx_id}]: Sem dados de cripto para a RNN processar.") + + if not investment_decisions: + logger.info(f"BG TASK [{rnn_tx_id}]: RNN não gerou nenhuma decisão de investimento.") + else: + logger.info(f"BG TASK [{rnn_tx_id}]: RNN gerou {len(investment_decisions)} decisões: {investment_decisions}") + + except Exception as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante análise RNN: {str(e)}", exc_info=True) + rnn_analysis_success = False + error_details += f"RNN analysis failed: {str(e)}; " + + if not rnn_analysis_success: + final_status = "failed_rnn_analysis" + # (Pular para o callback) + pass + + transactions_db[rnn_tx_id]["rnn_decisions"] = investment_decisions + transactions_db[rnn_tx_id]["status_details"] = "Preparing to execute orders" + total_usd_allocated_by_rnn = sum(d['target_usd_amount'] for d in investment_decisions if d['action'] == 'BUY') + + + # ========================================================================= + # 3. EXECUÇÃO DE ORDENS (Será detalhado no próximo passo) + # ========================================================================= + # ... (execução de ordens aqui, usando `exchange` ccxt e `investment_decisions`) + # ... (Incluindo cálculo de `current_portfolio_value` e `cash_remaining_after_allocation`) + logger.info(f"BG TASK [{rnn_tx_id}]: Executando ordens baseadas nas decisões da RNN...") + transactions_db[rnn_tx_id]["status_details"] = "Executing investment orders" + executed_trades_info = [] + order_execution_success = True + cash_remaining_after_allocation = amount - total_usd_allocated_by_rnn + current_portfolio_value = 0 # Valor dos ativos comprados + + # Placeholder para execução + if investment_decisions: + for decision in investment_decisions: + if decision.get("action") == "BUY": + usd_amount_to_spend = decision.get("target_usd_amount") + simulated_cost = usd_amount_to_spend * random.uniform(0.99, 1.0) + executed_trades_info.append({ + "asset": decision.get("asset_id"), "order_id": f"sim_ord_{uuid.uuid4()}", + "status": "filled", "cost_usd": simulated_cost + }) + current_portfolio_value += simulated_cost + await asyncio.sleep(random.uniform(1,3) * len(investment_decisions)) # Simula tempo de execução + else: + cash_remaining_after_allocation = amount + + transactions_db[rnn_tx_id]["executed_trades"] = executed_trades_info + transactions_db[rnn_tx_id]["status_details"] = "Simulating holding period and profit/loss" + + + # ========================================================================= + # 4. SIMULAÇÃO DO PERÍODO DE INVESTIMENTO E CÁLCULO DE LUCRO/PERDA + # ========================================================================= + # ... (Inserir lógia ) + logger.info(f"BG TASK [{rnn_tx_id}]: Simulando período de investimento e fechamento de posições...") + await asyncio.sleep(random.uniform(5, 10)) + + if current_portfolio_value > 0: + profit_on_invested_part = current_portfolio_value * (0.042 * random.uniform(0.8, 1.2)) + value_of_investments_at_eod = current_portfolio_value + profit_on_invested_part + else: + value_of_investments_at_eod = 0 + profit_on_invested_part = 0 + + calculated_final_amount = value_of_investments_at_eod + cash_remaining_after_allocation + + logger.info(f"BG TASK [{rnn_tx_id}]: Valor inicial total: {amount:.2f}. " + f"Valor alocado para investimento: {total_usd_allocated_by_rnn:.2f}. " + f"Valor dos investimentos no EOD: {value_of_investments_at_eod:.2f}. " + f"Caixa não alocado: {cash_remaining_after_allocation:.2f}. " + f"Valor final total: {calculated_final_amount:.2f}") + + transactions_db[rnn_tx_id]["eod_portfolio_value_simulated"] = value_of_investments_at_eod + + # ========================================================================= + # 5. TOKENIZAÇÃO / REGISTRO DA OPERAÇÃO + # ========================================================================= + # ... (Inserir) + logger.info(f"BG TASK [{rnn_tx_id}]: Registrando (tokenizando) operação detalhadamente...") + # ... (código do hash proof) + await asyncio.sleep(1) + + # ========================================================================= + # 6. PREPARAR E ENVIAR CALLBACK PARA AIBANK + # ========================================================================= + # (Fechamento da conexão ccxt antes do callback, se a conexão foi aberta e bem sucedida) + if exchange and hasattr(exchange, 'close'): + try: + await exchange.close() + logger.info(f"BG TASK [{rnn_tx_id}]: Conexão ccxt com {CCXT_EXCHANGE_ID} fechada.") + except Exception as e: + logger.warning(f"BG TASK [{rnn_tx_id}]: Erro ao fechar conexão ccxt: {str(e)}") + + # (callback , usando `final_status`, `error_details`, `calculated_final_amount`) + # ... + if not AIBANK_CALLBACK_URL or not CALLBACK_SHARED_SECRET: + logger.error(f"BG TASK [{rnn_tx_id}]: AIBANK_CALLBACK_URL ou CALLBACK_SHARED_SECRET não configurado. Não é possível enviar callback.") + transactions_db[rnn_tx_id]["callback_status"] = "config_missing" + return # Não pode prosseguir sem config de callback + + callback_payload_data = InvestmentResultPayload( + rnn_transaction_id=rnn_tx_id, + aibank_transaction_token=aibank_tx_token, + client_id=client_id, + initial_amount=amount, + final_amount=calculated_final_amount, + profit_loss=calculated_final_amount - amount, + status=final_status, # O status final da operação + timestamp=datetime.utcnow(), + details=error_details if final_status != "completed" else "Investment cycle completed successfully." + ) + payload_json = callback_payload_data.model_dump_json() + signature = hmac.new(CALLBACK_SHARED_SECRET.encode('utf-8'), payload_json.encode('utf-8'), hashlib.sha256).hexdigest() + headers = {'Content-Type': 'application/json', 'X-RNN-Signature': signature} + + logger.info(f"BG TASK [{rnn_tx_id}]: Enviando callback para AIBank: {AIBANK_CALLBACK_URL} com status final '{final_status}'") + transactions_db[rnn_tx_id]["callback_status"] = "sending" + try: + async with httpx.AsyncClient() as client: + response = await client.post(AIBANK_CALLBACK_URL, content=payload_json, headers=headers, timeout=30.0) + response.raise_for_status() + logger.info(f"BG TASK [{rnn_tx_id}]: Callback enviado com sucesso para AIBank. Status da resposta: {response.status_code}") + transactions_db[rnn_tx_id]["callback_status"] = f"sent_success_{response.status_code}" + except Exception as e: # Captura mais genérica para erros de callback + logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao enviar callback para AIBank: {str(e)}", exc_info=True) + # Classificar o erro para o log + if isinstance(e, httpx.RequestError): + transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_request_error" + elif isinstance(e, httpx.HTTPStatusError): + transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_http_error_{e.response.status_code}" + else: + transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_unknown_error" + + + +# --- +""" async def execute_investment_strategy_background( + rnn_tx_id: str, + client_id: str, + amount: float, + aibank_tx_token: str +): + + logger.info(f"BG TASK [{rnn_tx_id}]: Iniciando estratégia de investimento para cliente {client_id}, valor {amount}.") + transactions_db[rnn_tx_id]["status"] = "processing_market_data" + + final_status = "completed" + error_details = "" + calculated_final_amount = amount # Valor inicial + + try: + # 1. COLETAR DADOS DE MERCADO (Placeholder) + + logger.info(f"BG TASK [{rnn_tx_id}]: Coletando dados de mercado...") + transactions_db[rnn_tx_id]["status_details"] = "Fetching market data" + market_data_results = {} + market_data_fetch_success = True + + # Exemplo para Cripto com ccxt (requer 'pip install ccxt') + # import ccxt.async_support as ccxt # Coloque no topo do app.py + # exchange_id = 'binance' # Exemplo + # exchange_class = getattr(ccxt, exchange_id) + # exchange = exchange_class({ + # 'apiKey': EXCHANGE_API_KEY, # Do os.environ + # 'secret': EXCHANGE_API_SECRET, # Do os.environ + # 'enableRateLimit': True, # Importante + # }) + + try: + # --- Exemplo para buscar dados de BTC/USDT --- + # if exchange.has['fetchTicker']: + # ticker_btc = await exchange.fetch_ticker('BTC/USDT') + # market_data_results['BTC_USDT_ticker'] = ticker_btc + # logger.info(f"BG TASK [{rnn_tx_id}]: Ticker BTC/USDT: {ticker_btc['last']}") + # if exchange.has['fetchOHLCV']: + # ohlcv_btc = await exchange.fetch_ohlcv('BTC/USDT', timeframe='1h', limit=100) # Últimas 100 horas + # market_data_results['BTC_USDT_ohlcv_1h'] = ohlcv_btc + # logger.info(f"BG TASK [{rnn_tx_id}]: Coletado {len(ohlcv_btc)} candles OHLCV para BTC/USDT 1h.") + + # --- Exemplo para Ações com yfinance (requer 'pip install yfinance') --- + # import yfinance as yf # Coloque no topo do app.py + # aapl = yf.Ticker("AAPL") + # hist_aapl = aapl.history(period="1mo") # Dados do último mês + # market_data_results['AAPL_history_1mo'] = hist_aapl.to_dict() # Pode ser grande, serialize com cuidado + # current_price_aapl = hist_aapl['Close'].iloc[-1] if not hist_aapl.empty else None + # market_data_results['AAPL_current_price'] = current_price_aapl + # logger.info(f"BG TASK [{rnn_tx_id}]: Preço atual AAPL (yfinance): {current_price_aapl}") + + # --- Placeholder para sua lógica real de coleta --- + # Você precisará definir QUAIS ativos e QUAIS dados são necessários para sua RNN. + # Este é um ponto crucial para sua estratégia. + # Simulação para prosseguir: + await asyncio.sleep(random.uniform(3, 7)) # Simula demora da coleta real + market_data_results['simulated_index_level'] = random.uniform(10000, 15000) + market_data_results['simulated_crypto_sentiment'] = random.uniform(-1, 1) + logger.info(f"BG TASK [{rnn_tx_id}]: Dados de mercado (simulados/reais) coletados.") + + except Exception as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao coletar dados de mercado: {str(e)}", exc_info=True) + market_data_fetch_success = False + error_details += f"Market data collection failed: {str(e)}; " + # Decida se a falha aqui é crítica e impede de continuar. + # Se sim, atualize final_status e pule para o callback. + + # finally: # Importante para fechar conexões de exchange em ccxt + # if 'exchange' in locals() and hasattr(exchange, 'close'): + # await exchange.close() + + if not market_data_fetch_success: + # Lógica para lidar com falha na coleta de dados (ex: não prosseguir) + final_status = "failed_market_data" + # ... (atualize transactions_db e pule para a seção de callback) ... + # (Este 'return' ou lógica de pular precisa ser implementada se a falha for fatal) + pass # Por ora, deixamos prosseguir com dados possivelmente incompletos ou apenas simulados + + transactions_db[rnn_tx_id]["market_data_collected"] = market_data_results # Armazene para auditoria/debug + transactions_db[rnn_tx_id]["status_details"] = "Processing RNN analysis" + + + # 2. ANÁLISE PELA RNN E TOMADA DE DECISÃO (Placeholder) + l + + logger.info(f"BG TASK [{rnn_tx_id}]: Executando análise RNN com os dados coletados...") + transactions_db[rnn_tx_id]["status_details"] = "Running RNN model" + investment_decisions = [] # Lista de decisões: {'asset': 'BTC/USDT', 'action': 'BUY', 'amount_usd': 5000, 'price_target': 70000} + rnn_analysis_success = True + + try: + # --- SUBSTITUA PELA CHAMADA AO SEU MODELO RNN --- + # Exemplo: Supondo que você tenha uma classe ou função rnn_predictor + # from rnn.models.predictor import rnn_predictor # Exemplo de import + + # Supondo que seu predictor precise dos dados de mercado e do montante a investir + # investment_decisions = await rnn_predictor.generate_signals_async( + # market_data_results, + # amount_to_invest=amount # O montante total disponível para este ciclo + # ) + + # Simulação para prosseguir: + await asyncio.sleep(random.uniform(8, 15)) # Simula processamento da RNN + if random.random() > 0.1: # 90% de chance de "decidir" investir + num_assets_to_invest = random.randint(1, 3) + for i in range(num_assets_to_invest): + asset_name = random.choice(["SIMULATED_CRYPTO_X", "SIMULATED_STOCK_Y", "SIMULATED_BOND_Z"]) + action = random.choice(["BUY", "HOLD"]) # Simplificado, sem SELL por enquanto + if action == "BUY": + # Alocar uma porção do 'amount' total para este ativo + allocated_amount = (amount / num_assets_to_invest) * random.uniform(0.8, 1.0) + investment_decisions.append({ + "asset_id": f"{asset_name}_{i}", + "type": "CRYPTO" if "CRYPTO" in asset_name else "STOCK", # Exemplo + "action": action, + "target_usd_amount": round(allocated_amount, 2), + "reasoning": "RNN signal strong based on simulated data" # Adicione o output real da RNN + }) + + if not investment_decisions: + logger.info(f"BG TASK [{rnn_tx_id}]: RNN não gerou nenhuma decisão de investimento (ou decidiu não investir).") + # Isso pode ser um resultado válido. + else: + logger.info(f"BG TASK [{rnn_tx_id}]: RNN gerou {len(investment_decisions)} decisões: {investment_decisions}") + + except Exception as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante análise RNN: {str(e)}", exc_info=True) + rnn_analysis_success = False + error_details += f"RNN analysis failed: {str(e)}; " + + if not rnn_analysis_success: + final_status = "failed_rnn_analysis" + # ... (atualize transactions_db e pule para a seção de callback) ... + pass + + transactions_db[rnn_tx_id]["rnn_decisions"] = investment_decisions + transactions_db[rnn_tx_id]["status_details"] = "Preparing to execute orders" + + # Antes de executar ordens, vamos calcular o valor que REALMENTE foi investido + # e o que pode ter sobrado, já que a RNN pode não usar todo o 'amount'. + total_usd_allocated_by_rnn = sum(d['target_usd_amount'] for d in investment_decisions if d['action'] == 'BUY') + # calculated_final_amount = amount # Inicializa com o montante original + # Esta variável será atualizada após a execução das ordens e cálculo do lucro/perda + + + # 3. EXECUÇÃO DE ORDENS (Placeholder) + + # Dentro de execute_investment_strategy_background, substituindo a seção de execução de ordens: + + logger.info(f"BG TASK [{rnn_tx_id}]: Executando ordens baseadas nas decisões da RNN...") + transactions_db[rnn_tx_id]["status_details"] = "Executing investment orders" + executed_trades_info = [] + order_execution_success = True + + # O calculated_final_amount começa como o 'amount' inicial. + # Vamos deduzir o que foi efetivamente usado para compras + # e depois adicionar os lucros/perdas. + # Por enquanto, vamos assumir que o investimento visa usar o 'total_usd_allocated_by_rnn'. + # E o restante do 'amount' não alocado fica como "cash". + cash_remaining_after_allocation = amount - total_usd_allocated_by_rnn + current_portfolio_value = 0 # Valor dos ativos comprados + + # --- Exemplo de lógica de execução para decisões de COMPRA (BUY) --- + if investment_decisions: # Apenas se houver decisões + # exchange_exec = ccxt.binance({'apiKey': EXCHANGE_API_KEY, 'secret': EXCHANGE_API_SECRET}) # Exemplo + try: + for decision in investment_decisions: + if decision.get("action") == "BUY": + asset_id_to_buy = decision.get("asset_id") # Ex: "BTC/USDT" ou um ID interno que mapeia para um símbolo + usd_amount_to_spend = decision.get("target_usd_amount") + + logger.info(f"BG TASK [{rnn_tx_id}]: Tentando comprar {usd_amount_to_spend} USD de {asset_id_to_buy}") + + # --- SUBSTITUA PELA LÓGICA REAL DE EXECUÇÃO NA EXCHANGE --- + # Exemplo com ccxt (precisa de mais detalhes como símbolo de mercado correto): + # symbol_on_exchange = convert_asset_id_to_exchange_symbol(asset_id_to_buy, exchange_exec.id) + # current_price = (await exchange_exec.fetch_ticker(symbol_on_exchange))['last'] + # amount_of_asset_to_buy = usd_amount_to_spend / current_price + + # order = await exchange_exec.create_market_buy_order(symbol_on_exchange, amount_of_asset_to_buy) + # logger.info(f"BG TASK [{rnn_tx_id}]: Ordem de compra para {asset_id_to_buy} enviada: {order['id']}") + # executed_trades_info.append({ + # "asset": asset_id_to_buy, + # "order_id": order['id'], + # "status": order.get('status', 'unknown'), + # "amount_filled": order.get('filled', 0), + # "avg_price": order.get('average', current_price), + # "cost_usd": order.get('cost', usd_amount_to_spend), # Custo real da ordem + # "fees": order.get('fee', {}), + # }) + # current_portfolio_value += order.get('cost', usd_amount_to_spend) # Adiciona o valor do ativo comprado + + # Simulação para prosseguir: + await asyncio.sleep(random.uniform(1, 3)) # Simula envio de ordem + simulated_order_id = f"sim_ord_{uuid.uuid4()}" + simulated_cost = usd_amount_to_spend * random.uniform(0.99, 1.0) # Slippage simulado + executed_trades_info.append({ + "asset": asset_id_to_buy, + "order_id": simulated_order_id, + "status": "filled", + "amount_filled": simulated_cost / random.uniform(100, 200), # Qtd de ativo simulada + "avg_price": random.uniform(100, 200), # Preço simulado + "cost_usd": simulated_cost, + "fees": {"currency": "USD", "cost": simulated_cost * 0.001} # Taxa simulada + }) + current_portfolio_value += simulated_cost + logger.info(f"BG TASK [{rnn_tx_id}]: Ordem simulada {simulated_order_id} para {asset_id_to_buy} preenchida, custo {simulated_cost:.2f} USD.") + else: + logger.info(f"BG TASK [{rnn_tx_id}]: Decisão '{decision.get('action')}' para {decision.get('asset_id')} não é uma compra, pulando execução por enquanto.") + + except Exception as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante execução de ordens: {str(e)}", exc_info=True) + order_execution_success = False + error_details += f"Order execution failed: {str(e)}; " + # finally: + # if 'exchange_exec' in locals() and hasattr(exchange_exec, 'close'): + # await exchange_exec.close() + else: + logger.info(f"BG TASK [{rnn_tx_id}]: Nenhuma decisão de investimento para executar.") + # Se não há decisões, o current_portfolio_value é 0 e o cash_remaining é todo o 'amount' + cash_remaining_after_allocation = amount + + if not order_execution_success: + final_status = "failed_order_execution" + # ... (atualize transactions_db e pule para a seção de callback) ... + # O valor do portfólio aqui pode ser parcial se algumas ordens falharam + pass + + transactions_db[rnn_tx_id]["executed_trades"] = executed_trades_info + transactions_db[rnn_tx_id]["status_details"] = "Simulating holding period and profit/loss" + + # --- SIMULAÇÃO DO PERÍODO DE INVESTIMENTO E CÁLCULO DE LUCRO/PERDA DIÁRIO --- + # Em um sistema real, você monitoraria as posições e as fecharia no final do dia. + # Ou, se for um investimento de mais longo prazo, apenas calcularia o valor atual do portfólio. + # Para o objetivo de 4.2% ao dia, é implícito que as posições são fechadas diariamente. + + logger.info(f"BG TASK [{rnn_tx_id}]: Simulando período de investimento e fechamento de posições...") + await asyncio.sleep(random.uniform(5, 10)) # Simula o dia passando + + # Supondo que todas as posições são vendidas no final do "dia" + # E o current_portfolio_value muda com base no mercado. + # Para simular o objetivo de 4.2% sobre o VALOR INVESTIDO (current_portfolio_value no momento da compra): + if current_portfolio_value > 0: # Se algo foi investido + profit_on_invested_part = current_portfolio_value * (0.042 * random.uniform(0.8, 1.2)) # Simula variação no lucro + value_of_investments_at_eod = current_portfolio_value + profit_on_invested_part + else: # Nada foi investido + value_of_investments_at_eod = 0 + profit_on_invested_part = 0 + + # O calculated_final_amount é o valor dos investimentos no fim do dia + o caixa que não foi alocado + calculated_final_amount = value_of_investments_at_eod + cash_remaining_after_allocation + + logger.info(f"BG TASK [{rnn_tx_id}]: Valor inicial total: {amount:.2f}. " + f"Valor alocado para investimento: {total_usd_allocated_by_rnn:.2f}. " + f"Valor dos investimentos no EOD: {value_of_investments_at_eod:.2f}. " + f"Caixa não alocado: {cash_remaining_after_allocation:.2f}. " + f"Valor final total: {calculated_final_amount:.2f}") + + transactions_db[rnn_tx_id]["eod_portfolio_value_simulated"] = value_of_investments_at_eod + + + + # 4. TOKENIZAÇÃO / REGISTRO DA OPERAÇÃO (Placeholder) + logger.info(f"BG TASK [{rnn_tx_id}]: Registrando (tokenizando) operação...") + # Aqui você implementaria sua lógica de tokenização. + # Poderia ser salvar em uma blockchain, ou um registro detalhado e imutável no seu DB. + # Ex: await tokenize_operation_async(rnn_tx_id, client_id, investment_decisions, execution_results) + await asyncio.sleep(2) + transactions_db[rnn_tx_id]["tokenization_status"] = "completed" + logger.info(f"BG TASK [{rnn_tx_id}]: Operação registrada/tokenizada.") + + transactions_db[rnn_tx_id]["status"] = "completed" + transactions_db[rnn_tx_id]["final_amount"] = calculated_final_amount + transactions_db[rnn_tx_id]["profit_loss"] = calculated_final_amount - amount + + except Exception as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante execução da estratégia: {str(e)}", exc_info=True) + final_status = "failed" + error_details = str(e) + transactions_db[rnn_tx_id]["status"] = "failed" + transactions_db[rnn_tx_id]["error"] = error_details + # Em caso de falha, o final_amount pode ser o inicial ou o que foi possível recuperar + calculated_final_amount = amount # Ou o valor parcial se algumas ordens falharam + + # 5. PREPARAR E ENVIAR CALLBACK PARA AIBANK + if not AIBANK_CALLBACK_URL or not CALLBACK_SHARED_SECRET: + logger.error(f"BG TASK [{rnn_tx_id}]: AIBANK_CALLBACK_URL ou CALLBACK_SHARED_SECRET não configurado. Não é possível enviar callback.") + transactions_db[rnn_tx_id]["callback_status"] = "config_missing" + return + + callback_payload = InvestmentResultPayload( + rnn_transaction_id=rnn_tx_id, + aibank_transaction_token=aibank_tx_token, + client_id=client_id, + initial_amount=amount, + final_amount=calculated_final_amount, + profit_loss=calculated_final_amount - amount, + status=final_status, + timestamp=datetime.utcnow(), + details=error_details if final_status == "failed" else "Investment cycle completed." + ) + + payload_json = callback_payload.model_dump_json() + + # Criar assinatura HMAC para segurança do callback + signature = hmac.new( + CALLBACK_SHARED_SECRET.encode('utf-8'), + payload_json.encode('utf-8'), + hashlib.sha256 + ).hexdigest() + + headers = { + 'Content-Type': 'application/json', + 'X-RNN-Signature': signature # Assinatura para o aibank verificar + } + + logger.info(f"BG TASK [{rnn_tx_id}]: Enviando callback para AIBank: {AIBANK_CALLBACK_URL}") + transactions_db[rnn_tx_id]["callback_status"] = "sending" + try: + async with httpx.AsyncClient() as client: + response = await client.post(AIBANK_CALLBACK_URL, content=payload_json, headers=headers, timeout=30.0) + response.raise_for_status() # Lança exceção para erros HTTP 4xx/5xx + logger.info(f"BG TASK [{rnn_tx_id}]: Callback enviado com sucesso para AIBank. Status: {response.status_code}") + transactions_db[rnn_tx_id]["callback_status"] = f"sent_success_{response.status_code}" + except httpx.RequestError as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao enviar callback para AIBank (RequestError): {str(e)}") + transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_request_error" + except httpx.HTTPStatusError as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro HTTP ao enviar callback para AIBank (HTTPStatusError): {e.response.status_code} - {e.response.text}") + transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_http_error_{e.response.status_code}" + except Exception as e: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro inesperado ao enviar callback: {str(e)}", exc_info=True) + transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_unknown_error" + + """ +# --- Endpoints da API --- +@app.post("/api/invest", + response_model=InvestmentResponse, + dependencies=[Depends(verify_aibank_key)]) +async def initiate_investment( + request_data: InvestmentRequest, + background_tasks: BackgroundTasks +): + """ + Endpoint para o AIBank iniciar um ciclo de investimento. + Responde rapidamente e executa a lógica pesada em background. + """ + logger.info(f"Requisição de investimento recebida para client_id: {request_data.client_id}, " + f"amount: {request_data.amount}, aibank_tx_token: {request_data.aibank_transaction_token}") + + rnn_tx_id = str(uuid.uuid4()) + + # Armazena informações iniciais da transação DB real para ser mais robusto + transactions_db[rnn_tx_id] = { + "rnn_transaction_id": rnn_tx_id, + "aibank_transaction_token": request_data.aibank_transaction_token, + "client_id": request_data.client_id, + "initial_amount": request_data.amount, + "status": "pending_background_processing", + "received_at": datetime.utcnow().isoformat(), + "callback_status": "not_sent_yet" + } + + # Adiciona a tarefa de longa duração ao background + background_tasks.add_task( + execute_investment_strategy_background, + rnn_tx_id, + request_data.client_id, + request_data.amount, + request_data.aibank_transaction_token + ) + + logger.info(f"Estratégia de investimento para rnn_tx_id: {rnn_tx_id} agendada para execução em background.") + return InvestmentResponse( + status="pending", + message="Investment request received and is being processed in the background. Await callback for results.", + rnn_transaction_id=rnn_tx_id + ) + +@app.get("/api/transaction_status/{rnn_tx_id}", response_class=JSONResponse) +async def get_transaction_status(rnn_tx_id: str): + """ Endpoint para verificar o status de uma transação (para debug/admin) """ + transaction = transactions_db.get(rnn_tx_id) + if not transaction: + raise HTTPException(status_code=404, detail="Transaction not found") + return transaction + + +# --- Dashboard (Existente, adaptado) --- +# Setup para arquivos estáticos e templates + +try: + app.mount("/static", StaticFiles(directory="rnn/static"), name="static") + templates = Environment(loader=FileSystemLoader("rnn/templates")) +except RuntimeError as e: + logger.warning(f"Não foi possível montar /static ou carregar templates: {e}. O dashboard pode não funcionar.") + templates = None # Para evitar erros se o loader falhar + +@app.get("/", response_class=HTMLResponse) +async def index(request: Request): + if not templates: + return HTMLResponse("

Dashboard indisponível

Configuração de templates/estáticos falhou.

") + + agora = datetime.now() + agentes_simulados = [ + # dados de agentes ... + ] + template = templates.get_template("index.html") + # Adicionar transações recentes ao contexto do template + recent_txs = list(transactions_db.values())[-5:] # Últimas 5 transações + return HTMLResponse(template.render(request=request, agentes=agentes_simulados, transactions=recent_txs)) + +# --- Imports para Background Task --- +import asyncio +import random + +# Função de logger dummy s +# class DummyLogger: +# def info(self, msg, *args, **kwargs): print(f"INFO: {msg}") +# def warning(self, msg, *args, **kwargs): print(f"WARNING: {msg}") +# def error(self, msg, *args, **kwargs): print(f"ERROR: {msg}", kwargs.get('exc_info')) + +# if __name__ == "__main__": # Para teste local +# # logger = DummyLogger() # se não tiver get_logger() +# # Configuração das variáveis de ambiente para teste local +# os.environ["AIBANK_API_KEY"] = "test_aibank_key_from_rnn_server" +# os.environ["AIBANK_CALLBACK_URL"] = "http://localhost:8001/api/rnn_investment_result_callback" # URL do aibank simulado +# os.environ["CALLBACK_SHARED_SECRET"] = "super_secret_for_callback_signing" +# # import uvicorn +# # uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..12bcaf927e6a55f3a954440237a49cf2aaf807ca --- /dev/null +++ b/app.py @@ -0,0 +1,750 @@ +# rnn/app.py + +import os +import uuid +import time +import hmac +import hashlib +import json +from datetime import datetime, timedelta +from typing import Dict, Any, List, Optional +from rnn.app.ccxt_utils import get_ccxt_exchange, fetch_crypto_data + +import httpx # Para fazer chamadas HTTP assíncronas (para o callback) +from fastapi import FastAPI, Request, HTTPException, Depends, Header, BackgroundTasks +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from jinja2 import Environment, FileSystemLoader +from pydantic import BaseModel, Field + +from rnn.app.model.rnn_predictor import RNNModelPredictor +from rnn.app.utils.logger import get_logger + + + +logger = get_logger() + +# --- Configuração Inicial e Variáveis de Ambiente (Secrets do Hugging Face) --- +AIBANK_API_KEY = os.environ.get("AIBANK_API_KEY") # Chave que o aibank usa para chamar esta API RNN +AIBANK_CALLBACK_URL = os.environ.get("AIBANK_CALLBACK_URL") # URL no aibank para onde esta API RNN enviará o resultado +CALLBACK_SHARED_SECRET = os.environ.get("CALLBACK_SHARED_SECRET") # Segredo para assinar/verificar o payload do callback + +# Chaves para serviços externos +MARKET_DATA_API_KEY = os.environ.get("MARKET_DATA_API_KEY") +EXCHANGE_API_KEY = os.environ.get("EXCHANGE_API_KEY") +EXCHANGE_API_SECRET = os.environ.get("EXCHANGE_API_SECRET") + +if not AIBANK_API_KEY: + logger.warning("AIBANK_API_KEY não configurada. A autenticação para /api/invest falhou.") +if not AIBANK_CALLBACK_URL: + logger.warning("AIBANK_CALLBACK_URL não configurada. O callback para o aibank falhou.") +if not CALLBACK_SHARED_SECRET: + logger.warning("CALLBACK_SHARED_SECRET não configurado. A segurança do callback está comprometida.") + + + + +app = FastAPI(title="ATCoin Neural Agents - Investment API") + +# --- Middlewares --- +app.add_middleware( + CORSMiddleware, + allow_origins=[ + "http://localhost:3000", # URL desenvolvimento local + "http://aibank.app.br", # URL de produção + "https://*.aibank.app.br", # subdomínios + "https://*.hf.space" # HF Space + ], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# --- Simulação de Banco de Dados de Transações DEV --- +# Em produção MongoDB +transactions_db: Dict[str, Dict[str, Any]] = {} + +# --- Modelos Pydantic --- +class InvestmentRequest(BaseModel): + client_id: str + amount: float = Field(..., gt=0) # Garante que o montante seja positivo + aibank_transaction_token: str # Token único gerado pelo aibank para rastreamento + +class InvestmentResponse(BaseModel): + status: str + message: str + rnn_transaction_id: str # ID da transação this.API + +class InvestmentResultPayload(BaseModel): # Payload para o callback para o aibank + rnn_transaction_id: str + aibank_transaction_token: str + client_id: str + initial_amount: float + final_amount: float + profit_loss: float + status: str # "completed", "failed" + timestamp: datetime + details: str = "" + + +# --- Dependência de Autenticação --- +async def verify_aibank_key(authorization: str = Header(None)): + if not AIBANK_API_KEY: # Checagem se a chave do servidor está configurada + logger.error("CRITICAL: AIBANK_API_KEY (server-side) não está configurada nos Secrets.") + raise HTTPException(status_code=500, detail="Internal Server Configuration Error: Missing server API Key.") + + if authorization is None: + logger.warning("Authorization header ausente na chamada do AIBank.") + raise HTTPException(status_code=401, detail="Authorization header is missing") + + parts = authorization.split() + if len(parts) != 2 or parts[0].lower() != 'bearer': + logger.warning(f"Formato inválido do Authorization header: {authorization}") + raise HTTPException(status_code=401, detail="Authorization header must be 'Bearer '") + + token_from_aibank = parts[1] + if not hmac.compare_digest(token_from_aibank, AIBANK_API_KEY): + logger.warning(f"Chave de API inválida fornecida pelo AIBank. Token: {token_from_aibank[:10]}...") + raise HTTPException(status_code=403, detail="Invalid API Key provided by AIBank.") + logger.info("API Key do AIBank verificada com sucesso.") + return True + + +# --- Lógica de Negócio Principal (Simulada e em Background) --- + + +async def execute_investment_strategy_background( + rnn_tx_id: str, + client_id: str, + amount: float, + aibank_tx_token: str +): + logger.info(f"BG TASK [{rnn_tx_id}]: Iniciando estratégia de investimento para cliente {client_id}, valor {amount}.") + transactions_db[rnn_tx_id]["status"] = "processing" + transactions_db[rnn_tx_id]["status_details"] = "Initializing investment cycle" + + final_status = "completed" + error_details = "" # Acumula mensagens de erro de várias etapas + calculated_final_amount = amount + + # Inicializa a exchange ccxt usando o utilitário + # O logger do app.py é passado para ccxt_utils para que os logs apareçam no mesmo stream + exchange = await get_ccxt_exchange(logger_instance=logger) # MODIFICADO + + if not exchange: + # get_ccxt_exchange já loga o erro. Se a exchange é crucial, podemos falhar aqui. + logger.warning(f"BG TASK [{rnn_tx_id}]: Falha ao inicializar a exchange. A estratégia pode não funcionar como esperado para cripto.") + # Se as chaves CCXT foram fornecidas no ambiente mas a exchange falhou, considere isso um erro de config. + if os.environ.get("CCXT_API_KEY") and os.environ.get("CCXT_API_SECRET"): + error_details += "Failed to initialize CCXT exchange despite API keys being present; " + final_status = "failed_config" + # (PULAR PARA CALLBACK - veja a seção de tratamento de erro crítico abaixo) + + # ========================================================================= + # 1. COLETAR DADOS DE MERCADO + # ========================================================================= + logger.info(f"BG TASK [{rnn_tx_id}]: Coletando dados de mercado...") + transactions_db[rnn_tx_id]["status_details"] = "Fetching market data" + market_data_results = {"crypto": {}, "stocks": {}, "other": {}} + critical_data_fetch_failed = False # Flag para falha crítica na coleta de dados + + # --- Coleta de dados de Cripto via ccxt_utils --- + if exchange: + crypto_pairs_to_fetch = ["BTC/USDT", "ETH/USDT", "SOL/USDT"] # Mantenha configurável + + crypto_data, crypto_fetch_ok, crypto_err_msg = await fetch_crypto_data( + exchange, + crypto_pairs_to_fetch, + logger_instance=logger + ) + market_data_results["crypto"] = crypto_data + if not crypto_fetch_ok: + error_details += f"Crypto data fetch issues: {crypto_err_msg}; " + # Decida se a falha na coleta de cripto é crítica + # Se for, defina critical_data_fetch_failed = True + if os.environ.get("CCXT_API_KEY"): # Se esperávamos dados de cripto + critical_data_fetch_failed = True + logger.error(f"BG TASK [{rnn_tx_id}]: Falha crítica na coleta de dados de cripto.") + else: + logger.info(f"BG TASK [{rnn_tx_id}]: Instância da exchange ccxt não disponível. Pulando coleta de dados de cripto.") + if os.environ.get("CCXT_API_KEY"): # Se esperávamos dados de cripto mas a exchange não inicializou + error_details += "CCXT exchange not initialized, crypto data skipped; " + critical_data_fetch_failed = True + + + # --- Coleta de dados para outros tipos de ativos (ex: Ações com yfinance) --- + # (Sua lógica yfinance aqui, se aplicável, similarmente atualizando market_data_results["stocks"]) + # try: + # import yfinance as yf # Mova para o topo do app.py se for usar + # # ... lógica yfinance ... + # except Exception as e_yf: + # logger.warning(f"BG TASK [{rnn_tx_id}]: Falha ao buscar dados de ações com yfinance: {e_yf}") + # error_details += f"YFinance data fetch failed: {str(e_yf)}; " + # # Decida se isso é crítico: critical_data_fetch_failed = True + + market_data_results["other"]['simulated_index_level'] = random.uniform(10000, 15000) # Mantém simulação + + transactions_db[rnn_tx_id]["market_data_collected"] = market_data_results + + # --- PONTO DE CHECAGEM PARA FALHA CRÍTICA NA COLETA DE DADOS --- + if critical_data_fetch_failed: + final_status = "failed_market_data" + logger.error(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado falhou criticamente. {error_details}") + # Pular para a seção de callback + # (A lógica de envio do callback precisa ser alcançada) + else: + logger.info(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado concluída.") + transactions_db[rnn_tx_id]["status_details"] = "Processing RNN analysis" + + + + + + # ========================================================================= + # 2. ANÁLISE PELA RNN E TOMADA DE DECISÃO + # ========================================================================= + investment_decisions: List[Dict[str, Any]] = [] + total_usd_allocated_by_rnn = 0.0 + loop = asyncio.get_running_loop() + + if final_status == "completed": + logger.info(f"BG TASK [{rnn_tx_id}]: Executando análise RNN...") + transactions_db[rnn_tx_id]["status_details"] = "Running RNN model" + rnn_analysis_success = True + + # CORRIGIDO: Acessando app.state.rnn_predictor + predictor: Optional[RNNModelPredictor] = getattr(app.state, 'rnn_predictor', None) + + try: + crypto_data_for_rnn = market_data_results.get("crypto", {}) + candidate_assets = [ + asset_key for asset_key, data in crypto_data_for_rnn.items() + if data and not data.get("error") and data.get("ohlcv_1h") # Apenas com dados válidos + ] + + # --- Parâmetros de Gerenciamento de Risco e Alocação (AJUSTE FINO É CRUCIAL) --- + # Risco total do portfólio para este ciclo (ex: não usar mais que 50% do capital total em novas posições) + MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE = 0.75 # Usar até 75% do 'amount' + + # Risco por ativo individual (percentual do 'amount' TOTAL) + MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL = 0.15 # Ex: máx 15% do capital total em UM ativo + MIN_ALLOCATION_PER_ASSET_PCT_OF_TOTAL = 0.02 # Ex: mín 2% do capital total para valer a pena + + MIN_USD_PER_ORDER = 25.00 # Mínimo de USD por ordem + MAX_CONCURRENT_POSITIONS = 4 # Máximo de posições abertas simultaneamente + + # Limiares de Confiança da RNN + CONFIDENCE_STRONG_BUY = 0.80 # Confiança para considerar uma alocação maior + CONFIDENCE_MODERATE_BUY = 0.65 # Confiança mínima para considerar uma alocação base + CONFIDENCE_WEAK_BUY = 0.55 # Confiança para uma alocação muito pequena ou nenhuma + + allocated_capital_this_cycle = 0.0 + + # Para diversificação, podemos querer limitar a avaliação ou dar pesos + # random.shuffle(candidate_assets) + + for asset_key in candidate_assets: + if len(investment_decisions) >= MAX_CONCURRENT_POSITIONS: + logger.info(f"BG TASK [{rnn_tx_id}]: Limite de {MAX_CONCURRENT_POSITIONS} posições concorrentes atingido.") + break + + # Verifica se já usamos o capital máximo para o ciclo + if allocated_capital_this_cycle >= amount * MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE: + logger.info(f"BG TASK [{rnn_tx_id}]: Limite de capital para o ciclo ({MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE*100}%) atingido.") + break + + asset_symbol = asset_key.replace("_", "/") + logger.info(f"BG TASK [{rnn_tx_id}]: RNN avaliando ativo: {asset_symbol}") + + signal, confidence_prob = await predictor.predict_for_asset( + crypto_data_for_rnn[asset_key], + loop=loop + ) + + if signal == 1 and confidence_prob is not None: # Sinal de COMPRA e confiança válida + target_usd_allocation = 0.0 + + if confidence_prob >= CONFIDENCE_STRONG_BUY: + # Alocação maior para sinais fortes + # Ex: entre 60% e 100% da alocação máxima permitida por ativo + alloc_factor = 0.6 + 0.4 * ((confidence_prob - CONFIDENCE_STRONG_BUY) / (1.0 - CONFIDENCE_STRONG_BUY + 1e-6)) + target_usd_allocation = (amount * MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL) * alloc_factor + reason = f"RNN STRONG BUY signal (Conf: {confidence_prob:.3f})" + elif confidence_prob >= CONFIDENCE_MODERATE_BUY: + # Alocação base para sinais moderados + # Ex: entre 30% e 60% da alocação máxima permitida por ativo + alloc_factor = 0.3 + 0.3 * ((confidence_prob - CONFIDENCE_MODERATE_BUY) / (CONFIDENCE_STRONG_BUY - CONFIDENCE_MODERATE_BUY + 1e-6)) + target_usd_allocation = (amount * MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL) * alloc_factor + reason = f"RNN MODERATE BUY signal (Conf: {confidence_prob:.3f})" + elif confidence_prob >= CONFIDENCE_WEAK_BUY: + # Alocação pequena para sinais fracos (ou nenhuma) + alloc_factor = 0.1 + 0.2 * ((confidence_prob - CONFIDENCE_WEAK_BUY) / (CONFIDENCE_MODERATE_BUY - CONFIDENCE_WEAK_BUY + 1e-6)) + target_usd_allocation = (amount * MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL) * alloc_factor + reason = f"RNN WEAK BUY signal (Conf: {confidence_prob:.3f})" + else: + logger.info(f"BG TASK [{rnn_tx_id}]: Sinal COMPRA para {asset_symbol} mas confiança ({confidence_prob:.3f}) abaixo do limiar WEAK_BUY ({CONFIDENCE_WEAK_BUY}). Pulando.") + continue + + # Garantir que a alocação não seja menor que a mínima permitida (percentual do total) + target_usd_allocation = max(target_usd_allocation, amount * MIN_ALLOCATION_PER_ASSET_PCT_OF_TOTAL) + + # Garantir que não exceda o capital restante disponível neste CICLO + capital_left_for_this_cycle = (amount * MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE) - allocated_capital_this_cycle + actual_usd_allocation = min(target_usd_allocation, capital_left_for_this_cycle) + + # Garantir que a ordem mínima em USD seja respeitada + if actual_usd_allocation < MIN_USD_PER_ORDER: + logger.info(f"BG TASK [{rnn_tx_id}]: Alocação final ({actual_usd_allocation:.2f}) para {asset_symbol} abaixo do mínimo de ordem ({MIN_USD_PER_ORDER}). Pulando.") + continue + + # Adicionar à lista de decisões + investment_decisions.append({ + "asset_id": asset_symbol, "type": "CRYPTO", "action": "BUY", + "target_usd_amount": round(actual_usd_allocation, 2), + "rnn_confidence": round(confidence_prob, 4), + "reasoning": reason + }) + allocated_capital_this_cycle += round(actual_usd_allocation, 2) + logger.info(f"BG TASK [{rnn_tx_id}]: Decisão: COMPRAR {actual_usd_allocation:.2f} USD de {asset_symbol}. {reason}") + + # ... (restante da lógica para signal 0 ou None) ... + except Exception as e: # Captura exceções da lógica da RNN + logger.error(f"BG TASK [{rnn_tx_id}]: Erro CRÍTICO durante análise/predição RNN: {str(e)}", exc_info=True) + rnn_analysis_success = False # Marca que a análise RNN falhou + error_details += f"Critical RNN analysis/prediction error: {str(e)}; " + + + total_usd_allocated_by_rnn = allocated_capital_this_cycle + + + + + if not predictor or not predictor.model: # Verifica se o preditor e o modelo interno existem + logger.warning(f"BG TASK [{rnn_tx_id}]: Instância do preditor RNN não disponível ou modelo interno não carregado. Pulando análise RNN.") + rnn_analysis_success = False + error_details += "RNN model/predictor not available for prediction; " + else: + try: + # ... (lógica de iteração sobre `candidate_assets` e chamada a `predictor.predict_for_asset` como na resposta anterior) + # ... (lógica de alocação de capital como na resposta anterior) + # Garantir que toda essa lógica está dentro deste bloco 'else' + crypto_data_for_rnn = market_data_results.get("crypto", {}) + candidate_assets = [ + asset_key for asset_key, data in crypto_data_for_rnn.items() + if data and not data.get("error") and data.get("ohlcv_1h") + ] + + MAX_RISK_PER_ASSET_PCT = 0.05 + MIN_USD_PER_ORDER = 20.00 + MAX_CONCURRENT_POSITIONS = 5 + CONFIDENCE_THRESHOLD_FOR_MAX_ALLOC = 0.85 + CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC = 0.60 + BASE_ALLOCATION_PCT_OF_TOTAL_CAPITAL = 0.10 + + allocated_capital_this_cycle = 0.0 + + for asset_key in candidate_assets: + if len(investment_decisions) >= MAX_CONCURRENT_POSITIONS: + logger.info(f"BG TASK [{rnn_tx_id}]: Limite de posições concorrentes ({MAX_CONCURRENT_POSITIONS}) atingido.") + break + if allocated_capital_this_cycle >= amount * 0.90: + logger.info(f"BG TASK [{rnn_tx_id}]: Limite de capital do ciclo atingido.") + break + + asset_symbol = asset_key.replace("_", "/") + logger.info(f"BG TASK [{rnn_tx_id}]: RNN avaliando ativo: {asset_symbol}") + + signal, confidence_prob = await predictor.predict_for_asset( + crypto_data_for_rnn[asset_key], + loop=loop + # window_size e expected_features serão os defaults de rnn_predictor.py + # ou podem ser passados explicitamente se você quiser variar por ativo + ) + + if signal == 1: + if confidence_prob is None or confidence_prob < CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC: + logger.info(f"BG TASK [{rnn_tx_id}]: Sinal COMPRA para {asset_symbol} mas confiança ({confidence_prob}) abaixo do mínimo {CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC}. Pulando.") + continue + + confidence_factor = 0.5 + if confidence_prob >= CONFIDENCE_THRESHOLD_FOR_MAX_ALLOC: + confidence_factor = 1.0 + elif confidence_prob > CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC: + confidence_factor = 0.5 + 0.5 * ( + (confidence_prob - CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC) / + (CONFIDENCE_THRESHOLD_FOR_MAX_ALLOC - CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC) + ) + + potential_usd_allocation = amount * BASE_ALLOCATION_PCT_OF_TOTAL_CAPITAL * confidence_factor + potential_usd_allocation = min(potential_usd_allocation, amount * MAX_RISK_PER_ASSET_PCT) + remaining_capital_for_cycle = amount - allocated_capital_this_cycle # Recalcula a cada iteração + actual_usd_allocation = min(potential_usd_allocation, remaining_capital_for_cycle) + + if actual_usd_allocation < MIN_USD_PER_ORDER: + logger.info(f"BG TASK [{rnn_tx_id}]: Alocação calculada ({actual_usd_allocation:.2f}) para {asset_symbol} abaixo do mínimo ({MIN_USD_PER_ORDER}). Pulando.") + continue + + investment_decisions.append({ + "asset_id": asset_symbol, "type": "CRYPTO", "action": "BUY", + "target_usd_amount": round(actual_usd_allocation, 2), + "rnn_confidence": round(confidence_prob, 4) if confidence_prob is not None else None, + "reasoning": f"RNN signal BUY for {asset_symbol} with confidence {confidence_prob:.2f}" + }) + allocated_capital_this_cycle += round(actual_usd_allocation, 2) + logger.info(f"BG TASK [{rnn_tx_id}]: Decisão: COMPRAR {actual_usd_allocation:.2f} USD de {asset_symbol} (Conf: {confidence_prob:.2f})") + + elif signal == 0: + logger.info(f"BG TASK [{rnn_tx_id}]: RNN sinal NÃO COMPRAR para {asset_symbol} (Conf: {confidence_prob:.2f if confidence_prob is not None else 'N/A'})") + else: + logger.warning(f"BG TASK [{rnn_tx_id}]: RNN não gerou sinal para {asset_symbol}.") + + if not investment_decisions: + logger.info(f"BG TASK [{rnn_tx_id}]: RNN não gerou decisões de COMPRA válidas após avaliação e alocação.") + + except Exception as e: # Captura exceções da lógica da RNN + logger.error(f"BG TASK [{rnn_tx_id}]: Erro CRÍTICO durante análise/predição RNN: {str(e)}", exc_info=True) + rnn_analysis_success = False # Marca que a análise RNN falhou + error_details += f"Critical RNN analysis/prediction error: {str(e)}; " + + if not rnn_analysis_success: # Se a flag foi setada para False + final_status = "failed_rnn_analysis" + + transactions_db[rnn_tx_id]["rnn_decisions"] = investment_decisions + + total_usd_allocated_by_rnn = allocated_capital_this_cycle + transactions_db[rnn_tx_id]["status_details"] = "Preparing to execute orders" + + + + + + + + + + + # ========================================================================= + # 3. EXECUÇÃO DE ORDENS (Só executa se a RNN não falhou e gerou ordens) + # ========================================================================= + executed_trades_info: List[Dict[str, Any]] = [] + current_portfolio_value = 0.0 # Valor dos ativos comprados, baseado no custo + cash_remaining_after_execution = amount # Começa com todo o montante + + if final_status == "completed" and investment_decisions and exchange: + logger.info(f"BG TASK [{rnn_tx_id}]: Executando {len(investment_decisions)} ordens...") + transactions_db[rnn_tx_id]["status_details"] = "Executing investment orders" + order_execution_overall_success = True + + # Placeholder para LÓGICA REAL DE EXECUÇÃO DE ORDENS (CREATE_ORDER_PLACEHOLDER) + # Esta seção precisa ser preenchida com: + # 1. Iterar sobre `investment_decisions`. + # 2. Para cada decisão de "BUY": + # a. Determinar o símbolo correto na exchange (ex: "BTC/USDT"). + # b. Obter o preço atual (ticker) para calcular a quantidade de ativo a comprar. + # `amount_of_asset = target_usd_amount / current_price_of_asset` + # c. Considerar saldo disponível na exchange (se estiver gerenciando isso). + # d. Criar a ordem via `await exchange.create_market_buy_order(symbol, amount_of_asset)` + # ou `create_limit_buy_order(symbol, amount_of_asset, limit_price)`. + # Para ordens limite, a RNN precisaria fornecer o `limit_price`. + # e. Tratar respostas da exchange (sucesso, falha, ID da ordem). + # `ccxt.InsufficientFunds`, `ccxt.InvalidOrder`, etc. + # f. Armazenar detalhes da ordem em `executed_trades_info`: + # { "asset_id": ..., "order_id_exchange": ..., "type": "market/limit", "side": "buy", + # "requested_usd_amount": ..., "asset_quantity_ordered": ..., + # "status_from_exchange": ..., "filled_quantity": ..., "average_fill_price": ..., + # "cost_in_usd": ..., "fees_paid": ..., "timestamp": ... } + # g. Atualizar `current_portfolio_value` com o `cost_in_usd` da ordem preenchida. + # h. Deduzir `cost_in_usd` de `cash_remaining_after_execution`. + # 3. Para decisões de "SELL" (se sua RNN gerar): + # a. Verificar se você possui o ativo (requer gerenciamento de portfólio). + # b. Criar ordem de venda. + # c. Atualizar `current_portfolio_value` e `cash_remaining_after_execution`. + + # Simulação atual: + for decision in investment_decisions: + if decision.get("action") == "BUY" and decision.get("type") == "CRYPTO": + asset_symbol = decision["asset_id"] + usd_to_spend = decision["target_usd_amount"] + + # Simular pequena chance de falha na ordem + if random.random() < 0.05: + logger.warning(f"BG TASK [{rnn_tx_id}]: Falha simulada ao executar ordem para {asset_symbol}.") + executed_trades_info.append({ + "asset_id": asset_symbol, "status": "failed_simulated", + "requested_usd_amount": usd_to_spend, "error": "Simulated exchange rejection" + }) + order_execution_overall_success = False # Marca que pelo menos uma falhou + continue # Pula para a próxima decisão + + # Simular slippage e custo + simulated_cost = usd_to_spend * random.uniform(0.995, 1.005) # +/- 0.5% slippage + + # Garantir que não estamos gastando mais do que o caixa restante + if simulated_cost > cash_remaining_after_execution: + simulated_cost = cash_remaining_after_execution # Gasta apenas o que tem + if simulated_cost < 1: # Se não há quase nada, não faz a ordem + logger.info(f"BG TASK [{rnn_tx_id}]: Saldo insuficiente ({cash_remaining_after_execution:.2f}) para ordem de {asset_symbol}, pulando.") + continue + + + if simulated_cost > 0: + current_portfolio_value += simulated_cost + cash_remaining_after_execution -= simulated_cost + executed_trades_info.append({ + "asset_id": asset_symbol, "order_id_exchange": f"sim_ord_{uuid.uuid4()}", + "type": "market", "side": "buy", + "requested_usd_amount": usd_to_spend, + "status_from_exchange": "filled", "cost_in_usd": round(simulated_cost, 2), + "timestamp": datetime.utcnow().isoformat() + }) + logger.info(f"BG TASK [{rnn_tx_id}]: Ordem simulada para {asset_symbol} (custo: {simulated_cost:.2f} USD) preenchida.") + + await asyncio.sleep(random.uniform(1, 2) * len(investment_decisions) if investment_decisions else 1) + + if not order_execution_overall_success: + error_details += "One or more orders failed during execution; " + # Decida se isso torna o status final 'failed_order_execution' ou se 'completed_with_partial_failure' + # final_status = "completed_with_partial_failure" # Exemplo de um novo status + + elif not exchange and investment_decisions: + logger.warning(f"BG TASK [{rnn_tx_id}]: Decisões de investimento geradas, mas a exchange não está disponível para execução.") + error_details += "Exchange not available for order execution; " + final_status = "failed_order_execution" # Se a execução é crítica + cash_remaining_after_execution = amount # Nada foi gasto + + transactions_db[rnn_tx_id]["executed_trades"] = executed_trades_info + transactions_db[rnn_tx_id]["cash_after_execution"] = round(cash_remaining_after_execution, 2) + transactions_db[rnn_tx_id]["portfolio_value_after_execution"] = round(current_portfolio_value, 2) + + + # ========================================================================= + # 4. SIMULAÇÃO DO PERÍODO DE INVESTIMENTO E CÁLCULO DE LUCRO/PERDA (Só se não houve falha crítica antes) + # ========================================================================= + value_of_investments_at_eod = current_portfolio_value # Começa com o valor de custo + + if final_status == "completed": # Ou "completed_with_partial_failure" + transactions_db[rnn_tx_id]["status_details"] = "Simulating EOD valuation" + logger.info(f"BG TASK [{rnn_tx_id}]: Simulando valorização do portfólio no final do dia...") + await asyncio.sleep(random.uniform(3, 7)) + + if current_portfolio_value > 0: + # Simular mudança de valor do portfólio. A meta de 4.2% é sobre o capital INVESTIDO. + # O lucro/perda é aplicado ao `current_portfolio_value` (o que foi efetivamente comprado). + daily_return_factor = 0.042 # A meta + simulated_performance_factor = random.uniform(0.7, 1.3) # Variação em torno da meta (pode ser prejuízo) + # Para ser mais realista, o fator de performance deveria ser algo como: + # random.uniform(-0.05, 0.08) -> -5% a +8% de retorno diário sobre o investido (ainda alto) + # E não diretamente ligado à meta de 4.2% + + # Ajuste para uma simulação de retorno mais plausível (ainda agressiva) + # Suponha que o retorno diário real possa variar de -3% a +5% sobre o investido + actual_daily_return_on_portfolio = random.uniform(-0.03, 0.05) + + profit_or_loss_on_portfolio = current_portfolio_value * actual_daily_return_on_portfolio + value_of_investments_at_eod = current_portfolio_value + profit_or_loss_on_portfolio + logger.info(f"BG TASK [{rnn_tx_id}]: Portfólio inicial: {current_portfolio_value:.2f}, Retorno simulado: {actual_daily_return_on_portfolio*100:.2f}%, " + f"Lucro/Prejuízo no portfólio: {profit_or_loss_on_portfolio:.2f}, Valor EOD do portfólio: {value_of_investments_at_eod:.2f}") + else: + logger.info(f"BG TASK [{rnn_tx_id}]: Nenhum portfólio para valorizar no EOD (nada foi comprado).") + value_of_investments_at_eod = 0.0 + + # O calculated_final_amount é o valor dos investimentos liquidados + o caixa que não foi usado + calculated_final_amount = value_of_investments_at_eod + cash_remaining_after_execution + + else: # Se houve falha antes, o valor final é o que sobrou após a falha + calculated_final_amount = cash_remaining_after_execution + current_portfolio_value # current_portfolio_value pode ser 0 ou parcial + logger.warning(f"BG TASK [{rnn_tx_id}]: Ciclo de investimento não concluído normalmente ({final_status}). Valor final baseado no estado atual.") + + transactions_db[rnn_tx_id]["eod_portfolio_value_simulated"] = round(value_of_investments_at_eod, 2) + transactions_db[rnn_tx_id]["final_calculated_amount"] = round(calculated_final_amount, 2) + + + # ========================================================================= + # 5. TOKENIZAÇÃO / REGISTRO DA OPERAÇÃO (Só se não houve falha crítica antes) + # ========================================================================= + if final_status not in ["failed_config", "failed_market_data", "failed_rnn_analysis"]: # Prossegue se ao menos tentou executar + transactions_db[rnn_tx_id]["status_details"] = "Finalizing transaction log (tokenization)" + logger.info(f"BG TASK [{rnn_tx_id}]: Registrando (tokenizando) operação detalhadamente...") + # Placeholder para LÓGICA REAL DE TOKENIZAÇÃO (TOKENIZATION_PLACEHOLDER) + # 1. Coletar todos os dados relevantes da transação de `transactions_db[rnn_tx_id]` + # (market_data_collected, rnn_decisions, executed_trades, eod_portfolio_value_simulated, etc.) + # 2. Se for usar blockchain: + # a. Preparar os dados para um contrato inteligente. + # b. Interagir com o contrato (ex: web3.py para Ethereum). + # c. Armazenar o hash da transação da blockchain. + # 3. Se for um registro interno avançado: + # a. Assinar digitalmente os dados da transação. + # b. Armazenar em um sistema de log imutável ou banco de dados com auditoria. + + # Simulação atual (hash dos dados da transação): + transaction_data_for_hash = { + "rnn_tx_id": rnn_tx_id, "client_id": client_id, "initial_amount": amount, + "final_amount_calculated": calculated_final_amount, + # Incluir resumos ou hashes dos dados coletados para não tornar o hash gigante + "market_data_summary_keys": list(transactions_db[rnn_tx_id].get("market_data_collected", {}).keys()), + "rnn_decisions_count": len(transactions_db[rnn_tx_id].get("rnn_decisions", [])), + "executed_trades_count": len(transactions_db[rnn_tx_id].get("executed_trades", [])), + "eod_portfolio_value": transactions_db[rnn_tx_id].get("eod_portfolio_value_simulated"), + "timestamp": datetime.utcnow().isoformat() + } + ordered_tx_data_str = json.dumps(transaction_data_for_hash, sort_keys=True) + proof_token_hash = hashlib.sha256(ordered_tx_data_str.encode('utf-8')).hexdigest() + + transactions_db[rnn_tx_id]["proof_of_operation_token"] = proof_token_hash + transactions_db[rnn_tx_id]["tokenization_method"] = "internal_summary_hash_proof" + await asyncio.sleep(0.5) # Simula tempo de escrita/hash + logger.info(f"BG TASK [{rnn_tx_id}]: Operação registrada. Prova (hash): {proof_token_hash[:10]}...") + + + # ========================================================================= + # 6. PREPARAR E ENVIAR CALLBACK PARA AIBANK + # ========================================================================= + if exchange and hasattr(exchange, 'close'): + try: + await exchange.close() + logger.info(f"BG TASK [{rnn_tx_id}]: Conexão ccxt fechada.") + except Exception as e_close: # Especificar o tipo de exceção se souber + logger.warning(f"BG TASK [{rnn_tx_id}]: Erro ao fechar conexão ccxt: {str(e_close)}") + + if not AIBANK_CALLBACK_URL or not CALLBACK_SHARED_SECRET: + logger.error(f"BG TASK [{rnn_tx_id}]: Configuração de callback ausente. Não é possível notificar o AIBank.") + transactions_db[rnn_tx_id]["callback_status"] = "config_missing_critical" + return + + # Certifique-se que `final_status` reflete o estado real da operação + # Se `error_details` não estiver vazio e `final_status` ainda for "completed", ajuste-o + if error_details and final_status == "completed": + final_status = "completed_with_warnings" # Ou um status mais apropriado + + callback_payload_data = InvestmentResultPayload( + rnn_transaction_id=rnn_tx_id, aibank_transaction_token=aibank_tx_token, client_id=client_id, + initial_amount=amount, final_amount=round(calculated_final_amount, 2), # Arredonda para 2 casas decimais + profit_loss=round(calculated_final_amount - amount, 2), + status=final_status, timestamp=datetime.utcnow(), + details=error_details if error_details else "Investment cycle processed." + ) + payload_json_str = callback_payload_data.model_dump_json() # Garante que está usando a string serializada + + signature = hmac.new(CALLBACK_SHARED_SECRET.encode('utf-8'), payload_json_str.encode('utf-8'), hashlib.sha256).hexdigest() + headers = {'Content-Type': 'application/json', 'X-RNN-Signature': signature} + + logger.info(f"BG TASK [{rnn_tx_id}]: Enviando callback para AIBank ({AIBANK_CALLBACK_URL}) com status final '{final_status}'. Payload: {payload_json_str}") + transactions_db[rnn_tx_id]["callback_status"] = "sending" + try: + async with httpx.AsyncClient(timeout=30.0) as client: # Timeout global para o cliente + response = await client.post(AIBANK_CALLBACK_URL, content=payload_json_str, headers=headers) + response.raise_for_status() + logger.info(f"BG TASK [{rnn_tx_id}]: Callback para AIBank enviado com sucesso. Resposta: {response.status_code}") + transactions_db[rnn_tx_id]["callback_status"] = f"sent_success_{response.status_code}" + except httpx.RequestError as e_req: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro de REDE ao enviar callback para AIBank: {e_req}") + transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_network_error" + except httpx.HTTPStatusError as e_http: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro HTTP do AIBank ao receber callback: {e_http.response.status_code} - {e_http.response.text[:200]}") + transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_http_error_{e_http.response.status_code}" + except Exception as e_cb_final: + logger.error(f"BG TASK [{rnn_tx_id}]: Erro INESPERADO ao enviar callback: {e_cb_final}", exc_info=True) + transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_unknown_error" + + +import asyncio +import random + + + +# --- Endpoints da API --- +@app.post("/api/invest", + response_model=InvestmentResponse, + dependencies=[Depends(verify_aibank_key)]) +async def initiate_investment( + request_data: InvestmentRequest, + background_tasks: BackgroundTasks +): + """ + Endpoint para o AIBank iniciar um ciclo de investimento. + Responde rapidamente e executa a lógica pesada em background. + """ + logger.info(f"Requisição de investimento recebida para client_id: {request_data.client_id}, " + f"amount: {request_data.amount}, aibank_tx_token: {request_data.aibank_transaction_token}") + + rnn_tx_id = str(uuid.uuid4()) + + # Armazena informações iniciais da transação DB real para ser mais robusto + transactions_db[rnn_tx_id] = { + "rnn_transaction_id": rnn_tx_id, + "aibank_transaction_token": request_data.aibank_transaction_token, + "client_id": request_data.client_id, + "initial_amount": request_data.amount, + "status": "pending_background_processing", + "received_at": datetime.utcnow().isoformat(), + "callback_status": "not_sent_yet" + } + + # Adiciona a tarefa de longa duração ao background + background_tasks.add_task( + execute_investment_strategy_background, + rnn_tx_id, + request_data.client_id, + request_data.amount, + request_data.aibank_transaction_token + ) + + logger.info(f"Estratégia de investimento para rnn_tx_id: {rnn_tx_id} agendada para execução em background.") + return InvestmentResponse( + status="pending", + message="Investment request received and is being processed in the background. Await callback for results.", + rnn_transaction_id=rnn_tx_id + ) + +@app.get("/api/transaction_status/{rnn_tx_id}", response_class=JSONResponse) +async def get_transaction_status(rnn_tx_id: str): + """ Endpoint para verificar o status de uma transação (para debug/admin) """ + transaction = transactions_db.get(rnn_tx_id) + if not transaction: + raise HTTPException(status_code=404, detail="Transaction not found") + return transaction + + +# --- Dashboard (Existente, adaptado) --- +# Setup para arquivos estáticos e templates + +try: + app.mount("/static", StaticFiles(directory="rnn/static"), name="static") + templates = Environment(loader=FileSystemLoader("rnn/templates")) +except RuntimeError as e: + logger.warning(f"Não foi possível montar /static ou carregar templates: {e}. O dashboard pode não funcionar.") + templates = None # Para evitar erros se o loader falhar + +@app.get("/", response_class=HTMLResponse) +async def index(request: Request): + if not templates: + return HTMLResponse("

Dashboard indisponível

Configuração de templates/estáticos falhou.

") + + agora = datetime.now() + agentes_simulados = [ + # dados de agentes ... + ] + template = templates.get_template("index.html") + # Adicionar transações recentes ao contexto do template + recent_txs = list(transactions_db.values())[-5:] # Últimas 5 transações + return HTMLResponse(template.render(request=request, agentes=agentes_simulados, transactions=recent_txs)) + +# --- Imports para Background Task --- +import asyncio +import random + +# Função de logger dummy +# class DummyLogger: +# def info(self, msg, *args, **kwargs): print(f"INFO: {msg}") +# def warning(self, msg, *args, **kwargs): print(f"WARNING: {msg}") +# def error(self, msg, *args, **kwargs): print(f"ERROR: {msg}", kwargs.get('exc_info')) + +# if __name__ == "__main__": # Para teste local +# # logger = DummyLogger() # se não tiver get_logger() +# # Configuração das variáveis de ambiente para teste local +# os.environ["AIBANK_API_KEY"] = "test_aibank_key_from_rnn_server" +# os.environ["AIBANK_CALLBACK_URL"] = "http://localhost:8001/api/rnn_investment_result_callback" # URL do aibank simulado +# os.environ["CALLBACK_SHARED_SECRET"] = "super_secret_for_callback_signing" +# # import uvicorn +# # uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/app/config/config.py b/app/config/config.py new file mode 100644 index 0000000000000000000000000000000000000000..147b38998a9febebbd889b79dfd3bea87b869488 --- /dev/null +++ b/app/config/config.py @@ -0,0 +1,115 @@ +# --- Parâmetros de Configuração --- +# Dados +SYMBOL = 'ETH/USDT' # MUDAMOS PARA ETH/USDT PARA TESTE +MULTI_ASSET_SYMBOLS = { + 'crypto_eth': 'ETH-USD', # yfinance ticker para ETH/USD + 'crypto_ada': 'ADA-USD', # yfinance ticker para ADA/USD + 'stock_aapl': 'AAPL', # NASDAQ + 'stock_petr': 'PETR4.SA' # B3 +} # Use os tickers corretos para yfinance ou ccxt +TIMEFRAME_YFINANCE = '1h' # yfinance suporta '1m', '2m', '5m', '15m', '30m', '60m', '90m', '1h', '1d', '5d', '1wk', '1mo', '3mo' +# Para '1h', yfinance só retorna os últimos 730 dias. Para mais dados, use '1d'. +# Se usar ccxt, TIMEFRAME = '1h' como antes. +DAYS_TO_FETCH = 365 * 2 # 2 anos + +# Lista das features base que você quer calcular para CADA ativo +# (as 19 que definimos antes) +INDIVIDUAL_ASSET_BASE_FEATURES = [ + 'open', 'high', 'low', 'close', 'volume', # OHLCV originais são necessários para os cálculos + 'sma_10', 'rsi_14', 'macd', 'macds', 'atr', 'bbp', 'cci_37', 'mfi_37', 'adx_14', + 'volume_zscore', 'body_size', 'body_size_norm_atr', 'body_vs_avg_body', + 'log_return', 'buy_condition_v1', # 'sma_50' é calculada dentro de buy_condition_v1 + # As colunas _div_atr serão criadas a partir destas +] + +# Features que serão normalizadas pelo ATR +COLS_TO_NORM_BY_ATR = ['open', 'high', 'low', 'close', 'volume', 'sma_10', 'macd', 'body_size'] + + +TIMEFRAME = '1h' +DAYS_OF_DATA_TO_FETCH = 365 * 2 +LIMIT_PER_FETCH = 1000 + +# Features e Janela +WINDOW_SIZE = 60 + +# BASE_FEATURE_COLS define as features *originais* ou *derivadas diretamente dos dados brutos* +# que serão usadas ANTES do escalonamento específico para o modelo no script de treino, +# e também as features que o rnn_predictor.py precisará calcular/ter ANTES de aplicar SEUS scalers. +BASE_FEATURE_COLS = [ + 'open_div_atr', # Feature 1 (Preço normalizado) + 'high_div_atr', # Feature 2 (Preço normalizado) + 'low_div_atr', # Feature 3 (Preço normalizado) + 'close_div_atr', # Feature 4 (Preço normalizado) + 'volume_div_atr', # Feature 5 (Volume normalizado) + 'log_return', # Feature 6 (Momento) + 'rsi_14', # Feature 7 (Oscilador) + 'atr', # Feature 8 (Volatilidade - será escalada) + 'bbp', # Feature 9 (%B - será escalado) + 'cci_37', # Feature 10 (Oscilador - será escalado) + 'mfi_37', # Feature 11 (Volume/Oscilador - será escalado) + 'body_size_norm_atr', # Feature 12 (Candle normalizado) + 'body_vs_avg_body', # Feature 13 (Candle relativo) + 'macd', # Feature 14 (Linha MACD - será escalada) + 'sma_10_div_atr', # Feature 15 (Preço normalizado) + 'adx_14', # Feature 16 (Força da Tendência - será escalada) + 'volume_zscore', # Feature 17 (Volume relativo - será escalado) + 'buy_condition_v1', # Feature 18 (Condição Composta - binária, pode ou não ser escalada) + # 'cond_compra_v1', # Parece ser um duplicado de 'buy_condition_v1', remova se for. + # Se for diferente, mantenha, mas garanta que está sendo calculada. +] +# REMOVA as colunas _scaled de BASE_FEATURE_COLS. Elas são o *resultado* do escalonamento. +# BASE_FEATURE_COLS são as features ANTES do último passo de escalonamento para o modelo. + +# Vamos assumir que cond_compra_v1 e buy_condition_v1 são a mesma. +# Se forem diferentes, você precisará ajustar. +if 'cond_compra_v1' in BASE_FEATURE_COLS and 'buy_condition_v1' in BASE_FEATURE_COLS: + if 'cond_compra_v1' == 'buy_condition_v1': # Redundante se nomes iguais, mas para clareza + print("AVISO em config.py: 'cond_compra_v1' e 'buy_condition_v1' parecem ser a mesma feature. Verifique.") + # Decida qual manter ou se são realmente diferentes. Por ora, vou assumir que você quer ambas se estiverem listadas. + # Se forem a mesma, remova uma. Vou remover 'cond_compra_v1' se 'buy_condition_v1' for a oficial. + # BASE_FEATURE_COLS.remove('cond_compra_v1') # Exemplo + + +NUM_FEATURES = len(BASE_FEATURE_COLS) # ATUALIZADO AUTOMATICAMENTE + +# Alvo da Predição (Target) +PREDICTION_HORIZON = 5 +PRICE_CHANGE_THRESHOLD = 0.0075 # Você aumentou, OK. + +# Modelo RNN +LSTM_UNITS = [64, 64] # Boa escolha para mais features +DENSE_UNITS = 32 +DROPOUT_RATE = 0.3 # Bom para regularizar um modelo maior +LEARNING_RATE = 0.0005 # LR inicial para ReduceLROnPlateau +L2_REG = 0.0001 # Regularização L2 leve + +# Treinamento +BATCH_SIZE = 128 +EPOCHS = 100 # Deixe EarlyStopping controlar + +# Caminhos para Salvar +MODEL_SAVE_DIR = "app/model" +MODEL_NAME = "model.h5" +# Nomes de scaler mais descritivos que você sugeriu: +PRICE_VOL_SCALER_NAME = "price_volume_atr_norm_scaler.joblib" +INDICATOR_SCALER_NAME = "other_indicators_scaler.joblib" + +# EXPECTED_SCALED_FEATURES_FOR_MODEL define os nomes das colunas APÓS o escalonamento +# que são usadas para criar as sequências e alimentar o modelo no script de treino. +# E também o que o rnn_predictor.py DEVE produzir após aplicar seus scalers carregados. +EXPECTED_SCALED_FEATURES_FOR_MODEL = [f"{col}_scaled" for col in BASE_FEATURE_COLS] +# ^^^ IMPORTANTE: Esta linha assume que TODAS as features em BASE_FEATURE_COLS +# serão escaladas e terão o sufixo _scaled. +# Se 'rsi_14', 'bbp', 'buy_condition_v1' não forem escaladas ou tiverem +# outro tratamento, esta lista precisa ser ajustada manualmente. +# Exemplo: Se 'buy_condition_v1' é binária e não escalada: +# EXPECTED_SCALED_FEATURES_FOR_MODEL = [f"{col}_scaled" for col in BASE_FEATURE_COLS if col != 'buy_condition_v1'] + ['buy_condition_v1'] +# Por simplicidade, vamos assumir que todas são escaladas por enquanto. +EXPECTED_FEATURES_ORDER = EXPECTED_SCALED_FEATURES_FOR_MODEL + +INDIVIDUAL_ASSET_BASE_FEATURES = EXPECTED_FEATURES_ORDER + +MODEL_SAVE_DIR = "app/model" +PRICE_VOL_SCALER_NAME = "price_volume_atr_norm_scaler.joblib" +INDICATOR_SCALER_NAME = "other_indicators_scaler.joblib" \ No newline at end of file diff --git a/app/dokerfile b/app/dokerfile new file mode 100644 index 0000000000000000000000000000000000000000..3cf6f1639da7a944ebbbee3b53fa0a315c5a03d3 --- /dev/null +++ b/app/dokerfile @@ -0,0 +1,10 @@ +FROM python:3.10-slim + +WORKDIR /app +COPY . . + +RUN pip install --no-cache-dir -r requirements.txt + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..b6465bd62c7e4fe2c1344772d17194115787f0bb --- /dev/null +++ b/app/main.py @@ -0,0 +1,21 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from app.utils.predict import predict_quality +import uvicorn + +app = FastAPI(title="CNN SP500 Predictor") + +class InputData(BaseModel): + data: list # lista de listas com as features + +@app.post("/predict") +def predict(input_data: InputData): + try: + prediction = predict_quality(input_data.data) + return {"prediction": prediction} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +# Descomente se for rodar diretamente +# if __name__ == "__main__": +# uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/app/model/Treinamento_v2.md b/app/model/Treinamento_v2.md new file mode 100644 index 0000000000000000000000000000000000000000..ebf02e95b7bad81b7d7ded2b958daa75b80ee034 --- /dev/null +++ b/app/model/Treinamento_v2.md @@ -0,0 +1,162 @@ + python train_rnn_model.py +2025-06-07 07:39:59.310646: I external/local_xla/xla/tsl/cuda/cudart_stub.cc:32] Could not find cuda drivers on your machine, GPU will not be used. +2025-06-07 07:39:59.321958: I external/local_xla/xla/tsl/cuda/cudart_stub.cc:32] Could not find cuda drivers on your machine, GPU will not be used. +2025-06-07 07:39:59.371907: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:467] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered +WARNING: All log messages before absl::InitializeLog() is called are written to STDERR +E0000 00:00:1749292799.459426 15662 cuda_dnn.cc:8579] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered +E0000 00:00:1749292799.484280 15662 cuda_blas.cc:1407] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered +W0000 00:00:1749292799.543487 15662 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once. +W0000 00:00:1749292799.543564 15662 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once. +W0000 00:00:1749292799.543574 15662 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once. +W0000 00:00:1749292799.543582 15662 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once. +/home/verticalagent/dev/Atcoin-token/.venv/lib/python3.12/site-packages/pandas_ta/__init__.py:7: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81. + from pkg_resources import get_distribution, DistributionNotFound +2025-06-07 07:40:09.968679: E external/local_xla/xla/stream_executor/cuda/cuda_platform.cc:51] failed call to cuInit: INTERNAL: CUDA error: Failed call to cuInit: UNKNOWN ERROR (303) +Iniciando script de treinamento da RNN... +Buscando dados para BTC/USDT na binance com timeframe 1h... +/home/verticalagent/dev/Atcoin-token/rnn/scripts/train_rnn_model.py:65: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). + since = exchange.parse8601((datetime.utcnow() - timedelta(days=days_to_fetch)).isoformat()) +Buscando 1000 candles desde 2023-06-08T10:40:10.023Z... +Coletados 1000 candles. Último timestamp: 2023-07-20T02:00:00.000Z. Total: 1000 +Buscando 1000 candles desde 2023-07-20T02:00:00.050Z... +Coletados 1000 candles. Último timestamp: 2023-08-30T18:00:00.000Z. Total: 2000 +Buscando 1000 candles desde 2023-08-30T18:00:00.050Z... +Coletados 1000 candles. Último timestamp: 2023-10-11T10:00:00.000Z. Total: 3000 +Buscando 1000 candles desde 2023-10-11T10:00:00.050Z... +Coletados 1000 candles. Último timestamp: 2023-11-22T02:00:00.000Z. Total: 4000 +Buscando 1000 candles desde 2023-11-22T02:00:00.050Z... +Coletados 1000 candles. Último timestamp: 2024-01-02T18:00:00.000Z. Total: 5000 +Buscando 1000 candles desde 2024-01-02T18:00:00.050Z... +Coletados 1000 candles. Último timestamp: 2024-02-13T10:00:00.000Z. Total: 6000 +Buscando 1000 candles desde 2024-02-13T10:00:00.050Z... +Coletados 1000 candles. Último timestamp: 2024-03-26T02:00:00.000Z. Total: 7000 +Buscando 1000 candles desde 2024-03-26T02:00:00.050Z... +Coletados 1000 candles. Último timestamp: 2024-05-06T18:00:00.000Z. Total: 8000 +Buscando 1000 candles desde 2024-05-06T18:00:00.050Z... +Coletados 1000 candles. Último timestamp: 2024-06-17T10:00:00.000Z. Total: 9000 +Buscando 1000 candles desde 2024-06-17T10:00:00.050Z... +Coletados 1000 candles. Último timestamp: 2024-07-29T02:00:00.000Z. Total: 10000 +Buscando 1000 candles desde 2024-07-29T02:00:00.050Z... +Coletados 1000 candles. Último timestamp: 2024-09-08T18:00:00.000Z. Total: 11000 +Buscando 1000 candles desde 2024-09-08T18:00:00.050Z... +Coletados 1000 candles. Último timestamp: 2024-10-20T10:00:00.000Z. Total: 12000 +Buscando 1000 candles desde 2024-10-20T10:00:00.050Z... +Coletados 1000 candles. Último timestamp: 2024-12-01T02:00:00.000Z. Total: 13000 +Buscando 1000 candles desde 2024-12-01T02:00:00.050Z... +Coletados 1000 candles. Último timestamp: 2025-01-11T18:00:00.000Z. Total: 14000 +Buscando 1000 candles desde 2025-01-11T18:00:00.050Z... +Coletados 1000 candles. Último timestamp: 2025-02-22T10:00:00.000Z. Total: 15000 +Buscando 1000 candles desde 2025-02-22T10:00:00.050Z... +Coletados 1000 candles. Último timestamp: 2025-04-05T02:00:00.000Z. Total: 16000 +Buscando 1000 candles desde 2025-04-05T02:00:00.050Z... +Coletados 1000 candles. Último timestamp: 2025-05-16T18:00:00.000Z. Total: 17000 +Buscando 1000 candles desde 2025-05-16T18:00:00.050Z... +Coletados 520 candles. Último timestamp: 2025-06-07T10:00:00.000Z. Total: 17520 +Total de 17520 candles OHLCV coletados para BTC/USDT. +Calculando indicadores técnicos... +Criando coluna alvo para predição... +Distribuição do Alvo: +target +0 0.748643 +1 0.251357 +Name: proportion, dtype: float64 +Escalonando features... +Criando sequências de dados... +Shape de X (sequências de entrada): (17441, 60, 4) +Shape de y (alvos): (17441,) +Dividindo dados em treino e teste... +Tamanho do conjunto de treino: 13952, Teste: 3489 +Construindo modelo LSTM... +/home/verticalagent/dev/Atcoin-token/.venv/lib/python3.12/site-packages/keras/src/layers/rnn/rnn.py:199: UserWarning: Do not pass an `input_shape`/`input_dim` argument to a layer. When using Sequential models, prefer using an `Input(shape)` object as the first layer in the model instead. + super().__init__(**kwargs) +Model: "sequential" +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓ +┃ Layer (type) ┃ Output Shape ┃ Param # ┃ +┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩ +│ lstm (LSTM) │ (None, 60, 64) │ 17,664 │ +├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤ +│ dropout (Dropout) │ (None, 60, 64) │ 0 │ +├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤ +│ lstm_1 (LSTM) │ (None, 32) │ 12,416 │ +├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤ +│ dropout_1 (Dropout) │ (None, 32) │ 0 │ +├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤ +│ dense (Dense) │ (None, 32) │ 1,056 │ +├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤ +│ dropout_2 (Dropout) │ (None, 32) │ 0 │ +├──────────────────────────────────────┼─────────────────────────────┼─────────────────┤ +│ dense_1 (Dense) │ (None, 1) │ 33 │ +└──────────────────────────────────────┴─────────────────────────────┴─────────────────┘ + Total params: 31,169 (121.75 KB) + Trainable params: 31,169 (121.75 KB) + Non-trainable params: 0 (0.00 B) +Iniciando treinamento do modelo... +Pesos de Classe para Treinamento: {0: 0.6682632436057093, 1: 1.9857671505835468} +Epoch 1/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 57s 116ms/step - accuracy: 0.4573 - loss: 0.6918 - val_accuracy: 0.4070 - val_loss: 0.6990 +Epoch 2/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 51s 116ms/step - accuracy: 0.4664 - loss: 0.6832 - val_accuracy: 0.4050 - val_loss: 0.7455 +Epoch 3/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 49s 113ms/step - accuracy: 0.4887 - loss: 0.6828 - val_accuracy: 0.4285 - val_loss: 0.6778 +Epoch 4/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 50s 114ms/step - accuracy: 0.4869 - loss: 0.6831 - val_accuracy: 0.4139 - val_loss: 0.6996 +Epoch 5/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 50s 114ms/step - accuracy: 0.4983 - loss: 0.6745 - val_accuracy: 0.5007 - val_loss: 0.6649 +Epoch 6/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 50s 115ms/step - accuracy: 0.4866 - loss: 0.6859 - val_accuracy: 0.6240 - val_loss: 0.6687 +Epoch 7/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 50s 115ms/step - accuracy: 0.5359 - loss: 0.6742 - val_accuracy: 0.5067 - val_loss: 0.6813 +Epoch 8/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 50s 114ms/step - accuracy: 0.5559 - loss: 0.6728 - val_accuracy: 0.6314 - val_loss: 0.6450 +Epoch 9/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 50s 115ms/step - accuracy: 0.5436 - loss: 0.6705 - val_accuracy: 0.5056 - val_loss: 0.6872 +Epoch 10/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 51s 117ms/step - accuracy: 0.5351 - loss: 0.6703 - val_accuracy: 0.4285 - val_loss: 0.7185 +Epoch 11/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 50s 115ms/step - accuracy: 0.5336 - loss: 0.6769 - val_accuracy: 0.6420 - val_loss: 0.6415 +Epoch 12/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 50s 115ms/step - accuracy: 0.5346 - loss: 0.6737 - val_accuracy: 0.5701 - val_loss: 0.6718 +Epoch 13/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 51s 116ms/step - accuracy: 0.5799 - loss: 0.6645 - val_accuracy: 0.5219 - val_loss: 0.6836 +Epoch 14/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 50s 116ms/step - accuracy: 0.5697 - loss: 0.6682 - val_accuracy: 0.6016 - val_loss: 0.6615 +Epoch 15/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 50s 114ms/step - accuracy: 0.5654 - loss: 0.6694 - val_accuracy: 0.6048 - val_loss: 0.6633 +Epoch 16/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 51s 116ms/step - accuracy: 0.5811 - loss: 0.6673 - val_accuracy: 0.5251 - val_loss: 0.6855 +Epoch 17/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 51s 116ms/step - accuracy: 0.5703 - loss: 0.6648 - val_accuracy: 0.6028 - val_loss: 0.6629 +Epoch 18/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 51s 116ms/step - accuracy: 0.6102 - loss: 0.6551 - val_accuracy: 0.6294 - val_loss: 0.6546 +Epoch 19/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 50s 114ms/step - accuracy: 0.5871 - loss: 0.6652 - val_accuracy: 0.6234 - val_loss: 0.6492 +Epoch 20/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 49s 113ms/step - accuracy: 0.5976 - loss: 0.6602 - val_accuracy: 0.6225 - val_loss: 0.6478 +Epoch 21/50 +436/436 ━━━━━━━━━━━━━━━━━━━━ 49s 112ms/step - accuracy: 0.5873 - loss: 0.6679 - val_accuracy: 0.5939 - val_loss: 0.6782 +Avaliando modelo no conjunto de teste... +Perda no Teste: 0.6415 +Acurácia no Teste: 0.6420 +110/110 ━━━━━━━━━━━━━━━━━━━━ 6s 48ms/step + +Relatório de Classificação no Conjunto de Teste: + precision recall f1-score support + + No Rise (0) 0.80 0.69 0.74 2611 + Rise (1) 0.35 0.50 0.41 878 + + accuracy 0.64 3489 + macro avg 0.58 0.60 0.58 3489 +weighted avg 0.69 0.64 0.66 3489 + + +Matriz de Confusão no Conjunto de Teste: +[[1799 812] + [ 437 441]] +Salvando modelo e scalers... +WARNING:absl:You are saving your model as an HDF5 file via `model.save()` or `keras.saving.save_model(model)`. This file format is considered legacy. We recommend using instead the native Keras format, e.g. `model.save('my_model.keras')` or `keras.saving.save_model(model, 'my_model.keras')`. +Modelo salvo em: app/model/model.h5 +Scaler de Preço/Volume salvo em: app/model/price_volume_scaler.joblib +Scaler de Indicadores salvo em: app/model/indicator_scaler.joblib +Gráfico do histórico de treinamento salvo em: app/model/training_history.png +Script de treinamento concluído. \ No newline at end of file diff --git a/app/model/confusion_matrix.png b/app/model/confusion_matrix.png new file mode 100644 index 0000000000000000000000000000000000000000..c2d4735442d43217f5cdc4a4e32e4c8b182259ad Binary files /dev/null and b/app/model/confusion_matrix.png differ diff --git a/app/model/model.h5 b/app/model/model.h5 new file mode 100644 index 0000000000000000000000000000000000000000..eab229b8a9d13ae0bd78bfba1e0bfe5c1b1e5323 --- /dev/null +++ b/app/model/model.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebeb93aba0df13d84138bd9b174b58b6961e9f721d90b841fa940a5e1bac43a4 +size 957128 diff --git a/app/model/new_predictor/rnn_predictor.py b/app/model/new_predictor/rnn_predictor.py new file mode 100644 index 0000000000000000000000000000000000000000..ea4c380bc4489a41518afe165f445c21a55b30c5 --- /dev/null +++ b/app/model/new_predictor/rnn_predictor.py @@ -0,0 +1,258 @@ + +# rnn_predictor.py + +import os +import numpy as np +import pandas as pd +import tensorflow as tf +from typing import Any, Optional, Dict, List, Tuple + +# Biblioteca para indicadores técnicos +try: + import pandas_ta as ta +except ImportError: + print("AVISO: Biblioteca pandas_ta não instalada. Indicadores técnicos não serão calculados.") + ta = None # Define como None se não puder importar + +# Biblioteca para carregar scalers +try: + import joblib +except ImportError: + print("AVISO: Biblioteca joblib não instalada. Scalers não poderão ser carregados de arquivos.") + joblib = None + +# Importar configurações +from config import ( + WINDOW_SIZE, NUM_FEATURES, EXPECTED_FEATURES_ORDER, + MODEL_SAVE_DIR, PRICE_VOL_SCALER_NAME, INDICATOR_SCALER_NAME, + BASE_FEATURE_COLS # Para determinar as colunas de escalonamento +) + +# Caminhos para os scalers salvos +SCALER_DIR = MODEL_SAVE_DIR # Agora vem do config +PRICE_VOL_SCALER_PATH = os.path.join(SCALER_DIR, PRICE_VOL_SCALER_NAME) +INDICATOR_SCALER_PATH = os.path.join(SCALER_DIR, INDICATOR_SCALER_NAME) + +def calculate_technical_indicators(ohlcv_df: pd.DataFrame, logger_instance) -> pd.DataFrame: + """ + Calcula indicadores técnicos a partir de um DataFrame OHLCV pandas. + """ + if ta is None: + logger_instance.warning("TA: pandas_ta não disponível, pulando cálculo de indicadores.") + return ohlcv_df # Retorna o DataFrame original + + df_with_indicators = ohlcv_df.copy() + try: + df_with_indicators.ta.sma(length=10, close=\'close\', append=True, col_names=(\'sma_10\')) + df_with_indicators.ta.rsi(length=14, close=\'close\', append=True, col_names=(\'rsi_14\')) + df_with_indicators.ta.macd(close=\'close\', append=True, col_names=(\'macd\', \'macdh\', \'macds\')) + df_with_indicators.ta.atr(length=14, append=True, col_names=(\'atr\',)) + df_with_indicators.ta.bbands(length=20, close=\'close\', append=True, col_names=(\'bbl\', \'bbm\', \'bbu\', \'bbb\', \'bbp\')) + + # Normalização por ATR (consistente com data_handler.py) + epsilon = 1e-6 # Ajustado para um valor um pouco maior para robustez + df_with_indicators[\'close_div_atr\'] = df_with_indicators[\'close\'] / (df_with_indicators[\'atr\'] + epsilon) + df_with_indicators[\'volume_div_atr\'] = df_with_indicators[\'volume\'] / (df_with_indicators[\'atr\'] + epsilon) + df_with_indicators[\'sma_10_div_atr\'] = df_with_indicators[\'sma_10\'] / (df_with_indicators[\'atr\'] + epsilon) + df_with_indicators[\'macd_div_atr\'] = df_with_indicators[\'macd\'] / (df_with_indicators[\'atr\'] + epsilon) + + logger_instance.info("TA: Indicadores SMA(10), RSI(14), MACD, ATR, BBANDS calculados.") + except Exception as e: + logger_instance.error(f"TA: Erro ao calcular indicadores técnicos: {e}", exc_info=True) + return ohlcv_df + + return df_with_indicators + +def preprocess_input_data_for_asset( + asset_market_data: Dict[str, Any], + window_size: int = WINDOW_SIZE, # Usa do config + expected_features_order: List[str] = EXPECTED_FEATURES_ORDER, # Usa do config + price_vol_scaler = None, + indicator_scaler = None, + logger_instance = None +) -> np.ndarray: + """ + Pré-processa os dados de mercado de UM ÚNICO ATIVO para o formato esperado pela RNN. + """ + if logger_instance is None: + import logging + logger_instance = logging.getLogger(__name__) + + ohlcv_raw = asset_market_data.get(\'ohlcv_1h\', []) + + if not ohlcv_raw or len(ohlcv_raw) < window_size: + min_data_needed = window_size + 15 # Exemplo: SMA10 e RSI14 precisam de alguns pontos antes da janela de 60 + if len(ohlcv_raw) < min_data_needed: + logger_instance.warning(f"Preprocessing: Dados OHLCV insuficientes para {asset_market_data.get(\'symbol\', \'ativo\')}. " + f"Necessário (para janela+indicadores): ~{min_data_needed}, Disponível: {len(ohlcv_raw)}") + return np.array([]) + + try: + ohlcv_df = pd.DataFrame(ohlcv_raw, columns=[\'timestamp\', \'open\', \'high\', \'low\', \'close\', \'volume\']) + ohlcv_df.set_index(pd.to_datetime(ohlcv_df[\'timestamp\'], unit=\'ms\'), inplace=True) + ohlcv_df.drop(\'timestamp\', axis=1, inplace=True) + except Exception as e_df: + logger_instance.error(f"Preprocessing: Erro ao criar DataFrame OHLCV: {e_df}", exc_info=True) + return np.array([]) + + df_with_ta = calculate_technical_indicators(ohlcv_df, logger_instance) + df_with_ta.dropna(inplace=True) + + if len(df_with_ta) < window_size: + logger_instance.warning(f"Preprocessing: Dados insuficientes para {asset_market_data.get(\'symbol\', \'ativo\')} após TA e dropna. " + f"Necessário: {window_size}, Disponível: {len(df_with_ta)}") + return np.array([]) + + final_window_df = df_with_ta.tail(window_size).copy() + + scaled_df = pd.DataFrame(index=final_window_df.index) + + # Definir as colunas para cada scaler, consistente com train_rnn_model.py + price_volume_cols_for_scaler = [col for col in BASE_FEATURE_COLS if \'close\' in col or \'volume\' in col] + indicator_cols_for_scaler = [col for col in BASE_FEATURE_COLS if col not in price_volume_cols_for_scaler] + + # Scaler para Preço/Volume + if all(col in final_window_df.columns for col in price_volume_cols_for_scaler): + if price_vol_scaler: + try: + scaled_pv_data = price_vol_scaler.transform(final_window_df[price_volume_cols_for_scaler]) + for i, col_name in enumerate(price_volume_cols_for_scaler): + scaled_df[col_name.replace(\'_div_atr\', \'_scaled\')] = scaled_pv_data[:, i] + logger_instance.info("Preprocessing: Features de preço/volume escaladas.") + except Exception as e_pv_scale: + logger_instance.error(f"Preprocessing: Erro ao escalar features de preço/volume: {e_pv_scale}", exc_info=True) + return np.array([]) + else: + logger_instance.warning("Preprocessing: Scaler de Preço/Volume não fornecido. Usando dados originais (NÃO RECOMENDADO).") + for col_name in price_volume_cols_for_scaler: + scaled_df[col_name.replace(\'_div_atr\', \'_scaled\')] = final_window_df[col_name] + else: + logger_instance.error(f"Preprocessing: Colunas de preço/volume {price_volume_cols_for_scaler} não encontradas.") + return np.array([]) + + # Scaler para Indicadores + if all(col in final_window_df.columns for col in indicator_cols_for_scaler): + if indicator_scaler: + try: + if final_window_df[indicator_cols_for_scaler].isnull().values.any(): + logger_instance.error(f"Preprocessing: NaNs encontrados nas colunas de indicadores {indicator_cols_for_scaler} ANTES de escalar.") + return np.array([]) + + scaled_ind_data = indicator_scaler.transform(final_window_df[indicator_cols_for_scaler]) + for i, col_name in enumerate(indicator_cols_for_scaler): + scaled_df[col_name.replace(\'_div_atr\', \'_scaled\')] = scaled_ind_data[:, i] + logger_instance.info("Preprocessing: Features de indicadores escaladas.") + except Exception as e_ind_scale: + logger_instance.error(f"Preprocessing: Erro ao escalar features de indicadores: {e_ind_scale}", exc_info=True) + return np.array([]) + else: + logger_instance.warning("Preprocessing: Scaler de Indicadores não fornecido. Usando dados originais (NÃO RECOMENDADO).") + for col_name in indicator_cols_for_scaler: + scaled_df[col_name.replace(\'_div_atr\', \'_scaled\')] = final_window_df[col_name] + else: + logger_instance.error(f"Preprocessing: Colunas de indicadores {indicator_cols_for_scaler} não encontradas.") + return np.array([]) + + missing_final_features = [feat for feat in expected_features_order if feat not in scaled_df.columns] + if missing_final_features: + logger_instance.error(f"Preprocessing: Features FINAIS esperadas ausentes no DataFrame escalado: {missing_final_features}. " + f"Disponíveis: {scaled_df.columns.tolist()}") + return np.array([]) + + final_features_array = scaled_df[expected_features_order].values + + reshaped_data = np.reshape(final_features_array, (1, window_size, NUM_FEATURES)) + + logger_instance.info(f"Preprocessing: Dados pré-processados para {asset_market_data.get(\'symbol\', \'ativo\')} com shape: {reshaped_data.shape}") + return reshaped_data + + +class RNNModelPredictor: + def __init__(self, model_path: str, logger_instance): + self.model_path = model_path + self.model: Optional[tf.keras.Model] = None + self.logger = logger_instance + + self.price_volume_scaler = self._load_scaler(PRICE_VOL_SCALER_PATH, "Price/Volume") + self.indicator_scaler = self._load_scaler(INDICATOR_SCALER_PATH, "Indicator") # Corrigido self_load_scaler para _load_scaler + + self._load_model() + + def _load_scaler(self, scaler_path: str, scaler_name: str): + if joblib is None: + self.logger.warning(f"ScalerLoader: Joblib não disponível, não é possível carregar scaler {scaler_name} de {scaler_path}.") + return None + try: + if os.path.exists(scaler_path): + scaler = joblib.load(scaler_path) + self.logger.info(f"ScalerLoader: Scaler {scaler_name} carregado de {scaler_path}.") + return scaler + else: + self.logger.warning(f"ScalerLoader: Arquivo do scaler {scaler_name} não encontrado em {scaler_path}. " + "O pré-processamento pode usar dados não escalados (NÃO RECOMENDADO).") + return None + except Exception as e: + self.logger.error(f"ScalerLoader: Erro ao carregar scaler {scaler_name} de {scaler_path}: {e}", exc_info=True) + return None + + def _load_model(self): + try: + self.logger.info(f"RNNPredictor: Carregando modelo de {self.model_path}...") + self.model = tf.keras.models.load_model(self.model_path) + self.logger.info(f"RNNPredictor: Modelo carregado com sucesso. Input shape esperado pelo modelo: {self.model.input_shape}") + + if self.model.input_shape != (None, WINDOW_SIZE, NUM_FEATURES): + self.logger.warning(f"RNNPredictor: Discrepância no input shape! " + f"Modelo espera: {self.model.input_shape}, " + f"Configurado para: (None, {WINDOW_SIZE}, {NUM_FEATURES}). " + f"Verifique WINDOW_SIZE e EXPECTED_FEATURES_ORDER no config.py.") + except Exception as e: + self.logger.error(f"RNNPredictor: Falha ao carregar modelo de {self.model_path}: {e}", exc_info=True) + self.model = None + + async def predict_for_asset( + self, + asset_market_data: Dict[str, Any], + loop=None, + ) -> Tuple[Optional[int], Optional[float]]: + if not self.model: + self.logger.warning(f"RNNPredictor: Modelo não carregado, predição pulada para {asset_market_data.get(\'symbol\', \'ativo\')}.") + return None, None + + if self.price_volume_scaler is None or self.indicator_scaler is None: + self.logger.warning(f"RNNPredictor: Scalers não carregados. A predição para {asset_market_data.get(\'symbol\', \'ativo\')} pode ser imprecisa ou falhar.") + + try: + processed_input = preprocess_input_data_for_asset( + asset_market_data, + window_size=WINDOW_SIZE, + expected_features_order=EXPECTED_FEATURES_ORDER, + price_vol_scaler=self.price_volume_scaler, + indicator_scaler=self.indicator_scaler, + logger_instance=self.logger + ) + + if processed_input.size == 0: + self.logger.warning(f"RNNPredictor: Pré-processamento falhou para {asset_market_data.get(\'symbol\', \'ativo\')}.") + return None, None + + if loop is None: + import asyncio + loop = asyncio.get_running_loop() + + raw_predictions = await loop.run_in_executor(None, self.model.predict, processed_input) + + if raw_predictions.ndim == 2 and raw_predictions.shape[0] == 1 and raw_predictions.shape[1] == 1: + prediction_prob = float(raw_predictions[0, 0]) + signal = int(prediction_prob > 0.50) + self.logger.info(f"RNNPredictor: Predição para {asset_market_data.get(\'symbol\', \'ativo\')} - Prob: {prediction_prob:.4f}, Sinal: {signal}") + return signal, prediction_prob + else: + self.logger.warning(f"RNNPredictor: Formato de predição inesperado para {asset_market_data.get(\'symbol\', \'ativo\')}: {raw_predictions.shape}") + return None, None + + except Exception as e: + self.logger.error(f"RNNPredictor: Erro durante a predição para {asset_market_data.get(\'symbol\', \'ativo\')}: {e}", exc_info=True) + return None, None + + diff --git a/app/model/other_indicators_scaler.joblib b/app/model/other_indicators_scaler.joblib new file mode 100644 index 0000000000000000000000000000000000000000..67652327fe798eaff83b3f53b8f9c622ac2cf87b --- /dev/null +++ b/app/model/other_indicators_scaler.joblib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a6da3bb001be7c9ef9478e450121e3394560286cee01fca0a889123bc09d1be +size 1527 diff --git a/app/model/preprocess.py b/app/model/preprocess.py new file mode 100644 index 0000000000000000000000000000000000000000..b4213db6dfb19fca09ac9cbef3d172154bf5048f --- /dev/null +++ b/app/model/preprocess.py @@ -0,0 +1,12 @@ +import numpy as np +from sklearn.preprocessing import StandardScaler + +# Simula o mesmo shape e pré-processamento do modelo +scaler = StandardScaler() + +def preprocess_input_data(data): + data = np.array(data).astype(np.float32) + if data.ndim == 1: + data = np.expand_dims(data, axis=0) + data = scaler.fit_transform(data) + return np.expand_dims(data, axis=2) # [batch, features, 1] diff --git a/app/model/price_volume_atr_norm_scaler.joblib b/app/model/price_volume_atr_norm_scaler.joblib new file mode 100644 index 0000000000000000000000000000000000000000..e0b2c144474cb1e8ccd27c716e2db2069a2ee53f --- /dev/null +++ b/app/model/price_volume_atr_norm_scaler.joblib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93e76c808185e6dc301c8100431c3d18563c63ee6d6e4ed410eb3b325d060b93 +size 1255 diff --git a/app/model/rnn_predictor.py b/app/model/rnn_predictor.py new file mode 100644 index 0000000000000000000000000000000000000000..42864492c01acb712ea105c6d6ee85c055928f54 --- /dev/null +++ b/app/model/rnn_predictor.py @@ -0,0 +1,506 @@ +# rnn/app/models/rnn_predictor.py (ou o nome que você deu, ex: data_preprocessing_api.py) + +import os +import numpy as np +import pandas as pd +import tensorflow as tf +from typing import Optional, Dict, List, Tuple + +try: + import pandas_ta as ta +except ImportError: + print("AVISO URGENTE (RNN PREDICTOR): pandas_ta não instalado! Cálculo de features falhará.") + ta = None + +try: + import joblib +except ImportError: + print("AVISO URGENTE (RNN PREDICTOR): joblib não instalado! Carregamento de scalers falhará.") + joblib = None + +# Importar DO CONFIG.PY para consistência +from ..config import ( # Supondo que config.py está um nível acima na pasta 'app' + WINDOW_SIZE as DEFAULT_WINDOW_SIZE, # Renomeado para evitar conflito de nome local + BASE_FEATURE_COLS, # Não precisamos da lista de nomes base aqui diretamente, mas sim das colunas específicas + # NUM_FEATURES, # Será derivado do input_shape do modelo + MODEL_SAVE_DIR as DEFAULT_MODEL_SAVE_DIR, # Para construir caminhos + MODEL_NAME as DEFAULT_MODEL_NAME, + PRICE_VOL_SCALER_NAME as DEFAULT_PRICE_VOL_SCALER_NAME, + INDICATOR_SCALER_NAME as DEFAULT_INDICATOR_SCALER_NAME, + EXPECTED_SCALED_FEATURES_FOR_MODEL # Esta é a ordem final das features escaladas +) + +# Defina as colunas EXATAS que cada scaler da API espera, baseado no config e no script de treino +# Estas devem corresponder às colunas usadas para FITAR os scalers no train_rnn_model.py +# ANTES de serem escaladas pelo general_training_scaler. +API_PRICE_VOL_COLS_TO_SCALE = ['close_div_atr', 'volume_div_atr', 'open_div_atr', 'high_div_atr', 'low_div_atr', 'body_size_norm_atr'] +API_INDICATOR_COLS_TO_SCALE = ['log_return', 'rsi_14', 'atr', 'bbp', 'cci_37', 'mfi_37', 'body_vs_avg_body', 'macd', 'sma_10_div_atr', 'adx_14', 'volume_zscore', 'buy_condition_v1'] + + +API_PRICE_VOL_COLS = ['close_div_atr', 'volume_div_atr'] # Como no seu log de treino + +API_INDICATOR_COLS = [col for col in BASE_FEATURE_COLS if col not in API_PRICE_VOL_COLS] + +def calculate_features_for_prediction(ohlcv_df: pd.DataFrame, logger_instance) -> Optional[pd.DataFrame]: + """ + Calcula TODAS as features base necessárias para a predição, + EXATAMENTE como no script de treinamento ANTES do escalonamento final. + """ + if logger_instance is None: import logging; logger_instance = logging.getLogger(__name__) + if ta is None: logger_instance.error("pandas_ta não está disponível para calcular features."); return None + + df = ohlcv_df.copy() + required_ohlc_cols = ['open', 'high', 'low', 'close', 'volume'] + if not all(col in df.columns for col in required_ohlc_cols): + logger_instance.error(f"DataFrame OHLCV não contém todas as colunas necessárias: {required_ohlc_cols}") + return None + try: + # Indicadores base + df.ta.sma(length=10, close='close', append=True, col_names=('sma_10',)) + df.ta.rsi(length=14, close='close', append=True, col_names=('rsi_14',)) + macd_out = df.ta.macd(close='close', append=False) + if macd_out is not None: + df['macd'] = macd_out.iloc[:,0] # MACD line + df['macds'] = macd_out.iloc[:,2] # Signal line + df.ta.atr(length=14, append=True, col_names=('atr',)) + df.ta.bbands(length=20, close='close', append=True, col_names=('bbl', 'bbm', 'bbu', 'bbb', 'bbp')) + df.ta.cci(length=37, append=True, col_names=('cci_37',)) # Usando o período do seu log + df.ta.mfi(length=37, append=True, col_names=('mfi_37',)) # Usando o período do seu log + df.ta.adx(length=14, append=True) # Adiciona ADX_14, DMP_14, DMN_14 + if 'ADX_14' not in df.columns and 'ADX_14_ADX' in df.columns: # Handle naming variation + df.rename(columns={'ADX_14_ADX': 'ADX_14'}, inplace=True) + + # Features de Volume Z-score + rolling_vol_mean = df['volume'].rolling(window=20).mean() + rolling_vol_std = df['volume'].rolling(window=20).std() + df['volume_zscore'] = (df['volume'] - rolling_vol_mean) / (rolling_vol_std + 1e-9) + + # Features de Candle + df['body_size'] = abs(df['close'] - df['open']) + df['body_size_norm_atr'] = df['body_size'] / (df['atr'] + 1e-9) + df['body_vs_avg_body'] = df['body_size'] / (df['body_size'].rolling(window=20).mean() + 1e-9) + + # Log Return + df['log_return'] = np.log(df['close'] / df['close'].shift(1)) + + # Normalização pelo ATR (DEPOIS de calcular ATR e outras features que o usam) + # Garanta que o ATR não é zero para evitar divisão por zero + df_atr_valid = df[df['atr'] > 1e-7].copy() # Trabalhar em cópia para evitar SettingWithCopyWarning + if df_atr_valid.empty: # Se todos os ATRs forem zero ou NaN + logger_instance.warning("ATR é zero ou NaN para todos os dados, não é possível normalizar pelo ATR.") + # Preencher colunas _div_atr com um valor (ex: 0 ou o valor original) ou retornar None + # Para simplificar, vamos preencher com o valor original se ATR for problemático + cols_to_norm_by_atr = ['open', 'high', 'low', 'close', 'volume', 'sma_10', 'macd'] + for col in cols_to_norm_by_atr: + df[f'{col}_div_atr'] = df[col] # Fallback + else: + df['open_div_atr'] = df['open'] / (df['atr'] + 1e-9) + df['high_div_atr'] = df['high'] / (df['atr'] + 1e-9) + df['low_div_atr'] = df['low'] / (df['atr'] + 1e-9) + df['close_div_atr'] = df['close'] / (df['atr'] + 1e-9) + df['volume_div_atr'] = df['volume'] / (df['atr'] + 1e-9) + if 'sma_10' in df.columns: df['sma_10_div_atr'] = df['sma_10'] / (df['atr'] + 1e-9) + if 'macd' in df.columns: df['macd_div_atr'] = df['macd'] / (df['atr'] + 1e-9) + + # Feature de Condição de Compra + sma_50_series = df.ta.sma(length=50, close='close', append=False) + if sma_50_series is not None: df['sma_50'] = sma_50_series + else: df['sma_50'] = np.nan + + if all(col in df.columns for col in ['macd', 'macds', 'rsi_14', 'close', 'sma_50']): + df['buy_condition_v1'] = ((df['macd'] > df['macds']) & (df['rsi_14'] > 50) & (df['close'] > df['sma_50'])).astype(int) + else: + df['buy_condition_v1'] = 0 # Fallback + + df.dropna(inplace=True) # Remove todos os NaNs gerados + logger_instance.info("Features para predição calculadas.") + return df + + except Exception as e: + logger_instance.error(f"Erro ao calcular features para predição: {e}", exc_info=True) + return None + + +def preprocess_for_model_prediction( + features_df: pd.DataFrame, + price_vol_scaler, + indicator_scaler, + expected_scaled_feature_order: List[str], # Vem do config.EXPECTED_SCALED_FEATURES_FOR_MODEL + window_size: int, + logger_instance +) -> np.ndarray: + """ + Aplica scalers carregados e formata os dados para a entrada do modelo. + `features_df` deve conter TODAS as colunas de `API_PRICE_VOL_COLS_TO_SCALE` e `API_INDICATOR_COLS_TO_SCALE`. + """ + if features_df.empty or len(features_df) < window_size: + logger_instance.warning(f"Preprocessing API: Dados insuficientes para janela. Necessário: {window_size}, Disponível: {len(features_df)}") + return np.array([]) + + # Pegar a última janela de dados + window_data_df = features_df.tail(window_size).copy() + + if len(window_data_df) < window_size: # Checagem extra + logger_instance.warning(f"Preprocessing API: Janela de dados incompleta após tail. Necessário: {window_size}, Disponível: {len(window_data_df)}") + return np.array([]) + + scaled_features_dict = {} + + # Aplicar scaler de Preço/Volume + if price_vol_scaler and all(col in window_data_df.columns for col in API_PRICE_VOL_COLS_TO_SCALE): + scaled_pv = price_vol_scaler.transform(window_data_df[API_PRICE_VOL_COLS_TO_SCALE]) + for i, col_name in enumerate(API_PRICE_VOL_COLS_TO_SCALE): + scaled_features_dict[f"{col_name}_scaled"] = scaled_pv[:, i] + elif not price_vol_scaler: + logger_instance.error("Preprocessing API: price_volume_scaler não carregado!") + return np.array([]) + else: + missing = [col for col in API_PRICE_VOL_COLS_TO_SCALE if col not in window_data_df.columns] + logger_instance.error(f"Preprocessing API: Colunas ausentes para price_volume_scaler: {missing}") + return np.array([]) + + # Aplicar scaler de Indicadores + if indicator_scaler and all(col in window_data_df.columns for col in API_INDICATOR_COLS_TO_SCALE): + scaled_ind = indicator_scaler.transform(window_data_df[API_INDICATOR_COLS_TO_SCALE]) + for i, col_name in enumerate(API_INDICATOR_COLS_TO_SCALE): + scaled_features_dict[f"{col_name}_scaled"] = scaled_ind[:, i] + + + + + + elif not indicator_scaler: + logger_instance.error("Preprocessing API: indicator_scaler não carregado!") + return np.array([]) + else: + missing = [col for col in API_INDICATOR_COLS_TO_SCALE if col not in window_data_df.columns] + logger_instance.error(f"Preprocessing API: Colunas ausentes para indicator_scaler: {missing}") + return np.array([]) + + + # News -- + + if features_df.empty or len(features_df) < window_size: + logger_instance.warning(f"API Preprocessing: Dados insuficientes. Necessário: {window_size}, Disp: {len(features_df)}") + return np.array([]) + + window_data_df = features_df.tail(window_size).copy() + if len(window_data_df) < window_size: return np.array([]) # Checagem extra + + scaled_data_dict = {} + + if price_vol_scaler and all(col in window_data_df.columns for col in API_PRICE_VOL_COLS): + scaled_pv = price_vol_scaler.transform(window_data_df[API_PRICE_VOL_COLS]) + for i, col_name in enumerate(API_PRICE_VOL_COLS): + scaled_data_dict[f"{col_name}_scaled"] = scaled_pv[:, i] + else: + missing = [col for col in API_INDICATOR_COLS_TO_SCALE if col not in window_data_df.columns] + logger_instance.error(f"Preprocessing API: Colunas ausentes para indicator_scaler: {missing}") + return np.array([]) + + + if indicator_scaler and all(col in window_data_df.columns for col in API_INDICATOR_COLS): + scaled_ind = indicator_scaler.transform(window_data_df[API_INDICATOR_COLS]) + for i, col_name in enumerate(API_INDICATOR_COLS): + # A feature 'buy_condition_v1' é binária. Escalar pode não ser ideal, + # mas se foi escalada no treino, precisa ser aqui também. + # Se não foi escalada no treino e está em EXPECTED_SCALED_FEATURES_FOR_MODEL sem _scaled, + # a lógica abaixo precisaria de ajuste. + scaled_data_dict[f"{col_name}_scaled"] = scaled_ind[:, i] + else: + missing = [col for col in API_INDICATOR_COLS_TO_SCALE if col not in window_data_df.columns] + logger_instance.error(f"Preprocessing API: Colunas ausentes para indicator_scaler: {missing}") + return np.array([]) + + # Montar o array final na ordem de EXPECTED_SCALED_FEATURES_FOR_MODEL + final_feature_list_for_model = [] + for scaled_feature_name in EXPECTED_SCALED_FEATURES_FOR_MODEL: # Esta é config.EXPECTED_SCALED_FEATURES_FOR_MODEL + if scaled_feature_name in scaled_data_dict: + final_feature_list_for_model.append(scaled_data_dict[scaled_feature_name]) + else: + # Isso aconteceria se uma feature em EXPECTED_SCALED_FEATURES_FOR_MODEL + # não foi corretamente gerada e adicionada ao scaled_data_dict. + # Ex: se 'buy_condition_v1' não fosse escalada mas seu nome '_scaled' estivesse em EXPECTED_SCALED_FEATURES_FOR_MODEL + logger_instance.error(f"API Preprocessing: Feature escalada '{scaled_feature_name}' não encontrada no dict. Verifique config e lógica de scaling.") + return np.array([]) + + final_features_array = np.stack(final_feature_list_for_model, axis=-1) # (window_size, num_model_features) + + if final_features_array.shape[1] != len(EXPECTED_SCALED_FEATURES_FOR_MODEL): + logger_instance.error(f"API Preprocessing: Discrepância no número de features! " + f"Esperado: {len(EXPECTED_SCALED_FEATURES_FOR_MODEL)}, Obtido: {final_features_array.shape[1]}") + return np.array([]) + + + #Criar DataFrame com todas as features escaladas e na ordem correta + try: + # Construir o array de features na ordem de EXPECTED_SCALED_FEATURES_FOR_MODEL + final_feature_list_for_model = [] + final_ordered_features_list = [] + for scaled_col_name in expected_scaled_feature_order: # Esta é config.EXPECTED_SCALED_FEATURES_FOR_MODEL + if scaled_col_name in scaled_features_dict: + final_ordered_features_list.append(scaled_features_dict[scaled_col_name]) + else: + # Se uma feature em EXPECTED_SCALED_FEATURES_FOR_MODEL não foi gerada/escalada + # Isso indica um desalinhamento entre config.py e a lógica aqui. + # Exemplo: se 'buy_condition_v1' não for escalada e seu nome escalado não existir. + # Tentativa: pegar a coluna original se o _scaled não existir E a original estiver em expected_scaled_feature_order + original_col_name = scaled_col_name.replace("_scaled", "") + if original_col_name in window_data_df.columns and original_col_name == scaled_col_name : # Se a feature esperada é a original não escalada + logger_instance.warning(f"Preprocessing API: Usando feature original não escalada '{original_col_name}' conforme esperado.") + final_ordered_features_list.append(window_data_df[original_col_name].values) + else: + logger_instance.error(f"Preprocessing API: Feature escalada '{scaled_col_name}' esperada pelo modelo não foi encontrada no dicionário de features escaladas.") + return np.array([]) + + # Transpor para ter (timesteps, features) e depois adicionar dimensão de batch + final_features_array = np.stack(final_ordered_features_list, axis=-1) # (window_size, num_features) + final_features_array = np.stack(final_feature_list_for_model, axis=-1) + except KeyError as e_key: + logger_instance.error(f"Preprocessing API: Erro de chave ao montar features finais. Provavelmente uma feature em " + f"EXPECTED_SCALED_FEATURES_FOR_MODEL não foi corretamente gerada/escalada. Erro: {e_key}", exc_info=True) + return np.array([]) + except Exception as e_stack: + logger_instance.error(f"Preprocessing API: Erro ao empilhar features finais: {e_stack}", exc_info=True) + return np.array([]) + + + if final_features_array.shape != (window_size, len(expected_scaled_feature_order)): + logger_instance.error(f"Preprocessing API: Shape incorreto após escalonamento e ordenação. " + f"Esperado: ({window_size}, {len(expected_scaled_feature_order)}), " + f"Obtido: {final_features_array.shape}") + return np.array([]) + + reshaped_data = np.reshape(final_features_array, (1, window_size, len(expected_scaled_feature_order))) + logger_instance.info(f"Preprocessing API: Dados pré-processados com shape: {reshaped_data.shape}") + return reshaped_data + + + + + # + + +class RNNModelPredictor: + def __init__(self, model_dir: str, model_filename: str, + pv_scaler_filename: str, ind_scaler_filename: str, + logger_instance): + self.model_path = os.path.join(model_dir, model_filename) + self.pv_scaler_path = os.path.join(model_dir, pv_scaler_filename) + self.ind_scaler_path = os.path.join(model_dir, ind_scaler_filename) + + self.model: Optional[tf.keras.Model] = None + self.price_volume_scaler = None + self.indicator_scaler = None + self.logger = logger_instance + self.num_model_features = len(EXPECTED_SCALED_FEATURES_FOR_MODEL) + self._load_model_and_scalers() + + + + self._load_model_and_scalers() + + def _load_scaler(self, scaler_path: str, scaler_name: str): # ... (como antes) + if not joblib: self.logger.error(f"Joblib não importado, não é possível carregar scaler {scaler_name}."); return None + try: + if os.path.exists(scaler_path): + scaler = joblib.load(scaler_path) + self.logger.info(f"Scaler {scaler_name} carregado de {scaler_path}.") + return scaler + else: + self.logger.error(f"Arquivo do scaler {scaler_name} NÃO ENCONTRADO em {scaler_path}.") + return None + except Exception as e: + self.logger.error(f"Erro ao carregar scaler {scaler_name} de {scaler_path}: {e}", exc_info=True) + return None + + + + def _load_scaler(self, scaler_path: str, scaler_name: str): # ... (como antes) + if not joblib: self.logger.error(f"Joblib não importado, não é possível carregar scaler {scaler_name}."); return None + try: + if os.path.exists(scaler_path): + scaler = joblib.load(scaler_path) + self.logger.info(f"Scaler {scaler_name} carregado de {scaler_path}.") + return scaler + else: + self.logger.error(f"Arquivo do scaler {scaler_name} NÃO ENCONTRADO em {scaler_path}.") + return None + except Exception as e: + self.logger.error(f"Erro ao carregar scaler {scaler_name} de {scaler_path}: {e}", exc_info=True) + return None + + + + def _load_model_and_scalers(self): + try: + self.logger.info(f"RNNPredictor: Carregando modelo de {self.model_path}...") + if not os.path.exists(self.model_path): + self.logger.error(f"Arquivo do modelo NÃO ENCONTRADO em {self.model_path}") + return + self.model = tf.keras.models.load_model(self.model_path) + self.logger.info(f"RNNPredictor: Modelo carregado. Input shape esperado: {self.model.input_shape}") + + model_features_count = self.model.input_shape[-1] + if model_features_count != self.num_model_features_expected: + self.logger.error(f"DISCREPÂNCIA DE FEATURES! Modelo espera {model_features_count} features, " + f"mas config.EXPECTED_SCALED_FEATURES_FOR_MODEL tem {self.num_model_features_expected} features.") + self.model = None + return + + self.price_volume_scaler = self._load_scaler(self.pv_scaler_path, "Price/Volume (API)") + self.indicator_scaler = self._load_scaler(self.ind_scaler_path, "Indicator (API)") + + if self.price_volume_scaler is None or self.indicator_scaler is None: + self.logger.error("Um ou ambos os scalers não puderam ser carregados. O preditor pode não funcionar.") + except Exception as e: + self.logger.error(f"RNNPredictor: Falha crítica ao carregar modelo ou scalers: {e}", exc_info=True) + self.model = None # Invalida tudo se houver erro + self.price_volume_scaler = None + self.indicator_scaler = None + + async def predict_for_asset_ohlcv( + self, + ohlcv_df_raw: pd.DataFrame, + api_operation_threshold: float = 0.65 # Seu threshold escolhido + ) -> Tuple[Optional[int], Optional[float]]: + + current_loop = asyncio.get_event_loop() + + if self.model is None or self.price_volume_scaler is None or self.indicator_scaler is None: + self.logger.warning("API Predict: Modelo ou scalers não carregados. Predição pulada.") + return None, None + + # 1. Calcular todas as features base + features_df_base = await current_loop.run_in_executor( + None, calculate_all_base_features_api, ohlcv_df_raw, self.logger + ) + if features_df_base is None or features_df_base.empty: + self.logger.warning("API Predict: Falha ao calcular features base.") + return None, None + + # 2. Aplicar scalers e formatar + processed_input = await current_loop.run_in_executor( + None, preprocess_for_model_prediction, + features_df_base, # Passa o DF com as features base calculadas + self.price_volume_scaler, + self.indicator_scaler, + EXPECTED_SCALED_FEATURES_FOR_MODEL, # Do config + DEFAULT_WINDOW_SIZE, # Do config + self.logger + ) + + if processed_input.size == 0: + self.logger.warning(f"API Predict: Pré-processamento falhou.") + return None, None + + + # 3. Fazer a predição + try: + raw_predictions = await current_loop.run_in_executor(None, self.model.predict, processed_input_sequence) + + if raw_predictions.ndim == 2 and raw_predictions.shape[0] == 1 and raw_predictions.shape[1] == 1: + prediction_prob = float(raw_predictions[0, 0]) + signal = int(prediction_prob > api_operation_threshold) + self.logger.info(f"API Predict: Prob: {prediction_prob:.4f}, Thr: {api_operation_threshold}, Sinal: {signal}") + return signal, prediction_prob + else: + self.logger.warning(f"RNNPredictor: Formato de predição inesperado: {raw_predictions.shape}") + return None, None + except Exception as e: + self.logger.error(f"RNNPredictor: Erro durante a predição com o modelo: {e}", exc_info=True) + return None, None + +# --- Para teste local do rnn_predictor.py (exemplo) --- +async def test_predictor(): + # Este é apenas um exemplo de como você poderia testar o predictor isoladamente. + # Você precisaria fornecer um DataFrame ohlcv_df_raw de teste. + + # Configuração de Log para Teste + import logging + test_logger = logging.getLogger("TestRNNPredictor") + test_logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + if not test_logger.handlers: # Evitar adicionar handlers múltiplos se rodar várias vezes + test_logger.addHandler(handler) + + # Caminhos (ajuste se config.py não estiver acessível diretamente assim) + # Supondo que este script está em rnn/app/models/ e config.py em rnn/app/ + # e os modelos/scalers estão em rnn/app/model/ + model_dir_path = os.path.join(os.path.dirname(__file__), "..", "model") # rnn/app/model/ + + predictor = RNNModelPredictor( + model_dir=model_dir_path, + model_filename=DEFAULT_MODEL_NAME, # Do config + pv_scaler_filename=DEFAULT_PRICE_VOL_SCALER_NAME, # Do config + ind_scaler_filename=DEFAULT_INDICATOR_SCALER_NAME, # Do config + logger_instance=test_logger + ) + + if predictor.model is None: + test_logger.error("Teste Falhou: Modelo não carregado no preditor.") + return + + # Criar um DataFrame OHLCV de exemplo (substitua por dados reais ou carregados de CSV) + # Precisa ter pelo menos WINDOW_SIZE + (maior lookback de indicador) linhas + num_test_rows = DEFAULT_WINDOW_SIZE + 50 + example_data = { + 'open': np.random.rand(num_test_rows) * 1000 + 30000, + 'high': np.random.rand(num_test_rows) * 1200 + 30000, + 'low': np.random.rand(num_test_rows) * 800 + 29800, + 'close': np.random.rand(num_test_rows) * 1000 + 30000, + 'volume': np.random.rand(num_test_rows) * 100 + 10 + } + # Gerar timestamps de 1h para trás a partir de agora + end_time = pd.Timestamp.now(tz='UTC') + start_time = end_time - pd.Timedelta(hours=num_test_rows - 1) + timestamps = pd.date_range(start=start_time, end=end_time, freq='h') + + # Ajustar se o número de timestamps não bater exatamente com num_test_rows + if len(timestamps) > num_test_rows: + timestamps = timestamps[:num_test_rows] + elif len(timestamps) < num_test_rows: + # Ajustar dados de exemplo para o número de timestamps disponíveis + for key in example_data: + example_data[key] = example_data[key][:len(timestamps)] + + test_ohlcv_df = pd.DataFrame(example_data, index=timestamps) + test_ohlcv_df['high'] = np.maximum(test_ohlcv_df['high'], test_ohlcv_df['close'], test_ohlcv_df['open']) + test_ohlcv_df['low'] = np.minimum(test_ohlcv_df['low'], test_ohlcv_df['close'], test_ohlcv_df['open']) + + test_logger.info(f"DataFrame de teste criado com {len(test_ohlcv_df)} linhas.") + + signal, probability = await predictor.predict_for_asset_ohlcv(test_ohlcv_df, api_operation_threshold=0.65) + + if signal is not None: + test_logger.info(f"Resultado do Teste - Sinal: {signal}, Probabilidade: {probability:.4f}") + else: + test_logger.error("Resultado do Teste - Predição falhou.") + +if __name__ == '__main__': + # Para rodar o teste: python rnn/app/models/rnn_predictor.py (ou o nome que você deu) + # Certifique-se que o config.py está acessível (ex: no mesmo diretório ou PYTHONPATH) + # E que os arquivos de modelo e scalers existem em app/model/ + import asyncio + # Definir um caminho base para que os imports relativos de config funcionem se rodar daqui + # Isso é um pouco hacky para teste direto do script. Idealmente, teste via pytest com estrutura de projeto. + import sys + # Adiciona o diretório 'app' ao sys.path para encontrar config.py + # Supondo que este script rnn_predictor.py está em rnn/app/models/ + # e config.py está em rnn/app/ + # Sobe dois níveis para 'rnn', depois desce para 'app' + # app_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + # if app_dir not in sys.path: + # sys.path.insert(0, app_dir) + # Para o import `from ..config import ...` funcionar, você geralmente roda o módulo + # como parte de um pacote, não diretamente. + # Para teste direto, você pode precisar de um import absoluto ou ajuste de PYTHONPATH. + + # Simplesmente para teste rápido, vamos assumir que o diretório de trabalho é 'rnn' + # e o import de config seria 'from app.config import ...' + # Se você rodar `python rnn/app/models/rnn_predictor.py` da raiz 'rnn', o import ..config deve funcionar. + + print("Rodando teste do RNNModelPredictor...") + asyncio.run(test_predictor()) \ No newline at end of file diff --git a/app/model/rnn_predictor_notebook.py b/app/model/rnn_predictor_notebook.py new file mode 100644 index 0000000000000000000000000000000000000000..242f7809401a3519bebe5743488ee78b57955051 --- /dev/null +++ b/app/model/rnn_predictor_notebook.py @@ -0,0 +1,425 @@ +# rnn/app/models/rnn_predictor.py (ou o nome que você deu, ex: data_preprocessing_api.py) + +import os +import numpy as np +import pandas as pd +import tensorflow as tf +from typing import Optional, Dict, List, Tuple + +try: + import pandas_ta as ta +except ImportError: + print("AVISO URGENTE (RNN PREDICTOR): pandas_ta não instalado! Cálculo de features falhará.") + ta = None + +try: + import joblib +except ImportError: + print("AVISO URGENTE (RNN PREDICTOR): joblib não instalado! Carregamento de scalers falhará.") + joblib = None + +# Importar DO CONFIG.PY para consistência +from ..config import ( # Supondo que config.py está um nível acima na pasta 'app' + WINDOW_SIZE as DEFAULT_WINDOW_SIZE, # Renomeado para evitar conflito de nome local + # BASE_FEATURE_COLS, # Não precisamos da lista de nomes base aqui diretamente, mas sim das colunas específicas + # NUM_FEATURES, # Será derivado do input_shape do modelo + MODEL_SAVE_DIR as DEFAULT_MODEL_SAVE_DIR, # Para construir caminhos + MODEL_NAME as DEFAULT_MODEL_NAME, + PRICE_VOL_SCALER_NAME as DEFAULT_PRICE_VOL_SCALER_NAME, + INDICATOR_SCALER_NAME as DEFAULT_INDICATOR_SCALER_NAME, + EXPECTED_SCALED_FEATURES_FOR_MODEL # Esta é a ordem final das features escaladas +) + +# Defina as colunas EXATAS que cada scaler da API espera, baseado no config e no script de treino +# Estas devem corresponder às colunas usadas para FITAR os scalers no train_rnn_model.py +# ANTES de serem escaladas pelo general_training_scaler. +API_PRICE_VOL_COLS_TO_SCALE = ['close_div_atr', 'volume_div_atr', 'open_div_atr', 'high_div_atr', 'low_div_atr', 'body_size_norm_atr'] +API_INDICATOR_COLS_TO_SCALE = ['log_return', 'rsi_14', 'atr', 'bbp', 'cci_37', 'mfi_37', 'body_vs_avg_body', 'macd', 'sma_10_div_atr', 'adx_14', 'volume_zscore', 'buy_condition_v1'] + + +def calculate_features_for_prediction(ohlcv_df: pd.DataFrame, logger_instance) -> Optional[pd.DataFrame]: + """ + Calcula TODAS as features base necessárias para a predição, + EXATAMENTE como no script de treinamento ANTES do escalonamento final. + """ + if logger_instance is None: import logging; logger_instance = logging.getLogger(__name__) + if ta is None: logger_instance.error("pandas_ta não está disponível para calcular features."); return None + + df = ohlcv_df.copy() + required_ohlc_cols = ['open', 'high', 'low', 'close', 'volume'] + if not all(col in df.columns for col in required_ohlc_cols): + logger_instance.error(f"DataFrame OHLCV não contém todas as colunas necessárias: {required_ohlc_cols}") + return None + try: + # Indicadores base + df.ta.sma(length=10, close='close', append=True, col_names=('sma_10',)) + df.ta.rsi(length=14, close='close', append=True, col_names=('rsi_14',)) + macd_out = df.ta.macd(close='close', append=False) + if macd_out is not None: + df['macd'] = macd_out.iloc[:,0] # MACD line + df['macds'] = macd_out.iloc[:,2] # Signal line + df.ta.atr(length=14, append=True, col_names=('atr',)) + df.ta.bbands(length=20, close='close', append=True, col_names=('bbl', 'bbm', 'bbu', 'bbb', 'bbp')) + df.ta.cci(length=37, append=True, col_names=('cci_37',)) # Usando o período do seu log + df.ta.mfi(length=37, append=True, col_names=('mfi_37',)) # Usando o período do seu log + df.ta.adx(length=14, append=True) # Adiciona ADX_14, DMP_14, DMN_14 + if 'ADX_14' not in df.columns and 'ADX_14_ADX' in df.columns: # Handle naming variation + df.rename(columns={'ADX_14_ADX': 'ADX_14'}, inplace=True) + + # Features de Volume Z-score + rolling_vol_mean = df['volume'].rolling(window=20).mean() + rolling_vol_std = df['volume'].rolling(window=20).std() + df['volume_zscore'] = (df['volume'] - rolling_vol_mean) / (rolling_vol_std + 1e-9) + + # Features de Candle + df['body_size'] = abs(df['close'] - df['open']) + df['body_size_norm_atr'] = df['body_size'] / (df['atr'] + 1e-9) + df['body_vs_avg_body'] = df['body_size'] / (df['body_size'].rolling(window=20).mean() + 1e-9) + + # Log Return + df['log_return'] = np.log(df['close'] / df['close'].shift(1)) + + # Normalização pelo ATR (DEPOIS de calcular ATR e outras features que o usam) + # Garanta que o ATR não é zero para evitar divisão por zero + df_atr_valid = df[df['atr'] > 1e-7].copy() # Trabalhar em cópia para evitar SettingWithCopyWarning + if df_atr_valid.empty: # Se todos os ATRs forem zero ou NaN + logger_instance.warning("ATR é zero ou NaN para todos os dados, não é possível normalizar pelo ATR.") + # Preencher colunas _div_atr com um valor (ex: 0 ou o valor original) ou retornar None + # Para simplificar, vamos preencher com o valor original se ATR for problemático + cols_to_norm_by_atr = ['open', 'high', 'low', 'close', 'volume', 'sma_10', 'macd'] + for col in cols_to_norm_by_atr: + df[f'{col}_div_atr'] = df[col] # Fallback + else: + df['open_div_atr'] = df['open'] / (df['atr'] + 1e-9) + df['high_div_atr'] = df['high'] / (df['atr'] + 1e-9) + df['low_div_atr'] = df['low'] / (df['atr'] + 1e-9) + df['close_div_atr'] = df['close'] / (df['atr'] + 1e-9) + df['volume_div_atr'] = df['volume'] / (df['atr'] + 1e-9) + if 'sma_10' in df.columns: df['sma_10_div_atr'] = df['sma_10'] / (df['atr'] + 1e-9) + if 'macd' in df.columns: df['macd_div_atr'] = df['macd'] / (df['atr'] + 1e-9) + + # Feature de Condição de Compra + sma_50_series = df.ta.sma(length=50, close='close', append=False) + if sma_50_series is not None: df['sma_50'] = sma_50_series + else: df['sma_50'] = np.nan + + if all(col in df.columns for col in ['macd', 'macds', 'rsi_14', 'close', 'sma_50']): + df['buy_condition_v1'] = ((df['macd'] > df['macds']) & (df['rsi_14'] > 50) & (df['close'] > df['sma_50'])).astype(int) + else: + df['buy_condition_v1'] = 0 # Fallback + + df.dropna(inplace=True) # Remove todos os NaNs gerados + logger_instance.info("Features para predição calculadas.") + return df + + except Exception as e: + logger_instance.error(f"Erro ao calcular features para predição: {e}", exc_info=True) + return None + + +def preprocess_for_model_prediction( + features_df: pd.DataFrame, + price_vol_scaler, + indicator_scaler, + expected_scaled_feature_order: List[str], # Vem do config.EXPECTED_SCALED_FEATURES_FOR_MODEL + window_size: int, + logger_instance +) -> np.ndarray: + """ + Aplica scalers carregados e formata os dados para a entrada do modelo. + `features_df` deve conter TODAS as colunas de `API_PRICE_VOL_COLS_TO_SCALE` e `API_INDICATOR_COLS_TO_SCALE`. + """ + if features_df.empty or len(features_df) < window_size: + logger_instance.warning(f"Preprocessing API: Dados insuficientes para janela. Necessário: {window_size}, Disponível: {len(features_df)}") + return np.array([]) + + # Pegar a última janela de dados + window_data_df = features_df.tail(window_size).copy() + + if len(window_data_df) < window_size: # Checagem extra + logger_instance.warning(f"Preprocessing API: Janela de dados incompleta após tail. Necessário: {window_size}, Disponível: {len(window_data_df)}") + return np.array([]) + + scaled_features_dict = {} + + # Aplicar scaler de Preço/Volume + if price_vol_scaler and all(col in window_data_df.columns for col in API_PRICE_VOL_COLS_TO_SCALE): + scaled_pv = price_vol_scaler.transform(window_data_df[API_PRICE_VOL_COLS_TO_SCALE]) + for i, col_name in enumerate(API_PRICE_VOL_COLS_TO_SCALE): + scaled_features_dict[f"{col_name}_scaled"] = scaled_pv[:, i] + elif not price_vol_scaler: + logger_instance.error("Preprocessing API: price_volume_scaler não carregado!") + return np.array([]) + else: + missing = [col for col in API_PRICE_VOL_COLS_TO_SCALE if col not in window_data_df.columns] + logger_instance.error(f"Preprocessing API: Colunas ausentes para price_volume_scaler: {missing}") + return np.array([]) + + # Aplicar scaler de Indicadores + if indicator_scaler and all(col in window_data_df.columns for col in API_INDICATOR_COLS_TO_SCALE): + scaled_ind = indicator_scaler.transform(window_data_df[API_INDICATOR_COLS_TO_SCALE]) + for i, col_name in enumerate(API_INDICATOR_COLS_TO_SCALE): + scaled_features_dict[f"{col_name}_scaled"] = scaled_ind[:, i] + elif not indicator_scaler: + logger_instance.error("Preprocessing API: indicator_scaler não carregado!") + return np.array([]) + else: + missing = [col for col in API_INDICATOR_COLS_TO_SCALE if col not in window_data_df.columns] + logger_instance.error(f"Preprocessing API: Colunas ausentes para indicator_scaler: {missing}") + return np.array([]) + + # Criar DataFrame com todas as features escaladas e na ordem correta + try: + # Construir o array de features na ordem de EXPECTED_SCALED_FEATURES_FOR_MODEL + final_ordered_features_list = [] + for scaled_col_name in expected_scaled_feature_order: # Esta é config.EXPECTED_SCALED_FEATURES_FOR_MODEL + if scaled_col_name in scaled_features_dict: + final_ordered_features_list.append(scaled_features_dict[scaled_col_name]) + else: + # Se uma feature em EXPECTED_SCALED_FEATURES_FOR_MODEL não foi gerada/escalada + # Isso indica um desalinhamento entre config.py e a lógica aqui. + # Exemplo: se 'buy_condition_v1' não for escalada e seu nome escalado não existir. + # Tentativa: pegar a coluna original se o _scaled não existir E a original estiver em expected_scaled_feature_order + original_col_name = scaled_col_name.replace("_scaled", "") + if original_col_name in window_data_df.columns and original_col_name == scaled_col_name : # Se a feature esperada é a original não escalada + logger_instance.warning(f"Preprocessing API: Usando feature original não escalada '{original_col_name}' conforme esperado.") + final_ordered_features_list.append(window_data_df[original_col_name].values) + else: + logger_instance.error(f"Preprocessing API: Feature escalada '{scaled_col_name}' esperada pelo modelo não foi encontrada no dicionário de features escaladas.") + return np.array([]) + + # Transpor para ter (timesteps, features) e depois adicionar dimensão de batch + final_features_array = np.stack(final_ordered_features_list, axis=-1) # (window_size, num_features) + except KeyError as e_key: + logger_instance.error(f"Preprocessing API: Erro de chave ao montar features finais. Provavelmente uma feature em " + f"EXPECTED_SCALED_FEATURES_FOR_MODEL não foi corretamente gerada/escalada. Erro: {e_key}", exc_info=True) + return np.array([]) + except Exception as e_stack: + logger_instance.error(f"Preprocessing API: Erro ao empilhar features finais: {e_stack}", exc_info=True) + return np.array([]) + + + if final_features_array.shape != (window_size, len(expected_scaled_feature_order)): + logger_instance.error(f"Preprocessing API: Shape incorreto após escalonamento e ordenação. " + f"Esperado: ({window_size}, {len(expected_scaled_feature_order)}), " + f"Obtido: {final_features_array.shape}") + return np.array([]) + + reshaped_data = np.reshape(final_features_array, (1, window_size, len(expected_scaled_feature_order))) + logger_instance.info(f"Preprocessing API: Dados pré-processados com shape: {reshaped_data.shape}") + return reshaped_data + + +class RNNModelPredictor: + def __init__(self, model_dir: str, model_filename: str, + pv_scaler_filename: str, ind_scaler_filename: str, + logger_instance): + self.model_path = os.path.join(model_dir, model_filename) + self.pv_scaler_path = os.path.join(model_dir, pv_scaler_filename) + self.ind_scaler_path = os.path.join(model_dir, ind_scaler_filename) + + self.model: Optional[tf.keras.Model] = None + self.price_volume_scaler = None + self.indicator_scaler = None + self.logger = logger_instance + self.num_model_features = 0 # Será definido ao carregar o modelo + + self._load_model_and_scalers() + + def _load_scaler(self, scaler_path: str, scaler_name: str): + if not joblib: self.logger.error(f"Joblib não importado, não é possível carregar scaler {scaler_name}."); return None + try: + if os.path.exists(scaler_path): + scaler = joblib.load(scaler_path) + self.logger.info(f"Scaler {scaler_name} carregado de {scaler_path}.") + return scaler + else: + self.logger.error(f"Arquivo do scaler {scaler_name} NÃO ENCONTRADO em {scaler_path}.") + return None + except Exception as e: + self.logger.error(f"Erro ao carregar scaler {scaler_name} de {scaler_path}: {e}", exc_info=True) + return None + + def _load_model_and_scalers(self): + try: + self.logger.info(f"RNNPredictor: Carregando modelo de {self.model_path}...") + if not os.path.exists(self.model_path): + self.logger.error(f"Arquivo do modelo NÃO ENCONTRADO em {self.model_path}") + return + self.model = tf.keras.models.load_model(self.model_path) + self.logger.info(f"RNNPredictor: Modelo carregado. Input shape: {self.model.input_shape}") + # (batch_size, timesteps, features) -> Ex: (None, 60, 19) + if len(self.model.input_shape) == 3: + self.num_model_features = self.model.input_shape[2] + # Validar contra EXPECTED_SCALED_FEATURES_FOR_MODEL do config.py + if self.num_model_features != len(EXPECTED_SCALED_FEATURES_FOR_MODEL): + self.logger.error(f"DISCREPÂNCIA DE FEATURES! Modelo espera {self.num_model_features} features, " + f"mas EXPECTED_SCALED_FEATURES_FOR_MODEL tem {len(EXPECTED_SCALED_FEATURES_FOR_MODEL)} features.") + self.model = None # Invalida o modelo se houver discrepância + return + else: + self.logger.error(f"Input shape do modelo inesperado: {self.model.input_shape}") + self.model = None + return + + self.price_volume_scaler = self._load_scaler(self.pv_scaler_path, "Price/Volume (API)") + self.indicator_scaler = self._load_scaler(self.ind_scaler_path, "Indicator (API)") + + if self.price_volume_scaler is None or self.indicator_scaler is None: + self.logger.error("Um ou ambos os scalers não puderam ser carregados. O preditor pode não funcionar.") + # Você pode decidir se invalida o modelo aqui também + # self.model = None + except Exception as e: + self.logger.error(f"RNNPredictor: Falha crítica ao carregar modelo ou scalers: {e}", exc_info=True) + self.model = None + self.price_volume_scaler = None + self.indicator_scaler = None + + async def predict_for_asset_ohlcv( + self, + ohlcv_df_raw: pd.DataFrame, # Espera um DataFrame pandas com OHLCV bruto + api_operation_threshold: float = 0.60 # Threshold para decisão final + ) -> Tuple[Optional[int], Optional[float]]: + + current_loop = asyncio.get_event_loop() + + if self.model is None or self.price_volume_scaler is None or self.indicator_scaler is None: + self.logger.warning("RNNPredictor: Modelo ou scalers não carregados. Predição pulada.") + return None, None + + # 1. Calcular todas as features base (exatamente como no treino) + features_for_scaling_df = await current_loop.run_in_executor( + None, + calculate_features_for_prediction, # Esta função precisa ser síncrona + ohlcv_df_raw, + self.logger + ) + if features_for_scaling_df is None or features_for_scaling_df.empty: + self.logger.warning("RNNPredictor: Falha ao calcular features base para predição.") + return None, None + + # 2. Aplicar scalers e formatar para o modelo + processed_input_sequence = await current_loop.run_in_executor( + None, + preprocess_for_model_prediction, # Esta função precisa ser síncrona + features_for_scaling_df, + self.price_volume_scaler, + self.indicator_scaler, + EXPECTED_SCALED_FEATURES_FOR_MODEL, # Importado do config.py + DEFAULT_WINDOW_SIZE, # Importado do config.py + self.logger + ) + + if processed_input_sequence.size == 0: + self.logger.warning(f"RNNPredictor: Pré-processamento da API falhou.") + return None, None + + # 3. Fazer a predição + try: + raw_predictions = await current_loop.run_in_executor(None, self.model.predict, processed_input_sequence) + + if raw_predictions.ndim == 2 and raw_predictions.shape[0] == 1 and raw_predictions.shape[1] == 1: + prediction_prob = float(raw_predictions[0, 0]) + signal = int(prediction_prob > api_operation_threshold) + self.logger.info(f"RNNPredictor: Predição - Prob: {prediction_prob:.4f}, Threshold: {api_operation_threshold}, Sinal: {signal}") + return signal, prediction_prob + else: + self.logger.warning(f"RNNPredictor: Formato de predição inesperado: {raw_predictions.shape}") + return None, None + except Exception as e: + self.logger.error(f"RNNPredictor: Erro durante a predição com o modelo: {e}", exc_info=True) + return None, None + +# --- Para teste local do rnn_predictor.py (exemplo) --- +async def test_predictor(): + # Este é apenas um exemplo de como você poderia testar o predictor isoladamente. + # Você precisaria fornecer um DataFrame ohlcv_df_raw de teste. + + # Configuração de Log para Teste + import logging + test_logger = logging.getLogger("TestRNNPredictor") + test_logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + if not test_logger.handlers: # Evitar adicionar handlers múltiplos se rodar várias vezes + test_logger.addHandler(handler) + + # Caminhos (ajuste se config.py não estiver acessível diretamente assim) + # Supondo que este script está em rnn/app/models/ e config.py em rnn/app/ + # e os modelos/scalers estão em rnn/app/model/ + model_dir_path = os.path.join(os.path.dirname(__file__), "..", "model") # rnn/app/model/ + + predictor = RNNModelPredictor( + model_dir=model_dir_path, + model_filename=DEFAULT_MODEL_NAME, # Do config + pv_scaler_filename=DEFAULT_PRICE_VOL_SCALER_NAME, # Do config + ind_scaler_filename=DEFAULT_INDICATOR_SCALER_NAME, # Do config + logger_instance=test_logger + ) + + if predictor.model is None: + test_logger.error("Teste Falhou: Modelo não carregado no preditor.") + return + + # Criar um DataFrame OHLCV de exemplo (substitua por dados reais ou carregados de CSV) + # Precisa ter pelo menos WINDOW_SIZE + (maior lookback de indicador) linhas + num_test_rows = DEFAULT_WINDOW_SIZE + 50 + example_data = { + 'open': np.random.rand(num_test_rows) * 1000 + 30000, + 'high': np.random.rand(num_test_rows) * 1200 + 30000, + 'low': np.random.rand(num_test_rows) * 800 + 29800, + 'close': np.random.rand(num_test_rows) * 1000 + 30000, + 'volume': np.random.rand(num_test_rows) * 100 + 10 + } + # Gerar timestamps de 1h para trás a partir de agora + end_time = pd.Timestamp.now(tz='UTC') + start_time = end_time - pd.Timedelta(hours=num_test_rows - 1) + timestamps = pd.date_range(start=start_time, end=end_time, freq='h') + + # Ajustar se o número de timestamps não bater exatamente com num_test_rows + if len(timestamps) > num_test_rows: + timestamps = timestamps[:num_test_rows] + elif len(timestamps) < num_test_rows: + # Ajustar dados de exemplo para o número de timestamps disponíveis + for key in example_data: + example_data[key] = example_data[key][:len(timestamps)] + + test_ohlcv_df = pd.DataFrame(example_data, index=timestamps) + test_ohlcv_df['high'] = np.maximum(test_ohlcv_df['high'], test_ohlcv_df['close'], test_ohlcv_df['open']) + test_ohlcv_df['low'] = np.minimum(test_ohlcv_df['low'], test_ohlcv_df['close'], test_ohlcv_df['open']) + + test_logger.info(f"DataFrame de teste criado com {len(test_ohlcv_df)} linhas.") + + signal, probability = await predictor.predict_for_asset_ohlcv(test_ohlcv_df, api_operation_threshold=0.65) + + if signal is not None: + test_logger.info(f"Resultado do Teste - Sinal: {signal}, Probabilidade: {probability:.4f}") + else: + test_logger.error("Resultado do Teste - Predição falhou.") + +if __name__ == '__main__': + # Para rodar o teste: python rnn/app/models/rnn_predictor.py (ou o nome que você deu) + # Certifique-se que o config.py está acessível (ex: no mesmo diretório ou PYTHONPATH) + # E que os arquivos de modelo e scalers existem em app/model/ + import asyncio + # Definir um caminho base para que os imports relativos de config funcionem se rodar daqui + # Isso é um pouco hacky para teste direto do script. Idealmente, teste via pytest com estrutura de projeto. + import sys + # Adiciona o diretório 'app' ao sys.path para encontrar config.py + # Supondo que este script rnn_predictor.py está em rnn/app/models/ + # e config.py está em rnn/app/ + # Sobe dois níveis para 'rnn', depois desce para 'app' + # app_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + # if app_dir not in sys.path: + # sys.path.insert(0, app_dir) + # Para o import `from ..config import ...` funcionar, você geralmente roda o módulo + # como parte de um pacote, não diretamente. + # Para teste direto, você pode precisar de um import absoluto ou ajuste de PYTHONPATH. + + # Simplesmente para teste rápido, vamos assumir que o diretório de trabalho é 'rnn' + # e o import de config seria 'from app.config import ...' + # Se você rodar `python rnn/app/models/rnn_predictor.py` da raiz 'rnn', o import ..config deve funcionar. + + print("Rodando teste do RNNModelPredictor...") + asyncio.run(test_predictor()) \ No newline at end of file diff --git a/app/model/training_history.png b/app/model/training_history.png new file mode 100644 index 0000000000000000000000000000000000000000..e8045ec348d20f4e43f511a6cc0dd4bc9b7962a2 Binary files /dev/null and b/app/model/training_history.png differ diff --git a/app/requeriments.txt b/app/requeriments.txt new file mode 100644 index 0000000000000000000000000000000000000000..b5bea182a46a50abfc48a4ca4e15784a3f20b3ed --- /dev/null +++ b/app/requeriments.txt @@ -0,0 +1,6 @@ +fastapi +uvicorn +tensorflow +scikit-learn +numpy +pandas_ta diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..95dc34e370eaf74e61ec8c503f12e680e64ecfcf --- /dev/null +++ b/app/utils/__init__.py @@ -0,0 +1 @@ +# This file makes 'utils' a Python subpackage of 'app'. \ No newline at end of file diff --git a/app/utils/ccxt_utils.py b/app/utils/ccxt_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..4d380fbae09722f149cc273f91744992e4141f47 --- /dev/null +++ b/app/utils/ccxt_utils.py @@ -0,0 +1,141 @@ +import os +import ccxt.async_support as ccxt +from typing import Dict, Any, List, Optional, Tuple + + +# from .utils.logger import get_logger +# logger = get_logger() +# Ou logger passado como argumento para as funções + +# Variáveis de ambiente lidas uma vez no início ou passadas para funções +CCXT_EXCHANGE_ID_ENV = os.environ.get("CCXT_EXCHANGE_ID", "binance") +CCXT_API_KEY_ENV = os.environ.get("CCXT_API_KEY") +CCXT_API_SECRET_ENV = os.environ.get("CCXT_API_SECRET") +CCXT_API_PASSWORD_ENV = os.environ.get("CCXT_API_PASSWORD") +CCXT_SANDBOX_MODE_ENV = os.environ.get("CCXT_SANDBOX_MODE", "false").lower() == "true" + + +async def get_ccxt_exchange(logger_instance) -> Optional[ccxt.Exchange]: + """ + Inicializa e retorna uma instância da exchange ccxt. + Retorna None se a configuração estiver ausente ou a inicialização falhar. + """ + if not CCXT_API_KEY_ENV or not CCXT_API_SECRET_ENV: + logger_instance.warning("CCXT_API_KEY ou CCXT_API_SECRET não configurados. Não é possível inicializar a exchange.") + return None + + try: + exchange_class = getattr(ccxt, CCXT_EXCHANGE_ID_ENV) + config = { + 'apiKey': CCXT_API_KEY_ENV, + 'secret': CCXT_API_SECRET_ENV, + 'enableRateLimit': True, + } + if CCXT_API_PASSWORD_ENV: + config['password'] = CCXT_API_PASSWORD_ENV + + exchange = exchange_class(config) + + if CCXT_SANDBOX_MODE_ENV: + # Configurar sandbox/testnet + + # Método set_sandbox_mode (preferível) + if hasattr(exchange, 'set_sandbox_mode') and callable(exchange.set_sandbox_mode): + try: + exchange.set_sandbox_mode(True) + logger_instance.info(f"CCXT: Modo SANDBOX ativado para {CCXT_EXCHANGE_ID_ENV} via set_sandbox_mode.") + except Exception as e_sandbox: + logger_instance.warning(f"CCXT: Tentativa de set_sandbox_mode para {CCXT_EXCHANGE_ID_ENV} falhou: {e_sandbox}. Tentando URL de teste.") + # Tentar fallback para URL de teste se set_sandbox_mode falhar + if 'test' in exchange.urls: + exchange.urls['api'] = exchange.urls['test'] + logger_instance.info(f"CCXT: URLs alteradas para TESTNET para {CCXT_EXCHANGE_ID_ENV} (fallback).") + else: + logger_instance.warning(f"CCXT: Nenhuma URL de teste encontrada para {CCXT_EXCHANGE_ID_ENV} como fallback.") + # Alterar URLs diretamente (se set_sandbox_mode não estiver disponível) + elif 'test' in exchange.urls: + exchange.urls['api'] = exchange.urls['test'] + logger_instance.info(f"CCXT: URLs alteradas para TESTNET para {CCXT_EXCHANGE_ID_ENV} (método direto).") + else: + logger_instance.warning(f"CCXT: Modo SANDBOX solicitado, mas não há método set_sandbox_mode nem URL de teste para {CCXT_EXCHANGE_ID_ENV}.") + + # Carregar mercados, útil para todas as operações + # await exchange.load_markets() + # logger_instance.info(f"CCXT: Mercados carregados para {CCXT_EXCHANGE_ID_ENV}.") + return exchange + + except AttributeError: + logger_instance.error(f"CCXT: Exchange ID '{CCXT_EXCHANGE_ID_ENV}' inválida ou não suportada.") + return None + except Exception as e: + logger_instance.error(f"CCXT: Erro ao inicializar exchange {CCXT_EXCHANGE_ID_ENV}: {str(e)}", exc_info=True) + return None + + +async def fetch_crypto_data( + exchange: ccxt.Exchange, + pairs: List[str], + logger_instance +) -> Tuple[Dict[str, Any], bool, str]: + """ + Busca dados de mercado (ticker, OHLCV) para uma lista de pares de cripto. + Retorna: (dados_coletados, sucesso, mensagem_de_erro_detalhada) + """ + collected_data: Dict[str, Any] = {} + fetch_successful = True + error_message = "" + + logger_instance.info(f"CCXT: Iniciando coleta de dados para pares: {pairs}") + + for pair_symbol in pairs: + pair_data_key = pair_symbol.replace("/", "_") + current_pair_data = {} + try: + if exchange.has['fetchTicker']: + ticker = await exchange.fetch_ticker(pair_symbol) + current_pair_data['ticker'] = { + 'last': ticker.get('last'), 'bid': ticker.get('bid'), 'ask': ticker.get('ask'), + 'volume': ticker.get('baseVolume'), 'timestamp': ticker.get('timestamp') + } + + if exchange.has['fetchOHLCV']: + # Parametrizar timeframe e limit + ohlcv = await exchange.fetch_ohlcv(pair_symbol, timeframe='1h', limit=72) + current_pair_data['ohlcv_1h'] = ohlcv + + # Adicionar mais tipos de dados necessários (order book, trades) + + collected_data[pair_data_key] = current_pair_data + logger_instance.info(f"CCXT: Dados coletados para {pair_symbol}: Ticker OK, OHLCV OK (len: {len(ohlcv if 'ohlcv' in current_pair_data else [])})") + + except ccxt.NetworkError as e_net: + logger_instance.error(f"CCXT: Erro de REDE ao buscar dados para {pair_symbol}: {e_net}") + error_message += f"NetworkError for {pair_symbol}: {e_net}; " + fetch_successful = False # Flha parcial ou total + break # Interrompe a coleta para todos + except ccxt.ExchangeError as e_exc: + logger_instance.error(f"CCXT: Erro da EXCHANGE ao buscar dados para {pair_symbol}: {e_exc}") + error_message += f"ExchangeError for {pair_symbol}: {e_exc}; " + fetch_successful = False + # Algumas ExchangeErrors podem ser específicas do par (par não existe), + # Continuar para outros pares. Outras podem ser fatais (API key inválida). + # Considerar falha e parar para este par. + collected_data[pair_data_key] = {"error": str(e_exc)} # Registra o erro para o par + except Exception as e_gen: + logger_instance.error(f"CCXT: Erro GERAL ao buscar dados para {pair_symbol}: {e_gen}", exc_info=True) + error_message += f"General error for {pair_symbol}: {e_gen}; " + fetch_successful = False + collected_data[pair_data_key] = {"error": str(e_gen)} + + if not fetch_successful: + logger_instance.warning(f"CCXT: Coleta de dados de cripto encontrou erros. Detalhes: {error_message}") + else: + logger_instance.info("CCXT: Coleta de dados de cripto concluída com sucesso.") + + return collected_data, fetch_successful, error_message + +# Adicione aqui funções para executar ordens (CREATE_ORDER_PLACEHOLDER) +# async def create_crypto_order(...) -> Tuple[Optional[Dict], bool, str]: +# async def fetch_order_status(...) + +# Adicionar funções para fechar posições, buscar balanços. \ No newline at end of file diff --git a/app/utils/cria_h5.py b/app/utils/cria_h5.py new file mode 100644 index 0000000000000000000000000000000000000000..0d343d899d3668ead1a13f3cbbef66bf08b09570 --- /dev/null +++ b/app/utils/cria_h5.py @@ -0,0 +1,5 @@ +# +import tensorflow as tf +placeholder_model = tf.keras.Sequential([tf.keras.layers.Dense(1, input_shape=(10,))]) # Ajuste input_shape +placeholder_model.compile(optimizer='adam', loss='mse') +placeholder_model.save("app/model/model.h5") \ No newline at end of file diff --git a/app/utils/logger.py b/app/utils/logger.py new file mode 100644 index 0000000000000000000000000000000000000000..28310ce85354a78752bde296d94f10d91836eaea --- /dev/null +++ b/app/utils/logger.py @@ -0,0 +1,50 @@ +import logging +import logging.handlers +import json +import os +import sys +from datetime import datetime +from dotenv import load_dotenv + +load_dotenv() + +LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper() +LOG_DIR = "logs" +LOG_FILE = f"{LOG_DIR}/atcoin_{datetime.utcnow().strftime('%Y-%m-%d')}.log" + +class JsonFormatter(logging.Formatter): + def format(self, record): + log_record = { + "timestamp": datetime.utcnow().isoformat(), + "level": record.levelname, + "message": record.getMessage(), + "name": record.name, + "module": record.module, + "function": record.funcName, + "line": record.lineno, + } + return json.dumps(log_record) + +def get_logger(name: str = "atcoin"): + logger = logging.getLogger(name) + logger.setLevel(LOG_LEVEL) + + formatter = JsonFormatter() + + # Evita múltiplos handlers duplicados + if not logger.handlers: + try: + os.makedirs(LOG_DIR, exist_ok=True) + file_handler = logging.handlers.TimedRotatingFileHandler( + LOG_FILE, when="midnight", backupCount=7, encoding="utf-8" + ) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + except Exception as e: + # Fallback para stderr + stream_handler = logging.StreamHandler(sys.stderr) + stream_handler.setFormatter(formatter) + logger.addHandler(stream_handler) + logger.warning(f"Falha ao configurar arquivo de log: {e}") + + return logger diff --git a/app/utils/predict.py b/app/utils/predict.py new file mode 100644 index 0000000000000000000000000000000000000000..d36d6893317e5cbc54fd29a6f43b5f0a4d0cc307 --- /dev/null +++ b/app/utils/predict.py @@ -0,0 +1,12 @@ +import numpy as np +import tensorflow as tf +from app.model.preprocess import preprocess_input_data + +model = tf.keras.models.load_model("app/model/model.h5") + +def predict_quality(raw_data): + processed = preprocess_input_data(raw_data) + preds = model.predict(processed) + return [int(p > 0.5) for p in preds] + + diff --git a/cnn_model.py b/cnn_model.py new file mode 100644 index 0000000000000000000000000000000000000000..9ee5739a7cafb460d8f69cb7dea144a16922a8d3 --- /dev/null +++ b/cnn_model.py @@ -0,0 +1,31 @@ +import pandas as pd +import numpy as np +from sklearn.preprocessing import MinMaxScaler +from tensorflow.keras.models import Sequential +from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten, Dense, Dropout +from tensorflow.keras.optimizers import Adam + +def carregar_e_preparar_dados(caminho_csv): + df = pd.read_csv(caminho_csv, parse_dates=["Date"], index_col="Date") + df = df[["Open", "High", "Low", "Close", "Volume"]] + scaler = MinMaxScaler() + dados_escalados = scaler.fit_transform(df) + X, y = [], [] + for i in range(len(dados_escalados) - 60): + X.append(dados_escalados[i:i+60]) + y.append(dados_escalados[i+60][3]) + return np.array(X), np.array(y), scaler + +def criar_modelo_cnn(input_shape): + model = Sequential([ + Conv1D(64, 3, activation='relu', input_shape=input_shape), + MaxPooling1D(2), + Conv1D(128, 3, activation='relu'), + MaxPooling1D(2), + Flatten(), + Dense(64, activation='relu'), + Dropout(0.2), + Dense(1) + ]) + model.compile(optimizer=Adam(0.001), loss='mean_squared_error') + return model diff --git a/cnn_model_trainer.py b/cnn_model_trainer.py new file mode 100644 index 0000000000000000000000000000000000000000..82b718bf29959e59b935c99f8dca4d91bbee7753 --- /dev/null +++ b/cnn_model_trainer.py @@ -0,0 +1,62 @@ +import pandas as pd +import numpy as np +from sklearn.model_selection import train_test_split +from sklearn.preprocessing import StandardScaler +import tensorflow as tf +from tensorflow.keras.models import Sequential +from tensorflow.keras.layers import Dense, Conv1D, MaxPooling1D, Flatten, Dropout +import matplotlib.pyplot as plt + +# 1. Carregar os dados +df = pd.read_csv("sp500_ratios.csv") + +# 2. Pré-processar os dados +df = df.drop(columns=["Date", "Ticker"]) # Colunas não numéricas +df = df.dropna() # Remove linhas com valores ausentes + +# 3. Separar recursos e alvo (vamos usar "ROE - Return On Equity" como alvo de previsão) +target_column = "ROE - Return On Equity" +X = df.drop(columns=[target_column]).values +y = df[target_column].values + +# 4. Normalizar +scaler = StandardScaler() +X_scaled = scaler.fit_transform(X) + +# 5. Redimensionar para CNN (samples, time_steps, features) +X_scaled = X_scaled.reshape((X_scaled.shape[0], X_scaled.shape[1], 1)) # 1D CNN + +# 6. Divisão treino/teste +X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2, random_state=42) + +# 7. Modelo CNN +model = Sequential([ + Conv1D(32, kernel_size=3, activation='relu', input_shape=(X_train.shape[1], 1)), + MaxPooling1D(pool_size=2), + Conv1D(64, kernel_size=3, activation='relu'), + MaxPooling1D(pool_size=2), + Flatten(), + Dense(128, activation='relu'), + Dropout(0.3), + Dense(1) # Saída para regressão +]) + +model.compile(optimizer='adam', loss='mse', metrics=['mae']) + +# 8. Treinamento +history = model.fit(X_train, y_train, validation_split=0.2, epochs=50, batch_size=32, verbose=1) + +# 9. Avaliação +loss, mae = model.evaluate(X_test, y_test) +print(f"\n✅ Test MAE: {mae:.4f}") + +# 10. Plot +plt.plot(history.history['mae'], label='MAE Treino') +plt.plot(history.history['val_mae'], label='MAE Validação') +plt.xlabel("Épocas") +plt.ylabel("Erro Absoluto Médio") +plt.title("Performance do Modelo") +plt.legend() +plt.grid() +plt.tight_layout() +plt.show() diff --git a/cnn_preprocessor.py b/cnn_preprocessor.py new file mode 100644 index 0000000000000000000000000000000000000000..c568100c35a2d8f361c2a297374b86595f455d1c --- /dev/null +++ b/cnn_preprocessor.py @@ -0,0 +1,55 @@ +import pandas as pd +import numpy as np +from sklearn.preprocessing import MinMaxScaler +from pathlib import Path + +def load_and_preprocess_data(filepath, window_size=30, features=None): + # 1. Carregar os dados + df = pd.read_csv(filepath) + print("🔍 Dados carregados:", df.shape) + + # 2. Conversão de data + df['Date'] = pd.to_datetime(df['Date']) + df = df.sort_values(['Ticker', 'Date']) + + # 3. Seleção de colunas + if features is None: + features = ['Open', 'Close', 'Volume', 'Asset Turnover', 'Current Ratio', + 'Debt/Equity Ratio', 'Gross Margin', 'Net Profit Margin', 'ROA - Return On Assets'] + + df = df[['Ticker', 'Date'] + features].dropna() + + # 4. Normalização por Ticker + scalers = {} + grouped = df.groupby('Ticker') + sequences = [] + tickers = [] + + for ticker, group in grouped: + scaler = MinMaxScaler() + values = scaler.fit_transform(group[features]) + scalers[ticker] = scaler + + closes = group['Close'].values # Para prever retorno futuro + + # 5. Janela deslizante para sequências temporais + for i in range(len(values) - window_size): + seq = values[i:i+window_size] + label = closes[i+window_size] # Preço após a janela + sequences.append(seq) + tickers.append(ticker) + + + + X = np.array([s for s, _ in sequences]) + y = np.array([l for _, l in sequences]) + + + + X = np.array(sequences) + print(f"✅ Total de sequências geradas: {X.shape[0]} | Formato da entrada: {X.shape}") + return X, tickers, scalers + +if __name__ == "__main__": + filepath = Path("sp500_ratios.csv") + X, tickers, scalers = load_and_preprocess_data(filepath) diff --git a/cnn_visualizer.py b/cnn_visualizer.py new file mode 100644 index 0000000000000000000000000000000000000000..ddb233d64d31f11bc4424776362593d1cf68d49a --- /dev/null +++ b/cnn_visualizer.py @@ -0,0 +1,42 @@ +import matplotlib.pyplot as plt +import json + +def plot_training_history(history): + # Caso tenha sido salvo como objeto History + if isinstance(history, dict): + hist = history + else: + hist = history.history # Keras History object + + plt.figure(figsize=(12, 5)) + + # Plot Loss + plt.subplot(1, 2, 1) + plt.plot(hist['loss'], label='Train Loss') + plt.plot(hist['val_loss'], label='Val Loss') + plt.title('Loss por Época') + plt.xlabel('Época') + plt.ylabel('MSE') + plt.legend() + + # Plot MAE + plt.subplot(1, 2, 2) + plt.plot(hist['mae'], label='Train MAE') + plt.plot(hist['val_mae'], label='Val MAE') + plt.title('Erro Absoluto Médio por Época') + plt.xlabel('Época') + plt.ylabel('MAE') + plt.legend() + + plt.tight_layout() + plt.show() + + +# Exemplo: carregando histórico salvo como JSON (opcional) +def load_history_from_json(filepath): + with open(filepath, 'r') as f: + return json.load(f) + +# Exemplo de uso: +# history = load_history_from_json("cnn_training_history.json") +# plot_training_history(history) diff --git a/data.json b/data.json new file mode 100644 index 0000000000000000000000000000000000000000..0207dbcfa85cffc3c2cb963670b470de14cb99c2 --- /dev/null +++ b/data.json @@ -0,0 +1,38 @@ +[ + { + "nome": "Coletor de Dados", + "status": "Ativo", + "ultima_acao": "Captura de dados da B3 às 13:00", + "proxima_execucao": "2025-05-09 14:00", + "resultado": "+2.8%" + }, + { + "nome": "Analisador de Sentimentos", + "status": "Ativo", + "ultima_acao": "Análise do Twitter sobre VALE3", + "proxima_execucao": "2025-05-09 14:15", + "resultado": "+1.4%" + }, + { + "nome": "Executor de Investimentos", + "status": "Ativo", + "ultima_acao": "Compra de PETR4 via Algotrader", + "proxima_execucao": "2025-05-09 14:30", + "resultado": "+3.1%" + }, + { + "nome": "Ajuste de Portfólio", + "status": "Em espera", + "ultima_acao": "Rebalanceamento de ativos", + "proxima_execucao": "2025-05-09 15:00", + "resultado": "+0.9%" + }, + { + "nome": "Gerenciador de Risco", + "status": "Ativo", + "ultima_acao": "Redução de exposição em ativos voláteis", + "proxima_execucao": "2025-05-09 14:45", + "resultado": "+1.7%" + } + ] + \ No newline at end of file diff --git a/data/cnn_predictor.py b/data/cnn_predictor.py new file mode 100644 index 0000000000000000000000000000000000000000..fa811932b93b9910bb72870fcb498dff8f936543 --- /dev/null +++ b/data/cnn_predictor.py @@ -0,0 +1,35 @@ +import numpy as np +import matplotlib.pyplot as plt +from keras.models import load_model + +def load_model_and_predict(model_path, X_val, y_val, ticker_index=0): + """ + Carrega um modelo CNN salvo e plota previsão vs. valor real para um ticker específico. + + Args: + model_path (str): Caminho para o modelo salvo (ex: 'cnn_model.keras') + X_val (np.ndarray): Dados de entrada de validação (shape: [tickers, time, features]) + y_val (np.ndarray): Valores reais de saída de validação + ticker_index (int): Índice do ticker a ser visualizado + """ + print(f"Carregando modelo de {model_path}...") + model = load_model(model_path) + + X_sample = X_val[ticker_index] + y_true = y_val[ticker_index] + + if len(X_sample.shape) == 2: + X_sample = X_sample[np.newaxis, ...] + + y_pred = model.predict(X_sample).squeeze() + y_true = y_true.squeeze() + + plt.figure(figsize=(10, 5)) + plt.plot(y_true, label='Real', linewidth=2) + plt.plot(y_pred, label='Previsto', linestyle='--') + plt.title(f'CNN - Previsão vs Real (Ticker {ticker_index})') + plt.xlabel('Dias Futuros') + plt.ylabel('Preço Normalizado') + plt.legend() + plt.tight_layout() + plt.show() diff --git a/deep_mql_model.py b/deep_mql_model.py new file mode 100644 index 0000000000000000000000000000000000000000..08e95118dde08dd903412631c5b6be359adae221 --- /dev/null +++ b/deep_mql_model.py @@ -0,0 +1,235 @@ +import pandas as pd +import numpy as np +from sklearn.preprocessing import MinMaxScaler +from tensorflow.keras.models import Sequential +from tensorflow.keras.layers import LSTM, Dense, Dropout +# (Potentially add more imports for other model types or libraries later) + +def preprocess_data_for_deep_mql(df: pd.DataFrame, look_back: int = 60, features_cols=['Close', 'Volume'], target_col='Close'): + """ + Prepares data for a deep learning model (e.g., LSTM) for MQL-like tasks. + - Scales features. + - Creates sequences for time series forecasting. + """ + df_copy = df.copy() + + # Feature engineering (can be expanded) + df_copy['returns'] = df_copy['Close'].pct_change() + df_copy['ma10'] = df_copy['Close'].rolling(window=10).mean() + df_copy['ma30'] = df_copy['Close'].rolling(window=30).mean() + df_copy = df_copy.dropna() + + all_cols = list(set(features_cols + [target_col] + ['returns', 'ma10', 'ma30'])) + data_to_scale = df_copy[all_cols].values + + scaler = MinMaxScaler(feature_range=(0, 1)) + scaled_data = scaler.fit_transform(data_to_scale) + + # Find the index of the target column in the scaled data + try: + # Attempt to get column names if df_copy[all_cols] is a DataFrame + scaled_df_cols = df_copy[all_cols].columns.tolist() + target_idx_in_scaled = scaled_df_cols.index(target_col) + except AttributeError: + # Fallback if it's already a NumPy array or has no columns attribute + # This assumes target_col was one of the original features_cols or added deterministically + # A more robust solution might be needed if column order is not guaranteed + if target_col in features_cols: + target_idx_in_scaled = features_cols.index(target_col) + elif target_col == 'returns': # Example, adjust if more features are added before target + target_idx_in_scaled = len(features_cols) + elif target_col == 'ma10': + target_idx_in_scaled = len(features_cols) + 1 + elif target_col == 'ma30': + target_idx_in_scaled = len(features_cols) + 2 + else: # Default to first column if not found, or raise error + print(f"Warning: Target column '{target_col}' not reliably found in scaled data. Defaulting to index 0.") + target_idx_in_scaled = 0 + + + X, y = [], [] + for i in range(look_back, len(scaled_data)): + X.append(scaled_data[i-look_back:i]) + y.append(scaled_data[i, target_idx_in_scaled]) # Predicting the target column + + return np.array(X), np.array(y), scaler, df_copy[all_cols].columns.tolist() + +def create_deep_mql_model(input_shape): + """ + Creates a Deep Learning model (LSTM example) for MQL-like tasks. + This is a basic example and can be significantly enhanced. + """ + model = Sequential([ + LSTM(units=50, return_sequences=True, input_shape=input_shape), + Dropout(0.2), + LSTM(units=50, return_sequences=False), + Dropout(0.2), + Dense(units=25, activation='relu'), + Dense(units=1) # Output: e.g., predicted price or a value to derive a signal + ]) + model.compile(optimizer='adam', loss='mean_squared_error') + return model + +def generate_trading_signals(model, data_X, scaler, last_known_price, threshold=0.005): + """ + Generates trading signals based on model predictions. + - Predicts next step. + - Compares prediction with current price to generate signal. + + Signal: 1 (Buy), -1 (Sell), 0 (Hold) + """ + # Ensure data_X is in the correct shape for prediction (e.g., for a single prediction) + # It should be (1, look_back, num_features) + if data_X.ndim == 2: # If it's a single sequence (look_back, num_features) + data_X = np.expand_dims(data_X, axis=0) + + predicted_scaled_value = model.predict(data_X) + + # Inverse transform: + # We need to create a dummy array with the same number of features as during scaling, + # then place our predicted value in the correct column (target column) before inverse transforming. + num_features = data_X.shape[2] + dummy_array = np.zeros((len(predicted_scaled_value), num_features)) + + # Assuming the model predicts the 'Close' price and 'Close' was the first feature during scaling. + # This needs to be robust. Let's assume target was the first column for simplicity here. + # A better way is to pass the index of the target column used during scaling. + target_column_index_in_scaler = 0 # Placeholder: This should match the target_col's position during scaling + dummy_array[:, target_column_index_in_scaler] = predicted_scaled_value.ravel() + + try: + predicted_value = scaler.inverse_transform(dummy_array)[:, target_column_index_in_scaler] + except ValueError as e: + print(f"Error during inverse_transform: {e}") + print("Ensure dummy_array shape matches scaler's n_features_in_.") + print(f"dummy_array shape: {dummy_array.shape}, scaler.n_features_in_: {scaler.n_features_in_}") + # Fallback or re-raise + return 0 + + + signal = 0 + if predicted_value > last_known_price * (1 + threshold): + signal = 1 # Buy + elif predicted_value < last_known_price * (1 - threshold): + signal = -1 # Sell + return signal, predicted_value[0] + + +if __name__ == '__main__': + # Example Usage (requires financial_data_agent and more setup) + # from agents.financial_data_agent import fetch_historical_ohlcv + # raw_data_mql = fetch_historical_ohlcv("MSFT", period="2y", interval="1d") + + # For demonstration, creating a dummy DataFrame: + dates_mql = pd.date_range(start='2022-01-01', periods=500, freq='B') + data_mql_np = np.random.rand(500, 5) * 150 + 50 + raw_data_mql = pd.DataFrame(data_mql_np, index=dates_mql, columns=['Open', 'High', 'Low', 'Close', 'Volume']) + raw_data_mql['Close'] = raw_data_mql['Close'] + np.sin(np.linspace(0, 20, 500)) * 20 # Add some trend + + if not raw_data_mql.empty: + look_back_mql = 60 + # Use more features for DeepMQL + features_mql = ['Close', 'Volume', 'Open', 'High', 'Low'] + target_mql = 'Close' + + X_mql, y_mql, scaler_mql, scaled_cols_mql = preprocess_data_for_deep_mql( + raw_data_mql, + look_back=look_back_mql, + features_cols=features_mql, + target_col=target_mql + ) + + if X_mql.shape[0] > 0: + print(f"X_mql shape: {X_mql.shape}, y_mql shape: {y_mql.shape}") + + # 1. Create and Train Model + model_mql = create_deep_mql_model(input_shape=(X_mql.shape[1], X_mql.shape[2])) + print("Training DeepMQL model (example)...") + # Split data for training (e.g., first 80% for train, rest for test/simulation) + train_size = int(len(X_mql) * 0.8) + X_train_mql, y_train_mql = X_mql[:train_size], y_mql[:train_size] + + # Ensure y_train_mql is 2D for Keras if it's not already + if y_train_mql.ndim == 1: + y_train_mql = y_train_mql.reshape(-1, 1) + + model_mql.fit(X_train_mql, y_train_mql, epochs=10, batch_size=32, verbose=1) # Few epochs for demo + print("DeepMQL model trained.") + + # 2. Generate Signal for a new data point (example) + # Use the last 'look_back' points from the original data as input for prediction + if len(X_mql) > train_size: + last_sequence = X_mql[-1] # Last available sequence from our dataset (could be from test set) + # For a real scenario, this would be the latest live data + + # Get the actual last known closing price from the unscaled data + # The 'last_sequence' corresponds to data up to index -1. + # The price to compare against is raw_data_mql[target_mql].iloc[-1] + # (or the close price of the last day of last_sequence) + + # Find the original 'Close' price that corresponds to the end of the last_sequence + # The 'y' value for X_mql[-1] is y_mql[-1], which is the scaled target for the *next* period. + # We need the 'Close' price of the *last day included in* X_mql[-1]. + # The data used for X_mql was from df_copy in preprocess_data_for_deep_mql + # The last row of df_copy that contributes to X_mql[-1] is df_copy.iloc[len(df_copy) - 1 - (len(X_mql) - (len(X_mql)-1)) ] + # This is complex. Simpler: use the last 'Close' from the original dataframe that was part of the input to scaling. + # The 'scaled_data' was created from 'df_copy'. 'X' takes from 'scaled_data'. + # The last 'Close' price in 'df_copy' before any potential future data. + + # Let's get the unscaled 'Close' price from the day corresponding to the last observation in 'last_sequence' + # 'X_mql' was created from 'scaled_data'. 'scaled_data' was created from 'df_copy'. + # The last row of 'df_copy' is raw_data_mql.iloc[len(raw_data_mql)-1] after initial dropna. + # The 'last_known_price' should be the closing price of the last day in the 'last_sequence'. + + # To get the actual 'Close' price for the last day of the last_sequence: + # The 'X_mql' sequences end at index i-1 of scaled_data. + # So, the last day in X_mql[-1] corresponds to scaled_data[len(scaled_data)-2]. + # The corresponding original data is in df_copy used in preprocess_data_for_deep_mql. + # This df_copy was raw_data_mql after some processing. + # Let's use the last 'Close' from the raw_data_mql that was used to create X_mql. + # The number of rows in df_copy (after dropna) is len(scaled_data). + # The last 'Close' price in the data that could form a sequence. + + # Simplification for example: Use the last 'Close' price from the original raw_data_mql + # This assumes raw_data_mql is aligned with the sequences. + # A more robust way is to track indices or pass the unscaled 'Close' series. + + # The 'y' value is the target for the *next* period. + # The 'last_known_price' should be the 'Close' of the last day *in* the 'last_sequence'. + # If X_mql[-1] is data from t-look_back to t-1, then last_known_price is Close at t-1. + + # Get the original 'Close' column from raw_data_mql that corresponds to the scaled data + original_close_series = raw_data_mql['Close'].iloc[len(raw_data_mql) - len(X_mql) + X_mql.shape[1] -1 - (len(X_mql) - (np.where(X_mql == last_sequence)[0][0])) : len(raw_data_mql) - (len(X_mql) - (np.where(X_mql == last_sequence)[0][0]))] + # This is getting too complex for the example. Let's use a simpler reference. + # The last 'Close' price in the raw_data_mql that was part of the input to the last sequence. + # If X_mql has N samples, it means we used N+look_back-1 rows of data. + # The last row of raw_data_mql used for features for X_mql[-1] is raw_data_mql.iloc[some_index_ending_the_last_sequence] + + # Let's assume the last 'Close' price from the original dataframe is relevant. + # This is not perfectly aligned for prediction but good for a demo. + last_actual_close_price = raw_data_mql[target_mql].iloc[-1] + + + print(f"\nGenerating signal based on last sequence (shape: {last_sequence.shape})...") + print(f"Last actual close price for reference: {last_actual_close_price}") + + signal, predicted_price = generate_trading_signals( + model_mql, + last_sequence, + scaler_mql, # Pass the scaler used for X_mql + last_actual_close_price, # The actual close price of the last day in 'last_sequence' + threshold=0.001 # Smaller threshold for demo + ) + print(f"Predicted Next '{target_mql}': {predicted_price:.2f}") + if signal == 1: + print("Signal: BUY") + elif signal == -1: + print("Signal: SELL") + else: + print("Signal: HOLD") + else: + print("Not enough data to generate a signal after training split.") + else: + print("X_mql is empty. Check preprocessing for DeepMQL.") + else: + print("Raw data for MQL is empty. Check data fetching.") \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..fa04be3e79c02bb3074513ad2d9071acf751fb51 --- /dev/null +++ b/dockerfile @@ -0,0 +1,17 @@ +# Base image +FROM python:3.10-slim + +# Diretório de trabalho +WORKDIR /app + +# Copia os arquivos do projeto +COPY . /app + +# Instala as dependências +RUN pip install --upgrade pip && pip install -r requirements.txt + +# Porta usada pelo Uvicorn +EXPOSE 7860 + +# Comando de inicialização +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"] diff --git a/endpoints/__init__.py b/endpoints/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..fbf997c84f00c9d4ac3aa4c8deb539bc80dccd90 --- /dev/null +++ b/endpoints/__init__.py @@ -0,0 +1 @@ +# This file makes 'endpoints' a Python subpackage of 'rnn'. \ No newline at end of file diff --git a/endpoints/__pycache__/__init__.cpython-312.pyc b/endpoints/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a899aab2b45484fec7b29f45637fbea84b3a323 Binary files /dev/null and b/endpoints/__pycache__/__init__.cpython-312.pyc differ diff --git a/endpoints/__pycache__/predict.cpython-312.pyc b/endpoints/__pycache__/predict.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7995eedcd8b6a66320a4c8eaf475557b453959a0 Binary files /dev/null and b/endpoints/__pycache__/predict.cpython-312.pyc differ diff --git a/endpoints/predict.py b/endpoints/predict.py new file mode 100644 index 0000000000000000000000000000000000000000..e8c1c4f0f392834127269b45fbaf7a0a6ecb38d8 --- /dev/null +++ b/endpoints/predict.py @@ -0,0 +1,40 @@ +import os +from fastapi import APIRouter, Security, Depends, HTTPException +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from pydantic import BaseModel +import jwt +import numpy as np +import tensorflow as tf +from rnn.models.deep_portfolio import DeepPortfolioAI + + +router = APIRouter() +security = HTTPBearer() +MODEL = DeepPortfolioAI(num_assets=10) +SECRET_KEY = os.getenv("SECRET_KEY") + +class PredictionRequest(BaseModel): + market_data: list + news_data: list + +def verify_jwt(credentials: HTTPAuthorizationCredentials = Security(security)): + try: + return jwt.decode(credentials.credentials, SECRET_KEY, algorithms=["HS256"]) + except: + raise HTTPException(status_code=401, detail="Invalid token") + +@router.post("/predict") +async def predict( + request: PredictionRequest, + user=Depends(verify_jwt) +): + try: + market_tensor = tf.convert_to_tensor(request.market_data, dtype=tf.float32) + prediction = MODEL([market_tensor, request.news_data]) + return { + "success": True, + "prediction": prediction.numpy().tolist(), + "timestamp": np.datetime64('now').astype(str) + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/huggingface_data_loader.py b/huggingface_data_loader.py new file mode 100644 index 0000000000000000000000000000000000000000..a2330135a88d4658482213c5ad069467d173fa7d --- /dev/null +++ b/huggingface_data_loader.py @@ -0,0 +1,24 @@ +import pandas as pd +from zipfile import ZipFile +import os + +# Caminho local do cache do Hugging Face +hf_cache_path = os.path.expanduser("~/.cache/huggingface/hub/datasets--pmoe7--SP_500_Stocks_Data-ratios_news_price_10_yrs/snapshots") + +# Descobre snapshot baixado +snapshot_dir = os.listdir(hf_cache_path)[0] +base_path = os.path.join(hf_cache_path, snapshot_dir) + +# Carrega os dados de preços + múltiplos fundamentalistas +ratios_path = os.path.join(base_path, "sp500_daily_ratios_20yrs.zip") +with ZipFile(ratios_path, 'r') as zip_ref: + zip_ref.extractall("data/") +ratios_df = pd.read_csv("data/sp500_daily_ratios_20yrs.csv") + +# Carrega os dados de notícias e sentimentos +news_path = os.path.join(base_path, "sp500_news_290k_articles.csv") +news_df = pd.read_csv(news_path) + +# Salva para uso futuro +ratios_df.to_csv("data/sp500_ratios.csv", index=False) +news_df.to_csv("data/sp500_news.csv", index=False) diff --git a/index.html b/index.html new file mode 100644 index 0000000000000000000000000000000000000000..dfec96424e9f61c28f1270a719841c8664637f34 --- /dev/null +++ b/index.html @@ -0,0 +1,97 @@ + + + + + + Agentes ATCoin Token + + + + +
+

Agentes da Inteligência Artificial — AIBANK Token

+
+ + + + + + + + + + + +
AgenteStatusÚltima AçãoPróxima ExecuçãoResultado
+
+ +
+

Performance dos Agentes

+ +
+
+ + + + diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e08eeaafc7ed9291b1daf65a7316824a43212fb9 Binary files /dev/null and b/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/models/__pycache__/deep_portfolio.cpython-312.pyc b/models/__pycache__/deep_portfolio.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..08d17e0429eb4959d041b325597c7b4e9176d6c3 Binary files /dev/null and b/models/__pycache__/deep_portfolio.cpython-312.pyc differ diff --git a/models/custom_policies.py b/models/custom_policies.py new file mode 100644 index 0000000000000000000000000000000000000000..d612e1cf229af837c855fca9bccd38fd5e866e2a --- /dev/null +++ b/models/custom_policies.py @@ -0,0 +1,121 @@ +# rnn/agents/custom_policies.py (NOVO ARQUIVO, ou adicione ao deep_portfolio.py) + +import gymnasium as gym # Usar gymnasium +import tensorflow as tf +from stable_baselines3.common.torch_layers import BaseFeaturesExtractor as PyTorchBaseFeaturesExtractor +# Para TensorFlow, precisamos de um extrator de features compatível ou construir a política de forma diferente. +# Stable Baselines3 tem melhor suporte nativo para PyTorch. Para TF, é um pouco mais manual. +# VAMOS USAR A ABORDAGEM DE POLÍTICA CUSTOMIZADA COM TF DIRETAMENTE. +from stable_baselines3.common.policies import ActorCriticPolicy +from typing import List, Dict, Any, Union, Type +# Importar sua rede e configs +from .deep_portfolio import DeepPortfolioAgentNetwork +# from ..config import (NUM_ASSETS, WINDOW_SIZE, NUM_FEATURES_PER_ASSET, ...) # Importe do seu config real +# VALORES DE EXEMPLO (PEGUE DO SEU CONFIG.PY REAL) +NUM_ASSETS_POLICY = 4 +WINDOW_SIZE_POLICY = 60 +NUM_FEATURES_PER_ASSET_POLICY = 26 +# Hiperparâmetros para DeepPortfolioAgentNetwork quando usada como extrator +ASSET_CNN_FILTERS1_POLICY = 32 +ASSET_CNN_FILTERS2_POLICY = 64 +ASSET_LSTM_UNITS1_POLICY = 64 +ASSET_LSTM_UNITS2_POLICY = 32 # Esta será a dimensão das features latentes para ator/crítico +ASSET_DROPOUT_POLICY = 0.2 +MHA_NUM_HEADS_POLICY = 4 +MHA_KEY_DIM_DIVISOR_POLICY = 2 # Para key_dim = 32 // 2 = 16 +FINAL_DENSE_UNITS1_POLICY = 128 +FINAL_DENSE_UNITS2_POLICY = ASSET_LSTM_UNITS2_POLICY # A saída da dense2 SÃO as features latentes +FINAL_DROPOUT_POLICY = 0.3 + + +class TFPortfolioFeaturesExtractor(tf.keras.layers.Layer): # Herda de tf.keras.layers.Layer + """ + Extrator de features customizado para SB3 que usa DeepPortfolioAgentNetwork. + A observação do ambiente é (batch, window, num_assets * num_features_per_asset). + A saída são as features latentes (batch, latent_dim). + """ + def __init__(self, observation_space: gym.spaces.Box, features_dim: int = ASSET_LSTM_UNITS2_POLICY): + super(TFPortfolioFeaturesExtractor, self).__init__() + self.features_dim = features_dim # SB3 usa isso para saber o tamanho da saída + + # Instanciar a rede base para extrair features + # Ela deve retornar as ativações ANTES da camada softmax de alocação. + self.network = DeepPortfolioAgentNetwork( + num_assets=NUM_ASSETS_POLICY, + sequence_length=WINDOW_SIZE_POLICY, + num_features_per_asset=NUM_FEATURES_PER_ASSET_POLICY, + asset_cnn_filters1=ASSET_CNN_FILTERS1_POLICY, + asset_cnn_filters2=ASSET_CNN_FILTERS2_POLICY, + asset_lstm_units1=ASSET_LSTM_UNITS1_POLICY, + asset_lstm_units2=ASSET_LSTM_UNITS2_POLICY, # Define a saída do asset_processor + asset_dropout=ASSET_DROPOUT_POLICY, + mha_num_heads=MHA_NUM_HEADS_POLICY, + mha_key_dim_divisor=MHA_KEY_DIM_DIVISOR_POLICY, + final_dense_units1=FINAL_DENSE_UNITS1_POLICY, + final_dense_units2=self.features_dim, # A saída da dense2 é a nossa feature latente + final_dropout=FINAL_DROPOUT_POLICY, + output_latent_features=True # MUITO IMPORTANTE! + ) + print("TFPortfolioFeaturesExtractor inicializado e usando DeepPortfolioAgentNetwork (output_latent_features=True).") + + def call(self, observations: tf.Tensor, training: bool = False) -> tf.Tensor: + # A DeepPortfolioAgentNetwork já lida com o fatiamento e processamento. + # Ela foi configurada para retornar features latentes. + return self.network(observations, training=training) + + + + + + +class CustomPortfolioPolicySB3(ActorCriticPolicy): + def __init__( + self, + observation_space: gym.spaces.Space, + action_space: gym.spaces.Space, + lr_schedule, # Função que retorna a taxa de aprendizado + net_arch: Optional[List[Union[int, Dict[str, List[int]]]]] = None, # Arquitetura para MLPs pós-extrator + activation_fn: Type[tf.Module] = tf.nn.relu, # Usar tf.nn.relu para TF + # Adicionar quaisquer outros parâmetros específicos que o extrator precise + features_extractor_kwargs: Optional[Dict[str, Any]] = None, + **kwargs, + ): + if features_extractor_kwargs is None: + features_extractor_kwargs = {} + + # A dimensão das features que o nosso extrator PortfolioFeatureExtractor vai cuspir. + # Deve ser igual a ASSET_LSTM_UNITS2_POLICY (ou final_dense_units2 do extrator) + # Se não for passado, o construtor do ActorCriticPolicy pode tentar inferir. + # Vamos passar explicitamente para garantir. + features_extractor_kwargs.setdefault("features_dim", ASSET_LSTM_UNITS2_POLICY) # Ou o valor que você definiu + + super().__init__( + observation_space, + action_space, + lr_schedule, + net_arch=net_arch, # Para camadas Dense APÓS o extrator de features + activation_fn=activation_fn, + features_extractor_class=TFPortfolioFeaturesExtractor, + features_extractor_kwargs=features_extractor_kwargs, + **kwargs, + ) + # Otimizador é criado na classe base. + # As redes de ator e crítico são construídas no método _build da classe base, + # usando o self.features_extractor e depois o self.mlp_extractor (que é + # construído com base no net_arch). + + # Não precisamos sobrescrever _build_mlp_extractor se o features_extractor + # já fizer o trabalho pesado e o net_arch padrão para as cabeças for suficiente. + # Se quisermos MLPs customizados para ator e crítico APÓS o extrator: + # def _build_mlp_extractor(self) -> None: + # # self.mlp_extractor é uma instância de MlpExtractor (ou similar) + # # A entrada para ele é self.features_extractor.features_dim + # # Aqui, net_arch definiria a estrutura do mlp_extractor + # self.mlp_extractor = MlpExtractor( + # feature_dim=self.features_extractor.features_dim, + # net_arch=self.net_arch, # net_arch é uma lista de ints para camadas da política e valor + # activation_fn=self.activation_fn, + # device=self.device, + # ) + # As redes de ação e valor (action_net, value_net) são então criadas + # no _build da classe ActorCriticPolicy, no topo do mlp_extractor. \ No newline at end of file diff --git a/models/deep_portfolio.py b/models/deep_portfolio.py new file mode 100644 index 0000000000000000000000000000000000000000..8b7d525486f19554e8cea58238d42c24b13d499a --- /dev/null +++ b/models/deep_portfolio.py @@ -0,0 +1,193 @@ +# 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 \ No newline at end of file diff --git "a/models/deep_portfolio_extrator_features_Integrar DeepPortfolioAgentNetwork no Stable-Baselines3 (PPO)/deep_portfolio_teste_integra\303\247\303\243o.py" "b/models/deep_portfolio_extrator_features_Integrar DeepPortfolioAgentNetwork no Stable-Baselines3 (PPO)/deep_portfolio_teste_integra\303\247\303\243o.py" new file mode 100644 index 0000000000000000000000000000000000000000..8478deec4240b45b9aa5a701f9b2aefcd39315b6 --- /dev/null +++ "b/models/deep_portfolio_extrator_features_Integrar DeepPortfolioAgentNetwork no Stable-Baselines3 (PPO)/deep_portfolio_teste_integra\303\247\303\243o.py" @@ -0,0 +1,382 @@ +# rnn/agents/deep_portfolio.py (ou onde você tem DeepPortfolioAI / DeepPortfolioAgentNetwork) +import numpy as np +from tensorflow.keras import regularizers +import tensorflow as tf +from tensorflow.keras.layers import ( + Input, Conv1D, LSTM, Dense, Dropout, + MultiHeadAttention, Reshape, Concatenate, + TimeDistributed, GlobalAveragePooling1D, LayerNormalization +) +from tensorflow.keras.models import Model +# Comente as importações do transformers se não for testar o sentimento agora para simplificar +# from transformers import AutoTokenizer, TFAutoModelForSequenceClassification + +# --- DEFINIÇÕES DE CONFIGURAÇÃO (COPIE OU IMPORTE DO SEU CONFIG.PY) --- +# Se você não importar do config.py, defina-as aqui para o teste +WINDOW_SIZE_CONF = 60 +NUM_ASSETS_CONF = 4 # Ex: ETH, BTC, ADA, SOL +NUM_FEATURES_PER_ASSET_CONF = 26 # Número de features calculadas para CADA ativo + # (open_div_atr, ..., buy_condition_v1, etc.) +L2_REG = 0.0001 # Exemplo, use o valor do seu config + +# (Cole as classes AssetProcessor e DeepPortfolioAgentNetwork aqui se estiver em um novo script) +# Ou, se estiver no mesmo arquivo, elas já estarão definidas. + +# ... (Definição das classes AssetProcessor e DeepPortfolioAgentNetwork como na resposta anterior) ... +# Certifique-se que a classe DeepPortfolioAgentNetwork está usando estas constantes: +# num_assets=NUM_ASSETS_CONF, +# sequence_length=WINDOW_SIZE_CONF, +# num_features_per_asset=NUM_FEATURES_PER_ASSET_CONF + +class AssetProcessor(tf.keras.Model): + def __init__(self, sequence_length, num_features, cnn_filters1=32, cnn_filters2=64, lstm_units1=64, lstm_units2=32, dropout_rate=0.2, name="single_asset_processor_module", **kwargs): # Adicionado **kwargs + super(AssetProcessor, self).__init__(name=name, **kwargs) # Adicionado **kwargs + self.sequence_length = sequence_length + self.num_features = num_features + self.cnn_filters1 = cnn_filters1 # Salvar para get_config + self.cnn_filters2 = cnn_filters2 + self.lstm_units1 = lstm_units1 + self.lstm_units2 = lstm_units2 + self.dropout_rate = dropout_rate + + self.conv1 = Conv1D(filters=cnn_filters1, kernel_size=3, activation='relu', padding='same', name="asset_cnn1") + self.dropout_cnn1 = Dropout(dropout_rate, name="asset_cnn1_dropout") + self.conv2 = Conv1D(filters=cnn_filters2, kernel_size=3, activation='relu', padding='same', name="asset_cnn2") + self.dropout_cnn2 = Dropout(dropout_rate, name="asset_cnn2_dropout") + self.lstm1 = LSTM(lstm_units1, return_sequences=True, name="asset_lstm1") + self.dropout_lstm1 = Dropout(dropout_rate, name="asset_lstm1_dropout") + self.lstm2 = LSTM(lstm_units2, return_sequences=False, name="asset_lstm2_final") + self.dropout_lstm2 = Dropout(dropout_rate, name="asset_lstm2_dropout") + + def call(self, inputs, training=False): + x = self.conv1(inputs) + x = self.dropout_cnn1(x, training=training) + x = self.conv2(x) + x = self.dropout_cnn2(x, training=training) + x = self.lstm1(x, training=training) + x = self.dropout_lstm1(x, training=training) + x = self.lstm2(x, training=training) + x_processed_asset = self.dropout_lstm2(x, training=training) + return x_processed_asset + + def get_config(self): + config = super().get_config() + config.update({ + "sequence_length": self.sequence_length, + "num_features": self.num_features, + "cnn_filters1": self.cnn_filters1, + "cnn_filters2": self.cnn_filters2, + "lstm_units1": self.lstm_units1, + "lstm_units2": self.lstm_units2, + "dropout_rate": self.dropout_rate, + }) + return config + +class DeepPortfolioAgentNetwork(tf.keras.Model): + def __init__(self, + num_assets=NUM_ASSETS_CONF, + sequence_length=WINDOW_SIZE_CONF, + num_features_per_asset=NUM_FEATURES_PER_ASSET_CONF, + asset_cnn_filters1=32, asset_cnn_filters2=64, + asset_lstm_units1=64, asset_lstm_units2=32, asset_dropout=0.2, + mha_num_heads=4, mha_key_dim_divisor=2, + final_dense_units1=128, final_dense_units2=64, final_dropout=0.3, # final_dense_units2 será a dimensão das features latentes + use_sentiment_analysis=False, + # NOVO: Flag para controlar a saída + output_latent_features=False, # Se True, retorna features antes do softmax + **kwargs): + super(DeepPortfolioAgentNetwork, self).__init__(name="deep_portfolio_agent_network", **kwargs) + + self.num_assets = num_assets + self.sequence_length = sequence_length + self.num_features_per_asset = num_features_per_asset + self.asset_lstm_output_dim = asset_lstm_units2 + self.output_latent_features = output_latent_features # ARMAZENA O FLAG + + # ... (definição de self.asset_processor, self.attention, self.attention_norm, self.global_avg_pool_attention como antes) ... + self.asset_processor = AssetProcessor(...) # Preencha com os args + calculated_key_dim = self.asset_lstm_output_dim // mha_key_dim_divisor + if calculated_key_dim == 0: calculated_key_dim = self.asset_lstm_output_dim + self.attention = MultiHeadAttention(num_heads=mha_num_heads, key_dim=calculated_key_dim, dropout=0.1, name="multi_asset_attention") + self.attention_norm = LayerNormalization(epsilon=1e-6, name="attention_layernorm") + self.global_avg_pool_attention = GlobalAveragePooling1D(name="gap_after_attention") + + self.use_sentiment = use_sentiment_analysis + # ... (código do FinBERT se self.use_sentiment for True) ... + + # Camadas Densas que produzem as features latentes + self.dense1 = Dense(final_dense_units1, activation='relu', kernel_regularizer=regularizers.l2(L2_REG), name="final_dense1") + self.dropout1 = Dropout(final_dropout, name="final_dropout1") + self.latent_features_layer = Dense(final_dense_units2, activation='relu', kernel_regularizer=regularizers.l2(L2_REG), name="latent_features_output") # Renomeado para clareza + self.dropout_latent = Dropout(final_dropout, name="final_dropout_latent") # Dropout após features latentes + + # Camada de alocação (só é adicionada se não estivermos retornando features latentes) + if not self.output_latent_features: + self.output_allocation = Dense(self.num_assets, activation='softmax', name="portfolio_allocation_output") + + def call(self, inputs, training=False): + market_data_flat = inputs + + # ... (Processamento individual de ativos com self.asset_processor) ... + asset_representations_list = [] + for i in range(self.num_assets): + 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] + processed_asset_representation = self.asset_processor(current_asset_data, training=training) + asset_representations_list.append(processed_asset_representation) + stacked_asset_features = tf.stack(asset_representations_list, axis=1) + + # ... (Atenção e Agregação) ... + attention_output = self.attention(query=stacked_asset_features, value=stacked_asset_features, key=stacked_asset_features, training=training) + attention_output = self.attention_norm(stacked_asset_features + attention_output) + context_vector_from_attention = self.global_avg_pool_attention(attention_output) + + current_features_for_dense = context_vector_from_attention + # ... (Lógica de Sentimento se self.use_sentiment) ... + + x = self.dense1(current_features_for_dense) + x = self.dropout1(x, training=training) + x_latent = self.latent_features_layer(x) # Saída da camada de features latentes + x_latent_dropout = self.dropout_latent(x_latent, training=training) + + if self.output_latent_features: + return x_latent_dropout # Retorna as features latentes para SB3 + else: + # Se chamado para inferência direta (como no seu teste de forward pass) + portfolio_weights = self.output_allocation(x_latent_dropout) # Aplica a camada de saída final + return portfolio_weights + + # ... (get_config precisa ser atualizado para incluir output_latent_features e outros params) ... + def get_config(self): + config = super().get_config() + config.update({ + "num_assets": self.num_assets, + "sequence_length": self.sequence_length, + "num_features_per_asset": self.num_features_per_asset, + # ... todos os outros parâmetros do __init__ ... + "asset_lstm_output_dim": self.asset_lstm_output_dim, # Embora seja derivado de asset_lstm_units2 + "output_latent_features": self.output_latent_features + }) + # Para AssetProcessor, é melhor ele ter seu próprio get_config e from_config, + # e aqui você pode serializar sua config ou instanciá-lo diretamente no from_config. + # Por ora, manteremos simples. + return config + + # @classmethod + # def from_config(cls, config): + # # Precisa de cuidado para recriar submodelos como AssetProcessor + # return cls(**config) + +class DeepPortfolioAgentNetwork(tf.keras.Model): + def __init__(self, + num_assets=NUM_ASSETS_CONF, + sequence_length=WINDOW_SIZE_CONF, + num_features_per_asset=NUM_FEATURES_PER_ASSET_CONF, + asset_cnn_filters1=32, asset_cnn_filters2=64, + asset_lstm_units1=64, asset_lstm_units2=32, asset_dropout=0.2, + mha_num_heads=4, mha_key_dim_divisor=2, # key_dim será asset_lstm_units2 // mha_key_dim_divisor + final_dense_units1=128, final_dense_units2=64, final_dropout=0.3, + use_sentiment_analysis=False, **kwargs): # Adicionado **kwargs + super(DeepPortfolioAgentNetwork, self).__init__(name="deep_portfolio_agent_network", **kwargs) # Adicionado **kwargs + + self.num_assets = num_assets + self.sequence_length = sequence_length + self.num_features_per_asset = num_features_per_asset + self.asset_lstm_output_dim = asset_lstm_units2 + + self.asset_processor = AssetProcessor( + sequence_length=self.sequence_length, num_features=self.num_features_per_asset, + cnn_filters1=asset_cnn_filters1, cnn_filters2=asset_cnn_filters2, + lstm_units1=asset_lstm_units1, lstm_units2=asset_lstm_units2, + dropout_rate=asset_dropout + ) + + # Ajustar key_dim para ser compatível com a dimensão de entrada e num_heads + # key_dim * num_heads deve ser idealmente igual a asset_lstm_output_dim se for auto-atenção direta, + # ou o MHA projeta internamente. Para simplificar, vamos fazer key_dim ser divisível. + # Se asset_lstm_output_dim não for divisível por num_heads, key_dim pode ser diferente. + # Vamos definir key_dim explicitamente. Se asset_lstm_output_dim = 32 e num_heads = 4, key_dim pode ser 8. + # Ou deixar o MHA lidar com a projeção se key_dim for diferente. + # Para maior clareza, calculamos uma key_dim sensata. + calculated_key_dim = self.asset_lstm_output_dim // mha_key_dim_divisor + if calculated_key_dim == 0: # Evitar key_dim zero + calculated_key_dim = self.asset_lstm_output_dim # Fallback se for muito pequeno + print(f"AVISO: asset_lstm_output_dim ({self.asset_lstm_output_dim}) muito pequeno para mha_key_dim_divisor ({mha_key_dim_divisor}). Usando key_dim = {calculated_key_dim}") + + self.attention = MultiHeadAttention(num_heads=mha_num_heads, key_dim=calculated_key_dim, dropout=0.1, name="multi_asset_attention") + self.attention_norm = LayerNormalization(epsilon=1e-6, name="attention_layernorm") + self.global_avg_pool_attention = GlobalAveragePooling1D(name="gap_after_attention") + + self.use_sentiment = use_sentiment_analysis # Desabilitado por padrão para este teste + self.sentiment_embedding_size = 3 + if self.use_sentiment: + # ... (código do FinBERT como antes) ... + pass + + dense_input_dim = self.asset_lstm_output_dim + # if self.use_sentiment: dense_input_dim += self.sentiment_embedding_size + + self.dense1 = Dense(final_dense_units1, activation='relu', kernel_regularizer=regularizers.l2(L2_REG), name="final_dense1") + self.dropout1 = Dropout(final_dropout, name="final_dropout1") + self.dense2 = Dense(final_dense_units2, activation='relu', kernel_regularizer=regularizers.l2(L2_REG), name="final_dense2") + self.dropout2 = Dropout(final_dropout, name="final_dropout2") + self.output_allocation = Dense(self.num_assets, activation='softmax', name="portfolio_allocation_output") + + def call(self, inputs, training=False): + market_data_flat = inputs + + asset_representations_list = [] + for i in range(self.num_assets): + 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] + processed_asset_representation = self.asset_processor(current_asset_data, training=training) + asset_representations_list.append(processed_asset_representation) + + stacked_asset_features = tf.stack(asset_representations_list, axis=1) + + # Para MHA, query, value, key são (batch_size, Tq, dim), (batch_size, Tv, dim) + # Aqui, T = num_assets, dim = asset_lstm_output_dim + attention_output = self.attention( + query=stacked_asset_features, value=stacked_asset_features, key=stacked_asset_features, + training=training + ) + attention_output = self.attention_norm(stacked_asset_features + attention_output) + + context_vector_from_attention = self.global_avg_pool_attention(attention_output) + + current_features_for_dense = context_vector_from_attention + # if self.use_sentiment: ... (lógica de concatenação) + + x = self.dense1(current_features_for_dense) + x = self.dropout1(x, training=training) + x = self.dense2(x) + x = self.dropout2(x, training=training) + + portfolio_weights = self.output_allocation(x) + return portfolio_weights + + def get_config(self): # Necessário se você quiser salvar/carregar o modelo que usa este sub-modelo + config = super().get_config() + config.update({ + "num_assets": self.num_assets, + "sequence_length": self.sequence_length, + "num_features_per_asset": self.num_features_per_asset, + # Adicione outros args do __init__ aqui para todas as camadas e sub-modelos + "asset_lstm_output_dim": self.asset_lstm_output_dim, + # ... e os parâmetros passados para AssetProcessor e MHA, etc. + }) + return config + + # @classmethod + # def from_config(cls, config): # Necessário para carregar com sub-modelo customizado + # # Extrair config do AssetProcessor se necessário + # return cls(**config) + + +if __name__ == '__main__': + print("Testando o Forward Pass do DeepPortfolioAgentNetwork...") + + # 1. Definir Parâmetros para o Teste (devem corresponder ao config.py) + batch_size_test = 2 # Um batch pequeno para teste + seq_len_test = WINDOW_SIZE_CONF + num_assets_test = NUM_ASSETS_CONF + num_features_per_asset_test = NUM_FEATURES_PER_ASSET_CONF + total_features_flat = num_assets_test * num_features_per_asset_test + + print(f"Configuração do Teste:") + print(f" Batch Size: {batch_size_test}") + print(f" Sequence Length (Window): {seq_len_test}") + print(f" Number of Assets: {num_assets_test}") + print(f" Features per Asset: {num_features_per_asset_test}") + print(f" Total Flat Features per Timestep: {total_features_flat}") + + # 2. Criar Tensor de Input Mockado + # Shape: (batch_size, sequence_length, num_assets * num_features_per_asset) + mock_market_data_flat = tf.random.normal( + shape=(batch_size_test, seq_len_test, total_features_flat) + ) + print(f"Shape do Input Mockado (market_data_flat): {mock_market_data_flat.shape}") + + # 3. Instanciar o Modelo + # Use os mesmos hiperparâmetros que você definiria no config.py para a rede + print("\nInstanciando DeepPortfolioAgentNetwork...") + agent_network = DeepPortfolioAgentNetwork( + num_assets=num_assets_test, + sequence_length=seq_len_test, + num_features_per_asset=num_features_per_asset_test, + # Você pode variar os próximos parâmetros para testar diferentes configs + asset_cnn_filters1=32, asset_cnn_filters2=64, + asset_lstm_units1=64, asset_lstm_units2=32, # asset_lstm_units2 define asset_lstm_output_dim + asset_dropout=0.1, + mha_num_heads=4, mha_key_dim_divisor=4, # Ex: 32 // 4 = 8 para key_dim + final_dense_units1=64, final_dense_units2=32, final_dropout=0.2, + use_sentiment_analysis=False # Testar sem sentimento primeiro + ) + + # Para construir o modelo e ver o summary, você pode chamar com o input mockado + # ou explicitamente chamar model.build() se souber o input shape completo + # Chamar com input mockado é mais fácil para construir. + print("\nConstruindo o modelo com input mockado (primeira chamada)...") + try: + # É uma boa prática fazer a primeira chamada dentro de um tf.function para otimizar + # ou apenas chamar diretamente para teste. + _ = agent_network(mock_market_data_flat) # Chamada para construir as camadas + print("\n--- Summary da Rede Principal (DeepPortfolioAgentNetwork) ---") + agent_network.summary() + + # O summary do asset_processor já foi impresso no __init__ do DeepPortfolioAgentNetwork + # se você descomentar as linhas de build/summary lá. + # Ou você pode imprimir aqui: + print("\n--- Summary do AssetProcessor (Sub-Modelo) ---") + agent_network.asset_processor.summary() + + + except Exception as e: + print(f"Erro ao construir a rede principal: {e}", exc_info=True) + exit() + + # 4. Chamar model(mock_input) para o Forward Pass + print("\nExecutando Forward Pass...") + try: + predictions = agent_network(mock_market_data_flat, training=False) # Passar training=False para inferência + print("Forward Pass concluído com sucesso!") + except Exception as e: + print(f"Erro durante o Forward Pass: {e}", exc_info=True) + exit() + + # 5. Verificar o Shape da Saída + print(f"\nShape da Saída (predictions): {predictions.shape}") + expected_output_shape = (batch_size_test, num_assets_test) + if predictions.shape == expected_output_shape: + print(f"Shape da Saída está CORRETO! Esperado: {expected_output_shape}") + else: + print(f"ERRO: Shape da Saída INCORRETO. Esperado: {expected_output_shape}, Obtido: {predictions.shape}") + + # Verificar se a saída é uma distribuição de probabilidade (softmax) + if hasattr(predictions, 'numpy'): # Se for um EagerTensor + output_sum = tf.reduce_sum(predictions, axis=-1).numpy() + print(f"Soma das probabilidades de saída por amostra no batch (deve ser próximo de 1): {output_sum}") + if np.allclose(output_sum, 1.0): + print("Saída Softmax parece CORRETA (soma 1).") + else: + print("AVISO: Saída Softmax pode NÃO estar correta (soma diferente de 1).") + + print("\nExemplo das primeiras predições (pesos do portfólio):") + print(predictions.numpy()[:min(5, batch_size_test)]) # Imprime até 5 predições do batch + + # Teste com sentimento (se implementado e FinBERT carregado) + # agent_network.use_sentiment = True # Ativar para teste + # if agent_network.use_sentiment and hasattr(agent_network, 'tokenizer'): + # print("\nTestando Forward Pass COM SENTIMENTO...") + # mock_news_batch = ["positive news for asset 1", "market is very volatile today"] # Exemplo + # # A forma como você passa 'news' para o call() precisa ser definida. + # # Se for um dicionário: + # # mock_inputs_with_news = {"market_data": mock_market_data_flat, "news_data": mock_news_batch} + # # predictions_with_sentiment = agent_network(mock_inputs_with_news, training=False) + # # print(f"Shape da Saída com Sentimento: {predictions_with_sentiment.shape}") + # else: + # print("\nTeste com sentimento pulado (use_sentiment=False ou FinBERT não carregado).") + + print("\nTeste do Forward Pass Concluído!") \ No newline at end of file diff --git a/models/deep_portfolio_extrator_features_Integrar DeepPortfolioAgentNetwork no Stable-Baselines3 (PPO)/extrator_features_Integrar DeepPortfolioAgentNetwork no Stable-Baselines3 (PPO).py b/models/deep_portfolio_extrator_features_Integrar DeepPortfolioAgentNetwork no Stable-Baselines3 (PPO)/extrator_features_Integrar DeepPortfolioAgentNetwork no Stable-Baselines3 (PPO).py new file mode 100644 index 0000000000000000000000000000000000000000..72ad1352a00c57529600c6101f8144f2d81171ce --- /dev/null +++ b/models/deep_portfolio_extrator_features_Integrar DeepPortfolioAgentNetwork no Stable-Baselines3 (PPO)/extrator_features_Integrar DeepPortfolioAgentNetwork no Stable-Baselines3 (PPO).py @@ -0,0 +1,192 @@ +# rnn/agents/deep_portfolio.py (ou onde você tem DeepPortfolioAI) + +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 \ No newline at end of file diff --git "a/models/deep_portfolio_teste_integra\303\247\303\243o.py" "b/models/deep_portfolio_teste_integra\303\247\303\243o.py" new file mode 100644 index 0000000000000000000000000000000000000000..e6a0bae514244dfcfd0b6c67e57d9aaac712abcc --- /dev/null +++ "b/models/deep_portfolio_teste_integra\303\247\303\243o.py" @@ -0,0 +1,286 @@ +# rnn/agents/deep_portfolio.py (ou onde você tem DeepPortfolioAI / DeepPortfolioAgentNetwork) +import numpy as np +from tensorflow.keras import regularizers +import tensorflow as tf +from tensorflow.keras.layers import ( + Input, Conv1D, LSTM, Dense, Dropout, + MultiHeadAttention, Reshape, Concatenate, + TimeDistributed, GlobalAveragePooling1D, LayerNormalization +) +from tensorflow.keras.models import Model +# Comente as importações do transformers se não for testar o sentimento agora para simplificar +# from transformers import AutoTokenizer, TFAutoModelForSequenceClassification + +# --- DEFINIÇÕES DE CONFIGURAÇÃO (COPIE OU IMPORTE DO SEU CONFIG.PY) --- +# Se você não importar do config.py, defina-as aqui para o teste +WINDOW_SIZE_CONF = 60 +NUM_ASSETS_CONF = 4 # Ex: ETH, BTC, ADA, SOL +NUM_FEATURES_PER_ASSET_CONF = 26 # Número de features calculadas para CADA ativo + # (open_div_atr, ..., buy_condition_v1, etc.) +L2_REG = 0.0001 # Exemplo, use o valor do seu config + +# (Cole as classes AssetProcessor e DeepPortfolioAgentNetwork aqui se estiver em um novo script) +# Ou, se estiver no mesmo arquivo, elas já estarão definidas. + +# ... (Definição das classes AssetProcessor e DeepPortfolioAgentNetwork como na resposta anterior) ... +# Certifique-se que a classe DeepPortfolioAgentNetwork está usando estas constantes: +# num_assets=NUM_ASSETS_CONF, +# sequence_length=WINDOW_SIZE_CONF, +# num_features_per_asset=NUM_FEATURES_PER_ASSET_CONF + +class AssetProcessor(tf.keras.Model): + def __init__(self, sequence_length, num_features, cnn_filters1=32, cnn_filters2=64, lstm_units1=64, lstm_units2=32, dropout_rate=0.2, name="single_asset_processor_module", **kwargs): # Adicionado **kwargs + super(AssetProcessor, self).__init__(name=name, **kwargs) # Adicionado **kwargs + self.sequence_length = sequence_length + self.num_features = num_features + self.cnn_filters1 = cnn_filters1 # Salvar para get_config + self.cnn_filters2 = cnn_filters2 + self.lstm_units1 = lstm_units1 + self.lstm_units2 = lstm_units2 + self.dropout_rate = dropout_rate + + self.conv1 = Conv1D(filters=cnn_filters1, kernel_size=3, activation='relu', padding='same', name="asset_cnn1") + self.dropout_cnn1 = Dropout(dropout_rate, name="asset_cnn1_dropout") + self.conv2 = Conv1D(filters=cnn_filters2, kernel_size=3, activation='relu', padding='same', name="asset_cnn2") + self.dropout_cnn2 = Dropout(dropout_rate, name="asset_cnn2_dropout") + self.lstm1 = LSTM(lstm_units1, return_sequences=True, name="asset_lstm1") + self.dropout_lstm1 = Dropout(dropout_rate, name="asset_lstm1_dropout") + self.lstm2 = LSTM(lstm_units2, return_sequences=False, name="asset_lstm2_final") + self.dropout_lstm2 = Dropout(dropout_rate, name="asset_lstm2_dropout") + + def call(self, inputs, training=False): + x = self.conv1(inputs) + x = self.dropout_cnn1(x, training=training) + x = self.conv2(x) + x = self.dropout_cnn2(x, training=training) + x = self.lstm1(x, training=training) + x = self.dropout_lstm1(x, training=training) + x = self.lstm2(x, training=training) + x_processed_asset = self.dropout_lstm2(x, training=training) + return x_processed_asset + + def get_config(self): + config = super().get_config() + config.update({ + "sequence_length": self.sequence_length, + "num_features": self.num_features, + "cnn_filters1": self.cnn_filters1, + "cnn_filters2": self.cnn_filters2, + "lstm_units1": self.lstm_units1, + "lstm_units2": self.lstm_units2, + "dropout_rate": self.dropout_rate, + }) + return config + +class DeepPortfolioAgentNetwork(tf.keras.Model): + def __init__(self, + num_assets=NUM_ASSETS_CONF, + sequence_length=WINDOW_SIZE_CONF, + num_features_per_asset=NUM_FEATURES_PER_ASSET_CONF, + asset_cnn_filters1=32, asset_cnn_filters2=64, + asset_lstm_units1=64, asset_lstm_units2=32, asset_dropout=0.2, + mha_num_heads=4, mha_key_dim_divisor=2, # key_dim será asset_lstm_units2 // mha_key_dim_divisor + final_dense_units1=128, final_dense_units2=64, final_dropout=0.3, + use_sentiment_analysis=False, **kwargs): # Adicionado **kwargs + super(DeepPortfolioAgentNetwork, self).__init__(name="deep_portfolio_agent_network", **kwargs) # Adicionado **kwargs + + self.num_assets = num_assets + self.sequence_length = sequence_length + self.num_features_per_asset = num_features_per_asset + self.asset_lstm_output_dim = asset_lstm_units2 + + self.asset_processor = AssetProcessor( + sequence_length=self.sequence_length, num_features=self.num_features_per_asset, + cnn_filters1=asset_cnn_filters1, cnn_filters2=asset_cnn_filters2, + lstm_units1=asset_lstm_units1, lstm_units2=asset_lstm_units2, + dropout_rate=asset_dropout + ) + + # Ajustar key_dim para ser compatível com a dimensão de entrada e num_heads + # key_dim * num_heads deve ser idealmente igual a asset_lstm_output_dim se for auto-atenção direta, + # ou o MHA projeta internamente. Para simplificar, vamos fazer key_dim ser divisível. + # Se asset_lstm_output_dim não for divisível por num_heads, key_dim pode ser diferente. + # Vamos definir key_dim explicitamente. Se asset_lstm_output_dim = 32 e num_heads = 4, key_dim pode ser 8. + # Ou deixar o MHA lidar com a projeção se key_dim for diferente. + # Para maior clareza, calculamos uma key_dim sensata. + calculated_key_dim = self.asset_lstm_output_dim // mha_key_dim_divisor + if calculated_key_dim == 0: # Evitar key_dim zero + calculated_key_dim = self.asset_lstm_output_dim # Fallback se for muito pequeno + print(f"AVISO: asset_lstm_output_dim ({self.asset_lstm_output_dim}) muito pequeno para mha_key_dim_divisor ({mha_key_dim_divisor}). Usando key_dim = {calculated_key_dim}") + + self.attention = MultiHeadAttention(num_heads=mha_num_heads, key_dim=calculated_key_dim, dropout=0.1, name="multi_asset_attention") + self.attention_norm = LayerNormalization(epsilon=1e-6, name="attention_layernorm") + self.global_avg_pool_attention = GlobalAveragePooling1D(name="gap_after_attention") + + self.use_sentiment = use_sentiment_analysis # Desabilitado por padrão para este teste + self.sentiment_embedding_size = 3 + if self.use_sentiment: + # ... (código do FinBERT como antes) ... + pass + + dense_input_dim = self.asset_lstm_output_dim + # if self.use_sentiment: dense_input_dim += self.sentiment_embedding_size + + self.dense1 = Dense(final_dense_units1, activation='relu', kernel_regularizer=regularizers.l2(L2_REG), name="final_dense1") + self.dropout1 = Dropout(final_dropout, name="final_dropout1") + self.dense2 = Dense(final_dense_units2, activation='relu', kernel_regularizer=regularizers.l2(L2_REG), name="final_dense2") + self.dropout2 = Dropout(final_dropout, name="final_dropout2") + self.output_allocation = Dense(self.num_assets, activation='softmax', name="portfolio_allocation_output") + + def call(self, inputs, training=False): + market_data_flat = inputs + + asset_representations_list = [] + for i in range(self.num_assets): + 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] + processed_asset_representation = self.asset_processor(current_asset_data, training=training) + asset_representations_list.append(processed_asset_representation) + + stacked_asset_features = tf.stack(asset_representations_list, axis=1) + + # Para MHA, query, value, key são (batch_size, Tq, dim), (batch_size, Tv, dim) + # Aqui, T = num_assets, dim = asset_lstm_output_dim + attention_output = self.attention( + query=stacked_asset_features, value=stacked_asset_features, key=stacked_asset_features, + training=training + ) + attention_output = self.attention_norm(stacked_asset_features + attention_output) + + context_vector_from_attention = self.global_avg_pool_attention(attention_output) + + current_features_for_dense = context_vector_from_attention + # if self.use_sentiment: ... (lógica de concatenação) + + x = self.dense1(current_features_for_dense) + x = self.dropout1(x, training=training) + x = self.dense2(x) + x = self.dropout2(x, training=training) + + portfolio_weights = self.output_allocation(x) + return portfolio_weights + + def get_config(self): # Necessário se você quiser salvar/carregar o modelo que usa este sub-modelo + config = super().get_config() + config.update({ + "num_assets": self.num_assets, + "sequence_length": self.sequence_length, + "num_features_per_asset": self.num_features_per_asset, + # Adicione outros args do __init__ aqui para todas as camadas e sub-modelos + "asset_lstm_output_dim": self.asset_lstm_output_dim, + # ... e os parâmetros passados para AssetProcessor e MHA, etc. + }) + return config + + # @classmethod + # def from_config(cls, config): # Necessário para carregar com sub-modelo customizado + # # Extrair config do AssetProcessor se necessário + # return cls(**config) + + +if __name__ == '__main__': + print("Testando o Forward Pass do DeepPortfolioAgentNetwork...") + + # 1. Definir Parâmetros para o Teste (devem corresponder ao config.py) + batch_size_test = 2 # Um batch pequeno para teste + seq_len_test = WINDOW_SIZE_CONF + num_assets_test = NUM_ASSETS_CONF + num_features_per_asset_test = NUM_FEATURES_PER_ASSET_CONF + total_features_flat = num_assets_test * num_features_per_asset_test + + print(f"Configuração do Teste:") + print(f" Batch Size: {batch_size_test}") + print(f" Sequence Length (Window): {seq_len_test}") + print(f" Number of Assets: {num_assets_test}") + print(f" Features per Asset: {num_features_per_asset_test}") + print(f" Total Flat Features per Timestep: {total_features_flat}") + + # 2. Criar Tensor de Input Mockado + # Shape: (batch_size, sequence_length, num_assets * num_features_per_asset) + mock_market_data_flat = tf.random.normal( + shape=(batch_size_test, seq_len_test, total_features_flat) + ) + print(f"Shape do Input Mockado (market_data_flat): {mock_market_data_flat.shape}") + + # 3. Instanciar o Modelo + # Use os mesmos hiperparâmetros que você definiria no config.py para a rede + print("\nInstanciando DeepPortfolioAgentNetwork...") + agent_network = DeepPortfolioAgentNetwork( + num_assets=num_assets_test, + sequence_length=seq_len_test, + num_features_per_asset=num_features_per_asset_test, + # Você pode variar os próximos parâmetros para testar diferentes configs + asset_cnn_filters1=32, asset_cnn_filters2=64, + asset_lstm_units1=64, asset_lstm_units2=32, # asset_lstm_units2 define asset_lstm_output_dim + asset_dropout=0.1, + mha_num_heads=4, mha_key_dim_divisor=4, # Ex: 32 // 4 = 8 para key_dim + final_dense_units1=64, final_dense_units2=32, final_dropout=0.2, + use_sentiment_analysis=False # Testar sem sentimento primeiro + ) + + # Para construir o modelo e ver o summary, você pode chamar com o input mockado + # ou explicitamente chamar model.build() se souber o input shape completo + # Chamar com input mockado é mais fácil para construir. + print("\nConstruindo o modelo com input mockado (primeira chamada)...") + try: + # É uma boa prática fazer a primeira chamada dentro de um tf.function para otimizar + # ou apenas chamar diretamente para teste. + _ = agent_network(mock_market_data_flat) # Chamada para construir as camadas + print("\n--- Summary da Rede Principal (DeepPortfolioAgentNetwork) ---") + agent_network.summary() + + # O summary do asset_processor já foi impresso no __init__ do DeepPortfolioAgentNetwork + # se você descomentar as linhas de build/summary lá. + # Ou você pode imprimir aqui: + print("\n--- Summary do AssetProcessor (Sub-Modelo) ---") + agent_network.asset_processor.summary() + + + except Exception as e: + print(f"Erro ao construir a rede principal: {e}", exc_info=True) + exit() + + # 4. Chamar model(mock_input) para o Forward Pass + print("\nExecutando Forward Pass...") + try: + predictions = agent_network(mock_market_data_flat, training=False) # Passar training=False para inferência + print("Forward Pass concluído com sucesso!") + except Exception as e: + print(f"Erro durante o Forward Pass: {e}", exc_info=True) + exit() + + # 5. Verificar o Shape da Saída + print(f"\nShape da Saída (predictions): {predictions.shape}") + expected_output_shape = (batch_size_test, num_assets_test) + if predictions.shape == expected_output_shape: + print(f"Shape da Saída está CORRETO! Esperado: {expected_output_shape}") + else: + print(f"ERRO: Shape da Saída INCORRETO. Esperado: {expected_output_shape}, Obtido: {predictions.shape}") + + # Verificar se a saída é uma distribuição de probabilidade (softmax) + if hasattr(predictions, 'numpy'): # Se for um EagerTensor + output_sum = tf.reduce_sum(predictions, axis=-1).numpy() + print(f"Soma das probabilidades de saída por amostra no batch (deve ser próximo de 1): {output_sum}") + if np.allclose(output_sum, 1.0): + print("Saída Softmax parece CORRETA (soma 1).") + else: + print("AVISO: Saída Softmax pode NÃO estar correta (soma diferente de 1).") + + print("\nExemplo das primeiras predições (pesos do portfólio):") + print(predictions.numpy()[:min(5, batch_size_test)]) # Imprime até 5 predições do batch + + # Teste com sentimento (se implementado e FinBERT carregado) + # agent_network.use_sentiment = True # Ativar para teste + # if agent_network.use_sentiment and hasattr(agent_network, 'tokenizer'): + # print("\nTestando Forward Pass COM SENTIMENTO...") + # mock_news_batch = ["positive news for asset 1", "market is very volatile today"] # Exemplo + # # A forma como você passa 'news' para o call() precisa ser definida. + # # Se for um dicionário: + # # mock_inputs_with_news = {"market_data": mock_market_data_flat, "news_data": mock_news_batch} + # # predictions_with_sentiment = agent_network(mock_inputs_with_news, training=False) + # # print(f"Shape da Saída com Sentimento: {predictions_with_sentiment.shape}") + # else: + # print("\nTeste com sentimento pulado (use_sentiment=False ou FinBERT não carregado).") + + print("\nTeste do Forward Pass Concluído!") \ No newline at end of file diff --git a/models/deep_portfolio_torch.py b/models/deep_portfolio_torch.py new file mode 100644 index 0000000000000000000000000000000000000000..f957a1fafc6f81023efd3b2de883068b5be62580 --- /dev/null +++ b/models/deep_portfolio_torch.py @@ -0,0 +1,80 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +class SingleAssetProcessor(nn.Module): + def __init__(self, sequence_length, num_features_per_asset, cnn_filters1, cnn_filters2, lstm_units): + super().__init__() + self.conv1 = nn.Conv1d(num_features_per_asset, cnn_filters1, kernel_size=3, padding=1) + self.conv2 = nn.Conv1d(cnn_filters1, cnn_filters2, kernel_size=3, padding=1) + self.lstm = nn.LSTM(input_size=cnn_filters2, hidden_size=lstm_units, batch_first=True) + + def forward(self, x): + # x: (batch, seq_len, num_features_per_asset) + x = x.transpose(1, 2) # (batch, num_features_per_asset, seq_len) + x = F.relu(self.conv1(x)) + x = F.relu(self.conv2(x)) + x = x.transpose(1, 2) # (batch, seq_len, cnn_filters2) + _, (h_n, _) = self.lstm(x) # h_n: (1, batch, lstm_units) + return h_n.squeeze(0) # (batch, lstm_units) + +class DeepPortfolioAgentNetworkTorch(nn.Module): + def __init__(self, num_assets, sequence_length, num_features_per_asset, + asset_cnn_filters1=32, asset_cnn_filters2=64, + asset_lstm_units1=64, asset_lstm_units2=32, + final_dense_units1=128, final_dense_units2=32, + final_dropout=0.3, mha_num_heads=4, mha_key_dim_divisor=2, + output_latent_features=True, use_sentiment_analysis=False): + super().__init__() + self.num_assets = num_assets + self.sequence_length = sequence_length + self.num_features_per_asset = num_features_per_asset + self.output_latent_features = output_latent_features + self.use_sentiment_analysis = use_sentiment_analysis + + # Processador individual de ativos + self.asset_processor = SingleAssetProcessor( + sequence_length, num_features_per_asset, + asset_cnn_filters1, asset_cnn_filters2, asset_lstm_units1 + ) + # Atenção multi-cabeça + self.attention = nn.MultiheadAttention( + embed_dim=asset_lstm_units1, + num_heads=mha_num_heads, + batch_first=True + ) + # Pooling global + self.global_avg_pool = nn.AdaptiveAvgPool1d(1) + # Camadas densas finais + self.dense1 = nn.Linear(asset_lstm_units1, final_dense_units1) + self.dropout1 = nn.Dropout(final_dropout) + self.dense2 = nn.Linear(final_dense_units1, final_dense_units2) + self.dropout2 = nn.Dropout(final_dropout) + self.output_allocation = nn.Linear(final_dense_units2, num_assets) + + def forward(self, x): + # x: (batch, seq_len, num_assets * num_features_per_asset) + batch_size = x.size(0) + # Separar cada ativo + asset_representations = [] + for i in range(self.num_assets): + start = i * self.num_features_per_asset + end = (i + 1) * self.num_features_per_asset + asset_data = x[:, :, start:end] # (batch, seq_len, num_features_per_asset) + asset_repr = self.asset_processor(asset_data) # (batch, lstm_units1) + asset_representations.append(asset_repr) + # Empilhar ativos: (batch, num_assets, lstm_units1) + stacked = torch.stack(asset_representations, dim=1) + # Atenção multi-cabeça + attn_output, _ = self.attention(stacked, stacked, stacked) + # Pooling global sobre ativos (num_assets) + pooled = self.global_avg_pool(attn_output.transpose(1,2)).squeeze(-1) # (batch, lstm_units1) + # Camadas densas finais + x = F.relu(self.dense1(pooled)) + x = self.dropout1(x) + x = F.relu(self.dense2(x)) + x = self.dropout2(x) + if self.output_latent_features: + return x # (batch, final_dense_units2) + else: + return F.softmax(self.output_allocation(x), dim=-1) # (batch, num_assets) diff --git a/models/deep_portfolio_v1/deep_portfolio.py b/models/deep_portfolio_v1/deep_portfolio.py new file mode 100644 index 0000000000000000000000000000000000000000..2e13a9c60abc9ce554adbaca3b7ed39e7eb9240e --- /dev/null +++ b/models/deep_portfolio_v1/deep_portfolio.py @@ -0,0 +1,104 @@ +import numpy as np +import tensorflow as tf +from tensorflow.keras.layers import LSTM, Dense, Conv1D, MultiHeadAttention +from transformers import AutoTokenizer, TFAutoModelForSequenceClassification # Changed here +import gymnasium as gym +from gymnasium import spaces + +class DeepPortfolioAI(tf.keras.Model): + def __init__(self, num_assets, sequence_length=60): + super(DeepPortfolioAI, self).__init__() + + # Parâmetros do modelo + self.num_assets = num_assets + self.sequence_length = sequence_length + + # CNN para análise de padrões técnicos + self.conv1 = Conv1D(64, 3, activation='relu') + self.conv2 = Conv1D(128, 3, activation='relu') + + # LSTM para análise temporal + self.lstm1 = LSTM(128, return_sequences=True) + self.lstm2 = LSTM(64) + + # Attention para correlações entre ativos + self.attention = MultiHeadAttention(num_heads=8, key_dim=64) + + # Camadas densas para decisão final + self.dense1 = Dense(256, activation='relu') + self.dense2 = Dense(128, activation='relu') + self.output_layer = Dense(num_assets, activation='softmax') + + # Inicializar tokenizer e modelo de sentimento + self.tokenizer = AutoTokenizer.from_pretrained('ProsusAI/finbert') + self.sentiment_model = TFAutoModelForSequenceClassification.from_pretrained('ProsusAI/finbert') # Changed here + + def call(self, inputs): + market_data, news_data = inputs + + # Análise técnica com CNN + x_technical = self.conv1(market_data) + x_technical = self.conv2(x_technical) + + # Análise temporal com LSTM + x_temporal = self.lstm1(x_technical) + x_temporal = self.lstm2(x_temporal) + + # Attention para correlações + x_attention = self.attention(x_temporal, x_temporal, x_temporal) + + # Combinar com análise de sentimento + sentiment_embeddings = self._process_news(news_data) + x_combined = tf.concat([x_attention, sentiment_embeddings], axis=-1) + + # Camadas densas finais + x = self.dense1(x_combined) + x = self.dense2(x) + return self.output_layer(x) + + def _process_news(self, news_data): + inputs = self.tokenizer(news_data, return_tensors="pt", padding=True, truncation=True) + sentiment_scores = self.sentiment_model(**inputs).logits + return tf.convert_to_tensor(sentiment_scores.detach().numpy()) + +class PortfolioEnvironment(gym.Env): + def __init__(self, data, initial_balance=100000): + super(PortfolioEnvironment, self).__init__() + + self.data = data + self.initial_balance = initial_balance + self.current_step = 0 + + # Define espaços de ação e observação + self.action_space = spaces.Box( + low=0, high=1, shape=(len(data.columns),), dtype=np.float32) + self.observation_space = spaces.Box( + low=-np.inf, high=np.inf, shape=(60, len(data.columns)), dtype=np.float32) + + def reset(self): + self.current_step = 0 + self.balance = self.initial_balance + self.portfolio = np.zeros(len(self.data.columns)) + return self._get_observation() + + def step(self, action): + # Implementar lógica de negociação + current_prices = self.data.iloc[self.current_step] + next_prices = self.data.iloc[self.current_step + 1] + + # Calcular retorno + returns = (next_prices - current_prices) / current_prices + reward = np.sum(action * returns) + + # Atualizar portfolio + self.portfolio = action + self.balance *= (1 + reward) + + # Incrementar step + self.current_step += 1 + done = self.current_step >= len(self.data) - 1 + + return self._get_observation(), reward, done, {} + + def _get_observation(self): + return self.data.iloc[self.current_step-60:self.current_step].values \ No newline at end of file diff --git a/models/dep_portfolio_agent_network_integrado/DeepPortfolioAgentNetwork.py b/models/dep_portfolio_agent_network_integrado/DeepPortfolioAgentNetwork.py new file mode 100644 index 0000000000000000000000000000000000000000..d34c81498dd4c464c9b0832e460e53754e63ecb8 --- /dev/null +++ b/models/dep_portfolio_agent_network_integrado/DeepPortfolioAgentNetwork.py @@ -0,0 +1,192 @@ +# rnn/agents/deep_portfolio.py (ou onde você tem DeepPortfolioAI) + +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 \ No newline at end of file diff --git a/models_deep_portfolio.py b/models_deep_portfolio.py new file mode 100644 index 0000000000000000000000000000000000000000..7535193339f84eeba82f628d4560e8b03f2a821b --- /dev/null +++ b/models_deep_portfolio.py @@ -0,0 +1,104 @@ +import numpy as np +import tensorflow as tf +from tensorflow.keras.layers import LSTM, Dense, Conv1D, MultiHeadAttention +from transformers import AutoTokenizer, AutoModelForSequenceClassification +import gym +from gym import spaces + +class DeepPortfolioAI(tf.keras.Model): + def __init__(self, num_assets, sequence_length=60): + super(DeepPortfolioAI, self).__init__() + + # Parâmetros do modelo + self.num_assets = num_assets + self.sequence_length = sequence_length + + # CNN para análise de padrões técnicos + self.conv1 = Conv1D(64, 3, activation='relu') + self.conv2 = Conv1D(128, 3, activation='relu') + + # LSTM para análise temporal + self.lstm1 = LSTM(128, return_sequences=True) + self.lstm2 = LSTM(64) + + # Attention para correlações entre ativos + self.attention = MultiHeadAttention(num_heads=8, key_dim=64) + + # Camadas densas para decisão final + self.dense1 = Dense(256, activation='relu') + self.dense2 = Dense(128, activation='relu') + self.output_layer = Dense(num_assets, activation='softmax') + + # Inicializar tokenizer e modelo de sentimento + self.tokenizer = AutoTokenizer.from_pretrained('finbert-tone') + self.sentiment_model = AutoModelForSequenceClassification.from_pretrained('finbert-tone') + + def call(self, inputs): + market_data, news_data = inputs + + # Análise técnica com CNN + x_technical = self.conv1(market_data) + x_technical = self.conv2(x_technical) + + # Análise temporal com LSTM + x_temporal = self.lstm1(x_technical) + x_temporal = self.lstm2(x_temporal) + + # Attention para correlações + x_attention = self.attention(x_temporal, x_temporal, x_temporal) + + # Combinar com análise de sentimento + sentiment_embeddings = self._process_news(news_data) + x_combined = tf.concat([x_attention, sentiment_embeddings], axis=-1) + + # Camadas densas finais + x = self.dense1(x_combined) + x = self.dense2(x) + return self.output_layer(x) + + def _process_news(self, news_data): + inputs = self.tokenizer(news_data, return_tensors="pt", padding=True, truncation=True) + sentiment_scores = self.sentiment_model(**inputs).logits + return tf.convert_to_tensor(sentiment_scores.detach().numpy()) + +class PortfolioEnvironment(gym.Env): + def __init__(self, data, initial_balance=100000): + super(PortfolioEnvironment, self).__init__() + + self.data = data + self.initial_balance = initial_balance + self.current_step = 0 + + # Define espaços de ação e observação + self.action_space = spaces.Box( + low=0, high=1, shape=(len(data.columns),), dtype=np.float32) + self.observation_space = spaces.Box( + low=-np.inf, high=np.inf, shape=(60, len(data.columns)), dtype=np.float32) + + def reset(self): + self.current_step = 0 + self.balance = self.initial_balance + self.portfolio = np.zeros(len(self.data.columns)) + return self._get_observation() + + def step(self, action): + # Implementar lógica de negociação + current_prices = self.data.iloc[self.current_step] + next_prices = self.data.iloc[self.current_step + 1] + + # Calcular retorno + returns = (next_prices - current_prices) / current_prices + reward = np.sum(action * returns) + + # Atualizar portfolio + self.portfolio = action + self.balance *= (1 + reward) + + # Incrementar step + self.current_step += 1 + done = self.current_step >= len(self.data) - 1 + + return self._get_observation(), reward, done, {} + + def _get_observation(self): + return self.data.iloc[self.current_step-60:self.current_step].values \ No newline at end of file diff --git a/requeriments.txt b/requeriments.txt new file mode 100644 index 0000000000000000000000000000000000000000..2e0abd76e9e61bf5952a1d6370cf1034759ceff6 --- /dev/null +++ b/requeriments.txt @@ -0,0 +1,21 @@ +pandas +numpy +scikit-learn +tensorflow +streamlit +matplotlib +fastapi +uvicorn +httpx +pydantic +python-dotenv +loguru +# agno @ git+https://github.com/agno-agi/agno.git +jinja2 +yfinance +PyJWT +transformers +gymnasium +tf-keras +ccxt +pandas_ta \ No newline at end of file diff --git a/scripts/__pycache__/config.cpython-312.pyc b/scripts/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..052d81ea346cbf989317986e4bd0db76a25c7714 Binary files /dev/null and b/scripts/__pycache__/config.cpython-312.pyc differ diff --git a/scripts/__pycache__/data_handler.cpython-312.pyc b/scripts/__pycache__/data_handler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..561d05db50debe7eea7c7c0480622ba38f2cd788 Binary files /dev/null and b/scripts/__pycache__/data_handler.cpython-312.pyc differ diff --git a/scripts/__pycache__/data_handler_multi_asset.cpython-312.pyc b/scripts/__pycache__/data_handler_multi_asset.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..266232b5e1aaa12d4b79075497f99b55c343b6eb Binary files /dev/null and b/scripts/__pycache__/data_handler_multi_asset.cpython-312.pyc differ diff --git a/scripts/__pycache__/model_builder.cpython-312.pyc b/scripts/__pycache__/model_builder.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f082c16fc91af7ac0a1ec7b1d235d06ae55a4fd9 Binary files /dev/null and b/scripts/__pycache__/model_builder.cpython-312.pyc differ diff --git a/scripts/app/model/confusion_matrix.png b/scripts/app/model/confusion_matrix.png new file mode 100644 index 0000000000000000000000000000000000000000..614c2e91954a5af11f2fd0b705e4eb705ad81864 Binary files /dev/null and b/scripts/app/model/confusion_matrix.png differ diff --git a/scripts/app/model/model.h5 b/scripts/app/model/model.h5 new file mode 100644 index 0000000000000000000000000000000000000000..ba787b6ff5a62eb57ecc445c2f32dc0f8b447a55 --- /dev/null +++ b/scripts/app/model/model.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d53a4c3b1a372e32f1a7ae4ebea13f01fcee076a5af2462dc0d1b0d7ef22f22 +size 957128 diff --git a/scripts/app/model/other_indicators_scaler.joblib b/scripts/app/model/other_indicators_scaler.joblib new file mode 100644 index 0000000000000000000000000000000000000000..66416e29c837cfe4082a490720dc6892530c8b86 --- /dev/null +++ b/scripts/app/model/other_indicators_scaler.joblib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f97c813a832be458af052ccaebb5077e280fb1a44069a358dc051e2d1ab2576 +size 1527 diff --git a/scripts/app/model/price_volume_atr_norm_scaler.joblib b/scripts/app/model/price_volume_atr_norm_scaler.joblib new file mode 100644 index 0000000000000000000000000000000000000000..e0b2c144474cb1e8ccd27c716e2db2069a2ee53f --- /dev/null +++ b/scripts/app/model/price_volume_atr_norm_scaler.joblib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93e76c808185e6dc301c8100431c3d18563c63ee6d6e4ed410eb3b325d060b93 +size 1255 diff --git a/scripts/app/model/training_history.png b/scripts/app/model/training_history.png new file mode 100644 index 0000000000000000000000000000000000000000..8ab6513add36739ba11e1af172e27b9992079730 Binary files /dev/null and b/scripts/app/model/training_history.png differ diff --git a/scripts/config.md b/scripts/config.md new file mode 100644 index 0000000000000000000000000000000000000000..0d91747d9bb970a491e0385602c43c01a49be7c7 --- /dev/null +++ b/scripts/config.md @@ -0,0 +1,13 @@ +# EXPECTED_FEATURES_ORDER define como as colunas DEVEM ESTAR no DataFrame +# que entra na função create_sequences, APÓS o escalonamento e ANTES do sufixo _scaled. +# E também como o rnn_predictor.py espera as features ANTES de aplicar os scalers. +# Se rnn_predictor.py vai aplicar scalers separados, ele precisa dos nomes originais (ou _div_atr). +# O script de treino vai criar colunas com sufixo _scaled para alimentar o modelo. +# A lista EXPECTED_FEATURES_ORDER no config.py não é diretamente usada no train_rnn_model.py +# da forma como está agora, mas é CRUCIAL para alinhar com rnn_predictor.py. +# Fazer o train_rnn_model.py funcionar e depois alinhar o rnn_predictor.py. +# Importante que as NUM_FEATURES e o input_shape do modelo estejam corretos. + +# Esta variável será usada para nomear as colunas escaladas que vão para o modelo +# e deve corresponder ao que o rnn_predictor.py espera encontrar como features escaladas. +# Os nomes aqui devem ser as colunas de BASE_FEATURE_COLS com "_scaled" no final. \ No newline at end of file diff --git a/scripts/config.py b/scripts/config.py new file mode 100644 index 0000000000000000000000000000000000000000..147b38998a9febebbd889b79dfd3bea87b869488 --- /dev/null +++ b/scripts/config.py @@ -0,0 +1,115 @@ +# --- Parâmetros de Configuração --- +# Dados +SYMBOL = 'ETH/USDT' # MUDAMOS PARA ETH/USDT PARA TESTE +MULTI_ASSET_SYMBOLS = { + 'crypto_eth': 'ETH-USD', # yfinance ticker para ETH/USD + 'crypto_ada': 'ADA-USD', # yfinance ticker para ADA/USD + 'stock_aapl': 'AAPL', # NASDAQ + 'stock_petr': 'PETR4.SA' # B3 +} # Use os tickers corretos para yfinance ou ccxt +TIMEFRAME_YFINANCE = '1h' # yfinance suporta '1m', '2m', '5m', '15m', '30m', '60m', '90m', '1h', '1d', '5d', '1wk', '1mo', '3mo' +# Para '1h', yfinance só retorna os últimos 730 dias. Para mais dados, use '1d'. +# Se usar ccxt, TIMEFRAME = '1h' como antes. +DAYS_TO_FETCH = 365 * 2 # 2 anos + +# Lista das features base que você quer calcular para CADA ativo +# (as 19 que definimos antes) +INDIVIDUAL_ASSET_BASE_FEATURES = [ + 'open', 'high', 'low', 'close', 'volume', # OHLCV originais são necessários para os cálculos + 'sma_10', 'rsi_14', 'macd', 'macds', 'atr', 'bbp', 'cci_37', 'mfi_37', 'adx_14', + 'volume_zscore', 'body_size', 'body_size_norm_atr', 'body_vs_avg_body', + 'log_return', 'buy_condition_v1', # 'sma_50' é calculada dentro de buy_condition_v1 + # As colunas _div_atr serão criadas a partir destas +] + +# Features que serão normalizadas pelo ATR +COLS_TO_NORM_BY_ATR = ['open', 'high', 'low', 'close', 'volume', 'sma_10', 'macd', 'body_size'] + + +TIMEFRAME = '1h' +DAYS_OF_DATA_TO_FETCH = 365 * 2 +LIMIT_PER_FETCH = 1000 + +# Features e Janela +WINDOW_SIZE = 60 + +# BASE_FEATURE_COLS define as features *originais* ou *derivadas diretamente dos dados brutos* +# que serão usadas ANTES do escalonamento específico para o modelo no script de treino, +# e também as features que o rnn_predictor.py precisará calcular/ter ANTES de aplicar SEUS scalers. +BASE_FEATURE_COLS = [ + 'open_div_atr', # Feature 1 (Preço normalizado) + 'high_div_atr', # Feature 2 (Preço normalizado) + 'low_div_atr', # Feature 3 (Preço normalizado) + 'close_div_atr', # Feature 4 (Preço normalizado) + 'volume_div_atr', # Feature 5 (Volume normalizado) + 'log_return', # Feature 6 (Momento) + 'rsi_14', # Feature 7 (Oscilador) + 'atr', # Feature 8 (Volatilidade - será escalada) + 'bbp', # Feature 9 (%B - será escalado) + 'cci_37', # Feature 10 (Oscilador - será escalado) + 'mfi_37', # Feature 11 (Volume/Oscilador - será escalado) + 'body_size_norm_atr', # Feature 12 (Candle normalizado) + 'body_vs_avg_body', # Feature 13 (Candle relativo) + 'macd', # Feature 14 (Linha MACD - será escalada) + 'sma_10_div_atr', # Feature 15 (Preço normalizado) + 'adx_14', # Feature 16 (Força da Tendência - será escalada) + 'volume_zscore', # Feature 17 (Volume relativo - será escalado) + 'buy_condition_v1', # Feature 18 (Condição Composta - binária, pode ou não ser escalada) + # 'cond_compra_v1', # Parece ser um duplicado de 'buy_condition_v1', remova se for. + # Se for diferente, mantenha, mas garanta que está sendo calculada. +] +# REMOVA as colunas _scaled de BASE_FEATURE_COLS. Elas são o *resultado* do escalonamento. +# BASE_FEATURE_COLS são as features ANTES do último passo de escalonamento para o modelo. + +# Vamos assumir que cond_compra_v1 e buy_condition_v1 são a mesma. +# Se forem diferentes, você precisará ajustar. +if 'cond_compra_v1' in BASE_FEATURE_COLS and 'buy_condition_v1' in BASE_FEATURE_COLS: + if 'cond_compra_v1' == 'buy_condition_v1': # Redundante se nomes iguais, mas para clareza + print("AVISO em config.py: 'cond_compra_v1' e 'buy_condition_v1' parecem ser a mesma feature. Verifique.") + # Decida qual manter ou se são realmente diferentes. Por ora, vou assumir que você quer ambas se estiverem listadas. + # Se forem a mesma, remova uma. Vou remover 'cond_compra_v1' se 'buy_condition_v1' for a oficial. + # BASE_FEATURE_COLS.remove('cond_compra_v1') # Exemplo + + +NUM_FEATURES = len(BASE_FEATURE_COLS) # ATUALIZADO AUTOMATICAMENTE + +# Alvo da Predição (Target) +PREDICTION_HORIZON = 5 +PRICE_CHANGE_THRESHOLD = 0.0075 # Você aumentou, OK. + +# Modelo RNN +LSTM_UNITS = [64, 64] # Boa escolha para mais features +DENSE_UNITS = 32 +DROPOUT_RATE = 0.3 # Bom para regularizar um modelo maior +LEARNING_RATE = 0.0005 # LR inicial para ReduceLROnPlateau +L2_REG = 0.0001 # Regularização L2 leve + +# Treinamento +BATCH_SIZE = 128 +EPOCHS = 100 # Deixe EarlyStopping controlar + +# Caminhos para Salvar +MODEL_SAVE_DIR = "app/model" +MODEL_NAME = "model.h5" +# Nomes de scaler mais descritivos que você sugeriu: +PRICE_VOL_SCALER_NAME = "price_volume_atr_norm_scaler.joblib" +INDICATOR_SCALER_NAME = "other_indicators_scaler.joblib" + +# EXPECTED_SCALED_FEATURES_FOR_MODEL define os nomes das colunas APÓS o escalonamento +# que são usadas para criar as sequências e alimentar o modelo no script de treino. +# E também o que o rnn_predictor.py DEVE produzir após aplicar seus scalers carregados. +EXPECTED_SCALED_FEATURES_FOR_MODEL = [f"{col}_scaled" for col in BASE_FEATURE_COLS] +# ^^^ IMPORTANTE: Esta linha assume que TODAS as features em BASE_FEATURE_COLS +# serão escaladas e terão o sufixo _scaled. +# Se 'rsi_14', 'bbp', 'buy_condition_v1' não forem escaladas ou tiverem +# outro tratamento, esta lista precisa ser ajustada manualmente. +# Exemplo: Se 'buy_condition_v1' é binária e não escalada: +# EXPECTED_SCALED_FEATURES_FOR_MODEL = [f"{col}_scaled" for col in BASE_FEATURE_COLS if col != 'buy_condition_v1'] + ['buy_condition_v1'] +# Por simplicidade, vamos assumir que todas são escaladas por enquanto. +EXPECTED_FEATURES_ORDER = EXPECTED_SCALED_FEATURES_FOR_MODEL + +INDIVIDUAL_ASSET_BASE_FEATURES = EXPECTED_FEATURES_ORDER + +MODEL_SAVE_DIR = "app/model" +PRICE_VOL_SCALER_NAME = "price_volume_atr_norm_scaler.joblib" +INDICATOR_SCALER_NAME = "other_indicators_scaler.joblib" \ No newline at end of file diff --git a/scripts/data_handler.py b/scripts/data_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..93ddd7eaf785cd735c28d7e8f2d89b7c081e5b57 --- /dev/null +++ b/scripts/data_handler.py @@ -0,0 +1,342 @@ +from typing import List, Tuple +import pandas as pd +import numpy as np +import ccxt +import pandas_ta as ta +from datetime import datetime, timedelta, timezone # Adicionado timezone + +# Importa constantes do config.py +from config import PREDICTION_HORIZON, PRICE_CHANGE_THRESHOLD, BASE_FEATURE_COLS, WINDOW_SIZE + + +# Novos inidcadores mover para config +# Períodos para as novas MAs +SHORT_MA_PERIOD = 10 # Exemplo: SMA de 10 períodos +LONG_MA_PERIOD = 50 # Exemplo: SMA de 50 períodos + +# Períodos para ADX +ADX_PERIOD = 14 + +# Níveis para Osciladores Extremos +RSI_OVERBOUGHT = 70 +RSI_OVERSOLD = 30 +STOCH_OVERBOUGHT = 80 # Para Estocástico %K +STOCH_OVERSOLD = 20 +CCI_OVERBOUGHT = 100 +CCI_OVERSOLD = -100 +MFI_OVERBOUGHT = 80 +MFI_OVERSOLD = 20 + +# Período para Média Móvel do Volume +VOLUME_AVG_PERIOD = 20 + +#Fim NI + +def fetch_ohlcv_data_ccxt(symbol: str, timeframe: str, days_to_fetch: int, limit_per_call: int = 1000) -> pd.DataFrame: + exchange = ccxt.binance() + print(f"Buscando dados para {symbol} na {exchange.id} com timeframe {timeframe}...") + all_ohlcv = [] + since_dt = datetime.now(timezone.utc) - timedelta(days=days_to_fetch) + since = exchange.parse8601(since_dt.isoformat()) + + while True: + try: + print(f"Buscando {limit_per_call} candles desde {exchange.iso8601(since)}...") + ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since, limit_per_call) + if not ohlcv: break + all_ohlcv.extend(ohlcv) + last_timestamp_in_batch = ohlcv[-1][0] + since = last_timestamp_in_batch + exchange.rateLimit + print(f"Coletados {len(ohlcv)} candles. Último: {exchange.iso8601(last_timestamp_in_batch)}. Total: {len(all_ohlcv)}") + if len(ohlcv) < limit_per_call: break + # Condição de parada mais robusta + processed_days = (last_timestamp_in_batch - exchange.parse8601(since_dt.isoformat())) / (1000 * 60 * 60 * 24) + if processed_days >= days_to_fetch: + break + except ccxt.NetworkError as e: + print(f"Erro de rede CCXT: {e}. Tentando novamente em 5s...") + # Adicionar um retry real aqui seria bom, ou time.sleep(5) + except ccxt.ExchangeError as e: + print(f"Erro da Exchange CCXT: {e}. Parando busca.") + break + except Exception as e: + print(f"Erro inesperado no fetch: {e}. Parando busca.") + break + + if not all_ohlcv: + raise ValueError("Nenhum dado OHLCV foi coletado.") + df = pd.DataFrame(all_ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']) + df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms') + df.set_index('timestamp', inplace=True) + print(f"Total de {len(df)} candles OHLCV coletados para {symbol}.") + return df + +def calculate_technical_indicators(ohlcv_df: pd.DataFrame) -> pd.DataFrame: + print("Calculando indicadores técnicos...") + df = ohlcv_df.copy() + if ta: + # Indicadores base + df.ta.sma(length=10, close='close', append=True, col_names=('sma_10',)) + df.ta.rsi(length=14, close='close', append=True, col_names=('rsi_14',)) + df.ta.log_return(length=14, close='close', append=True, col_names=('log_return',)) + df.ta.macd(close='close', append=True, col_names=('macd', 'macdh', 'macds')) # Pega só 'macd' depois + df.ta.atr(length=14, append=True, col_names=('atr',)) + df.ta.bbands(length=20, close='close', append=True, col_names=('bbl', 'bbm', 'bbu', 'bbb', 'bbp')) # Pega só 'bbp' depois + df.ta.cci(length=37, append=True, col_names=('cci_37',)) # Usando o período do exemplo + df.ta.mfi(length=37, append=True, col_names=('mfi_37',)) # Usando o período do exemplo + df['body_size'] = abs(df['close'] - df['open']) + df['body_size_norm_atr'] = df['body_size'] / (df['atr'] + 1e-7) + avg_body_period = 12 # Do exemplo do EA + df['avg_body_prev_12'] = df['body_size'].shift(1).rolling(window=avg_body_period).mean() # Média dos 12 corpos anteriores + df['body_vs_avg_body'] = df['body_size'] / (df['avg_body_prev_12'] + 1e-7) + # Normalização pelo ATR (APÓS cálculo de ATR e outros indicadores base) + # Garantir que 'atr' não é zero ou NaN antes da divisão + df.dropna(subset=['atr'], inplace=True) # Remove linhas onde ATR não pôde ser calculado + df = df[df['atr'] > 1e-7] # Remove linhas com ATR muito pequeno ou zero + df['open_div_atr'] = df['open'] / df['atr'] + df['high_div_atr'] = df['high'] / df['atr'] + df['low_div_atr'] = df['low'] / df['atr'] + df['close_div_atr'] = df['close'] / df['atr'] + df['close_div_atr'] = df['close'] / df['atr'] + df['volume_div_atr'] = df['volume'] / df['atr'] + # Assegura que as colunas base para estas normalizações existam + if 'sma_10' in df.columns: + df['sma_10_div_atr'] = df['sma_10'] / df['atr'] + if 'macd' in df.columns: # 'macd' é a linha principal do MACD + df['macd_div_atr'] = df['macd'] / df['atr'] + + # ------ Novos indicadores -- # + # --- Indicadores Base anteriores --- + if 'sma_10' in BASE_FEATURE_COLS or 'sma_10_div_atr' in BASE_FEATURE_COLS: # Só calcula se for usar + df.ta.sma(length=10, close='close', append=True, col_names=('sma_10',)) + if 'rsi_14' in BASE_FEATURE_COLS: + df.ta.rsi(length=14, close='close', append=True, col_names=('rsi_14',)) + + # Calcula componentes MACD, mesmo que só use 'macd' depois + if 'macd' in BASE_FEATURE_COLS or 'macd_div_atr' in BASE_FEATURE_COLS or 'macd_cross_signal' in BASE_FEATURE_COLS: + macd_results = df.ta.macd(close='close', append=False) + if macd_results is not None and not macd_results.empty: + df['macd'] = macd_results.iloc[:,0] # Linha MACD + df['macds'] = macd_results.iloc[:,1] # Linha de Sinal MACD + # df['macdh'] = macd_results.iloc[:,2] # Histograma MACD (opcional) + + if 'atr' in BASE_FEATURE_COLS or any('_div_atr' in col for col in BASE_FEATURE_COLS): + df.ta.atr(length=14, append=True, col_names=('atr',)) + + if 'bbp' in BASE_FEATURE_COLS or 'dist_bbu_norm_atr' in BASE_FEATURE_COLS or 'dist_bbl_norm_atr' in BASE_FEATURE_COLS: + df.ta.bbands(length=20, close='close', append=True, col_names=('bbl', 'bbm', 'bbu', 'bbb', 'bbp')) + + if 'cci_37' in BASE_FEATURE_COLS: + df.ta.cci(length=37, append=True, col_names=('cci_37',)) + + if 'mfi_37' in BASE_FEATURE_COLS: + df.ta.mfi(length=37, append=True, col_names=('mfi_37',)) + + if 'body_size_norm_atr' in BASE_FEATURE_COLS or 'body_vs_avg_body' in BASE_FEATURE_COLS: + if 'open' in df.columns and 'close' in df.columns: + df['body_size'] = abs(df['close'] - df['open']) + + # --- 1. Cruzamentos de Médias Móveis --- + if 'ma_short' not in df.columns and ('ma_cross_signal' in BASE_FEATURE_COLS or 'ma_diff_norm_atr' in BASE_FEATURE_COLS): + df.ta.sma(length=SHORT_MA_PERIOD, close='close', append=True, col_names=('ma_short',)) + if 'ma_long' not in df.columns and ('ma_cross_signal' in BASE_FEATURE_COLS or 'ma_diff_norm_atr' in BASE_FEATURE_COLS): + df.ta.sma(length=LONG_MA_PERIOD, close='close', append=True, col_names=('ma_long',)) + + if 'ma_cross_signal' in BASE_FEATURE_COLS and 'ma_short' in df.columns and 'ma_long' in df.columns: + df['ma_short_prev'] = df['ma_short'].shift(1) + df['ma_long_prev'] = df['ma_long'].shift(1) + # Sinal de Compra: curta cruza longa para cima + buy_cross = (df['ma_short_prev'] < df['ma_long_prev']) & (df['ma_short'] > df['ma_long']) + # Sinal de Venda: curta cruza longa para baixo + sell_cross = (df['ma_short_prev'] > df['ma_long_prev']) & (df['ma_short'] < df['ma_long']) + df['ma_cross_signal'] = 0 + df.loc[buy_cross, 'ma_cross_signal'] = 1 # Compra + df.loc[sell_cross, 'ma_cross_signal'] = -1 # Venda (ou use 2 para outra classe) + print("TA: Feature 'ma_cross_signal' calculada.") + + if 'ma_diff_norm_atr' in BASE_FEATURE_COLS and 'ma_short' in df.columns and 'ma_long' in df.columns and 'atr' in df.columns: + df['ma_diff'] = df['ma_short'] - df['ma_long'] + df['ma_diff_norm_atr'] = df['ma_diff'] / (df['atr'] + 1e-9) # Evitar divisão por zero no ATR + print("TA: Feature 'ma_diff_norm_atr' calculada.") + + # --- 2. Força da Tendência (ADX) --- + if 'adx_14' in BASE_FEATURE_COLS or 'adx_trend_signal' in BASE_FEATURE_COLS: + adx_results = df.ta.adx(length=ADX_PERIOD, append=False) + if adx_results is not None and not adx_results.empty: + df['adx_14'] = adx_results.iloc[:,0] # ADX + df['dmp_14'] = adx_results.iloc[:,1] # +DI ou DMP + df['dmn_14'] = adx_results.iloc[:,2] # -DI ou DMN + if 'adx_trend_signal' in BASE_FEATURE_COLS: + df['adx_trend_signal'] = 0 + # ADX > 20-25 geralmente indica tendência. +DI > -DI = tendência de alta. + strong_uptrend = (df['adx_14'] > 20) & (df['dmp_14'] > df['dmn_14']) + strong_downtrend = (df['adx_14'] > 20) & (df['dmn_14'] > df['dmp_14']) + df.loc[strong_uptrend, 'adx_trend_signal'] = 1 + df.loc[strong_downtrend, 'adx_trend_signal'] = -1 + print("TA: Features ADX calculadas.") + + # --- 3. Níveis de Suporte/Resistência Dinâmicos (Distância para BBands normalizada por ATR) --- + if 'dist_bbu_norm_atr' in BASE_FEATURE_COLS and 'close' in df.columns and 'bbu' in df.columns and 'atr' in df.columns: + df['dist_bbu_norm_atr'] = (df['bbu'] - df['close']) / (df['atr'] + 1e-9) + print("TA: Feature 'dist_bbu_norm_atr' calculada.") + if 'dist_bbl_norm_atr' in BASE_FEATURE_COLS and 'close' in df.columns and 'bbl' in df.columns and 'atr' in df.columns: + df['dist_bbl_norm_atr'] = (df['close'] - df['bbl']) / (df['atr'] + 1e-9) + print("TA: Feature 'dist_bbl_norm_atr' calculada.") + + # --- 4. Osciladores em Níveis Extremos --- + # Estocástico (preciso calcular primeiro) + if 'stoch_k_extreme' in BASE_FEATURE_COLS or 'stoch_d_extreme' in BASE_FEATURE_COLS: + stoch_results = df.ta.stoch(k=14, d=3, smooth_k=3, append=False) # Configurações padrão + if stoch_results is not None and not stoch_results.empty: + df['stoch_k'] = stoch_results.iloc[:,0] # %K + df['stoch_d'] = stoch_results.iloc[:,1] # %D + if 'stoch_k_extreme' in BASE_FEATURE_COLS: + df['stoch_k_extreme'] = 0 + df.loc[df['stoch_k'] > STOCH_OVERBOUGHT, 'stoch_k_extreme'] = 1 # Sobrecomprado + df.loc[df['stoch_k'] < STOCH_OVERSOLD, 'stoch_k_extreme'] = -1 # Sobrevendido + print("TA: Features Estocástico e stoch_k_extreme calculadas.") + + if 'rsi_extreme' in BASE_FEATURE_COLS and 'rsi_14' in df.columns: + df['rsi_extreme'] = 0 + df.loc[df['rsi_14'] > RSI_OVERBOUGHT, 'rsi_extreme'] = 1 + df.loc[df['rsi_14'] < RSI_OVERSOLD, 'rsi_extreme'] = -1 + print("TA: Feature 'rsi_extreme' calculada.") + + if 'cci_extreme' in BASE_FEATURE_COLS and 'cci_37' in df.columns: + df['cci_extreme'] = 0 + df.loc[df['cci_37'] > CCI_OVERBOUGHT, 'cci_extreme'] = 1 + df.loc[df['cci_37'] < CCI_OVERSOLD, 'cci_extreme'] = -1 + print("TA: Feature 'cci_extreme' calculada.") + + if 'mfi_extreme' in BASE_FEATURE_COLS and 'mfi_37' in df.columns: + df['mfi_extreme'] = 0 + df.loc[df['mfi_37'] > MFI_OVERBOUGHT, 'mfi_extreme'] = 1 + df.loc[df['mfi_37'] < MFI_OVERSOLD, 'mfi_extreme'] = -1 + print("TA: Feature 'mfi_extreme' calculada.") + + # --- 5. Sinais Combinados (Simples) --- + # Ex: RSI sobrevendido E MACD cruzou para cima recentemente? + if 'rsi_macd_buy_combo' in BASE_FEATURE_COLS and 'rsi_14' in df.columns and 'macd' in df.columns and 'macds' in df.columns: + rsi_is_oversold = df['rsi_14'] < RSI_OVERSOLD + # MACD cruzou sinal para cima no candle anterior ou atual + macd_crossed_up = (df['macd'].shift(1) < df['macds'].shift(1)) & (df['macd'] > df['macds']) + df['rsi_macd_buy_combo'] = (rsi_is_oversold & macd_crossed_up).astype(int) + print("TA: Feature 'rsi_macd_buy_combo' calculada.") + + # --- 6. Volume Anômalo --- + if 'volume_anom_signal' in BASE_FEATURE_COLS and 'volume' in df.columns: + df['volume_avg'] = df['volume'].rolling(window=VOLUME_AVG_PERIOD).mean() + # Sinal se volume atual > 2x a média + df['volume_anom_signal'] = (df['volume'] > 2 * df['volume_avg']).astype(int) + print("TA: Feature 'volume_anom_signal' calculada.") + + # --- Features de Corpo de Candle (já tinha antes, mas garantindo que serão usadas se estiverem em BASE_FEATURE_COLS) --- + if 'body_size_norm_atr' in BASE_FEATURE_COLS: + if 'body_size' in df.columns and 'atr' in df.columns: + # Re-checar se ATR é válido, pois body_size pode ter menos NaNs + temp_df_bs = df[['body_size', 'atr']].copy() + temp_df_bs.dropna(subset=['atr'], inplace=True) + temp_df_bs = temp_df_bs[temp_df_bs['atr'] > 1e-9] + if not temp_df_bs.empty: + df['body_size_norm_atr'] = temp_df_bs['body_size'] / temp_df_bs['atr'] + # print("TA: body_size_norm_atr (re)calculado/verificado.") + # else: + # print("AVISO: 'body_size' ou 'atr' ausente para 'body_size_norm_atr'.") + + if 'body_vs_avg_body' in BASE_FEATURE_COLS: + if 'body_size' in df.columns: + if 'avg_body_prev' not in df.columns: # Evitar recalcular se já existe + df['avg_body_prev'] = df['body_size'].shift(1).rolling(window=12).mean() + df['body_vs_avg_body'] = df['body_size'] / (df['avg_body_prev'] + 1e-7) + # print("TA: body_vs_avg_body (re)calculado/verificado.") + # else: + # print("AVISO: 'body_size' ausente para 'body_vs_avg_body'.") + + # --- Features Derivadas _div_atr (já tinha antes, garantindo que serão usadas se estiverem em BASE_FEATURE_COLS) --- + df.dropna(subset=['atr'], inplace=True) + df = df[df['atr'] > 1e-9] + + if 'open_div_atr' in BASE_FEATURE_COLS and 'open' in df.columns: df['open_div_atr'] = df['open'] / df['atr'] + if 'high_div_atr' in BASE_FEATURE_COLS and 'high' in df.columns: df['high_div_atr'] = df['high'] / df['atr'] + if 'low_div_atr' in BASE_FEATURE_COLS and 'low' in df.columns: df['low_div_atr'] = df['low'] / df['atr'] + if 'close_div_atr' in BASE_FEATURE_COLS and 'close' in df.columns: df['close_div_atr'] = df['close'] / df['atr'] + if 'volume_div_atr' in BASE_FEATURE_COLS and 'volume' in df.columns: df['volume_div_atr'] = df['volume'] / df['atr'] + if 'sma_10_div_atr' in BASE_FEATURE_COLS and 'sma_10' in df.columns: df['sma_10_div_atr'] = df['sma_10'] / df['atr'] + if 'macd_div_atr' in BASE_FEATURE_COLS and 'macd' in df.columns: df['macd_div_atr'] = df['macd'] / df['atr'] + + if 'log_return_1' in BASE_FEATURE_COLS and 'close' in df.columns: + df['log_return_1'] = np.log(df['close'] / df['close'].shift(1)) + + # Novas features AD e volume + rolling_vol_mean = df['volume'].rolling(window=20).mean() + rolling_vol_std = df['volume'].rolling(window=20).std() + df['volume_zscore'] = (df['volume'] - rolling_vol_mean) / (rolling_vol_std + 1e-7) + + # Certifique-se que MACD, MACDS, SMA_50 já foram calculados + df.ta.sma(length=50, close='close', append=True, col_names=('sma_50',)) + df['buy_condition_v1'] = ((df['macd'] > df['macds']) & (df['rsi_14'] > 50) & (df['close'] > df['sma_50'])).astype(int) + + df['cond_compra_v1'] = ((df['macd'] > df['macds']) & (df['rsi_14'] > 50) & (df['close'] > df.ta.sma(length=50, append=False))).astype(int) + + + + df.dropna(inplace=True) + + final_cols_present = [col for col in BASE_FEATURE_COLS if col in df.columns] + if len(final_cols_present) != len(BASE_FEATURE_COLS): + missing = list(set(BASE_FEATURE_COLS) - set(final_cols_present)) + print(f"ALERTA: Após todos os cálculos, colunas de BASE_FEATURE_COLS estão faltando: {missing}") + print(f"Verifique os cálculos e se as colunas base para eles (open, high, low, close, volume) existem no input.") + print(f"Colunas disponíveis: {df.columns.tolist()}") + # raise ValueError(f"Nem todas as features base foram geradas: {missing}") + + # Selecionar apenas as colunas que realmente existem e estão em BASE_FEATURE_COLS + # para evitar erros se alguma não pôde ser calculada. + # O script de treino verificará se todas as BASE_FEATURE_COLS existem antes de escalar. + existing_base_features = [col for col in BASE_FEATURE_COLS if col in df.columns] + print(f"Indicadores técnicos e features derivadas calculadas. Features retornadas: {existing_base_features}") + return df[existing_base_features + (['open', 'high', 'low', 'close', 'volume'] if not any(c in existing_base_features for c in ['open', 'high', 'low', 'close', 'volume']) else [])] # Garante que OHLCV original está lá para calculate_targets, se não for parte das features + + + + # ---- Fim NI + + # Remover todos os NaNs restantes gerados pelos indicadores + df.dropna(inplace=True) + print("Indicadores técnicos calculados e features normalizadas pelo ATR criadas.") + else: + print("pandas_ta não disponível. Verifique a instalação.") + return df + +def calculate_targets(df: pd.DataFrame, horizon: int, threshold: float) -> pd.DataFrame: + print("Criando coluna alvo para predição...") + data = df.copy() + data['future_price'] = data['close'].shift(-horizon) + data['price_change_pct'] = (data['future_price'] - data['close']) / data['close'] + data['target'] = (data['price_change_pct'] > threshold).astype(int) + data.dropna(subset=['future_price', 'price_change_pct', 'target'], inplace=True) + print(f"Distribuição do Alvo:\n{data['target'].value_counts(normalize=True, dropna=False)}") + return data + +def create_sequences(data: pd.DataFrame, target_col_name: str, window_size: int, feature_col_names: List[str]) -> tuple[np.ndarray, np.ndarray]: + print(f"Criando sequências com window_size={window_size} usando features: {feature_col_names}") + X_list, y_list = [], [] + + # Verificar se todas as feature_col_names e target_col_name existem no DataFrame + required_cols = feature_col_names + [target_col_name] + missing_cols = [col for col in required_cols if col not in data.columns] + if missing_cols: + raise ValueError(f"Colunas ausentes no DataFrame para criar sequências: {missing_cols}. Colunas disponíveis: {data.columns.tolist()}") + + # Usar .values pode ser mais rápido para DataFrames grandes, mas indexar por nome é mais seguro + feature_values = data[feature_col_names].values + target_values = data[target_col_name].values + + for i in range(len(feature_values) - window_size + 1): # Ajuste no loop para incluir o último elemento possível + X_list.append(feature_values[i : i + window_size]) + y_list.append(target_values[i + window_size - 1]) # Alvo correspondente ao final da janela + + X = np.array(X_list) + y = np.array(y_list) + print(f"Shape de X (sequências): {X.shape}, Shape de y (alvos): {y.shape}") + return X, y \ No newline at end of file diff --git a/scripts/data_handler_multi_asset.py b/scripts/data_handler_multi_asset.py new file mode 100644 index 0000000000000000000000000000000000000000..a244d4bd21d8ac8c6dc892d04fd64c337b7b51bf --- /dev/null +++ b/scripts/data_handler_multi_asset.py @@ -0,0 +1,269 @@ +# rnn/data_handler_multi_asset.py (NOVO ARQUIVO) + +import pandas as pd +import numpy as np +import yfinance as yf # Ou ccxt, dependendo da sua preferência de fonte de dados +import pandas_ta as ta +from datetime import datetime, timedelta, timezone +from typing import List, Dict, Optional + +# Importar do seu config.py +# Assumindo que config.py está em ../config.py ou rnn/config.py +# Ajuste o import conforme sua estrutura. +# Se train_rnn_model.py e este data_handler estiverem na mesma pasta 'scripts', +# e config.py estiver um nível acima: +# from ..app/config.py import ( +# MULTI_ASSET_LIST, TIMEFRAME, DAYS_OF_DATA_TO_FETCH, +# # etc. para features a serem calculadas +# ) +# Por agora, vamos definir aqui para exemplo: + +# EXEMPLO DE CONFIGURAÇÃO (Mova para config.py depois) +MULTI_ASSET_SYMBOLS = { + 'crypto_eth': 'ETH-USD', # yfinance ticker para ETH/USD + 'crypto_ada': 'ADA-USD', # yfinance ticker para ADA/USD + 'stock_aapl': 'AAPL', # NASDAQ + 'stock_petr': 'PETR4.SA' # B3 +} # Use os tickers corretos para yfinance ou ccxt +TIMEFRAME_YFINANCE = '1h' # yfinance suporta '1m', '2m', '5m', '15m', '30m', '60m', '90m', '1h', '1d', '5d', '1wk', '1mo', '3mo' +# Para '1h', yfinance só retorna os últimos 730 dias. Para mais dados, use '1d'. +# Se usar ccxt, TIMEFRAME = '1h' como antes. +DAYS_TO_FETCH = 365 * 2 # 2 anos + +# Lista das features base que você quer calcular para CADA ativo +# (as 19 que definimos antes) +INDIVIDUAL_ASSET_BASE_FEATURES = [ + 'open', 'high', 'low', 'close', 'volume', # OHLCV originais são necessários para os cálculos + 'sma_10', 'rsi_14', 'macd', 'macds', 'atr', 'bbp', 'cci_37', 'mfi_37', 'adx_14', + 'volume_zscore', 'body_size', 'body_size_norm_atr', 'body_vs_avg_body', + 'log_return', 'buy_condition_v1', # 'sma_50' é calculada dentro de buy_condition_v1 + # As colunas _div_atr serão criadas a partir destas +] + +# Features que serão normalizadas pelo ATR +COLS_TO_NORM_BY_ATR = ['open', 'high', 'low', 'close', 'volume', 'sma_10', 'macd', 'body_size'] + + +def fetch_single_asset_ohlcv_yf(ticker_symbol: str, period: str = "2y", interval: str = "1h") -> pd.DataFrame: + """ Adaptação da sua função fetch_historical_ohlcv de financial_data_agent.py """ + print(f"Buscando dados para {ticker_symbol} com yfinance (period: {period}, interval: {interval})...") + try: + ticker = yf.Ticker(ticker_symbol) + # Para dados horários, o período máximo é geralmente 730 dias com yfinance + # Se precisar de mais, considere '1d' e depois reamostre, ou use ccxt para cripto. + if interval == '1h' and period.endswith('y') and int(period[:-1]) * 365 > 730: + print(f"AVISO: yfinance pode limitar dados horários a 730 dias. Buscando 'max' para {interval} e depois fatiando.") + data = ticker.history(interval=interval, period="730d") # Pega o máximo possível + elif interval == '1d' and period.endswith('y'): + data = ticker.history(period=period, interval=interval) + else: # Para períodos menores ou outros intervalos + data = ticker.history(period=period, interval=interval) + + if data.empty: + print(f"Nenhum dado encontrado para {ticker_symbol}.") + return pd.DataFrame() + + data.rename(columns={ + "Open": "open", "High": "high", "Low": "low", + "Close": "close", "Volume": "volume", "Adj Close": "adj_close" + }, inplace=True) + + # Selecionar apenas as colunas OHLCV e garantir que o índice é DatetimeIndex UTC + data = data[['open', 'high', 'low', 'close', 'volume']] + if data.index.tz is None: + data.index = data.index.tz_localize('UTC') + else: + data.index = data.index.tz_convert('UTC') + + # Para dados horários, yfinance pode retornar dados do fim de semana (sem volume) + # e o último candle pode estar incompleto. + # if interval == '1h': + # data = data[data['volume'] > 0] # Remover candles sem volume + # data = data[:-1] # Remover o último candle que pode estar incompleto + + print(f"Dados coletados para {ticker_symbol}: {len(data)} linhas.") + return data + except Exception as e: + print(f"Erro ao buscar dados para {ticker_symbol} com yfinance: {e}") + return pd.DataFrame() + + +def calculate_all_features_for_single_asset(ohlcv_df: pd.DataFrame) -> Optional[pd.DataFrame]: + """Calcula todas as features base para um único ativo.""" + if ohlcv_df.empty: return None + df = ohlcv_df.copy() + print(f"Calculando features para ativo (shape inicial: {df.shape})...") + + if ta: + df.ta.sma(length=10, close='close', append=True, col_names=('sma_10',)) + df.ta.rsi(length=14, close='close', append=True, col_names=('rsi_14',)) + macd_out = df.ta.macd(close='close', append=False) + if macd_out is not None and not macd_out.empty: + df['macd'] = macd_out.iloc[:,0] + df['macds'] = macd_out.iloc[:,2] # Linha de sinal para buy_condition + df.ta.atr(length=14, append=True, col_names=('atr',)) + df.ta.bbands(length=20, close='close', append=True, col_names=('bbl', 'bbm', 'bbu', 'bbb', 'bbp')) + df.ta.cci(length=37, append=True, col_names=('cci_37',)) + df.ta.mfi(length=37, append=True, col_names=('mfi_37',)) + adx_out = df.ta.adx(length=14, append=False) + if adx_out is not None and not adx_out.empty: + df['adx_14'] = adx_out.iloc[:,0] + + rolling_vol_mean = df['volume'].rolling(window=20).mean() + rolling_vol_std = df['volume'].rolling(window=20).std() + df['volume_zscore'] = (df['volume'] - rolling_vol_mean) / (rolling_vol_std + 1e-9) + + df['body_size'] = abs(df['close'] - df['open']) + + # ATR precisa existir para as próximas. Drop NaNs do ATR primeiro. + df.dropna(subset=['atr'], inplace=True) + df_atr_valid = df[df['atr'] > 1e-9].copy() + if df_atr_valid.empty: + print("AVISO: ATR inválido para todas as linhas restantes, features _div_atr e body_size_norm_atr podem ser todas NaN ou vazias.") + # Criar colunas com NaN para manter a estrutura + df['body_size_norm_atr'] = np.nan + for col in COLS_TO_NORM_BY_ATR: + df[f'{col}_div_atr'] = np.nan + else: + df['body_size_norm_atr'] = df['body_size'] / df['atr'] # ATR já filtrado para > 1e-9 + for col in COLS_TO_NORM_BY_ATR: + if col in df.columns: + df[f'{col}_div_atr'] = df[col] / (df['atr'] + 1e-9) # Adicionar 1e-9 aqui também por segurança + else: + df[f'{col}_div_atr'] = np.nan + + + df['body_vs_avg_body'] = df['body_size'] / (df['body_size'].rolling(window=20).mean() + 1e-9) + df['log_return'] = np.log(df['close'] / df['close'].shift(1)) + + sma_50_series = df.ta.sma(length=50, close='close', append=False) + if sma_50_series is not None: df['sma_50'] = sma_50_series + else: df['sma_50'] = np.nan + + if all(col in df.columns for col in ['macd', 'macds', 'rsi_14', 'close', 'sma_50']): + df['buy_condition_v1'] = ((df['macd'] > df['macds']) & (df['rsi_14'] > 50) & (df['close'] > df['sma_50'])).astype(int) + else: + df['buy_condition_v1'] = 0 + + # Selecionar apenas as colunas que realmente usaremos como features base para o modelo + # (incluindo as _div_atr e as originais que não foram normalizadas por ATR) + # Esta lista de features é a que será passada para os scalers no script de treino. + # E também as colunas que o rnn_predictor.py precisará ter antes de aplicar seus scalers. + # Esta lista deve vir do config.py (BASE_FEATURE_COLS) + + # Exemplo: + # final_feature_columns = [ + # 'open_div_atr', 'high_div_atr', 'low_div_atr', 'close_div_atr', 'volume_div_atr', + # 'log_return', 'rsi_14', 'atr', 'bbp', 'cci_37', 'mfi_37', + # 'body_size_norm_atr', 'body_vs_avg_body', 'macd', 'sma_10_div_atr', + # 'adx_14', 'volume_zscore', 'buy_condition_v1' + # ] # Esta é a BASE_FEATURE_COLS do seu config.py + + # Verificar se todas as colunas em INDIVIDUAL_ASSET_BASE_FEATURES existem + # (INDIVIDUAL_ASSET_BASE_FEATURES deve ser igual a config.BASE_FEATURE_COLS) + current_feature_cols = [col for col in INDIVIDUAL_ASSET_BASE_FEATURES if col in df.columns] + missing_cols = [col for col in INDIVIDUAL_ASSET_BASE_FEATURES if col not in df.columns] + if missing_cols: + print(f"AVISO: Colunas de features ausentes após cálculo: {missing_cols}. Usando apenas as disponíveis: {current_feature_cols}") + + df_final_features = df[current_feature_cols].copy() + df_final_features.dropna(inplace=True) + print(f"Features calculadas. Shape após dropna: {df_final_features.shape}. Colunas: {df_final_features.columns.tolist()}") + return df_final_features + else: + print("pandas_ta não está disponível.") + return None + + +def get_multi_asset_data_for_rl( + asset_symbols_map: Dict[str, str], # Ex: {'crypto_eth': 'ETH-USD', ...} + timeframe_yf: str, + days_to_fetch: int +) -> Optional[pd.DataFrame]: + """ + Busca, processa e combina dados de múltiplos ativos em um DataFrame achatado. + """ + all_asset_features_list = [] + min_data_length = float('inf') # Para truncar todos os DFs para o mesmo comprimento + + # Usar os tickers yfinance do asset_symbols_map + for asset_key, yf_ticker in asset_symbols_map.items(): + print(f"\n--- Processando {asset_key} ({yf_ticker}) ---") + # Para yfinance, '1h' retorna max 730d. '1d' retorna mais. + # Se days_to_fetch for > 730 e timeframe_yf for '1h', ajuste o período. + period_yf = f"{days_to_fetch}d" # yfinance aceita "Xd" para dias + if timeframe_yf == '1h' and days_to_fetch > 730: + print(f"AVISO: Para {timeframe_yf}, buscando no máximo 730 dias com yfinance para {yf_ticker}.") + period_yf = "730d" + + single_asset_ohlcv = fetch_single_asset_ohlcv_yf(yf_ticker, period=period_yf, interval=timeframe_yf) + if single_asset_ohlcv.empty: + print(f"AVISO: Sem dados OHLCV para {yf_ticker}, pulando este ativo.") + continue + + single_asset_features = calculate_all_features_for_single_asset(single_asset_ohlcv) + if single_asset_features is None or single_asset_features.empty: + print(f"AVISO: Sem features calculadas para {yf_ticker}, pulando este ativo.") + continue + + # Adicionar prefixo para achatar + single_asset_features = single_asset_features.add_prefix(f"{asset_key}_") + all_asset_features_list.append(single_asset_features) + min_data_length = min(min_data_length, len(single_asset_features)) + + if not all_asset_features_list: + print("ERRO: Nenhum dado de feature de ativo foi processado com sucesso.") + return None + + # Truncar todos os DataFrames para o mesmo comprimento (o do menor) ANTES de concatenar + # para garantir alinhamento temporal mais robusto se os históricos tiverem começos diferentes. + # Isso é feito alinhando pelo final dos DataFrames. + if min_data_length == float('inf') or min_data_length == 0 : + print("ERRO: min_data_length inválido, não é possível truncar DataFrames.") + return None + + truncated_asset_features_list = [df.tail(min_data_length) for df in all_asset_features_list] + + # Concatenar todos os DataFrames de features (alinhados por timestamp/índice) + # join='inner' garante que só teremos timestamps onde TODOS os ativos (que retornaram dados) têm dados. + combined_df = pd.concat(truncated_asset_features_list, axis=1, join='inner') + + # Verificar se o resultado não está vazio + if combined_df.empty: + print("ERRO: DataFrame combinado está vazio após concatenação e join. Verifique os dados dos ativos.") + return None + + # Drop quaisquer linhas que ainda possam ter NaNs após o join (improvável se cada df individual já foi tratado) + combined_df.dropna(inplace=True) + + if combined_df.empty: + print("ERRO: DataFrame combinado está vazio após dropna final.") + return None + + print(f"\nDataFrame multi-ativo final gerado com shape: {combined_df.shape}") + print(f"Exemplo de colunas: {combined_df.columns.tolist()[:10]}...") # Mostra as primeiras 10 + return combined_df + + +if __name__ == '__main__': + print("Testando data_handler_multi_asset.py...") + # Substitua pelos tickers yfinance reais que você quer usar + test_assets = { + 'eth': 'ETH-USD', + 'btc': 'BTC-USD', + # 'aapl': 'AAPL' # Exemplo de ação + } + multi_asset_data = get_multi_asset_data_for_rl( + test_assets, + timeframe_yf='1h', # Para teste rápido, período menor + days_to_fetch=90 # Para teste rápido, período menor + ) + + if multi_asset_data is not None and not multi_asset_data.empty: + print("\n--- Exemplo do DataFrame Multi-Ativo Gerado ---") + print(multi_asset_data.head()) + print(f"\nShape: {multi_asset_data.shape}") + print(f"\nInfo:") + multi_asset_data.info() + else: + print("\nFalha ao gerar DataFrame multi-ativo.") \ No newline at end of file diff --git a/scripts/model_builder.py b/scripts/model_builder.py new file mode 100644 index 0000000000000000000000000000000000000000..14aa96ae5524bc9155b2fa4b5b303eece46e2663 --- /dev/null +++ b/scripts/model_builder.py @@ -0,0 +1,48 @@ +# model_builder.py + +import tensorflow as tf +from tensorflow.keras import regularizers # Boa prática manter o import aqui também + +# Importa as constantes do config.py +from config import LSTM_UNITS, DENSE_UNITS, DROPOUT_RATE, LEARNING_RATE, L2_REG + +def build_lstm_model(input_shape): # LEARNING_RATE é pega do config.py agora + """Constrói o modelo LSTM com regularização L2.""" + + model = tf.keras.Sequential() + + # Camadas LSTM + for i, units in enumerate(LSTM_UNITS): + return_sequences = True if i < len(LSTM_UNITS) - 1 else False + layer_name_lstm = f'lstm_{i}' # Adicionar nomes às camadas é uma boa prática + if i == 0: + model.add(tf.keras.layers.LSTM(units, + return_sequences=return_sequences, + input_shape=input_shape, + kernel_regularizer=regularizers.l2(L2_REG), + name=layer_name_lstm + )) + else: + model.add(tf.keras.layers.LSTM(units, + return_sequences=return_sequences, + kernel_regularizer=regularizers.l2(L2_REG), + name=layer_name_lstm + )) + model.add(tf.keras.layers.Dropout(DROPOUT_RATE, name=f'dropout_lstm_{i}')) + + # Camada Densa + model.add(tf.keras.layers.Dense(DENSE_UNITS, + activation='relu', + kernel_regularizer=regularizers.l2(L2_REG), + name='dense_main' + )) + model.add(tf.keras.layers.Dropout(DROPOUT_RATE, name='dropout_dense')) + + # Camada de Saída + model.add(tf.keras.layers.Dense(1, activation='sigmoid', name='output')) # CORRIGIDO: aspas simples + + # O LEARNING_RATE é pego do config.py + optimizer = tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE, amsgrad=True, clipvalue=1.0) # clipvalue é bom para RNNs + model.compile(optimizer=optimizer, loss='binary_crossentropy', metrics=['accuracy']) # CORRIGIDO: aspas + model.summary() + return model \ No newline at end of file diff --git a/scripts/old_train_model/train_rnn_model_0-50_treshoud_stable.py b/scripts/old_train_model/train_rnn_model_0-50_treshoud_stable.py new file mode 100644 index 0000000000000000000000000000000000000000..1fe20cc16f470e60469ec8642b0e1fa6cd203e70 --- /dev/null +++ b/scripts/old_train_model/train_rnn_model_0-50_treshoud_stable.py @@ -0,0 +1,642 @@ + +import os +import numpy as np +import pandas as pd +import tensorflow as tf +from sklearn.model_selection import train_test_split +from sklearn.preprocessing import MinMaxScaler +import pandas_ta as ta # Para indicadores +import joblib # Para salvar scalers +import matplotlib.pyplot as plt # Para visualização +import ccxt # Para buscar dados (opcional) +from datetime import datetime, timedelta, timezone +from tensorflow.keras import regularizers + +# --- Parâmetros de Configuração --- +# Dados +SYMBOL = 'S&P 500' # Par de cripto para treinar +TIMEFRAME = '1h' # Timeframe dos candles ('1m', '5m', '1h', '1d') +# Quantos dados buscar (2 anos de dados de 1h)S + +DAYS_OF_DATA_TO_FETCH = 365 * 2 +LIMIT_PER_FETCH = 1000 # Limite de candles por chamada da API da exchange + +# Features e Janela +WINDOW_SIZE = 60 # Número de passos de tempo na sequência de entrada +# Features que usaremos (OHLCV + Indicadores) +# A ordem aqui é importante para o escalonamento e para o modelo. +# O nome das colunas após o escalonamento será:'close_scaled', 'volume_scaled'... +# No nosso rnn_predictor.py, usamos EXPECTED_FEATURES_ORDER com nomes _scaled. +# Preparar as colunas base e depois escalar. +BASE_FEATURE_COLS = [ + 'close_div_atr', # Normalizado + 'volume_div_atr', # Normalizado + 'sma_10_div_atr', # Normalizado + 'rsi_14', # Índice (0-100), não precisa normalizar pelo ATR + 'macd_div_atr', # Normalizado + 'atr', # Average True Range + 'bbp' ] # Antes do scaling +# O NÚMERO DE FEATURES QUE O MODELO REALMENTE VERÁ APÓS O SCALING +NUM_FEATURES = len(BASE_FEATURE_COLS) + +# Alvo da Predição (Target) +# Prever se o preço vai subir N passos no futuro +PREDICTION_HORIZON = 5 # Previsão de subida em 5 horas +PRICE_CHANGE_THRESHOLD = 0.005 # Considerar "subiu" se o preço aumentar > 0.5% + +# Modelo RNN +LSTM_UNITS = [32,16] # Unidades nas camadas LSTM +DENSE_UNITS = 16 +DROPOUT_RATE = 0.0 +LEARNING_RATE = 0.001 + +# Treinamento +BATCH_SIZE = 32 +EPOCHS = 100 + +# Patch para Salvar +MODEL_SAVE_DIR = "app/model" +MODEL_NAME = "model.h5" +PRICE_VOL_SCALER_NAME = "price_volume_scaler.joblib" +INDICATOR_SCALER_NAME = "indicator_scaler.joblib" +# único scaler para todas as features: +# ALL_FEATURES_SCALER_NAME = "all_features_scaler.joblib" + + +def fetch_ohlcv_data_ccxt(symbol, timeframe, days_to_fetch, limit_per_call=1000): + """Busca dados OHLCV de uma exchange usando ccxt.""" + exchange = ccxt.binance() # Ou outra exchange () + print(f"Buscando dados para {symbol} na {exchange.id} com timeframe {timeframe}...") + + + all_ohlcv = [] + + + since_dt = datetime.now(timezone.utc) - timedelta(days=days_to_fetch) + since = exchange.parse8601(since_dt.isoformat()) + + while True: + try: + print(f"Buscando {limit_per_call} candles desde {exchange.iso8601(since)}...") + ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since, limit_per_call) + if not ohlcv: break + all_ohlcv.extend(ohlcv) + last_timestamp_in_batch = ohlcv[-1][0] + since = last_timestamp_in_batch + exchange.rateLimit + print(f"Coletados {len(ohlcv)} candles. Último: {exchange.iso8601(last_timestamp_in_batch)}. Total: {len(all_ohlcv)}") + if len(ohlcv) < limit_per_call: break + # CORRIGIDO: Usar datetime.now(timezone.utc) + if len(all_ohlcv) >= (days_to_fetch * 24) and last_timestamp_in_batch > exchange.parse8601(datetime.now(timezone.utc).isoformat()): + break + + all_ohlcv.extend(ohlcv) + last_timestamp_in_batch = ohlcv[-1][0] + + # Para evitar buscar o mesmo candle, avançamos 1ms do último timestamp + since = last_timestamp_in_batch + exchange.rateLimit # Adiciona o rateLimit para a próxima busca + + print(f"Coletados {len(ohlcv)} candles. Último timestamp: {exchange.iso8601(last_timestamp_in_batch)}. Total: {len(all_ohlcv)}") + + # Condição de parada se já buscamos dados suficientes ou se a exchange retorna menos que o limite + if len(ohlcv) < limit_per_call: + break + if len(all_ohlcv) >= (days_to_fetch * (24 if timeframe.endswith('h') else 1440 if timeframe.endswith('m') else 1)): # Estimativa + if all_ohlcv[-1][0] > exchange.parse8601(datetime.utcnow().isoformat()): # Se passou do tempo atual + break + + # Pequena pausa para respeitar rate limits, ccxt já faz isso com enableRateLimit=True + # mas uma pausa extra pode ser útil se buscar muitos dados. + # time.sleep(exchange.rateLimit / 1000) + + except ccxt.NetworkError as e: + print(f"Erro de rede CCXT: {e}. Tentando novamente em 5s...") + # time.sleep(5) + except ccxt.ExchangeError as e: + print(f"Erro da Exchange CCXT: {e}. Parando busca.") + break + except Exception as e: + print(f"Erro inesperado: {e}. Parando busca.") + break + + if not all_ohlcv: + raise ValueError("Nenhum dado OHLCV foi coletado. Verifique o símbolo, timeframe ou a exchange.") + + df = pd.DataFrame(all_ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']) + df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms') + df.set_index('timestamp', inplace=True) + print(f"Total de {len(df)} candles OHLCV coletados para {symbol}.") + return df + + +def calculate_targets(df, horizon=PREDICTION_HORIZON, threshold=PRICE_CHANGE_THRESHOLD): + """ + Cria o alvo da predição. + Retorna 1 se o preço futuro (horizonte) subir mais que o threshold, 0 caso contrário. + """ + df['future_price'] = df['close'].shift(-horizon) + df['price_change_pct'] = (df['future_price'] - df['close']) / df['close'] + df['target'] = (df['price_change_pct'] > threshold).astype(int) + df.dropna(inplace=True) # Remove linhas onde future_price é NaN + return df + + +def create_sequences(data, target_col, window_size, feature_cols): + """Cria sequências de dados e seus alvos correspondentes.""" + X, y = [], [] + # Assegura que estamos trabalhando com um NumPy array para slicing eficiente + data_values = data[feature_cols].values + target_values = data[target_col].values + + for i in range(len(data_values) - window_size): + X.append(data_values[i:(i + window_size)]) + y.append(target_values[i + window_size -1]) # Alvo correspondente ao final da janela + # Ou, se o alvo foi calculado com shift(-horizon), o alvo já está alinhado + # y.append(target_values[i + window_size -1 + horizon -1]) # Se o target é para o futuro da janela + return np.array(X), np.array(y) + + +def build_lstm_model(input_shape, lstm_units, dense_units, dropout_rate, initial_learning_rate): + """Constrói o modelo LSTM com regularização L2.""" # Descrição atualizada + + + model = tf.keras.Sequential() # DEFINE O MODELO PRIMEIRO + + L2_REG = 0.0001 # Valor para regularização L2 + + # Camadas LSTM + for i, units in enumerate(lstm_units): + return_sequences = True if i < len(lstm_units) - 1 else False + if i == 0: + model.add(tf.keras.layers.LSTM(units, + return_sequences=return_sequences, + input_shape=input_shape, + kernel_regularizer=regularizers.l2(L2_REG) + )) + else: + model.add(tf.keras.layers.LSTM(units, + return_sequences=return_sequences, + kernel_regularizer=regularizers.l2(L2_REG) # ADICIONADO AQUI + + )) + model.add(tf.keras.layers.Dropout(dropout_rate)) + + # Camada Densa + model.add(tf.keras.layers.Dense(dense_units, + activation='relu', + kernel_regularizer=regularizers.l2(L2_REG) # ADICIONADO AQUI + )) + model.add(tf.keras.layers.Dropout(dropout_rate)) + + # Camada de Saída (classificação binária) + model.add(tf.keras.layers.Dense(1, activation='sigmoid')) + + optimizer = tf.keras.optimizers.Adam(learning_rate=initial_learning_rate ,amsgrad=True, clipvalue=1.0) + model.compile(optimizer=optimizer, loss='binary_crossentropy', metrics=['accuracy']) + model.summary() + return model + + +def main(): + print("Iniciando script de treinamento da RNN...") + + # --- 1. Carregar Dados --- + # Buscar com CCXT + try: + ohlcv_df = fetch_ohlcv_data_ccxt(SYMBOL, TIMEFRAME, DAYS_OF_DATA_TO_FETCH, LIMIT_PER_FETCH) + except Exception as e: + print(f"Falha ao buscar dados com CCXT: {e}") + print("Verifique se você tem um arquivo 'data.csv' no mesmo diretório para fallback.") + # Carregar de CSV + try: + ohlcv_df = pd.read_csv("data.csv", index_col='timestamp', parse_dates=True) + print("Dados carregados de data.csv") + except FileNotFoundError: + print("Erro: data.csv não encontrado. Forneça dados para treinamento.") + return + + if ohlcv_df.empty: + print("DataFrame de dados está vazio. Encerrando.") + return + + + + + # --- + # >>> SLIDAR COM ÍNDICES DUPLICADOS <<< + print(f"Shape original do ohlcv_df: {ohlcv_df.shape}") + if not ohlcv_df.index.is_unique: + print("AVISO: Timestamps duplicados encontrados no índice. Removendo duplicatas (mantendo a primeira ocorrência)...") + # Mantém a primeira ocorrência de cada timestamp duplicado + ohlcv_df = ohlcv_df[~ohlcv_df.index.duplicated(keep='first')] + # Alternativa: manter a última ocorrência: + # ohlcv_df = ohlcv_df[~ohlcv_df.index.duplicated(keep='last')] + # Alternativa: fazer uma média das linhas duplicadas (mais complexo se os valores OHLCV diferirem) + print(f"Shape do ohlcv_df após remover duplicatas: {ohlcv_df.shape}") + + # Opcional: Reordenar o índice apenas para garantir, embora o fetch_ohlcv geralmente retorne ordenado + ohlcv_df.sort_index(inplace=True) + # >>> FIM DA SEÇÃO DE ÍNDICES DUPLICADOS <<< + + + # >>> SEÇÃO DE LIMPEZA ROBUSTA DO ÍNDICE E TIMESTAMPS <<< + print(f"Shape original do ohlcv_df: {ohlcv_df.shape}") + + # Mover o índice (timestamp) para uma coluna temporária + if isinstance(ohlcv_df.index, pd.DatetimeIndex): + ohlcv_df.reset_index(inplace=True) # Move o índice para uma coluna 'timestamp' (ou 'index') + + # Renomear a coluna de timestamp para 'ts' para evitar conflito se já houver 'timestamp' + # Se o reset_index criou uma coluna 'index' contendo os timestamps: + if 'index' in ohlcv_df.columns and pd.api.types.is_datetime64_any_dtype(ohlcv_df['index']): + ohlcv_df.rename(columns={'index': 'ts_temp'}, inplace=True) + timestamp_col_name = 'ts_temp' + elif 'timestamp' in ohlcv_df.columns and pd.api.types.is_datetime64_any_dtype(ohlcv_df['timestamp']): + # Se já existe uma coluna 'timestamp' e ela é o antigo índice + ohlcv_df.rename(columns={'timestamp': 'ts_temp'}, inplace=True) + timestamp_col_name = 'ts_temp' + else: + print("ERRO: Não foi possível identificar a coluna de timestamp após reset_index.") + return + + # Verificar duplicatas na coluna de timestamp e remover + initial_rows = len(ohlcv_df) + ohlcv_df.drop_duplicates(subset=[timestamp_col_name], keep='first', inplace=True) + rows_removed = initial_rows - len(ohlcv_df) + if rows_removed > 0: + print(f"AVISO: Removidas {rows_removed} linhas com timestamps duplicados (coluna '{timestamp_col_name}').") + + # Passo 3: Recriar o índice a partir da coluna de timestamp limpa + ohlcv_df.set_index(timestamp_col_name, inplace=True) + ohlcv_df.index.name = 'timestamp' # Renomeia o índice de volta para 'timestamp' + + # Passo 4: Garantir que o índice está ordenado + ohlcv_df.sort_index(inplace=True) + + # Verificação final de unicidade do índice + if not ohlcv_df.index.is_unique: + print("ERRO CRÍTICO: O índice ainda não é único após a limpeza. Algo está errado.") + # Inspecionar ohlcv_df.index[ohlcv_df.index.duplicated(keep=False)] + return + else: + print(f"Índice agora é único. Shape do ohlcv_df após limpeza: {ohlcv_df.shape}") + # >>> FIM DA SEÇÃO DE LIMPEZA ROBUSTA DO ÍNDICE <<< + + # --- 2. Calcular Indicadores Técnicos --- + print("Calculando indicadores técnicos...") + if ta: + ohlcv_df.ta.sma(length=10, close='close', append=True, col_names=('sma_10',)) + ohlcv_df.ta.rsi(length=14, close='close', append=True, col_names=('rsi_14',)) + + ohlcv_df.ta.macd(close='close', append=True, col_names=('macd', 'macdh', 'macds')) # Adiciona 3 colunas + ohlcv_df.ta.atr(length=14, append=True, col_names=('atr',)) # Adiciona ATR + ohlcv_df.ta.bbands(length=20, close='close', append=True, col_names=('bbl', 'bbm', 'bbu', 'bbb', 'bbp')) # Adiciona 5 colunas de Bollinger + + + + + # Após o cálculo de todos os indicadores e ohlcv_df.dropna() + ohlcv_df['close_div_atr'] = ohlcv_df['close'] / (ohlcv_df['atr'] + 1e-7) # Adiciona epsilon para evitar divisão por zero + ohlcv_df['volume_div_atr'] = ohlcv_df['volume'] / (ohlcv_df['atr'] + 1e-7) # Volume também pode ser normalizado se fizer sentido + # Features baseadas em preço (sma_10, macd, bbm) + ohlcv_df['sma_10_div_atr'] = ohlcv_df['sma_10'] / (ohlcv_df['atr'] + 1e-7) + ohlcv_df['macd_div_atr'] = ohlcv_df['macd'] / (ohlcv_df['atr'] + 1e-7) + + + ohlcv_df.dropna(inplace=True) # Remove NaNs dos indicadores + + # --- 3. Criar Alvo (Target) --- + print("Criando coluna alvo para predição...") + ohlcv_df_with_target = calculate_targets(ohlcv_df.copy(), PREDICTION_HORIZON, PRICE_CHANGE_THRESHOLD) + + + + #Logica dos Scalers + api_price_vol_cols = ['close_div_atr', 'volume_div_atr'] + if all(col in ohlcv_df_with_target.columns for col in api_price_vol_cols): + price_volume_scaler_to_save = MinMaxScaler() + price_volume_scaler_to_save.fit(ohlcv_df_with_target[api_price_vol_cols]) + joblib.dump(price_volume_scaler_to_save, os.path.join(MODEL_SAVE_DIR, PRICE_VOL_SCALER_NAME)) + print(f"Scaler de Preço/Volume (API: {api_price_vol_cols}) salvo em: {os.path.join(MODEL_SAVE_DIR, PRICE_VOL_SCALER_NAME)}") + else: + print(f"ERRO: Colunas para salvar price_volume_scaler ({api_price_vol_cols}) não encontradas em ohlcv_df_with_target.") + return # Falha crítica se não puder salvar scalers esperados + + # Colunas que representam "indicadores" para a API + # Estas são as colunas de BASE_FEATURE_COLS que NÃO são 'close_div_atr' ou 'volume_div_atr' + # E que o rnn_predictor.py também vai escalar com o indicator_scaler. + # ATENÇÃO: `BASE_FEATURE_COLS` deve ser a lista FINAL de features que entram no modelo, + # então `indicator_cols_for_api_scaler` deve pegar as colunas de INDICADORES dessa lista. + + # Supondo que BASE_FEATURE_COLS atualizada é: + BASE_FEATURE_COLS = ['close_div_atr', 'volume_div_atr', 'sma_10_div_atr', 'rsi_14', 'macd_div_atr', 'atr', 'bbp'] + indicator_cols_for_api_scaler = ['sma_10_div_atr', 'rsi_14', 'macd_div_atr', 'atr', 'bbp'] + + + if indicator_cols_for_api_scaler and all(col in ohlcv_df_with_target.columns for col in indicator_cols_for_api_scaler): + indicator_data_to_fit_api_scaler = ohlcv_df_with_target[indicator_cols_for_api_scaler].copy() + indicator_data_to_fit_api_scaler.dropna(subset=indicator_cols_for_api_scaler, inplace=True) + + if not indicator_data_to_fit_api_scaler.empty: + indicator_scaler_to_save = MinMaxScaler() + indicator_scaler_to_save.fit(indicator_data_to_fit_api_scaler) + joblib.dump(indicator_scaler_to_save, os.path.join(MODEL_SAVE_DIR, INDICATOR_SCALER_NAME)) + print(f"Scaler de Indicadores (API: {indicator_cols_for_api_scaler}) salvo em: {os.path.join(MODEL_SAVE_DIR, INDICATOR_SCALER_NAME)}") + else: + print(f"ERRO: Não foi possível treinar/salvar o scaler de indicadores (API: {indicator_cols_for_api_scaler}) devido a dados vazios.") + return # Falha crítica + elif not indicator_cols_for_api_scaler: + print("Nenhuma coluna de indicador definida para salvar o indicator_scaler para API.") + else: + missing_cols = [col for col in indicator_cols_for_api_scaler if col not in ohlcv_df_with_target.columns] + print(f"ERRO: Colunas para salvar indicator_scaler ({indicator_cols_for_api_scaler}) não encontradas ou lista vazia. Ausentes: {missing_cols}") + return # Falha crítica + + + + #Fimfitagem + + # --- 4. Escalonar TODAS as Features (PARA TREINAMENTO DO MODELO ATUAL) --- + print("Escalonando features para criação de sequências (usando as colunas de BASE_FEATURE_COLS)...") + + # Garantir que ohlcv_df_with_target contenha todas as BASE_FEATURE_COLS + if not all(col in ohlcv_df_with_target.columns for col in BASE_FEATURE_COLS): + missing_final_base_cols = [col for col in BASE_FEATURE_COLS if col not in ohlcv_df_with_target.columns] + print(f"ERRO: Colunas finais em BASE_FEATURE_COLS ausentes em ohlcv_df_with_target: {missing_final_base_cols}") + print(f"Colunas disponíveis: {ohlcv_df_with_target.columns.tolist()}") + return + + features_to_scale_df = ohlcv_df_with_target[BASE_FEATURE_COLS].copy() + + # Este scaler 'geral' é usado para preparar os dados para as sequências. + general_training_scaler = MinMaxScaler() + scaled_features_values = general_training_scaler.fit_transform(features_to_scale_df) # Fita e transforma TODAS as BASE_FEATURE_COLS juntas + + scaled_features_df = pd.DataFrame(scaled_features_values, + columns=[f"{col}_scaled" for col in BASE_FEATURE_COLS], + index=features_to_scale_df.index) + + + + # --- + + if ohlcv_df_with_target.empty: + print("DataFrame vazio após cálculo do alvo. Verifique os parâmetros de horizonte/threshold. Encerrando.") + return + + print(f"Distribuição do Alvo:\n{ohlcv_df_with_target['target'].value_counts(normalize=True)}") + pv_cols_for_scaling = ['close_div_atr', 'volume_div_atr'] + if all(col in ohlcv_df_with_target.columns for col in pv_cols_for_scaling): # Verifica se existem + price_volume_scaler = MinMaxScaler() + price_volume_data_original = ohlcv_df_with_target[pv_cols_for_scaling].copy() + price_volume_scaler.fit(price_volume_data_original) + joblib.dump(price_volume_scaler, os.path.join(MODEL_SAVE_DIR, PRICE_VOL_SCALER_NAME)) + print(f"Scaler de Preço/Volume salvo em: {os.path.join(MODEL_SAVE_DIR, PRICE_VOL_SCALER_NAME)}") + else: + print(f"Aviso: Colunas para price_volume_scaler não encontradas: {pv_cols_for_scaling}") + + # Scaler para TODOS os outros indicadores definidos em BASE_FEATURE_COLS + indicator_cols_for_scaling = [col for col in BASE_FEATURE_COLS if col not in ['close_div_atr', 'volume_div_atr']] + + if indicator_cols_for_scaling and all(col in ohlcv_df_with_target.columns for col in indicator_cols_for_scaling): + indicator_data_original = ohlcv_df_with_target[indicator_cols_for_scaling].copy() + indicator_data_original.dropna(inplace=True) # Importante, pois alguns indicadores podem ter mais NaNs no início + + if not indicator_data_original.empty: + indicator_scaler_instance = MinMaxScaler() + indicator_scaler_instance.fit(indicator_data_original) + joblib.dump(indicator_scaler_instance, os.path.join(MODEL_SAVE_DIR, INDICATOR_SCALER_NAME)) + print(f"Scaler de Indicadores ({indicator_cols_for_scaling}) salvo em: {os.path.join(MODEL_SAVE_DIR, INDICATOR_SCALER_NAME)}") + else: + print(f"Não foi possível treinar/salvar o scaler de indicadores ({indicator_cols_for_scaling}) devido a dados vazios após dropna.") + elif not indicator_cols_for_scaling: + print("Nenhuma coluna de indicador definida para o indicator_scaler.") + else: + print(f"Aviso: Algumas colunas para indicator_scaler não encontradas: {indicator_cols_for_scaling}") + + # Verificar se as colunas base existem após os indicadores + missing_base_cols = [col for col in BASE_FEATURE_COLS if col not in ohlcv_df.columns] + if missing_base_cols: + print(f"Erro: Colunas base para features ausentes após cálculo de indicadores: {missing_base_cols}") + print(f"Colunas disponíveis: {ohlcv_df.columns.tolist()}") + print(f"Verifique BASE_FEATURE_COLS e a saída dos indicadores.") + return + + + # --- 4. Escalonar Features --- + print("Escalonando features...") + # Separar features que serão escaladas + features_to_scale_df = ohlcv_df_with_target[BASE_FEATURE_COLS].copy() + + # IMPORTANTE: Escalar ANTES de criar as sequências + # E escalar APENAS as features, não o target. + + # Único scaler para todas as features (mais simples de gerenciar) + # Scalers separados (no rnn_predictor.py), precisará adaptar. + scaler = MinMaxScaler() + scaled_features_values = scaler.fit_transform(features_to_scale_df) + + # Criar um DataFrame com as features escaladas para facilitar a criação de sequências + scaled_features_df = pd.DataFrame(scaled_features_values, + columns=[f"{col}_scaled" for col in BASE_FEATURE_COLS], + index=features_to_scale_df.index) + + # Juntar o target de volta com as features escaladas + # Assegurar que os índices estão alinhados para o join + data_for_sequences = scaled_features_df.join(ohlcv_df_with_target[['target']]) + data_for_sequences.dropna(inplace=True) # Caso o join crie NaNs (improvável se os índices estiverem corretos) + + if data_for_sequences.empty: + print("DataFrame vazio após escalonamento e join com target. Encerrando.") + return + + # As colunas de features para criar sequências são as escaladas + sequence_feature_cols = [f"{col}_scaled" for col in BASE_FEATURE_COLS] + + # --- 5. Criar Sequências --- + print("Criando sequências de dados...") + X, y = create_sequences(data_for_sequences, 'target', WINDOW_SIZE, sequence_feature_cols) + + if X.shape[0] == 0: + print("Nenhuma sequência foi criada. Verifique o tamanho dos dados e WINDOW_SIZE. Encerrando.") + return + print(f"Shape de X (sequências de entrada): {X.shape}") # (num_samples, window_size, num_features) + print(f"Shape de y (alvos): {y.shape}") # (num_samples,) + + # --- 6. Dividir em Treino e Teste --- + print("Dividindo dados em treino e teste...") + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y) + print(f"Tamanho do conjunto de treino: {X_train.shape[0]}, Teste: {X_test.shape[0]}") + + # --- 7. Construir e Treinar Modelo --- + print("Construindo modelo LSTM...") + # O input_shape para o modelo é (WINDOW_SIZE, NUM_FEATURES) + model = build_lstm_model((WINDOW_SIZE, NUM_FEATURES), LSTM_UNITS, DENSE_UNITS, DROPOUT_RATE, LEARNING_RATE) + + print("Iniciando treinamento do modelo...") + # Adicionar callbacks (opcional, mas recomendado para treinamento longo) + reduce_lr_callback = tf.keras.callbacks.ReduceLROnPlateau( + monitor='val_loss', + factor=0.2, # Reduz LR em 80% (multiplica por 0.2) + patience=7, # Paciência um pouco menor que o EarlyStopping + min_lr=1e-7, # Não reduzir abaixo disso + verbose=1 + ) + early_stopping_callback = tf.keras.callbacks.EarlyStopping( + monitor='val_loss', + patience=25, + restore_best_weights=True + ) + callbacks = [early_stopping_callback, reduce_lr_callback] + + from sklearn.utils.class_weight import compute_class_weight + unique_classes_in_train = np.unique(y_train) + class_weights_values = compute_class_weight('balanced', classes=unique_classes_in_train, y=y_train) + class_weights = {cls: weight for cls, weight in zip(unique_classes_in_train, class_weights_values)} + print(f"Pesos de Classe para Treinamento: {class_weights}") + + history = model.fit(X_train, y_train, + epochs=EPOCHS, + batch_size=BATCH_SIZE, + validation_data=(X_test, y_test), + callbacks=callbacks, + class_weight=class_weights, + verbose=1) + + # --- 8. Avaliar Modelo --- + print("Avaliando modelo no conjunto de teste...") + loss, accuracy = model.evaluate(X_test, y_test, verbose=0) + print(f"Perda no Teste: {loss:.4f}") + print(f"Acurácia no Teste: {accuracy:.4f}") + + # Após model.evaluate() + from sklearn.metrics import classification_report, confusion_matrix + y_pred_probs = model.predict(X_test) + y_pred_classes = (y_pred_probs > 0.65).astype(int) # 0.5, 0.6, 0.65, 0.7, 0.75 Valores de referencia par Thresholud + + print("\nRelatório de Classificação no Conjunto de Teste:") + print(classification_report(y_test, y_pred_classes, target_names=['No Rise (0)', 'Rise (1)'])) + + print("\nMatriz de Confusão no Conjunto de Teste:") + cm = confusion_matrix(y_test, y_pred_classes) + print(cm) + import seaborn as sns # Para um plot mais bonito da matriz + plt.figure(figsize=(6,5)) + sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", xticklabels=['No Rise', 'Rise'], yticklabels=['No Rise', 'Rise']) + plt.xlabel('Predito') + plt.ylabel('Verdadeiro') + plt.title('Matriz de Confusão') + plt.savefig(os.path.join(MODEL_SAVE_DIR, "confusion_matrix.png")) + + # --- 9. Salvar Modelo e Scalers --- + print("Salvando modelo e scalers...") + os.makedirs(MODEL_SAVE_DIR, exist_ok=True) # Cria o diretório se não existir + + model_path = os.path.join(MODEL_SAVE_DIR, MODEL_NAME) + model.save(model_path) + print(f"Modelo salvo em: {model_path}") + + # Salvar o scaler usado para TODAS as features + # Scalers separados em rnn_predictor.py (price_vol_scaler, indicator_scaler), + # Treinar e salvar esses scalers separadamente aqui. + # Por agora, este script usa um único scaler para `BASE_FEATURE_COLS`. + # ADAPTAR ESTA PARTE PARA CORRESPONDER AO `rnn_predictor.py` + + # rnn_predictor.py espera PRICE_VOL_SCALER_PATH e INDICATOR_SCALER_PATH: + # Criar dois DataFrames: um com 'close', 'volume' e outro com 'sma_10', 'rsi_14' + # Treinar um scaler para cada e salvá-los. + + # Salvar price_volume_scaler: + pv_cols_for_scaling = ['volume_div_atr', 'volume_div_atr'] # Colunas originais ANTES de escalar + pv_data_to_fit_scaler = ohlcv_df_with_target[pv_cols_for_scaling].copy() # Usa o DF antes de criar sequências + + # Usar o mesmo scaler que foi usado para criar `scaled_features_df` + #`scaler.fit_transform(features_to_scale_df)` onde `features_to_scale_df` + # continha todas as `BASE_FEATURE_COLS`, então `scaler` é o que foi treinado em todas elas. + + # Para salvar scalers separados: + + # Scaler para Preço e Volume + price_volume_scaler = MinMaxScaler() + price_volume_data_original = ohlcv_df_with_target[['close_div_atr', 'volume_div_atr']].copy() # Dados originais + price_volume_scaler.fit(price_volume_data_original) # Fita nos dados originais + joblib.dump(price_volume_scaler, os.path.join(MODEL_SAVE_DIR, PRICE_VOL_SCALER_NAME)) + print(f"Scaler de Preço/Volume salvo em: {os.path.join(MODEL_SAVE_DIR, PRICE_VOL_SCALER_NAME)}") + + # Scaler para Indicadores + indicator_cols_for_scaling = ['sma_10', 'rsi_14'] # Colunas originais dos indicadores + indicator_data_original = ohlcv_df_with_target[indicator_cols_for_scaling].copy() + indicator_data_original.dropna(inplace=True) # Garante que não há NaNs antes de fitar + if not indicator_data_original.empty: + indicator_scaler_instance = MinMaxScaler() + indicator_scaler_instance.fit(indicator_data_original) + joblib.dump(indicator_scaler_instance, os.path.join(MODEL_SAVE_DIR, INDICATOR_SCALER_NAME)) + print(f"Scaler de Indicadores salvo em: {os.path.join(MODEL_SAVE_DIR, INDICATOR_SCALER_NAME)}") + else: + print("Não foi possível treinar/salvar o scaler de indicadores devido a dados vazios.") + + + # --- 10. Plotar Histórico de Treinamento --- + plt.figure(figsize=(12, 6)) + plt.subplot(1, 2, 1) + plt.plot(history.history['accuracy'], label='Acurácia Treino') + plt.plot(history.history['val_accuracy'], label='Acurácia Validação') + plt.title('Acurácia do Modelo') + plt.xlabel('Época') + plt.ylabel('Acurácia') + plt.legend() + + plt.subplot(1, 2, 2) + plt.plot(history.history['loss'], label='Perda Treino') + plt.plot(history.history['val_loss'], label='Perda Validação') + plt.title('Perda do Modelo') + plt.xlabel('Época') + plt.ylabel('Perda') + plt.legend() + + + plot_path = os.path.join(MODEL_SAVE_DIR, "training_history.png") + plt.savefig(plot_path) + print(f"Gráfico do histórico de treinamento salvo em: {plot_path}") + plt.show() + + print("Script de treinamento concluído.") + + # Resultado de vários thresholds indicado e utilizado no rnn_predict + y_pred_probs = model.predict(X_test) + thresholds_to_test = [0.50, 0.55, 0.60, 0.65, 0.70, 0.75] + for thresh in thresholds_to_test: + print(f"\n--- Resultados com Threshold: {thresh:.2f} ---") + y_pred_classes = (y_pred_probs > thresh).astype(int) + print(classification_report(y_test, y_pred_classes, target_names=['No Rise (0)', 'Rise (1)'], zero_division=0)) + print(confusion_matrix(y_test, y_pred_classes)) + + +if __name__ == '__main__': + # Configurar GPU (TensorFlow com suporte a GPU) + gpus = tf.config.experimental.list_physical_devices('GPU') + if gpus: + try: + for gpu in gpus: + tf.config.experimental.set_memory_growth(gpu, True) + logical_gpus = tf.config.experimental.list_logical_devices('GPU') + print(len(gpus), "Physical GPUs,", len(logical_gpus), "Logical GPUs") + except RuntimeError as e: + print(e) + + + + + + + + main() + + +""" + ESTABILIDADE CONQUISTADA! Esta é a maior vitória desta rodada. As curvas de treinamento e validação estão significativamente mais estáveis. O BATCH_SIZE = 64 e o clipvalue=1.0 foram eficazes. + + MELHOR F1-SCORE PARA "RISE" ATÉ AGORA (com threshold 0.50): Um F1-score de 0.43 para a classe minoritária, com um recall de 0.58, é um progresso muito bom. Isso significa que o modelo está identificando mais da metade das oportunidades de subida, e quando ele sinaliza uma subida, tem uma chance razoável (embora não ideal) de estar correto. + + TRADE-OFF PRECISION-RECALL É EVIDENTE: Ao aumentar o threshold, a precisão para "Rise" tende a melhorar, mas o recall cai drasticamente. + + O MODELO ESTÁ APRENDENDO PADRÕES ÚTEIS: Os resultados com threshold 0.50 são um forte indicador de que as features e a arquitetura (com a estabilização) estão permitindo que o modelo capture sinais preditivos. """ \ No newline at end of file diff --git a/scripts/old_train_model/train_rnn_model_on_file_dev.py b/scripts/old_train_model/train_rnn_model_on_file_dev.py new file mode 100644 index 0000000000000000000000000000000000000000..93aec6392037ab4db14992bbb32570a66a31cd26 --- /dev/null +++ b/scripts/old_train_model/train_rnn_model_on_file_dev.py @@ -0,0 +1,648 @@ + +import os +import numpy as np +import pandas as pd +import tensorflow as tf +from sklearn.model_selection import train_test_split +from sklearn.preprocessing import MinMaxScaler +import pandas_ta as ta # Para indicadores +import joblib # Para salvar scalers +import matplotlib.pyplot as plt # Para visualização +import ccxt # Para buscar dados (opcional) +from datetime import datetime, timedelta, timezone +from tensorflow.keras import regularizers + +# --- Parâmetros de Configuração --- +# Dados +SYMBOL = 'BTC/USDT' # Par de cripto para treinar +TIMEFRAME = '1h' # Timeframe dos candles ('1m', '5m', '1h', '1d') +# Quantos dados buscar (2 anos de dados de 1h) + +DAYS_OF_DATA_TO_FETCH = 365 * 2 +LIMIT_PER_FETCH = 1000 # Limite de candles por chamada da API da exchange + +# Features e Janela +WINDOW_SIZE = 60 # Número de passos de tempo na sequência de entrada +# Features que usaremos (OHLCV + Indicadores) +# A ordem aqui é importante para o escalonamento e para o modelo. +# O nome das colunas após o escalonamento será:'close_scaled', 'volume_scaled'... +# No nosso rnn_predictor.py, usamos EXPECTED_FEATURES_ORDER com nomes _scaled. +# Preparar as colunas base e depois escalar. +BASE_FEATURE_COLS = [ + 'close_div_atr', # Normalizado + 'volume_div_atr', # Normalizado + #'sma_10_div_atr', # Normalizado + 'rsi_14', # Índice (0-100), não precisa normalizar pelo ATR + #'macd_div_atr', # Normalizado + 'atr', # Average True Range + 'bbp' ] # Antes do scaling + +# O NÚMERO DE FEATURES QUE O MODELO REALMENTE VERÁ APÓS O SCALING +NUM_FEATURES = len(BASE_FEATURE_COLS) + +# Alvo da Predição (Target) +# Prever se o preço vai subir N passos no futuro +PREDICTION_HORIZON = 5 # Previsão de subida em 5 horas +PRICE_CHANGE_THRESHOLD = 0.005 # Considerar "subiu" se o preço aumentar > 0.5% + +# Modelo RNN +LSTM_UNITS = [32,16] # Unidades nas camadas LSTM +DENSE_UNITS = 16 +DROPOUT_RATE = 0.1 +LEARNING_RATE = 0.001 + +# Treinamento +BATCH_SIZE = 64 +EPOCHS = 100 + +# Patch para Salvar +MODEL_SAVE_DIR = "app/model" +MODEL_NAME = "model.h5" +PRICE_VOL_SCALER_NAME = "price_volume_scaler.joblib" +INDICATOR_SCALER_NAME = "indicator_scaler.joblib" +# único scaler para todas as features: +# ALL_FEATURES_SCALER_NAME = "all_features_scaler.joblib" + + +def fetch_ohlcv_data_ccxt(symbol, timeframe, days_to_fetch, limit_per_call=1000): + """Busca dados OHLCV de uma exchange usando ccxt.""" + exchange = ccxt.binance() # Ou outra exchange de sua preferência + print(f"Buscando dados para {symbol} na {exchange.id} com timeframe {timeframe}...") + + + all_ohlcv = [] + + + since_dt = datetime.now(timezone.utc) - timedelta(days=days_to_fetch) + since = exchange.parse8601(since_dt.isoformat()) + + while True: + try: + print(f"Buscando {limit_per_call} candles desde {exchange.iso8601(since)}...") + ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since, limit_per_call) + if not ohlcv: break + all_ohlcv.extend(ohlcv) + last_timestamp_in_batch = ohlcv[-1][0] + since = last_timestamp_in_batch + exchange.rateLimit + print(f"Coletados {len(ohlcv)} candles. Último: {exchange.iso8601(last_timestamp_in_batch)}. Total: {len(all_ohlcv)}") + if len(ohlcv) < limit_per_call: break + + if len(all_ohlcv) >= (days_to_fetch * 24) and last_timestamp_in_batch > exchange.parse8601(datetime.now(timezone.utc).isoformat()): + break + + all_ohlcv.extend(ohlcv) + last_timestamp_in_batch = ohlcv[-1][0] + + # Para evitar buscar o mesmo candle, avançar 1ms do último timestamp + since = last_timestamp_in_batch + exchange.rateLimit # Adiciona o rateLimit para a próxima busca + + print(f"Coletados {len(ohlcv)} candles. Último timestamp: {exchange.iso8601(last_timestamp_in_batch)}. Total: {len(all_ohlcv)}") + + # Condição de parada se já buscamos dados suficientes ou se a exchange retorna menos que o limite + if len(ohlcv) < limit_per_call: + break + if len(all_ohlcv) >= (days_to_fetch * (24 if timeframe.endswith('h') else 1440 if timeframe.endswith('m') else 1)): # Estimativa + if all_ohlcv[-1][0] > exchange.parse8601(datetime.utcnow().isoformat()): # Se passou do tempo atual + break + + # Pequena pausa para respeitar rate limits, ccxt já faz isso com enableRateLimit=True + # Pausa extra para buscar muitos dados. + #time.sleep(exchange.rateLimit / 1000) + + except ccxt.NetworkError as e: + print(f"Erro de rede CCXT: {e}. Tentando novamente em 5s...") + # time.sleep(5) + except ccxt.ExchangeError as e: + print(f"Erro da Exchange CCXT: {e}. Parando busca.") + break + except Exception as e: + print(f"Erro inesperado: {e}. Parando busca.") + break + + if not all_ohlcv: + raise ValueError("Nenhum dado OHLCV foi coletado. Verifique o símbolo, timeframe ou a exchange.") + + df = pd.DataFrame(all_ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']) + df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms') + df.set_index('timestamp', inplace=True) + print(f"Total de {len(df)} candles OHLCV coletados para {symbol}.") + return df + + +def calculate_targets(df, horizon=PREDICTION_HORIZON, threshold=PRICE_CHANGE_THRESHOLD): + """ + Cria o alvo da predição. + Retorna 1 se o preço futuro (horizonte) subir mais que o threshold, 0 caso contrário. + """ + + # Shift close para pegar o 'close' do próximo candle para comparar com o 'open' atual + df['next_close'] = df['close'].shift(-1) + # O alvo é 1 se o próximo candle fechou acima do seu próprio open (candle verde) + # Ou você pode querer prever se o próximo close será maior que o close ATUAL + # df['target'] = (df['next_close'] > df['close']).astype(int) # Prever se o próximo close é > close atual + # Vamos tentar prever se o próximo candle em si é de alta: + df['next_open'] = df['open'].shift(-1) # Precisamos do open do próximo candle + df['target_raw'] = df['next_close'] - df['next_open'] # Variação do próximo candle + df['target'] = (df['target_raw'] > 0).astype(int) # 1 se o próximo candle for de alta + + # Remover a última linha que terá NaN para next_close e next_open + df.dropna(subset=['next_close', 'next_open', 'target'], inplace=True) + return df + + + """ df['future_price'] = df['close'].shift(-horizon) + df['price_change_pct'] = (df['future_price'] - df['close']) / df['close'] + df['target'] = (df['price_change_pct'] > threshold).astype(int) + df.dropna(inplace=True) # Remove future_price é NaN + return df """ + + +def create_sequences(data, target_col, window_size, feature_cols): + """Cria sequências de dados e seus alvos correspondentes.""" + X, y = [], [] + # Assegura que estamos trabalhando com um NumPy array para slicing eficiente + data_values = data[feature_cols].values + target_values = data[target_col].values + + for i in range(len(data_values) - window_size): + X.append(data_values[i:(i + window_size)]) + y.append(target_values[i + window_size -1]) # Alvo correspondente ao final da janela + # Ou, se o alvo foi calculado com shift(-horizon), o alvo já está alinhado + # y.append(target_values[i + window_size -1 + horizon -1]) # Se o target é para o futuro da janela + return np.array(X), np.array(y) + + +def build_lstm_model(input_shape, lstm_units, dense_units, dropout_rate, initial_learning_rate): + """Constrói o modelo LSTM com regularização L2.""" # Descrição atualizada + + + model = tf.keras.Sequential() # DEFINO O MODELO + + L2_REG = 0.0001 # Valor para regularização L2 + + # Camadas LSTM + for i, units in enumerate(lstm_units): + return_sequences = True if i < len(lstm_units) - 1 else False + if i == 0: + model.add(tf.keras.layers.LSTM(units, + return_sequences=return_sequences, + input_shape=input_shape, + kernel_regularizer=regularizers.l2(L2_REG) + )) + else: + model.add(tf.keras.layers.LSTM(units, + return_sequences=return_sequences, + kernel_regularizer=regularizers.l2(L2_REG) + + )) + model.add(tf.keras.layers.Dropout(dropout_rate)) + + # Camada Densa + model.add(tf.keras.layers.Dense(dense_units, + activation='relu', + kernel_regularizer=regularizers.l2(L2_REG) # ADICIONADO AQUI + )) + model.add(tf.keras.layers.Dropout(dropout_rate)) + + # Camada de Saída (classificação binária) + model.add(tf.keras.layers.Dense(1, activation='sigmoid')) + + optimizer = tf.keras.optimizers.Adam(learning_rate=initial_learning_rate ,amsgrad=True, clipvalue=1.0) + model.compile(optimizer=optimizer, loss='binary_crossentropy', metrics=['accuracy']) + model.summary() + return model + + +def main(): + print("Iniciando script de treinamento da RNN...") + + # --- 1. Carregar Dados --- + # Opção A: Buscar com CCXT + try: + ohlcv_df = fetch_ohlcv_data_ccxt(SYMBOL, TIMEFRAME, DAYS_OF_DATA_TO_FETCH, LIMIT_PER_FETCH) + except Exception as e: + print(f"Falha ao buscar dados com CCXT: {e}") + print("Verifique se você tem um arquivo 'data.csv' no mesmo diretório para fallback.") + # Opção B: Carregar de CSV (se a busca falhar ou preferir) + try: + ohlcv_df = pd.read_csv("data.csv", index_col='timestamp', parse_dates=True) + print("Dados carregados de data.csv") + except FileNotFoundError: + print("Erro: data.csv não encontrado. Forneça dados para treinamento.") + return + + if ohlcv_df.empty: + print("DataFrame de dados está vazio. Encerrando.") + return + + + + + # --- + # >>> SLIDAR COM ÍNDICES DUPLICADOS <<< + print(f"Shape original do ohlcv_df: {ohlcv_df.shape}") + if not ohlcv_df.index.is_unique: + print("AVISO: Timestamps duplicados encontrados no índice. Removendo duplicatas (mantendo a primeira ocorrência)...") + # Mantém a primeira ocorrência de cada timestamp duplicado + ohlcv_df = ohlcv_df[~ohlcv_df.index.duplicated(keep='first')] + # Alternativa: manter a última ocorrência: + # ohlcv_df = ohlcv_df[~ohlcv_df.index.duplicated(keep='last')] + # Alternativa: fazer uma média das linhas duplicadas (mais complexo se os valores OHLCV diferirem) + print(f"Shape do ohlcv_df após remover duplicatas: {ohlcv_df.shape}") + + # Opcional: Reordenar o índice apenas para garantir, embora o fetch_ohlcv geralmente retorne ordenado + ohlcv_df.sort_index(inplace=True) + # >>> FIM DA SEÇÃO DE ÍNDICES DUPLICADOS <<< + + + # >>> SEÇÃO DE LIMPEZA ROBUSTA DO ÍNDICE E TIMESTAMPS <<< + print(f"Shape original do ohlcv_df: {ohlcv_df.shape}") + + # Passo 1: Mover o índice (timestamp) para uma coluna temporária + if isinstance(ohlcv_df.index, pd.DatetimeIndex): + ohlcv_df.reset_index(inplace=True) # Move o índice para uma coluna 'timestamp' (ou 'index') + + # Renomear a coluna de timestamp para 'ts' para evitar conflito se já houver 'timestamp' + # Se o reset_index criou uma coluna 'index' contendo os timestamps: + if 'index' in ohlcv_df.columns and pd.api.types.is_datetime64_any_dtype(ohlcv_df['index']): + ohlcv_df.rename(columns={'index': 'ts_temp'}, inplace=True) + timestamp_col_name = 'ts_temp' + elif 'timestamp' in ohlcv_df.columns and pd.api.types.is_datetime64_any_dtype(ohlcv_df['timestamp']): + # Se já existe uma coluna 'timestamp' e ela é o antigo índice + ohlcv_df.rename(columns={'timestamp': 'ts_temp'}, inplace=True) + timestamp_col_name = 'ts_temp' + else: + print("ERRO: Não foi possível identificar a coluna de timestamp após reset_index.") + return + + # Verificar duplicatas na coluna de timestamp e remover + initial_rows = len(ohlcv_df) + ohlcv_df.drop_duplicates(subset=[timestamp_col_name], keep='first', inplace=True) + rows_removed = initial_rows - len(ohlcv_df) + if rows_removed > 0: + print(f"AVISO: Removidas {rows_removed} linhas com timestamps duplicados (coluna '{timestamp_col_name}').") + + # Passo 3: Recriar o índice a partir da coluna de timestamp limpa + ohlcv_df.set_index(timestamp_col_name, inplace=True) + ohlcv_df.index.name = 'timestamp' # Renomeia o índice de volta para 'timestamp' + + # Passo 4: Garantir que o índice está ordenado + ohlcv_df.sort_index(inplace=True) + + # Verificação final de unicidade do índice + if not ohlcv_df.index.is_unique: + print("ERRO CRÍTICO: O índice ainda não é único após a limpeza. Algo está errado.") + # Você pode querer inspecionar ohlcv_df.index[ohlcv_df.index.duplicated(keep=False)] + return + else: + print(f"Índice agora é único. Shape do ohlcv_df após limpeza: {ohlcv_df.shape}") + # >>> FIM DA SEÇÃO DE LIMPEZA ROBUSTA DO ÍNDICE <<< + + # --- 2. Calcular Indicadores Técnicos --- + print("Calculando indicadores técnicos...") + if ta: + ohlcv_df.ta.sma(length=10, close='close', append=True, col_names=('sma_10',)) + ohlcv_df.ta.rsi(length=14, close='close', append=True, col_names=('rsi_14',)) + + ohlcv_df.ta.macd(close='close', append=True, col_names=('macd', 'macdh', 'macds')) # Adiciona 3 colunas + ohlcv_df.ta.atr(length=14, append=True, col_names=('atr',)) # Adiciona ATR + ohlcv_df.ta.bbands(length=20, close='close', append=True, col_names=('bbl', 'bbm', 'bbu', 'bbb', 'bbp')) # Adiciona 5 colunas de Bollinger + + + + + # Após o cálculo de todos os indicadores e ohlcv_df.dropna() + ohlcv_df['close_div_atr'] = ohlcv_df['close'] / (ohlcv_df['atr'] + 1e-7) # Adiciona epsilon para evitar divisão por zero + ohlcv_df['volume_div_atr'] = ohlcv_df['volume'] / (ohlcv_df['atr'] + 1e-7) # Volume também pode ser normalizado se fizer sentido + # Faça o mesmo para outras features baseadas em preço se desejar (ex: sma_10, macd, bbm) + ohlcv_df['sma_10_div_atr'] = ohlcv_df['sma_10'] / (ohlcv_df['atr'] + 1e-7) + ohlcv_df['macd_div_atr'] = ohlcv_df['macd'] / (ohlcv_df['atr'] + 1e-7) + + + ohlcv_df.dropna(inplace=True) # Remove NaNs dos indicadores + + # --- 3. Criar Alvo (Target) --- + print("Criando coluna alvo para predição...") + ohlcv_df_with_target = calculate_targets(ohlcv_df.copy(), PREDICTION_HORIZON, PRICE_CHANGE_THRESHOLD) + + + + #Logica dos Scalers + api_price_vol_cols = ['close_div_atr', 'volume_div_atr'] + if all(col in ohlcv_df_with_target.columns for col in api_price_vol_cols): + price_volume_scaler_to_save = MinMaxScaler() + price_volume_scaler_to_save.fit(ohlcv_df_with_target[api_price_vol_cols]) + joblib.dump(price_volume_scaler_to_save, os.path.join(MODEL_SAVE_DIR, PRICE_VOL_SCALER_NAME)) + print(f"Scaler de Preço/Volume (API: {api_price_vol_cols}) salvo em: {os.path.join(MODEL_SAVE_DIR, PRICE_VOL_SCALER_NAME)}") + else: + print(f"ERRO: Colunas para salvar price_volume_scaler ({api_price_vol_cols}) não encontradas em ohlcv_df_with_target.") + return # Falha crítica se não puder salvar scalers esperados + + # Colunas que representam "indicadores" para a API + # Estas são as colunas de BASE_FEATURE_COLS que NÃO são 'close_div_atr' ou 'volume_div_atr' + # E que o rnn_predictor.py também vai escalar com o indicator_scaler. + # ATENÇÃO: `BASE_FEATURE_COLS` deve ser a lista FINAL de features que entram no modelo, + # então `indicator_cols_for_api_scaler` deve pegar as colunas de INDICADORES dessa lista. + + # Supondo que BASE_FEATURE_COLS atualizada é: + BASE_FEATURE_COLS = ['close_div_atr', 'volume_div_atr', 'sma_10_div_atr', 'rsi_14', 'macd_div_atr', 'atr', 'bbp'] + indicator_cols_for_api_scaler = ['sma_10_div_atr', 'rsi_14', 'macd_div_atr', 'atr', 'bbp'] + + + if indicator_cols_for_api_scaler and all(col in ohlcv_df_with_target.columns for col in indicator_cols_for_api_scaler): + indicator_data_to_fit_api_scaler = ohlcv_df_with_target[indicator_cols_for_api_scaler].copy() + indicator_data_to_fit_api_scaler.dropna(subset=indicator_cols_for_api_scaler, inplace=True) + + if not indicator_data_to_fit_api_scaler.empty: + indicator_scaler_to_save = MinMaxScaler() + indicator_scaler_to_save.fit(indicator_data_to_fit_api_scaler) + joblib.dump(indicator_scaler_to_save, os.path.join(MODEL_SAVE_DIR, INDICATOR_SCALER_NAME)) + print(f"Scaler de Indicadores (API: {indicator_cols_for_api_scaler}) salvo em: {os.path.join(MODEL_SAVE_DIR, INDICATOR_SCALER_NAME)}") + else: + print(f"ERRO: Não foi possível treinar/salvar o scaler de indicadores (API: {indicator_cols_for_api_scaler}) devido a dados vazios.") + return # Falha crítica + elif not indicator_cols_for_api_scaler: + print("Nenhuma coluna de indicador definida para salvar o indicator_scaler para API.") + else: + missing_cols = [col for col in indicator_cols_for_api_scaler if col not in ohlcv_df_with_target.columns] + print(f"ERRO: Colunas para salvar indicator_scaler ({indicator_cols_for_api_scaler}) não encontradas ou lista vazia. Ausentes: {missing_cols}") + return # Falha crítica + + + + #Fimfitagem + + # --- 4. Escalonar TODAS as Features (PARA TREINAMENTO DO MODELO ATUAL) --- + print("Escalonando features para criação de sequências (usando as colunas de BASE_FEATURE_COLS)...") + + # Garantir que ohlcv_df_with_target contenha todas as BASE_FEATURE_COLS + if not all(col in ohlcv_df_with_target.columns for col in BASE_FEATURE_COLS): + missing_final_base_cols = [col for col in BASE_FEATURE_COLS if col not in ohlcv_df_with_target.columns] + print(f"ERRO: Colunas finais em BASE_FEATURE_COLS ausentes em ohlcv_df_with_target: {missing_final_base_cols}") + print(f"Colunas disponíveis: {ohlcv_df_with_target.columns.tolist()}") + return + + features_to_scale_df = ohlcv_df_with_target[BASE_FEATURE_COLS].copy() + + # Este scaler 'geral' é usado para preparar os dados para as sequências. + general_training_scaler = MinMaxScaler() + scaled_features_values = general_training_scaler.fit_transform(features_to_scale_df) # Fita e transforma TODAS as BASE_FEATURE_COLS juntas + + scaled_features_df = pd.DataFrame(scaled_features_values, + columns=[f"{col}_scaled" for col in BASE_FEATURE_COLS], + index=features_to_scale_df.index) + + + + # --- + + if ohlcv_df_with_target.empty: + print("DataFrame vazio após cálculo do alvo. Verifique os parâmetros de horizonte/threshold. Encerrando.") + return + + print(f"Distribuição do Alvo:\n{ohlcv_df_with_target['target'].value_counts(normalize=True)}") + pv_cols_for_scaling = ['close_div_atr', 'volume_div_atr'] + if all(col in ohlcv_df_with_target.columns for col in pv_cols_for_scaling): # Verifica se existem + price_volume_scaler = MinMaxScaler() + price_volume_data_original = ohlcv_df_with_target[pv_cols_for_scaling].copy() + price_volume_scaler.fit(price_volume_data_original) + joblib.dump(price_volume_scaler, os.path.join(MODEL_SAVE_DIR, PRICE_VOL_SCALER_NAME)) + print(f"Scaler de Preço/Volume salvo em: {os.path.join(MODEL_SAVE_DIR, PRICE_VOL_SCALER_NAME)}") + else: + print(f"Aviso: Colunas para price_volume_scaler não encontradas: {pv_cols_for_scaling}") + + # Scaler para TODOS os outros indicadores definidos em BASE_FEATURE_COLS + indicator_cols_for_scaling = [col for col in BASE_FEATURE_COLS if col not in ['close_div_atr', 'volume_div_atr']] + + if indicator_cols_for_scaling and all(col in ohlcv_df_with_target.columns for col in indicator_cols_for_scaling): + indicator_data_original = ohlcv_df_with_target[indicator_cols_for_scaling].copy() + indicator_data_original.dropna(inplace=True) # Importante, pois alguns indicadores podem ter mais NaNs no início + + if not indicator_data_original.empty: + indicator_scaler_instance = MinMaxScaler() + indicator_scaler_instance.fit(indicator_data_original) + joblib.dump(indicator_scaler_instance, os.path.join(MODEL_SAVE_DIR, INDICATOR_SCALER_NAME)) + print(f"Scaler de Indicadores ({indicator_cols_for_scaling}) salvo em: {os.path.join(MODEL_SAVE_DIR, INDICATOR_SCALER_NAME)}") + else: + print(f"Não foi possível treinar/salvar o scaler de indicadores ({indicator_cols_for_scaling}) devido a dados vazios após dropna.") + elif not indicator_cols_for_scaling: + print("Nenhuma coluna de indicador definida para o indicator_scaler.") + else: + print(f"Aviso: Algumas colunas para indicator_scaler não encontradas: {indicator_cols_for_scaling}") + + # Verificar se as colunas base existem após os indicadores + missing_base_cols = [col for col in BASE_FEATURE_COLS if col not in ohlcv_df.columns] + if missing_base_cols: + print(f"Erro: Colunas base para features ausentes após cálculo de indicadores: {missing_base_cols}") + print(f"Colunas disponíveis: {ohlcv_df.columns.tolist()}") + print(f"Verifique BASE_FEATURE_COLS e a saída dos indicadores.") + return + + + # --- 4. Escalonar Features --- + print("Escalonando features...") + # Separar features que serão escaladas + features_to_scale_df = ohlcv_df_with_target[BASE_FEATURE_COLS].copy() + + # IMPORTANTE: Escalar ANTES de criar as sequências + # E escalar APENAS as features, não o target. + + # Exemplo com um único scaler para todas as features (mais simples de gerenciar) + # Se você tiver scalers separados (como no rnn_predictor.py), precisará adaptar. + scaler = MinMaxScaler() + scaled_features_values = scaler.fit_transform(features_to_scale_df) + + # Criar um DataFrame com as features escaladas para facilitar a criação de sequências + scaled_features_df = pd.DataFrame(scaled_features_values, + columns=[f"{col}_scaled" for col in BASE_FEATURE_COLS], + index=features_to_scale_df.index) + + # Juntar o target de volta com as features escaladas + # Assegurar que os índices estão alinhados para o join + data_for_sequences = scaled_features_df.join(ohlcv_df_with_target[['target']]) + data_for_sequences.dropna(inplace=True) # Caso o join crie NaNs (improvável se os índices estiverem corretos) + + if data_for_sequences.empty: + print("DataFrame vazio após escalonamento e join com target. Encerrando.") + return + + # As colunas de features para criar sequências são as escaladas + sequence_feature_cols = [f"{col}_scaled" for col in BASE_FEATURE_COLS] + + # --- 5. Criar Sequências --- + print("Criando sequências de dados...") + X, y = create_sequences(data_for_sequences, 'target', WINDOW_SIZE, sequence_feature_cols) + + if X.shape[0] == 0: + print("Nenhuma sequência foi criada. Verifique o tamanho dos dados e WINDOW_SIZE. Encerrando.") + return + print(f"Shape de X (sequências de entrada): {X.shape}") # (num_samples, window_size, num_features) + print(f"Shape de y (alvos): {y.shape}") # (num_samples,) + + # --- 6. Dividir em Treino e Teste --- + print("Dividindo dados em treino e teste...") + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y) + print(f"Tamanho do conjunto de treino: {X_train.shape[0]}, Teste: {X_test.shape[0]}") + + # --- 7. Construir e Treinar Modelo --- + print("Construindo modelo LSTM...") + # O input_shape para o modelo é (WINDOW_SIZE, NUM_FEATURES) + model = build_lstm_model((WINDOW_SIZE, NUM_FEATURES), LSTM_UNITS, DENSE_UNITS, DROPOUT_RATE, LEARNING_RATE) + + print("Iniciando treinamento do modelo...") + # Adicionar callbacks (opcional, mas recomendado para treinamento longo) + reduce_lr_callback = tf.keras.callbacks.ReduceLROnPlateau( + monitor='val_loss', + factor=0.2, # Reduz LR em 80% (multiplica por 0.2) + patience=7, # Paciência um pouco menor que o EarlyStopping + min_lr=1e-7, # Não reduzir abaixo disso + verbose=1 + ) + early_stopping_callback = tf.keras.callbacks.EarlyStopping( + monitor='val_loss', + patience=25, + restore_best_weights=True + ) + callbacks = [early_stopping_callback, reduce_lr_callback] + + from sklearn.utils.class_weight import compute_class_weight + unique_classes_in_train = np.unique(y_train) + class_weights_values = compute_class_weight('balanced', classes=unique_classes_in_train, y=y_train) + class_weights = {cls: weight for cls, weight in zip(unique_classes_in_train, class_weights_values)} + print(f"Pesos de Classe para Treinamento: {class_weights}") + + history = model.fit(X_train, y_train, + epochs=EPOCHS, + batch_size=BATCH_SIZE, + validation_data=(X_test, y_test), + callbacks=callbacks, + class_weight=class_weights, + verbose=1) + + # --- 8. Avaliar Modelo --- + print("Avaliando modelo no conjunto de teste...") + loss, accuracy = model.evaluate(X_test, y_test, verbose=0) + print(f"Perda no Teste: {loss:.4f}") + print(f"Acurácia no Teste: {accuracy:.4f}") + + # Após model.evaluate() + from sklearn.metrics import classification_report, confusion_matrix + y_pred_probs = model.predict(X_test) + y_pred_classes = (y_pred_probs > 0.65).astype(int) # 0.5, 0.6, 0.65, 0.7, 0.75 Valores de referencia par Thresholud + + print("\nRelatório de Classificação no Conjunto de Teste:") + print(classification_report(y_test, y_pred_classes, target_names=['No Rise (0)', 'Rise (1)'])) + + print("\nMatriz de Confusão no Conjunto de Teste:") + cm = confusion_matrix(y_test, y_pred_classes) + print(cm) + # import seaborn as sns # Para um plot mais bonito da matriz + # plt.figure(figsize=(6,5)) + # sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", xticklabels=['No Rise', 'Rise'], yticklabels=['No Rise', 'Rise']) + # plt.xlabel('Predito') + # plt.ylabel('Verdadeiro') + # plt.title('Matriz de Confusão') + # plt.savefig(os.path.join(MODEL_SAVE_DIR, "confusion_matrix.png")) + + # --- 9. Salvar Modelo e Scalers --- + print("Salvando modelo e scalers...") + os.makedirs(MODEL_SAVE_DIR, exist_ok=True) # Cria o diretório se não existir + + model_path = os.path.join(MODEL_SAVE_DIR, MODEL_NAME) + model.save(model_path) + print(f"Modelo salvo em: {model_path}") + + # Salvar o scaler usado para TODAS as features + # Se você usou scalers separados em rnn_predictor.py (price_vol_scaler, indicator_scaler), + # você precisará treinar e salvar esses scalers separadamente aqui. + # Por agora, este script usa um único scaler para `BASE_FEATURE_COLS`. + # VOCÊ PRECISA ADAPTAR ESTA PARTE PARA CORRESPONDER AO SEU `rnn_predictor.py` + + # Se rnn_predictor.py espera PRICE_VOL_SCALER_PATH e INDICATOR_SCALER_PATH: + # Você precisaria criar dois DataFrames aqui: um com 'close', 'volume' e outro com 'sma_10', 'rsi_14' + # Treinar um scaler para cada e salvá-los. + + # Exemplo para salvar price_volume_scaler: + pv_cols_for_scaling = ['volume_div_atr', 'volume_div_atr'] # Colunas originais ANTES de escalar + pv_data_to_fit_scaler = ohlcv_df_with_target[pv_cols_for_scaling].copy() # Usa o DF antes de criar sequências + + # É importante usar o mesmo scaler que foi usado para criar `scaled_features_df` + # Se você fez `scaler.fit_transform(features_to_scale_df)` onde `features_to_scale_df` + # continha todas as `BASE_FEATURE_COLS`, então esse `scaler` é o que foi treinado em todas elas. + # Para salvar scalers separados como esperado por rnn_predictor.py, você precisa de: + + # Scaler para Preço e Volume + price_volume_scaler = MinMaxScaler() + price_volume_data_original = ohlcv_df_with_target[['close_div_atr', 'volume_div_atr']].copy() # Dados originais + price_volume_scaler.fit(price_volume_data_original) # Fita nos dados originais + joblib.dump(price_volume_scaler, os.path.join(MODEL_SAVE_DIR, PRICE_VOL_SCALER_NAME)) + print(f"Scaler de Preço/Volume salvo em: {os.path.join(MODEL_SAVE_DIR, PRICE_VOL_SCALER_NAME)}") + + # Scaler para Indicadores + indicator_cols_for_scaling = ['sma_10', 'rsi_14'] # Colunas originais dos indicadores + indicator_data_original = ohlcv_df_with_target[indicator_cols_for_scaling].copy() + indicator_data_original.dropna(inplace=True) # Garante que não há NaNs antes de fitar + if not indicator_data_original.empty: + indicator_scaler_instance = MinMaxScaler() + indicator_scaler_instance.fit(indicator_data_original) + joblib.dump(indicator_scaler_instance, os.path.join(MODEL_SAVE_DIR, INDICATOR_SCALER_NAME)) + print(f"Scaler de Indicadores salvo em: {os.path.join(MODEL_SAVE_DIR, INDICATOR_SCALER_NAME)}") + else: + print("Não foi possível treinar/salvar o scaler de indicadores devido a dados vazios.") + + + # --- 10. Plotar Histórico de Treinamento (Opcional) --- + plt.figure(figsize=(12, 6)) + plt.subplot(1, 2, 1) + plt.plot(history.history['accuracy'], label='Acurácia Treino') + plt.plot(history.history['val_accuracy'], label='Acurácia Validação') + plt.title('Acurácia do Modelo') + plt.xlabel('Época') + plt.ylabel('Acurácia') + plt.legend() + + plt.subplot(1, 2, 2) + plt.plot(history.history['loss'], label='Perda Treino') + plt.plot(history.history['val_loss'], label='Perda Validação') + plt.title('Perda do Modelo') + plt.xlabel('Época') + plt.ylabel('Perda') + plt.legend() + + # Salvar o gráfico + plot_path = os.path.join(MODEL_SAVE_DIR, "training_history.png") + plt.savefig(plot_path) + print(f"Gráfico do histórico de treinamento salvo em: {plot_path}") + # plt.show() # Descomente se quiser ver o gráfico imediatamente + + print("Script de treinamento concluído.") + + # No final do script de treino + y_pred_probs = model.predict(X_test) + thresholds_to_test = [0.50, 0.55, 0.60, 0.65, 0.70, 0.75] + for thresh in thresholds_to_test: + print(f"\n--- Resultados com Threshold: {thresh:.2f} ---") + y_pred_classes = (y_pred_probs > thresh).astype(int) + print(classification_report(y_test, y_pred_classes, target_names=['No Rise (0)', 'Rise (1)'], zero_division=0)) + print(confusion_matrix(y_test, y_pred_classes)) + + +if __name__ == '__main__': + # Configurar GPU (Opcional, se tiver TensorFlow com suporte a GPU) + gpus = tf.config.experimental.list_physical_devices('GPU') + if gpus: + try: + for gpu in gpus: + tf.config.experimental.set_memory_growth(gpu, True) + logical_gpus = tf.config.experimental.list_logical_devices('GPU') + print(len(gpus), "Physical GPUs,", len(logical_gpus), "Logical GPUs") + except RuntimeError as e: + print(e) + + + + + + + + main() \ No newline at end of file diff --git a/scripts/train_rl_portfolio_agent.py b/scripts/train_rl_portfolio_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..57d9353f49aac05c34da609507077a6717c02052 --- /dev/null +++ b/scripts/train_rl_portfolio_agent.py @@ -0,0 +1,82 @@ +# train_rl_portfolio_agent.py +from stable_baselines3 import PPO +from stable_baselines3.common.env_checker import check_env + +from data_handler_multi_asset import get_multi_asset_data_for_rl, MULTI_ASSET_SYMBOLS # Do seu config/data_handler +from rnn.agents.portfolio_environment import PortfolioEnv # Seu ambiente +from models.deep_portfolio import DeepPortfolioAI # Seu modelo (usado como policy) +# from config import ... # Outras configs + +# 1. Carregar e preparar dados multi-ativos +# (MULTI_ASSET_SYMBOLS viria do config.py) +asset_keys_list = list(MULTI_ASSET_SYMBOLS.keys()) # ['crypto_eth', 'crypto_ada', ...] + +multi_asset_df = get_multi_asset_data_for_rl( + MULTI_ASSET_SYMBOLS, + timeframe_yf='1h', # Ou TIMEFRAME_YFINANCE do config + days_to_fetch=365*2 # Ou DAYS_TO_FETCH do config +) + +if multi_asset_df is None or multi_asset_df.empty: + print("Falha ao carregar dados multi-ativos. Encerrando treinamento RL.") + exit() + +# 2. Criar o Ambiente +# O multi_asset_df já deve ter as features para observação E as colunas de preço de close original +env = PortfolioEnv(df_multi_asset_features=multi_asset_df, asset_symbols_list=asset_keys_list) + +# Opcional: Verificar se o ambiente está em conformidade com a API do Gymnasium +# check_env(env) # Pode dar avisos/erros se algo estiver errado +print("Ambiente de Portfólio Criado.") +print(f"Observation Space: {env.observation_space.shape}") +print(f"Action Space: {env.action_space.shape}") + +# 3. Definir a Política de Rede Neural +# Stable-Baselines3 permite que você defina uma arquitetura customizada. +# Precisamos de uma forma de passar sua arquitetura DeepPortfolioAI para o PPO. +# Uma maneira é criar uma classe de política customizada. +# Por agora, vamos usar a política padrão "MlpPolicy" e depois vemos como integrar a sua. +# Ou, se DeepPortfolioAI for uma tf.keras.Model, podemos tentar usá-la em policy_kwargs. + +# Para usar sua DeepPortfolioAI, você precisaria de uma FeatureExtractor customizada +# ou uma política que a incorpore, o que é mais avançado com Stable-Baselines3. +# Vamos começar com MlpPolicy para testar o ambiente. + +# policy_kwargs = dict( +# features_extractor_class=YourCustomFeatureExtractor, # Se a entrada precisar de tratamento especial +# features_extractor_kwargs=dict(features_dim=128), +# net_arch=[dict(pi=[256, 128], vf=[256, 128])] # Exemplo de arquitetura para policy e value networks +# ) +# Ou, se o DeepPortfolioAI puder ser adaptado para ser a policy_network: +# policy_kwargs = dict( +# net_arch=dict( +# pi=[{'model': DeepPortfolioAI(num_assets=env.num_assets)}], # Não é direto assim +# vf=[] # Value function pode ser separada ou compartilhada +# ) +# ) + +# Para começar e testar o ambiente, use a MlpPolicy padrão. +# O input da MlpPolicy será a observação achatada (WINDOW_SIZE * num_total_features). +# Isso pode não ser ideal para dados sequenciais. "MlpLstmPolicy" é melhor. + +model_ppo = PPO("MlpLstmPolicy", env, verbose=1, tensorboard_log="./ppo_portfolio_tensorboard/") +# Se "MlpLstmPolicy" não funcionar bem com o shape da observação (janela, features_totais), +# você pode precisar de um FeatureExtractor que achate a janela, ou uma política customizada. + +# 4. Treinar o Agente +print("Iniciando treinamento do agente PPO...") +model_ppo.learn(total_timesteps=50000, progress_bar=True) # Aumente timesteps para treino real + +# 5. Salvar o Modelo Treinado +model_ppo.save("rl_models/ppo_deep_portfolio_agent") +print("Modelo RL treinado salvo.") + +# (Opcional) Testar o agente treinado +obs, _ = env.reset() +for _ in range(200): + action, _states = model_ppo.predict(obs, deterministic=True) + obs, rewards, terminated, truncated, info = env.step(action) + env.render() + if terminated or truncated: + obs, _ = env.reset() +env.close() \ No newline at end of file diff --git a/scripts/train_rnn_model.py b/scripts/train_rnn_model.py new file mode 100644 index 0000000000000000000000000000000000000000..3c5e5baf3837f926b38fc2dc73b4e05a48297772 --- /dev/null +++ b/scripts/train_rnn_model.py @@ -0,0 +1,382 @@ +# train_rnn_model.py + +import os +import numpy as np +import pandas as pd +import tensorflow as tf +from sklearn.model_selection import train_test_split +from sklearn.preprocessing import MinMaxScaler +import joblib +import matplotlib.pyplot as plt +from datetime import timezone # Adicionado para datetime.now(timezone.utc) + +# Importar configurações e módulos +from config import ( + NUM_FEATURES, SYMBOL, TIMEFRAME, DAYS_OF_DATA_TO_FETCH, LIMIT_PER_FETCH, + WINDOW_SIZE, BASE_FEATURE_COLS, # NUM_FEATURES é derivado no model_builder + PREDICTION_HORIZON, PRICE_CHANGE_THRESHOLD, + EPOCHS, BATCH_SIZE, # LSTM_UNITS, DENSE_UNITS, etc., são usados em model_builder + MODEL_SAVE_DIR, MODEL_NAME, + PRICE_VOL_SCALER_NAME, INDICATOR_SCALER_NAME, PRICE_VOL_SCALER_NAME, INDICATOR_SCALER_NAME, # Nomes dos arquivos + EXPECTED_SCALED_FEATURES_FOR_MODEL, EXPECTED_FEATURES_ORDER # Usaremos esta para as colunas de entrada das sequências + +) +# Note: LEARNING_RATE é usado em model_builder, não precisa importar aqui diretamente se não for para os callbacks. + +from data_handler import fetch_ohlcv_data_ccxt, calculate_technical_indicators, calculate_targets, create_sequences +from model_builder import build_lstm_model + +# Para métricas de classificação +from sklearn.metrics import classification_report, confusion_matrix +from sklearn.utils.class_weight import compute_class_weight + + +def main(): + print("Iniciando script de treinamento da RNN...") + os.makedirs(MODEL_SAVE_DIR, exist_ok=True) + + # --- 1. Obter e Pré-processar Dados --- + try: + ohlcv_df_raw = fetch_ohlcv_data_ccxt(SYMBOL, TIMEFRAME, DAYS_OF_DATA_TO_FETCH, LIMIT_PER_FETCH) + except Exception as e: + print(f"Falha ao buscar dados com CCXT: {e}") + # ... (lógica de fallback para CSV como antes) ... + return + + if ohlcv_df_raw.empty: print("DataFrame de dados raw está vazio."); return + + ohlcv_df_with_ta = calculate_technical_indicators(ohlcv_df_raw) + if ohlcv_df_with_ta.empty: print("DataFrame vazio após cálculo de indicadores."); return + + ohlcv_df_final_features = calculate_targets(ohlcv_df_with_ta, PREDICTION_HORIZON, PRICE_CHANGE_THRESHOLD) + if ohlcv_df_final_features.empty: print("DataFrame vazio após cálculo de alvos."); return + + # --- 2. Salvar Scalers para API (Fitados nas Colunas Base) --- + + api_price_vol_atr_cols = [ # Features que já estão normalizadas ou são valores de preço/vol normalizados + 'open_div_atr', 'high_div_atr', 'low_div_atr', 'close_div_atr', + 'volume_div_atr', 'body_size_norm_atr' + ] + # Garante que só pegamos o que está em BASE_FEATURE_COLS + api_price_vol_cols = [col for col in api_price_vol_atr_cols if col in BASE_FEATURE_COLS] + + api_indicator_cols = [ # Features que são indicadores brutos ou taxas + 'log_return_1', 'rsi_14', 'atr', 'bbp', 'cci_37', 'mfi_37', + 'body_vs_avg_body', 'macd', 'sma_10_div_atr' + ] + + # Garante que só pegamos o que está em BASE_FEATURE_COLS e não está no outro grupo + api_indicator_cols = [col for col in api_indicator_cols if col in BASE_FEATURE_COLS and col not in api_price_vol_cols] + + print("Preparando e salvando scalers para a API...") + + # Garantir que todas as colunas de BASE_FEATURE_COLS existem ANTES de tentar fitar os scalers + missing_base_for_api_scalers = [col for col in BASE_FEATURE_COLS if col not in ohlcv_df_final_features.columns] + if missing_base_for_api_scalers: + print(f"ERRO: Colunas de BASE_FEATURE_COLS ({missing_base_for_api_scalers}) não encontradas para fitar scalers da API.") + print(f"Disponíveis: {ohlcv_df_final_features.columns.tolist()}") + return + + api_price_vol_cols = [col for col in BASE_FEATURE_COLS if 'close_div_atr' in col or 'volume_div_atr' in col] + api_indicator_cols = [col for col in BASE_FEATURE_COLS if col not in api_price_vol_cols] + + if api_price_vol_cols: + price_volume_scaler_api = MinMaxScaler() + price_volume_scaler_api.fit(ohlcv_df_final_features[api_price_vol_cols]) + joblib.dump(price_volume_scaler_api, os.path.join(MODEL_SAVE_DIR, PRICE_VOL_SCALER_NAME)) + print(f"Scaler de Preço/Volume (API: {api_price_vol_cols}) salvo.") + else: + print("Aviso: Nenhuma coluna de preço/volume definida para o scaler da API.") + + if api_indicator_cols: + indicator_scaler_api = MinMaxScaler() + indicator_scaler_api.fit(ohlcv_df_final_features[api_indicator_cols]) + joblib.dump(indicator_scaler_api, os.path.join(MODEL_SAVE_DIR, INDICATOR_SCALER_NAME)) + print(f"Scaler de Indicadores (API: {api_indicator_cols}) salvo.") + else: + print("Aviso: Nenhuma coluna de indicador definida para o scaler da API.") + + # --- 3. Escalonar TODAS as Features para Treinamento do Modelo Atual --- + # Este scaler é apenas para o processo de treinamento aqui. + # Ele é treinado em TODAS as BASE_FEATURE_COLS juntas. + print(f"Escalonando features para treinamento (colunas: {BASE_FEATURE_COLS})...") + training_features_df = ohlcv_df_final_features[BASE_FEATURE_COLS].copy() + + general_training_scaler = MinMaxScaler() + scaled_values_for_training = general_training_scaler.fit_transform(training_features_df) + + # DataFrame com colunas escaladas, ex: 'close_div_atr_scaled', 'rsi_14_scaled' + # Usa EXPECTED_SCALED_FEATURES_FOR_MODEL do config.py + df_scaled_for_sequences = pd.DataFrame( + scaled_values_for_training, + columns=EXPECTED_SCALED_FEATURES_FOR_MODEL, + index=training_features_df.index + ) + + # Juntar o target de volta + df_for_sequences = df_scaled_for_sequences.join(ohlcv_df_final_features[['target']]) + df_for_sequences.dropna(inplace=True) # Se o join criar NaNs (improvável) + if df_for_sequences.empty: print("DataFrame para sequências vazio após escalonamento/join."); return + + # --- 4. Criar Sequências --- + # As colunas para as sequências são as EXPECTED_SCALED_FEATURES_FOR_MODEL + X, y = create_sequences(df_for_sequences, "target", WINDOW_SIZE, EXPECTED_SCALED_FEATURES_FOR_MODEL) + if X.shape[0] == 0: print("Nenhuma sequência criada."); return + + print("Preparando, fitando e salvando scalers...") + os.makedirs(MODEL_SAVE_DIR, exist_ok=True) + + # Garantir que todas as colunas de BASE_FEATURE_COLS existem em ohlcv_df_final_features + missing_base_cols = [col for col in BASE_FEATURE_COLS if col not in ohlcv_df_final_features.columns] + if missing_base_cols: + print(f"ERRO FATAL: Colunas de BASE_FEATURE_COLS ({missing_base_cols}) não encontradas em ohlcv_df_final_features.") + return + + # Definir quais colunas de BASE_FEATURE_COLS vão para cada scaler + # Estas são as colunas que representam valores normalizados pelo ATR (preço, volume, corpo) + price_vol_atr_norm_cols = [ + 'open_div_atr', 'high_div_atr', 'low_div_atr', 'close_div_atr', + 'volume_div_atr', 'body_size_norm_atr' + ] + # Remover colunas que podem não existir se você não as adicionou a BASE_FEATURE_COLS + price_vol_atr_norm_cols = [col for col in price_vol_atr_norm_cols if col in BASE_FEATURE_COLS] + + + # As colunas restantes de BASE_FEATURE_COLS são os "outros indicadores" + other_indicator_cols = [col for col in BASE_FEATURE_COLS if col not in price_vol_atr_norm_cols] + + # DataFrames para fitar os scalers + df_for_pv_scaler = ohlcv_df_final_features[price_vol_atr_norm_cols].copy() + df_for_ind_scaler = ohlcv_df_final_features[other_indicator_cols].copy() + + # Fitar e Salvar o Price/Volume (ATR Normalized) Scaler + if not df_for_pv_scaler.empty: + pv_atr_scaler = MinMaxScaler() + pv_atr_scaler.fit(df_for_pv_scaler) + joblib.dump(pv_atr_scaler, os.path.join(MODEL_SAVE_DIR, PRICE_VOL_SCALER_NAME)) + print(f"Scaler de Preço/Volume (ATR Norm) (API: {price_vol_atr_norm_cols}) salvo.") + # Transformar os dados para o treinamento com este scaler + scaled_pv_data = pv_atr_scaler.transform(df_for_pv_scaler) + else: + print(f"AVISO: Sem dados para o scaler de Preço/Volume (ATR Norm) ({price_vol_atr_norm_cols}).") + scaled_pv_data = pd.DataFrame() # DataFrame vazio para evitar erro de concatenação + + # Fitar e Salvar o Other Indicators Scaler + if not df_for_ind_scaler.empty: + other_ind_scaler = MinMaxScaler() + other_ind_scaler.fit(df_for_ind_scaler) + joblib.dump(other_ind_scaler, os.path.join(MODEL_SAVE_DIR, INDICATOR_SCALER_NAME)) + print(f"Scaler de Outros Indicadores (API: {other_indicator_cols}) salvo.") + # Transformar os dados para o treinamento com este scaler + scaled_other_ind_data = other_ind_scaler.transform(df_for_ind_scaler) + else: + print(f"AVISO: Sem dados para o scaler de Outros Indicadores ({other_indicator_cols}).") + scaled_other_ind_data = pd.DataFrame() + + # --- Montar o DataFrame com TODAS as features escaladas para o treinamento --- + # As colunas devem estar na ORDEM DE EXPECTED_SCALED_FEATURES_FOR_MODEL + # (que é derivada da ordem de BASE_FEATURE_COLS) + + df_scaled_for_sequences = pd.DataFrame(index=ohlcv_df_final_features.index) + + # Adicionar colunas escaladas de preço/volume + if not df_for_pv_scaler.empty: + for i, col_name in enumerate(price_vol_atr_norm_cols): + df_scaled_for_sequences[f"{col_name}_scaled"] = scaled_pv_data[:, i] + + # Adicionar colunas escaladas de outros indicadores + if not df_for_ind_scaler.empty: + for i, col_name in enumerate(other_indicator_cols): + df_scaled_for_sequences[f"{col_name}_scaled"] = scaled_other_ind_data[:, i] + + # Verificar se todas as colunas escaladas esperadas foram criadas + # `EXPECTED_SCALED_FEATURES_FOR_MODEL` vem do config.py + missing_scaled_cols = [col for col in EXPECTED_SCALED_FEATURES_FOR_MODEL if col not in df_scaled_for_sequences.columns] + if missing_scaled_cols: + print(f"ERRO FATAL: Colunas escaladas esperadas ({missing_scaled_cols}) não foram criadas para as sequências.") + print(f"Colunas escaladas disponíveis: {df_scaled_for_sequences.columns.tolist()}") + return + + # Reordenar colunas para garantir a ordem de EXPECTED_SCALED_FEATURES_FOR_MODEL + df_scaled_for_sequences = df_scaled_for_sequences[EXPECTED_SCALED_FEATURES_FOR_MODEL] + + # Juntar o target de volta + df_for_sequences = df_scaled_for_sequences.join(ohlcv_df_final_features[['target']]) + df_for_sequences.dropna(inplace=True) + if df_for_sequences.empty: print("DataFrame para sequências vazio após escalonamento/join."); return + + # --- 5. Dividir Dados --- + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y) + print(f"Treino: {X_train.shape[0]} amostras, Teste: {X_test.shape[0]} amostras.") + + X, y = create_sequences(df_for_sequences, "target", WINDOW_SIZE, EXPECTED_SCALED_FEATURES_FOR_MODEL) + if X.shape[0] == 0: print("Nenhuma sequência criada."); return + + # --- 6. Dividir Dados --- + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y) + print(f"Treino: {X_train.shape[0]} amostras, Teste: {X_test.shape[0]} amostras.") + + # --- 7. Construir e Treinar Modelo --- + print("Construindo modelo LSTM...") + # NUM_FEATURES é importado de config.py e deve ser len(BASE_FEATURE_COLS) + # EXPECTED_SCALED_FEATURES_FOR_MODEL também é de config.py e é [f"{col}_scaled" for col in BASE_FEATURE_COLS] + # O número de colunas em X_train (X_train.shape[2]) DEVE ser igual a NUM_FEATURES. + # E NUM_FEATURES DEVE ser igual a len(EXPECTED_SCALED_FEATURES_FOR_MODEL). + + actual_num_features = NUM_FEATURES # Se estiverem consistentes + + model_input_shape = (WINDOW_SIZE, actual_num_features) + model = build_lstm_model(model_input_shape) # model_builder.py usa LEARNING_RATE do config + + print("Iniciando treinamento do modelo...") + reduce_lr_cb = tf.keras.callbacks.ReduceLROnPlateau( + monitor='val_loss', + factor=0.5, # Reduz pela metade, um pouco menos agressivo que 0.2 + patience=7, + min_lr=1e-7, # Pode ser 1e-6 também + verbose=1 + ) + early_stopping_cb = tf.keras.callbacks.EarlyStopping( + monitor='val_loss', + patience=25, # Mantém sua paciência maior + restore_best_weights=True # MUITO IMPORTANTE + ) + callbacks_list = [early_stopping_cb, reduce_lr_cb] + + class_weights_map = { + 0: 0.4, # Diminui o peso da classe majoritária + 1: 4.0 # Aumenta BASTANTE o peso da classe minoritária (Rise) + } + + unique_classes, counts = np.unique(y_train, return_counts=True) + print(f"Distribuição das classes no treino: {dict(zip(unique_classes, counts))}") + class_weights_vals = compute_class_weight('balanced', classes=unique_classes, y=y_train) + class_weights_map = {cls: weight for cls, weight in zip(unique_classes, class_weights_vals)} + + + + class_weights_map = {0: 0.5, 1: 3.5} + + + + print(f"Pesos de Classe: {class_weights_map}") + + """ class_weights_map = { + 0: 0.4, # Diminui o peso da classe majoritária + 1: 4.0 # Aumenta BASTANTE o peso da classe minoritária (Rise) + } """ + + + history = model.fit(X_train, y_train, epochs=EPOCHS, batch_size=BATCH_SIZE, + validation_split=0.1, # Usa 10% do X_train/y_train para validação + callbacks=callbacks_list, class_weight=class_weights_map, verbose=1) + + # --- 8. Avaliar o Modelo TREINADO --- + # O modelo 'model' aqui é o treinado, com os melhores pesos restaurados pelo EarlyStopping + print("Avaliando modelo treinado no conjunto de teste...") + loss, accuracy_keras = model.evaluate(X_test, y_test, verbose=0) + print(f"Perda no Teste (Keras): {loss:.4f}") + print(f"Acurácia no Teste (Keras, thr=0.5): {accuracy_keras:.4f}") + + + + # Após model.evaluate() + from sklearn.metrics import classification_report, confusion_matrix + y_pred_probs = model.predict(X_test) + y_pred_classes = (y_pred_probs > 0.65).astype(int) # 0.5, 0.6, 0.65, 0.7, 0.75 Valores de referencia par Thresholud + + print("\nRelatório de Classificação no Conjunto de Teste:") + print(classification_report(y_test, y_pred_classes, target_names=['No Rise (0)', 'Rise (1)'])) + + print("\nMatriz de Confusão no Conjunto de Teste:") + cm = confusion_matrix(y_test, y_pred_classes) + print(cm) + import seaborn as sns # Para um plot mais bonito da matriz + plt.figure(figsize=(6,5)) + sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", xticklabels=['No Rise', 'Rise'], yticklabels=['No Rise', 'Rise']) + plt.xlabel('Predito') + plt.ylabel('Verdadeiro') + plt.title('Matriz de Confusão') + plt.savefig(os.path.join(MODEL_SAVE_DIR, "confusion_matrix.png")) + + # --- 9. Salvar Modelo e Scalers --- + print("Salvando modelo e scalers...") + os.makedirs(MODEL_SAVE_DIR, exist_ok=True) # Cria o diretório se não existir + + model_path = os.path.join(MODEL_SAVE_DIR, MODEL_NAME) + model.save(model_path) + print(f"Modelo salvo em: {model_path}") + + # --- 9. Salvar o Modelo TREINADO --- + # Não reconstrua o modelo aqui! Salve o que foi treinado. + model_save_path = os.path.join(MODEL_SAVE_DIR, MODEL_NAME) + model.save(model_save_path) + print(f"Modelo TREINADO salvo em: {model_save_path}") + + + + # --- 10. Plotar Histórico de Treinamento (Opcional) --- + plt.figure(figsize=(12, 6)) + plt.subplot(1, 2, 1) + plt.plot(history.history['accuracy'], label='Acurácia Treino') + plt.plot(history.history['val_accuracy'], label='Acurácia Validação') + plt.title('Acurácia do Modelo') + plt.xlabel('Época') + plt.ylabel('Acurácia') + plt.legend() + + plt.subplot(1, 2, 2) + plt.plot(history.history['loss'], label='Perda Treino') + plt.plot(history.history['val_loss'], label='Perda Validação') + plt.title('Perda do Modelo') + plt.xlabel('Época') + plt.ylabel('Perda') + plt.legend() + + # Salvar o gráfico + plot_path = os.path.join(MODEL_SAVE_DIR, "training_history.png") + plt.savefig(plot_path) + print(f"Gráfico do histórico de treinamento salvo em: {plot_path}") + plt.show() # Descomente se quiser ver o gráfico imediatamente + + + # --- Imprimindo rodade de Tresh + + print("\nAnálise com diferentes thresholds de predição no conjunto de teste:") + y_pred_probs = model.predict(X_test) + thresholds_to_test = [0.50, 0.55, 0.60, 0.65, 0.70, 0.75] + for thresh in thresholds_to_test: + print(f"\n--- Resultados com Threshold: {thresh:.2f} ---") + y_pred_classes = (y_pred_probs > thresh).astype(int) + # Use target_names se suas classes forem 0 e 1. Se y_test tiver outros valores, ajuste. + # Supondo que 0 é 'No Rise' e 1 é 'Rise' + print(classification_report(y_test, y_pred_classes, target_names=['No Rise (0)', 'Rise (1)'], zero_division=0)) + print("Matriz de Confusão:") + print(confusion_matrix(y_test, y_pred_classes)) + + + + + + + + print("Script de treinamento concluído.") + + + +if __name__ == "__main__": + main() + + # Configurar GPU (Opcional, se tiver TensorFlow com suporte a GPU) + print("Iniciando configuração de GPU") + gpus = tf.config.experimental.list_physical_devices('GPU') + if gpus: + try: + for gpu in gpus: + tf.config.experimental.set_memory_growth(gpu, True) + logical_gpus = tf.config.experimental.list_logical_devices('GPU') + print(len(gpus), "Physical GPUs,", len(logical_gpus), "Logical GPUs") + except RuntimeError as e: + print(e) + + diff --git "a/sistema-de-gest\303\243o-de-ia-para-cr\303\251dito.mermaid" "b/sistema-de-gest\303\243o-de-ia-para-cr\303\251dito.mermaid" new file mode 100644 index 0000000000000000000000000000000000000000..f14f4670e2d5d49075a352e4d2457b9f7a570f32 --- /dev/null +++ "b/sistema-de-gest\303\243o-de-ia-para-cr\303\251dito.mermaid" @@ -0,0 +1,7 @@ +graph TD + A[Token Smart Contract] --> B[Oracle] + B --> C[IA Credit Scoring System] + C --> D[Automated Lending Pool] + D --> E[Risk Management Module] + E --> F[Governance Module] + F --> A \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000000000000000000000000000000000000..96f22fc6f5eaf119f1d2201a353ffedbaf1dd242 --- /dev/null +++ b/static/style.css @@ -0,0 +1,36 @@ +body { + font-family: 'Segoe UI', sans-serif; + background: #f0f2f5; + padding: 20px; + } + + .card { + background: white; + border-radius: 10px; + padding: 25px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + max-width: 1000px; + margin: auto; + } + + h1 { + text-align: center; + margin-bottom: 30px; + } + + table { + width: 100%; + border-collapse: collapse; + } + + th, td { + padding: 12px 15px; + border-bottom: 1px solid #ddd; + text-align: left; + } + + th { + background: #333; + color: white; + } + \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000000000000000000000000000000000000..916a92f4363e063192bbf06f4e2a9c3446114441 --- /dev/null +++ b/style.css @@ -0,0 +1,28 @@ +body { + padding: 2rem; + font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif; +} + +h1 { + font-size: 16px; + margin-top: 0; +} + +p { + color: rgb(107, 114, 128); + font-size: 15px; + margin-bottom: 10px; + margin-top: 5px; +} + +.card { + max-width: 620px; + margin: 0 auto; + padding: 16px; + border: 1px solid lightgray; + border-radius: 16px; +} + +.card p:last-child { + margin-bottom: 0; +} diff --git a/supervised_financial_model.py b/supervised_financial_model.py new file mode 100644 index 0000000000000000000000000000000000000000..638f0a78036f3b8791ac0e4bd8a5a1b115e99cb8 --- /dev/null +++ b/supervised_financial_model.py @@ -0,0 +1,121 @@ +import pandas as pd +import numpy as np +from sklearn.model_selection import train_test_split +from sklearn.preprocessing import StandardScaler +from sklearn.linear_model import LogisticRegression # Example model +from sklearn.metrics import accuracy_score, classification_report + +# (Placeholder for more sophisticated feature engineering) +def create_features(df: pd.DataFrame, target_lag: int = 1) -> pd.DataFrame: + """ + Creates basic features for financial time series. + - Lagged returns + - Target variable (e.g., price goes up or down) + """ + df_copy = df.copy() + df_copy['returns'] = df_copy['Close'].pct_change() + + # Simple target: 1 if next day's close is higher, 0 otherwise + df_copy['target'] = (df_copy['Close'].shift(-target_lag) > df_copy['Close']).astype(int) + + # Add more features: e.g., moving averages, RSI, MACD + df_copy['ma5'] = df_copy['Close'].rolling(window=5).mean() + df_copy['ma20'] = df_copy['Close'].rolling(window=20).mean() + + df_copy = df_copy.dropna() + return df_copy + +def preprocess_data_for_supervised( + df: pd.DataFrame, + features_list: list = ['returns', 'ma5', 'ma20'], + target_col: str = 'target', + test_size: float = 0.2, + random_state: int = 42 +): + """ + Prepares data for supervised learning. + - Selects features and target. + - Splits data into training and testing sets. + - Scales features. + """ + X = df[features_list] + y = df[target_col] + + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=random_state, shuffle=False) # Time series data, so no shuffle + + scaler = StandardScaler() + X_train_scaled = scaler.fit_transform(X_train) + X_test_scaled = scaler.transform(X_test) + + return X_train_scaled, X_test_scaled, y_train, y_test, scaler + +def train_supervised_model(X_train, y_train, model_type='logistic_regression', model_params=None): + """ + Trains a supervised learning model. + """ + if model_params is None: + model_params = {} + + if model_type == 'logistic_regression': + model = LogisticRegression(**model_params, random_state=42, max_iter=1000) # Added max_iter + # Add other model types here (e.g., SVM, RandomForest, GradientBoosting) + # elif model_type == 'random_forest': + # from sklearn.ensemble import RandomForestClassifier + # model = RandomForestClassifier(**model_params, random_state=42) + else: + raise ValueError(f"Unsupported model type: {model_type}") + + model.fit(X_train, y_train) + return model + +def evaluate_supervised_model(model, X_test, y_test): + """ + Evaluates the trained supervised model. + """ + predictions = model.predict(X_test) + accuracy = accuracy_score(y_test, predictions) + report = classification_report(y_test, predictions) + + print(f"Model Accuracy: {accuracy:.4f}") + print("Classification Report:") + print(report) + + return accuracy, report, predictions + +if __name__ == '__main__': + # This is an example of how to use the functions. + # You'll need to integrate this with your data fetching agent. + + # 1. Fetch data (using the financial_data_agent you modified) + # from agents.financial_data_agent import fetch_historical_ohlcv + # raw_data = fetch_historical_ohlcv("AAPL", period="1y", interval="1d") + + # For demonstration, creating a dummy DataFrame: + dates = pd.date_range(start='2023-01-01', periods=200, freq='B') + data = np.random.rand(200, 5) * 100 + 100 + raw_data = pd.DataFrame(data, index=dates, columns=['Open', 'High', 'Low', 'Close', 'Volume']) + raw_data['Close'] = raw_data['Close'] + np.sin(np.linspace(0, 10, 200)) * 10 # Add some trend + + if not raw_data.empty: + # 2. Create features + featured_data = create_features(raw_data) + + if not featured_data.empty: + # 3. Preprocess data + X_train_scaled, X_test_scaled, y_train, y_test, scaler = preprocess_data_for_supervised( + featured_data, + features_list=['returns', 'ma5', 'ma20'] # Ensure these features exist + ) + + # 4. Train model + print("Training supervised model...") + trained_model = train_supervised_model(X_train_scaled, y_train) + print("Model trained.") + + # 5. Evaluate model + print("\nEvaluating model...") + evaluate_supervised_model(trained_model, X_test_scaled, y_test) + else: + print("Featured data is empty. Check feature creation.") + else: + print("Raw data is empty. Check data fetching.") \ No newline at end of file diff --git a/templates/credit_dashboard.html b/templates/credit_dashboard.html new file mode 100644 index 0000000000000000000000000000000000000000..e867541d2fb15a4bb6dad14b8af05b9da354b38b --- /dev/null +++ b/templates/credit_dashboard.html @@ -0,0 +1,39 @@ + + + + + + Dashboard de Crédito — ATCoin Token + + + + +
+

Dashboard de Crédito

+ +
+

Workflow dos Agentes e Rede Neural

+
+graph TD + A[Agente de Coleta de Dados] --> B(Rede Neural); + B --> C[Agente de Análise de Risco]; + C --> D[Agente de Decisão de Crédito]; + D --> E[Resultado da Análise]; +
+
+ +
+

Transações de Investimento

+ +
+ +
+

Assistente de Crédito (Chatbot RAG)

+ +
+
+ + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000000000000000000000000000000000000..9bf7d527b8d7f04701c9178c4b36750bdc1144ae --- /dev/null +++ b/templates/index.html @@ -0,0 +1,64 @@ + + + + + + Dashboard — ATCoin Token + + + + +
+

Agentes da Inteligência Artificial — ATCoin Token

+ + + + + + + + + + + + {% for agente in agentes %} + + + + + + + + {% endfor %} + +
AgenteStatusÚltima AçãoPróxima ExecuçãoResultado
{{ agente.nome }}{{ agente.status }}{{ agente.ultima_acao }}{{ agente.proxima_execucao }}{{ agente.resultado }}
+
+
+

Dashboard de Crédito

+ +
+

Workflow dos Agentes e Rede Neural

+
+graph TD + A[Agente de Coleta de Dados] --> B(Rede Neural); + B --> C[Agente de Análise de Risco]; + C --> D[Agente de Decisão de Crédito]; + D --> E[Resultado da Análise]; +
+
+ +
+

Transações de Investimento

+ +
+ +
+

Assistente de Crédito (Chatbot RAG)

+ +
+
+ + + diff --git a/transfer_learning_model.py b/transfer_learning_model.py new file mode 100644 index 0000000000000000000000000000000000000000..59f5c71797e4a77391fbf6ea84690e0d602b25d3 --- /dev/null +++ b/transfer_learning_model.py @@ -0,0 +1,202 @@ +import pandas as pd +import numpy as np +from sklearn.preprocessing import MinMaxScaler +from tensorflow.keras.models import Sequential, Model +from tensorflow.keras.layers import LSTM, Dense, Dropout, Input +from tensorflow.keras.optimizers import Adam + +# (Helper functions for data preprocessing - can be imported or defined here) +# For simplicity, let's assume a similar preprocessing to deep_mql_model +def preprocess_data_for_transfer(df: pd.DataFrame, look_back: int = 60, features_cols=['Close'], target_col='Close'): + """ + Prepares data for a deep learning model, similar to deep_mql_model. + """ + df_copy = df.copy() + if 'returns' not in df_copy.columns and 'Close' in df_copy.columns: + df_copy['returns'] = df_copy['Close'].pct_change() + + # Example: ensure all specified columns exist, dropna if necessary + all_cols = list(set(features_cols + [target_col] + (['returns'] if 'returns' in df_copy.columns else []))) + df_copy = df_copy[all_cols].dropna() + + if df_copy.empty: + return np.array([]), np.array([]), None, [] + + data_to_scale = df_copy.values + + scaler = MinMaxScaler(feature_range=(0, 1)) + scaled_data = scaler.fit_transform(data_to_scale) + + target_idx_in_scaled = df_copy.columns.tolist().index(target_col) + + X, y = [], [] + for i in range(look_back, len(scaled_data)): + X.append(scaled_data[i-look_back:i]) + y.append(scaled_data[i, target_idx_in_scaled]) + + return np.array(X), np.array(y), scaler, df_copy.columns.tolist() + +def create_base_model(input_shape, base_model_type='lstm', units1=50, units2=50, dropout_rate=0.2): + """ + Creates a base model architecture for pre-training or as part of transfer learning. + """ + if base_model_type == 'lstm': + model = Sequential([ + LSTM(units1, return_sequences=True, input_shape=input_shape, name="base_lstm_1"), + Dropout(dropout_rate, name="base_dropout_1"), + LSTM(units2, return_sequences=False, name="base_lstm_2"), + Dropout(dropout_rate, name="base_dropout_2"), + Dense(25, activation='relu', name="base_dense_1") + ], name="base_model") + # Add other base model types like CNN here + # elif base_model_type == 'cnn': + # from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten + # model = Sequential([ + # Conv1D(64, 3, activation='relu', input_shape=input_shape, name="base_conv1d_1"), + # MaxPooling1D(2, name="base_maxpool_1"), + # Flatten(name="base_flatten"), + # Dense(50, activation='relu', name="base_dense_1") + # ], name="base_cnn_model") + else: + raise ValueError(f"Unsupported base_model_type: {base_model_type}") + + # The model is not compiled here as it might be part of a larger model or compiled later. + return model + +def adapt_model_for_transfer(base_model: Model, num_classes_new_task=1, learning_rate=0.001): + """ + Adapts a pre-trained base model for a new task. + - Freezes base model layers. + - Adds new classification/regression head. + - Compiles the new model. + """ + # Freeze the layers of the base model + base_model.trainable = False + + # Create new model on top + inputs = Input(shape=base_model.input_shape[1:]) # Get shape without batch size + x = base_model(inputs, training=False) # Pass training=False for frozen layers + # Add new layers for the specific task + x = Dense(128, activation='relu', name="transfer_dense_1")(x) + x = Dropout(0.3, name="transfer_dropout_1")(x) + outputs = Dense(num_classes_new_task, activation='linear' if num_classes_new_task == 1 else 'softmax', name="transfer_output")(x) + + adapted_model = Model(inputs, outputs, name="adapted_transfer_model") + + adapted_model.compile(optimizer=Adam(learning_rate=learning_rate), + loss='mean_squared_error' if num_classes_new_task == 1 else 'categorical_crossentropy', + metrics=['mean_absolute_error'] if num_classes_new_task == 1 else ['accuracy']) + return adapted_model + +def fine_tune_model(model: Model, X_train, y_train, X_val, y_val, unfreeze_at_layer_name=None, fine_tune_lr=1e-5, epochs=10, batch_size=32): + """ + Fine-tunes the model. + - Optionally unfreezes some layers of the base model. + - Re-compiles with a lower learning rate. + - Continues training. + """ + if unfreeze_at_layer_name: + model.trainable = True # Unfreeze the entire model first + # Then, selectively re-freeze layers *before* the unfreeze_at_layer_name + # This is a common strategy: unfreeze top layers of base model + for layer in model.get_layer('base_model').layers: # Assumes base_model is nested with this name + if layer.name == unfreeze_at_layer_name: + break + layer.trainable = False + else: # If no specific layer, keep base model frozen or unfreeze all (depending on previous state) + # For this example, let's assume we unfreeze the whole base_model if unfreeze_at_layer_name is None + # Or, more commonly, one might unfreeze the last few layers of the base model. + # If base_model is a sub-model: + if 'base_model' in [l.name for l in model.layers]: + model.get_layer('base_model').trainable = True # Unfreeze the whole base model part + print("Unfrozen all layers in 'base_model' for fine-tuning.") + + + model.compile(optimizer=Adam(learning_rate=fine_tune_lr), + loss=model.loss, # Use the same loss as before + metrics=model.metrics_names[1:]) # Use the same metrics + + print(f"Starting fine-tuning with learning rate: {fine_tune_lr}") + history = model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, validation_data=(X_val, y_val), verbose=1) + return model, history + +if __name__ == '__main__': + # --- Simulate Pre-training (on a 'large' general dataset) --- + print("Simulating pre-training of base model...") + # Dummy general dataset + dates_general = pd.date_range(start='2020-01-01', periods=1000, freq='B') + data_general_np = np.random.rand(1000, 1) * 100 + 50 # Single feature 'Close' + general_data = pd.DataFrame(data_general_np, index=dates_general, columns=['Close']) + general_data['Close'] = general_data['Close'] + np.sin(np.linspace(0, 50, 1000)) * 30 + + look_back_tl = 60 + X_general, y_general, scaler_general, _ = preprocess_data_for_transfer(general_data, look_back=look_back_tl) + + if X_general.shape[0] > 0: + base_model_input_shape = (X_general.shape[1], X_general.shape[2]) + base_model_tl = create_base_model(base_model_input_shape) + + # Compile for pre-training (if it were standalone) + base_model_tl.compile(optimizer=Adam(0.001), loss='mean_squared_error') + print(f"Base model summary (for pre-training):") + base_model_tl.summary() + # Simulate pre-training + # base_model_tl.fit(X_general, y_general, epochs=5, batch_size=32, verbose=1) # Short pre-train for demo + print("Base model 'pre-trained' (simulated - no actual training in this step for speed).") + # In a real scenario, you would save these weights: base_model_tl.save_weights('pretrained_base_weights.h5') + else: + print("Not enough general data for pre-training simulation.") + base_model_tl = None + + # --- Transfer Learning (on a 'small' specific dataset) --- + if base_model_tl: + print("\nSimulating transfer learning to a new task/dataset...") + # Dummy specific dataset + dates_specific = pd.date_range(start='2023-01-01', periods=200, freq='B') + data_specific_np = np.random.rand(200, 1) * 70 + 30 # Different scale/behavior + specific_data = pd.DataFrame(data_specific_np, index=dates_specific, columns=['Close']) + specific_data['Close'] = specific_data['Close'] + np.cos(np.linspace(0, 10, 200)) * 15 + + X_specific, y_specific, scaler_specific, _ = preprocess_data_for_transfer(specific_data, look_back=look_back_tl) + + if X_specific.shape[0] > 100: # Ensure enough data for train/val split + split_idx = int(len(X_specific) * 0.8) + X_train_sp, y_train_sp = X_specific[:split_idx], y_specific[:split_idx] + X_val_sp, y_val_sp = X_specific[split_idx:], y_specific[split_idx:] + + # 1. Adapt the "pre-trained" base model + # Assume base_model_tl has pre-trained weights (even if just initialized for this demo) + adapted_model_tl = adapt_model_for_transfer(base_model_tl, num_classes_new_task=1) + print("Adapted model summary:") + adapted_model_tl.summary() + + # 2. Initial training on new task (with base frozen) + print("Training adapted model on new task (base frozen)...") + # adapted_model_tl.fit(X_train_sp, y_train_sp, epochs=10, batch_size=16, validation_data=(X_val_sp, y_val_sp), verbose=1) + print("'Trained' adapted model (simulated - no actual training for speed).") + + # 3. Fine-tune (unfreeze some layers of base_model and train with low LR) + print("\nFine-tuning model...") + # Example: unfreeze layers from 'base_lstm_2' onwards in the base_model part + # For this demo, let's try unfreezing the whole base_model part by passing None + fine_tuned_model, history = fine_tune_model( + adapted_model_tl, + X_train_sp, y_train_sp, + X_val_sp, y_val_sp, + unfreeze_at_layer_name=None, # Unfreeze all of base_model + # unfreeze_at_layer_name='base_lstm_2', # Or specify a layer + fine_tune_lr=1e-5, + epochs=5, # Short fine-tune for demo + batch_size=16 + ) + print("Model fine-tuned.") + + # Example prediction + if len(X_val_sp) > 0: + preds = fine_tuned_model.predict(X_val_sp) + print(f"\nSample predictions on validation set (first 5): {preds[:5].flatten()}") + print(f"Actual values (first 5): {y_val_sp[:5].flatten()}") + else: + print("Not enough specific data for transfer learning simulation.") + else: + print("Base model not available, skipping transfer learning simulation.") \ No newline at end of file diff --git a/upload-model.js b/upload-model.js new file mode 100644 index 0000000000000000000000000000000000000000..8563e5056786a974d983b72512c815b53aed22bf --- /dev/null +++ b/upload-model.js @@ -0,0 +1,59 @@ +import { HfApi, HfFolder } from "@huggingface/hub"; + +async function uploadToHuggingFace() { + try { + console.log("Iniciando processo de upload para Hugging Face..."); + + // Inicializar o cliente API + const api = new HfApi(); + + // Configurar suas credenciais + // Você precisa ter um token de acesso do Hugging Face + // Obtenha em: https://huggingface.co/settings/tokens + const token = "seu_token_aqui"; // Substitua pelo seu token + + // Nome do seu repositório + const repoId = "amos-fernadnes/meu-modelo"; + + // Criar novo repositório + console.log("Criando novo repositório..."); + await api.createRepo({ + token, + name: "meu-modelo", + organization: "amos-fernadnes", // seu username + private: false, + }); + + // Upload dos arquivos do modelo + console.log("Fazendo upload dos arquivos..."); + + // Lista de arquivos para upload + const files = [ + "config.json", + "pytorch_model.bin", + "special_tokens_map.json", + "tokenizer.json", + "tokenizer_config.json", + "vocab.txt" + ]; + + for (const file of files) { + console.log(`Uploading ${file}...`); + await api.uploadFile({ + token, + repo: repoId, + path: file, + file: `./modelo/${file}` // Caminho local dos seus arquivos + }); + } + + console.log("Upload concluído com sucesso!"); + console.log(`Seu modelo está disponível em: https://huggingface.co/${repoId}`); + + } catch (error) { + console.error("Erro durante o upload:", error); + } +} + +// Executar o upload +uploadToHuggingFace(); \ No newline at end of file