package name.abuchen.portfolio.model;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Optional;
import java.util.ResourceBundle;
import name.abuchen.portfolio.money.CurrencyConverter;
import name.abuchen.portfolio.money.Money;
import name.abuchen.portfolio.money.MoneyCollectors;
import name.abuchen.portfolio.money.Quote;
import name.abuchen.portfolio.money.Values;
public class PortfolioTransaction extends Transaction
{
public enum Type
{
/** Records the purchase of a security. */
BUY(true),
/** Records the sale of a security. */
SELL(false),
/** Records the transfer of assets from another portfolio. */
TRANSFER_IN(true),
/** Records the transfer of assets to another portfolio. */
TRANSFER_OUT(false),
/** Records the transfer of assets into the portfolio. */
DELIVERY_INBOUND(true),
/** Records the transfer of assets out of a portfolio. */
DELIVERY_OUTBOUND(false);
private static final ResourceBundle RESOURCES = ResourceBundle.getBundle("name.abuchen.portfolio.model.labels"); //$NON-NLS-1$
private final boolean isPurchase;
private Type(boolean isPurchase)
{
this.isPurchase = isPurchase;
}
/**
* True if the transaction is one of the purchase types such as buy,
* transfer in, or an inbound delivery.
*/
public boolean isPurchase()
{
return isPurchase;
}
/**
* True if the transaction is one of the liquidation types such as sell,
* transfer out, or an outbound delivery.
*/
public boolean isLiquidation()
{
return !isPurchase;
}
@Override
public String toString()
{
return RESOURCES.getString("portfolio." + name()); //$NON-NLS-1$
}
}
private Type type;
@Deprecated
/* package */transient long fees;
@Deprecated
/* package */transient long taxes;
public PortfolioTransaction()
{
// needed for xstream de-serialization
}
public PortfolioTransaction(LocalDate date, String currencyCode, long amount, Security security, long shares,
Type type, long fees, long taxes)
{
super(date, currencyCode, amount, security, shares, null);
this.type = type;
if (fees != 0)
addUnit(new Unit(Unit.Type.FEE, Money.of(currencyCode, fees)));
if (taxes != 0)
addUnit(new Unit(Unit.Type.TAX, Money.of(currencyCode, taxes)));
}
public PortfolioTransaction(String date, String currencyCode, long amount, Security security, long shares,
Type type, long fees, long taxes)
{
this(LocalDate.parse(date), currencyCode, amount, security, shares, type, fees, taxes);
}
public Type getType()
{
return type;
}
public void setType(Type type)
{
this.type = type;
}
/**
* Returns the monetary amount in the term currency of the given currency
* converter. If applicable, it uses the exchange rate given in the
* transaction instead of the historic exchange rate of that day.
*/
public Money getMonetaryAmount(CurrencyConverter converter)
{
if (getCurrencyCode().equals(converter.getTermCurrency()))
{
return getMonetaryAmount();
}
else if (getSecurity().getCurrencyCode().equals(converter.getTermCurrency()))
{
Optional<Unit> grossValue = getUnit(Unit.Type.GROSS_VALUE);
// use exchange rate used within the transaction,
// not the historical exchange rate
BigDecimal exchangeRate = grossValue.isPresent() ? grossValue.get().getExchangeRate()
: converter.getRate(getDate(), getCurrencyCode()).getValue();
return Money.of(converter.getTermCurrency(),
BigDecimal.ONE.divide(exchangeRate, 10, BigDecimal.ROUND_HALF_DOWN)
.multiply(BigDecimal.valueOf(getAmount()))
.setScale(0, BigDecimal.ROUND_HALF_DOWN).longValue());
}
else
{
return converter.convert(getDate(), getMonetaryAmount());
}
}
/**
* Returns the gross value, i.e. the value including taxes and fees. See
* {@link #getGrossValue()}.
*/
public long getGrossValueAmount()
{
long taxAndFees = getUnits().filter(u -> u.getType() == Unit.Type.TAX || u.getType() == Unit.Type.FEE)
.collect(MoneyCollectors.sum(getCurrencyCode(), u -> u.getAmount())).getAmount();
if (this.type.isPurchase())
return getAmount() - taxAndFees;
else
return getAmount() + taxAndFees;
}
/**
* Returns the gross value, i.e. the value before taxes and fees are
* applied. In the case of a buy transaction, that are the gross costs, i.e.
* before adding additional taxes and fees. In the case of sell
* transactions, that are the gross proceeds before the deduction of taxes
* and fees.
*/
public Money getGrossValue()
{
return Money.of(getCurrencyCode(), getGrossValueAmount());
}
/**
* Returns the gross value in the term currency of the currency converter.
* If applicable, it uses the exchange rate given in the transaction instead
* of the historic exchange rate of that day.
*/
public Money getGrossValue(CurrencyConverter converter)
{
if (getCurrencyCode().equals(converter.getTermCurrency()))
{
return getGrossValue();
}
else if (getSecurity().getCurrencyCode().equals(converter.getTermCurrency()))
{
Optional<Unit> grossValue = getUnit(Unit.Type.GROSS_VALUE);
return Money.of(converter.getTermCurrency(),
grossValue.isPresent() ? grossValue.get().getForex().getAmount()
: getGrossValue().with(converter.at(getDate())).getAmount());
}
else
{
return converter.convert(getDate(), getGrossValue());
}
}
/**
* Returns the gross price per share, i.e. the gross value divided by the
* number of shares bought or sold. The quote uses the currency of the
* transaction.
*/
public Quote getGrossPricePerShare()
{
if (getShares() == 0)
return Quote.of(getCurrencyCode(), 0);
double grossPrice = getGrossValueAmount() * Values.Share.factor() * Values.Quote.factorToMoney()
/ (double) getShares();
return Quote.of(getCurrencyCode(), Math.round(grossPrice));
}
/**
* Returns the gross price per share in the given currency. Converted is not
* the price per share but the gross value before dividing by the number of
* shares in order minimize rounding errors.
*/
public Quote getGrossPricePerShare(CurrencyConverter converter)
{
if (getShares() == 0)
return Quote.of(converter.getTermCurrency(), 0);
if (converter.getTermCurrency().equals(getCurrencyCode()))
return getGrossPricePerShare();
// Because the gross value is calculated with taxes (which might be in
// transaction currency and not in security currency) we must convert
// the gross value (instead of checking the unit type GROSS_VALUE)
long grossValue = getGrossValue().with(converter.at(getDate())).getAmount();
double grossPrice = grossValue * Values.Share.factor() * Values.Quote.factorToMoney() / (double) getShares();
return Quote.of(converter.getTermCurrency(), Math.round(grossPrice));
}
}