# -*- 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

# Import do controlador de parada
try:
    import controlador_stop
except ImportError:
    controlador_stop = None

init()  # Inicializa suporte a ANSI em Windows

# ===================== CONFIGURAÇÕES INICIAIS =====================

def carregar_configuracoes(filepath):
    """Carrega configurações do arquivo JSON"""
    with open(filepath, 'r', encoding='utf-8') as file:
        return json.load(file)

def salvar_estado_portfolio(filepath, estado):
    """Salva o estado atual do portfólio"""
    with open(filepath, 'w', encoding='utf-8') as file:
        json.dump(estado, file, indent=2, ensure_ascii=False)

def carregar_estado_portfolio(filepath):
    """Carrega o estado do portfólio se existir"""
    try:
        if os.path.exists(filepath):
            with open(filepath, 'r', encoding='utf-8') as file:
                data = json.load(file)
                if data:
                    return data
                else:
                    logging.warning(f"Arquivo {filepath} está vazio")
                    return None
        return None
    except json.JSONDecodeError as e:
        logging.error(f"Erro ao decodificar JSON em {filepath}: {e}")
        return None
    except Exception as e:
        logging.error(f"Erro ao carregar estado: {e}")
        return None

# Configurar locale e timezone
try:
    locale.setlocale(locale.LC_ALL, 'pt_BR.UTF-8')
except locale.Error:
    try:
        locale.setlocale(locale.LC_ALL, 'Portuguese_Brazil.1252')
    except locale.Error:
        pass

# Carregar configurações
config = carregar_configuracoes('configuracoes_balanceamento.json')

# ===================== VARIÁVEIS GLOBAIS =====================

API_KEY = config.get("api_key", "")
API_SECRET = config.get("api_secret", "")
API_KEY_TESTNET = config.get("api_key_testnet", "")
API_SECRET_TESTNET = config.get("api_secret_testnet", "")
TESTNET = config.get("testnet", False)
MOEDA_BASE = config.get("moeda_base", "USDT")
CAPITAL_INICIAL = float(config.get("capital_inicial", 1000))
TEMPO_CICLO = int(config.get("tempo_ciclo", 5))
TEMPO_REFRESH = int(config.get("tempo_refresh_tela", 5))
TIMEFRAME = config.get("timeframe", "5m")
VARIACAO_BALANCEAMENTO = float(config.get("variacao_balanceamento", 2.0))
COMISSAO = float(config.get("comissao", 0.1)) / 100
SALDO_MINIMO = float(config.get("saldo_minimo", 10))
SALDO_MINIMO_USDT = float(config.get("saldo_minimo_usdt", 100))  # Proteção USDT: não usa USDT abaixo deste mínimo
VALOR_MINIMO_OPERACAO = 5.0  # Valor mínimo de operação na Binance em USD/USDT
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"""
    max_tentativas = 3
    for tentativa in range(max_tentativas):
        try:
            balance = exchange.fetch_balance()
            free = balance.get('free', {})
            return float(free.get(ativo, 0))
        except Exception as e:
            erro_str = str(e)
            # Verificar se é erro de timestamp (-1021)
            if "-1021" in erro_str or "Timestamp" in erro_str:
                if tentativa < max_tentativas - 1:
                    log_aviso(f"Erro de timestamp ao obter saldo de {ativo}, resincronizando... (tentativa {tentativa + 1}/{max_tentativas})")
                    global time_offset
                    time_offset = sincronizar_timestamp_binance()
                    exchange.options['timeDifference'] = time_offset  # Usar timeDifference
                    time.sleep(2)  # Esperar 2 segundos antes de retry
                    continue
            
            if tentativa == max_tentativas - 1:
                log_erro(f"Erro ao obter saldo de {ativo}: {e}")
            return 0.0

def obter_saldos_portfolio(moedas):
    """Obtém saldos de todas as moedas do portfólio"""
    saldos = {}
    for moeda in moedas:
        saldos[moeda] = obter_saldo(moeda)
    saldos[MOEDA_BASE] = obter_saldo(MOEDA_BASE)
    return saldos

def verificar_par_disponivel(symbol):
    """
    Verifica se um par de trading está disponível e funcionando no testnet
    
    Returns:
        tuple: (está_disponível, info_diagnóstica)
    """
    try:
        # Tentar carregar informações do mercado do par
        if symbol not in exchange.symbols:
            return False, f"Par {symbol} não está na lista de símbolos disponíveis no testnet"
        
        # Tentar obter ticker (informações de preço)
        ticker = exchange.fetch_ticker(symbol)
        last_price = ticker.get('last') or ticker.get('close')
        volume = ticker.get('baseVolume')
        
        if not last_price or last_price <= 0:
            return False, f"Par {symbol} não tem preço válido (último: {last_price})"
        
        if volume is None or volume == 0:
            return False, f"Par {symbol} tem volume zero - sem liquidez no testnet"
        
        return True, f"Par {symbol} OK - Preço: {last_price:.8f}, Volume: {volume:.8f}"
        
    except Exception as e:
        return False, f"Erro ao verificar par {symbol}: {str(e)}"

def obter_valor_portfolio(moedas, saldos, precos):
    """Calcula o valor total do portfólio em USDT"""
    valor_total = saldos.get(MOEDA_BASE, 0)
    for moeda in moedas:
        quantidade = saldos.get(moeda, 0)
        preco = precos.get(moeda, 0)
        if quantidade > 0 and preco:
            valor_total += quantidade * preco
    return valor_total

def calcular_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 com histórico_trades
    
    Returns:
        float: Ganho/Perda real em USDT (apenas das moedas compradas)
    """
    # Calcular capital investido em moedas (somar compras - vendas do histórico)
    capital_investido_moedas = 0
    if estado and 'historico_trades' in estado:
        for trade in estado['historico_trades']:
            if trade['tipo'] in ['compra_inicial', 'compra_balanceamento']:  # ✓ Tipos corretos
                capital_investido_moedas += trade['valor']
            elif trade['tipo'] in ['venda_balanceamento']:  # ✓ Tipo correto (sem venda_construcao)
                capital_investido_moedas -= trade['valor']
    
    # 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:
                valor_moedas_atual += 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:
            valor_moeda = quantidade * preco
            percentuais[moeda] = (valor_moeda / valor_total) * 100
        else:
            percentuais[moeda] = 0
    
    return percentuais

