# -*- 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
from decimal import Decimal

# Import do controlador de parada
try:
    import controlador_stop
except ImportError:
    controlador_stop = None

# Import para integração com SQLite
try:
    import requests
    SQLITE_DISPONIVEL = True
except ImportError:
    SQLITE_DISPONIVEL = False
    logging.warning("⚠ requests não instalado - SQLite não estará disponível")

# Import de configuração da API SQLite
try:
    from config_sqlite_api import obter_url_api
    CONFIG_API_DISPONIVEL = True
except ImportError:
    CONFIG_API_DISPONIVEL = False
    def obter_url_api():
        return "http://localhost/projetos/rex/api.php"

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 registrar_trade_sqlite(moeda, tipo, preco, quantidade, valor):
    """
    Registra um trade no banco de dados SQLite (DIRETO, sem API HTTP)
    
    Args:
        moeda (str): Código da moeda (BTC, ETH, etc)
        tipo (str): Tipo de trade:
            - compra_inicial: Compra na fase inicial de construção do portfólio
            - venda_balanceamento: Venda durante rebalanceamento normal
            - compra_balanceamento: Compra durante rebalanceamento normal
            - compra_ajuste_capital: Compra quando capital é incrementado (novo capital investido)
            - venda_ajuste_capital: Venda quando capital é diminuído (capital retirado)
        preco (float): Preço unitário
        quantidade (float): Quantidade
        valor (float): Valor total
    
    Returns:
        bool: True se registrado com sucesso, False caso contrário
    """
    if not SQLITE_DISPONIVEL:
        return False
    
    try:
        import sqlite3
        
        # Registrar diretamente no banco de dados SQLite (em vez de usar API HTTP)
        db_path = os.path.join(os.path.dirname(__file__), 'trades.db')
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()
        
        timestamp = datetime.now(timezone).isoformat()
        
        # Inserir trade
        cursor.execute("""
            INSERT INTO trades (moeda, tipo, preco, quantidade, valor, timestamp)
            VALUES (?, ?, ?, ?, ?, ?)
        """, (moeda, tipo, float(preco), float(quantidade), float(valor), timestamp))
        
        conn.commit()
        trade_id = cursor.lastrowid
        conn.close()
        
        logging.debug(f"✓ Trade registrado no SQLite: ID {trade_id} - {moeda} ({tipo})")
        log_sucesso(f"✓ Trade registrado no DB: {moeda} {tipo} @ ${preco:.8f}")
        return True
        
    except Exception as e:
        logging.warning(f"⚠ Erro ao registrar trade no SQLite: {str(e)}")
        log_aviso(f"⚠ Erro ao registrar trade: {str(e)}")
        return False

def obter_trades_sqlite():
    """
    Obtém todos os trades do banco de dados SQLite
    
    Returns:
        list: Lista de trades do SQLite
    """
    if not SQLITE_DISPONIVEL:
        return []
    
    try:
        import sqlite3
        
        db_path = os.path.join(os.path.dirname(__file__), 'trades.db')
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()
        
        # Obter todos os trades
        cursor.execute("""
            SELECT id, moeda, tipo, preco, quantidade, valor, timestamp 
            FROM trades 
            ORDER BY id DESC
            LIMIT 10000
        """)
        
        trades = []
        for row in cursor.fetchall():
            trades.append({
                'id': row[0],
                'moeda': row[1],
                'tipo': row[2],
                'preco': row[3],
                'quantidade': row[4],
                'valor': row[5],
                'timestamp': row[6]
            })
        
        conn.close()
        logging.debug(f"✓ Obtidos {len(trades)} trades do SQLite")
        return trades
        
    except Exception as e:
        logging.warning(f"⚠ Erro ao obter trades do SQLite: {str(e)}")
        return []

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

def atualizar_quantidade_carteira_real(filepath, moedas, saldos):
    """
    ✓ NOVO: Salva as quantidades REAIS de cada moeda no portfolio_estado.json
    Isso serve como "fonte de verdade" para recuperação em caso de parada do robô
    
    Args:
        filepath: Caminho do arquivo de estado
        moedas: Lista de moedas
        saldos: Dicionário com os saldos atuais da exchange
    """
    try:
        if os.path.exists(filepath):
            with open(filepath, 'r', encoding='utf-8') as file:
                estado = json.load(file)
        else:
            estado = {}
        
        # Salvar quantidades reais de cada moeda
        if 'quantidade_carteira_real' not in estado:
            estado['quantidade_carteira_real'] = {}
        
        for moeda in moedas:
            saldo = saldos.get(moeda, 0)
            if saldo > 0 or moeda in estado['quantidade_carteira_real']:
                # ✓ saldo já é Decimal, garantir 8 casas decimais com quantize()
                if isinstance(saldo, Decimal):
                    saldo_preciso = float(saldo.quantize(Decimal('0.00000001')))
                else:
                    # Fallback para valores não-Decimal
                    saldo_preciso = float(Decimal(str(saldo)).quantize(Decimal('0.00000001')))
                estado['quantidade_carteira_real'][moeda] = saldo_preciso
        
        # Atualizar timestamp
        estado['timestamp_carteira_real'] = datetime.now(timezone).isoformat()
        
        # Salvar estado
        with open(filepath, 'w', encoding='utf-8') as file:
            json.dump(estado, file, indent=2, ensure_ascii=False)
        
        logging.info(f"✓ Quantidades reais da carteira salvas em {filepath}")
        
    except Exception as e:
        logging.error(f"Erro ao atualizar quantidades reais: {e}")

def verificar_discrepancia_carteira(estado, moedas, saldos):
    """
    ✓ NOVO: Verifica se há discrepâncias entre as quantidades salvas e as atuais
    Se houver diferenças significativas, registra em log para auditoría
    
    Args:
        estado: Estado do portfólio carregado
        moedas: Lista de moedas
        saldos: Dicionário com saldos atuais da exchange
    
    Returns:
        dict: Relatório de discrepâncias encontradas
    """
    discrepancias = {}
    
    if 'quantidade_carteira_real' not in estado:
        return discrepancias
    
    quantidade_salva = estado['quantidade_carteira_real']
    limite_minimo = 0.00001  # Limite abaixo do qual ignorar diferenças
    
    for moeda in moedas:
        if moeda in quantidade_salva:
            qtd_salva = quantidade_salva[moeda]
            qtd_atual = saldos.get(moeda, 0)
            # ✓ Usar Decimal para comparação precisa de quantidades
            qtd_salva_d = Decimal(str(qtd_salva))
            qtd_atual_d = Decimal(str(qtd_atual))
            diferenca = float(abs(qtd_atual_d - qtd_salva_d))
            
            # Se diferença é significativa (maior que 0.01% ou acima do limite)
            if diferenca > limite_minimo:
                percentual_diff = float((diferenca / max(float(qtd_salva_d), float(qtd_atual_d), limite_minimo)) * 100)
                
                if percentual_diff > 0.1:  # Mais de 0.1% de diferença
                    discrepancias[moeda] = {
                        'quantidade_salva': float(qtd_salva_d.quantize(Decimal('0.00000001'))),
                        'quantidade_atual': float(qtd_atual_d.quantize(Decimal('0.00000001'))),
                        'diferenca': float((qtd_atual_d - qtd_salva_d).quantize(Decimal('0.00000001'))),
                        'percentual': round(percentual_diff, 2)
                    }
    
    # Registrar discrepâncias em log
    if discrepancias:
        logging.warning("\n🚨 DISCREPÂNCIAS DETECTADAS NA CARTEIRA:")
        logging.warning("   (Quantidades salvas vs. quantidades atuais na exchange)")
        for moeda, info in discrepancias.items():
            logging.warning(f"   • {moeda}:")
            logging.warning(f"     - Salva: {info['quantidade_salva']} {moeda}")
            logging.warning(f"     - Atual: {info['quantidade_atual']} {moeda}")
            logging.warning(f"     - Diferença: {info['diferenca']} ({info['percentual']}%)")
        
        # Sugestão: usar as quantidades atuais como verdade
        logging.warning("\n💡 SUGESTÃO: As quantidades ATUAIS serão usadas como referência.")
        logging.warning("   Se quiser sincronizar forçadamente, as quantidades da exchange têm precedência.")
    
    return discrepancias

