|
|
|
|
|
|
|
|
import os
|
|
|
import uuid
|
|
|
import time
|
|
|
import hmac
|
|
|
import hashlib
|
|
|
import json
|
|
|
from datetime import datetime, timedelta
|
|
|
from typing import Dict, Any
|
|
|
import ccxt.async_support as ccxt
|
|
|
|
|
|
import httpx
|
|
|
from fastapi import FastAPI, Request, HTTPException, Depends, Header, BackgroundTasks
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
from fastapi.responses import HTMLResponse, JSONResponse
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
from app. utils.logger import get_logger
|
|
|
|
|
|
logger = get_logger()
|
|
|
|
|
|
|
|
|
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.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CCXT_EXCHANGE_ID = os.environ.get("CCXT_EXCHANGE_ID", "binance")
|
|
|
CCXT_API_KEY = os.environ.get("CCXT_API_KEY")
|
|
|
CCXT_API_SECRET = os.environ.get("CCXT_API_SECRET")
|
|
|
CCXT_API_PASSWORD = os.environ.get("CCXT_API_PASSWORD")
|
|
|
|
|
|
|
|
|
CCXT_SANDBOX_MODE = os.environ.get("CCXT_SANDBOX_MODE", "false").lower() == "true"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
):
|
|
|
"""
|
|
|
Esta função roda em background. Simula a coleta de dados, RNN, execução e tokenização.
|
|
|
No final, chama o callback para o aibank.
|
|
|
"""
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Iniciando estratégia de investimento para cliente {client_id}, valor {amount}.")
|
|
|
transactions_db[rnn_tx_id]["status"] = "processing"
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Initializing investment cycle"
|
|
|
|
|
|
final_status = "completed"
|
|
|
error_details = ""
|
|
|
calculated_final_amount = amount
|
|
|
|
|
|
|
|
|
exchange = None
|
|
|
if CCXT_API_KEY and CCXT_API_SECRET:
|
|
|
try:
|
|
|
exchange_class = getattr(ccxt, CCXT_EXCHANGE_ID)
|
|
|
config = {
|
|
|
'apiKey': CCXT_API_KEY,
|
|
|
'secret': CCXT_API_SECRET,
|
|
|
'enableRateLimit': True,
|
|
|
|
|
|
}
|
|
|
if CCXT_API_PASSWORD:
|
|
|
config['password'] = CCXT_API_PASSWORD
|
|
|
|
|
|
exchange = exchange_class(config)
|
|
|
|
|
|
if CCXT_SANDBOX_MODE:
|
|
|
if hasattr(exchange, 'set_sandbox_mode'):
|
|
|
exchange.set_sandbox_mode(True)
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: CCXT configurado para modo SANDBOX para {CCXT_EXCHANGE_ID}.")
|
|
|
elif 'test' in exchange.urls:
|
|
|
exchange.urls['api'] = exchange.urls['test']
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: CCXT URLs alteradas para TESTNET para {CCXT_EXCHANGE_ID}.")
|
|
|
else:
|
|
|
logger.warning(f"BG TASK [{rnn_tx_id}]: Modo SANDBOX solicitado mas não explicitamente suportado por ccxt para {CCXT_EXCHANGE_ID} ou URL de teste não encontrada.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
except AttributeError:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Exchange ID '{CCXT_EXCHANGE_ID}' inválida ou não suportada pelo ccxt.")
|
|
|
error_details += f"Invalid CCXT_EXCHANGE_ID: {CCXT_EXCHANGE_ID}; "
|
|
|
final_status = "failed_config"
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao inicializar ccxt para {CCXT_EXCHANGE_ID}: {str(e)}", exc_info=True)
|
|
|
error_details += f"CCXT initialization error: {str(e)}; "
|
|
|
final_status = "failed_config"
|
|
|
|
|
|
else:
|
|
|
logger.warning(f"BG TASK [{rnn_tx_id}]: CCXT_API_KEY ou CCXT_API_SECRET não configurados. A coleta de dados de cripto e execução de ordens via ccxt serão puladas.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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": {}
|
|
|
}
|
|
|
market_data_fetch_success = True
|
|
|
|
|
|
|
|
|
|
|
|
crypto_pairs_to_fetch = ["BTC/USDT", "ETH/USDT", "SOL/USDT"]
|
|
|
|
|
|
if exchange:
|
|
|
try:
|
|
|
for pair in crypto_pairs_to_fetch:
|
|
|
pair_data = {}
|
|
|
if exchange.has['fetchTicker']:
|
|
|
ticker = await exchange.fetch_ticker(pair)
|
|
|
pair_data['ticker'] = {
|
|
|
'last': ticker.get('last'),
|
|
|
'bid': ticker.get('bid'),
|
|
|
'ask': ticker.get('ask'),
|
|
|
'volume': ticker.get('baseVolume'),
|
|
|
'timestamp': ticker.get('timestamp')
|
|
|
}
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Ticker {pair}: Preço {ticker.get('last')}")
|
|
|
|
|
|
if exchange.has['fetchOHLCV']:
|
|
|
|
|
|
|
|
|
ohlcv = await exchange.fetch_ohlcv(pair, timeframe='1h', limit=72)
|
|
|
|
|
|
pair_data['ohlcv_1h'] = ohlcv
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Coletado {len(ohlcv)} candles OHLCV para {pair} (1h).")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
market_data_results["crypto"][pair.replace("/", "_")] = pair_data
|
|
|
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Dados de cripto via ccxt coletados.")
|
|
|
|
|
|
except ccxt.NetworkError as e:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro de rede ccxt ao coletar dados de mercado: {str(e)}", exc_info=True)
|
|
|
market_data_fetch_success = False
|
|
|
error_details += f"CCXT NetworkError: {str(e)}; "
|
|
|
except ccxt.ExchangeError as e:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro da exchange ccxt ao coletar dados de mercado: {str(e)}", exc_info=True)
|
|
|
market_data_fetch_success = False
|
|
|
error_details += f"CCXT ExchangeError: {str(e)}; "
|
|
|
except Exception as e:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro geral ao coletar dados de cripto via ccxt: {str(e)}", exc_info=True)
|
|
|
market_data_fetch_success = False
|
|
|
error_details += f"General CCXT data collection error: {str(e)}; "
|
|
|
else:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Instância da exchange ccxt não disponível. Pulando coleta de dados de cripto.")
|
|
|
|
|
|
|
|
|
if CCXT_API_KEY and CCXT_API_SECRET:
|
|
|
market_data_fetch_success = False
|
|
|
error_details += "CCXT exchange object not initialized despite API keys being present; "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
market_data_results["other"]['simulated_index_level'] = random.uniform(10000, 15000)
|
|
|
market_data_results["other"]['simulated_crypto_sentiment'] = random.uniform(-1, 1)
|
|
|
|
|
|
if not market_data_fetch_success and (CCXT_API_KEY and CCXT_API_SECRET):
|
|
|
final_status = "failed_market_data"
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado falhou criticamente. {error_details}")
|
|
|
|
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
transactions_db[rnn_tx_id]["market_data_collected"] = market_data_results
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Processing RNN analysis"
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Coleta de dados de mercado concluída (sucesso: {market_data_fetch_success}).")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Executando análise RNN com os dados coletados...")
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Running RNN model"
|
|
|
investment_decisions = []
|
|
|
rnn_analysis_success = True
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await asyncio.sleep(random.uniform(8, 15))
|
|
|
if market_data_results["crypto"]:
|
|
|
if random.random() > 0.1:
|
|
|
num_crypto_assets_to_invest = random.randint(1, len(crypto_pairs_to_fetch))
|
|
|
chosen_pairs = random.sample(list(market_data_results["crypto"].keys()), k=num_crypto_assets_to_invest)
|
|
|
|
|
|
for crypto_key in chosen_pairs:
|
|
|
asset_symbol = crypto_key.replace("_", "/")
|
|
|
allocated_amount = (amount / num_crypto_assets_to_invest) * random.uniform(0.7, 0.9)
|
|
|
investment_decisions.append({
|
|
|
"asset_id": asset_symbol,
|
|
|
"type": "CRYPTO",
|
|
|
"action": "BUY",
|
|
|
"target_usd_amount": round(allocated_amount, 2),
|
|
|
"reasoning": f"RNN signal for {asset_symbol} based on simulated data and ticker {market_data_results['crypto'][crypto_key].get('ticker', {}).get('last', 'N/A')}"
|
|
|
})
|
|
|
else:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Sem dados de cripto para a RNN processar.")
|
|
|
|
|
|
if not investment_decisions:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: RNN não gerou nenhuma decisão de investimento.")
|
|
|
else:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: RNN gerou {len(investment_decisions)} decisões: {investment_decisions}")
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante análise RNN: {str(e)}", exc_info=True)
|
|
|
rnn_analysis_success = False
|
|
|
error_details += f"RNN analysis failed: {str(e)}; "
|
|
|
|
|
|
if not rnn_analysis_success:
|
|
|
final_status = "failed_rnn_analysis"
|
|
|
|
|
|
pass
|
|
|
|
|
|
transactions_db[rnn_tx_id]["rnn_decisions"] = investment_decisions
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Preparing to execute orders"
|
|
|
total_usd_allocated_by_rnn = sum(d['target_usd_amount'] for d in investment_decisions if d['action'] == 'BUY')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Executando ordens baseadas nas decisões da RNN...")
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Executing investment orders"
|
|
|
executed_trades_info = []
|
|
|
order_execution_success = True
|
|
|
cash_remaining_after_allocation = amount - total_usd_allocated_by_rnn
|
|
|
current_portfolio_value = 0
|
|
|
|
|
|
|
|
|
if investment_decisions:
|
|
|
for decision in investment_decisions:
|
|
|
if decision.get("action") == "BUY":
|
|
|
usd_amount_to_spend = decision.get("target_usd_amount")
|
|
|
simulated_cost = usd_amount_to_spend * random.uniform(0.99, 1.0)
|
|
|
executed_trades_info.append({
|
|
|
"asset": decision.get("asset_id"), "order_id": f"sim_ord_{uuid.uuid4()}",
|
|
|
"status": "filled", "cost_usd": simulated_cost
|
|
|
})
|
|
|
current_portfolio_value += simulated_cost
|
|
|
await asyncio.sleep(random.uniform(1,3) * len(investment_decisions))
|
|
|
else:
|
|
|
cash_remaining_after_allocation = amount
|
|
|
|
|
|
transactions_db[rnn_tx_id]["executed_trades"] = executed_trades_info
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Simulating holding period and profit/loss"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Simulando período de investimento e fechamento de posições...")
|
|
|
await asyncio.sleep(random.uniform(5, 10))
|
|
|
|
|
|
if current_portfolio_value > 0:
|
|
|
profit_on_invested_part = current_portfolio_value * (0.042 * random.uniform(0.8, 1.2))
|
|
|
value_of_investments_at_eod = current_portfolio_value + profit_on_invested_part
|
|
|
else:
|
|
|
value_of_investments_at_eod = 0
|
|
|
profit_on_invested_part = 0
|
|
|
|
|
|
calculated_final_amount = value_of_investments_at_eod + cash_remaining_after_allocation
|
|
|
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Valor inicial total: {amount:.2f}. "
|
|
|
f"Valor alocado para investimento: {total_usd_allocated_by_rnn:.2f}. "
|
|
|
f"Valor dos investimentos no EOD: {value_of_investments_at_eod:.2f}. "
|
|
|
f"Caixa não alocado: {cash_remaining_after_allocation:.2f}. "
|
|
|
f"Valor final total: {calculated_final_amount:.2f}")
|
|
|
|
|
|
transactions_db[rnn_tx_id]["eod_portfolio_value_simulated"] = value_of_investments_at_eod
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Registrando (tokenizando) operação detalhadamente...")
|
|
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if exchange and hasattr(exchange, 'close'):
|
|
|
try:
|
|
|
await exchange.close()
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Conexão ccxt com {CCXT_EXCHANGE_ID} fechada.")
|
|
|
except Exception as e:
|
|
|
logger.warning(f"BG TASK [{rnn_tx_id}]: Erro ao fechar conexão ccxt: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
if not AIBANK_CALLBACK_URL or not CALLBACK_SHARED_SECRET:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: AIBANK_CALLBACK_URL ou CALLBACK_SHARED_SECRET não configurado. Não é possível enviar callback.")
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = "config_missing"
|
|
|
return
|
|
|
|
|
|
callback_payload_data = InvestmentResultPayload(
|
|
|
rnn_transaction_id=rnn_tx_id,
|
|
|
aibank_transaction_token=aibank_tx_token,
|
|
|
client_id=client_id,
|
|
|
initial_amount=amount,
|
|
|
final_amount=calculated_final_amount,
|
|
|
profit_loss=calculated_final_amount - amount,
|
|
|
status=final_status,
|
|
|
timestamp=datetime.utcnow(),
|
|
|
details=error_details if final_status != "completed" else "Investment cycle completed successfully."
|
|
|
)
|
|
|
payload_json = callback_payload_data.model_dump_json()
|
|
|
signature = hmac.new(CALLBACK_SHARED_SECRET.encode('utf-8'), payload_json.encode('utf-8'), hashlib.sha256).hexdigest()
|
|
|
headers = {'Content-Type': 'application/json', 'X-RNN-Signature': signature}
|
|
|
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Enviando callback para AIBank: {AIBANK_CALLBACK_URL} com status final '{final_status}'")
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = "sending"
|
|
|
try:
|
|
|
async with httpx.AsyncClient() as client:
|
|
|
response = await client.post(AIBANK_CALLBACK_URL, content=payload_json, headers=headers, timeout=30.0)
|
|
|
response.raise_for_status()
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Callback enviado com sucesso para AIBank. Status da resposta: {response.status_code}")
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = f"sent_success_{response.status_code}"
|
|
|
except Exception as e:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao enviar callback para AIBank: {str(e)}", exc_info=True)
|
|
|
|
|
|
if isinstance(e, httpx.RequestError):
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_request_error"
|
|
|
elif isinstance(e, httpx.HTTPStatusError):
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_http_error_{e.response.status_code}"
|
|
|
else:
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_unknown_error"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
""" async def execute_investment_strategy_background(
|
|
|
rnn_tx_id: str,
|
|
|
client_id: str,
|
|
|
amount: float,
|
|
|
aibank_tx_token: str
|
|
|
):
|
|
|
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Iniciando estratégia de investimento para cliente {client_id}, valor {amount}.")
|
|
|
transactions_db[rnn_tx_id]["status"] = "processing_market_data"
|
|
|
|
|
|
final_status = "completed"
|
|
|
error_details = ""
|
|
|
calculated_final_amount = amount # Valor inicial
|
|
|
|
|
|
try:
|
|
|
# 1. COLETAR DADOS DE MERCADO (Placeholder)
|
|
|
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Coletando dados de mercado...")
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Fetching market data"
|
|
|
market_data_results = {}
|
|
|
market_data_fetch_success = True
|
|
|
|
|
|
# Exemplo para Cripto com ccxt (requer 'pip install ccxt')
|
|
|
# import ccxt.async_support as ccxt # Coloque no topo do app.py
|
|
|
# exchange_id = 'binance' # Exemplo
|
|
|
# exchange_class = getattr(ccxt, exchange_id)
|
|
|
# exchange = exchange_class({
|
|
|
# 'apiKey': EXCHANGE_API_KEY, # Do os.environ
|
|
|
# 'secret': EXCHANGE_API_SECRET, # Do os.environ
|
|
|
# 'enableRateLimit': True, # Importante
|
|
|
# })
|
|
|
|
|
|
try:
|
|
|
# --- Exemplo para buscar dados de BTC/USDT ---
|
|
|
# if exchange.has['fetchTicker']:
|
|
|
# ticker_btc = await exchange.fetch_ticker('BTC/USDT')
|
|
|
# market_data_results['BTC_USDT_ticker'] = ticker_btc
|
|
|
# logger.info(f"BG TASK [{rnn_tx_id}]: Ticker BTC/USDT: {ticker_btc['last']}")
|
|
|
# if exchange.has['fetchOHLCV']:
|
|
|
# ohlcv_btc = await exchange.fetch_ohlcv('BTC/USDT', timeframe='1h', limit=100) # Últimas 100 horas
|
|
|
# market_data_results['BTC_USDT_ohlcv_1h'] = ohlcv_btc
|
|
|
# logger.info(f"BG TASK [{rnn_tx_id}]: Coletado {len(ohlcv_btc)} candles OHLCV para BTC/USDT 1h.")
|
|
|
|
|
|
# --- Exemplo para Ações com yfinance (requer 'pip install yfinance') ---
|
|
|
# import yfinance as yf # Coloque no topo do app.py
|
|
|
# aapl = yf.Ticker("AAPL")
|
|
|
# hist_aapl = aapl.history(period="1mo") # Dados do último mês
|
|
|
# market_data_results['AAPL_history_1mo'] = hist_aapl.to_dict() # Pode ser grande, serialize com cuidado
|
|
|
# current_price_aapl = hist_aapl['Close'].iloc[-1] if not hist_aapl.empty else None
|
|
|
# market_data_results['AAPL_current_price'] = current_price_aapl
|
|
|
# logger.info(f"BG TASK [{rnn_tx_id}]: Preço atual AAPL (yfinance): {current_price_aapl}")
|
|
|
|
|
|
# --- Placeholder para sua lógica real de coleta ---
|
|
|
# Você precisará definir QUAIS ativos e QUAIS dados são necessários para sua RNN.
|
|
|
# Este é um ponto crucial para sua estratégia.
|
|
|
# Simulação para prosseguir:
|
|
|
await asyncio.sleep(random.uniform(3, 7)) # Simula demora da coleta real
|
|
|
market_data_results['simulated_index_level'] = random.uniform(10000, 15000)
|
|
|
market_data_results['simulated_crypto_sentiment'] = random.uniform(-1, 1)
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Dados de mercado (simulados/reais) coletados.")
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao coletar dados de mercado: {str(e)}", exc_info=True)
|
|
|
market_data_fetch_success = False
|
|
|
error_details += f"Market data collection failed: {str(e)}; "
|
|
|
# Decida se a falha aqui é crítica e impede de continuar.
|
|
|
# Se sim, atualize final_status e pule para o callback.
|
|
|
|
|
|
# finally: # Importante para fechar conexões de exchange em ccxt
|
|
|
# if 'exchange' in locals() and hasattr(exchange, 'close'):
|
|
|
# await exchange.close()
|
|
|
|
|
|
if not market_data_fetch_success:
|
|
|
# Lógica para lidar com falha na coleta de dados (ex: não prosseguir)
|
|
|
final_status = "failed_market_data"
|
|
|
# ... (atualize transactions_db e pule para a seção de callback) ...
|
|
|
# (Este 'return' ou lógica de pular precisa ser implementada se a falha for fatal)
|
|
|
pass # Por ora, deixamos prosseguir com dados possivelmente incompletos ou apenas simulados
|
|
|
|
|
|
transactions_db[rnn_tx_id]["market_data_collected"] = market_data_results # Armazene para auditoria/debug
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Processing RNN analysis"
|
|
|
|
|
|
|
|
|
# 2. ANÁLISE PELA RNN E TOMADA DE DECISÃO (Placeholder)
|
|
|
l
|
|
|
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Executando análise RNN com os dados coletados...")
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Running RNN model"
|
|
|
investment_decisions = [] # Lista de decisões: {'asset': 'BTC/USDT', 'action': 'BUY', 'amount_usd': 5000, 'price_target': 70000}
|
|
|
rnn_analysis_success = True
|
|
|
|
|
|
try:
|
|
|
# --- SUBSTITUA PELA CHAMADA AO SEU MODELO RNN ---
|
|
|
# Exemplo: Supondo que você tenha uma classe ou função rnn_predictor
|
|
|
# from rnn.models.predictor import rnn_predictor # Exemplo de import
|
|
|
|
|
|
# Supondo que seu predictor precise dos dados de mercado e do montante a investir
|
|
|
# investment_decisions = await rnn_predictor.generate_signals_async(
|
|
|
# market_data_results,
|
|
|
# amount_to_invest=amount # O montante total disponível para este ciclo
|
|
|
# )
|
|
|
|
|
|
# Simulação para prosseguir:
|
|
|
await asyncio.sleep(random.uniform(8, 15)) # Simula processamento da RNN
|
|
|
if random.random() > 0.1: # 90% de chance de "decidir" investir
|
|
|
num_assets_to_invest = random.randint(1, 3)
|
|
|
for i in range(num_assets_to_invest):
|
|
|
asset_name = random.choice(["SIMULATED_CRYPTO_X", "SIMULATED_STOCK_Y", "SIMULATED_BOND_Z"])
|
|
|
action = random.choice(["BUY", "HOLD"]) # Simplificado, sem SELL por enquanto
|
|
|
if action == "BUY":
|
|
|
# Alocar uma porção do 'amount' total para este ativo
|
|
|
allocated_amount = (amount / num_assets_to_invest) * random.uniform(0.8, 1.0)
|
|
|
investment_decisions.append({
|
|
|
"asset_id": f"{asset_name}_{i}",
|
|
|
"type": "CRYPTO" if "CRYPTO" in asset_name else "STOCK", # Exemplo
|
|
|
"action": action,
|
|
|
"target_usd_amount": round(allocated_amount, 2),
|
|
|
"reasoning": "RNN signal strong based on simulated data" # Adicione o output real da RNN
|
|
|
})
|
|
|
|
|
|
if not investment_decisions:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: RNN não gerou nenhuma decisão de investimento (ou decidiu não investir).")
|
|
|
# Isso pode ser um resultado válido.
|
|
|
else:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: RNN gerou {len(investment_decisions)} decisões: {investment_decisions}")
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante análise RNN: {str(e)}", exc_info=True)
|
|
|
rnn_analysis_success = False
|
|
|
error_details += f"RNN analysis failed: {str(e)}; "
|
|
|
|
|
|
if not rnn_analysis_success:
|
|
|
final_status = "failed_rnn_analysis"
|
|
|
# ... (atualize transactions_db e pule para a seção de callback) ...
|
|
|
pass
|
|
|
|
|
|
transactions_db[rnn_tx_id]["rnn_decisions"] = investment_decisions
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Preparing to execute orders"
|
|
|
|
|
|
# Antes de executar ordens, vamos calcular o valor que REALMENTE foi investido
|
|
|
# e o que pode ter sobrado, já que a RNN pode não usar todo o 'amount'.
|
|
|
total_usd_allocated_by_rnn = sum(d['target_usd_amount'] for d in investment_decisions if d['action'] == 'BUY')
|
|
|
# calculated_final_amount = amount # Inicializa com o montante original
|
|
|
# Esta variável será atualizada após a execução das ordens e cálculo do lucro/perda
|
|
|
|
|
|
|
|
|
# 3. EXECUÇÃO DE ORDENS (Placeholder)
|
|
|
|
|
|
# Dentro de execute_investment_strategy_background, substituindo a seção de execução de ordens:
|
|
|
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Executando ordens baseadas nas decisões da RNN...")
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Executing investment orders"
|
|
|
executed_trades_info = []
|
|
|
order_execution_success = True
|
|
|
|
|
|
# O calculated_final_amount começa como o 'amount' inicial.
|
|
|
# Vamos deduzir o que foi efetivamente usado para compras
|
|
|
# e depois adicionar os lucros/perdas.
|
|
|
# Por enquanto, vamos assumir que o investimento visa usar o 'total_usd_allocated_by_rnn'.
|
|
|
# E o restante do 'amount' não alocado fica como "cash".
|
|
|
cash_remaining_after_allocation = amount - total_usd_allocated_by_rnn
|
|
|
current_portfolio_value = 0 # Valor dos ativos comprados
|
|
|
|
|
|
# --- Exemplo de lógica de execução para decisões de COMPRA (BUY) ---
|
|
|
if investment_decisions: # Apenas se houver decisões
|
|
|
# exchange_exec = ccxt.binance({'apiKey': EXCHANGE_API_KEY, 'secret': EXCHANGE_API_SECRET}) # Exemplo
|
|
|
try:
|
|
|
for decision in investment_decisions:
|
|
|
if decision.get("action") == "BUY":
|
|
|
asset_id_to_buy = decision.get("asset_id") # Ex: "BTC/USDT" ou um ID interno que mapeia para um símbolo
|
|
|
usd_amount_to_spend = decision.get("target_usd_amount")
|
|
|
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Tentando comprar {usd_amount_to_spend} USD de {asset_id_to_buy}")
|
|
|
|
|
|
# --- SUBSTITUA PELA LÓGICA REAL DE EXECUÇÃO NA EXCHANGE ---
|
|
|
# Exemplo com ccxt (precisa de mais detalhes como símbolo de mercado correto):
|
|
|
# symbol_on_exchange = convert_asset_id_to_exchange_symbol(asset_id_to_buy, exchange_exec.id)
|
|
|
# current_price = (await exchange_exec.fetch_ticker(symbol_on_exchange))['last']
|
|
|
# amount_of_asset_to_buy = usd_amount_to_spend / current_price
|
|
|
|
|
|
# order = await exchange_exec.create_market_buy_order(symbol_on_exchange, amount_of_asset_to_buy)
|
|
|
# logger.info(f"BG TASK [{rnn_tx_id}]: Ordem de compra para {asset_id_to_buy} enviada: {order['id']}")
|
|
|
# executed_trades_info.append({
|
|
|
# "asset": asset_id_to_buy,
|
|
|
# "order_id": order['id'],
|
|
|
# "status": order.get('status', 'unknown'),
|
|
|
# "amount_filled": order.get('filled', 0),
|
|
|
# "avg_price": order.get('average', current_price),
|
|
|
# "cost_usd": order.get('cost', usd_amount_to_spend), # Custo real da ordem
|
|
|
# "fees": order.get('fee', {}),
|
|
|
# })
|
|
|
# current_portfolio_value += order.get('cost', usd_amount_to_spend) # Adiciona o valor do ativo comprado
|
|
|
|
|
|
# Simulação para prosseguir:
|
|
|
await asyncio.sleep(random.uniform(1, 3)) # Simula envio de ordem
|
|
|
simulated_order_id = f"sim_ord_{uuid.uuid4()}"
|
|
|
simulated_cost = usd_amount_to_spend * random.uniform(0.99, 1.0) # Slippage simulado
|
|
|
executed_trades_info.append({
|
|
|
"asset": asset_id_to_buy,
|
|
|
"order_id": simulated_order_id,
|
|
|
"status": "filled",
|
|
|
"amount_filled": simulated_cost / random.uniform(100, 200), # Qtd de ativo simulada
|
|
|
"avg_price": random.uniform(100, 200), # Preço simulado
|
|
|
"cost_usd": simulated_cost,
|
|
|
"fees": {"currency": "USD", "cost": simulated_cost * 0.001} # Taxa simulada
|
|
|
})
|
|
|
current_portfolio_value += simulated_cost
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Ordem simulada {simulated_order_id} para {asset_id_to_buy} preenchida, custo {simulated_cost:.2f} USD.")
|
|
|
else:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Decisão '{decision.get('action')}' para {decision.get('asset_id')} não é uma compra, pulando execução por enquanto.")
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante execução de ordens: {str(e)}", exc_info=True)
|
|
|
order_execution_success = False
|
|
|
error_details += f"Order execution failed: {str(e)}; "
|
|
|
# finally:
|
|
|
# if 'exchange_exec' in locals() and hasattr(exchange_exec, 'close'):
|
|
|
# await exchange_exec.close()
|
|
|
else:
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Nenhuma decisão de investimento para executar.")
|
|
|
# Se não há decisões, o current_portfolio_value é 0 e o cash_remaining é todo o 'amount'
|
|
|
cash_remaining_after_allocation = amount
|
|
|
|
|
|
if not order_execution_success:
|
|
|
final_status = "failed_order_execution"
|
|
|
# ... (atualize transactions_db e pule para a seção de callback) ...
|
|
|
# O valor do portfólio aqui pode ser parcial se algumas ordens falharam
|
|
|
pass
|
|
|
|
|
|
transactions_db[rnn_tx_id]["executed_trades"] = executed_trades_info
|
|
|
transactions_db[rnn_tx_id]["status_details"] = "Simulating holding period and profit/loss"
|
|
|
|
|
|
# --- SIMULAÇÃO DO PERÍODO DE INVESTIMENTO E CÁLCULO DE LUCRO/PERDA DIÁRIO ---
|
|
|
# Em um sistema real, você monitoraria as posições e as fecharia no final do dia.
|
|
|
# Ou, se for um investimento de mais longo prazo, apenas calcularia o valor atual do portfólio.
|
|
|
# Para o objetivo de 4.2% ao dia, é implícito que as posições são fechadas diariamente.
|
|
|
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Simulando período de investimento e fechamento de posições...")
|
|
|
await asyncio.sleep(random.uniform(5, 10)) # Simula o dia passando
|
|
|
|
|
|
# Supondo que todas as posições são vendidas no final do "dia"
|
|
|
# E o current_portfolio_value muda com base no mercado.
|
|
|
# Para simular o objetivo de 4.2% sobre o VALOR INVESTIDO (current_portfolio_value no momento da compra):
|
|
|
if current_portfolio_value > 0: # Se algo foi investido
|
|
|
profit_on_invested_part = current_portfolio_value * (0.042 * random.uniform(0.8, 1.2)) # Simula variação no lucro
|
|
|
value_of_investments_at_eod = current_portfolio_value + profit_on_invested_part
|
|
|
else: # Nada foi investido
|
|
|
value_of_investments_at_eod = 0
|
|
|
profit_on_invested_part = 0
|
|
|
|
|
|
# O calculated_final_amount é o valor dos investimentos no fim do dia + o caixa que não foi alocado
|
|
|
calculated_final_amount = value_of_investments_at_eod + cash_remaining_after_allocation
|
|
|
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Valor inicial total: {amount:.2f}. "
|
|
|
f"Valor alocado para investimento: {total_usd_allocated_by_rnn:.2f}. "
|
|
|
f"Valor dos investimentos no EOD: {value_of_investments_at_eod:.2f}. "
|
|
|
f"Caixa não alocado: {cash_remaining_after_allocation:.2f}. "
|
|
|
f"Valor final total: {calculated_final_amount:.2f}")
|
|
|
|
|
|
transactions_db[rnn_tx_id]["eod_portfolio_value_simulated"] = value_of_investments_at_eod
|
|
|
|
|
|
|
|
|
|
|
|
# 4. TOKENIZAÇÃO / REGISTRO DA OPERAÇÃO (Placeholder)
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Registrando (tokenizando) operação...")
|
|
|
# Aqui você implementaria sua lógica de tokenização.
|
|
|
# Poderia ser salvar em uma blockchain, ou um registro detalhado e imutável no seu DB.
|
|
|
# Ex: await tokenize_operation_async(rnn_tx_id, client_id, investment_decisions, execution_results)
|
|
|
await asyncio.sleep(2)
|
|
|
transactions_db[rnn_tx_id]["tokenization_status"] = "completed"
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Operação registrada/tokenizada.")
|
|
|
|
|
|
transactions_db[rnn_tx_id]["status"] = "completed"
|
|
|
transactions_db[rnn_tx_id]["final_amount"] = calculated_final_amount
|
|
|
transactions_db[rnn_tx_id]["profit_loss"] = calculated_final_amount - amount
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro durante execução da estratégia: {str(e)}", exc_info=True)
|
|
|
final_status = "failed"
|
|
|
error_details = str(e)
|
|
|
transactions_db[rnn_tx_id]["status"] = "failed"
|
|
|
transactions_db[rnn_tx_id]["error"] = error_details
|
|
|
# Em caso de falha, o final_amount pode ser o inicial ou o que foi possível recuperar
|
|
|
calculated_final_amount = amount # Ou o valor parcial se algumas ordens falharam
|
|
|
|
|
|
# 5. PREPARAR E ENVIAR CALLBACK PARA AIBANK
|
|
|
if not AIBANK_CALLBACK_URL or not CALLBACK_SHARED_SECRET:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: AIBANK_CALLBACK_URL ou CALLBACK_SHARED_SECRET não configurado. Não é possível enviar callback.")
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = "config_missing"
|
|
|
return
|
|
|
|
|
|
callback_payload = InvestmentResultPayload(
|
|
|
rnn_transaction_id=rnn_tx_id,
|
|
|
aibank_transaction_token=aibank_tx_token,
|
|
|
client_id=client_id,
|
|
|
initial_amount=amount,
|
|
|
final_amount=calculated_final_amount,
|
|
|
profit_loss=calculated_final_amount - amount,
|
|
|
status=final_status,
|
|
|
timestamp=datetime.utcnow(),
|
|
|
details=error_details if final_status == "failed" else "Investment cycle completed."
|
|
|
)
|
|
|
|
|
|
payload_json = callback_payload.model_dump_json()
|
|
|
|
|
|
# Criar assinatura HMAC para segurança do callback
|
|
|
signature = hmac.new(
|
|
|
CALLBACK_SHARED_SECRET.encode('utf-8'),
|
|
|
payload_json.encode('utf-8'),
|
|
|
hashlib.sha256
|
|
|
).hexdigest()
|
|
|
|
|
|
headers = {
|
|
|
'Content-Type': 'application/json',
|
|
|
'X-RNN-Signature': signature # Assinatura para o aibank verificar
|
|
|
}
|
|
|
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Enviando callback para AIBank: {AIBANK_CALLBACK_URL}")
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = "sending"
|
|
|
try:
|
|
|
async with httpx.AsyncClient() as client:
|
|
|
response = await client.post(AIBANK_CALLBACK_URL, content=payload_json, headers=headers, timeout=30.0)
|
|
|
response.raise_for_status() # Lança exceção para erros HTTP 4xx/5xx
|
|
|
logger.info(f"BG TASK [{rnn_tx_id}]: Callback enviado com sucesso para AIBank. Status: {response.status_code}")
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = f"sent_success_{response.status_code}"
|
|
|
except httpx.RequestError as e:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro ao enviar callback para AIBank (RequestError): {str(e)}")
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_request_error"
|
|
|
except httpx.HTTPStatusError as e:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro HTTP ao enviar callback para AIBank (HTTPStatusError): {e.response.status_code} - {e.response.text}")
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = f"sent_failed_http_error_{e.response.status_code}"
|
|
|
except Exception as e:
|
|
|
logger.error(f"BG TASK [{rnn_tx_id}]: Erro inesperado ao enviar callback: {str(e)}", exc_info=True)
|
|
|
transactions_db[rnn_tx_id]["callback_status"] = "sent_failed_unknown_error"
|
|
|
|
|
|
"""
|
|
|
|
|
|
@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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|