def remover_outliers_iqr(precos):
    """
    Remove outliers usando o método Interquartile Range (IQR)
    
    Args:
        precos: lista de preços
    
    Returns:
        tuple: (preços filtrados sem outliers, índices dos outliers)
    """
    if len(precos) < 4:
        return precos, []
    
    # Calcular Q1 e Q3
    sorted_precos = sorted(precos)
    n = len(sorted_precos)
    q1_idx = n // 4
    q3_idx = 3 * n // 4
    
    q1 = sorted_precos[q1_idx]
    q3 = sorted_precos[q3_idx]
    iqr = q3 - q1
    
    # Definir limites (1.5 × IQR é o padrão)
    limite_inferior = q1 - 1.5 * iqr
    limite_superior = q3 + 1.5 * iqr
    
    # Filtrar outliers
    precos_filtrados = []
    indices_outliers = []
    
    for idx, preco in enumerate(precos):
        if limite_inferior <= preco <= limite_superior:
            precos_filtrados.append(preco)
        else:
            indices_outliers.append(idx)
    
    return precos_filtrados, indices_outliers

def obter_melhores_precos_5dias(moedas):
    """
    DESCONTINUADO: Use obter_melhores_precos_30dias() em vez disso.
    Mantido para compatibilidade com versões antigas.
    """
    return obter_melhores_precos_30dias(moedas)

