Rev 774 | Rev 776 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed
| Rev | Author | Line No. | Line |
|---|---|---|---|
| 760 | blopes | 1 | package br.com.kronus.core; |
| 2 | |||
| 775 | blopes | 3 | import java.math.BigDecimal; |
| 760 | blopes | 4 | import java.util.ArrayList; |
| 764 | blopes | 5 | import java.util.List; |
| 760 | blopes | 6 | |
| 764 | blopes | 7 | import br.com.sl.domain.model.Candle; |
| 8 | import br.com.sl.domain.util.BigDecimalUtils; |
||
| 9 | |||
| 760 | blopes | 10 | /** |
| 773 | blopes | 11 | * Detector de padrões de gatilhos (GR, G1, G2, G3) |
| 775 | blopes | 12 | * para o modelo Kronus. |
| 760 | blopes | 13 | * |
| 775 | blopes | 14 | * REGRAS GERAIS: |
| 15 | * - Não usar candles INSIDE (range totalmente dentro do candle anterior). |
||
| 16 | * - Gatilhos sempre em ordem cronológica: GR -> G1 -> G2 -> G3. |
||
| 17 | * - Um candle não pode ser usado para dois gatilhos. |
||
| 18 | * - Não pode haver G2 antes de G1. |
||
| 760 | blopes | 19 | * |
| 775 | blopes | 20 | * ------------------------------------------------------------- |
| 21 | * PADRÃO COMPRADOR (OPERAÇÃO VENDEDORA) |
||
| 22 | * ------------------------------------------------------------- |
||
| 773 | blopes | 23 | * |
| 775 | blopes | 24 | * 1) Referência (GR) e G1 |
| 760 | blopes | 25 | * |
| 775 | blopes | 26 | * A) Tenho um candle A COMPRADOR. |
| 27 | * Se o PRÓXIMO candle direcional (não-inside) tiver mudança direcional |
||
| 28 | * (for VENDEDOR), então A passa a ser CANDIDATO À REFERÊNCIA. |
||
| 760 | blopes | 29 | * |
| 775 | blopes | 30 | * B) Referência dinâmica (antes do G1): |
| 31 | * Enquanto G1 ainda não apareceu, entre o índice do candidato e o G1: |
||
| 32 | * - Se algum candle posterior fizer MÁXIMA MAIOR que a máxima do candidato, |
||
| 33 | * esse candle passa a ser o NOVO candidato à referência. |
||
| 760 | blopes | 34 | * |
| 775 | blopes | 35 | * C) G1: |
| 36 | * - Se algum candle VENDEDOR subsequente romper o FUNDO do candidato à referência |
||
| 37 | * (minima < minimaCandRef), esse candle será o G1. |
||
| 38 | * - O candidato atual torna-se o GR definitivo. |
||
| 774 | blopes | 39 | * |
| 775 | blopes | 40 | * D) Regra de saída pelo G1: |
| 41 | * - Considerar a Fibo do G1 com origem na MÁXIMA do G1 e destino na MÍNIMA do G1. |
||
| 42 | * - Calcular o nível 200% dessa Fibo. |
||
| 43 | * - Se ALGUM candle após o G1 ATINGIR ou PASSAR ESSE NÍVEL (mínima <= nível200) |
||
| 44 | * ANTES de identificar G3, descartar a operação: |
||
| 45 | * -> descartar padrão e reiniciar análise a partir do candle APÓS o GR. |
||
| 760 | blopes | 46 | * |
| 775 | blopes | 47 | * 2) G2 (após G1) |
| 764 | blopes | 48 | * |
| 775 | blopes | 49 | * - Após o G1, QUALQUER candle COMPRADOR que tiver FECHAMENTO DENTRO da região |
| 50 | * do GR (minGR <= fechamento <= maxGR) passa a ser CANDIDATO a G2. |
||
| 764 | blopes | 51 | * |
| 775 | blopes | 52 | * - Regra de descarte: |
| 53 | * Se ALGUM candle posterior ao G1 ROMPER o TOPO do GR (máxima > maxGR), |
||
| 54 | * descartar a operação: |
||
| 55 | * -> descartar padrão e reiniciar análise a partir do candle APÓS o GR. |
||
| 764 | blopes | 56 | * |
| 775 | blopes | 57 | * - G2 dinâmico (entre o primeiro G2 candidato e o G3): |
| 58 | * Enquanto G3 não tiver aparecido: |
||
| 59 | * - Se surgir outro candle COMPRADOR com FECHAMENTO dentro da região do GR |
||
| 60 | * e MÁXIMA MAIOR que a máxima do candidato atual a G2, |
||
| 61 | * esse novo candle passa a ser o NOVO G2. |
||
| 764 | blopes | 62 | * |
| 775 | blopes | 63 | * 3) G3 (após ter G2) |
| 771 | blopes | 64 | * |
| 775 | blopes | 65 | * - Após existir um candidato a G2: |
| 66 | * Se algum candle VENDEDOR posterior: |
||
| 67 | * * romper o FUNDO do G2 (minima < minimaG2) e |
||
| 68 | * * tiver TOPO MENOR OU IGUAL ao TOPO do GR (max <= maxGR) |
||
| 69 | * => esse candle será o G3, confirmando o padrão. |
||
| 771 | blopes | 70 | * |
| 775 | blopes | 71 | * - Ao encontrar G3, o padrão COMPRADOR é considerado COMPLETO (GR,G1,G2,G3). |
| 771 | blopes | 72 | * |
| 775 | blopes | 73 | * ------------------------------------------------------------- |
| 74 | * PADRÃO VENDEDOR (OPERAÇÃO COMPRADORA) |
||
| 75 | * ------------------------------------------------------------- |
||
| 76 | * |
||
| 77 | * 1) Referência (GR) e G1 |
||
| 78 | * |
||
| 79 | * A) Tenho um candle A VENDEDOR. |
||
| 80 | * Se o PRÓXIMO candle direcional (não-inside) tiver mudança direcional |
||
| 81 | * (for COMPRADOR), então A passa a ser CANDIDATO À REFERÊNCIA. |
||
| 82 | * |
||
| 83 | * B) Referência dinâmica (antes do G1): |
||
| 84 | * Enquanto G1 ainda não apareceu, entre o índice do candidato e o G1: |
||
| 85 | * - Se algum candle posterior fizer MÍNIMA MENOR que a mínima do candidato, |
||
| 86 | * esse candle passa a ser o NOVO candidato à referência. |
||
| 87 | * |
||
| 88 | * C) G1: |
||
| 89 | * - Se algum candle COMPRADOR subsequente romper o TOPO do candidato à referência |
||
| 90 | * (máxima > máximaCandRef), esse candle será o G1. |
||
| 91 | * - O candidato atual torna-se o GR definitivo. |
||
| 92 | * |
||
| 93 | * D) Regra de saída pelo G1: |
||
| 94 | * - Considerar a Fibo do G1 com origem na MÍNIMA do G1 e destino na MÁXIMA do G1. |
||
| 95 | * - Calcular o nível -100% dessa Fibo. |
||
| 96 | * - Se ALGUM candle após o G1 ATINGIR ou PASSAR ESSE NÍVEL para baixo |
||
| 97 | * (mínima <= nível -100%) ANTES de identificar G3, |
||
| 98 | * descartar a operação: |
||
| 99 | * -> descartar padrão e reiniciar análise a partir do candle APÓS o GR. |
||
| 100 | * |
||
| 101 | * 2) G2 (após G1) |
||
| 102 | * |
||
| 103 | * - Após o G1, QUALQUER candle VENDEDOR que tiver FECHAMENTO DENTRO da região |
||
| 104 | * do GR (minGR <= fechamento <= maxGR) passa a ser CANDIDATO a G2. |
||
| 105 | * |
||
| 106 | * - Regra de descarte: |
||
| 107 | * Se ALGUM candle posterior ao G1 ROMPER o FUNDO do GR (mínima < minGR), |
||
| 108 | * descartar a operação: |
||
| 109 | * -> descartar padrão e reiniciar análise a partir do candle APÓS o GR. |
||
| 110 | * |
||
| 111 | * - G2 dinâmico (entre o primeiro G2 candidato e o G3): |
||
| 112 | * Enquanto G3 não tiver aparecido: |
||
| 113 | * - Se surgir outro candle VENDEDOR com FECHAMENTO dentro da região do GR |
||
| 114 | * e MÍNIMA MENOR que a mínima do candidato a G2, |
||
| 115 | * esse novo candle passa a ser o NOVO G2. |
||
| 116 | * |
||
| 117 | * 3) G3 (após ter G2) |
||
| 118 | * |
||
| 119 | * - Após existir um candidato a G2: |
||
| 120 | * Se algum candle COMPRADOR posterior: |
||
| 121 | * * romper o TOPO do G2 (máxima > máximaG2) e |
||
| 122 | * * tiver FUNDO MENOR OU IGUAL ao FUNDO do GR (mínima <= minGR) |
||
| 123 | * => esse candle será o G3, confirmando o padrão. |
||
| 124 | * |
||
| 125 | * - Ao encontrar G3, o padrão VENDEDOR é considerado COMPLETO (GR,G1,G2,G3). |
||
| 126 | * |
||
| 127 | * ------------------------------------------------------------- |
||
| 128 | * BACKTEST: |
||
| 129 | * - identificarPadroes: varre a lista inteira e retorna todos os padrões |
||
| 130 | * completos ou parciais (até G2). |
||
| 131 | * |
||
| 132 | * TEMPO REAL: |
||
| 133 | * - processarCandleTempoReal: chamado a cada novo candle, retorna um padrão |
||
| 134 | * assim que ele for identificado (até G3). |
||
| 135 | * |
||
| 136 | * DEBUG: |
||
| 137 | * - debugarAPartirDoIndice: roda a detecção a partir de um índice e |
||
| 138 | * retorna um relatório (List<String>) com tudo o que aconteceu. |
||
| 760 | blopes | 139 | */ |
| 140 | public class DetectorGatilhos { |
||
| 141 | |||
| 764 | blopes | 142 | private final boolean logAtivo; |
| 143 | private int idxProximaAnaliseTempoReal = 0; |
||
| 760 | blopes | 144 | |
| 775 | blopes | 145 | /** |
| 146 | * Buffer opcional para capturar logs em memória (modo debug). |
||
| 147 | * Se for null, não acumula; se não for null, log() adiciona aqui também. |
||
| 148 | */ |
||
| 149 | private List<String> bufferDebug; |
||
| 150 | |||
| 764 | blopes | 151 | public DetectorGatilhos() { |
| 775 | blopes | 152 | this(true); |
| 764 | blopes | 153 | } |
| 760 | blopes | 154 | |
| 764 | blopes | 155 | public DetectorGatilhos(boolean logAtivo) { |
| 156 | this.logAtivo = logAtivo; |
||
| 157 | } |
||
| 760 | blopes | 158 | |
| 764 | blopes | 159 | private void log(String msg) { |
| 160 | if (logAtivo) { |
||
| 161 | System.out.println(msg); |
||
| 162 | } |
||
| 775 | blopes | 163 | if (bufferDebug != null) { |
| 164 | bufferDebug.add(msg); |
||
| 165 | } |
||
| 764 | blopes | 166 | } |
| 760 | blopes | 167 | |
| 775 | blopes | 168 | /** |
| 169 | * Estrutura interna para carregar o resultado da detecção |
||
| 170 | * iniciando em um índice específico. |
||
| 171 | * |
||
| 172 | * lastIndex = último índice efetivamente analisado na busca daquele padrão. |
||
| 173 | * proximoInicio = índice sugerido para o PRÓXIMO início de análise: |
||
| 174 | * - Se NÃO existiu GR: idxA + 1 |
||
| 175 | * - Se existiu GR: idxGR + 1 (recomeça após a referência) |
||
| 176 | */ |
||
| 764 | blopes | 177 | private static class ResultadoPadrao { |
| 771 | blopes | 178 | PadraoGatilho padrao; |
| 179 | int lastIndex; |
||
| 775 | blopes | 180 | int proximoInicio; |
| 773 | blopes | 181 | |
| 775 | blopes | 182 | ResultadoPadrao(PadraoGatilho padrao, int lastIndex, int proximoInicio) { |
| 764 | blopes | 183 | this.padrao = padrao; |
| 184 | this.lastIndex = lastIndex; |
||
| 775 | blopes | 185 | this.proximoInicio = proximoInicio; |
| 764 | blopes | 186 | } |
| 187 | } |
||
| 760 | blopes | 188 | |
| 775 | blopes | 189 | /** |
| 190 | * Cria um resultado com padrão PARCIAL (até G2). |
||
| 191 | * Sempre que há GR, o próximo início deve ser após o GR. |
||
| 192 | */ |
||
| 193 | private ResultadoPadrao criarResultadoParcialComG2(Candle ref, |
||
| 764 | blopes | 194 | Candle g1, |
| 195 | Candle g2, |
||
| 775 | blopes | 196 | int lastIndex, |
| 197 | int idxGR) { |
||
| 764 | blopes | 198 | PadraoGatilho padrao = new PadraoGatilho(); |
| 775 | blopes | 199 | padrao.setReferencia(ref); |
| 764 | blopes | 200 | padrao.setGatilho1(g1); |
| 201 | padrao.setGatilho2(g2); |
||
| 202 | padrao.setGatilho3(null); |
||
| 775 | blopes | 203 | padrao.setGatilho4(null); // G4 será tratado na camada de estratégia, não aqui. |
| 204 | return new ResultadoPadrao(padrao, lastIndex, idxGR + 1); |
||
| 764 | blopes | 205 | } |
| 760 | blopes | 206 | |
| 764 | blopes | 207 | // ===================================================================== |
| 775 | blopes | 208 | // HELPERS |
| 209 | // ===================================================================== |
||
| 210 | |||
| 211 | private boolean isInside(List<Candle> candles, int idx) { |
||
| 212 | if (idx <= 0) return false; |
||
| 213 | Candle atual = candles.get(idx); |
||
| 214 | Candle anterior = candles.get(idx - 1); |
||
| 215 | return BigDecimalUtils.ehMenorOuIgualQue(atual.getMaxima(), anterior.getMaxima()) |
||
| 216 | && BigDecimalUtils.ehMaiorOuIgualQue(atual.getMinima(), anterior.getMinima()); |
||
| 217 | } |
||
| 218 | |||
| 219 | private boolean isDirecional(Candle c) { |
||
| 220 | return c.isCandleComprador() || c.isCandleVendedor(); |
||
| 221 | } |
||
| 222 | |||
| 223 | private boolean fechamentoDentroRegiaoGR(Candle c, Candle gr) { |
||
| 224 | return BigDecimalUtils.ehMaiorOuIgualQue(c.getFechamento(), gr.getMinima()) |
||
| 225 | && BigDecimalUtils.ehMenorOuIgualQue(c.getFechamento(), gr.getMaxima()); |
||
| 226 | } |
||
| 227 | |||
| 228 | /** |
||
| 229 | * Extensão de Fibonacci simples: |
||
| 230 | * origem + (destino - origem) * fator |
||
| 231 | */ |
||
| 232 | private BigDecimal fibExtend(BigDecimal origem, BigDecimal destino, BigDecimal fator) { |
||
| 233 | BigDecimal diff = destino.subtract(origem); |
||
| 234 | return origem.add(diff.multiply(fator)); |
||
| 235 | } |
||
| 236 | |||
| 237 | // ===================================================================== |
||
| 771 | blopes | 238 | // API PRINCIPAL – BACKTEST |
| 764 | blopes | 239 | // ===================================================================== |
| 760 | blopes | 240 | |
| 764 | blopes | 241 | public List<PadraoGatilho> identificarPadroes(List<Candle> candles) { |
| 242 | List<PadraoGatilho> padroes = new ArrayList<>(); |
||
| 243 | int n = candles.size(); |
||
| 771 | blopes | 244 | if (n < 4) return padroes; |
| 760 | blopes | 245 | |
| 764 | blopes | 246 | int idxRef = 0; |
| 247 | while (idxRef < n - 3) { |
||
| 248 | ResultadoPadrao resultado = detectarPadraoAPartir(candles, idxRef); |
||
| 760 | blopes | 249 | |
| 764 | blopes | 250 | if (resultado == null) { |
| 251 | idxRef++; |
||
| 252 | continue; |
||
| 253 | } |
||
| 760 | blopes | 254 | |
| 764 | blopes | 255 | if (resultado.padrao != null) { |
| 256 | padroes.add(resultado.padrao); |
||
| 257 | } |
||
| 760 | blopes | 258 | |
| 775 | blopes | 259 | // Regra de avanço: |
| 260 | // - Se houve GR, proximoInicio = idxGR + 1 |
||
| 261 | // - Se não houve GR, proximoInicio = idxRef + 1 |
||
| 262 | int proximoInicio = resultado.proximoInicio; |
||
| 263 | if (proximoInicio <= idxRef) { |
||
| 264 | proximoInicio = idxRef + 1; |
||
| 265 | } |
||
| 266 | |||
| 267 | idxRef = proximoInicio; |
||
| 764 | blopes | 268 | } |
| 760 | blopes | 269 | |
| 764 | blopes | 270 | return padroes; |
| 271 | } |
||
| 760 | blopes | 272 | |
| 764 | blopes | 273 | private ResultadoPadrao detectarPadraoAPartir(List<Candle> candles, int idxRef) { |
| 274 | Candle ref = candles.get(idxRef); |
||
| 775 | blopes | 275 | |
| 764 | blopes | 276 | if (ref.isCandleComprador()) { |
| 277 | return detectarPadraoComprador(candles, idxRef); |
||
| 278 | } else if (ref.isCandleVendedor()) { |
||
| 279 | return detectarPadraoVendedor(candles, idxRef); |
||
| 280 | } else { |
||
| 775 | blopes | 281 | // Candle neutro não é A; não existe GR, avança 1 candle. |
| 282 | return new ResultadoPadrao(null, idxRef, idxRef + 1); |
||
| 764 | blopes | 283 | } |
| 284 | } |
||
| 760 | blopes | 285 | |
| 764 | blopes | 286 | // ===================================================================== |
| 775 | blopes | 287 | // PADRÃO COMPRADOR (OPERAÇÃO VENDEDORA) |
| 764 | blopes | 288 | // ===================================================================== |
| 760 | blopes | 289 | |
| 775 | blopes | 290 | private ResultadoPadrao detectarPadraoComprador(List<Candle> candles, int idxA) { |
| 291 | int n = candles.size(); |
||
| 292 | Candle candleA = candles.get(idxA); |
||
| 293 | if (!candleA.isCandleComprador()) { |
||
| 294 | // Não pode ser A comprador -> reinicia no próximo candle. |
||
| 295 | return new ResultadoPadrao(null, idxA, idxA + 1); |
||
| 296 | } |
||
| 760 | blopes | 297 | |
| 775 | blopes | 298 | int lastIndex = idxA; |
| 299 | log(String.format("[VENDER] Iniciando busca a partir de A[%d]", idxA + 1)); |
||
| 760 | blopes | 300 | |
| 775 | blopes | 301 | // 1) Verifica se o próximo candle direcional (não-inside) muda a direção |
| 302 | int idxProxDirecional = -1; |
||
| 303 | for (int i = idxA + 1; i < n; i++) { |
||
| 304 | Candle c = candles.get(i); |
||
| 305 | if (!isDirecional(c) || isInside(candles, i)) { |
||
| 306 | log(String.format("[VENDER] Candle[%d] ignorado (não direcional ou inside)", i + 1)); |
||
| 307 | continue; |
||
| 308 | } |
||
| 309 | idxProxDirecional = i; |
||
| 310 | break; |
||
| 311 | } |
||
| 773 | blopes | 312 | |
| 775 | blopes | 313 | if (idxProxDirecional == -1) { |
| 314 | // Não houve candle B; A não vira candidato; recomeça do próximo candle. |
||
| 315 | return new ResultadoPadrao(null, idxA, idxA + 1); |
||
| 316 | } |
||
| 771 | blopes | 317 | |
| 775 | blopes | 318 | Candle prox = candles.get(idxProxDirecional); |
| 319 | if (!prox.isCandleVendedor()) { |
||
| 320 | // Não houve mudança direcional imediata -> A não vira candidato |
||
| 321 | return new ResultadoPadrao(null, idxA, idxA + 1); |
||
| 764 | blopes | 322 | } |
| 760 | blopes | 323 | |
| 775 | blopes | 324 | // A vira candidato à referência |
| 325 | Candle candidatoRef = candleA; |
||
| 326 | int idxCandidatoRef = idxA; |
||
| 327 | log(String.format("[VENDER] CandidatoRef inicial = idx %d", idxCandidatoRef + 1)); |
||
| 760 | blopes | 328 | |
| 775 | blopes | 329 | // 2) Busca G1, com referência dinâmica até G1 |
| 771 | blopes | 330 | Candle g1 = null; |
| 764 | blopes | 331 | int idxG1 = -1; |
| 773 | blopes | 332 | |
| 775 | blopes | 333 | for (int i = idxA + 1; i < n; i++) { |
| 764 | blopes | 334 | Candle c = candles.get(i); |
| 775 | blopes | 335 | if (!isDirecional(c) || isInside(candles, i)) { |
| 336 | log(String.format("[VENDER] Candle[%d] ignorado (não direcional ou inside) antes do G1", i + 1)); |
||
| 771 | blopes | 337 | continue; |
| 774 | blopes | 338 | } |
| 760 | blopes | 339 | |
| 775 | blopes | 340 | lastIndex = i; |
| 760 | blopes | 341 | |
| 775 | blopes | 342 | // Primeiro checa G1: vendedor que rompe fundo do candidatoRef |
| 773 | blopes | 343 | if (c.isCandleVendedor() |
| 344 | && BigDecimalUtils.ehMenorQue(c.getMinima(), candidatoRef.getMinima())) { |
||
| 771 | blopes | 345 | g1 = c; |
| 764 | blopes | 346 | idxG1 = i; |
| 775 | blopes | 347 | log(String.format("[VENDER] G1 encontrado em [%d], rompendo fundo de candRef[%d]", idxG1 + 1, idxCandidatoRef + 1)); |
| 764 | blopes | 348 | break; |
| 349 | } |
||
| 773 | blopes | 350 | |
| 775 | blopes | 351 | // Se ainda não encontrou G1, atualiza candidatoRef se máxima maior |
| 352 | if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), candidatoRef.getMaxima())) { |
||
| 353 | candidatoRef = c; |
||
| 354 | idxCandidatoRef = i; |
||
| 355 | log(String.format("[VENDER] CandidatoRef atualizado dinamicamente para idx %d", idxCandidatoRef + 1)); |
||
| 356 | } |
||
| 764 | blopes | 357 | } |
| 760 | blopes | 358 | |
| 775 | blopes | 359 | if (g1 == null) { |
| 360 | // Não formou G1; não existe GR definitivo; recomeça a partir do próximo candle após A. |
||
| 361 | return new ResultadoPadrao(null, lastIndex, idxA + 1); |
||
| 773 | blopes | 362 | } |
| 760 | blopes | 363 | |
| 775 | blopes | 364 | Candle gr = candidatoRef; |
| 365 | int idxGR = idxCandidatoRef; |
||
| 366 | log(String.format("[VENDER] GR definido em [%d], G1 em [%d]", idxGR + 1, idxG1 + 1)); |
||
| 760 | blopes | 367 | |
| 775 | blopes | 368 | // 3) Regra de saída do G1 – Fibonacci 200% (origem = máxG1, destino = mínG1) |
| 369 | BigDecimal fib200 = fibExtend(g1.getMaxima(), g1.getMinima(), new BigDecimal("2")); |
||
| 370 | log(String.format("[VENDER] Fibo200 G1[%d] = %s", idxG1, fib200.toPlainString())); |
||
| 371 | |||
| 372 | // 4) Busca G2 dinâmico e G3 |
||
| 771 | blopes | 373 | Candle g2 = null; |
| 374 | int idxG2 = -1; |
||
| 775 | blopes | 375 | Candle g3 = null; |
| 376 | int idxG3 = -1; |
||
| 771 | blopes | 377 | |
| 764 | blopes | 378 | for (int i = idxG1 + 1; i < n; i++) { |
| 379 | Candle c = candles.get(i); |
||
| 775 | blopes | 380 | if (!isDirecional(c) || isInside(candles, i)) { |
| 381 | log(String.format("[VENDER] Candle[%d] ignorado (não direcional ou inside) após G1", i + 1)); |
||
| 771 | blopes | 382 | continue; |
| 773 | blopes | 383 | } |
| 384 | |||
| 775 | blopes | 385 | lastIndex = i; |
| 773 | blopes | 386 | |
| 775 | blopes | 387 | // --- Regras de DESCARTE após G1 (devem reiniciar do candle após o GR) --- |
| 760 | blopes | 388 | |
| 775 | blopes | 389 | // 4.1) Se candle romper TOPO do GR => descarta operação |
| 773 | blopes | 390 | if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), gr.getMaxima())) { |
| 775 | blopes | 391 | log(String.format("[VENDER] GR[%d]: candle[%d] rompeu topo do GR. Descartando padrão.", idxGR + 1, i + 1)); |
| 392 | return new ResultadoPadrao(null, i, idxGR + 1); |
||
| 767 | blopes | 393 | } |
| 394 | |||
| 775 | blopes | 395 | // 4.2) Regra Fib 200% do G1: se mínima <= fib200 => descarta |
| 396 | if (BigDecimalUtils.ehMenorOuIgualQue(c.getMinima(), fib200)) { |
||
| 397 | log(String.format("[VENDER] GR[%d], G1[%d]: candle[%d] atingiu 200%% da fibo G1. Descartando padrão.", |
||
| 398 | idxGR + 1, idxG1 + 1, i + 1)); |
||
| 399 | return new ResultadoPadrao(null, i, idxGR + 1); |
||
| 764 | blopes | 400 | } |
| 760 | blopes | 401 | |
| 775 | blopes | 402 | // --- Construção de G2 dinâmico (COMPRADOR com FECHAMENTO dentro da região do GR) --- |
| 403 | if (c.isCandleComprador() && fechamentoDentroRegiaoGR(c, gr)) { |
||
| 404 | if (g2 == null) { |
||
| 405 | g2 = c; |
||
| 406 | idxG2 = i; |
||
| 407 | log(String.format("[VENDER] GR[%d], G1[%d]: candidato G2 em [%d]", idxGR + 1, idxG1 + 1, idxG2 + 1)); |
||
| 408 | } else { |
||
| 409 | // Atualização dinâmica: máxima maior e fechamento ainda dentro da região |
||
| 410 | if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), g2.getMaxima())) { |
||
| 411 | g2 = c; |
||
| 412 | idxG2 = i; |
||
| 413 | log(String.format("[VENDER] GR[%d], G1[%d]: G2 atualizado em [%d]", idxGR + 1, idxG1 + 1, idxG2 + 1)); |
||
| 414 | } |
||
| 415 | } |
||
| 764 | blopes | 416 | continue; |
| 774 | blopes | 417 | } |
| 760 | blopes | 418 | |
| 775 | blopes | 419 | // --- G3: só pode ser avaliado após existir candidato G2 --- |
| 420 | if (g2 != null && c.isCandleVendedor()) { |
||
| 774 | blopes | 421 | boolean rompeFundoG2 = BigDecimalUtils.ehMenorQue(c.getMinima(), g2.getMinima()); |
| 422 | boolean topoMenorOuIgualGR = BigDecimalUtils.ehMenorOuIgualQue(c.getMaxima(), gr.getMaxima()); |
||
| 775 | blopes | 423 | |
| 774 | blopes | 424 | if (rompeFundoG2 && topoMenorOuIgualGR) { |
| 425 | g3 = c; |
||
| 426 | idxG3 = i; |
||
| 775 | blopes | 427 | log(String.format("[VENDER] GR[%d], G1[%d], G2[%d]: G3 em [%d] (padrão confirmado)", |
| 428 | idxGR + 1, idxG1 + 1, idxG2 + 1, idxG3 + 1)); |
||
| 774 | blopes | 429 | break; |
| 430 | } |
||
| 764 | blopes | 431 | } |
| 432 | } |
||
| 773 | blopes | 433 | |
| 434 | if (g3 == null) { |
||
| 775 | blopes | 435 | // Padrão só até G2 (se G2 existir), senão nada. |
| 436 | if (g2 != null) { |
||
| 437 | log(String.format("[VENDER] Padrão parcial (GR[%d], G1[%d], G2[%d]) sem G3.", idxGR + 1, idxG1 + 1, idxG2 + 1)); |
||
| 438 | return criarResultadoParcialComG2(gr, g1, g2, lastIndex, idxGR); |
||
| 439 | } |
||
| 440 | // Já houve GR e G1, mas não houve G2/G3 -> recomeça após GR. |
||
| 441 | log(String.format("[VENDER] GR[%d], G1[%d] sem G2/G3. Recomeçando após GR.", idxGR + 1, idxG1 + 1)); |
||
| 442 | return new ResultadoPadrao(null, lastIndex, idxGR + 1); |
||
| 773 | blopes | 443 | } |
| 760 | blopes | 444 | |
| 775 | blopes | 445 | // Finaliza padrão no G3 |
| 764 | blopes | 446 | PadraoGatilho padrao = new PadraoGatilho(); |
| 771 | blopes | 447 | padrao.setReferencia(gr); |
| 764 | blopes | 448 | padrao.setGatilho1(g1); |
| 449 | padrao.setGatilho2(g2); |
||
| 450 | padrao.setGatilho3(g3); |
||
| 773 | blopes | 451 | padrao.setGatilho4(null); |
| 760 | blopes | 452 | |
| 775 | blopes | 453 | // Padrão completo -> próxima busca deve começar após o GR |
| 454 | return new ResultadoPadrao(padrao, idxG3, idxGR + 1); |
||
| 764 | blopes | 455 | } |
| 760 | blopes | 456 | |
| 764 | blopes | 457 | // ===================================================================== |
| 775 | blopes | 458 | // PADRÃO VENDEDOR (OPERAÇÃO COMPRADORA) |
| 764 | blopes | 459 | // ===================================================================== |
| 760 | blopes | 460 | |
| 775 | blopes | 461 | private ResultadoPadrao detectarPadraoVendedor(List<Candle> candles, int idxA) { |
| 764 | blopes | 462 | int n = candles.size(); |
| 775 | blopes | 463 | Candle candleA = candles.get(idxA); |
| 464 | if (!candleA.isCandleVendedor()) { |
||
| 465 | // Não pode ser A vendedor -> reinicia no próximo candle. |
||
| 466 | return new ResultadoPadrao(null, idxA, idxA + 1); |
||
| 467 | } |
||
| 760 | blopes | 468 | |
| 775 | blopes | 469 | int lastIndex = idxA; |
| 470 | log(String.format("[COMPRAR] Iniciando busca a partir de A[%d]", idxA + 1)); |
||
| 471 | |||
| 472 | // 1) Verifica se o próximo candle direcional (não-inside) muda a direção |
||
| 473 | int idxProxDirecional = -1; |
||
| 474 | for (int i = idxA + 1; i < n; i++) { |
||
| 475 | Candle c = candles.get(i); |
||
| 476 | if (!isDirecional(c) || isInside(candles, i)) { |
||
| 477 | log(String.format("[COMPRAR] Candle[%d] ignorado (não direcional ou inside)", i + 1)); |
||
| 478 | continue; |
||
| 479 | } |
||
| 480 | idxProxDirecional = i; |
||
| 481 | break; |
||
| 764 | blopes | 482 | } |
| 760 | blopes | 483 | |
| 775 | blopes | 484 | if (idxProxDirecional == -1) { |
| 485 | // Não houve candle B; A não vira candidato; recomeça do próximo candle. |
||
| 486 | return new ResultadoPadrao(null, idxA, idxA + 1); |
||
| 487 | } |
||
| 760 | blopes | 488 | |
| 775 | blopes | 489 | Candle prox = candles.get(idxProxDirecional); |
| 490 | if (!prox.isCandleComprador()) { |
||
| 491 | // Não houve mudança direcional imediata -> A não vira candidato |
||
| 492 | return new ResultadoPadrao(null, idxA, idxA + 1); |
||
| 493 | } |
||
| 494 | |||
| 495 | // A vira candidato à referência |
||
| 496 | Candle candidatoRef = candleA; |
||
| 497 | int idxCandidatoRef = idxA; |
||
| 498 | log(String.format("[COMPRAR] CandidatoRef inicial = idx %d", idxCandidatoRef + 1)); |
||
| 499 | |||
| 500 | // 2) Busca G1, com referência dinâmica até G1 |
||
| 771 | blopes | 501 | Candle g1 = null; |
| 764 | blopes | 502 | int idxG1 = -1; |
| 773 | blopes | 503 | |
| 775 | blopes | 504 | for (int i = idxA + 1; i < n; i++) { |
| 764 | blopes | 505 | Candle c = candles.get(i); |
| 775 | blopes | 506 | if (!isDirecional(c) || isInside(candles, i)) { |
| 507 | log(String.format("[COMPRAR] Candle[%d] ignorado (não direcional ou inside) antes do G1", i + 1)); |
||
| 771 | blopes | 508 | continue; |
| 774 | blopes | 509 | } |
| 760 | blopes | 510 | |
| 775 | blopes | 511 | lastIndex = i; |
| 760 | blopes | 512 | |
| 775 | blopes | 513 | // Primeiro checa G1: comprador que rompe topo do candidatoRef |
| 773 | blopes | 514 | if (c.isCandleComprador() |
| 515 | && BigDecimalUtils.ehMaiorQue(c.getMaxima(), candidatoRef.getMaxima())) { |
||
| 771 | blopes | 516 | g1 = c; |
| 764 | blopes | 517 | idxG1 = i; |
| 775 | blopes | 518 | log(String.format("[COMPRAR] G1 encontrado em [%d], rompendo topo de candRef[%d]", idxG1 + 1, idxCandidatoRef + 1)); |
| 764 | blopes | 519 | break; |
| 520 | } |
||
| 773 | blopes | 521 | |
| 775 | blopes | 522 | // Se ainda não encontrou G1, atualiza candidatoRef se mínima menor |
| 523 | if (BigDecimalUtils.ehMenorQue(c.getMinima(), candidatoRef.getMinima())) { |
||
| 524 | candidatoRef = c; |
||
| 525 | idxCandidatoRef = i; |
||
| 526 | log(String.format("[COMPRAR] CandidatoRef atualizado dinamicamente para idx %d", idxCandidatoRef + 1)); |
||
| 527 | } |
||
| 764 | blopes | 528 | } |
| 760 | blopes | 529 | |
| 775 | blopes | 530 | if (g1 == null) { |
| 531 | // Não formou G1; não existe GR definitivo; recomeça a partir do próximo candle após A. |
||
| 532 | return new ResultadoPadrao(null, lastIndex, idxA + 1); |
||
| 773 | blopes | 533 | } |
| 760 | blopes | 534 | |
| 775 | blopes | 535 | Candle gr = candidatoRef; |
| 536 | int idxGR = idxCandidatoRef; |
||
| 537 | log(String.format("[COMPRAR] GR definido em [%d], G1 em [%d]", idxGR + 1, idxG1 + 1)); |
||
| 773 | blopes | 538 | |
| 775 | blopes | 539 | // 3) Regra de saída do G1 – Fibonacci 200% (origem = mínG1, destino = máxG1) |
| 540 | BigDecimal fib200MinMax = fibExtend(g1.getMinima(), g1.getMaxima(), new BigDecimal("2")); |
||
| 541 | log(String.format("[COMPRAR] Fibo200 G1[%d] = %s", idxG1 + 1, fib200MinMax.toPlainString())); |
||
| 542 | |||
| 543 | // 4) Busca G2 dinâmico e G3 |
||
| 771 | blopes | 544 | Candle g2 = null; |
| 545 | int idxG2 = -1; |
||
| 775 | blopes | 546 | Candle g3 = null; |
| 547 | int idxG3 = -1; |
||
| 760 | blopes | 548 | |
| 764 | blopes | 549 | for (int i = idxG1 + 1; i < n; i++) { |
| 550 | Candle c = candles.get(i); |
||
| 775 | blopes | 551 | if (!isDirecional(c) || isInside(candles, i)) { |
| 552 | log(String.format("[COMPRAR] Candle[%d] ignorado (não direcional ou inside) após G1", i + 1)); |
||
| 553 | continue; |
||
| 773 | blopes | 554 | } |
| 555 | |||
| 775 | blopes | 556 | lastIndex = i; |
| 773 | blopes | 557 | |
| 775 | blopes | 558 | // --- Regras de DESCARTE após G1 (reiniciar após GR) --- |
| 760 | blopes | 559 | |
| 775 | blopes | 560 | // 4.1) Se candle romper FUNDO do GR => descarta operação |
| 773 | blopes | 561 | if (BigDecimalUtils.ehMenorQue(c.getMinima(), gr.getMinima())) { |
| 775 | blopes | 562 | log(String.format("[COMPRAR] GR[%d]: candle[%d] rompeu fundo do GR. Descartando padrão.", idxGR + 1, i + 1)); |
| 563 | return new ResultadoPadrao(null, i, idxGR + 1); |
||
| 767 | blopes | 564 | } |
| 565 | |||
| 775 | blopes | 566 | // 4.2) Regra Fib 200% do G1: se mínima > fib200MinMax => descarta |
| 567 | if (BigDecimalUtils.ehMaiorQue(c.getMinima(), fib200MinMax)) { |
||
| 568 | log(String.format("[COMPRAR] GR[%d], G1[%d]: candle[%d] atingiu 200%% da fibo G1. Descartando padrão.", |
||
| 569 | idxGR + 1, idxG1 + 1, i + 1)); |
||
| 570 | return new ResultadoPadrao(null, i, idxGR + 1); |
||
| 764 | blopes | 571 | } |
| 760 | blopes | 572 | |
| 775 | blopes | 573 | // --- Construção de G2 dinâmico (VENDEDOR com FECHAMENTO dentro da região do GR) --- |
| 574 | if (c.isCandleVendedor() && fechamentoDentroRegiaoGR(c, gr)) { |
||
| 575 | if (g2 == null) { |
||
| 576 | g2 = c; |
||
| 577 | idxG2 = i; |
||
| 578 | log(String.format("[COMPRAR] GR[%d], G1[%d]: candidato G2 em [%d]", idxGR + 1, idxG1 + 1, idxG2 + 1)); |
||
| 579 | } else { |
||
| 580 | // Atualização dinâmica: mínima menor e fechamento ainda dentro da região |
||
| 581 | if (BigDecimalUtils.ehMenorQue(c.getMinima(), g2.getMinima())) { |
||
| 582 | g2 = c; |
||
| 583 | idxG2 = i; |
||
| 584 | log(String.format("[COMPRAR] GR[%d], G1[%d]: G2 atualizado em [%d]", idxGR + 1, idxG1 + 1, idxG2 + 1)); |
||
| 585 | } |
||
| 586 | } |
||
| 764 | blopes | 587 | continue; |
| 774 | blopes | 588 | } |
| 760 | blopes | 589 | |
| 775 | blopes | 590 | // --- G3: só pode ser avaliado após existir candidato G2 --- |
| 591 | if (g2 != null && c.isCandleComprador()) { |
||
| 592 | boolean rompeTopoG2 = BigDecimalUtils.ehMaiorQue(c.getMaxima(), g2.getMaxima()); |
||
| 593 | boolean fundoMenorOuIgualGR = BigDecimalUtils.ehMenorOuIgualQue(c.getMinima(), gr.getMinima()); |
||
| 773 | blopes | 594 | |
| 775 | blopes | 595 | if (rompeTopoG2 && fundoMenorOuIgualGR) { |
| 774 | blopes | 596 | g3 = c; |
| 597 | idxG3 = i; |
||
| 775 | blopes | 598 | log(String.format("[COMPRAR] GR[%d], G1[%d], G2[%d]: G3 em [%d] (padrão confirmado)", |
| 599 | idxGR + 1, idxG1 + 1, idxG2 + 1, idxG3 + 1)); |
||
| 774 | blopes | 600 | break; |
| 601 | } |
||
| 764 | blopes | 602 | } |
| 603 | } |
||
| 773 | blopes | 604 | |
| 605 | if (g3 == null) { |
||
| 775 | blopes | 606 | // Padrão só até G2 (se G2 existir), senão nada |
| 607 | if (g2 != null) { |
||
| 608 | log(String.format("[COMPRAR] Padrão parcial (GR[%d], G1[%d], G2[%d]) sem G3.", idxGR + 1, idxG1 + 1, idxG2 + 1)); |
||
| 609 | return criarResultadoParcialComG2(gr, g1, g2, lastIndex, idxGR); |
||
| 610 | } |
||
| 611 | // Já houve GR e G1, mas não houve G2/G3 -> recomeça após GR. |
||
| 612 | log(String.format("[COMPRAR] GR[%d], G1[%d] sem G2/G3. Recomeçando após GR.", idxGR + 1, idxG1 + 1)); |
||
| 613 | return new ResultadoPadrao(null, lastIndex, idxGR + 1); |
||
| 773 | blopes | 614 | } |
| 760 | blopes | 615 | |
| 775 | blopes | 616 | // Finaliza padrão no G3 |
| 764 | blopes | 617 | PadraoGatilho padrao = new PadraoGatilho(); |
| 771 | blopes | 618 | padrao.setReferencia(gr); |
| 764 | blopes | 619 | padrao.setGatilho1(g1); |
| 620 | padrao.setGatilho2(g2); |
||
| 621 | padrao.setGatilho3(g3); |
||
| 773 | blopes | 622 | padrao.setGatilho4(null); |
| 760 | blopes | 623 | |
| 775 | blopes | 624 | // Padrão completo -> próxima busca deve começar após o GR |
| 625 | return new ResultadoPadrao(padrao, idxG3, idxGR + 1); |
||
| 764 | blopes | 626 | } |
| 760 | blopes | 627 | |
| 764 | blopes | 628 | // ===================================================================== |
| 771 | blopes | 629 | // MODO TEMPO REAL |
| 764 | blopes | 630 | // ===================================================================== |
| 760 | blopes | 631 | |
| 764 | blopes | 632 | public void resetTempoReal() { |
| 633 | this.idxProximaAnaliseTempoReal = 0; |
||
| 634 | } |
||
| 760 | blopes | 635 | |
| 774 | blopes | 636 | /** |
| 637 | * Deve ser chamado SEMPRE que um novo candle for adicionado à lista. |
||
| 638 | * |
||
| 639 | * Exemplo: |
||
| 640 | * candles.add(novoCandle); |
||
| 641 | * PadraoGatilho padrao = detector.processarCandleTempoReal(candles); |
||
| 642 | * |
||
| 643 | * if (padrao != null) { |
||
| 644 | * // padrão completo (até G3) encontrado |
||
| 645 | * } |
||
| 646 | */ |
||
| 764 | blopes | 647 | public PadraoGatilho processarCandleTempoReal(List<Candle> candles) { |
| 648 | int n = candles.size(); |
||
| 771 | blopes | 649 | if (n < 4) return null; |
| 760 | blopes | 650 | |
| 764 | blopes | 651 | while (idxProximaAnaliseTempoReal < n - 3) { |
| 652 | ResultadoPadrao resultado = detectarPadraoAPartir(candles, idxProximaAnaliseTempoReal); |
||
| 760 | blopes | 653 | |
| 764 | blopes | 654 | if (resultado == null) { |
| 655 | idxProximaAnaliseTempoReal++; |
||
| 656 | continue; |
||
| 657 | } |
||
| 760 | blopes | 658 | |
| 775 | blopes | 659 | int proximoInicio = resultado.proximoInicio; |
| 764 | blopes | 660 | if (proximoInicio <= idxProximaAnaliseTempoReal) { |
| 661 | proximoInicio = idxProximaAnaliseTempoReal + 1; |
||
| 662 | } |
||
| 663 | idxProximaAnaliseTempoReal = proximoInicio; |
||
| 760 | blopes | 664 | |
| 764 | blopes | 665 | if (resultado.padrao != null) { |
| 666 | return resultado.padrao; |
||
| 667 | } |
||
| 668 | } |
||
| 760 | blopes | 669 | |
| 764 | blopes | 670 | return null; |
| 671 | } |
||
| 760 | blopes | 672 | |
| 775 | blopes | 673 | // ===================================================================== |
| 674 | // DEBUG / RELATÓRIO |
||
| 675 | // ===================================================================== |
||
| 676 | |||
| 677 | /** |
||
| 678 | * Roda a lógica de detecção a partir de um índice específico e devolve |
||
| 679 | * um "relatório" em forma de lista de strings com tudo que aconteceu. |
||
| 680 | * |
||
| 681 | * NÃO altera idxProximaAnaliseTempoReal. |
||
| 682 | */ |
||
| 683 | public List<String> debugarAPartirDoIndice(List<Candle> candles, int idxInicio) { |
||
| 684 | List<String> relatorio = new ArrayList<>(); |
||
| 685 | List<String> antigoBuffer = this.bufferDebug; |
||
| 686 | this.bufferDebug = relatorio; |
||
| 687 | try { |
||
| 688 | log("===================================================="); |
||
| 689 | log(String.format("DEBUG: iniciando debugarAPartirDoIndice(%d)", idxInicio)); |
||
| 690 | detectarPadraoAPartir(candles, idxInicio); |
||
| 691 | log(String.format("DEBUG: fim da análise a partir do índice %d", idxInicio)); |
||
| 692 | log("===================================================="); |
||
| 693 | } finally { |
||
| 694 | this.bufferDebug = antigoBuffer; |
||
| 695 | } |
||
| 696 | return relatorio; |
||
| 697 | } |
||
| 698 | |||
| 764 | blopes | 699 | } |