Subversion Repositories Integrator Subversion

Rev

Blame | Last modification | View Log | Download | RSS feed

package br.com.kronus.core;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

import br.com.sl.domain.model.Candle;
import br.com.sl.domain.util.BigDecimalUtils;

/**
 * Detector de padrões de gatilhos (GR, G1, G2, G3)
 * para o modelo Kronus.
 *
 * REGRAS GERAIS:
 * - Não usar candles INSIDE (range totalmente dentro do candle anterior).
 * - Gatilhos sempre em ordem cronológica: GR -> G1 -> G2 -> G3.
 * - Um candle não pode ser usado para dois gatilhos.
 * - Não pode haver G2 antes de G1.
 *
 * -------------------------------------------------------------
 * PADRÃO COMPRADOR (OPERAÇÃO VENDEDORA)
 * -------------------------------------------------------------
 *
 * 1) Referência (GR) e G1
 *
 * A) Tenho um candle A COMPRADOR.
 *    Se o PRÓXIMO candle direcional (não-inside) tiver mudança direcional
 *    (for VENDEDOR), então A passa a ser CANDIDATO À REFERÊNCIA.
 *
 * B) Referência dinâmica (antes do G1):
 *    Enquanto G1 ainda não apareceu, entre o índice do candidato e o G1:
 *    - Se algum candle posterior fizer MÁXIMA MAIOR que a máxima do candidato,
 *      esse candle passa a ser o NOVO candidato à referência.
 *
 * C) G1:
 *    - Se algum candle VENDEDOR subsequente romper o FUNDO do candidato à referência
 *      (minima < minimaCandRef), esse candle será o G1.
 *    - O candidato atual torna-se o GR definitivo.
 *
 * D) Regra de saída pelo G1:
 *    - Considerar a Fibo do G1 com origem na MÁXIMA do G1 e destino na MÍNIMA do G1.
 *    - Calcular o nível 200% dessa Fibo.
 *    - Se ALGUM candle após o G1 ATINGIR ou PASSAR ESSE NÍVEL (mínima <= nível200)
 *      ANTES de identificar G3, descartar a operação:
 *        -> descartar padrão e reiniciar análise a partir do candle APÓS o GR.
 *
 * 2) G2 (após G1)
 *
 *    - Após o G1, QUALQUER candle COMPRADOR que tiver FECHAMENTO DENTRO da região
 *      do GR (minGR <= fechamento <= maxGR) passa a ser CANDIDATO a G2.
 *
 *    - Regra de descarte:
 *        Se ALGUM candle posterior ao G1 ROMPER o TOPO do GR (máxima > maxGR),
 *        descartar a operação:
 *          -> descartar padrão e reiniciar análise a partir do candle APÓS o GR.
 *
 *    - G2 dinâmico (entre o primeiro G2 candidato e o G3):
 *        Enquanto G3 não tiver aparecido:
 *        - Se surgir outro candle COMPRADOR com FECHAMENTO dentro da região do GR
 *          e MÁXIMA MAIOR que a máxima do candidato atual a G2,
 *          esse novo candle passa a ser o NOVO G2.
 *
 * 3) G3 (após ter G2)
 *
 *    - Após existir um candidato a G2:
 *      Se algum candle VENDEDOR posterior:
 *        * romper o FUNDO do G2 (minima < minimaG2) e
 *        * tiver TOPO MENOR OU IGUAL ao TOPO do GR (max <= maxGR)
 *      => esse candle será o G3, confirmando o padrão.
 *
 *    - Ao encontrar G3, o padrão COMPRADOR é considerado COMPLETO (GR,G1,G2,G3).
 *
 * -------------------------------------------------------------
 * PADRÃO VENDEDOR (OPERAÇÃO COMPRADORA)
 * -------------------------------------------------------------
 *
 * 1) Referência (GR) e G1
 *
 * A) Tenho um candle A VENDEDOR.
 *    Se o PRÓXIMO candle direcional (não-inside) tiver mudança direcional
 *    (for COMPRADOR), então A passa a ser CANDIDATO À REFERÊNCIA.
 *
 * B) Referência dinâmica (antes do G1):
 *    Enquanto G1 ainda não apareceu, entre o índice do candidato e o G1:
 *    - Se algum candle posterior fizer MÍNIMA MENOR que a mínima do candidato,
 *      esse candle passa a ser o NOVO candidato à referência.
 *
 * C) G1:
 *    - Se algum candle COMPRADOR subsequente romper o TOPO do candidato à referência
 *      (máxima > máximaCandRef), esse candle será o G1.
 *    - O candidato atual torna-se o GR definitivo.
 *
 * D) Regra de saída pelo G1:
 *    - Considerar a Fibo do G1 com origem na MÍNIMA do G1 e destino na MÁXIMA do G1.
 *    - Calcular o nível -100% dessa Fibo.
 *    - Se ALGUM candle após o G1 ATINGIR ou PASSAR ESSE NÍVEL para baixo
 *      (mínima <= nível -100%) ANTES de identificar G3,
 *      descartar a operação:
 *        -> descartar padrão e reiniciar análise a partir do candle APÓS o GR.
 *
 * 2) G2 (após G1)
 *
 *    - Após o G1, QUALQUER candle VENDEDOR que tiver FECHAMENTO DENTRO da região
 *      do GR (minGR <= fechamento <= maxGR) passa a ser CANDIDATO a G2.
 *
 *    - Regra de descarte:
 *        Se ALGUM candle posterior ao G1 ROMPER o FUNDO do GR (mínima < minGR),
 *        descartar a operação:
 *          -> descartar padrão e reiniciar análise a partir do candle APÓS o GR.
 *
 *    - G2 dinâmico (entre o primeiro G2 candidato e o G3):
 *        Enquanto G3 não tiver aparecido:
 *        - Se surgir outro candle VENDEDOR com FECHAMENTO dentro da região do GR
 *          e MÍNIMA MENOR que a mínima do candidato a G2,
 *          esse novo candle passa a ser o NOVO G2.
 *
 * 3) G3 (após ter G2)
 *
 *    - Após existir um candidato a G2:
 *      Se algum candle COMPRADOR posterior:
 *        * romper o TOPO do G2 (máxima > máximaG2) e
 *        * tiver FUNDO MENOR OU IGUAL ao FUNDO do GR (mínima <= minGR)
 *      => esse candle será o G3, confirmando o padrão.
 *
 *    - Ao encontrar G3, o padrão VENDEDOR é considerado COMPLETO (GR,G1,G2,G3).
 *
 * -------------------------------------------------------------
 * BACKTEST:
 *  - identificarPadroes: varre a lista inteira e retorna todos os padrões
 *    completos ou parciais (até G2).
 *
 * TEMPO REAL:
 *  - processarCandleTempoReal: chamado a cada novo candle, retorna um padrão
 *    assim que ele for identificado (até G3).
 *
 * DEBUG:
 *  - debugarAPartirDoIndice: roda a detecção a partir de um índice e
 *    retorna um relatório (List<String>) com tudo o que aconteceu.
 */

public class DetectorGatilhosAnterior {

    private final boolean logAtivo;
    private int idxProximaAnaliseTempoReal = 0;

    /**
     * Buffer opcional para capturar logs em memória (modo debug).
     * Se for null, não acumula; se não for null, log() adiciona aqui também.
     */

    private List<String> bufferDebug;

    public DetectorGatilhosAnterior() {
        this(true);
    }

    public DetectorGatilhosAnterior(boolean logAtivo) {
        this.logAtivo = logAtivo;
    }

    private void log(String msg) {
        if (logAtivo) {
            System.out.println(msg);
        }
        if (bufferDebug != null) {
            bufferDebug.add(msg);
        }
    }

    /**
     * Estrutura interna para carregar o resultado da detecção
     * iniciando em um índice específico.
     *
     * lastIndex      = último índice efetivamente analisado na busca daquele padrão.
     * proximoInicio  = índice sugerido para o PRÓXIMO início de análise:
     *                  - Se NÃO existiu GR: idxA + 1
     *                  - Se existiu GR: idxGR + 1 (recomeça após a referência)
     */

    private static class ResultadoPadrao {
        PadraoGatilho padrao;
        int lastIndex;
        int proximoInicio;

        ResultadoPadrao(PadraoGatilho padrao, int lastIndex, int proximoInicio) {
            this.padrao = padrao;
            this.lastIndex = lastIndex;
            this.proximoInicio = proximoInicio;
        }
    }

    /**
     * Cria um resultado com padrão PARCIAL (até G2).
     * Sempre que há GR, o próximo início deve ser após o GR.
     */

