com.github.triceo.robozonky.strategy.natural.NaturalLanguagePurchaseStrategy Maven / Gradle / Ivy
/*
* Copyright 2017 Lukáš Petrovický
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.github.triceo.robozonky.strategy.natural;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.github.triceo.robozonky.api.remote.entities.Participation;
import com.github.triceo.robozonky.api.remote.enums.Rating;
import com.github.triceo.robozonky.api.strategies.ParticipationDescriptor;
import com.github.triceo.robozonky.api.strategies.PortfolioOverview;
import com.github.triceo.robozonky.api.strategies.PurchaseStrategy;
import com.github.triceo.robozonky.api.strategies.RecommendedParticipation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class NaturalLanguagePurchaseStrategy implements PurchaseStrategy {
private static final Logger LOGGER = LoggerFactory.getLogger(NaturalLanguagePurchaseStrategy.class);
private static final Comparator
BY_TERM = Comparator.comparingInt(p -> p.item().getRemainingInstalmentCount()),
BY_RECENCY = Comparator.comparing(p -> p.item().getDeadline()),
BY_REMAINING =
Comparator.comparing((ParticipationDescriptor p) -> p.item().getRemainingPrincipal()).reversed();
private static Map> sortByRating(
final Stream items) {
return Collections.unmodifiableMap(items.distinct().collect(Collectors.groupingBy(l -> l.item().getRating())));
}
/**
* Pick a loan ordering such that it maximizes the chances the loan is still available on the marketplace when the
* investment operation is triggered.
* @return Comparator to order the marketplace with.
*/
private static Comparator getLoanComparator() {
return BY_TERM.thenComparing(BY_REMAINING).thenComparing(BY_RECENCY);
}
private final ParsedStrategy strategy;
public NaturalLanguagePurchaseStrategy(final ParsedStrategy p) {
this.strategy = p;
}
private int[] getRecommendationBoundaries(final Participation participation) {
final Rating rating = participation.getRating();
final int minimumInvestment = strategy.getMinimumInvestmentSizeInCzk(rating);
final int maximumInvestment = strategy.getMaximumInvestmentSizeInCzk(rating);
return new int[]{minimumInvestment, maximumInvestment};
}
boolean sizeMatchesStrategy(final Participation participation, final int balance) {
final int id = participation.getLoanId();
return recommendInvestmentAmount(participation).map(recommended -> {
final int minimumRecommendation = recommended[0];
final int maximumRecommendation = recommended[1];
LOGGER.trace("Recommended investment range for loan #{} is <{}; {}> CZK.", id, minimumRecommendation,
maximumRecommendation);
// round to nearest lower increment
final double price = participation.getRemainingPrincipal().doubleValue();
if (balance < price) {
LOGGER.debug("Loan #{} not recommended due to price over balance.", id);
return false;
} else if (minimumRecommendation > price) {
LOGGER.debug("Loan #{} not recommended due to price below minimum.", id);
return false;
} else if (price > maximumRecommendation) {
LOGGER.debug("Loan #{} not recommended due to price over maximum.", id);
return false;
} else {
LOGGER.debug("Final recommendation for loan #{} is to buy.", id);
return true;
}
}).orElse(false); // not recommended
}
private Optional recommendInvestmentAmount(final Participation item) {
final int[] recommended = getRecommendationBoundaries(item);
final int minimumRecommendation = recommended[0];
final int maximumRecommendation = recommended[1];
final int loanId = item.getLoanId();
LOGGER.trace("Strategy gives investment range for loan #{} of <{}; {}> CZK.", loanId, minimumRecommendation,
maximumRecommendation);
final int minimumInvestment = strategy.getMinimumInvestmentSizeInCzk(item.getRating());
final int maximumInvestment = strategy.getMaximumInvestmentSizeInCzk(item.getRating());
if (maximumInvestment < minimumInvestment) {
LOGGER.trace("Loan #{} skipped; {} CZK > {} CZK.", loanId, minimumInvestment, maximumInvestment);
return Optional.empty();
}
return Optional.of(new int[]{minimumInvestment, maximumInvestment});
}
@Override
public Stream recommend(final Collection available,
final PortfolioOverview portfolio) {
if (!Util.isAcceptable(strategy, portfolio)) {
LOGGER.debug("Not recommending anything due to unacceptable portfolio.");
return Stream.empty();
}
// split available marketplace into buckets per rating
final Map> splitByRating =
sortByRating(strategy.getApplicableParticipations(available));
// prepare map of ratings and their shares
final Map relevantPortfolio = splitByRating.keySet().stream()
.collect(Collectors.toMap(Function.identity(), portfolio::getShareOnInvestment));
// recommend amount to invest per strategy
return Util.rankRatingsByDemand(strategy, relevantPortfolio)
.flatMap(rating -> { // prioritize marketplace by their ranking's demand
return splitByRating.get(rating).stream().sorted(getLoanComparator());
})
.peek(d -> LOGGER.trace("Evaluating {}.", d.item()))
.filter(d -> sizeMatchesStrategy(d.item(), portfolio.getCzkAvailable()))
.map(ParticipationDescriptor::recommend) // must do full amount; Zonky enforces
.flatMap(r -> r.map(Stream::of).orElse(Stream.empty()));
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy