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

com.github.robozonky.app.daemon.PurchasingSession 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.investmentPurchased;
import static com.github.robozonky.app.events.impl.EventFactory.investmentPurchasedLazy;
import static com.github.robozonky.app.events.impl.EventFactory.purchasingCompleted;
import static com.github.robozonky.app.events.impl.EventFactory.purchasingCompletedLazy;
import static com.github.robozonky.app.events.impl.EventFactory.purchasingStarted;
import static com.github.robozonky.app.events.impl.EventFactory.purchasingStartedLazy;

import java.util.stream.Stream;

import javax.ws.rs.BadRequestException;
import javax.ws.rs.NotFoundException;

import com.github.robozonky.api.remote.entities.Participation;
import com.github.robozonky.api.strategies.ParticipationDescriptor;
import com.github.robozonky.api.strategies.PurchaseStrategy;
import com.github.robozonky.app.tenant.PowerTenant;

/**
 * Represents a single session over secondary marketplace, consisting of several attempts to purchase participations.
 * 

* 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 PurchasingSession extends AbstractSession { PurchasingSession(final Stream marketplace, final PowerTenant tenant) { super(marketplace, tenant, d -> d.item() .getId(), "discardedParticipations", Audit.purchasing()); } public static Stream purchase(final PowerTenant auth, final Stream items, final PurchaseStrategy strategy) { final PurchasingSession s = new PurchasingSession(items, auth); s.tenant.fire(purchasingStartedLazy(() -> purchasingStarted(auth.getPortfolio() .getOverview()))); s.purchase(strategy); s.tenant.fire(purchasingCompletedLazy(() -> purchasingCompleted(auth.getPortfolio() .getOverview()))); return s.getResult(); } private void purchase(final PurchaseStrategy strategy) { getAvailable() .filter(i -> strategy.recommend(i, () -> tenant.getPortfolio() .getOverview(), tenant.getSessionInfo())) .map(RecommendedParticipation::new) .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 boolean actualPurchase(final Participation participation) { ResponseTimeTracker.executeAsync((r, nanotime) -> r.dispatch(nanotime, participation)); try { tenant.run(zonky -> zonky.purchase(participation)); logger.info("Purchased a participation worth {}.", participation.getRemainingPrincipal()); return true; } catch (BadRequestException ex) { var response = getResponseEntity(ex.getResponse()); if (response.contains("INSUFFICIENT_BALANCE")) { logger.debug("Failed purchasing participation worth {}. We don't have sufficient balance.", participation.getRemainingPrincipal()); tenant.setKnownBalanceUpperBound(participation.getRemainingPrincipal() .subtract(1)); return false; } else if (response.contains("ALREADY_HAVE_INVESTMENT")) { logger.debug("Failed purchasing participation #{}, already have investment.", participation.getId()); return false; } else if (response.contains("INVESTMENT_LOCK_BY_OTHER_USER")) { logger.debug("Failed purchasing participation #{}, locked by another user.", participation.getId()); return false; } throw new IllegalStateException("Unknown problem during purchasing. Reason given: '" + response + "'.", ex); } catch (NotFoundException ex) { logger.debug("Failed purchasing participation #{}, not found.", participation.getId()); return false; } catch (Exception ex) { var message = ex.getMessage(); if (message != null && message.contains("HTTP 429")) { // Zonky only allows one purchase per second; ignore. logger.debug("Failed purchasing participation #{}, Zonky rejected request with HTTP 429.", participation.getId()); return false; } else throw new IllegalStateException("Unknown problem during purchasing.", ex); } } @Override protected boolean accept(final RecommendedParticipation recommendation) { if (!isBalanceAcceptable(recommendation)) { logger.debug("Will not purchase {} due to balance ({}) likely too low.", recommendation, tenant.getKnownBalanceUpperBound()); return false; } final Participation participation = recommendation.descriptor() .item(); final boolean succeeded = tenant.getSessionInfo() .isDryRun() || actualPurchase(participation); discard(recommendation.descriptor()); if (succeeded) { result.add(participation); tenant.getPortfolio() .simulateCharge(participation.getLoanId(), participation.getInterestRate(), recommendation.amount()); tenant.setKnownBalanceUpperBound(tenant.getKnownBalanceUpperBound() .subtract(recommendation.amount())); tenant.fire(investmentPurchasedLazy(() -> investmentPurchased(participation, recommendation.descriptor() .related(), recommendation.amount(), tenant.getPortfolio() .getOverview()))); } return succeeded; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy