package br.com.kronus.core;
import br.com.kronus.core.Candle;
import br.com.kronus.core.SinalDeTrade;
import br.com.kronus.strategy.Strategy;
import java.util.ArrayList;
import java.util.List;
public class BacktestEngine
{
/**
* Roda o backtest: gera sinais com a estratégia e simula trade por trade.
*/
public BacktestSummary runBacktest
(List<Candle
> candles, Strategy strategy
) {
BacktestSummary summary =
new BacktestSummary
();
if (candles ==
null || candles.
isEmpty()) {
return summary
;
}
List<SinalDeTrade
> sinais = strategy.
gerarSinais(candles
);
for (SinalDeTrade sinal : sinais
) {
BacktestTradeResult result = simularTrade
(candles, sinal
);
summary.
addTrade(result
);
}
summary.
calcularResumo();
return summary
;
}
/**
* Simula um trade a partir de um sinal de compra/venda.
* Aqui implementado para COMPRA; para VENDA é só adaptar as condições (mirror).
*/
private BacktestTradeResult simularTrade
(List<Candle
> candles, SinalDeTrade sinal
) {
BacktestTradeResult result =
new BacktestTradeResult
(sinal
);
// Só implementei compra aqui – se precisar de venda também, dá pra expandir
if (sinal.
getDirecao() == SinalDeTrade.
Direcao.
COMPRA) {
simularCompra
(candles, sinal, result
);
} else {
// TODO: implementar lógica espelhada para venda, se você usar
result.
setExitReason(BacktestTradeResult.
ExitReason.
NAO_EXECUTADO);
}
return result
;
}
private void simularCompra
(List<Candle
> candles, SinalDeTrade sinal, BacktestTradeResult result
) {
double precoEntrada = sinal.
getPrecoEntrada();
double stop = sinal.
getStopLoss();
double alvo = sinal.
getAlvo();
// 1) Descobrir a partir de qual índice de candle começar (>= horário do sinal)
int startIdx = encontrarIndiceInicial
(candles, sinal
);
if (startIdx == -
1) {
// Não há candles após o sinal
result.
setExitReason(BacktestTradeResult.
ExitReason.
NAO_EXECUTADO);
return;
}
// 2) Procurar candle onde a entrada é acionada (touch na faixa [min, max])
int idxEntrada = -
1;
for (int i = startIdx
; i
< candles.
size(); i++
) {
Candle c = candles.
get(i
);
if (c.
getMinima() <= precoEntrada
&& c.
getMaxima() >= precoEntrada
) {
idxEntrada = i
;
break;
}
}
if (idxEntrada == -
1) {
// Preço nunca tocou na entrada
result.
setExitReason(BacktestTradeResult.
ExitReason.
NAO_EXECUTADO);
return;
}
// Marca entrada
Candle candleEntrada = candles.
get(idxEntrada
);
result.
setExecutado(true);
result.
setEntryPrice(precoEntrada
);
result.
setEntryCandle(candleEntrada
);
// 3) A partir do candle de entrada, checar stop/alvo
for (int i = idxEntrada
; i
< candles.
size(); i++
) {
Candle c = candles.
get(i
);
boolean tocouStop = c.
getMinima() <= stop
;
boolean tocouAlvo = c.
getMaxima() >= alvo
;
if (tocouStop
&& tocouAlvo
) {
// Mesma barra: por conservadorismo, considerar que o STOP veio primeiro
result.
setExitPrice(stop
);
result.
setPnl(stop - precoEntrada
);
result.
setExitCandle(c
);
result.
setExitReason(BacktestTradeResult.
ExitReason.
STOP);
return;
} else if (tocouStop
) {
result.
setExitPrice(stop
);
result.
setPnl(stop - precoEntrada
);
result.
setExitCandle(c
);
result.
setExitReason(BacktestTradeResult.
ExitReason.
STOP);
return;
} else if (tocouAlvo
) {
result.
setExitPrice(alvo
);
result.
setPnl(alvo - precoEntrada
);
result.
setExitCandle(c
);
result.
setExitReason(BacktestTradeResult.
ExitReason.
ALVO);
return;
}
}
// 4) Se chegou aqui, nem stop nem alvo foram atingidos: encerra no último candle (expirado)
Candle ultimo = candles.
get(candles.
size() -
1);
double exitPrice = ultimo.
getFechamento();
result.
setExitPrice(exitPrice
);
result.
setPnl(exitPrice - precoEntrada
);
result.
setExitCandle(ultimo
);
result.
setExitReason(BacktestTradeResult.
ExitReason.
EXPIRADO);
}
/**
* Procura primeiro candle com horário >= horário do sinal.
*/
private int encontrarIndiceInicial
(List<Candle
> candles, SinalDeTrade sinal
) {
for (int i =
0; i
< candles.
size(); i++
) {
if (!candles.
get(i
).
getTime().
isBefore(sinal.
getTime())) {
return i
;
}
}
return -
1;
}
}