Java tutorial
/* * 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.tasks; import com.nubits.nubot.bot.Global; import com.nubits.nubot.bot.NuBotConnectionException; import com.nubits.nubot.bot.SessionManager; import com.nubits.nubot.global.Constant; import com.nubits.nubot.global.Settings; import com.nubits.nubot.models.ApiResponse; import com.nubits.nubot.models.BidAskPair; import com.nubits.nubot.models.LastPrice; import com.nubits.nubot.notifications.HipChatNotifications; import com.nubits.nubot.notifications.MailNotifications; import com.nubits.nubot.pricefeeds.PriceFeedManager; import com.nubits.nubot.strategy.Secondary.StrategySecondaryPegTask; import com.nubits.nubot.utils.FilesystemUtils; import com.nubits.nubot.utils.Utils; import io.evanwong.oss.hipchat.v2.rooms.MessageColor; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.util.*; /** * A task for monitoring prices and triggering actions */ public class PriceMonitorTriggerTask extends TimerTask { private static final int REFRESH_OFFSET = 1000; //this is how close to the refresh interval is considered a fail (millisecond) private static final int PRICE_PERCENTAGE = 10; //this is the percentage at which refresh action is taken private static final Logger LOG = LoggerFactory.getLogger(PriceMonitorTriggerTask.class.getName()); private static int SLEEP_COUNT = 0; private final int MOVING_AVERAGE_SIZE = 30; //this is how many elements the Moving average queue holds /** * threshold for signaling a deviation of prices */ private final double DISTANCE_TRESHHOLD = 10; private final int MAX_ATTEMPTS = 5; protected PriceFeedManager pfm = null; //set up a Queue to hold the prices used to calculate the moving average of prices protected Queue<Double> queueMA = new LinkedList<>(); protected LastPrice lastPrice; protected ArrayList<LastPrice> lastPrices; private double wallchangeThreshold; //options private double sellPriceUSD, buyPriceUSD; private String pegPriceDirection; private LastPrice currentWallPEGPrice; private boolean wallsBeingShifted = false; private BidAskPair bidask; private StrategySecondaryPegTask strategy = null; private int count; private boolean isFirstTimeExecution = true; private String wallshiftsFilePathCSV = Global.sessionLogFolder + "/" + Settings.WALLSHIFTS_FILENAME + ".csv"; private String wallshiftsFilePathJSON = Global.sessionLogFolder + "/" + Settings.WALLSHIFTS_FILENAME + ".json"; private String emailHistory = ""; private Long currentTime = null; private boolean first = true; private final int PR = Settings.DEFAULT_PRECISION; public void init() { File c = new File(this.wallshiftsFilePathCSV); if (!c.exists()) { try { c.createNewFile(); } catch (Exception e) { LOG.error("error creating " + c); } } if (SessionManager.sessionInterrupted()) return; //external interruption FilesystemUtils.writeToFile("timestamp,source,crypto,price,currency,sellprice,buyprice,otherfeeds\n", wallshiftsFilePathCSV, true); //create json file if it doesn't already exist File json = new File(this.wallshiftsFilePathJSON); if (!json.exists()) { try { json.createNewFile(); } catch (Exception e) { LOG.error("error creating " + json); } JSONObject history = new JSONObject(); JSONArray wall_shifts = new JSONArray(); history.put("wall_shifts", wall_shifts); FilesystemUtils.writeToFile(history.toJSONString(), this.wallshiftsFilePathJSON, true); } } public void setPriceFeedManager(PriceFeedManager pfm) { this.pfm = pfm; } @Override public void run() { if (SessionManager.sessionInterrupted()) return; //external interruption LOG.debug("Executing " + this.getClass()); if (first) { LOG.info("running PriceMonitorTrigger for first time"); init(); first = false; } //if a problem occurred we sleep for a period using the SLEEP_COUNTER if (SLEEP_COUNT > 0) { LOG.error("error occurred. sleep " + SLEEP_COUNT); SLEEP_COUNT--; currentTime = System.currentTimeMillis(); return; } if (SessionManager.sessionInterrupted()) return; //external interruption //take a note of the current time. //sudden changes in price can cause the bot to re-request the price data repeatedly // until the moving average is within 10% of the reported price. //we don't want that process to take longer than the price refresh interval currentTime = System.currentTimeMillis(); LOG.debug("Executing task : PriceMonitorTriggerTask "); if (pfm == null || strategy == null) { LOG.error( "PriceMonitorTriggerTask task needs a PriceFeedManager and a Strategy to work. Please assign it before running it"); } else { count = 1; try { executeUpdatePrice(count); if (SessionManager.sessionInterrupted()) return; //external interruption } catch (FeedPriceException e) { LOG.error("" + e); sendErrorNotification(); Global.exchange.getTrade().clearOrders(Global.options.getPair()); if (SessionManager.sessionInterrupted()) return; //external interruption } } } private void initStrategy(double peg_price) throws NuBotConnectionException { if (SessionManager.sessionInterrupted()) return; //external interruption Global.conversion = peg_price; //used then for liquidity info //Compute the buy/sell prices in USD //get the TX fee ApiResponse txFeeNTBPEGResponse = Global.exchange.getTrade().getTxFee(Global.options.getPair()); if (!txFeeNTBPEGResponse.isPositive()) { throw new NuBotConnectionException( "Cannot get txFee : " + txFeeNTBPEGResponse.getError().getDescription()); } double txfee = (Double) txFeeNTBPEGResponse.getResponseObject(); sellPriceUSD = 1 + (0.01 * txfee); if (!Global.options.isDualSide()) { sellPriceUSD = sellPriceUSD + Global.options.getPriceIncrement(); } buyPriceUSD = 1 - (0.01 * txfee); //Add(remove) the offset % from prices //compute half of the spread double halfSpread = Utils.round(Global.options.getSpread() / 2, 6); double offset = Utils.round(halfSpread / 100, 6); LOG.debug("halfspread " + halfSpread); LOG.debug("offset " + offset); sellPriceUSD = sellPriceUSD + offset; buyPriceUSD = buyPriceUSD - offset; String message = "Computing USD prices with spread " + Global.options.getSpread() + "% : sell @ " + sellPriceUSD; if (Global.options.isDualSide()) { message += " buy @ " + buyPriceUSD; } LOG.info(message); //convert sell price to PEG double sellPricePEGInitial; double buyPricePEGInitial; if (Global.swappedPair) { //NBT as paymentCurrency sellPricePEGInitial = Utils.round(Global.conversion * sellPriceUSD, PR); buyPricePEGInitial = Utils.round(Global.conversion * buyPriceUSD, PR); } else { sellPricePEGInitial = Utils.round(sellPriceUSD / peg_price, PR); buyPricePEGInitial = Utils.round(buyPriceUSD / peg_price, PR); } //store first value this.bidask = new BidAskPair(buyPricePEGInitial, sellPricePEGInitial); String message2 = "Converted price (using 1 " + Global.options.getPair().getPaymentCurrency().getCode() + " = " + peg_price + " USD)" + " : sell @ " + sellPricePEGInitial + " " + Global.options.getPair().getPaymentCurrency().getCode() + ""; if (Global.options.isDualSide()) { message2 += "; buy @ " + buyPricePEGInitial + " " + Global.options.getPair().getPaymentCurrency().getCode(); } LOG.info(message2); if (SessionManager.sessionInterrupted()) return; //external interruption //Assign prices StrategySecondaryPegTask secTask = (StrategySecondaryPegTask) Global.taskManager.getSecondaryPegTask() .getTask(); if (!Global.swappedPair) { secTask.setBuyPricePEG(buyPricePEGInitial); secTask.setSellPricePEG(sellPricePEGInitial); } else { secTask.setBuyPricePEG(sellPricePEGInitial); secTask.setSellPricePEG(buyPricePEGInitial); } //Start strategy Global.taskManager.getSecondaryPegTask().start(); //Send email notification String title = " production (" + Global.options.getExchangeName() + ") [" + pfm.getPair().toString() + "] price tracking started"; String tldr = pfm.getPair().getOrderCurrency().getCode().toUpperCase() + " price tracking started at " + peg_price + " " + pfm.getPair().getPaymentCurrency().getCode().toUpperCase() + ".\n" + "Will send a new mail notification everytime the price of " + pfm.getPair().getOrderCurrency().getCode().toUpperCase() + " changes more than " + Global.options.getWallchangeThreshold() + "%."; MailNotifications.send(Global.options.getMailRecipient(), title, tldr); } private void executeUpdatePrice(int countTrials) throws FeedPriceException { if (SessionManager.sessionInterrupted()) return; //external interruption if (countTrials <= MAX_ATTEMPTS) { if (SessionManager.sessionInterrupted()) return; //external interruption pfm.fetchLastPrices(); ArrayList<LastPrice> currentPriceList = pfm.getLastPrices(); LOG.debug("CheckLastPrice received values from remote feeds. "); boolean gotall = currentPriceList.size() == pfm.getFeedList().size(); if (gotall) { //All feeds returned a positive value //Check if mainPrice is close enough to the others // I am assuming that mainPrice is the first element of the list if (sanityCheck(currentPriceList, 0)) { //mainPrice is reliable compared to the others if (SessionManager.sessionInterrupted()) return; //external interruption this.updateLastPrice(currentPriceList.get(0), currentPriceList); } else { //mainPrice is not reliable compared to the others //Check if other backup prices are close enough to each other if (SessionManager.sessionInterrupted()) return; //external interruption boolean foundSomeValidBackUp = false; LastPrice goodPrice = null; for (int l = 1; l < currentPriceList.size(); l++) { if (sanityCheck(currentPriceList, l)) { goodPrice = currentPriceList.get(l); foundSomeValidBackUp = true; break; } } if (foundSomeValidBackUp) { //goodPrice is a valid price backup! if (SessionManager.sessionInterrupted()) return; //external interruption this.updateLastPrice(goodPrice, currentPriceList); } else { //None of the source are in accord with others. //Try to send a notification unableToUpdatePrice(currentPriceList); } } } else { //One or more feed returned an error value if (SessionManager.sessionInterrupted()) return; //external interruption if (currentPriceList.size() == 2) { // if only 2 values are available if (SessionManager.sessionInterrupted()) return; //external interruption double p1 = currentPriceList.get(0).getPrice().getQuantity(); double p2 = currentPriceList.get(1).getPrice().getQuantity(); if (closeEnough(this.DISTANCE_TRESHHOLD, p1, p2)) { this.updateLastPrice(currentPriceList.get(0), currentPriceList); } else { //The two values are too unreliable unableToUpdatePrice(currentPriceList); } } else if (currentPriceList.size() > 2) { // more than two if (SessionManager.sessionInterrupted()) return; //external interruption //Check if other backup prices are close enough to each other boolean foundSomeValidBackUp = false; LastPrice goodPrice = null; for (int l = 1; l < currentPriceList.size(); l++) { if (sanityCheck(currentPriceList, l)) { goodPrice = currentPriceList.get(l); foundSomeValidBackUp = true; break; } } if (foundSomeValidBackUp) { //goodPrice is a valid price backup! this.updateLastPrice(goodPrice, currentPriceList); } else { //None of the source are in accord with others. //Try to send a notification unableToUpdatePrice(currentPriceList); } } else {//if only one or 0 feeds are positive unableToUpdatePrice(currentPriceList); } } } else { //Tried more than three times without success throw new FeedPriceException( "The price has failed updating more than " + MAX_ATTEMPTS + " times in a row"); } } private void unableToUpdatePrice(ArrayList<LastPrice> priceList) { count++; try { Thread.sleep(count * 60 * 1000); } catch (InterruptedException ex) { LOG.error(ex.toString()); } try { executeUpdatePrice(count); } catch (FeedPriceException ex) { LOG.error(ex.toString()); } } public void gracefulPause(LastPrice lp) { //This is called is an abnormal price is detected for one whole refresh period String logMessage; String notification; String subject; MessageColor notificationColor; double sleepTime = 0; //we need to check the reason that the refresh took a whole period. //if it's because of a no connection issue, we need to wait to see if connection restarts if (!Global.exchange.getLiveData().isConnected()) { currentTime = System.currentTimeMillis(); logMessage = "There has been a connection issue for " + Settings.CHECK_PRICE_INTERVAL + " seconds\n" + "Consider restarting the bot if the connection issue persists"; notification = ""; notificationColor = MessageColor.YELLOW; subject = Global.exchange.getName() + " Bot is suffering a connection issue"; } else { //otherwise something bad has happened so we shutdown. int p = 3; sleepTime = Settings.CHECK_PRICE_INTERVAL * p; logMessage = "The Fetched Exchange rate data has remained outside of the required price band for " + Settings.CHECK_PRICE_INTERVAL + "seconds.\nThe bot will notify and restart in " + sleepTime + "seconds."; notification = "A large price difference was detected at " + Global.exchange.getName() + ".\nThe Last obtained price of " + Objects.toString(lp.getPrice().getQuantity()) + " was outside of " + Objects.toString(PRICE_PERCENTAGE) + "% of the moving average figure of " + Objects.toString(getMovingAverage()) + ".\nNuBot will remove the current orders and replace them in " + sleepTime + "seconds."; notificationColor = MessageColor.PURPLE; subject = Global.exchange.getName() + " Moving Average issue. Bot will replace orders in " + sleepTime + "seconds."; } //we want to send Hip Chat and mail notifications, // cancel all orders to avoid arbitrage against the bot and // exit execution gracefully LOG.error(logMessage); LOG.error("Notifying HipChat"); HipChatNotifications.sendMessage(notification, notificationColor); LOG.error("Sending Email"); MailNotifications.send(Global.options.getMailRecipient(), subject, notification); if (sleepTime > 0) { LOG.error("Cancelling Orders to avoid Arbitrage against the bot"); Global.exchange.getTrade().clearOrders(Global.options.getPair()); //clear the moving average so the restart is fresh queueMA.clear(); LOG.error("Sleeping for " + sleepTime); SLEEP_COUNT = 3; } currentTime = System.currentTimeMillis(); } public void updateLastPrice(LastPrice lp, ArrayList<LastPrice> priceList) { if (SessionManager.sessionInterrupted()) return; //external interruption //We need to fill up the moving average queue so that 30 data points exist. if (queueMA.size() < MOVING_AVERAGE_SIZE) { initMA(lp.getPrice().getQuantity()); } if (!Global.options.isMultipleCustodians()) { // //we check against the moving average double current = lp.getPrice().getQuantity(); double MA = getMovingAverage(); //calculate the percentage difference double percentageDiff = (((MA - current) / ((MA + current) / 2)) * 100); if ((percentageDiff > PRICE_PERCENTAGE) || (percentageDiff < -PRICE_PERCENTAGE)) { //The potential price is more than % different to the moving average //add it to the MA-Queue to raise the Moving Average and re-request the currency data //in this way we can react to a large change in price when we are sure it is not an anomaly LOG.warn("Latest price " + Objects.toString(current) + " is " + Objects.toString(percentageDiff) + "% outside of the moving average of " + Objects.toString(MA) + "." + "\nShifting moving average and re-fetching exchange rate data."); updateMovingAverageQueue(current); try { int trials = 1; executeUpdatePrice(trials); } catch (FeedPriceException ex) { } return; } //the potential price is within the % boundary. //add it to the MA-Queue to keep the moving average moving // Only do this if the standard update interval hasn't passed if (((System.currentTimeMillis() - (currentTime + REFRESH_OFFSET)) / 1000L) < Settings.CHECK_PRICE_INTERVAL) { updateMovingAverageQueue(current); } else { //If we get here, we haven't had a price within % of the average for as long as a standard update period //the action is to send notifications, cancel all orders and turn off the bot gracefulPause(lp); return; } } if (SessionManager.sessionInterrupted()) return; //external interruption //carry on with updating the wall price shift this.lastPrice = lp; this.lastPrices = priceList; LOG.info("Price Updated. " + lp.getSource() + ":1 " + lp.getCurrencyMeasured().getCode() + " = " + "" + lp.getPrice().getQuantity() + " " + lp.getPrice().getCurrency().getCode()); if (isFirstTimeExecution) { try { initStrategy(lp.getPrice().getQuantity()); } catch (NuBotConnectionException e) { } currentWallPEGPrice = lp; isFirstTimeExecution = false; } else { verifyPegPrices(); } } private void verifyPegPrices() { if (SessionManager.sessionInterrupted()) return; //external interruption LOG.debug("Executing tryMoveWalls"); boolean needToShift = true; if (!Global.options.isMultipleCustodians()) { needToShift = needToMoveWalls(lastPrice); //check if price moved more than x% from when the wall was setup } if (needToShift && !isWallsBeingShifted()) { //prevent a wall shift trigger if the strategy is already shifting walls. LOG.info("Walls need to be shifted"); //Compute price for walls currentWallPEGPrice = lastPrice; computeNewPrices(); } else { LOG.debug("No need to move walls"); currentTime = System.currentTimeMillis(); if (isWallsBeingShifted() && needToShift) { LOG.warn( "Wall shift is postponed: another process is already shifting existing walls. Will try again on next execution."); } } } private boolean needToMoveWalls(LastPrice last) { double currentWallPEGprice = currentWallPEGPrice.getPrice().getQuantity(); double distance = Math.abs(last.getPrice().getQuantity() - currentWallPEGprice); double percentageDistance = Utils.round((distance * 100) / currentWallPEGprice, 4); LOG.debug("delta =" + percentageDistance + "% (old : " + currentWallPEGprice + " new " + last.getPrice().getQuantity() + ")"); if (percentageDistance < wallchangeThreshold) { return false; } else { return true; } } private void computeNewPrices() { if (SessionManager.sessionInterrupted()) return; //external interruption double peg_price = lastPrice.getPrice().getQuantity(); double sellPricePEG_new; double buyPricePEG_new; if (Global.swappedPair) { //NBT as paymentCurrency sellPricePEG_new = Utils.round(sellPriceUSD * Global.conversion, Settings.DEFAULT_PRECISION); buyPricePEG_new = Utils.round(buyPriceUSD * Global.conversion, Settings.DEFAULT_PRECISION); } else { //convert sell price to PEG sellPricePEG_new = Utils.round(sellPriceUSD / peg_price, Settings.DEFAULT_PRECISION); buyPricePEG_new = Utils.round(buyPriceUSD / peg_price, Settings.DEFAULT_PRECISION); } BidAskPair newPrice = new BidAskPair(buyPricePEG_new, sellPricePEG_new); //check if the price increased or decreased compared to last if ((newPrice.getAsk() - this.bidask.getAsk()) > 0) { this.pegPriceDirection = Constant.UP; } else { this.pegPriceDirection = Constant.DOWN; } //Store new value this.bidask = newPrice; LOG.info("Sell Price " + sellPricePEG_new + " | " + "Buy Price " + buyPricePEG_new); //------------ here for output csv String source = currentWallPEGPrice.getSource(); double price = currentWallPEGPrice.getPrice().getQuantity(); String currency = currentWallPEGPrice.getPrice().getCurrency().getCode(); String crypto = pfm.getPair().getOrderCurrency().getCode(); //Call Strategy and notify the price change strategy.notifyPriceChanged(sellPricePEG_new, buyPricePEG_new, price, pegPriceDirection); Global.conversion = price; Date currentDate = new Date(); String row = currentDate + "," + source + "," + crypto + "," + price + "," + currency + "," + sellPricePEG_new + "," + buyPricePEG_new + ","; JSONArray backup_feeds = new JSONArray(); JSONObject otherPricesAtThisTime = new JSONObject(); for (int i = 0; i < this.lastPrices.size(); i++) { LastPrice tempPrice = lastPrices.get(i); otherPricesAtThisTime.put("feed", tempPrice.getSource()); otherPricesAtThisTime.put("price", tempPrice.getPrice().getQuantity()); } LOG.info("New price computed [" + row + "]"); if (SessionManager.sessionInterrupted()) return; //external interruption row += otherPricesAtThisTime.toString() + "\n"; backup_feeds.add(otherPricesAtThisTime); logrow(row, wallshiftsFilePathCSV, true); //Also update a json version of the output file //build the latest data into a JSONObject JSONObject wall_shift = new JSONObject(); wall_shift.put("timestamp", currentDate.getTime()); wall_shift.put("feed", source); wall_shift.put("crypto", crypto); wall_shift.put("price", price); wall_shift.put("currency", currency); wall_shift.put("sell_price", sellPricePEG_new); wall_shift.put("buy_price", buyPricePEG_new); wall_shift.put("backup_feed", backup_feeds); //now read the existing object if one exists JSONParser parser = new JSONParser(); JSONObject wall_shift_info = new JSONObject(); JSONArray wall_shifts = new JSONArray(); try { //object already exists in file wall_shift_info = (JSONObject) parser.parse(FilesystemUtils.readFromFile(this.wallshiftsFilePathJSON)); wall_shifts = (JSONArray) wall_shift_info.get("wall_shifts"); } catch (ParseException pe) { LOG.error("Unable to parse order_history.json"); } //add the latest orders to the orders array wall_shifts.add(wall_shift); wall_shift_info.put("wall_shifts", wall_shifts); //then save logWallShift(wall_shift_info.toJSONString()); if (SessionManager.sessionInterrupted()) return; //external interruption if (Global.options.sendMails()) { String title = " production (" + Global.options.getExchangeName() + ") [" + pfm.getPair().toString() + "] price changed more than " + wallchangeThreshold + "%"; String messageNow = row; emailHistory += messageNow; String tldr = pfm.getPair().toString() + " price changed more than " + wallchangeThreshold + "% since last notification: " + "now is " + price + " " + pfm.getPair().getPaymentCurrency().getCode().toUpperCase() + ".\n" + "Here are the prices the bot used in the new orders : \n" + "Sell at " + sellPricePEG_new + " " + pfm.getPair().getOrderCurrency().getCode().toUpperCase() + " " + "and buy at " + buyPricePEG_new + " " + pfm.getPair().getOrderCurrency().getCode().toUpperCase() + "\n" + "\n#########\n" + "Below you can see the history of price changes. You can copy paste to create a csv report." + "For each row the bot should have shifted the sell/buy walls.\n\n"; if (!Global.options.isMultipleCustodians()) { MailNotifications.send(Global.options.getMailRecipient(), title, tldr + emailHistory); } } } public void setWallchangeThreshold(double wallchangeThreshold) { this.wallchangeThreshold = wallchangeThreshold; } public void setStrategy(StrategySecondaryPegTask strategy) { this.strategy = strategy; } public boolean isWallsBeingShifted() { return wallsBeingShifted; } public void setWallsBeingShifted(boolean wallsBeingShifted) { currentTime = System.currentTimeMillis(); this.wallsBeingShifted = wallsBeingShifted; } private void sendErrorNotification() { String title = "Problems while updating " + pfm.getPair().getOrderCurrency().getCode() + " price. Cannot find a reliable feed."; String message = "NuBot timed out after " + MAX_ATTEMPTS + " failed attempts to update " + pfm.getPair().getOrderCurrency().getCode() + "" + " price. Please restart the bot and get in touch with Nu Dev team "; message += "[<strong>" + SessionManager.sessionId + "</strong>]"; MailNotifications.sendCritical(Global.options.getMailRecipient(), title, message); HipChatNotifications.sendMessageCritical(title + message); LOG.error(title + message); } // ----- price utils ------ public double getMovingAverage() { double MA = 0; for (Iterator<Double> price = queueMA.iterator(); price.hasNext();) { MA += price.next(); } MA = MA / queueMA.size(); return MA; } public void updateMovingAverageQueue(double price) { if (price == 0) { //don't add 0 return; } queueMA.add(price); //trim the queue so that it is a moving average over the correct number of data points if (queueMA.size() > MOVING_AVERAGE_SIZE) { queueMA.remove(); } } /** * init queue by filling it with one price only * * @param price */ protected void initMA(double price) { for (int i = 0; i <= 30; i++) { updateMovingAverageQueue(price); } } protected boolean closeEnough(double distanceTreshold, double mainPrice, double temp) { //if temp differs from mainPrice for more than a threshold%, return false double distance = Math.abs(mainPrice - temp); double percentageDistance = Utils.round(distance * 100 / mainPrice, 4); if (percentageDistance > distanceTreshold) { return false; } else { return true; } } /** * Measure if mainPrice is close to other two values * * @param priceList * @param mainPriceIndex * @return */ protected boolean sanityCheck(ArrayList<LastPrice> priceList, int mainPriceIndex) { boolean[] ok = new boolean[priceList.size() - 1]; double mainPrice = priceList.get(mainPriceIndex).getPrice().getQuantity(); //Test mainPrice vs backup sources int f = 0; for (int i = 0; i < priceList.size(); i++) { if (i != mainPriceIndex) { LastPrice tempPrice = priceList.get(i); double temp = tempPrice.getPrice().getQuantity(); ok[f] = closeEnough(DISTANCE_TRESHHOLD, mainPrice, temp); f++; } } int countOk = 0; for (int j = 0; j < ok.length; j++) { if (ok[j]) { countOk++; } } boolean overallOk = false; //is considered ok if the mainPrice is closeEnough to more than a half of backupPrices //Need to distinguish pair vs odd if (ok.length % 2 == 0) { if (countOk >= (int) ok.length / 2) { overallOk = true; } } else { if (countOk > (int) ok.length / 2) { overallOk = true; } } return overallOk; } protected void notifyDeviation(ArrayList<LastPrice> priceList) { String title = "Problems while updating " + pfm.getPair().getOrderCurrency().getCode() + " price. Cannot find a reliable feed."; String message = "Positive response from " + priceList.size() + "/" + pfm.getFeedList().size() + " feeds\n"; message += "[<strong>" + SessionManager.sessionId + "</strong>]"; for (int i = 0; i < priceList.size(); i++) { LastPrice tempPrice = priceList.get(i); message += (tempPrice.getSource() + ":1 " + tempPrice.getCurrencyMeasured().getCode() + " = " + tempPrice.getPrice().getQuantity() + " " + tempPrice.getPrice().getCurrency().getCode()) + "\n"; } MailNotifications.sendCritical(Global.options.getMailRecipient(), title, message); HipChatNotifications.sendMessageCritical(title + message); LOG.error(title + message); } private void logrow(String row, String outputPath, boolean append) { FilesystemUtils.writeToFile(row, outputPath, append); } private void logWallShift(String wall_shift) { FilesystemUtils.writeToFile(wall_shift, this.wallshiftsFilePathJSON, false); } }