protected static PortfolioOverview mockPortfolioOverview(final int balance) { final PortfolioOverview po = mock(PortfolioOverview.class); when(po.getCzkAvailable()).thenReturn(BigDecimal.valueOf(balance)); when(po.getCzkInvested()).thenReturn(BigDecimal.ZERO); when(po.getCzkInvested(any())).thenReturn(BigDecimal.ZERO); when(po.getCzkAtRisk()).thenReturn(BigDecimal.ZERO); when(po.getCzkAtRisk(any())).thenReturn(BigDecimal.ZERO); when(po.getShareAtRisk()).thenReturn(BigDecimal.ZERO); when(po.getShareOnInvestment(any())).thenReturn(BigDecimal.ZERO); when(po.getAtRiskShareOnInvestment(any())).thenReturn(BigDecimal.ZERO); when(po.getTimestamp()).thenReturn(ZonedDateTime.now()); return po; }
static boolean isAcceptable(final ParsedStrategy strategy, final PortfolioOverview portfolio) { final long balance = portfolio.getCzkAvailable().longValue(); if (balance < strategy.getMinimumBalance()) { Decisions.report(logger -> logger.debug("Not recommending any loans due to balance under minimum.")); return false; } final long invested = portfolio.getCzkInvested().longValue(); final long investmentCeiling = strategy.getMaximumInvestmentSizeInCzk(); if (invested >= investmentCeiling) { Decisions.report(logger -> logger.debug("Not recommending any loans due to reaching the ceiling.")); return false; } return true; } }
public static Map<String, Object> summarizePortfolioStructure(final PortfolioOverview portfolioOverview) { return Maps.ofEntries( entry("absoluteShare", perRating(portfolioOverview::getCzkInvested)), entry("relativeShare", perRating(portfolioOverview::getShareOnInvestment)), entry("absoluteRisk", perRating(portfolioOverview::getCzkAtRisk)), entry("relativeRisk", perRating(portfolioOverview::getAtRiskShareOnInvestment)), entry("total", portfolioOverview.getCzkInvested()), entry("totalRisk", portfolioOverview.getCzkAtRisk()), entry("totalShare", portfolioOverview.getShareAtRisk()), entry("balance", portfolioOverview.getCzkAvailable()), entry("timestamp", toDate(portfolioOverview.getTimestamp())) ); }
@Override boolean shouldNotify(final ExecutionStartedEvent event, final SessionInfo sessionInfo) { final Optional<BigDecimal> lastKnownBalance = balanceTracker.getLastKnownBalance(sessionInfo); final BigDecimal newBalance = event.getPortfolioOverview().getCzkAvailable(); final BigDecimal expectedBalance = BigDecimal.valueOf(targetBalance); logger.debug("Last known balance: {}, target: {}, new: {}.", lastKnownBalance, expectedBalance, newBalance); final boolean balanceNowExceeded = newBalance.compareTo(expectedBalance) > 0; final boolean wasFineLastTime = !lastKnownBalance.isPresent() || lastKnownBalance.get().compareTo(expectedBalance) < 0; return (balanceNowExceeded && wasFineLastTime); } }
@Override protected Map<String, Object> getData(final LoanRepaidEvent event) { final Map<String, Object> result = super.getData(event); final int invested = event.getPortfolioOverview().getCzkInvested().intValue(); result.put("yield", FinancialCalculator.actualInterestAfterFees(event.getInvestment(), invested)); return result; } }
static Stream<Rating> rankRatingsByDemand(final ParsedStrategy strategy, final Collection<Rating> ratings, final PortfolioOverview portfolio) { final SortedMap<BigDecimal, EnumSet<Rating>> mostWantedRatings = new TreeMap<>(Comparator.reverseOrder()); // put the ratings into buckets based on how much we're missing them ratings.forEach(r -> { final BigDecimal currentRatingShare = portfolio.getShareOnInvestment(r); final BigDecimal maximumAllowedShare = divide(strategy.getMaximumShare(r), ONE_HUNDRED); final BigDecimal undershare = maximumAllowedShare.subtract(currentRatingShare); if (undershare.signum() < 1) { // we over-invested into this rating; do not include final BigDecimal pp = undershare.multiply(ONE_HUNDRED).negate(); Decisions.report(logger -> logger.debug("Rating {} over-invested by {} percentage points.", r, pp)); return; } // rank the rating mostWantedRatings.computeIfAbsent(undershare, k -> EnumSet.noneOf(Rating.class)); mostWantedRatings.get(undershare).add(r); // inform that the rating is under-invested final BigDecimal minimumNeededShare = divide(strategy.getMinimumShare(r), ONE_HUNDRED); if (currentRatingShare.compareTo(minimumNeededShare) < 0) { final BigDecimal pp = minimumNeededShare.subtract(currentRatingShare).multiply(ONE_HUNDRED); Decisions.report(logger -> logger.debug("Rating {} under-invested by {} percentage points.", r, pp)); } }); return mostWantedRatings.values().stream().flatMap(Collection::stream); }
@Test void timestamp() { final PortfolioOverview po = new PortfolioOverviewImpl(BigDecimal.TEN, Collections.emptyMap(), Collections.emptyMap()); assertThat(po.getTimestamp()).isBeforeOrEqualTo(ZonedDateTime.now()); }
public static Map<String, Object> summarizePortfolioStructure(final PortfolioOverview portfolioOverview) { return Maps.ofEntries( entry("absoluteShare", perRating(portfolioOverview::getCzkInvested)), entry("relativeShare", perRating(portfolioOverview::getShareOnInvestment)), entry("absoluteRisk", perRating(portfolioOverview::getCzkAtRisk)), entry("relativeRisk", perRating(portfolioOverview::getAtRiskShareOnInvestment)), entry("total", portfolioOverview.getCzkInvested()), entry("totalRisk", portfolioOverview.getCzkAtRisk()), entry("totalShare", portfolioOverview.getShareAtRisk()), entry("balance", portfolioOverview.getCzkAvailable()), entry("timestamp", toDate(portfolioOverview.getTimestamp())) ); }
@Override boolean shouldNotify(final ExecutionStartedEvent event, final SessionInfo sessionInfo) { final Optional<BigDecimal> lastKnownBalance = balanceTracker.getLastKnownBalance(sessionInfo); final BigDecimal newBalance = event.getPortfolioOverview().getCzkAvailable(); final BigDecimal expectedBalance = BigDecimal.valueOf(minimumBalance); LOGGER.debug("Last known balance: {}, minimum: {}, new: {}.", lastKnownBalance, expectedBalance, newBalance); final boolean balanceNowUnder = newBalance.compareTo(expectedBalance) < 0; final boolean wasFineLastTime = !lastKnownBalance.isPresent() || lastKnownBalance.get().compareTo(expectedBalance) >= 0; return (balanceNowUnder && wasFineLastTime); } }
static boolean isAcceptable(final ParsedStrategy strategy, final PortfolioOverview portfolio) { final long balance = portfolio.getCzkAvailable().longValue(); if (balance < strategy.getMinimumBalance()) { Decisions.report(logger -> logger.debug("Not recommending any loans due to balance under minimum.")); return false; } final long invested = portfolio.getCzkInvested().longValue(); final long investmentCeiling = strategy.getMaximumInvestmentSizeInCzk(); if (invested >= investmentCeiling) { Decisions.report(logger -> logger.debug("Not recommending any loans due to reaching the ceiling.")); return false; } return true; } }
@Override protected Map<String, Object> getData(final InvestmentSoldEvent event) { final Map<String, Object> result = super.getData(event); final int invested = event.getPortfolioOverview().getCzkInvested().intValue(); result.put("yield", FinancialCalculator.actualInterestAfterFees(event.getInvestment(), invested, true)); return result; } }
static Stream<Rating> rankRatingsByDemand(final ParsedStrategy strategy, final Collection<Rating> ratings, final PortfolioOverview portfolio) { final SortedMap<BigDecimal, EnumSet<Rating>> mostWantedRatings = new TreeMap<>(Comparator.reverseOrder()); // put the ratings into buckets based on how much we're missing them ratings.forEach(r -> { final BigDecimal currentRatingShare = portfolio.getShareOnInvestment(r); final BigDecimal maximumAllowedShare = divide(strategy.getMaximumShare(r), ONE_HUNDRED); final BigDecimal undershare = maximumAllowedShare.subtract(currentRatingShare); if (undershare.signum() < 1) { // we over-invested into this rating; do not include final BigDecimal pp = undershare.multiply(ONE_HUNDRED).negate(); Decisions.report(logger -> logger.debug("Rating {} over-invested by {} percentage points.", r, pp)); return; } // rank the rating mostWantedRatings.computeIfAbsent(undershare, k -> EnumSet.noneOf(Rating.class)); mostWantedRatings.get(undershare).add(r); // inform that the rating is under-invested final BigDecimal minimumNeededShare = divide(strategy.getMinimumShare(r), ONE_HUNDRED); if (currentRatingShare.compareTo(minimumNeededShare) < 0) { final BigDecimal pp = minimumNeededShare.subtract(currentRatingShare).multiply(ONE_HUNDRED); Decisions.report(logger -> logger.debug("Rating {} under-invested by {} percentage points.", r, pp)); } }); return mostWantedRatings.values().stream().flatMap(Collection::stream); }
protected static PortfolioOverview mockPortfolioOverview(final int balance) { final PortfolioOverview po = mock(PortfolioOverview.class); when(po.getCzkAvailable()).thenReturn(BigDecimal.valueOf(balance)); when(po.getCzkInvested()).thenReturn(BigDecimal.ZERO); when(po.getCzkInvested(any())).thenReturn(BigDecimal.ZERO); when(po.getCzkAtRisk()).thenReturn(BigDecimal.ZERO); when(po.getCzkAtRisk(any())).thenReturn(BigDecimal.ZERO); when(po.getShareAtRisk()).thenReturn(BigDecimal.ZERO); when(po.getShareOnInvestment(any())).thenReturn(BigDecimal.ZERO); when(po.getAtRiskShareOnInvestment(any())).thenReturn(BigDecimal.ZERO); when(po.getTimestamp()).thenReturn(ZonedDateTime.now()); return po; }
@Override boolean shouldNotify(final ExecutionStartedEvent event, final SessionInfo sessionInfo) { final Optional<BigDecimal> lastKnownBalance = balanceTracker.getLastKnownBalance(sessionInfo); final BigDecimal newBalance = event.getPortfolioOverview().getCzkAvailable(); final BigDecimal expectedBalance = BigDecimal.valueOf(targetBalance); LOGGER.debug("Last known balance: {}, target: {}, new: {}.", lastKnownBalance, expectedBalance, newBalance); final boolean balanceNowExceeded = newBalance.compareTo(expectedBalance) > 0; final boolean wasFineLastTime = !lastKnownBalance.isPresent() || lastKnownBalance.get().compareTo(expectedBalance) < 0; return (balanceNowExceeded && wasFineLastTime); } }
@Test void unacceptablePortfolioDueToLowBalance() { final ParsedStrategy p = new ParsedStrategy(DefaultPortfolio.EMPTY); final PurchaseStrategy s = new NaturalLanguagePurchaseStrategy(p); final PortfolioOverview portfolio = mock(PortfolioOverview.class); when(portfolio.getCzkAvailable()).thenReturn(BigDecimal.ZERO); when(portfolio.getCzkInvested()).thenReturn(BigDecimal.ZERO); final Stream<RecommendedParticipation> result = s.recommend(Collections.singletonList(mockDescriptor()), portfolio, new Restrictions()); assertThat(result).isEmpty(); }
@Override protected Map<String, Object> getData(final InvestmentMadeEvent event) { final Investment i = event.getInvestment(); final Map<String, Object> result = super.getData(event); final long invested = event.getPortfolioOverview().getCzkInvested().longValue(); result.put("yield", FinancialCalculator.expectedInterestAfterFees(i, invested)); final BigDecimal interestRate = FinancialCalculator.expectedInterestRateAfterFees(i, invested); result.put("relativeYield", interestRate); return result; } }
@Test void emptyPortfolioWithAdjustmentsAndRisks() { final BigDecimal adj = BigDecimal.TEN; final Map<Rating, BigDecimal> in = Collections.singletonMap(Rating.D, adj); final PortfolioOverview po = new PortfolioOverviewImpl(BigDecimal.ZERO, in, in); SoftAssertions.assertSoftly(softly -> { softly.assertThat(po.getCzkAvailable()).isEqualTo(BigDecimal.ZERO); softly.assertThat(po.getCzkInvested()).isEqualTo(adj); softly.assertThat(po.getCzkAtRisk()).isEqualTo(adj); final BigDecimal share = divide(po.getCzkAtRisk(), po.getCzkInvested()); softly.assertThat(po.getShareAtRisk()).isEqualTo(share); for (final Rating r : Rating.values()) { final BigDecimal expectedAbsolute = r == Rating.D ? adj : BigDecimal.ZERO; final BigDecimal expectedRelative = r == Rating.D ? BigDecimal.ONE : BigDecimal.ZERO; softly.assertThat(po.getCzkInvested(r)).as(r + " invested").isEqualTo(expectedAbsolute); softly.assertThat(po.getCzkAtRisk(r)).as(r + " at risk").isEqualTo(expectedAbsolute); softly.assertThat(po.getShareOnInvestment(r)) .as(r + " as a share") .isEqualTo(expectedRelative); softly.assertThat(po.getAtRiskShareOnInvestment(r)) .as(r + " at risk as a share") .isEqualTo(expectedRelative); } }); } }
@Override boolean shouldNotify(final ExecutionStartedEvent event, final SessionInfo sessionInfo) { final Optional<BigDecimal> lastKnownBalance = balanceTracker.getLastKnownBalance(sessionInfo); final BigDecimal newBalance = event.getPortfolioOverview().getCzkAvailable(); final BigDecimal expectedBalance = BigDecimal.valueOf(minimumBalance); logger.debug("Last known balance: {}, minimum: {}, new: {}.", lastKnownBalance, expectedBalance, newBalance); final boolean balanceNowUnder = newBalance.compareTo(expectedBalance) < 0; final boolean wasFineLastTime = !lastKnownBalance.isPresent() || lastKnownBalance.get().compareTo(expectedBalance) >= 0; return (balanceNowUnder && wasFineLastTime); } }
@Override protected Map<String, Object> getData(final InvestmentPurchasedEvent event) { final Investment i = event.getInvestment(); final Map<String, Object> result = super.getData(event); final long invested = event.getPortfolioOverview().getCzkInvested().longValue(); result.put("yield", FinancialCalculator.expectedInterestAfterFees(i, invested)); final BigDecimal interestRate = FinancialCalculator.expectedInterestRateAfterFees(i, invested); result.put("relativeYield", interestRate); return result; } }
@Test void emptyPortfolio() { final BigDecimal balance = BigDecimal.TEN; final PortfolioOverview po = new PortfolioOverviewImpl(balance, Collections.emptyMap(), Collections.emptyMap()); SoftAssertions.assertSoftly(softly -> { softly.assertThat(po.getCzkAvailable()).isEqualTo(balance); softly.assertThat(po.getCzkInvested()).isEqualTo(BigDecimal.ZERO); softly.assertThat(po.getCzkAtRisk()).isEqualTo(BigDecimal.ZERO); softly.assertThat(po.getShareAtRisk()).isEqualTo(BigDecimal.ZERO); for (final Rating r : Rating.values()) { softly.assertThat(po.getCzkInvested(r)).as(r + " invested").isEqualTo(BigDecimal.ZERO); softly.assertThat(po.getCzkAtRisk(r)).as(r + " at risk").isEqualTo(BigDecimal.ZERO); softly.assertThat(po.getShareOnInvestment(r)) .as(r + " as a share") .isEqualTo(BigDecimal.ZERO); softly.assertThat(po.getAtRiskShareOnInvestment(r)) .as(r + " at risk as a share") .isEqualTo(BigDecimal.ZERO); } }); }