4183 lines
181 KiB
JavaScript
4183 lines
181 KiB
JavaScript
'use strict';
|
|
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 fetchIntervalSingleDay_WhenOpen = 90 * timespans.oneSecond;
|
|
//var fetchIntervalSingleDay_WhenClosed = 30 * timespans.oneMinute;
|
|
//var fetchInterval = 500;
|
|
var initialPageTitle;
|
|
var fetchTimer = 0;
|
|
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;
|
|
},
|
|
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].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;
|
|
}
|
|
}
|
|
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].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;
|
|
}
|
|
}
|
|
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].lastFetchedSingleDay) { this.Data[i].lastFetchedSingleDay = 0 };
|
|
if (lastFetchedDate > this.Data[i].lastFetchedSingleDay) {
|
|
lastFetchedDate > this.Data[i].lastFetchedSingleDay;
|
|
lastFetchedIndex = i;
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
zzzzz_MarketIsOpen: function (instrument) {
|
|
let currentDT = new Date().getTime();
|
|
let startOfDay = new Date().setHours(0, 0, 0);
|
|
if (instrument.SingleDayStartDate > (startOfDay + timespans.oneDay)) {
|
|
currentDT += timespans.oneDay;
|
|
}
|
|
//if (instrument.Symbol == 'QAN.AX') {
|
|
//console.info('Qantas');
|
|
//console.info(' Current local time: ' + new Date().getTime().toString() + new Date().yyyymmddhhmmss());
|
|
//console.info(' SingleDayStartDate: ' + new Date(instrument.SingleDayStartDate).getTime().toString() + new Date(instrument.SingleDayStartDate).yyyymmddhhmmss());
|
|
//console.info(' SingleDayEndDate: ' + new Date(instrument.SingleDayEndDate).getTime().toString() + new Date(instrument.SingleDayEndDate).yyyymmddhhmmss());
|
|
//}
|
|
if (currentDT < instrument.SingleDayStartDate) {
|
|
return 0; //Not open yet
|
|
} else {
|
|
if (currentDT > instrument.SingleDayEndDate) {
|
|
return 2; //Market closed (end of day)
|
|
} else {
|
|
return 1; //Market is currently open
|
|
}
|
|
}
|
|
},
|
|
zzzz_MarketIsOpen: function (instrument) {
|
|
let currentMarketDT = new Date().getTime() + instrument.GMTOffset;
|
|
|
|
if (currentMarketDT < instrument.SingleDayStartDate) {
|
|
return 0; //Not open yet
|
|
} else {
|
|
if (currentMarketDT > instrument.SingleDayEndDate) {
|
|
return 2; //Market closed (end of day)
|
|
} else {
|
|
return 1; //Market is currently open
|
|
}
|
|
}
|
|
},
|
|
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 (Name, CurrentPrice) {
|
|
var newObject = { Name: Name, CurrentPrice: CurrentPrice };
|
|
var i = this.indexOfName(Name);
|
|
if (i >= 0) {
|
|
this.Data[i] = newObject;
|
|
} else {
|
|
this.Data.push(newObject);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
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'), ' ');
|
|
$.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 = [];
|
|
/*i.MarketIsOpen = function (timeMargin) {
|
|
timeMargin = timeMargin | 0;
|
|
let currentMarketDT = new Date().getTime() + this.GMTOffset; //Should this be new Date().getUTCTime()???
|
|
|
|
if (currentMarketDT < this.SingleDayStartDate-timeMargin) {
|
|
return 0; //Not open yet
|
|
} else {
|
|
if (currentMarketDT > this.SingleDayEndDate+timeMargin) {
|
|
return 2; //Market closed (end of day)
|
|
} else {
|
|
return 1; //Market is currently open
|
|
}
|
|
}
|
|
};*/
|
|
}
|
|
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>YTD</th>' +
|
|
'<th>1 Month</th>' +
|
|
'<th>1 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) {
|
|
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 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);
|
|
t += '<tr><td><span class="deletebutton" onclick="showModalDialog_SoldHolding(' + h.HoldingID + ')">×</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.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>' +
|
|
'</tr>';
|
|
}
|
|
t += '</table>';
|
|
return (noRows>0) ? t : '';
|
|
} else {
|
|
delete Instrument.TotalUnitsHeld;
|
|
delete Instrument.TotalHoldingsCost;
|
|
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> ' +
|
|
//'<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 + "'" + ')" /> ' +
|
|
'<img src="icons/UpArrow.png" alt="Move Instrument Up" width="16" height="16" onclick="moveInstrumentUp(' + "'" + instrument.Symbol + "'" + ')" /> ' +
|
|
'<img src="icons/DownArrow.png" alt="Move Instrument Up" width="16" height="16" onclick="moveInstrumentDown(' + "'" + instrument.Symbol + "'" + ')" /> ' +
|
|
'<img src="' + breakevenIcon + '" class="breakeven-button" alt="Show Breakeven Line" width="16" height="16" onclick="toggleBreakevenLine(' + "'" + instrument.Symbol + "'" + ')" /> ' +
|
|
'<img src="' + previousHoldingsIcon + '" alt="Show Previous Holdings" width="16" height="16" onclick="togglePreviousHoldings(' + "'" + instrument.Symbol + "'" + ')" /> ' +
|
|
'<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 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));
|
|
/*tempContent = $("#shareRow" + do1).html();
|
|
$("#shareRow" + do1).html($("#shareRow" + do2).html());
|
|
$("#shareRow" + do2).html(tempContent);
|
|
*/
|
|
|
|
//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, 750);
|
|
},
|
|
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 monthStartDate = new Date().setHours(0, 0, 0) - timespans.oneMonth;
|
|
let monthEndDate = new Date().getTime();
|
|
//if (Instrument.MidChart) {
|
|
try { Instrument.MidChart.destroy(); } catch (ex) { }
|
|
Instrument.MidChart = createChart(Instrument, monthStartDate, monthEndDate, 'divMidChart' + rowNo.toString(), 'divMidSummary' + rowNo.toString());
|
|
}
|
|
function createShortChart(Instrument) {
|
|
let rowNo = Instrument.DisplayOrder;
|
|
let weekStartDate = new Date().setHours(0, 0, 0) - timespans.oneWeek;
|
|
let weekEndDate = new Date().getTime();
|
|
//if (Instrument.ShortChart) {
|
|
try { Instrument.ShortChart.destroy(); } catch (ex) { }
|
|
Instrument.ShortChart = createChart(Instrument, weekStartDate, weekEndDate, '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 = 0; i < Instrument.DailyData.length; i++) {
|
|
let quote = Instrument.DailyData[i];
|
|
if (quote.DT >= dateFrom && quote.DT < minFoundDate) {
|
|
result.push(quote);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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]);
|
|
}
|
|
|
|
/*
|
|
if (Instrument.Symbol == 'FLT.AX') {
|
|
console.info("Before removing:");
|
|
for (let i = 0; i < result.length; i++) {
|
|
console.info(new Date(result[i][0]).yyyymmddhhmmss() + ' = ' + String(result[i][1]));
|
|
}
|
|
}
|
|
*/
|
|
//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--; }
|
|
/*
|
|
if (Instrument.Symbol == 'FLT.AX') {
|
|
console.info("After removing:");
|
|
for (let i = 0; i < result.length; i++) {
|
|
console.info(new Date(result[i][0]).yyyymmddhhmmss() + ' = ' + String(result[i][1]));
|
|
}
|
|
}
|
|
*/
|
|
|
|
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 = getChartDataSubset(Instrument, startDate, endDate);
|
|
let chartData = [];
|
|
if (endDate - endDate <= timespans.oneDay) {
|
|
chartData = getChartDataSubset(Instrument, startDate, endDate);
|
|
} else {
|
|
chartData = getChartDataSubset(Instrument, startDate - (timespans.oneDay * 3), endDate);
|
|
}
|
|
|
|
//console.info(new Date().yyyymmddhhmmss() + ' - createChart ' + Instrument.Symbol + ' ' + chartContainerID + ' ' + new Date(startDate).yyyymmddhhmmss() + ' -> ' + new Date(endDate).yyyymmddhhmmss());
|
|
let seriesData = [];
|
|
let volumeData = [];
|
|
if (chartData.length > 1) {
|
|
/*let transformedAndSummarisedData = transformAndSummariseChartData(chartData);
|
|
let summaryData = transformedAndSummarisedData.summaryData;
|
|
let basePrice = transformedAndSummarisedData.basePrice;
|
|
seriesData = transformedAndSummarisedData.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 = (!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 ($('#showBreakevenLine').prop('checked')) {
|
|
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 {
|
|
//series.unshift({ data: [[startDate, Instrument.BreakevenPrice], [endDate, Instrument.BreakevenPrice]], color: '#24BCC7', shadowSize: 0, lines: { lineWidth: 1 } });
|
|
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.unshift({ data: b, color: '#00cc00', shadowSize: 0, lines: { lineWidth: 1.5 } });
|
|
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: 'blue', shadowSize: 0, lines: { lineWidth: 1 } });
|
|
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: 'blue', shadowSize: 0, lines: { lineWidth: 1 } });
|
|
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,
|
|
/*min: startDate, */
|
|
max: chartTimespan > timespans.oneDay ? summaryData.EndDT : endDate,
|
|
/*max: 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 = 0; i < Instrument.DailyData.length; i++) {
|
|
let quote = Instrument.DailyData[i];
|
|
if (quote.DT >= dateFrom && quote.DT < minFoundDate) {
|
|
result.push(quote);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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]);
|
|
}
|
|
|
|
/*
|
|
if (Instrument.Symbol == 'FLT.AX') {
|
|
console.info("Before removing:");
|
|
for (let i = 0; i < result.length; i++) {
|
|
console.info(new Date(result[i][0]).yyyymmddhhmmss() + ' = ' + String(result[i][1]));
|
|
}
|
|
}
|
|
*/
|
|
//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--; }
|
|
/*
|
|
if (Instrument.Symbol == 'FLT.AX') {
|
|
console.info("After removing:");
|
|
for (let i = 0; i < result.length; i++) {
|
|
console.info(new Date(result[i][0]).yyyymmddhhmmss() + ' = ' + String(result[i][1]));
|
|
}
|
|
}
|
|
*/
|
|
|
|
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 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) {
|
|
/*let transformedAndSummarisedData = transformAndSummariseChartData(chartData);
|
|
let summaryData = transformedAndSummarisedData.summaryData;
|
|
let basePrice = transformedAndSummarisedData.basePrice;
|
|
seriesData = transformedAndSummarisedData.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 = (!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 */ });
|
|
|
|
/*
|
|
//Dispose of any previous flot 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 xAxisBreaks = [];
|
|
if (chartTimespan > timespans.oneDay && chartTimespan < timespans.oneMonth * 3) {
|
|
xAxisBreaks = generateAxisBreaks(seriesData, timespans.oneHour, 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);
|
|
}
|
|
*/
|
|
|
|
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 }
|
|
/*grid: { show: true, hoverable: true, color: 'white', borderColor: '#505050' },*/
|
|
/*crosshair: { mode: 'x', snapX: true, showXValue: false },*/
|
|
/*xaxis: {
|
|
timezone: 'browser',
|
|
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);
|
|
}
|
|
|
|
//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);
|
|
}
|
|
|
|
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 {
|
|
//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 = 0; i < Instrument.DailyData.length; i++) {
|
|
let quote = Instrument.DailyData[i];
|
|
if (quote.DT >= dateFrom && quote.DT < minFoundDate) {
|
|
result.push(quote);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
//Add an entry for today's latest data from SingleDayData
|
|
if (Instrument.SingleDayData) {
|
|
if (Instrument.SingleDayData.length > 0) {
|
|
if (Instrument.SingleDayData.length > 1) {
|
|
//Aggregate all of the SingleDayData into a single entry
|
|
let e = Instrument.SingleDayData[0];
|
|
let o = e.open;
|
|
let h = e.high;
|
|
let l = e.low;
|
|
let v = e.volume;
|
|
for (let x = 1; x < Instrument.SingleDayData.length; x++) {
|
|
e = Instrument.SingleDayData[x];
|
|
if (e.high > h) {h = e.high}
|
|
if (e.low < l) { l = e.low }
|
|
v += e.volume;
|
|
}
|
|
let c = e.close;
|
|
result.push({DT: e.DT, open: o, high: h, low: l, close: c, volume: v});
|
|
} else {
|
|
result.push(Instrument.SingleDayData[0]);
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
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,
|
|
//tickPixelInterval: 30,
|
|
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 = 0; i < Instrument.DailyData.length; i++) {
|
|
let quote = Instrument.DailyData[i];
|
|
if (quote.DT >= dateFrom && quote.DT < minFoundDate) {
|
|
result.push(quote);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
//Add an entry for today's latest data from SingleDayData
|
|
if (Instrument.SingleDayData) {
|
|
if (Instrument.SingleDayData.length > 0) {
|
|
if (Instrument.SingleDayData.length > 1) {
|
|
//Aggregate all of the SingleDayData into a single entry
|
|
let e = Instrument.SingleDayData[0];
|
|
let o = e.open;
|
|
let h = e.high;
|
|
let l = e.low;
|
|
let v = e.volume;
|
|
for (let x = 1; x < Instrument.SingleDayData.length; x++) {
|
|
e = Instrument.SingleDayData[x];
|
|
if (e.high > h) {h = e.high}
|
|
if (e.low < l) { l = e.low }
|
|
v += e.volume;
|
|
}
|
|
let c = e.close;
|
|
result.push({DT: e.DT, open: o, high: h, low: l, close: c, volume: v});
|
|
} else {
|
|
result.push(Instrument.SingleDayData[0]);
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
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 = 0; i < Instrument.DailyData.length; i++) {
|
|
let quote = Instrument.DailyData[i];
|
|
if (quote.DT >= dateFrom && quote.DT < minFoundDate) {
|
|
result.push(quote);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
//Add an entry for today's latest data from SingleDayData
|
|
if (Instrument.SingleDayData) {
|
|
if (Instrument.SingleDayData.length > 0) {
|
|
if (Instrument.SingleDayData.length > 1) {
|
|
//Aggregate all of the SingleDayData into a single entry
|
|
let e = Instrument.SingleDayData[0];
|
|
let o = e.open;
|
|
let h = e.high;
|
|
let l = e.low;
|
|
let v = e.volume;
|
|
for (let x = 1; x < Instrument.SingleDayData.length; x++) {
|
|
e = Instrument.SingleDayData[x];
|
|
if (e.high > h) { h = e.high }
|
|
if (e.low < l) { l = e.low }
|
|
v += e.volume;
|
|
}
|
|
let c = e.close;
|
|
result.push({ DT: e.DT, open: o, high: h, low: l, close: c, volume: v });
|
|
} else {
|
|
result.push(Instrument.SingleDayData[0]);
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
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, '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',
|
|
//tickPixelInterval: 60,
|
|
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',
|
|
//endOnTick: false,
|
|
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 = 0; i < Instrument.DailyData.length; i++) {
|
|
let quote = Instrument.DailyData[i];
|
|
if (quote.DT >= dateFrom && quote.DT < minFoundDate) {
|
|
result.push(quote);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
//Add an entry for today's latest data from SingleDayData
|
|
if (Instrument.SingleDayData) {
|
|
if (Instrument.SingleDayData.length > 0) {
|
|
if (Instrument.SingleDayData.length > 1) {
|
|
//Aggregate all of the SingleDayData into a single entry
|
|
let e = Instrument.SingleDayData[0];
|
|
let o = e.open;
|
|
let h = e.high;
|
|
let l = e.low;
|
|
let v = e.volume;
|
|
for (let x = 1; x < Instrument.SingleDayData.length; x++) {
|
|
e = Instrument.SingleDayData[x];
|
|
if (e.high > h) { h = e.high }
|
|
if (e.low < l) { l = e.low }
|
|
v += e.volume;
|
|
}
|
|
let c = e.close;
|
|
result.push({ DT: e.DT, open: o, high: h, low: l, close: c, volume: v });
|
|
} else {
|
|
result.push(Instrument.SingleDayData[0]);
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
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 = $('#displayAsPercent').prop('checked') ? 'percent' : '';
|
|
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);
|
|
|
|
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 priceHint = dataSeries.meta.priceHint;
|
|
let scale = dataSeries.meta.scale || 1;
|
|
if (priceHint != 2 || scale != 1) {
|
|
let minDT = dataSeries.timestamp[0] * 1000;
|
|
let maxDT = dataSeries.timestamp[dataSeries.timestamp.length - 1] * 1000;
|
|
let prices = [];
|
|
for (let t = 0; t < dataSeries.timestamp.length - 1; t++) {
|
|
let DT = dataSeries.timestamp[t] * 1000;
|
|
prices.push({ DT: DT, Date: new Date(DT).yyyymmddhhmmss(), price: dataSeries.indicators.quote[0].close[t]});
|
|
}
|
|
console.info({
|
|
priceHint: priceHint,
|
|
scale: scale,
|
|
symbol: Instrument.Symbol,
|
|
minDT: minDT,
|
|
maxDT: maxDT,
|
|
minDate: new Date(minDT).yyyymmddhhmmss(),
|
|
maxDate: new Date(maxDT).yyyymmddhhmmss(),
|
|
prices: prices
|
|
});
|
|
}
|
|
*/
|
|
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 {
|
|
//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);
|
|
}
|
|
if (newData.length) {
|
|
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 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)) + ' -> ' + (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)) + ' -> ' + (Instrument.CurrentPrice < 0.1 ? Instrument.CurrentPrice.autoScale() : Instrument.CurrentPrice.toFixed(2)) + ': ' + getProfitOrLoss(Instrument.BreakevenPrice, Instrument.CurrentPrice) + '<br>';
|
|
currentValue += 'Value: ' + (Instrument.TotalHoldingsCost).toFixed(2) + ' -> ' + (Instrument.TotalUnitsHeld * Instrument.CurrentPrice).toFixed(2) + ': ' + getProfitOrLoss(Instrument.TotalHoldingsCost, Instrument.TotalUnitsHeld * Instrument.CurrentPrice);
|
|
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.TotalHoldingsCost / exRate, 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 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 (!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.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 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
|
|
Instrument.DailyData.push(NewData[i]);
|
|
if (NewData[i].DT <= currentMaxDT) {
|
|
resortRequired = true;
|
|
}
|
|
}
|
|
}
|
|
//Resort 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;
|
|
}
|
|
);
|
|
}
|
|
|
|
/*
|
|
for (let i = 0; i < NewData.length; i++) {
|
|
if (NewData[i].DT > currentMaxDT || NewData[i].DT < currentMinDT) {
|
|
Instrument.DailyData.push(NewData[i]);
|
|
}
|
|
}
|
|
*/
|
|
|
|
$.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 zzz_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) {
|
|
aggUnits += h.NoUnits;
|
|
aggBookCost += h.NoUnits * h.PurchasePricePerUnit;
|
|
aggBookCostGBP += h.NoUnits * h.PurchasePricePerUnit / exchangeRate;
|
|
if (h.PurchaseDate < minPurchaseData) { minPurchaseData = h.PurchaseDate; }
|
|
if (h.PurchaseDate > maxPurchaseDate) { maxPurchaseDate = h.PurchaseDate; }
|
|
if (!accountNames.includes(h.AccountName)) { accountNames.push(h.AccountName); }
|
|
|
|
aggSingleDayGainGBP += ((i.CurrentPrice - (h.PurchaseDate > i.SingleDayStartDate ? h.PurchasePricePerUnit : i.SingleDayPreviousClose)) * h.NoUnits) / exchangeRate;
|
|
|
|
bookCostPerAccount.addCategoryAmount(h.AccountName, (h.NoUnits * h.PurchasePricePerUnit) / exchangeRate);
|
|
currentValuePerAccount.addCategoryAmount(h.AccountName, (h.NoUnits * i.CurrentPrice) / exchangeRate);
|
|
}
|
|
}
|
|
|
|
let aggPurchasePricePerUnit = aggBookCost / aggUnits;
|
|
let aggCurrentValue = aggUnits * (i.Currency == 'GBp' ? i.CurrentPrice / 100 : i.CurrentPrice);
|
|
let aggCurrentValueGBP = (aggUnits * i.CurrentPrice) / exchangeRate;
|
|
let aggGainGBP = ((i.CurrentPrice - aggPurchasePricePerUnit) * aggUnits) / exchangeRate;
|
|
|
|
/*
|
|
let aggSingleDayGainGBP = ((i.CurrentPrice - i.SingleDayPreviousClose) * aggUnits) / exchangeRate;
|
|
*/
|
|
let aggSingleDayGainPercent = ((i.CurrentPrice / i.SingleDayPreviousClose) - 1) * 100;
|
|
//let aggSingleDayProfitOrLoss = i.CurrentPrice < i.SingleDayPreviousClose ? 'loss' : 'profit';
|
|
let aggSingleDayProfitOrLoss = aggSingleDayGainGBP < 0 ? 'loss' : 'profit';
|
|
|
|
return {
|
|
displayOrder: i.DisplayOrder,
|
|
holdingNumber: 0,
|
|
instrumentName: i.InstrumentName,
|
|
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,
|
|
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') });
|
|
}
|
|
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 = [];
|
|
|
|
for (let n = 0; n < Instruments.Data.length; n++) {
|
|
let i = Instruments.Data[n];
|
|
if (i.InstrumentType == 'EQUITY') {
|
|
if (i.CurrentPrice) {
|
|
if (groupHoldings) {
|
|
let agg = aggregateHoldings(i);
|
|
if (agg.noUnits > 0) {
|
|
r.push(agg);
|
|
|
|
if (agg.currentValueGBP > maxCurrentValueGBP) { maxCurrentValueGBP = agg.currentValueGBP };
|
|
|
|
holdingCurrencies.addHoldingCurrency(i.Currency, agg.noUnits * agg.purchasePricePerUnit);
|
|
holdingCurrenciesCurrentValue.addHoldingCurrency(i.Currency, agg.noUnits * i.CurrentPrice);
|
|
|
|
totalBookCostGBP += agg.bookCostGBP;
|
|
totalValueGBP += agg.currentValueGBP;
|
|
totalGainGBP += agg.gainGBP;
|
|
todaysGainGBP += (agg.marketIsOpen == 0 ? 0 : agg.singleDayGainGBP);
|
|
}
|
|
} else {
|
|
let marketIsOpen = Instruments.MarketIsOpen(i);
|
|
let exchangeRate = Instruments.GetExchangeRate(i.Currency, 'GBP');
|
|
|
|
for (let hn = 0; hn < i.Holdings.length; hn++) {
|
|
let h = i.Holdings[hn];
|
|
if (h.SoldDate == null) {
|
|
let currentHoldingValue = h.NoUnits * (i.Currency == 'GBp' ? i.CurrentPrice / 100 : i.CurrentPrice);
|
|
let holdingBookCost = h.NoUnits * h.PurchasePricePerUnit;
|
|
let holdingCurrentValueGBP = (h.NoUnits * i.CurrentPrice) / exchangeRate;
|
|
let holdingGainGBP = ((i.CurrentPrice - h.PurchasePricePerUnit) * h.NoUnits) / exchangeRate;
|
|
|
|
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,
|
|
instrumentName: i.InstrumentName,
|
|
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,
|
|
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);
|
|
holdingCurrenciesCurrentValue.addHoldingCurrency(i.Currency, h.NoUnits * i.CurrentPrice);
|
|
bookCostPerAccount.addCategoryAmount(h.AccountName, holdingBookCost / exchangeRate);
|
|
currentValuePerAccount.addCategoryAmount(h.AccountName, holdingCurrentValueGBP);
|
|
|
|
totalBookCostGBP += holdingBookCost / exchangeRate;
|
|
totalValueGBP += holdingCurrentValueGBP;
|
|
totalGainGBP += holdingGainGBP;
|
|
todaysGainGBP += (marketIsOpen == 0 ? 0 : singleDayGainGBP);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
totalBookCostGBP: totalBookCostGBP,
|
|
totalValueGBP: totalValueGBP,
|
|
totalGainGBP: totalGainGBP,
|
|
todaysGainGBP: todaysGainGBP,
|
|
maxCurrentValueGBP: maxCurrentValueGBP,
|
|
holdings: r,
|
|
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>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's<br>Gain £</th>' +
|
|
'<th>Today's<br>Gain %</th>' +
|
|
'</tr></thead><tbody id="tbMainHoldings"></tbody><tfoot id="tfMainHoldings"><tr><td colspan="17"> </td></tr></tfoot></table>';
|
|
|
|
//$('#holdingsDiv').html(tbl);
|
|
$('#holdingsTableDiv').html(tbl);
|
|
|
|
$("#tblMainHoldings").tablesorter();
|
|
}
|
|
}
|
|
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;"> </div>' +
|
|
'<div class="pcLabel" style="width:' + cellWidth + 'px">' + labelText + '</div>' +
|
|
'</td>';
|
|
}
|
|
|
|
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 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 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 + '">' + 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>' + new Date(h.purchaseDateMin).yyyymmdd() + '</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 + '">' + formatAmount(h.purchasePricePerUnit, h.currency, 2) + '</td>' + //Buy Price
|
|
'<td class="num ' + openOrClosed + '">' + formatAmount(h.currentPrice, h.currency, 2) + '</td>' + //Current Price //'<td class="num">' + formatAmount(i.CurrentPrice, i.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 + '">' + formatAmount(h.currentValue, h.currency == 'GBp' ? 'GBP' : h.currency, 0) + '</td>' + //Current Value
|
|
'<td class="num ' + profitOrLoss + '">' + formatAmount(h.gain, h.currency == 'GBp' ? 'GBP' : h.currency, 0) + '</td>' + //Gain
|
|
'<td class="num ' + profitOrLoss + '">' + formatAmount(h.gainGBP, 'GBP', 0) + '</td>' + //Gain £
|
|
'<td class="num ' + profitOrLoss + '">' + h.gainPercent.toFixed(1) + '%</td>' + //Gain %
|
|
'<td class="num ' + openOrClosed + '">' + formatAmount(h.currentValueGBP, 'GBP', 2) + '</td>' + //Current Value £
|
|
//'<td class="num">' + formatAmount(h.currentValueGBP / totalValueGBP * 100, '', 1) + '%</td>' + //Allocation
|
|
getPercentCell('#3f2d95', 70, scaledAllocation, allocationPercent.toFixed(1) + '%') + //Allocation
|
|
(h.marketIsOpen == 0 ? '<td/>' : '<td class="num ' + h.singleDayProfitOrLoss + '">' + formatAmount(h.singleDayGainGBP, 'GBP', 0) + '</td>') + //Today's Gain £
|
|
(h.marketIsOpen == 0 ? '<td/>' : '<td class="num ' + h.singleDayProfitOrLoss + '">' + h.singleDayGainPercent.toFixed(1) + '%</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 row = '<tr id="tfHoldingsTotals"><td id="holdingsLastUpdated">Updated: ' + new Date().yyyymmddhhmmss() + '</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td>' +
|
|
'<td class="num">' + formatAmount(totalBookCostGBP, 'GBP', 2) + '</td>' +
|
|
'<td></td><td></td>' +
|
|
'<td class="num ' + profitOrLoss + '">' + formatAmount(totalGainGBP, 'GBP', 2) + '</td>' +
|
|
'<td class="num ' + profitOrLoss + '">' + formatAmount((((totalValueGBP / totalBookCostGBP) - 1) * 100), '', 1) + '%</td>' +
|
|
'<td class="num">' + formatAmount(totalValueGBP, 'GBP', 2) + '</td>' +
|
|
'<td> </td>' + //Allocation
|
|
'<td class="num ' + todaysProfitOrLoss + '">' + formatAmount(todaysGainGBP, 'GBP', 2) + '</td>' +
|
|
'<td class="num ' + todaysProfitOrLoss + '">' + formatAmount(((todaysGainGBP / totalBookCostGBP) * 100), '', 1) + '%</td>' +
|
|
'</tr>';
|
|
addTableRow("tfHoldingsTotals", row, "tfMainHoldings", false);
|
|
|
|
//Flash the last updated cell
|
|
$("#holdingsLastUpdated").addClass("highlighted");
|
|
//Remove the highlight from all rows/cells
|
|
$("#tblMainHoldings").find(".highlighted").removeClass("highlighted", 1200);
|
|
|
|
//Tell tablesorter that the table has been updated
|
|
var resort = true;
|
|
$("#tblMainHoldings").trigger("update", [resort]);
|
|
|
|
//Update the total holdings span in the site banner
|
|
$("#spnTotalHoldings").html("Total holdings: " + formatAmount(totalValueGBP, "GBP", 2));
|
|
|
|
//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: { 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 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) {
|
|
aggUnits += h.NoUnits;
|
|
aggBookCost += h.NoUnits * h.PurchasePricePerUnit;
|
|
aggBookCostGBP += h.NoUnits * h.PurchasePricePerUnit / exchangeRate;
|
|
bookCostPerAccount.addCategoryAmount(h.AccountName, (h.NoUnits * h.PurchasePricePerUnit) / exchangeRate);
|
|
|
|
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;
|
|
aggSingleDayGainPercent = ((i.CurrentPrice / i.SingleDayPreviousClose) - 1) * 100;
|
|
aggSingleDayProfitOrLoss = aggSingleDayGainGBP < 0 ? 'loss' : 'profit';
|
|
}
|
|
}
|
|
|
|
return {
|
|
displayOrder: i.DisplayOrder,
|
|
holdingNumber: 0,
|
|
instrumentType: i.InstrumentType,
|
|
instrumentName: i.InstrumentName,
|
|
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,
|
|
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') });
|
|
}
|
|
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 = [];
|
|
|
|
for (let n = 0; n < Instruments.Data.length; n++) {
|
|
let i = Instruments.Data[n];
|
|
//if (i.InstrumentType == 'EQUITY') {
|
|
if (i.InstrumentType == 'EQUITY' || i.InstrumentType == 'CRYPTOCURRENCY') {
|
|
//if (i.CurrentPrice) {
|
|
if (groupHoldings) {
|
|
let agg = aggregateHoldings(i);
|
|
if (agg.noUnits > 0) {
|
|
r.push(agg);
|
|
|
|
holdingCurrencies.addHoldingCurrency(i.Currency, agg.noUnits * agg.purchasePricePerUnit);
|
|
if (i.CurrentPrice) {
|
|
if (agg.currentValueGBP > maxCurrentValueGBP) { maxCurrentValueGBP = agg.currentValueGBP };
|
|
holdingCurrenciesCurrentValue.addHoldingCurrency(i.Currency, agg.noUnits * i.CurrentPrice);
|
|
if (agg.currentExchangeRate) {
|
|
totalBookCostGBP += agg.bookCostGBP;
|
|
totalValueGBP += agg.currentValueGBP;
|
|
totalGainGBP += agg.gainGBP;
|
|
todaysGainGBP += (agg.marketIsOpen == 0 ? 0 : agg.singleDayGainGBP);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
let marketIsOpen = Instruments.MarketIsOpen(i);
|
|
let exchangeRate = Instruments.GetExchangeRate(i.Currency, 'GBP');
|
|
|
|
for (let hn = 0; hn < i.Holdings.length; hn++) {
|
|
let h = i.Holdings[hn];
|
|
if (h.SoldDate == null) {
|
|
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 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,
|
|
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,
|
|
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;
|
|
totalValueGBP += holdingCurrentValueGBP;
|
|
totalGainGBP += holdingGainGBP;
|
|
todaysGainGBP += (marketIsOpen == 0 ? 0 : singleDayGainGBP);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//} //if (i.CurrentPrice)
|
|
}
|
|
}
|
|
|
|
return {
|
|
totalBookCostGBP: totalBookCostGBP,
|
|
totalValueGBP: totalValueGBP,
|
|
totalGainGBP: totalGainGBP,
|
|
todaysGainGBP: todaysGainGBP,
|
|
maxCurrentValueGBP: maxCurrentValueGBP,
|
|
holdings: r,
|
|
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's<br>Gain £</th>' +
|
|
'<th>Today's<br>Gain %</th>' +
|
|
'</tr></thead><tbody id="tbMainHoldings"></tbody><tfoot id="tfMainHoldings"><tr><td colspan="18"> </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 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;"> </div>' +
|
|
'<div class="pcLabel" style="width:' + cellWidth + 'px">' + labelText + '</div>' +
|
|
'</td>';
|
|
}
|
|
|
|
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 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.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 ? formatAmount(h.gain, h.currency == 'GBp' ? 'GBP' : h.currency, 0) : '') + '</td>' + //Gain
|
|
'<td class="num ' + profitOrLoss + '">' + (h.currentExchangeRate ? formatAmount(h.gainGBP, 'GBP', 0) : '') + '</td>' + //Gain £
|
|
'<td class="num ' + profitOrLoss + '">' + (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 row = '<tr id="tfHoldingsTotals"><td></td><td id="holdingsLastUpdated">Updated: ' + new Date().yyyymmddhhmmss() + '</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td>' +
|
|
'<td class="num">' + formatAmount(totalBookCostGBP, 'GBP', 0) + '</td>' + //Book Cost
|
|
'<td></td><td></td>' +
|
|
'<td class="num ' + profitOrLoss + '">' + formatAmount(totalGainGBP, 'GBP', 0) + '</td>' + //Gain GBP
|
|
'<td class="num ' + profitOrLoss + '">' + formatAmount((((totalValueGBP / totalBookCostGBP) - 1) * 100), '', 1) + '%</td>' + //Gain %
|
|
'<td class="num">' + formatAmount(totalValueGBP, 'GBP', 0) + '</td>' + //Current Value
|
|
'<td> </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 %
|
|
'</tr>';
|
|
addTableRow("tfHoldingsTotals", row, "tfMainHoldings", false);
|
|
|
|
//Flash the last updated cell
|
|
$("#holdingsLastUpdated").addClass("highlighted");
|
|
//Remove the highlight from all rows/cells
|
|
$("#tblMainHoldings").find(".highlighted").removeClass("highlighted", 1200);
|
|
|
|
//Tell tablesorter that the table has been updated
|
|
var resort = true;
|
|
$("#tblMainHoldings").trigger("update", [resort]);
|
|
|
|
//Update the total holdings span in the site banner
|
|
let formattedAmount = formatAmount(totalValueGBP, "GBP", 0);
|
|
$("#spnTotalHoldings").html("Total holdings: " + formattedAmount);
|
|
//Set the page title
|
|
document.title = formattedAmount + ' - ' + initialPageTitle;
|
|
|
|
//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 };
|
|
accounts.push(a);
|
|
}
|
|
let h = {
|
|
instrumentName: instrument.InstrumentName,
|
|
symbol: instrument.Symbol,
|
|
currency: instrument.Currency,
|
|
purchaseDate: holding.PurchaseDate,
|
|
purchasePricePerUnit: holding.PurchasePricePerUnit,
|
|
noUnits: holding.NoUnits,
|
|
cost: holding.NoUnits * holding.PurchasePricePerUnit
|
|
}
|
|
if (exchangeRate) {
|
|
h.costGBP = h.cost / exchangeRate;
|
|
a.totalCostGBP += h.costGBP;
|
|
}
|
|
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;
|
|
a.totalValueGBP += h.currentValueGBP;
|
|
a.totalGainGBP += h.gainGBP;
|
|
}
|
|
}
|
|
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();
|
|
|
|
//let eDT = new Date();
|
|
//console.info('createAccountsTable after getAccounts: ' + (eDT.getTime() - sDT.getTime()) + ' (' + sDT.yyyymmddhhmmss() + ' -> ' + eDT.yyyymmddhhmmss() + ')');
|
|
|
|
//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;
|
|
});
|
|
|
|
//eDT = new Date();
|
|
//console.info('createAccountsTable after array sort: ' + (eDT.getTime() - sDT.getTime()) + ' (' + sDT.yyyymmddhhmmss() + ' -> ' + eDT.yyyymmddhhmmss() + ')');
|
|
|
|
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 accountsTables = ''
|
|
let tableIDs = ['accountsSummaryTable'];
|
|
let altRow = 1;
|
|
for (let ax = 0; ax < accounts.length; ax++) {
|
|
let a = accounts[ax];
|
|
altRow = !altRow;
|
|
let profitOrLoss = a.totalGainGBP < 0 ? 'loss' : 'profit';
|
|
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(a.totalValueGBP || 0, 'GBP', 2) + "</td>" +
|
|
"<td class='num " + profitOrLoss + "'>" + formatAmount(a.totalGainGBP || 0, 'GBP', 2) + "</td>" +
|
|
"</tr>";
|
|
|
|
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';
|
|
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 ' + holdingProfitOrLoss + '">' + formatAmount(h.gainGBP, 'GBP', 2) + '</td>' +
|
|
'<td class="num ' + holdingProfitOrLoss + '">' + ((h.gain / h.cost) * 100).toFixed(1) + '%</td>' +
|
|
'</tr>';
|
|
}
|
|
accountsTables += '</tbody></table>';
|
|
}
|
|
summaryTable += "</tbody></table>";
|
|
$("#accoountsDiv").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"> </td></tr><tr><td colspan="15"> </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 + ': ' + i.CurrentPrice + ' </font><font class="loss">' + percentChange.toFixed(1) + '%</font>');
|
|
} else {
|
|
t.push('<font class="' + openOrClosed + '">' + i.DisplayName + ': ' + i.CurrentPrice + ' </font><font class="profit">+' + percentChange.toFixed(1) + '%</font>');
|
|
}
|
|
} else {
|
|
t.push('<font class="' + openOrClosed + '">' + i.DisplayName + ': ' + i.CurrentPrice + '</font>');
|
|
}
|
|
}
|
|
}
|
|
if (t.length > 0) {
|
|
t.push(new Date().yyyymmddhhmmss());
|
|
let newContent = t.join("  ");
|
|
indexMarquee.setContent(newContent);
|
|
}
|
|
}*/
|
|
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));
|
|
}
|
|
if (c.length > 0) {
|
|
$("#spnCurrencies").html('<font class="open">' + c.join(' ') + '</font>');
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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 +
|
|
'<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><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);
|
|
});
|
|
|
|
$("#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();
|
|
|
|
if (soldDate != '' && soldHour != '' && soldHour != 'Hour' && soldMinute != '' && soldMinute != 'Min') {
|
|
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) });
|
|
}
|
|
}
|
|
} catch (err) { }
|
|
}
|
|
function soldHolding(holdingID, soldDate) {
|
|
//console.info("Sold holding: " + holdingID.toString());
|
|
|
|
let o = Instruments.GetHolding(holdingID);
|
|
let h = o.holding;
|
|
let i = o.instrument;
|
|
|
|
h.SoldDate = soldDate;
|
|
|
|
$.ajax({
|
|
type: "POST",
|
|
contentType: "application/json",
|
|
url: "SharePrices.aspx/SoldHolding",
|
|
dataType: "json",
|
|
data: JSON.stringify({ HoldingID: holdingID, SoldDate: soldDate }),
|
|
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)); }
|
|
});
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
//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</th><td><input type="text" id="newHoldingTotalCost" /></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);
|
|
});
|
|
$("#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();
|
|
|
|
if (accountName != '' && accountName != 'Select account...' && noUnits > 0 && price > 0 && purchaseDate != '' && purchaseHour != '' && purchaseHour != 'Hour' && purchaseMinute != '' && purchaseMinute != 'Min') {
|
|
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) });
|
|
}
|
|
}
|
|
} catch (err) { }
|
|
}
|
|
function AddHolding (accountName, symbol, noUnits, purchasePricePerUnit, purchaseDate) {
|
|
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() }),
|
|
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)); }
|
|
});
|
|
}
|
|
|
|
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) { 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";
|
|
}
|