package hirondelle.stocks.portfolio; import java.util.*; import java.math.BigDecimal; import hirondelle.stocks.util.HashCodeUtil; import hirondelle.stocks.util.EqualsUtil; import hirondelle.stocks.util.Args; import hirondelle.stocks.util.Consts; import hirondelle.stocks.quotes.QuotesDAO; import hirondelle.stocks.util.DataAccessException; import hirondelle.stocks.quotes.Quote; import hirondelle.stocks.quotes.Stock; /** * Represents a uniquely-named set of {@link Stock} objects, * in which the user has an interest in monitoring. * *<P>Methods in this class which take a <tt>Collection</tt> of {@link Quote} * objects as a parameter *<ul> * <li>treat the <tt>Collection</tt> as a filter : * if a <tt>Stock</tt> in this <tt>Portfolio</tt> is not present * in the <tt>aQuotes</tt> param, then that <tt>Stock</tt> will * not contribute to the result of such a method. * <li> the <tt>Quote</tt> collection must only contain <tt>Quote</tt>s * which correspond to a <tt>Stock</tt> known to this portfolio. *</ul> * * <P>Here is an example illustrating how to change the stocks contained in the * portfolio : <pre> fWorkingCopy = new TreeSet(fCurrentPortfolio.getStocks()); //..edit the contents of fWorkingCopy fCurrentPortfolio.setStocks(fWorkingCopy); </pre> * Note that edits to <tt>fWorkingCopy</tt> can * be easily abandoned, without affecting <tt>fCurrentPortfolio</tt>. */ public final class Portfolio { /** * Constructor. * * @param aName is the unique identifier for this <tt>Portfolio</tt>, has * visible content * @param aStocks is possibly-empty, and represents a reference to an * object which is shared with the caller; no defensive copy is made. */ public Portfolio(String aName, Set<Stock> aStocks) { setName(aName); setStocks(aStocks); } /** * Return a <tt>Portfolio</tt> which contains no stocks, and whose title * is an empty <tt>String</tt>. * * <P>An untitled <tt>Portfolio</tt> is a special object, which cannot be created either * directly (by using the constructor), nor indirectly (by using the constructor and * changing object state). These special objects are only obtained through this * method. */ public static Portfolio getUntitledPortfolio(){ return new Portfolio(); } /** * Return <tt>true</tt> only if the name of this <tt>Portfolio</tt> is an * empty <tt>String</tt>. */ public boolean isUntitled(){ return getName().equals(UNTITLED); } /** * Return the unique name of this <tt>Portfolio</tt>. * * @return possibly-empty <tt>String</tt>. */ public String getName(){ return fName; } /** * Set the unique name of this <tt>Portfolio</tt>. * * @param aName see {@link #Portfolio(String, Set)} for conditions on aName. */ public void setName( String aName ){ Args.checkForContent(aName); fName = aName; } /** * Return the {@link Stock} objects contained in this <tt>Portfolio</tt>. * * <P>The returned value is unmodifiable. See class description for example of * of changing the <tt>Stock</tt>s in a <tt>Portfolio</tt>. */ public Set<Stock> getStocks() { return fStocks; } /** * Replace the {@link Stock} objects contained in this <tt>Portfolio</tt>. * * @param aStocks see {@link #Portfolio(String, Set)} for conditions on aStocks. */ public void setStocks( Set<Stock> aStocks ){ Args.checkForNull(aStocks); fStocks = Collections.unmodifiableSet(aStocks); } /** * Return {@link Quote} objects, one for each <tt>Stock</tt> in this * <tt>Portfolio</tt>. */ public List<Quote> getQuotes() throws DataAccessException { QuotesDAO quotesDAO = new QuotesDAO(QuotesDAO.UseMonitor.TRUE, fStocks); return quotesDAO.getQuotes(); } /** * Return the cost of acquisition of all items in this<tt>Portfolio</tt> which * also appear in <tt>aQuotes</tt>. * * Each {@link Stock} in <tt>aQuotes</tt> contributes the value * {@link Stock#getBookValue}. * * @param aQuotes is a possibly-filtered collection of {@link Quote} objects; see * {@link hirondelle.stocks.table.QuoteFilter}; each <tt>Quote</tt> object * corresponds to a <tt>Stock</tt> known to this <tt>Portfolio</tt>. * @return a value greater than or equal to <tt>0.00</tt>. */ public BigDecimal getBookValue(Collection<Quote> aQuotes){ BigDecimal result = new BigDecimal("0.00"); for (Quote quote: aQuotes) { Stock stock = quote.getStock(); if ( ! fStocks.contains(stock) ) { throw new IllegalArgumentException("Unknown stock: " + stock); } result = result.add( stock.getBookValue() ); } return result.setScale(Consts.MONEY_DECIMAL_PLACES, Consts.MONEY_ROUNDING_STYLE); } /** * Return the current worth of all items in the <tt>Portfolio</tt> which * also appear in <tt>aQuotes</tt>. * * Each {@link Quote} contributes the value {@link Quote#getCurrentValue}. * * @param aQuotes is a possibly-filtered collection of {@link Quote} objects; see * {@link hirondelle.stocks.table.QuoteFilter}. * @return a value greater than or equal to <tt>0.00</tt>. */ public BigDecimal getCurrentValue(Collection<Quote> aQuotes){ BigDecimal result = new BigDecimal("0.00"); for(Quote quote : aQuotes) { Stock stock = quote.getStock(); if ( ! fStocks.contains(stock) ) { throw new IllegalArgumentException("Unknown stock: " + stock); } result = result.add(quote.getCurrentValue()); } return result.setScale(Consts.MONEY_DECIMAL_PLACES, Consts.MONEY_ROUNDING_STYLE); } /** * Return {@link #getCurrentValue} less {@link #getBookValue}. * * @param aQuotes is a possibly-filtered collection of {@link Quote} objects; see * {@link hirondelle.stocks.table.QuoteFilter}. * @return value is positive for a profit, and negative for a loss. */ public BigDecimal getProfit(Collection<Quote> aQuotes){ return getCurrentValue(aQuotes).subtract( getBookValue(aQuotes) ); } /** * Return {@link #getProfit} divided by {@link #getBookValue}; if the book value * is zero, then return zero. * * @param aQuotes is a possibly-filtered collection of {@link Quote} objects; see * {@link hirondelle.stocks.table.QuoteFilter}. */ public BigDecimal getPercentageProfit(Collection<Quote> aQuotes){ BigDecimal result = Consts.ZERO_MONEY; BigDecimal bookValue = getBookValue(aQuotes); if (bookValue.compareTo(Consts.ZERO_MONEY) != 0){ BigDecimal profit = getProfit(aQuotes); result = profit.divide(bookValue, Consts.MONEY_ROUNDING_STYLE); } return result; } /** * Represent this object as a <tt>String</tt> - intended for debugging * purposes only. */ @Override public String toString() { StringBuilder result = new StringBuilder(); String newLine = System.getProperty("line.separator"); result.append( this.getClass().getName() ); result.append(" {"); result.append(newLine); result.append(" fName = ").append(fName).append(newLine); result.append(" fStocks = ").append(fStocks).append(newLine); result.append("}"); result.append(newLine); return result.toString(); } @Override public boolean equals( Object aThat ) { if ( this == aThat ) return true; if ( !(aThat instanceof Portfolio) ) return false; Portfolio that = (Portfolio)aThat; return EqualsUtil.areEqual(this.fName, that.fName) && EqualsUtil.areEqual(this.fStocks, that.fStocks) ; } @Override public int hashCode() { int result = HashCodeUtil.SEED; result = HashCodeUtil.hash(result, fName); result = HashCodeUtil.hash(result, fStocks); return result; } // PRIVATE private String fName; private Set<Stock> fStocks; /** * This String will never conflict with a Portfolio name input by an end user, * since those names must have a non-zero trimmed length, as enforced through the * public constructor. */ private static final String UNTITLED = Consts.EMPTY_STRING; /** * Constructor allows for the creation of a special case, that of an * untitled <tt>Portfolio</tt>, whose name is an empty String, and whose * Set of Stock objects is an empty Set. * * Note that this special case can only be constructed internally, and not * through the public constructor; the public constructor's validations are * more restrictive. As well, the special case cannot be created in steps, through * altering the state of an object returned by the public constructor. * * @see #getUntitledPortfolio */ private Portfolio() { fName = UNTITLED; setStocks( new TreeSet<Stock>() ); } }