Steves_Code/Websites/SharePrices/SharePrices/scripts/SharePrices.js

2070 lines
92 KiB
JavaScript

// @ts-check
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";
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 lastSuccessfulFetch;
var currencyFormatter = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' });
var instrumentSearchResults = [];
var tablesUpdateTimings = { lastUpdate: 0, timeBetweenUpdates: 10000, timer: 0, updateNeeded: true };
var indexMarquee = {
newContent: '',
currentContent: '',
marqueOptions: { speed: 100, duplicated: true, gap: 150, startVisible: true, delayBeforeStart: 500 },
init: function (container, initialContent) {
this.currentContent = initialContent;
this.container = container;
this.lastUpdated = new Date();
let marqueObject = this;
this.container.html(initialContent).marquee(this.marqueOptions).bind('finished', function () { marqueObject.marqueeFinished() });
},
setContent: function (newContent) {
if (newContent != this.currentContent) {
this.newContent = newContent;
}
if (this.lastUpdated < new Date() - (5 * timespans.oneMinute)) {
logInfo("indexMarquee has not updated for 5 minutes. Forcing update now");
this.marqueeFinished();
}
},
marqueeFinished: function () {
if (this.newContent.length > 0) {
this.lastUpdated = new Date();
let cont = this.container;
let opts = this.marqueOptions;
let newContent = this.newContent;
let marqueObject = this;
this.currentContent = newContent;
this.newContent = '';
cont.marquee('destroy').html(newContent).marquee(opts).bind('finished', function () { marqueObject.marqueeFinished() });
}
}
}
var Instruments = {
Data: [],
indexOfSymbol: function (Symbol) { return this.Data.map(function (item) { return item.Symbol; }).indexOf(Symbol); },
GetSymbol: function (Symbol) { return this.Data[this.indexOfSymbol(Symbol)]; },
GetHolding: function (holdingID) {
for (let x = 0; x < this.Data.length; x++) {
let i = this.Data[x];
for (let n = 0; n < i.Holdings.length; n++) {
let h = i.Holdings[n];
if (h.HoldingID == holdingID) {
return { instrument: i, holdingIndex: n, holding: h};
}
}
}
},
GetCurrentHoldings: function (Instrument) {
let result = [];
for (let x = 0; x < Instrument.Holdings.length; x++) {
let h = Instrument.Holdings[x];
if (h.SoldDate == null) {
result.push(h);
}
}
return result;
},
GetAccountCurrencyHolding: function (AccountName, CurrencyInstrument) {
if (CurrencyInstrument.Holdings) {
for (let x = 0; x < CurrencyInstrument.Holdings.length; x++) {
let h = CurrencyInstrument.Holdings[x];
if (h.SoldDate == null && h.AccountName == AccountName) {
return (h);
}
}
}
},
GetLastFetchedDaily: function () {
//if (!this.Data[0].lastFetchedDaily) { this.Data[0].lastFetchedDaily = this.Data[0].MaxDailyDate };
if (!this.Data[0].lastFetchedDaily) { this.Data[0].lastFetchedDaily = new Date().getTime() - 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() - vars.fetchTiming.fetchIntervalDaily + (4 * timespans.oneMinute) };
if (lastFetchedDate > this.Data[i].lastFetchedDaily) {
lastFetchedDate = this.Data[i].lastFetchedDaily;
lastFetchedIndex = i;
}
}
}
//console.info({ lastFetchedType: 'Daily', Date: new Date(lastFetchedDate).yyyymmddhhmmss(), Index: lastFetchedIndex, Symbol: Instruments.Data[lastFetchedIndex].Symbol });
return this.Data[lastFetchedIndex];
},
GetLastFetchedIntraday: function () {
//if (!this.Data[0].lastFetchedIntraday) { this.Data[0].lastFetchedIntraday = this.Data[0].MaxIntradayDate };
if (!this.Data[0].lastFetchedIntraday) { this.Data[0].lastFetchedIntraday = new Date().getTime() - 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() - vars.fetchTiming.fetchIntervalIntraday + (2 * timespans.oneMinute) };
if (lastFetchedDate > this.Data[i].lastFetchedIntraday) {
lastFetchedDate = this.Data[i].lastFetchedIntraday;
lastFetchedIndex = i;
}
}
}
//console.info({ lastFetchedType: 'Intraday', Date: new Date(lastFetchedDate).yyyymmddhhmmss(), Index: lastFetchedIndex, Symbol: Instruments.Data[lastFetchedIndex].Symbol });
return this.Data[lastFetchedIndex];
},
GetLastFetchedSingleDay: function () {
if (!this.Data[0].lastFetchedSingleDay) { this.Data[0].lastFetchedSingleDay = 0 };
let lastFetchedDate = this.Data[0].lastFetchedSingleDay;
let lastFetchedIndex = 0;
for (let i = 1; i < this.Data.length; i++) {
if (this.Data[i].Symbol != 'GBP' && this.Data[i].Symbol != 'GBp' && this.Data[i].TrackPriceHistory == 1) {
if (!this.Data[i].lastFetchedSingleDay) { this.Data[i].lastFetchedSingleDay = 0 };
if (lastFetchedDate > this.Data[i].lastFetchedSingleDay) {
lastFetchedDate = this.Data[i].lastFetchedSingleDay;
lastFetchedIndex = i;
}
}
}
//console.info({ lastFetchedType: 'SingleDay', Date: new Date(lastFetchedDate).yyyymmddhhmmss(), Index: lastFetchedIndex, Symbol: Instruments.Data[lastFetchedIndex].Symbol });
return this.Data[lastFetchedIndex];
},
GetCurrentPrice: function (Instrument) {
if (Instrument.CurrentPrice) {
return Instrument.CurrentPrice;
} else {
for (let i = Instrument.IntradayData.length - 1; i >= 0; i--) {
if (Instrument.IntradayData[i].close) {
return Instrument.IntradayData[i].close;
}
}
for (let i = Instrument.DailyData.length - 1; i >= 0; i--) {
if (Instrument.DailyData[i].close) {
return Instrument.DailyData[i].close;
}
}
}
},
GetExchangeRate: function (fromCurrency, toCurrency) {
if (fromCurrency == 'GBp' && toCurrency == 'GBP') {
return 100;
} else {
if (fromCurrency == 'GBP' && toCurrency == 'GBP') {
return 1;
} else {
let curr = this.GetSymbol(toCurrency + fromCurrency + '=X');
if (curr) {
//return curr.CurrentPrice;
return this.GetCurrentPrice(curr);
} else {
curr = this.GetSymbol(toCurrency + fromCurrency + '=X');
if (curr) {
if (curr.CurrentPrice) {
//return 1/curr.CurrentPrice;
return 1 / this.GetCurrentPrice(curr);
}
}
}
}
}
},
MarketIsOpen: function (instrument, preOpenThreshold, postCloseThreshold) {
let currentMarketDT = new Date().getTime() + instrument.GMTOffset;
if (currentMarketDT < (instrument.SingleDayStartDate - (preOpenThreshold ? preOpenThreshold : 0))) {
return 0; //Not open yet
} else {
if (currentMarketDT > (instrument.SingleDayEndDate - (postCloseThreshold ? postCloseThreshold : 0))) {
return 2; //Market closed (end of day)
} else {
return 1; //Market is currently open
}
}
},
GetCurrentTradingDay: function () {
for (let x = 0; x < this.Data.length; x++) {
let i = Instruments.Data[x];
let marketCurrentDT = new Date().getTime() + i.GMTOffset;
if (i.InstrumentType == 'EQUITY') {
if (marketCurrentDT >= i.SingleDayStartDate && marketCurrentDT <= i.SingleDayEndDate) {
//This instrument's market is currently open - return the current day
return new Date(marketCurrentDT).yyyymmdd();
}
}
}
//No markets are open - return the current day
return new Date().yyyymmdd();
}
}
var Currencies = {
Data: [],
indexOfName: function (Name) { return this.Data.map(function (item) { return item.Name; }).indexOf(Name); },
updateSymbol: function (Symbol, Name, CurrentPrice) {
var newObject = { Name: Name, CurrentPrice: CurrentPrice, URL: 'https://uk.finance.yahoo.com/quote/' + Symbol };
var i = this.indexOfName(Name);
if (i >= 0) {
this.Data[i] = newObject;
} else {
this.Data.push(newObject);
}
}
}
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 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>';
}*/
export function getInstruments() {
try {
//logInfo('getInstruments()');
vars.initialPageTitle = document.title;
if (vars.fetchTiming.fetchTimer != 0) { clearTimeout(vars.fetchTiming.fetchTimer) };
indexMarquee.init($('#divIndexMarquee'), '&emsp;');
$.ajax({
type: "POST",
contentType: "application/json",
url: "SharePrices.aspx/GetInstruments",
dataType: "json",
success: function (response) {
//logInfo('getInstruments Success');
Instruments.Data = response.Instruments;
Instruments.Accounts = response.Accounts;
for (let ix = 0; ix < Instruments.Data.length; ix++) {
let i = Instruments.Data[ix];
i.Holdings = [];
i.HoldingsHistory = [];
}
let symbol = '';
let instrument = {};
for (let i = 0; i < response.Holdings.length; i++) {
if (symbol != response.Holdings[i].Symbol) {
symbol = response.Holdings[i].Symbol;
instrument = Instruments.GetSymbol(response.Holdings[i].Symbol);
}
instrument.Holdings.push(response.Holdings[i]);
}
for (let i = 0; i < response.HoldingsHistory.length; i++) {
let h = response.HoldingsHistory[i];
if (symbol != h.Symbol) {
symbol = h.Symbol;
instrument = Instruments.GetSymbol(h.Symbol);
}
instrument.HoldingsHistory.push(h);
}
createSharesTable();
getAllLocalDailyData();
tablesUpdateTimings.timer = setInterval(updateTables, 500);
},
failure: function (response) { console.error("GetInstruments error: " + response.d) }
});
} catch (err) {
logError({ MSG: "Error in getInstruments", err: err });
}
};
export function createSharesTable() {
try {
let excludeNoHoldings = $('#excludeNoHoldings').prop('checked');
//logInfo('createSharesTable()');
var tbl = '<table id="tblInstruments"><tr><th>Instrument</th>' +
'<th>All</th>' +
'<th>3 Month</th>' +
'<th>2 Week</th>' +
'<th>1 Day</th></tr>';
for (var rowNo = 0; rowNo < Instruments.Data.length; rowNo++) {
let i = Instruments.Data[rowNo];
//if (i.DisplayOrder) {
//if ((!excludeNoHoldings) || i.InstrumentType != 'EQUITY' || Instruments.GetCurrentHoldings(i).length > 0) {
if ((!excludeNoHoldings) || (i.InstrumentType != 'EQUITY' && i.InstrumentType != 'CRYPTOCURRENCY') || Instruments.GetCurrentHoldings(i).length > 0) {
if (i.Symbol != 'GBP') {
let cls = (rowNo % 2) ? 'shareRow altShareRow' : 'shareRow';
let tr = '<tr class="' + cls + '" id="shareRow' + i.DisplayOrder.toString() + '">' + createSharesTableRow(i) + '</tr>';
tbl += tr;
}
}
}
tbl += '</table>';
$('#chartsDiv').html(tbl);
//getAllLocalDailyData();
} catch (err) {
logError({ MSG: "Error in createSharesTable", err: err });
}
};
function createSharesTableRow(instrument) {
function getHoldingsTable(Instrument) {
if (Instrument.Holdings.length > 0) {
let totalUnits = 0;
let totalPrice = 0;
let totalPriceGBP = 0;
let noRows = 0;
let t = '<table class="miniHoldings"><tr><th>Account</th><th>Buy Date</th><th>Price</th><th>Units</th><th>Total</th></tr>';
for (let i = 0; i < Instrument.Holdings.length; i++) {
let h = Instrument.Holdings[i];
if (h.SoldDate == null) {
noRows++;
totalUnits += h.NoUnits;
totalPrice += (h.NoUnits * h.PurchasePricePerUnit);
totalPriceGBP += h.BookCostGBP;
t += '<tr><td><span class="deletebutton" onclick="importedFunctions.showModalDialog_SoldHolding(' + h.HoldingID + ')">&times;</span>' + h.AccountName + '</td>' +
'<td>' + new Date(h.PurchaseDate).yyyymmdd() + '</td>' +
'<td class="num">' + formatAmount(h.PurchasePricePerUnit, '', 2) + '</td>' +
//'<td class="num">' + formatAmount(h.NoUnits, '', 0) + '</td>' +
'<td class="num">' + h.NoUnits.autoScale() + '</td>' +
'<td class="num">' + formatAmount(h.NoUnits * h.PurchasePricePerUnit, '', 0) + '</td></tr>';
} else {
if (Instrument.ShowPreviousHoldings | 0 == 1) {
noRows++;
t += '<tr class="soldHolding" title="Sold: ' + new Date(h.SoldDate).yyyymmddhhmmss() + '"><td>' + h.AccountName + '</td>' +
'<td>' + new Date(h.PurchaseDate).yyyymmdd() + '</td>' +
'<td class="num">' + formatAmount(h.PurchasePricePerUnit, '', 2) + '</td>' +
//'<td class="num">' + formatAmount(h.NoUnits, '', 0) + '</td>' +
'<td class="num">' + h.NoUnits.autoScale() + '</td>' +
'<td class="num">' + formatAmount(h.NoUnits * h.PurchasePricePerUnit, '', 0) + '</td></tr>';
}
}
}
//if (Instrument.Holdings.length > 1) {
if (totalUnits > 0) {
Instrument.TotalUnitsHeld = totalUnits;
Instrument.TotalHoldingsCost = totalPrice;
Instrument.TotalHoldingsCostGBP = totalPriceGBP;
//Instrument.BreakevenPrice = totalPrice / totalUnits;
Instrument.BreakevenPrice = totalPrice / totalUnits;
t += '<tr><td class="no-border"></td>' +
'<td class="no-border">Total</td>' +
'<td class="num">' + formatAmount(Instrument.BreakevenPrice, '', 2) + '</td>' +
//'<td class="num">' + formatAmount(Instrument.TotalUnitsHeld, '', 0) + '</td>' +
'<td class="num">' + Instrument.TotalUnitsHeld.autoScale() + '</td>' +
//'<td class="num">' + formatAmount(Instrument.TotalHoldingsCost, '', 0) + '</td>' +
'<td class="num">' + formatAmount(Instrument.TotalHoldingsCost, '', 0) + '</td>' +
'</tr>';
}
t += '</table>';
return (noRows>0) ? t : '';
} else {
delete Instrument.TotalUnitsHeld;
delete Instrument.TotalHoldingsCost;
delete Instrument.TotalHoldingsCostGBP;
delete Instrument.BreakevenPrice;
return '';
}
}
let dispOrder = instrument.DisplayOrder.toString();
let breakevenIcon = (instrument.ShowBreakevenLine | 0) == 1 ? 'icons/Breakeven.png' : 'icons/Breakeven_Off.png';
let previousHoldingsIcon = (instrument.ShowPreviousHoldings | 0) == 1 ? 'icons/History.png' : 'icons/History_Off.png';
return '<td><span alt="' + instrument.InstrumentName + '" class="instrumentName">' + instrument.DisplayName + '</span><br>' + //'<td><span class="instrumentName">' + instrument.InstrumentName + '</span><br>' +
'<a target="_blank" href="https://uk.finance.yahoo.com/quote/' + instrument.Symbol + '">' + instrument.Symbol + '</a>&nbsp;&nbsp;' +
//'<span class="addHolding" data-symbol="' + instrument.Symbol + '">+</span><div class="spacer"></div>' +
'<img src="icons/Plus.png" alt="Add Holding" width="16" height="16" onclick="importedFunctions.showModalDialog_AddHolding(' + "'" + instrument.Symbol + "'" + ')" />&nbsp;&nbsp;' +
'<img src="icons/UpArrow.png" alt="Move Instrument Up" width="16" height="16" onclick="moveInstrumentUp(' + "'" + instrument.Symbol + "'" + ')" />&nbsp;' +
'<img src="icons/DownArrow.png" alt="Move Instrument Up" width="16" height="16" onclick="moveInstrumentDown(' + "'" + instrument.Symbol + "'" + ')" />&nbsp;&nbsp;' +
'<img src="' + breakevenIcon + '" class="breakeven-button" alt="Show Breakeven Line" width="16" height="16" onclick="toggleBreakevenLine(' + "'" + instrument.Symbol + "'" + ')" />&nbsp;&nbsp;' +
'<img src="' + previousHoldingsIcon + '" alt="Show Previous Holdings" width="16" height="16" onclick="togglePreviousHoldings(' + "'" + instrument.Symbol + "'" + ')" />&nbsp;&nbsp;' +
'<img src="icons/Chart.png" alt="Full Screen Chart" width="16" height="16" onclick="importedFunctions.showModalDialog_FullScreenChart(' + "'" + instrument.Symbol + "'" + ')" />' +
getHoldingsTable(instrument) +
'<div class="price-summary" id="divCurrentValue' + dispOrder + '">' + getCurrentValueContent(instrument) + '</div>' +
'</td>' +
'<td class="longContainer"><div class="LongChart" id="divLongChart' + dispOrder + '"></div><div class="LongSummary" id="divLongSummary' + dispOrder + '" /></td>' +
'<td class="midContainer"><div class="MidChart" id="divMidChart' + dispOrder + '"></div><div class="MidSummary" id="divMidSummary' + dispOrder + '" /></td>' +
'<td class="shortContainer"><div class="ShortChart" id="divShortChart' + dispOrder + '"></div><div class="ShortSummary" id="divShortSummary' + dispOrder + '" /></td>' +
'<td class="dayContainer"><div class="DayChart" id="divDayChart' + dispOrder + '"></div><div class="DaySummary" id="divDaySummary' + dispOrder + '" /></td>'
//'<td><div class="DayChart" id="divDayFlotChart' + dispOrder + '"></div><div class="DayChart" id="divDayChart' + dispOrder + '"></div><div class="DaySummary" id="divDaySummary' + dispOrder + '" /></td>'
};
function getCurrentValueContent(Instrument) {
let currentValue = '';
if (Instrument.CurrentPrice) {
if (Instrument.TotalUnitsHeld > 0) {
currentValue = '<div class="spacer"></div>';
//currentValue += 'Price (' + Instrument.Currency + '): ' + (Instrument.BreakevenPrice < 0.1 ? Instrument.BreakevenPrice.toPrecision(3) : Instrument.BreakevenPrice.toFixed(2)) + ' -&gt; ' + (Instrument.CurrentPrice < 0.1 ? Instrument.CurrentPrice.toPrecision(3) : Instrument.CurrentPrice.toFixed(2)) + ': ' + getProfitOrLoss(Instrument.BreakevenPrice, Instrument.CurrentPrice) + '<br>';
currentValue += 'Price (' + Instrument.Currency + '): ' + (Instrument.BreakevenPrice < 0.1 ? Instrument.BreakevenPrice.autoScale() : Instrument.BreakevenPrice.toFixed(2)) + ' -&gt; ' + (Instrument.CurrentPrice < 0.1 ? Instrument.CurrentPrice.autoScale() : Instrument.CurrentPrice.toFixed(2)) + ': ' + getProfitOrLoss(Instrument.BreakevenPrice, Instrument.CurrentPrice) + '<br>';
//currentValue += 'Value: ' + (Instrument.TotalHoldingsCost).toFixed(2) + ' -&gt; ' + (Instrument.TotalUnitsHeld * Instrument.CurrentPrice).toFixed(2) + ': ' + getProfitOrLoss(Instrument.TotalHoldingsCost, Instrument.TotalUnitsHeld * Instrument.CurrentPrice);
currentValue += 'Book Cost (GBP): ' + (Instrument.TotalHoldingsCostGBP).toFixed(2);
let exRate = Instruments.GetExchangeRate(Instrument.Currency, 'GBP');
if (exRate) {
if (Instrument.Currency != 'GBp') {
currentValue += '<br>GBP/' + Instrument.Currency + ' = ' + exRate.toFixed(4);
}
currentValue += '<div class="current-value spacer">';
//currentValue += 'Current value:<br>' + currencyFormatter.format(Instrument.TotalUnitsHeld * Instrument.CurrentPrice / exRate) + ': ' + getProfitOrLoss(Instrument.TotalHoldingsCostGBP / exRate, Instrument.TotalUnitsHeld * Instrument.CurrentPrice / exRate, 1);
currentValue += 'Current value:<br>' + currencyFormatter.format(Instrument.TotalUnitsHeld * Instrument.CurrentPrice / exRate) + ': ' + getProfitOrLoss(Instrument.TotalHoldingsCostGBP, Instrument.TotalUnitsHeld * Instrument.CurrentPrice / exRate, 1);
currentValue += '</div>';
} else {
currentValue += '<br>' + Instrument.Currency;
}
} else {
//INDEX, CURRENCY, EQUITY
if (Instrument.InstrumentType == 'CURRENCY') {
currentValue = 'Exchange Rate: ' + Instrument.CurrentPrice.toFixed(4);
} else {
if (Instrument.InstrumentType == 'CRYPTOCURRENCY' && Math.abs(Instrument.CurrentPrice) < 0.001) {
currentValue = 'Price (' + Instrument.Currency + '): ' + Instrument.CurrentPrice.toExponential();
} else {
currentValue = 'Price (' + Instrument.Currency + '): ' + Instrument.CurrentPrice.toFixed(2);
}
}
}
}
return currentValue;
}
function moveInstrumentUp(symbol) {
let i = Instruments.indexOfSymbol(symbol);
if (i > 0) {
swapInstrumentDisplayOrders(symbol, Instruments.Data[i - 1].Symbol);
}
}
function moveInstrumentDown(symbol) {
let i = Instruments.indexOfSymbol(symbol);
if (i < Instruments.Data.length -1) {
swapInstrumentDisplayOrders(symbol, Instruments.Data[i + 1].Symbol);
}
}
function swapInstrumentDisplayOrders(symbol1, symbol2) {
$.ajax({
type: "POST",
contentType: "application/json",
url: "SharePrices.aspx/SwapInstrumentDisplayOrders",
dataType: "json",
data: JSON.stringify({Symbol1: symbol1, Symbol2: symbol2}),
success: function (response) {
//{Symbol1, Symbol1NewDisplayOrder, Symbol2, Symbol2NewDisplayOrder}
let i1 = Instruments.indexOfSymbol(response.Symbol1);
let i2 = Instruments.indexOfSymbol(response.Symbol2);
//Update the DisplayOrders in the existing instruments
Instruments.Data[i1].DisplayOrder = response.Symbol1NewDisplayOrder;
Instruments.Data[i2].DisplayOrder = response.Symbol2NewDisplayOrder;
//Swap the instruments in the array
let temp = Instruments.Data[i1];
Instruments.Data[i1] = Instruments.Data[i2];
Instruments.Data[i2] = temp;
//Swap the rows in the charts table
let i = Instruments.Data[i1];
$("#shareRow" + i.DisplayOrder).html(createSharesTableRow(i));
i = Instruments.Data[i2];
$("#shareRow" + i.DisplayOrder).html(createSharesTableRow(i));
//Redraw the charts on both rows
redrawShareRowCharts(Instruments.Data[i1]);
redrawShareRowCharts(Instruments.Data[i2]);
},
failure: function (response) { console.error("swapInstrumentDisplayOrders error:"); console.info(response); }
});
}
function toggleBreakevenLine(symbol) {
let i = Instruments.GetSymbol(symbol);
i.ShowBreakevenLine = !(i.ShowBreakevenLine | 0);
$("#shareRow" + i.DisplayOrder).html(createSharesTableRow(i)); //Redraw the entire share table row
redrawShareRowCharts(i);
}
function togglePreviousHoldings(symbol) {
let i = Instruments.GetSymbol(symbol);
i.ShowPreviousHoldings = !(i.ShowPreviousHoldings | 0);
$("#shareRow" + i.DisplayOrder).html(createSharesTableRow(i)); //Redraw the entire share table row
redrawShareRowCharts(i);
}
function getNewRemoteData() {
try {
//logInfo("getNewRemoteData");
//clearTimeout(fetchTimer);
//logInfo(new Date().yyyymmddhhmmss() + ": getNewRemoteData()")
let nothingToFetch = false;
let lastFetchedDailyInstrument = Instruments.GetLastFetchedDaily();
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() - 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() - vars.fetchTiming.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);
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 });
}
}
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);
//vars.fetchTiming.fetchTimer = setInterval(getNewRemoteData, 1000);
vars.fetchTiming.fetchTimer = setTimeout(getNewRemoteData, 200);
},
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 });
}
}
export 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 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();
vars.fetchTiming.lastFetchStartTime = 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) {
vars.fetchTiming.lastFetchDuration = (new Date().getTime()) - vars.fetchTiming.lastFetchStartTime;
let newData = [];
if (response.chart && response.chart.result) {
let dataSeries = response.chart.result[0];
let nonSubmittedEntries = 0;
for (let i = 0; i < dataSeries.timestamp.length; i++) {
let quote = dataSeries.indicators.quote[0];
if (quote.open[i] == null || quote.high[i] == null || quote.low[i] == null || quote.close[i] == null || quote.volume[i] == null) {
//console.info("Not submitting entry with null values: " + JSON.stringify({ DT: dataSeries.timestamp[i] * 1000, open: quote.open[i], high: quote.high[i], low: quote.low[i], close: quote.close[i], volume: quote.volume[i] }));
nonSubmittedEntries += 1;
} else {
//newData.push({ DT: dataSeries.timestamp[i] * 1000, open: quote.open[i], high: quote.high[i], low: quote.low[i], close: quote.close[i], volume: quote.volume[i] });
if (Instrument.InstrumentType == 'CRYPTOCURRENCY') {
newData.push({
DT: dataSeries.timestamp[i] * 1000,
open: quote.open[i],
high: quote.high[i],
low: quote.low[i],
close: quote.close[i],
volume: quote.volume[i]
});
} else {
newData.push({
DT: dataSeries.timestamp[i] * 1000,
open: fixDecimals(quote.open[i], 4),
high: fixDecimals(quote.high[i], 4),
low: fixDecimals(quote.low[i], 4),
close: fixDecimals(quote.close[i], 4),
volume: quote.volume[i]
});
}
}
}
if (nonSubmittedEntries > 0) {
//console.warn("Didn't submit " + nonSubmittedEntries.toString() + " entries (" + newData.length.toString() + " OK) with null values for " + Instrument.Symbol);
}
//let d = new Date(newData[newData.length - 1].DT).yyyymmddhhmmss();
//console.info("getYahooDailyData: " + Instrument.Symbol + ', Last data point: ' + d);
if (newData.length > 0) {
vars.fetchTiming.lastSuccessfulFetch = new Date();
vars.fetchTiming.lastSuccessfulFetchDuration = (new Date()) - vars.fetchTiming.lastSuccessfulFetchStartTime;
}
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) {
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",
url: "SharePrices.aspx/FetchYahooFinanceIntraday?S=" + Instrument.Symbol,
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];
let nonSubmittedEntries = 0;
if (dataSeries.timestamp) {
for (let i = 0; i < dataSeries.timestamp.length; i++) {
let quote = dataSeries.indicators.quote[0];
if (quote.open[i] == null || quote.high[i] == null || quote.low[i] == null || quote.close[i] == null || quote.volume[i] == null) {
nonSubmittedEntries += 1;
//console.info("Not submitting entry with null values: " + JSON.stringify({ DT: dataSeries.timestamp[i] * 1000, open: quote.open[i], high: quote.high[i], low: quote.low[i], close: quote.close[i], volume: quote.volume[i] }));
} else {
if (Instrument.InstrumentType == 'CRYPTOCURRENCY') {
newData.push({ DT: dataSeries.timestamp[i] * 1000,
open: quote.open[i],
high: quote.high[i],
low: quote.low[i],
close: quote.close[i],
volume: quote.volume[i] });
} else {
newData.push({ DT: dataSeries.timestamp[i] * 1000,
open: fixDecimals(quote.open[i], 4),
high: fixDecimals(quote.high[i], 4),
low: fixDecimals(quote.low[i], 4),
close: fixDecimals(quote.close[i], 4),
volume: quote.volume[i] });
}
}
}
if (nonSubmittedEntries > 0) {
//console.warn("Didn't submit " + nonSubmittedEntries.toString() + " entries (" + newData.length.toString() + " OK) with null values for " + Instrument.Symbol);
}
if (newData.length) {
vars.fetchTiming.lastSuccessfulFetch = new Date();
submitNewIntradayData(Instrument, newData);
createMidChart(Instrument);
createShortChart(Instrument);
} else {
console.info("No NEW data received for symbol: " + Instrument.Symbol);
}
} else {
//logWarning({ MSG: "No timestamp series received for symbol: " + Instrument.Symbol, response: response });
}
} else {
//console.warn("No data received for symbol: " + Instrument.Symbol);
logWarning({ MSG: "No data received for symbol: " + Instrument.Symbol, response: response });
}
},
failure: function (response) {
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",
url: "SharePrices.aspx/FetchYahooFinanceSingleDay?S=" + Instrument.Symbol,
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];
if (dataSeries.meta) {
Instrument.SingleDayPreviousClose = dataSeries.meta.previousClose;
Instrument.SingleDayStartDate = dataSeries.meta.currentTradingPeriod.regular.start * 1000;
Instrument.SingleDayEndDate = dataSeries.meta.currentTradingPeriod.regular.end * 1000;
Instrument.GMTOffset = dataSeries.meta.gmtoffset;
Instrument.InstrumentType = dataSeries.meta.instrumentType;
Instrument.Currency = dataSeries.meta.currency;
Instrument.CurrentPrice = dataSeries.meta.regularMarketPrice;
}
if (response.chart.result[0].timestamp) {
let ignoredEntries = 0;
for (let i = 0; i < dataSeries.timestamp.length; i++) {
let quote = dataSeries.indicators.quote[0];
if (quote.open[i] == null || quote.high[i] == null || quote.low[i] == null || quote.close[i] == null || quote.volume[i] == null) {
ignoredEntries++;
} else {
newData.push({ DT: dataSeries.timestamp[i] * 1000, open: quote.open[i], high: quote.high[i], low: quote.low[i], close: quote.close[i], volume: quote.volume[i] });
}
}
if (ignoredEntries > 0) {
//console.warn("Ignored " + ignoredEntries.toString() + " entries (" + newData.length.toString() + " OK) with null values for " + Instrument.Symbol);
}
if (newData.length > 0) {
vars.fetchTiming.lastSuccessfulFetch = new Date();
}
if (!Instrument.SingleDayData || Instrument.SingleDayData[0].DT != newData[0].DT || newData.length > Instrument.SingleDayData.length) {
Instrument.SingleDayData = newData;
createSingleDayChart(Instrument);
//updateHoldingsTable();
//updateAnalysisTable();
tablesUpdateTimings.updateNeeded = true;
}
Instrument.SingleDayOpenPrice = dataSeries.indicators.quote[0].open[0];
$('#divCurrentValue' + Instrument.DisplayOrder).html(getCurrentValueContent(Instrument));
if (Instrument.InstrumentType == "CURRENCY") {
Currencies.updateSymbol(Instrument.Symbol, Instrument.InstrumentName, Instrument.CurrentPrice);
}
} else {
//console.warn("No timestamp series received for symbol: " + Instrument.Symbol);
//logWarning("No timestamp series received for symbol: " + Instrument.Symbol);
logWarning({ MSG: "No timestamp series received for symbol: " + Instrument.Symbol, response: response });
}
} else {
//console.info("No data received for symbol: " + Instrument.Symbol);
//logInfo("No data received for symbol: " + Instrument.Symbol);
logWarning({ MSG: "No data received for symbol: " + Instrument.Symbol, response: response });
}
},
failure: function (response) {
vars.fetchTiming.lastFetchDuration = (new Date().getTime()) - vars.fetchTiming.lastFetchStartTime;
console.error("getYahooSingleDayData error:");
console.info(response);
}
});
}
function getLseSingleDayData(Instrument) {
//console.info('getLseSingleDayData: ' + Instrument.Symbol);
Instrument.lastFetchedSingleDay = new Date().getTime();
let fromDate = new Date().setHours(0, 0, 0);
let toDate = new Date().getTime();
vars.fetchTiming.lastFetchStartTime = new Date().getTime();
$.ajax({
type: "POST",
contentType: "application/json",
url: "SharePrices.aspx/FetchLseSingleDay?S=" + Instrument.Symbol,
dataType: "json",
data: JSON.stringify({ Symbol: Instrument.Symbol, FromDate: fromDate, ToDate: toDate }),
success: function (response) {
vars.fetchTiming.lastFetchDuration = (new Date().getTime()) - vars.fetchTiming.lastFetchStartTime;
console.info("getLseSingleDayData: " + JSON.stringify(response));
},
failure: function (response) {
vars.fetchTiming.lastFetchDuration = (new Date().getTime()) - vars.fetchTiming.lastFetchStartTime;
console.error("getLseSingleDayData error:");
console.info(response);
}
});
}
function submitNewDailyData(Instrument, NewData) {
function indexOfDT(DT) {
for (var i = 0; i < Instrument.DailyData.length; i++) {
if (Instrument.DailyData[i].DT == DT) {
return i;
}
}
return -1;
}
//console.info("submitNewDailyData");
let currentMaxDT = Instrument.DailyData.length ? Instrument.DailyData[Instrument.DailyData.length - 1].DT : 0;
let currentMinDT = Instrument.DailyData.length ? Instrument.DailyData[0].DT : 0;
//Add the new data to the DailyData array
let resortRequired = false;
for (let i = 0; i < NewData.length; i++) {
NewData[i].DT = new Date(NewData[i].DT).setHours(0, 0, 0, 0); //Remove the hours/mins/secs from this DT as there should only be one entry per day
let x = indexOfDT(NewData[i].DT);
if (x >= 0) {
//Don't update the existing entry for this DT as it may have been manually overridden in the DB
//Instrument.DailyData[x] = NewData[i];
} else {
//This is a new entry
if (!Instrument.DailyData) {
Instrument.DailyData = [];
}
try {
Instrument.DailyData.push(NewData[i]);
}
catch {
console.error({"Instrument.DailyData.push error. Instrument: ": Instrument});
}
if (NewData[i].DT <= currentMaxDT) {
resortRequired = true;
}
}
}
//Re-sort the DailyData by date if required
if (resortRequired) {
Instrument.DailyData.sort(function compare(a, b) {
if (a.DT < b.DT) {
return -1;
}
if (a.DT > b.DT) {
return 1;
}
return 0;
}
);
}
$.ajax({
type: "POST",
contentType: "application/json",
url: "SharePrices.aspx/SubmitDailyData?S=" + Instrument.Symbol,
dataType: "json",
data: JSON.stringify({Symbol: Instrument.Symbol, DailyPrices: NewData}),
success: function (response) {
//console.info(' SubmitDailyData success: ' + Instrument.Symbol + ' - ' + JSON.stringify(response));
},
failure: function (response) { console.error("SubmitDailyData error: " + JSON.stringify(response)); }
});
}
function submitNewIntradayData(Instrument, NewData) {
//console.info("submitNewIntradayData");
$.ajax({
type: "POST",
contentType: "application/json",
url: "SharePrices.aspx/SubmitIntradayData?S=" + Instrument.Symbol,
dataType: "json",
//data: JSON.stringify({ Symbol: Instrument.Symbol, DailyPrices: NewData }),
data: JSON.stringify({ Symbol: Instrument.Symbol, DailyPrices: NewData, GMTOffset: Instrument.GMTOffset, Currency: Instrument.Currency, CurrentPrice: Instrument.CurrentPrice, InstrumentType: Instrument.InstrumentType, TradeDayStart: Instrument.SingleDayStartDate, TradeDayEnd: Instrument.SingleDayEndDate }),
success: function (response) {
//console.info('SubmitIntradayData success: ' + Instrument.Symbol + ' - ' + JSON.stringify(response));
},
failure: function (response) { console.error("SubmitIntradayData error: " + JSON.stringify(response)); }
});
//Add new data to Instrument
let currentSummary = {};
if (!Instrument.IntradayData || Instrument.IntradayData.length<1) {
Instrument.IntradayData = [];
currentSummary = { minDate: new Date().getTime(), maxDate: 0 };
} else {
currentSummary = { minDate: Instrument.IntradayData[0].DT, maxDate: Instrument.IntradayData[Instrument.IntradayData.length-1].DT };
}
let dataToAddToFront = [];
for (let i = 0; i < NewData.length; i++) {
if (NewData[i].DT < currentSummary.minDate) {
dataToAddToFront.push(NewData[i]);
} else {
if (NewData[i].DT > currentSummary.maxDate) Instrument.IntradayData.push(NewData[i]);
}
}
for (let i = dataToAddToFront.length - 1; i >= 0; i--) {
Instrument.IntradayData.unshift(dataToAddToFront[i]);
}
}
function updateTables() {
//Only update the tables every 5 seconds (unless lastTablesUpdate has been reset)
if (tablesUpdateTimings.updateNeeded && tablesUpdateTimings.lastUpdate + tablesUpdateTimings.timeBetweenUpdates < new Date().getTime()) {
tablesUpdateTimings.lastUpdate = new Date().getTime();
createHoldingsTable(Instruments, vars);
createAccountsTables();
createAnalysisTable();
createIndexMarquee();
updateCurrenciesSpan();
}
}
function createAccountsTables() {
function getAccounts() {
let accounts = [];
function indexOfAccount(accountName) { return accounts.map(function (item) { return item.accountName; }).indexOf(accountName); };
function getAccount(accountName) { return accounts[indexOfAccount(accountName)]; };
function addAccountHolding(instrument, exchangeRate, holding) {
let a = getAccount(holding.AccountName);
if (!a) {
a = { accountName: holding.AccountName, holdings: [], totalCostGBP: 0, totalValueGBP: 0, totalGainGBP: 0, totalCashGBP: 0 };
accounts.push(a);
}
let h = {
instrumentName: instrument.InstrumentName,
symbol: instrument.Symbol,
instrumentType: instrument.InstrumentType,
currency: instrument.Currency,
purchaseDate: holding.PurchaseDate,
purchasePricePerUnit: holding.PurchasePricePerUnit,
noUnits: holding.NoUnits,
cost: holding.NoUnits * holding.PurchasePricePerUnit
}
//if (exchangeRate) {
//h.costGBP = h.cost / exchangeRate;
h.costGBP = holding.BookCostGBP;
if (instrument.InstrumentType != 'CURRENCY') {
a.totalCostGBP += holding.BookCostGBP;
}
//}
if (instrument.CurrentPrice) {
h.currentPrice = instrument.CurrentPrice;
h.currentValue = holding.NoUnits * instrument.CurrentPrice;
h.gain = h.currentValue - h.cost;
if (exchangeRate) {
h.currentValueGBP = h.currentValue / exchangeRate;
//h.gainGBP = h.gain / exchangeRate;
h.gainGBP = h.currentValueGBP - h.costGBP;
if (instrument.InstrumentType != 'CURRENCY') {
a.totalGainGBP += h.gainGBP;
a.totalValueGBP += h.currentValueGBP;
}
}
}
if (instrument.InstrumentType == 'CURRENCY') {
//Don't add cash holdings - just add the cash value to the account
a.totalCashGBP += h.currentValueGBP;
} else {
a.holdings.push(h);
}
}
for (let ix = 0; ix < Instruments.Data.length; ix++) {
let i = Instruments.Data[ix];
let exchangegRate = Instruments.GetExchangeRate(i.Currency, 'GBP');
for (let hx = 0; hx < i.Holdings.length; hx++) {
let h = i.Holdings[hx];
if (h.SoldDate == null) {
addAccountHolding(i, exchangegRate, h);
}
}
}
return accounts;
}
let accounts = getAccounts();
//Sort the accounts array by accountName
accounts.sort(function (a, b) {
if (a.accountName < b.accountName) return -1;
if (a.accountName > b.accountName) return 1;
return 0;
});
//let summaryTable = "<table class='accounts' id='accountsSummaryTable'><thead><tr><th>Account Name</th><th>No. Holdings</th><th>Total Cost (GBP)</th><th>Current Value</th><th>Total Gain</th></tr></thead><tbody>";
let summaryTable = "<table class='accounts' id='accountsSummaryTable'><thead><tr>" +
"<th>Account Name</th>" +
"<th>No. Holdings</th>" +
"<th>Total Cost (GBP)</th>" +
"<th>Total Gains (GBP)</th>" +
"<th>Total Losses (GBP)</th>" +
"<th>Current Investements Value</th>" +
"<th>NET Gain</th>" +
"<th>Cash Balance GBP</th>" +
"<th>Total Value GBP</th>" +
"</tr></thead><tbody>";
let accountsTables = ''
let tableIDs = ['accountsSummaryTable'];
let hideEmptyAccounts = $('#hideEmptyAccounts').prop('checked');
let altRow = 1;
for (let ax = 0; ax < accounts.length; ax++) {
let a = accounts[ax];
let accountGainsGBP = 0;
let accountLossesGBP = 0;
let accountValueGBP = 0;
let profitOrLoss = a.totalGainGBP < 0 ? 'loss' : 'profit';
if (a.holdings.length > 0) {
altRow = !altRow;
accountsTables += '<br>' + a.accountName + '<br>'
let tableID = 'accountTable' + ax;
tableIDs.push(tableID);
accountsTables += '<table class="accounts" id="' + tableID + '">' +
'<thead><tr>' +
'<th>Name</th>' +
'<th>Symbol</th>' +
'<th>Buy Date</th>' +
'<th>No Units</th>' +
'<th>Buy Price</th>' +
'<th>Current Price</th>' +
'<th>Book Cost</th>' +
'<th>Approx Book Cost £</th>' +
'<th>Current Value</th>' +
'<th>Current Value £</th>' +
'<th>Gain</th>' +
'<th>Gain £</th>' +
'<th>Gain %</th>' +
'</tr></thead><tbody>';
let accountAltRow = 1;
for (let hx = 0; hx < a.holdings.length; hx++) {
let h = a.holdings[hx];
accountAltRow = !accountAltRow;
let holdingProfitOrLoss = h.gain < 0 ? 'loss' : 'profit';
let holdingGBPProfitOrLoss = h.gainGBP < 0 ? 'loss' : 'profit';
if (h.gainGBP > 0) {
accountGainsGBP += h.gainGBP;
} else {
accountLossesGBP += h.gainGBP;
}
accountValueGBP += h.currentValueGBP;
accountsTables += '<tr' + (accountAltRow ? ' class="altShareRow"' : '') + '>' +
'<td>' + h.instrumentName + '</td>' +
'<td>' + h.symbol + '</td>' +
'<td>' + new Date(h.purchaseDate).yyyymmdd() + '</td>' +
'<td class="num">' + h.noUnits + '</td>' +
'<td class="num">' + formatAmount(h.purchasePricePerUnit, h.currency, 2) + '</td>' +
'<td class="num">' + formatAmount(h.currentPrice, h.currency, 2) + '</td>' +
'<td class="num">' + (h.currency == 'GBp' ? formatAmount(h.cost / 100, 'GBP', 2) : formatAmount(h.cost, h.currency, 2)) + '</td>' +
'<td class="num">' + formatAmount(h.costGBP, 'GBP', 2) + '</td>' +
'<td class="num">' + (h.currency == 'GBp' ? formatAmount(h.currentValue / 100, 'GBP', 2) : formatAmount(h.currentValue, h.currency, 2)) + '</td>' +
'<td class="num">' + formatAmount(h.currentValueGBP, 'GBP', 2) + '</td>' +
'<td class="num ' + holdingProfitOrLoss + '">' + formatAmount(h.gain, h.currency, 2) + '</td>' +
'<td class="num ' + holdingGBPProfitOrLoss + '">' + formatAmount(h.gainGBP, 'GBP', 2) + '</td>' +
//'<td class="num ' + holdingProfitOrLoss + '">' + ((h.gain / h.cost) * 100).toFixed(1) + '%</td>' +
'<td class="num ' + holdingGBPProfitOrLoss + '">' + ((h.gainGBP / h.costGBP) * 100).toFixed(1) + '%</td>' +
'</tr>';
}
//Add the Total Gains
accountAltRow = !accountAltRow;
accountsTables += '<tr' + (accountAltRow ? ' class="altShareRow"' : '') + '>' +
'<td class="num" colspan="11">Total Gains</td>' +
'<td class="num profit">' + formatAmount(accountGainsGBP, 'GBP', 2) + '</td>' +
'<td class="num">&nbsp;</td>' +
'</tr>';
//Add the Total Losses
accountAltRow = !accountAltRow;
accountsTables += '<tr' + (accountAltRow ? ' class="altShareRow"' : '') + '>' +
'<td class="num" colspan="11">Total Losses</td>' +
'<td class="num loss">' + formatAmount(accountLossesGBP, 'GBP', 2) + '</td>' +
'<td class="num">&nbsp;</td>' +
'</tr>';
//Add the NET Gains summary line
accountAltRow = !accountAltRow;
accountsTables += '<tr' + (accountAltRow ? ' class="altShareRow"' : '') + '>' +
'<td class="num" colspan="9">Account Value:</td>' +
'<td class="num ' + ((accountGainsGBP+accountLossesGBP) > 0 ? 'profit' : 'loss') + '">' + formatAmount(accountValueGBP, 'GBP', 2) + '</td>' +
'<td class="num">NET Gain</td>' +
'<td class="num ' + ((accountGainsGBP + accountLossesGBP) > 0 ? 'profit' : 'loss') + '">' + formatAmount(accountGainsGBP + accountLossesGBP, 'GBP', 2) + '</td>' +
'<td class="num">&nbsp;</td>' +
'</tr>';
accountsTables += '</tbody></table>';
}
//Add an entry to the summary table
if (a.totalValueGBP + a.totalCashGBP > 0 || !hideEmptyAccounts) {
summaryTable += "<tr" + (altRow ? ' class="altShareRow"' : '') + "><td>" + a.accountName + "</td>" +
"<td>" + a.holdings.length + "</td>" +
"<td class='num'>" + formatAmount(a.totalCostGBP || 0, 'GBP', 2) + "</td>" +
"<td class='num'>" + formatAmount(accountGainsGBP, 'GBP', 2) + "</td>" +
"<td class='num'>" + formatAmount(accountLossesGBP, 'GBP', 2) + "</td>" +
"<td class='num'>" + formatAmount(a.totalValueGBP || 0, 'GBP', 2) + "</td>" +
"<td class='num " + profitOrLoss + "'>" + formatAmount(a.totalGainGBP || 0, 'GBP', 2) + "</td>" +
"<td class='num'>" + formatAmount(a.totalCashGBP || 0, 'GBP', 2) + "</td>" +
"<td class='num'>" + formatAmount(a.totalValueGBP + a.totalCashGBP || 0, 'GBP', 2) + "</td>" +
"</tr>";
}
}
summaryTable += "</tbody></table>";
$("#accountsSummaryDiv").html(summaryTable + '<br>' + accountsTables);
}
function createAnalysisTable() {
function getHighestPriceBeforeDate(Instrument, maxDT) {
if (Instrument.DailyData) {
let r = { Price: 0, DT: 0 };
let dt = 0;
for (let i = 0; i < Instrument.DailyData.length && dt < maxDT; i++) {
let price = Instrument.DailyData[i];
dt = price.DT;
if (dt <= maxDT && price.high > r.Price) {
r.Price = price.high;
r.DT = dt;
}
}
return r;
} else {
return { Price: NaN, DT: NaN };
}
}
function getHighestPriceBetweenDates(Instrument, minDT, maxDT) {
if (Instrument.DailyData) {
let r = { Price: 0, DT: 0 };
let dt = 0;
for (let i = 0; i < Instrument.DailyData.length && dt < maxDT; i++) {
let price = Instrument.DailyData[i];
dt = price.DT;
if (dt >= minDT && dt <= maxDT && price.high > r.Price) {
r.Price = price.high;
r.DT = dt;
}
}
return r;
} else {
return { Price: NaN, DT: NaN };
}
}
function getLowestPriceAfterDate(Instrument, minDT) {
if (Instrument.DailyData) {
let r = { Price: 0, DT: 0 };
let dt = 0;
let i = 0;
let DD = Instrument.DailyData;
while (dt < minDT && i < DD.length - 1) {
i++;
dt = DD[i].DT;
}
if (i < DD.length) {
r.Price = DD[i].low;
r.DT = DD[i].DT;
while (i < DD.length - 1) {
i++;
if (DD[i].low < r.Price) {
r.Price = DD[i].low;
r.DT = DD[i].DT;
}
}
}
return r;
} else {
return { Price: NaN, DT: NaN };
}
}
function getEquities() {
function getHoldingsValueGBP(Instrument) {
let noUnits = 0;
if (Instrument.CurrentPrice) {
let c = Instruments.GetCurrentHoldings(Instrument);
for (let x = 0; x < c.length; x++) {
noUnits += c[x].NoUnits;
}
let exchangeRate = Instruments.GetExchangeRate(Instrument.Currency, 'GBP');
return noUnits * Instrument.CurrentPrice / exchangeRate;
} else {
return 0;
}
}
let totalHoldingsValueGBP = 0;
let r = [];
for (let n = 0; n < Instruments.Data.length; n++) {
let i = Instruments.Data[n];
try {
if (i.InstrumentType == 'EQUITY') {
let prePandemicHigh = getHighestPriceBetweenDates(i, new Date('2020-01-01'), pandemicCrashStartDate);
let postPandemicLow = getLowestPriceAfterDate(i, pandemicCrashStartDate);
let dilutedTarget = prePandemicHigh.Price * i.PostPandemicDilution;
let currentHoldingsValueGBP = getHoldingsValueGBP(i);
let remainingDilutedPotential = ((dilutedTarget - i.CurrentPrice) / i.CurrentPrice) * 100;
totalHoldingsValueGBP += currentHoldingsValueGBP;
r.push({
prePandemicHigh: prePandemicHigh,
postPandemicLow: postPandemicLow,
displayOrder: i.DisplayOrder,
instrumentName: i.InstrumentName,
symbol: i.Symbol,
currency: i.Currency,
currentPrice: i.CurrentPrice,
fallPercent: ((postPandemicLow.Price / prePandemicHigh.Price) - 1) * 100,
postPandemicDilution: i.PostPandemicDilution * 100,
dilutedTarget: dilutedTarget,
currentDiscount: ((i.CurrentPrice / prePandemicHigh.Price) - 1) * 100,
potentialDilutedRebound: (((prePandemicHigh.Price * i.PostPandemicDilution) / postPandemicLow.Price) - 1) * 100,
retracementPercent: ((i.CurrentPrice - postPandemicLow.Price) / (prePandemicHigh.Price - postPandemicLow.Price)) * 100,
remainingPotential: ((prePandemicHigh.Price - i.CurrentPrice) / i.CurrentPrice) * 100,
remainingDilutedPotential: remainingDilutedPotential,
currentHoldingsValueGBP: currentHoldingsValueGBP,
possibleRemainingProfit: currentHoldingsValueGBP * remainingDilutedPotential / 100
});
}
}
catch (err) {
console.error("Error analysing " + i.Symbol + ": " + err.message);
}
}
return { totalHoldingsValueGBP: totalHoldingsValueGBP, equities: r };
}
function createTable() {
let t = $("#tblAnalysis");
if (t.length) {
return t;
} else {
let tbl = '<table id="tblAnalysis" class="analysis"><thead><tr>' +
'<th>#</th>' +
'<th>Name</th>' +
/*'<th>Symbol</th>' +*/
/*'<th>Currency</th>' +*/
'<th>Pre-pandemic peak</th>' +
'<th>Bottom</th>' +
'<th>% Fall</th>' +
'<th>Current Price</th>' +
'<th>Diluted Value %</th>' +
'<th>Diluted Target</th>' +
'<th>Current Discount</th>' +
'<th>Potential<br>Diluted<br>Rebound</th>' +
'<th>Retracement %</th>' +
'<th>Remaining Potential</th>' +
'<th>Remaining<br>Diluted<br>Potential</th>' +
'<th>Current Holding</th>' +
'<th>Current Allocation %</th>' +
'<th>Possible Remaining Profit</th>' +
'</tr></thead><tbody id="tbAnal"><tfoot><tr><td colspan="16">&nbsp;</td></tr><tr><td colspan="15">&nbsp;</td><td id="tdPossibleRemainingProfit" class="num"></td></tr></tfoot></tbody></table>';
$('#analDiv').html(tbl);
$("#tblAnalysis").tablesorter();
}
}
function addTableRow(id, row) {
let r = $("#" + id);
if (r.length) {
$("#" + id).replaceWith(row); //Update existing row
} else {
$("#tbAnal").append(row);
}
}
//analysisTableTimings.LastUpdateTime = new Date().getTime();
let pandemicCrashStartDate = new Date("2020-02-23").getTime();
//Calculate values for all equities
let tmp = getEquities();
let equities = tmp.equities;
let totalHoldingsValueGBP = tmp.totalHoldingsValueGBP;
createTable();
let altRow = 1;
let totalPossibleRemainingProfit = 0;
for (let n = 0; n < equities.length; n++) {
altRow = !altRow;
let e = equities[n];
totalPossibleRemainingProfit += e.possibleRemainingProfit > 0 ? e.possibleRemainingProfit : 0;
try {
let rowID = "analRow" + e.displayOrder
let row = '<tr' + (altRow ? ' class="altShareRow"' : '') + ' id="' + rowID + '">' +
'<td>' + e.displayOrder + '</td>' +
'<td>' + e.instrumentName + '</td>' +
/*'<td><a target="_blank" href="https://uk.finance.yahoo.com/quote/' + e.symbol + '">' + e.symbol + '</a>' + '</td>' +*/
/*'<td>' + e.currency + '</td>' +*/
'<td class="num" title="' + new Date(e.prePandemicHigh.DT).yyyymmddhhmmss() + '">' + formatAmount(e.prePandemicHigh.Price, e.currency, 2) + '</td>' + //Pre-pandemic peak
'<td class="num" title="' + new Date(e.postPandemicLow.DT).yyyymmddhhmmss() + '">' + formatAmount(e.postPandemicLow.Price, e.currency, 2) + '</td>' + //Bottom
'<td class="num">' + e.fallPercent.toFixed(2) + '%</td>' + //% Fall
'<td class="num">' + formatAmount(e.currentPrice, e.currency, 2) + '</td>' + //Current Price
'<td class="num">' + e.postPandemicDilution + '%</td>' + //Diluted Value
'<td class="num">' + formatAmount(e.dilutedTarget, e.currency, 2) + '</td>' + //Diluted target
'<td class="num">' + e.currentDiscount.toFixed(2) + '%</td>' + //Current Discount
'<td class="num">' + e.potentialDilutedRebound.toFixed(0) + '%</td>' + //Potential Diluted Rebound
'<td class="num">' + e.retracementPercent.toFixed(1) + '%</td>' + //Retracement %
'<td class="num">' + e.remainingPotential.toFixed(1) + '%</td>' + //Remaining Potential
'<td class="num">' + e.remainingDilutedPotential.toFixed(1) + '%</td>' + //Remaining Diluted Potential
'<td class="num">' + (e.currentHoldingsValueGBP > 0 ? formatAmount(e.currentHoldingsValueGBP, 'GBP', 2) : '') + '</td>' + //Current Holding
'<td class="num">' + (e.currentHoldingsValueGBP > 0 ? ((e.currentHoldingsValueGBP / totalHoldingsValueGBP) * 100).toFixed(2) + '%' : '') + '</td>' + //Current Allocation %
'<td class="num">' + (e.possibleRemainingProfit > 0 ? formatAmount(e.possibleRemainingProfit, 'GBP', 2) : '') + '</td>' + //Possible Remaining Profit
'</tr>';
addTableRow(rowID, row);
}
catch (err) {
console.error("Error adding " + e.symbol + " to analysis table: " + err.message);
}
}
$("#tdPossibleRemainingProfit").html(formatAmount(totalPossibleRemainingProfit, 'GBP', 2));
var resort = true;
$("#tblAnalysis").trigger("update", [resort]);
}
function createIndexMarquee() {
let t = [];
for (var ix = 0; ix < Instruments.Data.length; ix++) {
let i = Instruments.Data[ix];
//if (i.InstrumentType == 'INDEX' || i.InstrumentType == 'FUTURE') {
if (i.ShowInMarquee == 'A') {
let openOrClosed = Instruments.MarketIsOpen(i) == 1 ? 'open' : 'closed';
if (i.SingleDayPreviousClose) {
let percentChange = ((i.CurrentPrice / i.SingleDayPreviousClose) - 1) * 100;
if (percentChange < 0) {
t.push('<font class="' + openOrClosed + '">' + i.DisplayName + '<br/>' + i.CurrentPrice + ' </font><font class="loss">' + percentChange.toFixed(1) + '%</font>');
} else {
t.push('<font class="' + openOrClosed + '">' + i.DisplayName + '<br/>' + i.CurrentPrice + ' </font><font class="profit">+' + percentChange.toFixed(1) + '%</font>');
}
} else {
t.push('<font class="' + openOrClosed + '">' + i.DisplayName + '<br/>' + i.CurrentPrice + '</font>');
}
}
}
if (t.length > 0) {
//t.push(new Date().yyyymmddhhmmss());
let newContent = "<table style='width:100%;'><tr style='width:100%;'><td style='text-align:center;'>" + t.join("</td><td style='text-align:center;'>") + "</td></tr></table>";
indexMarquee.container.html(newContent);
}
}
function updateCurrenciesSpan() {
var c = [];
for (var i = 0; i < Currencies.Data.length; i++) {
//c.push(Currencies.Data[i].Name + ": " + Currencies.Data[i].CurrentPrice.toFixed(4));
c.push("<a target='_blank' href='" + Currencies.Data[i].URL + "'>" + Currencies.Data[i].Name + "</a>: " + Currencies.Data[i].CurrentPrice.toFixed(4));
}
if (c.length > 0) {
$("#spnCurrencies").html('<font class="open">' + c.join('&emsp;') + '</font>');
}
}
export 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)); }
});
}
export function showModalDialog_AddHolding(symbol) {
//function showModalDialog_AddHolding(span) {
function GetAccountsDropdown() {
let dd = '<select id="addHoldingAccountName"><option value="">Select account...</option>';
for (let i = 0; i < Instruments.Accounts.length; i++) {
dd += '<option value="' + Instruments.Accounts[i].AccountName + '">' + Instruments.Accounts[i].AccountName + '</option>';
}
dd += '</select>';
return dd;
}
function GetTimeDropdowns() {
let dd = '<select id="addHoldingTimeHour"><option value="">Hour</option>';
for (let i = 0; i < 24; i++) {
dd += '<option value="' + i.toString().padStart(2, '0') + '">' + i.toString().padStart(2, '0') + '</option>';
}
dd += '</select>';
dd += '<select id="addHoldingTimeMinute"><option value="">Min</option>';
for (let i = 0; i < 60; i++) {
dd += '<option value="' + i.toString().padStart(2, '0') + '">' + i.toString().padStart(2, '0') + '</option>';
}
dd += '</select>';
return dd;
}
function GetCurrencyDropdown() {
let dd = '<select id="newHoldingPurchaseCurrency"><option value="">Currency</option>';
for (let i = 0; i < Instruments.Data.length; i++) {
if (Instruments.Data[i].InstrumentType == 'CURRENCY') {
dd += '<option value="' + Instruments.Data[i].Symbol + '">' + Instruments.Data[i].DisplayName + '</option>';
}
}
dd += '</select>';
return dd;
}
//let symbol = $(span).attr('data-symbol');
let i = Instruments.GetSymbol(symbol);
let modal = document.getElementById("modalDialog");
$("#modalTitle").html("Add Holding");
let content = '<div><table><tr><th>Instrument Name</th><td>' + i.InstrumentName + '</td></tr>' +
'<tr><th>Symbol</th><td>' + symbol + '</td></tr>' +
'<tr><th>Account</th><td>' + GetAccountsDropdown() + '</td></tr>' +
'<tr><th>Current price</th><td>' + (i.CurrentPrice ? formatAmount(i.CurrentPrice, i.Currency, 2) : '') + '</td></tr>' +
'<tr><th>Purchase price / unit</th><td><input type="text" id="newHoldingPrice" value="' + (i.CurrentPrice ? i.CurrentPrice.toString() : '') + '" /></td></tr>' +
'<tr><th>No units</th><td><input type="text" id="newHoldingNoUnits" /></td></tr>' +
'<tr><th>Total cost in instrument currency</th><td><input type="text" id="newHoldingTotalCost" /></td></tr>' +
'<tr><th>Purchase Currency</th><td>' + GetCurrencyDropdown() + '</td></tr>>' +
'<tr><th>Book cost in purchase currency</th><td><input type="text" id="newHoldingBookCost" /></td></tr>' +
'<tr><th>Purchase date</th><td><input type="text" id="newHoldingDate" value="' + new Date().yyyymmdd() + '" />' + GetTimeDropdowns() + '</td></tr>' +
'</table></div><br>' +
'<div><span id="newHoldingOKButton" class="ok-button-disabled">OK</span></div>';
$("#modalContentDiv").html(content);
$("#newHoldingDate").datepicker({ dateFormat: 'yy-mm-dd', onSelect: function () { ValidateNewHolding(symbol) }});
$("#modalDialog .close").click(function () { modal.style.display = "none"; });
window.onclick = function (event) { // When the user clicks anywhere outside of the modal, close it
if (event.target == modal) {
modal.style.display = "none";
}
}
$("#newHoldingPrice").bind('input', function () {
$("#newHoldingOKButton").removeClass("ok-button").addClass("ok-button-disabled").unbind();
try {
let noUnits = parseFloat($("#newHoldingNoUnits").val());
let price = parseFloat($("#newHoldingPrice").val());
$("#newHoldingTotalCost").val((noUnits * price).toFixed(2));
ValidateNewHolding(symbol);
} catch (err) { }
});
$("#newHoldingNoUnits").bind('input', function () {
$("#newHoldingOKButton").removeClass("ok-button").addClass("ok-button-disabled").unbind();
try {
let noUnits = parseFloat($("#newHoldingNoUnits").val());
let price = parseFloat($("#newHoldingPrice").val());
$("#newHoldingTotalCost").val((noUnits * price).toFixed(2));
ValidateNewHolding(symbol);
} catch (err) { }
});
$("#newHoldingTotalCost").bind('input', function () {
$("#newHoldingOKButton").removeClass("ok-button").addClass("ok-button-disabled").unbind();
try {
let noUnits = parseFloat($("#newHoldingNoUnits").val());
let totalCost = parseFloat($("#newHoldingTotalCost").val());
//$("#newHoldingPrice").val((totalCost / noUnits).toFixed(4));
$("#newHoldingPrice").val((totalCost / noUnits));
ValidateNewHolding(symbol);
} catch (err) { }
});
$("#newHoldingDate").bind('input', function () {
ValidateNewHolding(symbol);
});
$("#addHoldingTimeHour").bind('input', function () {
ValidateNewHolding(symbol);
});
$("#addHoldingTimeMinute").bind('input', function () {
ValidateNewHolding(symbol);
});
$("#newHoldingPurchaseCurrency").bind('input', function () {
ValidateNewHolding(symbol);
});
$("#newHoldingBookCost").bind('input', function () {
ValidateNewHolding(symbol);
});
$("#addHoldingAccountName").bind('input', function () {
ValidateNewHolding(symbol);
});
//Display the dialog
modal.style.display = "block";
return false; //Prevent click event propogating to parent
}
function ValidateNewHolding(symbol) {
$("#newHoldingOKButton").removeClass("ok-button").addClass("ok-button-disabled").unbind();
try {
let accountName = $("#addHoldingAccountName").val();
let noUnits = parseFloat($("#newHoldingNoUnits").val());
let price = parseFloat($("#newHoldingPrice").val());
let purchaseDate = $("#newHoldingDate").val();
let purchaseHour = $("#addHoldingTimeHour").val();
let purchaseMinute = $("#addHoldingTimeMinute").val();
let purchaseCurrency = $("#newHoldingPurchaseCurrency").val();
let bookCost = $("#newHoldingBookCost").val();
if (accountName != '' && accountName != 'Select account...' && noUnits > 0 && price > 0 && purchaseDate != '' && purchaseHour != '' && purchaseHour != 'Hour' && purchaseMinute != '' && purchaseMinute != 'Min' && purchaseCurrency != 'Currency' && bookCost > 0) {
let purchaseDT = new Date(purchaseDate + ' ' + purchaseHour + ':' + purchaseMinute);
if (purchaseDT <= new Date()) {
$("#newHoldingOKButton").removeClass("ok-button-disabled").addClass("ok-button").unbind().bind('click', function () { $("#modalDialog").css("display", "none"); AddHolding(accountName, symbol, noUnits, price, purchaseDT, purchaseCurrency, bookCost) });
}
}
} catch (err) { }
}
function AddHolding(accountName, symbol, noUnits, purchasePricePerUnit, purchaseDate, purchaseCurrency, bookCost) {
console.info("AddHolding: " + accountName + ", " + symbol + ", " + noUnits.toString() + ", " + purchasePricePerUnit.toFixed(4) + ", " + new Date(purchaseDate).yyyymmdd());
$.ajax({
type: "POST",
contentType: "application/json",
url: "SharePrices.aspx/AddHolding",
dataType: "json",
data: JSON.stringify({ AccountName: accountName, Symbol: symbol, NoUnits: noUnits, PurchasePricePerUnit: purchasePricePerUnit, PurchaseDate: new Date(purchaseDate).getTime(), PurchaseCurrency: purchaseCurrency, BookCostInPurchaseCurrency: bookCost }),
success: function (response) {
if (response.HoldingID) {
let i = Instruments.GetSymbol(symbol);
if (!i.Holdings) i.Holdings = [];
i.Holdings.push(response);
let tr = $("#shareRow" + i.DisplayOrder.toString());
tr.html(createSharesTableRow(i));
createLongChart(i);
createMidChart(i);
createShortChart(i);
createSingleDayChart(i);
} else {
console.error("addInstrument response error: " + JSON.stringify(response));
}
},
failure: function (response) { console.error("addInstrument AJAX error: " + JSON.stringify(response)); }
});
//Update the account cash balance to reflect the purchase
let balance = 0 - bookCost;
let curr = Instruments.GetAccountCurrencyHolding(accountName, Instruments.GetSymbol(purchaseCurrency));
if (curr) {
balance += curr.NoUnits;
}
$.ajax({
type: "POST",
contentType: "application/json",
url: "SharePrices.aspx/SetAccountCashValue",
dataType: "json",
data: JSON.stringify({ AccountName: accountName, Currency: purchaseCurrency, Value: balance }),
success: function (response) {
console.info("SetAccountCashValue success: " + JSON.stringify(response));
let c = Instruments.GetSymbol(response.Symbol);
let ch = Instruments.GetAccountCurrencyHolding(response.AccountName, c);
if (ch) {
ch.NoUnits = response.NoUnits;
} else {
c.Holdings.push(response);
}
},
failure: function (response) { console.error("SetAccountCashValue error: " + JSON.stringify(response)); }
});
}
export function showModalDialog_SoldHolding(holdingID) {
function GetTimeDropdowns() {
let dd = '<select id="soldHoldingTimeHour"><option value="">Hour</option>';
for (let i = 0; i < 24; i++) {
dd += '<option value="' + i.toString().padStart(2, '0') + '">' + i.toString().padStart(2, '0') + '</option>';
}
dd += '</select>';
dd += '<select id="soldHoldingTimeMinute"><option value="">Min</option>';
for (let i = 0; i < 60; i++) {
dd += '<option value="' + i.toString().padStart(2, '0') + '">' + i.toString().padStart(2, '0') + '</option>';
}
dd += '</select>';
return dd;
}
function GetCurrencyDropdown() {
let dd = '<select id="soldHoldingCurrency"><option value="">Currency</option>';
for (let i = 0; i < Instruments.Data.length; i++) {
if (Instruments.Data[i].InstrumentType == 'CURRENCY') {
dd += '<option value="' + Instruments.Data[i].Symbol + '">' + Instruments.Data[i].DisplayName + '</option>';
}
}
dd += '</select>';
return dd;
}
let modal = document.getElementById("modalDialog");
$("#modalTitle").html("Sold Holding");
let o = Instruments.GetHolding(holdingID);
let h = o.holding;
let i = o.instrument;
let content = 'Symbol: ' + i.Symbol +
'<br>Name: ' + i.InstrumentName +
'<br>Account: ' + h.AccountName + '<input type="hidden" id="soldHoldingsAccountName" value="' + h.AccountName + '" />' +
'<br>Purchase Date: ' + new Date(h.PurchaseDate).yyyymmddhhmmss() +
'<br>Purchase Price: ' + formatAmount(h.PurchasePricePerUnit, i.Currency, 2) +
'<br>No Units: ' + formatAmount(h.NoUnits, '', 0) +
'<br>Cost: ' + formatAmount(h.NoUnits * h.PurchasePricePerUnit, i.Currency, 2) +
'<br><br>Mark holding as sold?' +
'<br><br>Sold date: <input type="text" id="soldHoldingDate" value="' + new Date().yyyymmdd() + '" />' + GetTimeDropdowns() +
'<br>Currency: ' + GetCurrencyDropdown() + ' Proceeds: <input type="text" id="soldHoldingProceeds">' +
'<br><br><span id="soldHoldingOKButton" class="ok-button-disabled">OK</span>';
$("#modalContentDiv").html(content);
$("#soldHoldingDate").datepicker({ dateFormat: 'yy-mm-dd', onSelect: function () { ValidateSoldHolding(h) } });
$("#soldHoldingDate").bind('input', function () { ValidateSoldHolding(h); });
$("#soldHoldingTimeHour").bind('input', function () { ValidateSoldHolding(h); });
$("#soldHoldingTimeMinute").bind('input', function () { ValidateSoldHolding(h); });
$("#soldHoldingCurrency").bind('input', function () { ValidateSoldHolding(h); });
$("#soldHoldingProceeds").bind('input', function () { ValidateSoldHolding(h); });
$("#modalDialog .close").click(function () { modal.style.display = "none"; });
window.onclick = function (event) { // When the user clicks anywhere outside of the modal, close it
if (event.target == modal) {
modal.style.display = "none";
}
}
//$("#modalDialog .ok-button").click(function () { modal.style.display = "none"; deleteHolding(holdingID); });
//Display the dialog
modal.style.display = "block";
}
function ValidateSoldHolding(holding) {
$("#soldHoldingOKButton").removeClass("ok-button").addClass("ok-button-disabled").unbind();
try {
let holdingID = holding.HoldingID;
let purchaseDT = holding.PurchaseDate;
let soldDate = $("#soldHoldingDate").val();
let soldHour = $("#soldHoldingTimeHour").val();
let soldMinute = $("#soldHoldingTimeMinute").val();
let accountName = $("#soldHoldingsAccountName").val();
let soldCurrency = $("#soldHoldingCurrency").val();
let soldProceeds = parseFloat($("#soldHoldingProceeds").val());
if (soldDate != '' && soldHour != '' && soldHour != 'Hour' && soldMinute != '' && soldMinute != 'Min' && soldCurrency != 'Currency' && soldProceeds > 0) {
let soldDT = new Date(soldDate + ' ' + soldHour + ':' + soldMinute).getTime();
if (soldDT <= new Date().getTime() && soldDT > purchaseDT) {
$("#soldHoldingOKButton").removeClass("ok-button-disabled").addClass("ok-button").unbind().bind('click', function () { $("#modalDialog").css("display", "none"); soldHolding(holdingID, soldDT, accountName, soldCurrency, soldProceeds) });
}
}
} catch (err) { }
}
function soldHolding(holdingID, soldDate, accountName, soldCurrency, soldProceeds) {
//console.info("Sold holding: " + holdingID.toString());
let o = Instruments.GetHolding(holdingID);
let h = o.holding;
let i = o.instrument;
h.SoldDate = soldDate;
//Mark the holding as sold in the database
$.ajax({
type: "POST",
contentType: "application/json",
url: "SharePrices.aspx/SoldHolding",
dataType: "json",
data: JSON.stringify({ HoldingID: holdingID, SoldDate: soldDate, SoldCurrency: soldCurrency, SoldProceeds: soldProceeds }),
success: function (response) {
$("#shareRow" + i.DisplayOrder).html(createSharesTableRow(i));
createLongChart(i);
createMidChart(i);
createShortChart(i);
createSingleDayChart(i);
},
failure: function (response) { console.error("SoldHolding error: " + JSON.stringify(response)); }
});
let balance = soldProceeds;
let curr = Instruments.GetAccountCurrencyHolding(accountName, Instruments.GetSymbol(soldCurrency));
if (curr) {
balance += curr.NoUnits;
}
//Update the account cash value to reflect the sale
$.ajax({
type: "POST",
contentType: "application/json",
url: "SharePrices.aspx/SetAccountCashValue",
dataType: "json",
data: JSON.stringify({ AccountName: accountName, Currency: soldCurrency, Value: balance }),
success: function (response) {
console.info("SetAccountCashValue success: " + JSON.stringify(response));
let c = Instruments.GetSymbol(response.Symbol);
let ch = Instruments.GetAccountCurrencyHolding(response.AccountName, c);
if (ch) {
ch.NoUnits = response.NoUnits;
} else {
c.Holdings.push(response);
}
},
failure: function (response) { console.error("SetAccountCashValue error: " + JSON.stringify(response)); }
});
}
export function showModalDialog_FullScreenChart(Symbol) {
let i = Instruments.GetSymbol(Symbol);
let modal = document.getElementById("modalChart");
$("#modalChartTitle").html("Detailed Chart: " + i.InstrumentName + " (" + i.Symbol + ")");
$("#modalChart .close").click(function () { modal.style.display = "none"; });
$('#spnCompareInstruments').click(function () { compareInstruments(i) });
window.onclick = function (event) { // When the user clicks anywhere outside of the modal, close it
if (event.target == modal) {
modal.style.display = "none";
}
}
//$("#modalDialog .ok-button").click(function () { modal.style.display = "none"; });
$(document).on('keypress', function (event) { console.info("Keypress: " + event.keyCode.toString()); if (event.keyCode == 27 /*Escape key*/) modal.style.display = "none"; });
//Display the dialog
modal.style.display = "block";
let chartDiv = $("#divFullScreenChart");
let height = chartDiv.parent().height();
let width = chartDiv.parent().width();
chartDiv.height(height).width(width);
let startDate = i.DailyData[0].DT;
let endDate = new Date().getTime();
createFullScreenChart(i, startDate, endDate);
//createFullScreenComparisonChart(i, Instruments.GetSymbol('BTC-USD'), startDate, endDate);
}
function compareInstruments(Instrument1) {
function GetAccountsDropdown() {
let dd = '<select id="selectInstrument2"><option value="">Select 2nd instrument...</option>';
for (let x = 0; x < Instruments.Data.length; x++) {
let i = Instruments.Data[x];
if (i.Symbol != Instrument1.Symbol) {
dd += '<option value="' + i.Symbol + '">' + i.InstrumentName + ' (' + i.Symbol + ')</option>';
}
}
dd += '</select>';
return dd;
}
let modal = document.getElementById("modalDialog");
$("#modalTitle").html("Compare Instruments");
let content = '<div>Select instrument to compare against <strog>' + Instrument1.Symbol + '</strong><br/>' + GetAccountsDropdown() + '</div><br>' +
'<div><span id="compareInstrumentsOKButton" class="ok-button-disabled">OK</span></div>';
$("#modalContentDiv").html(content);
$("#modalDialog .close").click(function () { modal.style.display = "none"; });
window.onclick = function (event) { // When the user clicks anywhere outside of the modal, close it
if (event.target == modal) {
modal.style.display = "none";
}
}
$("#selectInstrument2").bind('input', function () {
$("#compareInstrumentsOKButton").removeClass("ok-button").addClass("ok-button-disabled").unbind();
let instrument2Symbol = $("#selectInstrument2").val();
if (instrument2Symbol != '' && instrument2Symbol != 'Select 2nd instrument...') {
$("#compareInstrumentsOKButton").removeClass("ok-button-disabled").addClass("ok-button").bind('click', function () {
$("#modalDialog").css("display", "none");
let Instrument2 = Instruments.GetSymbol(instrument2Symbol);
let startDate = Instrument1.DailyData[0].DT > Instrument2.DailyData[0].DT ? Instrument1.DailyData[0].DT : Instrument2.DailyData[0].DT;
let endDate = new Date().getTime();
createFullScreenComparisonChart(Instrument1, Instrument2, startDate, endDate);
});
}
});
//Display the dialog
modal.style.display = "block";
}
export function refreshHistoryChart() {
let chartContainer = $('#histDiv');
$.ajax({
type: "POST",
contentType: "application/json",
url: "SharePrices.aspx/GetTotalHoldingsHistory",
dataType: "json",
success: function (response) {
let seriesData = [];
for (let i = 0; i < response.length; i++) {
let d = response[i];
seriesData.push([d.HistoryDate, d.Open, d.High, d.Low, d.Close]);
}
chartContainer.html(response);
Highcharts.stockChart('histDiv', {
chart: {
backgroundColor: 'rgba(0,0,0,0)'/*,
spacingTop: 5,
spacingBottom: 0,
spacingLeft: 2,
spacingRight: 0*/
},
title: {
text: 'Total Holdings History',
style: { color: "#ffffff" }
},
series: [{
type: 'candlestick',
name: 'Total Holdings',
color: '#ff4141',
lineColor: '#ff4141',
upColor: '#07b200',
upLineColor: '#07b200',
data: seriesData/*,
lineWidth: 4*/
}],
xAxis: [{
lineColor: '#505050',
tickColor: '#505050',
labels: { style: { color: '#b0b0b0'/*, fontSize: 'x-small'*/ } }
}],
yAxis: [{
//title: { text: undefined },
gridLineColor: '#505050',
labels: { style: { color: '#b0b0b0'/*, fontSize: 'x-small'*/ } },
offset: 40
}],
rangeSelector: {
buttonTheme: {
fill: "#303030",
stroke: "",
"stroke-width": 3,
style: { border: "1px solid red", color: "#dddddd" }
}
},
plotOptions: {
ohlc: {
color: '#ff4141',
upColor: '#07b200'
}
}
});
},
failure: function (response) { console.error("GetTotalHoldingsHistory error: " + response.d) }
});
}
//$(document).ready(getInstruments());