Amós e Souza Fernandes
commited on
Upload 120 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- AutomatedLendingPool.sol +35 -0
- CreditOracle.sol +27 -0
- FuzzedDataProvider.js +15 -0
- ImeAtcoinGovernance.sol +59 -0
- ImeAtcoinPayment.js +15 -0
- MarketMaker.py +30 -0
- PrivateTransactions.sol +23 -0
- README.md +10 -1
- RewardSystem.sol +22 -0
- __init__.py +0 -0
- __pycache__/__init__.cpython-312.pyc +0 -0
- __pycache__/app.cpython-312.pyc +0 -0
- agents/DeepPortfolioAgent.py +361 -0
- agents/__init__.py +0 -0
- agents/__pycache__/DeepPortfolioAgent.cpython-312.pyc +0 -0
- agents/__pycache__/DeepPortfolioAgentNetwork.cpython-312.pyc +0 -0
- agents/__pycache__/__init__.cpython-312.pyc +0 -0
- agents/__pycache__/config.cpython-312.pyc +0 -0
- agents/__pycache__/custom_policies.cpython-312.pyc +0 -0
- agents/__pycache__/data_handler_multi_asset.cpython-312.pyc +0 -0
- agents/__pycache__/dataset_update_agent.cpython-312.pyc +0 -0
- agents/__pycache__/deep_portfolio.cpython-312.pyc +0 -0
- agents/__pycache__/deep_portfolio_torch.cpython-312.pyc +0 -0
- agents/__pycache__/portfolio_environment.cpython-312.pyc +0 -0
- agents/__pycache__/portfolio_features_extractor_torch.cpython-312.pyc +0 -0
- agents/__pycache__/train_rl_portfolio_agent.cpython-312.pyc +0 -0
- agents/config.md +13 -0
- agents/config.py +126 -0
- agents/custom_policies.py +99 -0
- agents/data_handler_multi_asset.py +448 -0
- agents/dataset_update_agent.py +9 -0
- agents/deep_portfolio.py +104 -0
- agents/deep_portfolio_torch.py +80 -0
- agents/financial_data_agent.py +76 -0
- agents/investment_agent.py +7 -0
- agents/portfolio_environment.py +246 -0
- agents/portfolio_features_extractor_torch.py +35 -0
- agents/ppo_deep_portfolio_tensorboard/PPO_1/events.out.tfevents.1750287361.verticalagent-X555LPB.89910.0 +3 -0
- agents/ppo_deep_portfolio_tensorboard/PPO_2/events.out.tfevents.1750321811.verticalagent-X555LPB.160180.0 +3 -0
- agents/ppo_deep_portfolio_tensorboard/PPO_3/events.out.tfevents.1750336135.verticalagent-X555LPB.200649.0 +3 -0
- agents/rl_agent.py +9 -0
- agents/train_agent_tfagents.py +127 -0
- agents/train_rl_portfolio_agent.py +755 -0
- api/main.py +61 -0
- app-modularizado.py +606 -0
- app-old-no-module.py +901 -0
- app-old-v1.py +901 -0
- app.py +750 -0
- app/config/config.py +115 -0
- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"]
|