/*
* Copyright (C) 2015 Nu Development Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package com.nubits.nubot.trading;
import com.nubits.nubot.bot.Global;
import com.nubits.nubot.bot.SessionManager;
import com.nubits.nubot.global.Constant;
import com.nubits.nubot.models.*;
import com.nubits.nubot.options.NuBotOptionsDefault;
import com.nubits.nubot.testsmanual.WrapperTestUtils;
import com.nubits.nubot.trading.keys.ApiKeys;
import com.nubits.nubot.utils.Utils;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.TreeMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import static java.util.concurrent.TimeUnit.SECONDS;
public class TradeUtils {
private static final Logger LOG = LoggerFactory.getLogger(TradeUtils.class.getName());
public static double getSellPrice(double txFee) {
if (Global.options.isDualSide()) {
return 1 + (0.01 * txFee);
} else {
return 1 + (0.01 * txFee) + Global.options.getPriceIncrement();
}
}
public static double getBuyPrice(double txFeeUSDNTB) {
return 1 - (0.01 * txFeeUSDNTB);
}
/**
* Build the query string given a set of query parameters
*
* @param args
* @param encoding
* @return
*/
public static String buildQueryString(AbstractMap<String, String> args, String encoding) {
String result = new String();
for (String hashkey : args.keySet()) {
if (result.length() > 0) {
result += '&';
}
try {
result += URLEncoder.encode(hashkey, encoding) + "="
+ URLEncoder.encode(args.get(hashkey), encoding);
} catch (Exception ex) {
LOG.error(ex.toString());
}
}
return result;
}
public static String buildQueryString(TreeMap<String, String> args, String encoding) {
String result = new String();
for (String hashkey : args.keySet()) {
if (result.length() > 0) {
result += '&';
}
try {
result += URLEncoder.encode(hashkey, encoding) + "="
+ URLEncoder.encode(args.get(hashkey), encoding);
} catch (Exception ex) {
LOG.error(ex.toString());
}
}
return result;
}
public static String signRequest(String secret, String hash_data, String hashfFunction, String encoding) {
String sign = "";
try {
Mac mac;
SecretKeySpec key;
// Create a new secret key
key = new SecretKeySpec(secret.getBytes(encoding), hashfFunction);
// Create a new mac
mac = Mac.getInstance(hashfFunction);
// Init mac with key.
mac.init(key);
sign = Hex.encodeHexString(mac.doFinal(hash_data.getBytes(encoding)));
} catch (UnsupportedEncodingException uee) {
LOG.error("Unsupported encoding exception: " + uee.toString());
} catch (NoSuchAlgorithmException nsae) {
LOG.error("No such algorithm exception: " + nsae.toString());
} catch (InvalidKeyException ike) {
LOG.error("Invalid key exception: " + ike.toString());
}
return sign;
}
public static BidAskPair computePEGPrices(BidAskPair usdPrices, double peg_price) {
double sellPricePEG;
double buyPricePEG;
if (Global.swappedPair) { //NBT as paymentCurrency
sellPricePEG = Utils.roundPlaces(Global.conversion * usdPrices.getAsk(), 8);
buyPricePEG = Utils.roundPlaces(Global.conversion * usdPrices.getBid(), 8);
} else {
sellPricePEG = Utils.roundPlaces(usdPrices.getAsk() / peg_price, 8);
buyPricePEG = Utils.roundPlaces(usdPrices.getBid() / peg_price, 8);
}
return new BidAskPair(buyPricePEG, sellPricePEG);
}
public static BidAskPair computeUSDPrices(double pegPrice) {
double txfee = NuBotOptionsDefault.defaultFactory().getTxFee();
if (Global.options != null) {
txfee = Global.options.getTxFee();
ApiResponse txFeeNTBPEGResponse = Global.trade.getTxFee(Global.options.getPair());
if (!txFeeNTBPEGResponse.isPositive()) {
LOG.warn("Error getting tx fee from exchange. Will use locally set fee");
} else {
txfee = (Double) txFeeNTBPEGResponse.getResponseObject();
}
}
double sellPriceUSD = 1 + (0.01 * txfee);
if (Global.options != null) {
if (!Global.options.isDualSide()) {
sellPriceUSD = sellPriceUSD + Global.options.getPriceIncrement();
} else {
if (NuBotOptionsDefault.defaultFactory().isDualSide()) {
sellPriceUSD = sellPriceUSD + NuBotOptionsDefault.defaultFactory().getPriceIncrement();
}
}
}
double buyPriceUSD = 1 - (0.01 * txfee);
//Add(remove) the offset % from prices
sellPriceUSD = sellPriceUSD + Global.options.bookSellOffset;
buyPriceUSD = buyPriceUSD - Global.options.bookBuyOffset;
return new BidAskPair(buyPriceUSD, sellPriceUSD);
}
/**
* Implementation of TradeInterface.placeOrders
* places orders sequencially, with a delay between each order
*/
//Expressed in ms - TODO empirically test these values and find the best fit for each exchange
public static final int INTERVAL_FAST = 0;
public static final int INTERVAL_MID = 100;
public static final int INTERVAL_SLOW = 600;
public static ApiResponse placeMultipleOrdersSequentiallyImplementation(OrderBatch batch, CurrencyPair pair, int interval, TradeInterface trade) {
ApiResponse toReturn = new ApiResponse();
long starTime = System.currentTimeMillis(); //tic
ArrayList<OrderPlaced> individualResponses = new ArrayList<>(); //To allocate single responses
ArrayList<OrderToPlace> orders = batch.getOrders();
//Iterate on all orders
for (int i = 0; i < orders.size(); i++) {
if (SessionManager.sessionInterrupted())
return new ApiResponse(false, null, new ApiError(-9999, "External interruption operation"));
//Sleep to relax API flooding
if (interval != 0) {
try {
Thread.sleep(interval);
} catch (InterruptedException e) {
LOG.error(e.toString());
}
}
if (SessionManager.sessionInterrupted()) return toReturn;
OrderToPlace orderToPlace = orders.get(i);
//Swap between sells and buys
ApiResponse response = null;
//Place order and read the response
if (orderToPlace.getType().equals(Constant.SELL)) {
response = trade.sell(pair, orderToPlace.getSize(), orderToPlace.getPrice());
} else {
response = trade.buy(pair, orderToPlace.getSize(), orderToPlace.getPrice());
}
if (i == batch.getWallIndex()) { //Save the id of the wall
String orderID = (String) response.getResponseObject();
double orderSize = orderToPlace.getSize();
if (response.isPositive()) {
if (orderToPlace.getType().equals(Constant.SELL)) {
Global.sellWallOrderID = orderID;
Global.sellWallOrderSize = orderSize;
} else {
Global.buyWallOrderID = orderID;
Global.buyWallOrderSize = orderSize;
}
LOG.info(orderToPlace.getType() + " wall order updated. ID : " + orderID + " size: " + orderSize);
} else {
if (orderToPlace.getType().equals(Constant.SELL)) {
Global.sellWallOrderID = "";
LOG.warn("Cannot save orderID of SELL wall, problem placing it : " + response.getError());
} else {
Global.buyWallOrderID = "";
LOG.warn("Cannot save orderID of BUY wall, problem placing it : " + response.getError());
}
}
}
//Create a new OrderPlaced object and add it to the list individualResponses
individualResponses.add(new OrderPlaced(orderToPlace, response));
}
//toc
long timeElapsed = System.currentTimeMillis() - starTime;
//When finished, create the MultipleOrderResponse Object
MultipleOrdersResponse multipleOrdersResponse = new MultipleOrdersResponse(individualResponses);
multipleOrdersResponse.setTimeElapsed(timeElapsed);
toReturn.setResponseObject(multipleOrdersResponse);
//TODO when does it need to be a general error? nevah? wrong API?
return toReturn;
}
/**
* Implementation of TradeInterface.placeOrdersParallel
* places orders in parallel iterating keys provided
* Returns nothing as it is async
*/
public static ApiResponse placeMultipleOrdersParallelImplementation(OrderBatch batch, CurrencyPair pair, ArrayList<ApiKeys> keys, TradeInterface ti) {
int numberOfProcessesAvail = keys.size();
int totalNumberOfOrders = batch.getOrders().size();
if (numberOfProcessesAvail > 0 && totalNumberOfOrders > 0) {
int ordersPerChunk = Math.round(totalNumberOfOrders / numberOfProcessesAvail);
if (numberOfProcessesAvail > totalNumberOfOrders)
numberOfProcessesAvail = totalNumberOfOrders;
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(numberOfProcessesAvail); //Create a pool of threads
for (int i = 0; i < numberOfProcessesAvail; i++) {
ti.setKeys(keys.get(i));
int startIndex = i * numberOfProcessesAvail;
int endIndex = startIndex + ordersPerChunk - 1;
if (i == (numberOfProcessesAvail - 1)) {
endIndex = totalNumberOfOrders - 1;
}
ArrayList tempOrders = new ArrayList<>(batch.getOrders().subList(startIndex, endIndex));
OrderBatch tempBatch = new OrderBatch(tempOrders, -1);
final Runnable executorTask = new Runnable() {
public void run() {
ApiResponse tempResp = TradeUtils.placeMultipleOrdersSequentiallyImplementation(tempBatch, pair, 1000, ti);
if (!tempResp.isPositive()) {
LOG.error("Error while placing multiple batch in parallel " + tempResp.getError().toString());
}
}
};
scheduler.schedule(executorTask, 0, SECONDS);
}
} else {
LOG.error("Empty order list or apikey list");
}
return new ApiResponse(true, true, null);
}
public static void placeSomeRandomOrders() {
CurrencyPair testPair = CurrencyList.NBT_BTC;
ArrayList<OrderToPlace> ordersToPlace = new ArrayList<>();
for (int i = 0; i <= 3; i++)
ordersToPlace.add(new OrderToPlace(Constant.SELL, testPair, 0.2 + Math.random() * 0.1, 1));
WrapperTestUtils.testPlaceOrders(new OrderBatch(ordersToPlace, 0), testPair);
ordersToPlace = new ArrayList<>();
for (int i = 0; i <= 3; i++)
ordersToPlace.add(new OrderToPlace(Constant.BUY, testPair, 0.2 + Math.random() * 0.1, 0.00001));
WrapperTestUtils.testPlaceOrders(new OrderBatch(ordersToPlace, 0), testPair);
}
}