Subversion Repositories Integrator Subversion

Rev

Rev 771 | Rev 774 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed

package br.com.kronus.core;

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)
 * trabalhando tanto em modo backtest quanto em modo tempo real.
 *
 * IMPORTANTE:
 *  - Este detector FINALIZA o padrão assim que identifica o G3.
 *  - O G4 será identificado apenas na camada de sinais de trade, fora desta classe.
 *
 * REGRAS IMPLEMENTADAS (com GR DINÂMICO invertido)
 * ================================================
 *
 * COMPRADOR
 * ---------
 * Ponto de partida: um candle COMPRADOR é candidato à referência.
 *
 * G1:
 *   - Enquanto não houver G1:
 *       * O candidato à referência pode ser atualizado dinamicamente:
 *         QUALQUER candle posterior, antes do G1, que fizer uma MÁXIMA
 *         MAIOR que o candidato atual passa a ser o novo candidato.
 *       * Se algum candle VENDEDOR subsequente ROMPER o FUNDO
 *         do candle candidato à referência (mínima < mínima do candidato),
 *         esse candle será o gatilho tipo 1 [G1] e o candidato vigente
 *         torna-se o candle referência [GR].
 *
 * G2:
 *   - Após o G1 (com GR definido):
 *       * Assim que tiver nova tendência COMPRADORA:
 *           - O último candle dessa sequência compradora deve ter o FECHAMENTO
 *             dentro da região total (mínima..máxima) do GR.
 *           - Se mudar para vendedor sem nenhum candle fechar dentro da região
 *             do GR → descarta o padrão.
 *       * Se qualquer candle (após o GR) romper o TOPO do GR
 *         (máxima > máxima GR) → descarta o padrão.
 *       * Se válido, esse último comprador é o gatilho tipo 2 [G2].
 *
 * G3:
 *   - Após o G2:
 *       * O próximo direcional deve ser VENDEDOR; se for comprador,
 *         o padrão vai apenas até G2 (parcial).
 *       * Numa sequência vendedora:
 *           - Se algum candle VENDEDOR posterior ROMPER o FUNDO do G2
 *             (mínima < mínima G2) e tiver TOPO <= topo do GR
 *             (máxima <= máxima GR), sem mudança de tendência
 *             (sem compradores no meio), será o gatilho tipo 3 [G3].
 *
 * Regra global comprador:
 *   - Se algum candle posterior ao GR ROMPER o TOPO do GR
 *     (máxima > máxima GR), desconsiderar o padrão.
 *
 * -----------------------------------------------------------
 *
 * VENDEDOR
 * --------
 * Ponto de partida: um candle VENDEDOR é candidato à referência.
 *
 * G1:
 *   - Enquanto não houver G1:
 *       * O candidato à referência pode ser atualizado dinamicamente:
 *         QUALQUER candle posterior, antes do G1, que fizer uma MÍNIMA
 *         MENOR que a do candidato atual passa a ser o novo candidato.
 *       * Se algum candle COMPRADOR subsequente ROMPER o TOPO
 *         do candidato (máxima > máxima do candidato), esse candle
 *         será o gatilho tipo 1 [G1] e o candidato vigente torna-se o GR.
 *
 * Regra global vendedor:
 *   - Se algum candle posterior ao GR ROMPER o FUNDO do GR
 *     (mínima < mínima GR), desconsiderar o padrão.
 *
 * G2:
 *   - Após o G1 (com GR definido):
 *       * Assim que tiver nova tendência VENDEDORA:
 *           - O último candle dessa sequência vendedora deve ter FECHAMENTO
 *             dentro da região total (mínima..máxima) do GR.
 *           - Se mudar para comprador sem nenhum candle fechar dentro
 *             da região do GR → descarta o padrão.
 *       * Se romper o FUNDO do GR em qualquer momento após o GR
 *         → descarta o padrão.
 *       * Se válido, esse último vendedor é o gatilho tipo 2 [G2].
 *
 * G3:
 *   - Após o G2:
 *       * O próximo direcional deve ser COMPRADOR; se for vendedor,
 *         o padrão vai até G2 (parcial).
 *       * Numa sequência compradora:
 *           - Se algum COMPRADOR posterior ROMPER o TOPO de G2
 *             (máxima > máxima G2) e tiver FUNDO >= fundo do GR
 *             (mínima >= mínima GR), sem mudança de tendência
 *             (sem vendedores no meio), será o gatilho tipo 3 [G3].
 *
 * Regra global adicional (texto original repetia):
 *   - Mantida a regra principal: vendedor é invalidado
 *     quando rompem o FUNDO do GR.
 *
 * -----------------------------------------------------------
 *
 * OUTSIDE (ambos os lados):
 *   - Candle cuja máxima > máxima do candle anterior
 *     E mínima < mínima do candle anterior.
 *   - Se houver um OUTSIDE antes de identificar o G3:
 *       * Desconsidera o padrão atual (GR+G1+G2) e recomeça
 *         a busca a partir do GR (na prática, aborta o padrão).
 *
 * OBS:
 *   - Os gatilhos devem seguir ORDEM cronológica:
 *     G1 -> G2 -> G3, sempre em candles diferentes.
 */