def obter_melhores_precos_30dias(moedas):
    """
    Busca os melhores preços (máxima e mínima) dos últimos 30 dias (ou máximo disponível) para cada moeda
    usando dados OHLCV (Open, High, Low, Close, Volume) da exchange
    Remove outliers para análise de padrões de horário mais confiáveis
    
    Retorna análise consolidada sem detalhe dia a dia para maior confiabilidade.
    Com ~250+ candles (≈10-11 dias), a confiança dos padrões é muito boa.
    
    Returns:
        dict: {
            'moeda': {
                'melhor_hora_compra': { hora, confianca%, ocorrencias },
                'melhor_hora_venda': { hora, confianca%, ocorrencias },
                'analise': {
                    'total_registros': int (candles coletados),
                    'registros_validos': int (após remover outliers),
                    'total_outliers': int,
                    'percentual_outliers': float (%)
                },
                'timestamp': str (ISO 8601)
            }
        }
    """
    melhores_precos = {}
    
    try:
        # Buscar histórico máximo disponível em candles de 1 hora
        # Idealmente 30 dias × 24 horas = 720 candles, mas aceita o máximo disponível
        timeframe = '1h'
        
        for moeda in moedas:
            try:
                symbol = f"{moeda}/{MOEDA_BASE}"
                
                # Buscar máximo de candles disponíveis (a API retorna o máximo que consegue)
                candles = exchange.fetch_ohlcv(symbol, timeframe, limit=250)
                
                if not candles or len(candles) < 200:  # Mínimo 8-9 dias de dados
                    log_aviso(f"Candles insuficientes para {moeda} (precisa 200, tem {len(candles)})")
                    continue
                
                # Usar todos os candles disponíveis
                candles = candles[-250:] if len(candles) >= 250 else candles
                
                total_registros = len(candles)
                
                # Extrair todos os preços altos e baixos para análise consolidada
                highs = [candle[2] for candle in candles]
                lows = [candle[3] for candle in candles]
                
                # Remover outliers usando IQR
                highs_filtrados, outliers_high = remover_outliers_iqr(highs)
                lows_filtrados, outliers_low = remover_outliers_iqr(lows)
                
                # Contar outliers únicos
                indices_outliers = set(outliers_high) | set(outliers_low)
                total_outliers = len(indices_outliers)
                registros_validos = total_registros - total_outliers
                percentual_outliers = (total_outliers / total_registros * 100) if total_registros > 0 else 0
                
                # Agrupamento por DATA para análise diária
                # Para cada dia, encontrar qual período é melhor para compra/venda
                candles_por_data = {}
                
                for idx, candle in enumerate(candles):
                    if idx not in indices_outliers:
                        timestamp = candle[0]
                        dt = datetime.fromtimestamp(timestamp / 1000, tz=timezone)
                        data_str = dt.strftime('%Y-%m-%d')
                        
                        if data_str not in candles_por_data:
                            candles_por_data[data_str] = []
                        
                        candles_por_data[data_str].append({
                            'hora': dt.hour,
                            'low': candle[3],
                            'high': candle[2]
                        })
                
                total_dias = len(candles_por_data)
                
                # Definir grupos de 4 horas com rastreamento de horas exatas
                grupos_horas = {
                    0: {'label': '00:00-03:59', 'dias_vencedor_compra': 0, 'dias_vencedor_venda': 0, 'horas_compra': [], 'horas_venda': []},
                    1: {'label': '04:00-07:59', 'dias_vencedor_compra': 0, 'dias_vencedor_venda': 0, 'horas_compra': [], 'horas_venda': []},
                    2: {'label': '08:00-11:59', 'dias_vencedor_compra': 0, 'dias_vencedor_venda': 0, 'horas_compra': [], 'horas_venda': []},
                    3: {'label': '12:00-15:59', 'dias_vencedor_compra': 0, 'dias_vencedor_venda': 0, 'horas_compra': [], 'horas_venda': []},
                    4: {'label': '16:00-19:59', 'dias_vencedor_compra': 0, 'dias_vencedor_venda': 0, 'horas_compra': [], 'horas_venda': []},
                    5: {'label': '20:00-23:59', 'dias_vencedor_compra': 0, 'dias_vencedor_venda': 0, 'horas_compra': [], 'horas_venda': []},
                }
                
                # Processar cada dia
                for data_str, candles_dia in candles_por_data.items():
                    if not candles_dia:
                        continue
                    
                    # Agrupar candles do dia por período de 4 horas
                    grupos_dia = {i: [] for i in range(6)}
                    
                    for candle in candles_dia:
                        grupo_idx = candle['hora'] // 4
                        if grupo_idx >= 6:
                            grupo_idx = 5
                        grupos_dia[grupo_idx].append(candle)
                    
                    # Calcular preço médio por período neste dia
                    precos_medios_compra = {}  # Menor preço = melhor
                    precos_medios_venda = {}   # Maior preço = melhor
                    
                    for grupo_idx, candles_grupo in grupos_dia.items():
                        if candles_grupo:
                            preco_medio_low = sum(c['low'] for c in candles_grupo) / len(candles_grupo)
                            preco_medio_high = sum(c['high'] for c in candles_grupo) / len(candles_grupo)
                            precos_medios_compra[grupo_idx] = preco_medio_low
                            precos_medios_venda[grupo_idx] = preco_medio_high
                    
                    # Encontrar melhor período para COMPRA (menor preço médio)
                    if precos_medios_compra:
                        melhor_compra_dia = min(precos_medios_compra, key=precos_medios_compra.get)
                        grupos_horas[melhor_compra_dia]['dias_vencedor_compra'] += 1
                        # Rastrear as horas exatas do período vencedor
                        for candle in grupos_dia[melhor_compra_dia]:
                            grupos_horas[melhor_compra_dia]['horas_compra'].append(candle['hora'])
                    
                    # Encontrar melhor período para VENDA (maior preço médio)
                    if precos_medios_venda:
                        melhor_venda_dia = max(precos_medios_venda, key=precos_medios_venda.get)
                        grupos_horas[melhor_venda_dia]['dias_vencedor_venda'] += 1
                        # Rastrear as horas exatas do período vencedor
                        for candle in grupos_dia[melhor_venda_dia]:
                            grupos_horas[melhor_venda_dia]['horas_venda'].append(candle['hora'])
                
                if total_dias == 0:
                    log_aviso(f"{moeda}: Nenhum dia com dados válidos")
                    continue
                
                # Encontrar grupos vencedores com base em quantos dias foram melhores
                melhor_grupo_compra = max(grupos_horas, key=lambda x: grupos_horas[x]['dias_vencedor_compra'])
                melhor_grupo_venda = max(grupos_horas, key=lambda x: grupos_horas[x]['dias_vencedor_venda'])
                
                # Calcular confiança como: dias em que foi o melhor / total de dias × 100
                dias_vencedor_compra = grupos_horas[melhor_grupo_compra]['dias_vencedor_compra']
                dias_vencedor_venda = grupos_horas[melhor_grupo_venda]['dias_vencedor_venda']
                
                confianca_compra = (dias_vencedor_compra / total_dias * 100) if total_dias > 0 else 0
                confianca_venda = (dias_vencedor_venda / total_dias * 100) if total_dias > 0 else 0
                
                # Calcular hora média das ocorrências
                def calcular_hora_media(horas_list):
                    """Calcula a hora média em formato HH:MM"""
                    if not horas_list:
                        return 'N/A'
                    
                    # Calcular hora média (convertendo para minutos)
                    total_minutos = sum(h * 60 for h in horas_list)
                    media_minutos = total_minutos / len(horas_list)
                    horas = int(media_minutos // 60)
                    minutos = int(media_minutos % 60)
                    return f"{horas:02d}:{minutos:02d}"
                
                hora_media_compra = calcular_hora_media(grupos_horas[melhor_grupo_compra]['horas_compra'])
                hora_media_venda = calcular_hora_media(grupos_horas[melhor_grupo_venda]['horas_venda'])
                
                # Montar dados consolidados
                dados_moeda = {
                    'melhor_hora_compra': {
                        'hora': grupos_horas[melhor_grupo_compra]['label'],
                        'hora_media': hora_media_compra,
                        'confianca': round(confianca_compra, 1),
                        'ocorrencias': dias_vencedor_compra,
                    },
                    'melhor_hora_venda': {
                        'hora': grupos_horas[melhor_grupo_venda]['label'],
                        'hora_media': hora_media_venda,
                        'confianca': round(confianca_venda, 1),
                        'ocorrencias': dias_vencedor_venda,
                    },
                    'analise': {
                        'total_registros': total_registros,
                        'registros_validos': registros_validos,
                        'total_outliers': total_outliers,
                        'percentual_outliers': round(percentual_outliers, 1),
                        'total_dias': total_dias  # ✅ NOVO: Salvar total_dias para sincronizar com PHP
                    },
                    'timestamp': datetime.now(tz=timezone).isoformat()
                }
                
                melhores_precos[moeda] = dados_moeda
                
                # Log informativo consolidado
                log_info(f"  {moeda}:")
                log_info(f"    📊 Registros: {total_registros} coletados | {registros_validos} válidos ({100-percentual_outliers:.1f}%)")
                log_info(f"    ⚠️  Outliers: {total_outliers} removidos ({percentual_outliers:.1f}%)")
                log_info(f"    ✓ Faixa COMPRA:  {dados_moeda['melhor_hora_compra']['hora']} | Hora Média: {hora_media_compra} | Confiança: {confianca_compra:.1f}% | Melhor em {dias_vencedor_compra} de {total_dias} dias")
                log_info(f"    ✓ Faixa VENDA:   {dados_moeda['melhor_hora_venda']['hora']} | Hora Média: {hora_media_venda} | Confiança: {confianca_venda:.1f}% | Melhor em {dias_vencedor_venda} de {total_dias} dias")
                
            except Exception as e:
                log_aviso(f"Erro ao buscar dados de {moeda}: {e}")
                continue
        
        return melhores_precos
        
    except Exception as e:
        log_erro(f"Erro ao obter melhores preços: {e}")
        return {}

def atualizar_melhores_precos_portfolio(estado, moedas):
    """
    Atualiza o arquivo de estado do portfólio com os melhores preços dos últimos 30 dias (ou máximo disponível)
    Incluindo análise de padrões de horário e detecção de outliers
    """
    try:
        log_info("Atualizando dados de melhores preços (até 30 dias com análise consolidada)...")
        
        melhores_precos = obter_melhores_precos_30dias(moedas)
        
        if melhores_precos:
            estado['best_prices_30days'] = melhores_precos
            salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
            log_info(f"✓ Dados de melhores preços (30 dias) atualizados para {len(melhores_precos)} moedas")
        
        return estado
        
    except Exception as e:
        log_erro(f"Erro ao atualizar melhores preços: {e}")
        return estado

def executar_ordem(symbol, side, quantidade):
    """
    Executa uma ordem de compra ou venda (limit order)
    Limit orders são mais confiáveis no testnet que market orders
    """
    try:
        # Obter preço atual para usar como referência no limit order
        preco_atual = obter_preco_atual(symbol)
        
        if not preco_atual or preco_atual <= 0:
            log_erro(f"Falha ao obter preço para {symbol}. Não será possível criar limit order.")
            return None
        
        # Calcular preço limite (adiciona 1% de margem para garantir execução)
        if side == 'buy':
            preco_limite = preco_atual * 1.02  # 2% acima para compra
        else:
            preco_limite = preco_atual * 0.98  # 2% abaixo para venda
        
        log_info(f"  Criando limit order: {side} {quantidade} {symbol}")
        log_info(f"    Preço Atual: {preco_atual:.8f}")
        log_info(f"    Preço Limite: {preco_limite:.8f}")
        
        # Tentar criar limit order em vez de market order
        try:
            ordem = exchange.create_limit_order(
                symbol, 
                side, 
                quantidade, 
                preco_limite,
                {
                    'timeInForce': 'GTC',  # Good-Till-Cancelled (no time limit)
                }
            )
            timestamp = datetime.now(timezone).strftime("%d/%m/%Y %H:%M:%S")
            
            # Log detalhado da ordem
            order_id = ordem.get('id', 'N/A')
            status = ordem.get('status', 'desconhecido')
            filled = ordem.get('filled', 0)
            amount = ordem.get('amount', 0)
            
            log_info(f"  ID: {order_id} | Status: {status} | Preenchida: {filled:.8f}/{amount:.8f}")
            
            # Diagnosticar se ordem expirou ou foi rejeitada
            if status == 'expired':
                log_erro(f"  ⚠️ AVISO: Ordem expirou imediatamente (ID: {order_id})")
                log_erro(f"     Possíveis causas:")
                log_erro(f"     1. Par {symbol} pode não ter liquidez no testnet")
                log_erro(f"     2. Quantidade {quantidade:.8f} pode ser inválida")
                log_erro(f"     3. Preço {preco_limite:.8f} fora do spread")
                return None
            elif status == 'canceled':
                log_erro(f"  ⚠️ AVISO: Ordem foi cancelada pela Binance (ID: {order_id})")
                return None
            
            if side == 'sell':
                log_sucesso(f"✓ Ordem de VENDA executada: {quantidade:.8f} {symbol} em {timestamp}")
            else:
                log_sucesso(f"✓ Ordem de COMPRA executada: {quantidade:.8f} {symbol} em {timestamp}")
            
            return ordem
            
        except Exception as e_limit:
            erro_str = str(e_limit)
            log_aviso(f"  Limit order falhou: {erro_str}")
            log_aviso(f"  Tentando market order como fallback...")
            
            # Fallback para market order se limit order falhar
            try:
                ordem = exchange.create_market_order(symbol, side, quantidade)
                timestamp = datetime.now(timezone).strftime("%d/%m/%Y %H:%M:%S")
                
                order_id = ordem.get('id', 'N/A')
                status = ordem.get('status', 'desconhecido')
                filled = ordem.get('filled', 0)
                amount = ordem.get('amount', 0)
                
                log_info(f"  ID: {order_id} | Status: {status} | Preenchida: {filled:.8f}/{amount:.8f}")
                
                if status == 'expired':
                    log_erro(f"  ⚠️ Market order também expirou (ID: {order_id})")
                    log_erro(f"     Problema aparentemente com par/liquidez no testnet")
                    return None
                
                if side == 'sell':
                    log_sucesso(f"✓ Ordem de VENDA (market) executada: {quantidade:.8f} {symbol} em {timestamp}")
                else:
                    log_sucesso(f"✓ Ordem de COMPRA (market) executada: {quantidade:.8f} {symbol} em {timestamp}")
                
                return ordem
            except Exception as e_market:
                log_erro(f"  Ambas market e limit order falharam para {symbol}")
                log_erro(f"  Market error: {str(e_market)}")
                return None
                
    except Exception as e:
        log_erro(f"Falha ao executar ordem {side} para {symbol} ({quantidade:.8f}): {e}")
        log_erro(f"  Rastreamento: {traceback.format_exc()}")
        return None

def aplicar_comissao(quantidade):
    """Aplica comissão à quantidade"""
    return quantidade * (1 - COMISSAO)

def validar_valor_operacao(valor_operacao, moeda, tipo_operacao):
    """
    Valida se o valor da operação atende ao mínimo da Binance ($5 USD/USDT)
    
    Args:
        valor_operacao: Valor da operação em USDT
        moeda: Símbolo da moeda (ex: BTC)
        tipo_operacao: 'compra' ou 'venda'
    
    Returns:
        tuple: (é_válido, mensagem)
    """
    if valor_operacao < VALOR_MINIMO_OPERACAO:
        mensagem = (f"Operação de {tipo_operacao} de {moeda} BLOQUEADA: "
                  f"Valor ${valor_operacao:.2f} é inferior ao mínimo de "
                  f"${VALOR_MINIMO_OPERACAO:.2f} exigido pela Binance")
        return False, mensagem
    return True, f"Operação de {tipo_operacao} de {moeda} validada: ${valor_operacao:.2f}"

# ===================== FUNÇÕES DE OTIMIZAÇÃO DE TIMING =====================

def extrair_horario_compra(estado, moeda):
    """
    Extrai a melhor hora de compra do estado do portfólio
    
    Args:
        estado: Estado atual do portfólio
        moeda: Símbolo da moeda
    
    Returns:
        tuple: (hora_inicio, hora_fim, confianca) ou (None, None, 0) se não encontrar
        Exemplo: (8, 11, 60.5) para o período 08:00-11:59
    """
    try:
        if 'best_prices_30days' not in estado or moeda not in estado['best_prices_30days']:
            return None, None, 0
        
        dados_moeda = estado['best_prices_30days'][moeda]
        if 'melhor_hora_compra' not in dados_moeda:
            return None, None, 0
        
        melhor_hora = dados_moeda['melhor_hora_compra']['hora']  # Ex: '08:00-11:59'
        confianca = dados_moeda['melhor_hora_compra']['confianca']
        
        # Extrair horas do formato '08:00-11:59'
        partes = melhor_hora.split('-')
        if len(partes) == 2:
            hora_inicio = int(partes[0].split(':')[0])
            hora_fim = int(partes[1].split(':')[0])
            return hora_inicio, hora_fim, confianca
        
        return None, None, 0
    except Exception as e:
        log_aviso(f"Erro ao extrair horário de compra para {moeda}: {e}")
        return None, None, 0

def eh_horario_compra_ideal(moeda, estado):
    """
    Verifica se a hora atual está dentro do melhor período de compra para a moeda
    
    Args:
        moeda: Símbolo da moeda
        estado: Estado atual do portfólio com best_prices_30days
    
    Returns:
        tuple: (está_no_horário_ideal, mensagem_info)
    """
    hora_inicio, hora_fim, confianca = extrair_horario_compra(estado, moeda)
    
    if hora_inicio is None:
        return False, f"{moeda}: Dados de melhor hora não disponíveis (análise em progresso)"
    
    hora_atual = datetime.now(tz=timezone).hour
    
    # Verificar se está dentro do período
    if hora_inicio <= hora_fim:
        # Período normal (ex: 08:00-11:59)
        esta_dentro = hora_inicio <= hora_atual <= hora_fim
    else:
        # Período que passa pela meia-noite (ex: 20:00-03:59) - não deve acontecer neste caso
        esta_dentro = hora_atual >= hora_inicio or hora_atual <= hora_fim
    
    periodo_str = f"{hora_inicio:02d}:00-{hora_fim:02d}:59"
    
    if esta_dentro:
        mensagem = f"{moeda}: ✓ HORÁRIO IDEAL de compra! ({periodo_str}, confiança: {confianca:.1f}%)"
        return True, mensagem
    else:
        proxima_hora_compra = f"{hora_inicio:02d}:00"
        mensagem = f"{moeda}: ⏰ Aguardando melhor hora de compra ({periodo_str}, confiança: {confianca:.1f}%). Próxima às {proxima_hora_compra}"
        return False, mensagem

def calcular_tempo_ate_melhor_hora(moeda, estado):
    """
    Calcula quantos minutos faltam até a melhor hora de compra
    
    Args:
        moeda: Símbolo da moeda
        estado: Estado atual do portfólio
    
    Returns:
        int: Minutos até a melhor hora (ou 0 se já está na melhor hora)
    """
    hora_inicio, hora_fim, _ = extrair_horario_compra(estado, moeda)
    
    if hora_inicio is None:
        return 0
    
    agora = datetime.now(tz=timezone)
    hora_atual = agora.hour
    minuto_atual = agora.minute
    
    if hora_inicio <= hora_atual <= hora_fim:
        # Já está na melhor hora
        return 0
    
    if hora_atual < hora_inicio:
        # Próxima melhor hora é hoje
        tempo_ate = (hora_inicio - hora_atual) * 60 - minuto_atual
    else:
        # Próxima melhor hora é amanhã
        tempo_ate = (24 - hora_atual + hora_inicio) * 60 - minuto_atual
    
    return max(0, tempo_ate)

def aguardar_melhor_hora_compra(moeda, estado, tempo_maximo_minutos=None):
    """
    Aguarda até a melhor hora de compra para a moeda
    
    Args:
        moeda: Símbolo da moeda
        estado: Estado atual do portfólio
        tempo_maximo_minutos: Tempo máximo de espera em minutos (None = 5 min como padrão)
    
    Returns:
        tuple: (aguardou, tempo_esperado_minutos, mensagem)
    """
    # Usar 5 minutos como padrão para evitar travamentos indefinidos
    if tempo_maximo_minutos is None:
        tempo_maximo_minutos = 5
    
    hora_inicio, hora_fim, confianca = extrair_horario_compra(estado, moeda)
    
    if hora_inicio is None:
        log_aviso(f"{moeda}: Análise de melhor hora não disponível - prosseguindo com compra imediata")
        return False, 0, f"{moeda}: Análise de melhor hora não disponível"
    
    tempo_inicial = time.time()
    tempo_decorrido_segundos = 0
    intervalo_verificacao = 10  # Verificar a cada 10 segundos (reduzido de 60)
    proximo_log = 0  # Para controlar logs periódicos
    
    try:
        while True:
            esta_ideal, msg = eh_horario_compra_ideal(moeda, estado)
            
            if esta_ideal:
                tempo_esperado_min = round(tempo_decorrido_segundos / 60, 1)
                mensagem_sucesso = f"{moeda}: ✓ Horário ideal de compra atingido após {tempo_esperado_min} min de espera (confiança: {confianca:.1f}%)"
                log_sucesso(mensagem_sucesso)
                return True, tempo_esperado_min, mensagem_sucesso
            
            tempo_decorrido_segundos = time.time() - tempo_inicial
            tempo_decorrido_minutos = tempo_decorrido_segundos / 60
            
            # Verificar se excedeu tempo máximo
            if tempo_decorrido_minutos >= tempo_maximo_minutos:
                log_aviso(f"{moeda}: Tempo máximo de espera ({tempo_maximo_minutos} min) atingido. Prosseguindo com compra.")
                return False, tempo_decorrido_minutos, f"{moeda}: Timeout de espera atingido"
            
            # Log a cada 2 minutos
            if tempo_decorrido_minutos >= proximo_log:
                tempo_até = calcular_tempo_ate_melhor_hora(moeda, estado)
                log_info(f"{moeda}: Aguardando melhor hora... ({tempo_decorrido_minutos:.1f}/{tempo_maximo_minutos} min, {tempo_até} min até a janela ideal)")
                proximo_log = tempo_decorrido_minutos + 2
            
            # Aguardar com intervalo reduzido antes de verificar novamente
            time.sleep(intervalo_verificacao)
    
    except Exception as e:
        log_erro(f"Erro ao aguardar melhor hora para {moeda}: {e}")
        return False, 0, f"{moeda}: Erro na espera - prosseguindo com compra"

# ===================== FUNÇÕES 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:
            valor_venda = saldo * preco
            
            log_aviso(f"\n🔄 Vendendo {moeda} residual:")
            log_info(f"   Saldo: {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 {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
    saldo_usdt_residual = max(0, saldo_usdt_final - 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

# ===================== 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: Detectar se houve incremento de capital
        capital_rastreado = estado_existente.get('capital_rastreado', estado_existente.get('capital_inicial', 0))
        
        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()
        else:
            # Sem incremento significativo, 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
        
        # ✓ 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)
        
        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
            'moedas_compradas': moedas.copy(),  # Todas já existem
            'moedas_pendentes': [],
            'percentuais_alvo': percentuais_alvo,
            'precos_compra': precos_compra,
            'historico_trades': []
        }
        log_info("✓ Fase de Balanceamento iniciada (portfólio existente)")
        return estado, moedas
    
    # Novo portfólio (sem moedas no testnet)
    estado = {
        'fase': 'construcao',
        'data_inicio': datetime.now(timezone).isoformat(),
        'capital_inicial': CAPITAL_INICIAL,
        'capital_rastreado': CAPITAL_INICIAL,  # ✓ NOVO: Rastrear capital para detectar incrementos
        'em_incremento_capital': False,  # ✓ NOVO: Flag para modo incremento de capital
        'moedas_compradas': [],
        'moedas_pendentes': moedas.copy(),
        'percentuais_alvo': percentuais_alvo,
        'precos_compra': precos_compra,
        'historico_trades': []
    }
    
    log_info("===================== NOVO PORTFÓLIO INICIADO =====================")
    log_info(f"Capital Inicial: {CAPITAL_INICIAL} {MOEDA_BASE}")
    log_info(f"Moedas no Portfólio: {', '.join(moedas)}")
    log_info(f"Percentuais Alvo: {percentuais_alvo}")
    log_info(f"Preços de Compra (entrada): {precos_compra}")
    log_info("=" * 60)
    
    return estado, moedas

def fase_construcao_portfolio(estado, moedas):
    """
    Fase 1: Completa o portfólio comprando as moedas até os preços definidos
    """
    log_info("\n========== FASE DE CONSTRUÇÃO DO PORTFÓLIO ==========")
    
    # ✓ 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 = saldo_usdt
    for moeda in moedas:
        quantidade = saldos.get(moeda, 0)
        preco = precos.get(moeda, 0)
        if quantidade > 0 and preco:
            valor_portfolio += quantidade * preco
    
    # ✓ 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)
        usdt_residual = max(0, saldo_usdt - 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:
            log_info(f"{moeda}: Já possui {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 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
                    quantidade = valor_compra / preco_atual * (1 - COMISSAO)
                    valor_ajustado = True
                    log_aviso(f"    ⚡ Valor ajustado de ${valor_compra_original:.2f} para ${valor_compra:.2f} (mínimo da Binance)")
                
                # Usar saldo real para compra, respeitando proteção de USDT residual
                saldo_disponivel = saldo_usdt if saldo_usdt > 0 else CAPITAL_INICIAL
                
                # Validar valor mínimo de operação na Binance
                eh_valido, msg_validacao = validar_valor_operacao(valor_compra, moeda, 'compra')
                
                # ✓ 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: {quantidade:.8f}")
                    
                    # Verificar se o par está disponível ANTES de tentar comprar
                    symbol = f"{moeda}/{MOEDA_BASE}"
                    par_disponivel, info_par = verificar_par_disponivel(symbol)
                    
                    if not par_disponivel:
                        log_erro(f"  ⚠️ Par {symbol} não está disponível no testnet!")
                        log_erro(f"     {info_par}")
                        log_aviso(f"  Pulando compra de {moeda} - par indisponível")
                        continue
                    
                    log_info(f"  ✓ {info_par}")
                    
                    # ✓ NOVO: Aguardar melhor hora de compra para maximizar lucros
                    # Timeout de 5 minutos para construção inicial (evita travamentos)
                    # 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, tempo_maximo_minutos=5)
                        if aguardou:
                            log_sucesso(f"  {msg_tempo}")
                        else:
                            log_info(f"  {msg_tempo}")
                    else:
                        log_info(f"  → Compra imediata (aguardar_melhor_hora desabilitado)")
                    
                    # Obter saldo ANTES da compra
                    saldo_antes = obter_saldo(moeda)
                    
                    ordem = executar_ordem(symbol, 'buy', quantidade)
                    
                    if ordem:
                        # Aguardar um pouco para a Binance processar
                        time.sleep(1)
                        
                        # Obter saldo DEPOIS da compra
                        saldo_depois = obter_saldo(moeda)
                        
                        # Se saldo não mudou, aguardar mais e tentar novamente
                        if saldo_depois == saldo_antes:
                            log_aviso(f"  Saldo não mudou na primeira verificação, aguardando mais 2s...")
                            time.sleep(2)
                            saldo_depois = obter_saldo(moeda)
                        
                        # Verificar se o saldo realmente aumentou
                        diferenca_saldo = saldo_depois - saldo_antes
                        
                        if diferenca_saldo > 0:
                            log_sucesso(f"✓ Compra verificada: Saldo aumentou {diferenca_saldo:.8f} {moeda}")
                            if moeda not in moedas_compradas:
                                moedas_compradas.append(moeda)
                            estado['moedas_compradas'] = moedas_compradas
                            estado['historico_trades'].append({
                                'timestamp': datetime.now(timezone).isoformat(),
                                'moeda': moeda,
                                'tipo': 'compra_inicial',
                                'preco': preco_atual,
                                'quantidade': quantidade,
                                'valor': valor_compra
                            })
                        else:
                            log_erro(f"✗ FALHA: Compra de {moeda} não confirmada! Saldo não aumentou.")
                            log_erro(f"  Antes: {saldo_antes:.8f} {moeda} | Depois: {saldo_depois:.8f} {moeda}")
                            log_erro(f"  ⚠ AVISO: A Binance aceitou a ordem mas não creditou o saldo!")
                            log_erro(f"  Possível causa: Problema no par {symbol} no testnet")
                        
                        time.sleep(1)
                elif not eh_valido:
                    log_aviso(msg_validacao)
                else:
                    log_aviso(f"Saldo insuficiente para comprar {moeda} (precisa {valor_compra:.2f}, tem {saldo_disponivel:.2f})")
            else:
                # Mostrar por que está aguardando
                if 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()
    
    salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
    return estado

def fase_balanceamento_portfolio(estado, moedas):
    """
    Fase 2: Balanceia o portfólio mantendo os percentuais definidos
    Vende moedas acima do percentual e compra moedas abaixo
    """
    log_info("\n========== FASE DE BALANCEAMENTO DO PORTFÓLIO ==========")
    
    # ✓ 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)
    
    # ✓ 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)
        usdt_residual = max(0, saldo_usdt_atual - 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 = []
    
    for moeda in moedas:
        pct_atual = percentuais_atuais.get(moeda, 0)
        pct_alvo = percentuais_alvo.get(moeda, 0)
        diferenca = pct_atual - pct_alvo
        
        log_info(f"{moeda}: {pct_atual:.2f}% (Alvo: {pct_alvo:.2f}%) - Diferença: {diferenca:+.2f}%")
        
        if diferenca > VARIACAO_BALANCEAMENTO:
            moedas_venda.append({
                'moeda': moeda,
                'diferenca': diferenca,
                'percentual_atual': pct_atual,
                'preco': precos.get(moeda, 0)
            })
            log_aviso(f"  → {moeda} acima do alvo (venda)")
        elif diferenca < -VARIACAO_BALANCEAMENTO:
            moedas_compra.append({
                'moeda': moeda,
                'diferenca': abs(diferenca),
                'percentual_atual': pct_atual,
                'preco': precos.get(moeda, 0)
            })
            log_aviso(f"  → {moeda} abaixo do alvo (compra)")
    
    # Executar vendas (ordenar por diferença decrescente)
    saldo_usdt_antes = saldos.get(MOEDA_BASE, 0)
    
    for item_venda in sorted(moedas_venda, key=lambda x: x['diferenca'], reverse=True):
        moeda = item_venda['moeda']
        preco = item_venda['preco']
        diferenca = item_venda['diferenca']
        quantidade_atual = saldos.get(moeda, 0)
        
        if quantidade_atual > 0 and preco > 0:
            # Calcular quantidade a vender para retornar ao percentual alvo
            # ✓ 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 USDT, executar imediatamente com o mínimo (5 USDT) para não perder oscilações
            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 * (1 - COMISSAO)
                valor_ajustado = True
                log_aviso(f"    ⚡ Valor ajustado de ${valor_excesso_original:.2f} para ${valor_excesso:.2f} (mínimo da Binance)")
            
            # Validar valor mínimo de operação na Binance
            eh_valido, msg_validacao = validar_valor_operacao(valor_excesso, moeda, 'venda')
            
            if eh_valido and quantidade_venda > 0 and quantidade_venda <= quantidade_atual:
                log_aviso(f"\n>>> Vendendo {moeda} <<<")
                log_info(f"    Preço Atual: {preco:.8f}")
                log_info(f"    Quantidade: {quantidade_venda:.8f}")
                log_info(f"    Valor: {valor_excesso:.2f} {MOEDA_BASE}")
                
                # Obter saldo ANTES da venda
                saldo_antes = obter_saldo(moeda)
                
                symbol = f"{moeda}/{MOEDA_BASE}"
                ordem = executar_ordem(symbol, 'sell', quantidade_venda)
                
                if ordem:
                    # Aguardar um pouco para a Binance processar
                    time.sleep(1)
                    
                    # Obter saldo DEPOIS da venda
                    saldo_depois = obter_saldo(moeda)
                    
                    # Se saldo não mudou, aguardar mais e tentar novamente
                    if saldo_depois == saldo_antes:
                        log_aviso(f"  Saldo não mudou na primeira verificação, aguardando mais 2s...")
                        time.sleep(2)
                        saldo_depois = obter_saldo(moeda)
                    
                    # Verificar se o saldo realmente diminuiu
                    diferenca_saldo = saldo_antes - saldo_depois
                    
                    if diferenca_saldo > 0:
                        log_sucesso(f"✓ Venda verificada: Saldo diminuiu {diferenca_saldo:.8f} {moeda}")
                        saldo_usdt_antes += valor_excesso
                        estado['historico_trades'].append({
                            'timestamp': datetime.now(timezone).isoformat(),
                            'moeda': moeda,
                            'tipo': 'venda_balanceamento',
                            'preco': preco,
                            'quantidade': quantidade_venda,
                            'valor': valor_excesso
                        })
                    else:
                        log_erro(f"✗ FALHA: Venda de {moeda} não confirmada! Saldo não diminuiu.")
                        log_erro(f"  Antes: {saldo_antes:.8f} {moeda} | Depois: {saldo_depois:.8f} {moeda}")
                    
                    time.sleep(1)
            elif not eh_valido:
                log_aviso(msg_validacao)
    
    # Atualizar saldos
    saldos = obter_saldos_portfolio(moedas)
    saldo_usdt_atual = saldos.get(MOEDA_BASE, 0)
    
    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)
    
    # Se não há saldo suficiente, reduzir proporcionalmente todos os valores
    fator_reducao = 1.0
    if total_usdt_necessario > saldo_usdt_atual:
        fator_reducao = max(0, (saldo_usdt_atual - saldo_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:.2f}")
        log_aviso(f"   • Proteção USDT: ${saldo_usdt_minimo_protegido:.2f}")
        log_aviso(f"   • USDT Livre (pode gastar): ${max(0, saldo_usdt_atual - 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:.2f}")
        log_info(f"   • Proteção USDT: ${saldo_usdt_minimo_protegido:.2f}")
        log_info(f"   • USDT Livre (pode gastar): ${max(0, saldo_usdt_atual - 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']
        
        # 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, tempo_maximo_minutos=3)
                    if 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)
                
                ordem = executar_ordem(symbol, 'buy', quantidade_compra)
                
                if ordem:
                    # Aguardar um pouco para a Binance processar
                    time.sleep(1)
                    
                    # Obter saldo DEPOIS da compra
                    saldo_depois = obter_saldo(moeda)
                    
                    # Se saldo não mudou, aguardar mais e tentar novamente
                    if saldo_depois == saldo_antes:
                        log_aviso(f"  Saldo não mudou na primeira verificação, aguardando mais 2s...")
                        time.sleep(2)
                        saldo_depois = obter_saldo(moeda)
                    
                    # Verificar se o saldo realmente aumentou
                    diferenca_saldo = saldo_depois - saldo_antes
                    
                    if diferenca_saldo > 0:
                        log_sucesso(f"✓ Compra verificada: Saldo aumentou {diferenca_saldo:.8f} {moeda}")
                        saldo_usdt_atual -= valor_compra
                        estado['historico_trades'].append({
                            'timestamp': datetime.now(timezone).isoformat(),
                            'moeda': moeda,
                            'tipo': 'compra_balanceamento',
                            'preco': preco,
                            'quantidade': quantidade_compra,
                            'valor': valor_compra
                        })
                    else:
                        log_erro(f"✗ FALHA: Compra de {moeda} não confirmada! Saldo não aumentou.")
                        log_erro(f"  Antes: {saldo_antes:.8f} {moeda} | Depois: {saldo_depois:.8f} {moeda}")
                    
                    time.sleep(1)
            elif not eh_valido:
                log_aviso(msg_validacao)
            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)
        log_info(f"{moeda}: {pct_final:.2f}% (Saldo: {saldo_moeda:.8f})")
    
    # ✓ 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%)")
    
    salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
    return estado

def relatorio_portfolio():
    """Exibe relatório detalhado do portfólio"""
    try:
        estado = carregar_estado_portfolio(PORTFOLIO_STATE_FILE)
        if not estado or not isinstance(estado, dict):
            log_aviso("Portfólio não inicializado ou corrompido")
            return None
        
        moedas = [m['moeda'] for m in PORTFOLIO_CONFIG if m.get('ativa', True)]
        saldos = obter_saldos_portfolio(moedas)
        precos = {m: obter_preco_atual(f"{m}/{MOEDA_BASE}") for m in moedas}
        valor_portfolio = obter_valor_portfolio(moedas, saldos, precos)
        percentuais = calcular_percentuais_atuais(moedas, saldos, precos, valor_portfolio)
        
        # ✓ NOVO: Capturar variação de 24h para cada moeda
        variacoes_24h = {m: obter_variacao_24h(f"{m}/{MOEDA_BASE}") for m in moedas}
        
        # ✓ NOVO: Atualizar melhores preços dos últimos 3 dias
        estado = atualizar_melhores_precos_portfolio(estado, moedas)
        
        # ✓ NOVO: Salvar preços atuais no estado (para exibição em tempo real)
        estado['precos_atuais'] = precos
        estado['timestamp_precos'] = datetime.now(timezone).isoformat()
        
        # ✓ NOVO: Salvar variações de 24h no estado
        estado['variacoes_24h'] = variacoes_24h
        
        # Salvar estado com preços e variações atualizados
        salvar_estado_portfolio(PORTFOLIO_STATE_FILE, estado)
        
        log_info("\n" + "=" * 70)
        log_info("RELATÓRIO DO PORTFÓLIO")
        log_info("=" * 70)
        log_info(f"Fase: {estado.get('fase', 'desconhecida').upper()}")
        log_info(f"Data de Início: {estado.get('data_inicio', 'N/A')}")
        log_info(f"Capital Inicial: {estado.get('capital_inicial', 0)} {MOEDA_BASE}")
        log_info(f"Valor Atual: {valor_portfolio:.2f} {MOEDA_BASE}")
        
        capital = estado.get('capital_inicial', 0)
        if capital > 0:
            log_info(f"Ganho/Perda: {valor_portfolio - capital:+.2f} {MOEDA_BASE}")
            log_info(f"Ganho %: {((valor_portfolio / capital) - 1) * 100:+.2f}%")
        
        log_info("-" * 70)
        
        for moeda in moedas:
            saldo = saldos.get(moeda, 0)
            preco = precos.get(moeda, 0)
            valor = saldo * preco if preco else 0
            pct = percentuais.get(moeda, 0)
            pct_alvo = estado.get('percentuais_alvo', {}).get(moeda, 0)
            
            log_info(f"{moeda}: Saldo={saldo:15.8f} | " +
                f"Valor: {valor:12.2f} | {pct:6.2f}% (Alvo: {pct_alvo:.2f}%)")
        
        log_info("=" * 70 + "\n")
        
        # ✓ Retornar estado com best_prices_3days atualizado
        return estado
    except Exception as e:
        log_erro(f"Erro ao exibir relatório: {e}")
        traceback.print_exc()
        return None

# ===================== MAIN LOOP =====================

def main():
    """Função principal"""
    log_info("\n" + "=" * 70)
    log_info("ROBÔ DE BALANCEAMENTO DE PORTFÓLIO - INICIADO")
    log_info("=" * 70)
    
    estado, moedas = inicializar_portfolio()
    contador_ciclo = 0
    contador_relatorio = 0
    
    try:
        while True:
            # 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)
                
                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()
