Upload 151 files
Browse filesCommit others PPO
This view is limited to 50 files because it contains too many changes.
See raw diff
- agents/AutomatedLendingPool.sol +35 -0
- agents/CreditOracle.sol +27 -0
- agents/FuzzedDataProvider.js +15 -0
- agents/ImeAtcoinGovernance.sol +59 -0
- agents/ImeAtcoinPayment.js +15 -0
- agents/MarketMaker.py +30 -0
- agents/PrivateTransactions.sol +23 -0
- agents/README.md +12 -0
- agents/RewardSystem.sol +22 -0
- agents/__pycache__/__init__.cpython-312.pyc +0 -0
- agents/__pycache__/app.cpython-312.pyc +0 -0
- agents/agents/DeepPortfolioAgent.py +361 -0
- agents/agents/__init__.py +0 -0
- agents/agents/__pycache__/DeepPortfolioAgent.cpython-312.pyc +0 -0
- agents/agents/__pycache__/DeepPortfolioAgentNetwork.cpython-312.pyc +0 -0
- agents/agents/__pycache__/__init__.cpython-312.pyc +0 -0
- agents/agents/__pycache__/config.cpython-312.pyc +0 -0
- agents/agents/__pycache__/custom_policies.cpython-312.pyc +0 -0
- agents/agents/__pycache__/data_handler_multi_asset.cpython-312.pyc +0 -0
- agents/agents/__pycache__/dataset_update_agent.cpython-312.pyc +0 -0
- agents/agents/__pycache__/deep_portfolio.cpython-312.pyc +0 -0
- agents/agents/__pycache__/deep_portfolio_torch.cpython-312.pyc +0 -0
- agents/agents/__pycache__/portfolio_environment.cpython-312.pyc +0 -0
- agents/agents/__pycache__/portfolio_features_extractor_torch.cpython-312.pyc +0 -0
- agents/agents/__pycache__/train_rl_portfolio_agent.cpython-312.pyc +0 -0
- agents/agents/__pycache__/train_rl_portfolio_agent_from_app.cpython-312.pyc +0 -0
- agents/agents/app/model/model2.h5 +3 -0
- agents/agents/app/model/model3.zip +3 -0
- agents/agents/app/model/ppo_custom_deep_portfolio_agent.zip +3 -0
- agents/agents/config.md +13 -0
- agents/agents/config.py +126 -0
- agents/agents/custom_policies.py +99 -0
- agents/agents/data_handler_multi_asset.py +448 -0
- agents/agents/dataset_update_agent.py +9 -0
- agents/agents/deep_portfolio.py +104 -0
- agents/agents/deep_portfolio_torch.py +80 -0
- agents/agents/financial_data_agent.py +76 -0
- agents/agents/investment_agent.py +7 -0
- agents/agents/portfolio_environment.py +246 -0
- agents/agents/portfolio_features_extractor_torch.py +35 -0
- agents/agents/ppo_deep_portfolio_tensorboard/PPO_1/events.out.tfevents.1750287361.verticalagent-X555LPB.89910.0 +3 -0
- agents/agents/ppo_deep_portfolio_tensorboard/PPO_10/events.out.tfevents.1750497081.codespaces-72cb68.2589.0 +3 -0
- agents/agents/ppo_deep_portfolio_tensorboard/PPO_11/events.out.tfevents.1750534135.codespaces-72cb68.3018.0 +3 -0
- agents/agents/ppo_deep_portfolio_tensorboard/PPO_12/events.out.tfevents.1750560310.codespaces-72cb68.253920.0 +3 -0
- agents/agents/ppo_deep_portfolio_tensorboard/PPO_13/events.out.tfevents.1750568153.codespaces-72cb68.2534.0 +3 -0
- agents/agents/ppo_deep_portfolio_tensorboard/PPO_14/events.out.tfevents.1750587177.verticalagent-X555LPB.125274.0 +3 -0
- agents/agents/ppo_deep_portfolio_tensorboard/PPO_15/events.out.tfevents.1750636729.verticalagent-X555LPB.266088.0 +3 -0
- agents/agents/ppo_deep_portfolio_tensorboard/PPO_16/events.out.tfevents.1750638335.verticalagent-X555LPB.270772.0 +3 -0
- agents/agents/ppo_deep_portfolio_tensorboard/PPO_17/events.out.tfevents.1750638480.verticalagent-X555LPB.271132.0 +3 -0
- agents/agents/ppo_deep_portfolio_tensorboard/PPO_18/events.out.tfevents.1750639418.verticalagent-X555LPB.273960.0 +3 -0
agents/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 |
+
}
|
agents/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 |
+
}
|
agents/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 };
|
agents/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 |
+
}
|
agents/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;
|
agents/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()
|
agents/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 |
+
}
|
agents/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
agents/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 |
+
}
|
agents/__pycache__/__init__.cpython-312.pyc
CHANGED
|
Binary files a/agents/__pycache__/__init__.cpython-312.pyc and b/agents/__pycache__/__init__.cpython-312.pyc differ
|
|
|
agents/__pycache__/app.cpython-312.pyc
ADDED
|
Binary file (3.16 kB). View file
|
|
|
agents/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/agents/__init__.py
ADDED
|
File without changes
|
agents/agents/__pycache__/DeepPortfolioAgent.cpython-312.pyc
ADDED
|
Binary file (14.7 kB). View file
|
|
|
agents/agents/__pycache__/DeepPortfolioAgentNetwork.cpython-312.pyc
ADDED
|
Binary file (6.39 kB). View file
|
|
|
agents/agents/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (156 Bytes). View file
|
|
|
agents/agents/__pycache__/config.cpython-312.pyc
ADDED
|
Binary file (2.71 kB). View file
|
|
|
agents/agents/__pycache__/custom_policies.cpython-312.pyc
ADDED
|
Binary file (5.13 kB). View file
|
|
|
agents/agents/__pycache__/data_handler_multi_asset.cpython-312.pyc
ADDED
|
Binary file (14.4 kB). View file
|
|
|
agents/agents/__pycache__/dataset_update_agent.cpython-312.pyc
ADDED
|
Binary file (850 Bytes). View file
|
|
|
agents/agents/__pycache__/deep_portfolio.cpython-312.pyc
ADDED
|
Binary file (5.93 kB). View file
|
|
|
agents/agents/__pycache__/deep_portfolio_torch.cpython-312.pyc
ADDED
|
Binary file (4.86 kB). View file
|
|
|
agents/agents/__pycache__/portfolio_environment.cpython-312.pyc
ADDED
|
Binary file (10.7 kB). View file
|
|
|
agents/agents/__pycache__/portfolio_features_extractor_torch.cpython-312.pyc
ADDED
|
Binary file (1.68 kB). View file
|
|
|
agents/agents/__pycache__/train_rl_portfolio_agent.cpython-312.pyc
ADDED
|
Binary file (16.2 kB). View file
|
|
|
agents/agents/__pycache__/train_rl_portfolio_agent_from_app.cpython-312.pyc
ADDED
|
Binary file (16.2 kB). View file
|
|
|
agents/agents/app/model/model2.h5
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a6e092222aca7f924ce78038117a42b2a69ad5dfd3727d0999e8e880e8a1648f
|
| 3 |
+
size 1178756
|
agents/agents/app/model/model3.zip
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a6e092222aca7f924ce78038117a42b2a69ad5dfd3727d0999e8e880e8a1648f
|
| 3 |
+
size 1178756
|
agents/agents/app/model/ppo_custom_deep_portfolio_agent.zip
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a6e092222aca7f924ce78038117a42b2a69ad5dfd3727d0999e8e880e8a1648f
|
| 3 |
+
size 1178756
|
agents/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/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.0
|
| 105 |
+
PPO_TOTAL_TIMESTEPS = 2048 #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/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/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/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/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/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/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/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/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=240, # 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/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/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/agents/ppo_deep_portfolio_tensorboard/PPO_10/events.out.tfevents.1750497081.codespaces-72cb68.2589.0
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6ea8284249717efcc6b46e7957eef6b8ceff0c6d92487095fdbe12f141125074
|
| 3 |
+
size 254591
|
agents/agents/ppo_deep_portfolio_tensorboard/PPO_11/events.out.tfevents.1750534135.codespaces-72cb68.3018.0
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8c4af51ef094dc62cefc525b73b30f2f35ea2a3572dc386c6653c97511d71d8f
|
| 3 |
+
size 255329
|
agents/agents/ppo_deep_portfolio_tensorboard/PPO_12/events.out.tfevents.1750560310.codespaces-72cb68.253920.0
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:37b09133c93eec65e524f5a76873048d399c6dab52f5516f1a01e4c6df736f2a
|
| 3 |
+
size 14741
|
agents/agents/ppo_deep_portfolio_tensorboard/PPO_13/events.out.tfevents.1750568153.codespaces-72cb68.2534.0
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:790362af681bc65a145098768cb0d33b0537524b368420f92f1902de8c559e1b
|
| 3 |
+
size 177101
|
agents/agents/ppo_deep_portfolio_tensorboard/PPO_14/events.out.tfevents.1750587177.verticalagent-X555LPB.125274.0
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5a125c886db6fb22c394b8fc76d1ac2c99ed0fe68ba5713a69e4fea0551c5c35
|
| 3 |
+
size 36143
|
agents/agents/ppo_deep_portfolio_tensorboard/PPO_15/events.out.tfevents.1750636729.verticalagent-X555LPB.266088.0
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ed84c8ebfc3274f6f2d0dff3f5f7278f4aca81954e7f45121a0180504654e164
|
| 3 |
+
size 135
|
agents/agents/ppo_deep_portfolio_tensorboard/PPO_16/events.out.tfevents.1750638335.verticalagent-X555LPB.270772.0
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:68d42ca276a5b5bc587bd082ac51200daca54a62ba8a75af0bcd916712f4ebf4
|
| 3 |
+
size 88
|
agents/agents/ppo_deep_portfolio_tensorboard/PPO_17/events.out.tfevents.1750638480.verticalagent-X555LPB.271132.0
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e42d9077440ade5a761b7142c9645030018a476581f6e1701f809afe490c58ac
|
| 3 |
+
size 135
|
agents/agents/ppo_deep_portfolio_tensorboard/PPO_18/events.out.tfevents.1750639418.verticalagent-X555LPB.273960.0
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:811fff7b41423665bb07a8490f6d6c8eb4cba229a7f40de47eb2e51fba4e35a3
|
| 3 |
+
size 1353
|