From 3796ed5f81853205604b57f0a54d1fb6d004422f Mon Sep 17 00:00:00 2001 From: "Nelson R. Perez" Date: Fri, 17 Nov 2017 16:46:26 -0500 Subject: [PATCH] Updating the OrderBook class, adding #calculateObtainedQuote and #calculateRequiredBase methods --- .../java/cy/agorise/graphenej/LimitOrder.java | 9 +- .../java/cy/agorise/graphenej/OrderBook.java | 107 +++++++++++------- .../cy/agorise/graphenej/OrderBookTest.java | 64 +++++++++++ .../graphenej/api/GetLimitOrdersTest.java | 48 +------- 4 files changed, 141 insertions(+), 87 deletions(-) create mode 100644 graphenej/src/test/java/cy/agorise/graphenej/OrderBookTest.java diff --git a/graphenej/src/main/java/cy/agorise/graphenej/LimitOrder.java b/graphenej/src/main/java/cy/agorise/graphenej/LimitOrder.java index 8d26d76..50a5c33 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/LimitOrder.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/LimitOrder.java @@ -1,5 +1,6 @@ package cy.agorise.graphenej; +import com.google.common.primitives.UnsignedLong; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; @@ -28,7 +29,7 @@ public class LimitOrder extends GrapheneObject implements ByteSerializable { private String expiration; private UserAccount seller; - private long forSale; + private UnsignedLong forSale; private long deferredFee; private Price sellPrice; @@ -52,11 +53,11 @@ public class LimitOrder extends GrapheneObject implements ByteSerializable { this.seller = seller; } - public long getForSale() { + public UnsignedLong getForSale() { return forSale; } - public void setForSale(long forSale) { + public void setForSale(UnsignedLong forSale) { this.forSale = forSale; } @@ -127,7 +128,7 @@ public class LimitOrder extends GrapheneObject implements ByteSerializable { LimitOrder limitOrder = new LimitOrder(id); limitOrder.setExpiration(expiration); limitOrder.setSeller(seller); - limitOrder.setForSale(Long.parseLong(forSale)); + limitOrder.setForSale(UnsignedLong.valueOf(forSale)); limitOrder.setSellPrice(price); limitOrder.setDeferredFee(deferredFee); return limitOrder; diff --git a/graphenej/src/main/java/cy/agorise/graphenej/OrderBook.java b/graphenej/src/main/java/cy/agorise/graphenej/OrderBook.java index 2fceb6a..a2abb60 100644 --- a/graphenej/src/main/java/cy/agorise/graphenej/OrderBook.java +++ b/graphenej/src/main/java/cy/agorise/graphenej/OrderBook.java @@ -1,9 +1,7 @@ package cy.agorise.graphenej; -import com.google.common.math.DoubleMath; import com.google.common.primitives.UnsignedLong; -import java.math.RoundingMode; import java.util.List; import cy.agorise.graphenej.operations.LimitOrderCreateOperation; @@ -14,9 +12,9 @@ import cy.agorise.graphenej.operations.LimitOrderCreateOperation; * It also provides a handy method that should return the appropriate LimitOrderCreateOperation * object needed in case the user wants to perform market-priced operations. * - * It is important to keep the order book updated, ideally by listening to blockchain events, and calling the 'update' method. + * It is important to keep the order book updated, ideally by listening to blockchain events, + * and calling the 'update' method. * - * Created by nelson on 3/25/17. */ public class OrderBook { private List limitOrders; @@ -52,53 +50,86 @@ public class OrderBook { * @return An instance of the LimitOrderCreateOperation class, which is ready to be broadcasted. */ public LimitOrderCreateOperation exchange(UserAccount seller, Asset myBaseAsset, AssetAmount myQuoteAmount, int expiration){ - AssetAmount toSell = new AssetAmount(UnsignedLong.valueOf(calculateRequiredBase(myQuoteAmount)), myBaseAsset); + AssetAmount toSell = new AssetAmount(calculateRequiredBase(myQuoteAmount), myBaseAsset); AssetAmount toReceive = myQuoteAmount; LimitOrderCreateOperation buyOrder = new LimitOrderCreateOperation(seller, toSell, toReceive, expiration, true); return buyOrder; } + public LimitOrderCreateOperation exchange(UserAccount seller, AssetAmount baseAmount, Asset quoteAsset, int expiration){ + AssetAmount minToReceive = new AssetAmount(calculateObtainedQuote(baseAmount), quoteAsset); + return new LimitOrderCreateOperation(seller, baseAmount, minToReceive, expiration, true); + } + /** - * Given a specific amount of a desired asset, this method will calculate how much of the corresponding - * asset we need to offer to perform a successful transaction with the current order book. - * @param quoteAmount: The amount of the desired asset. - * @return: The minimum amount of the base asset that we need to give away + * Method that calculates the amount of an asset that we will obtain (the quote amount) if we trade + * a known fixed amount of the asset we already have (the base amount). + * + * @param baseAmount The fixed amount of the asset we have and want to sell + * @return The equivalent amount to receive in exchange of the base amount */ - public long calculateRequiredBase(AssetAmount quoteAmount){ - long totalBought = 0; - long totalSold = 0; - for(int i = 0; i < this.limitOrders.size() && totalBought < quoteAmount.getAmount().longValue(); i++){ - LimitOrder order = this.limitOrders.get(i); + public UnsignedLong calculateObtainedQuote(AssetAmount baseAmount){ + UnsignedLong myBase = baseAmount.getAmount(); + UnsignedLong obtainedQuote = UnsignedLong.ZERO; + for(int i = 0; i < limitOrders.size() && myBase.compareTo(UnsignedLong.ZERO) > 0; i++){ + LimitOrder order = limitOrders.get(i); - // If the base asset is the same as our quote asset, we have a match - if(order.getSellPrice().base.getAsset().getObjectId().equals(quoteAmount.getAsset().getObjectId())){ - // My quote amount, is the order's base amount - long orderAmount = order.getForSale(); + // Checking to make sure the order matches our needs + if(order.getSellPrice().quote.getAsset().equals(baseAmount.getAsset())){ + UnsignedLong orderBase = order.getSellPrice().base.getAmount(); + UnsignedLong orderQuote = order.getSellPrice().quote.getAmount(); + UnsignedLong availableBase = order.getForSale(); - // The amount of the quote asset we still need - long stillNeed = quoteAmount.getAmount().longValue() - totalBought; - - // If the offered amount is greater than what we still need, we exchange just what we need - if(orderAmount >= stillNeed) { - totalBought += stillNeed; - double additionalRatio = (double) stillNeed / (double) order.getSellPrice().base.getAmount().longValue(); - double additionalAmount = order.getSellPrice().quote.getAmount().longValue() * additionalRatio; - long longAdditional = DoubleMath.roundToLong(additionalAmount, RoundingMode.HALF_UP); - totalSold += longAdditional; + UnsignedLong myQuote = UnsignedLong.valueOf((long)(myBase.times(orderBase).doubleValue() / (orderQuote.doubleValue()))); + if(myQuote.compareTo(availableBase) > 0){ + // We consume this order entirely + // myBase = myBase - (for_sale) * (order_quote / order_base) + myBase = myBase.minus(availableBase.times(orderQuote).dividedBy(orderBase)); + // We need more than this order can offer us, but have to take in consideration how much there really is. + // (order base / order quote) x (available order base / order base) + UnsignedLong thisBatch = UnsignedLong.valueOf((long)(orderBase.times(availableBase).doubleValue() / orderQuote.times(orderBase).doubleValue())); + obtainedQuote = obtainedQuote.plus(thisBatch); }else{ - // If the offered amount is less than what we need, we exchange the whole order - totalBought += orderAmount; - - // The amount specified in the price ratio is not always all for sale. So in order to calculate - // the amount actually sold we have to do: - // actually_sold = for_sale * quote / base - double sellRatio = ((double) orderAmount) / ((double) order.getSellPrice().base.getAmount().longValue()); - - totalSold += Math.floor(order.getSellPrice().quote.getAmount().doubleValue() * sellRatio); + // This order consumes all our base asset + // obtained_quote = obtained_quote + (my base * order_base / order_quote) + obtainedQuote = obtainedQuote.plus(myBase.times(orderBase.dividedBy(orderQuote))); + myBase = UnsignedLong.ZERO; } } } - return totalSold; + return obtainedQuote; + } + + /** + * Method that calculates the amount of an asset that we will consume (the base amount) if we want to obtain + * a known fixed amount of another asset (the quote amount). + * @param quoteAmount The fixed amount of an asset that we want to obtain + * @return The amount of an asset we already have that will be consumed by the trade + */ + public UnsignedLong calculateRequiredBase(AssetAmount quoteAmount){ + UnsignedLong myQuote = quoteAmount.getAmount(); + UnsignedLong obtainedBase = UnsignedLong.ZERO; + for(int i = 0; i < limitOrders.size() && myQuote.compareTo(UnsignedLong.ZERO) > 0; i++){ + LimitOrder order = limitOrders.get(i); + + // Checking to make sure the order matches our needs + if(order.getSellPrice().base.getAsset().equals(quoteAmount.getAsset())){ + UnsignedLong orderBase = order.getSellPrice().base.getAmount(); + UnsignedLong orderQuote = order.getSellPrice().quote.getAmount(); + UnsignedLong forSale = order.getForSale(); + + if(forSale.compareTo(myQuote) > 0){ + // Found an order that fills our requirements + obtainedBase = obtainedBase.plus(UnsignedLong.valueOf((long) (myQuote.doubleValue() * orderQuote.doubleValue() / orderBase.doubleValue()))); + myQuote = UnsignedLong.ZERO; + }else{ + // Found an order that partially fills our needs + obtainedBase = obtainedBase.plus(UnsignedLong.valueOf((long) (forSale.doubleValue() * orderQuote.doubleValue() / orderBase.doubleValue()))); + myQuote = myQuote.minus(forSale); + } + } + } + return obtainedBase; } } diff --git a/graphenej/src/test/java/cy/agorise/graphenej/OrderBookTest.java b/graphenej/src/test/java/cy/agorise/graphenej/OrderBookTest.java new file mode 100644 index 0000000..4973013 --- /dev/null +++ b/graphenej/src/test/java/cy/agorise/graphenej/OrderBookTest.java @@ -0,0 +1,64 @@ +package cy.agorise.graphenej; + +import com.google.common.primitives.UnsignedLong; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Type; +import java.util.List; + +import cy.agorise.graphenej.models.WitnessResponse; + +/** + * Testing the {@link OrderBook} class + */ + +public class OrderBookTest { + + @Test + public void testRequiredBase(){ + String serializedOrderBook = "{\"id\": 1,\"result\": [{\"id\": \"1.7.37284933\",\"expiration\": \"2018-11-17T00:03:49\",\"seller\": \"1.2.132823\",\"for_sale\": 10,\"sell_price\": {\"base\": {\"amount\": 1,\"asset_id\": \"1.3.121\"},\"quote\": {\"amount\": 10,\"asset_id\": \"1.3.0\"}},\"deferred_fee\": 0},{\"id\": \"1.7.37284933\",\"expiration\": \"2018-11-17T00:03:49\",\"seller\": \"1.2.132823\",\"for_sale\": 20,\"sell_price\": {\"base\": {\"amount\": 1,\"asset_id\": \"1.3.121\"},\"quote\": {\"amount\": 20,\"asset_id\": \"1.3.0\"}},\"deferred_fee\": 0}]}"; + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer()); + builder.registerTypeAdapter(UserAccount.class, new UserAccount.UserAccountSimpleDeserializer()); + builder.registerTypeAdapter(LimitOrder.class, new LimitOrder.LimitOrderDeserializer()); + Type GetLimitOrdersResponse = new TypeToken>>() {}.getType(); + WitnessResponse> witnessResponse = builder.create().fromJson(serializedOrderBook, GetLimitOrdersResponse); + List orders = witnessResponse.result; + OrderBook orderBook = new OrderBook(orders); + + + final Asset _quote = new Asset("1.3.121", "USD", 4); + final Asset _base = new Asset("1.3.0", "BTS", 5); + long _totalQuote = 14; + UnsignedLong _totalBase = orderBook.calculateRequiredBase(new AssetAmount(UnsignedLong.valueOf(_totalQuote), _quote)); + Assert.assertEquals("Should buy 10 at 10 and 4 at 20, which sums up to 180",180, _totalBase.longValue()); + System.out.println(String.format("Base: %s, Quote: %s", _base.getObjectId(), _quote.getObjectId())); + System.out.println(String.format("_totalQuote: %d, _totalBase: %d", _totalQuote, _totalBase.longValue())); + } + + @Test + public void testRequiredQuote(){ + String serializedOrderBook = "{\"id\":1,\"result\":[{\"id\":\"1.7.37284933\",\"expiration\":\"2018-11-17T00:03:49\",\"seller\":\"1.2.132823\",\"for_sale\":20,\"sell_price\":{\"base\":{\"amount\":20,\"asset_id\":\"1.3.0\"},\"quote\":{\"amount\":1,\"asset_id\":\"1.3.121\"}},\"deferred_fee\":0},{\"id\":\"1.7.37284933\",\"expiration\":\"2018-11-17T00:03:49\",\"seller\":\"1.2.132823\",\"for_sale\":100,\"sell_price\":{\"base\":{\"amount\":10,\"asset_id\":\"1.3.0\"},\"quote\":{\"amount\":1,\"asset_id\":\"1.3.121\"}},\"deferred_fee\":0}]}"; + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapter(AssetAmount.class, new AssetAmount.AssetAmountDeserializer()); + builder.registerTypeAdapter(UserAccount.class, new UserAccount.UserAccountSimpleDeserializer()); + builder.registerTypeAdapter(LimitOrder.class, new LimitOrder.LimitOrderDeserializer()); + Type GetLimitOrdersResponse = new TypeToken>>() {}.getType(); + WitnessResponse> witnessResponse = builder.create().fromJson(serializedOrderBook, GetLimitOrdersResponse); + List orders = witnessResponse.result; + OrderBook orderBook = new OrderBook(orders); + + + final Asset _base = new Asset("1.3.121", "USD", 4); + final Asset _quote = new Asset("1.3.0", "BTS", 5); + long _totalBase = 2; + UnsignedLong _totalQuote = orderBook.calculateObtainedQuote(new AssetAmount(UnsignedLong.valueOf(_totalBase), _base)); + Assert.assertEquals("Should be able to buy 20 on the first order and 10 on the second, totalling 30",30, _totalQuote.longValue()); + System.out.println(String.format("Base: %s, Quote: %s", _base.getObjectId(), _quote.getObjectId())); + System.out.println(String.format("_totalQuote: %d, _totalBase: %d", _totalQuote.longValue(), _totalQuote.longValue())); + } +} diff --git a/graphenej/src/test/java/cy/agorise/graphenej/api/GetLimitOrdersTest.java b/graphenej/src/test/java/cy/agorise/graphenej/api/GetLimitOrdersTest.java index d0fd134..fecbeb0 100644 --- a/graphenej/src/test/java/cy/agorise/graphenej/api/GetLimitOrdersTest.java +++ b/graphenej/src/test/java/cy/agorise/graphenej/api/GetLimitOrdersTest.java @@ -33,7 +33,9 @@ public class GetLimitOrdersTest extends BaseApiTest { @Before public void setup(){ - System.out.println("Connecting to node: "+NODE_URL); + if(NODE_URL != null){ + System.out.println("Connecting to node: "+NODE_URL); + } } @Test @@ -141,50 +143,6 @@ public class GetLimitOrdersTest extends BaseApiTest { } } - @Test - public void testRequiredBase(){ - try { - final Asset _quote = new Asset("1.3.121", "USD", 4); - final Asset _base = new Asset("1.3.0", "BTS", 5); - mWebSocket.addListener(new GetLimitOrders(_base.getObjectId(), _quote.getObjectId(), 100, new WitnessResponseListener() { - @Override - public void onSuccess(WitnessResponse response) { - List orders = (List) response.result; - OrderBook orderBook = new OrderBook(orders); - - long _totalQuote = 1000; - long _totalBase = orderBook.calculateRequiredBase(new AssetAmount(UnsignedLong.valueOf(_totalQuote), _quote)); - - System.out.println(String.format("Base: %s, Quote: %s", _base.getObjectId(), _quote.getObjectId())); - System.out.println(String.format("_totalQuote: %d, _totalBase: %d", _totalQuote, _totalBase)); - - synchronized (GetLimitOrdersTest.this){ - GetLimitOrdersTest.this.notifyAll(); - } - } - - @Override - public void onError(BaseResponse.Error error) { - System.out.println("onError. Msg: "+error.message); - synchronized (GetLimitOrdersTest.this){ - GetLimitOrdersTest.this.notifyAll(); - } - } - })); - - mWebSocket.connect(); - - synchronized (this){ - wait(); - } - - } catch (WebSocketException e) { - System.out.println("WebSocketException. Msg: " + e.getMessage()); - } catch (InterruptedException e) { - System.out.println("InterruptedException. Msg: "+e.getMessage()); - } - } - @After public void tearDown() throws Exception { mWebSocket.disconnect();