    private ResultadoPadrao criarResultadoParcialComG2(Candle ref,
                                                       Candle g1,
                                                       Candle g2,
                                                       int lastIndex,
                                                       int idxGR) {
        PadraoGatilho padrao = new PadraoGatilho();
        padrao.setReferencia(ref);
        padrao.setGatilho1(g1);
        padrao.setGatilho2(g2);
        padrao.setGatilho3(null);
        padrao.setGatilho4(null); // G4 será tratado na camada de estratégia, não aqui.
        return new ResultadoPadrao(padrao, lastIndex, idxGR + 1);
    }

    // =====================================================================
    // HELPERS
    // =====================================================================

    private boolean isInside(List<Candle> candles, int idx) {
        if (idx <= 0) return false;
        Candle atual = candles.get(idx);
        Candle anterior = candles.get(idx - 1);
        return BigDecimalUtils.ehMenorOuIgualQue(atual.getMaxima(), anterior.getMaxima())
                && BigDecimalUtils.ehMaiorOuIgualQue(atual.getMinima(), anterior.getMinima());
    }

    private boolean isDirecional(Candle c) {
        return c.isCandleComprador() || c.isCandleVendedor();
    }

    private boolean fechamentoDentroRegiaoGR(Candle c, Candle gr) {
        return BigDecimalUtils.ehMaiorOuIgualQue(c.getFechamento(), gr.getMinima())
                && BigDecimalUtils.ehMenorOuIgualQue(c.getFechamento(), gr.getMaxima());
    }

    /**
     * Extensão de Fibonacci simples:
     * origem + (destino - origem) * fator
     */

    private BigDecimal fibExtend(BigDecimal origem, BigDecimal destino, BigDecimal fator) {
        BigDecimal diff = destino.subtract(origem);
        return origem.add(diff.multiply(fator));
    }

    // =====================================================================
    // API PRINCIPAL – BACKTEST
    // =====================================================================

    public List<PadraoGatilho> identificarPadroes(List<Candle> candles) {
        List<PadraoGatilho> padroes = new ArrayList<>();
        int n = candles.size();
        if (n < 4) return padroes;

        int idxRef = 0;
        while (idxRef < n - 3) {
            ResultadoPadrao resultado = detectarPadraoAPartir(candles, idxRef);

            if (resultado == null) {
                idxRef++;
                continue;
            }

            if (resultado.padrao != null) {
                padroes.add(resultado.padrao);
            }

            // Regra de avanço:
            // - Se houve GR, proximoInicio = idxGR + 1
            // - Se não houve GR, proximoInicio = idxRef + 1
            int proximoInicio = resultado.proximoInicio;
            if (proximoInicio <= idxRef) {
                proximoInicio = idxRef + 1;
            }

            idxRef = proximoInicio;
        }

        return padroes;
    }

    private ResultadoPadrao detectarPadraoAPartir(List<Candle> candles, int idxRef) {
        Candle ref = candles.get(idxRef);

        if (ref.isCandleComprador()) {
            return detectarPadraoComprador(candles, idxRef);
        } else if (ref.isCandleVendedor()) {
            return detectarPadraoVendedor(candles, idxRef);
        } else {
            // Candle neutro não é A; não existe GR, avança 1 candle.
            return new ResultadoPadrao(null, idxRef, idxRef + 1);
        }
    }

    // =====================================================================
    // PADRÃO COMPRADOR (OPERAÇÃO VENDEDORA)
    // =====================================================================

