# -*- coding: utf-8 -*-
"""
ROBÔ DE BALANCEAMENTO DE PORTFÓLIO
Permite operação simultânea em múltiplas moedas com rebalanceamento automático
Baseado na estrutura do robo_trade.py
"""

import ccxt
import pandas as pd
import time
import logging
from logging.handlers import RotatingFileHandler
import json
import locale
import os
import traceback
from datetime import datetime, timedelta
import pytz
from colorama import init, Fore, Style

init()  # Inicializa suporte a ANSI em Windows

# ===================== CONFIGURAÇÕES INICIAIS =====================

def carregar_configuracoes(filepath):
    """Carrega configurações do arquivo JSON"""
    with open(filepath, 'r', encoding='utf-8') as file:
        return json.load(file)

def salvar_estado_portfolio(filepath, estado):
    """Salva o estado atual do portfólio"""
    with open(filepath, 'w', encoding='utf-8') as file:
        json.dump(estado, file, indent=2, ensure_ascii=False)

def carregar_estado_portfolio(filepath):
    """Carrega o estado do portfólio se existir"""
    try:
        if os.path.exists(filepath):
            with open(filepath, 'r', encoding='utf-8') as file:
                data = json.load(file)
                if data:
                    return data
                else:
                    logging.warning(f"Arquivo {filepath} está vazio")
                    return None
        return None
    except json.JSONDecodeError as e:
        logging.error(f"Erro ao decodificar JSON em {filepath}: {e}")
        return None
    except Exception as e:
        logging.error(f"Erro ao carregar estado: {e}")
        return None

# Configurar locale e timezone
try:
    locale.setlocale(locale.LC_ALL, 'pt_BR.UTF-8')
except locale.Error:
    try:
        locale.setlocale(locale.LC_ALL, 'Portuguese_Brazil.1252')
    except locale.Error:
        pass

# Carregar configurações
config = carregar_configuracoes('configuracoes_balanceamento.json')

# ===================== VARIÁVEIS GLOBAIS =====================

API_KEY = config.get("api_key", "")
API_SECRET = config.get("api_secret", "")
API_KEY_TESTNET = config.get("api_key_testnet", "")
API_SECRET_TESTNET = config.get("api_secret_testnet", "")
TESTNET = config.get("testnet", False)
MOEDA_BASE = config.get("moeda_base", "USDT")
CAPITAL_INICIAL = float(config.get("capital_inicial", 1000))
TEMPO_CICLO = int(config.get("tempo_ciclo", 5))
TEMPO_REFRESH = int(config.get("tempo_refresh_tela", 5))
TIMEFRAME = config.get("timeframe", "5m")
VARIACAO_BALANCEAMENTO = float(config.get("variacao_balanceamento", 2.0))
COMISSAO = float(config.get("comissao", 0.1)) / 100
SALDO_MINIMO = float(config.get("saldo_minimo", 10))
VALOR_MINIMO_OPERACAO = 5.0  # Valor mínimo de operação na Binance em USD/USDT
COMPRAR_IMEDIATAMENTE = config.get("comprar_imediatamente", False)
AGUARDAR_MELHOR_HORA = config.get("aguardar_melhor_hora", False)  # ✓ NOVO: Desabilitar espera por melhor hora se True
PORTFOLIO_CONFIG = config.get("portfolio", [])

# Paths
LOG_FILENAME = 'log_balanceamento.log'
PORTFOLIO_STATE_FILE = 'portfolio_estado.json'

# Configurar logging com rotação automática
# Rotaciona quando o arquivo atinge 50MB, mantendo 3 backups (log.1, log.2, log.3)
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Remover handlers padrão se existirem
for handler in logger.handlers[:]:
    logger.removeHandler(handler)

# Criar RotatingFileHandler
rotating_handler = RotatingFileHandler(
    LOG_FILENAME,
    maxBytes=20 * 1024 * 1024,  # 20 MB
    backupCount=3,  # Manter 3 backups
    encoding='utf-8'
)
rotating_handler.setLevel(logging.INFO)

# Criar formatter
formatter = logging.Formatter('%(message)s')
rotating_handler.setFormatter(formatter)

# Adicionar handler ao logger
logger.addHandler(rotating_handler)

timezone = pytz.timezone("America/Sao_Paulo")

# Função para sincronizar timestamp com servidor Binance
def sincronizar_timestamp_binance():
    """Sincroniza o timestamp com o servidor da Binance para evitar erros -1021"""
    try:
        # Criar exchange temporário sem credenciais para teste
        public_exchange = ccxt.binance()
        server_time = public_exchange.fetch_time()
        local_time = int(time.time() * 1000)
        time_offset = server_time - local_time
        
        if abs(time_offset) > 1000:  # Mais de 1 segundo de diferença
            logging.warning(f"Desincronização de relógio detectada: {abs(time_offset)}ms")
            print(Fore.YELLOW + f"⚠ AVISO: Relógio do PC está {time_offset/1000:.1f}s {'adiantado' if time_offset > 0 else 'atrasado'}" + Style.RESET_ALL)
        
        return time_offset
    except Exception as e:
        logging.warning(f"Não foi possível sincronizar timestamp: {e}")
        return 0

# Sincronizar relógio
time_offset = sincronizar_timestamp_binance()

# Selecionar chaves corretas baseado no modo
if TESTNET:
    chave_api = API_KEY_TESTNET
    chave_secreta = API_SECRET_TESTNET
else:
    chave_api = API_KEY
    chave_secreta = API_SECRET

# Conectar à exchange
exchange = ccxt.binance({
    'apiKey': chave_api,
    'secret': chave_secreta,
    'enableRateLimit': True,
    'options': {
        'defaultType': 'spot',
        'recvWindow': 20000,  # Janela de recepção 20 segundos para tolerância maior
        'timeDifference': time_offset,  # Usar timeDifference para sincronização
    },
})

if TESTNET:
    exchange.set_sandbox_mode(True)
    logging.info("Modo Testnet: ATIVADO (usando chaves testnet)")
    print("Modo Testnet: ATIVADO (usando chaves testnet)")
else:
    logging.info("Modo Testnet: DESATIVADO")
    print("Modo Testnet: DESATIVADO")

if COMPRAR_IMEDIATAMENTE:
    logging.info("Compra Imediata: ATIVADA (tolerância 10%)")
    print("Compra Imediata: ATIVADA (tolerância 10%)")
else:
    logging.info("Compra Imediata: DESATIVADA (tolerância 2%)")
    print("Compra Imediata: DESATIVADA (tolerância 2%)")

logging.info(f"Valor Mínimo de Operação: ${VALOR_MINIMO_OPERACAO:.2f} (Binance)")
print(f"Valor Mínimo de Operação: ${VALOR_MINIMO_OPERACAO:.2f} (Binance)")

try:
    exchange.load_markets()
    logging.info("✓ Exchange conectada com sucesso")
    print(Fore.GREEN + "✓ Mercados carregados (Exchange pronta)" + Style.RESET_ALL)
except Exception as e:
    logging.error(f"Falha ao configurar exchange: {e}")
    print(f"Falha ao configurar exchange: {e}")

# ===================== FUNÇÕES DE UTILIDADE =====================

def log_info(mensagem):
    """Log e print simultâneos"""
    timestamp = datetime.now(timezone).strftime("%d/%m/%Y %H:%M:%S")
    msg_completa = f"[{timestamp}] {mensagem}"
    logging.info(msg_completa)
    print(msg_completa)

def log_erro(mensagem):
    """Log e print de erro"""
    timestamp = datetime.now(timezone).strftime("%d/%m/%Y %H:%M:%S")
    msg_completa = f"[{timestamp}] ERRO: {mensagem}"
    logging.error(msg_completa)
    print(Fore.RED + msg_completa + Style.RESET_ALL)

def log_aviso(mensagem):
    """Log e print de aviso"""
    timestamp = datetime.now(timezone).strftime("%d/%m/%Y %H:%M:%S")
    msg_completa = f"[{timestamp}] AVISO: {mensagem}"
    logging.warning(msg_completa)
    print(Fore.YELLOW + msg_completa + Style.RESET_ALL)

