From 8bd9f293a48abf452239f7798a7438abde1659c2 Mon Sep 17 00:00:00 2001 From: "Nelson R. Perez" Date: Mon, 27 Mar 2017 15:33:00 -0500 Subject: [PATCH] Added the OrderBook class --- .../bitsharesmunich/graphenej/Converter.java | 1 - .../bitsharesmunich/graphenej/OrderBook.java | 81 ++++++++++++ .../operations/LimitOrderCreateOperation.java | 15 ++- .../graphenej/api/GetLimitOrdersTest.java | 119 +++++++++++++----- 4 files changed, 185 insertions(+), 31 deletions(-) create mode 100644 graphenej/src/main/java/de/bitsharesmunich/graphenej/OrderBook.java diff --git a/graphenej/src/main/java/de/bitsharesmunich/graphenej/Converter.java b/graphenej/src/main/java/de/bitsharesmunich/graphenej/Converter.java index d6e1f33..257c422 100644 --- a/graphenej/src/main/java/de/bitsharesmunich/graphenej/Converter.java +++ b/graphenej/src/main/java/de/bitsharesmunich/graphenej/Converter.java @@ -3,7 +3,6 @@ package de.bitsharesmunich.graphenej; import java.math.BigDecimal; import java.math.MathContext; -import com.google.common.primitives.UnsignedLong; import de.bitsharesmunich.graphenej.errors.IncompleteAssetError; import de.bitsharesmunich.graphenej.models.BucketObject; diff --git a/graphenej/src/main/java/de/bitsharesmunich/graphenej/OrderBook.java b/graphenej/src/main/java/de/bitsharesmunich/graphenej/OrderBook.java new file mode 100644 index 0000000..e82e9be --- /dev/null +++ b/graphenej/src/main/java/de/bitsharesmunich/graphenej/OrderBook.java @@ -0,0 +1,81 @@ +package de.bitsharesmunich.graphenej; + +import com.google.common.primitives.UnsignedLong; + +import java.util.List; + +import de.bitsharesmunich.graphenej.operations.LimitOrderCreateOperation; + +/** + * This class will maintain a snapshot of the order book between two assets. + * + * 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. + * + * Created by nelson on 3/25/17. + */ +public class OrderBook { + private List limitOrders; + + public OrderBook(List limitOrders){ + this.limitOrders = limitOrders; + } + + /** + * Replaces the current limit order by the list provided as parameter. + * @param limitOrders: New list of orders + */ + public void update(List limitOrders){ + this.limitOrders = limitOrders; + } + + public void update(LimitOrder limitOrder){ + //TODO: Implement the method that will update a single limit order from the order book + } + + /** + * Method used to exchange a specific amount of an asset (The base) for another one (The quote) at market value. + * + * It should analyze the order book and figure out the optimal amount of the base asset to give + * away in order to obtain the desired amount of the quote asset. + * + * @param seller: User account of the seller, used to build the limit order create operation + * @param myBaseAsset: The asset the user is willing to give away + * @param myQuoteAmount: The amount of a given asset the user wants + * + * @return An instance of the LimitOrderCreateOperation class, which is ready to be broadcasted. + */ + public LimitOrderCreateOperation exchange(UserAccount seller, Asset myBaseAsset, AssetAmount myQuoteAmount){ + long totalBought = 0; + long totalSold = 0; + for(int i = 0; i < this.limitOrders.size() && totalBought < myQuoteAmount.getAmount().longValue(); i++){ + LimitOrder order = this.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(myQuoteAmount.getAsset().getObjectId())){ + // My quote amount, is the order's base amount + long orderAmount = order.getSellPrice().base.getAmount().longValue(); + + // The amount of the quote asset we still need + long stillNeed = myQuoteAmount.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; + totalSold += (order.getSellPrice().quote.getAmount().longValue() * stillNeed) / order.getSellPrice().base.getAmount().longValue();; + }else{ + // If the offered amount is less than what we need, we exchange the whole order + totalBought += orderAmount; + totalSold += order.getSellPrice().quote.getAmount().longValue(); + } + } + } + AssetAmount toSell = new AssetAmount(UnsignedLong.valueOf(totalSold), myBaseAsset); + AssetAmount toReceive = myQuoteAmount; + LimitOrderCreateOperation buyOrder = new LimitOrderCreateOperation(seller, toSell, toReceive, true); + + return buyOrder; + } +} diff --git a/graphenej/src/main/java/de/bitsharesmunich/graphenej/operations/LimitOrderCreateOperation.java b/graphenej/src/main/java/de/bitsharesmunich/graphenej/operations/LimitOrderCreateOperation.java index 7b60c70..96a1403 100644 --- a/graphenej/src/main/java/de/bitsharesmunich/graphenej/operations/LimitOrderCreateOperation.java +++ b/graphenej/src/main/java/de/bitsharesmunich/graphenej/operations/LimitOrderCreateOperation.java @@ -4,13 +4,18 @@ import com.google.common.primitives.Bytes; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import de.bitsharesmunich.graphenej.*; import java.nio.ByteBuffer; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; +import de.bitsharesmunich.graphenej.AssetAmount; +import de.bitsharesmunich.graphenej.BaseOperation; +import de.bitsharesmunich.graphenej.OperationType; +import de.bitsharesmunich.graphenej.UserAccount; +import de.bitsharesmunich.graphenej.Util; + /** * Operation used to denote the creation of a limit order on the blockchain. * @@ -42,6 +47,14 @@ public class LimitOrderCreateOperation extends BaseOperation { private int expiration; private boolean fillOrKill; + public LimitOrderCreateOperation(UserAccount seller, AssetAmount toSell, AssetAmount minToReceive, boolean fillOrKill){ + super(OperationType.LIMIT_ORDER_CREATE_OPERATION); + this.seller = seller; + this.amountToSell = toSell; + this.minToReceive = minToReceive; + this.fillOrKill = fillOrKill; + } + /** * @param seller: Id of the seller * @param toSell: Id of the asset to sell diff --git a/graphenej/src/test/java/de/bitsharesmunich/graphenej/api/GetLimitOrdersTest.java b/graphenej/src/test/java/de/bitsharesmunich/graphenej/api/GetLimitOrdersTest.java index f23fbe0..2474f0c 100644 --- a/graphenej/src/test/java/de/bitsharesmunich/graphenej/api/GetLimitOrdersTest.java +++ b/graphenej/src/test/java/de/bitsharesmunich/graphenej/api/GetLimitOrdersTest.java @@ -1,70 +1,88 @@ package de.bitsharesmunich.graphenej.api; +import com.google.common.primitives.UnsignedLong; import com.neovisionaries.ws.client.WebSocket; import com.neovisionaries.ws.client.WebSocketException; import com.neovisionaries.ws.client.WebSocketFactory; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import java.io.IOException; -import java.security.NoSuchAlgorithmException; import java.util.List; import javax.net.ssl.SSLContext; import de.bitsharesmunich.graphenej.Asset; +import de.bitsharesmunich.graphenej.AssetAmount; import de.bitsharesmunich.graphenej.Converter; import de.bitsharesmunich.graphenej.LimitOrder; +import de.bitsharesmunich.graphenej.OrderBook; +import de.bitsharesmunich.graphenej.UserAccount; import de.bitsharesmunich.graphenej.interfaces.WitnessResponseListener; import de.bitsharesmunich.graphenej.models.BaseResponse; import de.bitsharesmunich.graphenej.models.WitnessResponse; +import de.bitsharesmunich.graphenej.operations.LimitOrderCreateOperation; import de.bitsharesmunich.graphenej.test.NaiveSSLContext; +import static org.hamcrest.CoreMatchers.is; + /** * Created by nelson on 3/24/17. */ public class GetLimitOrdersTest { private String BLOCK_PAY_DE = System.getenv("BLOCKPAY_DE"); - private final Asset base = new Asset("1.3.120", "EUR", 4); + private UserAccount seller = new UserAccount("1.2.143563"); + private final Asset base = new Asset("1.3.0", "BTS", 5); private final Asset quote = new Asset("1.3.121", "USD", 4); + private SSLContext context; + private WebSocket mWebSocket; @Before public void setUp() throws Exception { + context = NaiveSSLContext.getInstance("TLS"); + WebSocketFactory factory = new WebSocketFactory(); + // Set the custom SSL context. + factory.setSSLContext(context); + + mWebSocket = factory.createSocket(BLOCK_PAY_DE); } @Test public void testGetLimitOrders(){ - SSLContext context = null; try { - context = NaiveSSLContext.getInstance("TLS"); - WebSocketFactory factory = new WebSocketFactory(); - - // Set the custom SSL context. - factory.setSSLContext(context); - - WebSocket mWebSocket = factory.createSocket(BLOCK_PAY_DE); - mWebSocket.addListener(new GetLimitOrders(base.getObjectId(), quote.getObjectId(), 100, new WitnessResponseListener() { @Override public void onSuccess(WitnessResponse response) { List orders = (List) response.result; + Assert.assertThat("Checking that we have orders", orders.isEmpty(), is(false)); Converter converter = new Converter(); - System.out.println(); - for(LimitOrder order : orders){ - order.getSellPrice().base.getAsset().setPrecision(base.getPrecision()); - order.getSellPrice().quote.getAsset().setPrecision(quote.getPrecision()); - double baseToQuoteExchange = converter.getConversionRate(order.getSellPrice(), Converter.BASE_TO_QUOTE); - double quoteToBaseExchange = converter.getConversionRate(order.getSellPrice(), Converter.QUOTE_TO_BASE); - System.out.println(String.format("base to quote: %.5f, quote to base: %.5f", baseToQuoteExchange, quoteToBaseExchange)); + double baseToQuoteExchange, quoteToBaseExchange; - synchronized (GetLimitOrdersTest.this){ - GetLimitOrdersTest.this.notifyAll(); + for(LimitOrder order : orders){ + if(order.getSellPrice().base.getAsset().getObjectId().equals(base.getObjectId())){ + order.getSellPrice().base.getAsset().setPrecision(base.getPrecision()); + order.getSellPrice().quote.getAsset().setPrecision(quote.getPrecision()); + + baseToQuoteExchange = converter.getConversionRate(order.getSellPrice(), Converter.BASE_TO_QUOTE); + quoteToBaseExchange = converter.getConversionRate(order.getSellPrice(), Converter.QUOTE_TO_BASE); + System.out.println(String.format("> id: %s, base to quote: %.5f, quote to base: %.5f", order.getObjectId(), baseToQuoteExchange, quoteToBaseExchange)); + }else{ + order.getSellPrice().base.getAsset().setPrecision(quote.getPrecision()); + order.getSellPrice().quote.getAsset().setPrecision(base.getPrecision()); + + baseToQuoteExchange = converter.getConversionRate(order.getSellPrice(), Converter.BASE_TO_QUOTE); + quoteToBaseExchange = converter.getConversionRate(order.getSellPrice(), Converter.QUOTE_TO_BASE); + System.out.println(String.format("< id: %s, base to quote: %.5f, quote to base: %.5f", order.getObjectId(), baseToQuoteExchange, quoteToBaseExchange)); } } + + synchronized (GetLimitOrdersTest.this){ + GetLimitOrdersTest.this.notifyAll(); + } } @Override @@ -75,18 +93,62 @@ public class GetLimitOrdersTest { } } })); + mWebSocket.connect(); - System.out.println("Waiting.."); + synchronized (this){ wait(); } - System.out.println("Released!"); - } catch (NoSuchAlgorithmException e) { - System.out.println("NoSuchAlgorithmException. Msg: " + e.getMessage()); } catch (WebSocketException e) { System.out.println("WebSocketException. Msg: " + e.getMessage()); - } catch (IOException e) { - System.out.println("IOException. Msg: " + e.getMessage()); + } catch (InterruptedException e) { + System.out.println("InterruptedException. Msg: "+e.getMessage()); + } + } + + /** + * This method is designed to test how the OrderBook class handles data obtained + * from the GetLimitOrders API call. + */ + @Test + public void testOrderBook(){ + LimitOrderCreateOperation limitOrderOperation; + + try { + 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); + + AssetAmount toBuy = new AssetAmount(UnsignedLong.valueOf(9850000), quote); + LimitOrderCreateOperation operation = orderBook.exchange(seller, base, toBuy); + + // Testing the successfull creation of a limit order create operation + Assert.assertTrue(operation != null); + + 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()); } @@ -94,7 +156,6 @@ public class GetLimitOrdersTest { @After public void tearDown() throws Exception { - + mWebSocket.disconnect(); } - } \ No newline at end of file