    private ResultadoPadrao detectarPadraoComprador(List<Candle> candles, int idxA) {
        int n = candles.size();
        Candle candleA = candles.get(idxA);
        if (!candleA.isCandleComprador()) {
            // Não pode ser A comprador -> reinicia no próximo candle.
            return new ResultadoPadrao(null, idxA, idxA + 1);
        }

        int lastIndex = idxA;
        log(String.format("[VENDER] Iniciando busca a partir de A[%d]", idxA + 1));

        // 1) Verifica se o próximo candle direcional (não-inside) muda a direção
        int idxProxDirecional = -1;
        for (int i = idxA + 1; i < n; i++) {
            Candle c = candles.get(i);
            if (!isDirecional(c) || isInside(candles, i)) {
                log(String.format("[VENDER] Candle[%d] ignorado (não direcional ou inside)", i + 1));
                continue;
            }
            idxProxDirecional = i;
            break;
        }

        if (idxProxDirecional == -1) {
            // Não houve candle B; A não vira candidato; recomeça do próximo candle.
            return new ResultadoPadrao(null, idxA, idxA + 1);
        }

        Candle prox = candles.get(idxProxDirecional);
        if (!prox.isCandleVendedor()) {
            // Não houve mudança direcional imediata -> A não vira candidato
            return new ResultadoPadrao(null, idxA, idxA + 1);
        }

        // A vira candidato à referência
        Candle candidatoRef = candleA;
        int idxCandidatoRef = idxA;
        log(String.format("[VENDER] CandidatoRef inicial = idx %d", idxCandidatoRef + 1));

        // 2) Busca G1, com referência dinâmica até G1
        Candle g1 = null;
        int idxG1 = -1;

        for (int i = idxA + 1; i < n; i++) {
            Candle c = candles.get(i);
            if (!isDirecional(c) || isInside(candles, i)) {
                log(String.format("[VENDER] Candle[%d] ignorado (não direcional ou inside) antes do G1", i + 1));
                continue;
            }

            lastIndex = i;

            // Primeiro checa G1: vendedor que rompe fundo do candidatoRef
            if (c.isCandleVendedor()
                    && BigDecimalUtils.ehMenorQue(c.getMinima(), candidatoRef.getMinima())) {
                g1 = c;
                idxG1 = i;
                log(String.format("[VENDER] G1 encontrado em [%d], rompendo fundo de candRef[%d]", idxG1 + 1, idxCandidatoRef + 1));
                break;
            }

            // Se ainda não encontrou G1, atualiza candidatoRef se máxima maior
            if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), candidatoRef.getMaxima())) {
                candidatoRef = c;
                idxCandidatoRef = i;
                log(String.format("[VENDER] CandidatoRef atualizado dinamicamente para idx %d", idxCandidatoRef + 1));
            }
        }

        if (g1 == null) {
            // Não formou G1; não existe GR definitivo; recomeça a partir do próximo candle após A.
            return new ResultadoPadrao(null, lastIndex, idxA + 1);
        }

        Candle gr = candidatoRef;
        int idxGR = idxCandidatoRef;
        log(String.format("[VENDER] GR definido em [%d], G1 em [%d]", idxGR + 1, idxG1 + 1));

        // 3) Regra de saída do G1 – Fibonacci 200% (origem = máxG1, destino = mínG1)
        BigDecimal fib200 = fibExtend(g1.getMaxima(), g1.getMinima(), new BigDecimal("2"));
        log(String.format("[VENDER] Fibo200 G1[%d] = %s", idxG1, fib200.toPlainString()));

        // 4) Busca G2 dinâmico e G3
        Candle g2 = null;
        int idxG2 = -1;
        Candle g3 = null;
        int idxG3 = -1;

        for (int i = idxG1 + 1; i < n; i++) {
            Candle c = candles.get(i);
            if (!isDirecional(c) || isInside(candles, i)) {
                log(String.format("[VENDER] Candle[%d] ignorado (não direcional ou inside) após G1", i + 1));
                continue;
            }

            lastIndex = i;

            // --- Regras de DESCARTE após G1 (devem reiniciar do candle após o GR) ---

            // 4.1) Se candle romper TOPO do GR => descarta operação
            if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), gr.getMaxima())) {
                log(String.format("[VENDER] GR[%d]: candle[%d] rompeu topo do GR. Descartando padrão.", idxGR + 1, i + 1));
                return new ResultadoPadrao(null, i, idxGR + 1);
            }

            // 4.2) Regra Fib 200% do G1: se mínima <= fib200 => descarta
            if (BigDecimalUtils.ehMenorOuIgualQue(c.getMinima(), fib200)) {
                log(String.format("[VENDER] GR[%d], G1[%d]: candle[%d] atingiu 200%% da fibo G1. Descartando padrão.",
                        idxGR + 1, idxG1 + 1, i + 1));
                return new ResultadoPadrao(null, i, idxGR + 1);
            }

            // --- Construção de G2 dinâmico (COMPRADOR com FECHAMENTO dentro da região do GR) ---
            if (c.isCandleComprador() && fechamentoDentroRegiaoGR(c, gr)) {
                if (g2 == null) {
                    g2 = c;
                    idxG2 = i;
                    log(String.format("[VENDER] GR[%d], G1[%d]: candidato G2 em [%d]", idxGR + 1, idxG1 + 1, idxG2 + 1));
                } else {
                    // Atualização dinâmica: máxima maior e fechamento ainda dentro da região
                    if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), g2.getMaxima())) {
                        g2 = c;
                        idxG2 = i;
                        log(String.format("[VENDER] GR[%d], G1[%d]: G2 atualizado em [%d]", idxGR + 1, idxG1 + 1, idxG2 + 1));
                    }
                }
                continue;
            }

            // --- G3: só pode ser avaliado após existir candidato G2 ---
            if (g2 != null && c.isCandleVendedor()) {
                boolean rompeFundoG2 = BigDecimalUtils.ehMenorQue(c.getMinima(), g2.getMinima());
                boolean topoMenorOuIgualGR = BigDecimalUtils.ehMenorOuIgualQue(c.getMaxima(), gr.getMaxima());

                if (rompeFundoG2 && topoMenorOuIgualGR) {
                    g3 = c;
                    idxG3 = i;
                    log(String.format("[VENDER] GR[%d], G1[%d], G2[%d]: G3 em [%d] (padrão confirmado)",
                            idxGR + 1, idxG1 + 1, idxG2 + 1, idxG3 + 1));
                    break;
                }
            }
        }

        if (g3 == null) {
            // Padrão só até G2 (se G2 existir), senão nada.
            if (g2 != null) {
                log(String.format("[VENDER] Padrão parcial (GR[%d], G1[%d], G2[%d]) sem G3.", idxGR + 1, idxG1 + 1, idxG2 + 1));
                return criarResultadoParcialComG2(gr, g1, g2, lastIndex, idxGR);
            }
            // Já houve GR e G1, mas não houve G2/G3 -> recomeça após GR.
            log(String.format("[VENDER] GR[%d], G1[%d] sem G2/G3. Recomeçando após GR.", idxGR + 1, idxG1 + 1));
            return new ResultadoPadrao(null, lastIndex, idxGR + 1);
        }

        // Finaliza padrão no G3
        PadraoGatilho padrao = new PadraoGatilho();
        padrao.setReferencia(gr);
        padrao.setGatilho1(g1);
        padrao.setGatilho2(g2);
        padrao.setGatilho3(g3);
        padrao.setGatilho4(null);

        // Padrão completo -> próxima busca deve começar após o GR
        return new ResultadoPadrao(padrao, idxG3, idxGR + 1);
    }

    // =====================================================================
    // PADRÃO VENDEDOR (OPERAÇÃO COMPRADORA)
    // =====================================================================

    private ResultadoPadrao detectarPadraoVendedor(List<Candle> candles, int idxA) {
        int n = candles.size();
        Candle candleA = candles.get(idxA);
        if (!candleA.isCandleVendedor()) {
            // Não pode ser A vendedor -> reinicia no próximo candle.
            return new ResultadoPadrao(null, idxA, idxA + 1);
        }

        int lastIndex = idxA;
        log(String.format("[COMPRAR] Iniciando busca a partir de A[%d]", idxA + 1));

        // 1) Verifica se o próximo candle direcional (não-inside) muda a direção
        int idxProxDirecional = -1;
        for (int i = idxA + 1; i < n; i++) {
            Candle c = candles.get(i);
            if (!isDirecional(c) || isInside(candles, i)) {
                log(String.format("[COMPRAR] Candle[%d] ignorado (não direcional ou inside)", i + 1));
                continue;
            }
            idxProxDirecional = i;
            break;
        }

        if (idxProxDirecional == -1) {
            // Não houve candle B; A não vira candidato; recomeça do próximo candle.
            return new ResultadoPadrao(null, idxA, idxA + 1);
        }

        Candle prox = candles.get(idxProxDirecional);
        if (!prox.isCandleComprador()) {
            // Não houve mudança direcional imediata -> A não vira candidato
            return new ResultadoPadrao(null, idxA, idxA + 1);
        }

        // A vira candidato à referência
        Candle candidatoRef = candleA;
        int idxCandidatoRef = idxA;
        log(String.format("[COMPRAR] CandidatoRef inicial = idx %d", idxCandidatoRef + 1));

        // 2) Busca G1, com referência dinâmica até G1
        Candle g1 = null;
        int idxG1 = -1;

        for (int i = idxA + 1; i < n; i++) {
            Candle c = candles.get(i);
            if (!isDirecional(c) || isInside(candles, i)) {
                log(String.format("[COMPRAR] Candle[%d] ignorado (não direcional ou inside) antes do G1", i + 1));
                continue;
            }

            lastIndex = i;

            // Primeiro checa G1: comprador que rompe topo do candidatoRef
            if (c.isCandleComprador()
                    && BigDecimalUtils.ehMaiorQue(c.getMaxima(), candidatoRef.getMaxima())) {
                g1 = c;
                idxG1 = i;
                log(String.format("[COMPRAR] G1 encontrado em [%d], rompendo topo de candRef[%d]", idxG1 + 1, idxCandidatoRef + 1));
                break;
            }

            // Se ainda não encontrou G1, atualiza candidatoRef se mínima menor
            if (BigDecimalUtils.ehMenorQue(c.getMinima(), candidatoRef.getMinima())) {
                candidatoRef = c;
                idxCandidatoRef = i;
                log(String.format("[COMPRAR] CandidatoRef atualizado dinamicamente para idx %d", idxCandidatoRef + 1));
            }
        }

        if (g1 == null) {
            // Não formou G1; não existe GR definitivo; recomeça a partir do próximo candle após A.
            return new ResultadoPadrao(null, lastIndex, idxA + 1);
        }

        Candle gr = candidatoRef;
        int idxGR = idxCandidatoRef;
        log(String.format("[COMPRAR] GR definido em [%d], G1 em [%d]", idxGR + 1, idxG1 + 1));

        // 3) Regra de saída do G1 – Fibonacci 200% (origem = mínG1, destino = máxG1)
        BigDecimal fib200MinMax = fibExtend(g1.getMinima(), g1.getMaxima(), new BigDecimal("2"));
        log(String.format("[COMPRAR] Fibo200 G1[%d] = %s", idxG1 + 1, fib200MinMax.toPlainString()));

        // 4) Busca G2 dinâmico e G3
        Candle g2 = null;
        int idxG2 = -1;
        Candle g3 = null;
        int idxG3 = -1;

        for (int i = idxG1 + 1; i < n; i++) {
            Candle c = candles.get(i);
            if (!isDirecional(c) || isInside(candles, i)) {
                log(String.format("[COMPRAR] Candle[%d] ignorado (não direcional ou inside) após G1", i + 1));
                continue;
            }

            lastIndex = i;

            // --- Regras de DESCARTE após G1 (reiniciar após GR) ---

            // 4.1) Se candle romper FUNDO do GR => descarta operação
            if (BigDecimalUtils.ehMenorQue(c.getMinima(), gr.getMinima())) {
                log(String.format("[COMPRAR] GR[%d]: candle[%d] rompeu fundo do GR. Descartando padrão.", idxGR + 1, i + 1));
                return new ResultadoPadrao(null, i, idxGR + 1);
            }

            // 4.2) Regra Fib 200% do G1: se mínima > fib200MinMax => descarta
            if (BigDecimalUtils.ehMaiorQue(c.getMinima(), fib200MinMax)) {
                log(String.format("[COMPRAR] GR[%d], G1[%d]: candle[%d] atingiu 200%% da fibo G1. Descartando padrão.",
                        idxGR + 1, idxG1 + 1, i + 1));
                return new ResultadoPadrao(null, i, idxGR + 1);
            }

            // --- Construção de G2 dinâmico (VENDEDOR com FECHAMENTO dentro da região do GR) ---
            if (c.isCandleVendedor() && fechamentoDentroRegiaoGR(c, gr)) {
                if (g2 == null) {
                    g2 = c;
                    idxG2 = i;
                    log(String.format("[COMPRAR] GR[%d], G1[%d]: candidato G2 em [%d]", idxGR + 1, idxG1 + 1, idxG2 + 1));
                } else {
                    // Atualização dinâmica: mínima menor e fechamento ainda dentro da região
                    if (BigDecimalUtils.ehMenorQue(c.getMinima(), g2.getMinima())) {
                        g2 = c;
                        idxG2 = i;
                        log(String.format("[COMPRAR] GR[%d], G1[%d]: G2 atualizado em [%d]", idxGR + 1, idxG1 + 1, idxG2 + 1));
                    }
                }
                continue;
            }

            // --- G3: só pode ser avaliado após existir candidato G2 ---
            if (g2 != null && c.isCandleComprador()) {
                boolean rompeTopoG2 = BigDecimalUtils.ehMaiorQue(c.getMaxima(), g2.getMaxima());
                boolean fundoMaiorOuIgualGR = BigDecimalUtils.ehMaiorOuIgualQue(c.getMinima(), gr.getMinima());

                if (rompeTopoG2 && fundoMaiorOuIgualGR) {
                    g3 = c;
                    idxG3 = i;
                    log(String.format("[COMPRAR] GR[%d], G1[%d], G2[%d]: G3 em [%d] (padrão confirmado)",
                            idxGR + 1, idxG1 + 1, idxG2 + 1, idxG3 + 1));
                    break;
                }
            }
        }

        if (g3 == null) {
            // Padrão só até G2 (se G2 existir), senão nada
            if (g2 != null) {
                log(String.format("[COMPRAR] Padrão parcial (GR[%d], G1[%d], G2[%d]) sem G3.", idxGR + 1, idxG1 + 1, idxG2 + 1));
                return criarResultadoParcialComG2(gr, g1, g2, lastIndex, idxGR);
            }
            // Já houve GR e G1, mas não houve G2/G3 -> recomeça após GR.
            log(String.format("[COMPRAR] GR[%d], G1[%d] sem G2/G3. Recomeçando após GR.", idxGR + 1, idxG1 + 1));
            return new ResultadoPadrao(null, lastIndex, idxGR + 1);
        }

        // Finaliza padrão no G3
        PadraoGatilho padrao = new PadraoGatilho();
        padrao.setReferencia(gr);
        padrao.setGatilho1(g1);
        padrao.setGatilho2(g2);
        padrao.setGatilho3(g3);
        padrao.setGatilho4(null);

        // Padrão completo -> próxima busca deve começar após o GR
        return new ResultadoPadrao(padrao, idxG3, idxGR + 1);
    }

    // =====================================================================
    // MODO TEMPO REAL
    // =====================================================================

    public void resetTempoReal() {
        this.idxProximaAnaliseTempoReal = 0;
    }

    /**
     * Deve ser chamado SEMPRE que um novo candle for adicionado à lista.
     *
     * Exemplo:
     *   candles.add(novoCandle);
     *   PadraoGatilho padrao = detector.processarCandleTempoReal(candles);
     *
     *   if (padrao != null) {
     *       // padrão completo (até G3) encontrado
     *   }
     */

    public PadraoGatilho processarCandleTempoReal(List<Candle> candles) {
        int n = candles.size();
        if (n < 4) return null;

        while (idxProximaAnaliseTempoReal < n - 3) {
            ResultadoPadrao resultado = detectarPadraoAPartir(candles, idxProximaAnaliseTempoReal);

            if (resultado == null) {
                idxProximaAnaliseTempoReal++;
                continue;
            }

            int proximoInicio = resultado.proximoInicio;
            if (proximoInicio <= idxProximaAnaliseTempoReal) {
                proximoInicio = idxProximaAnaliseTempoReal + 1;
            }
            idxProximaAnaliseTempoReal = proximoInicio;

            if (resultado.padrao != null) {
                return resultado.padrao;
            }
        }

        return null;
    }

    // =====================================================================
    // DEBUG / RELATÓRIO
    // =====================================================================

    /**
     * Roda a lógica de detecção a partir de um índice específico e devolve
     * um "relatório" em forma de lista de strings com tudo que aconteceu.
     *
     * NÃO altera idxProximaAnaliseTempoReal.
     */

    public List<String> debugarAPartirDoIndice(List<Candle> candles, int idxInicio) {
        List<String> relatorio = new ArrayList<>();
        List<String> antigoBuffer = this.bufferDebug;
        this.bufferDebug = relatorio;
        try {
            log("====================================================");
            log(String.format("DEBUG: iniciando debugarAPartirDoIndice(%d)", idxInicio));
            detectarPadraoAPartir(candles, idxInicio);
            log(String.format("DEBUG: fim da análise a partir do índice %d", idxInicio));
            log("====================================================");
        } finally {
            this.bufferDebug = antigoBuffer;
        }
        return relatorio;
    }

}