public class DetectorGatilhos {

    private final boolean logAtivo;
    private int idxProximaAnaliseTempoReal = 0;

    public DetectorGatilhos() {
        this(false);
    }

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

    private void log(String msg) {
        if (logAtivo) {
            System.out.println(msg);
        }
    }

    // -----------------------------------------------------------------
    // Estrutura interna de retorno
    // -----------------------------------------------------------------
    private static class ResultadoPadrao {
        PadraoGatilho padrao;
        int lastIndex;

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

    private ResultadoPadrao criarResultadoParcialComG2(Candle ref,
                                                       Candle g1,
                                                       Candle g2,
                                                       int lastIndex) {
        PadraoGatilho padrao = new PadraoGatilho();
        padrao.setReferencia(ref);
        padrao.setGatilho1(g1);
        padrao.setGatilho2(g2);
        padrao.setGatilho3(null);
        padrao.setGatilho4(null); // G4 só na camada de trade
        return new ResultadoPadrao(padrao, lastIndex);
    }

    // =====================================================================
    // 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);
            }

            idxRef = Math.max(resultado.lastIndex + 1, idxRef + 1);
        }

        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 {
            return new ResultadoPadrao(null, idxRef);
        }
    }

    // =====================================================================
    // 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 isOutside(List<Candle> candles, int idx) {
        if (idx <= 0) return false;
        Candle atual = candles.get(idx);
        Candle anterior = candles.get(idx - 1);
        return BigDecimalUtils.ehMaiorQue(atual.getMaxima(), anterior.getMaxima())
                && BigDecimalUtils.ehMenorQue(atual.getMinima(), anterior.getMinima());
    }

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

    // =====================================================================
    // CASO COMPRADOR – GR DINÂMICO PELA MÁXIMA
    // =====================================================================

    private ResultadoPadrao detectarPadraoComprador(List<Candle> candles, int idxInicio) {
        int n = candles.size();
        int lastIndex = idxInicio;

        Candle candidatoRef = candles.get(idxInicio);
        if (!candidatoRef.isCandleComprador()) {
            return new ResultadoPadrao(null, idxInicio);
        }

        int idxCandidatoRef = idxInicio;

        // --------------------
        // 1) Buscar G1 (vendedor rompe fundo do candidato)
        // --------------------
        Candle g1 = null;
        int idxG1 = -1;
        boolean temGR = false;
        Candle gr = null;
        int idxGR = -1;

        for (int i = idxInicio + 1; i < n; i++) {
            Candle c = candles.get(i);
            if (isInside(candles, i) || !isDirecional(c))
                continue;

            // Outside antes de G3 (aqui ainda antes de G1)
            if (isOutside(candles, i)) {
                lastIndex = i;
                log(String.format("Comprador: OUTSIDE em [%d] antes de G1. Abortando padrão.", i));
                return new ResultadoPadrao(null, lastIndex);
            }

            // G1: vendedor rompe fundo do candidato
            if (c.isCandleVendedor()
                    && BigDecimalUtils.ehMenorQue(c.getMinima(), candidatoRef.getMinima())) {
                g1 = c;
                idxG1 = i;
                gr = candidatoRef;
                idxGR = idxCandidatoRef;
                temGR = true;
                lastIndex = i;
                break;
            }

            // GR DINÂMICO (compra): atualiza pelo TOPO mais alto antes de G1
            if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), candidatoRef.getMaxima())) {
                candidatoRef = c;
                idxCandidatoRef = i;
                lastIndex = i;
                continue;
            }

            lastIndex = i;
        }

        if (g1 == null || !temGR) {
            log(String.format("Comprador: não foi possível formar G1 a partir de idx %d.", idxInicio));
            return new ResultadoPadrao(null, lastIndex);
        }

        log(String.format("GR COMPRADOR em [%d], G1 (vendedor) em [%d].", idxGR, idxG1));

        // --------------------
        // 2) Buscar G2 (tendência compradora com fechamento dentro do GR)
        // --------------------
        Candle g2 = null;
        int idxG2 = -1;

        int idxPrimeiroComprador = -1;
        for (int i = idxG1 + 1; i < n; i++) {
            Candle c = candles.get(i);
            if (isInside(candles, i) || !isDirecional(c))
                continue;

            // Outside antes de G3
            if (isOutside(candles, i)) {
                lastIndex = idxGR;
                log(String.format("GR[%d]: OUTSIDE em [%d] antes da perna compradora de G2.", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            // Regra global: romper topo do GR invalida
            if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), gr.getMaxima())) {
                lastIndex = i;
                log(String.format("GR[%d]: candle[%d] rompeu topo do GR antes de G2.", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            if (c.isCandleComprador()) {
                idxPrimeiroComprador = i;
                break;
            } else {
                lastIndex = i;
            }
        }

        if (idxPrimeiroComprador == -1) {
            log(String.format("GR[%d], G1[%d]: não houve nova tendência compradora para G2.", idxGR, idxG1));
            return new ResultadoPadrao(null, lastIndex);
        }

        Candle ultimoComprador = null;
        int idxUltimoComprador = -1;

        for (int i = idxPrimeiroComprador; i < n; i++) {
            Candle c = candles.get(i);
            if (isInside(candles, i) || !isDirecional(c))
                continue;

            // Outside durante G2
            if (isOutside(candles, i)) {
                lastIndex = idxGR;
                log(String.format("GR[%d]: OUTSIDE em [%d] durante G2 (comprador).", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            // Regra global: romper topo do GR invalida
            if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), gr.getMaxima())) {
                lastIndex = i;
                log(String.format("GR[%d]: candle[%d] rompeu topo do GR durante G2.", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            if (c.isCandleComprador()) {
                ultimoComprador = c;
                idxUltimoComprador = i;
                lastIndex = i;
            } else if (c.isCandleVendedor()) {
                lastIndex = i - 1;
                break;
            }
        }

        if (ultimoComprador == null) {
            log(String.format("GR[%d], G1[%d]: não houve comprador válido para G2.", idxGR, idxG1));
            return new ResultadoPadrao(null, lastIndex);
        }

        boolean fechamentoDentroRegiaoGR =
                BigDecimalUtils.ehMaiorOuIgualQue(ultimoComprador.getFechamento(), gr.getMinima()) &&
                BigDecimalUtils.ehMenorOuIgualQue(ultimoComprador.getFechamento(), gr.getMaxima());

        if (!fechamentoDentroRegiaoGR) {
            log(String.format(
                    "GR[%d], G1[%d]: último comprador[%d] não fechou dentro da região do GR (fech=%s, faixa=[%s,%s]).",
                    idxGR, idxG1, idxUltimoComprador,
                    ultimoComprador.getFechamento().toPlainString(),
                    gr.getMinima().toPlainString(),
                    gr.getMaxima().toPlainString()
            ));
            return new ResultadoPadrao(null, lastIndex);
        }

        g2 = ultimoComprador;
        idxG2 = idxUltimoComprador;
        log(String.format("GR[%d], G1[%d] => G2 (comprador) em [%d].", idxGR, idxG1, idxG2));

        // --------------------
        // 3) Buscar G3 (vendedor rompe fundo de G2 com topo <= topo GR)
        // --------------------
        Candle g3 = null;
        int idxG3 = -1;

        int idxPrimeiroVendedorAposG2 = -1;
        for (int i = idxG2 + 1; i < n; i++) {
            Candle c = candles.get(i);
            if (isInside(candles, i) || !isDirecional(c))
                continue;

            if (isOutside(candles, i)) {
                lastIndex = idxGR;
                log(String.format("GR[%d]: OUTSIDE em [%d] antes da sequência vendedora de G3.", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            // Regra global: romper topo do GR invalida
            if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), gr.getMaxima())) {
                lastIndex = i;
                log(String.format("GR[%d]: candle[%d] rompeu topo do GR antes da perna vendedora de G3.", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            if (c.isCandleVendedor()) {
                idxPrimeiroVendedorAposG2 = i;
                break;
            } else {
                // primeiro direcional após G2 é comprador → padrão só até G2
                lastIndex = i;
                log(String.format("GR[%d], G1[%d], G2[%d]: primeiro direcional após G2 é comprador (idx %d).",
                        idxGR, idxG1, idxG2, i));
                return criarResultadoParcialComG2(gr, g1, g2, lastIndex);
            }
        }

        if (idxPrimeiroVendedorAposG2 == -1) {
            log(String.format("GR[%d], G1[%d], G2[%d]: não houve vendedor após G2.", idxGR, idxG1, idxG2));
            return criarResultadoParcialComG2(gr, g1, g2, lastIndex);
        }

        for (int i = idxPrimeiroVendedorAposG2; i < n; i++) {
            Candle c = candles.get(i);
            if (isInside(candles, i) || !isDirecional(c))
                continue;

            if (isOutside(candles, i)) {
                lastIndex = idxGR;
                log(String.format("GR[%d]: OUTSIDE em [%d] durante formação de G3 (vendedor).", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            // Regra global: romper topo do GR invalida
            if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), gr.getMaxima())) {
                lastIndex = i;
                log(String.format("GR[%d]: candle[%d] rompeu topo do GR durante busca de G3.", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            if (!c.isCandleVendedor()) {
                // apareceu comprador antes de G3 → padrão só até G2
                lastIndex = i - 1;
                log(String.format("GR[%d], G1[%d], G2[%d]: comprador em [%d] antes de G3.", idxGR, idxG1, idxG2, i));
                return criarResultadoParcialComG2(gr, g1, g2, lastIndex);
            }

            lastIndex = i;
            boolean rompeFundoG2 = BigDecimalUtils.ehMenorQue(c.getMinima(), g2.getMinima());
            boolean topoMenorOuIgualGR = BigDecimalUtils.ehMenorOuIgualQue(c.getMaxima(), gr.getMaxima());
            if (rompeFundoG2 && topoMenorOuIgualGR) {
                g3 = c;
                idxG3 = i;
                break;
            }
        }

        if (g3 == null) {
            log(String.format("GR[%d], G1[%d], G2[%d]: não houve G3, padrão parcial.", idxGR, idxG1, idxG2));
            return criarResultadoParcialComG2(gr, g1, g2, lastIndex);
        }

        lastIndex = idxG3;
        log(String.format("GR[%d], G1[%d], G2[%d] => G3 (vendedor) em [%d].", idxGR, idxG1, idxG2, idxG3));

        PadraoGatilho padrao = new PadraoGatilho();
        padrao.setReferencia(gr);
        padrao.setGatilho1(g1);
        padrao.setGatilho2(g2);
        padrao.setGatilho3(g3);
        padrao.setGatilho4(null);

        return new ResultadoPadrao(padrao, lastIndex);
    }

    // =====================================================================
    // CASO VENDEDOR – GR DINÂMICO PELA MÍNIMA
    // =====================================================================

    private ResultadoPadrao detectarPadraoVendedor(List<Candle> candles, int idxInicio) {
        int n = candles.size();
        int lastIndex = idxInicio;

        Candle candidatoRef = candles.get(idxInicio);
        if (!candidatoRef.isCandleVendedor()) {
            return new ResultadoPadrao(null, idxInicio);
        }

        int idxCandidatoRef = idxInicio;

        // --------------------
        // 1) Buscar G1 (comprador rompe topo do candidato)
        // --------------------
        Candle g1 = null;
        int idxG1 = -1;
        boolean temGR = false;
        Candle gr = null;
        int idxGR = -1;

        for (int i = idxInicio + 1; i < n; i++) {
            Candle c = candles.get(i);
            if (isInside(candles, i) || !isDirecional(c))
                continue;

            // Outside antes de G1
            if (isOutside(candles, i)) {
                lastIndex = i;
                log(String.format("Vendedor: OUTSIDE em [%d] antes de G1. Abortando padrão.", i));
                return new ResultadoPadrao(null, lastIndex);
            }

            // G1: comprador rompe topo do candidato
            if (c.isCandleComprador()
                    && BigDecimalUtils.ehMaiorQue(c.getMaxima(), candidatoRef.getMaxima())) {
                g1 = c;
                idxG1 = i;
                gr = candidatoRef;
                idxGR = idxCandidatoRef;
                temGR = true;
                lastIndex = i;
                break;
            }

            // GR DINÂMICO (venda): atualiza pelo FUNDO mais baixo antes de G1
            if (BigDecimalUtils.ehMenorQue(c.getMinima(), candidatoRef.getMinima())) {
                candidatoRef = c;
                idxCandidatoRef = i;
                lastIndex = i;
                continue;
            }

            lastIndex = i;
        }

        if (g1 == null || !temGR) {
            log(String.format("Vendedor: não foi possível formar G1 a partir de idx %d.", idxInicio));
            return new ResultadoPadrao(null, lastIndex);
        }

        log(String.format("GR VENDEDOR em [%d], G1 (comprador) em [%d].", idxGR, idxG1));

        // --------------------
        // 2) Buscar G2 (tendência vendedora com fechamento dentro do GR)
        // --------------------
        Candle g2 = null;
        int idxG2 = -1;

        int idxPrimeiroVendedor = -1;
        for (int i = idxG1 + 1; i < n; i++) {
            Candle c = candles.get(i);
            if (isInside(candles, i) || !isDirecional(c))
                continue;

            // Outside antes de G3
            if (isOutside(candles, i)) {
                lastIndex = idxGR;
                log(String.format("GR[%d]: OUTSIDE em [%d] antes da perna vendedora de G2.", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            // Regra global vendedor: romper fundo do GR invalida
            if (BigDecimalUtils.ehMenorQue(c.getMinima(), gr.getMinima())) {
                lastIndex = i;
                log(String.format("GR[%d]: candle[%d] rompeu fundo do GR antes de G2.", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            if (c.isCandleVendedor()) {
                idxPrimeiroVendedor = i;
                break;
            } else {
                lastIndex = i;
            }
        }

        if (idxPrimeiroVendedor == -1) {
            log(String.format("GR[%d], G1[%d]: não houve nova tendência vendedora para G2.", idxGR, idxG1));
            return new ResultadoPadrao(null, lastIndex);
        }

        Candle ultimoVendedor = null;
        int idxUltimoVendedor = -1;

        for (int i = idxPrimeiroVendedor; i < n; i++) {
            Candle c = candles.get(i);
            if (isInside(candles, i) || !isDirecional(c))
                continue;

            // Outside durante G2
            if (isOutside(candles, i)) {
                lastIndex = idxGR;
                log(String.format("GR[%d]: OUTSIDE em [%d] durante G2 (vendedor).", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            // Regra global vendedor: romper fundo do GR invalida
            if (BigDecimalUtils.ehMenorQue(c.getMinima(), gr.getMinima())) {
                lastIndex = i;
                log(String.format("GR[%d]: candle[%d] rompeu fundo do GR durante G2.", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            if (c.isCandleVendedor()) {
                ultimoVendedor = c;
                idxUltimoVendedor = i;
                lastIndex = i;
            } else if (c.isCandleComprador()) {
                lastIndex = i - 1;
                break;
            }
        }

        if (ultimoVendedor == null) {
            log(String.format("GR[%d], G1[%d]: não houve vendedor válido para G2.", idxGR, idxG1));
            return new ResultadoPadrao(null, lastIndex);
        }

        boolean fechamentoDentroRegiaoGR =
                BigDecimalUtils.ehMaiorOuIgualQue(ultimoVendedor.getFechamento(), gr.getMinima()) &&
                BigDecimalUtils.ehMenorOuIgualQue(ultimoVendedor.getFechamento(), gr.getMaxima());

        if (!fechamentoDentroRegiaoGR) {
            log(String.format(
                    "GR[%d], G1[%d]: último vendedor[%d] não fechou dentro da região do GR (fech=%s, faixa=[%s,%s]).",
                    idxGR, idxG1, idxUltimoVendedor,
                    ultimoVendedor.getFechamento().toPlainString(),
                    gr.getMinima().toPlainString(),
                    gr.getMaxima().toPlainString()
            ));
            return new ResultadoPadrao(null, lastIndex);
        }

        g2 = ultimoVendedor;
        idxG2 = idxUltimoVendedor;
        log(String.format("GR[%d], G1[%d] => G2 (vendedor) em [%d].", idxGR, idxG1, idxG2));

        // --------------------
        // 3) Buscar G3 (comprador rompe topo de G2 com fundo >= fundo GR)
        // --------------------
        Candle g3 = null;
        int idxG3 = -1;

        int idxPrimeiroCompradorAposG2 = -1;
        for (int i = idxG2 + 1; i < n; i++) {
            Candle c = candles.get(i);
            if (isInside(candles, i) || !isDirecional(c))
                continue;

            if (isOutside(candles, i)) {
                lastIndex = idxGR;
                log(String.format("GR[%d]: OUTSIDE em [%d] antes da sequência compradora de G3.", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            // Regra global vendedor: romper fundo do GR antes de G3 invalida
            if (BigDecimalUtils.ehMenorQue(c.getMinima(), gr.getMinima())) {
                lastIndex = i;
                log(String.format("GR[%d]: candle[%d] rompeu fundo do GR antes de G3.", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            if (c.isCandleComprador()) {
                idxPrimeiroCompradorAposG2 = i;
                break;
            } else {
                // primeiro direcional não é comprador → padrão até G2
                lastIndex = i;
                log(String.format("GR[%d], G1[%d], G2[%d]: primeiro direcional após G2 não é comprador (idx %d).",
                        idxGR, idxG1, idxG2, i));
                return criarResultadoParcialComG2(gr, g1, g2, lastIndex);
            }
        }

        if (idxPrimeiroCompradorAposG2 == -1) {
            log(String.format("GR[%d], G1[%d], G2[%d]: não houve comprador após G2.", idxGR, idxG1, idxG2));
            return criarResultadoParcialComG2(gr, g1, g2, lastIndex);
        }

        for (int i = idxPrimeiroCompradorAposG2; i < n; i++) {
            Candle c = candles.get(i);
            if (isInside(candles, i) || !isDirecional(c))
                continue;

            if (isOutside(candles, i)) {
                lastIndex = idxGR;
                log(String.format("GR[%d]: OUTSIDE em [%d] durante formação de G3 (comprador).", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            // Regra global vendedor: romper fundo do GR invalida
            if (BigDecimalUtils.ehMenorQue(c.getMinima(), gr.getMinima())) {
                lastIndex = i;
                log(String.format("GR[%d]: candle[%d] rompeu fundo do GR durante busca de G3.", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            if (!c.isCandleComprador()) {
                // apareceu vendedor antes de G3 → padrão só até G2
                lastIndex = i - 1;
                log(String.format("GR[%d], G1[%d], G2[%d]: vendedor em [%d] antes de G3.", idxGR, idxG1, idxG2, i));
                return criarResultadoParcialComG2(gr, g1, g2, lastIndex);
            }

            lastIndex = i;
            boolean rompeTopoG2 = BigDecimalUtils.ehMaiorQue(c.getMaxima(), g2.getMaxima());
            boolean fundoMaiorOuIgualGR = BigDecimalUtils.ehMaiorOuIgualQue(c.getMinima(), gr.getMinima());
            if (rompeTopoG2 && fundoMaiorOuIgualGR) {
                g3 = c;
                idxG3 = i;
                break;
            }
        }

        if (g3 == null) {
            log(String.format("GR[%d], G1[%d], G2[%d]: não houve G3, padrão parcial.", idxGR, idxG1, idxG2));
            return criarResultadoParcialComG2(gr, g1, g2, lastIndex);
        }

        lastIndex = idxG3;
        log(String.format("GR[%d], G1[%d], G2[%d] => G3 (comprador) em [%d].", idxGR, idxG1, idxG2, idxG3));

        PadraoGatilho padrao = new PadraoGatilho();
        padrao.setReferencia(gr);
        padrao.setGatilho1(g1);
        padrao.setGatilho2(g2);
        padrao.setGatilho3(g3);
        padrao.setGatilho4(null);

        return new ResultadoPadrao(padrao, lastIndex);
    }

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

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

    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.lastIndex + 1;
            if (proximoInicio <= idxProximaAnaliseTempoReal) {
                proximoInicio = idxProximaAnaliseTempoReal + 1;
            }
            idxProximaAnaliseTempoReal = proximoInicio;

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

        return null;
    }

}