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