Subversion Repositories Integrator Subversion

Rev

Rev 773 | Rev 775 | 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 (versão alinhada)
 * ======================================
 *
 * 1) PADRÃO COMPRADOR
 * --------------------
 * Ponto de partida:
 *   - Um candle COMPRADOR é tomado como CANDIDATO À REFERÊNCIA.
 *
 * GR DINÂMICO (compra):
 *   - Enquanto NÃO houver G1:
 *       * Qualquer candle DIRECIONAL posterior (comprador ou vendedor)
 *         que fizer uma MÁXIMA MAIOR que a do candidato atual
 *         passa a ser o NOVO candidato à referência.
 *
 * G1 (compra):
 *   - Pode ser QUALQUER candle vendedor subsequente (não precisa ser o primeiro),
 *     desde que ROMPA o FUNDO do candidato:
 *        mínima(candle vendedor) < mínima(candidatoRef).
 *   - No momento em que isso ocorrer:
 *        * Esse candle vendedor é o G1.
 *        * O candidato vigente passa a ser o GR (gatilho referência).
 *
 * G2 (compra):
 *   - Após o G1, com GR definido:
 *       * Procurar uma nova tendência COMPRADORA:
 *           - Achar o primeiro candle COMPRADOR após o G1.
 *           - Seguir a sequência compradora (ignorando INSIDE e neutros)
 *             até aparecer candle vendedor.
 *       * O ÚLTIMO candle dessa tendência compradora será candidato a G2.
 *       * Para ser G2:
 *           - O FECHAMENTO deve ficar DENTRO da região [mínGR, máxGR].
 *       * Se mudar para vendedor sem nenhum candle fechar dentro da região do GR:
 *           - descarta o padrão.
 *   - Regra global comprador:
 *       * Se qualquer candle, após o GR, ROMPER o TOPO do GR
 *         (máxima > máximaGR) em qualquer ponto antes da conclusão:
 *           - descarta o padrão.
 *
 * G3 (compra) – NOVA REGRA:
 *   - Após o G2:
 *       * Se ALGUM candle vendedor subsequente:
 *           - romper o FUNDO do G2 (mínima < mínimaG2) E
 *           - tiver TOPO menor ou igual ao TOPO do GR (máxima ≤ máximaGR)
 *         então esse candle será o G3.
 *   - Não precisa ser o primeiro vendedor após o G2; pode haver mudança de tendência,
 *     candles inside, etc., desde que a regra acima seja satisfeita e não ocorra:
 *       * outside antes do G3, ou
 *       * rompimento do topo do GR (regra global).
 *   - Ao identificar G3, o padrão é finalizado.
 *
 * 2) PADRÃO VENDEDOR
 * -------------------
 * Ponto de partida:
 *   - Um candle VENDEDOR é tomado como CANDIDATO À REFERÊNCIA.
 *
 * GR DINÂMICO (venda):
 *   - Enquanto NÃO houver G1:
 *       * Qualquer candle DIRECIONAL posterior (comprador ou vendedor)
 *         que fizer uma MÍNIMA MENOR que a do candidato atual
 *         passa a ser o NOVO candidato à referência.
 *
 * G1 (venda):
 *   - Pode ser QUALQUER candle comprador subsequente (não precisa ser o primeiro),
 *     desde que ROMPA o TOPO do candidato:
 *        máxima(candle comprador) > máxima(candidatoRef).
 *   - No momento em que isso ocorrer:
 *        * Esse candle comprador é o G1.
 *        * O candidato vigente passa a ser o GR (gatilho referência).
 *
 * Regra global vendedor:
 *   - Se qualquer candle posterior ao GR ROMPER o FUNDO do GR
 *     (mínima < mínimaGR) em qualquer momento antes da conclusão:
 *       - descarta o padrão.
 *
 * G2 (venda):
 *   - Após o G1, com GR definido:
 *       * Procurar uma nova tendência VENDEDORA:
 *           - Achar o primeiro VENDEDOR após o G1.
 *           - Seguir a sequência vendedora (ignorando INSIDE e neutros)
 *             até aparecer candle comprador.
 *       * O ÚLTIMO candle dessa tendência vendedora será candidato a G2.
 *       * Para ser G2:
 *           - O FECHAMENTO deve ficar DENTRO da região [mínGR, máxGR].
 *       * Se mudar para comprador sem nenhum candle fechar dentro da região do GR:
 *           - descarta o padrão.
 *       * Se durante essa fase algum candle romper o FUNDO do GR
 *         (mínima < mínimaGR) → descarta o padrão (regra global).
 *
 * G3 (venda) – NOVA REGRA:
 *   - Após o G2:
 *       * Se ALGUM candle comprador subsequente:
 *           - romper o TOPO do G2 (máxima > máximaG2) E
 *           - tiver FUNDO maior ou igual ao FUNDO do GR (mínima ≥ mínimaGR)
 *         então esse candle será o G3.
 *   - Não precisa ser o primeiro comprador após o G2; pode haver mudança de tendência,
 *     candles inside, etc., desde que a regra acima seja satisfeita e não ocorra:
 *       * outside antes do G3, ou
 *       * rompimento do fundo do GR (regra global).
 *   - Ao identificar G3, o padrão é finalizado.
 *
 * 3) OUTSIDE (ambos os lados)
 * ---------------------------
 *   - Candle cuja:
 *       máxima > máxima(candle anterior)
 *       E mínima < mínima(candle anterior).
 *   - Se houver OUTSIDE em qualquer estágio antes de G3:
 *       - o padrão atual (GR/G1/G2) é descartado.
 *
 * 4) ORDEM CRONOLÓGICA
 * ---------------------
 *   - Sempre GR -> G1 -> G2 -> G3.
 *   - Todos em candles diferentes, na ordem do tempo.
 */

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 gr,
                                                       Candle g1,
                                                       Candle g2,
                                                       int lastIndex) {
        PadraoGatilho padrao = new PadraoGatilho();
        padrao.setReferencia(gr);
        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: QUALQUER vendedor subsequente que rompa o fundo do candidatoRef
        //    (com GR dinâmico pela MÁXIMA antes de G1)
        // --------------------
        Candle g1 = null;
        int idxG1 = -1;
        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 => descarta padrão
            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);
            }

            // GR dinâmico (compra): atualiza candidatoRef pelo TOPO mais alto
            // enquanto não existir G1
            if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), candidatoRef.getMaxima())) {
                candidatoRef = c;
                idxCandidatoRef = i;
                lastIndex = i;
                continue;
            }

            // G1: QUALQUER vendedor subsequente que rompa o fundo do candidatoRef
            if (c.isCandleVendedor()
                    && BigDecimalUtils.ehMenorQue(c.getMinima(), candidatoRef.getMinima())) {
                g1 = c;
                idxG1 = i;
                gr = candidatoRef;
                idxGR = idxCandidatoRef;
                lastIndex = i;
                break;
            }

            lastIndex = i;
        }

        if (g1 == null || gr == null) {
            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 G2 => descarta padrão
            if (isOutside(candles, i)) {
                lastIndex = idxGR;
                log(String.format("GR[%d]: OUTSIDE em [%d] antes da tendência compradora de G2.", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            // Regra global comprador: se romper topo do GR => descarta
            if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), gr.getMaxima())) {
                lastIndex = i;
                log(String.format("GR[%d]: candle[%d] rompeu TOPO do GR antes/na formação 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 => descarta padrão
            if (isOutside(candles, i)) {
                lastIndex = idxGR;
                log(String.format("GR[%d]: OUTSIDE em [%d] durante formação de G2 (comprador).", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            // Regra global comprador: se romper topo do GR => descarta
            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.",
                    idxGR, idxG1, idxUltimoComprador
            ));
            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 – NOVA REGRA:
        //    QUALQUER vendedor subsequente que rompa fundo de G2
        //    e tenha topo <= topo do GR.
        // --------------------
        Candle g3 = null;
        int idxG3 = -1;

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

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

            // Regra global comprador: romper topo do GR => descarta
            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()) {
                boolean rompeFundoG2 = BigDecimalUtils.ehMenorQue(c.getMinima(), g2.getMinima());
                boolean topoMenorOuIgualGR = BigDecimalUtils.ehMenorOuIgualQue(c.getMaxima(), gr.getMaxima());
                if (rompeFundoG2 && topoMenorOuIgualGR) {
                    g3 = c;
                    idxG3 = i;
                    lastIndex = i;
                    break;
                }
            }
        }

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

        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: QUALQUER comprador subsequente que rompa o topo do candidatoRef
        //    (com GR dinâmico pela MÍNIMA antes de G1)
        // --------------------
        Candle g1 = null;
        int idxG1 = -1;
        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 => descarta padrão
            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);
            }

            // GR dinâmico (venda): atualiza candidatoRef pelo FUNDO mais baixo
            // enquanto não existir G1
            if (BigDecimalUtils.ehMenorQue(c.getMinima(), candidatoRef.getMinima())) {
                candidatoRef = c;
                idxCandidatoRef = i;
                lastIndex = i;
                continue;
            }

            // G1: QUALQUER comprador subsequente que rompa o topo do candidatoRef
            if (c.isCandleComprador()
                    && BigDecimalUtils.ehMaiorQue(c.getMaxima(), candidatoRef.getMaxima())) {
                g1 = c;
                idxG1 = i;
                gr = candidatoRef;
                idxGR = idxCandidatoRef;
                lastIndex = i;
                break;
            }

            lastIndex = i;
        }

        if (g1 == null || gr == null) {
            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 G2 => descarta padrão
            if (isOutside(candles, i)) {
                lastIndex = idxGR;
                log(String.format("GR[%d]: OUTSIDE em [%d] antes da tendência vendedora de G2.", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            // Regra global vendedor: romper fundo do GR => descarta
            if (BigDecimalUtils.ehMenorQue(c.getMinima(), gr.getMinima())) {
                lastIndex = i;
                log(String.format("GR[%d]: candle[%d] rompeu FUNDO do GR antes/na formação 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 => descarta padrão
            if (isOutside(candles, i)) {
                lastIndex = idxGR;
                log(String.format("GR[%d]: OUTSIDE em [%d] durante formação de G2 (vendedor).", idxGR, i));
                return new ResultadoPadrao(null, lastIndex);
            }

            // Regra global vendedor: romper fundo do GR => descarta
            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.",
                    idxGR, idxG1, idxUltimoVendedor
            ));
            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 – NOVA REGRA:
        //    QUALQUER comprador subsequente que rompa topo de G2
        //    e tenha fundo >= fundo do GR.
        // --------------------
        Candle g3 = null;
        int idxG3 = -1;

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

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

            // Regra global vendedor: romper fundo do GR => descarta
            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()) {
                boolean rompeTopoG2 = BigDecimalUtils.ehMaiorQue(c.getMaxima(), g2.getMaxima());
                boolean fundoMaiorOuIgualGR = BigDecimalUtils.ehMaiorOuIgualQue(c.getMinima(), gr.getMinima());
                if (rompeTopoG2 && fundoMaiorOuIgualGR) {
                    g3 = c;
                    idxG3 = i;
                    lastIndex = i;
                    break;
                }
            }
        }

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

        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;
    }

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

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

        return null;
    }

}