All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.robozonky.app.daemon.InvestingSession Maven / Gradle / Ivy

/*
 * Copyright 2021 The RoboZonky Project
 *
 * 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.robozonky.app.daemon;

import static com.github.robozonky.app.events.impl.EventFactory.executionCompleted;
import static com.github.robozonky.app.events.impl.EventFactory.executionCompletedLazy;
import static com.github.robozonky.app.events.impl.EventFactory.executionStarted;
import static com.github.robozonky.app.events.impl.EventFactory.executionStartedLazy;
import static com.github.robozonky.app.events.impl.EventFactory.investmentMade;
import static com.github.robozonky.app.events.impl.EventFactory.investmentMadeLazy;

import java.util.stream.Stream;

import javax.ws.rs.BadRequestException;

import com.github.robozonky.api.remote.entities.Loan;
import com.github.robozonky.api.strategies.InvestmentStrategy;
import com.github.robozonky.api.strategies.LoanDescriptor;
import com.github.robozonky.app.tenant.PowerTenant;

/**
 * Represents a single investment session over a certain marketplace, consisting of several attempts to invest into
 * given loan.
 * 

* Instances of this class are supposed to be short-lived, as the marketplace and Zonky account balance can change * externally at any time. Essentially, one remote marketplace check should correspond to one instance of this class. */ final class InvestingSession extends AbstractSession { InvestingSession(final Stream marketplace, final PowerTenant tenant) { super(marketplace, tenant, d -> d.item() .getId(), "discardedLoans", Audit.investing()); } public static Stream invest(final PowerTenant tenant, final Stream loans, final InvestmentStrategy strategy) { final InvestingSession s = new InvestingSession(loans, tenant); s.tenant.fire(executionStartedLazy(() -> executionStarted(tenant.getPortfolio() .getOverview()))); s.invest(strategy); // make sure we get fresh portfolio reference here s.tenant.fire(executionCompletedLazy(() -> executionCompleted(tenant.getPortfolio() .getOverview()))); return s.getResult(); } private void invest(final InvestmentStrategy strategy) { logger.debug("Starting the investing mechanism with balance upper bound of {}.", tenant.getKnownBalanceUpperBound()); getAvailable() .flatMap(i -> strategy.recommend(i, () -> tenant.getPortfolio() .getOverview(), tenant.getSessionInfo()) .map(amount -> new RecommendedLoan(i, amount)) .stream()) .takeWhile(this::isBalanceAcceptable) // no need to try if we don't have enough money .forEach(this::accept); // keep trying until investment opportunities are exhausted } private void processSuccessfulInvestment(final RecommendedLoan recommendation) { var l = recommendation.descriptor() .item(); var amount = recommendation.amount(); result.add(l); tenant.getPortfolio() .simulateCharge(l.getId(), l.getInterestRate(), amount); tenant.setKnownBalanceUpperBound(tenant.getKnownBalanceUpperBound() .subtract(amount)); discard(recommendation.descriptor()); // never show again tenant.fire(investmentMadeLazy(() -> investmentMade(l, amount, tenant.getPortfolio() .getOverview()))); logger.info("Invested {} into loan #{}.", amount, l.getId()); } @Override protected boolean accept(final RecommendedLoan recommendation) { ResponseTimeTracker.executeAsync((r, nanotime) -> r.dispatch(nanotime, recommendation.descriptor() .item())); if (!isBalanceAcceptable(recommendation)) { logger.debug("Will not invest in {} due to balance ({}) likely too low.", recommendation, tenant.getKnownBalanceUpperBound()); return false; } try { if (tenant.getSessionInfo() .isDryRun()) { logger.debug("Dry run. Otherwise would attempt investing: {}.", recommendation); } else { logger.debug("Will attempt to invest in {}.", recommendation); tenant.run(z -> z.invest(recommendation.descriptor() .item(), recommendation.amount() .getValue() .intValue())); } processSuccessfulInvestment(recommendation); return true; } catch (BadRequestException ex) { var response = getResponseEntity(ex.getResponse()); if (response.contains("TOO_MANY_REQUESTS")) { // HTTP 429 needs to terminate investing and throw failure up to the availability algorithm. throw new IllegalStateException("HTTP 429 Too Many Requests caught during investing.", ex); } else if (response.contains("insufficientBalance")) { var amount = recommendation.amount(); logger.debug("Failed investing {}. We don't have sufficient balance.", amount); tenant.setKnownBalanceUpperBound(amount.subtract(1)); return false; } var acceptable = Stream .of("cancelled", "withdrawn", "reservedInvestmentOnly", "overInvestment", "multipleInvestment", "alreadyCovered") .anyMatch(response::contains); if (acceptable) { logger.debug("Failed investing {} into loan #{}. Reason given: '{}'.", recommendation.amount(), recommendation.descriptor() .item() .getId(), response); return false; } throw new IllegalStateException("Unknown problem during investing. Reason given: '" + response + "'.", ex); } catch (Exception ex) { throw new IllegalStateException("Unknown problem during investing.", ex); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy