package org.cryptocoinpartners.util;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang.ObjectUtils;
import org.cryptocoinpartners.schema.Asset;
/**
* Class describing a set of currencies and all the cross rates between them.
*/
public class ListingsMatrix {
/**
* The map between the currencies and their order.
*/
private final ConcurrentHashMap<Asset, ConcurrentHashMap<Asset, Long>> listings;
/**
* Constructor with no currency. The ListingsMatrix constructed has no currency and no rates.
*/
public ListingsMatrix() {
listings = new ConcurrentHashMap<>();
}
/**
* Constructor with one currency. The ListingsMatrix has one currency with a 1.0 exchange rate to itself.
* @param ccy The currency.
*/
public ListingsMatrix(final Asset ccy) {
ArgumentChecker.notNull(ccy, "Asset");
listings = new ConcurrentHashMap<>();
listings.put(ccy, new ConcurrentHashMap<Asset, Long>());
listings.get(ccy).put(ccy, (long) (1.0 / ccy.getBasis()));
}
/**
* Constructor with an initial currency pair.
* @param ccy1 The first currency.
* @param ccy2 The second currency.
* @param rate TheListings rate between ccy1 and the ccy2. It is 1 ccy1 = rate * ccy2. The Listings matrix will be completed with the ccy2/ccy1 rate.
*/
public ListingsMatrix(final Asset ccy1, final Asset ccy2, final long rate) {
listings = new ConcurrentHashMap<>();
addAsset(ccy1, ccy2, rate);
}
/**
* Constructor from an existing ListingsMatrix. A new map and array are created.
* @param ListingsMatrix The ListingsMatrix.
*/
public ListingsMatrix(final ListingsMatrix ListingsMatrix) {
ArgumentChecker.notNull(ListingsMatrix, "ListingsMatrix");
listings = new ConcurrentHashMap<>(ListingsMatrix.listings);
}
/**
* Add a new currency to the Listings matrix.
* @param ccyToAdd The currency to add. Should not be in the Listings matrix already.
* @param ccyReference The reference currency used to compute the cross rates with the new currency. Should already be in the matrix, except if the matrix is empty.
* IF the Listings matrix is empty, the reference currency will be used as currency 0.
* @param rate TheListings rate between the new currency and the reference currency. It is 1 ccyToAdd = rate ccyReference. The Listings matrix will be completed using cross rate
* coherent with the data provided.
*/
public void addAsset(Asset ccyToAdd, Asset ccyReference, long rate) {
ArgumentChecker.notNull(ccyToAdd, "Asset to add to the Listings matrix should not be null");
ArgumentChecker.notNull(ccyReference, "Reference currency should not be null");
ArgumentChecker.isTrue(!ccyToAdd.equals(ccyReference), "Currencies should be different");
if ((listings.get(ccyReference) == null && rate != 0) || (listings.isEmpty() && rate != 0)) { // Listings Matrix is empty.
BigDecimal inverseRateBD = (((BigDecimal.valueOf(1.0 / (ccyReference.getBasis()))).divide(BigDecimal.valueOf(rate), ccyToAdd.getScale(),
RoundingMode.HALF_EVEN)).divide(BigDecimal.valueOf(ccyToAdd.getBasis())));
long inverseCrossRate = inverseRateBD.longValue();
listings.put(ccyReference, new ConcurrentHashMap<Asset, Long>());
listings.put(ccyToAdd, new ConcurrentHashMap<Asset, Long>());
listings.get(ccyToAdd).put(ccyReference, rate);
listings.get(ccyToAdd).put(ccyToAdd, (long) (1.0 / ccyToAdd.getBasis()));
listings.get(ccyReference).put(ccyToAdd, inverseCrossRate);
listings.get(ccyReference).put(ccyReference, (long) (1.0 / ccyReference.getBasis()));
} else if (rate != 0) {
// ArgumentChecker.isTrue(listings.containsKey(ccyReference), " {} not in the Listings matrix", ccyReference);
//ArgumentChecker.isTrue(!listings.containsKey(ccyToAdd), "New currency {} already in the Listings matrix", ccyToAdd);
Iterator<Asset> lit = listings.keySet().iterator();
while (lit.hasNext()) {
Asset ccy = lit.next();
if (!ccyToAdd.equals(ccy)) {
long inverseCrossRate = 0;
long crossRate = 0;
// new matrix create of rates that is _currenciesLookup (size) x _currenciesLookup.sise()
// loop over each of the quote currencies and get the cross rate, converting to th the baiss of the new curency
// if (listings.get(ccyReference)==null)
// listings.put(ccyReference, value)
if (listings.get(ccyReference).get(ccy) == null) {
BigDecimal inverseRateBD = (((BigDecimal.valueOf(1.0 / (ccyReference.getBasis()))).divide(BigDecimal.valueOf(rate),
ccyToAdd.getScale(), RoundingMode.HALF_EVEN)).divide(BigDecimal.valueOf(ccy.getBasis())));
inverseCrossRate = inverseRateBD.longValue();
//listings.put(ccyReference, new ConcurrentHashMap<Asset, Long>());
//listings.put(ccy, new ConcurrentHashMap<Asset, Long>());
listings.get(ccy).put(ccyReference, rate);
listings.get(ccy).put(ccy, (long) (1.0 / ccy.getBasis()));
listings.get(ccyReference).put(ccy, inverseCrossRate);
listings.get(ccyReference).put(ccyReference, (long) (1.0 / ccyReference.getBasis()));
}
// break;
crossRate = Math.round((rate * listings.get(ccyReference).get(ccy).longValue() * (ccyToAdd.getBasis())));
// get the rate for the
if (crossRate != 0) {
BigDecimal crossRateBD = BigDecimal.valueOf(crossRate);
// calculate the inverse by getting the basis of the currenct rate and setting the scale to ttha tof the quote currency.
BigDecimal inverseCrossRateBD = ((BigDecimal.valueOf(1.0 / (ccy.getBasis()))).divide(crossRateBD, ccyToAdd.getScale(),
RoundingMode.HALF_EVEN));
// divine the rate by the basis fo the currency to be added
inverseCrossRateBD = inverseCrossRateBD.divide(BigDecimal.valueOf(ccyToAdd.getBasis()));
inverseCrossRate = inverseCrossRateBD.longValue();
// update the base currecny vs the quote
if (listings.get(ccyToAdd) == null) {
listings.put(ccyToAdd, new ConcurrentHashMap<Asset, Long>());
}
}
if (this.listings.get(ccyToAdd) == null)
this.listings.put(ccyToAdd, new ConcurrentHashMap<Asset, Long>());
listings.get(ccyToAdd).put(ccy, Long.valueOf(crossRate));
if (this.listings.get(ccy) == null)
this.listings.put(ccy, new ConcurrentHashMap<Asset, Long>());
listings.get(ccy).put(ccyToAdd, Long.valueOf(inverseCrossRate));
}
}
listings.get(ccyToAdd).put(ccyToAdd, (long) (1.0 / ccyToAdd.getBasis()));
}
}
/**
* Return the exchange rate between two currencies.
* @param ccy1 The first currency.
* @param ccy2 The second currency.
* @return The exchange rate: 1.0 * ccy1 = x * ccy2.
*/
public long getRate(final Asset ccy1, final Asset ccy2) {
if (ccy1.equals(ccy2)) {
return (long) (1.0 / ccy1.getBasis());
}
final ConcurrentHashMap<Asset, Long> index1 = listings.get(ccy1);
final ConcurrentHashMap<Asset, Long> index2 = listings.get(ccy2);
ArgumentChecker.isTrue(listings.get(ccy1) != null, "Asset {} is not in the Listings Matrix", ccy1);
ArgumentChecker.isTrue(listings.get(ccy1).get(ccy2) != null, "Asset {} and {} not in the Listings Matrix", ccy1, ccy2);
return listings.get(ccy1).get(ccy2).longValue();
}
/**
* @param ccy1 The first currency
* @param ccy2 The second currency
* @return True if the matrix contains both currencies
*/
public boolean containsPair(final Asset ccy1, final Asset ccy2) {
return listings.containsKey(ccy1) && listings.containsKey(ccy2);
}
/**
* Reset the exchange rate of a given currency.
* @param ccyToUpdate The currency for which the exchange rats should be updated. Should be in the Listings matrix already.
* @param ccyReference The reference currency used to compute the cross rates with the new currency. Should already be in the matrix.
* @param rate TheListings rate between the new currency and the reference currency. It is 1.0 * ccyToAdd = rate * ccyReference. The Listings matrix will be changed for currency1
* using cross rate coherent with the data provided.
*/
public void updateRates(final Asset ccyToUpdate, final Asset ccyReference, final long rate) {
ArgumentChecker.isTrue(listings.get(ccyReference) != null, "Reference Asset not in the Listings matrix");
ArgumentChecker.isTrue(listings.get(ccyReference).get(ccyToUpdate) != null, "Asset to update not in the Listings matrix");
if (rate != 0) {
Iterator<Asset> lit = listings.keySet().iterator();
while (lit.hasNext()) {
Asset ccy = lit.next();
if (!ccyToUpdate.equals(ccy)) {
long inverseCrossRate = 0;
long crossRate = 0;
if ((this.listings.get(ccyReference) != null) && (((ConcurrentHashMap) this.listings.get(ccyReference)).get(ccy) != null)) {
crossRate = Math.round((rate * listings.get(ccyReference).get(ccy).longValue() * (ccyReference.getBasis())));
// get the rate for the
if (crossRate != 0) {
BigDecimal crossRateBD = BigDecimal.valueOf(crossRate);
// calculate the inverse by getting the basis of the currenct rate and setting the scale to ttha tof the quote currency.
BigDecimal inverseCrossRateBD = ((BigDecimal.valueOf(1.0 / (ccy.getBasis()))).divide(crossRateBD, ccyToUpdate.getScale(),
RoundingMode.HALF_EVEN));
// divine the rate by the basis fo the currency to be added
inverseCrossRateBD = inverseCrossRateBD.divide(BigDecimal.valueOf(ccyToUpdate.getBasis()));
inverseCrossRate = inverseCrossRateBD.longValue();
}
}
listings.get(ccyToUpdate).put(ccy, crossRate);
listings.get(ccy).put(ccyToUpdate, inverseCrossRate);
}
}
listings.get(ccyToUpdate).put(ccyToUpdate, (long) (1.0 / ccyToUpdate.getBasis()));
}
}
@Override
public String toString() {
return listings.keySet().toString() + " - ";
}
@Override
public int hashCode() {
return listings.hashCode();
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final ListingsMatrix other = (ListingsMatrix) obj;
if (!ObjectUtils.equals(listings.keySet(), other.listings.keySet())) {
return false;
}
return true;
}
}