Updating the OrderBook class, adding #calculateObtainedQuote and #calculateRequiredBase methods
This commit is contained in:
parent
ab7f88d17c
commit
3796ed5f81
4 changed files with 141 additions and 87 deletions
|
@ -1,5 +1,6 @@
|
||||||
package cy.agorise.graphenej;
|
package cy.agorise.graphenej;
|
||||||
|
|
||||||
|
import com.google.common.primitives.UnsignedLong;
|
||||||
import com.google.gson.JsonDeserializationContext;
|
import com.google.gson.JsonDeserializationContext;
|
||||||
import com.google.gson.JsonDeserializer;
|
import com.google.gson.JsonDeserializer;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
|
@ -28,7 +29,7 @@ public class LimitOrder extends GrapheneObject implements ByteSerializable {
|
||||||
|
|
||||||
private String expiration;
|
private String expiration;
|
||||||
private UserAccount seller;
|
private UserAccount seller;
|
||||||
private long forSale;
|
private UnsignedLong forSale;
|
||||||
private long deferredFee;
|
private long deferredFee;
|
||||||
private Price sellPrice;
|
private Price sellPrice;
|
||||||
|
|
||||||
|
@ -52,11 +53,11 @@ public class LimitOrder extends GrapheneObject implements ByteSerializable {
|
||||||
this.seller = seller;
|
this.seller = seller;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getForSale() {
|
public UnsignedLong getForSale() {
|
||||||
return forSale;
|
return forSale;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setForSale(long forSale) {
|
public void setForSale(UnsignedLong forSale) {
|
||||||
this.forSale = forSale;
|
this.forSale = forSale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +128,7 @@ public class LimitOrder extends GrapheneObject implements ByteSerializable {
|
||||||
LimitOrder limitOrder = new LimitOrder(id);
|
LimitOrder limitOrder = new LimitOrder(id);
|
||||||
limitOrder.setExpiration(expiration);
|
limitOrder.setExpiration(expiration);
|
||||||
limitOrder.setSeller(seller);
|
limitOrder.setSeller(seller);
|
||||||
limitOrder.setForSale(Long.parseLong(forSale));
|
limitOrder.setForSale(UnsignedLong.valueOf(forSale));
|
||||||
limitOrder.setSellPrice(price);
|
limitOrder.setSellPrice(price);
|
||||||
limitOrder.setDeferredFee(deferredFee);
|
limitOrder.setDeferredFee(deferredFee);
|
||||||
return limitOrder;
|
return limitOrder;
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
package cy.agorise.graphenej;
|
package cy.agorise.graphenej;
|
||||||
|
|
||||||
import com.google.common.math.DoubleMath;
|
|
||||||
import com.google.common.primitives.UnsignedLong;
|
import com.google.common.primitives.UnsignedLong;
|
||||||
|
|
||||||
import java.math.RoundingMode;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import cy.agorise.graphenej.operations.LimitOrderCreateOperation;
|
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
|
* It also provides a handy method that should return the appropriate LimitOrderCreateOperation
|
||||||
* object needed in case the user wants to perform market-priced operations.
|
* 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 {
|
public class OrderBook {
|
||||||
private List<LimitOrder> limitOrders;
|
private List<LimitOrder> limitOrders;
|
||||||
|
@ -52,53 +50,86 @@ public class OrderBook {
|
||||||
* @return An instance of the LimitOrderCreateOperation class, which is ready to be broadcasted.
|
* @return An instance of the LimitOrderCreateOperation class, which is ready to be broadcasted.
|
||||||
*/
|
*/
|
||||||
public LimitOrderCreateOperation exchange(UserAccount seller, Asset myBaseAsset, AssetAmount myQuoteAmount, int expiration){
|
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;
|
AssetAmount toReceive = myQuoteAmount;
|
||||||
LimitOrderCreateOperation buyOrder = new LimitOrderCreateOperation(seller, toSell, toReceive, expiration, true);
|
LimitOrderCreateOperation buyOrder = new LimitOrderCreateOperation(seller, toSell, toReceive, expiration, true);
|
||||||
|
|
||||||
return buyOrder;
|
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
|
* Method that calculates the amount of an asset that we will obtain (the quote amount) if we trade
|
||||||
* asset we need to offer to perform a successful transaction with the current order book.
|
* a known fixed amount of the asset we already have (the base amount).
|
||||||
* @param quoteAmount: The amount of the desired asset.
|
*
|
||||||
* @return: The minimum amount of the base asset that we need to give away
|
* @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){
|
public UnsignedLong calculateObtainedQuote(AssetAmount baseAmount){
|
||||||
long totalBought = 0;
|
UnsignedLong myBase = baseAmount.getAmount();
|
||||||
long totalSold = 0;
|
UnsignedLong obtainedQuote = UnsignedLong.ZERO;
|
||||||
for(int i = 0; i < this.limitOrders.size() && totalBought < quoteAmount.getAmount().longValue(); i++){
|
for(int i = 0; i < limitOrders.size() && myBase.compareTo(UnsignedLong.ZERO) > 0; i++){
|
||||||
LimitOrder order = this.limitOrders.get(i);
|
LimitOrder order = limitOrders.get(i);
|
||||||
|
|
||||||
// If the base asset is the same as our quote asset, we have a match
|
// Checking to make sure the order matches our needs
|
||||||
if(order.getSellPrice().base.getAsset().getObjectId().equals(quoteAmount.getAsset().getObjectId())){
|
if(order.getSellPrice().quote.getAsset().equals(baseAmount.getAsset())){
|
||||||
// My quote amount, is the order's base amount
|
UnsignedLong orderBase = order.getSellPrice().base.getAmount();
|
||||||
long orderAmount = order.getForSale();
|
UnsignedLong orderQuote = order.getSellPrice().quote.getAmount();
|
||||||
|
UnsignedLong availableBase = order.getForSale();
|
||||||
|
|
||||||
// The amount of the quote asset we still need
|
UnsignedLong myQuote = UnsignedLong.valueOf((long)(myBase.times(orderBase).doubleValue() / (orderQuote.doubleValue())));
|
||||||
long stillNeed = quoteAmount.getAmount().longValue() - totalBought;
|
if(myQuote.compareTo(availableBase) > 0){
|
||||||
|
// We consume this order entirely
|
||||||
// If the offered amount is greater than what we still need, we exchange just what we need
|
// myBase = myBase - (for_sale) * (order_quote / order_base)
|
||||||
if(orderAmount >= stillNeed) {
|
myBase = myBase.minus(availableBase.times(orderQuote).dividedBy(orderBase));
|
||||||
totalBought += stillNeed;
|
// We need more than this order can offer us, but have to take in consideration how much there really is.
|
||||||
double additionalRatio = (double) stillNeed / (double) order.getSellPrice().base.getAmount().longValue();
|
// (order base / order quote) x (available order base / order base)
|
||||||
double additionalAmount = order.getSellPrice().quote.getAmount().longValue() * additionalRatio;
|
UnsignedLong thisBatch = UnsignedLong.valueOf((long)(orderBase.times(availableBase).doubleValue() / orderQuote.times(orderBase).doubleValue()));
|
||||||
long longAdditional = DoubleMath.roundToLong(additionalAmount, RoundingMode.HALF_UP);
|
obtainedQuote = obtainedQuote.plus(thisBatch);
|
||||||
totalSold += longAdditional;
|
|
||||||
}else{
|
}else{
|
||||||
// If the offered amount is less than what we need, we exchange the whole order
|
// This order consumes all our base asset
|
||||||
totalBought += orderAmount;
|
// obtained_quote = obtained_quote + (my base * order_base / order_quote)
|
||||||
|
obtainedQuote = obtainedQuote.plus(myBase.times(orderBase.dividedBy(orderQuote)));
|
||||||
|
myBase = UnsignedLong.ZERO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obtainedQuote;
|
||||||
|
}
|
||||||
|
|
||||||
// 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:
|
* Method that calculates the amount of an asset that we will consume (the base amount) if we want to obtain
|
||||||
// actually_sold = for_sale * quote / base
|
* a known fixed amount of another asset (the quote amount).
|
||||||
double sellRatio = ((double) orderAmount) / ((double) order.getSellPrice().base.getAmount().longValue());
|
* @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);
|
||||||
|
|
||||||
totalSold += Math.floor(order.getSellPrice().quote.getAmount().doubleValue() * sellRatio);
|
// 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 totalSold;
|
return obtainedBase;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<WitnessResponse<List<LimitOrder>>>() {}.getType();
|
||||||
|
WitnessResponse<List<LimitOrder>> witnessResponse = builder.create().fromJson(serializedOrderBook, GetLimitOrdersResponse);
|
||||||
|
List<LimitOrder> 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<WitnessResponse<List<LimitOrder>>>() {}.getType();
|
||||||
|
WitnessResponse<List<LimitOrder>> witnessResponse = builder.create().fromJson(serializedOrderBook, GetLimitOrdersResponse);
|
||||||
|
List<LimitOrder> 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()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,8 +33,10 @@ public class GetLimitOrdersTest extends BaseApiTest {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup(){
|
public void setup(){
|
||||||
|
if(NODE_URL != null){
|
||||||
System.out.println("Connecting to node: "+NODE_URL);
|
System.out.println("Connecting to node: "+NODE_URL);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetLimitOrders(){
|
public void testGetLimitOrders(){
|
||||||
|
@ -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<LimitOrder> orders = (List<LimitOrder>) 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
|
@After
|
||||||
public void tearDown() throws Exception {
|
public void tearDown() throws Exception {
|
||||||
mWebSocket.disconnect();
|
mWebSocket.disconnect();
|
||||||
|
|
Loading…
Reference in a new issue