/*
* Copyright (C) 2012-2016 The Android Money Manager Ex Project Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.money.manager.ex.servicelayer;
import android.content.Context;
import android.database.Cursor;
import com.money.manager.ex.Constants;
import com.money.manager.ex.R;
import com.money.manager.ex.account.AccountTypes;
import com.money.manager.ex.assetallocation.ItemType;
import com.money.manager.ex.log.ExceptionHandler;
import com.money.manager.ex.currency.CurrencyService;
import com.money.manager.ex.datalayer.AccountRepository;
import com.money.manager.ex.datalayer.AssetClassRepository;
import com.money.manager.ex.datalayer.AssetClassStockRepository;
import com.money.manager.ex.datalayer.StockRepository;
import com.money.manager.ex.domainmodel.Account;
import com.money.manager.ex.domainmodel.AssetClass;
import com.money.manager.ex.domainmodel.AssetClassStock;
import com.money.manager.ex.domainmodel.Stock;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import info.javaperformance.money.Money;
import info.javaperformance.money.MoneyFactory;
/*
Group Asset Class
allocation sum setAllocation <= the only set value!
value sum totalValue * setAllocation / 100
Current allocation sum value * 100 / totalValue
current value sum value of all stocks (numStocks * price) in base currency!
difference sum currentValue - setValue
*/
/**
* Functions for the Asset Allocation
*/
public class AssetAllocationService
extends ServiceBase {
private static final String CashName = "Cash";
public AssetAllocationService(Context context) {
super(context);
this.repository = new AssetClassRepository(context);
mCurrencyService = new CurrencyService(context);
// mAccountCurrencies = new HashMap<>();
}
public AssetClassRepository repository;
private CurrencyService mCurrencyService;
/**
* Hashmap of Account Id / Currency Id pairs to speed up the calculation with caching.
*/
private HashMap<Integer, Integer> mAccountCurrencies;
public boolean deleteAllocation(int assetClassId) {
ExceptionHandler handler = new ExceptionHandler(getContext(), this);
AssetClassRepository repo = new AssetClassRepository(getContext());
// todo: use transaction? (see bulkUpdate)
// Delete all child elements.
List<Integer> childIds = getAllChildrenIds(assetClassId);
repo.deleteAll(childIds);
// delete any stock links
AssetClassStockRepository stockRepo = new AssetClassStockRepository(getContext());
boolean linksDeleted = stockRepo.deleteAllForAssetClass(assetClassId);
if (!linksDeleted) {
handler.showMessage("Error deleting stock links.");
return false;
}
// delete allocation record
boolean assetClassDeleted = repo.delete(assetClassId);
if (!assetClassDeleted) {
handler.showMessage("Error deleting asset class.");
return false;
}
return true;
}
public List<Integer> getAllChildrenIds(int assetClassId) {
List<Integer> ids = new ArrayList<>();
AssetClassRepository repo = new AssetClassRepository(getContext());
List<Integer> childIds = repo.loadAllChildrenIds(assetClassId);
ids.addAll(childIds);
// iterate recursively and get all children's children ids.
for (int childId : childIds) {
ids.addAll(getAllChildrenIds(childId));
}
return ids;
}
/**
* Main entry point when no data is loaded yet..
* @return Asset Allocation, see the method with the cursor parameter.
*/
public AssetClass loadAssetAllocation() {
// http://docs.mongodb.org/manual/tutorial/model-tree-structures/
// Step 1: Load all elements, ordered by ParentId.
Cursor c = loadData();
return loadAssetAllocationFrom(c);
}
/**
* Main entry point.
* @param c Cursor with asset classes, from which to load the Asset Allocation.
* @return Full Asset Allocation with all the calculated fields.
*/
private AssetClass loadAssetAllocationFrom(Cursor c) {
if (c == null) return null;
// Main asset allocation object.
AssetClass root = AssetClass.create("Asset Allocation");
root.setType(ItemType.Group);
// Fill a hash map with one pass through cursor. Used for easier fetching of asset classes.
HashMap<Integer, AssetClass> map = loadMap(c);
c.close();
// Assign children to their parents. Create a hierarchical list.
List<AssetClass> list = assignChildren(map);
// Load stock links and stocks to asset allocations.
loadStocks(list);
root.setChildren(list);
// create automatic Cash asset class by taking cash amounts from investment accounts.
addCash(root);
sortChildren(root);
// Calculate and store current Value amounts.
Money totalValue = calculateCurrentValue(list);
root.setCurrentValue(totalValue);
// Calculate all the derived values.
calculateStats(root, totalValue);
return root;
}
/**
* Loads asset class name, given the id.
* @param id Id of the asset class.
* @return String name of the asset class.
*/
public String loadName(int id) {
if (id == Constants.NOT_SET) return "";
AssetClassRepository repo = new AssetClassRepository(getContext());
Cursor c = repo.openCursor(
new String[]{AssetClass.NAME},
AssetClass.ID + "=?",
new String[]{Integer.toString(id)}
);
if (c == null) return null;
c.moveToNext();
AssetClass ac = AssetClass.from(c);
c.close();
return ac.getName();
}
/**
* Move the asset class down in the sort order.
* Increase sort order for this item. Finds the next in order and decrease it's sort order.
*/
public void moveClassDown(int id) {
// todo: this is incomplete. Need to set the default value on creation and e
// deletions. Also pay attention if the order will be ascending or descending and adjust.
// List<AssetClass> bulk = new ArrayList();
AssetClass up = repository.load(id);
Integer currentPosition = up.getSortOrder();
if (currentPosition == null) currentPosition = 0;
int upPosition = currentPosition + 1;
up.setSortOrder(upPosition);
// bulk.add(up);
// WhereStatementGenerator where = new WhereStatementGenerator();
// String filter = where.getStatement(AssetClass.SORTORDER, "=", upPosition);
// AssetClass down = repository.first(filter);
// if (down != null) {
// down.setSortOrder(currentPosition);
// bulk.add(down);
// }
//
// // update in transaction
// repository.bulkUpdate(bulk);
// for now, just increase the sort order on the selected item
repository.update(up);
}
/**
* Increase the ranking value, effectively moving the item down in the list.
*/
public void moveClassUp(int id) {
AssetClass assetClass = repository.load(id);
Integer currentPosition = assetClass.getSortOrder();
if (currentPosition == null) currentPosition = 0;
int nextPosition = currentPosition - 1;
if (nextPosition < 0) return;
assetClass.setSortOrder(nextPosition);
repository.update(assetClass);
}
public boolean assignStockToAssetClass(String stockSymbol, Integer assetClassId) {
AssetClassStock link = AssetClassStock.create(assetClassId, stockSymbol);
AssetClassStockRepository repo = new AssetClassStockRepository(getContext());
boolean success = repo.insert(link);
if (!success) {
ExceptionHandler handler = new ExceptionHandler(getContext(), this);
handler.showMessage(getContext().getString(R.string.error));
}
return success;
}
/**
* Find asset allocation by id.
* @param childId Id of the class to find.
* @return asset class with the required id.
*/
public AssetClass findChild(int childId, AssetClass tree) {
AssetClass result = null;
if (childId == Constants.NOT_SET) {
return tree;
}
// iterate through all elements
Integer id = tree.getId();
if (id != null && id == childId) return tree;
for (AssetClass child : tree.getChildren()) {
result = findChild(childId, child);
if (result != null) break;
}
return result;
}
// Private.
/**
* Add Cash as a separate asset class that uses all the cash amounts from
* the investment accounts.
* @param assetAllocation Main asset allocation object.
*/
private void addCash(AssetClass assetAllocation) {
//String cashLocalizedName = getContext().getString(R.string.cash);
AssetClass cash = assetAllocation.getDirectChild(CashName);
if (cash == null) {
cash = createCashAssetClass();
assetAllocation.addChild(cash);
}
cash.setType(ItemType.Cash);
Money currentValue = calculateCashCurrentValue();
cash.setCurrentValue(currentValue);
}
private Money calculateCurrentAllocation(Money currentValue, Money portfolioValue) {
Money currentAllocation = currentValue
.multiply(100)
.divide(portfolioValue.toDouble(), Constants.DEFAULT_PRECISION);
return currentAllocation;
}
/**
* The magic happens here. Calculate all dependent variables.
* @param portfolioValue The total value of the portfolio, in base currency.
*/
private void calculateStatsFor(AssetClass item, Money portfolioValue) {
Money zero = MoneyFactory.fromDouble(0);
if (portfolioValue.toDouble() == 0) {
item.setValue(zero);
item.setCurrentAllocation(zero);
item.setCurrentValue(zero);
item.setDifference(zero);
return;
}
// Set Value
Money allocation = item.getAllocation();
// setValue = allocation * portfolioValue / 100;
Money setValue = calculateSetValue(portfolioValue, allocation);
item.setValue(setValue);
// Current value
Money currentValue = sumStockValues(item.getStocks());
item.setCurrentValue(currentValue);
// Current allocation.
Money currentAllocation = calculateCurrentAllocation(currentValue, portfolioValue);
item.setCurrentAllocation(currentAllocation);
// difference
Money difference = currentValue.subtract(setValue);
item.setDifference(difference);
}
private Money calculateSetValue(Money portfolioValue, Money allocation) {
Money value = portfolioValue
.multiply(allocation.toDouble())
.divide(100, Constants.DEFAULT_PRECISION);
return value;
}
private AssetClass createCashAssetClass() {
//String cashLocalizedName = getContext().getString(R.string.cash);
// Create a new asset class for Cash.
AssetClass cash = AssetClass.create(CashName);
AssetClassRepository repo = new AssetClassRepository(getContext());
repo.insert(cash);
return cash;
}
private Money calculateCashCurrentValue() {
// get all investment accounts, their currencies and cash balances.
AccountService accountService = new AccountService(getContext());
List<String> investmentAccounts = new ArrayList<>();
investmentAccounts.add(AccountTypes.INVESTMENT.toString());
int destinationCurrency = mCurrencyService.getBaseCurrencyId();
List<Account> accounts = accountService.loadAccounts(false, false, investmentAccounts);
Money sum = MoneyFactory.fromDouble(0);
// Get the balances in base currency.
for (Account account : accounts) {
int sourceCurrency = account.getCurrencyId();
Money amountInBase = mCurrencyService.doCurrencyExchange(destinationCurrency,
account.getInitialBalance(), sourceCurrency);
sum = sum.add(amountInBase);
}
return sum;
}
private Cursor loadData() {
Cursor c = repository.openCursor(null, null, null, AssetClass.PARENTID);
return c;
}
private HashMap<Integer, AssetClass> loadMap(Cursor c) {
HashMap<Integer, AssetClass> result = new HashMap<>();
if (c == null) return result;
while (c.moveToNext()) {
AssetClass ac = AssetClass.from(c);
result.put(ac.getId(), ac);
}
// c.close();
return result;
}
private List<AssetClass> assignChildren(HashMap<Integer, AssetClass> map) {
List<AssetClass> children = new ArrayList<>();
// Iterate through all the allocations.
for (AssetClass ac : map.values()) {
Integer parentId = ac.getParentId();
if (parentId != null && parentId != Constants.NOT_SET) {
// add child elements to their parents based on the Id field.
AssetClass parent = map.get(parentId);
// delete any orphans
if (parent == null) {
deleteAllocation(ac.getId());
continue;
}
parent.setType(ItemType.Group);
parent.addChild(ac);
} else {
// this is one of the root elements
children.add(ac);
}
}
return children;
}
private List<AssetClass> loadStocks(List<AssetClass> allocation) {
// iterate
for (AssetClass ac : allocation) {
// if element has no children, load related stocks
if (ac.getChildren().size() == 0) {
loadStocks(ac);
} else {
loadStocks(ac.getChildren());
}
}
return allocation;
}
private void loadStocks(AssetClass assetClass) {
if (assetClass.getChildren().size() > 0) {
// Group. Load values for child elements.
for (AssetClass child : assetClass.getChildren()) {
loadStocks(child);
}
} else {
// No child elements. This is the actual allocation. Load value from linked stocks.
assetClass.setType(ItemType.Allocation);
// load stock links
AssetClassStockRepository linkRepo = new AssetClassStockRepository(getContext());
List<AssetClassStock> links = linkRepo.loadForClass(assetClass.getId());
assetClass.setStockLinks(links);
if (assetClass.getStockLinks().size() == 0) return;
int size = assetClass.getStockLinks().size();
String[] symbols = new String[size];
for (int i = 0; i < size; i++) {
AssetClassStock link = assetClass.getStockLinks().get(i);
symbols[i] = link.getStockSymbol();
}
StockRepository stockRepo = new StockRepository(getContext());
List<Stock> stocks = stockRepo.loadForSymbols(symbols);
assetClass.setStocks(stocks);
}
}
private Money calculateCurrentValue(List<AssetClass> allocations) {
Money result = MoneyFactory.fromDouble(0);
for (AssetClass ac : allocations) {
Money itemValue;
ItemType type = ac.getType();
switch (type) {
case Group:
// Group. Calculate for children.
itemValue = calculateCurrentValue(ac.getChildren());
break;
case Allocation:
// Allocation. get value of all stocks.
itemValue = sumStockValues(ac.getStocks());
break;
case Cash:
itemValue = ac.getCurrentValue();
break;
default:
ExceptionHandler handler = new ExceptionHandler(getContext());
handler.showMessage("encountered an item with no type set!");
itemValue = MoneyFactory.fromDouble(0);
break;
}
ac.setCurrentValue(itemValue);
result = result.add(itemValue);
}
return result;
}
/**
* Calculate all dependent statistics for allocation records.
* @param allocation Asset Class/Allocation record
* @param portfolioValue Total value of the portfolio. Used to calculate the current allocation.
*/
private void calculateStats(AssetClass allocation, Money portfolioValue) {
List<AssetClass> children = allocation.getChildren();
Money setAllocation, currentAllocation, setValue, currentValue, difference;
ItemType type = allocation.getType();
switch (type) {
case Group:
// Group. Calculate stats for children *and* get the summaries here.
for (AssetClass child : children) {
// find edge node
calculateStats(child, portfolioValue);
}
// Allocation
setAllocation = getAllocationSum(children);
allocation.setAllocation(setAllocation);
// Value
setValue = getValueSum(children);
allocation.setValue(setValue);
// current allocation
currentAllocation = getCurrentAllocationSum(children);
allocation.setCurrentAllocation(currentAllocation);
// current value
currentValue = getCurrentValueSum(children);
allocation.setCurrentValue(currentValue);
// difference
difference = getDifferenceSum(children);
allocation.setDifference(difference);
break;
case Allocation:
// Allocation. Calculate all stats.
calculateStatsFor(allocation, portfolioValue);
break;
case Cash:
// Allocation. Set manually.
// Set Value
setValue = calculateSetValue(portfolioValue, allocation.getAllocation());
allocation.setValue(setValue);
// Current Allocation
currentAllocation = calculateCurrentAllocation(allocation.getCurrentValue(), portfolioValue);
allocation.setCurrentAllocation(currentAllocation);
// Current Value. Calculated when Cash created/loaded.
// Difference
difference = allocation.getCurrentValue().subtract(setValue);
allocation.setDifference(difference);
break;
}
}
private Money getAllocationSum(List<AssetClass> group) {
List<Money> allocations = new ArrayList<>();
for (AssetClass item : group) {
allocations.add(item.getAllocation());
}
Money sum = MoneyFactory.fromString("0");
for (Money allocation : allocations) {
sum = sum.add(allocation);
}
return sum;
}
private Money getValueSum(List<AssetClass> group) {
Money sum = MoneyFactory.fromDouble(0);
for (AssetClass item : group) {
sum = sum.add(item.getValue());
}
return sum;
}
private Money getCurrentAllocationSum(List<AssetClass> group) {
Money sum = MoneyFactory.fromDouble(0);
for (AssetClass item : group) {
sum = sum.add(item.getCurrentAllocation());
}
return sum;
}
private Money getCurrentValueSum(List<AssetClass> group) {
Money sum = MoneyFactory.fromDouble(0);
for (AssetClass item : group) {
sum = sum.add(item.getCurrentValue());
}
return sum;
}
private Money getDifferenceSum(List<AssetClass> group) {
Money sum = MoneyFactory.fromDouble(0);
for (AssetClass item : group) {
sum = sum.add(item.getDifference());
}
return sum;
}
private void sortChildren(AssetClass allocation) {
// sort immediate children
Collections.sort(allocation.getChildren(), new Comparator<AssetClass>() {
@Override
public int compare(AssetClass lhs, AssetClass rhs) {
return lhs.getSortOrder().compareTo(rhs.getSortOrder());
}
});
// sort grandchildren recursively
for (AssetClass child : allocation.getChildren()) {
sortChildren(child);
}
}
private Money sumStockValues(List<Stock> stocks) {
Money sum = MoneyFactory.fromDouble(0);
int baseCurrencyId = mCurrencyService.getBaseCurrencyId();
for (Stock stock : stocks) {
// convert the stock value to the base currency.
int accountId = stock.getHeldAt();
int currencyId = getAccountCurrencyId(accountId);
Money value = mCurrencyService.doCurrencyExchange(baseCurrencyId, stock.getValue(), currencyId);
sum = sum.add(value);
}
return sum;
}
private Integer getAccountCurrencyId(int accountId) {
if (mAccountCurrencies == null) {
mAccountCurrencies = new HashMap<>();
}
if (mAccountCurrencies.containsKey(accountId)) {
return mAccountCurrencies.get(accountId);
}
// else load
AccountRepository repo = new AccountRepository(getContext());
Integer currencyId = repo.loadCurrencyIdFor(accountId);
mAccountCurrencies.put(accountId, currencyId);
return currencyId;
}
}