Amós e Souza Fernandes commited on
Commit
5f10e37
·
verified ·
1 Parent(s): 4d4863c

Upload 120 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. AutomatedLendingPool.sol +35 -0
  2. CreditOracle.sol +27 -0
  3. FuzzedDataProvider.js +15 -0
  4. ImeAtcoinGovernance.sol +59 -0
  5. ImeAtcoinPayment.js +15 -0
  6. MarketMaker.py +30 -0
  7. PrivateTransactions.sol +23 -0
  8. README.md +10 -1
  9. RewardSystem.sol +22 -0
  10. __init__.py +0 -0
  11. __pycache__/__init__.cpython-312.pyc +0 -0
  12. __pycache__/app.cpython-312.pyc +0 -0
  13. agents/DeepPortfolioAgent.py +361 -0
  14. agents/__init__.py +0 -0
  15. agents/__pycache__/DeepPortfolioAgent.cpython-312.pyc +0 -0
  16. agents/__pycache__/DeepPortfolioAgentNetwork.cpython-312.pyc +0 -0
  17. agents/__pycache__/__init__.cpython-312.pyc +0 -0
  18. agents/__pycache__/config.cpython-312.pyc +0 -0
  19. agents/__pycache__/custom_policies.cpython-312.pyc +0 -0
  20. agents/__pycache__/data_handler_multi_asset.cpython-312.pyc +0 -0
  21. agents/__pycache__/dataset_update_agent.cpython-312.pyc +0 -0
  22. agents/__pycache__/deep_portfolio.cpython-312.pyc +0 -0
  23. agents/__pycache__/deep_portfolio_torch.cpython-312.pyc +0 -0
  24. agents/__pycache__/portfolio_environment.cpython-312.pyc +0 -0
  25. agents/__pycache__/portfolio_features_extractor_torch.cpython-312.pyc +0 -0
  26. agents/__pycache__/train_rl_portfolio_agent.cpython-312.pyc +0 -0
  27. agents/config.md +13 -0
  28. agents/config.py +126 -0
  29. agents/custom_policies.py +99 -0
  30. agents/data_handler_multi_asset.py +448 -0
  31. agents/dataset_update_agent.py +9 -0
  32. agents/deep_portfolio.py +104 -0
  33. agents/deep_portfolio_torch.py +80 -0
  34. agents/financial_data_agent.py +76 -0
  35. agents/investment_agent.py +7 -0
  36. agents/portfolio_environment.py +246 -0
  37. agents/portfolio_features_extractor_torch.py +35 -0
  38. agents/ppo_deep_portfolio_tensorboard/PPO_1/events.out.tfevents.1750287361.verticalagent-X555LPB.89910.0 +3 -0
  39. agents/ppo_deep_portfolio_tensorboard/PPO_2/events.out.tfevents.1750321811.verticalagent-X555LPB.160180.0 +3 -0
  40. agents/ppo_deep_portfolio_tensorboard/PPO_3/events.out.tfevents.1750336135.verticalagent-X555LPB.200649.0 +3 -0
  41. agents/rl_agent.py +9 -0
  42. agents/train_agent_tfagents.py +127 -0
  43. agents/train_rl_portfolio_agent.py +755 -0
  44. api/main.py +61 -0
  45. app-modularizado.py +606 -0
  46. app-old-no-module.py +901 -0
  47. app-old-v1.py +901 -0
  48. app.py +750 -0
  49. app/config/config.py +115 -0
  50. app/dokerfile +10 -0
AutomatedLendingPool.sol ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pragma solidity ^0.8.0;
2
+ import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
3
+ import "./CreditOracle.sol";
4
+
5
+ contract AutomatedLendingPool {
6
+ IERC20 public immutable token;
7
+ CreditOracle public immutable creditOracle;
8
+
9
+ struct Loan {
10
+ uint256 amount;
11
+ uint256 interest;
12
+ uint256 dueDate;
13
+ }
14
+
15
+ mapping(address => Loan) public loans;
16
+
17
+ constructor(address _token, address _creditOracle) {
18
+ token = IERC20(_token);
19
+ creditOracle = CreditOracle(_creditOracle);
20
+ }
21
+
22
+ function requestLoan(uint256 amount) external {
23
+ require(loans[msg.sender].amount == 0, "Existing loan must be repaid");
24
+ bytes32 requestId = creditOracle.requestCreditScore(msg.sender);
25
+ // Lógica para processar o empréstimo com base no score de crédito
26
+ }
27
+
28
+ function repayLoan() external {
29
+ Loan storage loan = loans[msg.sender];
30
+ require(loan.amount > 0, "No active loan");
31
+ uint256 totalDue = loan.amount + loan.interest;
32
+ require(token.transferFrom(msg.sender, address(this), totalDue), "Transfer failed");
33
+ delete loans[msg.sender];
34
+ }
35
+ }
CreditOracle.sol ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pragma solidity ^0.8.0;
2
+ import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol";
3
+
4
+ contract CreditOracle is ChainlinkClient {
5
+ using Chainlink for Chainlink.Request;
6
+
7
+ address private oracle;
8
+ bytes32 private jobId;
9
+ uint256 private fee;
10
+
11
+ constructor() {
12
+ setPublicChainlinkToken();
13
+ oracle = 0x...; // Endereço do nó Chainlink
14
+ jobId = "..."; // ID do job Chainlink
15
+ fee = 0.1 * 10 ** 18; // 0.1 LINK
16
+ }
17
+
18
+ function requestCreditScore(address user) public returns (bytes32 requestId) {
19
+ Chainlink.Request memory request = buildChainlinkRequest(jobId, address(this), this.fulfill.selector);
20
+ request.add("userId", uint256(uint160(user)));
21
+ return sendChainlinkRequestTo(oracle, request, fee);
22
+ }
23
+
24
+ function fulfill(bytes32 _requestId, uint256 _creditScore) public recordChainlinkFulfillment(_requestId) {
25
+ // Lógica para atualizar o score de crédito do usuário
26
+ }
27
+ }
FuzzedDataProvider.js ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { FuzzedDataProvider } = require('fuzzing-tools');
2
+
3
+ function fuzzTest(data) {
4
+ const fuzz = new FuzzedDataProvider(data);
5
+ const amount = fuzz.consumeNumber();
6
+ const recipient = fuzz.consumeAddress();
7
+
8
+ try {
9
+ token.transfer(recipient, amount);
10
+ } catch (error) {
11
+ // Registrar e analisar erros
12
+ }
13
+ }
14
+
15
+ module.exports = { fuzzTest };
ImeAtcoinGovernance.sol ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pragma solidity ^0.8.0;
2
+ import "@openzeppelin/contracts/governance/Governor.sol";
3
+ import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol";
4
+ import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
5
+ import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
6
+ import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
7
+ import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
8
+
9
+ contract ImeAtcoinGovernance is Governor, GovernorSettings, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl {
10
+ constructor(IVotes _token, TimelockController _timelock)
11
+ Governor("ImeAtcoinGovernance")
12
+ GovernorSettings(1 /* 1 block */, 45818 /* 1 week */, 0)
13
+ GovernorVotes(_token)
14
+ GovernorVotesQuorumFraction(4)
15
+ GovernorTimelockControl(_timelock)
16
+ {}
17
+
18
+ function votingDelay() public view override(IGovernor, GovernorSettings) returns (uint256) {
19
+ return super.votingDelay();
20
+ }
21
+
22
+ function votingPeriod() public view override(IGovernor, GovernorSettings) returns (uint256) {
23
+ return super.votingPeriod();
24
+ }
25
+
26
+ function quorum(uint256 blockNumber) public view override(IGovernor, GovernorVotesQuorumFraction) returns (uint256) {
27
+ return super.quorum(blockNumber);
28
+ }
29
+
30
+ function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) {
31
+ return super.state(proposalId);
32
+ }
33
+
34
+ function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description)
35
+ public override(Governor, IGovernor) returns (uint256)
36
+ {
37
+ return super.propose(targets, values, calldatas, description);
38
+ }
39
+
40
+ function _execute(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
41
+ internal override(Governor, GovernorTimelockControl)
42
+ {
43
+ super._execute(proposalId, targets, values, calldatas, descriptionHash);
44
+ }
45
+
46
+ function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
47
+ internal override(Governor, GovernorTimelockControl) returns (uint256)
48
+ {
49
+ return super._cancel(targets, values, calldatas, descriptionHash);
50
+ }
51
+
52
+ function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) {
53
+ return super._executor();
54
+ }
55
+
56
+ function supportsInterface(bytes4 interfaceId) public view override(Governor, GovernorTimelockControl) returns (bool) {
57
+ return super.supportsInterface(interfaceId);
58
+ }
59
+ }
ImeAtcoinPayment.js ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class ImeAtcoinPayment {
2
+ constructor(apiKey) {
3
+ this.apiKey = apiKey;
4
+ }
5
+
6
+ async createPayment(amount, currency) {
7
+ // Lógica para criar um pagamento
8
+ }
9
+
10
+ async verifyPayment(paymentId) {
11
+ // Lógica para verificar um pagamento
12
+ }
13
+ }
14
+
15
+ module.exports = ImeAtcoinPayment;
MarketMaker.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ccxt
2
+
3
+ class MarketMaker:
4
+ def __init__(self, exchange, symbol, spread=0.01):
5
+ self.exchange = ccxt.exchange({'apiKey': '...', 'secret': '...'})
6
+ self.symbol = symbol
7
+ self.spread = spread
8
+
9
+ def place_orders(self):
10
+ ticker = self.exchange.fetch_ticker(self.symbol)
11
+ mid_price = (ticker['bid'] + ticker['ask']) / 2
12
+
13
+ bid_price = mid_price * (1 - self.spread / 2)
14
+ ask_price = mid_price * (1 + self.spread / 2)
15
+
16
+ self.exchange.create_limit_buy_order(self.symbol, 1, bid_price)
17
+ self.exchange.create_limit_sell_order(self.symbol, 1, ask_price)
18
+
19
+ def run(self):
20
+ while True:
21
+ try:
22
+ self.place_orders()
23
+ time.sleep(60) # Atualiza ordens a cada minuto
24
+ except Exception as e:
25
+ print(f"Error: {e}")
26
+ time.sleep(60)
27
+
28
+ if __name__ == "__main__":
29
+ market_maker = MarketMaker("binance", "I*****ME/USDT")
30
+ market_maker.run()
PrivateTransactions.sol ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pragma solidity ^0.8.0;
2
+ import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
3
+ import "./ZkSnarkVerifier.sol";
4
+
5
+ contract PrivateTransactions {
6
+ IERC20 public immutable token;
7
+ ZkSnarkVerifier public immutable verifier;
8
+
9
+ constructor(address _token, address _verifier) {
10
+ token = IERC20(_token);
11
+ verifier = ZkSnarkVerifier(_verifier);
12
+ }
13
+
14
+ function privateTransfer(
15
+ uint256[2] memory a,
16
+ uint256[2][2] memory b,
17
+ uint256[2] memory c,
18
+ uint256[3] memory input
19
+ ) external {
20
+ require(verifier.verifyProof(a, b, c, input), "Invalid zk-SNARK proof");
21
+ // Executar a transferência privada
22
+ }
23
+ }
README.md CHANGED
@@ -1,3 +1,12 @@
1
  ---
2
- license: mit
 
 
 
 
 
 
 
3
  ---
 
 
 
1
  ---
2
+ title: Aibank Token
3
+ emoji: 🐠
4
+ colorFrom: yellow
5
+ colorTo: pink
6
+ sdk: static
7
+ pinned: false
8
+ license: other
9
+ short_description: Token aibank
10
  ---
11
+
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
RewardSystem.sol ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pragma solidity ^0.8.0;
2
+ import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
3
+
4
+ contract RewardSystem {
5
+ IERC20 public immutable token;
6
+ mapping(address => uint256) public rewards;
7
+
8
+ constructor(address _token) {
9
+ token = IERC20(_token);
10
+ }
11
+
12
+ function addReward(address user, uint256 amount) external {
13
+ rewards[user] += amount;
14
+ }
15
+
16
+ function claimReward() external {
17
+ uint256 amount = rewards[msg.sender];
18
+ require(amount > 0, "No rewards to claim");
19
+ rewards[msg.sender] = 0;
20
+ require(token.transfer(msg.sender, amount), "Transfer failed");
21
+ }
22
+ }
__init__.py ADDED
File without changes
__pycache__/__init__.cpython-312.pyc ADDED
Binary file (149 Bytes). View file
 
__pycache__/app.cpython-312.pyc ADDED
Binary file (3.16 kB). View file
 
agents/DeepPortfolioAgent.py ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # rnn/agents/deep_portfolio.py (ou onde você tem DeepPortfolioAI / DeepPortfolioAgentNetwork)
3
+ import numpy as np
4
+ from tensorflow.keras import regularizers
5
+ import tensorflow as tf
6
+ from tensorflow.keras.layers import (
7
+ Input, Conv1D, LSTM, Dense, Dropout,
8
+ MultiHeadAttention, Reshape, Concatenate,
9
+ TimeDistributed, GlobalAveragePooling1D, LayerNormalization
10
+ )
11
+ from tensorflow.keras.models import Model
12
+ from transformers import AutoTokenizer, TFAutoModelForSequenceClassification
13
+ # Comente as importações do transformers se não for testar o sentimento agora para simplificar
14
+ # from transformers import AutoTokenizer, TFAutoModelForSequenceClassification
15
+
16
+ # --- DEFINIÇÕES DE CONFIGURAÇÃO (COPIE OU IMPORTE DO SEU CONFIG.PY) ---
17
+ # Se você não importar do config.py, defina-as aqui para o teste
18
+ WINDOW_SIZE_CONF = 60
19
+ NUM_ASSETS_CONF = 4 # Ex: ETH, BTC, ADA, SOL
20
+ NUM_FEATURES_PER_ASSET_CONF = 26 # Número de features calculadas para CADA ativo
21
+ # (open_div_atr, ..., buy_condition_v1, etc.)
22
+ L2_REG = 0.0001 # Exemplo, use o valor do seu config
23
+
24
+ # (Cole as classes AssetProcessor e DeepPortfolioAgentNetwork aqui se estiver em um novo script)
25
+ # Ou, se estiver no mesmo arquivo, elas já estarão definidas.
26
+
27
+ # ... (Definição das classes AssetProcessor e DeepPortfolioAgentNetwork como na resposta anterior) ...
28
+ # Certifique-se que a classe DeepPortfolioAgentNetwork está usando estas constantes:
29
+ # num_assets=NUM_ASSETS_CONF,
30
+ # sequence_length=WINDOW_SIZE_CONF,
31
+ # num_features_per_asset=NUM_FEATURES_PER_ASSET_CONF
32
+
33
+ class AssetProcessor(tf.keras.Model):
34
+ 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
35
+ super(AssetProcessor, self).__init__(name=name, **kwargs) # Adicionado **kwargs
36
+ self.sequence_length = sequence_length
37
+ self.num_features = num_features
38
+ self.cnn_filters1 = cnn_filters1 # Salvar para get_config
39
+ self.cnn_filters2 = cnn_filters2
40
+ self.lstm_units1 = lstm_units1
41
+ self.lstm_units2 = lstm_units2
42
+ self.dropout_rate = dropout_rate
43
+
44
+ self.conv1 = Conv1D(filters=cnn_filters1, kernel_size=3, activation='relu', padding='same', name="asset_cnn1")
45
+ self.dropout_cnn1 = Dropout(dropout_rate, name="asset_cnn1_dropout")
46
+ self.conv2 = Conv1D(filters=cnn_filters2, kernel_size=3, activation='relu', padding='same', name="asset_cnn2")
47
+ self.dropout_cnn2 = Dropout(dropout_rate, name="asset_cnn2_dropout")
48
+ self.lstm1 = LSTM(lstm_units1, return_sequences=True, name="asset_lstm1")
49
+ self.dropout_lstm1 = Dropout(dropout_rate, name="asset_lstm1_dropout")
50
+ self.lstm2 = LSTM(lstm_units2, return_sequences=False, name="asset_lstm2_final")
51
+ self.dropout_lstm2 = Dropout(dropout_rate, name="asset_lstm2_dropout")
52
+
53
+ def call(self, inputs, training=False):
54
+ x = self.conv1(inputs)
55
+ x = self.dropout_cnn1(x, training=training)
56
+ x = self.conv2(x)
57
+ x = self.dropout_cnn2(x, training=training)
58
+ x = self.lstm1(x, training=training)
59
+ x = self.dropout_lstm1(x, training=training)
60
+ x = self.lstm2(x, training=training)
61
+ x_processed_asset = self.dropout_lstm2(x, training=training)
62
+ return x_processed_asset
63
+
64
+ def get_config(self):
65
+ config = super().get_config()
66
+ config.update({
67
+ "sequence_length": self.sequence_length,
68
+ "num_features": self.num_features,
69
+ "cnn_filters1": self.cnn_filters1,
70
+ "cnn_filters2": self.cnn_filters2,
71
+ "lstm_units1": self.lstm_units1,
72
+ "lstm_units2": self.lstm_units2,
73
+ "dropout_rate": self.dropout_rate,
74
+ })
75
+ return config
76
+
77
+ class DeepPortfolioAgentNetwork(tf.keras.Model):
78
+ def __init__(self,
79
+ num_assets=int(NUM_ASSETS_CONF),
80
+ sequence_length=int(WINDOW_SIZE_CONF),
81
+ num_features_per_asset=int(NUM_FEATURES_PER_ASSET_CONF),
82
+ asset_cnn_filters1=32, asset_cnn_filters2=64,
83
+ asset_lstm_units1=64, asset_lstm_units2=32, asset_dropout=0.2,
84
+ mha_num_heads=4, mha_key_dim_divisor=2, # key_dim será asset_lstm_units2 // mha_key_dim_divisor
85
+ final_dense_units1=128, final_dense_units2=64, final_dropout=0.3,
86
+ use_sentiment_analysis=True,
87
+ output_latent_features=False, **kwargs): # Adicionado **kwargs
88
+ super(DeepPortfolioAgentNetwork, self).__init__(name="deep_portfolio_agent_network", **kwargs) # Adicionado **kwargs
89
+
90
+
91
+ print(f"DPN __init__ > num_assets ENTRADA: {num_assets}, tipo: {type(num_assets)}")
92
+
93
+ # Tentar extrair o valor escalar se for um tensor/variável ou TrackedDict
94
+ def get_int_value(param_name, val):
95
+ if isinstance(val, (tf.Tensor, tf.Variable)):
96
+ if val.shape == tf.TensorShape([]): # Escalar
97
+ print(f"DPN __init__: Convertendo {param_name} (Tensor/Variable escalar) para int.")
98
+ return int(val.numpy())
99
+ else:
100
+ raise ValueError(f"{param_name} é um Tensor/Variable mas não é escalar. Shape: {val.shape}")
101
+ elif isinstance(val, dict): # Pode ser um TrackedDict
102
+ # TrackedDict pode se comportar como um dict. Se o valor real está "escondido",
103
+ # precisamos descobrir como acessá-lo.
104
+ # Por agora, vamos tentar a conversão direta, e se falhar, o erro será mais claro.
105
+ # Se for um dict simples com uma chave específica, você precisaria dessa chave.
106
+ # O erro 'KeyError: value' sugere que ['value'] não é a forma correta.
107
+ # Geralmente, para hiperparâmetros, o TrackedDict deve conter o valor diretamente
108
+ # se o SB3 o passou corretamente.
109
+ print(f"DPN __init__: Tentando converter {param_name} (dict-like) para int.")
110
+ try:
111
+ return int(val) # Tentar conversão direta
112
+ except TypeError:
113
+ # Se TrackedDict se comporta como um tensor quando usado em ops TF,
114
+ # tf.get_static_value pode funcionar, ou apenas o uso direto
115
+ # em operações TF (mas range() não é uma op TF).
116
+ # Se for um tensor TF "disfarçado", .numpy() pode funcionar.
117
+ # Se for um dict com uma chave específica, essa chave seria necessária.
118
+ # O erro mostra que ['value'] não funcionou.
119
+ print(f"DPN __init__: Conversão direta de {param_name} (dict-like) para int falhou. Investigar TrackedDict.")
120
+ # Para depuração, você pode tentar imprimir os itens do dict:
121
+ # if isinstance(val, collections.abc.Mapping): # Checa se é um dict-like
122
+ # for k, v_item in val.items():
123
+ # print(f" {param_name} item: {k} -> {v_item}")
124
+ raise TypeError(f"{param_name} é {type(val)} e não pôde ser convertido para int diretamente. Valor: {val}")
125
+ else: # Tenta conversão direta para outros tipos
126
+ return int(val)
127
+
128
+ try:
129
+ self.num_assets = get_int_value("num_assets", num_assets)
130
+ self.sequence_length = get_int_value("sequence_length", sequence_length)
131
+ self.num_features_per_asset = get_int_value("num_features_per_asset", num_features_per_asset)
132
+ self.asset_lstm_output_dim = get_int_value("asset_lstm_units2", asset_lstm_units2) # Do kwargs
133
+
134
+ # Faça o mesmo para TODOS os outros parâmetros que devem ser inteiros e são passados
135
+ # para construtores de camadas Keras (cnn_filters, lstm_units, mha_num_heads, etc.)
136
+ # Exemplo:
137
+ # self.asset_cnn_filters1_val = get_int_value("asset_cnn_filters1", kwargs.get("asset_cnn_filters1"))
138
+
139
+ except Exception as e_conv:
140
+ print(f"ERRO CRÍTICO DE CONVERSÃO DE TIPO no __init__ da DeepPortfolioAgentNetwork: {e_conv}")
141
+ raise
142
+
143
+ print(f"DPN __init__ > self.num_assets APÓS conversão: {self.num_assets}, tipo: {type(self.num_assets)}")
144
+
145
+
146
+
147
+
148
+
149
+
150
+ self.num_assets = num_assets
151
+ self.sequence_length = sequence_length
152
+ self.num_features_per_asset = num_features_per_asset
153
+ self.asset_lstm_output_dim = asset_lstm_units2
154
+
155
+ self.asset_processor = AssetProcessor(
156
+ sequence_length=self.sequence_length, num_features=self.num_features_per_asset,
157
+ cnn_filters1=asset_cnn_filters1, cnn_filters2=asset_cnn_filters2,
158
+ lstm_units1=asset_lstm_units1, lstm_units2=asset_lstm_units2,
159
+ dropout_rate=asset_dropout
160
+ )
161
+
162
+ # Ajustar key_dim para ser compatível com a dimensão de entrada e num_heads
163
+ # key_dim * num_heads deve ser idealmente igual a asset_lstm_output_dim se for auto-atenção direta,
164
+ # ou o MHA projeta internamente. Para simplificar, vamos fazer key_dim ser divisível.
165
+ # Se asset_lstm_output_dim não for divisível por num_heads, key_dim pode ser diferente.
166
+ # Vamos definir key_dim explicitamente. Se asset_lstm_output_dim = 32 e num_heads = 4, key_dim pode ser 8.
167
+ # Ou deixar o MHA lidar com a projeção se key_dim for diferente.
168
+ # Para maior clareza, calculamos uma key_dim sensata.
169
+ calculated_key_dim = self.asset_lstm_output_dim // mha_key_dim_divisor
170
+ if calculated_key_dim == 0: # Evitar key_dim zero
171
+ calculated_key_dim = self.asset_lstm_output_dim # Fallback se for muito pequeno
172
+ 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}")
173
+
174
+ self.attention = MultiHeadAttention(num_heads=mha_num_heads, key_dim=calculated_key_dim, dropout=0.1, name="multi_asset_attention")
175
+ self.attention_norm = LayerNormalization(epsilon=1e-6, name="attention_layernorm")
176
+ self.global_avg_pool_attention = GlobalAveragePooling1D(name="gap_after_attention")
177
+
178
+ self.use_sentiment = use_sentiment_analysis # Desabilitado por padrão para este teste
179
+ self.sentiment_embedding_size = 3
180
+ if self.use_sentiment:
181
+ try:
182
+ self.tokenizer = AutoTokenizer.from_pretrained('ProsusAI/finbert')
183
+ self.sentiment_model = TFAutoModelForSequenceClassification.from_pretrained('ProsusAI/finbert', from_pt=True)
184
+ print("Modelo FinBERT carregado para análise de sentimento.")
185
+ except Exception as e:
186
+ print(f"AVISO: Falha ao carregar FinBERT: {e}. Análise de sentimento será desabilitada.")
187
+ self.use_sentiment = False
188
+
189
+
190
+ dense_input_dim = self.use_sentiment
191
+ #if self.use_sentiment: dense_input_dim += self.sentiment_embedding_size
192
+
193
+ self.dense1 = Dense(final_dense_units1, activation='relu', kernel_regularizer=regularizers.l2(L2_REG), name="final_dense1")
194
+ self.dropout1 = Dropout(final_dropout, name="final_dropout1")
195
+ self.dense2 = Dense(final_dense_units2, activation='relu', kernel_regularizer=regularizers.l2(L2_REG), name="final_dense2")
196
+ self.dropout2 = Dropout(final_dropout, name="final_dropout2")
197
+ self.output_allocation = Dense(self.num_assets, activation='softmax', name="portfolio_allocation_output")
198
+
199
+ def call(self, inputs, training=False):
200
+ market_data_flat = inputs
201
+
202
+ print(type(self.num_assets))
203
+ asset_representations_list = []
204
+ for i in range(self.num_assets):
205
+ start_idx = i * self.num_features_per_asset
206
+ end_idx = (i + 1) * self.num_features_per_asset
207
+ current_asset_data = market_data_flat[:, :, start_idx:end_idx]
208
+ processed_asset_representation = self.asset_processor(current_asset_data, training=training)
209
+ asset_representations_list.append(processed_asset_representation)
210
+
211
+ stacked_asset_features = tf.stack(asset_representations_list, axis=1)
212
+
213
+ # Para MHA, query, value, key são (batch_size, Tq, dim), (batch_size, Tv, dim)
214
+ # Aqui, T = num_assets, dim = asset_lstm_output_dim
215
+ attention_output = self.attention(
216
+ query=stacked_asset_features, value=stacked_asset_features, key=stacked_asset_features,
217
+ training=training
218
+ )
219
+ attention_output = self.attention_norm(stacked_asset_features + attention_output)
220
+
221
+ context_vector_from_attention = self.global_avg_pool_attention(attention_output)
222
+
223
+ current_features_for_dense = context_vector_from_attention
224
+ # if self.use_sentiment: ... (lógica de concatenação)
225
+
226
+ x = self.dense1(current_features_for_dense)
227
+ x = self.dropout1(x, training=training)
228
+ x = self.dense2(x)
229
+ x = self.dropout2(x, training=training)
230
+
231
+ portfolio_weights = self.output_allocation(x)
232
+ return portfolio_weights
233
+
234
+ def get_config(self): # Necessário se você quiser salvar/carregar o modelo que usa este sub-modelo
235
+ config = super().get_config()
236
+ config.update({
237
+ "num_assets": self.num_assets,
238
+ "sequence_length": self.sequence_length,
239
+ "num_features_per_asset": self.num_features_per_asset,
240
+ # Adicione outros args do __init__ aqui para todas as camadas e sub-modelos
241
+ "asset_lstm_output_dim": self.asset_lstm_output_dim,
242
+ # ... e os parâmetros passados para AssetProcessor e MHA, etc.
243
+ })
244
+ return config
245
+
246
+ # @classmethod
247
+ # def from_config(cls, config): # Necessário para carregar com sub-modelo customizado
248
+ # # Extrair config do AssetProcessor se necessário
249
+ # return cls(**config)
250
+
251
+
252
+ if __name__ == '__main__':
253
+ print("Testando o Forward Pass do DeepPortfolioAgentNetwork...")
254
+
255
+ # 1. Definir Parâmetros para o Teste (devem corresponder ao config.py)
256
+ batch_size_test = 2 # Um batch pequeno para teste
257
+ seq_len_test = WINDOW_SIZE_CONF
258
+ num_assets_test = NUM_ASSETS_CONF
259
+ num_features_per_asset_test = NUM_FEATURES_PER_ASSET_CONF
260
+ total_features_flat = num_assets_test * num_features_per_asset_test
261
+
262
+ print(f"Configuração do Teste:")
263
+ print(f" Batch Size: {batch_size_test}")
264
+ print(f" Sequence Length (Window): {seq_len_test}")
265
+ print(f" Number of Assets: {num_assets_test}")
266
+ print(f" Features per Asset: {num_features_per_asset_test}")
267
+ print(f" Total Flat Features per Timestep: {total_features_flat}")
268
+
269
+ # 2. Criar Tensor de Input Mockado
270
+ # Shape: (batch_size, sequence_length, num_assets * num_features_per_asset)
271
+ mock_market_data_flat = tf.random.normal(
272
+ shape=(batch_size_test, seq_len_test, total_features_flat)
273
+ )
274
+ print(f"Shape do Input Mockado (market_data_flat): {mock_market_data_flat.shape}")
275
+
276
+ # 3. Instanciar o Modelo
277
+ # Use os mesmos hiperparâmetros que você definiria no config.py para a rede
278
+ print("\nInstanciando DeepPortfolioAgentNetwork...")
279
+ agent_network = DeepPortfolioAgentNetwork(
280
+ num_assets=num_assets_test,
281
+ sequence_length=seq_len_test,
282
+ num_features_per_asset=num_features_per_asset_test,
283
+ # Você pode variar os próximos parâmetros para testar diferentes configs
284
+ asset_cnn_filters1=32, asset_cnn_filters2=64,
285
+ asset_lstm_units1=64, asset_lstm_units2=32, # asset_lstm_units2 define asset_lstm_output_dim
286
+ asset_dropout=0.1,
287
+ mha_num_heads=4, mha_key_dim_divisor=4, # Ex: 32 // 4 = 8 para key_dim
288
+ final_dense_units1=64, final_dense_units2=32, final_dropout=0.2,
289
+ use_sentiment_analysis=False # Testar sem sentimento primeiro
290
+ )
291
+
292
+ # Para construir o modelo e ver o summary, você pode chamar com o input mockado
293
+ # ou explicitamente chamar model.build() se souber o input shape completo
294
+ # Chamar com input mockado é mais fácil para construir.
295
+ print("\nConstruindo o modelo com input mockado (primeira chamada)...")
296
+ try:
297
+ # É uma boa prática fazer a primeira chamada dentro de um tf.function para otimizar
298
+ # ou apenas chamar diretamente para teste.
299
+ _ = agent_network(mock_market_data_flat) # Chamada para construir as camadas
300
+ print("\n--- Summary da Rede Principal (DeepPortfolioAgentNetwork) ---")
301
+ agent_network.summary()
302
+
303
+ # O summary do asset_processor já foi impresso no __init__ do DeepPortfolioAgentNetwork
304
+ # se você descomentar as linhas de build/summary lá.
305
+ # Ou você pode imprimir aqui:
306
+ print("\n--- Summary do AssetProcessor (Sub-Modelo) ---")
307
+ agent_network.asset_processor.summary()
308
+
309
+
310
+ except Exception as e:
311
+ print(f"Erro ao construir a rede principal: {e}", exc_info=True)
312
+ exit()
313
+
314
+ # 4. Chamar model(mock_input) para o Forward Pass
315
+ print("\nExecutando Forward Pass...")
316
+ try:
317
+ predictions = agent_network(mock_market_data_flat, training=False) # Passar training=False para inferência
318
+ print("Forward Pass concluído com sucesso!")
319
+ except Exception as e:
320
+ print(f"Erro durante o Forward Pass: {e}", exc_info=True)
321
+ exit()
322
+
323
+ # 5. Verificar o Shape da Saída
324
+ print(f"\nShape da Saída (predictions): {predictions.shape}")
325
+ expected_output_shape = (batch_size_test, num_assets_test)
326
+ if predictions.shape == expected_output_shape:
327
+ print(f"Shape da Saída está CORRETO! Esperado: {expected_output_shape}")
328
+ else:
329
+ print(f"ERRO: Shape da Saída INCORRETO. Esperado: {expected_output_shape}, Obtido: {predictions.shape}")
330
+
331
+ # Verificar se a saída é uma distribuição de probabilidade (softmax)
332
+ if hasattr(predictions, 'numpy'): # Se for um EagerTensor
333
+ output_sum = tf.reduce_sum(predictions, axis=-1).numpy()
334
+ print(f"Soma das probabilidades de saída por amostra no batch (deve ser próximo de 1): {output_sum}")
335
+ if np.allclose(output_sum, 1.0):
336
+ print("Saída Softmax parece CORRETA (soma 1).")
337
+ else:
338
+ print("AVISO: Saída Softmax pode NÃO estar correta (soma diferente de 1).")
339
+
340
+ print("\nExemplo das primeiras predições (pesos do portfólio):")
341
+ print(predictions.numpy()[:min(5, batch_size_test)]) # Imprime até 5 predições do batch
342
+
343
+ # Teste com sentimento (se implementado e FinBERT carregado)
344
+ # agent_network.use_sentiment = True # Ativar para teste
345
+ # if agent_network.use_sentiment and hasattr(agent_network, 'tokenizer'):
346
+ # print("\nTestando Forward Pass COM SENTIMENTO...")
347
+ # mock_news_batch = ["positive news for asset 1", "market is very volatile today"] # Exemplo
348
+ # # A forma como você passa 'news' para o call() precisa ser definida.
349
+ # # Se for um dicionário:
350
+ # # mock_inputs_with_news = {"market_data": mock_market_data_flat, "news_data": mock_news_batch}
351
+ # # predictions_with_sentiment = agent_network(mock_inputs_with_news, training=False)
352
+ # # print(f"Shape da Saída com Sentimento: {predictions_with_sentiment.shape}")
353
+ # else:
354
+ # print("\nTeste com sentimento pulado (use_sentiment=False ou FinBERT não carregado).")
355
+
356
+ print("\nTeste do Forward Pass Concluído!")
357
+
358
+
359
+
360
+
361
+
agents/__init__.py ADDED
File without changes
agents/__pycache__/DeepPortfolioAgent.cpython-312.pyc ADDED
Binary file (14.7 kB). View file
 
agents/__pycache__/DeepPortfolioAgentNetwork.cpython-312.pyc ADDED
Binary file (6.39 kB). View file
 
agents/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (156 Bytes). View file
 
agents/__pycache__/config.cpython-312.pyc ADDED
Binary file (2.71 kB). View file
 
agents/__pycache__/custom_policies.cpython-312.pyc ADDED
Binary file (5.16 kB). View file
 
agents/__pycache__/data_handler_multi_asset.cpython-312.pyc ADDED
Binary file (14.4 kB). View file
 
agents/__pycache__/dataset_update_agent.cpython-312.pyc ADDED
Binary file (850 Bytes). View file
 
agents/__pycache__/deep_portfolio.cpython-312.pyc ADDED
Binary file (5.95 kB). View file
 
agents/__pycache__/deep_portfolio_torch.cpython-312.pyc ADDED
Binary file (4.89 kB). View file
 
agents/__pycache__/portfolio_environment.cpython-312.pyc ADDED
Binary file (10.7 kB). View file
 
agents/__pycache__/portfolio_features_extractor_torch.cpython-312.pyc ADDED
Binary file (1.7 kB). View file
 
agents/__pycache__/train_rl_portfolio_agent.cpython-312.pyc ADDED
Binary file (16 kB). View file
 
agents/config.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # EXPECTED_FEATURES_ORDER define como as colunas DEVEM ESTAR no DataFrame
2
+ # que entra na função create_sequences, APÓS o escalonamento e ANTES do sufixo _scaled.
3
+ # E também como o rnn_predictor.py espera as features ANTES de aplicar os scalers.
4
+ # Se rnn_predictor.py vai aplicar scalers separados, ele precisa dos nomes originais (ou _div_atr).
5
+ # O script de treino vai criar colunas com sufixo _scaled para alimentar o modelo.
6
+ # A lista EXPECTED_FEATURES_ORDER no config.py não é diretamente usada no train_rnn_model.py
7
+ # da forma como está agora, mas é CRUCIAL para alinhar com rnn_predictor.py.
8
+ # Fazer o train_rnn_model.py funcionar e depois alinhar o rnn_predictor.py.
9
+ # Importante que as NUM_FEATURES e o input_shape do modelo estejam corretos.
10
+
11
+ # Esta variável será usada para nomear as colunas escaladas que vão para o modelo
12
+ # e deve corresponder ao que o rnn_predictor.py espera encontrar como features escaladas.
13
+ # Os nomes aqui devem ser as colunas de BASE_FEATURE_COLS com "_scaled" no final.
agents/config.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config.py
2
+
3
+ # --- Parâmetros de Dados ---
4
+ SYMBOL = 'ETH/USDT' # Ativo principal para o modelo de classificação (se ainda usar)
5
+ MULTI_ASSET_SYMBOLS = { # Para o agente de portfólio RL
6
+ 'eth': 'ETH-USD', # Chave amigável: ticker_yfinance
7
+ 'btc': 'BTC-USD',
8
+ 'ada': 'ADA-USD',
9
+ 'sol': 'SOL-USD'
10
+ }
11
+ NUM_ASSETS_PORTFOLIO = len(MULTI_ASSET_SYMBOLS) # Número de ativos no portfólio
12
+ NUM_ASSETS=4
13
+ TIMEFRAME = '1h' # Usado tanto para yfinance quanto para ccxt (se adaptar)
14
+ DAYS_OF_DATA_TO_FETCH = 365 * 2
15
+ LIMIT_PER_FETCH = 1000 # Para ccxt
16
+
17
+ # --- Parâmetros de Features e Janela ---
18
+ WINDOW_SIZE = 60
19
+
20
+ # BASE_FEATURE_COLS: Colunas calculadas para CADA ativo ANTES do escalonamento final para o modelo.
21
+ # Estas são as features que seu data_handler_multi_asset.py DEVE produzir para cada ativo.
22
+ # O rnn_predictor.py (ou a parte de features da DeepPortfolioAgentNetwork) também as calculará.
23
+ BASE_FEATURES_PER_ASSET_INPUT = [ # Renomeado para clareza
24
+ 'open_div_atr', 'high_div_atr', 'low_div_atr', 'close_div_atr', 'volume_div_atr',
25
+ 'log_return', 'rsi_14', 'atr', 'bbp', 'cci_37', 'mfi_37',
26
+ 'body_size_norm_atr', 'body_vs_avg_body', 'macd', 'sma_10_div_atr',
27
+ 'adx_14', 'volume_zscore', 'buy_condition_v1'
28
+ # Se 'cond_compra_v1' for diferente de 'buy_condition_v1', adicione aqui.
29
+ # Se for igual, remova a redundância. Assumindo que 'buy_condition_v1' é a correta.
30
+ ]
31
+ # Nomes das colunas de preço/volume (normalizadas por ATR) que usarão o price_vol_scaler
32
+ API_PRICE_VOL_COLS = ['open_div_atr', 'high_div_atr', 'low_div_atr', 'close_div_atr', 'volume_div_atr', 'body_size_norm_atr']
33
+ # Nomes das colunas de indicadores (e outras) que usarão o indicator_scaler
34
+ API_INDICATOR_COLS = [col for col in BASE_FEATURES_PER_ASSET_INPUT if col not in API_PRICE_VOL_COLS]
35
+
36
+ # Número de features que CADA ativo terá após todos os cálculos e ANTES do escalonamento final.
37
+ NUM_FEATURES_PER_ASSET = len(BASE_FEATURES_PER_ASSET_INPUT)
38
+
39
+ # Nomes das colunas escaladas que o modelo RNN/RL efetivamente verá como entrada.
40
+ # Esta é a ordem que deve ser mantida após o escalonamento no data_handler e no rnn_predictor.
41
+ # E também o que o create_sequences espera.
42
+ EXPECTED_SCALED_FEATURES_FOR_MODEL = [f"{col}_scaled" for col in BASE_FEATURES_PER_ASSET_INPUT]
43
+ # NUM_FEATURES_MODEL_INPUT será len(EXPECTED_SCALED_FEATURES_FOR_MODEL), que é igual a NUM_FEATURES_PER_ASSET
44
+
45
+ # --- Parâmetros do Alvo da Predição (Para o Modelo de Classificação Supervisionado, se ainda usar) ---
46
+ PREDICTION_HORIZON = 5
47
+ PRICE_CHANGE_THRESHOLD = 0.0075
48
+
49
+ # --- Parâmetros da Rede Neural (DeepPortfolioAgentNetwork e seu AssetProcessor) ---
50
+ # Para AssetProcessor (processamento individual de ativo)
51
+ ASSET_CNN_FILTERS1 = 32
52
+ ASSET_CNN_FILTERS2 = 64
53
+ ASSET_LSTM_UNITS1 = 64
54
+ ASSET_LSTM_UNITS2 = 32 # Saída do AssetProcessor, se torna a dimensão da feature latente por ativo
55
+ ASSET_DROPOUT = 0.2
56
+
57
+ # Para DeepPortfolioAgentNetwork (camadas após processamento individual)
58
+ MHA_NUM_HEADS = 4
59
+ # key_dim da MHA será ASSET_LSTM_UNITS2 // MHA_KEY_DIM_DIVISOR
60
+ MHA_KEY_DIM_DIVISOR = 2 # Ex: 32 // 2 = 16. Garanta que ASSET_LSTM_UNITS2 seja divisível. Se não, ajuste.
61
+
62
+ # Camadas densas FINAIS DENTRO da DeepPortfolioAgentNetwork, ANTES da saída de features latentes
63
+ # ou da camada de alocação softmax (se não estiver retornando features latentes).
64
+ # `FINAL_DENSE_UNITS2_EXTRACTOR` será a dimensão das features que o extrator cospe para o SB3.
65
+ DPN_FINAL_DENSE1_UNITS = 64 # "DPN" para DeepPortfolioNetwork
66
+ 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.
67
+ # Se você adicionou Dense(final_dense_units1) e Dense(final_dense_units2) APÓS a atenção
68
+ # no DeepPortfolioAgentNetwork, então DPN_LATENT_FEATURE_DIM seria final_dense_units2.
69
+ # No nosso último design, era a saída do global_avg_pool_attention, então ASSET_LSTM_UNITS2.
70
+ # Vamos assumir que a saída do GAP é usada como feature latente por enquanto.
71
+ # DPN_LATENT_FEATURE_DIM = ASSET_LSTM_UNITS2
72
+
73
+ # Ajustando com base no seu código de `deep_portfolio.py` onde você tinha `final_dense_units1` e `final_dense_units2`
74
+ # após a atenção e antes do output de alocação.
75
+ # Estas são as camadas que produzem as features latentes para SB3.
76
+ DPN_SHARED_HEAD_DENSE1_UNITS = 128 # Corresponde a final_dense_units1 na sua DeepPortfolioAgentNetwork
77
+ DPN_SHARED_HEAD_LATENT_DIM = 64 # Corresponde a final_dense_units2, que será o self.features_dim do extrator
78
+ DPN_SHARED_HEAD_DROPOUT = 0.3
79
+
80
+ DEFAULT_EXTRACTOR_KWARGS=DPN_SHARED_HEAD_DENSE1_UNITS
81
+
82
+ # Para as cabeças de Política (Ator) e Valor (Crítico) no Stable-Baselines3 (APÓS o extrator)
83
+ # Se vazias, a saída do extrator é usada diretamente para as camadas finais de ação/valor.
84
+ POLICY_HEAD_NET_ARCH = [64] # Ex: [64, 32] ou [] se não quiser camadas extras
85
+ VALUE_HEAD_NET_ARCH = [64] # Ex: [64, 32] ou []
86
+
87
+ # --- Parâmetros Gerais do Modelo (se aplicável a ambos os tipos de modelo) ---
88
+ MODEL_DROPOUT_RATE = 0.3 # Você usou 0.3 na última rodada de classificação bem-sucedida
89
+ MODEL_L2_REG = 0.0001 # Você usou 0.0001 ou 0.0005
90
+
91
+ L2_REG = 0.0001
92
+
93
+ # --- Parâmetros de Treinamento ---
94
+ # Para o modelo de classificação supervisionado (se ainda usar)
95
+ SUPERVISED_LEARNING_RATE = 0.0005
96
+ SUPERVISED_BATCH_SIZE = 128
97
+ SUPERVISED_EPOCHS = 100
98
+ LEARNING_RATE=0.0005
99
+
100
+ # Para o agente RL (PPO)
101
+ PPO_LEARNING_RATE = 0.0003 # Padrão do SB3 PPO, pode ajustar
102
+ PPO_N_STEPS = 2048
103
+ PPO_BATCH_SIZE_RL = 64 # Mini-batch size do PPO
104
+ PPO_ENT_COEF = 0.01
105
+ PPO_TOTAL_TIMESTEPS = 1000000 # Comece com menos para teste (ex: 50k-100k)
106
+
107
+ # --- Parâmetros do Ambiente RL ---
108
+ # RISK_FREE_RATE_ANNUAL = 0.02 # Taxa livre de risco anual (ex: 2%)
109
+ # REWARD_WINDOW_SHARPE = 252 * 1 # Ex: Janela de 1 ano de dados horários para Sharpe (252 dias * 24h)
110
+ # Ou uma janela menor como 60 ou 120 passos.
111
+ INITIAL_BALANCE = 100000
112
+ TRANSACTION_COST_PCT = 0.001 # 0.1%
113
+
114
+ # --- Caminhos para Salvar ---
115
+ MODEL_ROOT_DIR = "app/model" # Diretório raiz para todos os modelos e scalers
116
+ # Para modelo de classificação supervisionado (se mantiver)
117
+ SUPERVISED_MODEL_NAME = "classification_model.h5"
118
+ SUPERVISED_PV_SCALER_NAME = "supervisor_pv_scaler.joblib"
119
+ SUPERVISED_IND_SCALER_NAME = "supervisor_ind_scaler.joblib"
120
+ # Para modelo RL (agente PPO salvo pelo SB3)
121
+ RL_AGENT_MODEL_NAME = "ppo_deep_portfolio_agent" # SB3 adiciona .zip
122
+ # Scalers usados para preparar dados para o DeepPortfolioAgentNetwork (que é o extrator do RL)
123
+ RL_PV_SCALER_NAME = "rl_price_volume_atr_norm_scaler.joblib" # Seus nomes descritivos
124
+ RL_INDICATOR_SCALER_NAME = "rl_other_indicators_scaler.joblib"
125
+ FINAL_DENSE_UNITS1_EXTRACTOR=DEFAULT_EXTRACTOR_KWARGS
126
+ USE_SENTIMENT_CONFIG=True
agents/custom_policies.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rnn/agents/custom_policies.py (NOVO ARQUIVO, ou adicione ao deep_portfolio.py)
2
+
3
+ import gymnasium as gym # Usar gymnasium
4
+ import tensorflow as tf
5
+ from stable_baselines3.common.torch_layers import BaseFeaturesExtractor as PyTorchBaseFeaturesExtractor
6
+
7
+ from stable_baselines3.common.torch_layers import MlpExtractor
8
+ import torch.nn as nn
9
+ import torch
10
+
11
+ class CustomTFMlpExtractor(tf.keras.layers.Layer):
12
+ def __init__(self, feature_dim, net_arch, activation_fn=tf.nn.relu):
13
+ super().__init__()
14
+ self.net = tf.keras.Sequential()
15
+ for units in net_arch:
16
+ self.net.add(tf.keras.layers.Dense(units))
17
+ self.net.add(tf.keras.layers.Activation(activation_fn))
18
+
19
+ def call(self, inputs, training=False):
20
+ return self.net(inputs, training=training)
21
+
22
+
23
+
24
+ class CustomMlpExtractor(MlpExtractor):
25
+ def __init__(self, input_dim, net_arch, activation_fn, device):
26
+ super().__init__(input_dim, net_arch, activation_fn, device)
27
+
28
+ def forward(self, features):
29
+ for layer in self.policy_net:
30
+ if isinstance(layer, nn.ReLU):
31
+ features = layer(features) # Passando 'features' como argumento
32
+ else:
33
+ features = layer(features)
34
+ return features
35
+ # Para TensorFlow, precisamos de um extrator de features compatível ou construir a política de forma diferente.
36
+ # Stable Baselines3 tem melhor suporte nativo para PyTorch. Para TF, é um pouco mais manual.
37
+ # VAMOS USAR A ABORDAGEM DE POLÍTICA CUSTOMIZADA COM TF DIRETAMENTE.
38
+ from stable_baselines3.common.policies import ActorCriticPolicy
39
+ from typing import List, Dict, Any, Optional, Union, Type
40
+ # Importar sua rede e configs
41
+ #import agents.DeepPortfolioAgent as DeepPortfolioAgent
42
+ from portfolio_features_extractor_torch import PortfolioFeaturesExtractorTorch
43
+ # from ..config import (NUM_ASSETS, WINDOW_SIZE, NUM_FEATURES_PER_ASSET, ...) # Importe do seu config real
44
+ # VALORES DE EXEMPLO (PEGUE DO SEU CONFIG.PY REAL)
45
+ NUM_ASSETS_POLICY = 4
46
+ WINDOW_SIZE_POLICY = 60
47
+ NUM_FEATURES_PER_ASSET_POLICY = 26
48
+ # Hiperparâmetros para DeepPortfolioAgentNetwork quando usada como extrator
49
+ ASSET_CNN_FILTERS1_POLICY = 32
50
+ ASSET_CNN_FILTERS2_POLICY = 64
51
+ ASSET_LSTM_UNITS1_POLICY = 64
52
+ ASSET_LSTM_UNITS2_POLICY = 32 # Esta será a dimensão das features latentes para ator/crítico
53
+ ASSET_DROPOUT_POLICY = 0.2
54
+ MHA_NUM_HEADS_POLICY = 4
55
+ MHA_KEY_DIM_DIVISOR_POLICY = 2 # Para key_dim = 32 // 2 = 16
56
+ FINAL_DENSE_UNITS1_POLICY = 128
57
+ FINAL_DENSE_UNITS2_POLICY = ASSET_LSTM_UNITS2_POLICY # A saída da dense2 SÃO as features latentes
58
+ FINAL_DROPOUT_POLICY = 0.3
59
+
60
+
61
+
62
+
63
+ class CustomPolicy(ActorCriticPolicy):
64
+ def __init__(self, *args, **kwargs):
65
+ super().__init__(*args, **kwargs)
66
+ self.mlp_extractor = CustomTFMlpExtractor(
67
+ input_dim=self.observation_space.shape[0],
68
+ net_arch=[64, 64], # Exemplo de arquitetura
69
+ activation_fn=nn.ReLU,
70
+ device=self.device
71
+ )
72
+
73
+ from torch import nn
74
+
75
+ class CustomPortfolioPolicySB3(ActorCriticPolicy):
76
+ def __init__(
77
+ self,
78
+ observation_space: gym.spaces.Space,
79
+ action_space: gym.spaces.Space,
80
+ lr_schedule,
81
+ net_arch: Optional[List[Union[int, Dict[str, List[int]]]]] = None,
82
+ activation_fn: Type[nn.Module] = nn.ReLU, # Use PyTorch ReLU
83
+ features_extractor_kwargs: Optional[Dict[str, Any]] = None,
84
+ **kwargs,
85
+ ):
86
+ if features_extractor_kwargs is None:
87
+ features_extractor_kwargs = {}
88
+ features_extractor_kwargs.setdefault("features_dim", ASSET_LSTM_UNITS2_POLICY)
89
+
90
+ super().__init__(
91
+ observation_space,
92
+ action_space,
93
+ lr_schedule,
94
+ net_arch=net_arch,
95
+ activation_fn=activation_fn, # Passando nn.ReLU
96
+ features_extractor_class=PortfolioFeaturesExtractorTorch,
97
+ features_extractor_kwargs=features_extractor_kwargs,
98
+ **kwargs,
99
+ )
agents/data_handler_multi_asset.py ADDED
@@ -0,0 +1,448 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rnn/data_handler_multi_asset.py (NOVO ARQUIVO)
2
+
3
+
4
+ import pandas as pd
5
+ import numpy as np
6
+ import yfinance as yf # Ou ccxt, dependendo da sua preferência de fonte de dados
7
+ import pandas_ta as ta
8
+ from datetime import datetime, timedelta, timezone
9
+ from typing import List, Dict, Optional
10
+
11
+ WINDOW_SIZE = 60
12
+
13
+ # Importar do seu config.py
14
+ # Assumindo que config.py está em ../config.py ou rnn/config.py
15
+ # Ajuste o import conforme sua estrutura.
16
+ # Se train_rnn_model.py e este data_handler estiverem na mesma pasta 'scripts',
17
+ # e config.py estiver um nível acima:
18
+ # from ..app/config.py import (
19
+ # MULTI_ASSET_LIST, TIMEFRAME, DAYS_OF_DATA_TO_FETCH,
20
+ # # etc. para features a serem calculadas
21
+ # )
22
+ # Por agora, vamos definir aqui para exemplo:
23
+
24
+ # EXEMPLO DE CONFIGURAÇÃO (Mova para config.py depois)
25
+ MULTI_ASSET_SYMBOLS = {
26
+ 'eth': 'ETH-USD',
27
+ 'btc': 'BTC-USD',
28
+ 'ada': 'ADA-USD',
29
+ 'sol': 'SOL-USD'
30
+ } # Use os tickers corretos para yfinance ou ccxt
31
+ TIMEFRAME_YFINANCE = '1h' # yfinance suporta '1m', '2m', '5m', '15m', '30m', '60m', '90m', '1h', '1d', '5d', '1wk', '1mo', '3mo'
32
+ # Para '1h', yfinance só retorna os últimos 730 dias. Para mais dados, use '1d'.
33
+ # Se usar ccxt, TIMEFRAME = '1h' como antes.
34
+ DAYS_TO_FETCH = 365 * 2 # 2 anos
35
+
36
+ # Lista das features base que você quer calcular para CADA ativo
37
+ # (as 19 que definimos antes)
38
+ INDIVIDUAL_ASSET_BASE_FEATURES = [
39
+ 'open', 'high', 'low', 'close', 'volume', # OHLCV originais são necessários para os cálculos
40
+ 'sma_10', 'rsi_14', 'macd', 'macds', 'atr', 'bbp', 'cci_37', 'mfi_37', 'adx_14',
41
+ 'volume_zscore', 'body_size', 'body_size_norm_atr', 'body_vs_avg_body',
42
+ 'log_return', 'buy_condition_v1',
43
+ 'open_div_atr', 'high_div_atr',
44
+ 'low_div_atr',
45
+ 'close_div_atr',
46
+ 'volume_div_atr',
47
+ 'sma_10_div_atr' # 'sma_50' é calculada dentro de buy_condition_v1
48
+ # As colunas _div_atr serão criadas a partir destas
49
+ ]
50
+
51
+ # Features que serão normalizadas pelo ATR
52
+ COLS_TO_NORM_BY_ATR = ['open', 'high', 'low', 'close', 'volume', 'sma_10', 'macd', 'body_size']
53
+ #----------------------------------
54
+ # -- New get multi asset data for rl
55
+ # ... (imports e outras funções como antes) ...
56
+
57
+ def get_multi_asset_data_for_rl(
58
+ asset_symbols_map: Dict[str, str],
59
+ timeframe_yf: str,
60
+ days_to_fetch: int,
61
+ logger_instance # Adicionar logger como parâmetro
62
+ ) -> Optional[pd.DataFrame]: # Adicionar logger como parâmetro
63
+
64
+ all_asset_features_list: List[pd.DataFrame] = [] # Tipagem para clareza
65
+ min_data_length = float('inf')
66
+ print(all_asset_features_list)
67
+
68
+ #logger_instance.info(f"Iniciando get_multi_asset_data_for_rl para: {list(asset_symbols_map.keys())}")
69
+
70
+ for asset_key, yf_ticker in asset_symbols_map.items():
71
+ #logger_instance.info(f"\n--- Processando {asset_key} ({yf_ticker}) ---")
72
+ # ... (lógica de fetch_single_asset_ohlcv_yf como antes) ...
73
+ 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
74
+
75
+ # if single_asset_ohlcv.empty:
76
+ # logger_instance.warning(f"Sem dados OHLCV para {yf_ticker}, pulando.")
77
+ # continue
78
+
79
+ single_asset_features = calculate_all_features_for_single_asset(single_asset_ohlcv)#, logger_instance)
80
+
81
+ if single_asset_features is None or single_asset_features.empty:
82
+ logger_instance.warning(f"Sem features calculadas para {yf_ticker}, pulando.")
83
+ continue
84
+
85
+ #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()}")
86
+
87
+ # Garantir que o índice é DatetimeIndex e UTC para todos antes de adicionar prefixo e à lista
88
+ if not isinstance(single_asset_features.index, pd.DatetimeIndex):
89
+ single_asset_features.index = pd.to_datetime(single_asset_features.index)
90
+ if single_asset_features.index.tz is None:
91
+ single_asset_features.index = single_asset_features.index.tz_localize('UTC')
92
+ else:
93
+ single_asset_features.index = single_asset_features.index.tz_convert('UTC')
94
+
95
+ single_asset_features = single_asset_features.add_prefix(f"{asset_key}_")
96
+ all_asset_features_list.append(single_asset_features)
97
+ min_data_length = min(min_data_length, len(single_asset_features))
98
+
99
+ if not all_asset_features_list:
100
+ logger_instance.error("Nenhum DataFrame de feature de ativo foi adicionado à lista.")
101
+ return None
102
+
103
+ if min_data_length == float('inf') or min_data_length < WINDOW_SIZE: # Adicionada checagem de WINDOW_SIZE
104
+ 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.")
105
+ return None
106
+
107
+ #logger_instance.info(f"Menor número de linhas de dados encontrado (min_data_length): {min_data_length}")
108
+
109
+ # Truncar para garantir que todos os DFs tenham o mesmo comprimento ANTES do concat
110
+ # E que tenham pelo menos min_data_length.
111
+ # É importante que os ÍNDICES de data/hora se sobreponham para o join='inner' funcionar.
112
+ # Apenas pegar o .tail() pode não alinhar os timestamps se os DFs tiverem começos diferentes.
113
+
114
+ # Melhor abordagem: encontrar o índice comum mais recente e o mais antigo.
115
+ if not all_asset_features_list: # Checagem se a lista não ficou vazia por algum motivo
116
+ logger_instance.error("all_asset_features_list está vazia antes do alinhamento de índice.")
117
+ return None
118
+ print(all_asset_features_list)
119
+ # Alinhar DataFrames por um índice comum antes de concatenar
120
+ # 1. Encontrar o primeiro timestamp comum a todos
121
+ # 2. Encontrar o último timestamp comum a todos
122
+ # Ou, mais simples, confiar no join='inner' do concat, mas garantir que os DFs são válidos.
123
+
124
+ # Vamos simplificar e manter o truncamento pelo tail, mas com mais logs
125
+ # e garantir que são DataFrames.
126
+
127
+ truncated_asset_features_list = []
128
+ for i, df_asset in enumerate(all_asset_features_list):
129
+ if isinstance(df_asset, pd.DataFrame) and len(df_asset) >= min_data_length:
130
+ truncated_df = df_asset.tail(min_data_length)
131
+ #logger_instance.info(f" DF truncado {i} ({df_asset.columns[0].split('_')[0]}): shape {truncated_df.shape}, ")
132
+ #f"Index Min: {truncated_df.index.min()}, Max: {truncated_df.index.max()}")
133
+ truncated_asset_features_list.append(truncated_df)
134
+ else:
135
+ asset_name_debug = df_asset.columns[0].split('_')[0] if isinstance(df_asset, pd.DataFrame) and not df_asset.empty else f"DF_{i}"
136
+ 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.")
137
+
138
+ if not truncated_asset_features_list:
139
+ #ogger_instance.error("Nenhum DataFrame válido restou após truncamento para concatenar.")
140
+ return None
141
+
142
+ # Se houver apenas UM DataFrame na lista, não precisa concatenar, apenas retorna ele.
143
+ if len(truncated_asset_features_list) == 1:
144
+ #logger_instance.info("Apenas um DataFrame de ativo processado, retornando-o diretamente.")
145
+ combined_df = truncated_asset_features_list[0]
146
+ else:
147
+ #logger_instance.info(f"Concatenando {len(truncated_asset_features_list)} DataFrames de ativos com join='inner'...")
148
+ try:
149
+ combined_df = pd.concat(truncated_asset_features_list, axis=1, join='outer')
150
+ print(combined_df)
151
+ except Exception as e_concat:
152
+ logger_instance.error(f"ERRO CRÍTICO durante pd.concat: {e_concat}", exc_info=True)
153
+ return None
154
+
155
+
156
+ combined_df.fillna(method='ffill', inplace=True)
157
+ # Depois do ffill, ainda pode haver NaNs no início se algum ativo começar depois dos outros.
158
+ if not combined_df.empty:
159
+ #logger_instance.info(f"Shape após ffill: {combined_df.shape}. Buscando primeiro/último índice válido...")
160
+ first_valid_index = combined_df.first_valid_index()
161
+ last_valid_index = combined_df.last_valid_index()
162
+
163
+ if pd.isna(first_valid_index) or pd.isna(last_valid_index):
164
+ print("Não foi possível determinar first/last_valid_index após ffill.")
165
+ return None
166
+
167
+ print(f"Primeiro índice válido: {first_valid_index}, Último índice válido: {last_valid_index}")
168
+ combined_df = combined_df.loc[first_valid_index:last_valid_index]
169
+ print(f"Shape após fatiar por first/last valid index: {combined_df.shape}")
170
+
171
+ # Um dropna final pode ser necessário se o ffill não pegou tudo (improvável, mas seguro)
172
+ combined_df.dropna(inplace=True)
173
+ print(f"Shape após dropna final (pós-fatiamento): {combined_df.shape}")
174
+ print("Imprimindo DF_COMBINED com index ")
175
+ print(combined_df)
176
+
177
+ #logger_instance.info(f"Tipo de combined_df após concat: {type(combined_df)}")
178
+ if not isinstance(combined_df, pd.DataFrame):
179
+ logger_instance.error(f"combined_df NÃO é um DataFrame após concat. Tipo: {type(combined_df)}")
180
+ return None
181
+
182
+
183
+ return combined_df
184
+ # if combined_df.empty:
185
+
186
+ # logger_instance.error("DataFrame combinado está VAZIO após concatenação e join='inner'. "
187
+ # "Isso geralmente significa que não há timestamps comuns entre TODOS os ativos processados.")
188
+ # # Adicionar mais depuração aqui se isso acontecer:
189
+ # # for i, df_trunc_debug in enumerate(truncated_asset_features_list):
190
+ # # logger_instance.info(f"Debug DF {i} - Head:\n{df_trunc_debug.head(3)}")
191
+ # # logger_instance.info(f"Debug DF {i} - Tail:\n{df_trunc_debug.tail(3)}")
192
+ # # logger_instance.info(f"DataFrame combinado ANTES do dropna final, shape: {combined_df.shape}")
193
+ # print(combined_df.head()) # Descomente para depuração pesada
194
+ # return None
195
+
196
+
197
+
198
+
199
+ # ...
200
+ #----------------------------------
201
+
202
+ # def get_multi_asset_data_for_rl(
203
+ # asset_symbols_map: Dict[str, str],
204
+ # timeframe_yf: str,
205
+ # days_to_fetch: int
206
+ # ) -> Optional[pd.DataFrame]:
207
+ # """
208
+ # Busca, processa e combina dados de múltiplos ativos em um DataFrame achatado.
209
+ # """
210
+ # all_asset_features_list = []
211
+ # # min_data_length = float('inf') # Inicializar com um valor alto
212
+ # # Vamos inicializar com 0 e pegar o len do primeiro DF válido
213
+ # min_data_length = 0
214
+ # first_valid_df_processed = False
215
+
216
+ # for asset_key, yf_ticker in asset_symbols_map.items():
217
+ # print(f"\n--- Processando {asset_key} ({yf_ticker}) ---")
218
+ # period_yf = f"{days_to_fetch}d"
219
+ # if timeframe_yf == '1h' and days_to_fetch > 730:
220
+ # print(f"AVISO: Para {timeframe_yf}, buscando no máximo 730 dias com yfinance para {yf_ticker}.")
221
+ # period_yf = "730d"
222
+
223
+ # single_asset_ohlcv = fetch_single_asset_ohlcv_yf(yf_ticker, period=period_yf, interval=timeframe_yf)
224
+ # if single_asset_ohlcv.empty:
225
+ # print(f"AVISO: Sem dados OHLCV para {yf_ticker}, pulando este ativo.")
226
+ # continue
227
+
228
+ # single_asset_features = calculate_all_features_for_single_asset(single_asset_ohlcv) # Passar logger se tiver
229
+ # if single_asset_features is None or single_asset_features.empty:
230
+ # print(f"AVISO: Sem features calculadas para {yf_ticker}, pulando este ativo.")
231
+ # continue
232
+
233
+ # single_asset_features = single_asset_features.add_prefix(f"{asset_key}_")
234
+ # all_asset_features_list.append(single_asset_features)
235
+
236
+ # # Atualizar min_data_length de forma mais segura
237
+ # if not first_valid_df_processed:
238
+ # min_data_length = len(single_asset_features)
239
+ # first_valid_df_processed = True
240
+ # else:
241
+ # min_data_length = min(min_data_length, len(single_asset_features))
242
+
243
+ # if not all_asset_features_list: # Se nenhum ativo foi processado com sucesso
244
+ # print("ERRO: Nenhum dado de feature de ativo foi processado com sucesso para a lista `all_asset_features_list`.")
245
+ # return None # Retorna None, que não tem '.empty' mas será checado por 'is None'
246
+
247
+ # if min_data_length == 0 : # Checagem adicional se algo deu muito errado
248
+ # print("ERRO: min_data_length é zero após processar ativos, não é possível truncar DataFrames.")
249
+ # return None
250
+
251
+ # print(f"Menor número de linhas de dados encontrado entre os ativos (min_data_length): {min_data_length}")
252
+ # 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]
253
+
254
+ # # Verificar se a lista de DFs truncados não está vazia ANTES de concatenar
255
+ # if not truncated_asset_features_list:
256
+ # print("ERRO: Nenhum DataFrame válido restou após o truncamento. Não é possível concatenar.")
257
+ # return None
258
+
259
+ # print(f"Concatenando {len(truncated_asset_features_list)} DataFrames de ativos...")
260
+ # try:
261
+ # combined_df = pd.concat(truncated_asset_features_list, axis=1, join='inner')
262
+ # except Exception as e_concat:
263
+ # print(f"ERRO durante pd.concat: {e_concat}")
264
+ # return None # Retorna None em caso de erro na concatenação
265
+
266
+ # # Agora, combined_df DEVE ser um DataFrame (mesmo que vazio se o join falhar)
267
+ # if not isinstance(combined_df, pd.DataFrame):
268
+ # print(f"ERRO: pd.concat não retornou um DataFrame. Tipo retornado: {type(combined_df)}")
269
+ # return None
270
+
271
+ # if combined_df.empty: # Esta checagem agora deve funcionar ou ser desnecessária se o anterior já retornou None
272
+ # print("ERRO: DataFrame combinado está vazio após concatenação e join. Verifique os dados dos ativos e o alinhamento de datas.")
273
+ # return None
274
+
275
+ # combined_df.dropna(inplace=True)
276
+
277
+ # if combined_df.empty: # Checagem final após dropna
278
+ # print("ERRO: DataFrame combinado está vazio após dropna final.")
279
+ # return None
280
+
281
+ # print(f"\nDataFrame multi-ativo final gerado com shape: {combined_df.shape}")
282
+ # return combined_df
283
+
284
+
285
+
286
+
287
+ def fetch_single_asset_ohlcv_yf(ticker_symbol: str, period: str = "2y", interval: str = "1h") -> pd.DataFrame:
288
+ """ Adaptação da sua função fetch_historical_ohlcv de financial_data_agent.py """
289
+ print(f"Buscando dados para {ticker_symbol} com yfinance (period: {period}, interval: {interval})...")
290
+ try:
291
+ ticker = yf.Ticker(ticker_symbol)
292
+ # Para dados horários, o período máximo é geralmente 730 dias com yfinance
293
+ # Se precisar de mais, considere '1d' e depois reamostre, ou use ccxt para cripto.
294
+ if interval == '1h' and period.endswith('y') and int(period[:-1]) * 365 > 730:
295
+ print(f"AVISO: yfinance pode limitar dados horários a 730 dias. Buscando 'max' para {interval} e depois fatiando.")
296
+ data = ticker.history(interval=interval, period="730d") # Pega o máximo possível
297
+ elif interval == '1d' and period.endswith('y'):
298
+ data = ticker.history(period=period, interval=interval)
299
+ else: # Para períodos menores ou outros intervalos
300
+ data = ticker.history(period=period, interval=interval)
301
+
302
+ if data.empty:
303
+ print(f"Nenhum dado encontrado para {ticker_symbol}.")
304
+ return pd.DataFrame()
305
+
306
+ data.rename(columns={
307
+ "Open": "open", "High": "high", "Low": "low",
308
+ "Close": "close", "Volume": "volume", "Adj Close": "adj_close"
309
+ }, inplace=True)
310
+
311
+ # Selecionar apenas as colunas OHLCV e garantir que o índice é DatetimeIndex UTC
312
+ data = data[['open', 'high', 'low', 'close', 'volume']]
313
+ if data.index.tz is None:
314
+ data.index = data.index.tz_localize('UTC')
315
+ else:
316
+ data.index = data.index.tz_convert('UTC')
317
+
318
+ # Para dados horários, yfinance pode retornar dados do fim de semana (sem volume)
319
+ # e o último candle pode estar incompleto.
320
+ # if interval == '1h':
321
+ # data = data[data['volume'] > 0] # Remover candles sem volume
322
+ # data = data[:-1] # Remover o último candle que pode estar incompleto
323
+
324
+ print(f"Dados coletados para {ticker_symbol}: {len(data)} linhas.")
325
+ return data
326
+ except Exception as e:
327
+ print(f"Erro ao buscar dados para {ticker_symbol} com yfinance: {e}")
328
+ return pd.DataFrame()
329
+
330
+
331
+ def calculate_all_features_for_single_asset(ohlcv_df: pd.DataFrame) -> Optional[pd.DataFrame]:
332
+ """Calcula todas as features base para um único ativo."""
333
+ if ohlcv_df.empty: return None
334
+ df = ohlcv_df.copy()
335
+ print(f"Calculando features para ativo (shape inicial: {df.shape})...")
336
+
337
+ # GARANTIR que a coluna 'close' original será preservada no DataFrame final
338
+ if 'close' in df.columns:
339
+ df['close'] = df['close'] # redundante, mas deixa explícito que não será sobrescrita
340
+
341
+ if ta:
342
+ df.ta.sma(length=10, close='close', append=True, col_names=('sma_10',))
343
+ df.ta.rsi(length=14, close='close', append=True, col_names=('rsi_14',))
344
+ macd_out = df.ta.macd(close='close', append=False)
345
+ if macd_out is not None and not macd_out.empty:
346
+ df['macd'] = macd_out.iloc[:,0]
347
+ df['macds'] = macd_out.iloc[:,2] # Linha de sinal para buy_condition
348
+ df.ta.atr(length=14, append=True, col_names=('atr',))
349
+ df.ta.bbands(length=20, close='close', append=True, col_names=('bbl', 'bbm', 'bbu', 'bbb', 'bbp'))
350
+ df.ta.cci(length=37, append=True, col_names=('cci_37',))
351
+ df['volume'] = df['volume'].astype(float)
352
+ df.ta.mfi(length=37, close='close', high='high', low='low', volume='volume', append=True, col_names=('mfi_37',))
353
+ df.ta.mfi(length=37, append=True, col_names=('mfi_37',))
354
+ adx_out = df.ta.adx(length=14, append=False)
355
+ if adx_out is not None and not adx_out.empty:
356
+ df['adx_14'] = adx_out.iloc[:,0]
357
+
358
+ rolling_vol_mean = df['volume'].rolling(window=20).mean()
359
+ rolling_vol_std = df['volume'].rolling(window=20).std()
360
+ df['volume_zscore'] = (df['volume'] - rolling_vol_mean) / (rolling_vol_std + 1e-9)
361
+
362
+ df['body_size'] = abs(df['close'] - df['open'])
363
+
364
+ # ATR precisa existir para as próximas. Drop NaNs do ATR primeiro.
365
+ df.dropna(subset=['atr'], inplace=True)
366
+ df_atr_valid = df[df['atr'] > 1e-9].copy()
367
+ if df_atr_valid.empty:
368
+ print("AVISO: ATR inválido para todas as linhas restantes, features _div_atr e body_size_norm_atr podem ser todas NaN ou vazias.")
369
+ # Criar colunas com NaN para manter a estrutura
370
+ df['body_size_norm_atr'] = np.nan
371
+ for col in COLS_TO_NORM_BY_ATR:
372
+ df[f'{col}_div_atr'] = np.nan
373
+ else:
374
+ df['body_size_norm_atr'] = df['body_size'] / df['atr'] # ATR já filtrado para > 1e-9
375
+ for col in COLS_TO_NORM_BY_ATR:
376
+ if col in df.columns:
377
+ df[f'{col}_div_atr'] = df[col] / (df['atr'] + 1e-9) # Adicionar 1e-9 aqui também por segurança
378
+ else:
379
+ df[f'{col}_div_atr'] = np.nan
380
+
381
+
382
+ df['body_vs_avg_body'] = df['body_size'] / (df['body_size'].rolling(window=20).mean() + 1e-9)
383
+ df['log_return'] = np.log(df['close'] / df['close'].shift(1))
384
+
385
+ sma_50_series = df.ta.sma(length=50, close='close', append=False)
386
+ if sma_50_series is not None: df['sma_50'] = sma_50_series
387
+ else: df['sma_50'] = np.nan
388
+
389
+ if all(col in df.columns for col in ['macd', 'macds', 'rsi_14', 'close', 'sma_50']):
390
+ df['buy_condition_v1'] = ((df['macd'] > df['macds']) & (df['rsi_14'] > 50) & (df['close'] > df['sma_50'])).astype(int)
391
+ else:
392
+ df['buy_condition_v1'] = 0
393
+
394
+ # Selecionar apenas as colunas que realmente usaremos como features base para o modelo
395
+ # (incluindo as _div_atr e as originais que não foram normalizadas por ATR)
396
+ # Esta lista de features é a que será passada para os scalers no script de treino.
397
+ # E também as colunas que o rnn_predictor.py precisará ter antes de aplicar seus scalers.
398
+ # Esta lista deve vir do config.py (BASE_FEATURE_COLS)
399
+
400
+ # Exemplo:
401
+ # final_feature_columns = [
402
+ # 'open_div_atr', 'high_div_atr', 'low_div_atr', 'close_div_atr', 'volume_div_atr',
403
+ # 'log_return', 'rsi_14', 'atr', 'bbp', 'cci_37', 'mfi_37',
404
+ # 'body_size_norm_atr', 'body_vs_avg_body', 'macd', 'sma_10_div_atr',
405
+ # 'adx_14', 'volume_zscore', 'buy_condition_v1'
406
+ # ] # Esta é a BASE_FEATURE_COLS do seu config.py
407
+
408
+ # Verificar se todas as colunas em INDIVIDUAL_ASSET_BASE_FEATURES existem
409
+ # (INDIVIDUAL_ASSET_BASE_FEATURES deve ser igual a config.BASE_FEATURE_COLS)
410
+ current_feature_cols = [col for col in INDIVIDUAL_ASSET_BASE_FEATURES if col in df.columns]
411
+ missing_cols = [col for col in INDIVIDUAL_ASSET_BASE_FEATURES if col not in df.columns]
412
+ if missing_cols:
413
+ print(f"AVISO: Colunas de features ausentes após cálculo: {missing_cols}. Usando apenas as disponíveis: {current_feature_cols}")
414
+ # GARANTIR que 'close' (preço original) está presente nas features finais
415
+ if 'close' not in current_feature_cols and 'close' in df.columns:
416
+ current_feature_cols.append('close')
417
+
418
+ df_final_features = df[current_feature_cols].copy()
419
+ df_final_features.dropna(inplace=True)
420
+ print(f"Features calculadas. Shape após dropna: {df_final_features.shape}. Colunas: {df_final_features.columns.tolist()}")
421
+ return df_final_features
422
+ else:
423
+ print("pandas_ta não está disponível.")
424
+ return None
425
+
426
+
427
+ if __name__ == '__main__':
428
+ print("Testando data_handler_multi_asset.py...")
429
+ # Substitua pelos tickers yfinance reais que você quer usar
430
+ test_assets = {
431
+ 'eth': 'ETH-USD',
432
+ 'btc': 'BTC-USD',
433
+ # 'aapl': 'AAPL' # Exemplo de ação
434
+ }
435
+ multi_asset_data = get_multi_asset_data_for_rl(
436
+ test_assets,
437
+ timeframe_yf='1h', # Para teste rápido, período menor
438
+ days_to_fetch=90 # Para teste rápido, período menor
439
+ )
440
+
441
+ if multi_asset_data is not None and not multi_asset_data.empty:
442
+ print("\n--- Exemplo do DataFrame Multi-Ativo Gerado ---")
443
+ print(multi_asset_data.head())
444
+ print(f"\nShape: {multi_asset_data.shape}")
445
+ print(f"\nInfo:")
446
+ multi_asset_data.info()
447
+ else:
448
+ print("\nFalha ao gerar DataFrame multi-ativo.")
agents/dataset_update_agent.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # agents/dataset_update_agent.py
2
+ import pandas as pd
3
+ from datetime import datetime
4
+
5
+ def update_dataset(path="data/sp500_news.csv", new_data=pd.DataFrame()):
6
+ df = pd.read_csv(path)
7
+ combined = pd.concat([df, new_data]).drop_duplicates().reset_index(drop=True)
8
+ combined.to_csv(path, index=False)
9
+ print(f"Dataset atualizado: {path}")
agents/deep_portfolio.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import tensorflow as tf
3
+ from tensorflow.keras.layers import LSTM, Dense, Conv1D, MultiHeadAttention
4
+ from transformers import AutoTokenizer, TFAutoModelForSequenceClassification # Changed here
5
+ import gymnasium as gym
6
+ from gymnasium import spaces
7
+
8
+ class DeepPortfolioAI(tf.keras.Model):
9
+ def __init__(self, num_assets, sequence_length=60):
10
+ super(DeepPortfolioAI, self).__init__()
11
+
12
+ # Parâmetros do modelo
13
+ self.num_assets = num_assets
14
+ self.sequence_length = sequence_length
15
+
16
+ # CNN para análise de padrões técnicos
17
+ self.conv1 = Conv1D(64, 3, activation='relu')
18
+ self.conv2 = Conv1D(128, 3, activation='relu')
19
+
20
+ # LSTM para análise temporal
21
+ self.lstm1 = LSTM(128, return_sequences=True)
22
+ self.lstm2 = LSTM(64)
23
+
24
+ # Attention para correlações entre ativos
25
+ self.attention = MultiHeadAttention(num_heads=8, key_dim=64)
26
+
27
+ # Camadas densas para decisão final
28
+ self.dense1 = Dense(256, activation='relu')
29
+ self.dense2 = Dense(128, activation='relu')
30
+ self.output_layer = Dense(num_assets, activation='softmax')
31
+
32
+ # Inicializar tokenizer e modelo de sentimento
33
+ self.tokenizer = AutoTokenizer.from_pretrained('ProsusAI/finbert')
34
+ self.sentiment_model = TFAutoModelForSequenceClassification.from_pretrained('ProsusAI/finbert') # Changed here
35
+
36
+ def call(self, inputs):
37
+ market_data, news_data = inputs
38
+
39
+ # Análise técnica com CNN
40
+ x_technical = self.conv1(market_data)
41
+ x_technical = self.conv2(x_technical)
42
+
43
+ # Análise temporal com LSTM
44
+ x_temporal = self.lstm1(x_technical)
45
+ x_temporal = self.lstm2(x_temporal)
46
+
47
+ # Attention para correlações
48
+ x_attention = self.attention(x_temporal, x_temporal, x_temporal)
49
+
50
+ # Combinar com análise de sentimento
51
+ sentiment_embeddings = self._process_news(news_data)
52
+ x_combined = tf.concat([x_attention, sentiment_embeddings], axis=-1)
53
+
54
+ # Camadas densas finais
55
+ x = self.dense1(x_combined)
56
+ x = self.dense2(x)
57
+ return self.output_layer(x)
58
+
59
+ def _process_news(self, news_data):
60
+ inputs = self.tokenizer(news_data, return_tensors="pt", padding=True, truncation=True)
61
+ sentiment_scores = self.sentiment_model(**inputs).logits
62
+ return tf.convert_to_tensor(sentiment_scores.detach().numpy())
63
+
64
+ class PortfolioEnvironment(gym.Env):
65
+ def __init__(self, data, initial_balance=100000):
66
+ super(PortfolioEnvironment, self).__init__()
67
+
68
+ self.data = data
69
+ self.initial_balance = initial_balance
70
+ self.current_step = 0
71
+
72
+ # Define espaços de ação e observação
73
+ self.action_space = spaces.Box(
74
+ low=0, high=1, shape=(len(data.columns),), dtype=np.float32)
75
+ self.observation_space = spaces.Box(
76
+ low=-np.inf, high=np.inf, shape=(60, len(data.columns)), dtype=np.float32)
77
+
78
+ def reset(self):
79
+ self.current_step = 0
80
+ self.balance = self.initial_balance
81
+ self.portfolio = np.zeros(len(self.data.columns))
82
+ return self._get_observation()
83
+
84
+ def step(self, action):
85
+ # Implementar lógica de negociação
86
+ current_prices = self.data.iloc[self.current_step]
87
+ next_prices = self.data.iloc[self.current_step + 1]
88
+
89
+ # Calcular retorno
90
+ returns = (next_prices - current_prices) / current_prices
91
+ reward = np.sum(action * returns)
92
+
93
+ # Atualizar portfolio
94
+ self.portfolio = action
95
+ self.balance *= (1 + reward)
96
+
97
+ # Incrementar step
98
+ self.current_step += 1
99
+ done = self.current_step >= len(self.data) - 1
100
+
101
+ return self._get_observation(), reward, done, {}
102
+
103
+ def _get_observation(self):
104
+ return self.data.iloc[self.current_step-60:self.current_step].values
agents/deep_portfolio_torch.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+
5
+ class SingleAssetProcessor(nn.Module):
6
+ def __init__(self, sequence_length, num_features_per_asset, cnn_filters1, cnn_filters2, lstm_units):
7
+ super().__init__()
8
+ self.conv1 = nn.Conv1d(num_features_per_asset, cnn_filters1, kernel_size=3, padding=1)
9
+ self.conv2 = nn.Conv1d(cnn_filters1, cnn_filters2, kernel_size=3, padding=1)
10
+ self.lstm = nn.LSTM(input_size=cnn_filters2, hidden_size=lstm_units, batch_first=True)
11
+
12
+ def forward(self, x):
13
+ # x: (batch, seq_len, num_features_per_asset)
14
+ x = x.transpose(1, 2) # (batch, num_features_per_asset, seq_len)
15
+ x = F.relu(self.conv1(x))
16
+ x = F.relu(self.conv2(x))
17
+ x = x.transpose(1, 2) # (batch, seq_len, cnn_filters2)
18
+ _, (h_n, _) = self.lstm(x) # h_n: (1, batch, lstm_units)
19
+ return h_n.squeeze(0) # (batch, lstm_units)
20
+
21
+ class DeepPortfolioAgentNetworkTorch(nn.Module):
22
+ def __init__(self, num_assets, sequence_length, num_features_per_asset,
23
+ asset_cnn_filters1=32, asset_cnn_filters2=64,
24
+ asset_lstm_units1=64, asset_lstm_units2=32,
25
+ final_dense_units1=128, final_dense_units2=32,
26
+ final_dropout=0.3, mha_num_heads=4, mha_key_dim_divisor=2,
27
+ output_latent_features=True, use_sentiment_analysis=False):
28
+ super().__init__()
29
+ self.num_assets = num_assets
30
+ self.sequence_length = sequence_length
31
+ self.num_features_per_asset = num_features_per_asset
32
+ self.output_latent_features = output_latent_features
33
+ self.use_sentiment_analysis = use_sentiment_analysis
34
+
35
+ # Processador individual de ativos
36
+ self.asset_processor = SingleAssetProcessor(
37
+ sequence_length, num_features_per_asset,
38
+ asset_cnn_filters1, asset_cnn_filters2, asset_lstm_units1
39
+ )
40
+ # Atenção multi-cabeça
41
+ self.attention = nn.MultiheadAttention(
42
+ embed_dim=asset_lstm_units1,
43
+ num_heads=mha_num_heads,
44
+ batch_first=True
45
+ )
46
+ # Pooling global
47
+ self.global_avg_pool = nn.AdaptiveAvgPool1d(1)
48
+ # Camadas densas finais
49
+ self.dense1 = nn.Linear(asset_lstm_units1, final_dense_units1)
50
+ self.dropout1 = nn.Dropout(final_dropout)
51
+ self.dense2 = nn.Linear(final_dense_units1, final_dense_units2)
52
+ self.dropout2 = nn.Dropout(final_dropout)
53
+ self.output_allocation = nn.Linear(final_dense_units2, num_assets)
54
+
55
+ def forward(self, x):
56
+ # x: (batch, seq_len, num_assets * num_features_per_asset)
57
+ batch_size = x.size(0)
58
+ # Separar cada ativo
59
+ asset_representations = []
60
+ for i in range(self.num_assets):
61
+ start = i * self.num_features_per_asset
62
+ end = (i + 1) * self.num_features_per_asset
63
+ asset_data = x[:, :, start:end] # (batch, seq_len, num_features_per_asset)
64
+ asset_repr = self.asset_processor(asset_data) # (batch, lstm_units1)
65
+ asset_representations.append(asset_repr)
66
+ # Empilhar ativos: (batch, num_assets, lstm_units1)
67
+ stacked = torch.stack(asset_representations, dim=1)
68
+ # Atenção multi-cabeça
69
+ attn_output, _ = self.attention(stacked, stacked, stacked)
70
+ # Pooling global sobre ativos (num_assets)
71
+ pooled = self.global_avg_pool(attn_output.transpose(1,2)).squeeze(-1) # (batch, lstm_units1)
72
+ # Camadas densas finais
73
+ x = F.relu(self.dense1(pooled))
74
+ x = self.dropout1(x)
75
+ x = F.relu(self.dense2(x))
76
+ x = self.dropout2(x)
77
+ if self.output_latent_features:
78
+ return x # (batch, final_dense_units2)
79
+ else:
80
+ return F.softmax(self.output_allocation(x), dim=-1) # (batch, num_assets)
agents/financial_data_agent.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # agents/financial_data_agent.py
2
+ import yfinance as yf
3
+ import pandas as pd
4
+ from agno.agent import Agent # Commented out
5
+ from agno.models.anthropic import Claude # Commented out
6
+ from agno.tools.yfinance import YFinanceTools # Commented out
7
+
8
+ def get_stock_report(ticker="NVDA"): # Commented out
9
+ agent = Agent(
10
+ model=Claude(id="claude-3-7-sonnet-latest"),
11
+ tools=[
12
+ YFinanceTools(
13
+ stock_price=True,
14
+ analyst_recommendations=True,
15
+ company_info=True,
16
+ company_news=True,
17
+ )
18
+ ],
19
+ instructions=[
20
+ "Use tables to display data",
21
+ "Only output the report, no other text",
22
+ ],
23
+ markdown=True,
24
+ )
25
+ return agent.get_response(f"Write a financial report on {ticker}")
26
+
27
+ def fetch_historical_ohlcv(ticker_symbol: str, period: str = "1y", interval: str = "1d") -> pd.DataFrame:
28
+ """
29
+ Fetches historical OHLCV data for a given ticker symbol.
30
+
31
+ Args:
32
+ ticker_symbol (str): The stock ticker symbol (e.g., "AAPL" for Apple on NASDAQ,
33
+ "PETR4.SA" for Petrobras on B3, "000001.SS" for SSE Composite Index).
34
+ period (str): The period for which to download data (e.g., "1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "ytd", "max").
35
+ interval (str): The interval of data points (e.g., "1m", "2m", "5m", "15m", "30m", "60m", "90m", "1h", "1d", "5d", "1wk", "1mo", "3mo").
36
+
37
+ Returns:
38
+ pd.DataFrame: A pandas DataFrame containing the OHLCV data, or an empty DataFrame if an error occurs.
39
+ """
40
+ try:
41
+ ticker = yf.Ticker(ticker_symbol)
42
+ data = ticker.history(period=period, interval=interval)
43
+ if data.empty:
44
+ print(f"No data found for {ticker_symbol} for the given period/interval.")
45
+ return pd.DataFrame()
46
+ # Ensure column names are consistent (Yahoo Finance sometimes uses 'Adj Close')
47
+ data.rename(columns={"Adj Close": "Adj_Close"}, inplace=True)
48
+ return data
49
+ except Exception as e:
50
+ print(f"Error fetching data for {ticker_symbol}: {e}")
51
+ return pd.DataFrame()
52
+
53
+ if __name__ == '__main__':
54
+ # Example usage:
55
+ # NASDAQ
56
+ aapl_data = fetch_historical_ohlcv("AAPL", period="1mo", interval="1d")
57
+ if not aapl_data.empty:
58
+ print("\nAAPL Data (NASDAQ):")
59
+ print(aapl_data.head())
60
+
61
+ # B3 (Brazilian Stock Exchange) - Example: Petrobras
62
+ petr4_data = fetch_historical_ohlcv("PETR4.SA", period="1mo", interval="1d")
63
+ if not petr4_data.empty:
64
+ print("\nPETR4.SA Data (B3):")
65
+ print(petr4_data.head())
66
+
67
+ # Asian Market - Example: Samsung Electronics (Korea Exchange)
68
+ samsung_data = fetch_historical_ohlcv("005930.KS", period="1mo", interval="1d")
69
+ if not samsung_data.empty:
70
+ print("\n005930.KS Data (Samsung - KRX):")
71
+ print(samsung_data.head())
72
+
73
+ # Example for a non-existent ticker or error
74
+ error_data = fetch_historical_ohlcv("NONEXISTENTTICKER", period="1d")
75
+ if error_data.empty:
76
+ print("\nSuccessfully handled non-existent ticker.")
agents/investment_agent.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # agents/investment_agent.py
2
+ def execute_investment(prediction, threshold=0.8):
3
+ if prediction > threshold:
4
+ print("Comprar ativos com probabilidade:", prediction)
5
+ # chamada API para execução real
6
+ else:
7
+ print("Não investir. Probabilidade baixa:", prediction)
agents/portfolio_environment.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # agents/portfolio_environment.py (ou atcoin_env.py)
2
+
3
+ from typing import List
4
+ import numpy as np
5
+ import pandas as pd
6
+ import gymnasium as gym
7
+ from gymnasium import spaces
8
+ from collections import deque
9
+
10
+ # Importar do config.py
11
+ # from ..config import WINDOW_SIZE # Ajuste o import
12
+ WINDOW_SIZE_ENV = 60 # Exemplo, pegue do config
13
+ NUM_ASSETS=4
14
+ WINDOW_SIZE=60
15
+
16
+ NUM_FEATURES_PER_ASSET=26
17
+
18
+
19
+
20
+
21
+
22
+
23
+ class PortfolioEnv(gym.Env): # Renomeado para seguir convenção de Gymnasium (Opcional)
24
+ metadata = {'render_modes': ['human'], 'render_fps': 30}
25
+
26
+ def __init__(self, df_multi_asset_features: pd.DataFrame,
27
+ asset_symbols_list: List[str], # Lista de chaves dos ativos ex: ['crypto_eth', 'stock_aapl']
28
+ initial_balance=100000,
29
+ window_size=WINDOW_SIZE_ENV,
30
+ transaction_cost_pct=0.001,
31
+ reward_window_size=60, # Janela para cálculo do Sharpe Ratio (ex: 60 passos/horas)
32
+ risk_free_rate_per_step=None): # Custo de transação de 0.1%
33
+ super(PortfolioEnv, self).__init__()
34
+
35
+ self.df = df_multi_asset_features.copy() # DataFrame ACHATADO com todas as features de todos os ativos
36
+ self.asset_keys = asset_symbols_list # Usado para identificar colunas de preço de fechamento
37
+ self.num_assets = len(asset_symbols_list)
38
+ self.initial_balance = initial_balance
39
+ self.window_size = window_size
40
+ self.transaction_cost_pct = transaction_cost_pct
41
+ self.reward_window_size = reward_window_size
42
+ # Cálculo automático da taxa livre de risco por passo se não for passada
43
+ RISK_FREE_RATE_ANNUAL = 0.02 # 2% ao ano
44
+ TRADING_DAYS_PER_YEAR = 252
45
+ HOURS_PER_DAY_TRADING = 24
46
+ if risk_free_rate_per_step is None:
47
+ self.risk_free_rate_per_step = RISK_FREE_RATE_ANNUAL / (TRADING_DAYS_PER_YEAR * HOURS_PER_DAY_TRADING)
48
+ else:
49
+ self.risk_free_rate_per_step = risk_free_rate_per_step
50
+ self.portfolio_returns_history = deque(maxlen=self.reward_window_size) # Armazena retorenos do portifólio por passos
51
+
52
+ self.current_step = 0
53
+ self.balance = self.initial_balance
54
+ self.portfolio_weights = np.full(self.num_assets, 1.0 / self.num_assets if self.num_assets > 0 else 0) # Pesos iniciais iguais
55
+ self.portfolio_value = self.initial_balance
56
+ self.total_steps = len(self.df) - self.window_size -2 # -1 para ter um next_prices
57
+
58
+ # Espaço de Ação: pesos do portfólio para cada ativo (devem somar 1, via Softmax da rede)
59
+ # A rede neural vai outputar pesos que somam 1 (softmax).
60
+ self.action_space = spaces.Box(low=0, high=1, shape=(NUM_ASSETS,), dtype=np.float32)
61
+
62
+ # Espaço de Observação: janela de N features para M ativos (achatado)
63
+ # O número de colunas no df é num_assets * num_features_per_asset
64
+ num_total_features = self.df.shape[1]
65
+ self.observation_space = spaces.Box(
66
+ low=-np.inf, high=np.inf,
67
+ shape=(WINDOW_SIZE, NUM_ASSETS * NUM_FEATURES_PER_ASSET),
68
+ dtype=np.float32
69
+ )
70
+
71
+
72
+
73
+
74
+ self.current_prices_cols = [f"{key}_close" for key in self.asset_keys] # Assumindo que 'close' é uma das features base
75
+ # Se você usa 'close_div_atr', então seria f"{key}_close_div_atr"
76
+ # É importante ter uma coluna de preço de fechamento *original* (não escalada, não normalizada por ATR)
77
+ # para calcular os retornos reais do portfólio. Se não estiver no df, precisará ser adicionada/mantida.
78
+ # Por agora, vamos assumir que o df passado já tem as colunas de preço de fechamento originais,
79
+ # ou você precisará de um df separado só com os preços para o cálculo de retorno.
80
+ # VOU ASSUMIR QUE VOCÊ ADICIONA COLUNAS DE PREÇO DE FECHAMENTO ORIGINAIS AO `df_multi_asset_features`
81
+ # com nomes como `eth_orig_close`, `ada_orig_close` etc.
82
+ self.orig_close_price_cols = [f"{asset_prefix}_close" for asset_prefix in self.asset_keys] # Ex: 'crypto_eth_close'
83
+
84
+ # Verificar se as colunas de preço de fechamento original existem
85
+ missing_price_cols = [col for col in self.orig_close_price_cols if col not in self.df.columns]
86
+ if missing_price_cols:
87
+ raise ValueError(f"Colunas de preço de fechamento original ausentes no DataFrame do ambiente: {missing_price_cols}. "
88
+ "Adicione-as ao DataFrame com prefixo do ativo (ex: 'crypto_eth_close').")
89
+
90
+
91
+ def _get_observation(self):
92
+ # Pega as features da janela atual
93
+ # O DataFrame self.df já deve estar achatado e conter TODAS as features de TODOS os ativos
94
+ start = self.current_step
95
+ end = start + self.window_size
96
+ obs = self.df.iloc[start:end].values
97
+ return obs.astype(np.float32)
98
+
99
+ def _get_current_prices(self):
100
+ # Pega os preços de fechamento originais do passo atual para cálculo de retorno
101
+ # O índice é window_size - 1 dentro da observação atual, que corresponde a self.current_step + self.window_size -1 no df original.
102
+ # Mas para o cálculo de PnL, precisamos do preço no início do step e no final do step.
103
+ # Preço no início do step (t)
104
+ return self.df[self.orig_close_price_cols].iloc[self.current_step + self.window_size -1].values
105
+
106
+ def _get_next_prices(self):
107
+ # Preço no final do step (t+1)
108
+ return self.df[self.orig_close_price_cols].iloc[self.current_step + self.window_size].values
109
+
110
+ def reset(self, seed=None, options=None): # Assinatura atualizada do Gymnasium
111
+ super().reset(seed=seed) # Importante para Gymnasium
112
+ self.current_step = 0 # Inicia do primeiro ponto onde uma janela completa pode ser formada
113
+ self.balance = self.initial_balance
114
+ self.portfolio_value = self.initial_balance
115
+ self.portfolio_weights = np.full(self.num_assets, 1.0 / self.num_assets if self.num_assets > 0 else 0)
116
+ self.portfolio_returns_history.clear()
117
+
118
+ observation = self._get_observation()
119
+ info = self._get_info() # Informações adicionais (opcional)
120
+ return observation, info
121
+
122
+ def _calculate_sharpe_ratio(self) -> float:
123
+ """Calcula o Sharpe Ratio anualizado a partir do histórico de retornos por passo."""
124
+ if len(self.portfolio_returns_history) < self.reward_window_size / 2: # Precisa de um mínimo de dados
125
+ return 0.0 # Ou uma pequena penalidade por não ter histórico suficiente
126
+
127
+ returns_array = np.array(self.portfolio_returns_history)
128
+
129
+ # Média dos retornos por passo
130
+ mean_return_per_step = np.mean(returns_array)
131
+ # Desvio padrão dos retornos por passo
132
+ std_return_per_step = np.std(returns_array)
133
+
134
+ 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}")
135
+
136
+ if std_return_per_step < 1e-9: # Evitar divisão por zero se não houver volatilidade
137
+ print(" DEBUG Sharpe: Std dev muito baixo, retornando 0.")
138
+ return 0.0
139
+
140
+ # Sharpe Ratio por passo
141
+ sharpe_per_step = (mean_return_per_step - self.risk_free_rate_per_step) / std_return_per_step
142
+
143
+ # Anualizar o Sharpe Ratio (assumindo passos horários e ~252 dias de negociação * 24 horas)
144
+ annualization_factor = np.sqrt(252 * 24) # Ajuste se seu timeframe for diferente
145
+
146
+ annualized_sharpe = sharpe_per_step * annualization_factor
147
+ print(f" DEBUG Sharpe: sharpe_per_step={sharpe_per_step:.4f}, annualized_sharpe={annualized_sharpe:.4f}")
148
+ return annualized_sharpe
149
+
150
+
151
+ def step(self, action_weights: np.ndarray): # Ação são os pesos do portfólio
152
+ current_portfolio_value_before_rebalance = self.portfolio_value
153
+
154
+ # Normalizar pesos da ação se não somarem 1 (saída softmax da rede já deve fazer isso)
155
+
156
+ if not np.isclose(np.sum(action_weights), 1.0):
157
+ action_weights = action_weights / (np.sum(action_weights) + 1e-9)
158
+ action_weights = np.clip(action_weights, 0, 1) # Garantir que os pesos estão entre 0 e 1 # Normaliza
159
+
160
+ # Calcular custo de transação para rebalancear
161
+ # Valor de cada ativo ANTES do rebalanceamento
162
+ current_asset_values = self.portfolio_weights * current_portfolio_value_before_rebalance
163
+ # Valor de cada ativo DEPOIS do rebalanceamento (com base nos novos pesos)
164
+ target_asset_values = action_weights * current_portfolio_value_before_rebalance # Valor do portfólio ainda não mudou por preço
165
+
166
+ # Volume negociado (absoluto) para cada ativo
167
+ trade_volume_per_asset = np.abs(target_asset_values - current_asset_values)
168
+ total_trade_volume = np.sum(trade_volume_per_asset)
169
+ transaction_costs = total_trade_volume * self.transaction_cost_pct
170
+
171
+ # Deduzir custos do valor do portfólio
172
+ current_portfolio_value_after_costs = current_portfolio_value_before_rebalance - transaction_costs
173
+
174
+ # Atualizar os pesos do portfólio
175
+ self.portfolio_weights = action_weights
176
+
177
+ # Pegar preços atuais (t) e próximos (t+1)
178
+ prices_t = self._get_current_prices()
179
+ self.current_step += 1 # Avançar para o próximo estado
180
+ prices_t_plus_1 = self._get_next_prices()
181
+
182
+ # Calcular retornos dos ativos
183
+ asset_returns_on_step = (prices_t_plus_1 - prices_t) / (prices_t + 1e-9)
184
+
185
+ # Calcular retorno do portfólio neste passo, APÓS custos e com os NOVOS pesos
186
+ portfolio_return_on_step = np.sum(self.portfolio_weights * asset_returns_on_step)
187
+
188
+ # Atualizar valor do portfólio
189
+ self.portfolio_value = current_portfolio_value_after_costs * (1 + portfolio_return_on_step)
190
+
191
+ # Adicionar retorno do passo ao histórico
192
+ self.portfolio_returns_history.append(portfolio_return_on_step)
193
+
194
+ # Calcular Recompensa (Sharpe Ratio)
195
+ # Pode ser o Sharpe Ratio incremental ou o Sharpe Ratio da janela inteira
196
+ # Para RL, uma recompensa mais frequente é geralmente melhor.
197
+ # Usar o retorno do passo como recompensa imediata pode ser mais estável para PPO.
198
+ # Ou, podemos dar o Sharpe Ratio da janela como recompensa a cada N passos, ou no final.
199
+ # Por agora, vamos usar o retorno do passo como recompensa principal, e o Sharpe pode ser parte do 'info'.
200
+ # Se quisermos o Sharpe Ratio *como* recompensa, ele seria calculado aqui.
201
+
202
+ # Opção A: Recompensa = Retorno do Passo (mais simples e denso)
203
+ # ultima iteração -- reward = portfolio_return_on_step
204
+
205
+ #Opção B: Recompensa = Sharpe Ratio da Janela (mais complexo, pode ser esparso se calculado raramente)
206
+ # Em PortfolioEnv.step()
207
+ # reward = portfolio_return_on_step # Recompensa atual
208
+
209
+ # NOVA RECOMPENSA (Opção B da nossa discussão anterior):
210
+ REWARD_SCALE_FACTOR_SHARPE = 0.1 # Ou 0.01, experimente
211
+ if len(self.portfolio_returns_history) >= self.reward_window_size:
212
+ current_sharpe = self._calculate_sharpe_ratio()
213
+ reward = np.clip(current_sharpe, -5, 5) * REWARD_SCALE_FACTOR_SHARPE
214
+ print(f" Sharpe Ratio Calculado: {current_sharpe:.4f}, Recompensa (Sharpe escalado): {reward:.6f}")
215
+ elif len(self.portfolio_returns_history) > 1:
216
+ reward = portfolio_return_on_step * 0.1
217
+ print(f" Retorno Simples como Recompensa (escalado): {reward:.6f}")
218
+ else:
219
+ reward = 0.0
220
+
221
+ terminated = self.current_step >= self.total_steps
222
+ truncated = False
223
+
224
+ observation = self._get_observation()
225
+ info = self._get_info() # Adicionar Sharpe Ratio ao info
226
+
227
+ return observation, reward, terminated, truncated, info
228
+
229
+
230
+ def _get_info(self): # Opcional, para retornar métricas
231
+ current_sharpe = self._calculate_sharpe_ratio() if len(self.portfolio_returns_history) > 1 else 0.0
232
+ return {
233
+ "current_step": self.current_step,
234
+ "portfolio_value": self.portfolio_value,
235
+ "balance": self.balance, # Se você rastrear cash separadamente
236
+ "portfolio_weights": self.portfolio_weights.tolist(),
237
+ "last_step_return": self.portfolio_returns_history[-1] if self.portfolio_returns_history else 0.0,
238
+ "sharpe_ratio_window": current_sharpe
239
+ }
240
+
241
+ def render(self, mode='human'):
242
+ if mode == 'human':
243
+ print(f"Step: {self.current_step}, Portfolio Value: {self.portfolio_value:.2f}, Weights: {self.portfolio_weights}")
244
+
245
+ def close(self):
246
+ pass # Limpar recursos se necessário
agents/portfolio_features_extractor_torch.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from stable_baselines3.common.torch_layers import BaseFeaturesExtractor
4
+ from deep_portfolio_torch import DeepPortfolioAgentNetworkTorch
5
+
6
+ class PortfolioFeaturesExtractorTorch(BaseFeaturesExtractor):
7
+ def __init__(self, observation_space, features_dim=32,
8
+ num_assets=4, sequence_length=60, num_features_per_asset=26,
9
+ asset_cnn_filters1=32, asset_cnn_filters2=64,
10
+ asset_lstm_units1=64, asset_lstm_units2=32,
11
+ final_dense_units1=128, final_dense_units2=32,
12
+ final_dropout=0.3, mha_num_heads=4, mha_key_dim_divisor=2,
13
+ output_latent_features=True, use_sentiment_analysis=False):
14
+ super().__init__(observation_space, features_dim)
15
+ self.network = DeepPortfolioAgentNetworkTorch(
16
+ num_assets=num_assets,
17
+ sequence_length=sequence_length,
18
+ num_features_per_asset=num_features_per_asset,
19
+ asset_cnn_filters1=asset_cnn_filters1,
20
+ asset_cnn_filters2=asset_cnn_filters2,
21
+ asset_lstm_units1=asset_lstm_units1,
22
+ asset_lstm_units2=asset_lstm_units2,
23
+ final_dense_units1=final_dense_units1,
24
+ final_dense_units2=final_dense_units2,
25
+ final_dropout=final_dropout,
26
+ mha_num_heads=mha_num_heads,
27
+ mha_key_dim_divisor=mha_key_dim_divisor,
28
+ output_latent_features=output_latent_features,
29
+ use_sentiment_analysis=use_sentiment_analysis
30
+ )
31
+ self._features_dim = features_dim
32
+
33
+ def forward(self, observations):
34
+ # observations: (batch, seq_len, num_assets * num_features_per_asset)
35
+ return self.network(observations)
agents/ppo_deep_portfolio_tensorboard/PPO_1/events.out.tfevents.1750287361.verticalagent-X555LPB.89910.0 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:416a6f624b3dae44c64d6ad216c0d9c90927c65aa2fffc56dde39d387a66b0d2
3
+ size 83375
agents/ppo_deep_portfolio_tensorboard/PPO_2/events.out.tfevents.1750321811.verticalagent-X555LPB.160180.0 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:554a8d430bd4a7a956b2166b588b07e2345511df984019982768a8bb199c83c8
3
+ size 42785
agents/ppo_deep_portfolio_tensorboard/PPO_3/events.out.tfevents.1750336135.verticalagent-X555LPB.200649.0 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8ee8dfec483892359b26ff8390f513acca25fc47bc447f47f82b2fc5eda2e014
3
+ size 30977
agents/rl_agent.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # agents/rl_agent.py
2
+ from stable_baselines3 import PPO
3
+ from atcoin_env import TradingEnv # custom env
4
+
5
+ def train_rl_model():
6
+ env = TradingEnv()
7
+ model = PPO("MlpPolicy", env, verbose=1)
8
+ model.learn(total_timesteps=10000)
9
+ model.save("models/ppo_trading")
agents/train_agent_tfagents.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tensorflow as tf
2
+ import numpy as np
3
+ from tf_agents.environments import tf_py_environment, wrappers, gym_wrapper
4
+ from tf_agents.agents.ppo import ppo_agent
5
+ from tf_agents.networks import actor_distribution_network, value_network
6
+ from tf_agents.trajectories import trajectory
7
+ from tf_agents.drivers.dynamic_step_driver import DynamicStepDriver
8
+ from tf_agents.replay_buffers.tf_uniform_replay_buffer import TFUniformReplayBuffer
9
+ from tf_agents.utils import common
10
+ from tf_agents.metrics import tf_metrics
11
+ from tensorflow.keras.optimizers import Adam
12
+
13
+ # Seus módulos
14
+ from portfolio_environment import PortfolioEnv # Adaptado do seu código
15
+ from DeepPortfolioAgent import DeepPortfolioAgentNetwork
16
+
17
+ # --- CONFIGS ---
18
+ NUM_ASSETS = 4
19
+ WINDOW_SIZE = 60
20
+ FEATURES_PER_ASSET = 26
21
+ TOTAL_FEATURES = NUM_ASSETS * FEATURES_PER_ASSET
22
+ BATCH_SIZE = 64
23
+ REPLAY_BUFFER_CAPACITY = 1000
24
+ LEARNING_RATE = 3e-4
25
+ NUM_ITERATIONS = 200
26
+
27
+ import pandas as pd
28
+
29
+ # Mock: gera dados aleatórios para teste
30
+ num_steps = 500
31
+ symbols = ["ETH-USD","BTC-USD","ADA-USD","SOL-USD"]
32
+ features = ["open", "high", "low", "close", "volume"]
33
+ df_data = {}
34
+
35
+ for symbol in symbols:
36
+ for feat in features:
37
+ df_data[f"{symbol}_{feat}"] = np.random.rand(num_steps)
38
+
39
+ df_mock = pd.DataFrame(df_data)
40
+
41
+
42
+ # --- ENV SETUP ---
43
+ py_env = PortfolioEnv(df_mock, asset_symbols_list=symbols)
44
+ train_env = tf_py_environment.TFPyEnvironment(gym_wrapper.GymWrapper(py_env))
45
+
46
+ # --- FEATURE EXTRACTOR WRAPPER ---
47
+ class FeatureProjector(tf.keras.Model):
48
+ def __init__(self):
49
+ super().__init__()
50
+ self.extractor = DeepPortfolioAgentNetwork(
51
+ num_assets=NUM_ASSETS,
52
+ sequence_length=WINDOW_SIZE,
53
+ num_features_per_asset=FEATURES_PER_ASSET,
54
+ asset_cnn_filters1=32,
55
+ asset_cnn_filters2=64,
56
+ asset_lstm_units1=64,
57
+ asset_lstm_units2=32,
58
+ mha_num_heads=4,
59
+ mha_key_dim_divisor=4,
60
+ final_dense_units1=64,
61
+ final_dense_units2=32,
62
+ final_dropout=0.2,
63
+ use_sentiment_analysis=False
64
+ )
65
+
66
+ def call(self, observation, training=False):
67
+ return self.extractor(observation, training=training)
68
+
69
+ # --- NETWORKS ---
70
+ feature_combiner = FeatureProjector()
71
+
72
+ actor_net = actor_distribution_network.ActorDistributionNetwork(
73
+ input_tensor_spec=train_env.observation_spec(),
74
+ output_tensor_spec=train_env.action_spec(),
75
+ preprocessing_combiner=feature_combiner,
76
+ fc_layer_params=()
77
+ )
78
+
79
+ value_net = value_network.ValueNetwork(
80
+ input_tensor_spec=train_env.observation_spec(),
81
+ preprocessing_combiner=feature_combiner,
82
+ fc_layer_params=()
83
+ )
84
+
85
+ # --- AGENT ---
86
+ optimizer = Adam(learning_rate=LEARNING_RATE)
87
+ train_step = tf.Variable(0)
88
+ agent = ppo_agent.PPOAgent(
89
+ train_env.time_step_spec(),
90
+ train_env.action_spec(),
91
+ actor_net=actor_net,
92
+ value_net=value_net,
93
+ optimizer=optimizer,
94
+ num_epochs=5,
95
+ train_step_counter=train_step
96
+ )
97
+ agent.initialize()
98
+
99
+ # --- REPLAY BUFFER ---
100
+ replay_buffer = TFUniformReplayBuffer(
101
+ data_spec=agent.collect_data_spec,
102
+ batch_size=train_env.batch_size,
103
+ max_length=REPLAY_BUFFER_CAPACITY
104
+ )
105
+
106
+ # --- DRIVER ---
107
+ observer = replay_buffer.add_batch
108
+ driver = DynamicStepDriver(
109
+ env=train_env,
110
+ policy=agent.collect_policy,
111
+ observers=[observer],
112
+ num_steps=1
113
+ )
114
+
115
+ # --- METRICS (opcional) ---
116
+ avg_return = tf_metrics.AverageReturnMetric()
117
+
118
+ # --- TREINAMENTO ---
119
+ print("🚦 Iniciando ciclo de treino PPO com TFAgents e DeepPortfolioAgentNetwork...")
120
+ for i in range(NUM_ITERATIONS):
121
+ driver.run()
122
+ experience = replay_buffer.gather_all()
123
+ train_loss = agent.train(experience)
124
+ replay_buffer.clear()
125
+ print(f"Iteração {i+1}/{NUM_ITERATIONS} - Loss: {train_loss.loss:.5f}")
126
+
127
+ print("✅ Treinamento finalizado com sucesso!")
agents/train_rl_portfolio_agent.py ADDED
@@ -0,0 +1,755 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # train_rl_portfolio_agent.py
2
+ from stable_baselines3 import PPO
3
+ from stable_baselines3.common.env_checker import check_env
4
+ #from transformers import logger
5
+
6
+ # rnn/agents/custom_policies.py (NOVO ARQUIVO, ou adicione ao deep_portfolio.py)
7
+
8
+ import gymnasium as gym # Usar gymnasium
9
+ import tensorflow as tf
10
+ from stable_baselines3.common.torch_layers import BaseFeaturesExtractor as PyTorchBaseFeaturesExtractor
11
+
12
+ from stable_baselines3.common.torch_layers import MlpExtractor
13
+ import torch.nn as nn
14
+
15
+ class CustomMlpExtractor(MlpExtractor):
16
+ def __init__(self, input_dim, net_arch, activation_fn, device):
17
+ super().__init__(input_dim, net_arch, activation_fn, device)
18
+
19
+ def forward(self, features):
20
+ for layer in self.policy_net:
21
+ if isinstance(layer, nn.ReLU):
22
+ features = layer(features) # Passando 'features' como argumento
23
+ else:
24
+ features = layer(features)
25
+ return features
26
+ # Para TensorFlow, precisamos de um extrator de features compatível ou construir a política de forma diferente.
27
+ # Stable Baselines3 tem melhor suporte nativo para PyTorch. Para TF, é um pouco mais manual.
28
+ # VAMOS USAR A ABORDAGEM DE POLÍTICA CUSTOMIZADA COM TF DIRETAMENTE.
29
+ from stable_baselines3.common.policies import ActorCriticPolicy
30
+ from typing import List, Dict, Any, Optional, Union, Type
31
+ # Importar sua rede e configs
32
+ #import agents.DeepPortfolioAgent as DeepPortfolioAgent
33
+ from DeepPortfolioAgent import DeepPortfolioAgentNetwork
34
+ # from ..config import (NUM_ASSETS, WINDOW_SIZE, NUM_FEATURES_PER_ASSET, ...) # Importe do seu config real
35
+ # VALORES DE EXEMPLO (PEGUE DO SEU CONFIG.PY REAL)
36
+ NUM_ASSETS_POLICY = 4
37
+ WINDOW_SIZE_POLICY = 60
38
+ NUM_FEATURES_PER_ASSET_POLICY = 26
39
+ # Hiperparâmetros para DeepPortfolioAgentNetwork quando usada como extrator
40
+ ASSET_CNN_FILTERS1_POLICY = 32
41
+ ASSET_CNN_FILTERS2_POLICY = 64
42
+ ASSET_LSTM_UNITS1_POLICY = 64
43
+ ASSET_LSTM_UNITS2_POLICY = 32 # Esta será a dimensão das features latentes para ator/crítico
44
+ ASSET_DROPOUT_POLICY = 0.2
45
+ MHA_NUM_HEADS_POLICY = 4
46
+ MHA_KEY_DIM_DIVISOR_POLICY = 2 # Para key_dim = 32 // 2 = 16
47
+ FINAL_DENSE_UNITS1_POLICY = 128
48
+ FINAL_DENSE_UNITS2_POLICY = ASSET_LSTM_UNITS2_POLICY # A saída da dense2 SÃO as features latentes
49
+ FINAL_DROPOUT_POLICY = 0.3
50
+
51
+
52
+ class TFPortfolioFeaturesExtractor(tf.keras.layers.Layer): # Herda de tf.keras.layers.Layer
53
+ """
54
+ Extrator de features customizado para SB3 que usa DeepPortfolioAgentNetwork.
55
+ A observação do ambiente é (batch, window, num_assets * num_features_per_asset).
56
+ A saída são as features latentes (batch, latent_dim).
57
+ """
58
+ def __init__(self, observation_space: gym.spaces.Box, features_dim: int = ASSET_LSTM_UNITS2_POLICY):
59
+ super(TFPortfolioFeaturesExtractor, self).__init__()
60
+ self.features_dim = features_dim # SB3 usa isso para saber o tamanho da saída
61
+
62
+ # Instanciar a rede base para extrair features
63
+ # Ela deve retornar as ativações ANTES da camada softmax de alocação.
64
+ self.network = DeepPortfolioAgentNetwork(
65
+ num_assets=NUM_ASSETS_POLICY,
66
+ sequence_length=WINDOW_SIZE_POLICY,
67
+ num_features_per_asset=NUM_FEATURES_PER_ASSET_POLICY,
68
+ asset_cnn_filters1=ASSET_CNN_FILTERS1_POLICY,
69
+ asset_cnn_filters2=ASSET_CNN_FILTERS2_POLICY,
70
+ asset_lstm_units1=ASSET_LSTM_UNITS1_POLICY,
71
+ asset_lstm_units2=ASSET_LSTM_UNITS2_POLICY, # Define a saída do asset_processor
72
+ asset_dropout=ASSET_DROPOUT_POLICY,
73
+ mha_num_heads=MHA_NUM_HEADS_POLICY,
74
+ mha_key_dim_divisor=MHA_KEY_DIM_DIVISOR_POLICY,
75
+ final_dense_units1=FINAL_DENSE_UNITS1_POLICY,
76
+ final_dense_units2=self.features_dim, # A saída da dense2 é a nossa feature latente
77
+ final_dropout=FINAL_DROPOUT_POLICY,
78
+ output_latent_features=True,
79
+ use_sentiment_analysis=True # MUITO IMPORTANTE!
80
+ )
81
+ print("TFPortfolioFeaturesExtractor inicializado e usando DeepPortfolioAgentNetwork (output_latent_features=True).")
82
+
83
+ def call(self, observations: tf.Tensor, training: bool = False) -> tf.Tensor:
84
+ # A DeepPortfolioAgentNetwork já lida com o fatiamento e processamento.
85
+ # Ela foi configurada para retornar features latentes.
86
+ return self.network(observations, training=training)
87
+
88
+
89
+ class CustomPolicy(ActorCriticPolicy):
90
+ def __init__(self, *args, **kwargs):
91
+ super().__init__(*args, **kwargs)
92
+ self.mlp_extractor = CustomMlpExtractor(
93
+ input_dim=self.observation_space.shape[0],
94
+ net_arch=[64, 64], # Exemplo de arquitetura
95
+ activation_fn=nn.ReLU,
96
+ device=self.device
97
+ )
98
+
99
+
100
+
101
+ class CustomPortfolioPolicySB3(ActorCriticPolicy):
102
+ def __init__(
103
+ self,
104
+ observation_space: gym.spaces.Space,
105
+ action_space: gym.spaces.Space,
106
+ lr_schedule, # Função que retorna a taxa de aprendizado
107
+ net_arch: Optional[List[Union[int, Dict[str, List[int]]]]] = None, # Arquitetura para MLPs pós-extrator
108
+ activation_fn: Type[tf.Module] = tf.nn.relu, # Usar tf.nn.relu para TF
109
+ # Adicionar quaisquer outros parâmetros específicos que o extrator precise
110
+ features_extractor_kwargs: Optional[Dict[str, Any]] = None,
111
+ **kwargs,
112
+ ):
113
+ if features_extractor_kwargs is None:
114
+ features_extractor_kwargs = {}
115
+
116
+ # A dimensão das features que o nosso extrator PortfolioFeatureExtractor vai cuspir.
117
+ # Deve ser igual a ASSET_LSTM_UNITS2_POLICY (ou final_dense_units2 do extrator)
118
+ # Se não for passado, o construtor do ActorCriticPolicy pode tentar inferir.
119
+ # Vamos passar explicitamente para garantir.
120
+ features_extractor_kwargs.setdefault("features_dim", ASSET_LSTM_UNITS2_POLICY) # Ou o valor que você definiu
121
+
122
+ super().__init__(
123
+ observation_space,
124
+ action_space,
125
+ lr_schedule,
126
+ net_arch=net_arch, # Para camadas Dense APÓS o extrator de features
127
+ activation_fn=activation_fn,
128
+ features_extractor_class=TFPortfolioFeaturesExtractor,
129
+ features_extractor_kwargs=features_extractor_kwargs,
130
+ **kwargs,
131
+ )
132
+ # Otimizador é criado na classe base.
133
+ # As redes de ator e crítico são construídas no método _build da classe base,
134
+ # usando o self.features_extractor e depois o self.mlp_extractor (que é
135
+ # construído com base no net_arch).
136
+
137
+ # Não precisamos sobrescrever _build_mlp_extractor se o features_extractor
138
+ # já fizer o trabalho pesado e o net_arch padrão para as cabeças for suficiente.
139
+ # Se quisermos MLPs customizados para ator e crítico APÓS o extrator:
140
+ # def _build_mlp_extractor(self) -> None:
141
+ # # self.mlp_extractor é uma instância de MlpExtractor (ou similar)
142
+ # # A entrada para ele é self.features_extractor.features_dim
143
+ # # Aqui, net_arch definiria a estrutura do mlp_extractor
144
+ # self.mlp_extractor = MlpExtractor(
145
+ # feature_dim=self.features_extractor.features_dim,
146
+ # net_arch=self.net_arch, # net_arch é uma lista de ints para camadas da política e valor
147
+ # activation_fn=self.activation_fn,
148
+ # device=self.device,
149
+ # )
150
+ # As redes de ação e valor (action_net, value_net) são então criadas
151
+ # no _build da classe ActorCriticPolicy, no topo do mlp_extractor.# rnn/agents/custom_policies.py (NOVO ARQUIVO, ou adicione ao deep_portfolio.py)
152
+
153
+ import gymnasium as gym # Usar gymnasium
154
+ import tensorflow as tf
155
+ from stable_baselines3.common.torch_layers import BaseFeaturesExtractor as PyTorchBaseFeaturesExtractor
156
+
157
+ from stable_baselines3.common.torch_layers import MlpExtractor
158
+ import torch.nn as nn
159
+
160
+ class CustomMlpExtractor(MlpExtractor):
161
+ def __init__(self, input_dim, net_arch, activation_fn, device):
162
+ super().__init__(input_dim, net_arch, activation_fn, device)
163
+
164
+ def forward(self, features):
165
+ for layer in self.policy_net:
166
+ if isinstance(layer, nn.ReLU):
167
+ features = layer(features) # Passando 'features' como argumento
168
+ else:
169
+ features = layer(features)
170
+ return features
171
+ # Para TensorFlow, precisamos de um extrator de features compatível ou construir a política de forma diferente.
172
+ # Stable Baselines3 tem melhor suporte nativo para PyTorch. Para TF, é um pouco mais manual.
173
+ # VAMOS USAR A ABORDAGEM DE POLÍTICA CUSTOMIZADA COM TF DIRETAMENTE.
174
+ from stable_baselines3.common.policies import ActorCriticPolicy
175
+ from typing import List, Dict, Any, Optional, Union, Type
176
+ # Importar sua rede e configs
177
+ #import agents.DeepPortfolioAgent as DeepPortfolioAgent
178
+ from DeepPortfolioAgent import DeepPortfolioAgentNetwork
179
+ # from ..config import (NUM_ASSETS, WINDOW_SIZE, NUM_FEATURES_PER_ASSET, ...) # Importe do seu config real
180
+ # VALORES DE EXEMPLO (PEGUE DO SEU CONFIG.PY REAL)
181
+ NUM_ASSETS_POLICY = 4
182
+ WINDOW_SIZE_POLICY = 60
183
+ NUM_FEATURES_PER_ASSET_POLICY = 26
184
+ # Hiperparâmetros para DeepPortfolioAgentNetwork quando usada como extrator
185
+ ASSET_CNN_FILTERS1_POLICY = 32
186
+ ASSET_CNN_FILTERS2_POLICY = 64
187
+ ASSET_LSTM_UNITS1_POLICY = 64
188
+ ASSET_LSTM_UNITS2_POLICY = 32 # Esta será a dimensão das features latentes para ator/crítico
189
+ ASSET_DROPOUT_POLICY = 0.2
190
+ MHA_NUM_HEADS_POLICY = 4
191
+ MHA_KEY_DIM_DIVISOR_POLICY = 2 # Para key_dim = 32 // 2 = 16
192
+ FINAL_DENSE_UNITS1_POLICY = 128
193
+ FINAL_DENSE_UNITS2_POLICY = ASSET_LSTM_UNITS2_POLICY # A saída da dense2 SÃO as features latentes
194
+ FINAL_DROPOUT_POLICY = 0.3
195
+
196
+
197
+ class TFPortfolioFeaturesExtractor(tf.keras.layers.Layer): # Herda de tf.keras.layers.Layer
198
+ """
199
+ Extrator de features customizado para SB3 que usa DeepPortfolioAgentNetwork.
200
+ A observação do ambiente é (batch, window, num_assets * num_features_per_asset).
201
+ A saída são as features latentes (batch, latent_dim).
202
+ """
203
+ def __init__(self, observation_space: gym.spaces.Box, features_dim: int = ASSET_LSTM_UNITS2_POLICY):
204
+ super(TFPortfolioFeaturesExtractor, self).__init__()
205
+ self.features_dim = features_dim # SB3 usa isso para saber o tamanho da saída
206
+
207
+ # Instanciar a rede base para extrair features
208
+ # Ela deve retornar as ativações ANTES da camada softmax de alocação.
209
+ self.network = DeepPortfolioAgentNetwork(
210
+ num_assets=NUM_ASSETS_POLICY,
211
+ sequence_length=WINDOW_SIZE_POLICY,
212
+ num_features_per_asset=NUM_FEATURES_PER_ASSET_POLICY,
213
+ asset_cnn_filters1=ASSET_CNN_FILTERS1_POLICY,
214
+ asset_cnn_filters2=ASSET_CNN_FILTERS2_POLICY,
215
+ asset_lstm_units1=ASSET_LSTM_UNITS1_POLICY,
216
+ asset_lstm_units2=ASSET_LSTM_UNITS2_POLICY, # Define a saída do asset_processor
217
+ asset_dropout=ASSET_DROPOUT_POLICY,
218
+ mha_num_heads=MHA_NUM_HEADS_POLICY,
219
+ mha_key_dim_divisor=MHA_KEY_DIM_DIVISOR_POLICY,
220
+ final_dense_units1=FINAL_DENSE_UNITS1_POLICY,
221
+ final_dense_units2=self.features_dim, # A saída da dense2 é a nossa feature latente
222
+ final_dropout=FINAL_DROPOUT_POLICY,
223
+ output_latent_features=True,
224
+ use_sentiment_analysis=True # MUITO IMPORTANTE!
225
+ )
226
+ print("TFPortfolioFeaturesExtractor inicializado e usando DeepPortfolioAgentNetwork (output_latent_features=True).")
227
+
228
+ def call(self, observations: tf.Tensor, training: bool = False) -> tf.Tensor:
229
+ # A DeepPortfolioAgentNetwork já lida com o fatiamento e processamento.
230
+ # Ela foi configurada para retornar features latentes.
231
+ return self.network(observations, training=training)
232
+
233
+
234
+ class CustomPolicy(ActorCriticPolicy):
235
+ def __init__(self, *args, **kwargs):
236
+ super().__init__(*args, **kwargs)
237
+ self.mlp_extractor = CustomMlpExtractor(
238
+ input_dim=self.observation_space.shape[0],
239
+ net_arch=[64, 64], # Exemplo de arquitetura
240
+ activation_fn=nn.ReLU,
241
+ device=self.device
242
+ )
243
+
244
+
245
+
246
+ class CustomPortfolioPolicySB3(ActorCriticPolicy):
247
+ def __init__(
248
+ self,
249
+ observation_space: gym.spaces.Space,
250
+ action_space: gym.spaces.Space,
251
+ lr_schedule, # Função que retorna a taxa de aprendizado
252
+ net_arch: Optional[List[Union[int, Dict[str, List[int]]]]] = None, # Arquitetura para MLPs pós-extrator
253
+ activation_fn: Type[tf.Module] = tf.nn.relu, # Usar tf.nn.relu para TF
254
+ # Adicionar quaisquer outros parâmetros específicos que o extrator precise
255
+ features_extractor_kwargs: Optional[Dict[str, Any]] = None,
256
+ **kwargs,
257
+ ):
258
+ if features_extractor_kwargs is None:
259
+ features_extractor_kwargs = {}
260
+
261
+ # A dimensão das features que o nosso extrator PortfolioFeatureExtractor vai cuspir.
262
+ # Deve ser igual a ASSET_LSTM_UNITS2_POLICY (ou final_dense_units2 do extrator)
263
+ # Se não for passado, o construtor do ActorCriticPolicy pode tentar inferir.
264
+ # Vamos passar explicitamente para garantir.
265
+ features_extractor_kwargs.setdefault("features_dim", ASSET_LSTM_UNITS2_POLICY) # Ou o valor que você definiu
266
+
267
+ super().__init__(
268
+ observation_space,
269
+ action_space,
270
+ lr_schedule,
271
+ net_arch=net_arch, # Para camadas Dense APÓS o extrator de features
272
+ activation_fn=activation_fn,
273
+ features_extractor_class=TFPortfolioFeaturesExtractor,
274
+ features_extractor_kwargs=features_extractor_kwargs,
275
+ **kwargs,
276
+ )
277
+ # Otimizador é criado na classe base.
278
+ # As redes de ator e crítico são construídas no método _build da classe base,
279
+ # usando o self.features_extractor e depois o self.mlp_extractor (que é
280
+ # construído com base no net_arch).
281
+
282
+ # Não precisamos sobrescrever _build_mlp_extractor se o features_extractor
283
+ # já fizer o trabalho pesado e o net_arch padrão para as cabeças for suficiente.
284
+ # Se quisermos MLPs customizados para ator e crítico APÓS o extrator:
285
+ # def _build_mlp_extractor(self) -> None:
286
+ # # self.mlp_extractor é uma instância de MlpExtractor (ou similar)
287
+ # # A entrada para ele é self.features_extractor.features_dim
288
+ # # Aqui, net_arch definiria a estrutura do mlp_extractor
289
+ # self.mlp_extractor = MlpExtractor(
290
+ # feature_dim=self.features_extractor.features_dim,
291
+ # net_arch=self.net_arch, # net_arch é uma lista de ints para camadas da política e valor
292
+ # activation_fn=self.activation_fn,
293
+ # device=self.device,
294
+ # )
295
+ # As redes de ação e valor (action_net, value_net) são então criadas
296
+ # no _build da classe ActorCriticPolicy, no topo do mlp_extractor.# rnn/agents/custom_policies.py (NOVO ARQUIVO, ou adicione ao deep_portfolio.py)
297
+
298
+ import gymnasium as gym # Usar gymnasium
299
+ import tensorflow as tf
300
+ from stable_baselines3.common.torch_layers import BaseFeaturesExtractor as PyTorchBaseFeaturesExtractor
301
+
302
+ from stable_baselines3.common.torch_layers import MlpExtractor
303
+ import torch.nn as nn
304
+
305
+ class CustomMlpExtractor(MlpExtractor):
306
+ def __init__(self, input_dim, net_arch, activation_fn, device):
307
+ super().__init__(input_dim, net_arch, activation_fn, device)
308
+
309
+ def forward(self, features):
310
+ for layer in self.policy_net:
311
+ if isinstance(layer, nn.ReLU):
312
+ features = layer(features) # Passando 'features' como argumento
313
+ else:
314
+ features = layer(features)
315
+ return features
316
+ # Para TensorFlow, precisamos de um extrator de features compatível ou construir a política de forma diferente.
317
+ # Stable Baselines3 tem melhor suporte nativo para PyTorch. Para TF, é um pouco mais manual.
318
+ # VAMOS USAR A ABORDAGEM DE POLÍTICA CUSTOMIZADA COM TF DIRETAMENTE.
319
+ from stable_baselines3.common.policies import ActorCriticPolicy
320
+ from typing import List, Dict, Any, Optional, Union, Type
321
+ # Importar sua rede e configs
322
+ #import agents.DeepPortfolioAgent as DeepPortfolioAgent
323
+ from DeepPortfolioAgent import DeepPortfolioAgentNetwork
324
+ # from ..config import (NUM_ASSETS, WINDOW_SIZE, NUM_FEATURES_PER_ASSET, ...) # Importe do seu config real
325
+ # VALORES DE EXEMPLO (PEGUE DO SEU CONFIG.PY REAL)
326
+ NUM_ASSETS_POLICY = 4
327
+ WINDOW_SIZE_POLICY = 60
328
+ NUM_FEATURES_PER_ASSET_POLICY = 26
329
+ # Hiperparâmetros para DeepPortfolioAgentNetwork quando usada como extrator
330
+ ASSET_CNN_FILTERS1_POLICY = 32
331
+ ASSET_CNN_FILTERS2_POLICY = 64
332
+ ASSET_LSTM_UNITS1_POLICY = 64
333
+ ASSET_LSTM_UNITS2_POLICY = 32 # Esta será a dimensão das features latentes para ator/crítico
334
+ ASSET_DROPOUT_POLICY = 0.2
335
+ MHA_NUM_HEADS_POLICY = 4
336
+ MHA_KEY_DIM_DIVISOR_POLICY = 2 # Para key_dim = 32 // 2 = 16
337
+ FINAL_DENSE_UNITS1_POLICY = 128
338
+ FINAL_DENSE_UNITS2_POLICY = ASSET_LSTM_UNITS2_POLICY # A saída da dense2 SÃO as features latentes
339
+ FINAL_DROPOUT_POLICY = 0.3
340
+
341
+
342
+ class TFPortfolioFeaturesExtractor(tf.keras.layers.Layer): # Herda de tf.keras.layers.Layer
343
+ """
344
+ Extrator de features customizado para SB3 que usa DeepPortfolioAgentNetwork.
345
+ A observação do ambiente é (batch, window, num_assets * num_features_per_asset).
346
+ A saída são as features latentes (batch, latent_dim).
347
+ """
348
+ def __init__(self, observation_space: gym.spaces.Box, features_dim: int = ASSET_LSTM_UNITS2_POLICY):
349
+ super(TFPortfolioFeaturesExtractor, self).__init__()
350
+ self.features_dim = features_dim # SB3 usa isso para saber o tamanho da saída
351
+
352
+ # Instanciar a rede base para extrair features
353
+ # Ela deve retornar as ativações ANTES da camada softmax de alocação.
354
+ self.network = DeepPortfolioAgentNetwork(
355
+ num_assets=NUM_ASSETS_POLICY,
356
+ sequence_length=WINDOW_SIZE_POLICY,
357
+ num_features_per_asset=NUM_FEATURES_PER_ASSET_POLICY,
358
+ asset_cnn_filters1=ASSET_CNN_FILTERS1_POLICY,
359
+ asset_cnn_filters2=ASSET_CNN_FILTERS2_POLICY,
360
+ asset_lstm_units1=ASSET_LSTM_UNITS1_POLICY,
361
+ asset_lstm_units2=ASSET_LSTM_UNITS2_POLICY, # Define a saída do asset_processor
362
+ asset_dropout=ASSET_DROPOUT_POLICY,
363
+ mha_num_heads=MHA_NUM_HEADS_POLICY,
364
+ mha_key_dim_divisor=MHA_KEY_DIM_DIVISOR_POLICY,
365
+ final_dense_units1=FINAL_DENSE_UNITS1_POLICY,
366
+ final_dense_units2=self.features_dim, # A saída da dense2 é a nossa feature latente
367
+ final_dropout=FINAL_DROPOUT_POLICY,
368
+ output_latent_features=True,
369
+ use_sentiment_analysis=True # MUITO IMPORTANTE!
370
+ )
371
+ print("TFPortfolioFeaturesExtractor inicializado e usando DeepPortfolioAgentNetwork (output_latent_features=True).")
372
+
373
+ def call(self, observations: tf.Tensor, training: bool = False) -> tf.Tensor:
374
+ # A DeepPortfolioAgentNetwork já lida com o fatiamento e processamento.
375
+ # Ela foi configurada para retornar features latentes.
376
+ return self.network(observations, training=training)
377
+
378
+
379
+ class CustomPolicy(ActorCriticPolicy):
380
+ def __init__(self, *args, **kwargs):
381
+ super().__init__(*args, **kwargs)
382
+ self.mlp_extractor = CustomMlpExtractor(
383
+ input_dim=self.observation_space.shape[0],
384
+ net_arch=[64, 64], # Exemplo de arquitetura
385
+ activation_fn=nn.ReLU,
386
+ device=self.device
387
+ )
388
+
389
+
390
+
391
+ class CustomPortfolioPolicySB3(ActorCriticPolicy):
392
+ def __init__(
393
+ self,
394
+ observation_space: gym.spaces.Space,
395
+ action_space: gym.spaces.Space,
396
+ lr_schedule, # Função que retorna a taxa de aprendizado
397
+ net_arch: Optional[List[Union[int, Dict[str, List[int]]]]] = None, # Arquitetura para MLPs pós-extrator
398
+ activation_fn: Type[tf.Module] = tf.nn.relu, # Usar tf.nn.relu para TF
399
+ # Adicionar quaisquer outros parâmetros específicos que o extrator precise
400
+ features_extractor_kwargs: Optional[Dict[str, Any]] = None,
401
+ **kwargs,
402
+ ):
403
+ if features_extractor_kwargs is None:
404
+ features_extractor_kwargs = {}
405
+
406
+ # A dimensão das features que o nosso extrator PortfolioFeatureExtractor vai cuspir.
407
+ # Deve ser igual a ASSET_LSTM_UNITS2_POLICY (ou final_dense_units2 do extrator)
408
+ # Se não for passado, o construtor do ActorCriticPolicy pode tentar inferir.
409
+ # Vamos passar explicitamente para garantir.
410
+ features_extractor_kwargs.setdefault("features_dim", ASSET_LSTM_UNITS2_POLICY) # Ou o valor que você definiu
411
+
412
+ super().__init__(
413
+ observation_space,
414
+ action_space,
415
+ lr_schedule,
416
+ net_arch=net_arch, # Para camadas Dense APÓS o extrator de features
417
+ activation_fn=activation_fn,
418
+ features_extractor_class=TFPortfolioFeaturesExtractor,
419
+ features_extractor_kwargs=features_extractor_kwargs,
420
+ **kwargs,
421
+ )
422
+ # Otimizador é criado na classe base.
423
+ # As redes de ator e crítico são construídas no método _build da classe base,
424
+ # usando o self.features_extractor e depois o self.mlp_extractor (que é
425
+ # construído com base no net_arch).
426
+
427
+ # Não precisamos sobrescrever _build_mlp_extractor se o features_extractor
428
+ # já fizer o trabalho pesado e o net_arch padrão para as cabeças for suficiente.
429
+ # Se quisermos MLPs customizados para ator e crítico APÓS o extrator:
430
+ # def _build_mlp_extractor(self) -> None:
431
+ # # self.mlp_extractor é uma instância de MlpExtractor (ou similar)
432
+ # # A entrada para ele é self.features_extractor.features_dim
433
+ # # Aqui, net_arch definiria a estrutura do mlp_extractor
434
+ # self.mlp_extractor = MlpExtractor(
435
+ # feature_dim=self.features_extractor.features_dim,
436
+ # net_arch=self.net_arch, # net_arch é uma lista de ints para camadas da política e valor
437
+ # activation_fn=self.activation_fn,
438
+ # device=self.device,
439
+ # )
440
+ # As redes de ação e valor (action_net, value_net) são então criadas
441
+ # no _build da classe ActorCriticPolicy, no topo do mlp_extractor.# rnn/agents/custom_policies.py (NOVO ARQUIVO, ou adicione ao deep_portfolio.py)
442
+
443
+ import gymnasium as gym # Usar gymnasium
444
+ import tensorflow as tf
445
+ from stable_baselines3.common.torch_layers import BaseFeaturesExtractor as PyTorchBaseFeaturesExtractor
446
+
447
+ from stable_baselines3.common.torch_layers import MlpExtractor
448
+ import torch.nn as nn
449
+
450
+ class CustomMlpExtractor(MlpExtractor):
451
+ def __init__(self, input_dim, net_arch, activation_fn, device):
452
+ super().__init__(input_dim, net_arch, activation_fn, device)
453
+
454
+ def forward(self, features):
455
+ for layer in self.policy_net:
456
+ if isinstance(layer, nn.ReLU):
457
+ features = layer(features) # Passando 'features' como argumento
458
+ else:
459
+ features = layer(features)
460
+ return features
461
+ # Para TensorFlow, precisamos de um extrator de features compatível ou construir a política de forma diferente.
462
+ # Stable Baselines3 tem melhor suporte nativo para PyTorch. Para TF, é um pouco mais manual.
463
+ # VAMOS USAR A ABORDAGEM DE POLÍTICA CUSTOMIZADA COM TF DIRETAMENTE.
464
+ from stable_baselines3.common.policies import ActorCriticPolicy
465
+ from typing import List, Dict, Any, Optional, Union, Type
466
+ # Importar sua rede e configs
467
+ #import agents.DeepPortfolioAgent as DeepPortfolioAgent
468
+ from DeepPortfolioAgent import DeepPortfolioAgentNetwork
469
+ # from ..config import (NUM_ASSETS, WINDOW_SIZE, NUM_FEATURES_PER_ASSET, ...) # Importe do seu config real
470
+ # VALORES DE EXEMPLO (PEGUE DO SEU CONFIG.PY REAL)
471
+ NUM_ASSETS_POLICY = 4
472
+ WINDOW_SIZE_POLICY = 60
473
+ NUM_FEATURES_PER_ASSET_POLICY = 26
474
+ # Hiperparâmetros para DeepPortfolioAgentNetwork quando usada como extrator
475
+ ASSET_CNN_FILTERS1_POLICY = 32
476
+ ASSET_CNN_FILTERS2_POLICY = 64
477
+ ASSET_LSTM_UNITS1_POLICY = 64
478
+ ASSET_LSTM_UNITS2_POLICY = 32 # Esta será a dimensão das features latentes para ator/crítico
479
+ ASSET_DROPOUT_POLICY = 0.2
480
+ MHA_NUM_HEADS_POLICY = 4
481
+ MHA_KEY_DIM_DIVISOR_POLICY = 2 # Para key_dim = 32 // 2 = 16
482
+ FINAL_DENSE_UNITS1_POLICY = 128
483
+ FINAL_DENSE_UNITS2_POLICY = ASSET_LSTM_UNITS2_POLICY # A saída da dense2 SÃO as features latentes
484
+ FINAL_DROPOUT_POLICY = 0.3
485
+
486
+
487
+ class TFPortfolioFeaturesExtractor(tf.keras.layers.Layer): # Herda de tf.keras.layers.Layer
488
+ """
489
+ Extrator de features customizado para SB3 que usa DeepPortfolioAgentNetwork.
490
+ A observação do ambiente é (batch, window, num_assets * num_features_per_asset).
491
+ A saída são as features latentes (batch, latent_dim).
492
+ """
493
+ def __init__(self, observation_space: gym.spaces.Box, features_dim: int = ASSET_LSTM_UNITS2_POLICY):
494
+ super(TFPortfolioFeaturesExtractor, self).__init__()
495
+ self.features_dim = features_dim # SB3 usa isso para saber o tamanho da saída
496
+
497
+ # Instanciar a rede base para extrair features
498
+ # Ela deve retornar as ativações ANTES da camada softmax de alocação.
499
+ self.network = DeepPortfolioAgentNetwork(
500
+ num_assets=NUM_ASSETS_POLICY,
501
+ sequence_length=WINDOW_SIZE_POLICY,
502
+ num_features_per_asset=NUM_FEATURES_PER_ASSET_POLICY,
503
+ asset_cnn_filters1=ASSET_CNN_FILTERS1_POLICY,
504
+ asset_cnn_filters2=ASSET_CNN_FILTERS2_POLICY,
505
+ asset_lstm_units1=ASSET_LSTM_UNITS1_POLICY,
506
+ asset_lstm_units2=ASSET_LSTM_UNITS2_POLICY, # Define a saída do asset_processor
507
+ asset_dropout=ASSET_DROPOUT_POLICY,
508
+ mha_num_heads=MHA_NUM_HEADS_POLICY,
509
+ mha_key_dim_divisor=MHA_KEY_DIM_DIVISOR_POLICY,
510
+ final_dense_units1=FINAL_DENSE_UNITS1_POLICY,
511
+ final_dense_units2=self.features_dim, # A saída da dense2 é a nossa feature latente
512
+ final_dropout=FINAL_DROPOUT_POLICY,
513
+ output_latent_features=True,
514
+ use_sentiment_analysis=True # MUITO IMPORTANTE!
515
+ )
516
+ print("TFPortfolioFeaturesExtractor inicializado e usando DeepPortfolioAgentNetwork (output_latent_features=True).")
517
+
518
+ def call(self, observations: tf.Tensor, training: bool = False) -> tf.Tensor:
519
+ # A DeepPortfolioAgentNetwork já lida com o fatiamento e processamento.
520
+ # Ela foi configurada para retornar features latentes.
521
+ return self.network(observations, training=training)
522
+
523
+
524
+ class CustomPolicy(ActorCriticPolicy):
525
+ def __init__(self, *args, **kwargs):
526
+ super().__init__(*args, **kwargs)
527
+ self.mlp_extractor = CustomMlpExtractor(
528
+ input_dim=self.observation_space.shape[0],
529
+ net_arch=[64, 64], # Exemplo de arquitetura
530
+ activation_fn=nn.ReLU,
531
+ device=self.device
532
+ )
533
+
534
+
535
+
536
+ class CustomPortfolioPolicySB3(ActorCriticPolicy):
537
+ def __init__(
538
+ self,
539
+ observation_space: gym.spaces.Space,
540
+ action_space: gym.spaces.Space,
541
+ lr_schedule, # Função que retorna a taxa de aprendizado
542
+ net_arch: Optional[List[Union[int, Dict[str, List[int]]]]] = None, # Arquitetura para MLPs pós-extrator
543
+ activation_fn: Type[tf.Module] = tf.nn.relu, # Usar tf.nn.relu para TF
544
+ # Adicionar quaisquer outros parâmetros específicos que o extrator precise
545
+ features_extractor_kwargs: Optional[Dict[str, Any]] = None,
546
+ **kwargs,
547
+ ):
548
+ if features_extractor_kwargs is None:
549
+ features_extractor_kwargs = {}
550
+
551
+ # A dimensão das features que o nosso extrator PortfolioFeatureExtractor vai cuspir.
552
+ # Deve ser igual a ASSET_LSTM_UNITS2_POLICY (ou final_dense_units2 do extrator)
553
+ # Se não for passado, o construtor do ActorCriticPolicy pode tentar inferir.
554
+ # Vamos passar explicitamente para garantir.
555
+ features_extractor_kwargs.setdefault("features_dim", ASSET_LSTM_UNITS2_POLICY) # Ou o valor que você definiu
556
+
557
+ super().__init__(
558
+ observation_space,
559
+ action_space,
560
+ lr_schedule,
561
+ net_arch=net_arch, # Para camadas Dense APÓS o extrator de features
562
+ activation_fn=activation_fn,
563
+ features_extractor_class=TFPortfolioFeaturesExtractor,
564
+ features_extractor_kwargs=features_extractor_kwargs,
565
+ **kwargs,
566
+ )
567
+ # Otimizador é criado na classe base.
568
+ # As redes de ator e crítico são construídas no método _build da classe base,
569
+ # usando o self.features_extractor e depois o self.mlp_extractor (que é
570
+ # construído com base no net_arch).
571
+
572
+ # Não precisamos sobrescrever _build_mlp_extractor se o features_extractor
573
+ # já fizer o trabalho pesado e o net_arch padrão para as cabeças for suficiente.
574
+ # Se quisermos MLPs customizados para ator e crítico APÓS o extrator:
575
+ # def _build_mlp_extractor(self) -> None:
576
+ # # self.mlp_extractor é uma instância de MlpExtractor (ou similar)
577
+ # # A entrada para ele é self.features_extractor.features_dim
578
+ # # Aqui, net_arch definiria a estrutura do mlp_extractor
579
+ # self.mlp_extractor = MlpExtractor(
580
+ # feature_dim=self.features_extractor.features_dim,
581
+ # net_arch=self.net_arch, # net_arch é uma lista de ints para camadas da política e valor
582
+ # activation_fn=self.activation_fn,
583
+ # device=self.device,
584
+ # )
585
+ # As redes de ação e valor (action_net, value_net) são então criadas
586
+ # no _build da classe ActorCriticPolicy, no topo do mlp_extractor.
587
+
588
+ from custom_policies import CustomPortfolioPolicySB3 # Importar a política customizada
589
+ #from .deep_portfolio import NUM_ASSETS_CONF, WINDOW_SIZE_CONF, NUM_FEATURES_PER_ASSET_CONF # Se precisar para policy_kwargs
590
+ # (Importe as configs do config.py)
591
+ #from ..config import LEARNING_RATE as PPO_LEARNING_RATE
592
+ PPO_LEARNING_RATE = 0.0005
593
+
594
+
595
+ from data_handler_multi_asset import get_multi_asset_data_for_rl, MULTI_ASSET_SYMBOLS # Do seu config/data_handler
596
+ from portfolio_environment import PortfolioEnv
597
+ from deep_portfolio import DeepPortfolioAI # Seu modelo (usado como policy)
598
+ # from config import ... # Outras configs
599
+
600
+ RISK_FREE_RATE_ANNUAL = 0.2
601
+ REWARD_WINDOW = 252
602
+ frisk_free_per_step = 0.0
603
+
604
+ # Janela de recompensa para Sharpe (ex: últimos 60 passos/horas)
605
+ # Deve ser menor ou igual ao ep_len_mean ou um valor razoável
606
+ reward_calc_window = 60
607
+
608
+ # 1. Carregar e preparar dados multi-ativos
609
+ # (MULTI_ASSET_SYMBOLS viria do config.py)
610
+ asset_keys_list = list(MULTI_ASSET_SYMBOLS.keys()) # ['crypto_eth', 'crypto_ada', ...]
611
+
612
+ multi_asset_df = get_multi_asset_data_for_rl(
613
+ MULTI_ASSET_SYMBOLS,
614
+ timeframe_yf='1h', # Ou TIMEFRAME_YFINANCE do config
615
+ days_to_fetch=365*2,
616
+ logger_instance=any
617
+ # Ou DAYS_TO_FETCH do config
618
+ )
619
+
620
+ print("Imprimindo retorno para df_combined passado para train_rl_portifolio")
621
+ print(multi_asset_df)
622
+
623
+
624
+ #-------------------
625
+
626
+ if multi_asset_df is None or multi_asset_df.empty:
627
+ print("Falha ao carregar dados multi-ativos. Encerrando treinamento RL.")
628
+ exit()
629
+
630
+ env = PortfolioEnv(df_multi_asset_features=multi_asset_df, asset_symbols_list=asset_keys_list)
631
+ print("Ambiente de Portfólio Criado.")
632
+
633
+ # --- Usar a Política Customizada ---
634
+ # Hiperparâmetros para o PPO
635
+ learning_rate_ppo = PPO_LEARNING_RATE # Ex: 3e-4 ou 1e-4 (do config.py)
636
+ n_steps_ppo = 2048
637
+ batch_size_ppo = 64
638
+ ent_coef_ppo = 0.01
639
+
640
+ # policy_kwargs para passar para o __init__ da CustomPortfolioPolicySB3, se necessário
641
+ # (além dos que já são passados para o features_extractor_kwargs)
642
+ # Exemplo: Se você adicionou mais args ao __init__ de CustomPortfolioPolicySB3
643
+ # policy_custom_kwargs = dict(
644
+ # meu_parametro_customizado=valor,
645
+ # # features_extractor_kwargs já é tratado pela classe base se você passar features_extractor_class
646
+ # )
647
+
648
+ print("Instanciando PPO com Política Customizada (DeepPortfolioAgentNetwork)...")
649
+ model_ppo = PPO(
650
+ CustomPortfolioPolicySB3,
651
+ env,
652
+ verbose=1,
653
+ learning_rate=learning_rate_ppo, # Pode ser uma função lr_schedule
654
+ n_steps=n_steps_ppo,
655
+ batch_size=batch_size_ppo,
656
+ ent_coef=ent_coef_ppo,
657
+ # policy_kwargs=policy_custom_kwargs, # Se tiver kwargs específicos para a política
658
+ tensorboard_log="./ppo_deep_portfolio_tensorboard/"
659
+ )
660
+
661
+ print("Iniciando treinamento do agente PPO com rede customizada...")
662
+ model_ppo.learn(total_timesteps=1000000, progress_bar=True) # Comece com menos timesteps para teste (ex: 50k)
663
+
664
+ model_ppo.save("rl_models/ppo_custom_deep_portfolio_agent")
665
+ print("Modelo RL com política customizada treinado e salvo.")
666
+
667
+ #----------
668
+
669
+
670
+
671
+
672
+ # if multi_asset_df is None or multi_asset_df.empty:
673
+ # print("Falha ao carregar dados multi-ativos. Encerrando treinamento RL.")
674
+ # exit()
675
+
676
+ # # 2. Criar o Ambiente
677
+ # # O multi_asset_df já deve ter as features para observação E as colunas de preço de close original
678
+ # env = PortfolioEnv(df_multi_asset_features=multi_asset_df, asset_symbols_list=asset_keys_list)
679
+
680
+
681
+ # risk_free_per_step = 0.0
682
+
683
+ # # Janela de recompensa para Sharpe (ex: últimos 60 passos/horas)
684
+ # # Deve ser menor ou igual ao ep_len_mean ou um valor razoável
685
+ # reward_calc_window = 60
686
+
687
+ # env = PortfolioEnv(
688
+ # df_multi_asset_features=multi_asset_df,
689
+ # asset_symbols_list=asset_keys_list,
690
+ # initial_balance=100000, # Do config
691
+ # window_size=60, # Do config
692
+ # transaction_cost_pct=0.001, # Do config ou defina aqui
693
+ # reward_window_size=reward_calc_window,
694
+ # risk_free_rate_per_step=risk_free_per_step
695
+ # )
696
+
697
+ # # Opcional: Verificar se o ambiente está em conformidade com a API do Gymnasium
698
+ # # check_env(env) # Pode dar avisos/erros se algo estiver errado
699
+ # print("Ambiente de Portfólio Criado.")
700
+ # print(f"Observation Space: {env.observation_space.shape}")
701
+ # print(f"Action Space: {env.action_space.shape}")
702
+
703
+
704
+
705
+
706
+ # # 3. Definir a Política de Rede Neural
707
+ # # Stable-Baselines3 permite que você defina uma arquitetura customizada.
708
+ # # Precisamos de uma forma de passar sua arquitetura DeepPortfolioAI para o PPO.
709
+ # # Uma maneira é criar uma classe de política customizada.
710
+ # # Por agora, vamos usar a política padrão "MlpPolicy" e depois vemos como integrar a sua.
711
+ # # Ou, se DeepPortfolioAI for uma tf.keras.Model, podemos tentar usá-la em policy_kwargs.
712
+
713
+ # # Para usar sua DeepPortfolioAI, você precisaria de uma FeatureExtractor customizada
714
+ # # ou uma política que a incorpore, o que é mais avançado com Stable-Baselines3.
715
+ # # Vamos começar com MlpPolicy para testar o ambiente.
716
+
717
+ # # policy_kwargs = dict(
718
+ # # features_extractor_class=YourCustomFeatureExtractor, # Se a entrada precisar de tratamento especial
719
+ # # features_extractor_kwargs=dict(features_dim=128),
720
+ # # net_arch=[dict(pi=[256, 128], vf=[256, 128])] # Exemplo de arquitetura para policy e value networks
721
+ # # )
722
+ # # Ou, se o DeepPortfolioAI puder ser adaptado para ser a policy_network:
723
+ # policy_kwargs = dict(
724
+ # net_arch=dict(
725
+ # pi=[{'model': DeepPortfolioAI(num_assets=env.num_assets)}], # Não é direto assim
726
+ # vf=[] # Value function pode ser separada ou compartilhada
727
+ # )
728
+ # )
729
+
730
+ # # Para começar e testar o ambiente, use a MlpPolicy padrão.
731
+ # # O input da MlpPolicy será a observação achatada (WINDOW_SIZE * num_total_features).
732
+ # # Isso pode não ser ideal para dados sequenciais. "MlpLstmPolicy" é melhor.
733
+
734
+ # model_ppo = PPO("MlpPolicy", env, verbose=1, ent_coef=0.01, tensorboard_log="./ppo_portfolio_tensorboard/")
735
+ # # Se "MlpLstmPolicy" não funcionar bem com o shape da observação (janela, features_totais),
736
+ # # você pode precisar de um FeatureExtractor que achate a janela, ou uma política customizada.
737
+
738
+ # # 4. Treinar o Agente
739
+ # print("Iniciando treinamento do agente PPO...")
740
+ # model_ppo.learn(total_timesteps=int("1000000"), progress_bar=True) # Aumente timesteps para treino real
741
+
742
+ # # 5. Salvar o Modelo Treinado
743
+ # model_ppo.save("rl_models/ppo_deep_portfolio_agent")
744
+ # print("Modelo RL treinado salvo.")
745
+
746
+
747
+ # # (Opcional) Testar o agente treinado
748
+ # obs, _ = env.reset()
749
+ # for _ in range(200):
750
+ # action, _states = model_ppo.predict(obs, deterministic=True)
751
+ # obs, rewards, terminated, truncated, info = env.step(action)
752
+ # env.render()
753
+ # if terminated or truncated:
754
+ # obs, _ = env.reset()
755
+ # env.close()
api/main.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from endpoints.predict import router as predict_router
4
+ from agents.dataset_update_agent import router as dataset_router
5
+ from agents.financial_data_agent import router as finance_router
6
+ from agents.investment_agent import router as invest_router
7
+ from agents.rl_agent import router as rl_router
8
+ from utils.logger import get_logger
9
+ from fastapi import FastAPI, Request
10
+ from fastapi.responses import HTMLResponse
11
+ from fastapi.staticfiles import StaticFiles
12
+ from jinja2 import Environment, FileSystemLoader
13
+ import os
14
+
15
+
16
+
17
+
18
+ logger = get_logger()
19
+
20
+ app = FastAPI(title="ATCoin Neural Agents")
21
+
22
+ app.add_middleware(
23
+ CORSMiddleware,
24
+ allow_origins=["http://localhost:3000"],
25
+ allow_credentials=True,
26
+ allow_methods=["*"],
27
+ allow_headers=["*"],
28
+ )
29
+
30
+ # Registrar rotas
31
+ app.include_router(predict_router)
32
+ app.include_router(dataset_router, prefix="/dataset")
33
+ app.include_router(finance_router, prefix="/finance")
34
+ app.include_router(invest_router, prefix="/invest")
35
+ app.include_router(rl_router, prefix="/rl")
36
+
37
+
38
+
39
+
40
+ @app.get("/", response_class=HTMLResponse)
41
+ async def index():
42
+ agentes = [
43
+ {
44
+ "nome": "Captura de Dados",
45
+ "status": "Ativo",
46
+ "ultima_acao": "08/05/2025 10:00",
47
+ "proxima_execucao": "09/05/2025 10:00",
48
+ "resultado": "12 ativos atualizados"
49
+ },
50
+ {
51
+ "nome": "PPO Trainer",
52
+ "status": "Treinando",
53
+ "ultima_acao": "08/05/2025 12:00",
54
+ "proxima_execucao": "09/05/2025 12:00",
55
+ "resultado": "Reward: 0.89"
56
+ },
57
+ # outros agentes...
58
+ ]
59
+
60
+ template = templates.get_template("index.html")
61
+ return HTMLResponse(template.render(agentes=agentes))
app-modularizado.py ADDED
@@ -0,0 +1,606 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rnn/app.py
2
+
3
+ import os
4
+ import uuid
5
+ import time
6
+ import hmac
7
+ import hashlib
8
+ import json
9
+ from datetime import datetime, timedelta
10
+ from typing import Dict, Any
11
+ from rnn.app.ccxt_utils import get_ccxt_exchange, fetch_crypto_data
12
+
13
+ import httpx # Para fazer chamadas HTTP assíncronas (para o callback)
14
+ from fastapi import FastAPI, Request, HTTPException, Depends, Header, BackgroundTasks
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ from fastapi.responses import HTMLResponse, JSONResponse
17
+ from fastapi.staticfiles import StaticFiles
18
+ from jinja2 import Environment, FileSystemLoader
19
+ from pydantic import BaseModel, Field
20
+
21
+ from rnn.app.utils.logger import get_logger
22
+
23
+
24
+
25
+ logger = get_logger()
26
+
27
+ # --- Configuração Inicial e Variáveis de Ambiente (Secrets do Hugging Face) ---
28
+ AIBANK_API_KEY = os.environ.get("AIBANK_API_KEY") # Chave que o aibank usa para chamar esta API RNN
29
+ AIBANK_CALLBACK_URL = os.environ.get("AIBANK_CALLBACK_URL") # URL no aibank para onde esta API RNN enviará o resultado
30
+ CALLBACK_SHARED_SECRET = os.environ.get("CALLBACK_SHARED_SECRET") # Segredo para assinar/verificar o payload do callback
31
+
32
+ # Chaves para serviços externos
33
+ MARKET_DATA_API_KEY = os.environ.get("MARKET_DATA_API_KEY")
34
+ EXCHANGE_API_KEY = os.environ.get("EXCHANGE_API_KEY")
35
+ EXCHANGE_API_SECRET = os.environ.get("EXCHANGE_API_SECRET")
36
+
37
+ if not AIBANK_API_KEY:
38
+ logger.warning("AIBANK_API_KEY não configurada. A autenticação para /api/invest falhou.")
39
+ if not AIBANK_CALLBACK_URL:
40
+ logger.warning("AIBANK_CALLBACK_URL não configurada. O callback para o aibank falhou.")
41
+ if not CALLBACK_SHARED_SECRET:
42
+ logger.warning("CALLBACK_SHARED_SECRET não configurado. A segurança do callback está comprometida.")
43
+
44
+
45
+
46
+
47
+ app = FastAPI(title="ATCoin Neural Agents - Investment API")
48
+
49
+ # --- Middlewares ---
50
+ app.add_middleware(
51
+ CORSMiddleware,
52
+ allow_origins=[
53
+ "http://localhost:3000", # URL desenvolvimento local
54
+ "http://aibank.app.br", # URL de produção
55
+ "https://*.aibank.app.br", # subdomínios
56
+ "https://*.hf.space" # HF Space
57
+ ],
58
+ allow_credentials=True,
59
+ allow_methods=["*"],
60
+ allow_headers=["*"],
61
+ )
62
+
63
+ # --- Simulação de Banco de Dados de Transações DEV ---
64
+ # Em produção MongoDB
65
+ transactions_db: Dict[str, Dict[str, Any]] = {}
66
+
67
+ # --- Modelos Pydantic ---
68
+ class InvestmentRequest(BaseModel):
69
+ client_id: str
70
+ amount: float = Field(..., gt=0) # Garante que o montante seja positivo
71
+ aibank_transaction_token: str # Token único gerado pelo aibank para rastreamento
72
+
73
+ class InvestmentResponse(BaseModel):
74
+ status: str
75
+ message: str
76
+ rnn_transaction_id: str # ID da transação this.API
77
+
78
+ class InvestmentResultPayload(BaseModel): # Payload para o callback para o aibank
79
+ rnn_transaction_id: str
80
+ aibank_transaction_token: str
81
+ client_id: str
82
+ initial_amount: float
83
+ final_amount: float
84
+ profit_loss: float
85
+ status: str # "completed", "failed"
86
+ timestamp: datetime
87
+ details: str = ""
88
+
89
+
90
+ # --- Dependência de Autenticação ---
91
+ async def verify_aibank_key(authorization: str = Header(None)):
92
+ if not AIBANK_API_KEY: # Checagem se a chave do servidor está configurada
93
+ logger.error("CRITICAL: AIBANK_API_KEY (server-side) não está configurada nos Secrets.")
94
+ raise HTTPException(status_code=500, detail="Internal Server Configuration Error: Missing server API Key.")
95
+
96
+ if authorization is None:
97
+ logger.warning("Authorization header ausente na chamada do AIBank.")
98
+ raise HTTPException(status_code=401, detail="Authorization header is missing")
99
+
100
+ parts = authorization.split()
101
+ if len(parts) != 2 or parts[0].lower() != 'bearer':
102
+ logger.warning(f"Formato inválido do Authorization header: {authorization}")
103
+ raise HTTPException(status_code=401, detail="Authorization header must be 'Bearer <token>'")
104
+
105
+ token_from_aibank = parts[1]
106
+ if not hmac.compare_digest(token_from_aibank, AIBANK_API_KEY):
107
+ logger.warning(f"Chave de API inválida fornecida pelo AIBank. Token: {token_from_aibank[:10]}...")
108
+ raise HTTPException(status_code=403, detail="Invalid API Key provided by AIBank.")
109
+ logger.info("API Key do AIBank verificada com sucesso.")
110
+ return True
111
+
112
+
113
+ # --- Lógica de Negócio Principal (Simulada e em Background) ---
114
+
115
+
116
+ async def execute_investment_strategy_background(
117
+ rnn_tx_id: str,
118
+ client_id: str,
119
+ amount: float,
120
+ aibank_tx_token: str
121
+ ):
122
+ logger.info(f"BG TASK [{rnn_tx_id}]: Iniciando estratégia de investimento para cliente {client_id}, valor {amount}.")
123
+ transactions_db[rnn_tx_id]["status"] = "processing"
124
+ transactions_db[rnn_tx_id]["status_details"] = "Initializing investment cycle"
125
+
126
+ final_status = "completed"
127
+ error_details = "" # Acumula mensagens de erro de várias etapas
128
+ calculated_final_amount = amount
129
+
130
+ # Inicializa a exchange ccxt usando o utilitário
131
+ # O logger do app.py é passado para ccxt_utils para que os logs apareçam no mesmo stream
132
+ exchange = await get_ccxt_exchange(logger_instance=logger) # MODIFICADO
133
+
134
+ if not exchange:
135
+ # get_ccxt_exchange já loga o erro. Se a exchange é crucial, podemos falhar aqui.
136
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Falha ao inicializar a exchange. A estratégia pode não funcionar como esperado para cripto.")
137
+ # Se as chaves CCXT foram fornecidas no ambiente mas a exchange falhou, considere isso um erro de config.
138
+ if os.environ.get("CCXT_API_KEY") and os.environ.get("CCXT_API_SECRET"):
139
+ error_details += "Failed to initialize CCXT exchange despite API keys being present; "
140
+ final_status = "failed_config"
141
+ # (PULAR PARA CALLBACK - veja a seção de tratamento de erro crítico abaixo)
142
+
143
+ # =========================================================================
144
+ # 1. COLETAR DADOS DE MERCADO
145
+ # =========================================================================
146
+ logger.info(f"BG TASK [{rnn_tx_id}]: Coletando dados de mercado...")
147
+ transactions_db[rnn_tx_id]["status_details"] = "Fetching market data"
148
+ market_data_results = {"crypto": {}, "stocks": {}, "other": {}}
149
+ critical_data_fetch_failed = False # Flag para falha crítica na coleta de dados
150
+
151
+ # --- Coleta de dados de Cripto via ccxt_utils ---
152
+ if exchange:
153
+ crypto_pairs_to_fetch = ["BTC/USDT", "ETH/USDT", "SOL/USDT"] # Mantenha configurável
154
+
155
+ crypto_data, crypto_fetch_ok, crypto_err_msg = await fetch_crypto_data(
156
+ exchange,
157
+ crypto_pairs_to_fetch,
158
+ logger_instance=logger
159
+ )
160
+ market_data_results["crypto"] = crypto_data
161
+ if not crypto_fetch_ok:
162
+ error_details += f"Crypto data fetch issues: {crypto_err_msg}; "
163
+ # Decida se a falha na coleta de cripto é crítica
164
+ # Se for, defina critical_data_fetch_failed = True
165
+ if os.environ.get("CCXT_API_KEY"): # Se esperávamos dados de cripto
166
+ critical_data_fetch_failed = True
167
+ logger.error(f"BG TASK [{rnn_tx_id}]: Falha crítica na coleta de dados de cripto.")
168
+ else:
169
+ logger.info(f"BG TASK [{rnn_tx_id}]: Instância da exchange ccxt não disponível. Pulando coleta de dados de cripto.")
170
+ if os.environ.get("CCXT_API_KEY"): # Se esperávamos dados de cripto mas a exchange não inicializou
171
+ error_details += "CCXT exchange not initialized, crypto data skipped; "
172
+ critical_data_fetch_failed = True
173
+
174
+
175
+ # --- Coleta de dados para outros tipos de ativos (ex: Ações com yfinance) ---
176
+ # (Sua lógica yfinance aqui, se aplicável, similarmente atualizando market_data_results["stocks"])
177
+ # try:
178
+ # import yfinance as yf # Mova para o topo do app.py se for usar
179
+ # # ... lógica yfinance ...
180
+ # except Exception as e_yf:
181
+ # logger.warning(f"BG TASK [{rnn_tx_id}]: Falha ao buscar dados de ações com yfinance: {e_yf}")
182
+ # error_details += f"YFinance data fetch failed: {str(e_yf)}; "
183
+ # # Decida se isso é crítico: critical_data_fetch_failed = True
184
+
185
+ market_data_results["other"]['simulated_index_level'] = random.uniform(10000, 15000) # Mantém simulação
186
+
187
+ transactions_db[rnn_tx_id]["market_data_collected"] = market_data_results
188
+
189
+ # --- PONTO DE CHECAGEM PARA FALHA CRÍTICA NA COLETA DE DADOS ---
190
+ if critical_data_fetch_failed:
191
+ final_status = "failed_market_data"
192
+ logger.error(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado falhou criticamente. {error_details}")
193
+ # Pular para a seção de callback
194
+ # (A lógica de envio do callback precisa ser alcançada)
195
+ else:
196
+ logger.info(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado concluída.")
197
+ transactions_db[rnn_tx_id]["status_details"] = "Processing RNN analysis"
198
+
199
+
200
+ # =========================================================================
201
+ # 2. ANÁLISE PELA RNN E TOMADA DE DECISÃO (Só executa se a coleta de dados não falhou criticamente)
202
+ # =========================================================================
203
+ investment_decisions: List[Dict[str, Any]] = []
204
+ total_usd_allocated_by_rnn = 0.0
205
+
206
+ if final_status == "completed": # Prossiga apenas se não houve erro crítico anterior
207
+ logger.info(f"BG TASK [{rnn_tx_id}]: Executando análise RNN...")
208
+ transactions_db[rnn_tx_id]["status_details"] = "Running RNN model"
209
+ rnn_analysis_success = True
210
+ try:
211
+ # Placeholder para a LÓGICA REAL DA RNN (RNN_LOGIC_PLACEHOLDER)
212
+ # Esta seção precisa ser preenchida com:
213
+ # 1. Carregamento do seu modelo RNN treinado (ex: TensorFlow, PyTorch, scikit-learn).
214
+ # O modelo pode ser carregado uma vez quando a API inicia, ou sob demanda.
215
+ # Se for grande, carregue na inicialização da API.
216
+ # Ex: from rnn.models.my_rnn_predictor import MyRNNModel
217
+ # if 'rnn_model_instance' not in app.state: # Carregar uma vez
218
+ # app.state.rnn_model_instance = MyRNNModel(path_to_model_weights)
219
+ # predictor = app.state.rnn_model_instance
220
+ #
221
+ # 2. Pré-processamento dos `market_data_results` para o formato que sua RNN espera.
222
+ # Ex: features = preprocess_market_data_for_rnn(market_data_results)
223
+ #
224
+ # 3. Inferência/Predição com a RNN.
225
+ # Ex: raw_rnn_output = await predictor.predict_async(features, client_profile_if_any)
226
+ #
227
+ # 4. Pós-processamento do output da RNN para gerar `investment_decisions` no formato:
228
+ # [{ "asset_id": "BTC/USDT", "type": "CRYPTO", "action": "BUY",
229
+ # "target_usd_amount": 5000.00, "confidence_score": 0.85,
230
+ # "stop_loss_price": 60000, "take_profit_price": 75000,
231
+ # "reasoning": "RNN output details..." }, ...]
232
+
233
+ # Simulação atual:
234
+ await asyncio.sleep(random.uniform(5, 10)) # Simula processamento da RNN
235
+ if market_data_results.get("crypto"):
236
+ if random.random() > 0.2: # 80% chance
237
+ num_crypto_to_invest = min(random.randint(1, 2), len(market_data_results["crypto"]))
238
+ available_crypto_keys = [k for k,v in market_data_results["crypto"].items() if not v.get("error")]
239
+ if available_crypto_keys:
240
+ chosen_pairs_keys = random.sample(available_crypto_keys, k=min(num_crypto_to_invest, len(available_crypto_keys)))
241
+
242
+ base_allocation_per_asset = amount / len(chosen_pairs_keys) if chosen_pairs_keys else 0
243
+
244
+ for crypto_key in chosen_pairs_keys:
245
+ asset_symbol = crypto_key.replace("_", "/")
246
+ # Alocação mais realista: não necessariamente usar todo o 'amount'
247
+ # E a RNN pode dar pesos diferentes
248
+ asset_specific_allocation_factor = random.uniform(0.3, 0.7) / len(chosen_pairs_keys)
249
+ allocated_amount = amount * asset_specific_allocation_factor
250
+
251
+ # Evitar alocar mais do que o 'amount' total disponível
252
+ if total_usd_allocated_by_rnn + allocated_amount > amount * 0.95: # Deixa uma margem
253
+ allocated_amount = max(0, (amount * 0.95) - total_usd_allocated_by_rnn)
254
+
255
+ if allocated_amount > 10: # Investimento mínimo de $10 (exemplo)
256
+ investment_decisions.append({
257
+ "asset_id": asset_symbol, "type": "CRYPTO", "action": "BUY",
258
+ "target_usd_amount": round(allocated_amount, 2),
259
+ "reasoning": f"Simulated RNN signal for {asset_symbol}"
260
+ })
261
+ total_usd_allocated_by_rnn += round(allocated_amount, 2)
262
+
263
+ if not investment_decisions:
264
+ logger.info(f"BG TASK [{rnn_tx_id}]: RNN não gerou decisões de COMPRA.")
265
+ else:
266
+ logger.info(f"BG TASK [{rnn_tx_id}]: RNN gerou {len(investment_decisions)} decisões de compra: {investment_decisions}")
267
+
268
+ except Exception as e:
269
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante análise RNN: {str(e)}", exc_info=True)
270
+ rnn_analysis_success = False
271
+ error_details += f"RNN analysis failed: {str(e)}; "
272
+
273
+ if not rnn_analysis_success:
274
+ final_status = "failed_rnn_analysis"
275
+
276
+ transactions_db[rnn_tx_id]["rnn_decisions"] = investment_decisions
277
+
278
+ # Atualiza o total_usd_allocated_by_rnn com base nas decisões finais
279
+ total_usd_allocated_by_rnn = sum(d['target_usd_amount'] for d in investment_decisions if d['action'] == 'BUY')
280
+ transactions_db[rnn_tx_id]["status_details"] = "Preparing to execute orders"
281
+
282
+
283
+ # =========================================================================
284
+ # 3. EXECUÇÃO DE ORDENS (Só executa se a RNN não falhou e gerou ordens)
285
+ # =========================================================================
286
+ executed_trades_info: List[Dict[str, Any]] = []
287
+ current_portfolio_value = 0.0 # Valor dos ativos comprados, baseado no custo
288
+ cash_remaining_after_execution = amount # Começa com todo o montante
289
+
290
+ if final_status == "completed" and investment_decisions and exchange:
291
+ logger.info(f"BG TASK [{rnn_tx_id}]: Executando {len(investment_decisions)} ordens...")
292
+ transactions_db[rnn_tx_id]["status_details"] = "Executing investment orders"
293
+ order_execution_overall_success = True
294
+
295
+ # Placeholder para LÓGICA REAL DE EXECUÇÃO DE ORDENS (CREATE_ORDER_PLACEHOLDER)
296
+ # Esta seção precisa ser preenchida com:
297
+ # 1. Iterar sobre `investment_decisions`.
298
+ # 2. Para cada decisão de "BUY":
299
+ # a. Determinar o símbolo correto na exchange (ex: "BTC/USDT").
300
+ # b. Obter o preço atual (ticker) para calcular a quantidade de ativo a comprar.
301
+ # `amount_of_asset = target_usd_amount / current_price_of_asset`
302
+ # c. Considerar saldo disponível na exchange (se estiver gerenciando isso).
303
+ # d. Criar a ordem via `await exchange.create_market_buy_order(symbol, amount_of_asset)`
304
+ # ou `create_limit_buy_order(symbol, amount_of_asset, limit_price)`.
305
+ # Para ordens limite, a RNN precisaria fornecer o `limit_price`.
306
+ # e. Tratar respostas da exchange (sucesso, falha, ID da ordem).
307
+ # `ccxt.InsufficientFunds`, `ccxt.InvalidOrder`, etc.
308
+ # f. Armazenar detalhes da ordem em `executed_trades_info`:
309
+ # { "asset_id": ..., "order_id_exchange": ..., "type": "market/limit", "side": "buy",
310
+ # "requested_usd_amount": ..., "asset_quantity_ordered": ...,
311
+ # "status_from_exchange": ..., "filled_quantity": ..., "average_fill_price": ...,
312
+ # "cost_in_usd": ..., "fees_paid": ..., "timestamp": ... }
313
+ # g. Atualizar `current_portfolio_value` com o `cost_in_usd` da ordem preenchida.
314
+ # h. Deduzir `cost_in_usd` de `cash_remaining_after_execution`.
315
+ # 3. Para decisões de "SELL" (se sua RNN gerar):
316
+ # a. Verificar se você possui o ativo (requer gerenciamento de portfólio).
317
+ # b. Criar ordem de venda.
318
+ # c. Atualizar `current_portfolio_value` e `cash_remaining_after_execution`.
319
+
320
+ # Simulação atual:
321
+ for decision in investment_decisions:
322
+ if decision.get("action") == "BUY" and decision.get("type") == "CRYPTO":
323
+ asset_symbol = decision["asset_id"]
324
+ usd_to_spend = decision["target_usd_amount"]
325
+
326
+ # Simular pequena chance de falha na ordem
327
+ if random.random() < 0.05:
328
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Falha simulada ao executar ordem para {asset_symbol}.")
329
+ executed_trades_info.append({
330
+ "asset_id": asset_symbol, "status": "failed_simulated",
331
+ "requested_usd_amount": usd_to_spend, "error": "Simulated exchange rejection"
332
+ })
333
+ order_execution_overall_success = False # Marca que pelo menos uma falhou
334
+ continue # Pula para a próxima decisão
335
+
336
+ # Simular slippage e custo
337
+ simulated_cost = usd_to_spend * random.uniform(0.995, 1.005) # +/- 0.5% slippage
338
+
339
+ # Garantir que não estamos gastando mais do que o caixa restante
340
+ if simulated_cost > cash_remaining_after_execution:
341
+ simulated_cost = cash_remaining_after_execution # Gasta apenas o que tem
342
+ if simulated_cost < 1: # Se não há quase nada, não faz a ordem
343
+ logger.info(f"BG TASK [{rnn_tx_id}]: Saldo insuficiente ({cash_remaining_after_execution:.2f}) para ordem de {asset_symbol}, pulando.")
344
+ continue
345
+
346
+
347
+ if simulated_cost > 0:
348
+ current_portfolio_value += simulated_cost
349
+ cash_remaining_after_execution -= simulated_cost
350
+ executed_trades_info.append({
351
+ "asset_id": asset_symbol, "order_id_exchange": f"sim_ord_{uuid.uuid4()}",
352
+ "type": "market", "side": "buy",
353
+ "requested_usd_amount": usd_to_spend,
354
+ "status_from_exchange": "filled", "cost_in_usd": round(simulated_cost, 2),
355
+ "timestamp": datetime.utcnow().isoformat()
356
+ })
357
+ logger.info(f"BG TASK [{rnn_tx_id}]: Ordem simulada para {asset_symbol} (custo: {simulated_cost:.2f} USD) preenchida.")
358
+
359
+ await asyncio.sleep(random.uniform(1, 2) * len(investment_decisions) if investment_decisions else 1)
360
+
361
+ if not order_execution_overall_success:
362
+ error_details += "One or more orders failed during execution; "
363
+ # Decida se isso torna o status final 'failed_order_execution' ou se 'completed_with_partial_failure'
364
+ # final_status = "completed_with_partial_failure" # Exemplo de um novo status
365
+
366
+ elif not exchange and investment_decisions:
367
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Decisões de investimento geradas, mas a exchange não está disponível para execução.")
368
+ error_details += "Exchange not available for order execution; "
369
+ final_status = "failed_order_execution" # Se a execução é crítica
370
+ cash_remaining_after_execution = amount # Nada foi gasto
371
+
372
+ transactions_db[rnn_tx_id]["executed_trades"] = executed_trades_info
373
+ transactions_db[rnn_tx_id]["cash_after_execution"] = round(cash_remaining_after_execution, 2)
374
+ transactions_db[rnn_tx_id]["portfolio_value_after_execution"] = round(current_portfolio_value, 2)
375
+
376
+
377
+ # =========================================================================
378
+ # 4. SIMULAÇÃO DO PERÍODO DE INVESTIMENTO E CÁLCULO DE LUCRO/PERDA (Só se não houve falha crítica antes)
379
+ # =========================================================================
380
+ value_of_investments_at_eod = current_portfolio_value # Começa com o valor de custo
381
+
382
+ if final_status == "completed": # Ou "completed_with_partial_failure"
383
+ transactions_db[rnn_tx_id]["status_details"] = "Simulating EOD valuation"
384
+ logger.info(f"BG TASK [{rnn_tx_id}]: Simulando valorização do portfólio no final do dia...")
385
+ await asyncio.sleep(random.uniform(3, 7))
386
+
387
+ if current_portfolio_value > 0:
388
+ # Simular mudança de valor do portfólio. A meta de 4.2% é sobre o capital INVESTIDO.
389
+ # O lucro/perda é aplicado ao `current_portfolio_value` (o que foi efetivamente comprado).
390
+ daily_return_factor = 0.042 # A meta
391
+ simulated_performance_factor = random.uniform(0.7, 1.3) # Variação em torno da meta (pode ser prejuízo)
392
+ # Para ser mais realista, o fator de performance deveria ser algo como:
393
+ # random.uniform(-0.05, 0.08) -> -5% a +8% de retorno diário sobre o investido (ainda alto)
394
+ # E não diretamente ligado à meta de 4.2%
395
+
396
+ # Ajuste para uma simulação de retorno mais plausível (ainda agressiva)
397
+ # Suponha que o retorno diário real possa variar de -3% a +5% sobre o investido
398
+ actual_daily_return_on_portfolio = random.uniform(-0.03, 0.05)
399
+
400
+ profit_or_loss_on_portfolio = current_portfolio_value * actual_daily_return_on_portfolio
401
+ value_of_investments_at_eod = current_portfolio_value + profit_or_loss_on_portfolio
402
+ logger.info(f"BG TASK [{rnn_tx_id}]: Portfólio inicial: {current_portfolio_value:.2f}, Retorno simulado: {actual_daily_return_on_portfolio*100:.2f}%, "
403
+ f"Lucro/Prejuízo no portfólio: {profit_or_loss_on_portfolio:.2f}, Valor EOD do portfólio: {value_of_investments_at_eod:.2f}")
404
+ else:
405
+ logger.info(f"BG TASK [{rnn_tx_id}]: Nenhum portfólio para valorizar no EOD (nada foi comprado).")
406
+ value_of_investments_at_eod = 0.0
407
+
408
+ # O calculated_final_amount é o valor dos investimentos liquidados + o caixa que não foi usado
409
+ calculated_final_amount = value_of_investments_at_eod + cash_remaining_after_execution
410
+
411
+ else: # Se houve falha antes, o valor final é o que sobrou após a falha
412
+ calculated_final_amount = cash_remaining_after_execution + current_portfolio_value # current_portfolio_value pode ser 0 ou parcial
413
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Ciclo de investimento não concluído normalmente ({final_status}). Valor final baseado no estado atual.")
414
+
415
+ transactions_db[rnn_tx_id]["eod_portfolio_value_simulated"] = round(value_of_investments_at_eod, 2)
416
+ transactions_db[rnn_tx_id]["final_calculated_amount"] = round(calculated_final_amount, 2)
417
+
418
+
419
+ # =========================================================================
420
+ # 5. TOKENIZAÇÃO / REGISTRO DA OPERAÇÃO (Só se não houve falha crítica antes)
421
+ # =========================================================================
422
+ if final_status not in ["failed_config", "failed_market_data", "failed_rnn_analysis"]: # Prossegue se ao menos tentou executar
423
+ transactions_db[rnn_tx_id]["status_details"] = "Finalizing transaction log (tokenization)"
424
+ logger.info(f"BG TASK [{rnn_tx_id}]: Registrando (tokenizando) operação detalhadamente...")
425
+ # Placeholder para LÓGICA REAL DE TOKENIZAÇÃO (TOKENIZATION_PLACEHOLDER)
426
+ # 1. Coletar todos os dados relevantes da transação de `transactions_db[rnn_tx_id]`
427
+ # (market_data_collected, rnn_decisions, executed_trades, eod_portfolio_value_simulated, etc.)
428
+ # 2. Se for usar blockchain:
429
+ # a. Preparar os dados para um contrato inteligente.
430
+ # b. Interagir com o contrato (ex: web3.py para Ethereum).
431
+ # c. Armazenar o hash da transação da blockchain.
432
+ # 3. Se for um registro interno avançado:
433
+ # a. Assinar digitalmente os dados da transação.
434
+ # b. Armazenar em um sistema de log imutável ou banco de dados com auditoria.
435
+
436
+ # Simulação atual (hash dos dados da transação):
437
+ transaction_data_for_hash = {
438
+ "rnn_tx_id": rnn_tx_id, "client_id": client_id, "initial_amount": amount,
439
+ "final_amount_calculated": calculated_final_amount,
440
+ # Incluir resumos ou hashes dos dados coletados para não tornar o hash gigante
441
+ "market_data_summary_keys": list(transactions_db[rnn_tx_id].get("market_data_collected", {}).keys()),
442
+ "rnn_decisions_count": len(transactions_db[rnn_tx_id].get("rnn_decisions", [])),
443
+ "executed_trades_count": len(transactions_db[rnn_tx_id].get("executed_trades", [])),
444
+ "eod_portfolio_value": transactions_db[rnn_tx_id].get("eod_portfolio_value_simulated"),
445
+ "timestamp": datetime.utcnow().isoformat()
446
+ }
447
+ ordered_tx_data_str = json.dumps(transaction_data_for_hash, sort_keys=True)
448
+ proof_token_hash = hashlib.sha256(ordered_tx_data_str.encode('utf-8')).hexdigest()
449
+
450
+ transactions_db[rnn_tx_id]["proof_of_operation_token"] = proof_token_hash
451
+ transactions_db[rnn_tx_id]["tokenization_method"] = "internal_summary_hash_proof"
452
+ await asyncio.sleep(0.5) # Simula tempo de escrita/hash
453
+ logger.info(f"BG TASK [{rnn_tx_id}]: Operação registrada. Prova (hash): {proof_token_hash[:10]}...")
454
+
455
+
456
+ # =========================================================================
457
+ # 6. PREPARAR E ENVIAR CALLBACK PARA AIBANK
458
+ # =========================================================================
459
+ if exchange and hasattr(exchange, 'close'):
460
+ try:
461
+ await exchange.close()
462
+ logger.info(f"BG TASK [{rnn_tx_id}]: Conexão ccxt fechada.")
463
+ except Exception as e_close: # Especificar o tipo de exceção se souber
464
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Erro ao fechar conexão ccxt: {str(e_close)}")
465
+
466
+ if not AIBANK_CALLBACK_URL or not CALLBACK_SHARED_SECRET:
467
+ logger.error(f"BG TASK [{rnn_tx_id}]: Configuração de callback ausente. Não é possível notificar o AIBank.")
468
+ transactions_db[rnn_tx_id]["callback_status"] = "config_missing_critical"
469
+ return
470
+
471
+ # Certifique-se que `final_status` reflete o estado real da operação
472
+ # Se `error_details` não estiver vazio e `final_status` ainda for "completed", ajuste-o
473
+ if error_details and final_status == "completed":
474
+ final_status = "completed_with_warnings" # Ou um status mais apropriado
475
+
476
+ callback_payload_data = InvestmentResultPayload(
477
+ rnn_transaction_id=rnn_tx_id, aibank_transaction_token=aibank_tx_token, client_id=client_id,
478
+ initial_amount=amount, final_amount=round(calculated_final_amount, 2), # Arredonda para 2 casas decimais
479
+ profit_loss=round(calculated_final_amount - amount, 2),
480
+ status=final_status, timestamp=datetime.utcnow(),
481
+ details=error_details if error_details else "Investment cycle processed."
482
+ )
483
+ payload_json_str = callback_payload_data.model_dump_json() # Garante que está usando a string serializada
484
+
485
+ signature = hmac.new(CALLBACK_SHARED_SECRET.encode('utf-8'), payload_json_str.encode('utf-8'), hashlib.sha256).hexdigest()
486
+ headers = {'Content-Type': 'application/json', 'X-RNN-Signature': signature}
487
+
488
+ logger.info(f"BG TASK [{rnn_tx_id}]: Enviando callback para AIBank ({AIBANK_CALLBACK_URL}) com status final '{final_status}'. Payload: {payload_json_str}")
489
+ transactions_db[rnn_tx_id]["callback_status"] = "sending"
490
+ try:
491
+ async with httpx.AsyncClient(timeout=30.0) as client: # Timeout global para o cliente
492
+ response = await client.post(AIBANK_CALLBACK_URL, content=payload_json_str, headers=headers)
493
+ response.raise_for_status()
494
+ logger.info(f"BG TASK [{rnn_tx_id}]: Callback para AIBank enviado com sucesso. Resposta: {response.status_code}")
495
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_success_{response.status_code}"
496
+ except httpx.RequestError as e_req:
497
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro de REDE ao enviar callback para AIBank: {e_req}")
498
+ transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_network_error"
499
+ except httpx.HTTPStatusError as e_http:
500
+ 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]}")
501
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_http_error_{e_http.response.status_code}"
502
+ except Exception as e_cb_final:
503
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro INESPERADO ao enviar callback: {e_cb_final}", exc_info=True)
504
+ transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_unknown_error"
505
+
506
+
507
+ import asyncio
508
+ import random
509
+
510
+
511
+
512
+ # --- Endpoints da API ---
513
+ @app.post("/api/invest",
514
+ response_model=InvestmentResponse,
515
+ dependencies=[Depends(verify_aibank_key)])
516
+ async def initiate_investment(
517
+ request_data: InvestmentRequest,
518
+ background_tasks: BackgroundTasks
519
+ ):
520
+ """
521
+ Endpoint para o AIBank iniciar um ciclo de investimento.
522
+ Responde rapidamente e executa a lógica pesada em background.
523
+ """
524
+ logger.info(f"Requisição de investimento recebida para client_id: {request_data.client_id}, "
525
+ f"amount: {request_data.amount}, aibank_tx_token: {request_data.aibank_transaction_token}")
526
+
527
+ rnn_tx_id = str(uuid.uuid4())
528
+
529
+ # Armazena informações iniciais da transação DB real para ser mais robusto
530
+ transactions_db[rnn_tx_id] = {
531
+ "rnn_transaction_id": rnn_tx_id,
532
+ "aibank_transaction_token": request_data.aibank_transaction_token,
533
+ "client_id": request_data.client_id,
534
+ "initial_amount": request_data.amount,
535
+ "status": "pending_background_processing",
536
+ "received_at": datetime.utcnow().isoformat(),
537
+ "callback_status": "not_sent_yet"
538
+ }
539
+
540
+ # Adiciona a tarefa de longa duração ao background
541
+ background_tasks.add_task(
542
+ execute_investment_strategy_background,
543
+ rnn_tx_id,
544
+ request_data.client_id,
545
+ request_data.amount,
546
+ request_data.aibank_transaction_token
547
+ )
548
+
549
+ logger.info(f"Estratégia de investimento para rnn_tx_id: {rnn_tx_id} agendada para execução em background.")
550
+ return InvestmentResponse(
551
+ status="pending",
552
+ message="Investment request received and is being processed in the background. Await callback for results.",
553
+ rnn_transaction_id=rnn_tx_id
554
+ )
555
+
556
+ @app.get("/api/transaction_status/{rnn_tx_id}", response_class=JSONResponse)
557
+ async def get_transaction_status(rnn_tx_id: str):
558
+ """ Endpoint para verificar o status de uma transação (para debug/admin) """
559
+ transaction = transactions_db.get(rnn_tx_id)
560
+ if not transaction:
561
+ raise HTTPException(status_code=404, detail="Transaction not found")
562
+ return transaction
563
+
564
+
565
+ # --- Dashboard (Existente, adaptado) ---
566
+ # Setup para arquivos estáticos e templates
567
+
568
+ try:
569
+ app.mount("/static", StaticFiles(directory="rnn/static"), name="static")
570
+ templates = Environment(loader=FileSystemLoader("rnn/templates"))
571
+ except RuntimeError as e:
572
+ logger.warning(f"Não foi possível montar /static ou carregar templates: {e}. O dashboard pode não funcionar.")
573
+ templates = None # Para evitar erros se o loader falhar
574
+
575
+ @app.get("/", response_class=HTMLResponse)
576
+ async def index(request: Request):
577
+ if not templates:
578
+ return HTMLResponse("<html><body><h1>Dashboard indisponível</h1><p>Configuração de templates/estáticos falhou.</p></body></html>")
579
+
580
+ agora = datetime.now()
581
+ agentes_simulados = [
582
+ # dados de agentes ...
583
+ ]
584
+ template = templates.get_template("index.html")
585
+ # Adicionar transações recentes ao contexto do template
586
+ recent_txs = list(transactions_db.values())[-5:] # Últimas 5 transações
587
+ return HTMLResponse(template.render(request=request, agentes=agentes_simulados, transactions=recent_txs))
588
+
589
+ # --- Imports para Background Task ---
590
+ import asyncio
591
+ import random
592
+
593
+ # Função de logger dummy s
594
+ # class DummyLogger:
595
+ # def info(self, msg, *args, **kwargs): print(f"INFO: {msg}")
596
+ # def warning(self, msg, *args, **kwargs): print(f"WARNING: {msg}")
597
+ # def error(self, msg, *args, **kwargs): print(f"ERROR: {msg}", kwargs.get('exc_info'))
598
+
599
+ # if __name__ == "__main__": # Para teste local
600
+ # # logger = DummyLogger() # se não tiver get_logger()
601
+ # # Configuração das variáveis de ambiente para teste local
602
+ # os.environ["AIBANK_API_KEY"] = "test_aibank_key_from_rnn_server"
603
+ # os.environ["AIBANK_CALLBACK_URL"] = "http://localhost:8001/api/rnn_investment_result_callback" # URL do aibank simulado
604
+ # os.environ["CALLBACK_SHARED_SECRET"] = "super_secret_for_callback_signing"
605
+ # # import uvicorn
606
+ # # uvicorn.run(app, host="0.0.0.0", port=8000)
app-old-no-module.py ADDED
@@ -0,0 +1,901 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ import os
4
+ import uuid
5
+ import time
6
+ import hmac
7
+ import hashlib
8
+ import json
9
+ from datetime import datetime, timedelta
10
+ from typing import Dict, Any
11
+ import ccxt.async_support as ccxt
12
+
13
+ import httpx # Para fazer chamadas HTTP assíncronas (para o callback)
14
+ from fastapi import FastAPI, Request, HTTPException, Depends, Header, BackgroundTasks
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ from fastapi.responses import HTMLResponse, JSONResponse
17
+ from fastapi.staticfiles import StaticFiles
18
+ from jinja2 import Environment, FileSystemLoader
19
+ from pydantic import BaseModel, Field
20
+
21
+ from app. utils.logger import get_logger
22
+
23
+ logger = get_logger()
24
+
25
+ # --- Configuração Inicial e Variáveis de Ambiente (Secrets do Hugging Face) ---
26
+ AIBANK_API_KEY = os.environ.get("AIBANK_API_KEY") # Chave que o aibank usa para chamar esta API RNN
27
+ AIBANK_CALLBACK_URL = os.environ.get("AIBANK_CALLBACK_URL") # URL no aibank para onde esta API RNN enviará o resultado
28
+ CALLBACK_SHARED_SECRET = os.environ.get("CALLBACK_SHARED_SECRET") # Segredo para assinar/verificar o payload do callback
29
+
30
+ # Chaves para serviços externos
31
+ MARKET_DATA_API_KEY = os.environ.get("MARKET_DATA_API_KEY")
32
+ EXCHANGE_API_KEY = os.environ.get("EXCHANGE_API_KEY")
33
+ EXCHANGE_API_SECRET = os.environ.get("EXCHANGE_API_SECRET")
34
+
35
+ if not AIBANK_API_KEY:
36
+ logger.warning("AIBANK_API_KEY não configurada. A autenticação para /api/invest falhou.")
37
+ if not AIBANK_CALLBACK_URL:
38
+ logger.warning("AIBANK_CALLBACK_URL não configurada. O callback para o aibank falhou.")
39
+ if not CALLBACK_SHARED_SECRET:
40
+ logger.warning("CALLBACK_SHARED_SECRET não configurado. A segurança do callback está comprometida.")
41
+
42
+
43
+
44
+ #CHAVES PARA CCXT (para Binance)
45
+ # configuradas como vriaveis no Hugging Face Space
46
+
47
+ CCXT_EXCHANGE_ID = os.environ.get("CCXT_EXCHANGE_ID", "binance")
48
+ CCXT_API_KEY = os.environ.get("CCXT_API_KEY")
49
+ CCXT_API_SECRET = os.environ.get("CCXT_API_SECRET")
50
+ CCXT_API_PASSWORD = os.environ.get("CCXT_API_PASSWORD") # Opcional
51
+
52
+ # Binance Testnet
53
+ CCXT_SANDBOX_MODE = os.environ.get("CCXT_SANDBOX_MODE", "false").lower() == "true"
54
+
55
+
56
+
57
+
58
+
59
+ app = FastAPI(title="ATCoin Neural Agents - Investment API")
60
+
61
+ # --- Middlewares ---
62
+ app.add_middleware(
63
+ CORSMiddleware,
64
+ allow_origins=[
65
+ "http://localhost:3000", # URL desenvolvimento local
66
+ "http://aibank.app.br", # URL de produção
67
+ "https://*.aibank.app.br", # subdomínios
68
+ "https://*.hf.space" # HF Space
69
+ ],
70
+ allow_credentials=True,
71
+ allow_methods=["*"],
72
+ allow_headers=["*"],
73
+ )
74
+
75
+ # --- Simulação de Banco de Dados de Transações DEV ---
76
+ # Em produção MongoDB
77
+ transactions_db: Dict[str, Dict[str, Any]] = {}
78
+
79
+ # --- Modelos Pydantic ---
80
+ class InvestmentRequest(BaseModel):
81
+ client_id: str
82
+ amount: float = Field(..., gt=0) # Garante que o montante seja positivo
83
+ aibank_transaction_token: str # Token único gerado pelo aibank para rastreamento
84
+
85
+ class InvestmentResponse(BaseModel):
86
+ status: str
87
+ message: str
88
+ rnn_transaction_id: str # ID da transação this.API
89
+
90
+ class InvestmentResultPayload(BaseModel): # Payload para o callback para o aibank
91
+ rnn_transaction_id: str
92
+ aibank_transaction_token: str
93
+ client_id: str
94
+ initial_amount: float
95
+ final_amount: float
96
+ profit_loss: float
97
+ status: str # "completed", "failed"
98
+ timestamp: datetime
99
+ details: str = ""
100
+
101
+
102
+ # --- Dependência de Autenticação ---
103
+ async def verify_aibank_key(authorization: str = Header(None)):
104
+ if not AIBANK_API_KEY: # Checagem se a chave do servidor está configurada
105
+ logger.error("CRITICAL: AIBANK_API_KEY (server-side) não está configurada nos Secrets.")
106
+ raise HTTPException(status_code=500, detail="Internal Server Configuration Error: Missing server API Key.")
107
+
108
+ if authorization is None:
109
+ logger.warning("Authorization header ausente na chamada do AIBank.")
110
+ raise HTTPException(status_code=401, detail="Authorization header is missing")
111
+
112
+ parts = authorization.split()
113
+ if len(parts) != 2 or parts[0].lower() != 'bearer':
114
+ logger.warning(f"Formato inválido do Authorization header: {authorization}")
115
+ raise HTTPException(status_code=401, detail="Authorization header must be 'Bearer <token>'")
116
+
117
+ token_from_aibank = parts[1]
118
+ if not hmac.compare_digest(token_from_aibank, AIBANK_API_KEY):
119
+ logger.warning(f"Chave de API inválida fornecida pelo AIBank. Token: {token_from_aibank[:10]}...")
120
+ raise HTTPException(status_code=403, detail="Invalid API Key provided by AIBank.")
121
+ logger.info("API Key do AIBank verificada com sucesso.")
122
+ return True
123
+
124
+
125
+ # --- Lógica de Negócio Principal (Simulada e em Background) ---
126
+
127
+ async def execute_investment_strategy_background(
128
+ rnn_tx_id: str,
129
+ client_id: str,
130
+ amount: float,
131
+ aibank_tx_token: str
132
+ ):
133
+ """
134
+ Esta função roda em background. Simula a coleta de dados, RNN, execução e tokenização.
135
+ No final, chama o callback para o aibank.
136
+ """
137
+ logger.info(f"BG TASK [{rnn_tx_id}]: Iniciando estratégia de investimento para cliente {client_id}, valor {amount}.")
138
+ transactions_db[rnn_tx_id]["status"] = "processing" # Status geral inicial
139
+ transactions_db[rnn_tx_id]["status_details"] = "Initializing investment cycle"
140
+
141
+ final_status = "completed" # Status final presumido
142
+ error_details = ""
143
+ calculated_final_amount = amount # Valor inicial, será modificado
144
+
145
+ # Inicializa o objeto da exchange ccxt
146
+ exchange = None
147
+ if CCXT_API_KEY and CCXT_API_SECRET: #nicializa se as chaves básicas estiverem presentes
148
+ try:
149
+ exchange_class = getattr(ccxt, CCXT_EXCHANGE_ID)
150
+ config = {
151
+ 'apiKey': CCXT_API_KEY,
152
+ 'secret': CCXT_API_SECRET,
153
+ 'enableRateLimit': True, # evitar bans da API
154
+ # 'verbose': True, # Para debug detalhado das chamadas ccxt
155
+ }
156
+ if CCXT_API_PASSWORD:
157
+ config['password'] = CCXT_API_PASSWORD
158
+
159
+ exchange = exchange_class(config)
160
+
161
+ if CCXT_SANDBOX_MODE:
162
+ if hasattr(exchange, 'set_sandbox_mode'): # Nem todas as exchanges suportam isso diretamente em ccxt
163
+ exchange.set_sandbox_mode(True)
164
+ logger.info(f"BG TASK [{rnn_tx_id}]: CCXT configurado para modo SANDBOX para {CCXT_EXCHANGE_ID}.")
165
+ elif 'test' in exchange.urls: # usar testnet another.way
166
+ exchange.urls['api'] = exchange.urls['test']
167
+ logger.info(f"BG TASK [{rnn_tx_id}]: CCXT URLs alteradas para TESTNET para {CCXT_EXCHANGE_ID}.")
168
+ else:
169
+ 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.")
170
+
171
+ # Carregar mercados para validar símbolos, demorado.
172
+ # await exchange.load_markets()
173
+ # logger.info(f"BG TASK [{rnn_tx_id}]: Mercados carregados para {CCXT_EXCHANGE_ID}.")
174
+
175
+ except AttributeError:
176
+ logger.error(f"BG TASK [{rnn_tx_id}]: Exchange ID '{CCXT_EXCHANGE_ID}' inválida ou não suportada pelo ccxt.")
177
+ error_details += f"Invalid CCXT_EXCHANGE_ID: {CCXT_EXCHANGE_ID}; "
178
+ final_status = "failed_config" # Um novo status para falha de configuração
179
+ # (Pular para o callback aqui)
180
+ except Exception as e:
181
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao inicializar ccxt para {CCXT_EXCHANGE_ID}: {str(e)}", exc_info=True)
182
+ error_details += f"CCXT initialization error: {str(e)}; "
183
+ final_status = "failed_config"
184
+ # (Pular para o callback aqui)
185
+ else:
186
+ 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.")
187
+ #Decidir se é uma falha fatal ou se a estratégia pode prosseguir sem dados de cripto.
188
+ # Precisa lidar com dados ausentes.
189
+
190
+
191
+ # =========================================================================
192
+ # 1. COLETAR DADOS DE MERCADO (com ccxt integrado)
193
+ # =========================================================================
194
+ logger.info(f"BG TASK [{rnn_tx_id}]: Coletando dados de mercado...")
195
+ transactions_db[rnn_tx_id]["status_details"] = "Fetching market data"
196
+ market_data_results = {
197
+ "crypto": {}, # Dados específicos de cripto
198
+ "stocks": {}, # Dados de ações (integrar yfinance ou outro)
199
+ "other": {} # Outros dados
200
+ }
201
+ market_data_fetch_success = True
202
+
203
+ # --- Defina os pares de cripto ---
204
+ # Estes poderiam vir de uma configuração, ou serem dinâmicos.
205
+ crypto_pairs_to_fetch = ["BTC/USDT", "ETH/USDT", "SOL/USDT"]
206
+
207
+ if exchange: # Buscar se a exchange foi inicializada corretamente
208
+ try:
209
+ for pair in crypto_pairs_to_fetch:
210
+ pair_data = {}
211
+ if exchange.has['fetchTicker']:
212
+ ticker = await exchange.fetch_ticker(pair)
213
+ pair_data['ticker'] = {
214
+ 'last': ticker.get('last'),
215
+ 'bid': ticker.get('bid'),
216
+ 'ask': ticker.get('ask'),
217
+ 'volume': ticker.get('baseVolume'), # Ou 'quoteVolume'
218
+ 'timestamp': ticker.get('timestamp')
219
+ }
220
+ logger.info(f"BG TASK [{rnn_tx_id}]: Ticker {pair}: Preço {ticker.get('last')}")
221
+
222
+ if exchange.has['fetchOHLCV']:
223
+ # timeframe: '1m', '5m', '15m', '1h', '4h', '1d', '1w', '1M'
224
+ # limit: número de candles
225
+ ohlcv = await exchange.fetch_ohlcv(pair, timeframe='1h', limit=72) # Últimas 72 horas
226
+ # Formato OHLCV: [timestamp, open, high, low, close, volume]
227
+ pair_data['ohlcv_1h'] = ohlcv
228
+ logger.info(f"BG TASK [{rnn_tx_id}]: Coletado {len(ohlcv)} candles OHLCV para {pair} (1h).")
229
+
230
+ # Outros dados que ccxt:
231
+ # if exchange.has['fetchOrderBook']:
232
+ # order_book = await exchange.fetch_order_book(pair, limit=5) # Top 5 bids/asks
233
+ # pair_data['order_book_L1'] = { # Exemplo de Level 1
234
+ # 'bids': order_book['bids'][0] if order_book['bids'] else None,
235
+ # 'asks': order_book['asks'][0] if order_book['asks'] else None,
236
+ # }
237
+ # if exchange.has['fetchTrades']: # Últimas negociações públicas
238
+ # trades = await exchange.fetch_trades(pair, limit=10)
239
+ # pair_data['recent_trades'] = trades
240
+
241
+ market_data_results["crypto"][pair.replace("/", "_")] = pair_data # Usar _ para chaves de dict seguras
242
+
243
+ logger.info(f"BG TASK [{rnn_tx_id}]: Dados de cripto via ccxt coletados.")
244
+
245
+ except ccxt.NetworkError as e:
246
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro de rede ccxt ao coletar dados de mercado: {str(e)}", exc_info=True)
247
+ market_data_fetch_success = False
248
+ error_details += f"CCXT NetworkError: {str(e)}; "
249
+ except ccxt.ExchangeError as e:
250
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro da exchange ccxt ao coletar dados de mercado: {str(e)}", exc_info=True)
251
+ market_data_fetch_success = False
252
+ error_details += f"CCXT ExchangeError: {str(e)}; "
253
+ except Exception as e:
254
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro geral ao coletar dados de cripto via ccxt: {str(e)}", exc_info=True)
255
+ market_data_fetch_success = False
256
+ error_details += f"General CCXT data collection error: {str(e)}; "
257
+ else:
258
+ logger.info(f"BG TASK [{rnn_tx_id}]: Instância da exchange ccxt não disponível. Pulando coleta de dados de cripto.")
259
+ # Se não há 'exchange', a coleta de dados de cripto não pode ocorrerá.
260
+ # Erro, dependendo se as chaves Not.ok.
261
+ if CCXT_API_KEY and CCXT_API_SECRET: # Chaves dadas mas a 'exchange' falhou na init
262
+ market_data_fetch_success = False # Falha se a config estava lá
263
+ error_details += "CCXT exchange object not initialized despite API keys being present; "
264
+
265
+
266
+ # --- Coleta de dados para outros tipos de ativos (Ações com yfinance) ---
267
+ # try:
268
+ # import yfinance as yf
269
+ # aapl = yf.Ticker("AAPL")
270
+ # hist_aapl = aapl.history(period="5d", interval="1h") # Últimos 5 dias, candles de 1h
271
+ # if not hist_aapl.empty:
272
+ # market_data_results["stocks"]["AAPL"] = {
273
+ # "ohlcv_1h": [[ts.timestamp() * 1000] + list(row) for ts, row in hist_aapl[['Open', 'High', 'Low', 'Close', 'Volume']].iterrows()]
274
+ # }
275
+ # logger.info(f"BG TASK [{rnn_tx_id}]: Coletado {len(hist_aapl)} candles para AAPL via yfinance.")
276
+ # except Exception as e:
277
+ # logger.warning(f"BG TASK [{rnn_tx_id}]: Falha ao buscar dados de ações com yfinance: {e}")
278
+ #
279
+
280
+
281
+ # --- Simulação para outros dados, se necessário ---
282
+ market_data_results["other"]['simulated_index_level'] = random.uniform(10000, 15000)
283
+ market_data_results["other"]['simulated_crypto_sentiment'] = random.uniform(-1, 1)
284
+
285
+ 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
286
+ final_status = "failed_market_data"
287
+ logger.error(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado falhou criticamente. {error_details}")
288
+ # (Pular para o callback aqui, pois a RNN pode não ter dados suficientes)
289
+ # 'return' ou lógica de pular precisa ser implementada se a falha for fatal
290
+ # Registrar e permitir que a lógica da RNN tente lidar com dados ausentes/parciais
291
+ pass
292
+
293
+ transactions_db[rnn_tx_id]["market_data_collected"] = market_data_results # Armazena para auditoria/debug
294
+ transactions_db[rnn_tx_id]["status_details"] = "Processing RNN analysis"
295
+ logger.info(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado concluída (sucesso: {market_data_fetch_success}).")
296
+
297
+ # =========================================================================
298
+ # 2. ANÁLISE PELA RNN E TOMADA DE DECISÃO
299
+ # =========================================================================
300
+ # (alimentada por market_data_results)
301
+ # ...
302
+ logger.info(f"BG TASK [{rnn_tx_id}]: Executando análise RNN com os dados coletados...")
303
+ transactions_db[rnn_tx_id]["status_details"] = "Running RNN model"
304
+ investment_decisions = []
305
+ rnn_analysis_success = True
306
+
307
+ try:
308
+
309
+ # A RNN precisará ser capaz de lidar com a estrutura de market_data_results,
310
+ # incluindo a possibilidade de alguns dados estarem ausentes se a coleta falhar.
311
+
312
+ # investment_decisions = await rnn_model.predict_async(market_data_results, amount_to_invest=amount)
313
+
314
+ # Simulação para prosseguir:
315
+ await asyncio.sleep(random.uniform(8, 15))
316
+ if market_data_results["crypto"]: # Com dados de cripto
317
+ if random.random() > 0.1:
318
+ num_crypto_assets_to_invest = random.randint(1, len(crypto_pairs_to_fetch))
319
+ chosen_pairs = random.sample(list(market_data_results["crypto"].keys()), k=num_crypto_assets_to_invest)
320
+
321
+ for crypto_key in chosen_pairs: # crypto_key "BTC_USDT"
322
+ asset_symbol = crypto_key.replace("_", "/") # Converte de volta para "BTC/USDT"
323
+ allocated_amount = (amount / num_crypto_assets_to_invest) * random.uniform(0.7, 0.9) # Aloca uma parte do total
324
+ investment_decisions.append({
325
+ "asset_id": asset_symbol, # Usa o símbolo da exchange
326
+ "type": "CRYPTO",
327
+ "action": "BUY",
328
+ "target_usd_amount": round(allocated_amount, 2),
329
+ "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')}"
330
+ })
331
+ else:
332
+ logger.info(f"BG TASK [{rnn_tx_id}]: Sem dados de cripto para a RNN processar.")
333
+
334
+ if not investment_decisions:
335
+ logger.info(f"BG TASK [{rnn_tx_id}]: RNN não gerou nenhuma decisão de investimento.")
336
+ else:
337
+ logger.info(f"BG TASK [{rnn_tx_id}]: RNN gerou {len(investment_decisions)} decisões: {investment_decisions}")
338
+
339
+ except Exception as e:
340
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante análise RNN: {str(e)}", exc_info=True)
341
+ rnn_analysis_success = False
342
+ error_details += f"RNN analysis failed: {str(e)}; "
343
+
344
+ if not rnn_analysis_success:
345
+ final_status = "failed_rnn_analysis"
346
+ # (Pular para o callback)
347
+ pass
348
+
349
+ transactions_db[rnn_tx_id]["rnn_decisions"] = investment_decisions
350
+ transactions_db[rnn_tx_id]["status_details"] = "Preparing to execute orders"
351
+ total_usd_allocated_by_rnn = sum(d['target_usd_amount'] for d in investment_decisions if d['action'] == 'BUY')
352
+
353
+
354
+ # =========================================================================
355
+ # 3. EXECUÇÃO DE ORDENS (Será detalhado no próximo passo)
356
+ # =========================================================================
357
+ # ... (execução de ordens aqui, usando `exchange` ccxt e `investment_decisions`)
358
+ # ... (Incluindo cálculo de `current_portfolio_value` e `cash_remaining_after_allocation`)
359
+ logger.info(f"BG TASK [{rnn_tx_id}]: Executando ordens baseadas nas decisões da RNN...")
360
+ transactions_db[rnn_tx_id]["status_details"] = "Executing investment orders"
361
+ executed_trades_info = []
362
+ order_execution_success = True
363
+ cash_remaining_after_allocation = amount - total_usd_allocated_by_rnn
364
+ current_portfolio_value = 0 # Valor dos ativos comprados
365
+
366
+ # Placeholder para execução
367
+ if investment_decisions:
368
+ for decision in investment_decisions:
369
+ if decision.get("action") == "BUY":
370
+ usd_amount_to_spend = decision.get("target_usd_amount")
371
+ simulated_cost = usd_amount_to_spend * random.uniform(0.99, 1.0)
372
+ executed_trades_info.append({
373
+ "asset": decision.get("asset_id"), "order_id": f"sim_ord_{uuid.uuid4()}",
374
+ "status": "filled", "cost_usd": simulated_cost
375
+ })
376
+ current_portfolio_value += simulated_cost
377
+ await asyncio.sleep(random.uniform(1,3) * len(investment_decisions)) # Simula tempo de execução
378
+ else:
379
+ cash_remaining_after_allocation = amount
380
+
381
+ transactions_db[rnn_tx_id]["executed_trades"] = executed_trades_info
382
+ transactions_db[rnn_tx_id]["status_details"] = "Simulating holding period and profit/loss"
383
+
384
+
385
+ # =========================================================================
386
+ # 4. SIMULAÇÃO DO PERÍODO DE INVESTIMENTO E CÁLCULO DE LUCRO/PERDA
387
+ # =========================================================================
388
+ # ... (Inserir lógia )
389
+ logger.info(f"BG TASK [{rnn_tx_id}]: Simulando período de investimento e fechamento de posições...")
390
+ await asyncio.sleep(random.uniform(5, 10))
391
+
392
+ if current_portfolio_value > 0:
393
+ profit_on_invested_part = current_portfolio_value * (0.042 * random.uniform(0.8, 1.2))
394
+ value_of_investments_at_eod = current_portfolio_value + profit_on_invested_part
395
+ else:
396
+ value_of_investments_at_eod = 0
397
+ profit_on_invested_part = 0
398
+
399
+ calculated_final_amount = value_of_investments_at_eod + cash_remaining_after_allocation
400
+
401
+ logger.info(f"BG TASK [{rnn_tx_id}]: Valor inicial total: {amount:.2f}. "
402
+ f"Valor alocado para investimento: {total_usd_allocated_by_rnn:.2f}. "
403
+ f"Valor dos investimentos no EOD: {value_of_investments_at_eod:.2f}. "
404
+ f"Caixa não alocado: {cash_remaining_after_allocation:.2f}. "
405
+ f"Valor final total: {calculated_final_amount:.2f}")
406
+
407
+ transactions_db[rnn_tx_id]["eod_portfolio_value_simulated"] = value_of_investments_at_eod
408
+
409
+ # =========================================================================
410
+ # 5. TOKENIZAÇÃO / REGISTRO DA OPERAÇÃO
411
+ # =========================================================================
412
+ # ... (Inserir)
413
+ logger.info(f"BG TASK [{rnn_tx_id}]: Registrando (tokenizando) operação detalhadamente...")
414
+ # ... (código do hash proof)
415
+ await asyncio.sleep(1)
416
+
417
+ # =========================================================================
418
+ # 6. PREPARAR E ENVIAR CALLBACK PARA AIBANK
419
+ # =========================================================================
420
+ # (Fechamento da conexão ccxt antes do callback, se a conexão foi aberta e bem sucedida)
421
+ if exchange and hasattr(exchange, 'close'):
422
+ try:
423
+ await exchange.close()
424
+ logger.info(f"BG TASK [{rnn_tx_id}]: Conexão ccxt com {CCXT_EXCHANGE_ID} fechada.")
425
+ except Exception as e:
426
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Erro ao fechar conexão ccxt: {str(e)}")
427
+
428
+ # (callback , usando `final_status`, `error_details`, `calculated_final_amount`)
429
+ # ...
430
+ if not AIBANK_CALLBACK_URL or not CALLBACK_SHARED_SECRET:
431
+ logger.error(f"BG TASK [{rnn_tx_id}]: AIBANK_CALLBACK_URL ou CALLBACK_SHARED_SECRET não configurado. Não é possível enviar callback.")
432
+ transactions_db[rnn_tx_id]["callback_status"] = "config_missing"
433
+ return # Não pode prosseguir sem config de callback
434
+
435
+ callback_payload_data = InvestmentResultPayload(
436
+ rnn_transaction_id=rnn_tx_id,
437
+ aibank_transaction_token=aibank_tx_token,
438
+ client_id=client_id,
439
+ initial_amount=amount,
440
+ final_amount=calculated_final_amount,
441
+ profit_loss=calculated_final_amount - amount,
442
+ status=final_status, # O status final da operação
443
+ timestamp=datetime.utcnow(),
444
+ details=error_details if final_status != "completed" else "Investment cycle completed successfully."
445
+ )
446
+ payload_json = callback_payload_data.model_dump_json()
447
+ signature = hmac.new(CALLBACK_SHARED_SECRET.encode('utf-8'), payload_json.encode('utf-8'), hashlib.sha256).hexdigest()
448
+ headers = {'Content-Type': 'application/json', 'X-RNN-Signature': signature}
449
+
450
+ logger.info(f"BG TASK [{rnn_tx_id}]: Enviando callback para AIBank: {AIBANK_CALLBACK_URL} com status final '{final_status}'")
451
+ transactions_db[rnn_tx_id]["callback_status"] = "sending"
452
+ try:
453
+ async with httpx.AsyncClient() as client:
454
+ response = await client.post(AIBANK_CALLBACK_URL, content=payload_json, headers=headers, timeout=30.0)
455
+ response.raise_for_status()
456
+ logger.info(f"BG TASK [{rnn_tx_id}]: Callback enviado com sucesso para AIBank. Status da resposta: {response.status_code}")
457
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_success_{response.status_code}"
458
+ except Exception as e: # Captura mais genérica para erros de callback
459
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao enviar callback para AIBank: {str(e)}", exc_info=True)
460
+ # Classificar o erro para o log
461
+ if isinstance(e, httpx.RequestError):
462
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_request_error"
463
+ elif isinstance(e, httpx.HTTPStatusError):
464
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_http_error_{e.response.status_code}"
465
+ else:
466
+ transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_unknown_error"
467
+
468
+
469
+
470
+ # ---
471
+ """ async def execute_investment_strategy_background(
472
+ rnn_tx_id: str,
473
+ client_id: str,
474
+ amount: float,
475
+ aibank_tx_token: str
476
+ ):
477
+
478
+ logger.info(f"BG TASK [{rnn_tx_id}]: Iniciando estratégia de investimento para cliente {client_id}, valor {amount}.")
479
+ transactions_db[rnn_tx_id]["status"] = "processing_market_data"
480
+
481
+ final_status = "completed"
482
+ error_details = ""
483
+ calculated_final_amount = amount # Valor inicial
484
+
485
+ try:
486
+ # 1. COLETAR DADOS DE MERCADO (Placeholder)
487
+
488
+ logger.info(f"BG TASK [{rnn_tx_id}]: Coletando dados de mercado...")
489
+ transactions_db[rnn_tx_id]["status_details"] = "Fetching market data"
490
+ market_data_results = {}
491
+ market_data_fetch_success = True
492
+
493
+ # Exemplo para Cripto com ccxt (requer 'pip install ccxt')
494
+ # import ccxt.async_support as ccxt # Coloque no topo do app.py
495
+ # exchange_id = 'binance' # Exemplo
496
+ # exchange_class = getattr(ccxt, exchange_id)
497
+ # exchange = exchange_class({
498
+ # 'apiKey': EXCHANGE_API_KEY, # Do os.environ
499
+ # 'secret': EXCHANGE_API_SECRET, # Do os.environ
500
+ # 'enableRateLimit': True, # Importante
501
+ # })
502
+
503
+ try:
504
+ # --- Exemplo para buscar dados de BTC/USDT ---
505
+ # if exchange.has['fetchTicker']:
506
+ # ticker_btc = await exchange.fetch_ticker('BTC/USDT')
507
+ # market_data_results['BTC_USDT_ticker'] = ticker_btc
508
+ # logger.info(f"BG TASK [{rnn_tx_id}]: Ticker BTC/USDT: {ticker_btc['last']}")
509
+ # if exchange.has['fetchOHLCV']:
510
+ # ohlcv_btc = await exchange.fetch_ohlcv('BTC/USDT', timeframe='1h', limit=100) # Últimas 100 horas
511
+ # market_data_results['BTC_USDT_ohlcv_1h'] = ohlcv_btc
512
+ # logger.info(f"BG TASK [{rnn_tx_id}]: Coletado {len(ohlcv_btc)} candles OHLCV para BTC/USDT 1h.")
513
+
514
+ # --- Exemplo para Ações com yfinance (requer 'pip install yfinance') ---
515
+ # import yfinance as yf # Coloque no topo do app.py
516
+ # aapl = yf.Ticker("AAPL")
517
+ # hist_aapl = aapl.history(period="1mo") # Dados do último mês
518
+ # market_data_results['AAPL_history_1mo'] = hist_aapl.to_dict() # Pode ser grande, serialize com cuidado
519
+ # current_price_aapl = hist_aapl['Close'].iloc[-1] if not hist_aapl.empty else None
520
+ # market_data_results['AAPL_current_price'] = current_price_aapl
521
+ # logger.info(f"BG TASK [{rnn_tx_id}]: Preço atual AAPL (yfinance): {current_price_aapl}")
522
+
523
+ # --- Placeholder para sua lógica real de coleta ---
524
+ # Você precisará definir QUAIS ativos e QUAIS dados são necessários para sua RNN.
525
+ # Este é um ponto crucial para sua estratégia.
526
+ # Simulação para prosseguir:
527
+ await asyncio.sleep(random.uniform(3, 7)) # Simula demora da coleta real
528
+ market_data_results['simulated_index_level'] = random.uniform(10000, 15000)
529
+ market_data_results['simulated_crypto_sentiment'] = random.uniform(-1, 1)
530
+ logger.info(f"BG TASK [{rnn_tx_id}]: Dados de mercado (simulados/reais) coletados.")
531
+
532
+ except Exception as e:
533
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao coletar dados de mercado: {str(e)}", exc_info=True)
534
+ market_data_fetch_success = False
535
+ error_details += f"Market data collection failed: {str(e)}; "
536
+ # Decida se a falha aqui é crítica e impede de continuar.
537
+ # Se sim, atualize final_status e pule para o callback.
538
+
539
+ # finally: # Importante para fechar conexões de exchange em ccxt
540
+ # if 'exchange' in locals() and hasattr(exchange, 'close'):
541
+ # await exchange.close()
542
+
543
+ if not market_data_fetch_success:
544
+ # Lógica para lidar com falha na coleta de dados (ex: não prosseguir)
545
+ final_status = "failed_market_data"
546
+ # ... (atualize transactions_db e pule para a seção de callback) ...
547
+ # (Este 'return' ou lógica de pular precisa ser implementada se a falha for fatal)
548
+ pass # Por ora, deixamos prosseguir com dados possivelmente incompletos ou apenas simulados
549
+
550
+ transactions_db[rnn_tx_id]["market_data_collected"] = market_data_results # Armazene para auditoria/debug
551
+ transactions_db[rnn_tx_id]["status_details"] = "Processing RNN analysis"
552
+
553
+
554
+ # 2. ANÁLISE PELA RNN E TOMADA DE DECISÃO (Placeholder)
555
+ l
556
+
557
+ logger.info(f"BG TASK [{rnn_tx_id}]: Executando análise RNN com os dados coletados...")
558
+ transactions_db[rnn_tx_id]["status_details"] = "Running RNN model"
559
+ investment_decisions = [] # Lista de decisões: {'asset': 'BTC/USDT', 'action': 'BUY', 'amount_usd': 5000, 'price_target': 70000}
560
+ rnn_analysis_success = True
561
+
562
+ try:
563
+ # --- SUBSTITUA PELA CHAMADA AO SEU MODELO RNN ---
564
+ # Exemplo: Supondo que você tenha uma classe ou função rnn_predictor
565
+ # from rnn.models.predictor import rnn_predictor # Exemplo de import
566
+
567
+ # Supondo que seu predictor precise dos dados de mercado e do montante a investir
568
+ # investment_decisions = await rnn_predictor.generate_signals_async(
569
+ # market_data_results,
570
+ # amount_to_invest=amount # O montante total disponível para este ciclo
571
+ # )
572
+
573
+ # Simulação para prosseguir:
574
+ await asyncio.sleep(random.uniform(8, 15)) # Simula processamento da RNN
575
+ if random.random() > 0.1: # 90% de chance de "decidir" investir
576
+ num_assets_to_invest = random.randint(1, 3)
577
+ for i in range(num_assets_to_invest):
578
+ asset_name = random.choice(["SIMULATED_CRYPTO_X", "SIMULATED_STOCK_Y", "SIMULATED_BOND_Z"])
579
+ action = random.choice(["BUY", "HOLD"]) # Simplificado, sem SELL por enquanto
580
+ if action == "BUY":
581
+ # Alocar uma porção do 'amount' total para este ativo
582
+ allocated_amount = (amount / num_assets_to_invest) * random.uniform(0.8, 1.0)
583
+ investment_decisions.append({
584
+ "asset_id": f"{asset_name}_{i}",
585
+ "type": "CRYPTO" if "CRYPTO" in asset_name else "STOCK", # Exemplo
586
+ "action": action,
587
+ "target_usd_amount": round(allocated_amount, 2),
588
+ "reasoning": "RNN signal strong based on simulated data" # Adicione o output real da RNN
589
+ })
590
+
591
+ if not investment_decisions:
592
+ logger.info(f"BG TASK [{rnn_tx_id}]: RNN não gerou nenhuma decisão de investimento (ou decidiu não investir).")
593
+ # Isso pode ser um resultado válido.
594
+ else:
595
+ logger.info(f"BG TASK [{rnn_tx_id}]: RNN gerou {len(investment_decisions)} decisões: {investment_decisions}")
596
+
597
+ except Exception as e:
598
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante análise RNN: {str(e)}", exc_info=True)
599
+ rnn_analysis_success = False
600
+ error_details += f"RNN analysis failed: {str(e)}; "
601
+
602
+ if not rnn_analysis_success:
603
+ final_status = "failed_rnn_analysis"
604
+ # ... (atualize transactions_db e pule para a seção de callback) ...
605
+ pass
606
+
607
+ transactions_db[rnn_tx_id]["rnn_decisions"] = investment_decisions
608
+ transactions_db[rnn_tx_id]["status_details"] = "Preparing to execute orders"
609
+
610
+ # Antes de executar ordens, vamos calcular o valor que REALMENTE foi investido
611
+ # e o que pode ter sobrado, já que a RNN pode não usar todo o 'amount'.
612
+ total_usd_allocated_by_rnn = sum(d['target_usd_amount'] for d in investment_decisions if d['action'] == 'BUY')
613
+ # calculated_final_amount = amount # Inicializa com o montante original
614
+ # Esta variável será atualizada após a execução das ordens e cálculo do lucro/perda
615
+
616
+
617
+ # 3. EXECUÇÃO DE ORDENS (Placeholder)
618
+
619
+ # Dentro de execute_investment_strategy_background, substituindo a seção de execução de ordens:
620
+
621
+ logger.info(f"BG TASK [{rnn_tx_id}]: Executando ordens baseadas nas decisões da RNN...")
622
+ transactions_db[rnn_tx_id]["status_details"] = "Executing investment orders"
623
+ executed_trades_info = []
624
+ order_execution_success = True
625
+
626
+ # O calculated_final_amount começa como o 'amount' inicial.
627
+ # Vamos deduzir o que foi efetivamente usado para compras
628
+ # e depois adicionar os lucros/perdas.
629
+ # Por enquanto, vamos assumir que o investimento visa usar o 'total_usd_allocated_by_rnn'.
630
+ # E o restante do 'amount' não alocado fica como "cash".
631
+ cash_remaining_after_allocation = amount - total_usd_allocated_by_rnn
632
+ current_portfolio_value = 0 # Valor dos ativos comprados
633
+
634
+ # --- Exemplo de lógica de execução para decisões de COMPRA (BUY) ---
635
+ if investment_decisions: # Apenas se houver decisões
636
+ # exchange_exec = ccxt.binance({'apiKey': EXCHANGE_API_KEY, 'secret': EXCHANGE_API_SECRET}) # Exemplo
637
+ try:
638
+ for decision in investment_decisions:
639
+ if decision.get("action") == "BUY":
640
+ asset_id_to_buy = decision.get("asset_id") # Ex: "BTC/USDT" ou um ID interno que mapeia para um símbolo
641
+ usd_amount_to_spend = decision.get("target_usd_amount")
642
+
643
+ logger.info(f"BG TASK [{rnn_tx_id}]: Tentando comprar {usd_amount_to_spend} USD de {asset_id_to_buy}")
644
+
645
+ # --- SUBSTITUA PELA LÓGICA REAL DE EXECUÇÃO NA EXCHANGE ---
646
+ # Exemplo com ccxt (precisa de mais detalhes como símbolo de mercado correto):
647
+ # symbol_on_exchange = convert_asset_id_to_exchange_symbol(asset_id_to_buy, exchange_exec.id)
648
+ # current_price = (await exchange_exec.fetch_ticker(symbol_on_exchange))['last']
649
+ # amount_of_asset_to_buy = usd_amount_to_spend / current_price
650
+
651
+ # order = await exchange_exec.create_market_buy_order(symbol_on_exchange, amount_of_asset_to_buy)
652
+ # logger.info(f"BG TASK [{rnn_tx_id}]: Ordem de compra para {asset_id_to_buy} enviada: {order['id']}")
653
+ # executed_trades_info.append({
654
+ # "asset": asset_id_to_buy,
655
+ # "order_id": order['id'],
656
+ # "status": order.get('status', 'unknown'),
657
+ # "amount_filled": order.get('filled', 0),
658
+ # "avg_price": order.get('average', current_price),
659
+ # "cost_usd": order.get('cost', usd_amount_to_spend), # Custo real da ordem
660
+ # "fees": order.get('fee', {}),
661
+ # })
662
+ # current_portfolio_value += order.get('cost', usd_amount_to_spend) # Adiciona o valor do ativo comprado
663
+
664
+ # Simulação para prosseguir:
665
+ await asyncio.sleep(random.uniform(1, 3)) # Simula envio de ordem
666
+ simulated_order_id = f"sim_ord_{uuid.uuid4()}"
667
+ simulated_cost = usd_amount_to_spend * random.uniform(0.99, 1.0) # Slippage simulado
668
+ executed_trades_info.append({
669
+ "asset": asset_id_to_buy,
670
+ "order_id": simulated_order_id,
671
+ "status": "filled",
672
+ "amount_filled": simulated_cost / random.uniform(100, 200), # Qtd de ativo simulada
673
+ "avg_price": random.uniform(100, 200), # Preço simulado
674
+ "cost_usd": simulated_cost,
675
+ "fees": {"currency": "USD", "cost": simulated_cost * 0.001} # Taxa simulada
676
+ })
677
+ current_portfolio_value += simulated_cost
678
+ logger.info(f"BG TASK [{rnn_tx_id}]: Ordem simulada {simulated_order_id} para {asset_id_to_buy} preenchida, custo {simulated_cost:.2f} USD.")
679
+ else:
680
+ 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.")
681
+
682
+ except Exception as e:
683
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante execução de ordens: {str(e)}", exc_info=True)
684
+ order_execution_success = False
685
+ error_details += f"Order execution failed: {str(e)}; "
686
+ # finally:
687
+ # if 'exchange_exec' in locals() and hasattr(exchange_exec, 'close'):
688
+ # await exchange_exec.close()
689
+ else:
690
+ logger.info(f"BG TASK [{rnn_tx_id}]: Nenhuma decisão de investimento para executar.")
691
+ # Se não há decisões, o current_portfolio_value é 0 e o cash_remaining é todo o 'amount'
692
+ cash_remaining_after_allocation = amount
693
+
694
+ if not order_execution_success:
695
+ final_status = "failed_order_execution"
696
+ # ... (atualize transactions_db e pule para a seção de callback) ...
697
+ # O valor do portfólio aqui pode ser parcial se algumas ordens falharam
698
+ pass
699
+
700
+ transactions_db[rnn_tx_id]["executed_trades"] = executed_trades_info
701
+ transactions_db[rnn_tx_id]["status_details"] = "Simulating holding period and profit/loss"
702
+
703
+ # --- SIMULAÇÃO DO PERÍODO DE INVESTIMENTO E CÁLCULO DE LUCRO/PERDA DIÁRIO ---
704
+ # Em um sistema real, você monitoraria as posições e as fecharia no final do dia.
705
+ # Ou, se for um investimento de mais longo prazo, apenas calcularia o valor atual do portfólio.
706
+ # Para o objetivo de 4.2% ao dia, é implícito que as posições são fechadas diariamente.
707
+
708
+ logger.info(f"BG TASK [{rnn_tx_id}]: Simulando período de investimento e fechamento de posições...")
709
+ await asyncio.sleep(random.uniform(5, 10)) # Simula o dia passando
710
+
711
+ # Supondo que todas as posições são vendidas no final do "dia"
712
+ # E o current_portfolio_value muda com base no mercado.
713
+ # Para simular o objetivo de 4.2% sobre o VALOR INVESTIDO (current_portfolio_value no momento da compra):
714
+ if current_portfolio_value > 0: # Se algo foi investido
715
+ profit_on_invested_part = current_portfolio_value * (0.042 * random.uniform(0.8, 1.2)) # Simula variação no lucro
716
+ value_of_investments_at_eod = current_portfolio_value + profit_on_invested_part
717
+ else: # Nada foi investido
718
+ value_of_investments_at_eod = 0
719
+ profit_on_invested_part = 0
720
+
721
+ # O calculated_final_amount é o valor dos investimentos no fim do dia + o caixa que não foi alocado
722
+ calculated_final_amount = value_of_investments_at_eod + cash_remaining_after_allocation
723
+
724
+ logger.info(f"BG TASK [{rnn_tx_id}]: Valor inicial total: {amount:.2f}. "
725
+ f"Valor alocado para investimento: {total_usd_allocated_by_rnn:.2f}. "
726
+ f"Valor dos investimentos no EOD: {value_of_investments_at_eod:.2f}. "
727
+ f"Caixa não alocado: {cash_remaining_after_allocation:.2f}. "
728
+ f"Valor final total: {calculated_final_amount:.2f}")
729
+
730
+ transactions_db[rnn_tx_id]["eod_portfolio_value_simulated"] = value_of_investments_at_eod
731
+
732
+
733
+
734
+ # 4. TOKENIZAÇÃO / REGISTRO DA OPERAÇÃO (Placeholder)
735
+ logger.info(f"BG TASK [{rnn_tx_id}]: Registrando (tokenizando) operação...")
736
+ # Aqui você implementaria sua lógica de tokenização.
737
+ # Poderia ser salvar em uma blockchain, ou um registro detalhado e imutável no seu DB.
738
+ # Ex: await tokenize_operation_async(rnn_tx_id, client_id, investment_decisions, execution_results)
739
+ await asyncio.sleep(2)
740
+ transactions_db[rnn_tx_id]["tokenization_status"] = "completed"
741
+ logger.info(f"BG TASK [{rnn_tx_id}]: Operação registrada/tokenizada.")
742
+
743
+ transactions_db[rnn_tx_id]["status"] = "completed"
744
+ transactions_db[rnn_tx_id]["final_amount"] = calculated_final_amount
745
+ transactions_db[rnn_tx_id]["profit_loss"] = calculated_final_amount - amount
746
+
747
+ except Exception as e:
748
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante execução da estratégia: {str(e)}", exc_info=True)
749
+ final_status = "failed"
750
+ error_details = str(e)
751
+ transactions_db[rnn_tx_id]["status"] = "failed"
752
+ transactions_db[rnn_tx_id]["error"] = error_details
753
+ # Em caso de falha, o final_amount pode ser o inicial ou o que foi possível recuperar
754
+ calculated_final_amount = amount # Ou o valor parcial se algumas ordens falharam
755
+
756
+ # 5. PREPARAR E ENVIAR CALLBACK PARA AIBANK
757
+ if not AIBANK_CALLBACK_URL or not CALLBACK_SHARED_SECRET:
758
+ logger.error(f"BG TASK [{rnn_tx_id}]: AIBANK_CALLBACK_URL ou CALLBACK_SHARED_SECRET não configurado. Não é possível enviar callback.")
759
+ transactions_db[rnn_tx_id]["callback_status"] = "config_missing"
760
+ return
761
+
762
+ callback_payload = InvestmentResultPayload(
763
+ rnn_transaction_id=rnn_tx_id,
764
+ aibank_transaction_token=aibank_tx_token,
765
+ client_id=client_id,
766
+ initial_amount=amount,
767
+ final_amount=calculated_final_amount,
768
+ profit_loss=calculated_final_amount - amount,
769
+ status=final_status,
770
+ timestamp=datetime.utcnow(),
771
+ details=error_details if final_status == "failed" else "Investment cycle completed."
772
+ )
773
+
774
+ payload_json = callback_payload.model_dump_json()
775
+
776
+ # Criar assinatura HMAC para segurança do callback
777
+ signature = hmac.new(
778
+ CALLBACK_SHARED_SECRET.encode('utf-8'),
779
+ payload_json.encode('utf-8'),
780
+ hashlib.sha256
781
+ ).hexdigest()
782
+
783
+ headers = {
784
+ 'Content-Type': 'application/json',
785
+ 'X-RNN-Signature': signature # Assinatura para o aibank verificar
786
+ }
787
+
788
+ logger.info(f"BG TASK [{rnn_tx_id}]: Enviando callback para AIBank: {AIBANK_CALLBACK_URL}")
789
+ transactions_db[rnn_tx_id]["callback_status"] = "sending"
790
+ try:
791
+ async with httpx.AsyncClient() as client:
792
+ response = await client.post(AIBANK_CALLBACK_URL, content=payload_json, headers=headers, timeout=30.0)
793
+ response.raise_for_status() # Lança exceção para erros HTTP 4xx/5xx
794
+ logger.info(f"BG TASK [{rnn_tx_id}]: Callback enviado com sucesso para AIBank. Status: {response.status_code}")
795
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_success_{response.status_code}"
796
+ except httpx.RequestError as e:
797
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao enviar callback para AIBank (RequestError): {str(e)}")
798
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_request_error"
799
+ except httpx.HTTPStatusError as e:
800
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro HTTP ao enviar callback para AIBank (HTTPStatusError): {e.response.status_code} - {e.response.text}")
801
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_http_error_{e.response.status_code}"
802
+ except Exception as e:
803
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro inesperado ao enviar callback: {str(e)}", exc_info=True)
804
+ transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_unknown_error"
805
+
806
+ """
807
+ # --- Endpoints da API ---
808
+ @app.post("/api/invest",
809
+ response_model=InvestmentResponse,
810
+ dependencies=[Depends(verify_aibank_key)])
811
+ async def initiate_investment(
812
+ request_data: InvestmentRequest,
813
+ background_tasks: BackgroundTasks
814
+ ):
815
+ """
816
+ Endpoint para o AIBank iniciar um ciclo de investimento.
817
+ Responde rapidamente e executa a lógica pesada em background.
818
+ """
819
+ logger.info(f"Requisição de investimento recebida para client_id: {request_data.client_id}, "
820
+ f"amount: {request_data.amount}, aibank_tx_token: {request_data.aibank_transaction_token}")
821
+
822
+ rnn_tx_id = str(uuid.uuid4())
823
+
824
+ # Armazena informações iniciais da transação DB real para ser mais robusto
825
+ transactions_db[rnn_tx_id] = {
826
+ "rnn_transaction_id": rnn_tx_id,
827
+ "aibank_transaction_token": request_data.aibank_transaction_token,
828
+ "client_id": request_data.client_id,
829
+ "initial_amount": request_data.amount,
830
+ "status": "pending_background_processing",
831
+ "received_at": datetime.utcnow().isoformat(),
832
+ "callback_status": "not_sent_yet"
833
+ }
834
+
835
+ # Adiciona a tarefa de longa duração ao background
836
+ background_tasks.add_task(
837
+ execute_investment_strategy_background,
838
+ rnn_tx_id,
839
+ request_data.client_id,
840
+ request_data.amount,
841
+ request_data.aibank_transaction_token
842
+ )
843
+
844
+ logger.info(f"Estratégia de investimento para rnn_tx_id: {rnn_tx_id} agendada para execução em background.")
845
+ return InvestmentResponse(
846
+ status="pending",
847
+ message="Investment request received and is being processed in the background. Await callback for results.",
848
+ rnn_transaction_id=rnn_tx_id
849
+ )
850
+
851
+ @app.get("/api/transaction_status/{rnn_tx_id}", response_class=JSONResponse)
852
+ async def get_transaction_status(rnn_tx_id: str):
853
+ """ Endpoint para verificar o status de uma transação (para debug/admin) """
854
+ transaction = transactions_db.get(rnn_tx_id)
855
+ if not transaction:
856
+ raise HTTPException(status_code=404, detail="Transaction not found")
857
+ return transaction
858
+
859
+
860
+ # --- Dashboard (Existente, adaptado) ---
861
+ # Setup para arquivos estáticos e templates
862
+
863
+ try:
864
+ app.mount("/static", StaticFiles(directory="rnn/static"), name="static")
865
+ templates = Environment(loader=FileSystemLoader("rnn/templates"))
866
+ except RuntimeError as e:
867
+ logger.warning(f"Não foi possível montar /static ou carregar templates: {e}. O dashboard pode não funcionar.")
868
+ templates = None # Para evitar erros se o loader falhar
869
+
870
+ @app.get("/", response_class=HTMLResponse)
871
+ async def index(request: Request):
872
+ if not templates:
873
+ return HTMLResponse("<html><body><h1>Dashboard indisponível</h1><p>Configuração de templates/estáticos falhou.</p></body></html>")
874
+
875
+ agora = datetime.now()
876
+ agentes_simulados = [
877
+ # dados de agentes ...
878
+ ]
879
+ template = templates.get_template("index.html")
880
+ # Adicionar transações recentes ao contexto do template
881
+ recent_txs = list(transactions_db.values())[-5:] # Últimas 5 transações
882
+ return HTMLResponse(template.render(request=request, agentes=agentes_simulados, transactions=recent_txs))
883
+
884
+ # --- Imports para Background Task ---
885
+ import asyncio
886
+ import random
887
+
888
+ # Função de logger dummy s
889
+ # class DummyLogger:
890
+ # def info(self, msg, *args, **kwargs): print(f"INFO: {msg}")
891
+ # def warning(self, msg, *args, **kwargs): print(f"WARNING: {msg}")
892
+ # def error(self, msg, *args, **kwargs): print(f"ERROR: {msg}", kwargs.get('exc_info'))
893
+
894
+ # if __name__ == "__main__": # Para teste local
895
+ # # logger = DummyLogger() # se não tiver get_logger()
896
+ # # Configuração das variáveis de ambiente para teste local
897
+ # os.environ["AIBANK_API_KEY"] = "test_aibank_key_from_rnn_server"
898
+ # os.environ["AIBANK_CALLBACK_URL"] = "http://localhost:8001/api/rnn_investment_result_callback" # URL do aibank simulado
899
+ # os.environ["CALLBACK_SHARED_SECRET"] = "super_secret_for_callback_signing"
900
+ # # import uvicorn
901
+ # # uvicorn.run(app, host="0.0.0.0", port=8000)
app-old-v1.py ADDED
@@ -0,0 +1,901 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ import os
4
+ import uuid
5
+ import time
6
+ import hmac
7
+ import hashlib
8
+ import json
9
+ from datetime import datetime, timedelta
10
+ from typing import Dict, Any
11
+ import ccxt.async_support as ccxt
12
+
13
+ import httpx # Para fazer chamadas HTTP assíncronas (para o callback)
14
+ from fastapi import FastAPI, Request, HTTPException, Depends, Header, BackgroundTasks
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ from fastapi.responses import HTMLResponse, JSONResponse
17
+ from fastapi.staticfiles import StaticFiles
18
+ from jinja2 import Environment, FileSystemLoader
19
+ from pydantic import BaseModel, Field
20
+
21
+ from app. utils.logger import get_logger
22
+
23
+ logger = get_logger()
24
+
25
+ # --- Configuração Inicial e Variáveis de Ambiente (Secrets do Hugging Face) ---
26
+ AIBANK_API_KEY = os.environ.get("AIBANK_API_KEY") # Chave que o aibank usa para chamar esta API RNN
27
+ AIBANK_CALLBACK_URL = os.environ.get("AIBANK_CALLBACK_URL") # URL no aibank para onde esta API RNN enviará o resultado
28
+ CALLBACK_SHARED_SECRET = os.environ.get("CALLBACK_SHARED_SECRET") # Segredo para assinar/verificar o payload do callback
29
+
30
+ # Chaves para serviços externos
31
+ MARKET_DATA_API_KEY = os.environ.get("MARKET_DATA_API_KEY")
32
+ EXCHANGE_API_KEY = os.environ.get("EXCHANGE_API_KEY")
33
+ EXCHANGE_API_SECRET = os.environ.get("EXCHANGE_API_SECRET")
34
+
35
+ if not AIBANK_API_KEY:
36
+ logger.warning("AIBANK_API_KEY não configurada. A autenticação para /api/invest falhou.")
37
+ if not AIBANK_CALLBACK_URL:
38
+ logger.warning("AIBANK_CALLBACK_URL não configurada. O callback para o aibank falhou.")
39
+ if not CALLBACK_SHARED_SECRET:
40
+ logger.warning("CALLBACK_SHARED_SECRET não configurado. A segurança do callback está comprometida.")
41
+
42
+
43
+
44
+ #CHAVES PARA CCXT (para Binance)
45
+ # configuradas como vriaveis no Hugging Face Space
46
+
47
+ CCXT_EXCHANGE_ID = os.environ.get("CCXT_EXCHANGE_ID", "binance")
48
+ CCXT_API_KEY = os.environ.get("CCXT_API_KEY")
49
+ CCXT_API_SECRET = os.environ.get("CCXT_API_SECRET")
50
+ CCXT_API_PASSWORD = os.environ.get("CCXT_API_PASSWORD") # Opcional
51
+
52
+ # Binance Testnet
53
+ CCXT_SANDBOX_MODE = os.environ.get("CCXT_SANDBOX_MODE", "false").lower() == "true"
54
+
55
+
56
+
57
+
58
+
59
+ app = FastAPI(title="ATCoin Neural Agents - Investment API")
60
+
61
+ # --- Middlewares ---
62
+ app.add_middleware(
63
+ CORSMiddleware,
64
+ allow_origins=[
65
+ "http://localhost:3000", # URL desenvolvimento local
66
+ "http://aibank.app.br", # URL de produção
67
+ "https://*.aibank.app.br", # subdomínios
68
+ "https://*.hf.space" # HF Space
69
+ ],
70
+ allow_credentials=True,
71
+ allow_methods=["*"],
72
+ allow_headers=["*"],
73
+ )
74
+
75
+ # --- Simulação de Banco de Dados de Transações DEV ---
76
+ # Em produção MongoDB
77
+ transactions_db: Dict[str, Dict[str, Any]] = {}
78
+
79
+ # --- Modelos Pydantic ---
80
+ class InvestmentRequest(BaseModel):
81
+ client_id: str
82
+ amount: float = Field(..., gt=0) # Garante que o montante seja positivo
83
+ aibank_transaction_token: str # Token único gerado pelo aibank para rastreamento
84
+
85
+ class InvestmentResponse(BaseModel):
86
+ status: str
87
+ message: str
88
+ rnn_transaction_id: str # ID da transação this.API
89
+
90
+ class InvestmentResultPayload(BaseModel): # Payload para o callback para o aibank
91
+ rnn_transaction_id: str
92
+ aibank_transaction_token: str
93
+ client_id: str
94
+ initial_amount: float
95
+ final_amount: float
96
+ profit_loss: float
97
+ status: str # "completed", "failed"
98
+ timestamp: datetime
99
+ details: str = ""
100
+
101
+
102
+ # --- Dependência de Autenticação ---
103
+ async def verify_aibank_key(authorization: str = Header(None)):
104
+ if not AIBANK_API_KEY: # Checagem se a chave do servidor está configurada
105
+ logger.error("CRITICAL: AIBANK_API_KEY (server-side) não está configurada nos Secrets.")
106
+ raise HTTPException(status_code=500, detail="Internal Server Configuration Error: Missing server API Key.")
107
+
108
+ if authorization is None:
109
+ logger.warning("Authorization header ausente na chamada do AIBank.")
110
+ raise HTTPException(status_code=401, detail="Authorization header is missing")
111
+
112
+ parts = authorization.split()
113
+ if len(parts) != 2 or parts[0].lower() != 'bearer':
114
+ logger.warning(f"Formato inválido do Authorization header: {authorization}")
115
+ raise HTTPException(status_code=401, detail="Authorization header must be 'Bearer <token>'")
116
+
117
+ token_from_aibank = parts[1]
118
+ if not hmac.compare_digest(token_from_aibank, AIBANK_API_KEY):
119
+ logger.warning(f"Chave de API inválida fornecida pelo AIBank. Token: {token_from_aibank[:10]}...")
120
+ raise HTTPException(status_code=403, detail="Invalid API Key provided by AIBank.")
121
+ logger.info("API Key do AIBank verificada com sucesso.")
122
+ return True
123
+
124
+
125
+ # --- Lógica de Negócio Principal (Simulada e em Background) ---
126
+
127
+ async def execute_investment_strategy_background(
128
+ rnn_tx_id: str,
129
+ client_id: str,
130
+ amount: float,
131
+ aibank_tx_token: str
132
+ ):
133
+ """
134
+ Esta função roda em background. Simula a coleta de dados, RNN, execução e tokenização.
135
+ No final, chama o callback para o aibank.
136
+ """
137
+ logger.info(f"BG TASK [{rnn_tx_id}]: Iniciando estratégia de investimento para cliente {client_id}, valor {amount}.")
138
+ transactions_db[rnn_tx_id]["status"] = "processing" # Status geral inicial
139
+ transactions_db[rnn_tx_id]["status_details"] = "Initializing investment cycle"
140
+
141
+ final_status = "completed" # Status final presumido
142
+ error_details = ""
143
+ calculated_final_amount = amount # Valor inicial, será modificado
144
+
145
+ # Inicializa o objeto da exchange ccxt
146
+ exchange = None
147
+ if CCXT_API_KEY and CCXT_API_SECRET: #nicializa se as chaves básicas estiverem presentes
148
+ try:
149
+ exchange_class = getattr(ccxt, CCXT_EXCHANGE_ID)
150
+ config = {
151
+ 'apiKey': CCXT_API_KEY,
152
+ 'secret': CCXT_API_SECRET,
153
+ 'enableRateLimit': True, # evitar bans da API
154
+ # 'verbose': True, # Para debug detalhado das chamadas ccxt
155
+ }
156
+ if CCXT_API_PASSWORD:
157
+ config['password'] = CCXT_API_PASSWORD
158
+
159
+ exchange = exchange_class(config)
160
+
161
+ if CCXT_SANDBOX_MODE:
162
+ if hasattr(exchange, 'set_sandbox_mode'): # Nem todas as exchanges suportam isso diretamente em ccxt
163
+ exchange.set_sandbox_mode(True)
164
+ logger.info(f"BG TASK [{rnn_tx_id}]: CCXT configurado para modo SANDBOX para {CCXT_EXCHANGE_ID}.")
165
+ elif 'test' in exchange.urls: # usar testnet another.way
166
+ exchange.urls['api'] = exchange.urls['test']
167
+ logger.info(f"BG TASK [{rnn_tx_id}]: CCXT URLs alteradas para TESTNET para {CCXT_EXCHANGE_ID}.")
168
+ else:
169
+ 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.")
170
+
171
+ # Carregar mercados para validar símbolos, demorado.
172
+ # await exchange.load_markets()
173
+ # logger.info(f"BG TASK [{rnn_tx_id}]: Mercados carregados para {CCXT_EXCHANGE_ID}.")
174
+
175
+ except AttributeError:
176
+ logger.error(f"BG TASK [{rnn_tx_id}]: Exchange ID '{CCXT_EXCHANGE_ID}' inválida ou não suportada pelo ccxt.")
177
+ error_details += f"Invalid CCXT_EXCHANGE_ID: {CCXT_EXCHANGE_ID}; "
178
+ final_status = "failed_config" # Um novo status para falha de configuração
179
+ # (Pular para o callback aqui)
180
+ except Exception as e:
181
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao inicializar ccxt para {CCXT_EXCHANGE_ID}: {str(e)}", exc_info=True)
182
+ error_details += f"CCXT initialization error: {str(e)}; "
183
+ final_status = "failed_config"
184
+ # (Pular para o callback aqui)
185
+ else:
186
+ 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.")
187
+ #Decidir se é uma falha fatal ou se a estratégia pode prosseguir sem dados de cripto.
188
+ # Precisa lidar com dados ausentes.
189
+
190
+
191
+ # =========================================================================
192
+ # 1. COLETAR DADOS DE MERCADO (com ccxt integrado)
193
+ # =========================================================================
194
+ logger.info(f"BG TASK [{rnn_tx_id}]: Coletando dados de mercado...")
195
+ transactions_db[rnn_tx_id]["status_details"] = "Fetching market data"
196
+ market_data_results = {
197
+ "crypto": {}, # Dados específicos de cripto
198
+ "stocks": {}, # Dados de ações (integrar yfinance ou outro)
199
+ "other": {} # Outros dados
200
+ }
201
+ market_data_fetch_success = True
202
+
203
+ # --- Defina os pares de cripto ---
204
+ # Estes poderiam vir de uma configuração, ou serem dinâmicos.
205
+ crypto_pairs_to_fetch = ["BTC/USDT", "ETH/USDT", "SOL/USDT"]
206
+
207
+ if exchange: # Buscar se a exchange foi inicializada corretamente
208
+ try:
209
+ for pair in crypto_pairs_to_fetch:
210
+ pair_data = {}
211
+ if exchange.has['fetchTicker']:
212
+ ticker = await exchange.fetch_ticker(pair)
213
+ pair_data['ticker'] = {
214
+ 'last': ticker.get('last'),
215
+ 'bid': ticker.get('bid'),
216
+ 'ask': ticker.get('ask'),
217
+ 'volume': ticker.get('baseVolume'), # Ou 'quoteVolume'
218
+ 'timestamp': ticker.get('timestamp')
219
+ }
220
+ logger.info(f"BG TASK [{rnn_tx_id}]: Ticker {pair}: Preço {ticker.get('last')}")
221
+
222
+ if exchange.has['fetchOHLCV']:
223
+ # timeframe: '1m', '5m', '15m', '1h', '4h', '1d', '1w', '1M'
224
+ # limit: número de candles
225
+ ohlcv = await exchange.fetch_ohlcv(pair, timeframe='1h', limit=72) # Últimas 72 horas
226
+ # Formato OHLCV: [timestamp, open, high, low, close, volume]
227
+ pair_data['ohlcv_1h'] = ohlcv
228
+ logger.info(f"BG TASK [{rnn_tx_id}]: Coletado {len(ohlcv)} candles OHLCV para {pair} (1h).")
229
+
230
+ # Outros dados que ccxt:
231
+ # if exchange.has['fetchOrderBook']:
232
+ # order_book = await exchange.fetch_order_book(pair, limit=5) # Top 5 bids/asks
233
+ # pair_data['order_book_L1'] = { # Exemplo de Level 1
234
+ # 'bids': order_book['bids'][0] if order_book['bids'] else None,
235
+ # 'asks': order_book['asks'][0] if order_book['asks'] else None,
236
+ # }
237
+ # if exchange.has['fetchTrades']: # Últimas negociações públicas
238
+ # trades = await exchange.fetch_trades(pair, limit=10)
239
+ # pair_data['recent_trades'] = trades
240
+
241
+ market_data_results["crypto"][pair.replace("/", "_")] = pair_data # Usar _ para chaves de dict seguras
242
+
243
+ logger.info(f"BG TASK [{rnn_tx_id}]: Dados de cripto via ccxt coletados.")
244
+
245
+ except ccxt.NetworkError as e:
246
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro de rede ccxt ao coletar dados de mercado: {str(e)}", exc_info=True)
247
+ market_data_fetch_success = False
248
+ error_details += f"CCXT NetworkError: {str(e)}; "
249
+ except ccxt.ExchangeError as e:
250
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro da exchange ccxt ao coletar dados de mercado: {str(e)}", exc_info=True)
251
+ market_data_fetch_success = False
252
+ error_details += f"CCXT ExchangeError: {str(e)}; "
253
+ except Exception as e:
254
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro geral ao coletar dados de cripto via ccxt: {str(e)}", exc_info=True)
255
+ market_data_fetch_success = False
256
+ error_details += f"General CCXT data collection error: {str(e)}; "
257
+ else:
258
+ logger.info(f"BG TASK [{rnn_tx_id}]: Instância da exchange ccxt não disponível. Pulando coleta de dados de cripto.")
259
+ # Se não há 'exchange', a coleta de dados de cripto não pode ocorrerá.
260
+ # Erro, dependendo se as chaves Not.ok.
261
+ if CCXT_API_KEY and CCXT_API_SECRET: # Chaves dadas mas a 'exchange' falhou na init
262
+ market_data_fetch_success = False # Falha se a config estava lá
263
+ error_details += "CCXT exchange object not initialized despite API keys being present; "
264
+
265
+
266
+ # --- Coleta de dados para outros tipos de ativos (Ações com yfinance) ---
267
+ # try:
268
+ # import yfinance as yf
269
+ # aapl = yf.Ticker("AAPL")
270
+ # hist_aapl = aapl.history(period="5d", interval="1h") # Últimos 5 dias, candles de 1h
271
+ # if not hist_aapl.empty:
272
+ # market_data_results["stocks"]["AAPL"] = {
273
+ # "ohlcv_1h": [[ts.timestamp() * 1000] + list(row) for ts, row in hist_aapl[['Open', 'High', 'Low', 'Close', 'Volume']].iterrows()]
274
+ # }
275
+ # logger.info(f"BG TASK [{rnn_tx_id}]: Coletado {len(hist_aapl)} candles para AAPL via yfinance.")
276
+ # except Exception as e:
277
+ # logger.warning(f"BG TASK [{rnn_tx_id}]: Falha ao buscar dados de ações com yfinance: {e}")
278
+ #
279
+
280
+
281
+ # --- Simulação para outros dados, se necessário ---
282
+ market_data_results["other"]['simulated_index_level'] = random.uniform(10000, 15000)
283
+ market_data_results["other"]['simulated_crypto_sentiment'] = random.uniform(-1, 1)
284
+
285
+ 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
286
+ final_status = "failed_market_data"
287
+ logger.error(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado falhou criticamente. {error_details}")
288
+ # (Pular para o callback aqui, pois a RNN pode não ter dados suficientes)
289
+ # 'return' ou lógica de pular precisa ser implementada se a falha for fatal
290
+ # Registrar e permitir que a lógica da RNN tente lidar com dados ausentes/parciais
291
+ pass
292
+
293
+ transactions_db[rnn_tx_id]["market_data_collected"] = market_data_results # Armazena para auditoria/debug
294
+ transactions_db[rnn_tx_id]["status_details"] = "Processing RNN analysis"
295
+ logger.info(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado concluída (sucesso: {market_data_fetch_success}).")
296
+
297
+ # =========================================================================
298
+ # 2. ANÁLISE PELA RNN E TOMADA DE DECISÃO
299
+ # =========================================================================
300
+ # (alimentada por market_data_results)
301
+ # ...
302
+ logger.info(f"BG TASK [{rnn_tx_id}]: Executando análise RNN com os dados coletados...")
303
+ transactions_db[rnn_tx_id]["status_details"] = "Running RNN model"
304
+ investment_decisions = []
305
+ rnn_analysis_success = True
306
+
307
+ try:
308
+
309
+ # A RNN precisará ser capaz de lidar com a estrutura de market_data_results,
310
+ # incluindo a possibilidade de alguns dados estarem ausentes se a coleta falhar.
311
+
312
+ # investment_decisions = await rnn_model.predict_async(market_data_results, amount_to_invest=amount)
313
+
314
+ # Simulação para prosseguir:
315
+ await asyncio.sleep(random.uniform(8, 15))
316
+ if market_data_results["crypto"]: # Com dados de cripto
317
+ if random.random() > 0.1:
318
+ num_crypto_assets_to_invest = random.randint(1, len(crypto_pairs_to_fetch))
319
+ chosen_pairs = random.sample(list(market_data_results["crypto"].keys()), k=num_crypto_assets_to_invest)
320
+
321
+ for crypto_key in chosen_pairs: # crypto_key "BTC_USDT"
322
+ asset_symbol = crypto_key.replace("_", "/") # Converte de volta para "BTC/USDT"
323
+ allocated_amount = (amount / num_crypto_assets_to_invest) * random.uniform(0.7, 0.9) # Aloca uma parte do total
324
+ investment_decisions.append({
325
+ "asset_id": asset_symbol, # Usa o símbolo da exchange
326
+ "type": "CRYPTO",
327
+ "action": "BUY",
328
+ "target_usd_amount": round(allocated_amount, 2),
329
+ "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')}"
330
+ })
331
+ else:
332
+ logger.info(f"BG TASK [{rnn_tx_id}]: Sem dados de cripto para a RNN processar.")
333
+
334
+ if not investment_decisions:
335
+ logger.info(f"BG TASK [{rnn_tx_id}]: RNN não gerou nenhuma decisão de investimento.")
336
+ else:
337
+ logger.info(f"BG TASK [{rnn_tx_id}]: RNN gerou {len(investment_decisions)} decisões: {investment_decisions}")
338
+
339
+ except Exception as e:
340
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante análise RNN: {str(e)}", exc_info=True)
341
+ rnn_analysis_success = False
342
+ error_details += f"RNN analysis failed: {str(e)}; "
343
+
344
+ if not rnn_analysis_success:
345
+ final_status = "failed_rnn_analysis"
346
+ # (Pular para o callback)
347
+ pass
348
+
349
+ transactions_db[rnn_tx_id]["rnn_decisions"] = investment_decisions
350
+ transactions_db[rnn_tx_id]["status_details"] = "Preparing to execute orders"
351
+ total_usd_allocated_by_rnn = sum(d['target_usd_amount'] for d in investment_decisions if d['action'] == 'BUY')
352
+
353
+
354
+ # =========================================================================
355
+ # 3. EXECUÇÃO DE ORDENS (Será detalhado no próximo passo)
356
+ # =========================================================================
357
+ # ... (execução de ordens aqui, usando `exchange` ccxt e `investment_decisions`)
358
+ # ... (Incluindo cálculo de `current_portfolio_value` e `cash_remaining_after_allocation`)
359
+ logger.info(f"BG TASK [{rnn_tx_id}]: Executando ordens baseadas nas decisões da RNN...")
360
+ transactions_db[rnn_tx_id]["status_details"] = "Executing investment orders"
361
+ executed_trades_info = []
362
+ order_execution_success = True
363
+ cash_remaining_after_allocation = amount - total_usd_allocated_by_rnn
364
+ current_portfolio_value = 0 # Valor dos ativos comprados
365
+
366
+ # Placeholder para execução
367
+ if investment_decisions:
368
+ for decision in investment_decisions:
369
+ if decision.get("action") == "BUY":
370
+ usd_amount_to_spend = decision.get("target_usd_amount")
371
+ simulated_cost = usd_amount_to_spend * random.uniform(0.99, 1.0)
372
+ executed_trades_info.append({
373
+ "asset": decision.get("asset_id"), "order_id": f"sim_ord_{uuid.uuid4()}",
374
+ "status": "filled", "cost_usd": simulated_cost
375
+ })
376
+ current_portfolio_value += simulated_cost
377
+ await asyncio.sleep(random.uniform(1,3) * len(investment_decisions)) # Simula tempo de execução
378
+ else:
379
+ cash_remaining_after_allocation = amount
380
+
381
+ transactions_db[rnn_tx_id]["executed_trades"] = executed_trades_info
382
+ transactions_db[rnn_tx_id]["status_details"] = "Simulating holding period and profit/loss"
383
+
384
+
385
+ # =========================================================================
386
+ # 4. SIMULAÇÃO DO PERÍODO DE INVESTIMENTO E CÁLCULO DE LUCRO/PERDA
387
+ # =========================================================================
388
+ # ... (Inserir lógia )
389
+ logger.info(f"BG TASK [{rnn_tx_id}]: Simulando período de investimento e fechamento de posições...")
390
+ await asyncio.sleep(random.uniform(5, 10))
391
+
392
+ if current_portfolio_value > 0:
393
+ profit_on_invested_part = current_portfolio_value * (0.042 * random.uniform(0.8, 1.2))
394
+ value_of_investments_at_eod = current_portfolio_value + profit_on_invested_part
395
+ else:
396
+ value_of_investments_at_eod = 0
397
+ profit_on_invested_part = 0
398
+
399
+ calculated_final_amount = value_of_investments_at_eod + cash_remaining_after_allocation
400
+
401
+ logger.info(f"BG TASK [{rnn_tx_id}]: Valor inicial total: {amount:.2f}. "
402
+ f"Valor alocado para investimento: {total_usd_allocated_by_rnn:.2f}. "
403
+ f"Valor dos investimentos no EOD: {value_of_investments_at_eod:.2f}. "
404
+ f"Caixa não alocado: {cash_remaining_after_allocation:.2f}. "
405
+ f"Valor final total: {calculated_final_amount:.2f}")
406
+
407
+ transactions_db[rnn_tx_id]["eod_portfolio_value_simulated"] = value_of_investments_at_eod
408
+
409
+ # =========================================================================
410
+ # 5. TOKENIZAÇÃO / REGISTRO DA OPERAÇÃO
411
+ # =========================================================================
412
+ # ... (Inserir)
413
+ logger.info(f"BG TASK [{rnn_tx_id}]: Registrando (tokenizando) operação detalhadamente...")
414
+ # ... (código do hash proof)
415
+ await asyncio.sleep(1)
416
+
417
+ # =========================================================================
418
+ # 6. PREPARAR E ENVIAR CALLBACK PARA AIBANK
419
+ # =========================================================================
420
+ # (Fechamento da conexão ccxt antes do callback, se a conexão foi aberta e bem sucedida)
421
+ if exchange and hasattr(exchange, 'close'):
422
+ try:
423
+ await exchange.close()
424
+ logger.info(f"BG TASK [{rnn_tx_id}]: Conexão ccxt com {CCXT_EXCHANGE_ID} fechada.")
425
+ except Exception as e:
426
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Erro ao fechar conexão ccxt: {str(e)}")
427
+
428
+ # (callback , usando `final_status`, `error_details`, `calculated_final_amount`)
429
+ # ...
430
+ if not AIBANK_CALLBACK_URL or not CALLBACK_SHARED_SECRET:
431
+ logger.error(f"BG TASK [{rnn_tx_id}]: AIBANK_CALLBACK_URL ou CALLBACK_SHARED_SECRET não configurado. Não é possível enviar callback.")
432
+ transactions_db[rnn_tx_id]["callback_status"] = "config_missing"
433
+ return # Não pode prosseguir sem config de callback
434
+
435
+ callback_payload_data = InvestmentResultPayload(
436
+ rnn_transaction_id=rnn_tx_id,
437
+ aibank_transaction_token=aibank_tx_token,
438
+ client_id=client_id,
439
+ initial_amount=amount,
440
+ final_amount=calculated_final_amount,
441
+ profit_loss=calculated_final_amount - amount,
442
+ status=final_status, # O status final da operação
443
+ timestamp=datetime.utcnow(),
444
+ details=error_details if final_status != "completed" else "Investment cycle completed successfully."
445
+ )
446
+ payload_json = callback_payload_data.model_dump_json()
447
+ signature = hmac.new(CALLBACK_SHARED_SECRET.encode('utf-8'), payload_json.encode('utf-8'), hashlib.sha256).hexdigest()
448
+ headers = {'Content-Type': 'application/json', 'X-RNN-Signature': signature}
449
+
450
+ logger.info(f"BG TASK [{rnn_tx_id}]: Enviando callback para AIBank: {AIBANK_CALLBACK_URL} com status final '{final_status}'")
451
+ transactions_db[rnn_tx_id]["callback_status"] = "sending"
452
+ try:
453
+ async with httpx.AsyncClient() as client:
454
+ response = await client.post(AIBANK_CALLBACK_URL, content=payload_json, headers=headers, timeout=30.0)
455
+ response.raise_for_status()
456
+ logger.info(f"BG TASK [{rnn_tx_id}]: Callback enviado com sucesso para AIBank. Status da resposta: {response.status_code}")
457
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_success_{response.status_code}"
458
+ except Exception as e: # Captura mais genérica para erros de callback
459
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao enviar callback para AIBank: {str(e)}", exc_info=True)
460
+ # Classificar o erro para o log
461
+ if isinstance(e, httpx.RequestError):
462
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_request_error"
463
+ elif isinstance(e, httpx.HTTPStatusError):
464
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_http_error_{e.response.status_code}"
465
+ else:
466
+ transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_unknown_error"
467
+
468
+
469
+
470
+ # ---
471
+ """ async def execute_investment_strategy_background(
472
+ rnn_tx_id: str,
473
+ client_id: str,
474
+ amount: float,
475
+ aibank_tx_token: str
476
+ ):
477
+
478
+ logger.info(f"BG TASK [{rnn_tx_id}]: Iniciando estratégia de investimento para cliente {client_id}, valor {amount}.")
479
+ transactions_db[rnn_tx_id]["status"] = "processing_market_data"
480
+
481
+ final_status = "completed"
482
+ error_details = ""
483
+ calculated_final_amount = amount # Valor inicial
484
+
485
+ try:
486
+ # 1. COLETAR DADOS DE MERCADO (Placeholder)
487
+
488
+ logger.info(f"BG TASK [{rnn_tx_id}]: Coletando dados de mercado...")
489
+ transactions_db[rnn_tx_id]["status_details"] = "Fetching market data"
490
+ market_data_results = {}
491
+ market_data_fetch_success = True
492
+
493
+ # Exemplo para Cripto com ccxt (requer 'pip install ccxt')
494
+ # import ccxt.async_support as ccxt # Coloque no topo do app.py
495
+ # exchange_id = 'binance' # Exemplo
496
+ # exchange_class = getattr(ccxt, exchange_id)
497
+ # exchange = exchange_class({
498
+ # 'apiKey': EXCHANGE_API_KEY, # Do os.environ
499
+ # 'secret': EXCHANGE_API_SECRET, # Do os.environ
500
+ # 'enableRateLimit': True, # Importante
501
+ # })
502
+
503
+ try:
504
+ # --- Exemplo para buscar dados de BTC/USDT ---
505
+ # if exchange.has['fetchTicker']:
506
+ # ticker_btc = await exchange.fetch_ticker('BTC/USDT')
507
+ # market_data_results['BTC_USDT_ticker'] = ticker_btc
508
+ # logger.info(f"BG TASK [{rnn_tx_id}]: Ticker BTC/USDT: {ticker_btc['last']}")
509
+ # if exchange.has['fetchOHLCV']:
510
+ # ohlcv_btc = await exchange.fetch_ohlcv('BTC/USDT', timeframe='1h', limit=100) # Últimas 100 horas
511
+ # market_data_results['BTC_USDT_ohlcv_1h'] = ohlcv_btc
512
+ # logger.info(f"BG TASK [{rnn_tx_id}]: Coletado {len(ohlcv_btc)} candles OHLCV para BTC/USDT 1h.")
513
+
514
+ # --- Exemplo para Ações com yfinance (requer 'pip install yfinance') ---
515
+ # import yfinance as yf # Coloque no topo do app.py
516
+ # aapl = yf.Ticker("AAPL")
517
+ # hist_aapl = aapl.history(period="1mo") # Dados do último mês
518
+ # market_data_results['AAPL_history_1mo'] = hist_aapl.to_dict() # Pode ser grande, serialize com cuidado
519
+ # current_price_aapl = hist_aapl['Close'].iloc[-1] if not hist_aapl.empty else None
520
+ # market_data_results['AAPL_current_price'] = current_price_aapl
521
+ # logger.info(f"BG TASK [{rnn_tx_id}]: Preço atual AAPL (yfinance): {current_price_aapl}")
522
+
523
+ # --- Placeholder para sua lógica real de coleta ---
524
+ # Você precisará definir QUAIS ativos e QUAIS dados são necessários para sua RNN.
525
+ # Este é um ponto crucial para sua estratégia.
526
+ # Simulação para prosseguir:
527
+ await asyncio.sleep(random.uniform(3, 7)) # Simula demora da coleta real
528
+ market_data_results['simulated_index_level'] = random.uniform(10000, 15000)
529
+ market_data_results['simulated_crypto_sentiment'] = random.uniform(-1, 1)
530
+ logger.info(f"BG TASK [{rnn_tx_id}]: Dados de mercado (simulados/reais) coletados.")
531
+
532
+ except Exception as e:
533
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao coletar dados de mercado: {str(e)}", exc_info=True)
534
+ market_data_fetch_success = False
535
+ error_details += f"Market data collection failed: {str(e)}; "
536
+ # Decida se a falha aqui é crítica e impede de continuar.
537
+ # Se sim, atualize final_status e pule para o callback.
538
+
539
+ # finally: # Importante para fechar conexões de exchange em ccxt
540
+ # if 'exchange' in locals() and hasattr(exchange, 'close'):
541
+ # await exchange.close()
542
+
543
+ if not market_data_fetch_success:
544
+ # Lógica para lidar com falha na coleta de dados (ex: não prosseguir)
545
+ final_status = "failed_market_data"
546
+ # ... (atualize transactions_db e pule para a seção de callback) ...
547
+ # (Este 'return' ou lógica de pular precisa ser implementada se a falha for fatal)
548
+ pass # Por ora, deixamos prosseguir com dados possivelmente incompletos ou apenas simulados
549
+
550
+ transactions_db[rnn_tx_id]["market_data_collected"] = market_data_results # Armazene para auditoria/debug
551
+ transactions_db[rnn_tx_id]["status_details"] = "Processing RNN analysis"
552
+
553
+
554
+ # 2. ANÁLISE PELA RNN E TOMADA DE DECISÃO (Placeholder)
555
+ l
556
+
557
+ logger.info(f"BG TASK [{rnn_tx_id}]: Executando análise RNN com os dados coletados...")
558
+ transactions_db[rnn_tx_id]["status_details"] = "Running RNN model"
559
+ investment_decisions = [] # Lista de decisões: {'asset': 'BTC/USDT', 'action': 'BUY', 'amount_usd': 5000, 'price_target': 70000}
560
+ rnn_analysis_success = True
561
+
562
+ try:
563
+ # --- SUBSTITUA PELA CHAMADA AO SEU MODELO RNN ---
564
+ # Exemplo: Supondo que você tenha uma classe ou função rnn_predictor
565
+ # from rnn.models.predictor import rnn_predictor # Exemplo de import
566
+
567
+ # Supondo que seu predictor precise dos dados de mercado e do montante a investir
568
+ # investment_decisions = await rnn_predictor.generate_signals_async(
569
+ # market_data_results,
570
+ # amount_to_invest=amount # O montante total disponível para este ciclo
571
+ # )
572
+
573
+ # Simulação para prosseguir:
574
+ await asyncio.sleep(random.uniform(8, 15)) # Simula processamento da RNN
575
+ if random.random() > 0.1: # 90% de chance de "decidir" investir
576
+ num_assets_to_invest = random.randint(1, 3)
577
+ for i in range(num_assets_to_invest):
578
+ asset_name = random.choice(["SIMULATED_CRYPTO_X", "SIMULATED_STOCK_Y", "SIMULATED_BOND_Z"])
579
+ action = random.choice(["BUY", "HOLD"]) # Simplificado, sem SELL por enquanto
580
+ if action == "BUY":
581
+ # Alocar uma porção do 'amount' total para este ativo
582
+ allocated_amount = (amount / num_assets_to_invest) * random.uniform(0.8, 1.0)
583
+ investment_decisions.append({
584
+ "asset_id": f"{asset_name}_{i}",
585
+ "type": "CRYPTO" if "CRYPTO" in asset_name else "STOCK", # Exemplo
586
+ "action": action,
587
+ "target_usd_amount": round(allocated_amount, 2),
588
+ "reasoning": "RNN signal strong based on simulated data" # Adicione o output real da RNN
589
+ })
590
+
591
+ if not investment_decisions:
592
+ logger.info(f"BG TASK [{rnn_tx_id}]: RNN não gerou nenhuma decisão de investimento (ou decidiu não investir).")
593
+ # Isso pode ser um resultado válido.
594
+ else:
595
+ logger.info(f"BG TASK [{rnn_tx_id}]: RNN gerou {len(investment_decisions)} decisões: {investment_decisions}")
596
+
597
+ except Exception as e:
598
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante análise RNN: {str(e)}", exc_info=True)
599
+ rnn_analysis_success = False
600
+ error_details += f"RNN analysis failed: {str(e)}; "
601
+
602
+ if not rnn_analysis_success:
603
+ final_status = "failed_rnn_analysis"
604
+ # ... (atualize transactions_db e pule para a seção de callback) ...
605
+ pass
606
+
607
+ transactions_db[rnn_tx_id]["rnn_decisions"] = investment_decisions
608
+ transactions_db[rnn_tx_id]["status_details"] = "Preparing to execute orders"
609
+
610
+ # Antes de executar ordens, vamos calcular o valor que REALMENTE foi investido
611
+ # e o que pode ter sobrado, já que a RNN pode não usar todo o 'amount'.
612
+ total_usd_allocated_by_rnn = sum(d['target_usd_amount'] for d in investment_decisions if d['action'] == 'BUY')
613
+ # calculated_final_amount = amount # Inicializa com o montante original
614
+ # Esta variável será atualizada após a execução das ordens e cálculo do lucro/perda
615
+
616
+
617
+ # 3. EXECUÇÃO DE ORDENS (Placeholder)
618
+
619
+ # Dentro de execute_investment_strategy_background, substituindo a seção de execução de ordens:
620
+
621
+ logger.info(f"BG TASK [{rnn_tx_id}]: Executando ordens baseadas nas decisões da RNN...")
622
+ transactions_db[rnn_tx_id]["status_details"] = "Executing investment orders"
623
+ executed_trades_info = []
624
+ order_execution_success = True
625
+
626
+ # O calculated_final_amount começa como o 'amount' inicial.
627
+ # Vamos deduzir o que foi efetivamente usado para compras
628
+ # e depois adicionar os lucros/perdas.
629
+ # Por enquanto, vamos assumir que o investimento visa usar o 'total_usd_allocated_by_rnn'.
630
+ # E o restante do 'amount' não alocado fica como "cash".
631
+ cash_remaining_after_allocation = amount - total_usd_allocated_by_rnn
632
+ current_portfolio_value = 0 # Valor dos ativos comprados
633
+
634
+ # --- Exemplo de lógica de execução para decisões de COMPRA (BUY) ---
635
+ if investment_decisions: # Apenas se houver decisões
636
+ # exchange_exec = ccxt.binance({'apiKey': EXCHANGE_API_KEY, 'secret': EXCHANGE_API_SECRET}) # Exemplo
637
+ try:
638
+ for decision in investment_decisions:
639
+ if decision.get("action") == "BUY":
640
+ asset_id_to_buy = decision.get("asset_id") # Ex: "BTC/USDT" ou um ID interno que mapeia para um símbolo
641
+ usd_amount_to_spend = decision.get("target_usd_amount")
642
+
643
+ logger.info(f"BG TASK [{rnn_tx_id}]: Tentando comprar {usd_amount_to_spend} USD de {asset_id_to_buy}")
644
+
645
+ # --- SUBSTITUA PELA LÓGICA REAL DE EXECUÇÃO NA EXCHANGE ---
646
+ # Exemplo com ccxt (precisa de mais detalhes como símbolo de mercado correto):
647
+ # symbol_on_exchange = convert_asset_id_to_exchange_symbol(asset_id_to_buy, exchange_exec.id)
648
+ # current_price = (await exchange_exec.fetch_ticker(symbol_on_exchange))['last']
649
+ # amount_of_asset_to_buy = usd_amount_to_spend / current_price
650
+
651
+ # order = await exchange_exec.create_market_buy_order(symbol_on_exchange, amount_of_asset_to_buy)
652
+ # logger.info(f"BG TASK [{rnn_tx_id}]: Ordem de compra para {asset_id_to_buy} enviada: {order['id']}")
653
+ # executed_trades_info.append({
654
+ # "asset": asset_id_to_buy,
655
+ # "order_id": order['id'],
656
+ # "status": order.get('status', 'unknown'),
657
+ # "amount_filled": order.get('filled', 0),
658
+ # "avg_price": order.get('average', current_price),
659
+ # "cost_usd": order.get('cost', usd_amount_to_spend), # Custo real da ordem
660
+ # "fees": order.get('fee', {}),
661
+ # })
662
+ # current_portfolio_value += order.get('cost', usd_amount_to_spend) # Adiciona o valor do ativo comprado
663
+
664
+ # Simulação para prosseguir:
665
+ await asyncio.sleep(random.uniform(1, 3)) # Simula envio de ordem
666
+ simulated_order_id = f"sim_ord_{uuid.uuid4()}"
667
+ simulated_cost = usd_amount_to_spend * random.uniform(0.99, 1.0) # Slippage simulado
668
+ executed_trades_info.append({
669
+ "asset": asset_id_to_buy,
670
+ "order_id": simulated_order_id,
671
+ "status": "filled",
672
+ "amount_filled": simulated_cost / random.uniform(100, 200), # Qtd de ativo simulada
673
+ "avg_price": random.uniform(100, 200), # Preço simulado
674
+ "cost_usd": simulated_cost,
675
+ "fees": {"currency": "USD", "cost": simulated_cost * 0.001} # Taxa simulada
676
+ })
677
+ current_portfolio_value += simulated_cost
678
+ logger.info(f"BG TASK [{rnn_tx_id}]: Ordem simulada {simulated_order_id} para {asset_id_to_buy} preenchida, custo {simulated_cost:.2f} USD.")
679
+ else:
680
+ 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.")
681
+
682
+ except Exception as e:
683
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante execução de ordens: {str(e)}", exc_info=True)
684
+ order_execution_success = False
685
+ error_details += f"Order execution failed: {str(e)}; "
686
+ # finally:
687
+ # if 'exchange_exec' in locals() and hasattr(exchange_exec, 'close'):
688
+ # await exchange_exec.close()
689
+ else:
690
+ logger.info(f"BG TASK [{rnn_tx_id}]: Nenhuma decisão de investimento para executar.")
691
+ # Se não há decisões, o current_portfolio_value é 0 e o cash_remaining é todo o 'amount'
692
+ cash_remaining_after_allocation = amount
693
+
694
+ if not order_execution_success:
695
+ final_status = "failed_order_execution"
696
+ # ... (atualize transactions_db e pule para a seção de callback) ...
697
+ # O valor do portfólio aqui pode ser parcial se algumas ordens falharam
698
+ pass
699
+
700
+ transactions_db[rnn_tx_id]["executed_trades"] = executed_trades_info
701
+ transactions_db[rnn_tx_id]["status_details"] = "Simulating holding period and profit/loss"
702
+
703
+ # --- SIMULAÇÃO DO PERÍODO DE INVESTIMENTO E CÁLCULO DE LUCRO/PERDA DIÁRIO ---
704
+ # Em um sistema real, você monitoraria as posições e as fecharia no final do dia.
705
+ # Ou, se for um investimento de mais longo prazo, apenas calcularia o valor atual do portfólio.
706
+ # Para o objetivo de 4.2% ao dia, é implícito que as posições são fechadas diariamente.
707
+
708
+ logger.info(f"BG TASK [{rnn_tx_id}]: Simulando período de investimento e fechamento de posições...")
709
+ await asyncio.sleep(random.uniform(5, 10)) # Simula o dia passando
710
+
711
+ # Supondo que todas as posições são vendidas no final do "dia"
712
+ # E o current_portfolio_value muda com base no mercado.
713
+ # Para simular o objetivo de 4.2% sobre o VALOR INVESTIDO (current_portfolio_value no momento da compra):
714
+ if current_portfolio_value > 0: # Se algo foi investido
715
+ profit_on_invested_part = current_portfolio_value * (0.042 * random.uniform(0.8, 1.2)) # Simula variação no lucro
716
+ value_of_investments_at_eod = current_portfolio_value + profit_on_invested_part
717
+ else: # Nada foi investido
718
+ value_of_investments_at_eod = 0
719
+ profit_on_invested_part = 0
720
+
721
+ # O calculated_final_amount é o valor dos investimentos no fim do dia + o caixa que não foi alocado
722
+ calculated_final_amount = value_of_investments_at_eod + cash_remaining_after_allocation
723
+
724
+ logger.info(f"BG TASK [{rnn_tx_id}]: Valor inicial total: {amount:.2f}. "
725
+ f"Valor alocado para investimento: {total_usd_allocated_by_rnn:.2f}. "
726
+ f"Valor dos investimentos no EOD: {value_of_investments_at_eod:.2f}. "
727
+ f"Caixa não alocado: {cash_remaining_after_allocation:.2f}. "
728
+ f"Valor final total: {calculated_final_amount:.2f}")
729
+
730
+ transactions_db[rnn_tx_id]["eod_portfolio_value_simulated"] = value_of_investments_at_eod
731
+
732
+
733
+
734
+ # 4. TOKENIZAÇÃO / REGISTRO DA OPERAÇÃO (Placeholder)
735
+ logger.info(f"BG TASK [{rnn_tx_id}]: Registrando (tokenizando) operação...")
736
+ # Aqui você implementaria sua lógica de tokenização.
737
+ # Poderia ser salvar em uma blockchain, ou um registro detalhado e imutável no seu DB.
738
+ # Ex: await tokenize_operation_async(rnn_tx_id, client_id, investment_decisions, execution_results)
739
+ await asyncio.sleep(2)
740
+ transactions_db[rnn_tx_id]["tokenization_status"] = "completed"
741
+ logger.info(f"BG TASK [{rnn_tx_id}]: Operação registrada/tokenizada.")
742
+
743
+ transactions_db[rnn_tx_id]["status"] = "completed"
744
+ transactions_db[rnn_tx_id]["final_amount"] = calculated_final_amount
745
+ transactions_db[rnn_tx_id]["profit_loss"] = calculated_final_amount - amount
746
+
747
+ except Exception as e:
748
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante execução da estratégia: {str(e)}", exc_info=True)
749
+ final_status = "failed"
750
+ error_details = str(e)
751
+ transactions_db[rnn_tx_id]["status"] = "failed"
752
+ transactions_db[rnn_tx_id]["error"] = error_details
753
+ # Em caso de falha, o final_amount pode ser o inicial ou o que foi possível recuperar
754
+ calculated_final_amount = amount # Ou o valor parcial se algumas ordens falharam
755
+
756
+ # 5. PREPARAR E ENVIAR CALLBACK PARA AIBANK
757
+ if not AIBANK_CALLBACK_URL or not CALLBACK_SHARED_SECRET:
758
+ logger.error(f"BG TASK [{rnn_tx_id}]: AIBANK_CALLBACK_URL ou CALLBACK_SHARED_SECRET não configurado. Não é possível enviar callback.")
759
+ transactions_db[rnn_tx_id]["callback_status"] = "config_missing"
760
+ return
761
+
762
+ callback_payload = InvestmentResultPayload(
763
+ rnn_transaction_id=rnn_tx_id,
764
+ aibank_transaction_token=aibank_tx_token,
765
+ client_id=client_id,
766
+ initial_amount=amount,
767
+ final_amount=calculated_final_amount,
768
+ profit_loss=calculated_final_amount - amount,
769
+ status=final_status,
770
+ timestamp=datetime.utcnow(),
771
+ details=error_details if final_status == "failed" else "Investment cycle completed."
772
+ )
773
+
774
+ payload_json = callback_payload.model_dump_json()
775
+
776
+ # Criar assinatura HMAC para segurança do callback
777
+ signature = hmac.new(
778
+ CALLBACK_SHARED_SECRET.encode('utf-8'),
779
+ payload_json.encode('utf-8'),
780
+ hashlib.sha256
781
+ ).hexdigest()
782
+
783
+ headers = {
784
+ 'Content-Type': 'application/json',
785
+ 'X-RNN-Signature': signature # Assinatura para o aibank verificar
786
+ }
787
+
788
+ logger.info(f"BG TASK [{rnn_tx_id}]: Enviando callback para AIBank: {AIBANK_CALLBACK_URL}")
789
+ transactions_db[rnn_tx_id]["callback_status"] = "sending"
790
+ try:
791
+ async with httpx.AsyncClient() as client:
792
+ response = await client.post(AIBANK_CALLBACK_URL, content=payload_json, headers=headers, timeout=30.0)
793
+ response.raise_for_status() # Lança exceção para erros HTTP 4xx/5xx
794
+ logger.info(f"BG TASK [{rnn_tx_id}]: Callback enviado com sucesso para AIBank. Status: {response.status_code}")
795
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_success_{response.status_code}"
796
+ except httpx.RequestError as e:
797
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao enviar callback para AIBank (RequestError): {str(e)}")
798
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_request_error"
799
+ except httpx.HTTPStatusError as e:
800
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro HTTP ao enviar callback para AIBank (HTTPStatusError): {e.response.status_code} - {e.response.text}")
801
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_http_error_{e.response.status_code}"
802
+ except Exception as e:
803
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro inesperado ao enviar callback: {str(e)}", exc_info=True)
804
+ transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_unknown_error"
805
+
806
+ """
807
+ # --- Endpoints da API ---
808
+ @app.post("/api/invest",
809
+ response_model=InvestmentResponse,
810
+ dependencies=[Depends(verify_aibank_key)])
811
+ async def initiate_investment(
812
+ request_data: InvestmentRequest,
813
+ background_tasks: BackgroundTasks
814
+ ):
815
+ """
816
+ Endpoint para o AIBank iniciar um ciclo de investimento.
817
+ Responde rapidamente e executa a lógica pesada em background.
818
+ """
819
+ logger.info(f"Requisição de investimento recebida para client_id: {request_data.client_id}, "
820
+ f"amount: {request_data.amount}, aibank_tx_token: {request_data.aibank_transaction_token}")
821
+
822
+ rnn_tx_id = str(uuid.uuid4())
823
+
824
+ # Armazena informações iniciais da transação DB real para ser mais robusto
825
+ transactions_db[rnn_tx_id] = {
826
+ "rnn_transaction_id": rnn_tx_id,
827
+ "aibank_transaction_token": request_data.aibank_transaction_token,
828
+ "client_id": request_data.client_id,
829
+ "initial_amount": request_data.amount,
830
+ "status": "pending_background_processing",
831
+ "received_at": datetime.utcnow().isoformat(),
832
+ "callback_status": "not_sent_yet"
833
+ }
834
+
835
+ # Adiciona a tarefa de longa duração ao background
836
+ background_tasks.add_task(
837
+ execute_investment_strategy_background,
838
+ rnn_tx_id,
839
+ request_data.client_id,
840
+ request_data.amount,
841
+ request_data.aibank_transaction_token
842
+ )
843
+
844
+ logger.info(f"Estratégia de investimento para rnn_tx_id: {rnn_tx_id} agendada para execução em background.")
845
+ return InvestmentResponse(
846
+ status="pending",
847
+ message="Investment request received and is being processed in the background. Await callback for results.",
848
+ rnn_transaction_id=rnn_tx_id
849
+ )
850
+
851
+ @app.get("/api/transaction_status/{rnn_tx_id}", response_class=JSONResponse)
852
+ async def get_transaction_status(rnn_tx_id: str):
853
+ """ Endpoint para verificar o status de uma transação (para debug/admin) """
854
+ transaction = transactions_db.get(rnn_tx_id)
855
+ if not transaction:
856
+ raise HTTPException(status_code=404, detail="Transaction not found")
857
+ return transaction
858
+
859
+
860
+ # --- Dashboard (Existente, adaptado) ---
861
+ # Setup para arquivos estáticos e templates
862
+
863
+ try:
864
+ app.mount("/static", StaticFiles(directory="rnn/static"), name="static")
865
+ templates = Environment(loader=FileSystemLoader("rnn/templates"))
866
+ except RuntimeError as e:
867
+ logger.warning(f"Não foi possível montar /static ou carregar templates: {e}. O dashboard pode não funcionar.")
868
+ templates = None # Para evitar erros se o loader falhar
869
+
870
+ @app.get("/", response_class=HTMLResponse)
871
+ async def index(request: Request):
872
+ if not templates:
873
+ return HTMLResponse("<html><body><h1>Dashboard indisponível</h1><p>Configuração de templates/estáticos falhou.</p></body></html>")
874
+
875
+ agora = datetime.now()
876
+ agentes_simulados = [
877
+ # dados de agentes ...
878
+ ]
879
+ template = templates.get_template("index.html")
880
+ # Adicionar transações recentes ao contexto do template
881
+ recent_txs = list(transactions_db.values())[-5:] # Últimas 5 transações
882
+ return HTMLResponse(template.render(request=request, agentes=agentes_simulados, transactions=recent_txs))
883
+
884
+ # --- Imports para Background Task ---
885
+ import asyncio
886
+ import random
887
+
888
+ # Função de logger dummy s
889
+ # class DummyLogger:
890
+ # def info(self, msg, *args, **kwargs): print(f"INFO: {msg}")
891
+ # def warning(self, msg, *args, **kwargs): print(f"WARNING: {msg}")
892
+ # def error(self, msg, *args, **kwargs): print(f"ERROR: {msg}", kwargs.get('exc_info'))
893
+
894
+ # if __name__ == "__main__": # Para teste local
895
+ # # logger = DummyLogger() # se não tiver get_logger()
896
+ # # Configuração das variáveis de ambiente para teste local
897
+ # os.environ["AIBANK_API_KEY"] = "test_aibank_key_from_rnn_server"
898
+ # os.environ["AIBANK_CALLBACK_URL"] = "http://localhost:8001/api/rnn_investment_result_callback" # URL do aibank simulado
899
+ # os.environ["CALLBACK_SHARED_SECRET"] = "super_secret_for_callback_signing"
900
+ # # import uvicorn
901
+ # # uvicorn.run(app, host="0.0.0.0", port=8000)
app.py ADDED
@@ -0,0 +1,750 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rnn/app.py
2
+
3
+ import os
4
+ import uuid
5
+ import time
6
+ import hmac
7
+ import hashlib
8
+ import json
9
+ from datetime import datetime, timedelta
10
+ from typing import Dict, Any, List, Optional
11
+ from rnn.app.ccxt_utils import get_ccxt_exchange, fetch_crypto_data
12
+
13
+ import httpx # Para fazer chamadas HTTP assíncronas (para o callback)
14
+ from fastapi import FastAPI, Request, HTTPException, Depends, Header, BackgroundTasks
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ from fastapi.responses import HTMLResponse, JSONResponse
17
+ from fastapi.staticfiles import StaticFiles
18
+ from jinja2 import Environment, FileSystemLoader
19
+ from pydantic import BaseModel, Field
20
+
21
+ from rnn.app.model.rnn_predictor import RNNModelPredictor
22
+ from rnn.app.utils.logger import get_logger
23
+
24
+
25
+
26
+ logger = get_logger()
27
+
28
+ # --- Configuração Inicial e Variáveis de Ambiente (Secrets do Hugging Face) ---
29
+ AIBANK_API_KEY = os.environ.get("AIBANK_API_KEY") # Chave que o aibank usa para chamar esta API RNN
30
+ AIBANK_CALLBACK_URL = os.environ.get("AIBANK_CALLBACK_URL") # URL no aibank para onde esta API RNN enviará o resultado
31
+ CALLBACK_SHARED_SECRET = os.environ.get("CALLBACK_SHARED_SECRET") # Segredo para assinar/verificar o payload do callback
32
+
33
+ # Chaves para serviços externos
34
+ MARKET_DATA_API_KEY = os.environ.get("MARKET_DATA_API_KEY")
35
+ EXCHANGE_API_KEY = os.environ.get("EXCHANGE_API_KEY")
36
+ EXCHANGE_API_SECRET = os.environ.get("EXCHANGE_API_SECRET")
37
+
38
+ if not AIBANK_API_KEY:
39
+ logger.warning("AIBANK_API_KEY não configurada. A autenticação para /api/invest falhou.")
40
+ if not AIBANK_CALLBACK_URL:
41
+ logger.warning("AIBANK_CALLBACK_URL não configurada. O callback para o aibank falhou.")
42
+ if not CALLBACK_SHARED_SECRET:
43
+ logger.warning("CALLBACK_SHARED_SECRET não configurado. A segurança do callback está comprometida.")
44
+
45
+
46
+
47
+
48
+ app = FastAPI(title="ATCoin Neural Agents - Investment API")
49
+
50
+ # --- Middlewares ---
51
+ app.add_middleware(
52
+ CORSMiddleware,
53
+ allow_origins=[
54
+ "http://localhost:3000", # URL desenvolvimento local
55
+ "http://aibank.app.br", # URL de produção
56
+ "https://*.aibank.app.br", # subdomínios
57
+ "https://*.hf.space" # HF Space
58
+ ],
59
+ allow_credentials=True,
60
+ allow_methods=["*"],
61
+ allow_headers=["*"],
62
+ )
63
+
64
+ # --- Simulação de Banco de Dados de Transações DEV ---
65
+ # Em produção MongoDB
66
+ transactions_db: Dict[str, Dict[str, Any]] = {}
67
+
68
+ # --- Modelos Pydantic ---
69
+ class InvestmentRequest(BaseModel):
70
+ client_id: str
71
+ amount: float = Field(..., gt=0) # Garante que o montante seja positivo
72
+ aibank_transaction_token: str # Token único gerado pelo aibank para rastreamento
73
+
74
+ class InvestmentResponse(BaseModel):
75
+ status: str
76
+ message: str
77
+ rnn_transaction_id: str # ID da transação this.API
78
+
79
+ class InvestmentResultPayload(BaseModel): # Payload para o callback para o aibank
80
+ rnn_transaction_id: str
81
+ aibank_transaction_token: str
82
+ client_id: str
83
+ initial_amount: float
84
+ final_amount: float
85
+ profit_loss: float
86
+ status: str # "completed", "failed"
87
+ timestamp: datetime
88
+ details: str = ""
89
+
90
+
91
+ # --- Dependência de Autenticação ---
92
+ async def verify_aibank_key(authorization: str = Header(None)):
93
+ if not AIBANK_API_KEY: # Checagem se a chave do servidor está configurada
94
+ logger.error("CRITICAL: AIBANK_API_KEY (server-side) não está configurada nos Secrets.")
95
+ raise HTTPException(status_code=500, detail="Internal Server Configuration Error: Missing server API Key.")
96
+
97
+ if authorization is None:
98
+ logger.warning("Authorization header ausente na chamada do AIBank.")
99
+ raise HTTPException(status_code=401, detail="Authorization header is missing")
100
+
101
+ parts = authorization.split()
102
+ if len(parts) != 2 or parts[0].lower() != 'bearer':
103
+ logger.warning(f"Formato inválido do Authorization header: {authorization}")
104
+ raise HTTPException(status_code=401, detail="Authorization header must be 'Bearer <token>'")
105
+
106
+ token_from_aibank = parts[1]
107
+ if not hmac.compare_digest(token_from_aibank, AIBANK_API_KEY):
108
+ logger.warning(f"Chave de API inválida fornecida pelo AIBank. Token: {token_from_aibank[:10]}...")
109
+ raise HTTPException(status_code=403, detail="Invalid API Key provided by AIBank.")
110
+ logger.info("API Key do AIBank verificada com sucesso.")
111
+ return True
112
+
113
+
114
+ # --- Lógica de Negócio Principal (Simulada e em Background) ---
115
+
116
+
117
+ async def execute_investment_strategy_background(
118
+ rnn_tx_id: str,
119
+ client_id: str,
120
+ amount: float,
121
+ aibank_tx_token: str
122
+ ):
123
+ logger.info(f"BG TASK [{rnn_tx_id}]: Iniciando estratégia de investimento para cliente {client_id}, valor {amount}.")
124
+ transactions_db[rnn_tx_id]["status"] = "processing"
125
+ transactions_db[rnn_tx_id]["status_details"] = "Initializing investment cycle"
126
+
127
+ final_status = "completed"
128
+ error_details = "" # Acumula mensagens de erro de várias etapas
129
+ calculated_final_amount = amount
130
+
131
+ # Inicializa a exchange ccxt usando o utilitário
132
+ # O logger do app.py é passado para ccxt_utils para que os logs apareçam no mesmo stream
133
+ exchange = await get_ccxt_exchange(logger_instance=logger) # MODIFICADO
134
+
135
+ if not exchange:
136
+ # get_ccxt_exchange já loga o erro. Se a exchange é crucial, podemos falhar aqui.
137
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Falha ao inicializar a exchange. A estratégia pode não funcionar como esperado para cripto.")
138
+ # Se as chaves CCXT foram fornecidas no ambiente mas a exchange falhou, considere isso um erro de config.
139
+ if os.environ.get("CCXT_API_KEY") and os.environ.get("CCXT_API_SECRET"):
140
+ error_details += "Failed to initialize CCXT exchange despite API keys being present; "
141
+ final_status = "failed_config"
142
+ # (PULAR PARA CALLBACK - veja a seção de tratamento de erro crítico abaixo)
143
+
144
+ # =========================================================================
145
+ # 1. COLETAR DADOS DE MERCADO
146
+ # =========================================================================
147
+ logger.info(f"BG TASK [{rnn_tx_id}]: Coletando dados de mercado...")
148
+ transactions_db[rnn_tx_id]["status_details"] = "Fetching market data"
149
+ market_data_results = {"crypto": {}, "stocks": {}, "other": {}}
150
+ critical_data_fetch_failed = False # Flag para falha crítica na coleta de dados
151
+
152
+ # --- Coleta de dados de Cripto via ccxt_utils ---
153
+ if exchange:
154
+ crypto_pairs_to_fetch = ["BTC/USDT", "ETH/USDT", "SOL/USDT"] # Mantenha configurável
155
+
156
+ crypto_data, crypto_fetch_ok, crypto_err_msg = await fetch_crypto_data(
157
+ exchange,
158
+ crypto_pairs_to_fetch,
159
+ logger_instance=logger
160
+ )
161
+ market_data_results["crypto"] = crypto_data
162
+ if not crypto_fetch_ok:
163
+ error_details += f"Crypto data fetch issues: {crypto_err_msg}; "
164
+ # Decida se a falha na coleta de cripto é crítica
165
+ # Se for, defina critical_data_fetch_failed = True
166
+ if os.environ.get("CCXT_API_KEY"): # Se esperávamos dados de cripto
167
+ critical_data_fetch_failed = True
168
+ logger.error(f"BG TASK [{rnn_tx_id}]: Falha crítica na coleta de dados de cripto.")
169
+ else:
170
+ logger.info(f"BG TASK [{rnn_tx_id}]: Instância da exchange ccxt não disponível. Pulando coleta de dados de cripto.")
171
+ if os.environ.get("CCXT_API_KEY"): # Se esperávamos dados de cripto mas a exchange não inicializou
172
+ error_details += "CCXT exchange not initialized, crypto data skipped; "
173
+ critical_data_fetch_failed = True
174
+
175
+
176
+ # --- Coleta de dados para outros tipos de ativos (ex: Ações com yfinance) ---
177
+ # (Sua lógica yfinance aqui, se aplicável, similarmente atualizando market_data_results["stocks"])
178
+ # try:
179
+ # import yfinance as yf # Mova para o topo do app.py se for usar
180
+ # # ... lógica yfinance ...
181
+ # except Exception as e_yf:
182
+ # logger.warning(f"BG TASK [{rnn_tx_id}]: Falha ao buscar dados de ações com yfinance: {e_yf}")
183
+ # error_details += f"YFinance data fetch failed: {str(e_yf)}; "
184
+ # # Decida se isso é crítico: critical_data_fetch_failed = True
185
+
186
+ market_data_results["other"]['simulated_index_level'] = random.uniform(10000, 15000) # Mantém simulação
187
+
188
+ transactions_db[rnn_tx_id]["market_data_collected"] = market_data_results
189
+
190
+ # --- PONTO DE CHECAGEM PARA FALHA CRÍTICA NA COLETA DE DADOS ---
191
+ if critical_data_fetch_failed:
192
+ final_status = "failed_market_data"
193
+ logger.error(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado falhou criticamente. {error_details}")
194
+ # Pular para a seção de callback
195
+ # (A lógica de envio do callback precisa ser alcançada)
196
+ else:
197
+ logger.info(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado concluída.")
198
+ transactions_db[rnn_tx_id]["status_details"] = "Processing RNN analysis"
199
+
200
+
201
+
202
+
203
+
204
+ # =========================================================================
205
+ # 2. ANÁLISE PELA RNN E TOMADA DE DECISÃO
206
+ # =========================================================================
207
+ investment_decisions: List[Dict[str, Any]] = []
208
+ total_usd_allocated_by_rnn = 0.0
209
+ loop = asyncio.get_running_loop()
210
+
211
+ if final_status == "completed":
212
+ logger.info(f"BG TASK [{rnn_tx_id}]: Executando análise RNN...")
213
+ transactions_db[rnn_tx_id]["status_details"] = "Running RNN model"
214
+ rnn_analysis_success = True
215
+
216
+ # CORRIGIDO: Acessando app.state.rnn_predictor
217
+ predictor: Optional[RNNModelPredictor] = getattr(app.state, 'rnn_predictor', None)
218
+
219
+ try:
220
+ crypto_data_for_rnn = market_data_results.get("crypto", {})
221
+ candidate_assets = [
222
+ asset_key for asset_key, data in crypto_data_for_rnn.items()
223
+ if data and not data.get("error") and data.get("ohlcv_1h") # Apenas com dados válidos
224
+ ]
225
+
226
+ # --- Parâmetros de Gerenciamento de Risco e Alocação (AJUSTE FINO É CRUCIAL) ---
227
+ # Risco total do portfólio para este ciclo (ex: não usar mais que 50% do capital total em novas posições)
228
+ MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE = 0.75 # Usar até 75% do 'amount'
229
+
230
+ # Risco por ativo individual (percentual do 'amount' TOTAL)
231
+ MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL = 0.15 # Ex: máx 15% do capital total em UM ativo
232
+ MIN_ALLOCATION_PER_ASSET_PCT_OF_TOTAL = 0.02 # Ex: mín 2% do capital total para valer a pena
233
+
234
+ MIN_USD_PER_ORDER = 25.00 # Mínimo de USD por ordem
235
+ MAX_CONCURRENT_POSITIONS = 4 # Máximo de posições abertas simultaneamente
236
+
237
+ # Limiares de Confiança da RNN
238
+ CONFIDENCE_STRONG_BUY = 0.80 # Confiança para considerar uma alocação maior
239
+ CONFIDENCE_MODERATE_BUY = 0.65 # Confiança mínima para considerar uma alocação base
240
+ CONFIDENCE_WEAK_BUY = 0.55 # Confiança para uma alocação muito pequena ou nenhuma
241
+
242
+ allocated_capital_this_cycle = 0.0
243
+
244
+ # Para diversificação, podemos querer limitar a avaliação ou dar pesos
245
+ # random.shuffle(candidate_assets)
246
+
247
+ for asset_key in candidate_assets:
248
+ if len(investment_decisions) >= MAX_CONCURRENT_POSITIONS:
249
+ logger.info(f"BG TASK [{rnn_tx_id}]: Limite de {MAX_CONCURRENT_POSITIONS} posições concorrentes atingido.")
250
+ break
251
+
252
+ # Verifica se já usamos o capital máximo para o ciclo
253
+ if allocated_capital_this_cycle >= amount * MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE:
254
+ logger.info(f"BG TASK [{rnn_tx_id}]: Limite de capital para o ciclo ({MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE*100}%) atingido.")
255
+ break
256
+
257
+ asset_symbol = asset_key.replace("_", "/")
258
+ logger.info(f"BG TASK [{rnn_tx_id}]: RNN avaliando ativo: {asset_symbol}")
259
+
260
+ signal, confidence_prob = await predictor.predict_for_asset(
261
+ crypto_data_for_rnn[asset_key],
262
+ loop=loop
263
+ )
264
+
265
+ if signal == 1 and confidence_prob is not None: # Sinal de COMPRA e confiança válida
266
+ target_usd_allocation = 0.0
267
+
268
+ if confidence_prob >= CONFIDENCE_STRONG_BUY:
269
+ # Alocação maior para sinais fortes
270
+ # Ex: entre 60% e 100% da alocação máxima permitida por ativo
271
+ alloc_factor = 0.6 + 0.4 * ((confidence_prob - CONFIDENCE_STRONG_BUY) / (1.0 - CONFIDENCE_STRONG_BUY + 1e-6))
272
+ target_usd_allocation = (amount * MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL) * alloc_factor
273
+ reason = f"RNN STRONG BUY signal (Conf: {confidence_prob:.3f})"
274
+ elif confidence_prob >= CONFIDENCE_MODERATE_BUY:
275
+ # Alocação base para sinais moderados
276
+ # Ex: entre 30% e 60% da alocação máxima permitida por ativo
277
+ alloc_factor = 0.3 + 0.3 * ((confidence_prob - CONFIDENCE_MODERATE_BUY) / (CONFIDENCE_STRONG_BUY - CONFIDENCE_MODERATE_BUY + 1e-6))
278
+ target_usd_allocation = (amount * MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL) * alloc_factor
279
+ reason = f"RNN MODERATE BUY signal (Conf: {confidence_prob:.3f})"
280
+ elif confidence_prob >= CONFIDENCE_WEAK_BUY:
281
+ # Alocação pequena para sinais fracos (ou nenhuma)
282
+ alloc_factor = 0.1 + 0.2 * ((confidence_prob - CONFIDENCE_WEAK_BUY) / (CONFIDENCE_MODERATE_BUY - CONFIDENCE_WEAK_BUY + 1e-6))
283
+ target_usd_allocation = (amount * MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL) * alloc_factor
284
+ reason = f"RNN WEAK BUY signal (Conf: {confidence_prob:.3f})"
285
+ else:
286
+ 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.")
287
+ continue
288
+
289
+ # Garantir que a alocação não seja menor que a mínima permitida (percentual do total)
290
+ target_usd_allocation = max(target_usd_allocation, amount * MIN_ALLOCATION_PER_ASSET_PCT_OF_TOTAL)
291
+
292
+ # Garantir que não exceda o capital restante disponível neste CICLO
293
+ capital_left_for_this_cycle = (amount * MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE) - allocated_capital_this_cycle
294
+ actual_usd_allocation = min(target_usd_allocation, capital_left_for_this_cycle)
295
+
296
+ # Garantir que a ordem mínima em USD seja respeitada
297
+ if actual_usd_allocation < MIN_USD_PER_ORDER:
298
+ 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.")
299
+ continue
300
+
301
+ # Adicionar à lista de decisões
302
+ investment_decisions.append({
303
+ "asset_id": asset_symbol, "type": "CRYPTO", "action": "BUY",
304
+ "target_usd_amount": round(actual_usd_allocation, 2),
305
+ "rnn_confidence": round(confidence_prob, 4),
306
+ "reasoning": reason
307
+ })
308
+ allocated_capital_this_cycle += round(actual_usd_allocation, 2)
309
+ logger.info(f"BG TASK [{rnn_tx_id}]: Decisão: COMPRAR {actual_usd_allocation:.2f} USD de {asset_symbol}. {reason}")
310
+
311
+ # ... (restante da lógica para signal 0 ou None) ...
312
+ except Exception as e: # Captura exceções da lógica da RNN
313
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro CRÍTICO durante análise/predição RNN: {str(e)}", exc_info=True)
314
+ rnn_analysis_success = False # Marca que a análise RNN falhou
315
+ error_details += f"Critical RNN analysis/prediction error: {str(e)}; "
316
+
317
+
318
+ total_usd_allocated_by_rnn = allocated_capital_this_cycle
319
+
320
+
321
+
322
+
323
+ if not predictor or not predictor.model: # Verifica se o preditor e o modelo interno existem
324
+ 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.")
325
+ rnn_analysis_success = False
326
+ error_details += "RNN model/predictor not available for prediction; "
327
+ else:
328
+ try:
329
+ # ... (lógica de iteração sobre `candidate_assets` e chamada a `predictor.predict_for_asset` como na resposta anterior)
330
+ # ... (lógica de alocação de capital como na resposta anterior)
331
+ # Garantir que toda essa lógica está dentro deste bloco 'else'
332
+ crypto_data_for_rnn = market_data_results.get("crypto", {})
333
+ candidate_assets = [
334
+ asset_key for asset_key, data in crypto_data_for_rnn.items()
335
+ if data and not data.get("error") and data.get("ohlcv_1h")
336
+ ]
337
+
338
+ MAX_RISK_PER_ASSET_PCT = 0.05
339
+ MIN_USD_PER_ORDER = 20.00
340
+ MAX_CONCURRENT_POSITIONS = 5
341
+ CONFIDENCE_THRESHOLD_FOR_MAX_ALLOC = 0.85
342
+ CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC = 0.60
343
+ BASE_ALLOCATION_PCT_OF_TOTAL_CAPITAL = 0.10
344
+
345
+ allocated_capital_this_cycle = 0.0
346
+
347
+ for asset_key in candidate_assets:
348
+ if len(investment_decisions) >= MAX_CONCURRENT_POSITIONS:
349
+ logger.info(f"BG TASK [{rnn_tx_id}]: Limite de posições concorrentes ({MAX_CONCURRENT_POSITIONS}) atingido.")
350
+ break
351
+ if allocated_capital_this_cycle >= amount * 0.90:
352
+ logger.info(f"BG TASK [{rnn_tx_id}]: Limite de capital do ciclo atingido.")
353
+ break
354
+
355
+ asset_symbol = asset_key.replace("_", "/")
356
+ logger.info(f"BG TASK [{rnn_tx_id}]: RNN avaliando ativo: {asset_symbol}")
357
+
358
+ signal, confidence_prob = await predictor.predict_for_asset(
359
+ crypto_data_for_rnn[asset_key],
360
+ loop=loop
361
+ # window_size e expected_features serão os defaults de rnn_predictor.py
362
+ # ou podem ser passados explicitamente se você quiser variar por ativo
363
+ )
364
+
365
+ if signal == 1:
366
+ if confidence_prob is None or confidence_prob < CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC:
367
+ 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.")
368
+ continue
369
+
370
+ confidence_factor = 0.5
371
+ if confidence_prob >= CONFIDENCE_THRESHOLD_FOR_MAX_ALLOC:
372
+ confidence_factor = 1.0
373
+ elif confidence_prob > CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC:
374
+ confidence_factor = 0.5 + 0.5 * (
375
+ (confidence_prob - CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC) /
376
+ (CONFIDENCE_THRESHOLD_FOR_MAX_ALLOC - CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC)
377
+ )
378
+
379
+ potential_usd_allocation = amount * BASE_ALLOCATION_PCT_OF_TOTAL_CAPITAL * confidence_factor
380
+ potential_usd_allocation = min(potential_usd_allocation, amount * MAX_RISK_PER_ASSET_PCT)
381
+ remaining_capital_for_cycle = amount - allocated_capital_this_cycle # Recalcula a cada iteração
382
+ actual_usd_allocation = min(potential_usd_allocation, remaining_capital_for_cycle)
383
+
384
+ if actual_usd_allocation < MIN_USD_PER_ORDER:
385
+ 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.")
386
+ continue
387
+
388
+ investment_decisions.append({
389
+ "asset_id": asset_symbol, "type": "CRYPTO", "action": "BUY",
390
+ "target_usd_amount": round(actual_usd_allocation, 2),
391
+ "rnn_confidence": round(confidence_prob, 4) if confidence_prob is not None else None,
392
+ "reasoning": f"RNN signal BUY for {asset_symbol} with confidence {confidence_prob:.2f}"
393
+ })
394
+ allocated_capital_this_cycle += round(actual_usd_allocation, 2)
395
+ logger.info(f"BG TASK [{rnn_tx_id}]: Decisão: COMPRAR {actual_usd_allocation:.2f} USD de {asset_symbol} (Conf: {confidence_prob:.2f})")
396
+
397
+ elif signal == 0:
398
+ 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'})")
399
+ else:
400
+ logger.warning(f"BG TASK [{rnn_tx_id}]: RNN não gerou sinal para {asset_symbol}.")
401
+
402
+ if not investment_decisions:
403
+ 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.")
404
+
405
+ except Exception as e: # Captura exceções da lógica da RNN
406
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro CRÍTICO durante análise/predição RNN: {str(e)}", exc_info=True)
407
+ rnn_analysis_success = False # Marca que a análise RNN falhou
408
+ error_details += f"Critical RNN analysis/prediction error: {str(e)}; "
409
+
410
+ if not rnn_analysis_success: # Se a flag foi setada para False
411
+ final_status = "failed_rnn_analysis"
412
+
413
+ transactions_db[rnn_tx_id]["rnn_decisions"] = investment_decisions
414
+
415
+ total_usd_allocated_by_rnn = allocated_capital_this_cycle
416
+ transactions_db[rnn_tx_id]["status_details"] = "Preparing to execute orders"
417
+
418
+
419
+
420
+
421
+
422
+
423
+
424
+
425
+
426
+
427
+ # =========================================================================
428
+ # 3. EXECUÇÃO DE ORDENS (Só executa se a RNN não falhou e gerou ordens)
429
+ # =========================================================================
430
+ executed_trades_info: List[Dict[str, Any]] = []
431
+ current_portfolio_value = 0.0 # Valor dos ativos comprados, baseado no custo
432
+ cash_remaining_after_execution = amount # Começa com todo o montante
433
+
434
+ if final_status == "completed" and investment_decisions and exchange:
435
+ logger.info(f"BG TASK [{rnn_tx_id}]: Executando {len(investment_decisions)} ordens...")
436
+ transactions_db[rnn_tx_id]["status_details"] = "Executing investment orders"
437
+ order_execution_overall_success = True
438
+
439
+ # Placeholder para LÓGICA REAL DE EXECUÇÃO DE ORDENS (CREATE_ORDER_PLACEHOLDER)
440
+ # Esta seção precisa ser preenchida com:
441
+ # 1. Iterar sobre `investment_decisions`.
442
+ # 2. Para cada decisão de "BUY":
443
+ # a. Determinar o símbolo correto na exchange (ex: "BTC/USDT").
444
+ # b. Obter o preço atual (ticker) para calcular a quantidade de ativo a comprar.
445
+ # `amount_of_asset = target_usd_amount / current_price_of_asset`
446
+ # c. Considerar saldo disponível na exchange (se estiver gerenciando isso).
447
+ # d. Criar a ordem via `await exchange.create_market_buy_order(symbol, amount_of_asset)`
448
+ # ou `create_limit_buy_order(symbol, amount_of_asset, limit_price)`.
449
+ # Para ordens limite, a RNN precisaria fornecer o `limit_price`.
450
+ # e. Tratar respostas da exchange (sucesso, falha, ID da ordem).
451
+ # `ccxt.InsufficientFunds`, `ccxt.InvalidOrder`, etc.
452
+ # f. Armazenar detalhes da ordem em `executed_trades_info`:
453
+ # { "asset_id": ..., "order_id_exchange": ..., "type": "market/limit", "side": "buy",
454
+ # "requested_usd_amount": ..., "asset_quantity_ordered": ...,
455
+ # "status_from_exchange": ..., "filled_quantity": ..., "average_fill_price": ...,
456
+ # "cost_in_usd": ..., "fees_paid": ..., "timestamp": ... }
457
+ # g. Atualizar `current_portfolio_value` com o `cost_in_usd` da ordem preenchida.
458
+ # h. Deduzir `cost_in_usd` de `cash_remaining_after_execution`.
459
+ # 3. Para decisões de "SELL" (se sua RNN gerar):
460
+ # a. Verificar se você possui o ativo (requer gerenciamento de portfólio).
461
+ # b. Criar ordem de venda.
462
+ # c. Atualizar `current_portfolio_value` e `cash_remaining_after_execution`.
463
+
464
+ # Simulação atual:
465
+ for decision in investment_decisions:
466
+ if decision.get("action") == "BUY" and decision.get("type") == "CRYPTO":
467
+ asset_symbol = decision["asset_id"]
468
+ usd_to_spend = decision["target_usd_amount"]
469
+
470
+ # Simular pequena chance de falha na ordem
471
+ if random.random() < 0.05:
472
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Falha simulada ao executar ordem para {asset_symbol}.")
473
+ executed_trades_info.append({
474
+ "asset_id": asset_symbol, "status": "failed_simulated",
475
+ "requested_usd_amount": usd_to_spend, "error": "Simulated exchange rejection"
476
+ })
477
+ order_execution_overall_success = False # Marca que pelo menos uma falhou
478
+ continue # Pula para a próxima decisão
479
+
480
+ # Simular slippage e custo
481
+ simulated_cost = usd_to_spend * random.uniform(0.995, 1.005) # +/- 0.5% slippage
482
+
483
+ # Garantir que não estamos gastando mais do que o caixa restante
484
+ if simulated_cost > cash_remaining_after_execution:
485
+ simulated_cost = cash_remaining_after_execution # Gasta apenas o que tem
486
+ if simulated_cost < 1: # Se não há quase nada, não faz a ordem
487
+ logger.info(f"BG TASK [{rnn_tx_id}]: Saldo insuficiente ({cash_remaining_after_execution:.2f}) para ordem de {asset_symbol}, pulando.")
488
+ continue
489
+
490
+
491
+ if simulated_cost > 0:
492
+ current_portfolio_value += simulated_cost
493
+ cash_remaining_after_execution -= simulated_cost
494
+ executed_trades_info.append({
495
+ "asset_id": asset_symbol, "order_id_exchange": f"sim_ord_{uuid.uuid4()}",
496
+ "type": "market", "side": "buy",
497
+ "requested_usd_amount": usd_to_spend,
498
+ "status_from_exchange": "filled", "cost_in_usd": round(simulated_cost, 2),
499
+ "timestamp": datetime.utcnow().isoformat()
500
+ })
501
+ logger.info(f"BG TASK [{rnn_tx_id}]: Ordem simulada para {asset_symbol} (custo: {simulated_cost:.2f} USD) preenchida.")
502
+
503
+ await asyncio.sleep(random.uniform(1, 2) * len(investment_decisions) if investment_decisions else 1)
504
+
505
+ if not order_execution_overall_success:
506
+ error_details += "One or more orders failed during execution; "
507
+ # Decida se isso torna o status final 'failed_order_execution' ou se 'completed_with_partial_failure'
508
+ # final_status = "completed_with_partial_failure" # Exemplo de um novo status
509
+
510
+ elif not exchange and investment_decisions:
511
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Decisões de investimento geradas, mas a exchange não está disponível para execução.")
512
+ error_details += "Exchange not available for order execution; "
513
+ final_status = "failed_order_execution" # Se a execução é crítica
514
+ cash_remaining_after_execution = amount # Nada foi gasto
515
+
516
+ transactions_db[rnn_tx_id]["executed_trades"] = executed_trades_info
517
+ transactions_db[rnn_tx_id]["cash_after_execution"] = round(cash_remaining_after_execution, 2)
518
+ transactions_db[rnn_tx_id]["portfolio_value_after_execution"] = round(current_portfolio_value, 2)
519
+
520
+
521
+ # =========================================================================
522
+ # 4. SIMULAÇÃO DO PERÍODO DE INVESTIMENTO E CÁLCULO DE LUCRO/PERDA (Só se não houve falha crítica antes)
523
+ # =========================================================================
524
+ value_of_investments_at_eod = current_portfolio_value # Começa com o valor de custo
525
+
526
+ if final_status == "completed": # Ou "completed_with_partial_failure"
527
+ transactions_db[rnn_tx_id]["status_details"] = "Simulating EOD valuation"
528
+ logger.info(f"BG TASK [{rnn_tx_id}]: Simulando valorização do portfólio no final do dia...")
529
+ await asyncio.sleep(random.uniform(3, 7))
530
+
531
+ if current_portfolio_value > 0:
532
+ # Simular mudança de valor do portfólio. A meta de 4.2% é sobre o capital INVESTIDO.
533
+ # O lucro/perda é aplicado ao `current_portfolio_value` (o que foi efetivamente comprado).
534
+ daily_return_factor = 0.042 # A meta
535
+ simulated_performance_factor = random.uniform(0.7, 1.3) # Variação em torno da meta (pode ser prejuízo)
536
+ # Para ser mais realista, o fator de performance deveria ser algo como:
537
+ # random.uniform(-0.05, 0.08) -> -5% a +8% de retorno diário sobre o investido (ainda alto)
538
+ # E não diretamente ligado à meta de 4.2%
539
+
540
+ # Ajuste para uma simulação de retorno mais plausível (ainda agressiva)
541
+ # Suponha que o retorno diário real possa variar de -3% a +5% sobre o investido
542
+ actual_daily_return_on_portfolio = random.uniform(-0.03, 0.05)
543
+
544
+ profit_or_loss_on_portfolio = current_portfolio_value * actual_daily_return_on_portfolio
545
+ value_of_investments_at_eod = current_portfolio_value + profit_or_loss_on_portfolio
546
+ logger.info(f"BG TASK [{rnn_tx_id}]: Portfólio inicial: {current_portfolio_value:.2f}, Retorno simulado: {actual_daily_return_on_portfolio*100:.2f}%, "
547
+ f"Lucro/Prejuízo no portfólio: {profit_or_loss_on_portfolio:.2f}, Valor EOD do portfólio: {value_of_investments_at_eod:.2f}")
548
+ else:
549
+ logger.info(f"BG TASK [{rnn_tx_id}]: Nenhum portfólio para valorizar no EOD (nada foi comprado).")
550
+ value_of_investments_at_eod = 0.0
551
+
552
+ # O calculated_final_amount é o valor dos investimentos liquidados + o caixa que não foi usado
553
+ calculated_final_amount = value_of_investments_at_eod + cash_remaining_after_execution
554
+
555
+ else: # Se houve falha antes, o valor final é o que sobrou após a falha
556
+ calculated_final_amount = cash_remaining_after_execution + current_portfolio_value # current_portfolio_value pode ser 0 ou parcial
557
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Ciclo de investimento não concluído normalmente ({final_status}). Valor final baseado no estado atual.")
558
+
559
+ transactions_db[rnn_tx_id]["eod_portfolio_value_simulated"] = round(value_of_investments_at_eod, 2)
560
+ transactions_db[rnn_tx_id]["final_calculated_amount"] = round(calculated_final_amount, 2)
561
+
562
+
563
+ # =========================================================================
564
+ # 5. TOKENIZAÇÃO / REGISTRO DA OPERAÇÃO (Só se não houve falha crítica antes)
565
+ # =========================================================================
566
+ if final_status not in ["failed_config", "failed_market_data", "failed_rnn_analysis"]: # Prossegue se ao menos tentou executar
567
+ transactions_db[rnn_tx_id]["status_details"] = "Finalizing transaction log (tokenization)"
568
+ logger.info(f"BG TASK [{rnn_tx_id}]: Registrando (tokenizando) operação detalhadamente...")
569
+ # Placeholder para LÓGICA REAL DE TOKENIZAÇÃO (TOKENIZATION_PLACEHOLDER)
570
+ # 1. Coletar todos os dados relevantes da transação de `transactions_db[rnn_tx_id]`
571
+ # (market_data_collected, rnn_decisions, executed_trades, eod_portfolio_value_simulated, etc.)
572
+ # 2. Se for usar blockchain:
573
+ # a. Preparar os dados para um contrato inteligente.
574
+ # b. Interagir com o contrato (ex: web3.py para Ethereum).
575
+ # c. Armazenar o hash da transação da blockchain.
576
+ # 3. Se for um registro interno avançado:
577
+ # a. Assinar digitalmente os dados da transação.
578
+ # b. Armazenar em um sistema de log imutável ou banco de dados com auditoria.
579
+
580
+ # Simulação atual (hash dos dados da transação):
581
+ transaction_data_for_hash = {
582
+ "rnn_tx_id": rnn_tx_id, "client_id": client_id, "initial_amount": amount,
583
+ "final_amount_calculated": calculated_final_amount,
584
+ # Incluir resumos ou hashes dos dados coletados para não tornar o hash gigante
585
+ "market_data_summary_keys": list(transactions_db[rnn_tx_id].get("market_data_collected", {}).keys()),
586
+ "rnn_decisions_count": len(transactions_db[rnn_tx_id].get("rnn_decisions", [])),
587
+ "executed_trades_count": len(transactions_db[rnn_tx_id].get("executed_trades", [])),
588
+ "eod_portfolio_value": transactions_db[rnn_tx_id].get("eod_portfolio_value_simulated"),
589
+ "timestamp": datetime.utcnow().isoformat()
590
+ }
591
+ ordered_tx_data_str = json.dumps(transaction_data_for_hash, sort_keys=True)
592
+ proof_token_hash = hashlib.sha256(ordered_tx_data_str.encode('utf-8')).hexdigest()
593
+
594
+ transactions_db[rnn_tx_id]["proof_of_operation_token"] = proof_token_hash
595
+ transactions_db[rnn_tx_id]["tokenization_method"] = "internal_summary_hash_proof"
596
+ await asyncio.sleep(0.5) # Simula tempo de escrita/hash
597
+ logger.info(f"BG TASK [{rnn_tx_id}]: Operação registrada. Prova (hash): {proof_token_hash[:10]}...")
598
+
599
+
600
+ # =========================================================================
601
+ # 6. PREPARAR E ENVIAR CALLBACK PARA AIBANK
602
+ # =========================================================================
603
+ if exchange and hasattr(exchange, 'close'):
604
+ try:
605
+ await exchange.close()
606
+ logger.info(f"BG TASK [{rnn_tx_id}]: Conexão ccxt fechada.")
607
+ except Exception as e_close: # Especificar o tipo de exceção se souber
608
+ logger.warning(f"BG TASK [{rnn_tx_id}]: Erro ao fechar conexão ccxt: {str(e_close)}")
609
+
610
+ if not AIBANK_CALLBACK_URL or not CALLBACK_SHARED_SECRET:
611
+ logger.error(f"BG TASK [{rnn_tx_id}]: Configuração de callback ausente. Não é possível notificar o AIBank.")
612
+ transactions_db[rnn_tx_id]["callback_status"] = "config_missing_critical"
613
+ return
614
+
615
+ # Certifique-se que `final_status` reflete o estado real da operação
616
+ # Se `error_details` não estiver vazio e `final_status` ainda for "completed", ajuste-o
617
+ if error_details and final_status == "completed":
618
+ final_status = "completed_with_warnings" # Ou um status mais apropriado
619
+
620
+ callback_payload_data = InvestmentResultPayload(
621
+ rnn_transaction_id=rnn_tx_id, aibank_transaction_token=aibank_tx_token, client_id=client_id,
622
+ initial_amount=amount, final_amount=round(calculated_final_amount, 2), # Arredonda para 2 casas decimais
623
+ profit_loss=round(calculated_final_amount - amount, 2),
624
+ status=final_status, timestamp=datetime.utcnow(),
625
+ details=error_details if error_details else "Investment cycle processed."
626
+ )
627
+ payload_json_str = callback_payload_data.model_dump_json() # Garante que está usando a string serializada
628
+
629
+ signature = hmac.new(CALLBACK_SHARED_SECRET.encode('utf-8'), payload_json_str.encode('utf-8'), hashlib.sha256).hexdigest()
630
+ headers = {'Content-Type': 'application/json', 'X-RNN-Signature': signature}
631
+
632
+ logger.info(f"BG TASK [{rnn_tx_id}]: Enviando callback para AIBank ({AIBANK_CALLBACK_URL}) com status final '{final_status}'. Payload: {payload_json_str}")
633
+ transactions_db[rnn_tx_id]["callback_status"] = "sending"
634
+ try:
635
+ async with httpx.AsyncClient(timeout=30.0) as client: # Timeout global para o cliente
636
+ response = await client.post(AIBANK_CALLBACK_URL, content=payload_json_str, headers=headers)
637
+ response.raise_for_status()
638
+ logger.info(f"BG TASK [{rnn_tx_id}]: Callback para AIBank enviado com sucesso. Resposta: {response.status_code}")
639
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_success_{response.status_code}"
640
+ except httpx.RequestError as e_req:
641
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro de REDE ao enviar callback para AIBank: {e_req}")
642
+ transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_network_error"
643
+ except httpx.HTTPStatusError as e_http:
644
+ 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]}")
645
+ transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_http_error_{e_http.response.status_code}"
646
+ except Exception as e_cb_final:
647
+ logger.error(f"BG TASK [{rnn_tx_id}]: Erro INESPERADO ao enviar callback: {e_cb_final}", exc_info=True)
648
+ transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_unknown_error"
649
+
650
+
651
+ import asyncio
652
+ import random
653
+
654
+
655
+
656
+ # --- Endpoints da API ---
657
+ @app.post("/api/invest",
658
+ response_model=InvestmentResponse,
659
+ dependencies=[Depends(verify_aibank_key)])
660
+ async def initiate_investment(
661
+ request_data: InvestmentRequest,
662
+ background_tasks: BackgroundTasks
663
+ ):
664
+ """
665
+ Endpoint para o AIBank iniciar um ciclo de investimento.
666
+ Responde rapidamente e executa a lógica pesada em background.
667
+ """
668
+ logger.info(f"Requisição de investimento recebida para client_id: {request_data.client_id}, "
669
+ f"amount: {request_data.amount}, aibank_tx_token: {request_data.aibank_transaction_token}")
670
+
671
+ rnn_tx_id = str(uuid.uuid4())
672
+
673
+ # Armazena informações iniciais da transação DB real para ser mais robusto
674
+ transactions_db[rnn_tx_id] = {
675
+ "rnn_transaction_id": rnn_tx_id,
676
+ "aibank_transaction_token": request_data.aibank_transaction_token,
677
+ "client_id": request_data.client_id,
678
+ "initial_amount": request_data.amount,
679
+ "status": "pending_background_processing",
680
+ "received_at": datetime.utcnow().isoformat(),
681
+ "callback_status": "not_sent_yet"
682
+ }
683
+
684
+ # Adiciona a tarefa de longa duração ao background
685
+ background_tasks.add_task(
686
+ execute_investment_strategy_background,
687
+ rnn_tx_id,
688
+ request_data.client_id,
689
+ request_data.amount,
690
+ request_data.aibank_transaction_token
691
+ )
692
+
693
+ logger.info(f"Estratégia de investimento para rnn_tx_id: {rnn_tx_id} agendada para execução em background.")
694
+ return InvestmentResponse(
695
+ status="pending",
696
+ message="Investment request received and is being processed in the background. Await callback for results.",
697
+ rnn_transaction_id=rnn_tx_id
698
+ )
699
+
700
+ @app.get("/api/transaction_status/{rnn_tx_id}", response_class=JSONResponse)
701
+ async def get_transaction_status(rnn_tx_id: str):
702
+ """ Endpoint para verificar o status de uma transação (para debug/admin) """
703
+ transaction = transactions_db.get(rnn_tx_id)
704
+ if not transaction:
705
+ raise HTTPException(status_code=404, detail="Transaction not found")
706
+ return transaction
707
+
708
+
709
+ # --- Dashboard (Existente, adaptado) ---
710
+ # Setup para arquivos estáticos e templates
711
+
712
+ try:
713
+ app.mount("/static", StaticFiles(directory="rnn/static"), name="static")
714
+ templates = Environment(loader=FileSystemLoader("rnn/templates"))
715
+ except RuntimeError as e:
716
+ logger.warning(f"Não foi possível montar /static ou carregar templates: {e}. O dashboard pode não funcionar.")
717
+ templates = None # Para evitar erros se o loader falhar
718
+
719
+ @app.get("/", response_class=HTMLResponse)
720
+ async def index(request: Request):
721
+ if not templates:
722
+ return HTMLResponse("<html><body><h1>Dashboard indisponível</h1><p>Configuração de templates/estáticos falhou.</p></body></html>")
723
+
724
+ agora = datetime.now()
725
+ agentes_simulados = [
726
+ # dados de agentes ...
727
+ ]
728
+ template = templates.get_template("index.html")
729
+ # Adicionar transações recentes ao contexto do template
730
+ recent_txs = list(transactions_db.values())[-5:] # Últimas 5 transações
731
+ return HTMLResponse(template.render(request=request, agentes=agentes_simulados, transactions=recent_txs))
732
+
733
+ # --- Imports para Background Task ---
734
+ import asyncio
735
+ import random
736
+
737
+ # Função de logger dummy
738
+ # class DummyLogger:
739
+ # def info(self, msg, *args, **kwargs): print(f"INFO: {msg}")
740
+ # def warning(self, msg, *args, **kwargs): print(f"WARNING: {msg}")
741
+ # def error(self, msg, *args, **kwargs): print(f"ERROR: {msg}", kwargs.get('exc_info'))
742
+
743
+ # if __name__ == "__main__": # Para teste local
744
+ # # logger = DummyLogger() # se não tiver get_logger()
745
+ # # Configuração das variáveis de ambiente para teste local
746
+ # os.environ["AIBANK_API_KEY"] = "test_aibank_key_from_rnn_server"
747
+ # os.environ["AIBANK_CALLBACK_URL"] = "http://localhost:8001/api/rnn_investment_result_callback" # URL do aibank simulado
748
+ # os.environ["CALLBACK_SHARED_SECRET"] = "super_secret_for_callback_signing"
749
+ # # import uvicorn
750
+ # # uvicorn.run(app, host="0.0.0.0", port=8000)
app/config/config.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Parâmetros de Configuração ---
2
+ # Dados
3
+ SYMBOL = 'ETH/USDT' # MUDAMOS PARA ETH/USDT PARA TESTE
4
+ MULTI_ASSET_SYMBOLS = {
5
+ 'crypto_eth': 'ETH-USD', # yfinance ticker para ETH/USD
6
+ 'crypto_ada': 'ADA-USD', # yfinance ticker para ADA/USD
7
+ 'stock_aapl': 'AAPL', # NASDAQ
8
+ 'stock_petr': 'PETR4.SA' # B3
9
+ } # Use os tickers corretos para yfinance ou ccxt
10
+ TIMEFRAME_YFINANCE = '1h' # yfinance suporta '1m', '2m', '5m', '15m', '30m', '60m', '90m', '1h', '1d', '5d', '1wk', '1mo', '3mo'
11
+ # Para '1h', yfinance só retorna os últimos 730 dias. Para mais dados, use '1d'.
12
+ # Se usar ccxt, TIMEFRAME = '1h' como antes.
13
+ DAYS_TO_FETCH = 365 * 2 # 2 anos
14
+
15
+ # Lista das features base que você quer calcular para CADA ativo
16
+ # (as 19 que definimos antes)
17
+ INDIVIDUAL_ASSET_BASE_FEATURES = [
18
+ 'open', 'high', 'low', 'close', 'volume', # OHLCV originais são necessários para os cálculos
19
+ 'sma_10', 'rsi_14', 'macd', 'macds', 'atr', 'bbp', 'cci_37', 'mfi_37', 'adx_14',
20
+ 'volume_zscore', 'body_size', 'body_size_norm_atr', 'body_vs_avg_body',
21
+ 'log_return', 'buy_condition_v1', # 'sma_50' é calculada dentro de buy_condition_v1
22
+ # As colunas _div_atr serão criadas a partir destas
23
+ ]
24
+
25
+ # Features que serão normalizadas pelo ATR
26
+ COLS_TO_NORM_BY_ATR = ['open', 'high', 'low', 'close', 'volume', 'sma_10', 'macd', 'body_size']
27
+
28
+
29
+ TIMEFRAME = '1h'
30
+ DAYS_OF_DATA_TO_FETCH = 365 * 2
31
+ LIMIT_PER_FETCH = 1000
32
+
33
+ # Features e Janela
34
+ WINDOW_SIZE = 60
35
+
36
+ # BASE_FEATURE_COLS define as features *originais* ou *derivadas diretamente dos dados brutos*
37
+ # que serão usadas ANTES do escalonamento específico para o modelo no script de treino,
38
+ # e também as features que o rnn_predictor.py precisará calcular/ter ANTES de aplicar SEUS scalers.
39
+ BASE_FEATURE_COLS = [
40
+ 'open_div_atr', # Feature 1 (Preço normalizado)
41
+ 'high_div_atr', # Feature 2 (Preço normalizado)
42
+ 'low_div_atr', # Feature 3 (Preço normalizado)
43
+ 'close_div_atr', # Feature 4 (Preço normalizado)
44
+ 'volume_div_atr', # Feature 5 (Volume normalizado)
45
+ 'log_return', # Feature 6 (Momento)
46
+ 'rsi_14', # Feature 7 (Oscilador)
47
+ 'atr', # Feature 8 (Volatilidade - será escalada)
48
+ 'bbp', # Feature 9 (%B - será escalado)
49
+ 'cci_37', # Feature 10 (Oscilador - será escalado)
50
+ 'mfi_37', # Feature 11 (Volume/Oscilador - será escalado)
51
+ 'body_size_norm_atr', # Feature 12 (Candle normalizado)
52
+ 'body_vs_avg_body', # Feature 13 (Candle relativo)
53
+ 'macd', # Feature 14 (Linha MACD - será escalada)
54
+ 'sma_10_div_atr', # Feature 15 (Preço normalizado)
55
+ 'adx_14', # Feature 16 (Força da Tendência - será escalada)
56
+ 'volume_zscore', # Feature 17 (Volume relativo - será escalado)
57
+ 'buy_condition_v1', # Feature 18 (Condição Composta - binária, pode ou não ser escalada)
58
+ # 'cond_compra_v1', # Parece ser um duplicado de 'buy_condition_v1', remova se for.
59
+ # Se for diferente, mantenha, mas garanta que está sendo calculada.
60
+ ]
61
+ # REMOVA as colunas _scaled de BASE_FEATURE_COLS. Elas são o *resultado* do escalonamento.
62
+ # BASE_FEATURE_COLS são as features ANTES do último passo de escalonamento para o modelo.
63
+
64
+ # Vamos assumir que cond_compra_v1 e buy_condition_v1 são a mesma.
65
+ # Se forem diferentes, você precisará ajustar.
66
+ if 'cond_compra_v1' in BASE_FEATURE_COLS and 'buy_condition_v1' in BASE_FEATURE_COLS:
67
+ if 'cond_compra_v1' == 'buy_condition_v1': # Redundante se nomes iguais, mas para clareza
68
+ print("AVISO em config.py: 'cond_compra_v1' e 'buy_condition_v1' parecem ser a mesma feature. Verifique.")
69
+ # Decida qual manter ou se são realmente diferentes. Por ora, vou assumir que você quer ambas se estiverem listadas.
70
+ # Se forem a mesma, remova uma. Vou remover 'cond_compra_v1' se 'buy_condition_v1' for a oficial.
71
+ # BASE_FEATURE_COLS.remove('cond_compra_v1') # Exemplo
72
+
73
+
74
+ NUM_FEATURES = len(BASE_FEATURE_COLS) # ATUALIZADO AUTOMATICAMENTE
75
+
76
+ # Alvo da Predição (Target)
77
+ PREDICTION_HORIZON = 5
78
+ PRICE_CHANGE_THRESHOLD = 0.0075 # Você aumentou, OK.
79
+
80
+ # Modelo RNN
81
+ LSTM_UNITS = [64, 64] # Boa escolha para mais features
82
+ DENSE_UNITS = 32
83
+ DROPOUT_RATE = 0.3 # Bom para regularizar um modelo maior
84
+ LEARNING_RATE = 0.0005 # LR inicial para ReduceLROnPlateau
85
+ L2_REG = 0.0001 # Regularização L2 leve
86
+
87
+ # Treinamento
88
+ BATCH_SIZE = 128
89
+ EPOCHS = 100 # Deixe EarlyStopping controlar
90
+
91
+ # Caminhos para Salvar
92
+ MODEL_SAVE_DIR = "app/model"
93
+ MODEL_NAME = "model.h5"
94
+ # Nomes de scaler mais descritivos que você sugeriu:
95
+ PRICE_VOL_SCALER_NAME = "price_volume_atr_norm_scaler.joblib"
96
+ INDICATOR_SCALER_NAME = "other_indicators_scaler.joblib"
97
+
98
+ # EXPECTED_SCALED_FEATURES_FOR_MODEL define os nomes das colunas APÓS o escalonamento
99
+ # que são usadas para criar as sequências e alimentar o modelo no script de treino.
100
+ # E também o que o rnn_predictor.py DEVE produzir após aplicar seus scalers carregados.
101
+ EXPECTED_SCALED_FEATURES_FOR_MODEL = [f"{col}_scaled" for col in BASE_FEATURE_COLS]
102
+ # ^^^ IMPORTANTE: Esta linha assume que TODAS as features em BASE_FEATURE_COLS
103
+ # serão escaladas e terão o sufixo _scaled.
104
+ # Se 'rsi_14', 'bbp', 'buy_condition_v1' não forem escaladas ou tiverem
105
+ # outro tratamento, esta lista precisa ser ajustada manualmente.
106
+ # Exemplo: Se 'buy_condition_v1' é binária e não escalada:
107
+ # EXPECTED_SCALED_FEATURES_FOR_MODEL = [f"{col}_scaled" for col in BASE_FEATURE_COLS if col != 'buy_condition_v1'] + ['buy_condition_v1']
108
+ # Por simplicidade, vamos assumir que todas são escaladas por enquanto.
109
+ EXPECTED_FEATURES_ORDER = EXPECTED_SCALED_FEATURES_FOR_MODEL
110
+
111
+ INDIVIDUAL_ASSET_BASE_FEATURES = EXPECTED_FEATURES_ORDER
112
+
113
+ MODEL_SAVE_DIR = "app/model"
114
+ PRICE_VOL_SCALER_NAME = "price_volume_atr_norm_scaler.joblib"
115
+ INDICATOR_SCALER_NAME = "other_indicators_scaler.joblib"
app/dokerfile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+ COPY . .
5
+
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ EXPOSE 8000
9
+
10
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]