EvaluationService.java
package com.tdmconsult.ete.evaluation;
import com.tdmconsult.ete.evaluation.commands.BuyCommand;
import com.tdmconsult.ete.evaluation.commands.EoyEvaluationCommand;
import com.tdmconsult.ete.evaluation.commands.SellCommand;
import com.tdmconsult.ete.masterdata.EvaluationRate;
import com.tdmconsult.ete.masterdata.Security;
import com.tdmconsult.ete.transactions.TradeTransaction;
import com.tdmconsult.ete.transactions.TradeTransactionRepository;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
@Service
@RequiredArgsConstructor
@Slf4j
public class EvaluationService {
private final TradeTransactionRepository tradeTransactionRepository;
private final EvaluationRateService evaluationRateService;
private final EvaluationStateMapperService evaluationStateMapperService;
@Data
@Accessors(chain = true)
public static class EvaluationContext {
private int fiscalYear;
private List<EvaluationRate> evaluationRates = new ArrayList<>();
}
public List<EvaluationStateDto> getEvaluationStates(final int fiscalYear) {
final var evaluations = evaluate(fiscalYear);
return evaluations.stream().map(evaluationStateMapperService::toDto).toList();
}
public EvaluationStateDto getEvaluationState(final Security security, final int fiscalYear) {
final var evaluation = evaluate(security, fiscalYear);
return evaluationStateMapperService.toDto(evaluation);
}
public EvaluationState evaluate(final Security security, final int fiscalYear) {
final var transactions = tradeTransactionRepository.findBySecurityIsin(security.getIsin());
return evaluate(security, transactions, fiscalYear);
}
public List<EvaluationState> evaluate(final int fiscalYear) {
final var context = new EvaluationContext()
.setFiscalYear(fiscalYear)
.setEvaluationRates(evaluationRateService.getMostRecent());
final var base = tradeTransactionRepository
.findAll().stream()
.map(EvaluableTransaction::of)
.collect(Collectors.groupingBy(EvaluableTransaction::getSecurity));
final var answer = base.entrySet().stream().map(e -> {
final var state = new EvaluationState(e.getKey(), fiscalYear);
final var transactionCommandQueue = createTransactionCommandQueue(context, e.getKey(), e.getValue());
transactionCommandQueue.execute(state);
return state;
}).toList();
return answer;
}
public EvaluationState evaluate(
final Security security,
final Collection<TradeTransaction> transactions,
final int fiscalYear) {
final var evaluableTransactions = transactions.stream()
.map(EvaluableTransaction::of)
.peek(t -> Assert.isTrue(t.getSecurity().getIsin().equals(security.getIsin()), "ISIN mismatch"))
.toList();
final var state = new EvaluationState(security, fiscalYear);
final var context = new EvaluationContext()
.setFiscalYear(fiscalYear)
.setEvaluationRates(evaluationRateService.getMostRecent(security));
final var transactionCommandQueue = createTransactionCommandQueue(context, security, evaluableTransactions);
transactionCommandQueue.execute(state);
return state;
}
private TransactionCommandQueue createTransactionCommandQueue(
final EvaluationContext evaluationContext,
final Security security,
final Collection<? extends EvaluableTransaction> transactions) {
final var transactionCommandQueue = new TransactionCommandQueue();
// group transaction by fiscal year
final var transactionsByFiscalYear = transactions.stream()
.filter(transaction -> transaction.getTradingDate().getYear() <= evaluationContext.getFiscalYear())
.collect(Collectors.groupingBy(x -> x.getTradingDate().getYear()));
final var startYear = transactionsByFiscalYear.keySet().stream()
.min(Integer::compareTo)
.orElse(evaluationContext.getFiscalYear());
final var currentRate = new AtomicReference<EvaluationPosition.Rate>();
for (int fy = startYear; fy <= evaluationContext.getFiscalYear(); fy++) {
final var currentFiscalYear = fy;
// 1. add transaction itself
final var currentTransactions = transactionsByFiscalYear.get(currentFiscalYear);
if (currentTransactions != null) {
addBuyAndSellToQueue(transactionCommandQueue, currentTransactions);
}
// 2. add EOY evaluation records for that fiscal year
final var rateTableEntry = evaluationContext.getEvaluationRates().stream()
.filter(r -> r.getFiscalYear() == currentFiscalYear)
.filter(r -> r.getSecurity().equals(security))
.findFirst()
.orElse(null);
if (rateTableEntry != null) {
currentRate.set(EvaluationPosition.Rate.of(rateTableEntry));
transactionCommandQueue.add(
new EoyEvaluationCommand(
currentFiscalYear,
security,
currentRate.get()));
} else {
if (currentRate.get() != null) {
// FIXME: maybe its better to take last deal price - because why should we make an
// evaluation with an outdated rate???
transactionCommandQueue.add(
new EoyEvaluationCommand(
currentFiscalYear,
security,
currentRate.get().makeEoyDeprecated()));
} else {
// take rate of the last transaction up to that FY
final var rate = currentTransactions != null ? currentTransactions.stream()
.max(Comparator.comparing(EvaluableTransaction::getTradingDate))
.map(EvaluationPosition.Rate::deal)
.orElse(null) : null;
if (rate != null) {
currentRate.set(rate);
transactionCommandQueue.add(
new EoyEvaluationCommand(
currentFiscalYear,
security,
currentRate.get()));
}
}
}
}
return transactionCommandQueue;
}
private static void addBuyAndSellToQueue(
final TransactionCommandQueue transactionCommandQueue,
final List<? extends EvaluableTransaction> transactionsOfOneFiscalYear) {
// create simple transaction commands ordered by trading date
transactionsOfOneFiscalYear.stream()
.sorted(Comparator.comparing(EvaluableTransaction::getTradingDate))
.forEach(transaction -> {
if (transaction.isLikeBuy()) {
transactionCommandQueue.add(new BuyCommand(transaction));
} else if (transaction.isSell()) {
transactionCommandQueue.add(new SellCommand(transaction));
}
});
}
}