// @ts-check
'use strict';
//Test commit v2
var timespans = { oneSecond: 1000, oneMinute: 60 * 1000, oneHour: 60 * 60 * 1000, oneDay: 24 * 60 * 60 * 1000, oneWeek: 7 * 24 * 60 * 60 * 1000, oneMonth: 31 * 24 * 60 * 60 * 1000 };
var fetchIntervalDaily = 12 * timespans.oneHour;
var fetchIntervalIntraday = 4 * timespans.oneHour;
var fetchIntervalSingleDay = 2 * timespans.oneMinute;
var initialPageTitle;
var fetchTimer = 0;
var lastSuccessfulFetch;
var lastTotalHoldingsValue = 0; //The last total holdings value successfully set in the database
var currencyFormatter = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' });
var instrumentSearchResults = [];
var tablesUpdateTimings = { lastUpdate: 0, timeBetweenUpdates: 10000, timer: 0, updateNeeded: true };
var indexMarquee = {
  newContent: '',
  currentContent: '',
  marqueOptions: { speed: 100, duplicated: true, gap: 150, startVisible: true, delayBeforeStart: 500 },
  init: function (container, initialContent) {
    this.currentContent = initialContent;
    this.container = container;
    this.lastUpdated = new Date();
    let marqueObject = this;
    this.container.html(initialContent).marquee(this.marqueOptions).bind('finished', function () { marqueObject.marqueeFinished() });
  },
  setContent: function (newContent) {
    if (newContent != this.currentContent) {
      this.newContent = newContent;
    }
    if (this.lastUpdated < new Date() - (5 * timespans.oneMinute)) {
      logInfo("indexMarquee has not updated for 5 minutes. Forcing update now");
      this.marqueeFinished();
    }
  },
  marqueeFinished: function () {
    if (this.newContent.length > 0) {
      this.lastUpdated = new Date();
      let cont = this.container;
      let opts = this.marqueOptions;
      let newContent = this.newContent;
      let marqueObject = this;
      this.currentContent = newContent;
      this.newContent = '';
      cont.marquee('destroy').html(newContent).marquee(opts).bind('finished', function () { marqueObject.marqueeFinished() });
    }
  }
}

var Instruments = {
  Data: [],
  indexOfSymbol: function (Symbol) { return this.Data.map(function (item) { return item.Symbol; }).indexOf(Symbol); },
  GetSymbol: function (Symbol) { return this.Data[this.indexOfSymbol(Symbol)]; },
  GetHolding: function (holdingID) {
    for (let x = 0; x < this.Data.length; x++) {
      let i = this.Data[x];
      for (let n = 0; n < i.Holdings.length; n++) {
        let h = i.Holdings[n];
        if (h.HoldingID == holdingID) {
          return { instrument: i, holdingIndex: n, holding: h};
        }
      }
    }
  },
  GetCurrentHoldings: function (Instrument) {
    let result = [];
    for (let x = 0; x < Instrument.Holdings.length; x++) {
      let h = Instrument.Holdings[x];
      if (h.SoldDate == null) {
        result.push(h);
      }
    }
    return result;
  },
  GetAccountCurrencyHolding: function (AccountName, CurrencyInstrument) {
    if (CurrencyInstrument.Holdings) {
      for (let x = 0; x < CurrencyInstrument.Holdings.length; x++) {
        let h = CurrencyInstrument.Holdings[x];
        if (h.SoldDate == null && h.AccountName == AccountName) {
          return (h);
        }
      }
    }
  },
  GetLastFetchedDaily: function () {
    //if (!this.Data[0].lastFetchedDaily) { this.Data[0].lastFetchedDaily = this.Data[0].MaxDailyDate };
    if (!this.Data[0].lastFetchedDaily) { this.Data[0].lastFetchedDaily = new Date().getTime() - fetchIntervalDaily + (4 * timespans.oneMinute) };
    let lastFetchedDate = this.Data[0].lastFetchedDaily;
    let lastFetchedIndex = 0;
    for (let i = 1; i < this.Data.length; i++) {
      if (this.Data[i].Symbol != 'GBP' && this.Data[i].Symbol != 'GBp') {
        //if (!this.Data[i].lastFetchedDaily) { this.Data[i].lastFetchedDaily = this.Data[i].MaxDailyDate };
        if (!this.Data[i].lastFetchedDaily) { this.Data[i].lastFetchedDaily = new Date().getTime() - fetchIntervalDaily + (4 * timespans.oneMinute) };
        if (lastFetchedDate > this.Data[i].lastFetchedDaily) {
          lastFetchedDate = this.Data[i].lastFetchedDaily;
          lastFetchedIndex = i;
        }
      }
    }
    //console.info({ lastFetchedType: 'Daily', Date: new Date(lastFetchedDate).yyyymmddhhmmss(), Index: lastFetchedIndex, Symbol: Instruments.Data[lastFetchedIndex].Symbol });
    return this.Data[lastFetchedIndex];
  },
  GetLastFetchedIntraday: function () {
    //if (!this.Data[0].lastFetchedIntraday) { this.Data[0].lastFetchedIntraday = this.Data[0].MaxIntradayDate };
    if (!this.Data[0].lastFetchedIntraday) { this.Data[0].lastFetchedIntraday = new Date().getTime() - fetchIntervalIntraday + (2 * timespans.oneMinute) };
    let lastFetchedDate = this.Data[0].lastFetchedIntraday;
    let lastFetchedIndex = 0;
    for (let i = 1; i < this.Data.length; i++) {
      if (this.Data[i].Symbol != 'GBP' && this.Data[i].Symbol != 'GBp') {
        //if (!this.Data[i].lastFetchedIntraday) { this.Data[i].lastFetchedIntraday = this.Data[i].MaxIntradayDate };
        if (!this.Data[i].lastFetchedIntraday) { this.Data[i].lastFetchedIntraday = new Date().getTime() - fetchIntervalIntraday + (2 * timespans.oneMinute) };
        if (lastFetchedDate > this.Data[i].lastFetchedIntraday) {
          lastFetchedDate = this.Data[i].lastFetchedIntraday;
          lastFetchedIndex = i;
        }
      }
    }
    //console.info({ lastFetchedType: 'Intraday', Date: new Date(lastFetchedDate).yyyymmddhhmmss(), Index: lastFetchedIndex, Symbol: Instruments.Data[lastFetchedIndex].Symbol });
    return this.Data[lastFetchedIndex];
  },
  GetLastFetchedSingleDay: function () {
    if (!this.Data[0].lastFetchedSingleDay) { this.Data[0].lastFetchedSingleDay = 0 };
    let lastFetchedDate = this.Data[0].lastFetchedSingleDay;
    let lastFetchedIndex = 0;
    for (let i = 1; i < this.Data.length; i++) {
      if (this.Data[i].Symbol != 'GBP' && this.Data[i].Symbol != 'GBp' && this.Data[i].TrackPriceHistory == 1) {
        if (!this.Data[i].lastFetchedSingleDay) { this.Data[i].lastFetchedSingleDay = 0 };
        if (lastFetchedDate > this.Data[i].lastFetchedSingleDay) {
          lastFetchedDate = this.Data[i].lastFetchedSingleDay;
          lastFetchedIndex = i;
        }
      }
    }
    //console.info({ lastFetchedType: 'SingleDay', Date: new Date(lastFetchedDate).yyyymmddhhmmss(), Index: lastFetchedIndex, Symbol: Instruments.Data[lastFetchedIndex].Symbol });
    return this.Data[lastFetchedIndex];
  },
  GetCurrentPrice: function (Instrument) {
    if (Instrument.CurrentPrice) {
      return Instrument.CurrentPrice;
    } else {
      for (let i = Instrument.IntradayData.length - 1; i >= 0; i--) {
        if (Instrument.IntradayData[i].close) {
          return Instrument.IntradayData[i].close;
        }
      }
      for (let i = Instrument.DailyData.length - 1; i >= 0; i--) {
        if (Instrument.DailyData[i].close) {
          return Instrument.DailyData[i].close;
        }
      }
    }
  },
  GetExchangeRate: function (fromCurrency, toCurrency) {
    if (fromCurrency == 'GBp' && toCurrency == 'GBP') {
      return 100;
    } else {
      if (fromCurrency == 'GBP' && toCurrency == 'GBP') {
        return 1;
      } else {
        let curr = this.GetSymbol(toCurrency + fromCurrency + '=X');
        if (curr) {
          //return curr.CurrentPrice;
          return this.GetCurrentPrice(curr);
        } else {
          curr = this.GetSymbol(toCurrency + fromCurrency + '=X');
          if (curr) {
            if (curr.CurrentPrice) {
              //return 1/curr.CurrentPrice;
              return 1 / this.GetCurrentPrice(curr);
            }
          }
        }
      }
    }
  },
  MarketIsOpen: function (instrument, preOpenThreshold, postCloseThreshold) {

    let currentMarketDT = new Date().getTime() + instrument.GMTOffset;
    
    if (currentMarketDT < (instrument.SingleDayStartDate - (preOpenThreshold ? preOpenThreshold : 0))) {
      return 0; //Not open yet
    } else {
      if (currentMarketDT > (instrument.SingleDayEndDate - (postCloseThreshold ? postCloseThreshold : 0))) {
        return 2; //Market closed (end of day)
      } else {
        return 1; //Market is currently open
      }
    }
  },
  GetCurrentTradingDay: function () {
    for (let x = 0; x < this.Data.length; x++) {
      let i = Instruments.Data[x];
      let marketCurrentDT = new Date().getTime() + i.GMTOffset;
      if (i.InstrumentType == 'EQUITY') {
        if (marketCurrentDT >= i.SingleDayStartDate && marketCurrentDT <= i.SingleDayEndDate) {
          //This instrument's market is currently open - return the current day
          return new Date(marketCurrentDT).yyyymmdd();
        }
      }
    }
    //No markets are open - return the current day
    return new Date().yyyymmdd();
  }
}

var Currencies = {
  Data: [],
  indexOfName: function (Name) { return this.Data.map(function (item) { return item.Name; }).indexOf(Name); },
  updateSymbol: function (Symbol, Name, CurrentPrice) {
    var newObject = { Name: Name, CurrentPrice: CurrentPrice, URL: 'https://uk.finance.yahoo.com/quote/' + Symbol };
    var i = this.indexOfName(Name);
    if (i >= 0) {
      this.Data[i] = newObject;
    } else {
      this.Data.push(newObject);
    }
  }
}
var previousDay, previousClose, previousWeek, previousWeekClose;

Date.prototype.yyyymmddhhmmss = function () {
  var yyyy = this.getFullYear().toString();
  var mm = (this.getMonth() + 1).toString(); // getMonth() is zero-based         
  var dd = this.getDate().toString();
  var hh = this.getHours().toString();
  var nn = this.getMinutes().toString();
  var ss = this.getSeconds().toString();
  return yyyy + '-' + (mm[1] ? mm : "0" + mm[0]) + '-' + (dd[1] ? dd : "0" + dd[0]) + " " + (hh[1] ? hh : "0" + hh[0]) + ":" + (nn[1] ? nn : "0" + nn[0]) + ":" + (ss[1] ? ss : "0" + ss[0]);
};
Date.prototype.yyyymmdd = function () {
  var yyyy = this.getFullYear().toString();
  var mm = (this.getMonth() + 1).toString(); // getMonth() is zero-based         
  var dd = this.getDate().toString();
  return yyyy + '-' + (mm[1] ? mm : "0" + mm[0]) + '-' + (dd[1] ? dd : "0" + dd[0]);
};
Date.prototype.hhmm = function () {
  var hh = this.getHours().toString();
  var nn = this.getMinutes().toString();
  return (hh[1] ? hh : "0" + hh[0]) + ":" + (nn[1] ? nn : "0" + nn[0]);
};
Number.prototype.autoDP = function () {
  let a = Math.abs(this);
  if (a >= 10000) {
    return this.toFixed(0);
  } else {
    if (a >= 1000) {
      return this.toFixed(1);
    } else {
      if (a < 0.001) {
        return this.toExponential(2);
      } else {
        return this.toFixed(2);
      }
    }
  }
};
Number.prototype.autoScale = function () {
  let a = Math.abs(this);
  if (a >= 1000000000) {
    return (this / 1000000000).toFixed(1) + 'b';
  } else {
    if (a >= 1000000) {
      return (this/1000000).toFixed(1) + 'm';
    } else {
      if (a >= 100) {
        return this.toFixed(0);
      } else {
        if (a >= 10) {
          return this.toFixed(1);
        } else {
          if (a < 0.001) {
            return this.toExponential(2);
          } else {
            return this.toFixed(2);
          }
        }
      }
    }
  }
};

function logInfo(msg) {
  console.info(msg);
  if (typeof msg == 'object') {
    $("#logDiv").css("font-color", "white").text(JSON.stringify(msg));
  } else {
    $("#logDiv").css("font-color", "white").text(msg);
  }
}
function logWarning(msg) {
  console.warn(msg);
  if (typeof msg == 'object') {
    $("#logDiv").css("font-color", "orange").text(JSON.stringify(msg));
  } else {
    $("#logDiv").css("font-color", "orange").text(msg);
  }
}
function logError(msg) {
  console.error(msg);
  if (typeof msg == 'object') {
    $("#logDiv").css("font-color", "red").text(JSON.stringify(msg));
  } else {
    $("#logDiv").css("font-color", "red").text(msg);
  }
}
function formatAmount(amount, currency, decimals) {
  if (amount) {
    let isNegative = (amount < 0);
    let s = Math.abs(amount).toFixed(decimals);
    if (Math.abs(amount) < 0.01) {
      s = Math.abs(amount).toPrecision(2);
    }
    let pos = s.indexOf('.');
    let wholeNumber = (pos >= 0) ? s.substring(0, pos) : s;
    let decimal = (pos >= 0) ? s.substring(pos) : '';
    s = '';
    while (wholeNumber.length > 0) {
      //if (s.length > 0 && wholeNumber!='-') s = ',' + s; //Don't add a comma if the only thing left to add to the string is the minus sign
      if (s.length > 0) s = ',' + s;
      if (wholeNumber.length >= 3) {
        s = wholeNumber.substring(wholeNumber.length - 3) + s;
        wholeNumber = wholeNumber.substring(0, wholeNumber.length - 3);
      } else {
        s = wholeNumber + s;
        wholeNumber = '';
      }
    }
    s = s + decimal;
    let sign = isNegative ? '-' : '';
    switch (currency) {
      case 'GBP':
        s = sign + '£' + s;
        break;
      case 'GBp':
        s = sign + s + 'p';
        break;
      case 'USD':
        s = sign + 'U$' + s;
        break;
      case 'AUD':
        s = sign + 'A$' + s;
        break;
      case 'EUR':
        s = sign + '€' + s;
        break;
      default:
        s = sign + currency + s;
    }
    return s;
  } else {
    return '';
  }
}
function getProfitOrLoss(BasePrice, CurrentPrice, ShowPercent) {
  //let priceMovement = (CurrentPrice - BasePrice).toFixed(2);
  let priceMovement = (CurrentPrice - BasePrice).autoDP();
  let percentMovement = (((CurrentPrice / BasePrice) - 1) * 100).toFixed(1) + '%';
  if (percentMovement[0] == '-') percentMovement = percentMovement.substring(1);
  return '<span class="' + (CurrentPrice < BasePrice ? 'loss' : 'profit') + '">' + (CurrentPrice < BasePrice ? '' : '+') + priceMovement + (ShowPercent ? ' (' + percentMovement + ')' : '') + '</span>';
}
function createCategoryAggregator() {
  let aggregator = {
    data: [],
    indexOfCategory: function (categoryName) { return this.data.map(function (item) { return item.categoryName; }).indexOf(categoryName); },
    getCategory: function (categoryName) { return this.data[this.indexOfCategory(categoryName)]; },
    addCategoryAmount: function (categoryName, amount) {
      let c = this.getCategory(categoryName);
      if (c) {
        c.total += amount;
      } else {
        this.data.push({ categoryName: categoryName, total: amount });
      }
    },
    getChartData: function () {
      let d = []
      for (let i = 0; i < this.data.length; i++) {
        d.push({ label: this.data[i].categoryName, data: this.data[i].total });
      }
      return d;
    }
  };
  return aggregator;
}
function generateAxisBreaks(data, maxGapSize, breakSize) {
  let breaks = [];
  if (data.length > 1) {
    let previousX = data[0][0];

    for (let i = 1; i < data.length; i++) {
      if (data[i][0] != null && data[i][1] != null) {
        let currentX = data[i][0];
        if (currentX - previousX > maxGapSize) {
          let newBreak = {
            from: previousX,
            to: currentX,
            breakSize: breakSize
          };
          breaks.push(newBreak);
        }
        previousX = currentX;
      }
    }
  }
  return breaks;
}

function getInstruments() {
  try {
    //logInfo('getInstruments()');
    initialPageTitle = document.title;
    if (fetchTimer != 0) { clearTimeout(fetchTimer) };
    indexMarquee.init($('#divIndexMarquee'), '&emsp;');
    $.ajax({
      type: "POST",
      contentType: "application/json",
      url: "SharePrices.aspx/GetInstruments",
      dataType: "json",
      success: function (response) {
        //logInfo('getInstruments Success');
        Instruments.Data = response.Instruments;
        Instruments.Accounts = response.Accounts;
        for (let ix = 0; ix < Instruments.Data.length; ix++) {
          let i = Instruments.Data[ix];
          i.Holdings = [];
          i.HoldingsHistory = [];
        }
        let symbol = '';
        let instrument = {};
        for (let i = 0; i < response.Holdings.length; i++) {
          if (symbol != response.Holdings[i].Symbol) {
            symbol = response.Holdings[i].Symbol;
            instrument = Instruments.GetSymbol(response.Holdings[i].Symbol);
          }
          instrument.Holdings.push(response.Holdings[i]);
        }
        for (let i = 0; i < response.HoldingsHistory.length; i++) {
          let h = response.HoldingsHistory[i];
          if (symbol != h.Symbol) {
            symbol = h.Symbol;
            instrument = Instruments.GetSymbol(h.Symbol);
          }
          instrument.HoldingsHistory.push(h);
        }
        createSharesTable();
        getAllLocalDailyData();
        tablesUpdateTimings.timer = setInterval(updateTables, 500);
      },
      failure: function (response) { console.error("GetInstruments error: " + response.d) }
    });
  } catch (err) {
    logError({ MSG: "Error in getInstruments", err: err });
  }
};

function createSharesTable() {
  try {
    let excludeNoHoldings = $('#excludeNoHoldings').prop('checked');
    //logInfo('createSharesTable()');
    var tbl = '<table id="tblInstruments"><tr><th>Instrument</th>' +
                                             '<th>All</th>' +
                                             '<th>3 Month</th>' +
                                             '<th>2 Week</th>' +
                                             '<th>1 Day</th></tr>';
    for (var rowNo = 0; rowNo < Instruments.Data.length; rowNo++) {
      let i = Instruments.Data[rowNo];
      //if (i.DisplayOrder) {
      //if ((!excludeNoHoldings) || i.InstrumentType != 'EQUITY' || Instruments.GetCurrentHoldings(i).length > 0) {
      if ((!excludeNoHoldings) || (i.InstrumentType != 'EQUITY' && i.InstrumentType != 'CRYPTOCURRENCY') || Instruments.GetCurrentHoldings(i).length > 0) {
        if (i.Symbol != 'GBP') {
          let cls = (rowNo % 2) ? 'shareRow altShareRow' : 'shareRow';
          let tr = '<tr class="' + cls + '" id="shareRow' + i.DisplayOrder.toString() + '">' + createSharesTableRow(i) + '</tr>';
          tbl += tr;
        }
      }
    }
    tbl += '</table>';
    $('#chartsDiv').html(tbl);
    //getAllLocalDailyData();
  } catch (err) {
    logError({ MSG: "Error in createSharesTable", err: err });
  }
};
function createSharesTableRow(instrument) {
  function getHoldingsTable(Instrument) {
    if (Instrument.Holdings.length > 0) {
      let totalUnits = 0;
      let totalPrice = 0;
      let totalPriceGBP = 0;
      let noRows = 0;
      let t = '<table class="miniHoldings"><tr><th>Account</th><th>Buy Date</th><th>Price</th><th>Units</th><th>Total</th></tr>';
      for (let i = 0; i < Instrument.Holdings.length; i++) {
        let h = Instrument.Holdings[i];
        if (h.SoldDate == null) {
          noRows++;
          totalUnits += h.NoUnits;
          totalPrice += (h.NoUnits * h.PurchasePricePerUnit);
          totalPriceGBP += h.BookCostGBP;
          t += '<tr><td><span class="deletebutton" onclick="showModalDialog_SoldHolding(' + h.HoldingID + ')">&times;</span>' + h.AccountName + '</td>' +
                   '<td>' + new Date(h.PurchaseDate).yyyymmdd() + '</td>' +
                   '<td class="num">' + formatAmount(h.PurchasePricePerUnit, '', 2) + '</td>' +
                   //'<td class="num">' + formatAmount(h.NoUnits, '', 0) + '</td>' +
                   '<td class="num">' + h.NoUnits.autoScale() + '</td>' +
                   '<td class="num">' + formatAmount(h.NoUnits * h.PurchasePricePerUnit, '', 0) + '</td></tr>';
        } else {
          if (Instrument.ShowPreviousHoldings | 0 == 1) {
            noRows++;
            t += '<tr class="soldHolding" title="Sold: ' + new Date(h.SoldDate).yyyymmddhhmmss() + '"><td>' + h.AccountName + '</td>' +
                     '<td>' + new Date(h.PurchaseDate).yyyymmdd() + '</td>' +
                     '<td class="num">' + formatAmount(h.PurchasePricePerUnit, '', 2) + '</td>' +
                     //'<td class="num">' + formatAmount(h.NoUnits, '', 0) + '</td>' +
                     '<td class="num">' + h.NoUnits.autoScale() + '</td>' +
                     '<td class="num">' + formatAmount(h.NoUnits * h.PurchasePricePerUnit, '', 0) + '</td></tr>';
          }
        }
      }
      //if (Instrument.Holdings.length > 1) {
      if (totalUnits > 0) {
        Instrument.TotalUnitsHeld = totalUnits;
        Instrument.TotalHoldingsCost = totalPrice;
        Instrument.TotalHoldingsCostGBP = totalPriceGBP;
        //Instrument.BreakevenPrice = totalPrice / totalUnits;
        Instrument.BreakevenPrice = totalPrice / totalUnits;
        t += '<tr><td class="no-border"></td>' +
                 '<td class="no-border">Total</td>' +
                 '<td class="num">' + formatAmount(Instrument.BreakevenPrice, '', 2) + '</td>' +
                 //'<td class="num">' + formatAmount(Instrument.TotalUnitsHeld, '', 0) + '</td>' +
                 '<td class="num">' + Instrument.TotalUnitsHeld.autoScale() + '</td>' +
                 //'<td class="num">' + formatAmount(Instrument.TotalHoldingsCost, '', 0) + '</td>' +
                 '<td class="num">' + formatAmount(Instrument.TotalHoldingsCost, '', 0) + '</td>' +
             '</tr>';
      }
      t += '</table>';
      return (noRows>0) ? t : '';
    } else {
      delete Instrument.TotalUnitsHeld;
      delete Instrument.TotalHoldingsCost;
      delete Instrument.TotalHoldingsCostGBP;
      delete Instrument.BreakevenPrice;
      return '';
    }
  }
  
  let dispOrder = instrument.DisplayOrder.toString();
  let breakevenIcon = (instrument.ShowBreakevenLine | 0) == 1 ? 'icons/Breakeven.png' : 'icons/Breakeven_Off.png';
  let previousHoldingsIcon = (instrument.ShowPreviousHoldings | 0) == 1 ? 'icons/History.png' : 'icons/History_Off.png';
  return '<td><span alt="' + instrument.InstrumentName + '" class="instrumentName">' + instrument.DisplayName + '</span><br>' + //'<td><span class="instrumentName">' + instrument.InstrumentName + '</span><br>' +
           '<a target="_blank" href="https://uk.finance.yahoo.com/quote/' + instrument.Symbol + '">' + instrument.Symbol + '</a>&nbsp;&nbsp;' +
           //'<span class="addHolding" data-symbol="' + instrument.Symbol + '">+</span><div class="spacer"></div>' +
           '<img src="icons/Plus.png" alt="Add Holding" width="16" height="16" onclick="showModalDialog_AddHolding(' + "'" + instrument.Symbol + "'" + ')" />&nbsp;&nbsp;' +
           '<img src="icons/UpArrow.png" alt="Move Instrument Up" width="16" height="16" onclick="moveInstrumentUp(' + "'" + instrument.Symbol + "'" + ')" />&nbsp;' +
           '<img src="icons/DownArrow.png" alt="Move Instrument Up" width="16" height="16" onclick="moveInstrumentDown(' + "'" + instrument.Symbol + "'" + ')" />&nbsp;&nbsp;' +
           '<img src="' + breakevenIcon + '" class="breakeven-button" alt="Show Breakeven Line" width="16" height="16" onclick="toggleBreakevenLine(' + "'" + instrument.Symbol + "'" + ')" />&nbsp;&nbsp;' +
           '<img src="' + previousHoldingsIcon + '" alt="Show Previous Holdings" width="16" height="16" onclick="togglePreviousHoldings(' + "'" + instrument.Symbol + "'" + ')" />&nbsp;&nbsp;' +
           '<img src="icons/Chart.png" alt="Full Screen Chart" width="16" height="16" onclick="showModalDialog_FullScreenChart(' + "'" + instrument.Symbol + "'" + ')" />' +

           getHoldingsTable(instrument) +
           '<div class="price-summary" id="divCurrentValue' + dispOrder + '">' + getCurrentValueContent(instrument) + '</div>' +
         '</td>' +
         '<td class="longContainer"><div class="LongChart" id="divLongChart' + dispOrder + '"></div><div class="LongSummary" id="divLongSummary' + dispOrder + '" /></td>' +
         '<td class="midContainer"><div class="MidChart" id="divMidChart' + dispOrder + '"></div><div class="MidSummary" id="divMidSummary' + dispOrder + '" /></td>' +
         '<td class="shortContainer"><div class="ShortChart" id="divShortChart' + dispOrder + '"></div><div class="ShortSummary" id="divShortSummary' + dispOrder + '" /></td>' +
         '<td class="dayContainer"><div class="DayChart" id="divDayChart' + dispOrder + '"></div><div class="DaySummary" id="divDaySummary' + dispOrder + '" /></td>'

         //'<td><div class="DayChart" id="divDayFlotChart' + dispOrder + '"></div><div class="DayChart" id="divDayChart' + dispOrder + '"></div><div class="DaySummary" id="divDaySummary' + dispOrder + '" /></td>'
};
function getCurrentValueContent(Instrument) {
  let currentValue = '';
  if (Instrument.CurrentPrice) {
    if (Instrument.TotalUnitsHeld > 0) {
      currentValue = '<div class="spacer"></div>';
      //currentValue += 'Price (' + Instrument.Currency + '): ' + (Instrument.BreakevenPrice < 0.1 ? Instrument.BreakevenPrice.toPrecision(3) : Instrument.BreakevenPrice.toFixed(2)) + ' -&gt; ' + (Instrument.CurrentPrice < 0.1 ? Instrument.CurrentPrice.toPrecision(3) : Instrument.CurrentPrice.toFixed(2)) + ': ' + getProfitOrLoss(Instrument.BreakevenPrice, Instrument.CurrentPrice) + '<br>';
      currentValue += 'Price (' + Instrument.Currency + '): ' + (Instrument.BreakevenPrice < 0.1 ? Instrument.BreakevenPrice.autoScale() : Instrument.BreakevenPrice.toFixed(2)) + ' -&gt; ' + (Instrument.CurrentPrice < 0.1 ? Instrument.CurrentPrice.autoScale() : Instrument.CurrentPrice.toFixed(2)) + ': ' + getProfitOrLoss(Instrument.BreakevenPrice, Instrument.CurrentPrice) + '<br>';
      //currentValue += 'Value: ' + (Instrument.TotalHoldingsCost).toFixed(2) + ' -&gt; ' + (Instrument.TotalUnitsHeld * Instrument.CurrentPrice).toFixed(2) + ': ' + getProfitOrLoss(Instrument.TotalHoldingsCost, Instrument.TotalUnitsHeld * Instrument.CurrentPrice);
      currentValue += 'Book Cost (GBP): ' + (Instrument.TotalHoldingsCostGBP).toFixed(2);
      let exRate = Instruments.GetExchangeRate(Instrument.Currency, 'GBP');
      if (exRate) {
        if (Instrument.Currency != 'GBp') {
          currentValue += '<br>GBP/' + Instrument.Currency + ' = ' + exRate.toFixed(4);
        }
        currentValue += '<div class="current-value spacer">';
        //currentValue += 'Current value:<br>' + currencyFormatter.format(Instrument.TotalUnitsHeld * Instrument.CurrentPrice / exRate) + ': ' + getProfitOrLoss(Instrument.TotalHoldingsCostGBP / exRate, Instrument.TotalUnitsHeld * Instrument.CurrentPrice / exRate, 1);
        currentValue += 'Current value:<br>' + currencyFormatter.format(Instrument.TotalUnitsHeld * Instrument.CurrentPrice / exRate) + ': ' + getProfitOrLoss(Instrument.TotalHoldingsCostGBP, Instrument.TotalUnitsHeld * Instrument.CurrentPrice / exRate, 1);
        currentValue += '</div>';
      } else {
        currentValue += '<br>' + Instrument.Currency;
      }
    } else {
      //INDEX, CURRENCY, EQUITY
      if (Instrument.InstrumentType == 'CURRENCY') {
        currentValue = 'Exchange Rate: ' + Instrument.CurrentPrice.toFixed(4);
      } else {
        if (Instrument.InstrumentType == 'CRYPTOCURRENCY' && Math.abs(Instrument.CurrentPrice) < 0.001) {
          currentValue = 'Price (' + Instrument.Currency + '): ' + Instrument.CurrentPrice.toExponential();
        } else {
          currentValue = 'Price (' + Instrument.Currency + '): ' + Instrument.CurrentPrice.toFixed(2);
        }
      }
    }
  }
  return currentValue;
}
function moveInstrumentUp(symbol) {
  let i = Instruments.indexOfSymbol(symbol);
  if (i > 0) {
    swapInstrumentDisplayOrders(symbol, Instruments.Data[i - 1].Symbol);
  }
}
function moveInstrumentDown(symbol) {
  let i = Instruments.indexOfSymbol(symbol);
  if (i < Instruments.Data.length -1) {
    swapInstrumentDisplayOrders(symbol, Instruments.Data[i + 1].Symbol);
  }
}
function swapInstrumentDisplayOrders(symbol1, symbol2) {
  $.ajax({
    type: "POST",
    contentType: "application/json",
    url: "SharePrices.aspx/SwapInstrumentDisplayOrders",
    dataType: "json",
    data: JSON.stringify({Symbol1: symbol1, Symbol2: symbol2}),
    success: function (response) {
      //{Symbol1, Symbol1NewDisplayOrder, Symbol2, Symbol2NewDisplayOrder}
      let i1 = Instruments.indexOfSymbol(response.Symbol1);
      let i2 = Instruments.indexOfSymbol(response.Symbol2);

      //Update the DisplayOrders in the existing instruments
      Instruments.Data[i1].DisplayOrder = response.Symbol1NewDisplayOrder;
      Instruments.Data[i2].DisplayOrder = response.Symbol2NewDisplayOrder;

      //Swap the instruments in the array
      let temp = Instruments.Data[i1];
      Instruments.Data[i1] = Instruments.Data[i2];
      Instruments.Data[i2] = temp;

      //Swap the rows in the charts table
      let i = Instruments.Data[i1];
      $("#shareRow" + i.DisplayOrder).html(createSharesTableRow(i));
      i = Instruments.Data[i2];
      $("#shareRow" + i.DisplayOrder).html(createSharesTableRow(i));
      
      //Redraw the charts on both rows
      redrawShareRowCharts(Instruments.Data[i1]);
      redrawShareRowCharts(Instruments.Data[i2]);
    },
    failure: function (response) { console.error("swapInstrumentDisplayOrders error:"); console.info(response); }
  });
}
function toggleBreakevenLine(symbol) {
  let i = Instruments.GetSymbol(symbol);
  i.ShowBreakevenLine = !(i.ShowBreakevenLine | 0);
  $("#shareRow" + i.DisplayOrder).html(createSharesTableRow(i)); //Redraw the entire share table row
  redrawShareRowCharts(i);
}
function togglePreviousHoldings(symbol) {
  let i = Instruments.GetSymbol(symbol);
  i.ShowPreviousHoldings = !(i.ShowPreviousHoldings | 0);
  $("#shareRow" + i.DisplayOrder).html(createSharesTableRow(i)); //Redraw the entire share table row
  redrawShareRowCharts(i);
}

function getNewRemoteData() {
  try {
    //logInfo("getNewRemoteData");
    //clearTimeout(fetchTimer);

    //logInfo(new Date().yyyymmddhhmmss() + ": getNewRemoteData()")

    let lastFetchedDailyInstrument = Instruments.GetLastFetchedDaily();
    if (lastFetchedDailyInstrument.lastFetchedDaily < (new Date().getTime() - fetchIntervalDaily)) {
      //logInfo(new Date().yyyymmddhhmmss() + ": Last fetched daily: " + lastFetchedDailyInstrument.Symbol + ", Last fetched date: " + new Date(lastFetchedDailyInstrument.lastFetchedDaily).yyyymmddhhmmss());
      getYahooDailyData(lastFetchedDailyInstrument);
    } else {
      let lastFetchedIntradayInstrument = Instruments.GetLastFetchedIntraday();
      if (lastFetchedIntradayInstrument.lastFetchedIntraday < (new Date().getTime() - fetchIntervalIntraday)) {
        //logInfo(new Date().yyyymmddhhmmss() + ": Last fetched intraday: " + lastFetchedIntradayInstrument.Symbol + ", Last fetched date: " + new Date(lastFetchedIntradayInstrument.lastFetchedIntraday).yyyymmddhhmmss());
        getYahooIntradayData(lastFetchedIntradayInstrument);
      } else {
        let lastFetchedSingleDayInstrument = Instruments.GetLastFetchedSingleDay();
        if (lastFetchedSingleDayInstrument.lastFetchedSingleDay < (new Date().getTime() - fetchIntervalSingleDay)) {
          //logInfo(new Date().yyyymmddhhmmss() + ": Last fetched single day: " + lastFetchedSingleDayInstrument.Symbol + ", Last fetched date: " + new Date(lastFetchedSingleDayInstrument.lastFetchedSingleDay).yyyymmddhhmmss());
          getYahooSingleDayData(lastFetchedSingleDayInstrument);
        } //else {
          //fetchInterval = 3000;
          //fetchTimer = setTimeout(getNewRemoteData, fetchInterval);
        //}
      }
    }
  } catch (err) {
    logError({ MSG: "Error in getNewRemoteData", err: err });
  }
}
function getAllLocalDailyData() {
  try {
    //logInfo("getAllLocalDailyData");
    let startDate = new Date(new Date().getFullYear(), 0, 1);
    //let endDate = new Date(new Date().setHours(0, 0, 0, 0));

    // Set start date to 2020-01-01 to include pre-pandemic prices
    startDate = new Date("2020-01-01T00:00:00");
    
    let endDate = new Date(new Date());
    let params = { startDate: startDate.getTime(), endDate: endDate.getTime() };
    $.ajax({
      type: "POST",
      contentType: "application/json",
      url: "SharePrices.aspx/GetAllDailyData",
      dataType: "json",
      data: JSON.stringify(params),
      success: function (response) {
        for (let x = 0; x < response.length; x++) {
          let i = Instruments.GetSymbol(response[x].Symbol);
          i.DailyData = response[x].DailyData;
        }
        getAllLocalIntradayData();
      },
      failure: function (response) { console.error("getYTDData error:"); console.info(response); }
    });
  } catch (err) {
    logError({ MSG: "Error in getAllLocalDailyData", err: err });
  }
}
function getAllLocalIntradayData() {
  try {
    logInfo("getAllLocalIntradayData");
    let startDate = new Date(new Date().getFullYear(), 0, 1);
    //let endDate = new Date(new Date().setHours(0, 0, 0, 0));
    let endDate = new Date(new Date());

    // Set start date to 2020-01-01 to include pre-pandemic prices
    startDate = new Date("2020-01-01T00:00:00");

    let params = { startDate: startDate.getTime(), endDate: endDate.getTime() };
    $.ajax({
      type: "POST",
      contentType: "application/json",
      url: "SharePrices.aspx/GetAllIntradayData",
      dataType: "json",
      data: JSON.stringify(params),
      success: function (response) {
        for (let x = 0; x < response.length; x++) {
          let i = Instruments.GetSymbol(response[x].Symbol);
          i.IntradayData = response[x].IntradayData;
          if (i.DailyData.length > 0) {
            createMidChart(i);
            createLongChart(i);
          }
          if (i.IntradayData.length > 0) {
            createShortChart(i);
          }
        }
        //fetchTimer = setTimeout(getNewRemoteData, 500);
        //fetchTimer = setInterval(getNewRemoteData, 1250);
        fetchTimer = setInterval(getNewRemoteData, 1000);
      },
      failure: function (response) { console.error("getYTDData error:"); console.info(response); }
    });
  } catch (err) {
    logError({ MSG: "Error in getAllLocalIntradayData", err: err });
  }
}

function redrawAllSharesCharts() {
  let excludeNoHoldings = $('#excludeNoHoldings').prop('checked');
  for (let n = 0; n < Instruments.Data.length; n++) {
    let i = Instruments.Data[n];
    //if ((!excludeNoHoldings) || i.InstrumentType != 'EQUITY' || Instruments.GetCurrentHoldings(i).length > 0) {
    if ((!excludeNoHoldings) || (i.InstrumentType != 'EQUITY' && i.InstrumentType != 'CRYPTOCURRENCY') || Instruments.GetCurrentHoldings(i).length > 0) {
      redrawShareRowCharts(i);
    }
  }
}
function redrawShareRowCharts(instrument) {
  createLongChart(instrument);
  createMidChart(instrument);
  createShortChart(instrument);
  createSingleDayChart(instrument);
}
function createLongChart(Instrument) {
  let rowNo = Instrument.DisplayOrder;
  let ytdStartDate = new Date(new Date().getFullYear(), 0, 1).getTime();

  // Set start date to 2020-01-01 to include pre-pandemic prices
  ytdStartDate = new Date("2020-01-01T00:00:00");

  let ytdEndDate = new Date().getTime();
  //if (Instrument.LongChart) {
  try { Instrument.LongChart.destroy(); } catch (ex) { }
  Instrument.LongChart = createChart(Instrument, ytdStartDate, ytdEndDate, 'divLongChart' + rowNo.toString(), 'divLongSummary' + rowNo.toString());
}
function createMidChart(Instrument) {
  let rowNo = Instrument.DisplayOrder;
  let quarterStartDate = new Date().setHours(0, 0, 0) - (timespans.oneMonth * 3);
  let quarterEndDate = new Date().getTime();
  try { Instrument.MidChart.destroy(); } catch (ex) { }
  Instrument.MidChart = createChart(Instrument, quarterStartDate, quarterEndDate, 'divMidChart' + rowNo.toString(), 'divMidSummary' + rowNo.toString());
}
function createShortChart(Instrument) {
  let rowNo = Instrument.DisplayOrder;
  let fortnightStartDate = new Date().setHours(0, 0, 0) - (timespans.oneWeek * 2);
  let fortnightEndDate = new Date().getTime();
  try { Instrument.ShortChart.destroy(); } catch (ex) { }
  Instrument.ShortChart = createChart(Instrument, fortnightStartDate, fortnightEndDate, 'divShortChart' + rowNo.toString(), 'divShortSummary' + rowNo.toString());
}
function createSingleDayChart(Instrument) {
  let rowNo = Instrument.DisplayOrder;
  let dayStartDate = 0;
  let dayEndDate = 0;
  let dt = new Date().getTime();
  dayStartDate = Instrument.SingleDayStartDate;
  dayEndDate = Instrument.SingleDayEndDate;
  let previousClose = Instrument.SingleDayPreviousClose;

  try { Instrument.SingleDayChart.destroy(); } catch (ex) { }

  Instrument.SingleDayChart = createChart(Instrument, dayStartDate, dayEndDate, 'divDayChart' + rowNo.toString(), 'divDaySummary' + rowNo.toString(), previousClose);
  //createChart_Flot(Instrument, dayStartDate, dayEndDate, 'divDayFlotChart' + rowNo.toString(), 'divDaySummary' + rowNo.toString(), previousClose);
}
function createChart_Flot(Instrument, startDate, endDate, chartContainerID, summaryContainerID, previousClose) {
  function getChartDataSubset(Instrument, dateFrom, dateTo) {
    let result = [];
    //Single day data can be passed direct
    if (dateTo - dateFrom <= timespans.oneDay) {
      result = Instrument.SingleDayData;
      //return Instrument.SingleDayData;
    } else {
      //Anything over one month should only use daily data
      if (dateTo - dateFrom > (timespans.oneMonth + (timespans.oneDay*4))) {
        for (let i = 0; i < Instrument.DailyData.length; i++) {
          let quote = Instrument.DailyData[i];
          if (quote.DT >= dateFrom && quote.DT <= dateTo) {
            result.push(quote);
          }
        }
      } else {
        //Get all in-date-range intraday data
        let foundDates = { min: new Date().getTime(), max: 0 }
        for (let i = 0; i < Instrument.IntradayData.length; i++) {
          let quote = Instrument.IntradayData[i];
          if (quote.DT >= dateFrom && quote.DT <= dateTo) {
            if (quote.DT > foundDates.max) foundDates.max = quote.DT;
            if (quote.DT < foundDates.min) foundDates.min = quote.DT;
            result.push(quote);
          }
        }
        //Back-fill with daily data if the intraday data doesn't cover the whole date range
        let minFoundDate = new Date(foundDates.min).setHours(0, 0, 0);
        if (minFoundDate > dateFrom) {
          for (let i = Instrument.DailyData.length - 1; i > 0; i--) {
            let quote = Instrument.DailyData[i];
            if (quote.DT >= dateFrom && quote.DT < minFoundDate) {
              result.unshift(quote);
            }
          }
        }
      }
    }
    return result;
  }
  function getBreakevenLine(Instrument, dateFrom, dateTo) {
    let result = [];
    let currentPrice = 0;
    for (let i = 0; i < Instrument.HoldingsHistory.length; i++) {
      let h = Instrument.HoldingsHistory[i];
      if (currentPrice > 0) { result.push([h.DT - 1, currentPrice]) };
      if (h.TotalUnits > 0) {
        currentPrice = h.TotalCost / h.TotalUnits;
        result.push([h.DT, (h.TotalCost / h.TotalUnits)]);
      } else {
        currentPrice = 0;
        result.push([h.DT, null]);
      }
    }
    if (currentPrice > 0) {
      result.push([dateTo, currentPrice]);
    }

    //Remove all points before the start of the date range
    let i = result.length - 1;
    while (i >= 0 && result[i][0] >= startDate) i--;
    while (i > 0) { result.shift(); i--; }
    
    return result;
  }
  function transformAndSummariseChartData(cd) {
    let seriesData = [];
    //Find the first entry within the date range
    let x = 0;
    let summaryData = {
      StartDT: cd[x].DT,
      EndDT: cd[cd.length - 1].DT,
      Open: cd[x].open,
      High: cd[x].high,
      HighDT: cd[x].DT,
      Low: cd[x].low,
      LowDT: cd[x].DT,
      Close: cd[cd.length - 1].close
    };
    while (x < cd.length && cd[x].DT < startDate) {
      summaryData.StartDT = cd[x].DT;
      summaryData.EndDT = cd[cd.length - 1].DT;
      summaryData.Open = cd[x].open;
      summaryData.High = cd[x].high;
      summaryData.HighDT = cd[x].DT;
      summaryData.Low = cd[x].low;
      summaryData.LowDT = cd[x].DT;
      summaryData.Close = cd[cd.length - 1].close;
      x++;
    };
    let basePrice = (!previousClose) ? summaryData.Open : previousClose;
    for (let i = 0; i < cd.length; i++) {
      let entry = cd[i];
      if (summaryData.High < entry.high) {
        summaryData.High = entry.high;
        summaryData.HighDT = cd[x].DT;
      }
      if (summaryData.Low > entry.low) {
        summaryData.Low = entry.low;
        summaryData.LowDT = cd[x].DT;
      }
      if (i >= x) {
        if (priceType == 'percent') {
          seriesData.push([entry.DT, ((entry.close / basePrice) - 1) * 100]);
        } else {
          seriesData.push([entry.DT, entry.close]);
        }
      }
    }
    return { summaryData: summaryData, seriesData: seriesData, basePrice: basePrice };
  }
  
  let chartContainer = $("#" + chartContainerID);
  let priceType = $('#displayAsPercent').prop('checked') ? 'percent' : '';
  let chartData = [];
  if (endDate - endDate <= timespans.oneDay) {
    chartData = getChartDataSubset(Instrument, startDate, endDate);
  } else {
    chartData = getChartDataSubset(Instrument, startDate - (timespans.oneDay * 3), endDate);
  }
  
  let seriesData = [];
  let volumeData = [];
  if (chartData.length > 1) {
    //Summarise the data and transform values to percentage if required
    let summaryData = {
      StartDT: chartData[0].DT,
      EndDT: chartData[chartData.length - 1].DT,
      Open: chartData[0].open,
      High: chartData[0].high,
      HighDT: chartData[0].DT,
      Low: chartData[0].low,
      LowDT: chartData[0].DT,
      Close: chartData[chartData.length - 1].close
    };
    let basePrice = (!previousClose) ? summaryData.Open : previousClose;
    for (let i = 0; i < chartData.length; i++) {
      let entry = chartData[i];
      if (summaryData.High < entry.high) {
        summaryData.High = entry.high;
        summaryData.HighDT = entry.DT;
      }
      if (summaryData.Low > entry.low) {
        summaryData.Low = entry.low;
        summaryData.LowDT = entry.DT;
      }
      if (priceType == 'percent') {
        seriesData.push([entry.DT, ((entry.close / basePrice) - 1) * 100]);
      } else {
        seriesData.push([entry.DT, entry.close]);
      }
      volumeData.push([entry.DT, entry.volume]);
    }
        
    //let weekendMarkings = getWeekendMarkings(startDate, endDate);
    let color = (summaryData.Close > basePrice) ? "#00cc00" : (summaryData.Close < basePrice) ? "red" : "blue";
    let series = [{ data: volumeData, yaxis: 2, color: "#304e57", bars: { show: true } }];

    //Add the breakeven price line
    if ((Instrument.ShowBreakevenLine | 0) == 1) {
      if (priceType == 'percent') {
        series.unshift({ data: [[startDate, ((Instrument.BreakevenPrice / basePrice) - 1) * 100], [endDate, ((Instrument.BreakevenPrice / basePrice) - 1) * 100]], color: '#24BCC7', shadowSize: 0, lines: { lineWidth: 1 } });
      } else {
        let b = getBreakevenLine(Instrument, startDate, endDate);
        if (b.length > 0 && b[0][0]<endDate && b[b.length-1][0]>startDate) { //Only add the series if it falls within the chart date range
          series.push({ data: b, color: 'green', shadowSize: 0, lines: { lineWidth: 1.5 } });
        }
      }
    }

    //Add the previousClose line to the series
    if (previousClose) {
      if (priceType == 'percent') {
        series.unshift({ data: [[startDate, ((previousClose / basePrice) - 1) * 100], [endDate, ((previousClose / basePrice) - 1) * 100]], color: '#3b88ff', shadowSize: 0, lines: { lineWidth: 1 } });
      } else {
        series.unshift({ data: [[startDate, previousClose], [endDate, previousClose]], color: '#3b88ff', shadowSize: 0, lines: { lineWidth: 1 } });
      }
    }

    //Add the volume bars to the end of the series, so it is plotted behind all other lines
    series.push({ data: seriesData, color: color, shadowSize: 0, lines: { lineWidth: 1 } });
    
    //Dispose of any previous chart to prevent memory leaks (orphaaned DOM nodes)
    let oldChart = chartContainer.data('plot');
    if (oldChart) {// If it's destroyed, then data('plot') will be undefined
      oldChart.destroy();
    }

    //Draw the chart
    let chartTimespan = endDate - startDate;
    let gapMap = [];
    if (chartTimespan > timespans.oneDay && chartTimespan < timespans.oneMonth * 3) {
      gapMap = generateChartGapMap(seriesData, timespans.oneHour);
    }
    function volumeAxisFormatter(val, axis) {
      if (val >= 1000000000)
        return (val / 1000000000).toFixed(axis.tickDecimals) + "b";
      else if (val >= 1000000)
        return (val / 1000000).toFixed(axis.tickDecimals) + "m";
      else if (val >= 1000)
        return (val / 1000).toFixed(axis.tickDecimals) + "k";
      else
        return val.toFixed(axis.tickDecimals);
    }
    var plot;
    try {
      plot = $.plot(chartContainer, series, {
        series: { downsample: { threshold: 100 } },
        grid: { show: true, hoverable: true, color: 'white', borderColor: '#505050' /*, markings: weekendMarkings*/ },
        /*crosshair: { mode: 'x', snapX: true, showXValue: false },*/
        xaxis: {
          mode: "time",
          timezone: 'browser',
          min: chartTimespan > timespans.oneDay ? summaryData.StartDT : startDate,
          max: chartTimespan > timespans.oneDay ? summaryData.EndDT : endDate,
          transform: function (v) { return mapChartXValue(v, gapMap); },
          inverseTransform: function (v) { return unmapChartXValue(v, gapMap); }
        },
        yaxes: [{ position: 'left' }, { position: 'right', tickFormatter: volumeAxisFormatter }]
      });
    }
    catch (ex) {
      logError(ex.message);
    }
    
    //Bind the chart tooltip
    if (plot) {
      chartContainer.bind("plothover", function (event, pos, item) {
        if (!pos.x || !pos.y) { return; }
        if (item) {
          let x = new Date(item.datapoint[0]).yyyymmddhhmmss();
          let y = item.datapoint[1].toFixed(2);
          let content = x + " = " + y;

          let o = plot.getPlotOffset();
          let t = o.top;
          let l = o.left;

          //let tt = $("#" + chartContainerID + " .chart-tooltip");
          let tt = chartContainer.children(".chart-tooltip");
          if (tt.length > 0) {
            tt.html(content).css({ top: t, left: l }).fadeIn(200);
          } else {
            tt = "<div class='chart-tooltip' style='top: " + t + "; left: " + l + ";'>" + content + "</div>";
            chartContainer.append(tt).fadeIn(200);
          }
        } else {
          //$("#" + chartContainerID + " .chart-tooltip").fadeOut(1);
          chartContainer.children(".chart-tooltip").fadeOut(1);
        }
      });
    }

    //Create the summary table
    let summaryTable = '<table class="summary">';
    if ((endDate - startDate) <= timespans.oneDay) {
      //Add market times and previous close info for Single Dat chart
      let dt = new Date().getTime();
      let marketHours = (new Date(Instrument.SingleDayStartDate)).hhmm() + ' - ' + (new Date(Instrument.SingleDayEndDate)).hhmm();
      summaryTable += '<tr><td>' + ((dt >= Instrument.SingleDayStartDate && dt <= Instrument.SingleDayEndDate) ? '<span style="color:limegreen">Market Open' : '<span style="color:firebrick">Market Closed') + '</span> (' + marketHours + ')</td>';
      summaryTable += (!previousClose) ? '' : ('<td colspan="2">Prev close: ' + previousClose.toString() + '</td>');
      summaryTable += '</tr>';
    }
    
    summaryTable += '<tr><td>O: ' + summaryData.Open.autoDP() + ' ' + ((previousClose) ? getProfitOrLoss(previousClose, summaryData.Open, 1) : '') + '</td>';
    summaryTable += '<td title="' + new Date(summaryData.HighDT).yyyymmddhhmmss() + '">H: ' + summaryData.High.autoDP() + ' ' + getProfitOrLoss(basePrice, summaryData.High, 1) + '</td><tr>';
    summaryTable += '<tr><td title="' + new Date(summaryData.LowDT).yyyymmddhhmmss() + '">L: ' + summaryData.Low.autoDP() + ' ' + getProfitOrLoss(basePrice, summaryData.Low, 1) + '</td>';
    summaryTable += '<td>C: ' + summaryData.Close.autoDP() + ' ' + getProfitOrLoss(basePrice, summaryData.Close, 1) + '</tr></td>';

    //Create CurrentPrice for this instrument if it doesn't exist yet
    Instrument.CurrentPrice = Instrument.CurrentPrice || summaryData.Close;
    
    summaryTable += '</table>';
    $('#' + summaryContainerID).html(summaryTable);
  } else {
    //console.warn('Not enough data to plot ' + Instrument.Symbol + ' ' + new Date(startDate).yyyymmddhhmmss() + ' -> ' + new Date(endDate).yyyymmddhhmmss() + ' to ' + chartContainerID);
  }
}
function createChart(Instrument, startDate, endDate, chartContainerID, summaryContainerID, previousClose) {
  function getChartDataSubset(Instrument, dateFrom, dateTo) {
    let result = [];
    //Single day data can be passed direct
    if (dateTo - dateFrom <= timespans.oneDay) {
      result = Instrument.SingleDayData;
      //return Instrument.SingleDayData;
    } else {
      //Anything over one month should only use daily data
      if (dateTo - dateFrom > (timespans.oneMonth + (timespans.oneDay * 4))) {
        for (let i = 0; i < Instrument.DailyData.length; i++) {
          let quote = Instrument.DailyData[i];
          if (quote.DT >= dateFrom && quote.DT <= dateTo) {
            result.push(quote);
          }
        }
      } else {
        //Get all in-date-range intraday data
        let foundDates = { min: new Date().getTime(), max: 0 }
        for (let i = 0; i < Instrument.IntradayData.length; i++) {
          let quote = Instrument.IntradayData[i];
          if (quote.DT >= dateFrom && quote.DT <= dateTo) {
            if (quote.DT > foundDates.max) foundDates.max = quote.DT;
            if (quote.DT < foundDates.min) foundDates.min = quote.DT;
            result.push(quote);
          }
        }
        //Back-fill with daily data if the intraday data doesn't cover the whole date range
        let minFoundDate = new Date(foundDates.min).setHours(0, 0, 0);
        if (minFoundDate > dateFrom) {
          for (let i = Instrument.DailyData.length - 1; i > 0; i--) {
            let quote = Instrument.DailyData[i];
            if (quote.DT >= dateFrom && quote.DT < minFoundDate) {
              result.unshift(quote);
            }
          }
        }
      }
    }
    return result;
  }
  function getBreakevenLine(Instrument, dateFrom, dateTo) {
    let result = [];
    let currentPrice = 0;
    for (let i = 0; i < Instrument.HoldingsHistory.length; i++) {
      let h = Instrument.HoldingsHistory[i];
      if (currentPrice > 0) { result.push([h.DT - 1, currentPrice]) };
      if (h.TotalUnits > 0) {
        currentPrice = h.TotalCost / h.TotalUnits;
        result.push([h.DT, (h.TotalCost / h.TotalUnits)]);
      } else {
        currentPrice = 0;
        result.push([h.DT, null]);
      }
    }
    if (currentPrice > 0) {
      result.push([dateTo, currentPrice]);
    }

    //Remove all points before the start of the date range
    let i = result.length - 1;
    while (i >= 0 && result[i][0] >= startDate) i--;
    while (i > 0) { result.shift(); i--; }
    
    return result;
  }
  function transformAndSummariseChartData(cd) {
    let seriesData = [];
    //Find the first entry within the date range
    let x = 0;
    let summaryData = {
      StartDT: cd[x].DT,
      EndDT: cd[cd.length - 1].DT,
      Open: cd[x].open,
      High: cd[x].high,
      HighDT: cd[x].DT,
      Low: cd[x].low,
      LowDT: cd[x].DT,
      Close: cd[cd.length - 1].close
    };
    while (x < cd.length && cd[x].DT < startDate) {
      summaryData.StartDT = cd[x].DT;
      summaryData.EndDT = cd[cd.length - 1].DT;
      summaryData.Open = cd[x].open;
      summaryData.High = cd[x].high;
      summaryData.HighDT = cd[x].DT;
      summaryData.Low = cd[x].low;
      summaryData.LowDT = cd[x].DT;
      summaryData.Close = cd[cd.length - 1].close;
      x++;
    };
    let basePrice = (!previousClose) ? summaryData.Open : previousClose;
    for (let i = 0; i < cd.length; i++) {
      let entry = cd[i];
      if (summaryData.High < entry.high) {
        summaryData.High = entry.high;
        summaryData.HighDT = cd[x].DT;
      }
      if (summaryData.Low > entry.low) {
        summaryData.Low = entry.low;
        summaryData.LowDT = cd[x].DT;
      }
      if (i >= x) {
        if (priceType == 'percent') {
          seriesData.push([entry.DT, ((entry.close / basePrice) - 1) * 100]);
        } else {
          seriesData.push([entry.DT, entry.close]);
        }
      }
    }
    return { summaryData: summaryData, seriesData: seriesData, basePrice: basePrice };
  }

  let chartContainer = $('#' + chartContainerID);
  if (chartContainer.length > 0) {
    let chart = {};
    let priceType = $('#displayAsPercent').prop('checked') ? 'percent' : '';

    let chartData = [];
    if (endDate - endDate <= timespans.oneDay) {
      chartData = getChartDataSubset(Instrument, startDate, endDate);
    } else {
      chartData = getChartDataSubset(Instrument, startDate - (timespans.oneDay * 3), endDate);
    }

    let seriesData = [];
    let volumeData = [];
    if (chartData && chartData.length > 1) {
      
      //Summarise the data and transform values to percentage if required
      let summaryData = {
        StartDT: chartData[0].DT,
        EndDT: chartData[chartData.length - 1].DT,
        Open: chartData[0].open,
        High: chartData[0].high,
        HighDT: chartData[0].DT,
        Low: chartData[0].low,
        LowDT: chartData[0].DT,
        Close: chartData[chartData.length - 1].close
      };
      let basePrice = (!previousClose) ? summaryData.Open : previousClose;
      for (let i = 0; i < chartData.length; i++) {
        let entry = chartData[i];
        if (summaryData.High < entry.high) {
          summaryData.High = entry.high;
          summaryData.HighDT = entry.DT;
        }
        if (summaryData.Low > entry.low) {
          summaryData.Low = entry.low;
          summaryData.LowDT = entry.DT;
        }
        if (priceType == 'percent') {
          seriesData.push([entry.DT, ((entry.close / basePrice) - 1) * 100]);
        } else {
          seriesData.push([entry.DT, entry.close]);
        }
        volumeData.push([entry.DT, entry.volume]);
      }

      let series = [{ name: 'Volume', type: 'column', yAxis: 1, color: "#304e57", borderColor: "#304e57", data: volumeData }];

      //Add the breakeven price line
      if ((Instrument.ShowBreakevenLine | 0) == 1) {
        if (priceType == 'percent') {
          series.unshift({ name: 'Breakeven', type: 'line', color: '#24BCC7', lineWidth: 1, marker: { enabled: false }, data: [[startDate, ((Instrument.BreakevenPrice / basePrice) - 1) * 100], [endDate, ((Instrument.BreakevenPrice / basePrice) - 1) * 100]] /* shadowSize: 0 */ });
        } else {
          let b = getBreakevenLine(Instrument, startDate, endDate);
          if (b.length > 0 && b[0][0] < endDate && b[b.length - 1][0] > startDate) { //Only add the series if it falls within the chart date range
            series.push({ name: 'Breakeven', type: 'line', yAxis: 0, color: 'green', lineWidth: 1, marker: { enabled: false }, data: b /* shadowSize: 0 */ });
          }
        }
      }

      //Add the previousClose line to the series
      if (previousClose) {
        if (priceType == 'percent') {
          series.unshift({ name: 'Previous Close', type: 'line', yAxis: 0, color: '#3b88ff', lineWidth: 1, marker: { enabled: false }, data: [[startDate, ((previousClose / basePrice) - 1) * 100], [endDate, ((previousClose / basePrice) - 1) * 100]] /* shadowSize: 0 */ });
        } else {
          series.unshift({ name: 'Previous Close', type: 'line', yAxis: 0, color: '#3b88ff', lineWidth: 1, marker: { enabled: false }, data: [[startDate, previousClose], [endDate, previousClose]] /* shadowSize: 0 */ });
        }
      }

      //Add the price data to the end of the series, so it is plotted above all other lines
      let color = (summaryData.Close > basePrice) ? "#00cc00" : (summaryData.Close < basePrice) ? "red" : "blue";
      series.push({ name: 'Price', type: 'line', color: color, lineWidth: 1, data: seriesData /* shadowSize: 0 */ });

      //Draw the chart
      let chartTimespan = endDate - startDate;
      let xAxisBreaks = [];
      if (chartTimespan > timespans.oneDay && chartTimespan < timespans.oneMonth * 3) {
        xAxisBreaks = generateAxisBreaks(seriesData, timespans.oneHour, timespans.oneHour);
      }
      
      try {
        chart = Highcharts.chart(chartContainerID, {
          series: series,
          chart: {
            backgroundColor: 'rgba(0,0,0,0)',
            spacingTop: 5,
            spacingBottom: 0,
            spacingLeft: 2,
            spacingRight: 0
          },
          title: { text: undefined },
          legend: { enabled: false },
          xAxis: [{
            type: 'datetime',
            lineColor: '#505050',
            tickColor: '#505050',
            tickPixelInterval: 60,
            min: chartTimespan > timespans.oneDay ? summaryData.StartDT : startDate,
            max: chartTimespan > timespans.oneDay ? summaryData.EndDT : endDate,
            breaks: xAxisBreaks,
            labels: { style: { color: '#b0b0b0', fontSize: 'x-small' } }
          }],
          yAxis: [{
            title: { text: undefined },
            gridLineColor: '#505050',
            tickPixelInterval: 30,
            endOnTick: false,
            tickLength: 20,
            labels: { style: { color: '#b0b0b0', fontSize: 'x-small' } }
          },
          {
            title: { text: undefined },
            /*gridLineColor: '#505050',*/
            gridLineWidth: 0,
            tickPixelInterval: 30,
            endOnTick: false,
            opposite: true,
            labels: { style: { color: '#b0b0b0', fontSize: 'x-small' } }
          }],
          credits: { enabled: false }
        });
      }
      catch (ex) {
        logError(ex.message);
      }

      //Create the summary table
      let summaryTable = '<table class="summary">';
      if ((endDate - startDate) <= timespans.oneDay) {
        //Add market times and previous close info for Single Day chart
        let dt = new Date().getTime();
        let marketHours = (new Date(Instrument.SingleDayStartDate)).hhmm() + ' - ' + (new Date(Instrument.SingleDayEndDate)).hhmm();
        summaryTable += '<tr><td>' + ((dt >= Instrument.SingleDayStartDate && dt <= Instrument.SingleDayEndDate) ? '<span style="color:limegreen">Market Open' : '<span style="color:firebrick">Market Closed') + '</span> (' + marketHours + ')</td>';
        summaryTable += (!previousClose) ? '' : ('<td colspan="2">Prev close: ' + previousClose.toString() + '</td>');
        summaryTable += '</tr>';
      }

      summaryTable += '<tr><td>O: ' + summaryData.Open.autoDP() + ' ' + ((previousClose) ? getProfitOrLoss(previousClose, summaryData.Open, 1) : '') + '</td>';
      summaryTable += '<td title="' + new Date(summaryData.HighDT).yyyymmddhhmmss() + '">H: ' + summaryData.High.autoDP() + ' ' + getProfitOrLoss(basePrice, summaryData.High, 1) + '</td><tr>';
      summaryTable += '<tr><td title="' + new Date(summaryData.LowDT).yyyymmddhhmmss() + '">L: ' + summaryData.Low.autoDP() + ' ' + getProfitOrLoss(basePrice, summaryData.Low, 1) + '</td>';
      summaryTable += '<td>C: ' + summaryData.Close.autoDP() + ' ' + getProfitOrLoss(basePrice, summaryData.Close, 1) + '</tr></td>';

      //Create CurrentPrice for this instrument if it doesn't exist yet
      Instrument.CurrentPrice = Instrument.CurrentPrice || summaryData.Close;

      summaryTable += '</table>';
      $('#' + summaryContainerID).html(summaryTable);
    } else {
      //console.warn('Not enough data to plot ' + Instrument.Symbol + ' ' + new Date(startDate).yyyymmddhhmmss() + ' -> ' + new Date(endDate).yyyymmddhhmmss() + ' to ' + chartContainerID);
    }

    return chart;
  }
}
function createFullScreenChart(Instrument, startDate, endDate) {
  function getChartDataSubset(Instrument, dateFrom, dateTo) {
    let result = [];
    //Single day data can be passed direct
    if (dateTo - dateFrom <= timespans.oneDay) {
      result = Instrument.SingleDayData;
    } else {
      //Get all in-date-range intraday data
      let foundDates = { min: new Date().getTime(), max: 0 }
      for (let i = 0; i < Instrument.IntradayData.length; i++) {
        let quote = Instrument.IntradayData[i];
        if (quote.DT >= dateFrom && quote.DT <= dateTo) {
          if (quote.DT > foundDates.max) foundDates.max = quote.DT;
          if (quote.DT < foundDates.min) foundDates.min = quote.DT;
          result.push(quote);
        }
      }
      //Back-fill with daily data if the intraday data doesn't cover the whole date range
      let minFoundDate = new Date(foundDates.min).setHours(0, 0, 0);

      if (minFoundDate > dateFrom) {
        for (let i = Instrument.DailyData.length - 1; i > 0; i--) {
          let quote = Instrument.DailyData[i];
          if (quote.DT >= dateFrom && quote.DT < minFoundDate) {
            result.unshift(quote);
          }
        }
      }
    }

    return result;
  }
  function getBreakevenLine(Instrument, dateFrom, dateTo) {
    let result = [];
    let currentPrice = 0;
    for (let i = 0; i < Instrument.HoldingsHistory.length; i++) {
      let h = Instrument.HoldingsHistory[i];
      if (currentPrice > 0) { result.push([h.DT - 1, currentPrice]) };
      if (h.TotalUnits > 0) {
        currentPrice = h.TotalCost / h.TotalUnits;
        result.push([h.DT, (h.TotalCost / h.TotalUnits)]);
      } else {
        currentPrice = 0;
        result.push([h.DT, null]);
      }
    }
    if (currentPrice > 0) {
      result.push([dateTo, currentPrice]);
    }

    return result;
  }

  let chartContainerID = 'divFullScreenChart';
  
  let priceType = $('#displayAsPercent').prop('checked') ? 'percent' : '';
  let chartData = getChartDataSubset(Instrument, startDate, endDate);
  let seriesData = [];
  let volumeData = [];

  if (chartData.length > 1) {
    //Summarise the data and transform values to percentage if required
    let summaryData = {
      StartDT: chartData[0].DT,
      EndDT: chartData[chartData.length - 1].DT,
      Open: chartData[0].open,
      High: chartData[0].high,
      HighDT: chartData[0].DT,
      Low: chartData[0].low,
      LowDT: chartData[0].DT,
      Close: chartData[chartData.length - 1].close
    };
    let basePrice = summaryData.Open;
    for (let i = 0; i < chartData.length; i++) {
      let entry = chartData[i];
      if (summaryData.High < entry.high) {
        summaryData.High = entry.high;
        summaryData.HighDT = entry.DT;
      }
      if (summaryData.Low > entry.low) {
        summaryData.Low = entry.low;
        summaryData.LowDT = entry.DT;
      }
      if (priceType == 'percent') {
        seriesData.push([entry.DT, ((entry.close / basePrice) - 1) * 100]);
      } else {
        seriesData.push([entry.DT, entry.close]);
      }
      volumeData.push([entry.DT, entry.volume]);
    }

    let color = (summaryData.Close > basePrice) ? "#00cc00" : (summaryData.Close < basePrice) ? "red" : "blue";
    let series = [{ name: 'Volume', type: 'column', yAxis: 1, color: "#304e57", borderColor: "#304e57", data: volumeData }];

    //Add the breakeven price line
    if (priceType == 'percent') {
      series.unshift({ name: 'Breakeven', type: 'line', yAxis: 0, color: '#24BCC7', lineWidth: 1, data: [[startDate, ((Instrument.BreakevenPrice / basePrice) - 1) * 100], [endDate, ((Instrument.BreakevenPrice / basePrice) - 1) * 100]] });
    } else {
      let b = getBreakevenLine(Instrument, startDate, endDate);
      if (b.length > 0 && b[0][0] < endDate && b[b.length - 1][0] > startDate) { //Only add the series if it falls within the chart date range
        series.push({ name: 'Breakeven', type: 'line', yAxis: 0, color: 'green', lineWidth: 1, data: b });
      }
    }
    
    //Add the price data to the end of the series, so it is plotted over all other lines
    series.push({ name: 'Price', type: 'line', yAxis: 0, color: color, lineWidth: 1, data: seriesData });

    //Draw the chart
    let chartTimespan = endDate - startDate;
    let xAxisBreaks = [];
    if (chartTimespan > timespans.oneDay && chartTimespan < timespans.oneMonth * 3) {
      xAxisBreaks = generateAxisBreaks(series[0].data, timespans.oneHour, timespans.oneHour);
    }
    
    let chart = Highcharts.chart(chartContainerID, {
      series: series,
      chart: {
        backgroundColor: 'rgba(0,0,0,0)',
        zoomType: 'x'
      },
      title: { text: undefined },
      legend: {
        itemStyle: { color: '#b0b0b0' }
      },
      xAxis: [{
        type: 'datetime',
        lineColor: '#505050',
        tickColor: '#505050',
        //tickPixelInterval: 60,
        min: chartTimespan > timespans.oneDay ? summaryData.StartDT : startDate,
        max: chartTimespan > timespans.oneDay ? summaryData.EndDT : endDate,
        breaks: xAxisBreaks,
        labels: { style: { color: '#b0b0b0' /*, fontSize: 'x-small' */} }
      }],
      yAxis: [{
        title: {
          text: 'Price (' + Instrument.Currency + ')',
          style: { color: '#b0b0b0' }
        },
        gridLineColor: '#505050',
        //endOnTick: false,
        labels: { style: { color: '#b0b0b0' /*, fontSize: 'x-small'*/ } }
      },
      {
        title: {
          text: 'Volume',
          style: { color: '#b0b0b0' }
        },
        /*gridLineColor: '#505050',*/
        gridLineWidth: 0,
        endOnTick: false,
        opposite: true,
        labels: { style: { color: '#b0b0b0' /*, fontSize: 'x-small'*/ } }
      }],
      credits: { enabled: false }
    });
  } else {
    //console.warn('Not enough data to plot ' + Instrument.Symbol + ' ' + new Date(startDate).yyyymmddhhmmss() + ' -> ' + new Date(endDate).yyyymmddhhmmss() + ' to ' + chartContainerID);
  }
}
function createFullScreenChart_Flot(Instrument, startDate, endDate) {
  function getChartDataSubset(Instrument, dateFrom, dateTo) {
    let result = [];
    //Single day data can be passed direct
    if (dateTo - dateFrom <= timespans.oneDay) {
      result = Instrument.SingleDayData;
    } else {
      //Anything over 3 months should only use daily data
      if (dateTo - dateFrom > ((3 * timespans.oneMonth) + (timespans.oneDay * 4))) {
        for (let i = 0; i < Instrument.DailyData.length; i++) {
          let quote = Instrument.DailyData[i];
          if (quote.DT >= dateFrom && quote.DT <= dateTo) {
            result.push(quote);
          }
        }
      } else {
        //Get all in-date-range intraday data
        let foundDates = { min: new Date().getTime(), max: 0 }
        for (let i = 0; i < Instrument.IntradayData.length; i++) {
          let quote = Instrument.IntradayData[i];
          if (quote.DT >= dateFrom && quote.DT <= dateTo) {
            if (quote.DT > foundDates.max) foundDates.max = quote.DT;
            if (quote.DT < foundDates.min) foundDates.min = quote.DT;
            result.push(quote);
          }
        }
        //Back-fill with daily data if the intraday data doesn't cover the whole date range
        let minFoundDate = new Date(foundDates.min).setHours(0, 0, 0);
        if (minFoundDate > dateFrom) {
          for (let i = Instrument.DailyData.length - 1; i > 0; i--) {
            let quote = Instrument.DailyData[i];
            if (quote.DT >= dateFrom && quote.DT < minFoundDate) {
              result.unshift(quote);
            }
          }
        }
      }
    }

    return result;
  }
  function getBreakevenLine(Instrument, dateFrom, dateTo) {
    let result = [];
    let currentPrice = 0;
    for (let i = 0; i < Instrument.HoldingsHistory.length; i++) {
      let h = Instrument.HoldingsHistory[i];
      if (currentPrice > 0) { result.push([h.DT - 1, currentPrice]) };
      if (h.TotalUnits > 0) {
        currentPrice = h.TotalCost / h.TotalUnits;
        result.push([h.DT, (h.TotalCost / h.TotalUnits)]);
      } else {
        currentPrice = 0;
        result.push([h.DT, null]);
      }
    }
    if (currentPrice > 0) {
      result.push([dateTo, currentPrice]);
    }

    return result;
  }

  let chartContainerID = 'divFullScreenChart';
  let chartContainer = $("#" + chartContainerID);

  let priceType = $('#displayAsPercent').prop('checked') ? 'percent' : '';
  let chartData = getChartDataSubset(Instrument, startDate, endDate);
  let seriesData = [];
  let volumeData = [];

  if (chartData.length > 1) {
    //Summarise the data and transform values to percentage if required
    let summaryData = {
      StartDT: chartData[0].DT,
      EndDT: chartData[chartData.length - 1].DT,
      Open: chartData[0].open,
      High: chartData[0].high,
      HighDT: chartData[0].DT,
      Low: chartData[0].low,
      LowDT: chartData[0].DT,
      Close: chartData[chartData.length - 1].close
    };
    let basePrice = summaryData.Open;
    for (let i = 0; i < chartData.length; i++) {
      let entry = chartData[i];
      if (summaryData.High < entry.high) {
        summaryData.High = entry.high;
        summaryData.HighDT = entry.DT;
      }
      if (summaryData.Low > entry.low) {
        summaryData.Low = entry.low;
        summaryData.LowDT = entry.DT;
      }
      if (priceType == 'percent') {
        seriesData.push([entry.DT, ((entry.close / basePrice) - 1) * 100]);
      } else {
        seriesData.push([entry.DT, entry.close]);
      }
      volumeData.push([entry.DT, entry.volume]);
    }

    //let weekendMarkings = getWeekendMarkings(startDate, endDate);
    let color = (summaryData.Close > basePrice) ? "#00cc00" : (summaryData.Close < basePrice) ? "red" : "blue";
    let series = [{ data: volumeData, yaxis: 2, color: "#304e57", bars: { show: true } }];

    //Add the breakeven price line
    //if ($('#showBreakevenLine').prop('checked')) {
    if (priceType == 'percent') {
      series.unshift({ data: [[startDate, ((Instrument.BreakevenPrice / basePrice) - 1) * 100], [endDate, ((Instrument.BreakevenPrice / basePrice) - 1) * 100]], color: '#24BCC7', shadowSize: 0, lines: { lineWidth: 1 } });
    } else {
      let b = getBreakevenLine(Instrument, startDate, endDate);
      if (b.length > 0 && b[0][0] < endDate && b[b.length - 1][0] > startDate) { //Only add the series if it falls within the chart date range
        series.push({ data: b, color: 'green', shadowSize: 0, lines: { lineWidth: 1.5 } });
      }
    }
    //}

    //Add the price data to the end of the series, so it is plotted over all other lines
    series.push({ data: seriesData, color: color, shadowSize: 0, lines: { lineWidth: 1 } });

    //Draw the chart
    let chartTimespan = endDate - startDate;
    let gapMap = [];
    if (chartTimespan > timespans.oneDay && chartTimespan < timespans.oneMonth * 3) {
      gapMap = generateChartGapMap(series[0].data, timespans.oneHour);
    }
    function volumeAxisFormatter(val, axis) {
      if (val >= 1000000000)
        return (val / 1000000000).toFixed(axis.tickDecimals) + "b";
      else if (val >= 1000000)
        return (val / 1000000).toFixed(axis.tickDecimals) + "m";
      else if (val >= 1000)
        return (val / 1000).toFixed(axis.tickDecimals) + "k";
      else
        return val.toFixed(axis.tickDecimals);
    }
    let plot = $.plot(chartContainer, series, {
      grid: { show: true, hoverable: true, clickable: true, color: 'white', borderColor: '#505050' },
      selection: { mode: "x" },
      xaxis: {
        mode: "time",
        timezone: 'browser',
        min: chartTimespan > timespans.oneDay ? summaryData.StartDT : startDate,
        max: chartTimespan > timespans.oneDay ? summaryData.EndDT : endDate,
        transform: function (v) { return mapChartXValue(v, gapMap); },
        inverseTransform: function (v) { return unmapChartXValue(v, gapMap); }
      },
      yaxes: [{ axisLabel: 'Price (' + Instrument.Currency + ')', position: 'left' }, { axisLabel: 'Volume', position: 'right', tickFormatter: volumeAxisFormatter }]
    });

    //Unbind the container from any previously bound events
    chartContainer.unbind();

    //Bind the plotselected event to allow zooming
    chartContainer.bind("plotselected", function (event, ranges) { createFullScreenChart(Instrument, Math.floor(ranges.xaxis.from), Math.ceil(ranges.xaxis.to)); });
    //Reset zoom level on right-click
    chartContainer.bind("contextmenu", function () { createFullScreenComparison(Instrument1, 0, new Date().getTime()); return false; });

    //Bind the chart tooltip
    chartContainer.bind("plothover", function (event, pos, item) {
      if (!pos.x || !pos.y) { return; }
      if (item) {
        let x = new Date(item.datapoint[0]).yyyymmddhhmmss();
        let y = item.datapoint[1].toFixed(2);
        let content = x + " = " + y;

        let o = plot.getPlotOffset();
        let t = o.top;
        let l = o.left;

        //let tt = $("#" + chartContainerID + " .chart-tooltip");
        let tt = chartContainer.children(".chart-tooltip");
        if (tt.length > 0) {
          tt.html(content).css({ top: t, left: l }).fadeIn(200);
        } else {
          tt = "<div class='chart-tooltip' style='top: " + t + "; left: " + l + ";'>" + content + "</div>";
          chartContainer.append(tt).fadeIn(200);
        }
      } else {
        //$("#" + chartContainerID + " .chart-tooltip").fadeOut(1);
        chartContainer.children(".chart-tooltip").fadeOut(1);
      }
    });
    chartContainer.bind("mouseout", function (event, pos, item) {
      //$("#chartTooltip").fadeOut(1);
      chartContainer.children(".chart-tooltip").fadeOut(1);
    });
  } else {
    //console.warn('Not enough data to plot ' + Instrument.Symbol + ' ' + new Date(startDate).yyyymmddhhmmss() + ' -> ' + new Date(endDate).yyyymmddhhmmss() + ' to ' + chartContainerID);
  }
}
function createFullScreenComparisonChart(Instrument1, Instrument2, startDate, endDate) {
  function getChartDataSubset(Instrument, dateFrom, dateTo) {
    let result = [];
    //Single day data can be passed direct
    if (dateTo - dateFrom <= timespans.oneDay) {
      result = Instrument.SingleDayData;
    } else {
      //Anything over 3 months should only use daily data
      if (dateTo - dateFrom > ((3 * timespans.oneMonth) + (timespans.oneDay * 4))) {
        for (let i = 0; i < Instrument.DailyData.length; i++) {
          let quote = Instrument.DailyData[i];
          if (quote.DT >= dateFrom && quote.DT <= dateTo) {
            result.push(quote);
          }
        }
      } else {
        //Get all in-date-range intraday data
        let foundDates = { min: new Date().getTime(), max: 0 }
        for (let i = 0; i < Instrument.IntradayData.length; i++) {
          let quote = Instrument.IntradayData[i];
          if (quote.DT >= dateFrom && quote.DT <= dateTo) {
            if (quote.DT > foundDates.max) foundDates.max = quote.DT;
            if (quote.DT < foundDates.min) foundDates.min = quote.DT;
            result.push(quote);
          }
        }
        //Back-fill with daily data if the intraday data doesn't cover the whole date range
        let minFoundDate = new Date(foundDates.min).setHours(0, 0, 0);
        if (minFoundDate > dateFrom) {
          for (let i = Instrument.DailyData.length - 1; i > 0; i--) {
            let quote = Instrument.DailyData[i];
            if (quote.DT >= dateFrom && quote.DT < minFoundDate) {
              result.unshift(quote);
            }
          }
        }
      }
    }

    return result;
  }
  function transformAndSummariseData(chartData, priceType) {
    let seriesData = [];

    //Summarise the data and transform values to percentage if required
    let summaryData = {
      StartDT: chartData[0].DT,
      EndDT: chartData[chartData.length - 1].DT,
      Open: chartData[0].open,
      High: chartData[0].high,
      HighDT: chartData[0].DT,
      Low: chartData[0].low,
      LowDT: chartData[0].DT,
      Close: chartData[chartData.length - 1].close
    };
    let basePrice = summaryData.Open;
    for (let i = 0; i < chartData.length; i++) {
      let entry = chartData[i];
      if (summaryData.High < entry.high) {
        summaryData.High = entry.high;
        summaryData.HighDT = entry.DT;
      }
      if (summaryData.Low > entry.low) {
        summaryData.Low = entry.low;
        summaryData.LowDT = entry.DT;
      }
      if (priceType == 'percent') {
        seriesData.push([entry.DT, ((entry.close / basePrice) - 1) * 100]);
      } else {
        seriesData.push([entry.DT, entry.close]);
      }
    }
    return { summaryData: summaryData, seriesData: seriesData };
  }
  
  let chartContainerID = 'divFullScreenChart';

  let instrument1Data = getChartDataSubset(Instrument1, startDate, endDate);
  let instrument2Data = getChartDataSubset(Instrument2, startDate, endDate);
  
  if (instrument1Data.length > 1 && instrument2Data.length > 1) {
    let sd = transformAndSummariseData(instrument1Data, 'percent');
    let price1Data = sd.seriesData;
    let price1Summary = sd.summaryData;
    sd = transformAndSummariseData(instrument2Data, 'percent');
    let price2Data = sd.seriesData;
    let price2Summary = sd.summaryData;

    let series = [{ name: Instrument1.InstrumentName + '(' + Instrument1.Symbol + ')', type: 'line', color: 'blue', lineWidth: 1, data: price1Data },
                  { name: Instrument2.InstrumentName + '(' + Instrument2.Symbol + ')', type: 'line', color: 'yellow', lineWidth: 1, data: price2Data }];

    //Draw the chart
    let chartTimespan = endDate - startDate;
    let xAxisBreaks = [];
    if (chartTimespan > timespans.oneDay && chartTimespan < timespans.oneMonth * 3) {
      xAxisBreaks = generateAxisBreaks(series[0].data, timespans.oneHour, timespans.oneHour);
    }

    let chart = Highcharts.chart(chartContainerID, {
      series: series,
      chart: {
        backgroundColor: 'rgba(0,0,0,0)',
        zoomType: 'x'
      },
      title: { text: undefined },
      legend: {
        itemStyle: { color: '#b0b0b0' }
      },
      xAxis: [{
        type: 'datetime',
        lineColor: '#505050',
        tickColor: '#505050',
        min: chartTimespan > timespans.oneDay ? price1Summary.StartDT : startDate,
        max: chartTimespan > timespans.oneDay ? price1Summary.EndDT : endDate,
        breaks: xAxisBreaks,
        labels: { style: { color: '#b0b0b0' /*, fontSize: 'x-small' */ } }
      }],
      yAxis: [{
        title: {
          text: 'Price Delta (%)',
          style: { color: '#b0b0b0' }
        },
        gridLineColor: '#505050',
        labels: { style: { color: '#b0b0b0' /*, fontSize: 'x-small'*/ } }
      }],
      credits: { enabled: false }
    });

  } else {
    chartContainer.html('Not enough data to plot ' + Instrument1.Symbol + ' vs ' + Instrument2.Symbol + ' ' + new Date(startDate).yyyymmddhhmmss() + ' -> ' + new Date(endDate).yyyymmddhhmmss());
  }
}
function createFullScreenComparisonChart_Flot(Instrument1, Instrument2, startDate, endDate) {
  function getChartDataSubset(Instrument, dateFrom, dateTo) {
    let result = [];
    //Single day data can be passed direct
    if (dateTo - dateFrom <= timespans.oneDay) {
      result = Instrument.SingleDayData;
    } else {
      //Anything over 3 months should only use daily data
      if (dateTo - dateFrom > ((3 * timespans.oneMonth) + (timespans.oneDay * 4))) {
        for (let i = 0; i < Instrument.DailyData.length; i++) {
          let quote = Instrument.DailyData[i];
          if (quote.DT >= dateFrom && quote.DT <= dateTo) {
            result.push(quote);
          }
        }
      } else {
        //Get all in-date-range intraday data
        let foundDates = { min: new Date().getTime(), max: 0 }
        for (let i = 0; i < Instrument.IntradayData.length; i++) {
          let quote = Instrument.IntradayData[i];
          if (quote.DT >= dateFrom && quote.DT <= dateTo) {
            if (quote.DT > foundDates.max) foundDates.max = quote.DT;
            if (quote.DT < foundDates.min) foundDates.min = quote.DT;
            result.push(quote);
          }
        }
        //Back-fill with daily data if the intraday data doesn't cover the whole date range
        let minFoundDate = new Date(foundDates.min).setHours(0, 0, 0);
        if (minFoundDate > dateFrom) {
          for (let i = Instrument.DailyData.length - 1; i > 0; i--) {
            let quote = Instrument.DailyData[i];
            if (quote.DT >= dateFrom && quote.DT < minFoundDate) {
              result.unshift(quote);
            }
          }
        }
      }
    }

    return result;
  }
  function transformAndSummariseData(chartData, priceType) {
    let seriesData = [];

    //Summarise the data and transform values to percentage if required
    let summaryData = {
      StartDT: chartData[0].DT,
      EndDT: chartData[chartData.length - 1].DT,
      Open: chartData[0].open,
      High: chartData[0].high,
      HighDT: chartData[0].DT,
      Low: chartData[0].low,
      LowDT: chartData[0].DT,
      Close: chartData[chartData.length - 1].close
    };
    let basePrice = summaryData.Open;
    for (let i = 0; i < chartData.length; i++) {
      let entry = chartData[i];
      if (summaryData.High < entry.high) {
        summaryData.High = entry.high;
        summaryData.HighDT = entry.DT;
      }
      if (summaryData.Low > entry.low) {
        summaryData.Low = entry.low;
        summaryData.LowDT = entry.DT;
      }
      if (priceType == 'percent') {
        seriesData.push([entry.DT, ((entry.close / basePrice) - 1) * 100]);
      } else {
        seriesData.push([entry.DT, entry.close]);
      }
    }
    return { summaryData: summaryData, seriesData: seriesData };
  }

  let chartContainerID = 'divFullScreenChart';
  let chartContainer = $("#" + chartContainerID);

  let priceType = 'percent';
  let instrument1Data = getChartDataSubset(Instrument1, startDate, endDate);
  let instrument2Data = getChartDataSubset(Instrument2, startDate, endDate);

  if (instrument1Data.length > 1 && instrument2Data.length > 1) {
    let sd = transformAndSummariseData(instrument1Data, priceType);
    let price1Data = sd.seriesData;
    let price1Summary = sd.summaryData;
    sd = transformAndSummariseData(instrument2Data, priceType);
    let price2Data = sd.seriesData;
    let price2Summary = sd.summaryData;

    let series = [{ label: Instrument1.InstrumentName + '(' + Instrument1.Symbol + ')', data: price1Data, color: 'blue', /*yaxis: 1,*/ shadowSize: 0, lines: { lineWidth: 1 } },
                  { label: Instrument2.InstrumentName + '(' + Instrument2.Symbol + ')', data: price2Data, color: 'yellow', /*yaxis: 2,*/ shadowSize: 0, lines: { lineWidth: 1 } }];

    //Draw the chart
    let chartTimespan = endDate - startDate;
    let gapMap = [];
    if (chartTimespan > timespans.oneDay && chartTimespan < timespans.oneMonth * 3) {
      gapMap = generateChartGapMap(series[0].data, timespans.oneHour);
    }
    let plot = $.plot(chartContainer, series, {
      grid: { show: true, hoverable: true, clickable: true, color: 'white', borderColor: '#505050' },
      selection: { mode: "x" },
      legend: { show: true, position: "nw", backgroundColor: 'transparent', backgroundOpacity: 0.3 },
      xaxis: {
        mode: "time",
        timezone: 'browser',
        min: chartTimespan > timespans.oneDay ? price1Summary.StartDT : startDate,
        max: chartTimespan > timespans.oneDay ? price1Summary.EndDT : endDate,
        transform: function (v) { return mapChartXValue(v, gapMap); },
        inverseTransform: function (v) { return unmapChartXValue(v, gapMap); }
      },
      yaxes: [{ axisLabel: '% Change', position: 'left' }/*,
              { axisLabel: Instrument2.InstrumentName + ' (' + Instrument2.Currency + ')', position: 'right' }*/]
    });

    //Unbind the container from any previously bound events
    chartContainer.unbind();

    //Bind the plotselected event to allow zooming
    chartContainer.bind("plotselected", function (event, ranges) { createFullScreenComparisonChart(Instrument1, Instrument2, Math.floor(ranges.xaxis.from), Math.ceil(ranges.xaxis.to)); });
    //Reset zoom level on right-click
    chartContainer.bind("contextmenu", function () { createFullScreenComparisonChart(Instrument1, Instrument2, 0, new Date().getTime()); return false; });

    //Bind the chart tooltip
    chartContainer.bind("plothover", function (event, pos, item) {
      if (!pos.x || !pos.y) { return; }
      if (item) {
        let x = new Date(item.datapoint[0]).yyyymmddhhmmss();
        let y = item.datapoint[1].toFixed(2);
        let content = x + " = " + y;

        let o = plot.getPlotOffset();
        let t = o.top;
        let l = o.left;

        //let tt = $("#" + chartContainerID + " .chart-tooltip");
        let tt = chartContainer.children(".chart-tooltip");
        if (tt.length > 0) {
          tt.html(content).css({ top: t, left: l }).fadeIn(200);
        } else {
          tt = "<div class='chart-tooltip' style='top: " + t + "; left: " + l + ";'>" + content + "</div>";
          chartContainer.append(tt).fadeIn(200);
        }
      } else {
        //$("#" + chartContainerID + " .chart-tooltip").fadeOut(1);
        chartContainer.children(".chart-tooltip").fadeOut(1);
      }
    });
    chartContainer.bind("mouseout", function (event, pos, item) {
      //$("#chartTooltip").fadeOut(1);
      chartContainer.children(".chart-tooltip").fadeOut(1);
    });

  } else {
    chartContainer.html('Not enough data to plot ' + Instrument1.Symbol + ' vs ' + Instrument2.Symbol + ' ' + new Date(startDate).yyyymmddhhmmss() + ' -> ' + new Date(endDate).yyyymmddhhmmss());
  }
}

function fixDecimals(val, place) {
    if (isNaN(val)) {
        return val;
    }
    const pow = Math.pow(10, place);
    return +(Math.round(val * pow) / pow);
}
function getYahooDailyData(Instrument) {
  //console.info('getYahooDailyData: ' + Instrument.Symbol);
  Instrument.lastFetchedDaily = new Date().getTime();
  $.ajax({
    type: "POST",
    contentType: "application/json",
    url: "SharePrices.aspx/FetchYahooFinanceDaily?S=" + Instrument.Symbol,
    dataType: "json",
    data: JSON.stringify({ Symbol: Instrument.Symbol, MinDailyDT: (Instrument.DailyData.length ? Instrument.DailyData[0].DT: new Date().getTime()) }),
    success: function (response) {
      let newData = [];
      if (response.chart && response.chart.result) {
        let dataSeries = response.chart.result[0];
        let nonSubmittedEntries = 0;
        for (let i = 0; i < dataSeries.timestamp.length; i++) {
          let quote = dataSeries.indicators.quote[0];
          if (quote.open[i] == null || quote.high[i] == null || quote.low[i] == null || quote.close[i] == null || quote.volume[i] == null) {
            //console.info("Not submitting entry with null values: " + JSON.stringify({ DT: dataSeries.timestamp[i] * 1000, open: quote.open[i], high: quote.high[i], low: quote.low[i], close: quote.close[i], volume: quote.volume[i] }));
            nonSubmittedEntries += 1;
          } else {
            //newData.push({ DT: dataSeries.timestamp[i] * 1000, open: quote.open[i], high: quote.high[i], low: quote.low[i], close: quote.close[i], volume: quote.volume[i] });
            if (Instrument.InstrumentType == 'CRYPTOCURRENCY') {
              newData.push({
                DT: dataSeries.timestamp[i] * 1000,
                open: quote.open[i],
                high: quote.high[i],
                low: quote.low[i],
                close: quote.close[i],
                volume: quote.volume[i]
              });
            } else {
              newData.push({
                DT: dataSeries.timestamp[i] * 1000,
                open: fixDecimals(quote.open[i], 4),
                high: fixDecimals(quote.high[i], 4),
                low: fixDecimals(quote.low[i], 4),
                close: fixDecimals(quote.close[i], 4),
                volume: quote.volume[i]
              });
            }
          }
        }
        if (nonSubmittedEntries > 0) {
          //console.warn("Didn't submit " + nonSubmittedEntries.toString() + " entries (" + newData.length.toString() + " OK) with null values for " + Instrument.Symbol);
        }

        //let d = new Date(newData[newData.length - 1].DT).yyyymmddhhmmss();
        //console.info("getYahooDailyData: " + Instrument.Symbol + ', Last data point: ' + d);

        if (newData.length > 0) {
          lastSuccessfulFetch = new Date();
        }

        submitNewDailyData(Instrument, newData);
        createMidChart(Instrument);
        createLongChart(Instrument);
      } else {
        //console.info("No data received for symbol: " + Instrument.Symbol);
        logWarning({ MSG: "No data received for symbol: " + Instrument.Symbol, response: response })
      }
    },
    failure: function (response) { console.error("getYahooDailyData error:"); console.info(response); }
  });
}
function getYahooIntradayData(Instrument) {
  //console.info('getYahooIntradayData: ' + Instrument.Symbol);
  Instrument.lastFetchedIntraday = new Date().getTime();
  let minIntradayDT = Instrument.IntradayData.length > 0 ? Instrument.IntradayData[0].DT : new Date().getTime();
  $.ajax({
    type: "POST",
    contentType: "application/json",
    url: "SharePrices.aspx/FetchYahooFinanceIntraday?S=" + Instrument.Symbol,
    dataType: "json",
    data: JSON.stringify({ Symbol: Instrument.Symbol, MinIntradayDT: minIntradayDT }),
    success: function (response) {
      let newData = [];
      if (response.chart && response.chart.result) {
        let dataSeries = response.chart.result[0];
        let nonSubmittedEntries = 0;
        if (dataSeries.timestamp) {
          for (let i = 0; i < dataSeries.timestamp.length; i++) {
            let quote = dataSeries.indicators.quote[0];
            if (quote.open[i] == null || quote.high[i] == null || quote.low[i] == null || quote.close[i] == null || quote.volume[i] == null) {
              nonSubmittedEntries += 1;
              //console.info("Not submitting entry with null values: " + JSON.stringify({ DT: dataSeries.timestamp[i] * 1000, open: quote.open[i], high: quote.high[i], low: quote.low[i], close: quote.close[i], volume: quote.volume[i] }));
            } else {
              if (Instrument.InstrumentType == 'CRYPTOCURRENCY') {
                newData.push({ DT: dataSeries.timestamp[i] * 1000,
                open: quote.open[i],
                high: quote.high[i],
                low: quote.low[i],
                close: quote.close[i],
                volume: quote.volume[i] });
              } else {
                newData.push({ DT: dataSeries.timestamp[i] * 1000,
                  open: fixDecimals(quote.open[i], 4),
                  high: fixDecimals(quote.high[i], 4),
                  low: fixDecimals(quote.low[i], 4),
                  close: fixDecimals(quote.close[i], 4),
                  volume: quote.volume[i] });
              }
            }
          }
          if (nonSubmittedEntries > 0) {
            //console.warn("Didn't submit " + nonSubmittedEntries.toString() + " entries (" + newData.length.toString() + " OK) with null values for " + Instrument.Symbol);
          }
          if (newData.length) {
            lastSuccessfulFetch = new Date();
            submitNewIntradayData(Instrument, newData);
            createMidChart(Instrument);
            createShortChart(Instrument);
          } else {
            console.info("No NEW data received for symbol: " + Instrument.Symbol);
          }
        } else {
          //logWarning({ MSG: "No timestamp series received for symbol: " + Instrument.Symbol, response: response });
        }
      } else {
        //console.warn("No data received for symbol: " + Instrument.Symbol);
        logWarning({ MSG: "No data received for symbol: " + Instrument.Symbol, response: response });
      }
    },
    failure: function (response) { console.error("getYahooIntradayData error:"); console.info(response); }
  });  
}
function getYahooSingleDayData(Instrument) {
  //console.info('getYahooSingleDayData: ' + Instrument.Symbol);
  Instrument.lastFetchedSingleDay = new Date().getTime();
  $.ajax({
    type: "POST",
    contentType: "application/json",
    url: "SharePrices.aspx/FetchYahooFinanceSingleDay?S=" + Instrument.Symbol,
    dataType: "json",
    data: JSON.stringify({ Symbol: Instrument.Symbol }),
    success: function (response) {
      let newData = [];
      if (response.chart && response.chart.result) {
        let dataSeries = response.chart.result[0];

        if (dataSeries.meta) {
          Instrument.SingleDayPreviousClose = dataSeries.meta.previousClose;
          Instrument.SingleDayStartDate = dataSeries.meta.currentTradingPeriod.regular.start * 1000;
          Instrument.SingleDayEndDate = dataSeries.meta.currentTradingPeriod.regular.end * 1000;
          Instrument.GMTOffset = dataSeries.meta.gmtoffset;
          Instrument.InstrumentType = dataSeries.meta.instrumentType;
          Instrument.Currency = dataSeries.meta.currency;
          Instrument.CurrentPrice = dataSeries.meta.regularMarketPrice;
        }

        if (response.chart.result[0].timestamp) {
          let ignoredEntries = 0;
          for (let i = 0; i < dataSeries.timestamp.length; i++) {
            let quote = dataSeries.indicators.quote[0];
            if (quote.open[i] == null || quote.high[i] == null || quote.low[i] == null || quote.close[i] == null || quote.volume[i] == null) {
              ignoredEntries++;
            } else {
              newData.push({ DT: dataSeries.timestamp[i] * 1000, open: quote.open[i], high: quote.high[i], low: quote.low[i], close: quote.close[i], volume: quote.volume[i] });
            }
          }
          if (ignoredEntries > 0) {
            //console.warn("Ignored " + ignoredEntries.toString() + " entries (" + newData.length.toString() + " OK) with null values for " + Instrument.Symbol);
          }
          if (newData.length > 0) {
            lastSuccessfulFetch = new Date();
          }

          if (!Instrument.SingleDayData || Instrument.SingleDayData[0].DT != newData[0].DT || newData.length > Instrument.SingleDayData.length) {
            Instrument.SingleDayData = newData;

            createSingleDayChart(Instrument);
            //updateHoldingsTable();
            //updateAnalysisTable();
            tablesUpdateTimings.updateNeeded = true;
          }
          
          Instrument.SingleDayOpenPrice = dataSeries.indicators.quote[0].open[0];

          $('#divCurrentValue' + Instrument.DisplayOrder).html(getCurrentValueContent(Instrument));

          if (Instrument.InstrumentType == "CURRENCY") {
            Currencies.updateSymbol(Instrument.Symbol, Instrument.InstrumentName, Instrument.CurrentPrice);
          }
        } else {
          //console.warn("No timestamp series received for symbol: " + Instrument.Symbol);
          //logWarning("No timestamp series received for symbol: " + Instrument.Symbol);
          logWarning({ MSG: "No timestamp series received for symbol: " + Instrument.Symbol, response: response });
        }
      } else {
        //console.info("No data received for symbol: " + Instrument.Symbol);
        //logInfo("No data received for symbol: " + Instrument.Symbol);
        logWarning({ MSG: "No data received for symbol: " + Instrument.Symbol, response: response });
      }
    },
    failure: function (response) { console.error("getYahooSingleDayData error:"); console.info(response); }
  });
}
function getLseSingleDayData(Instrument) {
  //console.info('getLseSingleDayData: ' + Instrument.Symbol);
  Instrument.lastFetchedSingleDay = new Date().getTime();
  let fromDate = new Date().setHours(0, 0, 0);
  let toDate = new Date().getTime();
  $.ajax({
    type: "POST",
    contentType: "application/json",
    url: "SharePrices.aspx/FetchLseSingleDay?S=" + Instrument.Symbol,
    dataType: "json",
    data: JSON.stringify({ Symbol: Instrument.Symbol, FromDate: fromDate, ToDate: toDate }),
    success: function (response) {
      console.info("getLseSingleDayData: " + JSON.stringify(response));
      
    },
    failure: function (response) { console.error("getLseSingleDayData error:"); console.info(response); }
  });
}

function submitNewDailyData(Instrument, NewData) {
  function indexOfDT(DT) {
    for (var i = 0; i < Instrument.DailyData.length; i++) {
      if (Instrument.DailyData[i].DT == DT) {
        return i;
      }
    }
    return -1;
  }

  //console.info("submitNewDailyData");
  let currentMaxDT = Instrument.DailyData.length ? Instrument.DailyData[Instrument.DailyData.length - 1].DT : 0;
  let currentMinDT = Instrument.DailyData.length ? Instrument.DailyData[0].DT : 0;

  //Add the new data to the DailyData array
  let resortRequired = false;
  for (let i = 0; i < NewData.length; i++) {
    NewData[i].DT = new Date(NewData[i].DT).setHours(0, 0, 0, 0); //Remove the hours/mins/secs from this DT as there should only be one entry per day
    let x = indexOfDT(NewData[i].DT);
    if (x >= 0) {
      //Don't update the existing entry for this DT as it may have been manually overridden in the DB
      //Instrument.DailyData[x] = NewData[i];
    } else {
      //This is a new entry
      if (!Instrument.DailyData) {
        Instrument.DailyData = [];
      }
      Instrument.DailyData.push(NewData[i]);
      if (NewData[i].DT <= currentMaxDT) {
        resortRequired = true;
      }
    }
  }
  //Re-sort the DailyData by date if required
  if (resortRequired) {
    Instrument.DailyData.sort(function compare(a, b) {
        if (a.DT < b.DT) {
          return -1;
        }
        if (a.DT > b.DT) {
          return 1;
        }
        return 0;
      }
    );
  }

  $.ajax({
    type: "POST",
    contentType: "application/json",
    url: "SharePrices.aspx/SubmitDailyData?S=" + Instrument.Symbol,
    dataType: "json",
    data: JSON.stringify({Symbol: Instrument.Symbol, DailyPrices: NewData}),
    success: function (response) {
      //console.info('  SubmitDailyData success: ' + Instrument.Symbol + ' - ' + JSON.stringify(response));
    },
    failure: function (response) { console.error("SubmitDailyData error: " + JSON.stringify(response)); }
  });
}
function submitNewIntradayData(Instrument, NewData) {
  //console.info("submitNewIntradayData");
  $.ajax({
    type: "POST",
    contentType: "application/json",
    url: "SharePrices.aspx/SubmitIntradayData?S=" + Instrument.Symbol,
    dataType: "json",
    //data: JSON.stringify({ Symbol: Instrument.Symbol, DailyPrices: NewData }),
    data: JSON.stringify({ Symbol: Instrument.Symbol, DailyPrices: NewData, GMTOffset: Instrument.GMTOffset, Currency: Instrument.Currency, CurrentPrice: Instrument.CurrentPrice, InstrumentType: Instrument.InstrumentType, TradeDayStart: Instrument.SingleDayStartDate, TradeDayEnd: Instrument.SingleDayEndDate }),
    success: function (response) {
      //console.info('SubmitIntradayData success: ' + Instrument.Symbol + ' - ' + JSON.stringify(response));
    },
    failure: function (response) { console.error("SubmitIntradayData error: " + JSON.stringify(response)); }
  });

  //Add new data to Instrument
  let currentSummary = {};
  if (!Instrument.IntradayData || Instrument.IntradayData.length<1) {
    Instrument.IntradayData = [];
    currentSummary = { minDate: new Date().getTime(), maxDate: 0 };
  } else {
    currentSummary = { minDate: Instrument.IntradayData[0].DT, maxDate: Instrument.IntradayData[Instrument.IntradayData.length-1].DT };
  }

  let dataToAddToFront = [];
  for (let i = 0; i < NewData.length; i++) {
    if (NewData[i].DT < currentSummary.minDate) {
      dataToAddToFront.push(NewData[i]);
    } else {
      if (NewData[i].DT > currentSummary.maxDate) Instrument.IntradayData.push(NewData[i]);
    }
  }
  for (let i = dataToAddToFront.length - 1; i >= 0; i--) {
    Instrument.IntradayData.unshift(dataToAddToFront[i]);
  }
}

function updateTables() {
  //Only update the tables every 5 seconds (unless lastTablesUpdate has been reset)
  if (tablesUpdateTimings.updateNeeded && tablesUpdateTimings.lastUpdate + tablesUpdateTimings.timeBetweenUpdates < new Date().getTime()) {
    tablesUpdateTimings.lastUpdate = new Date().getTime();
    
    createHoldingsTable();
    createAccountsTables();
    createAnalysisTable();
    createIndexMarquee();
    updateCurrenciesSpan();
  }
}

function createHoldingsTable() {
  function getHoldings() {
    function aggregateHoldings(i) {
      let aggUnits = 0;
      let aggBookCost = 0;
      let aggBookCostGBP = 0;
      let minPurchaseData = new Date().getTime();
      let maxPurchaseDate = 0;
      let marketIsOpen = Instruments.MarketIsOpen(i);
      let exchangeRate = Instruments.GetExchangeRate(i.Currency, 'GBP');
      let accountNames = [];
      let aggSingleDayGainGBP = 0;

      for (let hn = 0; hn < i.Holdings.length; hn++) {
        let h = i.Holdings[hn];

        if (h.SoldDate == null && h.NoUnits != 0) {
          aggUnits += h.NoUnits;
          aggBookCost += h.NoUnits * h.PurchasePricePerUnit;
          //aggBookCostGBP += h.NoUnits * h.PurchasePricePerUnit / exchangeRate;
          aggBookCostGBP += h.BookCostGBP;
          //bookCostPerAccount.addCategoryAmount(h.AccountName, (h.NoUnits * h.PurchasePricePerUnit) / exchangeRate);
          bookCostPerAccount.addCategoryAmount(h.AccountName, h.BookCostGBP);

          if (h.PurchaseDate < minPurchaseData) { minPurchaseData = h.PurchaseDate; }
          if (h.PurchaseDate > maxPurchaseDate) { maxPurchaseDate = h.PurchaseDate; }
          if (!accountNames.includes(h.AccountName)) { accountNames.push(h.AccountName); }

          if (i.CurrentPrice && exchangeRate) {
            aggSingleDayGainGBP += ((i.CurrentPrice - (h.PurchaseDate > i.SingleDayStartDate ? h.PurchasePricePerUnit : i.SingleDayPreviousClose)) * h.NoUnits) / exchangeRate;
            currentValuePerAccount.addCategoryAmount(h.AccountName, (h.NoUnits * i.CurrentPrice) / exchangeRate);
          }
        }
      }

      let aggPurchasePricePerUnit = aggBookCost / aggUnits;
      let aggCurrentValue = 0,
        aggCurrentValueGBP = 0,
        aggGainGBP = 0,
        aggSingleDayGainPercent = 0,
        aggSingleDayProfitOrLoss = 0;
      if (i.CurrentPrice) {
        aggCurrentValue = aggUnits * (i.Currency == 'GBp' ? i.CurrentPrice / 100 : i.CurrentPrice);
        if (exchangeRate) {
          aggCurrentValueGBP = (aggUnits * i.CurrentPrice) / exchangeRate;
          //aggGainGBP = ((i.CurrentPrice - aggPurchasePricePerUnit) * aggUnits) / exchangeRate;
          aggGainGBP = aggCurrentValueGBP - aggBookCostGBP;
          aggSingleDayGainPercent = ((i.CurrentPrice / i.SingleDayPreviousClose) - 1) * 100;
          aggSingleDayProfitOrLoss = aggSingleDayGainGBP < 0 ? 'loss' : 'profit';
        }
      }

      return {
        displayOrder: i.DisplayOrder,
        holdingNumber: 0,
        instrumentType: i.InstrumentType,
        instrumentName: i.InstrumentName,
        displayName: i.DisplayName,
        symbol: i.Symbol,
        currency: i.Currency,
        accountName: accountNames.length > 1 ? 'Multiple (' + accountNames.length + ')' : accountNames[0],
        accountNames: accountNames.length > 1 ? accountNames.join(', ') : '',
        purchaseDateMin: minPurchaseData,
        purchaseDateMax: maxPurchaseDate,
        noUnits: aggUnits,
        purchasePricePerUnit: aggPurchasePricePerUnit,
        bookCost: aggBookCost,
        bookCostGBP: aggBookCostGBP,
        currentValue: aggCurrentValue,
        gain: (i.Currency == 'GBp' ? (i.CurrentPrice - aggPurchasePricePerUnit) / 100 : (i.CurrentPrice - aggPurchasePricePerUnit)) * aggUnits,
        //gainPercent: (i.CurrentPrice - aggPurchasePricePerUnit) / aggPurchasePricePerUnit * 100,
        gainPercent: aggGainGBP / aggBookCostGBP * 100,
        currentValueGBP: aggCurrentValueGBP,
        gainGBP: aggGainGBP,
        singleDayGainGBP: aggSingleDayGainGBP,
        singleDayGainPercent: aggSingleDayGainPercent,
        singleDayProfitOrLoss: aggSingleDayProfitOrLoss,
        currentPrice: i.CurrentPrice,
        currentExchangeRate: exchangeRate,
        marketIsOpen: marketIsOpen
      };
    }

    //Create a structure to summarise total holdings in each currency, to be used in the pie chart
    let holdingCurrencies = {
      data: [],
      indexOfHoldingCurrency: function (symbol) { return this.data.map(function (item) { return item.symbol; }).indexOf(symbol); },
      getHoldingCurrency: function (symbol) { return this.data[this.indexOfHoldingCurrency(symbol)]; },
      addHoldingCurrency: function (symbol, amount) {
        let c = this.getHoldingCurrency(symbol);
        if (c) {
          c.total += amount;
        } else {
          this.data.push({ symbol: symbol, total: amount });
        }
      },
      getChartData: function () {
        let d = []
        for (let i = 0; i < this.data.length; i++) {
          //d.push({ label: this.data[i].symbol, data: this.data[i].total / Instruments.GetExchangeRate(this.data[i].symbol, 'GBP') });
          d.push({ label: this.data[i].symbol, data: this.data[i].total });
        }
        return d;
      }
    };
    let holdingCurrenciesCurrentValue = {
      data: [],
      indexOfHoldingCurrency: function (symbol) { return this.data.map(function (item) { return item.symbol; }).indexOf(symbol); },
      getHoldingCurrency: function (symbol) { return this.data[this.indexOfHoldingCurrency(symbol)]; },
      addHoldingCurrency: function (symbol, amount) {
        let c = this.getHoldingCurrency(symbol);
        if (c) {
          c.total += amount;
        } else {
          this.data.push({ symbol: symbol, total: amount });
        }
      },
      getChartData: function () {
        let d = []
        for (let i = 0; i < this.data.length; i++) {
          d.push({ label: this.data[i].symbol, data: this.data[i].total / Instruments.GetExchangeRate(this.data[i].symbol, 'GBP') });
        }
        return d;
      }
    };
    let bookCostPerAccount = createCategoryAggregator();
    let currentValuePerAccount = createCategoryAggregator();
    let totalHoldingsValueGBP = 0;
    let totalBookCostGBP = 0;
    let totalValueGBP = 0;
    let totalGainGBP = 0;
    let todaysGainGBP = 0;
    let maxCurrentValueGBP = 0;
    let groupHoldings = $('#groupBySymbol').prop('checked');
    let r = [];
    let w = []; //Watchlist (instruments with no current holdings)

    for (let n = 0; n < Instruments.Data.length; n++) {
      let i = Instruments.Data[n];
      //if (i.InstrumentType == 'EQUITY') {
      if (i.InstrumentType == 'EQUITY' || i.InstrumentType == 'ETF' || i.InstrumentType == 'CRYPTOCURRENCY' || i.InstrumentType == 'CURRENCY') {
        //if (i.CurrentPrice) {
        if (groupHoldings) {
          let agg = aggregateHoldings(i);
          if (agg.noUnits > 0) {
            r.push(agg);

            //holdingCurrencies.addHoldingCurrency(i.Currency, agg.noUnits * agg.purchasePricePerUnit);
            //holdingCurrencies.addHoldingCurrency(i.Currency.toUpperCase(), agg.noUnits * agg.purchasePricePerUnit / (i.Currency == "GBp" ? 100 : 1));
            holdingCurrencies.addHoldingCurrency(i.Currency.toUpperCase(), agg.bookCostGBP);
            if (i.CurrentPrice) {
              if (agg.currentValueGBP > maxCurrentValueGBP) { maxCurrentValueGBP = agg.currentValueGBP };
              //holdingCurrenciesCurrentValue.addHoldingCurrency(i.Currency, agg.noUnits * i.CurrentPrice);
              holdingCurrenciesCurrentValue.addHoldingCurrency(i.Currency.toUpperCase(), agg.noUnits * i.CurrentPrice / (i.Currency == "GBp" ? 100 : 1));
              if (agg.currentExchangeRate) {
                totalBookCostGBP += agg.bookCostGBP;
                totalValueGBP += agg.currentValueGBP;
                totalGainGBP += agg.gainGBP;
                todaysGainGBP += (agg.marketIsOpen == 0 ? 0 : agg.singleDayGainGBP);
              }
            }
          } else {
            //No current holdings for this instrument - add it to the watchlist
            if (i.WatchlistID) {
              w.push(i);
            }
          }
        } else {
          let marketIsOpen = Instruments.MarketIsOpen(i);
          let exchangeRate = Instruments.GetExchangeRate(i.Currency, 'GBP');

          let noCurrentHoldings = 0;
          for (let hn = 0; hn < i.Holdings.length; hn++) {
            let h = i.Holdings[hn];
            if (h.SoldDate == null) {
              noCurrentHoldings++;
              let holdingBookCost = h.NoUnits * h.PurchasePricePerUnit;

              let currentHoldingValue = h.NoUnits * (i.Currency == 'GBp' ? i.CurrentPrice / 100 : i.CurrentPrice);

              let holdingCurrentValueGBP = 0,
                holdingGainGBP = 0,
                singleDayGainGBP = 0,
                singleDayGainPercent = 0,
                singleDayProfitOrLoss = 0;
              if (i.CurrentPrice) {
                let currentHoldingValue = h.NoUnits * (i.Currency == 'GBp' ? i.CurrentPrice / 100 : i.CurrentPrice);

                if (exchangeRate) {
                  let holdingCurrentValueGBP = (h.NoUnits * i.CurrentPrice) / exchangeRate;
                  //let holdingGainGBP = ((i.CurrentPrice - h.PurchasePricePerUnit) * h.NoUnits) / exchangeRate;
                  let holdingGainGBP = holdingCurrentValueGBP - h.BookCostGBP;

                  let singleDayGainGBP = ((i.CurrentPrice - (h.PurchaseDate > i.SingleDayStartDate ? h.PurchasePricePerUnit : i.SingleDayPreviousClose)) * h.NoUnits) / exchangeRate;
                  let singleDayGainPercent = ((i.CurrentPrice / i.SingleDayPreviousClose) - 1) * 100;
                  let singleDayProfitOrLoss = singleDayGainGBP < 0 ? 'loss' : 'profit';

                  if (holdingCurrentValueGBP > maxCurrentValueGBP) { maxCurrentValueGBP = holdingCurrentValueGBP };
                }
              }

              r.push({
                displayOrder: i.DisplayOrder,
                holdingNumber: hn,
                instrumentType: i.InstrumentType,
                instrumentName: i.InstrumentName,
                displayName: i.DisplayName,
                symbol: i.Symbol,
                currency: i.Currency,
                accountName: h.AccountName,
                purchaseDateMin: h.PurchaseDate,
                purchaseDateMax: h.PurchaseDate,
                noUnits: h.NoUnits,
                purchasePricePerUnit: h.PurchasePricePerUnit,
                bookCost: holdingBookCost,
                currentValue: currentHoldingValue,
                gain: (i.Currency == 'GBp' ? (i.CurrentPrice - h.PurchasePricePerUnit) / 100 : (i.CurrentPrice - h.PurchasePricePerUnit)) * h.NoUnits,
                //gainPercent: (i.CurrentPrice - h.PurchasePricePerUnit) / h.PurchasePricePerUnit * 100,
                gainPercent: holdingGainGBP / h.BookCostGBP * 100,
                currentValueGBP: holdingCurrentValueGBP,
                gainGBP: holdingGainGBP,
                singleDayGainGBP: singleDayGainGBP,
                singleDayGainPercent: singleDayGainPercent,
                singleDayProfitOrLoss: singleDayProfitOrLoss,
                currentPrice: i.CurrentPrice,
                currentExchangeRate: exchangeRate,
                marketIsOpen: marketIsOpen
              });

              holdingCurrencies.addHoldingCurrency(i.Currency, h.NoUnits * h.PurchasePricePerUnit);
              if (i.CurrentPrice) {
                holdingCurrenciesCurrentValue.addHoldingCurrency(i.Currency, h.NoUnits * i.CurrentPrice);
                if (exchangeRate) {
                  currentValuePerAccount.addCategoryAmount(h.AccountName, holdingCurrentValueGBP);
                  bookCostPerAccount.addCategoryAmount(h.AccountName, holdingBookCost / exchangeRate);

                  //totalBookCostGBP += holdingBookCost / exchangeRate;
                  totalBookCostGBP += h.BookCostGBP;
                  totalValueGBP += holdingCurrentValueGBP;
                  totalGainGBP += holdingGainGBP;
                  todaysGainGBP += (marketIsOpen == 0 ? 0 : singleDayGainGBP);
                }
              }
            }
            if (noCurrentHoldings == 0) {
              //No current holdings for this instrument - add it to the watchlist
              if (i.WatchlistID) {
                w.push(i);
              }
            }
          }
        }
        //} //if (i.CurrentPrice)
      }
    }

    return {
      totalBookCostGBP: totalBookCostGBP,
      totalValueGBP: totalValueGBP,
      totalGainGBP: totalGainGBP,
      todaysGainGBP: todaysGainGBP,
      maxCurrentValueGBP: maxCurrentValueGBP,
      holdings: r,
      watchlist: w,
      holdingCurrencies: holdingCurrencies,
      holdingCurrenciesCurrentValue: holdingCurrenciesCurrentValue,
      bookCostPerAccount: bookCostPerAccount,
      currentValuePerAccount: currentValuePerAccount
    };
  }
  function createTable() {
    let t = $("#tblMainHoldings");
    if (t.length) {
      return;
    } else {
      let tbl = '<table id="tblMainHoldings" class="mainHoldings"><thead>' +
                  '<tr><th>Type</th>' +
                  '<th>Name</th>' +
                  '<th>Symbol</th>' +
                  '<th>Ccy</th>' +
                  '<th>Account</th>' +
                  '<th>Buy Date</th>' +
                  '<th>No Units</th>' +
                  '<th>Buy<br>Price</th>' +
                  '<th>Current<br>Price</th>' +
                  '<th>Book<br>Cost</th>' +
                  '<th>Current<br>Value</th>' +
                  '<th>Gain</th>' +
                  '<th>Gain £</th>' +
                  '<th>Gain %</th>' +
                  '<th>Current<br>Value £</th>' +
                  '<th>Allocation</th>' +
                  '<th>Today&apos;s<br>Gain £</th>' +
                  '<th>Today&apos;s<br>Gain %</th>' +
              '</tr></thead><tbody id="tbMainHoldings"></tbody><tfoot id="tfMainHoldings"><tr><td colspan="18">&nbsp;</td></tr></tfoot></table>';

      //$('#holdingsDiv').html(tbl);
      $('#holdingsTableDiv').html(tbl);

      $("#tblMainHoldings").tablesorter({ ignoreCase: false });
      $("#tblMainHoldings").trigger("sorton", Cookies.get('sortHoldings'));
      $('#tblMainHoldings').on('sortEnd', function (event) {
        // Prints the current sort order to the console
        Cookies.set('sortHoldings', event.target.config.sortList, { 'sameSite': 'strict' });
      });
    }
  }
  function createWatchlistTable() {
    let t = $("#tblWatchlist");
    if (t.length) {
      return;
    } else {
      let tbl = '<table id="tblWatchlist" class="watchlist"><thead>' +
                  '<tr><th>Type</th>' +
                  '<th>Name</th>' +
                  '<th>Symbol</th>' +
                  '<th>Ccy</th>' +
                  '<th>Current Price</th>' +
                  '<th>Today&apos;s Gain</th>' +
                  '<th>Today&apos;s Gain %</th>' +
                  '<th>Since Last Sold<th>' +
              '</tr></thead><tbody id="tbWatchlist"></tbody></table>';

      $('#watchlistTableDiv').html(tbl);

      $("#tblWatchlist").tablesorter({ ignoreCase: false });
      $("#tblWatchlist").trigger("sorton", Cookies.get('sortWatchlist'));
      $('#tblWatchlist').on('sortEnd', function (event) {
        // Prints the current sort order to the console
        Cookies.set('sorWatchlist', event.target.config.sortList, { 'sameSite': 'strict' });
      });
    }
  }
  function addTableRow(rowID, content, tbodyID, highlightUpdates) {
    let r = $("#" + rowID);
    let newText = $(content).text(); //$(content).text();
    if (r.length) {
      let currentContent = r.text();
      if (currentContent != newText) { //Only update row if the content has changed
        $("#" + rowID).replaceWith(content);
        if (highlightUpdates) {
          $("#" + rowID).addClass("highlighted");
        }
      }
    } else {
      $("#" + tbodyID).append(content); //.addClass("highlighted");
      //$("#" + rowID).addClass("highlighted");
    }
  }
  function getPercentCell(backgroundColor, cellWidth, valuePercent, label) {
    let labelText = label ? label : (valuePercent.toFixed(1) + '%');
    let barWidth = ((valuePercent / 100) * cellWidth);
    return '<td>' +
              '<div class="pcBackground" style="background-color: ' + backgroundColor + '; width:' + barWidth.toFixed(0) + 'px;">&nbsp;</div>' +
              '<div class="pcLabel" style="width:' + cellWidth + 'px">' + labelText + '</div>' +
           '</td>';
  }
  function profitOrLoss(value) {
    return value >= 0 ? "profit" : "loss";
  }

  let t = getHoldings();
  let totalBookCostGBP = t.totalBookCostGBP;
  let totalValueGBP = t.totalValueGBP;
  let totalGainGBP = t.totalGainGBP;
  let todaysGainGBP = t.todaysGainGBP;
  let maxCurrentValueGBP = t.maxCurrentValueGBP;
  let holdings = t.holdings;
  let watchlist = t.watchlist;
  let holdingCurrencies = t.holdingCurrencies;
  let holdingCurrenciesCurrentValue = t.holdingCurrenciesCurrentValue;
  let bookCostPerAccount = t.bookCostPerAccount;
  let currentValuePerAccount = t.currentValuePerAccount;

  let maxAllocationPercent = (maxCurrentValueGBP / totalValueGBP) * 100;
  let allocationScaleFactor = 100 / maxAllocationPercent;

  let altRow = 1;

  createTable();
  for (let n = 0; n < holdings.length; n++) {
    try {
      altRow = !altRow;

      let h = holdings[n];
      let type = h.instrumentType == 'CURRENCY' ? '$' : h.instrumentType.substring(0, 1);
      //let profitOrLoss = h.currentPrice < h.purchasePricePerUnit ? 'loss' : 'profit'; //let profitOrLoss = i.CurrentPrice < h.PurchasePricePerUnit ? 'loss' : 'profit';
      let openOrClosed = h.marketIsOpen == 1 ? 'open' : 'closed';
      let allocationPercent = h.currentValueGBP / totalValueGBP * 100;
      let scaledAllocation = allocationScaleFactor * allocationPercent;

      let rowID = "holdingRow_" + h.displayOrder + "_" + h.holdingNumber;
      let row = '<tr' + (altRow ? ' class="altShareRow"' : '') + ' id="' + rowID + '">' +
                  '<td class="' + openOrClosed + '">' + type + '</td>' +
                  '<td class="' + openOrClosed + '">' + (h.displayName != '' ? h.displayName : h.instrumentName) + '</td>' +
                  '<td><a target="_blank" href="https://uk.finance.yahoo.com/quote/' + h.symbol + '">' + h.symbol + '</a>' + '</td>' +
                  '<td class="' + openOrClosed + '">' + h.currency + '</td>' +
                  '<td class="' + openOrClosed + '" title="' + (h.accountNames ? h.accountNames : '') + '">' + h.accountName + '</td>' +
                  '<td class="' + openOrClosed + '"' + (h.purchaseDateMin == h.purchaseDateMax ? ('>' + new Date(h.purchaseDateMin).yyyymmdd()) : (' title="' + new Date(h.purchaseDateMax).yyyymmdd() + ' - ' + new Date(h.purchaseDateMin).yyyymmdd() + '">' + new Date(h.purchaseDateMin).yyyymmdd()) + '+') + '</td>' +
                  //'<td class="num ' + openOrClosed + '">' + formatAmount(h.noUnits, '', 0) + '</td>' +
                  '<td class="num ' + openOrClosed + '">' + h.noUnits.autoScale() + '</td>' +
                  '<td class="num ' + openOrClosed + '">' + formatAmount(h.purchasePricePerUnit, h.currency, 2) + '</td>' + //Buy Price
                  '<td class="num ' + openOrClosed + '">' + (h.currentPrice ? formatAmount(h.currentPrice, h.currency, 2) : '') + '</td>' + //Current Price
                  '<td class="num ' + openOrClosed + '">' + formatAmount(h.currency == 'GBp' ? h.bookCost / 100 : h.bookCost, h.currency == 'GBp' ? 'GBP' : h.currency, 0) + '</td>' + //Book Cost
                  '<td class="num ' + openOrClosed + '">' + (h.currentExchangeRate ? formatAmount(h.currentValue, h.currency == 'GBp' ? 'GBP' : h.currency, 0) : '') + '</td>' + //Current Value
                  '<td class="num ' + profitOrLoss(h.currentPrice - h.purchasePricePerUnit) + '">' + (h.currentPrice ? formatAmount(h.gain, h.currency == 'GBp' ? 'GBP' : h.currency, 0) : '') + '</td>' + //Gain
                  '<td class="num ' + profitOrLoss(h.gainGBP) + '">' + (h.currentExchangeRate ? formatAmount(h.gainGBP, 'GBP', 0) : '') + '</td>' + //Gain £
                  '<td class="num ' + profitOrLoss(h.gainGBP) + '">' + (h.currentExchangeRate ? h.gainPercent.toFixed(1) + '%' : '') + '</td>' + //Gain %
                  '<td class="num ' + openOrClosed + '">' + (h.currentExchangeRate ? formatAmount(h.currentValueGBP, 'GBP', 0) : '') + '</td>' + //Current Value £
                  (h.currentExchangeRate ? getPercentCell('#3f2d95', 70, scaledAllocation, allocationPercent.toFixed(1) + '%') : '') + //Allocation
                  (h.currentExchangeRate ? (h.marketIsOpen == 0 ? '<td/>' : '<td class="num ' + h.singleDayProfitOrLoss + '">' + formatAmount(h.singleDayGainGBP, 'GBP', 0) + '</td>') : '') + //Today's Gain £
                  (h.singleDayGainPercent ? (h.marketIsOpen == 0 ? '<td/>' : '<td class="num ' + h.singleDayProfitOrLoss + '">' + h.singleDayGainPercent.toFixed(1) + '%</td>') : '<td/>') + //Today's Gain %
               '</tr>';
      addTableRow(rowID, row, "tbMainHoldings", true);
    }
    catch (err) {
      logError("Error adding " + holdings[n].symbol + " to main holdings table: " + err.message);
    }
  }

  //Add the totals line at the bottom of the table
  //let profitOrLoss = totalValueGBP < totalBookCostGBP ? 'loss' : 'profit';
  let todaysProfitOrLoss = todaysGainGBP < 0 ? 'loss' : 'profit';
  let lSF = '';
  if (lastSuccessfulFetch) {
    let lastFetchAge = ((new Date()) - lastSuccessfulFetch);
    let staleFetch = ((new Date()) - lastSuccessfulFetch) > (timespans.oneMinute * 3);
    lSF = (lastSuccessfulFetch ? ' - Last Successful Fetch: ' + (staleFetch ? '<span class="loss">' : '') + lastSuccessfulFetch.yyyymmddhhmmss() + (staleFetch ? '</span>' : '') : '');
  }
  let row = '<tr id="tfHoldingsTotals"><td></td><td id="holdingsLastUpdated" colspan="8">Updated: ' + new Date().yyyymmddhhmmss() + lSF + '</td>' +
             '<td class="num">' + formatAmount(totalBookCostGBP, 'GBP', 0) + '</td>' + //Book Cost
             '<td></td><td></td>' +
             '<td class="num ' + profitOrLoss(totalValueGBP - totalBookCostGBP) + '">' + formatAmount(totalGainGBP, 'GBP', 0) + '</td>' + //Gain GBP
             '<td class="num ' + profitOrLoss(totalValueGBP - totalBookCostGBP) + '">' + formatAmount((((totalValueGBP / totalBookCostGBP) - 1) * 100), '', 1) + '%</td>' + //Gain %
             '<td class="num">' + formatAmount(totalValueGBP, 'GBP', 0) + '</td>' + //Current Value
             '<td>&nbsp;</td>' + //Allocation
             '<td class="num ' + todaysProfitOrLoss + '">' + formatAmount(todaysGainGBP, 'GBP', 0) + '</td>' + //Today's Gain GBP
             //'<td class="num ' + todaysProfitOrLoss + '">' + formatAmount(((todaysGainGBP / totalBookCostGBP) * 100), '', 1) + '%</td>' + //Today's Gain %
             '<td class="num ' + todaysProfitOrLoss + '">' + formatAmount(((todaysGainGBP / (totalValueGBP-todaysGainGBP)) * 100), '', 1) + '%</td>' + //Today's Gain %
          '</tr>';
  addTableRow("tfHoldingsTotals", row, "tfMainHoldings", false);

  //Create the watchlist table
  createWatchlistTable();
  altRow = 1;
  for (let n = 0; n < watchlist.length; n++) {
    altRow = !altRow;
    let wi = watchlist[n];
    try {
      let type = wi.InstrumentType == 'CURRENCY' ? '$' : wi.InstrumentType.substring(0, 1);
      let openOrClosed = Instruments.MarketIsOpen(wi) == 1 ? 'open' : 'closed';
      let profitOrLoss = wi.CurrentPrice < wi.SingleDayPreviousClose ? 'loss' : 'profit';
      let rowID = "watchlistRow_" + wi.DisplayOrder;

      let sinceLastSold = '<td></td>';
      if (wi.LastSoldPrice) {
        let delta = (wi.CurrentPrice - wi.LastSoldPrice) / wi.LastSoldPrice * 100;
        let tooltip = new Date(wi.LastSoldDate).yyyymmdd() + ' @ ' + wi.LastSoldPrice.toFixed(2);
        sinceLastSold = '<td title="' + tooltip + '" class="num ' + (delta < 0 ? 'loss' : 'profit') + '">' + (delta).toFixed(1) + '%</td>';
      }

      //Type, Name, Symbol, Ccy, Current Price, Today's Gain, Today's Gain %
      let row = '<tr' + (altRow ? ' class="altShareRow"' : '') + ' id="' + rowID + '">' +
                  '<td class="' + openOrClosed + '">' + type + '</td>' +
                  '<td class="' + openOrClosed + '">' + wi.InstrumentName + '</td>' +
                  '<td><a target="_blank" href="https://uk.finance.yahoo.com/quote/' + wi.Symbol + '">' + wi.Symbol + '</a>' + '</td>' +
                  '<td class="' + openOrClosed + '">' + wi.Currency + '</td>' +
                  '<td class="num ' + openOrClosed + '">' + (wi.CurrentPrice ? formatAmount(wi.CurrentPrice, wi.Currency, 2) : '') + '</td>' + //Current Price
                  (Instruments.marketIsOpen == 0 ? '<td/>' : '<td class="num ' + profitOrLoss + '">' + (wi.CurrentPrice ? formatAmount(wi.CurrentPrice - wi.SingleDayPreviousClose, wi.Currency, 2) : '') + '</td>') + //Today's Gain
                  (Instruments.marketIsOpen == 0 ? '<td/>' : '<td class="num ' + profitOrLoss + '">' + (wi.CurrentPrice ? ((wi.CurrentPrice - wi.SingleDayPreviousClose) / wi.SingleDayPreviousClose * 100).toFixed(1) : '') + '</td>') + //Today's Gain %
                  //'<td>' + (new Date(wi.LastSoldDate)).yyyymmdd() + '</td>' +
                  sinceLastSold +
                '</tr>';
      addTableRow(rowID, row, "tbWatchlist", true);
    }
    catch (err) {
      logError("Error adding " + wi.symbol + " to watchlist table: " + err.message);
    }
  }

  //Flash the last updated cell
  $("#holdingsLastUpdated").addClass("highlighted");
  //Remove the highlight from all rows/cells
  $("#tblMainHoldings").find(".highlighted").removeClass("highlighted", 1200);
  $("#tblWatchlist").find(".highlighted").removeClass("highlighted", 1200);

  //Tell tablesorter that the table has been updated
  var resort = true;
  $("#tblMainHoldings").trigger("update", [resort]);

  //Update the daily holdings value table in the DB
  if (totalValueGBP != lastTotalHoldingsValue) {
    $.ajax({
      type: "POST",
      contentType: "application/json",
      url: "SharePrices.aspx/SetTotalHoldingsValue",
      dataType: "json",
      data: JSON.stringify({ TotalValue: totalValueGBP }),
      success: function (response) {
        previousDay = response.PreviousDay;
        previousClose = response.PreviousClose;
        previousWeek = response.PreviousWeekDate;
        previousWeekClose = response.PreviousWeekClose;

        //Update the total holdings span in the site banner
        let formattedAmount = formatAmount(totalValueGBP, "GBP", 0);
        let holdingsHTML = '<span style="font-size: small;">Total holdings:&nbsp;</span>' + '<span style="font-size: large">' + formattedAmount + '</span>&nbsp;';
        holdingsHTML += '<span style="font-size: small;"><span class="' + (previousClose < totalValueGBP ? 'profit">+' : 'loss">') + formatAmount(totalValueGBP - previousClose, "GBP", 0) + '</span>&nbsp;(d)';
        holdingsHTML += '&nbsp;/&nbsp;<span class="' + (previousWeekClose < totalValueGBP ? 'profit">+' : 'loss">') + formatAmount(totalValueGBP - previousWeekClose, "GBP", 0) + '</span>&nbsp;(w)</span>';
        $("#spnTotalHoldings").html(holdingsHTML);
        //Set the page title
        document.title = formattedAmount + ' - ' + initialPageTitle;

        lastTotalHoldingsValue = totalValueGBP;
      },
      failure: function (response) { console.error("SetTotalHoldingsValue error: " + JSON.stringify(response)); }
    });
  }

  //Create / update the currency allocation pie chart
  function labelFormatter(label, series) {
    return "<div class='pieLabel'>" + label + "<br/>" + Math.round(series.percent) + "%<br>" + formatAmount(series.data[0][1], 'GBP', 0) + "</div>";
  }
  let ops = {
    series: {
      downsample: { threshold: 0 },
      pie: { show: true, radius: 1, label: { show: true, radius: 0.7, formatter: labelFormatter } }
    },
    legend: { show: false },
    grid: { hoverable: true },
    tooltip: {
      show: true,
      content: "%p.0%, %s, n=%n", // show percentages, rounding to 2 decimal places
      shifts: { x: 20, y: 0 },
      defaultTheme: false
    }
  };
  let cd = holdingCurrencies.getChartData();
  if (cd.length) {
    $.plot('#divHoldingCurrenciesChart', cd, ops);
  }

  cd = holdingCurrenciesCurrentValue.getChartData();
  if (cd.length) {
    $.plot('#divHoldingCurrenciesChartCurrentValue', cd, ops);
  }

  function labelFormatter2(label, series) {
    return "<div class='pieLabel'>" + label + "<br/>" + formatAmount(series.data[0][1], 'GBP', 0) + "</div>";
  }
  ops.series.pie.label.formatter = labelFormatter2;
  cd = bookCostPerAccount.getChartData();
  if (cd.length) {
    $.plot('#divAccountBookCost', cd, ops);
  }

  cd = currentValuePerAccount.getChartData();
  if (cd.length) {
    $.plot('#divAccountValue', cd, ops);
  }
}
function createAccountsTables() {
  function getAccounts() {
    let accounts = [];
    function indexOfAccount(accountName) { return accounts.map(function (item) { return item.accountName; }).indexOf(accountName); };
    function getAccount(accountName) { return accounts[indexOfAccount(accountName)]; };
    function addAccountHolding(instrument, exchangeRate, holding) {
      let a = getAccount(holding.AccountName);
      if (!a) {
        a = { accountName: holding.AccountName, holdings: [], totalCostGBP: 0, totalValueGBP: 0, totalGainGBP: 0, totalCashGBP: 0 };
        accounts.push(a);
      }
      let h = {
        instrumentName: instrument.InstrumentName,
        symbol: instrument.Symbol,
        instrumentType: instrument.InstrumentType,
        currency: instrument.Currency,
        purchaseDate: holding.PurchaseDate,
        purchasePricePerUnit: holding.PurchasePricePerUnit,
        noUnits: holding.NoUnits,
        cost: holding.NoUnits * holding.PurchasePricePerUnit
      }
      //if (exchangeRate) {
      //h.costGBP = h.cost / exchangeRate;
      h.costGBP = holding.BookCostGBP;
      if (instrument.InstrumentType != 'CURRENCY') {
        a.totalCostGBP += holding.BookCostGBP;
      }
      //}
      if (instrument.CurrentPrice) {
        h.currentPrice = instrument.CurrentPrice;
        h.currentValue = holding.NoUnits * instrument.CurrentPrice;
        h.gain = h.currentValue - h.cost;
        if (exchangeRate) {
          h.currentValueGBP = h.currentValue / exchangeRate;
          //h.gainGBP = h.gain / exchangeRate;
          h.gainGBP = h.currentValueGBP - h.costGBP;
          if (instrument.InstrumentType != 'CURRENCY') {
            a.totalGainGBP += h.gainGBP;
            a.totalValueGBP += h.currentValueGBP;
          }
        }
      }
      if (instrument.InstrumentType == 'CURRENCY') {
        //Don't add cash holdings - just add the cash value to the account
        a.totalCashGBP += h.currentValueGBP;
      } else {
        a.holdings.push(h);
      }
    }
    
    for (let ix = 0; ix < Instruments.Data.length; ix++) {
      let i = Instruments.Data[ix];
      let exchangegRate = Instruments.GetExchangeRate(i.Currency, 'GBP');
      for (let hx = 0; hx < i.Holdings.length; hx++) {
        let h = i.Holdings[hx];
        if (h.SoldDate == null) {
          addAccountHolding(i, exchangegRate, h);
        }
      }
    }
  
    return accounts;
  }

  let accounts = getAccounts();

  //Sort the accounts array by accountName
  accounts.sort(function (a, b) {
    if (a.accountName < b.accountName) return -1;
    if (a.accountName > b.accountName) return 1;
    return 0;
  });

  //let summaryTable = "<table class='accounts' id='accountsSummaryTable'><thead><tr><th>Account Name</th><th>No. Holdings</th><th>Total Cost (GBP)</th><th>Current Value</th><th>Total Gain</th></tr></thead><tbody>";
  let summaryTable = "<table class='accounts' id='accountsSummaryTable'><thead><tr>" +
                        "<th>Account Name</th>" +
                        "<th>No. Holdings</th>" +
                        "<th>Total Cost (GBP)</th>" +
                        "<th>Total Gains (GBP)</th>" +
                        "<th>Total Losses (GBP)</th>" +
                        "<th>Current Investements Value</th>" +
                        "<th>NET Gain</th>" +
                        "<th>Cash Balance GBP</th>" +
                        "<th>Total Value GBP</th>" +
                      "</tr></thead><tbody>";
  let accountsTables = ''
  let tableIDs = ['accountsSummaryTable'];
  let hideEmptyAccounts = $('#hideEmptyAccounts').prop('checked');
  let altRow = 1;
  for (let ax = 0; ax < accounts.length; ax++) {
    let a = accounts[ax];
    let accountGainsGBP = 0;
    let accountLossesGBP = 0;
    let accountValueGBP = 0;
    let profitOrLoss = a.totalGainGBP < 0 ? 'loss' : 'profit';
    if (a.holdings.length > 0) {
      altRow = !altRow;
      
      accountsTables += '<br>' + a.accountName + '<br>'
      let tableID = 'accountTable' + ax;
      tableIDs.push(tableID);
      accountsTables += '<table class="accounts" id="' + tableID + '">' +
                          '<thead><tr>' +
                                    '<th>Name</th>' +
                                    '<th>Symbol</th>' +
                                    '<th>Buy Date</th>' +
                                    '<th>No Units</th>' +
                                    '<th>Buy Price</th>' +
                                    '<th>Current Price</th>' +
                                    '<th>Book Cost</th>' +
                                    '<th>Approx Book Cost £</th>' +
                                    '<th>Current Value</th>' +
                                    '<th>Current Value £</th>' +
                                    '<th>Gain</th>' +
                                    '<th>Gain £</th>' +
                                    '<th>Gain %</th>' +
                                  '</tr></thead><tbody>';
      let accountAltRow = 1;
      for (let hx = 0; hx < a.holdings.length; hx++) {
        let h = a.holdings[hx];
        accountAltRow = !accountAltRow;
        let holdingProfitOrLoss = h.gain < 0 ? 'loss' : 'profit';
        let holdingGBPProfitOrLoss = h.gainGBP < 0 ? 'loss' : 'profit';
        if (h.gainGBP > 0) {
          accountGainsGBP += h.gainGBP;
        } else {
          accountLossesGBP += h.gainGBP;
        }
        accountValueGBP += h.currentValueGBP;
        accountsTables += '<tr' + (accountAltRow ? ' class="altShareRow"' : '') + '>' +
                              '<td>' + h.instrumentName + '</td>' +
                              '<td>' + h.symbol + '</td>' +
                              '<td>' + new Date(h.purchaseDate).yyyymmdd() + '</td>' +
                              '<td class="num">' + h.noUnits + '</td>' +
                              '<td class="num">' + formatAmount(h.purchasePricePerUnit, h.currency, 2) + '</td>' +
                              '<td class="num">' + formatAmount(h.currentPrice, h.currency, 2) + '</td>' +
                              '<td class="num">' + (h.currency == 'GBp' ? formatAmount(h.cost / 100, 'GBP', 2) : formatAmount(h.cost, h.currency, 2)) + '</td>' +
                              '<td class="num">' + formatAmount(h.costGBP, 'GBP', 2) + '</td>' +
                              '<td class="num">' + (h.currency == 'GBp' ? formatAmount(h.currentValue / 100, 'GBP', 2) : formatAmount(h.currentValue, h.currency, 2)) + '</td>' +
                              '<td class="num">' + formatAmount(h.currentValueGBP, 'GBP', 2) + '</td>' +
                              '<td class="num ' + holdingProfitOrLoss + '">' + formatAmount(h.gain, h.currency, 2) + '</td>' +
                              '<td class="num ' + holdingGBPProfitOrLoss + '">' + formatAmount(h.gainGBP, 'GBP', 2) + '</td>' +
                              //'<td class="num ' + holdingProfitOrLoss + '">' + ((h.gain / h.cost) * 100).toFixed(1) + '%</td>' +
                              '<td class="num ' + holdingGBPProfitOrLoss + '">' + ((h.gainGBP / h.costGBP) * 100).toFixed(1) + '%</td>' +
                          '</tr>';
      }
      //Add the Total Gains
      accountAltRow = !accountAltRow;
      accountsTables += '<tr' + (accountAltRow ? ' class="altShareRow"' : '') + '>' +
                              '<td class="num" colspan="11">Total Gains</td>' +
                              '<td class="num profit">' + formatAmount(accountGainsGBP, 'GBP', 2) + '</td>' +
                              '<td class="num">&nbsp;</td>' +
                          '</tr>';
      //Add the Total Losses
      accountAltRow = !accountAltRow;
      accountsTables += '<tr' + (accountAltRow ? ' class="altShareRow"' : '') + '>' +
                              '<td class="num" colspan="11">Total Losses</td>' +
                              '<td class="num loss">' + formatAmount(accountLossesGBP, 'GBP', 2) + '</td>' +
                              '<td class="num">&nbsp;</td>' +
                          '</tr>';
      //Add the NET Gains summary line
      accountAltRow = !accountAltRow;
      accountsTables += '<tr' + (accountAltRow ? ' class="altShareRow"' : '') + '>' +
                              '<td class="num" colspan="9">Account Value:</td>' +
                              '<td class="num ' + ((accountGainsGBP+accountLossesGBP) > 0 ? 'profit' : 'loss') + '">' + formatAmount(accountValueGBP, 'GBP', 2) + '</td>' +
                              '<td class="num">NET Gain</td>' +
                              '<td class="num ' + ((accountGainsGBP + accountLossesGBP) > 0 ? 'profit' : 'loss') + '">' + formatAmount(accountGainsGBP + accountLossesGBP, 'GBP', 2) + '</td>' +
                              '<td class="num">&nbsp;</td>' +
                          '</tr>';
      accountsTables += '</tbody></table>';
    }

    //Add an entry to the summary table
    if (a.totalValueGBP + a.totalCashGBP > 0 || !hideEmptyAccounts) {
      summaryTable += "<tr" + (altRow ? ' class="altShareRow"' : '') + "><td>" + a.accountName + "</td>" +
                          "<td>" + a.holdings.length + "</td>" +
                          "<td class='num'>" + formatAmount(a.totalCostGBP || 0, 'GBP', 2) + "</td>" +
                          "<td class='num'>" + formatAmount(accountGainsGBP, 'GBP', 2) + "</td>" +
                          "<td class='num'>" + formatAmount(accountLossesGBP, 'GBP', 2) + "</td>" +
                          "<td class='num'>" + formatAmount(a.totalValueGBP || 0, 'GBP', 2) + "</td>" +
                          "<td class='num " + profitOrLoss + "'>" + formatAmount(a.totalGainGBP || 0, 'GBP', 2) + "</td>" +
                          "<td class='num'>" + formatAmount(a.totalCashGBP || 0, 'GBP', 2) + "</td>" +
                          "<td class='num'>" + formatAmount(a.totalValueGBP + a.totalCashGBP || 0, 'GBP', 2) + "</td>" +
                      "</tr>";
    }
  }
  summaryTable += "</tbody></table>";
  $("#accountsSummaryDiv").html(summaryTable + '<br>' + accountsTables);
}
function createAnalysisTable() {
  function getHighestPriceBeforeDate(Instrument, maxDT) {
    if (Instrument.DailyData) {
      let r = { Price: 0, DT: 0 };
      let dt = 0;
      for (let i = 0; i < Instrument.DailyData.length && dt < maxDT; i++) {
        let price = Instrument.DailyData[i];
        dt = price.DT;
        if (dt <= maxDT && price.high > r.Price) {
          r.Price = price.high;
          r.DT = dt;
        }
      }
      return r;
    } else {
      return { Price: NaN, DT: NaN };
    }
  }
  function getHighestPriceBetweenDates(Instrument, minDT, maxDT) {
    if (Instrument.DailyData) {
      let r = { Price: 0, DT: 0 };
      let dt = 0;
      for (let i = 0; i < Instrument.DailyData.length && dt < maxDT; i++) {
        let price = Instrument.DailyData[i];
        dt = price.DT;
        if (dt >= minDT && dt <= maxDT && price.high > r.Price) {
          r.Price = price.high;
          r.DT = dt;
        }
      }
      return r;
    } else {
      return { Price: NaN, DT: NaN };
    }
  }
  function getLowestPriceAfterDate(Instrument, minDT) {
    if (Instrument.DailyData) {
      let r = { Price: 0, DT: 0 };
      let dt = 0;
      let i = 0;
      let DD = Instrument.DailyData;
      while (dt < minDT && i < DD.length - 1) {
        i++;
        dt = DD[i].DT;
      }
      if (i < DD.length) {
        r.Price = DD[i].low;
        r.DT = DD[i].DT;

        while (i < DD.length - 1) {
          i++;
          if (DD[i].low < r.Price) {
            r.Price = DD[i].low;
            r.DT = DD[i].DT;
          }
        }
      }
      return r;
    } else {
      return { Price: NaN, DT: NaN };
    }
  }
  function getEquities() {
    function getHoldingsValueGBP(Instrument) {
      let noUnits = 0;
      if (Instrument.CurrentPrice) {
        let c = Instruments.GetCurrentHoldings(Instrument);
        for (let x = 0; x < c.length; x++) {
          noUnits += c[x].NoUnits;
        }
        let exchangeRate = Instruments.GetExchangeRate(Instrument.Currency, 'GBP');
        return noUnits * Instrument.CurrentPrice / exchangeRate;
      } else {
        return 0;
      }
    }

    let totalHoldingsValueGBP = 0;
    let r = [];
    for (let n = 0; n < Instruments.Data.length; n++) {
      let i = Instruments.Data[n];
      try {
        if (i.InstrumentType == 'EQUITY') {
          let prePandemicHigh = getHighestPriceBetweenDates(i, new Date('2020-01-01'), pandemicCrashStartDate);
          let postPandemicLow = getLowestPriceAfterDate(i, pandemicCrashStartDate);
          let dilutedTarget = prePandemicHigh.Price * i.PostPandemicDilution;
          let currentHoldingsValueGBP = getHoldingsValueGBP(i);
          let remainingDilutedPotential = ((dilutedTarget - i.CurrentPrice) / i.CurrentPrice) * 100;
          totalHoldingsValueGBP += currentHoldingsValueGBP;

          r.push({
            prePandemicHigh: prePandemicHigh,
            postPandemicLow: postPandemicLow,
            displayOrder: i.DisplayOrder,
            instrumentName: i.InstrumentName,
            symbol: i.Symbol,
            currency: i.Currency,
            currentPrice: i.CurrentPrice,
            fallPercent: ((postPandemicLow.Price / prePandemicHigh.Price) - 1) * 100,
            postPandemicDilution: i.PostPandemicDilution * 100,
            dilutedTarget: dilutedTarget,
            currentDiscount: ((i.CurrentPrice / prePandemicHigh.Price) - 1) * 100,
            potentialDilutedRebound: (((prePandemicHigh.Price * i.PostPandemicDilution) / postPandemicLow.Price) - 1) * 100,
            retracementPercent: ((i.CurrentPrice - postPandemicLow.Price) / (prePandemicHigh.Price - postPandemicLow.Price)) * 100,
            remainingPotential: ((prePandemicHigh.Price - i.CurrentPrice) / i.CurrentPrice) * 100,
            remainingDilutedPotential: remainingDilutedPotential,
            currentHoldingsValueGBP: currentHoldingsValueGBP,
            possibleRemainingProfit: currentHoldingsValueGBP * remainingDilutedPotential / 100
          });
        }
      }
      catch (err) {
        console.error("Error analysing " + i.Symbol + ": " + err.message);
      }
    }
    return { totalHoldingsValueGBP: totalHoldingsValueGBP, equities: r };
  }
  function createTable() {
    let t = $("#tblAnalysis");
    if (t.length) {
      return t;
    } else {
      let tbl = '<table id="tblAnalysis" class="analysis"><thead><tr>' +
                  '<th>#</th>' +
                  '<th>Name</th>' +
                  /*'<th>Symbol</th>' +*/
                  /*'<th>Currency</th>' +*/
                  '<th>Pre-pandemic peak</th>' +
                  '<th>Bottom</th>' +
                  '<th>% Fall</th>' +
                  '<th>Current Price</th>' +
                  '<th>Diluted Value %</th>' +
                  '<th>Diluted Target</th>' +
                  '<th>Current Discount</th>' +
                  '<th>Potential<br>Diluted<br>Rebound</th>' +
                  '<th>Retracement %</th>' +
                  '<th>Remaining Potential</th>' +
                  '<th>Remaining<br>Diluted<br>Potential</th>' +
                  '<th>Current Holding</th>' +
                  '<th>Current Allocation %</th>' +
                  '<th>Possible Remaining Profit</th>' +
              '</tr></thead><tbody id="tbAnal"><tfoot><tr><td colspan="16">&nbsp;</td></tr><tr><td colspan="15">&nbsp;</td><td id="tdPossibleRemainingProfit" class="num"></td></tr></tfoot></tbody></table>';

      $('#analDiv').html(tbl);
      $("#tblAnalysis").tablesorter();
    }
  }
  function addTableRow(id, row) {
    let r = $("#" + id);
    if (r.length) {
      $("#" + id).replaceWith(row); //Update existing row
    } else {
      $("#tbAnal").append(row);
    }
  }

  //analysisTableTimings.LastUpdateTime = new Date().getTime();
  
  let pandemicCrashStartDate = new Date("2020-02-23").getTime();

  //Calculate values for all equities
  let tmp = getEquities();
  let equities = tmp.equities;
  let totalHoldingsValueGBP = tmp.totalHoldingsValueGBP;
  
  createTable();
  
  let altRow = 1;
  let totalPossibleRemainingProfit = 0;
  for (let n = 0; n < equities.length; n++) {
    altRow = !altRow;
    let e = equities[n];
    totalPossibleRemainingProfit += e.possibleRemainingProfit > 0 ? e.possibleRemainingProfit : 0;
    try {
      let rowID = "analRow" + e.displayOrder
      let row = '<tr' + (altRow ? ' class="altShareRow"' : '') + ' id="' + rowID + '">' +
                  '<td>' + e.displayOrder + '</td>' +
                  '<td>' + e.instrumentName + '</td>' +
                  /*'<td><a target="_blank" href="https://uk.finance.yahoo.com/quote/' + e.symbol + '">' + e.symbol + '</a>' + '</td>' +*/
                  /*'<td>' + e.currency + '</td>' +*/
                  '<td class="num" title="' + new Date(e.prePandemicHigh.DT).yyyymmddhhmmss() + '">' + formatAmount(e.prePandemicHigh.Price, e.currency, 2) + '</td>' + //Pre-pandemic peak
                  '<td class="num" title="' + new Date(e.postPandemicLow.DT).yyyymmddhhmmss() + '">' + formatAmount(e.postPandemicLow.Price, e.currency, 2) + '</td>' + //Bottom
                  '<td class="num">' + e.fallPercent.toFixed(2) + '%</td>' + //% Fall
                  '<td class="num">' + formatAmount(e.currentPrice, e.currency, 2) + '</td>' + //Current Price
                  '<td class="num">' + e.postPandemicDilution + '%</td>' + //Diluted Value
                  '<td class="num">' + formatAmount(e.dilutedTarget, e.currency, 2) + '</td>' + //Diluted target
                  '<td class="num">' + e.currentDiscount.toFixed(2) + '%</td>' + //Current Discount
                  '<td class="num">' + e.potentialDilutedRebound.toFixed(0) + '%</td>' + //Potential Diluted Rebound
                  '<td class="num">' + e.retracementPercent.toFixed(1) + '%</td>' + //Retracement %
                  '<td class="num">' + e.remainingPotential.toFixed(1) + '%</td>' + //Remaining Potential
                  '<td class="num">' + e.remainingDilutedPotential.toFixed(1) + '%</td>' + //Remaining Diluted Potential
                  '<td class="num">' + (e.currentHoldingsValueGBP > 0 ? formatAmount(e.currentHoldingsValueGBP, 'GBP', 2) : '') + '</td>' + //Current Holding
                  '<td class="num">' + (e.currentHoldingsValueGBP > 0 ? ((e.currentHoldingsValueGBP / totalHoldingsValueGBP) * 100).toFixed(2) + '%' : '') + '</td>' + //Current Allocation %
                  '<td class="num">' + (e.possibleRemainingProfit > 0 ? formatAmount(e.possibleRemainingProfit, 'GBP', 2) : '') + '</td>' + //Possible Remaining Profit
                '</tr>';
      addTableRow(rowID, row);
    }
    catch (err) {
      console.error("Error adding " + e.symbol + " to analysis table: " + err.message);
    }
  }
  $("#tdPossibleRemainingProfit").html(formatAmount(totalPossibleRemainingProfit, 'GBP', 2));

  var resort = true;
  $("#tblAnalysis").trigger("update", [resort]);
}

function createIndexMarquee() {
  let t = [];
  for (var ix = 0; ix < Instruments.Data.length; ix++) {
    let i = Instruments.Data[ix];
    //if (i.InstrumentType == 'INDEX' || i.InstrumentType == 'FUTURE') {
    if (i.ShowInMarquee == 'A') {
      let openOrClosed = Instruments.MarketIsOpen(i) == 1 ? 'open' : 'closed';
      if (i.SingleDayPreviousClose) {
        let percentChange = ((i.CurrentPrice / i.SingleDayPreviousClose) - 1) * 100;
        if (percentChange < 0) {
          t.push('<font class="' + openOrClosed + '">' + i.DisplayName + '<br/>' + i.CurrentPrice + ' </font><font class="loss">' + percentChange.toFixed(1) + '%</font>');
        } else {
          t.push('<font class="' + openOrClosed + '">' + i.DisplayName + '<br/>' + i.CurrentPrice + ' </font><font class="profit">+' + percentChange.toFixed(1) + '%</font>');
        }
      } else {
        t.push('<font class="' + openOrClosed + '">' + i.DisplayName + '<br/>' + i.CurrentPrice + '</font>');
      }
    }
  }
  if (t.length > 0) {
    //t.push(new Date().yyyymmddhhmmss());
    let newContent = "<table style='width:100%;'><tr style='width:100%;'><td style='text-align:center;'>" + t.join("</td><td style='text-align:center;'>") + "</td></tr></table>";
    indexMarquee.container.html(newContent);
  }
}
function updateCurrenciesSpan() {
  var c = [];
  for (var i = 0; i < Currencies.Data.length; i++) {
    //c.push(Currencies.Data[i].Name + ": " + Currencies.Data[i].CurrentPrice.toFixed(4));
    c.push("<a target='_blank' href='" + Currencies.Data[i].URL + "'>" + Currencies.Data[i].Name + "</a>: " + Currencies.Data[i].CurrentPrice.toFixed(4));
  }
  if (c.length > 0) {
    $("#spnCurrencies").html('<font class="open">' + c.join('&emsp;') + '</font>');
  }
}

function showModalDialog_AddInstrument() {
  let modal = document.getElementById("modalDialog");

  $("#modalTitle").html("Add Instrument");

  let content = '<div>Search text: <input type="text" oninput="searchForYahooInstrument(this.value)"/></div><br>' +
                '<div id="addInstrumentSearchResults"></div>';
  $("#modalContentDiv").html(content);

  $("#modalDialog .close").click(function () { modal.style.display = "none"; });

  window.onclick = function (event) { // When the user clicks anywhere outside of the modal, close it
    if (event.target == modal) {
      modal.style.display = "none";
    }
  }

  //$("#modalDialog .ok-button").click(function () { modal.style.display = "none";  });

  //Display the dialog
  modal.style.display = "block";
}
function searchForYahooInstrument(searchString) {
  if (searchString.length > 1) {
    $.ajax({
      type: "POST",
      contentType: "application/json",
      url: "SharePrices.aspx/SearchYahooFinanceShares",
      dataType: "json",
      data: JSON.stringify({ SearchString: searchString }),
      success: function (response) {
        let results = '';
        if (response.quotes) {
          instrumentSearchResults = response.quotes;
          results = '<table class="instrumentSearchResults"><tr><th>Symbol</th><th>Name</th><th>Exchange</th><th>Type</th></tr>';
          for (let x = 0; x < instrumentSearchResults.length; x++) {
            let q = instrumentSearchResults[x];
            results += '<tr id="searchResult' + x.toString() + '" onclick="selectSearchResult(' + x.toString() + ')"><td>' + q.symbol + '</td>' +
                           '<td>' + (q.longname ? q.longname : q.shortname) + '</td>' +
                           '<td>' + q.exchange + '</td>' +
                           '<td>' + q.typeDisp + '</td>' +
                       '</tr>';
          }
          results += '</table><br><div><span id="addInstrumentOKButton" class="ok-button-disabled">Add Instrument</span></div>';
        }
        $("#addInstrumentSearchResults").html(results);
      },
      failure: function (response) { console.error("searchForYahooInstrument error: " + JSON.stringify(response)); }
    });
  }
}
function selectSearchResult(index) {
  for (let x = 0; x < instrumentSearchResults.length; x++) {
    if (x == index) {
      $("#searchResult" + x.toString()).addClass('selectedSearchResult');
    } else {
      $("#searchResult" + x.toString()).removeClass('selectedSearchResult');
    }
  }
  $("#addInstrumentOKButton").removeClass("ok-button-disabled").addClass("ok-button").unbind().click(function () { $("#modalDialog").css("display", "none"); addInstrument(index) });
}
function addInstrument(index) {
  let i = instrumentSearchResults[index];
  console.info("Add instrument: " + JSON.stringify(i));
  $.ajax({
    type: "POST",
    contentType: "application/json",
    url: "SharePrices.aspx/AddInstrument",
    dataType: "json",
    data: JSON.stringify({ Symbol: i.symbol, FullName: (i.longname ? i.longname : i.shortname) }),
    success: function (response) {
      if (response.Symbol) {
        Instruments.Data.push(response);
        let i = Instruments.Data[Instruments.Data.length - 1];
        i.Holdings = [];
        i.DailyData = [];
        i.IntradayData = [];
        let lastTR = $('#tblInstruments>tbody>tr').last();
        let cls = (lastTR.hasClass('shareRow')) ? 'altShareRow' : 'shareRow';
        let tr = '<tr class="' + cls + '" id="shareRow' + i.DisplayOrder.toString() + '">' + createSharesTableRow(i) + '</tr>';
        lastTR.after(tr);
      } else {
        console.error("addInstrument response error: " + JSON.stringify(response));
      }
    },
    failure: function (response) { console.error("addInstrument AJAX error: " + JSON.stringify(response)); }
  });
}

function showModalDialog_AddHolding(symbol) {
//function showModalDialog_AddHolding(span) {
  function GetAccountsDropdown() {
    let dd = '<select id="addHoldingAccountName"><option value="">Select account...</option>';
    for (let i = 0; i < Instruments.Accounts.length; i++) {
      dd += '<option value="' + Instruments.Accounts[i].AccountName + '">' + Instruments.Accounts[i].AccountName + '</option>';
    }
    dd += '</select>';
    return dd;
  }
  function GetTimeDropdowns() {
    let dd = '<select id="addHoldingTimeHour"><option value="">Hour</option>';
    for (let i = 0; i < 24; i++) {
      dd += '<option value="' + i.toString().padStart(2, '0') + '">' + i.toString().padStart(2, '0') + '</option>';
    }
    dd += '</select>';
    dd += '<select id="addHoldingTimeMinute"><option value="">Min</option>';
    for (let i = 0; i < 60; i++) {
      dd += '<option value="' + i.toString().padStart(2, '0') + '">' + i.toString().padStart(2, '0') + '</option>';
    }
    dd += '</select>';
    return dd;
  }
  function GetCurrencyDropdown() {
    let dd = '<select id="newHoldingPurchaseCurrency"><option value="">Currency</option>';
    for (let i = 0; i < Instruments.Data.length; i++) {
      if (Instruments.Data[i].InstrumentType == 'CURRENCY') {
        dd += '<option value="' + Instruments.Data[i].Symbol + '">' + Instruments.Data[i].DisplayName + '</option>';
      }
    }
    dd += '</select>';
    return dd;
  }

  //let symbol = $(span).attr('data-symbol');
  let i = Instruments.GetSymbol(symbol);

  let modal = document.getElementById("modalDialog");
  $("#modalTitle").html("Add Holding");

  let content = '<div><table><tr><th>Instrument Name</th><td>' + i.InstrumentName + '</td></tr>' +
                            '<tr><th>Symbol</th><td>' + symbol + '</td></tr>' +
                            '<tr><th>Account</th><td>' + GetAccountsDropdown() + '</td></tr>' +
                            '<tr><th>Current price</th><td>' + (i.CurrentPrice ? formatAmount(i.CurrentPrice, i.Currency, 2) : '') + '</td></tr>' +
                            '<tr><th>Purchase price / unit</th><td><input type="text" id="newHoldingPrice" value="' + (i.CurrentPrice ? i.CurrentPrice.toString() : '') + '" /></td></tr>' +
                            '<tr><th>No units</th><td><input type="text" id="newHoldingNoUnits" /></td></tr>' +
                            '<tr><th>Total cost in instrument currency</th><td><input type="text" id="newHoldingTotalCost" /></td></tr>' +
                            '<tr><th>Purchase Currency</th><td>' + GetCurrencyDropdown() + '</td></tr>>' +
                            '<tr><th>Book cost in purchase currency</th><td><input type="text" id="newHoldingBookCost" /></td></tr>' +
                            '<tr><th>Purchase date</th><td><input type="text" id="newHoldingDate" value="' + new Date().yyyymmdd() + '" />' + GetTimeDropdowns() + '</td></tr>' +
                     '</table></div><br>' +
                '<div><span id="newHoldingOKButton" class="ok-button-disabled">OK</span></div>';
  $("#modalContentDiv").html(content);
  $("#newHoldingDate").datepicker({ dateFormat: 'yy-mm-dd', onSelect: function () { ValidateNewHolding(symbol) }});
  $("#modalDialog .close").click(function () { modal.style.display = "none"; });
  window.onclick = function (event) { // When the user clicks anywhere outside of the modal, close it
    if (event.target == modal) {
      modal.style.display = "none";
    }
  }

  $("#newHoldingPrice").bind('input', function () {
    $("#newHoldingOKButton").removeClass("ok-button").addClass("ok-button-disabled").unbind();
    try {
      let noUnits = parseFloat($("#newHoldingNoUnits").val());
      let price = parseFloat($("#newHoldingPrice").val());
      $("#newHoldingTotalCost").val((noUnits * price).toFixed(2));
      ValidateNewHolding(symbol);
    } catch (err) { }
  });
  $("#newHoldingNoUnits").bind('input', function () {
    $("#newHoldingOKButton").removeClass("ok-button").addClass("ok-button-disabled").unbind();
    try {
      let noUnits = parseFloat($("#newHoldingNoUnits").val());
      let price = parseFloat($("#newHoldingPrice").val());
      $("#newHoldingTotalCost").val((noUnits * price).toFixed(2));
      ValidateNewHolding(symbol);
    } catch (err) { }
  });
  $("#newHoldingTotalCost").bind('input', function () {
    $("#newHoldingOKButton").removeClass("ok-button").addClass("ok-button-disabled").unbind();
    try {
      let noUnits = parseFloat($("#newHoldingNoUnits").val());
      let totalCost = parseFloat($("#newHoldingTotalCost").val());
      //$("#newHoldingPrice").val((totalCost / noUnits).toFixed(4));
      $("#newHoldingPrice").val((totalCost / noUnits));
      ValidateNewHolding(symbol);
    } catch (err) { }
  });
  $("#newHoldingDate").bind('input', function () {
    ValidateNewHolding(symbol);
  });
  $("#addHoldingTimeHour").bind('input', function () {
    ValidateNewHolding(symbol);
  });
  $("#addHoldingTimeMinute").bind('input', function () {
    ValidateNewHolding(symbol);
  });
  $("#newHoldingPurchaseCurrency").bind('input', function () {
    ValidateNewHolding(symbol);
  });
  $("#newHoldingBookCost").bind('input', function () {
    ValidateNewHolding(symbol);
  });
  $("#addHoldingAccountName").bind('input', function () {
    ValidateNewHolding(symbol);
  });

  //Display the dialog
  modal.style.display = "block";

  return false; //Prevent click event propogating to parent
}
function ValidateNewHolding(symbol) {
  $("#newHoldingOKButton").removeClass("ok-button").addClass("ok-button-disabled").unbind();
  try {
    let accountName = $("#addHoldingAccountName").val();
    let noUnits = parseFloat($("#newHoldingNoUnits").val());
    let price = parseFloat($("#newHoldingPrice").val());
    let purchaseDate = $("#newHoldingDate").val();
    let purchaseHour = $("#addHoldingTimeHour").val();
    let purchaseMinute = $("#addHoldingTimeMinute").val();
    let purchaseCurrency = $("#newHoldingPurchaseCurrency").val();
    let bookCost = $("#newHoldingBookCost").val();

    if (accountName != '' && accountName != 'Select account...' && noUnits > 0 && price > 0 && purchaseDate != '' && purchaseHour != '' && purchaseHour != 'Hour' && purchaseMinute != '' && purchaseMinute != 'Min' && purchaseCurrency != 'Currency' && bookCost > 0) {
      let purchaseDT = new Date(purchaseDate + ' ' + purchaseHour + ':' + purchaseMinute);
      if (purchaseDT <= new Date()) {
        $("#newHoldingOKButton").removeClass("ok-button-disabled").addClass("ok-button").unbind().bind('click', function () { $("#modalDialog").css("display", "none"); AddHolding(accountName, symbol, noUnits, price, purchaseDT, purchaseCurrency, bookCost) });
      }
    }
  } catch (err) { }
}
function AddHolding(accountName, symbol, noUnits, purchasePricePerUnit, purchaseDate, purchaseCurrency, bookCost) {
  console.info("AddHolding: " + accountName + ", " + symbol + ", " + noUnits.toString() + ", " + purchasePricePerUnit.toFixed(4) + ", " + new Date(purchaseDate).yyyymmdd());

  $.ajax({
    type: "POST",
    contentType: "application/json",
    url: "SharePrices.aspx/AddHolding",
    dataType: "json",
    data: JSON.stringify({ AccountName: accountName, Symbol: symbol, NoUnits: noUnits, PurchasePricePerUnit: purchasePricePerUnit, PurchaseDate: new Date(purchaseDate).getTime(), PurchaseCurrency: purchaseCurrency, BookCostInPurchaseCurrency: bookCost }),
    success: function (response) {
      if (response.HoldingID) {
        let i = Instruments.GetSymbol(symbol);
        if (!i.Holdings) i.Holdings = [];
        i.Holdings.push(response);
        let tr = $("#shareRow" + i.DisplayOrder.toString());
        tr.html(createSharesTableRow(i));
        createLongChart(i);
        createMidChart(i);
        createShortChart(i);
        createSingleDayChart(i);
      } else {
        console.error("addInstrument response error: " + JSON.stringify(response));
      }
    },
    failure: function (response) { console.error("addInstrument AJAX error: " + JSON.stringify(response)); }
  });

  //Update the account cash balance to reflect the purchase
  let balance = 0 - bookCost;
  let curr = Instruments.GetAccountCurrencyHolding(accountName, Instruments.GetSymbol(purchaseCurrency));
  if (curr) {
    balance += curr.NoUnits;
  }

  $.ajax({
    type: "POST",
    contentType: "application/json",
    url: "SharePrices.aspx/SetAccountCashValue",
    dataType: "json",
    data: JSON.stringify({ AccountName: accountName, Currency: purchaseCurrency, Value: balance }),
    success: function (response) {
      console.info("SetAccountCashValue success: " + JSON.stringify(response));

      let c = Instruments.GetSymbol(response.Symbol);

      let ch = Instruments.GetAccountCurrencyHolding(response.AccountName, c);
      if (ch) {
        ch.NoUnits = response.NoUnits;
      } else {
        c.Holdings.push(response);
      }
    },
    failure: function (response) { console.error("SetAccountCashValue error: " + JSON.stringify(response)); }
  });

}

function showModalDialog_SoldHolding(holdingID) {
  function GetTimeDropdowns() {
    let dd = '<select id="soldHoldingTimeHour"><option value="">Hour</option>';
    for (let i = 0; i < 24; i++) {
      dd += '<option value="' + i.toString().padStart(2, '0') + '">' + i.toString().padStart(2, '0') + '</option>';
    }
    dd += '</select>';
    dd += '<select id="soldHoldingTimeMinute"><option value="">Min</option>';
    for (let i = 0; i < 60; i++) {
      dd += '<option value="' + i.toString().padStart(2, '0') + '">' + i.toString().padStart(2, '0') + '</option>';
    }
    dd += '</select>';
    return dd;
  }
  function GetCurrencyDropdown() {
    let dd = '<select id="soldHoldingCurrency"><option value="">Currency</option>';
    for (let i = 0; i < Instruments.Data.length; i++) {
      if (Instruments.Data[i].InstrumentType == 'CURRENCY') {
        dd += '<option value="' + Instruments.Data[i].Symbol + '">' + Instruments.Data[i].DisplayName + '</option>';
      }
    }
    dd += '</select>';
    return dd;
  }

  let modal = document.getElementById("modalDialog");
  $("#modalTitle").html("Sold Holding");

  let o = Instruments.GetHolding(holdingID);
  let h = o.holding;
  let i = o.instrument;
  let content = 'Symbol: ' + i.Symbol +
            '<br>Name: ' + i.InstrumentName +
            '<br>Account: ' + h.AccountName + '<input type="hidden" id="soldHoldingsAccountName" value="' + h.AccountName + '" />' +
            '<br>Purchase Date: ' + new Date(h.PurchaseDate).yyyymmddhhmmss() +
            '<br>Purchase Price: ' + formatAmount(h.PurchasePricePerUnit, i.Currency, 2) +
            '<br>No Units: ' + formatAmount(h.NoUnits, '', 0) +
            '<br>Cost: ' + formatAmount(h.NoUnits * h.PurchasePricePerUnit, i.Currency, 2) +
            '<br><br>Mark holding as sold?' +
            '<br><br>Sold date: <input type="text" id="soldHoldingDate" value="' + new Date().yyyymmdd() + '" />' + GetTimeDropdowns() +
            '<br>Currency: ' + GetCurrencyDropdown() + ' Proceeds: <input type="text" id="soldHoldingProceeds">' +
            '<br><br><span id="soldHoldingOKButton" class="ok-button-disabled">OK</span>';
  $("#modalContentDiv").html(content);

  $("#soldHoldingDate").datepicker({ dateFormat: 'yy-mm-dd', onSelect: function () { ValidateSoldHolding(h) } });
  $("#soldHoldingDate").bind('input', function () { ValidateSoldHolding(h); });
  $("#soldHoldingTimeHour").bind('input', function () { ValidateSoldHolding(h); });
  $("#soldHoldingTimeMinute").bind('input', function () { ValidateSoldHolding(h); });
  $("#soldHoldingCurrency").bind('input', function () { ValidateSoldHolding(h); });
  $("#soldHoldingProceeds").bind('input', function () { ValidateSoldHolding(h); });

  $("#modalDialog .close").click(function () { modal.style.display = "none"; });

  window.onclick = function (event) { // When the user clicks anywhere outside of the modal, close it
    if (event.target == modal) {
      modal.style.display = "none";
    }
  }

  //$("#modalDialog .ok-button").click(function () { modal.style.display = "none"; deleteHolding(holdingID); });

  //Display the dialog
  modal.style.display = "block";
}
function ValidateSoldHolding(holding) {
  $("#soldHoldingOKButton").removeClass("ok-button").addClass("ok-button-disabled").unbind();
  try {
    let holdingID = holding.HoldingID;
    let purchaseDT = holding.PurchaseDate;
    let soldDate = $("#soldHoldingDate").val();
    let soldHour = $("#soldHoldingTimeHour").val();
    let soldMinute = $("#soldHoldingTimeMinute").val();
    let accountName = $("#soldHoldingsAccountName").val();
    let soldCurrency = $("#soldHoldingCurrency").val();
    let soldProceeds = parseFloat($("#soldHoldingProceeds").val());

    if (soldDate != '' && soldHour != '' && soldHour != 'Hour' && soldMinute != '' && soldMinute != 'Min' && soldCurrency != 'Currency' && soldProceeds > 0) {
      let soldDT = new Date(soldDate + ' ' + soldHour + ':' + soldMinute).getTime();
      if (soldDT <= new Date().getTime() && soldDT > purchaseDT) {
        $("#soldHoldingOKButton").removeClass("ok-button-disabled").addClass("ok-button").unbind().bind('click', function () { $("#modalDialog").css("display", "none"); soldHolding(holdingID, soldDT, accountName, soldCurrency, soldProceeds) });
      }
    }
  } catch (err) { }
}
function soldHolding(holdingID, soldDate, accountName, soldCurrency, soldProceeds) {
  //console.info("Sold holding: " + holdingID.toString());

  let o = Instruments.GetHolding(holdingID);
  let h = o.holding;
  let i = o.instrument;

  h.SoldDate = soldDate;

  //Mark the holding as sold in the database
  $.ajax({
    type: "POST",
    contentType: "application/json",
    url: "SharePrices.aspx/SoldHolding",
    dataType: "json",
    data: JSON.stringify({ HoldingID: holdingID, SoldDate: soldDate, SoldCurrency: soldCurrency, SoldProceeds: soldProceeds }),
    success: function (response) {
      $("#shareRow" + i.DisplayOrder).html(createSharesTableRow(i));
      createLongChart(i);
      createMidChart(i);
      createShortChart(i);
      createSingleDayChart(i);
    },
    failure: function (response) { console.error("SoldHolding error: " + JSON.stringify(response)); }
  });

  let balance = soldProceeds;
  let curr = Instruments.GetAccountCurrencyHolding(accountName, Instruments.GetSymbol(soldCurrency));
  if (curr) {
    balance += curr.NoUnits;
  }

  //Update the account cash value to reflect the sale
  $.ajax({
    type: "POST",
    contentType: "application/json",
    url: "SharePrices.aspx/SetAccountCashValue",
    dataType: "json",
    data: JSON.stringify({ AccountName: accountName, Currency: soldCurrency, Value: balance }),
    success: function (response) {
      console.info("SetAccountCashValue success: " + JSON.stringify(response));

      let c = Instruments.GetSymbol(response.Symbol);

      let ch = Instruments.GetAccountCurrencyHolding(response.AccountName, c);
      if (ch) {
        ch.NoUnits = response.NoUnits;
      } else {
        c.Holdings.push(response);
      }
    },
    failure: function (response) { console.error("SetAccountCashValue error: " + JSON.stringify(response)); }
  });
}

function showModalDialog_FullScreenChart(Symbol) {
  let i = Instruments.GetSymbol(Symbol);

  let modal = document.getElementById("modalChart");

  $("#modalChartTitle").html("Detailed Chart: " + i.InstrumentName + " (" + i.Symbol + ")");

  $("#modalChart .close").click(function () { modal.style.display = "none"; });
  $('#spnCompareInstruments').click(function () { compareInstruments(i) });

  window.onclick = function (event) { // When the user clicks anywhere outside of the modal, close it
    if (event.target == modal) {
      modal.style.display = "none";
    }
  }

  //$("#modalDialog .ok-button").click(function () { modal.style.display = "none";  });

  $(document).on('keypress', function (event) { console.info("Keypress: " + event.keyCode.toString()); if (event.keyCode == 27 /*Escape key*/) modal.style.display = "none"; });

  //Display the dialog
  modal.style.display = "block";

  let chartDiv = $("#divFullScreenChart");
  let height = chartDiv.parent().height();
  let width = chartDiv.parent().width();
  chartDiv.height(height).width(width);

  let startDate = i.DailyData[0].DT;
  let endDate = new Date().getTime();
  createFullScreenChart(i, startDate, endDate);
  //createFullScreenComparisonChart(i, Instruments.GetSymbol('BTC-USD'), startDate, endDate);
}
function compareInstruments(Instrument1) {
  function GetAccountsDropdown() {
    let dd = '<select id="selectInstrument2"><option value="">Select 2nd instrument...</option>';
    for (let x = 0; x < Instruments.Data.length; x++) {
      let i = Instruments.Data[x];
      if (i.Symbol != Instrument1.Symbol) {
        dd += '<option value="' + i.Symbol + '">' + i.InstrumentName + ' (' + i.Symbol + ')</option>';
      }
    }
    dd += '</select>';
    return dd;
  }

  let modal = document.getElementById("modalDialog");
  $("#modalTitle").html("Compare Instruments");

  let content = '<div>Select instrument to compare against <strog>' + Instrument1.Symbol + '</strong><br/>' + GetAccountsDropdown() + '</div><br>' +
                '<div><span id="compareInstrumentsOKButton" class="ok-button-disabled">OK</span></div>';
  $("#modalContentDiv").html(content);
  $("#modalDialog .close").click(function () { modal.style.display = "none"; });
  window.onclick = function (event) { // When the user clicks anywhere outside of the modal, close it
    if (event.target == modal) {
      modal.style.display = "none";
    }
  }

  $("#selectInstrument2").bind('input', function () {
    $("#compareInstrumentsOKButton").removeClass("ok-button").addClass("ok-button-disabled").unbind();
    let instrument2Symbol = $("#selectInstrument2").val();
    if (instrument2Symbol != '' && instrument2Symbol != 'Select 2nd instrument...') {
      $("#compareInstrumentsOKButton").removeClass("ok-button-disabled").addClass("ok-button").bind('click', function () {
        $("#modalDialog").css("display", "none");
        let Instrument2 = Instruments.GetSymbol(instrument2Symbol);
        let startDate = Instrument1.DailyData[0].DT > Instrument2.DailyData[0].DT ? Instrument1.DailyData[0].DT : Instrument2.DailyData[0].DT;
        let endDate = new Date().getTime();
        createFullScreenComparisonChart(Instrument1, Instrument2, startDate, endDate);
      });
    }
  });

  //Display the dialog
  modal.style.display = "block";
}

function refreshHistoryChart() {
  let chartContainer = $('#histDiv');

  $.ajax({
    type: "POST",
    contentType: "application/json",
    url: "SharePrices.aspx/GetTotalHoldingsHistory",
    dataType: "json",
    success: function (response) {
      let seriesData = [];

      for (let i = 0; i < response.length; i++) {
        let d = response[i];
        seriesData.push([d.HistoryDate, d.Open, d.High, d.Low, d.Close]);
      }

      chartContainer.html(response);

      Highcharts.stockChart('histDiv', {
        chart: {
          backgroundColor: 'rgba(0,0,0,0)'/*,
          spacingTop: 5,
          spacingBottom: 0,
          spacingLeft: 2,
          spacingRight: 0*/
        },

        title: {
          text: 'Total Holdings History'
        },

        series: [{
          type: 'candlestick',
          name: 'Total Holdings',
          color: '#ff4141',
          lineColor: '#ff4141',
          upColor: '#07b200',
          upLineColor: '#07b200',
          data: seriesData/*,
          lineWidth: 4*/
        }],

        xAxis: [{
          lineColor: '#505050',
          tickColor: '#505050',
          labels: { style: { color: '#b0b0b0'/*, fontSize: 'x-small'*/ } }
        }],
        yAxis: [{
          //title: { text: undefined },
          gridLineColor: '#505050',
          labels: { style: { color: '#b0b0b0'/*, fontSize: 'x-small'*/ } },
          offset: 40
        }],

        plotOptions: {
          ohlc: {
            color: '#ff4141',
            upColor: '#07b200'
          }
        }

      });

    },
    failure: function (response) { console.error("GetTotalHoldingsHistory error: " + response.d) }
  });
  
}