# 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))
SALDO_MINIMO_USDT = float(config.get("saldo_minimo_usdt", 100))
# ✓ NOVO: Histerese Assimétrica (anti-ping-pong)
VARIACAO_ENTRADA_NORMAL = VARIACAO_BALANCEAMENTO  # Usa a mesma variação configurada
VARIACAO_HISTERESE_INCREMENTO = float(config.get("variacao_histerese_incremento", 0.11))  # Incremento adicional para direção oposta
VARIACAO_ENTRADA_OPOSTA_APOS_TRADE_FORCADO = VARIACAO_ENTRADA_NORMAL + VARIACAO_HISTERESE_INCREMENTO  # 0.14 + 0.11 = 0.25
CICLOS_HISTERESE_ATIVA = int(config.get("ciclos_histerese_ativa", 10))  # Quantos ciclos manter a histerese ativa  # Proteção USDT: não usa USDT abaixo deste mínimo
VALOR_MINIMO_OPERACAO = float(config.get("valor_minimo_operacao_inicial", 5.04))  # Valor mínimo operacional (configurável em JSON, defaut: $5.04 = $5.00 + 0.07% taxa)
VARIACAO_MINIMA_NORMALIZACAO = VARIACAO_BALANCEAMENTO  # ✓ Moeda volta ao normal quando < variacao_balanceamento (remove de residuais)
COMPRAR_IMEDIATAMENTE = config.get("comprar_imediatamente", False)
REINVESTIR_GANHOS_PERCENTUAL = float(config.get("reinvestir_ganhos_percentual", 50))  # % dos ganhos a reinvestir (0-100)
RESET_PORTFOLIO_AO_INICIAR = config.get("reset_portfolio_ao_iniciar", False)  # ✓ NOVO: Limpar moedas residuais ao iniciar
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)")
logging.info(f"Proteção USDT: Mínimo ${SALDO_MINIMO_USDT:.2f} (não será gasto)")
print(f"Proteção USDT: Mínimo ${SALDO_MINIMO_USDT:.2f} (não será gasto)")

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 (retorna Decimal para precisão total)"""
    max_tentativas = 3
    for tentativa in range(max_tentativas):
        try:
            balance = exchange.fetch_balance()
            free = balance.get('free', {})
            valor = free.get(ativo, 0)
            # ✓ Retornar DECIMAL para manter precisão total (sem conversão para float)
            if isinstance(valor, str):
                return Decimal(valor)
            else:
                return Decimal(str(valor))
        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 Decimal(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 = float(saldos.get(MOEDA_BASE, 0))
    for moeda in moedas:
        quantidade = saldos.get(moeda, 0)
        preco = precos.get(moeda, 0)
        if quantidade > 0 and preco:
            # ✓ Converter Decimal para float para operação com preco (float)
            valor_total += float(quantidade) * preco
    return valor_total

def calcular_ganho_real(moedas, saldos, precos, estado):
    """
    ✓ NOVO: Calcula o ganho REAL do portfólio baseado no histórico de trades
    
    Ganho Real = (Valor atual das moedas em USDT) - (Capital investido em moedas)
    NÃO inclui USDT congelado/residual
    
    Args:
        moedas: Lista de moedas do portfólio
        saldos: Dicionário com saldos atuais
        precos: Dicionário com preços atuais
        estado: Estado do portfólio (não usado mais, mantido para compatibilidade)
    
    Returns:
        float: Ganho/Perda real em USDT (apenas das moedas compradas)
    """
    # ✓ REFATORADO: Ler trades do SQLite em vez do JSON
    capital_investido_moedas = 0
    trades = obter_trades_sqlite()
    
    for trade in trades:
        # ✓ ATUALIZADO: Incluir também compra_ajuste_capital
        if trade.get('tipo') in ['compra_inicial', 'compra_balanceamento', 'compra_ajuste_capital']:
            capital_investido_moedas += float(trade.get('valor', 0))
        # ✓ ATUALIZADO: Incluir também venda_ajuste_capital
        elif trade.get('tipo') in ['venda_balanceamento', 'venda_ajuste_capital']:
            capital_investido_moedas -= float(trade.get('valor', 0))
    
    # Calcular valor atual das moedas em USDT (apenas moedas, sem USDT congelado)
    valor_moedas_atual = 0
    for moeda in moedas:
        if moeda != 'USDT':  # Nunca contar USDT como moeda
            quantidade = saldos.get(moeda, 0)
            preco = precos.get(moeda, 0)
            if quantidade > 0 and preco > 0:
                # ✓ Converter Decimal para float para operação com preco (float)
                valor_moedas_atual += float(quantidade) * preco
    
    # Ganho = valor_atual - capital_investido
    # Se capital_investido = 0 (portfólio novo), ganho = 0
    ganho_real = valor_moedas_atual - capital_investido_moedas
    return ganho_real

def calcular_limite_reinvestimento(valor_portfolio_real, moedas=None, saldos=None, precos=None, estado=None):
    """
    Calcula o limite de portfólio considerando reinvestimento parcial de ganhos
    
    Args:
        valor_portfolio_real: Valor total real do portfólio em USDT
        moedas: Lista de moedas (opcional, para cálculo de ganho real)
        saldos: Dicionário de saldos (opcional, para cálculo de ganho real)
        precos: Dicionário de preços (opcional, para cálculo de ganho real)
        estado: Estado do portfólio (opcional, para cálculo de ganho real com histórico)
    
    Returns:
        tuple: (valor_limite, ganho_total, ganho_reinvestido, status_msg)
        - valor_limite: Valor a usar para cálculos de compra/venda
        - ganho_total: Ganho total acumulado (REAL, baseado em trades)
        - ganho_reinvestido: Valor do ganho sendo reinvestido
        - status_msg: Mensagem de status para log
    """
    # ✓ NOVO: Usar ganho REAL se histórico disponível, senão fallback para cálculo simples
    if moedas and saldos and precos and estado:
        ganho_total = calcular_ganho_real(moedas, saldos, precos, estado)
    else:
        # Fallback: calcular ganho simples (compatibilidade com código antigo)
        ganho_total = max(0, valor_portfolio_real - CAPITAL_INICIAL)
    
    # ✓ NOVO: Se reinvestimento = 0%, usar EXATAMENTE o capital inicial (sem ganhos)
    if REINVESTIR_GANHOS_PERCENTUAL <= 0:
        valor_limite = CAPITAL_INICIAL
        ganho_reinvestido = 0
        status_msg = f"Modo PROTETOR: Usando EXATAMENTE ${CAPITAL_INICIAL:.2f} (ganhos congelados)"
    else:
        ganho_reinvestido = ganho_total * (REINVESTIR_GANHOS_PERCENTUAL / 100)
        valor_limite = CAPITAL_INICIAL + ganho_reinvestido
        
        if ganho_total > 0:
            ganho_congelado = ganho_total - ganho_reinvestido
            status_msg = (f"Ganho Total: ${ganho_total:.2f} | "
                         f"Reinvestindo: ${ganho_reinvestido:.2f} ({REINVESTIR_GANHOS_PERCENTUAL:.0f}%) | "
                         f"Congelado: ${ganho_congelado:.2f}")
        elif ganho_total < 0:
            status_msg = f"Perda Atual: ${ganho_total:.2f} (portfólio em recuperação)"
        else:
            status_msg = "Sem ganhos/perdas ainda (portfólio em fase de acumulação)"
    
    return valor_limite, ganho_total, ganho_reinvestido, status_msg

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:
            # ✓ Converter Decimal para float para operação com preco (float)
            valor_moeda = float(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 ~700 candles (≈29-30 dias), a confiança dos padrões é excelente.
    
    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=700)
                
                if not candles or len(candles) < 150:  # Mínimo 6-7 dias de dados
                    log_aviso(f"Candles insuficientes para {moeda} (precisa 150, tem {len(candles)})")
                    continue
                
                # Usar todos os candles disponíveis
                candles = candles[-700:] if len(candles) >= 700 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 margem para garantir execução)
        if side == 'buy':
            preco_limite = preco_atual * 1.01  # 1% acima para compra (reduzido de 2% para melhorar notional)
        else:
            preco_limite = preco_atual * 0.99  # 1% abaixo para venda (reduzido de 2% para melhorar notional)
        
        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 float(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}"

def verificar_histerese_ativa(estado, moeda, contador_ciclo):
    """
    ✓ NOVO: Verifica se a histerese assimétrica está ativa para uma moeda
    
    Quando um trade superdimensionado ocorre (ex: venda de $5.20 por notional insuficiente),
    marca uma flag de histerese que força:
    - Gate MAIOR (0.25%) para a direção OPOSTA do último trade
    - Gate normal (0.17%) para a MESMA direção
    
    Args:
        estado: Estado do portfólio
        moeda: Símbolo da moeda
        contador_ciclo: Contador do ciclo atual
    
    Returns:
        tuple: (histerese_ativa, ultima_direcao_trade, ciclos_restantes)
               - histerese_ativa: bool (True se histerese está ativa)
               - ultima_direcao_trade: 'compra' ou 'venda' ou None
               - ciclos_restantes: int (ciclos até desativar histerese)
    """
    if 'histerese_asimetrica' not in estado:
        estado['histerese_asimetrica'] = {}
    
    histerese_data = estado['histerese_asimetrica'].get(moeda, {})
    
    if not histerese_data:
        return False, None, 0
    
    ciclo_inicio = histerese_data.get('ciclo_marcado', contador_ciclo)
    ultima_direcao = histerese_data.get('ultima_direcao_trade', None)
    ciclos_decorridos = contador_ciclo - ciclo_inicio
    ciclos_restantes = max(0, CICLOS_HISTERESE_ATIVA - ciclos_decorridos)
    
    if ciclos_restantes <= 0:
        # Histerese expirou, remover
        del estado['histerese_asimetrica'][moeda]
        return False, None, 0
    
    return True, ultima_direcao, ciclos_restantes

def obter_variacao_gate_com_histerese(moeda, estado, contador_ciclo, direcao_planejada):
    """
    ✓ NOVO: Obtém o gate de variação (threshold) considerando a histerese assimétrica
    
    Args:
        moeda: Símbolo da moeda
        estado: Estado do portfólio
        contador_ciclo: Contador do ciclo atual
        direcao_planejada: 'compra' ou 'venda' (a direção que o robô quer fazer AGORA)
    
    Returns:
        tuple: (gate_variacao, motivo, ciclos_restantes)
               - gate_variacao: float (e.g. 0.17 ou 0.25)
               - motivo: str (explicação do por quê)
               - ciclos_restantes: int (se histerese ativa)
    """
    histerese_ativa, ultima_direcao, ciclos_restantes = verificar_histerese_ativa(estado, moeda, contador_ciclo)
    
    if not histerese_ativa:
        return VARIACAO_ENTRADA_NORMAL, f"Gate Normal", 0
    
    # Histerese está ativa
    if direcao_planejada == ultima_direcao:
        # Mesma direção do último trade - usar gate NORMAL
        gate = VARIACAO_ENTRADA_NORMAL
        motivo = f"Histerese Ativa (MESMA direção): {direcao_planejada} novamente\n        → Gate Normal: {gate}% ({ciclos_restantes} ciclos restantes)"
    else:
        # Direção OPOSTA do último trade - usar gate MAIOR
        gate = VARIACAO_ENTRADA_OPOSTA_APOS_TRADE_FORCADO
        motivo = f"Histerese Ativa (DIREÇÃO OPOSTA): último={ultima_direcao}, agora={direcao_planejada}\n        → Gate Aumentado: {gate}% ({ciclos_restantes} ciclos restantes)"
    
    return gate, motivo, ciclos_restantes

def marcar_histerese_ativa(estado, moeda, direcao_trade, contador_ciclo):
    """
    ✓ NOVO: Marca que uma operação superdimensionada foi feita nesta moeda
    Ativa a histerese assimétrica para proteger contra ping-pong
    
    Args:
        estado: Estado do portfólio
        moeda: Símbolo da moeda
        direcao_trade: 'compra' ou 'venda'
        contador_ciclo: Contador do ciclo atual
    """
    if 'histerese_asimetrica' not in estado:
        estado['histerese_asimetrica'] = {}
    
    estado['histerese_asimetrica'][moeda] = {
        'ultima_direcao_trade': direcao_trade,
        'ciclo_marcado': contador_ciclo,
        'timestamp': datetime.now().isoformat()
    }
    
    log_aviso(f"🔒 Histerese Ativa para {moeda}: {direcao_trade.upper()}")
    log_info(f"   Próxima operação na direção OPOSTA precisará de gate {VARIACAO_ENTRADA_OPOSTA_APOS_TRADE_FORCADO}%")
    log_info(f"   Duração: {CICLOS_HISTERESE_ATIVA} ciclos")

def validar_notional_limite_ordem(quantidade, preco_atual, side, moeda):
    """
    Valida se o valor notional da ordem (com preço limite) atende ao mínimo da Binance.
    CRÍTICO: A Binance valida usando o preço da ordem, não o preço de mercado
    
    Args:
        quantidade: Quantidade a vender/comprar
        preco_atual: Preço atual de mercado
        side: 'buy' ou 'sell'
        moeda: Símbolo da moeda (para log)
    
    Returns:
        tuple: (é_válido, valor_notional, mensagem, preco_limite)
    """
    # Calcular preço limite (mesmo cálculo de executar_ordem)
    if side == 'buy':
        preco_limite = preco_atual * 1.01  # 1% acima para compra (reduzido de 2%)
    else:
        preco_limite = preco_atual * 0.99  # 1% abaixo para venda (reduzido de 2%)
    
    # Calcular valor notional da ordem (com preço limite)
    valor_notional = float(quantidade) * preco_limite
    
    if valor_notional < VALOR_MINIMO_OPERACAO:
        mensagem = (f"Notional insuficiente para {moeda} ({side.upper()}): "
                   f"${valor_notional:.2f} < ${VALOR_MINIMO_OPERACAO:.2f} | "
                   f"Qtd: {quantidade:.8f} × Preço Limite: ${preco_limite:.8f}")
        return False, valor_notional, mensagem, preco_limite
    
    return True, valor_notional, f"Notional OK: ${valor_notional:.2f}", preco_limite

def processar_residual_com_retry_inteligente(estado, moeda, diferenca_percentual, contador_ciclo, capital_rastreado=None, config=None):
    """
    ✓ NOVO: Gerencia moedas residuais com Retry Inteligente + Espera de Ciclos
    
    Estratégia INTELIGENTE:
    1. Primeira falha: Guardar estado + aguardar ciclos (deixar preço flutuar)
    2. Ciclo N+X: Tenta novamente com valor original
    3. Se falhar 2ª vez: Aguarda mais ciclos (backoff progressivo)
    4. Após N falhas consecutivas: Começa progressão de valores ($5.2 → $5.8 → $6.5...)
    5. Normalização: Se desequilíbrio normalizar, remove de residuais
    
    Args:
        estado: Estado do portfólio
        moeda: Símbolo da moeda que falhou
        diferenca_percentual: Desequilíbrio atual em %
        contador_ciclo: Contador do ciclo atual
        capital_rastreado: Capital total para cálculo de percentual
        config: Configurações do portfólio
    
    Returns:
        tuple: (deve_tentar_novamente, quantidade_ajustada, ciclos_skip)
               - deve_tentar_novamente: bool (True = tentar agora)
               - quantidade_ajustada: float (valor em USDT se deve_tentar_novamente=True)
               - ciclos_skip: int (ciclos a pular se deve_tentar_novamente=False)
    """
    if config is None:
        config = carregar_configuracoes('configuracoes_balanceamento.json')
    
    valor_minimo_inicial = float(config.get('valor_minimo_operacao_inicial', 5.2))
    ciclos_espera = int(config.get('ciclos_espera_notional_falha', 5))
    tentativas_max = int(config.get('tentativas_antes_progressao', 3))
    
    if 'moedas_residuais_historico' not in estado:
        estado['moedas_residuais_historico'] = {}
    
    residual_info = estado['moedas_residuais_historico'].get(moeda, {})
    
    # ✓ Verificar se desequilíbrio normalizou (< threshold)
    if moeda in estado['moedas_residuais_historico'] and diferenca_percentual < VARIACAO_MINIMA_NORMALIZACAO:
        log_sucesso(f"    ✅ {moeda}: Desequilíbrio NORMALIZADO ({diferenca_percentual:.2f}% < {VARIACAO_MINIMA_NORMALIZACAO}%)")
        log_info(f"    💚 Removendo de residuais - voltando ao rebalanceamento normal")
        del estado['moedas_residuais_historico'][moeda]
        return False, None, 0
    
    # ✓ Verificar se deve pular este ciclo (está em período de skip/espera)
    proxima_tentativa_ciclo = residual_info.get('proxima_tentativa_ciclo', contador_ciclo)
    if contador_ciclo < proxima_tentativa_ciclo:
        ciclos_ainda = proxima_tentativa_ciclo - contador_ciclo
        log_aviso(f"    ⏸️  {moeda}: Aguardando ciclos ({ciclos_ainda} ciclos ainda até ciclo {proxima_tentativa_ciclo})")
        return False, None, 0
    
    # ✓ PRIMEIRA FALHA: Guardar e aguardar ciclos
    if 'tentativas_falhadas' not in residual_info:
        log_aviso(f"    🔴 PRIMEIRA FALHA: {moeda}")
        log_info(f"    � Config carregada:")
        log_info(f"       - Valor mínimo inicial: ${valor_minimo_inicial:.2f} USD")
        log_info(f"       - Ciclos espera: {ciclos_espera}")
        log_info(f"       - Tentativas antes progressão: {tentativas_max}")
        log_info(f"    💤 Aguardando {ciclos_espera} ciclos para deixar preço flutuar naturalmente (evitar spam)")
        estado['moedas_residuais_historico'][moeda] = {
            'motivo': 'notional_insuficiente_ciclo_venda',
            'timestamp': datetime.now().isoformat(),
            'desequilibrio_inicial': diferenca_percentual,
            'tentativas_falhadas': 1,
            'ciclos_espera_inicial': ciclos_espera,
            'ciclo_marcado': contador_ciclo,
            'proxima_tentativa_ciclo': contador_ciclo + ciclos_espera,
            'valor_testado': None,
            'historico_testes': []
        }
        log_info(f"    ⏳ Estado registrado em moedas_residuais_historico")
        log_info(f"    ⏳ Próxima tentativa: ciclo {contador_ciclo + ciclos_espera}")
        return False, None, ciclos_espera
    
    # ✓ PRÓXIMAS TENTATIVAS: Incrementar contador e verificar se começar progressão
    tentativas = residual_info.get('tentativas_falhadas', 0)
    valor_testado_anterior = residual_info.get('valor_testado', None)
    
    log_info(f"    ♻️  {moeda}: Tentativa {tentativas} (continuando da falha anterior)")
    
    # ✓ VERIFICAR se há valor agendado esperando para ser testado
    historico = residual_info.get('historico_testes', [])
    if len(historico) > 0 and historico[-1]['resultado'] == 'agendada':
        # ✅ Hora de tentar o valor agendado!
        valor_agendado = historico[-1]['valor']
        valor_venda_ajustado = valor_agendado / 0.98
        log_sucesso(f"    ✅ {moeda}: TENTANDO valor agendado ${valor_agendado:.2f} USD")
        
        # Marcar como em teste
        historico[-1]['resultado'] = 'em_teste'
        historico[-1]['ciclo_teste'] = contador_ciclo
        
        # Marcar tentativa anterior como falha se não foi bem sucedida
        if valor_testado_anterior is not None and len(historico) > 1:
            if historico[-2]['resultado'] != 'sucesso':
                historico[-2]['resultado'] = 'falha'
                log_aviso(f"    ❌ Tentativa anterior com ${valor_testado_anterior:.2f} USD falhou no notional")
        
        return True, valor_venda_ajustado, 0  # ✅ TENTAR AGORA com valor agendado
    
    # Marcar tentativa anterior como falha no histórico
    if valor_testado_anterior is not None and 'historico_testes' in residual_info:
        historico = residual_info['historico_testes']
        if len(historico) > 0 and historico[-1]['resultado'] == 'em_teste':
            historico[-1]['resultado'] = 'falha'
            log_aviso(f"    ❌ Tentativa anterior com ${valor_testado_anterior:.2f} USD falhou no notional")
    
    # ✓ ESTRATÉGIA REVISADA: Progressão automática de valores sem esperar ciclos
    # Se falhou com $10, tenta $12, depois $15, $18, etc
    valores_progression = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
    
    # ✓ NOVO: Se completou toda a progressão (tentativas >= len), REINICIAR com pausa
    if tentativas >= len(valores_progression):
        ciclos_pause_apos_max = ciclos_espera  # ✅ Usar valor já carregado, não novo default
        log_aviso(f"    🔄 CICLO COMPLETO: Já tentou todos os {len(valores_progression)} valores")
        log_aviso(f"    💤 REINICIANDO: Aguardando {ciclos_pause_apos_max} ciclos antes de recomeçar do menor valor")
        
        # Resetar tentativas para 0 (recomeça da menor)
        estado['moedas_residuais_historico'][moeda]['tentativas_falhadas'] = 0
        estado['moedas_residuais_historico'][moeda]['proxima_tentativa_ciclo'] = contador_ciclo + ciclos_pause_apos_max
        salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
        
        return False, None, ciclos_pause_apos_max
    
    # Mapeamento: tentativa N → índice na progressão
    indice_progressao = min(tentativas - 1, len(valores_progression) - 1)
    valor_para_testar = valores_progression[indice_progressao]
    
    log_info(f"    ♻️  {moeda}: Tentativa {tentativas}/{len(valores_progression)}")
    log_info(f"    📈 Escalando para ${valor_para_testar:.2f} USD (índice {indice_progressao})")
    
    # ✓ Ajustar pela margem de preço (0.98 para venda)
    valor_venda_ajustado = valor_para_testar / 0.98
    log_info(f"    🔢 Valor-alvo: ${valor_para_testar:.2f} USD")
    log_info(f"    🔢 Valor ajustado (÷0.98): ${valor_venda_ajustado:.2f} USD")
    
    # ✓ AGUARDAR ciclos_espera ENTRE cada tentativa de progressão
    log_info(f"    ⏳ Tentativa {tentativas} será executada no próximo ciclo após {ciclos_espera} espera")
    log_info(f"    💤 Marcando para tentar ${valor_para_testar:.2f} USD no ciclo {contador_ciclo + ciclos_espera}")
    
    # ✓ Registrar tentativa no histórico
    tentativa_record = {
        'valor': valor_para_testar,
        'ciclo': contador_ciclo,
        'timestamp': datetime.now().isoformat(),
        'resultado': 'agendada'  # Agendada para próximo ciclo
    }
    estado['moedas_residuais_historico'][moeda]['historico_testes'].append(tentativa_record)
    
    # ✓ Atualizar rastreadores
    estado['moedas_residuais_historico'][moeda]['valor_testado'] = valor_para_testar
    estado['moedas_residuais_historico'][moeda]['tentativas_falhadas'] = tentativas + 1
    # ✅ IMPORTANTE: Aguardar ciclos_espera ENTRE tentativas
    estado['moedas_residuais_historico'][moeda]['proxima_tentativa_ciclo'] = contador_ciclo + ciclos_espera
    salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
    
    return False, None, ciclos_espera  # Aguardar ciclos_espera antes de tentar

# ===================== 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 extrair_hora_media_compra(estado, moeda):
    """
    Extrai a hora média de compra do estado do portfólio
    
    Args:
        estado: Estado atual do portfólio
        moeda: Símbolo da moeda
    
    Returns:
        tuple: (hora_media, minuto_medio) ou (None, None) se não encontrar
        Exemplo: (9, 30) para 09:30
    """
    try:
        if 'best_prices_30days' not in estado or moeda not in estado['best_prices_30days']:
            return None, None
        
        dados_moeda = estado['best_prices_30days'][moeda]
        if 'melhor_hora_compra' not in dados_moeda or 'hora_media' not in dados_moeda['melhor_hora_compra']:
            return None, None
        
        hora_media_str = dados_moeda['melhor_hora_compra']['hora_media']  # Ex: '09:30'
        partes = hora_media_str.split(':')
        if len(partes) == 2:
            hora = int(partes[0])
            minuto = int(partes[1])
            return hora, minuto
        
        return None, None
    except Exception as e:
        log_aviso(f"Erro ao extrair hora média de compra para {moeda}: {e}")
        return None, None

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 usando hora média inteligente
    
    Args:
        moeda: Símbolo da moeda
        estado: Estado atual do portfólio
        tempo_maximo_minutos: Tempo máximo de espera em minutos (None = 30 min como padrão)
    
    Returns:
        tuple: (aguardou, tempo_esperado_minutos, mensagem)
    """
    # Usar 30 minutos como padrão (máximo até a hora média + 5 min de margem)
    if tempo_maximo_minutos is None:
        tempo_maximo_minutos = 30
    
    hora_inicio, hora_fim, confianca = extrair_horario_compra(estado, moeda)
    hora_media, minuto_media = extrair_hora_media_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"
    
    agora = datetime.now(tz=timezone)
    hora_atual = agora.hour
    minuto_atual = agora.minute
    tempo_inicial = time.time()
    intervalo_verificacao = 10  # Verificar a cada 10 segundos
    proximo_log = 0  # Para controlar logs periódicos
    
    # Calcular decisão inteligente baseada em hora média
    try:
        # 1️⃣ Verificar se já está dentro do período ideal
        if hora_inicio <= hora_fim:
            # Período normal (ex: 08:00-11:59)
            esta_no_periodo = hora_inicio <= hora_atual <= hora_fim
        else:
            # Período que passa pela meia-noite
            esta_no_periodo = hora_atual >= hora_inicio or hora_atual <= hora_fim
        
        # Calcular tempo até hora média (em minutos)
        if hora_media is not None:
            tempo_ate_media = (hora_media * 60 + minuto_media) - (hora_atual * 60 + minuto_atual)
            if tempo_ate_media < 0:
                tempo_ate_media += 24 * 60  # Próximo dia
        else:
            tempo_ate_media = None
        
        # 2️⃣ DECISÃO INTELIGENTE
        if esta_no_periodo:
            # ✅ JÁ ESTÁ DENTRO DO PERÍODO IDEAL
            if hora_media is not None:
                if hora_atual == hora_media and minuto_atual <= minuto_media:
                    # Ainda não chegou na hora média, mas está no período - pode esperar um pouco
                    if tempo_ate_media is not None and tempo_ate_media <= 30:
                        log_info(f"{moeda}: Já está no período ideal, mas faltam {tempo_ate_media:.0f}min para hora média. Aguardando...")
                        # Aguardar apenas até hora média
                        tempo_decorrido_segundos = 0
                        while tempo_decorrido_segundos < tempo_ate_media * 60:
                            tempo_decorrido_segundos = time.time() - tempo_inicial
                            
                            # Verificar a cada 10 segundos
                            if tempo_decorrido_segundos >= proximo_log:
                                tempo_faltando = tempo_ate_media * 60 - tempo_decorrido_segundos
                                log_info(f"{moeda}: Aguardando hora média... ({tempo_decorrido_segundos/60:.1f}min decorridos, {tempo_faltando/60:.1f}min faltam até {hora_media:02d}:{minuto_media:02d})")
                                proximo_log = tempo_decorrido_segundos + 120  # Log a cada 2 min
                            
                            time.sleep(intervalo_verificacao)
                        
                        log_sucesso(f"{moeda}: ✓ Hora média de compra atingida ({hora_media:02d}:{minuto_media:02d}, confiança: {confianca:.1f}%)")
                        return True, tempo_decorrido_segundos / 60, f"{moeda}: Hora média atingida"
                    else:
                        # Hora média é na próxima janela, compra agora no período atual
                        log_sucesso(f"{moeda}: ✓ Horário ideal de compra! ({hora_inicio:02d}:00-{hora_fim:02d}:59, confiança: {confianca:.1f}%)")
                        return True, 0, f"{moeda}: Já está no horário ideal de compra"
                else:
                    # Já passou a hora média - compra imediatamente
                    log_sucesso(f"{moeda}: ✓ Horário ideal de compra! (Passou hora média {hora_media:02d}:{minuto_media:02d}, confiança: {confianca:.1f}%)")
                    return True, 0, f"{moeda}: Já está no horário ideal de compra (passou hora média)"
            else:
                # Sem hora média, mas está no período - compra
                log_sucesso(f"{moeda}: ✓ Horário ideal de compra! ({hora_inicio:02d}:00-{hora_fim:02d}:59, confiança: {confianca:.1f}%)")
                return True, 0, f"{moeda}: Já está no horário ideal de compra"
        else:
            # ⏳ NÃO ESTÁ NO PERÍODO IDEAL - AGUARDAR
            if tempo_ate_media is not None:
                tempo_ate_media_msg = f"{tempo_ate_media:.0f}min até hora média ({hora_media:02d}:{minuto_media:02d})"
            else:
                tempo_ate_media_msg = f"próximo período é {hora_inicio:02d}:00"
            
            # ✓ NOVO: Se a próxima janela ideal está muito distante (> tempo_maximo), pular compra
            # Retorna None para indicar "não compre agora, tente no próximo ciclo"
            if tempo_ate_media is not None and tempo_ate_media > tempo_maximo_minutos:
                log_aviso(f"{moeda}: ⏳ Próxima janela ideal está muito distante ({tempo_ate_media:.0f}min > {tempo_maximo_minutos}min). Pulando compra neste ciclo.")
                return None, 0, f"{moeda}: Pulando - próximo horário ideal muito distante"
            
            log_info(f"{moeda}: Aguardando melhor hora de compra... ({tempo_ate_media_msg})")
            
            tempo_decorrido_segundos = 0
            
            while True:
                # Verificar se chegou no período ideal
                agora = datetime.now(tz=timezone)
                hora_atual = agora.hour
                minuto_atual = agora.minute
                
                if hora_inicio <= hora_fim:
                    esta_dentro = hora_inicio <= hora_atual <= hora_fim
                else:
                    esta_dentro = hora_atual >= hora_inicio or hora_atual <= hora_fim
                
                if esta_dentro:
                    tempo_decorrido_minutos = round(tempo_decorrido_segundos / 60, 1)
                    log_sucesso(f"{moeda}: ✓ Horário ideal de compra atingido após {tempo_decorrido_minutos}min (confiança: {confianca:.1f}%)")
                    return True, tempo_decorrido_minutos, f"{moeda}: Horário ideal atingido"
                
                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_restante = tempo_maximo_minutos - tempo_decorrido_minutos
                    log_info(f"{moeda}: Aguardando ({tempo_decorrido_minutos:.1f}/{tempo_maximo_minutos}min, {tempo_restante:.1f}min restante)")
                    proximo_log = tempo_decorrido_minutos + 2
                
                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 DE OTIMIZAÇÃO DE TIMING - VENDA =====================

