package net.objectlab.kit.pf.ucits;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.objectlab.kit.pf.AssetDetailsProvider;
import net.objectlab.kit.pf.AssetEligibilityProvider;
import net.objectlab.kit.pf.ExistingPortfolio;
import net.objectlab.kit.pf.RuleNames;
import net.objectlab.kit.pf.Severity;
import net.objectlab.kit.pf.ValidationEngine;
import net.objectlab.kit.pf.ValidationResults;
import net.objectlab.kit.pf.validator.Results;
import net.objectlab.kit.pf.validator.ValidatedPortfolioLineImpl;
import net.objectlab.kit.util.BigDecimalUtil;
import net.objectlab.kit.util.Total;
/**
* Default is the usual 5-10-40 rule.
* http://www.alfi.lu/investor-centre/investor-protection/how-ucits-funds-protect-investors#Diversification
*/
public class BasicUcitsConcentrationValidator implements ValidationEngine {
private final BigDecimal maxConcentrationPerIssuer;
private final BigDecimal mediumConcentrationPerIssuer;
private final BigDecimal maxForMediumConcentration;
private final AssetDetailsProvider assetDetailsProvider;
private final AssetEligibilityProvider assetEligibilityProvider;
private final UcitsLimitProvider ucitsLimitProvider;
public static class Builder {
private BigDecimal maxConcentrationPerIssuer = new BigDecimal("0.1");
private BigDecimal mediumConcentrationPerIssuer = new BigDecimal("0.05");
private BigDecimal maxForMediumConcentration = new BigDecimal("0.4");
private AssetDetailsProvider assetDetailsProvider;
private AssetEligibilityProvider assetEligibilityProvider;
private UcitsLimitProvider ucitsLimitProvider;
public Builder maxConcentrationPerIssuer(final BigDecimal maxConcentrationPerIssuer) {
this.maxConcentrationPerIssuer = maxConcentrationPerIssuer;
return this;
}
public Builder mediumConcentrationPerIssuer(final BigDecimal mediumConcentrationPerIssuer) {
this.mediumConcentrationPerIssuer = mediumConcentrationPerIssuer;
return this;
}
public Builder maxForMediumConcentration(final BigDecimal maxForMediumConcentration) {
this.maxForMediumConcentration = maxForMediumConcentration;
return this;
}
public Builder assetDetailsProvider(final AssetDetailsProvider assetDetailsProvider) {
this.assetDetailsProvider = assetDetailsProvider;
return this;
}
public Builder assetEligibilityProvider(final AssetEligibilityProvider assetEligibilityProvider) {
this.assetEligibilityProvider = assetEligibilityProvider;
return this;
}
public Builder ucitsLimitProvider(final UcitsLimitProvider ucitsLimitProvider) {
this.ucitsLimitProvider = ucitsLimitProvider;
return this;
}
}
public BasicUcitsConcentrationValidator(final Builder builder) {
this.maxConcentrationPerIssuer = builder.maxConcentrationPerIssuer;
this.maxForMediumConcentration = builder.maxForMediumConcentration;
this.mediumConcentrationPerIssuer = builder.mediumConcentrationPerIssuer;
this.assetDetailsProvider = builder.assetDetailsProvider;
this.assetEligibilityProvider = builder.assetEligibilityProvider;
this.ucitsLimitProvider = builder.ucitsLimitProvider;
}
private static final class TotalPerIssuer {
private final String issuer;
private final Total total = new Total();
private final List<ValidatedPortfolioLineImpl> lines = new ArrayList<>();
public TotalPerIssuer(String issuer) {
this.issuer = issuer;
}
public String getIssuer() {
return issuer;
}
public void add(final ValidatedPortfolioLineImpl l) {
lines.add(l);
total.add(l.getAllocationWeight());
}
public BigDecimal getTotalWeight() {
return total.getTotal();
}
public List<ValidatedPortfolioLineImpl> getLines() {
return lines;
}
}
@Override
public ValidationResults validate(final ExistingPortfolio portfolio) {
// for each line:
final Results results = new Results(portfolio);
// get the value of the portfolio
final BigDecimal porfolioValue = portfolio.getPortfolioValue();
// calculate the value per Issuer (using AssetDetails)
// check if asset is eligible, if not -> Breach
final Map<String, TotalPerIssuer> totalPerIssuer = new HashMap<>();
results.getLines().forEach(l -> {
if (!assetEligibilityProvider.isEligible(l.getAssetCode())) {
l.addIssue(Severity.MANDATORY, RuleNames.ELIGIBILITY, "Asset not eligible.");
}
l.setAllocationWeight(BigDecimalUtil.divide(8, l.getValueInPortfolioCcy(), porfolioValue, BigDecimal.ROUND_HALF_UP));
// calculate the weight for each issuer
totalPerIssuer.computeIfAbsent(assetDetailsProvider.getDetails(l.getAssetCode()).getUltimateIssuerCode(), k -> new TotalPerIssuer(k))
.add(l);
});
final Total totalMediumConcentration = new Total();
final List<ValidatedPortfolioLineImpl> mediumLines = new ArrayList<>();
totalPerIssuer.values().forEach(issuer -> {
final BigDecimal totalWeight = issuer.getTotalWeight();
if (BigDecimalUtil.compareTo(totalWeight, maxConcentrationPerIssuer) > 0) {
// if weight > maxConcentrationPerIssuer (e.g. 10%) -> Breach
issuer.lines.forEach(line -> line.addIssue(Severity.MANDATORY, RuleNames.ISSUER_MAX_CONCENTRATION,
"Concentration above " + BigDecimalUtil.movePoint(maxConcentrationPerIssuer, 2) + "% for " + issuer.getIssuer() + " ["
+ BigDecimalUtil.movePoint(totalWeight, 2) + "]"));
} else if (BigDecimalUtil.compareTo(totalWeight, mediumConcentrationPerIssuer) > 0) {
// if weight > mediumConcentrationPerIssuer (e.g. 5%) -> sum them
totalMediumConcentration.add(totalWeight);
mediumLines.addAll(issuer.getLines());
}
});
// if sum of those > maxForMediumConcentration -> Breach
if (BigDecimalUtil.compareTo(totalMediumConcentration.getTotal(), maxForMediumConcentration) > 0) {
mediumLines.forEach(line -> line.addIssue(
Severity.MANDATORY,
RuleNames.ISSUER_MEDIUM_CONCENTRATION,
"Total medium concentration is above " + BigDecimalUtil.movePoint(maxForMediumConcentration, 2) + "% ["
+ BigDecimalUtil.movePoint(totalMediumConcentration.getTotal(), 2) + "]"));
}
return results;
}
}