diff --git a/app.js b/app.js new file mode 100644 index 0000000..fff880a --- /dev/null +++ b/app.js @@ -0,0 +1,47 @@ +var app = angular.module("app", ['ui.router']); + +app.config(function($stateProvider, $urlRouterProvider){ + $urlRouterProvider.otherwise('/index/portfolio'); + + $stateProvider.state('index', { + url: '/index', + views: { + 'index': { + templateUrl: 'js/templates/index.html', + controller: 'MainCtrl' + }, + 'historical': { + templateUrl: 'js/templates/historical.html', + controller: 'historyCtrl' + } + } + }) + .state("index.portfolio", { + url: '/portfolio', + views: { + 'display@': { + templateUrl: "js/templates/portfolio.html", + controller: "portfolioCtrl" + } + } + }) + .state("index.trade", { + url: '/trade/:symbol', + views: { + 'display@': { + templateUrl: "js/templates/trade.html", + controller: "tradeCtrl" + } + } + }) + + .state("index.transaction", { + url: '/transaction', + views: { + 'display@': { + templateUrl: "js/templates/transactions.html", + controller: "transactionCtrl" + } + } + }) +}) diff --git a/index.html b/index.html new file mode 100644 index 0000000..fd61985 --- /dev/null +++ b/index.html @@ -0,0 +1,58 @@ + + + + + Fideliguard + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/controllers/historyController.js b/js/controllers/historyController.js new file mode 100644 index 0000000..4750a54 --- /dev/null +++ b/js/controllers/historyController.js @@ -0,0 +1,70 @@ +app.controller("historyCtrl", ['$scope', '$filter', 'historicalStock', 'selectedDate', function($scope, $filter, historicalStock, selectedDate){ + $scope.stockData = historicalStock.getStockData(); + + $scope.sortCriteria = ""; + $scope.reverse = false; + + $scope.sortBy = function(criteria){ + var columns = { + 1: "Symbol", + 2: "Close", + 3: $scope.getYesterday, + 4: $scope.getLastMonth, + 5: $scope.getLast6Month, + } + if (!columns[criteria]) return; + + if (columns[criteria] == $scope.sortCriteria){ + $scope.reverse = !$scope.reverse; + } else { + $scope.sortCriteria = columns[criteria]; + } + } + + + + $scope.symbols = function() { + return $scope.stockData.reduce(function(result, current){ + if (result.indexOf(current.Symbol) == -1) { + result.push(current.Symbol); + } + return result; + }, []) + } + + $scope.selectedDate = selectedDate.getDate; + + $scope.getLast = function(offset, entry){ + var currentClosing = entry.Close; + var startingDate = new Date(entry.Date); + var targetDays = []; + targetDays.push(startingDate.setDate(startingDate.getDate() - offset)) + targetDays.push(startingDate.setDate(startingDate.getDate() - 1)) + targetDays.push(startingDate.setDate(startingDate.getDate() - 1)) + targetDays.push(startingDate.setDate(startingDate.getDate() - 1)) + targetDays.push(startingDate.setDate(startingDate.getDate() - 1)) + + var filteredData = $scope.stockData.filter(function(el){ return el.Symbol == entry.Symbol }) + var results = []; + targetDays.forEach(function(day) { + var result = $filter("dateFilter")(filteredData, day); + if (result.length > 0 && results.length == 0) { + results = result; + } + }); + + return (results.length < 1) ? "N/A" : currentClosing - results[0].Close + } + + $scope.getYesterday = function(entry){ + return $scope.getLast(0, entry); + } + + $scope.getLastMonth = function(entry){ + return $scope.getLast(29, entry); + } + + $scope.getLast6Month = function(entry){ + return $scope.getLast(179, entry); + } +}]); diff --git a/js/controllers/mainController.js b/js/controllers/mainController.js new file mode 100644 index 0000000..d44d34a --- /dev/null +++ b/js/controllers/mainController.js @@ -0,0 +1,7 @@ +app.controller("MainCtrl", ['$scope', 'selectedDate', function($scope, selectedDate){ + $scope.selectedDate = selectedDate.getDate; + $scope.currentDate = ""; + $scope.$watch("currentDate", function(){ + selectedDate.setDate($scope.currentDate); + }) +}]); diff --git a/js/controllers/portfolioController.js b/js/controllers/portfolioController.js new file mode 100644 index 0000000..ba0a672 --- /dev/null +++ b/js/controllers/portfolioController.js @@ -0,0 +1,61 @@ +app.controller("portfolioCtrl", ['$scope', + 'transactionService', + 'historicalStock', + 'selectedDate', + function($scope, transactionService, historicalStock, selectedDate){ + $scope.currentDate = selectedDate.getDate; + $scope.portfolio = transactionService.portfolio(selectedDate.getDate()) + $scope.money = transactionService.getMoney($scope.currentDate()) + + $scope.getPrice = historicalStock.getPrice; + $scope.yesterday = historicalStock.getYesterday; + $scope.lastMonth = historicalStock.getLastMonth; + $scope.last6 = historicalStock.getLast6Month; + + $scope.portfolioPrevious = function(offset){ + var now = new Date($scope.currentDate()) + now = new Date(now.setDate(now.getDate() - offset)); + + console.log(now); + if (now < Date.parse('1/1/2014')) return "N/A" + + var previousPortfolio = transactionService.portfolio(now); + + return transactionService.getMoney(now) + previousPortfolio.reduce(function(total, el){ + total += historicalStock.getPrice(now, el.symbol) * el.quantity; + return total; + }, 0) + } + + $scope.portfolioYesterday = function(){ + return $scope.portfolioPrevious(1); + + } + + $scope.portfolioLastMonth = function(){ + return $scope.portfolioPrevious(30); + } + + $scope.portfolioLast6Month = function(){ + return $scope.portfolioPrevious(180); + } + + $scope.stockValue = function(){ + return $scope.portfolio.reduce(function(total, el){ + total += historicalStock.getPrice($scope.currentDate(), el.symbol) * el.quantity; + return total; + }, 0) + } + + $scope.totalCost = function(){ + return $scope.portfolio.reduce(function(total, el){ + total += el.cost + return total; + }, 0) + } + + $scope.$watch('currentDate()', function(){ + $scope.portfolio = transactionService.portfolio($scope.currentDate()); + $scope.money = transactionService.getMoney($scope.currentDate()) + }); +}]); diff --git a/js/controllers/tradeController.js b/js/controllers/tradeController.js new file mode 100644 index 0000000..96c7011 --- /dev/null +++ b/js/controllers/tradeController.js @@ -0,0 +1,43 @@ +app.controller("tradeCtrl", ['$scope', '$stateParams', 'selectedDate', 'historicalStock', 'transactionService', + function($scope, $stateParams, selectedDate, historicalStock, transactionService){ + $scope.tradeData = {}; + $scope.currentMoney = transactionService.getMoney; + $scope.tradeData.symbol = $stateParams.symbol; + $scope.tradeData.date = selectedDate.getDate; + + $scope.displayMoney = function(){ + return $scope.currentMoney($scope.tradeData.date()); + } + + $scope.futureMoney = function(){ + return transactionService.getFutureMoney(); + } + + $scope.calcCost = function(){ + return $scope.calcPrice() * Number($scope.tradeData.quantity); + } + + $scope.calcPrice = function(){ + return historicalStock.getPrice(selectedDate.getDate(), $scope.tradeData.symbol); + } + + $scope.createTranscation = function() { + var tradeRecord = JSON.parse(JSON.stringify($scope.tradeData)); + tradeRecord.action = $scope.tradeData.action == "true" ? true : false + tradeRecord.date = $scope.tradeData.date(); + tradeRecord.cost = Number($scope.calcCost()); + tradeRecord.price = Number($scope.calcPrice()); + tradeRecord.quantity = Number(tradeRecord.quantity); + transactionService.addTransaction(tradeRecord); + } + + $scope.validBuy = function() { + return $scope.tradeData.action == "true" && transactionService.validateBuy($scope.tradeData.date(), $scope.calcCost()); + } + + $scope.validSell = function() { + return $scope.tradeData.action != "true" && transactionService.validateSale($scope.tradeData.symbol, + $scope.tradeData.date(), + $scope.tradeData.quantity); + } +}]); diff --git a/js/controllers/transactionController.js b/js/controllers/transactionController.js new file mode 100644 index 0000000..2d00a4c --- /dev/null +++ b/js/controllers/transactionController.js @@ -0,0 +1,24 @@ +app.controller("transactionCtrl", ['$scope', 'transactionService', function($scope, transactionService){ + $scope.transactions = transactionService.transactions; + + $scope.sortCriteria = ""; + $scope.reverse = false; + + $scope.sortBy = function(criteria){ + var columns = { + 1: "date", + 2: "symbol", + 3: "action", + 4: "quantity", + 5: "price" + } + if (!columns[criteria]) return; + + if (columns[criteria] == $scope.sortCriteria){ + $scope.reverse = !$scope.reverse; + } else { + $scope.sortCriteria = columns[criteria]; + $scope.reverse = false; + } + } +}]); diff --git a/js/directives/stateDropdown.html b/js/directives/stateDropdown.html new file mode 100644 index 0000000..13e2600 --- /dev/null +++ b/js/directives/stateDropdown.html @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/js/directives/stateDropdown.js b/js/directives/stateDropdown.js new file mode 100644 index 0000000..c6fc290 --- /dev/null +++ b/js/directives/stateDropdown.js @@ -0,0 +1,7 @@ +app.directive("stateDropdown", function(){ + return { + templateUrl: "js/directives/stateDropdown.html", + restrict: "E", + scope: {} + } +}) \ No newline at end of file diff --git a/js/filters/dateFilter.js b/js/filters/dateFilter.js new file mode 100644 index 0000000..cef64ea --- /dev/null +++ b/js/filters/dateFilter.js @@ -0,0 +1,39 @@ +app.filter("dateFilter", function(){ + return function(collection, date){ + return collection.filter(function(el) { + var chosenDate = new Date(el.Date) + targetDate = new Date(date) + chosenDate.setHours(0,0,0,0); + targetDate.setHours(0,0,0,0); + targetDate.setDate(targetDate.getDate() - 1); + return (chosenDate <= targetDate && chosenDate >= targetDate); + }); + }; +}); + +app.filter("beforeDateFilter", function(){ + return function(collection, date){ + return collection.filter(function(el) { + var chosenDate = new Date(el.date) + targetDate = new Date(date) + chosenDate.setHours(0,0,0,0); + targetDate.setHours(0,0,0,0); + targetDate.setDate(targetDate.getDate() - 1); + return (chosenDate <= targetDate); + }); + }; +}); + +app.filter("afterDateFilter", function(){ + return function(collection, date){ + return collection.filter(function(el) { + var chosenDate = new Date(el.date) + targetDate = new Date(date) + chosenDate.setHours(0,0,0,0); + targetDate.setHours(0,0,0,0); + targetDate.setDate(targetDate.getDate() - 1); + return !(chosenDate <= targetDate); + }); + }; +}); + diff --git a/js/filters/tableFilter.js b/js/filters/tableFilter.js new file mode 100644 index 0000000..63fb3dc --- /dev/null +++ b/js/filters/tableFilter.js @@ -0,0 +1,19 @@ +app.filter('tableFilter', function(orderByFilter){ + return function(collection, expression, reverse){ + if (typeof expression == 'string') { + if ( expression == "Close") { + var result = collection.sort(function(a, b){ + return Number(a.Close) - Number(b.Close); + }); + return reverse ? result.reverse() : result; + } else { + return orderByFilter(collection, expression, reverse) + } + } else { + var result = collection.sort(function(a, b){ + return expression(a) - expression(b); + }) + return reverse ? result.reverse() : result; + } + } +}) \ No newline at end of file diff --git a/js/services/historicalStock.js b/js/services/historicalStock.js new file mode 100644 index 0000000..0bf5909 --- /dev/null +++ b/js/services/historicalStock.js @@ -0,0 +1,70 @@ +app.factory('historicalStock', ['$http', '$filter', function($http, $filter){ + var _symbols = ["AAPL", "GOOG", "MSFT", "FB", "YHOO", "NFLX"]; + var stockData = []; + + _symbols.forEach(function(sym) { + _getSymData(sym, function(result){ + stockData.push.apply(stockData, result.data.query.results.quote) + }); + }) + + function getStockData() { + return stockData; + } + + function getPrice(date, symbol){ + var target = $filter("dateFilter")(stockData, date).filter(function(el){ return el.Symbol == symbol })[0]; + return target ? target.Close : null; + } + + function _getSymData(sym, callback) { + var url = 'http://query.yahooapis.com/v1/public/yql?q=%20select%20*%20from%20yahoo.finance.historicaldata%20where%20symbol%20=%20%22' + sym + '%22%20and%20startDate%20=%20%222014-01-01%22%20and%20endDate%20=%20%222014-12-31%22%20&format=json%20&diagnostics=true%20&env=store://datatables.org/alltableswithkeys%20&callback=' + return $http.get(url) + .then(callback) + } + + function getLast(offset, date, symbol){ + var currentClosing = getPrice(date, symbol); + var startingDate = new Date(date); + var targetDays = []; + targetDays.push(startingDate.setDate(startingDate.getDate() - offset - 1)) + targetDays.push(startingDate.setDate(startingDate.getDate() - 1)) + targetDays.push(startingDate.setDate(startingDate.getDate() - 1)) + targetDays.push(startingDate.setDate(startingDate.getDate() - 1)) + targetDays.push(startingDate.setDate(startingDate.getDate() - 1)) + + + var filteredData = stockData.filter(function(el){ return el.Symbol == symbol }) + var results = []; + targetDays.forEach(function(day) { + var result = $filter("dateFilter")(filteredData, day); + if (result.length > 0 && results.length == 0) { + results = result; + } + }); + + if (!currentClosing) return "N/A" + + return (results.length < 1) ? "N/A" : currentClosing - results[0].Close + } + + function getYesterday(date, symbol){ + return getLast(0, date, symbol); + } + + function getLastMonth(date, symbol){ + return getLast(29, date, symbol); + } + + function getLast6Month(date, symbol){ + return getLast(179, date, symbol); + } + + return { + getPrice: getPrice, + getStockData: getStockData, + getYesterday: getYesterday, + getLastMonth: getLastMonth, + getLast6Month: getLast6Month, + } +}]); diff --git a/js/services/selectedDate.js b/js/services/selectedDate.js new file mode 100644 index 0000000..650d225 --- /dev/null +++ b/js/services/selectedDate.js @@ -0,0 +1,19 @@ +app.factory('selectedDate', function(){ + var _currentDate; + + function getDate() { + return _currentDate; + }; + + function setDate(offset) { + var baseDate = new Date('01/01/2014'); + var startDate = new Date(baseDate); + startDate.setDate(startDate.getDate()+Number(offset)); + _currentDate = startDate; + }; + + return { + getDate: getDate, + setDate: setDate + } +}) diff --git a/js/services/transactionService.js b/js/services/transactionService.js new file mode 100644 index 0000000..6314ebc --- /dev/null +++ b/js/services/transactionService.js @@ -0,0 +1,171 @@ +app.factory('transactionService', ["$filter", function($filter){ + var transactions = [ + { + action: true, + cost: 5318.20023, + date: new Date('Fri Apr 04 2014 00:00:00 GMT-0500 (CDT)'), + price: 531.820023, + quantity: 10, + symbol: "AAPL" + }, + { + action: true, + cost: 44.95, + date: new Date('Fri Aug 20 2014 00:00:00 GMT-0500 (CDT)'), + price: 44.95, + quantity: 1, + symbol: "MSFT", + }, + { + action: true, + cost: 67835.727417, + date: new Date('Jan 23 2014 00:00:00 GMT-0500 (CDT)'), + price: 551.509979, + quantity: 123, + symbol: "AAPL", + }, + { + action: false, + cost: 66964.887171, + date: new Date('Feb 22 2014 00:00:00 GMT-0500 (CDT)'), + price: 544.429977, + quantity: 123, + symbol: "AAPL", + } + ] + + // Day bought + // Symbol + // Total amount bought + + // For the portfolio, the quantity of held shares and the cost + // basis for a given symbol are required from this service. + + + function addTransaction(tradeData){ + transactions.push(tradeData); + } + + function getMoney(date){ + var startingMoney = 100000; + var spentMoney = calculateSpentMoney(date); + var profits = calculateProfits(date); + + return startingMoney - spentMoney + profits; + } + + function getFutureMoney(){ + return getMoney(new Date('12/31/2014')) + } + + function calculateSpentMoney(date){ + return $filter("beforeDateFilter")(transactions, date).filter(function (el){ + return el.action + }).reduce(function (total, el){ + return total += el.cost; + }, 0) + } + + function calculateProfits(date){ + return $filter("beforeDateFilter")(transactions, date).filter(function (el){ + return !el.action + }).reduce(function (total, el){ + return total += el.cost; + }, 0) + } + + // To make a purchase you both need sufficient money, + // and this purchase cannot cause a future purchase to put you in the red. + function validateBuy(date, cost){ + // Calculate how much money we'd have at the given date if we bought + var currentMoney = getMoney(date) - cost; + + // If we have insufficient funds right now, return false. + if (currentMoney < 0) return false; + + // Get a list of all transactions that happen on/after this date. + // Sort them by date. + var futureTransactions = $filter("afterDateFilter")(transactions, date).sort(function(a, b){ + return new Date(a.date) - new Date(b.date); + }); + + // Return whether every transaction applied in order never puts us in red. + return futureTransactions.every(function (el){ + currentMoney += el.action ? -el.cost : el.cost + return (currentMoney >= 0); + }) + } + + // Perform the same validation as buying but checking the integrity + // of the share count instead. + function validateSale(symbol, date, quantity){ + var currentShares = countShares(symbol, date) - quantity; + + if (currentShares < 0 || isNaN(quantity)) return false; + + var futureTransactions = $filter("afterDateFilter")(transactions, date).sort(function(a, b){ + return new Date(a.date) - new Date(b.date); + }); + + return futureTransactions.every(function (el){ + currentShares += el.action ? el.quantity : -el.quantity + return (currentShares >= 0); + }) + } + + // Finds the first sale of a given symbol on/after the given date. + function countFutureShares(symbol){ + return countShares(symbol, new Date('12/31/2014')) + } + + // Calculates the value of shares held using transactions before/on + // given date. + function countShares(symbol, date) { + return $filter("beforeDateFilter")(transactions, date) + .reduce(function(total, ele){ + if (ele.symbol == symbol && ele.action) { + total += ele.quantity; + } else if (ele.symbol == symbol) { + total -= ele.quantity; + } + return total; + }, 0) + } + + function portfolio(date) { + var portfolio = $filter("beforeDateFilter")(transactions, date).reduce(function (result, el){ + if (!result[el.symbol]) result[el.symbol] = { quantity: 0, cost: 0.00}; + if (el.action){ + result[el.symbol].quantity += el.quantity; + result[el.symbol].cost -= el.cost; + } else { + result[el.symbol].quantity -= el.quantity; + result[el.symbol].cost += el.cost; + } + return result; + }, {}) + + var filteredPortfolio = []; + + for (symbol in portfolio){ + // if (portfolio[symbol].quantity > 0){ + var portfolioRow = portfolio[symbol] + portfolioRow.symbol = symbol; + filteredPortfolio.push(portfolioRow) + // } + } + + return filteredPortfolio; + } + + return { + getMoney: getMoney, + getFutureMoney: getFutureMoney, + transactions: transactions, + addTransaction: addTransaction, + validateSale: validateSale, + validateBuy: validateBuy, + portfolio: portfolio, + } + +}]); diff --git a/js/templates/historical.html b/js/templates/historical.html new file mode 100644 index 0000000..28e4762 --- /dev/null +++ b/js/templates/historical.html @@ -0,0 +1,32 @@ +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + +
SymbolPrice1d30d6monthTrade?
{{ dataRow.Symbol }}{{ dataRow.Close | currency }}{{ getYesterday(dataRow) | currency }}{{ getLastMonth(dataRow) | currency }}{{ getLast6Month(dataRow) | currency }}trade
+
+
diff --git a/js/templates/index.html b/js/templates/index.html new file mode 100644 index 0000000..ef4d965 --- /dev/null +++ b/js/templates/index.html @@ -0,0 +1,6 @@ +
+
+ 1/1/2014 12/31/2014 + {{ selectedDate() | date }} +
+
diff --git a/js/templates/portfolio.html b/js/templates/portfolio.html new file mode 100644 index 0000000..5a00c99 --- /dev/null +++ b/js/templates/portfolio.html @@ -0,0 +1,58 @@ +
+
+

Portfolio

+
+
+ + + + + + + + + + + + + + + + + +
Cost BasisCurrent ValueProfit1d30d6month
{{ money - totalCost() | currency }}{{ money + stockValue() | currency }}{{ stockValue() + totalCost() | currency }}{{ money + stockValue() - portfolioYesterday() | currency }}{{ money + stockValue() - portfolioLastMonth() | currency }}{{ money + stockValue() - portfolioLast6Month() | currency }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SymbolQuantityCost BasisCurrent ValueProfitsCurrent PriceTrade?
Cash {{ money | currency }} {{ money | currency }} {{ money | currency }}
{{ row.symbol }}{{ row.quantity }}{{ row.cost | currency }}{{ row.quantity * getPrice(currentDate(), row.symbol) | currency }}{{ row.quantity * getPrice(currentDate(), row.symbol) + row.cost | currency }}{{ getPrice(currentDate(), row.symbol) | currency }}trade
+
+ +
+ diff --git a/js/templates/trade.html b/js/templates/trade.html new file mode 100644 index 0000000..dd0363e --- /dev/null +++ b/js/templates/trade.html @@ -0,0 +1,42 @@ +
+
+
+

Trade

+
+
+ Symbol: +
+
+ Buy/Sell: +
+
+ Quantity: +
+
+ Date: +
+
+ Price: +
+
+ Cost: +
+ +

Cash Available: {{ displayMoney() | currency }}

+

Final Cash Value at 12/31/2014 :{{ futureMoney() | currency }}

+ + Order Status: + + + + +
+
+
+ + + +
diff --git a/js/templates/transactions.html b/js/templates/transactions.html new file mode 100644 index 0000000..893eca8 --- /dev/null +++ b/js/templates/transactions.html @@ -0,0 +1,28 @@ +
+
+

Transactions

+
+
+ + + + + + + + + + + + + + + + + +
DateSymbolTypeQuantityPrice
{{ transaction.date | date }}{{ transaction.symbol }}{{ transaction.action ? "Buy" : "Sell" }}{{ transaction.quantity }}{{ transaction.price }}
+
+
+Sorting By: {{ sortCriteria? sortCriteria : "Nothing" }}, Descending {{ reverse }} + +