NuBotTrading / src / main / java / com / nubits / nubot / trading / TradeUtils.java

/*
 * 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);
    }
}