def log_sucesso(mensagem):
    """Log e print de sucesso"""
    timestamp = datetime.now(timezone).strftime("%d/%m/%Y %H:%M:%S")
    msg_completa = f"[{timestamp}] ✓ {mensagem}"
    logging.info(msg_completa)
    print(Fore.GREEN + msg_completa + Style.RESET_ALL)

def obter_preco_atual(symbol):
    """Obtém o preço atual de um símbolo com retry automático"""
    max_tentativas = 3
    for tentativa in range(max_tentativas):
        try:
            ticker = exchange.fetch_ticker(symbol)
            price = ticker.get('last') or ticker.get('close')
            return float(price) if price else None
        except Exception as e:
            erro_str = str(e)
            # Verificar se é erro de timestamp (-1021)
            if "-1021" in erro_str or "Timestamp" in erro_str:
                if tentativa < max_tentativas - 1:
                    log_aviso(f"Erro de timestamp ao obter {symbol}, resincronizando... (tentativa {tentativa + 1}/{max_tentativas})")
                    global time_offset
                    time_offset = sincronizar_timestamp_binance()
                    exchange.options['timeDifference'] = time_offset  # Usar timeDifference
                    time.sleep(2)  # Esperar 2 segundos antes de retry
                    continue
            
            if tentativa == max_tentativas - 1:
                log_erro(f"Erro ao obter preço de {symbol}: {e}")
            return None

def obter_variacao_24h(symbol):
    """Obtém a variação percentual de 24h de um símbolo com retry automático"""
    max_tentativas = 3
    for tentativa in range(max_tentativas):
        try:
            ticker = exchange.fetch_ticker(symbol)
            # CCXT retorna em diferentes campos dependendo da exchange
            # Tentar múltiplas fontes de variação de 24h
            variacao = ticker.get('percentage') or ticker.get('change') or ticker.get('changePercent')
            
            # Se não encontrar, calcular a partir de open e close
            if variacao is None:
                close_price = ticker.get('last') or ticker.get('close')
                open_price = ticker.get('open')
                if close_price and open_price and open_price > 0:
                    variacao = ((close_price - open_price) / open_price) * 100
            
            return float(variacao) if variacao is not None else 0.0
        except Exception as e:
            erro_str = str(e)
            if "-1021" in erro_str or "Timestamp" in erro_str:
                if tentativa < max_tentativas - 1:
                    global time_offset
                    time_offset = sincronizar_timestamp_binance()
                    exchange.options['timeDifference'] = time_offset
                    time.sleep(2)
                    continue
            
            if tentativa == max_tentativas - 1:
                log_erro(f"Erro ao obter variação 24h de {symbol}: {e}")
            return None

def obter_saldo(ativo):
    """Obtém saldo disponível de um ativo com retry automático"""
    max_tentativas = 3
    for tentativa in range(max_tentativas):
        try:
            balance = exchange.fetch_balance()
            free = balance.get('free', {})
            return float(free.get(ativo, 0))
        except Exception as e:
            erro_str = str(e)
            # Verificar se é erro de timestamp (-1021)
            if "-1021" in erro_str or "Timestamp" in erro_str:
                if tentativa < max_tentativas - 1:
                    log_aviso(f"Erro de timestamp ao obter saldo de {ativo}, resincronizando... (tentativa {tentativa + 1}/{max_tentativas})")
                    global time_offset
                    time_offset = sincronizar_timestamp_binance()
                    exchange.options['timeDifference'] = time_offset  # Usar timeDifference
                    time.sleep(2)  # Esperar 2 segundos antes de retry
                    continue
            
            if tentativa == max_tentativas - 1:
                log_erro(f"Erro ao obter saldo de {ativo}: {e}")
            return 0.0

def obter_saldos_portfolio(moedas):
    """Obtém saldos de todas as moedas do portfólio"""
    saldos = {}
    for moeda in moedas:
        saldos[moeda] = obter_saldo(moeda)
    saldos[MOEDA_BASE] = obter_saldo(MOEDA_BASE)
    return saldos

def verificar_par_disponivel(symbol):
    """
    Verifica se um par de trading está disponível e funcionando no testnet
    
    Returns:
        tuple: (está_disponível, info_diagnóstica)
    """
    try:
        # Tentar carregar informações do mercado do par
        if symbol not in exchange.symbols:
            return False, f"Par {symbol} não está na lista de símbolos disponíveis no testnet"
        
        # Tentar obter ticker (informações de preço)
        ticker = exchange.fetch_ticker(symbol)
        last_price = ticker.get('last') or ticker.get('close')
        volume = ticker.get('baseVolume')
        
        if not last_price or last_price <= 0:
            return False, f"Par {symbol} não tem preço válido (último: {last_price})"
        
        if volume is None or volume == 0:
            return False, f"Par {symbol} tem volume zero - sem liquidez no testnet"
        
        return True, f"Par {symbol} OK - Preço: {last_price:.8f}, Volume: {volume:.8f}"
        
    except Exception as e:
        return False, f"Erro ao verificar par {symbol}: {str(e)}"

def obter_valor_portfolio(moedas, saldos, precos):
    """Calcula o valor total do portfólio em USDT"""
    valor_total = saldos.get(MOEDA_BASE, 0)
    for moeda in moedas:
        quantidade = saldos.get(moeda, 0)
        preco = precos.get(moeda, 0)
        if quantidade > 0 and preco:
            valor_total += quantidade * preco
    return valor_total

def calcular_percentuais_atuais(moedas, saldos, precos, valor_total):
    """Calcula os percentuais atuais de cada moeda no portfólio"""
    percentuais = {}
    if valor_total <= 0:
        for moeda in moedas:
            percentuais[moeda] = 0
        return percentuais
    
    for moeda in moedas:
        quantidade = saldos.get(moeda, 0)
        preco = precos.get(moeda, 0)
        if quantidade > 0 and preco:
            valor_moeda = quantidade * preco
            percentuais[moeda] = (valor_moeda / valor_total) * 100
        else:
            percentuais[moeda] = 0
    
    return percentuais

def remover_outliers_iqr(precos):
    """
    Remove outliers usando o método Interquartile Range (IQR)
    
    Args:
        precos: lista de preços
    
    Returns:
        tuple: (preços filtrados sem outliers, índices dos outliers)
    """
    if len(precos) < 4:
        return precos, []
    
    # Calcular Q1 e Q3
    sorted_precos = sorted(precos)
    n = len(sorted_precos)
    q1_idx = n // 4
    q3_idx = 3 * n // 4
    
    q1 = sorted_precos[q1_idx]
    q3 = sorted_precos[q3_idx]
    iqr = q3 - q1
    
    # Definir limites (1.5 × IQR é o padrão)
    limite_inferior = q1 - 1.5 * iqr
    limite_superior = q3 + 1.5 * iqr
    
    # Filtrar outliers
    precos_filtrados = []
    indices_outliers = []
    
    for idx, preco in enumerate(precos):
        if limite_inferior <= preco <= limite_superior:
            precos_filtrados.append(preco)
        else:
            indices_outliers.append(idx)
    
    return precos_filtrados, indices_outliers

def obter_melhores_precos_5dias(moedas):
    """
    DESCONTINUADO: Use obter_melhores_precos_30dias() em vez disso.
    Mantido para compatibilidade com versões antigas.
    """
    return obter_melhores_precos_30dias(moedas)