def extrair_horario_venda(estado, moeda):
    """
    Extrai a melhor hora de venda 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: (14, 17, 55.2) para o período 14:00-17: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_venda' not in dados_moeda:
            return None, None, 0
        
        melhor_hora = dados_moeda['melhor_hora_venda']['hora']  # Ex: '14:00-17:59'
        confianca = dados_moeda['melhor_hora_venda']['confianca']
        
        # Extrair horas do formato '14:00-17: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 venda para {moeda}: {e}")
        return None, None, 0

def extrair_hora_media_venda(estado, moeda):
    """
    Extrai a hora média de venda do estado do portfólio
    
    Args:
        estado: Estado atual do portfólio
        moeda: Símbolo da moeda
    
    Returns:
        tuple: (hora_media, minuto_medio) ou (None, None) se não encontrar
        Exemplo: (15, 45) para 15:45
    """
    try:
        if 'best_prices_30days' not in estado or moeda not in estado['best_prices_30days']:
            return None, None
        
        dados_moeda = estado['best_prices_30days'][moeda]
        if 'melhor_hora_venda' not in dados_moeda or 'hora_media' not in dados_moeda['melhor_hora_venda']:
            return None, None
        
        hora_media_str = dados_moeda['melhor_hora_venda']['hora_media']  # Ex: '15:45'
        partes = hora_media_str.split(':')
        if len(partes) == 2:
            hora = int(partes[0])
            minuto = int(partes[1])
            return hora, minuto
        
        return None, None
    except Exception as e:
        log_aviso(f"Erro ao extrair hora média de venda para {moeda}: {e}")
        return None, None

def eh_horario_venda_ideal(moeda, estado):
    """
    Verifica se a hora atual está dentro do melhor período de venda 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_venda(estado, moeda)
    
    if hora_inicio is None:
        return False, f"{moeda}: Dados de melhor hora de venda 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: 14:00-17:59)
        esta_dentro = hora_inicio <= hora_atual <= hora_fim
    else:
        # Período que passa pela meia-noite (ex: 20:00-03:59) - pode acontecer em venda
        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 venda! ({periodo_str}, confiança: {confianca:.1f}%)"
        return True, mensagem
    else:
        proxima_hora_venda = f"{hora_inicio:02d}:00"
        mensagem = f"{moeda}: ⏰ Aguardando melhor hora de venda ({periodo_str}, confiança: {confianca:.1f}%). Próxima às {proxima_hora_venda}"
        return False, mensagem

