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

com.github.triceo.robozonky.app.investing.Session Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2017 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.triceo.robozonky.app.investing;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import com.github.triceo.robozonky.api.confirmations.ConfirmationProvider;
import com.github.triceo.robozonky.api.notifications.ExecutionCompletedEvent;
import com.github.triceo.robozonky.api.notifications.ExecutionStartedEvent;
import com.github.triceo.robozonky.api.notifications.InvestmentDelegatedEvent;
import com.github.triceo.robozonky.api.notifications.InvestmentMadeEvent;
import com.github.triceo.robozonky.api.notifications.InvestmentRejectedEvent;
import com.github.triceo.robozonky.api.notifications.InvestmentRequestedEvent;
import com.github.triceo.robozonky.api.notifications.InvestmentSkippedEvent;
import com.github.triceo.robozonky.api.remote.ControlApi;
import com.github.triceo.robozonky.api.remote.entities.Investment;
import com.github.triceo.robozonky.api.strategies.LoanDescriptor;
import com.github.triceo.robozonky.api.strategies.PortfolioOverview;
import com.github.triceo.robozonky.api.strategies.RecommendedLoan;
import com.github.triceo.robozonky.app.Events;
import com.github.triceo.robozonky.app.portfolio.Portfolio;
import com.github.triceo.robozonky.app.util.ApiUtil;
import com.github.triceo.robozonky.common.remote.Zonky;
import com.github.triceo.robozonky.internal.api.Defaults;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 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. */ class Session implements AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(Session.class); private static final AtomicReference INSTANCE = new AtomicReference<>(null); /** * Create a new instance of this class. Only one instance is expected to exist at a time. *

* At any given time, there may only be 1 instance of this class that is being used. This is due to the fact that * the sessions share state through a file, and therefore multiple concurrent sessions would interfere with one * another. * @param investor Confirmation layer around the investment API. * @param api Authenticated access to Zonky for data retrieval. * @param marketplace Loans that are available in the marketplace. * @return * @throws IllegalStateException When another {@link Session} instance was not {@link #close()}d. */ public synchronized static Session create(final Investor.Builder investor, final Zonky api, final Collection marketplace) { if (Session.INSTANCE.get() != null) { throw new IllegalStateException("Investment session already exists."); } final Session s = new Session(new LinkedHashSet<>(marketplace), investor, api); Session.INSTANCE.set(s); return s; } static Collection invest(final Investor.Builder investor, final Zonky api, final InvestmentCommand command) { try (final Session session = Session.create(investor, api, command.getLoans())) { final int balance = session.getPortfolioOverview().getCzkAvailable(); Events.fire(new ExecutionStartedEvent(command.getLoans(), session.getPortfolioOverview())); if (balance >= Defaults.MINIMUM_INVESTMENT_IN_CZK && !session.getAvailable().isEmpty()) { command.accept(session); } final PortfolioOverview portfolio = session.getPortfolioOverview(); Session.LOGGER.info("Current value of portfolio is {} CZK, annual expected yield is {} % ({} CZK).", portfolio.getCzkInvested(), portfolio.getRelativeExpectedYield() .scaleByPowerOfTen(2) .setScale(2, RoundingMode.HALF_EVEN), portfolio.getCzkExpectedYield()); final Collection result = session.getResult(); Events.fire(new ExecutionCompletedEvent(result, portfolio)); return Collections.unmodifiableCollection(result); } } private final List loansStillAvailable; private final Collection investmentsMadeNow = new LinkedHashSet<>(); private PortfolioOverview portfolioOverview; private final Investor investor; private BigDecimal balance; private final SessionState state; private Session(final Set marketplace, final Investor.Builder proxy, final Zonky zonky) { this.investor = proxy.build(zonky); this.balance = investor.isDryRun() ? ApiUtil.getDryRunBalance(zonky) : ApiUtil.getLiveBalance(zonky); Session.LOGGER.info("Starting account balance: {} CZK.", balance); this.state = new SessionState(marketplace); this.loansStillAvailable = marketplace.stream() .filter(l -> state.getDiscardedLoans().stream().noneMatch(l2 -> l.item().getId() == l2.item().getId())) .filter(l -> Portfolio.INSTANCE.getPending().noneMatch(i -> l.item().getId() == i.getLoanId())) .collect(Collectors.toList()); this.portfolioOverview = Portfolio.INSTANCE.calculateOverview(balance); } private synchronized void ensureOpen() { final Session s = Session.INSTANCE.get(); if (!Objects.equals(s, this)) { throw new IllegalStateException("Session already closed."); } } /** * Get information about the portfolio, which is up to date relative to the current point in the session. * @return Portfolio. */ public synchronized PortfolioOverview getPortfolioOverview() { return portfolioOverview; } /** * Get loans that are available to be evaluated by the strategy. These are loans that come from the marketplace, * minus loans that are already invested into or discarded due to the {@link ConfirmationProvider} mechanism. * @return Loans in the marketplace in which the user could potentially invest. Unmodifiable. */ public synchronized Collection getAvailable() { return Collections.unmodifiableList(new ArrayList<>(loansStillAvailable)); } /** * Get investments made during this session. * @return Investments made so far during this session. Unmodifiable. */ public synchronized List getResult() { return Collections.unmodifiableList(new ArrayList<>(investmentsMadeNow)); } /** * Request {@link ControlApi} to invest in a given loan, leveraging the {@link ConfirmationProvider}. * @param recommendation Loan to invest into. * @return True if investment successful. The investment is reflected in {@link #getResult()}. * @throws IllegalStateException When already {@link #close()}d. */ public synchronized boolean invest(final RecommendedLoan recommendation) { ensureOpen(); final LoanDescriptor loan = recommendation.descriptor(); final int loanId = loan.item().getId(); if (balance.intValue() < recommendation.amount().intValue()) { // should not be allowed by the calling code return false; } Events.fire(new InvestmentRequestedEvent(recommendation)); final boolean seenBefore = state.getSeenLoans().stream().anyMatch(l -> l.item().getId() == loanId); final ZonkyResponse response = investor.invest(recommendation, seenBefore); Session.LOGGER.debug("Response for loan {}: {}.", loanId, response); final String providerId = investor.getConfirmationProviderId().orElse("-"); switch (response.getType()) { case REJECTED: return investor.getConfirmationProviderId().map(c -> { Events.fire(new InvestmentRejectedEvent(recommendation, balance.intValue(), providerId)); // rejected through a confirmation provider => forget discard(loan); return false; }).orElseGet(() -> { // rejected due to no confirmation provider => make available for direct investment later Events.fire(new InvestmentSkippedEvent(recommendation)); Session.LOGGER.debug("Loan #{} protected by CAPTCHA, will check back later.", loanId); skip(loan); return false; }); case DELEGATED: Events.fire(new InvestmentDelegatedEvent(recommendation, balance.intValue(), providerId)); if (recommendation.isConfirmationRequired()) { // confirmation required, delegation successful => forget discard(loan); } else { // confirmation not required, delegation successful => make available for direct investment later skip(loan); } return false; case INVESTED: final int confirmedAmount = response.getConfirmedAmount().getAsInt(); final Investment i = new Investment(recommendation.descriptor().item(), confirmedAmount); markSuccessfulInvestment(i); Events.fire(new InvestmentMadeEvent(i, balance.intValue(), investor.isDryRun())); return true; case SEEN_BEFORE: Events.fire(new InvestmentSkippedEvent(recommendation)); return false; default: throw new IllegalStateException("Not possible."); } } private synchronized void markSuccessfulInvestment(final Investment i) { investmentsMadeNow.add(i); loansStillAvailable.removeIf(l -> l.item().getId() == i.getLoanId()); balance = balance.subtract(i.getAmount()); Portfolio.INSTANCE.update(investor.getZonky(), Portfolio.UpdateType.PARTIAL); portfolioOverview = Portfolio.INSTANCE.calculateOverview(balance); } private synchronized void discard(final LoanDescriptor loan) { skip(loan); state.discard(loan); } private synchronized void skip(final LoanDescriptor loan) { loansStillAvailable.removeIf(l -> Objects.equals(loan, l)); state.skip(loan); } @Override public synchronized void close() { Session.INSTANCE.set(null); // the session can no longer be used } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy