New asset pricing scheme (take 2)

Orders are back to having "amount" and "price".
(No more "unitPrice" or "wantAmount").

Order "amount" is expressed in terms of asset with highest
assetID.
"price" is expressed in (lowest-assetID)/(highest-assetID).

Given an order with two assets, e.g. QORA (0) and GOLD (31),
"amount" is in GOLD (31), "price" is in QORA/GOLD (0/31).

Order's "fulfilled" is in the same asset as "amount".

Yet more tests and debugging.

For simplicity's sake, the change to HSQLDB repository is
assumed to take place when 'new' pricing switch also
occurs.

Don't forget to change "newAssetPricingTimestamp" in
blockchain config JSON file.
This commit is contained in:
catbref
2019-04-10 07:18:50 +01:00
parent 1b45ee85e7
commit a5e963911d
22 changed files with 1262 additions and 758 deletions

View File

@@ -20,9 +20,9 @@ public class AggregatedOrder {
this.orderData = orderData;
}
@XmlElement(name = "unitPrice")
public BigDecimal getUnitPrice() {
return this.orderData.getUnitPrice();
@XmlElement(name = "price")
public BigDecimal getPrice() {
return this.orderData.getPrice();
}
@XmlElement(name = "unfulfilled")

View File

@@ -18,27 +18,38 @@ import org.qora.data.asset.TradeData;
import org.qora.repository.AssetRepository;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import com.google.common.hash.HashCode;
import org.qora.utils.Base58;
public class Order {
/** BigDecimal scale for representing unit price in asset orders. */
public static final int BD_PRICE_SCALE = 38;
/** BigDecimal scale for representing unit price in asset orders in storage context. */
public static final int BD_PRICE_STORAGE_SCALE = BD_PRICE_SCALE + 10;
private static final Logger LOGGER = LogManager.getLogger(Order.class);
// Properties
private Repository repository;
private OrderData orderData;
// Used quite a bit
private final boolean isOurOrderNewPricing;
private final long haveAssetId;
private final long wantAssetId;
/** Cache of price-pair units e.g. QORA/GOLD, but use getPricePair() instead! */
private String cachedPricePair;
/** Cache of have-asset data - but use getHaveAsset() instead! */
AssetData cachedHaveAssetData;
/** Cache of want-asset data - but use getWantAsset() instead! */
AssetData cachedWantAssetData;
// Constructors
public Order(Repository repository, OrderData orderData) {
this.repository = repository;
this.orderData = orderData;
this.isOurOrderNewPricing = this.orderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
this.haveAssetId = this.orderData.getHaveAssetId();
this.wantAssetId = this.orderData.getWantAssetId();
}
// Getters/Setters
@@ -66,128 +77,200 @@ public class Order {
}
/**
* Returns want-asset granularity/unit-size given price.
* Returns granularity/batch-size of matched-amount, given price, so that return-amount is valid size.
* <p>
* @param theirPrice
* @return unit price of want asset
* If matched-amount of matched-asset is traded when two orders match,
* then the corresponding return-amount of the other (return) asset needs to be either
* an integer, if return-asset is indivisible,
* or to the nearest 0.00000001 if return-asset is divisible.
* <p>
* @return granularity of matched-amount
*/
public static BigDecimal calculateAmountGranularity(AssetData haveAssetData, AssetData wantAssetData, OrderData theirOrderData) {
public static BigDecimal calculateAmountGranularity(boolean isAmountAssetDivisible, boolean isReturnAssetDivisible, BigDecimal price) {
// Multiplier to scale BigDecimal fractional amounts into integer domain
BigInteger multiplier = BigInteger.valueOf(1_0000_0000L);
// Calculate the minimum increment at which I can buy using greatest-common-divisor
BigInteger haveAmount;
BigInteger wantAmount;
if (theirOrderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp()) {
// "new" pricing scheme
haveAmount = theirOrderData.getAmount().movePointRight(8).toBigInteger();
wantAmount = theirOrderData.getWantAmount().movePointRight(8).toBigInteger();
} else {
// legacy "old" behaviour
haveAmount = multiplier; // 1 unit (* multiplier)
wantAmount = theirOrderData.getUnitPrice().movePointRight(8).toBigInteger();
}
// Calculate the minimum increment for matched-amount using greatest-common-divisor
BigInteger returnAmount = multiplier; // 1 unit (* multiplier)
BigInteger matchedAmount = price.movePointRight(8).toBigInteger();
BigInteger gcd = haveAmount.gcd(wantAmount);
haveAmount = haveAmount.divide(gcd);
wantAmount = wantAmount.divide(gcd);
BigInteger gcd = returnAmount.gcd(matchedAmount);
returnAmount = returnAmount.divide(gcd);
matchedAmount = matchedAmount.divide(gcd);
// Calculate GCD in combination with divisibility
if (wantAssetData.getIsDivisible())
haveAmount = haveAmount.multiply(multiplier);
if (isAmountAssetDivisible)
returnAmount = returnAmount.multiply(multiplier);
if (haveAssetData.getIsDivisible())
wantAmount = wantAmount.multiply(multiplier);
if (isReturnAssetDivisible)
matchedAmount = matchedAmount.multiply(multiplier);
gcd = haveAmount.gcd(wantAmount);
gcd = returnAmount.gcd(matchedAmount);
// Calculate the granularity at which we have to buy
BigDecimal granularity = new BigDecimal(haveAmount.divide(gcd));
if (wantAssetData.getIsDivisible())
BigDecimal granularity = new BigDecimal(returnAmount.divide(gcd));
if (isAmountAssetDivisible)
granularity = granularity.movePointLeft(8);
// Return
return granularity;
}
/**
* Returns price-pair in string form.
* <p>
* e.g. <tt>"QORA/GOLD"</tt>
*/
public String getPricePair() throws DataException {
if (cachedPricePair == null)
calcPricePair();
return cachedPricePair;
}
/** Calculate price pair. (e.g. QORA/GOLD)
* <p>
* Under 'new' pricing scheme, lowest-assetID asset is first,
* so if QORA has assetID 0 and GOLD has assetID 10, then
* the pricing pair is QORA/GOLD.
* <p>
* This means the "amount" fields are expressed in terms
* of the higher-assetID asset. (e.g. GOLD)
*/
private void calcPricePair() throws DataException {
AssetData haveAssetData = getHaveAsset();
AssetData wantAssetData = getWantAsset();
if (isOurOrderNewPricing && haveAssetId > wantAssetId)
cachedPricePair = wantAssetData.getName() + "/" + haveAssetData.getName();
else
cachedPricePair = haveAssetData.getName() + "/" + wantAssetData.getName();
}
private BigDecimal calcHaveAssetCommittment() {
BigDecimal committedCost = this.orderData.getAmount();
// If 'new' pricing and "amount" is in want-asset then we need to convert
if (isOurOrderNewPricing && haveAssetId < wantAssetId)
committedCost = committedCost.multiply(this.orderData.getPrice()).setScale(8, RoundingMode.HALF_UP);
return committedCost;
}
// Navigation
public List<TradeData> getTrades() throws DataException {
return this.repository.getAssetRepository().getOrdersTrades(this.orderData.getOrderId());
}
public AssetData getHaveAsset() throws DataException {
if (cachedHaveAssetData == null)
cachedHaveAssetData = this.repository.getAssetRepository().fromAssetId(haveAssetId);
return cachedHaveAssetData;
}
public AssetData getWantAsset() throws DataException {
if (cachedWantAssetData == null)
cachedWantAssetData = this.repository.getAssetRepository().fromAssetId(wantAssetId);
return cachedWantAssetData;
}
/**
* Returns AssetData for asset in effect for "amount" field.
* <p>
* For 'old' pricing, this is the have-asset.<br>
* For 'new' pricing, this is the asset with highest assetID.
*/
public AssetData getAmountAsset() throws DataException {
if (isOurOrderNewPricing && wantAssetId > haveAssetId)
return getWantAsset();
else
return getHaveAsset();
}
/**
* Returns AssetData for other (return) asset traded.
* <p>
* For 'old' pricing, this is the want-asset.<br>
* For 'new' pricing, this is the asset with lowest assetID.
*/
public AssetData getReturnAsset() throws DataException {
if (isOurOrderNewPricing && haveAssetId < wantAssetId)
return getHaveAsset();
else
return getWantAsset();
}
// Processing
private void logOrder(String orderPrefix, boolean isMatchingNotInitial, OrderData orderData) throws DataException {
private void logOrder(String orderPrefix, boolean isOurOrder, OrderData orderData) throws DataException {
// Avoid calculations if possible
if (LOGGER.getLevel().isMoreSpecificThan(Level.DEBUG))
return;
final String weThey = isMatchingNotInitial ? "They" : "We";
final String weThey = isOurOrder ? "We" : "They";
final String ourTheir = isOurOrder ? "Our" : "Their";
AssetData haveAssetData = this.repository.getAssetRepository().fromAssetId(orderData.getHaveAssetId());
AssetData wantAssetData = this.repository.getAssetRepository().fromAssetId(orderData.getWantAssetId());
// NOTE: the following values are specific to passed orderData, not the same as class instance values!
LOGGER.debug(String.format("%s %s", orderPrefix, HashCode.fromBytes(orderData.getOrderId()).toString()));
final boolean isOrderNewAssetPricing = orderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
LOGGER.trace(String.format("%s have: %s %s", weThey, orderData.getAmount().stripTrailingZeros().toPlainString(), haveAssetData.getName()));
final long haveAssetId = orderData.getHaveAssetId();
final long wantAssetId = orderData.getWantAssetId();
LOGGER.trace(String.format("%s want at least %s %s per %s (minimum %s %s total)", weThey,
orderData.getUnitPrice().toPlainString(), wantAssetData.getName(), haveAssetData.getName(),
orderData.getWantAmount().stripTrailingZeros().toPlainString(), wantAssetData.getName()));
final AssetData haveAssetData = this.repository.getAssetRepository().fromAssetId(haveAssetId);
final AssetData wantAssetData = this.repository.getAssetRepository().fromAssetId(wantAssetId);
final long amountAssetId = (isOurOrderNewPricing && wantAssetId > haveAssetId) ? wantAssetId : haveAssetId;
final long returnAssetId = (isOurOrderNewPricing && haveAssetId < wantAssetId) ? haveAssetId : wantAssetId;
final AssetData amountAssetData = this.repository.getAssetRepository().fromAssetId(amountAssetId);
final AssetData returnAssetData = this.repository.getAssetRepository().fromAssetId(returnAssetId);
LOGGER.debug(String.format("%s %s", orderPrefix, Base58.encode(orderData.getOrderId())));
LOGGER.trace(String.format("%s have %s, want %s. '%s' pricing scheme.", weThey, haveAssetData.getName(), wantAssetData.getName(), isOrderNewAssetPricing ? "new" : "old"));
LOGGER.trace(String.format("%s amount: %s (ordered) - %s (fulfilled) = %s %s left", ourTheir,
orderData.getAmount().stripTrailingZeros().toPlainString(),
orderData.getFulfilled().stripTrailingZeros().toPlainString(),
Order.getAmountLeft(orderData).stripTrailingZeros().toPlainString(),
amountAssetData.getName()));
BigDecimal maxReturnAmount = Order.getAmountLeft(orderData).multiply(orderData.getPrice()).setScale(8, RoundingMode.HALF_UP);
LOGGER.trace(String.format("%s price: %s %s (%s %s tradable)", ourTheir,
orderData.getPrice().toPlainString(), getPricePair(),
maxReturnAmount.stripTrailingZeros().toPlainString(), returnAssetData.getName()));
}
public void process() throws DataException {
AssetRepository assetRepository = this.repository.getAssetRepository();
long haveAssetId = this.orderData.getHaveAssetId();
AssetData haveAssetData = assetRepository.fromAssetId(haveAssetId);
long wantAssetId = this.orderData.getWantAssetId();
AssetData wantAssetData = assetRepository.fromAssetId(wantAssetId);
AssetData haveAssetData = getHaveAsset();
AssetData wantAssetData = getWantAsset();
// Subtract asset from creator
/** The asset while working out amount that matches. */
AssetData matchingAssetData = isOurOrderNewPricing ? getAmountAsset() : wantAssetData;
/** The return asset traded if trade completes. */
AssetData returnAssetData = isOurOrderNewPricing ? getReturnAsset() : haveAssetData;
// Subtract have-asset from creator
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).subtract(this.orderData.getAmount()));
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).subtract(this.calcHaveAssetCommittment()));
// Save this order into repository so it's available for matching, possibly by itself
this.repository.getAssetRepository().save(this.orderData);
/*
* Our order example ("old"):
*
* haveAssetId=[GOLD], amount=10,000, wantAssetId=[QORA], price=0.002
*
* This translates to "we have 10,000 GOLD and want to buy QORA at a price of 0.002 QORA per GOLD"
*
* So if our order matched, we'd end up with 10,000 * 0.002 = 20 QORA, essentially costing 1/0.002 = 500 GOLD each.
*
* So 500 GOLD [each] is our (selling) unit price and want-amount is 20 QORA.
*
* Another example (showing representation error and hence move to "new" pricing):
* haveAssetId=[QORA], amount=24, wantAssetId=[GOLD], price=0.08333333
* unit price: 12.00000048 GOLD, want-amount: 1.9999992 GOLD
*/
logOrder("Processing our order", true, this.orderData);
/*
* Our order example ("new"):
*
* haveAssetId=[GOLD], amount=10,000, wantAssetId=0 (QORA), want-amount=20
*
* This translates to "we have 10,000 GOLD and want to buy 20 QORA"
*
* So if our order matched, we'd end up with 20 QORA, essentially costing 10,000 / 20 = 500 GOLD each.
*
* So 500 GOLD [each] is our (selling) unit price and want-amount is 20 QORA.
*
* Another example:
* haveAssetId=[QORA], amount=24, wantAssetId=[GOLD], want-amount=2
* unit price: 12.00000000 GOLD, want-amount: 2.00000000 GOLD
*/
logOrder("Processing our order", false, this.orderData);
// Fetch corresponding open orders that might potentially match, hence reversed want/have assetId args.
// Fetch corresponding open orders that might potentially match, hence reversed want/have assetIDs.
// Returned orders are sorted with lowest "price" first.
List<OrderData> orders = assetRepository.getOpenOrders(wantAssetId, haveAssetId);
List<OrderData> orders = assetRepository.getOpenOrdersForTrading(wantAssetId, haveAssetId, isOurOrderNewPricing ? this.orderData.getPrice() : null);
LOGGER.trace("Open orders fetched from repository: " + orders.size());
if (orders.isEmpty())
@@ -195,146 +278,145 @@ public class Order {
// Attempt to match orders
BigDecimal ourUnitPrice = this.orderData.getUnitPrice();
LOGGER.trace(String.format("Our minimum price: %s %s per %s", ourUnitPrice.toPlainString(), wantAssetData.getName(), haveAssetData.getName()));
/*
* Potential matching order example ("old"):
*
* Our order:
* haveAssetId=[GOLD], wantAssetId=0 (QORA), amount=40 (GOLD), price=486 (QORA/GOLD)
* This translates to "we have 40 GOLD and want QORA at a price of 486 QORA per GOLD"
* If our order matched, we'd end up with 40 * 486 = 19,440 QORA.
*
* Their order:
* haveAssetId=0 (QORA), wantAssetId=[GOLD], amount=20,000 (QORA), price=0.00205761 (GOLD/QORA)
* This translates to "they have 20,000 QORA and want GOLD at a price of 0.00205761 GOLD per QORA"
*
* Their price, converted into 'our' units of QORA/GOLD, is: 1 / 0.00205761 = 486.00074844 QORA/GOLD.
* This is better than our requested 486 QORA/GOLD so this order matches.
*
* Using their price, we end up with 40 * 486.00074844 = 19440.02993760 QORA. They end up with 40 GOLD.
*
* If their order had 19,440 QORA left, only 19,440 * 0.00205761 = 39.99993840 GOLD would be traded.
*/
/*
* Potential matching order example ("new"):
*
* Our order:
* haveAssetId=[GOLD], wantAssetId=0 (QORA), amount=40 (GOLD), price=486 (QORA/GOLD)
* This translates to "we have 40 GOLD and want QORA at a price of 486 QORA per GOLD"
* If our order matched, we'd end up with 19,440 QORA at a cost of 19,440 / 486 = 40 GOLD.
*
* Their order:
* haveAssetId=0 (QORA), wantAssetId=[GOLD], amount=40 (GOLD), price=486.00074844 (QORA/GOLD)
* This translates to "they have QORA and want GOLD at a price of 486.00074844 QORA per GOLD"
*
* Their price is better than our requested 486 QORA/GOLD so this order matches.
*
* Using their price, we end up with 40 * 486.00074844 = 19440.02993760 QORA. They end up with 40 GOLD.
*
* If their order only had 36 GOLD left, only 36 * 486.00074844 = 17496.02694384 QORA would be traded.
*/
BigDecimal ourPrice = this.orderData.getPrice();
for (OrderData theirOrderData : orders) {
logOrder("Considering order", true, theirOrderData);
logOrder("Considering order", false, theirOrderData);
/*
* Potential matching order example ("old"):
*
* haveAssetId=0 (QORA), amount=40, wantAssetId=[GOLD], price=486
*
* This translates to "we have 40 QORA and want to buy GOLD at a price of 486 GOLD per QORA"
*
* So if their order matched, they'd end up with 40 * 486 = 19,440 GOLD, essentially costing 1/486 = 0.00205761 QORA each.
*
* So 0.00205761 QORA [each] is their unit price and maximum amount is 19,440 GOLD.
*/
// Not used:
// boolean isTheirOrderNewAssetPricing = theirOrderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
/*
* Potential matching order example ("new"):
*
* haveAssetId=0 (QORA), amount=40, wantAssetId=[GOLD], price=19,440
*
* This translates to "we have 40 QORA and want to buy 19,440 GOLD"
*
* So if their order matched, they'd end up with 19,440 GOLD, essentially costing 40 / 19,440 = 0.00205761 QORA each.
*
* So 0.00205761 QORA [each] is their unit price and maximum amount is 19,440 GOLD.
*/
// Determine their order price
BigDecimal theirPrice;
boolean isTheirOrderNewAssetPricing = theirOrderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
BigDecimal theirBuyingPrice = BigDecimal.ONE.setScale(isTheirOrderNewAssetPricing ? Order.BD_PRICE_STORAGE_SCALE : 8).divide(theirOrderData.getUnitPrice(), RoundingMode.DOWN);
LOGGER.trace(String.format("Their price: %s %s per %s", theirBuyingPrice.toPlainString(), wantAssetData.getName(), haveAssetData.getName()));
if (isOurOrderNewPricing) {
// Pricing units are the same way round for both orders, so no conversion needed.
// Orders under 'old' pricing have been converted during repository update.
theirPrice = theirOrderData.getPrice();
LOGGER.trace(String.format("Their price: %s %s", theirPrice.toPlainString(), getPricePair()));
} else {
// If our order is 'old' pricing then all other existing orders must be 'old' pricing too
// Their order pricing will be inverted, so convert
theirPrice = BigDecimal.ONE.setScale(8).divide(theirOrderData.getPrice(), RoundingMode.DOWN);
LOGGER.trace(String.format("Their price: %s %s per %s", theirPrice.toPlainString(), wantAssetData.getName(), haveAssetData.getName()));
}
// If their buyingPrice is less than what we're willing to accept then we're done as prices only get worse as we iterate through list of orders
if (theirBuyingPrice.compareTo(ourUnitPrice) < 0)
if (theirPrice.compareTo(ourPrice) < 0)
break;
// Calculate how many want-asset we could buy at their price
BigDecimal ourMaxWantAmount = this.getAmountLeft().multiply(theirBuyingPrice).setScale(8, RoundingMode.DOWN);
LOGGER.trace("ourMaxWantAmount (max we could buy at their price): " + ourMaxWantAmount.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName());
// Calculate how much we could buy at their price.
BigDecimal ourMaxAmount;
if (isOurOrderNewPricing)
// In 'new' pricing scheme, "amount" is expressed in terms of asset with highest assetID
ourMaxAmount = this.getAmountLeft();
else
// In 'old' pricing scheme, "amount" is expressed in terms of our want-asset.
ourMaxAmount = this.getAmountLeft().multiply(theirPrice).setScale(8, RoundingMode.DOWN);
LOGGER.trace("ourMaxAmount (max we could trade at their price): " + ourMaxAmount.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
if (isTheirOrderNewAssetPricing) {
ourMaxWantAmount = ourMaxWantAmount.max(this.getAmountLeft().divide(theirOrderData.getUnitPrice(), RoundingMode.DOWN).setScale(8, RoundingMode.DOWN));
LOGGER.trace("ourMaxWantAmount (max we could buy at their price) using inverted calculation: " + ourMaxWantAmount.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName());
}
// How many want-asset is remaining available in their order. (have-asset amount from their perspective).
BigDecimal theirWantAmountLeft = Order.getAmountLeft(theirOrderData);
LOGGER.trace("theirWantAmountLeft (max amount remaining in their order): " + theirWantAmountLeft.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName());
// How much is remaining available in their order.
BigDecimal theirAmountLeft = Order.getAmountLeft(theirOrderData);
LOGGER.trace("theirAmountLeft (max amount remaining in their order): " + theirAmountLeft.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// So matchable want-asset amount is the minimum of above two values
BigDecimal matchedWantAmount = ourMaxWantAmount.min(theirWantAmountLeft);
LOGGER.trace("matchedWantAmount: " + matchedWantAmount.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName());
BigDecimal matchedAmount = ourMaxAmount.min(theirAmountLeft);
LOGGER.trace("matchedAmount: " + matchedAmount.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// If we can't buy anything then try another order
if (matchedWantAmount.compareTo(BigDecimal.ZERO) <= 0)
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
continue;
// Calculate want-amount granularity, based on price and both assets' divisibility, so that have-amount traded is a valid amount (integer or to 8 d.p.)
BigDecimal wantGranularity = calculateAmountGranularity(haveAssetData, wantAssetData, theirOrderData);
LOGGER.trace("wantGranularity (want-asset amount granularity): " + wantGranularity.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName());
// Calculate amount granularity, based on price and both assets' divisibility, so that return-amount traded is a valid value (integer or to 8 d.p.)
BigDecimal granularity = calculateAmountGranularity(matchingAssetData.getIsDivisible(), returnAssetData.getIsDivisible(), theirOrderData.getPrice());
LOGGER.trace("granularity (amount granularity): " + granularity.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// Reduce matched amount (if need be) to fit granularity
matchedWantAmount = matchedWantAmount.subtract(matchedWantAmount.remainder(wantGranularity));
LOGGER.trace("matchedWantAmount adjusted for granularity: " + matchedWantAmount.stripTrailingZeros().toPlainString() + " " + wantAssetData.getName());
matchedAmount = matchedAmount.subtract(matchedAmount.remainder(granularity));
LOGGER.trace("matchedAmount adjusted for granularity: " + matchedAmount.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// If we can't buy anything then try another order
if (matchedWantAmount.compareTo(BigDecimal.ZERO) <= 0)
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
continue;
// Safety checks
if (matchedWantAmount.compareTo(Order.getAmountLeft(theirOrderData)) > 0) {
// Safety check
if (!matchingAssetData.getIsDivisible() && matchedAmount.stripTrailingZeros().scale() > 0) {
Account participant = new PublicKeyAccount(this.repository, theirOrderData.getCreatorPublicKey());
String message = String.format("Refusing to trade more %s then requested %s [assetId %d] for %s",
matchedWantAmount.toPlainString(), Order.getAmountLeft(theirOrderData).toPlainString(), wantAssetId, participant.getAddress());
LOGGER.error(message);
throw new DataException(message);
}
if (!wantAssetData.getIsDivisible() && matchedWantAmount.stripTrailingZeros().scale() > 0) {
Account participant = new PublicKeyAccount(this.repository, theirOrderData.getCreatorPublicKey());
String message = String.format("Refusing to trade fractional %s [indivisible assetId %d] for %s",
matchedWantAmount.toPlainString(), wantAssetId, participant.getAddress());
String message = String.format("Refusing to trade fractional %s [indivisible assetID %d] for %s",
matchedAmount.toPlainString(), matchingAssetData.getAssetId(), participant.getAddress());
LOGGER.error(message);
throw new DataException(message);
}
// Trade can go ahead!
// Calculate the total cost to us, in have-asset, based on their price
BigDecimal haveAmountTraded;
// Calculate the total cost to us, in return-asset, based on their price
BigDecimal returnAmountTraded = matchedAmount.multiply(theirOrderData.getPrice()).setScale(8, RoundingMode.DOWN);
LOGGER.trace("returnAmountTraded: " + returnAmountTraded.stripTrailingZeros().toPlainString() + " " + returnAssetData.getName());
if (isTheirOrderNewAssetPricing) {
BigDecimal theirTruncatedPrice = theirBuyingPrice.setScale(Order.BD_PRICE_SCALE, RoundingMode.DOWN);
BigDecimal ourTruncatedPrice = ourUnitPrice.setScale(Order.BD_PRICE_SCALE, RoundingMode.DOWN);
// Safety check
if (theirTruncatedPrice.compareTo(ourTruncatedPrice) < 0) {
String message = String.format("Refusing to trade at worse price %s than our minimum of %s",
theirTruncatedPrice.toPlainString(), ourTruncatedPrice.toPlainString(), creator.getAddress());
LOGGER.error(message);
throw new DataException(message);
}
haveAmountTraded = matchedWantAmount.divide(theirTruncatedPrice, RoundingMode.DOWN).setScale(8, RoundingMode.DOWN);
} else {
haveAmountTraded = matchedWantAmount.multiply(theirOrderData.getUnitPrice()).setScale(8, RoundingMode.DOWN);
}
LOGGER.trace("haveAmountTraded: " + haveAmountTraded.stripTrailingZeros().toPlainString() + " " + haveAssetData.getName());
// Safety checks
if (haveAmountTraded.compareTo(this.getAmountLeft()) > 0) {
String message = String.format("Refusing to trade more %s then requested %s [assetId %d] for %s",
haveAmountTraded.toPlainString(), this.getAmountLeft().toPlainString(), haveAssetId, creator.getAddress());
LOGGER.error(message);
throw new DataException(message);
}
if (!haveAssetData.getIsDivisible() && haveAmountTraded.stripTrailingZeros().scale() > 0) {
String message = String.format("Refusing to trade fractional %s [indivisible assetId %d] for %s",
haveAmountTraded.toPlainString(), haveAssetId, creator.getAddress());
// Safety check
if (!returnAssetData.getIsDivisible() && returnAmountTraded.stripTrailingZeros().scale() > 0) {
String message = String.format("Refusing to trade fractional %s [indivisible assetID %d] for %s",
returnAmountTraded.toPlainString(), returnAssetData.getAssetId(), creator.getAddress());
LOGGER.error(message);
throw new DataException(message);
}
// Construct trade
TradeData tradeData = new TradeData(this.orderData.getOrderId(), theirOrderData.getOrderId(), matchedWantAmount, haveAmountTraded,
BigDecimal tradedWantAmount = (isOurOrderNewPricing && wantAssetId < haveAssetId) ? returnAmountTraded : matchedAmount;
BigDecimal tradedHaveAmount = (isOurOrderNewPricing && haveAssetId > wantAssetId) ? matchedAmount : returnAmountTraded;
TradeData tradeData = new TradeData(this.orderData.getOrderId(), theirOrderData.getOrderId(), tradedWantAmount, tradedHaveAmount,
this.orderData.getTimestamp());
// Process trade, updating corresponding orders in repository
Trade trade = new Trade(this.repository, tradeData);
trade.process();
// Update our order in terms of fulfilment, etc. but do not save into repository as that's handled by Trade above
this.orderData.setFulfilled(this.orderData.getFulfilled().add(haveAmountTraded));
LOGGER.trace("Updated our order's fulfilled amount to: " + this.orderData.getFulfilled().stripTrailingZeros().toPlainString() + " " + haveAssetData.getName());
LOGGER.trace("Our order's amount remaining: " + this.getAmountLeft().stripTrailingZeros().toPlainString() + " " + haveAssetData.getName());
BigDecimal amountFulfilled = isOurOrderNewPricing ? matchedAmount : returnAmountTraded;
this.orderData.setFulfilled(this.orderData.getFulfilled().add(amountFulfilled));
LOGGER.trace("Updated our order's fulfilled amount to: " + this.orderData.getFulfilled().stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
LOGGER.trace("Our order's amount remaining: " + this.getAmountLeft().stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// Continue on to process other open orders if we still have amount left to match
if (this.getAmountLeft().compareTo(BigDecimal.ZERO) <= 0)
@@ -355,8 +437,9 @@ public class Order {
// Return asset to creator
long haveAssetId = this.orderData.getHaveAssetId();
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).add(this.orderData.getAmount()));
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).add(this.calcHaveAssetCommittment()));
}
// This is called by CancelOrderTransaction so that an Order can no longer trade

View File

@@ -1,7 +1,10 @@
package org.qora.asset;
import java.math.BigDecimal;
import org.qora.account.Account;
import org.qora.account.PublicKeyAccount;
import org.qora.block.BlockChain;
import org.qora.data.asset.OrderData;
import org.qora.data.asset.TradeData;
import org.qora.repository.AssetRepository;
@@ -31,14 +34,19 @@ public class Trade {
// Update corresponding Orders on both sides of trade
OrderData initiatingOrder = assetRepository.fromOrderId(this.tradeData.getInitiator());
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().add(tradeData.getInitiatorAmount()));
OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget());
// Under 'new' pricing scheme, "amount" and "fulfilled" are the same asset for both orders
boolean isNewPricing = initiatingOrder.getTimestamp() > BlockChain.getInstance().getNewAssetPricingTimestamp();
BigDecimal newPricingAmount = (initiatingOrder.getHaveAssetId() < initiatingOrder.getWantAssetId()) ? this.tradeData.getTargetAmount() : this.tradeData.getInitiatorAmount();
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().add(isNewPricing ? newPricingAmount : tradeData.getInitiatorAmount()));
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
// Set isClosed to true if isFulfilled now true
initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled());
assetRepository.save(initiatingOrder);
OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget());
targetOrder.setFulfilled(targetOrder.getFulfilled().add(tradeData.getTargetAmount()));
targetOrder.setFulfilled(targetOrder.getFulfilled().add(isNewPricing ? newPricingAmount : tradeData.getTargetAmount()));
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
// Set isClosed to true if isFulfilled now true
targetOrder.setIsClosed(targetOrder.getIsFulfilled());
@@ -59,14 +67,19 @@ public class Trade {
// Revert corresponding Orders on both sides of trade
OrderData initiatingOrder = assetRepository.fromOrderId(this.tradeData.getInitiator());
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().subtract(tradeData.getInitiatorAmount()));
OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget());
// Under 'new' pricing scheme, "amount" and "fulfilled" are the same asset for both orders
boolean isNewPricing = initiatingOrder.getTimestamp() > BlockChain.getInstance().getNewAssetPricingTimestamp();
BigDecimal newPricingAmount = (initiatingOrder.getHaveAssetId() < initiatingOrder.getWantAssetId()) ? this.tradeData.getTargetAmount() : this.tradeData.getInitiatorAmount();
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().subtract(isNewPricing ? newPricingAmount : tradeData.getInitiatorAmount()));
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
// Set isClosed to false if isFulfilled now false
initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled());
assetRepository.save(initiatingOrder);
OrderData targetOrder = assetRepository.fromOrderId(this.tradeData.getTarget());
targetOrder.setFulfilled(targetOrder.getFulfilled().subtract(tradeData.getTargetAmount()));
targetOrder.setFulfilled(targetOrder.getFulfilled().subtract(isNewPricing ? newPricingAmount : tradeData.getTargetAmount()));
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
// Set isClosed to false if isFulfilled now false
targetOrder.setIsClosed(targetOrder.getIsFulfilled());

View File

@@ -4,7 +4,6 @@ import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -22,17 +21,13 @@ public class OrderData implements Comparable<OrderData> {
@Schema(description = "asset wanted to receive by order creator")
private long wantAssetId;
@Schema(description = "amount of \"have\" asset to trade")
@Schema(description = "amount of highest-assetID asset to trade")
private BigDecimal amount;
@Schema(name = "return", description = "amount of \"want\" asset to receive")
@XmlElement(name = "return")
private BigDecimal wantAmount;
@Schema(description = "price in lowest-assetID asset / highest-assetID asset")
private BigDecimal price;
@Schema(description = "amount of \"want\" asset to receive per unit of \"have\" asset traded")
private BigDecimal unitPrice;
@Schema(description = "how much \"have\" asset has traded")
@Schema(description = "how much of \"amount\" has traded")
private BigDecimal fulfilled;
private long timestamp;
@@ -49,24 +44,22 @@ public class OrderData implements Comparable<OrderData> {
protected OrderData() {
}
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal fulfilled, BigDecimal wantAmount,
BigDecimal unitPrice, long timestamp, boolean isClosed, boolean isFulfilled) {
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal fulfilled, BigDecimal price, long timestamp, boolean isClosed, boolean isFulfilled) {
this.orderId = orderId;
this.creatorPublicKey = creatorPublicKey;
this.haveAssetId = haveAssetId;
this.wantAssetId = wantAssetId;
this.amount = amount;
this.fulfilled = fulfilled;
this.wantAmount = wantAmount;
this.unitPrice = unitPrice;
this.price = price;
this.timestamp = timestamp;
this.isClosed = isClosed;
this.isFulfilled = isFulfilled;
}
/** Constructs OrderData using typical deserialized network data */
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal wantAmount, BigDecimal unitPrice, long timestamp) {
this(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, BigDecimal.ZERO.setScale(8), wantAmount, unitPrice, timestamp, false, false);
public OrderData(byte[] orderId, byte[] creatorPublicKey, long haveAssetId, long wantAssetId, BigDecimal amount, BigDecimal price, long timestamp) {
this(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, BigDecimal.ZERO.setScale(8), price, timestamp, false, false);
}
// Getters/setters
@@ -99,12 +92,8 @@ public class OrderData implements Comparable<OrderData> {
this.fulfilled = fulfilled;
}
public BigDecimal getWantAmount() {
return this.wantAmount;
}
public BigDecimal getUnitPrice() {
return this.unitPrice;
public BigDecimal getPrice() {
return this.price;
}
public long getTimestamp() {
@@ -130,7 +119,7 @@ public class OrderData implements Comparable<OrderData> {
@Override
public int compareTo(OrderData orderData) {
// Compare using prices
return this.unitPrice.compareTo(orderData.getUnitPrice());
return this.price.compareTo(orderData.getPrice());
}
}

View File

@@ -20,11 +20,10 @@ public class CreateAssetOrderTransactionData extends TransactionData {
private long haveAssetId;
@Schema(description = "ID of asset wanted to receive by order creator", example = "0")
private long wantAssetId;
@Schema(description = "amount of \"have\" asset to trade")
@Schema(description = "amount of highest-assetID asset to trade")
private BigDecimal amount;
@Schema(name = "return", description = "amount of \"want\" asset to receive")
@XmlElement(name = "return")
private BigDecimal wantAmount;
@Schema(description = "price in lowest-assetID asset / highest-assetID asset")
private BigDecimal price;
// Constructors
@@ -34,18 +33,18 @@ public class CreateAssetOrderTransactionData extends TransactionData {
}
public CreateAssetOrderTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, long haveAssetId, long wantAssetId,
BigDecimal amount, BigDecimal wantAmount, BigDecimal fee, byte[] signature) {
BigDecimal amount, BigDecimal price, BigDecimal fee, byte[] signature) {
super(TransactionType.CREATE_ASSET_ORDER, timestamp, txGroupId, reference, creatorPublicKey, fee, signature);
this.haveAssetId = haveAssetId;
this.wantAssetId = wantAssetId;
this.amount = amount;
this.wantAmount = wantAmount;
this.price = price;
}
public CreateAssetOrderTransactionData(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, long haveAssetId, long wantAssetId,
BigDecimal amount, BigDecimal wantAmount, BigDecimal fee) {
this(timestamp, txGroupId, reference, creatorPublicKey, haveAssetId, wantAssetId, amount, wantAmount, fee, null);
BigDecimal amount, BigDecimal price, BigDecimal fee) {
this(timestamp, txGroupId, reference, creatorPublicKey, haveAssetId, wantAssetId, amount, price, fee, null);
}
// Getters/Setters
@@ -62,8 +61,8 @@ public class CreateAssetOrderTransactionData extends TransactionData {
return this.amount;
}
public BigDecimal getWantAmount() {
return this.wantAmount;
public BigDecimal getPrice() {
return this.price;
}
// Re-expose creatorPublicKey for this transaction type for JAXB

View File

@@ -1,5 +1,6 @@
package org.qora.repository;
import java.math.BigDecimal;
import java.util.List;
import org.qora.data.asset.AssetData;
@@ -44,6 +45,8 @@ public interface AssetRepository {
return getOpenOrders(haveAssetId, wantAssetId, null, null, null);
}
public List<OrderData> getOpenOrdersForTrading(long haveAssetId, long wantAssetId, BigDecimal minimumPrice) throws DataException;
public List<OrderData> getAggregatedOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset, Boolean reverse) throws DataException;
public List<OrderData> getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled, Integer limit, Integer offset, Boolean reverse)

View File

@@ -192,7 +192,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
@Override
public OrderData fromOrderId(byte[] orderId) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute(
"SELECT creator, have_asset_id, want_asset_id, amount, fulfilled, want_amount, unit_price, ordered, is_closed, is_fulfilled FROM AssetOrders WHERE asset_order_id = ?",
"SELECT creator, have_asset_id, want_asset_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled FROM AssetOrders WHERE asset_order_id = ?",
orderId)) {
if (resultSet == null)
return null;
@@ -202,13 +202,12 @@ public class HSQLDBAssetRepository implements AssetRepository {
long wantAssetId = resultSet.getLong(3);
BigDecimal amount = resultSet.getBigDecimal(4);
BigDecimal fulfilled = resultSet.getBigDecimal(5);
BigDecimal wantAmount = resultSet.getBigDecimal(6);
BigDecimal unitPrice = resultSet.getBigDecimal(7);
long timestamp = resultSet.getTimestamp(8, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
boolean isClosed = resultSet.getBoolean(9);
boolean isFulfilled = resultSet.getBoolean(10);
BigDecimal price = resultSet.getBigDecimal(6);
long timestamp = resultSet.getTimestamp(7, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
boolean isClosed = resultSet.getBoolean(8);
boolean isFulfilled = resultSet.getBoolean(9);
return new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, wantAmount, unitPrice, timestamp, isClosed, isFulfilled);
return new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled, price, timestamp, isClosed, isFulfilled);
} catch (SQLException e) {
throw new DataException("Unable to fetch asset order from repository", e);
}
@@ -217,8 +216,8 @@ public class HSQLDBAssetRepository implements AssetRepository {
@Override
public List<OrderData> getOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset,
Boolean reverse) throws DataException {
String sql = "SELECT creator, asset_order_id, amount, fulfilled, want_amount, unit_price, ordered FROM AssetOrders "
+ "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE ORDER BY unit_price";
String sql = "SELECT creator, asset_order_id, amount, fulfilled, price, ordered FROM AssetOrders "
+ "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE ORDER BY price";
if (reverse != null && reverse)
sql += " DESC";
sql += ", ordered";
@@ -237,14 +236,13 @@ public class HSQLDBAssetRepository implements AssetRepository {
byte[] orderId = resultSet.getBytes(2);
BigDecimal amount = resultSet.getBigDecimal(3);
BigDecimal fulfilled = resultSet.getBigDecimal(4);
BigDecimal wantAmount = resultSet.getBigDecimal(5);
BigDecimal unitPrice = resultSet.getBigDecimal(6);
long timestamp = resultSet.getTimestamp(7, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
BigDecimal price = resultSet.getBigDecimal(5);
long timestamp = resultSet.getTimestamp(6, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
boolean isClosed = false;
boolean isFulfilled = false;
OrderData order = new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled,
wantAmount, unitPrice, timestamp, isClosed, isFulfilled);
price, timestamp, isClosed, isFulfilled);
orders.add(order);
} while (resultSet.next());
@@ -254,11 +252,53 @@ public class HSQLDBAssetRepository implements AssetRepository {
}
}
@Override
public List<OrderData> getOpenOrdersForTrading(long haveAssetId, long wantAssetId, BigDecimal minimumPrice) throws DataException {
Object[] bindParams;
String sql = "SELECT creator, asset_order_id, amount, fulfilled, price, ordered FROM AssetOrders "
+ "WHERE have_asset_id = ? AND want_asset_id = ? AND NOT is_closed AND NOT is_fulfilled ";
if (minimumPrice != null) {
sql += "AND price >= ? ";
bindParams = new Object[] {haveAssetId, wantAssetId, minimumPrice};
} else {
bindParams = new Object[] {haveAssetId, wantAssetId};
}
sql += "ORDER BY price DESC, ordered";
List<OrderData> orders = new ArrayList<OrderData>();
try (ResultSet resultSet = this.repository.checkedExecute(sql, bindParams)) {
if (resultSet == null)
return orders;
do {
byte[] creatorPublicKey = resultSet.getBytes(1);
byte[] orderId = resultSet.getBytes(2);
BigDecimal amount = resultSet.getBigDecimal(3);
BigDecimal fulfilled = resultSet.getBigDecimal(4);
BigDecimal price = resultSet.getBigDecimal(5);
long timestamp = resultSet.getTimestamp(6, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
boolean isClosed = false;
boolean isFulfilled = false;
OrderData order = new OrderData(orderId, creatorPublicKey, haveAssetId, wantAssetId, amount, fulfilled,
price, timestamp, isClosed, isFulfilled);
orders.add(order);
} while (resultSet.next());
return orders;
} catch (SQLException e) {
throw new DataException("Unable to fetch open asset orders for trading from repository", e);
}
}
@Override
public List<OrderData> getAggregatedOpenOrders(long haveAssetId, long wantAssetId, Integer limit, Integer offset,
Boolean reverse) throws DataException {
String sql = "SELECT unit_price, SUM(amount - fulfilled), MAX(ordered) FROM AssetOrders "
+ "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE GROUP BY unit_price ORDER BY unit_price";
String sql = "SELECT price, SUM(amount - fulfilled), MAX(ordered) FROM AssetOrders "
+ "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE GROUP BY price ORDER BY price";
if (reverse != null && reverse)
sql += " DESC";
sql += HSQLDBRepository.limitOffsetSql(limit, offset);
@@ -270,12 +310,12 @@ public class HSQLDBAssetRepository implements AssetRepository {
return orders;
do {
BigDecimal unitPrice = resultSet.getBigDecimal(1);
BigDecimal price = resultSet.getBigDecimal(1);
BigDecimal totalUnfulfilled = resultSet.getBigDecimal(2);
long timestamp = resultSet.getTimestamp(3).getTime();
OrderData order = new OrderData(null, null, haveAssetId, wantAssetId, totalUnfulfilled, BigDecimal.ZERO,
BigDecimal.ZERO, unitPrice, timestamp, false, false);
price, timestamp, false, false);
orders.add(order);
} while (resultSet.next());
@@ -288,7 +328,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
@Override
public List<OrderData> getAccountsOrders(byte[] publicKey, Boolean optIsClosed, Boolean optIsFulfilled,
Integer limit, Integer offset, Boolean reverse) throws DataException {
String sql = "SELECT asset_order_id, have_asset_id, want_asset_id, amount, fulfilled, want_amount, unit_price, ordered, is_closed, is_fulfilled "
String sql = "SELECT asset_order_id, have_asset_id, want_asset_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled "
+ "FROM AssetOrders WHERE creator = ?";
if (optIsClosed != null)
sql += " AND is_closed = " + (optIsClosed ? "TRUE" : "FALSE");
@@ -311,14 +351,13 @@ public class HSQLDBAssetRepository implements AssetRepository {
long wantAssetId = resultSet.getLong(3);
BigDecimal amount = resultSet.getBigDecimal(4);
BigDecimal fulfilled = resultSet.getBigDecimal(5);
BigDecimal wantAmount = resultSet.getBigDecimal(6);
BigDecimal unitPrice = resultSet.getBigDecimal(7);
long timestamp = resultSet.getTimestamp(8, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
boolean isClosed = resultSet.getBoolean(9);
boolean isFulfilled = resultSet.getBoolean(10);
BigDecimal price = resultSet.getBigDecimal(6);
long timestamp = resultSet.getTimestamp(7, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
boolean isClosed = resultSet.getBoolean(8);
boolean isFulfilled = resultSet.getBoolean(9);
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, wantAmount,
unitPrice, timestamp, isClosed, isFulfilled);
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled,
price, timestamp, isClosed, isFulfilled);
orders.add(order);
} while (resultSet.next());
@@ -331,7 +370,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
@Override
public List<OrderData> getAccountsOrders(byte[] publicKey, long haveAssetId, long wantAssetId, Boolean optIsClosed,
Boolean optIsFulfilled, Integer limit, Integer offset, Boolean reverse) throws DataException {
String sql = "SELECT asset_order_id, amount, fulfilled, want_amount, unit_price, ordered, is_closed, is_fulfilled "
String sql = "SELECT asset_order_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled "
+ "FROM AssetOrders WHERE creator = ? AND have_asset_id = ? AND want_asset_id = ?";
if (optIsClosed != null)
sql += " AND is_closed = " + (optIsClosed ? "TRUE" : "FALSE");
@@ -352,14 +391,13 @@ public class HSQLDBAssetRepository implements AssetRepository {
byte[] orderId = resultSet.getBytes(1);
BigDecimal amount = resultSet.getBigDecimal(2);
BigDecimal fulfilled = resultSet.getBigDecimal(3);
BigDecimal wantAmount = resultSet.getBigDecimal(4);
BigDecimal unitPrice = resultSet.getBigDecimal(5);
long timestamp = resultSet.getTimestamp(6, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
boolean isClosed = resultSet.getBoolean(7);
boolean isFulfilled = resultSet.getBoolean(8);
BigDecimal price = resultSet.getBigDecimal(4);
long timestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
boolean isClosed = resultSet.getBoolean(6);
boolean isFulfilled = resultSet.getBoolean(7);
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled, wantAmount,
unitPrice, timestamp, isClosed, isFulfilled);
OrderData order = new OrderData(orderId, publicKey, haveAssetId, wantAssetId, amount, fulfilled,
price, timestamp, isClosed, isFulfilled);
orders.add(order);
} while (resultSet.next());
@@ -376,8 +414,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
saveHelper.bind("asset_order_id", orderData.getOrderId()).bind("creator", orderData.getCreatorPublicKey())
.bind("have_asset_id", orderData.getHaveAssetId()).bind("want_asset_id", orderData.getWantAssetId())
.bind("amount", orderData.getAmount()).bind("fulfilled", orderData.getFulfilled())
.bind("want_amount", orderData.getWantAmount()).bind("unit_price", orderData.getUnitPrice())
.bind("ordered", new Timestamp(orderData.getTimestamp()))
.bind("price", orderData.getPrice()).bind("ordered", new Timestamp(orderData.getTimestamp()))
.bind("is_closed", orderData.getIsClosed()).bind("is_fulfilled", orderData.getIsFulfilled());
try {

View File

@@ -7,6 +7,7 @@ import java.sql.Statement;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qora.block.BlockChain;
public class HSQLDBDatabaseUpdates {
@@ -655,6 +656,35 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("ALTER TABLE CreateAssetOrderTransactions ALTER COLUMN price RENAME TO want_amount");
break;
case 42:
// New asset pricing #2
/*
* Use "price" (discard want-amount) but enforce pricing units in one direction
* to avoid all the reciprocal and round issues.
*/
stmt.execute("ALTER TABLE CreateAssetOrderTransactions ALTER COLUMN want_amount RENAME TO price");
stmt.execute("ALTER TABLE AssetOrders DROP COLUMN want_amount");
stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN unit_price RENAME TO price");
stmt.execute("ALTER TABLE AssetOrders ALTER COLUMN price QoraAmount");
/*
* Normalize any 'old' orders to 'new' pricing.
* We must do this so that requesting open orders can be sorted by price.
*/
// Make sure new asset pricing timestamp (used below) is UTC
stmt.execute("SET TIME ZONE INTERVAL '0:00' HOUR TO MINUTE");
// Normalize amount/fulfilled to asset with highest assetID, BEFORE price correction
stmt.execute("UPDATE AssetOrders SET amount = amount * price, fulfilled = fulfilled * price "
+ "WHERE ordered < timestamp(" + BlockChain.getInstance().getNewAssetPricingTimestamp() + ") "
+ "AND have_asset_id < want_asset_id");
// Normalize price into lowest-assetID/highest-assetID price-pair, e.g. QORA/asset100
// Note: HSQLDB uses BigDecimal's dividend.divide(divisor, RoundingMode.DOWN) too
stmt.execute("UPDATE AssetOrders SET price = CAST(1 AS QoraAmount) / price "
+ "WHERE ordered < timestamp(" + BlockChain.getInstance().getNewAssetPricingTimestamp() + ") "
+ "AND have_asset_id < want_asset_id");
// Revert time zone change above
stmt.execute("SET TIME ZONE LOCAL");
break;
default:
// nothing to do
return false;

View File

@@ -18,16 +18,16 @@ public class HSQLDBCreateAssetOrderTransactionRepository extends HSQLDBTransacti
TransactionData fromBase(long timestamp, int txGroupId, byte[] reference, byte[] creatorPublicKey, BigDecimal fee, byte[] signature) throws DataException {
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT have_asset_id, amount, want_asset_id, want_amount FROM CreateAssetOrderTransactions WHERE signature = ?", signature)) {
.checkedExecute("SELECT have_asset_id, amount, want_asset_id, price FROM CreateAssetOrderTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
long haveAssetId = resultSet.getLong(1);
BigDecimal amount = resultSet.getBigDecimal(2);
long wantAssetId = resultSet.getLong(3);
BigDecimal wantAmount = resultSet.getBigDecimal(4);
BigDecimal price = resultSet.getBigDecimal(4);
return new CreateAssetOrderTransactionData(timestamp, txGroupId, reference, creatorPublicKey, haveAssetId, wantAssetId, amount, wantAmount, fee, signature);
return new CreateAssetOrderTransactionData(timestamp, txGroupId, reference, creatorPublicKey, haveAssetId, wantAssetId, amount, price, fee, signature);
} catch (SQLException e) {
throw new DataException("Unable to fetch create order transaction from repository", e);
}
@@ -41,7 +41,7 @@ public class HSQLDBCreateAssetOrderTransactionRepository extends HSQLDBTransacti
saveHelper.bind("signature", createOrderTransactionData.getSignature()).bind("creator", createOrderTransactionData.getCreatorPublicKey())
.bind("have_asset_id", createOrderTransactionData.getHaveAssetId()).bind("amount", createOrderTransactionData.getAmount())
.bind("want_asset_id", createOrderTransactionData.getWantAssetId()).bind("want_amount", createOrderTransactionData.getWantAmount());
.bind("want_asset_id", createOrderTransactionData.getWantAssetId()).bind("price", createOrderTransactionData.getPrice());
try {
saveHelper.execute(this.repository);

View File

@@ -1,7 +1,6 @@
package org.qora.transaction;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -83,7 +82,7 @@ public class CreateAssetOrderTransaction extends Transaction {
return ValidationResult.NEGATIVE_AMOUNT;
// Check price is positive
if (createOrderTransactionData.getWantAmount().compareTo(BigDecimal.ZERO) <= 0)
if (createOrderTransactionData.getPrice().compareTo(BigDecimal.ZERO) <= 0)
return ValidationResult.NEGATIVE_PRICE;
// Check fee is positive
@@ -104,19 +103,63 @@ public class CreateAssetOrderTransaction extends Transaction {
Account creator = getCreator();
// Check reference is correct
if (!Arrays.equals(creator.getLastReference(), createOrderTransactionData.getReference()))
return ValidationResult.INVALID_REFERENCE;
boolean isNewPricing = createOrderTransactionData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
BigDecimal committedCost;
BigDecimal maxOtherAmount;
if (isNewPricing) {
/*
* This is different under "new" pricing scheme as "amount" might be either have-asset or want-asset,
* whichever has the highest assetID.
*
* e.g. with assetID 11 "GOLD":
* haveAssetId: 0 (QORA), wantAssetId: 11 (GOLD), amount: 123 (GOLD), price: 400 (QORA/GOLD)
* stake 49200 QORA, return 123 GOLD
*
* haveAssetId: 11 (GOLD), wantAssetId: 0 (QORA), amount: 123 (GOLD), price: 400 (QORA/GOLD)
* stake 123 GOLD, return 49200 QORA
*/
boolean isAmountWantAsset = haveAssetId < wantAssetId;
if (isAmountWantAsset) {
// have/commit 49200 QORA, want/return 123 GOLD
committedCost = createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getPrice());
maxOtherAmount = createOrderTransactionData.getAmount();
} else {
// have/commit 123 GOLD, want/return 49200 QORA
committedCost = createOrderTransactionData.getAmount();
maxOtherAmount = createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getPrice());
}
} else {
/*
* Under "old" pricing scheme, "amount" is always have-asset and price is always want-per-have.
*
* e.g. with assetID 11 "GOLD":
* haveAssetId: 0 (QORA), wantAssetId: 11 (GOLD), amount: 49200 (QORA), price: 0.00250000 (GOLD/QORA)
* haveAssetId: 11 (GOLD), wantAssetId: 0 (QORA), amount: 123 (GOLD), price: 400 (QORA/GOLD)
*/
committedCost = createOrderTransactionData.getAmount();
maxOtherAmount = createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getPrice());
}
// Check amount is integer if amount's asset is not divisible
if (!haveAssetData.getIsDivisible() && committedCost.stripTrailingZeros().scale() > 0)
return ValidationResult.INVALID_AMOUNT;
// Check total return from fulfilled order would be integer if return's asset is not divisible
if (!wantAssetData.getIsDivisible() && maxOtherAmount.stripTrailingZeros().scale() > 0)
return ValidationResult.INVALID_RETURN;
// Check order creator has enough asset balance AFTER removing fee, in case asset is QORA
// If asset is QORA then we need to check amount + fee in one go
if (haveAssetId == Asset.QORA) {
// Check creator has enough funds for amount + fee in QORA
if (creator.getConfirmedBalance(Asset.QORA).compareTo(createOrderTransactionData.getAmount().add(createOrderTransactionData.getFee())) < 0)
if (creator.getConfirmedBalance(Asset.QORA).compareTo(committedCost.add(createOrderTransactionData.getFee())) < 0)
return ValidationResult.NO_BALANCE;
} else {
// Check creator has enough funds for amount in whatever asset
if (creator.getConfirmedBalance(haveAssetId).compareTo(createOrderTransactionData.getAmount()) < 0)
if (creator.getConfirmedBalance(haveAssetId).compareTo(committedCost) < 0)
return ValidationResult.NO_BALANCE;
// Check creator has enough funds for fee in QORA
@@ -126,21 +169,9 @@ public class CreateAssetOrderTransaction extends Transaction {
return ValidationResult.NO_BALANCE;
}
// Check "have" amount is integer if "have" asset is not divisible
if (!haveAssetData.getIsDivisible() && createOrderTransactionData.getAmount().stripTrailingZeros().scale() > 0)
return ValidationResult.INVALID_AMOUNT;
// Check total return from fulfilled order would be integer if "want" asset is not divisible
if (createOrderTransactionData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp()) {
// "new" asset pricing
if (!wantAssetData.getIsDivisible() && createOrderTransactionData.getWantAmount().stripTrailingZeros().scale() > 0)
return ValidationResult.INVALID_RETURN;
} else {
// "old" asset pricing
if (!wantAssetData.getIsDivisible()
&& createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getWantAmount()).stripTrailingZeros().scale() > 0)
return ValidationResult.INVALID_RETURN;
}
// Check reference is correct
if (!Arrays.equals(creator.getLastReference(), createOrderTransactionData.getReference()))
return ValidationResult.INVALID_REFERENCE;
return ValidationResult.OK;
}
@@ -161,22 +192,9 @@ public class CreateAssetOrderTransaction extends Transaction {
// Order Id is transaction's signature
byte[] orderId = createOrderTransactionData.getSignature();
BigDecimal wantAmount;
BigDecimal unitPrice;
if (createOrderTransactionData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp()) {
// "new" asset pricing: want-amount provided, unit price to be calculated
wantAmount = createOrderTransactionData.getWantAmount();
unitPrice = wantAmount.setScale(Order.BD_PRICE_STORAGE_SCALE).divide(createOrderTransactionData.getAmount().setScale(Order.BD_PRICE_STORAGE_SCALE), RoundingMode.DOWN);
} else {
// "old" asset pricing: selling unit price provided, want-amount to be calculated
wantAmount = createOrderTransactionData.getAmount().multiply(createOrderTransactionData.getWantAmount());
unitPrice = createOrderTransactionData.getWantAmount(); // getWantAmount() was getPrice() in the "old" pricing scheme
}
// Process the order itself
OrderData orderData = new OrderData(orderId, createOrderTransactionData.getCreatorPublicKey(), createOrderTransactionData.getHaveAssetId(),
createOrderTransactionData.getWantAssetId(), createOrderTransactionData.getAmount(), wantAmount, unitPrice,
createOrderTransactionData.getWantAssetId(), createOrderTransactionData.getAmount(), createOrderTransactionData.getPrice(),
createOrderTransactionData.getTimestamp());
new Order(this.repository, orderData).process();

View File

@@ -35,7 +35,7 @@ public class CreateAssetOrderTransactionTransformer extends TransactionTransform
layout.add("ID of asset of offer", TransformationType.LONG);
layout.add("ID of asset wanted", TransformationType.LONG);
layout.add("amount of asset on offer", TransformationType.ASSET_QUANTITY);
layout.add("amount of wanted asset", TransformationType.ASSET_QUANTITY);
layout.add("trade price in (lowest-assetID asset)/(highest-assetID asset)", TransformationType.ASSET_QUANTITY);
layout.add("fee", TransformationType.AMOUNT);
layout.add("signature", TransformationType.SIGNATURE);
}
@@ -58,7 +58,6 @@ public class CreateAssetOrderTransactionTransformer extends TransactionTransform
BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer, AMOUNT_LENGTH);
// Under "new" asset pricing, this is actually the want-amount
BigDecimal price = Serialization.deserializeBigDecimal(byteBuffer, AMOUNT_LENGTH);
BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
@@ -87,8 +86,7 @@ public class CreateAssetOrderTransactionTransformer extends TransactionTransform
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getAmount(), AMOUNT_LENGTH);
// Under "new" asset pricing, this is actually the want-amount
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getWantAmount(), AMOUNT_LENGTH);
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getPrice(), AMOUNT_LENGTH);
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getFee());
@@ -128,7 +126,7 @@ public class CreateAssetOrderTransactionTransformer extends TransactionTransform
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getAmount(), AMOUNT_LENGTH);
// This is the crucial difference
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getWantAmount(), FEE_LENGTH);
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getPrice(), FEE_LENGTH);
Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getFee());