def calcular_tempo_ate_melhor_hora_venda(moeda, estado):
    """
    Calcula quantos minutos faltam até a melhor hora de venda
    
    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_venda(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_fim:
        # Período normal
        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
    else:
        # Período que passa pela meia-noite (ex: 20:00-03:59)
        if hora_atual >= hora_inicio or hora_atual <= hora_fim:
            # Já está na melhor hora
            return 0
        
        # Próxima melhor hora é hoje
        tempo_ate = (hora_inicio - hora_atual) * 60 - minuto_atual
    
    return max(0, tempo_ate)

def aguardar_melhor_hora_venda(moeda, estado, tempo_maximo_minutos=None):
    """
    Aguarda até a melhor hora de venda para a moeda usando hora média inteligente
    Útil para retirada de capital e rebalanceamento com otimização de preço
    
    Args:
        moeda: Símbolo da moeda
        estado: Estado atual do portfólio
        tempo_maximo_minutos: Tempo máximo de espera em minutos (None = 30 min como padrão)
    
    Returns:
        tuple: (aguardou, tempo_esperado_minutos, mensagem)
    """
    # Usar 30 minutos como padrão (máximo até a hora média + 5 min de margem)
    if tempo_maximo_minutos is None:
        tempo_maximo_minutos = 30
    
    hora_inicio, hora_fim, confianca = extrair_horario_venda(estado, moeda)
    hora_media, minuto_media = extrair_hora_media_venda(estado, moeda)
    
    if hora_inicio is None:
        log_aviso(f"{moeda}: Análise de melhor hora de venda não disponível - prosseguindo com venda imediata")
        return False, 0, f"{moeda}: Análise de melhor hora de venda não disponível"
    
    agora = datetime.now(tz=timezone)
    hora_atual = agora.hour
    minuto_atual = agora.minute
    tempo_inicial = time.time()
    intervalo_verificacao = 10  # Verificar a cada 10 segundos
    proximo_log = 0  # Para controlar logs periódicos
    
    # Calcular decisão inteligente baseada em hora média
    try:
        # 1️⃣ Verificar se já está dentro do período ideal
        if hora_inicio <= hora_fim:
            # Período normal (ex: 14:00-17:59)
            esta_no_periodo = hora_inicio <= hora_atual <= hora_fim
        else:
            # Período que passa pela meia-noite
            esta_no_periodo = hora_atual >= hora_inicio or hora_atual <= hora_fim
        
        # Calcular tempo até hora média (em minutos)
        if hora_media is not None:
            tempo_ate_media = (hora_media * 60 + minuto_media) - (hora_atual * 60 + minuto_atual)
            if tempo_ate_media < 0:
                tempo_ate_media += 24 * 60  # Próximo dia
        else:
            tempo_ate_media = None
        
        # 2️⃣ DECISÃO INTELIGENTE
        if esta_no_periodo:
            # ✅ JÁ ESTÁ DENTRO DO PERÍODO IDEAL
            if hora_media is not None:
                if hora_atual == hora_media and minuto_atual <= minuto_media:
                    # Ainda não chegou na hora média, mas está no período - pode esperar um pouco
                    if tempo_ate_media is not None and tempo_ate_media <= 30:
                        log_info(f"{moeda}: Já está no período ideal, mas faltam {tempo_ate_media:.0f}min para hora média. Aguardando...")
                        # Aguardar apenas até hora média
                        tempo_decorrido_segundos = 0
                        while tempo_decorrido_segundos < tempo_ate_media * 60:
                            tempo_decorrido_segundos = time.time() - tempo_inicial
                            
                            # Verificar a cada 10 segundos
                            if tempo_decorrido_segundos >= proximo_log:
                                tempo_faltando = tempo_ate_media * 60 - tempo_decorrido_segundos
                                log_info(f"{moeda}: Aguardando hora média... ({tempo_decorrido_segundos/60:.1f}min decorridos, {tempo_faltando/60:.1f}min faltam até {hora_media:02d}:{minuto_media:02d})")
                                proximo_log = tempo_decorrido_segundos + 120  # Log a cada 2 min
                            
                            time.sleep(intervalo_verificacao)
                        
                        log_sucesso(f"{moeda}: ✓ Hora média de venda atingida ({hora_media:02d}:{minuto_media:02d}, confiança: {confianca:.1f}%)")
                        return True, tempo_decorrido_segundos / 60, f"{moeda}: Hora média atingida"
                    else:
                        # Hora média é na próxima janela, vende agora no período atual
                        log_sucesso(f"{moeda}: ✓ Horário ideal de venda! ({hora_inicio:02d}:00-{hora_fim:02d}:59, confiança: {confianca:.1f}%)")
                        return True, 0, f"{moeda}: Já está no horário ideal de venda"
                else:
                    # Já passou a hora média - vende imediatamente
                    log_sucesso(f"{moeda}: ✓ Horário ideal de venda! (Passou hora média {hora_media:02d}:{minuto_media:02d}, confiança: {confianca:.1f}%)")
                    return True, 0, f"{moeda}: Já está no horário ideal de venda (passou hora média)"
            else:
                # Sem hora média, mas está no período - vende
                log_sucesso(f"{moeda}: ✓ Horário ideal de venda! ({hora_inicio:02d}:00-{hora_fim:02d}:59, confiança: {confianca:.1f}%)")
                return True, 0, f"{moeda}: Já está no horário ideal de venda"
        else:
            # ⏳ NÃO ESTÁ NO PERÍODO IDEAL - AGUARDAR
            if tempo_ate_media is not None:
                tempo_ate_media_msg = f"{tempo_ate_media:.0f}min até hora média ({hora_media:02d}:{minuto_media:02d})"
            else:
                tempo_ate_media_msg = f"próximo período é {hora_inicio:02d}:00"
            
            # ✓ NOVO: Se a próxima janela ideal está muito distante (> tempo_maximo), pular venda
            # Retorna None para indicar "não venda agora, tente no próximo ciclo"
            if tempo_ate_media is not None and tempo_ate_media > tempo_maximo_minutos:
                log_aviso(f"{moeda}: ⏳ Próxima janela ideal está muito distante ({tempo_ate_media:.0f}min > {tempo_maximo_minutos}min). Pulando venda neste ciclo.")
                return None, 0, f"{moeda}: Pulando - próximo horário ideal muito distante"
            
            log_info(f"{moeda}: Aguardando melhor hora de venda... ({tempo_ate_media_msg})")
            
            tempo_decorrido_segundos = 0
            
            while True:
                # Verificar se chegou no período ideal
                agora = datetime.now(tz=timezone)
                hora_atual = agora.hour
                minuto_atual = agora.minute
                
                if hora_inicio <= hora_fim:
                    esta_dentro = hora_inicio <= hora_atual <= hora_fim
                else:
                    esta_dentro = hora_atual >= hora_inicio or hora_atual <= hora_fim
                
                if esta_dentro:
                    tempo_decorrido_minutos = round(tempo_decorrido_segundos / 60, 1)
                    log_sucesso(f"{moeda}: ✓ Horário ideal de venda atingido após {tempo_decorrido_minutos}min (confiança: {confianca:.1f}%)")
                    return True, tempo_decorrido_minutos, f"{moeda}: Horário ideal atingido"
                
                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 venda.")
                    return False, tempo_decorrido_minutos, f"{moeda}: Timeout de espera atingido"
                
                # Log a cada 2 minutos
                if tempo_decorrido_minutos >= proximo_log:
                    tempo_restante = tempo_maximo_minutos - tempo_decorrido_minutos
                    log_info(f"{moeda}: Aguardando ({tempo_decorrido_minutos:.1f}/{tempo_maximo_minutos}min, {tempo_restante:.1f}min restante)")
                    proximo_log = tempo_decorrido_minutos + 2
                
                time.sleep(intervalo_verificacao)
    
    except Exception as e:
        log_erro(f"Erro ao aguardar melhor hora de venda para {moeda}: {e}")
        return False, 0, f"{moeda}: Erro na espera - prosseguindo com venda"

# ===================== FUNÇÕES DE RESET =====================

def desativar_reset_apos_limpeza():
    """
    ✓ NOVO: Desativa automaticamente o reset após ser executado com sucesso
    Evita rodar limpeza desnecessária no próximo ciclo
    - Desativa flag reset_portfolio_ao_iniciar na configuração
    - Deleta portfolio_estado.json para forçar recriação com valores corretos
    """
    try:
        config_file = 'configuracoes_balanceamento.json'
        with open(config_file, 'r', encoding='utf-8') as f:
            config = json.load(f)
        
        if config.get('reset_portfolio_ao_iniciar') == True:
            config['reset_portfolio_ao_iniciar'] = False
            
            with open(config_file, 'w', encoding='utf-8') as f:
                json.dump(config, f, indent=2, ensure_ascii=False)
            
            # ✓ NOVO: Deletar arquivo de estado para recriação com histórico correto
            try:
                if os.path.exists(PORTFOLIO_STATE_FILE):
                    os.remove(PORTFOLIO_STATE_FILE)
                    log_sucesso(f"✓ Arquivo {PORTFOLIO_STATE_FILE} deletado para recriação")
            except Exception as e:
                log_aviso(f"⚠ Não foi possível deletar {PORTFOLIO_STATE_FILE}: {e}")
            
            log_sucesso("✓ Reset desativado automaticamente nas configurações")
            return True
    except Exception as e:
        log_aviso(f"⚠ Não foi possível desativar reset na config: {e}")
    
    return False

def limpar_moedas_residuais(moedas):
    """
    ✓ NOVO: Vende todas as moedas residuais para limpar o portfólio
    Útil quando "reset_portfolio_ao_iniciar" está ativado
    
    Args:
        moedas: Lista de moedas do portfólio
    
    Returns:
        float: Saldo USDT total após limpar tudo (deve ser ~2000)
    """
    if not RESET_PORTFOLIO_AO_INICIAR:
        return 0
    
    log_aviso("\n" + "="*60)
    log_aviso("🧹 MODO RESET ATIVADO - LIMPANDO PORTFÓLIO COMPLETAMENTE")
    log_aviso("="*60)
    log_info(f"\n📋 Objetivo: Resetar para capital inicial de ${CAPITAL_INICIAL:.2f} USDT")
    
    precos = {m: obter_preco_atual(f"{m}/{MOEDA_BASE}") for m in moedas}
    saldos = obter_saldos_portfolio(moedas)
    
    total_usdt_recuperado = 0
    moedas_vendidas = 0
    
    for moeda in moedas:
        saldo = saldos.get(moeda, 0)
        preco = precos.get(moeda, 0)
        
        # Vender qualquer saldo residual > 0.00001
        if saldo > 0.00001 and preco > 0:
            # ✓ Converter Decimal para float para operação com preco (float)
            valor_venda = float(saldo) * preco
            
            log_aviso(f"\n🔄 Vendendo {moeda} residual:")
            log_info(f"   Saldo: {float(saldo):.8f} {moeda}")
            log_info(f"   Preço: ${preco:.8f}")
            log_info(f"   Valor: ${valor_venda:.2f}")
            
            symbol = f"{moeda}/{MOEDA_BASE}"
            
            try:
                # Tentar vender com 1% de margem
                preco_limite = preco * 0.99  # 1% abaixo para garantir venda
                
                ordem = exchange.create_limit_order(
                    symbol,
                    'sell',
                    saldo,
                    preco_limite,
                    {'timeInForce': 'IOC'}  # Immediate or Cancel
                )
                
                if ordem:
                    time.sleep(1)
                    saldo_depois = obter_saldo(moeda)
                    
                    if saldo_depois < 0.00001:
                        log_sucesso(f"   ✓ {moeda} vendido com sucesso!")
                        total_usdt_recuperado += valor_venda
                        moedas_vendidas += 1
                    else:
                        log_aviso(f"   ⚠ Venda parcial de {moeda} (restam {float(saldo_depois):.8f})")
            except Exception as e:
                log_erro(f"   ✗ Erro ao vender {moeda}: {e}")
    
    # Obter saldo final USDT
    saldo_usdt_final = obter_saldo(MOEDA_BASE)
    
    # ✓ NOVO: Se houver USDT residual acima do capital inicial, avisar
    # ✓ Converter Decimal para float para evitar TypeError em operação aritmética
    saldo_usdt_final_float = float(saldo_usdt_final) if saldo_usdt_final is not None else 0
    saldo_usdt_residual = max(0, saldo_usdt_final_float - CAPITAL_INICIAL)
    
    log_info("\n" + "="*60)
    log_sucesso(f"✓ RESET CONCLUÍDO: {moedas_vendidas} moedas vendidas")
    log_info(f"  Valor recuperado: ${total_usdt_recuperado:.2f}")
    log_info(f"  Saldo USDT final: ${saldo_usdt_final:.2f}")
    log_info(f"  Capital Inicial: ${CAPITAL_INICIAL:.2f}")
    
    if saldo_usdt_residual > 50:  # Só avisar se residual > $50
        log_aviso(f"\n  ⚠️ AVISO: Há ${saldo_usdt_residual:.2f} em USDT residual!")
        log_aviso(f"  Este valor NÃO foi reinvestido na fase anterior.")
        log_info(f"  O robô usará exatamente ${CAPITAL_INICIAL:.2f} + reinvestimentos.")
    else:
        log_sucesso(f"  ✓ Portfólio limpo com sucesso!")
    
    log_info("="*60 + "\n")
    
    # ✓ NOVO: Desativar reset na configuração após sucesso
    desativar_reset_apos_limpeza()
    
    return saldo_usdt_final_float

# ===================== 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)}
    
    # ✓ NOVO: Se modo reset está ativado, limpar moedas residuais primeiro
    if RESET_PORTFOLIO_AO_INICIAR:
        saldo_final = limpar_moedas_residuais(moedas)
        log_info(f"Após reset: Capital disponível = ${saldo_final:.2f}")
        time.sleep(2)  # Aguardar processamento
    
    estado_existente = carregar_estado_portfolio(PORTFOLIO_STATE_FILE)
    
    if estado_existente:
        log_info("Carregando estado anterior do portfólio...")
        
        # ✓ NOVO: Verificar discrepâncias na carteira antes de usar o estado
        saldos_atuais = obter_saldos_portfolio(moedas)
        discrepancias = verificar_discrepancia_carteira(estado_existente, moedas, saldos_atuais)
        
        if discrepancias:
            log_aviso("\n⚠️ O robô detectou discrepâncias entre as quantidades salvas e as atuais.")
            log_aviso("   Isso pode ocorrer se:")
            log_aviso("   • O robô foi parado abruptamente")
            log_aviso("   • Houve transações manuais na exchange durante a parada")
            log_aviso("   • Problemas de sincronização com cálculos de ganhos")
            log_info("\n   → Usando as quantidades ATUAIS da exchange como referência")
        
        # ✓ NOVO: Detectar incremento E DIMINUIÇÃO de capital
        capital_rastreado = estado_existente.get('capital_rastreado', estado_existente.get('capital_inicial', 0))
        
        # ✅ INCREMENTO DE CAPITAL
        if CAPITAL_INICIAL > capital_rastreado + 100:  # Incremento maior que $100
            # Houve incremento significativo de capital
            incremento = CAPITAL_INICIAL - capital_rastreado
            log_aviso(f"\n💰 INCREMENTO DE CAPITAL DETECTADO!")
            log_aviso(f"   • Capital Anterior: ${capital_rastreado:.2f}")
            log_aviso(f"   • Capital Atual: ${CAPITAL_INICIAL:.2f}")
            log_aviso(f"   • Incremento: ${incremento:.2f}")
            log_aviso(f"   ⏰ Aguardando melhor hora de compra para investir novo capital...")
            
            estado_existente['capital_inicial'] = CAPITAL_INICIAL
            estado_existente['capital_rastreado'] = CAPITAL_INICIAL  # Atualizar rastreamento
            estado_existente['em_incremento_capital'] = True  # Flag para aguardar melhor hora
            estado_existente['data_incremento_capital'] = datetime.now(timezone).isoformat()
            
            # ✓ CRÍTICO: Salvar estado IMEDIATAMENTE após detectar incremento
            # Se não salvar aqui, o incremento será perdido se robô parar antes de balancear
            salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado_existente)
            log_info("✓ Estado com incremento de capital SALVO no disco")
        
        # 🆕 DIMINUIÇÃO DE CAPITAL
        elif CAPITAL_INICIAL < capital_rastreado - 100:  # Diminuição maior que $100
            # Houve diminuição significativa de capital
            diminuicao = capital_rastreado - CAPITAL_INICIAL
            log_aviso(f"\n💸 DIMINUIÇÃO DE CAPITAL DETECTADA!")
            log_aviso(f"   • Capital Anterior: ${capital_rastreado:.2f}")
            log_aviso(f"   • Capital Atual: ${CAPITAL_INICIAL:.2f}")
            log_aviso(f"   • Diminuição: ${diminuicao:.2f}")
            log_aviso(f"   ⏰ Iniciando retirada de capital - serão vendidas moedas para retirar ${diminuicao:.2f}...")
            
            estado_existente['capital_inicial'] = CAPITAL_INICIAL
            estado_existente['capital_rastreado'] = CAPITAL_INICIAL  # Atualizar rastreamento
            estado_existente['em_retirada_capital'] = True  # Flag para modo retirada
            estado_existente['valor_retirada'] = diminuicao  # Valor a retirar
            estado_existente['data_retirada_capital'] = datetime.now(timezone).isoformat()
            
            # ✓ CRÍTICO: Salvar estado IMEDIATAMENTE após detectar diminuição
            salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado_existente)
            log_info("✓ Estado com diminuição de capital SALVO no disco")
        
        else:
            # Sem mudança significativa, volta ao modo normal
            estado_existente['capital_rastreado'] = CAPITAL_INICIAL
            
            if estado_existente.get('em_incremento_capital', False):
                log_sucesso("✓ Incremento de capital concluído! Voltando ao rebalanceamento normal.")
                estado_existente['em_incremento_capital'] = False
            
            if estado_existente.get('em_retirada_capital', False):
                log_sucesso("✓ Retirada de capital concluída! Voltando ao rebalanceamento normal.")
                estado_existente['em_retirada_capital'] = False
            
            # ✓ Salvar conclusão
            if estado_existente.get('em_incremento_capital') is False or estado_existente.get('em_retirada_capital') is False:
                salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado_existente)
        
        # ✓ GARANTIR que best_prices_30days está populado (importante para aguardar_melhor_hora)
        # Isso é necessário quando o capital aumenta e o robô é reiniciado
        if 'best_prices_30days' not in estado_existente or not estado_existente['best_prices_30days']:
            log_aviso("Atualizando dados de melhor hora de compra antes de iniciar...")
            estado_existente = atualizar_melhores_precos_portfolio(estado_existente, moedas)
        
        # ✓ NOVO: Sincronizar quantidades reais da carteira
        saldos_atuais = obter_saldos_portfolio(moedas)
        atualizar_quantidade_carteira_real(PORTFOLIO_STATE_FILE, moedas, saldos_atuais)
        
        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,
            'capital_rastreado': CAPITAL_INICIAL,  # ✓ NOVO: Rastrear capital para detectar incrementos
            'em_incremento_capital': False,  # ✓ NOVO: Flag para modo incremento de capital
            'em_retirada_capital': False,  # 🆕 NOVO: Flag para modo retirada de capital
            'valor_retirada': 0,  # 🆕 NOVO: Valor a retirar
            'moedas_compradas': moedas.copy(),  # Todas já existem
            'moedas_pendentes': [],
            'percentuais_alvo': percentuais_alvo,
            'precos_compra': precos_compra,
            # ✓ REFATORADO: Histórico de trades agora apenas no SQLite, não em JSON
        }
        log_info("✓ Fase de Balanceamento iniciada (portfólio existente)")
        
        # ✓ NOVO: Sincronizar quantidades reais da carteira
        saldos_atuais = obter_saldos_portfolio(moedas)
        atualizar_quantidade_carteira_real(PORTFOLIO_STATE_FILE, moedas, saldos_atuais)
        
        return estado, moedas
    
    # Novo portfólio (sem moedas no testnet)
    estado = {
        'fase': 'construcao',
        'data_inicio': datetime.now(timezone).isoformat(),
        'capital_inicial': CAPITAL_INICIAL,
        'capital_rastreado': CAPITAL_INICIAL,  # ✓ NOVO: Rastrear capital para detectar incrementos
        'em_incremento_capital': False,  # ✓ NOVO: Flag para modo incremento de capital
        'em_retirada_capital': False,  # 🆕 NOVO: Flag para modo retirada de capital
        'valor_retirada': 0,  # 🆕 NOVO: Valor a retirar
        'moedas_compradas': [],
        'moedas_pendentes': moedas.copy(),
        'percentuais_alvo': percentuais_alvo,
        'precos_compra': precos_compra,
        # ✓ REFATORADO: Histórico de trades agora apenas no SQLite, não em JSON
    }
    
    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)
    
    # ✓ NOVO: Inicializar quantidade_carteira_real no novo portfólio
    saldos_atuais = obter_saldos_portfolio(moedas)
    atualizar_quantidade_carteira_real(PORTFOLIO_STATE_FILE, moedas, saldos_atuais)
    
    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 ==========")
    
    # ✓ GARANTIR que best_prices_30days está populado (importante para aguardar_melhor_hora)
    if 'best_prices_30days' not in estado or not estado['best_prices_30days']:
        log_aviso("Atualizando dados de melhor hora de compra para respeitar AGUARDAR_MELHOR_HORA...")
        estado = atualizar_melhores_precos_portfolio(estado, moedas)
    
    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 = float(saldo_usdt)
    for moeda in moedas:
        quantidade = saldos.get(moeda, 0)
        preco = precos.get(moeda, 0)
        if quantidade > 0 and preco:
            # ✓ Converter Decimal para float para operação com preco (float)
            valor_portfolio += float(quantidade) * preco
    
    # ✓ Mostrar estado ANTES de calcular limite
    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 (REAL): {valor_portfolio:.2f} {MOEDA_BASE}")
    
    # ✓ IMPORTANTE: Calcular limite respeitando reinvestimento de ganhos
    # Capital base + (ganhos × percentual de reinvestimento)
    valor_portfolio_limite, ganho_total, ganho_reinv, msg_ganho = calcular_limite_reinvestimento(valor_portfolio, moedas, saldos, precos, estado)
    
    # ✓ NOVO: Se reinvestimento = 0%, congelar TODO o USDT residual
    saldo_usdt_minimo_protegido = SALDO_MINIMO_USDT
    
    if REINVESTIR_GANHOS_PERCENTUAL <= 0 and saldo_usdt > 0:
        # Calcular USDT residual (não usado na construção)
        # ✓ Converter Decimal para float para evitar TypeError
        saldo_usdt_float = float(saldo_usdt) if isinstance(saldo_usdt, Decimal) else saldo_usdt
        usdt_residual = max(0, saldo_usdt_float - valor_portfolio_limite)
        if usdt_residual > 50:  # Só proteger se for significativo
            saldo_usdt_minimo_protegido = SALDO_MINIMO_USDT + usdt_residual
            log_aviso(f"\n🔒 MODO PROTETOR: Congelando ${usdt_residual:.2f} em USDT residual")
            log_aviso(f"   Saldo mínimo protegido ajustado para: ${saldo_usdt_minimo_protegido:.2f}")
    
    # Se saldo_usdt é 0, usar capital inicial como base de trabalho
    saldo_para_compra = saldo_usdt if saldo_usdt > 0 else CAPITAL_INICIAL
    
    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:
            # ✓ Converter Decimal para float para formatação com .8f
            log_info(f"{moeda}: Já possui {float(quantidade_atual):.8f} em carteira")
            if moeda not in moedas_compradas:
                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 baseada no melhor horário: compra quando está no melhor horário definido pelos últimos 30 dias
                # Ignora o preco_alvo e usa apenas o preço atual
                esta_no_melhor_horario, msg_horario = eh_horario_compra_ideal(moeda, estado)
                condicao_compra = esta_no_melhor_horario
                
                # Se não está no melhor horário, pode comprar se o preço estiver BEM abaixo do alvo (oportunidade)
                if not condicao_compra and preco_alvo > 0:
                    diferenca_percentual = ((preco_alvo - preco_atual) / preco_alvo * 100)
                    # Compra mesmo fora do horário ideal se estiver 5% ou mais abaixo do alvo (oportunidade)
                    condicao_compra = diferenca_percentual >= 5
            
            if preco_atual and condicao_compra:
                # Preço está bom para compra
                percentual_alvo = percentuais_alvo.get(moeda, 0) / 100
                # ✓ Usar valor_portfolio_limite para respeitar CAPITAL_INICIAL
                valor_compra = valor_portfolio_limite * percentual_alvo
                quantidade = valor_compra / preco_atual * (1 - COMISSAO)
                
                # ✓ AJUSTE: Se valor < 5.2 USDT, executar imediatamente com o mínimo (5.2 USDT) para não perder oscilações
                # ✓ CORRIGIDO: Calcular quantidade SEM aplicar comissão novamente (o valor já é o final desejado)
                valor_ajustado = False
                if valor_compra < VALOR_MINIMO_OPERACAO:
                    valor_compra_original = valor_compra
                    valor_compra = VALOR_MINIMO_OPERACAO
                    quantidade = valor_compra / preco_atual  # SEM aplicar comissão (já está no valor final)
                    valor_ajustado = True
                    log_aviso(f"    ⚡ Valor ajustado de ${valor_compra_original:.2f} para ${valor_compra:.2f} (mínimo de operação)")
                
                # Usar saldo real para compra, respeitando proteção de USDT residual
                # ✓ Converter Decimal para float para evitar TypeError
                saldo_disponivel = float(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')
                
                # ✓ NOVO: Verificar se tem saldo livre após proteção
                saldo_livre = max(0, saldo_disponivel - saldo_usdt_minimo_protegido)
                
                if eh_valido and valor_compra >= VALOR_MINIMO_OPERACAO and saldo_livre >= 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: {float(quantidade):.8f}")
                    
                    # ✓ NOVO: Inicializar flag para rastrear se é operação superdimensionada
                    eh_operacao_superdimensionada = False
                    
                    # 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)
                    # Na fase de construção, usar AGUARDAR_MELHOR_HORA normalmente
                    # Na fase de balanceamento (reinicializações), usar em_incremento_capital
                    deve_aguardar = AGUARDAR_MELHOR_HORA or estado.get('em_incremento_capital', False)
                    
                    if deve_aguardar:
                        log_info(f"  ⏰ Buscando melhor momento de compra para maximizar lucros...")
                        aguardou, tempo_esperado, msg_tempo = aguardar_melhor_hora_compra(moeda, estado)
                        if aguardou is None:
                            # ✓ Horário ideal está muito distante - pular esta moeda neste ciclo
                            log_aviso(f"  {msg_tempo}")
                            continue
                        elif 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)
                    
                    # ✓ CRÍTICO: Obter preço atual NO MOMENTO da ordem
                    preco_atual = obter_preco_atual(f"{moeda}/{MOEDA_BASE}")
                    
                    # ✓ NOVO: Validar notional ANTES de tentar a ordem (mesmo sistema de vendas)
                    if preco_atual and preco_atual > 0:
                        eh_valido_notional, valor_notional, msg_notional, preco_limite = validar_notional_limite_ordem(
                            quantidade, preco_atual, 'buy', moeda
                        )
                        
                        if not eh_valido_notional:
                            log_aviso(f"  {msg_notional}")
                            
                            # ✓ NOVO: Processar com Retry Inteligente (construção inicial)
                            capital_rastreado = estado.get('capital_rastreado', CAPITAL_INICIAL)
                            
                            deve_tentar, valor_ajustado_usd, ciclos_skip = processar_residual_com_retry_inteligente(
                                estado, moeda, percentual_alvo * 100, 0, capital_rastreado, config
                            )
                            
                            # ✓ NOVO: Rastrear se é operação superdimensionada para marcar histerese depois
                            eh_operacao_superdimensionada = deve_tentar and valor_ajustado_usd and valor_ajustado_usd > 0
                            
                            if eh_operacao_superdimensionada:
                                # ✓ NOVO: Teste Progressivo - calcular quantidade para valor USD
                                quantidade_ajustada = valor_ajustado_usd / preco_atual
                                log_info(f"    💡 Teste Progressivo (construção):")
                                log_info(f"       Quantidade: {float(quantidade):.8f} → {quantidade_ajustada:.8f}")
                                log_info(f"       Valor: ${valor_compra:.2f} → ${valor_ajustado_usd:.2f}")
                                quantidade = quantidade_ajustada
                                valor_compra = valor_ajustado_usd  # ✓ CRÍTICO: Atualizar valor_compra também!
                                # NÃO fazer continue - deixar tentar a ordem
                            else:
                                # Não deve tentar agora, pular
                                salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
                                continue
                        else:
                            log_info(f"  {msg_notional}")
                    else:
                        log_erro(f"Não foi possível obter preço para validar notional de {moeda}")
                        continue
                    
                    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:
                            # ✓ Converter Decimal para float para formatação com .8f
                            log_sucesso(f"✓ Compra verificada: Saldo aumentou {float(diferenca_saldo):.8f} {moeda}")
                            if moeda not in moedas_compradas:
                                moedas_compradas.append(moeda)
                            estado['moedas_compradas'] = moedas_compradas
                            
                            # ✓ NOVO: Se foi operacao superdimensionada, marcar histerese
                            if eh_operacao_superdimensionada:
                                marcar_histerese_ativa(estado, moeda, 'compra', 0)
                            
                            # ✓ NOVO: Marcar teste como sucesso no historico de residuais
                            if moeda in estado.get('moedas_residuais_historico', {}):
                                residual = estado['moedas_residuais_historico'][moeda]
                                if 'historico_testes' in residual and len(residual['historico_testes']) > 0:
                                    # Última tentativa foi bem-sucedida
                                    residual['historico_testes'][-1]['resultado'] = 'sucesso'
                                    log_sucesso(f"    ✅ Teste com ${residual['valor_testado']:.2f} EXECUTADO com sucesso!")
                                    # Limpar da lista de residuais pois conseguimos comprar
                                    del estado['moedas_residuais_historico'][moeda]
                                    log_sucesso(f"    💚 {moeda} removido de residuais após teste bem-sucedido")
                            
                            # ✓ REFATORADO: Remover do JSON, manter apenas no SQLite
                            # estado['historico_trades'].append({...})
                            # ✓ CRÍTICO: Registrar com QUANTIDADE REAL comprada (diferenca_saldo, não quantidade)
                            # porque retry pode ter ajustado quantidade, mas o que realmente importa é
                            # quanto foi REALMENTE comprado na Binance (diferenca_saldo)
                            valor_compra_real = float(diferenca_saldo) * preco_atual
                            registrar_trade_sqlite(moeda, 'compra_inicial', preco_atual, float(diferenca_saldo), valor_compra_real)
                        else:
                            log_erro(f"✗ FALHA: Compra de {moeda} não confirmada! Saldo não aumentou.")
                            log_erro(f"  Antes: {float(saldo_antes):.8f} {moeda} | Depois: {float(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)
                    else:
                        # ✓ NOVO: Ordem de compra falhou mesmo após validação de notional passar!
                        log_erro(f"  ✗ Ordem de compra falhou durante construção inicial!")
                        log_erro(f"     Provável causa: Condição de corrida (preço mudou entre validação e execução)")
                        
                        # ✓ NOVO: Processar com Retry Inteligente para escalar valor
                        diferenca_aprox = (valor_compra / valor_portfolio_limite * 100) if valor_portfolio_limite > 0 else 0
                        
                        capital_rastreado = estado.get('capital_rastreado', estado.get('capital_inicial', 0))
                        deve_tentar, valor_ajustado_usd, ciclos_skip = processar_residual_com_retry_inteligente(
                            estado, moeda, diferenca_aprox, contador_ciclo, capital_rastreado, config
                        )
                        
                        if deve_tentar and valor_ajustado_usd and valor_ajustado_usd > 0:
                            log_info(f"  💡 Escalando valor para próximo ciclo: ${valor_compra:.2f} → ${valor_ajustado_usd:.2f}")
                            log_info(f"     Próxima tentativa em {ciclos_skip} ciclo(s)")
                            # Salvar estado antes de continuar
                            salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
                        else:
                            log_aviso(f"  ⏸️  {moeda}: Aguardando melhor horário para tentar novamente")
                            salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
                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 COMPRAR_IMEDIATAMENTE:
                    # Modo de compra imediata - mostrar informação de preço
                    if preco_alvo > 0:
                        diferenca_pct = ((preco_atual - preco_alvo) / preco_alvo * 100)
                        mensagem = f"{moeda}: Preço muito alto para compra imediata ({diferenca_pct:.1f}% acima). Esperando melhora..."
                    else:
                        mensagem = f"{moeda}: Preço alvo não definido na configuração"
                else:
                    # Modo de compra por melhor horário - mostrar informação de horário ideal
                    hora_inicio, hora_fim, confianca = extrair_horario_compra(estado, moeda)
                    if hora_inicio is not None:
                        hora_atual = datetime.now(tz=timezone).hour
                        tempo_ate = calcular_tempo_ate_melhor_hora(moeda, estado)
                        periodo_str = f"{hora_inicio:02d}:00-{hora_fim:02d}:59"
                        mensagem = f"{moeda}: Aguardando melhor hora de compra ({periodo_str}, confiança: {confianca:.1f}%). Faltam {tempo_ate:.0f} min. Preço: {preco_atual:.8f}"
                    else:
                        mensagem = f"{moeda}: Análise de melhor hora não disponível ainda"
                log_info(mensagem)
        else:
            if moeda not in moedas_compradas:
                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()
    
    # ✓ NOVO: Atualizar quantidades reais antes de salvar
    saldos_atuais = obter_saldos_portfolio(moedas)
    atualizar_quantidade_carteira_real(PORTFOLIO_STATE_FILE, moedas, saldos_atuais)
    
    salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
    return estado

def fase_balanceamento_portfolio(estado, moedas, contador_ciclo=0):
    """
    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 ==========")
    
    # ✓ NOVO: Inicializar rastreador de moedas residuais neste ciclo
    # Evita loop infinito em moedas com notional insuficiente
    moedas_residuais_ciclo = set()
    
    # ✓ GARANTIR que best_prices_30days está populado (importante para aguardar_melhor_hora)
    # Protege contra ausência de dados ao reiniciar com capital aumentado
    if 'best_prices_30days' not in estado or not estado['best_prices_30days']:
        log_aviso("Atualizando dados de melhor hora de compra para respeitar AGUARDAR_MELHOR_HORA...")
        estado = atualizar_melhores_precos_portfolio(estado, moedas)
    
    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)
    saldo_usdt_atual = saldos.get(MOEDA_BASE, 0)
    # ✓ Converter Decimal para float para evitar TypeError em operações aritméticas
    saldo_usdt_atual = float(saldo_usdt_atual) if isinstance(saldo_usdt_atual, Decimal) else saldo_usdt_atual
    
    # ✓ Mostrar estado ANTES de calcular limite
    log_info(f"Valor Total do Portfólio (REAL): {valor_portfolio:.2f} {MOEDA_BASE}")
    
    # ✓ IMPORTANTE: Calcular limite respeitando reinvestimento de ganhos
    # Capital base + (ganhos × percentual de reinvestimento)
    valor_portfolio_limite, ganho_total, ganho_reinv, msg_ganho = calcular_limite_reinvestimento(valor_portfolio, moedas, saldos, precos, estado)
    
    log_info(f"⚠️  Valor Total para CÁLCULOS (COM REINVESTIMENTO): {valor_portfolio_limite:.2f} {MOEDA_BASE}")
    log_info(f"📊 {msg_ganho}")
    
    # ✓ NOVO: Se reinvestimento = 0%, congelar TODO o USDT residual
    saldo_usdt_minimo_protegido = SALDO_MINIMO_USDT
    
    if REINVESTIR_GANHOS_PERCENTUAL <= 0 and saldo_usdt_atual > 0:
        # Calcular USDT residual (não usado na construção)
        # ✓ Converter Decimal para float para evitar TypeError
        saldo_usdt_atual_float = float(saldo_usdt_atual) if isinstance(saldo_usdt_atual, Decimal) else saldo_usdt_atual
        usdt_residual = max(0, saldo_usdt_atual_float - valor_portfolio_limite)
        if usdt_residual > 50:  # Só proteger se for significativo
            saldo_usdt_minimo_protegido = SALDO_MINIMO_USDT + usdt_residual
            log_aviso(f"\n🔒 MODO PROTETOR: Congelando ${usdt_residual:.2f} em USDT residual")
            log_aviso(f"   Saldo mínimo protegido ajustado para: ${saldo_usdt_minimo_protegido:.2f}")
    
    log_info(f"🔒 PROTEÇÃO USDT: Mínimo ${saldo_usdt_minimo_protegido:.2f} não será gasto")
    
    # Calcular percentuais atuais - USAR O VALOR LIMITADO para calcular percentuais
    percentuais_atuais = calcular_percentuais_atuais(moedas, saldos, precos, valor_portfolio_limite)
    percentuais_alvo = estado['percentuais_alvo']
    
    log_info("\n--- Análise de Percentuais ---")
    moedas_venda = []
    moedas_compra = []
    
    # ✓ NOVO: VERIFICAR NORMALIZAÇÃO DE MOEDAS EM RESIDUAIS (ANTES de processar)
    # Isso garante que se uma moeda normalizou (por venda ou flutuação), ela seja removida
    if 'moedas_residuais_historico' in estado and estado['moedas_residuais_historico']:
        moedas_residuais_remover = []
        for moeda_residual in estado['moedas_residuais_historico'].keys():
            if moeda_residual in moedas:
                pct_atual = percentuais_atuais.get(moeda_residual, 0)
                pct_alvo = percentuais_alvo.get(moeda_residual, 0)
                diferenca = abs(pct_atual - pct_alvo)
                
                # Verificar se desequilíbrio normalizou (< threshold)
                if diferenca < VARIACAO_MINIMA_NORMALIZACAO:
                    log_sucesso(f"    ✅ {moeda_residual}: Desequilíbrio NORMALIZADO ({diferenca:.2f}% < {VARIACAO_MINIMA_NORMALIZACAO}%)")
                    log_info(f"    💚 Removendo de residuais - voltando ao rebalanceamento normal")
                    moedas_residuais_remover.append(moeda_residual)
        
        # Remover após iteração para evitar modificar dict durante iteração
        for moeda_rem in moedas_residuais_remover:
            del estado['moedas_residuais_historico'][moeda_rem]
    
    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}%")
        
        # ✓ PRÉ-CHECK: Se está em residuais aguardando retry, pular esta moeda
        residual_info = estado.get('moedas_residuais_historico', {}).get(moeda, {})
        if residual_info:
            proxima_tentativa = residual_info.get('proxima_tentativa_ciclo', contador_ciclo)
            if contador_ciclo < proxima_tentativa:
                ciclos_restantes = proxima_tentativa - contador_ciclo
                log_aviso(f"  ⏸️  {moeda}: Aguardando ciclos de retry ({ciclos_restantes} ciclos ainda)")
                continue  # Pula esta moeda neste ciclo
        
        # ✓ NOVO: Aplicar gates com histerese assimétrica
        gate_venda, motivo_venda, ciclos_rest_venda = obter_variacao_gate_com_histerese(moeda, estado, contador_ciclo, 'venda')
        gate_compra, motivo_compra, ciclos_rest_compra = obter_variacao_gate_com_histerese(moeda, estado, contador_ciclo, 'compra')
        
        if diferenca > gate_venda:
            if ciclos_rest_venda > 0:
                log_info(f"   {motivo_venda}")
            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 < -gate_compra:
            if ciclos_rest_compra > 0:
                log_info(f"   {motivo_compra}")
            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)")
    
    # 🆕 NOVO: Se está em modo retirada de capital, vender moedas prioritariamente
    if estado.get('em_retirada_capital', False):
        valor_retirada = estado.get('valor_retirada', 0)
        saldo_usdt_atual = saldos.get(MOEDA_BASE, 0)
        # ✓ Converter Decimal para float para evitar TypeError em operações aritméticas
        saldo_usdt_atual = float(saldo_usdt_atual) if isinstance(saldo_usdt_atual, Decimal) else saldo_usdt_atual
        valor_ainda_falta = valor_retirada - saldo_usdt_atual
        
        if valor_ainda_falta > VALOR_MINIMO_OPERACAO:
            log_aviso(f"\n🔄 MODO RETIRADA DE CAPITAL ATIVO")
            log_aviso(f"   Faltam retirar: ${valor_ainda_falta:.2f}")
            log_aviso(f"   Vendendo moedas prioritariamente para completar retirada...")
            
            # Vender moedas em ordem de maior valor (mais rápido conseguir USDT)
            for moeda in moedas:
                if moeda == 'USDT':
                    continue
                
                quantidade = saldos.get(moeda, 0)
                preco = precos.get(moeda, 0)
                
                if quantidade > VALOR_MINIMO_OPERACAO / preco and preco > 0:
                    # Tentar vender até conseguir o USDT necessário
                    # ✓ Converter Decimal para float para operação com preco (float)
                    valor_moeda = float(quantidade) * preco
                    
                    if valor_moeda > VALOR_MINIMO_OPERACAO:
                        # ✓ Converter quantidade para float para operação com min()
                        quantidade_venda = min(float(quantidade), valor_ainda_falta / preco) * (1 - COMISSAO)
                        
                        if quantidade_venda > 0:
                            symbol = f"{moeda}/{MOEDA_BASE}"
                            log_info(f"  Vendendo {moeda}: {quantidade_venda:.8f} por ${float(quantidade_venda) * preco:.2f}")
                            
                            # ✓ CRÍTICO: Obter saldo ANTES da venda para validação
                            saldo_antes = obter_saldo(moeda)
                            
                            # Validar notional ANTES de tentar a ordem
                            preco_atual = obter_preco_atual(symbol)
                            if preco_atual and preco_atual > 0:
                                eh_valido_notional, valor_notional, msg_notional, preco_limite = validar_notional_limite_ordem(
                                    quantidade_venda, preco_atual, 'sell', moeda
                                )
                                
                                if not eh_valido_notional:
                                    log_aviso(f"  {msg_notional}")
                                    
                                    # ✓ NOVO: Processar com Retry Inteligente + Teste Progressivo
                                    diferenca_aprox = (float(quantidade_venda) * preco_atual / valor_portfolio_limite * 100) if valor_portfolio_limite > 0 else 0
                                    
                                    capital_rastreado = estado.get('capital_rastreado', estado.get('capital_inicial', 0))
                                    deve_tentar, valor_ajustado_usd, ciclos_skip = processar_residual_com_retry_inteligente(
                                        estado, moeda, diferenca_aprox, contador_ciclo, capital_rastreado, config
                                    )
                                    
                                    # ✓ NOVO: Rastrear se é operação superdimensionada para marcar histerese depois
                                    eh_operacao_superdimensionada = deve_tentar and valor_ajustado_usd and valor_ajustado_usd > 0
                                    
                                    if eh_operacao_superdimensionada:
                                        # ✓ NOVO: Teste Progressivo - calcular quantidade para valor USD
                                        quantidade_venda_ajustada = valor_ajustado_usd / preco_atual
                                        log_info(f"    💡 Teste Progressivo:")
                                        log_info(f"       Quantidade: {quantidade_venda:.8f} → {quantidade_venda_ajustada:.8f}")
                                        log_info(f"       Valor: ${float(quantidade_venda) * preco_atual:.2f} → ${valor_ajustado_usd:.2f}")
                                        quantidade_venda = quantidade_venda_ajustada
                                        # NÃO fazer continue - deixar tentar a ordem
                                    else:
                                        # Não deve tentar agora, pular
                                        # ✓ SALVAR ESTADO antes de pular (estado foi atualizado com moedas_residuais_historico)
                                        salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
                                        continue
                                
                                log_info(f"  {msg_notional}")
                            else:
                                log_erro(f"Não foi possível obter preço para validar notional de {moeda}")
                                continue
                            
                            # ✓ NOVO: Aguardar melhor hora de venda mesmo na venda prioritária
                            # Usar padrão inteligente de 30 minutos com hora média
                            log_info(f"  ⏰ Aguardando melhor hora de venda para maximizar retorno...")
                            aguardou, tempo_esperado, msg_tempo = aguardar_melhor_hora_venda(moeda, estado)
                            if aguardou is None:
                                # ✓ Horário ideal está muito distante - pular esta moeda neste ciclo
                                log_aviso(f"  {msg_tempo}")
                                continue
                            elif aguardou:
                                log_sucesso(f"  {msg_tempo}")
                            else:
                                log_info(f"  {msg_tempo}")
                            
                            ordem = executar_ordem(symbol, 'sell', quantidade_venda)
                            
                            if ordem:
                                # Aguardar um pouco para a Binance processar
                                time.sleep(1)
                                
                                # ✓ CRÍTICO: Obter saldo DEPOIS da venda para validar execução
                                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 {float(diferenca_saldo):.8f} {moeda}")
                                    
                                    # NOVO: Se foi operacao superdimensionada, marcar histerese
                                    if eh_operacao_superdimensionada:
                                        marcar_histerese_ativa(estado, moeda, 'venda', contador_ciclo)
                                    
                                    # ✓ NOVO: Marcar teste como sucesso no historico de residuais
                                    if moeda in estado.get('moedas_residuais_historico', {}):
                                        residual = estado['moedas_residuais_historico'][moeda]
                                        if 'historico_testes' in residual and len(residual['historico_testes']) > 0:
                                            # Última tentativa foi bem-sucedida
                                            residual['historico_testes'][-1]['resultado'] = 'sucesso'
                                            log_sucesso(f"    ✅ Teste com ${residual['valor_testado']:.2f} EXECUTADO com sucesso!")
                                            # Limpar da lista de residuais pois conseguimos vender
                                            del estado['moedas_residuais_historico'][moeda]
                                            log_sucesso(f"    💚 {moeda} removido de residuais após teste bem-sucedido")
                                    
                                    time.sleep(1)
                                    
                                    # ✓ CRÍTICO: REGISTRAR a venda de retirada de capital na base de dados
                                    # Obter preço atual para registro
                                    preco_venda = obter_preco_atual(f"{moeda}/{MOEDA_BASE}")
                                    if preco_venda and preco_venda > 0:
                                        # ✓ CRÍTICO: Usar quantidade REAL vendida (diferenca_saldo, não quantidade_venda)
                                        valor_venda_real = float(diferenca_saldo) * preco_venda
                                        registrar_trade_sqlite(moeda, 'venda_ajuste_capital', preco_venda, float(diferenca_saldo), valor_venda_real)
                                        log_sucesso(f"✓ Venda registrada: {moeda} @ ${preco_venda:.8f} ({float(diferenca_saldo):.8f}) = ${valor_venda_real:.2f}")
                                else:
                                    log_erro(f"✗ FALHA: Venda de {moeda} não confirmada! Saldo não diminuiu.")
                                
                                # Atualizar USDT e valor ainda falta
                                saldos = obter_saldos_portfolio(moedas)
                                saldo_usdt_atual = saldos.get(MOEDA_BASE, 0)
                                # ✓ Converter Decimal para float para evitar TypeError em operações aritméticas
                                saldo_usdt_atual = float(saldo_usdt_atual) if isinstance(saldo_usdt_atual, Decimal) else saldo_usdt_atual
                                valor_ainda_falta = valor_retirada - saldo_usdt_atual
                                
                                if valor_ainda_falta <= VALOR_MINIMO_OPERACAO:
                                    break  # Já conseguiu o USDT necessário
                                
                                time.sleep(0.5)
                            else:
                                # ✓ NOVO: Ordem de venda falhou mesmo após validação de notional passar!
                                log_erro(f"  ✗ Ordem de venda falhou durante retirada de capital!")
                                log_erro(f"     Provável causa: Condição de corrida (preço mudou entre validação e execução)")
                                
                                # ✓ NOVO: Processar com Retry Inteligente para escalar valor
                                diferenca_aprox = (float(quantidade_venda) * preco_atual / valor_portfolio_limite * 100) if valor_portfolio_limite > 0 else 0
                                
                                capital_rastreado = estado.get('capital_rastreado', estado.get('capital_inicial', 0))
                                deve_tentar, valor_ajustado_usd, ciclos_skip = processar_residual_com_retry_inteligente(
                                    estado, moeda, diferenca_aprox, contador_ciclo, capital_rastreado, config
                                )
                                
                                if deve_tentar and valor_ajustado_usd and valor_ajustado_usd > 0:
                                    log_info(f"  💡 Escalando valor para próximo ciclo: ${quantidade_venda * preco_atual:.2f} → ${valor_ajustado_usd:.2f}")
                                    log_info(f"     Próxima tentativa em {ciclos_skip} ciclo(s)")
                                    # Salvar estado antes de continuar
                                    salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
                                    continue
                                else:
                                    log_aviso(f"  ⏸️  {moeda}: Aguardando melhor horário para tentar novamente")
                                    salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
    
    # 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
            # ✓ Usar valor_portfolio_limite para respeitar CAPITAL_INICIAL
            valor_excesso = (diferenca / 100) * valor_portfolio_limite
            quantidade_venda = valor_excesso / preco * (1 - COMISSAO)
            
            # ✓ AJUSTE: Se valor < 5.2 USDT, executar imediatamente com o mínimo (5.2 USDT) para não perder oscilações
            # ✓ CORRIGIDO: Calcular quantidade SEM aplicar comissão novamente (o valor já é o final desejado)
            valor_ajustado = False
            if valor_excesso < VALOR_MINIMO_OPERACAO and quantidade_atual > 0:
                valor_excesso_original = valor_excesso
                valor_excesso = VALOR_MINIMO_OPERACAO
                quantidade_venda = valor_excesso / preco  # SEM aplicar comissão (já está no valor final)
                valor_ajustado = True
                log_aviso(f"    ⚡ Valor ajustado de ${valor_excesso_original:.2f} para ${valor_excesso:.2f} (mínimo de operação)")
            
            # 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}")
                
                # ✓ NOVO: Aguardar melhor hora de venda se estiver em retirada de capital
                # Usar padrão inteligente de 30 minutos com hora média
                deve_aguardar_venda = estado.get('em_retirada_capital', False)
                
                if deve_aguardar_venda:
                    log_info(f"  ⏰ Buscando melhor momento de venda para retirar capital...")
                    aguardou, tempo_esperado, msg_tempo = aguardar_melhor_hora_venda(moeda, estado)
                    if aguardou is None:
                        # ✓ Horário ideal está muito distante - pular esta moeda neste ciclo
                        log_aviso(f"  {msg_tempo}")
                        continue
                    elif aguardou:
                        log_sucesso(f"  {msg_tempo}")
                    else:
                        log_info(f"  {msg_tempo}")
                else:
                    log_info(f"  → Venda imediata (rebalanceamento normal)")
                
                # ✓ NOVO: Obter preco_atual DEPOIS de possível espera (para ter preço atualizado)
                preco_atual = obter_preco_atual(f"{moeda}/{MOEDA_BASE}")
                if preco_atual and preco_atual > 0:
                    eh_valido_notional, valor_notional, msg_notional, preco_limite = validar_notional_limite_ordem(
                        quantidade_venda, preco_atual, 'sell', moeda
                    )
                    
                    if not eh_valido_notional:
                        log_aviso(f"  {msg_notional}")
                        
                        # ✓ NOVO: Processar com Retry Inteligente + Teste Progressivo
                        # Calcula diferença aproximada para rebalanceamento
                        diferenca_aprox = (float(quantidade_venda) * preco_atual / valor_portfolio_limite * 100) if valor_portfolio_limite > 0 else 0
                        
                        capital_rastreado = estado.get('capital_rastreado', estado.get('capital_inicial', 0))
                        deve_tentar, valor_ajustado_usd, ciclos_skip = processar_residual_com_retry_inteligente(
                            estado, moeda, diferenca_aprox, contador_ciclo, capital_rastreado, config
                        )
                        
                        if deve_tentar and valor_ajustado_usd and valor_ajustado_usd > 0:
                            # ✓ NOVO: Teste Progressivo - calcular quantidade para valor USD
                            quantidade_venda_ajustada = valor_ajustado_usd / preco_atual
                            log_info(f"    💡 Teste Progressivo:")
                            log_info(f"       Quantidade: {quantidade_venda:.8f} → {quantidade_venda_ajustada:.8f}")
                            log_info(f"       Valor: ${float(quantidade_venda) * preco_atual:.2f} → ${valor_ajustado_usd:.2f}")
                            quantidade_venda = quantidade_venda_ajustada
                            valor_excesso = valor_ajustado_usd  # ✓ CRÍTICO: Atualizar valor_excesso também!
                            # NÃO fazer continue - deixar tentar a ordem
                        else:
                            # Não deve tentar agora, pular
                            # ✓ SALVAR ESTADO antes de pular (estado foi atualizado com moedas_residuais_historico)
                            salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
                            continue
                    else:
                        log_info(f"  {msg_notional}")
                else:
                    log_erro(f"Não foi possível obter preço para validar notional de {moeda}")
                    continue
                
                # 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)
                    
                    # ✓ NOVO: Marcar teste como sucesso no histórico de residuais
                    if moeda in estado.get('moedas_residuais_historico', {}):
                        residual = estado['moedas_residuais_historico'][moeda]
                        if 'historico_testes' in residual and len(residual['historico_testes']) > 0:
                            # Última tentativa foi bem-sucedida
                            residual['historico_testes'][-1]['resultado'] = 'sucesso'
                            log_sucesso(f"    ✅ Teste com ${residual['valor_testado']:.2f} EXECUTADO com sucesso!")
                            # Limpar da lista de residuais pois conseguimos vender
                            del estado['moedas_residuais_historico'][moeda]
                            log_sucesso(f"    💚 {moeda} removido de residuais após teste bem-sucedido")
                    
                    # 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:
                        # ✓ Converter Decimal para float para formatação com .8f
                        log_sucesso(f"✓ Venda verificada: Saldo diminuiu {float(diferenca_saldo):.8f} {moeda}")
                        # ✓ Converter Decimal para float para evitar TypeError em operação aritmética
                        saldo_usdt_antes = float(saldo_usdt_antes) if isinstance(saldo_usdt_antes, Decimal) else saldo_usdt_antes
                        saldo_usdt_antes += valor_excesso
                        # ✓ NOVO: Usar 'venda_ajuste_capital' se estiver em retirada de capital
                        tipo_venda = 'venda_ajuste_capital' if estado.get('em_retirada_capital', False) else 'venda_balanceamento'
                        # ✓ REFATORADO: Remover do JSON, manter apenas no SQLite
                        # estado['historico_trades'].append({...})
                        # ✓ CRÍTICO: Registrar com QUANTIDADE REAL vendida (diferenca_saldo, não quantidade_venda)
                        # porque retry pode ter ajustado quantidade_venda, mas o que realmente importa é
                        # quanto foi REALMENTE vendido na Binance (diferenca_saldo)
                        valor_venda_real = float(diferenca_saldo) * preco_atual
                        registrar_trade_sqlite(moeda, tipo_venda, preco_atual, float(diferenca_saldo), valor_venda_real)
                    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)
                else:
                    # ✓ NOVO: Ordem de venda falhou mesmo após validação de notional passar!
                    log_erro(f"  ✗ Ordem de venda falhou após validação de notional passar!")
                    log_erro(f"     Provável causa: Condição de corrida (preço mudou entre validação e execução)")
                    
                    # ✓ NOVO: Calcular diferença aproximada para retry (mesmo que na compra)
                    diferenca_aprox = (float(quantidade_venda) * preco_atual / valor_portfolio_limite * 100) if valor_portfolio_limite > 0 else 0
                    
                    # ✓ NOVO: Processar com Retry Inteligente para escalar valor
                    capital_rastreado = estado.get('capital_rastreado', estado.get('capital_inicial', 0))
                    deve_tentar, valor_ajustado_usd, ciclos_skip = processar_residual_com_retry_inteligente(
                        estado, moeda, diferenca_aprox, contador_ciclo, capital_rastreado, config
                    )
                    
                    if deve_tentar and valor_ajustado_usd and valor_ajustado_usd > 0:
                        log_info(f"  💡 Escalando valor para próximo ciclo: ${valor_excesso:.2f} → ${valor_ajustado_usd:.2f}")
                        log_info(f"     Próxima tentativa em {ciclos_skip} ciclo(s)")
                        # Salvar estado antes de pular para próximo ciclo
                        salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
                    else:
                        log_aviso(f"  ⏸️  {moeda}: Aguardando melhor horário para tentar novamente")
                        salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
            elif not eh_valido:
                log_aviso(msg_validacao)
    
    # Atualizar saldos
    saldos = obter_saldos_portfolio(moedas)
    saldo_usdt_atual = saldos.get(MOEDA_BASE, 0)
    # ✓ Converter Decimal para float para evitar TypeError em operações aritméticas
    saldo_usdt_atual = float(saldo_usdt_atual) if isinstance(saldo_usdt_atual, Decimal) else saldo_usdt_atual
    
    log_info(f"\n🔍 ESTADO ATUAL DO PORTFÓLIO:")
    log_info(f"   • USDT em Caixa: ${saldo_usdt_atual:.2f}")
    log_info(f"   • Moedas a Comprar: {len(moedas_compra)} → {[m['moeda'] for m in moedas_compra]}")
    log_info(f"   • Moedas a Vender: {len(moedas_venda)} → {[m['moeda'] for m in moedas_venda]}")
    
    # 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:
            # ✓ Usar valor_portfolio_limite para respeitar CAPITAL_INICIAL
            valor_compra = (diferenca / 100) * valor_portfolio_limite
            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)
    
    # ✓ Converter Decimal para float para evitar TypeError em operações aritméticas
    saldo_usdt_atual_float = float(saldo_usdt_atual) if isinstance(saldo_usdt_atual, Decimal) else saldo_usdt_atual
    
    # Se não há saldo suficiente, reduzir proporcionalmente todos os valores
    fator_reducao = 1.0
    if total_usdt_necessario > saldo_usdt_atual_float:
        fator_reducao = max(0, (saldo_usdt_atual_float - saldo_usdt_minimo_protegido) / total_usdt_necessario) if total_usdt_necessario > 0 else 0
        log_aviso(f"\n💰 CÁLCULO DE COMPRAS (SALDO INSUFICIENTE):")
        log_aviso(f"   • USDT Disponível: ${saldo_usdt_atual_float:.2f}")
        log_aviso(f"   • Proteção USDT: ${saldo_usdt_minimo_protegido:.2f}")
        log_aviso(f"   • USDT Livre (pode gastar): ${max(0, saldo_usdt_atual_float - saldo_usdt_minimo_protegido):.2f}")
        log_aviso(f"   • Total Necessário: ${total_usdt_necessario:.2f}")
        log_aviso(f"   • Fator de Redução: {fator_reducao*100:.1f}%")
        if fator_reducao > 0:
            log_aviso(f"⚠ Saldo insuficiente para completar todas as compras. Reduzindo proporcionalmente a {fator_reducao*100:.1f}%")
    else:
        # Saldo suficiente, mas verificar proteção USDT
        log_info(f"\n💰 CÁLCULO DE COMPRAS (SALDO SUFICIENTE):")
        log_info(f"   • USDT Disponível: ${saldo_usdt_atual_float:.2f}")
        log_info(f"   • Proteção USDT: ${saldo_usdt_minimo_protegido:.2f}")
        log_info(f"   • USDT Livre (pode gastar): ${max(0, saldo_usdt_atual_float - saldo_usdt_minimo_protegido):.2f}")
        log_info(f"   • Total Necessário: ${total_usdt_necessario:.2f}")
        log_info(f"   • Fator de Redução: 100% (saldo ok)")
    
    # Executar compras com distribuição justa do saldo
    for compra in compras_planejadas:
        moeda = compra['moeda']
        preco = compra['preco']
        valor_ideal = compra['valor_ideal']
        
        # ✓ NOVO: Inicializar flag para rastrear se é operação superdimensionada
        eh_operacao_superdimensionada = False
        
        # Aplicar fator de redução se necessário
        valor_compra = valor_ideal * fator_reducao
        
        # ✓ AJUSTE: Se valor < 5 USDT, executar imediatamente com o mínimo (5 USDT) para não perder oscilações
        valor_ajustado = False
        if valor_compra < VALOR_MINIMO_OPERACAO:
            valor_compra_original = valor_compra
            valor_compra = VALOR_MINIMO_OPERACAO
            valor_ajustado = True
            log_aviso(f"    ⚡ Valor ajustado de ${valor_compra_original:.2f} para ${valor_compra:.2f} (mínimo da Binance)")
        
        if preco > 0 and saldo_usdt_atual > SALDO_MINIMO_USDT:
            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 >= VALOR_MINIMO_OPERACAO and saldo_usdt_atual >= (valor_compra + SALDO_MINIMO_USDT):
                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)
                # SÓ aguarda se estiver em modo de incremento de capital (não no rebalanceamento normal)
                deve_aguardar = estado.get('em_incremento_capital', False)
                
                if deve_aguardar:
                    log_info(f"  ⏰ Buscando melhor momento de compra para investir novo capital...")
                    aguardou, tempo_esperado, msg_tempo = aguardar_melhor_hora_compra(moeda, estado)
                    if aguardou is None:
                        # ✓ Horário ideal está muito distante - pular esta moeda neste ciclo
                        log_aviso(f"  {msg_tempo}")
                        continue
                    elif aguardou:
                        log_sucesso(f"  {msg_tempo}")
                    else:
                        log_info(f"  {msg_tempo}")
                else:
                    log_info(f"  → Compra imediata (rebalanceamento normal)")
                
                # Obter saldo ANTES da compra
                saldo_antes = obter_saldo(moeda)
                
                # ✓ CRÍTICO: Obter preço atual NO MOMENTO da ordem (não usar preço antigo)
                preco_atual_compra = obter_preco_atual(f"{moeda}/{MOEDA_BASE}")
                
                # ✓ NOVO: Validar notional ANTES de tentar a ordem (mesmo sistema de vendas)
                if preco_atual_compra and preco_atual_compra > 0:
                    # ✓ NOVO: Calcular diferença AQUI para poder usar em retry se ordem falhar
                    diferenca_aprox = (valor_ideal / valor_portfolio_limite * 100) if valor_portfolio_limite > 0 else 0
                    
                    eh_valido_notional, valor_notional, msg_notional, preco_limite = validar_notional_limite_ordem(
                        quantidade_compra, preco_atual_compra, 'buy', moeda
                    )
                    
                    if not eh_valido_notional:
                        log_aviso(f"  {msg_notional}")
                        
                        # ✓ NOVO: Processar com Retry Inteligente + Teste Progressivo (MESMO SISTEMA DE VENDAS)
                        capital_rastreado = estado.get('capital_rastreado', estado.get('capital_inicial', 0))
                        deve_tentar, valor_ajustado_usd, ciclos_skip = processar_residual_com_retry_inteligente(
                            estado, moeda, diferenca_aprox, contador_ciclo, capital_rastreado, config
                        )
                        
                        # ✓ NOVO: Rastrear se é operação superdimensionada para marcar histerese depois
                        eh_operacao_superdimensionada = deve_tentar and valor_ajustado_usd and valor_ajustado_usd > 0
                        
                        if eh_operacao_superdimensionada:
                            # ✓ NOVO: Teste Progressivo - calcular quantidade para valor USD
                            quantidade_compra_ajustada = valor_ajustado_usd / preco_atual_compra
                            log_info(f"    💡 Teste Progressivo:")
                            log_info(f"       Quantidade: {quantidade_compra:.8f} → {quantidade_compra_ajustada:.8f}")
                            log_info(f"       Valor: ${valor_compra:.2f} → ${valor_ajustado_usd:.2f}")
                            quantidade_compra = quantidade_compra_ajustada
                            valor_compra = valor_ajustado_usd  # ✓ CRÍTICO: Atualizar valor_compra também!
                            # NÃO fazer continue - deixar tentar a ordem
                        else:
                            # Não deve tentar agora, pular
                            # ✓ SALVAR ESTADO antes de pular (estado foi atualizado com moedas_residuais_historico)
                            salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
                            continue
                    else:
                        log_info(f"  {msg_notional}")
                else:
                    log_erro(f"Não foi possível obter preço para validar notional de {moeda}")
                    continue
                
                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:
                        # ✓ Converter Decimal para float para formatação com .8f
                        log_sucesso(f"✓ Compra verificada: Saldo aumentou {float(diferenca_saldo):.8f} {moeda}")
                        # ✓ Converter Decimal para float para evitar TypeError em operação aritmética
                        saldo_usdt_atual = float(saldo_usdt_atual) if isinstance(saldo_usdt_atual, Decimal) else saldo_usdt_atual
                        saldo_usdt_atual -= valor_compra
                        
                        # ✓ NOVO: Se foi operacao superdimensionada, marcar histerese
                        if eh_operacao_superdimensionada:
                            marcar_histerese_ativa(estado, moeda, 'compra', contador_ciclo)
                        
                        # ✓ NOVO: Marcar teste como sucesso no historico de residuais
                        if moeda in estado.get('moedas_residuais_historico', {}):
                            residual = estado['moedas_residuais_historico'][moeda]
                            if 'historico_testes' in residual and len(residual['historico_testes']) > 0:
                                # Última tentativa foi bem-sucedida
                                residual['historico_testes'][-1]['resultado'] = 'sucesso'
                                log_sucesso(f"    ✅ Teste com ${residual['valor_testado']:.2f} EXECUTADO com sucesso!")
                                # Limpar da lista de residuais pois conseguimos comprar
                                del estado['moedas_residuais_historico'][moeda]
                                log_sucesso(f"    💚 {moeda} removido de residuais após teste bem-sucedido")
                        
                        # ✓ NOVO: Usar 'compra_ajuste_capital' se estiver em incremento de capital
                        tipo_compra = 'compra_ajuste_capital' if estado.get('em_incremento_capital', False) else 'compra_balanceamento'
                        # ✓ REFATORADO: Remover do JSON, manter apenas no SQLite
                        # estado['historico_trades'].append({...})
                        # ✓ CRÍTICO: Registrar com QUANTIDADE REAL comprada (diferenca_saldo, não quantidade_compra)
                        # porque retry pode ter ajustado quantidade_compra, mas o que realmente importa é
                        # quanto foi REALMENTE comprado na Binance (diferenca_saldo)
                        valor_compra_real = float(diferenca_saldo) * preco_atual_compra
                        registrar_trade_sqlite(moeda, tipo_compra, preco_atual_compra, float(diferenca_saldo), valor_compra_real)
                    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)
                else:
                    # ✓ NOVO: Ordem falhou mesmo após validação de notional passar!
                    # Isso geralmente indica condição de corrida (preço mudou entre validação e execução)
                    log_erro(f"  ✗ Ordem de compra falhou após validação de notional passar!")
                    log_erro(f"     Provável causa: Condição de corrida (preço mudou entre validação e execução)")
                    
                    # ✓ NOVO: Processar com Retry Inteligente para escalar valor na próxima tentativa
                    capital_rastreado = estado.get('capital_rastreado', estado.get('capital_inicial', 0))
                    deve_tentar, valor_ajustado_usd, ciclos_skip = processar_residual_com_retry_inteligente(
                        estado, moeda, diferenca_aprox, contador_ciclo, capital_rastreado, config
                    )
                    
                    if deve_tentar and valor_ajustado_usd and valor_ajustado_usd > 0:
                        log_info(f"  💡 Escalando valor para próximo ciclo: ${valor_compra:.2f} → ${valor_ajustado_usd:.2f}")
                        log_info(f"     Próxima tentativa em {ciclos_skip} ciclo(s)")
                        # Salvar estado antes de pular para próximo ciclo
                        salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
                    else:
                        log_aviso(f"  ⏸️  {moeda}: Aguardando melhor horário para tentar novamente")
                        salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
            elif not eh_valido:
                log_aviso(msg_validacao)
            else:
                if saldo_usdt_atual <= SALDO_MINIMO_USDT:
                    log_aviso(f"  {moeda}: ⛔ Compra bloqueada - Saldo USDT (${saldo_usdt_atual:.2f}) ≤ Proteção (${SALDO_MINIMO_USDT:.2f})")
                elif saldo_usdt_atual < (valor_compra + SALDO_MINIMO_USDT):
                    log_aviso(f"  {moeda}: ⛔ Compra bloqueada - Saldo insuficiente (precisa ${valor_compra + SALDO_MINIMO_USDT:.2f}, tem ${saldo_usdt_atual:.2f})")
                    log_aviso(f"       └─ USDT Necessário: ${valor_compra:.2f} + Proteção: ${SALDO_MINIMO_USDT:.2f}")
                else:
                    log_aviso(f"  {moeda}: Compra rejeitada por outro motivo (validação)")
    
    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)
    
    # ✓ NOVO: Calcular limite novamente para mostrar percentuais CORRETOS no resumo
    valor_portfolio_limite_final, ganho_final, _, _ = calcular_limite_reinvestimento(valor_portfolio_final, moedas, saldos, precos, estado)
    percentuais_finais = calcular_percentuais_atuais(moedas, saldos, precos, valor_portfolio_limite_final)
    
    # Mostrar com base no limite, não no valor real
    for moeda in moedas:
        pct_final = percentuais_finais.get(moeda, 0)
        saldo_moeda = saldos.get(moeda, 0)
        # ✓ saldo_moeda já é Decimal, exibir diretamente com str()
        log_info(f"{moeda}: {pct_final:.2f}% (Saldo: {str(saldo_moeda)})")
    
    # ✓ NOVO: Logar ganho REAL (baseado em histórico de trades)
    log_info(f"Valor Total (REAL): {valor_portfolio_final:.2f} {MOEDA_BASE}")
    log_info(f"Valor Total (CÁLCULOS): {valor_portfolio_limite_final:.2f} {MOEDA_BASE}")
    log_info(f"Ganho REAL (baseado em trades): {ganho_final:+.2f} {MOEDA_BASE}")
    
    # ✓ NOVO: Verificar se incremento de capital foi concluído
    # Se estamos em modo de incremento de capital, verificar se os percentuais estão próximos aos alvos
    if estado.get('em_incremento_capital', False):
        # Verificar desvio máximo de percentual
        desvios = [abs(percentuais_finais.get(m, 0) - estado['percentuais_alvo'].get(m, 0)) for m in moedas]
        desvio_maximo = max(desvios) if desvios else 0
        
        # Se desvio máximo < 2% dos alvos, considerar incremento concluído
        if desvio_maximo < 2.0:
            log_sucesso("\n✅ INCREMENTO DE CAPITAL CONCLUÍDO!")
            log_sucesso(f"   Desvio máximo de percentuais: {desvio_maximo:.2f}%")
            log_sucesso("   Voltando ao rebalanceamento normal (sem aguardar melhor hora)...")
            estado['em_incremento_capital'] = False
        else:
            log_info(f"\n⏳ Incremento de capital em progresso...")
            log_info(f"   Desvio máximo de percentuais: {desvio_maximo:.2f}% (alvo: < 2.0%)")
    
    # 🆕 NOVO: Verificar se retirada de capital foi concluída
    # Se estamos em modo de retirada de capital, verificar se conseguiu vender moedas suficientes
    if estado.get('em_retirada_capital', False):
        valor_retirada_solicitado = estado.get('valor_retirada', 0)
        usdt_atual = saldos.get('USDT', 0)
        
        # Verificar se já possui USDT suficiente para a retirada
        if usdt_atual >= valor_retirada_solicitado:
            log_sucesso("\n✅ RETIRADA DE CAPITAL CONCLUÍDA!")
            log_sucesso(f"   Valor retirado: ${valor_retirada_solicitado:.2f}")
            log_sucesso(f"   USDT disponível: ${usdt_atual:.2f}")
            log_sucesso("   Voltando ao rebalanceamento normal...")
            estado['em_retirada_capital'] = False
            estado['valor_retirada'] = 0  # Limpar valor
        else:
            usdt_faltando = valor_retirada_solicitado - usdt_atual
            percentual_concluido = (usdt_atual / valor_retirada_solicitado * 100) if valor_retirada_solicitado > 0 else 0
            log_info(f"\n⏳ Retirada de capital em progresso...")
            log_info(f"   Retirada solicitada: ${valor_retirada_solicitado:.2f}")
            log_info(f"   USDT já retirado: ${usdt_atual:.2f}")
            log_info(f"   Faltando retirar: ${usdt_faltando:.2f} ({percentual_concluido:.1f}% concluído)")
            log_info(f"   O robô continuará vendendo moedas para completar a retirada...")
    
    # ✓ NOVO: Rastrear moedas residuais para auditoria (apenas ÚLTIMA ocorrência por moeda)
    if moedas_residuais_ciclo:
        log_aviso(f"\n⚠️  MOEDAS RESIDUAIS NESTE CICLO: {', '.join(sorted(moedas_residuais_ciclo))}")
        log_aviso(f"    Estas moedas serão consolidadas quando forem realizadas futuras compras")
        log_aviso(f"    Verificar moedas_residuais_historico para detalhes de tentativas e backoff")
        
        # ✓ Usar dicionário (não array) para armazenar apenas ÚLTIMA ocorrência de cada moeda
        if 'moedas_residuais_historico' not in estado:
            estado['moedas_residuais_historico'] = {}
        
        for moeda in moedas_residuais_ciclo:
            # ✓ IMPORTANTE: NÃO sobrescrever se já tem estrutura completa da função inteligente
            # Só adicionar/atualizar se é um caso antigo sem os campos de retry
            if moeda in estado['moedas_residuais_historico']:
                residual_info = estado['moedas_residuais_historico'][moeda]
                # Se já tem tentativas_falhadas, é estrutura nova - não sobrescrever
                if 'tentativas_falhadas' in residual_info:
                    continue  # Já foi processada pela função inteligente
            
            # Estrutura simples (compatibilidade com código antigo)
            estado['moedas_residuais_historico'][moeda] = {
                'motivo': 'notional_insuficiente_ciclo_venda',
                'timestamp': datetime.now().isoformat(),
                'valor_aproximado': f"${float(saldos.get(moeda, 0)) * float(precos.get(moeda, 0)):.2f}"
            }
    
    # ✓ NOVO: Atualizar quantidades reais antes de salvar estado
    atualizar_quantidade_carteira_real(PORTFOLIO_STATE_FILE, moedas, saldos)
    
    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
        
        # ✓ NOVO: Atualizar quantidades reais antes de salvar estado
        atualizar_quantidade_carteira_real(PORTFOLIO_STATE_FILE, moedas, saldos)
        
        # 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 = float(saldo) * preco if preco else 0
            pct = percentuais.get(moeda, 0)
            pct_alvo = estado.get('percentuais_alvo', {}).get(moeda, 0)
            
            # ✓ saldo já é Decimal, usar str() para exibição precisa
            log_info(f"{moeda}: Saldo={str(saldo):>16} | " +
                f"Valor: {float(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:
            # Verificar se recebeu sinal de parada do controlador web
            if controlador_stop and controlador_stop.deve_parar():
                info = controlador_stop.obter_info_parada()
                log_info("\n" + "=" * 70)
                log_info("⏹️  SINAL DE PARADA RECEBIDO")
                if info:
                    log_info(f"Solicitado por: {info.get('pedido_por')}")
                    log_info(f"Timestamp: {info.get('timestamp')}")
                log_info("Encerrando robô gracefully...")
                log_info("=" * 70)
                controlador_stop.limpar_stop()
                break
            
            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, contador_ciclo)
                
                # ✓ NOVO: Salvar o contador_ciclo no estado para exibição em tempo real no painel web
                estado['contador_ciclo_atual'] = contador_ciclo
                salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
                
                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()
