Compare commits

...

2 Commits

9 changed files with 789 additions and 700 deletions

File diff suppressed because one or more lines are too long

View File

@ -141,6 +141,30 @@
<div id="chartTooltip" class="chart-tooltip"></div>
<script>
let importedFunctions = {};
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 showTab(tab) {
//$('.navbar>a').removeClass('activenav');
$('.navbar').find('.activenav').removeClass('activenav');
@ -161,11 +185,11 @@
redrawAllSharesCharts();
break;*/
case 'displayAsPercent':
redrawAllSharesCharts();
importedFunctions.redrawAllSharesCharts();
break;
case 'excludeNoHoldings':
createSharesTable();
redrawAllSharesCharts();
importedFunctions.createSharesTable();
importedFunctions.redrawAllSharesCharts();
break;
/*case 'showPreviousHoldings':
redrawAllSharesCharts();
@ -182,12 +206,24 @@
}
</script>
<script type="module">
import { getInstruments, refreshHistoryChart, showModalDialog_AddInstrument, showModalDialog_AddHolding, showModalDialog_FullScreenChart, showModalDialog_SoldHolding } from "./scripts/SharePrices.js";
import {
createSharesTable,
getInstruments,
redrawAllSharesCharts,
refreshHistoryChart,
showModalDialog_AddInstrument,
showModalDialog_AddHolding,
showModalDialog_FullScreenChart,
showModalDialog_SoldHolding
} from "./scripts/SharePrices.js";
importedFunctions = {
createSharesTable: createSharesTable,
getInstruments: getInstruments,
redrawAllSharesCharts: redrawAllSharesCharts,
refreshHistoryChart: refreshHistoryChart,
showModalDialog_AddInstrument: showModalDialog_AddInstrument,
showModalDialog_AddHolding: showModalDialog_AddHolding,
showModalDialog_FullScreenChart: showModalDialog_FullScreenChart,
refreshHistoryChart: refreshHistoryChart,
showModalDialog_SoldHolding: showModalDialog_SoldHolding
}

View File

@ -20,6 +20,8 @@
<IISExpressAnonymousAuthentication />
<IISExpressWindowsAuthentication />
<IISExpressUseClassicPipelineMode />
<Use64BitIISExpress />
<UseGlobalApplicationHostFile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -259,6 +261,7 @@
<Content Include="scripts\js.cookie-2.2.1.min.js" />
<Content Include="scripts\SharePrices.js" />
<Content Include="scripts\SharePrices_Charts.js" />
<Content Include="scripts\SharePrices_Holdings.js" />
<Content Include="scripts\tablesorter\dragtable.mod.css" />
<Content Include="scripts\tablesorter\filter.formatter.css" />
<Content Include="scripts\tablesorter\highlights.css" />

View File

@ -1,7 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<NameOfLastUsedPublishProfile>WinSrv1</NameOfLastUsedPublishProfile>
<LastActiveSolutionConfig>Debug|Any CPU</LastActiveSolutionConfig>
<UseIISExpress>false</UseIISExpress>
<Use64BitIISExpress />
<IISExpressSSLPort />
<IISExpressAnonymousAuthentication />
<IISExpressWindowsAuthentication />
<IISExpressUseClassicPipelineMode />
<UseGlobalApplicationHostFile />
</PropertyGroup>
<ProjectExtensions>
<VisualStudio>

View File

@ -1,21 +1,35 @@
// @ts-check
import {generateAxisBreaks, getProfitOrLoss, timespans} from "./SharePrices_Common.js";
import {formatAmount, generateAxisBreaks, getProfitOrLoss, timespans} from "./SharePrices_Common.js";
import {createLongChart, createMidChart, createShortChart, createSingleDayChart, createChart_Flot,
createChart, createFullScreenChart, createFullScreenChart_Flot, createFullScreenComparisonChart,
createFullScreenComparisonChart_Flot} from "./SharePrices_Charts.js";
createFullScreenComparisonChart_Flot
} from "./SharePrices_Charts.js";
import {createHoldingsTable} from "./SharePrices_Holdings.js";
'use strict';
var vars = {
fetchTiming: {
fetchIntervalDaily: 12 * timespans.oneHour,
fetchIntervalIntraday: 4 * timespans.oneHour,
fetchIntervalSingleDay: 2 * timespans.oneMinute,
lastSuccessfulFetch: new Date(0),
lastFetchStartTime: new Date(0),
lastFetchDuration: 0,
fetchTimer: 0,
displayTimings: false
},
lastTotalHoldingsValue: 0, //The last total holdings value successfully set in the database
initialPageTitle: '',
previousDay: 0,
previousClose: 0,
previousWeek: 0,
previousWeekClose:0
}
//var timespans = { oneSecond: 1000, oneMinute: 60 * 1000, oneHour: 60 * 60 * 1000, oneDay: 24 * 60 * 60 * 1000, oneWeek: 7 * 24 * 60 * 60 * 1000, oneMonth: 31 * 24 * 60 * 60 * 1000 };
var fetchIntervalDaily = 12 * timespans.oneHour;
var fetchIntervalIntraday = 4 * timespans.oneHour;
var fetchIntervalSingleDay = 2 * timespans.oneMinute;
var initialPageTitle;
var fetchTimer = 0;
var lastSuccessfulFetch;
var lastTotalHoldingsValue = 0; //The last total holdings value successfully set in the database
//var lastSuccessfulFetch;
var currencyFormatter = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' });
var instrumentSearchResults = [];
var tablesUpdateTimings = { lastUpdate: 0, timeBetweenUpdates: 10000, timer: 0, updateNeeded: true };
@ -90,13 +104,13 @@ var Instruments = {
},
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) };
if (!this.Data[0].lastFetchedDaily) { this.Data[0].lastFetchedDaily = new Date().getTime() - vars.fetchTiming.fetchIntervalDaily + (4 * timespans.oneMinute) };
let lastFetchedDate = this.Data[0].lastFetchedDaily;
let lastFetchedIndex = 0;
for (let i = 1; i < this.Data.length; i++) {
if (this.Data[i].Symbol != 'GBP' && this.Data[i].Symbol != 'GBp') {
//if (!this.Data[i].lastFetchedDaily) { this.Data[i].lastFetchedDaily = this.Data[i].MaxDailyDate };
if (!this.Data[i].lastFetchedDaily) { this.Data[i].lastFetchedDaily = new Date().getTime() - fetchIntervalDaily + (4 * timespans.oneMinute) };
if (!this.Data[i].lastFetchedDaily) { this.Data[i].lastFetchedDaily = new Date().getTime() - vars.fetchTiming.fetchIntervalDaily + (4 * timespans.oneMinute) };
if (lastFetchedDate > this.Data[i].lastFetchedDaily) {
lastFetchedDate = this.Data[i].lastFetchedDaily;
lastFetchedIndex = i;
@ -108,13 +122,13 @@ var Instruments = {
},
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) };
if (!this.Data[0].lastFetchedIntraday) { this.Data[0].lastFetchedIntraday = new Date().getTime() - vars.fetchTiming.fetchIntervalIntraday + (2 * timespans.oneMinute) };
let lastFetchedDate = this.Data[0].lastFetchedIntraday;
let lastFetchedIndex = 0;
for (let i = 1; i < this.Data.length; i++) {
if (this.Data[i].Symbol != 'GBP' && this.Data[i].Symbol != 'GBp') {
//if (!this.Data[i].lastFetchedIntraday) { this.Data[i].lastFetchedIntraday = this.Data[i].MaxIntradayDate };
if (!this.Data[i].lastFetchedIntraday) { this.Data[i].lastFetchedIntraday = new Date().getTime() - fetchIntervalIntraday + (2 * timespans.oneMinute) };
if (!this.Data[i].lastFetchedIntraday) { this.Data[i].lastFetchedIntraday = new Date().getTime() - vars.fetchTiming.fetchIntervalIntraday + (2 * timespans.oneMinute) };
if (lastFetchedDate > this.Data[i].lastFetchedIntraday) {
lastFetchedDate = this.Data[i].lastFetchedIntraday;
lastFetchedIndex = i;
@ -222,7 +236,6 @@ var Currencies = {
}
}
}
var previousDay, previousClose, previousWeek, previousWeekClose;
Date.prototype.yyyymmddhhmmss = function () {
var yyyy = this.getFullYear().toString();
@ -285,78 +298,6 @@ Number.prototype.autoScale = function () {
}
};
function logInfo(msg) {
console.info(msg);
if (typeof msg == 'object') {
$("#logDiv").css("font-color", "white").text(JSON.stringify(msg));
} else {
$("#logDiv").css("font-color", "white").text(msg);
}
}
function logWarning(msg) {
console.warn(msg);
if (typeof msg == 'object') {
$("#logDiv").css("font-color", "orange").text(JSON.stringify(msg));
} else {
$("#logDiv").css("font-color", "orange").text(msg);
}
}
function logError(msg) {
console.error(msg);
if (typeof msg == 'object') {
$("#logDiv").css("font-color", "red").text(JSON.stringify(msg));
} else {
$("#logDiv").css("font-color", "red").text(msg);
}
}
function formatAmount(amount, currency, decimals) {
if (amount) {
let isNegative = (amount < 0);
let s = Math.abs(amount).toFixed(decimals);
if (Math.abs(amount) < 0.01) {
s = Math.abs(amount).toPrecision(2);
}
let pos = s.indexOf('.');
let wholeNumber = (pos >= 0) ? s.substring(0, pos) : s;
let decimal = (pos >= 0) ? s.substring(pos) : '';
s = '';
while (wholeNumber.length > 0) {
//if (s.length > 0 && wholeNumber!='-') s = ',' + s; //Don't add a comma if the only thing left to add to the string is the minus sign
if (s.length > 0) s = ',' + s;
if (wholeNumber.length >= 3) {
s = wholeNumber.substring(wholeNumber.length - 3) + s;
wholeNumber = wholeNumber.substring(0, wholeNumber.length - 3);
} else {
s = wholeNumber + s;
wholeNumber = '';
}
}
s = s + decimal;
let sign = isNegative ? '-' : '';
switch (currency) {
case 'GBP':
s = sign + '£' + s;
break;
case 'GBp':
s = sign + s + 'p';
break;
case 'USD':
s = sign + 'U$' + s;
break;
case 'AUD':
s = sign + 'A$' + s;
break;
case 'EUR':
s = sign + '€' + s;
break;
default:
s = sign + currency + s;
}
return s;
} else {
return '';
}
}
/*function getProfitOrLoss(BasePrice, CurrentPrice, ShowPercent) {
//let priceMovement = (CurrentPrice - BasePrice).toFixed(2);
let priceMovement = (CurrentPrice - BasePrice).autoDP();
@ -364,35 +305,12 @@ function formatAmount(amount, currency, decimals) {
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;
}
export function getInstruments() {
try {
//logInfo('getInstruments()');
initialPageTitle = document.title;
if (fetchTimer != 0) { clearTimeout(fetchTimer) };
vars.initialPageTitle = document.title;
if (vars.fetchTiming.fetchTimer != 0) { clearTimeout(vars.fetchTiming.fetchTimer) };
indexMarquee.init($('#divIndexMarquee'), '&emsp;');
$.ajax({
type: "POST",
@ -436,7 +354,7 @@ export function getInstruments() {
}
};
function createSharesTable() {
export function createSharesTable() {
try {
let excludeNoHoldings = $('#excludeNoHoldings').prop('checked');
//logInfo('createSharesTable()');
@ -649,27 +567,45 @@ function getNewRemoteData() {
//logInfo(new Date().yyyymmddhhmmss() + ": getNewRemoteData()")
let nothingToFetch = false;
let lastFetchedDailyInstrument = Instruments.GetLastFetchedDaily();
if (lastFetchedDailyInstrument.lastFetchedDaily < (new Date().getTime() - fetchIntervalDaily)) {
if (lastFetchedDailyInstrument.lastFetchedDaily < (new Date().getTime() - vars.fetchTiming.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)) {
if (lastFetchedIntradayInstrument.lastFetchedIntraday < (new Date().getTime() - vars.fetchTiming.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)) {
if (lastFetchedSingleDayInstrument.lastFetchedSingleDay < (new Date().getTime() - vars.fetchTiming.fetchIntervalSingleDay)) {
//logInfo(new Date().yyyymmddhhmmss() + ": Last fetched single day: " + lastFetchedSingleDayInstrument.Symbol + ", Last fetched date: " + new Date(lastFetchedSingleDayInstrument.lastFetchedSingleDay).yyyymmddhhmmss());
getYahooSingleDayData(lastFetchedSingleDayInstrument);
} //else {
} else {
//fetchInterval = 3000;
//fetchTimer = setTimeout(getNewRemoteData, fetchInterval);
//}
nothingToFetch = true;
}
}
}
var nextFetchDelay = 5000;
if (!nothingToFetch) {
nextFetchDelay = vars.fetchTiming.lastFetchDuration + 100;
}
if (vars.fetchTiming.displayTimings) {
console.info({
msg: 'Fetch timing info',
currentTime: (new Date()).yyyymmddhhmmss(),
nothingToFetch: nothingToFetch,
lastFetchDuration: vars.fetchTiming.lastFetchDuration,
nextFetchDelay: nextFetchDelay
});
}
vars.fetchTiming.fetchTimer = setTimeout(getNewRemoteData, nextFetchDelay);
} catch (err) {
vars.fetchTiming.fetchTimer = setTimeout(getNewRemoteData, 15000);
logError({ MSG: "Error in getNewRemoteData", err: err });
}
}
@ -733,17 +669,21 @@ function getAllLocalIntradayData() {
}
}
//fetchTimer = setTimeout(getNewRemoteData, 500);
//fetchTimer = setInterval(getNewRemoteData, 1250);
fetchTimer = setInterval(getNewRemoteData, 1000);
//vars.fetchTiming.fetchTimer = setInterval(getNewRemoteData, 1000);
vars.fetchTiming.fetchTimer = setTimeout(getNewRemoteData, 200);
},
failure: function (response) { console.error("getYTDData error:"); console.info(response); }
failure: function (response) {
vars.fetchTiming.fetchTimer = setTimeout(getNewRemoteData, 5000);
console.error("getYTDData error:");
console.info(response);
}
});
} catch (err) {
logError({ MSG: "Error in getAllLocalIntradayData", err: err });
}
}
function redrawAllSharesCharts() {
export function redrawAllSharesCharts() {
let excludeNoHoldings = $('#excludeNoHoldings').prop('checked');
for (let n = 0; n < Instruments.Data.length; n++) {
let i = Instruments.Data[n];
@ -770,6 +710,7 @@ function fixDecimals(val, place) {
function getYahooDailyData(Instrument) {
//console.info('getYahooDailyData: ' + Instrument.Symbol);
Instrument.lastFetchedDaily = new Date().getTime();
vars.fetchTiming.lastFetchStartTime = new Date().getTime();
$.ajax({
type: "POST",
contentType: "application/json",
@ -777,6 +718,7 @@ function getYahooDailyData(Instrument) {
dataType: "json",
data: JSON.stringify({ Symbol: Instrument.Symbol, MinDailyDT: (Instrument.DailyData.length ? Instrument.DailyData[0].DT: new Date().getTime()) }),
success: function (response) {
vars.fetchTiming.lastFetchDuration = (new Date().getTime()) - vars.fetchTiming.lastFetchStartTime;
let newData = [];
if (response.chart && response.chart.result) {
let dataSeries = response.chart.result[0];
@ -817,7 +759,8 @@ function getYahooDailyData(Instrument) {
//console.info("getYahooDailyData: " + Instrument.Symbol + ', Last data point: ' + d);
if (newData.length > 0) {
lastSuccessfulFetch = new Date();
vars.fetchTiming.lastSuccessfulFetch = new Date();
vars.fetchTiming.lastSuccessfulFetchDuration = (new Date()) - vars.fetchTiming.lastSuccessfulFetchStartTime;
}
submitNewDailyData(Instrument, newData);
@ -828,13 +771,18 @@ function getYahooDailyData(Instrument) {
logWarning({ MSG: "No data received for symbol: " + Instrument.Symbol, response: response })
}
},
failure: function (response) { console.error("getYahooDailyData error:"); console.info(response); }
failure: function (response) {
vars.fetchTiming.lastFetchDuration = (new Date().getTime()) - vars.fetchTiming.lastFetchStartTime;
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();
vars.fetchTiming.lastFetchStartTime = new Date().getTime();
$.ajax({
type: "POST",
contentType: "application/json",
@ -842,6 +790,7 @@ function getYahooIntradayData(Instrument) {
dataType: "json",
data: JSON.stringify({ Symbol: Instrument.Symbol, MinIntradayDT: minIntradayDT }),
success: function (response) {
vars.fetchTiming.lastFetchDuration = (new Date().getTime()) - vars.fetchTiming.lastFetchStartTime;
let newData = [];
if (response.chart && response.chart.result) {
let dataSeries = response.chart.result[0];
@ -874,7 +823,7 @@ function getYahooIntradayData(Instrument) {
//console.warn("Didn't submit " + nonSubmittedEntries.toString() + " entries (" + newData.length.toString() + " OK) with null values for " + Instrument.Symbol);
}
if (newData.length) {
lastSuccessfulFetch = new Date();
vars.fetchTiming.lastSuccessfulFetch = new Date();
submitNewIntradayData(Instrument, newData);
createMidChart(Instrument);
createShortChart(Instrument);
@ -889,12 +838,17 @@ function getYahooIntradayData(Instrument) {
logWarning({ MSG: "No data received for symbol: " + Instrument.Symbol, response: response });
}
},
failure: function (response) { console.error("getYahooIntradayData error:"); console.info(response); }
failure: function (response) {
vars.fetchTiming.lastFetchDuration = (new Date().getTime()) - vars.fetchTiming.lastFetchStartTime;
console.error("getYahooIntradayData error:");
console.info(response);
}
});
}
function getYahooSingleDayData(Instrument) {
//console.info('getYahooSingleDayData: ' + Instrument.Symbol);
Instrument.lastFetchedSingleDay = new Date().getTime();
vars.fetchTiming.lastFetchStartTime = new Date().getTime();
$.ajax({
type: "POST",
contentType: "application/json",
@ -902,6 +856,7 @@ function getYahooSingleDayData(Instrument) {
dataType: "json",
data: JSON.stringify({ Symbol: Instrument.Symbol }),
success: function (response) {
vars.fetchTiming.lastFetchDuration = (new Date().getTime()) - vars.fetchTiming.lastFetchStartTime;
let newData = [];
if (response.chart && response.chart.result) {
let dataSeries = response.chart.result[0];
@ -930,7 +885,7 @@ function getYahooSingleDayData(Instrument) {
//console.warn("Ignored " + ignoredEntries.toString() + " entries (" + newData.length.toString() + " OK) with null values for " + Instrument.Symbol);
}
if (newData.length > 0) {
lastSuccessfulFetch = new Date();
vars.fetchTiming.lastSuccessfulFetch = new Date();
}
if (!Instrument.SingleDayData || Instrument.SingleDayData[0].DT != newData[0].DT || newData.length > Instrument.SingleDayData.length) {
@ -960,7 +915,11 @@ function getYahooSingleDayData(Instrument) {
logWarning({ MSG: "No data received for symbol: " + Instrument.Symbol, response: response });
}
},
failure: function (response) { console.error("getYahooSingleDayData error:"); console.info(response); }
failure: function (response) {
vars.fetchTiming.lastFetchDuration = (new Date().getTime()) - vars.fetchTiming.lastFetchStartTime;
console.error("getYahooSingleDayData error:");
console.info(response);
}
});
}
function getLseSingleDayData(Instrument) {
@ -968,6 +927,7 @@ function getLseSingleDayData(Instrument) {
Instrument.lastFetchedSingleDay = new Date().getTime();
let fromDate = new Date().setHours(0, 0, 0);
let toDate = new Date().getTime();
vars.fetchTiming.lastFetchStartTime = new Date().getTime();
$.ajax({
type: "POST",
contentType: "application/json",
@ -975,10 +935,15 @@ function getLseSingleDayData(Instrument) {
dataType: "json",
data: JSON.stringify({ Symbol: Instrument.Symbol, FromDate: fromDate, ToDate: toDate }),
success: function (response) {
vars.fetchTiming.lastFetchDuration = (new Date().getTime()) - vars.fetchTiming.lastFetchStartTime;
console.info("getLseSingleDayData: " + JSON.stringify(response));
},
failure: function (response) { console.error("getLseSingleDayData error:"); console.info(response); }
failure: function (response) {
vars.fetchTiming.lastFetchDuration = (new Date().getTime()) - vars.fetchTiming.lastFetchStartTime;
console.error("getLseSingleDayData error:");
console.info(response);
}
});
}
@ -1088,7 +1053,7 @@ function updateTables() {
if (tablesUpdateTimings.updateNeeded && tablesUpdateTimings.lastUpdate + tablesUpdateTimings.timeBetweenUpdates < new Date().getTime()) {
tablesUpdateTimings.lastUpdate = new Date().getTime();
createHoldingsTable();
createHoldingsTable(Instruments, vars);
createAccountsTables();
createAnalysisTable();
createIndexMarquee();
@ -1096,570 +1061,6 @@ function updateTables() {
}
}
function createHoldingsTable() {
function getHoldings() {
function aggregateHoldings(i) {
let aggUnits = 0;
let aggBookCost = 0;
let aggBookCostGBP = 0;
let minPurchaseData = new Date().getTime();
let maxPurchaseDate = 0;
let marketIsOpen = Instruments.MarketIsOpen(i);
let exchangeRate = Instruments.GetExchangeRate(i.Currency, 'GBP');
let accountNames = [];
let aggSingleDayGainGBP = 0;
for (let hn = 0; hn < i.Holdings.length; hn++) {
let h = i.Holdings[hn];
if (h.SoldDate == null && h.NoUnits != 0) {
aggUnits += h.NoUnits;
aggBookCost += h.NoUnits * h.PurchasePricePerUnit;
//aggBookCostGBP += h.NoUnits * h.PurchasePricePerUnit / exchangeRate;
aggBookCostGBP += h.BookCostGBP;
//bookCostPerAccount.addCategoryAmount(h.AccountName, (h.NoUnits * h.PurchasePricePerUnit) / exchangeRate);
bookCostPerAccount.addCategoryAmount(h.AccountName, h.BookCostGBP);
if (h.PurchaseDate < minPurchaseData) { minPurchaseData = h.PurchaseDate; }
if (h.PurchaseDate > maxPurchaseDate) { maxPurchaseDate = h.PurchaseDate; }
if (!accountNames.includes(h.AccountName)) { accountNames.push(h.AccountName); }
if (i.CurrentPrice && exchangeRate) {
aggSingleDayGainGBP += ((i.CurrentPrice - (h.PurchaseDate > i.SingleDayStartDate ? h.PurchasePricePerUnit : i.SingleDayPreviousClose)) * h.NoUnits) / exchangeRate;
currentValuePerAccount.addCategoryAmount(h.AccountName, (h.NoUnits * i.CurrentPrice) / exchangeRate);
}
}
}
let aggPurchasePricePerUnit = aggBookCost / aggUnits;
let aggCurrentValue = 0,
aggCurrentValueGBP = 0,
aggGainGBP = 0,
aggSingleDayGainPercent = 0,
aggSingleDayProfitOrLoss = 0;
if (i.CurrentPrice) {
aggCurrentValue = aggUnits * (i.Currency == 'GBp' ? i.CurrentPrice / 100 : i.CurrentPrice);
if (exchangeRate) {
aggCurrentValueGBP = (aggUnits * i.CurrentPrice) / exchangeRate;
//aggGainGBP = ((i.CurrentPrice - aggPurchasePricePerUnit) * aggUnits) / exchangeRate;
aggGainGBP = aggCurrentValueGBP - aggBookCostGBP;
aggSingleDayGainPercent = ((i.CurrentPrice / i.SingleDayPreviousClose) - 1) * 100;
aggSingleDayProfitOrLoss = aggSingleDayGainGBP < 0 ? 'loss' : 'profit';
}
}
return {
displayOrder: i.DisplayOrder,
holdingNumber: 0,
instrumentType: i.InstrumentType,
instrumentName: i.InstrumentName,
displayName: i.DisplayName,
symbol: i.Symbol,
currency: i.Currency,
accountName: accountNames.length > 1 ? 'Multiple (' + accountNames.length + ')' : accountNames[0],
accountNames: accountNames.length > 1 ? accountNames.join(', ') : '',
purchaseDateMin: minPurchaseData,
purchaseDateMax: maxPurchaseDate,
noUnits: aggUnits,
purchasePricePerUnit: aggPurchasePricePerUnit,
bookCost: aggBookCost,
bookCostGBP: aggBookCostGBP,
currentValue: aggCurrentValue,
gain: (i.Currency == 'GBp' ? (i.CurrentPrice - aggPurchasePricePerUnit) / 100 : (i.CurrentPrice - aggPurchasePricePerUnit)) * aggUnits,
//gainPercent: (i.CurrentPrice - aggPurchasePricePerUnit) / aggPurchasePricePerUnit * 100,
gainPercent: aggGainGBP / aggBookCostGBP * 100,
currentValueGBP: aggCurrentValueGBP,
gainGBP: aggGainGBP,
singleDayGainGBP: aggSingleDayGainGBP,
singleDayGainPercent: aggSingleDayGainPercent,
singleDayProfitOrLoss: aggSingleDayProfitOrLoss,
currentPrice: i.CurrentPrice,
currentExchangeRate: exchangeRate,
marketIsOpen: marketIsOpen
};
}
//Create a structure to summarise total holdings in each currency, to be used in the pie chart
let holdingCurrencies = {
data: [],
indexOfHoldingCurrency: function (symbol) { return this.data.map(function (item) { return item.symbol; }).indexOf(symbol); },
getHoldingCurrency: function (symbol) { return this.data[this.indexOfHoldingCurrency(symbol)]; },
addHoldingCurrency: function (symbol, amount) {
let c = this.getHoldingCurrency(symbol);
if (c) {
c.total += amount;
} else {
this.data.push({ symbol: symbol, total: amount });
}
},
getChartData: function () {
let d = []
for (let i = 0; i < this.data.length; i++) {
//d.push({ label: this.data[i].symbol, data: this.data[i].total / Instruments.GetExchangeRate(this.data[i].symbol, 'GBP') });
d.push({ label: this.data[i].symbol, data: this.data[i].total });
}
return d;
}
};
let holdingCurrenciesCurrentValue = {
data: [],
indexOfHoldingCurrency: function (symbol) { return this.data.map(function (item) { return item.symbol; }).indexOf(symbol); },
getHoldingCurrency: function (symbol) { return this.data[this.indexOfHoldingCurrency(symbol)]; },
addHoldingCurrency: function (symbol, amount) {
let c = this.getHoldingCurrency(symbol);
if (c) {
c.total += amount;
} else {
this.data.push({ symbol: symbol, total: amount });
}
},
getChartData: function () {
let d = []
for (let i = 0; i < this.data.length; i++) {
d.push({ label: this.data[i].symbol, data: this.data[i].total / Instruments.GetExchangeRate(this.data[i].symbol, 'GBP') });
}
return d;
}
};
let bookCostPerAccount = createCategoryAggregator();
let currentValuePerAccount = createCategoryAggregator();
let totalHoldingsValueGBP = 0;
let totalBookCostGBP = 0;
let totalValueGBP = 0;
let totalGainGBP = 0;
let todaysGainGBP = 0;
let maxCurrentValueGBP = 0;
let groupHoldings = $('#groupBySymbol').prop('checked');
let r = [];
let w = []; //Watchlist (instruments with no current holdings)
for (let n = 0; n < Instruments.Data.length; n++) {
let i = Instruments.Data[n];
//if (i.InstrumentType == 'EQUITY') {
if (i.InstrumentType == 'EQUITY' || i.InstrumentType == 'ETF' || i.InstrumentType == 'CRYPTOCURRENCY' || i.InstrumentType == 'CURRENCY') {
//if (i.CurrentPrice) {
if (groupHoldings) {
let agg = aggregateHoldings(i);
if (agg.noUnits > 0) {
r.push(agg);
//holdingCurrencies.addHoldingCurrency(i.Currency, agg.noUnits * agg.purchasePricePerUnit);
//holdingCurrencies.addHoldingCurrency(i.Currency.toUpperCase(), agg.noUnits * agg.purchasePricePerUnit / (i.Currency == "GBp" ? 100 : 1));
holdingCurrencies.addHoldingCurrency(i.Currency.toUpperCase(), agg.bookCostGBP);
if (i.CurrentPrice) {
if (agg.currentValueGBP > maxCurrentValueGBP) { maxCurrentValueGBP = agg.currentValueGBP };
//holdingCurrenciesCurrentValue.addHoldingCurrency(i.Currency, agg.noUnits * i.CurrentPrice);
holdingCurrenciesCurrentValue.addHoldingCurrency(i.Currency.toUpperCase(), agg.noUnits * i.CurrentPrice / (i.Currency == "GBp" ? 100 : 1));
if (agg.currentExchangeRate) {
totalBookCostGBP += agg.bookCostGBP;
totalValueGBP += agg.currentValueGBP;
totalGainGBP += agg.gainGBP;
todaysGainGBP += (agg.marketIsOpen == 0 ? 0 : agg.singleDayGainGBP);
}
}
} else {
//No current holdings for this instrument - add it to the watchlist
if (i.WatchlistID) {
w.push(i);
}
}
} else {
let marketIsOpen = Instruments.MarketIsOpen(i);
let exchangeRate = Instruments.GetExchangeRate(i.Currency, 'GBP');
let noCurrentHoldings = 0;
for (let hn = 0; hn < i.Holdings.length; hn++) {
let h = i.Holdings[hn];
if (h.SoldDate == null) {
noCurrentHoldings++;
let holdingBookCost = h.NoUnits * h.PurchasePricePerUnit;
let currentHoldingValue = h.NoUnits * (i.Currency == 'GBp' ? i.CurrentPrice / 100 : i.CurrentPrice);
let holdingCurrentValueGBP = 0,
holdingGainGBP = 0,
singleDayGainGBP = 0,
singleDayGainPercent = 0,
singleDayProfitOrLoss = 0;
if (i.CurrentPrice) {
let currentHoldingValue = h.NoUnits * (i.Currency == 'GBp' ? i.CurrentPrice / 100 : i.CurrentPrice);
if (exchangeRate) {
let holdingCurrentValueGBP = (h.NoUnits * i.CurrentPrice) / exchangeRate;
//let holdingGainGBP = ((i.CurrentPrice - h.PurchasePricePerUnit) * h.NoUnits) / exchangeRate;
let holdingGainGBP = holdingCurrentValueGBP - h.BookCostGBP;
let singleDayGainGBP = ((i.CurrentPrice - (h.PurchaseDate > i.SingleDayStartDate ? h.PurchasePricePerUnit : i.SingleDayPreviousClose)) * h.NoUnits) / exchangeRate;
let singleDayGainPercent = ((i.CurrentPrice / i.SingleDayPreviousClose) - 1) * 100;
let singleDayProfitOrLoss = singleDayGainGBP < 0 ? 'loss' : 'profit';
if (holdingCurrentValueGBP > maxCurrentValueGBP) { maxCurrentValueGBP = holdingCurrentValueGBP };
}
}
r.push({
displayOrder: i.DisplayOrder,
holdingNumber: hn,
instrumentType: i.InstrumentType,
instrumentName: i.InstrumentName,
displayName: i.DisplayName,
symbol: i.Symbol,
currency: i.Currency,
accountName: h.AccountName,
purchaseDateMin: h.PurchaseDate,
purchaseDateMax: h.PurchaseDate,
noUnits: h.NoUnits,
purchasePricePerUnit: h.PurchasePricePerUnit,
bookCost: holdingBookCost,
currentValue: currentHoldingValue,
gain: (i.Currency == 'GBp' ? (i.CurrentPrice - h.PurchasePricePerUnit) / 100 : (i.CurrentPrice - h.PurchasePricePerUnit)) * h.NoUnits,
//gainPercent: (i.CurrentPrice - h.PurchasePricePerUnit) / h.PurchasePricePerUnit * 100,
gainPercent: holdingGainGBP / h.BookCostGBP * 100,
currentValueGBP: holdingCurrentValueGBP,
gainGBP: holdingGainGBP,
singleDayGainGBP: singleDayGainGBP,
singleDayGainPercent: singleDayGainPercent,
singleDayProfitOrLoss: singleDayProfitOrLoss,
currentPrice: i.CurrentPrice,
currentExchangeRate: exchangeRate,
marketIsOpen: marketIsOpen
});
holdingCurrencies.addHoldingCurrency(i.Currency, h.NoUnits * h.PurchasePricePerUnit);
if (i.CurrentPrice) {
holdingCurrenciesCurrentValue.addHoldingCurrency(i.Currency, h.NoUnits * i.CurrentPrice);
if (exchangeRate) {
currentValuePerAccount.addCategoryAmount(h.AccountName, holdingCurrentValueGBP);
bookCostPerAccount.addCategoryAmount(h.AccountName, holdingBookCost / exchangeRate);
//totalBookCostGBP += holdingBookCost / exchangeRate;
totalBookCostGBP += h.BookCostGBP;
totalValueGBP += holdingCurrentValueGBP;
totalGainGBP += holdingGainGBP;
todaysGainGBP += (marketIsOpen == 0 ? 0 : singleDayGainGBP);
}
}
}
if (noCurrentHoldings == 0) {
//No current holdings for this instrument - add it to the watchlist
if (i.WatchlistID) {
w.push(i);
}
}
}
}
//} //if (i.CurrentPrice)
}
}
return {
totalBookCostGBP: totalBookCostGBP,
totalValueGBP: totalValueGBP,
totalGainGBP: totalGainGBP,
todaysGainGBP: todaysGainGBP,
maxCurrentValueGBP: maxCurrentValueGBP,
holdings: r,
watchlist: w,
holdingCurrencies: holdingCurrencies,
holdingCurrenciesCurrentValue: holdingCurrenciesCurrentValue,
bookCostPerAccount: bookCostPerAccount,
currentValuePerAccount: currentValuePerAccount
};
}
function createTable() {
let t = $("#tblMainHoldings");
if (t.length) {
return;
} else {
let tbl = '<table id="tblMainHoldings" class="mainHoldings"><thead>' +
'<tr><th>Type</th>' +
'<th>Name</th>' +
'<th>Symbol</th>' +
'<th>Ccy</th>' +
'<th>Account</th>' +
'<th>Buy Date</th>' +
'<th>No Units</th>' +
'<th>Buy<br>Price</th>' +
'<th>Current<br>Price</th>' +
'<th>Book<br>Cost</th>' +
'<th>Current<br>Value</th>' +
'<th>Gain</th>' +
'<th>Gain £</th>' +
'<th>Gain %</th>' +
'<th>Current<br>Value £</th>' +
'<th>Allocation</th>' +
'<th>Today&apos;s<br>Gain £</th>' +
'<th>Today&apos;s<br>Gain %</th>' +
'</tr></thead><tbody id="tbMainHoldings"></tbody><tfoot id="tfMainHoldings"><tr><td colspan="18">&nbsp;</td></tr></tfoot></table>';
//$('#holdingsDiv').html(tbl);
$('#holdingsTableDiv').html(tbl);
$("#tblMainHoldings").tablesorter({ ignoreCase: false });
$("#tblMainHoldings").trigger("sorton", Cookies.get('sortHoldings'));
$('#tblMainHoldings').on('sortEnd', function (event) {
// Prints the current sort order to the console
if (event.target.config.sortList.length > 3) {
console.info({ "Setting sortHoldings Cookie (array too long?)": event.target.config.sortList });
}
Cookies.set('sortHoldings', event.target.config.sortList, { 'sameSite': 'strict' });
});
}
}
function createWatchlistTable() {
let t = $("#tblWatchlist");
if (t.length) {
return;
} else {
let tbl = '<table id="tblWatchlist" class="watchlist"><thead>' +
'<tr><th>Type</th>' +
'<th>Name</th>' +
'<th>Symbol</th>' +
'<th>Ccy</th>' +
'<th>Current Price</th>' +
'<th>Today&apos;s Gain</th>' +
'<th>Today&apos;s Gain %</th>' +
'<th>Since Last Sold<th>' +
'</tr></thead><tbody id="tbWatchlist"></tbody></table>';
$('#watchlistTableDiv').html(tbl);
$("#tblWatchlist").tablesorter({ ignoreCase: false });
$("#tblWatchlist").trigger("sorton", Cookies.get('sortWatchlist'));
$('#tblWatchlist').on('sortEnd', function (event) {
// Prints the current sort order to the console
Cookies.set('sorWatchlist', event.target.config.sortList, { 'sameSite': 'strict' });
});
}
}
function addTableRow(rowID, content, tbodyID, highlightUpdates) {
let r = $("#" + rowID);
let newText = $(content).text(); //$(content).text();
if (r.length) {
let currentContent = r.text();
if (currentContent != newText) { //Only update row if the content has changed
$("#" + rowID).replaceWith(content);
if (highlightUpdates) {
$("#" + rowID).addClass("highlighted");
}
}
} else {
$("#" + tbodyID).append(content); //.addClass("highlighted");
//$("#" + rowID).addClass("highlighted");
}
}
function getPercentCell(backgroundColor, cellWidth, valuePercent, label) {
let labelText = label ? label : (valuePercent.toFixed(1) + '%');
let barWidth = ((valuePercent / 100) * cellWidth);
return '<td>' +
'<div class="pcBackground" style="background-color: ' + backgroundColor + '; width:' + barWidth.toFixed(0) + 'px;">&nbsp;</div>' +
'<div class="pcLabel" style="width:' + cellWidth + 'px">' + labelText + '</div>' +
'</td>';
}
function profitOrLoss(value) {
return value >= 0 ? "profit" : "loss";
}
let t = getHoldings();
let totalBookCostGBP = t.totalBookCostGBP;
let totalValueGBP = t.totalValueGBP;
let totalGainGBP = t.totalGainGBP;
let todaysGainGBP = t.todaysGainGBP;
let maxCurrentValueGBP = t.maxCurrentValueGBP;
let holdings = t.holdings;
let watchlist = t.watchlist;
let holdingCurrencies = t.holdingCurrencies;
let holdingCurrenciesCurrentValue = t.holdingCurrenciesCurrentValue;
let bookCostPerAccount = t.bookCostPerAccount;
let currentValuePerAccount = t.currentValuePerAccount;
let maxAllocationPercent = (maxCurrentValueGBP / totalValueGBP) * 100;
let allocationScaleFactor = 100 / maxAllocationPercent;
let altRow = 1;
createTable();
for (let n = 0; n < holdings.length; n++) {
try {
altRow = !altRow;
let h = holdings[n];
let type = h.instrumentType == 'CURRENCY' ? '$' : h.instrumentType.substring(0, 1);
//let profitOrLoss = h.currentPrice < h.purchasePricePerUnit ? 'loss' : 'profit'; //let profitOrLoss = i.CurrentPrice < h.PurchasePricePerUnit ? 'loss' : 'profit';
let openOrClosed = h.marketIsOpen == 1 ? 'open' : 'closed';
let allocationPercent = h.currentValueGBP / totalValueGBP * 100;
let scaledAllocation = allocationScaleFactor * allocationPercent;
let rowID = "holdingRow_" + h.displayOrder + "_" + h.holdingNumber;
let row = '<tr' + (altRow ? ' class="altShareRow"' : '') + ' id="' + rowID + '">' +
'<td class="' + openOrClosed + '">' + type + '</td>' +
'<td class="' + openOrClosed + '">' + (h.displayName != '' ? h.displayName : h.instrumentName) + '</td>' +
'<td><a target="_blank" href="https://uk.finance.yahoo.com/quote/' + h.symbol + '">' + h.symbol + '</a>' + '</td>' +
'<td class="' + openOrClosed + '">' + h.currency + '</td>' +
'<td class="' + openOrClosed + '" title="' + (h.accountNames ? h.accountNames : '') + '">' + h.accountName + '</td>' +
'<td class="' + openOrClosed + '"' + (h.purchaseDateMin == h.purchaseDateMax ? ('>' + new Date(h.purchaseDateMin).yyyymmdd()) : (' title="' + new Date(h.purchaseDateMax).yyyymmdd() + ' - ' + new Date(h.purchaseDateMin).yyyymmdd() + '">' + new Date(h.purchaseDateMin).yyyymmdd()) + '+') + '</td>' +
//'<td class="num ' + openOrClosed + '">' + formatAmount(h.noUnits, '', 0) + '</td>' +
'<td class="num ' + openOrClosed + '">' + h.noUnits.autoScale() + '</td>' +
'<td class="num ' + openOrClosed + '">' + formatAmount(h.purchasePricePerUnit, h.currency, 2) + '</td>' + //Buy Price
'<td class="num ' + openOrClosed + '">' + (h.currentPrice ? formatAmount(h.currentPrice, h.currency, 2) : '') + '</td>' + //Current Price
'<td class="num ' + openOrClosed + '">' + formatAmount(h.currency == 'GBp' ? h.bookCost / 100 : h.bookCost, h.currency == 'GBp' ? 'GBP' : h.currency, 0) + '</td>' + //Book Cost
'<td class="num ' + openOrClosed + '">' + (h.currentExchangeRate ? formatAmount(h.currentValue, h.currency == 'GBp' ? 'GBP' : h.currency, 0) : '') + '</td>' + //Current Value
'<td class="num ' + profitOrLoss(h.currentPrice - h.purchasePricePerUnit) + '">' + (h.currentPrice ? formatAmount(h.gain, h.currency == 'GBp' ? 'GBP' : h.currency, 0) : '') + '</td>' + //Gain
'<td class="num ' + profitOrLoss(h.gainGBP) + '">' + (h.currentExchangeRate ? formatAmount(h.gainGBP, 'GBP', 0) : '') + '</td>' + //Gain £
'<td class="num ' + profitOrLoss(h.gainGBP) + '">' + (h.currentExchangeRate ? h.gainPercent.toFixed(1) + '%' : '') + '</td>' + //Gain %
'<td class="num ' + openOrClosed + '">' + (h.currentExchangeRate ? formatAmount(h.currentValueGBP, 'GBP', 0) : '') + '</td>' + //Current Value £
(h.currentExchangeRate ? getPercentCell('#3f2d95', 70, scaledAllocation, allocationPercent.toFixed(1) + '%') : '') + //Allocation
(h.currentExchangeRate ? (h.marketIsOpen == 0 ? '<td/>' : '<td class="num ' + h.singleDayProfitOrLoss + '">' + formatAmount(h.singleDayGainGBP, 'GBP', 0) + '</td>') : '') + //Today's Gain £
(h.singleDayGainPercent ? (h.marketIsOpen == 0 ? '<td/>' : '<td class="num ' + h.singleDayProfitOrLoss + '">' + h.singleDayGainPercent.toFixed(1) + '%</td>') : '<td/>') + //Today's Gain %
'</tr>';
addTableRow(rowID, row, "tbMainHoldings", true);
}
catch (err) {
logError("Error adding " + holdings[n].symbol + " to main holdings table: " + err.message);
}
}
//Add the totals line at the bottom of the table
//let profitOrLoss = totalValueGBP < totalBookCostGBP ? 'loss' : 'profit';
let todaysProfitOrLoss = todaysGainGBP < 0 ? 'loss' : 'profit';
let lSF = '';
if (lastSuccessfulFetch) {
let lastFetchAge = ((new Date()) - lastSuccessfulFetch);
let staleFetch = ((new Date()) - lastSuccessfulFetch) > (timespans.oneMinute * 3);
lSF = (lastSuccessfulFetch ? ' - Last Successful Fetch: ' + (staleFetch ? '<span class="loss">' : '') + lastSuccessfulFetch.yyyymmddhhmmss() + (staleFetch ? '</span>' : '') : '');
}
let row = '<tr id="tfHoldingsTotals"><td></td><td id="holdingsLastUpdated" colspan="8">Updated: ' + new Date().yyyymmddhhmmss() + lSF + '</td>' +
'<td class="num">' + formatAmount(totalBookCostGBP, 'GBP', 0) + '</td>' + //Book Cost
'<td></td><td></td>' +
'<td class="num ' + profitOrLoss(totalValueGBP - totalBookCostGBP) + '">' + formatAmount(totalGainGBP, 'GBP', 0) + '</td>' + //Gain GBP
'<td class="num ' + profitOrLoss(totalValueGBP - totalBookCostGBP) + '">' + formatAmount((((totalValueGBP / totalBookCostGBP) - 1) * 100), '', 1) + '%</td>' + //Gain %
'<td class="num">' + formatAmount(totalValueGBP, 'GBP', 0) + '</td>' + //Current Value
'<td>&nbsp;</td>' + //Allocation
'<td class="num ' + todaysProfitOrLoss + '">' + formatAmount(todaysGainGBP, 'GBP', 0) + '</td>' + //Today's Gain GBP
//'<td class="num ' + todaysProfitOrLoss + '">' + formatAmount(((todaysGainGBP / totalBookCostGBP) * 100), '', 1) + '%</td>' + //Today's Gain %
'<td class="num ' + todaysProfitOrLoss + '">' + formatAmount(((todaysGainGBP / (totalValueGBP-todaysGainGBP)) * 100), '', 1) + '%</td>' + //Today's Gain %
'</tr>';
addTableRow("tfHoldingsTotals", row, "tfMainHoldings", false);
//Create the watchlist table
createWatchlistTable();
altRow = 1;
for (let n = 0; n < watchlist.length; n++) {
altRow = !altRow;
let wi = watchlist[n];
try {
let type = wi.InstrumentType == 'CURRENCY' ? '$' : wi.InstrumentType.substring(0, 1);
let openOrClosed = Instruments.MarketIsOpen(wi) == 1 ? 'open' : 'closed';
let profitOrLoss = wi.CurrentPrice < wi.SingleDayPreviousClose ? 'loss' : 'profit';
let rowID = "watchlistRow_" + wi.DisplayOrder;
let sinceLastSold = '<td></td>';
if (wi.LastSoldPrice) {
let delta = (wi.CurrentPrice - wi.LastSoldPrice) / wi.LastSoldPrice * 100;
let tooltip = new Date(wi.LastSoldDate).yyyymmdd() + ' @ ' + wi.LastSoldPrice.toFixed(2);
sinceLastSold = '<td title="' + tooltip + '" class="num ' + (delta < 0 ? 'loss' : 'profit') + '">' + (delta).toFixed(1) + '%</td>';
}
//Type, Name, Symbol, Ccy, Current Price, Today's Gain, Today's Gain %
let row = '<tr' + (altRow ? ' class="altShareRow"' : '') + ' id="' + rowID + '">' +
'<td class="' + openOrClosed + '">' + type + '</td>' +
'<td class="' + openOrClosed + '">' + wi.InstrumentName + '</td>' +
'<td><a target="_blank" href="https://uk.finance.yahoo.com/quote/' + wi.Symbol + '">' + wi.Symbol + '</a>' + '</td>' +
'<td class="' + openOrClosed + '">' + wi.Currency + '</td>' +
'<td class="num ' + openOrClosed + '">' + (wi.CurrentPrice ? formatAmount(wi.CurrentPrice, wi.Currency, 2) : '') + '</td>' + //Current Price
(Instruments.marketIsOpen == 0 ? '<td/>' : '<td class="num ' + profitOrLoss + '">' + (wi.CurrentPrice ? formatAmount(wi.CurrentPrice - wi.SingleDayPreviousClose, wi.Currency, 2) : '') + '</td>') + //Today's Gain
(Instruments.marketIsOpen == 0 ? '<td/>' : '<td class="num ' + profitOrLoss + '">' + (wi.CurrentPrice ? ((wi.CurrentPrice - wi.SingleDayPreviousClose) / wi.SingleDayPreviousClose * 100).toFixed(1) : '') + '</td>') + //Today's Gain %
//'<td>' + (new Date(wi.LastSoldDate)).yyyymmdd() + '</td>' +
sinceLastSold +
'</tr>';
addTableRow(rowID, row, "tbWatchlist", true);
}
catch (err) {
logError("Error adding " + wi.symbol + " to watchlist table: " + err.message);
}
}
//Flash the last updated cell
$("#holdingsLastUpdated").addClass("highlighted");
//Remove the highlight from all rows/cells
$("#tblMainHoldings").find(".highlighted").removeClass("highlighted", 1200);
$("#tblWatchlist").find(".highlighted").removeClass("highlighted", 1200);
//Tell tablesorter that the table has been updated
var resort = true;
$("#tblMainHoldings").trigger("update", [resort]);
//Update the daily holdings value table in the DB
if (totalValueGBP != lastTotalHoldingsValue) {
$.ajax({
type: "POST",
contentType: "application/json",
url: "SharePrices.aspx/SetTotalHoldingsValue",
dataType: "json",
data: JSON.stringify({ TotalValue: totalValueGBP }),
success: function (response) {
previousDay = response.PreviousDay;
previousClose = response.PreviousClose;
previousWeek = response.PreviousWeekDate;
previousWeekClose = response.PreviousWeekClose;
//Update the total holdings span in the site banner
let formattedAmount = formatAmount(totalValueGBP, "GBP", 0);
let holdingsHTML = '<span style="font-size: small;">Total holdings:&nbsp;</span>' + '<span style="font-size: large">' + formattedAmount + '</span>&nbsp;';
holdingsHTML += '<span style="font-size: small;"><span class="' + (previousClose < totalValueGBP ? 'profit">+' : 'loss">') + formatAmount(totalValueGBP - previousClose, "GBP", 0) + '</span>&nbsp;(d)';
holdingsHTML += '&nbsp;/&nbsp;<span class="' + (previousWeekClose < totalValueGBP ? 'profit">+' : 'loss">') + formatAmount(totalValueGBP - previousWeekClose, "GBP", 0) + '</span>&nbsp;(w)</span>';
$("#spnTotalHoldings").html(holdingsHTML);
//Set the page title
document.title = formattedAmount + ' - ' + initialPageTitle;
lastTotalHoldingsValue = totalValueGBP;
},
failure: function (response) { console.error("SetTotalHoldingsValue error: " + JSON.stringify(response)); }
});
}
//Create / update the currency allocation pie chart
function labelFormatter(label, series) {
return "<div class='pieLabel'>" + label + "<br/>" + Math.round(series.percent) + "%<br>" + formatAmount(series.data[0][1], 'GBP', 0) + "</div>";
}
let ops = {
series: {
downsample: { threshold: 0 },
pie: { show: true, radius: 1, label: { show: true, radius: 0.7, formatter: labelFormatter } }
},
legend: { show: false },
grid: { hoverable: true },
tooltip: {
show: true,
content: "%p.0%, %s, n=%n", // show percentages, rounding to 2 decimal places
shifts: { x: 20, y: 0 },
defaultTheme: false
}
};
let cd = holdingCurrencies.getChartData();
if (cd.length) {
$.plot('#divHoldingCurrenciesChart', cd, ops);
}
cd = holdingCurrenciesCurrentValue.getChartData();
if (cd.length) {
$.plot('#divHoldingCurrenciesChartCurrentValue', cd, ops);
}
function labelFormatter2(label, series) {
return "<div class='pieLabel'>" + label + "<br/>" + formatAmount(series.data[0][1], 'GBP', 0) + "</div>";
}
ops.series.pie.label.formatter = labelFormatter2;
cd = bookCostPerAccount.getChartData();
if (cd.length) {
$.plot('#divAccountBookCost', cd, ops);
}
cd = currentValuePerAccount.getChartData();
if (cd.length) {
$.plot('#divAccountValue', cd, ops);
}
}
function createAccountsTables() {
function getAccounts() {
let accounts = [];

View File

@ -6,6 +6,55 @@
oneWeek: 7 * 24 * 60 * 60 * 1000,
oneMonth: 31 * 24 * 60 * 60 * 1000 };
export function formatAmount(amount, currency, decimals) {
if (amount) {
let isNegative = (amount < 0);
let s = Math.abs(amount).toFixed(decimals);
if (Math.abs(amount) < 0.01) {
s = Math.abs(amount).toPrecision(2);
}
let pos = s.indexOf('.');
let wholeNumber = (pos >= 0) ? s.substring(0, pos) : s;
let decimal = (pos >= 0) ? s.substring(pos) : '';
s = '';
while (wholeNumber.length > 0) {
//if (s.length > 0 && wholeNumber!='-') s = ',' + s; //Don't add a comma if the only thing left to add to the string is the minus sign
if (s.length > 0) s = ',' + s;
if (wholeNumber.length >= 3) {
s = wholeNumber.substring(wholeNumber.length - 3) + s;
wholeNumber = wholeNumber.substring(0, wholeNumber.length - 3);
} else {
s = wholeNumber + s;
wholeNumber = '';
}
}
s = s + decimal;
let sign = isNegative ? '-' : '';
switch (currency) {
case 'GBP':
s = sign + '£' + s;
break;
case 'GBp':
s = sign + s + 'p';
break;
case 'USD':
s = sign + 'U$' + s;
break;
case 'AUD':
s = sign + 'A$' + s;
break;
case 'EUR':
s = sign + '€' + s;
break;
default:
s = sign + currency + s;
}
return s;
} else {
return '';
}
}
export function getProfitOrLoss(BasePrice, CurrentPrice, ShowPercent) {
//let priceMovement = (CurrentPrice - BasePrice).toFixed(2);
let priceMovement = (CurrentPrice - BasePrice).autoDP();

View File

@ -0,0 +1,591 @@
'use strict';
import { formatAmount } from "./SharePrices_Common.js";
export function createHoldingsTable(Instruments, vars) {
function getHoldings() {
function aggregateHoldings(i) {
let aggUnits = 0;
let aggBookCost = 0;
let aggBookCostGBP = 0;
let minPurchaseData = new Date().getTime();
let maxPurchaseDate = 0;
let marketIsOpen = Instruments.MarketIsOpen(i);
let exchangeRate = Instruments.GetExchangeRate(i.Currency, 'GBP');
let accountNames = [];
let aggSingleDayGainGBP = 0;
for (let hn = 0; hn < i.Holdings.length; hn++) {
let h = i.Holdings[hn];
if (h.SoldDate == null && h.NoUnits != 0) {
aggUnits += h.NoUnits;
aggBookCost += h.NoUnits * h.PurchasePricePerUnit;
//aggBookCostGBP += h.NoUnits * h.PurchasePricePerUnit / exchangeRate;
aggBookCostGBP += h.BookCostGBP;
//bookCostPerAccount.addCategoryAmount(h.AccountName, (h.NoUnits * h.PurchasePricePerUnit) / exchangeRate);
bookCostPerAccount.addCategoryAmount(h.AccountName, h.BookCostGBP);
if (h.PurchaseDate < minPurchaseData) { minPurchaseData = h.PurchaseDate; }
if (h.PurchaseDate > maxPurchaseDate) { maxPurchaseDate = h.PurchaseDate; }
if (!accountNames.includes(h.AccountName)) { accountNames.push(h.AccountName); }
if (i.CurrentPrice && exchangeRate) {
aggSingleDayGainGBP += ((i.CurrentPrice - (h.PurchaseDate > i.SingleDayStartDate ? h.PurchasePricePerUnit : i.SingleDayPreviousClose)) * h.NoUnits) / exchangeRate;
currentValuePerAccount.addCategoryAmount(h.AccountName, (h.NoUnits * i.CurrentPrice) / exchangeRate);
}
}
}
let aggPurchasePricePerUnit = aggBookCost / aggUnits;
let aggCurrentValue = 0,
aggCurrentValueGBP = 0,
aggGainGBP = 0,
aggSingleDayGainPercent = 0,
aggSingleDayProfitOrLoss = 0;
if (i.CurrentPrice) {
aggCurrentValue = aggUnits * (i.Currency == 'GBp' ? i.CurrentPrice / 100 : i.CurrentPrice);
if (exchangeRate) {
aggCurrentValueGBP = (aggUnits * i.CurrentPrice) / exchangeRate;
//aggGainGBP = ((i.CurrentPrice - aggPurchasePricePerUnit) * aggUnits) / exchangeRate;
aggGainGBP = aggCurrentValueGBP - aggBookCostGBP;
aggSingleDayGainPercent = ((i.CurrentPrice / i.SingleDayPreviousClose) - 1) * 100;
aggSingleDayProfitOrLoss = aggSingleDayGainGBP < 0 ? 'loss' : 'profit';
}
}
return {
displayOrder: i.DisplayOrder,
holdingNumber: 0,
instrumentType: i.InstrumentType,
instrumentName: i.InstrumentName,
displayName: i.DisplayName,
symbol: i.Symbol,
currency: i.Currency,
accountName: accountNames.length > 1 ? 'Multiple (' + accountNames.length + ')' : accountNames[0],
accountNames: accountNames.length > 1 ? accountNames.join(', ') : '',
purchaseDateMin: minPurchaseData,
purchaseDateMax: maxPurchaseDate,
noUnits: aggUnits,
purchasePricePerUnit: aggPurchasePricePerUnit,
bookCost: aggBookCost,
bookCostGBP: aggBookCostGBP,
currentValue: aggCurrentValue,
gain: (i.Currency == 'GBp' ? (i.CurrentPrice - aggPurchasePricePerUnit) / 100 : (i.CurrentPrice - aggPurchasePricePerUnit)) * aggUnits,
//gainPercent: (i.CurrentPrice - aggPurchasePricePerUnit) / aggPurchasePricePerUnit * 100,
gainPercent: aggGainGBP / aggBookCostGBP * 100,
currentValueGBP: aggCurrentValueGBP,
gainGBP: aggGainGBP,
singleDayGainGBP: aggSingleDayGainGBP,
singleDayGainPercent: aggSingleDayGainPercent,
singleDayProfitOrLoss: aggSingleDayProfitOrLoss,
currentPrice: i.CurrentPrice,
currentExchangeRate: exchangeRate,
marketIsOpen: marketIsOpen
};
}
//Create a structure to summarise total holdings in each currency, to be used in the pie chart
let holdingCurrencies = {
data: [],
indexOfHoldingCurrency: function (symbol) { return this.data.map(function (item) { return item.symbol; }).indexOf(symbol); },
getHoldingCurrency: function (symbol) { return this.data[this.indexOfHoldingCurrency(symbol)]; },
addHoldingCurrency: function (symbol, amount) {
let c = this.getHoldingCurrency(symbol);
if (c) {
c.total += amount;
} else {
this.data.push({ symbol: symbol, total: amount });
}
},
getChartData: function () {
let d = []
for (let i = 0; i < this.data.length; i++) {
//d.push({ label: this.data[i].symbol, data: this.data[i].total / Instruments.GetExchangeRate(this.data[i].symbol, 'GBP') });
d.push({ label: this.data[i].symbol, data: this.data[i].total });
}
return d;
}
};
let holdingCurrenciesCurrentValue = {
data: [],
indexOfHoldingCurrency: function (symbol) { return this.data.map(function (item) { return item.symbol; }).indexOf(symbol); },
getHoldingCurrency: function (symbol) { return this.data[this.indexOfHoldingCurrency(symbol)]; },
addHoldingCurrency: function (symbol, amount) {
let c = this.getHoldingCurrency(symbol);
if (c) {
c.total += amount;
} else {
this.data.push({ symbol: symbol, total: amount });
}
},
getChartData: function () {
let d = []
for (let i = 0; i < this.data.length; i++) {
d.push({ label: this.data[i].symbol, data: this.data[i].total / Instruments.GetExchangeRate(this.data[i].symbol, 'GBP') });
}
return d;
}
};
let bookCostPerAccount = createCategoryAggregator();
let currentValuePerAccount = createCategoryAggregator();
let totalHoldingsValueGBP = 0;
let totalBookCostGBP = 0;
let totalValueGBP = 0;
let totalGainGBP = 0;
let todaysGainGBP = 0;
let maxCurrentValueGBP = 0;
let groupHoldings = $('#groupBySymbol').prop('checked');
let r = [];
let w = []; //Watchlist (instruments with no current holdings)
for (let n = 0; n < Instruments.Data.length; n++) {
let i = Instruments.Data[n];
//if (i.InstrumentType == 'EQUITY') {
if (i.InstrumentType == 'EQUITY' || i.InstrumentType == 'ETF' || i.InstrumentType == 'CRYPTOCURRENCY' || i.InstrumentType == 'CURRENCY') {
//if (i.CurrentPrice) {
if (groupHoldings) {
let agg = aggregateHoldings(i);
if (agg.noUnits > 0) {
r.push(agg);
//holdingCurrencies.addHoldingCurrency(i.Currency, agg.noUnits * agg.purchasePricePerUnit);
//holdingCurrencies.addHoldingCurrency(i.Currency.toUpperCase(), agg.noUnits * agg.purchasePricePerUnit / (i.Currency == "GBp" ? 100 : 1));
holdingCurrencies.addHoldingCurrency(i.Currency.toUpperCase(), agg.bookCostGBP);
if (i.CurrentPrice) {
if (agg.currentValueGBP > maxCurrentValueGBP) { maxCurrentValueGBP = agg.currentValueGBP };
//holdingCurrenciesCurrentValue.addHoldingCurrency(i.Currency, agg.noUnits * i.CurrentPrice);
holdingCurrenciesCurrentValue.addHoldingCurrency(i.Currency.toUpperCase(), agg.noUnits * i.CurrentPrice / (i.Currency == "GBp" ? 100 : 1));
if (agg.currentExchangeRate) {
totalBookCostGBP += agg.bookCostGBP;
totalValueGBP += agg.currentValueGBP;
totalGainGBP += agg.gainGBP;
todaysGainGBP += (agg.marketIsOpen == 0 ? 0 : agg.singleDayGainGBP);
}
}
} else {
//No current holdings for this instrument - add it to the watchlist
if (i.WatchlistID) {
w.push(i);
}
}
} else {
let marketIsOpen = Instruments.MarketIsOpen(i);
let exchangeRate = Instruments.GetExchangeRate(i.Currency, 'GBP');
let noCurrentHoldings = 0;
for (let hn = 0; hn < i.Holdings.length; hn++) {
let h = i.Holdings[hn];
if (h.SoldDate == null) {
noCurrentHoldings++;
let holdingBookCost = h.NoUnits * h.PurchasePricePerUnit;
let currentHoldingValue = h.NoUnits * (i.Currency == 'GBp' ? i.CurrentPrice / 100 : i.CurrentPrice);
let holdingCurrentValueGBP = 0,
holdingGainGBP = 0,
singleDayGainGBP = 0,
singleDayGainPercent = 0,
singleDayProfitOrLoss = 0;
if (i.CurrentPrice) {
let currentHoldingValue = h.NoUnits * (i.Currency == 'GBp' ? i.CurrentPrice / 100 : i.CurrentPrice);
if (exchangeRate) {
let holdingCurrentValueGBP = (h.NoUnits * i.CurrentPrice) / exchangeRate;
//let holdingGainGBP = ((i.CurrentPrice - h.PurchasePricePerUnit) * h.NoUnits) / exchangeRate;
let holdingGainGBP = holdingCurrentValueGBP - h.BookCostGBP;
let singleDayGainGBP = ((i.CurrentPrice - (h.PurchaseDate > i.SingleDayStartDate ? h.PurchasePricePerUnit : i.SingleDayPreviousClose)) * h.NoUnits) / exchangeRate;
let singleDayGainPercent = ((i.CurrentPrice / i.SingleDayPreviousClose) - 1) * 100;
let singleDayProfitOrLoss = singleDayGainGBP < 0 ? 'loss' : 'profit';
if (holdingCurrentValueGBP > maxCurrentValueGBP) { maxCurrentValueGBP = holdingCurrentValueGBP };
}
}
r.push({
displayOrder: i.DisplayOrder,
holdingNumber: hn,
instrumentType: i.InstrumentType,
instrumentName: i.InstrumentName,
displayName: i.DisplayName,
symbol: i.Symbol,
currency: i.Currency,
accountName: h.AccountName,
purchaseDateMin: h.PurchaseDate,
purchaseDateMax: h.PurchaseDate,
noUnits: h.NoUnits,
purchasePricePerUnit: h.PurchasePricePerUnit,
bookCost: holdingBookCost,
currentValue: currentHoldingValue,
gain: (i.Currency == 'GBp' ? (i.CurrentPrice - h.PurchasePricePerUnit) / 100 : (i.CurrentPrice - h.PurchasePricePerUnit)) * h.NoUnits,
//gainPercent: (i.CurrentPrice - h.PurchasePricePerUnit) / h.PurchasePricePerUnit * 100,
gainPercent: holdingGainGBP / h.BookCostGBP * 100,
currentValueGBP: holdingCurrentValueGBP,
gainGBP: holdingGainGBP,
singleDayGainGBP: singleDayGainGBP,
singleDayGainPercent: singleDayGainPercent,
singleDayProfitOrLoss: singleDayProfitOrLoss,
currentPrice: i.CurrentPrice,
currentExchangeRate: exchangeRate,
marketIsOpen: marketIsOpen
});
holdingCurrencies.addHoldingCurrency(i.Currency, h.NoUnits * h.PurchasePricePerUnit);
if (i.CurrentPrice) {
holdingCurrenciesCurrentValue.addHoldingCurrency(i.Currency, h.NoUnits * i.CurrentPrice);
if (exchangeRate) {
currentValuePerAccount.addCategoryAmount(h.AccountName, holdingCurrentValueGBP);
bookCostPerAccount.addCategoryAmount(h.AccountName, holdingBookCost / exchangeRate);
//totalBookCostGBP += holdingBookCost / exchangeRate;
totalBookCostGBP += h.BookCostGBP;
totalValueGBP += holdingCurrentValueGBP;
totalGainGBP += holdingGainGBP;
todaysGainGBP += (marketIsOpen == 0 ? 0 : singleDayGainGBP);
}
}
}
if (noCurrentHoldings == 0) {
//No current holdings for this instrument - add it to the watchlist
if (i.WatchlistID) {
w.push(i);
}
}
}
}
//} //if (i.CurrentPrice)
}
}
return {
totalBookCostGBP: totalBookCostGBP,
totalValueGBP: totalValueGBP,
totalGainGBP: totalGainGBP,
todaysGainGBP: todaysGainGBP,
maxCurrentValueGBP: maxCurrentValueGBP,
holdings: r,
watchlist: w,
holdingCurrencies: holdingCurrencies,
holdingCurrenciesCurrentValue: holdingCurrenciesCurrentValue,
bookCostPerAccount: bookCostPerAccount,
currentValuePerAccount: currentValuePerAccount
};
}
function createTable() {
let t = $("#tblMainHoldings");
if (t.length) {
return;
} else {
let tbl = '<table id="tblMainHoldings" class="mainHoldings"><thead>' +
'<tr><th>Type</th>' +
'<th>Name</th>' +
'<th>Symbol</th>' +
'<th>Ccy</th>' +
'<th>Account</th>' +
'<th>Buy Date</th>' +
'<th>No Units</th>' +
'<th>Buy<br>Price</th>' +
'<th>Current<br>Price</th>' +
'<th>Book<br>Cost</th>' +
'<th>Current<br>Value</th>' +
'<th>Gain</th>' +
'<th>Gain £</th>' +
'<th>Gain %</th>' +
'<th>Current<br>Value £</th>' +
'<th>Allocation</th>' +
'<th>Today&apos;s<br>Gain £</th>' +
'<th>Today&apos;s<br>Gain %</th>' +
'</tr></thead><tbody id="tbMainHoldings"></tbody><tfoot id="tfMainHoldings"><tr><td colspan="18">&nbsp;</td></tr></tfoot></table>';
//$('#holdingsDiv').html(tbl);
$('#holdingsTableDiv').html(tbl);
$("#tblMainHoldings").tablesorter({ ignoreCase: false });
$("#tblMainHoldings").trigger("sorton", Cookies.get('sortHoldings'));
$('#tblMainHoldings').on('sortEnd', function (event) {
// Prints the current sort order to the console
if (event.target.config.sortList.length > 3) {
console.info({ "Setting sortHoldings Cookie (array too long?)": event.target.config.sortList });
}
Cookies.set('sortHoldings', event.target.config.sortList, { 'sameSite': 'strict' });
});
}
}
function createWatchlistTable() {
let t = $("#tblWatchlist");
if (t.length) {
return;
} else {
let tbl = '<table id="tblWatchlist" class="watchlist"><thead>' +
'<tr><th>Type</th>' +
'<th>Name</th>' +
'<th>Symbol</th>' +
'<th>Ccy</th>' +
'<th>Current Price</th>' +
'<th>Today&apos;s Gain</th>' +
'<th>Today&apos;s Gain %</th>' +
'<th>Since Last Sold<th>' +
'</tr></thead><tbody id="tbWatchlist"></tbody></table>';
$('#watchlistTableDiv').html(tbl);
$("#tblWatchlist").tablesorter({ ignoreCase: false });
$("#tblWatchlist").trigger("sorton", Cookies.get('sortWatchlist'));
$('#tblWatchlist').on('sortEnd', function (event) {
// Prints the current sort order to the console
Cookies.set('sorWatchlist', event.target.config.sortList, { 'sameSite': 'strict' });
});
}
}
function addTableRow(rowID, content, tbodyID, highlightUpdates) {
let r = $("#" + rowID);
let newText = $(content).text(); //$(content).text();
if (r.length) {
let currentContent = r.text();
if (currentContent != newText) { //Only update row if the content has changed
$("#" + rowID).replaceWith(content);
if (highlightUpdates) {
$("#" + rowID).addClass("highlighted");
}
}
} else {
$("#" + tbodyID).append(content); //.addClass("highlighted");
//$("#" + rowID).addClass("highlighted");
}
}
function getPercentCell(backgroundColor, cellWidth, valuePercent, label) {
let labelText = label ? label : (valuePercent.toFixed(1) + '%');
let barWidth = ((valuePercent / 100) * cellWidth);
return '<td>' +
'<div class="pcBackground" style="background-color: ' + backgroundColor + '; width:' + barWidth.toFixed(0) + 'px;">&nbsp;</div>' +
'<div class="pcLabel" style="width:' + cellWidth + 'px">' + labelText + '</div>' +
'</td>';
}
function profitOrLoss(value) {
return value >= 0 ? "profit" : "loss";
}
let t = getHoldings();
let totalBookCostGBP = t.totalBookCostGBP;
let totalValueGBP = t.totalValueGBP;
let totalGainGBP = t.totalGainGBP;
let todaysGainGBP = t.todaysGainGBP;
let maxCurrentValueGBP = t.maxCurrentValueGBP;
let holdings = t.holdings;
let watchlist = t.watchlist;
let holdingCurrencies = t.holdingCurrencies;
let holdingCurrenciesCurrentValue = t.holdingCurrenciesCurrentValue;
let bookCostPerAccount = t.bookCostPerAccount;
let currentValuePerAccount = t.currentValuePerAccount;
let maxAllocationPercent = (maxCurrentValueGBP / totalValueGBP) * 100;
let allocationScaleFactor = 100 / maxAllocationPercent;
let altRow = 1;
createTable();
for (let n = 0; n < holdings.length; n++) {
try {
altRow = !altRow;
let h = holdings[n];
let type = h.instrumentType == 'CURRENCY' ? '$' : h.instrumentType.substring(0, 1);
//let profitOrLoss = h.currentPrice < h.purchasePricePerUnit ? 'loss' : 'profit'; //let profitOrLoss = i.CurrentPrice < h.PurchasePricePerUnit ? 'loss' : 'profit';
let openOrClosed = h.marketIsOpen == 1 ? 'open' : 'closed';
let allocationPercent = h.currentValueGBP / totalValueGBP * 100;
let scaledAllocation = allocationScaleFactor * allocationPercent;
let rowID = "holdingRow_" + h.displayOrder + "_" + h.holdingNumber;
let row = '<tr' + (altRow ? ' class="altShareRow"' : '') + ' id="' + rowID + '">' +
'<td class="' + openOrClosed + '">' + type + '</td>' +
'<td class="' + openOrClosed + '">' + (h.displayName != '' ? h.displayName : h.instrumentName) + '</td>' +
'<td><a target="_blank" href="https://uk.finance.yahoo.com/quote/' + h.symbol + '">' + h.symbol + '</a>' + '</td>' +
'<td class="' + openOrClosed + '">' + h.currency + '</td>' +
'<td class="' + openOrClosed + '" title="' + (h.accountNames ? h.accountNames : '') + '">' + h.accountName + '</td>' +
'<td class="' + openOrClosed + '"' + (h.purchaseDateMin == h.purchaseDateMax ? ('>' + new Date(h.purchaseDateMin).yyyymmdd()) : (' title="' + new Date(h.purchaseDateMax).yyyymmdd() + ' - ' + new Date(h.purchaseDateMin).yyyymmdd() + '">' + new Date(h.purchaseDateMin).yyyymmdd()) + '+') + '</td>' +
//'<td class="num ' + openOrClosed + '">' + formatAmount(h.noUnits, '', 0) + '</td>' +
'<td class="num ' + openOrClosed + '">' + h.noUnits.autoScale() + '</td>' +
'<td class="num ' + openOrClosed + '">' + formatAmount(h.purchasePricePerUnit, h.currency, 2) + '</td>' + //Buy Price
'<td class="num ' + openOrClosed + '">' + (h.currentPrice ? formatAmount(h.currentPrice, h.currency, 2) : '') + '</td>' + //Current Price
'<td class="num ' + openOrClosed + '">' + formatAmount(h.currency == 'GBp' ? h.bookCost / 100 : h.bookCost, h.currency == 'GBp' ? 'GBP' : h.currency, 0) + '</td>' + //Book Cost
'<td class="num ' + openOrClosed + '">' + (h.currentExchangeRate ? formatAmount(h.currentValue, h.currency == 'GBp' ? 'GBP' : h.currency, 0) : '') + '</td>' + //Current Value
'<td class="num ' + profitOrLoss(h.currentPrice - h.purchasePricePerUnit) + '">' + (h.currentPrice ? formatAmount(h.gain, h.currency == 'GBp' ? 'GBP' : h.currency, 0) : '') + '</td>' + //Gain
'<td class="num ' + profitOrLoss(h.gainGBP) + '">' + (h.currentExchangeRate ? formatAmount(h.gainGBP, 'GBP', 0) : '') + '</td>' + //Gain £
'<td class="num ' + profitOrLoss(h.gainGBP) + '">' + (h.currentExchangeRate ? h.gainPercent.toFixed(1) + '%' : '') + '</td>' + //Gain %
'<td class="num ' + openOrClosed + '">' + (h.currentExchangeRate ? formatAmount(h.currentValueGBP, 'GBP', 0) : '') + '</td>' + //Current Value £
(h.currentExchangeRate ? getPercentCell('#3f2d95', 70, scaledAllocation, allocationPercent.toFixed(1) + '%') : '') + //Allocation
(h.currentExchangeRate ? (h.marketIsOpen == 0 ? '<td/>' : '<td class="num ' + h.singleDayProfitOrLoss + '">' + formatAmount(h.singleDayGainGBP, 'GBP', 0) + '</td>') : '') + //Today's Gain £
(h.singleDayGainPercent ? (h.marketIsOpen == 0 ? '<td/>' : '<td class="num ' + h.singleDayProfitOrLoss + '">' + h.singleDayGainPercent.toFixed(1) + '%</td>') : '<td/>') + //Today's Gain %
'</tr>';
addTableRow(rowID, row, "tbMainHoldings", true);
}
catch (err) {
logError("Error adding " + holdings[n].symbol + " to main holdings table: " + err.message);
}
}
//Add the totals line at the bottom of the table
//let profitOrLoss = totalValueGBP < totalBookCostGBP ? 'loss' : 'profit';
let todaysProfitOrLoss = todaysGainGBP < 0 ? 'loss' : 'profit';
let lSF = '';
if (vars.fetchTiming.lastSuccessfulFetch) {
let lastFetchAge = ((new Date()) - vars.fetchTiming.lastSuccessfulFetch);
let staleFetch = ((new Date()) - vars.fetchTiming.lastSuccessfulFetch) > (vars.fetchTiming.fetchIntervalSingleDay * 1.5);
lSF = (vars.fetchTiming.lastSuccessfulFetch ? ' - Last Successful Fetch: ' + (staleFetch ? '<span class="loss">' : '') + vars.fetchTiming.lastSuccessfulFetch.yyyymmddhhmmss() + (staleFetch ? '</span>' : '') : '');
}
let row = '<tr id="tfHoldingsTotals"><td></td><td id="holdingsLastUpdated" colspan="8">Updated: ' + new Date().yyyymmddhhmmss() + lSF + '</td>' +
'<td class="num">' + formatAmount(totalBookCostGBP, 'GBP', 0) + '</td>' + //Book Cost
'<td></td><td></td>' +
'<td class="num ' + profitOrLoss(totalValueGBP - totalBookCostGBP) + '">' + formatAmount(totalGainGBP, 'GBP', 0) + '</td>' + //Gain GBP
'<td class="num ' + profitOrLoss(totalValueGBP - totalBookCostGBP) + '">' + formatAmount((((totalValueGBP / totalBookCostGBP) - 1) * 100), '', 1) + '%</td>' + //Gain %
'<td class="num">' + formatAmount(totalValueGBP, 'GBP', 0) + '</td>' + //Current Value
'<td>&nbsp;</td>' + //Allocation
'<td class="num ' + todaysProfitOrLoss + '">' + formatAmount(todaysGainGBP, 'GBP', 0) + '</td>' + //Today's Gain GBP
//'<td class="num ' + todaysProfitOrLoss + '">' + formatAmount(((todaysGainGBP / totalBookCostGBP) * 100), '', 1) + '%</td>' + //Today's Gain %
'<td class="num ' + todaysProfitOrLoss + '">' + formatAmount(((todaysGainGBP / (totalValueGBP - todaysGainGBP)) * 100), '', 1) + '%</td>' + //Today's Gain %
'</tr>';
addTableRow("tfHoldingsTotals", row, "tfMainHoldings", false);
//Create the watchlist table
createWatchlistTable();
altRow = 1;
for (let n = 0; n < watchlist.length; n++) {
altRow = !altRow;
let wi = watchlist[n];
try {
let type = wi.InstrumentType == 'CURRENCY' ? '$' : wi.InstrumentType.substring(0, 1);
let openOrClosed = Instruments.MarketIsOpen(wi) == 1 ? 'open' : 'closed';
let profitOrLoss = wi.CurrentPrice < wi.SingleDayPreviousClose ? 'loss' : 'profit';
let rowID = "watchlistRow_" + wi.DisplayOrder;
let sinceLastSold = '<td></td>';
if (wi.LastSoldPrice) {
let delta = (wi.CurrentPrice - wi.LastSoldPrice) / wi.LastSoldPrice * 100;
let tooltip = new Date(wi.LastSoldDate).yyyymmdd() + ' @ ' + wi.LastSoldPrice.toFixed(2);
sinceLastSold = '<td title="' + tooltip + '" class="num ' + (delta < 0 ? 'loss' : 'profit') + '">' + (delta).toFixed(1) + '%</td>';
}
//Type, Name, Symbol, Ccy, Current Price, Today's Gain, Today's Gain %
let row = '<tr' + (altRow ? ' class="altShareRow"' : '') + ' id="' + rowID + '">' +
'<td class="' + openOrClosed + '">' + type + '</td>' +
'<td class="' + openOrClosed + '">' + wi.InstrumentName + '</td>' +
'<td><a target="_blank" href="https://uk.finance.yahoo.com/quote/' + wi.Symbol + '">' + wi.Symbol + '</a>' + '</td>' +
'<td class="' + openOrClosed + '">' + wi.Currency + '</td>' +
'<td class="num ' + openOrClosed + '">' + (wi.CurrentPrice ? formatAmount(wi.CurrentPrice, wi.Currency, 2) : '') + '</td>' + //Current Price
(Instruments.marketIsOpen == 0 ? '<td/>' : '<td class="num ' + profitOrLoss + '">' + (wi.CurrentPrice ? formatAmount(wi.CurrentPrice - wi.SingleDayPreviousClose, wi.Currency, 2) : '') + '</td>') + //Today's Gain
(Instruments.marketIsOpen == 0 ? '<td/>' : '<td class="num ' + profitOrLoss + '">' + (wi.CurrentPrice ? ((wi.CurrentPrice - wi.SingleDayPreviousClose) / wi.SingleDayPreviousClose * 100).toFixed(1) : '') + '</td>') + //Today's Gain %
//'<td>' + (new Date(wi.LastSoldDate)).yyyymmdd() + '</td>' +
sinceLastSold +
'</tr>';
addTableRow(rowID, row, "tbWatchlist", true);
}
catch (err) {
logError("Error adding " + wi.symbol + " to watchlist table: " + err.message);
}
}
//Flash the last updated cell
$("#holdingsLastUpdated").addClass("highlighted");
//Remove the highlight from all rows/cells
$("#tblMainHoldings").find(".highlighted").removeClass("highlighted", 1200);
$("#tblWatchlist").find(".highlighted").removeClass("highlighted", 1200);
//Tell tablesorter that the table has been updated
var resort = true;
$("#tblMainHoldings").trigger("update", [resort]);
//Update the daily holdings value table in the DB
if (totalValueGBP != vars.lastTotalHoldingsValue) {
$.ajax({
type: "POST",
contentType: "application/json",
url: "SharePrices.aspx/SetTotalHoldingsValue",
dataType: "json",
data: JSON.stringify({ TotalValue: totalValueGBP }),
success: function (response) {
vars.previousDay = response.PreviousDay;
vars.previousClose = response.PreviousClose;
vars.previousWeek = response.PreviousWeekDate;
vars.previousWeekClose = response.PreviousWeekClose;
//Update the total holdings span in the site banner
let formattedAmount = formatAmount(totalValueGBP, "GBP", 0);
let holdingsHTML = '<span style="font-size: small;">Total holdings:&nbsp;</span>' + '<span style="font-size: large">' + formattedAmount + '</span>&nbsp;';
holdingsHTML += '<span style="font-size: small;"><span class="' + (vars.previousClose < totalValueGBP ? 'profit">+' : 'loss">') + formatAmount(totalValueGBP - vars.previousClose, "GBP", 0) + '</span>&nbsp;(d)';
holdingsHTML += '&nbsp;/&nbsp;<span class="' + (vars.previousWeekClose < totalValueGBP ? 'profit">+' : 'loss">') + formatAmount(totalValueGBP - vars.previousWeekClose, "GBP", 0) + '</span>&nbsp;(w)</span>';
$("#spnTotalHoldings").html(holdingsHTML);
//Set the page title
document.title = formattedAmount + ' - ' + vars.initialPageTitle;
vars.lastTotalHoldingsValue = totalValueGBP;
},
failure: function (response) { console.error("SetTotalHoldingsValue error: " + JSON.stringify(response)); }
});
}
//Create / update the currency allocation pie chart
function labelFormatter(label, series) {
return "<div class='pieLabel'>" + label + "<br/>" + Math.round(series.percent) + "%<br>" + formatAmount(series.data[0][1], 'GBP', 0) + "</div>";
}
let ops = {
series: {
downsample: { threshold: 0 },
pie: { show: true, radius: 1, label: { show: true, radius: 0.7, formatter: labelFormatter } }
},
legend: { show: false },
grid: { hoverable: true },
tooltip: {
show: true,
content: "%p.0%, %s, n=%n", // show percentages, rounding to 2 decimal places
shifts: { x: 20, y: 0 },
defaultTheme: false
}
};
let cd = holdingCurrencies.getChartData();
if (cd.length) {
$.plot('#divHoldingCurrenciesChart', cd, ops);
}
cd = holdingCurrenciesCurrentValue.getChartData();
if (cd.length) {
$.plot('#divHoldingCurrenciesChartCurrentValue', cd, ops);
}
function labelFormatter2(label, series) {
return "<div class='pieLabel'>" + label + "<br/>" + formatAmount(series.data[0][1], 'GBP', 0) + "</div>";
}
ops.series.pie.label.formatter = labelFormatter2;
cd = bookCostPerAccount.getChartData();
if (cd.length) {
$.plot('#divAccountBookCost', cd, ops);
}
cd = currentValuePerAccount.getChartData();
if (cd.length) {
$.plot('#divAccountValue', cd, ops);
}
}
function 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;
}