def obter_melhores_precos_30dias(moedas):
    """
    Busca os melhores preços (máxima e mínima) dos últimos 30 dias (ou máximo disponível) para cada moeda
    usando dados OHLCV (Open, High, Low, Close, Volume) da exchange
    Remove outliers para análise de padrões de horário mais confiáveis
    
    Retorna análise consolidada sem detalhe dia a dia para maior confiabilidade.
    Com ~250+ candles (≈10-11 dias), a confiança dos padrões é muito boa.
    
    Returns:
        dict: {
            'moeda': {
                'melhor_hora_compra': { hora, confianca%, ocorrencias },
                'melhor_hora_venda': { hora, confianca%, ocorrencias },
                'analise': {
                    'total_registros': int (candles coletados),
                    'registros_validos': int (após remover outliers),
                    'total_outliers': int,
                    'percentual_outliers': float (%)
                },
                'timestamp': str (ISO 8601)
            }
        }
    """
    melhores_precos = {}
    
    try:
        # Buscar histórico máximo disponível em candles de 1 hora
        # Idealmente 30 dias × 24 horas = 720 candles, mas aceita o máximo disponível
        timeframe = '1h'
        
        for moeda in moedas:
            try:
                symbol = f"{moeda}/{MOEDA_BASE}"
                
                # Buscar máximo de candles disponíveis (a API retorna o máximo que consegue)
                candles = exchange.fetch_ohlcv(symbol, timeframe, limit=250)
                
                if not candles or len(candles) < 200:  # Mínimo 8-9 dias de dados
                    log_aviso(f"Candles insuficientes para {moeda} (precisa 200, tem {len(candles)})")
                    continue
                
                # Usar todos os candles disponíveis
                candles = candles[-250:] if len(candles) >= 250 else candles
                
                total_registros = len(candles)
                
                # Extrair todos os preços altos e baixos para análise consolidada
                highs = [candle[2] for candle in candles]
                lows = [candle[3] for candle in candles]
                
                # Remover outliers usando IQR
                highs_filtrados, outliers_high = remover_outliers_iqr(highs)
                lows_filtrados, outliers_low = remover_outliers_iqr(lows)
                
                # Contar outliers únicos
                indices_outliers = set(outliers_high) | set(outliers_low)
                total_outliers = len(indices_outliers)
                registros_validos = total_registros - total_outliers
                percentual_outliers = (total_outliers / total_registros * 100) if total_registros > 0 else 0
                
                # Agrupamento por DATA para análise diária
                # Para cada dia, encontrar qual período é melhor para compra/venda
                candles_por_data = {}
                
                for idx, candle in enumerate(candles):
                    if idx not in indices_outliers:
                        timestamp = candle[0]
                        dt = datetime.fromtimestamp(timestamp / 1000, tz=timezone)
                        data_str = dt.strftime('%Y-%m-%d')
                        
                        if data_str not in candles_por_data:
                            candles_por_data[data_str] = []
                        
                        candles_por_data[data_str].append({
                            'hora': dt.hour,
                            'low': candle[3],
                            'high': candle[2]
                        })
                
                total_dias = len(candles_por_data)
                
                # Definir grupos de 4 horas com rastreamento de horas exatas
                grupos_horas = {
                    0: {'label': '00:00-03:59', 'dias_vencedor_compra': 0, 'dias_vencedor_venda': 0, 'horas_compra': [], 'horas_venda': []},
                    1: {'label': '04:00-07:59', 'dias_vencedor_compra': 0, 'dias_vencedor_venda': 0, 'horas_compra': [], 'horas_venda': []},
                    2: {'label': '08:00-11:59', 'dias_vencedor_compra': 0, 'dias_vencedor_venda': 0, 'horas_compra': [], 'horas_venda': []},
                    3: {'label': '12:00-15:59', 'dias_vencedor_compra': 0, 'dias_vencedor_venda': 0, 'horas_compra': [], 'horas_venda': []},
                    4: {'label': '16:00-19:59', 'dias_vencedor_compra': 0, 'dias_vencedor_venda': 0, 'horas_compra': [], 'horas_venda': []},
                    5: {'label': '20:00-23:59', 'dias_vencedor_compra': 0, 'dias_vencedor_venda': 0, 'horas_compra': [], 'horas_venda': []},
                }
                
                # Processar cada dia
                for data_str, candles_dia in candles_por_data.items():
                    if not candles_dia:
                        continue
                    
                    # Agrupar candles do dia por período de 4 horas
                    grupos_dia = {i: [] for i in range(6)}
                    
                    for candle in candles_dia:
                        grupo_idx = candle['hora'] // 4
                        if grupo_idx >= 6:
                            grupo_idx = 5
                        grupos_dia[grupo_idx].append(candle)
                    
                    # Calcular preço médio por período neste dia
                    precos_medios_compra = {}  # Menor preço = melhor
                    precos_medios_venda = {}   # Maior preço = melhor
                    
                    for grupo_idx, candles_grupo in grupos_dia.items():
                        if candles_grupo:
                            preco_medio_low = sum(c['low'] for c in candles_grupo) / len(candles_grupo)
                            preco_medio_high = sum(c['high'] for c in candles_grupo) / len(candles_grupo)
                            precos_medios_compra[grupo_idx] = preco_medio_low
                            precos_medios_venda[grupo_idx] = preco_medio_high
                    
                    # Encontrar melhor período para COMPRA (menor preço médio)
                    if precos_medios_compra:
                        melhor_compra_dia = min(precos_medios_compra, key=precos_medios_compra.get)
                        grupos_horas[melhor_compra_dia]['dias_vencedor_compra'] += 1
                        # Rastrear as horas exatas do período vencedor
                        for candle in grupos_dia[melhor_compra_dia]:
                            grupos_horas[melhor_compra_dia]['horas_compra'].append(candle['hora'])
                    
                    # Encontrar melhor período para VENDA (maior preço médio)
                    if precos_medios_venda:
                        melhor_venda_dia = max(precos_medios_venda, key=precos_medios_venda.get)
                        grupos_horas[melhor_venda_dia]['dias_vencedor_venda'] += 1
                        # Rastrear as horas exatas do período vencedor
                        for candle in grupos_dia[melhor_venda_dia]:
                            grupos_horas[melhor_venda_dia]['horas_venda'].append(candle['hora'])
                
                if total_dias == 0:
                    log_aviso(f"{moeda}: Nenhum dia com dados válidos")
                    continue
                
                # Encontrar grupos vencedores com base em quantos dias foram melhores
                melhor_grupo_compra = max(grupos_horas, key=lambda x: grupos_horas[x]['dias_vencedor_compra'])
                melhor_grupo_venda = max(grupos_horas, key=lambda x: grupos_horas[x]['dias_vencedor_venda'])
                
                # Calcular confiança como: dias em que foi o melhor / total de dias × 100
                dias_vencedor_compra = grupos_horas[melhor_grupo_compra]['dias_vencedor_compra']
                dias_vencedor_venda = grupos_horas[melhor_grupo_venda]['dias_vencedor_venda']
                
                confianca_compra = (dias_vencedor_compra / total_dias * 100) if total_dias > 0 else 0
                confianca_venda = (dias_vencedor_venda / total_dias * 100) if total_dias > 0 else 0
                
                # Calcular hora média das ocorrências
                def calcular_hora_media(horas_list):
                    """Calcula a hora média em formato HH:MM"""
                    if not horas_list:
                        return 'N/A'
                    
                    # Calcular hora média (convertendo para minutos)
                    total_minutos = sum(h * 60 for h in horas_list)
                    media_minutos = total_minutos / len(horas_list)
                    horas = int(media_minutos // 60)
                    minutos = int(media_minutos % 60)
                    return f"{horas:02d}:{minutos:02d}"
                
                hora_media_compra = calcular_hora_media(grupos_horas[melhor_grupo_compra]['horas_compra'])
                hora_media_venda = calcular_hora_media(grupos_horas[melhor_grupo_venda]['horas_venda'])
                
                # Montar dados consolidados
                dados_moeda = {
                    'melhor_hora_compra': {
                        'hora': grupos_horas[melhor_grupo_compra]['label'],
                        'hora_media': hora_media_compra,
                        'confianca': round(confianca_compra, 1),
                        'ocorrencias': dias_vencedor_compra,
                    },
                    'melhor_hora_venda': {
                        'hora': grupos_horas[melhor_grupo_venda]['label'],
                        'hora_media': hora_media_venda,
                        'confianca': round(confianca_venda, 1),
                        'ocorrencias': dias_vencedor_venda,
                    },
                    'analise': {
                        'total_registros': total_registros,
                        'registros_validos': registros_validos,
                        'total_outliers': total_outliers,
                        'percentual_outliers': round(percentual_outliers, 1),
                        'total_dias': total_dias  # ✅ NOVO: Salvar total_dias para sincronizar com PHP
                    },
                    'timestamp': datetime.now(tz=timezone).isoformat()
                }
                
                melhores_precos[moeda] = dados_moeda
                
                # Log informativo consolidado
                log_info(f"  {moeda}:")
                log_info(f"    📊 Registros: {total_registros} coletados | {registros_validos} válidos ({100-percentual_outliers:.1f}%)")
                log_info(f"    ⚠️  Outliers: {total_outliers} removidos ({percentual_outliers:.1f}%)")
                log_info(f"    ✓ Faixa COMPRA:  {dados_moeda['melhor_hora_compra']['hora']} | Hora Média: {hora_media_compra} | Confiança: {confianca_compra:.1f}% | Melhor em {dias_vencedor_compra} de {total_dias} dias")
                log_info(f"    ✓ Faixa VENDA:   {dados_moeda['melhor_hora_venda']['hora']} | Hora Média: {hora_media_venda} | Confiança: {confianca_venda:.1f}% | Melhor em {dias_vencedor_venda} de {total_dias} dias")
                
            except Exception as e:
                log_aviso(f"Erro ao buscar dados de {moeda}: {e}")
                continue
        
        return melhores_precos
        
    except Exception as e:
        log_erro(f"Erro ao obter melhores preços: {e}")
        return {}

def atualizar_melhores_precos_portfolio(estado, moedas):
    """
    Atualiza o arquivo de estado do portfólio com os melhores preços dos últimos 30 dias (ou máximo disponível)
    Incluindo análise de padrões de horário e detecção de outliers
    """
    try:
        log_info("Atualizando dados de melhores preços (até 30 dias com análise consolidada)...")
        
        melhores_precos = obter_melhores_precos_30dias(moedas)
        
        if melhores_precos:
            estado['best_prices_30days'] = melhores_precos
            salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
            log_info(f"✓ Dados de melhores preços (30 dias) atualizados para {len(melhores_precos)} moedas")
        
        return estado
        
    except Exception as e:
        log_erro(f"Erro ao atualizar melhores preços: {e}")
        return estado

def executar_ordem(symbol, side, quantidade):
    """
    Executa uma ordem de compra ou venda (limit order)
    Limit orders são mais confiáveis no testnet que market orders
    """
    try:
        # Obter preço atual para usar como referência no limit order
        preco_atual = obter_preco_atual(symbol)
        
        if not preco_atual or preco_atual <= 0:
            log_erro(f"Falha ao obter preço para {symbol}. Não será possível criar limit order.")
            return None
        
        # Calcular preço limite (adiciona 1% de margem para garantir execução)
        if side == 'buy':
            preco_limite = preco_atual * 1.02  # 2% acima para compra
        else:
            preco_limite = preco_atual * 0.98  # 2% abaixo para venda
        
        log_info(f"  Criando limit order: {side} {quantidade} {symbol}")
        log_info(f"    Preço Atual: {preco_atual:.8f}")
        log_info(f"    Preço Limite: {preco_limite:.8f}")
        
        # Tentar criar limit order em vez de market order
        try:
            ordem = exchange.create_limit_order(
                symbol, 
                side, 
                quantidade, 
                preco_limite,
                {
                    'timeInForce': 'GTC',  # Good-Till-Cancelled (no time limit)
                }
            )
            timestamp = datetime.now(timezone).strftime("%d/%m/%Y %H:%M:%S")
            
            # Log detalhado da ordem
            order_id = ordem.get('id', 'N/A')
            status = ordem.get('status', 'desconhecido')
            filled = ordem.get('filled', 0)
            amount = ordem.get('amount', 0)
            
            log_info(f"  ID: {order_id} | Status: {status} | Preenchida: {filled:.8f}/{amount:.8f}")
            
            # Diagnosticar se ordem expirou ou foi rejeitada
            if status == 'expired':
                log_erro(f"  ⚠️ AVISO: Ordem expirou imediatamente (ID: {order_id})")
                log_erro(f"     Possíveis causas:")
                log_erro(f"     1. Par {symbol} pode não ter liquidez no testnet")
                log_erro(f"     2. Quantidade {quantidade:.8f} pode ser inválida")
                log_erro(f"     3. Preço {preco_limite:.8f} fora do spread")
                return None
            elif status == 'canceled':
                log_erro(f"  ⚠️ AVISO: Ordem foi cancelada pela Binance (ID: {order_id})")
                return None
            
            if side == 'sell':
                log_sucesso(f"✓ Ordem de VENDA executada: {quantidade:.8f} {symbol} em {timestamp}")
            else:
                log_sucesso(f"✓ Ordem de COMPRA executada: {quantidade:.8f} {symbol} em {timestamp}")
            
            return ordem
            
        except Exception as e_limit:
            erro_str = str(e_limit)
            log_aviso(f"  Limit order falhou: {erro_str}")
            log_aviso(f"  Tentando market order como fallback...")
            
            # Fallback para market order se limit order falhar
            try:
                ordem = exchange.create_market_order(symbol, side, quantidade)
                timestamp = datetime.now(timezone).strftime("%d/%m/%Y %H:%M:%S")
                
                order_id = ordem.get('id', 'N/A')
                status = ordem.get('status', 'desconhecido')
                filled = ordem.get('filled', 0)
                amount = ordem.get('amount', 0)
                
                log_info(f"  ID: {order_id} | Status: {status} | Preenchida: {filled:.8f}/{amount:.8f}")
                
                if status == 'expired':
                    log_erro(f"  ⚠️ Market order também expirou (ID: {order_id})")
                    log_erro(f"     Problema aparentemente com par/liquidez no testnet")
                    return None
                
                if side == 'sell':
                    log_sucesso(f"✓ Ordem de VENDA (market) executada: {quantidade:.8f} {symbol} em {timestamp}")
                else:
                    log_sucesso(f"✓ Ordem de COMPRA (market) executada: {quantidade:.8f} {symbol} em {timestamp}")
                
                return ordem
            except Exception as e_market:
                log_erro(f"  Ambas market e limit order falharam para {symbol}")
                log_erro(f"  Market error: {str(e_market)}")
                return None
                
    except Exception as e:
        log_erro(f"Falha ao executar ordem {side} para {symbol} ({quantidade:.8f}): {e}")
        log_erro(f"  Rastreamento: {traceback.format_exc()}")
        return None

def aplicar_comissao(quantidade):
    """Aplica comissão à quantidade"""
    return quantidade * (1 - COMISSAO)

def validar_valor_operacao(valor_operacao, moeda, tipo_operacao):
    """
    Valida se o valor da operação atende ao mínimo da Binance ($5 USD/USDT)
    
    Args:
        valor_operacao: Valor da operação em USDT
        moeda: Símbolo da moeda (ex: BTC)
        tipo_operacao: 'compra' ou 'venda'
    
    Returns:
        tuple: (é_válido, mensagem)
    """
    if valor_operacao < VALOR_MINIMO_OPERACAO:
        mensagem = (f"Operação de {tipo_operacao} de {moeda} BLOQUEADA: "
                  f"Valor ${valor_operacao:.2f} é inferior ao mínimo de "
                  f"${VALOR_MINIMO_OPERACAO:.2f} exigido pela Binance")
        return False, mensagem
    return True, f"Operação de {tipo_operacao} de {moeda} validada: ${valor_operacao:.2f}"

# ===================== FUNÇÕES DE OTIMIZAÇÃO DE TIMING =====================

def extrair_horario_compra(estado, moeda):
    """
    Extrai a melhor hora de compra do estado do portfólio
    
    Args:
        estado: Estado atual do portfólio
        moeda: Símbolo da moeda
    
    Returns:
        tuple: (hora_inicio, hora_fim, confianca) ou (None, None, 0) se não encontrar
        Exemplo: (8, 11, 60.5) para o período 08:00-11:59
    """
    try:
        if 'best_prices_30days' not in estado or moeda not in estado['best_prices_30days']:
            return None, None, 0
        
        dados_moeda = estado['best_prices_30days'][moeda]
        if 'melhor_hora_compra' not in dados_moeda:
            return None, None, 0
        
        melhor_hora = dados_moeda['melhor_hora_compra']['hora']  # Ex: '08:00-11:59'
        confianca = dados_moeda['melhor_hora_compra']['confianca']
        
        # Extrair horas do formato '08:00-11:59'
        partes = melhor_hora.split('-')
        if len(partes) == 2:
            hora_inicio = int(partes[0].split(':')[0])
            hora_fim = int(partes[1].split(':')[0])
            return hora_inicio, hora_fim, confianca
        
        return None, None, 0
    except Exception as e:
        log_aviso(f"Erro ao extrair horário de compra para {moeda}: {e}")
        return None, None, 0

def eh_horario_compra_ideal(moeda, estado):
    """
    Verifica se a hora atual está dentro do melhor período de compra para a moeda
    
    Args:
        moeda: Símbolo da moeda
        estado: Estado atual do portfólio com best_prices_30days
    
    Returns:
        tuple: (está_no_horário_ideal, mensagem_info)
    """
    hora_inicio, hora_fim, confianca = extrair_horario_compra(estado, moeda)
    
    if hora_inicio is None:
        return False, f"{moeda}: Dados de melhor hora não disponíveis (análise em progresso)"
    
    hora_atual = datetime.now(tz=timezone).hour
    
    # Verificar se está dentro do período
    if hora_inicio <= hora_fim:
        # Período normal (ex: 08:00-11:59)
        esta_dentro = hora_inicio <= hora_atual <= hora_fim
    else:
        # Período que passa pela meia-noite (ex: 20:00-03:59) - não deve acontecer neste caso
        esta_dentro = hora_atual >= hora_inicio or hora_atual <= hora_fim
    
    periodo_str = f"{hora_inicio:02d}:00-{hora_fim:02d}:59"
    
    if esta_dentro:
        mensagem = f"{moeda}: ✓ HORÁRIO IDEAL de compra! ({periodo_str}, confiança: {confianca:.1f}%)"
        return True, mensagem
    else:
        proxima_hora_compra = f"{hora_inicio:02d}:00"
        mensagem = f"{moeda}: ⏰ Aguardando melhor hora de compra ({periodo_str}, confiança: {confianca:.1f}%). Próxima às {proxima_hora_compra}"
        return False, mensagem

def calcular_tempo_ate_melhor_hora(moeda, estado):
    """
    Calcula quantos minutos faltam até a melhor hora de compra
    
    Args:
        moeda: Símbolo da moeda
        estado: Estado atual do portfólio
    
    Returns:
        int: Minutos até a melhor hora (ou 0 se já está na melhor hora)
    """
    hora_inicio, hora_fim, _ = extrair_horario_compra(estado, moeda)
    
    if hora_inicio is None:
        return 0
    
    agora = datetime.now(tz=timezone)
    hora_atual = agora.hour
    minuto_atual = agora.minute
    
    if hora_inicio <= hora_atual <= hora_fim:
        # Já está na melhor hora
        return 0
    
    if hora_atual < hora_inicio:
        # Próxima melhor hora é hoje
        tempo_ate = (hora_inicio - hora_atual) * 60 - minuto_atual
    else:
        # Próxima melhor hora é amanhã
        tempo_ate = (24 - hora_atual + hora_inicio) * 60 - minuto_atual
    
    return max(0, tempo_ate)

def aguardar_melhor_hora_compra(moeda, estado, tempo_maximo_minutos=None):
    """
    Aguarda até a melhor hora de compra para a moeda
    
    Args:
        moeda: Símbolo da moeda
        estado: Estado atual do portfólio
        tempo_maximo_minutos: Tempo máximo de espera em minutos (None = 5 min como padrão)
    
    Returns:
        tuple: (aguardou, tempo_esperado_minutos, mensagem)
    """
    # Usar 5 minutos como padrão para evitar travamentos indefinidos
    if tempo_maximo_minutos is None:
        tempo_maximo_minutos = 5
    
    hora_inicio, hora_fim, confianca = extrair_horario_compra(estado, moeda)
    
    if hora_inicio is None:
        log_aviso(f"{moeda}: Análise de melhor hora não disponível - prosseguindo com compra imediata")
        return False, 0, f"{moeda}: Análise de melhor hora não disponível"
    
    tempo_inicial = time.time()
    tempo_decorrido_segundos = 0
    intervalo_verificacao = 10  # Verificar a cada 10 segundos (reduzido de 60)
    proximo_log = 0  # Para controlar logs periódicos
    
    try:
        while True:
            esta_ideal, msg = eh_horario_compra_ideal(moeda, estado)
            
            if esta_ideal:
                tempo_esperado_min = round(tempo_decorrido_segundos / 60, 1)
                mensagem_sucesso = f"{moeda}: ✓ Horário ideal de compra atingido após {tempo_esperado_min} min de espera (confiança: {confianca:.1f}%)"
                log_sucesso(mensagem_sucesso)
                return True, tempo_esperado_min, mensagem_sucesso
            
            tempo_decorrido_segundos = time.time() - tempo_inicial
            tempo_decorrido_minutos = tempo_decorrido_segundos / 60
            
            # Verificar se excedeu tempo máximo
            if tempo_decorrido_minutos >= tempo_maximo_minutos:
                log_aviso(f"{moeda}: Tempo máximo de espera ({tempo_maximo_minutos} min) atingido. Prosseguindo com compra.")
                return False, tempo_decorrido_minutos, f"{moeda}: Timeout de espera atingido"
            
            # Log a cada 2 minutos
            if tempo_decorrido_minutos >= proximo_log:
                tempo_até = calcular_tempo_ate_melhor_hora(moeda, estado)
                log_info(f"{moeda}: Aguardando melhor hora... ({tempo_decorrido_minutos:.1f}/{tempo_maximo_minutos} min, {tempo_até} min até a janela ideal)")
                proximo_log = tempo_decorrido_minutos + 2
            
            # Aguardar com intervalo reduzido antes de verificar novamente
            time.sleep(intervalo_verificacao)
    
    except Exception as e:
        log_erro(f"Erro ao aguardar melhor hora para {moeda}: {e}")
        return False, 0, f"{moeda}: Erro na espera - prosseguindo com compra"

# ===================== FUNÇÕES PRINCIPAIS =====================

def inicializar_portfolio():
    """Inicializa o estado do portfólio"""
    moedas = [m['moeda'] for m in PORTFOLIO_CONFIG if m.get('ativa', True)]
    percentuais_alvo = {m['moeda']: m['percentual'] for m in PORTFOLIO_CONFIG if m.get('ativa', True)}
    precos_compra = {m['moeda']: m['preco_compra'] for m in PORTFOLIO_CONFIG if m.get('ativa', True)}
    
    estado_existente = carregar_estado_portfolio(PORTFOLIO_STATE_FILE)
    
    if estado_existente:
        log_info("Carregando estado anterior do portfólio...")
        return estado_existente, moedas
    
    # Verificar se já existe portfólio no testnet (moedas com saldo)
    saldos_reais = obter_saldos_portfolio(moedas)
    moedas_com_saldo = [m for m in moedas if saldos_reais.get(m, 0) > 0.00001]
    
    if moedas_com_saldo:
        # Portfólio já existe no testnet, pular para balanceamento
        log_aviso(f"Portfólio detectado no testnet com {len(moedas_com_saldo)} moedas: {', '.join(moedas_com_saldo)}")
        
        estado = {
            'fase': 'balanceamento',  # Já com moedas, vai direto para balanceamento
            'data_inicio': datetime.now(timezone).isoformat(),
            'capital_inicial': CAPITAL_INICIAL,
            'moedas_compradas': moedas.copy(),  # Todas já existem
            'moedas_pendentes': [],
            'percentuais_alvo': percentuais_alvo,
            'precos_compra': precos_compra,
            'historico_trades': []
        }
        log_info("✓ Fase de Balanceamento iniciada (portfólio existente)")
        return estado, moedas
    
    # Novo portfólio (sem moedas no testnet)
    estado = {
        'fase': 'construcao',
        'data_inicio': datetime.now(timezone).isoformat(),
        'capital_inicial': CAPITAL_INICIAL,
        'moedas_compradas': [],
        'moedas_pendentes': moedas.copy(),
        'percentuais_alvo': percentuais_alvo,
        'precos_compra': precos_compra,
        'historico_trades': []
    }
    
    log_info("===================== NOVO PORTFÓLIO INICIADO =====================")
    log_info(f"Capital Inicial: {CAPITAL_INICIAL} {MOEDA_BASE}")
    log_info(f"Moedas no Portfólio: {', '.join(moedas)}")
    log_info(f"Percentuais Alvo: {percentuais_alvo}")
    log_info(f"Preços de Compra (entrada): {precos_compra}")
    log_info("=" * 60)
    
    return estado, moedas

def fase_construcao_portfolio(estado, moedas):
    """
    Fase 1: Completa o portfólio comprando as moedas até os preços definidos
    """
    log_info("\n========== FASE DE CONSTRUÇÃO DO PORTFÓLIO ==========")
    
    saldos = obter_saldos_portfolio(moedas)
    precos = {m: obter_preco_atual(f"{m}/{MOEDA_BASE}") for m in moedas}
    
    # Calcular valor do portfólio baseado em saldos REAIS
    saldo_usdt = saldos.get(MOEDA_BASE, 0)
    valor_portfolio = saldo_usdt
    for moeda in moedas:
        quantidade = saldos.get(moeda, 0)
        preco = precos.get(moeda, 0)
        if quantidade > 0 and preco:
            valor_portfolio += quantidade * preco
    
    # Se saldo_usdt é 0, usar capital inicial como base de trabalho
    saldo_para_compra = saldo_usdt if saldo_usdt > 0 else CAPITAL_INICIAL
    
    log_info(f"Saldo USDT atual: {saldo_usdt:.2f}")
    if saldo_usdt == 0 and CAPITAL_INICIAL > 0:
        log_aviso(f"Testnet sem USDT. Usando capital inicial como referência: {CAPITAL_INICIAL:.2f}")
    log_info(f"Valor Total do Portfólio: {valor_portfolio:.2f} {MOEDA_BASE}")
    
    moedas_compradas = estado.get('moedas_compradas', [])
    moedas_pendentes = []
    percentuais_alvo = estado['percentuais_alvo']
    precos_compra = estado['precos_compra']
    
    for moeda in moedas:
        preco_atual = precos.get(moeda, 0)
        preco_alvo = precos_compra.get(moeda, 0)  # Preço limite de entrada desejado
        quantidade_atual = saldos.get(moeda, 0)
        
        # Moeda já possui saldo, não precisa comprar
        if quantidade_atual > 0.00001:
            log_info(f"{moeda}: Já possui {quantidade_atual:.8f} em carteira")
            moedas_compradas.append(moeda)
            continue
        
        if moeda not in moedas_compradas:
            # Moeda não foi comprada ainda
            moedas_pendentes.append(moeda)
            
            # Lógica de compra melhorada
            if COMPRAR_IMEDIATAMENTE:
                # Compra imediata: compra agora mesmo com preço um pouco acima do alvo
                if preco_alvo > 0:
                    diferenca_percentual = ((preco_atual - preco_alvo) / preco_alvo * 100)
                    condicao_compra = diferenca_percentual <= 50  # Aceita até 50% acima do alvo em compra imediata
                else:
                    condicao_compra = True  # Se não há alvo definido, compra qualquer preço
            else:
                # Compra conservadora: só compra se preço está abaixo do alvo (ou muito perto)
                tolerancia = 0.02  # 2% de tolerância
                if preco_alvo > 0:
                    condicao_compra = preco_atual <= preco_alvo * (1 + tolerancia)
                else:
                    condicao_compra = False  # Sem preço alvo definido, não compra
            
            if preco_atual and condicao_compra:
                # Preço está bom para compra
                percentual_alvo = percentuais_alvo.get(moeda, 0) / 100
                valor_compra = valor_portfolio * percentual_alvo
                quantidade = valor_compra / preco_atual * (1 - COMISSAO)
                
                # Usar saldo real para compra, se zero usa capital inicial como referência
                saldo_disponivel = saldo_usdt if saldo_usdt > 0 else CAPITAL_INICIAL
                
                # Validar valor mínimo de operação na Binance
                eh_valido, msg_validacao = validar_valor_operacao(valor_compra, moeda, 'compra')
                
                if eh_valido and valor_compra >= SALDO_MINIMO and saldo_disponivel >= valor_compra:
                    log_aviso(f"\n>>> Comprando {moeda} <<<")
                    log_info(f"    Preço Atual: {preco_atual:.8f}")
                    log_info(f"    Preço Alvo: {preco_alvo:.8f}")
                    log_info(f"    Valor: {valor_compra:.2f} {MOEDA_BASE}")
                    log_info(f"    Quantidade: {quantidade:.8f}")
                    
                    # Verificar se o par está disponível ANTES de tentar comprar
                    symbol = f"{moeda}/{MOEDA_BASE}"
                    par_disponivel, info_par = verificar_par_disponivel(symbol)
                    
                    if not par_disponivel:
                        log_erro(f"  ⚠️ Par {symbol} não está disponível no testnet!")
                        log_erro(f"     {info_par}")
                        log_aviso(f"  Pulando compra de {moeda} - par indisponível")
                        continue
                    
                    log_info(f"  ✓ {info_par}")
                    
                    # ✓ NOVO: Aguardar melhor hora de compra para maximizar lucros
                    # Timeout de 5 minutos para construção inicial (evita travamentos)
                    if AGUARDAR_MELHOR_HORA:
                        log_info(f"  ⏰ Buscando melhor momento de compra para maximizar lucros...")
                        aguardou, tempo_esperado, msg_tempo = aguardar_melhor_hora_compra(moeda, estado, tempo_maximo_minutos=5)
                        if aguardou:
                            log_sucesso(f"  {msg_tempo}")
                        else:
                            log_info(f"  {msg_tempo}")
                    else:
                        log_info(f"  → Compra imediata (aguardar_melhor_hora desabilitado)")
                    
                    # Obter saldo ANTES da compra
                    saldo_antes = obter_saldo(moeda)
                    
                    ordem = executar_ordem(symbol, 'buy', quantidade)
                    
                    if ordem:
                        # Aguardar um pouco para a Binance processar
                        time.sleep(1)
                        
                        # Obter saldo DEPOIS da compra
                        saldo_depois = obter_saldo(moeda)
                        
                        # Se saldo não mudou, aguardar mais e tentar novamente
                        if saldo_depois == saldo_antes:
                            log_aviso(f"  Saldo não mudou na primeira verificação, aguardando mais 2s...")
                            time.sleep(2)
                            saldo_depois = obter_saldo(moeda)
                        
                        # Verificar se o saldo realmente aumentou
                        diferenca_saldo = saldo_depois - saldo_antes
                        
                        if diferenca_saldo > 0:
                            log_sucesso(f"✓ Compra verificada: Saldo aumentou {diferenca_saldo:.8f} {moeda}")
                            moedas_compradas.append(moeda)
                            estado['moedas_compradas'] = moedas_compradas
                            estado['historico_trades'].append({
                                'timestamp': datetime.now(timezone).isoformat(),
                                'moeda': moeda,
                                'tipo': 'compra_inicial',
                                'preco': preco_atual,
                                'quantidade': quantidade,
                                'valor': valor_compra
                            })
                        else:
                            log_erro(f"✗ FALHA: Compra de {moeda} não confirmada! Saldo não aumentou.")
                            log_erro(f"  Antes: {saldo_antes:.8f} {moeda} | Depois: {saldo_depois:.8f} {moeda}")
                            log_erro(f"  ⚠ AVISO: A Binance aceitou a ordem mas não creditou o saldo!")
                            log_erro(f"  Possível causa: Problema no par {symbol} no testnet")
                        
                        time.sleep(1)
                elif not eh_valido:
                    log_aviso(msg_validacao)
                else:
                    log_aviso(f"Saldo insuficiente para comprar {moeda} (precisa {valor_compra:.2f}, tem {saldo_disponivel:.2f})")
            else:
                # Mostrar por que está aguardando
                if preco_alvo > 0:
                    diferenca_pct = ((preco_atual - preco_alvo) / preco_alvo * 100)
                    if COMPRAR_IMEDIATAMENTE:
                        mensagem = f"{moeda}: Preço muito alto para compra imediata ({diferenca_pct:.1f}% acima). Esperando melhora..."
                    else:
                        mensagem = f"{moeda}: Aguardando preço melhor (Atual: {preco_atual:.8f} | Alvo: {preco_alvo:.8f} | Diferença: +{diferenca_pct:.1f}%)"
                else:
                    mensagem = f"{moeda}: Preço alvo não definido na configuração"
                log_info(mensagem)
        else:
            moedas_compradas.append(moeda)
    
    estado['moedas_pendentes'] = moedas_pendentes
    estado['moedas_compradas'] = moedas_compradas
    
    # Verificar se portfólio está completo
    if len(moedas_pendentes) == 0:
        log_sucesso("PORTFÓLIO COMPLETO! Iniciando fase de balanceamento...")
        estado['fase'] = 'balanceamento'
        estado['data_balanceamento_inicio'] = datetime.now(timezone).isoformat()
    
    salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
    return estado

def fase_balanceamento_portfolio(estado, moedas):
    """
    Fase 2: Balanceia o portfólio mantendo os percentuais definidos
    Vende moedas acima do percentual e compra moedas abaixo
    """
    log_info("\n========== FASE DE BALANCEAMENTO DO PORTFÓLIO ==========")
    
    saldos = obter_saldos_portfolio(moedas)
    precos = {m: obter_preco_atual(f"{m}/{MOEDA_BASE}") for m in moedas}
    valor_portfolio = obter_valor_portfolio(moedas, saldos, precos)
    
    log_info(f"Valor Total do Portfólio: {valor_portfolio:.2f} {MOEDA_BASE}")
    
    # Calcular percentuais atuais
    percentuais_atuais = calcular_percentuais_atuais(moedas, saldos, precos, valor_portfolio)
    percentuais_alvo = estado['percentuais_alvo']
    
    log_info("\n--- Análise de Percentuais ---")
    moedas_venda = []
    moedas_compra = []
    
    for moeda in moedas:
        pct_atual = percentuais_atuais.get(moeda, 0)
        pct_alvo = percentuais_alvo.get(moeda, 0)
        diferenca = pct_atual - pct_alvo
        
        log_info(f"{moeda}: {pct_atual:.2f}% (Alvo: {pct_alvo:.2f}%) - Diferença: {diferenca:+.2f}%")
        
        if diferenca > VARIACAO_BALANCEAMENTO:
            moedas_venda.append({
                'moeda': moeda,
                'diferenca': diferenca,
                'percentual_atual': pct_atual,
                'preco': precos.get(moeda, 0)
            })
            log_aviso(f"  → {moeda} acima do alvo (venda)")
        elif diferenca < -VARIACAO_BALANCEAMENTO:
            moedas_compra.append({
                'moeda': moeda,
                'diferenca': abs(diferenca),
                'percentual_atual': pct_atual,
                'preco': precos.get(moeda, 0)
            })
            log_aviso(f"  → {moeda} abaixo do alvo (compra)")
    
    # Executar vendas (ordenar por diferença decrescente)
    saldo_usdt_antes = saldos.get(MOEDA_BASE, 0)
    
    for item_venda in sorted(moedas_venda, key=lambda x: x['diferenca'], reverse=True):
        moeda = item_venda['moeda']
        preco = item_venda['preco']
        diferenca = item_venda['diferenca']
        quantidade_atual = saldos.get(moeda, 0)
        
        if quantidade_atual > 0 and preco > 0:
            # Calcular quantidade a vender para retornar ao percentual alvo
            valor_excesso = (diferenca / 100) * valor_portfolio
            quantidade_venda = valor_excesso / preco * (1 - COMISSAO)
            
            # Validar valor mínimo de operação na Binance
            eh_valido, msg_validacao = validar_valor_operacao(valor_excesso, moeda, 'venda')
            
            if eh_valido and quantidade_venda > 0 and quantidade_venda <= quantidade_atual:
                log_aviso(f"\n>>> Vendendo {moeda} <<<")
                log_info(f"    Preço Atual: {preco:.8f}")
                log_info(f"    Quantidade: {quantidade_venda:.8f}")
                log_info(f"    Valor: {valor_excesso:.2f} {MOEDA_BASE}")
                
                # Obter saldo ANTES da venda
                saldo_antes = obter_saldo(moeda)
                
                symbol = f"{moeda}/{MOEDA_BASE}"
                ordem = executar_ordem(symbol, 'sell', quantidade_venda)
                
                if ordem:
                    # Aguardar um pouco para a Binance processar
                    time.sleep(1)
                    
                    # Obter saldo DEPOIS da venda
                    saldo_depois = obter_saldo(moeda)
                    
                    # Se saldo não mudou, aguardar mais e tentar novamente
                    if saldo_depois == saldo_antes:
                        log_aviso(f"  Saldo não mudou na primeira verificação, aguardando mais 2s...")
                        time.sleep(2)
                        saldo_depois = obter_saldo(moeda)
                    
                    # Verificar se o saldo realmente diminuiu
                    diferenca_saldo = saldo_antes - saldo_depois
                    
                    if diferenca_saldo > 0:
                        log_sucesso(f"✓ Venda verificada: Saldo diminuiu {diferenca_saldo:.8f} {moeda}")
                        saldo_usdt_antes += valor_excesso
                        estado['historico_trades'].append({
                            'timestamp': datetime.now(timezone).isoformat(),
                            'moeda': moeda,
                            'tipo': 'venda_balanceamento',
                            'preco': preco,
                            'quantidade': quantidade_venda,
                            'valor': valor_excesso
                        })
                    else:
                        log_erro(f"✗ FALHA: Venda de {moeda} não confirmada! Saldo não diminuiu.")
                        log_erro(f"  Antes: {saldo_antes:.8f} {moeda} | Depois: {saldo_depois:.8f} {moeda}")
                    
                    time.sleep(1)
            elif not eh_valido:
                log_aviso(msg_validacao)
    
    # Atualizar saldos
    saldos = obter_saldos_portfolio(moedas)
    saldo_usdt_atual = saldos.get(MOEDA_BASE, 0)
    
    # PRÉ-CÁLCULO: Calcular total necessário para todas as compras
    compras_planejadas = []
    total_usdt_necessario = 0
    
    for item_compra in moedas_compra:
        moeda = item_compra['moeda']
        preco = item_compra['preco']
        diferenca = item_compra['diferenca']
        
        if preco > 0:
            valor_compra = (diferenca / 100) * valor_portfolio
            compras_planejadas.append({
                'moeda': moeda,
                'preco': preco,
                'diferenca': diferenca,
                'valor_ideal': valor_compra
            })
            total_usdt_necessario += valor_compra
    
    # Ordenar por diferença decrescente (prioridade)
    compras_planejadas = sorted(compras_planejadas, key=lambda x: x['diferenca'], reverse=True)
    
    # Se não há saldo suficiente, reduzir proporcionalmente todos os valores
    fator_reducao = 1.0
    if total_usdt_necessario > saldo_usdt_atual:
        fator_reducao = max(0, (saldo_usdt_atual - SALDO_MINIMO) / total_usdt_necessario) if total_usdt_necessario > 0 else 0
        if fator_reducao > 0:
            log_aviso(f"\n⚠ Saldo insuficiente para completar todas as compras. Reduzindo proporcionalmente a {fator_reducao*100:.1f}%")
    
    # Executar compras com distribuição justa do saldo
    for compra in compras_planejadas:
        moeda = compra['moeda']
        preco = compra['preco']
        valor_ideal = compra['valor_ideal']
        
        # Aplicar fator de redução se necessário
        valor_compra = valor_ideal * fator_reducao
        
        # Pular se valor for muito pequeno
        if valor_compra < VALOR_MINIMO_OPERACAO:
            log_aviso(f"  {moeda}: Valor ajustado ${valor_compra:.2f} abaixo do mínimo ${VALOR_MINIMO_OPERACAO:.2f}")
            continue
        
        if preco > 0 and saldo_usdt_atual >= VALOR_MINIMO_OPERACAO:
            quantidade_compra = valor_compra / preco * (1 - COMISSAO)
            
            # Validar valor mínimo de operação na Binance
            eh_valido, msg_validacao = validar_valor_operacao(valor_compra, moeda, 'compra')
            
            if eh_valido and valor_compra >= SALDO_MINIMO and saldo_usdt_atual >= valor_compra:
                log_aviso(f"\n>>> Comprando {moeda} <<<")
                log_info(f"    Preço Atual: {preco:.8f}")
                log_info(f"    Quantidade: {quantidade_compra:.8f}")
                log_info(f"    Valor: {valor_compra:.2f} {MOEDA_BASE} (Ideal: {valor_ideal:.2f})")
                
                # Verificar se o par está disponível ANTES de tentar comprar
                symbol = f"{moeda}/{MOEDA_BASE}"
                par_disponivel, info_par = verificar_par_disponivel(symbol)
                
                if not par_disponivel:
                    log_erro(f"  ⚠️ Par {symbol} não está disponível no testnet!")
                    log_erro(f"     {info_par}")
                    log_aviso(f"  Pulando compra de {moeda} - par indisponível")
                    continue
                
                log_info(f"  ✓ {info_par}")
                
                # ✓ NOVO: Aguardar melhor hora de compra para maximizar lucros
                # Timeout de 3 minutos para rebalanceamento (evita travamentos)
                if AGUARDAR_MELHOR_HORA:
                    log_info(f"  ⏰ Buscando melhor momento de compra para maximizar lucros...")
                    aguardou, tempo_esperado, msg_tempo = aguardar_melhor_hora_compra(moeda, estado, tempo_maximo_minutos=3)
                    if aguardou:
                        log_sucesso(f"  {msg_tempo}")
                    else:
                        log_info(f"  {msg_tempo}")
                else:
                    log_info(f"  → Compra imediata (aguardar_melhor_hora desabilitado)")
                
                # Obter saldo ANTES da compra
                saldo_antes = obter_saldo(moeda)
                
                ordem = executar_ordem(symbol, 'buy', quantidade_compra)
                
                if ordem:
                    # Aguardar um pouco para a Binance processar
                    time.sleep(1)
                    
                    # Obter saldo DEPOIS da compra
                    saldo_depois = obter_saldo(moeda)
                    
                    # Se saldo não mudou, aguardar mais e tentar novamente
                    if saldo_depois == saldo_antes:
                        log_aviso(f"  Saldo não mudou na primeira verificação, aguardando mais 2s...")
                        time.sleep(2)
                        saldo_depois = obter_saldo(moeda)
                    
                    # Verificar se o saldo realmente aumentou
                    diferenca_saldo = saldo_depois - saldo_antes
                    
                    if diferenca_saldo > 0:
                        log_sucesso(f"✓ Compra verificada: Saldo aumentou {diferenca_saldo:.8f} {moeda}")
                        saldo_usdt_atual -= valor_compra
                        estado['historico_trades'].append({
                            'timestamp': datetime.now(timezone).isoformat(),
                            'moeda': moeda,
                            'tipo': 'compra_balanceamento',
                            'preco': preco,
                            'quantidade': quantidade_compra,
                            'valor': valor_compra
                        })
                    else:
                        log_erro(f"✗ FALHA: Compra de {moeda} não confirmada! Saldo não aumentou.")
                        log_erro(f"  Antes: {saldo_antes:.8f} {moeda} | Depois: {saldo_depois:.8f} {moeda}")
                    
                    time.sleep(1)
            elif not eh_valido:
                log_aviso(msg_validacao)
    
    log_info("\n--- Resumo do Balanceamento ---")
    
    # Atualizar saldos finais
    saldos = obter_saldos_portfolio(moedas)
    precos = {m: obter_preco_atual(f"{m}/{MOEDA_BASE}") for m in moedas}
    valor_portfolio_final = obter_valor_portfolio(moedas, saldos, precos)
    percentuais_finais = calcular_percentuais_atuais(moedas, saldos, precos, valor_portfolio_final)
    
    for moeda in moedas:
        log_info(f"{moeda}: {percentuais_finais.get(moeda, 0):.2f}% (Saldo: {saldos.get(moeda, 0):.8f})")
    
    log_info(f"Valor Total Final: {valor_portfolio_final:.2f} {MOEDA_BASE}")
    
    salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
    return estado

def relatorio_portfolio():
    """Exibe relatório detalhado do portfólio"""
    try:
        estado = carregar_estado_portfolio(PORTFOLIO_STATE_FILE)
        if not estado or not isinstance(estado, dict):
            log_aviso("Portfólio não inicializado ou corrompido")
            return None
        
        moedas = [m['moeda'] for m in PORTFOLIO_CONFIG if m.get('ativa', True)]
        saldos = obter_saldos_portfolio(moedas)
        precos = {m: obter_preco_atual(f"{m}/{MOEDA_BASE}") for m in moedas}
        valor_portfolio = obter_valor_portfolio(moedas, saldos, precos)
        percentuais = calcular_percentuais_atuais(moedas, saldos, precos, valor_portfolio)
        
        # ✓ NOVO: Capturar variação de 24h para cada moeda
        variacoes_24h = {m: obter_variacao_24h(f"{m}/{MOEDA_BASE}") for m in moedas}
        
        # ✓ NOVO: Atualizar melhores preços dos últimos 3 dias
        estado = atualizar_melhores_precos_portfolio(estado, moedas)
        
        # ✓ NOVO: Salvar preços atuais no estado (para exibição em tempo real)
        estado['precos_atuais'] = precos
        estado['timestamp_precos'] = datetime.now(timezone).isoformat()
        
        # ✓ NOVO: Salvar variações de 24h no estado
        estado['variacoes_24h'] = variacoes_24h
        
        # Salvar estado com preços e variações atualizados
        salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
        
        log_info("\n" + "=" * 70)
        log_info("RELATÓRIO DO PORTFÓLIO")
        log_info("=" * 70)
        log_info(f"Fase: {estado.get('fase', 'desconhecida').upper()}")
        log_info(f"Data de Início: {estado.get('data_inicio', 'N/A')}")
        log_info(f"Capital Inicial: {estado.get('capital_inicial', 0)} {MOEDA_BASE}")
        log_info(f"Valor Atual: {valor_portfolio:.2f} {MOEDA_BASE}")
        
        capital = estado.get('capital_inicial', 0)
        if capital > 0:
            log_info(f"Ganho/Perda: {valor_portfolio - capital:+.2f} {MOEDA_BASE}")
            log_info(f"Ganho %: {((valor_portfolio / capital) - 1) * 100:+.2f}%")
        
        log_info("-" * 70)
        
        for moeda in moedas:
            saldo = saldos.get(moeda, 0)
            preco = precos.get(moeda, 0)
            valor = saldo * preco if preco else 0
            pct = percentuais.get(moeda, 0)
            pct_alvo = estado.get('percentuais_alvo', {}).get(moeda, 0)
            
            log_info(f"{moeda}: Saldo={saldo:15.8f} | " +
                f"Valor: {valor:12.2f} | {pct:6.2f}% (Alvo: {pct_alvo:.2f}%)")
        
        log_info("=" * 70 + "\n")
        
        # ✓ Retornar estado com best_prices_3days atualizado
        return estado
    except Exception as e:
        log_erro(f"Erro ao exibir relatório: {e}")
        traceback.print_exc()
        return None

# ===================== MAIN LOOP =====================

def main():
    """Função principal"""
    log_info("\n" + "=" * 70)
    log_info("ROBÔ DE BALANCEAMENTO DE PORTFÓLIO - INICIADO")
    log_info("=" * 70)
    
    estado, moedas = inicializar_portfolio()
    contador_ciclo = 0
    contador_relatorio = 0
    
    try:
        while True:
            contador_ciclo += 1
            contador_relatorio += 1
            
            try:
                if estado['fase'] == 'construcao':
                    estado = fase_construcao_portfolio(estado, moedas)
                elif estado['fase'] == 'balanceamento':
                    estado = fase_balanceamento_portfolio(estado, moedas)
                
                if contador_relatorio >= TEMPO_REFRESH:
                    estado_atualizado = relatorio_portfolio()
                    if estado_atualizado:
                        estado = estado_atualizado  # Atualizar estado com best_prices_3days
                    contador_relatorio = 0
                
                log_info(f"\nPróximo ciclo em {TEMPO_CICLO} segundos...")
                time.sleep(TEMPO_CICLO)
                
            except Exception as e:
                log_erro(f"Erro no ciclo {contador_ciclo}: {e}")
                traceback.print_exc()
                time.sleep(TEMPO_CICLO)
    
    except KeyboardInterrupt:
        log_info("\nRobô interrompido pelo usuário")
        relatorio_portfolio()
    except Exception as e:
        log_erro(f"Erro fatal: {e}")
        traceback.print_exc()

if __name__ == "__main__":
    main()
