|
|
|
|
|
from agents.train_rl_portfolio_agent_from_app import model_ppo
|
|
|
model_ppo()
|
|
|
|
|
|
import os
|
|
|
import uuid
|
|
|
import time
|
|
|
import hmac
|
|
|
import hashlib
|
|
|
import json
|
|
|
from datetime import datetime, timedelta
|
|
|
from typing import Dict, Any, List, Optional
|
|
|
from rnn.app.utils.ccxt_utils import get_ccxt_exchange, fetch_crypto_data
|
|
|
|
|
|
import httpx
|
|
|
from fastapi import FastAPI, Request, HTTPException, Depends, Header, BackgroundTasks
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
from fastapi.responses import HTMLResponse, JSONResponse
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
from rnn.app.model.rnn_predictor import RNNModelPredictor
|
|
|
from rnn.app.utils.logger import get_logger
|
|
|
|
|
|
|
|
|
|
|
|
logger = get_logger()
|
|
|
|
|
|
|
|
|
AIBANK_API_KEY = os.environ.get("AIBANK_API_KEY")
|
|
|
AIBANK_CALLBACK_URL = os.environ.get("AIBANK_CALLBACK_URL")
|
|
|
CALLBACK_SHARED_SECRET = os.environ.get("CALLBACK_SHARED_SECRET")
|
|
|
|
|
|
|
|
|
MARKET_DATA_API_KEY = os.environ.get("MARKET_DATA_API_KEY")
|
|
|
EXCHANGE_API_KEY = os.environ.get("EXCHANGE_API_KEY")
|
|
|
EXCHANGE_API_SECRET = os.environ.get("EXCHANGE_API_SECRET")
|
|
|
|
|
|
if not AIBANK_API_KEY:
|
|
|
logger.warning("AIBANK_API_KEY não configurada. A autenticação para /api/invest falhou.")
|
|
|
if not AIBANK_CALLBACK_URL:
|
|
|
logger.warning("AIBANK_CALLBACK_URL não configurada. O callback para o aibank falhou.")
|
|
|
if not CALLBACK_SHARED_SECRET:
|
|
|
logger.warning("CALLBACK_SHARED_SECRET não configurado. A segurança do callback está comprometida.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app = FastAPI(title="ATCoin Neural Agents - Investment API")
|
|
|
|
|
|
|
|
|
app.add_middleware(
|
|
|
CORSMiddleware,
|
|
|
allow_origins=[
|
|
|
"http://localhost:3000",
|
|
|
"http://aibank.app.br",
|
|
|
"https://*.aibank.app.br",
|
|
|
"https://*.hf.space"
|
|
|
],
|
|
|
allow_credentials=True,
|
|
|
allow_methods=["*"],
|
|
|
allow_headers=["*"],
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
transactions_db: Dict[str, Dict[str, Any]] = {}
|
|
|
|
|
|
|
|
|
class InvestmentRequest(BaseModel):
|
|
|
client_id: str
|
|
|
amount: float = Field(..., gt=0)
|
|
|
aibank_transaction_token: str
|
|
|
|
|
|
class InvestmentResponse(BaseModel):
|
|
|
status: str
|
|
|
message: str
|
|
|
rnn_transaction_id: str
|
|
|
|
|
|
class InvestmentResultPayload(BaseModel):
|
|
|
rnn_transaction_id: str
|
|
|
aibank_transaction_token: str
|
|
|
client_id: str
|
|
|
initial_amount: float
|
|
|
final_amount: float
|
|
|
profit_loss: float
|
|
|
status: str
|
|
|
timestamp: datetime
|
|
|
details: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
async def verify_aibank_key(authorization: str = Header(None)):
|
|
|
if not AIBANK_API_KEY:
|
|
|
logger.error("CRITICAL: AIBANK_API_KEY (server-side) não está configurada nos Secrets.")
|
|
|
raise HTTPException(status_code=500, detail="Internal Server Configuration Error: Missing server API Key.")
|
|
|
|
|
|
if authorization is None:
|
|
|
logger.warning("Authorization header ausente na chamada do AIBank.")
|
|
|
raise HTTPException(status_code=401, detail="Authorization header is missing")
|
|
|
|
|
|
parts = authorization.split()
|
|
|
if len(parts) != 2 or parts[0].lower() != 'bearer':
|
|
|
logger.warning(f"Formato inválido do Authorization header: {authorization}")
|
|
|
raise HTTPException(status_code=401, detail="Authorization header must be 'Bearer <token>'")
|
|
|
|
|
|
token_from_aibank = parts[1]
|
|
|
if not hmac.compare_digest(token_from_aibank, AIBANK_API_KEY):
|
|
|
logger.warning(f"Chave de API inválida fornecida pelo AIBank. Token: {token_from_aibank[:10]}...")
|
|
|
raise HTTPException(status_code=403, detail="Invalid API Key provided by AIBank.")
|
|
|
logger.info("API Key do AIBank verificada com sucesso.")
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def execute_investment_strategy_background(
|
|
|
rnn_tx_id: str,
|
|
|
client_id: str,
|
|
|
amount: float,
|
|
|
aibank_tx_token: str
|
|
|
):
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Iniciando estratégia de investimento para cliente {client_id}, valor {amount}.")
|
|
|
transactions_db[rnn_tx_id]["status"] = "processing"
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Initializing investment cycle"
|
|
|
|
|
|
final_status = "completed"
|
|
|
error_details = ""
|
|
|
calculated_final_amount = amount
|
|
|
|
|
|
|
|
|
|
|
|
exchange = await get_ccxt_exchange(logger_instance=logger)
|
|
|
|
|
|
if not exchange:
|
|
|
|
|
|
logger.warning(f"BG TASK [{rnn_tx_id}]: Falha ao inicializar a exchange. A estratégia pode não funcionar como esperado para cripto.")
|
|
|
|
|
|
if os.environ.get("CCXT_API_KEY") and os.environ.get("CCXT_API_SECRET"):
|
|
|
error_details += "Failed to initialize CCXT exchange despite API keys being present; "
|
|
|
final_status = "failed_config"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Coletando dados de mercado...")
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Fetching market data"
|
|
|
market_data_results = {"crypto": {}, "stocks": {}, "other": {}}
|
|
|
critical_data_fetch_failed = False
|
|
|
|
|
|
|
|
|
if exchange:
|
|
|
crypto_pairs_to_fetch = ["BTC/USDT", "ETH/USDT", "SOL/USDT"]
|
|
|
|
|
|
crypto_data, crypto_fetch_ok, crypto_err_msg = await fetch_crypto_data(
|
|
|
exchange,
|
|
|
crypto_pairs_to_fetch,
|
|
|
logger_instance=logger
|
|
|
)
|
|
|
market_data_results["crypto"] = crypto_data
|
|
|
if not crypto_fetch_ok:
|
|
|
error_details += f"Crypto data fetch issues: {crypto_err_msg}; "
|
|
|
|
|
|
|
|
|
if os.environ.get("CCXT_API_KEY"):
|
|
|
critical_data_fetch_failed = True
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Falha crítica na coleta de dados de cripto.")
|
|
|
else:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Instância da exchange ccxt não disponível. Pulando coleta de dados de cripto.")
|
|
|
if os.environ.get("CCXT_API_KEY"):
|
|
|
error_details += "CCXT exchange not initialized, crypto data skipped; "
|
|
|
critical_data_fetch_failed = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
market_data_results["other"]['simulated_index_level'] = random.uniform(10000, 15000)
|
|
|
|
|
|
transactions_db[rnn_tx_id]["market_data_collected"] = market_data_results
|
|
|
|
|
|
|
|
|
if critical_data_fetch_failed:
|
|
|
final_status = "failed_market_data"
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado falhou criticamente. {error_details}")
|
|
|
|
|
|
|
|
|
else:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado concluída.")
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Processing RNN analysis"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
investment_decisions: List[Dict[str, Any]] = []
|
|
|
total_usd_allocated_by_rnn = 0.0
|
|
|
loop = asyncio.get_running_loop()
|
|
|
|
|
|
if final_status == "completed":
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Executando análise RNN...")
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Running RNN model"
|
|
|
rnn_analysis_success = True
|
|
|
|
|
|
|
|
|
predictor: Optional[RNNModelPredictor] = getattr(app.state, 'rnn_predictor', None)
|
|
|
|
|
|
try:
|
|
|
crypto_data_for_rnn = market_data_results.get("crypto", {})
|
|
|
candidate_assets = [
|
|
|
asset_key for asset_key, data in crypto_data_for_rnn.items()
|
|
|
if data and not data.get("error") and data.get("ohlcv_1h")
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE = 0.75
|
|
|
|
|
|
|
|
|
MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL = 0.15
|
|
|
MIN_ALLOCATION_PER_ASSET_PCT_OF_TOTAL = 0.02
|
|
|
|
|
|
MIN_USD_PER_ORDER = 25.00
|
|
|
MAX_CONCURRENT_POSITIONS = 4
|
|
|
|
|
|
|
|
|
CONFIDENCE_STRONG_BUY = 0.80
|
|
|
CONFIDENCE_MODERATE_BUY = 0.65
|
|
|
CONFIDENCE_WEAK_BUY = 0.55
|
|
|
|
|
|
allocated_capital_this_cycle = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for asset_key in candidate_assets:
|
|
|
if len(investment_decisions) >= MAX_CONCURRENT_POSITIONS:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Limite de {MAX_CONCURRENT_POSITIONS} posições concorrentes atingido.")
|
|
|
break
|
|
|
|
|
|
|
|
|
if allocated_capital_this_cycle >= amount * MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Limite de capital para o ciclo ({MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE*100}%) atingido.")
|
|
|
break
|
|
|
|
|
|
asset_symbol = asset_key.replace("_", "/")
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: RNN avaliando ativo: {asset_symbol}")
|
|
|
|
|
|
signal, confidence_prob = await predictor.predict_for_asset(
|
|
|
crypto_data_for_rnn[asset_key],
|
|
|
loop=loop
|
|
|
)
|
|
|
|
|
|
if signal == 1 and confidence_prob is not None:
|
|
|
target_usd_allocation = 0.0
|
|
|
|
|
|
if confidence_prob >= CONFIDENCE_STRONG_BUY:
|
|
|
|
|
|
|
|
|
alloc_factor = 0.6 + 0.4 * ((confidence_prob - CONFIDENCE_STRONG_BUY) / (1.0 - CONFIDENCE_STRONG_BUY + 1e-6))
|
|
|
target_usd_allocation = (amount * MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL) * alloc_factor
|
|
|
reason = f"RNN STRONG BUY signal (Conf: {confidence_prob:.3f})"
|
|
|
elif confidence_prob >= CONFIDENCE_MODERATE_BUY:
|
|
|
|
|
|
|
|
|
alloc_factor = 0.3 + 0.3 * ((confidence_prob - CONFIDENCE_MODERATE_BUY) / (CONFIDENCE_STRONG_BUY - CONFIDENCE_MODERATE_BUY + 1e-6))
|
|
|
target_usd_allocation = (amount * MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL) * alloc_factor
|
|
|
reason = f"RNN MODERATE BUY signal (Conf: {confidence_prob:.3f})"
|
|
|
elif confidence_prob >= CONFIDENCE_WEAK_BUY:
|
|
|
|
|
|
alloc_factor = 0.1 + 0.2 * ((confidence_prob - CONFIDENCE_WEAK_BUY) / (CONFIDENCE_MODERATE_BUY - CONFIDENCE_WEAK_BUY + 1e-6))
|
|
|
target_usd_allocation = (amount * MAX_ALLOCATION_PER_ASSET_PCT_OF_TOTAL) * alloc_factor
|
|
|
reason = f"RNN WEAK BUY signal (Conf: {confidence_prob:.3f})"
|
|
|
else:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Sinal COMPRA para {asset_symbol} mas confiança ({confidence_prob:.3f}) abaixo do limiar WEAK_BUY ({CONFIDENCE_WEAK_BUY}). Pulando.")
|
|
|
continue
|
|
|
|
|
|
|
|
|
target_usd_allocation = max(target_usd_allocation, amount * MIN_ALLOCATION_PER_ASSET_PCT_OF_TOTAL)
|
|
|
|
|
|
|
|
|
capital_left_for_this_cycle = (amount * MAX_CAPITAL_DEPLOYMENT_PCT_THIS_CYCLE) - allocated_capital_this_cycle
|
|
|
actual_usd_allocation = min(target_usd_allocation, capital_left_for_this_cycle)
|
|
|
|
|
|
|
|
|
if actual_usd_allocation < MIN_USD_PER_ORDER:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Alocação final ({actual_usd_allocation:.2f}) para {asset_symbol} abaixo do mínimo de ordem ({MIN_USD_PER_ORDER}). Pulando.")
|
|
|
continue
|
|
|
|
|
|
|
|
|
investment_decisions.append({
|
|
|
"asset_id": asset_symbol, "type": "CRYPTO", "action": "BUY",
|
|
|
"target_usd_amount": round(actual_usd_allocation, 2),
|
|
|
"rnn_confidence": round(confidence_prob, 4),
|
|
|
"reasoning": reason
|
|
|
})
|
|
|
allocated_capital_this_cycle += round(actual_usd_allocation, 2)
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Decisão: COMPRAR {actual_usd_allocation:.2f} USD de {asset_symbol}. {reason}")
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro CRÍTICO durante análise/predição RNN: {str(e)}", exc_info=True)
|
|
|
rnn_analysis_success = False
|
|
|
error_details += f"Critical RNN analysis/prediction error: {str(e)}; "
|
|
|
|
|
|
|
|
|
total_usd_allocated_by_rnn = allocated_capital_this_cycle
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not predictor or not predictor.model:
|
|
|
logger.warning(f"BG TASK [{rnn_tx_id}]: Instância do preditor RNN não disponível ou modelo interno não carregado. Pulando análise RNN.")
|
|
|
rnn_analysis_success = False
|
|
|
error_details += "RNN model/predictor not available for prediction; "
|
|
|
else:
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
|
|
crypto_data_for_rnn = market_data_results.get("crypto", {})
|
|
|
candidate_assets = [
|
|
|
asset_key for asset_key, data in crypto_data_for_rnn.items()
|
|
|
if data and not data.get("error") and data.get("ohlcv_1h")
|
|
|
]
|
|
|
|
|
|
MAX_RISK_PER_ASSET_PCT = 0.05
|
|
|
MIN_USD_PER_ORDER = 20.00
|
|
|
MAX_CONCURRENT_POSITIONS = 5
|
|
|
CONFIDENCE_THRESHOLD_FOR_MAX_ALLOC = 0.85
|
|
|
CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC = 0.60
|
|
|
BASE_ALLOCATION_PCT_OF_TOTAL_CAPITAL = 0.10
|
|
|
|
|
|
allocated_capital_this_cycle = 0.0
|
|
|
|
|
|
for asset_key in candidate_assets:
|
|
|
if len(investment_decisions) >= MAX_CONCURRENT_POSITIONS:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Limite de posições concorrentes ({MAX_CONCURRENT_POSITIONS}) atingido.")
|
|
|
break
|
|
|
if allocated_capital_this_cycle >= amount * 0.90:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Limite de capital do ciclo atingido.")
|
|
|
break
|
|
|
|
|
|
asset_symbol = asset_key.replace("_", "/")
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: RNN avaliando ativo: {asset_symbol}")
|
|
|
|
|
|
signal, confidence_prob = await predictor.predict_for_asset(
|
|
|
crypto_data_for_rnn[asset_key],
|
|
|
loop=loop
|
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
if signal == 1:
|
|
|
if confidence_prob is None or confidence_prob < CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Sinal COMPRA para {asset_symbol} mas confiança ({confidence_prob}) abaixo do mínimo {CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC}. Pulando.")
|
|
|
continue
|
|
|
|
|
|
confidence_factor = 0.5
|
|
|
if confidence_prob >= CONFIDENCE_THRESHOLD_FOR_MAX_ALLOC:
|
|
|
confidence_factor = 1.0
|
|
|
elif confidence_prob > CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC:
|
|
|
confidence_factor = 0.5 + 0.5 * (
|
|
|
(confidence_prob - CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC) /
|
|
|
(CONFIDENCE_THRESHOLD_FOR_MAX_ALLOC - CONFIDENCE_THRESHOLD_FOR_MIN_ALLOC)
|
|
|
)
|
|
|
|
|
|
potential_usd_allocation = amount * BASE_ALLOCATION_PCT_OF_TOTAL_CAPITAL * confidence_factor
|
|
|
potential_usd_allocation = min(potential_usd_allocation, amount * MAX_RISK_PER_ASSET_PCT)
|
|
|
remaining_capital_for_cycle = amount - allocated_capital_this_cycle
|
|
|
actual_usd_allocation = min(potential_usd_allocation, remaining_capital_for_cycle)
|
|
|
|
|
|
if actual_usd_allocation < MIN_USD_PER_ORDER:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Alocação calculada ({actual_usd_allocation:.2f}) para {asset_symbol} abaixo do mínimo ({MIN_USD_PER_ORDER}). Pulando.")
|
|
|
continue
|
|
|
|
|
|
investment_decisions.append({
|
|
|
"asset_id": asset_symbol, "type": "CRYPTO", "action": "BUY",
|
|
|
"target_usd_amount": round(actual_usd_allocation, 2),
|
|
|
"rnn_confidence": round(confidence_prob, 4) if confidence_prob is not None else None,
|
|
|
"reasoning": f"RNN signal BUY for {asset_symbol} with confidence {confidence_prob:.2f}"
|
|
|
})
|
|
|
allocated_capital_this_cycle += round(actual_usd_allocation, 2)
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Decisão: COMPRAR {actual_usd_allocation:.2f} USD de {asset_symbol} (Conf: {confidence_prob:.2f})")
|
|
|
|
|
|
elif signal == 0:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: RNN sinal NÃO COMPRAR para {asset_symbol} (Conf: {confidence_prob:.2f if confidence_prob is not None else 'N/A'})")
|
|
|
else:
|
|
|
logger.warning(f"BG TASK [{rnn_tx_id}]: RNN não gerou sinal para {asset_symbol}.")
|
|
|
|
|
|
if not investment_decisions:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: RNN não gerou decisões de COMPRA válidas após avaliação e alocação.")
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro CRÍTICO durante análise/predição RNN: {str(e)}", exc_info=True)
|
|
|
rnn_analysis_success = False
|
|
|
error_details += f"Critical RNN analysis/prediction error: {str(e)}; "
|
|
|
|
|
|
if not rnn_analysis_success:
|
|
|
final_status = "failed_rnn_analysis"
|
|
|
|
|
|
transactions_db[rnn_tx_id]["rnn_decisions"] = investment_decisions
|
|
|
|
|
|
total_usd_allocated_by_rnn = allocated_capital_this_cycle
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Preparing to execute orders"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
executed_trades_info: List[Dict[str, Any]] = []
|
|
|
current_portfolio_value = 0.0
|
|
|
cash_remaining_after_execution = amount
|
|
|
|
|
|
if final_status == "completed" and investment_decisions and exchange:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Executando {len(investment_decisions)} ordens...")
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Executing investment orders"
|
|
|
order_execution_overall_success = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for decision in investment_decisions:
|
|
|
if decision.get("action") == "BUY" and decision.get("type") == "CRYPTO":
|
|
|
asset_symbol = decision["asset_id"]
|
|
|
usd_to_spend = decision["target_usd_amount"]
|
|
|
|
|
|
|
|
|
if random.random() < 0.05:
|
|
|
logger.warning(f"BG TASK [{rnn_tx_id}]: Falha simulada ao executar ordem para {asset_symbol}.")
|
|
|
executed_trades_info.append({
|
|
|
"asset_id": asset_symbol, "status": "failed_simulated",
|
|
|
"requested_usd_amount": usd_to_spend, "error": "Simulated exchange rejection"
|
|
|
})
|
|
|
order_execution_overall_success = False
|
|
|
continue
|
|
|
|
|
|
|
|
|
simulated_cost = usd_to_spend * random.uniform(0.995, 1.005)
|
|
|
|
|
|
|
|
|
if simulated_cost > cash_remaining_after_execution:
|
|
|
simulated_cost = cash_remaining_after_execution
|
|
|
if simulated_cost < 1:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Saldo insuficiente ({cash_remaining_after_execution:.2f}) para ordem de {asset_symbol}, pulando.")
|
|
|
continue
|
|
|
|
|
|
|
|
|
if simulated_cost > 0:
|
|
|
current_portfolio_value += simulated_cost
|
|
|
cash_remaining_after_execution -= simulated_cost
|
|
|
executed_trades_info.append({
|
|
|
"asset_id": asset_symbol, "order_id_exchange": f"sim_ord_{uuid.uuid4()}",
|
|
|
"type": "market", "side": "buy",
|
|
|
"requested_usd_amount": usd_to_spend,
|
|
|
"status_from_exchange": "filled", "cost_in_usd": round(simulated_cost, 2),
|
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
|
})
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Ordem simulada para {asset_symbol} (custo: {simulated_cost:.2f} USD) preenchida.")
|
|
|
|
|
|
await asyncio.sleep(random.uniform(1, 2) * len(investment_decisions) if investment_decisions else 1)
|
|
|
|
|
|
if not order_execution_overall_success:
|
|
|
error_details += "One or more orders failed during execution; "
|
|
|
|
|
|
|
|
|
|
|
|
elif not exchange and investment_decisions:
|
|
|
logger.warning(f"BG TASK [{rnn_tx_id}]: Decisões de investimento geradas, mas a exchange não está disponível para execução.")
|
|
|
error_details += "Exchange not available for order execution; "
|
|
|
final_status = "failed_order_execution"
|
|
|
cash_remaining_after_execution = amount
|
|
|
|
|
|
transactions_db[rnn_tx_id]["executed_trades"] = executed_trades_info
|
|
|
transactions_db[rnn_tx_id]["cash_after_execution"] = round(cash_remaining_after_execution, 2)
|
|
|
transactions_db[rnn_tx_id]["portfolio_value_after_execution"] = round(current_portfolio_value, 2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
value_of_investments_at_eod = current_portfolio_value
|
|
|
|
|
|
if final_status == "completed":
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Simulating EOD valuation"
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Simulando valorização do portfólio no final do dia...")
|
|
|
await asyncio.sleep(random.uniform(3, 7))
|
|
|
|
|
|
if current_portfolio_value > 0:
|
|
|
|
|
|
|
|
|
daily_return_factor = 0.042
|
|
|
simulated_performance_factor = random.uniform(0.7, 1.3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
actual_daily_return_on_portfolio = random.uniform(-0.03, 0.05)
|
|
|
|
|
|
profit_or_loss_on_portfolio = current_portfolio_value * actual_daily_return_on_portfolio
|
|
|
value_of_investments_at_eod = current_portfolio_value + profit_or_loss_on_portfolio
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Portfólio inicial: {current_portfolio_value:.2f}, Retorno simulado: {actual_daily_return_on_portfolio*100:.2f}%, "
|
|
|
f"Lucro/Prejuízo no portfólio: {profit_or_loss_on_portfolio:.2f}, Valor EOD do portfólio: {value_of_investments_at_eod:.2f}")
|
|
|
else:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Nenhum portfólio para valorizar no EOD (nada foi comprado).")
|
|
|
value_of_investments_at_eod = 0.0
|
|
|
|
|
|
|
|
|
calculated_final_amount = value_of_investments_at_eod + cash_remaining_after_execution
|
|
|
|
|
|
else:
|
|
|
calculated_final_amount = cash_remaining_after_execution + current_portfolio_value
|
|
|
logger.warning(f"BG TASK [{rnn_tx_id}]: Ciclo de investimento não concluído normalmente ({final_status}). Valor final baseado no estado atual.")
|
|
|
|
|
|
transactions_db[rnn_tx_id]["eod_portfolio_value_simulated"] = round(value_of_investments_at_eod, 2)
|
|
|
transactions_db[rnn_tx_id]["final_calculated_amount"] = round(calculated_final_amount, 2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if final_status not in ["failed_config", "failed_market_data", "failed_rnn_analysis"]:
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Finalizing transaction log (tokenization)"
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Registrando (tokenizando) operação detalhadamente...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
transaction_data_for_hash = {
|
|
|
"rnn_tx_id": rnn_tx_id, "client_id": client_id, "initial_amount": amount,
|
|
|
"final_amount_calculated": calculated_final_amount,
|
|
|
|
|
|
"market_data_summary_keys": list(transactions_db[rnn_tx_id].get("market_data_collected", {}).keys()),
|
|
|
"rnn_decisions_count": len(transactions_db[rnn_tx_id].get("rnn_decisions", [])),
|
|
|
"executed_trades_count": len(transactions_db[rnn_tx_id].get("executed_trades", [])),
|
|
|
"eod_portfolio_value": transactions_db[rnn_tx_id].get("eod_portfolio_value_simulated"),
|
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
|
}
|
|
|
ordered_tx_data_str = json.dumps(transaction_data_for_hash, sort_keys=True)
|
|
|
proof_token_hash = hashlib.sha256(ordered_tx_data_str.encode('utf-8')).hexdigest()
|
|
|
|
|
|
transactions_db[rnn_tx_id]["proof_of_operation_token"] = proof_token_hash
|
|
|
transactions_db[rnn_tx_id]["tokenization_method"] = "internal_summary_hash_proof"
|
|
|
await asyncio.sleep(0.5)
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Operação registrada. Prova (hash): {proof_token_hash[:10]}...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if exchange and hasattr(exchange, 'close'):
|
|
|
try:
|
|
|
await exchange.close()
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Conexão ccxt fechada.")
|
|
|
except Exception as e_close:
|
|
|
logger.warning(f"BG TASK [{rnn_tx_id}]: Erro ao fechar conexão ccxt: {str(e_close)}")
|
|
|
|
|
|
if not AIBANK_CALLBACK_URL or not CALLBACK_SHARED_SECRET:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Configuração de callback ausente. Não é possível notificar o AIBank.")
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = "config_missing_critical"
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if error_details and final_status == "completed":
|
|
|
final_status = "completed_with_warnings"
|
|
|
|
|
|
callback_payload_data = InvestmentResultPayload(
|
|
|
rnn_transaction_id=rnn_tx_id, aibank_transaction_token=aibank_tx_token, client_id=client_id,
|
|
|
initial_amount=amount, final_amount=round(calculated_final_amount, 2),
|
|
|
profit_loss=round(calculated_final_amount - amount, 2),
|
|
|
status=final_status, timestamp=datetime.utcnow(),
|
|
|
details=error_details if error_details else "Investment cycle processed."
|
|
|
)
|
|
|
payload_json_str = callback_payload_data.model_dump_json()
|
|
|
|
|
|
signature = hmac.new(CALLBACK_SHARED_SECRET.encode('utf-8'), payload_json_str.encode('utf-8'), hashlib.sha256).hexdigest()
|
|
|
headers = {'Content-Type': 'application/json', 'X-RNN-Signature': signature}
|
|
|
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Enviando callback para AIBank ({AIBANK_CALLBACK_URL}) com status final '{final_status}'. Payload: {payload_json_str}")
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = "sending"
|
|
|
try:
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
|
response = await client.post(AIBANK_CALLBACK_URL, content=payload_json_str, headers=headers)
|
|
|
response.raise_for_status()
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Callback para AIBank enviado com sucesso. Resposta: {response.status_code}")
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = f"sent_success_{response.status_code}"
|
|
|
except httpx.RequestError as e_req:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro de REDE ao enviar callback para AIBank: {e_req}")
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_network_error"
|
|
|
except httpx.HTTPStatusError as e_http:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro HTTP do AIBank ao receber callback: {e_http.response.status_code} - {e_http.response.text[:200]}")
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_http_error_{e_http.response.status_code}"
|
|
|
except Exception as e_cb_final:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro INESPERADO ao enviar callback: {e_cb_final}", exc_info=True)
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_unknown_error"
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
import random
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/api/invest",
|
|
|
response_model=InvestmentResponse,
|
|
|
dependencies=[Depends(verify_aibank_key)])
|
|
|
async def initiate_investment(
|
|
|
request_data: InvestmentRequest,
|
|
|
background_tasks: BackgroundTasks
|
|
|
):
|
|
|
"""
|
|
|
Endpoint para o AIBank iniciar um ciclo de investimento.
|
|
|
Responde rapidamente e executa a lógica pesada em background.
|
|
|
"""
|
|
|
logger.info(f"Requisição de investimento recebida para client_id: {request_data.client_id}, "
|
|
|
f"amount: {request_data.amount}, aibank_tx_token: {request_data.aibank_transaction_token}")
|
|
|
|
|
|
rnn_tx_id = str(uuid.uuid4())
|
|
|
|
|
|
|
|
|
transactions_db[rnn_tx_id] = {
|
|
|
"rnn_transaction_id": rnn_tx_id,
|
|
|
"aibank_transaction_token": request_data.aibank_transaction_token,
|
|
|
"client_id": request_data.client_id,
|
|
|
"initial_amount": request_data.amount,
|
|
|
"status": "pending_background_processing",
|
|
|
"received_at": datetime.utcnow().isoformat(),
|
|
|
"callback_status": "not_sent_yet"
|
|
|
}
|
|
|
|
|
|
|
|
|
background_tasks.add_task(
|
|
|
execute_investment_strategy_background,
|
|
|
rnn_tx_id,
|
|
|
request_data.client_id,
|
|
|
request_data.amount,
|
|
|
request_data.aibank_transaction_token
|
|
|
)
|
|
|
|
|
|
logger.info(f"Estratégia de investimento para rnn_tx_id: {rnn_tx_id} agendada para execução em background.")
|
|
|
return InvestmentResponse(
|
|
|
status="pending",
|
|
|
message="Investment request received and is being processed in the background. Await callback for results.",
|
|
|
rnn_transaction_id=rnn_tx_id
|
|
|
)
|
|
|
|
|
|
@app.get("/api/transaction_status/{rnn_tx_id}", response_class=JSONResponse)
|
|
|
async def get_transaction_status(rnn_tx_id: str):
|
|
|
""" Endpoint para verificar o status de uma transação (para debug/admin) """
|
|
|
transaction = transactions_db.get(rnn_tx_id)
|
|
|
if not transaction:
|
|
|
raise HTTPException(status_code=404, detail="Transaction not found")
|
|
|
return transaction
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
app.mount("/static", StaticFiles(directory="rnn/static"), name="static")
|
|
|
templates = Environment(loader=FileSystemLoader("rnn/templates"))
|
|
|
except RuntimeError as e:
|
|
|
logger.warning(f"Não foi possível montar /static ou carregar templates: {e}. O dashboard pode não funcionar.")
|
|
|
templates = None
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
|
async def index(request: Request):
|
|
|
if not templates:
|
|
|
return HTMLResponse("<html><body><h1>Dashboard indisponível</h1><p>Configuração de templates/estáticos falhou.</p></body></html>")
|
|
|
|
|
|
agora = datetime.now()
|
|
|
agentes_simulados = [
|
|
|
|
|
|
]
|
|
|
template = templates.get_template("index.html")
|
|
|
|
|
|
recent_txs = list(transactions_db.values())[-5:]
|
|
|
return HTMLResponse(template.render(request=request, agentes=agentes_simulados, transactions=recent_txs))
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
import random
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|