Rev 771 | Rev 774 | 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 | |||
| 3 | import java.util.ArrayList; |
||
| 764 | blopes | 4 | import java.util.List; |
| 760 | blopes | 5 | |
| 764 | blopes | 6 | import br.com.sl.domain.model.Candle; |
| 7 | import br.com.sl.domain.util.BigDecimalUtils; |
||
| 8 | |||
| 760 | blopes | 9 | /** |
| 773 | blopes | 10 | * Detector de padrões de gatilhos (GR, G1, G2, G3) |
| 764 | blopes | 11 | * trabalhando tanto em modo backtest quanto em modo tempo real. |
| 760 | blopes | 12 | * |
| 773 | blopes | 13 | * IMPORTANTE: |
| 14 | * - Este detector FINALIZA o padrão assim que identifica o G3. |
||
| 15 | * - O G4 será identificado apenas na camada de sinais de trade, fora desta classe. |
||
| 760 | blopes | 16 | * |
| 773 | blopes | 17 | * REGRAS IMPLEMENTADAS (com GR DINÂMICO invertido) |
| 18 | * ================================================ |
||
| 19 | * |
||
| 771 | blopes | 20 | * COMPRADOR |
| 21 | * --------- |
||
| 773 | blopes | 22 | * Ponto de partida: um candle COMPRADOR é candidato à referência. |
| 760 | blopes | 23 | * |
| 773 | blopes | 24 | * G1: |
| 25 | * - Enquanto não houver G1: |
||
| 26 | * * O candidato à referência pode ser atualizado dinamicamente: |
||
| 27 | * QUALQUER candle posterior, antes do G1, que fizer uma MÁXIMA |
||
| 28 | * MAIOR que o candidato atual passa a ser o novo candidato. |
||
| 29 | * * Se algum candle VENDEDOR subsequente ROMPER o FUNDO |
||
| 30 | * do candle candidato à referência (mínima < mínima do candidato), |
||
| 31 | * esse candle será o gatilho tipo 1 [G1] e o candidato vigente |
||
| 32 | * torna-se o candle referência [GR]. |
||
| 760 | blopes | 33 | * |
| 773 | blopes | 34 | * G2: |
| 35 | * - Após o G1 (com GR definido): |
||
| 36 | * * Assim que tiver nova tendência COMPRADORA: |
||
| 37 | * - O último candle dessa sequência compradora deve ter o FECHAMENTO |
||
| 38 | * dentro da região total (mínima..máxima) do GR. |
||
| 39 | * - Se mudar para vendedor sem nenhum candle fechar dentro da região |
||
| 40 | * do GR → descarta o padrão. |
||
| 41 | * * Se qualquer candle (após o GR) romper o TOPO do GR |
||
| 42 | * (máxima > máxima GR) → descarta o padrão. |
||
| 43 | * * Se válido, esse último comprador é o gatilho tipo 2 [G2]. |
||
| 760 | blopes | 44 | * |
| 773 | blopes | 45 | * G3: |
| 46 | * - Após o G2: |
||
| 47 | * * O próximo direcional deve ser VENDEDOR; se for comprador, |
||
| 48 | * o padrão vai apenas até G2 (parcial). |
||
| 49 | * * Numa sequência vendedora: |
||
| 50 | * - Se algum candle VENDEDOR posterior ROMPER o FUNDO do G2 |
||
| 51 | * (mínima < mínima G2) e tiver TOPO <= topo do GR |
||
| 52 | * (máxima <= máxima GR), sem mudança de tendência |
||
| 53 | * (sem compradores no meio), será o gatilho tipo 3 [G3]. |
||
| 760 | blopes | 54 | * |
| 773 | blopes | 55 | * Regra global comprador: |
| 56 | * - Se algum candle posterior ao GR ROMPER o TOPO do GR |
||
| 57 | * (máxima > máxima GR), desconsiderar o padrão. |
||
| 764 | blopes | 58 | * |
| 773 | blopes | 59 | * ----------------------------------------------------------- |
| 764 | blopes | 60 | * |
| 771 | blopes | 61 | * VENDEDOR |
| 62 | * -------- |
||
| 773 | blopes | 63 | * Ponto de partida: um candle VENDEDOR é candidato à referência. |
| 764 | blopes | 64 | * |
| 773 | blopes | 65 | * G1: |
| 66 | * - Enquanto não houver G1: |
||
| 67 | * * O candidato à referência pode ser atualizado dinamicamente: |
||
| 68 | * QUALQUER candle posterior, antes do G1, que fizer uma MÍNIMA |
||
| 69 | * MENOR que a do candidato atual passa a ser o novo candidato. |
||
| 70 | * * Se algum candle COMPRADOR subsequente ROMPER o TOPO |
||
| 71 | * do candidato (máxima > máxima do candidato), esse candle |
||
| 72 | * será o gatilho tipo 1 [G1] e o candidato vigente torna-se o GR. |
||
| 764 | blopes | 73 | * |
| 773 | blopes | 74 | * Regra global vendedor: |
| 75 | * - Se algum candle posterior ao GR ROMPER o FUNDO do GR |
||
| 76 | * (mínima < mínima GR), desconsiderar o padrão. |
||
| 764 | blopes | 77 | * |
| 773 | blopes | 78 | * G2: |
| 79 | * - Após o G1 (com GR definido): |
||
| 80 | * * Assim que tiver nova tendência VENDEDORA: |
||
| 81 | * - O último candle dessa sequência vendedora deve ter FECHAMENTO |
||
| 82 | * dentro da região total (mínima..máxima) do GR. |
||
| 83 | * - Se mudar para comprador sem nenhum candle fechar dentro |
||
| 84 | * da região do GR → descarta o padrão. |
||
| 85 | * * Se romper o FUNDO do GR em qualquer momento após o GR |
||
| 86 | * → descarta o padrão. |
||
| 87 | * * Se válido, esse último vendedor é o gatilho tipo 2 [G2]. |
||
| 771 | blopes | 88 | * |
| 773 | blopes | 89 | * G3: |
| 90 | * - Após o G2: |
||
| 91 | * * O próximo direcional deve ser COMPRADOR; se for vendedor, |
||
| 92 | * o padrão vai até G2 (parcial). |
||
| 93 | * * Numa sequência compradora: |
||
| 94 | * - Se algum COMPRADOR posterior ROMPER o TOPO de G2 |
||
| 95 | * (máxima > máxima G2) e tiver FUNDO >= fundo do GR |
||
| 96 | * (mínima >= mínima GR), sem mudança de tendência |
||
| 97 | * (sem vendedores no meio), será o gatilho tipo 3 [G3]. |
||
| 771 | blopes | 98 | * |
| 773 | blopes | 99 | * Regra global adicional (texto original repetia): |
| 100 | * - Mantida a regra principal: vendedor é invalidado |
||
| 101 | * quando rompem o FUNDO do GR. |
||
| 771 | blopes | 102 | * |
| 773 | blopes | 103 | * ----------------------------------------------------------- |
| 104 | * |
||
| 771 | blopes | 105 | * OUTSIDE (ambos os lados): |
| 773 | blopes | 106 | * - Candle cuja máxima > máxima do candle anterior |
| 107 | * E mínima < mínima do candle anterior. |
||
| 108 | * - Se houver um OUTSIDE antes de identificar o G3: |
||
| 109 | * * Desconsidera o padrão atual (GR+G1+G2) e recomeça |
||
| 110 | * a busca a partir do GR (na prática, aborta o padrão). |
||
| 111 | * |
||
| 112 | * OBS: |
||
| 113 | * - Os gatilhos devem seguir ORDEM cronológica: |
||
| 114 | * G1 -> G2 -> G3, sempre em candles diferentes. |
||
| 760 | blopes | 115 | */ |
| 116 | public class DetectorGatilhos { |
||
| 117 | |||
| 764 | blopes | 118 | private final boolean logAtivo; |
| 119 | private int idxProximaAnaliseTempoReal = 0; |
||
| 760 | blopes | 120 | |
| 764 | blopes | 121 | public DetectorGatilhos() { |
| 122 | this(false); |
||
| 123 | } |
||
| 760 | blopes | 124 | |
| 764 | blopes | 125 | public DetectorGatilhos(boolean logAtivo) { |
| 126 | this.logAtivo = logAtivo; |
||
| 127 | } |
||
| 760 | blopes | 128 | |
| 764 | blopes | 129 | private void log(String msg) { |
| 130 | if (logAtivo) { |
||
| 131 | System.out.println(msg); |
||
| 132 | } |
||
| 133 | } |
||
| 760 | blopes | 134 | |
| 773 | blopes | 135 | // ----------------------------------------------------------------- |
| 136 | // Estrutura interna de retorno |
||
| 137 | // ----------------------------------------------------------------- |
||
| 764 | blopes | 138 | private static class ResultadoPadrao { |
| 771 | blopes | 139 | PadraoGatilho padrao; |
| 140 | int lastIndex; |
||
| 773 | blopes | 141 | |
| 764 | blopes | 142 | ResultadoPadrao(PadraoGatilho padrao, int lastIndex) { |
| 143 | this.padrao = padrao; |
||
| 144 | this.lastIndex = lastIndex; |
||
| 145 | } |
||
| 146 | } |
||
| 760 | blopes | 147 | |
| 764 | blopes | 148 | private ResultadoPadrao criarResultadoParcialComG2(Candle ref, |
| 149 | Candle g1, |
||
| 150 | Candle g2, |
||
| 151 | int lastIndex) { |
||
| 152 | PadraoGatilho padrao = new PadraoGatilho(); |
||
| 153 | padrao.setReferencia(ref); |
||
| 154 | padrao.setGatilho1(g1); |
||
| 155 | padrao.setGatilho2(g2); |
||
| 156 | padrao.setGatilho3(null); |
||
| 773 | blopes | 157 | padrao.setGatilho4(null); // G4 só na camada de trade |
| 764 | blopes | 158 | return new ResultadoPadrao(padrao, lastIndex); |
| 159 | } |
||
| 760 | blopes | 160 | |
| 764 | blopes | 161 | // ===================================================================== |
| 771 | blopes | 162 | // API PRINCIPAL – BACKTEST |
| 764 | blopes | 163 | // ===================================================================== |
| 760 | blopes | 164 | |
| 764 | blopes | 165 | public List<PadraoGatilho> identificarPadroes(List<Candle> candles) { |
| 166 | List<PadraoGatilho> padroes = new ArrayList<>(); |
||
| 167 | int n = candles.size(); |
||
| 771 | blopes | 168 | if (n < 4) return padroes; |
| 760 | blopes | 169 | |
| 764 | blopes | 170 | int idxRef = 0; |
| 171 | while (idxRef < n - 3) { |
||
| 172 | ResultadoPadrao resultado = detectarPadraoAPartir(candles, idxRef); |
||
| 760 | blopes | 173 | |
| 764 | blopes | 174 | if (resultado == null) { |
| 175 | idxRef++; |
||
| 176 | continue; |
||
| 177 | } |
||
| 760 | blopes | 178 | |
| 764 | blopes | 179 | if (resultado.padrao != null) { |
| 180 | padroes.add(resultado.padrao); |
||
| 181 | } |
||
| 760 | blopes | 182 | |
| 764 | blopes | 183 | idxRef = Math.max(resultado.lastIndex + 1, idxRef + 1); |
| 184 | } |
||
| 760 | blopes | 185 | |
| 764 | blopes | 186 | return padroes; |
| 187 | } |
||
| 760 | blopes | 188 | |
| 764 | blopes | 189 | private ResultadoPadrao detectarPadraoAPartir(List<Candle> candles, int idxRef) { |
| 190 | Candle ref = candles.get(idxRef); |
||
| 191 | if (ref.isCandleComprador()) { |
||
| 192 | return detectarPadraoComprador(candles, idxRef); |
||
| 193 | } else if (ref.isCandleVendedor()) { |
||
| 194 | return detectarPadraoVendedor(candles, idxRef); |
||
| 195 | } else { |
||
| 196 | return new ResultadoPadrao(null, idxRef); |
||
| 197 | } |
||
| 198 | } |
||
| 760 | blopes | 199 | |
| 764 | blopes | 200 | // ===================================================================== |
| 771 | blopes | 201 | // HELPERS |
| 764 | blopes | 202 | // ===================================================================== |
| 760 | blopes | 203 | |
| 771 | blopes | 204 | private boolean isInside(List<Candle> candles, int idx) { |
| 205 | if (idx <= 0) return false; |
||
| 206 | Candle atual = candles.get(idx); |
||
| 207 | Candle anterior = candles.get(idx - 1); |
||
| 208 | return BigDecimalUtils.ehMenorOuIgualQue(atual.getMaxima(), anterior.getMaxima()) |
||
| 209 | && BigDecimalUtils.ehMaiorOuIgualQue(atual.getMinima(), anterior.getMinima()); |
||
| 210 | } |
||
| 760 | blopes | 211 | |
| 771 | blopes | 212 | private boolean isOutside(List<Candle> candles, int idx) { |
| 213 | if (idx <= 0) return false; |
||
| 214 | Candle atual = candles.get(idx); |
||
| 215 | Candle anterior = candles.get(idx - 1); |
||
| 216 | return BigDecimalUtils.ehMaiorQue(atual.getMaxima(), anterior.getMaxima()) |
||
| 217 | && BigDecimalUtils.ehMenorQue(atual.getMinima(), anterior.getMinima()); |
||
| 218 | } |
||
| 760 | blopes | 219 | |
| 773 | blopes | 220 | private boolean isDirecional(Candle c) { |
| 221 | return c.isCandleComprador() || c.isCandleVendedor(); |
||
| 222 | } |
||
| 223 | |||
| 771 | blopes | 224 | // ===================================================================== |
| 773 | blopes | 225 | // CASO COMPRADOR – GR DINÂMICO PELA MÁXIMA |
| 771 | blopes | 226 | // ===================================================================== |
| 227 | |||
| 773 | blopes | 228 | private ResultadoPadrao detectarPadraoComprador(List<Candle> candles, int idxInicio) { |
| 771 | blopes | 229 | int n = candles.size(); |
| 773 | blopes | 230 | int lastIndex = idxInicio; |
| 760 | blopes | 231 | |
| 773 | blopes | 232 | Candle candidatoRef = candles.get(idxInicio); |
| 233 | if (!candidatoRef.isCandleComprador()) { |
||
| 234 | return new ResultadoPadrao(null, idxInicio); |
||
| 764 | blopes | 235 | } |
| 760 | blopes | 236 | |
| 773 | blopes | 237 | int idxCandidatoRef = idxInicio; |
| 760 | blopes | 238 | |
| 773 | blopes | 239 | // -------------------- |
| 240 | // 1) Buscar G1 (vendedor rompe fundo do candidato) |
||
| 241 | // -------------------- |
||
| 771 | blopes | 242 | Candle g1 = null; |
| 764 | blopes | 243 | int idxG1 = -1; |
| 773 | blopes | 244 | boolean temGR = false; |
| 245 | Candle gr = null; |
||
| 246 | int idxGR = -1; |
||
| 247 | |||
| 248 | for (int i = idxInicio + 1; i < n; i++) { |
||
| 764 | blopes | 249 | Candle c = candles.get(i); |
| 773 | blopes | 250 | if (isInside(candles, i) || !isDirecional(c)) |
| 771 | blopes | 251 | continue; |
| 760 | blopes | 252 | |
| 773 | blopes | 253 | // Outside antes de G3 (aqui ainda antes de G1) |
| 254 | if (isOutside(candles, i)) { |
||
| 255 | lastIndex = i; |
||
| 256 | log(String.format("Comprador: OUTSIDE em [%d] antes de G1. Abortando padrão.", i)); |
||
| 764 | blopes | 257 | return new ResultadoPadrao(null, lastIndex); |
| 258 | } |
||
| 760 | blopes | 259 | |
| 773 | blopes | 260 | // G1: vendedor rompe fundo do candidato |
| 261 | if (c.isCandleVendedor() |
||
| 262 | && BigDecimalUtils.ehMenorQue(c.getMinima(), candidatoRef.getMinima())) { |
||
| 771 | blopes | 263 | g1 = c; |
| 764 | blopes | 264 | idxG1 = i; |
| 773 | blopes | 265 | gr = candidatoRef; |
| 266 | idxGR = idxCandidatoRef; |
||
| 267 | temGR = true; |
||
| 268 | lastIndex = i; |
||
| 764 | blopes | 269 | break; |
| 270 | } |
||
| 773 | blopes | 271 | |
| 272 | // GR DINÂMICO (compra): atualiza pelo TOPO mais alto antes de G1 |
||
| 273 | if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), candidatoRef.getMaxima())) { |
||
| 274 | candidatoRef = c; |
||
| 275 | idxCandidatoRef = i; |
||
| 276 | lastIndex = i; |
||
| 277 | continue; |
||
| 278 | } |
||
| 279 | |||
| 280 | lastIndex = i; |
||
| 764 | blopes | 281 | } |
| 760 | blopes | 282 | |
| 773 | blopes | 283 | if (g1 == null || !temGR) { |
| 284 | log(String.format("Comprador: não foi possível formar G1 a partir de idx %d.", idxInicio)); |
||
| 285 | return new ResultadoPadrao(null, lastIndex); |
||
| 286 | } |
||
| 760 | blopes | 287 | |
| 773 | blopes | 288 | log(String.format("GR COMPRADOR em [%d], G1 (vendedor) em [%d].", idxGR, idxG1)); |
| 760 | blopes | 289 | |
| 773 | blopes | 290 | // -------------------- |
| 291 | // 2) Buscar G2 (tendência compradora com fechamento dentro do GR) |
||
| 292 | // -------------------- |
||
| 771 | blopes | 293 | Candle g2 = null; |
| 294 | int idxG2 = -1; |
||
| 295 | |||
| 764 | blopes | 296 | int idxPrimeiroComprador = -1; |
| 297 | for (int i = idxG1 + 1; i < n; i++) { |
||
| 298 | Candle c = candles.get(i); |
||
| 773 | blopes | 299 | if (isInside(candles, i) || !isDirecional(c)) |
| 771 | blopes | 300 | continue; |
| 760 | blopes | 301 | |
| 773 | blopes | 302 | // Outside antes de G3 |
| 303 | if (isOutside(candles, i)) { |
||
| 304 | lastIndex = idxGR; |
||
| 305 | log(String.format("GR[%d]: OUTSIDE em [%d] antes da perna compradora de G2.", idxGR, i)); |
||
| 306 | return new ResultadoPadrao(null, lastIndex); |
||
| 307 | } |
||
| 308 | |||
| 309 | // Regra global: romper topo do GR invalida |
||
| 310 | if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), gr.getMaxima())) { |
||
| 311 | lastIndex = i; |
||
| 312 | log(String.format("GR[%d]: candle[%d] rompeu topo do GR antes de G2.", idxGR, i)); |
||
| 313 | return new ResultadoPadrao(null, lastIndex); |
||
| 314 | } |
||
| 315 | |||
| 764 | blopes | 316 | if (c.isCandleComprador()) { |
| 317 | idxPrimeiroComprador = i; |
||
| 318 | break; |
||
| 319 | } else { |
||
| 320 | lastIndex = i; |
||
| 321 | } |
||
| 322 | } |
||
| 760 | blopes | 323 | |
| 773 | blopes | 324 | if (idxPrimeiroComprador == -1) { |
| 325 | log(String.format("GR[%d], G1[%d]: não houve nova tendência compradora para G2.", idxGR, idxG1)); |
||
| 326 | return new ResultadoPadrao(null, lastIndex); |
||
| 327 | } |
||
| 760 | blopes | 328 | |
| 771 | blopes | 329 | Candle ultimoComprador = null; |
| 330 | int idxUltimoComprador = -1; |
||
| 760 | blopes | 331 | |
| 764 | blopes | 332 | for (int i = idxPrimeiroComprador; i < n; i++) { |
| 333 | Candle c = candles.get(i); |
||
| 773 | blopes | 334 | if (isInside(candles, i) || !isDirecional(c)) |
| 335 | continue; |
||
| 760 | blopes | 336 | |
| 773 | blopes | 337 | // Outside durante G2 |
| 771 | blopes | 338 | if (isOutside(candles, i)) { |
| 773 | blopes | 339 | lastIndex = idxGR; |
| 340 | log(String.format("GR[%d]: OUTSIDE em [%d] durante G2 (comprador).", idxGR, i)); |
||
| 341 | return new ResultadoPadrao(null, lastIndex); |
||
| 342 | } |
||
| 343 | |||
| 344 | // Regra global: romper topo do GR invalida |
||
| 345 | if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), gr.getMaxima())) { |
||
| 767 | blopes | 346 | lastIndex = i; |
| 773 | blopes | 347 | log(String.format("GR[%d]: candle[%d] rompeu topo do GR durante G2.", idxGR, i)); |
| 767 | blopes | 348 | return new ResultadoPadrao(null, lastIndex); |
| 349 | } |
||
| 350 | |||
| 771 | blopes | 351 | if (c.isCandleComprador()) { |
| 352 | ultimoComprador = c; |
||
| 353 | idxUltimoComprador = i; |
||
| 354 | lastIndex = i; |
||
| 355 | } else if (c.isCandleVendedor()) { |
||
| 356 | lastIndex = i - 1; |
||
| 764 | blopes | 357 | break; |
| 358 | } |
||
| 359 | } |
||
| 760 | blopes | 360 | |
| 773 | blopes | 361 | if (ultimoComprador == null) { |
| 362 | log(String.format("GR[%d], G1[%d]: não houve comprador válido para G2.", idxGR, idxG1)); |
||
| 363 | return new ResultadoPadrao(null, lastIndex); |
||
| 364 | } |
||
| 760 | blopes | 365 | |
| 771 | blopes | 366 | boolean fechamentoDentroRegiaoGR = |
| 367 | BigDecimalUtils.ehMaiorOuIgualQue(ultimoComprador.getFechamento(), gr.getMinima()) && |
||
| 368 | BigDecimalUtils.ehMenorOuIgualQue(ultimoComprador.getFechamento(), gr.getMaxima()); |
||
| 760 | blopes | 369 | |
| 771 | blopes | 370 | if (!fechamentoDentroRegiaoGR) { |
| 773 | blopes | 371 | log(String.format( |
| 372 | "GR[%d], G1[%d]: último comprador[%d] não fechou dentro da região do GR (fech=%s, faixa=[%s,%s]).", |
||
| 373 | idxGR, idxG1, idxUltimoComprador, |
||
| 374 | ultimoComprador.getFechamento().toPlainString(), |
||
| 375 | gr.getMinima().toPlainString(), |
||
| 376 | gr.getMaxima().toPlainString() |
||
| 377 | )); |
||
| 764 | blopes | 378 | return new ResultadoPadrao(null, lastIndex); |
| 379 | } |
||
| 760 | blopes | 380 | |
| 771 | blopes | 381 | g2 = ultimoComprador; |
| 382 | idxG2 = idxUltimoComprador; |
||
| 773 | blopes | 383 | log(String.format("GR[%d], G1[%d] => G2 (comprador) em [%d].", idxGR, idxG1, idxG2)); |
| 760 | blopes | 384 | |
| 773 | blopes | 385 | // -------------------- |
| 386 | // 3) Buscar G3 (vendedor rompe fundo de G2 com topo <= topo GR) |
||
| 387 | // -------------------- |
||
| 771 | blopes | 388 | Candle g3 = null; |
| 389 | int idxG3 = -1; |
||
| 390 | |||
| 764 | blopes | 391 | int idxPrimeiroVendedorAposG2 = -1; |
| 392 | for (int i = idxG2 + 1; i < n; i++) { |
||
| 393 | Candle c = candles.get(i); |
||
| 773 | blopes | 394 | if (isInside(candles, i) || !isDirecional(c)) |
| 764 | blopes | 395 | continue; |
| 760 | blopes | 396 | |
| 773 | blopes | 397 | if (isOutside(candles, i)) { |
| 398 | lastIndex = idxGR; |
||
| 399 | log(String.format("GR[%d]: OUTSIDE em [%d] antes da sequência vendedora de G3.", idxGR, i)); |
||
| 400 | return new ResultadoPadrao(null, lastIndex); |
||
| 401 | } |
||
| 402 | |||
| 403 | // Regra global: romper topo do GR invalida |
||
| 404 | if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), gr.getMaxima())) { |
||
| 405 | lastIndex = i; |
||
| 406 | log(String.format("GR[%d]: candle[%d] rompeu topo do GR antes da perna vendedora de G3.", idxGR, i)); |
||
| 407 | return new ResultadoPadrao(null, lastIndex); |
||
| 408 | } |
||
| 409 | |||
| 764 | blopes | 410 | if (c.isCandleVendedor()) { |
| 411 | idxPrimeiroVendedorAposG2 = i; |
||
| 412 | break; |
||
| 413 | } else { |
||
| 773 | blopes | 414 | // primeiro direcional após G2 é comprador → padrão só até G2 |
| 764 | blopes | 415 | lastIndex = i; |
| 773 | blopes | 416 | log(String.format("GR[%d], G1[%d], G2[%d]: primeiro direcional após G2 é comprador (idx %d).", |
| 417 | idxGR, idxG1, idxG2, i)); |
||
| 771 | blopes | 418 | return criarResultadoParcialComG2(gr, g1, g2, lastIndex); |
| 764 | blopes | 419 | } |
| 420 | } |
||
| 773 | blopes | 421 | |
| 422 | if (idxPrimeiroVendedorAposG2 == -1) { |
||
| 423 | log(String.format("GR[%d], G1[%d], G2[%d]: não houve vendedor após G2.", idxGR, idxG1, idxG2)); |
||
| 771 | blopes | 424 | return criarResultadoParcialComG2(gr, g1, g2, lastIndex); |
| 773 | blopes | 425 | } |
| 760 | blopes | 426 | |
| 764 | blopes | 427 | for (int i = idxPrimeiroVendedorAposG2; i < n; i++) { |
| 428 | Candle c = candles.get(i); |
||
| 773 | blopes | 429 | if (isInside(candles, i) || !isDirecional(c)) |
| 430 | continue; |
||
| 767 | blopes | 431 | |
| 771 | blopes | 432 | if (isOutside(candles, i)) { |
| 773 | blopes | 433 | lastIndex = idxGR; |
| 771 | blopes | 434 | log(String.format("GR[%d]: OUTSIDE em [%d] durante formação de G3 (vendedor).", idxGR, i)); |
| 767 | blopes | 435 | return new ResultadoPadrao(null, lastIndex); |
| 436 | } |
||
| 437 | |||
| 773 | blopes | 438 | // Regra global: romper topo do GR invalida |
| 771 | blopes | 439 | if (BigDecimalUtils.ehMaiorQue(c.getMaxima(), gr.getMaxima())) { |
| 440 | lastIndex = i; |
||
| 773 | blopes | 441 | log(String.format("GR[%d]: candle[%d] rompeu topo do GR durante busca de G3.", idxGR, i)); |
| 771 | blopes | 442 | return new ResultadoPadrao(null, lastIndex); |
| 443 | } |
||
| 444 | |||
| 764 | blopes | 445 | if (!c.isCandleVendedor()) { |
| 773 | blopes | 446 | // apareceu comprador antes de G3 → padrão só até G2 |
| 764 | blopes | 447 | lastIndex = i - 1; |
| 773 | blopes | 448 | log(String.format("GR[%d], G1[%d], G2[%d]: comprador em [%d] antes de G3.", idxGR, idxG1, idxG2, i)); |
| 449 | return criarResultadoParcialComG2(gr, g1, g2, lastIndex); |
||
| 764 | blopes | 450 | } |
| 760 | blopes | 451 | |
| 764 | blopes | 452 | lastIndex = i; |
| 453 | boolean rompeFundoG2 = BigDecimalUtils.ehMenorQue(c.getMinima(), g2.getMinima()); |
||
| 771 | blopes | 454 | boolean topoMenorOuIgualGR = BigDecimalUtils.ehMenorOuIgualQue(c.getMaxima(), gr.getMaxima()); |
| 455 | if (rompeFundoG2 && topoMenorOuIgualGR) { |
||
| 456 | g3 = c; |
||
| 764 | blopes | 457 | idxG3 = i; |
| 458 | break; |
||
| 459 | } |
||
| 460 | } |
||
| 760 | blopes | 461 | |
| 773 | blopes | 462 | if (g3 == null) { |
| 463 | log(String.format("GR[%d], G1[%d], G2[%d]: não houve G3, padrão parcial.", idxGR, idxG1, idxG2)); |
||
| 771 | blopes | 464 | return criarResultadoParcialComG2(gr, g1, g2, lastIndex); |
| 773 | blopes | 465 | } |
| 760 | blopes | 466 | |
| 773 | blopes | 467 | lastIndex = idxG3; |
| 468 | log(String.format("GR[%d], G1[%d], G2[%d] => G3 (vendedor) em [%d].", idxGR, idxG1, idxG2, idxG3)); |
||
| 760 | blopes | 469 | |
| 764 | blopes | 470 | PadraoGatilho padrao = new PadraoGatilho(); |
| 771 | blopes | 471 | padrao.setReferencia(gr); |
| 764 | blopes | 472 | padrao.setGatilho1(g1); |
| 473 | padrao.setGatilho2(g2); |
||
| 474 | padrao.setGatilho3(g3); |
||
| 773 | blopes | 475 | padrao.setGatilho4(null); |
| 760 | blopes | 476 | |
| 764 | blopes | 477 | return new ResultadoPadrao(padrao, lastIndex); |
| 478 | } |
||
| 760 | blopes | 479 | |
| 764 | blopes | 480 | // ===================================================================== |
| 773 | blopes | 481 | // CASO VENDEDOR – GR DINÂMICO PELA MÍNIMA |
| 764 | blopes | 482 | // ===================================================================== |
| 760 | blopes | 483 | |
| 773 | blopes | 484 | private ResultadoPadrao detectarPadraoVendedor(List<Candle> candles, int idxInicio) { |
| 764 | blopes | 485 | int n = candles.size(); |
| 773 | blopes | 486 | int lastIndex = idxInicio; |
| 760 | blopes | 487 | |
| 773 | blopes | 488 | Candle candidatoRef = candles.get(idxInicio); |
| 489 | if (!candidatoRef.isCandleVendedor()) { |
||
| 490 | return new ResultadoPadrao(null, idxInicio); |
||
| 764 | blopes | 491 | } |
| 760 | blopes | 492 | |
| 773 | blopes | 493 | int idxCandidatoRef = idxInicio; |
| 760 | blopes | 494 | |
| 773 | blopes | 495 | // -------------------- |
| 496 | // 1) Buscar G1 (comprador rompe topo do candidato) |
||
| 497 | // -------------------- |
||
| 771 | blopes | 498 | Candle g1 = null; |
| 764 | blopes | 499 | int idxG1 = -1; |
| 773 | blopes | 500 | boolean temGR = false; |
| 501 | Candle gr = null; |
||
| 502 | int idxGR = -1; |
||
| 503 | |||
| 504 | for (int i = idxInicio + 1; i < n; i++) { |
||
| 764 | blopes | 505 | Candle c = candles.get(i); |
| 773 | blopes | 506 | if (isInside(candles, i) || !isDirecional(c)) |
| 771 | blopes | 507 | continue; |
| 760 | blopes | 508 | |
| 773 | blopes | 509 | // Outside antes de G1 |
| 510 | if (isOutside(candles, i)) { |
||
| 511 | lastIndex = i; |
||
| 512 | log(String.format("Vendedor: OUTSIDE em [%d] antes de G1. Abortando padrão.", i)); |
||
| 764 | blopes | 513 | return new ResultadoPadrao(null, lastIndex); |
| 514 | } |
||
| 760 | blopes | 515 | |
| 773 | blopes | 516 | // G1: comprador rompe topo do candidato |
| 517 | if (c.isCandleComprador() |
||
| 518 | && BigDecimalUtils.ehMaiorQue(c.getMaxima(), candidatoRef.getMaxima())) { |
||
| 771 | blopes | 519 | g1 = c; |
| 764 | blopes | 520 | idxG1 = i; |
| 773 | blopes | 521 | gr = candidatoRef; |
| 522 | idxGR = idxCandidatoRef; |
||
| 523 | temGR = true; |
||
| 524 | lastIndex = i; |
||
| 764 | blopes | 525 | break; |
| 526 | } |
||
| 773 | blopes | 527 | |
| 528 | // GR DINÂMICO (venda): atualiza pelo FUNDO mais baixo antes de G1 |
||
| 529 | if (BigDecimalUtils.ehMenorQue(c.getMinima(), candidatoRef.getMinima())) { |
||
| 530 | candidatoRef = c; |
||
| 531 | idxCandidatoRef = i; |
||
| 532 | lastIndex = i; |
||
| 533 | continue; |
||
| 534 | } |
||
| 535 | |||
| 536 | lastIndex = i; |
||
| 764 | blopes | 537 | } |
| 760 | blopes | 538 | |
| 773 | blopes | 539 | if (g1 == null || !temGR) { |
| 540 | log(String.format("Vendedor: não foi possível formar G1 a partir de idx %d.", idxInicio)); |
||
| 541 | return new ResultadoPadrao(null, lastIndex); |
||
| 542 | } |
||
| 760 | blopes | 543 | |
| 773 | blopes | 544 | log(String.format("GR VENDEDOR em [%d], G1 (comprador) em [%d].", idxGR, idxG1)); |
| 545 | |||
| 546 | // -------------------- |
||
| 547 | // 2) Buscar G2 (tendência vendedora com fechamento dentro do GR) |
||
| 548 | // -------------------- |
||
| 771 | blopes | 549 | Candle g2 = null; |
| 550 | int idxG2 = -1; |
||
| 760 | blopes | 551 | |
| 764 | blopes | 552 | int idxPrimeiroVendedor = -1; |
| 553 | for (int i = idxG1 + 1; i < n; i++) { |
||
| 554 | Candle c = candles.get(i); |
||
| 773 | blopes | 555 | if (isInside(candles, i) || !isDirecional(c)) |
| 764 | blopes | 556 | continue; |
| 760 | blopes | 557 | |
| 773 | blopes | 558 | // Outside antes de G3 |
| 559 | if (isOutside(candles, i)) { |
||
| 560 | lastIndex = idxGR; |
||
| 561 | log(String.format("GR[%d]: OUTSIDE em [%d] antes da perna vendedora de G2.", idxGR, i)); |
||
| 562 | return new ResultadoPadrao(null, lastIndex); |
||
| 563 | } |
||
| 564 | |||
| 565 | // Regra global vendedor: romper fundo do GR invalida |
||
| 566 | if (BigDecimalUtils.ehMenorQue(c.getMinima(), gr.getMinima())) { |
||
| 567 | lastIndex = i; |
||
| 568 | log(String.format("GR[%d]: candle[%d] rompeu fundo do GR antes de G2.", idxGR, i)); |
||
| 569 | return new ResultadoPadrao(null, lastIndex); |
||
| 570 | } |
||
| 571 | |||
| 764 | blopes | 572 | if (c.isCandleVendedor()) { |
| 573 | idxPrimeiroVendedor = i; |
||
| 574 | break; |
||
| 575 | } else { |
||
| 576 | lastIndex = i; |
||
| 577 | } |
||
| 578 | } |
||
| 760 | blopes | 579 | |
| 773 | blopes | 580 | if (idxPrimeiroVendedor == -1) { |
| 581 | log(String.format("GR[%d], G1[%d]: não houve nova tendência vendedora para G2.", idxGR, idxG1)); |
||
| 582 | return new ResultadoPadrao(null, lastIndex); |
||
| 583 | } |
||
| 584 | |||
| 771 | blopes | 585 | Candle ultimoVendedor = null; |
| 586 | int idxUltimoVendedor = -1; |
||
| 760 | blopes | 587 | |
| 764 | blopes | 588 | for (int i = idxPrimeiroVendedor; i < n; i++) { |
| 589 | Candle c = candles.get(i); |
||
| 773 | blopes | 590 | if (isInside(candles, i) || !isDirecional(c)) |
| 591 | continue; |
||
| 760 | blopes | 592 | |
| 773 | blopes | 593 | // Outside durante G2 |
| 771 | blopes | 594 | if (isOutside(candles, i)) { |
| 773 | blopes | 595 | lastIndex = idxGR; |
| 596 | log(String.format("GR[%d]: OUTSIDE em [%d] durante G2 (vendedor).", idxGR, i)); |
||
| 597 | return new ResultadoPadrao(null, lastIndex); |
||
| 598 | } |
||
| 599 | |||
| 600 | // Regra global vendedor: romper fundo do GR invalida |
||
| 601 | if (BigDecimalUtils.ehMenorQue(c.getMinima(), gr.getMinima())) { |
||
| 767 | blopes | 602 | lastIndex = i; |
| 773 | blopes | 603 | log(String.format("GR[%d]: candle[%d] rompeu fundo do GR durante G2.", idxGR, i)); |
| 767 | blopes | 604 | return new ResultadoPadrao(null, lastIndex); |
| 605 | } |
||
| 606 | |||
| 771 | blopes | 607 | if (c.isCandleVendedor()) { |
| 608 | ultimoVendedor = c; |
||
| 609 | idxUltimoVendedor = i; |
||
| 610 | lastIndex = i; |
||
| 611 | } else if (c.isCandleComprador()) { |
||
| 612 | lastIndex = i - 1; |
||
| 764 | blopes | 613 | break; |
| 614 | } |
||
| 615 | } |
||
| 760 | blopes | 616 | |
| 773 | blopes | 617 | if (ultimoVendedor == null) { |
| 618 | log(String.format("GR[%d], G1[%d]: não houve vendedor válido para G2.", idxGR, idxG1)); |
||
| 764 | blopes | 619 | return new ResultadoPadrao(null, lastIndex); |
| 773 | blopes | 620 | } |
| 760 | blopes | 621 | |
| 771 | blopes | 622 | boolean fechamentoDentroRegiaoGR = |
| 623 | BigDecimalUtils.ehMaiorOuIgualQue(ultimoVendedor.getFechamento(), gr.getMinima()) && |
||
| 624 | BigDecimalUtils.ehMenorOuIgualQue(ultimoVendedor.getFechamento(), gr.getMaxima()); |
||
| 760 | blopes | 625 | |
| 773 | blopes | 626 | if (!fechamentoDentroRegiaoGR) { |
| 627 | log(String.format( |
||
| 628 | "GR[%d], G1[%d]: último vendedor[%d] não fechou dentro da região do GR (fech=%s, faixa=[%s,%s]).", |
||
| 629 | idxGR, idxG1, idxUltimoVendedor, |
||
| 630 | ultimoVendedor.getFechamento().toPlainString(), |
||
| 631 | gr.getMinima().toPlainString(), |
||
| 632 | gr.getMaxima().toPlainString() |
||
| 633 | )); |
||
| 764 | blopes | 634 | return new ResultadoPadrao(null, lastIndex); |
| 773 | blopes | 635 | } |
| 760 | blopes | 636 | |
| 771 | blopes | 637 | g2 = ultimoVendedor; |
| 638 | idxG2 = idxUltimoVendedor; |
||
| 773 | blopes | 639 | log(String.format("GR[%d], G1[%d] => G2 (vendedor) em [%d].", idxGR, idxG1, idxG2)); |
| 760 | blopes | 640 | |
| 773 | blopes | 641 | // -------------------- |
| 642 | // 3) Buscar G3 (comprador rompe topo de G2 com fundo >= fundo GR) |
||
| 643 | // -------------------- |
||
| 771 | blopes | 644 | Candle g3 = null; |
| 645 | int idxG3 = -1; |
||
| 646 | |||
| 764 | blopes | 647 | int idxPrimeiroCompradorAposG2 = -1; |
| 648 | for (int i = idxG2 + 1; i < n; i++) { |
||
| 649 | Candle c = candles.get(i); |
||
| 773 | blopes | 650 | if (isInside(candles, i) || !isDirecional(c)) |
| 764 | blopes | 651 | continue; |
| 760 | blopes | 652 | |
| 773 | blopes | 653 | if (isOutside(candles, i)) { |
| 654 | lastIndex = idxGR; |
||
| 655 | log(String.format("GR[%d]: OUTSIDE em [%d] antes da sequência compradora de G3.", idxGR, i)); |
||
| 656 | return new ResultadoPadrao(null, lastIndex); |
||
| 657 | } |
||
| 658 | |||
| 659 | // Regra global vendedor: romper fundo do GR antes de G3 invalida |
||
| 660 | if (BigDecimalUtils.ehMenorQue(c.getMinima(), gr.getMinima())) { |
||
| 661 | lastIndex = i; |
||
| 662 | log(String.format("GR[%d]: candle[%d] rompeu fundo do GR antes de G3.", idxGR, i)); |
||
| 663 | return new ResultadoPadrao(null, lastIndex); |
||
| 664 | } |
||
| 665 | |||
| 764 | blopes | 666 | if (c.isCandleComprador()) { |
| 667 | idxPrimeiroCompradorAposG2 = i; |
||
| 668 | break; |
||
| 669 | } else { |
||
| 773 | blopes | 670 | // primeiro direcional não é comprador → padrão até G2 |
| 764 | blopes | 671 | lastIndex = i; |
| 773 | blopes | 672 | log(String.format("GR[%d], G1[%d], G2[%d]: primeiro direcional após G2 não é comprador (idx %d).", |
| 673 | idxGR, idxG1, idxG2, i)); |
||
| 771 | blopes | 674 | return criarResultadoParcialComG2(gr, g1, g2, lastIndex); |
| 764 | blopes | 675 | } |
| 676 | } |
||
| 773 | blopes | 677 | |
| 678 | if (idxPrimeiroCompradorAposG2 == -1) { |
||
| 679 | log(String.format("GR[%d], G1[%d], G2[%d]: não houve comprador após G2.", idxGR, idxG1, idxG2)); |
||
| 771 | blopes | 680 | return criarResultadoParcialComG2(gr, g1, g2, lastIndex); |
| 773 | blopes | 681 | } |
| 760 | blopes | 682 | |
| 764 | blopes | 683 | for (int i = idxPrimeiroCompradorAposG2; i < n; i++) { |
| 684 | Candle c = candles.get(i); |
||
| 773 | blopes | 685 | if (isInside(candles, i) || !isDirecional(c)) |
| 686 | continue; |
||
| 767 | blopes | 687 | |
| 771 | blopes | 688 | if (isOutside(candles, i)) { |
| 773 | blopes | 689 | lastIndex = idxGR; |
| 771 | blopes | 690 | log(String.format("GR[%d]: OUTSIDE em [%d] durante formação de G3 (comprador).", idxGR, i)); |
| 767 | blopes | 691 | return new ResultadoPadrao(null, lastIndex); |
| 692 | } |
||
| 693 | |||
| 773 | blopes | 694 | // Regra global vendedor: romper fundo do GR invalida |
| 771 | blopes | 695 | if (BigDecimalUtils.ehMenorQue(c.getMinima(), gr.getMinima())) { |
| 696 | lastIndex = i; |
||
| 773 | blopes | 697 | log(String.format("GR[%d]: candle[%d] rompeu fundo do GR durante busca de G3.", idxGR, i)); |
| 771 | blopes | 698 | return new ResultadoPadrao(null, lastIndex); |
| 699 | } |
||
| 700 | |||
| 764 | blopes | 701 | if (!c.isCandleComprador()) { |
| 773 | blopes | 702 | // apareceu vendedor antes de G3 → padrão só até G2 |
| 764 | blopes | 703 | lastIndex = i - 1; |
| 773 | blopes | 704 | log(String.format("GR[%d], G1[%d], G2[%d]: vendedor em [%d] antes de G3.", idxGR, idxG1, idxG2, i)); |
| 705 | return criarResultadoParcialComG2(gr, g1, g2, lastIndex); |
||
| 764 | blopes | 706 | } |
| 760 | blopes | 707 | |
| 764 | blopes | 708 | lastIndex = i; |
| 709 | boolean rompeTopoG2 = BigDecimalUtils.ehMaiorQue(c.getMaxima(), g2.getMaxima()); |
||
| 771 | blopes | 710 | boolean fundoMaiorOuIgualGR = BigDecimalUtils.ehMaiorOuIgualQue(c.getMinima(), gr.getMinima()); |
| 711 | if (rompeTopoG2 && fundoMaiorOuIgualGR) { |
||
| 712 | g3 = c; |
||
| 764 | blopes | 713 | idxG3 = i; |
| 714 | break; |
||
| 715 | } |
||
| 716 | } |
||
| 760 | blopes | 717 | |
| 773 | blopes | 718 | if (g3 == null) { |
| 719 | log(String.format("GR[%d], G1[%d], G2[%d]: não houve G3, padrão parcial.", idxGR, idxG1, idxG2)); |
||
| 771 | blopes | 720 | return criarResultadoParcialComG2(gr, g1, g2, lastIndex); |
| 773 | blopes | 721 | } |
| 760 | blopes | 722 | |
| 773 | blopes | 723 | lastIndex = idxG3; |
| 724 | log(String.format("GR[%d], G1[%d], G2[%d] => G3 (comprador) em [%d].", idxGR, idxG1, idxG2, idxG3)); |
||
| 760 | blopes | 725 | |
| 764 | blopes | 726 | PadraoGatilho padrao = new PadraoGatilho(); |
| 771 | blopes | 727 | padrao.setReferencia(gr); |
| 764 | blopes | 728 | padrao.setGatilho1(g1); |
| 729 | padrao.setGatilho2(g2); |
||
| 730 | padrao.setGatilho3(g3); |
||
| 773 | blopes | 731 | padrao.setGatilho4(null); |
| 760 | blopes | 732 | |
| 764 | blopes | 733 | return new ResultadoPadrao(padrao, lastIndex); |
| 734 | } |
||
| 760 | blopes | 735 | |
| 764 | blopes | 736 | // ===================================================================== |
| 771 | blopes | 737 | // MODO TEMPO REAL |
| 764 | blopes | 738 | // ===================================================================== |
| 760 | blopes | 739 | |
| 764 | blopes | 740 | public void resetTempoReal() { |
| 741 | this.idxProximaAnaliseTempoReal = 0; |
||
| 742 | } |
||
| 760 | blopes | 743 | |
| 764 | blopes | 744 | public PadraoGatilho processarCandleTempoReal(List<Candle> candles) { |
| 745 | int n = candles.size(); |
||
| 771 | blopes | 746 | if (n < 4) return null; |
| 760 | blopes | 747 | |
| 764 | blopes | 748 | while (idxProximaAnaliseTempoReal < n - 3) { |
| 749 | ResultadoPadrao resultado = detectarPadraoAPartir(candles, idxProximaAnaliseTempoReal); |
||
| 760 | blopes | 750 | |
| 764 | blopes | 751 | if (resultado == null) { |
| 752 | idxProximaAnaliseTempoReal++; |
||
| 753 | continue; |
||
| 754 | } |
||
| 760 | blopes | 755 | |
| 764 | blopes | 756 | int proximoInicio = resultado.lastIndex + 1; |
| 757 | if (proximoInicio <= idxProximaAnaliseTempoReal) { |
||
| 758 | proximoInicio = idxProximaAnaliseTempoReal + 1; |
||
| 759 | } |
||
| 760 | idxProximaAnaliseTempoReal = proximoInicio; |
||
| 760 | blopes | 761 | |
| 764 | blopes | 762 | if (resultado.padrao != null) { |
| 763 | return resultado.padrao; |
||
| 764 | } |
||
| 765 | } |
||
| 760 | blopes | 766 | |
| 764 | blopes | 767 | return null; |
| 768 | } |
||
| 760 | blopes | 769 | |
| 764 | blopes | 770 | } |