analyze-dca
Version:
Analyzes past stock values using dollar cost averaging investment strategy
217 lines (180 loc) • 7.36 kB
text/typescript
// Import necessary dependencies -> stock APIs
var yahooFinance = require('yahoo-finance');
// Special type for period (how often the user is buying the stock)
type Period = 'd' | 'w' | 'm' | 'y';
// Input argument (JSON obj) for the dollar cost averaging function
interface dca_request {
amountInvested: number // amount that the user was looking forward to invest
stockSymbol: string // TSLA, AMZN, GOOGL
period: Period // day/week/month/year
numPeriods: number // how many periods, past # days/weeks/months/years
}
// Main JSON output format
interface results {
sharesOwnedWithDCA: number
sharesOwnedWithDirect: number
directProfit: number
dcaProfit: number
sharesData: sharesInfo
}
// Extra output object format
interface sharesInfo {
date: Date,
open: number;
close: number;
high: number;
low: number;
symbol: string;
}
// Core function that calculates gains/loses from stocks using DCA strategy
function analyze_dca(req: dca_request) {
var curr: Date = new Date();
var startDate: Date = getDatePoints(req.period, req.numPeriods, curr);
var diffInDays = (curr.getTime() - startDate.getTime()) / (1000 * 3600 * 24);
var endpoints = { // range of the period
start: startDate,
end: curr
}
// Using the yahooFinance api
var result = yahooFinance.historical({
symbol: req.stockSymbol,
from: endpoints.start.toISOString().substring(0,10),
to: endpoints.end.toISOString().substring(0,10)
}).then(function (quotes) {
if (quotes.length == 0) { // Handle error state
return "No Data, Invalid input parameters (ERROR)";
}
// Adjust quotes to contain equal number of data to num_periods if period == day
if (req.period == 'd') {
quotes = quotes.slice(0, req.numPeriods);
}
// quotes contains all requested shares data
var amtPerPeriod: number = req.amountInvested / req.numPeriods; // for Dollar Cost Averaging strategy
var sharesData: sharesInfo[] = [];
var sharesOwnedWithDCA: number = getSharesOwnedDCA(quotes, amtPerPeriod, req.period, req.numPeriods, sharesData);
var sharesOwnedWithDirect: number = getSharesOwnedDirect(req.amountInvested, sharesData[sharesData.length - 1].open);
var conclusion = { // Final result object that is returned
sharesOwnedWithDCA: sharesOwnedWithDCA,
sharesOwnedWithDirect: sharesOwnedWithDirect,
directProfit: getGainOrLoss(req.amountInvested, quotes[0].open, sharesOwnedWithDirect),
dcaProfit: getGainOrLoss(req.amountInvested, quotes[0].open, sharesOwnedWithDCA),
sharesData: sharesData
}
return conclusion;
})
return result; // Returning a promise
}
// Returns the start time (ISO) when stocks are first purchased
function getDatePoints(period: Period, numPeriods: number, curr: Date) {
var start: Date = new Date();
switch (period) {
case 'd':
var actualDays = Math.ceil(numPeriods / 5) + 1
start.setDate(curr.getDate() - (actualDays * 7));
break;
case 'w':
start.setDate(curr.getDate() - (numPeriods * 7));
start.setDate(start.getDate() - 1);
break;
case 'm':
start.setMonth(curr.getMonth() - numPeriods);
start.setDate(start.getDate() - 1);
break;
case 'y':
start.setFullYear(curr.getFullYear() - (numPeriods - 1));
start.setDate(start.getDate() - 1);
break;
default:
Promise.reject(new Error("Invalid period parameter"))
}
// Adjust for startDate that falls on a weekend to the last time market was open
getPreviousWeekday(start);
return start;
}
// Calculate the number of shares owned by buying every period
function getSharesOwnedDCA(quotes, amount: number, period: Period, numPeriods: number, sharesData: Array<sharesInfo>) {
var totalSharesBought: number = 0;
// Offset in days for quotes array traversal
var weekOffset: number = 5;
var monthOffset: number = 23;
var yearOffset: number = 257;
var tempDate = new Date(quotes[0].date) // starting backwards from current date (in terms of market days)
// Buy shares with a set amount every period
for (var i = 0; i < quotes.length; ) {
var j = i;
// array access, should mostly be constant O(#) time
while (Date.parse(quotes[j].date) < Date.parse(tempDate.toISOString())) {
j--;
}
// Calculate # of stocks you can buy with the amount & add it to the cumulative sum
totalSharesBought += getSharesOwnedDirect(amount, quotes[j].open);
// Store the correct data (open, high, low)
sharesData.push({
date: quotes[j].date,
open: quotes[j].open,
close: quotes[j].close,
high: quotes[j].high,
low: quotes[j].low,
symbol: quotes[j].symbol
})
// Update tempDate for next iteration
switch (period) {
case 'd':
i += 1;
if (i < quotes.length) {
tempDate = new Date(quotes[i].date.getDate());
}
break;
case 'w':
i += weekOffset;
tempDate.setDate(tempDate.getDate() - 7);
break;
case 'm':
i += monthOffset;
tempDate.setMonth(tempDate.getMonth() - 1)
break;
case 'y':
i += yearOffset;
tempDate.setFullYear(tempDate.getFullYear() - 1);
break;
}
}
// For any extra leftover date missed out due to miscalculations
if (sharesData.length != numPeriods) {
var k = quotes.length - 1;
while (Date.parse(quotes[k].date) < Date.parse(tempDate.toISOString())) {
k--;
}
totalSharesBought += getSharesOwnedDirect(amount, quotes[j].open); // for cumulative sum
sharesData.push({ // add this last piece of data
date: quotes[k].date,
open: quotes[k].open,
close: quotes[k].close,
high: quotes[k].high,
low: quotes[k].low,
symbol: quotes[k].symbol
})
}
return totalSharesBought;
}
// Returns the number of stocks owned using direct investment
function getSharesOwnedDirect(amount: number, price: number) {
return amount/price;
}
// Returns the value that was gained/losed from buying the stock at old price & selling it at new price
function getGainOrLoss(amount: number, currPrice: number, shares: number) {
return (shares * currPrice) - amount;
}
// Adjust the date to the next weekday if this date is a weekend
function getNextWeekday(date: Date) {
if ([6, 7].includes(date.getDay())) {
date.setDate(date.getDate() + (8 - date.getDay()));
}
}
// Adjust the date to the previous weekday if this date is a weekend
function getPreviousWeekday(date: Date) {
if ([6, 7].includes(date.getDay())) {
date.setDate(date.getDate() - (date.getDay() - 5));
}
}
module.exports = analyze_dca; // exports the package with this namespace